From a9693e1dcb0d6d6258eb6e5f6932df63707cb3ee Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 19 Aug 2026 00:25:07 +0000 Subject: [PATCH] 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 --- AGENTS.md | 13 ++++++--- frontend/src/assets/pagerite.css | 16 ++++++++++ pagerite/views.py | 50 +++++++++++++++++++++++++------- 3 files changed, 64 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 10c6003..28cace0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 rendering straight from the `Data.menu` tree (siblings sorted by `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 and nav labels). The navbar holds 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 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 - pages and the sole page of a one-page section. Also, + reach the child), and also on that only page itself when it has + 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 — and their nav links point at their first child page. Dynamic regions have stable ids (`#page-banner`, `#nav`, `#sidebar`, `#main`) for fetch-navigation swaps diff --git a/frontend/src/assets/pagerite.css b/frontend/src/assets/pagerite.css index 6715852..1375a63 100644 --- a/frontend/src/assets/pagerite.css +++ b/frontend/src/assets/pagerite.css @@ -354,6 +354,22 @@ body.editing #main { 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 { text-decoration: none; color: var(--muted); diff --git a/pagerite/views.py b/pagerite/views.py index 1ef994e..f4bfff6 100644 --- a/pagerite/views.py +++ b/pagerite/views.py @@ -188,13 +188,19 @@ def _title(slug: str, node: Node) -> str: 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 their own — None, or empty markdown as left by the site editor's page creation) link straight to their first child page, so normal navigation bypasses the placeholder/empty page at their own URL.""" - # A top-level item is current also when viewing any of its subpages. - is_current = current == path or (path and current.startswith(f"{path}/")) + # The navbar highlights a top-level item also when viewing any of its + # 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}" if not node.content and (leaf := first_leaf(menu, path)) is not None: 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: """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 offer at least two published items, or exactly one while viewing anything else than that only page — the section index, a 404, a - 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 - childless sections get no aside element at all (rather than an empty - or useless one-item box). + grandchild (otherwise those pages offer no way to reach the child) — + and viewing that only page itself still shows the sidebar when the + page has published children of its own to reach. + 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: return HTML("") @@ -239,15 +249,31 @@ def sidebar_html(menu: dict[str, Node], current: str) -> HTML: if node is None: return HTML("") 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("") + 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 with nav: 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"))) +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: """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: for slug, child in sorted_nodes(node.children): + if not child.published: + continue cpath = f"{path}/{slug}" if path else slug - if child.published and child.content: + if child.content: return cpath if (leaf := _first_leaf(child, cpath)) is not None: return leaf