4.3 KiB
Themes
Three themes ship, and the same palettes drive the diagrams as well as the
pages. soleprint is the default; the others are switched to on purpose.
| Theme | Reads as | For |
|---|---|---|
soleprint |
dark, rounded, amber | the default — tools and dev chrome |
mcrn |
dark, square, monospace, orange glow | the terminal look, matched to mariano.mcrn.ar |
lucid |
light, hairline, printable | regulated documents, and sitting beside a real lucid.app export |
Switch with ?theme=lucid, or the toggle in the corner. The choice persists.
Where it lives
soleprint/common/theme/
tokens.css the vocabulary + neutral defaults
themes/*.css one file per theme
theme.js resolve, apply, remember
bake.py inline the defaults into pages
Served together at /theme.css — tokens first, then every theme, each scoped to
[data-theme="..."]. Adding a theme is adding a file: run.py lists the
directory rather than carrying a hardcoded list.
Two naming families, one set of values
Both are answered, because both were already in use and renaming across a dozen templates would have been the larger change:
--bg/--surface/--border/--text/--muted/--accent— the station tools and the docs site--surface-0..3/--text-primary/--panel-radius—common/ui's Vue components
The second family is derived from the first in tokens.css, so a page using
either name gets the same colour and a theme author fills in one set.
Order matters
<!-- baked defaults --> <style>:root { … }</style>
<link rel="stylesheet" href="/theme.css">
<style> …the page's own rules… </style>
The theme has to load before the page's styles, so its element defaults
underpin the page rather than override it. Get this backwards and
tokens.css's body { background: var(--bg) } flattens whatever the page
wanted — which is exactly how artery, atlas and station briefly lost their
coloured content columns.
Baked defaults, and why
/theme.css is an absolute path, and soleprint is not always at the root — in a
room's nginx it sits under /spr/ while location / goes to the frontend. A
page that says background: var(--bg) and never receives --bg does not fall
back to something plainer: the declaration is invalid at computed-value time, so
the background goes transparent and the text goes initial-black on a design that
assumed dark. Unstyled, not merely unbranded.
So every page carries a generated :root block before the link. Both are
:root, so document order decides: the served stylesheet wins when it loads,
and the baked block is what is left when it does not.
python3 common/theme/bake.py # regenerate
python3 common/theme/bake.py --check # fail if a page is stale
Only the variables a page actually uses are emitted, so the blocks stay small.
No webfonts
The stacks name faces that exist on the target rather than fetching any:
--font-ui: "Segoe UI", Inter, system-ui, -apple-system, Arial, sans-serif;
--font-mono: "Cascadia Mono", "JetBrains Mono", Consolas, "SF Mono", monospace;
Segoe UI and Consolas ship with Windows. A regulated network blocks
fonts.googleapis.com and file:// stalls on it, and neither failure looks
like a missing font — they look like a broken page.
Diagrams follow
docs/graphs/themes/*.gvpr carry the same palettes for graphviz, so a diagram
and the page around it are one visual language. See
the graphs README
and Export / Compile.
make docs graphs # every graph, every theme
Diagrams are baked per theme rather than styled by CSS, because the docs embed
them with <img src=…> — which makes the SVG a separate document the page's
stylesheet cannot reach. A plexus that inlines the SVG can style it live,
and the bundle plexus does exactly that.
Contrast
lucid is the first light theme, and the palette was measured rather than
guessed — against both #ffffff and the #f5f7fa panel, at the sizes actually
used. --dim drives 11px notes and --status-warn drives 10px labels, so both
need 4.5:1 rather than the 3:1 large text gets away with. The obvious lighter
greys came in at 3.4–3.8 and were dropped for that reason.