@weasel-js/forge 1.13.0-rc.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 (40) hide show
  1. package/dist/{FrameController-Tv3jrHyT.d.ts → FrameController-DgfVaENb.d.ts} +13 -1
  2. package/dist/{chunk-GH5RE5X5.js → chunk-2ZBZRBHI.js} +2 -2
  3. package/dist/{chunk-GH5RE5X5.js.map → chunk-2ZBZRBHI.js.map} +1 -1
  4. package/dist/{chunk-KEQBGDUZ.js → chunk-7XJNVJB5.js} +3 -3
  5. package/dist/{chunk-KEQBGDUZ.js.map → chunk-7XJNVJB5.js.map} +1 -1
  6. package/dist/{chunk-LTTEILXY.js → chunk-COT4I347.js} +3 -3
  7. package/dist/{chunk-LTTEILXY.js.map → chunk-COT4I347.js.map} +1 -1
  8. package/dist/{chunk-35DOPXHZ.js → chunk-GHPMNT2A.js} +13 -10
  9. package/dist/chunk-GHPMNT2A.js.map +1 -0
  10. package/dist/{chunk-STF6NQ5N.js → chunk-RVJHUY7W.js} +2 -2
  11. package/dist/chunk-RVJHUY7W.js.map +1 -0
  12. package/dist/{chunk-BUXUWAI4.js → chunk-WFT5IOHX.js} +61 -8
  13. package/dist/chunk-WFT5IOHX.js.map +1 -0
  14. package/dist/{chunk-RKQ277P5.js → chunk-XIBKYSRU.js} +5 -4
  15. package/dist/chunk-XIBKYSRU.js.map +1 -0
  16. package/dist/{config-Bd6FVlh7.d.ts → config-6fWf87Tt.d.ts} +3 -21
  17. package/dist/csf/preview-api.js +2 -2
  18. package/dist/frame/index.d.ts +14 -8
  19. package/dist/frame/index.js +109 -77
  20. package/dist/frame/index.js.map +1 -1
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.js +2 -2
  23. package/dist/library-CdTj-hFp.d.ts +21 -0
  24. package/dist/{mountFrame-FdJDb3TB.d.ts → mountFrame-Bs7u1ro-.d.ts} +1 -1
  25. package/dist/shell/index.css +2 -3
  26. package/dist/shell/index.css.map +1 -1
  27. package/dist/shell/index.d.ts +5 -4
  28. package/dist/shell/index.js +150 -94
  29. package/dist/shell/index.js.map +1 -1
  30. package/dist/test/index.css +2 -3
  31. package/dist/test/index.css.map +1 -1
  32. package/dist/test/index.d.ts +1 -1
  33. package/dist/test/index.js +4 -4
  34. package/dist/vite/index.js +9 -2
  35. package/dist/vite/index.js.map +1 -1
  36. package/package.json +9 -9
  37. package/dist/chunk-35DOPXHZ.js.map +0 -1
  38. package/dist/chunk-BUXUWAI4.js.map +0 -1
  39. package/dist/chunk-RKQ277P5.js.map +0 -1
  40. package/dist/chunk-STF6NQ5N.js.map +0 -1
@@ -1,16 +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, librariesFrom, LibrariesContext, LAB_DENSITY, createKitRoot, schemaFromDescription, LibraryBadge, Description, IndexPage, LabBadge, openChannel } from '../chunk-BUXUWAI4.js';
4
- import { isIndexId, indexId, indexEntries } from '../chunk-STF6NQ5N.js';
5
- import { taggedIds, 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, withArgs, scrollerOf } from '../chunk-RKQ277P5.js';
7
- export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-RKQ277P5.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';
8
8
  import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, PLAYHEAD_KEY, labOptions, isForgePath, isGlobalsPath } from '../chunk-3BAM6M26.js';
9
- import { loadStories, indexRenderOf, useNearViewport, IN_VIEW_MARGIN, varRefs, cssRulesOf } from '../chunk-35DOPXHZ.js';
9
+ import { loadStories, useNearViewport, IN_VIEW_MARGIN, applyGlobalsTo, varRefs, cssRulesOf } from '../chunk-GHPMNT2A.js';
10
10
  import '../chunk-L5XM5VKB.js';
