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.
209 lines
7.7 KiB
TypeScript
209 lines
7.7 KiB
TypeScript
/**
|
|
* WFDASH theme registry.
|
|
*
|
|
* Base themes inspired by real product surfaces. Each theme is just a
|
|
* set of OKLCH tokens (already defined in src/styles/themes.css via the
|
|
* `data-theme="<id>"` selector) and a pair of font CSS-var names.
|
|
*
|
|
* The picker just flips `data-theme` on <html> and `class` (`.dark` for dark
|
|
* mode). No inline-style overrides, no custom-theme builder, no
|
|
* `!important` theme-specific component rules.
|
|
*
|
|
* Standard palettes provide dark surfaces alongside their light palette.
|
|
* Natively dark palettes stay dark in either mode. Light is the default mode.
|
|
*/
|
|
|
|
import type { ThemeDefinition } from "@/lib/theme-types";
|
|
|
|
export const THEMES: ThemeDefinition[] = [
|
|
{
|
|
id: "crisp",
|
|
label: "Crisp",
|
|
tagline: "Calm neutrals with a steel-blue accent.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.625rem",
|
|
reference: "Neutral",
|
|
category: "standard",
|
|
},
|
|
{
|
|
id: "airbnb",
|
|
label: "Airbnb",
|
|
tagline: "Warm white + Rausch. Photography-led, restrained copy.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.5rem",
|
|
reference: "Airbnb",
|
|
category: "standard",
|
|
},
|
|
{
|
|
id: "airtable",
|
|
label: "Airtable",
|
|
tagline: "Soft blue + cream. Spreadsheet-friendly, calm density.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.375rem",
|
|
reference: "Airtable",
|
|
category: "standard",
|
|
},
|
|
{
|
|
id: "cursor",
|
|
label: "Cursor",
|
|
tagline: "Deep dark + violet. Editor at night, dev-tool energy.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.5rem",
|
|
reference: "Cursor",
|
|
category: "dark",
|
|
},
|
|
{
|
|
id: "supabase",
|
|
label: "Supabase",
|
|
tagline: "Deep green-black. Dense panels, generous data surface.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.5rem",
|
|
reference: "Supabase",
|
|
category: "dark",
|
|
},
|
|
/* ─────────────────────────────────────────────────────────────────────
|
|
* Brand-inspired themes (added 2026-07)
|
|
* Revolut / NVIDIA / Ant / Energetic / Artistic / MiniMax /
|
|
* Cosmic / Neon — all token-driven, no inline-style fights.
|
|
* ─────────────────────────────────────────────────────────────────── */
|
|
{
|
|
id: "revolut",
|
|
label: "Revolut",
|
|
tagline: "Soft off-black canvas + electric mint. Premium fintech.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.75rem",
|
|
reference: "Revolut",
|
|
category: "dark",
|
|
},
|
|
{
|
|
id: "nvidia",
|
|
label: "NVIDIA",
|
|
tagline: "Pure black + signature green. Engineering precision.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.375rem",
|
|
reference: "NVIDIA",
|
|
category: "dark",
|
|
},
|
|
{
|
|
id: "ant",
|
|
label: "Ant",
|
|
tagline: "Cool grey + Ant blue. Calm enterprise density.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.375rem",
|
|
reference: "Ant Group",
|
|
category: "standard",
|
|
},
|
|
{
|
|
id: "energetic",
|
|
label: "Energetic",
|
|
tagline: "Bone-white + atomic orange. Sports broadcast energy.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.5rem",
|
|
reference: "Custom",
|
|
category: "standard",
|
|
},
|
|
{
|
|
id: "artistic",
|
|
label: "Artistic",
|
|
tagline: "Warm cream + ink + magenta. Editorial gallery feel.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.5rem",
|
|
reference: "Custom",
|
|
category: "standard",
|
|
},
|
|
{
|
|
id: "MiniMax",
|
|
label: "MiniMax",
|
|
tagline: "Cool charcoal + electric violet. Modern AI product surface.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.625rem",
|
|
reference: "MiniMax",
|
|
category: "dark",
|
|
},
|
|
{
|
|
id: "cosmic",
|
|
label: "Cosmic",
|
|
tagline: "Deep indigo + electric cyan. Futuristic, immersive.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.75rem",
|
|
reference: "Custom",
|
|
category: "dark",
|
|
},
|
|
{
|
|
id: "neon",
|
|
label: "Neon",
|
|
tagline: "Pure black + magenta / cyan double-flourish. Synthwave.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.5rem",
|
|
reference: "Custom",
|
|
category: "dark",
|
|
},
|
|
/* ─────────────────────────────────────────────────────────────────────
|
|
* Brand theme added 2026-07 — Tangerine. Vendor-supplied OKLCH tokens
|
|
* mapped onto the standard WFDash picker-card hex shape. Hex values
|
|
* here are picker-only (chip + chip-soft + dark-mode preview). The
|
|
* actual on-screen colours come from themes.css via data-theme="tangerine".
|
|
* ─────────────────────────────────────────────────────────────────── */
|
|
{
|
|
id: "tangerine",
|
|
label: "Tangerine",
|
|
tagline: "Warm tangerine + cool neutral. Friendly operator surface.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.75rem",
|
|
reference: "Custom",
|
|
category: "standard",
|
|
},
|
|
/* ─────────────────────────────────────────────────────────────────────
|
|
* Lime — added 2026-07-21. Bright lime primary (#aff33e) on bone-white
|
|
* (light) / near-black slate (dark). Hex values here are picker-only;
|
|
* the on-screen tokens come from themes.css via data-theme="lime".
|
|
* ─────────────────────────────────────────────────────────────────── */
|
|
{
|
|
id: "lime",
|
|
label: "Lime",
|
|
tagline: "Fresh lime + clean neutrals. Bright, energetic surface.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "1rem",
|
|
reference: "Custom",
|
|
category: "standard",
|
|
},
|
|
/* ─────────────────────────────────────────────────────────────────────
|
|
* WFDASH — brand theme added 2026-07-26. Palette sampled directly from
|
|
* /public/wfdash-logo.png so the in-app shell reads as the same brand
|
|
* surface as the logo. Defined in src/styles/themes.css via
|
|
* `data-theme="wfdash"` (light + dark variants — dark mode matches the
|
|
* logo canvas exactly). Hex values here are picker-only; the on-screen
|
|
* tokens come from themes.css.
|
|
* ─────────────────────────────────────────────────────────────────── */
|
|
{
|
|
id: "wfdash",
|
|
label: "WFDASH",
|
|
tagline: "Brand navy and blue. The default palette.",
|
|
fontHeading: "var(--font-inter)",
|
|
fontBody: "var(--font-inter)",
|
|
radius: "0.625rem",
|
|
reference: "WFDASH",
|
|
category: "standard",
|
|
},
|
|
];
|
|
|
|
export const DEFAULT_THEME_ID = "wfdash";
|
|
|
|
export function getThemeById(id: string | null | undefined): ThemeDefinition {
|
|
return THEMES.find((t) => t.id === id) ?? THEMES.find((t) => t.id === DEFAULT_THEME_ID)!;
|
|
}
|