feat(theming): dropdown sections and added links in the public header (phase 10) #125
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/theming-nav-phase-10"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
The capability asked for before the cutover: the public header can have dropdown sections, with links organised inside them. Phase 10 of
THEMING_AND_NAV.md; docs pair: RunicGateway/docs#108. Targetsedge, on top of the now-merged #124.An admin can create a dropdown section, drag the coded entries into it, and add links of their own. An instance where nobody opens the screen renders exactly as before.
This amends §7 on purpose
§7 said the override layer "cannot introduce a
tothat is not already in the hardcodedNAVarray". That stays true of every coded entry. An admin may now also add a link, and the amended constraint says what one may be://host, no whitespace or quotes. The nav is not a place to send visitors to an origin the operator does not control;roles/featureof its own, and needing none: the page behind it enforces its own access, so an added link advertises a route and never grants one. A link to/admin/usersis accepted and 403s for anyone who could not already reach it — exactly as typing the address would.A section has a label and a position and no route at all — it only opens, never navigates — so it adds no reachable surface whatsoever.
The property is kept structurally rather than by vigilance: coded entries live in an
itemsmap whose keys must be routes the base array declares, so that map cannot invent a route, while everything that can name an arbitrary path lives inlinks, which is the one place the path rule is applied — on the write path and the read path, so a hand-edited row cannot put an off-site link in the header either.The storage change was free, and only now
nav_publicgrew a{ items, sections, links }wrapper. A bare map still reads as the items map (every item key is a path, so it can never collide with the literal keyitems), and a nav with no sections still stores the bare map — so this changed nothing for a nav that does not use it. Doing this before the cutover costs a forgiving read; after it, it would have cost a migration or a version field.sections/linksare dropped fornav_admin/nav_player, whose layouts cannot render them.Why the Public tab needed its own editor
The admin sidebar's four sections are a fixed frame the code declares — only membership moves. A public section is something the admin created and can drag among the pills, so it is an entry in the top-level order. That is a tree, not a list of groups.
PublicNavTree.jsxrenders it with a nestedSortableContextper section; the Admin and Player tabs keep the phase-7 grouped editor untouched, and the sharedRowwas generalised so its destination<select>takes a list of choices instead of knowing about admin group titles.Moving between containers is still the dropdown, not a drag — same as the Admin tab. Deleting a section returns its entries to the top level, never removes them: those are coded pages and the admin's own links, and it is the one destructive act this screen could otherwise commit. Locked by a test.
The menu itself
NavDropdown.jsxopens on click, never hover, and its trigger is not a link: a hover menu is unusable on touch, and a trigger that navigates means tapping to open takes you somewhere instead. Escape closes and returns focus to the trigger, an outside press closes, navigating closes, Arrow Up/Down walk the items,aria-haspopup/aria-expandedlet it be announced as a menu, and the menu clamps tocalc(100vw - 24px)so it cannot push a narrow page sideways.pruneNavapplies the shard-feature gate inside a section and drops one it leaves empty, so a dropdown never opens onto nothing.A bug this surfaced in #124
The save path judged "does this route still exist?" against the palette — the base array already filtered to what the editing admin can see. For the admin nav that is harmless (an admin sees every row), but on the public header a shard-feature-gated row is filtered out, so the guard meant to carry its override through could never fire and their save would have silently reset it. Membership is now judged against the full coded nav while the rows still come from the palette: two different questions. Caught by the test written for it.
How it was tested
cd server && DB_HOST=127.0.0.1 DB_PORT=59999 npm test— 792 pass, 0 fail (782 before). New: ids and caps, duplicate ids, an off-origintorefused six ways, a link to a gated path accepted, a danglingsectionfalling to the top level, and sections/links dropped for the other two navs.cd client && node --test— 111 pass, 0 fail (94 before), incl. the empty-section drop, the bare-map compatibility read, an added link never pruned, the round trip's stability, and deleting a section returning its children.npm run buildclean.npm run swaggerregenerated (one description line);npm run routes:manifest— no new routes, zero diff, soapi-route-inventory.jsonis unchanged.https://discord.gg/…→ refused with a readable message, added/wiki/new-player-guideinstead, saved → the header showed The World ⌄ with all four inside, Escape closed it; then disabled the features behind its only two coded members → the dropdown disappeared entirely while the added link stayed (it has no gate); reset → zero rows and today's header, byte for byte.One flake, seen once: a full-suite run failed one test under load and passed on a clean rerun — the same timing-sensitive behaviour noted in #123. Untouched by this PR.
Notes
@dnd-kitarrived in #124.Checklist
AI-assisted contributions (required)
Claude Code (Opus 5). I have reviewed and understandevery change, and take responsibility for it. AI-authored commits are
marked with a
Co-Authored-By/Assisted-Bytrailer.License
(GNU GPL v3.0 or later), and I have the right to contribute it.
Phase 10 of docs/website/THEMING_AND_NAV.md, asked for before the edge -> main cutover. An admin can now create dropdown sections in the public header, organise the coded entries into them, and add links of their own. This deliberately amends §7, which said the override layer "cannot introduce a `to` that is not already in the hardcoded NAV array". That stays true of every CODED entry; an admin may now also add a link, restricted to a same-origin path — no scheme, 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. The invariant is kept structurally rather than by vigilance. Coded entries live in an `items` map whose keys must be routes the base array declares, so that map cannot invent a route; everything that CAN name an arbitrary path lives in `links`, which is the one place the path rule is applied — on both the write and the read path. nav_public therefore grew a { items, sections, links } wrapper. A bare map still reads as the items map, and a nav with no sections still stores one, so this changed nothing for a nav that does not use it. Free to do now because nothing has shipped; after the cutover it would have needed a migration. The Public tab gets its own editor. A public section is an entry in the top-level order that the admin created and can drag among the pills, unlike the admin sidebar's four coded sections, where only membership moves — that is a tree rather than a list of groups. Deleting a section returns its entries to the top level rather than removing them, which is the one destructive act this screen could otherwise commit. The dropdown opens on click and never on hover, and its trigger is not a link: a hover menu is unusable on touch, and a trigger that navigates means tapping to open takes you somewhere instead. Escape closes and returns focus, an outside press closes, navigating closes, and Arrow Up/Down walk the items. pruneNav applies the shard-feature gate inside a section and drops one it leaves empty, so a dropdown never opens onto nothing. Also fixes a bug this surfaced in the phase 6-8 code: the save path judged "does this route still exist?" against the palette — the base array already filtered to what the editing admin can see — so on the public header a feature-gated row's override could never be carried through and would have been silently reset. Membership is now judged against the full coded nav while the rows still come from the palette. Co-Authored-By: Claude <noreply@anthropic.com>