@weasel-js/forge 1.9.2 → 1.9.4

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 (48) hide show
  1. package/README.md +22 -0
  2. package/dist/{FrameController-CpcY0DNB.d.ts → FrameController-BQjKhdwk.d.ts} +2 -36
  3. package/dist/chunk-5DXUNLV2.js +6 -0
  4. package/dist/chunk-5DXUNLV2.js.map +1 -0
  5. package/dist/{chunk-MGQWCPYE.js → chunk-5KI5YSQ5.js} +4 -5
  6. package/dist/chunk-5KI5YSQ5.js.map +1 -0
  7. package/dist/{chunk-Q2EZXJFE.js → chunk-B3PPHJTS.js} +59 -45
  8. package/dist/chunk-B3PPHJTS.js.map +1 -0
  9. package/dist/{chunk-CJX3W5HY.js → chunk-FRUA5WJF.js} +4 -3
  10. package/dist/chunk-FRUA5WJF.js.map +1 -0
  11. package/dist/{chunk-GZPQMGXJ.js → chunk-JHL4ERDA.js} +4 -3
  12. package/dist/chunk-JHL4ERDA.js.map +1 -0
  13. package/dist/{chunk-JVRHBI3V.js → chunk-MQSFPRF6.js} +20 -44
  14. package/dist/chunk-MQSFPRF6.js.map +1 -0
  15. package/dist/chunk-TXLWQFF6.js +10 -0
  16. package/dist/chunk-TXLWQFF6.js.map +1 -0
  17. package/dist/{chunk-BGOHALHO.js → chunk-ZZCCIXEJ.js} +11 -90
  18. package/dist/chunk-ZZCCIXEJ.js.map +1 -0
  19. package/dist/{config-BfsYw5lg.d.ts → config-Dfu-NKxd.d.ts} +27 -4
  20. package/dist/csf/preview-api.js +2 -2
  21. package/dist/frame/index.d.ts +2 -2
  22. package/dist/frame/index.js +6 -40
  23. package/dist/frame/index.js.map +1 -1
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +3 -3
  26. package/dist/index.js.map +1 -1
  27. package/dist/shell/index.css +61 -61
  28. package/dist/shell/index.css.map +1 -1
  29. package/dist/shell/index.d.ts +5 -13
  30. package/dist/shell/index.js +345 -332
  31. package/dist/shell/index.js.map +1 -1
  32. package/dist/test/index.d.ts +4 -2
  33. package/dist/test/index.js +7 -7
  34. package/dist/test/index.js.map +1 -1
  35. package/dist/test/play.d.ts +3 -3
  36. package/dist/test/play.js.map +1 -1
  37. package/dist/vite/index.d.ts +3 -1
  38. package/dist/vite/index.js +365 -101
  39. package/dist/vite/index.js.map +1 -1
  40. package/package.json +5 -5
  41. package/dist/chunk-BGOHALHO.js.map +0 -1
  42. package/dist/chunk-CJX3W5HY.js.map +0 -1
  43. package/dist/chunk-GZPQMGXJ.js.map +0 -1
  44. package/dist/chunk-JVRHBI3V.js.map +0 -1
  45. package/dist/chunk-KJGXJD3U.js +0 -15
  46. package/dist/chunk-KJGXJD3U.js.map +0 -1
  47. package/dist/chunk-MGQWCPYE.js.map +0 -1
  48. package/dist/chunk-Q2EZXJFE.js.map +0 -1
@@ -1,23 +1,24 @@
1
- import { parseRoute, paramsToKnobs, knobPaths, sameKnob, knobsToParams, formatRoute, PLAYHEAD_PARAM, reservedParams } from '../chunk-CJX3W5HY.js';
2
- import { isIndexId, indexId, libraryOf, indexEntries, schemaFromDescription, LibraryBadge, openChannel, IndexPage } from '../chunk-JVRHBI3V.js';
1
+ import { parseRoute, formatRoute, paramsToKnobs, knobPaths, sameKnob, knobsToParams, PLAYHEAD_PARAM, reservedParams } from '../chunk-FRUA5WJF.js';
2
+ import { DEFAULT_TITLE } from '../chunk-5DXUNLV2.js';
3
+ import { TITLE_LIBRARIES, isIndexId, indexId, indexEntries, librariesFrom, LibrariesContext, schemaFromDescription, LibraryBadge, openChannel, IndexPage } from '../chunk-MQSFPRF6.js';
3
4
  import { isGallery } from '../chunk-WEQSLPKT.js';
4
- import { useTrialFrame, createTrialFrames, createTrialClocks, StoryGlobalsContext, TrialFramesContext, TrialClocksContext, StoryTrial, useCssOverrides, useNearViewport, IN_VIEW_MARGIN, useTrialClock, TrialHost } from '../chunk-BGOHALHO.js';
5
- export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-BGOHALHO.js';
6
- import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, PLAYHEAD_KEY, labOptions, isGlobalsPath } from '../chunk-GZPQMGXJ.js';
7
- import { loadStories, indexRenderOf } from '../chunk-Q2EZXJFE.js';
5
+ import { useTrialFrame, createTrialFrames, createTrialClocks, StoryGlobalsContext, TrialFramesContext, TrialClocksContext, StoryTrial, useCssOverrides, useTrialClock, TrialHost } from '../chunk-ZZCCIXEJ.js';
6
+ export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-ZZCCIXEJ.js';
7
+ import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, PLAYHEAD_KEY, labOptions, isGlobalsPath } from '../chunk-JHL4ERDA.js';
8
+ import { loadStories, indexRenderOf, useNearViewport, IN_VIEW_MARGIN } from '../chunk-B3PPHJTS.js';
8
9
  import '../chunk-L5XM5VKB.js';
9
10
  import '../chunk-HRGPFQBW.js';
10
- import '../chunk-MGQWCPYE.js';
11
- import { stableStringify, PORT_HANDOFF, FRAME_HELLO, PROTOCOL_VERSION } from '../chunk-KJGXJD3U.js';
11
+ import '../chunk-5KI5YSQ5.js';
12
+ import { stableStringify, PORT_HANDOFF, FRAME_HELLO, PROTOCOL_VERSION } from '../chunk-TXLWQFF6.js';
12
13
  import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, useTrialState, inferTokenType, ToggleBar as ToggleBar$1, Input, TokenPanel, refitScale } from '@weasel-js/labkit';
13
14
  import num from '@weasel-js/theme/numeric.module.css';
14
- import { createContext, useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect, useId, useContext, Fragment as Fragment$1, useSyncExternalStore } from 'react';
15
+ import { createContext, useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect, useId, useContext, Fragment as Fragment$1, useSyncExternalStore } from 'react';
15
16
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
16
17
  import { createRoot } from 'react-dom/client';
18
+ import { f, valueAtPath, isConfigBranch } from '@weasel-js/labkit/config';
17
19
  import { InfoIcon, ToggleBar, Checkbox, Dialog, TOKEN_CATEGORIES, Icon, DisclosureMark, Button as Button$1, Callout, Slider, Transport, SearchIcon } from '@weasel-js/ui';
18
20
  import { TOKEN_MANIFEST, THEME_SOURCES, isByAxis } from '@weasel-js/theme';
19
21
  import { scale, httpThemeApi } from '@weasel-js/theme/engine';
