@weasel-js/forge 1.11.0 → 1.12.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 (53) hide show
  1. package/README.md +92 -1
  2. package/dist/{FrameController-BRhShqut.d.ts → FrameController-CdnaIo4C.d.ts} +18 -10
  3. package/dist/{chunk-7BQPHMA2.js → chunk-3BAM6M26.js} +2 -2
  4. package/dist/chunk-3BAM6M26.js.map +1 -0
  5. package/dist/{chunk-ZAWXMUGZ.js → chunk-5WS67HMU.js} +8 -3
  6. package/dist/chunk-5WS67HMU.js.map +1 -0
  7. package/dist/{chunk-R5F44ECM.js → chunk-ATI7FYJ7.js} +18 -14
  8. package/dist/chunk-ATI7FYJ7.js.map +1 -0
  9. package/dist/{chunk-2OJAV522.js → chunk-GH5RE5X5.js} +2 -2
  10. package/dist/chunk-GH5RE5X5.js.map +1 -0
  11. package/dist/{chunk-SQ4KCIEH.js → chunk-KEQBGDUZ.js} +3 -3
  12. package/dist/{chunk-SQ4KCIEH.js.map → chunk-KEQBGDUZ.js.map} +1 -1
  13. package/dist/{chunk-DPTECNYC.js → chunk-LTTEILXY.js} +3 -3
  14. package/dist/{chunk-DPTECNYC.js.map → chunk-LTTEILXY.js.map} +1 -1
  15. package/dist/chunk-ORMMZ5PV.js +59 -0
  16. package/dist/chunk-ORMMZ5PV.js.map +1 -0
  17. package/dist/{chunk-NVNTQ4AT.js → chunk-WMGEO5M6.js} +133 -9
  18. package/dist/chunk-WMGEO5M6.js.map +1 -0
  19. package/dist/chunk-YDLRSEUN.js +501 -0
  20. package/dist/chunk-YDLRSEUN.js.map +1 -0
  21. package/dist/{config-BwScQukB.d.ts → config-Mg05Ja9r.d.ts} +20 -3
  22. package/dist/csf/preview-api.js +2 -2
  23. package/dist/frame/index.css +12 -0
  24. package/dist/frame/index.css.map +1 -1
  25. package/dist/frame/index.d.ts +62 -29
  26. package/dist/frame/index.js +142 -15
  27. package/dist/frame/index.js.map +1 -1
  28. package/dist/index.d.ts +4 -4
  29. package/dist/index.js +3 -3
  30. package/dist/index.js.map +1 -1
  31. package/dist/mountFrame-Ds3QLnBF.d.ts +35 -0
  32. package/dist/shell/index.css +131 -6
  33. package/dist/shell/index.css.map +1 -1
  34. package/dist/shell/index.d.ts +44 -16
  35. package/dist/shell/index.js +1663 -280
  36. package/dist/shell/index.js.map +1 -1
  37. package/dist/test/index.css +58 -0
  38. package/dist/test/index.css.map +1 -0
  39. package/dist/test/index.d.ts +2 -4
  40. package/dist/test/index.js +11 -13
  41. package/dist/test/index.js.map +1 -1
  42. package/dist/vite/index.js +515 -111
  43. package/dist/vite/index.js.map +1 -1
  44. package/package.json +13 -8
  45. package/dist/chunk-2OJAV522.js.map +0 -1
  46. package/dist/chunk-5DXUNLV2.js +0 -6
  47. package/dist/chunk-5DXUNLV2.js.map +0 -1
  48. package/dist/chunk-7BQPHMA2.js.map +0 -1
  49. package/dist/chunk-J3GYSNCK.js +0 -285
  50. package/dist/chunk-J3GYSNCK.js.map +0 -1
  51. package/dist/chunk-NVNTQ4AT.js.map +0 -1
  52. package/dist/chunk-R5F44ECM.js.map +0 -1
  53. package/dist/chunk-ZAWXMUGZ.js.map +0 -1
@@ -1,38 +1,58 @@
1
- import { parseRoute, formatRoute, paramsToKnobs, knobPaths, sameKnob, knobsToParams, PLAYHEAD_PARAM, reservedParams } from '../chunk-DPTECNYC.js';
2
- import { DEFAULT_TITLE } from '../chunk-5DXUNLV2.js';
3
- import { TITLE_LIBRARIES, isIndexId, indexId, indexEntries, librariesFrom, LibrariesContext, createKitRoot, schemaFromDescription, LibraryBadge, openChannel, IndexPage } from '../chunk-R5F44ECM.js';
1
+ import { parseRoute, formatRoute, paramsToKnobs, knobPaths, sameKnob, knobsToParams, PLAYHEAD_PARAM, reservedParams } from '../chunk-LTTEILXY.js';
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
4
  import { isGallery } from '../chunk-WEQSLPKT.js';
5
- import { useTrialFrame, createTrialFrames, createTrialClocks, StoryGlobalsContext, TrialFramesContext, TrialClocksContext, StoryTrial, useCssOverrides, useTrialClock, TrialHost } from '../chunk-J3GYSNCK.js';
6
- export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-J3GYSNCK.js';
7
- import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, PLAYHEAD_KEY, labOptions, isGlobalsPath } from '../chunk-7BQPHMA2.js';
8
- import { loadStories, indexRenderOf, useNearViewport, IN_VIEW_MARGIN } from '../chunk-NVNTQ4AT.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';
7
+ 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
9
  import '../chunk-L5XM5VKB.js';
10
- import '../chunk-ZAWXMUGZ.js';
11
- import '../chunk-SQ4KCIEH.js';
12
- import { stableStringify, PORT_HANDOFF, FRAME_HELLO, PROTOCOL_VERSION } from '../chunk-2OJAV522.js';
13
- import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, useTrialState, ToggleBar as ToggleBar$1, Input, TokenPanel, inferTokenType, refitScale } from '@weasel-js/labkit';
10
+ import '../chunk-5WS67HMU.js';
11
+ import '../chunk-KEQBGDUZ.js';
12
+ import { stableStringify, FRAME_HELLO, PORT_HANDOFF, PROTOCOL_VERSION } from '../chunk-GH5RE5X5.js';
13
+ import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, useTrialState, ToggleBar as ToggleBar$1, Input, TokenPanel, useLabEntries, inferTokenType, refitScale } from '@weasel-js/labkit';
14
14
  import num from '@weasel-js/theme/numeric.module.css';
15
- import { createContext, useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect, useId, useContext, Fragment as Fragment$1, useSyncExternalStore } from 'react';
15
+ import { createContext, useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect, useId, useContext, useReducer, useSyncExternalStore, Fragment as Fragment$1 } from 'react';
16
16
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
17
17
  import { createEmitter } from '@weasel-js/registry';
18
18
  import { f, valueAtPath, isConfigBranch } from '@weasel-js/labkit/config';
19
- import { formatShortcut, ToggleBar, Checkbox, Dialog, TOKEN_CATEGORIES, Icon, DisclosureMark, Button as Button$1, Callout, Slider, Transport } from '@weasel-js/ui';
19
+ import { Slider } from '@weasel-js/ui/components/Slider';
20
+ import { Transport } from '@weasel-js/ui/components/Timeline';
20
21
  import { TOKEN_MANIFEST, themeAxes, weaselTheme, THEME_SOURCES, resolveTheme, isColorModePreference, isByAxis } from '@weasel-js/theme';
21
22
  import { scale, httpThemeApi } from '@weasel-js/theme/engine';
22
- import { useLatest, historyKey, createHistory, swapOp, isPlainObject } from '@weasel-js/core';
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 { Button as Button$1 } from '@weasel-js/ui/components/Button';
25
+ import { Checkbox } from '@weasel-js/ui/components/Checkbox';
26
+ import { Icon } from '@weasel-js/ui/components/Icon';
27
+ import { TOKEN_CATEGORIES, tokenCategory } from '@weasel-js/ui/components/TokenPanel';
28
+ import { ToggleBar } from '@weasel-js/ui/components/ToggleBar';
23
29
  import { useResolvedColorMode } from '@weasel-js/theme/react';
30
+ import { formatShortcut } from '@weasel-js/ui/components/ToolPalette';
31
+ import { Dialog } from '@weasel-js/ui/components/Dialog';
32
+ import { createLensScene, settingsOf, resolveSource, LensOverlay, LensActions, lensIdsOf, reticleAt, lensesOf, MAX_LENSES, addLensOps, unavailableText, useShownSource, requestsOf, BODY_SIZE, setLensOps, readBitmap, LENS_REST_MS, probeUnderLens } from '@weasel-js/labkit/loupe';
33
+ import { KeymapHint } from '@weasel-js/ui/components/KeymapHint';
24
34
  import { pref } from '@weasel-js/prefs';
35
+ import { splitTopLevel, specificity, compareSpecificity } from '@weasel-js/svg';
36
+ import { Callout } from '@weasel-js/ui/components/Callout';
37
+ import { DisclosureMark } from '@weasel-js/ui/components/Disclosure';
25
38
 
26
- function Finding({ finding }) {
27
- return /* @__PURE__ */ jsxs("li", { className: "fg-a11y__finding", children: [
39
+ function pointAt(point, nodes) {
40
+ const enter = (event) => {
41
+ event.stopPropagation();
42
+ point(nodes);
43
+ };
44
+ return { onMouseOver: enter, onFocus: enter, onMouseLeave: () => point(null), onBlur: () => point(null) };
45
+ }
46
+ function Finding({ finding, point }) {
47
+ return /* @__PURE__ */ jsxs("li", { className: "fg-a11y__finding", ...pointAt(point, finding.nodes), children: [
28
48
  /* @__PURE__ */ jsxs("p", { className: "fg-a11y__help", children: [
29
49
  /* @__PURE__ */ jsx("a", { href: finding.helpUrl, target: "_blank", rel: "noreferrer", children: finding.help }),
30
50
  finding.impact ? /* @__PURE__ */ jsx("span", { className: "fg-a11y__impact", "data-impact": finding.impact, children: finding.impact }) : null
31
51
  ] }),
32
- /* @__PURE__ */ jsx("ul", { className: "fg-a11y__nodes", children: finding.nodes.map((node) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("code", { children: node.target.join(" ") }) }, `${finding.id}:${node.target.join(",")}`)) })
52
+ /* @__PURE__ */ jsx("ul", { className: "fg-a11y__nodes", children: finding.nodes.map((node) => /* @__PURE__ */ jsx("li", { ...pointAt(point, [node]), children: /* @__PURE__ */ jsx("code", { children: node.target.join(" ") }) }, `${finding.id}:${node.target.join(",")}`)) })
33
53
  ] });
34
54
  }
35
- function Findings({ title, findings }) {
55
+ function Findings({ title, findings, point }) {
36
56
  if (findings.length === 0) return null;
37
57
  return /* @__PURE__ */ jsxs("section", { className: "fg-a11y__group", children: [
38
58
  /* @__PURE__ */ jsxs("h3", { className: `fg-a11y__group-title ${num.numeric}`, children: [
@@ -41,7 +61,7 @@ function Findings({ title, findings }) {
41
61
  findings.length,
42
62
  ")"
43
63
  ] }),
44
- /* @__PURE__ */ jsx("ul", { className: "fg-a11y__findings", children: findings.map((finding) => /* @__PURE__ */ jsx(Finding, { finding }, finding.id)) })
64
+ /* @__PURE__ */ jsx("ul", { className: "fg-a11y__findings", children: findings.map((finding) => /* @__PURE__ */ jsx(Finding, { finding, point }, finding.id)) })
45
65
  ] });
46
66
  }
47
67
  function A11yPanel() {
@@ -60,6 +80,13 @@ function A11yPanel() {
60
80
  ran.current = audit;
61
81
  start(audit);
62
82
  }, [audit, start]);
83
+ const send = frame.send;
84
+ const [pointed, setPointed] = useState(null);
85
+ useEffect(() => {
86
+ if (!send || !pointed) return;
87
+ send({ type: "a11y.highlight", targets: pointed.map((node) => node.target) });
88
+ return () => send({ type: "a11y.highlight", targets: [] });
89
+ }, [send, pointed]);
63
90
  const rerun = () => {
64
91
  if (audit) start(audit);
65
92
  };
@@ -82,8 +109,8 @@ function A11yPanel() {
82
109
  outcome.report.passes,
83
110
  " passes"
84
111
  ] }),
85
- /* @__PURE__ */ jsx(Findings, { title: "Violations", findings: outcome.report.violations }),
86
- /* @__PURE__ */ jsx(Findings, { title: "Needs review", findings: outcome.report.incomplete })
112
+ /* @__PURE__ */ jsx(Findings, { title: "Violations", findings: outcome.report.violations, point: setPointed }),
113
+ /* @__PURE__ */ jsx(Findings, { title: "Needs review", findings: outcome.report.incomplete, point: setPointed })
87
114
  ] }) : null
88
115
  ] });
89
116
  }
@@ -102,6 +129,56 @@ var A11Y_SECTION = {
102
129
  region: "aside",
103
130
  item: { title: "Accessibility", body: /* @__PURE__ */ jsx(FocusedA11y, {}) }
104
131
  };
132
+
133
+ // src/shell/followEdits.ts
134
+ function followEdits(hot, workshop) {
135
+ const stale = /* @__PURE__ */ new Set();
136
+ hot?.on("forge:index", (next) => workshop.setIndex(next));
137
+ hot?.on("forge:deps", (next) => workshop.setDependencies(next));
138
+ hot?.on("forge:story", ({ file }) => {
139
+ stale.add(file);
140
+ workshop.revise();
141
+ });
142
+ hot?.on("vite:beforeUpdate", () => workshop.revise());
143
+ return {
144
+ importers(next) {
145
+ if (!next) return;
146
+ workshop.setImporters(next);
147
+ for (const file of stale) workshop.reloadStory(file);
148
+ stale.clear();
149
+ }
150
+ };
151
+ }
152
+ function createRevision() {
153
+ let n = 0;
154
+ const bumped = createEmitter();
155
+ return {
156
+ get: () => n,
157
+ subscribe: bumped.subscribe,
158
+ bump() {
159
+ n += 1;
160
+ bumped.emit();
161
+ }
162
+ };
163
+ }
164
+ function createCaptureTrials() {
165
+ const trials = /* @__PURE__ */ new Map();
166
+ const changed = createEmitter();
167
+ return {
168
+ publish(trialId, inputs) {
169
+ trials.set(trialId, inputs);
170
+ changed.emit();
171
+ return () => {
172
+ if (trials.get(trialId) !== inputs) return;
173
+ trials.delete(trialId);
174
+ changed.emit();
175
+ };
176
+ },
177
+ get: (trialId) => trials.get(trialId) ?? null,
178
+ subscribe: changed.subscribe
179
+ };
180
+ }
181
+ var LoupeCaptureContext = createContext(null);
105
182
  function createStoryChanges() {
106
183
  const changes = createEmitter();
107
184
  return { subscribe: changes.subscribe, emit: changes.emit };
@@ -282,7 +359,38 @@ function IndexTrial({ bundle, setup, config }) {
282
359
  };
283
360
  return /* @__PURE__ */ jsx(IndexPage, { env, render: bundle.render });
284
361
  };
