feat(web): customizable placeholder, footer text, and color theme via BOR_* env vars
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'.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# 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/…`.
|
||||
Reference in New Issue
Block a user