20
- import { f, valueAtPath, isConfigBranch } from '@weasel-js/labkit/config';
21
22
  import { useLatest } from '@weasel-js/core';
22
23
 
23
24
  function Finding({ finding }) {
@@ -44,19 +45,21 @@ function Findings({ title, findings }) {
44
45
  function A11yPanel() {
45
46
  const trialId = useTrialId();
46
47
  const frame = useTrialFrame(trialId);
47
- const [running, setRunning] = useState(false);
48
+ const [inFlight, setInFlight] = useState(0);
49
+ const running = inFlight > 0;
48
50
  const audit = frame.audit;
49
51
  const ran = useRef(null);
52
+ const start = useCallback((run) => {
53
+ setInFlight((n) => n + 1);
54
+ void run().finally(() => setInFlight((n) => n - 1));
55
+ }, []);
50
56
  useEffect(() => {
51
57
  if (!audit || ran.current === audit) return;
52
58
  ran.current = audit;
53
- setRunning(true);
54
- void audit().finally(() => setRunning(false));
55
- }, [audit]);
59
+ start(audit);
60
+ }, [audit, start]);
56
61
  const rerun = () => {
57
- if (!audit) return;
58
- setRunning(true);
59
- void audit().finally(() => setRunning(false));
62
+ if (audit) start(audit);
60
63
  };
61
64
  const outcome = frame.a11y;
62
65
  return /* @__PURE__ */ jsxs("div", { className: "fg-a11y", children: [
@@ -112,6 +115,243 @@ function createStoryChanges() {
112
115
  };
113
116
  }
114
117
  var DependenciesContext = createContext(null);
118
+ function readRoute() {
119
+ return parseRoute(location.hash).story;
120
+ }
121
+ function readRouteParams() {
122
+ return { ...parseRoute(location.hash).params };
123
+ }
124
+ function replaceRouteParams(update) {
125
+ const { story, params } = parseRoute(location.hash);
126
+ if (story === null) return;
127
+ const next = formatRoute({ story, params: update({ ...params }) });
128
+ if (next !== location.hash) history.replaceState(history.state, "", next);
129
+ }
130
+ function readRouteEntry() {
131
+ const state = history.state;
132
+ const entry = state?.forgeRoute;
133
+ return typeof entry?.step === "number" ? entry : null;
134
+ }
135
+ function crossesInPlace(from, to) {
136
+ if (!from || !to || from.step === to.step) return false;
137
+ return to.step < from.step ? from.inPlace : to.inPlace;
138
+ }
139
+ function stamp(entry, hash) {
140
+ history.replaceState({ ...history.state ?? {}, forgeRoute: entry }, "", hash);
141
+ }
142
+ function setRoute(id, options = {}) {
143
+ if (readRoute() === id) {
144
+ history.replaceState(history.state, "", formatRoute({ story: id, params: readRouteParams() }));
145
+ return;
146
+ }
147
+ const hash = formatRoute({ story: id, params: {} });
148
+ const from = readRouteEntry() ?? { step: 0, inPlace: false };
149
+ stamp(from, location.hash);
150
+ location.hash = hash;
151
+ stamp({ step: from.step + 1, inPlace: options.inPlace ?? false, ...options.fromTrial ? { fromTrial: true } : {} }, hash);
152
+ }
153
+ function takeRouteFromTrial() {
154
+ const entry = readRouteEntry();
155
+ if (!entry?.fromTrial) return false;
156
+ stamp({ step: entry.step, inPlace: entry.inPlace }, location.hash);
157
+ return true;
158
+ }
159
+ function useRoute() {
160
+ const [route, setCurrent] = useState(readRoute);
161
+ useEffect(() => {
162
+ const sync = () => setCurrent(readRoute());
163
+ window.addEventListener("hashchange", sync);
164
+ sync();
165
+ return () => window.removeEventListener("hashchange", sync);
166
+ }, []);
167
+ return [route, setRoute];
168
+ }
169
+
170
+ // src/timeline/playheadUrl.ts
171
+ function formatSeconds(ms) {
172
+ const rounded = Math.round(ms) / 1e3;
173
+ return String(rounded === 0 ? 0 : rounded);
174
+ }
175
+ function parseSeconds(text) {
176
+ if (text == null || !/^\s*-?(\d+\.?\d*|\.\d+)\s*$/.test(text)) return null;
177
+ return Math.round(Number(text) * 1e3);
178
+ }
179
+ function readPlayheadParam() {
180
+ return parseSeconds(readRouteParams()[PLAYHEAD_PARAM]);
181
+ }
182
+ function writePlayheadParam(ms) {
183
+ replaceRouteParams(
184
+ ({ [PLAYHEAD_PARAM]: _held, ...rest }) => ms === null ? rest : { ...rest, [PLAYHEAD_PARAM]: formatSeconds(ms) }
185
+ );
186
+ }
187
+ function TransportBar({ clock }) {
188
+ const { time, playing, loop, rate, span } = useSyncExternalStore(clock.subscribe, clock.get);
189
+ return /* @__PURE__ */ jsxs("div", { className: "fg-transport", children: [
190
+ /* @__PURE__ */ jsx(
191
+ Slider,
192
+ {
193
+ className: "fg-transport__scrub",
194
+ ariaLabel: "Scrub",
195
+ density: "slim",
196
+ readoutPlacement: "none",
197
+ min: span.start,
198
+ max: span.end > span.start ? span.end : span.start + 1,
199
+ thumbs: [{ value: time, valueText: `${formatSeconds(time)} seconds` }],
200
+ onInput: ([thumb]) => {
201
+ if (thumb) clock.seek(thumb.value);
202
+ }
203
+ }
204
+ ),
205
+ /* @__PURE__ */ jsx(
206
+ Transport,
207
+ {
208
+ paused: !playing,
209
+ loop,
210
+ rate,
211
+ playhead: time,
212
+ duration: span.end,
213
+ onPlay: clock.play,
214
+ onPause: clock.pause,
215
+ onLoopChange: (next) => clock.setLoop(next !== false && next !== 0),
216
+ onRateChange: clock.setRate
217
+ }
218
+ )
219
+ ] });
220
+ }
221
+ function TrialTransport({ trialId }) {
222
+ const clock = useTrialClock(trialId);
223
+ return clock ? /* @__PURE__ */ jsx(TransportBar, { clock }) : null;
224
+ }
225
+
226
+ // src/shell/breadcrumb.ts
227
+ function crumbs(title, name) {
228
+ const segments = title.split("/");
229
+ const steps = segments.map((label, i) => ({ label, path: segments.slice(0, i + 1).join("/") }));
230
+ steps.push({ label: name, path: null });
231
+ return steps.filter((step, i) => i === 0 || step.label.toLowerCase() !== "index");
232
+ }
233
+ function breadcrumb(title, name) {
234
+ return crumbs(title, name).map((step) => step.label).join(" > ");
235
+ }
236
+
237
+ // src/shell/treeReveal.ts
238
+ var listeners = /* @__PURE__ */ new Set();
239
+ function revealInTree(path) {
240
+ for (const listener of listeners) listener(path);
241
+ }
242
+ function onRevealInTree(listener) {
243
+ listeners.add(listener);
244
+ return () => {
245
+ listeners.delete(listener);
246
+ };
247
+ }
248
+ function Crumbs({ entry }) {
249
+ const steps = crumbs(entry.title, entry.name);
250
+ return /* @__PURE__ */ jsx("span", { className: "fg-crumbs", children: steps.map((step, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
251
+ i > 0 ? /* @__PURE__ */ jsx("span", { className: "fg-crumbs__sep", "aria-hidden": "true", children: "\u203A" }) : null,
252
+ step.path === null ? /* @__PURE__ */ jsx("span", { className: "fg-crumbs__here", children: step.label }) : /* @__PURE__ */ jsx(
253
+ "button",
254
+ {
255
+ type: "button",
256
+ className: "fg-crumbs__step",
257
+ title: `Show ${step.label} in the story tree`,
258
+ onPointerDown: (event) => event.stopPropagation(),
259
+ onClick: () => revealInTree(step.path),
260
+ children: step.label
261
+ }
262
+ )
263
+ ] }, step.path ?? `${step.label}:${i}`)) });
264
+ }
265
+ function crumbTitle(entry) {
266
+ const title = breadcrumb(entry.title, entry.name);
267
+ return { title, renderTitle: (shown) => shown === title ? /* @__PURE__ */ jsx(Crumbs, { entry }) : shown };
268
+ }
269
+ function IndexTrial({ bundle, setup, config }) {
270
+ const lab = useLabContext();
271
+ const trialId = useContext(TrialIdContext);
272
+ const graph = useContext(DependenciesContext);
273
+ const gallery = isGallery(bundle);
274
+ const dependencies = gallery ? void 0 : graph?.[bundle.title];
275
+ const open = (id) => {
276
+ if (!trialId || !lab.instruments.some((instrument) => instrument.name === id)) return;
277
+ lab.swapTrial(trialId, id);
278
+ setRoute(id, { inPlace: true });
279
+ };
280
+ const render = (globals) => {
281
+ const env = {
282
+ title: bundle.title,
283
+ library: bundle.library,
284
+ ...bundle.description === void 0 ? {} : { description: bundle.description },
285
+ stories: bundle.stories,
286
+ descriptions: bundle.descriptions,
287
+ gallery,
288
+ galleries: bundle.galleries,
289
+ ...dependencies ? { dependencies } : {},
290
+ globals,
291
+ decorators: setup.decorators ?? [],
292
+ open,
293
+ // A story that throws shows its error in its own cell; the rest of the page is still worth reading.
294
+ onError: (error) => console.error(error)
295
+ };
296
+ return /* @__PURE__ */ jsx(IndexPage, { env, render: bundle.render });
297
+ };
298
+ return /* @__PURE__ */ jsx(TrialHost, { layout: "fullscreen", setup, config, children: render });
299
+ }
300
+ function withPlayhead(schema) {
301
+ return f.schema({ ...schema.nodes, [PLAYHEAD_KEY]: f.custom("forge-playhead", null).hidden() });
302
+ }
303
+ function documentInstrument(options) {
304
+ const { entry, loaded, setup, globals = {} } = options;
305
+ const schema = loaded.kind === "story" ? loaded.story.config : f.schema({});
306
+ const timed = loaded.kind === "story" && loaded.story.timeline !== null;
307
+ const config = withGlobals(timed ? withPlayhead(schema) : schema, globals);
308
+ const initialState = loaded.kind === "story" ? loaded.story.initialState : null;
309
+ return {
310
+ name: entry.id,
311
+ ...crumbTitle(entry),
312
+ config,
313
+ defaultConfig: () => config.defaults(),
314
+ initialState: (c) => initialState?.(c) ?? null,
315
+ render: (ctx) => loaded.kind === "story" ? /* @__PURE__ */ jsx(StoryTrial, { story: loaded.story, setup, ctx }, loaded.story.id) : /* @__PURE__ */ jsx(IndexTrial, { bundle: loaded.bundle, setup, config: ctx.config }),
316
+ ...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {},
317
+ ...timed ? { chrome: [{ id: "fg-transport", region: "status", render: (c) => /* @__PURE__ */ jsx(TrialTransport, { trialId: c.trialId }) }] } : {}
318
+ };
319
+ }
320
+ function Fault({ phase, message }) {
321
+ return /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
322
+ /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: phase }),
323
+ /* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: message })
324
+ ] });
325
+ }
326
+ function StoryLoad({ entry, load, fault }) {
327
+ useEffect(() => {
328
+ if (!fault) load(entry);
329
+ }, [entry, load, fault]);
330
+ return /* @__PURE__ */ jsx("div", { className: "fg-story fg-story--loading", "data-fg-layout": "fullscreen", children: fault ? /* @__PURE__ */ jsx(Fault, { phase: "import", message: fault.message }) : null });
331
+ }
332
+ function pendingInstrument(options) {
333
+ const { entry, globals = {} } = options;
334
+ const config = withGlobals(f.schema({}), globals);
335
+ return {
336
+ name: entry.id,
337
+ ...crumbTitle(entry),
338
+ config,
339
+ defaultConfig: () => config.defaults(),
340
+ initialState: () => null,
341
+ render: () => /* @__PURE__ */ jsx(StoryLoad, { ...options }, options.generation ?? 0)
342
+ };
343
+ }
344
+ function missingInstrument(id, globals = {}) {
345
+ const config = withGlobals(f.schema({}), globals);
346
+ return {
347
+ name: id,
348
+ title: `No story ${id}`,
349
+ config,
350
+ defaultConfig: () => config.defaults(),
351
+ initialState: () => null,
352
+ render: () => /* @__PURE__ */ jsx("div", { className: "fg-story fg-story--loading", "data-fg-layout": "fullscreen", children: /* @__PURE__ */ jsx(Fault, { phase: "route", message: `No story ${id}` }) })
353
+ };
354
+ }
115
355
  var SEED_REF = /^\{seeds\.([^}]+)\}$/;
