docs(website): theming phases 6-8 as built, and phase 9 cancelled

Marks the nav wiring and the builder UI landed, and records the five places the
build differed from the design:

- The server had no way to store a nav row. The design scoped 6-8 as client
  work, but updateSettings would have written a nav object as "[object Object]"
  — a save that 200s and does nothing, for ever.
- The server deliberately cannot check that a `to` exists: the base NAV arrays
  are client constants, and a server-side copy would be a second source of truth
  for navigation. Shape is the server's question, membership the client's.
- `hidden: false` is accepted and never stored, so hiding stays subtractive.
- The nav editor cannot be hidden, enforced in three places.
- Orders are written only when something actually moved, compared against the
  base restricted to the rows the editing admin can see.

Phase 9 (hue-carrying rgba literals + Parchment) is cancelled rather than
deferred. The finding that motivated it is kept as the record: those literals
carry a hue, so they are a rough edge in the three dark presets and not only a
blocker for a hypothetical light one.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-08 00:02:46 -05:00
parent 2a8b9d5748
commit 5c24ff1378
2 changed files with 95 additions and 9 deletions

View File

@@ -538,15 +538,16 @@ today until the admin acts.
| **3 — Theme engine** ✅ | Three presets, the combined Google Fonts link, `SiteContext` extension, and the effective-value resolution in `getPublic().brand` (§4.5) |
| **4 — Admin theme UI** ✅ | `/admin/appearance` view + route in `App.jsx` + `NAV`/`TITLES` entries in `AdminLayout.jsx` |
| **5 — Brand assets** ✅ | Cached-shell rewrite in `app.js` (§4.3); upload endpoint on the existing multer config; `<img>` logo slot beside `MoonDot` in the shells; `heroImage` chain extension |
| **6 — Public nav wiring** | `SiteHeader.jsx``nav_public`. Lowest risk of the three: no roles, no groups |
| **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 (optional)** | Promote the hue-carrying `rgba()` literals of §4.8 so they follow the palette (the **dark** presets need this too — see "Phases 34 as landed"), then the light-mode port with its own contrast pass across every component |
| **6 — Public nav wiring** | `SiteHeader.jsx``nav_public`. Lowest risk of the three: no roles, no groups |
| **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 |
Phases 02 are one PR pair (website + docs), 34 a second, 5 a third, 68 a
fourth. **All four PR pairs target `edge`, not `main`** — the feature reaches
`main` as one `edge``main` merge once every phase is in, so no release ever
carries a half-wired theme engine.
carries a half-wired theme engine. Phase 8 is the last one, so that merge is
what closes the feature.
### Phases 02 as landed
@@ -756,6 +757,75 @@ admin panel still has the env name in its tab and its link previews. Fixing it
would change the served shell for instances with no `brand_assets` row, which is
exactly what §9 says must not change in this phase. It wants its own change.
### Phases 68 as landed
The nav half, wired end to end: the public header, the admin sidebar and the
player portal all read their override row, and `/admin/navigation` writes them.
Five things the design did not settle.
**1. The server had no way to store a nav row, and would have stored garbage.**
§8 described phases 68 as client work, and for the *merge* that is right. But
`updateSettings` validates and stringifies `theme_visual` and `brand_assets` and
lets everything else through to `settingsDb.set` — so a `nav_public` object would
have been written as the string `"[object Object]"`, which `parseJsonSetting`
then reads as absent. The save would have returned 200 and done nothing, for
ever. `server/src/utils/navOverrides.js` mirrors `utils/brandAssets.js`:
`validateNavOverrides` is strict on write and names the offending key,
`resolveNavOverrides` is forgiving and drops fields that would do nothing.
**2. The server cannot check that a `to` exists, and should not try.** The three
base `NAV` arrays are client constants. Shipping a copy to the server would
create a second source of truth for navigation that drifts the first time a route
is added, and it would buy nothing: `applyNavOverrides` already drops an entry
whose `to` the base array does not declare, which is the right place for it — a
route deleted in code stops mattering immediately, with no migration. **The
server validates shape; the client owns membership.** So the write path accepts
any app-internal path as a key (absolute, no scheme, no `//host`, no whitespace)
and rejects everything else, and it rejects any field that is not one of the
four — a `roles` or `to` in the body is a 400, not something quietly stored.
**3. `hidden: false` is accepted and never stored.** The editor sends it while a
row is being edited, so rejecting it would be hostile; storing it would leave a
row that reads like an instruction to *force* something visible, which this layer
must never be able to express. It is dropped on the way in, and hiding stays
subtractive.
**4. The nav editor cannot be hidden, and that is enforced three times.** An
admin who hid `/admin/navigation` would lose the only screen that can un-hide it.
The row's eye toggle is disabled with a note saying why; `resolveNavOverrides`
drops `hidden` on that one `to` for `nav_admin`; and `AdminLayout` strips it
again before merging, which is what also covers a row edited straight in the
database. Typing the URL still works regardless — the guard is about not
stranding an admin who never learned it.
**5. Orders are written only when something actually moved.** §7.1 says the
editor writes an order for every item "the way drag-and-drop does", and it does —
but only for a nav whose sequence differs from the code's. An admin who renames
one item stores exactly one field, and a route added to `NAV` later still lands
where the code puts it. The comparison is against the base **restricted to the
rows that admin can see**, so a role- or feature-gated item missing from their
palette is not mistaken for a reorder. An override for such an item is carried
through their save untouched rather than quietly reset.
Two smaller notes. The section dropdown offers "(no section)" only to rows coded
into an untitled group (Dashboard, Account): for anything else it is a move an
override cannot express (§6.4 allows an existing titled section or nothing), so
offering it would silently do nothing. And `useNavOverrides` keeps one
module-level copy of the two authenticated rows, which is what lets a save in the
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 9, cancelled
The §4.8 `rgba()` literal promotion and the Parchment light-mode port are **not
scheduled**. The finding that motivated them stands and is worth keeping: those
literals carry a *hue*, not merely a light/dark assumption — `.btn-ghost` is
`rgba(11,22,48,0.45)`, so the portal quick-links read blue on Fantasy's warm
page. It is a real rough edge in the three dark presets, not only a blocker for a
hypothetical light one. It is simply not worth the contrast pass across every
component right now. Anyone picking it up should start from the census in §4.8
and the live observation in "Phases 34 as landed".
### 8.1 Admin builder UI notes
- Tabbed control for the three navs; drag-and-drop reorderable list.