docs(website): add theming & nav build contract #103

Merged
whitlocktech merged 1 commits from docs/theming-and-nav-plan into main 2026-08-07 22:07:49 +00:00
Member

What & why

Adds docs/website/THEMING_AND_NAV.md — the design of record for admin-configurable theming, brand assets and navigation, plus its index row in README.md.

This takes the draft spec (Spec: Admin-Configurable Theming & Navigation), audits it against the current website/ tree, and turns it into a build contract in the same shape as HERO_EDITOR.md: locked decisions → corrections to current-code reality → phased build → acceptance criteria. Docs only — no website/ code is touched. Per CLAUDE.md, implementation waits on approval of this document.

Blocking gaps found in the draft spec

These would each have stopped implementation partway:

  1. There is no way to delete a setting. settings.db.js exposes get/getAll/set/seedDefault only, and the admin API is PUT /admin/settings taking a key/value object. Every "Reset to defaults" in the feature — an acceptance criterion in the draft — depends on deleting a row. Needs settingsDb.remove() + DELETE /admin/settings/:key.
  2. Editors, moderators and players cannot read their own nav overrides. The draft has nav_admin/nav_player "fetched by the authenticated AdminLayout/PlayerPortalLayout", but GET /admin/settings is gated requireRole('admin') while those layouts render for editors, moderators and players. Needs a new GET /settings/nav behind isLoggedIn.
  3. renderIndexHtml runs once at boot, not per request (app.js:207). Overriding the templated logo/favicon is a lifecycle change (cached shell + invalidation), not the await the draft implies.
  4. Settings values are TEXT, not objects. JSON keys are stored stringified and parsed client-side; the draft's settings.brand_assets?.hero reads as if they arrive parsed.
  5. getPublic().brand is a cross-repo contract. publicBrand.test.js locks its field list and the Android app seeds its whole Material theme from brand.accent; brand.accentInt colors Discord embeds. New keys would have left the phone app and the bot on the old accent.

Decisions locked in this doc

  • Effective values resolved server-side into getPublic().brand (override → env), so Android and Discord track admin theming with no client change. accentInt recomputes per request.
  • Radius + shadow tokens only. spacingUnit and borderWeight are cut, not deferred — theme.css has zero calc() spacings and 39 hand-written 1px borders (several semantic), so they are ~40-declaration refactors, not variable promotions.
  • Radius tokens seeded at today's real values (four tokens, not three). The draft's --radius-card: 8px would have restyled .card/.panel (10px) and .panel-flat (12px) on every existing instance, contradicting its own byte-for-byte criterion.
  • Three dark presets in v1; Parchment deferred to its own phase — it needs 28 dark-assuming rgba() literals plus hero overlays in five files ported, which no [data-theme] block reaches.
  • Font shortlist finalized: 12 options across 3 roles, 8 web families, one combined css2? request. Arial maps onto today's --sans so it costs no webfont; IM Fell English has no bold and will faux-bold at 600–700.
  • PNG-only faviconsMIME_EXT has no .ico entry, and deriving the stored extension from that map is precisely what makes the upload path safe.

Smaller notes captured

The hero already has a third override layer (hero_layout.background.image_url beats brand.hero); the footer "powered by" badge must not follow brand_assets.logo; nav renames don't reach the portal hero's quick-links; the nav editor must refuse to hide its own entry; CSP already allows both Google Fonts hosts so no policy change is needed.

How it was tested

Docs only — nothing to build or run. Every claim about current behavior was verified by reading the source, and the doc cites file:line for each. Spot checks behind the numbers:

  • Radius census in theme.css: 14×8px, 4×999px, 4×10px, 1×12px, 1×7px, 1×6px.
  • 39 hand-written 1px borders; 5 calc() uses, all page-shell widths; 28 rgba() literals.
  • brand.logo has zero client consumers; the header/sidebar logo slot is MoonDot.jsx, a CSS span.
  • getPublic() (settings.model.js:130-141) already emits accent/logo/hero/favicon, and publicBrand.test.js:30 asserts on all four.
  • Android BrandDto + MainActivity.kt:72 confirm the app themes from brand.accent.

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code (Opus 5). I have reviewed and understand
    every change, and take responsibility for it. AI-authored commits are
    marked with a Co-Authored-By / Assisted-By trailer.

License

  • I agree that my contribution is licensed under this project's license
    (GNU GPL v3.0 or later), and I have the right to contribute it.