11
11
  import '../chunk-5WS67HMU.js';
12
- import '../chunk-KEQBGDUZ.js';
13
- 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';
14
14
  import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, useTrialState, ToggleBar as ToggleBar$1, Input, TokenPanel, useLabEntries, inferTokenType, refitScale } from '@weasel-js/labkit';
15
15
  import num from '@weasel-js/theme/numeric.module.css';
16
16
  import { createContext, useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect, useId, useContext, useReducer, useSyncExternalStore, Fragment as Fragment$1 } from 'react';
@@ -21,7 +21,7 @@ import { Slider } from '@weasel-js/ui/components/Slider';
21
21
  import { Transport } from '@weasel-js/ui/components/Timeline';
22
22
  import { TOKEN_MANIFEST, themeAxes, weaselTheme, THEME_SOURCES, resolveTheme, isColorModePreference, isByAxis } from '@weasel-js/theme';
23
23
  import { scale, httpThemeApi } from '@weasel-js/theme/engine';
24
- 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';
25
25
  import { Button as Button$1 } from '@weasel-js/ui/components/Button';
26
26
  import { Checkbox } from '@weasel-js/ui/components/Checkbox';
27
27
  import { Icon } from '@weasel-js/ui/components/Icon';
@@ -342,32 +342,12 @@ function IndexTrial({ bundle, setup, config }) {
342
342
  const lab = useLabContext();
343
343
  const trialId = useContext(TrialIdContext);
344
344
  const graph = useContext(DependenciesContext);
345
- const gallery = isGallery(bundle);
346
- const dependencies = gallery ? void 0 : graph?.[bundle.title];
347
345
  const open = (id) => {
348
346
  if (!trialId || !lab.instruments.some((instrument) => instrument.name === id)) return;
349
347
  lab.swapTrial(trialId, id);
350
348
  setRoute(id, { inPlace: true });
351
349
  };
352
- const render = (globals) => {
353
- const env = {
354
- title: bundle.title,
355
- library: bundle.library,
356
- ...bundle.description === void 0 ? {} : { description: bundle.description },
357
- stories: bundle.stories,
358
- descriptions: bundle.descriptions,
359
- gallery,
360
- galleries: bundle.galleries,
361
- labs: bundle.labs,
362
- ...dependencies ? { dependencies } : {},
363
- globals,
364
- decorators: setup.decorators ?? [],
365
- open,
366
- // A story that throws shows its error in its own cell; the rest of the page is still worth reading.
367
- onError: (error) => console.error(error)
368
- };
369
- return /* @__PURE__ */ jsx(IndexPage, { env, render: bundle.render });
370
- };
350
+ const render = (globals) => /* @__PURE__ */ jsx(IndexPage, { env: indexEnvOf(bundle, { graph, globals, decorators: setup.decorators ?? [], open }), render: bundle.render });
371
351
  return /* @__PURE__ */ jsxs(Fragment, { children: [
372
352
  /* @__PURE__ */ jsx(TrialHost, { layout: "fullscreen", setup, config, children: render }),
373
353
  /* @__PURE__ */ jsx(LiveCopies, { trialId, layout: "fullscreen", setup, trialConfig: config, children: render })
@@ -416,10 +396,18 @@ function documentInstrument(options) {
416
396
  config,
417
397
  defaultConfig: () => config.defaults(),
418
398
  initialState: (c) => initialState?.(c) ?? null,
419
- render: (ctx) => loaded.kind === "story" ? /* @__PURE__ */ jsxs(Fragment, { children: [
420
- /* @__PURE__ */ jsx(StoryTrial, { story: loaded.story, setup, ctx }, loaded.story.id),
421
- /* @__PURE__ */ jsx(CaptureInputs, { entry, layout: loaded.story.layout, config: ctx.config, state: ctx.state })
422
- ] }) : /* @__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
+ ] }),
423
411
  ...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {},
424
412
  ...timed ? { chrome: [{ id: "fg-transport", region: "status", render: (c) => /* @__PURE__ */ jsx(TrialTransport, { trialId: c.trialId }) }] } : {}
425
413
  };
@@ -797,10 +785,10 @@ function useThemeSave({ trialId, globals, themeApi, scales, overrides, history:
797
785
  ...Object.keys(tokens).map((token) => `${TOKEN_PREFIX}${token}`)
798
786
  ]);
799
787
  const groups = new Set(scales.map((scale) => scale.group));
800
- const now = history2.current();
788
+ const now2 = history2.current();
801
789
  history2.reset({
802
- overrides: Object.fromEntries(Object.entries(now.overrides).filter(([name]) => !saved.has(name))),
803
- 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)))
804
792
  });
