@weasel-js/forge 1.5.2 → 1.6.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.
Files changed (39) hide show
  1. package/README.md +72 -10
  2. package/dist/{FrameController-eZM3hlcE.d.ts → FrameController-BspC9Zr5.d.ts} +61 -4
  3. package/dist/chunk-A4JOEGYQ.js +325 -0
  4. package/dist/chunk-A4JOEGYQ.js.map +1 -0
  5. package/dist/{chunk-5YHEO26U.js → chunk-FWPBKKYB.js} +58 -240
  6. package/dist/chunk-FWPBKKYB.js.map +1 -0
  7. package/dist/{chunk-6SRQIHJQ.js → chunk-HRGPFQBW.js} +3 -3
  8. package/dist/{chunk-6SRQIHJQ.js.map → chunk-HRGPFQBW.js.map} +1 -1
  9. package/dist/chunk-PO5JLVYT.js +318 -0
  10. package/dist/chunk-PO5JLVYT.js.map +1 -0
  11. package/dist/{chunk-TLO6UHF2.js → chunk-VEKEBILL.js} +17 -4
  12. package/dist/chunk-VEKEBILL.js.map +1 -0
  13. package/dist/{config-CSVP6duU.d.ts → config-DU_WuORS.d.ts} +7 -1
  14. package/dist/csf/preview-api.js +1 -2
  15. package/dist/csf/preview-api.js.map +1 -1
  16. package/dist/frame/index.css +102 -0
  17. package/dist/frame/index.css.map +1 -1
  18. package/dist/frame/index.d.ts +11 -3
  19. package/dist/frame/index.js +365 -5
  20. package/dist/frame/index.js.map +1 -1
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/shell/index.css +96 -19
  24. package/dist/shell/index.css.map +1 -1
  25. package/dist/shell/index.d.ts +75 -15
  26. package/dist/shell/index.js +762 -214
  27. package/dist/shell/index.js.map +1 -1
  28. package/dist/test/index.d.ts +3 -3
  29. package/dist/test/index.js +67 -72
  30. package/dist/test/index.js.map +1 -1
  31. package/dist/vite/index.js +43 -5
  32. package/dist/vite/index.js.map +1 -1
  33. package/package.json +5 -4
  34. package/dist/chunk-2JDYOBWM.js +0 -118
  35. package/dist/chunk-2JDYOBWM.js.map +0 -1
  36. package/dist/chunk-5YHEO26U.js.map +0 -1
  37. package/dist/chunk-FTFR2YPM.js +0 -13
  38. package/dist/chunk-FTFR2YPM.js.map +0 -1
  39. package/dist/chunk-TLO6UHF2.js.map +0 -1
package/README.md CHANGED
@@ -1,22 +1,53 @@
1
1
  # @weasel-js/forge
2
2
 
3
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
4
+ renders each story in the workshop page as a labkit trial: the story's
5
+ controls, state, undo and snapshots live in the trial, so two settings of one
6
6
  component can be compared side by side.
7
7
 
8
8
  Direction: `docs/superpowers/specs/2026-09-13-forge-design.md`.
9
9
 
10
+ ## Where a story renders
11
+
12
+ A story renders inside a host element in the workshop document. The host
13
+ carries the theme attributes the globals resolve to, keeps its CSS variable
14
+ overrides on itself, contains its fixed-position descendants, and is where
15
+ weasel overlays portal, so a Dialog opened in one trial stays in that trial.
16
+ `vw`, `vh` and media queries read the workshop page, not the host; a story
17
+ with a `viewport` renders as a fixed-size box the trial pans and zooms.
18
+
19
+ A story that needs a document of its own says so, with the reason:
20
+
21
+ ```tsx
22
+ export const Interactive = story({ isolate: 'ToastRegion portals to document.body', render: … });
23
+ // CSF: parameters: { forge: { isolate: 'ToastRegion portals to document.body' } }
24
+ ```
25
+
26
+ It then renders in an iframe, as every story once did. The frame path is kept
27
+ for those stories and gets no new features; `npm run check:forge-isolate`
28
+ lists the isolated stories and fails when their number goes above the one the
29
+ script holds, so the count only goes down. The value is read from the source
30
+ without running it, and must be a string literal.
31
+
32
+ A story may stay isolated as long as it needs to. If the count ever reaches
33
+ zero, the frame side goes: `FrameView`, `framePool`, `trialFrames`,
34
+ `answers`, `readyKey`, `labHarness`, `mountFrame`, `protocol/*`, the frame
35
+ entry, `frame.html`, `html.ts`, the frame page in `build.ts`, the `./frame`
36
+ and `./frame.css` exports, the ratchet script, and the `docs/TODO.md` entries
37
+ about frame reloads; `defineFrameConfig` folds into one config.
38
+
10
39
  ## Config modules
11
40
 
12
- The vite plugin takes two optional config modules, one per realm, because the
13
- workshop page and each frame document are separate documents:
41
+ The vite plugin takes two optional config modules:
14
42
 
15
43
  - `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.
44
+ every story, `applyGlobals` for what a global does to a story's host (or to
45
+ a frame document's root, for an isolated story), `cssVarsScope`,
46
+ `parameters`, the project-wide CSF parameters that Storybook keeps in
47
+ `preview`, and `prepare`, awaited before a story first renders, which is
48
+ where to import what only some stories need. `applyGlobals` is handed a
49
+ target: its `root`, a `scope` selector matching only that root, and
50
+ `style(css)` for a rule that should reach that story alone.
20
51
  - `shellConfig` default-exports `defineShellConfig({...})`: lab chrome, control
21
52
  renderers, and global declarations. Only the workshop page imports it.
22
53
 
@@ -24,10 +55,41 @@ workshop page and each frame document are separate documents:
24
55
  forge({ stories: ['src/**/*.stories.tsx'], frameConfig: 'forge.frame.tsx', shellConfig: 'forge.shell.tsx' });
25
56
  ```
26
57
 
