Nested sidebar nav, banner inherit label fix, editor UX polish
- Sidebar renders the section's whole subtree as nested lists (third level and deeper, article-list-style markers); the one-item sidebar rule yields when that item has children of its own; only the viewed page is highlighted (navbar keeps ancestor highlighting); first_leaf skips unpublished branches - Banner design selector: inherit option names the resolved design and its true source (backend banner_design_source excludes the node's own setting; doc payload carries banner_design_inherited) - Article editor: fenced code block on empty lines, table size picker popup, borderless save icon disabled by saturate(0) alone - While editing, window scroll is locked and only #main scrolls; the panel exactly fills the available window height; editor scroll drives the article
This commit is contained in:
@@ -107,15 +107,20 @@ not for the public pages. See `docs/design-principles.md` for the design.
|
|||||||
placeholders (`Title`, `Brand`, `Banner`, `Nav`, `Sidebar`, `Main`), nav
|
placeholders (`Title`, `Brand`, `Banner`, `Nav`, `Sidebar`, `Main`), nav
|
||||||
rendering straight from the `Data.menu` tree (siblings sorted by
|
rendering straight from the `Data.menu` tree (siblings sorted by
|
||||||
`Node.order`; nav links to content-less labels point at their first
|
`Node.order`; nav links to content-less labels point at their first
|
||||||
child via `first_leaf`), and page/404 rendering. If the markdown contains its own h1, the page title
|
child via `first_leaf`, the first published descendant with content),
|
||||||
|
and page/404 rendering. If the markdown contains its own h1, the page title
|
||||||
is NOT rendered as an additional h1 (it still supplies <title> and nav
|
is NOT rendered as an additional h1 (it still supplies <title> and nav
|
||||||
labels). The navbar holds
|
labels). The navbar holds
|
||||||
top-level items only; the current section's subitems go to a left
|
top-level items only; the current section's subitems go to a left
|
||||||
`#sidebar`, which is rendered when the section offers at least two
|
`#sidebar` as a nested list (the section's direct children plain,
|
||||||
|
deeper levels indented with article-list-style markers), which is
|
||||||
|
rendered when the section offers at least two
|
||||||
published items, or exactly one while viewing anything other than that
|
published items, or exactly one while viewing anything other than that
|
||||||
only page — the section index, a 404, a grandchild (so those pages can
|
only page — the section index, a 404, a grandchild (so those pages can
|
||||||
reach the child); no aside element at all on the front page, leaf
|
reach the child), and also on that only page itself when it has
|
||||||
pages and the sole page of a one-page section. Also,
|
published children of its own; no aside element at all on the front
|
||||||
|
page, leaf
|
||||||
|
pages and the sole childless page of a one-page section. Also,
|
||||||
category labels are nodes without content — None *or* empty markdown —
|
category labels are nodes without content — None *or* empty markdown —
|
||||||
and their nav links point at their first child page. Dynamic regions have stable ids
|
and their nav links point at their first child page. Dynamic regions have stable ids
|
||||||
(`#page-banner`, `#nav`, `#sidebar`, `#main`) for fetch-navigation swaps
|
(`#page-banner`, `#nav`, `#sidebar`, `#main`) for fetch-navigation swaps
|
||||||
|
|||||||
@@ -354,6 +354,22 @@ body.editing #main {
|
|||||||
line-height: 1.0;
|
line-height: 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Third level (and deeper): nested like the first level of article lists —
|
||||||
|
slight indent, emoji marker — while the sidebar's own top level (the
|
||||||
|
section's direct children) stays plain, without indicators or indent. */
|
||||||
|
#sidebar ul ul {
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
padding-inline-start: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar ul ul li::before {
|
||||||
|
content: "🔹";
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: -1.3em;
|
||||||
|
width: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
#sidebar a {
|
#sidebar a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|||||||
+39
-11
@@ -188,13 +188,19 @@ def _title(slug: str, node: Node) -> str:
|
|||||||
return node.title or prettify(slug) or "Home"
|
return node.title or prettify(slug) or "Home"
|
||||||
|
|
||||||
|
|
||||||
def _nav_link(doc, menu: dict[str, Node], node: Node, path: str, current: str) -> None:
|
def _nav_link(
|
||||||
|
doc, menu: dict[str, Node], node: Node, path: str, current: str,
|
||||||
|
ancestors_current: bool = True,
|
||||||
|
) -> None:
|
||||||
"""Render one <li> linking the node. Category labels (no content of
|
"""Render one <li> linking the node. Category labels (no content of
|
||||||
their own — None, or empty markdown as left by the site editor's
|
their own — None, or empty markdown as left by the site editor's
|
||||||
page creation) link straight to their first child page, so normal
|
page creation) link straight to their first child page, so normal
|
||||||
navigation bypasses the placeholder/empty page at their own URL."""
|
navigation bypasses the placeholder/empty page at their own URL."""
|
||||||
# A top-level item is current also when viewing any of its subpages.
|
# The navbar highlights a top-level item also when viewing any of its
|
||||||
is_current = current == path or (path and current.startswith(f"{path}/"))
|
# subpages; the sidebar highlights only the actually viewed page.
|
||||||
|
is_current = current == path or (
|
||||||
|
ancestors_current and path and current.startswith(f"{path}/")
|
||||||
|
)
|
||||||
href = f"/{path}"
|
href = f"/{path}"
|
||||||
if not node.content and (leaf := first_leaf(menu, path)) is not None:
|
if not node.content and (leaf := first_leaf(menu, path)) is not None:
|
||||||
href = f"/{leaf}"
|
href = f"/{leaf}"
|
||||||
@@ -223,14 +229,18 @@ def nav_html(menu: dict[str, Node], current: str) -> HTML:
|
|||||||
def sidebar_html(menu: dict[str, Node], current: str) -> HTML:
|
def sidebar_html(menu: dict[str, Node], current: str) -> HTML:
|
||||||
"""Render the #sidebar element for the current path (empty when none).
|
"""Render the #sidebar element for the current path (empty when none).
|
||||||
|
|
||||||
The sidebar is the current main level section's sub-navigation, so it
|
The sidebar is the current main level section's sub-navigation: the
|
||||||
|
section's direct children as the top list level, with each item's own
|
||||||
|
published children nested under it (third level and deeper), so it
|
||||||
exists only when there is something to navigate: the section must
|
exists only when there is something to navigate: the section must
|
||||||
offer at least two published items, or exactly one while viewing
|
offer at least two published items, or exactly one while viewing
|
||||||
anything else than that only page — the section index, a 404, a
|
anything else than that only page — the section index, a 404, a
|
||||||
grandchild (otherwise those pages offer no way to reach the child).
|
grandchild (otherwise those pages offer no way to reach the child) —
|
||||||
The front page, leaf pages, the sole page of a one-page section and
|
and viewing that only page itself still shows the sidebar when the
|
||||||
childless sections get no aside element at all (rather than an empty
|
page has published children of its own to reach.
|
||||||
or useless one-item box).
|
The front page, leaf pages, the sole childless page of a one-page
|
||||||
|
section and childless sections get no aside element at all (rather
|
||||||
|
than an empty or useless one-item box).
|
||||||
"""
|
"""
|
||||||
if not current:
|
if not current:
|
||||||
return HTML("")
|
return HTML("")
|
||||||
@@ -239,15 +249,31 @@ def sidebar_html(menu: dict[str, Node], current: str) -> HTML:
|
|||||||
if node is None:
|
if node is None:
|
||||||
return HTML("")
|
return HTML("")
|
||||||
items = [(s, c) for s, c in sorted_nodes(node.children) if c.published]
|
items = [(s, c) for s, c in sorted_nodes(node.children) if c.published]
|
||||||
if not items or (len(items) == 1 and current == f"{section}/{items[0][0]}"):
|
if not items:
|
||||||
return HTML("")
|
return HTML("")
|
||||||
|
if len(items) == 1 and current == f"{section}/{items[0][0]}":
|
||||||
|
# Viewing the only item: useless unless it has children to reach.
|
||||||
|
if not any(c.published for c in items[0][1].children.values()):
|
||||||
|
return HTML("")
|
||||||
nav = E.ul
|
nav = E.ul
|
||||||
with nav:
|
with nav:
|
||||||
for slug, child in items:
|
for slug, child in items:
|
||||||
_nav_link(nav, menu, child, f"{section}/{slug}", current)
|
_sidebar_item(nav, menu, child, f"{section}/{slug}", current)
|
||||||
return HTML(str(E.aside(nav, id="sidebar")))
|
return HTML(str(E.aside(nav, id="sidebar")))
|
||||||
|
|
||||||
|
|
||||||
|
def _sidebar_item(doc, menu: dict[str, Node], node: Node, path: str, current: str) -> None:
|
||||||
|
"""One sidebar <li>: the node link, with its published children as a
|
||||||
|
nested list (third level and deeper, recursively)."""
|
||||||
|
_nav_link(doc, menu, node, path, current, ancestors_current=False)
|
||||||
|
sub = [(s, c) for s, c in sorted_nodes(node.children) if c.published]
|
||||||
|
if sub:
|
||||||
|
# doc.li.a(...) above left the <li> open for nesting.
|
||||||
|
with doc.ul:
|
||||||
|
for slug, child in sub:
|
||||||
|
_sidebar_item(doc, menu, child, f"{path}/{slug}", current)
|
||||||
|
|
||||||
|
|
||||||
def first_leaf(menu: dict[str, Node], path: str) -> str | None:
|
def first_leaf(menu: dict[str, Node], path: str) -> str | None:
|
||||||
"""First published descendant page (content set) in menu order.
|
"""First published descendant page (content set) in menu order.
|
||||||
|
|
||||||
@@ -261,8 +287,10 @@ def first_leaf(menu: dict[str, Node], path: str) -> str | None:
|
|||||||
|
|
||||||
def _first_leaf(node: Node, path: str) -> str | None:
|
def _first_leaf(node: Node, path: str) -> str | None:
|
||||||
for slug, child in sorted_nodes(node.children):
|
for slug, child in sorted_nodes(node.children):
|
||||||
|
if not child.published:
|
||||||
|
continue
|
||||||
cpath = f"{path}/{slug}" if path else slug
|
cpath = f"{path}/{slug}" if path else slug
|
||||||
if child.published and child.content:
|
if child.content:
|
||||||
return cpath
|
return cpath
|
||||||
if (leaf := _first_leaf(child, cpath)) is not None:
|
if (leaf := _first_leaf(child, cpath)) is not None:
|
||||||
return leaf
|
return leaf
|
||||||
|
|||||||
Reference in New Issue
Block a user