feat(theming): settings-store, nav merge util and radius tokens (phases 0-2) #121

Merged
whitlocktech merged 1 commits from feat/theming-nav-phase-0-2 into edge 2026-08-07 23:25:27 +00:00
Member

What & why

Phases 0–2 of docs/website/THEMING_AND_NAV.md (approved in docs#103). Groundwork only — no admin UI, no consumer wiring. An instance that never touches the new settings keys renders exactly as it does today, which is the acceptance criterion for all three phases.

Targets edge, not main. The whole feature lands on main as one edgemain merge once every phase is in, so no release carries a half-wired theme engine. Docs pair: RunicGateway/docs#104.

Phase 0 — settings-store groundwork

  • settingsDb.remove() + DELETE /api/v1/admin/settings/:key — the "reset to default" primitive. Defaults for these keys live in BRAND_* env, theme.css and the hardcoded NAV arrays, so reset has to delete the row; writing a stored copy of a default would stop tracking the default forever. Allowlisted (DELETABLE_KEYS) to the five theming/nav keys plus hero_layout_draft — an unrestricted DELETE would let a stray request drop site_mode or the uo-link config, where an absent row means something else entirely. Admin-only, and idempotent: resetting a key that was never set succeeds.
  • GET /api/v1/settings/nav behind requireAuth, no role gate — §4.2's blocking gap. AdminLayout renders for editors and moderators and PlayerPortalLayout for players, and none of them can read GET /admin/settings, so without this route their nav override would silently never apply.
  • A fifth router group for it, which the design named a URL for but not a home. None of the four fit: /public is anonymous (and the admin nav's labels describe the shape of the admin surface), /admin/settings is adminOnly, /player is data scoped to req.user.id. This is configuration that happens to need a login. Group gate is noindex, requireAuth, declared in settings/index.js ahead of every mount.
  • parseJsonSetting() in utils/settingsJson.js. settings.value is TEXT, so every JSON-valued key arrives as a string; malformed, non-object, or validator-rejected values read as absent — never as an error, never half-applied. Same fail-safe posture as the client's existing parseLayout.
  • Key registration: theme_visual / brand_assets / nav_public join PUBLIC_KEYS; nav_admin / nav_player deliberately do not. No migration seeds any of them — absence is the "use the default" state.

Phase 1 — client/src/lib/navOverrides.js

The pure merge util, the one piece with real correctness risk. Presentation only: it may set label, order, hidden and (grouped navs) group, and nothing else. It cannot introduce a to, cannot touch roles/feature/icon/end, and hidden: false cannot un-hide anything — hiding is subtractive, and the existing filters in SiteHeader/AdminLayout run afterward unchanged and remain the boundary.

Three rules the design left open, settled here and locked by tests:

  • Ordering — an item the admin never reordered keeps its base-array index as its sort key, so setting one order doesn't scramble the rest. Explicit and implicit keys share one number line, so ties break explicit first ("0" means first), and equal explicit orders keep code order via a stable sort.
  • group — accepted only when it names a title the base nav already declares, so an item can never land under a header that doesn't exist. Group order is not overridable.
  • Field-by-field validation — a bad label doesn't discard a good order beside it; hidden is honored only as the literal true; unknown to values are dropped, so removing a route in code can't leave a dangling override.

Phase 2 — radius tokens

23 border-radius literals promoted to four tokens at today's values, matching the §4.7 census exactly: 14×8px--radius-input, 4×999px--radius-pill, 4×10px--radius-card, 1×12px--radius-panel. The 7px (.rte-btn) and 6px (.rte-linkmenu-item) editor chrome and the two 50% circles stay literal. --shadow-card and --panel-grad turned out to already be tokens and already derived, so the shadow half of the phase was a no-op — both box-shadow declarations in theme.css already read var(--shadow-card).

How it was tested

  • cd server && npm test695 pass, 0 fail, including 16 new tests in test/settingsTheming.test.js: the allowlist (each theming key resettable; site_mode/uo_link_token/player_registration/hero_layout refused), reset-never-writes, idempotency, editor 403, /settings/nav reachable by admin/editor/moderator/player and 401 anonymous, nav_admin/nav_player never public, and parseJsonSetting's absent/malformed/validator cases.
  • cd client && node --test67 pass, 0 fail, including 20 new tests in test/navOverrides.test.js. The load-bearing negatives: no override returns the same array reference, an unknown to is never added, gates survive verbatim, and hiding every item in a group leaves no orphaned header.
  • npm run swagger and npm run routes:manifest regenerated; the manifest diff is exactly the two new routes. routeManifest.test.js's guard assertion now covers /settings/** alongside /admin/** and /player/**.
  • cd client && npm run build — clean. Built CSS confirmed to carry --radius-pill: 999px, --radius-panel: 12px, --radius-card: 10px, --radius-input: 8px, i.e. the promotion resolves to the same pixels.

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 Phases 0–2 of [`docs/website/THEMING_AND_NAV.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/THEMING_AND_NAV.md) (approved in docs#103). **Groundwork only** — no admin UI, no consumer wiring. An instance that never touches the new settings keys renders exactly as it does today, which is the acceptance criterion for all three phases. Targets **`edge`**, not `main`. The whole feature lands on `main` as one `edge` → `main` merge once every phase is in, so no release carries a half-wired theme engine. Docs pair: RunicGateway/docs#104. ### Phase 0 — settings-store groundwork - **`settingsDb.remove()` + `DELETE /api/v1/admin/settings/:key`** — the "reset to default" primitive. Defaults for these keys live in `BRAND_*` env, `theme.css` and the hardcoded `NAV` arrays, so reset has to *delete the row*; writing a stored copy of a default would stop tracking the default forever. Allowlisted (`DELETABLE_KEYS`) to the five theming/nav keys plus `hero_layout_draft` — an unrestricted DELETE would let a stray request drop `site_mode` or the uo-link config, where an absent row means something else entirely. Admin-only, and idempotent: resetting a key that was never set succeeds. - **`GET /api/v1/settings/nav`** behind `requireAuth`, no role gate — §4.2's blocking gap. `AdminLayout` renders for editors and moderators and `PlayerPortalLayout` for players, and none of them can read `GET /admin/settings`, so without this route their nav override would silently never apply. - **A fifth router group** for it, which the design named a URL for but not a home. None of the four fit: `/public` is anonymous (and the admin nav's labels describe the shape of the admin surface), `/admin/settings` is `adminOnly`, `/player` is data scoped to `req.user.id`. This is configuration that happens to need a login. Group gate is `noindex, requireAuth`, declared in `settings/index.js` ahead of every mount. - **`parseJsonSetting()`** in `utils/settingsJson.js`. `settings.value` is `TEXT`, so every JSON-valued key arrives as a string; malformed, non-object, or validator-rejected values read as **absent** — never as an error, never half-applied. Same fail-safe posture as the client's existing `parseLayout`. - **Key registration:** `theme_visual` / `brand_assets` / `nav_public` join `PUBLIC_KEYS`; `nav_admin` / `nav_player` deliberately do not. No migration seeds any of them — absence *is* the "use the default" state. ### Phase 1 — `client/src/lib/navOverrides.js` The pure merge util, the one piece with real correctness risk. **Presentation only:** it may set `label`, `order`, `hidden` and (grouped navs) `group`, and nothing else. It cannot introduce a `to`, cannot touch `roles`/`feature`/`icon`/`end`, and `hidden: false` cannot un-hide anything — hiding is subtractive, and the existing filters in `SiteHeader`/`AdminLayout` run afterward unchanged and remain the boundary. Three rules the design left open, settled here and locked by tests: - **Ordering** — an item the admin never reordered keeps its base-array index as its sort key, so setting one `order` doesn't scramble the rest. Explicit and implicit keys share one number line, so ties break *explicit first* ("0" means first), and equal explicit orders keep code order via a stable sort. - **`group`** — accepted only when it names a title the base nav already declares, so an item can never land under a header that doesn't exist. Group *order* is not overridable. - **Field-by-field validation** — a bad `label` doesn't discard a good `order` beside it; `hidden` is honored only as the literal `true`; unknown `to` values are dropped, so removing a route in code can't leave a dangling override. ### Phase 2 — radius tokens 23 `border-radius` literals promoted to four tokens **at today's values**, matching the §4.7 census exactly: 14×`8px` → `--radius-input`, 4×`999px` → `--radius-pill`, 4×`10px` → `--radius-card`, 1×`12px` → `--radius-panel`. The `7px` (`.rte-btn`) and `6px` (`.rte-linkmenu-item`) editor chrome and the two `50%` circles stay literal. `--shadow-card` and `--panel-grad` turned out to already be tokens and already derived, so the shadow half of the phase was a no-op — both `box-shadow` declarations in `theme.css` already read `var(--shadow-card)`. ## How it was tested - `cd server && npm test` — **695 pass, 0 fail**, including 16 new tests in `test/settingsTheming.test.js`: the allowlist (each theming key resettable; `site_mode`/`uo_link_token`/`player_registration`/`hero_layout` refused), reset-never-writes, idempotency, editor 403, `/settings/nav` reachable by admin/editor/moderator/player and 401 anonymous, `nav_admin`/`nav_player` never public, and `parseJsonSetting`'s absent/malformed/validator cases. - `cd client && node --test` — **67 pass, 0 fail**, including 20 new tests in `test/navOverrides.test.js`. The load-bearing negatives: no override returns the *same array reference*, an unknown `to` is never added, gates survive verbatim, and hiding every item in a group leaves no orphaned header. - `npm run swagger` and `npm run routes:manifest` regenerated; the manifest diff is exactly the two new routes. `routeManifest.test.js`'s guard assertion now covers `/settings/**` alongside `/admin/**` and `/player/**`. - `cd client && npm run build` — clean. Built CSS confirmed to carry `--radius-pill: 999px`, `--radius-panel: 12px`, `--radius-card: 10px`, `--radius-input: 8px`, i.e. the promotion resolves to the same pixels. ## 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 23:16:43 +00:00
Phases 0-2 of docs/website/THEMING_AND_NAV.md. Groundwork only: no admin UI,
no consumer wiring, and an instance that never touches the new settings keys
renders exactly as it does today.

Phase 0 - settings store:
- settingsDb.remove() and DELETE /api/v1/admin/settings/:key, the "reset to
  default" primitive. Defaults for these keys live in BRAND_* env, theme.css
  and the hardcoded NAV arrays, so reset has to delete the row rather than
  store a copy of the default. Allowlisted to the five theming/nav keys plus
  hero_layout_draft, admin-only, idempotent.
- GET /api/v1/settings/nav behind requireAuth with no role gate. AdminLayout
  renders for editors and moderators and PlayerPortalLayout for players, and
  none of them can read GET /admin/settings, so without this their nav
  override would silently never apply.
- A fifth router group for it: /public is anonymous, /admin/settings is
  adminOnly, /player is self-scoped data. This is configuration that needs a
  login.
- parseJsonSetting() in utils/settingsJson.js. settings.value is TEXT, so
  every JSON key arrives as a string; malformed or wrong-shaped reads as
  absent, never as an error and never half-applied.
- theme_visual / brand_assets / nav_public join PUBLIC_KEYS; nav_admin and
  nav_player deliberately do not.

Phase 1 - client/src/lib/navOverrides.js, the pure merge util. Presentation
only: it can set label/order/hidden and (grouped navs) group, and nothing
else. It cannot introduce a `to`, cannot touch roles/feature, and hidden:false
cannot un-hide anything - the existing filters run afterward, unchanged, and
remain the boundary.

Phase 2 - promoted 23 border-radius literals in theme.css to four tokens at
today's values (14x8px, 4x999px, 4x10px, 1x12px). The 7px/6px editor chrome
and the two 50% circles stay literal. --shadow-card and --panel-grad were
already tokens.

Tests: 16 new server tests, 20 new client tests. The route-manifest guard now
also asserts /settings/** sits behind requireAuth. Swagger and both route
artifacts regenerated.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit 0a2ccafff6 into edge 2026-08-07 23:25:27 +00:00
whitlocktech deleted branch feat/theming-nav-phase-0-2 2026-08-07 23:25:28 +00:00
Sign in to join this conversation.
No description provided.