BOR_INPUT_PLACEHOLDER / BOR_FOOTER_TEXT / BOR_THEME (+ the indigo.css example theme); authoring guide: frontend/assets/themes/README.md, docs: README 'Customizing the look'.
4.1 KiB
4.1 KiB
Themes — authoring guide (phase 62)
A theme is a small CSS file that overrides the :root palette variables.
That is the entire mechanism — no component CSS is theme-aware, every
color in the app reads a --* variable, so a later stylesheet wins by
cascade order.
How a theme loads
- Set
BOR_THEME=<file>(a bare FILENAME, e.g.BOR_THEME=indigo.css).app/config.pyvalidates it at startup — anything not matching^[a-z0-9_-]+\.css$(a path,.., uppercase, a missing extension) refuses to boot, naming the value (the phase-56 fail-loud house style). - The value rides the existing boot fetch:
GET /api/config→frontend/assets/brand.jsinserts<link rel="stylesheet" href="/assets/themes/<file>">IMMEDIATELY AFTER thestyles.csslink — later wins the cascade. - A theme file MISSING at runtime (typo past the validator, or the file
deleted after the image was built) degrades to the built-in theme —
brand.jswarns in the console, the page never breaks (the loadHealth house style, A5). - UNSET (
BOR_THEMEempty) ⇒ no link is inserted at all — the deployment renders byte-identical to the built-in dark-tech palette. Loading is opt-in via the env var, never by directory scanning.
The variables
A theme overrides the 8 identity variables in a single :root block.
Built-in values (from frontend/assets/styles.css) for reference:
| Variable | Built-in | Role |
|---|---|---|
--bg |
#0f0a0a |
page background (text on it: --ink) |
--surface |
#1a0f0f |
cards, panels, code blocks (text on it: --ink) |
--ink |
#f0e6e6 |
primary text |
--ink-soft |
#b8a8a8 |
secondary text (5.1:1 on --surface) |
--line |
#2d1a1a |
decorative 1px borders (no contrast obligation) |
--brand |
#f43f5e |
brand accent — buttons, links (text ON it is --bg) |
--brand-soft |
#2d0a0a |
brand-tinted surface (chips, hover washes) |
--brand-ink |
#fca5a5 |
brand-tinted text (9.0:1 on --surface) |
The semantic families are deliberately NOT identity — do not
override them: --accent-* (deflection amber), --ok-* (success
green), --err-* (error red) encode states, and they are already AA
in the built-in theme. A theme that keeps them stays honest: your
indigo app still tells success from error.
Rules
- Filename:
^[a-z0-9_-]+\.css$— lowercase, bare filename, in this directory. The server validator rejects anything else at startup (naming the value), so keep the env var and the filename in lockstep. - One
:rootblock. No selectors, no@media, no other declarations — the file overrides variables and nothing else (the cascade does the rest).indigo.cssis the reference shape. - Every text/background pair ≥ 4.5:1 (AGENTS.md rule 5, WCAG 2.1
AA). The pairs that matter:
--inkon--bgand on--surface,--ink-softon--surface,--bgon--brand(the text on brand buttons is the DARK background ink — that is the pattern), and--brand-inkon--surface. - Never white-on-brand. The built-in documents the trap: white on
#f43f5eis 3.7:1 — it fails. Pick a--brandwhose luminance carries the dark--bgink at ≥ 4.5:1 (indigo.css: 6.5:1). - Keep
--lineclose to--surface(a 1px step, not a wall) — the layout reads by surfaces, not borders.
Deployment
- Dev: works immediately — the file is served from the static dir
(
frontend/,BOR_STATIC_DIR), so drop the file in, setBOR_THEME, restart uvicorn. - Container: rebuild the image. Stage 1 ships the WHOLE directory
(
cp -r ./assets/themes /out/assets/themes— no per-file esbuild), so a new or edited theme file needs no Containerfile change (A7): whatever is infrontend/assets/themes/at build time is what the image serves at/assets/themes/….