Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ shared/types/lexicons

# output
.vercel

*storybook.log
storybook-static
135 changes: 135 additions & 0 deletions .storybook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Why are we using Storybook?

Storybook is a development environment for UI components that helps catch changes in UI while also having integrations for different kinds of tests. For testing, Storybook provides:

- **Accessibility tests** - Built-in a11y checks (link to example)
- **Visual tests** - Compare JPG screenshots (link to example)
- **Snapshot tests** - Compare HTML output (link to example)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on years of user feedback, I'd strongly advice against HTML snapshot testing. It usually ends up being mindlessly updating the snapshots again and again, without any real use. Visual tests are much better snapshots, as they are easier to review and more granular because the diff is on the pixel level and contains styling too.

- **Vitest tests** - Use stories directly in your unit tests (link to example)

## Component Categories

We organize components into 3 categories.

### UI Library Components

**Generic, reusable components** used throughout your application.

- Examples: Button, Input, Modal, Card
- **Testing focus:** Props, variants, accessibility
- **Coverage:** All variants and states

### Composite Components

**Domain-specific components** built from UI library components.

- Examples: UserProfile, ProductCard, SearchForm
- **Testing focus:** Integration patterns, user interactions
- **Coverage:** Common usage scenarios

### Page Components

**Full-page layouts** shown to end users.

- Examples: HomePage, Dashboard, CheckoutPage
- **Testing focus:** Layout, responsive behavior, integration testing
- **Coverage:** Critical user flows and breakpoints

## Coverage Guidelines

### Which Components Need Stories?

TBD

### Convention

- An edge case per story
- Do not use `autodocs`

# How to Use
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this section reads to me like it's just a concise version of the Storybook docs, so I'm not sure it's valuable? The "Place a .stories.ts file next to your component" line is good, because you're establishing a convention. The import type { Meta, StoryObj } from '@nuxtjs/storybook' is also good because I would expect the LLM to trip up there a lot. but the rest might not be necessary?


## Writing Stories

1. **Create your first story** - Place a `.stories.ts` file next to your component:

```
components/
├── Button.vue
└── Button.stories.ts
```

2. **Add the story code** - Each story file follows this pattern:

```ts
// Button.stories.ts
import type { Meta, StoryObj } from '@nuxtjs/storybook'
import Component from './Button.vue'

const meta = {
component: Component,
// component configuration goes here
} satisfies Meta<typeof Component>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
// story configuration goes here
}
```

3. **Run Storybook locally:**

```sh
pnpm storybook
```

4. **Find your story** - Storybook URLs mirror your project structure.

For a component at `app/components/Button/Button.stories.ts`, the story will be available at `http://localhost:6006/?path=/story/components-button--default`

## Configuration

### Global Configuration (`.storybook/preview.ts`)

Affects all stories across the project:

```ts
export default {
globals: {
locale: 'en-US',
},
}
```

### Component Configuration (meta)

Overrides settings for a specific component:

```ts
const meta = {
component: Button,
parameters: {
layout: 'centered',
},
globals: {
locale: 'ja-JP',
},
}
```

### Story Configuration

Overrides settings for individual stories:

```ts
export const SpecialCase: Story = {
globals: {
locale: 'fr-FR',
},
}
```

## Global App Settings

Global application settings are added to the Storybook toolbar for easy testing and viewing. Configure these in `.storybook/preview.ts` using the `globals` property with toolbar definitions.
11 changes: 11 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { StorybookConfig } from '@nuxtjs/storybook'

const config = {
stories: ['../app/**/*.stories.@(js|ts|mdx)'],
addons: ['@storybook/addon-a11y', '@storybook/addon-docs'],
framework: '@storybook-vue/nuxt',
features: {
backgrounds: false,
},
} satisfies StorybookConfig
export default config
111 changes: 111 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { Preview } from '@nuxtjs/storybook'
import { currentLocales } from '../config/i18n'
import { fn } from 'storybook/test'
import { ACCENT_COLORS } from '../shared/utils/constants'

