@weasel-js/forge 1.5.2 → 1.6.1

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-CSAXZIMV.js +318 -0
  4. package/dist/chunk-CSAXZIMV.js.map +1 -0
  5. package/dist/chunk-DIOLWCH7.js +342 -0
  6. package/dist/chunk-DIOLWCH7.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-TLO6UHF2.js → chunk-JXIWZFD4.js} +17 -4
  10. package/dist/chunk-JXIWZFD4.js.map +1 -0
  11. package/dist/{chunk-5YHEO26U.js → chunk-OLWZURU3.js} +58 -240
  12. package/dist/chunk-OLWZURU3.js.map +1 -0
  13. package/dist/{config-CSVP6duU.d.ts → config-C5Vq5Kjj.d.ts} +12 -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 +76 -17
  26. package/dist/shell/index.js +763 -215
  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,318 @@
1
+ import { StoryHost, isPlainObject } from './chunk-OLWZURU3.js';
2
+ import { sanitize } from './chunk-HRGPFQBW.js';
3
+ import { PROTOCOL_VERSION } from './chunk-JXIWZFD4.js';
4
+ import { f, isConfigBranch, resolveConfigSchema, schemaNodeAtPath, titleCase } from '@weasel-js/labkit/config';
5
+ import { Button } from '@weasel-js/ui/components/Button';
6
+ import { createContext, useMemo, useState, useRef, useEffect, useContext, useId } from 'react';
7
+ import { jsx, jsxs } from 'react/jsx-runtime';
8
+
9
+ var joinPath = (at, key) => at === "" ? key : `${at}.${key}`;
10
+ function describeShape(shape) {
11
+ const out = {};
12
+ for (const [key, entry] of Object.entries(shape)) {
13
+ const { section, showIf } = entry.options;
14
+ const common = {
15
+ annotations: { ...entry.annotations },
16
+ ...section === void 0 ? {} : { section: { ...section } },
17
+ conditional: showIf !== void 0
18
+ };
19
+ out[key] = isConfigBranch(entry) ? { branch: true, ...common, children: describeShape(entry.children) } : {
20
+ branch: false,
21
+ kind: entry.kind,
22
+ default: entry.default,
23
+ ...common,
24
+ validated: entry.options.validate !== void 0,
25
+ customControl: entry.options.render !== void 0
26
+ };
27
+ }
28
+ return out;
29
+ }
30
+ function describeSchema(schema) {
31
+ return { nodes: describeShape(schema.nodes) };
32
+ }
33
+ function rebuildShape(shape, at, answers) {
34
+ const out = {};
35
+ for (const [key, entry] of Object.entries(shape)) {
36
+ const path = joinPath(at, key);
37
+ const options = {
38
+ ...entry.section === void 0 ? {} : { section: entry.section },
39
+ ...entry.conditional ? { showIf: (config) => !answers.hidden(path, config) } : {}
40
+ };
41
+ if (entry.branch) {
42
+ const branch = {
43
+ children: rebuildShape(entry.children, path, answers),
44
+ annotations: entry.annotations,
45
+ options
46
+ };
47
+ out[key] = branch;
48
+ } else {
49
+ const node = {
50
+ kind: entry.kind,
51
+ default: entry.default,
52
+ annotations: entry.annotations,
53
+ options: entry.validated ? { ...options, validate: (_leaf, config) => answers.errors(path, config) } : options
54
+ };
55
+ out[key] = node;
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+ function schemaFromDescription(description, answers) {
61
+ return f.schema(rebuildShape(description.nodes, "", answers));
62
+ }
63
+ function answerSchema(schema, config) {
64
+ const hidden = [];
65
+ const errors = {};
66
+ const record = config;
67
+ let group;
68
+ const walk = (shape, at) => {
69
+ for (const [key, entry] of Object.entries(shape)) {
70
+ const path = joinPath(at, key);
71
+ if (entry.options.showIf && !entry.options.showIf(record)) hidden.push(path);
72
+ if (isConfigBranch(entry)) {
73
+ walk(entry.children, path);
74
+ continue;
75
+ }
76
+ const { validate } = entry.options;
77
+ if (!validate) continue;
78
+ group ??= resolveConfigSchema(schema).group;
79
+ const leaf = schemaNodeAtPath(group, path);
80
+ if (!leaf || "children" in leaf) continue;
81
+ const found = validate(leaf, record);
82
+ if (found.length > 0) errors[path] = found;
83
+ }
84
+ };
85
+ walk(schema.nodes, "");
86
+ return { hidden, errors };
87
+ }
88
+
89
+ // src/protocol/channel.ts
90
+ function openChannel(port, options = {}) {
91
+ const listeners = /* @__PURE__ */ new Set();
92
+ const onMessage = (event) => {
93
+ const data = event.data;
94
+ if (typeof data !== "object" || data === null || !("msg" in data)) {
95
+ options.onMismatch?.({ reason: "not-an-envelope" });
96
+ return;
97
+ }
98
+ const envelope = data;
99
+ if (envelope.v !== PROTOCOL_VERSION) {
100
+ options.onMismatch?.({ reason: "version", version: envelope.v });
101
+ return;
102
+ }
103
+ for (const fn of listeners) fn(envelope.msg);
104
+ };
105
+ port.addEventListener("message", onMessage);
106
+ port.start?.();
107
+ return {
108
+ send: (msg) => port.postMessage({ v: PROTOCOL_VERSION, msg }),
109
+ on: (fn) => {
110
+ listeners.add(fn);
111
+ return () => listeners.delete(fn);
112
+ },
113
+ close: () => {
114
+ port.removeEventListener("message", onMessage);
115
+ listeners.clear();
116
+ port.close();
117
+ }
118
+ };
119
+ }
120
+
121
+ // src/story/indexPages.ts
122
+ var SUFFIX = ":index";
123
+ var INDEX_NAME = "Index";
124
+ function indexId(title) {
125
+ return `${sanitize(title)}${SUFFIX}`;
126
+ }
127
+ function isIndexId(id) {
128
+ return id.endsWith(SUFFIX);
129
+ }
130
+ function indexEntries(stories) {
131
+ const byTitle = /* @__PURE__ */ new Map();
132
+ for (const entry of stories) {
133
+ if (byTitle.has(entry.title)) continue;
134
+ byTitle.set(entry.title, {
135
+ id: indexId(entry.title),
136
+ title: entry.title,
137
+ name: INDEX_NAME,
138
+ exportName: "",
139
+ file: entry.file,
140
+ ...entry.componentDescription === void 0 ? {} : { componentDescription: entry.componentDescription },
141
+ ...entry.componentName === void 0 ? {} : { componentName: entry.componentName }
142
+ });
143
+ }
144
+ return [...byTitle.values()];
145
+ }
146
+ var MAX_ENUM_OPTIONS = 8;
147
+ var MAX_ROWS = 6;
148
+ var labelOf = (key, name) => name ?? titleCase(key);
149
+ function variantRows(schema) {
150
+ const defaults = schema.defaults();
151
+ const rows = [];
152
+ const walk = (shape, at) => {
153
+ for (const [key, entry] of Object.entries(shape)) {
154
+ if (rows.length >= MAX_ROWS) return;
155
+ const path = at === "" ? key : `${at}.${key}`;
156
+ if (entry.options.showIf && !entry.options.showIf(defaults)) continue;
157
+ if (isConfigBranch(entry)) {
158
+ walk(entry.children, path);
159
+ continue;
160
+ }
161
+ if (entry.annotations.hidden || entry.options.render) continue;
162
+ const label = labelOf(key, entry.annotations.name);
163
+ if (entry.kind === "boolean") {
164
+ rows.push({ path, label, values: [false, true].map((value) => ({ value, label: String(value) })) });
165
+ } else if (entry.kind === "enum") {
166
+ const options = entry.annotations.options ?? [];
167
+ if (options.length < 2 || options.length > MAX_ENUM_OPTIONS) continue;
168
+ rows.push({ path, label, values: options.map((o) => ({ value: o.value, label: o.label })) });
169
+ }
170
+ }
171
+ };
172
+ walk(schema.nodes, "");
173
+ return rows;
174
+ }
175
+ function mergeConfig(base, over) {
176
+ if (!isPlainObject(base) || !isPlainObject(over)) return over === void 0 ? base : over;
177
+ const out = { ...base };
178
+ for (const [key, value] of Object.entries(over)) out[key] = mergeConfig(base[key], value);
179
+ return out;
180
+ }
181
+ function withValueAt(config, path, value) {
182
+ const [head, ...rest] = path.split(".");
183
+ if (head === void 0) return value;
184
+ const record = isPlainObject(config) ? config : {};
185
+ return { ...record, [head]: rest.length === 0 ? value : withValueAt(record[head], rest.join("."), value) };
186
+ }
187
+ var IndexEnvContext = createContext(null);
188
+ function useEnv() {
189
+ const env = useContext(IndexEnvContext);
190
+ if (!env) throw new Error("An index page part rendered outside its index page");
191
+ return env;
192
+ }
193
+ var NEAR_MARGIN = "100%";
194
+ function useNearViewport(ref) {
195
+ const [near, setNear] = useState(typeof IntersectionObserver === "undefined");
196
+ useEffect(() => {
197
+ const el = ref.current;
198
+ if (!el || typeof IntersectionObserver === "undefined") return;
199
+ const observer = new IntersectionObserver((entries) => {
200
+ const last = entries.at(-1);
201
+ if (last) setNear(last.isIntersecting);
202
+ }, { rootMargin: NEAR_MARGIN });
203
+ observer.observe(el);
204
+ return () => observer.disconnect();
205
+ }, [ref]);
206
+ return near;
207
+ }
208
+ function IndexStory({ story, config: over, label }) {
209
+ const env = useEnv();
210
+ const [config, setConfig] = useState(() => mergeConfig(story.config.defaults(), over));
211
+ const [state, setState] = useState(() => story.initialState?.(config) ?? null);
212
+ const cell = useRef(null);
213
+ const near = useNearViewport(cell);
214
+ const [height, setHeight] = useState(null);
215
+ useEffect(() => {
216
+ const el = cell.current;
217
+ if (!near || !el || typeof ResizeObserver === "undefined") return;
218
+ const observer = new ResizeObserver(() => setHeight(el.getBoundingClientRect().height));
219
+ observer.observe(el);
220
+ return () => observer.disconnect();
221
+ }, [near]);
222
+ const ctx = {
223
+ config,
224
+ setConfig: (path, value) => setConfig((prev) => withValueAt(prev, path, value)),
225
+ state,
226
+ setState: (next) => setState((prev) => typeof next === "function" ? next(prev) : next),
227
+ globals: env.globals,
228
+ title: story.title,
229
+ name: story.name
230
+ };
231
+ return /* @__PURE__ */ jsxs(
232
+ "figure",
233
+ {
234
+ ref: cell,
235
+ className: `fg-index-cell fg-index-cell--${story.layout}`,
236
+ style: !near && height !== null ? { minHeight: height } : void 0,
237
+ children: [
238
+ /* @__PURE__ */ jsx("div", { className: "fg-index-cell__stage", children: near ? /* @__PURE__ */ jsx(StoryHost, { story, ctx, decorators: env.decorators, resetKey: 0, onError: env.onError }) : null }),
239
+ label === void 0 ? null : /* @__PURE__ */ jsx("figcaption", { className: "fg-index-cell__label", children: label })
240
+ ]
241
+ }
242
+ );
243
+ }
244
+ function IndexVariants({ story }) {
245
+ const rows = useMemo(() => variantRows(story.config), [story]);
246
+ const defaults = useMemo(() => story.config.defaults(), [story]);
247
+ if (rows.length === 0) return null;
248
+ return /* @__PURE__ */ jsx("div", { className: "fg-index-variants", children: rows.map((row) => /* @__PURE__ */ jsxs("section", { className: "fg-index-variants__row", "aria-label": row.label, children: [
249
+ /* @__PURE__ */ jsx("h4", { className: "fg-index-variants__label", children: row.label }),
250
+ /* @__PURE__ */ jsx("div", { className: "fg-index-variants__cells", children: row.values.map((v) => /* @__PURE__ */ jsx(
251
+ IndexStory,
252
+ {
253
+ story,
254
+ config: withValueAt(defaults, row.path, v.value),
255
+ label: v.label
256
+ },
257
+ String(v.value)
258
+ )) })
259
+ ] }, row.path)) });
260
+ }
261
+ function StorySection({ story, variants }) {
262
+ const env = useEnv();
263
+ const headingId = useId();
264
+ const description = env.descriptions[story.id];
265
+ return /* @__PURE__ */ jsxs("section", { className: "fg-index-story", "aria-labelledby": headingId, children: [
266
+ /* @__PURE__ */ jsxs("header", { className: "fg-index-story__head", children: [
267
+ /* @__PURE__ */ jsx("h2", { id: headingId, className: "fg-index-story__name", children: story.name }),
268
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: () => env.open(story.id), children: "Open" })
269
+ ] }),
270
+ description ? /* @__PURE__ */ jsx("p", { className: "fg-index-story__description", children: description }) : null,
271
+ /* @__PURE__ */ jsx(IndexStory, { story }),
272
+ variants ? /* @__PURE__ */ jsx(IndexVariants, { story }) : null
273
+ ] });
274
+ }
275
+ function firstOfEachControlSet(stories) {
276
+ const seen = /* @__PURE__ */ new Set();
277
+ const shown = /* @__PURE__ */ new Set();
278
+ for (const story of stories) {
279
+ const key = JSON.stringify(variantRows(story.config));
280
+ if (seen.has(key)) continue;
281
+ seen.add(key);
282
+ shown.add(story.id);
283
+ }
284
+ return shown;
285
+ }
286
+ function DefaultIndex() {
287
+ const env = useEnv();
288
+ const withVariants = useMemo(() => firstOfEachControlSet(env.stories), [env.stories]);
289
+ const segments = env.title.split("/");
290
+ const name = segments.pop();
291
+ return /* @__PURE__ */ jsxs("article", { className: "fg-index", children: [
292
+ /* @__PURE__ */ jsxs("header", { className: "fg-index__head", children: [
293
+ segments.length > 0 ? /* @__PURE__ */ jsx("p", { className: "fg-index__path", children: segments.join(" / ") }) : null,
294
+ /* @__PURE__ */ jsx("h1", { className: "fg-index__title", children: name }),
295
+ env.description ? /* @__PURE__ */ jsx("p", { className: "fg-index__description", children: env.description }) : null
296
+ ] }),
297
+ env.stories.map((story) => /* @__PURE__ */ jsx(StorySection, { story, variants: withVariants.has(story.id) }, story.id))
298
+ ] });
299
+ }
300
+ function indexContext(env) {
301
+ return {
302
+ title: env.title,
303
+ ...env.description === void 0 ? {} : { description: env.description },
304
+ stories: env.stories,
305
+ globals: env.globals,
306
+ Story: IndexStory,
307
+ Variants: IndexVariants,
308
+ DefaultIndex,
309
+ open: env.open
310
+ };
311
+ }
312
+ function IndexPage({ env, render }) {
313
+ return /* @__PURE__ */ jsx(IndexEnvContext.Provider, { value: env, children: render ? render(indexContext(env)) : /* @__PURE__ */ jsx(DefaultIndex, {}) });
314
+ }
315
+
316
+ export { IndexPage, answerSchema, describeSchema, indexEntries, indexId, isIndexId, openChannel, schemaFromDescription };
317
+ //# sourceMappingURL=chunk-CSAXZIMV.js.map
318
+ //# sourceMappingURL=chunk-CSAXZIMV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/protocol/schema.ts","../src/protocol/channel.ts","../src/story/indexPages.ts","../src/frame/index/variants.ts","../src/frame/index/IndexPage.tsx"],"names":["isConfigBranch"],"mappings":";;;;;;;;AAqDA,IAAM,QAAA,GAAW,CAAC,EAAA,EAAY,GAAA,KAAyB,EAAA,KAAO,KAAK,GAAA,GAAM,CAAA,EAAG,EAAE,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA;AAErF,SAAS,cAAc,KAAA,EAAoC;AACzD,EAAA,MAAM,MAAsB,EAAC;AAC7B,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,EAAG;AAChD,IAAA,MAAM,EAAE,OAAA,EAAS,MAAA,EAAO,GAAI,KAAA,CAAM,OAAA;AAClC,IAAA,MAAM,MAAA,GAAS;AAAA,MACb,WAAA,EAAa,EAAE,GAAG,KAAA,CAAM,WAAA,EAAY;AAAA,MACpC,GAAI,OAAA,KAAY,MAAA,GAAY,EAAC,GAAI,EAAE,OAAA,EAAS,EAAE,GAAG,OAAA,EAAQ,EAAE;AAAA,MAC3D,aAAa,MAAA,KAAW;AAAA,KAC1B;AACA,IAAA,GAAA,CAAI,GAAG,CAAA,GAAI,cAAA,CAAe,KAAK,IAC3B,EAAE,MAAA,EAAQ,IAAA,EAAM,GAAG,QAAQ,QAAA,EAAU,aAAA,CAAc,KAAA,CAAM,QAAQ,GAAE,GACnE;AAAA,MACE,MAAA,EAAQ,KAAA;AAAA,MACR,MAAM,KAAA,CAAM,IAAA;AAAA,MACZ,SAAS,KAAA,CAAM,OAAA;AAAA,MACf,GAAG,MAAA;AAAA,MACH,SAAA,EAAW,KAAA,CAAM,OAAA,CAAQ,QAAA,KAAa,MAAA;AAAA,MACtC,aAAA,EAAe,KAAA,CAAM,OAAA,CAAQ,MAAA,KAAW;AAAA,KAC1C;AAAA,EACN;AACA,EAAA,OAAO,GAAA;AACT;AAEO,SAAS,eAAe,MAAA,EAAkD;AAC/E,EAAA,OAAO,EAAE,KAAA,EAAO,aAAA,CAAc,MAAA,CAAO,KAAK,CAAA,EAAE;AAC9C;AAEA,SAAS,YAAA,CAAa,KAAA,EAAuB,EAAA,EAAY,OAAA,EAAqC;AAC5F,EAAA,MAAM,MAAmC,EAAC;AAC1C,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,EAAG;AAChD,IAAA,MAAM,IAAA,GAAO,QAAA,CAAS,EAAA,EAAI,GAAG,CAAA;AAC7B,IAAA,MAAM,OAAA,GAAuB;AAAA,MAC3B,GAAI,MAAM,OAAA,KAAY,MAAA,GAAY,EAAC,GAAI,EAAE,OAAA,EAAS,KAAA,CAAM,OAAA,EAAQ;AAAA,MAChE,GAAI,KAAA,CAAM,WAAA,GAAc,EAAE,QAAQ,CAAC,MAAA,KAAW,CAAC,OAAA,CAAQ,MAAA,CAAO,IAAA,EAAM,MAAM,CAAA,KAAM;AAAC,KACnF;AACA,IAAA,IAAI,MAAM,MAAA,EAAQ;AAChB,MAAA,MAAM,MAAA,GAAuB;AAAA,QAC3B,QAAA,EAAU,YAAA,CAAa,KAAA,CAAM,QAAA,EAAU,MAAM,OAAO,CAAA;AAAA,QACpD,aAAa,KAAA,CAAM,WAAA;AAAA,QACnB;AAAA,OACF;AACA,MAAA,GAAA,CAAI,GAAG,CAAA,GAAI,MAAA;AAAA,IACb,CAAA,MAAO;AACL,MAAA,MAAM,IAAA,GAAmB;AAAA,QACvB,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ,SAAS,KAAA,CAAM,OAAA;AAAA,QACf,aAAa,KAAA,CAAM,WAAA;AAAA,QACnB,OAAA,EAAS,KAAA,CAAM,SAAA,GAAY,EAAE,GAAG,OAAA,EAAS,QAAA,EAAU,CAAC,KAAA,EAAO,WAAW,OAAA,CAAQ,MAAA,CAAO,IAAA,EAAM,MAAM,GAAE,GAAI;AAAA,OACzG;AACA,MAAA,GAAA,CAAI,GAAG,CAAA,GAAI,IAAA;AAAA,IACb;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;AAEO,SAAS,qBAAA,CACd,aACA,OAAA,EACuB;AACvB,EAAA,OAAO,EAAE,MAAA,CAAO,YAAA,CAAa,YAAY,KAAA,EAAO,EAAA,EAAI,OAAO,CAAC,CAAA;AAC9D;AAGO,SAAS,YAAA,CACd,QACA,MAAA,EACwD;AACxD,EAAA,MAAM,SAAmB,EAAC;AAC1B,EAAA,MAAM,SAAmC,EAAC;AAC1C,EAAA,MAAM,MAAA,GAAS,MAAA;AACf,EAAA,IAAI,KAAA;AAEJ,EAAA,MAAM,IAAA,GAAO,CAAC,KAAA,EAAoB,EAAA,KAAqB;AACrD,IAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,EAAG;AAChD,MAAA,MAAM,IAAA,GAAO,QAAA,CAAS,EAAA,EAAI,GAAG,CAAA;AAC7B,MAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,MAAA,IAAU,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAA,CAAO,MAAM,CAAA,EAAG,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA;AAC3E,MAAA,IAAI,cAAA,CAAe,KAAK,CAAA,EAAG;AACzB,QAAA,IAAA,CAAK,KAAA,CAAM,UAAU,IAAI,CAAA;AACzB,QAAA;AAAA,MACF;AACA,MAAA,MAAM,EAAE,QAAA,EAAS,GAAI,KAAA,CAAM,OAAA;AAC3B,MAAA,IAAI,CAAC,QAAA,EAAU;AACf,MAAA,KAAA,KAAU,mBAAA,CAAoB,MAAM,CAAA,CAAE,KAAA;AACtC,MAAA,MAAM,IAAA,GAAO,gBAAA,CAAiB,KAAA,EAAO,IAAI,CAAA;AACzC,MAAA,IAAI,CAAC,IAAA,IAAQ,UAAA,IAAc,IAAA,EAAM;AACjC,MAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,IAAA,EAAM,MAAM,CAAA;AACnC,MAAA,IAAI,KAAA,CAAM,MAAA,GAAS,CAAA,EAAG,MAAA,CAAO,IAAI,CAAA,GAAI,KAAA;AAAA,IACvC;AAAA,EACF,CAAA;AACA,EAAA,IAAA,CAAK,MAAA,CAAO,OAAO,EAAE,CAAA;AACrB,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC1B;;;AC5HO,SAAS,WAAA,CAAqB,IAAA,EAAgB,OAAA,GAA0B,EAAC,EAAqB;AACnG,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAuB;AAC7C,EAAA,MAAM,SAAA,GAAY,CAAC,KAAA,KAAwB;AACzC,IAAA,MAAM,OAAgB,KAAA,CAAM,IAAA;AAC5B,IAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,SAAS,IAAA,IAAQ,EAAE,SAAS,IAAA,CAAA,EAAO;AACjE,MAAA,OAAA,CAAQ,UAAA,GAAa,EAAE,MAAA,EAAQ,iBAAA,EAAmB,CAAA;AAClD,MAAA;AAAA,IACF;AACA,IAAA,MAAM,QAAA,GAAW,IAAA;AACjB,IAAA,IAAI,QAAA,CAAS,MAAM,gBAAA,EAAkB;AACnC,MAAA,OAAA,CAAQ,aAAa,EAAE,MAAA,EAAQ,WAAW,OAAA,EAAS,QAAA,CAAS,GAAG,CAAA;AAC/D,MAAA;AAAA,IACF;AACA,IAAA,KAAA,MAAW,EAAA,IAAM,SAAA,EAAW,EAAA,CAAG,QAAA,CAAS,GAAS,CAAA;AAAA,EACnD,CAAA;AACA,EAAA,IAAA,CAAK,gBAAA,CAAiB,WAAW,SAAS,CAAA;AAC1C,EAAA,IAAA,CAAK,KAAA,IAAQ;AACb,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,CAAC,GAAA,KAAQ,IAAA,CAAK,YAAY,EAAE,CAAA,EAAG,gBAAA,EAAkB,GAAA,EAA6B,CAAA;AAAA,IACpF,EAAA,EAAI,CAAC,EAAA,KAAO;AACV,MAAA,SAAA,CAAU,IAAI,EAAE,CAAA;AAChB,MAAA,OAAO,MAAM,SAAA,CAAU,MAAA,CAAO,EAAE,CAAA;AAAA,IAClC,CAAA;AAAA,IACA,OAAO,MAAM;AACX,MAAA,IAAA,CAAK,mBAAA,CAAoB,WAAW,SAAS,CAAA;AAC7C,MAAA,SAAA,CAAU,KAAA,EAAM;AAChB,MAAA,IAAA,CAAK,KAAA,EAAM;AAAA,IACb;AAAA,GACF;AACF;;;AChDA,IAAM,MAAA,GAAS,QAAA;AAGR,IAAM,UAAA,GAAa,OAAA;AAGnB,SAAS,QAAQ,KAAA,EAAuB;AAC7C,EAAA,OAAO,CAAA,EAAG,QAAA,CAAS,KAAK,CAAC,GAAG,MAAM,CAAA,CAAA;AACpC;AAEO,SAAS,UAAU,EAAA,EAAqB;AAC7C,EAAA,OAAO,EAAA,CAAG,SAAS,MAAM,CAAA;AAC3B;AAMO,SAAS,aAAa,OAAA,EAA8C;AACzE,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAwB;AAC5C,EAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,IAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,KAAA,CAAM,KAAK,CAAA,EAAG;AAC9B,IAAA,OAAA,CAAQ,GAAA,CAAI,MAAM,KAAA,EAAO;AAAA,MACvB,EAAA,EAAI,OAAA,CAAQ,KAAA,CAAM,KAAK,CAAA;AAAA,MACvB,OAAO,KAAA,CAAM,KAAA;AAAA,MACb,IAAA,EAAM,UAAA;AAAA,MACN,UAAA,EAAY,EAAA;AAAA,MACZ,MAAM,KAAA,CAAM,IAAA;AAAA,MACZ,GAAI,MAAM,oBAAA,KAAyB,MAAA,GAAY,EAAC,GAAI,EAAE,oBAAA,EAAsB,KAAA,CAAM,oBAAA,EAAqB;AAAA,MACvG,GAAI,MAAM,aAAA,KAAkB,MAAA,GAAY,EAAC,GAAI,EAAE,aAAA,EAAe,KAAA,CAAM,aAAA;AAAc,KACnF,CAAA;AAAA,EACH;AACA,EAAA,OAAO,CAAC,GAAG,OAAA,CAAQ,MAAA,EAAQ,CAAA;AAC7B;ACnBO,IAAM,gBAAA,GAAmB,CAAA;AACzB,IAAM,QAAA,GAAW,CAAA;AAExB,IAAM,UAAU,CAAC,GAAA,EAAa,IAAA,KAAqC,IAAA,IAAQ,UAAU,GAAG,CAAA;AAOjF,SAAS,YAAY,MAAA,EAA6C;AACvE,EAAA,MAAM,QAAA,GAAW,OAAO,QAAA,EAAS;AACjC,EAAA,MAAM,OAAqB,EAAC;AAC5B,EAAA,MAAM,IAAA,GAAO,CAAC,KAAA,EAAoB,EAAA,KAAqB;AACrD,IAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,EAAG;AAChD,MAAA,IAAI,IAAA,CAAK,UAAU,QAAA,EAAU;AAC7B,MAAA,MAAM,OAAO,EAAA,KAAO,EAAA,GAAK,MAAM,CAAA,EAAG,EAAE,IAAI,GAAG,CAAA,CAAA;AAC3C,MAAA,IAAI,KAAA,CAAM,QAAQ,MAAA,IAAU,CAAC,MAAM,OAAA,CAAQ,MAAA,CAAO,QAAQ,CAAA,EAAG;AAC7D,MAAA,IAAIA,cAAAA,CAAe,KAAK,CAAA,EAAG;AACzB,QAAA,IAAA,CAAK,KAAA,CAAM,UAAU,IAAI,CAAA;AACzB,QAAA;AAAA,MACF;AACA,MAAA,IAAI,KAAA,CAAM,WAAA,CAAY,MAAA,IAAU,KAAA,CAAM,QAAQ,MAAA,EAAQ;AACtD,MAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,GAAA,EAAK,KAAA,CAAM,YAAY,IAAI,CAAA;AACjD,MAAA,IAAI,KAAA,CAAM,SAAS,SAAA,EAAW;AAC5B,QAAA,IAAA,CAAK,IAAA,CAAK,EAAE,IAAA,EAAM,KAAA,EAAO,QAAQ,CAAC,KAAA,EAAO,IAAI,CAAA,CAAE,GAAA,CAAI,CAAC,KAAA,MAAW,EAAE,OAAO,KAAA,EAAO,MAAA,CAAO,KAAK,CAAA,EAAE,CAAE,GAAG,CAAA;AAAA,MACpG,CAAA,MAAA,IAAW,KAAA,CAAM,IAAA,KAAS,MAAA,EAAQ;AAChC,QAAA,MAAM,OAAA,GAAU,KAAA,CAAM,WAAA,CAAY,OAAA,IAAW,EAAC;AAC9C,QAAA,IAAI,OAAA,CAAQ,MAAA,GAAS,CAAA,IAAK,OAAA,CAAQ,SAAS,gBAAA,EAAkB;AAC7D,QAAA,IAAA,CAAK,KAAK,EAAE,IAAA,EAAM,OAAO,MAAA,EAAQ,OAAA,CAAQ,IAAI,CAAC,CAAA,MAAO,EAAE,KAAA,EAAO,EAAE,KAAA,EAAO,KAAA,EAAO,EAAE,KAAA,EAAM,CAAE,GAAG,CAAA;AAAA,MAC7F;AAAA,IACF;AAAA,EACF,CAAA;AACA,EAAA,IAAA,CAAK,MAAA,CAAO,OAAO,EAAE,CAAA;AACrB,EAAA,OAAO,IAAA;AACT;AAGO,SAAS,WAAA,CAAY,MAAe,IAAA,EAAwB;AACjE,EAAA,IAAI,CAAC,aAAA,CAAc,IAAI,CAAA,IAAK,CAAC,aAAA,CAAc,IAAI,CAAA,EAAG,OAAO,IAAA,KAAS,MAAA,GAAY,IAAA,GAAO,IAAA;AACrF,EAAA,MAAM,GAAA,GAA+B,EAAE,GAAG,IAAA,EAAK;AAC/C,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,IAAI,CAAA,EAAG,GAAA,CAAI,GAAG,CAAA,GAAI,WAAA,CAAY,IAAA,CAAK,GAAG,GAAG,KAAK,CAAA;AACxF,EAAA,OAAO,GAAA;AACT;AAGO,SAAS,WAAA,CAAY,MAAA,EAAiB,IAAA,EAAc,KAAA,EAAyB;AAClF,EAAA,MAAM,CAAC,IAAA,EAAM,GAAG,IAAI,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AACtC,EAAA,IAAI,IAAA,KAAS,QAAW,OAAO,KAAA;AAC/B,EAAA,MAAM,MAAA,GAAS,aAAA,CAAc,MAAM,CAAA,GAAI,SAAS,EAAC;AACjD,EAAA,OAAO,EAAE,GAAG,MAAA,EAAQ,CAAC,IAAI,GAAG,KAAK,MAAA,KAAW,CAAA,GAAI,QAAQ,WAAA,CAAY,MAAA,CAAO,IAAI,CAAA,EAAG,IAAA,CAAK,KAAK,GAAG,CAAA,EAAG,KAAK,CAAA,EAAE;AAC3G;AC9CO,IAAM,eAAA,GAAkB,cAA+B,IAAI,CAAA;AAElE,SAAS,MAAA,GAAmB;AAC1B,EAAA,MAAM,GAAA,GAAM,WAAW,eAAe,CAAA;AACtC,EAAA,IAAI,CAAC,GAAA,EAAK,MAAM,IAAI,MAAM,oDAAoD,CAAA;AAC9E,EAAA,OAAO,GAAA;AACT;AAGA,IAAM,WAAA,GAAc,MAAA;AAGpB,SAAS,gBAAgB,GAAA,EAAyC;AAChE,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,IAAI,QAAA,CAAS,OAAO,yBAAyB,WAAW,CAAA;AAC5E,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,KAAK,GAAA,CAAI,OAAA;AACf,IAAA,IAAI,CAAC,EAAA,IAAM,OAAO,oBAAA,KAAyB,WAAA,EAAa;AACxD,IAAA,MAAM,QAAA,GAAW,IAAI,oBAAA,CAAqB,CAAC,OAAA,KAAY;AACrD,MAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,EAAA,CAAG,EAAE,CAAA;AAC1B,MAAA,IAAI,IAAA,EAAM,OAAA,CAAQ,IAAA,CAAK,cAAc,CAAA;AAAA,IACvC,CAAA,EAAG,EAAE,UAAA,EAAY,WAAA,EAAa,CAAA;AAC9B,IAAA,QAAA,CAAS,QAAQ,EAAE,CAAA;AACnB,IAAA,OAAO,MAAM,SAAS,UAAA,EAAW;AAAA,EACnC,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AACR,EAAA,OAAO,IAAA;AACT;AAMO,SAAS,WAAW,EAAE,KAAA,EAAO,MAAA,EAAQ,IAAA,EAAM,OAAM,EAAoB;AAC1E,EAAA,MAAM,MAAM,MAAA,EAAO;AACnB,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAI,QAAA,CAAS,MAAM,WAAA,CAAY,KAAA,CAAM,MAAA,CAAO,QAAA,EAAS,EAAG,IAAI,CAAC,CAAA;AACrF,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,QAAA,CAAkB,MAAM,KAAA,CAAM,YAAA,GAAe,MAAM,CAAA,IAAK,IAAI,CAAA;AACtF,EAAA,MAAM,IAAA,GAAO,OAAoB,IAAI,CAAA;AACrC,EAAA,MAAM,IAAA,GAAO,gBAAgB,IAAI,CAAA;AAEjC,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAI,SAAwB,IAAI,CAAA;AACxD,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,KAAK,IAAA,CAAK,OAAA;AAChB,IAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,EAAA,IAAM,OAAO,mBAAmB,WAAA,EAAa;AAC3D,IAAA,MAAM,QAAA,GAAW,IAAI,cAAA,CAAe,MAAM,UAAU,EAAA,CAAG,qBAAA,EAAsB,CAAE,MAAM,CAAC,CAAA;AACtF,IAAA,QAAA,CAAS,QAAQ,EAAE,CAAA;AACnB,IAAA,OAAO,MAAM,SAAS,UAAA,EAAW;AAAA,EACnC,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AACT,EAAA,MAAM,GAAA,GAAoB;AAAA,IACxB,MAAA;AAAA,IACA,SAAA,EAAW,CAAC,IAAA,EAAM,KAAA,KAAU,SAAA,CAAU,CAAC,IAAA,KAAkB,WAAA,CAAY,IAAA,EAAM,IAAA,EAAM,KAAK,CAAC,CAAA;AAAA,IACvF,KAAA;AAAA,IACA,QAAA,EAAU,CAAC,IAAA,KACT,QAAA,CAAS,CAAC,IAAA,KAAmB,OAAO,IAAA,KAAS,UAAA,GAAc,IAAA,CAAiC,IAAI,CAAA,GAAI,IAAK,CAAA;AAAA,IAC3G,SAAS,GAAA,CAAI,OAAA;AAAA,IACb,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,MAAM,KAAA,CAAM;AAAA,GACd;AACA,EAAA,uBACE,IAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACC,GAAA,EAAK,IAAA;AAAA,MACL,SAAA,EAAW,CAAA,6BAAA,EAAgC,KAAA,CAAM,MAAM,CAAA,CAAA;AAAA,MACvD,KAAA,EAAO,CAAC,IAAA,IAAQ,MAAA,KAAW,OAAO,EAAE,SAAA,EAAW,QAAO,GAAI,MAAA;AAAA,MAE1D,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,SAAI,SAAA,EAAU,sBAAA,EACZ,QAAA,EAAA,IAAA,mBACC,GAAA,CAAC,aAAU,KAAA,EAAc,GAAA,EAAU,UAAA,EAAY,GAAA,CAAI,YAAY,QAAA,EAAU,CAAA,EAAG,SAAS,GAAA,CAAI,OAAA,EAAS,IAChG,IAAA,EACN,CAAA;AAAA,QACC,UAAU,MAAA,GAAY,IAAA,uBAAQ,YAAA,EAAA,EAAW,SAAA,EAAU,wBAAwB,QAAA,EAAA,KAAA,EAAM;AAAA;AAAA;AAAA,GACpF;AAEJ;AAGO,SAAS,aAAA,CAAc,EAAE,KAAA,EAAM,EAA2B;AAC/D,EAAA,MAAM,IAAA,GAAO,QAAQ,MAAM,WAAA,CAAY,MAAM,MAAM,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAC7D,EAAA,MAAM,QAAA,GAAW,QAAQ,MAAM,KAAA,CAAM,OAAO,QAAA,EAAS,EAAG,CAAC,KAAK,CAAC,CAAA;AAC/D,EAAA,IAAI,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AAC9B,EAAA,uBACE,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,mBAAA,EACZ,eAAK,GAAA,CAAI,CAAC,GAAA,qBACT,IAAA,CAAC,SAAA,EAAA,EAAuB,SAAA,EAAU,wBAAA,EAAyB,YAAA,EAAY,IAAI,KAAA,EACzE,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAU,0BAAA,EAA4B,QAAA,EAAA,GAAA,CAAI,KAAA,EAAM,CAAA;AAAA,oBACpD,GAAA,CAAC,SAAI,SAAA,EAAU,0BAAA,EACZ,cAAI,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,qBACf,GAAA;AAAA,MAAC,UAAA;AAAA,MAAA;AAAA,QAEC,KAAA;AAAA,QACA,QAAQ,WAAA,CAAY,QAAA,EAAU,GAAA,CAAI,IAAA,EAAM,EAAE,KAAK,CAAA;AAAA,QAC/C,OAAO,CAAA,CAAE;AAAA,OAAA;AAAA,MAHJ,MAAA,CAAO,EAAE,KAAK;AAAA,KAKtB,CAAA,EACH;AAAA,GAAA,EAAA,EAXY,GAAA,CAAI,IAYlB,CACD,CAAA,EACH,CAAA;AAEJ;AAEA,SAAS,YAAA,CAAa,EAAE,KAAA,EAAO,QAAA,EAAS,EAA8C;AACpF,EAAA,MAAM,MAAM,MAAA,EAAO;AACnB,EAAA,MAAM,YAAY,KAAA,EAAM;AACxB,EAAA,MAAM,WAAA,GAAc,GAAA,CAAI,YAAA,CAAa,KAAA,CAAM,EAAE,CAAA;AAC7C,EAAA,uBACE,IAAA,CAAC,SAAA,EAAA,EAAQ,SAAA,EAAU,gBAAA,EAAiB,mBAAiB,SAAA,EACnD,QAAA,EAAA;AAAA,oBAAA,IAAA,CAAC,QAAA,EAAA,EAAO,WAAU,sBAAA,EAChB,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,QAAG,EAAA,EAAI,SAAA,EAAW,SAAA,EAAU,sBAAA,EAC1B,gBAAM,IAAA,EACT,CAAA;AAAA,sBACA,GAAA,CAAC,MAAA,EAAA,EAAO,OAAA,EAAQ,WAAA,EAAY,IAAA,EAAK,IAAA,EAAK,OAAA,EAAS,MAAM,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,EAAE,GAAG,QAAA,EAAA,MAAA,EAEzE;AAAA,KAAA,EACF,CAAA;AAAA,IACC,8BAAc,GAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAU,6BAAA,EAA+B,uBAAY,CAAA,GAAO,IAAA;AAAA,oBAC9E,GAAA,CAAC,cAAW,KAAA,EAAc,CAAA;AAAA,IACzB,QAAA,mBAAW,GAAA,CAAC,aAAA,EAAA,EAAc,KAAA,EAAc,CAAA,GAAK;AAAA,GAAA,EAChD,CAAA;AAEJ;AAIA,SAAS,sBAAsB,OAAA,EAA8C;AAC3E,EAAA,MAAM,IAAA,uBAAW,GAAA,EAAY;AAC7B,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAY;AAC9B,EAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,IAAA,MAAM,MAAM,IAAA,CAAK,SAAA,CAAU,WAAA,CAAY,KAAA,CAAM,MAAM,CAAC,CAAA;AACpD,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA,EAAG;AACnB,IAAA,IAAA,CAAK,IAAI,GAAG,CAAA;AACZ,IAAA,KAAA,CAAM,GAAA,CAAI,MAAM,EAAE,CAAA;AAAA,EACpB;AACA,EAAA,OAAO,KAAA;AACT;AAIO,SAAS,YAAA,GAAe;AAC7B,EAAA,MAAM,MAAM,MAAA,EAAO;AACnB,EAAA,MAAM,YAAA,GAAe,OAAA,CAAQ,MAAM,qBAAA,CAAsB,GAAA,CAAI,OAAO,CAAA,EAAG,CAAC,GAAA,CAAI,OAAO,CAAC,CAAA;AACpF,EAAA,MAAM,QAAA,GAAW,GAAA,CAAI,KAAA,CAAM,KAAA,CAAM,GAAG,CAAA;AACpC,EAAA,MAAM,IAAA,GAAO,SAAS,GAAA,EAAI;AAC1B,EAAA,uBACE,IAAA,CAAC,SAAA,EAAA,EAAQ,SAAA,EAAU,UAAA,EACjB,QAAA,EAAA;AAAA,oBAAA,IAAA,CAAC,QAAA,EAAA,EAAO,WAAU,gBAAA,EACf,QAAA,EAAA;AAAA,MAAA,QAAA,CAAS,MAAA,GAAS,CAAA,mBAAI,GAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAU,kBAAkB,QAAA,EAAA,QAAA,CAAS,IAAA,CAAK,KAAK,CAAA,EAAE,CAAA,GAAO,IAAA;AAAA,sBAClF,GAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAU,iBAAA,EAAmB,QAAA,EAAA,IAAA,EAAK,CAAA;AAAA,MACrC,GAAA,CAAI,8BAAc,GAAA,CAAC,GAAA,EAAA,EAAE,WAAU,uBAAA,EAAyB,QAAA,EAAA,GAAA,CAAI,aAAY,CAAA,GAAO;AAAA,KAAA,EAClF,CAAA;AAAA,IACC,IAAI,OAAA,CAAQ,GAAA,CAAI,CAAC,KAAA,yBACf,YAAA,EAAA,EAA4B,KAAA,EAAc,QAAA,EAAU,YAAA,CAAa,IAAI,KAAA,CAAM,EAAE,CAAA,EAAA,EAA3D,KAAA,CAAM,EAAwD,CAClF;AAAA,GAAA,EACH,CAAA;AAEJ;AAGO,SAAS,aAAa,GAAA,EAA6B;AACxD,EAAA,OAAO;AAAA,IACL,OAAO,GAAA,CAAI,KAAA;AAAA,IACX,GAAI,IAAI,WAAA,KAAgB,MAAA,GAAY,EAAC,GAAI,EAAE,WAAA,EAAa,GAAA,CAAI,WAAA,EAAY;AAAA,IACxE,SAAS,GAAA,CAAI,OAAA;AAAA,IACb,SAAS,GAAA,CAAI,OAAA;AAAA,IACb,KAAA,EAAO,UAAA;AAAA,IACP,QAAA,EAAU,aAAA;AAAA,IACV,YAAA;AAAA,IACA,MAAM,GAAA,CAAI;AAAA,GACZ;AACF;AAGO,SAAS,SAAA,CAAU,EAAE,GAAA,EAAK,MAAA,EAAO,EAAyE;AAC/G,EAAA,uBACE,GAAA,CAAC,eAAA,CAAgB,QAAA,EAAhB,EAAyB,OAAO,GAAA,EAAM,QAAA,EAAA,MAAA,GAAS,MAAA,CAAO,YAAA,CAAa,GAAG,CAAC,CAAA,mBAAI,GAAA,CAAC,gBAAa,CAAA,EAAG,CAAA;AAEjG","file":"chunk-CSAXZIMV.js","sourcesContent":["import {\n f,\n isConfigBranch,\n resolveConfigSchema,\n schemaNodeAtPath,\n type Annotations,\n type BranchAnnotations,\n type ConfigBranch,\n type ConfigEntry,\n type ConfigNode,\n type ConfigSchema,\n type ConfigShape,\n type NodeOptions,\n} from '@weasel-js/labkit/config';\n\nexport interface SectionDescription {\n label: string;\n collapsed?: boolean;\n}\n\nexport type DescribedEntry =\n | {\n branch: true;\n annotations: BranchAnnotations;\n section?: SectionDescription;\n conditional: boolean;\n children: DescribedShape;\n }\n | {\n branch: false;\n kind: string | null;\n default: unknown;\n annotations: Annotations;\n section?: SectionDescription;\n conditional: boolean;\n validated: boolean;\n /** The node draws its own row with `.render`, which stays in the frame. */\n customControl: boolean;\n };\n\nexport type DescribedShape = { [key: string]: DescribedEntry };\n\n/** A story's `f.schema` as structured-clone data. */\nexport interface SchemaDescription {\n nodes: DescribedShape;\n}\n\n/** Answers the workshop has for the rebuilt schema's predicates. */\nexport interface SchemaAnswers {\n hidden(path: string, config: unknown): boolean;\n errors(path: string, config: unknown): string[];\n}\n\nconst joinPath = (at: string, key: string): string => (at === '' ? key : `${at}.${key}`);\n\nfunction describeShape(shape: ConfigShape): DescribedShape {\n const out: DescribedShape = {};\n for (const [key, entry] of Object.entries(shape)) {\n const { section, showIf } = entry.options;\n const common = {\n annotations: { ...entry.annotations },\n ...(section === undefined ? {} : { section: { ...section } }),\n conditional: showIf !== undefined,\n };\n out[key] = isConfigBranch(entry)\n ? { branch: true, ...common, children: describeShape(entry.children) }\n : {\n branch: false,\n kind: entry.kind,\n default: entry.default,\n ...common,\n validated: entry.options.validate !== undefined,\n customControl: entry.options.render !== undefined,\n };\n }\n return out;\n}\n\nexport function describeSchema(schema: ConfigSchema<unknown>): SchemaDescription {\n return { nodes: describeShape(schema.nodes) };\n}\n\nfunction rebuildShape(shape: DescribedShape, at: string, answers: SchemaAnswers): ConfigShape {\n const out: Record<string, ConfigEntry> = {};\n for (const [key, entry] of Object.entries(shape)) {\n const path = joinPath(at, key);\n const options: NodeOptions = {\n ...(entry.section === undefined ? {} : { section: entry.section }),\n ...(entry.conditional ? { showIf: (config) => !answers.hidden(path, config) } : {}),\n };\n if (entry.branch) {\n const branch: ConfigBranch = {\n children: rebuildShape(entry.children, path, answers),\n annotations: entry.annotations,\n options,\n };\n out[key] = branch;\n } else {\n const node: ConfigNode = {\n kind: entry.kind,\n default: entry.default,\n annotations: entry.annotations,\n options: entry.validated ? { ...options, validate: (_leaf, config) => answers.errors(path, config) } : options,\n };\n out[key] = node;\n }\n }\n return out;\n}\n\nexport function schemaFromDescription(\n description: SchemaDescription,\n answers: SchemaAnswers,\n): ConfigSchema<unknown> {\n return f.schema(rebuildShape(description.nodes, '', answers)) as ConfigSchema<unknown>;\n}\n\n/** Evaluate a schema's own predicates for one config — the frame's half of `answers`. */\nexport function answerSchema(\n schema: ConfigSchema<unknown>,\n config: unknown,\n): { hidden: string[]; errors: Record<string, string[]> } {\n const hidden: string[] = [];\n const errors: Record<string, string[]> = {};\n const record = config as Record<string, unknown>;\n let group: ReturnType<typeof resolveConfigSchema>['group'] | undefined;\n\n const walk = (shape: ConfigShape, at: string): void => {\n for (const [key, entry] of Object.entries(shape)) {\n const path = joinPath(at, key);\n if (entry.options.showIf && !entry.options.showIf(record)) hidden.push(path);\n if (isConfigBranch(entry)) {\n walk(entry.children, path);\n continue;\n }\n const { validate } = entry.options;\n if (!validate) continue;\n group ??= resolveConfigSchema(schema).group;\n const leaf = schemaNodeAtPath(group, path);\n if (!leaf || 'children' in leaf) continue;\n const found = validate(leaf, record);\n if (found.length > 0) errors[path] = found;\n }\n };\n walk(schema.nodes, '');\n return { hidden, errors };\n}\n","import { type Envelope, PROTOCOL_VERSION } from './messages';\n\nexport interface PortLike {\n postMessage(message: unknown): void;\n addEventListener(type: 'message', fn: (event: MessageEvent) => void): void;\n removeEventListener(type: 'message', fn: (event: MessageEvent) => void): void;\n start?(): void;\n close(): void;\n}\n\nexport interface Channel<In, Out> {\n send(msg: Out): void;\n on(fn: (msg: In) => void): () => void;\n close(): void;\n}\n\nexport type Mismatch = { reason: 'not-an-envelope' } | { reason: 'version'; version: unknown };\n\nexport interface ChannelOptions {\n onMismatch?: (mismatch: Mismatch) => void;\n}\n\nexport function openChannel<In, Out>(port: PortLike, options: ChannelOptions = {}): Channel<In, Out> {\n const listeners = new Set<(msg: In) => void>();\n const onMessage = (event: MessageEvent) => {\n const data: unknown = event.data;\n if (typeof data !== 'object' || data === null || !('msg' in data)) {\n options.onMismatch?.({ reason: 'not-an-envelope' });\n return;\n }\n const envelope = data as Partial<Envelope<In>>;\n if (envelope.v !== PROTOCOL_VERSION) {\n options.onMismatch?.({ reason: 'version', version: envelope.v });\n return;\n }\n for (const fn of listeners) fn(envelope.msg as In);\n };\n port.addEventListener('message', onMessage);\n port.start?.();\n return {\n send: (msg) => port.postMessage({ v: PROTOCOL_VERSION, msg } satisfies Envelope<Out>),\n on: (fn) => {\n listeners.add(fn);\n return () => listeners.delete(fn);\n },\n close: () => {\n port.removeEventListener('message', onMessage);\n listeners.clear();\n port.close();\n },\n };\n}\n","import { sanitize } from './ids';\nimport type { IndexEntry } from './types';\n\nconst SUFFIX = ':index';\n\n/** The name an index page goes by in the sidebar and in its trial's title. */\nexport const INDEX_NAME = 'Index';\n\n/** The id of `title`'s index page. A colon never survives `sanitize`, so no story id can collide with it. */\nexport function indexId(title: string): string {\n return `${sanitize(title)}${SUFFIX}`;\n}\n\nexport function isIndexId(id: string): boolean {\n return id.endsWith(SUFFIX);\n}\n\n/**\n * One index-page entry per title in `stories`, in the order titles first appear. It routes and runs like a story:\n * its `file` is the title's first story file, and its descriptions are the title's own.\n */\nexport function indexEntries(stories: readonly IndexEntry[]): IndexEntry[] {\n const byTitle = new Map<string, IndexEntry>();\n for (const entry of stories) {\n if (byTitle.has(entry.title)) continue;\n byTitle.set(entry.title, {\n id: indexId(entry.title),\n title: entry.title,\n name: INDEX_NAME,\n exportName: '',\n file: entry.file,\n ...(entry.componentDescription === undefined ? {} : { componentDescription: entry.componentDescription }),\n ...(entry.componentName === undefined ? {} : { componentName: entry.componentName }),\n });\n }\n return [...byTitle.values()];\n}\n","import { type ConfigSchema, type ConfigShape, isConfigBranch, titleCase } from '@weasel-js/labkit/config';\nimport { isPlainObject } from '../../csf/isPlainObject';\n\nexport interface VariantValue {\n value: unknown;\n label: string;\n}\n\n/** One control a story is shown varying along, with every other control at its default. */\nexport interface VariantRow {\n /** Dotted, as `setConfig` takes it. */\n path: string;\n label: string;\n values: VariantValue[];\n}\n\n/** Above this many choices a row stops being something to scan. */\nexport const MAX_ENUM_OPTIONS = 8;\nexport const MAX_ROWS = 6;\n\nconst labelOf = (key: string, name: string | undefined): string => name ?? titleCase(key);\n\n/**\n * The boolean and enum controls of `schema` a story can be shown varying along, in schema order: skipped are\n * controls that draw their own row, controls hidden at the defaults, and enums with more than\n * `MAX_ENUM_OPTIONS` choices. At most `MAX_ROWS`.\n */\nexport function variantRows(schema: ConfigSchema<unknown>): VariantRow[] {\n const defaults = schema.defaults() as Record<string, unknown>;\n const rows: VariantRow[] = [];\n const walk = (shape: ConfigShape, at: string): void => {\n for (const [key, entry] of Object.entries(shape)) {\n if (rows.length >= MAX_ROWS) return;\n const path = at === '' ? key : `${at}.${key}`;\n if (entry.options.showIf && !entry.options.showIf(defaults)) continue;\n if (isConfigBranch(entry)) {\n walk(entry.children, path);\n continue;\n }\n if (entry.annotations.hidden || entry.options.render) continue;\n const label = labelOf(key, entry.annotations.name);\n if (entry.kind === 'boolean') {\n rows.push({ path, label, values: [false, true].map((value) => ({ value, label: String(value) })) });\n } else if (entry.kind === 'enum') {\n const options = entry.annotations.options ?? [];\n if (options.length < 2 || options.length > MAX_ENUM_OPTIONS) continue;\n rows.push({ path, label, values: options.map((o) => ({ value: o.value, label: o.label })) });\n }\n }\n };\n walk(schema.nodes, '');\n return rows;\n}\n\n/** `over` merged into `base`: nested plain objects merge, anything else replaces. */\nexport function mergeConfig(base: unknown, over: unknown): unknown {\n if (!isPlainObject(base) || !isPlainObject(over)) return over === undefined ? base : over;\n const out: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(over)) out[key] = mergeConfig(base[key], value);\n return out;\n}\n\n/** `config` with `value` at dotted `path`, copying each object along the way. */\nexport function withValueAt(config: unknown, path: string, value: unknown): unknown {\n const [head, ...rest] = path.split('.');\n if (head === undefined) return value;\n const record = isPlainObject(config) ? config : {};\n return { ...record, [head]: rest.length === 0 ? value : withValueAt(record[head], rest.join('.'), value) };\n}\n","// The component's own entry, not the barrel: the barrel would bring every weasel-ui component into each frame.\nimport { Button } from '@weasel-js/ui/components/Button';\nimport { createContext, type ReactNode, type RefObject, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';\nimport type { Globals } from '../../protocol/messages';\nimport type { Decorator, IndexContext, IndexStoryProps, LoadedStory, StoryContext } from '../../story/types';\nimport { StoryHost } from '../StoryHost';\nimport { mergeConfig, variantRows, withValueAt } from './variants';\n\n/** What every cell on an index page shares with the frame that hosts it. */\nexport interface IndexEnv {\n title: string;\n description?: string;\n stories: readonly LoadedStory[];\n /** Per story id, the JSDoc above its export. */\n descriptions: Readonly<Record<string, string>>;\n globals: Globals;\n /** The frame config's, outermost. */\n decorators: readonly Decorator[];\n open: (id: string) => void;\n onError: (error: unknown) => void;\n}\n\nexport const IndexEnvContext = createContext<IndexEnv | null>(null);\n\nfunction useEnv(): IndexEnv {\n const env = useContext(IndexEnvContext);\n if (!env) throw new Error('An index page part rendered outside its index page');\n return env;\n}\n\n/** How far past the viewport a cell stays mounted. */\nconst NEAR_MARGIN = '100%';\n\n/** Whether `ref`'s element is near the viewport; true where the browser cannot say. */\nfunction useNearViewport(ref: RefObject<Element | null>): boolean {\n const [near, setNear] = useState(typeof IntersectionObserver === 'undefined');\n useEffect(() => {\n const el = ref.current;\n if (!el || typeof IntersectionObserver === 'undefined') return;\n const observer = new IntersectionObserver((entries) => {\n const last = entries.at(-1);\n if (last) setNear(last.isIntersecting);\n }, { rootMargin: NEAR_MARGIN });\n observer.observe(el);\n return () => observer.disconnect();\n }, [ref]);\n return near;\n}\n\n/**\n * One story at its defaults with `config` over them. It keeps its own config and state, so it stays interactive, and\n * it mounts only while near the viewport, so a page of canvas stories stays under the browser's WebGL context cap.\n */\nexport function IndexStory({ story, config: over, label }: IndexStoryProps) {\n const env = useEnv();\n const [config, setConfig] = useState(() => mergeConfig(story.config.defaults(), over));\n const [state, setState] = useState<unknown>(() => story.initialState?.(config) ?? null);\n const cell = useRef<HTMLElement>(null);\n const near = useNearViewport(cell);\n // Held while unmounted, so scrolling past a cell does not move everything after it.\n const [height, setHeight] = useState<number | null>(null);\n useEffect(() => {\n const el = cell.current;\n if (!near || !el || typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(() => setHeight(el.getBoundingClientRect().height));\n observer.observe(el);\n return () => observer.disconnect();\n }, [near]);\n const ctx: StoryContext = {\n config,\n setConfig: (path, value) => setConfig((prev: unknown) => withValueAt(prev, path, value)),\n state,\n setState: (next) =>\n setState((prev: unknown) => (typeof next === 'function' ? (next as (p: unknown) => unknown)(prev) : next)),\n globals: env.globals,\n title: story.title,\n name: story.name,\n };\n return (\n <figure\n ref={cell}\n className={`fg-index-cell fg-index-cell--${story.layout}`}\n style={!near && height !== null ? { minHeight: height } : undefined}\n >\n <div className=\"fg-index-cell__stage\">\n {near ? (\n <StoryHost story={story} ctx={ctx} decorators={env.decorators} resetKey={0} onError={env.onError} />\n ) : null}\n </div>\n {label === undefined ? null : <figcaption className=\"fg-index-cell__label\">{label}</figcaption>}\n </figure>\n );\n}\n\n/** `story` once per value of each boolean and enum control, every other control at its default. */\nexport function IndexVariants({ story }: { story: LoadedStory }) {\n const rows = useMemo(() => variantRows(story.config), [story]);\n const defaults = useMemo(() => story.config.defaults(), [story]);\n if (rows.length === 0) return null;\n return (\n <div className=\"fg-index-variants\">\n {rows.map((row) => (\n <section key={row.path} className=\"fg-index-variants__row\" aria-label={row.label}>\n <h4 className=\"fg-index-variants__label\">{row.label}</h4>\n <div className=\"fg-index-variants__cells\">\n {row.values.map((v) => (\n <IndexStory\n key={String(v.value)}\n story={story}\n config={withValueAt(defaults, row.path, v.value) as Record<string, unknown>}\n label={v.label}\n />\n ))}\n </div>\n </section>\n ))}\n </div>\n );\n}\n\nfunction StorySection({ story, variants }: { story: LoadedStory; variants: boolean }) {\n const env = useEnv();\n const headingId = useId();\n const description = env.descriptions[story.id];\n return (\n <section className=\"fg-index-story\" aria-labelledby={headingId}>\n <header className=\"fg-index-story__head\">\n <h2 id={headingId} className=\"fg-index-story__name\">\n {story.name}\n </h2>\n <Button variant=\"secondary\" size=\"sm\" onClick={() => env.open(story.id)}>\n Open\n </Button>\n </header>\n {description ? <p className=\"fg-index-story__description\">{description}</p> : null}\n <IndexStory story={story} />\n {variants ? <IndexVariants story={story} /> : null}\n </section>\n );\n}\n\n/** Which of `stories` show their variants: the first of each set of controls. Stories of one CSF file share their\n * meta's controls, and the same rows under every story would only repeat. */\nfunction firstOfEachControlSet(stories: readonly LoadedStory[]): Set<string> {\n const seen = new Set<string>();\n const shown = new Set<string>();\n for (const story of stories) {\n const key = JSON.stringify(variantRows(story.config));\n if (seen.has(key)) continue;\n seen.add(key);\n shown.add(story.id);\n }\n return shown;\n}\n\n/** The generated page: the component's title and description, then each story, with the variants of the first of\n * each set of controls. */\nexport function DefaultIndex() {\n const env = useEnv();\n const withVariants = useMemo(() => firstOfEachControlSet(env.stories), [env.stories]);\n const segments = env.title.split('/');\n const name = segments.pop();\n return (\n <article className=\"fg-index\">\n <header className=\"fg-index__head\">\n {segments.length > 0 ? <p className=\"fg-index__path\">{segments.join(' / ')}</p> : null}\n <h1 className=\"fg-index__title\">{name}</h1>\n {env.description ? <p className=\"fg-index__description\">{env.description}</p> : null}\n </header>\n {env.stories.map((story) => (\n <StorySection key={story.id} story={story} variants={withVariants.has(story.id)} />\n ))}\n </article>\n );\n}\n\n/** What a component's own index page is handed. */\nexport function indexContext(env: IndexEnv): IndexContext {\n return {\n title: env.title,\n ...(env.description === undefined ? {} : { description: env.description }),\n stories: env.stories,\n globals: env.globals,\n Story: IndexStory,\n Variants: IndexVariants,\n DefaultIndex,\n open: env.open,\n };\n}\n\n/** The page itself: the component's own, or the generated one. */\nexport function IndexPage({ env, render }: { env: IndexEnv; render: ((ctx: IndexContext) => ReactNode) | null }) {\n return (\n <IndexEnvContext.Provider value={env}>{render ? render(indexContext(env)) : <DefaultIndex />}</IndexEnvContext.Provider>\n );\n}\n"]}