285
- return /* @__PURE__ */ jsx(TrialHost, { layout: "fullscreen", setup, config, children: render });
362
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
363
+ /* @__PURE__ */ jsx(TrialHost, { layout: "fullscreen", setup, config, children: render }),
364
+ /* @__PURE__ */ jsx(LiveCopies, { trialId, layout: "fullscreen", setup, trialConfig: config, children: render })
365
+ ] });
366
+ }
367
+ function CaptureInputs({ entry, layout, config, state }) {
368
+ const capture = useContext(LoupeCaptureContext);
369
+ const trialId = useContext(TrialIdContext);
370
+ const [overrides] = useCssOverrides();
371
+ const withdraw = useRef(null);
372
+ useEffect(() => {
373
+ if (capture && trialId) withdraw.current = capture.trials.publish(trialId, { entry, layout, config, state, overrides });
374
+ }, [capture, trialId, entry, layout, config, state, overrides]);
375
+ useEffect(
376
+ () => () => {
377
+ withdraw.current?.();
378
+ withdraw.current = null;
379
+ },
380
+ [capture, trialId]
381
+ );
382
+ return null;
383
+ }
384
+ var STILL = { get: () => 0, subscribe: () => () => {
385
+ } };
386
+ function LoupeProvider({ captureBase, revision = STILL, children }) {
387
+ const [lenses] = useState(createLiveLenses);
388
+ const [trials] = useState(createCaptureTrials);
389
+ const capture = useMemo(
390
+ () => captureBase === void 0 ? null : { base: captureBase, trials, revision },
391
+ [captureBase, trials, revision]
392
+ );
393
+ return /* @__PURE__ */ jsx(LiveLensesContext.Provider, { value: lenses, children: /* @__PURE__ */ jsx(LoupeCaptureContext.Provider, { value: capture, children }) });
286
394
  }
287
395
  function withPlayhead(schema) {
288
396
  return f.schema({ ...schema.nodes, [PLAYHEAD_KEY]: f.custom("forge-playhead", null).hidden() });
@@ -299,7 +407,10 @@ function documentInstrument(options) {
299
407
  config,
300
408
  defaultConfig: () => config.defaults(),
301
409
  initialState: (c) => initialState?.(c) ?? null,
302
- 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 }),
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 }),
303
414
  ...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {},
304
415
  ...timed ? { chrome: [{ id: "fg-transport", region: "status", render: (c) => /* @__PURE__ */ jsx(TrialTransport, { trialId: c.trialId }) }] } : {}
305
416
  };
@@ -339,6 +450,50 @@ function missingInstrument(id, globals = {}) {
339
450
  render: () => /* @__PURE__ */ jsx("div", { className: "fg-story fg-story--loading", "data-fg-layout": "fullscreen", children: /* @__PURE__ */ jsx(Fault, { phase: "route", message: `No story ${id}` }) })
340
451
  };
341
452
  }
453
+ var listeners = /* @__PURE__ */ new Set();
454
+ var pending = null;
455
+ function revealVar(name) {
456
+ pending = name;
457
+ for (const fn of [...listeners]) fn(name);
458
+ }
459
+ function subscribeReveal(fn) {
460
+ listeners.add(fn);
461
+ if (pending !== null) fn(pending);
462
+ return () => {
463
+ listeners.delete(fn);
464
+ };
465
+ }
466
+ var MARK = "data-revealed";
467
+ function useRevealToken(list, tokens, show, storyReported) {
468
+ const asked = useRef(null);
469
+ const [, rerender] = useState(0);
470
+ useEffect(
471
+ () => subscribeReveal((name) => {
472
+ const tab = ["theme", "story"].find((t) => tokens[t].some((token2) => token2.name === name));
473
+ const token = tab && tokens[tab].find((t) => t.name === name);
474
+ if (!tab || !token) {
475
+ if (storyReported && pending === name) pending = null;
476
+ return;
477
+ }
478
+ show(name, tab, tokenCategory(token));
479
+ asked.current = name;
480
+ rerender((n) => n + 1);
481
+ }),
482
+ [tokens, show, storyReported]
483
+ );
484
+ useLayoutEffect(() => {
485
+ const root = list.current;
486
+ const revealed = asked.current;
487
+ if (!root || revealed === null) return;
488
+ const target = root.querySelector(`[aria-label="${revealed.replace(/["\\]/g, "\\$&")}"]`);
489
+ for (const el of Array.from(root.querySelectorAll(`[${MARK}]`))) if (el !== target) el.removeAttribute(MARK);
490
+ if (!target) return;
491
+ target.setAttribute(MARK, "");
492
+ target.scrollIntoView?.({ block: "nearest" });
493
+ if (pending === revealed) pending = null;
494
+ asked.current = null;
495
+ });
496
+ }
342
497
  var ICONS = {
343
498
  color: (
344
499
  /* @icon */
@@ -485,8 +640,8 @@ function applyScaleEdits(definition, edits, selection) {
485
640
  const write = (slot, value) => {
486
641
  let byKey = writes.get(slot.holder);
487
642
  if (!byKey) writes.set(slot.holder, byKey = /* @__PURE__ */ new Map());
488
- const pending = byKey.get(slot.key);
489
- if (pending !== void 0 && pending !== value) throw new EditError(`${slot.where} would need to be both ${pending} and ${value}`);
643
+ const pending2 = byKey.get(slot.key);
644
+ if (pending2 !== void 0 && pending2 !== value) throw new EditError(`${slot.where} would need to be both ${pending2} and ${value}`);
490
645
  byKey.set(slot.key, value);
491
646
  };
492
647
  try {
@@ -786,6 +941,15 @@ function standingScale(source, amounts) {
786
941
  const base = (amounts[at] ?? 0) / (rule.factors[at] ?? 1);
787
942
  return { ...seed, base: Math.round(base * 100) / 100, rule };
788
943
  }
944
+ function unsavedScales(edited, overrides) {
945
+ return SCALE_SOURCES.flatMap((source) => {
946
+ const edit = edited[source.group];
947
+ if (!edit || !source.tokens.some((name2) => overrides[name2] !== void 0)) return [];
948
+ const { name, group, tokens } = source;
949
+ const clean = scaleValues(source, edit).every(([token, value]) => overrides[token] === value);
950
+ return clean ? [{ name, group, tokens, edit: { base: edit.base, rule: edit.rule } }] : [];
951
+ });
952
+ }
789
953
  var NO_GLOBALS = {};
790
954
  function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
791
955
  const trialId = useTrialId();
@@ -802,6 +966,14 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
802
966
  const themeTokens = useThemeTokens(globals, frame.vars);
803
967
  const story = useStoryTokens(frame.vars);
804
968
  const tokens = tab === "theme" ? themeTokens : story.tokens;
969
+ const listRef = useRef(null);
970
+ const byTab = useMemo(() => ({ theme: themeTokens, story: story.tokens }), [themeTokens, story.tokens]);
971
+ const show = useCallback((name, at, category) => {
972
+ setTab(at);
973
+ setFilter((was) => name.toLowerCase().includes(was.trim().toLowerCase()) ? was : "");
974
+ setFolds((prev) => prev[category] ? { ...prev, [category]: false } : prev);
975
+ }, [setFolds]);
976
+ useRevealToken(listRef, byTab, show, frame.vars.length > 0);
805
977
  const query = filter.trim().toLowerCase();
806
978
  const shown = tokens.map((token) => {
807
979
  const override = overrides[token.name];
@@ -842,17 +1014,8 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
842
1014
  `scale:${group}`
843
1015
  );
844
1016
  };
845
- const unsaved = useMemo(
846
- () => SCALE_SOURCES.flatMap((source) => {
847
- const edit = edited[source.group];
848
- if (!edit || !source.tokens.some((name2) => overrides[name2] !== void 0)) return [];
849
- const { name, group, tokens: tokens2 } = source;
850
- const clean = scaleValues(source, edit).every(([token, value]) => overrides[token] === value);
851
- return clean ? [{ name, group, tokens: tokens2, edit: { base: edit.base, rule: edit.rule } }] : [];
852
- }),
853
- [edited, overrides]
854
- );
855
- const { pending, saving, notice, save } = useThemeSave({ trialId, globals, themeApi, scales: unsaved, overrides, history: history2 });
1017
+ const unsaved = useMemo(() => unsavedScales(edited, overrides), [edited, overrides]);
1018
+ const { pending: pending2, saving, notice, save } = useThemeSave({ trialId, globals, themeApi, scales: unsaved, overrides, history: history2 });
856
1019
  const onCollapsedChange = useCallback(
857
1020
  (category, collapsed) => setFolds((prev) => ({ ...prev, [category]: collapsed })),
858
1021
  [setFolds]
@@ -911,9 +1074,9 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
911
1074
  ] })
912
1075
  ] }),
913
1076
  tab === "story" ? /* @__PURE__ */ jsx(Checkbox, { className: "fg-css-vars__unused", isSelected: story.showUnused, onChange: story.setShowUnused, children: "Show variables the story doesn\u2019t use" }) : null,
914
- tab === "theme" ? /* @__PURE__ */ jsx(SaveRow, { pending, saving, notice, onSave: () => void save() }) : null
1077
+ tab === "theme" ? /* @__PURE__ */ jsx(SaveRow, { pending: pending2, saving, notice, onSave: () => void save() }) : null
915
1078
  ] }),
