@weasel-js/forge 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +72 -10
  2. package/dist/{FrameController-eZM3hlcE.d.ts → FrameController-BspC9Zr5.d.ts} +61 -4
  3. package/dist/chunk-A4JOEGYQ.js +325 -0
  4. package/dist/chunk-A4JOEGYQ.js.map +1 -0
  5. package/dist/{chunk-5YHEO26U.js → chunk-FWPBKKYB.js} +58 -240
  6. package/dist/chunk-FWPBKKYB.js.map +1 -0
  7. package/dist/{chunk-6SRQIHJQ.js → chunk-HRGPFQBW.js} +3 -3
  8. package/dist/{chunk-6SRQIHJQ.js.map → chunk-HRGPFQBW.js.map} +1 -1
  9. package/dist/chunk-PO5JLVYT.js +318 -0
  10. package/dist/chunk-PO5JLVYT.js.map +1 -0
  11. package/dist/{chunk-TLO6UHF2.js → chunk-VEKEBILL.js} +17 -4
  12. package/dist/chunk-VEKEBILL.js.map +1 -0
  13. package/dist/{config-CSVP6duU.d.ts → config-DU_WuORS.d.ts} +7 -1
  14. package/dist/csf/preview-api.js +1 -2
  15. package/dist/csf/preview-api.js.map +1 -1
  16. package/dist/frame/index.css +102 -0
  17. package/dist/frame/index.css.map +1 -1
  18. package/dist/frame/index.d.ts +11 -3
  19. package/dist/frame/index.js +365 -5
  20. package/dist/frame/index.js.map +1 -1
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/shell/index.css +96 -19
  24. package/dist/shell/index.css.map +1 -1
  25. package/dist/shell/index.d.ts +75 -15
  26. package/dist/shell/index.js +762 -214
  27. package/dist/shell/index.js.map +1 -1
  28. package/dist/test/index.d.ts +3 -3
  29. package/dist/test/index.js +67 -72
  30. package/dist/test/index.js.map +1 -1
  31. package/dist/vite/index.js +43 -5
  32. package/dist/vite/index.js.map +1 -1
  33. package/package.json +5 -4
  34. package/dist/chunk-2JDYOBWM.js +0 -118
  35. package/dist/chunk-2JDYOBWM.js.map +0 -1
  36. package/dist/chunk-5YHEO26U.js.map +0 -1
  37. package/dist/chunk-FTFR2YPM.js +0 -13
  38. package/dist/chunk-FTFR2YPM.js.map +0 -1
  39. package/dist/chunk-TLO6UHF2.js.map +0 -1
@@ -1,77 +1,19 @@
1
- import { schemaFromDescription, openChannel } from '../chunk-2JDYOBWM.js';
2
- import { stableStringify, PORT_HANDOFF, PROTOCOL_VERSION } from '../chunk-FTFR2YPM.js';
1
+ import { isIndexId, indexId, indexEntries, schemaFromDescription, openChannel, IndexPage } from '../chunk-PO5JLVYT.js';
2
+ import { useTrialFrame, createTrialFrames, labGlobals, StoryGlobalsContext, TrialFramesContext, withGlobals, StoryTrial, storyConfig, GLOBALS_KEY, effectiveGlobals, useCssOverrides, TrialHost, isGlobalsPath } from '../chunk-A4JOEGYQ.js';
3
+ export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-A4JOEGYQ.js';
4
+ import { loadStories, indexRenderOf } from '../chunk-FWPBKKYB.js';
5
+ import '../chunk-2GXKNXTJ.js';
6
+ import '../chunk-HRGPFQBW.js';
7
+ import { stableStringify, PORT_HANDOFF, FRAME_HELLO, PROTOCOL_VERSION } from '../chunk-VEKEBILL.js';
3
8
  import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, inferTokenType, ToggleBar as ToggleBar$1, Input, TokenPanel, refitScale } from '@weasel-js/labkit';
4
- import { createContext, useContext, useCallback, useSyncExternalStore, useState, useRef, useEffect, useMemo, useLayoutEffect, useId } from 'react';
9
+ import { createContext, useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect, useId, useContext } from 'react';
5
10
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
11
  import { createRoot } from 'react-dom/client';
7
- import { InfoIcon, ToggleBar, Checkbox, Dialog } from '@weasel-js/ui';
8
- import { TOKEN_MANIFEST, THEME_SOURCES } from '@weasel-js/theme';
9
- import { scale } from '@weasel-js/theme/engine';
12
+ import { InfoIcon, ToggleBar, Checkbox, Dialog, Badge, DisclosureMark, Button as Button$1, TuneIcon, Callout } from '@weasel-js/ui';
13
+ import { TOKEN_MANIFEST, THEME_SOURCES, isByAxis } from '@weasel-js/theme';
14
+ import { scale, httpThemeApi } from '@weasel-js/theme/engine';
10
15
  import { f, valueAtPath, isConfigBranch } from '@weasel-js/labkit/config';
11
16
 
