@weasel-js/forge 1.9.3 → 1.10.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 (55) hide show
  1. package/README.md +34 -4
  2. package/dist/{FrameController-CpcY0DNB.d.ts → FrameController-UeTY7ubF.d.ts} +26 -36
  3. package/dist/{chunk-Q2EZXJFE.js → chunk-4NM3RTYK.js} +70 -56
  4. package/dist/chunk-4NM3RTYK.js.map +1 -0
  5. package/dist/chunk-5DXUNLV2.js +6 -0
  6. package/dist/chunk-5DXUNLV2.js.map +1 -0
  7. package/dist/{chunk-GZPQMGXJ.js → chunk-DFKCLZKS.js} +5 -4
  8. package/dist/chunk-DFKCLZKS.js.map +1 -0
  9. package/dist/{chunk-CJX3W5HY.js → chunk-DKWHGSBZ.js} +4 -3
  10. package/dist/chunk-DKWHGSBZ.js.map +1 -0
  11. package/dist/{chunk-BGOHALHO.js → chunk-ENXU34PI.js} +23 -111
  12. package/dist/chunk-ENXU34PI.js.map +1 -0
  13. package/dist/chunk-FL2MUPUL.js +10 -0
  14. package/dist/chunk-FL2MUPUL.js.map +1 -0
  15. package/dist/{chunk-MGQWCPYE.js → chunk-GB6PRJET.js} +4 -5
  16. package/dist/chunk-GB6PRJET.js.map +1 -0
  17. package/dist/{chunk-JVRHBI3V.js → chunk-V4RI7GWD.js} +53 -59
  18. package/dist/chunk-V4RI7GWD.js.map +1 -0
  19. package/dist/{chunk-HRGPFQBW.js → chunk-ZAWXMUGZ.js} +6 -3
  20. package/dist/chunk-ZAWXMUGZ.js.map +1 -0
  21. package/dist/{config-BfsYw5lg.d.ts → config-Cz7ZyGF-.d.ts} +28 -5
  22. package/dist/csf/preview-api.d.ts +5 -2
  23. package/dist/csf/preview-api.js +2 -2
  24. package/dist/csf/preview-api.js.map +1 -1
  25. package/dist/frame/index.css +19 -2
  26. package/dist/frame/index.css.map +1 -1
  27. package/dist/frame/index.d.ts +2 -2
  28. package/dist/frame/index.js +7 -41
  29. package/dist/frame/index.js.map +1 -1
  30. package/dist/index.d.ts +4 -4
  31. package/dist/index.js +3 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/shell/index.css +93 -62
  34. package/dist/shell/index.css.map +1 -1
  35. package/dist/shell/index.d.ts +10 -17
  36. package/dist/shell/index.js +724 -587
  37. package/dist/shell/index.js.map +1 -1
  38. package/dist/test/index.d.ts +4 -2
  39. package/dist/test/index.js +8 -8
  40. package/dist/test/index.js.map +1 -1
  41. package/dist/test/play.d.ts +2 -4
  42. package/dist/test/play.js.map +1 -1
  43. package/dist/vite/index.d.ts +19 -2
  44. package/dist/vite/index.js +751 -162
  45. package/dist/vite/index.js.map +1 -1
  46. package/package.json +10 -5
  47. package/dist/chunk-BGOHALHO.js.map +0 -1
  48. package/dist/chunk-CJX3W5HY.js.map +0 -1
  49. package/dist/chunk-GZPQMGXJ.js.map +0 -1
  50. package/dist/chunk-HRGPFQBW.js.map +0 -1
  51. package/dist/chunk-JVRHBI3V.js.map +0 -1
  52. package/dist/chunk-KJGXJD3U.js +0 -15
  53. package/dist/chunk-KJGXJD3U.js.map +0 -1
  54. package/dist/chunk-MGQWCPYE.js.map +0 -1
  55. package/dist/chunk-Q2EZXJFE.js.map +0 -1
@@ -1,24 +1,26 @@
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-DKWHGSBZ.js';
2
+ import { DEFAULT_TITLE } from '../chunk-5DXUNLV2.js';
3
+ import { TITLE_LIBRARIES, isIndexId, indexId, indexEntries, librariesFrom, LibrariesContext, schemaFromDescription, LibraryBadge, openChannel, IndexPage } from '../chunk-V4RI7GWD.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-ENXU34PI.js';
6
+ export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-ENXU34PI.js';
7
+ import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, PLAYHEAD_KEY, labOptions, isGlobalsPath } from '../chunk-DFKCLZKS.js';
8
+ import { loadStories, indexRenderOf, useNearViewport, IN_VIEW_MARGIN } from '../chunk-4NM3RTYK.js';
8
9
  import '../chunk-L5XM5VKB.js';
9
- import '../chunk-HRGPFQBW.js';
10
- import '../chunk-MGQWCPYE.js';
11
- import { stableStringify, PORT_HANDOFF, FRAME_HELLO, PROTOCOL_VERSION } from '../chunk-KJGXJD3U.js';
10
+ import '../chunk-ZAWXMUGZ.js';
11
+ import '../chunk-GB6PRJET.js';
12
+ import { stableStringify, PORT_HANDOFF, FRAME_HELLO, PROTOCOL_VERSION } from '../chunk-FL2MUPUL.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 { createEmitter } from '@weasel-js/registry';
19
+ import { f, valueAtPath, isConfigBranch } from '@weasel-js/labkit/config';
17
20
  import { InfoIcon, ToggleBar, Checkbox, Dialog, TOKEN_CATEGORIES, Icon, DisclosureMark, Button as Button$1, Callout, Slider, Transport, SearchIcon } from '@weasel-js/ui';
18
21
  import { TOKEN_MANIFEST, THEME_SOURCES, isByAxis } from '@weasel-js/theme';
19
22
  import { scale, httpThemeApi } from '@weasel-js/theme/engine';
20
- import { f, valueAtPath, isConfigBranch } from '@weasel-js/labkit/config';
21
- import { useLatest } from '@weasel-js/core';
23
+ import { useLatest, historyKey, createHistory, swapOp, isPlainObject } from '@weasel-js/core';
22
24
 
