Merge pull request 'docs(website): theming & nav phases 3-4 as built' (#105) from docs/theming-nav-phase-3-4 into edge
Reviewed-on: #105 Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
This commit is contained in:
@@ -129,7 +129,10 @@ server/
|
|||||||
nav.router.js (1) /settings/nav — the nav_admin and
|
nav.router.js (1) /settings/nav — the nav_admin and
|
||||||
nav_player overrides, read by the
|
nav_player overrides, read by the
|
||||||
layouts that render them
|
layouts that render them
|
||||||
nav.controller.js
|
theme.router.js (1) /settings/theme/options — the closed
|
||||||
|
sets the admin appearance form is
|
||||||
|
built from. Static; no DB read
|
||||||
|
nav.controller.js + theme.controller.js
|
||||||
admin/ index.js mounts the capability routers below at their
|
admin/ index.js mounts the capability routers below at their
|
||||||
own prefixes; owns the shared
|
own prefixes; owns the shared
|
||||||
`noindex, isLoggedIn, staffOnly` gate and
|
`noindex, isLoggedIn, staffOnly` gate and
|
||||||
@@ -279,6 +282,17 @@ consumer parses it. Server side that is `utils/settingsJson.js`
|
|||||||
malformed or wrong-shaped value as **absent** rather than as an error, so a
|
malformed or wrong-shaped value as **absent** rather than as an error, so a
|
||||||
hand-edited row degrades to the default instead of rendering something broken.
|
hand-edited row degrades to the default instead of rendering something broken.
|
||||||
|
|
||||||
|
**`theme_visual` is resolved server-side, not shipped raw to the browser.**
|
||||||
|
`utils/themeResolve.js` layers `:root` ← preset ← custom, field by field, into
|
||||||
|
the CSS custom properties `getPublic()` returns as `theme`; the SPA's only job
|
||||||
|
is to write them onto `<html>` and take back what it wrote last time
|
||||||
|
(`client/src/lib/themeVars.js`). One authority for the merge means the effective
|
||||||
|
accent in `brand.accent` — the cross-repo contract the Android app and the
|
||||||
|
Discord bot theme themselves from — always agrees with what the website paints.
|
||||||
|
Values reaching a CSS variable are checked against closed sets on both paths:
|
||||||
|
strictly on write (400, naming the field) and forgivingly on read (drop the bad
|
||||||
|
field, keep its neighbours).
|
||||||
|
|
||||||
### activity_log — append-only
|
### activity_log — append-only
|
||||||
| col | type | notes |
|
| col | type | notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -633,7 +647,7 @@ are authoritative, and they answer different questions:
|
|||||||
|
|
||||||
| Artifact | Source of truth for | Generated by |
|
| Artifact | Source of truth for | Generated by |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `server/routes.manifest.json` — mirrored as [api-route-inventory.json](./api-route-inventory.json) | **What URLs exist.** 225 public routes + 2 on the internal listener, sorted, method + path only. | `npm run routes:manifest`, by walking the live Express stack |
|
| `server/routes.manifest.json` — mirrored as [api-route-inventory.json](./api-route-inventory.json) | **What URLs exist.** 226 public routes + 2 on the internal listener, sorted, method + path only. | `npm run routes:manifest`, by walking the live Express stack |
|
||||||
| `server/swagger/swagger-output.json` — served at `/api/docs` | **What each route means.** Parameters, bodies, response codes, security. | `npm run swagger`, from `#swagger.*` annotations |
|
| `server/swagger/swagger-output.json` — served at `/api/docs` | **What each route means.** Parameters, bodies, response codes, security. | `npm run swagger`, from `#swagger.*` annotations |
|
||||||
|
|
||||||
The split is deliberate: Swagger is annotation-derived, so an unannotated route is invisible in it and
|
The split is deliberate: Swagger is annotation-derived, so an unannotated route is invisible in it and
|
||||||
@@ -809,7 +823,7 @@ from the per-route **siteMode** middleware (§5), never from an auth gate.
|
|||||||
|
|
||||||
| Method | Path | Notes |
|
| Method | Path | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| GET | `/settings` | whitelisted public keys, derived `registration`/`gameAccountSignup` flags, the per-shard **`brand`** block (name, `accent` color, logo/hero/favicon) a client themes itself from — one image runs as any shard, asset fields may be site-relative paths (resolve against the base URL) — and a **`push`** block `{ ntfyUrl }` (M7): the client-facing ntfy relay URL the app's embedded distributor registers its device topic against, from `NTFY_PUBLIC_URL` / first `NTFY_ALLOWED_ORIGINS` (never the internal `NTFY_BASE_URL`); `null` when push isn't configured for the shard. |
|
| GET | `/settings` | whitelisted public keys, derived `registration`/`gameAccountSignup` flags, the per-shard **`brand`** block (name, `accent` color, logo/hero/favicon) a client themes itself from — one image runs as any shard, asset fields may be site-relative paths (resolve against the base URL); these are **effective** values, so an admin theme (`theme_visual`) beats `BRAND_ACCENT_COLOR` and an uploaded `brand_assets` asset beats its `BRAND_*` path — an optional **`theme`** block, the resolved CSS custom properties for that admin theme (absent when the instance was never themed, which is what makes it render from the shipped stylesheet unchanged) — and a **`push`** block `{ ntfyUrl }` (M7): the client-facing ntfy relay URL the app's embedded distributor registers its device topic against, from `NTFY_PUBLIC_URL` / first `NTFY_ALLOWED_ORIGINS` (never the internal `NTFY_BASE_URL`); `null` when push isn't configured for the shard. |
|
||||||
| GET | `/status` | status message + current mode, **plus a `version` block** (`{ service:'runic-gateway', api, server }`) so a client first-run probe recognizes the backend and can run a version-mismatch guard |
|
| GET | `/status` | status message + current mode, **plus a `version` block** (`{ service:'runic-gateway', api, server }`) so a client first-run probe recognizes the backend and can run a version-mismatch guard |
|
||||||
| GET | `/version` | lightweight, **DB-free** backend identity/version (`{ service, api, server }`) — the canonical target for the version guard and a cheap liveness check |
|
| GET | `/version` | lightweight, **DB-free** backend identity/version (`{ service, api, server }`) — the canonical target for the version guard and a cheap liveness check |
|
||||||
| GET | `/posts/:category` | published only; `category` ∈ news\|five-on-friday\|newsletter\|screenshots |
|
| GET | `/posts/:category` | published only; `category` ∈ news\|five-on-friday\|newsletter\|screenshots |
|
||||||
@@ -843,6 +857,7 @@ These rows are configuration that happens to need a login.
|
|||||||
| Method | Path | Purpose |
|
| Method | Path | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| GET | `/settings/nav` | `{ nav_admin, nav_player }` — the stored nav overrides as raw JSON strings (or `null`), for the two authenticated layouts that render them. Deliberately not public: an anonymous visitor has no use for either, and the admin nav's labels describe the shape of the admin surface. Open to **any** role because `AdminLayout` renders for editors and moderators and `PlayerPortalLayout` for players, none of whom can read `GET /admin/settings`. Presentation-only — the role/feature filters in those layouts still decide what is shown, and an override can never un-hide a gated item (see [THEMING_AND_NAV.md](THEMING_AND_NAV.md) §7) |
|
| GET | `/settings/nav` | `{ nav_admin, nav_player }` — the stored nav overrides as raw JSON strings (or `null`), for the two authenticated layouts that render them. Deliberately not public: an anonymous visitor has no use for either, and the admin nav's labels describe the shape of the admin surface. Open to **any** role because `AdminLayout` renders for editors and moderators and `PlayerPortalLayout` for players, none of whom can read `GET /admin/settings`. Presentation-only — the role/feature filters in those layouts still decide what is shown, and an override can never un-hide a gated item (see [THEMING_AND_NAV.md](THEMING_AND_NAV.md) §7) |
|
||||||
|
| GET | `/settings/theme/options` | The closed sets an admin may pick from when theming the site: the presets (each with its **full token map**, so a form can show what an unset field currently resolves to), the curated Google Fonts shortlist per role, the shadow depths, the editable color/radius field names paired with the CSS variable each drives, and `shippedTokens` (what `theme.css`'s `:root` declares). Static — derived from `config/themePresets.js`, no DB read. Served rather than duplicated in client code so the options the form **offers** can never drift from the ones `PUT /admin/settings` **accepts** |
|
||||||
|
|
||||||
### /admin (admin/index.js → the capability routers in §2) — all behind `isLoggedIn` + `noindex` + `staffOnly`
|
### /admin (admin/index.js → the capability routers in §2) — all behind `isLoggedIn` + `noindex` + `staffOnly`
|
||||||
|
|
||||||
@@ -878,7 +893,7 @@ file a route sits in — that is the property the route manifest freezes.
|
|||||||
| POST | `/posts/upload` | multipart image upload (multer) → `{image_url}` for screenshots |
|
| POST | `/posts/upload` | multipart image upload (multer) → `{image_url}` for screenshots |
|
||||||
| GET | `/wiki` · GET `/wiki/:slug` | read incl. unpublished |
|
| GET | `/wiki` · GET `/wiki/:slug` | read incl. unpublished |
|
||||||
| POST | `/wiki` · PUT `/wiki/:slug` · DELETE `/wiki/:slug` | manage pages |
|
| POST | `/wiki` · PUT `/wiki/:slug` · DELETE `/wiki/:slug` | manage pages |
|
||||||
| GET | `/settings` · PUT `/settings` | read all / update `{key:value,...}` |
|
| GET | `/settings` · PUT `/settings` | read all / update `{key:value,...}`. Enum-constrained keys are validated on the way in; `theme_visual` additionally has every value checked against the closed sets in `config/themePresets.js` (hex color, shortlisted font stack, bounded px radius, listed shadow) and is stored stringified. The read path drops bad fields anyway, so the `400` is about **feedback** — a save that appears to succeed and then does nothing is worse than a rejection |
|
||||||
| DELETE | `/settings/:key` | reset one setting to its default by deleting the row. Allowlisted to the keys whose default lives outside the store (`theme_visual`, `brand_assets`, `nav_public`, `nav_admin`, `nav_player`, `hero_layout_draft`) — anything else is `400`. Idempotent: resetting a key that was never set succeeds |
|
| DELETE | `/settings/:key` | reset one setting to its default by deleting the row. Allowlisted to the keys whose default lives outside the store (`theme_visual`, `brand_assets`, `nav_public`, `nav_admin`, `nav_player`, `hero_layout_draft`) — anything else is `400`. Idempotent: resetting a key that was never set succeeds |
|
||||||
| GET | `/activity?limit=&offset=` | paginated activity log |
|
| GET | `/activity?limit=&offset=` | paginated activity log |
|
||||||
| GET | `/users` · POST `/users` · PUT `/users/:id` · DELETE `/users/:id` | user mgmt (can't delete self / last admin; password hashed on write) |
|
| GET | `/users` · POST `/users` · PUT `/users/:id` · DELETE `/users/:id` | user mgmt (can't delete self / last admin; password hashed on write) |
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ Every new setting is an *override layer*, never a replacement:
|
|||||||
| # | Decision |
|
| # | Decision |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Brand contract | **`getPublic().brand` returns effective values** (override → env). The Android app and Discord embeds track admin theming for free — see §4.5 |
|
| Brand contract | **`getPublic().brand` returns effective values** (override → env). The Android app and Discord embeds track admin theming for free — see §4.5 |
|
||||||
|
| Theme delivery | **The server resolves the whole effective token set** and the client writes it as CSS custom properties. No `[data-theme]` blocks — see §6.2 |
|
||||||
| Structural tokens | **Radius + shadow depth only.** `spacingUnit` and `borderWeight` are **cut**, not deferred — see §4.6 |
|
| Structural tokens | **Radius + shadow depth only.** `spacingUnit` and `borderWeight` are **cut**, not deferred — see §4.6 |
|
||||||
| Radius token values | **Seeded at today's real values** (four tokens, not three), so the promotion step is a true no-op — see §4.7 |
|
| Radius token values | **Seeded at today's real values** (four tokens, not three), so the promotion step is a true no-op — see §4.7 |
|
||||||
| Presets in v1 | **Three dark presets** — Runic Gateway, Modern, Fantasy. Parchment (light) is Phase 9 — see §4.8 |
|
| Presets in v1 | **Three dark presets** — Runic Gateway, Modern, Fantasy. Parchment (light) is Phase 9 — see §4.8 |
|
||||||
@@ -156,8 +157,19 @@ The web client needs **no change** for this — its existing
|
|||||||
([`SiteContext.jsx:30-32`](../../website/client/src/contexts/SiteContext.jsx))
|
([`SiteContext.jsx:30-32`](../../website/client/src/contexts/SiteContext.jsx))
|
||||||
simply receives a better value. Consequences to handle:
|
simply receives a better value. Consequences to handle:
|
||||||
|
|
||||||
- `brand.accentInt` must be **recomputed from the effective accent** per request
|
- ~~`brand.accentInt` must be **recomputed from the effective accent** per
|
||||||
rather than read from the boot-time constant, or Discord embeds drift.
|
request rather than read from the boot-time constant, or Discord embeds
|
||||||
|
drift.~~ **Corrected in Phase 3 — this fix as written was a no-op.**
|
||||||
|
`getPublic().brand` never exposes `accentInt` (`publicBrand.test.js` asserts
|
||||||
|
it is `undefined`, deliberately: it is a Discord-only integer form), and the
|
||||||
|
server-side `brand.accentInt` has no consumer at all. Discord embeds are
|
||||||
|
colored by **`bot/src/brand.js`, in a separate process**, reading
|
||||||
|
`BRAND_ACCENT_COLOR` from env at boot — so there was nothing per-request to
|
||||||
|
recompute, and the drift the note describes was real but unfixable from the
|
||||||
|
server. What Phase 3 actually did: the bot now fetches
|
||||||
|
`GET /public/settings` → `brand.accent` (it already has a public-API client)
|
||||||
|
behind a 10-minute cached getter, keeping env as the fallback. See
|
||||||
|
"Phases 3–4 as landed" below.
|
||||||
- `publicBrand.test.js` gains cases: no rows → env values unchanged (the existing
|
- `publicBrand.test.js` gains cases: no rows → env values unchanged (the existing
|
||||||
assertions must still pass verbatim); `theme_visual` accent set → effective
|
assertions must still pass verbatim); `theme_visual` accent set → effective
|
||||||
accent returned; `brand_assets.favicon` set → favicon overridden while `logo`
|
accent returned; `brand_assets.favicon` set → favicon overridden while `logo`
|
||||||
@@ -362,10 +374,19 @@ null a field out to "clear" it** — remove it from the object.
|
|||||||
|
|
||||||
### 6.2 Preset blocks
|
### 6.2 Preset blocks
|
||||||
|
|
||||||
`:root` (no `data-theme` attribute set at all) stays the **Runic Gateway** default
|
> **Superseded in Phase 3.** The presets below are correct as *values* and were
|
||||||
— today's actual values — so an instance with no `theme_visual` row renders
|
> built as specified, but they do **not** live in `theme.css` as `[data-theme]`
|
||||||
exactly as it does now. `runic-gateway` is *also* declared as a named preset so
|
> blocks. They live in `server/src/config/themePresets.js`, and the server
|
||||||
that switching back to it after trying another is the same code path.
|
> resolves the effective token set into `getPublic().theme` for the client to
|
||||||
|
> write onto `<html>`. See "Phases 3–4 as landed" for why, and note two
|
||||||
|
> corrections the build made to the palettes: each preset carries the **full**
|
||||||
|
> color set (fifteen tokens, not the eight below), and `--shadow-card` is
|
||||||
|
> themed alongside the radii.
|
||||||
|
|
||||||
|
`:root` stays the **Runic Gateway** default — today's actual values — so an
|
||||||
|
instance with no `theme_visual` row renders exactly as it does now.
|
||||||
|
`runic-gateway` is *also* declared as a named preset so that switching back to
|
||||||
|
it after trying another is the same code path.
|
||||||
|
|
||||||
```css
|
```css
|
||||||
[data-theme="runic-gateway"] {
|
[data-theme="runic-gateway"] {
|
||||||
@@ -514,13 +535,13 @@ today until the admin acts.
|
|||||||
| **0 — Settings-store groundwork** ✅ | `settingsDb.remove()`; `DELETE /admin/settings/:key` with key allowlist; `GET /settings/nav` (§4.2); `parseJsonSetting()` helper; register the five keys; three into `PUBLIC_KEYS`. Swagger + route-manifest regen |
|
| **0 — Settings-store groundwork** ✅ | `settingsDb.remove()`; `DELETE /admin/settings/:key` with key allowlist; `GET /settings/nav` (§4.2); `parseJsonSetting()` helper; register the five keys; three into `PUBLIC_KEYS`. Swagger + route-manifest regen |
|
||||||
| **1 — `navOverrides.js` + tests** ✅ | The pure merge util, unit-tested in isolation. **The one piece with real correctness risk** |
|
| **1 — `navOverrides.js` + tests** ✅ | The pure merge util, unit-tested in isolation. **The one piece with real correctness risk** |
|
||||||
| **2 — Radius/shadow token groundwork** ✅ | Promote the literals in `theme.css` to the four tokens of §4.7, values unchanged. Verify zero visual diff before any admin UI exists |
|
| **2 — Radius/shadow token groundwork** ✅ | Promote the literals in `theme.css` to the four tokens of §4.7, values unchanged. Verify zero visual diff before any admin UI exists |
|
||||||
| **3 — Theme engine** | Three preset blocks, the combined Google Fonts link, `SiteContext` extension, and the effective-value resolution in `getPublic().brand` (§4.5) |
|
| **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` |
|
| **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 three shells; `heroImage` chain extension |
|
| **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 three shells; `heroImage` chain extension |
|
||||||
| **6 — Public nav wiring** | `SiteHeader.jsx` → `nav_public`. Lowest risk of the three: no roles, no groups |
|
| **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** |
|
| **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 |
|
| **8 — Admin + Player nav** | Wire the remaining two layouts, add the remaining two tabs, once the public pattern is validated in use |
|
||||||
| **9 — Parchment (optional)** | Light-mode port per §4.8 — its own contrast pass across every component |
|
| **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 3–4 as landed"), then the light-mode port with its own contrast pass across every component |
|
||||||
|
|
||||||
Phases 0–2 are one PR pair (website + docs), 3–4 a second, 5 a third, 6–8 a
|
Phases 0–2 are one PR pair (website + docs), 3–4 a second, 5 a third, 6–8 a
|
||||||
fourth. **All four PR pairs target `edge`, not `main`** — the feature reaches
|
fourth. **All four PR pairs target `edge`, not `main`** — the feature reaches
|
||||||
@@ -554,6 +575,97 @@ carries a half-wired theme engine.
|
|||||||
of the phase was a no-op; the only two `box-shadow` declarations in
|
of the phase was a no-op; the only two `box-shadow` declarations in
|
||||||
`theme.css` both already read `var(--shadow-card)`.
|
`theme.css` both already read `var(--shadow-card)`.
|
||||||
|
|
||||||
|
### Phases 3–4 as landed
|
||||||
|
|
||||||
|
Four things the design settled differently once it met the code.
|
||||||
|
|
||||||
|
**1. The server resolves the whole token set; there are no `[data-theme]`
|
||||||
|
blocks.** §6.2 put the presets in `theme.css` and had the client set a
|
||||||
|
`data-theme` attribute. That does not work as written: `SiteContext` writes
|
||||||
|
`--accent` as an **inline style on `<html>`** (`SiteContext.jsx:31`), and an
|
||||||
|
inline property beats any attribute-selector block. An admin who picked Fantasy
|
||||||
|
without also setting a custom accent would have had Fantasy's `#c9973f` painted
|
||||||
|
over by `brand.accent` from env — and §4.5's whole point is that
|
||||||
|
`getPublic().brand.accent` is what the phone app themes itself from, so the two
|
||||||
|
surfaces would have disagreed about the accent while both being "right".
|
||||||
|
|
||||||
|
The fix removes the conflict rather than sequencing around it. Presets live in
|
||||||
|
`server/src/config/themePresets.js`; `server/src/utils/themeResolve.js` layers
|
||||||
|
`:root` ← preset ← custom **per field** into a token map; `getPublic()` returns
|
||||||
|
it as `theme`; `client/src/lib/themeVars.js` writes it onto `<html>`. One
|
||||||
|
authority for the merge, `brand.accent` is by construction the accent the site
|
||||||
|
actually paints, and `theme.css`'s `:root` is untouched — an instance with no
|
||||||
|
row gets no `theme` block, the client writes nothing, and the page renders
|
||||||
|
byte-for-byte as today.
|
||||||
|
|
||||||
|
The client half's real logic is *removal*: inline properties are not cleared by
|
||||||
|
writing a smaller object over them, so `applyThemeTokens` tracks what it set
|
||||||
|
last time and `removeProperty`s whatever the new payload no longer mentions.
|
||||||
|
Without that, "Reset to defaults" would look broken until a reload.
|
||||||
|
|
||||||
|
**2. Presets carry the full fifteen-token palette, and theme `--shadow-card`.**
|
||||||
|
§6.2's blocks set eight colors. Applied literally, Fantasy's warm brown page
|
||||||
|
would have kept `--line: #2a3544` and `--blue: #13243c` — dark blue-grey borders
|
||||||
|
and a blue-grey active nav row — because those tokens are not in the list.
|
||||||
|
Every preset now sets `--panel-flat`, `--line`, `--line-soft`, `--head`,
|
||||||
|
`--muted`, `--dim` and `--blue` as well. The admin *form* still exposes only
|
||||||
|
§6.1's eight; the rest are supporting shades a preset gets right coherently but
|
||||||
|
that are not worth hand-picking. `--mode-live` / `--mode-maint` stay fixed
|
||||||
|
across every preset (green means live) and `--panel-grad` stays derived, both
|
||||||
|
locked by tests.
|
||||||
|
|
||||||
|
**3. The option catalog is served, not duplicated.**
|
||||||
|
`GET /api/v1/settings/theme/options` returns the presets (with their full token
|
||||||
|
maps, so a control can show what an unset field currently resolves to), the font
|
||||||
|
shortlist, the shadow depths, and the editable field names paired with the CSS
|
||||||
|
variable each drives. Duplicating those lists in client code would mean the form
|
||||||
|
could offer a font the server rejects, which surfaces as a save 400ing for no
|
||||||
|
visible reason. A test asserts every offered option validates.
|
||||||
|
|
||||||
|
Validation is deliberately asymmetric: **strict on write** (`PUT
|
||||||
|
/admin/settings` 400s and names the offending field) and **forgiving on read**
|
||||||
|
(a bad field is dropped, its neighbours keep applying). Strict-on-write gives
|
||||||
|
feedback; forgiving-on-read means a row hand-edited in the DB degrades to the
|
||||||
|
shipped default instead of rendering a broken site.
|
||||||
|
|
||||||
|
One addition to §5.1's twelve font options: **Georgia in the serif list.** The
|
||||||
|
shortlist gave the sans role a "today's default" option (Arial, byte-identical
|
||||||
|
to `--sans`) but left serif with no way back to `Georgia, "Times New Roman",
|
||||||
|
serif` short of resetting the whole theme. It pulls in no web family, so §5.2's
|
||||||
|
combined URL is unchanged.
|
||||||
|
|
||||||
|
**4. The Discord bot fetches the accent; §4.5's `accentInt` note was a no-op.**
|
||||||
|
See the correction in §4.5. `bot/src/brand.js` now reads
|
||||||
|
`GET /public/settings` → `brand.accent` through the public-API client it already
|
||||||
|
had, behind getters with a 10-minute TTL — so `brand.accentInt` stays a plain
|
||||||
|
property read at every existing call site, an embed never awaits a network call,
|
||||||
|
and any failure (site down, maintenance, malformed body) keeps the last known
|
||||||
|
good value with `BRAND_ACCENT_COLOR` as the floor.
|
||||||
|
|
||||||
|
**Found while smoke-testing: §4.8's rgba literals are not only a light-mode
|
||||||
|
problem.** The 28 dark-assuming `rgba()` literals were scoped to Phase 9 on the
|
||||||
|
reasoning that they break a *light* preset. Applying **Fantasy** on a live
|
||||||
|
instance shows they also carry a **hue**: `.btn-ghost`'s
|
||||||
|
`background: rgba(11, 22, 48, 0.45)` (essentially `--blue` at 45%) leaves the
|
||||||
|
portal's quick-link buttons reading blue on a warm brown page, and the hero
|
||||||
|
overlay stack in `heroLayout.js` is `rgba(11,15,20,…)` regardless of preset.
|
||||||
|
Nothing is broken or unreadable — it is a visible seam, not a bug — but Phase 9
|
||||||
|
should be re-scoped from "light-mode port" to "make the hue-carrying literals
|
||||||
|
follow the palette", which the dark presets need too. Not fixed here: it is the
|
||||||
|
23-declaration-style promotion Phase 2 was, and folding it into the phase that
|
||||||
|
introduced the presets would have hidden it inside an unrelated diff.
|
||||||
|
|
||||||
|
**Still open, by decision:** the theme arrives with the `/public/settings` fetch,
|
||||||
|
so a themed instance paints the shipped palette for one frame before repainting.
|
||||||
|
Phase 5 has to rewrite `renderIndexHtml` into a cached, invalidated shell anyway
|
||||||
|
(§4.3) — injecting a `<style>` block with the effective tokens there removes the
|
||||||
|
flash for free, so it is deferred rather than solved twice.
|
||||||
|
|
||||||
|
**Also fixed in passing:** `settings/nav.controller.js` imported the logger
|
||||||
|
*factory* rather than calling it, so `log.error` was `undefined` and a DB fault
|
||||||
|
would have thrown a `TypeError` inside the catch — no response sent, request
|
||||||
|
left hanging — instead of returning a 500. Introduced in Phase 0.
|
||||||
|
|
||||||
### 8.1 Admin builder UI notes
|
### 8.1 Admin builder UI notes
|
||||||
|
|
||||||
- Tabbed control for the three navs; drag-and-drop reorderable list.
|
- Tabbed control for the three navs; drag-and-drop reorderable list.
|
||||||
|
|||||||
@@ -900,6 +900,10 @@
|
|||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/api/v1/settings/nav"
|
"path": "/api/v1/settings/nav"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/v1/settings/theme/options"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"internal": [
|
"internal": [
|
||||||
|
|||||||
Reference in New Issue
Block a user