@neuralfog/elemix-storybook 0.0.1 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 brownhounds
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,19 +1,94 @@
1
- ## Notes
1
+ # ⚡ Elemix Storybook
2
+
3
+ Storybook integration for [Elemix](https://github.com/neuralfog/elemix). Renders elemix templates directly inside Storybook's web-components framework via a decorator, and provides typed helpers (`ElemixMeta`, `ElemixStory`) for writing stories with full TypeScript support.
4
+
5
+ ## Why?
6
+
7
+ `@storybook/web-components-vite` ships with a lit-html based renderer. Elemix uses its own renderer (`@neuralfog/elemix-renderer`) and a custom-element lifecycle that lit-html can't drive. This package bridges the two — Storybook keeps managing the UI shell, args panel, addons, and HMR, while elemix owns what actually mounts into the story canvas.
2
8
 
3
- bash
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install --save-dev @neuralfog/elemix-storybook
4
13
  ```
5
- npx storybook@latest init --type web_components
14
+
15
+ ## Setup
16
+
17
+ Register the decorator in `.storybook/preview.ts`:
18
+
19
+ ```typescript
20
+ import type { Preview } from '@storybook/web-components-vite';
21
+ import { elemixDecorator } from '@neuralfog/elemix-storybook';
22
+
23
+ const preview: Preview = {
24
+ decorators: [elemixDecorator],
25
+ parameters: {
26
+ controls: {
27
+ matchers: {
28
+ color: /(background|color)$/i,
29
+ date: /Date$/i,
30
+ },
31
+ },
32
+ },
33
+ };
34
+
35
+ export default preview;
36
+ ```
37
+
38
+ The decorator clears `#storybook-root`, mounts a fresh `<div data-elemix-root>` host, and pipes the story's returned template through `render()` from `@neuralfog/elemix-renderer`.
39
+
40
+ ## Writing a Story
41
+
42
+ ```typescript
43
+ import { html } from '@neuralfog/elemix';
44
+ import type { ElemixMeta, ElemixStory } from '@neuralfog/elemix-storybook';
45
+
46
+ type HelloArgs = { text: string };
47
+
48
+ const meta: ElemixMeta<HelloArgs> = {
49
+ title: 'Test/Hello',
50
+ args: { text: 'There' },
51
+ argTypes: { text: { control: 'text' } },
52
+ };
53
+
54
+ export default meta;
55
+
56
+ export const Default: ElemixStory<HelloArgs> = {
57
+ render: (args) => html`<div>Hello ${args.text}</div>`,
58
+ };
59
+ ```
60
+
61
+ `ElemixStory.render` returns an elemix `HtmlTemplate` directly — no need to wrap or unwrap. The decorator picks it up and renders it via elemix-renderer.
62
+
63
+ ## Per-Story Hooks
64
+
65
+ Stories can opt into setup / teardown / render hooks via `parameters.elemix`:
66
+
67
+ ```typescript
68
+ const meta: ElemixMeta<HelloArgs> = {
69
+ title: 'Test/Hello',
70
+ parameters: {
71
+ elemix: {
72
+ setup: (ctx) => {
73
+ return () => {};
74
+ },
75
+ beforeRender: (ctx) => {},
76
+ afterRender: (ctx) => {},
77
+ },
78
+ },
79
+ };
6
80
  ```
7
81
 
8
- Storybook wizard is ass, so have to create `tsconfig.json` first in order to bootstrap typescript project.
82
+ | Hook | When it runs |
83
+ |---|---|
84
+ | `setup` | Once per story id, before the first render. May return a teardown function. |
85
+ | `beforeRender` | Before every render (initial + every args change). |
86
+ | `afterRender` | After every render. |
9
87
 
10
- ## TODO
88
+ ## Notes
89
+
90
+ ```bash
91
+ npx storybook@latest init --type web_components
92
+ ```
11
93
 
12
- - [x] This was easy just works
13
- - [] Cant have globals in stories it will mess up concurrent runs of stories:
14
- - [] Application context is static class, needs changing
15
- - [] Application context inject in stories somehow
16
- - [] Signals are also global states that may be used by multiple components
17
- - [] Need to find a way to stub signals per story, this is not easy, it is
18
- but ergonomics matter, at moment signal are injected at the decorator stage :thinking:
19
- - [] Write helpers in testing package to pierce shadow dom and wait for element
94
+ The Storybook wizard does not bootstrap TypeScript on its own, so create `tsconfig.json` before running init.
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});var R={},d={},O;function se(){if(O)return d;O=1;const m="₥";var u=(o=>(o[o.EVENT=0]="EVENT",o[o.PROP=1]="PROP",o[o.MODEL=2]="MODEL",o[o.STD=3]="STD",o[o.REF=4]="REF",o[o.EMIT=5]="EMIT",o[o.BIND_ATTRS=6]="BIND_ATTRS",o[o.BIND_EVENTS=7]="BIND_EVENTS",o[o.DIRECT_CLASS=8]="DIRECT_CLASS",o))(u||{});const p=o=>{const f=new RegExp(`${m}(\\d+)`),g=o.match(f);if(!g)throw new Error("Unable to extract index from hole comment");return Number(g[1])},c=o=>`<!--${m}${o}-->`,l=o=>o.replace(/<([a-zA-Z][^\s/>]*)([\s\S]*?)\/>/g,(f,g,S)=>g.includes("-")?`<${g}${S}></${g}>`:f),T=o=>o.replace(/(\S+)=((<!--[\s\S]*?-->)|([^\s">]+))/g,'$1="$2"'),y=o=>o.replace(/([A-Z])/g,f=>"-"+f.toLowerCase()),N=(o,f)=>Array.from(new Set([...o.split(" "),...f.split(" ")].filter(Boolean))).join(" ").trim();return d.Attributes=u,d.TEMPLATE_MARKER_GLYPH=m,d.camelToKebab=y,d.fixAttributeQuotes=T,d.fixSelfClosingTags=l,d.getIndexFromComment=p,d.makeMarkerComment=c,d.mergeClasses=N,d}var $;function ie(){return $||($=1,(function(m){var u=Object.defineProperty,p=(r,e,s)=>e in r?u(r,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):r[e]=s,c=(r,e,s)=>p(r,typeof e!="symbol"?e+"":e,s);Object.defineProperty(m,Symbol.toStringTag,{value:"Module"});const l=se(),T=r=>{const e=r;return e.$cache||(e.$cache={template:new Map}),e.$cache};class y{constructor(e,s){c(this,"key",""),this.strings=e,this.values=s}}class N{constructor(e,s){c(this,"initialClass"),this.node=e,this.definition=s,this.initialClass=e.getAttribute("class")||""}setValue(e){if(e==null||typeof e!="object")return;const{node:s}=this,t=e;for(const[i,n]of Object.entries(t)){const a=l.camelToKebab(i);if(n==null||n===!1){s.removeAttribute(a),this.initialClass.length&&s.setAttribute("class",String(this.initialClass));continue}if(a==="class"){s.setAttribute("class",l.mergeClasses(this.initialClass,String(n)));continue}s.setAttribute(a,String(n))}}}class o{constructor(e,s){this.node=e,this.definition=s}setValue(e){if(e!=null&&typeof e=="object")for(const[s,t]of Object.entries(e))this.node[`on${s}`]=t}}class f{constructor(e,s){c(this,"initialClass"),this.node=e,this.definition=s,this.initialClass=e.getAttribute("class")||""}setValue(e){const{node:s}=this;if(e==null&&this.initialClass.length&&s.setAttribute("class",String(this.initialClass)),typeof e=="string"&&s.setAttribute("class",l.mergeClasses(this.initialClass,String(e))),typeof e=="object"){const t=Object.entries(e).filter(([,i])=>!!i).map(([i])=>i).join(" ");s.setAttribute("class",l.mergeClasses(this.initialClass,t))}}}class g{constructor(e,s){this.node=e,this.definition=s}setValue(e){if(e===void 0)return;const{name:s}=this.definition,t=this.node;t.$emits&&t.$emits.set(s.slice(s.indexOf(":")+1),e)}}class S{constructor(e,s){this.node=e,this.definition=s}setValue(e){if(e===void 0)return;const s=this.definition.name.slice(1);this.node[`on${s}`]=e}}class _{constructor(e,s){this.node=e,this.definition=s}setValue(e){if(e===void 0)return;const s=t=>{e.value=t.target.value};this.node.oninput||(this.node.value=e.value,this.node.oninput=s)}}class j{constructor(e,s){this.node=e,this.definition=s}setValue(e){const s=this.definition.name.slice(1),t=this.node;t.$props&&t.$props.set(s,e)}}class P{constructor(e,s){this.node=e,this.definition=s}setValue(e){e!==void 0&&(e.value=this.node)}}let x=class{constructor(r,e){this.node=r,this.definition=e}setValue(r){r!==void 0&&this.node.setAttribute(this.definition.name,String(r))}};const F=(r,e)=>{const s=/(\S+)(?==(?:["']?)$)/,t=r.match(s);if(t){const i={index:e,name:t[1],value:l.makeMarkerComment(e),virtual:!1,type:l.Attributes.STD};switch(t[1][0]){case"@":return t[1].startsWith("@emits:")?(i.type=l.Attributes.EMIT,i.virtual=!0,i):(i.type=l.Attributes.EVENT,i.virtual=!0,i);case":":return t[1].endsWith(":ref")?(i.type=l.Attributes.REF,i.virtual=!0,i):(i.type=l.Attributes.PROP,i.virtual=!0,i);case"~":return t[1].startsWith("~model")&&(i.type=l.Attributes.MODEL,i.virtual=!0),i;case".":return t[1].startsWith(".bind-attrs")?(i.type=l.Attributes.BIND_ATTRS,i.virtual=!0,i):t[1].startsWith(".bind-events")?(i.type=l.Attributes.BIND_EVENTS,i.virtual=!0,i):(t[1].startsWith(".class")&&(i.type=l.Attributes.DIRECT_CLASS,i.virtual=!0),i);default:return i}}},K=(r,e)=>{const s=r.querySelector(W(e.name,e.value));if(s)switch(e.virtual&&s.removeAttribute(e.name),e.type){case l.Attributes.EVENT:return new S(s,e);case l.Attributes.PROP:return new j(s,e);case l.Attributes.MODEL:return new _(s,e);case l.Attributes.REF:return new P(s,e);case l.Attributes.EMIT:return new g(s,e);case l.Attributes.BIND_ATTRS:return new N(s,e);case l.Attributes.DIRECT_CLASS:return new f(s,e);case l.Attributes.BIND_EVENTS:return new o(s,e);default:return new x(s,e)}},B=r=>{let e="";for(let s=0;s<r.length;s++){const t=r.charAt(s),i=t.charCodeAt(0);i>=48&&i<=57||i>=65&&i<=90||i>=97&&i<=122||t==="-"||t==="_"?e+=t:e+=`\\${t}`}return e},W=(r,e)=>`[${B(r)}='${e}']`,q=(r,e)=>{const s=r.length,t=e.length,i=Object.create(null);let n,a,h;for(n=0;n<s;n++)a=r[n].key,i[a]=n;const b=new Array(t),v=[],A=[],k=Object.create(null);for(n=0;n<t;n++){const E=e[n].key;k[E]=!0;const M=i[E];M===void 0?b[n]=-1:(b[n]=M,v.push(M),A.push(n))}const L=G(v),C=new Array(t);for(n=0;n<t;n++)C[n]=!1;const te=L.length;for(n=0;n<te;n++)C[A[L[n]]]=!0;const I=[],D=[],V=[];for(n=0;n<s;n++)h=r[n].key,k[h]!==!0&&I.push({key:h});for(n=0;n<t;n++){h=e[n].key;const E=n+1<t?e[n+1].key:void 0;b[n]===-1?D.push({key:h,value:e[n],beforeKey:E}):C[n]||V.push({key:h,beforeKey:E})}return{deletes:I,inserts:D,moves:V}},G=r=>{const e=r.length,s=new Array(e),t=[];let i,n,a,h;for(i=0;i<e;i++){for(n=0,a=t.length;n<a;)h=n+a>>>1,r[t[h]]<r[i]?n=h+1:a=h;n===t.length?t.push(i):t[n]=i,s[i]=n>0?t[n-1]:-1}const b=t.length,v=new Array(b);let A=t[b-1];for(i=b-1;i>=0;i--)v[i]=A,A=s[A];return v};class Q{constructor(e){c(this,"cache",{listTemplate:new Map,listNodes:new Map,listHtmlTemplate:[]}),this.commentNode=e}renderListElement(e,s){if(!this.commentNode)throw new Error("renderList method needs to accept instance of HTMLElement");if(!e.key)throw new Error("use repeat directive when rendering the lists");let t=this.cache.listTemplate.get(e.key);return t||(t=new w(e),this.cache.listTemplate.set(e.key,t),t.mountListElement(this.commentNode,e.key,e.values,this.cache,s)),t}renderAllItems(e){const s=e.length;for(let t=0;t<s;t++)this.renderListElement(e[t]).update(e[t].values)}emptyList(){for(const[,e]of this.cache.listNodes)e.remove();this.cache.listTemplate.clear(),this.cache.listNodes.clear()}deleteNodes(e){const s=e.length;for(let t=s-1;t>=0;t--){const i=this.cache.listNodes.get(e[t].key);i&&i.remove(),this.cache.listNodes.delete(e[t].key),this.cache.listTemplate.delete(e[t].key)}}moveNodes(e){const s=e.length;for(let t=s-1;t>=0;t--){const i=this.cache.listNodes.get(e[t].key),n=this.cache.listNodes.get(e[t].beforeKey);i&&n&&n?.before(i),!n&&i&&this.commentNode.before(i)}}insertNodes(e){const s=e.length;for(let t=s-1;t>=0;t--){const i=this.cache.listNodes.get(e[t].beforeKey);this.renderListElement(e[t].value,i)}}updateAllItems(e){const s=e.length;for(let t=0;t<s;t++){const i=this.cache.listTemplate.get(e[t].key);i?.update(e[t].values)}}render(e){if(!this.cache.listHtmlTemplate.length){this.renderAllItems(e),this.cache.listHtmlTemplate=e;return}if(!e.length){this.emptyList(),this.cache.listHtmlTemplate=e;return}const{deletes:s,inserts:t,moves:i}=q(this.cache.listHtmlTemplate,e);if(s.length===e.length||t.length===e.length){this.emptyList(),this.renderAllItems(e),this.cache.listHtmlTemplate=e;return}s.length&&this.deleteNodes(s),i.length&&this.moveNodes(i),t.length&&this.insertNodes(t),this.updateAllItems(e),this.cache.listHtmlTemplate=e}}class U{constructor(e){c(this,"renderer"),this.commentNode=e,this.renderer=new Q(e)}setValue(e){this.renderer.render(e)}}class Y{constructor(e){c(this,"node",document.createTextNode("")),this.commentNode=e,e.before(this.node)}setValue(e){const s=e!=null?String(e):"";this.node.textContent!==s&&(this.node.textContent=s)}}class z{constructor(e){c(this,"cache",{nodes:[]}),this.commentNode=e}removeNodes(){if(!this.cache.nodes.length)return;const e=this.cache.nodes.length;for(let s=0;s<e;s++)this.cache.nodes[s].remove();this.cache.nodes=[]}render(e){this.cache.strings!==e.strings&&(this.cache.fragment=void 0,this.removeNodes()),this.cache.fragment||(this.cache.fragment=new w(e),this.cache.strings=e.strings,this.cache.nodes=this.cache.fragment.mountTemplate(this.commentNode,e.values)),this.cache.fragment.update(e.values)}}class Z{constructor(e){c(this,"renderer"),this.commentNode=e,this.renderer=new z(e)}setValue(e){this.renderer.render(e)}}const J=(r,e)=>Array.isArray(r)?new U(e):r instanceof y?new Z(e):new Y(e);class w{constructor(e){c(this,"holes",new Map),c(this,"htmlString",""),c(this,"attributeMap",[]),this.parse(e.strings)}parse(e){const s=e.length;for(let t=0;t<s;t++)if(this.htmlString+=e[t],t<e.length-1){const i=F(this.htmlString,t);i&&this.attributeMap.push(i),this.htmlString+=l.makeMarkerComment(t)}this.htmlString=l.fixSelfClosingTags(this.htmlString),this.htmlString=l.fixAttributeQuotes(this.htmlString)}initFragment(){const e=document.createElement("template");return e.innerHTML=this.htmlString,e.content}hydrateAttributes(e){const s=this.attributeMap.length;for(let t=0;t<s;t++){const i=K(e,this.attributeMap[t]);i&&this.holes.set(this.attributeMap[t].index,i)}return e}hydrateTemplateHoles(e,s){var t;const i=document.createTreeWalker(e,NodeFilter.SHOW_COMMENT,null);for(;i.nextNode();){const n=i.currentNode;if((t=n.nodeValue)!=null&&t.includes(l.TEMPLATE_MARKER_GLYPH)){const a=l.getIndexFromComment(n.nodeValue),h=J(s[a],n);this.holes.set(a,h)}}return e}mount(e,s){const t=this.initFragment();this.hydrateTemplateHoles(t,s),this.hydrateAttributes(t),e.appendChild(t)}mountTemplate(e,s){const t=this.initFragment();this.hydrateTemplateHoles(t,s),this.hydrateAttributes(t);const i=Array.from(t.childNodes);return e.before(t),i}mountListElement(e,s,t,i,n){const a=this.initFragment();this.hydrateTemplateHoles(a,t),this.hydrateAttributes(a),n?n.before(a):e.before(a);const h=n?n.previousSibling:e.previousSibling;h&&i&&i.listNodes.set(s,h)}update(e){for(const[s,t]of this.holes)t.setValue(e[s])}}const X=(r,...e)=>new y(r,e),ee=(r,e)=>{if(!e)throw new Error("render method needs to accept instance of HTMLElement");const s=T(e);let t=s.template.get(r.strings);t||(t=new w(r),s.template.set(r.strings,t),t.mount(e,r.values)),t.update(r.values)};m.html=X,m.render=ee})(R)),R}var ne=ie();const H=new Map,re=(m,u)=>{const p=u.parameters?.elemix??{};if(p.setup&&!H.has(u.id)){const y=p.setup(u);H.set(u.id,y)}const c=document.createElement("div");c.setAttribute("data-elemix-root","");const l=document.getElementById("storybook-root")??document.body;l.innerHTML="",l.appendChild(c),p.beforeRender?.(u);const T=m(u);return ne.render(T,c),p.afterRender?.(u),c};exports.elemixDecorator=re;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@neuralfog/elemix/render");var t=new Map,n=(n,r)=>{let i=r.parameters?.elemix??{};if(i.setup&&!t.has(r.id)){let e=i.setup(r);t.set(r.id,e)}let a=document.createElement(`div`);a.setAttribute(`data-elemix-root`,``);let o=document.getElementById(`storybook-root`)??document.body;return o.innerHTML=``,o.appendChild(a),i.beforeRender?.(r),(0,e.render)(n(r),a),i.afterRender?.(r),a};exports.elemixDecorator=n;
@@ -1,4 +1,4 @@
1
- import type { HtmlTemplate } from '@neuralfog/elemix-renderer';
1
+ import type { HtmlTemplate } from '@neuralfog/elemix/render';
2
2
  import type { StoryContext, Parameters, Meta } from '@storybook/web-components-vite';
3
3
  export type ElemixTeardown = () => void;
4
4
  export type ElemixParams<TArgs = Record<string, never>> = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuralfog/elemix-storybook",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "author": "brownhounds",
6
6
  "main": "dist/index.js",
@@ -18,27 +18,29 @@
18
18
  "build-storybook": "storybook build",
19
19
  "lint": "tsc --noEmit && biome format && biome lint",
20
20
  "lint:fix": "biome format --write . && biome lint --write . && tsc --noEmit",
21
- "test": "vitest",
21
+ "test": "vitest run",
22
22
  "test:watch": "vitest --watch",
23
23
  "test:coverage": "vitest run --coverage",
24
24
  "release": "npm run clean && npm run build && npm publish --access public"
25
25
  },
26
+ "peerDependencies": {
27
+ "@neuralfog/elemix": "0.5.0"
28
+ },
26
29
  "devDependencies": {
27
- "@chromatic-com/storybook": "5.0.0",
30
+ "@chromatic-com/storybook": "5.2.1",
28
31
  "@neuralfog/biome-config": "0.1.2",
29
- "@neuralfog/elemix": "0.1.8",
30
- "@neuralfog/elemix-renderer": "0.1.8",
32
+ "@neuralfog/elemix": "0.5.0",
31
33
  "@neuralfog/ts-config": "0.1.2",
32
- "@storybook/addon-a11y": "10.2.1",
33
- "@storybook/addon-docs": "10.2.1",
34
- "@storybook/addon-vitest": "10.2.1",
35
- "@storybook/web-components-vite": "10.2.1",
36
- "@types/node": "25.1.0",
37
- "@vitest/browser-playwright": "4.0.18",
38
- "@vitest/coverage-v8": "4.0.18",
39
- "playwright": "1.58.0",
40
- "storybook": "10.2.1",
41
- "typescript": "5.9.3",
42
- "vitest": "4.0.18"
34
+ "@storybook/addon-a11y": "10.4.1",
35
+ "@storybook/addon-docs": "10.4.1",
36
+ "@storybook/addon-vitest": "10.4.1",
37
+ "@storybook/web-components-vite": "10.4.1",
38
+ "@types/node": "25.9.1",
39
+ "@vitest/browser-playwright": "4.1.8",
40
+ "@vitest/coverage-v8": "4.1.8",
41
+ "playwright": "1.60.0",
42
+ "storybook": "10.4.1",
43
+ "typescript": "6.0.3",
44
+ "vitest": "4.1.8"
43
45
  }
44
46
  }