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

+ +
+ + + +
+
+ `, + }), +} 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) }