Skip to content

Commit 0e3f107

Browse files
authored
Merge branch 'main' into jg/we-have-to-go-bigger
2 parents 70486ca + 6d5bb56 commit 0e3f107

38 files changed

+6496
-1662
lines changed

.github/workflows/chromatic.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: chromatic
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
11+
cancel-in-progress: true
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
chromatic:
18+
name: 📚 Chromatic
19+
runs-on: ubuntu-24.04-arm
20+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
21+
22+
steps:
23+
- name: ☑️ Checkout
24+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
25+
with:
26+
fetch-depth: 0
27+
ref: ${{ github.event.pull_request.head.ref || github.ref }}
28+
29+
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
30+
with:
31+
node-version: lts/*
32+
33+
- uses: pnpm/action-setup@1e1c8eafbd745f64b1ef30a7d7ed7965034c486c # 1e1c8eafbd745f64b1ef30a7d7ed7965034c486c
34+
name: 🟧 Install pnpm
35+
with:
36+
cache: true
37+
38+
- name: 📦 Install dependencies
39+
run: pnpm install
40+
41+
- name: 🧪 Run Chromatic Visual Tests
42+
uses: chromaui/action@a8ce9c58f59be5cc7090cadfc8f130fb08fcf0c3 # v15.1.0
43+
with:
44+
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
45+
env:
46+
CHROMATIC_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
47+
CHROMATIC_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
48+
CHROMATIC_SLUG: ${{ github.repository }}

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,9 @@ file-tree-sprite.svg
4545

4646
# output
4747
.vercel
48+
49+
# Storybook
50+
*storybook.log
51+
storybook-static
52+
4853
.nvmrc

.storybook/main.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { StorybookConfig } from '@storybook-vue/nuxt'
2+
3+
const config = {
4+
stories: ['../app/**/*.stories.@(js|ts)'],
5+
addons: ['@storybook/addon-a11y', '@storybook/addon-docs'],
6+
framework: '@storybook-vue/nuxt',
7+
features: {
8+
backgrounds: false,
9+
},
10+
async viteFinal(config) {
11+
// Replace the built-in vue-docgen plugin with a fault-tolerant version.
12+
// vue-docgen-api can crash on components that import types from other
13+
// .vue files (it tries to parse the SFC with @babel/parser as plain TS).
14+
// This wrapper catches those errors so the build doesn't fail.
15+
const docgenPlugin = config.plugins?.find(
16+
(p): p is Extract<typeof p, { name: string }> =>
17+
!!p && typeof p === 'object' && 'name' in p && p.name === 'storybook:vue-docgen-plugin',
18+
)
19+
20+
if (docgenPlugin && 'transform' in docgenPlugin) {
21+
const hook = docgenPlugin.transform
22+
// Vite plugin hooks can be a function or an object with a `handler` property
23+
const originalFn = typeof hook === 'function' ? hook : hook?.handler
24+
if (originalFn) {
25+
const wrapped = async function (this: unknown, ...args: unknown[]) {
26+
try {
27+
return await originalFn.apply(this, args)
28+
} catch {
29+
return undefined
30+
}
31+
}
32+
if (typeof hook === 'function') {
33+
docgenPlugin.transform = wrapped as typeof hook
34+
} else if (hook) {
35+
hook.handler = wrapped as typeof hook.handler
36+
}
37+
}
38+
}
39+
40+
return config
41+
},
42+
} satisfies StorybookConfig
43+
44+
export default config