916
- shown.length === 0 ? /* @__PURE__ */ jsx("p", { className: "fg-css-vars__empty", children: tokens.length > 0 ? "No variables match." : tab === "story" ? story.emptyText : "The story\u2019s frame has reported no variables." }) : /* @__PURE__ */ jsx("div", { className: "fg-css-vars__list", children: /* @__PURE__ */ jsx(
1079
+ shown.length === 0 ? /* @__PURE__ */ jsx("p", { className: "fg-css-vars__empty", children: tokens.length > 0 ? "No variables match." : tab === "story" ? story.emptyText : "The story\u2019s frame has reported no variables." }) : /* @__PURE__ */ jsx("div", { ref: listRef, className: "fg-css-vars__list", children: /* @__PURE__ */ jsx(
917
1080
  TokenPanel,
918
1081
  {
919
1082
  tokens: shown,
@@ -930,10 +1093,10 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
930
1093
  )
931
1094
  );
932
1095
  }
933
- function SaveRow({ pending, saving, notice, onSave }) {
934
- if (!pending && !notice) return null;
1096
+ function SaveRow({ pending: pending2, saving, notice, onSave }) {
1097
+ if (!pending2 && !notice) return null;
935
1098
  return /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__save", children: [
936
- pending ? /* @__PURE__ */ jsx(Button$1, { variant: "primary", size: "sm", loading: saving, disabled: saving, onClick: onSave, children: "Save to theme" }) : null,
1099
+ pending2 ? /* @__PURE__ */ jsx(Button$1, { variant: "primary", size: "sm", loading: saving, disabled: saving, onClick: onSave, children: "Save to theme" }) : null,
937
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
938
1101
  ] });
939
1102
  }
@@ -954,6 +1117,147 @@ var CSS_VARS_SECTION = {
954
1117
  region: "aside",
955
1118
  item: { title: "CSS Vars", actions: /* @__PURE__ */ jsx(SectionJump, {}), fill: true, body: /* @__PURE__ */ jsx(FocusedCssVars, {}) }
956
1119
  };
1120
+
1121
+ // src/shell/cssVars/cssVarsChrome.ts
1122
+ var RECORD = "fg-css-vars-shown";
1123
+ function cssVarsChrome(shown, toggle) {
1124
+ return [
1125
+ {
1126
+ id: "fg-css-vars-toggle",
1127
+ region: "header",
1128
+ group: "story",
1129
+ item: { icon: (
1130
+ /* @icon */
1131
+ "ui:typeBraces"
1132
+ ), label: "CSS Vars panel", pressed: shown, onActivate: toggle }
1133
+ },
1134
+ ...shown ? [CSS_VARS_SECTION] : []
1135
+ ];
1136
+ }
1137
+ function CssVarsChrome({ onChrome }) {
1138
+ const [shown, setShown] = usePersistedState(RECORD, false, { scope: "lab" });
1139
+ const chrome = useMemo(() => cssVarsChrome(shown, () => setShown((was) => !was)), [shown, setShown]);
1140
+ useEffect(() => onChrome(chrome), [chrome, onChrome]);
1141
+ useEffect(() => subscribeReveal(() => setShown(true)), [setShown]);
1142
+ return null;
1143
+ }
1144
+ function describeBinding(spec) {
1145
+ const chip = keySpecShortcut(spec);
1146
+ if (chip) return spec.kind === "key-held" ? `Hold ${formatShortcut(chip)}` : formatShortcut(chip) ?? "";
1147
+ switch (spec.kind) {
1148
+ case "wheel":
1149
+ return "Wheel";
1150
+ case "hover":
1151
+ return "Hover";
1152
+ case "drag":
1153
+ return "Drag";
1154
+ case "click":
1155
+ return "Click";
1156
+ default:
1157
+ return spec.kind[0].toUpperCase() + spec.kind.slice(1);
1158
+ }
1159
+ }
1160
+ function keymapDefs(entries) {
1161
+ const defs = [];
1162
+ for (const entry of entries.values()) {
1163
+ if (!entry.eligibility.keymap) continue;
1164
+ const open = entries.get(`${entry.id}.leader`)?.actions?.find((a) => a.id === keymapLeaderId(entry.id));
1165
+ const leader = open ? actionBindings(open).map((b) => b.spec) : [];
1166
+ if (leader.length === 0) continue;
1167
+ defs.push({ id: entry.id, label: entry.presentation?.label ?? entry.id, leader, bindings: entry.bindings ?? [] });
1168
+ }
1169
+ return defs;
1170
+ }
1171
+ function keyRows(actions, defs, overrides) {
1172
+ const inKeymap = /* @__PURE__ */ new Map();
1173
+ for (const def of defs) {
1174
+ for (const [id, chips] of keymapShortcuts(def, overrides)) {
1175
+ inKeymap.set(id, [...inKeymap.get(id) ?? [], ...chips.flatMap((c) => formatShortcut(c) ?? [])]);
1176
+ }
1177
+ }
1178
+ const rows = [];
1179
+ for (const action of actions) {
1180
+ const keys = /* @__PURE__ */ new Set();
1181
+ const others = /* @__PURE__ */ new Set();
1182
+ for (const { spec } of actionBindings(action)) {
1183
+ (keySpecShortcut(spec) ? keys : others).add(describeBinding(spec));
1184
+ }
1185
+ for (const chip of inKeymap.get(action.id) ?? []) keys.add(chip);
1186
+ if (keys.size + others.size === 0) continue;
1187
+ rows.push({ id: action.id, label: action.label, group: action.group ?? "other", keys: [...keys], others: [...others] });
1188
+ }
1189
+ return rows.sort((a, b) => a.group.localeCompare(b.group) || a.label.localeCompare(b.label));
1190
+ }
1191
+ function useKeyRows() {
1192
+ const registry = useActionsRegistry();
1193
+ const entries = useLabEntries();
1194
+ const [version, bump] = useReducer((n) => n + 1, 0);
1195
+ useEffect(() => registry?.subscribe(bump), [registry]);
1196
+ return useMemo(
1197
+ () => registry ? keyRows(registry.list(), keymapDefs(entries), registry.bindingOverrides()) : [],
1198
+ // `version` is the registry's change signal: `list()` has no identity to key on.
1199
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1200
+ [registry, entries, version]
1201
+ );
1202
+ }
1203
+ function KeysPanel() {
1204
+ const rows = useKeyRows();
1205
+ if (rows.length === 0) return /* @__PURE__ */ jsx("p", { className: "fg-keys__empty", children: "No key bindings." });
1206
+ const groups = /* @__PURE__ */ new Map();
1207
+ for (const row of rows) groups.set(row.group, [...groups.get(row.group) ?? [], row]);
1208
+ return /* @__PURE__ */ jsx("div", { className: "fg-keys", children: [...groups].map(([group, members]) => /* @__PURE__ */ jsxs("section", { className: "fg-keys__group", "aria-label": group, children: [
1209
+ /* @__PURE__ */ jsx("h3", { className: "fg-keys__heading", children: group }),
1210
+ /* @__PURE__ */ jsx("ul", { className: "fg-keys__list", children: members.map((row) => /* @__PURE__ */ jsxs("li", { className: "fg-keys__row", children: [
1211
+ /* @__PURE__ */ jsx("span", { className: "fg-keys__label", children: row.label }),
1212
+ /* @__PURE__ */ jsxs("span", { className: "fg-keys__chips", children: [
1213
+ row.keys.map((chip) => /* @__PURE__ */ jsx("kbd", { className: "fg-keys__chip", children: chip }, chip)),
1214
+ row.others.map((chip) => /* @__PURE__ */ jsx("kbd", { className: "fg-keys__chip fg-keys__chip--gesture", children: chip }, chip))
1215
+ ] })
1216
+ ] }, row.id)) })
1217
+ ] }, group)) });
1218
+ }
1219
+ var KEYS_SECTION = {
1220
+ id: "fg-keys",
1221
+ region: "aside",
1222
+ item: { title: "Keys", body: /* @__PURE__ */ jsx(KeysPanel, {}) }
1223
+ };
1224
+
1225
+ // src/shell/keys/keysChrome.ts
1226
+ var RECORD2 = "fg-keys-shown";
1227
+ var KEYS_TOGGLE_ID = "forge.keys.toggle";
1228
+ var OPEN_KEY = { kind: "key", key: "/", code: "Slash", mods: { alt: true } };
1229
+ function keysChrome(shown, toggle) {
1230
+ return [
1231
+ {
1232
+ id: "fg-keys-toggle",
1233
+ region: "header",
1234
+ group: "story",
1235
+ item: { icon: (
1236
+ /* @icon */
1237
+ "ui:keyboard"
1238
+ ), label: "Keys panel", pressed: shown, onActivate: toggle }
1239
+ },
1240
+ ...shown ? [KEYS_SECTION] : []
1241
+ ];
1242
+ }
1243
+ function KeysChrome({ onChrome }) {
1244
+ const [shown, setShown] = usePersistedState(RECORD2, false, { scope: "lab" });
1245
+ const registry = useActionsRegistry();
1246
+ const chrome = useMemo(() => keysChrome(shown, () => setShown((was) => !was)), [shown, setShown]);
1247
+ useEffect(() => onChrome(chrome), [chrome, onChrome]);
1248
+ useEffect(() => {
1249
+ if (!registry) return;
1250
+ const action = {
1251
+ id: KEYS_TOGGLE_ID,
1252
+ label: "Toggle the keys panel",
1253
+ group: "forge",
1254
+ defaultBinding: [{ spec: OPEN_KEY, opts: { id: KEYS_TOGGLE_ID } }],
1255
+ invoker: { timing: "immediate", run: () => setShown((was) => !was) }
1256
+ };
1257
+ return registry.register(action);
1258
+ }, [registry, setShown]);
1259
+ return null;
1260
+ }
957
1261
  var KEYS = { kind: "key", key: "i", mods: { mod: true } };
958
1262
  function infoItem(open) {
959
1263
  return {
@@ -1155,6 +1459,1010 @@ function createFramePool(frameUrl, size = 2) {
1155
1459
  };
1156
1460
  }
1157
1461
  var FramePoolContext = createContext(null);
1462
+ var COMPARE_WITH_ID = "loupe.compare.with";
1463
+ var COMPARE_DPR_ID = "loupe.compare.dpr";
1464
+ var PIXELS_ONLY = "Pixels only";
1465
+ var COMPARE_MENU_ID = "loupe.compare";
1466
+ var DPRS = [1, 2, 3];
1467
+ function argChoices(shape, at) {
1468
+ return Object.entries(shape).flatMap(([key, entry]) => {
1469
+ const path = at === "" ? key : `${at}.${key}`;
1470
+ if (isForgePath(path)) return [];
1471
+ if (isConfigBranch(entry)) return argChoices(entry.children, path);
1472
+ const options = entry.kind === "boolean" ? [
1473
+ { value: true, label: "On" },
1474
+ { value: false, label: "Off" }
1475
+ ] : entry.annotations.options ?? [];
1476
+ return options.map((o) => ({
1477
+ action: COMPARE_WITH_ID,
1478
+ key: `arg.${path}.${String(o.value)}`,
1479
+ group: `Arg ${path}`,
1480
+ label: o.label,
1481
+ compare: { kind: "custom", args: { [path]: o.value } }
1482
+ }));
1483
+ });
1484
+ }
1485
+ function compareChoices(declarations, schema) {
1486
+ const globals = Object.entries(declarations).flatMap(
1487
+ ([key, declaration]) => declaration.options.map((o) => ({
1488
+ action: COMPARE_WITH_ID,
1489
+ key: `global.${key}.${o.value}`,
1490
+ group: declaration.label,
1491
+ label: o.label,
1492
+ compare: { kind: "custom", globals: { [key]: o.value } }
1493
+ }))
1494
+ );
1495
+ const dprs = DPRS.map((dpr) => ({
1496
+ action: COMPARE_DPR_ID,
1497
+ key: `dpr.${dpr}`,
1498
+ group: "DPR",
1499
+ label: `${dpr}\xD7`,
1500
+ compare: { kind: "custom", dpr }
1501
+ }));
1502
+ return [...globals, ...dprs, ...schema ? argChoices(schema.nodes, "") : []];
1503
+ }
1504
+ function compareMenuDef(choices) {
1505
+ const groups = /* @__PURE__ */ new Map();
1506
+ for (const choice of choices) {
1507
+ const rows = groups.get(choice.group) ?? {};
1508
+ rows[`c${Object.keys(rows).length}`] = pref.action(`${choice.action}:${choice.key}`);
1509
+ groups.set(choice.group, rows);
1510
+ }
1511
+ const submenus = {};
1512
+ [...groups].forEach(([label, rows], n) => {
1513
+ submenus[`g${n}`] = pref.group(label, rows, { as: "menu" });
1514
+ });
1515
+ return { id: COMPARE_MENU_ID, menu: pref.group("", submenus) };
1516
+ }
1517
+ function compareActions(api, request) {
1518
+ const one = (id, label, enabled) => ({
1519
+ id,
1520
+ label,
1521
+ group: "loupe",
1522
+ enabled,
1523
+ variants: request.choices.filter((c) => c.action === id).map((c) => ({ key: c.key, label: `${c.group}: ${c.label}`, params: { compare: c.compare, lensId: request.lensId } })),
1524
+ invoker: {
1525
+ timing: "immediate",
1526
+ run(_deps, params) {
1527
+ const lensId = params?.lensId ?? api.current();
1528
+ const compare = params?.compare;
1529
+ if (lensId !== null && compare) api.apply(setLensOps(api.scene, lensId, { compare }));
1530
+ }
1531
+ }
1532
+ });
1533
+ return [
1534
+ one(COMPARE_WITH_ID, "Compare with", () => true),
1535
+ one(COMPARE_DPR_ID, "Compare at DPR", () => request.pixels ? true : PIXELS_ONLY)
1536
+ ];
1537
+ }
1538
+ var NO_CHOICES = [];
1539
+ function CompareMenu({ api, request }) {
1540
+ const registry = useActionsRegistry();
1541
+ const host = useMenuHost();
1542
+ const choices = request?.choices ?? NO_CHOICES;
1543
+ const defs = useMemo(() => [compareMenuDef(choices)], [choices]);
1544
+ useMenus(defs);
1545
+ useEffect(() => {
1546
+ if (!registry || !host || !request) return;
1547
+ const offs = compareActions(api, request).map((a) => registry.register(a));
1548
+ let held = true;
1549
+ const drop = () => {
1550
+ if (!held) return;
1551
+ held = false;
1552
+ for (const off of offs) off();
1553
+ };
1554
+ const opened = host.open({ menu: COMPARE_MENU_ID, client: request.client, source: "keyboard" }) ? host.current()?.seq : void 0;
1555
+ if (opened === void 0) {
1556
+ drop();
1557
+ return;
1558
+ }
1559
+ const unsubscribe = host.subscribe(() => {
1560
+ if (host.current()?.seq !== opened) queueMicrotask(drop);
1561
+ });
1562
+ return () => {
1563
+ unsubscribe();
1564
+ drop();
1565
+ };
1566
+ }, [registry, host, api, request]);
1567
+ return null;
1568
+ }
1569
+
1570
+ // src/shell/loupe/liveSource.ts
1571
+ var NOTHING_UNDER = "Nothing under the reticle";
1572
+ var sameContext = (a, b) => a.declarations === b.declarations && a.modeGlobal === b.modeGlobal && a.prefersDark === b.prefersDark && a.labMode === b.labMode;
1573
+ var liveCellId = (lensId, compare) => compare === null ? lensId : `${lensId}~compared`;
1574
+ function sameRequest(a, b) {
1575
+ 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
+ }
1577
+ function createLiveSource(lenses, resolve, conditions) {
1578
+ const measures = /* @__PURE__ */ new Set();
1579
+ const listeners2 = /* @__PURE__ */ new Set();
1580
+ return {
1581
+ kind: "live",
1582
+ produces: "dom",
1583
+ label: () => "Live",
1584
+ 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.
1586
+ subscribe(listener) {
1587
+ listeners2.add(listener);
1588
+ return () => {
1589
+ listeners2.delete(listener);
1590
+ };
1591
+ },
1592
+ attach(slot, first) {
1593
+ const doc = slot.ownerDocument;
1594
+ const target = doc.createElement("div");
1595
+ target.className = "fg-lens-live";
1596
+ const empty = doc.createElement("div");
1597
+ empty.className = "fg-lens-empty";
1598
+ empty.hidden = true;
1599
+ 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 });
1604
+ let request = first;
1605
+ let posted = null;
1606
+ const post = (next) => {
1607
+ if (next === null ? posted === null : sameRequest(posted, next)) return;
1608
+ posted = next;
1609
+ lenses.set(liveCellId(request.lensId, request.conditions), next);
1610
+ };
1611
+ const measure = () => {
1612
+ const { lensId, reticle, conditions: compare } = request;
1613
+ const hit = trialAt(reticle, doc);
1614
+ empty.hidden = hit !== null;
1615
+ if (!hit) {
1616
+ post(null);
1617
+ empty.textContent = NOTHING_UNDER;
1618
+ return;
1619
+ }
1620
+ const { base, ctx } = conditions();
1621
+ const resolved = resolve(compare, base, ctx);
1622
+ post({
1623
+ lensId: liveCellId(lensId, compare),
1624
+ slot: target,
1625
+ hit,
1626
+ factor: request.factor,
1627
+ size: request.size,
1628
+ compare,
1629
+ ctx,
1630
+ args: resolved.args
1631
+ });
1632
+ };
1633
+ measures.add(measure);
1634
+ measure();
1635
+ return {
1636
+ update(next) {
1637
+ request = next;
1638
+ measure();
1639
+ },
1640
+ detach() {
1641
+ watch?.disconnect();
1642
+ measures.delete(measure);
1643
+ lenses.set(liveCellId(request.lensId, request.conditions), null);
1644
+ target.remove();
1645
+ empty.remove();
1646
+ }
1647
+ };
1648
+ },
1649
+ remeasure() {
1650
+ for (const measure of [...measures]) measure();
1651
+ }
1652
+ };
1653
+ }
1654
+ var INLINE = "style attribute";
1655
+ function topmostIn(within, client) {
1656
+ return probeUnderLens(
1657
+ within,
1658
+ "interior",
1659
+ () => (within.ownerDocument.elementsFromPoint?.(client.x, client.y) ?? []).find((el) => within.contains(el))
1660
+ );
1661
+ }
1662
+ function backgroundOf(el, stop) {
1663
+ for (let at = el; at; at = at === stop ? null : at.parentElement) {
1664
+ const color = getComputedStyle(at).backgroundColor;
1665
+ if (color.trim() === "") continue;
1666
+ try {
1667
+ if (parseColor(color)[3] === 1) return { element: at, color };
1668
+ } catch {
1669
+ return { unreadable: color };
1670
+ }
1671
+ }
1672
+ return null;
1673
+ }
1674
+ var inside = (r, p) => p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom;
1675
+ function textUnder(el, client) {
1676
+ const range = el.ownerDocument.createRange();
1677
+ return Array.from(el.childNodes).some((n) => {
1678
+ if (n.nodeType !== Node.TEXT_NODE || (n.textContent ?? "").trim() === "") return false;
1679
+ range.selectNodeContents(n);
1680
+ return Array.from(range.getClientRects()).some((r) => inside(r, client));
1681
+ });
1682
+ }
1683
+ function colorAt(client, within) {
1684
+ const el = topmostIn(within, client);
1685
+ if (!el) return null;
1686
+ if (textUnder(el, client)) return { element: el, property: "color", value: getComputedStyle(el).color };
1687
+ const bg = backgroundOf(el, within);
1688
+ if (bg === null) return null;
1689
+ if ("unreadable" in bg) return { element: el, property: "background-color", value: bg.unreadable };
1690
+ return { element: bg.element, property: "background-color", value: bg.color };
1691
+ }
1692
+ var STYLE_RULE = 1;
1693
+ var SHORTHANDS = { "background-color": ["background"] };
1694
+ var INHERITED = (property) => property.startsWith("--") || property === "color";
1695
+ function applies(rule, doc) {
1696
+ const media = rule.media;
1697
+ if (media && media.mediaText && doc.defaultView && !doc.defaultView.matchMedia(media.mediaText).matches) return false;
1698
+ if (typeof CSSSupportsRule !== "undefined" && rule instanceof CSSSupportsRule) return CSS.supports(rule.conditionText);
1699
+ return true;
1700
+ }
1701
+ function nest(selector, parent) {
1702
+ if (parent === void 0) return selector;
1703
+ return splitTopLevel(selector, ",").map((s) => s.includes("&") ? s.replaceAll("&", `:is(${parent})`) : `:is(${parent}) ${s.trim()}`).join(", ");
1704
+ }
1705
+ function collect(rules, doc, into, parent) {
1706
+ for (const rule of Array.from(rules)) {
1707
+ if (!applies(rule, doc)) continue;
1708
+ const imported = rule.styleSheet;
1709
+ if (imported) {
1710
+ const inner = cssRulesOf(imported);
1711
+ if (inner) collect(inner, doc, into);
1712
+ continue;
1713
+ }
1714
+ const children = rule.cssRules;
1715
+ if (rule.type !== STYLE_RULE) {
1716
+ if (children) collect(children, doc, into, parent);
1717
+ continue;
1718
+ }
1719
+ const style = rule;
1720
+ const selector = nest(style.selectorText, parent);
1721
+ into.push({ selector, style: style.style });
1722
+ if (children) collect(children, doc, into, selector);
1723
+ }
1724
+ }
1725
+ function rulesOf(doc) {
1726
+ const out = [];
1727
+ for (const sheet of [...Array.from(doc.styleSheets), ...doc.adoptedStyleSheets ?? []]) {
1728
+ if (sheet.disabled) continue;
1729
+ const rules = cssRulesOf(sheet);
1730
+ if (rules) collect(rules, doc, out);
1731
+ }
1732
+ return out;
1733
+ }
1734
+ function matching(el, list) {
1735
+ let best = null;
1736
+ for (const one of splitTopLevel(list, ",")) {
1737
+ let hit = false;
1738
+ try {
1739
+ hit = el.matches(one);
1740
+ } catch {
1741
+ }
1742
+ const s = hit ? specificity(one.trim()) : null;
1743
+ if (s && (best === null || compareSpecificity(s, best) > 0)) best = s;
1744
+ }
1745
+ return best;
1746
+ }
1747
+ function declaredIn(style, property) {
1748
+ for (const name of [property, ...SHORTHANDS[property] ?? []]) {
1749
+ const value = style.getPropertyValue(name).trim();
1750
+ if (value) return { value, important: style.getPropertyPriority(name) === "important" };
1751
+ }
1752
+ return null;
1753
+ }
1754
+ function indexOf(rules) {
1755
+ const index = /* @__PURE__ */ new Map();
1756
+ return (property) => {
1757
+ let got = index.get(property);
1758
+ if (!got) {
1759
+ got = rules.flatMap((r) => {
1760
+ const d = declaredIn(r.style, property);
1761
+ return d ? [{ selector: r.selector, ...d }] : [];
1762
+ });
1763
+ index.set(property, got);
1764
+ }
1765
+ return got;
1766
+ };
1767
+ }
1768
+ function winning(el, property, rules) {
1769
+ let best = null;
1770
+ for (const got of rules) {
1771
+ const spec = matching(el, got.selector);
1772
+ if (!spec) continue;
1773
+ const wins = best === null || got.important && !best.important || got.important === best.important && compareSpecificity(spec, best.spec) >= 0;
1774
+ if (wins) best = { ...got, spec };
1775
+ }
1776
+ const inline = el.style;
1777
+ const own = inline ? declaredIn(inline, property) : null;
1778
+ if (own && (best === null || own.important || !best.important)) return { property, declared: own.value, selector: INLINE, element: el };
1779
+ return best ? { property, declared: best.value, selector: best.selector, element: el } : null;
1780
+ }
1781
+ function declarationOf(el, property, rules) {
1782
+ for (let at = el; at; at = INHERITED(property) ? at.parentElement : null) {
1783
+ const link = winning(at, property, rules);
1784
+ if (link) return link;
1785
+ }
1786
+ return null;
1787
+ }
1788
+ var revisions = /* @__PURE__ */ new WeakMap();
1789
+ function revisionOf(doc) {
1790
+ let r = revisions.get(doc);
1791
+ if (!r) {
1792
+ const made = { n: 0, observer: null };
1793
+ if (typeof MutationObserver !== "undefined" && doc.head) {
1794
+ made.observer = new MutationObserver(() => {
1795
+ made.n += 1;
1796
+ });
1797
+ made.observer.observe(doc.head, { childList: true, subtree: true, characterData: true, attributes: true });
1798
+ }
1799
+ revisions.set(doc, made);
1800
+ r = made;
1801
+ }
1802
+ if ((r.observer?.takeRecords().length ?? 0) > 0) r.n += 1;
1803
+ return r.n;
1804
+ }
1805
+ var chains = /* @__PURE__ */ new WeakMap();
1806
+ function tokenChain(at) {
1807
+ const doc = at.element.ownerDocument;
1808
+ const key = `${at.value}|${revisionOf(doc)}|${doc.styleSheets.length}|${doc.adoptedStyleSheets?.length ?? 0}`;
1809
+ let byProperty = chains.get(at.element);
1810
+ const held = byProperty?.get(at.property);
1811
+ if (held?.key === key) return held.chain;
1812
+ const chain = chainOf(at);
1813
+ if (!byProperty) {
1814
+ byProperty = /* @__PURE__ */ new Map();
1815
+ chains.set(at.element, byProperty);
1816
+ }
1817
+ byProperty.set(at.property, { key, chain });
1818
+ return chain;
1819
+ }
1820
+ function chainOf(at) {
1821
+ const declaring = indexOf(rulesOf(at.element.ownerDocument));
1822
+ const chain = [];
1823
+ const seen = /* @__PURE__ */ new Set();
1824
+ const follow = (el, property) => {
1825
+ if (seen.has(property)) return;
1826
+ seen.add(property);
1827
+ const link = declarationOf(el, property, declaring(property));
1828
+ if (!link) return;
1829
+ chain.push(link);
1830
+ for (const name of varRefs(link.declared)) follow(link.element, name);
1831
+ };
1832
+ follow(at.element, at.property);
1833
+ return chain;
1834
+ }
1835
+ var NO_TEXT = "No text under the aim";
1836
+ var NO_BACKGROUND = "No opaque background behind the text";
1837
+ var unreadable = (color) => `Cannot read the color "${color}"`;
1838
+ var NO_CAPTURE = "No capture to read";
1839
+ var OUTSIDE = "The aim is outside the capture";
1840
+ var NO_COLOR = "No color under the aim";
1841
+ var NEEDS_LIVE = "Token needs a Live source for changed conditions";
1842
+ var IN_FRAME = "Cannot read inside an isolated story's frame";
1843
+ var REACH = 2;
1844
+ function isLarge(style) {
1845
+ const px = Number.parseFloat(style.fontSize);
1846
+ const w = style.fontWeight === "bold" ? 700 : Number.parseInt(style.fontWeight, 10);
1847
+ return px >= 24 || px >= 18.66 && w >= 700;
1848
+ }
1849
+ var rgb = (d, i) => `rgb(${d[i]}, ${d[i + 1]}, ${d[i + 2]})`;
1850
+ function ofPixels(capture, point) {
1851
+ const img = capture ? readBitmap(capture.bitmap) : null;
1852
+ if (!capture || !img) return { unavailable: NO_CAPTURE };
1853
+ const px = Math.floor(point.x * capture.dpr);
1854
+ const py = Math.floor(point.y * capture.dpr);
1855
+ if (px < 0 || py < 0 || px >= img.width || py >= img.height) return { unavailable: OUTSIDE };
1856
+ const { data, width, height } = img;
1857
+ const center = (py * width + px) * 4;
1858
+ let far = center;
1859
+ let best = 0;
1860
+ for (let y = Math.max(0, py - REACH); y <= Math.min(height - 1, py + REACH); y += 1) {
1861
+ for (let x = Math.max(0, px - REACH); x <= Math.min(width - 1, px + REACH); x += 1) {
1862
+ const i = (y * width + x) * 4;
1863
+ const d = [0, 1, 2].reduce((sum, c) => sum + (data[i + c] - data[center + c]) ** 2, 0);
1864
+ if (d > best) {
1865
+ best = d;
1866
+ far = i;
1867
+ }
1868
+ }
1869
+ }
1870
+ const ratio = contrastRatio(rgb(data, center), rgb(data, far));
1871
+ return ratio === null ? { unavailable: NO_CAPTURE } : { ratio, ...wcagLevels(ratio, false), large: false };
1872
+ }
1873
+ function contrastAt(source, at) {
1874
+ if (source === "pixels") return ofPixels(at.capture, at.point);
1875
+ const text = at.within ? colorAt(at.point, at.within) : null;
1876
+ if (!text || text.property !== "color") return { unavailable: NO_TEXT };
1877
+ const style = getComputedStyle(text.element);
1878
+ const background = backgroundOf(text.element, at.within);
1879
+ if (background === null) return { unavailable: NO_BACKGROUND };
1880
+ if ("unreadable" in background) return { unavailable: unreadable(background.unreadable) };
1881
+ const ratio = contrastRatio(style.color, background.color);
1882
+ if (ratio === null) return { unavailable: unreadable(style.color) };
1883
+ const large = isLarge(style);
1884
+ return { ratio, ...wcagLevels(ratio, large), large };
1885
+ }
1886
+ function copyIn(interior) {
1887
+ const copies = Array.from(interior?.querySelectorAll("[data-fg-copy]") ?? []);
1888
+ return copies.find((c) => (c.getAttribute("data-fg-host") ?? "").endsWith("~compared")) ?? copies[0];
1889
+ }
1890
+ function comparedAim(interior) {
1891
+ const box = interior?.getBoundingClientRect();
1892
+ return box ? { x: box.left + box.width / 2, y: box.top + box.height / 2 } : null;
1893
+ }
1894
+ function liveAim(interior, settings, reticle) {
1895
+ const aim2 = settings.compare === null ? { within: trialAt(reticle)?.host, point: reticle } : { within: copyIn(interior), point: comparedAim(interior) ?? reticle };
1896
+ return aim2.within?.classList.contains("fg-frame-host") ? { unavailable: IN_FRAME } : aim2;
1897
+ }
1898
+ var verdict = (pass) => pass ? "pass" : "fail";
1899
+ function textOf(reading) {
1900
+ if ("unavailable" in reading) return reading.unavailable;
1901
+ const base = `${reading.ratio.toFixed(2)}:1 \xB7 AA ${verdict(reading.aa)} \xB7 AAA ${verdict(reading.aaa)}`;
1902
+ return reading.large ? `${base} \xB7 large text` : base;
1903
+ }
1904
+ function ContrastReadout({ lens, settings, sources, reticle, size, interior }) {
1905
+ const { source } = useShownSource(settings, sources);
1906
+ const kind = source?.kind;
1907
+ const subscribe = useCallback((fn) => source?.subscribe(fn) ?? (() => {
1908
+ }), [source]);
1909
+ const live = useSyncExternalStore(subscribe, () => {
1910
+ if (kind !== "live") return null;
1911
+ const aim2 = liveAim(interior, settings, reticle);
1912
+ return textOf("unavailable" in aim2 ? aim2 : contrastAt("live", aim2));
1913
+ });
1914
+ const [pixels, setPixels] = useState(null);
1915
+ const [tick, setTick] = useState(0);
1916
+ useEffect(() => source?.subscribe(() => setTick((n) => n + 1)), [source]);
1917
+ const latest = useLatest({ lens, settings, reticle, size });
1918
+ useEffect(() => {
1919
+ if (kind !== "pixels" || !source?.capture) return;
1920
+ const capture = source.capture.bind(source);
1921
+ let current = true;
1922
+ const timer = setTimeout(() => {
1923
+ const { lens: ids, settings: s, reticle: r, size: box } = latest.current;
1924
+ const [request] = requestsOf(source, ids, s, r, box);
1925
+ if (!request) return;
1926
+ void capture(request).then((got) => {
1927
+ if (current) setPixels("reason" in got ? { unavailable: got.reason } : contrastAt("pixels", { capture: got, point: got.at }));
1928
+ });
1929
+ }, 0);
1930
+ return () => {
1931
+ current = false;
1932
+ clearTimeout(timer);
1933
+ };
1934
+ }, [source, kind, reticle.x, reticle.y, settings.factor, settings.compare, tick, latest]);
1935
+ const text = kind === "live" ? live : kind === "pixels" && pixels ? textOf(pixels) : null;
1936
+ return text ? /* @__PURE__ */ jsx("span", { className: "fg-lens-contrast", children: text }) : null;
1937
+ }
1938
+ function tokenAt(kind, interior, settings, reticle) {
1939
+ if (kind === "pixels" && settings.compare !== null) return { unavailable: NEEDS_LIVE };
1940
+ const aim2 = liveAim(interior, settings, reticle);
1941
+ if ("unavailable" in aim2) return aim2;
1942
+ const { within, point } = aim2;
1943
+ const at = within ? colorAt(point, within) : null;
1944
+ if (!at) return { unavailable: NO_COLOR };
1945
+ try {
1946
+ parseColor(at.value);
1947
+ } catch {
1948
+ return { unavailable: unreadable(at.value) };
1949
+ }
1950
+ const chain = tokenChain(at);
1951
+ const branches = chain.some((l) => varRefs(l.declared).size > 1);
1952
+ const end = branches ? at.value : chain.at(-1)?.declared ?? at.value;
1953
+ return { steps: [at.property, ...chain.slice(1).map((l) => l.property), end] };
1954
+ }
1955
+ var isToken = (step) => step.startsWith("--wzl-");
1956
+ function TokenReadout({ settings, sources, reticle, interior }) {
1957
+ const { source } = useShownSource(settings, sources);
1958
+ const kind = source?.kind;
1959
+ const subscribe = useCallback((fn) => source?.subscribe(fn) ?? (() => {
1960
+ }), [source]);
1961
+ const json = useSyncExternalStore(
1962
+ subscribe,
1963
+ () => kind === "live" || kind === "pixels" ? JSON.stringify(tokenAt(kind, interior, settings, reticle)) : null
1964
+ );
1965
+ const reading = useMemo(() => json === null ? null : JSON.parse(json), [json]);
1966
+ if (!reading) return null;
1967
+ if ("unavailable" in reading) return /* @__PURE__ */ jsx("span", { className: "fg-lens-token", children: reading.unavailable });
1968
+ return /* @__PURE__ */ jsx("span", { className: "fg-lens-token", children: reading.steps.map((step, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
1969
+ i > 0 ? " \u2190 " : null,
1970
+ isToken(step) && i < reading.steps.length - 1 ? /* @__PURE__ */ jsx("button", { type: "button", className: "fg-lens-token__link", onClick: () => revealVar(step), children: step }) : step
1971
+ ] }, i)) });
1972
+ }
1973
+ function renderReadouts(props) {
1974
+ const { readouts } = props.settings;
1975
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1976
+ readouts.includes("token") ? /* @__PURE__ */ jsx(TokenReadout, { ...props }) : null,
1977
+ readouts.includes("contrast") ? /* @__PURE__ */ jsx(ContrastReadout, { ...props }) : null
1978
+ ] });
1979
+ }
1980
+ var STATE_UNSENDABLE = "this story's state cannot be sent to a capture";
1981
+ var CONFIG_UNSENDABLE = "this story's config cannot be sent to a capture";
1982
+ var NO_STORY = "this trial shows no story to capture";
1983
+ var NO_BOX = "the story's box has no size";
1984
+ function survived(value, round) {
1985
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value === round;
1986
+ if (typeof value === "number") return Number.isFinite(value) && value === round;
1987
+ if (Array.isArray(value)) {
1988
+ if (!Array.isArray(round) || value.length !== round.length) return false;
1989
+ for (let i = 0; i < value.length; i += 1) if (!(i in value) || !survived(value[i], round[i])) return false;
1990
+ return true;
1991
+ }
1992
+ if (!isPlainObject(value) || !isPlainObject(round)) return false;
1993
+ const keys = Object.keys(value);
1994
+ return keys.length === Object.keys(round).length && keys.every((key) => key in round && survived(value[key], round[key]));
1995
+ }
1996
+ function sendable(state) {
1997
+ try {
1998
+ return survived(state, JSON.parse(JSON.stringify(state) ?? "undefined"));
1999
+ } catch {
2000
+ return false;
2001
+ }
2002
+ }
2003
+ function withoutUndefined(value) {
2004
+ if (Array.isArray(value)) return value.map(withoutUndefined);
2005
+ if (!isPlainObject(value)) return value;
2006
+ return Object.fromEntries(
2007
+ Object.entries(value).flatMap(([key, v]) => v === void 0 ? [] : [[key, withoutUndefined(v)]])
2008
+ );
2009
+ }
2010
+ var clampDpr = (dpr) => Math.min(Math.max(dpr, MIN_DPR), MAX_DPR);
2011
+ function capturePayload(input) {
2012
+ const { entry, layout, config, state, globals, overrides, host, dpr, resolved, lab, colorScheme } = input;
2013
+ if (!sendable(state)) return { unavailable: STATE_UNSENDABLE };
2014
+ const sent = withoutUndefined(withArgs(storyConfig(config), resolved.args));
2015
+ if (!sendable(sent)) return { unavailable: CONFIG_UNSENDABLE };
2016
+ const box = { width: host.offsetWidth, height: host.offsetHeight };
2017
+ if (!(box.width > 0 && box.height > 0)) return { unavailable: NO_BOX };
2018
+ const isolate = entry.isolate !== void 0;
2019
+ const scroller = scrollerOf(host);
2020
+ const kept2 = config?.[PLAYHEAD_KEY];
2021
+ return {
2022
+ story: entry.id,
2023
+ isolate,
2024
+ config: sent,
2025
+ state,
2026
+ globals,
2027
+ overrides,
2028
+ playhead: !isolate && typeof kept2 === "number" && Number.isFinite(kept2) ? kept2 : null,
2029
+ box,
2030
+ layout,
2031
+ dpr: clampDpr(resolved.dpr ?? dpr),
2032
+ scroll: { left: scroller?.scrollLeft ?? 0, top: scroller?.scrollTop ?? 0 },
2033
+ lab,
2034
+ colorScheme
2035
+ };
2036
+ }
2037
+ function trialPayload(trial, lab, hit, dpr, c) {
2038
+ if (!trial) return { unavailable: NO_STORY };
2039
+ const ctx = resolveContextOf(lab);
2040
+ const own = effectiveGlobals(lab.labValues, trial.config?.[GLOBALS_KEY]);
2041
+ const resolved = resolveComparison(c, own, ctx);
2042
+ const colorScheme = ctx.prefersDark ? "dark" : "light";
2043
+ return capturePayload({
2044
+ ...trial,
2045
+ globals: resolved.globals,
2046
+ host: hit.host,
2047
+ dpr,
2048
+ resolved,
2049
+ lab: { globals: lab.labValues, mode: lab.mode === "auto" ? colorScheme : lab.mode },
2050
+ colorScheme
2051
+ });
2052
+ }
2053
+
2054
+ // src/shell/loupe/captureClient.ts
2055
+ var CHECKING = "checking for a capture browser";
2056
+ var UNAVAILABLE = "captures are unavailable";
2057
+ var NO_ENDPOINT = "this server takes no captures";
2058
+ function createCaptureClient(base, send) {
2059
+ const url = `${base}${CAPTURE_PATH}`;
2060
+ const listeners2 = /* @__PURE__ */ new Set();
2061
+ let status = CHECKING;
2062
+ let polling = null;
2063
+ const setStatus = (next) => {
2064
+ if (next === status) return;
2065
+ status = next;
2066
+ for (const listener of [...listeners2]) listener();
2067
+ };
2068
+ const poll = () => {
2069
+ polling ??= (async () => {
2070
+ try {
2071
+ const res = await send(url);
2072
+ if (!res.ok) return setStatus(NO_ENDPOINT);
2073
+ const body = await res.json();
2074
+ setStatus(body.available === true ? true : body.reason ?? UNAVAILABLE);
2075
+ } catch (err) {
2076
+ setStatus(`${NO_ENDPOINT}: ${messageOf(err)}`);
2077
+ } finally {
2078
+ polling = null;
2079
+ }
2080
+ })();
2081
+ };
2082
+ return {
2083
+ status: () => status,
2084
+ subscribe(listener) {
2085
+ listeners2.add(listener);
2086
+ poll();
2087
+ return () => listeners2.delete(listener);
2088
+ },
2089
+ poll,
2090
+ async take(payload) {
2091
+ const res = await send(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
2092
+ if (res.status === 503) {
2093
+ setStatus((await res.json()).reason ?? UNAVAILABLE);
2094
+ return null;
2095
+ }
2096
+ if (!res.ok) {
2097
+ const body = await res.json().catch(() => ({}));
2098
+ throw new Error(body.error ?? `capture failed: ${res.status}`);
2099
+ }
2100
+ return createImageBitmap(await res.blob());
2101
+ }
2102
+ };
2103
+ }
2104
+ var sideOf = (r) => `${r.lensId}:${r.dpr}${r.conditions === null ? "" : ":compared"}`;
2105
+ function aim(shared, request, doc) {
2106
+ const hit = trialAt(request.reticle, doc);
2107
+ if (!hit) return { reason: NOTHING_UNDER };
2108
+ const payload = shared.payloadFor(hit, request.dpr, request.conditions);
2109
+ if ("unavailable" in payload) return { reason: unavailableText("pixels", payload.unavailable) };
2110
+ return { hit, payload, key: stableStringify(payload) };
2111
+ }
2112
+ var fresh = (shared, shot, key) => shot?.key === key && shot.revision === shared.revision();
2113
+ function paint(canvas, shot, at, request) {
2114
+ const sd = canvas.ownerDocument.defaultView?.devicePixelRatio ?? 1;
2115
+ const width = Math.max(1, Math.round(request.size.width * sd));
2116
+ const height = Math.max(1, Math.round(request.size.height * sd));
2117
+ if (canvas.width !== width) canvas.width = width;
2118
+ if (canvas.height !== height) canvas.height = height;
2119
+ const ctx = canvas.getContext("2d");
2120
+ if (!ctx) return;
2121
+ ctx.imageSmoothingEnabled = false;
2122
+ ctx.clearRect(0, 0, width, height);
2123
+ const scale = request.factor * sd / shot.payload.dpr;
2124
+ const x = Math.round(width / 2 - at.x * request.factor * sd);
2125
+ const y = Math.round(height / 2 - at.y * request.factor * sd);
2126
+ ctx.drawImage(shot.bitmap, x, y, shot.bitmap.width * scale, shot.bitmap.height * scale);
2127
+ canvas.toggleAttribute("data-shown", true);
2128
+ }
2129
+ function blank(canvas) {
2130
+ canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
2131
+ canvas.toggleAttribute("data-shown", false);
2132
+ }
2133
+ function attachPixelsLens(slot, first, shared, onDetach) {
2134
+ const doc = slot.ownerDocument;
2135
+ const canvas = doc.createElement("canvas");
2136
+ canvas.className = "fg-lens-pixels";
2137
+ const empty = doc.createElement("div");
2138
+ empty.className = "fg-lens-empty";
2139
+ empty.hidden = true;
2140
+ slot.append(canvas, empty);
2141
+ let request = first;
2142
+ let wanted = null;
2143
+ let pending2 = null;
2144
+ let timer;
2145
+ let started = false;
2146
+ let gone = false;
2147
+ const say = (text) => {
2148
+ empty.hidden = text === null;
2149
+ empty.textContent = text ?? "";
2150
+ };
2151
+ const cancel = () => {
2152
+ clearTimeout(timer);
2153
+ timer = void 0;
2154
+ };
2155
+ const run = async () => {
2156
+ const target = wanted;
2157
+ if (!target) return;
2158
+ const token = {};
2159
+ pending2 = token;
2160
+ const at = shared.revision();
2161
+ const side = sideOf(request);
2162
+ try {
2163
+ const bitmap = await shared.take(target.payload);
2164
+ if (gone || pending2 !== token || bitmap === null || shared.revision() !== at) return;
2165
+ shared.shots.set(side, { key: target.key, revision: at, bitmap, payload: target.payload });
2166
+ measure();
2167
+ } catch (err) {
2168
+ if (!gone && pending2 === token) say(`Capture failed: ${messageOf(err)}`);
2169
+ } finally {
2170
+ if (pending2 === token) pending2 = null;
2171
+ }
2172
+ };
2173
+ const schedule = () => {
2174
+ cancel();
2175
+ if (!started) {
2176
+ started = true;
2177
+ void run();
2178
+ return;
2179
+ }
2180
+ timer = setTimeout(() => {
2181
+ timer = void 0;
2182
+ void run();
2183
+ }, LENS_REST_MS);
2184
+ };
2185
+ const measure = () => {
2186
+ const aimed = aim(shared, request, doc);
2187
+ if ("reason" in aimed) {
2188
+ wanted = null;
2189
+ cancel();
2190
+ blank(canvas);
2191
+ say(aimed.reason);
2192
+ return;
2193
+ }
2194
+ const { hit, payload, key } = aimed;
2195
+ const shot = shared.shots.get(sideOf(request));
2196
+ if (fresh(shared, shot, key)) {
2197
+ wanted = { key, payload };
2198
+ cancel();
2199
+ say(null);
2200
+ paint(canvas, shot, hit.local, request);
2201
+ return;
2202
+ }
2203
+ say(null);
2204
+ if (wanted?.key === key && pending2 !== null) return;
2205
+ wanted = { key, payload };
2206
+ schedule();
2207
+ };
2208
+ measure();
2209
+ return {
2210
+ measure,
2211
+ revised() {
2212
+ pending2 = null;
2213
+ wanted = null;
2214
+ blank(canvas);
2215
+ measure();
2216
+ },
2217
+ update(next) {
2218
+ request = next;
2219
+ measure();
2220
+ },
2221
+ detach() {
2222
+ gone = true;
2223
+ cancel();
2224
+ shared.shots.delete(sideOf(request));
2225
+ canvas.remove();
2226
+ empty.remove();
2227
+ onDetach();
2228
+ }
2229
+ };
2230
+ }
2231
+
2232
+ // src/shell/loupe/pixelsSource.ts
2233
+ var STORY_CHANGED = "the story changed while it was captured";
2234
+ function createPixelsSource(opts) {
2235
+ const client = createCaptureClient(opts.base, opts.fetch ?? ((input, init) => globalThis.fetch(input, init)));
2236
+ const shared = { take: client.take, payloadFor: opts.payloadFor, revision: opts.revision, shots: /* @__PURE__ */ new Map() };
2237
+ const lenses = /* @__PURE__ */ new Set();
2238
+ let unrevise = null;
2239
+ const remeasured = createEmitter();
2240
+ const revised = () => {
2241
+ shared.shots.clear();
2242
+ for (const lens of [...lenses]) lens.revised();
2243
+ };
2244
+ return {
2245
+ kind: "pixels",
2246
+ produces: "pixels",
2247
+ // A DPR comparison captures at its own DPR, as the payload does.
2248
+ label: ({ conditions, dpr }) => `Pixels ${(conditions?.kind === "custom" ? conditions.dpr : void 0) ?? dpr}\xD7`,
2249
+ status: client.status,
2250
+ // Also told whenever a capture may have gone stale, for a lens that captures without attaching, as Difference does.
2251
+ subscribe(listener) {
2252
+ const offs = [client.subscribe(listener), remeasured.subscribe(listener), opts.subscribeRevision(listener)];
2253
+ return () => {
2254
+ for (const off of offs) off();
2255
+ };
2256
+ },
2257
+ attach(slot, first) {
2258
+ client.poll();
2259
+ unrevise ??= opts.subscribeRevision(revised);
2260
+ const lens = attachPixelsLens(slot, first, shared, () => {
2261
+ lenses.delete(lens);
2262
+ if (lenses.size > 0) return;
2263
+ unrevise?.();
2264
+ unrevise = null;
2265
+ });
2266
+ lenses.add(lens);
2267
+ return lens;
2268
+ },
2269
+ async capture(request) {
2270
+ const aimed = aim(shared, request, document);
2271
+ if ("reason" in aimed) return { reason: aimed.reason };
2272
+ const side = sideOf(request);
2273
+ let shot = shared.shots.get(side);
2274
+ if (!fresh(shared, shot, aimed.key)) {
2275
+ const at = opts.revision();
2276
+ const bitmap = await client.take(aimed.payload);
2277
+ if (bitmap === null) {
2278
+ const status = client.status();
2279
+ return { reason: unavailableText("pixels", status === true ? "the capture was refused" : status) };
2280
+ }
2281
+ if (opts.revision() !== at) return { reason: STORY_CHANGED };
2282
+ shot = { key: aimed.key, revision: at, bitmap, payload: aimed.payload };
2283
+ shared.shots.set(side, shot);
2284
+ }
2285
+ return { bitmap: shot.bitmap, dpr: aimed.payload.dpr, box: aimed.payload.box, at: aimed.hit.local };
2286
+ },
2287
+ remeasure() {
2288
+ for (const lens of [...lenses]) lens.measure();
2289
+ remeasured.emit();
2290
+ }
2291
+ };
2292
+ }
2293
+
2294
+ // src/shell/loupe/usePixelsSource.ts
2295
+ function usePixelsSource(lab) {
2296
+ const capture = useContext(LoupeCaptureContext);
2297
+ const [source] = useState(
2298
+ () => capture ? createPixelsSource({
2299
+ base: capture.base,
2300
+ payloadFor: (hit, dpr, c) => trialPayload(capture.trials.get(hit.trialId), lab.current, hit, dpr, c),
2301
+ revision: capture.revision.get,
2302
+ subscribeRevision: capture.revision.subscribe
2303
+ }) : null
2304
+ );
2305
+ useEffect(() => source && capture ? capture.trials.subscribe(() => source.remeasure()) : void 0, [source, capture]);
2306
+ return source;
2307
+ }
2308
+ var STAGGER = 24;
2309
+ var OPPOSITE = { kind: "opposite-mode" };
2310
+ var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
2311
+ function bodyBeside(reticle, box, n) {
2312
+ const half = BODY_SIZE / 2;
2313
+ const gap = BODY_SIZE * 0.75 + STAGGER * n;
2314
+ const x = reticle.x + gap + half > box.width ? reticle.x - gap : reticle.x + gap;
2315
+ return { x: clamp(x, half, box.width - half), y: clamp(reticle.y + STAGGER * n, half, box.height - half) };
2316
+ }
2317
+ function lensClient(scene, lensId, body) {
2318
+ const ids = lensIdsOf(scene, `${lensId}:body`);
2319
+ const reticle = ids ? scene.get(ids.reticle) : void 0;
2320
+ const lens = ids ? scene.get(ids.body) : void 0;
2321
+ if (!body || !reticle || !lens) return null;
2322
+ const box = body.getBoundingClientRect();
2323
+ const center = (p) => ({ x: box.left + p.x + p.width / 2, y: box.top + p.y + p.height / 2 });
2324
+ return { reticle: center(effectivePose(scene, reticle)), body: center(effectivePose(scene, lens)) };
2325
+ }
2326
+ function useLensFocus() {
2327
+ const focus = useRef(null);
2328
+ const onFocusLens = useCallback((lensId) => {
2329
+ focus.current = lensId;
2330
+ }, []);
2331
+ return { focus, onFocusLens };
2332
+ }
2333
+ function createApi(scene, refs) {
2334
+ let next = 0;
2335
+ let hold = null;
2336
+ const toBody = (client, box) => ({ x: client.x - box.left, y: client.y - box.top });
2337
+ const apply = (ops) => {
2338
+ if (ops.length > 0) sceneToAdapter(scene).applyOps([...ops], "lens");
2339
+ };
2340
+ const current = () => {
2341
+ const focused = refs.focus.current;
2342
+ if (focused !== null && settingsOf(scene, focused)) return focused;
2343
+ return lensesOf(scene).at(-1)?.lensId ?? null;
2344
+ };
2345
+ return {
2346
+ scene,
2347
+ current,
2348
+ apply,
2349
+ add(at) {
2350
+ const body = refs.body.current;
2351
+ const count = lensesOf(scene).length;
2352
+ if (!body || count >= MAX_LENSES) return null;
2353
+ const box = body.getBoundingClientRect();
2354
+ const client = at ?? refs.pointer.current ?? { x: box.left + box.width / 2, y: box.top + box.height / 2 };
2355
+ const reticle = toBody(client, box);
2356
+ const lensId = `lens-${next++}`;
2357
+ apply(addLensOps(scene, lensId, { body: bodyBeside(reticle, box, count), reticle }));
2358
+ refs.focus.current = lensId;
2359
+ return lensId;
2360
+ },
2361
+ follow(client) {
2362
+ const lensId = current();
2363
+ const body = refs.body.current;
2364
+ if (lensId === null || !body) return;
2365
+ const pose = reticleAt(scene, lensId, toBody(client, body.getBoundingClientRect()));
2366
+ if (pose === null) return;
2367
+ hold ??= scene.overrides.hold();
2368
+ hold.set([[`${lensId}:reticle`, pose]]);
2369
+ },
2370
+ sourceAvailable: (kind) => refs.sources.current[kind]?.status() ?? "not provided",
2371
+ openCompareMenu: (lensId) => refs.compare?.current?.(lensId)
2372
+ };
2373
+ }
2374
+ function useRemeasure(sources, body, rerender) {
2375
+ const { trials, mode } = useLabContext();
2376
+ const labValues = useContext(StoryGlobalsContext);
2377
+ useEffect(() => {
2378
+ for (const source of sources) source.remeasure();
2379
+ }, [sources, trials, mode, labValues]);
2380
+ useEffect(() => {
2381
+ if (sources.length === 0 || !body) return;
2382
+ const remeasure = () => {
2383
+ for (const source of sources) source.remeasure();
2384
+ };
2385
+ const resize = () => {
2386
+ rerender();
2387
+ remeasure();
2388
+ };
2389
+ body.addEventListener("scroll", remeasure, { capture: true, passive: true });
2390
+ window.addEventListener("resize", resize);
2391
+ const unveiled = typeof MutationObserver === "undefined" ? null : new MutationObserver(remeasure);
2392
+ unveiled?.observe(body, { subtree: true, attributes: true, attributeFilter: ["data-pending"] });
2393
+ return () => {
2394
+ body.removeEventListener("scroll", remeasure, { capture: true });
2395
+ window.removeEventListener("resize", resize);
2396
+ unveiled?.disconnect();
2397
+ };
2398
+ }, [sources, body, rerender]);
2399
+ }
2400
+ function ForgeLoupe({ declarations, modeGlobal }) {
2401
+ const lenses = useContext(LiveLensesContext);
2402
+ const labValues = useContext(StoryGlobalsContext);
2403
+ const { mode, trials, instruments } = useLabContext();
2404
+ const [scene] = useState(createLensScene);
2405
+ const [body, setBody] = useState(null);
2406
+ const bodyRef = useRef(null);
2407
+ const pointer = useRef(null);
2408
+ const { focus, onFocusLens } = useLensFocus();
2409
+ const conditions = useLatest({ labValues, mode, declarations, modeGlobal });
2410
+ const [live] = useState(
2411
+ () => lenses ? createLiveSource(lenses, resolveComparison, () => ({ base: conditions.current.labValues, ctx: resolveContextOf(conditions.current) })) : null
2412
+ );
2413
+ const pixels = usePixelsSource(conditions);
2414
+ const sources = useMemo(
2415
+ () => ({ ...live ? { live } : {}, ...pixels ? { pixels } : {} }),
2416
+ [live, pixels]
2417
+ );
2418
+ const remeasured = useMemo(() => [live, pixels].filter((s) => s !== null), [live, pixels]);
2419
+ const sourcesRef = useLatest(sources);
2420
+ const [menu, setMenu] = useState(null);
2421
+ const compare = useLatest((lensId) => {
2422
+ const at = lensClient(scene, lensId, bodyRef.current);
2423
+ if (!at) return;
2424
+ const trialId = trialAt(at.reticle)?.trialId;
2425
+ const name = trials.find((t) => t.id === trialId)?.instrumentName;
2426
+ const schema = instruments.find((i) => i.name === name)?.config ?? null;
2427
+ const settings = settingsOf(scene, lensId);
2428
+ const pixels2 = settings !== null && (settings.view === "difference" || resolveSource(settings.source, sourcesRef.current).source?.kind === "pixels");
2429
+ const choices = compareChoices(declarations, schema);
2430
+ setMenu((last) => ({ seq: (last?.seq ?? 0) + 1, client: at.body, lensId, pixels: pixels2, choices }));
2431
+ });
2432
+ const [api] = useState(() => createApi(scene, { body: bodyRef, pointer, focus, sources: sourcesRef, compare }));
2433
+ const describe = useCallback(
2434
+ (c, at) => {
2435
+ const trialId = trialAt(at.reticle)?.trialId;
2436
+ const config = trials.find((t) => t.id === trialId)?.config;
2437
+ return describeLens(c, { labValues, mode, declarations, modeGlobal }, config, at.source === "pixels");
2438
+ },
2439
+ [declarations, labValues, mode, modeGlobal, trials]
2440
+ );
2441
+ const [, rerender] = useReducer((n) => n + 1, 0);
2442
+ useSyncExternalStore(scene.overrides.subscribe, scene.overrides.getGeneration);
2443
+ useRemeasure(remeasured, body, rerender);
2444
+ useEffect(() => {
2445
+ if (!body) return;
2446
+ const track = (e) => {
2447
+ pointer.current = { x: e.clientX, y: e.clientY };
2448
+ };
2449
+ body.addEventListener("pointermove", track, { passive: true });
2450
+ return () => body.removeEventListener("pointermove", track);
2451
+ }, [body]);
2452
+ const attachMarker = useCallback((el) => {
2453
+ const found = el?.closest(".lk-lab__body") ?? null;
2454
+ bodyRef.current = found;
2455
+ setBody(found);
2456
+ }, []);
2457
+ const opposite = useCallback(() => OPPOSITE, []);
2458
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2459
+ /* @__PURE__ */ jsx("span", { hidden: true, ref: attachMarker }),
2460
+ /* @__PURE__ */ jsx(LensOverlay, { scene, sources, describe, renderReadouts, onFocusLens }),
2461
+ /* @__PURE__ */ jsx(LensActions, { api, bodyRef, opposite }),
2462
+ /* @__PURE__ */ jsx(CompareMenu, { api, request: menu }),
2463
+ /* @__PURE__ */ jsx(KeymapHint, { className: "fg-loupe-hint" })
2464
+ ] });
2465
+ }
1158
2466
 
