feat(theming): server-resolved theme engine and admin appearance UI
Phases 3-4 of docs/website/THEMING_AND_NAV.md. Three presets, the curated font shortlist, and /admin/appearance to drive them. The design put the presets in theme.css as [data-theme] blocks. That does not work: SiteContext writes --accent as an inline style on <html>, which beats any attribute-selector block, so a preset's accent would have been painted over by BRAND_ACCENT_COLOR while getPublic().brand.accent -- the value the Android app themes itself from -- reported the other one. Presets now live in server/src/config/themePresets.js. themeResolve.js layers :root <- preset <- custom per field into a token map, getPublic() returns it as `theme`, and the client writes it onto <html>. One authority for the merge, and brand.accent is by construction the accent the site paints. theme.css's :root is untouched, so an instance with no row gets no theme block and renders as today. Also: presets carry the full 15-token palette (eight would have left Fantasy with blue-grey borders); the option catalog is served from GET /settings/theme/options so the form cannot offer what the server rejects; validation is strict on write and forgiving on read; and the Discord bot now fetches the effective accent instead of its boot-time env copy. Fixes a Phase 0 bug in passing: settings/nav.controller.js imported the logger factory rather than calling it, so a DB fault would have thrown a TypeError inside the catch instead of returning 500. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react'
|
||||
import { createContext, useContext, useEffect, useRef, useState, useCallback, useMemo } from 'react'
|
||||
import { api } from '../api/client.js'
|
||||
import { applyThemeTokens } from '../lib/themeVars.js'
|
||||
|
||||
const SiteContext = createContext(null)
|
||||
|
||||
@@ -25,11 +26,28 @@ export function SiteProvider({ children }) {
|
||||
|
||||
const brand = useMemo(() => settings.brand || {}, [settings])
|
||||
|
||||
// Apply the admin's theme. The whole effective token set is resolved
|
||||
// server-side, so this only writes it and takes back what it wrote before —
|
||||
// see lib/themeVars.js for why the removal half matters. No theme block means
|
||||
// the admin never themed this instance, and the shipped :root stands.
|
||||
const appliedTokens = useRef([])
|
||||
useEffect(() => {
|
||||
appliedTokens.current = applyThemeTokens(document.documentElement.style, settings.theme, appliedTokens.current)
|
||||
}, [settings.theme])
|
||||
|
||||
// Apply the instance accent color to the CSS variable the theme is built on,
|
||||
// so branding flows to every `var(--accent)` at runtime (no rebuild).
|
||||
// so branding flows to every `var(--accent)` at runtime (no rebuild). This is
|
||||
// the *effective* accent — the admin theme overrides BRAND_ACCENT_COLOR
|
||||
// server-side (docs/website/THEMING_AND_NAV.md §4.5) — so it agrees with the
|
||||
// theme block rather than fighting it.
|
||||
//
|
||||
// Deliberately ordered after the theme effect and re-run on any theme change:
|
||||
// resetting a theme removes --accent from the token map, and this has to be
|
||||
// the write that lands last or an instance with a custom BRAND_ACCENT_COLOR
|
||||
// would drop to the stylesheet's default accent until the next reload.
|
||||
useEffect(() => {
|
||||
if (brand.accent) document.documentElement.style.setProperty('--accent', brand.accent)
|
||||
}, [brand.accent])
|
||||
}, [brand.accent, settings.theme])
|
||||
|
||||
// Memoized so consumers don't re-render on every provider render (brand is a
|
||||
// fresh object each render, which would otherwise churn the context value).
|
||||
|
||||
Reference in New Issue
Block a user