The theme registry carried a hand-maintained set of hex swatches for
every palette, and they had drifted away from the CSS the app actually
paints. The picker previewed colours the product never showed.
The registry now holds only what cannot be read from CSS — id, label,
tagline, fonts, radius — and the picker, the Settings swatches and the
preview cards resolve --background, --primary and --foreground off the
theme itself. A preview can no longer disagree with the screen it
previews.
themes.css is rewritten so every palette carries a real dark surface,
and the palettes that are dark by nature stay dark whichever way the
mode switch is set. The pre-hydration script validates the stored theme
id against the registry, so a retired id falls back to WFDASH instead of
painting an unstyled page, and the old "warm" mode migrates to Light.
The legacy text-{warn,success,info}-foreground classes now resolve to
the semantic text colour and switch to the filled ink only on an exact
solid fill, so one class reads correctly on both a tinted badge and a
filled button. A solid focus outline replaces the faint ring, and the
charts and quote status badges take their colours from tokens rather
than hard-coded dark strokes.
Tests: theme-bootstrap.test.tsx covers saved-theme recovery and the
storage-blocked path; e2e/themes.spec.ts walks every palette in both
modes checking surface contrast, tinted and solid semantic pairs, focus
colour, chart tokens and the destructive hover, and asserts that the
picker preview matches the applied theme.
3.3 KiB
WFDASH themes
The current theme picker is Settings → Appearance (/settings). The historical
themes.html mockup in this directory predates the current registry and is not a
preview of the shipping palettes.
src/lib/themes.ts lists the selectable themes. src/styles/themes.css is the
single source for their colours; picker cards and the About swatches use those
same CSS tokens rather than maintaining a second set of hex approximations.
data-theme="<id>"selects a palette on<html>.light/darkclasses select the mode throughnext-themes.- Choices persist per browser under
wfdash-themeandwfdash-mode. - The default is WFDASH + Light. Old
warmmodes migrate tolightand retired or invalid theme IDs fall back to WFDASH before hydration. - Standard palettes provide both light and dark surfaces. The palettes marked
category: "dark"are intentionally dark in either mode. - Preview elements scope the same tokens through
.theme-preview[data-theme]; their mode follows the mode selected in Settings.
Adding or adjusting a palette
- Add its metadata to
THEMESinsrc/lib/themes.ts. Preserve existing IDs;MiniMaxis intentionally case-sensitive for saved preferences. - Define its tokens in
src/styles/themes.cssusing:is(:root, .theme-preview)[data-theme="<id>"]. For a standard palette, also define a.darkvariant with sufficient specificity to override its light surface. A bare.darkselector loses to the base palette selector. - Define background/foreground pairs, muted text, semantic status colours,
input/focus, sidebar, radius and fonts. Shared chart and AI tokens have
defaults in
globals.css. AI purple must follow the actual surface: include any new natively dark palette in the dark AI selector there. Legacytext-{warn,success,info}-foregroundutilities adapt to tinted/plain text and exact solid fill classes; new tinted badges should use the semantic text class directly. These custom text utilities replace the generic Tailwind aliases to avoid duplicate declarations. The inverse info background/border utilities remain available for selected-customer checkmark bubbles. - Keep business meaning consistent: success green, warnings amber/orange, destructive red, price checks blue, AI purple. Foreground tokens accompany solid fills; semantic text colours must also be readable on tinted badges.
- Verify in the actual app. The login page and business chart series retain their intentional brand/data colours; chart surfaces use theme tokens.
Verification
npm run test:run -- src/components/theme-bootstrap.test.tsx
npm run e2e -- e2e/themes.spec.ts
The browser matrix checks all registered themes in both modes for dark surfaces, text contrast, filled and tinted semantic pairs, primary button hover labels, focus-colour contrast and chart token coverage. It uses the shared CSS from the public login route, so this part requires no PocketBase fixture.
For authenticated preview/persistence and dialog/mobile checks, supply
E2E_TEST_EMAIL / E2E_TEST_PASSWORD, or a Playwright E2E_STORAGE_STATE file
for the test origin. These checks skip when no browser fixture is supplied.
Use isolated browser storage; never commit credentials or storage-state files.