@weasel-js/forge 1.11.1 → 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 +90 -0
- package/dist/{FrameController-Cj_i-Rdk.d.ts → FrameController-CdnaIo4C.d.ts} +1 -1
- 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-BKPDKUDN.js → chunk-ATI7FYJ7.js} +8 -5
- package/dist/chunk-ATI7FYJ7.js.map +1 -0
- package/dist/chunk-ORMMZ5PV.js +59 -0
- package/dist/chunk-ORMMZ5PV.js.map +1 -0
- package/dist/{chunk-JTCUIJPZ.js → chunk-WMGEO5M6.js} +49 -7
- 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-DN6IaNf6.d.ts → config-Mg05Ja9r.d.ts} +18 -1
- package/dist/frame/index.css +11 -0
- package/dist/frame/index.css.map +1 -1
- package/dist/frame/index.d.ts +62 -29
- package/dist/frame/index.js +134 -6
- package/dist/frame/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mountFrame-Ds3QLnBF.d.ts +35 -0
- package/dist/shell/index.css +116 -0
- package/dist/shell/index.css.map +1 -1
- package/dist/shell/index.d.ts +43 -7
- package/dist/shell/index.js +1570 -259
- 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 +1 -1
- package/dist/test/index.js +4 -4
- package/dist/vite/index.js +501 -106
- package/dist/vite/index.js.map +1 -1
- package/package.json +13 -8
- 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-BKPDKUDN.js.map +0 -1
- package/dist/chunk-FZNABWJC.js +0 -282
- package/dist/chunk-FZNABWJC.js.map +0 -1
- package/dist/chunk-JTCUIJPZ.js.map +0 -1
- package/dist/chunk-ZAWXMUGZ.js.map +0 -1
package/dist/shell/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { parseRoute, formatRoute, paramsToKnobs, knobPaths, sameKnob, knobsToParams, PLAYHEAD_PARAM, reservedParams } from '../chunk-LTTEILXY.js';
|
|
2
|
-
import { DEFAULT_TITLE } from '../chunk-
|
|
3
|
-
import { TITLE_LIBRARIES, isIndexId, indexId, indexEntries, librariesFrom, LibrariesContext, createKitRoot, schemaFromDescription, LibraryBadge,
|
|
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-
|
|
10
|
+
import '../chunk-5WS67HMU.js';
|
|
11
11
|
import '../chunk-KEQBGDUZ.js';
|
|
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';
|
|
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';
|
|
@@ -20,17 +20,20 @@ import { Slider } from '@weasel-js/ui/components/Slider';
|
|
|
20
20
|
import { Transport } from '@weasel-js/ui/components/Timeline';
|
|
21
21
|
import { TOKEN_MANIFEST, themeAxes, weaselTheme, THEME_SOURCES, resolveTheme, isColorModePreference, isByAxis } from '@weasel-js/theme';
|
|
22
22
|
import { scale, httpThemeApi } from '@weasel-js/theme/engine';
|
|
23
|
-
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
24
|
import { Button as Button$1 } from '@weasel-js/ui/components/Button';
|
|
25
25
|
import { Checkbox } from '@weasel-js/ui/components/Checkbox';
|
|
26
26
|
import { Icon } from '@weasel-js/ui/components/Icon';
|
|
27
|
+
import { TOKEN_CATEGORIES, tokenCategory } from '@weasel-js/ui/components/TokenPanel';
|
|
27
28
|
import { ToggleBar } from '@weasel-js/ui/components/ToggleBar';
|
|
28
|
-
import { TOKEN_CATEGORIES } from '@weasel-js/ui/components/TokenPanel';
|
|
29
29
|
import { useResolvedColorMode } from '@weasel-js/theme/react';
|
|
30
30
|
import { formatShortcut } from '@weasel-js/ui/components/ToolPalette';
|
|
31
31
|
import { Dialog } from '@weasel-js/ui/components/Dialog';
|
|
32
|
-
import {
|
|
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';
|
|
33
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';
|
|
34
37
|
import { DisclosureMark } from '@weasel-js/ui/components/Disclosure';
|
|
35
38
|
|
|
36
39
|
function pointAt(point, nodes) {
|
|
@@ -126,6 +129,56 @@ var A11Y_SECTION = {
|
|
|
126
129
|
region: "aside",
|
|
127
130
|
item: { title: "Accessibility", body: /* @__PURE__ */ jsx(FocusedA11y, {}) }
|
|
128
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);
|
|
129
182
|
function createStoryChanges() {
|
|
130
183
|
const changes = createEmitter();
|
|
131
184
|
return { subscribe: changes.subscribe, emit: changes.emit };
|
|
@@ -306,7 +359,38 @@ function IndexTrial({ bundle, setup, config }) {
|
|
|
306
359
|
};
|
|
307
360
|
return /* @__PURE__ */ jsx(IndexPage, { env, render: bundle.render });
|
|
308
361
|
};
|
|
309
|
-
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 }) });
|
|
310
394
|
}
|
|
311
395
|
function withPlayhead(schema) {
|
|
312
396
|
return f.schema({ ...schema.nodes, [PLAYHEAD_KEY]: f.custom("forge-playhead", null).hidden() });
|
|
@@ -323,7 +407,10 @@ function documentInstrument(options) {
|
|
|
323
407
|
config,
|
|
324
408
|
defaultConfig: () => config.defaults(),
|
|
325
409
|
initialState: (c) => initialState?.(c) ?? null,
|
|
326
|
-
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 }),
|
|
327
414
|
...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {},
|
|
328
415
|
...timed ? { chrome: [{ id: "fg-transport", region: "status", render: (c) => /* @__PURE__ */ jsx(TrialTransport, { trialId: c.trialId }) }] } : {}
|
|
329
416
|
};
|
|
@@ -363,6 +450,50 @@ function missingInstrument(id, globals = {}) {
|
|
|
363
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}` }) })
|
|
364
451
|
};
|
|
365
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
|
+
}
|
|
366
497
|
var ICONS = {
|
|
367
498
|
color: (
|
|
368
499
|
/* @icon */
|
|
@@ -509,8 +640,8 @@ function applyScaleEdits(definition, edits, selection) {
|
|
|
509
640
|
const write = (slot, value) => {
|
|
510
641
|
let byKey = writes.get(slot.holder);
|
|
511
642
|
if (!byKey) writes.set(slot.holder, byKey = /* @__PURE__ */ new Map());
|
|
512
|
-
const
|
|
513
|
-
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}`);
|
|
514
645
|
byKey.set(slot.key, value);
|
|
515
646
|
};
|
|
516
647
|
try {
|
|
@@ -810,6 +941,15 @@ function standingScale(source, amounts) {
|
|
|
810
941
|
const base = (amounts[at] ?? 0) / (rule.factors[at] ?? 1);
|
|
811
942
|
return { ...seed, base: Math.round(base * 100) / 100, rule };
|
|
812
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
|
+
}
|
|
813
953
|
var NO_GLOBALS = {};
|
|
814
954
|
function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
815
955
|
const trialId = useTrialId();
|
|
@@ -826,6 +966,14 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
|
826
966
|
const themeTokens = useThemeTokens(globals, frame.vars);
|
|
827
967
|
const story = useStoryTokens(frame.vars);
|
|
828
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);
|
|
829
977
|
const query = filter.trim().toLowerCase();
|
|
830
978
|
const shown = tokens.map((token) => {
|
|
831
979
|
const override = overrides[token.name];
|
|
@@ -866,17 +1014,8 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
|
866
1014
|
`scale:${group}`
|
|
867
1015
|
);
|
|
868
1016
|
};
|
|
869
|
-
const unsaved = useMemo(
|
|
870
|
-
|
|
871
|
-
const edit = edited[source.group];
|
|
872
|
-
if (!edit || !source.tokens.some((name2) => overrides[name2] !== void 0)) return [];
|
|
873
|
-
const { name, group, tokens: tokens2 } = source;
|
|
874
|
-
const clean = scaleValues(source, edit).every(([token, value]) => overrides[token] === value);
|
|
875
|
-
return clean ? [{ name, group, tokens: tokens2, edit: { base: edit.base, rule: edit.rule } }] : [];
|
|
876
|
-
}),
|
|
877
|
-
[edited, overrides]
|
|
878
|
-
);
|
|
879
|
-
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 });
|
|
880
1019
|
const onCollapsedChange = useCallback(
|
|
881
1020
|
(category, collapsed) => setFolds((prev) => ({ ...prev, [category]: collapsed })),
|
|
882
1021
|
[setFolds]
|
|
@@ -935,9 +1074,9 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
|
935
1074
|
] })
|
|
936
1075
|
] }),
|
|
937
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,
|
|
938
|
-
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
|
|
939
1078
|
] }),
|
|
940
|
-
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(
|
|
941
1080
|
TokenPanel,
|
|
942
1081
|
{
|
|
943
1082
|
tokens: shown,
|
|
@@ -954,10 +1093,10 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
|
954
1093
|
)
|
|
955
1094
|
);
|
|
956
1095
|
}
|
|
957
|
-
function SaveRow({ pending, saving, notice, onSave }) {
|
|
958
|
-
if (!
|
|
1096
|
+
function SaveRow({ pending: pending2, saving, notice, onSave }) {
|
|
1097
|
+
if (!pending2 && !notice) return null;
|
|
959
1098
|
return /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__save", children: [
|
|
960
|
-
|
|
1099
|
+
pending2 ? /* @__PURE__ */ jsx(Button$1, { variant: "primary", size: "sm", loading: saving, disabled: saving, onClick: onSave, children: "Save to theme" }) : null,
|
|
961
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
|
|
962
1101
|
] });
|
|
963
1102
|
}
|
|
@@ -999,6 +1138,124 @@ function CssVarsChrome({ onChrome }) {
|
|
|
999
1138
|
const [shown, setShown] = usePersistedState(RECORD, false, { scope: "lab" });
|
|
1000
1139
|
const chrome = useMemo(() => cssVarsChrome(shown, () => setShown((was) => !was)), [shown, setShown]);
|
|
1001
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]);
|
|
1002
1259
|
return null;
|
|
1003
1260
|
}
|
|
1004
1261
|
var KEYS = { kind: "key", key: "i", mods: { mod: true } };
|
|
@@ -1202,6 +1459,1010 @@ function createFramePool(frameUrl, size = 2) {
|
|
|
1202
1459
|
};
|
|
1203
1460
|
}
|
|
1204
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
|
+
}
|
|
1205
2466
|
|
|
1206
2467
|
// src/shell/routedTrial.ts
|
|
1207
2468
|
function routedTrial(trials, focusedTrialId, route) {
|
|
@@ -1253,9 +2514,9 @@ function RoutePlayhead() {
|
|
|
1253
2514
|
if (!trial || route === null) return null;
|
|
1254
2515
|
return /* @__PURE__ */ jsx(PlayheadSync, { trialId: trial.id, route, appliedRef: applied }, `${trial.id} ${route}`);
|
|
1255
2516
|
}
|
|
1256
|
-
var
|
|
2517
|
+
var RECORD3 = "fg-globals";
|
|
1257
2518
|
function useLabGlobals(declarations) {
|
|
1258
|
-
const [stored, setStored] = usePersistedState(
|
|
2519
|
+
const [stored, setStored] = usePersistedState(RECORD3, null, { scope: "lab" });
|
|
1259
2520
|
const values = useMemo(() => labGlobals(declarations, stored), [declarations, stored]);
|
|
1260
2521
|
const set = useCallback(
|
|
1261
2522
|
(key, value) => setStored((prev) => ({ ...labGlobals(declarations, prev), [key]: value })),
|
|
@@ -1668,13 +2929,13 @@ function StoryTree({ ctx, index }) {
|
|
|
1668
2929
|
const filtering = query.trim() !== "";
|
|
1669
2930
|
const isOpen = (path) => filtering || (folds[path] ?? routeAncestors.has(path));
|
|
1670
2931
|
const rows = [];
|
|
1671
|
-
const
|
|
2932
|
+
const collect2 = (nodes, parent) => {
|
|
1672
2933
|
for (const node of nodes) {
|
|
1673
2934
|
rows.push({ key: keyOf(node), node, parent });
|
|
1674
|
-
if (node.kind === "folder" && isOpen(node.path))
|
|
2935
|
+
if (node.kind === "folder" && isOpen(node.path)) collect2(node.children, keyOf(node));
|
|
1675
2936
|
}
|
|
1676
2937
|
};
|
|
1677
|
-
|
|
2938
|
+
collect2(shown, null);
|
|
1678
2939
|
const active = rows.some((row) => row.key === focusKey) ? focusKey : rows[0]?.key ?? null;
|
|
1679
2940
|
const openIds = new Set(ctx.trials.map((trial) => trial.instrumentName));
|
|
1680
2941
|
const setOpen = (path, open) => {
|
|
@@ -1953,6 +3214,171 @@ function createAnswerBook() {
|
|
|
1953
3214
|
|
|
1954
3215
|
// src/shell/readyKey.ts
|
|
1955
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
|
+
}
|
|
1956
3382
|
var kept = /* @__PURE__ */ new Map();
|
|
1957
3383
|
var shelf = null;
|
|
1958
3384
|
function shelfElement() {
|
|
@@ -2001,46 +3427,56 @@ function useKeptFrame(options) {
|
|
|
2001
3427
|
}, [latest]);
|
|
2002
3428
|
return useCallback(() => handedOn.current, []);
|
|
2003
3429
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
if (!
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
if (
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
current.inputs += 1;
|
|
2031
|
-
}
|
|
2032
|
-
if (globals !== sent.globals && stableStringify(globals) !== stableStringify(sent.globals)) {
|
|
2033
|
-
sent.globals = globals;
|
|
2034
|
-
current.channel.send({ type: "globals", globals });
|
|
2035
|
-
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);
|
|
2036
3456
|
}
|
|
2037
|
-
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
|
+
};
|
|
2038
3467
|
}
|
|
3468
|
+
var NO_ARGS = {};
|
|
2039
3469
|
function FrameView(props) {
|
|
2040
|
-
const { entry, frameUrl, answers, descriptionKey, ctx } = props;
|
|
3470
|
+
const { entry, frameUrl, answers, descriptionKey, ctx: trialCtx, copy } = props;
|
|
2041
3471
|
const labGlobals2 = useContext(StoryGlobalsContext);
|
|
2042
|
-
const pins =
|
|
2043
|
-
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
|
+
);
|
|
2044
3480
|
const trialId = useContext(TrialIdContext);
|
|
2045
3481
|
const frames = useContext(TrialFramesContext);
|
|
2046
3482
|
const [overrides] = useCssOverrides();
|
|
@@ -2052,185 +3488,49 @@ function FrameView(props) {
|
|
|
2052
3488
|
const hostRef = useRef(null);
|
|
2053
3489
|
const slotRef = useRef(null);
|
|
2054
3490
|
const inView = useNearViewport(hostRef, IN_VIEW_MARGIN);
|
|
2055
|
-
const
|
|
2056
|
-
const latestRef = useLatest({ ...props, globals, overrides, pool, lab, title });
|
|
3491
|
+
const latestRef = useLatest({ ...props, ctx, globals, overrides, pool, lab, title });
|
|
2057
3492
|
const [fault, setFault] = useState(null);
|
|
2058
|
-
const [
|
|
2059
|
-
const
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
for (const [name, value] of Object.entries(latestRef.current.overrides)) {
|
|
2073
|
-
current.channel.send({ type: "vars.set", name, value });
|
|
2074
|
-
}
|
|
2075
|
-
const key = readyKey(msg);
|
|
2076
|
-
current.awaiting = key;
|
|
2077
|
-
onReady(latestRef.current.entry, msg);
|
|
2078
|
-
if (current.awaiting === key && key === latestRef.current.descriptionKey) init(current);
|
|
2079
|
-
break;
|
|
2080
|
-
}
|
|
2081
|
-
case "rendered":
|
|
2082
|
-
setPending(false);
|
|
2083
|
-
current.hasRendered = true;
|
|
2084
|
-
for (const ask of current.queued.splice(0)) ask();
|
|
2085
|
-
break;
|
|
2086
|
-
case "setConfig":
|
|
2087
|
-
if (!isGlobalsPath(msg.path)) live.setConfig(msg.path, msg.value);
|
|
2088
|
-
break;
|
|
2089
|
-
case "setState":
|
|
2090
|
-
if (current.sent) current.sent.state = msg.state;
|
|
2091
|
-
live.setState(msg.state);
|
|
2092
|
-
break;
|
|
2093
|
-
case "answers":
|
|
2094
|
-
answers2.record(msg.answers);
|
|
2095
|
-
break;
|
|
2096
|
-
case "vars":
|
|
2097
|
-
if (trialId) frames?.report(trialId, msg.vars);
|
|
2098
|
-
break;
|
|
2099
|
-
case "a11y": {
|
|
2100
|
-
const outcome = msg.ok ? { ok: true, report: msg.report } : { ok: false, message: msg.message };
|
|
2101
|
-
if (trialId) frames?.reportA11y(trialId, outcome);
|
|
2102
|
-
current.pending.get(msg.id)?.(outcome);
|
|
2103
|
-
current.pending.delete(msg.id);
|
|
2104
|
-
break;
|
|
2105
|
-
}
|
|
2106
|
-
case "open": {
|
|
2107
|
-
const { lab: liveLab } = latestRef.current;
|
|
2108
|
-
if (!trialId || !liveLab.instruments.some((instrument) => instrument.name === msg.id)) break;
|
|
2109
|
-
liveLab.swapTrial(trialId, msg.id);
|
|
2110
|
-
setRoute(msg.id, { inPlace: true });
|
|
2111
|
-
break;
|
|
2112
|
-
}
|
|
2113
|
-
case "fault":
|
|
2114
|
-
if (msg.phase === "render" && msg.seq !== void 0 && msg.seq < current.inputs) break;
|
|
2115
|
-
clearTimeout(current.timer);
|
|
2116
|
-
setFault({ phase: msg.phase, message: msg.message });
|
|
2117
|
-
break;
|
|
2118
|
-
}
|
|
2119
|
-
};
|
|
2120
|
-
const connect = (frame) => {
|
|
2121
|
-
closeLink(link);
|
|
2122
|
-
setPending(true);
|
|
2123
|
-
const target = frame.contentWindow;
|
|
2124
|
-
if (!target) return;
|
|
2125
|
-
const { port1, port2 } = new MessageChannel();
|
|
2126
|
-
const timer = setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), START_TIMEOUT_MS);
|
|
2127
|
-
const channel = openChannel(port1, {
|
|
2128
|
-
onMismatch: (mismatch) => {
|
|
2129
|
-
clearTimeout(timer);
|
|
2130
|
-
setFault({ phase: "protocol", message: mismatchMessage(mismatch) });
|
|
2131
|
-
}
|
|
2132
|
-
});
|
|
2133
|
-
const waiting = /* @__PURE__ */ new Map();
|
|
2134
|
-
const audit = () => new Promise((resolve) => {
|
|
2135
|
-
const id = `a11y-${requests += 1}`;
|
|
2136
|
-
waiting.set(id, resolve);
|
|
2137
|
-
const ask = () => channel.send({ type: "a11y.run", id });
|
|
2138
|
-
if (current.hasRendered) ask();
|
|
2139
|
-
else current.queued.push(ask);
|
|
2140
|
-
});
|
|
2141
|
-
const disconnect = frames && trialId ? frames.connect(trialId, { send: channel.send, audit }) : () => {
|
|
2142
|
-
};
|
|
2143
|
-
const current = {
|
|
2144
|
-
channel,
|
|
2145
|
-
deliver: (msg) => receive(current, msg),
|
|
2146
|
-
timer,
|
|
2147
|
-
awaiting: null,
|
|
2148
|
-
sent: null,
|
|
2149
|
-
inputs: 0,
|
|
2150
|
-
disconnect,
|
|
2151
|
-
pending: waiting,
|
|
2152
|
-
hasRendered: false,
|
|
2153
|
-
queued: []
|
|
2154
|
-
};
|
|
2155
|
-
link.current = current;
|
|
2156
|
-
channel.on((msg) => current.deliver(msg));
|
|
2157
|
-
target.postMessage({ type: PORT_HANDOFF, id: latestRef.current.entry.id }, location.origin, [
|
|
2158
|
-
port2
|
|
2159
|
-
]);
|
|
2160
|
-
};
|
|
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
|
+
});
|
|
2161
3507
|
const connectRef = useLatest(connect);
|
|
2162
3508
|
const handedOn = useKeptFrame({
|
|
2163
|
-
key: trialId ? `${trialId} ${src}` : null,
|
|
3509
|
+
key: trialId && !copy ? `${trialId} ${src}` : null,
|
|
2164
3510
|
slot: slotRef,
|
|
2165
3511
|
frame: iframeRef,
|
|
2166
3512
|
link,
|
|
2167
|
-
onAdopt:
|
|
2168
|
-
adopted.deliver = (msg) => receive(adopted, msg);
|
|
2169
|
-
setPending(!adopted.hasRendered);
|
|
2170
|
-
},
|
|
3513
|
+
onAdopt: adopt2,
|
|
2171
3514
|
onDrop: () => closeLink(link)
|
|
2172
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
|
+
);
|
|
2173
3520
|
useEffect(() => {
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
const adopted = iframeRef.current;
|
|
2177
|
-
const claimed = adopted ? null : latestRef.current.pool?.claim() ?? null;
|
|
2178
|
-
const frame = adopted ?? claimed ?? document.createElement("iframe");
|
|
2179
|
-
frame.className = "fg-frame-view";
|
|
2180
|
-
frame.title = latestRef.current.title;
|
|
2181
|
-
frame.removeAttribute("tabindex");
|
|
2182
|
-
frame.toggleAttribute("data-pending", true);
|
|
2183
|
-
iframeRef.current = frame;
|
|
2184
|
-
const unheard = adopted || claimed ? void 0 : setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), HELLO_TIMEOUT_MS);
|
|
2185
|
-
const onMessage = (event) => {
|
|
2186
|
-
if (event.source !== frame.contentWindow || event.origin !== location.origin) return;
|
|
2187
|
-
if (event.data?.type !== FRAME_HELLO) return;
|
|
2188
|
-
clearTimeout(unheard);
|
|
2189
|
-
connectRef.current(frame);
|
|
2190
|
-
};
|
|
2191
|
-
window.addEventListener("message", onMessage);
|
|
2192
|
-
if (claimed) {
|
|
2193
|
-
moveFrame(slot, claimed);
|
|
2194
|
-
connectRef.current(claimed);
|
|
2195
|
-
} else if (!adopted) {
|
|
2196
|
-
frame.src = src;
|
|
2197
|
-
slot.append(frame);
|
|
2198
|
-
}
|
|
2199
|
-
return () => {
|
|
2200
|
-
window.removeEventListener("message", onMessage);
|
|
2201
|
-
clearTimeout(unheard);
|
|
2202
|
-
if (handedOn()) return;
|
|
2203
|
-
closeLink(link);
|
|
2204
|
-
frame.remove();
|
|
2205
|
-
iframeRef.current = null;
|
|
2206
|
-
setPending(true);
|
|
2207
|
-
setFault(null);
|
|
2208
|
-
};
|
|
2209
|
-
}, [inView, src, connectRef, latestRef, handedOn]);
|
|
2210
|
-
useEffect(() => {
|
|
2211
|
-
iframeRef.current?.toggleAttribute("data-pending", pending && !fault);
|
|
2212
|
-
}, [pending, fault, inView, src]);
|
|
3521
|
+
iframeRef.current?.toggleAttribute("data-pending", pending2 && !fault);
|
|
3522
|
+
}, [pending2, fault, inView, src]);
|
|
2213
3523
|
useEffect(() => {
|
|
2214
3524
|
if (iframeRef.current) iframeRef.current.title = title;
|
|
2215
3525
|
}, [title]);
|
|
2216
|
-
useEffect(() => answers.hold(ctx.config), [answers, ctx.config]);
|
|
2217
|
-
|
|
2218
|
-
const current = link.current;
|
|
2219
|
-
if (!current) return;
|
|
2220
|
-
if (current.awaiting !== null) {
|
|
2221
|
-
if (current.awaiting === descriptionKey) init(current);
|
|
2222
|
-
return;
|
|
2223
|
-
}
|
|
2224
|
-
if (current.sent && sendInputs(current, current.sent, { config: ctx.config, state: ctx.state, globals })) {
|
|
2225
|
-
setFault((shown) => shown?.phase === "render" ? null : shown);
|
|
2226
|
-
}
|
|
2227
|
-
}, [ctx.config, ctx.state, globals, descriptionKey, init]);
|
|
2228
|
-
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: [
|
|
2229
3528
|
/* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
|
|
2230
3529
|
fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
|
|
2231
3530
|
fault.phase ? /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: fault.phase }) : null,
|
|
2232
3531
|
/* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
|
|
2233
|
-
] }) : null
|
|
3532
|
+
] }) : null,
|
|
3533
|
+
copy ? null : /* @__PURE__ */ jsx(LiveFrameCopies, { trialId, trialConfig: trialCtx.config, children: (lensCopy) => /* @__PURE__ */ jsx(FrameView, { ...props, copy: lensCopy }) })
|
|
2234
3534
|
] });
|
|
2235
3535
|
}
|
|
2236
3536
|
function storyInstrument(options) {
|
|
@@ -2247,17 +3547,20 @@ function storyInstrument(options) {
|
|
|
2247
3547
|
config,
|
|
2248
3548
|
defaultConfig: () => config.defaults(),
|
|
2249
3549
|
initialState: () => null,
|
|
2250
|
-
render: (ctx) => /* @__PURE__ */
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
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
|
+
] }),
|
|
2261
3564
|
// Taken from the index until the frame reports, so the trial does not move the frame into a stage and reload it.
|
|
2262
3565
|
...viewport ? { stage: { size: { width: viewport.width, height: viewport.height } } } : {}
|
|
2263
3566
|
};
|
|
@@ -2274,7 +3577,7 @@ function useStoryRegistry(index, options) {
|
|
|
2274
3577
|
const [loaded, setLoaded] = useState(() => /* @__PURE__ */ new Map());
|
|
2275
3578
|
const [faults, setFaults] = useState(() => /* @__PURE__ */ new Map());
|
|
2276
3579
|
const [generations, setGenerations] = useState(() => /* @__PURE__ */ new Map());
|
|
2277
|
-
const [
|
|
3580
|
+
const [revisions2, setRevisions] = useState(() => /* @__PURE__ */ new Map());
|
|
2278
3581
|
const onReady = useCallback((entry, ready) => {
|
|
2279
3582
|
setReadies((prev) => {
|
|
2280
3583
|
const held = prev.get(entry.id);
|
|
@@ -2397,7 +3700,7 @@ function useStoryRegistry(index, options) {
|
|
|
2397
3700
|
const had = loaded.get(entry.id);
|
|
2398
3701
|
const fault = faults.get(entry.id);
|
|
2399
3702
|
const generation = generations.get(entry.file) ?? 0;
|
|
2400
|
-
const revision =
|
|
3703
|
+
const revision = revisions2.get(entry.id) ?? 0;
|
|
2401
3704
|
const entryKey = stableStringify(entry);
|
|
2402
3705
|
const answers = previous.books.get(entry.id) ?? createAnswerBook();
|
|
2403
3706
|
books.set(entry.id, answers);
|
|
@@ -2420,7 +3723,7 @@ function useStoryRegistry(index, options) {
|
|
|
2420
3723
|
});
|
|
2421
3724
|
const unchanged = list.length === previous.list.length && list.every((i, n) => i === previous.list[n]);
|
|
2422
3725
|
return { built, books, list: unchanged ? previous.list : list };
|
|
2423
|
-
}, [index, readies, loaded, faults, generations,
|
|
3726
|
+
}, [index, readies, loaded, faults, generations, revisions2, frameUrl, globals, setup, importers, onReady, request]);
|
|
2424
3727
|
useLayoutEffect(() => {
|
|
2425
3728
|
committed.current = cache;
|
|
2426
3729
|
}, [cache]);
|
|
@@ -2465,7 +3768,8 @@ function NoStories({ stories }) {
|
|
|
2465
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
|
|
2466
3769
|
] });
|
|
2467
3770
|
}
|
|
2468
|
-
function Workshop(
|
|
3771
|
+
function Workshop(props) {
|
|
3772
|
+
const { index, frameUrl, importers, setup, changes, config, stories = [], storageKey, storage, dependencies } = props;
|
|
2469
3773
|
const declarations = config?.globals ?? NO_DECLARATIONS;
|
|
2470
3774
|
const title = config?.title ?? DEFAULT_TITLE;
|
|
2471
3775
|
useDocumentTitle(title);
|
|
@@ -2510,26 +3814,30 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
|
|
|
2510
3814
|
);
|
|
2511
3815
|
const infoCommand = useMemo(() => infoItem(() => setInfoOpen(true)), []);
|
|
2512
3816
|
const showsCssVars = config?.cssVars !== false;
|
|
3817
|
+
const modeGlobal = config?.loupe?.modeGlobal ?? "mode";
|
|
2513
3818
|
const [cssVarsChrome2, setCssVarsChrome] = useState([]);
|
|
3819
|
+
const [keysChrome2, setKeysChrome] = useState([]);
|
|
2514
3820
|
const labChrome = useMemo(
|
|
2515
3821
|
() => [
|
|
2516
3822
|
{ id: "fg-stories", region: "sidebar", render: (ctx) => /* @__PURE__ */ jsx(StoryTree, { ctx, index }) },
|
|
2517
3823
|
...showsCssVars ? cssVarsChrome2 : [],
|
|
2518
3824
|
A11Y_SECTION,
|
|
3825
|
+
...keysChrome2,
|
|
2519
3826
|
...Object.keys(declarations).length > 0 ? [{ id: "fg-globals", region: "header", render: () => /* @__PURE__ */ jsx(GlobalsToolbar, { declarations }) }] : [],
|
|
2520
3827
|
{ id: "fg-info", region: "header", group: "story", item: infoCommand },
|
|
3828
|
+
{ id: "fg-loupe", region: "overlay", render: () => /* @__PURE__ */ jsx(ForgeLoupe, { declarations, modeGlobal }) },
|
|
2521
3829
|
...config?.labChrome ?? []
|
|
2522
3830
|
],
|
|
2523
|
-
[index, infoCommand, declarations, showsCssVars, cssVarsChrome2, config?.labChrome]
|
|
3831
|
+
[index, infoCommand, declarations, showsCssVars, cssVarsChrome2, keysChrome2, config?.labChrome, modeGlobal]
|
|
2524
3832
|
);
|
|
2525
3833
|
const [menus, setMenus] = useState();
|
|
2526
3834
|
const first = index[0];
|
|
2527
3835
|
if (!first) return /* @__PURE__ */ jsx(NoStories, { stories });
|
|
2528
|
-
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(
|
|
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(
|
|
2529
3837
|
Lab,
|
|
2530
3838
|
{
|
|
2531
3839
|
title,
|
|
2532
|
-
density:
|
|
3840
|
+
density: LAB_DENSITY,
|
|
2533
3841
|
...labTheme ? { theme: labTheme } : {},
|
|
2534
3842
|
instruments,
|
|
2535
3843
|
defaultInstrument: readRoute() ?? first.id,
|
|
@@ -2556,17 +3864,19 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
|
|
|
2556
3864
|
),
|
|
2557
3865
|
/* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues }),
|
|
2558
3866
|
/* @__PURE__ */ jsx(GlobalsMenus, { declarations, onMenus: setMenus }),
|
|
2559
|
-
showsCssVars ? /* @__PURE__ */ jsx(CssVarsChrome, { onChrome: setCssVarsChrome }) : null
|
|
3867
|
+
showsCssVars ? /* @__PURE__ */ jsx(CssVarsChrome, { onChrome: setCssVarsChrome }) : null,
|
|
3868
|
+
/* @__PURE__ */ jsx(KeysChrome, { onChrome: setKeysChrome })
|
|
2560
3869
|
]
|
|
2561
3870
|
}
|
|
2562
|
-
) }) }) }) }) }) });
|
|
3871
|
+
) }) }) }) }) }) }) });
|
|
2563
3872
|
}
|
|
2564
3873
|
function mountWorkshop({ container, ...props }) {
|
|
2565
3874
|
const target = container ?? document.getElementById("root");
|
|
2566
3875
|
if (!target) throw new Error("mountWorkshop: no container given and no #root element");
|
|
2567
3876
|
const root = createKitRoot(target, props.setup?.icons);
|
|
2568
3877
|
const changes = createStoryChanges();
|
|
2569
|
-
|
|
3878
|
+
const revision = createRevision();
|
|
3879
|
+
let current = { ...props, changes, revision };
|
|
2570
3880
|
root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
|
|
2571
3881
|
return {
|
|
2572
3882
|
setIndex(index) {
|
|
@@ -2582,10 +3892,11 @@ function mountWorkshop({ container, ...props }) {
|
|
|
2582
3892
|
current = { ...current, dependencies };
|
|
2583
3893
|
root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
|
|
2584
3894
|
},
|
|
3895
|
+
revise: () => revision.bump(),
|
|
2585
3896
|
unmount: () => root.unmount()
|
|
2586
3897
|
};
|
|
2587
3898
|
}
|
|
2588
3899
|
|
|
2589
|
-
export { A11Y_SECTION, A11yPanel, Workshop, mountWorkshop, useStoryRegistry };
|
|
3900
|
+
export { A11Y_SECTION, A11yPanel, Workshop, followEdits, mountWorkshop, useStoryRegistry };
|
|
2590
3901
|
//# sourceMappingURL=index.js.map
|
|
2591
3902
|
//# sourceMappingURL=index.js.map
|