1159
2467
  // src/shell/routedTrial.ts
1160
2468
  function routedTrial(trials, focusedTrialId, route) {
@@ -1206,9 +2514,9 @@ function RoutePlayhead() {
1206
2514
  if (!trial || route === null) return null;
1207
2515
  return /* @__PURE__ */ jsx(PlayheadSync, { trialId: trial.id, route, appliedRef: applied }, `${trial.id} ${route}`);
1208
2516
  }
1209
- var RECORD = "fg-globals";
2517
+ var RECORD3 = "fg-globals";
1210
2518
  function useLabGlobals(declarations) {
1211
- const [stored, setStored] = usePersistedState(RECORD, null, { scope: "lab" });
2519
+ const [stored, setStored] = usePersistedState(RECORD3, null, { scope: "lab" });
1212
2520
  const values = useMemo(() => labGlobals(declarations, stored), [declarations, stored]);
1213
2521
  const set = useCallback(
1214
2522
  (key, value) => setStored((prev) => ({ ...labGlobals(declarations, prev), [key]: value })),
@@ -1510,14 +2818,27 @@ function filterComponents(rows, query) {
1510
2818
  }
1511
2819
  return out;
1512
2820
  }
2821
+ function parentOf(row, byPath) {
2822
+ const segments = row.path.split("/");
2823
+ for (let take = segments.length - 1; take > 0; take--) {
2824
+ const parent = byPath.get(segments.slice(0, take).join("/"));
2825
+ if (parent && parent.library === row.library) return parent;
2826
+ }
2827
+ return void 0;
2828
+ }
1513
2829
  function componentNodes(rows) {
1514
2830
  const root = [];
1515
2831
  const folders = /* @__PURE__ */ new Map();
2832
+ const byPath = new Map(rows.map((row) => [row.path, row]));
2833
+ const nodes = /* @__PURE__ */ new Map();
2834
+ const subs = /* @__PURE__ */ new Map();
2835
+ for (const row of rows) subs.set(row.path, []);
1516
2836
  for (const row of rows) {
1517
- const labels = row.label.split("/");
2837
+ const parent = parentOf(row, byPath);
1518
2838
  const title = row.path.split("/");
1519
- let siblings = root;
1520
- let tag = row.library;
2839
+ const labels = parent ? title.slice(parent.path.split("/").length) : row.label.split("/");
2840
+ let siblings = parent ? subs.get(parent.path) : root;
2841
+ let tag = parent ? void 0 : row.library;
1521
2842
  for (let i = 0; i < labels.length - 1; i++) {
1522
2843
  const path = title.slice(0, title.length - labels.length + i + 1).join("/");
1523
2844
  let folder = folders.get(path);
@@ -1529,14 +2850,22 @@ function componentNodes(rows) {
1529
2850
  siblings = folder.children;
1530
2851
  tag = void 0;
1531
2852
  }
1532
- siblings.push({
2853
+ const node = {
1533
2854
  kind: "folder",
1534
2855
  label: labels[labels.length - 1],
1535
2856
  path: row.path,
1536
2857
  tag,
1537
2858
  ...row.index ? { index: row.index } : {},
1538
- children: row.entries.map((entry) => ({ kind: "story", entry }))
1539
- });
2859
+ children: []
2860
+ };
2861
+ nodes.set(row.path, node);
2862
+ siblings.push(node);
2863
+ }
2864
+ for (const row of rows) {
2865
+ nodes.get(row.path).children = [
2866
+ ...subs.get(row.path),
2867
+ ...row.entries.map((entry) => ({ kind: "story", entry }))
2868
+ ];
1540
2869
  }
1541
2870
  return foldGalleries(root);
1542
2871
  }
@@ -1600,13 +2929,13 @@ function StoryTree({ ctx, index }) {
1600
2929
  const filtering = query.trim() !== "";
1601
2930
  const isOpen = (path) => filtering || (folds[path] ?? routeAncestors.has(path));
1602
2931
  const rows = [];
1603
- const collect = (nodes, parent) => {
2932
+ const collect2 = (nodes, parent) => {
1604
2933
  for (const node of nodes) {
1605
2934
  rows.push({ key: keyOf(node), node, parent });
1606
- if (node.kind === "folder" && isOpen(node.path)) collect(node.children, keyOf(node));
2935
+ if (node.kind === "folder" && isOpen(node.path)) collect2(node.children, keyOf(node));
1607
2936
  }
1608
2937
  };
1609
- collect(shown, null);
2938
+ collect2(shown, null);
1610
2939
  const active = rows.some((row) => row.key === focusKey) ? focusKey : rows[0]?.key ?? null;
1611
2940
  const openIds = new Set(ctx.trials.map((trial) => trial.instrumentName));
1612
2941
  const setOpen = (path, open) => {
@@ -1885,6 +3214,171 @@ function createAnswerBook() {
1885
3214
 
1886
3215
  // src/shell/readyKey.ts
1887
3216
  var readyKey = (ready) => stableStringify({ schema: ready.schema, layout: ready.layout, viewport: ready.viewport });
3217
+ var START_TIMEOUT_MS = 1e4;
3218
+ var requests = 0;
3219
+ function closeLink(link) {
3220
+ if (!link.current) return;
3221
+ clearTimeout(link.current.timer);
3222
+ for (const settle of link.current.pending.values()) settle({ ok: false, message: "The frame went away" });
3223
+ link.current.pending.clear();
3224
+ link.current.disconnect();
3225
+ link.current.channel.close();
3226
+ link.current = null;
3227
+ }
3228
+ function mismatchMessage(mismatch) {
3229
+ 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";
3230
+ }
3231
+ function sendInputs(current, sent, next) {
3232
+ const before = current.inputs;
3233
+ const { state, globals } = next;
3234
+ const config = storyConfig(next.config, sent.config);
3235
+ if (config !== sent.config) {
3236
+ sent.config = config;
3237
+ current.channel.send({ type: "config", config });
3238
+ current.inputs += 1;
3239
+ }
3240
+ if (state !== sent.state) {
3241
+ sent.state = state;
3242
+ current.channel.send({ type: "state", state });
3243
+ current.inputs += 1;
3244
+ }
3245
+ if (globals !== sent.globals && stableStringify(globals) !== stableStringify(sent.globals)) {
3246
+ sent.globals = globals;
3247
+ current.channel.send({ type: "globals", globals });
3248
+ current.inputs += 1;
3249
+ }
3250
+ return current.inputs !== before;
3251
+ }
3252
+ function receive(deps, current, msg) {
3253
+ const { latest: latestRef, trialId, frames, setFault, setPending, init, copy } = deps;
3254
+ const { ctx: live, answers, onReady } = latestRef.current;
3255
+ switch (msg.type) {
3256
+ case "ready": {
3257
+ clearTimeout(current.timer);
3258
+ setFault(null);
3259
+ for (const [name, value] of Object.entries(latestRef.current.overrides)) {
3260
+ current.channel.send({ type: "vars.set", name, value });
3261
+ }
3262
+ if (copy) {
3263
+ init(current);
3264
+ break;
3265
+ }
3266
+ const key = readyKey(msg);
3267
+ current.awaiting = key;
3268
+ onReady(latestRef.current.entry, msg);
3269
+ if (current.awaiting === key && key === latestRef.current.descriptionKey) init(current);
3270
+ break;
3271
+ }
3272
+ case "rendered":
3273
+ setPending(false);
3274
+ current.hasRendered = true;
3275
+ for (const ask of current.queued.splice(0)) ask();
3276
+ break;
3277
+ case "setConfig":
3278
+ if (!isGlobalsPath(msg.path)) live.setConfig(msg.path, msg.value);
3279
+ break;
3280
+ case "setState":
3281
+ if (current.sent) current.sent.state = msg.state;
3282
+ live.setState(msg.state);
3283
+ break;
3284
+ case "answers":
3285
+ if (!copy) answers.record(msg.answers);
3286
+ break;
3287
+ case "vars":
3288
+ if (trialId) frames?.report(trialId, msg.vars);
3289
+ break;
3290
+ case "a11y": {
3291
+ const outcome = msg.ok ? { ok: true, report: msg.report } : { ok: false, message: msg.message };
3292
+ if (trialId) frames?.reportA11y(trialId, outcome);
3293
+ current.pending.get(msg.id)?.(outcome);
3294
+ current.pending.delete(msg.id);
3295
+ break;
3296
+ }
3297
+ case "open": {
3298
+ const { lab: liveLab } = latestRef.current;
3299
+ if (!trialId || !liveLab.instruments.some((instrument) => instrument.name === msg.id)) break;
3300
+ liveLab.swapTrial(trialId, msg.id);
3301
+ setRoute(msg.id, { inPlace: true });
3302
+ break;
3303
+ }
3304
+ case "fault":
3305
+ if (msg.phase === "render" && msg.seq !== void 0 && msg.seq < current.inputs) break;
3306
+ clearTimeout(current.timer);
3307
+ setFault({ phase: msg.phase, message: msg.message });
3308
+ break;
3309
+ }
3310
+ }
3311
+ function openLink(deps, link, frame) {
3312
+ const { latest: latestRef, src, trialId, frames, setFault, setPending } = deps;
3313
+ closeLink(link);
3314
+ setPending(true);
3315
+ const target = frame.contentWindow;
3316
+ if (!target) return;
3317
+ const { port1, port2 } = new MessageChannel();
3318
+ const timer = setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), START_TIMEOUT_MS);
3319
+ const channel = openChannel(port1, {
3320
+ onMismatch: (mismatch) => {
3321
+ clearTimeout(timer);
3322
+ setFault({ phase: "protocol", message: mismatchMessage(mismatch) });
3323
+ }
3324
+ });
3325
+ const waiting = /* @__PURE__ */ new Map();
3326
+ const audit = () => new Promise((resolve) => {
3327
+ const id = `a11y-${requests += 1}`;
3328
+ waiting.set(id, resolve);
3329
+ const ask = () => channel.send({ type: "a11y.run", id });
3330
+ if (current.hasRendered) ask();
3331
+ else current.queued.push(ask);
3332
+ });
3333
+ const disconnect = frames && trialId ? frames.connect(trialId, { send: channel.send, audit }) : () => {
3334
+ };
3335
+ const current = {
3336
+ channel,
3337
+ deliver: (msg) => receive(deps, current, msg),
3338
+ timer,
3339
+ awaiting: null,
3340
+ sent: null,
3341
+ inputs: 0,
3342
+ disconnect,
3343
+ pending: waiting,
3344
+ hasRendered: false,
3345
+ queued: []
3346
+ };
3347
+ link.current = current;
3348
+ channel.on((msg) => current.deliver(msg));
3349
+ target.postMessage({ type: PORT_HANDOFF, id: latestRef.current.entry.id }, location.origin, [
3350
+ port2
3351
+ ]);
3352
+ }
3353
+ function useFrameLink(opts) {
3354
+ const { latest: latestRef, src, trialId, frames, setFault, setPending, copy, config, state, globals, descriptionKey } = opts;
3355
+ const link = useRef(null);
3356
+ const init = useCallback((current) => {
3357
+ const { ctx: live, globals: liveGlobals } = latestRef.current;
3358
+ current.awaiting = null;
3359
+ current.sent = { config: storyConfig(live.config), state: live.state, globals: liveGlobals };
3360
+ current.inputs += 1;
3361
+ current.channel.send({ type: "init", ...current.sent });
3362
+ }, [latestRef]);
3363
+ const deps = { latest: latestRef, src, trialId, frames, setFault, setPending, copy, init };
3364
+ const connect = (frame) => openLink(deps, link, frame);
3365
+ const adopt2 = (adopted) => {
3366
+ adopted.deliver = (msg) => receive(deps, adopted, msg);
3367
+ setPending(!adopted.hasRendered);
3368
+ };
3369
+ useEffect(() => {
3370
+ const current = link.current;
3371
+ if (!current) return;
3372
+ if (current.awaiting !== null) {
3373
+ if (current.awaiting === descriptionKey) init(current);
3374
+ return;
3375
+ }
3376
+ if (current.sent && sendInputs(current, current.sent, { config, state, globals })) {
3377
+ setFault((shown) => shown?.phase === "render" ? null : shown);
3378
+ }
3379
+ }, [config, state, globals, descriptionKey, init, setFault]);
3380
+ return { connect, adopt: adopt2, link };
3381
+ }
1888
3382
  var kept = /* @__PURE__ */ new Map();
1889
3383
  var shelf = null;
1890
3384
  function shelfElement() {
@@ -1933,46 +3427,56 @@ function useKeptFrame(options) {
1933
3427
  }, [latest]);
1934
3428
  return useCallback(() => handedOn.current, []);
1935
3429
  }
1936
- var START_TIMEOUT_MS = 1e4;
1937
- var requests = 0;
1938
- function closeLink(link) {
1939
- if (!link.current) return;
1940
- clearTimeout(link.current.timer);
1941
- for (const settle of link.current.pending.values()) settle({ ok: false, message: "The frame went away" });
1942
- link.current.pending.clear();
1943
- link.current.disconnect();
1944
- link.current.channel.close();
1945
- link.current = null;
1946
- }
1947
- function mismatchMessage(mismatch) {
1948
- 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";
1949
- }
1950
- function sendInputs(current, sent, next) {
1951
- const before = current.inputs;
1952
- const { state, globals } = next;
1953
- const config = storyConfig(next.config, sent.config);
1954
- if (config !== sent.config) {
1955
- sent.config = config;
1956
- current.channel.send({ type: "config", config });
1957
- current.inputs += 1;
1958
- }
1959
- if (state !== sent.state) {
1960
- sent.state = state;
1961
- current.channel.send({ type: "state", state });
1962
- current.inputs += 1;
1963
- }
1964
- if (globals !== sent.globals && stableStringify(globals) !== stableStringify(sent.globals)) {
1965
- sent.globals = globals;
1966
- current.channel.send({ type: "globals", globals });
1967
- current.inputs += 1;
3430
+ function mountFrame(m) {
3431
+ const { inView, src, slotRef, iframeRef, latest: latestRef, connect: connectRef, link, handedOn, setFault, setPending } = m;
3432
+ const slot = slotRef.current;
3433
+ if (!inView || !slot) return void 0;
3434
+ const adopted = iframeRef.current;
3435
+ const claimed = adopted || latestRef.current.copy ? null : latestRef.current.pool?.claim() ?? null;
3436
+ const frame = adopted ?? claimed ?? document.createElement("iframe");
3437
+ frame.className = "fg-frame-view";
3438
+ frame.title = latestRef.current.title;
3439
+ frame.removeAttribute("tabindex");
3440
+ frame.toggleAttribute("data-pending", true);
3441
+ iframeRef.current = frame;
3442
+ const unheard = adopted || claimed ? void 0 : setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), HELLO_TIMEOUT_MS);
3443
+ const onMessage = (event) => {
3444
+ if (event.source !== frame.contentWindow || event.origin !== location.origin) return;
3445
+ if (event.data?.type !== FRAME_HELLO) return;
3446
+ clearTimeout(unheard);
3447
+ connectRef.current(frame);
3448
+ };
3449
+ window.addEventListener("message", onMessage);
3450
+ if (claimed) {
3451
+ moveFrame(slot, claimed);
3452
+ connectRef.current(claimed);
3453
+ } else if (!adopted) {
3454
+ frame.src = src;
3455
+ slot.append(frame);
1968
3456
  }
1969
- return current.inputs !== before;
3457
+ return () => {
3458
+ window.removeEventListener("message", onMessage);
3459
+ clearTimeout(unheard);
3460
+ if (handedOn()) return;
3461
+ closeLink(link);
3462
+ frame.remove();
3463
+ iframeRef.current = null;
3464
+ setPending(true);
3465
+ setFault(null);
3466
+ };
1970
3467
  }
3468
+ var NO_ARGS = {};
1971
3469
  function FrameView(props) {
1972
- const { entry, frameUrl, answers, descriptionKey, ctx } = props;
3470
+ const { entry, frameUrl, answers, descriptionKey, ctx: trialCtx, copy } = props;
1973
3471
  const labGlobals2 = useContext(StoryGlobalsContext);
1974
- const pins = ctx.config?.[GLOBALS_KEY];
1975
- const globals = useMemo(() => effectiveGlobals(labGlobals2, pins), [labGlobals2, pins]);
3472
+ const pins = trialCtx.config?.[GLOBALS_KEY];
3473
+ const own = useMemo(() => effectiveGlobals(labGlobals2, pins), [labGlobals2, pins]);
3474
+ const globals = copy ? copy.globals : own;
3475
+ const { config: laidConfig, setConfig: laidSetConfig } = useCopyConfig(trialCtx.config, trialCtx.setConfig, copy?.args ?? NO_ARGS);
3476
+ const ctx = useMemo(
3477
+ () => copy ? { ...trialCtx, config: laidConfig, setConfig: laidSetConfig } : trialCtx,
3478
+ [copy, trialCtx, laidConfig, laidSetConfig]
3479
+ );
1976
3480
  const trialId = useContext(TrialIdContext);
1977
3481
  const frames = useContext(TrialFramesContext);
1978
3482
  const [overrides] = useCssOverrides();
@@ -1984,185 +3488,49 @@ function FrameView(props) {
1984
3488
  const hostRef = useRef(null);
1985
3489
  const slotRef = useRef(null);
1986
3490
  const inView = useNearViewport(hostRef, IN_VIEW_MARGIN);
1987
- const link = useRef(null);
1988
- const latestRef = useLatest({ ...props, globals, overrides, pool, lab, title });
3491
+ const latestRef = useLatest({ ...props, ctx, globals, overrides, pool, lab, title });
1989
3492
  const [fault, setFault] = useState(null);
1990
- const [pending, setPending] = useState(true);
1991
- const init = useCallback((current) => {
1992
- const { ctx: live, globals: liveGlobals } = latestRef.current;
1993
- current.awaiting = null;
1994
- current.sent = { config: storyConfig(live.config), state: live.state, globals: liveGlobals };
1995
- current.inputs += 1;
1996
- current.channel.send({ type: "init", ...current.sent });
1997
- }, [latestRef]);
1998
- const receive = (current, msg) => {
1999
- const { ctx: live, answers: answers2, onReady } = latestRef.current;
2000
- switch (msg.type) {
2001
- case "ready": {
2002
- clearTimeout(current.timer);
2003
- setFault(null);
2004
- for (const [name, value] of Object.entries(latestRef.current.overrides)) {
2005
- current.channel.send({ type: "vars.set", name, value });
2006
- }
2007
- const key = readyKey(msg);
2008
- current.awaiting = key;
2009
- onReady(latestRef.current.entry, msg);
2010
- if (current.awaiting === key && key === latestRef.current.descriptionKey) init(current);
2011
- break;
2012
- }
2013
- case "rendered":
2014
- setPending(false);
2015
- current.hasRendered = true;
2016
- for (const ask of current.queued.splice(0)) ask();
2017
- break;
2018
- case "setConfig":
2019
- if (!isGlobalsPath(msg.path)) live.setConfig(msg.path, msg.value);
2020
- break;
2021
- case "setState":
2022
- if (current.sent) current.sent.state = msg.state;
2023
- live.setState(msg.state);
2024
- break;
2025
- case "answers":
2026
- answers2.record(msg.answers);
2027
- break;
2028
- case "vars":
2029
- if (trialId) frames?.report(trialId, msg.vars);
2030
- break;
2031
- case "a11y": {
2032
- const outcome = msg.ok ? { ok: true, report: msg.report } : { ok: false, message: msg.message };
2033
- if (trialId) frames?.reportA11y(trialId, outcome);
2034
- current.pending.get(msg.id)?.(outcome);
2035
- current.pending.delete(msg.id);
2036
- break;
2037
- }
2038
- case "open": {
2039
- const { lab: liveLab } = latestRef.current;
2040
- if (!trialId || !liveLab.instruments.some((instrument) => instrument.name === msg.id)) break;
2041
- liveLab.swapTrial(trialId, msg.id);
2042
- setRoute(msg.id, { inPlace: true });
2043
- break;
2044
- }
2045
- case "fault":
2046
- if (msg.phase === "render" && msg.seq !== void 0 && msg.seq < current.inputs) break;
2047
- clearTimeout(current.timer);
2048
- setFault({ phase: msg.phase, message: msg.message });
2049
- break;
2050
- }
2051
- };
2052
- const connect = (frame) => {
2053
- closeLink(link);
2054
- setPending(true);
2055
- const target = frame.contentWindow;
2056
- if (!target) return;
2057
- const { port1, port2 } = new MessageChannel();
2058
- const timer = setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), START_TIMEOUT_MS);
2059
- const channel = openChannel(port1, {
2060
- onMismatch: (mismatch) => {
2061
- clearTimeout(timer);
2062
- setFault({ phase: "protocol", message: mismatchMessage(mismatch) });
2063
- }
2064
- });
2065
- const waiting = /* @__PURE__ */ new Map();
2066
- const audit = () => new Promise((resolve) => {
2067
- const id = `a11y-${requests += 1}`;
2068
- waiting.set(id, resolve);
2069
- const ask = () => channel.send({ type: "a11y.run", id });
2070
- if (current.hasRendered) ask();
2071
- else current.queued.push(ask);
2072
- });
2073
- const disconnect = frames && trialId ? frames.connect(trialId, { send: channel.send, audit }) : () => {
2074
- };
2075
- const current = {
2076
- channel,
2077
- deliver: (msg) => receive(current, msg),
2078
- timer,
2079
- awaiting: null,
2080
- sent: null,
2081
- inputs: 0,
2082
- disconnect,
2083
- pending: waiting,
2084
- hasRendered: false,
2085
- queued: []
2086
- };
2087
- link.current = current;
2088
- channel.on((msg) => current.deliver(msg));
2089
- target.postMessage({ type: PORT_HANDOFF, id: latestRef.current.entry.id }, location.origin, [
2090
- port2
2091
- ]);
2092
- };
3493
+ const [pending2, setPending] = useState(true);
3494
+ const { connect, adopt: adopt2, link } = useFrameLink({
3495
+ latest: latestRef,
3496
+ src,
3497
+ trialId: copy ? null : trialId,
3498
+ frames: copy ? null : frames,
3499
+ setFault,
3500
+ setPending,
3501
+ copy: copy !== void 0,
3502
+ config: ctx.config,
3503
+ state: ctx.state,
3504
+ globals,
3505
+ descriptionKey
3506
+ });
2093
3507
  const connectRef = useLatest(connect);
2094
3508
  const handedOn = useKeptFrame({
2095
- key: trialId ? `${trialId} ${src}` : null,
3509
+ key: trialId && !copy ? `${trialId} ${src}` : null,
2096
3510
  slot: slotRef,
2097
3511
  frame: iframeRef,
2098
3512
  link,
2099
- onAdopt: (adopted) => {
2100
- adopted.deliver = (msg) => receive(adopted, msg);
2101
- setPending(!adopted.hasRendered);
2102
- },
3513
+ onAdopt: adopt2,
2103
3514
  onDrop: () => closeLink(link)
2104
3515
  });
3516
+ useEffect(
3517
+ () => mountFrame({ inView, src, slotRef, iframeRef, latest: latestRef, connect: connectRef, link, handedOn, setFault, setPending }),
3518
+ [inView, src, connectRef, latestRef, handedOn, link]
3519
+ );
2105
3520
  useEffect(() => {
2106
- const slot = slotRef.current;
2107
- if (!inView || !slot) return;
2108
- const adopted = iframeRef.current;
2109
- const claimed = adopted ? null : latestRef.current.pool?.claim() ?? null;
2110
- const frame = adopted ?? claimed ?? document.createElement("iframe");
2111
- frame.className = "fg-frame-view";
2112
- frame.title = latestRef.current.title;
2113
- frame.removeAttribute("tabindex");
2114
- frame.toggleAttribute("data-pending", true);
2115
- iframeRef.current = frame;
2116
- const unheard = adopted || claimed ? void 0 : setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), HELLO_TIMEOUT_MS);
2117
- const onMessage = (event) => {
2118
- if (event.source !== frame.contentWindow || event.origin !== location.origin) return;
2119
- if (event.data?.type !== FRAME_HELLO) return;
2120
- clearTimeout(unheard);
2121
- connectRef.current(frame);
2122
- };
2123
- window.addEventListener("message", onMessage);
2124
- if (claimed) {
2125
- moveFrame(slot, claimed);
2126
- connectRef.current(claimed);
2127
- } else if (!adopted) {
2128
- frame.src = src;
2129
- slot.append(frame);
2130
- }
2131
- return () => {
2132
- window.removeEventListener("message", onMessage);
2133
- clearTimeout(unheard);
2134
- if (handedOn()) return;
2135
- closeLink(link);
2136
- frame.remove();
2137
- iframeRef.current = null;
2138
- setPending(true);
2139
- setFault(null);
2140
- };
2141
- }, [inView, src, connectRef, latestRef, handedOn]);
2142
- useEffect(() => {
2143
- iframeRef.current?.toggleAttribute("data-pending", pending && !fault);
2144
- }, [pending, fault, inView, src]);
3521
+ iframeRef.current?.toggleAttribute("data-pending", pending2 && !fault);
3522
+ }, [pending2, fault, inView, src]);
2145
3523
  useEffect(() => {
2146
3524
  if (iframeRef.current) iframeRef.current.title = title;
2147
3525
  }, [title]);
