@pyreon/storybook 0.11.4 → 0.11.6
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 +49 -51
- package/lib/index.js.map +1 -1
- package/lib/preset.js.map +1 -1
- package/lib/preview.js.map +1 -1
- package/lib/types/index.d.ts +1 -1
- package/package.json +12 -12
- package/src/index.ts +6 -6
- package/src/preset.ts +4 -4
- package/src/preview-impl.tsx +3 -3
- package/src/preview.ts +1 -1
- package/src/render-impl.tsx +2 -2
- package/src/render.ts +1 -1
- package/src/tests/storybook.test.tsx +123 -123
- package/src/types.ts +2 -2
package/README.md
CHANGED
|
@@ -15,27 +15,27 @@ Configure Storybook to use the Pyreon renderer:
|
|
|
15
15
|
```ts
|
|
16
16
|
// .storybook/main.ts
|
|
17
17
|
export default {
|
|
18
|
-
stories: [
|
|
19
|
-
framework:
|
|
18
|
+
stories: ['../src/**/*.stories.ts'],
|
|
19
|
+
framework: '@pyreon/storybook',
|
|
20
20
|
}
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## Quick Start
|
|
24
24
|
|
|
25
25
|
```tsx
|
|
26
|
-
import type { Meta, StoryObj } from
|
|
27
|
-
import { Button } from
|
|
26
|
+
import type { Meta, StoryObj } from '@pyreon/storybook'
|
|
27
|
+
import { Button } from './Button'
|
|
28
28
|
|
|
29
29
|
const meta = {
|
|
30
30
|
component: Button,
|
|
31
|
-
args: { label:
|
|
31
|
+
args: { label: 'Click me' },
|
|
32
32
|
} satisfies Meta<typeof Button>
|
|
33
33
|
|
|
34
34
|
export default meta
|
|
35
35
|
type Story = StoryObj<typeof meta>
|
|
36
36
|
|
|
37
37
|
export const Primary: Story = {
|
|
38
|
-
args: { variant:
|
|
38
|
+
args: { variant: 'primary' },
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export const AllVariants: Story = {
|
|
@@ -54,13 +54,13 @@ export const AllVariants: Story = {
|
|
|
54
54
|
|
|
55
55
|
Core renderer called by Storybook to display stories. Handles cleanup of previous renders, error display, and mounting the VNode tree.
|
|
56
56
|
|
|
57
|
-
| Parameter
|
|
58
|
-
|
|
|
59
|
-
| `context.storyFn`
|
|
60
|
-
| `context.showMain`
|
|
61
|
-
| `context.showError`
|
|
62
|
-
| `context.forceRemount` | `boolean`
|
|
63
|
-
| `canvasElement`
|
|
57
|
+
| Parameter | Type | Description |
|
|
58
|
+
| ---------------------- | ------------------ | -------------------------------------- |
|
|
59
|
+
| `context.storyFn` | `() => VNodeChild` | Function that produces the story VNode |
|
|
60
|
+
| `context.showMain` | `() => void` | Show the main canvas |
|
|
61
|
+
| `context.showError` | `(error) => void` | Show an error panel |
|
|
62
|
+
| `context.forceRemount` | `boolean` | Whether to force a full remount |
|
|
63
|
+
| `canvasElement` | `HTMLElement` | DOM element to render into |
|
|
64
64
|
|
|
65
65
|
This function is used internally by the Storybook framework preset. You typically do not call it directly.
|
|
66
66
|
|
|
@@ -68,10 +68,10 @@ This function is used internally by the Storybook framework preset. You typicall
|
|
|
68
68
|
|
|
69
69
|
Default render implementation. Called when no custom `render` function is provided on the story or meta.
|
|
70
70
|
|
|
71
|
-
| Parameter
|
|
72
|
-
|
|
|
73
|
-
| `component` | `ComponentFn`
|
|
74
|
-
| `args`
|
|
71
|
+
| Parameter | Type | Description |
|
|
72
|
+
| ----------- | ------------------------- | -------------------------- |
|
|
73
|
+
| `component` | `ComponentFn` | Pyreon component function |
|
|
74
|
+
| `args` | `Record<string, unknown>` | Story args passed as props |
|
|
75
75
|
|
|
76
76
|
**Returns:** `VNodeChild` — result of `h(component, args)`
|
|
77
77
|
|
|
@@ -79,33 +79,33 @@ Default render implementation. Called when no custom `render` function is provid
|
|
|
79
79
|
|
|
80
80
|
Type for the default export of a story file. Provides type inference for args based on the component's props.
|
|
81
81
|
|
|
82
|
-
| Property
|
|
83
|
-
|
|
|
84
|
-
| `component`
|
|
85
|
-
| `title`
|
|
86
|
-
| `decorators`
|
|
87
|
-
| `args`
|
|
88
|
-
| `argTypes`
|
|
89
|
-
| `parameters`
|
|
90
|
-
| `tags`
|
|
91
|
-
| `render`
|
|
92
|
-
| `excludeStories` | `string \| string[] \| RegExp`
|
|
93
|
-
| `includeStories` | `string \| string[] \| RegExp`
|
|
82
|
+
| Property | Type | Description |
|
|
83
|
+
| ---------------- | --------------------------------- | ---------------------------------------- |
|
|
84
|
+
| `component` | `TComponent` | The component to document |
|
|
85
|
+
| `title` | `string` | Display title in sidebar |
|
|
86
|
+
| `decorators` | `DecoratorFn[]` | Decorators for all stories in the file |
|
|
87
|
+
| `args` | `Partial<InferProps<TComponent>>` | Default args |
|
|
88
|
+
| `argTypes` | `Record<string, unknown>` | Arg type definitions for Controls |
|
|
89
|
+
| `parameters` | `Record<string, unknown>` | Story parameters (backgrounds, viewport) |
|
|
90
|
+
| `tags` | `string[]` | Tags for filtering (e.g. `"autodocs"`) |
|
|
91
|
+
| `render` | `(args, context) => VNodeChild` | Default render function |
|
|
92
|
+
| `excludeStories` | `string \| string[] \| RegExp` | Exclude named exports |
|
|
93
|
+
| `includeStories` | `string \| string[] \| RegExp` | Include only named exports |
|
|
94
94
|
|
|
95
95
|
### `StoryObj<TMeta>`
|
|
96
96
|
|
|
97
97
|
Type for individual story exports. Args are merged with `Meta.args`.
|
|
98
98
|
|
|
99
|
-
| Property
|
|
100
|
-
|
|
|
101
|
-
| `args`
|
|
102
|
-
| `argTypes`
|
|
103
|
-
| `decorators` | `DecoratorFn[]`
|
|
104
|
-
| `parameters` | `Record<string, unknown>`
|
|
105
|
-
| `tags`
|
|
106
|
-
| `render`
|
|
107
|
-
| `name`
|
|
108
|
-
| `play`
|
|
99
|
+
| Property | Type | Description |
|
|
100
|
+
| ------------ | ------------------------------------ | ---------------------------- |
|
|
101
|
+
| `args` | `Partial<MetaArgs>` | Args for this story |
|
|
102
|
+
| `argTypes` | `Record<string, unknown>` | Arg type overrides |
|
|
103
|
+
| `decorators` | `DecoratorFn[]` | Story-specific decorators |
|
|
104
|
+
| `parameters` | `Record<string, unknown>` | Story parameters |
|
|
105
|
+
| `tags` | `string[]` | Story tags |
|
|
106
|
+
| `render` | `(args, context) => VNodeChild` | Custom render for this story |
|
|
107
|
+
| `name` | `string` | Display name override |
|
|
108
|
+
| `play` | `(context) => Promise<void> \| void` | Interaction test function |
|
|
109
109
|
|
|
110
110
|
### `DecoratorFn<TArgs>`
|
|
111
111
|
|
|
@@ -119,11 +119,11 @@ const withTheme: DecoratorFn<{ label: string }> = (storyFn, context) => (
|
|
|
119
119
|
|
|
120
120
|
### `StoryFn<TArgs>` / `StoryContext<TArgs>` / `InferProps<T>`
|
|
121
121
|
|
|
122
|
-
| Type
|
|
123
|
-
|
|
|
124
|
-
| `StoryFn<TArgs>`
|
|
122
|
+
| Type | Description |
|
|
123
|
+
| --------------------- | ------------------------------------------------------- |
|
|
124
|
+
| `StoryFn<TArgs>` | `(args, context) => VNodeChild` |
|
|
125
125
|
| `StoryContext<TArgs>` | `{ args, argTypes, globals, id, kind, name, viewMode }` |
|
|
126
|
-
| `InferProps<T>`
|
|
126
|
+
| `InferProps<T>` | Extract props type from a `ComponentFn<P>` |
|
|
127
127
|
|
|
128
128
|
## Patterns
|
|
129
129
|
|
|
@@ -136,9 +136,7 @@ const meta = {
|
|
|
136
136
|
component: Button,
|
|
137
137
|
decorators: [
|
|
138
138
|
(storyFn, context) => (
|
|
139
|
-
<div style="padding: 20px; background: #f5f5f5;">
|
|
140
|
-
{storyFn(context.args, context)}
|
|
141
|
-
</div>
|
|
139
|
+
<div style="padding: 20px; background: #f5f5f5;">{storyFn(context.args, context)}</div>
|
|
142
140
|
),
|
|
143
141
|
],
|
|
144
142
|
} satisfies Meta<typeof Button>
|
|
@@ -150,10 +148,10 @@ Use the `play` function for automated interaction testing.
|
|
|
150
148
|
|
|
151
149
|
```ts
|
|
152
150
|
export const Clickable: Story = {
|
|
153
|
-
args: { label:
|
|
151
|
+
args: { label: 'Click me' },
|
|
154
152
|
play: async ({ canvasElement, step }) => {
|
|
155
|
-
await step(
|
|
156
|
-
const button = canvasElement.querySelector(
|
|
153
|
+
await step('click the button', async () => {
|
|
154
|
+
const button = canvasElement.querySelector('button')!
|
|
157
155
|
button.click()
|
|
158
156
|
})
|
|
159
157
|
},
|
|
@@ -165,7 +163,7 @@ export const Clickable: Story = {
|
|
|
165
163
|
Use signals directly in stories to demonstrate interactive behavior.
|
|
166
164
|
|
|
167
165
|
```tsx
|
|
168
|
-
import { signal, effect } from
|
|
166
|
+
import { signal, effect } from '@pyreon/storybook'
|
|
169
167
|
|
|
170
168
|
export const Interactive: Story = {
|
|
171
169
|
render: (args) => {
|
|
@@ -173,7 +171,7 @@ export const Interactive: Story = {
|
|
|
173
171
|
return (
|
|
174
172
|
<div>
|
|
175
173
|
<p>{() => `Count: ${count()}`}</p>
|
|
176
|
-
<button onClick={() => count.update(n => n + 1)}>Increment</button>
|
|
174
|
+
<button onClick={() => count.update((n) => n + 1)}>Increment</button>
|
|
177
175
|
</div>
|
|
178
176
|
)
|
|
179
177
|
},
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["mount"],"sources":["../src/render-impl.tsx"],"sourcesContent":["import type { ComponentFn, VNodeChild } from
|
|
1
|
+
{"version":3,"file":"index.js","names":["mount"],"sources":["../src/render-impl.tsx"],"sourcesContent":["import type { ComponentFn, VNodeChild } from '@pyreon/core'\nimport { mount } from '@pyreon/runtime-dom'\n\n/**\n * State tracked per canvas element so we can clean up between renders.\n */\nconst canvasState = new WeakMap<HTMLElement, () => void>()\n\n/**\n * Render a Pyreon story into a Storybook canvas element.\n *\n * This is the core integration point — Storybook calls this function\n * every time a story needs to be displayed or re-rendered (e.g. when\n * the user changes args via the Controls panel).\n *\n * It handles:\n * 1. Cleaning up the previous mount (disposing effects, removing DOM)\n * 2. Building the VNode from the story function or component + args\n * 3. Mounting the new VNode into the canvas\n */\nexport function renderToCanvas(\n {\n storyFn,\n showMain,\n showError,\n }: {\n storyFn: () => VNodeChild\n storyContext: {\n component?: ComponentFn<any>\n args: Record<string, unknown>\n [key: string]: unknown\n }\n showMain: () => void\n showError: (error: { title: string; description: string }) => void\n forceRemount: boolean\n },\n canvasElement: HTMLElement,\n): void {\n // Always clean up the previous render\n const prevUnmount = canvasState.get(canvasElement)\n if (prevUnmount) {\n prevUnmount()\n canvasState.delete(canvasElement)\n }\n\n try {\n const element = storyFn()\n const unmount = mount(element, canvasElement)\n canvasState.set(canvasElement, unmount)\n showMain()\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err))\n showError({\n title: `Error rendering story`,\n description: error.message,\n })\n }\n}\n\n/**\n * Default render implementation used when no custom `render` is provided.\n */\nexport function defaultRender(\n component: ComponentFn<any>,\n args: Record<string, unknown>,\n): VNodeChild {\n const Component = component\n return <Component {...args} />\n}\n"],"mappings":";;;;;;;;;AAMA,MAAM,8BAAc,IAAI,SAAkC;;;;;;;;;;;;;AAc1D,SAAgB,eACd,EACE,SACA,UACA,aAYF,eACM;CAEN,MAAM,cAAc,YAAY,IAAI,cAAc;AAClD,KAAI,aAAa;AACf,eAAa;AACb,cAAY,OAAO,cAAc;;AAGnC,KAAI;EAEF,MAAM,UAAUA,QADA,SAAS,EACM,cAAc;AAC7C,cAAY,IAAI,eAAe,QAAQ;AACvC,YAAU;UACH,KAAK;AAEZ,YAAU;GACR,OAAO;GACP,cAHY,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC,EAG5C;GACpB,CAAC;;;;;;AAON,SAAgB,cACd,WACA,MACY;AAEZ,QAAO,oBADW,WACX,EAAW,GAAI,MAAQ"}
|
package/lib/preset.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preset.js","names":[],"sources":["../src/preset.ts"],"sourcesContent":["/**\n * Storybook preset for @pyreon/storybook.\n *\n * This file is loaded by Storybook's server when the user sets\n * `framework: \"@pyreon/storybook\"` in their `.storybook/main.ts`.\n *\n * It tells Storybook:\n * - Which renderer to use (via the preview entry)\n * - What framework name to report\n */\n\nimport { dirname, join } from
|
|
1
|
+
{"version":3,"file":"preset.js","names":[],"sources":["../src/preset.ts"],"sourcesContent":["/**\n * Storybook preset for @pyreon/storybook.\n *\n * This file is loaded by Storybook's server when the user sets\n * `framework: \"@pyreon/storybook\"` in their `.storybook/main.ts`.\n *\n * It tells Storybook:\n * - Which renderer to use (via the preview entry)\n * - What framework name to report\n */\n\nimport { dirname, join } from 'node:path'\n\nfunction _getAbsolutePath(value: string): string {\n return dirname(require.resolve(join(value, 'package.json')))\n}\n\nexport const addons: string[] = []\n\nexport const previewAnnotations: string[] = [join(__dirname, 'preview')]\n\nexport const core = {\n renderer: '@pyreon/storybook',\n}\n"],"mappings":";;;;;;;;;;;;;AAiBA,MAAa,SAAmB,EAAE;AAElC,MAAa,qBAA+B,CAAC,KAAK,WAAW,UAAU,CAAC;AAExE,MAAa,OAAO,EAClB,UAAU,qBACX"}
|
package/lib/preview.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preview.js","names":[],"sources":["../src/render-impl.tsx","../src/preview-impl.tsx"],"sourcesContent":["import type { ComponentFn, VNodeChild } from
|
|
1
|
+
{"version":3,"file":"preview.js","names":[],"sources":["../src/render-impl.tsx","../src/preview-impl.tsx"],"sourcesContent":["import type { ComponentFn, VNodeChild } from '@pyreon/core'\nimport { mount } from '@pyreon/runtime-dom'\n\n/**\n * State tracked per canvas element so we can clean up between renders.\n */\nconst canvasState = new WeakMap<HTMLElement, () => void>()\n\n/**\n * Render a Pyreon story into a Storybook canvas element.\n *\n * This is the core integration point — Storybook calls this function\n * every time a story needs to be displayed or re-rendered (e.g. when\n * the user changes args via the Controls panel).\n *\n * It handles:\n * 1. Cleaning up the previous mount (disposing effects, removing DOM)\n * 2. Building the VNode from the story function or component + args\n * 3. Mounting the new VNode into the canvas\n */\nexport function renderToCanvas(\n {\n storyFn,\n showMain,\n showError,\n }: {\n storyFn: () => VNodeChild\n storyContext: {\n component?: ComponentFn<any>\n args: Record<string, unknown>\n [key: string]: unknown\n }\n showMain: () => void\n showError: (error: { title: string; description: string }) => void\n forceRemount: boolean\n },\n canvasElement: HTMLElement,\n): void {\n // Always clean up the previous render\n const prevUnmount = canvasState.get(canvasElement)\n if (prevUnmount) {\n prevUnmount()\n canvasState.delete(canvasElement)\n }\n\n try {\n const element = storyFn()\n const unmount = mount(element, canvasElement)\n canvasState.set(canvasElement, unmount)\n showMain()\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err))\n showError({\n title: `Error rendering story`,\n description: error.message,\n })\n }\n}\n\n/**\n * Default render implementation used when no custom `render` is provided.\n */\nexport function defaultRender(\n component: ComponentFn<any>,\n args: Record<string, unknown>,\n): VNodeChild {\n const Component = component\n return <Component {...args} />\n}\n","import type { ComponentFn, VNodeChild } from '@pyreon/core'\nimport { renderToCanvas } from './render-impl'\n\n/**\n * Preview entry — Storybook loads this in the preview iframe.\n *\n * Exports the render function and default decorators/parameters\n * that apply to all stories using this renderer.\n */\n\nexport { renderToCanvas }\n\n/**\n * Default render function — if the story CSF has a `component` but no\n * explicit `render`, this is used to create the VNode.\n */\nexport function render<TArgs extends Record<string, unknown>>(\n args: TArgs,\n context: { component?: ComponentFn<any> },\n): VNodeChild {\n const Component = context.component\n if (!Component) {\n throw new Error(\n '[@pyreon/storybook] No component provided. Either set `component` in your meta or provide a `render` function.',\n )\n }\n return <Component {...args} />\n}\n"],"mappings":";;;;;;;AAMA,MAAM,8BAAc,IAAI,SAAkC;;;;;;;;;;;;;AAc1D,SAAgB,eACd,EACE,SACA,UACA,aAYF,eACM;CAEN,MAAM,cAAc,YAAY,IAAI,cAAc;AAClD,KAAI,aAAa;AACf,eAAa;AACb,cAAY,OAAO,cAAc;;AAGnC,KAAI;EAEF,MAAM,UAAU,MADA,SAAS,EACM,cAAc;AAC7C,cAAY,IAAI,eAAe,QAAQ;AACvC,YAAU;UACH,KAAK;AAEZ,YAAU;GACR,OAAO;GACP,cAHY,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC,EAG5C;GACpB,CAAC;;;;;;;;;;ACvCN,SAAgB,OACd,MACA,SACY;CACZ,MAAM,YAAY,QAAQ;AAC1B,KAAI,CAAC,UACH,OAAM,IAAI,MACR,iHACD;AAEH,QAAO,oBAAC,WAAD,EAAW,GAAI,MAAQ"}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ interface StoryContext<TArgs = Props$1> {
|
|
|
21
21
|
id: string;
|
|
22
22
|
kind: string;
|
|
23
23
|
name: string;
|
|
24
|
-
viewMode:
|
|
24
|
+
viewMode: 'story' | 'docs';
|
|
25
25
|
}
|
|
26
26
|
type StoryFn<TArgs = Props$1> = (args: TArgs, context: StoryContext<TArgs>) => VNodeChild$1;
|
|
27
27
|
type DecoratorFn<TArgs = Props$1> = (storyFn: StoryFn<TArgs>, context: StoryContext<TArgs>) => VNodeChild$1;
|
package/package.json
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/storybook",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
4
4
|
"description": "Storybook renderer for Pyreon — mount, render, and interact with Pyreon components in Storybook",
|
|
5
|
+
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/storybook#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/pyreon/pyreon/issues"
|
|
8
|
+
},
|
|
5
9
|
"license": "MIT",
|
|
6
10
|
"repository": {
|
|
7
11
|
"type": "git",
|
|
8
12
|
"url": "https://github.com/pyreon/pyreon.git",
|
|
9
13
|
"directory": "packages/tools/storybook"
|
|
10
14
|
},
|
|
11
|
-
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/storybook#readme",
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/pyreon/pyreon/issues"
|
|
14
|
-
},
|
|
15
|
-
"publishConfig": {
|
|
16
|
-
"access": "public"
|
|
17
|
-
},
|
|
18
15
|
"files": [
|
|
19
16
|
"lib",
|
|
20
17
|
"src",
|
|
@@ -44,17 +41,20 @@
|
|
|
44
41
|
},
|
|
45
42
|
"./package.json": "./package.json"
|
|
46
43
|
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "vl_rolldown_build",
|
|
49
49
|
"dev": "vl_rolldown_build-watch",
|
|
50
50
|
"test": "vitest run",
|
|
51
51
|
"typecheck": "tsc --noEmit",
|
|
52
|
-
"lint": "
|
|
52
|
+
"lint": "oxlint ."
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@pyreon/core": "^0.11.
|
|
56
|
-
"@pyreon/reactivity": "^0.11.
|
|
57
|
-
"@pyreon/runtime-dom": "^0.11.
|
|
55
|
+
"@pyreon/core": "^0.11.6",
|
|
56
|
+
"@pyreon/reactivity": "^0.11.6",
|
|
57
|
+
"@pyreon/runtime-dom": "^0.11.6",
|
|
58
58
|
"storybook": ">=8.0.0"
|
|
59
59
|
}
|
|
60
60
|
}
|
package/src/index.ts
CHANGED
|
@@ -31,15 +31,15 @@ export type {
|
|
|
31
31
|
StoryContext,
|
|
32
32
|
StoryFn,
|
|
33
33
|
StoryObj,
|
|
34
|
-
} from
|
|
34
|
+
} from './types'
|
|
35
35
|
|
|
36
36
|
// ─── Renderer ────────────────────────────────────────────────────────────────
|
|
37
37
|
|
|
38
|
-
export { defaultRender, renderToCanvas } from
|
|
38
|
+
export { defaultRender, renderToCanvas } from './render'
|
|
39
39
|
|
|
40
40
|
// ─── Pyreon re-exports for convenience ───────────────────────────────────────
|
|
41
41
|
|
|
42
|
-
export type { ComponentFn, Props, VNode, VNodeChild } from
|
|
43
|
-
export { Fragment, h } from
|
|
44
|
-
export { computed, effect, signal } from
|
|
45
|
-
export { mount } from
|
|
42
|
+
export type { ComponentFn, Props, VNode, VNodeChild } from '@pyreon/core'
|
|
43
|
+
export { Fragment, h } from '@pyreon/core'
|
|
44
|
+
export { computed, effect, signal } from '@pyreon/reactivity'
|
|
45
|
+
export { mount } from '@pyreon/runtime-dom'
|
package/src/preset.ts
CHANGED
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
* - What framework name to report
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { dirname, join } from
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
13
|
|
|
14
14
|
function _getAbsolutePath(value: string): string {
|
|
15
|
-
return dirname(require.resolve(join(value,
|
|
15
|
+
return dirname(require.resolve(join(value, 'package.json')))
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export const addons: string[] = []
|
|
19
19
|
|
|
20
|
-
export const previewAnnotations: string[] = [join(__dirname,
|
|
20
|
+
export const previewAnnotations: string[] = [join(__dirname, 'preview')]
|
|
21
21
|
|
|
22
22
|
export const core = {
|
|
23
|
-
renderer:
|
|
23
|
+
renderer: '@pyreon/storybook',
|
|
24
24
|
}
|
package/src/preview-impl.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ComponentFn, VNodeChild } from
|
|
2
|
-
import { renderToCanvas } from
|
|
1
|
+
import type { ComponentFn, VNodeChild } from '@pyreon/core'
|
|
2
|
+
import { renderToCanvas } from './render-impl'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Preview entry — Storybook loads this in the preview iframe.
|
|
@@ -21,7 +21,7 @@ export function render<TArgs extends Record<string, unknown>>(
|
|
|
21
21
|
const Component = context.component
|
|
22
22
|
if (!Component) {
|
|
23
23
|
throw new Error(
|
|
24
|
-
|
|
24
|
+
'[@pyreon/storybook] No component provided. Either set `component` in your meta or provide a `render` function.',
|
|
25
25
|
)
|
|
26
26
|
}
|
|
27
27
|
return <Component {...args} />
|
package/src/preview.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { render, renderToCanvas } from
|
|
1
|
+
export { render, renderToCanvas } from './preview-impl'
|
package/src/render-impl.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ComponentFn, VNodeChild } from
|
|
2
|
-
import { mount } from
|
|
1
|
+
import type { ComponentFn, VNodeChild } from '@pyreon/core'
|
|
2
|
+
import { mount } from '@pyreon/runtime-dom'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* State tracked per canvas element so we can clean up between renders.
|
package/src/render.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { defaultRender, renderToCanvas } from
|
|
1
|
+
export { defaultRender, renderToCanvas } from './render-impl'
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type { ComponentFn, VNodeChild } from
|
|
2
|
-
import { effect, signal } from
|
|
3
|
-
import { mount } from
|
|
4
|
-
import { render as previewRender } from
|
|
5
|
-
import { defaultRender, renderToCanvas } from
|
|
6
|
-
import type { DecoratorFn, Meta, StoryContext, StoryFn, StoryObj } from
|
|
1
|
+
import type { ComponentFn, VNodeChild } from '@pyreon/core'
|
|
2
|
+
import { effect, signal } from '@pyreon/reactivity'
|
|
3
|
+
import { mount } from '@pyreon/runtime-dom'
|
|
4
|
+
import { render as previewRender } from '../preview'
|
|
5
|
+
import { defaultRender, renderToCanvas } from '../render'
|
|
6
|
+
import type { DecoratorFn, Meta, StoryContext, StoryFn, StoryObj } from '../types'
|
|
7
7
|
|
|
8
8
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
9
9
|
|
|
10
10
|
function createCanvas(): HTMLElement {
|
|
11
|
-
const el = document.createElement(
|
|
11
|
+
const el = document.createElement('div')
|
|
12
12
|
document.body.appendChild(el)
|
|
13
13
|
return el
|
|
14
14
|
}
|
|
@@ -36,8 +36,8 @@ function makeRenderContext(overrides: {
|
|
|
36
36
|
|
|
37
37
|
// ─── renderToCanvas ──────────────────────────────────────────────────────────
|
|
38
38
|
|
|
39
|
-
describe(
|
|
40
|
-
it(
|
|
39
|
+
describe('renderToCanvas', () => {
|
|
40
|
+
it('mounts a simple VNode into the canvas', () => {
|
|
41
41
|
const canvas = createCanvas()
|
|
42
42
|
const ctx = makeRenderContext({
|
|
43
43
|
storyFn: () => <button>Click me</button>,
|
|
@@ -45,12 +45,12 @@ describe("renderToCanvas", () => {
|
|
|
45
45
|
|
|
46
46
|
renderToCanvas(ctx, canvas)
|
|
47
47
|
|
|
48
|
-
expect(canvas.innerHTML).toContain(
|
|
49
|
-
expect(canvas.querySelector(
|
|
48
|
+
expect(canvas.innerHTML).toContain('Click me')
|
|
49
|
+
expect(canvas.querySelector('button')).toBeTruthy()
|
|
50
50
|
canvas.remove()
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
-
it(
|
|
53
|
+
it('mounts a Pyreon component with props', () => {
|
|
54
54
|
function Button(props: { label: string; disabled?: boolean }) {
|
|
55
55
|
return <button disabled={props.disabled ?? false}>{props.label}</button>
|
|
56
56
|
}
|
|
@@ -62,27 +62,27 @@ describe("renderToCanvas", () => {
|
|
|
62
62
|
|
|
63
63
|
renderToCanvas(ctx, canvas)
|
|
64
64
|
|
|
65
|
-
const btn = canvas.querySelector(
|
|
65
|
+
const btn = canvas.querySelector('button')!
|
|
66
66
|
expect(btn).toBeTruthy()
|
|
67
|
-
expect(btn.textContent).toBe(
|
|
68
|
-
expect(btn.getAttribute(
|
|
67
|
+
expect(btn.textContent).toBe('Submit')
|
|
68
|
+
expect(btn.getAttribute('disabled')).not.toBeNull()
|
|
69
69
|
canvas.remove()
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
it(
|
|
72
|
+
it('cleans up previous mount on re-render', () => {
|
|
73
73
|
const canvas = createCanvas()
|
|
74
74
|
|
|
75
75
|
renderToCanvas(makeRenderContext({ storyFn: () => <div>First</div> }), canvas)
|
|
76
|
-
expect(canvas.textContent).toBe(
|
|
76
|
+
expect(canvas.textContent).toBe('First')
|
|
77
77
|
|
|
78
78
|
renderToCanvas(makeRenderContext({ storyFn: () => <div>Second</div> }), canvas)
|
|
79
|
-
expect(canvas.textContent).toBe(
|
|
79
|
+
expect(canvas.textContent).toBe('Second')
|
|
80
80
|
// Only one child — previous mount was cleaned up
|
|
81
81
|
expect(canvas.children.length).toBe(1)
|
|
82
82
|
canvas.remove()
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
-
it(
|
|
85
|
+
it('disposes reactive effects on cleanup', () => {
|
|
86
86
|
const canvas = createCanvas()
|
|
87
87
|
let effectRunCount = 0
|
|
88
88
|
|
|
@@ -112,13 +112,13 @@ describe("renderToCanvas", () => {
|
|
|
112
112
|
canvas.remove()
|
|
113
113
|
})
|
|
114
114
|
|
|
115
|
-
it(
|
|
115
|
+
it('shows error when storyFn throws an Error', () => {
|
|
116
116
|
const canvas = createCanvas()
|
|
117
117
|
let errorShown: { title: string; description: string } | null = null
|
|
118
118
|
|
|
119
119
|
const ctx = {
|
|
120
120
|
storyFn: () => {
|
|
121
|
-
throw new Error(
|
|
121
|
+
throw new Error('Boom')
|
|
122
122
|
},
|
|
123
123
|
storyContext: { args: {} },
|
|
124
124
|
showMain: () => {
|
|
@@ -133,17 +133,17 @@ describe("renderToCanvas", () => {
|
|
|
133
133
|
renderToCanvas(ctx, canvas)
|
|
134
134
|
|
|
135
135
|
expect(errorShown).not.toBeNull()
|
|
136
|
-
expect(errorShown!.description).toBe(
|
|
136
|
+
expect(errorShown!.description).toBe('Boom')
|
|
137
137
|
canvas.remove()
|
|
138
138
|
})
|
|
139
139
|
|
|
140
|
-
it(
|
|
140
|
+
it('shows error when storyFn throws a non-Error value', () => {
|
|
141
141
|
const canvas = createCanvas()
|
|
142
142
|
let errorShown: { title: string; description: string } | null = null
|
|
143
143
|
|
|
144
144
|
const ctx = {
|
|
145
145
|
storyFn: () => {
|
|
146
|
-
throw
|
|
146
|
+
throw 'string error'
|
|
147
147
|
},
|
|
148
148
|
storyContext: { args: {} },
|
|
149
149
|
showMain: () => {
|
|
@@ -158,11 +158,11 @@ describe("renderToCanvas", () => {
|
|
|
158
158
|
renderToCanvas(ctx, canvas)
|
|
159
159
|
|
|
160
160
|
expect(errorShown).not.toBeNull()
|
|
161
|
-
expect(errorShown!.description).toBe(
|
|
161
|
+
expect(errorShown!.description).toBe('string error')
|
|
162
162
|
canvas.remove()
|
|
163
163
|
})
|
|
164
164
|
|
|
165
|
-
it(
|
|
165
|
+
it('renders reactive components that update the DOM', () => {
|
|
166
166
|
const canvas = createCanvas()
|
|
167
167
|
const count = signal(0)
|
|
168
168
|
|
|
@@ -172,27 +172,27 @@ describe("renderToCanvas", () => {
|
|
|
172
172
|
|
|
173
173
|
renderToCanvas(makeRenderContext({ storyFn: () => <Counter /> }), canvas)
|
|
174
174
|
|
|
175
|
-
expect(canvas.textContent).toBe(
|
|
175
|
+
expect(canvas.textContent).toBe('Count: 0')
|
|
176
176
|
|
|
177
177
|
count.set(5)
|
|
178
|
-
expect(canvas.textContent).toBe(
|
|
178
|
+
expect(canvas.textContent).toBe('Count: 5')
|
|
179
179
|
canvas.remove()
|
|
180
180
|
})
|
|
181
181
|
})
|
|
182
182
|
|
|
183
183
|
// ─── defaultRender ───────────────────────────────────────────────────────────
|
|
184
184
|
|
|
185
|
-
describe(
|
|
186
|
-
it(
|
|
185
|
+
describe('defaultRender', () => {
|
|
186
|
+
it('creates a VNode from component + args', () => {
|
|
187
187
|
function Greeting(props: { name: string }) {
|
|
188
188
|
return <p>Hello, {props.name}!</p>
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
const canvas = createCanvas()
|
|
192
|
-
const vnode = defaultRender(Greeting, { name:
|
|
192
|
+
const vnode = defaultRender(Greeting, { name: 'World' })
|
|
193
193
|
const unmount = mount(vnode, canvas)
|
|
194
194
|
|
|
195
|
-
expect(canvas.textContent).toBe(
|
|
195
|
+
expect(canvas.textContent).toBe('Hello, World!')
|
|
196
196
|
unmount()
|
|
197
197
|
canvas.remove()
|
|
198
198
|
})
|
|
@@ -200,9 +200,9 @@ describe("defaultRender", () => {
|
|
|
200
200
|
|
|
201
201
|
// ─── Type-level tests (Meta / StoryObj) ──────────────────────────────────────
|
|
202
202
|
|
|
203
|
-
describe(
|
|
204
|
-
it(
|
|
205
|
-
function Button(props: { label: string; variant?:
|
|
203
|
+
describe('Meta and StoryObj types', () => {
|
|
204
|
+
it('Meta accepts a component and typed args', () => {
|
|
205
|
+
function Button(props: { label: string; variant?: 'primary' | 'secondary' }) {
|
|
206
206
|
return (
|
|
207
207
|
<button type="button" class={props.variant}>
|
|
208
208
|
{props.label}
|
|
@@ -212,23 +212,23 @@ describe("Meta and StoryObj types", () => {
|
|
|
212
212
|
|
|
213
213
|
const meta = {
|
|
214
214
|
component: Button,
|
|
215
|
-
title:
|
|
216
|
-
args: { label:
|
|
217
|
-
tags: [
|
|
215
|
+
title: 'Button',
|
|
216
|
+
args: { label: 'Click', variant: 'primary' as const },
|
|
217
|
+
tags: ['autodocs'],
|
|
218
218
|
} satisfies Meta<typeof Button>
|
|
219
219
|
|
|
220
220
|
expect(meta.component).toBe(Button)
|
|
221
|
-
expect(meta.args!.label).toBe(
|
|
221
|
+
expect(meta.args!.label).toBe('Click')
|
|
222
222
|
})
|
|
223
223
|
|
|
224
|
-
it(
|
|
224
|
+
it('StoryObj inherits args from Meta', () => {
|
|
225
225
|
function Input(props: { placeholder: string; disabled?: boolean }) {
|
|
226
226
|
return <input placeholder={props.placeholder} disabled={props.disabled} />
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
const _meta = {
|
|
230
230
|
component: Input,
|
|
231
|
-
args: { placeholder:
|
|
231
|
+
args: { placeholder: 'Type here' },
|
|
232
232
|
} satisfies Meta<typeof Input>
|
|
233
233
|
|
|
234
234
|
type Story = StoryObj<typeof _meta>
|
|
@@ -240,7 +240,7 @@ describe("Meta and StoryObj types", () => {
|
|
|
240
240
|
expect(primary.args!.disabled).toBe(true)
|
|
241
241
|
})
|
|
242
242
|
|
|
243
|
-
it(
|
|
243
|
+
it('StoryObj supports custom render function', () => {
|
|
244
244
|
function Card(props: { title: string }) {
|
|
245
245
|
return (
|
|
246
246
|
<div class="card">
|
|
@@ -251,7 +251,7 @@ describe("Meta and StoryObj types", () => {
|
|
|
251
251
|
|
|
252
252
|
const _meta = {
|
|
253
253
|
component: Card,
|
|
254
|
-
args: { title:
|
|
254
|
+
args: { title: 'Default' },
|
|
255
255
|
} satisfies Meta<typeof Card>
|
|
256
256
|
|
|
257
257
|
type Story = StoryObj<typeof _meta>
|
|
@@ -265,12 +265,12 @@ describe("Meta and StoryObj types", () => {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
const canvas = createCanvas()
|
|
268
|
-
const vnode = withWrapper.render!({ title:
|
|
268
|
+
const vnode = withWrapper.render!({ title: 'Custom' }, {} as any)
|
|
269
269
|
const unmount = mount(vnode, canvas)
|
|
270
270
|
|
|
271
|
-
expect(canvas.querySelector(
|
|
272
|
-
expect(canvas.querySelector(
|
|
273
|
-
expect(canvas.textContent).toBe(
|
|
271
|
+
expect(canvas.querySelector('.wrapper')).toBeTruthy()
|
|
272
|
+
expect(canvas.querySelector('.card')).toBeTruthy()
|
|
273
|
+
expect(canvas.textContent).toBe('Custom')
|
|
274
274
|
unmount()
|
|
275
275
|
canvas.remove()
|
|
276
276
|
})
|
|
@@ -278,8 +278,8 @@ describe("Meta and StoryObj types", () => {
|
|
|
278
278
|
|
|
279
279
|
// ─── Decorators ──────────────────────────────────────────────────────────────
|
|
280
280
|
|
|
281
|
-
describe(
|
|
282
|
-
it(
|
|
281
|
+
describe('Decorators', () => {
|
|
282
|
+
it('decorator wraps a story', () => {
|
|
283
283
|
function Button(props: { label: string }) {
|
|
284
284
|
return <button>{props.label}</button>
|
|
285
285
|
}
|
|
@@ -290,23 +290,23 @@ describe("Decorators", () => {
|
|
|
290
290
|
|
|
291
291
|
const canvas = createCanvas()
|
|
292
292
|
const storyResult = withPadding((args) => <Button {...args} />, {
|
|
293
|
-
args: { label:
|
|
293
|
+
args: { label: 'Wrapped' },
|
|
294
294
|
argTypes: {},
|
|
295
295
|
globals: {},
|
|
296
|
-
id:
|
|
297
|
-
kind:
|
|
298
|
-
name:
|
|
299
|
-
viewMode:
|
|
296
|
+
id: '1',
|
|
297
|
+
kind: 'Button',
|
|
298
|
+
name: 'Primary',
|
|
299
|
+
viewMode: 'story',
|
|
300
300
|
})
|
|
301
301
|
|
|
302
302
|
const unmount = mount(storyResult, canvas)
|
|
303
|
-
expect(canvas.querySelector(
|
|
304
|
-
expect(canvas.querySelector(
|
|
303
|
+
expect(canvas.querySelector('div[style]')).toBeTruthy()
|
|
304
|
+
expect(canvas.querySelector('button')!.textContent).toBe('Wrapped')
|
|
305
305
|
unmount()
|
|
306
306
|
canvas.remove()
|
|
307
307
|
})
|
|
308
308
|
|
|
309
|
-
it(
|
|
309
|
+
it('multiple decorators compose correctly', () => {
|
|
310
310
|
function Text(props: { content: string }) {
|
|
311
311
|
return <span>{props.content}</span>
|
|
312
312
|
}
|
|
@@ -320,13 +320,13 @@ describe("Decorators", () => {
|
|
|
320
320
|
)
|
|
321
321
|
|
|
322
322
|
const context: StoryContext<{ content: string }> = {
|
|
323
|
-
args: { content:
|
|
323
|
+
args: { content: 'Hello' },
|
|
324
324
|
argTypes: {},
|
|
325
325
|
globals: {},
|
|
326
|
-
id:
|
|
327
|
-
kind:
|
|
328
|
-
name:
|
|
329
|
-
viewMode:
|
|
326
|
+
id: '1',
|
|
327
|
+
kind: 'Text',
|
|
328
|
+
name: 'Default',
|
|
329
|
+
viewMode: 'story',
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
// Compose: withTheme(withBorder(story))
|
|
@@ -335,9 +335,9 @@ describe("Decorators", () => {
|
|
|
335
335
|
|
|
336
336
|
const canvas = createCanvas()
|
|
337
337
|
const unmount = mount(decorated, canvas)
|
|
338
|
-
expect(canvas.querySelector(
|
|
339
|
-
expect(canvas.querySelector(
|
|
340
|
-
expect(canvas.querySelector(
|
|
338
|
+
expect(canvas.querySelector('.theme-dark')).toBeTruthy()
|
|
339
|
+
expect(canvas.querySelector('.border')).toBeTruthy()
|
|
340
|
+
expect(canvas.querySelector('span')!.textContent).toBe('Hello')
|
|
341
341
|
unmount()
|
|
342
342
|
canvas.remove()
|
|
343
343
|
})
|
|
@@ -345,8 +345,8 @@ describe("Decorators", () => {
|
|
|
345
345
|
|
|
346
346
|
// ─── Fragment and multiple children ──────────────────────────────────────────
|
|
347
347
|
|
|
348
|
-
describe(
|
|
349
|
-
it(
|
|
348
|
+
describe('Fragment stories', () => {
|
|
349
|
+
it('renders a story returning a Fragment', () => {
|
|
350
350
|
const canvas = createCanvas()
|
|
351
351
|
renderToCanvas(
|
|
352
352
|
makeRenderContext({
|
|
@@ -360,48 +360,48 @@ describe("Fragment stories", () => {
|
|
|
360
360
|
canvas,
|
|
361
361
|
)
|
|
362
362
|
|
|
363
|
-
const paragraphs = canvas.querySelectorAll(
|
|
363
|
+
const paragraphs = canvas.querySelectorAll('p')
|
|
364
364
|
expect(paragraphs.length).toBe(2)
|
|
365
|
-
expect(paragraphs[0]!.textContent).toBe(
|
|
366
|
-
expect(paragraphs[1]!.textContent).toBe(
|
|
365
|
+
expect(paragraphs[0]!.textContent).toBe('Line 1')
|
|
366
|
+
expect(paragraphs[1]!.textContent).toBe('Line 2')
|
|
367
367
|
canvas.remove()
|
|
368
368
|
})
|
|
369
369
|
})
|
|
370
370
|
|
|
371
371
|
// ─── Preview render function ─────────────────────────────────────────────────
|
|
372
372
|
|
|
373
|
-
describe(
|
|
374
|
-
it(
|
|
373
|
+
describe('preview render', () => {
|
|
374
|
+
it('renders a component with args', () => {
|
|
375
375
|
function Badge(props: { text: string }) {
|
|
376
376
|
return <span class="badge">{props.text}</span>
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
const canvas = createCanvas()
|
|
380
|
-
const vnode = previewRender({ text:
|
|
380
|
+
const vnode = previewRender({ text: 'New' }, { component: Badge })
|
|
381
381
|
const unmount = mount(vnode, canvas)
|
|
382
382
|
|
|
383
|
-
expect(canvas.querySelector(
|
|
383
|
+
expect(canvas.querySelector('.badge')!.textContent).toBe('New')
|
|
384
384
|
unmount()
|
|
385
385
|
canvas.remove()
|
|
386
386
|
})
|
|
387
387
|
|
|
388
|
-
it(
|
|
389
|
-
expect(() => previewRender({ foo:
|
|
390
|
-
|
|
388
|
+
it('throws when no component is provided', () => {
|
|
389
|
+
expect(() => previewRender({ foo: 'bar' }, {})).toThrow(
|
|
390
|
+
'[@pyreon/storybook] No component provided',
|
|
391
391
|
)
|
|
392
392
|
})
|
|
393
393
|
|
|
394
|
-
it(
|
|
395
|
-
expect(() => previewRender({ foo:
|
|
396
|
-
|
|
394
|
+
it('throws when component is undefined', () => {
|
|
395
|
+
expect(() => previewRender({ foo: 'bar' }, { component: undefined } as any)).toThrow(
|
|
396
|
+
'[@pyreon/storybook] No component provided',
|
|
397
397
|
)
|
|
398
398
|
})
|
|
399
399
|
})
|
|
400
400
|
|
|
401
401
|
// ─── Decorator wrapping ─────────────────────────────────────────────────────
|
|
402
402
|
|
|
403
|
-
describe(
|
|
404
|
-
it(
|
|
403
|
+
describe('decorator wrapping', () => {
|
|
404
|
+
it('decorator modifies the rendered VNode structure', () => {
|
|
405
405
|
function Badge(props: { text: string }) {
|
|
406
406
|
return <span class="badge">{props.text}</span>
|
|
407
407
|
}
|
|
@@ -417,21 +417,21 @@ describe("decorator wrapping", () => {
|
|
|
417
417
|
|
|
418
418
|
const canvas = createCanvas()
|
|
419
419
|
const context: StoryContext<{ text: string }> = {
|
|
420
|
-
args: { text:
|
|
420
|
+
args: { text: 'Info' },
|
|
421
421
|
argTypes: {},
|
|
422
422
|
globals: {},
|
|
423
|
-
id:
|
|
424
|
-
kind:
|
|
425
|
-
name:
|
|
426
|
-
viewMode:
|
|
423
|
+
id: '1',
|
|
424
|
+
kind: 'Badge',
|
|
425
|
+
name: 'Default',
|
|
426
|
+
viewMode: 'story',
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
const decorated = withCard((args) => <Badge {...args} />, context)
|
|
430
430
|
const unmount = mount(decorated, canvas)
|
|
431
431
|
|
|
432
|
-
expect(canvas.querySelector(
|
|
433
|
-
expect(canvas.querySelector(
|
|
434
|
-
expect(canvas.querySelector(
|
|
432
|
+
expect(canvas.querySelector('.card')).toBeTruthy()
|
|
433
|
+
expect(canvas.querySelector('h3')!.textContent).toBe('Decorated')
|
|
434
|
+
expect(canvas.querySelector('.badge')!.textContent).toBe('Info')
|
|
435
435
|
unmount()
|
|
436
436
|
canvas.remove()
|
|
437
437
|
})
|
|
@@ -439,8 +439,8 @@ describe("decorator wrapping", () => {
|
|
|
439
439
|
|
|
440
440
|
// ─── Component with no args ─────────────────────────────────────────────────
|
|
441
441
|
|
|
442
|
-
describe(
|
|
443
|
-
it(
|
|
442
|
+
describe('component with no args', () => {
|
|
443
|
+
it('renders a component without any props via renderToCanvas', () => {
|
|
444
444
|
function Logo() {
|
|
445
445
|
return <img src="/logo.png" alt="Logo" />
|
|
446
446
|
}
|
|
@@ -454,12 +454,12 @@ describe("component with no args", () => {
|
|
|
454
454
|
canvas,
|
|
455
455
|
)
|
|
456
456
|
|
|
457
|
-
expect(canvas.querySelector(
|
|
458
|
-
expect(canvas.querySelector(
|
|
457
|
+
expect(canvas.querySelector('img')).toBeTruthy()
|
|
458
|
+
expect(canvas.querySelector('img')!.getAttribute('alt')).toBe('Logo')
|
|
459
459
|
canvas.remove()
|
|
460
460
|
})
|
|
461
461
|
|
|
462
|
-
it(
|
|
462
|
+
it('renders a component with no args via defaultRender', () => {
|
|
463
463
|
function Divider() {
|
|
464
464
|
return <hr class="divider" />
|
|
465
465
|
}
|
|
@@ -468,13 +468,13 @@ describe("component with no args", () => {
|
|
|
468
468
|
const vnode = defaultRender(Divider, {})
|
|
469
469
|
const unmount = mount(vnode, canvas)
|
|
470
470
|
|
|
471
|
-
expect(canvas.querySelector(
|
|
472
|
-
expect(canvas.querySelector(
|
|
471
|
+
expect(canvas.querySelector('hr')).toBeTruthy()
|
|
472
|
+
expect(canvas.querySelector('.divider')).toBeTruthy()
|
|
473
473
|
unmount()
|
|
474
474
|
canvas.remove()
|
|
475
475
|
})
|
|
476
476
|
|
|
477
|
-
it(
|
|
477
|
+
it('preview render works with empty args', () => {
|
|
478
478
|
function Spinner() {
|
|
479
479
|
return <div class="spinner">Loading...</div>
|
|
480
480
|
}
|
|
@@ -483,7 +483,7 @@ describe("component with no args", () => {
|
|
|
483
483
|
const vnode = previewRender({}, { component: Spinner })
|
|
484
484
|
const unmount = mount(vnode, canvas)
|
|
485
485
|
|
|
486
|
-
expect(canvas.querySelector(
|
|
486
|
+
expect(canvas.querySelector('.spinner')!.textContent).toBe('Loading...')
|
|
487
487
|
unmount()
|
|
488
488
|
canvas.remove()
|
|
489
489
|
})
|
|
@@ -491,15 +491,15 @@ describe("component with no args", () => {
|
|
|
491
491
|
|
|
492
492
|
// ─── Error handling ──────────────────────────────────────────────────────────
|
|
493
493
|
|
|
494
|
-
describe(
|
|
495
|
-
it(
|
|
494
|
+
describe('error handling', () => {
|
|
495
|
+
it('showError is called when storyFn itself throws', () => {
|
|
496
496
|
const canvas = createCanvas()
|
|
497
497
|
let errorShown: { title: string; description: string } | null = null
|
|
498
498
|
|
|
499
499
|
renderToCanvas(
|
|
500
500
|
{
|
|
501
501
|
storyFn: () => {
|
|
502
|
-
throw new Error(
|
|
502
|
+
throw new Error('storyFn exploded')
|
|
503
503
|
},
|
|
504
504
|
storyContext: { args: {} },
|
|
505
505
|
showMain: () => {
|
|
@@ -514,18 +514,18 @@ describe("error handling", () => {
|
|
|
514
514
|
)
|
|
515
515
|
|
|
516
516
|
expect(errorShown).not.toBeNull()
|
|
517
|
-
expect(errorShown!.description).toBe(
|
|
517
|
+
expect(errorShown!.description).toBe('storyFn exploded')
|
|
518
518
|
canvas.remove()
|
|
519
519
|
})
|
|
520
520
|
|
|
521
|
-
it(
|
|
521
|
+
it('showError receives string-coerced non-Error throws', () => {
|
|
522
522
|
const canvas = createCanvas()
|
|
523
523
|
let errorShown: { title: string; description: string } | null = null
|
|
524
524
|
|
|
525
525
|
renderToCanvas(
|
|
526
526
|
{
|
|
527
527
|
storyFn: () => {
|
|
528
|
-
throw
|
|
528
|
+
throw 'raw string error'
|
|
529
529
|
},
|
|
530
530
|
storyContext: { args: {} },
|
|
531
531
|
showMain: () => {
|
|
@@ -540,15 +540,15 @@ describe("error handling", () => {
|
|
|
540
540
|
)
|
|
541
541
|
|
|
542
542
|
expect(errorShown).not.toBeNull()
|
|
543
|
-
expect(errorShown!.description).toBe(
|
|
543
|
+
expect(errorShown!.description).toBe('raw string error')
|
|
544
544
|
canvas.remove()
|
|
545
545
|
})
|
|
546
546
|
|
|
547
|
-
it(
|
|
547
|
+
it('component that throws during setup is handled by the framework', () => {
|
|
548
548
|
const canvas = createCanvas()
|
|
549
549
|
|
|
550
550
|
function Broken(): never {
|
|
551
|
-
throw new Error(
|
|
551
|
+
throw new Error('Component exploded')
|
|
552
552
|
}
|
|
553
553
|
|
|
554
554
|
// Pyreon's mount catches component setup errors internally,
|
|
@@ -578,23 +578,23 @@ describe("error handling", () => {
|
|
|
578
578
|
|
|
579
579
|
// ─── Re-render cleanup ──────────────────────────────────────────────────────
|
|
580
580
|
|
|
581
|
-
describe(
|
|
582
|
-
it(
|
|
581
|
+
describe('re-render cleanup', () => {
|
|
582
|
+
it('calling renderToCanvas twice cleans up previous mount', () => {
|
|
583
583
|
const canvas = createCanvas()
|
|
584
584
|
|
|
585
585
|
renderToCanvas(makeRenderContext({ storyFn: () => <div class="first">First</div> }), canvas)
|
|
586
|
-
expect(canvas.querySelector(
|
|
586
|
+
expect(canvas.querySelector('.first')).toBeTruthy()
|
|
587
587
|
|
|
588
588
|
renderToCanvas(makeRenderContext({ storyFn: () => <div class="second">Second</div> }), canvas)
|
|
589
589
|
|
|
590
590
|
// Previous content should be gone
|
|
591
|
-
expect(canvas.querySelector(
|
|
592
|
-
expect(canvas.querySelector(
|
|
593
|
-
expect(canvas.textContent).toBe(
|
|
591
|
+
expect(canvas.querySelector('.first')).toBeNull()
|
|
592
|
+
expect(canvas.querySelector('.second')).toBeTruthy()
|
|
593
|
+
expect(canvas.textContent).toBe('Second')
|
|
594
594
|
canvas.remove()
|
|
595
595
|
})
|
|
596
596
|
|
|
597
|
-
it(
|
|
597
|
+
it('re-render disposes effects from previous story', () => {
|
|
598
598
|
const canvas = createCanvas()
|
|
599
599
|
let effectCount = 0
|
|
600
600
|
|
|
@@ -624,7 +624,7 @@ describe("re-render cleanup", () => {
|
|
|
624
624
|
canvas.remove()
|
|
625
625
|
})
|
|
626
626
|
|
|
627
|
-
it(
|
|
627
|
+
it('showMain is called on successful render', () => {
|
|
628
628
|
const canvas = createCanvas()
|
|
629
629
|
let mainShown = false
|
|
630
630
|
|
|
@@ -650,20 +650,20 @@ describe("re-render cleanup", () => {
|
|
|
650
650
|
|
|
651
651
|
// ─── Missing component in context ───────────────────────────────────────────
|
|
652
652
|
|
|
653
|
-
describe(
|
|
654
|
-
it(
|
|
653
|
+
describe('missing component in context', () => {
|
|
654
|
+
it('preview render throws when context has no component', () => {
|
|
655
655
|
expect(() => previewRender({ value: 1 }, {})).toThrow(
|
|
656
|
-
|
|
656
|
+
'[@pyreon/storybook] No component provided',
|
|
657
657
|
)
|
|
658
658
|
})
|
|
659
659
|
|
|
660
|
-
it(
|
|
660
|
+
it('preview render throws when context.component is null', () => {
|
|
661
661
|
expect(() => previewRender({ value: 1 }, { component: null } as any)).toThrow(
|
|
662
|
-
|
|
662
|
+
'[@pyreon/storybook] No component provided',
|
|
663
663
|
)
|
|
664
664
|
})
|
|
665
665
|
|
|
666
|
-
it(
|
|
666
|
+
it('renderToCanvas works without component in storyContext when storyFn is provided', () => {
|
|
667
667
|
const canvas = createCanvas()
|
|
668
668
|
|
|
669
669
|
renderToCanvas(
|
|
@@ -673,7 +673,7 @@ describe("missing component in context", () => {
|
|
|
673
673
|
canvas,
|
|
674
674
|
)
|
|
675
675
|
|
|
676
|
-
expect(canvas.textContent).toBe(
|
|
676
|
+
expect(canvas.textContent).toBe('No component needed')
|
|
677
677
|
canvas.remove()
|
|
678
678
|
})
|
|
679
679
|
})
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentFn, Props, VNodeChild } from
|
|
1
|
+
import type { ComponentFn, Props, VNodeChild } from '@pyreon/core'
|
|
2
2
|
|
|
3
3
|
// ─── Storybook Renderer Interface ────────────────────────────────────────────
|
|
4
4
|
|
|
@@ -26,7 +26,7 @@ export interface StoryContext<TArgs = Props> {
|
|
|
26
26
|
id: string
|
|
27
27
|
kind: string
|
|
28
28
|
name: string
|
|
29
|
-
viewMode:
|
|
29
|
+
viewMode: 'story' | 'docs'
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export type StoryFn<TArgs = Props> = (args: TArgs, context: StoryContext<TArgs>) => VNodeChild
|