58
+ Editing a story file reloads that story in every trial showing it, with the
59
+ trial's config and state kept; the page itself stays.
60
+
61
+ Clicking a story in the tree, or reaching one by its URL, shows it in the
62
+ focused trial; Shift-click opens another trial beside it.
63
+
64
+ ## Index pages
65
+
66
+ Every component — every story title — has an index page, opened by clicking
67
+ the component's row in the sidebar (its fold mark only folds it) and routed at
68
+ `#/<title id>:index`. It renders all
69
+ of the component's stories in one trial: each at its defaults, then once per
70
+ value of each of its boolean and enum controls, one control at a time.
71
+
72
+ A component can supply its own page. A native meta takes `index`; a CSF meta
73
+ sets `parameters.forge.index`. Either is handed an `IndexContext` holding the
74
+ stories and the generated page's parts, `Story`, `Variants` and `DefaultIndex`,
75
+ so a custom page composes them:
76
+
77
+ ```tsx
78
+ export default meta({
79
+ title: 'ui/Button',
80
+ index: ({ stories, Story, DefaultIndex }) => (
81
+ <>
82
+ <Story story={stories[0]} config={{ label: 'Save' }} label="In a form" />
83
+ <DefaultIndex />
84
+ </>
85
+ ),
86
+ });
87
+ ```
88
+
27
89
  ## Story tests
28
90
 
