@weasel-js/forge 1.11.1 → 1.13.0-rc.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 +110 -5
- package/dist/{FrameController-Cj_i-Rdk.d.ts → FrameController-Tv3jrHyT.d.ts} +3 -3
- package/dist/{chunk-JTCUIJPZ.js → chunk-35DOPXHZ.js} +119 -10
- package/dist/chunk-35DOPXHZ.js.map +1 -0
- 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-BUXUWAI4.js} +45 -60
- package/dist/chunk-BUXUWAI4.js.map +1 -0
- package/dist/chunk-ORMMZ5PV.js +59 -0
- package/dist/chunk-ORMMZ5PV.js.map +1 -0
- package/dist/chunk-RKQ277P5.js +501 -0
- package/dist/chunk-RKQ277P5.js.map +1 -0
- package/dist/chunk-STF6NQ5N.js +60 -0
- package/dist/chunk-STF6NQ5N.js.map +1 -0
- package/dist/chunk-VUPN73JP.js +16 -0
- package/dist/chunk-VUPN73JP.js.map +1 -0
- package/dist/{config-DN6IaNf6.d.ts → config-Bd6FVlh7.d.ts} +22 -5
- package/dist/frame/index.css +37 -0
- package/dist/frame/index.css.map +1 -1
- package/dist/frame/index.d.ts +62 -29
- package/dist/frame/index.js +138 -8
- package/dist/frame/index.js.map +1 -1
- package/dist/index.d.ts +28 -5
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mountFrame-FdJDb3TB.d.ts +35 -0
- package/dist/shell/index.css +147 -2
- package/dist/shell/index.css.map +1 -1
- package/dist/shell/index.d.ts +44 -8
- package/dist/shell/index.js +1614 -277
- 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 +571 -121
- 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-WEQSLPKT.js +0 -9
- package/dist/chunk-WEQSLPKT.js.map +0 -1
- package/dist/chunk-ZAWXMUGZ.js.map +0 -1
package/dist/shell/index.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
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,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
2
|
+
import { DEFAULT_TITLE, CAPTURE_PATH, messageOf, MIN_DPR, MAX_DPR } from '../chunk-ORMMZ5PV.js';
|
|
3
|
+
import { TITLE_LIBRARIES, librariesFrom, LibrariesContext, LAB_DENSITY, createKitRoot, schemaFromDescription, LibraryBadge, Description, IndexPage, LabBadge, openChannel } from '../chunk-BUXUWAI4.js';
|
|
4
|
+
import { isIndexId, indexId, indexEntries } from '../chunk-STF6NQ5N.js';
|
|
5
|
+
import { taggedIds, isGallery, isLab } from '../chunk-VUPN73JP.js';
|
|
6
|
+
import { useTrialFrame, createTrialFrames, createTrialClocks, StoryGlobalsContext, TrialFramesContext, TrialClocksContext, StoryTrial, LiveLensesContext, trialAt, describeLens, createLiveLenses, useCopyConfig, useCssOverrides, LiveFrameCopies, useTrialClock, TrialHost, LiveCopies, resolveContextOf, resolveComparison, withArgs, scrollerOf } from '../chunk-RKQ277P5.js';
|
|
7
|
+
export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-RKQ277P5.js';
|
|
8
|
+
import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, PLAYHEAD_KEY, labOptions, isForgePath, isGlobalsPath } from '../chunk-3BAM6M26.js';
|
|
9
|
+
import { loadStories, indexRenderOf, useNearViewport, IN_VIEW_MARGIN, varRefs, cssRulesOf } from '../chunk-35DOPXHZ.js';
|
|
9
10
|
import '../chunk-L5XM5VKB.js';
|
|
10
|
-
import '../chunk-
|
|
11
|
+
import '../chunk-5WS67HMU.js';
|
|
11
12
|
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';
|
|
13
|
+
import { stableStringify, FRAME_HELLO, PORT_HANDOFF, PROTOCOL_VERSION } from '../chunk-GH5RE5X5.js';
|
|
14
|
+
import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, useTrialState, ToggleBar as ToggleBar$1, Input, TokenPanel, useLabEntries, inferTokenType, refitScale } from '@weasel-js/labkit';
|
|
14
15
|
import num from '@weasel-js/theme/numeric.module.css';
|
|
15
|
-
import { createContext, useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect, useId, useContext, Fragment as Fragment$1
|
|
16
|
+
import { createContext, useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect, useId, useContext, useReducer, useSyncExternalStore, Fragment as Fragment$1 } from 'react';
|
|
16
17
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
17
18
|
import { createEmitter } from '@weasel-js/registry';
|
|
18
19
|
import { f, valueAtPath, isConfigBranch } from '@weasel-js/labkit/config';
|
|
@@ -20,17 +21,20 @@ import { Slider } from '@weasel-js/ui/components/Slider';
|
|
|
20
21
|
import { Transport } from '@weasel-js/ui/components/Timeline';
|
|
21
22
|
import { TOKEN_MANIFEST, themeAxes, weaselTheme, THEME_SOURCES, resolveTheme, isColorModePreference, isByAxis } from '@weasel-js/theme';
|
|
22
23
|
import { scale, httpThemeApi } from '@weasel-js/theme/engine';
|
|
23
|
-
import { useLatest, historyKey, createHistory,
|
|
24
|
+
import { useLatest, useActionsRegistry, effectivePose, useMenuHost, useMenus, sceneToAdapter, parseColor, contrastRatio, wcagLevels, historyKey, isPlainObject, createHistory, keymapShortcuts, actionBindings, keySpecShortcut, keymapLeaderId, swapOp } from '@weasel-js/core';
|
|
24
25
|
import { Button as Button$1 } from '@weasel-js/ui/components/Button';
|
|
25
26
|
import { Checkbox } from '@weasel-js/ui/components/Checkbox';
|
|
26
27
|
import { Icon } from '@weasel-js/ui/components/Icon';
|
|
28
|
+
import { TOKEN_CATEGORIES, tokenCategory } from '@weasel-js/ui/components/TokenPanel';
|
|
27
29
|
import { ToggleBar } from '@weasel-js/ui/components/ToggleBar';
|
|
28
|
-
import { TOKEN_CATEGORIES } from '@weasel-js/ui/components/TokenPanel';
|
|
29
30
|
import { useResolvedColorMode } from '@weasel-js/theme/react';
|
|
30
31
|
import { formatShortcut } from '@weasel-js/ui/components/ToolPalette';
|
|
31
32
|
import { Dialog } from '@weasel-js/ui/components/Dialog';
|
|
32
|
-
import {
|
|
33
|
+
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';
|
|
34
|
+
import { KeymapHint } from '@weasel-js/ui/components/KeymapHint';
|
|
33
35
|
import { pref } from '@weasel-js/prefs';
|
|
36
|
+
import { splitTopLevel, specificity, compareSpecificity } from '@weasel-js/svg';
|
|
37
|
+
import { Callout } from '@weasel-js/ui/components/Callout';
|
|
34
38
|
import { DisclosureMark } from '@weasel-js/ui/components/Disclosure';
|
|
35
39
|
|
|
36
40
|
function pointAt(point, nodes) {
|
|
@@ -89,7 +93,7 @@ function A11yPanel() {
|
|
|
89
93
|
};
|
|
90
94
|
const outcome = frame.a11y;
|
|
91
95
|
return /* @__PURE__ */ jsxs("div", { className: "fg-a11y", children: [
|
|
92
|
-
/* @__PURE__ */ jsx("div", { className: "fg-a11y__controls", children: /* @__PURE__ */ jsx(Button, { size: "sm",
|
|
96
|
+
/* @__PURE__ */ jsx("div", { className: "fg-a11y__controls", children: /* @__PURE__ */ jsx(Button, { size: "sm", onClick: rerun, disabled: !audit || running, children: running ? "Checking\u2026" : "Re-check" }) }),
|
|
93
97
|
!audit ? /* @__PURE__ */ jsx("p", { className: "fg-a11y__note", children: "No frame is connected." }) : null,
|
|
94
98
|
audit && !outcome ? /* @__PURE__ */ jsx("p", { className: "fg-a11y__note", children: running ? "Running axe\u2026" : "Not checked yet." }) : null,
|
|
95
99
|
outcome && !outcome.ok ? /* @__PURE__ */ jsxs("p", { className: "fg-a11y__note", role: "alert", children: [
|
|
@@ -126,6 +130,56 @@ var A11Y_SECTION = {
|
|
|
126
130
|
region: "aside",
|
|
127
131
|
item: { title: "Accessibility", body: /* @__PURE__ */ jsx(FocusedA11y, {}) }
|
|
128
132
|
};
|
|
133
|
+
|
|
134
|
+
// src/shell/followEdits.ts
|
|
135
|
+
function followEdits(hot, workshop) {
|
|
136
|
+
const stale = /* @__PURE__ */ new Set();
|
|
137
|
+
hot?.on("forge:index", (next) => workshop.setIndex(next));
|
|
138
|
+
hot?.on("forge:deps", (next) => workshop.setDependencies(next));
|
|
139
|
+
hot?.on("forge:story", ({ file }) => {
|
|
140
|
+
stale.add(file);
|
|
141
|
+
workshop.revise();
|
|
142
|
+
});
|
|
143
|
+
hot?.on("vite:beforeUpdate", () => workshop.revise());
|
|
144
|
+
return {
|
|
145
|
+
importers(next) {
|
|
146
|
+
if (!next) return;
|
|
147
|
+
workshop.setImporters(next);
|
|
148
|
+
for (const file of stale) workshop.reloadStory(file);
|
|
149
|
+
stale.clear();
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function createRevision() {
|
|
154
|
+
let n = 0;
|
|
155
|
+
const bumped = createEmitter();
|
|
156
|
+
return {
|
|
157
|
+
get: () => n,
|
|
158
|
+
subscribe: bumped.subscribe,
|
|
159
|
+
bump() {
|
|
160
|
+
n += 1;
|
|
161
|
+
bumped.emit();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function createCaptureTrials() {
|
|
166
|
+
const trials = /* @__PURE__ */ new Map();
|
|
167
|
+
const changed = createEmitter();
|
|
168
|
+
return {
|
|
169
|
+
publish(trialId, inputs) {
|
|
170
|
+
trials.set(trialId, inputs);
|
|
171
|
+
changed.emit();
|
|
172
|
+
return () => {
|
|
173
|
+
if (trials.get(trialId) !== inputs) return;
|
|
174
|
+
trials.delete(trialId);
|
|
175
|
+
changed.emit();
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
get: (trialId) => trials.get(trialId) ?? null,
|
|
179
|
+
subscribe: changed.subscribe
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
var LoupeCaptureContext = createContext(null);
|
|
129
183
|
function createStoryChanges() {
|
|
130
184
|
const changes = createEmitter();
|
|
131
185
|
return { subscribe: changes.subscribe, emit: changes.emit };
|
|
@@ -208,7 +262,9 @@ function TransportBar({ clock }) {
|
|
|
208
262
|
{
|
|
209
263
|
className: "fg-transport__scrub",
|
|
210
264
|
ariaLabel: "Scrub",
|
|
211
|
-
|
|
265
|
+
thumbShape: "dot",
|
|
266
|
+
trackHeight: "var(--wzl-slider-track-h)",
|
|
267
|
+
thumbSize: "var(--wzl-slider-thumb-size)",
|
|
212
268
|
readoutPlacement: "none",
|
|
213
269
|
min: span.start,
|
|
214
270
|
max: span.end > span.start ? span.end : span.start + 1,
|
|
@@ -240,10 +296,15 @@ function TrialTransport({ trialId }) {
|
|
|
240
296
|
}
|
|
241
297
|
|
|
242
298
|
// src/shell/breadcrumb.ts
|
|
299
|
+
var bare = (label) => label.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
|
|
300
|
+
function restatesTitle(title, name) {
|
|
301
|
+
return bare(title.slice(title.lastIndexOf("/") + 1)) === bare(name);
|
|
302
|
+
}
|
|
243
303
|
function crumbs(title, name) {
|
|
244
304
|
const segments = title.split("/");
|
|
245
305
|
const steps = segments.map((label, i) => ({ label, path: segments.slice(0, i + 1).join("/") }));
|
|
246
|
-
steps.
|
|
306
|
+
if (restatesTitle(title, name)) steps[steps.length - 1] = { label: segments[segments.length - 1], path: null };
|
|
307
|
+
else steps.push({ label: name, path: null });
|
|
247
308
|
return steps.filter((step, i) => i === 0 || step.label.toLowerCase() !== "index");
|
|
248
309
|
}
|
|
249
310
|
function breadcrumb(title, name) {
|
|
@@ -297,6 +358,7 @@ function IndexTrial({ bundle, setup, config }) {
|
|
|
297
358
|
descriptions: bundle.descriptions,
|
|
298
359
|
gallery,
|
|
299
360
|
galleries: bundle.galleries,
|
|
361
|
+
labs: bundle.labs,
|
|
300
362
|
...dependencies ? { dependencies } : {},
|
|
301
363
|
globals,
|
|
302
364
|
decorators: setup.decorators ?? [],
|
|
@@ -306,7 +368,38 @@ function IndexTrial({ bundle, setup, config }) {
|
|
|
306
368
|
};
|
|
307
369
|
return /* @__PURE__ */ jsx(IndexPage, { env, render: bundle.render });
|
|
308
370
|
};
|
|
309
|
-
return /* @__PURE__ */
|
|
371
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
372
|
+
/* @__PURE__ */ jsx(TrialHost, { layout: "fullscreen", setup, config, children: render }),
|
|
373
|
+
/* @__PURE__ */ jsx(LiveCopies, { trialId, layout: "fullscreen", setup, trialConfig: config, children: render })
|
|
374
|
+
] });
|
|
375
|
+
}
|
|
376
|
+
function CaptureInputs({ entry, layout, config, state }) {
|
|
377
|
+
const capture = useContext(LoupeCaptureContext);
|
|
378
|
+
const trialId = useContext(TrialIdContext);
|
|
379
|
+
const [overrides] = useCssOverrides();
|
|
380
|
+
const withdraw = useRef(null);
|
|
381
|
+
useEffect(() => {
|
|
382
|
+
if (capture && trialId) withdraw.current = capture.trials.publish(trialId, { entry, layout, config, state, overrides });
|
|
383
|
+
}, [capture, trialId, entry, layout, config, state, overrides]);
|
|
384
|
+
useEffect(
|
|
385
|
+
() => () => {
|
|
386
|
+
withdraw.current?.();
|
|
387
|
+
withdraw.current = null;
|
|
388
|
+
},
|
|
389
|
+
[capture, trialId]
|
|
390
|
+
);
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
var STILL = { get: () => 0, subscribe: () => () => {
|
|
394
|
+
} };
|
|
395
|
+
function LoupeProvider({ captureBase, revision = STILL, children }) {
|
|
396
|
+
const [lenses] = useState(createLiveLenses);
|
|
397
|
+
const [trials] = useState(createCaptureTrials);
|
|
398
|
+
const capture = useMemo(
|
|
399
|
+
() => captureBase === void 0 ? null : { base: captureBase, trials, revision },
|
|
400
|
+
[captureBase, trials, revision]
|
|
401
|
+
);
|
|
402
|
+
return /* @__PURE__ */ jsx(LiveLensesContext.Provider, { value: lenses, children: /* @__PURE__ */ jsx(LoupeCaptureContext.Provider, { value: capture, children }) });
|
|
310
403
|
}
|
|
311
404
|
function withPlayhead(schema) {
|
|
312
405
|
return f.schema({ ...schema.nodes, [PLAYHEAD_KEY]: f.custom("forge-playhead", null).hidden() });
|
|
@@ -323,7 +416,10 @@ function documentInstrument(options) {
|
|
|
323
416
|
config,
|
|
324
417
|
defaultConfig: () => config.defaults(),
|
|
325
418
|
initialState: (c) => initialState?.(c) ?? null,
|
|
326
|
-
render: (ctx) => loaded.kind === "story" ? /* @__PURE__ */
|
|
419
|
+
render: (ctx) => loaded.kind === "story" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
420
|
+
/* @__PURE__ */ jsx(StoryTrial, { story: loaded.story, setup, ctx }, loaded.story.id),
|
|
421
|
+
/* @__PURE__ */ jsx(CaptureInputs, { entry, layout: loaded.story.layout, config: ctx.config, state: ctx.state })
|
|
422
|
+
] }) : /* @__PURE__ */ jsx(IndexTrial, { bundle: loaded.bundle, setup, config: ctx.config }),
|
|
327
423
|
...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {},
|
|
328
424
|
...timed ? { chrome: [{ id: "fg-transport", region: "status", render: (c) => /* @__PURE__ */ jsx(TrialTransport, { trialId: c.trialId }) }] } : {}
|
|
329
425
|
};
|
|
@@ -363,6 +459,50 @@ function missingInstrument(id, globals = {}) {
|
|
|
363
459
|
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
460
|
};
|
|
365
461
|
}
|
|
462
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
463
|
+
var pending = null;
|
|
464
|
+
function revealVar(name) {
|
|
465
|
+
pending = name;
|
|
466
|
+
for (const fn of [...listeners]) fn(name);
|
|
467
|
+
}
|
|
468
|
+
function subscribeReveal(fn) {
|
|
469
|
+
listeners.add(fn);
|
|
470
|
+
if (pending !== null) fn(pending);
|
|
471
|
+
return () => {
|
|
472
|
+
listeners.delete(fn);
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
var MARK = "data-revealed";
|
|
476
|
+
function useRevealToken(list, tokens, show, storyReported) {
|
|
477
|
+
const asked = useRef(null);
|
|
478
|
+
const [, rerender] = useState(0);
|
|
479
|
+
useEffect(
|
|
480
|
+
() => subscribeReveal((name) => {
|
|
481
|
+
const tab = ["theme", "story"].find((t) => tokens[t].some((token2) => token2.name === name));
|
|
482
|
+
const token = tab && tokens[tab].find((t) => t.name === name);
|
|
483
|
+
if (!tab || !token) {
|
|
484
|
+
if (storyReported && pending === name) pending = null;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
show(name, tab, tokenCategory(token));
|
|
488
|
+
asked.current = name;
|
|
489
|
+
rerender((n) => n + 1);
|
|
490
|
+
}),
|
|
491
|
+
[tokens, show, storyReported]
|
|
492
|
+
);
|
|
493
|
+
useLayoutEffect(() => {
|
|
494
|
+
const root = list.current;
|
|
495
|
+
const revealed = asked.current;
|
|
496
|
+
if (!root || revealed === null) return;
|
|
497
|
+
const target = root.querySelector(`[aria-label="${revealed.replace(/["\\]/g, "\\$&")}"]`);
|
|
498
|
+
for (const el of Array.from(root.querySelectorAll(`[${MARK}]`))) if (el !== target) el.removeAttribute(MARK);
|
|
499
|
+
if (!target) return;
|
|
500
|
+
target.setAttribute(MARK, "");
|
|
501
|
+
target.scrollIntoView?.({ block: "nearest" });
|
|
502
|
+
if (pending === revealed) pending = null;
|
|
503
|
+
asked.current = null;
|
|
504
|
+
});
|
|
505
|
+
}
|
|
366
506
|
var ICONS = {
|
|
367
507
|
color: (
|
|
368
508
|
/* @icon */
|
|
@@ -509,8 +649,8 @@ function applyScaleEdits(definition, edits, selection) {
|
|
|
509
649
|
const write = (slot, value) => {
|
|
510
650
|
let byKey = writes.get(slot.holder);
|
|
511
651
|
if (!byKey) writes.set(slot.holder, byKey = /* @__PURE__ */ new Map());
|
|
512
|
-
const
|
|
513
|
-
if (
|
|
652
|
+
const pending2 = byKey.get(slot.key);
|
|
653
|
+
if (pending2 !== void 0 && pending2 !== value) throw new EditError(`${slot.where} would need to be both ${pending2} and ${value}`);
|
|
514
654
|
byKey.set(slot.key, value);
|
|
515
655
|
};
|
|
516
656
|
try {
|
|
@@ -634,22 +774,22 @@ function useThemeSave({ trialId, globals, themeApi, scales, overrides, history:
|
|
|
634
774
|
const stored = await api.get(THEME_NAME);
|
|
635
775
|
const file = `themes/${THEME_NAME}.json`;
|
|
636
776
|
if (JSON.stringify(stored.definition) !== JSON.stringify(THEME_SOURCES[THEME_NAME])) {
|
|
637
|
-
setNotice({
|
|
777
|
+
setNotice({ outcome: "error", text: `${file} has changed since this page loaded. Reload to edit against it.` });
|
|
638
778
|
return;
|
|
639
779
|
}
|
|
640
780
|
const scaled = applyScaleEdits(stored.definition, Object.fromEntries(scales.map((s) => [s.name, s.edit])), selection);
|
|
641
781
|
const next = scaled.ok ? applyTokenEdits(scaled.definition, tokens, selection) : scaled;
|
|
642
782
|
if (!next.ok) {
|
|
643
|
-
setNotice({
|
|
783
|
+
setNotice({ outcome: "error", text: next.message });
|
|
644
784
|
return;
|
|
645
785
|
}
|
|
646
786
|
const result = await api.put(THEME_NAME, next.definition, stored.hash);
|
|
647
787
|
if (result.status === "conflict") {
|
|
648
|
-
setNotice({
|
|
788
|
+
setNotice({ outcome: "error", text: `${file} changed on disk while saving. Nothing was written; reload to edit against it.` });
|
|
649
789
|
return;
|
|
650
790
|
}
|
|
651
791
|
if (result.status === "invalid") {
|
|
652
|
-
setNotice({
|
|
792
|
+
setNotice({ outcome: "error", text: result.message });
|
|
653
793
|
return;
|
|
654
794
|
}
|
|
655
795
|
const saved = /* @__PURE__ */ new Set([
|
|
@@ -663,10 +803,10 @@ function useThemeSave({ trialId, globals, themeApi, scales, overrides, history:
|
|
|
663
803
|
scales: Object.fromEntries(Object.entries(now.scales).filter(([group]) => !groups.has(group)))
|
|
664
804
|
});
|
|
665
805
|
setNotice(
|
|
666
|
-
result.regenerated || result.problems.length === 0 ? {
|
|
806
|
+
result.regenerated || result.problems.length === 0 ? { outcome: "saved", text: `Saved to ${file}.` } : { outcome: "error", text: `Saved to ${file}, but the tokens were not regenerated: ${result.problems.join("; ")}` }
|
|
667
807
|
);
|
|
668
808
|
} catch (e) {
|
|
669
|
-
setNotice({
|
|
809
|
+
setNotice({ outcome: "error", text: `Saving needs forge's dev server, which did not answer: ${e.message}` });
|
|
670
810
|
} finally {
|
|
671
811
|
setSaving(false);
|
|
672
812
|
}
|
|
@@ -810,6 +950,15 @@ function standingScale(source, amounts) {
|
|
|
810
950
|
const base = (amounts[at] ?? 0) / (rule.factors[at] ?? 1);
|
|
811
951
|
return { ...seed, base: Math.round(base * 100) / 100, rule };
|
|
812
952
|
}
|
|
953
|
+
function unsavedScales(edited, overrides) {
|
|
954
|
+
return SCALE_SOURCES.flatMap((source) => {
|
|
955
|
+
const edit = edited[source.group];
|
|
956
|
+
if (!edit || !source.tokens.some((name2) => overrides[name2] !== void 0)) return [];
|
|
957
|
+
const { name, group, tokens } = source;
|
|
958
|
+
const clean = scaleValues(source, edit).every(([token, value]) => overrides[token] === value);
|
|
959
|
+
return clean ? [{ name, group, tokens, edit: { base: edit.base, rule: edit.rule } }] : [];
|
|
960
|
+
});
|
|
961
|
+
}
|
|
813
962
|
var NO_GLOBALS = {};
|
|
814
963
|
function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
815
964
|
const trialId = useTrialId();
|
|
@@ -826,6 +975,14 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
|
826
975
|
const themeTokens = useThemeTokens(globals, frame.vars);
|
|
827
976
|
const story = useStoryTokens(frame.vars);
|
|
828
977
|
const tokens = tab === "theme" ? themeTokens : story.tokens;
|
|
978
|
+
const listRef = useRef(null);
|
|
979
|
+
const byTab = useMemo(() => ({ theme: themeTokens, story: story.tokens }), [themeTokens, story.tokens]);
|
|
980
|
+
const show = useCallback((name, at, category) => {
|
|
981
|
+
setTab(at);
|
|
982
|
+
setFilter((was) => name.toLowerCase().includes(was.trim().toLowerCase()) ? was : "");
|
|
983
|
+
setFolds((prev) => prev[category] ? { ...prev, [category]: false } : prev);
|
|
984
|
+
}, [setFolds]);
|
|
985
|
+
useRevealToken(listRef, byTab, show, frame.vars.length > 0);
|
|
829
986
|
const query = filter.trim().toLowerCase();
|
|
830
987
|
const shown = tokens.map((token) => {
|
|
831
988
|
const override = overrides[token.name];
|
|
@@ -866,17 +1023,8 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
|
866
1023
|
`scale:${group}`
|
|
867
1024
|
);
|
|
868
1025
|
};
|
|
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 });
|
|
1026
|
+
const unsaved = useMemo(() => unsavedScales(edited, overrides), [edited, overrides]);
|
|
1027
|
+
const { pending: pending2, saving, notice, save } = useThemeSave({ trialId, globals, themeApi, scales: unsaved, overrides, history: history2 });
|
|
880
1028
|
const onCollapsedChange = useCallback(
|
|
881
1029
|
(category, collapsed) => setFolds((prev) => ({ ...prev, [category]: collapsed })),
|
|
882
1030
|
[setFolds]
|
|
@@ -935,9 +1083,9 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
|
935
1083
|
] })
|
|
936
1084
|
] }),
|
|
937
1085
|
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
|
|
1086
|
+
tab === "theme" ? /* @__PURE__ */ jsx(SaveRow, { pending: pending2, saving, notice, onSave: () => void save() }) : null
|
|
939
1087
|
] }),
|
|
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(
|
|
1088
|
+
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
1089
|
TokenPanel,
|
|
942
1090
|
{
|
|
943
1091
|
tokens: shown,
|
|
@@ -954,11 +1102,11 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
|
954
1102
|
)
|
|
955
1103
|
);
|
|
956
1104
|
}
|
|
957
|
-
function SaveRow({ pending, saving, notice, onSave }) {
|
|
958
|
-
if (!
|
|
1105
|
+
function SaveRow({ pending: pending2, saving, notice, onSave }) {
|
|
1106
|
+
if (!pending2 && !notice) return null;
|
|
959
1107
|
return /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__save", children: [
|
|
960
|
-
|
|
961
|
-
notice ? /* @__PURE__ */ jsx("p", { className: `fg-css-vars__notice fg-css-vars__notice--${notice.
|
|
1108
|
+
pending2 ? /* @__PURE__ */ jsx(Button$1, { stance: "confirm", size: "sm", loading: saving, disabled: saving, onClick: onSave, children: "Save to theme" }) : null,
|
|
1109
|
+
notice ? /* @__PURE__ */ jsx("p", { className: `fg-css-vars__notice fg-css-vars__notice--${notice.outcome}`, role: notice.outcome === "error" ? "alert" : "status", children: notice.text }) : null
|
|
962
1110
|
] });
|
|
963
1111
|
}
|
|
964
1112
|
function FocusedCssVars() {
|
|
@@ -999,6 +1147,124 @@ function CssVarsChrome({ onChrome }) {
|
|
|
999
1147
|
const [shown, setShown] = usePersistedState(RECORD, false, { scope: "lab" });
|
|
1000
1148
|
const chrome = useMemo(() => cssVarsChrome(shown, () => setShown((was) => !was)), [shown, setShown]);
|
|
1001
1149
|
useEffect(() => onChrome(chrome), [chrome, onChrome]);
|
|
1150
|
+
useEffect(() => subscribeReveal(() => setShown(true)), [setShown]);
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
function describeBinding(spec) {
|
|
1154
|
+
const chip = keySpecShortcut(spec);
|
|
1155
|
+
if (chip) return spec.kind === "key-held" ? `Hold ${formatShortcut(chip)}` : formatShortcut(chip) ?? "";
|
|
1156
|
+
switch (spec.kind) {
|
|
1157
|
+
case "wheel":
|
|
1158
|
+
return "Wheel";
|
|
1159
|
+
case "hover":
|
|
1160
|
+
return "Hover";
|
|
1161
|
+
case "drag":
|
|
1162
|
+
return "Drag";
|
|
1163
|
+
case "click":
|
|
1164
|
+
return "Click";
|
|
1165
|
+
default:
|
|
1166
|
+
return spec.kind[0].toUpperCase() + spec.kind.slice(1);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function keymapDefs(entries) {
|
|
1170
|
+
const defs = [];
|
|
1171
|
+
for (const entry of entries.values()) {
|
|
1172
|
+
if (!entry.eligibility.keymap) continue;
|
|
1173
|
+
const open = entries.get(`${entry.id}.leader`)?.actions?.find((a) => a.id === keymapLeaderId(entry.id));
|
|
1174
|
+
const leader = open ? actionBindings(open).map((b) => b.spec) : [];
|
|
1175
|
+
if (leader.length === 0) continue;
|
|
1176
|
+
defs.push({ id: entry.id, label: entry.presentation?.label ?? entry.id, leader, bindings: entry.bindings ?? [] });
|
|
1177
|
+
}
|
|
1178
|
+
return defs;
|
|
1179
|
+
}
|
|
1180
|
+
function keyRows(actions, defs, overrides) {
|
|
1181
|
+
const inKeymap = /* @__PURE__ */ new Map();
|
|
1182
|
+
for (const def of defs) {
|
|
1183
|
+
for (const [id, chips] of keymapShortcuts(def, overrides)) {
|
|
1184
|
+
inKeymap.set(id, [...inKeymap.get(id) ?? [], ...chips.flatMap((c) => formatShortcut(c) ?? [])]);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
const rows = [];
|
|
1188
|
+
for (const action of actions) {
|
|
1189
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1190
|
+
const others = /* @__PURE__ */ new Set();
|
|
1191
|
+
for (const { spec } of actionBindings(action)) {
|
|
1192
|
+
(keySpecShortcut(spec) ? keys : others).add(describeBinding(spec));
|
|
1193
|
+
}
|
|
1194
|
+
for (const chip of inKeymap.get(action.id) ?? []) keys.add(chip);
|
|
1195
|
+
if (keys.size + others.size === 0) continue;
|
|
1196
|
+
rows.push({ id: action.id, label: action.label, group: action.group ?? "other", keys: [...keys], others: [...others] });
|
|
1197
|
+
}
|
|
1198
|
+
return rows.sort((a, b) => a.group.localeCompare(b.group) || a.label.localeCompare(b.label));
|
|
1199
|
+
}
|
|
1200
|
+
function useKeyRows() {
|
|
1201
|
+
const registry = useActionsRegistry();
|
|
1202
|
+
const entries = useLabEntries();
|
|
1203
|
+
const [version, bump] = useReducer((n) => n + 1, 0);
|
|
1204
|
+
useEffect(() => registry?.subscribe(bump), [registry]);
|
|
1205
|
+
return useMemo(
|
|
1206
|
+
() => registry ? keyRows(registry.list(), keymapDefs(entries), registry.bindingOverrides()) : [],
|
|
1207
|
+
// `version` is the registry's change signal: `list()` has no identity to key on.
|
|
1208
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1209
|
+
[registry, entries, version]
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
function KeysPanel() {
|
|
1213
|
+
const rows = useKeyRows();
|
|
1214
|
+
if (rows.length === 0) return /* @__PURE__ */ jsx("p", { className: "fg-keys__empty", children: "No key bindings." });
|
|
1215
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1216
|
+
for (const row of rows) groups.set(row.group, [...groups.get(row.group) ?? [], row]);
|
|
1217
|
+
return /* @__PURE__ */ jsx("div", { className: "fg-keys", children: [...groups].map(([group, members]) => /* @__PURE__ */ jsxs("section", { className: "fg-keys__group", "aria-label": group, children: [
|
|
1218
|
+
/* @__PURE__ */ jsx("h3", { className: "fg-keys__heading", children: group }),
|
|
1219
|
+
/* @__PURE__ */ jsx("ul", { className: "fg-keys__list", children: members.map((row) => /* @__PURE__ */ jsxs("li", { className: "fg-keys__row", children: [
|
|
1220
|
+
/* @__PURE__ */ jsx("span", { className: "fg-keys__label", children: row.label }),
|
|
1221
|
+
/* @__PURE__ */ jsxs("span", { className: "fg-keys__chips", children: [
|
|
1222
|
+
row.keys.map((chip) => /* @__PURE__ */ jsx("kbd", { className: "fg-keys__chip", children: chip }, chip)),
|
|
1223
|
+
row.others.map((chip) => /* @__PURE__ */ jsx("kbd", { className: "fg-keys__chip fg-keys__chip--gesture", children: chip }, chip))
|
|
1224
|
+
] })
|
|
1225
|
+
] }, row.id)) })
|
|
1226
|
+
] }, group)) });
|
|
1227
|
+
}
|
|
1228
|
+
var KEYS_SECTION = {
|
|
1229
|
+
id: "fg-keys",
|
|
1230
|
+
region: "aside",
|
|
1231
|
+
item: { title: "Keys", body: /* @__PURE__ */ jsx(KeysPanel, {}) }
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
// src/shell/keys/keysChrome.ts
|
|
1235
|
+
var RECORD2 = "fg-keys-shown";
|
|
1236
|
+
var KEYS_TOGGLE_ID = "forge.keys.toggle";
|
|
1237
|
+
var OPEN_KEY = { kind: "key", key: "/", code: "Slash", mods: { alt: true } };
|
|
1238
|
+
function keysChrome(shown, toggle) {
|
|
1239
|
+
return [
|
|
1240
|
+
{
|
|
1241
|
+
id: "fg-keys-toggle",
|
|
1242
|
+
region: "header",
|
|
1243
|
+
group: "story",
|
|
1244
|
+
item: { icon: (
|
|
1245
|
+
/* @icon */
|
|
1246
|
+
"ui:keyboard"
|
|
1247
|
+
), label: "Keys panel", pressed: shown, onActivate: toggle }
|
|
1248
|
+
},
|
|
1249
|
+
...shown ? [KEYS_SECTION] : []
|
|
1250
|
+
];
|
|
1251
|
+
}
|
|
1252
|
+
function KeysChrome({ onChrome }) {
|
|
1253
|
+
const [shown, setShown] = usePersistedState(RECORD2, false, { scope: "lab" });
|
|
1254
|
+
const registry = useActionsRegistry();
|
|
1255
|
+
const chrome = useMemo(() => keysChrome(shown, () => setShown((was) => !was)), [shown, setShown]);
|
|
1256
|
+
useEffect(() => onChrome(chrome), [chrome, onChrome]);
|
|
1257
|
+
useEffect(() => {
|
|
1258
|
+
if (!registry) return;
|
|
1259
|
+
const action = {
|
|
1260
|
+
id: KEYS_TOGGLE_ID,
|
|
1261
|
+
label: "Toggle the keys panel",
|
|
1262
|
+
group: "forge",
|
|
1263
|
+
defaultBinding: [{ spec: OPEN_KEY, opts: { id: KEYS_TOGGLE_ID } }],
|
|
1264
|
+
invoker: { timing: "immediate", run: () => setShown((was) => !was) }
|
|
1265
|
+
};
|
|
1266
|
+
return registry.register(action);
|
|
1267
|
+
}, [registry, setShown]);
|
|
1002
1268
|
return null;
|
|
1003
1269
|
}
|
|
1004
1270
|
var KEYS = { kind: "key", key: "i", mods: { mod: true } };
|
|
@@ -1088,8 +1354,8 @@ function ArgRows({ args }) {
|
|
|
1088
1354
|
function StoryDossier({ dossier }) {
|
|
1089
1355
|
return /* @__PURE__ */ jsxs("div", { className: "fg-info", children: [
|
|
1090
1356
|
dossier.description || dossier.componentDescription ? /* @__PURE__ */ jsxs("section", { className: "fg-info__section", "aria-label": "Docs", children: [
|
|
1091
|
-
dossier.description ? /* @__PURE__ */ jsx(
|
|
1092
|
-
dossier.componentDescription ? /* @__PURE__ */ jsx(
|
|
1357
|
+
dossier.description ? /* @__PURE__ */ jsx(Description, { text: dossier.description, className: "fg-info__doc" }) : null,
|
|
1358
|
+
dossier.componentDescription ? /* @__PURE__ */ jsx(Description, { text: dossier.componentDescription, className: "fg-info__doc fg-info__doc--component" }) : null
|
|
1093
1359
|
] }) : null,
|
|
1094
1360
|
/* @__PURE__ */ jsx("section", { className: "fg-info__section", "aria-label": "Identity", children: /* @__PURE__ */ jsxs("dl", { className: "fg-info__rows", children: [
|
|
1095
1361
|
/* @__PURE__ */ jsx(Row, { label: "Story", children: dossier.name }),
|
|
@@ -1202,6 +1468,1011 @@ function createFramePool(frameUrl, size = 2) {
|
|
|
1202
1468
|
};
|
|
1203
1469
|
}
|
|
1204
1470
|
var FramePoolContext = createContext(null);
|
|
1471
|
+
var COMPARE_WITH_ID = "loupe.compare.with";
|
|
1472
|
+
var COMPARE_DPR_ID = "loupe.compare.dpr";
|
|
1473
|
+
var PIXELS_ONLY = "Pixels only";
|
|
1474
|
+
var COMPARE_MENU_ID = "loupe.compare";
|
|
1475
|
+
var DPRS = [1, 2, 3];
|
|
1476
|
+
function argChoices(shape, at) {
|
|
1477
|
+
return Object.entries(shape).flatMap(([key, entry]) => {
|
|
1478
|
+
const path = at === "" ? key : `${at}.${key}`;
|
|
1479
|
+
if (isForgePath(path)) return [];
|
|
1480
|
+
if (isConfigBranch(entry)) return argChoices(entry.children, path);
|
|
1481
|
+
const options = entry.kind === "boolean" ? [
|
|
1482
|
+
{ value: true, label: "On" },
|
|
1483
|
+
{ value: false, label: "Off" }
|
|
1484
|
+
] : entry.annotations.options ?? [];
|
|
1485
|
+
return options.map((o) => ({
|
|
1486
|
+
action: COMPARE_WITH_ID,
|
|
1487
|
+
key: `arg.${path}.${String(o.value)}`,
|
|
1488
|
+
group: `Arg ${path}`,
|
|
1489
|
+
label: o.label,
|
|
1490
|
+
compare: { kind: "custom", args: { [path]: o.value } }
|
|
1491
|
+
}));
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
function compareChoices(declarations, schema) {
|
|
1495
|
+
const globals = Object.entries(declarations).flatMap(
|
|
1496
|
+
([key, declaration]) => declaration.options.map((o) => ({
|
|
1497
|
+
action: COMPARE_WITH_ID,
|
|
1498
|
+
key: `global.${key}.${o.value}`,
|
|
1499
|
+
group: declaration.label,
|
|
1500
|
+
label: o.label,
|
|
1501
|
+
compare: { kind: "custom", globals: { [key]: o.value } }
|
|
1502
|
+
}))
|
|
1503
|
+
);
|
|
1504
|
+
const dprs = DPRS.map((dpr) => ({
|
|
1505
|
+
action: COMPARE_DPR_ID,
|
|
1506
|
+
key: `dpr.${dpr}`,
|
|
1507
|
+
group: "DPR",
|
|
1508
|
+
label: `${dpr}\xD7`,
|
|
1509
|
+
compare: { kind: "custom", dpr }
|
|
1510
|
+
}));
|
|
1511
|
+
return [...globals, ...dprs, ...schema ? argChoices(schema.nodes, "") : []];
|
|
1512
|
+
}
|
|
1513
|
+
function compareMenuDef(choices) {
|
|
1514
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1515
|
+
for (const choice of choices) {
|
|
1516
|
+
const rows = groups.get(choice.group) ?? {};
|
|
1517
|
+
rows[`c${Object.keys(rows).length}`] = pref.action(`${choice.action}:${choice.key}`);
|
|
1518
|
+
groups.set(choice.group, rows);
|
|
1519
|
+
}
|
|
1520
|
+
const submenus = {};
|
|
1521
|
+
[...groups].forEach(([label, rows], n) => {
|
|
1522
|
+
submenus[`g${n}`] = pref.group(label, rows, { as: "menu" });
|
|
1523
|
+
});
|
|
1524
|
+
return { id: COMPARE_MENU_ID, menu: pref.group("", submenus) };
|
|
1525
|
+
}
|
|
1526
|
+
function compareActions(api, request) {
|
|
1527
|
+
const one = (id, label, enabled) => ({
|
|
1528
|
+
id,
|
|
1529
|
+
label,
|
|
1530
|
+
group: "loupe",
|
|
1531
|
+
enabled,
|
|
1532
|
+
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 } })),
|
|
1533
|
+
invoker: {
|
|
1534
|
+
timing: "immediate",
|
|
1535
|
+
run(_deps, params) {
|
|
1536
|
+
const lensId = params?.lensId ?? api.current();
|
|
1537
|
+
const compare = params?.compare;
|
|
1538
|
+
if (lensId !== null && compare) api.apply(setLensOps(api.scene, lensId, { compare }));
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
return [
|
|
1543
|
+
one(COMPARE_WITH_ID, "Compare with", () => true),
|
|
1544
|
+
one(COMPARE_DPR_ID, "Compare at DPR", () => request.pixels ? true : PIXELS_ONLY)
|
|
1545
|
+
];
|
|
1546
|
+
}
|
|
1547
|
+
var NO_CHOICES = [];
|
|
1548
|
+
function CompareMenu({ api, request }) {
|
|
1549
|
+
const registry = useActionsRegistry();
|
|
1550
|
+
const host = useMenuHost();
|
|
1551
|
+
const choices = request?.choices ?? NO_CHOICES;
|
|
1552
|
+
const defs = useMemo(() => [compareMenuDef(choices)], [choices]);
|
|
1553
|
+
useMenus(defs);
|
|
1554
|
+
useEffect(() => {
|
|
1555
|
+
if (!registry || !host || !request) return;
|
|
1556
|
+
const offs = compareActions(api, request).map((a) => registry.register(a));
|
|
1557
|
+
let held = true;
|
|
1558
|
+
const drop = () => {
|
|
1559
|
+
if (!held) return;
|
|
1560
|
+
held = false;
|
|
1561
|
+
for (const off of offs) off();
|
|
1562
|
+
};
|
|
1563
|
+
const opened = host.open({ menu: COMPARE_MENU_ID, client: request.client, source: "keyboard" }) ? host.current()?.seq : void 0;
|
|
1564
|
+
if (opened === void 0) {
|
|
1565
|
+
drop();
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
const unsubscribe = host.subscribe(() => {
|
|
1569
|
+
if (host.current()?.seq !== opened) queueMicrotask(drop);
|
|
1570
|
+
});
|
|
1571
|
+
return () => {
|
|
1572
|
+
unsubscribe();
|
|
1573
|
+
drop();
|
|
1574
|
+
};
|
|
1575
|
+
}, [registry, host, api, request]);
|
|
1576
|
+
return null;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/shell/loupe/liveSource.ts
|
|
1580
|
+
var NOTHING_UNDER = "Nothing under the reticle";
|
|
1581
|
+
var sameContext = (a, b) => a.declarations === b.declarations && a.modeGlobal === b.modeGlobal && a.prefersDark === b.prefersDark && a.labMode === b.labMode;
|
|
1582
|
+
var liveCellId = (lensId, compare) => compare === null ? lensId : `${lensId}~compared`;
|
|
1583
|
+
function sameRequest(a, b) {
|
|
1584
|
+
return a !== null && a.slot === b.slot && a.hit.host === b.hit.host && a.hit.trialId === b.hit.trialId && a.hit.local.x === b.hit.local.x && a.hit.local.y === b.hit.local.y && a.hit.scale === b.hit.scale && a.factor === b.factor && a.size.width === b.size.width && a.size.height === b.size.height && a.args === b.args && a.compare === b.compare && sameContext(a.ctx, b.ctx);
|
|
1585
|
+
}
|
|
1586
|
+
function createLiveSource(lenses, resolve, conditions) {
|
|
1587
|
+
const measures = /* @__PURE__ */ new Set();
|
|
1588
|
+
const listeners2 = /* @__PURE__ */ new Set();
|
|
1589
|
+
return {
|
|
1590
|
+
kind: "live",
|
|
1591
|
+
produces: "dom",
|
|
1592
|
+
label: () => "Live",
|
|
1593
|
+
status: () => true,
|
|
1594
|
+
// A copy renders after the lens asks for it and changes as its story does; a reader of the copy hears of both.
|
|
1595
|
+
subscribe(listener) {
|
|
1596
|
+
listeners2.add(listener);
|
|
1597
|
+
return () => {
|
|
1598
|
+
listeners2.delete(listener);
|
|
1599
|
+
};
|
|
1600
|
+
},
|
|
1601
|
+
attach(slot, first) {
|
|
1602
|
+
const doc = slot.ownerDocument;
|
|
1603
|
+
const target = doc.createElement("div");
|
|
1604
|
+
target.className = "fg-lens-live";
|
|
1605
|
+
const empty = doc.createElement("div");
|
|
1606
|
+
empty.className = "fg-lens-empty";
|
|
1607
|
+
empty.hidden = true;
|
|
1608
|
+
slot.append(target, empty);
|
|
1609
|
+
const watch = typeof MutationObserver === "undefined" ? null : new MutationObserver(() => {
|
|
1610
|
+
for (const listener of [...listeners2]) listener();
|
|
1611
|
+
});
|
|
1612
|
+
watch?.observe(target, { subtree: true, childList: true, characterData: true, attributes: true });
|
|
1613
|
+
let request = first;
|
|
1614
|
+
let posted = null;
|
|
1615
|
+
const post = (next) => {
|
|
1616
|
+
if (next === null ? posted === null : sameRequest(posted, next)) return;
|
|
1617
|
+
posted = next;
|
|
1618
|
+
lenses.set(liveCellId(request.lensId, request.conditions), next);
|
|
1619
|
+
};
|
|
1620
|
+
const measure = () => {
|
|
1621
|
+
const { lensId, reticle, conditions: compare } = request;
|
|
1622
|
+
const hit = trialAt(reticle, doc);
|
|
1623
|
+
empty.hidden = hit !== null;
|
|
1624
|
+
if (!hit) {
|
|
1625
|
+
post(null);
|
|
1626
|
+
empty.textContent = NOTHING_UNDER;
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const { base, ctx } = conditions();
|
|
1630
|
+
const resolved = resolve(compare, base, ctx);
|
|
1631
|
+
post({
|
|
1632
|
+
lensId: liveCellId(lensId, compare),
|
|
1633
|
+
slot: target,
|
|
1634
|
+
hit,
|
|
1635
|
+
factor: request.factor,
|
|
1636
|
+
size: request.size,
|
|
1637
|
+
compare,
|
|
1638
|
+
ctx,
|
|
1639
|
+
args: resolved.args
|
|
1640
|
+
});
|
|
1641
|
+
};
|
|
1642
|
+
measures.add(measure);
|
|
1643
|
+
measure();
|
|
1644
|
+
return {
|
|
1645
|
+
update(next) {
|
|
1646
|
+
request = next;
|
|
1647
|
+
measure();
|
|
1648
|
+
},
|
|
1649
|
+
detach() {
|
|
1650
|
+
watch?.disconnect();
|
|
1651
|
+
measures.delete(measure);
|
|
1652
|
+
lenses.set(liveCellId(request.lensId, request.conditions), null);
|
|
1653
|
+
target.remove();
|
|
1654
|
+
empty.remove();
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
},
|
|
1658
|
+
remeasure() {
|
|
1659
|
+
for (const measure of [...measures]) measure();
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
var INLINE = "style attribute";
|
|
1664
|
+
function topmostIn(within, client) {
|
|
1665
|
+
return probeUnderLens(
|
|
1666
|
+
within,
|
|
1667
|
+
"interior",
|
|
1668
|
+
() => (within.ownerDocument.elementsFromPoint?.(client.x, client.y) ?? []).find((el) => within.contains(el))
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
function backgroundOf(el, stop) {
|
|
1672
|
+
for (let at = el; at; at = at === stop ? null : at.parentElement) {
|
|
1673
|
+
const color = getComputedStyle(at).backgroundColor;
|
|
1674
|
+
if (color.trim() === "") continue;
|
|
1675
|
+
try {
|
|
1676
|
+
if (parseColor(color)[3] === 1) return { element: at, color };
|
|
1677
|
+
} catch {
|
|
1678
|
+
return { unreadable: color };
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1683
|
+
var inside = (r, p) => p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom;
|
|
1684
|
+
function textUnder(el, client) {
|
|
1685
|
+
const range = el.ownerDocument.createRange();
|
|
1686
|
+
return Array.from(el.childNodes).some((n) => {
|
|
1687
|
+
if (n.nodeType !== Node.TEXT_NODE || (n.textContent ?? "").trim() === "") return false;
|
|
1688
|
+
range.selectNodeContents(n);
|
|
1689
|
+
return Array.from(range.getClientRects()).some((r) => inside(r, client));
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
function colorAt(client, within) {
|
|
1693
|
+
const el = topmostIn(within, client);
|
|
1694
|
+
if (!el) return null;
|
|
1695
|
+
if (textUnder(el, client)) return { element: el, property: "color", value: getComputedStyle(el).color };
|
|
1696
|
+
const bg = backgroundOf(el, within);
|
|
1697
|
+
if (bg === null) return null;
|
|
1698
|
+
if ("unreadable" in bg) return { element: el, property: "background-color", value: bg.unreadable };
|
|
1699
|
+
return { element: bg.element, property: "background-color", value: bg.color };
|
|
1700
|
+
}
|
|
1701
|
+
var STYLE_RULE = 1;
|
|
1702
|
+
var SHORTHANDS = { "background-color": ["background"] };
|
|
1703
|
+
var INHERITED = (property) => property.startsWith("--") || property === "color";
|
|
1704
|
+
function applies(rule, doc) {
|
|
1705
|
+
const media = rule.media;
|
|
1706
|
+
if (media && media.mediaText && doc.defaultView && !doc.defaultView.matchMedia(media.mediaText).matches) return false;
|
|
1707
|
+
if (typeof CSSSupportsRule !== "undefined" && rule instanceof CSSSupportsRule) return CSS.supports(rule.conditionText);
|
|
1708
|
+
return true;
|
|
1709
|
+
}
|
|
1710
|
+
function nest(selector, parent) {
|
|
1711
|
+
if (parent === void 0) return selector;
|
|
1712
|
+
return splitTopLevel(selector, ",").map((s) => s.includes("&") ? s.replaceAll("&", `:is(${parent})`) : `:is(${parent}) ${s.trim()}`).join(", ");
|
|
1713
|
+
}
|
|
1714
|
+
function collect(rules, doc, into, parent) {
|
|
1715
|
+
for (const rule of Array.from(rules)) {
|
|
1716
|
+
if (!applies(rule, doc)) continue;
|
|
1717
|
+
const imported = rule.styleSheet;
|
|
1718
|
+
if (imported) {
|
|
1719
|
+
const inner = cssRulesOf(imported);
|
|
1720
|
+
if (inner) collect(inner, doc, into);
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
const children = rule.cssRules;
|
|
1724
|
+
if (rule.type !== STYLE_RULE) {
|
|
1725
|
+
if (children) collect(children, doc, into, parent);
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
const style = rule;
|
|
1729
|
+
const selector = nest(style.selectorText, parent);
|
|
1730
|
+
into.push({ selector, style: style.style });
|
|
1731
|
+
if (children) collect(children, doc, into, selector);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
function rulesOf(doc) {
|
|
1735
|
+
const out = [];
|
|
1736
|
+
for (const sheet of [...Array.from(doc.styleSheets), ...doc.adoptedStyleSheets ?? []]) {
|
|
1737
|
+
if (sheet.disabled) continue;
|
|
1738
|
+
const rules = cssRulesOf(sheet);
|
|
1739
|
+
if (rules) collect(rules, doc, out);
|
|
1740
|
+
}
|
|
1741
|
+
return out;
|
|
1742
|
+
}
|
|
1743
|
+
function matching(el, list) {
|
|
1744
|
+
let best = null;
|
|
1745
|
+
for (const one of splitTopLevel(list, ",")) {
|
|
1746
|
+
let hit = false;
|
|
1747
|
+
try {
|
|
1748
|
+
hit = el.matches(one);
|
|
1749
|
+
} catch {
|
|
1750
|
+
}
|
|
1751
|
+
const s = hit ? specificity(one.trim()) : null;
|
|
1752
|
+
if (s && (best === null || compareSpecificity(s, best) > 0)) best = s;
|
|
1753
|
+
}
|
|
1754
|
+
return best;
|
|
1755
|
+
}
|
|
1756
|
+
function declaredIn(style, property) {
|
|
1757
|
+
for (const name of [property, ...SHORTHANDS[property] ?? []]) {
|
|
1758
|
+
const value = style.getPropertyValue(name).trim();
|
|
1759
|
+
if (value) return { value, important: style.getPropertyPriority(name) === "important" };
|
|
1760
|
+
}
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
function indexOf(rules) {
|
|
1764
|
+
const index = /* @__PURE__ */ new Map();
|
|
1765
|
+
return (property) => {
|
|
1766
|
+
let got = index.get(property);
|
|
1767
|
+
if (!got) {
|
|
1768
|
+
got = rules.flatMap((r) => {
|
|
1769
|
+
const d = declaredIn(r.style, property);
|
|
1770
|
+
return d ? [{ selector: r.selector, ...d }] : [];
|
|
1771
|
+
});
|
|
1772
|
+
index.set(property, got);
|
|
1773
|
+
}
|
|
1774
|
+
return got;
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
function winning(el, property, rules) {
|
|
1778
|
+
let best = null;
|
|
1779
|
+
for (const got of rules) {
|
|
1780
|
+
const spec = matching(el, got.selector);
|
|
1781
|
+
if (!spec) continue;
|
|
1782
|
+
const wins = best === null || got.important && !best.important || got.important === best.important && compareSpecificity(spec, best.spec) >= 0;
|
|
1783
|
+
if (wins) best = { ...got, spec };
|
|
1784
|
+
}
|
|
1785
|
+
const inline = el.style;
|
|
1786
|
+
const own = inline ? declaredIn(inline, property) : null;
|
|
1787
|
+
if (own && (best === null || own.important || !best.important)) return { property, declared: own.value, selector: INLINE, element: el };
|
|
1788
|
+
return best ? { property, declared: best.value, selector: best.selector, element: el } : null;
|
|
1789
|
+
}
|
|
1790
|
+
function declarationOf(el, property, rules) {
|
|
1791
|
+
for (let at = el; at; at = INHERITED(property) ? at.parentElement : null) {
|
|
1792
|
+
const link = winning(at, property, rules);
|
|
1793
|
+
if (link) return link;
|
|
1794
|
+
}
|
|
1795
|
+
return null;
|
|
1796
|
+
}
|
|
1797
|
+
var revisions = /* @__PURE__ */ new WeakMap();
|
|
1798
|
+
function revisionOf(doc) {
|
|
1799
|
+
let r = revisions.get(doc);
|
|
1800
|
+
if (!r) {
|
|
1801
|
+
const made = { n: 0, observer: null };
|
|
1802
|
+
if (typeof MutationObserver !== "undefined" && doc.head) {
|
|
1803
|
+
made.observer = new MutationObserver(() => {
|
|
1804
|
+
made.n += 1;
|
|
1805
|
+
});
|
|
1806
|
+
made.observer.observe(doc.head, { childList: true, subtree: true, characterData: true, attributes: true });
|
|
1807
|
+
}
|
|
1808
|
+
revisions.set(doc, made);
|
|
1809
|
+
r = made;
|
|
1810
|
+
}
|
|
1811
|
+
if ((r.observer?.takeRecords().length ?? 0) > 0) r.n += 1;
|
|
1812
|
+
return r.n;
|
|
1813
|
+
}
|
|
1814
|
+
var chains = /* @__PURE__ */ new WeakMap();
|
|
1815
|
+
function tokenChain(at) {
|
|
1816
|
+
const doc = at.element.ownerDocument;
|
|
1817
|
+
const key = `${at.value}|${revisionOf(doc)}|${doc.styleSheets.length}|${doc.adoptedStyleSheets?.length ?? 0}`;
|
|
1818
|
+
let byProperty = chains.get(at.element);
|
|
1819
|
+
const held = byProperty?.get(at.property);
|
|
1820
|
+
if (held?.key === key) return held.chain;
|
|
1821
|
+
const chain = chainOf(at);
|
|
1822
|
+
if (!byProperty) {
|
|
1823
|
+
byProperty = /* @__PURE__ */ new Map();
|
|
1824
|
+
chains.set(at.element, byProperty);
|
|
1825
|
+
}
|
|
1826
|
+
byProperty.set(at.property, { key, chain });
|
|
1827
|
+
return chain;
|
|
1828
|
+
}
|
|
1829
|
+
function chainOf(at) {
|
|
1830
|
+
const declaring = indexOf(rulesOf(at.element.ownerDocument));
|
|
1831
|
+
const chain = [];
|
|
1832
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1833
|
+
const follow = (el, property) => {
|
|
1834
|
+
if (seen.has(property)) return;
|
|
1835
|
+
seen.add(property);
|
|
1836
|
+
const link = declarationOf(el, property, declaring(property));
|
|
1837
|
+
if (!link) return;
|
|
1838
|
+
chain.push(link);
|
|
1839
|
+
for (const name of varRefs(link.declared)) follow(link.element, name);
|
|
1840
|
+
};
|
|
1841
|
+
follow(at.element, at.property);
|
|
1842
|
+
return chain;
|
|
1843
|
+
}
|
|
1844
|
+
var NO_TEXT = "No text under the aim";
|
|
1845
|
+
var NO_BACKGROUND = "No opaque background behind the text";
|
|
1846
|
+
var unreadable = (color) => `Cannot read the color "${color}"`;
|
|
1847
|
+
var NO_CAPTURE = "No capture to read";
|
|
1848
|
+
var OUTSIDE = "The aim is outside the capture";
|
|
1849
|
+
var NO_COLOR = "No color under the aim";
|
|
1850
|
+
var NEEDS_LIVE = "Token needs a Live source for changed conditions";
|
|
1851
|
+
var IN_FRAME = "Cannot read inside an isolated story's frame";
|
|
1852
|
+
var REACH = 2;
|
|
1853
|
+
function isLarge(style) {
|
|
1854
|
+
const px = Number.parseFloat(style.fontSize);
|
|
1855
|
+
const w = style.fontWeight === "bold" ? 700 : Number.parseInt(style.fontWeight, 10);
|
|
1856
|
+
return px >= 24 || px >= 18.66 && w >= 700;
|
|
1857
|
+
}
|
|
1858
|
+
var rgb = (d, i) => `rgb(${d[i]}, ${d[i + 1]}, ${d[i + 2]})`;
|
|
1859
|
+
function ofPixels(capture, point) {
|
|
1860
|
+
const img = capture ? readBitmap(capture.bitmap) : null;
|
|
1861
|
+
if (!capture || !img) return { unavailable: NO_CAPTURE };
|
|
1862
|
+
const px = Math.floor(point.x * capture.dpr);
|
|
1863
|
+
const py = Math.floor(point.y * capture.dpr);
|
|
1864
|
+
if (px < 0 || py < 0 || px >= img.width || py >= img.height) return { unavailable: OUTSIDE };
|
|
1865
|
+
const { data, width, height } = img;
|
|
1866
|
+
const center = (py * width + px) * 4;
|
|
1867
|
+
let far = center;
|
|
1868
|
+
let best = 0;
|
|
1869
|
+
for (let y = Math.max(0, py - REACH); y <= Math.min(height - 1, py + REACH); y += 1) {
|
|
1870
|
+
for (let x = Math.max(0, px - REACH); x <= Math.min(width - 1, px + REACH); x += 1) {
|
|
1871
|
+
const i = (y * width + x) * 4;
|
|
1872
|
+
const d = [0, 1, 2].reduce((sum, c) => sum + (data[i + c] - data[center + c]) ** 2, 0);
|
|
1873
|
+
if (d > best) {
|
|
1874
|
+
best = d;
|
|
1875
|
+
far = i;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
const ratio = contrastRatio(rgb(data, center), rgb(data, far));
|
|
1880
|
+
return ratio === null ? { unavailable: NO_CAPTURE } : { ratio, ...wcagLevels(ratio, false), large: false };
|
|
1881
|
+
}
|
|
1882
|
+
function contrastAt(source, at) {
|
|
1883
|
+
if (source === "pixels") return ofPixels(at.capture, at.point);
|
|
1884
|
+
const text = at.within ? colorAt(at.point, at.within) : null;
|
|
1885
|
+
if (!text || text.property !== "color") return { unavailable: NO_TEXT };
|
|
1886
|
+
const style = getComputedStyle(text.element);
|
|
1887
|
+
const background = backgroundOf(text.element, at.within);
|
|
1888
|
+
if (background === null) return { unavailable: NO_BACKGROUND };
|
|
1889
|
+
if ("unreadable" in background) return { unavailable: unreadable(background.unreadable) };
|
|
1890
|
+
const ratio = contrastRatio(style.color, background.color);
|
|
1891
|
+
if (ratio === null) return { unavailable: unreadable(style.color) };
|
|
1892
|
+
const large = isLarge(style);
|
|
1893
|
+
return { ratio, ...wcagLevels(ratio, large), large };
|
|
1894
|
+
}
|
|
1895
|
+
function copyIn(interior) {
|
|
1896
|
+
const copies = Array.from(interior?.querySelectorAll("[data-fg-copy]") ?? []);
|
|
1897
|
+
return copies.find((c) => (c.getAttribute("data-fg-host") ?? "").endsWith("~compared")) ?? copies[0];
|
|
1898
|
+
}
|
|
1899
|
+
function comparedAim(interior) {
|
|
1900
|
+
const box = interior?.getBoundingClientRect();
|
|
1901
|
+
return box ? { x: box.left + box.width / 2, y: box.top + box.height / 2 } : null;
|
|
1902
|
+
}
|
|
1903
|
+
function liveAim(interior, settings, reticle) {
|
|
1904
|
+
const aim2 = settings.compare === null ? { within: trialAt(reticle)?.host, point: reticle } : { within: copyIn(interior), point: comparedAim(interior) ?? reticle };
|
|
1905
|
+
return aim2.within?.classList.contains("fg-frame-host") ? { unavailable: IN_FRAME } : aim2;
|
|
1906
|
+
}
|
|
1907
|
+
var verdict = (pass) => pass ? "pass" : "fail";
|
|
1908
|
+
function textOf(reading) {
|
|
1909
|
+
if ("unavailable" in reading) return reading.unavailable;
|
|
1910
|
+
const base = `${reading.ratio.toFixed(2)}:1 \xB7 AA ${verdict(reading.aa)} \xB7 AAA ${verdict(reading.aaa)}`;
|
|
1911
|
+
return reading.large ? `${base} \xB7 large text` : base;
|
|
1912
|
+
}
|
|
1913
|
+
function ContrastReadout({ lens, settings, sources, reticle, size, interior }) {
|
|
1914
|
+
const { source } = useShownSource(settings, sources);
|
|
1915
|
+
const kind = source?.kind;
|
|
1916
|
+
const subscribe = useCallback((fn) => source?.subscribe(fn) ?? (() => {
|
|
1917
|
+
}), [source]);
|
|
1918
|
+
const live = useSyncExternalStore(subscribe, () => {
|
|
1919
|
+
if (kind !== "live") return null;
|
|
1920
|
+
const aim2 = liveAim(interior, settings, reticle);
|
|
1921
|
+
return textOf("unavailable" in aim2 ? aim2 : contrastAt("live", aim2));
|
|
1922
|
+
});
|
|
1923
|
+
const [pixels, setPixels] = useState(null);
|
|
1924
|
+
const [tick, setTick] = useState(0);
|
|
1925
|
+
useEffect(() => source?.subscribe(() => setTick((n) => n + 1)), [source]);
|
|
1926
|
+
const latest = useLatest({ lens, settings, reticle, size });
|
|
1927
|
+
useEffect(() => {
|
|
1928
|
+
if (kind !== "pixels" || !source?.capture) return;
|
|
1929
|
+
const capture = source.capture.bind(source);
|
|
1930
|
+
let current = true;
|
|
1931
|
+
const timer = setTimeout(() => {
|
|
1932
|
+
const { lens: ids, settings: s, reticle: r, size: box } = latest.current;
|
|
1933
|
+
const [request] = requestsOf(source, ids, s, r, box);
|
|
1934
|
+
if (!request) return;
|
|
1935
|
+
void capture(request).then((got) => {
|
|
1936
|
+
if (current) setPixels("reason" in got ? { unavailable: got.reason } : contrastAt("pixels", { capture: got, point: got.at }));
|
|
1937
|
+
});
|
|
1938
|
+
}, 0);
|
|
1939
|
+
return () => {
|
|
1940
|
+
current = false;
|
|
1941
|
+
clearTimeout(timer);
|
|
1942
|
+
};
|
|
1943
|
+
}, [source, kind, reticle.x, reticle.y, settings.factor, settings.compare, tick, latest]);
|
|
1944
|
+
const text = kind === "live" ? live : kind === "pixels" && pixels ? textOf(pixels) : null;
|
|
1945
|
+
return text ? /* @__PURE__ */ jsx("span", { className: "fg-lens-contrast", children: text }) : null;
|
|
1946
|
+
}
|
|
1947
|
+
function tokenAt(kind, interior, settings, reticle) {
|
|
1948
|
+
if (kind === "pixels" && settings.compare !== null) return { unavailable: NEEDS_LIVE };
|
|
1949
|
+
const aim2 = liveAim(interior, settings, reticle);
|
|
1950
|
+
if ("unavailable" in aim2) return aim2;
|
|
1951
|
+
const { within, point } = aim2;
|
|
1952
|
+
const at = within ? colorAt(point, within) : null;
|
|
1953
|
+
if (!at) return { unavailable: NO_COLOR };
|
|
1954
|
+
try {
|
|
1955
|
+
parseColor(at.value);
|
|
1956
|
+
} catch {
|
|
1957
|
+
return { unavailable: unreadable(at.value) };
|
|
1958
|
+
}
|
|
1959
|
+
const chain = tokenChain(at);
|
|
1960
|
+
const branches = chain.some((l) => varRefs(l.declared).size > 1);
|
|
1961
|
+
const end = branches ? at.value : chain.at(-1)?.declared ?? at.value;
|
|
1962
|
+
return { steps: [at.property, ...chain.slice(1).map((l) => l.property), end] };
|
|
1963
|
+
}
|
|
1964
|
+
var isToken = (step) => step.startsWith("--wzl-");
|
|
1965
|
+
function TokenReadout({ settings, sources, reticle, interior }) {
|
|
1966
|
+
const { source } = useShownSource(settings, sources);
|
|
1967
|
+
const kind = source?.kind;
|
|
1968
|
+
const subscribe = useCallback((fn) => source?.subscribe(fn) ?? (() => {
|
|
1969
|
+
}), [source]);
|
|
1970
|
+
const json = useSyncExternalStore(
|
|
1971
|
+
subscribe,
|
|
1972
|
+
() => kind === "live" || kind === "pixels" ? JSON.stringify(tokenAt(kind, interior, settings, reticle)) : null
|
|
1973
|
+
);
|
|
1974
|
+
const reading = useMemo(() => json === null ? null : JSON.parse(json), [json]);
|
|
1975
|
+
if (!reading) return null;
|
|
1976
|
+
if ("unavailable" in reading) return /* @__PURE__ */ jsx("span", { className: "fg-lens-token", children: reading.unavailable });
|
|
1977
|
+
return /* @__PURE__ */ jsx("span", { className: "fg-lens-token", children: reading.steps.map((step, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1978
|
+
i > 0 ? " \u2190 " : null,
|
|
1979
|
+
isToken(step) && i < reading.steps.length - 1 ? /* @__PURE__ */ jsx("button", { type: "button", className: "fg-lens-token__link", onClick: () => revealVar(step), children: step }) : step
|
|
1980
|
+
] }, i)) });
|
|
1981
|
+
}
|
|
1982
|
+
function renderReadouts(props) {
|
|
1983
|
+
const { readouts } = props.settings;
|
|
1984
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1985
|
+
readouts.includes("token") ? /* @__PURE__ */ jsx(TokenReadout, { ...props }) : null,
|
|
1986
|
+
readouts.includes("contrast") ? /* @__PURE__ */ jsx(ContrastReadout, { ...props }) : null
|
|
1987
|
+
] });
|
|
1988
|
+
}
|
|
1989
|
+
var STATE_UNSENDABLE = "this story's state cannot be sent to a capture";
|
|
1990
|
+
var CONFIG_UNSENDABLE = "this story's config cannot be sent to a capture";
|
|
1991
|
+
var NO_STORY = "this trial shows no story to capture";
|
|
1992
|
+
var NO_BOX = "the story's box has no size";
|
|
1993
|
+
function survived(value, round) {
|
|
1994
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value === round;
|
|
1995
|
+
if (typeof value === "number") return Number.isFinite(value) && value === round;
|
|
1996
|
+
if (Array.isArray(value)) {
|
|
1997
|
+
if (!Array.isArray(round) || value.length !== round.length) return false;
|
|
1998
|
+
for (let i = 0; i < value.length; i += 1) if (!(i in value) || !survived(value[i], round[i])) return false;
|
|
1999
|
+
return true;
|
|
2000
|
+
}
|
|
2001
|
+
if (!isPlainObject(value) || !isPlainObject(round)) return false;
|
|
2002
|
+
const keys = Object.keys(value);
|
|
2003
|
+
return keys.length === Object.keys(round).length && keys.every((key) => key in round && survived(value[key], round[key]));
|
|
2004
|
+
}
|
|
2005
|
+
function sendable(state) {
|
|
2006
|
+
try {
|
|
2007
|
+
return survived(state, JSON.parse(JSON.stringify(state) ?? "undefined"));
|
|
2008
|
+
} catch {
|
|
2009
|
+
return false;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
function withoutUndefined(value) {
|
|
2013
|
+
if (Array.isArray(value)) return value.map(withoutUndefined);
|
|
2014
|
+
if (!isPlainObject(value)) return value;
|
|
2015
|
+
return Object.fromEntries(
|
|
2016
|
+
Object.entries(value).flatMap(([key, v]) => v === void 0 ? [] : [[key, withoutUndefined(v)]])
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
var clampDpr = (dpr) => Math.min(Math.max(dpr, MIN_DPR), MAX_DPR);
|
|
2020
|
+
function capturePayload(input) {
|
|
2021
|
+
const { entry, layout, config, state, globals, overrides, host, dpr, resolved, lab, colorScheme } = input;
|
|
2022
|
+
if (!sendable(state)) return { unavailable: STATE_UNSENDABLE };
|
|
2023
|
+
const sent = withoutUndefined(withArgs(storyConfig(config), resolved.args));
|
|
2024
|
+
if (!sendable(sent)) return { unavailable: CONFIG_UNSENDABLE };
|
|
2025
|
+
const box = { width: host.offsetWidth, height: host.offsetHeight };
|
|
2026
|
+
if (!(box.width > 0 && box.height > 0)) return { unavailable: NO_BOX };
|
|
2027
|
+
const isolate = entry.isolate !== void 0;
|
|
2028
|
+
const scroller = scrollerOf(host);
|
|
2029
|
+
const kept2 = config?.[PLAYHEAD_KEY];
|
|
2030
|
+
return {
|
|
2031
|
+
story: entry.id,
|
|
2032
|
+
isolate,
|
|
2033
|
+
config: sent,
|
|
2034
|
+
state,
|
|
2035
|
+
globals,
|
|
2036
|
+
overrides,
|
|
2037
|
+
playhead: !isolate && typeof kept2 === "number" && Number.isFinite(kept2) ? kept2 : null,
|
|
2038
|
+
box,
|
|
2039
|
+
layout,
|
|
2040
|
+
dpr: clampDpr(resolved.dpr ?? dpr),
|
|
2041
|
+
scroll: { left: scroller?.scrollLeft ?? 0, top: scroller?.scrollTop ?? 0 },
|
|
2042
|
+
lab,
|
|
2043
|
+
colorScheme
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
function trialPayload(trial, lab, hit, dpr, c) {
|
|
2047
|
+
if (!trial) return { unavailable: NO_STORY };
|
|
2048
|
+
const ctx = resolveContextOf(lab);
|
|
2049
|
+
const own = effectiveGlobals(lab.labValues, trial.config?.[GLOBALS_KEY]);
|
|
2050
|
+
const resolved = resolveComparison(c, own, ctx);
|
|
2051
|
+
const colorScheme = ctx.prefersDark ? "dark" : "light";
|
|
2052
|
+
return capturePayload({
|
|
2053
|
+
...trial,
|
|
2054
|
+
globals: resolved.globals,
|
|
2055
|
+
host: hit.host,
|
|
2056
|
+
dpr,
|
|
2057
|
+
resolved,
|
|
2058
|
+
lab: { globals: lab.labValues, mode: lab.mode === "auto" ? colorScheme : lab.mode },
|
|
2059
|
+
colorScheme
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// src/shell/loupe/captureClient.ts
|
|
2064
|
+
var CHECKING = "checking for a capture browser";
|
|
2065
|
+
var UNAVAILABLE = "captures are unavailable";
|
|
2066
|
+
var NO_ENDPOINT = "this server takes no captures";
|
|
2067
|
+
function createCaptureClient(base, send) {
|
|
2068
|
+
const url = `${base}${CAPTURE_PATH}`;
|
|
2069
|
+
const listeners2 = /* @__PURE__ */ new Set();
|
|
2070
|
+
let status = CHECKING;
|
|
2071
|
+
let polling = null;
|
|
2072
|
+
const setStatus = (next) => {
|
|
2073
|
+
if (next === status) return;
|
|
2074
|
+
status = next;
|
|
2075
|
+
for (const listener of [...listeners2]) listener();
|
|
2076
|
+
};
|
|
2077
|
+
const poll = () => {
|
|
2078
|
+
polling ??= (async () => {
|
|
2079
|
+
try {
|
|
2080
|
+
const res = await send(url);
|
|
2081
|
+
if (!res.ok) return setStatus(NO_ENDPOINT);
|
|
2082
|
+
const body = await res.json();
|
|
2083
|
+
setStatus(body.available === true ? true : body.reason ?? UNAVAILABLE);
|
|
2084
|
+
} catch (err) {
|
|
2085
|
+
setStatus(`${NO_ENDPOINT}: ${messageOf(err)}`);
|
|
2086
|
+
} finally {
|
|
2087
|
+
polling = null;
|
|
2088
|
+
}
|
|
2089
|
+
})();
|
|
2090
|
+
};
|
|
2091
|
+
return {
|
|
2092
|
+
status: () => status,
|
|
2093
|
+
subscribe(listener) {
|
|
2094
|
+
listeners2.add(listener);
|
|
2095
|
+
poll();
|
|
2096
|
+
return () => listeners2.delete(listener);
|
|
2097
|
+
},
|
|
2098
|
+
poll,
|
|
2099
|
+
async take(payload) {
|
|
2100
|
+
const res = await send(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
|
|
2101
|
+
if (res.status === 503) {
|
|
2102
|
+
setStatus((await res.json()).reason ?? UNAVAILABLE);
|
|
2103
|
+
return null;
|
|
2104
|
+
}
|
|
2105
|
+
if (!res.ok) {
|
|
2106
|
+
const body = await res.json().catch(() => ({}));
|
|
2107
|
+
throw new Error(body.error ?? `capture failed: ${res.status}`);
|
|
2108
|
+
}
|
|
2109
|
+
return createImageBitmap(await res.blob());
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
var sideOf = (r) => `${r.lensId}:${r.dpr}${r.conditions === null ? "" : ":compared"}`;
|
|
2114
|
+
function aim(shared, request, doc) {
|
|
2115
|
+
const hit = trialAt(request.reticle, doc);
|
|
2116
|
+
if (!hit) return { reason: NOTHING_UNDER };
|
|
2117
|
+
const payload = shared.payloadFor(hit, request.dpr, request.conditions);
|
|
2118
|
+
if ("unavailable" in payload) return { reason: unavailableText("pixels", payload.unavailable) };
|
|
2119
|
+
return { hit, payload, key: stableStringify(payload) };
|
|
2120
|
+
}
|
|
2121
|
+
var fresh = (shared, shot, key) => shot?.key === key && shot.revision === shared.revision();
|
|
2122
|
+
function paint(canvas, shot, at, request) {
|
|
2123
|
+
const sd = canvas.ownerDocument.defaultView?.devicePixelRatio ?? 1;
|
|
2124
|
+
const width = Math.max(1, Math.round(request.size.width * sd));
|
|
2125
|
+
const height = Math.max(1, Math.round(request.size.height * sd));
|
|
2126
|
+
if (canvas.width !== width) canvas.width = width;
|
|
2127
|
+
if (canvas.height !== height) canvas.height = height;
|
|
2128
|
+
const ctx = canvas.getContext("2d");
|
|
2129
|
+
if (!ctx) return;
|
|
2130
|
+
ctx.imageSmoothingEnabled = false;
|
|
2131
|
+
ctx.clearRect(0, 0, width, height);
|
|
2132
|
+
const scale = request.factor * sd / shot.payload.dpr;
|
|
2133
|
+
const x = Math.round(width / 2 - at.x * request.factor * sd);
|
|
2134
|
+
const y = Math.round(height / 2 - at.y * request.factor * sd);
|
|
2135
|
+
ctx.drawImage(shot.bitmap, x, y, shot.bitmap.width * scale, shot.bitmap.height * scale);
|
|
2136
|
+
canvas.toggleAttribute("data-shown", true);
|
|
2137
|
+
}
|
|
2138
|
+
function blank(canvas) {
|
|
2139
|
+
canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
|
|
2140
|
+
canvas.toggleAttribute("data-shown", false);
|
|
2141
|
+
}
|
|
2142
|
+
function attachPixelsLens(slot, first, shared, onDetach) {
|
|
2143
|
+
const doc = slot.ownerDocument;
|
|
2144
|
+
const canvas = doc.createElement("canvas");
|
|
2145
|
+
canvas.className = "fg-lens-pixels";
|
|
2146
|
+
const empty = doc.createElement("div");
|
|
2147
|
+
empty.className = "fg-lens-empty";
|
|
2148
|
+
empty.hidden = true;
|
|
2149
|
+
slot.append(canvas, empty);
|
|
2150
|
+
let request = first;
|
|
2151
|
+
let wanted = null;
|
|
2152
|
+
let pending2 = null;
|
|
2153
|
+
let timer;
|
|
2154
|
+
let started = false;
|
|
2155
|
+
let gone = false;
|
|
2156
|
+
const say = (text) => {
|
|
2157
|
+
empty.hidden = text === null;
|
|
2158
|
+
empty.textContent = text ?? "";
|
|
2159
|
+
};
|
|
2160
|
+
const cancel = () => {
|
|
2161
|
+
clearTimeout(timer);
|
|
2162
|
+
timer = void 0;
|
|
2163
|
+
};
|
|
2164
|
+
const run = async () => {
|
|
2165
|
+
const target = wanted;
|
|
2166
|
+
if (!target) return;
|
|
2167
|
+
const token = {};
|
|
2168
|
+
pending2 = token;
|
|
2169
|
+
const at = shared.revision();
|
|
2170
|
+
const side = sideOf(request);
|
|
2171
|
+
try {
|
|
2172
|
+
const bitmap = await shared.take(target.payload);
|
|
2173
|
+
if (gone || pending2 !== token || bitmap === null || shared.revision() !== at) return;
|
|
2174
|
+
shared.shots.set(side, { key: target.key, revision: at, bitmap, payload: target.payload });
|
|
2175
|
+
measure();
|
|
2176
|
+
} catch (err) {
|
|
2177
|
+
if (!gone && pending2 === token) say(`Capture failed: ${messageOf(err)}`);
|
|
2178
|
+
} finally {
|
|
2179
|
+
if (pending2 === token) pending2 = null;
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
const schedule = () => {
|
|
2183
|
+
cancel();
|
|
2184
|
+
if (!started) {
|
|
2185
|
+
started = true;
|
|
2186
|
+
void run();
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
timer = setTimeout(() => {
|
|
2190
|
+
timer = void 0;
|
|
2191
|
+
void run();
|
|
2192
|
+
}, LENS_REST_MS);
|
|
2193
|
+
};
|
|
2194
|
+
const measure = () => {
|
|
2195
|
+
const aimed = aim(shared, request, doc);
|
|
2196
|
+
if ("reason" in aimed) {
|
|
2197
|
+
wanted = null;
|
|
2198
|
+
cancel();
|
|
2199
|
+
blank(canvas);
|
|
2200
|
+
say(aimed.reason);
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
const { hit, payload, key } = aimed;
|
|
2204
|
+
const shot = shared.shots.get(sideOf(request));
|
|
2205
|
+
if (fresh(shared, shot, key)) {
|
|
2206
|
+
wanted = { key, payload };
|
|
2207
|
+
cancel();
|
|
2208
|
+
say(null);
|
|
2209
|
+
paint(canvas, shot, hit.local, request);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
say(null);
|
|
2213
|
+
if (wanted?.key === key && pending2 !== null) return;
|
|
2214
|
+
wanted = { key, payload };
|
|
2215
|
+
schedule();
|
|
2216
|
+
};
|
|
2217
|
+
measure();
|
|
2218
|
+
return {
|
|
2219
|
+
measure,
|
|
2220
|
+
revised() {
|
|
2221
|
+
pending2 = null;
|
|
2222
|
+
wanted = null;
|
|
2223
|
+
blank(canvas);
|
|
2224
|
+
measure();
|
|
2225
|
+
},
|
|
2226
|
+
update(next) {
|
|
2227
|
+
request = next;
|
|
2228
|
+
measure();
|
|
2229
|
+
},
|
|
2230
|
+
detach() {
|
|
2231
|
+
gone = true;
|
|
2232
|
+
cancel();
|
|
2233
|
+
shared.shots.delete(sideOf(request));
|
|
2234
|
+
canvas.remove();
|
|
2235
|
+
empty.remove();
|
|
2236
|
+
onDetach();
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// src/shell/loupe/pixelsSource.ts
|
|
2242
|
+
var STORY_CHANGED = "the story changed while it was captured";
|
|
2243
|
+
function createPixelsSource(opts) {
|
|
2244
|
+
const client = createCaptureClient(opts.base, opts.fetch ?? ((input, init) => globalThis.fetch(input, init)));
|
|
2245
|
+
const shared = { take: client.take, payloadFor: opts.payloadFor, revision: opts.revision, shots: /* @__PURE__ */ new Map() };
|
|
2246
|
+
const lenses = /* @__PURE__ */ new Set();
|
|
2247
|
+
let unrevise = null;
|
|
2248
|
+
const remeasured = createEmitter();
|
|
2249
|
+
const revised = () => {
|
|
2250
|
+
shared.shots.clear();
|
|
2251
|
+
for (const lens of [...lenses]) lens.revised();
|
|
2252
|
+
};
|
|
2253
|
+
return {
|
|
2254
|
+
kind: "pixels",
|
|
2255
|
+
produces: "pixels",
|
|
2256
|
+
// A DPR comparison captures at its own DPR, as the payload does.
|
|
2257
|
+
label: ({ conditions, dpr }) => `Pixels ${(conditions?.kind === "custom" ? conditions.dpr : void 0) ?? dpr}\xD7`,
|
|
2258
|
+
status: client.status,
|
|
2259
|
+
// Also told whenever a capture may have gone stale, for a lens that captures without attaching, as Difference does.
|
|
2260
|
+
subscribe(listener) {
|
|
2261
|
+
const offs = [client.subscribe(listener), remeasured.subscribe(listener), opts.subscribeRevision(listener)];
|
|
2262
|
+
return () => {
|
|
2263
|
+
for (const off of offs) off();
|
|
2264
|
+
};
|
|
2265
|
+
},
|
|
2266
|
+
attach(slot, first) {
|
|
2267
|
+
client.poll();
|
|
2268
|
+
unrevise ??= opts.subscribeRevision(revised);
|
|
2269
|
+
const lens = attachPixelsLens(slot, first, shared, () => {
|
|
2270
|
+
lenses.delete(lens);
|
|
2271
|
+
if (lenses.size > 0) return;
|
|
2272
|
+
unrevise?.();
|
|
2273
|
+
unrevise = null;
|
|
2274
|
+
});
|
|
2275
|
+
lenses.add(lens);
|
|
2276
|
+
return lens;
|
|
2277
|
+
},
|
|
2278
|
+
async capture(request) {
|
|
2279
|
+
const aimed = aim(shared, request, document);
|
|
2280
|
+
if ("reason" in aimed) return { reason: aimed.reason };
|
|
2281
|
+
const side = sideOf(request);
|
|
2282
|
+
let shot = shared.shots.get(side);
|
|
2283
|
+
if (!fresh(shared, shot, aimed.key)) {
|
|
2284
|
+
const at = opts.revision();
|
|
2285
|
+
const bitmap = await client.take(aimed.payload);
|
|
2286
|
+
if (bitmap === null) {
|
|
2287
|
+
const status = client.status();
|
|
2288
|
+
return { reason: unavailableText("pixels", status === true ? "the capture was refused" : status) };
|
|
2289
|
+
}
|
|
2290
|
+
if (opts.revision() !== at) return { reason: STORY_CHANGED };
|
|
2291
|
+
shot = { key: aimed.key, revision: at, bitmap, payload: aimed.payload };
|
|
2292
|
+
shared.shots.set(side, shot);
|
|
2293
|
+
}
|
|
2294
|
+
return { bitmap: shot.bitmap, dpr: aimed.payload.dpr, box: aimed.payload.box, at: aimed.hit.local };
|
|
2295
|
+
},
|
|
2296
|
+
remeasure() {
|
|
2297
|
+
for (const lens of [...lenses]) lens.measure();
|
|
2298
|
+
remeasured.emit();
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
// src/shell/loupe/usePixelsSource.ts
|
|
2304
|
+
function usePixelsSource(lab) {
|
|
2305
|
+
const capture = useContext(LoupeCaptureContext);
|
|
2306
|
+
const [source] = useState(
|
|
2307
|
+
() => capture ? createPixelsSource({
|
|
2308
|
+
base: capture.base,
|
|
2309
|
+
payloadFor: (hit, dpr, c) => trialPayload(capture.trials.get(hit.trialId), lab.current, hit, dpr, c),
|
|
2310
|
+
revision: capture.revision.get,
|
|
2311
|
+
subscribeRevision: capture.revision.subscribe
|
|
2312
|
+
}) : null
|
|
2313
|
+
);
|
|
2314
|
+
useEffect(() => source && capture ? capture.trials.subscribe(() => source.remeasure()) : void 0, [source, capture]);
|
|
2315
|
+
return source;
|
|
2316
|
+
}
|
|
2317
|
+
var STAGGER = 24;
|
|
2318
|
+
var OPPOSITE = { kind: "opposite-mode" };
|
|
2319
|
+
var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
|
|
2320
|
+
function bodyBeside(reticle, box, n) {
|
|
2321
|
+
const half = BODY_SIZE / 2;
|
|
2322
|
+
const gap = BODY_SIZE * 0.75 + STAGGER * n;
|
|
2323
|
+
const x = reticle.x + gap + half > box.width ? reticle.x - gap : reticle.x + gap;
|
|
2324
|
+
return { x: clamp(x, half, box.width - half), y: clamp(reticle.y + STAGGER * n, half, box.height - half) };
|
|
2325
|
+
}
|
|
2326
|
+
function lensClient(scene, lensId, body) {
|
|
2327
|
+
const ids = lensIdsOf(scene, `${lensId}:body`);
|
|
2328
|
+
const reticle = ids ? scene.get(ids.reticle) : void 0;
|
|
2329
|
+
const lens = ids ? scene.get(ids.body) : void 0;
|
|
2330
|
+
if (!body || !reticle || !lens) return null;
|
|
2331
|
+
const box = body.getBoundingClientRect();
|
|
2332
|
+
const center = (p) => ({ x: box.left + p.x + p.width / 2, y: box.top + p.y + p.height / 2 });
|
|
2333
|
+
return { reticle: center(effectivePose(scene, reticle)), body: center(effectivePose(scene, lens)) };
|
|
2334
|
+
}
|
|
2335
|
+
function useLensFocus() {
|
|
2336
|
+
const focus = useRef(null);
|
|
2337
|
+
const onFocusLens = useCallback((lensId) => {
|
|
2338
|
+
focus.current = lensId;
|
|
2339
|
+
}, []);
|
|
2340
|
+
return { focus, onFocusLens };
|
|
2341
|
+
}
|
|
2342
|
+
function createApi(scene, refs) {
|
|
2343
|
+
let next = 0;
|
|
2344
|
+
let hold = null;
|
|
2345
|
+
const toBody = (client, box) => ({ x: client.x - box.left, y: client.y - box.top });
|
|
2346
|
+
const apply = (ops) => {
|
|
2347
|
+
if (ops.length > 0) sceneToAdapter(scene).applyOps([...ops], "lens");
|
|
2348
|
+
};
|
|
2349
|
+
const current = () => {
|
|
2350
|
+
const focused = refs.focus.current;
|
|
2351
|
+
if (focused !== null && settingsOf(scene, focused)) return focused;
|
|
2352
|
+
return lensesOf(scene).at(-1)?.lensId ?? null;
|
|
2353
|
+
};
|
|
2354
|
+
return {
|
|
2355
|
+
scene,
|
|
2356
|
+
current,
|
|
2357
|
+
apply,
|
|
2358
|
+
add(at) {
|
|
2359
|
+
const body = refs.body.current;
|
|
2360
|
+
const count = lensesOf(scene).length;
|
|
2361
|
+
if (!body || count >= MAX_LENSES) return null;
|
|
2362
|
+
const box = body.getBoundingClientRect();
|
|
2363
|
+
const client = at ?? refs.pointer.current ?? { x: box.left + box.width / 2, y: box.top + box.height / 2 };
|
|
2364
|
+
const reticle = toBody(client, box);
|
|
2365
|
+
const lensId = `lens-${next++}`;
|
|
2366
|
+
apply(addLensOps(scene, lensId, { body: bodyBeside(reticle, box, count), reticle }));
|
|
2367
|
+
refs.focus.current = lensId;
|
|
2368
|
+
return lensId;
|
|
2369
|
+
},
|
|
2370
|
+
follow(client) {
|
|
2371
|
+
const lensId = current();
|
|
2372
|
+
const body = refs.body.current;
|
|
2373
|
+
if (lensId === null || !body) return;
|
|
2374
|
+
const pose = reticleAt(scene, lensId, toBody(client, body.getBoundingClientRect()));
|
|
2375
|
+
if (pose === null) return;
|
|
2376
|
+
hold ??= scene.overrides.hold();
|
|
2377
|
+
hold.set([[`${lensId}:reticle`, pose]]);
|
|
2378
|
+
},
|
|
2379
|
+
unfollow: () => hold?.drop(),
|
|
2380
|
+
sourceAvailable: (kind) => refs.sources.current[kind]?.status() ?? "not provided",
|
|
2381
|
+
openCompareMenu: (lensId) => refs.compare?.current?.(lensId)
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
function useRemeasure(sources, body, rerender) {
|
|
2385
|
+
const { trials, mode } = useLabContext();
|
|
2386
|
+
const labValues = useContext(StoryGlobalsContext);
|
|
2387
|
+
useEffect(() => {
|
|
2388
|
+
for (const source of sources) source.remeasure();
|
|
2389
|
+
}, [sources, trials, mode, labValues]);
|
|
2390
|
+
useEffect(() => {
|
|
2391
|
+
if (sources.length === 0 || !body) return;
|
|
2392
|
+
const remeasure = () => {
|
|
2393
|
+
for (const source of sources) source.remeasure();
|
|
2394
|
+
};
|
|
2395
|
+
const resize = () => {
|
|
2396
|
+
rerender();
|
|
2397
|
+
remeasure();
|
|
2398
|
+
};
|
|
2399
|
+
body.addEventListener("scroll", remeasure, { capture: true, passive: true });
|
|
2400
|
+
window.addEventListener("resize", resize);
|
|
2401
|
+
const unveiled = typeof MutationObserver === "undefined" ? null : new MutationObserver(remeasure);
|
|
2402
|
+
unveiled?.observe(body, { subtree: true, attributes: true, attributeFilter: ["data-pending"] });
|
|
2403
|
+
return () => {
|
|
2404
|
+
body.removeEventListener("scroll", remeasure, { capture: true });
|
|
2405
|
+
window.removeEventListener("resize", resize);
|
|
2406
|
+
unveiled?.disconnect();
|
|
2407
|
+
};
|
|
2408
|
+
}, [sources, body, rerender]);
|
|
2409
|
+
}
|
|
2410
|
+
function ForgeLoupe({ declarations, modeGlobal }) {
|
|
2411
|
+
const lenses = useContext(LiveLensesContext);
|
|
2412
|
+
const labValues = useContext(StoryGlobalsContext);
|
|
2413
|
+
const { mode, trials, instruments } = useLabContext();
|
|
2414
|
+
const [scene] = useState(createLensScene);
|
|
2415
|
+
const [body, setBody] = useState(null);
|
|
2416
|
+
const bodyRef = useRef(null);
|
|
2417
|
+
const pointer = useRef(null);
|
|
2418
|
+
const { focus, onFocusLens } = useLensFocus();
|
|
2419
|
+
const conditions = useLatest({ labValues, mode, declarations, modeGlobal });
|
|
2420
|
+
const [live] = useState(
|
|
2421
|
+
() => lenses ? createLiveSource(lenses, resolveComparison, () => ({ base: conditions.current.labValues, ctx: resolveContextOf(conditions.current) })) : null
|
|
2422
|
+
);
|
|
2423
|
+
const pixels = usePixelsSource(conditions);
|
|
2424
|
+
const sources = useMemo(
|
|
2425
|
+
() => ({ ...live ? { live } : {}, ...pixels ? { pixels } : {} }),
|
|
2426
|
+
[live, pixels]
|
|
2427
|
+
);
|
|
2428
|
+
const remeasured = useMemo(() => [live, pixels].filter((s) => s !== null), [live, pixels]);
|
|
2429
|
+
const sourcesRef = useLatest(sources);
|
|
2430
|
+
const [menu, setMenu] = useState(null);
|
|
2431
|
+
const compare = useLatest((lensId) => {
|
|
2432
|
+
const at = lensClient(scene, lensId, bodyRef.current);
|
|
2433
|
+
if (!at) return;
|
|
2434
|
+
const trialId = trialAt(at.reticle)?.trialId;
|
|
2435
|
+
const name = trials.find((t) => t.id === trialId)?.instrumentName;
|
|
2436
|
+
const schema = instruments.find((i) => i.name === name)?.config ?? null;
|
|
2437
|
+
const settings = settingsOf(scene, lensId);
|
|
2438
|
+
const pixels2 = settings !== null && (settings.view === "difference" || resolveSource(settings.source, sourcesRef.current).source?.kind === "pixels");
|
|
2439
|
+
const choices = compareChoices(declarations, schema);
|
|
2440
|
+
setMenu((last) => ({ seq: (last?.seq ?? 0) + 1, client: at.body, lensId, pixels: pixels2, choices }));
|
|
2441
|
+
});
|
|
2442
|
+
const [api] = useState(() => createApi(scene, { body: bodyRef, pointer, focus, sources: sourcesRef, compare }));
|
|
2443
|
+
const describe = useCallback(
|
|
2444
|
+
(c, at) => {
|
|
2445
|
+
const trialId = trialAt(at.reticle)?.trialId;
|
|
2446
|
+
const config = trials.find((t) => t.id === trialId)?.config;
|
|
2447
|
+
return describeLens(c, { labValues, mode, declarations, modeGlobal }, config, at.source === "pixels");
|
|
2448
|
+
},
|
|
2449
|
+
[declarations, labValues, mode, modeGlobal, trials]
|
|
2450
|
+
);
|
|
2451
|
+
const [, rerender] = useReducer((n) => n + 1, 0);
|
|
2452
|
+
useSyncExternalStore(scene.overrides.subscribe, scene.overrides.getGeneration);
|
|
2453
|
+
useRemeasure(remeasured, body, rerender);
|
|
2454
|
+
useEffect(() => {
|
|
2455
|
+
if (!body) return;
|
|
2456
|
+
const track = (e) => {
|
|
2457
|
+
pointer.current = { x: e.clientX, y: e.clientY };
|
|
2458
|
+
};
|
|
2459
|
+
body.addEventListener("pointermove", track, { passive: true });
|
|
2460
|
+
return () => body.removeEventListener("pointermove", track);
|
|
2461
|
+
}, [body]);
|
|
2462
|
+
const attachMarker = useCallback((el) => {
|
|
2463
|
+
const found = el?.closest(".lk-lab__body") ?? null;
|
|
2464
|
+
bodyRef.current = found;
|
|
2465
|
+
setBody(found);
|
|
2466
|
+
}, []);
|
|
2467
|
+
const opposite = useCallback(() => OPPOSITE, []);
|
|
2468
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2469
|
+
/* @__PURE__ */ jsx("span", { hidden: true, ref: attachMarker }),
|
|
2470
|
+
/* @__PURE__ */ jsx(LensOverlay, { scene, sources, describe, renderReadouts, onFocusLens }),
|
|
2471
|
+
/* @__PURE__ */ jsx(LensActions, { api, bodyRef, opposite }),
|
|
2472
|
+
/* @__PURE__ */ jsx(CompareMenu, { api, request: menu }),
|
|
2473
|
+
/* @__PURE__ */ jsx(KeymapHint, { className: "fg-loupe-hint" })
|
|
2474
|
+
] });
|
|
2475
|
+
}
|
|
1205
2476
|
|
|
1206
2477
|
// src/shell/routedTrial.ts
|
|
1207
2478
|
function routedTrial(trials, focusedTrialId, route) {
|
|
@@ -1253,9 +2524,9 @@ function RoutePlayhead() {
|
|
|
1253
2524
|
if (!trial || route === null) return null;
|
|
1254
2525
|
return /* @__PURE__ */ jsx(PlayheadSync, { trialId: trial.id, route, appliedRef: applied }, `${trial.id} ${route}`);
|
|
1255
2526
|
}
|
|
1256
|
-
var
|
|
2527
|
+
var RECORD3 = "fg-globals";
|
|
1257
2528
|
function useLabGlobals(declarations) {
|
|
1258
|
-
const [stored, setStored] = usePersistedState(
|
|
2529
|
+
const [stored, setStored] = usePersistedState(RECORD3, null, { scope: "lab" });
|
|
1259
2530
|
const values = useMemo(() => labGlobals(declarations, stored), [declarations, stored]);
|
|
1260
2531
|
const set = useCallback(
|
|
1261
2532
|
(key, value) => setStored((prev) => ({ ...labGlobals(declarations, prev), [key]: value })),
|
|
@@ -1462,6 +2733,16 @@ function foldGalleries(nodes) {
|
|
|
1462
2733
|
return { ...node, children };
|
|
1463
2734
|
});
|
|
1464
2735
|
}
|
|
2736
|
+
function hoistLoneStories(nodes) {
|
|
2737
|
+
return nodes.map((node) => {
|
|
2738
|
+
if (node.kind !== "folder") return node;
|
|
2739
|
+
const [only] = node.children;
|
|
2740
|
+
if (node.children.length === 1 && only?.kind === "story" && restatesTitle(node.path, only.entry.name)) {
|
|
2741
|
+
return { kind: "story", entry: only.entry, label: node.label, tag: node.tag ?? only.tag };
|
|
2742
|
+
}
|
|
2743
|
+
return { ...node, children: hoistLoneStories(node.children) };
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
1465
2746
|
function buildTree(index) {
|
|
1466
2747
|
const root = [];
|
|
1467
2748
|
const folders = /* @__PURE__ */ new Map();
|
|
@@ -1482,7 +2763,7 @@ function buildTree(index) {
|
|
|
1482
2763
|
}
|
|
1483
2764
|
siblings.push({ kind: "story", entry });
|
|
1484
2765
|
}
|
|
1485
|
-
return sortFolder(root);
|
|
2766
|
+
return hoistLoneStories(sortFolder(root));
|
|
1486
2767
|
}
|
|
1487
2768
|
function filterTree(nodes, query) {
|
|
1488
2769
|
const needle = query.trim().toLowerCase();
|
|
@@ -1606,7 +2887,7 @@ function componentNodes(rows) {
|
|
|
1606
2887
|
...row.entries.map((entry) => ({ kind: "story", entry }))
|
|
1607
2888
|
];
|
|
1608
2889
|
}
|
|
1609
|
-
return foldGalleries(root);
|
|
2890
|
+
return hoistLoneStories(foldGalleries(root));
|
|
1610
2891
|
}
|
|
1611
2892
|
function librariesIn(index, libraries) {
|
|
1612
2893
|
return [...new Set(index.map(libraries.of))].sort((a, b) => a.localeCompare(b));
|
|
@@ -1630,6 +2911,12 @@ function ancestorsOf(entry) {
|
|
|
1630
2911
|
function LibraryBadge2({ library }) {
|
|
1631
2912
|
return /* @__PURE__ */ jsx(LibraryBadge, { library, className: "fg-tree__tag" });
|
|
1632
2913
|
}
|
|
2914
|
+
function RowTags({ lab, library }) {
|
|
2915
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2916
|
+
lab ? /* @__PURE__ */ jsx(LabBadge, { className: "fg-tree__lab" }) : null,
|
|
2917
|
+
library ? /* @__PURE__ */ jsx(LibraryBadge2, { library }) : null
|
|
2918
|
+
] });
|
|
2919
|
+
}
|
|
1633
2920
|
function StoryTree({ ctx, index }) {
|
|
1634
2921
|
const headingId = useId();
|
|
1635
2922
|
const [route, setRoute2] = useRoute();
|
|
@@ -1668,13 +2955,13 @@ function StoryTree({ ctx, index }) {
|
|
|
1668
2955
|
const filtering = query.trim() !== "";
|
|
1669
2956
|
const isOpen = (path) => filtering || (folds[path] ?? routeAncestors.has(path));
|
|
1670
2957
|
const rows = [];
|
|
1671
|
-
const
|
|
2958
|
+
const collect2 = (nodes, parent) => {
|
|
1672
2959
|
for (const node of nodes) {
|
|
1673
2960
|
rows.push({ key: keyOf(node), node, parent });
|
|
1674
|
-
if (node.kind === "folder" && isOpen(node.path))
|
|
2961
|
+
if (node.kind === "folder" && isOpen(node.path)) collect2(node.children, keyOf(node));
|
|
1675
2962
|
}
|
|
1676
2963
|
};
|
|
1677
|
-
|
|
2964
|
+
collect2(shown, null);
|
|
1678
2965
|
const active = rows.some((row) => row.key === focusKey) ? focusKey : rows[0]?.key ?? null;
|
|
1679
2966
|
const openIds = new Set(ctx.trials.map((trial) => trial.instrumentName));
|
|
1680
2967
|
const setOpen = (path, open) => {
|
|
@@ -1837,7 +3124,7 @@ function StoryTree({ ctx, index }) {
|
|
|
1837
3124
|
}
|
|
1838
3125
|
),
|
|
1839
3126
|
/* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: node.label }),
|
|
1840
|
-
|
|
3127
|
+
/* @__PURE__ */ jsx(RowTags, { lab: !!node.index && isLab(node.index), library: node.tag })
|
|
1841
3128
|
]
|
|
1842
3129
|
}
|
|
1843
3130
|
),
|
|
@@ -1863,7 +3150,7 @@ function StoryTree({ ctx, index }) {
|
|
|
1863
3150
|
},
|
|
1864
3151
|
children: [
|
|
1865
3152
|
/* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: node.label ?? entry.name }),
|
|
1866
|
-
|
|
3153
|
+
/* @__PURE__ */ jsx(RowTags, { lab: isLab(entry), library: node.tag })
|
|
1867
3154
|
]
|
|
1868
3155
|
},
|
|
1869
3156
|
keyOf(node)
|
|
@@ -1953,6 +3240,171 @@ function createAnswerBook() {
|
|
|
1953
3240
|
|
|
1954
3241
|
// src/shell/readyKey.ts
|
|
1955
3242
|
var readyKey = (ready) => stableStringify({ schema: ready.schema, layout: ready.layout, viewport: ready.viewport });
|
|
3243
|
+
var START_TIMEOUT_MS = 1e4;
|
|
3244
|
+
var requests = 0;
|
|
3245
|
+
function closeLink(link) {
|
|
3246
|
+
if (!link.current) return;
|
|
3247
|
+
clearTimeout(link.current.timer);
|
|
3248
|
+
for (const settle of link.current.pending.values()) settle({ ok: false, message: "The frame went away" });
|
|
3249
|
+
link.current.pending.clear();
|
|
3250
|
+
link.current.disconnect();
|
|
3251
|
+
link.current.channel.close();
|
|
3252
|
+
link.current = null;
|
|
3253
|
+
}
|
|
3254
|
+
function mismatchMessage(mismatch) {
|
|
3255
|
+
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";
|
|
3256
|
+
}
|
|
3257
|
+
function sendInputs(current, sent, next) {
|
|
3258
|
+
const before = current.inputs;
|
|
3259
|
+
const { state, globals } = next;
|
|
3260
|
+
const config = storyConfig(next.config, sent.config);
|
|
3261
|
+
if (config !== sent.config) {
|
|
3262
|
+
sent.config = config;
|
|
3263
|
+
current.channel.send({ type: "config", config });
|
|
3264
|
+
current.inputs += 1;
|
|
3265
|
+
}
|
|
3266
|
+
if (state !== sent.state) {
|
|
3267
|
+
sent.state = state;
|
|
3268
|
+
current.channel.send({ type: "state", state });
|
|
3269
|
+
current.inputs += 1;
|
|
3270
|
+
}
|
|
3271
|
+
if (globals !== sent.globals && stableStringify(globals) !== stableStringify(sent.globals)) {
|
|
3272
|
+
sent.globals = globals;
|
|
3273
|
+
current.channel.send({ type: "globals", globals });
|
|
3274
|
+
current.inputs += 1;
|
|
3275
|
+
}
|
|
3276
|
+
return current.inputs !== before;
|
|
3277
|
+
}
|
|
3278
|
+
function receive(deps, current, msg) {
|
|
3279
|
+
const { latest: latestRef, trialId, frames, setFault, setPending, init, copy } = deps;
|
|
3280
|
+
const { ctx: live, answers, onReady } = latestRef.current;
|
|
3281
|
+
switch (msg.type) {
|
|
3282
|
+
case "ready": {
|
|
3283
|
+
clearTimeout(current.timer);
|
|
3284
|
+
setFault(null);
|
|
3285
|
+
for (const [name, value] of Object.entries(latestRef.current.overrides)) {
|
|
3286
|
+
current.channel.send({ type: "vars.set", name, value });
|
|
3287
|
+
}
|
|
3288
|
+
if (copy) {
|
|
3289
|
+
init(current);
|
|
3290
|
+
break;
|
|
3291
|
+
}
|
|
3292
|
+
const key = readyKey(msg);
|
|
3293
|
+
current.awaiting = key;
|
|
3294
|
+
onReady(latestRef.current.entry, msg);
|
|
3295
|
+
if (current.awaiting === key && key === latestRef.current.descriptionKey) init(current);
|
|
3296
|
+
break;
|
|
3297
|
+
}
|
|
3298
|
+
case "rendered":
|
|
3299
|
+
setPending(false);
|
|
3300
|
+
current.hasRendered = true;
|
|
3301
|
+
for (const ask of current.queued.splice(0)) ask();
|
|
3302
|
+
break;
|
|
3303
|
+
case "setConfig":
|
|
3304
|
+
if (!isGlobalsPath(msg.path)) live.setConfig(msg.path, msg.value);
|
|
3305
|
+
break;
|
|
3306
|
+
case "setState":
|
|
3307
|
+
if (current.sent) current.sent.state = msg.state;
|
|
3308
|
+
live.setState(msg.state);
|
|
3309
|
+
break;
|
|
3310
|
+
case "answers":
|
|
3311
|
+
if (!copy) answers.record(msg.answers);
|
|
3312
|
+
break;
|
|
3313
|
+
case "vars":
|
|
3314
|
+
if (trialId) frames?.report(trialId, msg.vars);
|
|
3315
|
+
break;
|
|
3316
|
+
case "a11y": {
|
|
3317
|
+
const outcome = msg.ok ? { ok: true, report: msg.report } : { ok: false, message: msg.message };
|
|
3318
|
+
if (trialId) frames?.reportA11y(trialId, outcome);
|
|
3319
|
+
current.pending.get(msg.id)?.(outcome);
|
|
3320
|
+
current.pending.delete(msg.id);
|
|
3321
|
+
break;
|
|
3322
|
+
}
|
|
3323
|
+
case "open": {
|
|
3324
|
+
const { lab: liveLab } = latestRef.current;
|
|
3325
|
+
if (!trialId || !liveLab.instruments.some((instrument) => instrument.name === msg.id)) break;
|
|
3326
|
+
liveLab.swapTrial(trialId, msg.id);
|
|
3327
|
+
setRoute(msg.id, { inPlace: true });
|
|
3328
|
+
break;
|
|
3329
|
+
}
|
|
3330
|
+
case "fault":
|
|
3331
|
+
if (msg.phase === "render" && msg.seq !== void 0 && msg.seq < current.inputs) break;
|
|
3332
|
+
clearTimeout(current.timer);
|
|
3333
|
+
setFault({ phase: msg.phase, message: msg.message });
|
|
3334
|
+
break;
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
function openLink(deps, link, frame) {
|
|
3338
|
+
const { latest: latestRef, src, trialId, frames, setFault, setPending } = deps;
|
|
3339
|
+
closeLink(link);
|
|
3340
|
+
setPending(true);
|
|
3341
|
+
const target = frame.contentWindow;
|
|
3342
|
+
if (!target) return;
|
|
3343
|
+
const { port1, port2 } = new MessageChannel();
|
|
3344
|
+
const timer = setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), START_TIMEOUT_MS);
|
|
3345
|
+
const channel = openChannel(port1, {
|
|
3346
|
+
onMismatch: (mismatch) => {
|
|
3347
|
+
clearTimeout(timer);
|
|
3348
|
+
setFault({ phase: "protocol", message: mismatchMessage(mismatch) });
|
|
3349
|
+
}
|
|
3350
|
+
});
|
|
3351
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
3352
|
+
const audit = () => new Promise((resolve) => {
|
|
3353
|
+
const id = `a11y-${requests += 1}`;
|
|
3354
|
+
waiting.set(id, resolve);
|
|
3355
|
+
const ask = () => channel.send({ type: "a11y.run", id });
|
|
3356
|
+
if (current.hasRendered) ask();
|
|
3357
|
+
else current.queued.push(ask);
|
|
3358
|
+
});
|
|
3359
|
+
const disconnect = frames && trialId ? frames.connect(trialId, { send: channel.send, audit }) : () => {
|
|
3360
|
+
};
|
|
3361
|
+
const current = {
|
|
3362
|
+
channel,
|
|
3363
|
+
deliver: (msg) => receive(deps, current, msg),
|
|
3364
|
+
timer,
|
|
3365
|
+
awaiting: null,
|
|
3366
|
+
sent: null,
|
|
3367
|
+
inputs: 0,
|
|
3368
|
+
disconnect,
|
|
3369
|
+
pending: waiting,
|
|
3370
|
+
hasRendered: false,
|
|
3371
|
+
queued: []
|
|
3372
|
+
};
|
|
3373
|
+
link.current = current;
|
|
3374
|
+
channel.on((msg) => current.deliver(msg));
|
|
3375
|
+
target.postMessage({ type: PORT_HANDOFF, id: latestRef.current.entry.id }, location.origin, [
|
|
3376
|
+
port2
|
|
3377
|
+
]);
|
|
3378
|
+
}
|
|
3379
|
+
function useFrameLink(opts) {
|
|
3380
|
+
const { latest: latestRef, src, trialId, frames, setFault, setPending, copy, config, state, globals, descriptionKey } = opts;
|
|
3381
|
+
const link = useRef(null);
|
|
3382
|
+
const init = useCallback((current) => {
|
|
3383
|
+
const { ctx: live, globals: liveGlobals } = latestRef.current;
|
|
3384
|
+
current.awaiting = null;
|
|
3385
|
+
current.sent = { config: storyConfig(live.config), state: live.state, globals: liveGlobals };
|
|
3386
|
+
current.inputs += 1;
|
|
3387
|
+
current.channel.send({ type: "init", ...current.sent });
|
|
3388
|
+
}, [latestRef]);
|
|
3389
|
+
const deps = { latest: latestRef, src, trialId, frames, setFault, setPending, copy, init };
|
|
3390
|
+
const connect = (frame) => openLink(deps, link, frame);
|
|
3391
|
+
const adopt2 = (adopted) => {
|
|
3392
|
+
adopted.deliver = (msg) => receive(deps, adopted, msg);
|
|
3393
|
+
setPending(!adopted.hasRendered);
|
|
3394
|
+
};
|
|
3395
|
+
useEffect(() => {
|
|
3396
|
+
const current = link.current;
|
|
3397
|
+
if (!current) return;
|
|
3398
|
+
if (current.awaiting !== null) {
|
|
3399
|
+
if (current.awaiting === descriptionKey) init(current);
|
|
3400
|
+
return;
|
|
3401
|
+
}
|
|
3402
|
+
if (current.sent && sendInputs(current, current.sent, { config, state, globals })) {
|
|
3403
|
+
setFault((shown) => shown?.phase === "render" ? null : shown);
|
|
3404
|
+
}
|
|
3405
|
+
}, [config, state, globals, descriptionKey, init, setFault]);
|
|
3406
|
+
return { connect, adopt: adopt2, link };
|
|
3407
|
+
}
|
|
1956
3408
|
var kept = /* @__PURE__ */ new Map();
|
|
1957
3409
|
var shelf = null;
|
|
1958
3410
|
function shelfElement() {
|
|
@@ -2001,46 +3453,56 @@ function useKeptFrame(options) {
|
|
|
2001
3453
|
}, [latest]);
|
|
2002
3454
|
return useCallback(() => handedOn.current, []);
|
|
2003
3455
|
}
|
|
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;
|
|
3456
|
+
function mountFrame(m) {
|
|
3457
|
+
const { inView, src, slotRef, iframeRef, latest: latestRef, connect: connectRef, link, handedOn, setFault, setPending } = m;
|
|
3458
|
+
const slot = slotRef.current;
|
|
3459
|
+
if (!inView || !slot) return void 0;
|
|
3460
|
+
const adopted = iframeRef.current;
|
|
3461
|
+
const claimed = adopted || latestRef.current.copy ? null : latestRef.current.pool?.claim() ?? null;
|
|
3462
|
+
const frame = adopted ?? claimed ?? document.createElement("iframe");
|
|
3463
|
+
frame.className = "fg-frame-view";
|
|
3464
|
+
frame.title = latestRef.current.title;
|
|
3465
|
+
frame.removeAttribute("tabindex");
|
|
3466
|
+
frame.toggleAttribute("data-pending", true);
|
|
3467
|
+
iframeRef.current = frame;
|
|
3468
|
+
const unheard = adopted || claimed ? void 0 : setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), HELLO_TIMEOUT_MS);
|
|
3469
|
+
const onMessage = (event) => {
|
|
3470
|
+
if (event.source !== frame.contentWindow || event.origin !== location.origin) return;
|
|
3471
|
+
if (event.data?.type !== FRAME_HELLO) return;
|
|
3472
|
+
clearTimeout(unheard);
|
|
3473
|
+
connectRef.current(frame);
|
|
3474
|
+
};
|
|
3475
|
+
window.addEventListener("message", onMessage);
|
|
3476
|
+
if (claimed) {
|
|
3477
|
+
moveFrame(slot, claimed);
|
|
3478
|
+
connectRef.current(claimed);
|
|
3479
|
+
} else if (!adopted) {
|
|
3480
|
+
frame.src = src;
|
|
3481
|
+
slot.append(frame);
|
|
2036
3482
|
}
|
|
2037
|
-
return
|
|
3483
|
+
return () => {
|
|
3484
|
+
window.removeEventListener("message", onMessage);
|
|
3485
|
+
clearTimeout(unheard);
|
|
3486
|
+
if (handedOn()) return;
|
|
3487
|
+
closeLink(link);
|
|
3488
|
+
frame.remove();
|
|
3489
|
+
iframeRef.current = null;
|
|
3490
|
+
setPending(true);
|
|
3491
|
+
setFault(null);
|
|
3492
|
+
};
|
|
2038
3493
|
}
|
|
3494
|
+
var NO_ARGS = {};
|
|
2039
3495
|
function FrameView(props) {
|
|
2040
|
-
const { entry, frameUrl, answers, descriptionKey, ctx } = props;
|
|
3496
|
+
const { entry, frameUrl, answers, descriptionKey, ctx: trialCtx, copy } = props;
|
|
2041
3497
|
const labGlobals2 = useContext(StoryGlobalsContext);
|
|
2042
|
-
const pins =
|
|
2043
|
-
const
|
|
3498
|
+
const pins = trialCtx.config?.[GLOBALS_KEY];
|
|
3499
|
+
const own = useMemo(() => effectiveGlobals(labGlobals2, pins), [labGlobals2, pins]);
|
|
3500
|
+
const globals = copy ? copy.globals : own;
|
|
3501
|
+
const { config: laidConfig, setConfig: laidSetConfig } = useCopyConfig(trialCtx.config, trialCtx.setConfig, copy?.args ?? NO_ARGS);
|
|
3502
|
+
const ctx = useMemo(
|
|
3503
|
+
() => copy ? { ...trialCtx, config: laidConfig, setConfig: laidSetConfig } : trialCtx,
|
|
3504
|
+
[copy, trialCtx, laidConfig, laidSetConfig]
|
|
3505
|
+
);
|
|
2044
3506
|
const trialId = useContext(TrialIdContext);
|
|
2045
3507
|
const frames = useContext(TrialFramesContext);
|
|
2046
3508
|
const [overrides] = useCssOverrides();
|
|
@@ -2052,185 +3514,49 @@ function FrameView(props) {
|
|
|
2052
3514
|
const hostRef = useRef(null);
|
|
2053
3515
|
const slotRef = useRef(null);
|
|
2054
3516
|
const inView = useNearViewport(hostRef, IN_VIEW_MARGIN);
|
|
2055
|
-
const
|
|
2056
|
-
const latestRef = useLatest({ ...props, globals, overrides, pool, lab, title });
|
|
3517
|
+
const latestRef = useLatest({ ...props, ctx, globals, overrides, pool, lab, title });
|
|
2057
3518
|
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
|
-
};
|
|
3519
|
+
const [pending2, setPending] = useState(true);
|
|
3520
|
+
const { connect, adopt: adopt2, link } = useFrameLink({
|
|
3521
|
+
latest: latestRef,
|
|
3522
|
+
src,
|
|
3523
|
+
trialId: copy ? null : trialId,
|
|
3524
|
+
frames: copy ? null : frames,
|
|
3525
|
+
setFault,
|
|
3526
|
+
setPending,
|
|
3527
|
+
copy: copy !== void 0,
|
|
3528
|
+
config: ctx.config,
|
|
3529
|
+
state: ctx.state,
|
|
3530
|
+
globals,
|
|
3531
|
+
descriptionKey
|
|
3532
|
+
});
|
|
2161
3533
|
const connectRef = useLatest(connect);
|
|
2162
3534
|
const handedOn = useKeptFrame({
|
|
2163
|
-
key: trialId ? `${trialId} ${src}` : null,
|
|
3535
|
+
key: trialId && !copy ? `${trialId} ${src}` : null,
|
|
2164
3536
|
slot: slotRef,
|
|
2165
3537
|
frame: iframeRef,
|
|
2166
3538
|
link,
|
|
2167
|
-
onAdopt:
|
|
2168
|
-
adopted.deliver = (msg) => receive(adopted, msg);
|
|
2169
|
-
setPending(!adopted.hasRendered);
|
|
2170
|
-
},
|
|
3539
|
+
onAdopt: adopt2,
|
|
2171
3540
|
onDrop: () => closeLink(link)
|
|
2172
3541
|
});
|
|
3542
|
+
useEffect(
|
|
3543
|
+
() => mountFrame({ inView, src, slotRef, iframeRef, latest: latestRef, connect: connectRef, link, handedOn, setFault, setPending }),
|
|
3544
|
+
[inView, src, connectRef, latestRef, handedOn, link]
|
|
3545
|
+
);
|
|
2173
3546
|
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]);
|
|
3547
|
+
iframeRef.current?.toggleAttribute("data-pending", pending2 && !fault);
|
|
3548
|
+
}, [pending2, fault, inView, src]);
|
|
2213
3549
|
useEffect(() => {
|
|
2214
3550
|
if (iframeRef.current) iframeRef.current.title = title;
|
|
2215
3551
|
}, [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: [
|
|
3552
|
+
useEffect(() => copy ? void 0 : answers.hold(ctx.config), [answers, ctx.config, copy]);
|
|
3553
|
+
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
3554
|
/* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
|
|
2230
3555
|
fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
|
|
2231
3556
|
fault.phase ? /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: fault.phase }) : null,
|
|
2232
3557
|
/* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
|
|
2233
|
-
] }) : null
|
|
3558
|
+
] }) : null,
|
|
3559
|
+
copy ? null : /* @__PURE__ */ jsx(LiveFrameCopies, { trialId, trialConfig: trialCtx.config, children: (lensCopy) => /* @__PURE__ */ jsx(FrameView, { ...props, copy: lensCopy }) })
|
|
2234
3560
|
] });
|
|
2235
3561
|
}
|
|
2236
3562
|
function storyInstrument(options) {
|
|
@@ -2247,17 +3573,20 @@ function storyInstrument(options) {
|
|
|
2247
3573
|
config,
|
|
2248
3574
|
defaultConfig: () => config.defaults(),
|
|
2249
3575
|
initialState: () => null,
|
|
2250
|
-
render: (ctx) => /* @__PURE__ */
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
3576
|
+
render: (ctx) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3577
|
+
/* @__PURE__ */ jsx(
|
|
3578
|
+
FrameView,
|
|
3579
|
+
{
|
|
3580
|
+
entry,
|
|
3581
|
+
frameUrl,
|
|
3582
|
+
answers,
|
|
3583
|
+
onReady,
|
|
3584
|
+
descriptionKey,
|
|
3585
|
+
ctx
|
|
3586
|
+
}
|
|
3587
|
+
),
|
|
3588
|
+
/* @__PURE__ */ jsx(CaptureInputs, { entry, layout: ready?.layout ?? "centered", config: ctx.config, state: ctx.state })
|
|
3589
|
+
] }),
|
|
2261
3590
|
// Taken from the index until the frame reports, so the trial does not move the frame into a stage and reload it.
|
|
2262
3591
|
...viewport ? { stage: { size: { width: viewport.width, height: viewport.height } } } : {}
|
|
2263
3592
|
};
|
|
@@ -2274,7 +3603,7 @@ function useStoryRegistry(index, options) {
|
|
|
2274
3603
|
const [loaded, setLoaded] = useState(() => /* @__PURE__ */ new Map());
|
|
2275
3604
|
const [faults, setFaults] = useState(() => /* @__PURE__ */ new Map());
|
|
2276
3605
|
const [generations, setGenerations] = useState(() => /* @__PURE__ */ new Map());
|
|
2277
|
-
const [
|
|
3606
|
+
const [revisions2, setRevisions] = useState(() => /* @__PURE__ */ new Map());
|
|
2278
3607
|
const onReady = useCallback((entry, ready) => {
|
|
2279
3608
|
setReadies((prev) => {
|
|
2280
3609
|
const held = prev.get(entry.id);
|
|
@@ -2341,7 +3670,7 @@ function useStoryRegistry(index, options) {
|
|
|
2341
3670
|
library: liveRef.current.libraries.of(entry),
|
|
2342
3671
|
...entry.componentDescription === void 0 ? {} : { description: entry.componentDescription },
|
|
2343
3672
|
...entry.tags === void 0 ? {} : { tags: entry.tags },
|
|
2344
|
-
|
|
3673
|
+
...taggedIds(entries),
|
|
2345
3674
|
stories,
|
|
2346
3675
|
descriptions: Object.fromEntries(entries.flatMap((e) => e.description ? [[e.id, e.description]] : [])),
|
|
2347
3676
|
render: mods.map(indexRenderOf).find((render) => render !== null) ?? null
|
|
@@ -2397,7 +3726,7 @@ function useStoryRegistry(index, options) {
|
|
|
2397
3726
|
const had = loaded.get(entry.id);
|
|
2398
3727
|
const fault = faults.get(entry.id);
|
|
2399
3728
|
const generation = generations.get(entry.file) ?? 0;
|
|
2400
|
-
const revision =
|
|
3729
|
+
const revision = revisions2.get(entry.id) ?? 0;
|
|
2401
3730
|
const entryKey = stableStringify(entry);
|
|
2402
3731
|
const answers = previous.books.get(entry.id) ?? createAnswerBook();
|
|
2403
3732
|
books.set(entry.id, answers);
|
|
@@ -2420,7 +3749,7 @@ function useStoryRegistry(index, options) {
|
|
|
2420
3749
|
});
|
|
2421
3750
|
const unchanged = list.length === previous.list.length && list.every((i, n) => i === previous.list[n]);
|
|
2422
3751
|
return { built, books, list: unchanged ? previous.list : list };
|
|
2423
|
-
}, [index, readies, loaded, faults, generations,
|
|
3752
|
+
}, [index, readies, loaded, faults, generations, revisions2, frameUrl, globals, setup, importers, onReady, request]);
|
|
2424
3753
|
useLayoutEffect(() => {
|
|
2425
3754
|
committed.current = cache;
|
|
2426
3755
|
}, [cache]);
|
|
@@ -2465,7 +3794,8 @@ function NoStories({ stories }) {
|
|
|
2465
3794
|
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
3795
|
] });
|
|
2467
3796
|
}
|
|
2468
|
-
function Workshop(
|
|
3797
|
+
function Workshop(props) {
|
|
3798
|
+
const { index, frameUrl, importers, setup, changes, config, stories = [], storageKey, storage, dependencies } = props;
|
|
2469
3799
|
const declarations = config?.globals ?? NO_DECLARATIONS;
|
|
2470
3800
|
const title = config?.title ?? DEFAULT_TITLE;
|
|
2471
3801
|
useDocumentTitle(title);
|
|
@@ -2510,26 +3840,30 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
|
|
|
2510
3840
|
);
|
|
2511
3841
|
const infoCommand = useMemo(() => infoItem(() => setInfoOpen(true)), []);
|
|
2512
3842
|
const showsCssVars = config?.cssVars !== false;
|
|
3843
|
+
const modeGlobal = config?.loupe?.modeGlobal ?? "mode";
|
|
2513
3844
|
const [cssVarsChrome2, setCssVarsChrome] = useState([]);
|
|
3845
|
+
const [keysChrome2, setKeysChrome] = useState([]);
|
|
2514
3846
|
const labChrome = useMemo(
|
|
2515
3847
|
() => [
|
|
2516
3848
|
{ id: "fg-stories", region: "sidebar", render: (ctx) => /* @__PURE__ */ jsx(StoryTree, { ctx, index }) },
|
|
2517
3849
|
...showsCssVars ? cssVarsChrome2 : [],
|
|
2518
3850
|
A11Y_SECTION,
|
|
3851
|
+
...keysChrome2,
|
|
2519
3852
|
...Object.keys(declarations).length > 0 ? [{ id: "fg-globals", region: "header", render: () => /* @__PURE__ */ jsx(GlobalsToolbar, { declarations }) }] : [],
|
|
2520
3853
|
{ id: "fg-info", region: "header", group: "story", item: infoCommand },
|
|
3854
|
+
{ id: "fg-loupe", region: "overlay", render: () => /* @__PURE__ */ jsx(ForgeLoupe, { declarations, modeGlobal }) },
|
|
2521
3855
|
...config?.labChrome ?? []
|
|
2522
3856
|
],
|
|
2523
|
-
[index, infoCommand, declarations, showsCssVars, cssVarsChrome2, config?.labChrome]
|
|
3857
|
+
[index, infoCommand, declarations, showsCssVars, cssVarsChrome2, keysChrome2, config?.labChrome, modeGlobal]
|
|
2524
3858
|
);
|
|
2525
3859
|
const [menus, setMenus] = useState();
|
|
2526
3860
|
const first = index[0];
|
|
2527
3861
|
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(
|
|
3862
|
+
return /* @__PURE__ */ jsx(LibrariesContext.Provider, { value: libraries, children: /* @__PURE__ */ jsx(StoryGlobalsContext.Provider, { value: labValues, children: /* @__PURE__ */ jsx(DependenciesContext.Provider, { value: dependencies ?? null, children: /* @__PURE__ */ jsx(FramePoolContext.Provider, { value: pool, children: /* @__PURE__ */ jsx(TrialFramesContext.Provider, { value: frames, children: /* @__PURE__ */ jsx(TrialClocksContext.Provider, { value: clocks, children: /* @__PURE__ */ jsx(LoupeProvider, { captureBase: props.captureBase, revision: props.revision, children: /* @__PURE__ */ jsxs(
|
|
2529
3863
|
Lab,
|
|
2530
3864
|
{
|
|
2531
3865
|
title,
|
|
2532
|
-
density:
|
|
3866
|
+
density: LAB_DENSITY,
|
|
2533
3867
|
...labTheme ? { theme: labTheme } : {},
|
|
2534
3868
|
instruments,
|
|
2535
3869
|
defaultInstrument: readRoute() ?? first.id,
|
|
@@ -2556,17 +3890,19 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
|
|
|
2556
3890
|
),
|
|
2557
3891
|
/* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues }),
|
|
2558
3892
|
/* @__PURE__ */ jsx(GlobalsMenus, { declarations, onMenus: setMenus }),
|
|
2559
|
-
showsCssVars ? /* @__PURE__ */ jsx(CssVarsChrome, { onChrome: setCssVarsChrome }) : null
|
|
3893
|
+
showsCssVars ? /* @__PURE__ */ jsx(CssVarsChrome, { onChrome: setCssVarsChrome }) : null,
|
|
3894
|
+
/* @__PURE__ */ jsx(KeysChrome, { onChrome: setKeysChrome })
|
|
2560
3895
|
]
|
|
2561
3896
|
}
|
|
2562
|
-
) }) }) }) }) }) });
|
|
3897
|
+
) }) }) }) }) }) }) });
|
|
2563
3898
|
}
|
|
2564
3899
|
function mountWorkshop({ container, ...props }) {
|
|
2565
3900
|
const target = container ?? document.getElementById("root");
|
|
2566
3901
|
if (!target) throw new Error("mountWorkshop: no container given and no #root element");
|
|
2567
3902
|
const root = createKitRoot(target, props.setup?.icons);
|
|
2568
3903
|
const changes = createStoryChanges();
|
|
2569
|
-
|
|
3904
|
+
const revision = createRevision();
|
|
3905
|
+
let current = { ...props, changes, revision };
|
|
2570
3906
|
root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
|
|
2571
3907
|
return {
|
|
2572
3908
|
setIndex(index) {
|
|
@@ -2582,10 +3918,11 @@ function mountWorkshop({ container, ...props }) {
|
|
|
2582
3918
|
current = { ...current, dependencies };
|
|
2583
3919
|
root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
|
|
2584
3920
|
},
|
|
3921
|
+
revise: () => revision.bump(),
|
|
2585
3922
|
unmount: () => root.unmount()
|
|
2586
3923
|
};
|
|
2587
3924
|
}
|
|
2588
3925
|
|
|
2589
|
-
export { A11Y_SECTION, A11yPanel, Workshop, mountWorkshop, useStoryRegistry };
|
|
3926
|
+
export { A11Y_SECTION, A11yPanel, Workshop, followEdits, mountWorkshop, useStoryRegistry };
|
|
2590
3927
|
//# sourceMappingURL=index.js.map
|
|
2591
3928
|
//# sourceMappingURL=index.js.map
|