@girardmedia/bootspring 3.3.2 → 3.4.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/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: storybook-patterns
|
|
3
|
+
description: Storybook patterns for CSF3 stories, args, decorators, play functions, visual regression testing, and addon configuration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Storybook Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Use Storybook to develop, document, and test UI components in isolation. These patterns cover Component Story Format 3 (CSF3), args-based stories, decorators for context providers, play functions for interaction testing, and visual regression testing. Storybook is essential when building a component library, onboarding new developers, or establishing visual QA workflows.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### CSF3 Story Structure
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// components/Button/Button.stories.tsx
|
|
17
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
18
|
+
import { Button } from './Button';
|
|
19
|
+
|
|
20
|
+
const meta = {
|
|
21
|
+
title: 'Components/Button',
|
|
22
|
+
component: Button,
|
|
23
|
+
parameters: {
|
|
24
|
+
layout: 'centered',
|
|
25
|
+
docs: {
|
|
26
|
+
description: { component: 'Primary UI button with multiple variants and sizes.' },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
argTypes: {
|
|
30
|
+
variant: {
|
|
31
|
+
control: 'select',
|
|
32
|
+
options: ['primary', 'secondary', 'ghost', 'danger'],
|
|
33
|
+
description: 'Visual style variant',
|
|
34
|
+
table: { defaultValue: { summary: 'primary' } },
|
|
35
|
+
},
|
|
36
|
+
size: {
|
|
37
|
+
control: 'radio',
|
|
38
|
+
options: ['sm', 'md', 'lg'],
|
|
39
|
+
},
|
|
40
|
+
disabled: { control: 'boolean' },
|
|
41
|
+
loading: { control: 'boolean' },
|
|
42
|
+
onClick: { action: 'clicked' },
|
|
43
|
+
},
|
|
44
|
+
tags: ['autodocs'],
|
|
45
|
+
} satisfies Meta<typeof Button>;
|
|
46
|
+
|
|
47
|
+
export default meta;
|
|
48
|
+
type Story = StoryObj<typeof meta>;
|
|
49
|
+
|
|
50
|
+
export const Primary: Story = {
|
|
51
|
+
args: { children: 'Button', variant: 'primary', size: 'md' },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Secondary: Story = {
|
|
55
|
+
args: { children: 'Button', variant: 'secondary' },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const Loading: Story = {
|
|
59
|
+
args: { children: 'Saving...', loading: true },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const Disabled: Story = {
|
|
63
|
+
args: { children: 'Unavailable', disabled: true },
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Composite Stories (All Variants)
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
export const AllVariants: Story = {
|
|
71
|
+
render: () => (
|
|
72
|
+
<div className="flex flex-col gap-4">
|
|
73
|
+
<div className="flex gap-2 items-center">
|
|
74
|
+
{(['primary', 'secondary', 'ghost', 'danger'] as const).map((variant) => (
|
|
75
|
+
<Button key={variant} variant={variant}>{variant}</Button>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
<div className="flex gap-2 items-center">
|
|
79
|
+
{(['sm', 'md', 'lg'] as const).map((size) => (
|
|
80
|
+
<Button key={size} size={size}>Size {size}</Button>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
),
|
|
85
|
+
parameters: { controls: { disable: true } },
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const DarkMode: Story = {
|
|
89
|
+
args: { children: 'Dark Mode', variant: 'primary' },
|
|
90
|
+
decorators: [
|
|
91
|
+
(Story) => (
|
|
92
|
+
<div className="dark bg-gray-900 p-8 rounded-lg">
|
|
93
|
+
<Story />
|
|
94
|
+
</div>
|
|
95
|
+
),
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Decorators (Context Providers)
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// .storybook/preview.tsx
|
|
104
|
+
import type { Preview } from '@storybook/react';
|
|
105
|
+
import { ThemeProvider } from '../src/providers/ThemeProvider';
|
|
106
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
107
|
+
import '../src/styles/globals.css';
|
|
108
|
+
|
|
109
|
+
const queryClient = new QueryClient({
|
|
110
|
+
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const preview: Preview = {
|
|
114
|
+
decorators: [
|
|
115
|
+
// Theme provider wrapper
|
|
116
|
+
(Story, context) => (
|
|
117
|
+
<ThemeProvider defaultTheme={context.globals.theme ?? 'light'}>
|
|
118
|
+
<Story />
|
|
119
|
+
</ThemeProvider>
|
|
120
|
+
),
|
|
121
|
+
// React Query provider
|
|
122
|
+
(Story) => (
|
|
123
|
+
<QueryClientProvider client={queryClient}>
|
|
124
|
+
<Story />
|
|
125
|
+
</QueryClientProvider>
|
|
126
|
+
),
|
|
127
|
+
// Layout padding
|
|
128
|
+
(Story) => (
|
|
129
|
+
<div className="p-4">
|
|
130
|
+
<Story />
|
|
131
|
+
</div>
|
|
132
|
+
),
|
|
133
|
+
],
|
|
134
|
+
globalTypes: {
|
|
135
|
+
theme: {
|
|
136
|
+
description: 'Global theme',
|
|
137
|
+
toolbar: {
|
|
138
|
+
title: 'Theme',
|
|
139
|
+
icon: 'circlehollow',
|
|
140
|
+
items: ['light', 'dark'],
|
|
141
|
+
dynamicTitle: true,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
parameters: {
|
|
146
|
+
actions: { argTypesRegex: '^on[A-Z].*' },
|
|
147
|
+
controls: { matchers: { color: /(background|color)$/i, date: /Date$/i } },
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export default preview;
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Play Functions (Interaction Testing)
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// components/LoginForm/LoginForm.stories.tsx
|
|
158
|
+
import { within, userEvent, expect, waitFor } from '@storybook/test';
|
|
159
|
+
|
|
160
|
+
export const FilledForm: Story = {
|
|
161
|
+
play: async ({ canvasElement }) => {
|
|
162
|
+
const canvas = within(canvasElement);
|
|
163
|
+
|
|
164
|
+
// Type into email field
|
|
165
|
+
const emailInput = canvas.getByLabelText('Email');
|
|
166
|
+
await userEvent.clear(emailInput);
|
|
167
|
+
await userEvent.type(emailInput, 'user@example.com');
|
|
168
|
+
|
|
169
|
+
// Type into password field
|
|
170
|
+
const passwordInput = canvas.getByLabelText('Password');
|
|
171
|
+
await userEvent.clear(passwordInput);
|
|
172
|
+
await userEvent.type(passwordInput, 'SecurePass123');
|
|
173
|
+
|
|
174
|
+
// Verify values
|
|
175
|
+
expect(emailInput).toHaveValue('user@example.com');
|
|
176
|
+
expect(passwordInput).toHaveValue('SecurePass123');
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const SubmitWithValidation: Story = {
|
|
181
|
+
play: async ({ canvasElement }) => {
|
|
182
|
+
const canvas = within(canvasElement);
|
|
183
|
+
|
|
184
|
+
// Submit empty form
|
|
185
|
+
const submitButton = canvas.getByRole('button', { name: /sign in/i });
|
|
186
|
+
await userEvent.click(submitButton);
|
|
187
|
+
|
|
188
|
+
// Check validation errors
|
|
189
|
+
await waitFor(() => {
|
|
190
|
+
expect(canvas.getByText('Email is required')).toBeInTheDocument();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Fill and submit
|
|
194
|
+
await userEvent.type(canvas.getByLabelText('Email'), 'user@example.com');
|
|
195
|
+
await userEvent.type(canvas.getByLabelText('Password'), 'SecurePass123');
|
|
196
|
+
await userEvent.click(submitButton);
|
|
197
|
+
|
|
198
|
+
// Verify errors cleared
|
|
199
|
+
await waitFor(() => {
|
|
200
|
+
expect(canvas.queryByText('Email is required')).not.toBeInTheDocument();
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### MSW Integration (Mocked API)
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// components/UserProfile/UserProfile.stories.tsx
|
|
210
|
+
import { http, HttpResponse } from 'msw';
|
|
211
|
+
|
|
212
|
+
export const WithData: Story = {
|
|
213
|
+
parameters: {
|
|
214
|
+
msw: {
|
|
215
|
+
handlers: [
|
|
216
|
+
http.get('/api/user/profile', () =>
|
|
217
|
+
HttpResponse.json({
|
|
218
|
+
name: 'Jane Doe',
|
|
219
|
+
email: 'jane@example.com',
|
|
220
|
+
avatar: 'https://placekitten.com/200/200',
|
|
221
|
+
})
|
|
222
|
+
),
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export const Loading: Story = {
|
|
229
|
+
parameters: {
|
|
230
|
+
msw: {
|
|
231
|
+
handlers: [
|
|
232
|
+
http.get('/api/user/profile', async () => {
|
|
233
|
+
await new Promise((r) => setTimeout(r, 999999)); // never resolves
|
|
234
|
+
return HttpResponse.json({});
|
|
235
|
+
}),
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export const Error: Story = {
|
|
242
|
+
parameters: {
|
|
243
|
+
msw: {
|
|
244
|
+
handlers: [
|
|
245
|
+
http.get('/api/user/profile', () =>
|
|
246
|
+
HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
247
|
+
),
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Visual Regression with Chromatic
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// .storybook/main.ts
|
|
258
|
+
import type { StorybookConfig } from '@storybook/react-vite';
|
|
259
|
+
|
|
260
|
+
const config: StorybookConfig = {
|
|
261
|
+
stories: ['../src/**/*.stories.@(ts|tsx)'],
|
|
262
|
+
addons: [
|
|
263
|
+
'@storybook/addon-essentials',
|
|
264
|
+
'@storybook/addon-interactions',
|
|
265
|
+
'@storybook/addon-a11y',
|
|
266
|
+
'@chromatic-com/storybook',
|
|
267
|
+
],
|
|
268
|
+
framework: '@storybook/react-vite',
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export default config;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
```yaml
|
|
275
|
+
# .github/workflows/chromatic.yml
|
|
276
|
+
name: Visual Tests
|
|
277
|
+
on: push
|
|
278
|
+
jobs:
|
|
279
|
+
chromatic:
|
|
280
|
+
runs-on: ubuntu-latest
|
|
281
|
+
steps:
|
|
282
|
+
- uses: actions/checkout@v4
|
|
283
|
+
with: { fetch-depth: 0 }
|
|
284
|
+
- uses: actions/setup-node@v4
|
|
285
|
+
with: { node-version: 20, cache: npm }
|
|
286
|
+
- run: npm ci
|
|
287
|
+
- uses: chromaui/action@latest
|
|
288
|
+
with:
|
|
289
|
+
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
|
290
|
+
exitOnceUploaded: true
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Accessibility Testing Addon
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// Per-story a11y configuration
|
|
297
|
+
export const AccessibleButton: Story = {
|
|
298
|
+
args: { children: 'Click me', variant: 'primary' },
|
|
299
|
+
parameters: {
|
|
300
|
+
a11y: {
|
|
301
|
+
config: {
|
|
302
|
+
rules: [
|
|
303
|
+
{ id: 'color-contrast', enabled: true },
|
|
304
|
+
{ id: 'button-name', enabled: true },
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Examples
|
|
313
|
+
|
|
314
|
+
| Story Type | Purpose | Key Feature |
|
|
315
|
+
|------------|---------|-------------|
|
|
316
|
+
| Args-based | Default prop permutations | Controls panel auto-generated |
|
|
317
|
+
| Composite render | All variants at once | Visual comparison grid |
|
|
318
|
+
| Play function | Interaction simulation | Click, type, assert in browser |
|
|
319
|
+
| MSW handler | API state simulation | Loading, success, error states |
|
|
320
|
+
| Chromatic snapshot | Visual regression | Pixel-diff on every PR |
|
|
321
|
+
|
|
322
|
+
## Checklist
|
|
323
|
+
- [ ] Stories use CSF3 format with `satisfies Meta<typeof Component>`
|
|
324
|
+
- [ ] `argTypes` defined for every configurable prop with descriptions
|
|
325
|
+
- [ ] `tags: ['autodocs']` enabled for automatic documentation generation
|
|
326
|
+
- [ ] Decorators provide all necessary context (theme, router, query client)
|
|
327
|
+
- [ ] Play functions test critical interaction flows with assertions
|
|
328
|
+
- [ ] MSW handlers mock all API states (success, loading, error)
|
|
329
|
+
- [ ] `@storybook/addon-a11y` enabled for accessibility violation detection
|
|
330
|
+
- [ ] Visual regression testing configured (Chromatic or Percy) in CI
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: svelte-patterns
|
|
3
|
+
description: Svelte patterns for reactivity, stores, actions, transitions, SvelteKit routing, and load functions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Svelte Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when building Svelte 4/5 or SvelteKit applications. Use this
|
|
11
|
+
skill for understanding Svelte's reactivity model, creating and using stores,
|
|
12
|
+
adding DOM behavior with actions, animating with transitions, and structuring
|
|
13
|
+
SvelteKit routes with load functions.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### Reactivity
|
|
18
|
+
|
|
19
|
+
Svelte's reactivity is compile-time. In Svelte 4, use `$:` for reactive
|
|
20
|
+
declarations. In Svelte 5 (runes), use `$state`, `$derived`, and `$effect`.
|
|
21
|
+
|
|
22
|
+
```svelte
|
|
23
|
+
<!-- Svelte 5 (runes) -->
|
|
24
|
+
<script lang="ts">
|
|
25
|
+
let count = $state(0)
|
|
26
|
+
let doubled = $derived(count * 2)
|
|
27
|
+
let items = $state<string[]>([])
|
|
28
|
+
|
|
29
|
+
$effect(() => {
|
|
30
|
+
console.log(`Count changed to ${count}`)
|
|
31
|
+
// cleanup returned automatically on re-run
|
|
32
|
+
return () => console.log('cleaning up')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
function addItem(text: string) {
|
|
36
|
+
items.push(text) // mutations tracked automatically with runes
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<button onclick={() => count++}>
|
|
41
|
+
{count} (doubled: {doubled})
|
|
42
|
+
</button>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```svelte
|
|
46
|
+
<!-- Svelte 4 (legacy reactive) -->
|
|
47
|
+
<script lang="ts">
|
|
48
|
+
let count = 0
|
|
49
|
+
$: doubled = count * 2
|
|
50
|
+
$: if (count > 10) {
|
|
51
|
+
console.log('count is high')
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Stores
|
|
57
|
+
|
|
58
|
+
Use writable stores for shared state. Use derived stores for computed values.
|
|
59
|
+
Use readable stores for external data sources. Subscribe with `$store` syntax.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// stores/auth.ts
|
|
63
|
+
import { writable, derived } from 'svelte/store'
|
|
64
|
+
|
|
65
|
+
interface User { id: string; name: string; role: string }
|
|
66
|
+
|
|
67
|
+
export const currentUser = writable<User | null>(null)
|
|
68
|
+
export const isAuthenticated = derived(currentUser, $user => $user !== null)
|
|
69
|
+
export const isAdmin = derived(currentUser, $user => $user?.role === 'admin')
|
|
70
|
+
|
|
71
|
+
export function login(credentials: Credentials) {
|
|
72
|
+
return api.login(credentials).then(user => {
|
|
73
|
+
currentUser.set(user)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function logout() {
|
|
78
|
+
currentUser.set(null)
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```svelte
|
|
83
|
+
<script>
|
|
84
|
+
import { currentUser, isAuthenticated, logout } from '$lib/stores/auth'
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
{#if $isAuthenticated}
|
|
88
|
+
<p>Welcome, {$currentUser.name}</p>
|
|
89
|
+
<button onclick={logout}>Logout</button>
|
|
90
|
+
{:else}
|
|
91
|
+
<a href="/login">Sign in</a>
|
|
92
|
+
{/if}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Actions
|
|
96
|
+
|
|
97
|
+
Actions add reusable DOM behavior to elements. Use them for click-outside
|
|
98
|
+
detection, tooltips, focus traps, and intersection observers.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// actions/clickOutside.ts
|
|
102
|
+
export function clickOutside(node: HTMLElement, callback: () => void) {
|
|
103
|
+
function handleClick(event: MouseEvent) {
|
|
104
|
+
if (!node.contains(event.target as Node)) {
|
|
105
|
+
callback()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
document.addEventListener('click', handleClick, true)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
destroy() {
|
|
113
|
+
document.removeEventListener('click', handleClick, true)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// actions/lazyLoad.ts
|
|
119
|
+
export function lazyLoad(node: HTMLImageElement) {
|
|
120
|
+
const observer = new IntersectionObserver((entries) => {
|
|
121
|
+
entries.forEach(entry => {
|
|
122
|
+
if (entry.isIntersecting) {
|
|
123
|
+
const src = node.dataset.src
|
|
124
|
+
if (src) node.src = src
|
|
125
|
+
observer.unobserve(node)
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
observer.observe(node)
|
|
130
|
+
return { destroy: () => observer.disconnect() }
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```svelte
|
|
135
|
+
<div use:clickOutside={() => menuOpen = false}>
|
|
136
|
+
<DropdownMenu bind:open={menuOpen} />
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<img use:lazyLoad data-src="/large-image.jpg" alt="Lazy loaded" />
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Transitions
|
|
143
|
+
|
|
144
|
+
Svelte has built-in transition directives. Use `transition:` for enter+leave,
|
|
145
|
+
`in:` and `out:` for separate control. Combine with `animate:flip` for list
|
|
146
|
+
reordering.
|
|
147
|
+
|
|
148
|
+
```svelte
|
|
149
|
+
<script>
|
|
150
|
+
import { fade, slide, fly } from 'svelte/transition'
|
|
151
|
+
import { flip } from 'svelte/animate'
|
|
152
|
+
|
|
153
|
+
let items = $state([...])
|
|
154
|
+
let visible = $state(true)
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
{#if visible}
|
|
158
|
+
<div transition:fade={{ duration: 200 }}>
|
|
159
|
+
Fades in and out
|
|
160
|
+
</div>
|
|
161
|
+
{/if}
|
|
162
|
+
|
|
163
|
+
{#each items as item (item.id)}
|
|
164
|
+
<div
|
|
165
|
+
animate:flip={{ duration: 300 }}
|
|
166
|
+
in:fly={{ y: 20, duration: 200 }}
|
|
167
|
+
out:fade={{ duration: 150 }}
|
|
168
|
+
>
|
|
169
|
+
{item.text}
|
|
170
|
+
</div>
|
|
171
|
+
{/each}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### SvelteKit Routing
|
|
175
|
+
|
|
176
|
+
File-based routing with `+page.svelte`, `+layout.svelte`, `+page.server.ts`.
|
|
177
|
+
Use `+page.ts` for universal load, `+page.server.ts` for server-only load.
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
routes/
|
|
181
|
+
+layout.svelte # root layout
|
|
182
|
+
+page.svelte # home page
|
|
183
|
+
projects/
|
|
184
|
+
+page.svelte # /projects
|
|
185
|
+
+page.server.ts # server load for /projects
|
|
186
|
+
[id]/
|
|
187
|
+
+page.svelte # /projects/:id
|
|
188
|
+
+page.ts # universal load
|
|
189
|
+
+error.svelte # error boundary
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Load Functions
|
|
193
|
+
|
|
194
|
+
Use `load` functions to fetch data before rendering. Return data as plain objects.
|
|
195
|
+
Use `depends` for invalidation. Throw `error()` for error pages.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// routes/projects/[id]/+page.server.ts
|
|
199
|
+
import { error } from '@sveltejs/kit'
|
|
200
|
+
import type { PageServerLoad } from './$types'
|
|
201
|
+
|
|
202
|
+
export const load: PageServerLoad = async ({ params, locals }) => {
|
|
203
|
+
const project = await db.project.findUnique({
|
|
204
|
+
where: { id: params.id, ownerId: locals.userId }
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
if (!project) {
|
|
208
|
+
throw error(404, 'Project not found')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
project,
|
|
213
|
+
tasks: await db.task.findMany({ where: { projectId: params.id } })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```svelte
|
|
219
|
+
<!-- routes/projects/[id]/+page.svelte -->
|
|
220
|
+
<script lang="ts">
|
|
221
|
+
import type { PageData } from './$types'
|
|
222
|
+
let { data }: { data: PageData } = $props()
|
|
223
|
+
</script>
|
|
224
|
+
|
|
225
|
+
<h1>{data.project.name}</h1>
|
|
226
|
+
{#each data.tasks as task}
|
|
227
|
+
<TaskCard {task} />
|
|
228
|
+
{/each}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Examples
|
|
232
|
+
|
|
233
|
+
**Pattern: Form action with progressive enhancement**
|
|
234
|
+
```typescript
|
|
235
|
+
// +page.server.ts
|
|
236
|
+
export const actions = {
|
|
237
|
+
default: async ({ request }) => {
|
|
238
|
+
const data = await request.formData()
|
|
239
|
+
const name = data.get('name') as string
|
|
240
|
+
if (!name) return fail(400, { name, missing: true })
|
|
241
|
+
await db.project.create({ data: { name } })
|
|
242
|
+
throw redirect(303, '/projects')
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Checklist
|
|
248
|
+
|
|
249
|
+
- [ ] Svelte 5 runes (`$state`, `$derived`, `$effect`) over `$:` reactive statements
|
|
250
|
+
- [ ] Stores for shared cross-component state; `$store` for auto-subscriptions
|
|
251
|
+
- [ ] Actions (`use:action`) for reusable DOM behavior with proper `destroy` cleanup
|
|
252
|
+
- [ ] Transitions use built-in directives (`transition:`, `in:`, `out:`)
|
|
253
|
+
- [ ] `animate:flip` on `{#each}` blocks with keyed items for smooth reordering
|
|
254
|
+
- [ ] SvelteKit load functions return plain objects, throw `error()` for error pages
|
|
255
|
+
- [ ] Server-only data in `+page.server.ts`, universal data in `+page.ts`
|
|
256
|
+
- [ ] Form actions with `fail()` for validation errors, `redirect()` for success
|
|
257
|
+
- [ ] `$effect` cleanup functions returned for subscriptions and listeners
|
|
258
|
+
- [ ] `{#snippet}` blocks (Svelte 5) for reusable template fragments
|