23
25
  function Finding({ finding }) {
24
26
  return /* @__PURE__ */ jsxs("li", { className: "fg-a11y__finding", children: [
@@ -44,19 +46,21 @@ function Findings({ title, findings }) {
44
46
  function A11yPanel() {
45
47
  const trialId = useTrialId();
46
48
  const frame = useTrialFrame(trialId);
47
- const [running, setRunning] = useState(false);
49
+ const [inFlight, setInFlight] = useState(0);
50
+ const running = inFlight > 0;
48
51
  const audit = frame.audit;
49
52
  const ran = useRef(null);
53
+ const start = useCallback((run) => {
54
+ setInFlight((n) => n + 1);
55
+ void run().finally(() => setInFlight((n) => n - 1));
56
+ }, []);
50
57
  useEffect(() => {
51
58
  if (!audit || ran.current === audit) return;
52
59
  ran.current = audit;
53
- setRunning(true);
54
- void audit().finally(() => setRunning(false));
55
- }, [audit]);
60
+ start(audit);
61
+ }, [audit, start]);
56
62
  const rerun = () => {
57
- if (!audit) return;
58
- setRunning(true);
59
- void audit().finally(() => setRunning(false));
63
+ if (audit) start(audit);
60
64
  };
61
65
  const outcome = frame.a11y;
62
66
  return /* @__PURE__ */ jsxs("div", { className: "fg-a11y", children: [
@@ -97,21 +101,310 @@ var A11Y_SECTION = {
97
101
  region: "aside",
98
102
  item: { title: "Accessibility", body: /* @__PURE__ */ jsx(FocusedA11y, {}) }
99
103
  };
100
-
101
- // src/shell/storyChanges.ts
102
104
  function createStoryChanges() {
103
- const listeners2 = /* @__PURE__ */ new Set();
105
+ const changes = createEmitter();
106
+ return { subscribe: changes.subscribe, emit: changes.emit };
107
+ }
108
+ var DependenciesContext = createContext(null);
109
+ function readRoute() {
110
+ return parseRoute(location.hash).story;
111
+ }
112
+ function readRouteParams() {
113
+ return { ...parseRoute(location.hash).params };
114
+ }
115
+ function replaceRouteParams(update) {
116
+ const { story, params } = parseRoute(location.hash);
117
+ if (story === null) return;
118
+ const next = formatRoute({ story, params: update({ ...params }) });
119
+ if (next !== location.hash) history.replaceState(history.state, "", next);
120
+ }
121
+ function readRouteEntry() {
122
+ const state = history.state;
123
+ const entry = state?.forgeRoute;
124
+ return typeof entry?.step === "number" ? entry : null;
125
+ }
126
+ function crossesInPlace(from, to) {
127
+ if (!from || !to || from.step === to.step) return false;
128
+ return to.step < from.step ? from.inPlace : to.inPlace;
129
+ }
130
+ function stamp(entry, hash) {
131
+ history.replaceState({ ...history.state ?? {}, forgeRoute: entry }, "", hash);
132
+ }
133
+ function setRoute(id, options = {}) {
134
+ if (readRoute() === id) {
135
+ history.replaceState(history.state, "", formatRoute({ story: id, params: readRouteParams() }));
136
+ return;
137
+ }
138
+ const hash = formatRoute({ story: id, params: {} });
139
+ const from = readRouteEntry() ?? { step: 0, inPlace: false };
140
+ stamp(from, location.hash);
141
+ location.hash = hash;
142
+ stamp({ step: from.step + 1, inPlace: options.inPlace ?? false, ...options.fromTrial ? { fromTrial: true } : {} }, hash);
143
+ }
144
+ function takeRouteFromTrial() {
145
+ const entry = readRouteEntry();
146
+ if (!entry?.fromTrial) return false;
147
+ stamp({ step: entry.step, inPlace: entry.inPlace }, location.hash);
148
+ return true;
149
+ }
150
+ function useRoute() {
151
+ const [route, setCurrent] = useState(readRoute);
152
+ useEffect(() => {
153
+ const sync = () => setCurrent(readRoute());
154
+ window.addEventListener("hashchange", sync);
155
+ sync();
156
+ return () => window.removeEventListener("hashchange", sync);
157
+ }, []);
158
+ return [route, setRoute];
159
+ }
160
+
161
+ // src/timeline/playheadUrl.ts
162
+ function formatSeconds(ms) {
163
+ const rounded = Math.round(ms) / 1e3;
164
+ return String(rounded === 0 ? 0 : rounded);
165
+ }
166
+ function parseSeconds(text) {
167
+ if (text == null || !/^\s*-?(\d+\.?\d*|\.\d+)\s*$/.test(text)) return null;
168
+ return Math.round(Number(text) * 1e3);
169
+ }
170
+ function readPlayheadParam() {
171
+ return parseSeconds(readRouteParams()[PLAYHEAD_PARAM]);
172
+ }
173
+ function writePlayheadParam(ms) {
174
+ replaceRouteParams(
175
+ ({ [PLAYHEAD_PARAM]: _held, ...rest }) => ms === null ? rest : { ...rest, [PLAYHEAD_PARAM]: formatSeconds(ms) }
176
+ );
177
+ }
178
+ function TransportBar({ clock }) {
179
+ const { time, playing, loop, rate, span } = useSyncExternalStore(clock.subscribe, clock.get);
180
+ return /* @__PURE__ */ jsxs("div", { className: "fg-transport", children: [
181
+ /* @__PURE__ */ jsx(
182
+ Slider,
183
+ {
184
+ className: "fg-transport__scrub",
185
+ ariaLabel: "Scrub",
186
+ density: "slim",
187
+ readoutPlacement: "none",
188
+ min: span.start,
189
+ max: span.end > span.start ? span.end : span.start + 1,
190
+ thumbs: [{ value: time, valueText: `${formatSeconds(time)} seconds` }],
191
+ onInput: ([thumb]) => {
192
+ if (thumb) clock.seek(thumb.value);
193
+ }
194
+ }
195
+ ),
196
+ /* @__PURE__ */ jsx(
197
+ Transport,
198
+ {
199
+ paused: !playing,
200
+ loop,
201
+ rate,
202
+ playhead: time,
203
+ duration: span.end,
204
+ onPlay: clock.play,
205
+ onPause: clock.pause,
206
+ onLoopChange: (next) => clock.setLoop(next !== false && next !== 0),
207
+ onRateChange: clock.setRate
208
+ }
209
+ )
210
+ ] });
211
+ }
212
+ function TrialTransport({ trialId }) {
213
+ const clock = useTrialClock(trialId);
214
+ return clock ? /* @__PURE__ */ jsx(TransportBar, { clock }) : null;
215
+ }
216
+
217
+ // src/shell/breadcrumb.ts
218
+ function crumbs(title, name) {
219
+ const segments = title.split("/");
220
+ const steps = segments.map((label, i) => ({ label, path: segments.slice(0, i + 1).join("/") }));
221
+ steps.push({ label: name, path: null });
222
+ return steps.filter((step, i) => i === 0 || step.label.toLowerCase() !== "index");
223
+ }
224
+ function breadcrumb(title, name) {
225
+ return crumbs(title, name).map((step) => step.label).join(" > ");
226
+ }
227
+ var reveals = createEmitter();
228
+ function revealInTree(path) {
229
+ reveals.emit(path);
230
+ }
231
+ function onRevealInTree(listener) {
232
+ return reveals.subscribe(listener);
233
+ }
234
+ function Crumbs({ entry }) {
235
+ const steps = crumbs(entry.title, entry.name);
236
+ return /* @__PURE__ */ jsx("span", { className: "fg-crumbs", children: steps.map((step, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
237
+ i > 0 ? /* @__PURE__ */ jsx("span", { className: "fg-crumbs__sep", "aria-hidden": "true", children: "\u203A" }) : null,
238
+ step.path === null ? /* @__PURE__ */ jsx("span", { className: "fg-crumbs__here", children: step.label }) : /* @__PURE__ */ jsx(
239
+ "button",
240
+ {
241
+ type: "button",
242
+ className: "fg-crumbs__step",
243
+ title: `Show ${step.label} in the story tree`,
244
+ onPointerDown: (event) => event.stopPropagation(),
245
+ onClick: () => revealInTree(step.path),
246
+ children: step.label
247
+ }
248
+ )
249
+ ] }, step.path ?? `${step.label}:${i}`)) });
250
+ }
251
+ function crumbTitle(entry) {
252
+ const title = breadcrumb(entry.title, entry.name);
253
+ return { title, renderTitle: (shown) => shown === title ? /* @__PURE__ */ jsx(Crumbs, { entry }) : shown };
254
+ }
255
+ function IndexTrial({ bundle, setup, config }) {
256
+ const lab = useLabContext();
257
+ const trialId = useContext(TrialIdContext);
258
+ const graph = useContext(DependenciesContext);
259
+ const gallery = isGallery(bundle);
260
+ const dependencies = gallery ? void 0 : graph?.[bundle.title];
261
+ const open = (id) => {
262
+ if (!trialId || !lab.instruments.some((instrument) => instrument.name === id)) return;
263
+ lab.swapTrial(trialId, id);
264
+ setRoute(id, { inPlace: true });
265
+ };
266
+ const render = (globals) => {
267
+ const env = {
268
+ title: bundle.title,
269
+ library: bundle.library,
270
+ ...bundle.description === void 0 ? {} : { description: bundle.description },
271
+ stories: bundle.stories,
272
+ descriptions: bundle.descriptions,
273
+ gallery,
274
+ galleries: bundle.galleries,
275
+ ...dependencies ? { dependencies } : {},
276
+ globals,
277
+ decorators: setup.decorators ?? [],
278
+ open,
279
+ // A story that throws shows its error in its own cell; the rest of the page is still worth reading.
280
+ onError: (error) => console.error(error)
281
+ };
282
+ return /* @__PURE__ */ jsx(IndexPage, { env, render: bundle.render });
283
+ };
284
+ return /* @__PURE__ */ jsx(TrialHost, { layout: "fullscreen", setup, config, children: render });
285
+ }
286
+ function withPlayhead(schema) {
287
+ return f.schema({ ...schema.nodes, [PLAYHEAD_KEY]: f.custom("forge-playhead", null).hidden() });
288
+ }
289
+ function documentInstrument(options) {
290
+ const { entry, loaded, setup, globals = {} } = options;
291
+ const schema = loaded.kind === "story" ? loaded.story.config : f.schema({});
292
+ const timed = loaded.kind === "story" && loaded.story.timeline !== null;
293
+ const config = withGlobals(timed ? withPlayhead(schema) : schema, globals);
294
+ const initialState = loaded.kind === "story" ? loaded.story.initialState : null;
104
295
  return {
105
- subscribe(listener) {
106
- listeners2.add(listener);
107
- return () => listeners2.delete(listener);
108
- },
109
- emit(file) {
110
- for (const listener of [...listeners2]) listener(file);
111
- }
296
+ name: entry.id,
297
+ ...crumbTitle(entry),
298
+ config,
299
+ defaultConfig: () => config.defaults(),
300
+ initialState: (c) => initialState?.(c) ?? null,
301
+ 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 }),
302
+ ...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {},
303
+ ...timed ? { chrome: [{ id: "fg-transport", region: "status", render: (c) => /* @__PURE__ */ jsx(TrialTransport, { trialId: c.trialId }) }] } : {}
112
304
  };
113
305
  }
114
- var DependenciesContext = createContext(null);
306
+ function Fault({ phase, message }) {
307
+ return /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
308
+ /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: phase }),
309
+ /* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: message })
310
+ ] });
311
+ }
312
+ function StoryLoad({ entry, load, fault }) {
313
+ useEffect(() => {
314
+ if (!fault) load(entry);
315
+ }, [entry, load, fault]);
316
+ 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 });
317
+ }
318
+ function pendingInstrument(options) {
319
+ const { entry, globals = {} } = options;
320
+ const config = withGlobals(f.schema({}), globals);
321
+ return {
322
+ name: entry.id,
323
+ ...crumbTitle(entry),
324
+ config,
325
+ defaultConfig: () => config.defaults(),
326
+ initialState: () => null,
327
+ render: () => /* @__PURE__ */ jsx(StoryLoad, { ...options }, options.generation ?? 0)
328
+ };
329
+ }
330
+ function missingInstrument(id, globals = {}) {
331
+ const config = withGlobals(f.schema({}), globals);
332
+ return {
333
+ name: id,
334
+ title: `No story ${id}`,
335
+ config,
336
+ defaultConfig: () => config.defaults(),
337
+ initialState: () => null,
338
+ render: () => /* @__PURE__ */ jsx("div", { className: "fg-story fg-story--loading", "data-fg-layout": "fullscreen", children: /* @__PURE__ */ jsx(Fault, { phase: "route", message: `No story ${id}` }) })
339
+ };
340
+ }
341
+ var ICONS = {
342
+ color: "drop",
343
+ type: "type",
344
+ size: "measure",
345
+ motion: "curveEaseInOut",
346
+ depth: "layers",
347
+ other: "more"
348
+ };
349
+ var PANEL = ".fg-css-vars";
350
+ var HOST = ".lk-sidebar-section, .lk-panel-tile";
351
+ var MANUAL = ["wheel", "touchmove", "keydown"];
352
+ var LIST = ".fg-css-vars__list";
353
+ function readLine(panel) {
354
+ return (panel.querySelector(LIST) ?? panel).getBoundingClientRect().top;
355
+ }
356
+ function sectionsOf(panel) {
357
+ return [...panel.querySelectorAll("[data-token-category]")];
358
+ }
359
+ function SectionJump() {
360
+ const ref = useRef(null);
361
+ const [present, setPresent] = useState([]);
362
+ const [current, setCurrent] = useState(null);
363
+ const held = useRef(false);
364
+ useEffect(() => {
365
+ const root = ref.current?.closest(HOST) ?? document.body;
366
+ const read = () => {
367
+ const panel = root.querySelector(PANEL);
368
+ const sections = panel ? sectionsOf(panel) : [];
369
+ setPresent(sections.map((s) => s.dataset.tokenCategory));
370
+ if (!panel || held.current) return;
371
+ const line = readLine(panel) + 1;
372
+ let top = sections[0]?.dataset.tokenCategory;
373
+ for (const s of sections) if (s.getBoundingClientRect().top <= line) top = s.dataset.tokenCategory;
374
+ setCurrent(top ?? null);
375
+ };
376
+ const release = () => {
377
+ held.current = false;
378
+ };
379
+ read();
380
+ const observer = new MutationObserver(read);
381
+ observer.observe(root, { childList: true, subtree: true });
382
+ document.addEventListener("scroll", read, { capture: true, passive: true });
383
+ for (const type of MANUAL) document.addEventListener(type, release, { capture: true, passive: true });
384
+ return () => {
385
+ observer.disconnect();
386
+ document.removeEventListener("scroll", read, { capture: true });
387
+ for (const type of MANUAL) document.removeEventListener(type, release, { capture: true });
388
+ };
389
+ }, []);
390
+ const items = TOKEN_CATEGORIES.map(({ id, title }) => ({
391
+ value: id,
392
+ label: /* @__PURE__ */ jsx(Icon, { name: ICONS[id], size: 14 }),
393
+ ariaLabel: title,
394
+ tooltip: title,
395
+ disabled: !present.includes(id)
396
+ }));
397
+ const jump = (next) => {
398
+ const panel = ref.current?.closest(HOST)?.querySelector(PANEL);
399
+ const target = next && panel?.querySelector(`[data-token-category="${next}"]`);
400
+ const scroller = panel?.querySelector(LIST);
401
+ if (!panel || !target || !scroller) return;
402
+ held.current = true;
403
+ setCurrent(next);
404
+ scroller.scrollTop += target.getBoundingClientRect().top - readLine(panel);
405
+ };
406
+ return /* @__PURE__ */ jsx("div", { ref, className: "fg-css-vars__jump", children: /* @__PURE__ */ jsx(ToggleBar, { ariaLabel: "Jump to section", variant: "flat", size: "sm", items, value: current, onChange: jump }) });
407
+ }
115
408
  var SEED_REF = /^\{seeds\.([^}]+)\}$/;