2148
- useEffect(() => answers.hold(ctx.config), [answers, ctx.config]);
2149
- useEffect(() => {
2150
- const current = link.current;
2151
- if (!current) return;
2152
- if (current.awaiting !== null) {
2153
- if (current.awaiting === descriptionKey) init(current);
2154
- return;
2155
- }
2156
- if (current.sent && sendInputs(current, current.sent, { config: ctx.config, state: ctx.state, globals })) {
2157
- setFault((shown) => shown?.phase === "render" ? null : shown);
2158
- }
2159
- }, [ctx.config, ctx.state, globals, descriptionKey, init]);
2160
- return /* @__PURE__ */ jsxs("div", { ref: hostRef, className: "fg-frame-host", children: [
3526
+ useEffect(() => copy ? void 0 : answers.hold(ctx.config), [answers, ctx.config, copy]);
3527
+ 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: [
2161
3528
  /* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
2162
3529
  fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
2163
3530
  fault.phase ? /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: fault.phase }) : null,
2164
3531
  /* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
2165
- ] }) : null
3532
+ ] }) : null,
3533
+ copy ? null : /* @__PURE__ */ jsx(LiveFrameCopies, { trialId, trialConfig: trialCtx.config, children: (lensCopy) => /* @__PURE__ */ jsx(FrameView, { ...props, copy: lensCopy }) })
2166
3534
  ] });
2167
3535
  }
