feat(appearance): rename default theme, add real brand-accurate Stalwart theme

The color theme previously named "Stalwart" was actually just the
neutral black/white default — it never used Stalwart's own brand
color. Renamed it to "Default" (ColorTheme 'stalwart' -> 'default'
value; label/i18n key updated) and added a genuinely new "Stalwart"
theme in its place.

Colors sourced directly from the official site's own design tokens
(D:\project\stalwart_website/src/styles/tokens.css): brand red/pink
#db2d54 (light primary/ring) and #ff4570 (--brand-hi, dark
primary/ring), converted to oklch via the sRGB->OKLab->OKLCH matrix
transform to match this project's existing token format. Neutrals use
near-true-gray values (also converted from the site's actual light/
dark backgrounds and text colors) rather than being re-hued toward the
brand color like the other theme presets — matching how the real site
treats color (saturated only on the accent, neutral everywhere else).

Verified in the running dev server: Default reverts to the original
neutral look, Stalwart renders the brand red across primary buttons,
active nav state, checkboxes, and badges in both light and dark mode.
No regression (243 tests, typecheck, lint all pass).
This commit is contained in:
Steven RYDELL
2026-08-01 23:02:32 +02:00
parent 410a01700b
commit 637d8bb286
3 changed files with 57 additions and 5 deletions
+1
View File
@@ -17,6 +17,7 @@
"switchToLight": "Switch to light mode",
"theme": {
"amber": "Amber",
"default": "Default",
"forest": "Forest",
"ocean": "Ocean",
"rose": "Rose",
+46 -1
View File
@@ -100,11 +100,56 @@
/* Color themes. Each theme re-hues the neutral tokens at the same lightness
and chroma as the default palette (contrast is preserved) and replaces
primary/ring with the theme color. The default "Stalwart" theme is the
primary/ring with the theme color. The neutral "Default" theme is the
bare :root/.dark above and needs no data-theme attribute. Dark theme
blocks must stay after their light counterpart: both selectors have the
same specificity, so source order decides. */
/* "Stalwart" theme: matches the brand colors from stalw.art's own site
(src/styles/tokens.css there) — brand red #db2d54/#ff4570 converted to
oklch, neutrals kept close to true gray (unlike the other theme
presets below, which lightly re-hue their neutrals) since that's what
the real site does: saturated color only on the accent, not the
backdrop. */
:root[data-theme='stalwart'] {
--foreground: oklch(0.147 0.009 285.3);
--content-background: oklch(0.974 0.003 286.4);
--primary: oklch(0.586 0.207 14.6);
--primary-foreground: oklch(0.985 0.002 285.3);
--secondary: oklch(0.965 0.005 285.3);
--secondary-foreground: oklch(0.205 0.017 285.3);
--muted: oklch(0.93 0.005 285.3);
--muted-foreground: oklch(0.431 0.016 285.7);
--accent: oklch(0.965 0.005 285.3);
--accent-foreground: oklch(0.205 0.017 285.3);
--border: oklch(0.926 0.005 286.3);
--input: oklch(0.926 0.005 286.3);
--ring: oklch(0.586 0.207 14.6);
--chart-1: 350 65% 52%;
}
.dark[data-theme='stalwart'] {
--background: oklch(0.136 0.007 285.5);
--foreground: oklch(0.968 0.003 286.4);
--card: oklch(0.19 0.012 285.2);
--card-foreground: oklch(0.968 0.003 286.4);
--popover: oklch(0.221 0.016 285.1);
--popover-foreground: oklch(0.968 0.003 286.4);
--content-background: oklch(0.11 0.006 285.5);
--primary: oklch(0.672 0.221 12.3);
--primary-foreground: oklch(0.15 0.03 14.6);
--secondary: oklch(0.256 0.019 285);
--secondary-foreground: oklch(0.968 0.003 286.4);
--muted: oklch(0.256 0.019 285);
--muted-foreground: oklch(0.719 0.014 286);
--accent: oklch(0.28 0.02 285.1);
--accent-foreground: oklch(0.968 0.003 286.4);
--border: oklch(0.272 0.017 285.2);
--input: oklch(0.272 0.017 285.2);
--ring: oklch(0.672 0.221 12.3);
--chart-1: 350 80% 65%;
}
:root[data-theme='ocean'] {
--foreground: oklch(0.145 0.02 264);
--content-background: oklch(0.965 0.008 264);
+10 -4
View File
@@ -8,7 +8,7 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type Theme = 'light' | 'dark';
export type ColorTheme = 'stalwart' | 'ocean' | 'forest' | 'violet' | 'rose' | 'amber' | 'teal';
export type ColorTheme = 'default' | 'stalwart' | 'ocean' | 'forest' | 'violet' | 'rose' | 'amber' | 'teal';
export type Radius = 'rounded' | 'square';
interface UIState {
@@ -35,8 +35,8 @@ function applyThemeClass(theme: Theme) {
}
function applyColorThemeAttribute(colorTheme: ColorTheme) {
// The default theme is defined directly on :root/.dark, so it needs no attribute.
if (colorTheme === 'stalwart') {
// The neutral "Default" theme is defined directly on :root/.dark, so it needs no attribute.
if (colorTheme === 'default') {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.dataset.theme = colorTheme;
@@ -48,11 +48,17 @@ function applyRadiusAttribute(radius: Radius) {
}
export const COLOR_THEMES: { value: ColorTheme; labelKey: string; fallback: string; swatch: string }[] = [
{
value: 'default',
labelKey: 'appearance.theme.default',
fallback: 'Default',
swatch: 'linear-gradient(135deg, oklch(0.205 0.017 285.823) 50%, oklch(0.92 0.007 285.823) 50%)',
},
{
value: 'stalwart',
labelKey: 'appearance.theme.stalwart',
fallback: 'Stalwart',
swatch: 'linear-gradient(135deg, oklch(0.205 0.017 285.823) 50%, oklch(0.92 0.007 285.823) 50%)',
swatch: 'oklch(0.586 0.207 14.6)',
},
{ value: 'ocean', labelKey: 'appearance.theme.ocean', fallback: 'Ocean', swatch: 'oklch(0.546 0.215 262.9)' },
{ value: 'forest', labelKey: 'appearance.theme.forest', fallback: 'Forest', swatch: 'oklch(0.527 0.137 150.1)' },