@pyreon/storybook 0.0.1
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/LICENSE +21 -0
- package/README.md +200 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/analysis/preset.js.html +5406 -0
- package/lib/analysis/preview.js.html +5406 -0
- package/lib/index.js +49 -0
- package/lib/index.js.map +1 -0
- package/lib/preset.js +20 -0
- package/lib/preset.js.map +1 -0
- package/lib/preview.js +53 -0
- package/lib/preview.js.map +1 -0
- package/lib/types/index.d.ts +116 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/preset.d.ts +17 -0
- package/lib/types/preset.d.ts.map +1 -0
- package/lib/types/preset2.d.ts +19 -0
- package/lib/types/preset2.d.ts.map +1 -0
- package/lib/types/preview.d.ts +57 -0
- package/lib/types/preview.d.ts.map +1 -0
- package/lib/types/preview2.d.ts +45 -0
- package/lib/types/preview2.d.ts.map +1 -0
- package/package.json +59 -0
- package/src/index.ts +45 -0
- package/src/preset.ts +24 -0
- package/src/preview.ts +29 -0
- package/src/render.ts +75 -0
- package/src/tests/storybook.test.ts +413 -0
- package/src/types.ts +102 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { h, Fragment } from '@pyreon/core'
|
|
2
|
+
import type { ComponentFn, VNodeChild } from '@pyreon/core'
|
|
3
|
+
import { signal, effect } from '@pyreon/reactivity'
|
|
4
|
+
import { mount } from '@pyreon/runtime-dom'
|
|
5
|
+
import { renderToCanvas, defaultRender } from '../render'
|
|
6
|
+
import { render as previewRender } from '../preview'
|
|
7
|
+
import type {
|
|
8
|
+
Meta,
|
|
9
|
+
StoryObj,
|
|
10
|
+
DecoratorFn,
|
|
11
|
+
StoryFn,
|
|
12
|
+
StoryContext,
|
|
13
|
+
} from '../types'
|
|
14
|
+
|
|
15
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function createCanvas(): HTMLElement {
|
|
18
|
+
const el = document.createElement('div')
|
|
19
|
+
document.body.appendChild(el)
|
|
20
|
+
return el
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeRenderContext(overrides: {
|
|
24
|
+
storyFn?: () => VNodeChild
|
|
25
|
+
component?: ComponentFn<any>
|
|
26
|
+
args?: Record<string, unknown>
|
|
27
|
+
}) {
|
|
28
|
+
return {
|
|
29
|
+
storyFn: overrides.storyFn ?? (() => h('div', null, 'default')),
|
|
30
|
+
storyContext: {
|
|
31
|
+
component: overrides.component,
|
|
32
|
+
args: overrides.args ?? {},
|
|
33
|
+
},
|
|
34
|
+
showMain: () => {
|
|
35
|
+
/* noop */
|
|
36
|
+
},
|
|
37
|
+
showError: (_err: { title: string; description: string }) => {
|
|
38
|
+
/* noop */
|
|
39
|
+
},
|
|
40
|
+
forceRemount: false,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── renderToCanvas ──────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
describe('renderToCanvas', () => {
|
|
47
|
+
it('mounts a simple VNode into the canvas', () => {
|
|
48
|
+
const canvas = createCanvas()
|
|
49
|
+
const ctx = makeRenderContext({
|
|
50
|
+
storyFn: () => h('button', null, 'Click me'),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
renderToCanvas(ctx, canvas)
|
|
54
|
+
|
|
55
|
+
expect(canvas.innerHTML).toContain('Click me')
|
|
56
|
+
expect(canvas.querySelector('button')).toBeTruthy()
|
|
57
|
+
canvas.remove()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('mounts a Pyreon component with props', () => {
|
|
61
|
+
function Button(props: { label: string; disabled?: boolean }) {
|
|
62
|
+
return h('button', { disabled: props.disabled ?? false }, props.label)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const canvas = createCanvas()
|
|
66
|
+
const ctx = makeRenderContext({
|
|
67
|
+
storyFn: () => h(Button, { label: 'Submit', disabled: true }),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
renderToCanvas(ctx, canvas)
|
|
71
|
+
|
|
72
|
+
const btn = canvas.querySelector('button')!
|
|
73
|
+
expect(btn).toBeTruthy()
|
|
74
|
+
expect(btn.textContent).toBe('Submit')
|
|
75
|
+
expect(btn.getAttribute('disabled')).not.toBeNull()
|
|
76
|
+
canvas.remove()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('cleans up previous mount on re-render', () => {
|
|
80
|
+
const canvas = createCanvas()
|
|
81
|
+
|
|
82
|
+
renderToCanvas(
|
|
83
|
+
makeRenderContext({ storyFn: () => h('div', null, 'First') }),
|
|
84
|
+
canvas,
|
|
85
|
+
)
|
|
86
|
+
expect(canvas.textContent).toBe('First')
|
|
87
|
+
|
|
88
|
+
renderToCanvas(
|
|
89
|
+
makeRenderContext({ storyFn: () => h('div', null, 'Second') }),
|
|
90
|
+
canvas,
|
|
91
|
+
)
|
|
92
|
+
expect(canvas.textContent).toBe('Second')
|
|
93
|
+
// Only one child — previous mount was cleaned up
|
|
94
|
+
expect(canvas.children.length).toBe(1)
|
|
95
|
+
canvas.remove()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('disposes reactive effects on cleanup', () => {
|
|
99
|
+
const canvas = createCanvas()
|
|
100
|
+
let effectRunCount = 0
|
|
101
|
+
|
|
102
|
+
const count = signal(0)
|
|
103
|
+
function Counter() {
|
|
104
|
+
effect(() => {
|
|
105
|
+
count()
|
|
106
|
+
effectRunCount++
|
|
107
|
+
})
|
|
108
|
+
return h('span', null, () => `${count()}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
renderToCanvas(
|
|
112
|
+
makeRenderContext({ storyFn: () => h(Counter, null) }),
|
|
113
|
+
canvas,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const initialCount = effectRunCount
|
|
117
|
+
count.set(1)
|
|
118
|
+
expect(effectRunCount).toBe(initialCount + 1)
|
|
119
|
+
|
|
120
|
+
// Re-render with a different story — should dispose previous effects
|
|
121
|
+
renderToCanvas(
|
|
122
|
+
makeRenderContext({ storyFn: () => h('div', null, 'New story') }),
|
|
123
|
+
canvas,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const countAfterCleanup = effectRunCount
|
|
127
|
+
count.set(2)
|
|
128
|
+
count.set(3)
|
|
129
|
+
// Effect should NOT have run again — it was disposed
|
|
130
|
+
expect(effectRunCount).toBe(countAfterCleanup)
|
|
131
|
+
canvas.remove()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('shows error when storyFn throws an Error', () => {
|
|
135
|
+
const canvas = createCanvas()
|
|
136
|
+
let errorShown: { title: string; description: string } | null = null
|
|
137
|
+
|
|
138
|
+
const ctx = {
|
|
139
|
+
storyFn: () => {
|
|
140
|
+
throw new Error('Boom')
|
|
141
|
+
},
|
|
142
|
+
storyContext: { args: {} },
|
|
143
|
+
showMain: () => {
|
|
144
|
+
/* noop */
|
|
145
|
+
},
|
|
146
|
+
showError: (err: { title: string; description: string }) => {
|
|
147
|
+
errorShown = err
|
|
148
|
+
},
|
|
149
|
+
forceRemount: false,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
renderToCanvas(ctx, canvas)
|
|
153
|
+
|
|
154
|
+
expect(errorShown).not.toBeNull()
|
|
155
|
+
expect(errorShown!.description).toBe('Boom')
|
|
156
|
+
canvas.remove()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('shows error when storyFn throws a non-Error value', () => {
|
|
160
|
+
const canvas = createCanvas()
|
|
161
|
+
let errorShown: { title: string; description: string } | null = null
|
|
162
|
+
|
|
163
|
+
const ctx = {
|
|
164
|
+
storyFn: () => {
|
|
165
|
+
throw 'string error'
|
|
166
|
+
},
|
|
167
|
+
storyContext: { args: {} },
|
|
168
|
+
showMain: () => {
|
|
169
|
+
/* noop */
|
|
170
|
+
},
|
|
171
|
+
showError: (err: { title: string; description: string }) => {
|
|
172
|
+
errorShown = err
|
|
173
|
+
},
|
|
174
|
+
forceRemount: false,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
renderToCanvas(ctx, canvas)
|
|
178
|
+
|
|
179
|
+
expect(errorShown).not.toBeNull()
|
|
180
|
+
expect(errorShown!.description).toBe('string error')
|
|
181
|
+
canvas.remove()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('renders reactive components that update the DOM', () => {
|
|
185
|
+
const canvas = createCanvas()
|
|
186
|
+
const count = signal(0)
|
|
187
|
+
|
|
188
|
+
function Counter() {
|
|
189
|
+
return h('span', { 'data-testid': 'count' }, () => `Count: ${count()}`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
renderToCanvas(
|
|
193
|
+
makeRenderContext({ storyFn: () => h(Counter, null) }),
|
|
194
|
+
canvas,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
expect(canvas.textContent).toBe('Count: 0')
|
|
198
|
+
|
|
199
|
+
count.set(5)
|
|
200
|
+
expect(canvas.textContent).toBe('Count: 5')
|
|
201
|
+
canvas.remove()
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// ─── defaultRender ───────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
describe('defaultRender', () => {
|
|
208
|
+
it('creates a VNode from component + args', () => {
|
|
209
|
+
function Greeting(props: { name: string }) {
|
|
210
|
+
return h('p', null, `Hello, ${props.name}!`)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const canvas = createCanvas()
|
|
214
|
+
const vnode = defaultRender(Greeting, { name: 'World' })
|
|
215
|
+
const unmount = mount(vnode, canvas)
|
|
216
|
+
|
|
217
|
+
expect(canvas.textContent).toBe('Hello, World!')
|
|
218
|
+
unmount()
|
|
219
|
+
canvas.remove()
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// ─── Type-level tests (Meta / StoryObj) ──────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
describe('Meta and StoryObj types', () => {
|
|
226
|
+
it('Meta accepts a component and typed args', () => {
|
|
227
|
+
function Button(props: {
|
|
228
|
+
label: string
|
|
229
|
+
variant?: 'primary' | 'secondary'
|
|
230
|
+
}) {
|
|
231
|
+
return h('button', { class: props.variant }, props.label)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const meta = {
|
|
235
|
+
component: Button,
|
|
236
|
+
title: 'Button',
|
|
237
|
+
args: { label: 'Click', variant: 'primary' as const },
|
|
238
|
+
tags: ['autodocs'],
|
|
239
|
+
} satisfies Meta<typeof Button>
|
|
240
|
+
|
|
241
|
+
expect(meta.component).toBe(Button)
|
|
242
|
+
expect(meta.args!.label).toBe('Click')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('StoryObj inherits args from Meta', () => {
|
|
246
|
+
function Input(props: { placeholder: string; disabled?: boolean }) {
|
|
247
|
+
return h('input', {
|
|
248
|
+
placeholder: props.placeholder,
|
|
249
|
+
disabled: props.disabled,
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const _meta = {
|
|
254
|
+
component: Input,
|
|
255
|
+
args: { placeholder: 'Type here' },
|
|
256
|
+
} satisfies Meta<typeof Input>
|
|
257
|
+
|
|
258
|
+
type Story = StoryObj<typeof _meta>
|
|
259
|
+
|
|
260
|
+
const primary: Story = {
|
|
261
|
+
args: { disabled: true },
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
expect(primary.args!.disabled).toBe(true)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('StoryObj supports custom render function', () => {
|
|
268
|
+
function Card(props: { title: string }) {
|
|
269
|
+
return h('div', { class: 'card' }, h('h2', null, props.title))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const _meta = {
|
|
273
|
+
component: Card,
|
|
274
|
+
args: { title: 'Default' },
|
|
275
|
+
} satisfies Meta<typeof Card>
|
|
276
|
+
|
|
277
|
+
type Story = StoryObj<typeof _meta>
|
|
278
|
+
|
|
279
|
+
const withWrapper: Story = {
|
|
280
|
+
render: (args) => h('div', { class: 'wrapper' }, h(Card, args)),
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const canvas = createCanvas()
|
|
284
|
+
const vnode = withWrapper.render!({ title: 'Custom' }, {} as any)
|
|
285
|
+
const unmount = mount(vnode, canvas)
|
|
286
|
+
|
|
287
|
+
expect(canvas.querySelector('.wrapper')).toBeTruthy()
|
|
288
|
+
expect(canvas.querySelector('.card')).toBeTruthy()
|
|
289
|
+
expect(canvas.textContent).toBe('Custom')
|
|
290
|
+
unmount()
|
|
291
|
+
canvas.remove()
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
// ─── Decorators ──────────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
describe('Decorators', () => {
|
|
298
|
+
it('decorator wraps a story', () => {
|
|
299
|
+
function Button(props: { label: string }) {
|
|
300
|
+
return h('button', null, props.label)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const withPadding: DecoratorFn<{ label: string }> = (storyFn, context) => {
|
|
304
|
+
return h(
|
|
305
|
+
'div',
|
|
306
|
+
{ style: 'padding: 1rem' },
|
|
307
|
+
storyFn(context.args, context),
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const canvas = createCanvas()
|
|
312
|
+
const storyResult = withPadding((args) => h(Button, args), {
|
|
313
|
+
args: { label: 'Wrapped' },
|
|
314
|
+
argTypes: {},
|
|
315
|
+
globals: {},
|
|
316
|
+
id: '1',
|
|
317
|
+
kind: 'Button',
|
|
318
|
+
name: 'Primary',
|
|
319
|
+
viewMode: 'story',
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
const unmount = mount(storyResult, canvas)
|
|
323
|
+
expect(canvas.querySelector('div[style]')).toBeTruthy()
|
|
324
|
+
expect(canvas.querySelector('button')!.textContent).toBe('Wrapped')
|
|
325
|
+
unmount()
|
|
326
|
+
canvas.remove()
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('multiple decorators compose correctly', () => {
|
|
330
|
+
function Text(props: { content: string }) {
|
|
331
|
+
return h('span', null, props.content)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const withBorder: DecoratorFn<{ content: string }> = (storyFn, ctx) =>
|
|
335
|
+
h('div', { class: 'border' }, storyFn(ctx.args, ctx))
|
|
336
|
+
|
|
337
|
+
const withTheme: DecoratorFn<{ content: string }> = (storyFn, ctx) =>
|
|
338
|
+
h('div', { class: 'theme-dark' }, storyFn(ctx.args, ctx))
|
|
339
|
+
|
|
340
|
+
const context: StoryContext<{ content: string }> = {
|
|
341
|
+
args: { content: 'Hello' },
|
|
342
|
+
argTypes: {},
|
|
343
|
+
globals: {},
|
|
344
|
+
id: '1',
|
|
345
|
+
kind: 'Text',
|
|
346
|
+
name: 'Default',
|
|
347
|
+
viewMode: 'story',
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Compose: withTheme(withBorder(story))
|
|
351
|
+
const story: StoryFn<{ content: string }> = (args) => h(Text, args)
|
|
352
|
+
const decorated = withTheme((_args, ctx) => withBorder(story, ctx), context)
|
|
353
|
+
|
|
354
|
+
const canvas = createCanvas()
|
|
355
|
+
const unmount = mount(decorated, canvas)
|
|
356
|
+
expect(canvas.querySelector('.theme-dark')).toBeTruthy()
|
|
357
|
+
expect(canvas.querySelector('.border')).toBeTruthy()
|
|
358
|
+
expect(canvas.querySelector('span')!.textContent).toBe('Hello')
|
|
359
|
+
unmount()
|
|
360
|
+
canvas.remove()
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
// ─── Fragment and multiple children ──────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
describe('Fragment stories', () => {
|
|
367
|
+
it('renders a story returning a Fragment', () => {
|
|
368
|
+
const canvas = createCanvas()
|
|
369
|
+
renderToCanvas(
|
|
370
|
+
makeRenderContext({
|
|
371
|
+
storyFn: () =>
|
|
372
|
+
h(Fragment, null, h('p', null, 'Line 1'), h('p', null, 'Line 2')),
|
|
373
|
+
}),
|
|
374
|
+
canvas,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
const paragraphs = canvas.querySelectorAll('p')
|
|
378
|
+
expect(paragraphs.length).toBe(2)
|
|
379
|
+
expect(paragraphs[0]!.textContent).toBe('Line 1')
|
|
380
|
+
expect(paragraphs[1]!.textContent).toBe('Line 2')
|
|
381
|
+
canvas.remove()
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// ─── Preview render function ─────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
describe('preview render', () => {
|
|
388
|
+
it('renders a component with args', () => {
|
|
389
|
+
function Badge(props: { text: string }) {
|
|
390
|
+
return h('span', { class: 'badge' }, props.text)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const canvas = createCanvas()
|
|
394
|
+
const vnode = previewRender({ text: 'New' }, { component: Badge })
|
|
395
|
+
const unmount = mount(vnode, canvas)
|
|
396
|
+
|
|
397
|
+
expect(canvas.querySelector('.badge')!.textContent).toBe('New')
|
|
398
|
+
unmount()
|
|
399
|
+
canvas.remove()
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('throws when no component is provided', () => {
|
|
403
|
+
expect(() => previewRender({ foo: 'bar' }, {})).toThrow(
|
|
404
|
+
'[@pyreon/storybook] No component provided',
|
|
405
|
+
)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('throws when component is undefined', () => {
|
|
409
|
+
expect(() =>
|
|
410
|
+
previewRender({ foo: 'bar' }, { component: undefined }),
|
|
411
|
+
).toThrow('[@pyreon/storybook] No component provided')
|
|
412
|
+
})
|
|
413
|
+
})
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ComponentFn, Props, VNodeChild } from '@pyreon/core'
|
|
2
|
+
|
|
3
|
+
// ─── Storybook Renderer Interface ────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The Pyreon renderer descriptor used by Storybook internally.
|
|
7
|
+
* This tells Storybook what our "component" and "storyResult" types are.
|
|
8
|
+
*/
|
|
9
|
+
export interface PyreonRenderer {
|
|
10
|
+
component: ComponentFn<any>
|
|
11
|
+
storyResult: VNodeChild
|
|
12
|
+
canvasElement: HTMLElement
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ─── Args & ArgTypes ─────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** Extract props type from a Pyreon component function. */
|
|
18
|
+
export type InferProps<T> = T extends ComponentFn<infer P> ? P : Props
|
|
19
|
+
|
|
20
|
+
// ─── Decorator ───────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface StoryContext<TArgs = Props> {
|
|
23
|
+
args: TArgs
|
|
24
|
+
argTypes: Record<string, unknown>
|
|
25
|
+
globals: Record<string, unknown>
|
|
26
|
+
id: string
|
|
27
|
+
kind: string
|
|
28
|
+
name: string
|
|
29
|
+
viewMode: 'story' | 'docs'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type StoryFn<TArgs = Props> = (
|
|
33
|
+
args: TArgs,
|
|
34
|
+
context: StoryContext<TArgs>,
|
|
35
|
+
) => VNodeChild
|
|
36
|
+
|
|
37
|
+
export type DecoratorFn<TArgs = Props> = (
|
|
38
|
+
storyFn: StoryFn<TArgs>,
|
|
39
|
+
context: StoryContext<TArgs>,
|
|
40
|
+
) => VNodeChild
|
|
41
|
+
|
|
42
|
+
// ─── Meta ────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export interface Meta<TComponent extends ComponentFn<any> = ComponentFn> {
|
|
45
|
+
/** The component to document. */
|
|
46
|
+
component?: TComponent
|
|
47
|
+
/** Display title in the sidebar. */
|
|
48
|
+
title?: string
|
|
49
|
+
/** Decorators applied to every story in this file. */
|
|
50
|
+
decorators?: DecoratorFn<InferProps<TComponent>>[]
|
|
51
|
+
/** Default args for all stories. */
|
|
52
|
+
args?: Partial<InferProps<TComponent>>
|
|
53
|
+
/** Arg type definitions for Controls panel. */
|
|
54
|
+
argTypes?: Record<string, unknown>
|
|
55
|
+
/** Story parameters (backgrounds, viewport, etc.). */
|
|
56
|
+
parameters?: Record<string, unknown>
|
|
57
|
+
/** Tags for filtering (e.g. "autodocs"). */
|
|
58
|
+
tags?: string[]
|
|
59
|
+
/**
|
|
60
|
+
* Default render function. If omitted, the component is called
|
|
61
|
+
* with args as props: `h(component, args)`.
|
|
62
|
+
*/
|
|
63
|
+
render?: (
|
|
64
|
+
args: InferProps<TComponent>,
|
|
65
|
+
context: StoryContext<InferProps<TComponent>>,
|
|
66
|
+
) => VNodeChild
|
|
67
|
+
/** Exclude arg names from Controls. */
|
|
68
|
+
excludeStories?: string | string[] | RegExp
|
|
69
|
+
/** Include only these story names. */
|
|
70
|
+
includeStories?: string | string[] | RegExp
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── StoryObj ────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export interface StoryObj<TMeta extends Meta<any> = Meta> {
|
|
76
|
+
/** Args for this specific story (merged with meta.args). */
|
|
77
|
+
args?: Partial<MetaArgs<TMeta>>
|
|
78
|
+
/** Arg type overrides. */
|
|
79
|
+
argTypes?: Record<string, unknown>
|
|
80
|
+
/** Decorators for this story only. */
|
|
81
|
+
decorators?: DecoratorFn<MetaArgs<TMeta>>[]
|
|
82
|
+
/** Parameters for this story. */
|
|
83
|
+
parameters?: Record<string, unknown>
|
|
84
|
+
/** Tags for this story. */
|
|
85
|
+
tags?: string[]
|
|
86
|
+
/** Override the render function for this story. */
|
|
87
|
+
render?: (
|
|
88
|
+
args: MetaArgs<TMeta>,
|
|
89
|
+
context: StoryContext<MetaArgs<TMeta>>,
|
|
90
|
+
) => VNodeChild
|
|
91
|
+
/** Story name override. */
|
|
92
|
+
name?: string
|
|
93
|
+
/** Play function for interaction tests. */
|
|
94
|
+
play?: (context: {
|
|
95
|
+
canvasElement: HTMLElement
|
|
96
|
+
args: MetaArgs<TMeta>
|
|
97
|
+
step: (name: string, fn: () => Promise<void>) => Promise<void>
|
|
98
|
+
}) => Promise<void> | void
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Extract the args type from a Meta definition. */
|
|
102
|
+
type MetaArgs<TMeta> = TMeta extends Meta<infer C> ? InferProps<C> : Props
|