29
91
  `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
92
+ test: each story renders in the test page through the same host the workshop
93
+ uses, with its defaults, plays, and fails on any fault. Pass it the same `frameConfig`. It needs vitest 4 in browser mode with a provider, such as
32
94
  `@vitest/browser-playwright`. This repo runs it with
33
95
  `npm run test:stories:forge`, not in CI.
@@ -1,5 +1,5 @@
1
1
  import { BranchAnnotations, Annotations, ConfigSchema } from '@weasel-js/labkit/config';
2
- import { ReactNode } from 'react';
2
+ import { ReactNode, ComponentType } from 'react';
3
3
 
4
4
  interface Channel<In, Out> {
5
5
  send(msg: Out): void;
@@ -147,6 +147,11 @@ type FromFrame = {
147
147
  } | {
148
148
  type: 'vars';
149
149
  vars: CssVarReport[];
150
+ }
151
+ /** Asks the shell to show story `id` in this frame's trial in place of what it shows now. */
152
+ | {
153
+ type: 'open';
154
+ id: string;
150
155
  } | {
151
156
  type: 'played';
152
157
  ok: boolean;
@@ -200,7 +205,38 @@ interface MetaSpec {
200
205
  title?: string;
201
206
  decorators?: Decorator[];
202
207
  layout?: Layout;
208
+ /** Renders this story in its own frame document, for a reason the value states. The default is the workshop document. */
209
+ isolate?: string;
210
+ /** The component's own index page, in place of the generated one. A CSF file sets `parameters.forge.index`. */
211
+ index?: IndexRender;
212
+ }
213
+ /** What an index page is given: its component's stories, and the parts the generated page is built from. */
214
+ interface IndexContext {
215
+ title: string;
216
+ /** The JSDoc above the file's meta, if any. */
217
+ description?: string;
218
+ /** In file order. */
219
+ stories: readonly LoadedStory[];
220
+ globals: Globals;
221
+ /** One story, interactive, at its defaults with `config` over them. */
222
+ Story: ComponentType<IndexStoryProps>;
223
+ /** One story rendered once per value of each of its boolean and enum controls, the rest at their defaults. */
224
+ Variants: ComponentType<{
225
+ story: LoadedStory;
226
+ }>;
227
+ /** The whole generated page. */
228
+ DefaultIndex: ComponentType;
229
+ /** Shows story `id` in this page's trial in its place. */
230
+ open: (id: string) => void;
203
231
  }
232
+ interface IndexStoryProps {
233
+ story: LoadedStory;
234
+ /** Merged over the story's defaults; nested groups merge rather than replace. */
235
+ config?: Record<string, unknown>;
236
+ /** Shown under the story. */
237
+ label?: string;
238
+ }
239
+ type IndexRender = (ctx: IndexContext) => ReactNode;
204
240
  interface StorySpec<C = Record<string, never>, S = undefined> {
205
241
  name?: string;
206
242
  config?: ConfigSchema<C>;
@@ -209,6 +245,8 @@ interface StorySpec<C = Record<string, never>, S = undefined> {
209
245
  decorators?: Decorator[];
210
246
  layout?: Layout;
211
247
  viewport?: Viewport;
248
+ /** Renders this story in its own frame document, for a reason the value states. The default is the workshop document. */
249
+ isolate?: string;
212
250
  play?: (ctx: PlayContext<C>) => void | Promise<void>;
213
251
  }
214
252
  /** One story, normalized — what both the native and CSF loaders produce. */
@@ -224,6 +262,7 @@ interface LoadedStory {
224
262
  decorators: Decorator[];
225
263
  layout: Layout;
226
264
  viewport: Viewport | null;
265
+ isolate: string | null;
227
266
  play: ((ctx: PlayContext) => void | Promise<void>) | null;
228
267
  }
229
268
  /** One story as the index knows it, before its module is loaded. */
@@ -240,13 +279,26 @@ interface IndexEntry {
240
279
  componentDescription?: string;
241
280
  /** The identifier the meta's `component` names, if it names one. */
242
281
  componentName?: string;
282
+ /** Read statically from the source; a story with it renders in its own frame. */
283
+ isolate?: string;
284
+ }
285
+
286
+ /** What a setup's `applyGlobals` is handed: the element the globals apply to, and a way to style it alone. */
287
+ interface GlobalsTarget {
288
+ /** A frame document's root, or one story's host in the workshop document. */
289
+ root: HTMLElement;
290
+ /** A selector matching `root` and nothing else, so a rule written under it reaches only this target. */
291
+ scope: string;
292
+ /** Replaces the CSS forge keeps for this target. */
293
+ style: (css: string) => void;
243
294
  }
244
295
 
245
296
  interface FrameSetup {
246
297
  /** Wraps every story, outermost; receives the globals. From the frame config (`frameConfig`). */
247
298
  decorators?: Decorator[];
248
- /** Applies globals to the frame document — theme mode, fonts. */
249
- applyGlobals?: (globals: Globals, root: HTMLElement) => void;
299
+ /** Applies globals to a target: theme mode, fonts. The target is a frame document's root, or one story's host in
300
+ * the workshop document, so a rule the setup writes goes through `target.style` under `target.scope`. */
301
+ applyGlobals?: (globals: Globals, target: GlobalsTarget) => void;
250
302
  /**
251
303
  * The elements a CSS variable override is declared on; `:root` by default. An override has to be declared on
252
304
  * whichever element declares the token itself — a theme wrapper, say — or that element's own value wins.
@@ -254,6 +306,11 @@ interface FrameSetup {
254
306
  cssVarsScope?: string;
255
307
  /** Project-wide CSF parameters, Storybook's preview `parameters`: under each meta's and story's. */
256
308
  parameters?: Record<string, unknown>;
309
+ /**
310
+ * Awaited once a story is loaded and before it first renders — the place to import what only some stories
311
+ * need, so the rest never load it.
312
+ */
313
+ prepare?: (story: LoadedStory) => void | Promise<void>;
257
314
  }
258
315
  interface StartFrameOptions {
259
316
  story: LoadedStory;
@@ -266,4 +323,4 @@ declare function startFrame({ story, channel, container, setup }: StartFrameOpti
266
323
  /** Reports an import failure — there is no story to start. */
267
324
  declare function reportImportFault(channel: Channel<ToFrame, FromFrame>, error: unknown): void;
268
325
 
269
- export { type A11yFinding as A, type CssVarReport as C, type Decorator as D, type FrameSetup as F, type Globals as G, type IndexEntry as I, type LoadedStory as L, type MetaSpec as M, type PlayContext as P, type StorySpec as S, type ToFrame as T, type Viewport as V, type A11yNode as a, type A11yReport as b, type StoryContext as c, type StartFrameOptions as d, type FromFrame as e, type CapturedPicture as f, reportImportFault as r, startFrame as s };
326
+ export { type A11yFinding as A, type CssVarReport as C, type Decorator as D, type FrameSetup as F, type Globals as G, type IndexContext as I, type LoadedStory as L, type MetaSpec as M, type PlayContext as P, type StorySpec as S, type ToFrame as T, type Viewport as V, type A11yNode as a, type A11yReport as b, type GlobalsTarget as c, type IndexRender as d, type IndexStoryProps as e, type StoryContext as f, type IndexEntry as g, type Layout as h, type FromFrame as i, type CapturedPicture as j, type StartFrameOptions as k, reportImportFault as r, startFrame as s };
@@ -0,0 +1,325 @@
1
+ import { createOverrides, createGlobalsTarget, StoryHost, scanCssVars, runAxe, captureElement } from './chunk-FWPBKKYB.js';
2
+ import { createContext, useContext, useCallback, useSyncExternalStore, useMemo, useId, useState, useRef, useLayoutEffect, useEffect } from 'react';
3
+ import { usePersistedState, TrialIdContext, LabBoundary } from '@weasel-js/labkit';
4
+ import { OverlayPortalProvider } from '@weasel-js/ui';
5
+ import { f } from '@weasel-js/labkit/config';
6
+ import { jsx } from 'react/jsx-runtime';
7
+
8
+ var NO_FRAME = { send: null, vars: [], audit: null, a11y: null, capture: null, size: null };
9
+ function createTrialFrames() {
10
+ const frames = /* @__PURE__ */ new Map();
11
+ const hosts = /* @__PURE__ */ new Map();
12
+ const listeners = /* @__PURE__ */ new Set();
13
+ const changed = () => {
14
+ for (const listener of [...listeners]) listener();
15
+ };
16
+ const patch = (trialId, next) => {
17
+ const frame = frames.get(trialId);
18
+ if (!frame) return;
19
+ frames.set(trialId, { ...frame, ...next });
20
+ changed();
21
+ };
22
+ return {
23
+ connect(trialId, calls) {
24
+ const held = frames.get(trialId);
25
+ frames.set(trialId, {
26
+ send: calls.send,
27
+ vars: [],
28
+ audit: calls.audit,
29
+ a11y: null,
30
+ capture: calls.capture,
31
+ size: held?.size ?? null
32
+ });
33
+ changed();
34
+ return () => {
35
+ if (frames.get(trialId)?.send !== calls.send) return;
36
+ frames.delete(trialId);
37
+ changed();
38
+ };
39
+ },
40
+ report: (trialId, vars) => patch(trialId, { vars }),
41
+ reportA11y: (trialId, a11y) => patch(trialId, { a11y }),
42
+ reportSize(trialId, size) {
43
+ const frame = frames.get(trialId) ?? NO_FRAME;
44
+ if (frame.size?.width === size.width && frame.size?.height === size.height) return;
45
+ frames.set(trialId, { ...frame, size });
46
+ changed();
47
+ },
48
+ hostRef(trialId) {
49
+ let ref = hosts.get(trialId);
50
+ if (!ref) {
51
+ ref = { current: null };
52
+ hosts.set(trialId, ref);
53
+ }
54
+ return ref;
55
+ },
56
+ get: (trialId) => frames.get(trialId) ?? NO_FRAME,
57
+ subscribe(listener) {
58
+ listeners.add(listener);
59
+ return () => listeners.delete(listener);
60
+ }
61
+ };
62
+ }
63
+ var TrialFramesContext = createContext(null);
64
+ function useTrialFrame(trialId) {
65
+ const frames = useContext(TrialFramesContext);
66
+ const subscribe = useCallback((listener) => frames?.subscribe(listener) ?? (() => {
67
+ }), [frames]);
68
+ const read = () => frames?.get(trialId) ?? NO_FRAME;
69
+ return useSyncExternalStore(subscribe, read, read);
70
+ }
71
+ var StoryGlobalsContext = createContext({});
72
+ var NONE = {};
73
+ function useCssOverrides() {
74
+ return usePersistedState("fg-css-overrides", NONE, { scope: "trial" });
75
+ }
76
+ var GLOBALS_KEY = "$globals";
77
+ var FOLLOW_LAB = "lab";
78
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
79
+ var isGlobalsPath = (path) => path === GLOBALS_KEY || path.startsWith(`${GLOBALS_KEY}.`);
80
+ function labGlobals(declarations, stored) {
81
+ const from = isRecord(stored) ? stored : {};
82
+ return Object.fromEntries(
83
+ Object.entries(declarations).map(([key, declaration]) => {
84
+ const value = from[key];
85
+ return [key, declaration.options.some((option) => option.value === value) ? value : declaration.default];
86
+ })
87
+ );
88
+ }
89
+ function effectiveGlobals(lab, pins) {
90
+ if (!isRecord(pins)) return lab;
91
+ const pinned = Object.entries(pins).filter(([key, value]) => key in lab && value !== FOLLOW_LAB);
92
+ return pinned.length === 0 ? lab : { ...lab, ...Object.fromEntries(pinned) };
93
+ }
94
+ function storyConfig(config, previous) {
95
+ if (!isRecord(config) || !(GLOBALS_KEY in config)) return config;
96
+ const rest = Object.fromEntries(Object.entries(config).filter(([key]) => key !== GLOBALS_KEY));
97
+ if (isRecord(previous)) {
98
+ const keys = Object.keys(rest);
99
+ if (keys.length === Object.keys(previous).length && keys.every((key) => rest[key] === previous[key])) return previous;
100
+ }
101
+ return rest;
102
+ }
103
+ function withGlobals(schema, declarations) {
104
+ const entries = Object.entries(declarations);
105
+ if (entries.length === 0) return schema;
106
+ const leaves = Object.fromEntries(
107
+ entries.map(([key, declaration]) => [
108
+ key,
109
+ f.enum(FOLLOW_LAB, [{ value: FOLLOW_LAB, label: "Lab" }, ...declaration.options]).label(declaration.label)
110
+ ])
111
+ );
112
+ const group = f.group(leaves).label("").section("Globals", { layout: "inline", pack: "pairs" });
113
+ return f.schema({ ...schema.nodes, [GLOBALS_KEY]: group });
114
+ }
115
+ var IN_VIEW_MARGIN = "50%";
116
+ var VARS_SETTLE_MS = 100;
117
+ function useInView(ref) {
118
+ const [inView, setInView] = useState(true);
119
+ useEffect(() => {
120
+ const el = ref.current;
121
+ if (!el || typeof IntersectionObserver === "undefined") return;
122
+ const observer = new IntersectionObserver(
123
+ (entries) => {
124
+ const last = entries.at(-1);
125
+ if (last) setInView(last.isIntersecting);
126
+ },
127
+ { rootMargin: IN_VIEW_MARGIN }
128
+ );
129
+ observer.observe(el);
130
+ return () => observer.disconnect();
131
+ }, [ref]);
132
+ return inView;
133
+ }
134
+ var scopeOf = (id) => `[data-fg-host="${id}"]`;
135
+ function TrialHost({ layout, setup, config, children, hostRef, onRendered }) {
136
+ const trialId = useContext(TrialIdContext);
137
+ const frames = useContext(TrialFramesContext);
138
+ const labGlobals2 = useContext(StoryGlobalsContext);
139
+ const pins = config?.[GLOBALS_KEY];
140
+ const globals = useMemo(() => effectiveGlobals(labGlobals2, pins), [labGlobals2, pins]);
141
+ const [persisted] = useCssOverrides();
142
+ const reactId = useId();
143
+ const hostId = trialId ?? reactId;
144
+ const [host, setHost] = useState(null);
145
+ const elementRef = useRef(null);
146
+ const inView = useInView(elementRef);
147
+ const [pending, setPending] = useState(true);
148
+ const overridesRef = useRef(null);
149
+ const latest = useRef({ setup, onRendered });
150
+ latest.current = { setup, onRendered };
151
+ useLayoutEffect(() => {
152
+ if (!host) return;
153
+ const doc = host.ownerDocument;
154
+ const scope = scopeOf(hostId);
155
+ const { cssVarsScope } = latest.current.setup;
156
+ const overrides = createOverrides(doc, cssVarsScope ? `${scope}, ${scope} :is(${cssVarsScope})` : scope);
157
+ overridesRef.current = overrides;
158
+ let lastVars = null;
159
+ let reported = null;
160
+ const publish = (vars) => {
161
+ lastVars = vars;
162
+ const key = JSON.stringify(vars);
163
+ if (key === reported) return;
164
+ reported = key;
165
+ if (trialId) frames?.report(trialId, vars);
166
+ };
167
+ const scan = () => publish(scanCssVars(doc, overrides.has, host));
168
+ let settleTimer;
169
+ const ownMutation = (record) => overrides.owns(record.target) || record.type === "childList" && [...record.addedNodes, ...record.removedNodes].length > 0 && [...record.addedNodes, ...record.removedNodes].every(overrides.owns);
170
+ const mutations = typeof MutationObserver === "undefined" ? null : new MutationObserver((records) => {
171
+ if (records.every(ownMutation)) return;
172
+ clearTimeout(settleTimer);
173
+ settleTimer = setTimeout(scan, VARS_SETTLE_MS);
174
+ });
175
+ mutations?.observe(host, { subtree: true, childList: true, characterData: true, attributes: true });
176
+ const send = (msg) => {
177
+ if (msg.type !== "vars.set") return;
178
+ overrides.set(msg.name, msg.value);
179
+ const at = lastVars?.findIndex((v) => v.name === msg.name) ?? -1;
180
+ if (!lastVars || at < 0) return;
181
+ const vars = lastVars.slice();
182
+ vars[at] = { ...vars[at], overridden: overrides.has(msg.name) };
183
+ scan();
184
+ };
185
+ const audit = async () => {
186
+ let outcome;
187
+ try {
188
+ outcome = { ok: true, report: await runAxe(host, { page: false }) };
189
+ } catch (error) {
190
+ outcome = { ok: false, message: error instanceof Error ? error.message : String(error) };
191
+ }
192
+ if (trialId) frames?.reportA11y(trialId, outcome);
193
+ return outcome;
194
+ };
195
+ const capture = async () => captureElement(host);
196
+ const disconnect = frames && trialId ? frames.connect(trialId, { send, audit, capture }) : () => {
197
+ };
198
+ if (frames && trialId) frames.hostRef(trialId).current = host;
199
+ const sizes = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
200
+ const { width, height } = host.getBoundingClientRect();
201
+ if (trialId) frames?.reportSize(trialId, { width: Math.ceil(width), height: Math.ceil(height) });
202
+ });
203
+ sizes?.observe(host);
204
+ return () => {
205
+ disconnect();
206
+ sizes?.disconnect();
207
+ mutations?.disconnect();
208
+ clearTimeout(settleTimer);
209
+ overrides.dispose();
210
+ overridesRef.current = null;
211
+ if (frames && trialId && frames.hostRef(trialId).current === host) frames.hostRef(trialId).current = null;
212
+ };
213
+ }, [host, hostId, trialId, frames]);
214
+ const applied = useRef({});
215
+ useLayoutEffect(() => {
216
+ const overrides = overridesRef.current;
217
+ if (!overrides) return;
218
+ for (const name of Object.keys(applied.current)) if (!(name in persisted)) overrides.set(name, null);
219
+ for (const [name, value] of Object.entries(persisted)) overrides.set(name, value);
220
+ applied.current = { ...persisted };
221
+ }, [host, persisted]);
222
+ useLayoutEffect(() => {
223
+ if (!host) return;
224
+ const target = createGlobalsTarget(host, scopeOf(hostId));
225
+ latest.current.setup.applyGlobals?.(globals, target);
226
+ return () => target.dispose();
227
+ }, [host, hostId, globals]);
228
+ const announced = useRef(false);
229
+ useEffect(() => {
230
+ if (!host || !inView || announced.current) return;
231
+ announced.current = true;
232
+ let canceled = false;
233
+ const done = () => {
234
+ if (canceled) return;
235
+ setPending(false);
236
+ latest.current.onRendered?.();
237
+ };
238
+ const { fonts } = host.ownerDocument;
239
+ if (!fonts) {
240
+ done();
241
+ return;
242
+ }
243
+ const afterPaint = () => requestAnimationFrame(done);
244
+ void fonts.ready.then(afterPaint, afterPaint);
245
+ return () => {
246
+ canceled = true;
247
+ };
248
+ }, [host, inView]);
249
+ return /* @__PURE__ */ jsx(
250
+ "div",
251
+ {
252
+ ref: (el) => {
253
+ elementRef.current = el;
254
+ if (hostRef) hostRef.current = el;
255
+ setHost(el);
256
+ },
257
+ className: "fg-story",
258
+ "data-fg-host": hostId,
259
+ "data-fg-layout": layout,
260
+ "data-pending": pending || void 0,
261
+ children: host && inView ? (
262
+ // The workshop is itself a lab; below the boundary a story sees no lab, trial or theme above it, as in a
263
+ // document of its own.
264
+ /* @__PURE__ */ jsx(LabBoundary, { children: /* @__PURE__ */ jsx(OverlayPortalProvider, { container: host, children: children(globals) }) })
265
+ ) : null
266
+ }
267
+ );
268
+ }
269
+ function StoryTrial({ story, setup, ctx, hostRef, onRendered, onError }) {
270
+ const latest = useRef(ctx);
271
+ latest.current = ctx;
272
+ const config = useMemo(() => storyConfig(ctx.config), [ctx.config]);
273
+ const seeded = useRef(false);
274
+ useEffect(() => {
275
+ if (seeded.current || ctx.state !== null || !story.initialState) return;
276
+ seeded.current = true;
277
+ ctx.setState(story.initialState(config));
278
+ }, [ctx, story, config]);
279
+ const input = useRef({ config, state: ctx.state, key: 0 });
280
+ if (input.current.config !== config || input.current.state !== ctx.state) {
281
+ input.current = { config, state: ctx.state, key: input.current.key + 1 };
282
+ }
283
+ const resetKey = input.current.key;
284
+ const setConfig = useCallback((path, value) => {
285
+ if (!isGlobalsPath(path)) latest.current.setConfig(path, value);
286
+ }, []);
287
+ const setState = useCallback((next) => latest.current.setState(next), []);
288
+ const decorators = setup.decorators ?? [];
289
+ const render = (globals) => {
290
+ const storyCtx = {
291
+ config,
292
+ setConfig,
293
+ state: ctx.state,
294
+ setState,
295
+ globals,
296
+ title: story.title,
297
+ name: story.name
298
+ };
299
+ return /* @__PURE__ */ jsx(
300
+ StoryHost,
301
+ {
302
+ story,
303
+ ctx: storyCtx,
304
+ decorators,
305
+ resetKey,
306
+ onError: (error) => onError?.(error)
307
+ }
308
+ );
309
+ };
310
+ return /* @__PURE__ */ jsx(
311
+ TrialHost,
312
+ {
313
+ layout: story.layout,
314
+ setup,
315
+ config: ctx.config,
316
+ ...hostRef ? { hostRef } : {},
317
+ ...onRendered ? { onRendered } : {},
318
+ children: render
319
+ }
320
+ );
321
+ }
322
+
323
+ export { GLOBALS_KEY, StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, createTrialFrames, effectiveGlobals, isGlobalsPath, labGlobals, storyConfig, useCssOverrides, useTrialFrame, withGlobals };
324
+ //# sourceMappingURL=chunk-A4JOEGYQ.js.map
325
+ //# sourceMappingURL=chunk-A4JOEGYQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shell/trialFrames.ts","../src/shell/StoryGlobalsContext.ts","../src/shell/cssVars/overrides.ts","../src/shell/globals.ts","../src/shell/TrialHost.tsx","../src/shell/StoryTrial.tsx"],"names":["createContext","useContext","labGlobals","useRef","useMemo","useEffect","useCallback","jsx"],"mappings":"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","file":"chunk-A4JOEGYQ.js","sourcesContent":["import { createContext, type RefObject, useCallback, useContext, useSyncExternalStore } from 'react';\nimport type { A11yReport, CapturedPicture, CssVarReport, ToFrame, Viewport } from '../protocol/messages';\n\ntype Send = (msg: ToFrame) => void;\n\n/** What an axe run in the frame came back with. */\nexport type A11yOutcome = { ok: true; report: A11yReport } | { ok: false; message: string };\n\n/** The half of a trial's frame that answers a request: each call is one round trip over the frame's channel. */\nexport interface TrialFrameCalls {\n send: Send;\n audit: () => Promise<A11yOutcome>;\n /** Rejects when the frame could not draw itself, or went away before it did. */\n capture: () => Promise<CapturedPicture>;\n}\n\n/** What trial chrome can reach of the frame its trial shows. */\nexport interface TrialFrame {\n /** Null while no frame is connected. */\n send: Send | null;\n /** The frame's latest `vars` report. */\n vars: readonly CssVarReport[];\n /** Runs axe over the story and records the answer as `a11y`. Null while no frame is connected. */\n audit: (() => Promise<A11yOutcome>) | null;\n /** What the last audit found; null until one has run against the connected frame. */\n a11y: A11yOutcome | null;\n /** The story's own picture, for an export to draw over. Null while no frame is connected. */\n capture: (() => Promise<CapturedPicture>) | null;\n /** The story's content box, as the frame last measured it; null until it has. */\n size: Viewport | null;\n}\n\n/** Each trial's frame, keyed by trial id: `FrameView` connects and reports, trial chrome reads. */\nexport interface TrialFrames {\n /** Returns a disconnect that does nothing once a newer connection replaced this one. */\n connect(trialId: string, calls: TrialFrameCalls): () => void;\n report(trialId: string, vars: readonly CssVarReport[]): void;\n reportA11y(trialId: string, outcome: A11yOutcome): void;\n reportSize(trialId: string, size: Viewport): void;\n /** The element a trial's frame is mounted in — stable across the frame reloads that replace the link,\n * because an annotation target's ref has to outlive them. */\n hostRef(trialId: string): RefObject<HTMLElement | null>;\n get(trialId: string): TrialFrame;\n subscribe(listener: () => void): () => void;\n}\n\nconst NO_FRAME: TrialFrame = { send: null, vars: [], audit: null, a11y: null, capture: null, size: null };\n\nexport function createTrialFrames(): TrialFrames {\n const frames = new Map<string, TrialFrame>();\n const hosts = new Map<string, RefObject<HTMLElement | null>>();\n const listeners = new Set<() => void>();\n const changed = () => {\n for (const listener of [...listeners]) listener();\n };\n const patch = (trialId: string, next: Partial<TrialFrame>) => {\n const frame = frames.get(trialId);\n if (!frame) return;\n frames.set(trialId, { ...frame, ...next });\n changed();\n };\n return {\n connect(trialId, calls) {\n const held = frames.get(trialId);\n frames.set(trialId, {\n send: calls.send,\n vars: [],\n audit: calls.audit,\n a11y: null,\n capture: calls.capture,\n size: held?.size ?? null,\n });\n changed();\n return () => {\n if (frames.get(trialId)?.send !== calls.send) return;\n frames.delete(trialId);\n changed();\n };\n },\n report: (trialId, vars) => patch(trialId, { vars }),\n reportA11y: (trialId, a11y) => patch(trialId, { a11y }),\n reportSize(trialId, size) {\n const frame = frames.get(trialId) ?? NO_FRAME;\n if (frame.size?.width === size.width && frame.size?.height === size.height) return;\n frames.set(trialId, { ...frame, size });\n changed();\n },\n hostRef(trialId) {\n let ref = hosts.get(trialId);\n if (!ref) {\n ref = { current: null };\n hosts.set(trialId, ref);\n }\n return ref;\n },\n get: (trialId) => frames.get(trialId) ?? NO_FRAME,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n}\n\nexport const TrialFramesContext = createContext<TrialFrames | null>(null);\n\n/** The frame of the trial `trialId` names, re-read when it connects or reports. */\nexport function useTrialFrame(trialId: string): TrialFrame {\n const frames = useContext(TrialFramesContext);\n const subscribe = useCallback((listener: () => void) => frames?.subscribe(listener) ?? (() => {}), [frames]);\n const read = () => frames?.get(trialId) ?? NO_FRAME;\n return useSyncExternalStore(subscribe, read, read);\n}\n","import { createContext } from 'react';\nimport type { Globals } from '../protocol/messages';\n\n/** The lab's global values, which a trial's pins override. A change reaches open frames without replacing instruments. */\nexport const StoryGlobalsContext = createContext<Globals>({});\n","import { usePersistedState } from '@weasel-js/labkit';\n\nexport type CssOverrides = Readonly<Record<string, string>>;\n\nconst NONE: CssOverrides = {};\n\n/** The CSS variable overrides of the trial this is rendered inside, persisted with it. */\nexport function useCssOverrides() {\n return usePersistedState<CssOverrides>('fg-css-overrides', NONE, { scope: 'trial' });\n}\n","import { type ConfigSchema, f } from '@weasel-js/labkit/config';\nimport type { Globals } from '../protocol/messages';\n\n/** One global the workshop's toolbar sets for every story, and a trial may pin for its own. */\nexport interface GlobalDeclaration {\n label: string;\n options: readonly { value: string; label: string }[];\n default: string;\n /** Another global's key: the toolbar shows this one in a popover beside that one rather than in the bar itself. */\n under?: string;\n}\n\nexport type GlobalDeclarations = Readonly<Record<string, GlobalDeclaration>>;\n\n/** The config group every story schema gains, holding the trial's pins. A story never sees it. */\nexport const GLOBALS_KEY = '$globals';\n/** A pin's value when the trial follows the lab. */\nexport const FOLLOW_LAB = 'lab';\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nexport const isGlobalsPath = (path: string): boolean => path === GLOBALS_KEY || path.startsWith(`${GLOBALS_KEY}.`);\n\n/** The lab's values from what was stored: declared keys only, and a value no option offers falls back to the default. */\nexport function labGlobals(declarations: GlobalDeclarations, stored: unknown): Globals {\n const from = isRecord(stored) ? stored : {};\n return Object.fromEntries(\n Object.entries(declarations).map(([key, declaration]) => {\n const value = from[key];\n return [key, declaration.options.some((option) => option.value === value) ? value : declaration.default];\n }),\n );\n}\n\n/** The lab's values, overridden by the pins in a trial's config that do not follow the lab. */\nexport function effectiveGlobals(lab: Globals, pins: unknown): Globals {\n if (!isRecord(pins)) return lab;\n const pinned = Object.entries(pins).filter(([key, value]) => key in lab && value !== FOLLOW_LAB);\n return pinned.length === 0 ? lab : { ...lab, ...Object.fromEntries(pinned) };\n}\n\n/**\n * The config a story receives: the trial's, without its pins. Returns `previous` when every other entry is the same,\n * so a change to a pin alone is not a change to the story's config.\n */\nexport function storyConfig(config: unknown, previous?: unknown): unknown {\n if (!isRecord(config) || !(GLOBALS_KEY in config)) return config;\n const rest = Object.fromEntries(Object.entries(config).filter(([key]) => key !== GLOBALS_KEY));\n if (isRecord(previous)) {\n const keys = Object.keys(rest);\n if (keys.length === Object.keys(previous).length && keys.every((key) => rest[key] === previous[key])) return previous;\n }\n return rest;\n}\n\n/** `schema` with a `$globals` group appended: one leaf per declaration, following the lab until pinned. */\nexport function withGlobals(schema: ConfigSchema<unknown>, declarations: GlobalDeclarations): ConfigSchema<unknown> {\n const entries = Object.entries(declarations);\n if (entries.length === 0) return schema;\n const leaves = Object.fromEntries(\n entries.map(([key, declaration]) => [\n key,\n f.enum(FOLLOW_LAB, [{ value: FOLLOW_LAB, label: 'Lab' }, ...declaration.options]).label(declaration.label),\n ]),\n );\n // An empty group name contributes rows without a heading of its own, under the section's.\n const group = f.group(leaves).label('').section('Globals', { layout: 'inline', pack: 'pairs' });\n return f.schema({ ...schema.nodes, [GLOBALS_KEY]: group }) as ConfigSchema<unknown>;\n}\n","import { LabBoundary, TrialIdContext } from '@weasel-js/labkit';\nimport { OverlayPortalProvider } from '@weasel-js/ui';\nimport {\n type ReactNode,\n type RefObject,\n useContext,\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { runAxe } from '../frame/a11y';\nimport { captureElement } from '../frame/capture';\nimport { createOverrides, type Overrides, scanCssVars } from '../frame/cssVars';\nimport type { FrameSetup } from '../frame/FrameController';\nimport { createGlobalsTarget } from '../frame/globalsTarget';\nimport type { CssVarReport, Globals, Layout, ToFrame } from '../protocol/messages';\nimport { useCssOverrides } from './cssVars/overrides';\nimport { effectiveGlobals, GLOBALS_KEY } from './globals';\nimport { StoryGlobalsContext } from './StoryGlobalsContext';\nimport { type A11yOutcome, TrialFramesContext } from './trialFrames';\n\nexport interface TrialHostProps {\n layout: Layout;\n setup: FrameSetup;\n /** The trial's config, read for its `$globals` pins. */\n config: unknown;\n /** Rendered once the host element exists, with the globals in force on it. */\n children: (globals: Globals) => ReactNode;\n /** Receives the host element, for a caller with no trial to reach it through. */\n hostRef?: RefObject<HTMLElement | null>;\n /** Called once the children's first commit has painted. */\n onRendered?: () => void;\n}\n\n/** How far past the viewport a story stays mounted, so a small scroll back does not remount it. */\nconst IN_VIEW_MARGIN = '50%';\nconst VARS_SETTLE_MS = 100;\n\n/** Whether `ref`'s element is near the viewport; true where the browser cannot say. */\nfunction useInView(ref: RefObject<Element | null>): boolean {\n const [inView, setInView] = useState(true);\n useEffect(() => {\n const el = ref.current;\n if (!el || typeof IntersectionObserver === 'undefined') return;\n const observer = new IntersectionObserver(\n (entries) => {\n const last = entries.at(-1);\n if (last) setInView(last.isIntersecting);\n },\n { rootMargin: IN_VIEW_MARGIN },\n );\n observer.observe(el);\n return () => observer.disconnect();\n }, [ref]);\n return inView;\n}\n\n/** A selector matching only the element carrying `id`. */\nconst scopeOf = (id: string) => `[data-fg-host=\"${id}\"]`;\n\n/**\n * One story's box in the workshop document: what a frame document was, as an element. It applies the globals to\n * itself, keeps its CSS variable overrides on itself, contains fixed-position descendants and portals, reports its\n * vars, size and audits to the trial's frame registry, and unmounts its content while out of view.\n */\nexport function TrialHost({ layout, setup, config, children, hostRef, onRendered }: TrialHostProps) {\n const trialId = useContext(TrialIdContext);\n const frames = useContext(TrialFramesContext);\n const labGlobals = useContext(StoryGlobalsContext);\n const pins = (config as Record<string, unknown> | null | undefined)?.[GLOBALS_KEY];\n const globals = useMemo(() => effectiveGlobals(labGlobals, pins), [labGlobals, pins]);\n const [persisted] = useCssOverrides();\n const reactId = useId();\n const hostId = trialId ?? reactId;\n const [host, setHost] = useState<HTMLDivElement | null>(null);\n const elementRef = useRef<HTMLDivElement | null>(null);\n const inView = useInView(elementRef);\n const [pending, setPending] = useState(true);\n const overridesRef = useRef<Overrides | null>(null);\n const latest = useRef({ setup, onRendered });\n latest.current = { setup, onRendered };\n\n // Everything the host owns for its lifetime: the globals target, the overrides rule, the registry connection.\n useLayoutEffect(() => {\n if (!host) return;\n const doc = host.ownerDocument;\n const scope = scopeOf(hostId);\n const { cssVarsScope } = latest.current.setup;\n const overrides = createOverrides(doc, cssVarsScope ? `${scope}, ${scope} :is(${cssVarsScope})` : scope);\n overridesRef.current = overrides;\n let lastVars: CssVarReport[] | null = null;\n let reported: string | null = null;\n const publish = (vars: CssVarReport[]) => {\n lastVars = vars;\n const key = JSON.stringify(vars);\n if (key === reported) return;\n reported = key;\n if (trialId) frames?.report(trialId, vars);\n };\n const scan = () => publish(scanCssVars(doc, overrides.has, host));\n let settleTimer: ReturnType<typeof setTimeout> | undefined;\n const ownMutation = (record: MutationRecord) =>\n overrides.owns(record.target) ||\n (record.type === 'childList' &&\n [...record.addedNodes, ...record.removedNodes].length > 0 &&\n [...record.addedNodes, ...record.removedNodes].every(overrides.owns));\n const mutations =\n typeof MutationObserver === 'undefined'\n ? null\n : new MutationObserver((records) => {\n if (records.every(ownMutation)) return;\n clearTimeout(settleTimer);\n settleTimer = setTimeout(scan, VARS_SETTLE_MS);\n });\n mutations?.observe(host, { subtree: true, childList: true, characterData: true, attributes: true });\n\n const send = (msg: ToFrame) => {\n if (msg.type !== 'vars.set') return;\n overrides.set(msg.name, msg.value);\n const at = lastVars?.findIndex((v) => v.name === msg.name) ?? -1;\n if (!lastVars || at < 0) return;\n const vars = lastVars.slice();\n vars[at] = { ...vars[at]!, overridden: overrides.has(msg.name) };\n scan();\n };\n const audit = async (): Promise<A11yOutcome> => {\n let outcome: A11yOutcome;\n try {\n outcome = { ok: true, report: await runAxe(host, { page: false }) };\n } catch (error) {\n outcome = { ok: false, message: error instanceof Error ? error.message : String(error) };\n }\n if (trialId) frames?.reportA11y(trialId, outcome);\n return outcome;\n };\n const capture = async () => captureElement(host);\n const disconnect = frames && trialId ? frames.connect(trialId, { send, audit, capture }) : () => {};\n if (frames && trialId) frames.hostRef(trialId).current = host;\n\n const sizes =\n typeof ResizeObserver === 'undefined'\n ? null\n : new ResizeObserver(() => {\n const { width, height } = host.getBoundingClientRect();\n if (trialId) frames?.reportSize(trialId, { width: Math.ceil(width), height: Math.ceil(height) });\n });\n sizes?.observe(host);\n\n return () => {\n disconnect();\n sizes?.disconnect();\n mutations?.disconnect();\n clearTimeout(settleTimer);\n overrides.dispose();\n overridesRef.current = null;\n if (frames && trialId && frames.hostRef(trialId).current === host) frames.hostRef(trialId).current = null;\n };\n }, [host, hostId, trialId, frames]);\n\n // The overrides the trial persisted, put back on every mount and kept in step with the panel's edits.\n const applied = useRef<Record<string, string>>({});\n useLayoutEffect(() => {\n const overrides = overridesRef.current;\n if (!overrides) return;\n for (const name of Object.keys(applied.current)) if (!(name in persisted)) overrides.set(name, null);\n for (const [name, value] of Object.entries(persisted)) overrides.set(name, value);\n applied.current = { ...persisted };\n }, [host, persisted]);\n\n useLayoutEffect(() => {\n if (!host) return;\n const target = createGlobalsTarget(host, scopeOf(hostId));\n latest.current.setup.applyGlobals?.(globals, target);\n return () => target.dispose();\n }, [host, hostId, globals]);\n\n // The host is hidden until its first commit has painted with its fonts, so a new trial never shows a jump.\n const announced = useRef(false);\n useEffect(() => {\n if (!host || !inView || announced.current) return;\n announced.current = true;\n let canceled = false;\n const done = () => {\n if (canceled) return;\n setPending(false);\n latest.current.onRendered?.();\n };\n const { fonts } = host.ownerDocument;\n if (!fonts) {\n done();\n return;\n }\n const afterPaint = () => requestAnimationFrame(done);\n void fonts.ready.then(afterPaint, afterPaint);\n return () => {\n canceled = true;\n };\n }, [host, inView]);\n\n return (\n <div\n ref={(el) => {\n elementRef.current = el;\n if (hostRef) hostRef.current = el;\n setHost(el);\n }}\n className=\"fg-story\"\n data-fg-host={hostId}\n data-fg-layout={layout}\n data-pending={pending || undefined}\n >\n {host && inView ? (\n // The workshop is itself a lab; below the boundary a story sees no lab, trial or theme above it, as in a\n // document of its own.\n <LabBoundary>\n <OverlayPortalProvider container={host}>{children(globals)}</OverlayPortalProvider>\n </LabBoundary>\n ) : null}\n </div>\n );\n}\n","import type { RenderContext } from '@weasel-js/labkit';\nimport { type RefObject, useCallback, useEffect, useMemo, useRef } from 'react';\nimport type { FrameSetup } from '../frame/FrameController';\nimport { StoryHost } from '../frame/StoryHost';\nimport type { Globals } from '../protocol/messages';\nimport type { LoadedStory, StoryContext } from '../story/types';\nimport { isGlobalsPath, storyConfig } from './globals';\nimport { TrialHost } from './TrialHost';\n\nexport interface StoryTrialProps {\n story: LoadedStory;\n setup: FrameSetup;\n /** The trial's config and state; the story reads and writes them through its `StoryContext`. */\n ctx: Pick<RenderContext<unknown, unknown>, 'config' | 'state' | 'setConfig' | 'setState'>;\n hostRef?: RefObject<HTMLElement | null>;\n onRendered?: () => void;\n /** A render throw, after the boundary has shown it. */\n onError?: (error: unknown) => void;\n}\n\n/** A story rendered in the workshop document, inside a `TrialHost`, from the trial's own config and state. */\nexport function StoryTrial({ story, setup, ctx, hostRef, onRendered, onError }: StoryTrialProps) {\n const latest = useRef(ctx);\n latest.current = ctx;\n const config = useMemo(() => storyConfig(ctx.config), [ctx.config]);\n\n // A trial opened on the provisional instrument starts at `null`; the story's own initial state fills it once.\n const seeded = useRef(false);\n useEffect(() => {\n if (seeded.current || ctx.state !== null || !story.initialState) return;\n seeded.current = true;\n ctx.setState(story.initialState(config));\n }, [ctx, story, config]);\n\n // Each new input retries a render the boundary caught, as the frame did on each message.\n const input = useRef({ config, state: ctx.state, key: 0 });\n if (input.current.config !== config || input.current.state !== ctx.state) {\n input.current = { config, state: ctx.state, key: input.current.key + 1 };\n }\n const resetKey = input.current.key;\n\n // Stable, as the frame's were: a story may list them as effect dependencies.\n // The pins belong to the trial; a story cannot see them, so it cannot set them either.\n const setConfig = useCallback((path: string, value: unknown) => {\n if (!isGlobalsPath(path)) latest.current.setConfig(path, value);\n }, []);\n const setState = useCallback((next: unknown) => latest.current.setState(next), []);\n\n const decorators = setup.decorators ?? [];\n const render = (globals: Globals) => {\n const storyCtx: StoryContext = {\n config,\n setConfig,\n state: ctx.state,\n setState,\n globals,\n title: story.title,\n name: story.name,\n };\n return (\n <StoryHost\n story={story}\n ctx={storyCtx}\n decorators={decorators}\n resetKey={resetKey}\n onError={(error) => onError?.(error)}\n />\n );\n };\n\n return (\n <TrialHost\n layout={story.layout}\n setup={setup}\n config={ctx.config}\n {...(hostRef ? { hostRef } : {})}\n {...(onRendered ? { onRendered } : {})}\n >\n {render}\n </TrialHost>\n );\n}\n"]}