@pyreon/storybook 0.10.0 → 0.11.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/lib/analysis/index.js.html +1 -1
- package/lib/analysis/preview.js.html +1 -1
- package/lib/index.js +1 -49
- package/lib/index.js.map +1 -1
- package/lib/preset.js.map +1 -1
- package/lib/preview.js +1 -49
- package/lib/preview.js.map +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +9 -8
- 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 +365 -105
- package/src/types.ts +4 -10
|
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
|
|
|
5386
5386
|
</script>
|
|
5387
5387
|
<script>
|
|
5388
5388
|
/*<!--*/
|
|
5389
|
-
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"
|
|
5389
|
+
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"92260a4b-1","name":"render-impl.tsx"},{"uid":"92260a4b-3","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"92260a4b-1":{"renderedLength":1280,"gzipLength":673,"brotliLength":0,"metaUid":"92260a4b-0"},"92260a4b-3":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"92260a4b-2"}},"nodeMetas":{"92260a4b-0":{"id":"/src/render-impl.tsx","moduleParts":{"index.js":"92260a4b-1"},"imported":[{"uid":"92260a4b-7"},{"uid":"92260a4b-8"}],"importedBy":[{"uid":"92260a4b-4"}]},"92260a4b-2":{"id":"/src/index.ts","moduleParts":{"index.js":"92260a4b-3"},"imported":[{"uid":"92260a4b-4"},{"uid":"92260a4b-5"},{"uid":"92260a4b-6"},{"uid":"92260a4b-7"}],"importedBy":[],"isEntry":true},"92260a4b-4":{"id":"/src/render.ts","moduleParts":{},"imported":[{"uid":"92260a4b-0"}],"importedBy":[{"uid":"92260a4b-2"}]},"92260a4b-5":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"92260a4b-2"}]},"92260a4b-6":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"92260a4b-2"}]},"92260a4b-7":{"id":"@pyreon/runtime-dom","moduleParts":{},"imported":[],"importedBy":[{"uid":"92260a4b-2"},{"uid":"92260a4b-0"}]},"92260a4b-8":{"id":"@pyreon/core/jsx-runtime","moduleParts":{},"imported":[],"importedBy":[{"uid":"92260a4b-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
|
|
5390
5390
|
|
|
5391
5391
|
const run = () => {
|
|
5392
5392
|
const width = window.innerWidth;
|
|
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
|
|
|
5386
5386
|
</script>
|
|
5387
5387
|
<script>
|
|
5388
5388
|
/*<!--*/
|
|
5389
|
-
const data = {"version":2,"tree":{"name":"root","children":[{"name":"preview.js","children":[{"name":"
|
|
5389
|
+
const data = {"version":2,"tree":{"name":"root","children":[{"name":"preview.js","children":[{"name":"src","children":[{"uid":"08ed9fb2-1","name":"render-impl.tsx"},{"uid":"08ed9fb2-3","name":"preview-impl.tsx"},{"uid":"08ed9fb2-5","name":"preview.ts"}]}]}],"isRoot":true},"nodeParts":{"08ed9fb2-1":{"renderedLength":1100,"gzipLength":595,"brotliLength":0,"metaUid":"08ed9fb2-0"},"08ed9fb2-3":{"renderedLength":452,"gzipLength":313,"brotliLength":0,"metaUid":"08ed9fb2-2"},"08ed9fb2-5":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"08ed9fb2-4"}},"nodeMetas":{"08ed9fb2-0":{"id":"/src/render-impl.tsx","moduleParts":{"preview.js":"08ed9fb2-1"},"imported":[{"uid":"08ed9fb2-7"},{"uid":"08ed9fb2-6"}],"importedBy":[{"uid":"08ed9fb2-2"}]},"08ed9fb2-2":{"id":"/src/preview-impl.tsx","moduleParts":{"preview.js":"08ed9fb2-3"},"imported":[{"uid":"08ed9fb2-0"},{"uid":"08ed9fb2-6"}],"importedBy":[{"uid":"08ed9fb2-4"}]},"08ed9fb2-4":{"id":"/src/preview.ts","moduleParts":{"preview.js":"08ed9fb2-5"},"imported":[{"uid":"08ed9fb2-2"}],"importedBy":[],"isEntry":true},"08ed9fb2-6":{"id":"@pyreon/core/jsx-runtime","moduleParts":{},"imported":[],"importedBy":[{"uid":"08ed9fb2-2"},{"uid":"08ed9fb2-0"}]},"08ed9fb2-7":{"id":"@pyreon/runtime-dom","moduleParts":{},"imported":[],"importedBy":[{"uid":"08ed9fb2-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
|
|
5390
5390
|
|
|
5391
5391
|
const run = () => {
|
|
5392
5392
|
const width = window.innerWidth;
|
package/lib/index.js
CHANGED
|
@@ -1,56 +1,8 @@
|
|
|
1
1
|
import { mount, mount as mount$1 } from "@pyreon/runtime-dom";
|
|
2
|
+
import { jsx } from "@pyreon/core/jsx-runtime";
|
|
2
3
|
import { Fragment, h } from "@pyreon/core";
|
|
3
4
|
import { computed, effect, signal } from "@pyreon/reactivity";
|
|
4
5
|
|
|
5
|
-
//#region ../../node_modules/.bun/@pyreon+core@0.7.11/node_modules/@pyreon/core/lib/jsx-runtime.js
|
|
6
|
-
/**
|
|
7
|
-
* Hyperscript function — the compiled output of JSX.
|
|
8
|
-
* `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
|
|
9
|
-
*
|
|
10
|
-
* Generic on P so TypeScript validates props match the component's signature
|
|
11
|
-
* at the call site, then stores the result in the loosely-typed VNode.
|
|
12
|
-
*/
|
|
13
|
-
/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */
|
|
14
|
-
const EMPTY_PROPS = {};
|
|
15
|
-
function h$1(type, props, ...children) {
|
|
16
|
-
return {
|
|
17
|
-
type,
|
|
18
|
-
props: props ?? EMPTY_PROPS,
|
|
19
|
-
children: normalizeChildren(children),
|
|
20
|
-
key: props?.key ?? null
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
function normalizeChildren(children) {
|
|
24
|
-
for (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);
|
|
25
|
-
return children;
|
|
26
|
-
}
|
|
27
|
-
function flattenChildren(children) {
|
|
28
|
-
const result = [];
|
|
29
|
-
for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));
|
|
30
|
-
else result.push(child);
|
|
31
|
-
return result;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* JSX automatic runtime.
|
|
35
|
-
*
|
|
36
|
-
* When tsconfig has `"jsxImportSource": "@pyreon/core"`, the TS/bundler compiler
|
|
37
|
-
* rewrites JSX to imports from this file automatically:
|
|
38
|
-
* <div class="x" /> → jsx("div", { class: "x" })
|
|
39
|
-
*/
|
|
40
|
-
function jsx(type, props, key) {
|
|
41
|
-
const { children, ...rest } = props;
|
|
42
|
-
const propsWithKey = key != null ? {
|
|
43
|
-
...rest,
|
|
44
|
-
key
|
|
45
|
-
} : rest;
|
|
46
|
-
if (typeof type === "function") return h$1(type, children !== void 0 ? {
|
|
47
|
-
...propsWithKey,
|
|
48
|
-
children
|
|
49
|
-
} : propsWithKey);
|
|
50
|
-
return h$1(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
//#endregion
|
|
54
6
|
//#region src/render-impl.tsx
|
|
55
7
|
/**
|
|
56
8
|
* State tracked per canvas element so we can clean up between renders.
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["
|
|
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
CHANGED
|
@@ -1,54 +1,6 @@
|
|
|
1
1
|
import { mount } from "@pyreon/runtime-dom";
|
|
2
|
+
import { jsx } from "@pyreon/core/jsx-runtime";
|
|
2
3
|
|
|
3
|
-
//#region ../../node_modules/.bun/@pyreon+core@0.7.11/node_modules/@pyreon/core/lib/jsx-runtime.js
|
|
4
|
-
/**
|
|
5
|
-
* Hyperscript function — the compiled output of JSX.
|
|
6
|
-
* `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
|
|
7
|
-
*
|
|
8
|
-
* Generic on P so TypeScript validates props match the component's signature
|
|
9
|
-
* at the call site, then stores the result in the loosely-typed VNode.
|
|
10
|
-
*/
|
|
11
|
-
/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */
|
|
12
|
-
const EMPTY_PROPS = {};
|
|
13
|
-
function h(type, props, ...children) {
|
|
14
|
-
return {
|
|
15
|
-
type,
|
|
16
|
-
props: props ?? EMPTY_PROPS,
|
|
17
|
-
children: normalizeChildren(children),
|
|
18
|
-
key: props?.key ?? null
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
function normalizeChildren(children) {
|
|
22
|
-
for (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);
|
|
23
|
-
return children;
|
|
24
|
-
}
|
|
25
|
-
function flattenChildren(children) {
|
|
26
|
-
const result = [];
|
|
27
|
-
for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));
|
|
28
|
-
else result.push(child);
|
|
29
|
-
return result;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* JSX automatic runtime.
|
|
33
|
-
*
|
|
34
|
-
* When tsconfig has `"jsxImportSource": "@pyreon/core"`, the TS/bundler compiler
|
|
35
|
-
* rewrites JSX to imports from this file automatically:
|
|
36
|
-
* <div class="x" /> → jsx("div", { class: "x" })
|
|
37
|
-
*/
|
|
38
|
-
function jsx(type, props, key) {
|
|
39
|
-
const { children, ...rest } = props;
|
|
40
|
-
const propsWithKey = key != null ? {
|
|
41
|
-
...rest,
|
|
42
|
-
key
|
|
43
|
-
} : rest;
|
|
44
|
-
if (typeof type === "function") return h(type, children !== void 0 ? {
|
|
45
|
-
...propsWithKey,
|
|
46
|
-
children
|
|
47
|
-
} : propsWithKey);
|
|
48
|
-
return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
//#endregion
|
|
52
4
|
//#region src/render-impl.tsx
|
|
53
5
|
/**
|
|
54
6
|
* State tracked per canvas element so we can clean up between renders.
|
package/lib/preview.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preview.js","names":[],"sources":["
|
|
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/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/render-impl.tsx"],"mappings":";;;;;;;;;UAQiB,cAAA;EACf,SAAA,EAAW,aAAA;EACX,WAAA,EAAa,YAAA;EACb,aAAA,EAAe,WAAA;AAAA;;KAML,UAAA,MAAgB,CAAA,SAAU,aAAA,YAAuB,CAAA,GAAI,OAAA;AAAA,UAIhD,YAAA,SAAqB,OAAA;EACpC,IAAA,EAAM,KAAA;EACN,QAAA,EAAU,MAAA;EACV,OAAA,EAAS,MAAA;EACT,EAAA;EACA,IAAA;EACA,IAAA;EACA,QAAA;AAAA;AAAA,KAGU,OAAA,SAAgB,OAAA,
|
|
1
|
+
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/render-impl.tsx"],"mappings":";;;;;;;;;UAQiB,cAAA;EACf,SAAA,EAAW,aAAA;EACX,WAAA,EAAa,YAAA;EACb,aAAA,EAAe,WAAA;AAAA;;KAML,UAAA,MAAgB,CAAA,SAAU,aAAA,YAAuB,CAAA,GAAI,OAAA;AAAA,UAIhD,YAAA,SAAqB,OAAA;EACpC,IAAA,EAAM,KAAA;EACN,QAAA,EAAU,MAAA;EACV,OAAA,EAAS,MAAA;EACT,EAAA;EACA,IAAA;EACA,IAAA;EACA,QAAA;AAAA;AAAA,KAGU,OAAA,SAAgB,OAAA,KAAU,IAAA,EAAM,KAAA,EAAO,OAAA,EAAS,YAAA,CAAa,KAAA,MAAW,YAAA;AAAA,KAExE,WAAA,SAAoB,OAAA,KAC9B,OAAA,EAAS,OAAA,CAAQ,KAAA,GACjB,OAAA,EAAS,YAAA,CAAa,KAAA,MACnB,YAAA;AAAA,UAIY,IAAA,oBAAwB,aAAA,QAAmB,aAAA;EAvBhC;EAyB1B,SAAA,GAAY,UAAA;EAzBmD;EA2B/D,KAAA;EA3BoE;EA6BpE,UAAA,GAAa,WAAA,CAAY,UAAA,CAAW,UAAA;EA7BV;EA+B1B,IAAA,GAAO,OAAA,CAAQ,UAAA,CAAW,UAAA;EA/B4B;EAiCtD,QAAA,GAAW,MAAA;EAjCoD;EAmC/D,UAAA,GAAa,MAAA;EAnCuD;EAqCpE,IAAA;EAjC2B;;;;EAsC3B,MAAA,IACE,IAAA,EAAM,UAAA,CAAW,UAAA,GACjB,OAAA,EAAS,YAAA,CAAa,UAAA,CAAW,UAAA,OAC9B,YAAA;EAtCI;EAwCT,cAAA,uBAAqC,MAAA;EAxCtB;EA0Cf,cAAA,uBAAqC,MAAA;AAAA;AAAA,UAKtB,QAAA,eAAuB,IAAA,QAAY,IAAA;EAjD5C;EAmDN,IAAA,GAAO,OAAA,CAAQ,QAAA,CAAS,KAAA;EAlDd;EAoDV,QAAA,GAAW,MAAA;EAnDF;EAqDT,UAAA,GAAa,WAAA,CAAY,QAAA,CAAS,KAAA;EAnDlC;EAqDA,UAAA,GAAa,MAAA;EAnDb;EAqDA,IAAA;EArDQ;EAuDR,MAAA,IAAU,IAAA,EAAM,QAAA,CAAS,KAAA,GAAQ,OAAA,EAAS,YAAA,CAAa,QAAA,CAAS,KAAA,OAAY,YAAA;EApD3D;EAsDjB,IAAA;EAtD0B;EAwD1B,IAAA,IAAQ,OAAA;IACN,aAAA,EAAe,WAAA;IACf,IAAA,EAAM,QAAA,CAAS,KAAA;IACf,IAAA,GAAO,IAAA,UAAc,EAAA,QAAU,OAAA,WAAkB,OAAA;EAAA,MAC7C,OAAA;AAAA;;KAIH,QAAA,UAAkB,KAAA,SAAc,IAAA,YAAgB,UAAA,CAAW,CAAA,IAAK,OAAA;;;;;;;AAvFrE;;;;;;;;iBCYgB,cAAA,CAAA;EAEZ,OAAA;EACA,QAAA;EACA;AAAA;EAEA,OAAA,QAAe,YAAA;EACf,YAAA;IACE,SAAA,GAAY,aAAA;IACZ,IAAA,EAAM,MAAA;IAAA,CACL,GAAA;EAAA;EAEH,QAAA;EACA,SAAA,GAAY,KAAA;IAAS,KAAA;IAAe,WAAA;EAAA;EACpC,YAAA;AAAA,GAEF,aAAA,EAAe,WAAA;;;;iBA0BD,aAAA,CACd,SAAA,EAAW,aAAA,OACX,IAAA,EAAM,MAAA,oBACL,YAAA"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/storybook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Storybook renderer for Pyreon — mount, render, and interact with Pyreon components in Storybook",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/pyreon/
|
|
9
|
-
"directory": "packages/storybook"
|
|
8
|
+
"url": "https://github.com/pyreon/pyreon.git",
|
|
9
|
+
"directory": "packages/tools/storybook"
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://github.com/pyreon/fundamentals/tree/main/packages/storybook#readme",
|
|
12
12
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/pyreon/
|
|
13
|
+
"url": "https://github.com/pyreon/pyreon/issues"
|
|
14
14
|
},
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"access": "public"
|
|
@@ -48,12 +48,13 @@
|
|
|
48
48
|
"build": "vl_rolldown_build",
|
|
49
49
|
"dev": "vl_rolldown_build-watch",
|
|
50
50
|
"test": "vitest run",
|
|
51
|
-
"typecheck": "tsc --noEmit"
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"lint": "biome check ."
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
|
-
"@pyreon/core": "
|
|
55
|
-
"@pyreon/reactivity": "
|
|
56
|
-
"@pyreon/runtime-dom": "
|
|
55
|
+
"@pyreon/core": "^0.11.0",
|
|
56
|
+
"@pyreon/reactivity": "^0.11.0",
|
|
57
|
+
"@pyreon/runtime-dom": "^0.11.0",
|
|
57
58
|
"storybook": ">=8.0.0"
|
|
58
59
|
}
|
|
59
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,20 +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 {
|
|
7
|
-
DecoratorFn,
|
|
8
|
-
Meta,
|
|
9
|
-
StoryContext,
|
|
10
|
-
StoryFn,
|
|
11
|
-
StoryObj,
|
|
12
|
-
} from '../types'
|
|
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"
|
|
13
7
|
|
|
14
8
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
15
9
|
|
|
16
10
|
function createCanvas(): HTMLElement {
|
|
17
|
-
const el = document.createElement(
|
|
11
|
+
const el = document.createElement("div")
|
|
18
12
|
document.body.appendChild(el)
|
|
19
13
|
return el
|
|
20
14
|
}
|
|
@@ -27,9 +21,7 @@ function makeRenderContext(overrides: {
|
|
|
27
21
|
return {
|
|
28
22
|
storyFn: overrides.storyFn ?? (() => <div>default</div>),
|
|
29
23
|
storyContext: {
|
|
30
|
-
...(overrides.component != null
|
|
31
|
-
? { component: overrides.component }
|
|
32
|
-
: {}),
|
|
24
|
+
...(overrides.component != null ? { component: overrides.component } : {}),
|
|
33
25
|
args: overrides.args ?? {},
|
|
34
26
|
},
|
|
35
27
|
showMain: () => {
|
|
@@ -44,8 +36,8 @@ function makeRenderContext(overrides: {
|
|
|
44
36
|
|
|
45
37
|
// ─── renderToCanvas ──────────────────────────────────────────────────────────
|
|
46
38
|
|
|
47
|
-
describe(
|
|
48
|
-
it(
|
|
39
|
+
describe("renderToCanvas", () => {
|
|
40
|
+
it("mounts a simple VNode into the canvas", () => {
|
|
49
41
|
const canvas = createCanvas()
|
|
50
42
|
const ctx = makeRenderContext({
|
|
51
43
|
storyFn: () => <button>Click me</button>,
|
|
@@ -53,12 +45,12 @@ describe('renderToCanvas', () => {
|
|
|
53
45
|
|
|
54
46
|
renderToCanvas(ctx, canvas)
|
|
55
47
|
|
|
56
|
-
expect(canvas.innerHTML).toContain(
|
|
57
|
-
expect(canvas.querySelector(
|
|
48
|
+
expect(canvas.innerHTML).toContain("Click me")
|
|
49
|
+
expect(canvas.querySelector("button")).toBeTruthy()
|
|
58
50
|
canvas.remove()
|
|
59
51
|
})
|
|
60
52
|
|
|
61
|
-
it(
|
|
53
|
+
it("mounts a Pyreon component with props", () => {
|
|
62
54
|
function Button(props: { label: string; disabled?: boolean }) {
|
|
63
55
|
return <button disabled={props.disabled ?? false}>{props.label}</button>
|
|
64
56
|
}
|
|
@@ -70,33 +62,27 @@ describe('renderToCanvas', () => {
|
|
|
70
62
|
|
|
71
63
|
renderToCanvas(ctx, canvas)
|
|
72
64
|
|
|
73
|
-
const btn = canvas.querySelector(
|
|
65
|
+
const btn = canvas.querySelector("button")!
|
|
74
66
|
expect(btn).toBeTruthy()
|
|
75
|
-
expect(btn.textContent).toBe(
|
|
76
|
-
expect(btn.getAttribute(
|
|
67
|
+
expect(btn.textContent).toBe("Submit")
|
|
68
|
+
expect(btn.getAttribute("disabled")).not.toBeNull()
|
|
77
69
|
canvas.remove()
|
|
78
70
|
})
|
|
79
71
|
|
|
80
|
-
it(
|
|
72
|
+
it("cleans up previous mount on re-render", () => {
|
|
81
73
|
const canvas = createCanvas()
|
|
82
74
|
|
|
83
|
-
renderToCanvas(
|
|
84
|
-
|
|
85
|
-
canvas,
|
|
86
|
-
)
|
|
87
|
-
expect(canvas.textContent).toBe('First')
|
|
75
|
+
renderToCanvas(makeRenderContext({ storyFn: () => <div>First</div> }), canvas)
|
|
76
|
+
expect(canvas.textContent).toBe("First")
|
|
88
77
|
|
|
89
|
-
renderToCanvas(
|
|
90
|
-
|
|
91
|
-
canvas,
|
|
92
|
-
)
|
|
93
|
-
expect(canvas.textContent).toBe('Second')
|
|
78
|
+
renderToCanvas(makeRenderContext({ storyFn: () => <div>Second</div> }), canvas)
|
|
79
|
+
expect(canvas.textContent).toBe("Second")
|
|
94
80
|
// Only one child — previous mount was cleaned up
|
|
95
81
|
expect(canvas.children.length).toBe(1)
|
|
96
82
|
canvas.remove()
|
|
97
83
|
})
|
|
98
84
|
|
|
99
|
-
it(
|
|
85
|
+
it("disposes reactive effects on cleanup", () => {
|
|
100
86
|
const canvas = createCanvas()
|
|
101
87
|
let effectRunCount = 0
|
|
102
88
|
|
|
@@ -116,10 +102,7 @@ describe('renderToCanvas', () => {
|
|
|
116
102
|
expect(effectRunCount).toBe(initialCount + 1)
|
|
117
103
|
|
|
118
104
|
// Re-render with a different story — should dispose previous effects
|
|
119
|
-
renderToCanvas(
|
|
120
|
-
makeRenderContext({ storyFn: () => <div>New story</div> }),
|
|
121
|
-
canvas,
|
|
122
|
-
)
|
|
105
|
+
renderToCanvas(makeRenderContext({ storyFn: () => <div>New story</div> }), canvas)
|
|
123
106
|
|
|
124
107
|
const countAfterCleanup = effectRunCount
|
|
125
108
|
count.set(2)
|
|
@@ -129,13 +112,13 @@ describe('renderToCanvas', () => {
|
|
|
129
112
|
canvas.remove()
|
|
130
113
|
})
|
|
131
114
|
|
|
132
|
-
it(
|
|
115
|
+
it("shows error when storyFn throws an Error", () => {
|
|
133
116
|
const canvas = createCanvas()
|
|
134
117
|
let errorShown: { title: string; description: string } | null = null
|
|
135
118
|
|
|
136
119
|
const ctx = {
|
|
137
120
|
storyFn: () => {
|
|
138
|
-
throw new Error(
|
|
121
|
+
throw new Error("Boom")
|
|
139
122
|
},
|
|
140
123
|
storyContext: { args: {} },
|
|
141
124
|
showMain: () => {
|
|
@@ -150,17 +133,17 @@ describe('renderToCanvas', () => {
|
|
|
150
133
|
renderToCanvas(ctx, canvas)
|
|
151
134
|
|
|
152
135
|
expect(errorShown).not.toBeNull()
|
|
153
|
-
expect(errorShown!.description).toBe(
|
|
136
|
+
expect(errorShown!.description).toBe("Boom")
|
|
154
137
|
canvas.remove()
|
|
155
138
|
})
|
|
156
139
|
|
|
157
|
-
it(
|
|
140
|
+
it("shows error when storyFn throws a non-Error value", () => {
|
|
158
141
|
const canvas = createCanvas()
|
|
159
142
|
let errorShown: { title: string; description: string } | null = null
|
|
160
143
|
|
|
161
144
|
const ctx = {
|
|
162
145
|
storyFn: () => {
|
|
163
|
-
throw
|
|
146
|
+
throw "string error"
|
|
164
147
|
},
|
|
165
148
|
storyContext: { args: {} },
|
|
166
149
|
showMain: () => {
|
|
@@ -175,11 +158,11 @@ describe('renderToCanvas', () => {
|
|
|
175
158
|
renderToCanvas(ctx, canvas)
|
|
176
159
|
|
|
177
160
|
expect(errorShown).not.toBeNull()
|
|
178
|
-
expect(errorShown!.description).toBe(
|
|
161
|
+
expect(errorShown!.description).toBe("string error")
|
|
179
162
|
canvas.remove()
|
|
180
163
|
})
|
|
181
164
|
|
|
182
|
-
it(
|
|
165
|
+
it("renders reactive components that update the DOM", () => {
|
|
183
166
|
const canvas = createCanvas()
|
|
184
167
|
const count = signal(0)
|
|
185
168
|
|
|
@@ -189,27 +172,27 @@ describe('renderToCanvas', () => {
|
|
|
189
172
|
|
|
190
173
|
renderToCanvas(makeRenderContext({ storyFn: () => <Counter /> }), canvas)
|
|
191
174
|
|
|
192
|
-
expect(canvas.textContent).toBe(
|
|
175
|
+
expect(canvas.textContent).toBe("Count: 0")
|
|
193
176
|
|
|
194
177
|
count.set(5)
|
|
195
|
-
expect(canvas.textContent).toBe(
|
|
178
|
+
expect(canvas.textContent).toBe("Count: 5")
|
|
196
179
|
canvas.remove()
|
|
197
180
|
})
|
|
198
181
|
})
|
|
199
182
|
|
|
200
183
|
// ─── defaultRender ───────────────────────────────────────────────────────────
|
|
201
184
|
|
|
202
|
-
describe(
|
|
203
|
-
it(
|
|
185
|
+
describe("defaultRender", () => {
|
|
186
|
+
it("creates a VNode from component + args", () => {
|
|
204
187
|
function Greeting(props: { name: string }) {
|
|
205
188
|
return <p>Hello, {props.name}!</p>
|
|
206
189
|
}
|
|
207
190
|
|
|
208
191
|
const canvas = createCanvas()
|
|
209
|
-
const vnode = defaultRender(Greeting, { name:
|
|
192
|
+
const vnode = defaultRender(Greeting, { name: "World" })
|
|
210
193
|
const unmount = mount(vnode, canvas)
|
|
211
194
|
|
|
212
|
-
expect(canvas.textContent).toBe(
|
|
195
|
+
expect(canvas.textContent).toBe("Hello, World!")
|
|
213
196
|
unmount()
|
|
214
197
|
canvas.remove()
|
|
215
198
|
})
|
|
@@ -217,12 +200,9 @@ describe('defaultRender', () => {
|
|
|
217
200
|
|
|
218
201
|
// ─── Type-level tests (Meta / StoryObj) ──────────────────────────────────────
|
|
219
202
|
|
|
220
|
-
describe(
|
|
221
|
-
it(
|
|
222
|
-
function Button(props: {
|
|
223
|
-
label: string
|
|
224
|
-
variant?: 'primary' | 'secondary'
|
|
225
|
-
}) {
|
|
203
|
+
describe("Meta and StoryObj types", () => {
|
|
204
|
+
it("Meta accepts a component and typed args", () => {
|
|
205
|
+
function Button(props: { label: string; variant?: "primary" | "secondary" }) {
|
|
226
206
|
return (
|
|
227
207
|
<button type="button" class={props.variant}>
|
|
228
208
|
{props.label}
|
|
@@ -232,23 +212,23 @@ describe('Meta and StoryObj types', () => {
|
|
|
232
212
|
|
|
233
213
|
const meta = {
|
|
234
214
|
component: Button,
|
|
235
|
-
title:
|
|
236
|
-
args: { label:
|
|
237
|
-
tags: [
|
|
215
|
+
title: "Button",
|
|
216
|
+
args: { label: "Click", variant: "primary" as const },
|
|
217
|
+
tags: ["autodocs"],
|
|
238
218
|
} satisfies Meta<typeof Button>
|
|
239
219
|
|
|
240
220
|
expect(meta.component).toBe(Button)
|
|
241
|
-
expect(meta.args!.label).toBe(
|
|
221
|
+
expect(meta.args!.label).toBe("Click")
|
|
242
222
|
})
|
|
243
223
|
|
|
244
|
-
it(
|
|
224
|
+
it("StoryObj inherits args from Meta", () => {
|
|
245
225
|
function Input(props: { placeholder: string; disabled?: boolean }) {
|
|
246
226
|
return <input placeholder={props.placeholder} disabled={props.disabled} />
|
|
247
227
|
}
|
|
248
228
|
|
|
249
229
|
const _meta = {
|
|
250
230
|
component: Input,
|
|
251
|
-
args: { placeholder:
|
|
231
|
+
args: { placeholder: "Type here" },
|
|
252
232
|
} satisfies Meta<typeof Input>
|
|
253
233
|
|
|
254
234
|
type Story = StoryObj<typeof _meta>
|
|
@@ -260,7 +240,7 @@ describe('Meta and StoryObj types', () => {
|
|
|
260
240
|
expect(primary.args!.disabled).toBe(true)
|
|
261
241
|
})
|
|
262
242
|
|
|
263
|
-
it(
|
|
243
|
+
it("StoryObj supports custom render function", () => {
|
|
264
244
|
function Card(props: { title: string }) {
|
|
265
245
|
return (
|
|
266
246
|
<div class="card">
|
|
@@ -271,7 +251,7 @@ describe('Meta and StoryObj types', () => {
|
|
|
271
251
|
|
|
272
252
|
const _meta = {
|
|
273
253
|
component: Card,
|
|
274
|
-
args: { title:
|
|
254
|
+
args: { title: "Default" },
|
|
275
255
|
} satisfies Meta<typeof Card>
|
|
276
256
|
|
|
277
257
|
type Story = StoryObj<typeof _meta>
|
|
@@ -285,12 +265,12 @@ describe('Meta and StoryObj types', () => {
|
|
|
285
265
|
}
|
|
286
266
|
|
|
287
267
|
const canvas = createCanvas()
|
|
288
|
-
const vnode = withWrapper.render!({ title:
|
|
268
|
+
const vnode = withWrapper.render!({ title: "Custom" }, {} as any)
|
|
289
269
|
const unmount = mount(vnode, canvas)
|
|
290
270
|
|
|
291
|
-
expect(canvas.querySelector(
|
|
292
|
-
expect(canvas.querySelector(
|
|
293
|
-
expect(canvas.textContent).toBe(
|
|
271
|
+
expect(canvas.querySelector(".wrapper")).toBeTruthy()
|
|
272
|
+
expect(canvas.querySelector(".card")).toBeTruthy()
|
|
273
|
+
expect(canvas.textContent).toBe("Custom")
|
|
294
274
|
unmount()
|
|
295
275
|
canvas.remove()
|
|
296
276
|
})
|
|
@@ -298,8 +278,8 @@ describe('Meta and StoryObj types', () => {
|
|
|
298
278
|
|
|
299
279
|
// ─── Decorators ──────────────────────────────────────────────────────────────
|
|
300
280
|
|
|
301
|
-
describe(
|
|
302
|
-
it(
|
|
281
|
+
describe("Decorators", () => {
|
|
282
|
+
it("decorator wraps a story", () => {
|
|
303
283
|
function Button(props: { label: string }) {
|
|
304
284
|
return <button>{props.label}</button>
|
|
305
285
|
}
|
|
@@ -310,23 +290,23 @@ describe('Decorators', () => {
|
|
|
310
290
|
|
|
311
291
|
const canvas = createCanvas()
|
|
312
292
|
const storyResult = withPadding((args) => <Button {...args} />, {
|
|
313
|
-
args: { label:
|
|
293
|
+
args: { label: "Wrapped" },
|
|
314
294
|
argTypes: {},
|
|
315
295
|
globals: {},
|
|
316
|
-
id:
|
|
317
|
-
kind:
|
|
318
|
-
name:
|
|
319
|
-
viewMode:
|
|
296
|
+
id: "1",
|
|
297
|
+
kind: "Button",
|
|
298
|
+
name: "Primary",
|
|
299
|
+
viewMode: "story",
|
|
320
300
|
})
|
|
321
301
|
|
|
322
302
|
const unmount = mount(storyResult, canvas)
|
|
323
|
-
expect(canvas.querySelector(
|
|
324
|
-
expect(canvas.querySelector(
|
|
303
|
+
expect(canvas.querySelector("div[style]")).toBeTruthy()
|
|
304
|
+
expect(canvas.querySelector("button")!.textContent).toBe("Wrapped")
|
|
325
305
|
unmount()
|
|
326
306
|
canvas.remove()
|
|
327
307
|
})
|
|
328
308
|
|
|
329
|
-
it(
|
|
309
|
+
it("multiple decorators compose correctly", () => {
|
|
330
310
|
function Text(props: { content: string }) {
|
|
331
311
|
return <span>{props.content}</span>
|
|
332
312
|
}
|
|
@@ -340,13 +320,13 @@ describe('Decorators', () => {
|
|
|
340
320
|
)
|
|
341
321
|
|
|
342
322
|
const context: StoryContext<{ content: string }> = {
|
|
343
|
-
args: { content:
|
|
323
|
+
args: { content: "Hello" },
|
|
344
324
|
argTypes: {},
|
|
345
325
|
globals: {},
|
|
346
|
-
id:
|
|
347
|
-
kind:
|
|
348
|
-
name:
|
|
349
|
-
viewMode:
|
|
326
|
+
id: "1",
|
|
327
|
+
kind: "Text",
|
|
328
|
+
name: "Default",
|
|
329
|
+
viewMode: "story",
|
|
350
330
|
}
|
|
351
331
|
|
|
352
332
|
// Compose: withTheme(withBorder(story))
|
|
@@ -355,9 +335,9 @@ describe('Decorators', () => {
|
|
|
355
335
|
|
|
356
336
|
const canvas = createCanvas()
|
|
357
337
|
const unmount = mount(decorated, canvas)
|
|
358
|
-
expect(canvas.querySelector(
|
|
359
|
-
expect(canvas.querySelector(
|
|
360
|
-
expect(canvas.querySelector(
|
|
338
|
+
expect(canvas.querySelector(".theme-dark")).toBeTruthy()
|
|
339
|
+
expect(canvas.querySelector(".border")).toBeTruthy()
|
|
340
|
+
expect(canvas.querySelector("span")!.textContent).toBe("Hello")
|
|
361
341
|
unmount()
|
|
362
342
|
canvas.remove()
|
|
363
343
|
})
|
|
@@ -365,8 +345,8 @@ describe('Decorators', () => {
|
|
|
365
345
|
|
|
366
346
|
// ─── Fragment and multiple children ──────────────────────────────────────────
|
|
367
347
|
|
|
368
|
-
describe(
|
|
369
|
-
it(
|
|
348
|
+
describe("Fragment stories", () => {
|
|
349
|
+
it("renders a story returning a Fragment", () => {
|
|
370
350
|
const canvas = createCanvas()
|
|
371
351
|
renderToCanvas(
|
|
372
352
|
makeRenderContext({
|
|
@@ -380,40 +360,320 @@ describe('Fragment stories', () => {
|
|
|
380
360
|
canvas,
|
|
381
361
|
)
|
|
382
362
|
|
|
383
|
-
const paragraphs = canvas.querySelectorAll(
|
|
363
|
+
const paragraphs = canvas.querySelectorAll("p")
|
|
384
364
|
expect(paragraphs.length).toBe(2)
|
|
385
|
-
expect(paragraphs[0]!.textContent).toBe(
|
|
386
|
-
expect(paragraphs[1]!.textContent).toBe(
|
|
365
|
+
expect(paragraphs[0]!.textContent).toBe("Line 1")
|
|
366
|
+
expect(paragraphs[1]!.textContent).toBe("Line 2")
|
|
387
367
|
canvas.remove()
|
|
388
368
|
})
|
|
389
369
|
})
|
|
390
370
|
|
|
391
371
|
// ─── Preview render function ─────────────────────────────────────────────────
|
|
392
372
|
|
|
393
|
-
describe(
|
|
394
|
-
it(
|
|
373
|
+
describe("preview render", () => {
|
|
374
|
+
it("renders a component with args", () => {
|
|
395
375
|
function Badge(props: { text: string }) {
|
|
396
376
|
return <span class="badge">{props.text}</span>
|
|
397
377
|
}
|
|
398
378
|
|
|
399
379
|
const canvas = createCanvas()
|
|
400
|
-
const vnode = previewRender({ text:
|
|
380
|
+
const vnode = previewRender({ text: "New" }, { component: Badge })
|
|
401
381
|
const unmount = mount(vnode, canvas)
|
|
402
382
|
|
|
403
|
-
expect(canvas.querySelector(
|
|
383
|
+
expect(canvas.querySelector(".badge")!.textContent).toBe("New")
|
|
404
384
|
unmount()
|
|
405
385
|
canvas.remove()
|
|
406
386
|
})
|
|
407
387
|
|
|
408
|
-
it(
|
|
409
|
-
expect(() => previewRender({ foo:
|
|
410
|
-
|
|
388
|
+
it("throws when no component is provided", () => {
|
|
389
|
+
expect(() => previewRender({ foo: "bar" }, {})).toThrow(
|
|
390
|
+
"[@pyreon/storybook] No component provided",
|
|
391
|
+
)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it("throws when component is undefined", () => {
|
|
395
|
+
expect(() => previewRender({ foo: "bar" }, { component: undefined } as any)).toThrow(
|
|
396
|
+
"[@pyreon/storybook] No component provided",
|
|
411
397
|
)
|
|
412
398
|
})
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
// ─── Decorator wrapping ─────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
describe("decorator wrapping", () => {
|
|
404
|
+
it("decorator modifies the rendered VNode structure", () => {
|
|
405
|
+
function Badge(props: { text: string }) {
|
|
406
|
+
return <span class="badge">{props.text}</span>
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const withCard: DecoratorFn<{ text: string }> = (storyFn, ctx) => {
|
|
410
|
+
return (
|
|
411
|
+
<div class="card">
|
|
412
|
+
<h3>Decorated</h3>
|
|
413
|
+
{storyFn(ctx.args, ctx)}
|
|
414
|
+
</div>
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const canvas = createCanvas()
|
|
419
|
+
const context: StoryContext<{ text: string }> = {
|
|
420
|
+
args: { text: "Info" },
|
|
421
|
+
argTypes: {},
|
|
422
|
+
globals: {},
|
|
423
|
+
id: "1",
|
|
424
|
+
kind: "Badge",
|
|
425
|
+
name: "Default",
|
|
426
|
+
viewMode: "story",
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const decorated = withCard((args) => <Badge {...args} />, context)
|
|
430
|
+
const unmount = mount(decorated, canvas)
|
|
431
|
+
|
|
432
|
+
expect(canvas.querySelector(".card")).toBeTruthy()
|
|
433
|
+
expect(canvas.querySelector("h3")!.textContent).toBe("Decorated")
|
|
434
|
+
expect(canvas.querySelector(".badge")!.textContent).toBe("Info")
|
|
435
|
+
unmount()
|
|
436
|
+
canvas.remove()
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
// ─── Component with no args ─────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
describe("component with no args", () => {
|
|
443
|
+
it("renders a component without any props via renderToCanvas", () => {
|
|
444
|
+
function Logo() {
|
|
445
|
+
return <img src="/logo.png" alt="Logo" />
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const canvas = createCanvas()
|
|
449
|
+
renderToCanvas(
|
|
450
|
+
makeRenderContext({
|
|
451
|
+
storyFn: () => <Logo />,
|
|
452
|
+
args: {},
|
|
453
|
+
}),
|
|
454
|
+
canvas,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
expect(canvas.querySelector("img")).toBeTruthy()
|
|
458
|
+
expect(canvas.querySelector("img")!.getAttribute("alt")).toBe("Logo")
|
|
459
|
+
canvas.remove()
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it("renders a component with no args via defaultRender", () => {
|
|
463
|
+
function Divider() {
|
|
464
|
+
return <hr class="divider" />
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const canvas = createCanvas()
|
|
468
|
+
const vnode = defaultRender(Divider, {})
|
|
469
|
+
const unmount = mount(vnode, canvas)
|
|
470
|
+
|
|
471
|
+
expect(canvas.querySelector("hr")).toBeTruthy()
|
|
472
|
+
expect(canvas.querySelector(".divider")).toBeTruthy()
|
|
473
|
+
unmount()
|
|
474
|
+
canvas.remove()
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it("preview render works with empty args", () => {
|
|
478
|
+
function Spinner() {
|
|
479
|
+
return <div class="spinner">Loading...</div>
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const canvas = createCanvas()
|
|
483
|
+
const vnode = previewRender({}, { component: Spinner })
|
|
484
|
+
const unmount = mount(vnode, canvas)
|
|
485
|
+
|
|
486
|
+
expect(canvas.querySelector(".spinner")!.textContent).toBe("Loading...")
|
|
487
|
+
unmount()
|
|
488
|
+
canvas.remove()
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
// ─── Error handling ──────────────────────────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
describe("error handling", () => {
|
|
495
|
+
it("showError is called when storyFn itself throws", () => {
|
|
496
|
+
const canvas = createCanvas()
|
|
497
|
+
let errorShown: { title: string; description: string } | null = null
|
|
498
|
+
|
|
499
|
+
renderToCanvas(
|
|
500
|
+
{
|
|
501
|
+
storyFn: () => {
|
|
502
|
+
throw new Error("storyFn exploded")
|
|
503
|
+
},
|
|
504
|
+
storyContext: { args: {} },
|
|
505
|
+
showMain: () => {
|
|
506
|
+
/* noop */
|
|
507
|
+
},
|
|
508
|
+
showError: (err: { title: string; description: string }) => {
|
|
509
|
+
errorShown = err
|
|
510
|
+
},
|
|
511
|
+
forceRemount: false,
|
|
512
|
+
},
|
|
513
|
+
canvas,
|
|
514
|
+
)
|
|
413
515
|
|
|
414
|
-
|
|
415
|
-
expect(()
|
|
416
|
-
|
|
417
|
-
|
|
516
|
+
expect(errorShown).not.toBeNull()
|
|
517
|
+
expect(errorShown!.description).toBe("storyFn exploded")
|
|
518
|
+
canvas.remove()
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
it("showError receives string-coerced non-Error throws", () => {
|
|
522
|
+
const canvas = createCanvas()
|
|
523
|
+
let errorShown: { title: string; description: string } | null = null
|
|
524
|
+
|
|
525
|
+
renderToCanvas(
|
|
526
|
+
{
|
|
527
|
+
storyFn: () => {
|
|
528
|
+
throw "raw string error"
|
|
529
|
+
},
|
|
530
|
+
storyContext: { args: {} },
|
|
531
|
+
showMain: () => {
|
|
532
|
+
/* noop */
|
|
533
|
+
},
|
|
534
|
+
showError: (err: { title: string; description: string }) => {
|
|
535
|
+
errorShown = err
|
|
536
|
+
},
|
|
537
|
+
forceRemount: false,
|
|
538
|
+
},
|
|
539
|
+
canvas,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
expect(errorShown).not.toBeNull()
|
|
543
|
+
expect(errorShown!.description).toBe("raw string error")
|
|
544
|
+
canvas.remove()
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
it("component that throws during setup is handled by the framework", () => {
|
|
548
|
+
const canvas = createCanvas()
|
|
549
|
+
|
|
550
|
+
function Broken(): never {
|
|
551
|
+
throw new Error("Component exploded")
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Pyreon's mount catches component setup errors internally,
|
|
555
|
+
// so renderToCanvas proceeds normally (showMain is called)
|
|
556
|
+
let mainShown = false
|
|
557
|
+
renderToCanvas(
|
|
558
|
+
{
|
|
559
|
+
storyFn: () => <Broken />,
|
|
560
|
+
storyContext: { args: {} },
|
|
561
|
+
showMain: () => {
|
|
562
|
+
mainShown = true
|
|
563
|
+
},
|
|
564
|
+
showError: () => {
|
|
565
|
+
/* noop */
|
|
566
|
+
},
|
|
567
|
+
forceRemount: false,
|
|
568
|
+
},
|
|
569
|
+
canvas,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
// mount() catches the component error internally — renderToCanvas's
|
|
573
|
+
// try/catch does not see it, so showMain is called
|
|
574
|
+
expect(mainShown).toBe(true)
|
|
575
|
+
canvas.remove()
|
|
576
|
+
})
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
// ─── Re-render cleanup ──────────────────────────────────────────────────────
|
|
580
|
+
|
|
581
|
+
describe("re-render cleanup", () => {
|
|
582
|
+
it("calling renderToCanvas twice cleans up previous mount", () => {
|
|
583
|
+
const canvas = createCanvas()
|
|
584
|
+
|
|
585
|
+
renderToCanvas(makeRenderContext({ storyFn: () => <div class="first">First</div> }), canvas)
|
|
586
|
+
expect(canvas.querySelector(".first")).toBeTruthy()
|
|
587
|
+
|
|
588
|
+
renderToCanvas(makeRenderContext({ storyFn: () => <div class="second">Second</div> }), canvas)
|
|
589
|
+
|
|
590
|
+
// Previous content should be gone
|
|
591
|
+
expect(canvas.querySelector(".first")).toBeNull()
|
|
592
|
+
expect(canvas.querySelector(".second")).toBeTruthy()
|
|
593
|
+
expect(canvas.textContent).toBe("Second")
|
|
594
|
+
canvas.remove()
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
it("re-render disposes effects from previous story", () => {
|
|
598
|
+
const canvas = createCanvas()
|
|
599
|
+
let effectCount = 0
|
|
600
|
+
|
|
601
|
+
const sig = signal(0)
|
|
602
|
+
function ReactiveStory() {
|
|
603
|
+
effect(() => {
|
|
604
|
+
sig()
|
|
605
|
+
effectCount++
|
|
606
|
+
})
|
|
607
|
+
return <div>Reactive</div>
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
renderToCanvas(makeRenderContext({ storyFn: () => <ReactiveStory /> }), canvas)
|
|
611
|
+
|
|
612
|
+
const afterFirst = effectCount
|
|
613
|
+
sig.set(1)
|
|
614
|
+
expect(effectCount).toBe(afterFirst + 1)
|
|
615
|
+
|
|
616
|
+
// Re-render with a different story
|
|
617
|
+
renderToCanvas(makeRenderContext({ storyFn: () => <div>Static</div> }), canvas)
|
|
618
|
+
|
|
619
|
+
const afterSecond = effectCount
|
|
620
|
+
sig.set(2)
|
|
621
|
+
sig.set(3)
|
|
622
|
+
// Old effect should be disposed — count should not increase
|
|
623
|
+
expect(effectCount).toBe(afterSecond)
|
|
624
|
+
canvas.remove()
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
it("showMain is called on successful render", () => {
|
|
628
|
+
const canvas = createCanvas()
|
|
629
|
+
let mainShown = false
|
|
630
|
+
|
|
631
|
+
renderToCanvas(
|
|
632
|
+
{
|
|
633
|
+
storyFn: () => <div>OK</div>,
|
|
634
|
+
storyContext: { args: {} },
|
|
635
|
+
showMain: () => {
|
|
636
|
+
mainShown = true
|
|
637
|
+
},
|
|
638
|
+
showError: () => {
|
|
639
|
+
/* noop */
|
|
640
|
+
},
|
|
641
|
+
forceRemount: false,
|
|
642
|
+
},
|
|
643
|
+
canvas,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
expect(mainShown).toBe(true)
|
|
647
|
+
canvas.remove()
|
|
648
|
+
})
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
// ─── Missing component in context ───────────────────────────────────────────
|
|
652
|
+
|
|
653
|
+
describe("missing component in context", () => {
|
|
654
|
+
it("preview render throws when context has no component", () => {
|
|
655
|
+
expect(() => previewRender({ value: 1 }, {})).toThrow(
|
|
656
|
+
"[@pyreon/storybook] No component provided",
|
|
657
|
+
)
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
it("preview render throws when context.component is null", () => {
|
|
661
|
+
expect(() => previewRender({ value: 1 }, { component: null } as any)).toThrow(
|
|
662
|
+
"[@pyreon/storybook] No component provided",
|
|
663
|
+
)
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
it("renderToCanvas works without component in storyContext when storyFn is provided", () => {
|
|
667
|
+
const canvas = createCanvas()
|
|
668
|
+
|
|
669
|
+
renderToCanvas(
|
|
670
|
+
makeRenderContext({
|
|
671
|
+
storyFn: () => <div>No component needed</div>,
|
|
672
|
+
}),
|
|
673
|
+
canvas,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
expect(canvas.textContent).toBe("No component needed")
|
|
677
|
+
canvas.remove()
|
|
418
678
|
})
|
|
419
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,13 +26,10 @@ 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
|
-
export type StoryFn<TArgs = Props> = (
|
|
33
|
-
args: TArgs,
|
|
34
|
-
context: StoryContext<TArgs>,
|
|
35
|
-
) => VNodeChild
|
|
32
|
+
export type StoryFn<TArgs = Props> = (args: TArgs, context: StoryContext<TArgs>) => VNodeChild
|
|
36
33
|
|
|
37
34
|
export type DecoratorFn<TArgs = Props> = (
|
|
38
35
|
storyFn: StoryFn<TArgs>,
|
|
@@ -84,10 +81,7 @@ export interface StoryObj<TMeta extends Meta<any> = Meta> {
|
|
|
84
81
|
/** Tags for this story. */
|
|
85
82
|
tags?: string[]
|
|
86
83
|
/** Override the render function for this story. */
|
|
87
|
-
render?: (
|
|
88
|
-
args: MetaArgs<TMeta>,
|
|
89
|
-
context: StoryContext<MetaArgs<TMeta>>,
|
|
90
|
-
) => VNodeChild
|
|
84
|
+
render?: (args: MetaArgs<TMeta>, context: StoryContext<MetaArgs<TMeta>>) => VNodeChild
|
|
91
85
|
/** Story name override. */
|
|
92
86
|
name?: string
|
|
93
87
|
/** Play function for interaction tests. */
|