2168
3536
  function storyInstrument(options) {
@@ -2179,17 +3547,20 @@ function storyInstrument(options) {
2179
3547
  config,
2180
3548
  defaultConfig: () => config.defaults(),
2181
3549
  initialState: () => null,
2182
- render: (ctx) => /* @__PURE__ */ jsx(
2183
- FrameView,
2184
- {
2185
- entry,
2186
- frameUrl,
2187
- answers,
2188
- onReady,
2189
- descriptionKey,
2190
- ctx
2191
- }
2192
- ),
3550
+ render: (ctx) => /* @__PURE__ */ jsxs(Fragment, { children: [
3551
+ /* @__PURE__ */ jsx(
3552
+ FrameView,
3553
+ {
3554
+ entry,
3555
+ frameUrl,
3556
+ answers,
3557
+ onReady,
3558
+ descriptionKey,
3559
+ ctx
3560
+ }
3561
+ ),
3562
+ /* @__PURE__ */ jsx(CaptureInputs, { entry, layout: ready?.layout ?? "centered", config: ctx.config, state: ctx.state })
3563
+ ] }),
2193
3564
  // Taken from the index until the frame reports, so the trial does not move the frame into a stage and reload it.
2194
3565
  ...viewport ? { stage: { size: { width: viewport.width, height: viewport.height } } } : {}
2195
3566
  };
@@ -2206,7 +3577,7 @@ function useStoryRegistry(index, options) {
2206
3577
  const [loaded, setLoaded] = useState(() => /* @__PURE__ */ new Map());
2207
3578
  const [faults, setFaults] = useState(() => /* @__PURE__ */ new Map());
2208
3579
  const [generations, setGenerations] = useState(() => /* @__PURE__ */ new Map());
2209
- const [revisions, setRevisions] = useState(() => /* @__PURE__ */ new Map());
3580
+ const [revisions2, setRevisions] = useState(() => /* @__PURE__ */ new Map());
2210
3581
  const onReady = useCallback((entry, ready) => {
2211
3582
  setReadies((prev) => {
2212
3583
  const held = prev.get(entry.id);
@@ -2329,7 +3700,7 @@ function useStoryRegistry(index, options) {
2329
3700
  const had = loaded.get(entry.id);
2330
3701
  const fault = faults.get(entry.id);
2331
3702
  const generation = generations.get(entry.file) ?? 0;
2332
- const revision = revisions.get(entry.id) ?? 0;
3703
+ const revision = revisions2.get(entry.id) ?? 0;
2333
3704
  const entryKey = stableStringify(entry);
2334
3705
  const answers = previous.books.get(entry.id) ?? createAnswerBook();
2335
3706
  books.set(entry.id, answers);
@@ -2352,7 +3723,7 @@ function useStoryRegistry(index, options) {
2352
3723
  });
2353
3724
  const unchanged = list.length === previous.list.length && list.every((i, n) => i === previous.list[n]);
2354
3725
  return { built, books, list: unchanged ? previous.list : list };
2355
- }, [index, readies, loaded, faults, generations, revisions, frameUrl, globals, setup, importers, onReady, request]);
3726
+ }, [index, readies, loaded, faults, generations, revisions2, frameUrl, globals, setup, importers, onReady, request]);
2356
3727
  useLayoutEffect(() => {
2357
3728
  committed.current = cache;
2358
3729
  }, [cache]);
@@ -2391,7 +3762,14 @@ function useDocumentTitle(title) {
2391
3762
  document.title = title;
2392
3763
  }, [title]);
2393
3764
  }
