From 80e09bb715fe257028b5e621a1608fb37265dd36 Mon Sep 17 00:00:00 2001 From: NikolaiKushner Date: Fri, 9 Oct 2026 11:36:28 +0300 Subject: [PATCH] Storybook: opens in modern, with a branded Welcome page. - People browsing the workshop start in the modern theme, as rowkit.dev does. The browser tests and their a11y gate still start in Windows 98 (Vite mode `test`), and VITE_RK_THEME picks either explicitly. - visual:check names the theme in the URL (Windows 98 unless --theme modern), so the pixel baseline does not follow the workshop's default. - A Welcome page first in the sidebar: the logo, one line on what rowkit is, how to use Theme, Scheme, Accessibility and Interactions, and links to the docs, GitHub and npm. Built from rowkit's components, so it reads in every theme; the logo takes its light letters on a dark scheme. - The docs pages take the chrome's light or dark brand theme. Co-Authored-By: Claude Opus 5.5 --- .storybook/env.d.ts | 4 +- .storybook/preview.ts | 21 ++++--- AGENTS.md | 1 + packages/ui/src/stories/Welcome.stories.ts | 72 ++++++++++++++++++++++ scripts/visual-check.mjs | 16 +++-- 5 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 packages/ui/src/stories/Welcome.stories.ts diff --git a/.storybook/env.d.ts b/.storybook/env.d.ts index ea3f119a..612d4fd8 100644 --- a/.storybook/env.d.ts +++ b/.storybook/env.d.ts @@ -3,8 +3,10 @@ declare module '*.css' interface ImportMetaEnv { - /** `modern` opens the stories in the modern theme; anything else, Windows 98. */ + /** `win98` or `modern`: the theme the stories open in, overriding the default. */ readonly VITE_RK_THEME?: string + /** Vite's mode: `test` under Vitest, `development` or `production` otherwise. */ + readonly MODE: string } interface ImportMeta { diff --git a/.storybook/preview.ts b/.storybook/preview.ts index a43c38f7..111b979a 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,5 +1,5 @@ import type { Preview } from '@storybook/vue3-vite' -import theme from './theme' +import { forScheme } from './theme' // The Windows 98 faces, loaded the way an app is told to load them. import '@fontsource/pt-sans/400.css' import '@fontsource/pt-sans/700.css' @@ -10,11 +10,18 @@ type Theme = 'win98' | 'modern' type Scheme = 'system' | 'light' | 'dark' /** - * The theme the stories open in. Windows 98 is rowkit's default; set - * `VITE_RK_THEME=modern` to run the browser tests (and their a11y gate) in the - * modern theme instead. + * The theme the stories open in. People browsing the workshop start in modern, + * as rowkit.dev does. The browser tests (Vitest, mode `test`) and their a11y + * gate start in Windows 98 — rowkit's own default, without an attribute — and + * `VITE_RK_THEME` picks either explicitly (`test:a11y:modern`). */ -const initialTheme: Theme = import.meta.env.VITE_RK_THEME === 'modern' ? 'modern' : 'win98' +const requested = import.meta.env.VITE_RK_THEME +const initialTheme: Theme = + requested === 'modern' || requested === 'win98' + ? requested + : import.meta.env.MODE === 'test' + ? 'win98' + : 'modern' const preview: Preview = { globalTypes: { @@ -47,10 +54,10 @@ const preview: Preview = { initialGlobals: { theme: initialTheme, scheme: 'system' }, parameters: { controls: { matchers: { color: /(background|color)$/i, date: /Date$/i } }, - docs: { theme }, + docs: { theme: forScheme() }, options: { storySort: { - order: ['Patterns', 'Foundations', 'Data', 'Overlay', '*'], + order: ['Welcome', 'Patterns', 'Foundations', 'Data', 'Overlay', '*'], }, }, a11y: { diff --git a/AGENTS.md b/AGENTS.md index 68ba053b..43d9efeb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,6 +129,7 @@ pnpm size # bundle budget, brotli pnpm storybook # then, in another terminal: pnpm visual:check # screenshot default stories → .visual-check/ pnpm visual:check Button # scoped to one component +pnpm visual:check --theme modern # the same in the modern theme (Windows 98 by default) pnpm test:a11y # the browser tests and a11y gate (Windows 98) pnpm test:a11y:modern # the same in the modern theme diff --git a/packages/ui/src/stories/Welcome.stories.ts b/packages/ui/src/stories/Welcome.stories.ts new file mode 100644 index 00000000..584a1d46 --- /dev/null +++ b/packages/ui/src/stories/Welcome.stories.ts @@ -0,0 +1,72 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import Badge from '../components/Badge/Badge.vue' +import Button from '../components/Button/Button.vue' +import Separator from '../components/Separator/Separator.vue' + +/** + * The first page of the workshop: the brand, what rowkit is, and how to use + * the toolbar. Drawn with rowkit's own components, so it reads in whichever + * theme the toolbar picks; only the logo — the brand, which belongs to no + * theme — swaps to its light letters on a dark scheme. + */ +const meta: Meta = { + title: 'Welcome', + // A page to read, not a component to poke: no addon panel under it. + parameters: { + controls: { disable: true }, + actions: { disable: true }, + options: { showPanel: false }, + }, +} +export default meta + +type Story = StoryObj + +const tips = [ + ['Theme', 'in the toolbar draws every story in Windows 98 or the modern theme.'], + ['Scheme', 'picks light or dark for a theme that has a dark scheme.'], + ['Accessibility', 'runs axe on the story — the same check gates every pull request.'], + ['Interactions', 'replays a story’s play function step by step.'], +] as const + +/** Dark letters unless the modern theme is drawn dark, by choice or by the system. */ +function darkScheme(theme: unknown, scheme: unknown): boolean { + if (theme !== 'modern') return false + if (scheme === 'dark') return true + if (scheme === 'light') return false + return window.matchMedia('(prefers-color-scheme: dark)').matches +} + +export const Default: Story = { + render: (_args, { globals }) => ({ + components: { Badge, Button, Separator }, + setup: () => ({ + logo: darkScheme(globals.theme, globals.scheme) ? '/logo-light.svg' : '/logo.svg', + tips, + }), + template: ` +
+
+ rowkit + 1.0 beta +
+

+ A professional Vue 3 toolkit — the components a product interface is built from. + This is its workshop: every component in every variant and state. +

+ +

How to use it

+
    +
  • + {{ name }} {{ text }} +
  • +
+
+ + + +
+
+ `, + }), +} diff --git a/scripts/visual-check.mjs b/scripts/visual-check.mjs index 10e22e29..6ddc9f72 100644 --- a/scripts/visual-check.mjs +++ b/scripts/visual-check.mjs @@ -6,6 +6,7 @@ * pnpm visual:check # default matrix * pnpm visual:check Button # only stories whose id contains "button" * pnpm visual:check --url http://127.0.0.1:6006 + * pnpm visual:check --theme modern # Windows 98 unless asked * * Expects Storybook already running (`pnpm storybook`). Writes PNGs under * `.visual-check/` — gitignored. Agents must Read those images before claiming @@ -44,16 +45,19 @@ const DEFAULT_STORIES = [ function parseArgs(argv) { let url = 'http://127.0.0.1:6006' + let theme = 'win98' const filters = [] for (let i = 0; i < argv.length; i++) { const arg = argv[i] if (arg === '--url') { url = argv[++i] ?? url + } else if (arg === '--theme') { + theme = argv[++i] ?? theme } else if (!arg.startsWith('-')) { filters.push(arg.toLowerCase()) } } - return { url, filters } + return { url, theme, filters } } function pickStories(filters) { @@ -77,8 +81,10 @@ async function waitForStorybook(page, url) { ) } -async function shot(page, baseUrl, storyId) { - const url = `${baseUrl}/iframe.html?id=${storyId}&viewMode=story` +async function shot(page, baseUrl, storyId, theme) { + // The theme is named, not left to the workshop's default (modern for people, + // Windows 98 for the pixel baseline). + const url = `${baseUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}` await page.goto(url, { waitUntil: 'networkidle' }) // Let fonts and portals settle. await page.waitForTimeout(200) @@ -88,7 +94,7 @@ async function shot(page, baseUrl, storyId) { } async function main() { - const { url, filters } = parseArgs(process.argv.slice(2)) + const { url, theme, filters } = parseArgs(process.argv.slice(2)) const stories = pickStories(filters) if (stories.length === 0) { console.error(`No default stories matched filter: ${filters.join(', ')}`) @@ -103,7 +109,7 @@ async function main() { await waitForStorybook(page, url) const written = [] for (const id of stories) { - const file = await shot(page, url, id) + const file = await shot(page, url, id, theme) written.push(file) console.log(file) }