@weasel-js/forge 1.9.3 → 1.10.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 +34 -4
- package/dist/{FrameController-CpcY0DNB.d.ts → FrameController-UeTY7ubF.d.ts} +26 -36
- package/dist/{chunk-Q2EZXJFE.js → chunk-4NM3RTYK.js} +70 -56
- package/dist/chunk-4NM3RTYK.js.map +1 -0
- package/dist/chunk-5DXUNLV2.js +6 -0
- package/dist/chunk-5DXUNLV2.js.map +1 -0
- package/dist/{chunk-GZPQMGXJ.js → chunk-DFKCLZKS.js} +5 -4
- package/dist/chunk-DFKCLZKS.js.map +1 -0
- package/dist/{chunk-CJX3W5HY.js → chunk-DKWHGSBZ.js} +4 -3
- package/dist/chunk-DKWHGSBZ.js.map +1 -0
- package/dist/{chunk-BGOHALHO.js → chunk-ENXU34PI.js} +23 -111
- package/dist/chunk-ENXU34PI.js.map +1 -0
- package/dist/chunk-FL2MUPUL.js +10 -0
- package/dist/chunk-FL2MUPUL.js.map +1 -0
- package/dist/{chunk-MGQWCPYE.js → chunk-GB6PRJET.js} +4 -5
- package/dist/chunk-GB6PRJET.js.map +1 -0
- package/dist/{chunk-JVRHBI3V.js → chunk-V4RI7GWD.js} +53 -59
- package/dist/chunk-V4RI7GWD.js.map +1 -0
- package/dist/{chunk-HRGPFQBW.js → chunk-ZAWXMUGZ.js} +6 -3
- package/dist/chunk-ZAWXMUGZ.js.map +1 -0
- package/dist/{config-BfsYw5lg.d.ts → config-Cz7ZyGF-.d.ts} +28 -5
- package/dist/csf/preview-api.d.ts +5 -2
- package/dist/csf/preview-api.js +2 -2
- package/dist/csf/preview-api.js.map +1 -1
- package/dist/frame/index.css +19 -2
- package/dist/frame/index.css.map +1 -1
- package/dist/frame/index.d.ts +2 -2
- package/dist/frame/index.js +7 -41
- package/dist/frame/index.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/shell/index.css +93 -62
- package/dist/shell/index.css.map +1 -1
- package/dist/shell/index.d.ts +10 -17
- package/dist/shell/index.js +724 -587
- package/dist/shell/index.js.map +1 -1
- package/dist/test/index.d.ts +4 -2
- package/dist/test/index.js +8 -8
- package/dist/test/index.js.map +1 -1
- package/dist/test/play.d.ts +2 -4
- package/dist/test/play.js.map +1 -1
- package/dist/vite/index.d.ts +19 -2
- package/dist/vite/index.js +751 -162
- package/dist/vite/index.js.map +1 -1
- package/package.json +10 -5
- package/dist/chunk-BGOHALHO.js.map +0 -1
- package/dist/chunk-CJX3W5HY.js.map +0 -1
- package/dist/chunk-GZPQMGXJ.js.map +0 -1
- package/dist/chunk-HRGPFQBW.js.map +0 -1
- package/dist/chunk-JVRHBI3V.js.map +0 -1
- package/dist/chunk-KJGXJD3U.js +0 -15
- package/dist/chunk-KJGXJD3U.js.map +0 -1
- package/dist/chunk-MGQWCPYE.js.map +0 -1
- package/dist/chunk-Q2EZXJFE.js.map +0 -1
package/dist/shell/index.js
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
import { parseRoute, paramsToKnobs, knobPaths, sameKnob, knobsToParams,
|
|
2
|
-
import {
|
|
1
|
+
import { parseRoute, formatRoute, paramsToKnobs, knobPaths, sameKnob, knobsToParams, PLAYHEAD_PARAM, reservedParams } from '../chunk-DKWHGSBZ.js';
|
|
2
|
+
import { DEFAULT_TITLE } from '../chunk-5DXUNLV2.js';
|
|
3
|
+
import { TITLE_LIBRARIES, isIndexId, indexId, indexEntries, librariesFrom, LibrariesContext, schemaFromDescription, LibraryBadge, openChannel, IndexPage } from '../chunk-V4RI7GWD.js';
|
|
3
4
|
import { isGallery } from '../chunk-WEQSLPKT.js';
|
|
4
|
-
import { useTrialFrame, createTrialFrames, createTrialClocks, StoryGlobalsContext, TrialFramesContext, TrialClocksContext, StoryTrial, useCssOverrides,
|
|
5
|
-
export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-
|
|
6
|
-
import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, PLAYHEAD_KEY, labOptions, isGlobalsPath } from '../chunk-
|
|
7
|
-
import { loadStories, indexRenderOf } from '../chunk-
|
|
5
|
+
import { useTrialFrame, createTrialFrames, createTrialClocks, StoryGlobalsContext, TrialFramesContext, TrialClocksContext, StoryTrial, useCssOverrides, useTrialClock, TrialHost } from '../chunk-ENXU34PI.js';
|
|
6
|
+
export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-ENXU34PI.js';
|
|
7
|
+
import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, PLAYHEAD_KEY, labOptions, isGlobalsPath } from '../chunk-DFKCLZKS.js';
|
|
8
|
+
import { loadStories, indexRenderOf, useNearViewport, IN_VIEW_MARGIN } from '../chunk-4NM3RTYK.js';
|
|
8
9
|
import '../chunk-L5XM5VKB.js';
|
|
9
|
-
import '../chunk-
|
|
10
|
-
import '../chunk-
|
|
11
|
-
import { stableStringify, PORT_HANDOFF, FRAME_HELLO, PROTOCOL_VERSION } from '../chunk-
|
|
10
|
+
import '../chunk-ZAWXMUGZ.js';
|
|
11
|
+
import '../chunk-GB6PRJET.js';
|
|
12
|
+
import { stableStringify, PORT_HANDOFF, FRAME_HELLO, PROTOCOL_VERSION } from '../chunk-FL2MUPUL.js';
|
|
12
13
|
import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, useTrialState, inferTokenType, ToggleBar as ToggleBar$1, Input, TokenPanel, refitScale } from '@weasel-js/labkit';
|
|
13
14
|
import num from '@weasel-js/theme/numeric.module.css';
|
|
14
|
-
import { createContext, useState, useRef,
|
|
15
|
+
import { createContext, useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect, useId, useContext, Fragment as Fragment$1, useSyncExternalStore } from 'react';
|
|
15
16
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
16
17
|
import { createRoot } from 'react-dom/client';
|
|
18
|
+
import { createEmitter } from '@weasel-js/registry';
|
|
19
|
+
import { f, valueAtPath, isConfigBranch } from '@weasel-js/labkit/config';
|
|
17
20
|
import { InfoIcon, ToggleBar, Checkbox, Dialog, TOKEN_CATEGORIES, Icon, DisclosureMark, Button as Button$1, Callout, Slider, Transport, SearchIcon } from '@weasel-js/ui';
|
|
18
21
|
import { TOKEN_MANIFEST, THEME_SOURCES, isByAxis } from '@weasel-js/theme';
|
|
19
22
|
import { scale, httpThemeApi } from '@weasel-js/theme/engine';
|
|
20
|
-
import {
|
|
21
|
-
import { useLatest } from '@weasel-js/core';
|
|
23
|
+
import { useLatest, historyKey, createHistory, swapOp, isPlainObject } from '@weasel-js/core';
|
|
22
24
|
|
|
23
25
|
function Finding({ finding }) {
|
|
24
26
|
return /* @__PURE__ */ jsxs("li", { className: "fg-a11y__finding", children: [
|
|
@@ -44,19 +46,21 @@ function Findings({ title, findings }) {
|
|
|
44
46
|
function A11yPanel() {
|
|
45
47
|
const trialId = useTrialId();
|
|
46
48
|
const frame = useTrialFrame(trialId);
|
|
47
|
-
const [
|
|
49
|
+
const [inFlight, setInFlight] = useState(0);
|
|
50
|
+
const running = inFlight > 0;
|
|
48
51
|
const audit = frame.audit;
|
|
49
52
|
const ran = useRef(null);
|
|
53
|
+
const start = useCallback((run) => {
|
|
54
|
+
setInFlight((n) => n + 1);
|
|
55
|
+
void run().finally(() => setInFlight((n) => n - 1));
|
|
56
|
+
}, []);
|
|
50
57
|
useEffect(() => {
|
|
51
58
|
if (!audit || ran.current === audit) return;
|
|
52
59
|
ran.current = audit;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}, [audit]);
|
|
60
|
+
start(audit);
|
|
61
|
+
}, [audit, start]);
|
|
56
62
|
const rerun = () => {
|
|
57
|
-
if (
|
|
58
|
-
setRunning(true);
|
|
59
|
-
void audit().finally(() => setRunning(false));
|
|
63
|
+
if (audit) start(audit);
|
|
60
64
|
};
|
|
61
65
|
const outcome = frame.a11y;
|
|
62
66
|
return /* @__PURE__ */ jsxs("div", { className: "fg-a11y", children: [
|
|
@@ -97,21 +101,310 @@ var A11Y_SECTION = {
|
|
|
97
101
|
region: "aside",
|
|
98
102
|
item: { title: "Accessibility", body: /* @__PURE__ */ jsx(FocusedA11y, {}) }
|
|
99
103
|
};
|
|
100
|
-
|
|
101
|
-
// src/shell/storyChanges.ts
|
|
102
104
|
function createStoryChanges() {
|
|
103
|
-
const
|
|
105
|
+
const changes = createEmitter();
|
|
106
|
+
return { subscribe: changes.subscribe, emit: changes.emit };
|
|
107
|
+
}
|
|
108
|
+
var DependenciesContext = createContext(null);
|
|
109
|
+
function readRoute() {
|
|
110
|
+
return parseRoute(location.hash).story;
|
|
111
|
+
}
|
|
112
|
+
function readRouteParams() {
|
|
113
|
+
return { ...parseRoute(location.hash).params };
|
|
114
|
+
}
|
|
115
|
+
function replaceRouteParams(update) {
|
|
116
|
+
const { story, params } = parseRoute(location.hash);
|
|
117
|
+
if (story === null) return;
|
|
118
|
+
const next = formatRoute({ story, params: update({ ...params }) });
|
|
119
|
+
if (next !== location.hash) history.replaceState(history.state, "", next);
|
|
120
|
+
}
|
|
121
|
+
function readRouteEntry() {
|
|
122
|
+
const state = history.state;
|
|
123
|
+
const entry = state?.forgeRoute;
|
|
124
|
+
return typeof entry?.step === "number" ? entry : null;
|
|
125
|
+
}
|
|
126
|
+
function crossesInPlace(from, to) {
|
|
127
|
+
if (!from || !to || from.step === to.step) return false;
|
|
128
|
+
return to.step < from.step ? from.inPlace : to.inPlace;
|
|
129
|
+
}
|
|
130
|
+
function stamp(entry, hash) {
|
|
131
|
+
history.replaceState({ ...history.state ?? {}, forgeRoute: entry }, "", hash);
|
|
132
|
+
}
|
|
133
|
+
function setRoute(id, options = {}) {
|
|
134
|
+
if (readRoute() === id) {
|
|
135
|
+
history.replaceState(history.state, "", formatRoute({ story: id, params: readRouteParams() }));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const hash = formatRoute({ story: id, params: {} });
|
|
139
|
+
const from = readRouteEntry() ?? { step: 0, inPlace: false };
|
|
140
|
+
stamp(from, location.hash);
|
|
141
|
+
location.hash = hash;
|
|
142
|
+
stamp({ step: from.step + 1, inPlace: options.inPlace ?? false, ...options.fromTrial ? { fromTrial: true } : {} }, hash);
|
|
143
|
+
}
|
|
144
|
+
function takeRouteFromTrial() {
|
|
145
|
+
const entry = readRouteEntry();
|
|
146
|
+
if (!entry?.fromTrial) return false;
|
|
147
|
+
stamp({ step: entry.step, inPlace: entry.inPlace }, location.hash);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
function useRoute() {
|
|
151
|
+
const [route, setCurrent] = useState(readRoute);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
const sync = () => setCurrent(readRoute());
|
|
154
|
+
window.addEventListener("hashchange", sync);
|
|
155
|
+
sync();
|
|
156
|
+
return () => window.removeEventListener("hashchange", sync);
|
|
157
|
+
}, []);
|
|
158
|
+
return [route, setRoute];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/timeline/playheadUrl.ts
|
|
162
|
+
function formatSeconds(ms) {
|
|
163
|
+
const rounded = Math.round(ms) / 1e3;
|
|
164
|
+
return String(rounded === 0 ? 0 : rounded);
|
|
165
|
+
}
|
|
166
|
+
function parseSeconds(text) {
|
|
167
|
+
if (text == null || !/^\s*-?(\d+\.?\d*|\.\d+)\s*$/.test(text)) return null;
|
|
168
|
+
return Math.round(Number(text) * 1e3);
|
|
169
|
+
}
|
|
170
|
+
function readPlayheadParam() {
|
|
171
|
+
return parseSeconds(readRouteParams()[PLAYHEAD_PARAM]);
|
|
172
|
+
}
|
|
173
|
+
function writePlayheadParam(ms) {
|
|
174
|
+
replaceRouteParams(
|
|
175
|
+
({ [PLAYHEAD_PARAM]: _held, ...rest }) => ms === null ? rest : { ...rest, [PLAYHEAD_PARAM]: formatSeconds(ms) }
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
function TransportBar({ clock }) {
|
|
179
|
+
const { time, playing, loop, rate, span } = useSyncExternalStore(clock.subscribe, clock.get);
|
|
180
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-transport", children: [
|
|
181
|
+
/* @__PURE__ */ jsx(
|
|
182
|
+
Slider,
|
|
183
|
+
{
|
|
184
|
+
className: "fg-transport__scrub",
|
|
185
|
+
ariaLabel: "Scrub",
|
|
186
|
+
density: "slim",
|
|
187
|
+
readoutPlacement: "none",
|
|
188
|
+
min: span.start,
|
|
189
|
+
max: span.end > span.start ? span.end : span.start + 1,
|
|
190
|
+
thumbs: [{ value: time, valueText: `${formatSeconds(time)} seconds` }],
|
|
191
|
+
onInput: ([thumb]) => {
|
|
192
|
+
if (thumb) clock.seek(thumb.value);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
),
|
|
196
|
+
/* @__PURE__ */ jsx(
|
|
197
|
+
Transport,
|
|
198
|
+
{
|
|
199
|
+
paused: !playing,
|
|
200
|
+
loop,
|
|
201
|
+
rate,
|
|
202
|
+
playhead: time,
|
|
203
|
+
duration: span.end,
|
|
204
|
+
onPlay: clock.play,
|
|
205
|
+
onPause: clock.pause,
|
|
206
|
+
onLoopChange: (next) => clock.setLoop(next !== false && next !== 0),
|
|
207
|
+
onRateChange: clock.setRate
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
] });
|
|
211
|
+
}
|
|
212
|
+
function TrialTransport({ trialId }) {
|
|
213
|
+
const clock = useTrialClock(trialId);
|
|
214
|
+
return clock ? /* @__PURE__ */ jsx(TransportBar, { clock }) : null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/shell/breadcrumb.ts
|
|
218
|
+
function crumbs(title, name) {
|
|
219
|
+
const segments = title.split("/");
|
|
220
|
+
const steps = segments.map((label, i) => ({ label, path: segments.slice(0, i + 1).join("/") }));
|
|
221
|
+
steps.push({ label: name, path: null });
|
|
222
|
+
return steps.filter((step, i) => i === 0 || step.label.toLowerCase() !== "index");
|
|
223
|
+
}
|
|
224
|
+
function breadcrumb(title, name) {
|
|
225
|
+
return crumbs(title, name).map((step) => step.label).join(" > ");
|
|
226
|
+
}
|
|
227
|
+
var reveals = createEmitter();
|
|
228
|
+
function revealInTree(path) {
|
|
229
|
+
reveals.emit(path);
|
|
230
|
+
}
|
|
231
|
+
function onRevealInTree(listener) {
|
|
232
|
+
return reveals.subscribe(listener);
|
|
233
|
+
}
|
|
234
|
+
function Crumbs({ entry }) {
|
|
235
|
+
const steps = crumbs(entry.title, entry.name);
|
|
236
|
+
return /* @__PURE__ */ jsx("span", { className: "fg-crumbs", children: steps.map((step, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
237
|
+
i > 0 ? /* @__PURE__ */ jsx("span", { className: "fg-crumbs__sep", "aria-hidden": "true", children: "\u203A" }) : null,
|
|
238
|
+
step.path === null ? /* @__PURE__ */ jsx("span", { className: "fg-crumbs__here", children: step.label }) : /* @__PURE__ */ jsx(
|
|
239
|
+
"button",
|
|
240
|
+
{
|
|
241
|
+
type: "button",
|
|
242
|
+
className: "fg-crumbs__step",
|
|
243
|
+
title: `Show ${step.label} in the story tree`,
|
|
244
|
+
onPointerDown: (event) => event.stopPropagation(),
|
|
245
|
+
onClick: () => revealInTree(step.path),
|
|
246
|
+
children: step.label
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
] }, step.path ?? `${step.label}:${i}`)) });
|
|
250
|
+
}
|
|
251
|
+
function crumbTitle(entry) {
|
|
252
|
+
const title = breadcrumb(entry.title, entry.name);
|
|
253
|
+
return { title, renderTitle: (shown) => shown === title ? /* @__PURE__ */ jsx(Crumbs, { entry }) : shown };
|
|
254
|
+
}
|
|
255
|
+
function IndexTrial({ bundle, setup, config }) {
|
|
256
|
+
const lab = useLabContext();
|
|
257
|
+
const trialId = useContext(TrialIdContext);
|
|
258
|
+
const graph = useContext(DependenciesContext);
|
|
259
|
+
const gallery = isGallery(bundle);
|
|
260
|
+
const dependencies = gallery ? void 0 : graph?.[bundle.title];
|
|
261
|
+
const open = (id) => {
|
|
262
|
+
if (!trialId || !lab.instruments.some((instrument) => instrument.name === id)) return;
|
|
263
|
+
lab.swapTrial(trialId, id);
|
|
264
|
+
setRoute(id, { inPlace: true });
|
|
265
|
+
};
|
|
266
|
+
const render = (globals) => {
|
|
267
|
+
const env = {
|
|
268
|
+
title: bundle.title,
|
|
269
|
+
library: bundle.library,
|
|
270
|
+
...bundle.description === void 0 ? {} : { description: bundle.description },
|
|
271
|
+
stories: bundle.stories,
|
|
272
|
+
descriptions: bundle.descriptions,
|
|
273
|
+
gallery,
|
|
274
|
+
galleries: bundle.galleries,
|
|
275
|
+
...dependencies ? { dependencies } : {},
|
|
276
|
+
globals,
|
|
277
|
+
decorators: setup.decorators ?? [],
|
|
278
|
+
open,
|
|
279
|
+
// A story that throws shows its error in its own cell; the rest of the page is still worth reading.
|
|
280
|
+
onError: (error) => console.error(error)
|
|
281
|
+
};
|
|
282
|
+
return /* @__PURE__ */ jsx(IndexPage, { env, render: bundle.render });
|
|
283
|
+
};
|
|
284
|
+
return /* @__PURE__ */ jsx(TrialHost, { layout: "fullscreen", setup, config, children: render });
|
|
285
|
+
}
|
|
286
|
+
function withPlayhead(schema) {
|
|
287
|
+
return f.schema({ ...schema.nodes, [PLAYHEAD_KEY]: f.custom("forge-playhead", null).hidden() });
|
|
288
|
+
}
|
|
289
|
+
function documentInstrument(options) {
|
|
290
|
+
const { entry, loaded, setup, globals = {} } = options;
|
|
291
|
+
const schema = loaded.kind === "story" ? loaded.story.config : f.schema({});
|
|
292
|
+
const timed = loaded.kind === "story" && loaded.story.timeline !== null;
|
|
293
|
+
const config = withGlobals(timed ? withPlayhead(schema) : schema, globals);
|
|
294
|
+
const initialState = loaded.kind === "story" ? loaded.story.initialState : null;
|
|
104
295
|
return {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
296
|
+
name: entry.id,
|
|
297
|
+
...crumbTitle(entry),
|
|
298
|
+
config,
|
|
299
|
+
defaultConfig: () => config.defaults(),
|
|
300
|
+
initialState: (c) => initialState?.(c) ?? null,
|
|
301
|
+
render: (ctx) => loaded.kind === "story" ? /* @__PURE__ */ jsx(StoryTrial, { story: loaded.story, setup, ctx }, loaded.story.id) : /* @__PURE__ */ jsx(IndexTrial, { bundle: loaded.bundle, setup, config: ctx.config }),
|
|
302
|
+
...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {},
|
|
303
|
+
...timed ? { chrome: [{ id: "fg-transport", region: "status", render: (c) => /* @__PURE__ */ jsx(TrialTransport, { trialId: c.trialId }) }] } : {}
|
|
112
304
|
};
|
|
113
305
|
}
|
|
114
|
-
|
|
306
|
+
function Fault({ phase, message }) {
|
|
307
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
|
|
308
|
+
/* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: phase }),
|
|
309
|
+
/* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: message })
|
|
310
|
+
] });
|
|
311
|
+
}
|
|
312
|
+
function StoryLoad({ entry, load, fault }) {
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
if (!fault) load(entry);
|
|
315
|
+
}, [entry, load, fault]);
|
|
316
|
+
return /* @__PURE__ */ jsx("div", { className: "fg-story fg-story--loading", "data-fg-layout": "fullscreen", children: fault ? /* @__PURE__ */ jsx(Fault, { phase: "import", message: fault.message }) : null });
|
|
317
|
+
}
|
|
318
|
+
function pendingInstrument(options) {
|
|
319
|
+
const { entry, globals = {} } = options;
|
|
320
|
+
const config = withGlobals(f.schema({}), globals);
|
|
321
|
+
return {
|
|
322
|
+
name: entry.id,
|
|
323
|
+
...crumbTitle(entry),
|
|
324
|
+
config,
|
|
325
|
+
defaultConfig: () => config.defaults(),
|
|
326
|
+
initialState: () => null,
|
|
327
|
+
render: () => /* @__PURE__ */ jsx(StoryLoad, { ...options }, options.generation ?? 0)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function missingInstrument(id, globals = {}) {
|
|
331
|
+
const config = withGlobals(f.schema({}), globals);
|
|
332
|
+
return {
|
|
333
|
+
name: id,
|
|
334
|
+
title: `No story ${id}`,
|
|
335
|
+
config,
|
|
336
|
+
defaultConfig: () => config.defaults(),
|
|
337
|
+
initialState: () => null,
|
|
338
|
+
render: () => /* @__PURE__ */ jsx("div", { className: "fg-story fg-story--loading", "data-fg-layout": "fullscreen", children: /* @__PURE__ */ jsx(Fault, { phase: "route", message: `No story ${id}` }) })
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
var ICONS = {
|
|
342
|
+
color: "drop",
|
|
343
|
+
type: "type",
|
|
344
|
+
size: "measure",
|
|
345
|
+
motion: "curveEaseInOut",
|
|
346
|
+
depth: "layers",
|
|
347
|
+
other: "more"
|
|
348
|
+
};
|
|
349
|
+
var PANEL = ".fg-css-vars";
|
|
350
|
+
var HOST = ".lk-sidebar-section, .lk-panel-tile";
|
|
351
|
+
var MANUAL = ["wheel", "touchmove", "keydown"];
|
|
352
|
+
var LIST = ".fg-css-vars__list";
|
|
353
|
+
function readLine(panel) {
|
|
354
|
+
return (panel.querySelector(LIST) ?? panel).getBoundingClientRect().top;
|
|
355
|
+
}
|
|
356
|
+
function sectionsOf(panel) {
|
|
357
|
+
return [...panel.querySelectorAll("[data-token-category]")];
|
|
358
|
+
}
|
|
359
|
+
function SectionJump() {
|
|
360
|
+
const ref = useRef(null);
|
|
361
|
+
const [present, setPresent] = useState([]);
|
|
362
|
+
const [current, setCurrent] = useState(null);
|
|
363
|
+
const held = useRef(false);
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
const root = ref.current?.closest(HOST) ?? document.body;
|
|
366
|
+
const read = () => {
|
|
367
|
+
const panel = root.querySelector(PANEL);
|
|
368
|
+
const sections = panel ? sectionsOf(panel) : [];
|
|
369
|
+
setPresent(sections.map((s) => s.dataset.tokenCategory));
|
|
370
|
+
if (!panel || held.current) return;
|
|
371
|
+
const line = readLine(panel) + 1;
|
|
372
|
+
let top = sections[0]?.dataset.tokenCategory;
|
|
373
|
+
for (const s of sections) if (s.getBoundingClientRect().top <= line) top = s.dataset.tokenCategory;
|
|
374
|
+
setCurrent(top ?? null);
|
|
375
|
+
};
|
|
376
|
+
const release = () => {
|
|
377
|
+
held.current = false;
|
|
378
|
+
};
|
|
379
|
+
read();
|
|
380
|
+
const observer = new MutationObserver(read);
|
|
381
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
382
|
+
document.addEventListener("scroll", read, { capture: true, passive: true });
|
|
383
|
+
for (const type of MANUAL) document.addEventListener(type, release, { capture: true, passive: true });
|
|
384
|
+
return () => {
|
|
385
|
+
observer.disconnect();
|
|
386
|
+
document.removeEventListener("scroll", read, { capture: true });
|
|
387
|
+
for (const type of MANUAL) document.removeEventListener(type, release, { capture: true });
|
|
388
|
+
};
|
|
389
|
+
}, []);
|
|
390
|
+
const items = TOKEN_CATEGORIES.map(({ id, title }) => ({
|
|
391
|
+
value: id,
|
|
392
|
+
label: /* @__PURE__ */ jsx(Icon, { name: ICONS[id], size: 14 }),
|
|
393
|
+
ariaLabel: title,
|
|
394
|
+
tooltip: title,
|
|
395
|
+
disabled: !present.includes(id)
|
|
396
|
+
}));
|
|
397
|
+
const jump = (next) => {
|
|
398
|
+
const panel = ref.current?.closest(HOST)?.querySelector(PANEL);
|
|
399
|
+
const target = next && panel?.querySelector(`[data-token-category="${next}"]`);
|
|
400
|
+
const scroller = panel?.querySelector(LIST);
|
|
401
|
+
if (!panel || !target || !scroller) return;
|
|
402
|
+
held.current = true;
|
|
403
|
+
setCurrent(next);
|
|
404
|
+
scroller.scrollTop += target.getBoundingClientRect().top - readLine(panel);
|
|
405
|
+
};
|
|
406
|
+
return /* @__PURE__ */ jsx("div", { ref, className: "fg-css-vars__jump", children: /* @__PURE__ */ jsx(ToggleBar, { ariaLabel: "Jump to section", variant: "flat", size: "sm", items, value: current, onChange: jump }) });
|
|
407
|
+
}
|
|
115
408
|
var SEED_REF = /^\{seeds\.([^}]+)\}$/;
|
|
116
409
|
var RULE_KEYS = { factors: "factors", ratio: "ratio", step: "step" };
|
|
117
410
|
function selectionFor(axes, globals) {
|
|
@@ -125,24 +418,24 @@ function selectionFor(axes, globals) {
|
|
|
125
418
|
}
|
|
126
419
|
var EditError = class extends Error {
|
|
127
420
|
};
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
let
|
|
131
|
-
let k = key;
|
|
132
|
-
let w = where;
|
|
133
|
-
let value = h[k];
|
|
134
|
-
while (isByAxis(value)) {
|
|
421
|
+
function branchOf(holder, key, where, selection) {
|
|
422
|
+
let slot = { holder, key, where };
|
|
423
|
+
for (let value = holder[key]; isByAxis(value); value = slot.holder[slot.key]) {
|
|
135
424
|
const axis = value.by;
|
|
136
425
|
const picked = selection[axis];
|
|
137
426
|
if (picked === void 0 || picked === "by" || !(picked in value)) {
|
|
138
|
-
throw new EditError(`${
|
|
427
|
+
throw new EditError(`${slot.where} differs by ${axis}, and which ${axis} this trial shows is not known`);
|
|
139
428
|
}
|
|
140
|
-
|
|
141
|
-
k = picked;
|
|
142
|
-
w = `${w}.${picked}`;
|
|
143
|
-
value = h[k];
|
|
429
|
+
slot = { holder: value, key: picked, where: `${slot.where}.${picked}` };
|
|
144
430
|
}
|
|
145
|
-
|
|
431
|
+
return slot;
|
|
432
|
+
}
|
|
433
|
+
function slotOf(root, holder, key, where, selection, depth = 0) {
|
|
434
|
+
if (depth > 16) throw new EditError(`${where} references itself`);
|
|
435
|
+
const slot = branchOf(holder, key, where, selection);
|
|
436
|
+
const { holder: h, key: k, where: w } = slot;
|
|
437
|
+
const value = h[k];
|
|
438
|
+
if (typeof value === "number") return slot;
|
|
146
439
|
if (typeof value === "string") {
|
|
147
440
|
const ref = SEED_REF.exec(value);
|
|
148
441
|
if (!ref) throw new EditError(`${w} is "${value}", which is not a number or a seed`);
|
|
@@ -154,18 +447,10 @@ function slotOf(root, holder, key, where, selection, depth = 0) {
|
|
|
154
447
|
throw new EditError(`${w} is not a number`);
|
|
155
448
|
}
|
|
156
449
|
function factorsOf(holder, where, selection) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (picked === void 0 || !(picked in value)) {
|
|
162
|
-
throw new EditError(`${w} differs by ${value.by}, and which ${value.by} this trial shows is not known`);
|
|
163
|
-
}
|
|
164
|
-
w = `${w}.${picked}`;
|
|
165
|
-
value = value[picked];
|
|
166
|
-
}
|
|
167
|
-
if (!Array.isArray(value)) throw new EditError(`${w} is not a list`);
|
|
168
|
-
return { list: value, where: w };
|
|
450
|
+
const slot = branchOf(holder, "factors", `${where}.factors`, selection);
|
|
451
|
+
const value = slot.holder[slot.key];
|
|
452
|
+
if (!Array.isArray(value)) throw new EditError(`${slot.where} is not a list`);
|
|
453
|
+
return { list: value, where: slot.where };
|
|
169
454
|
}
|
|
170
455
|
var kindOf = (scale) => scale.factors !== void 0 ? "factors" : scale.ratio !== void 0 ? "ratio" : scale.step !== void 0 ? "step" : null;
|
|
171
456
|
var plainLiteral = (value) => typeof value === "number" || Array.isArray(value) && value.every((v) => typeof v === "number");
|
|
@@ -221,90 +506,196 @@ function applyScaleEdits(definition, edits, selection) {
|
|
|
221
506
|
for (const [holder, byKey] of writes) for (const [key, value] of byKey) holder[key] = value;
|
|
222
507
|
return { ok: true, definition: root };
|
|
223
508
|
}
|
|
224
|
-
var
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
509
|
+
var REFERENCE = /\{[^}]+\}/;
|
|
510
|
+
var LAYERS = ["pins", "semantics", "components"];
|
|
511
|
+
function derivation(entry) {
|
|
512
|
+
const value = isPlainObject(entry) ? entry.value : entry;
|
|
513
|
+
if (isPlainObject(entry) && value === void 0) return "follows a rule";
|
|
514
|
+
if (isPlainObject(entry) && entry.alpha !== void 0) return "is drawn at an alpha";
|
|
515
|
+
if (typeof value === "string" && REFERENCE.test(value)) return `follows ${REFERENCE.exec(value)?.[0]}`;
|
|
516
|
+
return typeof value === "string" || typeof value === "number" ? null : "is a list";
|
|
517
|
+
}
|
|
518
|
+
function literalSlot(definition, token, selection) {
|
|
519
|
+
for (const layer of LAYERS) {
|
|
520
|
+
const entries = definition[layer];
|
|
521
|
+
if (!entries || !(token in entries)) continue;
|
|
522
|
+
const slot = branchOf(entries, token, `${layer}.${token}`, selection);
|
|
523
|
+
const why = derivation(slot.holder[slot.key]);
|
|
524
|
+
if (why) throw new EditError(`${slot.where} ${why}, and only a value written out in the theme can be saved`);
|
|
525
|
+
return slot;
|
|
239
526
|
}
|
|
240
|
-
|
|
527
|
+
for (const layer of ["ramps", "scales"]) {
|
|
528
|
+
const from = Object.keys(definition[layer] ?? {}).find((name) => token.startsWith(`${name}-`));
|
|
529
|
+
if (from) throw new EditError(`${token} is a step ${layer}.${from} generates, and the theme pins no value over it`);
|
|
530
|
+
}
|
|
531
|
+
throw new EditError(`the theme defines no token "${token}"`);
|
|
241
532
|
}
|
|
242
|
-
function
|
|
243
|
-
|
|
533
|
+
function writtenOut(definition, token, selection) {
|
|
534
|
+
try {
|
|
535
|
+
literalSlot(definition, token, selection);
|
|
536
|
+
return true;
|
|
537
|
+
} catch (e) {
|
|
538
|
+
if (e instanceof EditError) return false;
|
|
539
|
+
throw e;
|
|
540
|
+
}
|
|
244
541
|
}
|
|
245
|
-
function
|
|
246
|
-
|
|
542
|
+
function applyTokenEdits(definition, edits, selection) {
|
|
543
|
+
const root = structuredClone(definition);
|
|
544
|
+
try {
|
|
545
|
+
for (const [token, value] of Object.entries(edits)) {
|
|
546
|
+
const { holder, key } = literalSlot(root, token, selection);
|
|
547
|
+
const entry = holder[key];
|
|
548
|
+
holder[key] = isPlainObject(entry) ? { ...entry, value } : value;
|
|
549
|
+
}
|
|
550
|
+
} catch (e) {
|
|
551
|
+
if (e instanceof EditError) return { ok: false, message: `Cannot save: ${e.message}.` };
|
|
552
|
+
throw e;
|
|
553
|
+
}
|
|
554
|
+
return { ok: true, definition: root };
|
|
247
555
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
556
|
+
|
|
557
|
+
// src/shell/cssVars/useThemeSave.ts
|
|
558
|
+
var THEME_NAME = "weasel";
|
|
559
|
+
var TOKEN_PREFIX = "--wzl-";
|
|
560
|
+
function useThemeSave({ trialId, globals, themeApi, scales, overrides, history: history2 }) {
|
|
561
|
+
const tokens = useMemo(() => {
|
|
562
|
+
const definition = THEME_SOURCES[THEME_NAME];
|
|
563
|
+
if (!definition) return {};
|
|
564
|
+
const selection = selectionFor(definition.axes ?? {}, globals);
|
|
565
|
+
const generated = new Set(scales.flatMap((scale) => scale.tokens));
|
|
566
|
+
return Object.fromEntries(
|
|
567
|
+
Object.entries(overrides).flatMap(([name, value]) => {
|
|
568
|
+
if (!name.startsWith(TOKEN_PREFIX) || generated.has(name)) return [];
|
|
569
|
+
const token = name.slice(TOKEN_PREFIX.length);
|
|
570
|
+
return writtenOut(definition, token, selection) ? [[token, value]] : [];
|
|
571
|
+
})
|
|
572
|
+
);
|
|
573
|
+
}, [globals, scales, overrides]);
|
|
574
|
+
const [saving, setSaving] = useState(false);
|
|
575
|
+
const [held, setHeld] = useState(null);
|
|
576
|
+
const setNotice = (next) => setHeld(next && { ...next, trialId });
|
|
577
|
+
const save = async () => {
|
|
578
|
+
setSaving(true);
|
|
579
|
+
setNotice(null);
|
|
580
|
+
try {
|
|
581
|
+
const api = themeApi ?? httpThemeApi();
|
|
582
|
+
const stored = await api.get(THEME_NAME);
|
|
583
|
+
const file = `themes/${THEME_NAME}.json`;
|
|
584
|
+
if (JSON.stringify(stored.definition) !== JSON.stringify(THEME_SOURCES[THEME_NAME])) {
|
|
585
|
+
setNotice({ tone: "error", text: `${file} has changed since this page loaded. Reload to edit against it.` });
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const selection = selectionFor(stored.definition.axes ?? {}, globals);
|
|
589
|
+
const scaled = applyScaleEdits(stored.definition, Object.fromEntries(scales.map((s) => [s.name, s.edit])), selection);
|
|
590
|
+
const next = scaled.ok ? applyTokenEdits(scaled.definition, tokens, selection) : scaled;
|
|
591
|
+
if (!next.ok) {
|
|
592
|
+
setNotice({ tone: "error", text: next.message });
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const result = await api.put(THEME_NAME, next.definition, stored.hash);
|
|
596
|
+
if (result.status === "conflict") {
|
|
597
|
+
setNotice({ tone: "error", text: `${file} changed on disk while saving. Nothing was written; reload to edit against it.` });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (result.status === "invalid") {
|
|
601
|
+
setNotice({ tone: "error", text: result.message });
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const saved = /* @__PURE__ */ new Set([
|
|
605
|
+
...scales.flatMap((scale) => scale.tokens),
|
|
606
|
+
...Object.keys(tokens).map((token) => `${TOKEN_PREFIX}${token}`)
|
|
607
|
+
]);
|
|
608
|
+
const groups = new Set(scales.map((scale) => scale.group));
|
|
609
|
+
const now = history2.current();
|
|
610
|
+
history2.reset({
|
|
611
|
+
overrides: Object.fromEntries(Object.entries(now.overrides).filter(([name]) => !saved.has(name))),
|
|
612
|
+
scales: Object.fromEntries(Object.entries(now.scales).filter(([group]) => !groups.has(group)))
|
|
613
|
+
});
|
|
614
|
+
setNotice(
|
|
615
|
+
result.regenerated || result.problems.length === 0 ? { tone: "saved", text: `Saved to ${file}.` } : { tone: "error", text: `Saved to ${file}, but the tokens were not regenerated: ${result.problems.join("; ")}` }
|
|
616
|
+
);
|
|
617
|
+
} catch (e) {
|
|
618
|
+
setNotice({ tone: "error", text: `Saving needs forge's dev server, which did not answer: ${e.message}` });
|
|
619
|
+
} finally {
|
|
620
|
+
setSaving(false);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
return {
|
|
624
|
+
pending: scales.length > 0 || Object.keys(tokens).length > 0,
|
|
625
|
+
saving,
|
|
626
|
+
notice: held?.trialId === trialId ? held : null,
|
|
627
|
+
save
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
var SWAP = "fg.vars.swap";
|
|
631
|
+
var COALESCE_MS = 800;
|
|
632
|
+
var LIMIT = 100;
|
|
633
|
+
function useVarsHistory(state, apply) {
|
|
634
|
+
const trialId = useTrialId();
|
|
635
|
+
const [stored, setStored] = usePersistedState("fg-css-vars-history", null, { scope: "trial" });
|
|
636
|
+
const rendered = useLatest(state);
|
|
637
|
+
const ahead = useRef(null);
|
|
253
638
|
useEffect(() => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
639
|
+
ahead.current = null;
|
|
640
|
+
}, [state]);
|
|
641
|
+
const current = () => ahead.current ?? rendered.current;
|
|
642
|
+
const latestApply = useLatest(apply);
|
|
643
|
+
const emit = (to) => {
|
|
644
|
+
const from = current();
|
|
645
|
+
ahead.current = to;
|
|
646
|
+
latestApply.current(from, to);
|
|
647
|
+
};
|
|
648
|
+
const swap = (before, after, coalesceKey) => swapOp({ name: SWAP, label: "edit vars", before, after, emit, ...coalesceKey !== void 0 ? { coalesceKey } : {} });
|
|
649
|
+
const history2 = useMemo(() => {
|
|
650
|
+
const opened = createHistory(null, {
|
|
651
|
+
coalesceWindowMs: COALESCE_MS,
|
|
652
|
+
historyLimit: LIMIT,
|
|
653
|
+
rebuildOp: (name, args) => {
|
|
654
|
+
const { before, after } = args;
|
|
655
|
+
return name === SWAP ? swap(before, after) : null;
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
if (stored) opened.restore(stored);
|
|
659
|
+
return opened;
|
|
660
|
+
}, [trialId]);
|
|
661
|
+
useSyncExternalStore(history2.subscribe, history2.getVersion);
|
|
662
|
+
const keep = () => setStored(history2.canUndo() || history2.canRedo() ? history2.serialize() : null);
|
|
663
|
+
const tick = useRef(null);
|
|
664
|
+
return {
|
|
665
|
+
current,
|
|
666
|
+
commit(next, key) {
|
|
667
|
+
if (tick.current === null) {
|
|
668
|
+
tick.current = key;
|
|
669
|
+
queueMicrotask(() => {
|
|
670
|
+
tick.current = null;
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
history2.applyOps([swap(current(), next, tick.current)], "edit vars");
|
|
674
|
+
keep();
|
|
675
|
+
},
|
|
676
|
+
step(to) {
|
|
677
|
+
history2[to]();
|
|
678
|
+
keep();
|
|
679
|
+
},
|
|
680
|
+
canUndo: history2.canUndo(),
|
|
681
|
+
canRedo: history2.canRedo(),
|
|
682
|
+
reset(next) {
|
|
683
|
+
emit(next);
|
|
684
|
+
history2.clear();
|
|
685
|
+
setStored(null);
|
|
686
|
+
}
|
|
294
687
|
};
|
|
295
|
-
return /* @__PURE__ */ jsx("div", { ref, className: "fg-css-vars__jump", children: /* @__PURE__ */ jsx(ToggleBar, { ariaLabel: "Jump to section", variant: "flat", size: "sm", items, value: current, onChange: jump }) });
|
|
296
688
|
}
|
|
297
689
|
var TABS = [
|
|
298
690
|
{ value: "theme", label: "Theme" },
|
|
299
691
|
{ value: "story", label: "Story" }
|
|
300
692
|
];
|
|
301
693
|
var MANIFEST = new Map(TOKEN_MANIFEST.map((token) => [token.name, token]));
|
|
302
|
-
var THEME_NAME = "weasel";
|
|
303
694
|
var literal = (v) => typeof v === "number";
|
|
304
695
|
var SCALE_SOURCES = Object.entries(THEME_SOURCES[THEME_NAME]?.scales ?? {}).flatMap(([name, def]) => {
|
|
305
696
|
if (!Array.isArray(def.steps)) return [];
|
|
306
697
|
const steps = def.steps;
|
|
307
|
-
const tokens = steps.map((step) =>
|
|
698
|
+
const tokens = steps.map((step) => `${TOKEN_PREFIX}${name}-${step}`);
|
|
308
699
|
const group = MANIFEST.get(tokens[0] ?? "")?.group;
|
|
309
700
|
if (!group) return [];
|
|
310
701
|
const factors = Array.isArray(def.factors) && def.factors.every(literal) ? def.factors : null;
|
|
@@ -349,7 +740,7 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
|
349
740
|
{},
|
|
350
741
|
{ scope: "lab" }
|
|
351
742
|
);
|
|
352
|
-
const [edited, setEdited] = usePersistedState("fg-css-vars-scales", {}, { scope: "
|
|
743
|
+
const [edited, setEdited] = usePersistedState("fg-css-vars-scales", {}, { scope: "trial" });
|
|
353
744
|
const [tab, setTab] = useState("theme");
|
|
354
745
|
const [filter, setFilter] = useState("");
|
|
355
746
|
const tokens = useMemo(() => {
|
|
@@ -386,135 +777,115 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
|
386
777
|
return out;
|
|
387
778
|
}, [tab, shown, overrides, edited]);
|
|
388
779
|
const send = frame.send;
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
780
|
+
const state = useMemo(() => ({ overrides, scales: edited }), [overrides, edited]);
|
|
781
|
+
const history2 = useVarsHistory(state, (from, to) => {
|
|
782
|
+
setOverrides(to.overrides);
|
|
783
|
+
setEdited(to.scales);
|
|
784
|
+
for (const name of /* @__PURE__ */ new Set([...Object.keys(from.overrides), ...Object.keys(to.overrides)])) {
|
|
785
|
+
if (from.overrides[name] !== to.overrides[name]) send?.({ type: "vars.set", name, value: to.overrides[name] ?? null });
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
const write = (name, value) => {
|
|
789
|
+
const now = history2.current();
|
|
790
|
+
const { [name]: _dropped, ...rest } = now.overrides;
|
|
791
|
+
history2.commit({ ...now, overrides: value === null ? rest : { ...rest, [name]: value } }, `token:${name}`);
|
|
792
|
+
};
|
|
793
|
+
const onScaleChange = (group, next) => {
|
|
794
|
+
const source = SCALE_SOURCES.find((sc) => sc.group === group);
|
|
795
|
+
if (!source) return;
|
|
796
|
+
const now = history2.current();
|
|
797
|
+
history2.commit(
|
|
798
|
+
{ overrides: { ...now.overrides, ...Object.fromEntries(scaleValues(source, next)) }, scales: { ...now.scales, [group]: next } },
|
|
799
|
+
`scale:${group}`
|
|
800
|
+
);
|
|
801
|
+
};
|
|
408
802
|
const unsaved = useMemo(
|
|
409
803
|
() => SCALE_SOURCES.flatMap((source) => {
|
|
410
804
|
const edit = edited[source.group];
|
|
411
|
-
if (!edit || !source.tokens.some((
|
|
412
|
-
const
|
|
413
|
-
|
|
805
|
+
if (!edit || !source.tokens.some((name2) => overrides[name2] !== void 0)) return [];
|
|
806
|
+
const { name, group, tokens: tokens2 } = source;
|
|
807
|
+
const clean = scaleValues(source, edit).every(([token, value]) => overrides[token] === value);
|
|
808
|
+
return clean ? [{ name, group, tokens: tokens2, edit: { base: edit.base, rule: edit.rule } }] : [];
|
|
414
809
|
}),
|
|
415
810
|
[edited, overrides]
|
|
416
811
|
);
|
|
417
|
-
const
|
|
418
|
-
const [held, setHeld] = useState(null);
|
|
419
|
-
const notice = held?.trialId === trialId ? held : null;
|
|
420
|
-
const setNotice = (next) => setHeld(next && { ...next, trialId });
|
|
421
|
-
const save = async () => {
|
|
422
|
-
setSaving(true);
|
|
423
|
-
setNotice(null);
|
|
424
|
-
try {
|
|
425
|
-
const api = themeApi ?? httpThemeApi();
|
|
426
|
-
const stored = await api.get(THEME_NAME);
|
|
427
|
-
const file = `themes/${THEME_NAME}.json`;
|
|
428
|
-
if (JSON.stringify(stored.definition) !== JSON.stringify(THEME_SOURCES[THEME_NAME])) {
|
|
429
|
-
setNotice({ tone: "error", text: `${file} has changed since this page loaded. Reload to edit against it.` });
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
const edits = Object.fromEntries(
|
|
433
|
-
unsaved.map(({ source, edit }) => [source.name, { base: edit.base, rule: edit.rule }])
|
|
434
|
-
);
|
|
435
|
-
const next = applyScaleEdits(stored.definition, edits, selectionFor(stored.definition.axes ?? {}, globals));
|
|
436
|
-
if (!next.ok) {
|
|
437
|
-
setNotice({ tone: "error", text: next.message });
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
const result = await api.put(THEME_NAME, next.definition, stored.hash);
|
|
441
|
-
if (result.status === "conflict") {
|
|
442
|
-
setNotice({ tone: "error", text: `${file} changed on disk while saving. Nothing was written; reload to edit against it.` });
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
if (result.status === "invalid") {
|
|
446
|
-
setNotice({ tone: "error", text: result.message });
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
for (const { source } of unsaved) for (const name of source.tokens) write(name, null);
|
|
450
|
-
const groups = new Set(unsaved.map(({ source }) => source.group));
|
|
451
|
-
setEdited((prev) => Object.fromEntries(Object.entries(prev).filter(([group]) => !groups.has(group))));
|
|
452
|
-
setNotice(
|
|
453
|
-
result.regenerated || result.problems.length === 0 ? { tone: "saved", text: `Saved to ${file}.` } : { tone: "error", text: `Saved to ${file}, but the tokens were not regenerated: ${result.problems.join("; ")}` }
|
|
454
|
-
);
|
|
455
|
-
} catch (e) {
|
|
456
|
-
setNotice({ tone: "error", text: `Saving needs forge's dev server, which did not answer: ${e.message}` });
|
|
457
|
-
} finally {
|
|
458
|
-
setSaving(false);
|
|
459
|
-
}
|
|
460
|
-
};
|
|
812
|
+
const { pending, saving, notice, save } = useThemeSave({ trialId, globals, themeApi, scales: unsaved, overrides, history: history2 });
|
|
461
813
|
const onCollapsedChange = useCallback(
|
|
462
814
|
(category, collapsed) => setFolds((prev) => ({ ...prev, [category]: collapsed })),
|
|
463
815
|
[setFolds]
|
|
464
|
-
);
|
|
465
|
-
return
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
ToggleBar$1,
|
|
470
|
-
{
|
|
471
|
-
ariaLabel: "Variables",
|
|
472
|
-
variant: "flat",
|
|
473
|
-
size: "sm",
|
|
474
|
-
items: TABS,
|
|
475
|
-
value: tab,
|
|
476
|
-
onChange: (next) => {
|
|
477
|
-
if (next) setTab(next);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
),
|
|
481
|
-
/* @__PURE__ */ jsx(
|
|
482
|
-
Input,
|
|
483
|
-
{
|
|
484
|
-
className: "fg-css-vars__filter",
|
|
485
|
-
"aria-label": "Filter",
|
|
486
|
-
placeholder: "Filter",
|
|
487
|
-
value: filter,
|
|
488
|
-
onChange: setFilter,
|
|
489
|
-
leadingAdornment: /* @__PURE__ */ jsx(SearchIcon, { size: 12 })
|
|
490
|
-
}
|
|
491
|
-
)
|
|
492
|
-
] }),
|
|
493
|
-
tab === "theme" && (unsaved.length > 0 || notice) ? /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__save", children: [
|
|
494
|
-
unsaved.length > 0 ? /* @__PURE__ */ jsx(Button$1, { variant: "primary", size: "sm", loading: saving, disabled: saving, onClick: () => void save(), children: "Save scales to theme" }) : null,
|
|
495
|
-
notice ? /* @__PURE__ */ jsx(
|
|
496
|
-
"p",
|
|
497
|
-
{
|
|
498
|
-
className: `fg-css-vars__notice fg-css-vars__notice--${notice.tone}`,
|
|
499
|
-
role: notice.tone === "error" ? "alert" : "status",
|
|
500
|
-
children: notice.text
|
|
501
|
-
}
|
|
502
|
-
) : null
|
|
503
|
-
] }) : null
|
|
504
|
-
] }),
|
|
505
|
-
shown.length === 0 ? /* @__PURE__ */ jsx("p", { className: "fg-css-vars__empty", children: tokens.length === 0 ? "The story\u2019s frame has reported no variables." : "No variables match." }) : /* @__PURE__ */ jsx(
|
|
506
|
-
TokenPanel,
|
|
816
|
+
);
|
|
817
|
+
return (
|
|
818
|
+
// Capture: React Aria's fields stop a keydown from bubbling past them.
|
|
819
|
+
/* @__PURE__ */ jsxs(
|
|
820
|
+
"div",
|
|
507
821
|
{
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
822
|
+
className: "fg-css-vars",
|
|
823
|
+
onKeyDownCapture: (e) => {
|
|
824
|
+
const to = e.target.closest(".fg-css-vars__filter") ? null : historyKey(e);
|
|
825
|
+
if (!to) return;
|
|
826
|
+
e.preventDefault();
|
|
827
|
+
history2.step(to);
|
|
828
|
+
},
|
|
829
|
+
children: [
|
|
830
|
+
/* @__PURE__ */ jsxs("div", { className: "fg-css-vars__controls", children: [
|
|
831
|
+
/* @__PURE__ */ jsxs("div", { className: "fg-css-vars__bar", children: [
|
|
832
|
+
/* @__PURE__ */ jsx(
|
|
833
|
+
ToggleBar$1,
|
|
834
|
+
{
|
|
835
|
+
ariaLabel: "Variables",
|
|
836
|
+
variant: "flat",
|
|
837
|
+
size: "sm",
|
|
838
|
+
items: TABS,
|
|
839
|
+
value: tab,
|
|
840
|
+
onChange: (next) => {
|
|
841
|
+
if (next) setTab(next);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
),
|
|
845
|
+
/* @__PURE__ */ jsx(
|
|
846
|
+
Input,
|
|
847
|
+
{
|
|
848
|
+
className: "fg-css-vars__filter",
|
|
849
|
+
"aria-label": "Filter",
|
|
850
|
+
placeholder: "Filter",
|
|
851
|
+
value: filter,
|
|
852
|
+
onChange: setFilter,
|
|
853
|
+
leadingAdornment: /* @__PURE__ */ jsx(SearchIcon, { size: 12 })
|
|
854
|
+
}
|
|
855
|
+
),
|
|
856
|
+
/* @__PURE__ */ jsxs("div", { className: "fg-css-vars__steps", role: "group", "aria-label": "History", children: [
|
|
857
|
+
/* @__PURE__ */ jsx(Button$1, { iconOnly: true, ariaLabel: "Undo", variant: "ghost", size: "sm", disabled: !history2.canUndo, onClick: () => history2.step("undo"), children: /* @__PURE__ */ jsx(Icon, { name: "undo", size: 14 }) }),
|
|
858
|
+
/* @__PURE__ */ jsx(Button$1, { iconOnly: true, ariaLabel: "Redo", variant: "ghost", size: "sm", disabled: !history2.canRedo, onClick: () => history2.step("redo"), children: /* @__PURE__ */ jsx(Icon, { name: "redo", size: 14 }) })
|
|
859
|
+
] })
|
|
860
|
+
] }),
|
|
861
|
+
tab === "theme" && (pending || notice) ? /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__save", children: [
|
|
862
|
+
pending ? /* @__PURE__ */ jsx(Button$1, { variant: "primary", size: "sm", loading: saving, disabled: saving, onClick: () => void save(), children: "Save to theme" }) : null,
|
|
863
|
+
notice ? /* @__PURE__ */ jsx(
|
|
864
|
+
"p",
|
|
865
|
+
{
|
|
866
|
+
className: `fg-css-vars__notice fg-css-vars__notice--${notice.tone}`,
|
|
867
|
+
role: notice.tone === "error" ? "alert" : "status",
|
|
868
|
+
children: notice.text
|
|
869
|
+
}
|
|
870
|
+
) : null
|
|
871
|
+
] }) : null
|
|
872
|
+
] }),
|
|
873
|
+
shown.length === 0 ? /* @__PURE__ */ jsx("p", { className: "fg-css-vars__empty", children: tokens.length === 0 ? "The story\u2019s frame has reported no variables." : "No variables match." }) : /* @__PURE__ */ jsx("div", { className: "fg-css-vars__list", children: /* @__PURE__ */ jsx(
|
|
874
|
+
TokenPanel,
|
|
875
|
+
{
|
|
876
|
+
tokens: shown,
|
|
877
|
+
namePrefix: TOKEN_PREFIX,
|
|
878
|
+
onChange: write,
|
|
879
|
+
scales,
|
|
880
|
+
onScaleChange,
|
|
881
|
+
collapsed: folds,
|
|
882
|
+
onCollapsedChange
|
|
883
|
+
}
|
|
884
|
+
) })
|
|
885
|
+
]
|
|
515
886
|
}
|
|
516
887
|
)
|
|
517
|
-
|
|
888
|
+
);
|
|
518
889
|
}
|
|
519
890
|
function FocusedCssVars() {
|
|
520
891
|
const { focusedTrialId, trials, instruments } = useLabContext();
|
|
@@ -531,19 +902,8 @@ function FocusedCssVars() {
|
|
|
531
902
|
var CSS_VARS_SECTION = {
|
|
532
903
|
id: "fg-css-vars",
|
|
533
904
|
region: "aside",
|
|
534
|
-
item: { title: "CSS Vars", actions: /* @__PURE__ */ jsx(SectionJump, {}), body: /* @__PURE__ */ jsx(FocusedCssVars, {}) }
|
|
905
|
+
item: { title: "CSS Vars", actions: /* @__PURE__ */ jsx(SectionJump, {}), fill: true, body: /* @__PURE__ */ jsx(FocusedCssVars, {}) }
|
|
535
906
|
};
|
|
536
|
-
|
|
537
|
-
// src/shell/breadcrumb.ts
|
|
538
|
-
function crumbs(title, name) {
|
|
539
|
-
const segments = title.split("/");
|
|
540
|
-
const steps = segments.map((label, i) => ({ label, path: segments.slice(0, i + 1).join("/") }));
|
|
541
|
-
steps.push({ label: name, path: null });
|
|
542
|
-
return steps.filter((step, i) => i === 0 || step.label.toLowerCase() !== "index");
|
|
543
|
-
}
|
|
544
|
-
function breadcrumb(title, name) {
|
|
545
|
-
return crumbs(title, name).map((step) => step.label).join(" > ");
|
|
546
|
-
}
|
|
547
907
|
var GLOBALS = "$globals";
|
|
548
908
|
function leaves(shape, prefix = "") {
|
|
549
909
|
const out = [];
|
|
@@ -556,7 +916,7 @@ function leaves(shape, prefix = "") {
|
|
|
556
916
|
return out;
|
|
557
917
|
}
|
|
558
918
|
function storyDossier(input) {
|
|
559
|
-
const { entry, instrument, config, ready = true } = input;
|
|
919
|
+
const { entry, libraries = TITLE_LIBRARIES, instrument, config, ready = true } = input;
|
|
560
920
|
const schema = ready ? instrument?.config : void 0;
|
|
561
921
|
const args = schema ? leaves(schema.nodes).map(({ path, node }) => {
|
|
562
922
|
const value = valueAtPath(config, path);
|
|
@@ -575,7 +935,7 @@ function storyDossier(input) {
|
|
|
575
935
|
name: entry.name,
|
|
576
936
|
exportName: entry.exportName,
|
|
577
937
|
file: entry.file,
|
|
578
|
-
library:
|
|
938
|
+
library: libraries.of(entry),
|
|
579
939
|
...entry.componentName === void 0 ? {} : { componentName: entry.componentName },
|
|
580
940
|
...entry.description === void 0 ? {} : { description: entry.description },
|
|
581
941
|
...entry.componentDescription === void 0 ? {} : { componentDescription: entry.componentDescription },
|
|
@@ -638,11 +998,13 @@ function StoryDossier({ dossier }) {
|
|
|
638
998
|
}
|
|
639
999
|
function StoryInfoDialog({ index, isReady, isOpen, onOpenChange }) {
|
|
640
1000
|
const { focusedTrialId, trials, instruments } = useLabContext();
|
|
1001
|
+
const libraries = useContext(LibrariesContext);
|
|
641
1002
|
const trial = trials.find((record) => record.id === focusedTrialId);
|
|
642
1003
|
const entry = trial ? index.find((candidate) => candidate.id === trial.instrumentName) : void 0;
|
|
643
1004
|
if (!entry) return null;
|
|
644
1005
|
const dossier = storyDossier({
|
|
645
1006
|
entry,
|
|
1007
|
+
libraries,
|
|
646
1008
|
instrument: instruments.find((candidate) => candidate.name === entry.id),
|
|
647
1009
|
config: trial?.config,
|
|
648
1010
|
ready: isReady(entry.id)
|
|
@@ -658,6 +1020,7 @@ function StoryInfoDialog({ index, isReady, isOpen, onOpenChange }) {
|
|
|
658
1020
|
}
|
|
659
1021
|
);
|
|
660
1022
|
}
|
|
1023
|
+
var HELLO_TIMEOUT_MS = 3e4;
|
|
661
1024
|
function canMoveFrames() {
|
|
662
1025
|
return typeof Element !== "undefined" && "moveBefore" in Element.prototype;
|
|
663
1026
|
}
|
|
@@ -678,21 +1041,30 @@ function createFramePool(frameUrl, size = 2) {
|
|
|
678
1041
|
shelf.setAttribute("aria-hidden", "true");
|
|
679
1042
|
document.body.append(shelf);
|
|
680
1043
|
const warm = [];
|
|
681
|
-
|
|
1044
|
+
const waits = /* @__PURE__ */ new Set();
|
|
682
1045
|
let cancelRefill = null;
|
|
683
1046
|
let disposed = false;
|
|
684
1047
|
const load = () => {
|
|
685
|
-
loading += 1;
|
|
686
1048
|
const frame = document.createElement("iframe");
|
|
687
1049
|
frame.tabIndex = -1;
|
|
688
1050
|
frame.src = frameUrl;
|
|
1051
|
+
const stop = () => {
|
|
1052
|
+
window.removeEventListener("message", onMessage);
|
|
1053
|
+
clearTimeout(timer);
|
|
1054
|
+
waits.delete(stop);
|
|
1055
|
+
};
|
|
689
1056
|
const onMessage = (event) => {
|
|
690
1057
|
if (event.source !== frame.contentWindow || event.origin !== location.origin) return;
|
|
691
1058
|
if (event.data?.type !== FRAME_HELLO) return;
|
|
692
|
-
|
|
693
|
-
loading -= 1;
|
|
1059
|
+
stop();
|
|
694
1060
|
warm.push(frame);
|
|
695
1061
|
};
|
|
1062
|
+
const timer = setTimeout(() => {
|
|
1063
|
+
stop();
|
|
1064
|
+
frame.remove();
|
|
1065
|
+
refill();
|
|
1066
|
+
}, HELLO_TIMEOUT_MS);
|
|
1067
|
+
waits.add(stop);
|
|
696
1068
|
window.addEventListener("message", onMessage);
|
|
697
1069
|
shelf.append(frame);
|
|
698
1070
|
};
|
|
@@ -700,7 +1072,7 @@ function createFramePool(frameUrl, size = 2) {
|
|
|
700
1072
|
if (disposed || cancelRefill) return;
|
|
701
1073
|
cancelRefill = whenIdle(() => {
|
|
702
1074
|
cancelRefill = null;
|
|
703
|
-
while (!disposed && warm.length +
|
|
1075
|
+
while (!disposed && warm.length + waits.size < size) load();
|
|
704
1076
|
});
|
|
705
1077
|
};
|
|
706
1078
|
if (canMoveFrames()) refill();
|
|
@@ -714,6 +1086,7 @@ function createFramePool(frameUrl, size = 2) {
|
|
|
714
1086
|
dispose() {
|
|
715
1087
|
disposed = true;
|
|
716
1088
|
cancelRefill?.();
|
|
1089
|
+
for (const stop of [...waits]) stop();
|
|
717
1090
|
shelf.remove();
|
|
718
1091
|
}
|
|
719
1092
|
};
|
|
@@ -724,69 +1097,6 @@ var FramePoolContext = createContext(null);
|
|
|
724
1097
|
function routedTrial(trials, focusedTrialId, route) {
|
|
725
1098
|
return trials.find((t) => t.id === focusedTrialId && t.instrumentName === route) ?? trials.find((t) => t.instrumentName === route);
|
|
726
1099
|
}
|
|
727
|
-
function readRoute() {
|
|
728
|
-
return parseRoute(location.hash).story;
|
|
729
|
-
}
|
|
730
|
-
function readRouteParams() {
|
|
731
|
-
return { ...parseRoute(location.hash).params };
|
|
732
|
-
}
|
|
733
|
-
function replaceRouteParams(update) {
|
|
734
|
-
const { story, params } = parseRoute(location.hash);
|
|
735
|
-
if (story === null) return;
|
|
736
|
-
const next = formatRoute({ story, params: update({ ...params }) });
|
|
737
|
-
if (next !== location.hash) history.replaceState(history.state, "", next);
|
|
738
|
-
}
|
|
739
|
-
function readRouteEntry() {
|
|
740
|
-
const state = history.state;
|
|
741
|
-
const entry = state?.forgeRoute;
|
|
742
|
-
return typeof entry?.step === "number" ? entry : null;
|
|
743
|
-
}
|
|
744
|
-
function crossesInPlace(from, to) {
|
|
745
|
-
if (!from || !to || from.step === to.step) return false;
|
|
746
|
-
return to.step < from.step ? from.inPlace : to.inPlace;
|
|
747
|
-
}
|
|
748
|
-
function stamp(entry, hash) {
|
|
749
|
-
history.replaceState({ ...history.state ?? {}, forgeRoute: entry }, "", hash);
|
|
750
|
-
}
|
|
751
|
-
function setRoute(id, options = {}) {
|
|
752
|
-
if (readRoute() === id) {
|
|
753
|
-
history.replaceState(history.state, "", formatRoute({ story: id, params: readRouteParams() }));
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
const hash = formatRoute({ story: id, params: {} });
|
|
757
|
-
const from = readRouteEntry() ?? { step: 0, inPlace: false };
|
|
758
|
-
stamp(from, location.hash);
|
|
759
|
-
location.hash = hash;
|
|
760
|
-
stamp({ step: from.step + 1, inPlace: options.inPlace ?? false }, hash);
|
|
761
|
-
}
|
|
762
|
-
function useRoute() {
|
|
763
|
-
const [route, setCurrent] = useState(readRoute);
|
|
764
|
-
useEffect(() => {
|
|
765
|
-
const sync = () => setCurrent(readRoute());
|
|
766
|
-
window.addEventListener("hashchange", sync);
|
|
767
|
-
sync();
|
|
768
|
-
return () => window.removeEventListener("hashchange", sync);
|
|
769
|
-
}, []);
|
|
770
|
-
return [route, setRoute];
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// src/timeline/playheadUrl.ts
|
|
774
|
-
function formatSeconds(ms) {
|
|
775
|
-
const rounded = Math.round(ms) / 1e3;
|
|
776
|
-
return String(rounded === 0 ? 0 : rounded);
|
|
777
|
-
}
|
|
778
|
-
function parseSeconds(text) {
|
|
779
|
-
if (text == null || !/^\s*-?(\d+\.?\d*|\.\d+)\s*$/.test(text)) return null;
|
|
780
|
-
return Math.round(Number(text) * 1e3);
|
|
781
|
-
}
|
|
782
|
-
function readPlayheadParam() {
|
|
783
|
-
return parseSeconds(readRouteParams()[PLAYHEAD_PARAM]);
|
|
784
|
-
}
|
|
785
|
-
function writePlayheadParam(ms) {
|
|
786
|
-
replaceRouteParams(
|
|
787
|
-
({ [PLAYHEAD_PARAM]: _held, ...rest }) => ms === null ? rest : { ...rest, [PLAYHEAD_PARAM]: formatSeconds(ms) }
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
1100
|
function PlayheadSync({ trialId, route, applied }) {
|
|
791
1101
|
const clock = useTrialClock(trialId);
|
|
792
1102
|
useEffect(() => {
|
|
@@ -938,6 +1248,7 @@ function KnobSync({ schema, config }) {
|
|
|
938
1248
|
return () => window.removeEventListener("hashchange", onHashChange);
|
|
939
1249
|
}, []);
|
|
940
1250
|
useEffect(() => {
|
|
1251
|
+
if (reading.current && takeRouteFromTrial()) reading.current = false;
|
|
941
1252
|
if (reading.current) {
|
|
942
1253
|
reading.current = false;
|
|
943
1254
|
const knobs2 = paramsToKnobs(schema, readRouteParams());
|
|
@@ -987,18 +1298,6 @@ function revealTrial(trialId, root = document) {
|
|
|
987
1298
|
return true;
|
|
988
1299
|
}
|
|
989
1300
|
|
|
990
|
-
// src/shell/treeReveal.ts
|
|
991
|
-
var listeners = /* @__PURE__ */ new Set();
|
|
992
|
-
function revealInTree(path) {
|
|
993
|
-
for (const listener of listeners) listener(path);
|
|
994
|
-
}
|
|
995
|
-
function onRevealInTree(listener) {
|
|
996
|
-
listeners.add(listener);
|
|
997
|
-
return () => {
|
|
998
|
-
listeners.delete(listener);
|
|
999
|
-
};
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
1301
|
// src/shell/tree/buildTree.ts
|
|
1003
1302
|
function sortFolder(nodes) {
|
|
1004
1303
|
const folders = nodes.filter((n) => n.kind === "folder");
|
|
@@ -1055,7 +1354,7 @@ function filterTree(nodes, query) {
|
|
|
1055
1354
|
}
|
|
1056
1355
|
|
|
1057
1356
|
// src/shell/tree/buildComponents.ts
|
|
1058
|
-
function buildComponents(index) {
|
|
1357
|
+
function buildComponents(index, libraries) {
|
|
1059
1358
|
const byTitle = /* @__PURE__ */ new Map();
|
|
1060
1359
|
const pages = new Map(indexEntries(index).map((page) => [page.title, page]));
|
|
1061
1360
|
for (const entry of index) {
|
|
@@ -1065,7 +1364,7 @@ function buildComponents(index) {
|
|
|
1065
1364
|
row = {
|
|
1066
1365
|
path: entry.title,
|
|
1067
1366
|
label: segments[segments.length - 1],
|
|
1068
|
-
library:
|
|
1367
|
+
library: libraries.of(entry),
|
|
1069
1368
|
index: pages.get(entry.title),
|
|
1070
1369
|
entries: []
|
|
1071
1370
|
};
|
|
@@ -1142,8 +1441,8 @@ function componentNodes(rows) {
|
|
|
1142
1441
|
}
|
|
1143
1442
|
return foldGalleries(root);
|
|
1144
1443
|
}
|
|
1145
|
-
function librariesIn(index) {
|
|
1146
|
-
return [...new Set(index.map(
|
|
1444
|
+
function librariesIn(index, libraries) {
|
|
1445
|
+
return [...new Set(index.map(libraries.of))].sort((a, b) => a.localeCompare(b));
|
|
1147
1446
|
}
|
|
1148
1447
|
var VIEWS = [
|
|
1149
1448
|
{ value: "tree", label: "Tree" },
|
|
@@ -1173,7 +1472,8 @@ function StoryTree({ ctx, index }) {
|
|
|
1173
1472
|
const [hidden, setHidden] = usePersistedState("fg-tree-libraries", {}, { scope: "lab" });
|
|
1174
1473
|
const [focusKey, setFocusKey] = useState(null);
|
|
1175
1474
|
const items = useRef(/* @__PURE__ */ new Map());
|
|
1176
|
-
const
|
|
1475
|
+
const table = useContext(LibrariesContext);
|
|
1476
|
+
const libraries = useMemo(() => librariesIn(index, table), [index, table]);
|
|
1177
1477
|
const views = useMemo(() => {
|
|
1178
1478
|
const hasGallery = index.some(isGallery);
|
|
1179
1479
|
const hasOther = index.some((entry) => !isGallery(entry));
|
|
@@ -1181,7 +1481,7 @@ function StoryTree({ ctx, index }) {
|
|
|
1181
1481
|
}, [index]);
|
|
1182
1482
|
const enabled = (value) => views.some((item) => item.value === value && !item.disabled);
|
|
1183
1483
|
const view = [chosen, "components", "gallery"].find(enabled) ?? chosen;
|
|
1184
|
-
const kept = useMemo(() => index.filter((entry) => !hidden[
|
|
1484
|
+
const kept = useMemo(() => index.filter((entry) => !hidden[table.of(entry)]), [index, hidden, table]);
|
|
1185
1485
|
const [galleryEntries, rest] = useMemo(() => {
|
|
1186
1486
|
const yes = [];
|
|
1187
1487
|
const no = [];
|
|
@@ -1189,8 +1489,8 @@ function StoryTree({ ctx, index }) {
|
|
|
1189
1489
|
return [yes, no];
|
|
1190
1490
|
}, [kept]);
|
|
1191
1491
|
const tree = useMemo(() => buildTree(rest), [rest]);
|
|
1192
|
-
const components = useMemo(() => buildComponents(rest), [rest]);
|
|
1193
|
-
const galleries = useMemo(() => buildComponents(galleryEntries), [galleryEntries]);
|
|
1492
|
+
const components = useMemo(() => buildComponents(rest, table), [rest, table]);
|
|
1493
|
+
const galleries = useMemo(() => buildComponents(galleryEntries, table), [galleryEntries, table]);
|
|
1194
1494
|
const shown = useMemo(
|
|
1195
1495
|
() => view === "tree" ? filterTree(tree, query) : componentNodes(filterComponents(view === "gallery" ? galleries : components, query)),
|
|
1196
1496
|
[view, components, galleries, tree, query]
|
|
@@ -1221,8 +1521,11 @@ function StoryTree({ ctx, index }) {
|
|
|
1221
1521
|
const activate = (entry, another) => {
|
|
1222
1522
|
const focused = ctx.trials.find((trial) => trial.id === ctx.focusedTrialId);
|
|
1223
1523
|
if (another || !focused) ctx.addTrial(entry.id);
|
|
1224
|
-
else if (focused.instrumentName === entry.id)
|
|
1225
|
-
|
|
1524
|
+
else if (focused.instrumentName === entry.id) {
|
|
1525
|
+
revealTrial(focused.id);
|
|
1526
|
+
setRoute2(entry.id, { fromTrial: true });
|
|
1527
|
+
return;
|
|
1528
|
+
} else {
|
|
1226
1529
|
ctx.swapTrial(focused.id, entry.id);
|
|
1227
1530
|
setRoute2(entry.id, { inPlace: true });
|
|
1228
1531
|
return;
|
|
@@ -1443,13 +1746,11 @@ function StoryTree({ ctx, index }) {
|
|
|
1443
1746
|
/* @__PURE__ */ jsx("div", { role: "tree", "aria-labelledby": headingId, className: "fg-tree__items", onKeyDown, children: renderNodes(shown, 1) })
|
|
1444
1747
|
] });
|
|
1445
1748
|
}
|
|
1446
|
-
|
|
1447
|
-
// src/shell/answers.ts
|
|
1448
1749
|
var KEPT_CONFIGS = 50;
|
|
1449
1750
|
var UNANSWERED = stableStringify({ hidden: [], errors: {} });
|
|
1450
1751
|
function createAnswerBook() {
|
|
1451
1752
|
const byConfig = /* @__PURE__ */ new Map();
|
|
1452
|
-
const
|
|
1753
|
+
const answered = createEmitter();
|
|
1453
1754
|
const held = /* @__PURE__ */ new Map();
|
|
1454
1755
|
const keyOf2 = (config) => stableStringify(storyConfig(config));
|
|
1455
1756
|
const answerFor = (config) => byConfig.get(keyOf2(config));
|
|
@@ -1463,7 +1764,7 @@ function createAnswerBook() {
|
|
|
1463
1764
|
const oldest = byConfig.keys().next().value;
|
|
1464
1765
|
if (oldest !== void 0) byConfig.delete(oldest);
|
|
1465
1766
|
}
|
|
1466
|
-
if (changed && held.has(answers.configKey))
|
|
1767
|
+
if (changed && held.has(answers.configKey)) answered.emit();
|
|
1467
1768
|
},
|
|
1468
1769
|
hold(config) {
|
|
1469
1770
|
const key = keyOf2(config);
|
|
@@ -1479,164 +1780,23 @@ function createAnswerBook() {
|
|
|
1479
1780
|
return hidden?.some((h) => path === h || path.startsWith(`${h}.`)) ?? false;
|
|
1480
1781
|
},
|
|
1481
1782
|
errors: (path, config) => answerFor(config)?.errors[path] ?? [],
|
|
1482
|
-
subscribe
|
|
1483
|
-
listeners2.add(fn);
|
|
1484
|
-
return () => {
|
|
1485
|
-
listeners2.delete(fn);
|
|
1486
|
-
};
|
|
1487
|
-
}
|
|
1488
|
-
};
|
|
1489
|
-
}
|
|
1490
|
-
function TransportBar({ clock }) {
|
|
1491
|
-
const { time, playing, loop, rate, span } = useSyncExternalStore(clock.subscribe, clock.get);
|
|
1492
|
-
return /* @__PURE__ */ jsxs("div", { className: "fg-transport", children: [
|
|
1493
|
-
/* @__PURE__ */ jsx(
|
|
1494
|
-
Slider,
|
|
1495
|
-
{
|
|
1496
|
-
className: "fg-transport__scrub",
|
|
1497
|
-
ariaLabel: "Scrub",
|
|
1498
|
-
density: "slim",
|
|
1499
|
-
readoutPlacement: "none",
|
|
1500
|
-
min: span.start,
|
|
1501
|
-
max: span.end > span.start ? span.end : span.start + 1,
|
|
1502
|
-
thumbs: [{ value: time, valueText: `${formatSeconds(time)} seconds` }],
|
|
1503
|
-
onInput: ([thumb]) => {
|
|
1504
|
-
if (thumb) clock.seek(thumb.value);
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
),
|
|
1508
|
-
/* @__PURE__ */ jsx(
|
|
1509
|
-
Transport,
|
|
1510
|
-
{
|
|
1511
|
-
paused: !playing,
|
|
1512
|
-
loop,
|
|
1513
|
-
rate,
|
|
1514
|
-
playhead: time,
|
|
1515
|
-
duration: span.end,
|
|
1516
|
-
onPlay: clock.play,
|
|
1517
|
-
onPause: clock.pause,
|
|
1518
|
-
onLoopChange: (next) => clock.setLoop(next !== false && next !== 0),
|
|
1519
|
-
onRateChange: clock.setRate
|
|
1520
|
-
}
|
|
1521
|
-
)
|
|
1522
|
-
] });
|
|
1523
|
-
}
|
|
1524
|
-
function TrialTransport({ trialId }) {
|
|
1525
|
-
const clock = useTrialClock(trialId);
|
|
1526
|
-
return clock ? /* @__PURE__ */ jsx(TransportBar, { clock }) : null;
|
|
1527
|
-
}
|
|
1528
|
-
function Crumbs({ entry }) {
|
|
1529
|
-
const steps = crumbs(entry.title, entry.name);
|
|
1530
|
-
return /* @__PURE__ */ jsx("span", { className: "fg-crumbs", children: steps.map((step, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1531
|
-
i > 0 ? /* @__PURE__ */ jsx("span", { className: "fg-crumbs__sep", "aria-hidden": "true", children: "\u203A" }) : null,
|
|
1532
|
-
step.path === null ? /* @__PURE__ */ jsx("span", { className: "fg-crumbs__here", children: step.label }) : /* @__PURE__ */ jsx(
|
|
1533
|
-
"button",
|
|
1534
|
-
{
|
|
1535
|
-
type: "button",
|
|
1536
|
-
className: "fg-crumbs__step",
|
|
1537
|
-
title: `Show ${step.label} in the story tree`,
|
|
1538
|
-
onPointerDown: (event) => event.stopPropagation(),
|
|
1539
|
-
onClick: () => revealInTree(step.path),
|
|
1540
|
-
children: step.label
|
|
1541
|
-
}
|
|
1542
|
-
)
|
|
1543
|
-
] }, step.path ?? `${step.label}:${i}`)) });
|
|
1544
|
-
}
|
|
1545
|
-
function crumbTitle(entry) {
|
|
1546
|
-
const title = breadcrumb(entry.title, entry.name);
|
|
1547
|
-
return { title, renderTitle: (shown) => shown === title ? /* @__PURE__ */ jsx(Crumbs, { entry }) : shown };
|
|
1548
|
-
}
|
|
1549
|
-
function IndexTrial({ bundle, setup, config }) {
|
|
1550
|
-
const lab = useLabContext();
|
|
1551
|
-
const trialId = useContext(TrialIdContext);
|
|
1552
|
-
const graph = useContext(DependenciesContext);
|
|
1553
|
-
const gallery = isGallery(bundle);
|
|
1554
|
-
const dependencies = gallery ? void 0 : graph?.[bundle.title];
|
|
1555
|
-
const open = (id) => {
|
|
1556
|
-
if (!trialId || !lab.instruments.some((instrument) => instrument.name === id)) return;
|
|
1557
|
-
lab.swapTrial(trialId, id);
|
|
1558
|
-
setRoute(id, { inPlace: true });
|
|
1559
|
-
};
|
|
1560
|
-
const render = (globals) => {
|
|
1561
|
-
const env = {
|
|
1562
|
-
title: bundle.title,
|
|
1563
|
-
library: bundle.library,
|
|
1564
|
-
...bundle.description === void 0 ? {} : { description: bundle.description },
|
|
1565
|
-
stories: bundle.stories,
|
|
1566
|
-
descriptions: bundle.descriptions,
|
|
1567
|
-
gallery,
|
|
1568
|
-
galleries: bundle.galleries,
|
|
1569
|
-
...dependencies ? { dependencies } : {},
|
|
1570
|
-
globals,
|
|
1571
|
-
decorators: setup.decorators ?? [],
|
|
1572
|
-
open,
|
|
1573
|
-
// A story that throws shows its error in its own cell; the rest of the page is still worth reading.
|
|
1574
|
-
onError: (error) => console.error(error)
|
|
1575
|
-
};
|
|
1576
|
-
return /* @__PURE__ */ jsx(IndexPage, { env, render: bundle.render });
|
|
1577
|
-
};
|
|
1578
|
-
return /* @__PURE__ */ jsx(TrialHost, { layout: "fullscreen", setup, config, children: render });
|
|
1579
|
-
}
|
|
1580
|
-
function withPlayhead(schema) {
|
|
1581
|
-
return f.schema({ ...schema.nodes, [PLAYHEAD_KEY]: f.custom("forge-playhead", null).hidden() });
|
|
1582
|
-
}
|
|
1583
|
-
function documentInstrument(options) {
|
|
1584
|
-
const { entry, loaded, setup, globals = {} } = options;
|
|
1585
|
-
const schema = loaded.kind === "story" ? loaded.story.config : f.schema({});
|
|
1586
|
-
const timed = loaded.kind === "story" && loaded.story.timeline !== null;
|
|
1587
|
-
const config = withGlobals(timed ? withPlayhead(schema) : schema, globals);
|
|
1588
|
-
const initialState = loaded.kind === "story" ? loaded.story.initialState : null;
|
|
1589
|
-
return {
|
|
1590
|
-
name: entry.id,
|
|
1591
|
-
...crumbTitle(entry),
|
|
1592
|
-
config,
|
|
1593
|
-
defaultConfig: () => config.defaults(),
|
|
1594
|
-
initialState: (c) => initialState?.(c) ?? null,
|
|
1595
|
-
render: (ctx) => loaded.kind === "story" ? /* @__PURE__ */ jsx(StoryTrial, { story: loaded.story, setup, ctx }, loaded.story.id) : /* @__PURE__ */ jsx(IndexTrial, { bundle: loaded.bundle, setup, config: ctx.config }),
|
|
1596
|
-
...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {},
|
|
1597
|
-
...timed ? { chrome: [{ id: "fg-transport", region: "status", render: (c) => /* @__PURE__ */ jsx(TrialTransport, { trialId: c.trialId }) }] } : {}
|
|
1598
|
-
};
|
|
1599
|
-
}
|
|
1600
|
-
function StoryLoad({ entry, load, fault }) {
|
|
1601
|
-
useEffect(() => {
|
|
1602
|
-
if (!fault) load(entry);
|
|
1603
|
-
}, [entry, load, fault]);
|
|
1604
|
-
return /* @__PURE__ */ jsx("div", { className: "fg-story fg-story--loading", "data-fg-layout": "fullscreen", children: fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
|
|
1605
|
-
/* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: "import" }),
|
|
1606
|
-
/* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
|
|
1607
|
-
] }) : null });
|
|
1608
|
-
}
|
|
1609
|
-
function pendingInstrument(options) {
|
|
1610
|
-
const { entry, globals = {} } = options;
|
|
1611
|
-
const config = withGlobals(f.schema({}), globals);
|
|
1612
|
-
return {
|
|
1613
|
-
name: entry.id,
|
|
1614
|
-
...crumbTitle(entry),
|
|
1615
|
-
config,
|
|
1616
|
-
defaultConfig: () => config.defaults(),
|
|
1617
|
-
initialState: () => null,
|
|
1618
|
-
render: () => /* @__PURE__ */ jsx(StoryLoad, { ...options }, options.generation ?? 0)
|
|
1783
|
+
subscribe: answered.subscribe
|
|
1619
1784
|
};
|
|
1620
1785
|
}
|
|
1621
1786
|
|
|
1622
1787
|
// src/shell/readyKey.ts
|
|
1623
1788
|
var readyKey = (ready) => stableStringify({ schema: ready.schema, layout: ready.layout, viewport: ready.viewport });
|
|
1624
1789
|
var START_TIMEOUT_MS = 1e4;
|
|
1625
|
-
var HELLO_TIMEOUT_MS = 3e4;
|
|
1626
1790
|
var requests = 0;
|
|
1627
1791
|
function closeLink(link) {
|
|
1628
1792
|
if (!link.current) return;
|
|
1629
1793
|
clearTimeout(link.current.timer);
|
|
1630
|
-
for (const
|
|
1794
|
+
for (const settle of link.current.pending.values()) settle({ ok: false, message: "The frame went away" });
|
|
1631
1795
|
link.current.pending.clear();
|
|
1632
1796
|
link.current.disconnect();
|
|
1633
1797
|
link.current.channel.close();
|
|
1634
1798
|
link.current = null;
|
|
1635
1799
|
}
|
|
1636
|
-
function settle(link, id, value) {
|
|
1637
|
-
link.pending.get(id)?.resolve?.(value);
|
|
1638
|
-
link.pending.delete(id);
|
|
1639
|
-
}
|
|
1640
1800
|
function mismatchMessage(mismatch) {
|
|
1641
1801
|
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";
|
|
1642
1802
|
}
|
|
@@ -1657,29 +1817,29 @@ function FrameView(props) {
|
|
|
1657
1817
|
const slotRef = useRef(null);
|
|
1658
1818
|
const inView = useNearViewport(hostRef, IN_VIEW_MARGIN);
|
|
1659
1819
|
const link = useRef(null);
|
|
1660
|
-
const
|
|
1820
|
+
const latestRef = useLatest({ ...props, globals, overrides, pool, lab, title });
|
|
1661
1821
|
const [fault, setFault] = useState(null);
|
|
1662
1822
|
const [pending, setPending] = useState(true);
|
|
1663
1823
|
const init = useCallback((current) => {
|
|
1664
|
-
const { ctx: live, globals: liveGlobals } =
|
|
1824
|
+
const { ctx: live, globals: liveGlobals } = latestRef.current;
|
|
1665
1825
|
current.awaiting = null;
|
|
1666
1826
|
current.sent = { config: storyConfig(live.config), state: live.state, globals: liveGlobals };
|
|
1667
1827
|
current.inputs += 1;
|
|
1668
1828
|
current.channel.send({ type: "init", ...current.sent });
|
|
1669
|
-
}, [
|
|
1829
|
+
}, [latestRef]);
|
|
1670
1830
|
const receive = (current, msg) => {
|
|
1671
|
-
const { ctx: live, answers: answers2, onReady } =
|
|
1831
|
+
const { ctx: live, answers: answers2, onReady } = latestRef.current;
|
|
1672
1832
|
switch (msg.type) {
|
|
1673
1833
|
case "ready": {
|
|
1674
1834
|
clearTimeout(current.timer);
|
|
1675
1835
|
setFault(null);
|
|
1676
|
-
for (const [name, value] of Object.entries(
|
|
1836
|
+
for (const [name, value] of Object.entries(latestRef.current.overrides)) {
|
|
1677
1837
|
current.channel.send({ type: "vars.set", name, value });
|
|
1678
1838
|
}
|
|
1679
1839
|
const key = readyKey(msg);
|
|
1680
1840
|
current.awaiting = key;
|
|
1681
|
-
onReady(
|
|
1682
|
-
if (current.awaiting === key && key ===
|
|
1841
|
+
onReady(latestRef.current.entry, msg);
|
|
1842
|
+
if (current.awaiting === key && key === latestRef.current.descriptionKey) init(current);
|
|
1683
1843
|
break;
|
|
1684
1844
|
}
|
|
1685
1845
|
case "rendered":
|
|
@@ -1700,22 +1860,15 @@ function FrameView(props) {
|
|
|
1700
1860
|
case "vars":
|
|
1701
1861
|
if (trialId) frames?.report(trialId, msg.vars);
|
|
1702
1862
|
break;
|
|
1703
|
-
case "size":
|
|
1704
|
-
if (trialId) frames?.reportSize(trialId, { width: msg.width, height: msg.height });
|
|
1705
|
-
break;
|
|
1706
1863
|
case "a11y": {
|
|
1707
1864
|
const outcome = msg.ok ? { ok: true, report: msg.report } : { ok: false, message: msg.message };
|
|
1708
1865
|
if (trialId) frames?.reportA11y(trialId, outcome);
|
|
1709
|
-
|
|
1710
|
-
break;
|
|
1711
|
-
}
|
|
1712
|
-
case "capture":
|
|
1713
|
-
if (msg.ok) settle(current, msg.id, msg.picture);
|
|
1714
|
-
else current.pending.get(msg.id)?.fail(new Error(msg.message));
|
|
1866
|
+
current.pending.get(msg.id)?.(outcome);
|
|
1715
1867
|
current.pending.delete(msg.id);
|
|
1716
1868
|
break;
|
|
1869
|
+
}
|
|
1717
1870
|
case "open": {
|
|
1718
|
-
const { lab: liveLab } =
|
|
1871
|
+
const { lab: liveLab } = latestRef.current;
|
|
1719
1872
|
if (!trialId || !liveLab.instruments.some((instrument) => instrument.name === msg.id)) break;
|
|
1720
1873
|
liveLab.swapTrial(trialId, msg.id);
|
|
1721
1874
|
setRoute(msg.id, { inPlace: true });
|
|
@@ -1742,33 +1895,14 @@ function FrameView(props) {
|
|
|
1742
1895
|
}
|
|
1743
1896
|
});
|
|
1744
1897
|
const waiting = /* @__PURE__ */ new Map();
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
gone: () => gone(resolve, reject)
|
|
1750
|
-
});
|
|
1751
|
-
const ask = () => channel.send(msg);
|
|
1898
|
+
const audit = () => new Promise((resolve) => {
|
|
1899
|
+
const id = `a11y-${requests += 1}`;
|
|
1900
|
+
waiting.set(id, resolve);
|
|
1901
|
+
const ask = () => channel.send({ type: "a11y.run", id });
|
|
1752
1902
|
if (current.hasRendered) ask();
|
|
1753
1903
|
else current.queued.push(ask);
|
|
1754
1904
|
});
|
|
1755
|
-
const
|
|
1756
|
-
const id = `a11y-${requests += 1}`;
|
|
1757
|
-
return request(
|
|
1758
|
-
id,
|
|
1759
|
-
{ type: "a11y.run", id },
|
|
1760
|
-
(resolve) => resolve({ ok: false, message: "The frame went away" })
|
|
1761
|
-
);
|
|
1762
|
-
};
|
|
1763
|
-
const capture = () => {
|
|
1764
|
-
const id = `capture-${requests += 1}`;
|
|
1765
|
-
return request(
|
|
1766
|
-
id,
|
|
1767
|
-
{ type: "capture.run", id },
|
|
1768
|
-
(_resolve, fail) => fail(new Error("The frame could not draw itself"))
|
|
1769
|
-
);
|
|
1770
|
-
};
|
|
1771
|
-
const disconnect = frames && trialId ? frames.connect(trialId, { send: channel.send, audit, capture }) : () => {
|
|
1905
|
+
const disconnect = frames && trialId ? frames.connect(trialId, { send: channel.send, audit }) : () => {
|
|
1772
1906
|
};
|
|
1773
1907
|
const current = {
|
|
1774
1908
|
channel,
|
|
@@ -1783,7 +1917,7 @@ function FrameView(props) {
|
|
|
1783
1917
|
};
|
|
1784
1918
|
link.current = current;
|
|
1785
1919
|
channel.on((msg) => receive(current, msg));
|
|
1786
|
-
target.postMessage({ type: PORT_HANDOFF, id:
|
|
1920
|
+
target.postMessage({ type: PORT_HANDOFF, id: latestRef.current.entry.id }, location.origin, [
|
|
1787
1921
|
port2
|
|
1788
1922
|
]);
|
|
1789
1923
|
};
|
|
@@ -1791,10 +1925,10 @@ function FrameView(props) {
|
|
|
1791
1925
|
useEffect(() => {
|
|
1792
1926
|
const slot = slotRef.current;
|
|
1793
1927
|
if (!inView || !slot) return;
|
|
1794
|
-
const claimed =
|
|
1928
|
+
const claimed = latestRef.current.pool?.claim() ?? null;
|
|
1795
1929
|
const frame = claimed ?? document.createElement("iframe");
|
|
1796
1930
|
frame.className = "fg-frame-view";
|
|
1797
|
-
frame.title =
|
|
1931
|
+
frame.title = latestRef.current.title;
|
|
1798
1932
|
frame.removeAttribute("tabindex");
|
|
1799
1933
|
frame.toggleAttribute("data-pending", true);
|
|
1800
1934
|
iframeRef.current = frame;
|
|
@@ -1822,7 +1956,7 @@ function FrameView(props) {
|
|
|
1822
1956
|
setPending(true);
|
|
1823
1957
|
setFault(null);
|
|
1824
1958
|
};
|
|
1825
|
-
}, [inView, src, connectRef,
|
|
1959
|
+
}, [inView, src, connectRef, latestRef]);
|
|
1826
1960
|
useEffect(() => {
|
|
1827
1961
|
iframeRef.current?.toggleAttribute("data-pending", pending && !fault);
|
|
1828
1962
|
}, [pending, fault, inView, src]);
|
|
@@ -1861,23 +1995,13 @@ function FrameView(props) {
|
|
|
1861
1995
|
}
|
|
1862
1996
|
if (input) setFault((shown) => shown?.phase === "render" ? null : shown);
|
|
1863
1997
|
}, [ctx.config, ctx.state, globals, descriptionKey, init]);
|
|
1864
|
-
return /* @__PURE__ */ jsxs(
|
|
1865
|
-
"div",
|
|
1866
|
-
{
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
className: "fg-frame-host",
|
|
1872
|
-
children: [
|
|
1873
|
-
/* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
|
|
1874
|
-
fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
|
|
1875
|
-
fault.phase ? /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: fault.phase }) : null,
|
|
1876
|
-
/* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
|
|
1877
|
-
] }) : null
|
|
1878
|
-
]
|
|
1879
|
-
}
|
|
1880
|
-
);
|
|
1998
|
+
return /* @__PURE__ */ jsxs("div", { ref: hostRef, className: "fg-frame-host", children: [
|
|
1999
|
+
/* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
|
|
2000
|
+
fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
|
|
2001
|
+
fault.phase ? /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: fault.phase }) : null,
|
|
2002
|
+
/* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
|
|
2003
|
+
] }) : null
|
|
2004
|
+
] });
|
|
1881
2005
|
}
|
|
1882
2006
|
function storyInstrument(options) {
|
|
1883
2007
|
const { entry, ready, answers, frameUrl, onReady, globals = {} } = options;
|
|
@@ -1915,7 +2039,7 @@ var NO_SETUP = {};
|
|
|
1915
2039
|
var pruned = (map, ids) => [...map.keys()].every((id) => ids.has(id)) ? map : new Map([...map].filter(([id]) => ids.has(id)));
|
|
1916
2040
|
var toError = (error) => error instanceof Error ? error : new Error(String(error));
|
|
1917
2041
|
function useStoryRegistry(index, options) {
|
|
1918
|
-
const { frameUrl, globals = NO_GLOBALS2, importers, setup = NO_SETUP, load = loadStories } = options;
|
|
2042
|
+
const { frameUrl, globals = NO_GLOBALS2, importers, setup = NO_SETUP, libraries = TITLE_LIBRARIES, load = loadStories } = options;
|
|
1919
2043
|
const [readies, setReadies] = useState(() => /* @__PURE__ */ new Map());
|
|
1920
2044
|
const [loaded, setLoaded] = useState(() => /* @__PURE__ */ new Map());
|
|
1921
2045
|
const [faults, setFaults] = useState(() => /* @__PURE__ */ new Map());
|
|
@@ -1939,20 +2063,22 @@ function useStoryRegistry(index, options) {
|
|
|
1939
2063
|
setRevisions((prev) => pruned(prev, ids));
|
|
1940
2064
|
}
|
|
1941
2065
|
const imports = useRef(/* @__PURE__ */ new Map());
|
|
1942
|
-
const
|
|
2066
|
+
const liveRef = useLatest({ index, importers, setup, load, libraries });
|
|
2067
|
+
const missing = useRef(/* @__PURE__ */ new Set());
|
|
1943
2068
|
const importOf = useCallback((file) => {
|
|
1944
2069
|
let loading = imports.current.get(file);
|
|
1945
2070
|
if (!loading) {
|
|
1946
|
-
const importer =
|
|
2071
|
+
const importer = liveRef.current.importers?.[file];
|
|
2072
|
+
if (!importer) missing.current.add(file);
|
|
1947
2073
|
loading = importer ? importer() : Promise.reject(new Error(`No importer for ${file}`));
|
|
1948
2074
|
imports.current.set(file, loading);
|
|
1949
2075
|
}
|
|
1950
2076
|
return loading;
|
|
1951
|
-
}, [
|
|
2077
|
+
}, [liveRef]);
|
|
1952
2078
|
const request = useCallback(
|
|
1953
2079
|
(entry) => {
|
|
1954
|
-
const { setup: current, load: loadModule } =
|
|
1955
|
-
const
|
|
2080
|
+
const { setup: current, load: loadModule } = liveRef.current;
|
|
2081
|
+
const settle = (ids2, result) => {
|
|
1956
2082
|
setLoaded((prev) => {
|
|
1957
2083
|
const next = new Map(prev);
|
|
1958
2084
|
for (const [id, value] of result) next.set(id, value);
|
|
@@ -1969,7 +2095,7 @@ function useStoryRegistry(index, options) {
|
|
|
1969
2095
|
});
|
|
1970
2096
|
};
|
|
1971
2097
|
if (isIndexId(entry.id)) {
|
|
1972
|
-
const entries =
|
|
2098
|
+
const entries = liveRef.current.index.filter((e) => !isIndexId(e.id) && indexId(e.title) === entry.id);
|
|
1973
2099
|
const files = [...new Set(entries.map((e) => e.file))];
|
|
1974
2100
|
void Promise.all(files.map(importOf)).then(async (mods) => {
|
|
1975
2101
|
const byFile = new Map(files.map((file, i) => [file, loadModule(mods[i] ?? {}, entry.title, current.parameters)]));
|
|
@@ -1982,7 +2108,7 @@ function useStoryRegistry(index, options) {
|
|
|
1982
2108
|
kind: "index",
|
|
1983
2109
|
bundle: {
|
|
1984
2110
|
title: entry.title,
|
|
1985
|
-
library:
|
|
2111
|
+
library: liveRef.current.libraries.of(entry),
|
|
1986
2112
|
...entry.componentDescription === void 0 ? {} : { description: entry.componentDescription },
|
|
1987
2113
|
...entry.tags === void 0 ? {} : { tags: entry.tags },
|
|
1988
2114
|
galleries: entries.filter(isGallery).map((e) => e.id),
|
|
@@ -1991,11 +2117,11 @@ function useStoryRegistry(index, options) {
|
|
|
1991
2117
|
render: mods.map(indexRenderOf).find((render) => render !== null) ?? null
|
|
1992
2118
|
}
|
|
1993
2119
|
});
|
|
1994
|
-
|
|
2120
|
+
settle([entry.id], result);
|
|
1995
2121
|
}).catch((error) => fail([entry.id], error));
|
|
1996
2122
|
return;
|
|
1997
2123
|
}
|
|
1998
|
-
const siblings =
|
|
2124
|
+
const siblings = liveRef.current.index.filter((e) => e.file === entry.file && !isIndexId(e.id));
|
|
1999
2125
|
const ids = siblings.map((e) => e.id);
|
|
2000
2126
|
void importOf(entry.file).then(async (mod) => {
|
|
2001
2127
|
const stories = loadModule(mod, entry.title, current.parameters);
|
|
@@ -2007,22 +2133,29 @@ function useStoryRegistry(index, options) {
|
|
|
2007
2133
|
result.set(sibling.id, { kind: "story", story });
|
|
2008
2134
|
}
|
|
2009
2135
|
if (!result.has(entry.id)) throw new Error(`${entry.file} has no story export "${entry.exportName}"`);
|
|
2010
|
-
|
|
2136
|
+
settle(ids, result);
|
|
2011
2137
|
}).catch((error) => fail(ids, error));
|
|
2012
2138
|
},
|
|
2013
|
-
[importOf,
|
|
2139
|
+
[importOf, liveRef]
|
|
2014
2140
|
);
|
|
2015
2141
|
const reload = useCallback(
|
|
2016
2142
|
(file) => {
|
|
2017
2143
|
imports.current.delete(file);
|
|
2018
|
-
const affected =
|
|
2144
|
+
const affected = liveRef.current.index.filter((e) => e.file === file || isIndexId(e.id) && liveRef.current.index.some((s) => s.file === file && indexId(s.title) === e.id));
|
|
2019
2145
|
const ids = new Set(affected.map((e) => e.id));
|
|
2020
2146
|
setLoaded((prev) => pruned(prev, new Set([...prev.keys()].filter((id) => !ids.has(id)))));
|
|
2021
2147
|
setFaults((prev) => pruned(prev, new Set([...prev.keys()].filter((id) => !ids.has(id)))));
|
|
2022
2148
|
setGenerations((prev) => new Map(prev).set(file, (prev.get(file) ?? 0) + 1));
|
|
2023
2149
|
},
|
|
2024
|
-
[
|
|
2150
|
+
[liveRef]
|
|
2025
2151
|
);
|
|
2152
|
+
useEffect(() => {
|
|
2153
|
+
for (const file of [...missing.current]) {
|
|
2154
|
+
if (!importers?.[file]) continue;
|
|
2155
|
+
missing.current.delete(file);
|
|
2156
|
+
reload(file);
|
|
2157
|
+
}
|
|
2158
|
+
}, [importers, reload]);
|
|
2026
2159
|
const committed = useRef({ built: /* @__PURE__ */ new Map(), books: /* @__PURE__ */ new Map(), list: [] });
|
|
2027
2160
|
const cache = useMemo(() => {
|
|
2028
2161
|
const previous = committed.current;
|
|
@@ -2073,12 +2206,7 @@ function useStoryRegistry(index, options) {
|
|
|
2073
2206
|
return { instruments: cache.list, isReady, onReady, reload };
|
|
2074
2207
|
}
|
|
2075
2208
|
var NO_DECLARATIONS = {};
|
|
2076
|
-
|
|
2077
|
-
function initialStory(entries, fallback) {
|
|
2078
|
-
const route = readRoute();
|
|
2079
|
-
return route !== null && entries.some((entry) => entry.id === route) ? route : fallback;
|
|
2080
|
-
}
|
|
2081
|
-
function RouteOpener({ index }) {
|
|
2209
|
+
function RouteOpener() {
|
|
2082
2210
|
const lab = useLabContext();
|
|
2083
2211
|
const [route] = useRoute();
|
|
2084
2212
|
const handled = useRef(null);
|
|
@@ -2086,14 +2214,14 @@ function RouteOpener({ index }) {
|
|
|
2086
2214
|
useEffect(() => {
|
|
2087
2215
|
const from = last.current;
|
|
2088
2216
|
if (route !== from.route) last.current = { route, entry: readRouteEntry() };
|
|
2089
|
-
if (route === null || handled.current === route || !
|
|
2217
|
+
if (route === null || handled.current === route || !lab.instruments.some((i) => i.name === route)) return;
|
|
2090
2218
|
handled.current = route;
|
|
2091
2219
|
if (lab.trials.some((trial) => trial.instrumentName === route)) return;
|
|
2092
2220
|
const previous = lab.trials.find((trial) => trial.instrumentName === from.route);
|
|
2093
2221
|
const target = (previous && crossesInPlace(from.entry, last.current.entry) ? previous : void 0) ?? lab.trials.find((trial) => trial.id === lab.focusedTrialId) ?? lab.trials[0];
|
|
2094
2222
|
if (target) lab.swapTrial(target.id, route);
|
|
2095
2223
|
else lab.addTrial(route);
|
|
2096
|
-
}, [route,
|
|
2224
|
+
}, [route, lab]);
|
|
2097
2225
|
return null;
|
|
2098
2226
|
}
|
|
2099
2227
|
function useInfoShortcut(open) {
|
|
@@ -2130,12 +2258,21 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
|
|
|
2130
2258
|
};
|
|
2131
2259
|
}, [frameUrl, framed]);
|
|
2132
2260
|
const entries = useMemo(() => [...index, ...indexEntries(index)], [index]);
|
|
2261
|
+
const table = config?.libraries;
|
|
2262
|
+
const libraries = useMemo(() => librariesFrom(table), [table]);
|
|
2133
2263
|
const registry = useStoryRegistry(entries, {
|
|
2134
2264
|
frameUrl,
|
|
2135
2265
|
globals: declarations,
|
|
2266
|
+
libraries,
|
|
2136
2267
|
...importers ? { importers } : {},
|
|
2137
2268
|
...setup ? { setup } : {}
|
|
2138
2269
|
});
|
|
2270
|
+
const [route] = useRoute();
|
|
2271
|
+
const missing = route !== null && !entries.some((entry) => entry.id === route) ? route : null;
|
|
2272
|
+
const instruments = useMemo(
|
|
2273
|
+
() => missing === null ? registry.instruments : [...registry.instruments, missingInstrument(missing, declarations)],
|
|
2274
|
+
[registry.instruments, missing, declarations]
|
|
2275
|
+
);
|
|
2139
2276
|
const reload = registry.reload;
|
|
2140
2277
|
useEffect(() => changes?.subscribe(reload), [changes, reload]);
|
|
2141
2278
|
const [labValues, setLabValues] = useState(() => labGlobals(declarations, void 0));
|
|
@@ -2147,20 +2284,20 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
|
|
|
2147
2284
|
(next) => setLabValues((prev) => stableStringify(prev) === stableStringify(next) ? prev : next),
|
|
2148
2285
|
[]
|
|
2149
2286
|
);
|
|
2287
|
+
const infoCommand = useMemo(
|
|
2288
|
+
() => ({ icon: InfoIcon, label: "Info", shortcut: "\u2318I", onActivate: () => setInfoOpen(true) }),
|
|
2289
|
+
[]
|
|
2290
|
+
);
|
|
2150
2291
|
const labChrome = useMemo(
|
|
2151
2292
|
() => [
|
|
2152
2293
|
{ id: "fg-stories", region: "sidebar", render: (ctx) => /* @__PURE__ */ jsx(StoryTree, { ctx, index }) },
|
|
2153
|
-
{
|
|
2154
|
-
id: "fg-info",
|
|
2155
|
-
region: "palette",
|
|
2156
|
-
item: { icon: InfoIcon, label: "Info", shortcut: "\u2318I", onActivate: () => setInfoOpen(true) }
|
|
2157
|
-
},
|
|
2158
2294
|
...config?.cssVars === false ? [] : [CSS_VARS_SECTION],
|
|
2159
2295
|
A11Y_SECTION,
|
|
2160
2296
|
...Object.keys(declarations).length > 0 ? [{ id: "fg-globals", region: "header", render: () => /* @__PURE__ */ jsx(GlobalsToolbar, { declarations }) }] : [],
|
|
2297
|
+
{ id: "fg-info", region: "header", group: "story", item: infoCommand },
|
|
2161
2298
|
...config?.labChrome ?? []
|
|
2162
2299
|
],
|
|
2163
|
-
[index, declarations, config?.cssVars, config?.labChrome]
|
|
2300
|
+
[index, infoCommand, declarations, config?.cssVars, config?.labChrome]
|
|
2164
2301
|
);
|
|
2165
2302
|
const first = index[0];
|
|
2166
2303
|
if (!first) {
|
|
@@ -2169,14 +2306,14 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
|
|
|
2169
2306
|
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
|
|
2170
2307
|
] });
|
|
2171
2308
|
}
|
|
2172
|
-
return /* @__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(
|
|
2309
|
+
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(
|
|
2173
2310
|
Lab,
|
|
2174
2311
|
{
|
|
2175
2312
|
title,
|
|
2176
2313
|
density: "roomy",
|
|
2177
2314
|
...labTheme ? { theme: labTheme } : {},
|
|
2178
|
-
instruments
|
|
2179
|
-
defaultInstrument:
|
|
2315
|
+
instruments,
|
|
2316
|
+
defaultInstrument: readRoute() ?? first.id,
|
|
2180
2317
|
storageKey: storageKey ?? "weaselforge",
|
|
2181
2318
|
...storage ? { storage } : {},
|
|
2182
2319
|
labChrome,
|
|
@@ -2185,7 +2322,7 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
|
|
|
2185
2322
|
...config?.pages ? { pages: config.pages } : {},
|
|
2186
2323
|
...config?.path !== void 0 ? { path: config.path } : {},
|
|
2187
2324
|
children: [
|
|
2188
|
-
/* @__PURE__ */ jsx(RouteOpener, {
|
|
2325
|
+
/* @__PURE__ */ jsx(RouteOpener, {}),
|
|
2189
2326
|
/* @__PURE__ */ jsx(RouteKnobs, { instruments: registry.instruments, isReady: registry.isReady }),
|
|
2190
2327
|
/* @__PURE__ */ jsx(RoutePlayhead, {}),
|
|
2191
2328
|
/* @__PURE__ */ jsx(
|
|
@@ -2200,7 +2337,7 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
|
|
|
2200
2337
|
/* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues })
|
|
2201
2338
|
]
|
|
2202
2339
|
}
|
|
2203
|
-
) }) }) }) }) });
|
|
2340
|
+
) }) }) }) }) }) });
|
|
2204
2341
|
}
|
|
2205
2342
|
function mountWorkshop({ container, ...props }) {
|
|
2206
2343
|
const target = container ?? document.getElementById("root");
|