Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .storybook/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 14 additions & 7 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions packages/ui/src/stories/Welcome.stories.ts
Original file line number Diff line number Diff line change
@@ -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: `
<main class="mx-auto flex max-w-[640px] flex-col gap-5 py-6">
<div class="flex items-center gap-3">
<img :src="logo" alt="rowkit" width="167" height="32" />
<Badge variant="primary">1.0 beta</Badge>
</div>
<p class="m-0 text-doc text-foreground">
A professional Vue 3 toolkit — the components a product interface is built from.
This is its workshop: every component in every variant and state.
</p>
<Separator />
<h1 class="m-0 text-heading font-strong text-foreground">How to use it</h1>
<ul class="m-0 flex list-none flex-col gap-2 p-0 text-doc text-foreground">
<li v-for="[name, text] in tips" :key="name">
<strong class="font-strong">{{ name }}</strong> {{ text }}
</li>
</ul>
<div class="flex flex-wrap gap-2">
<Button as="a" href="https://rowkit.dev">Documentation</Button>
<Button as="a" variant="secondary" href="https://github.com/NikolaiKushner/rowkit">GitHub</Button>
<Button as="a" variant="secondary" href="https://www.npmjs.com/package/rowkit">npm</Button>
</div>
</main>
`,
}),
}
16 changes: 11 additions & 5 deletions scripts/visual-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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(', ')}`)
Expand All @@ -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)
}
Expand Down
Loading