2394
- function Workshop({ index, frameUrl, importers, setup, changes, config, stories = [], storageKey, storage, dependencies }) {
3765
+ function NoStories({ stories }) {
3766
+ return /* @__PURE__ */ jsxs("div", { className: "fg-empty", children: [
3767
+ /* @__PURE__ */ jsx("p", { className: "fg-empty__title", children: "No stories found" }),
3768
+ 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
3769
+ ] });
3770
+ }
3771
+ function Workshop(props) {
3772
+ const { index, frameUrl, importers, setup, changes, config, stories = [], storageKey, storage, dependencies } = props;
2395
3773
  const declarations = config?.globals ?? NO_DECLARATIONS;
2396
3774
  const title = config?.title ?? DEFAULT_TITLE;
2397
3775
  useDocumentTitle(title);
@@ -2435,30 +3813,31 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2435
3813
  []
2436
3814
  );
2437
3815
  const infoCommand = useMemo(() => infoItem(() => setInfoOpen(true)), []);
3816
+ const showsCssVars = config?.cssVars !== false;
3817
+ const modeGlobal = config?.loupe?.modeGlobal ?? "mode";
3818
+ const [cssVarsChrome2, setCssVarsChrome] = useState([]);
3819
+ const [keysChrome2, setKeysChrome] = useState([]);
2438
3820
  const labChrome = useMemo(
2439
3821
  () => [
2440
3822
  { id: "fg-stories", region: "sidebar", render: (ctx) => /* @__PURE__ */ jsx(StoryTree, { ctx, index }) },
2441
- ...config?.cssVars === false ? [] : [CSS_VARS_SECTION],
3823
+ ...showsCssVars ? cssVarsChrome2 : [],
2442
3824
  A11Y_SECTION,
3825
+ ...keysChrome2,
2443
3826
  ...Object.keys(declarations).length > 0 ? [{ id: "fg-globals", region: "header", render: () => /* @__PURE__ */ jsx(GlobalsToolbar, { declarations }) }] : [],
2444
3827
  { id: "fg-info", region: "header", group: "story", item: infoCommand },
3828
+ { id: "fg-loupe", region: "overlay", render: () => /* @__PURE__ */ jsx(ForgeLoupe, { declarations, modeGlobal }) },
2445
3829
  ...config?.labChrome ?? []
2446
3830
  ],
2447
- [index, infoCommand, declarations, config?.cssVars, config?.labChrome]
3831
+ [index, infoCommand, declarations, showsCssVars, cssVarsChrome2, keysChrome2, config?.labChrome, modeGlobal]
2448
3832
  );
2449
3833
  const [menus, setMenus] = useState();
2450
3834
  const first = index[0];
2451
- if (!first) {
2452
- return /* @__PURE__ */ jsxs("div", { className: "fg-empty", children: [
2453
- /* @__PURE__ */ jsx("p", { className: "fg-empty__title", children: "No stories found" }),
2454
- 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
2455
- ] });
2456
- }
2457
- 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(
3835
+ 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(
2458
3837
  Lab,
2459
3838
  {
2460
3839
  title,
2461
- density: "roomy",
3840
+ density: LAB_DENSITY,
2462
3841
  ...labTheme ? { theme: labTheme } : {},
2463
3842
  instruments,
2464
3843
  defaultInstrument: readRoute() ?? first.id,
@@ -2484,17 +3863,20 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2484
3863
  }
2485
3864
  ),
2486
3865
  /* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues }),
2487
- /* @__PURE__ */ jsx(GlobalsMenus, { declarations, onMenus: setMenus })
3866
+ /* @__PURE__ */ jsx(GlobalsMenus, { declarations, onMenus: setMenus }),
3867
+ showsCssVars ? /* @__PURE__ */ jsx(CssVarsChrome, { onChrome: setCssVarsChrome }) : null,
3868
+ /* @__PURE__ */ jsx(KeysChrome, { onChrome: setKeysChrome })
2488
3869
  ]
2489
3870
  }
