@weasel-js/forge 1.12.0 → 1.13.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 (46) hide show
  1. package/README.md +20 -5
  2. package/dist/{FrameController-CdnaIo4C.d.ts → FrameController-DgfVaENb.d.ts} +15 -3
  3. package/dist/{chunk-GH5RE5X5.js → chunk-2ZBZRBHI.js} +2 -2
  4. package/dist/{chunk-GH5RE5X5.js.map → chunk-2ZBZRBHI.js.map} +1 -1
  5. package/dist/{chunk-KEQBGDUZ.js → chunk-7XJNVJB5.js} +3 -3
  6. package/dist/{chunk-KEQBGDUZ.js.map → chunk-7XJNVJB5.js.map} +1 -1
  7. package/dist/{chunk-LTTEILXY.js → chunk-COT4I347.js} +3 -3
  8. package/dist/{chunk-LTTEILXY.js.map → chunk-COT4I347.js.map} +1 -1
  9. package/dist/{chunk-WMGEO5M6.js → chunk-GHPMNT2A.js} +83 -13
  10. package/dist/chunk-GHPMNT2A.js.map +1 -0
  11. package/dist/chunk-RVJHUY7W.js +60 -0
  12. package/dist/chunk-RVJHUY7W.js.map +1 -0
  13. package/dist/chunk-VUPN73JP.js +16 -0
  14. package/dist/chunk-VUPN73JP.js.map +1 -0
  15. package/dist/{chunk-ATI7FYJ7.js → chunk-WFT5IOHX.js} +98 -63
  16. package/dist/chunk-WFT5IOHX.js.map +1 -0
  17. package/dist/{chunk-YDLRSEUN.js → chunk-XIBKYSRU.js} +5 -4
  18. package/dist/chunk-XIBKYSRU.js.map +1 -0
  19. package/dist/{config-Mg05Ja9r.d.ts → config-6fWf87Tt.d.ts} +3 -21
  20. package/dist/csf/preview-api.js +2 -2
  21. package/dist/frame/index.css +26 -0
  22. package/dist/frame/index.css.map +1 -1
  23. package/dist/frame/index.d.ts +14 -8
  24. package/dist/frame/index.js +109 -75
  25. package/dist/frame/index.js.map +1 -1
  26. package/dist/index.d.ts +29 -5
  27. package/dist/index.js +3 -3
  28. package/dist/library-CdTj-hFp.d.ts +21 -0
  29. package/dist/{mountFrame-Ds3QLnBF.d.ts → mountFrame-Bs7u1ro-.d.ts} +1 -1
  30. package/dist/shell/index.css +33 -5
  31. package/dist/shell/index.css.map +1 -1
  32. package/dist/shell/index.d.ts +6 -5
  33. package/dist/shell/index.js +190 -108
  34. package/dist/shell/index.js.map +1 -1
  35. package/dist/test/index.css +2 -3
  36. package/dist/test/index.css.map +1 -1
  37. package/dist/test/index.d.ts +1 -1
  38. package/dist/test/index.js +4 -4
  39. package/dist/vite/index.js +78 -16
  40. package/dist/vite/index.js.map +1 -1
  41. package/package.json +9 -9
  42. package/dist/chunk-ATI7FYJ7.js.map +0 -1
  43. package/dist/chunk-WEQSLPKT.js +0 -9
  44. package/dist/chunk-WEQSLPKT.js.map +0 -1
  45. package/dist/chunk-WMGEO5M6.js.map +0 -1
  46. package/dist/chunk-YDLRSEUN.js.map +0 -1
@@ -1,15 +1,16 @@
1
- import { parseRoute, formatRoute, paramsToKnobs, knobPaths, sameKnob, knobsToParams, PLAYHEAD_PARAM, reservedParams } from '../chunk-LTTEILXY.js';
1
+ import { parseRoute, formatRoute, paramsToKnobs, knobPaths, sameKnob, knobsToParams, PLAYHEAD_PARAM, reservedParams } from '../chunk-COT4I347.js';
2
2
  import { DEFAULT_TITLE, CAPTURE_PATH, messageOf, MIN_DPR, MAX_DPR } from '../chunk-ORMMZ5PV.js';
3
- import { TITLE_LIBRARIES, isIndexId, indexId, indexEntries, librariesFrom, LibrariesContext, LAB_DENSITY, createKitRoot, schemaFromDescription, LibraryBadge, IndexPage, openChannel } from '../chunk-ATI7FYJ7.js';
4
- import { isGallery } from '../chunk-WEQSLPKT.js';
5
- import { useTrialFrame, createTrialFrames, createTrialClocks, StoryGlobalsContext, TrialFramesContext, TrialClocksContext, StoryTrial, LiveLensesContext, trialAt, describeLens, createLiveLenses, useCopyConfig, useCssOverrides, LiveFrameCopies, useTrialClock, TrialHost, LiveCopies, resolveContextOf, resolveComparison, withArgs, scrollerOf } from '../chunk-YDLRSEUN.js';
6
- export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-YDLRSEUN.js';
3
+ import { TITLE_LIBRARIES, loadIndexBundle, librariesFrom, LibrariesContext, LAB_DENSITY, createKitRoot, schemaFromDescription, LibraryBadge, Description, IndexPage, indexEnvOf, LabBadge, openChannel } from '../chunk-WFT5IOHX.js';
4
+ import { isIndexId, indexId, indexEntries } from '../chunk-RVJHUY7W.js';
5
+ import { isGallery, isLab } from '../chunk-VUPN73JP.js';
6
+ import { useTrialFrame, createTrialFrames, createTrialClocks, StoryGlobalsContext, TrialFramesContext, TrialClocksContext, StoryTrial, LiveLensesContext, trialAt, describeLens, createLiveLenses, useCopyConfig, useCssOverrides, LiveFrameCopies, useTrialClock, TrialHost, LiveCopies, resolveContextOf, resolveComparison, scrollerOf, withArgs } from '../chunk-XIBKYSRU.js';
7
+ export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-XIBKYSRU.js';
7
8
  import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, PLAYHEAD_KEY, labOptions, isForgePath, isGlobalsPath } from '../chunk-3BAM6M26.js';
