@fpkit/acss 1.0.0-beta.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/docs/README.md +325 -0
- package/docs/guides/accessibility.md +764 -0
- package/docs/guides/architecture.md +705 -0
- package/docs/guides/composition.md +688 -0
- package/docs/guides/css-variables.md +522 -0
- package/docs/guides/storybook.md +828 -0
- package/docs/guides/testing.md +817 -0
- package/docs/testing/focus-indicator-testing.md +437 -0
- package/libs/components/buttons/button.css +1 -1
- package/libs/components/buttons/button.css.map +1 -1
- package/libs/components/buttons/button.min.css +2 -2
- package/libs/components/icons/icon.d.cts +32 -32
- package/libs/components/icons/icon.d.ts +32 -32
- package/libs/components/list/list.css +1 -1
- package/libs/components/list/list.min.css +1 -1
- package/libs/index.css +1 -1
- package/libs/index.css.map +1 -1
- package/package.json +4 -3
- package/src/components/README.mdx +1 -1
- package/src/components/buttons/button.scss +5 -0
- package/src/components/buttons/button.stories.tsx +8 -5
- package/src/components/cards/card.stories.tsx +1 -1
- package/src/components/details/details.stories.tsx +1 -1
- package/src/components/form/form.stories.tsx +1 -1
- package/src/components/form/input.stories.tsx +1 -1
- package/src/components/form/select.stories.tsx +1 -1
- package/src/components/heading/README.mdx +292 -0
- package/src/components/icons/icon.stories.tsx +1 -1
- package/src/components/list/list.scss +1 -1
- package/src/components/nav/nav.stories.tsx +1 -1
- package/src/components/ui.stories.tsx +53 -19
- package/src/docs/accessibility.mdx +484 -0
- package/src/docs/composition.mdx +549 -0
- package/src/docs/css-variables.mdx +380 -0
- package/src/docs/fpkit-developer.mdx +545 -0
- package/src/introduction.mdx +356 -0
- package/src/styles/buttons/button.css +4 -0
- package/src/styles/buttons/button.css.map +1 -1
- package/src/styles/index.css +9 -3
- package/src/styles/index.css.map +1 -1
- package/src/styles/list/list.css +1 -1
- package/src/styles/utilities/_disabled.scss +5 -4
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
# Storybook Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This guide shows how to document custom components and compositions built with @fpkit/acss using Storybook. Whether you're building an internal component library or documenting your application components, Storybook provides an excellent development and documentation environment.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
### Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -D @storybook/react @storybook/react-vite @storybook/test storybook
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Initialize Storybook
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx storybook init
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Configuration
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
// .storybook/main.ts
|
|
27
|
+
import type { StorybookConfig } from '@storybook/react-vite'
|
|
28
|
+
|
|
29
|
+
const config: StorybookConfig = {
|
|
30
|
+
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
|
31
|
+
addons: [
|
|
32
|
+
'@storybook/addon-links',
|
|
33
|
+
'@storybook/addon-essentials',
|
|
34
|
+
'@storybook/addon-interactions',
|
|
35
|
+
],
|
|
36
|
+
framework: {
|
|
37
|
+
name: '@storybook/react-vite',
|
|
38
|
+
options: {},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default config
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Import fpkit Styles
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
// .storybook/preview.ts
|
|
49
|
+
import '@fpkit/acss/libs/index.css'
|
|
50
|
+
|
|
51
|
+
export default {
|
|
52
|
+
parameters: {
|
|
53
|
+
actions: { argTypesRegex: '^on.*' },
|
|
54
|
+
controls: {
|
|
55
|
+
matchers: {
|
|
56
|
+
color: /(background|color)$/i,
|
|
57
|
+
date: /Date$/,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Basic Story Structure
|
|
67
|
+
|
|
68
|
+
### Creating Your First Story
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// components/CustomButton.stories.tsx
|
|
72
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
73
|
+
import { Button } from '@fpkit/acss'
|
|
74
|
+
|
|
75
|
+
// Your custom composed component
|
|
76
|
+
const CustomButton = ({ loading, children, ...props }) => (
|
|
77
|
+
<Button {...props} disabled={loading}>
|
|
78
|
+
{loading ? 'Loading...' : children}
|
|
79
|
+
</Button>
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const meta = {
|
|
83
|
+
title: 'Components/CustomButton',
|
|
84
|
+
component: CustomButton,
|
|
85
|
+
tags: ['autodocs'],
|
|
86
|
+
args: {
|
|
87
|
+
children: 'Click me',
|
|
88
|
+
loading: false,
|
|
89
|
+
},
|
|
90
|
+
argTypes: {
|
|
91
|
+
loading: {
|
|
92
|
+
control: 'boolean',
|
|
93
|
+
description: 'Shows loading state',
|
|
94
|
+
},
|
|
95
|
+
onClick: {
|
|
96
|
+
action: 'clicked',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
} satisfies Meta<typeof CustomButton>
|
|
100
|
+
|
|
101
|
+
export default meta
|
|
102
|
+
type Story = StoryObj<typeof meta>
|
|
103
|
+
|
|
104
|
+
export const Default: Story = {}
|
|
105
|
+
|
|
106
|
+
export const Loading: Story = {
|
|
107
|
+
args: {
|
|
108
|
+
loading: true,
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Story Patterns
|
|
116
|
+
|
|
117
|
+
### Default Story
|
|
118
|
+
|
|
119
|
+
The simplest story uses the default args from `meta`:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
export const Default: Story = {}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Story with Custom Args
|
|
126
|
+
|
|
127
|
+
Override specific args for different variants:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
export const Primary: Story = {
|
|
131
|
+
args: {
|
|
132
|
+
variant: 'primary',
|
|
133
|
+
children: 'Primary Action',
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const Secondary: Story = {
|
|
138
|
+
args: {
|
|
139
|
+
variant: 'secondary',
|
|
140
|
+
children: 'Secondary Action',
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const Disabled: Story = {
|
|
145
|
+
args: {
|
|
146
|
+
disabled: true,
|
|
147
|
+
children: 'Disabled Button',
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Story with Custom Render
|
|
153
|
+
|
|
154
|
+
For complex compositions:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { Button, Badge } from '@fpkit/acss'
|
|
158
|
+
|
|
159
|
+
export const WithBadge: Story = {
|
|
160
|
+
render: (args) => (
|
|
161
|
+
<Button {...args}>
|
|
162
|
+
<span>{args.children}</span>
|
|
163
|
+
<Badge>New</Badge>
|
|
164
|
+
</Button>
|
|
165
|
+
),
|
|
166
|
+
args: {
|
|
167
|
+
children: 'Featured',
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const MultipleButtons: Story = {
|
|
172
|
+
render: () => (
|
|
173
|
+
<div style={{ display: 'flex', gap: '1rem' }}>
|
|
174
|
+
<Button variant="primary">Save</Button>
|
|
175
|
+
<Button variant="secondary">Cancel</Button>
|
|
176
|
+
<Button variant="tertiary">Reset</Button>
|
|
177
|
+
</div>
|
|
178
|
+
),
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Documenting Composed Components
|
|
185
|
+
|
|
186
|
+
### Card Composition Example
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// components/ActionCard.stories.tsx
|
|
190
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
191
|
+
import { Card, Button } from '@fpkit/acss'
|
|
192
|
+
|
|
193
|
+
const ActionCard = ({ title, children, actions }) => (
|
|
194
|
+
<Card>
|
|
195
|
+
<Card.Header>
|
|
196
|
+
<Card.Title>{title}</Card.Title>
|
|
197
|
+
</Card.Header>
|
|
198
|
+
<Card.Content>{children}</Card.Content>
|
|
199
|
+
<Card.Footer>
|
|
200
|
+
{actions.map((action, i) => (
|
|
201
|
+
<Button key={i} {...action} />
|
|
202
|
+
))}
|
|
203
|
+
</Card.Footer>
|
|
204
|
+
</Card>
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
const meta = {
|
|
208
|
+
title: 'Compositions/ActionCard',
|
|
209
|
+
component: ActionCard,
|
|
210
|
+
tags: ['autodocs'],
|
|
211
|
+
parameters: {
|
|
212
|
+
docs: {
|
|
213
|
+
description: {
|
|
214
|
+
component: `
|
|
215
|
+
ActionCard is a composition of fpkit Card, Button, and sub-components.
|
|
216
|
+
It provides a consistent layout for cards with title, content, and action buttons.
|
|
217
|
+
|
|
218
|
+
**Composed from:**
|
|
219
|
+
- Card (fpkit)
|
|
220
|
+
- Card.Header (fpkit)
|
|
221
|
+
- Card.Title (fpkit)
|
|
222
|
+
- Card.Content (fpkit)
|
|
223
|
+
- Card.Footer (fpkit)
|
|
224
|
+
- Button (fpkit)
|
|
225
|
+
`,
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
} satisfies Meta<typeof ActionCard>
|
|
230
|
+
|
|
231
|
+
export default meta
|
|
232
|
+
type Story = StoryObj<typeof meta>
|
|
233
|
+
|
|
234
|
+
export const Default: Story = {
|
|
235
|
+
args: {
|
|
236
|
+
title: 'Confirm Action',
|
|
237
|
+
children: 'Are you sure you want to proceed with this action?',
|
|
238
|
+
actions: [
|
|
239
|
+
{ children: 'Cancel', variant: 'secondary' },
|
|
240
|
+
{ children: 'Confirm', variant: 'primary' },
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const SingleAction: Story = {
|
|
246
|
+
args: {
|
|
247
|
+
title: 'Notification',
|
|
248
|
+
children: 'Your changes have been saved successfully.',
|
|
249
|
+
actions: [
|
|
250
|
+
{ children: 'OK', variant: 'primary' },
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## CSS Variable Customization Stories
|
|
259
|
+
|
|
260
|
+
Show how users can customize your components:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
export const CustomStyling: Story = {
|
|
264
|
+
render: () => (
|
|
265
|
+
<div
|
|
266
|
+
style={{
|
|
267
|
+
'--btn-primary-bg': '#7c3aed',
|
|
268
|
+
'--btn-primary-color': 'white',
|
|
269
|
+
'--btn-radius': '2rem',
|
|
270
|
+
'--btn-padding-inline': '3rem',
|
|
271
|
+
} as React.CSSProperties}
|
|
272
|
+
>
|
|
273
|
+
<Button variant="primary">Custom Styled Button</Button>
|
|
274
|
+
</div>
|
|
275
|
+
),
|
|
276
|
+
parameters: {
|
|
277
|
+
docs: {
|
|
278
|
+
description: {
|
|
279
|
+
story: `
|
|
280
|
+
Customize appearance using CSS variables:
|
|
281
|
+
- \`--btn-primary-bg\`: Background color
|
|
282
|
+
- \`--btn-primary-color\`: Text color
|
|
283
|
+
- \`--btn-radius\`: Border radius
|
|
284
|
+
- \`--btn-padding-inline\`: Horizontal padding
|
|
285
|
+
|
|
286
|
+
See the [CSS Variables Guide](/docs/guides/css-variables.md) for all available variables.
|
|
287
|
+
`,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export const ThemeExample: Story = {
|
|
294
|
+
render: () => (
|
|
295
|
+
<>
|
|
296
|
+
<div
|
|
297
|
+
className="light-theme"
|
|
298
|
+
style={{
|
|
299
|
+
'--btn-bg': 'white',
|
|
300
|
+
'--btn-color': '#333',
|
|
301
|
+
padding: '2rem',
|
|
302
|
+
background: '#f9f9f9',
|
|
303
|
+
} as React.CSSProperties}
|
|
304
|
+
>
|
|
305
|
+
<Button>Light Theme Button</Button>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<div
|
|
309
|
+
className="dark-theme"
|
|
310
|
+
style={{
|
|
311
|
+
'--btn-bg': '#2d2d2d',
|
|
312
|
+
'--btn-color': '#f0f0f0',
|
|
313
|
+
padding: '2rem',
|
|
314
|
+
background: '#1a1a1a',
|
|
315
|
+
marginTop: '1rem',
|
|
316
|
+
} as React.CSSProperties}
|
|
317
|
+
>
|
|
318
|
+
<Button>Dark Theme Button</Button>
|
|
319
|
+
</div>
|
|
320
|
+
</>
|
|
321
|
+
),
|
|
322
|
+
parameters: {
|
|
323
|
+
docs: {
|
|
324
|
+
description: {
|
|
325
|
+
story: 'Theme-based customization using scoped CSS variables.',
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Interactive Testing with Play Functions
|
|
335
|
+
|
|
336
|
+
Play functions enable automated interaction testing in Storybook:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import { within, userEvent, expect } from '@storybook/test'
|
|
340
|
+
|
|
341
|
+
export const InteractiveTest: Story = {
|
|
342
|
+
play: async ({ canvasElement, step }) => {
|
|
343
|
+
const canvas = within(canvasElement)
|
|
344
|
+
const button = canvas.getByRole('button')
|
|
345
|
+
|
|
346
|
+
await step('Button is rendered', async () => {
|
|
347
|
+
expect(button).toBeInTheDocument()
|
|
348
|
+
expect(button).toHaveTextContent('Click me')
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
await step('Button responds to click', async () => {
|
|
352
|
+
await userEvent.click(button)
|
|
353
|
+
// Check for expected behavior
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
await step('Button is keyboard accessible', async () => {
|
|
357
|
+
await userEvent.tab()
|
|
358
|
+
expect(button).toHaveFocus()
|
|
359
|
+
})
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Form Interaction Example
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
export const FormInteraction: Story = {
|
|
368
|
+
render: () => {
|
|
369
|
+
const [value, setValue] = useState('')
|
|
370
|
+
const [submitted, setSubmitted] = useState(false)
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<div>
|
|
374
|
+
<Input
|
|
375
|
+
value={value}
|
|
376
|
+
onChange={(e) => setValue(e.target.value)}
|
|
377
|
+
placeholder="Enter email..."
|
|
378
|
+
/>
|
|
379
|
+
<Button
|
|
380
|
+
onClick={() => setSubmitted(true)}
|
|
381
|
+
disabled={!value}
|
|
382
|
+
>
|
|
383
|
+
Submit
|
|
384
|
+
</Button>
|
|
385
|
+
{submitted && <Alert variant="success">Form submitted!</Alert>}
|
|
386
|
+
</div>
|
|
387
|
+
)
|
|
388
|
+
},
|
|
389
|
+
play: async ({ canvasElement, step }) => {
|
|
390
|
+
const canvas = within(canvasElement)
|
|
391
|
+
const input = canvas.getByPlaceholderText('Enter email...')
|
|
392
|
+
const button = canvas.getByRole('button')
|
|
393
|
+
|
|
394
|
+
await step('User types into input', async () => {
|
|
395
|
+
await userEvent.type(input, 'test@example.com')
|
|
396
|
+
expect(input).toHaveValue('test@example.com')
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
await step('Submit button becomes enabled', async () => {
|
|
400
|
+
expect(button).not.toHaveAttribute('aria-disabled', 'true')
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
await step('User submits form', async () => {
|
|
404
|
+
await userEvent.click(button)
|
|
405
|
+
expect(canvas.getByRole('alert')).toHaveTextContent('Form submitted!')
|
|
406
|
+
})
|
|
407
|
+
},
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## ArgTypes Configuration
|
|
414
|
+
|
|
415
|
+
Control how props appear in Storybook controls:
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
const meta = {
|
|
419
|
+
component: CustomButton,
|
|
420
|
+
argTypes: {
|
|
421
|
+
variant: {
|
|
422
|
+
control: 'select',
|
|
423
|
+
options: ['primary', 'secondary', 'tertiary'],
|
|
424
|
+
description: 'Visual style variant',
|
|
425
|
+
table: {
|
|
426
|
+
defaultValue: { summary: 'primary' },
|
|
427
|
+
type: { summary: 'string' },
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
size: {
|
|
431
|
+
control: 'radio',
|
|
432
|
+
options: ['small', 'medium', 'large'],
|
|
433
|
+
description: 'Button size',
|
|
434
|
+
},
|
|
435
|
+
disabled: {
|
|
436
|
+
control: 'boolean',
|
|
437
|
+
description: 'Disables the button',
|
|
438
|
+
},
|
|
439
|
+
loading: {
|
|
440
|
+
control: 'boolean',
|
|
441
|
+
description: 'Shows loading state',
|
|
442
|
+
},
|
|
443
|
+
onClick: {
|
|
444
|
+
action: 'clicked',
|
|
445
|
+
description: 'Click event handler',
|
|
446
|
+
},
|
|
447
|
+
// Hide internal props
|
|
448
|
+
ref: { table: { disable: true } },
|
|
449
|
+
as: { table: { disable: true } },
|
|
450
|
+
},
|
|
451
|
+
} satisfies Meta<typeof CustomButton>
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Documentation Parameters
|
|
457
|
+
|
|
458
|
+
### Component Description
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
const meta = {
|
|
462
|
+
title: 'Components/StatusButton',
|
|
463
|
+
component: StatusButton,
|
|
464
|
+
parameters: {
|
|
465
|
+
docs: {
|
|
466
|
+
description: {
|
|
467
|
+
component: `
|
|
468
|
+
# StatusButton
|
|
469
|
+
|
|
470
|
+
A button component with an integrated status badge.
|
|
471
|
+
|
|
472
|
+
## Features
|
|
473
|
+
- Built on fpkit Button
|
|
474
|
+
- Includes Badge for status indication
|
|
475
|
+
- Fully accessible
|
|
476
|
+
- Customizable via CSS variables
|
|
477
|
+
|
|
478
|
+
## Usage
|
|
479
|
+
\`\`\`tsx
|
|
480
|
+
<StatusButton status="active" onClick={handleClick}>
|
|
481
|
+
Server Status
|
|
482
|
+
</StatusButton>
|
|
483
|
+
\`\`\`
|
|
484
|
+
|
|
485
|
+
## Composed Components
|
|
486
|
+
- **Button** - Base interactive element
|
|
487
|
+
- **Badge** - Status indicator
|
|
488
|
+
|
|
489
|
+
See [Composition Guide](/docs/guides/composition.md) for composition patterns.
|
|
490
|
+
`,
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
} satisfies Meta<typeof StatusButton>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Story Description
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
export const Primary: Story = {
|
|
501
|
+
args: {
|
|
502
|
+
variant: 'primary',
|
|
503
|
+
},
|
|
504
|
+
parameters: {
|
|
505
|
+
docs: {
|
|
506
|
+
description: {
|
|
507
|
+
story: 'Primary variant for high-emphasis actions.',
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## Organizing Stories
|
|
517
|
+
|
|
518
|
+
### Title Hierarchy
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
// Group by category
|
|
522
|
+
title: 'Components/Buttons/CustomButton'
|
|
523
|
+
title: 'Components/Forms/SearchInput'
|
|
524
|
+
title: 'Compositions/Cards/ActionCard'
|
|
525
|
+
|
|
526
|
+
// Or by feature
|
|
527
|
+
title: 'Features/Authentication/LoginForm'
|
|
528
|
+
title: 'Features/Dashboard/StatsCard'
|
|
529
|
+
|
|
530
|
+
// Or by page
|
|
531
|
+
title: 'Pages/Home/HeroSection'
|
|
532
|
+
title: 'Pages/Settings/ProfileCard'
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Tags
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
tags: ['autodocs'] // Auto-generate documentation
|
|
539
|
+
tags: ['stable'] // Production-ready
|
|
540
|
+
tags: ['beta'] // Testing phase
|
|
541
|
+
tags: ['composition'] // fpkit composition
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## Accessibility Testing in Storybook
|
|
547
|
+
|
|
548
|
+
### Basic Accessibility Test
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
export const AccessibilityTest: Story = {
|
|
552
|
+
play: async ({ canvasElement, step }) => {
|
|
553
|
+
const canvas = within(canvasElement)
|
|
554
|
+
const button = canvas.getByRole('button')
|
|
555
|
+
|
|
556
|
+
await step('Has accessible name', async () => {
|
|
557
|
+
expect(button).toHaveAccessibleName()
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
await step('Is keyboard navigable', async () => {
|
|
561
|
+
await userEvent.tab()
|
|
562
|
+
expect(button).toHaveFocus()
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
await step('Activates with Enter key', async () => {
|
|
566
|
+
await userEvent.keyboard('{Enter}')
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
await step('Activates with Space key', async () => {
|
|
570
|
+
button.focus()
|
|
571
|
+
await userEvent.keyboard(' ')
|
|
572
|
+
})
|
|
573
|
+
},
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### ARIA Attributes Test
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
export const AriaAttributesTest: Story = {
|
|
581
|
+
args: {
|
|
582
|
+
'aria-label': 'Close dialog',
|
|
583
|
+
'aria-describedby': 'hint',
|
|
584
|
+
},
|
|
585
|
+
play: async ({ canvasElement, step }) => {
|
|
586
|
+
const canvas = within(canvasElement)
|
|
587
|
+
const button = canvas.getByRole('button')
|
|
588
|
+
|
|
589
|
+
await step('Has ARIA label', async () => {
|
|
590
|
+
expect(button).toHaveAttribute('aria-label', 'Close dialog')
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
await step('Has ARIA description', async () => {
|
|
594
|
+
expect(button).toHaveAttribute('aria-describedby', 'hint')
|
|
595
|
+
})
|
|
596
|
+
},
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
## State Comparison Stories
|
|
603
|
+
|
|
604
|
+
Show different states side-by-side:
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
export const AllStates: Story = {
|
|
608
|
+
render: () => (
|
|
609
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
610
|
+
<div>
|
|
611
|
+
<h3>Default</h3>
|
|
612
|
+
<Button>Default Button</Button>
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
<div>
|
|
616
|
+
<h3>Hover</h3>
|
|
617
|
+
<Button className="hover">Hover State</Button>
|
|
618
|
+
</div>
|
|
619
|
+
|
|
620
|
+
<div>
|
|
621
|
+
<h3>Focus</h3>
|
|
622
|
+
<Button className="focus">Focus State</Button>
|
|
623
|
+
</div>
|
|
624
|
+
|
|
625
|
+
<div>
|
|
626
|
+
<h3>Disabled</h3>
|
|
627
|
+
<Button disabled>Disabled Button</Button>
|
|
628
|
+
</div>
|
|
629
|
+
|
|
630
|
+
<div>
|
|
631
|
+
<h3>Loading</h3>
|
|
632
|
+
<Button disabled>Loading...</Button>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
),
|
|
636
|
+
parameters: {
|
|
637
|
+
docs: {
|
|
638
|
+
description: {
|
|
639
|
+
story: 'Comparison of all button states',
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
## Best Practices
|
|
649
|
+
|
|
650
|
+
### ✅ Do
|
|
651
|
+
|
|
652
|
+
- **Document compositions** - Show which fpkit components you're using
|
|
653
|
+
- **Include play functions** - Test interactions automatically
|
|
654
|
+
- **Show CSS customization** - Demonstrate variable usage
|
|
655
|
+
- **Test accessibility** - Keyboard navigation, ARIA attributes
|
|
656
|
+
- **Use descriptive titles** - Clear hierarchy and organization
|
|
657
|
+
- **Add component descriptions** - Explain purpose and usage
|
|
658
|
+
- **Show state variants** - Default, hover, disabled, loading, error
|
|
659
|
+
- **Document props** - Use argTypes for prop documentation
|
|
660
|
+
- **Include usage examples** - Code snippets in descriptions
|
|
661
|
+
|
|
662
|
+
### ❌ Don't
|
|
663
|
+
|
|
664
|
+
- **Don't duplicate fpkit stories** - Focus on your custom logic
|
|
665
|
+
- **Don't overcomplicate** - Keep stories simple and focused
|
|
666
|
+
- **Don't skip accessibility** - Always test keyboard and screen readers
|
|
667
|
+
- **Don't forget CSS imports** - Import fpkit styles in preview
|
|
668
|
+
- **Don't use vague titles** - Be specific about what's being demonstrated
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
## Running Storybook
|
|
673
|
+
|
|
674
|
+
```bash
|
|
675
|
+
# Start Storybook dev server
|
|
676
|
+
npm run storybook
|
|
677
|
+
|
|
678
|
+
# Build Storybook static site
|
|
679
|
+
npm run build-storybook
|
|
680
|
+
|
|
681
|
+
# Serve built Storybook
|
|
682
|
+
npx http-server storybook-static
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
## Example: Complete Story File
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
691
|
+
import { within, userEvent, expect, fn } from '@storybook/test'
|
|
692
|
+
import { Button, Badge } from '@fpkit/acss'
|
|
693
|
+
|
|
694
|
+
// Your composed component
|
|
695
|
+
const NotificationButton = ({ count, onClick }) => (
|
|
696
|
+
<Button onClick={onClick} aria-label={`Notifications (${count} unread)`}>
|
|
697
|
+
<span aria-hidden="true">🔔</span>
|
|
698
|
+
{count > 0 && (
|
|
699
|
+
<Badge aria-hidden="true">{count}</Badge>
|
|
700
|
+
)}
|
|
701
|
+
</Button>
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
const meta = {
|
|
705
|
+
title: 'Compositions/NotificationButton',
|
|
706
|
+
component: NotificationButton,
|
|
707
|
+
tags: ['autodocs', 'composition'],
|
|
708
|
+
args: {
|
|
709
|
+
count: 3,
|
|
710
|
+
onClick: fn(),
|
|
711
|
+
},
|
|
712
|
+
argTypes: {
|
|
713
|
+
count: {
|
|
714
|
+
control: 'number',
|
|
715
|
+
description: 'Number of unread notifications',
|
|
716
|
+
},
|
|
717
|
+
onClick: {
|
|
718
|
+
action: 'clicked',
|
|
719
|
+
description: 'Click handler',
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
parameters: {
|
|
723
|
+
docs: {
|
|
724
|
+
description: {
|
|
725
|
+
component: `
|
|
726
|
+
# NotificationButton
|
|
727
|
+
|
|
728
|
+
A button displaying notification count with a badge.
|
|
729
|
+
|
|
730
|
+
**Composed from:**
|
|
731
|
+
- Button (fpkit)
|
|
732
|
+
- Badge (fpkit)
|
|
733
|
+
|
|
734
|
+
**Accessibility:**
|
|
735
|
+
- Uses \`aria-label\` to announce count to screen readers
|
|
736
|
+
- Visual elements hidden from screen readers with \`aria-hidden\`
|
|
737
|
+
`,
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
} satisfies Meta<typeof NotificationButton>
|
|
742
|
+
|
|
743
|
+
export default meta
|
|
744
|
+
type Story = StoryObj<typeof meta>
|
|
745
|
+
|
|
746
|
+
export const Default: Story = {}
|
|
747
|
+
|
|
748
|
+
export const NoNotifications: Story = {
|
|
749
|
+
args: {
|
|
750
|
+
count: 0,
|
|
751
|
+
},
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export const ManyNotifications: Story = {
|
|
755
|
+
args: {
|
|
756
|
+
count: 99,
|
|
757
|
+
},
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export const CustomStyling: Story = {
|
|
761
|
+
render: (args) => (
|
|
762
|
+
<div
|
|
763
|
+
style={{
|
|
764
|
+
'--btn-padding-inline': '1.5rem',
|
|
765
|
+
'--badge-bg': '#ef4444',
|
|
766
|
+
} as React.CSSProperties}
|
|
767
|
+
>
|
|
768
|
+
<NotificationButton {...args} />
|
|
769
|
+
</div>
|
|
770
|
+
),
|
|
771
|
+
args: {
|
|
772
|
+
count: 5,
|
|
773
|
+
},
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export const InteractiveTest: Story = {
|
|
777
|
+
args: {
|
|
778
|
+
count: 3,
|
|
779
|
+
},
|
|
780
|
+
play: async ({ canvasElement, step, args }) => {
|
|
781
|
+
const canvas = within(canvasElement)
|
|
782
|
+
const button = canvas.getByRole('button')
|
|
783
|
+
|
|
784
|
+
await step('Has accessible label with count', async () => {
|
|
785
|
+
expect(button).toHaveAttribute('aria-label', 'Notifications (3 unread)')
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
await step('Shows badge with count', async () => {
|
|
789
|
+
expect(canvas.getByText('3')).toBeInTheDocument()
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
await step('Calls onClick when clicked', async () => {
|
|
793
|
+
await userEvent.click(button)
|
|
794
|
+
expect(args.onClick).toHaveBeenCalledTimes(1)
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
await step('Is keyboard accessible', async () => {
|
|
798
|
+
await userEvent.tab()
|
|
799
|
+
expect(button).toHaveFocus()
|
|
800
|
+
|
|
801
|
+
await userEvent.keyboard('{Enter}')
|
|
802
|
+
expect(args.onClick).toHaveBeenCalledTimes(2)
|
|
803
|
+
})
|
|
804
|
+
},
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
---
|
|
809
|
+
|
|
810
|
+
## Additional Resources
|
|
811
|
+
|
|
812
|
+
- **[Storybook Documentation](https://storybook.js.org/docs)** - Official Storybook docs
|
|
813
|
+
- **[Testing with Storybook](https://storybook.js.org/docs/writing-tests)** - Play functions and interaction testing
|
|
814
|
+
- **[Storybook Addons](https://storybook.js.org/addons)** - Extend functionality
|
|
815
|
+
- **[Component Story Format](https://storybook.js.org/docs/api/csf)** - CSF 3.0 specification
|
|
816
|
+
|
|
817
|
+
---
|
|
818
|
+
|
|
819
|
+
## Related Guides
|
|
820
|
+
|
|
821
|
+
- **[Composition Guide](./composition.md)** - Component composition patterns
|
|
822
|
+
- **[Testing Guide](./testing.md)** - Testing composed components
|
|
823
|
+
- **[Accessibility Guide](./accessibility.md)** - Accessibility testing patterns
|
|
824
|
+
- **[CSS Variables Guide](./css-variables.md)** - Styling customization
|
|
825
|
+
|
|
826
|
+
---
|
|
827
|
+
|
|
828
|
+
**Remember**: Storybook is for documenting **your components and compositions**. fpkit components already have stories in the fpkit Storybook - focus on showcasing how you use and compose them in your application.
|