116
356
  var RULE_KEYS = { factors: "factors", ratio: "ratio", step: "step" };
117
357
  function selectionFor(axes, globals) {
@@ -533,17 +773,6 @@ var CSS_VARS_SECTION = {
533
773
  region: "aside",
534
774
  item: { title: "CSS Vars", actions: /* @__PURE__ */ jsx(SectionJump, {}), body: /* @__PURE__ */ jsx(FocusedCssVars, {}) }
535
775
  };
536
-
537
- // src/shell/breadcrumb.ts
538
- function crumbs(title, name) {
539
- const segments = title.split("/");
540
- const steps = segments.map((label, i) => ({ label, path: segments.slice(0, i + 1).join("/") }));
541
- steps.push({ label: name, path: null });
542
- return steps.filter((step, i) => i === 0 || step.label.toLowerCase() !== "index");
543
- }
544
- function breadcrumb(title, name) {
545
- return crumbs(title, name).map((step) => step.label).join(" > ");
546
- }
547
776
  var GLOBALS = "$globals";
548
777
  function leaves(shape, prefix = "") {
549
778
  const out = [];
@@ -556,7 +785,7 @@ function leaves(shape, prefix = "") {
556
785
  return out;
557
786
  }
558
787
  function storyDossier(input) {
559
- const { entry, instrument, config, ready = true } = input;
788
+ const { entry, libraries = TITLE_LIBRARIES, instrument, config, ready = true } = input;
560
789
  const schema = ready ? instrument?.config : void 0;
561
790
  const args = schema ? leaves(schema.nodes).map(({ path, node }) => {
562
791
  const value = valueAtPath(config, path);
@@ -575,7 +804,7 @@ function storyDossier(input) {
575
804
  name: entry.name,
576
805
  exportName: entry.exportName,
577
806
  file: entry.file,
578
- library: libraryOf(entry),
807
+ library: libraries.of(entry),
579
808
  ...entry.componentName === void 0 ? {} : { componentName: entry.componentName },
580
809
  ...entry.description === void 0 ? {} : { description: entry.description },
581
810
  ...entry.componentDescription === void 0 ? {} : { componentDescription: entry.componentDescription },
@@ -638,11 +867,13 @@ function StoryDossier({ dossier }) {
638
867
  }
639
868
  function StoryInfoDialog({ index, isReady, isOpen, onOpenChange }) {
640
869
  const { focusedTrialId, trials, instruments } = useLabContext();
870
+ const libraries = useContext(LibrariesContext);
641
871
  const trial = trials.find((record) => record.id === focusedTrialId);
642
872
  const entry = trial ? index.find((candidate) => candidate.id === trial.instrumentName) : void 0;
643
873
  if (!entry) return null;
644
874
  const dossier = storyDossier({
645
875
  entry,
876
+ libraries,
646
877
  instrument: instruments.find((candidate) => candidate.name === entry.id),
647
878
  config: trial?.config,
648
879
  ready: isReady(entry.id)
@@ -658,6 +889,7 @@ function StoryInfoDialog({ index, isReady, isOpen, onOpenChange }) {
658
889
  }
659
890
  );
660
891
  }
892
+ var HELLO_TIMEOUT_MS = 3e4;
661
893
  function canMoveFrames() {
662
894
  return typeof Element !== "undefined" && "moveBefore" in Element.prototype;
663
895
  }
@@ -678,21 +910,30 @@ function createFramePool(frameUrl, size = 2) {
678
910
  shelf.setAttribute("aria-hidden", "true");
679
911
  document.body.append(shelf);
680
912
  const warm = [];
681
- let loading = 0;
913
+ const waits = /* @__PURE__ */ new Set();
682
914
  let cancelRefill = null;
683
915
  let disposed = false;
684
916
  const load = () => {
685
- loading += 1;
686
917
  const frame = document.createElement("iframe");
687
918
  frame.tabIndex = -1;
688
919
  frame.src = frameUrl;
920
+ const stop = () => {
921
+ window.removeEventListener("message", onMessage);
922
+ clearTimeout(timer);
923
+ waits.delete(stop);
924
+ };
689
925
  const onMessage = (event) => {
690
926
  if (event.source !== frame.contentWindow || event.origin !== location.origin) return;
691
927
  if (event.data?.type !== FRAME_HELLO) return;
692
- window.removeEventListener("message", onMessage);
693
- loading -= 1;
928
+ stop();
694
929
  warm.push(frame);
695
930
  };
931
+ const timer = setTimeout(() => {
932
+ stop();
933
+ frame.remove();
934
+ refill();
935
+ }, HELLO_TIMEOUT_MS);
936
+ waits.add(stop);
696
937
  window.addEventListener("message", onMessage);
697
938
  shelf.append(frame);
698
939
  };
@@ -700,7 +941,7 @@ function createFramePool(frameUrl, size = 2) {
700
941
  if (disposed || cancelRefill) return;
701
942
  cancelRefill = whenIdle(() => {
702
943
  cancelRefill = null;
703
- while (!disposed && warm.length + loading < size) load();
944
+ while (!disposed && warm.length + waits.size < size) load();
704
945
  });
705
946
  };
706
947
  if (canMoveFrames()) refill();
@@ -714,6 +955,7 @@ function createFramePool(frameUrl, size = 2) {
714
955
  dispose() {
715
956
  disposed = true;
716
957
  cancelRefill?.();
958
+ for (const stop of [...waits]) stop();
717
959
  shelf.remove();
718
960
  }
719
961
  };
@@ -724,69 +966,6 @@ var FramePoolContext = createContext(null);
724
966
  function routedTrial(trials, focusedTrialId, route) {
725
967
  return trials.find((t) => t.id === focusedTrialId && t.instrumentName === route) ?? trials.find((t) => t.instrumentName === route);
726
968
  }
727
- function readRoute() {
728
- return parseRoute(location.hash).story;
729
- }
730
- function readRouteParams() {
731
- return { ...parseRoute(location.hash).params };
732
- }
733
- function replaceRouteParams(update) {
734
- const { story, params } = parseRoute(location.hash);
735
- if (story === null) return;
736
- const next = formatRoute({ story, params: update({ ...params }) });
737
- if (next !== location.hash) history.replaceState(history.state, "", next);
738
- }
739
- function readRouteEntry() {
740
- const state = history.state;
741
- const entry = state?.forgeRoute;
742
- return typeof entry?.step === "number" ? entry : null;
743
- }
744
- function crossesInPlace(from, to) {
745
- if (!from || !to || from.step === to.step) return false;
746
- return to.step < from.step ? from.inPlace : to.inPlace;
747
- }
748
- function stamp(entry, hash) {
749
- history.replaceState({ ...history.state ?? {}, forgeRoute: entry }, "", hash);
750
- }
751
- function setRoute(id, options = {}) {
752
- if (readRoute() === id) {
753
- history.replaceState(history.state, "", formatRoute({ story: id, params: readRouteParams() }));
754
- return;
755
- }
756
- const hash = formatRoute({ story: id, params: {} });
757
- const from = readRouteEntry() ?? { step: 0, inPlace: false };
758
- stamp(from, location.hash);
759
- location.hash = hash;
760
- stamp({ step: from.step + 1, inPlace: options.inPlace ?? false }, hash);
761
- }
762
- function useRoute() {
763
- const [route, setCurrent] = useState(readRoute);
764
- useEffect(() => {
765
- const sync = () => setCurrent(readRoute());
766
- window.addEventListener("hashchange", sync);
767
- sync();
768
- return () => window.removeEventListener("hashchange", sync);
769
- }, []);
770
- return [route, setRoute];
771
- }
772
-
773
- // src/timeline/playheadUrl.ts
774
- function formatSeconds(ms) {
775
- const rounded = Math.round(ms) / 1e3;
776
- return String(rounded === 0 ? 0 : rounded);
777
- }
778
- function parseSeconds(text) {
779
- if (text == null || !/^\s*-?(\d+\.?\d*|\.\d+)\s*$/.test(text)) return null;
780
- return Math.round(Number(text) * 1e3);
781
- }
782
- function readPlayheadParam() {
783
- return parseSeconds(readRouteParams()[PLAYHEAD_PARAM]);
784
- }
785
- function writePlayheadParam(ms) {
786
- replaceRouteParams(
787
- ({ [PLAYHEAD_PARAM]: _held, ...rest }) => ms === null ? rest : { ...rest, [PLAYHEAD_PARAM]: formatSeconds(ms) }
788
- );
789
- }
790
969
  function PlayheadSync({ trialId, route, applied }) {
791
970
  const clock = useTrialClock(trialId);
792
971
  useEffect(() => {
@@ -938,6 +1117,7 @@ function KnobSync({ schema, config }) {
938
1117
  return () => window.removeEventListener("hashchange", onHashChange);
939
1118
  }, []);
940
1119
  useEffect(() => {
1120
+ if (reading.current && takeRouteFromTrial()) reading.current = false;
941
1121
  if (reading.current) {
942
1122
  reading.current = false;
943
1123
  const knobs2 = paramsToKnobs(schema, readRouteParams());
@@ -987,18 +1167,6 @@ function revealTrial(trialId, root = document) {
987
1167
  return true;
988
1168
  }
989
1169
 
990
- // src/shell/treeReveal.ts
991
- var listeners = /* @__PURE__ */ new Set();
992
- function revealInTree(path) {
993
- for (const listener of listeners) listener(path);
994
- }
995
- function onRevealInTree(listener) {
996
- listeners.add(listener);
997
- return () => {
998
- listeners.delete(listener);
999
- };
1000
- }
1001
-
1002
1170
  // src/shell/tree/buildTree.ts
1003
1171
  function sortFolder(nodes) {
1004
1172
  const folders = nodes.filter((n) => n.kind === "folder");
@@ -1055,7 +1223,7 @@ function filterTree(nodes, query) {
1055
1223
  }
1056
1224
 
1057
1225
  // src/shell/tree/buildComponents.ts
1058
- function buildComponents(index) {
1226
+ function buildComponents(index, libraries) {
1059
1227
  const byTitle = /* @__PURE__ */ new Map();
1060
1228
  const pages = new Map(indexEntries(index).map((page) => [page.title, page]));
1061
1229
  for (const entry of index) {
@@ -1065,7 +1233,7 @@ function buildComponents(index) {
1065
1233
  row = {
1066
1234
  path: entry.title,
1067
1235
  label: segments[segments.length - 1],
1068
- library: libraryOf(entry),
1236
+ library: libraries.of(entry),
1069
1237
  index: pages.get(entry.title),
1070
1238
  entries: []
1071
1239
  };
@@ -1142,8 +1310,8 @@ function componentNodes(rows) {
1142
1310
  }
1143
1311
  return foldGalleries(root);
1144
1312
  }
1145
- function librariesIn(index) {
1146
- return [...new Set(index.map(libraryOf))].sort((a, b) => a.localeCompare(b));
1313
+ function librariesIn(index, libraries) {
1314
+ return [...new Set(index.map(libraries.of))].sort((a, b) => a.localeCompare(b));
1147
1315
  }
1148
1316
  var VIEWS = [
1149
1317
  { value: "tree", label: "Tree" },
@@ -1173,7 +1341,8 @@ function StoryTree({ ctx, index }) {
1173
1341
  const [hidden, setHidden] = usePersistedState("fg-tree-libraries", {}, { scope: "lab" });
1174
1342
  const [focusKey, setFocusKey] = useState(null);
1175
1343
  const items = useRef(/* @__PURE__ */ new Map());
1176
- const libraries = useMemo(() => librariesIn(index), [index]);
1344
+ const table = useContext(LibrariesContext);
1345
+ const libraries = useMemo(() => librariesIn(index, table), [index, table]);
1177
1346
  const views = useMemo(() => {
1178
1347
  const hasGallery = index.some(isGallery);
1179
1348
  const hasOther = index.some((entry) => !isGallery(entry));
@@ -1181,7 +1350,7 @@ function StoryTree({ ctx, index }) {
1181
1350
  }, [index]);
1182
1351
  const enabled = (value) => views.some((item) => item.value === value && !item.disabled);
1183
1352
  const view = [chosen, "components", "gallery"].find(enabled) ?? chosen;
1184
- const kept = useMemo(() => index.filter((entry) => !hidden[libraryOf(entry)]), [index, hidden]);
1353
+ const kept = useMemo(() => index.filter((entry) => !hidden[table.of(entry)]), [index, hidden, table]);
1185
1354
  const [galleryEntries, rest] = useMemo(() => {
1186
1355
  const yes = [];
1187
1356
  const no = [];
@@ -1189,8 +1358,8 @@ function StoryTree({ ctx, index }) {
1189
1358
  return [yes, no];
1190
1359
  }, [kept]);
1191
1360
  const tree = useMemo(() => buildTree(rest), [rest]);
1192
- const components = useMemo(() => buildComponents(rest), [rest]);
1193
- const galleries = useMemo(() => buildComponents(galleryEntries), [galleryEntries]);
1361
+ const components = useMemo(() => buildComponents(rest, table), [rest, table]);
1362
+ const galleries = useMemo(() => buildComponents(galleryEntries, table), [galleryEntries, table]);
1194
1363
  const shown = useMemo(
1195
1364
  () => view === "tree" ? filterTree(tree, query) : componentNodes(filterComponents(view === "gallery" ? galleries : components, query)),
1196
1365
  [view, components, galleries, tree, query]
@@ -1221,8 +1390,11 @@ function StoryTree({ ctx, index }) {
1221
1390
  const activate = (entry, another) => {
1222
1391
  const focused = ctx.trials.find((trial) => trial.id === ctx.focusedTrialId);
1223
1392
  if (another || !focused) ctx.addTrial(entry.id);
1224
- else if (focused.instrumentName === entry.id) revealTrial(focused.id);
1225
- else {
1393
+ else if (focused.instrumentName === entry.id) {
1394
+ revealTrial(focused.id);
1395
+ setRoute2(entry.id, { fromTrial: true });
1396
+ return;
1397
+ } else {
1226
1398
  ctx.swapTrial(focused.id, entry.id);
1227
1399
  setRoute2(entry.id, { inPlace: true });
1228
1400
  return;
@@ -1487,156 +1659,20 @@ function createAnswerBook() {
1487
1659
  }
1488
1660
  };
1489
1661
  }
1490
- function TransportBar({ clock }) {
1491
- const { time, playing, loop, rate, span } = useSyncExternalStore(clock.subscribe, clock.get);
1492
- return /* @__PURE__ */ jsxs("div", { className: "fg-transport", children: [
1493
- /* @__PURE__ */ jsx(
1494
- Slider,
1495
- {
1496
- className: "fg-transport__scrub",
1497
- ariaLabel: "Scrub",
1498
- density: "slim",
1499
- readoutPlacement: "none",
1500
- min: span.start,
1501
- max: span.end > span.start ? span.end : span.start + 1,
1502
- thumbs: [{ value: time, valueText: `${formatSeconds(time)} seconds` }],
1503
- onInput: ([thumb]) => {
1504
- if (thumb) clock.seek(thumb.value);
1505
- }
1506
- }
1507
- ),
1508
- /* @__PURE__ */ jsx(
1509
- Transport,
1510
- {
1511
- paused: !playing,
1512
- loop,
1513
- rate,
1514
- playhead: time,
1515
- duration: span.end,
1516
- onPlay: clock.play,
1517
- onPause: clock.pause,
1518
- onLoopChange: (next) => clock.setLoop(next !== false && next !== 0),
1519
- onRateChange: clock.setRate
1520
- }
1521
- )
1522
- ] });
1523
- }
1524
- function TrialTransport({ trialId }) {
1525
- const clock = useTrialClock(trialId);
1526
- return clock ? /* @__PURE__ */ jsx(TransportBar, { clock }) : null;
1527
- }
1528
- function Crumbs({ entry }) {
1529
- const steps = crumbs(entry.title, entry.name);
1530
- return /* @__PURE__ */ jsx("span", { className: "fg-crumbs", children: steps.map((step, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
1531
- i > 0 ? /* @__PURE__ */ jsx("span", { className: "fg-crumbs__sep", "aria-hidden": "true", children: "\u203A" }) : null,
1532
- step.path === null ? /* @__PURE__ */ jsx("span", { className: "fg-crumbs__here", children: step.label }) : /* @__PURE__ */ jsx(
1533
- "button",
1534
- {
1535
- type: "button",
1536
- className: "fg-crumbs__step",
1537
- title: `Show ${step.label} in the story tree`,
1538
- onPointerDown: (event) => event.stopPropagation(),
1539
- onClick: () => revealInTree(step.path),
1540
- children: step.label
1541
- }
1542
- )
1543
- ] }, step.path ?? `${step.label}:${i}`)) });
1544
- }
1545
- function crumbTitle(entry) {
1546
- const title = breadcrumb(entry.title, entry.name);
1547
- return { title, renderTitle: (shown) => shown === title ? /* @__PURE__ */ jsx(Crumbs, { entry }) : shown };
1548
- }
1549
- function IndexTrial({ bundle, setup, config }) {
1550
- const lab = useLabContext();
1551
- const trialId = useContext(TrialIdContext);
1552
- const graph = useContext(DependenciesContext);
1553
- const gallery = isGallery(bundle);
1554
- const dependencies = gallery ? void 0 : graph?.[bundle.title];
1555
- const open = (id) => {
1556
- if (!trialId || !lab.instruments.some((instrument) => instrument.name === id)) return;
1557
- lab.swapTrial(trialId, id);
1558
- setRoute(id, { inPlace: true });
1559
- };
1560
- const render = (globals) => {
1561
- const env = {
1562
- title: bundle.title,
1563
- library: bundle.library,
1564
- ...bundle.description === void 0 ? {} : { description: bundle.description },
1565
- stories: bundle.stories,
1566
- descriptions: bundle.descriptions,
1567
- gallery,
1568
- galleries: bundle.galleries,
1569
- ...dependencies ? { dependencies } : {},
1570
- globals,
1571
- decorators: setup.decorators ?? [],
1572
- open,
1573
- // A story that throws shows its error in its own cell; the rest of the page is still worth reading.
1574
- onError: (error) => console.error(error)
1575
- };
1576
- return /* @__PURE__ */ jsx(IndexPage, { env, render: bundle.render });
1577
- };
1578
- return /* @__PURE__ */ jsx(TrialHost, { layout: "fullscreen", setup, config, children: render });
1579
- }
1580
- function withPlayhead(schema) {
1581
- return f.schema({ ...schema.nodes, [PLAYHEAD_KEY]: f.custom("forge-playhead", null).hidden() });
1582
- }
1583
- function documentInstrument(options) {
1584
- const { entry, loaded, setup, globals = {} } = options;
1585
- const schema = loaded.kind === "story" ? loaded.story.config : f.schema({});
1586
- const timed = loaded.kind === "story" && loaded.story.timeline !== null;
1587
- const config = withGlobals(timed ? withPlayhead(schema) : schema, globals);
1588
- const initialState = loaded.kind === "story" ? loaded.story.initialState : null;
1589
- return {
1590
- name: entry.id,
1591
- ...crumbTitle(entry),
1592
- config,
1593
- defaultConfig: () => config.defaults(),
1594
- initialState: (c) => initialState?.(c) ?? null,
1595
- render: (ctx) => loaded.kind === "story" ? /* @__PURE__ */ jsx(StoryTrial, { story: loaded.story, setup, ctx }, loaded.story.id) : /* @__PURE__ */ jsx(IndexTrial, { bundle: loaded.bundle, setup, config: ctx.config }),
1596
- ...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {},
1597
- ...timed ? { chrome: [{ id: "fg-transport", region: "status", render: (c) => /* @__PURE__ */ jsx(TrialTransport, { trialId: c.trialId }) }] } : {}
1598
- };
1599
- }
1600
- function StoryLoad({ entry, load, fault }) {
1601
- useEffect(() => {
1602
- if (!fault) load(entry);
1603
- }, [entry, load, fault]);
1604
- 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: [
1605
- /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: "import" }),
1606
- /* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
1607
- ] }) : null });
1608
- }
1609
- function pendingInstrument(options) {
1610
- const { entry, globals = {} } = options;
1611
- const config = withGlobals(f.schema({}), globals);
1612
- return {
1613
- name: entry.id,
1614
- ...crumbTitle(entry),
1615
- config,
1616
- defaultConfig: () => config.defaults(),
1617
- initialState: () => null,
1618
- render: () => /* @__PURE__ */ jsx(StoryLoad, { ...options }, options.generation ?? 0)
1619
- };
1620
- }
1621
1662
 
1622
1663
  // src/shell/readyKey.ts
1623
1664
  var readyKey = (ready) => stableStringify({ schema: ready.schema, layout: ready.layout, viewport: ready.viewport });
1624
1665
  var START_TIMEOUT_MS = 1e4;
1625
- var HELLO_TIMEOUT_MS = 3e4;
1626
1666
  var requests = 0;
1627
1667
  function closeLink(link) {
1628
1668
  if (!link.current) return;
1629
1669
  clearTimeout(link.current.timer);
1630
- for (const settle2 of link.current.pending.values()) settle2.gone();
1670
+ for (const settle of link.current.pending.values()) settle({ ok: false, message: "The frame went away" });
1631
1671
  link.current.pending.clear();
1632
1672
  link.current.disconnect();
1633
1673
  link.current.channel.close();
1634
1674
  link.current = null;
1635
1675
  }
1636
- function settle(link, id, value) {
1637
- link.pending.get(id)?.resolve?.(value);
1638
- link.pending.delete(id);
1639
- }
1640
1676
  function mismatchMessage(mismatch) {
1641
1677
  return mismatch.reason === "version" ? `Frame speaks protocol ${String(mismatch.version)}; this workshop speaks ${PROTOCOL_VERSION}` : "Frame sent a message that is not a forge envelope";
1642
1678
  }
@@ -1700,20 +1736,13 @@ function FrameView(props) {
1700
1736
  case "vars":
1701
1737
  if (trialId) frames?.report(trialId, msg.vars);
1702
1738
  break;
1703
- case "size":
1704
- if (trialId) frames?.reportSize(trialId, { width: msg.width, height: msg.height });
1705
- break;
1706
1739
  case "a11y": {
1707
1740
  const outcome = msg.ok ? { ok: true, report: msg.report } : { ok: false, message: msg.message };
1708
1741
  if (trialId) frames?.reportA11y(trialId, outcome);
1709
- settle(current, msg.id, outcome);
1710
- break;
1711
- }
1712
- case "capture":
1713
- if (msg.ok) settle(current, msg.id, msg.picture);
1714
- else current.pending.get(msg.id)?.fail(new Error(msg.message));
1742
+ current.pending.get(msg.id)?.(outcome);
1715
1743
  current.pending.delete(msg.id);
1716
1744
  break;
1745
+ }
1717
1746
  case "open": {
1718
1747
  const { lab: liveLab } = latest.current;
1719
1748
  if (!trialId || !liveLab.instruments.some((instrument) => instrument.name === msg.id)) break;
@@ -1742,33 +1771,14 @@ function FrameView(props) {
1742
1771
  }
1743
1772
  });
1744
1773
  const waiting = /* @__PURE__ */ new Map();
1745
- const request = (id, msg, gone) => new Promise((resolve, reject) => {
1746
- waiting.set(id, {
1747
- resolve,
1748
- fail: reject,
1749
- gone: () => gone(resolve, reject)
1750
- });
1751
- const ask = () => channel.send(msg);
1774
+ const audit = () => new Promise((resolve) => {
1775
+ const id = `a11y-${requests += 1}`;
1776
+ waiting.set(id, resolve);
1777
+ const ask = () => channel.send({ type: "a11y.run", id });
1752
1778
  if (current.hasRendered) ask();
1753
1779
  else current.queued.push(ask);
1754
1780
  });
1755
- const audit = () => {
1756
- const id = `a11y-${requests += 1}`;
1757
- return request(
1758
- id,
1759
- { type: "a11y.run", id },
1760
- (resolve) => resolve({ ok: false, message: "The frame went away" })
1761
- );
1762
- };
1763
- const capture = () => {
1764
- const id = `capture-${requests += 1}`;
1765
- return request(
1766
- id,
1767
- { type: "capture.run", id },
1768
- (_resolve, fail) => fail(new Error("The frame could not draw itself"))
1769
- );
1770
- };
1771
- const disconnect = frames && trialId ? frames.connect(trialId, { send: channel.send, audit, capture }) : () => {
1781
+ const disconnect = frames && trialId ? frames.connect(trialId, { send: channel.send, audit }) : () => {
1772
1782
  };
1773
1783
  const current = {
1774
1784
  channel,
@@ -1861,23 +1871,13 @@ function FrameView(props) {
1861
1871
  }
1862
1872
  if (input) setFault((shown) => shown?.phase === "render" ? null : shown);
1863
1873
  }, [ctx.config, ctx.state, globals, descriptionKey, init]);
1864
- return /* @__PURE__ */ jsxs(
1865
- "div",
1866
- {
1867
- ref: (el) => {
1868
- hostRef.current = el;
1869
- if (trialId && frames) frames.hostRef(trialId).current = el;
1870
- },
1871
- className: "fg-frame-host",
1872
- children: [
1873
- /* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
1874
- fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
1875
- fault.phase ? /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: fault.phase }) : null,
1876
- /* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
1877
- ] }) : null
1878
- ]
1879
- }
1880
- );
1874
+ return /* @__PURE__ */ jsxs("div", { ref: hostRef, className: "fg-frame-host", children: [
1875
+ /* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
1876
+ fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
1877
+ fault.phase ? /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: fault.phase }) : null,
1878
+ /* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
1879
+ ] }) : null
1880
+ ] });
1881
1881
  }
1882
1882
  function storyInstrument(options) {
1883
1883
  const { entry, ready, answers, frameUrl, onReady, globals = {} } = options;
@@ -1915,7 +1915,7 @@ var NO_SETUP = {};
1915
1915
  var pruned = (map, ids) => [...map.keys()].every((id) => ids.has(id)) ? map : new Map([...map].filter(([id]) => ids.has(id)));
1916
1916
  var toError = (error) => error instanceof Error ? error : new Error(String(error));
1917
1917
  function useStoryRegistry(index, options) {
1918
- const { frameUrl, globals = NO_GLOBALS2, importers, setup = NO_SETUP, load = loadStories } = options;
1918
+ const { frameUrl, globals = NO_GLOBALS2, importers, setup = NO_SETUP, libraries = TITLE_LIBRARIES, load = loadStories } = options;
1919
1919
  const [readies, setReadies] = useState(() => /* @__PURE__ */ new Map());
1920
1920
  const [loaded, setLoaded] = useState(() => /* @__PURE__ */ new Map());
1921
1921
  const [faults, setFaults] = useState(() => /* @__PURE__ */ new Map());
@@ -1939,11 +1939,13 @@ function useStoryRegistry(index, options) {
1939
1939
  setRevisions((prev) => pruned(prev, ids));
1940
1940
  }
1941
1941
  const imports = useRef(/* @__PURE__ */ new Map());
1942
- const live = useLatest({ index, importers, setup, load });
1942
+ const live = useLatest({ index, importers, setup, load, libraries });
1943
+ const missing = useRef(/* @__PURE__ */ new Set());
1943
1944
  const importOf = useCallback((file) => {
1944
1945
  let loading = imports.current.get(file);
1945
1946
  if (!loading) {
1946
1947
  const importer = live.current.importers?.[file];
1948
+ if (!importer) missing.current.add(file);
1947
1949
  loading = importer ? importer() : Promise.reject(new Error(`No importer for ${file}`));
1948
1950
  imports.current.set(file, loading);
1949
1951
  }
@@ -1952,7 +1954,7 @@ function useStoryRegistry(index, options) {
1952
1954
  const request = useCallback(
1953
1955
  (entry) => {
1954
1956
  const { setup: current, load: loadModule } = live.current;
1955
- const settle2 = (ids2, result) => {
1957
+ const settle = (ids2, result) => {
1956
1958
  setLoaded((prev) => {
1957
1959
  const next = new Map(prev);
1958
1960
  for (const [id, value] of result) next.set(id, value);
@@ -1982,7 +1984,7 @@ function useStoryRegistry(index, options) {
1982
1984
  kind: "index",
1983
1985
  bundle: {
1984
1986
  title: entry.title,
1985
- library: libraryOf(entry),
1987
+ library: live.current.libraries.of(entry),
1986
1988
  ...entry.componentDescription === void 0 ? {} : { description: entry.componentDescription },
1987
1989
  ...entry.tags === void 0 ? {} : { tags: entry.tags },
1988
1990
  galleries: entries.filter(isGallery).map((e) => e.id),
@@ -1991,7 +1993,7 @@ function useStoryRegistry(index, options) {
1991
1993
  render: mods.map(indexRenderOf).find((render) => render !== null) ?? null
1992
1994
  }
1993
1995
  });
1994
- settle2([entry.id], result);
1996
+ settle([entry.id], result);
1995
1997
  }).catch((error) => fail([entry.id], error));
1996
1998
  return;
1997
1999
  }
@@ -2007,7 +2009,7 @@ function useStoryRegistry(index, options) {
2007
2009
  result.set(sibling.id, { kind: "story", story });
2008
2010
  }
2009
2011
  if (!result.has(entry.id)) throw new Error(`${entry.file} has no story export "${entry.exportName}"`);
2010
- settle2(ids, result);
2012
+ settle(ids, result);
2011
2013
  }).catch((error) => fail(ids, error));
2012
2014
  },
2013
2015
  [importOf, live]
@@ -2023,6 +2025,13 @@ function useStoryRegistry(index, options) {
2023
2025
  },
2024
2026
  [live]
2025
2027
  );
2028
+ useEffect(() => {
2029
+ for (const file of [...missing.current]) {
2030
+ if (!importers?.[file]) continue;
2031
+ missing.current.delete(file);
2032
+ reload(file);
2033
+ }
2034
+ }, [importers, reload]);
2026
2035
  const committed = useRef({ built: /* @__PURE__ */ new Map(), books: /* @__PURE__ */ new Map(), list: [] });
2027
2036
  const cache = useMemo(() => {
2028
2037
  const previous = committed.current;
@@ -2073,12 +2082,7 @@ function useStoryRegistry(index, options) {
2073
2082
  return { instruments: cache.list, isReady, onReady, reload };
2074
2083
  }
2075
2084
  var NO_DECLARATIONS = {};
2076
- var DEFAULT_TITLE = "weaselforge";
2077
- function initialStory(entries, fallback) {
2078
- const route = readRoute();
2079
- return route !== null && entries.some((entry) => entry.id === route) ? route : fallback;
2080
- }
2081
- function RouteOpener({ index }) {
2085
+ function RouteOpener() {
2082
2086
  const lab = useLabContext();
2083
2087
  const [route] = useRoute();
2084
2088
  const handled = useRef(null);
@@ -2086,14 +2090,14 @@ function RouteOpener({ index }) {
2086
2090
  useEffect(() => {
2087
2091
  const from = last.current;
2088
2092
  if (route !== from.route) last.current = { route, entry: readRouteEntry() };
2089
- if (route === null || handled.current === route || !index.some((e) => e.id === route)) return;
2093
+ if (route === null || handled.current === route || !lab.instruments.some((i) => i.name === route)) return;
2090
2094
  handled.current = route;
2091
2095
  if (lab.trials.some((trial) => trial.instrumentName === route)) return;
2092
2096
  const previous = lab.trials.find((trial) => trial.instrumentName === from.route);
2093
2097
  const target = (previous && crossesInPlace(from.entry, last.current.entry) ? previous : void 0) ?? lab.trials.find((trial) => trial.id === lab.focusedTrialId) ?? lab.trials[0];
2094
2098
  if (target) lab.swapTrial(target.id, route);
2095
2099
  else lab.addTrial(route);
2096
- }, [route, index, lab]);
2100
+ }, [route, lab]);
2097
2101
  return null;
2098
2102
  }
2099
2103
  function useInfoShortcut(open) {
@@ -2130,12 +2134,21 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2130
2134
  };
2131
2135
  }, [frameUrl, framed]);
2132
2136
  const entries = useMemo(() => [...index, ...indexEntries(index)], [index]);
2137
+ const table = config?.libraries;
2138
+ const libraries = useMemo(() => librariesFrom(table), [table]);
2133
2139
  const registry = useStoryRegistry(entries, {
2134
2140
  frameUrl,
2135
2141
  globals: declarations,
2142
+ libraries,
2136
2143
  ...importers ? { importers } : {},
2137
2144
  ...setup ? { setup } : {}
2138
2145
  });
2146
+ const [route] = useRoute();
2147
+ const missing = route !== null && !entries.some((entry) => entry.id === route) ? route : null;
2148
+ const instruments = useMemo(
2149
+ () => missing === null ? registry.instruments : [...registry.instruments, missingInstrument(missing, declarations)],
2150
+ [registry.instruments, missing, declarations]
2151
+ );
2139
2152
  const reload = registry.reload;
2140
2153
  useEffect(() => changes?.subscribe(reload), [changes, reload]);
2141
2154
  const [labValues, setLabValues] = useState(() => labGlobals(declarations, void 0));
@@ -2147,20 +2160,20 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2147
2160
  (next) => setLabValues((prev) => stableStringify(prev) === stableStringify(next) ? prev : next),
2148
2161
  []
2149
2162
  );
2163
+ const infoCommand = useMemo(
2164
+ () => ({ icon: InfoIcon, label: "Info", shortcut: "\u2318I", onActivate: () => setInfoOpen(true) }),
2165
+ []
2166
+ );
2150
2167
  const labChrome = useMemo(
2151
2168
  () => [
2152
2169
  { id: "fg-stories", region: "sidebar", render: (ctx) => /* @__PURE__ */ jsx(StoryTree, { ctx, index }) },
2153
- {
2154
- id: "fg-info",
2155
- region: "palette",
2156
- item: { icon: InfoIcon, label: "Info", shortcut: "\u2318I", onActivate: () => setInfoOpen(true) }
2157
- },
2170
+ { id: "fg-info", region: "palette", item: infoCommand },
2158
2171
  ...config?.cssVars === false ? [] : [CSS_VARS_SECTION],
2159
2172
  A11Y_SECTION,
2160
2173
  ...Object.keys(declarations).length > 0 ? [{ id: "fg-globals", region: "header", render: () => /* @__PURE__ */ jsx(GlobalsToolbar, { declarations }) }] : [],
2161
2174
  ...config?.labChrome ?? []
2162
2175
  ],
2163
- [index, declarations, config?.cssVars, config?.labChrome]
2176
+ [index, infoCommand, declarations, config?.cssVars, config?.labChrome]
2164
2177
  );
2165
2178
  const first = index[0];
2166
2179
  if (!first) {
@@ -2169,14 +2182,14 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2169
2182
  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
2170
2183
  ] });
2171
2184
  }
2172
- return /* @__PURE__ */ jsx(StoryGlobalsContext.Provider, { value: labValues, children: /* @__PURE__ */ jsx(DependenciesContext.Provider, { value: dependencies ?? null, children: /* @__PURE__ */ jsx(FramePoolContext.Provider, { value: pool, children: /* @__PURE__ */ jsx(TrialFramesContext.Provider, { value: frames, children: /* @__PURE__ */ jsx(TrialClocksContext.Provider, { value: clocks, children: /* @__PURE__ */ jsxs(
2185
+ return /* @__PURE__ */ jsx(LibrariesContext.Provider, { value: libraries, children: /* @__PURE__ */ jsx(StoryGlobalsContext.Provider, { value: labValues, children: /* @__PURE__ */ jsx(DependenciesContext.Provider, { value: dependencies ?? null, children: /* @__PURE__ */ jsx(FramePoolContext.Provider, { value: pool, children: /* @__PURE__ */ jsx(TrialFramesContext.Provider, { value: frames, children: /* @__PURE__ */ jsx(TrialClocksContext.Provider, { value: clocks, children: /* @__PURE__ */ jsxs(
2173
2186
  Lab,
2174
2187
  {
2175
2188
  title,
2176
2189
  density: "roomy",
2177
2190
  ...labTheme ? { theme: labTheme } : {},
2178
- instruments: registry.instruments,
2179
- defaultInstrument: initialStory(entries, first.id),
2191
+ instruments,
2192
+ defaultInstrument: readRoute() ?? first.id,
2180
2193
  storageKey: storageKey ?? "weaselforge",
2181
2194
  ...storage ? { storage } : {},
2182
2195
  labChrome,
@@ -2185,7 +2198,7 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2185
2198
  ...config?.pages ? { pages: config.pages } : {},
2186
2199
  ...config?.path !== void 0 ? { path: config.path } : {},
2187
2200
  children: [
2188
- /* @__PURE__ */ jsx(RouteOpener, { index: entries }),
2201
+ /* @__PURE__ */ jsx(RouteOpener, {}),
2189
2202
  /* @__PURE__ */ jsx(RouteKnobs, { instruments: registry.instruments, isReady: registry.isReady }),
2190
2203
  /* @__PURE__ */ jsx(RoutePlayhead, {}),
2191
2204
  /* @__PURE__ */ jsx(
@@ -2200,7 +2213,7 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2200
2213
  /* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues })
2201
2214
  ]
2202
2215
  }
2203
- ) }) }) }) }) });
2216
+ ) }) }) }) }) }) });
2204
2217
  }
2205
2218
  function mountWorkshop({ container, ...props }) {
2206
2219
  const target = container ?? document.getElementById("root");