.storybook/preview.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type { Preview } from '@storybook-vue/nuxt'
2+
import { currentLocales } from '../config/i18n'
3+
import { fn } from 'storybook/test'
4+
import { ACCENT_COLORS } from '../shared/utils/constants'
5+
6+
// related: https://github.com/npmx-dev/npmx.dev/blob/1431d24be555bca5e1ae6264434d49ca15173c43/test/nuxt/setup.ts#L12-L26
7+
// Stub Nuxt specific globals
8+
// @ts-expect-error - dynamic global name
9+
globalThis['__NUXT_COLOR_MODE__'] ??= {
10+
preference: 'system',
11+
value: 'dark',
12+
getColorScheme: fn(() => 'dark'),
13+
addColorScheme: fn(),
14+
removeColorScheme: fn(),
15+
}
16+
// @ts-expect-error - dynamic global name
17+
globalThis.defineOgImageComponent = fn()
18+
19+
const preview: Preview = {
20+
parameters: {
21+
controls: {
22+
matchers: {
23+
color: /(background|color)$/i,
24+
date: /Date$/i,
25+
},
26+
},
27+
},
28+
// Provides toolbars to switch things like theming and language
29+
globalTypes: {
30+
locale: {
31+
name: 'Locale',
32+
description: 'UI language',
33+
defaultValue: 'en-US',
34+
toolbar: {
35+
icon: 'globe',
36+
dynamicTitle: true,
37+
items: [
38+
// English is at the top so it's easier to reset to it
39+
{ value: 'en-US', title: 'English (US)' },
40+
...currentLocales
41+
.filter(locale => locale.code !== 'en-US')
42+
.map(locale => ({ value: locale.code, title: locale.name })),
43+
],
44+
},
45+
},
46+
accentColor: {
47+
name: 'Accent Color',
48+
description: 'Accent color',
49+
toolbar: {
50+
icon: 'paintbrush',
51+
dynamicTitle: true,
52+
items: [
53+
...Object.keys(ACCENT_COLORS.light).map(color => ({
54+
value: color,
55+
title: color.charAt(0).toUpperCase() + color.slice(1),
56+
})),
57+
{ value: undefined, title: 'No Accent' },
58+
],
59+
},
60+
},
61+
theme: {
62+
name: 'Theme',
63+
description: 'Color mode',
64+
defaultValue: 'dark',
65+
toolbar: {
66+
icon: 'moon',
67+
dynamicTitle: true,
68+
items: [
69+
{ value: 'light', icon: 'sun', title: 'Light' },
70+
{ value: 'dark', icon: 'moon', title: 'Dark' },
71+
],
72+
},
73+
},
74+
},
75+
decorators: [
76+
(story, context) => {
77+
const { locale, theme, accentColor } = context.globals as {
78+
locale: string
79+
theme: string
80+
accentColor?: string
81+
}
82+
83+
// Set theme from globals
84+
document.documentElement.setAttribute('data-theme', theme)
85+
86+
// Set accent color from globals
87+
if (accentColor) {
88+
document.documentElement.style.setProperty('--accent-color', `var(--swatch-${accentColor})`)
89+
} else {
90+
document.documentElement.style.removeProperty('--accent-color')
91+
}
92+
93+
return {
94+
template: '<story />',
95+
// Set locale from globals
96+
created() {
97+
if (this.$i18n) {
98+
this.$i18n.setLocale(locale)
99+
}
100+
},
101+
updated() {
102+
if (this.$i18n) {
103+
this.$i18n.setLocale(locale)
104+
}
105+
},
106+
}
107+
},
108+
],
109+
}
110+
111+
export default preview

.vscode/settings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,9 @@
33
"editor.formatOnSave": true,
44
"i18n-ally.keystyle": "nested",
55
"i18n-ally.localesPaths": ["./i18n/locales"],
6-
"typescript.tsdk": "node_modules/typescript/lib"
6+
"typescript.tsdk": "node_modules/typescript/lib",
7+
"explorer.fileNesting.enabled": true,
8+
"explorer.fileNesting.patterns": {
9+
"*.vue": "${capture}.stories.ts"
10+
}
711
}