116
409
  var RULE_KEYS = { factors: "factors", ratio: "ratio", step: "step" };
117
410
  function selectionFor(axes, globals) {
@@ -125,24 +418,24 @@ function selectionFor(axes, globals) {
125
418
  }
126
419
  var EditError = class extends Error {
127
420
  };
128
- function slotOf(root, holder, key, where, selection, depth = 0) {
129
- if (depth > 16) throw new EditError(`${where} references itself`);
130
- let h = holder;
131
- let k = key;
132
- let w = where;
133
- let value = h[k];
134
- while (isByAxis(value)) {
421
+ function branchOf(holder, key, where, selection) {
422
+ let slot = { holder, key, where };
423
+ for (let value = holder[key]; isByAxis(value); value = slot.holder[slot.key]) {
135
424
  const axis = value.by;
136
425
  const picked = selection[axis];
137
426
  if (picked === void 0 || picked === "by" || !(picked in value)) {
138
- throw new EditError(`${w} differs by ${axis}, and which ${axis} this trial shows is not known`);
427
+ throw new EditError(`${slot.where} differs by ${axis}, and which ${axis} this trial shows is not known`);
139
428
  }
140
- h = value;
141
- k = picked;
142
- w = `${w}.${picked}`;
143
- value = h[k];
429
+ slot = { holder: value, key: picked, where: `${slot.where}.${picked}` };
144
430
  }
145
- if (typeof value === "number") return { holder: h, key: k, where: w };
431
+ return slot;
432
+ }
433
+ function slotOf(root, holder, key, where, selection, depth = 0) {
434
+ if (depth > 16) throw new EditError(`${where} references itself`);
435
+ const slot = branchOf(holder, key, where, selection);
436
+ const { holder: h, key: k, where: w } = slot;
437
+ const value = h[k];
438
+ if (typeof value === "number") return slot;
146
439
  if (typeof value === "string") {
147
440
  const ref = SEED_REF.exec(value);
148
441
  if (!ref) throw new EditError(`${w} is "${value}", which is not a number or a seed`);
@@ -154,18 +447,10 @@ function slotOf(root, holder, key, where, selection, depth = 0) {
154
447
  throw new EditError(`${w} is not a number`);
155
448
  }
156
449
  function factorsOf(holder, where, selection) {
157
- let value = holder.factors;
158
- let w = `${where}.factors`;
159
- while (isByAxis(value)) {
160
- const picked = selection[value.by];
161
- if (picked === void 0 || !(picked in value)) {
162
- throw new EditError(`${w} differs by ${value.by}, and which ${value.by} this trial shows is not known`);
163
- }
164
- w = `${w}.${picked}`;
165
- value = value[picked];
166
- }
167
- if (!Array.isArray(value)) throw new EditError(`${w} is not a list`);
168
- return { list: value, where: w };
450
+ const slot = branchOf(holder, "factors", `${where}.factors`, selection);
451
+ const value = slot.holder[slot.key];
452
+ if (!Array.isArray(value)) throw new EditError(`${slot.where} is not a list`);
453
+ return { list: value, where: slot.where };
169
454
  }
170
455
  var kindOf = (scale) => scale.factors !== void 0 ? "factors" : scale.ratio !== void 0 ? "ratio" : scale.step !== void 0 ? "step" : null;
171
456
  var plainLiteral = (value) => typeof value === "number" || Array.isArray(value) && value.every((v) => typeof v === "number");
@@ -221,90 +506,196 @@ function applyScaleEdits(definition, edits, selection) {
221
506
  for (const [holder, byKey] of writes) for (const [key, value] of byKey) holder[key] = value;
222
507
  return { ok: true, definition: root };
223
508
  }
224
- var ICONS = {
225
- color: "drop",
226
- type: "type",
227
- size: "measure",
228
- motion: "curveEaseInOut",
229
- depth: "layers",
230
- other: "more"
231
- };
232
- var PANEL = ".fg-css-vars";
233
- var HOST = ".lk-sidebar-section, .lk-panel-tile";
234
- var MANUAL = ["wheel", "touchmove", "keydown"];
235
- function scrollerOf(el) {
236
- for (let at = el.parentElement; at; at = at.parentElement) {
237
- const { overflowY } = getComputedStyle(at);
238
- if ((overflowY === "auto" || overflowY === "scroll") && at.scrollHeight > at.clientHeight) return at;
509
+ var REFERENCE = /\{[^}]+\}/;
510
+ var LAYERS = ["pins", "semantics", "components"];
511
+ function derivation(entry) {
512
+ const value = isPlainObject(entry) ? entry.value : entry;
513
+ if (isPlainObject(entry) && value === void 0) return "follows a rule";
514
+ if (isPlainObject(entry) && entry.alpha !== void 0) return "is drawn at an alpha";
515
+ if (typeof value === "string" && REFERENCE.test(value)) return `follows ${REFERENCE.exec(value)?.[0]}`;
516
+ return typeof value === "string" || typeof value === "number" ? null : "is a list";
517
+ }
518
+ function literalSlot(definition, token, selection) {
519
+ for (const layer of LAYERS) {
520
+ const entries = definition[layer];
521
+ if (!entries || !(token in entries)) continue;
522
+ const slot = branchOf(entries, token, `${layer}.${token}`, selection);
523
+ const why = derivation(slot.holder[slot.key]);
524
+ if (why) throw new EditError(`${slot.where} ${why}, and only a value written out in the theme can be saved`);
525
+ return slot;
239
526
  }
240
- return null;
527
+ for (const layer of ["ramps", "scales"]) {
528
+ const from = Object.keys(definition[layer] ?? {}).find((name) => token.startsWith(`${name}-`));
529
+ if (from) throw new EditError(`${token} is a step ${layer}.${from} generates, and the theme pins no value over it`);
530
+ }
531
+ throw new EditError(`the theme defines no token "${token}"`);
241
532
  }
242
- function readLine(panel) {
243
- return panel.querySelector(".fg-css-vars__controls")?.getBoundingClientRect().bottom ?? panel.getBoundingClientRect().top;
533
+ function writtenOut(definition, token, selection) {
534
+ try {
535
+ literalSlot(definition, token, selection);
536
+ return true;
537
+ } catch (e) {
538
+ if (e instanceof EditError) return false;
539
+ throw e;
540
+ }
244
541
  }
245
- function sectionsOf(panel) {
246
- return [...panel.querySelectorAll("[data-token-category]")];
542
+ function applyTokenEdits(definition, edits, selection) {
543
+ const root = structuredClone(definition);
544
+ try {
545
+ for (const [token, value] of Object.entries(edits)) {
546
+ const { holder, key } = literalSlot(root, token, selection);
547
+ const entry = holder[key];
548
+ holder[key] = isPlainObject(entry) ? { ...entry, value } : value;
549
+ }
550
+ } catch (e) {
551
+ if (e instanceof EditError) return { ok: false, message: `Cannot save: ${e.message}.` };
552
+ throw e;
553
+ }
554
+ return { ok: true, definition: root };
247
555
  }
248
- function SectionJump() {
249
- const ref = useRef(null);
250
- const [present, setPresent] = useState([]);
251
- const [current, setCurrent] = useState(null);
252
- const held = useRef(false);
556
+
557
+ // src/shell/cssVars/useThemeSave.ts
558
+ var THEME_NAME = "weasel";
559
+ var TOKEN_PREFIX = "--wzl-";
560
+ function useThemeSave({ trialId, globals, themeApi, scales, overrides, history: history2 }) {
561
+ const tokens = useMemo(() => {
562
+ const definition = THEME_SOURCES[THEME_NAME];
563
+ if (!definition) return {};
564
+ const selection = selectionFor(definition.axes ?? {}, globals);
565
+ const generated = new Set(scales.flatMap((scale) => scale.tokens));
566
+ return Object.fromEntries(
567
+ Object.entries(overrides).flatMap(([name, value]) => {
568
+ if (!name.startsWith(TOKEN_PREFIX) || generated.has(name)) return [];
569
+ const token = name.slice(TOKEN_PREFIX.length);
570
+ return writtenOut(definition, token, selection) ? [[token, value]] : [];
571
+ })
572
+ );
573
+ }, [globals, scales, overrides]);
574
+ const [saving, setSaving] = useState(false);
575
+ const [held, setHeld] = useState(null);
576
+ const setNotice = (next) => setHeld(next && { ...next, trialId });
577
+ const save = async () => {
578
+ setSaving(true);
579
+ setNotice(null);
580
+ try {
581
+ const api = themeApi ?? httpThemeApi();
582
+ const stored = await api.get(THEME_NAME);
583
+ const file = `themes/${THEME_NAME}.json`;
584
+ if (JSON.stringify(stored.definition) !== JSON.stringify(THEME_SOURCES[THEME_NAME])) {
585
+ setNotice({ tone: "error", text: `${file} has changed since this page loaded. Reload to edit against it.` });
586
+ return;
587
+ }
588
+ const selection = selectionFor(stored.definition.axes ?? {}, globals);
589
+ const scaled = applyScaleEdits(stored.definition, Object.fromEntries(scales.map((s) => [s.name, s.edit])), selection);
590
+ const next = scaled.ok ? applyTokenEdits(scaled.definition, tokens, selection) : scaled;
591
+ if (!next.ok) {
592
+ setNotice({ tone: "error", text: next.message });
593
+ return;
594
+ }
595
+ const result = await api.put(THEME_NAME, next.definition, stored.hash);
596
+ if (result.status === "conflict") {
597
+ setNotice({ tone: "error", text: `${file} changed on disk while saving. Nothing was written; reload to edit against it.` });
598
+ return;
599
+ }
600
+ if (result.status === "invalid") {
601
+ setNotice({ tone: "error", text: result.message });
602
+ return;
603
+ }
604
+ const saved = /* @__PURE__ */ new Set([
605
+ ...scales.flatMap((scale) => scale.tokens),
606
+ ...Object.keys(tokens).map((token) => `${TOKEN_PREFIX}${token}`)
607
+ ]);
608
+ const groups = new Set(scales.map((scale) => scale.group));
609
+ const now = history2.current();
610
+ history2.reset({
611
+ overrides: Object.fromEntries(Object.entries(now.overrides).filter(([name]) => !saved.has(name))),
612
+ scales: Object.fromEntries(Object.entries(now.scales).filter(([group]) => !groups.has(group)))
613
+ });
614
+ setNotice(
615
+ 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("; ")}` }
616
+ );
617
+ } catch (e) {
618
+ setNotice({ tone: "error", text: `Saving needs forge's dev server, which did not answer: ${e.message}` });
619
+ } finally {
620
+ setSaving(false);
621
+ }
622
+ };
623
+ return {
624
+ pending: scales.length > 0 || Object.keys(tokens).length > 0,
625
+ saving,
626
+ notice: held?.trialId === trialId ? held : null,
627
+ save
628
+ };
629
+ }
630
+ var SWAP = "fg.vars.swap";
631
+ var COALESCE_MS = 800;
632
+ var LIMIT = 100;
633
+ function useVarsHistory(state, apply) {
634
+ const trialId = useTrialId();
635
+ const [stored, setStored] = usePersistedState("fg-css-vars-history", null, { scope: "trial" });
636
+ const rendered = useLatest(state);
637
+ const ahead = useRef(null);
253
638
  useEffect(() => {
254
- const root = ref.current?.closest(HOST) ?? document.body;
255
- const read = () => {
256
- const panel = root.querySelector(PANEL);
257
- const sections = panel ? sectionsOf(panel) : [];
258
- setPresent(sections.map((s) => s.dataset.tokenCategory));
259
- if (!panel || held.current) return;
260
- const line = readLine(panel) + 1;
261
- let top = sections[0]?.dataset.tokenCategory;
262
- for (const s of sections) if (s.getBoundingClientRect().top <= line) top = s.dataset.tokenCategory;
263
- setCurrent(top ?? null);
264
- };
265
- const release = () => {
266
- held.current = false;
267
- };
268
- read();
269
- const observer = new MutationObserver(read);
270
- observer.observe(root, { childList: true, subtree: true });
271
- document.addEventListener("scroll", read, { capture: true, passive: true });
272
- for (const type of MANUAL) document.addEventListener(type, release, { capture: true, passive: true });
273
- return () => {
274
- observer.disconnect();
275
- document.removeEventListener("scroll", read, { capture: true });
276
- for (const type of MANUAL) document.removeEventListener(type, release, { capture: true });
277
- };
278
- }, []);
279
- const items = TOKEN_CATEGORIES.map(({ id, title }) => ({
280
- value: id,
281
- label: /* @__PURE__ */ jsx(Icon, { name: ICONS[id], size: 14 }),
282
- ariaLabel: title,
283
- tooltip: title,
284
- disabled: !present.includes(id)
285
- }));
286
- const jump = (next) => {
287
- const panel = ref.current?.closest(HOST)?.querySelector(PANEL);
288
- const target = next && panel?.querySelector(`[data-token-category="${next}"]`);
289
- const scroller = panel && scrollerOf(panel);
290
- if (!panel || !target || !scroller) return;
291
- held.current = true;
292
- setCurrent(next);
293
- scroller.scrollTop += target.getBoundingClientRect().top - readLine(panel);
639
+ ahead.current = null;
640
+ }, [state]);
641
+ const current = () => ahead.current ?? rendered.current;
642
+ const latestApply = useLatest(apply);
643
+ const emit = (to) => {
644
+ const from = current();
645
+ ahead.current = to;
646
+ latestApply.current(from, to);
647
+ };
648
+ const swap = (before, after, coalesceKey) => swapOp({ name: SWAP, label: "edit vars", before, after, emit, ...coalesceKey !== void 0 ? { coalesceKey } : {} });
649
+ const history2 = useMemo(() => {
650
+ const opened = createHistory(null, {
651
+ coalesceWindowMs: COALESCE_MS,
652
+ historyLimit: LIMIT,
653
+ rebuildOp: (name, args) => {
654
+ const { before, after } = args;
655
+ return name === SWAP ? swap(before, after) : null;
656
+ }
657
+ });
658
+ if (stored) opened.restore(stored);
659
+ return opened;
660
+ }, [trialId]);
661
+ useSyncExternalStore(history2.subscribe, history2.getVersion);
662
+ const keep = () => setStored(history2.canUndo() || history2.canRedo() ? history2.serialize() : null);
663
+ const tick = useRef(null);
664
+ return {
665
+ current,
666
+ commit(next, key) {
667
+ if (tick.current === null) {
668
+ tick.current = key;
669
+ queueMicrotask(() => {
670
+ tick.current = null;
671
+ });
672
+ }
673
+ history2.applyOps([swap(current(), next, tick.current)], "edit vars");
674
+ keep();
675
+ },
676
+ step(to) {
677
+ history2[to]();
678
+ keep();
679
+ },
680
+ canUndo: history2.canUndo(),
681
+ canRedo: history2.canRedo(),
682
+ reset(next) {
683
+ emit(next);
684
+ history2.clear();
685
+ setStored(null);
686
+ }
294
687
  };
295
- return /* @__PURE__ */ jsx("div", { ref, className: "fg-css-vars__jump", children: /* @__PURE__ */ jsx(ToggleBar, { ariaLabel: "Jump to section", variant: "flat", size: "sm", items, value: current, onChange: jump }) });
296
688
  }
297
689
  var TABS = [
298
690
  { value: "theme", label: "Theme" },
299
691
  { value: "story", label: "Story" }
300
692
  ];
301
693
  var MANIFEST = new Map(TOKEN_MANIFEST.map((token) => [token.name, token]));
302
- var THEME_NAME = "weasel";
303
694
  var literal = (v) => typeof v === "number";
304
695
  var SCALE_SOURCES = Object.entries(THEME_SOURCES[THEME_NAME]?.scales ?? {}).flatMap(([name, def]) => {
305
696
  if (!Array.isArray(def.steps)) return [];
306
697
  const steps = def.steps;
307
- const tokens = steps.map((step) => `--wzl-${name}-${step}`);
698
+ const tokens = steps.map((step) => `${TOKEN_PREFIX}${name}-${step}`);
308
699
  const group = MANIFEST.get(tokens[0] ?? "")?.group;
309
700
  if (!group) return [];
310
701
  const factors = Array.isArray(def.factors) && def.factors.every(literal) ? def.factors : null;
@@ -349,7 +740,7 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
349
740
  {},
350
741
  { scope: "lab" }
351
742
  );
352
- const [edited, setEdited] = usePersistedState("fg-css-vars-scales", {}, { scope: "lab" });
743
+ const [edited, setEdited] = usePersistedState("fg-css-vars-scales", {}, { scope: "trial" });
353
744
  const [tab, setTab] = useState("theme");
354
745
  const [filter, setFilter] = useState("");
355
746
  const tokens = useMemo(() => {
@@ -386,135 +777,115 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
386
777
  return out;
387
778
  }, [tab, shown, overrides, edited]);
388
779
  const send = frame.send;
389
- const write = useCallback(
390
- (name, value) => {
391
- setOverrides((prev) => {
392
- const { [name]: _dropped, ...rest } = prev;
393
- return value === null ? rest : { ...rest, [name]: value };
394
- });
395
- send?.({ type: "vars.set", name, value });
396
- },
397
- [setOverrides, send]
398
- );
399
- const onScaleChange = useCallback(
400
- (group, next) => {
401
- const source = SCALE_SOURCES.find((sc) => sc.group === group);
402
- if (!source) return;
403
- setEdited((prev) => ({ ...prev, [group]: next }));
404
- for (const [name, value] of scaleValues(source, next)) write(name, value);
405
- },
406
- [setEdited, write]
407
- );
780
+ const state = useMemo(() => ({ overrides, scales: edited }), [overrides, edited]);
781
+ const history2 = useVarsHistory(state, (from, to) => {
782
+ setOverrides(to.overrides);
783
+ setEdited(to.scales);
784
+ for (const name of /* @__PURE__ */ new Set([...Object.keys(from.overrides), ...Object.keys(to.overrides)])) {
785
+ if (from.overrides[name] !== to.overrides[name]) send?.({ type: "vars.set", name, value: to.overrides[name] ?? null });
786
+ }
787
+ });
788
+ const write = (name, value) => {
789
+ const now = history2.current();
790
+ const { [name]: _dropped, ...rest } = now.overrides;
791
+ history2.commit({ ...now, overrides: value === null ? rest : { ...rest, [name]: value } }, `token:${name}`);
792
+ };
793
+ const onScaleChange = (group, next) => {
794
+ const source = SCALE_SOURCES.find((sc) => sc.group === group);
795
+ if (!source) return;
796
+ const now = history2.current();
797
+ history2.commit(
798
+ { overrides: { ...now.overrides, ...Object.fromEntries(scaleValues(source, next)) }, scales: { ...now.scales, [group]: next } },
799
+ `scale:${group}`
800
+ );
801
+ };
408
802
  const unsaved = useMemo(
409
803
  () => SCALE_SOURCES.flatMap((source) => {
410
804
  const edit = edited[source.group];
411
- if (!edit || !source.tokens.some((name) => overrides[name] !== void 0)) return [];
412
- const values = scaleValues(source, edit);
413
- return values.every(([name, value]) => overrides[name] === value) ? [{ source, edit }] : [];
805
+ if (!edit || !source.tokens.some((name2) => overrides[name2] !== void 0)) return [];
806
+ const { name, group, tokens: tokens2 } = source;
807
+ const clean = scaleValues(source, edit).every(([token, value]) => overrides[token] === value);
808
+ return clean ? [{ name, group, tokens: tokens2, edit: { base: edit.base, rule: edit.rule } }] : [];
414
809
  }),
415
810
  [edited, overrides]
416
811
  );
417
- const [saving, setSaving] = useState(false);
418
- const [held, setHeld] = useState(null);
419
- const notice = held?.trialId === trialId ? held : null;
420
- const setNotice = (next) => setHeld(next && { ...next, trialId });
421
- const save = async () => {
422
- setSaving(true);
423
- setNotice(null);
424
- try {
425
- const api = themeApi ?? httpThemeApi();
426
- const stored = await api.get(THEME_NAME);
427
- const file = `themes/${THEME_NAME}.json`;
428
- if (JSON.stringify(stored.definition) !== JSON.stringify(THEME_SOURCES[THEME_NAME])) {
429
- setNotice({ tone: "error", text: `${file} has changed since this page loaded. Reload to edit against it.` });
430
- return;
431
- }
432
- const edits = Object.fromEntries(
433
- unsaved.map(({ source, edit }) => [source.name, { base: edit.base, rule: edit.rule }])
434
- );
435
- const next = applyScaleEdits(stored.definition, edits, selectionFor(stored.definition.axes ?? {}, globals));
436
- if (!next.ok) {
437
- setNotice({ tone: "error", text: next.message });
438
- return;
439
- }
440
- const result = await api.put(THEME_NAME, next.definition, stored.hash);
441
- if (result.status === "conflict") {
442
- setNotice({ tone: "error", text: `${file} changed on disk while saving. Nothing was written; reload to edit against it.` });
443
- return;
444
- }
445
- if (result.status === "invalid") {
446
- setNotice({ tone: "error", text: result.message });
447
- return;
448
- }
449
- for (const { source } of unsaved) for (const name of source.tokens) write(name, null);
450
- const groups = new Set(unsaved.map(({ source }) => source.group));
451
- setEdited((prev) => Object.fromEntries(Object.entries(prev).filter(([group]) => !groups.has(group))));
452
- setNotice(
453
- 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("; ")}` }
454
- );
455
- } catch (e) {
456
- setNotice({ tone: "error", text: `Saving needs forge's dev server, which did not answer: ${e.message}` });
457
- } finally {
458
- setSaving(false);
459
- }
460
- };
812
+ const { pending, saving, notice, save } = useThemeSave({ trialId, globals, themeApi, scales: unsaved, overrides, history: history2 });
461
813
  const onCollapsedChange = useCallback(
462
814
  (category, collapsed) => setFolds((prev) => ({ ...prev, [category]: collapsed })),
463
815
  [setFolds]
464
- );
465
- return /* @__PURE__ */ jsxs("div", { className: "fg-css-vars", children: [
466
- /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__controls", children: [
467
- /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__bar", children: [
468
- /* @__PURE__ */ jsx(
469
- ToggleBar$1,
470
- {
471
- ariaLabel: "Variables",
472
- variant: "flat",
473
- size: "sm",
474
- items: TABS,
475
- value: tab,
476
- onChange: (next) => {
477
- if (next) setTab(next);
478
- }
479
- }
480
- ),
481
- /* @__PURE__ */ jsx(
482
- Input,
483
- {
484
- className: "fg-css-vars__filter",
485
- "aria-label": "Filter",
486
- placeholder: "Filter",
487
- value: filter,
488
- onChange: setFilter,
489
- leadingAdornment: /* @__PURE__ */ jsx(SearchIcon, { size: 12 })
490
- }
491
- )
492
- ] }),
493
- tab === "theme" && (unsaved.length > 0 || notice) ? /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__save", children: [
494
- unsaved.length > 0 ? /* @__PURE__ */ jsx(Button$1, { variant: "primary", size: "sm", loading: saving, disabled: saving, onClick: () => void save(), children: "Save scales to theme" }) : null,
495
- notice ? /* @__PURE__ */ jsx(
496
- "p",
497
- {
498
- className: `fg-css-vars__notice fg-css-vars__notice--${notice.tone}`,
499
- role: notice.tone === "error" ? "alert" : "status",
500
- children: notice.text
501
- }
502
- ) : null
503
- ] }) : null
504
- ] }),
505
- 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(
506
- TokenPanel,
816
+ );
817
+ return (
818
+ // Capture: React Aria's fields stop a keydown from bubbling past them.
819
+ /* @__PURE__ */ jsxs(
820
+ "div",
507
821
  {
508
- tokens: shown,
509
- namePrefix: "--wzl-",
510
- onChange: write,
511
- scales,
512
- onScaleChange,
513
- collapsed: folds,
514
- onCollapsedChange
822
+ className: "fg-css-vars",
823
+ onKeyDownCapture: (e) => {
824
+ const to = e.target.closest(".fg-css-vars__filter") ? null : historyKey(e);
825
+ if (!to) return;
826
+ e.preventDefault();
827
+ history2.step(to);
828
+ },
829
+ children: [
830
+ /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__controls", children: [
831
+ /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__bar", children: [
832
+ /* @__PURE__ */ jsx(
833
+ ToggleBar$1,
834
+ {
835
+ ariaLabel: "Variables",
836
+ variant: "flat",
837
+ size: "sm",
838
+ items: TABS,
839
+ value: tab,
840
+ onChange: (next) => {
841
+ if (next) setTab(next);
842
+ }
843
+ }
844
+ ),
845
+ /* @__PURE__ */ jsx(
846
+ Input,
847
+ {
848
+ className: "fg-css-vars__filter",
849
+ "aria-label": "Filter",
850
+ placeholder: "Filter",
851
+ value: filter,
852
+ onChange: setFilter,
853
+ leadingAdornment: /* @__PURE__ */ jsx(SearchIcon, { size: 12 })
854
+ }
855
+ ),
856
+ /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__steps", role: "group", "aria-label": "History", children: [
857
+ /* @__PURE__ */ jsx(Button$1, { iconOnly: true, ariaLabel: "Undo", variant: "ghost", size: "sm", disabled: !history2.canUndo, onClick: () => history2.step("undo"), children: /* @__PURE__ */ jsx(Icon, { name: "undo", size: 14 }) }),
858
+ /* @__PURE__ */ jsx(Button$1, { iconOnly: true, ariaLabel: "Redo", variant: "ghost", size: "sm", disabled: !history2.canRedo, onClick: () => history2.step("redo"), children: /* @__PURE__ */ jsx(Icon, { name: "redo", size: 14 }) })
859
+ ] })
860
+ ] }),
861
+ tab === "theme" && (pending || notice) ? /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__save", children: [
862
+ pending ? /* @__PURE__ */ jsx(Button$1, { variant: "primary", size: "sm", loading: saving, disabled: saving, onClick: () => void save(), children: "Save to theme" }) : null,
863
+ notice ? /* @__PURE__ */ jsx(
864
+ "p",
865
+ {
866
+ className: `fg-css-vars__notice fg-css-vars__notice--${notice.tone}`,
867
+ role: notice.tone === "error" ? "alert" : "status",
868
+ children: notice.text
869
+ }
870
+ ) : null
871
+ ] }) : null
872
+ ] }),
873
+ 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("div", { className: "fg-css-vars__list", children: /* @__PURE__ */ jsx(
874
+ TokenPanel,
875
+ {
876
+ tokens: shown,
877
+ namePrefix: TOKEN_PREFIX,
878
+ onChange: write,
879
+ scales,
880
+ onScaleChange,
881
+ collapsed: folds,
882
+ onCollapsedChange
883
+ }
884
+ ) })
885
+ ]
515
886
  }
