@weasel-js/forge 1.5.1 → 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.
- package/README.md +72 -10
- package/dist/{FrameController-BPfCt4PJ.d.ts → FrameController-BspC9Zr5.d.ts} +67 -4
- package/dist/chunk-A4JOEGYQ.js +325 -0
- package/dist/chunk-A4JOEGYQ.js.map +1 -0
- package/dist/{chunk-2MADHLRA.js → chunk-FWPBKKYB.js} +58 -231
- package/dist/chunk-FWPBKKYB.js.map +1 -0
- package/dist/{chunk-6SRQIHJQ.js → chunk-HRGPFQBW.js} +3 -3
- package/dist/{chunk-6SRQIHJQ.js.map → chunk-HRGPFQBW.js.map} +1 -1
- package/dist/chunk-PO5JLVYT.js +318 -0
- package/dist/chunk-PO5JLVYT.js.map +1 -0
- package/dist/{chunk-TLO6UHF2.js → chunk-VEKEBILL.js} +17 -4
- package/dist/chunk-VEKEBILL.js.map +1 -0
- package/dist/{config-CrLpfSq9.d.ts → config-DU_WuORS.d.ts} +7 -1
- package/dist/csf/preview-api.js +1 -2
- package/dist/csf/preview-api.js.map +1 -1
- package/dist/frame/index.css +102 -0
- package/dist/frame/index.css.map +1 -1
- package/dist/frame/index.d.ts +11 -3
- package/dist/frame/index.js +365 -5
- package/dist/frame/index.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js.map +1 -1
- package/dist/shell/index.css +202 -13
- package/dist/shell/index.css.map +1 -1
- package/dist/shell/index.d.ts +76 -12
- package/dist/shell/index.js +1075 -187
- package/dist/shell/index.js.map +1 -1
- package/dist/test/index.d.ts +3 -3
- package/dist/test/index.js +67 -72
- package/dist/test/index.js.map +1 -1
- package/dist/vite/index.js +68 -11
- package/dist/vite/index.js.map +1 -1
- package/package.json +5 -4
- package/dist/chunk-2JDYOBWM.js +0 -118
- package/dist/chunk-2JDYOBWM.js.map +0 -1
- package/dist/chunk-2MADHLRA.js.map +0 -1
- package/dist/chunk-FTFR2YPM.js +0 -13
- package/dist/chunk-FTFR2YPM.js.map +0 -1
- 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
|
|
5
|
-
undo and snapshots live in
|
|
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
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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. */
|
|
@@ -234,13 +273,32 @@ interface IndexEntry {
|
|
|
234
273
|
exportName: string;
|
|
235
274
|
/** Absolute path of the story file. */
|
|
236
275
|
file: string;
|
|
276
|
+
/** The JSDoc written above this story's export, if any. */
|
|
277
|
+
description?: string;
|
|
278
|
+
/** The JSDoc written above the file's meta, if any. */
|
|
279
|
+
componentDescription?: string;
|
|
280
|
+
/** The identifier the meta's `component` names, if it names one. */
|
|
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;
|
|
237
294
|
}
|
|
238
295
|
|
|
239
296
|
interface FrameSetup {
|
|
240
297
|
/** Wraps every story, outermost; receives the globals. From the frame config (`frameConfig`). */
|
|
241
298
|
decorators?: Decorator[];
|
|
242
|
-
/** Applies globals to
|
|
243
|
-
|
|
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;
|
|
244
302
|
/**
|
|
245
303
|
* The elements a CSS variable override is declared on; `:root` by default. An override has to be declared on
|
|
246
304
|
* whichever element declares the token itself — a theme wrapper, say — or that element's own value wins.
|
|
@@ -248,6 +306,11 @@ interface FrameSetup {
|
|
|
248
306
|
cssVarsScope?: string;
|
|
249
307
|
/** Project-wide CSF parameters, Storybook's preview `parameters`: under each meta's and story's. */
|
|
250
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>;
|
|
251
314
|
}
|
|
252
315
|
interface StartFrameOptions {
|
|
253
316
|
story: LoadedStory;
|
|
@@ -260,4 +323,4 @@ declare function startFrame({ story, channel, container, setup }: StartFrameOpti
|
|
|
260
323
|
/** Reports an import failure — there is no story to start. */
|
|
261
324
|
declare function reportImportFault(channel: Channel<ToFrame, FromFrame>, error: unknown): void;
|
|
262
325
|
|
|
263
|
-
export { type A11yFinding as A, type CssVarReport as C, type Decorator as D, type FrameSetup as F, type Globals as G, type
|
|
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"]}
|