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'.
78 lines
4.1 KiB
Markdown
78 lines
4.1 KiB
Markdown
# 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
|
|
|
|
1. Set `BOR_THEME=<file>` (a bare FILENAME, e.g. `BOR_THEME=indigo.css`).
|
|
`app/config.py` validates 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).
|
|
2. The value rides the existing boot fetch: `GET /api/config` →
|
|
`frontend/assets/brand.js` inserts
|
|
`<link rel="stylesheet" href="/assets/themes/<file>">` IMMEDIATELY
|
|
AFTER the `styles.css` link — later wins the cascade.
|
|
3. 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.js` warns in the console, the page never breaks (the
|
|
loadHealth house style, A5).
|
|
4. UNSET (`BOR_THEME` empty) ⇒ 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 `:root` block.** No selectors, no `@media`, no other
|
|
declarations — the file overrides variables and nothing else (the
|
|
cascade does the rest). `indigo.css` is the reference shape.
|
|
- **Every text/background pair ≥ 4.5:1** (AGENTS.md rule 5, WCAG 2.1
|
|
AA). The pairs that matter: `--ink` on `--bg` and on `--surface`,
|
|
`--ink-soft` on `--surface`, `--bg` on `--brand` (the text on brand
|
|
buttons is the DARK background ink — that is the pattern), and
|
|
`--brand-ink` on `--surface`.
|
|
- **Never white-on-brand.** The built-in documents the trap: white on
|
|
`#f43f5e` is 3.7:1 — it fails. Pick a `--brand` whose luminance
|
|
carries the dark `--bg` ink at ≥ 4.5:1 (indigo.css: 6.5:1).
|
|
- Keep `--line` close 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, set
|
|
`BOR_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 in `frontend/assets/themes/` at build time is what the
|
|
image serves at `/assets/themes/…`.
|