From 2784cad6e401638534c9d038e3a46901b6a4d9c6 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sat, 8 Aug 2026 00:42:39 -0500 Subject: [PATCH] =?UTF-8?q?docs(website):=20phase=2010=20=E2=80=94=20publi?= =?UTF-8?q?c=20nav=20sections=20and=20added=20links,=20and=20the=20=C2=A77?= =?UTF-8?q?=20amendment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the capability asked for before the edge -> main cutover: dropdown sections in the public header, with the coded entries organised into them and admin-authored links alongside. §7 is amended rather than quietly contradicted. It said the override layer "cannot introduce a `to` that is not already in the hardcoded NAV array"; that remains true of every CODED entry, and the restated constraint spells out what an added link may be — a same-origin path, carrying no gate of its own, advertising a route rather than granting one — plus why the property is structural: coded entries live in a map keyed by routes the base array declares, and everything that can name an arbitrary path lives in `links`, where the rule is applied. §6.4 gains the { items, sections, links } wrapper, including the two properties worth knowing: a bare map still reads as the items map, and a nav with no sections still stores one. §9 gains three acceptance criteria — the empty dropdown does not render, an added link cannot leave the origin, and deleting a section returns its entries to the top level rather than removing them. "Phase 10 as landed" records why the Public tab needed its own tree editor, why moving between containers stayed a dropdown rather than a cross-container drag, why the menu opens on click and its trigger is not a link, and the palette-vs-full-nav bug this surfaced in the phase 6-8 save path. Co-Authored-By: Claude --- website/BACKEND_DESIGN.md | 13 ++++ website/THEMING_AND_NAV.md | 132 ++++++++++++++++++++++++++++++++++--- 2 files changed, 137 insertions(+), 8 deletions(-) diff --git a/website/BACKEND_DESIGN.md b/website/BACKEND_DESIGN.md index 3b1654f..7328a98 100644 --- a/website/BACKEND_DESIGN.md +++ b/website/BACKEND_DESIGN.md @@ -301,6 +301,19 @@ accepted (the editor sends it mid-edit) but never stored, so hiding stays subtractive. `hidden` on `/admin/navigation` is dropped for `nav_admin`, because that screen is the only UI that can un-hide anything. +**`nav_public` may also carry dropdown sections and admin-authored links**, as +`{ items, sections, links }` — a bare map still reads as `items`, and a nav with +no sections still stores one. A **section** has a label and a position and no +route at all: it only opens, so it adds no reachable surface. A **link** is the +one place a path may be named that the code does not declare, and is therefore +the one place the path rule applies: same-origin only, no scheme and no +protocol-relative `//host`. A link carries no gate of its own and needs none — +the page behind it enforces its own access, so an added link advertises a route +and never grants one. Coded entries stay in `items`, keyed by a route the base +array must declare, which is what keeps "an override cannot introduce a route" +structurally true. Sections and links are dropped for `nav_admin` / `nav_player`, +whose layouts cannot render them. + **`theme_visual` is resolved server-side, not shipped raw to the browser.** `utils/themeResolve.js` layers `:root` ← preset ← custom, field by field, into the CSS custom properties `getPublic()` returns as `theme`; the SPA's only job diff --git a/website/THEMING_AND_NAV.md b/website/THEMING_AND_NAV.md index d782c78..3611d54 100644 --- a/website/THEMING_AND_NAV.md +++ b/website/THEMING_AND_NAV.md @@ -455,27 +455,86 @@ Any field absent for a given `to` falls back to the code default — label from honored**, so removing a route in code can never leave a dangling override that does something unexpected. +**`nav_public` may also be a wrapper** (Phase 10), because the public header is +the one nav an admin can restructure rather than only reorder: + +```json +{ + "items": { "/site/champs": { "order": 0, "section": "sec_a1b2" } }, + "sections": [ { "id": "sec_a1b2", "label": "The World", "order": 4 } ], + "links": [ { "id": "lnk_c3d4", "label": "Player Guide", + "to": "/wiki/new-player-guide", "order": 1, "section": "sec_a1b2" } ] +} +``` + +- **A bare map is still read as the items map.** Every item key is a path + starting with `/`, so it can never collide with the literal key `items` — the + detection is unambiguous, and a nav with no sections still *stores* the bare + map, so this feature changed nothing for one that does not use it. +- `nav_admin` / `nav_player` keep the bare map; `sections` and `links` are + dropped for them, since neither layout can render an admin-created section. +- Top-level order is one number line shared by ungrouped entries **and + sections**; within a section, by its members. An admin-created entity with no + stored order appends after the coded ones rather than jumping to the front. +- **One level only.** No menu inside a menu. +- An `items[].section` or `links[].section` naming no declared section falls back + to the top level, mirroring the "group must name an existing title" rule. + ## 7. Navigation: hard constraint -The override system can **only** affect `label`, `order`, `hidden`, and — admin -nav only — `group` (which *existing* titled section an item sits under). +> **Amended in Phase 10.** This section originally said the override layer +> "cannot introduce a `to` that is not already in the corresponding hardcoded +> `NAV` array". That is still true of every **coded** entry, but the public +> header now also lets an admin add links of their own, so the constraint is +> restated below in the narrower form that survives. Nothing about the *gates* +> changed. + +The override system can affect a **coded** entry's `label`, `order`, `hidden`, +and which container it sits in — `group` on the admin nav (an *existing* titled +section) or `section` on the public header (an admin-created dropdown). It **cannot**: -- introduce a `to` that is not already in the corresponding hardcoded `NAV` array; -- change or remove an item's `roles` (admin nav) or `feature` (public nav) gate; -- un-hide an item for a viewer whose role or feature check would otherwise fail. +- change a coded entry's `to`, or introduce a new one in its place; +- change or remove an entry's `roles` (admin nav) or `feature` (public nav) gate; +- un-hide an entry for a viewer whose role or feature check would otherwise fail. + +**The public header may additionally carry admin-created `sections` and +admin-authored `links`** (§6.4, §7.2). This is a genuine widening and is worth +stating plainly: + +- A **section** is a container with a label and a position. It has no `to` and is + never itself a link — it only opens — so it adds no reachable surface at all. +- A **link** is the one thing an admin may add to a nav, and the only place a path + is not required to already exist in code. It is restricted to a **same-origin + path**: no scheme, no protocol-relative `//host`, no whitespace or quotes. The + nav is not a place to send visitors to an origin the operator does not control. +- A link carries **no `roles` or `feature` of its own, and needs none**: the page + behind it enforces its own access, so a link to somewhere the viewer cannot + reach behaves exactly as typing that address would. Adding a link advertises a + route; it never grants one. + +The property this rests on is structural rather than a check someone has to +remember: coded entries live in an `items` map whose keys **must** be routes the +base array declares, so that map can never introduce a route, while everything +that *can* name an arbitrary path lives in `links`, where the path rule is +applied on both the write and the read path. The existing filters in -[`SiteHeader.jsx:43`](../../website/client/src/components/SiteHeader.jsx) and -[`AdminLayout.jsx:155-164`](../../website/client/src/routes/admin/AdminLayout.jsx) +[`SiteHeader.jsx`](../../website/client/src/components/SiteHeader.jsx) and +[`AdminLayout.jsx`](../../website/client/src/routes/admin/AdminLayout.jsx) run **after** the override merge, unchanged, and remain the actual security boundary. The override layer is presentation-only. This is the same "server-enforced gate, client-side is only about not advertising a dead end" principle already documented in `SiteHeader.jsx`'s comments, and this feature must not weaken it. -Two existing behaviors the merge must not disturb: +Three existing behaviors the merge must not disturb: + +- **Empty dropdowns.** A section whose every entry is filtered out by a shard + feature must not render at all — a menu that opens onto nothing is worse than + no menu. `pruneNav` applies the gate inside a section and then drops one it + leaves empty. - **Moderator confinement.** `AdminLayout` restricts moderators to `MOD_PATHS` and redirects them out of anything else. Overrides apply before that filter, so a @@ -542,6 +601,7 @@ today until the admin acts. | **7 — Nav builder UI** ✅ | `NavEditor.jsx` with `@dnd-kit` (new dependency), **Public tab only** | | **8 — Admin + Player nav** ✅ | Wire the remaining two layouts, add the remaining two tabs, once the public pattern is validated in use | | **9 — Palette-following literals + Parchment** ❌ **cancelled** | Was: promote the hue-carrying `rgba()` literals of §4.8 so they follow the palette, then the light-mode port. Not scheduled — see "Phase 9, cancelled" below | +| **10 — Public nav sections + added links** ✅ | Admin-created dropdown sections in the public header, coded entries organised into them, and admin-authored same-origin links. `NavDropdown.jsx`, `buildPublicNav`/`pruneNav`, the `nav_public` wrapper of §6.4, and the Public tab's own tree editor. **Amends §7** | Phases 0–2 are one PR pair (website + docs), 3–4 a second, 5 a third, 6–8 a fourth. **All four PR pairs target `edge`, not `main`** — the feature reaches @@ -815,6 +875,54 @@ module-level copy of the two authenticated rows, which is what lets a save in th editor update the sidebar the admin is looking at without a reload — and stops the second layout to mount from flashing the coded nav first. +### Phase 10 as landed + +Asked for after phases 6–8 were built and before the `edge` → `main` cutover: +the public site should support dropdown sections with links organised inside +them. Scoped to the **public header only** — the admin sidebar keeps its four +coded sections and the player portal its three flat rows — and to **same-origin +links**, which is what makes §7's amendment a narrowing rather than an opening. + +**The shape change was free because nothing had shipped.** `nav_public` grew a +`{items, sections, links}` wrapper. Had this landed after the cutover it would +have needed a migration or a version field; before it, a forgiving read of the +bare map is enough, and that read is kept anyway as insurance for a row written +during review. + +**Sections are entries in the top-level order, which is why the Public tab has +its own editor.** The admin sidebar's groups are a fixed frame the code declares: +only membership moves. A public section is something the admin created and can +drag among the pills. That is a tree, not a list of groups, so +`PublicNavTree.jsx` renders it with a nested `SortableContext` per section, while +the other two tabs keep the phase-7 grouped editor. The shared `Row` was +generalised — its destination `