theme:lint only reported a total and exited 0, skipped whole files by name and never ran in CI. It now counts per file under src/ against scripts/theme-baseline.json, which only shrinks, like lint:rtl: the colour literals, arbitrary colour classes and numeric font sizes it already saw, plus palette classes (bg-gray-100, text-indigo-600, bg-white), raw text sizes (text-sm, text-[13px]) instead of the type tiers, z-index literals above z-50 instead of a step of the layering scale, and reads of the dark_mode setting. The file exemptions are gone; the map, PDF and brand files carry their count in the baseline instead. theme-lint-disable markers stay valid in a comment and are counted against scripts/theme-disable-baseline.json, like rtl-lint-disable in lint:rtl; a new one raises its file's entry by hand, where review sees it. The comment handling lint:rtl already had moves into scripts/lib/source.mjs so both read source the same way. The check runs in the client-quality job.
Appearance & theming
TREK's look is driven by design tokens (CSS custom properties) so a single per-user config can re-skin the whole app — color scheme, accent, transparency, text size, density — without touching component code. New pages get this for free if they follow the contract below.
How it works
- The per-user config is one validated blob (
AppearanceConfigin@trek/shared), stored under theappearancesettings key. applyAppearance()(src/theme/applyAppearance.ts) is the only code that writes styling to the DOM: it toggles.dark, setsdata-scheme/data-no-transparency/data-density/data-reduce-motionon<html>, and writes the custom-accent + type-scale CSS variables.- A render-blocking
public/theme-boot.jsreplays a cached snapshot before first paint to avoid FOUC (external classic script — the prod CSP blocks inline scripts). - Schemes are data: swatch metadata in
src/theme/schemes.ts, token values in the[data-scheme="…"]blocks insrc/index.css.
Token taxonomy
| Purpose | Token / utility |
|---|---|
| Surfaces | --bg-primary/secondary/tertiary/elevated/card/input/hover/selected · bg-surface* |
| Text | --text-primary/secondary/muted/faint · text-content* |
| Borders | --border-primary/secondary/faint · border-edge* |
| Accent | --accent (fill) · --accent-text (on fill) · --accent-on (on surface) · --accent-hover · --accent-subtle · bg-accent / text-accent-on / bg-accent-subtle |
| Status | --success/-soft, --danger/-soft, --warning/-soft, --info/-soft · bg-danger-soft text-danger etc. |
| Elevation | --shadow-sm/md/lg/card/elevated/modal/dropdown/popover · shadow-modal etc. |
| Overlay / inverse | --overlay · --bg-inverse / --text-inverse · bg-inverse text-inverse-text |
| Type tiers | text-title / text-subtitle / text-body / text-caption (scale with the user's per-tier multipliers) |
Layering
Fixed and sticky surfaces read their stacking order from one scale in
src/index.css, so two of them cannot end up in the wrong order by accident.
| Token | Value | For |
|---|---|---|
--z-bar |
60 | BottomNav, page-bound sticky bars |
--z-nav |
200 | the desktop navbar |
--z-modal |
10000 | shared/Modal, ConfirmDialog |
--z-overlay |
99990 | page-owned full-screen overlays |
--z-notice |
99998 | app-wide system and release notices |
--z-toast |
100000 | toasts, tooltips, context menus |
A new fixed surface docks onto the step it belongs to (z-[var(--z-modal)] as a
class, z-index: var(--z-modal) in CSS) instead of inventing a number. Older
files still carry literal values; the steps are far enough apart that those keep
their current position until the file gets converted.
The contract (enforced by npm run theme:lint)
- Surfaces/text/borders use the semantic utilities (
bg-surface*,text-content*,border-edge*) orvar(--token)— never rawbg-slate-*/text-gray-*/bg-white. - The primary/"black" action look uses
bg-accent+text-accent-text(+ring-accent) — neverbg-slate-900/bg-black/bg-indigo-*. This is what lets a user's accent reach new surfaces automatically. - Semantic text size uses the tier utilities
text-title/subtitle/body/caption— never an inlinefontSize: <px>, never rawtext-sm/text-xsor arbitrarytext-[13px]to mean a tier. - Translucent/blurred surfaces consume an alpha/glass token
(
--bg-elevated,--tooltip-bg,--glass-*) or abackdrop-blurutility, so transparency-off can neutralize them centrally — never inlinergba()backgrounds orbackdrop-filterin JSX. - Components never read
AppearanceConfigto compute styles. OnlyapplyAppearancereads the config and writes tokens; components read tokens. Dark state is read off the.darkclass, never duplicated into React state. - New appearance dimensions are added to
AppearanceConfig+applyAppearance+schemes.ts— never a second applier or a parallel token family. New feature palettes derive their accent fromvar(--accent).
Allowed to stay inline / literal
Genuinely dynamic values (data-driven colors like cat.color, computed
geometry/transforms/sizes), and the surfaces CSS variables can't reach: injected
map popup/marker HTML, Mapbox/MapLibre paint, and the standalone @react-pdf
documents. Mark intentional exceptions with a theme-lint-disable line comment.
The markers are counted per file against client/scripts/theme-disable-baseline.json,
like rtl-lint-disable in lint:rtl, so an exception is a decision a reviewer
sees rather than a way around the check. --update only lowers that file: a new
marker for a surface the tokens really cannot reach is added by raising (or
adding) its file's entry by hand in the same change, where review can see it.