2490
- ) }) }) }) }) }) });
3871
+ ) }) }) }) }) }) }) });
2491
3872
  }
2492
3873
  function mountWorkshop({ container, ...props }) {
2493
3874
  const target = container ?? document.getElementById("root");
2494
3875
  if (!target) throw new Error("mountWorkshop: no container given and no #root element");
2495
3876
  const root = createKitRoot(target, props.setup?.icons);
2496
3877
  const changes = createStoryChanges();
2497
- let current = { ...props, changes };
3878
+ const revision = createRevision();
3879
+ let current = { ...props, changes, revision };
2498
3880
  root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
2499
3881
  return {
2500
3882
  setIndex(index) {
@@ -2510,10 +3892,11 @@ function mountWorkshop({ container, ...props }) {
2510
3892
  current = { ...current, dependencies };
2511
3893
  root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
2512
3894
  },
3895
+ revise: () => revision.bump(),
2513
3896
  unmount: () => root.unmount()
2514
3897
  };
2515
3898
  }
2516
3899
 
2517
- export { A11Y_SECTION, A11yPanel, Workshop, mountWorkshop, useStoryRegistry };
3900
+ export { A11Y_SECTION, A11yPanel, Workshop, followEdits, mountWorkshop, useStoryRegistry };
2518
3901
  //# sourceMappingURL=index.js.map
2519
3902
  //# sourceMappingURL=index.js.map