805
793
  setNotice(
806
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("; ")}` }
@@ -1010,16 +998,16 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
1010
998
  }
1011
999
  });
1012
1000
  const write = (name, value) => {
1013
- const now = history2.current();
1014
- const { [name]: _dropped, ...rest } = now.overrides;
1015
- 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}`);
1016
1004
  };
1017
1005
  const onScaleChange = (group, next) => {
1018
1006
  const source = SCALE_SOURCES.find((sc) => sc.group === group);
1019
1007
  if (!source) return;
1020
- const now = history2.current();
1008
+ const now2 = history2.current();
1021
1009
  history2.commit(
1022
- { 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 } },
1023
1011
  `scale:${group}`
1024
1012
  );
1025
1013
  };
@@ -1583,15 +1571,40 @@ var liveCellId = (lensId, compare) => compare === null ? lensId : `${lensId}~com
1583
1571
  function sameRequest(a, b) {
1584
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);
1585
1573
  }
1586
- 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) {
1587
1590
  const measures = /* @__PURE__ */ new Set();
1588
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
+ };
1589
1602
  return {
1590
1603
  kind: "live",
1591
1604
  produces: "dom",
1592
1605
  label: () => "Live",
1593
1606
  status: () => true,
1594
- // 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.
1595
1608
  subscribe(listener) {
1596
1609
  listeners2.add(listener);
1597
1610
  return () => {
@@ -1606,10 +1619,9 @@ function createLiveSource(lenses, resolve, conditions) {
1606
1619
  empty.className = "fg-lens-empty";
1607
1620
  empty.hidden = true;
1608
1621
  slot.append(target, empty);
1609
- const watch = typeof MutationObserver === "undefined" ? null : new MutationObserver(() => {
1610
- for (const listener of [...listeners2]) listener();
1611
- });
1612
- watch?.observe(target, { subtree: true, childList: true, characterData: true, attributes: true });
1622
+ const watchCopy = watchOf(notify);
1623
+ watchCopy(target);
1624
+ const watchStory = watchOf(notify);
1613
1625
  let request = first;
1614
1626
  let posted = null;
1615
1627
  const post = (next) => {
@@ -1620,6 +1632,7 @@ function createLiveSource(lenses, resolve, conditions) {
1620
1632
  const measure = () => {
1621
1633
  const { lensId, reticle, conditions: compare } = request;
1622
1634
  const hit = trialAt(reticle, doc);
1635
+ watchStory(compare === null ? hit?.host ?? null : null);
1623
1636
  empty.hidden = hit !== null;
1624
1637
  if (!hit) {
1625
1638
  post(null);
@@ -1647,7 +1660,8 @@ function createLiveSource(lenses, resolve, conditions) {
1647
1660
  measure();
1648
1661
  },
1649
1662
  detach() {
1650
- watch?.disconnect();
1663
+ watchCopy(null);
1664
+ watchStory(null);
1651
1665
  measures.delete(measure);
1652
1666
  lenses.set(liveCellId(request.lensId, request.conditions), null);
1653
1667
  target.remove();
@@ -1986,6 +2000,22 @@ function renderReadouts(props) {
1986
2000
  readouts.includes("contrast") ? /* @__PURE__ */ jsx(ContrastReadout, { ...props }) : null
1987
2001
  ] });
1988
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
1989
2019
  var STATE_UNSENDABLE = "this story's state cannot be sent to a capture";
1990
2020
  var CONFIG_UNSENDABLE = "this story's config cannot be sent to a capture";
1991
2021
  var NO_STORY = "this trial shows no story to capture";
@@ -2025,7 +2055,6 @@ function capturePayload(input) {
2025
2055
  const box = { width: host.offsetWidth, height: host.offsetHeight };
2026
2056
  if (!(box.width > 0 && box.height > 0)) return { unavailable: NO_BOX };
2027
2057
  const isolate = entry.isolate !== void 0;
2028
- const scroller = scrollerOf(host);
2029
2058
  const kept2 = config?.[PLAYHEAD_KEY];
2030
2059
  return {
2031
2060
  story: entry.id,
@@ -2038,7 +2067,7 @@ function capturePayload(input) {
2038
2067
  box,
2039
2068
  layout,
2040
2069
  dpr: clampDpr(resolved.dpr ?? dpr),
2041
- scroll: { left: scroller?.scrollLeft ?? 0, top: scroller?.scrollTop ?? 0 },
2070
+ scroll: scrollOf(host),
2042
2071
  lab,
2043
2072
  colorScheme
2044
2073
  };
@@ -2341,7 +2370,14 @@ function useLensFocus() {
2341
2370
  }
2342
2371
  function createApi(scene, refs) {
2343
2372
  let next = 0;
2344
- 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
+ });
2345
2381
  const toBody = (client, box) => ({ x: client.x - box.left, y: client.y - box.top });
2346
2382
  const apply = (ops) => {
2347
2383
  if (ops.length > 0) sceneToAdapter(scene).applyOps([...ops], "lens");
@@ -2373,10 +2409,11 @@ function createApi(scene, refs) {
2373
2409
  if (lensId === null || !body) return;
2374
2410
  const pose = reticleAt(scene, lensId, toBody(client, body.getBoundingClientRect()));
2375
2411
  if (pose === null) return;
2376
- hold ??= scene.overrides.hold();
2412
+ const hold = holds.get(lensId) ?? scene.overrides.hold();
2413
+ holds.set(lensId, hold);
2377
2414
  hold.set([[`${lensId}:reticle`, pose]]);
2378
2415
  },
2379
- unfollow: () => hold?.drop(),
2416
+ unfollow: release,
2380
2417
  sourceAvailable: (kind) => refs.sources.current[kind]?.status() ?? "not provided",
2381
2418
  openCompareMenu: (lensId) => refs.compare?.current?.(lensId)
2382
2419
  };
@@ -2407,6 +2444,21 @@ function useRemeasure(sources, body, rerender) {
2407
2444
  };
2408
2445
  }, [sources, body, rerender]);
2409
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
+ }
2410
2462
  function ForgeLoupe({ declarations, modeGlobal }) {
2411
2463
  const lenses = useContext(LiveLensesContext);
2412
2464
  const labValues = useContext(StoryGlobalsContext);
@@ -2417,8 +2469,14 @@ function ForgeLoupe({ declarations, modeGlobal }) {
2417
2469
  const pointer = useRef(null);
2418
2470
  const { focus, onFocusLens } = useLensFocus();
2419
2471
  const conditions = useLatest({ labValues, mode, declarations, modeGlobal });
2472
+ const onFrame = useFrameSchedule();
2420
2473
  const [live] = useState(
2421
- () => 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
2422
2480
  );
2423
2481
  const pixels = usePixelsSource(conditions);
2424
2482
  const sources = useMemo(
@@ -3276,7 +3334,7 @@ function sendInputs(current, sent, next) {
3276
3334
  return current.inputs !== before;
3277
3335
  }
3278
3336
  function receive(deps, current, msg) {
3279
- const { latest: latestRef, trialId, frames, setFault, setPending, init, copy } = deps;
3337
+ const { latest: latestRef, trialId, frames: frames2, host, setFault, setPending, init, copy } = deps;
3280
3338
  const { ctx: live, answers, onReady } = latestRef.current;
3281
3339
  switch (msg.type) {
3282
3340
  case "ready": {
@@ -3311,15 +3369,18 @@ function receive(deps, current, msg) {
3311
3369
  if (!copy) answers.record(msg.answers);
3312
3370
  break;
3313
3371
  case "vars":
3314
- if (trialId) frames?.report(trialId, msg.vars);
3372
+ if (trialId) frames2?.report(trialId, msg.vars);
3315
3373
  break;
3316
3374
  case "a11y": {
3317
3375
  const outcome = msg.ok ? { ok: true, report: msg.report } : { ok: false, message: msg.message };
3318
- if (trialId) frames?.reportA11y(trialId, outcome);
3376
+ if (trialId) frames2?.reportA11y(trialId, outcome);
3319
3377
  current.pending.get(msg.id)?.(outcome);
3320
3378
  current.pending.delete(msg.id);
3321
3379
  break;
3322
3380
  }
3381
+ case "scroll":
3382
+ if (!copy && host.current) reportFrameScroll(host.current, msg);
3383
+ break;
3323
3384
  case "open": {
3324
3385
  const { lab: liveLab } = latestRef.current;
3325
3386
  if (!trialId || !liveLab.instruments.some((instrument) => instrument.name === msg.id)) break;
@@ -3335,7 +3396,7 @@ function receive(deps, current, msg) {
3335
3396
  }
3336
3397
  }
3337
3398
  function openLink(deps, link, frame) {
3338
- const { latest: latestRef, src, trialId, frames, setFault, setPending } = deps;
3399
+ const { latest: latestRef, src, trialId, frames: frames2, setFault, setPending } = deps;
3339
3400
  closeLink(link);
3340
3401
  setPending(true);
3341
3402
  const target = frame.contentWindow;
@@ -3356,7 +3417,7 @@ function openLink(deps, link, frame) {
3356
3417
  if (current.hasRendered) ask();
3357
3418
  else current.queued.push(ask);
3358
3419
  });
3359
- const disconnect = frames && trialId ? frames.connect(trialId, { send: channel.send, audit }) : () => {
3420
+ const disconnect = frames2 && trialId ? frames2.connect(trialId, { send: channel.send, audit }) : () => {
3360
3421
  };
3361
3422
  const current = {
3362
3423
  channel,
@@ -3377,7 +3438,7 @@ function openLink(deps, link, frame) {
3377
3438
  ]);
3378
3439
  }
3379
3440
  function useFrameLink(opts) {
3380
- 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;
3381
3442
  const link = useRef(null);
3382
3443
  const init = useCallback((current) => {
3383
3444
  const { ctx: live, globals: liveGlobals } = latestRef.current;
@@ -3386,7 +3447,7 @@ function useFrameLink(opts) {
3386
3447
  current.inputs += 1;
3387
3448
  current.channel.send({ type: "init", ...current.sent });
3388
3449
  }, [latestRef]);
3389
- 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 };
3390
3451
  const connect = (frame) => openLink(deps, link, frame);
3391
3452
  const adopt2 = (adopted) => {
3392
3453
  adopted.deliver = (msg) => receive(deps, adopted, msg);
@@ -3504,7 +3565,7 @@ function FrameView(props) {
3504
3565
  [copy, trialCtx, laidConfig, laidSetConfig]
3505
3566
  );
3506
3567
  const trialId = useContext(TrialIdContext);
3507
- const frames = useContext(TrialFramesContext);
3568
+ const frames2 = useContext(TrialFramesContext);
3508
3569
  const [overrides] = useCssOverrides();
3509
3570
  const pool = useContext(FramePoolContext);
3510
3571
  const lab = useLabContext();
@@ -3521,7 +3582,8 @@ function FrameView(props) {
3521
3582
  latest: latestRef,
3522
3583
  src,
3523
3584
  trialId: copy ? null : trialId,
3524
- frames: copy ? null : frames,
3585
+ frames: copy ? null : frames2,
3586
+ host: hostRef,
3525
3587
  setFault,
3526
3588
  setPending,
3527
3589
  copy: copy !== void 0,
@@ -3550,6 +3612,20 @@ function FrameView(props) {
3550
3612
  if (iframeRef.current) iframeRef.current.title = title;
3551
3613
  }, [title]);
3552
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]);
3553
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: [
3554
3630
  /* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
3555
3631
  fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
@@ -3560,7 +3636,7 @@ function FrameView(props) {
3560
3636
  ] });
3561
3637
  }
3562
3638
  function storyInstrument(options) {
3563
- const { entry, ready, answers, frameUrl, onReady, globals = {} } = options;
3639
+ const { entry, ready, answers, frameUrl, onReady, globals = {}, setup } = options;
3564
3640
  const config = withGlobals(
3565
3641
  ready ? schemaFromDescription(ready.schema, answers) : f.schema({}),
3566
3642
  globals
@@ -3582,7 +3658,8 @@ function storyInstrument(options) {
3582
3658
  answers,
3583
3659
  onReady,
3584
3660
  descriptionKey,
3585
- ctx
3661
+ ctx,
3662
+ ...setup ? { setup } : {}
3586
3663
  }
3587
3664
  ),
3588
3665
  /* @__PURE__ */ jsx(CaptureInputs, { entry, layout: ready?.layout ?? "centered", config: ctx.config, state: ctx.state })
@@ -3654,30 +3731,9 @@ function useStoryRegistry(index, options) {
3654
3731
  });
3655
3732
  };
3656
3733
  if (isIndexId(entry.id)) {
3657
- const entries = liveRef.current.index.filter((e) => !isIndexId(e.id) && indexId(e.title) === entry.id);
3658
- const files = [...new Set(entries.map((e) => e.file))];
3659
- void Promise.all(files.map(importOf)).then(async (mods) => {
3660
- const byFile = new Map(files.map((file, i) => [file, loadModule(mods[i] ?? {}, entry.title, current.parameters)]));
3661
- const stories = entries.flatMap(
3662
- (e) => byFile.get(e.file)?.find((s) => s.exportName === e.exportName) ?? []
3663
- );
3664
- for (const story of stories) await current.prepare?.(story);
3665
- const result = /* @__PURE__ */ new Map();
3666
- result.set(entry.id, {
3667
- kind: "index",
3668
- bundle: {
3669
- title: entry.title,
3670
- library: liveRef.current.libraries.of(entry),
3671
- ...entry.componentDescription === void 0 ? {} : { description: entry.componentDescription },
3672
- ...entry.tags === void 0 ? {} : { tags: entry.tags },
3673
- ...taggedIds(entries),
3674
- stories,
3675
- descriptions: Object.fromEntries(entries.flatMap((e) => e.description ? [[e.id, e.description]] : [])),
3676
- render: mods.map(indexRenderOf).find((render) => render !== null) ?? null
3677
- }
3678
- });
3679
- settle([entry.id], result);
3680
- }).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));
3681
3737
  return;
3682
3738
  }
3683
3739
  const siblings = liveRef.current.index.filter((e) => e.file === entry.file && !isIndexId(e.id));
@@ -3742,7 +3798,7 @@ function useStoryRegistry(index, options) {
3742
3798
  inDocument,
3743
3799
  generation,
3744
3800
  revision,
3745
- 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 } : {} })
3746
3802
  };
3747
3803
  built.set(entry.id, kept2);
3748
3804
  return kept2.instrument;
@@ -3799,7 +3855,7 @@ function Workshop(props) {
3799
3855
  const declarations = config?.globals ?? NO_DECLARATIONS;
3800
3856
  const title = config?.title ?? DEFAULT_TITLE;
3801
3857
  useDocumentTitle(title);
3802
- const [frames] = useState(createTrialFrames);
3858
+ const [frames2] = useState(createTrialFrames);
3803
3859
  const [clocks] = useState(createTrialClocks);
3804
3860
  const [pool, setPool] = useState(null);
3805
3861
  const framed = importers === void 0 || index.some((entry) => entry.isolate !== void 0);
@@ -3859,7 +3915,7 @@ function Workshop(props) {
3859
3915
  const [menus, setMenus] = useState();
3860
3916
  const first = index[0];
3861
3917
  if (!first) return /* @__PURE__ */ jsx(NoStories, { stories });
3862
- 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(
3863
3919
  Lab,
3864
3920
  {
3865
3921
  title,