From d59b3aec389db74ac272ae96035db6ef174447c7 Mon Sep 17 00:00:00 2001 From: Hugh Kaznowski Date: Sun, 6 Sep 2026 09:37:21 +0100 Subject: [PATCH] Add light variants for red, blue, yellow, pink, orange colour schemes Extends ticket 048's light_green pattern (light-inverted surface/border/text scale + light's neutral danger colours, same base and accent hues as the dark-surfaced counterpart) to the remaining five colour schemes across all six files that must stay in sync: index.css, themes.ts, api.ts, settings.rs, useSettingsPanel.ts, and NavMenu.tsx. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o --- src/settings.rs | 32 ++++ ...t-variants-for-remaining-colour-schemes.md | 77 --------- webapp/src/components/NavMenu.tsx | 5 + webapp/src/index.css | 160 ++++++++++++++++++ webapp/src/lib/api.ts | 5 + webapp/src/lib/themes.ts | 120 +++++++++++++ webapp/src/lib/useSettingsPanel.ts | 5 + 7 files changed, 327 insertions(+), 77 deletions(-) delete mode 100644 tickets/056-light-variants-for-remaining-colour-schemes.md diff --git a/src/settings.rs b/src/settings.rs index 98774c2..13fe719 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -40,6 +40,21 @@ pub enum ColorScheme { /// Brighter, light-surfaced counterpart to `Green` (ticket 048). #[serde(rename = "light_green")] LightGreen, + /// Brighter, light-surfaced counterpart to `Red` (ticket 056). + #[serde(rename = "light_red")] + LightRed, + /// Brighter, light-surfaced counterpart to `Blue` (ticket 056). + #[serde(rename = "light_blue")] + LightBlue, + /// Brighter, light-surfaced counterpart to `Yellow` (ticket 056). + #[serde(rename = "light_yellow")] + LightYellow, + /// Brighter, light-surfaced counterpart to `Pink` (ticket 056). + #[serde(rename = "light_pink")] + LightPink, + /// Brighter, light-surfaced counterpart to `Orange` (ticket 056). + #[serde(rename = "light_orange")] + LightOrange, #[default] System, } @@ -425,6 +440,23 @@ mod tests { assert_eq!(reloaded, settings); } + #[test] + fn light_variant_color_schemes_round_trip_through_serde() { + for (scheme, rename) in [ + (ColorScheme::LightGreen, "light_green"), + (ColorScheme::LightRed, "light_red"), + (ColorScheme::LightBlue, "light_blue"), + (ColorScheme::LightYellow, "light_yellow"), + (ColorScheme::LightPink, "light_pink"), + (ColorScheme::LightOrange, "light_orange"), + ] { + let json = serde_json::to_string(&scheme).unwrap(); + assert_eq!(json, format!("\"{rename}\"")); + let deserialized: ColorScheme = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized, scheme); + } + } + #[test] fn legacy_grey_color_scheme_deserializes_as_dark() { let settings: Settings = serde_json::from_str( diff --git a/tickets/056-light-variants-for-remaining-colour-schemes.md b/tickets/056-light-variants-for-remaining-colour-schemes.md deleted file mode 100644 index d331bdd..0000000 --- a/tickets/056-light-variants-for-remaining-colour-schemes.md +++ /dev/null @@ -1,77 +0,0 @@ -# 056 — Add `Light *` variants for the remaining colour schemes - -## Summary - -Ticket 048 already added one light-surfaced variant, `light_green` -(`webapp/src/lib/themes.ts:231-254`, commit `5371d3e`: "Adds a new -'Light Green' scheme (light-surfaced, same green/magenta hue family as -'Green') selectable alongside the existing built-ins"). The wishlist -comment that prompted 048 — "we need a `Light Green` alongside `Green`" -— is now clarified as applying to *every* colour scheme, not just green: -the current built-in set is `dark`, `light`, `red`, `green`, `blue`, -`yellow`, `pink`, `orange`, `light_green` -(`webapp/src/lib/themes.ts:38-255`), so `red`, `blue`, `yellow`, `pink`, -and `orange` still have no light-surfaced counterpart. - -Adding a scheme is not fully data-driven from `themes.ts` alone — six -files must be kept in sync per scheme, per the no-build-step-links-them -comment at `webapp/src/index.css:1-7`: - -1. `webapp/src/index.css:264-300` — the `html.light_green` CSS block is - the reference recipe: same hue + same accent hues as the dark - counterpart, but with `light`'s inverted (high-lightness) - surface/border/text scale, and `light`'s hue-neutral danger colors - (comment at `index.css:264-268` documents this explicitly). -2. `webapp/src/lib/themes.ts:231-254` — the matching `BUILTIN_THEMES` - entry (used for CSS export/download and validation). -3. `webapp/src/lib/api.ts:83-93` — the `ColorScheme` TS union type. -4. `src/settings.rs:30-42` — the matching Rust `ColorScheme` enum variant, - with `#[serde(rename = "light_")]`. -5. `webapp/src/lib/useSettingsPanel.ts:24-34` — `THEME_CLASSES`, used to - apply/strip the `html.*` class. -6. `webapp/src/components/NavMenu.tsx:20-31` — `COLOR_SCHEMES`, which - drives the picker UI; `schemeLabel()` (`NavMenu.tsx:34-35`) already - auto-titles `light_red` → "Light Red" by replacing underscores, so no - extra label wiring is needed once a name is added to this array. - -`PanelChart.tsx`'s multi-accent `color-mix()` series-color logic (touched -by ticket 048) is generic per-theme rather than per-scheme-name, so it -likely needs no changes — worth a quick check while implementing, not -expected to require edits. - -Tickets 047/048's original files were deleted on implementation (git -history only shows their removal commits, `5cb2dfd`/`7c331e7`; bodies -aren't recoverable), but commit `5371d3e`'s message and the resulting -`light_green` block are sufficient as the template. - -## Scope - -- Add `light_red`, `light_blue`, `light_yellow`, `light_pink`, and - `light_orange` following the exact `light_green` recipe: same base - hue and accent-hue choices as the corresponding dark-surfaced scheme, - `light`'s inverted surface/border/text lightness scale, `light`'s - neutral danger colors. -- Update all six files listed above for each new scheme: - `webapp/src/index.css`, `webapp/src/lib/themes.ts`, - `webapp/src/lib/api.ts`, `src/settings.rs`, - `webapp/src/lib/useSettingsPanel.ts`, `webapp/src/components/NavMenu.tsx`. -- Verify `schemeLabel()` produces correct titles ("Light Red", "Light - Blue", etc.) without additional label-mapping code. -- Spot-check `PanelChart.tsx`'s series-color logic against one new light - scheme to confirm it needs no changes (or fix it if it does). - -## Acceptance criteria - -- All five new schemes are selectable in the settings picker - (`NavMenu.tsx`), persist correctly, and are downloadable as CSS via - the existing "download built-in theme" path. -- Each new scheme's CSS block round-trips through `Settings` - serialization/deserialization (`src/settings.rs`) without errors, - verified with a test analogous to any existing `ColorScheme` - serialization test. -- `make build` and `make lint` pass with the new schemes added. - -## Depends on - -048 (implemented; this ticket extends its established pattern to the -remaining base hues). diff --git a/webapp/src/components/NavMenu.tsx b/webapp/src/components/NavMenu.tsx index c9e491e..c7a829c 100644 --- a/webapp/src/components/NavMenu.tsx +++ b/webapp/src/components/NavMenu.tsx @@ -28,6 +28,11 @@ const COLOR_SCHEMES: ColorScheme[] = [ 'pink', 'orange', 'light_green', + 'light_red', + 'light_blue', + 'light_yellow', + 'light_pink', + 'light_orange', ] /** Display label for a `ColorScheme`: underscores become spaces so `capitalize` (which titles each word) renders e.g. "light_green" as "Light Green". */ diff --git a/webapp/src/index.css b/webapp/src/index.css index fa2a231..a093ba0 100644 --- a/webapp/src/index.css +++ b/webapp/src/index.css @@ -296,6 +296,166 @@ html.light_green { --c-accent-3: hsl(50 70% 38%); } +/* Brighter, light-surfaced counterpart to `red` (ticket 056): same base hue + (0) and accent hues as `red`, with `light`'s inverted surface/border/text + scale and `light`'s hue-neutral danger colours. */ +html.light_red { + --c-surface-0: hsl(0 40% 96%); + --c-surface-1: hsl(0 35% 92%); + --c-surface-2: hsl(0 30% 85%); + + --c-border: hsl(0 25% 80%); + --c-border-soft: hsl(0 22% 70%); + --c-border-faint: hsl(0 35% 92%); + --c-border-strong: hsl(0 20% 40%); + + --c-text: hsl(0 30% 12%); + --c-text-secondary: hsl(0 20% 25%); + --c-text-muted: hsl(0 15% 40%); + --c-text-subtle: hsl(0 10% 50%); + --c-text-faint: hsl(0 10% 65%); + --c-text-faintest: hsl(0 12% 80%); + + --c-danger-bg: #fee2e2; + --c-danger-border: #fca5a5; + --c-danger-text: #b91c1c; + --c-danger-text-strong: #991b1b; + + --c-accent: hsl(180 70% 40%); + --c-accent-soft: hsl(150 60% 85%); + --c-accent-text: hsl(180 75% 28%); + --c-accent-2: hsl(90 70% 45%); + --c-accent-3: hsl(270 70% 38%); +} + +/* Brighter, light-surfaced counterpart to `blue` (ticket 056): same base hue + (210) and accent hues as `blue`, with `light`'s inverted surface/border/text + scale and `light`'s hue-neutral danger colours. */ +html.light_blue { + --c-surface-0: hsl(210 40% 96%); + --c-surface-1: hsl(210 35% 92%); + --c-surface-2: hsl(210 30% 85%); + + --c-border: hsl(210 25% 80%); + --c-border-soft: hsl(210 22% 70%); + --c-border-faint: hsl(210 35% 92%); + --c-border-strong: hsl(210 20% 40%); + + --c-text: hsl(210 30% 12%); + --c-text-secondary: hsl(210 20% 25%); + --c-text-muted: hsl(210 15% 40%); + --c-text-subtle: hsl(210 10% 50%); + --c-text-faint: hsl(210 10% 65%); + --c-text-faintest: hsl(210 12% 80%); + + --c-danger-bg: #fee2e2; + --c-danger-border: #fca5a5; + --c-danger-text: #b91c1c; + --c-danger-text-strong: #991b1b; + + --c-accent: hsl(30 70% 40%); + --c-accent-soft: hsl(0 60% 85%); + --c-accent-text: hsl(30 75% 28%); + --c-accent-2: hsl(300 70% 45%); + --c-accent-3: hsl(120 70% 38%); +} + +/* Brighter, light-surfaced counterpart to `yellow` (ticket 056): same base + hue (50) and accent hues as `yellow`, with `light`'s inverted + surface/border/text scale and `light`'s hue-neutral danger colours. */ +html.light_yellow { + --c-surface-0: hsl(50 40% 96%); + --c-surface-1: hsl(50 35% 92%); + --c-surface-2: hsl(50 30% 85%); + + --c-border: hsl(50 25% 80%); + --c-border-soft: hsl(50 22% 70%); + --c-border-faint: hsl(50 35% 92%); + --c-border-strong: hsl(50 20% 40%); + + --c-text: hsl(50 30% 12%); + --c-text-secondary: hsl(50 20% 25%); + --c-text-muted: hsl(50 15% 40%); + --c-text-subtle: hsl(50 10% 50%); + --c-text-faint: hsl(50 10% 65%); + --c-text-faintest: hsl(50 12% 80%); + + --c-danger-bg: #fee2e2; + --c-danger-border: #fca5a5; + --c-danger-text: #b91c1c; + --c-danger-text-strong: #991b1b; + + --c-accent: hsl(230 70% 40%); + --c-accent-soft: hsl(200 60% 85%); + --c-accent-text: hsl(230 75% 28%); + --c-accent-2: hsl(140 70% 45%); + --c-accent-3: hsl(320 70% 38%); +} + +/* Brighter, light-surfaced counterpart to `pink` (ticket 056): same base hue + (330) and accent hues as `pink`, with `light`'s inverted surface/border/text + scale and `light`'s hue-neutral danger colours. */ +html.light_pink { + --c-surface-0: hsl(330 40% 96%); + --c-surface-1: hsl(330 35% 92%); + --c-surface-2: hsl(330 30% 85%); + + --c-border: hsl(330 25% 80%); + --c-border-soft: hsl(330 22% 70%); + --c-border-faint: hsl(330 35% 92%); + --c-border-strong: hsl(330 20% 40%); + + --c-text: hsl(330 30% 12%); + --c-text-secondary: hsl(330 20% 25%); + --c-text-muted: hsl(330 15% 40%); + --c-text-subtle: hsl(330 10% 50%); + --c-text-faint: hsl(330 10% 65%); + --c-text-faintest: hsl(330 12% 80%); + + --c-danger-bg: #fee2e2; + --c-danger-border: #fca5a5; + --c-danger-text: #b91c1c; + --c-danger-text-strong: #991b1b; + + --c-accent: hsl(150 70% 40%); + --c-accent-soft: hsl(120 60% 85%); + --c-accent-text: hsl(150 75% 28%); + --c-accent-2: hsl(60 70% 45%); + --c-accent-3: hsl(240 70% 38%); +} + +/* Brighter, light-surfaced counterpart to `orange` (ticket 056): same base + hue (30) and accent hues as `orange`, with `light`'s inverted + surface/border/text scale and `light`'s hue-neutral danger colours. */ +html.light_orange { + --c-surface-0: hsl(30 40% 96%); + --c-surface-1: hsl(30 35% 92%); + --c-surface-2: hsl(30 30% 85%); + + --c-border: hsl(30 25% 80%); + --c-border-soft: hsl(30 22% 70%); + --c-border-faint: hsl(30 35% 92%); + --c-border-strong: hsl(30 20% 40%); + + --c-text: hsl(30 30% 12%); + --c-text-secondary: hsl(30 20% 25%); + --c-text-muted: hsl(30 15% 40%); + --c-text-subtle: hsl(30 10% 50%); + --c-text-faint: hsl(30 10% 65%); + --c-text-faintest: hsl(30 12% 80%); + + --c-danger-bg: #fee2e2; + --c-danger-border: #fca5a5; + --c-danger-text: #b91c1c; + --c-danger-text-strong: #991b1b; + + --c-accent: hsl(210 70% 40%); + --c-accent-soft: hsl(180 60% 85%); + --c-accent-text: hsl(210 75% 28%); + --c-accent-2: hsl(120 70% 45%); + --c-accent-3: hsl(300 70% 38%); +} + @theme inline { --color-surface-0: var(--c-surface-0); --color-surface-1: var(--c-surface-1); diff --git a/webapp/src/lib/api.ts b/webapp/src/lib/api.ts index 7832faf..21d348b 100644 --- a/webapp/src/lib/api.ts +++ b/webapp/src/lib/api.ts @@ -91,6 +91,11 @@ export type ColorScheme = | 'pink' | 'orange' | 'light_green' + | 'light_red' + | 'light_blue' + | 'light_yellow' + | 'light_pink' + | 'light_orange' export interface Settings { color_scheme: ColorScheme diff --git a/webapp/src/lib/themes.ts b/webapp/src/lib/themes.ts index 98a00f7..1c11251 100644 --- a/webapp/src/lib/themes.ts +++ b/webapp/src/lib/themes.ts @@ -252,6 +252,126 @@ export const BUILTIN_THEMES: Record = { '--c-accent-2': 'hsl(230 70% 45%)', '--c-accent-3': 'hsl(50 70% 38%)', }, + light_red: { + '--c-surface-0': 'hsl(0 40% 96%)', + '--c-surface-1': 'hsl(0 35% 92%)', + '--c-surface-2': 'hsl(0 30% 85%)', + '--c-border': 'hsl(0 25% 80%)', + '--c-border-soft': 'hsl(0 22% 70%)', + '--c-border-faint': 'hsl(0 35% 92%)', + '--c-border-strong': 'hsl(0 20% 40%)', + '--c-text': 'hsl(0 30% 12%)', + '--c-text-secondary': 'hsl(0 20% 25%)', + '--c-text-muted': 'hsl(0 15% 40%)', + '--c-text-subtle': 'hsl(0 10% 50%)', + '--c-text-faint': 'hsl(0 10% 65%)', + '--c-text-faintest': 'hsl(0 12% 80%)', + '--c-danger-bg': '#fee2e2', + '--c-danger-border': '#fca5a5', + '--c-danger-text': '#b91c1c', + '--c-danger-text-strong': '#991b1b', + '--c-accent': 'hsl(180 70% 40%)', + '--c-accent-soft': 'hsl(150 60% 85%)', + '--c-accent-text': 'hsl(180 75% 28%)', + '--c-accent-2': 'hsl(90 70% 45%)', + '--c-accent-3': 'hsl(270 70% 38%)', + }, + light_blue: { + '--c-surface-0': 'hsl(210 40% 96%)', + '--c-surface-1': 'hsl(210 35% 92%)', + '--c-surface-2': 'hsl(210 30% 85%)', + '--c-border': 'hsl(210 25% 80%)', + '--c-border-soft': 'hsl(210 22% 70%)', + '--c-border-faint': 'hsl(210 35% 92%)', + '--c-border-strong': 'hsl(210 20% 40%)', + '--c-text': 'hsl(210 30% 12%)', + '--c-text-secondary': 'hsl(210 20% 25%)', + '--c-text-muted': 'hsl(210 15% 40%)', + '--c-text-subtle': 'hsl(210 10% 50%)', + '--c-text-faint': 'hsl(210 10% 65%)', + '--c-text-faintest': 'hsl(210 12% 80%)', + '--c-danger-bg': '#fee2e2', + '--c-danger-border': '#fca5a5', + '--c-danger-text': '#b91c1c', + '--c-danger-text-strong': '#991b1b', + '--c-accent': 'hsl(30 70% 40%)', + '--c-accent-soft': 'hsl(0 60% 85%)', + '--c-accent-text': 'hsl(30 75% 28%)', + '--c-accent-2': 'hsl(300 70% 45%)', + '--c-accent-3': 'hsl(120 70% 38%)', + }, + light_yellow: { + '--c-surface-0': 'hsl(50 40% 96%)', + '--c-surface-1': 'hsl(50 35% 92%)', + '--c-surface-2': 'hsl(50 30% 85%)', + '--c-border': 'hsl(50 25% 80%)', + '--c-border-soft': 'hsl(50 22% 70%)', + '--c-border-faint': 'hsl(50 35% 92%)', + '--c-border-strong': 'hsl(50 20% 40%)', + '--c-text': 'hsl(50 30% 12%)', + '--c-text-secondary': 'hsl(50 20% 25%)', + '--c-text-muted': 'hsl(50 15% 40%)', + '--c-text-subtle': 'hsl(50 10% 50%)', + '--c-text-faint': 'hsl(50 10% 65%)', + '--c-text-faintest': 'hsl(50 12% 80%)', + '--c-danger-bg': '#fee2e2', + '--c-danger-border': '#fca5a5', + '--c-danger-text': '#b91c1c', + '--c-danger-text-strong': '#991b1b', + '--c-accent': 'hsl(230 70% 40%)', + '--c-accent-soft': 'hsl(200 60% 85%)', + '--c-accent-text': 'hsl(230 75% 28%)', + '--c-accent-2': 'hsl(140 70% 45%)', + '--c-accent-3': 'hsl(320 70% 38%)', + }, + light_pink: { + '--c-surface-0': 'hsl(330 40% 96%)', + '--c-surface-1': 'hsl(330 35% 92%)', + '--c-surface-2': 'hsl(330 30% 85%)', + '--c-border': 'hsl(330 25% 80%)', + '--c-border-soft': 'hsl(330 22% 70%)', + '--c-border-faint': 'hsl(330 35% 92%)', + '--c-border-strong': 'hsl(330 20% 40%)', + '--c-text': 'hsl(330 30% 12%)', + '--c-text-secondary': 'hsl(330 20% 25%)', + '--c-text-muted': 'hsl(330 15% 40%)', + '--c-text-subtle': 'hsl(330 10% 50%)', + '--c-text-faint': 'hsl(330 10% 65%)', + '--c-text-faintest': 'hsl(330 12% 80%)', + '--c-danger-bg': '#fee2e2', + '--c-danger-border': '#fca5a5', + '--c-danger-text': '#b91c1c', + '--c-danger-text-strong': '#991b1b', + '--c-accent': 'hsl(150 70% 40%)', + '--c-accent-soft': 'hsl(120 60% 85%)', + '--c-accent-text': 'hsl(150 75% 28%)', + '--c-accent-2': 'hsl(60 70% 45%)', + '--c-accent-3': 'hsl(240 70% 38%)', + }, + light_orange: { + '--c-surface-0': 'hsl(30 40% 96%)', + '--c-surface-1': 'hsl(30 35% 92%)', + '--c-surface-2': 'hsl(30 30% 85%)', + '--c-border': 'hsl(30 25% 80%)', + '--c-border-soft': 'hsl(30 22% 70%)', + '--c-border-faint': 'hsl(30 35% 92%)', + '--c-border-strong': 'hsl(30 20% 40%)', + '--c-text': 'hsl(30 30% 12%)', + '--c-text-secondary': 'hsl(30 20% 25%)', + '--c-text-muted': 'hsl(30 15% 40%)', + '--c-text-subtle': 'hsl(30 10% 50%)', + '--c-text-faint': 'hsl(30 10% 65%)', + '--c-text-faintest': 'hsl(30 12% 80%)', + '--c-danger-bg': '#fee2e2', + '--c-danger-border': '#fca5a5', + '--c-danger-text': '#b91c1c', + '--c-danger-text-strong': '#991b1b', + '--c-accent': 'hsl(210 70% 40%)', + '--c-accent-soft': 'hsl(180 60% 85%)', + '--c-accent-text': 'hsl(210 75% 28%)', + '--c-accent-2': 'hsl(120 70% 45%)', + '--c-accent-3': 'hsl(300 70% 38%)', + }, } export function generateThemeCss(vars: ThemeVars): string { diff --git a/webapp/src/lib/useSettingsPanel.ts b/webapp/src/lib/useSettingsPanel.ts index 107712f..548b039 100644 --- a/webapp/src/lib/useSettingsPanel.ts +++ b/webapp/src/lib/useSettingsPanel.ts @@ -31,6 +31,11 @@ const THEME_CLASSES = [ 'pink', 'orange', 'light_green', + 'light_red', + 'light_blue', + 'light_yellow', + 'light_pink', + 'light_orange', ] as const // Reflects `colorScheme` onto `` as a class + `data-color-scheme`; when a custom theme is active, no built-in class is added at all, since a class selector would out-specificity its injected `:root` override (see `applyCustomThemeCss`) and mask it.