@weasel-js/forge 1.5.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 +21 -0
- package/README.md +33 -0
- package/dist/FrameController-Ba3LaWfq.d.ts +196 -0
- package/dist/chunk-2GXKNXTJ.js +15 -0
- package/dist/chunk-2GXKNXTJ.js.map +1 -0
- package/dist/chunk-IOBG7GZQ.js +13 -0
- package/dist/chunk-IOBG7GZQ.js.map +1 -0
- package/dist/chunk-KKF7ZX5E.js +572 -0
- package/dist/chunk-KKF7ZX5E.js.map +1 -0
- package/dist/chunk-KKVTFMWV.js +59 -0
- package/dist/chunk-KKVTFMWV.js.map +1 -0
- package/dist/chunk-O5OQZQRZ.js +118 -0
- package/dist/chunk-O5OQZQRZ.js.map +1 -0
- package/dist/chunk-UZCXH724.js +24 -0
- package/dist/chunk-UZCXH724.js.map +1 -0
- package/dist/cli.js +107 -0
- package/dist/config-DdumOfyd.d.ts +28 -0
- package/dist/csf/preview-api.d.ts +9 -0
- package/dist/csf/preview-api.js +48 -0
- package/dist/csf/preview-api.js.map +1 -0
- package/dist/frame/index.css +35 -0
- package/dist/frame/index.css.map +1 -0
- package/dist/frame/index.d.ts +24 -0
- package/dist/frame/index.js +8 -0
- package/dist/frame/index.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/shell/index.css +198 -0
- package/dist/shell/index.css.map +1 -0
- package/dist/shell/index.d.ts +51 -0
- package/dist/shell/index.js +836 -0
- package/dist/shell/index.js.map +1 -0
- package/dist/test/index.d.ts +17 -0
- package/dist/test/index.js +132 -0
- package/dist/test/index.js.map +1 -0
- package/dist/vite/index.d.ts +29 -0
- package/dist/vite/index.js +392 -0
- package/dist/vite/index.js.map +1 -0
- package/package.json +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
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
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @weasel-js/forge
|
|
2
|
+
|
|
3
|
+
A component workshop built on labkit. It reads Storybook's story files and
|
|
4
|
+
renders each story in its own frame document; the story's controls, state,
|
|
5
|
+
undo and snapshots live in a labkit trial beside it, so two settings of one
|
|
6
|
+
component can be compared side by side.
|
|
7
|
+
|
|
8
|
+
Direction: `docs/superpowers/specs/2026-09-13-forge-design.md`.
|
|
9
|
+
|
|
10
|
+
## Config modules
|
|
11
|
+
|
|
12
|
+
The vite plugin takes two optional config modules, one per realm, because the
|
|
13
|
+
workshop page and each frame document are separate documents:
|
|
14
|
+
|
|
15
|
+
- `frameConfig` default-exports `defineFrameConfig({...})`: decorators around
|
|
16
|
+
every story, `applyGlobals` for what a global does to the frame document,
|
|
17
|
+
`cssVarsScope`, and `parameters`, the project-wide CSF parameters that
|
|
18
|
+
Storybook keeps in `preview`. Only frame documents import it, so its CSS
|
|
19
|
+
stays off the workshop page.
|
|
20
|
+
- `shellConfig` default-exports `defineShellConfig({...})`: lab chrome, control
|
|
21
|
+
renderers, and global declarations. Only the workshop page imports it.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
forge({ stories: ['src/**/*.stories.tsx'], frameConfig: 'forge.frame.tsx', shellConfig: 'forge.shell.tsx' });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Story tests
|
|
28
|
+
|
|
29
|
+
`forgeTest` from `@weasel-js/forge/vite` runs every story as a vitest browser
|
|
30
|
+
test: each story renders through forge's frame with its defaults, plays, and
|
|
31
|
+
fails on any fault. Pass it the same `frameConfig`. It needs vitest 4 in browser mode with a provider, such as
|
|
32
|
+
`@vitest/browser-playwright`. This repo runs it with
|
|
33
|
+
`npm run test:stories:forge`, not in CI.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { BranchAnnotations, Annotations, ConfigSchema } from '@weasel-js/labkit/config';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface Channel<In, Out> {
|
|
5
|
+
send(msg: Out): void;
|
|
6
|
+
on(fn: (msg: In) => void): () => void;
|
|
7
|
+
close(): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SectionDescription {
|
|
11
|
+
label: string;
|
|
12
|
+
collapsed?: boolean;
|
|
13
|
+
}
|
|
14
|
+
type DescribedEntry = {
|
|
15
|
+
branch: true;
|
|
16
|
+
annotations: BranchAnnotations;
|
|
17
|
+
section?: SectionDescription;
|
|
18
|
+
conditional: boolean;
|
|
19
|
+
children: DescribedShape;
|
|
20
|
+
} | {
|
|
21
|
+
branch: false;
|
|
22
|
+
kind: string | null;
|
|
23
|
+
default: unknown;
|
|
24
|
+
annotations: Annotations;
|
|
25
|
+
section?: SectionDescription;
|
|
26
|
+
conditional: boolean;
|
|
27
|
+
validated: boolean;
|
|
28
|
+
/** The node draws its own row with `.render`, which stays in the frame. */
|
|
29
|
+
customControl: boolean;
|
|
30
|
+
};
|
|
31
|
+
type DescribedShape = {
|
|
32
|
+
[key: string]: DescribedEntry;
|
|
33
|
+
};
|
|
34
|
+
/** A story's `f.schema` as structured-clone data. */
|
|
35
|
+
interface SchemaDescription {
|
|
36
|
+
nodes: DescribedShape;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type Globals = Readonly<Record<string, unknown>>;
|
|
40
|
+
type Layout = 'centered' | 'padded' | 'fullscreen';
|
|
41
|
+
interface Viewport {
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
}
|
|
45
|
+
type FaultPhase = 'import' | 'render' | 'play' | 'protocol';
|
|
46
|
+
/** One custom property as the frame found it. */
|
|
47
|
+
interface CssVarReport {
|
|
48
|
+
name: string;
|
|
49
|
+
value: string;
|
|
50
|
+
/** Whether a `vars.set` override is in force on it. */
|
|
51
|
+
overridden: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** Visibility and validation answers, keyed by the config they were computed for. */
|
|
54
|
+
interface ConfigAnswers {
|
|
55
|
+
/** `stableStringify(config)` of the config the frame evaluated. */
|
|
56
|
+
configKey: string;
|
|
57
|
+
hidden: string[];
|
|
58
|
+
errors: Record<string, string[]>;
|
|
59
|
+
}
|
|
60
|
+
type ToFrame = {
|
|
61
|
+
type: 'init';
|
|
62
|
+
config: unknown;
|
|
63
|
+
state: unknown;
|
|
64
|
+
globals: Globals;
|
|
65
|
+
} | {
|
|
66
|
+
type: 'config';
|
|
67
|
+
config: unknown;
|
|
68
|
+
} | {
|
|
69
|
+
type: 'state';
|
|
70
|
+
state: unknown;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'globals';
|
|
73
|
+
globals: Globals;
|
|
74
|
+
} | {
|
|
75
|
+
type: 'vars.set';
|
|
76
|
+
name: string;
|
|
77
|
+
value: string | null;
|
|
78
|
+
} | {
|
|
79
|
+
type: 'play';
|
|
80
|
+
};
|
|
81
|
+
type FromFrame = {
|
|
82
|
+
type: 'ready';
|
|
83
|
+
schema: SchemaDescription;
|
|
84
|
+
layout: Layout;
|
|
85
|
+
viewport: Viewport | null;
|
|
86
|
+
} | {
|
|
87
|
+
type: 'answers';
|
|
88
|
+
answers: ConfigAnswers;
|
|
89
|
+
} | {
|
|
90
|
+
type: 'setConfig';
|
|
91
|
+
path: string;
|
|
92
|
+
value: unknown;
|
|
93
|
+
} | {
|
|
94
|
+
type: 'setState';
|
|
95
|
+
state: unknown;
|
|
96
|
+
} | {
|
|
97
|
+
type: 'size';
|
|
98
|
+
width: number;
|
|
99
|
+
height: number;
|
|
100
|
+
} | {
|
|
101
|
+
type: 'vars';
|
|
102
|
+
vars: CssVarReport[];
|
|
103
|
+
} | {
|
|
104
|
+
type: 'played';
|
|
105
|
+
ok: boolean;
|
|
106
|
+
message?: string;
|
|
107
|
+
}
|
|
108
|
+
/** `seq`, on a render fault only: how many `init`/`config`/`state`/`globals` messages the frame had received. */
|
|
109
|
+
| {
|
|
110
|
+
type: 'fault';
|
|
111
|
+
phase: FaultPhase;
|
|
112
|
+
message: string;
|
|
113
|
+
stack?: string;
|
|
114
|
+
seq?: number;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
interface StoryContext<C = unknown, S = unknown> {
|
|
118
|
+
config: C;
|
|
119
|
+
setConfig: (path: string, value: unknown) => void;
|
|
120
|
+
state: S;
|
|
121
|
+
setState: (next: S | ((prev: S) => S)) => void;
|
|
122
|
+
globals: Globals;
|
|
123
|
+
title: string;
|
|
124
|
+
name: string;
|
|
125
|
+
}
|
|
126
|
+
interface PlayContext<C = unknown> {
|
|
127
|
+
canvasElement: HTMLElement;
|
|
128
|
+
config: C;
|
|
129
|
+
globals: Globals;
|
|
130
|
+
}
|
|
131
|
+
type Decorator = (story: () => ReactNode, ctx: StoryContext) => ReactNode;
|
|
132
|
+
interface MetaSpec {
|
|
133
|
+
title?: string;
|
|
134
|
+
decorators?: Decorator[];
|
|
135
|
+
layout?: Layout;
|
|
136
|
+
}
|
|
137
|
+
interface StorySpec<C = Record<string, never>, S = undefined> {
|
|
138
|
+
name?: string;
|
|
139
|
+
config?: ConfigSchema<C>;
|
|
140
|
+
state?: (config: C) => S;
|
|
141
|
+
render: (ctx: StoryContext<C, S>) => ReactNode;
|
|
142
|
+
decorators?: Decorator[];
|
|
143
|
+
layout?: Layout;
|
|
144
|
+
viewport?: Viewport;
|
|
145
|
+
play?: (ctx: PlayContext<C>) => void | Promise<void>;
|
|
146
|
+
}
|
|
147
|
+
/** One story, normalized — what both the native and CSF loaders produce. */
|
|
148
|
+
interface LoadedStory {
|
|
149
|
+
id: string;
|
|
150
|
+
title: string;
|
|
151
|
+
name: string;
|
|
152
|
+
exportName: string;
|
|
153
|
+
config: ConfigSchema<unknown>;
|
|
154
|
+
initialState: ((config: unknown) => unknown) | null;
|
|
155
|
+
render: (ctx: StoryContext) => ReactNode;
|
|
156
|
+
/** Innermost first: the story's own, then the meta's. Global decorators are added by the frame. */
|
|
157
|
+
decorators: Decorator[];
|
|
158
|
+
layout: Layout;
|
|
159
|
+
viewport: Viewport | null;
|
|
160
|
+
play: ((ctx: PlayContext) => void | Promise<void>) | null;
|
|
161
|
+
}
|
|
162
|
+
/** One story as the index knows it, before its module is loaded. */
|
|
163
|
+
interface IndexEntry {
|
|
164
|
+
id: string;
|
|
165
|
+
title: string;
|
|
166
|
+
name: string;
|
|
167
|
+
exportName: string;
|
|
168
|
+
/** Absolute path of the story file. */
|
|
169
|
+
file: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface FrameSetup {
|
|
173
|
+
/** Wraps every story, outermost; receives the globals. From the frame config (`frameConfig`). */
|
|
174
|
+
decorators?: Decorator[];
|
|
175
|
+
/** Applies globals to the frame document — theme mode, fonts. */
|
|
176
|
+
applyGlobals?: (globals: Globals, root: HTMLElement) => void;
|
|
177
|
+
/**
|
|
178
|
+
* The elements a CSS variable override is declared on; `:root` by default. An override has to be declared on
|
|
179
|
+
* whichever element declares the token itself — a theme wrapper, say — or that element's own value wins.
|
|
180
|
+
*/
|
|
181
|
+
cssVarsScope?: string;
|
|
182
|
+
/** Project-wide CSF parameters, Storybook's preview `parameters`: under each meta's and story's. */
|
|
183
|
+
parameters?: Record<string, unknown>;
|
|
184
|
+
}
|
|
185
|
+
interface StartFrameOptions {
|
|
186
|
+
story: LoadedStory;
|
|
187
|
+
channel: Channel<ToFrame, FromFrame>;
|
|
188
|
+
container: HTMLElement;
|
|
189
|
+
setup?: FrameSetup;
|
|
190
|
+
}
|
|
191
|
+
/** Sends `ready`, then renders the story on `init` and keeps it in step. Returns a stop function. */
|
|
192
|
+
declare function startFrame({ story, channel, container, setup }: StartFrameOptions): () => void;
|
|
193
|
+
/** Reports an import failure — there is no story to start. */
|
|
194
|
+
declare function reportImportFault(channel: Channel<ToFrame, FromFrame>, error: unknown): void;
|
|
195
|
+
|
|
196
|
+
export { type Decorator as D, type FrameSetup as F, type IndexEntry as I, type LoadedStory as L, type MetaSpec as M, type PlayContext as P, type StorySpec as S, type StoryContext as a, type StartFrameOptions as b, type FromFrame as c, reportImportFault as r, startFrame as s };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/story/define.ts
|
|
2
|
+
var NATIVE = /* @__PURE__ */ Symbol.for("weaselforge.native");
|
|
3
|
+
function meta(spec) {
|
|
4
|
+
return Object.assign({}, spec, { [NATIVE]: "meta" });
|
|
5
|
+
}
|
|
6
|
+
function story(spec) {
|
|
7
|
+
return Object.assign({}, spec, { [NATIVE]: "story" });
|
|
8
|
+
}
|
|
9
|
+
function isNative(value, kind) {
|
|
10
|
+
return !!value && typeof value === "object" && value[NATIVE] === kind;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { isNative, meta, story };
|
|
14
|
+
//# sourceMappingURL=chunk-2GXKNXTJ.js.map
|
|
15
|
+
//# sourceMappingURL=chunk-2GXKNXTJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/story/define.ts"],"names":[],"mappings":";AAEO,IAAM,MAAA,mBAAS,MAAA,CAAO,GAAA,CAAI,oBAAoB,CAAA;AAE9C,SAAS,KAAK,IAAA,EAA0B;AAC7C,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,EAAC,EAAG,IAAA,EAAM,EAAE,CAAC,MAAM,GAAG,MAAA,EAAiB,CAAA;AAC9D;AAEO,SAAS,MAAgD,IAAA,EAAwC;AACtG,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,EAAC,EAAG,IAAA,EAAM,EAAE,CAAC,MAAM,GAAG,OAAA,EAAkB,CAAA;AAC/D;AAEO,SAAS,QAAA,CAAS,OAAgB,IAAA,EAAiC;AACxE,EAAA,OAAO,CAAC,CAAC,KAAA,IAAS,OAAO,UAAU,QAAA,IAAa,KAAA,CAAkC,MAAM,CAAA,KAAM,IAAA;AAChG","file":"chunk-2GXKNXTJ.js","sourcesContent":["import type { MetaSpec, StorySpec } from './types';\n\nexport const NATIVE = Symbol.for('weaselforge.native');\n\nexport function meta(spec: MetaSpec): MetaSpec {\n return Object.assign({}, spec, { [NATIVE]: 'meta' as const });\n}\n\nexport function story<C = Record<string, never>, S = undefined>(spec: StorySpec<C, S>): StorySpec<C, S> {\n return Object.assign({}, spec, { [NATIVE]: 'story' as const });\n}\n\nexport function isNative(value: unknown, kind: 'meta' | 'story'): boolean {\n return !!value && typeof value === 'object' && (value as Record<symbol, unknown>)[NATIVE] === kind;\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/protocol/messages.ts
|
|
2
|
+
var PROTOCOL_VERSION = 1;
|
|
3
|
+
var PORT_HANDOFF = "weaselforge:port";
|
|
4
|
+
function stableStringify(value) {
|
|
5
|
+
return JSON.stringify(
|
|
6
|
+
value,
|
|
7
|
+
(_key, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a < b ? -1 : 1)) : v
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { PORT_HANDOFF, PROTOCOL_VERSION, stableStringify };
|
|
12
|
+
//# sourceMappingURL=chunk-IOBG7GZQ.js.map
|
|
13
|
+
//# sourceMappingURL=chunk-IOBG7GZQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/protocol/messages.ts"],"names":[],"mappings":";AAEO,IAAM,gBAAA,GAAmB;AAmDzB,IAAM,YAAA,GAAe;AAGrB,SAAS,gBAAgB,KAAA,EAAwB;AACtD,EAAA,OAAO,IAAA,CAAK,SAAA;AAAA,IAAU,KAAA;AAAA,IAAO,CAAC,IAAA,EAAM,CAAA,KAClC,CAAA,IAAK,OAAO,CAAA,KAAM,QAAA,IAAY,CAAC,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA,GAC1C,OAAO,WAAA,CAAY,MAAA,CAAO,OAAA,CAAQ,CAAC,CAAA,CAAE,IAAA,CAAK,CAAC,CAAC,CAAC,CAAA,EAAG,CAAC,CAAC,CAAA,KAAO,CAAA,GAAI,CAAA,GAAI,EAAA,GAAK,CAAE,CAAC,CAAA,GACzE;AAAA,GACN;AACF","file":"chunk-IOBG7GZQ.js","sourcesContent":["import type { SchemaDescription } from './schema';\n\nexport const PROTOCOL_VERSION = 1;\n\nexport type Globals = Readonly<Record<string, unknown>>;\nexport type Layout = 'centered' | 'padded' | 'fullscreen';\nexport interface Viewport {\n width: number;\n height: number;\n}\nexport type FaultPhase = 'import' | 'render' | 'play' | 'protocol';\n\n/** One custom property as the frame found it. */\nexport interface CssVarReport {\n name: string;\n value: string;\n /** Whether a `vars.set` override is in force on it. */\n overridden: boolean;\n}\n\n/** Visibility and validation answers, keyed by the config they were computed for. */\nexport interface ConfigAnswers {\n /** `stableStringify(config)` of the config the frame evaluated. */\n configKey: string;\n hidden: string[];\n errors: Record<string, string[]>;\n}\n\nexport type ToFrame =\n | { type: 'init'; config: unknown; state: unknown; globals: Globals }\n | { type: 'config'; config: unknown }\n | { type: 'state'; state: unknown }\n | { type: 'globals'; globals: Globals }\n | { type: 'vars.set'; name: string; value: string | null }\n | { type: 'play' };\n\nexport type FromFrame =\n | { type: 'ready'; schema: SchemaDescription; layout: Layout; viewport: Viewport | null }\n | { type: 'answers'; answers: ConfigAnswers }\n | { type: 'setConfig'; path: string; value: unknown }\n | { type: 'setState'; state: unknown }\n | { type: 'size'; width: number; height: number }\n | { type: 'vars'; vars: CssVarReport[] }\n | { type: 'played'; ok: boolean; message?: string }\n /** `seq`, on a render fault only: how many `init`/`config`/`state`/`globals` messages the frame had received. */\n | { type: 'fault'; phase: FaultPhase; message: string; stack?: string; seq?: number };\n\nexport interface Envelope<M> {\n v: number;\n msg: M;\n}\n\n/** Posted on the frame's window with the port transferred alongside it. */\nexport const PORT_HANDOFF = 'weaselforge:port';\n\n/** JSON with object keys sorted, so two equal configs produce one key. */\nexport function stableStringify(value: unknown): string {\n return JSON.stringify(value, (_key, v) =>\n v && typeof v === 'object' && !Array.isArray(v)\n ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => (a < b ? -1 : 1)))\n : v,\n );\n}\n"]}
|