## What & why Adds `docs/website/THEMING_AND_NAV.md` — the design of record for admin-configurable theming, brand assets and navigation, plus its index row in `README.md`. This takes the draft spec (*Spec: Admin-Configurable Theming & Navigation*), audits it against the current `website/` tree, and turns it into a build contract in the same shape as `HERO_EDITOR.md`: locked decisions → corrections to current-code reality → phased build → acceptance criteria. **Docs only — no `website/` code is touched.** Per `CLAUDE.md`, implementation waits on approval of this document. ### Blocking gaps found in the draft spec These would each have stopped implementation partway: 1. **There is no way to delete a setting.** `settings.db.js` exposes `get`/`getAll`/`set`/`seedDefault` only, and the admin API is `PUT /admin/settings` taking a key/value object. Every "Reset to defaults" in the feature — an acceptance criterion in the draft — depends on deleting a row. Needs `settingsDb.remove()` + `DELETE /admin/settings/:key`. 2. **Editors, moderators and players cannot read their own nav overrides.** The draft has `nav_admin`/`nav_player` "fetched by the authenticated `AdminLayout`/`PlayerPortalLayout`", but `GET /admin/settings` is gated `requireRole('admin')` while those layouts render for editors, moderators and players. Needs a new `GET /settings/nav` behind `isLoggedIn`. 3. **`renderIndexHtml` runs once at boot, not per request** (`app.js:207`). Overriding the templated logo/favicon is a lifecycle change (cached shell + invalidation), not the `await` the draft implies. 4. **Settings values are `TEXT`, not objects.** JSON keys are stored stringified and parsed client-side; the draft's `settings.brand_assets?.hero` reads as if they arrive parsed. 5. **`getPublic().brand` is a cross-repo contract.** `publicBrand.test.js` locks its field list and the Android app seeds its whole Material theme from `brand.accent`; `brand.accentInt` colors Discord embeds. New keys would have left the phone app and the bot on the old accent. ### Decisions locked in this doc - **Effective values resolved server-side** into `getPublic().brand` (override → env), so Android and Discord track admin theming with no client change. `accentInt` recomputes per request. - **Radius + shadow tokens only.** `spacingUnit` and `borderWeight` are cut, not deferred — `theme.css` has zero `calc()` spacings and 39 hand-written `1px` borders (several semantic), so they are ~40-declaration refactors, not variable promotions. - **Radius tokens seeded at today's real values** (four tokens, not three). The draft's `--radius-card: 8px` would have restyled `.card`/`.panel` (10px) and `.panel-flat` (12px) on every existing instance, contradicting its own byte-for-byte criterion. - **Three dark presets in v1**; Parchment deferred to its own phase — it needs 28 dark-assuming `rgba()` literals plus hero overlays in five files ported, which no `[data-theme]` block reaches. - **Font shortlist finalized**: 12 options across 3 roles, 8 web families, one combined `css2?` request. Arial maps onto today's `--sans` so it costs no webfont; IM Fell English has no bold and will faux-bold at 600–700. - **PNG-only favicons** — `MIME_EXT` has no `.ico` entry, and deriving the stored extension from that map is precisely what makes the upload path safe. ### Smaller notes captured The hero already has a third override layer (`hero_layout.background.image_url` beats `brand.hero`); the footer "powered by" badge must not follow `brand_assets.logo`; nav renames don't reach the portal hero's quick-links; the nav editor must refuse to hide its own entry; CSP already allows both Google Fonts hosts so no policy change is needed. ## How it was tested Docs only — nothing to build or run. Every claim about current behavior was verified by reading the source, and the doc cites `file:line` for each. Spot checks behind the numbers: - Radius census in `theme.css`: 14×`8px`, 4×`999px`, 4×`10px`, 1×`12px`, 1×`7px`, 1×`6px`. - 39 hand-written `1px` borders; 5 `calc()` uses, all page-shell widths; 28 `rgba()` literals. - `brand.logo` has zero client consumers; the header/sidebar logo slot is `MoonDot.jsx`, a CSS span. - `getPublic()` (`settings.model.js:130-141`) already emits `accent`/`logo`/`hero`/`favicon`, and `publicBrand.test.js:30` asserts on all four. - Android `BrandDto` + `MainActivity.kt:72` confirm the app themes from `brand.accent`. ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code (Opus 5)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` / `Assisted-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude added 1 commit 2026-08-07 22:06:15 +00:00
Corrects the design doc against the current codebase and locks the open
decisions, in the same shape as HERO_EDITOR.md (locked decisions ->
corrections to reality -> phased build).

Blocking gaps found in the design doc:
  - no delete path exists for a settings row, which every "reset to
    defaults" in the feature depends on
  - editors/moderators/players have no endpoint to read their own nav
    overrides (GET /admin/settings is admin-only)
  - renderIndexHtml runs once at boot, not per request
  - settings values are JSON strings, not objects
  - getPublic().brand is a cross-repo contract the Android app and
    Discord embeds theme from; new keys would silently bypass it

Locked: effective values resolved server-side into getPublic().brand;
radius + shadow tokens only (spacing/border cut); radius tokens seeded at
today's real values so the promotion is a no-op; three dark presets in v1
with Parchment deferred; 12-option font shortlist across 8 web families
in one request; PNG-only favicons.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech approved these changes 2026-08-07 22:07:40 +00:00
whitlocktech merged commit 6398285a13 into main 2026-08-07 22:07:49 +00:00
whitlocktech deleted branch docs/theming-and-nav-plan 2026-08-07 22:07:50 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/docs#103
No description provided.