516
887
  )
517
- ] });
888
+ );
518
889
  }
519
890
  function FocusedCssVars() {
520
891
  const { focusedTrialId, trials, instruments } = useLabContext();
@@ -531,19 +902,8 @@ function FocusedCssVars() {
531
902
  var CSS_VARS_SECTION = {
532
903
  id: "fg-css-vars",
533
904
  region: "aside",
534
- item: { title: "CSS Vars", actions: /* @__PURE__ */ jsx(SectionJump, {}), body: /* @__PURE__ */ jsx(FocusedCssVars, {}) }
905
+ item: { title: "CSS Vars", actions: /* @__PURE__ */ jsx(SectionJump, {}), fill: true, body: /* @__PURE__ */ jsx(FocusedCssVars, {}) }
535
906
  };
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
907
  var GLOBALS = "$globals";
548
908
  function leaves(shape, prefix = "") {
549
909
  const out = [];
@@ -556,7 +916,7 @@ function leaves(shape, prefix = "") {
556
916
  return out;
557
917
  }
558
918
  function storyDossier(input) {
559
- const { entry, instrument, config, ready = true } = input;
919
+ const { entry, libraries = TITLE_LIBRARIES, instrument, config, ready = true } = input;
560
920
  const schema = ready ? instrument?.config : void 0;
561
921
  const args = schema ? leaves(schema.nodes).map(({ path, node }) => {
562
922
  const value = valueAtPath(config, path);
@@ -575,7 +935,7 @@ function storyDossier(input) {
575
935
  name: entry.name,
576
936
  exportName: entry.exportName,
577
937
  file: entry.file,
578
- library: libraryOf(entry),
938
+ library: libraries.of(entry),
579
939
  ...entry.componentName === void 0 ? {} : { componentName: entry.componentName },
580
940
  ...entry.description === void 0 ? {} : { description: entry.description },
581
941
  ...entry.componentDescription === void 0 ? {} : { componentDescription: entry.componentDescription },
@@ -638,11 +998,13 @@ function StoryDossier({ dossier }) {
638
998
  }
639
999
  function StoryInfoDialog({ index, isReady, isOpen, onOpenChange }) {
640
1000
  const { focusedTrialId, trials, instruments } = useLabContext();
1001
+ const libraries = useContext(LibrariesContext);
641
1002
  const trial = trials.find((record) => record.id === focusedTrialId);
642
1003
  const entry = trial ? index.find((candidate) => candidate.id === trial.instrumentName) : void 0;
643
1004
  if (!entry) return null;
644
1005
  const dossier = storyDossier({
645
1006
  entry,
1007
+ libraries,
646
1008
  instrument: instruments.find((candidate) => candidate.name === entry.id),
647
1009
  config: trial?.config,
648
1010
  ready: isReady(entry.id)
@@ -658,6 +1020,7 @@ function StoryInfoDialog({ index, isReady, isOpen, onOpenChange }) {
658
1020
  }
659
1021
  );
660
1022
  }
1023
+ var HELLO_TIMEOUT_MS = 3e4;
661
1024
  function canMoveFrames() {
662
1025
  return typeof Element !== "undefined" && "moveBefore" in Element.prototype;
663
1026
  }
@@ -678,21 +1041,30 @@ function createFramePool(frameUrl, size = 2) {
678
1041
  shelf.setAttribute("aria-hidden", "true");
679
1042
  document.body.append(shelf);
680
1043
  const warm = [];
681
- let loading = 0;
1044
+ const waits = /* @__PURE__ */ new Set();
682
1045
  let cancelRefill = null;
683
1046
  let disposed = false;
684
1047
  const load = () => {
685
- loading += 1;
686
1048
  const frame = document.createElement("iframe");
687
1049
  frame.tabIndex = -1;
688
1050
  frame.src = frameUrl;
1051
+ const stop = () => {
1052
+ window.removeEventListener("message", onMessage);
1053
+ clearTimeout(timer);
1054
+ waits.delete(stop);
1055
+ };
689
1056
  const onMessage = (event) => {
690
1057
  if (event.source !== frame.contentWindow || event.origin !== location.origin) return;
691
1058
  if (event.data?.type !== FRAME_HELLO) return;
692
- window.removeEventListener("message", onMessage);
693
- loading -= 1;
1059
+ stop();
694
1060
  warm.push(frame);
695
1061
  };
1062
+ const timer = setTimeout(() => {
1063
+ stop();
1064
+ frame.remove();
1065
+ refill();
1066
+ }, HELLO_TIMEOUT_MS);
1067
+ waits.add(stop);
696
1068
  window.addEventListener("message", onMessage);
697
1069
  shelf.append(frame);
698
1070
  };
@@ -700,7 +1072,7 @@ function createFramePool(frameUrl, size = 2) {
700
1072
  if (disposed || cancelRefill) return;
701
1073
  cancelRefill = whenIdle(() => {
702
1074
  cancelRefill = null;
703
- while (!disposed && warm.length + loading < size) load();
1075
+ while (!disposed && warm.length + waits.size < size) load();
704
1076
  });
705
1077
  };
706
1078
  if (canMoveFrames()) refill();
@@ -714,6 +1086,7 @@ function createFramePool(frameUrl, size = 2) {
714
1086
  dispose() {
715
1087
  disposed = true;
716
1088
  cancelRefill?.();
1089
+ for (const stop of [...waits]) stop();
717
1090
  shelf.remove();
718
1091
  }
719
1092
  };
@@ -724,69 +1097,6 @@ var FramePoolContext = createContext(null);
724
1097
  function routedTrial(trials, focusedTrialId, route) {
725
1098
  return trials.find((t) => t.id === focusedTrialId && t.instrumentName === route) ?? trials.find((t) => t.instrumentName === route);
726
1099
  }
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
1100
  function PlayheadSync({ trialId, route, applied }) {
791
1101
  const clock = useTrialClock(trialId);
792
1102
  useEffect(() => {
@@ -938,6 +1248,7 @@ function KnobSync({ schema, config }) {
938
1248
  return () => window.removeEventListener("hashchange", onHashChange);
939
1249
  }, []);
940
1250
  useEffect(() => {
1251
+ if (reading.current && takeRouteFromTrial()) reading.current = false;
941
1252
  if (reading.current) {
942
1253
  reading.current = false;
943
1254
  const knobs2 = paramsToKnobs(schema, readRouteParams());
@@ -987,18 +1298,6 @@ function revealTrial(trialId, root = document) {
987
1298
  return true;
988
1299
  }
989
1300
 
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
1301
  // src/shell/tree/buildTree.ts
1003
1302
  function sortFolder(nodes) {
1004
1303
  const folders = nodes.filter((n) => n.kind === "folder");
@@ -1055,7 +1354,7 @@ function filterTree(nodes, query) {
1055
1354
  }
1056
1355
 
1057
1356
  // src/shell/tree/buildComponents.ts
1058
- function buildComponents(index) {
1357
+ function buildComponents(index, libraries) {
1059
1358
  const byTitle = /* @__PURE__ */ new Map();
1060
1359
  const pages = new Map(indexEntries(index).map((page) => [page.title, page]));
1061
1360
  for (const entry of index) {
@@ -1065,7 +1364,7 @@ function buildComponents(index) {
1065
1364
  row = {
1066
1365
  path: entry.title,
1067
1366
  label: segments[segments.length - 1],
1068
- library: libraryOf(entry),
1367
+ library: libraries.of(entry),
1069
1368
  index: pages.get(entry.title),
1070
1369
  entries: []
1071
1370
  };
@@ -1142,8 +1441,8 @@ function componentNodes(rows) {
1142
1441
  }
1143
1442
  return foldGalleries(root);
1144
1443
  }
1145
- function librariesIn(index) {
1146
- return [...new Set(index.map(libraryOf))].sort((a, b) => a.localeCompare(b));
1444
+ function librariesIn(index, libraries) {
1445
+ return [...new Set(index.map(libraries.of))].sort((a, b) => a.localeCompare(b));
1147
1446
  }
1148
1447
  var VIEWS = [
1149
1448
  { value: "tree", label: "Tree" },
@@ -1173,7 +1472,8 @@ function StoryTree({ ctx, index }) {
1173
1472
  const [hidden, setHidden] = usePersistedState("fg-tree-libraries", {}, { scope: "lab" });
1174
1473
  const [focusKey, setFocusKey] = useState(null);
1175
1474
  const items = useRef(/* @__PURE__ */ new Map());
1176
- const libraries = useMemo(() => librariesIn(index), [index]);
1475
+ const table = useContext(LibrariesContext);
1476
+ const libraries = useMemo(() => librariesIn(index, table), [index, table]);
1177
1477
  const views = useMemo(() => {
1178
1478
  const hasGallery = index.some(isGallery);
1179
1479
  const hasOther = index.some((entry) => !isGallery(entry));
@@ -1181,7 +1481,7 @@ function StoryTree({ ctx, index }) {
1181
1481
  }, [index]);
1182
1482
  const enabled = (value) => views.some((item) => item.value === value && !item.disabled);
1183
1483
  const view = [chosen, "components", "gallery"].find(enabled) ?? chosen;
1184
- const kept = useMemo(() => index.filter((entry) => !hidden[libraryOf(entry)]), [index, hidden]);
1484
+ const kept = useMemo(() => index.filter((entry) => !hidden[table.of(entry)]), [index, hidden, table]);
1185
1485
  const [galleryEntries, rest] = useMemo(() => {
1186
1486
  const yes = [];
1187
1487
  const no = [];
@@ -1189,8 +1489,8 @@ function StoryTree({ ctx, index }) {
1189
1489
  return [yes, no];
1190
1490
  }, [kept]);
1191
1491
  const tree = useMemo(() => buildTree(rest), [rest]);
1192
- const components = useMemo(() => buildComponents(rest), [rest]);
1193
- const galleries = useMemo(() => buildComponents(galleryEntries), [galleryEntries]);
1492
+ const components = useMemo(() => buildComponents(rest, table), [rest, table]);
1493
+ const galleries = useMemo(() => buildComponents(galleryEntries, table), [galleryEntries, table]);
1194
1494
  const shown = useMemo(
1195
1495
  () => view === "tree" ? filterTree(tree, query) : componentNodes(filterComponents(view === "gallery" ? galleries : components, query)),
1196
1496
  [view, components, galleries, tree, query]
@@ -1221,8 +1521,11 @@ function StoryTree({ ctx, index }) {
1221
1521
  const activate = (entry, another) => {
1222
1522
  const focused = ctx.trials.find((trial) => trial.id === ctx.focusedTrialId);
1223
1523
  if (another || !focused) ctx.addTrial(entry.id);
1224
- else if (focused.instrumentName === entry.id) revealTrial(focused.id);
1225
- else {
1524
+ else if (focused.instrumentName === entry.id) {
1525
+ revealTrial(focused.id);
1526
+ setRoute2(entry.id, { fromTrial: true });
1527
+ return;
1528
+ } else {
1226
1529
  ctx.swapTrial(focused.id, entry.id);
1227
1530
  setRoute2(entry.id, { inPlace: true });
1228
1531
  return;
@@ -1443,13 +1746,11 @@ function StoryTree({ ctx, index }) {
1443
1746
  /* @__PURE__ */ jsx("div", { role: "tree", "aria-labelledby": headingId, className: "fg-tree__items", onKeyDown, children: renderNodes(shown, 1) })
1444
1747
  ] });
1445
1748
  }
1446
-
1447
- // src/shell/answers.ts
1448
1749
  var KEPT_CONFIGS = 50;
1449
1750
  var UNANSWERED = stableStringify({ hidden: [], errors: {} });
1450
1751
  function createAnswerBook() {
1451
1752
  const byConfig = /* @__PURE__ */ new Map();
1452
- const listeners2 = /* @__PURE__ */ new Set();
1753
+ const answered = createEmitter();
1453
1754
  const held = /* @__PURE__ */ new Map();
1454
1755
  const keyOf2 = (config) => stableStringify(storyConfig(config));
1455
1756
  const answerFor = (config) => byConfig.get(keyOf2(config));
@@ -1463,7 +1764,7 @@ function createAnswerBook() {
1463
1764
  const oldest = byConfig.keys().next().value;
1464
1765
  if (oldest !== void 0) byConfig.delete(oldest);
1465
1766
  }
1466
- if (changed && held.has(answers.configKey)) for (const fn of listeners2) fn();
1767
+ if (changed && held.has(answers.configKey)) answered.emit();
1467
1768
  },
1468
1769
  hold(config) {
1469
1770
  const key = keyOf2(config);
@@ -1479,164 +1780,23 @@ function createAnswerBook() {
1479
1780
  return hidden?.some((h) => path === h || path.startsWith(`${h}.`)) ?? false;
1480
1781
  },
1481
1782
  errors: (path, config) => answerFor(config)?.errors[path] ?? [],
1482
- subscribe(fn) {
1483
- listeners2.add(fn);
1484
- return () => {
1485
- listeners2.delete(fn);
1486
- };
1487
- }
1488
- };
1489
- }
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)
1783
+ subscribe: answered.subscribe
1619
1784
  };
1620
1785
  }
1621
1786
 
1622
1787
  // src/shell/readyKey.ts
1623
1788
  var readyKey = (ready) => stableStringify({ schema: ready.schema, layout: ready.layout, viewport: ready.viewport });
1624
1789
  var START_TIMEOUT_MS = 1e4;
1625
- var HELLO_TIMEOUT_MS = 3e4;
1626
1790
  var requests = 0;
1627
1791
  function closeLink(link) {
1628
1792
  if (!link.current) return;
1629
1793
  clearTimeout(link.current.timer);
1630
- for (const settle2 of link.current.pending.values()) settle2.gone();
1794
+ for (const settle of link.current.pending.values()) settle({ ok: false, message: "The frame went away" });
1631
1795
  link.current.pending.clear();
1632
1796
  link.current.disconnect();
1633
1797
  link.current.channel.close();
1634
1798
  link.current = null;
1635
1799
  }
1636
- function settle(link, id, value) {
1637
- link.pending.get(id)?.resolve?.(value);
1638
- link.pending.delete(id);
1639
- }
1640
1800
  function mismatchMessage(mismatch) {
1641
1801
  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
1802
  }
@@ -1657,29 +1817,29 @@ function FrameView(props) {
1657
1817
  const slotRef = useRef(null);
1658
1818
  const inView = useNearViewport(hostRef, IN_VIEW_MARGIN);
1659
1819
  const link = useRef(null);
1660
- const latest = useLatest({ ...props, globals, overrides, pool, lab, title });
1820
+ const latestRef = useLatest({ ...props, globals, overrides, pool, lab, title });
1661
1821
  const [fault, setFault] = useState(null);
1662
1822
  const [pending, setPending] = useState(true);
1663
1823
  const init = useCallback((current) => {
1664
- const { ctx: live, globals: liveGlobals } = latest.current;
1824
+ const { ctx: live, globals: liveGlobals } = latestRef.current;
1665
1825
  current.awaiting = null;
1666
1826
  current.sent = { config: storyConfig(live.config), state: live.state, globals: liveGlobals };
1667
1827
  current.inputs += 1;
1668
1828
  current.channel.send({ type: "init", ...current.sent });
1669
- }, [latest]);
1829
+ }, [latestRef]);
1670
1830
  const receive = (current, msg) => {
1671
- const { ctx: live, answers: answers2, onReady } = latest.current;
1831
+ const { ctx: live, answers: answers2, onReady } = latestRef.current;
1672
1832
  switch (msg.type) {
1673
1833
  case "ready": {
1674
1834
  clearTimeout(current.timer);
1675
1835
  setFault(null);
1676
- for (const [name, value] of Object.entries(latest.current.overrides)) {
1836
+ for (const [name, value] of Object.entries(latestRef.current.overrides)) {
1677
1837
  current.channel.send({ type: "vars.set", name, value });
1678
1838
  }
1679
1839
  const key = readyKey(msg);
1680
1840
  current.awaiting = key;
1681
- onReady(latest.current.entry, msg);
1682
- if (current.awaiting === key && key === latest.current.descriptionKey) init(current);
1841
+ onReady(latestRef.current.entry, msg);
1842
+ if (current.awaiting === key && key === latestRef.current.descriptionKey) init(current);
1683
1843
  break;
1684
1844
  }
1685
1845
  case "rendered":
@@ -1700,22 +1860,15 @@ function FrameView(props) {
1700
1860
  case "vars":
1701
1861
  if (trialId) frames?.report(trialId, msg.vars);
1702
1862
  break;
1703
- case "size":
1704
- if (trialId) frames?.reportSize(trialId, { width: msg.width, height: msg.height });
1705
- break;
1706
1863
  case "a11y": {
1707
1864
  const outcome = msg.ok ? { ok: true, report: msg.report } : { ok: false, message: msg.message };
1708
1865
  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));
1866
+ current.pending.get(msg.id)?.(outcome);
1715
1867
  current.pending.delete(msg.id);
1716
1868
  break;
1869
+ }
1717
1870
  case "open": {
1718
- const { lab: liveLab } = latest.current;
1871
+ const { lab: liveLab } = latestRef.current;
1719
1872
  if (!trialId || !liveLab.instruments.some((instrument) => instrument.name === msg.id)) break;
1720
1873
  liveLab.swapTrial(trialId, msg.id);
1721
1874
  setRoute(msg.id, { inPlace: true });
@@ -1742,33 +1895,14 @@ function FrameView(props) {
1742
1895
  }
1743
1896
  });
1744
1897
  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);
1898
+ const audit = () => new Promise((resolve) => {
1899
+ const id = `a11y-${requests += 1}`;
1900
+ waiting.set(id, resolve);
1901
+ const ask = () => channel.send({ type: "a11y.run", id });
1752
1902
  if (current.hasRendered) ask();
1753
1903
  else current.queued.push(ask);
1754
1904
  });
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 }) : () => {
1905
+ const disconnect = frames && trialId ? frames.connect(trialId, { send: channel.send, audit }) : () => {
1772
1906
  };
1773
1907
  const current = {
1774
1908
  channel,
@@ -1783,7 +1917,7 @@ function FrameView(props) {
1783
1917
  };
1784
1918
  link.current = current;
1785
1919
  channel.on((msg) => receive(current, msg));
1786
- target.postMessage({ type: PORT_HANDOFF, id: latest.current.entry.id }, location.origin, [
1920
+ target.postMessage({ type: PORT_HANDOFF, id: latestRef.current.entry.id }, location.origin, [
1787
1921
  port2
1788
1922
  ]);
1789
1923
  };
@@ -1791,10 +1925,10 @@ function FrameView(props) {
1791
1925
  useEffect(() => {
1792
1926
  const slot = slotRef.current;
1793
1927
  if (!inView || !slot) return;
1794
- const claimed = latest.current.pool?.claim() ?? null;
1928
+ const claimed = latestRef.current.pool?.claim() ?? null;
1795
1929
  const frame = claimed ?? document.createElement("iframe");
1796
1930
  frame.className = "fg-frame-view";
1797
- frame.title = latest.current.title;
1931
+ frame.title = latestRef.current.title;
1798
1932
  frame.removeAttribute("tabindex");
1799
1933
  frame.toggleAttribute("data-pending", true);
1800
1934
  iframeRef.current = frame;
@@ -1822,7 +1956,7 @@ function FrameView(props) {
1822
1956
  setPending(true);
1823
1957
  setFault(null);
1824
1958
  };
1825
- }, [inView, src, connectRef, latest]);
1959
+ }, [inView, src, connectRef, latestRef]);
1826
1960
  useEffect(() => {
1827
1961
  iframeRef.current?.toggleAttribute("data-pending", pending && !fault);
1828
1962
  }, [pending, fault, inView, src]);
@@ -1861,23 +1995,13 @@ function FrameView(props) {
1861
1995
  }
1862
1996
  if (input) setFault((shown) => shown?.phase === "render" ? null : shown);
1863
1997
  }, [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
- );
1998
+ return /* @__PURE__ */ jsxs("div", { ref: hostRef, className: "fg-frame-host", children: [
1999
+ /* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
2000
+ fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
2001
+ fault.phase ? /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: fault.phase }) : null,
2002
+ /* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
2003
+ ] }) : null
2004
+ ] });
1881
2005
  }
1882
2006
  function storyInstrument(options) {
1883
2007
  const { entry, ready, answers, frameUrl, onReady, globals = {} } = options;
@@ -1915,7 +2039,7 @@ var NO_SETUP = {};
1915
2039
  var pruned = (map, ids) => [...map.keys()].every((id) => ids.has(id)) ? map : new Map([...map].filter(([id]) => ids.has(id)));
1916
2040
  var toError = (error) => error instanceof Error ? error : new Error(String(error));
1917
2041
  function useStoryRegistry(index, options) {
1918
- const { frameUrl, globals = NO_GLOBALS2, importers, setup = NO_SETUP, load = loadStories } = options;
2042
+ const { frameUrl, globals = NO_GLOBALS2, importers, setup = NO_SETUP, libraries = TITLE_LIBRARIES, load = loadStories } = options;
1919
2043
  const [readies, setReadies] = useState(() => /* @__PURE__ */ new Map());
1920
2044
  const [loaded, setLoaded] = useState(() => /* @__PURE__ */ new Map());
1921
2045
  const [faults, setFaults] = useState(() => /* @__PURE__ */ new Map());
@@ -1939,20 +2063,22 @@ function useStoryRegistry(index, options) {
1939
2063
  setRevisions((prev) => pruned(prev, ids));
1940
2064
  }
1941
2065
  const imports = useRef(/* @__PURE__ */ new Map());
1942
- const live = useLatest({ index, importers, setup, load });
2066
+ const liveRef = useLatest({ index, importers, setup, load, libraries });
2067
+ const missing = useRef(/* @__PURE__ */ new Set());
1943
2068
  const importOf = useCallback((file) => {
1944
2069
  let loading = imports.current.get(file);
1945
2070
  if (!loading) {
1946
- const importer = live.current.importers?.[file];
2071
+ const importer = liveRef.current.importers?.[file];
2072
+ if (!importer) missing.current.add(file);
1947
2073
  loading = importer ? importer() : Promise.reject(new Error(`No importer for ${file}`));
1948
2074
  imports.current.set(file, loading);
1949
2075
  }
1950
2076
  return loading;
1951
- }, [live]);
2077
+ }, [liveRef]);
1952
2078
  const request = useCallback(
1953
2079
  (entry) => {
1954
- const { setup: current, load: loadModule } = live.current;
1955
- const settle2 = (ids2, result) => {
2080
+ const { setup: current, load: loadModule } = liveRef.current;
2081
+ const settle = (ids2, result) => {
1956
2082
  setLoaded((prev) => {
1957
2083
  const next = new Map(prev);
1958
2084
  for (const [id, value] of result) next.set(id, value);
@@ -1969,7 +2095,7 @@ function useStoryRegistry(index, options) {
1969
2095
  });
1970
2096
  };
1971
2097
  if (isIndexId(entry.id)) {
1972
- const entries = live.current.index.filter((e) => !isIndexId(e.id) && indexId(e.title) === entry.id);
2098
+ const entries = liveRef.current.index.filter((e) => !isIndexId(e.id) && indexId(e.title) === entry.id);
1973
2099
  const files = [...new Set(entries.map((e) => e.file))];
1974
2100
  void Promise.all(files.map(importOf)).then(async (mods) => {
1975
2101
  const byFile = new Map(files.map((file, i) => [file, loadModule(mods[i] ?? {}, entry.title, current.parameters)]));
@@ -1982,7 +2108,7 @@ function useStoryRegistry(index, options) {
1982
2108
  kind: "index",
1983
2109
  bundle: {
1984
2110
  title: entry.title,
1985
- library: libraryOf(entry),
2111
+ library: liveRef.current.libraries.of(entry),
1986
2112
  ...entry.componentDescription === void 0 ? {} : { description: entry.componentDescription },
1987
2113
  ...entry.tags === void 0 ? {} : { tags: entry.tags },
1988
2114
  galleries: entries.filter(isGallery).map((e) => e.id),
@@ -1991,11 +2117,11 @@ function useStoryRegistry(index, options) {
1991
2117
  render: mods.map(indexRenderOf).find((render) => render !== null) ?? null
1992
2118
  }
1993
2119
  });
1994
- settle2([entry.id], result);
2120
+ settle([entry.id], result);
1995
2121
  }).catch((error) => fail([entry.id], error));
1996
2122
  return;
1997
2123
  }
1998
- const siblings = live.current.index.filter((e) => e.file === entry.file && !isIndexId(e.id));
2124
+ const siblings = liveRef.current.index.filter((e) => e.file === entry.file && !isIndexId(e.id));
1999
2125
  const ids = siblings.map((e) => e.id);
2000
2126
  void importOf(entry.file).then(async (mod) => {
2001
2127
  const stories = loadModule(mod, entry.title, current.parameters);
@@ -2007,22 +2133,29 @@ function useStoryRegistry(index, options) {
2007
2133
  result.set(sibling.id, { kind: "story", story });
2008
2134
  }
2009
2135
  if (!result.has(entry.id)) throw new Error(`${entry.file} has no story export "${entry.exportName}"`);
2010
- settle2(ids, result);
2136
+ settle(ids, result);
2011
2137
  }).catch((error) => fail(ids, error));
2012
2138
  },
2013
- [importOf, live]
2139
+ [importOf, liveRef]
2014
2140
  );
2015
2141
  const reload = useCallback(
2016
2142
  (file) => {
2017
2143
  imports.current.delete(file);
2018
- 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));
2144
+ const affected = liveRef.current.index.filter((e) => e.file === file || isIndexId(e.id) && liveRef.current.index.some((s) => s.file === file && indexId(s.title) === e.id));
2019
2145
  const ids = new Set(affected.map((e) => e.id));
2020
2146
  setLoaded((prev) => pruned(prev, new Set([...prev.keys()].filter((id) => !ids.has(id)))));
2021
2147
  setFaults((prev) => pruned(prev, new Set([...prev.keys()].filter((id) => !ids.has(id)))));
2022
2148
  setGenerations((prev) => new Map(prev).set(file, (prev.get(file) ?? 0) + 1));
2023
2149
  },
2024
- [live]
2150
+ [liveRef]
2025
2151
  );
2152
+ useEffect(() => {
2153
+ for (const file of [...missing.current]) {
2154
+ if (!importers?.[file]) continue;
2155
+ missing.current.delete(file);
2156
+ reload(file);
2157
+ }
2158
+ }, [importers, reload]);
2026
2159
  const committed = useRef({ built: /* @__PURE__ */ new Map(), books: /* @__PURE__ */ new Map(), list: [] });
2027
2160
  const cache = useMemo(() => {
2028
2161
  const previous = committed.current;
@@ -2073,12 +2206,7 @@ function useStoryRegistry(index, options) {
2073
2206
  return { instruments: cache.list, isReady, onReady, reload };
2074
2207
  }
2075
2208
  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 }) {
2209
+ function RouteOpener() {
2082
2210
  const lab = useLabContext();
2083
2211
  const [route] = useRoute();
2084
2212
  const handled = useRef(null);
@@ -2086,14 +2214,14 @@ function RouteOpener({ index }) {
2086
2214
  useEffect(() => {
2087
2215
  const from = last.current;
2088
2216
  if (route !== from.route) last.current = { route, entry: readRouteEntry() };
2089
- if (route === null || handled.current === route || !index.some((e) => e.id === route)) return;
2217
+ if (route === null || handled.current === route || !lab.instruments.some((i) => i.name === route)) return;
2090
2218
  handled.current = route;
2091
2219
  if (lab.trials.some((trial) => trial.instrumentName === route)) return;
2092
2220
  const previous = lab.trials.find((trial) => trial.instrumentName === from.route);
2093
2221
  const target = (previous && crossesInPlace(from.entry, last.current.entry) ? previous : void 0) ?? lab.trials.find((trial) => trial.id === lab.focusedTrialId) ?? lab.trials[0];
2094
2222
  if (target) lab.swapTrial(target.id, route);
2095
2223
  else lab.addTrial(route);
2096
- }, [route, index, lab]);
2224
+ }, [route, lab]);
2097
2225
  return null;
2098
2226
  }
2099
2227
  function useInfoShortcut(open) {
@@ -2130,12 +2258,21 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2130
2258
  };
2131
2259
  }, [frameUrl, framed]);
2132
2260
  const entries = useMemo(() => [...index, ...indexEntries(index)], [index]);
2261
+ const table = config?.libraries;
2262
+ const libraries = useMemo(() => librariesFrom(table), [table]);
2133
2263
  const registry = useStoryRegistry(entries, {
2134
2264
  frameUrl,
2135
2265
  globals: declarations,
2266
+ libraries,
2136
2267
  ...importers ? { importers } : {},
2137
2268
  ...setup ? { setup } : {}
2138
2269
  });
2270
+ const [route] = useRoute();
2271
+ const missing = route !== null && !entries.some((entry) => entry.id === route) ? route : null;
2272
+ const instruments = useMemo(
2273
+ () => missing === null ? registry.instruments : [...registry.instruments, missingInstrument(missing, declarations)],
2274
+ [registry.instruments, missing, declarations]
2275
+ );
2139
2276
  const reload = registry.reload;
2140
2277
  useEffect(() => changes?.subscribe(reload), [changes, reload]);
2141
2278
  const [labValues, setLabValues] = useState(() => labGlobals(declarations, void 0));
@@ -2147,20 +2284,20 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2147
2284
  (next) => setLabValues((prev) => stableStringify(prev) === stableStringify(next) ? prev : next),
2148
2285
  []
2149
2286
  );
2287
+ const infoCommand = useMemo(
2288
+ () => ({ icon: InfoIcon, label: "Info", shortcut: "\u2318I", onActivate: () => setInfoOpen(true) }),
2289
+ []
2290
+ );
2150
2291
  const labChrome = useMemo(
2151
2292
  () => [
2152
2293
  { 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
- },
2158
2294
  ...config?.cssVars === false ? [] : [CSS_VARS_SECTION],
2159
2295
  A11Y_SECTION,
2160
2296
  ...Object.keys(declarations).length > 0 ? [{ id: "fg-globals", region: "header", render: () => /* @__PURE__ */ jsx(GlobalsToolbar, { declarations }) }] : [],
2297
+ { id: "fg-info", region: "header", group: "story", item: infoCommand },
2161
2298
  ...config?.labChrome ?? []
2162
2299
  ],
2163
- [index, declarations, config?.cssVars, config?.labChrome]
2300
+ [index, infoCommand, declarations, config?.cssVars, config?.labChrome]
2164
2301
  );
2165
2302
  const first = index[0];
2166
2303
  if (!first) {
@@ -2169,14 +2306,14 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2169
2306
  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
2307
  ] });
2171
2308
  }
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(
2309
+ 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
2310
  Lab,
2174
2311
  {
2175
2312
  title,
2176
2313
  density: "roomy",
2177
2314
  ...labTheme ? { theme: labTheme } : {},
2178
- instruments: registry.instruments,
2179
- defaultInstrument: initialStory(entries, first.id),
2315
+ instruments,
2316
+ defaultInstrument: readRoute() ?? first.id,
2180
2317
  storageKey: storageKey ?? "weaselforge",
2181
2318
  ...storage ? { storage } : {},
2182
2319
  labChrome,
@@ -2185,7 +2322,7 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2185
2322
  ...config?.pages ? { pages: config.pages } : {},
2186
2323
  ...config?.path !== void 0 ? { path: config.path } : {},
2187
2324
  children: [
2188
- /* @__PURE__ */ jsx(RouteOpener, { index: entries }),
2325
+ /* @__PURE__ */ jsx(RouteOpener, {}),
2189
2326
  /* @__PURE__ */ jsx(RouteKnobs, { instruments: registry.instruments, isReady: registry.isReady }),
2190
2327
  /* @__PURE__ */ jsx(RoutePlayhead, {}),
2191
2328
  /* @__PURE__ */ jsx(
@@ -2200,7 +2337,7 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2200
2337
  /* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues })
2201
2338
  ]
2202
2339
  }
2203
- ) }) }) }) }) });
2340
+ ) }) }) }) }) }) });
2204
2341
  }
2205
2342
  function mountWorkshop({ container, ...props }) {
2206
2343
  const target = container ?? document.getElementById("root");