// related: https://github.com/npmx-dev/npmx.dev/blob/1431d24be555bca5e1ae6264434d49ca15173c43/test/nuxt/setup.ts#L12-L26
// Stub Nuxt specific globals
// @ts-expect-error - dynamic global name
globalThis['__NUXT_COLOR_MODE__'] ??= {
preference: 'system',
value: 'dark',
getColorScheme: fn(() => 'dark'),
addColorScheme: fn(),
removeColorScheme: fn(),
}
// @ts-expect-error - dynamic global name
globalThis.defineOgImageComponent = fn()

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
// Provides toolbars to switch things like theming and language
globalTypes: {
locale: {
name: 'Locale',
description: 'UI language',
defaultValue: 'en-US',
toolbar: {
icon: 'globe',
dynamicTitle: true,
items: [
// English is at the top so it's easier to reset to it
{ value: 'en-US', title: 'English (US)' },
...currentLocales
.filter(locale => locale.code !== 'en-US')
.map(locale => ({ value: locale.code, title: locale.name })),
],
},
},
accentColor: {
name: 'Accent Color',
description: 'Accent color',
toolbar: {
icon: 'paintbrush',
dynamicTitle: true,
items: [
...Object.keys(ACCENT_COLORS.light).map(color => ({
value: color,
title: color.charAt(0).toUpperCase() + color.slice(1),
})),
{ value: undefined, title: 'No Accent' },
],
},
},
theme: {
name: 'Theme',
description: 'Color mode',
defaultValue: 'dark',
toolbar: {
icon: 'moon',
dynamicTitle: true,
items: [
{ value: 'light', icon: 'sun', title: 'Light' },
{ value: 'dark', icon: 'moon', title: 'Dark' },
],
},
},
},
decorators: [
(story, context) => {
const { locale, theme, accentColor } = context.globals as {
locale: string
theme: string
accentColor?: string
}

// Set theme from globals
document.documentElement.setAttribute('data-theme', theme)

// Set accent color from globals
if (accentColor) {
document.documentElement.style.setProperty('--accent-color', `var(--swatch-${accentColor})`)
} else {
document.documentElement.style.removeProperty('--accent-color')
}

return {
template: '<story />',
// Set locale from globals
created() {
if (this.$i18n) {
this.$i18n.setLocale(locale)
}
},
updated() {
if (this.$i18n) {
this.$i18n.setLocale(locale)
}
},
}
},
],
}

export default preview
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
"editor.formatOnSave": true,
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": ["./i18n/locales"],
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.vue": "${capture}.stories.ts"
}
}
61 changes: 61 additions & 0 deletions app/components/AppFooter.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { Meta, StoryObj } from '@nuxtjs/storybook'
import AppFooter from './AppFooter.vue'

const meta = {
component: AppFooter,
parameters: {
layout: 'fullscreen',
},
globals: {
viewport: { value: undefined },
},
} satisfies Meta<typeof AppFooter>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {}

export const InContext: Story = {
render: () => ({
components: { AppFooter },
template: `
<div style="display: flex; flex-direction: column; min-height: 100vh;">
<div style="flex: 1; padding: 2rem;">
<h1>Some page content</h1>
<span>See footer at the bottom</span>
</div>
<AppFooter />
</div>
`,
}),
}

export const InLongPage: Story = {
render: () => ({
components: { AppFooter },
template: `
<div style="display: flex; flex-direction: column; min-height: 100vh;">
<div style="flex: 1; padding: 2rem;">
<h1>Footer is all the way at the bottom!</h1> <br />
${Array.from({ length: 50 }, (_, i) => `<p key="${i}">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>`).join('')}
</div>
<AppFooter />
</div>
`,
}),
}

export const MobileView: Story = {
...InContext,
globals: {
viewport: { value: 'mobile1' },
},
}

export const TabletView: Story = {
...InContext,
globals: {
viewport: { value: 'tablet' },
},
}
22 changes: 22 additions & 0 deletions app/components/AppHeader.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@nuxtjs/storybook'
import AppHeader from './AppHeader.vue'

const meta = {
component: AppHeader,
parameters: {
layout: 'fullscreen',
},
globals: {
viewport: { value: undefined },
},
} satisfies Meta<typeof AppHeader>

export default meta

export const Default: StoryObj<typeof AppHeader> = {}

export const Mobile: StoryObj<typeof AppHeader> = {
globals: {
viewport: { value: 'mobile1' },
},
}
Loading
Loading