@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.
- package/README.md +92 -1
- package/dist/{FrameController-BRhShqut.d.ts → FrameController-CdnaIo4C.d.ts} +18 -10
- package/dist/{chunk-7BQPHMA2.js → chunk-3BAM6M26.js} +2 -2
- package/dist/chunk-3BAM6M26.js.map +1 -0
- package/dist/{chunk-ZAWXMUGZ.js → chunk-5WS67HMU.js} +8 -3
- package/dist/chunk-5WS67HMU.js.map +1 -0
- package/dist/{chunk-R5F44ECM.js → chunk-ATI7FYJ7.js} +18 -14
- package/dist/chunk-ATI7FYJ7.js.map +1 -0
- package/dist/{chunk-2OJAV522.js → chunk-GH5RE5X5.js} +2 -2
- package/dist/chunk-GH5RE5X5.js.map +1 -0
- package/dist/{chunk-SQ4KCIEH.js → chunk-KEQBGDUZ.js} +3 -3
- package/dist/{chunk-SQ4KCIEH.js.map → chunk-KEQBGDUZ.js.map} +1 -1
- package/dist/{chunk-DPTECNYC.js → chunk-LTTEILXY.js} +3 -3
- package/dist/{chunk-DPTECNYC.js.map → chunk-LTTEILXY.js.map} +1 -1
- package/dist/chunk-ORMMZ5PV.js +59 -0
- package/dist/chunk-ORMMZ5PV.js.map +1 -0
- package/dist/{chunk-NVNTQ4AT.js → chunk-WMGEO5M6.js} +133 -9
- package/dist/chunk-WMGEO5M6.js.map +1 -0
- package/dist/chunk-YDLRSEUN.js +501 -0
- package/dist/chunk-YDLRSEUN.js.map +1 -0
- package/dist/{config-BwScQukB.d.ts → config-Mg05Ja9r.d.ts} +20 -3
- package/dist/csf/preview-api.js +2 -2
- package/dist/frame/index.css +12 -0
- package/dist/frame/index.css.map +1 -1
- package/dist/frame/index.d.ts +62 -29
- package/dist/frame/index.js +142 -15
- package/dist/frame/index.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/mountFrame-Ds3QLnBF.d.ts +35 -0
- package/dist/shell/index.css +131 -6
- package/dist/shell/index.css.map +1 -1
- package/dist/shell/index.d.ts +44 -16
- package/dist/shell/index.js +1663 -280
- package/dist/shell/index.js.map +1 -1
- package/dist/test/index.css +58 -0
- package/dist/test/index.css.map +1 -0
- package/dist/test/index.d.ts +2 -4
- package/dist/test/index.js +11 -13
- package/dist/test/index.js.map +1 -1
- package/dist/vite/index.js +515 -111
- package/dist/vite/index.js.map +1 -1
- package/package.json +13 -8
- package/dist/chunk-2OJAV522.js.map +0 -1
- package/dist/chunk-5DXUNLV2.js +0 -6
- package/dist/chunk-5DXUNLV2.js.map +0 -1
- package/dist/chunk-7BQPHMA2.js.map +0 -1
- package/dist/chunk-J3GYSNCK.js +0 -285
- package/dist/chunk-J3GYSNCK.js.map +0 -1
- package/dist/chunk-NVNTQ4AT.js.map +0 -1
- package/dist/chunk-R5F44ECM.js.map +0 -1
- package/dist/chunk-ZAWXMUGZ.js.map +0 -1
package/dist/shell/index.js
CHANGED
|
@@ -1,38 +1,58 @@
|
|
|
1
|
-
import { parseRoute, formatRoute, paramsToKnobs, knobPaths, sameKnob, knobsToParams, PLAYHEAD_PARAM, reservedParams } from '../chunk-
|
|
2
|
-
import { DEFAULT_TITLE } from '../chunk-
|
|
3
|
-
import { TITLE_LIBRARIES, isIndexId, indexId, indexEntries, librariesFrom, LibrariesContext, createKitRoot, schemaFromDescription, LibraryBadge,
|
|
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-
|
|
6
|
-
export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-
|
|
7
|
-
import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, PLAYHEAD_KEY, labOptions, isGlobalsPath } from '../chunk-
|
|
8
|
-
import { loadStories, indexRenderOf, useNearViewport, IN_VIEW_MARGIN } from '../chunk-
|
|
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-
|
|
11
|
-
import '../chunk-
|
|
12
|
-
import { stableStringify,
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
|
27
|
-
|
|
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__ */
|
|
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__ */
|
|
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
|
|
489
|
-
if (
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
2517
|
+
var RECORD3 = "fg-globals";
|
|
1210
2518
|
function useLabGlobals(declarations) {
|
|
1211
|
-
const [stored, setStored] = usePersistedState(
|
|
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
|
|
2837
|
+
const parent = parentOf(row, byPath);
|
|
1518
2838
|
const title = row.path.split("/");
|
|
1519
|
-
|
|
1520
|
-
let
|
|
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
|
-
|
|
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:
|
|
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
|
|
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))
|
|
2935
|
+
if (node.kind === "folder" && isOpen(node.path)) collect2(node.children, keyOf(node));
|
|
1607
2936
|
}
|
|
1608
2937
|
};
|
|
1609
|
-
|
|
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
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
if (!
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
if (
|
|
1960
|
-
|
|
1961
|
-
|
|
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
|
|
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 =
|
|
1975
|
-
const
|
|
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
|
|
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 [
|
|
1991
|
-
const
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
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:
|
|
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
|
-
|
|
2107
|
-
|
|
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
|
-
|
|
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__ */
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
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 [
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
-
...
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|