8
- import { loadStories, indexRenderOf, useNearViewport, IN_VIEW_MARGIN, varRefs, cssRulesOf } from '../chunk-WMGEO5M6.js';
9
+ import { loadStories, useNearViewport, IN_VIEW_MARGIN, applyGlobalsTo, varRefs, cssRulesOf } from '../chunk-GHPMNT2A.js';
9
10
  import '../chunk-L5XM5VKB.js';
10
11
  import '../chunk-5WS67HMU.js';
11
- import '../chunk-KEQBGDUZ.js';
12
- import { stableStringify, FRAME_HELLO, PORT_HANDOFF, PROTOCOL_VERSION } from '../chunk-GH5RE5X5.js';
12
+ import '../chunk-7XJNVJB5.js';
13
+ import { stableStringify, FRAME_HELLO, PORT_HANDOFF, PROTOCOL_VERSION } from '../chunk-2ZBZRBHI.js';
13
14
  import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, useTrialState, ToggleBar as ToggleBar$1, Input, TokenPanel, useLabEntries, inferTokenType, refitScale } from '@weasel-js/labkit';
14
15
  import num from '@weasel-js/theme/numeric.module.css';
15
16
  import { createContext, useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect, useId, useContext, useReducer, useSyncExternalStore, Fragment as Fragment$1 } from 'react';
@@ -20,7 +21,7 @@ import { Slider } from '@weasel-js/ui/components/Slider';
20
21
  import { Transport } from '@weasel-js/ui/components/Timeline';
21
22
  import { TOKEN_MANIFEST, themeAxes, weaselTheme, THEME_SOURCES, resolveTheme, isColorModePreference, isByAxis } from '@weasel-js/theme';
22
23
  import { scale, httpThemeApi } from '@weasel-js/theme/engine';
23
- import { useLatest, useActionsRegistry, effectivePose, useMenuHost, useMenus, sceneToAdapter, parseColor, contrastRatio, wcagLevels, historyKey, isPlainObject, createHistory, keymapShortcuts, actionBindings, keySpecShortcut, keymapLeaderId, swapOp } from '@weasel-js/core';
24
+ import { useLatest, useActionsRegistry, useVisibleRaf, effectivePose, useMenuHost, useMenus, sceneToAdapter, parseColor, contrastRatio, wcagLevels, historyKey, isPlainObject, createHistory, keymapShortcuts, actionBindings, keySpecShortcut, keymapLeaderId, swapOp } from '@weasel-js/core';
24
25
  import { Button as Button$1 } from '@weasel-js/ui/components/Button';
25
26
  import { Checkbox } from '@weasel-js/ui/components/Checkbox';
26
27
  import { Icon } from '@weasel-js/ui/components/Icon';
@@ -92,7 +93,7 @@ function A11yPanel() {
92
93
  };
93
94
  const outcome = frame.a11y;
