From 147d62e1c1c0f3c19b34642e1fe97c10ac84a503 Mon Sep 17 00:00:00 2001 From: eti Date: Thu, 27 Aug 2026 20:23:40 +0200 Subject: [PATCH] web/storybook: add a light/dark theme switch Signed-off-by: eti --- web/.storybook/main.ts | 1 + web/.storybook/preview.ts | 19 +++++++++++++++++++ web/package.json | 1 + web/pnpm-lock.yaml | 13 +++++++++++++ web/src/app.css | 19 ++++++++++++++++--- web/src/pierre.css | 4 ++-- 6 files changed, 52 insertions(+), 5 deletions(-) diff --git a/web/.storybook/main.ts b/web/.storybook/main.ts index af663b7a1..8188b56b7 100644 --- a/web/.storybook/main.ts +++ b/web/.storybook/main.ts @@ -8,6 +8,7 @@ const config: StorybookConfig = { "@chromatic-com/storybook", "@storybook/addon-vitest", "@storybook/addon-a11y", + "@storybook/addon-themes", "@storybook/addon-docs" ], framework: "@storybook/sveltekit" diff --git a/web/.storybook/preview.ts b/web/.storybook/preview.ts index 95281bab7..72478ee97 100644 --- a/web/.storybook/preview.ts +++ b/web/.storybook/preview.ts @@ -1,4 +1,5 @@ import type { Preview } from "@storybook/sveltekit"; +import { withThemeByDataAttribute } from "@storybook/addon-themes"; import MockAuth, { type MockAuthParam } from "./MockAuth.svelte"; import BoneCaptureDecorator from "./BoneCaptureDecorator.svelte"; @@ -8,6 +9,11 @@ import "../src/app.css"; const preview: Preview = { decorators: [ + withThemeByDataAttribute({ + themes: { light: "light", dark: "dark" }, + defaultTheme: "light", + attributeName: "data-theme" + }), (_story, context) => ({ Component: MockAuth, props: { auth: context.parameters.auth as MockAuthParam } @@ -21,6 +27,15 @@ const preview: Preview = { } ], parameters: { + // the two swatches are --color-background-canvas in each mode, so the + // backdrop matches the app instead of Storybook's grey + backgrounds: { + options: { + light: { name: "Light", value: "#f3f4f6" }, + dark: { name: "Dark", value: "#111827" } + } + }, + controls: { matchers: { color: /(background|color)$/i, @@ -34,6 +49,10 @@ const preview: Preview = { // 'off' - skip a11y checks entirely test: "todo" } + }, + + initialGlobals: { + backgrounds: { value: "light" } } }; diff --git a/web/package.json b/web/package.json index b19daf440..b6a37d86e 100644 --- a/web/package.json +++ b/web/package.json @@ -35,6 +35,7 @@ "@storybook/addon-a11y": "^10.5.0", "@storybook/addon-docs": "^10.5.0", "@storybook/addon-svelte-csf": "^5.1.2", + "@storybook/addon-themes": "^10.5.10", "@storybook/addon-vitest": "^10.5.0", "@storybook/sveltekit": "^10.5.0", "@sveltejs/adapter-cloudflare": "^7.2.9", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 5aa8b8295..9b6cf3f92 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -99,6 +99,9 @@ importers: '@storybook/addon-svelte-csf': specifier: ^5.1.2 version: 5.1.2(@storybook/svelte@10.5.0(storybook@10.5.0(@types/react@19.2.18)(prettier@3.9.5)(react@19.2.8))(svelte@5.56.9(@typescript-eslint/types@8.64.0)))(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.9(@typescript-eslint/types@8.64.0))(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)))(storybook@10.5.0(@types/react@19.2.18)(prettier@3.9.5)(react@19.2.8))(svelte@5.56.9(@typescript-eslint/types@8.64.0))(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + '@storybook/addon-themes': + specifier: ^10.5.10 + version: 10.5.10(storybook@10.5.0(@types/react@19.2.18)(prettier@3.9.5)(react@19.2.8)) '@storybook/addon-vitest': specifier: ^10.5.0 version: 10.5.0(@vitest/browser-playwright@4.1.10)(@vitest/browser@4.1.10)(@vitest/runner@4.1.10)(react@19.2.8)(storybook@10.5.0(@types/react@19.2.18)(prettier@3.9.5)(react@19.2.8))(vitest@4.1.10) @@ -1558,6 +1561,11 @@ packages: svelte: ^5.0.0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + '@storybook/addon-themes@10.5.10': + resolution: {integrity: sha512-XNTjmIBwJ0TUcIV11STGY6hcynCy6imgYCDZeDaDyNEQsCoYAlY25fYvWNfrz5jOu1MGACiaA/E0ypk03whDdQ==} + peerDependencies: + storybook: ^10.5.10 + '@storybook/addon-vitest@10.5.0': resolution: {integrity: sha512-o/H+ii8o7bu4r1M4pgyFt+DjVaIIccSeaAikpspNHKkKdiJ2GXfkgNwtib8Jru28HaBRfcP5u+m0QE3KBYIQkw==} peerDependencies: @@ -4743,6 +4751,11 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros + '@storybook/addon-themes@10.5.10(storybook@10.5.0(@types/react@19.2.18)(prettier@3.9.5)(react@19.2.8))': + dependencies: + storybook: 10.5.0(@types/react@19.2.18)(prettier@3.9.5)(react@19.2.8) + ts-dedent: 2.3.0 + '@storybook/addon-vitest@10.5.0(@vitest/browser-playwright@4.1.10)(@vitest/browser@4.1.10)(@vitest/runner@4.1.10)(react@19.2.8)(storybook@10.5.0(@types/react@19.2.18)(prettier@3.9.5)(react@19.2.8))(vitest@4.1.10)': dependencies: '@storybook/global': 5.0.0 diff --git a/web/src/app.css b/web/src/app.css index cc8abd48a..898558b6e 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -3,6 +3,19 @@ @import "./pierre.css"; @import "./logs.css"; +/* dark follows the system unless an ancestor pins the theme, so Storybook (and + later the theme setting) can force either mode with data-theme on */ +@custom-variant dark { + @media (prefers-color-scheme: dark) { + &:where(:not([data-theme="light"], [data-theme="light"] *)) { + @slot; + } + } + &:where([data-theme="dark"], [data-theme="dark"] *) { + @slot; + } +} + @theme { --font-sans: "InterVariable", "system-ui", sans-serif, ui-sans-serif; --font-mono: @@ -14,7 +27,7 @@ /* ========================================================================= Semantic color tokens — synced from Figma "Design System" › semantic colors Names mirror the Figma variables verbatim (`/` → `-`). Light-mode values - here; dark-mode overrides live in the prefers-color-scheme block below. + here; dark-mode overrides live in the dark variant block below. ========================================================================= */ /* --- background --- */ @@ -430,8 +443,8 @@ scrollbar-gutter: stable; } - @media (prefers-color-scheme: dark) { - :root { + :root { + @variant dark { /* --- background (Dark) --- */ --color-background-default: #1f2937; --color-background-subtle: #374151; diff --git a/web/src/pierre.css b/web/src/pierre.css index 6441a7d8d..7a583ade5 100644 --- a/web/src/pierre.css +++ b/web/src/pierre.css @@ -42,8 +42,8 @@ diffs-container.diffs-container { --diffs-token-changed: #df8e1d; /* yellow */ } -@media (prefers-color-scheme: dark) { - diffs-container.diffs-container { +diffs-container.diffs-container { + @variant dark { --diffs-fg-number-override: var(--color-foreground-subtle); /* the dark blend is 80/20, hence the subtle backgrounds */ -- 2.51.2