feat(theming): admin-configurable theme, brand assets and navigation (edge → main) #126
Reference in New Issue
Block a user
No description provided.
Delete Branch "edge"
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?
The cutover
Every phase of
docs/website/THEMING_AND_NAV.mdis in. This mergesedgeintomainas one release, so no release ever carried a half-wired theme engine. Docs pair: RunicGateway/docs#109.Five PRs, ten phases:
navOverrides.jsmerge util, radius tokens/admin/appearance/admin/navigationbuilderPhase 9 (hue-carrying
rgba()literals + a Parchment light theme) was cancelled, not deferred; the finding that motivated it is kept in the doc for whoever picks it up.What an operator gets
Colors, fonts and corner radius from three presets or per-field overrides; uploaded logo, hero and favicon; and a navigation builder that renames, reorders and hides entries in the public header, the admin sidebar and the player portal — plus dropdown sections in the public header with links of the admin's own.
The invariant this was built around
An instance where no admin touches any of it renders byte-for-byte as it does today. "Configured" means a settings row exists, not that a value is non-empty, so a reset deletes the row rather than storing a copy of the defaults — otherwise the stored copy stops tracking the default. No migration seeds anything.
The other property worth stating: the nav override layer is presentation, never authorization. The merge runs before the role and shard-feature filters in
SiteHeader.jsx/AdminLayout.jsx, which are unchanged and remain the boundary. Verified live, not only in tests — a hand-writtenhidden: falseon a feature-gated item and on a role-gated one showed nobody anything.Reach beyond this repo
getPublic().brandcarries effective values, so the Android app and the Discord bot track admin theming with no change of their own — the bot fetchesbrand.accentbehind a 10-minute TTL withBRAND_ACCENT_COLORas the floor.State at cutover
routes.manifest.jsongained exactly one route across the whole feature (POST /admin/settings/brand-asset/:slot), anddocs/website/api-route-inventory.jsonmirrors it.Things deliberately left
<title>and meta description still come fromBRAND_NAME/BRAND_DESCRIPTION, not the admin-setsite_titlethatbrand.nameprefers — so an instance renamed through the admin panel still shows the env name in its browser tab and link previews. Fixing it changes the served shell for instances with nobrand_assetsrow, which the byte-identical criterion forbade. It wants its own change..btnhas the sameline-heightgap #125 fixed on.pill; its anchor and button forms never share a row, so nothing is visibly wrong and 80 buttons was not a blast radius worth taking here.test/mobileSession.test.jsfailed under heavy full-suite load and passed on rerun. Timing-sensitive, untouched by any of this.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>