12
- var NO_FRAME = { send: null, vars: [], audit: null, a11y: null, capture: null, size: null };
13
- function createTrialFrames() {
14
- const frames = /* @__PURE__ */ new Map();
15
- const hosts = /* @__PURE__ */ new Map();
16
- const listeners = /* @__PURE__ */ new Set();
17
- const changed = () => {
18
- for (const listener of [...listeners]) listener();
19
- };
20
- const patch = (trialId, next) => {
21
- const frame = frames.get(trialId);
22
- if (!frame) return;
23
- frames.set(trialId, { ...frame, ...next });
24
- changed();
25
- };
26
- return {
27
- connect(trialId, calls) {
28
- const held = frames.get(trialId);
29
- frames.set(trialId, {
30
- send: calls.send,
31
- vars: [],
32
- audit: calls.audit,
33
- a11y: null,
34
- capture: calls.capture,
35
- size: held?.size ?? null
36
- });
37
- changed();
38
- return () => {
39
- if (frames.get(trialId)?.send !== calls.send) return;
40
- frames.delete(trialId);
41
- changed();
42
- };
43
- },
44
- report: (trialId, vars) => patch(trialId, { vars }),
45
- reportA11y: (trialId, a11y) => patch(trialId, { a11y }),
46
- reportSize(trialId, size) {
47
- const frame = frames.get(trialId) ?? NO_FRAME;
48
- if (frame.size?.width === size.width && frame.size?.height === size.height) return;
49
- frames.set(trialId, { ...frame, size });
50
- changed();
51
- },
52
- hostRef(trialId) {
53
- let ref = hosts.get(trialId);
54
- if (!ref) {
55
- ref = { current: null };
56
- hosts.set(trialId, ref);
57
- }
58
- return ref;
59
- },
60
- get: (trialId) => frames.get(trialId) ?? NO_FRAME,
61
- subscribe(listener) {
62
- listeners.add(listener);
63
- return () => listeners.delete(listener);
64
- }
65
- };
66
- }
67
- var TrialFramesContext = createContext(null);
68
- function useTrialFrame(trialId) {
69
- const frames = useContext(TrialFramesContext);
70
- const subscribe = useCallback((listener) => frames?.subscribe(listener) ?? (() => {
71
- }), [frames]);
72
- const read = () => frames?.get(trialId) ?? NO_FRAME;
73
- return useSyncExternalStore(subscribe, read, read);
74
- }
75
17
  function Finding({ finding }) {
76
18
  return /* @__PURE__ */ jsxs("li", { className: "fg-a11y__finding", children: [
77
19
  /* @__PURE__ */ jsxs("p", { className: "fg-a11y__help", children: [
@@ -149,17 +91,137 @@ var A11Y_SECTION = {
149
91
  region: "aside",
150
92
  item: { title: "Accessibility", body: /* @__PURE__ */ jsx(FocusedA11y, {}) }
151
93
  };
152
- var NONE = {};
153
- function useCssOverrides() {
154
- return usePersistedState("fg-css-overrides", NONE, { scope: "trial" });
94
+
95
+ // src/shell/storyChanges.ts
96
+ function createStoryChanges() {
97
+ const listeners = /* @__PURE__ */ new Set();
98
+ return {
99
+ subscribe(listener) {
100
+ listeners.add(listener);
101
+ return () => listeners.delete(listener);
102
+ },
103
+ emit(file) {
104
+ for (const listener of [...listeners]) listener(file);
105
+ }
106
+ };
107
+ }
108
+ var SEED_REF = /^\{seeds\.([^}]+)\}$/;
109
+ var RULE_KEYS = { factors: "factors", ratio: "ratio", step: "step" };
110
+ function selectionFor(axes, globals) {
111
+ const out = {};
112
+ for (const [axis, def] of Object.entries(axes)) {
113
+ const value = globals[axis];
114
+ if (value === void 0) out[axis] = def.default;
115
+ else if (typeof value === "string" && value in def.values) out[axis] = value;
116
+ }
117
+ return out;
118
+ }
119
+ var EditError = class extends Error {
120
+ };
121
+ function slotOf(root, holder, key, where, selection, depth = 0) {
122
+ if (depth > 16) throw new EditError(`${where} references itself`);
123
+ let h = holder;
124
+ let k = key;
125
+ let w = where;
126
+ let value = h[k];
127
+ while (isByAxis(value)) {
128
+ const axis = value.by;
129
+ const picked = selection[axis];
130
+ if (picked === void 0 || picked === "by" || !(picked in value)) {
131
+ throw new EditError(`${w} differs by ${axis}, and which ${axis} this trial shows is not known`);
132
+ }
133
+ h = value;
134
+ k = picked;
135
+ w = `${w}.${picked}`;
136
+ value = h[k];
137
+ }
138
+ if (typeof value === "number") return { holder: h, key: k, where: w };
139
+ if (typeof value === "string") {
140
+ const ref = SEED_REF.exec(value);
141
+ if (!ref) throw new EditError(`${w} is "${value}", which is not a number or a seed`);
142
+ const name = ref[1];
143
+ const seeds = root.seeds;
144
+ if (!seeds || !(name in seeds)) throw new EditError(`${w} names the seed "${name}", which this theme inherits rather than defines`);
145
+ return slotOf(root, seeds, name, `seeds.${name}`, selection, depth + 1);
146
+ }
147
+ throw new EditError(`${w} is not a number`);
148
+ }
149
+ function factorsOf(holder, where, selection) {
150
+ let value = holder.factors;
151
+ let w = `${where}.factors`;
152
+ while (isByAxis(value)) {
153
+ const picked = selection[value.by];
154
+ if (picked === void 0 || !(picked in value)) {
155
+ throw new EditError(`${w} differs by ${value.by}, and which ${value.by} this trial shows is not known`);
156
+ }
157
+ w = `${w}.${picked}`;
158
+ value = value[picked];
159
+ }
160
+ if (!Array.isArray(value)) throw new EditError(`${w} is not a list`);
161
+ return { list: value, where: w };
162
+ }
163
+ var kindOf = (scale) => scale.factors !== void 0 ? "factors" : scale.ratio !== void 0 ? "ratio" : scale.step !== void 0 ? "step" : null;
164
+ var plainLiteral = (value) => typeof value === "number" || Array.isArray(value) && value.every((v) => typeof v === "number");
165
+ function swapRule(scale, from, to, value) {
166
+ const entries = Object.entries(scale);
167
+ for (const [k] of entries) delete scale[k];
168
+ for (const [k, v] of entries) scale[k === from ? to : k] = k === from ? value : v;
169
+ if (from === null) scale[to] = value;
170
+ }
171
+ function applyScaleEdits(definition, edits, selection) {
172
+ const root = structuredClone(definition);
173
+ const writes = /* @__PURE__ */ new Map();
174
+ const write = (slot, value) => {
175
+ let byKey = writes.get(slot.holder);
176
+ if (!byKey) writes.set(slot.holder, byKey = /* @__PURE__ */ new Map());
177
+ const pending = byKey.get(slot.key);
178
+ if (pending !== void 0 && pending !== value) throw new EditError(`${slot.where} would need to be both ${pending} and ${value}`);
179
+ byKey.set(slot.key, value);
180
+ };
181
+ try {
182
+ for (const [name, edit] of Object.entries(edits)) {
183
+ const scales = root.scales;
184
+ const scale = scales?.[name];
185
+ if (!scales || !scale) throw new EditError(`the theme defines no scale "${name}"`);
186
+ const where = `scales.${name}`;
187
+ write(slotOf(root, scale, "base", `${where}.base`, selection), edit.base);
188
+ const { rule } = edit;
189
+ const had = kindOf(scale);
190
+ if (had !== rule.kind) {
191
+ const from = had ? RULE_KEYS[had] : null;
192
+ if (from && !plainLiteral(scale[from])) {
193
+ throw new EditError(`${where} would lose its ${from} to become a ${rule.kind} scale, and its ${from} is not a plain number`);
194
+ }
195
+ const value = rule.kind === "factors" ? [...rule.factors] : rule.kind === "ratio" ? rule.ratio : rule.step;
196
+ swapRule(scale, from, RULE_KEYS[rule.kind], value);
197
+ continue;
198
+ }
199
+ if (rule.kind === "factors") {
200
+ const { list, where: at } = factorsOf(scale, where, selection);
201
+ if (list.length !== rule.factors.length) {
202
+ throw new EditError(`${at} has ${list.length} factors, and the edit ${rule.factors.length}`);
203
+ }
204
+ rule.factors.forEach((factor, i) => write(slotOf(root, list, i, `${at}.${i}`, selection), factor));
205
+ } else {
206
+ const key = RULE_KEYS[rule.kind];
207
+ write(slotOf(root, scale, key, `${where}.${key}`, selection), rule.kind === "ratio" ? rule.ratio : rule.step);
208
+ }
209
+ }
210
+ } catch (e) {
211
+ if (e instanceof EditError) return { ok: false, message: `Cannot save: ${e.message}.` };
212
+ throw e;
213
+ }
214
+ for (const [holder, byKey] of writes) for (const [key, value] of byKey) holder[key] = value;
215
+ return { ok: true, definition: root };
155
216
  }
156
217
  var TABS = [
157
218
  { value: "theme", label: "Theme" },
158
219
  { value: "story", label: "Story" }
159
220
  ];
160
221
  var MANIFEST = new Map(TOKEN_MANIFEST.map((token) => [token.name, token]));
222
+ var THEME_NAME = "weasel";
161
223
  var literal = (v) => typeof v === "number";
162
- var SCALE_SOURCES = Object.entries(THEME_SOURCES.weasel?.scales ?? {}).flatMap(([name, def]) => {
224
+ var SCALE_SOURCES = Object.entries(THEME_SOURCES[THEME_NAME]?.scales ?? {}).flatMap(([name, def]) => {
163
225
  if (!Array.isArray(def.steps)) return [];
164
226
  const steps = def.steps;
165
227
  const tokens = steps.map((step) => `--wzl-${name}-${step}`);
@@ -169,6 +231,18 @@ var SCALE_SOURCES = Object.entries(THEME_SOURCES.weasel?.scales ?? {}).flatMap((
169
231
  const rule = def.factors !== void 0 ? factors ? { kind: "factors", factors } : "factors" : def.ratio !== void 0 ? literal(def.ratio) ? { kind: "ratio", ratio: def.ratio } : "ratio" : literal(def.step) ? { kind: "step", step: def.step } : "step";
170
232
  return [{ name, group, steps, tokens, rule }];
171
233
  });
234
+ function scaleValues(source, next) {
235
+ const { rule } = next;
236
+ const values = scale(source.steps, {
237
+ base: next.base,
238
+ ...rule.kind === "factors" ? { factors: rule.factors } : rule.kind === "ratio" ? { ratio: rule.ratio } : { step: rule.step }
239
+ });
240
+ return source.steps.flatMap((step, i) => {
241
+ const value = values[step];
242
+ const name = source.tokens[i];
243
+ return value !== void 0 && name !== void 0 ? [[name, value]] : [];
244
+ });
245
+ }
172
246
  function standingScale(source, amounts) {
173
247
  const seed = { tokens: source.tokens, base: amounts[0] ?? 0, rule: { kind: "step", step: 0 } };
174
248
  if (typeof source.rule === "string") return refitScale(seed, source.rule, amounts);
@@ -185,7 +259,8 @@ function groupOf(name) {
185
259
  const segments = name.replace(/^--/, "").split("-");
186
260
  return segments.length > 1 ? segments.slice(0, -1).join("-") : name.replace(/^--/, "");
187
261
  }
188
- function CssVarsPanel() {
262
+ var NO_GLOBALS = {};
263
+ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
189
264
  const trialId = useTrialId();
190
265
  const frame = useTrialFrame(trialId);
191
266
  const [overrides, setOverrides] = useCssOverrides();
@@ -246,19 +321,63 @@ function CssVarsPanel() {
246
321
  const source = SCALE_SOURCES.find((sc) => sc.group === group);
247
322
  if (!source) return;
248
323
  setEdited((prev) => ({ ...prev, [group]: next }));
249
- const { rule } = next;
250
- const values = scale(source.steps, {
251
- base: next.base,
252
- ...rule.kind === "factors" ? { factors: rule.factors } : rule.kind === "ratio" ? { ratio: rule.ratio } : { step: rule.step }
253
- });
254
- source.steps.forEach((step, i) => {
255
- const value = values[step];
256
- const name = source.tokens[i];
257
- if (value !== void 0 && name !== void 0) write(name, value);
258
- });
324
+ for (const [name, value] of scaleValues(source, next)) write(name, value);
259
325
  },
260
326
  [setEdited, write]
261
327
  );
328
+ const unsaved = useMemo(
329
+ () => SCALE_SOURCES.flatMap((source) => {
330
+ const edit = edited[source.group];
331
+ if (!edit || !source.tokens.some((name) => overrides[name] !== void 0)) return [];
332
+ const values = scaleValues(source, edit);
333
+ return values.every(([name, value]) => overrides[name] === value) ? [{ source, edit }] : [];
334
+ }),
335
+ [edited, overrides]
336
+ );
337
+ const [saving, setSaving] = useState(false);
338
+ const [held, setHeld] = useState(null);
339
+ const notice = held?.trialId === trialId ? held : null;
340
+ const setNotice = (next) => setHeld(next && { ...next, trialId });
341
+ const save = async () => {
342
+ setSaving(true);
343
+ setNotice(null);
344
+ try {
345
+ const api = themeApi ?? httpThemeApi();
346
+ const stored = await api.get(THEME_NAME);
347
+ const file = `themes/${THEME_NAME}.json`;
348
+ if (JSON.stringify(stored.definition) !== JSON.stringify(THEME_SOURCES[THEME_NAME])) {
349
+ setNotice({ tone: "error", text: `${file} has changed since this page loaded. Reload to edit against it.` });
350
+ return;
351
+ }
352
+ const edits = Object.fromEntries(
353
+ unsaved.map(({ source, edit }) => [source.name, { base: edit.base, rule: edit.rule }])
354
+ );
355
+ const next = applyScaleEdits(stored.definition, edits, selectionFor(stored.definition.axes ?? {}, globals));
356
+ if (!next.ok) {
357
+ setNotice({ tone: "error", text: next.message });
358
+ return;
359
+ }
360
+ const result = await api.put(THEME_NAME, next.definition, stored.hash);
361
+ if (result.status === "conflict") {
362
+ setNotice({ tone: "error", text: `${file} changed on disk while saving. Nothing was written; reload to edit against it.` });
363
+ return;
364
+ }
365
+ if (result.status === "invalid") {
366
+ setNotice({ tone: "error", text: result.message });
367
+ return;
368
+ }
369
+ for (const { source } of unsaved) for (const name of source.tokens) write(name, null);
370
+ const groups = new Set(unsaved.map(({ source }) => source.group));
371
+ setEdited((prev) => Object.fromEntries(Object.entries(prev).filter(([group]) => !groups.has(group))));
372
+ setNotice(
373
+ result.regenerated || result.problems.length === 0 ? { tone: "saved", text: `Saved to ${file}.` } : { tone: "error", text: `Saved to ${file}, but the tokens were not regenerated: ${result.problems.join("; ")}` }
374
+ );
375
+ } catch (e) {
376
+ setNotice({ tone: "error", text: `Saving needs forge's dev server, which did not answer: ${e.message}` });
377
+ } finally {
378
+ setSaving(false);
379
+ }
380
+ };
262
381
  const onCollapsedChange = useCallback(
263
382
  (category, collapsed) => setFolds((prev) => ({ ...prev, [category]: collapsed })),
264
383
  [setFolds]
@@ -277,7 +396,18 @@ function CssVarsPanel() {
277
396
  }
278
397
  }
279
398
  ),
280
- /* @__PURE__ */ jsx(Input, { "aria-label": "Filter", placeholder: "Filter by name or value", value: filter, onChange: setFilter })
399
+ /* @__PURE__ */ jsx(Input, { "aria-label": "Filter", placeholder: "Filter by name or value", value: filter, onChange: setFilter }),
400
+ tab === "theme" && (unsaved.length > 0 || notice) ? /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__save", children: [
401
+ unsaved.length > 0 ? /* @__PURE__ */ jsx(Button$1, { variant: "primary", size: "sm", loading: saving, disabled: saving, onClick: () => void save(), children: "Save scales to theme" }) : null,
402
+ notice ? /* @__PURE__ */ jsx(
403
+ "p",
404
+ {
405
+ className: `fg-css-vars__notice fg-css-vars__notice--${notice.tone}`,
406
+ role: notice.tone === "error" ? "alert" : "status",
407
+ children: notice.text
408
+ }
409
+ ) : null
410
+ ] }) : null
281
411
  ] }),
282
412
  shown.length === 0 ? /* @__PURE__ */ jsx("p", { className: "fg-css-vars__empty", children: tokens.length === 0 ? "The story\u2019s frame has reported no variables." : "No variables match." }) : /* @__PURE__ */ jsx(
283
413
  TokenPanel,
@@ -294,12 +424,14 @@ function CssVarsPanel() {
294
424
  }
295
425
  function FocusedCssVars() {
296
426
  const { focusedTrialId, trials, instruments } = useLabContext();
427
+ const labGlobals2 = useContext(StoryGlobalsContext);
297
428
  const record = trials.find((trial) => trial.id === focusedTrialId);
298
429
  if (!record) return null;
430
+ const pins = record.config?.[GLOBALS_KEY];
299
431
  const instrument = instruments.find((i) => i.name === record.instrumentName);
300
432
  return /* @__PURE__ */ jsxs("section", { className: "fg-css-vars-focus", "aria-label": "CSS Vars", children: [
301
433
  /* @__PURE__ */ jsx("p", { className: "fg-css-vars__trial", children: record.title ?? instrument?.title ?? record.instrumentName }),
302
- /* @__PURE__ */ jsx(TrialIdContext.Provider, { value: record.id, children: /* @__PURE__ */ jsx(CssVarsPanel, {}) })
434
+ /* @__PURE__ */ jsx(TrialIdContext.Provider, { value: record.id, children: /* @__PURE__ */ jsx(CssVarsPanel, { globals: effectiveGlobals(labGlobals2, pins) }) })
303
435
  ] });
304
436
  }
305
437
  var CSS_VARS_SECTION = {
@@ -308,9 +440,15 @@ var CSS_VARS_SECTION = {
308
440
  item: { title: "CSS Vars", body: /* @__PURE__ */ jsx(FocusedCssVars, {}) }
309
441
  };
310
442
 
443
+ // src/shell/breadcrumb.ts
444
+ function breadcrumb(title, name) {
445
+ const segments = [...title.split("/"), name].filter((segment, i) => i === 0 || segment.toLowerCase() !== "index");
446
+ return segments.join(" > ");
447
+ }
448
+
311
449
  // src/shell/tree/buildComponents.ts
312
450
  var LIBRARIES = [
313
- ["/packages/ui/", "weasel-ui"],
451
+ ["/packages/ui/", "ui"],
314
452
  ["/packages/labkit/", "labkit"],
315
453
  ["/packages/forge/", "forge"],
316
454
  ["/apps/draw/", "draw"],
@@ -333,6 +471,7 @@ function buildComponents(index) {
333
471
  path: entry.title,
334
472
  label: segments[segments.length - 1],
335
473
  library: libraryOf(entry),
474
+ index: indexEntries([entry])[0],
336
475
  entries: []
337
476
  };
338
477
  byTitle.set(entry.title, row);
@@ -348,9 +487,10 @@ function buildComponents(index) {
348
487
  function disambiguate(rows) {
349
488
  const byLabel = /* @__PURE__ */ new Map();
350
489
  for (const row of rows) {
351
- const group = byLabel.get(row.label);
490
+ const key = `${row.library}\0${row.label}`;
491
+ const group = byLabel.get(key);
352
492
  if (group) group.push(row);
353
- else byLabel.set(row.label, [row]);
493
+ else byLabel.set(key, [row]);
354
494
  }
355
495
  for (const group of byLabel.values()) {
356
496
  if (group.length < 2) continue;
@@ -373,20 +513,39 @@ function filterComponents(rows, query) {
373
513
  continue;
374
514
  }
375
515
  const entries = row.entries.filter((entry) => entry.name.toLowerCase().includes(needle));
376
- if (entries.length > 0) out.push({ ...row, entries });
516
+ if (entries.length > 0) out.push({ path: row.path, label: row.label, library: row.library, entries });
377
517
  }
378
518
  return out;
379
519
  }
380
520
  function componentNodes(rows) {
381
- return rows.map(
382
- (row) => row.entries.length === 1 ? { kind: "story", entry: row.entries[0], tag: row.library, label: row.label } : {
521
+ const root = [];
522
+ const folders = /* @__PURE__ */ new Map();
523
+ for (const row of rows) {
524
+ const labels = row.label.split("/");
525
+ const title = row.path.split("/");
526
+ let siblings = root;
527
+ let tag = row.library;
528
+ for (let i = 0; i < labels.length - 1; i++) {
529
+ const path = title.slice(0, title.length - labels.length + i + 1).join("/");
530
+ let folder = folders.get(path);
531
+ if (!folder) {
532
+ folder = { kind: "folder", label: labels[i], path, tag, children: [] };
533
+ folders.set(path, folder);
534
+ siblings.push(folder);
535
+ }
536
+ siblings = folder.children;
537
+ tag = void 0;
538
+ }
539
+ siblings.push({
383
540
  kind: "folder",
384
- label: row.label,
541
+ label: labels[labels.length - 1],
385
542
  path: row.path,
386
- tag: row.library,
543
+ tag,
544
+ ...row.index ? { index: row.index } : {},
387
545
  children: row.entries.map((entry) => ({ kind: "story", entry }))
388
- }
389
- );
546
+ });
547
+ }
548
+ return root;
390
549
  }
391
550
  function librariesIn(index) {
392
551
  return [...new Set(index.map(libraryOf))].sort((a, b) => a.localeCompare(b));
@@ -501,51 +660,73 @@ function StoryInfoDialog({ index, isReady, isOpen, onOpenChange }) {
501
660
  {
502
661
  isOpen,
503
662
  onOpenChange,
504
- title: `${dossier.title} / ${dossier.name}`,
663
+ title: breadcrumb(dossier.title, dossier.name),
505
664
  className: "fg-info-dialog",
506
665
  children: /* @__PURE__ */ jsx(StoryDossier, { dossier })
507
666
  }
508
667
  );
509
668
  }
510
- var GLOBALS_KEY = "$globals";
511
- var FOLLOW_LAB = "lab";
512
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
513
- var isGlobalsPath = (path) => path === GLOBALS_KEY || path.startsWith(`${GLOBALS_KEY}.`);
514
- function labGlobals(declarations, stored) {
515
- const from = isRecord(stored) ? stored : {};
516
- return Object.fromEntries(
517
- Object.entries(declarations).map(([key, declaration]) => {
518
- const value = from[key];
519
- return [key, declaration.options.some((option) => option.value === value) ? value : declaration.default];
520
- })
521
- );
669
+ function canMoveFrames() {
670
+ return typeof Element !== "undefined" && "moveBefore" in Element.prototype;
522
671
  }
523
- function effectiveGlobals(lab, pins) {
524
- if (!isRecord(pins)) return lab;
525
- const pinned = Object.entries(pins).filter(([key, value]) => key in lab && value !== FOLLOW_LAB);
526
- return pinned.length === 0 ? lab : { ...lab, ...Object.fromEntries(pinned) };
527
- }
528
- function storyConfig(config, previous) {
529
- if (!isRecord(config) || !(GLOBALS_KEY in config)) return config;
530
- const rest = Object.fromEntries(Object.entries(config).filter(([key]) => key !== GLOBALS_KEY));
531
- if (isRecord(previous)) {
532
- const keys = Object.keys(rest);
533
- if (keys.length === Object.keys(previous).length && keys.every((key) => rest[key] === previous[key])) return previous;
534
- }
535
- return rest;
672
+ function moveFrame(host, frame) {
673
+ host.moveBefore(frame, host.firstChild);
536
674
  }
537
- function withGlobals(schema, declarations) {
538
- const entries = Object.entries(declarations);
539
- if (entries.length === 0) return schema;
540
- const leaves2 = Object.fromEntries(
541
- entries.map(([key, declaration]) => [
542
- key,
543
- f.enum(FOLLOW_LAB, [{ value: FOLLOW_LAB, label: "Lab" }, ...declaration.options]).label(declaration.label)
544
- ])
545
- );
546
- const group = f.group(leaves2).label("").section("Globals", { layout: "inline", pack: "pairs" });
547
- return f.schema({ ...schema.nodes, [GLOBALS_KEY]: group });
675
+ var whenIdle = (fn) => {
676
+ if (typeof requestIdleCallback === "function") {
677
+ const handle2 = requestIdleCallback(fn, { timeout: 500 });
678
+ return () => cancelIdleCallback(handle2);
679
+ }
680
+ const handle = setTimeout(fn, 200);
681
+ return () => clearTimeout(handle);
682
+ };
683
+ function createFramePool(frameUrl, size = 2) {
684
+ const shelf = document.createElement("div");
685
+ shelf.className = "fg-frame-pool";
686
+ shelf.setAttribute("aria-hidden", "true");
687
+ document.body.append(shelf);
688
+ const warm = [];
689
+ let loading = 0;
690
+ let cancelRefill = null;
691
+ let disposed = false;
692
+ const load = () => {
693
+ loading += 1;
694
+ const frame = document.createElement("iframe");
695
+ frame.tabIndex = -1;
696
+ frame.src = frameUrl;
697
+ const onMessage = (event) => {
698
+ if (event.source !== frame.contentWindow || event.origin !== location.origin) return;
699
+ if (event.data?.type !== FRAME_HELLO) return;
700
+ window.removeEventListener("message", onMessage);
701
+ loading -= 1;
702
+ warm.push(frame);
703
+ };
704
+ window.addEventListener("message", onMessage);
705
+ shelf.append(frame);
706
+ };
707
+ const refill = () => {
708
+ if (disposed || cancelRefill) return;
709
+ cancelRefill = whenIdle(() => {
710
+ cancelRefill = null;
711
+ while (!disposed && warm.length + loading < size) load();
712
+ });
713
+ };
714
+ if (canMoveFrames()) refill();
715
+ return {
716
+ claim() {
717
+ if (!canMoveFrames()) return null;
718
+ const frame = warm.shift() ?? null;
719
+ refill();
720
+ return frame;
721
+ },
722
+ dispose() {
723
+ disposed = true;
724
+ cancelRefill?.();
725
+ shelf.remove();
726
+ }
727
+ };
548
728
  }
729
+ var FramePoolContext = createContext(null);
549
730
  var RECORD = "fg-globals";
550
731
  function useLabGlobals(declarations) {
551
732
  const [stored, setStored] = usePersistedState(RECORD, null, { scope: "lab" });
@@ -556,19 +737,65 @@ function useLabGlobals(declarations) {
556
737
  );
557
738
  return [values, set];
558
739
  }
559
- function GlobalsToolbar({ declarations }) {
560
- const [values, set] = useLabGlobals(declarations);
561
- return /* @__PURE__ */ jsx("div", { className: "fg-globals", role: "toolbar", "aria-label": "Globals", children: Object.entries(declarations).map(([key, declaration]) => /* @__PURE__ */ jsx(
740
+ function GlobalSelect({ name, declaration, values, set, labeled = false }) {
741
+ return /* @__PURE__ */ jsx(
562
742
  Select,
563
743
  {
564
- "aria-label": declaration.label,
565
- width: "fit",
566
- selectedKey: String(values[key]),
567
- onSelectionChange: (value) => set(key, value),
744
+ ...labeled ? { label: declaration.label } : { "aria-label": declaration.label },
745
+ width: labeled ? "fill" : "fit",
746
+ selectedKey: String(values[name]),
747
+ onSelectionChange: (value) => set(name, value),
568
748
  options: declaration.options
569
- },
570
- key
571
- )) });
749
+ }
750
+ );
751
+ }
752
+ function GlobalsToolbar({ declarations }) {
753
+ const [values, set] = useLabGlobals(declarations);
754
+ const entries = Object.entries(declarations);
755
+ return /* @__PURE__ */ jsx("div", { className: "fg-globals", role: "toolbar", "aria-label": "Globals", children: entries.filter(([, declaration]) => declaration.under === void 0).map(([key, declaration]) => {
756
+ const nested = entries.filter(([, other]) => other.under === key);
757
+ return /* @__PURE__ */ jsxs("div", { className: "fg-globals__item", children: [
758
+ /* @__PURE__ */ jsx(GlobalSelect, { name: key, declaration, values, set }),
759
+ nested.length > 0 ? /* @__PURE__ */ jsx(NestedGlobals, { label: declaration.label, nested, values, set }) : null
760
+ ] }, key);
761
+ }) });
762
+ }
763
+ function NestedGlobals({
764
+ label,
765
+ nested,
766
+ values,
767
+ set
768
+ }) {
769
+ const anchor = useRef(null);
770
+ const [open, setOpen] = useState(false);
771
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
772
+ /* @__PURE__ */ jsx(
773
+ Button$1,
774
+ {
775
+ ref: anchor,
776
+ variant: "ghost",
777
+ size: "sm",
778
+ iconOnly: true,
779
+ ariaLabel: `More ${label} settings`,
780
+ pressed: open,
781
+ onClick: () => setOpen((was) => !was),
782
+ children: /* @__PURE__ */ jsx(TuneIcon, { size: 16 })
783
+ }
784
+ ),
785
+ /* @__PURE__ */ jsx(
786
+ Callout,
787
+ {
788
+ triggerRef: anchor,
789
+ isOpen: open,
790
+ onOpenChange: setOpen,
791
+ placement: "bottom end",
792
+ showCloseButton: false,
793
+ title: label,
794
+ className: "fg-globals__popover",
795
+ children: nested.map(([key, declaration]) => /* @__PURE__ */ jsx(GlobalSelect, { name: key, declaration, values, set, labeled: true }, key))
796
+ }
797
+ )
798
+ ] });
572
799
  }
573
800
  function LabGlobals({
574
801
  declarations,
@@ -578,7 +805,6 @@ function LabGlobals({
578
805
  useEffect(() => onChange(values), [values, onChange]);
579
806
  return null;
580
807
  }
581
- var StoryGlobalsContext = createContext({});
582
808
 
583
809
  // src/shell/revealTrial.ts
584
810
  var FLASH_MS = 600;
@@ -605,33 +831,51 @@ function readRoute() {
605
831
  if (!hash.startsWith("#/")) return null;
606
832
  return decodeURIComponent(hash.slice(2)) || null;
607
833
  }
834
+ function readRouteEntry() {
835
+ const state = history.state;
836
+ const entry = state?.forgeRoute;
837
+ return typeof entry?.step === "number" ? entry : null;
838
+ }
839
+ function crossesInPlace(from, to) {
840
+ if (!from || !to || from.step === to.step) return false;
841
+ return to.step < from.step ? from.inPlace : to.inPlace;
842
+ }
843
+ function stamp(entry, hash) {
844
+ history.replaceState({ ...history.state ?? {}, forgeRoute: entry }, "", hash);
845
+ }
846
+ function setRoute(id, options = {}) {
847
+ const hash = `#/${encodeURIComponent(id)}`;
848
+ if (readRoute() === id) {
849
+ history.replaceState(history.state, "", hash);
850
+ return;
851
+ }
852
+ const from = readRouteEntry() ?? { step: 0, inPlace: false };
853
+ stamp(from, location.hash);
854
+ location.hash = hash;
855
+ stamp({ step: from.step + 1, inPlace: options.inPlace ?? false }, hash);
856
+ }
608
857
  function useRoute() {
609
- const [route, setRoute] = useState(readRoute);
858
+ const [route, setCurrent] = useState(readRoute);
610
859
  useEffect(() => {
611
- const sync = () => setRoute(readRoute());
860
+ const sync = () => setCurrent(readRoute());
612
861
  window.addEventListener("hashchange", sync);
613
862
  sync();
614
863
  return () => window.removeEventListener("hashchange", sync);
615
864
  }, []);
616
- const go = useCallback((id) => {
617
- const hash = `#/${encodeURIComponent(id)}`;
618
- if (readRoute() === id) history.replaceState(null, "", hash);
619
- else location.hash = hash;
620
- }, []);
621
- return [route, go];
865
+ return [route, setRoute];
622
866
  }
623
867
 
624
868
  // src/shell/tree/buildTree.ts
625
869
  function sortFolder(nodes) {
626
870
  const folders = nodes.filter((n) => n.kind === "folder");
627
- const stories = nodes.filter((n) => n.kind === "story");
628
871
  folders.sort((a, b) => a.label.localeCompare(b.label));
629
872
  for (const folder of folders) folder.children = sortFolder(folder.children);
630
- return [...folders, ...stories];
873
+ return [...folders, ...nodes.filter((n) => n.kind === "story")];
631
874
  }
632
875
  function buildTree(index) {
633
876
  const root = [];
634
877
  const folders = /* @__PURE__ */ new Map();
878
+ const pages = new Map(indexEntries(index).map((page) => [page.title, page]));
635
879
  for (const entry of index) {
636
880
  let siblings = root;
637
881
  let path = "";
@@ -639,7 +883,8 @@ function buildTree(index) {
639
883
  path = path ? `${path}/${label}` : label;
640
884
  let folder = folders.get(path);
641
885
  if (!folder) {
642
- folder = { kind: "folder", label, path, children: [] };
886
+ const page = pages.get(path);
887
+ folder = { kind: "folder", label, path, children: [], ...page ? { index: page } : {} };
643
888
  folders.set(path, folder);
644
889
  siblings.push(folder);
645
890
  }
@@ -657,7 +902,9 @@ function filterTree(nodes, query) {
657
902
  return `${node.entry.title}/${node.entry.name}`.toLowerCase().includes(needle) ? [node] : [];
658
903
  }
659
904
  const children = walk(node.children);
660
- return children.length > 0 ? [{ ...node, children }] : [];
905
+ if (children.length === 0) return [];
906
+ const { index: page, ...folder } = node;
907
+ return [page && node.path.toLowerCase().includes(needle) ? { ...folder, children, index: page } : { ...folder, children }];
661
908
  });
662
909
  return walk(nodes);
663
910
  }
@@ -676,12 +923,21 @@ function ancestorsOf(entry) {
676
923
  }
677
924
  return paths;
678
925
  }
926
+ var LIBRARY_TONES = {
927
+ ui: "color-mix(in oklab, var(--wzl-swatch-blue), var(--wzl-fg) 35%)",
928
+ labkit: "color-mix(in oklab, var(--wzl-swatch-teal), var(--wzl-fg) 45%)",
929
+ forge: "color-mix(in oklab, var(--wzl-swatch-amber), var(--wzl-fg) 40%)",
930
+ draw: "color-mix(in oklab, var(--wzl-swatch-rose), var(--wzl-fg) 35%)"
931
+ };
932
+ function LibraryBadge({ library }) {
933
+ return /* @__PURE__ */ jsx(Badge, { status: "muted", tone: LIBRARY_TONES[library], variant: "subtle", size: "sm", className: "fg-tree__tag", children: library });
934
+ }
679
935
  function StoryTree({ ctx, index }) {
680
936
  const headingId = useId();
681
- const [route, setRoute] = useRoute();
937
+ const [route, setRoute2] = useRoute();
682
938
  const [query, setQuery] = useState("");
683
939
  const [folds, setFolds] = usePersistedState("fg-tree-open", {}, { scope: "lab" });
684
- const [view, setView] = usePersistedState("fg-tree-view", "tree", { scope: "lab" });
940
+ const [view, setView] = usePersistedState("fg-tree-view", "components", { scope: "lab" });
685
941
  const [hidden, setHidden] = usePersistedState("fg-tree-libraries", {}, { scope: "lab" });
686
942
  const [focusKey, setFocusKey] = useState(null);
687
943
  const items = useRef(/* @__PURE__ */ new Map());
@@ -693,10 +949,11 @@ function StoryTree({ ctx, index }) {
693
949
  () => view === "components" ? componentNodes(filterComponents(components, query)) : filterTree(tree, query),
694
950
  [view, components, tree, query]
695
951
  );
696
- const routed = kept.find((entry) => entry.id === route);
952
+ const routable = useMemo(() => [...kept, ...indexEntries(kept)], [kept]);
953
+ const routed = routable.find((entry) => entry.id === route);
697
954
  const routeAncestors = useMemo(() => ancestorsOf(routed), [routed]);
698
955
  const filtering = query.trim() !== "";
699
- const isOpen = (path) => filtering || (folds[path] ?? (view === "tree" && routeAncestors.has(path)));
956
+ const isOpen = (path) => filtering || (folds[path] ?? routeAncestors.has(path));
700
957
  const rows = [];
701
958
  const collect = (nodes, parent) => {
702
959
  for (const node of nodes) {
@@ -719,8 +976,22 @@ function StoryTree({ ctx, index }) {
719
976
  const focused = ctx.trials.find((trial) => trial.id === ctx.focusedTrialId);
720
977
  if (another || !focused) ctx.addTrial(entry.id);
721
978
  else if (focused.instrumentName === entry.id) revealTrial(focused.id);
722
- else ctx.swapTrial(focused.id, entry.id);
723
- setRoute(entry.id);
979
+ else {
980
+ ctx.swapTrial(focused.id, entry.id);
981
+ setRoute2(entry.id, { inPlace: true });
982
+ return;
983
+ }
984
+ setRoute2(entry.id);
985
+ };
986
+ const openFolder = (node, another) => {
987
+ if (!node.index) {
988
+ setOpen(node.path, !isOpen(node.path));
989
+ return;
990
+ }
991
+ if (folds[node.path] === false) {
992
+ setFolds(({ [node.path]: _dropped, ...rest }) => rest);
993
+ }
994
+ activate(node.index, another);
724
995
  };
725
996
  const onKeyDown = (event) => {
726
997
  const at = rows.findIndex((row2) => row2.key === active);
@@ -751,8 +1022,8 @@ function StoryTree({ ctx, index }) {
751
1022
  break;
752
1023
  case "Enter":
753
1024
  case " ":
754
- if (node.kind === "folder") setOpen(node.path, !isOpen(node.path));
755
- else activate(node.entry, event.shiftKey);
1025
+ if (node.kind === "story") activate(node.entry, event.shiftKey);
1026
+ else openFolder(node, event.shiftKey);
756
1027
  break;
757
1028
  default:
758
1029
  return;
@@ -784,12 +1055,35 @@ function StoryTree({ ctx, index }) {
784
1055
  ...itemProps(node, level),
785
1056
  "aria-expanded": open,
786
1057
  "aria-label": node.label,
1058
+ "aria-current": node.index && openIds.has(node.index.id) ? "true" : void 0,
787
1059
  className: "fg-tree__node",
788
1060
  children: [
789
- /* @__PURE__ */ jsxs("div", { className: "fg-tree__item fg-tree__folder", onClick: () => setOpen(node.path, !open), children: [
790
- /* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: node.label }),
791
- node.tag ? /* @__PURE__ */ jsx("span", { className: "fg-tree__tag", children: node.tag }) : null
792
- ] }),
1061
+ /* @__PURE__ */ jsxs(
1062
+ "div",
1063
+ {
1064
+ className: "fg-tree__item fg-tree__folder",
1065
+ onClick: (event) => {
1066
+ setFocusKey(keyOf(node));
1067
+ openFolder(node, event.shiftKey);
1068
+ },
1069
+ children: [
1070
+ /* @__PURE__ */ jsx(
1071
+ "span",
1072
+ {
1073
+ "aria-hidden": "true",
1074
+ className: "fg-tree__fold",
1075
+ onClick: (event) => {
1076
+ event.stopPropagation();
1077
+ setOpen(node.path, !open);
1078
+ },
1079
+ children: /* @__PURE__ */ jsx(DisclosureMark, { open })
1080
+ }
1081
+ ),
1082
+ /* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: node.label }),
1083
+ node.tag ? /* @__PURE__ */ jsx(LibraryBadge, { library: node.tag }) : null
1084
+ ]
1085
+ }
1086
+ ),
793
1087
  open ? /* @__PURE__ */ jsx("div", { role: "group", className: "fg-tree__group", children: renderNodes(node.children, level + 1) }) : null
794
1088
  ]
795
1089
  },
@@ -812,7 +1106,7 @@ function StoryTree({ ctx, index }) {
812
1106
  },
813
1107
  children: [
814
1108
  /* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: node.label ?? entry.name }),
815
- node.tag ? /* @__PURE__ */ jsx("span", { className: "fg-tree__tag", children: node.tag }) : null
1109
+ node.tag ? /* @__PURE__ */ jsx(LibraryBadge, { library: node.tag }) : null
816
1110
  ]
817
1111
  },
818
1112
  keyOf(node)
@@ -843,7 +1137,7 @@ function StoryTree({ ctx, index }) {
843
1137
  {
844
1138
  isSelected: !hidden[library],
845
1139
  onChange: (on) => setHidden((prev) => ({ ...prev, [library]: !on })),
846
- children: library
1140
+ children: /* @__PURE__ */ jsx(LibraryBadge, { library })
847
1141
  },
848
1142
  library
849
1143
  ))
@@ -905,10 +1199,90 @@ function createAnswerBook() {
905
1199
  }
906
1200
  };
907
1201
  }
1202
+ function IndexTrial({ bundle, setup, config }) {
1203
+ const lab = useLabContext();
1204
+ const trialId = useContext(TrialIdContext);
1205
+ const open = (id) => {
1206
+ if (!trialId || !lab.instruments.some((instrument) => instrument.name === id)) return;
1207
+ lab.swapTrial(trialId, id);
1208
+ setRoute(id, { inPlace: true });
1209
+ };
1210
+ const render = (globals) => {
1211
+ const env = {
1212
+ title: bundle.title,
1213
+ ...bundle.description === void 0 ? {} : { description: bundle.description },
1214
+ stories: bundle.stories,
1215
+ descriptions: bundle.descriptions,
1216
+ globals,
1217
+ decorators: setup.decorators ?? [],
1218
+ open,
1219
+ // A story that throws shows its error in its own cell; the rest of the page is still worth reading.
1220
+ onError: (error) => console.error(error)
1221
+ };
1222
+ return /* @__PURE__ */ jsx(IndexPage, { env, render: bundle.render });
1223
+ };
1224
+ return /* @__PURE__ */ jsx(TrialHost, { layout: "fullscreen", setup, config, children: render });
1225
+ }
1226
+ function annotationsOn(frames) {
1227
+ return {
1228
+ targets: (_state, _config, trial) => {
1229
+ const ref = frames.hostRef(trial.id);
1230
+ const frame = frames.get(trial.id);
1231
+ const box = frame.size ?? { width: ref.current?.clientWidth ?? 0, height: ref.current?.clientHeight ?? 0 };
1232
+ const capture = frame.capture;
1233
+ return [
1234
+ {
1235
+ id: "story",
1236
+ ref,
1237
+ content: { w: Math.max(1, box.width), h: Math.max(1, box.height) },
1238
+ ...capture ? { base: () => capture() } : {}
1239
+ }
1240
+ ];
1241
+ }
1242
+ };
1243
+ }
1244
+ function documentInstrument(options) {
1245
+ const { entry, loaded, setup, globals = {}, frames } = options;
1246
+ const schema = loaded.kind === "story" ? loaded.story.config : f.schema({});
1247
+ const config = withGlobals(schema, globals);
1248
+ const initialState = loaded.kind === "story" ? loaded.story.initialState : null;
1249
+ return {
1250
+ name: entry.id,
1251
+ title: breadcrumb(entry.title, entry.name),
1252
+ config,
1253
+ defaultConfig: () => config.defaults(),
1254
+ initialState: (c) => initialState?.(c) ?? null,
1255
+ render: (ctx) => loaded.kind === "story" ? /* @__PURE__ */ jsx(StoryTrial, { story: loaded.story, setup, ctx }) : /* @__PURE__ */ jsx(IndexTrial, { bundle: loaded.bundle, setup, config: ctx.config }),
1256
+ ...frames ? { annotations: annotationsOn(frames) } : {},
1257
+ ...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {}
1258
+ };
1259
+ }
1260
+ function StoryLoad({ entry, load, fault }) {
1261
+ useEffect(() => {
1262
+ if (!fault) load(entry);
1263
+ }, [entry, load, fault]);
1264
+ return /* @__PURE__ */ jsx("div", { className: "fg-story fg-story--loading", "data-fg-layout": "fullscreen", children: fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
1265
+ /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: "import" }),
1266
+ /* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
1267
+ ] }) : null });
1268
+ }
1269
+ function pendingInstrument(options) {
1270
+ const { entry, globals = {} } = options;
1271
+ const config = withGlobals(f.schema({}), globals);
1272
+ return {
1273
+ name: entry.id,
1274
+ title: breadcrumb(entry.title, entry.name),
1275
+ config,
1276
+ defaultConfig: () => config.defaults(),
1277
+ initialState: () => null,
1278
+ render: () => /* @__PURE__ */ jsx(StoryLoad, { ...options }, options.generation ?? 0)
1279
+ };
1280
+ }
908
1281
 
909
1282
  // src/shell/readyKey.ts
910
1283
  var readyKey = (ready) => stableStringify({ schema: ready.schema, layout: ready.layout, viewport: ready.viewport });
911
1284
  var START_TIMEOUT_MS = 1e4;
1285
+ var HELLO_TIMEOUT_MS = 3e4;
912
1286
  var requests = 0;
913
1287
  var IN_VIEW_MARGIN = "50%";
914
1288
  function useInView(ref) {
@@ -952,13 +1326,17 @@ function FrameView(props) {
952
1326
  const trialId = useContext(TrialIdContext);
953
1327
  const frames = useContext(TrialFramesContext);
954
1328
  const [overrides] = useCssOverrides();
1329
+ const pool = useContext(FramePoolContext);
1330
+ const lab = useLabContext();
955
1331
  const src = `${frameUrl}#${entry.id}`;
1332
+ const title = breadcrumb(entry.title, entry.name);
956
1333
  const iframeRef = useRef(null);
957
1334
  const hostRef = useRef(null);
1335
+ const slotRef = useRef(null);
958
1336
  const inView = useInView(hostRef);
959
1337
  const link = useRef(null);
960
- const latest = useRef({ ...props, globals, overrides });
961
- latest.current = { ...props, globals, overrides };
1338
+ const latest = useRef({ ...props, globals, overrides, pool, lab, title });
1339
+ latest.current = { ...props, globals, overrides, pool, lab, title };
962
1340
  const [fault, setFault] = useState(null);
963
1341
  const [pending, setPending] = useState(true);
964
1342
  const init = (current) => {
@@ -1015,6 +1393,13 @@ function FrameView(props) {
1015
1393
  else current.pending.get(msg.id)?.fail(new Error(msg.message));
1016
1394
  current.pending.delete(msg.id);
1017
1395
  break;
1396
+ case "open": {
1397
+ const { lab: liveLab } = latest.current;
1398
+ if (!trialId || !liveLab.instruments.some((instrument) => instrument.name === msg.id)) break;
1399
+ liveLab.swapTrial(trialId, msg.id);
1400
+ setRoute(msg.id, { inPlace: true });
1401
+ break;
1402
+ }
1018
1403
  case "fault":
1019
1404
  if (msg.phase === "render" && msg.seq !== void 0 && msg.seq < current.inputs) break;
1020
1405
  clearTimeout(current.timer);
@@ -1022,10 +1407,10 @@ function FrameView(props) {
1022
1407
  break;
1023
1408
  }
1024
1409
  };
1025
- const onLoad = () => {
1410
+ const connect = (frame) => {
1026
1411
  closeLink(link);
1027
1412
  setPending(true);
1028
- const target = iframeRef.current?.contentWindow;
1413
+ const target = frame.contentWindow;
1029
1414
  if (!target) return;
1030
1415
  const { port1, port2 } = new MessageChannel();
1031
1416
  const timer = setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), START_TIMEOUT_MS);
@@ -1077,15 +1462,53 @@ function FrameView(props) {
1077
1462
  };
1078
1463
  link.current = current;
1079
1464
  channel.on((msg) => receive(current, msg));
1080
- target.postMessage({ type: PORT_HANDOFF }, location.origin, [port2]);
1465
+ target.postMessage({ type: PORT_HANDOFF, id: latest.current.entry.id }, location.origin, [
1466
+ port2
1467
+ ]);
1081
1468
  };
1082
- useEffect(() => () => closeLink(link), []);
1469
+ const connectRef = useRef(connect);
1470
+ connectRef.current = connect;
1083
1471
  useEffect(() => {
1084
- if (inView) return;
1085
- closeLink(link);
1086
- setPending(true);
1087
- setFault(null);
1088
- }, [inView]);
1472
+ const slot = slotRef.current;
1473
+ if (!inView || !slot) return;
1474
+ const claimed = latest.current.pool?.claim() ?? null;
1475
+ const frame = claimed ?? document.createElement("iframe");
1476
+ frame.className = "fg-frame-view";
1477
+ frame.title = latest.current.title;
1478
+ frame.removeAttribute("tabindex");
1479
+ frame.toggleAttribute("data-pending", true);
1480
+ iframeRef.current = frame;
1481
+ const unheard = claimed ? void 0 : setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), HELLO_TIMEOUT_MS);
1482
+ const onMessage = (event) => {
1483
+ if (event.source !== frame.contentWindow || event.origin !== location.origin) return;
1484
+ if (event.data?.type !== FRAME_HELLO) return;
1485
+ clearTimeout(unheard);
1486
+ connectRef.current(frame);
1487
+ };
1488
+ window.addEventListener("message", onMessage);
1489
+ if (claimed) {
1490
+ moveFrame(slot, claimed);
1491
+ connectRef.current(claimed);
1492
+ } else {
1493
+ frame.src = src;
1494
+ slot.append(frame);
1495
+ }
1496
+ return () => {
1497
+ window.removeEventListener("message", onMessage);
1498
+ clearTimeout(unheard);
1499
+ closeLink(link);
1500
+ frame.remove();
1501
+ iframeRef.current = null;
1502
+ setPending(true);
1503
+ setFault(null);
1504
+ };
1505
+ }, [inView, src]);
1506
+ useEffect(() => {
1507
+ iframeRef.current?.toggleAttribute("data-pending", pending && !fault);
1508
+ }, [pending, fault, inView, src]);
1509
+ useEffect(() => {
1510
+ if (iframeRef.current) iframeRef.current.title = title;
1511
+ }, [title]);
1089
1512
  useEffect(() => answers.hold(ctx.config), [answers, ctx.config]);
1090
1513
  useEffect(() => {
1091
1514
  const current = link.current;
@@ -1127,17 +1550,7 @@ function FrameView(props) {
1127
1550
  },
1128
1551
  className: "fg-frame-host",
1129
1552
  children: [
1130
- inView ? /* @__PURE__ */ jsx(
1131
- "iframe",
1132
- {
1133
- ref: iframeRef,
1134
- className: "fg-frame-view",
1135
- src,
1136
- title: `${entry.title} / ${entry.name}`,
1137
- onLoad,
1138
- "data-pending": pending && !fault ? "" : void 0
1139
- }
1140
- ) : null,
1553
+ /* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
1141
1554
  fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
1142
1555
  fault.phase ? /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: fault.phase }) : null,
1143
1556
  /* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
@@ -1146,7 +1559,7 @@ function FrameView(props) {
1146
1559
  }
1147
1560
  );
1148
1561
  }
1149
- function annotationsOn(frames) {
1562
+ function annotationsOn2(frames) {
1150
1563
  return {
1151
1564
  targets: (_state, _config, trial) => {
1152
1565
  const ref = frames.hostRef(trial.id);
@@ -1173,7 +1586,7 @@ function storyInstrument(options) {
1173
1586
  const descriptionKey = ready ? readyKey(ready) : null;
1174
1587
  return {
1175
1588
  name: entry.id,
1176
- title: `${entry.title} / ${entry.name}`,
1589
+ title: breadcrumb(entry.title, entry.name),
1177
1590
  config,
1178
1591
  defaultConfig: () => config.defaults(),
1179
1592
  initialState: () => null,
@@ -1188,17 +1601,22 @@ function storyInstrument(options) {
1188
1601
  ctx
1189
1602
  }
1190
1603
  ),
1191
- ...frames ? { annotations: annotationsOn(frames) } : {},
1604
+ ...frames ? { annotations: annotationsOn2(frames) } : {},
1192
1605
  ...ready?.viewport ? { stage: { size: { width: ready.viewport.width, height: ready.viewport.height } } } : {}
1193
1606
  };
1194
1607
  }
1195
1608
 
1196
1609
  // src/shell/useStoryRegistry.ts
1197
- var NO_GLOBALS = {};
1610
+ var NO_GLOBALS2 = {};
1611
+ var NO_SETUP = {};
1198
1612
  var pruned = (map, ids) => [...map.keys()].every((id) => ids.has(id)) ? map : new Map([...map].filter(([id]) => ids.has(id)));
1613
+ var toError = (error) => error instanceof Error ? error : new Error(String(error));
1199
1614
  function useStoryRegistry(index, options) {
1200
- const { frameUrl, globals = NO_GLOBALS, frames } = options;
1615
+ const { frameUrl, globals = NO_GLOBALS2, frames, importers, setup = NO_SETUP, load = loadStories } = options;
1201
1616
  const [readies, setReadies] = useState(() => /* @__PURE__ */ new Map());
1617
+ const [loaded, setLoaded] = useState(() => /* @__PURE__ */ new Map());
1618
+ const [faults, setFaults] = useState(() => /* @__PURE__ */ new Map());
1619
+ const [generations, setGenerations] = useState(() => /* @__PURE__ */ new Map());
1202
1620
  const [revisions, setRevisions] = useState(() => /* @__PURE__ */ new Map());
1203
1621
  const onReady = useCallback((entry, ready) => {
1204
1622
  setReadies((prev) => {
@@ -1210,8 +1628,93 @@ function useStoryRegistry(index, options) {
1210
1628
  useEffect(() => {
1211
1629
  const ids = new Set(index.map((entry) => entry.id));
1212
1630
  setReadies((prev) => pruned(prev, ids));
1631
+ setLoaded((prev) => pruned(prev, ids));
1632
+ setFaults((prev) => pruned(prev, ids));
1213
1633
  setRevisions((prev) => pruned(prev, ids));
1214
1634
  }, [index]);
1635
+ const imports = useRef(/* @__PURE__ */ new Map());
1636
+ const live = useRef({ index, importers, setup, load });
1637
+ live.current = { index, importers, setup, load };
1638
+ const importOf = useCallback((file) => {
1639
+ let loading = imports.current.get(file);
1640
+ if (!loading) {
1641
+ const importer = live.current.importers?.[file];
1642
+ loading = importer ? importer() : Promise.reject(new Error(`No importer for ${file}`));
1643
+ imports.current.set(file, loading);
1644
+ }
1645
+ return loading;
1646
+ }, []);
1647
+ const request = useCallback(
1648
+ (entry) => {
1649
+ const { setup: current, load: loadModule } = live.current;
1650
+ const settle2 = (ids2, result) => {
1651
+ setLoaded((prev) => {
1652
+ const next = new Map(prev);
1653
+ for (const [id, value] of result) next.set(id, value);
1654
+ return next;
1655
+ });
1656
+ setFaults((prev) => ids2.some((id) => prev.has(id)) ? pruned(prev, new Set([...prev.keys()].filter((id) => !ids2.includes(id)))) : prev);
1657
+ };
1658
+ const fail = (ids2, error) => {
1659
+ const fault = toError(error);
1660
+ setFaults((prev) => {
1661
+ const next = new Map(prev);
1662
+ for (const id of ids2) next.set(id, fault);
1663
+ return next;
1664
+ });
1665
+ };
1666
+ if (isIndexId(entry.id)) {
1667
+ const entries = live.current.index.filter((e) => !isIndexId(e.id) && indexId(e.title) === entry.id);
1668
+ const files = [...new Set(entries.map((e) => e.file))];
1669
+ void Promise.all(files.map(importOf)).then(async (mods) => {
1670
+ const byFile = new Map(files.map((file, i) => [file, loadModule(mods[i] ?? {}, entry.title, current.parameters)]));
1671
+ const stories = entries.flatMap(
1672
+ (e) => byFile.get(e.file)?.find((s) => s.exportName === e.exportName) ?? []
1673
+ );
1674
+ for (const story of stories) await current.prepare?.(story);
1675
+ const result = /* @__PURE__ */ new Map();
1676
+ result.set(entry.id, {
1677
+ kind: "index",
1678
+ bundle: {
1679
+ title: entry.title,
1680
+ ...entry.componentDescription === void 0 ? {} : { description: entry.componentDescription },
1681
+ stories,
1682
+ descriptions: Object.fromEntries(entries.flatMap((e) => e.description ? [[e.id, e.description]] : [])),
1683
+ render: mods.map(indexRenderOf).find((render) => render !== null) ?? null
1684
+ }
1685
+ });
1686
+ settle2([entry.id], result);
1687
+ }).catch((error) => fail([entry.id], error));
1688
+ return;
1689
+ }
1690
+ const siblings = live.current.index.filter((e) => e.file === entry.file && !isIndexId(e.id));
1691
+ const ids = siblings.map((e) => e.id);
1692
+ void importOf(entry.file).then(async (mod) => {
1693
+ const stories = loadModule(mod, entry.title, current.parameters);
1694
+ const result = /* @__PURE__ */ new Map();
1695
+ for (const sibling of siblings) {
1696
+ const story = stories.find((s) => s.exportName === sibling.exportName);
1697
+ if (!story) continue;
1698
+ await current.prepare?.(story);
1699
+ result.set(sibling.id, { kind: "story", story });
1700
+ }
1701
+ if (!result.has(entry.id)) throw new Error(`${entry.file} has no story export "${entry.exportName}"`);
1702
+ settle2(ids, result);
1703
+ }).catch((error) => fail(ids, error));
1704
+ },
1705
+ [importOf]
1706
+ );
1707
+ const reload = useCallback(
1708
+ (file) => {
1709
+ imports.current.delete(file);
1710
+ const affected = live.current.index.filter((e) => e.file === file || isIndexId(e.id) && live.current.index.some((s) => s.file === file && indexId(s.title) === e.id));
1711
+ const ids = new Set(affected.map((e) => e.id));
1712
+ setLoaded((prev) => pruned(prev, new Set([...prev.keys()].filter((id) => !ids.has(id)))));
1713
+ setFaults((prev) => pruned(prev, new Set([...prev.keys()].filter((id) => !ids.has(id)))));
1714
+ setGenerations((prev) => new Map(prev).set(file, (prev.get(file) ?? 0) + 1));
1715
+ },
1716
+ []
1717
+ );
1215
1718
  const committed = useRef({ built: /* @__PURE__ */ new Map(), books: /* @__PURE__ */ new Map(), list: [] });
1216
1719
  const cache = useMemo(() => {
1217
1720
  const previous = committed.current;
@@ -1219,26 +1722,35 @@ function useStoryRegistry(index, options) {
1219
1722
  const books = /* @__PURE__ */ new Map();
1220
1723
  const list = index.map((entry) => {
1221
1724
  const ready = readies.get(entry.id);
1725
+ const inDocument = importers !== void 0 && entry.isolate === void 0;
1726
+ const had = loaded.get(entry.id);
1727
+ const fault = faults.get(entry.id);
1728
+ const generation = generations.get(entry.file) ?? 0;
1222
1729
  const revision = revisions.get(entry.id) ?? 0;
1223
1730
  const entryKey = stableStringify(entry);
1224
1731
  const answers = previous.books.get(entry.id) ?? createAnswerBook();
1225
1732
  books.set(entry.id, answers);
1226
1733
  const held = previous.built.get(entry.id);
1227
- const kept = held && held.entryKey === entryKey && held.ready === ready && held.frameUrl === frameUrl && held.globals === globals && held.frames === frames && held.revision === revision ? held : {
1734
+ const kept = held && held.entryKey === entryKey && held.ready === ready && held.loaded === had && held.fault === fault && held.frameUrl === frameUrl && held.globals === globals && held.frames === frames && held.setup === setup && held.inDocument === inDocument && held.generation === generation && held.revision === revision ? held : {
1228
1735
  entryKey,
1229
1736
  ready,
1737
+ loaded: had,
1738
+ fault,
1230
1739
  frameUrl,
1231
1740
  globals,
1232
1741
  frames,
1742
+ setup,
1743
+ inDocument,
1744
+ generation,
1233
1745
  revision,
1234
- instrument: storyInstrument({ entry, ready, answers, frameUrl, onReady, globals, ...frames ? { frames } : {} })
1746
+ instrument: !inDocument ? storyInstrument({ entry, ready, answers, frameUrl, onReady, globals, ...frames ? { frames } : {} }) : had ? documentInstrument({ entry, loaded: had, setup, globals, ...frames ? { frames } : {} }) : pendingInstrument({ entry, globals, load: request, generation, ...fault ? { fault } : {} })
1235
1747
  };
1236
1748
  built.set(entry.id, kept);
1237
1749
  return kept.instrument;
1238
1750
  });
1239
1751
  const unchanged = list.length === previous.list.length && list.every((i, n) => i === previous.list[n]);
1240
1752
  return { built, books, list: unchanged ? previous.list : list };
1241
- }, [index, readies, revisions, frameUrl, globals, frames, onReady]);
1753
+ }, [index, readies, loaded, faults, generations, revisions, frameUrl, globals, frames, setup, importers, onReady, request]);
1242
1754
  useLayoutEffect(() => {
1243
1755
  committed.current = cache;
1244
1756
  }, [cache]);
@@ -1250,22 +1762,29 @@ function useStoryRegistry(index, options) {
1250
1762
  for (const off of offs) off();
1251
1763
  };
1252
1764
  }, [cache.books]);
1253
- const isReady = useCallback((id) => readies.has(id), [readies]);
1254
- return { instruments: cache.list, isReady, onReady };
1765
+ const isReady = useCallback((id) => readies.has(id) || loaded.has(id), [readies, loaded]);
1766
+ return { instruments: cache.list, isReady, onReady, reload };
1255
1767
  }
1256
1768
  var NO_DECLARATIONS = {};
1257
- function initialStory(index, fallback) {
1769
+ function initialStory(entries, fallback) {
1258
1770
  const route = readRoute();
1259
- return route !== null && index.some((entry) => entry.id === route) ? route : fallback;
1771
+ return route !== null && entries.some((entry) => entry.id === route) ? route : fallback;
1260
1772
  }
1261
1773
  function RouteOpener({ index }) {
1262
1774
  const lab = useLabContext();
1263
1775
  const [route] = useRoute();
1264
1776
  const handled = useRef(null);
1777
+ const last = useRef({ route: readRoute(), entry: readRouteEntry() });
1265
1778
  useEffect(() => {
1266
- if (route === null || handled.current === route || !index.some((entry) => entry.id === route)) return;
1779
+ const from = last.current;
1780
+ if (route !== from.route) last.current = { route, entry: readRouteEntry() };
1781
+ if (route === null || handled.current === route || !index.some((e) => e.id === route)) return;
1267
1782
  handled.current = route;
1268
- if (!lab.trials.some((trial) => trial.instrumentName === route)) lab.addTrial(route);
1783
+ if (lab.trials.some((trial) => trial.instrumentName === route)) return;
1784
+ const previous = lab.trials.find((trial) => trial.instrumentName === from.route);
1785
+ const target = (previous && crossesInPlace(from.entry, last.current.entry) ? previous : void 0) ?? lab.trials.find((trial) => trial.id === lab.focusedTrialId) ?? lab.trials[0];
1786
+ if (target) lab.swapTrial(target.id, route);
1787
+ else lab.addTrial(route);
1269
1788
  }, [route, index, lab]);
1270
1789
  return null;
1271
1790
  }
@@ -1283,11 +1802,33 @@ function useInfoShortcut(open) {
1283
1802
  return () => window.removeEventListener("keydown", onKeyDown);
1284
1803
  }, [open]);
1285
1804
  }
1286
- function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }) {
1805
+ function Workshop({ index, frameUrl, importers, setup, changes, config, stories = [], storageKey, storage }) {
1287
1806
  const declarations = config?.globals ?? NO_DECLARATIONS;
1288
1807
  const [frames] = useState(createTrialFrames);
1289
- const registry = useStoryRegistry(index, { frameUrl, globals: declarations, frames });
1808
+ const [pool, setPool] = useState(null);
1809
+ const framed = importers === void 0 || index.some((entry) => entry.isolate !== void 0);
1810
+ useEffect(() => {
1811
+ if (!framed) return;
1812
+ const next = createFramePool(frameUrl);
1813
+ setPool(next);
1814
+ return () => {
1815
+ next.dispose();
1816
+ setPool(null);
1817
+ };
1818
+ }, [frameUrl, framed]);
1819
+ const entries = useMemo(() => [...index, ...indexEntries(index)], [index]);
1820
+ const registry = useStoryRegistry(entries, {
1821
+ frameUrl,
1822
+ globals: declarations,
1823
+ frames,
1824
+ ...importers ? { importers } : {},
1825
+ ...setup ? { setup } : {}
1826
+ });
1827
+ const reload = registry.reload;
1828
+ useEffect(() => changes?.subscribe(reload), [changes, reload]);
1290
1829
  const [labValues, setLabValues] = useState(() => labGlobals(declarations, void 0));
1830
+ const themeFor = config?.labTheme;
1831
+ const labTheme = useMemo(() => themeFor?.(labValues), [themeFor, labValues]);
1291
1832
  const [infoOpen, setInfoOpen] = useState(false);
1292
1833
  useInfoShortcut(setInfoOpen);
1293
1834
  const reportLabValues = useCallback(
@@ -1316,13 +1857,14 @@ function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }
1316
1857
  stories.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "fg-empty__globs", "aria-label": "Story globs", children: stories.map((glob) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("code", { children: glob }) }, glob)) }) : null
1317
1858
  ] });
1318
1859
  }
1319
- return /* @__PURE__ */ jsx(StoryGlobalsContext.Provider, { value: labValues, children: /* @__PURE__ */ jsx(TrialFramesContext.Provider, { value: frames, children: /* @__PURE__ */ jsxs(
1860
+ return /* @__PURE__ */ jsx(StoryGlobalsContext.Provider, { value: labValues, children: /* @__PURE__ */ jsx(FramePoolContext.Provider, { value: pool, children: /* @__PURE__ */ jsx(TrialFramesContext.Provider, { value: frames, children: /* @__PURE__ */ jsxs(
1320
1861
  Lab,
1321
1862
  {
1322
1863
  title: "weaselforge",
1323
1864
  density: "roomy",
1865
+ ...labTheme ? { theme: labTheme } : {},
1324
1866
  instruments: registry.instruments,
1325
- defaultInstrument: initialStory(index, first.id),
1867
+ defaultInstrument: initialStory(entries, first.id),
1326
1868
  storageKey: storageKey ?? "weaselforge",
1327
1869
  ...storage ? { storage } : {},
1328
1870
  labChrome,
@@ -1331,7 +1873,7 @@ function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }
1331
1873
  ...config?.pages ? { pages: config.pages } : {},
1332
1874
  ...config?.path !== void 0 ? { path: config.path } : {},
1333
1875
  children: [
1334
- /* @__PURE__ */ jsx(RouteOpener, { index }),
1876
+ /* @__PURE__ */ jsx(RouteOpener, { index: entries }),
1335
1877
  /* @__PURE__ */ jsx(
1336
1878
  StoryInfoDialog,
1337
1879
  {
@@ -1344,23 +1886,29 @@ function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }
1344
1886
  /* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues })
1345
1887
  ]
1346
1888
  }
1347
- ) }) });
1889
+ ) }) }) });
1348
1890
  }
1349
1891
  function mountWorkshop({ container, ...props }) {
1350
1892
  const target = container ?? document.getElementById("root");
1351
1893
  if (!target) throw new Error("mountWorkshop: no container given and no #root element");
1352
1894
  const root = createRoot(target);
1353
- let current = props;
1895
+ const changes = createStoryChanges();
1896
+ let current = { ...props, changes };
1354
1897
  root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
1355
1898
  return {
1356
1899
  setIndex(index) {
1357
1900
  current = { ...current, index };
1358
1901
  root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
1359
1902
  },
1903
+ setImporters(importers) {
1904
+ current = { ...current, importers };
1905
+ root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
1906
+ },
1907
+ reloadStory: (file) => changes.emit(file),
1360
1908
  unmount: () => root.unmount()
1361
1909
  };
1362
1910
  }
1363
1911
 
1364
- export { A11Y_SECTION, A11yPanel, StoryGlobalsContext, TrialFramesContext, Workshop, mountWorkshop, useStoryRegistry, useTrialFrame };
1912
+ export { A11Y_SECTION, A11yPanel, Workshop, mountWorkshop, useStoryRegistry };
1365
1913
  //# sourceMappingURL=index.js.map
1366
1914
  //# sourceMappingURL=index.js.map