CONTRIBUTING.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ This focus helps guide our project decisions as a community and what we choose t
6060
- [Lighthouse performance tests](#lighthouse-performance-tests)
6161
- [End to end tests](#end-to-end-tests)
6262
- [Test fixtures (mocking external APIs)](#test-fixtures-mocking-external-apis)
63+
- [Storybook](#storybook)
64+
- [Component categories](#component-categories)
65+
- [Coverage guidelines](#coverage-guidelines)
66+
- [Project conventions](#project-conventions)
67+
- [Configuration](#configuration)
68+
- [Global app settings](#global-app-settings)
69+
- [Known limitations](#known-limitations)
6370
- [Submitting changes](#submitting-changes)
6471
- [Before submitting](#before-submitting)
6572
- [Pull request process](#pull-request-process)
@@ -885,6 +892,117 @@ The mock connector supports test endpoints for state manipulation:
885892
- `/__test__/user-packages` - Set user's packages
886893
- `/__test__/package` - Set package collaborators
887894

895+
## Storybook
896+
897+
Storybook is a development environment for UI components that helps catch UI changes and provides integrations for various testing types. For testing, Storybook offers:
898+
899+
- **Accessibility tests** - Built-in a11y checks
900+
- **Visual tests** - Compare JPG screenshots
901+
- **Vitest tests** - Use stories directly in the unit tests
902+
903+
### Component categories
904+
905+
The plan is to organize components into 3 categories.
906+
907+
#### UI Library Components
908+
909+
Generic and reusable components used throughout the application.
910+
911+
- Examples: Button, Input, Modal, Card
912+
- **Testing focus:** Props, variants, accessibility
913+
- **Coverage:** All variants and states
914+
915+
#### Composite Components
916+
917+
Single-use components that encapsulate one feature.
918+
919+
- Examples: UserProfile, WeeklyDownloadStats
920+
- **Testing focus:** Integration patterns, user interactions
921+
- **Coverage:** Common usage scenarios
922+
923+
#### Page Components
924+
925+
**Full-page layouts** should match what the users see.
926+
927+
- Examples: HomePage, Dashboard, CheckoutPage
928+
- **Testing focus:** Layout, responsive behavior, integration testing
929+
- **Coverage:** Critical user flows and breakpoints
930+
931+
### Coverage guidelines
932+
933+
#### Which Components Need Stories?
934+
935+
TBD
936+
937+
### Project conventions
938+
939+
#### Place `.stories.ts` files next to the component
940+
941+
```sh
942+
components/
943+
├── Button.vue
944+
└── Button.stories.ts
945+
```
946+
947+
#### Story Template
948+
949+
```ts
950+
// *.stories.ts
951+
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
952+
import Component from './Button.vue'
953+
954+
const meta = {
955+
component: Component,
956+
// component scope configuration goes here
957+
} satisfies Meta<typeof Component>
958+
959+
export default meta
960+
type Story = StoryObj<typeof meta>
961+
962+
export const Default: Story = {
963+
// story scope configuration goes here
964+
}
965+
```
966+
967+
#### JSDocs Annotation
968+
969+
The component should include descriptive comments.
970+
971+
```ts
972+
// Button.vue
973+
<script setup lang="ts">
974+
const props = withDefaults(
975+
defineProps<{
976+
/** Whether the button is disabled */
977+
disabled?: boolean
978+
/**
979+
* HTML button type attribute
980+
* @default "button"
981+
type?: 'button' | 'submit'
982+
// ...
983+
}>)
984+
</script>
985+
```
986+
987+
### Configuration
988+
989+
Stories can be configured at three levels:
990+
991+
- **Global scope** (`.storybook/preview.ts`) - Applies to all stories
992+
- **Component scope** - Applies to all stories for a specific component
993+
- **Story scope** - Applies to individual stories only
994+
995+
### Global app settings
996+
997+
Global application settings are added to the Storybook toolbar for easy testing and viewing. Configure these in `.storybook/preview.ts` under the `globalTypes` and `decorators` properties.
998+
999+
### Known limitations
1000+
1001+
- Changing `i18n` in the toolbar doesn't update the language. A manual story reload is required.
1002+
- `autodocs` currently is non-functional due bugs, its usage is discouraged at this time.
1003+
- `pnpm storybook` may log warnings or non-breaking errors for Nuxt modules due to the lack of mocks. If the UI renders correctly, these can be safely ignored.
1004+
- Do not `import type` from `.vue` files. The `vue-docgen-api` parser used by `@storybook/addon-docs` cannot follow type imports across SFCs and will crash. Extract shared types into a separate `.ts` file instead.
1005+
8881006
## Submitting changes
8891007
8901008
### Before submitting

0 commit comments

Comments
 (0)