94
95
  return /* @__PURE__ */ jsxs("div", { className: "fg-a11y", children: [
95
- /* @__PURE__ */ jsx("div", { className: "fg-a11y__controls", children: /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: rerun, disabled: !audit || running, children: running ? "Checking\u2026" : "Re-check" }) }),
96
+ /* @__PURE__ */ jsx("div", { className: "fg-a11y__controls", children: /* @__PURE__ */ jsx(Button, { size: "sm", onClick: rerun, disabled: !audit || running, children: running ? "Checking\u2026" : "Re-check" }) }),
96
97
  !audit ? /* @__PURE__ */ jsx("p", { className: "fg-a11y__note", children: "No frame is connected." }) : null,
97
98
  audit && !outcome ? /* @__PURE__ */ jsx("p", { className: "fg-a11y__note", children: running ? "Running axe\u2026" : "Not checked yet." }) : null,
98
99
  outcome && !outcome.ok ? /* @__PURE__ */ jsxs("p", { className: "fg-a11y__note", role: "alert", children: [
@@ -261,7 +262,9 @@ function TransportBar({ clock }) {
261
262
  {
262
263
  className: "fg-transport__scrub",
263
264
  ariaLabel: "Scrub",
264
- density: "slim",
265
+ thumbShape: "dot",
266
+ trackHeight: "var(--wzl-slider-track-h)",
267
+ thumbSize: "var(--wzl-slider-thumb-size)",
265
268
  readoutPlacement: "none",
266
269
  min: span.start,
267
270
  max: span.end > span.start ? span.end : span.start + 1,
@@ -293,10 +296,15 @@ function TrialTransport({ trialId }) {
293
296
  }
294
297
 
295
298
  // src/shell/breadcrumb.ts
299
+ var bare = (label) => label.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
300
+ function restatesTitle(title, name) {
301
+ return bare(title.slice(title.lastIndexOf("/") + 1)) === bare(name);
302
+ }
296
303
  function crumbs(title, name) {
297
304
  const segments = title.split("/");
298
305
  const steps = segments.map((label, i) => ({ label, path: segments.slice(0, i + 1).join("/") }));
299
- steps.push({ label: name, path: null });
306
+ if (restatesTitle(title, name)) steps[steps.length - 1] = { label: segments[segments.length - 1], path: null };
307
+ else steps.push({ label: name, path: null });
300
308
  return steps.filter((step, i) => i === 0 || step.label.toLowerCase() !== "index");
301
309
  }
302
310
  function breadcrumb(title, name) {
@@ -334,31 +342,12 @@ function IndexTrial({ bundle, setup, config }) {
334
342
  const lab = useLabContext();
335
343
  const trialId = useContext(TrialIdContext);
336
344
  const graph = useContext(DependenciesContext);
337
- const gallery = isGallery(bundle);
338
- const dependencies = gallery ? void 0 : graph?.[bundle.title];
339
345
  const open = (id) => {
340
346
  if (!trialId || !lab.instruments.some((instrument) => instrument.name === id)) return;
341
347
  lab.swapTrial(trialId, id);
342
348
  setRoute(id, { inPlace: true });
343
349
  };
344
- const render = (globals) => {
345
- const env = {
346
- title: bundle.title,
347
- library: bundle.library,
348
- ...bundle.description === void 0 ? {} : { description: bundle.description },
349
- stories: bundle.stories,
350
- descriptions: bundle.descriptions,
351
- gallery,
352
- galleries: bundle.galleries,
353
- ...dependencies ? { dependencies } : {},
354
- globals,
355
- decorators: setup.decorators ?? [],
356
- open,
357
- // A story that throws shows its error in its own cell; the rest of the page is still worth reading.
358
- onError: (error) => console.error(error)
359
- };
360
- return /* @__PURE__ */ jsx(IndexPage, { env, render: bundle.render });
361
- };
350
+ const render = (globals) => /* @__PURE__ */ jsx(IndexPage, { env: indexEnvOf(bundle, { graph, globals, decorators: setup.decorators ?? [], open }), render: bundle.render });
362
351
  return /* @__PURE__ */ jsxs(Fragment, { children: [
363
352
  /* @__PURE__ */ jsx(TrialHost, { layout: "fullscreen", setup, config, children: render }),
364
353
  /* @__PURE__ */ jsx(LiveCopies, { trialId, layout: "fullscreen", setup, trialConfig: config, children: render })
@@ -407,10 +396,18 @@ function documentInstrument(options) {
407
396
  config,
408
397
  defaultConfig: () => config.defaults(),
409
398
  initialState: (c) => initialState?.(c) ?? null,
410
- render: (ctx) => loaded.kind === "story" ? /* @__PURE__ */ jsxs(Fragment, { children: [
411
- /* @__PURE__ */ jsx(StoryTrial, { story: loaded.story, setup, ctx }, loaded.story.id),
412
- /* @__PURE__ */ jsx(CaptureInputs, { entry, layout: loaded.story.layout, config: ctx.config, state: ctx.state })
413
- ] }) : /* @__PURE__ */ jsx(IndexTrial, { bundle: loaded.bundle, setup, config: ctx.config }),
399
+ render: (ctx) => /* @__PURE__ */ jsxs(Fragment, { children: [
400
+ loaded.kind === "story" ? /* @__PURE__ */ jsx(StoryTrial, { story: loaded.story, setup, ctx }, loaded.story.id) : /* @__PURE__ */ jsx(IndexTrial, { bundle: loaded.bundle, setup, config: ctx.config }),
401
+ /* @__PURE__ */ jsx(
402
+ CaptureInputs,
403
+ {
404
+ entry,
405
+ layout: loaded.kind === "story" ? loaded.story.layout : "fullscreen",
406
+ config: ctx.config,
407
+ state: ctx.state
408
+ }
409
+ )
410
+ ] }),
414
411
  ...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {},
415
412
  ...timed ? { chrome: [{ id: "fg-transport", region: "status", render: (c) => /* @__PURE__ */ jsx(TrialTransport, { trialId: c.trialId }) }] } : {}
416
413
  };
@@ -765,22 +762,22 @@ function useThemeSave({ trialId, globals, themeApi, scales, overrides, history:
765
762
  const stored = await api.get(THEME_NAME);
766
763
  const file = `themes/${THEME_NAME}.json`;
767
764
  if (JSON.stringify(stored.definition) !== JSON.stringify(THEME_SOURCES[THEME_NAME])) {
768
- setNotice({ tone: "error", text: `${file} has changed since this page loaded. Reload to edit against it.` });
765
+ setNotice({ outcome: "error", text: `${file} has changed since this page loaded. Reload to edit against it.` });
769
766
  return;
770
767
  }
771
768
  const scaled = applyScaleEdits(stored.definition, Object.fromEntries(scales.map((s) => [s.name, s.edit])), selection);
772
769
  const next = scaled.ok ? applyTokenEdits(scaled.definition, tokens, selection) : scaled;
773
770
  if (!next.ok) {
774
- setNotice({ tone: "error", text: next.message });
771
+ setNotice({ outcome: "error", text: next.message });
775
772
  return;
776
773
  }
777
774
  const result = await api.put(THEME_NAME, next.definition, stored.hash);
778
775
  if (result.status === "conflict") {
779
- setNotice({ tone: "error", text: `${file} changed on disk while saving. Nothing was written; reload to edit against it.` });
776
+ setNotice({ outcome: "error", text: `${file} changed on disk while saving. Nothing was written; reload to edit against it.` });
780
777
  return;
781
778
  }
782
779
  if (result.status === "invalid") {
783
- setNotice({ tone: "error", text: result.message });
780
+ setNotice({ outcome: "error", text: result.message });
784
781
  return;
785
782
  }
786
783
  const saved = /* @__PURE__ */ new Set([
@@ -788,16 +785,16 @@ function useThemeSave({ trialId, globals, themeApi, scales, overrides, history:
788
785
  ...Object.keys(tokens).map((token) => `${TOKEN_PREFIX}${token}`)
789
786
  ]);
790
787
  const groups = new Set(scales.map((scale) => scale.group));
791
- const now = history2.current();
788
+ const now2 = history2.current();
792
789
  history2.reset({
793
- overrides: Object.fromEntries(Object.entries(now.overrides).filter(([name]) => !saved.has(name))),
794
- scales: Object.fromEntries(Object.entries(now.scales).filter(([group]) => !groups.has(group)))
790
+ overrides: Object.fromEntries(Object.entries(now2.overrides).filter(([name]) => !saved.has(name))),
791
+ scales: Object.fromEntries(Object.entries(now2.scales).filter(([group]) => !groups.has(group)))
795
792
  });
796
793
  setNotice(
797
- 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("; ")}` }
794
+ result.regenerated || result.problems.length === 0 ? { outcome: "saved", text: `Saved to ${file}.` } : { outcome: "error", text: `Saved to ${file}, but the tokens were not regenerated: ${result.problems.join("; ")}` }
798
795
  );
799
796
  } catch (e) {
800
- setNotice({ tone: "error", text: `Saving needs forge's dev server, which did not answer: ${e.message}` });
797
+ setNotice({ outcome: "error", text: `Saving needs forge's dev server, which did not answer: ${e.message}` });
801
798
  } finally {
802
799
  setSaving(false);
803
800
  }
@@ -1001,16 +998,16 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
1001
998
  }
1002
999
  });
1003
1000
  const write = (name, value) => {
1004
- const now = history2.current();
1005
- const { [name]: _dropped, ...rest } = now.overrides;
1006
- history2.commit({ ...now, overrides: value === null ? rest : { ...rest, [name]: value } }, `token:${name}`);
1001
+ const now2 = history2.current();
1002
+ const { [name]: _dropped, ...rest } = now2.overrides;
1003
+ history2.commit({ ...now2, overrides: value === null ? rest : { ...rest, [name]: value } }, `token:${name}`);
1007
1004
  };
1008
1005
  const onScaleChange = (group, next) => {
1009
1006
  const source = SCALE_SOURCES.find((sc) => sc.group === group);
1010
1007
  if (!source) return;
1011
- const now = history2.current();
1008
+ const now2 = history2.current();
1012
1009
  history2.commit(
1013
- { overrides: { ...now.overrides, ...Object.fromEntries(scaleValues(source, next)) }, scales: { ...now.scales, [group]: next } },
1010
+ { overrides: { ...now2.overrides, ...Object.fromEntries(scaleValues(source, next)) }, scales: { ...now2.scales, [group]: next } },
1014
1011
  `scale:${group}`
1015
1012
  );
1016
1013
  };
@@ -1096,8 +1093,8 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
1096
1093
  function SaveRow({ pending: pending2, saving, notice, onSave }) {
1097
1094
  if (!pending2 && !notice) return null;
1098
1095
  return /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__save", children: [
1099
- pending2 ? /* @__PURE__ */ jsx(Button$1, { variant: "primary", size: "sm", loading: saving, disabled: saving, onClick: onSave, children: "Save to theme" }) : null,
1100
- notice ? /* @__PURE__ */ jsx("p", { className: `fg-css-vars__notice fg-css-vars__notice--${notice.tone}`, role: notice.tone === "error" ? "alert" : "status", children: notice.text }) : null
1096
+ pending2 ? /* @__PURE__ */ jsx(Button$1, { stance: "confirm", size: "sm", loading: saving, disabled: saving, onClick: onSave, children: "Save to theme" }) : null,
1097
+ notice ? /* @__PURE__ */ jsx("p", { className: `fg-css-vars__notice fg-css-vars__notice--${notice.outcome}`, role: notice.outcome === "error" ? "alert" : "status", children: notice.text }) : null
1101
1098
  ] });
1102
1099
  }
1103
1100
  function FocusedCssVars() {
@@ -1345,8 +1342,8 @@ function ArgRows({ args }) {
1345
1342
  function StoryDossier({ dossier }) {
1346
1343
  return /* @__PURE__ */ jsxs("div", { className: "fg-info", children: [
1347
1344
  dossier.description || dossier.componentDescription ? /* @__PURE__ */ jsxs("section", { className: "fg-info__section", "aria-label": "Docs", children: [
1348
- dossier.description ? /* @__PURE__ */ jsx("p", { className: "fg-info__doc", children: dossier.description }) : null,
1349
- dossier.componentDescription ? /* @__PURE__ */ jsx("p", { className: "fg-info__doc fg-info__doc--component", children: dossier.componentDescription }) : null
1345
+ dossier.description ? /* @__PURE__ */ jsx(Description, { text: dossier.description, className: "fg-info__doc" }) : null,
1346
+ dossier.componentDescription ? /* @__PURE__ */ jsx(Description, { text: dossier.componentDescription, className: "fg-info__doc fg-info__doc--component" }) : null
1350
1347
  ] }) : null,
1351
1348
  /* @__PURE__ */ jsx("section", { className: "fg-info__section", "aria-label": "Identity", children: /* @__PURE__ */ jsxs("dl", { className: "fg-info__rows", children: [
1352
1349
  /* @__PURE__ */ jsx(Row, { label: "Story", children: dossier.name }),
@@ -1574,15 +1571,40 @@ var liveCellId = (lensId, compare) => compare === null ? lensId : `${lensId}~com
1574
1571
  function sameRequest(a, b) {
1575
1572
  return a !== null && a.slot === b.slot && a.hit.host === b.hit.host && a.hit.trialId === b.hit.trialId && a.hit.local.x === b.hit.local.x && a.hit.local.y === b.hit.local.y && a.hit.scale === b.hit.scale && a.factor === b.factor && a.size.width === b.size.width && a.size.height === b.size.height && a.args === b.args && a.compare === b.compare && sameContext(a.ctx, b.ctx);
1576
1573
  }
1577
- function createLiveSource(lenses, resolve, conditions) {
1574
+ var LOOK_EVENTS = ["pointerover", "pointerout", "focusin", "focusout", "transitionend"];
1575
+ function watchOf(notify) {
1576
+ const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver(notify);
1577
+ let watched = null;
1578
+ return (el) => {
1579
+ if (el === watched) return;
1580
+ observer?.disconnect();
1581
+ for (const type of LOOK_EVENTS) watched?.removeEventListener(type, notify);
1582
+ watched = el;
1583
+ if (!el) return;
1584
+ observer?.observe(el, { subtree: true, childList: true, characterData: true, attributes: true });
1585
+ for (const type of LOOK_EVENTS) el.addEventListener(type, notify);
1586
+ };
1587
+ }
1588
+ var now = (run) => run();
1589
+ function createLiveSource(lenses, resolve, conditions, schedule = now) {
1578
1590
  const measures = /* @__PURE__ */ new Set();
1579
1591
  const listeners2 = /* @__PURE__ */ new Set();
1592
+ let due = false;
1593
+ const flush = () => {
1594
+ due = false;
1595
+ for (const listener of [...listeners2]) listener();
1596
+ };
1597
+ const notify = () => {
1598
+ if (due) return;
1599
+ due = true;
1600
+ schedule(flush);
1601
+ };
1580
1602
  return {
1581
1603
  kind: "live",
1582
1604
  produces: "dom",
1583
1605
  label: () => "Live",
1584
1606
  status: () => true,
1585
- // A copy renders after the lens asks for it and changes as its story does; a reader of the copy hears of both.
1607
+ // A reader hears of what it reads: the lens's copy, and with nothing compared, the main story under the reticle.
1586
1608
  subscribe(listener) {
1587
1609
  listeners2.add(listener);
1588
1610
  return () => {
@@ -1597,10 +1619,9 @@ function createLiveSource(lenses, resolve, conditions) {
1597
1619
  empty.className = "fg-lens-empty";
1598
1620
  empty.hidden = true;
1599
1621
  slot.append(target, empty);
1600
- const watch = typeof MutationObserver === "undefined" ? null : new MutationObserver(() => {
1601
- for (const listener of [...listeners2]) listener();
1602
- });
1603
- watch?.observe(target, { subtree: true, childList: true, characterData: true, attributes: true });
1622
+ const watchCopy = watchOf(notify);
1623
+ watchCopy(target);
1624
+ const watchStory = watchOf(notify);
1604
1625
  let request = first;
1605
1626
  let posted = null;
1606
1627
  const post = (next) => {
@@ -1611,6 +1632,7 @@ function createLiveSource(lenses, resolve, conditions) {
1611
1632
  const measure = () => {
1612
1633
  const { lensId, reticle, conditions: compare } = request;
1613
1634
  const hit = trialAt(reticle, doc);
1635
+ watchStory(compare === null ? hit?.host ?? null : null);
1614
1636
  empty.hidden = hit !== null;
1615
1637
  if (!hit) {
1616
1638
  post(null);
@@ -1638,7 +1660,8 @@ function createLiveSource(lenses, resolve, conditions) {
1638
1660
  measure();
1639
1661
  },
1640
1662
  detach() {
1641
- watch?.disconnect();
1663
+ watchCopy(null);
1664
+ watchStory(null);
1642
1665
  measures.delete(measure);
1643
1666
  lenses.set(liveCellId(request.lensId, request.conditions), null);
1644
1667
  target.remove();
@@ -1977,6 +2000,22 @@ function renderReadouts(props) {
1977
2000
  readouts.includes("contrast") ? /* @__PURE__ */ jsx(ContrastReadout, { ...props }) : null
1978
2001
  ] });
1979
2002
  }
2003
+
2004
+ // src/shell/loupe/frameScroll.ts
2005
+ var ORIGIN = { left: 0, top: 0 };
2006
+ var frames = /* @__PURE__ */ new Map();
2007
+ var trialOf = (host) => host.dataset.fgHost ?? "";
2008
+ function reportFrameScroll(host, scroll) {
2009
+ frames.set(trialOf(host), { left: scroll.left, top: scroll.top });
2010
+ host.dispatchEvent(new Event("scroll"));
2011
+ }
2012
+ function scrollOf(host) {
2013
+ const scroller = scrollerOf(host);
2014
+ if (scroller) return { left: scroller.scrollLeft, top: scroller.scrollTop };
2015
+ return frames.get(trialOf(host)) ?? ORIGIN;
2016
+ }
2017
+
2018
+ // src/shell/loupe/capturePayload.ts
1980
2019
  var STATE_UNSENDABLE = "this story's state cannot be sent to a capture";
1981
2020
  var CONFIG_UNSENDABLE = "this story's config cannot be sent to a capture";
1982
2021
  var NO_STORY = "this trial shows no story to capture";
@@ -2016,7 +2055,6 @@ function capturePayload(input) {
2016
2055
  const box = { width: host.offsetWidth, height: host.offsetHeight };
2017
2056
  if (!(box.width > 0 && box.height > 0)) return { unavailable: NO_BOX };
2018
2057
  const isolate = entry.isolate !== void 0;
2019
- const scroller = scrollerOf(host);
2020
2058
  const kept2 = config?.[PLAYHEAD_KEY];
2021
2059
  return {
2022
2060
  story: entry.id,
@@ -2029,7 +2067,7 @@ function capturePayload(input) {
2029
2067
  box,
2030
2068
  layout,
2031
2069
  dpr: clampDpr(resolved.dpr ?? dpr),
2032
- scroll: { left: scroller?.scrollLeft ?? 0, top: scroller?.scrollTop ?? 0 },
2070
+ scroll: scrollOf(host),
2033
2071
  lab,
2034
2072
  colorScheme
2035
2073
  };
@@ -2332,7 +2370,14 @@ function useLensFocus() {
2332
2370
  }
2333
2371
  function createApi(scene, refs) {
2334
2372
  let next = 0;
2335
- let hold = null;
2373
+ const holds = /* @__PURE__ */ new Map();
2374
+ const release = (lensId) => {
2375
+ holds.get(lensId)?.drop();
2376
+ holds.delete(lensId);
2377
+ };
2378
+ scene.subscribe(() => {
2379
+ for (const lensId of [...holds.keys()]) if (settingsOf(scene, lensId)?.aim !== "follow") release(lensId);
2380
+ });
2336
2381
  const toBody = (client, box) => ({ x: client.x - box.left, y: client.y - box.top });
2337
2382
  const apply = (ops) => {
2338
2383
  if (ops.length > 0) sceneToAdapter(scene).applyOps([...ops], "lens");
@@ -2364,9 +2409,11 @@ function createApi(scene, refs) {
2364
2409
  if (lensId === null || !body) return;
2365
2410
  const pose = reticleAt(scene, lensId, toBody(client, body.getBoundingClientRect()));
2366
2411
  if (pose === null) return;
2367
- hold ??= scene.overrides.hold();
2412
+ const hold = holds.get(lensId) ?? scene.overrides.hold();
2413
+ holds.set(lensId, hold);
2368
2414
  hold.set([[`${lensId}:reticle`, pose]]);
2369
2415
  },
2416
+ unfollow: release,
2370
2417
  sourceAvailable: (kind) => refs.sources.current[kind]?.status() ?? "not provided",
2371
2418
  openCompareMenu: (lensId) => refs.compare?.current?.(lensId)
2372
2419
  };
@@ -2397,6 +2444,21 @@ function useRemeasure(sources, body, rerender) {
2397
2444
  };
2398
2445
  }, [sources, body, rerender]);
2399
2446
  }
2447
+ function useFrameSchedule() {
2448
+ const next = useRef(null);
2449
+ const frame = useVisibleRaf(() => {
2450
+ const run = next.current;
2451
+ next.current = null;
2452
+ run?.();
2453
+ });
2454
+ return useCallback(
2455
+ (run) => {
2456
+ next.current = run;
2457
+ frame.request();
2458
+ },
2459
+ [frame]
2460
+ );
2461
+ }
2400
2462
  function ForgeLoupe({ declarations, modeGlobal }) {
2401
2463
  const lenses = useContext(LiveLensesContext);
2402
2464
  const labValues = useContext(StoryGlobalsContext);
@@ -2407,8 +2469,14 @@ function ForgeLoupe({ declarations, modeGlobal }) {
2407
2469
  const pointer = useRef(null);
2408
2470
  const { focus, onFocusLens } = useLensFocus();
2409
2471
  const conditions = useLatest({ labValues, mode, declarations, modeGlobal });
2472
+ const onFrame = useFrameSchedule();
2410
2473
  const [live] = useState(
2411
- () => lenses ? createLiveSource(lenses, resolveComparison, () => ({ base: conditions.current.labValues, ctx: resolveContextOf(conditions.current) })) : null
2474
+ () => lenses ? createLiveSource(
2475
+ lenses,
2476
+ resolveComparison,
2477
+ () => ({ base: conditions.current.labValues, ctx: resolveContextOf(conditions.current) }),
2478
+ onFrame
2479
+ ) : null
2412
2480
  );
2413
2481
  const pixels = usePixelsSource(conditions);
2414
2482
  const sources = useMemo(
@@ -2723,6 +2791,16 @@ function foldGalleries(nodes) {
2723
2791
  return { ...node, children };
2724
2792
  });
2725
2793
  }
2794
+ function hoistLoneStories(nodes) {
2795
+ return nodes.map((node) => {
2796
+ if (node.kind !== "folder") return node;
2797
+ const [only] = node.children;
2798
+ if (node.children.length === 1 && only?.kind === "story" && restatesTitle(node.path, only.entry.name)) {
2799
+ return { kind: "story", entry: only.entry, label: node.label, tag: node.tag ?? only.tag };
2800
+ }
2801
+ return { ...node, children: hoistLoneStories(node.children) };
2802
+ });
2803
+ }
2726
2804
  function buildTree(index) {
2727
2805
  const root = [];
2728
2806
  const folders = /* @__PURE__ */ new Map();
@@ -2743,7 +2821,7 @@ function buildTree(index) {
2743
2821
  }
2744
2822
  siblings.push({ kind: "story", entry });
2745
2823
  }
2746
- return sortFolder(root);
2824
+ return hoistLoneStories(sortFolder(root));
2747
2825
  }
2748
2826
  function filterTree(nodes, query) {
2749
2827
  const needle = query.trim().toLowerCase();
@@ -2867,7 +2945,7 @@ function componentNodes(rows) {
2867
2945
  ...row.entries.map((entry) => ({ kind: "story", entry }))
2868
2946
  ];
2869
2947
  }
2870
- return foldGalleries(root);
2948
+ return hoistLoneStories(foldGalleries(root));
2871
2949
  }
2872
2950
  function librariesIn(index, libraries) {
2873
2951
  return [...new Set(index.map(libraries.of))].sort((a, b) => a.localeCompare(b));
@@ -2891,6 +2969,12 @@ function ancestorsOf(entry) {
2891
2969
  function LibraryBadge2({ library }) {
2892
2970
  return /* @__PURE__ */ jsx(LibraryBadge, { library, className: "fg-tree__tag" });
2893
2971
  }
2972
+ function RowTags({ lab, library }) {
2973
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2974
+ lab ? /* @__PURE__ */ jsx(LabBadge, { className: "fg-tree__lab" }) : null,
2975
+ library ? /* @__PURE__ */ jsx(LibraryBadge2, { library }) : null
2976
+ ] });
2977
+ }
2894
2978
  function StoryTree({ ctx, index }) {
2895
2979
  const headingId = useId();
2896
2980
  const [route, setRoute2] = useRoute();
@@ -3098,7 +3182,7 @@ function StoryTree({ ctx, index }) {
3098
3182
  }
3099
3183
  ),
3100
3184
  /* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: node.label }),
3101
- node.tag ? /* @__PURE__ */ jsx(LibraryBadge2, { library: node.tag }) : null
3185
+ /* @__PURE__ */ jsx(RowTags, { lab: !!node.index && isLab(node.index), library: node.tag })
3102
3186
  ]
3103
3187
  }
3104
3188
  ),
@@ -3124,7 +3208,7 @@ function StoryTree({ ctx, index }) {
3124
3208
  },
3125
3209
  children: [
3126
3210
  /* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: node.label ?? entry.name }),
3127
- node.tag ? /* @__PURE__ */ jsx(LibraryBadge2, { library: node.tag }) : null
3211
+ /* @__PURE__ */ jsx(RowTags, { lab: isLab(entry), library: node.tag })
3128
3212
  ]
3129
3213
  },
3130
3214
  keyOf(node)
@@ -3250,7 +3334,7 @@ function sendInputs(current, sent, next) {
3250
3334
  return current.inputs !== before;
3251
3335
  }
3252
3336
  function receive(deps, current, msg) {
3253
- const { latest: latestRef, trialId, frames, setFault, setPending, init, copy } = deps;
3337
+ const { latest: latestRef, trialId, frames: frames2, host, setFault, setPending, init, copy } = deps;
3254
3338
  const { ctx: live, answers, onReady } = latestRef.current;
3255
3339
  switch (msg.type) {
3256
3340
  case "ready": {
@@ -3285,15 +3369,18 @@ function receive(deps, current, msg) {
3285
3369
  if (!copy) answers.record(msg.answers);
3286
3370
  break;
3287
3371
  case "vars":
3288
- if (trialId) frames?.report(trialId, msg.vars);
3372
+ if (trialId) frames2?.report(trialId, msg.vars);
3289
3373
  break;
3290
3374
  case "a11y": {
3291
3375
  const outcome = msg.ok ? { ok: true, report: msg.report } : { ok: false, message: msg.message };
3292
- if (trialId) frames?.reportA11y(trialId, outcome);
3376
+ if (trialId) frames2?.reportA11y(trialId, outcome);
3293
3377
  current.pending.get(msg.id)?.(outcome);
3294
3378
  current.pending.delete(msg.id);
3295
3379
  break;
3296
3380
  }
3381
+ case "scroll":
3382
+ if (!copy && host.current) reportFrameScroll(host.current, msg);
3383
+ break;
3297
3384
  case "open": {
3298
3385
  const { lab: liveLab } = latestRef.current;
3299
3386
  if (!trialId || !liveLab.instruments.some((instrument) => instrument.name === msg.id)) break;
@@ -3309,7 +3396,7 @@ function receive(deps, current, msg) {
3309
3396
  }
3310
3397
  }
3311
3398
  function openLink(deps, link, frame) {
3312
- const { latest: latestRef, src, trialId, frames, setFault, setPending } = deps;
3399
+ const { latest: latestRef, src, trialId, frames: frames2, setFault, setPending } = deps;
3313
3400
  closeLink(link);
3314
3401
  setPending(true);
3315
3402
  const target = frame.contentWindow;
@@ -3330,7 +3417,7 @@ function openLink(deps, link, frame) {
3330
3417
  if (current.hasRendered) ask();
3331
3418
  else current.queued.push(ask);
3332
3419
  });
3333
- const disconnect = frames && trialId ? frames.connect(trialId, { send: channel.send, audit }) : () => {
3420
+ const disconnect = frames2 && trialId ? frames2.connect(trialId, { send: channel.send, audit }) : () => {
3334
3421
  };
3335
3422
  const current = {
3336
3423
  channel,
@@ -3351,7 +3438,7 @@ function openLink(deps, link, frame) {
3351
3438
  ]);
3352
3439
  }
3353
3440
  function useFrameLink(opts) {
3354
- const { latest: latestRef, src, trialId, frames, setFault, setPending, copy, config, state, globals, descriptionKey } = opts;
3441
+ const { latest: latestRef, src, trialId, frames: frames2, host, setFault, setPending, copy, config, state, globals, descriptionKey } = opts;
3355
3442
  const link = useRef(null);
3356
3443
  const init = useCallback((current) => {
3357
3444
  const { ctx: live, globals: liveGlobals } = latestRef.current;
@@ -3360,7 +3447,7 @@ function useFrameLink(opts) {
3360
3447
  current.inputs += 1;
3361
3448
  current.channel.send({ type: "init", ...current.sent });
3362
3449
  }, [latestRef]);
3363
- const deps = { latest: latestRef, src, trialId, frames, setFault, setPending, copy, init };
3450
+ const deps = { latest: latestRef, src, trialId, frames: frames2, host, setFault, setPending, copy, init };
3364
3451
  const connect = (frame) => openLink(deps, link, frame);
3365
3452
  const adopt2 = (adopted) => {
3366
3453
  adopted.deliver = (msg) => receive(deps, adopted, msg);
@@ -3478,7 +3565,7 @@ function FrameView(props) {
3478
3565
  [copy, trialCtx, laidConfig, laidSetConfig]
3479
3566
  );
3480
3567
  const trialId = useContext(TrialIdContext);
3481
- const frames = useContext(TrialFramesContext);
3568
+ const frames2 = useContext(TrialFramesContext);
3482
3569
  const [overrides] = useCssOverrides();
3483
3570
  const pool = useContext(FramePoolContext);
3484
3571
  const lab = useLabContext();
@@ -3495,7 +3582,8 @@ function FrameView(props) {
3495
3582
  latest: latestRef,
3496
3583
  src,
3497
3584
  trialId: copy ? null : trialId,
3498
- frames: copy ? null : frames,
3585
+ frames: copy ? null : frames2,
3586
+ host: hostRef,
3499
3587
  setFault,
3500
3588
  setPending,
3501
3589
  copy: copy !== void 0,
@@ -3524,6 +3612,20 @@ function FrameView(props) {
3524
3612
  if (iframeRef.current) iframeRef.current.title = title;
3525
3613
  }, [title]);
3526
3614
  useEffect(() => copy ? void 0 : answers.hold(ctx.config), [answers, ctx.config, copy]);
3615
+ const setup = props.setup;
3616
+ const copyId = copy?.id;
3617
+ useLayoutEffect(
3618
+ () => copyId && setup && hostRef.current ? applyGlobalsTo(hostRef.current, copyId, globals, setup) : void 0,
3619
+ [copyId, globals, setup]
3620
+ );
3621
+ const main = copy?.main;
3622
+ useEffect(() => {
3623
+ if (!main || pending2) return void 0;
3624
+ const follow = () => link.current?.channel.send({ type: "scroll", ...scrollOf(main) });
3625
+ follow();
3626
+ main.addEventListener("scroll", follow, { passive: true });
3627
+ return () => main.removeEventListener("scroll", follow);
3628
+ }, [main, pending2, link]);
3527
3629
  return /* @__PURE__ */ jsxs("div", { ref: hostRef, className: "fg-frame-host", "data-fg-host": copy?.id ?? trialId ?? void 0, "data-fg-copy": copy ? "" : void 0, children: [
3528
3630
  /* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
3529
3631
  fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
@@ -3534,7 +3636,7 @@ function FrameView(props) {
3534
3636
  ] });
3535
3637
  }
3536
3638
  function storyInstrument(options) {
3537
- const { entry, ready, answers, frameUrl, onReady, globals = {} } = options;
3639
+ const { entry, ready, answers, frameUrl, onReady, globals = {}, setup } = options;
3538
3640
  const config = withGlobals(
3539
3641
  ready ? schemaFromDescription(ready.schema, answers) : f.schema({}),
3540
3642
  globals
@@ -3556,7 +3658,8 @@ function storyInstrument(options) {
3556
3658
  answers,
3557
3659
  onReady,
3558
3660
  descriptionKey,
3559
- ctx
3661
+ ctx,
3662
+ ...setup ? { setup } : {}
3560
3663
  }
3561
3664
  ),
3562
3665
  /* @__PURE__ */ jsx(CaptureInputs, { entry, layout: ready?.layout ?? "centered", config: ctx.config, state: ctx.state })
@@ -3628,30 +3731,9 @@ function useStoryRegistry(index, options) {
3628
3731
  });
3629
3732
  };
3630
3733
  if (isIndexId(entry.id)) {
3631
- const entries = liveRef.current.index.filter((e) => !isIndexId(e.id) && indexId(e.title) === entry.id);
3632
- const files = [...new Set(entries.map((e) => e.file))];
3633
- void Promise.all(files.map(importOf)).then(async (mods) => {
3634
- const byFile = new Map(files.map((file, i) => [file, loadModule(mods[i] ?? {}, entry.title, current.parameters)]));
3635
- const stories = entries.flatMap(
3636
- (e) => byFile.get(e.file)?.find((s) => s.exportName === e.exportName) ?? []
3637
- );
3638
- for (const story of stories) await current.prepare?.(story);
3639
- const result = /* @__PURE__ */ new Map();
3640
- result.set(entry.id, {
3641
- kind: "index",
3642
- bundle: {
3643
- title: entry.title,
3644
- library: liveRef.current.libraries.of(entry),
3645
- ...entry.componentDescription === void 0 ? {} : { description: entry.componentDescription },
3646
- ...entry.tags === void 0 ? {} : { tags: entry.tags },
3647
- galleries: entries.filter(isGallery).map((e) => e.id),
3648
- stories,
3649
- descriptions: Object.fromEntries(entries.flatMap((e) => e.description ? [[e.id, e.description]] : [])),
3650
- render: mods.map(indexRenderOf).find((render) => render !== null) ?? null
3651
- }
3652
- });
3653
- settle([entry.id], result);
3654
- }).catch((error) => fail([entry.id], error));
3734
+ const { index: entries, libraries: libraries2 } = liveRef.current;
3735
+ const source = { index: entries, importOf, load: loadModule, parameters: current.parameters, prepare: current.prepare, libraries: libraries2 };
3736
+ void loadIndexBundle(entry.id, source).then((bundle) => settle([entry.id], /* @__PURE__ */ new Map([[entry.id, { kind: "index", bundle }]]))).catch((error) => fail([entry.id], error));
3655
3737
  return;
3656
3738
  }
3657
3739
  const siblings = liveRef.current.index.filter((e) => e.file === entry.file && !isIndexId(e.id));
@@ -3716,7 +3798,7 @@ function useStoryRegistry(index, options) {
3716
3798
  inDocument,
3717
3799
  generation,
3718
3800
  revision,
3719
- instrument: !inDocument ? storyInstrument({ entry, ready, answers, frameUrl, onReady, globals }) : had ? documentInstrument({ entry, loaded: had, setup, globals }) : pendingInstrument({ entry, globals, load: request, generation, ...fault ? { fault } : {} })
3801
+ instrument: !inDocument ? storyInstrument({ entry, ready, answers, frameUrl, onReady, globals, setup }) : had ? documentInstrument({ entry, loaded: had, setup, globals }) : pendingInstrument({ entry, globals, load: request, generation, ...fault ? { fault } : {} })
3720
3802
  };
3721
3803
  built.set(entry.id, kept2);
3722
3804
  return kept2.instrument;
@@ -3773,7 +3855,7 @@ function Workshop(props) {
3773
3855
  const declarations = config?.globals ?? NO_DECLARATIONS;
3774
3856
  const title = config?.title ?? DEFAULT_TITLE;
3775
3857
  useDocumentTitle(title);
3776
- const [frames] = useState(createTrialFrames);
3858
+ const [frames2] = useState(createTrialFrames);
3777
3859
  const [clocks] = useState(createTrialClocks);
3778
3860
  const [pool, setPool] = useState(null);
3779
3861
  const framed = importers === void 0 || index.some((entry) => entry.isolate !== void 0);
@@ -3833,7 +3915,7 @@ function Workshop(props) {
3833
3915
  const [menus, setMenus] = useState();
3834
3916
  const first = index[0];
3835
3917
  if (!first) return /* @__PURE__ */ jsx(NoStories, { stories });
3836
- 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__ */ jsx(LoupeProvider, { captureBase: props.captureBase, revision: props.revision, children: /* @__PURE__ */ jsxs(
3918
+ 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: frames2, children: /* @__PURE__ */ jsx(TrialClocksContext.Provider, { value: clocks, children: /* @__PURE__ */ jsx(LoupeProvider, { captureBase: props.captureBase, revision: props.revision, children: /* @__PURE__ */ jsxs(
3837
3919
  Lab,
3838
3920
  {
3839
3921
  title,