@weasel-js/forge 1.5.1 → 1.6.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 +72 -10
- package/dist/{FrameController-BPfCt4PJ.d.ts → FrameController-BspC9Zr5.d.ts} +67 -4
- package/dist/chunk-A4JOEGYQ.js +325 -0
- package/dist/chunk-A4JOEGYQ.js.map +1 -0
- package/dist/{chunk-2MADHLRA.js → chunk-FWPBKKYB.js} +58 -231
- package/dist/chunk-FWPBKKYB.js.map +1 -0
- package/dist/{chunk-6SRQIHJQ.js → chunk-HRGPFQBW.js} +3 -3
- package/dist/{chunk-6SRQIHJQ.js.map → chunk-HRGPFQBW.js.map} +1 -1
- package/dist/chunk-PO5JLVYT.js +318 -0
- package/dist/chunk-PO5JLVYT.js.map +1 -0
- package/dist/{chunk-TLO6UHF2.js → chunk-VEKEBILL.js} +17 -4
- package/dist/chunk-VEKEBILL.js.map +1 -0
- package/dist/{config-CrLpfSq9.d.ts → config-DU_WuORS.d.ts} +7 -1
- package/dist/csf/preview-api.js +1 -2
- package/dist/csf/preview-api.js.map +1 -1
- package/dist/frame/index.css +102 -0
- package/dist/frame/index.css.map +1 -1
- package/dist/frame/index.d.ts +11 -3
- package/dist/frame/index.js +365 -5
- package/dist/frame/index.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js.map +1 -1
- package/dist/shell/index.css +202 -13
- package/dist/shell/index.css.map +1 -1
- package/dist/shell/index.d.ts +76 -12
- package/dist/shell/index.js +1075 -187
- package/dist/shell/index.js.map +1 -1
- package/dist/test/index.d.ts +3 -3
- package/dist/test/index.js +67 -72
- package/dist/test/index.js.map +1 -1
- package/dist/vite/index.js +68 -11
- package/dist/vite/index.js.map +1 -1
- package/package.json +5 -4
- package/dist/chunk-2JDYOBWM.js +0 -118
- package/dist/chunk-2JDYOBWM.js.map +0 -1
- package/dist/chunk-2MADHLRA.js.map +0 -1
- package/dist/chunk-FTFR2YPM.js +0 -13
- package/dist/chunk-FTFR2YPM.js.map +0 -1
- package/dist/chunk-TLO6UHF2.js.map +0 -1
package/dist/shell/index.js
CHANGED
|
@@ -1,75 +1,19 @@
|
|
|
1
|
-
import { schemaFromDescription, openChannel } from '../chunk-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
1
|
+
import { isIndexId, indexId, indexEntries, schemaFromDescription, openChannel, IndexPage } from '../chunk-PO5JLVYT.js';
|
|
2
|
+
import { useTrialFrame, createTrialFrames, labGlobals, StoryGlobalsContext, TrialFramesContext, withGlobals, StoryTrial, storyConfig, GLOBALS_KEY, effectiveGlobals, useCssOverrides, TrialHost, isGlobalsPath } from '../chunk-A4JOEGYQ.js';
|
|
3
|
+
export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-A4JOEGYQ.js';
|
|
4
|
+
import { loadStories, indexRenderOf } from '../chunk-FWPBKKYB.js';
|
|
5
|
+
import '../chunk-2GXKNXTJ.js';
|
|
6
|
+
import '../chunk-HRGPFQBW.js';
|
|
7
|
+
import { stableStringify, PORT_HANDOFF, FRAME_HELLO, PROTOCOL_VERSION } from '../chunk-VEKEBILL.js';
|
|
8
|
+
import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, inferTokenType, ToggleBar as ToggleBar$1, Input, TokenPanel, refitScale } from '@weasel-js/labkit';
|
|
9
|
+
import { createContext, useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect, useId, useContext } from 'react';
|
|
5
10
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
11
|
import { createRoot } from 'react-dom/client';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
12
|
+
import { InfoIcon, ToggleBar, Checkbox, Dialog, Badge, DisclosureMark, Button as Button$1, TuneIcon, Callout } from '@weasel-js/ui';
|
|
13
|
+
import { TOKEN_MANIFEST, THEME_SOURCES, isByAxis } from '@weasel-js/theme';
|
|
14
|
+
import { scale, httpThemeApi } from '@weasel-js/theme/engine';
|
|
15
|
+
import { f, valueAtPath, isConfigBranch } from '@weasel-js/labkit/config';
|
|
9
16
|
|
|
10
|
-
var NO_FRAME = { send: null, vars: [], audit: null, a11y: null, capture: null, size: null };
|
|
11
|
-
function createTrialFrames() {
|
|
12
|
-
const frames = /* @__PURE__ */ new Map();
|
|
13
|
-
const hosts = /* @__PURE__ */ new Map();
|
|
14
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
15
|
-
const changed = () => {
|
|
16
|
-
for (const listener of [...listeners]) listener();
|
|
17
|
-
};
|
|
18
|
-
const patch = (trialId, next) => {
|
|
19
|
-
const frame = frames.get(trialId);
|
|
20
|
-
if (!frame) return;
|
|
21
|
-
frames.set(trialId, { ...frame, ...next });
|
|
22
|
-
changed();
|
|
23
|
-
};
|
|
24
|
-
return {
|
|
25
|
-
connect(trialId, calls) {
|
|
26
|
-
const held = frames.get(trialId);
|
|
27
|
-
frames.set(trialId, {
|
|
28
|
-
send: calls.send,
|
|
29
|
-
vars: [],
|
|
30
|
-
audit: calls.audit,
|
|
31
|
-
a11y: null,
|
|
32
|
-
capture: calls.capture,
|
|
33
|
-
size: held?.size ?? null
|
|
34
|
-
});
|
|
35
|
-
changed();
|
|
36
|
-
return () => {
|
|
37
|
-
if (frames.get(trialId)?.send !== calls.send) return;
|
|
38
|
-
frames.delete(trialId);
|
|
39
|
-
changed();
|
|
40
|
-
};
|
|
41
|
-
},
|
|
42
|
-
report: (trialId, vars) => patch(trialId, { vars }),
|
|
43
|
-
reportA11y: (trialId, a11y) => patch(trialId, { a11y }),
|
|
44
|
-
reportSize(trialId, size) {
|
|
45
|
-
const frame = frames.get(trialId) ?? NO_FRAME;
|
|
46
|
-
if (frame.size?.width === size.width && frame.size?.height === size.height) return;
|
|
47
|
-
frames.set(trialId, { ...frame, size });
|
|
48
|
-
changed();
|
|
49
|
-
},
|
|
50
|
-
hostRef(trialId) {
|
|
51
|
-
let ref = hosts.get(trialId);
|
|
52
|
-
if (!ref) {
|
|
53
|
-
ref = { current: null };
|
|
54
|
-
hosts.set(trialId, ref);
|
|
55
|
-
}
|
|
56
|
-
return ref;
|
|
57
|
-
},
|
|
58
|
-
get: (trialId) => frames.get(trialId) ?? NO_FRAME,
|
|
59
|
-
subscribe(listener) {
|
|
60
|
-
listeners.add(listener);
|
|
61
|
-
return () => listeners.delete(listener);
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
var TrialFramesContext = createContext(null);
|
|
66
|
-
function useTrialFrame(trialId) {
|
|
67
|
-
const frames = useContext(TrialFramesContext);
|
|
68
|
-
const subscribe = useCallback((listener) => frames?.subscribe(listener) ?? (() => {
|
|
69
|
-
}), [frames]);
|
|
70
|
-
const read = () => frames?.get(trialId) ?? NO_FRAME;
|
|
71
|
-
return useSyncExternalStore(subscribe, read, read);
|
|
72
|
-
}
|
|
73
17
|
function Finding({ finding }) {
|
|
74
18
|
return /* @__PURE__ */ jsxs("li", { className: "fg-a11y__finding", children: [
|
|
75
19
|
/* @__PURE__ */ jsxs("p", { className: "fg-a11y__help", children: [
|
|
@@ -147,20 +91,176 @@ var A11Y_SECTION = {
|
|
|
147
91
|
region: "aside",
|
|
148
92
|
item: { title: "Accessibility", body: /* @__PURE__ */ jsx(FocusedA11y, {}) }
|
|
149
93
|
};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
94
|
+
|
|
95
|
+
// src/shell/storyChanges.ts
|
|
96
|
+
function createStoryChanges() {
|
|
97
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
98
|
+
return {
|
|
99
|
+
subscribe(listener) {
|
|
100
|
+
listeners.add(listener);
|
|
101
|
+
return () => listeners.delete(listener);
|
|
102
|
+
},
|
|
103
|
+
emit(file) {
|
|
104
|
+
for (const listener of [...listeners]) listener(file);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
var SEED_REF = /^\{seeds\.([^}]+)\}$/;
|
|
109
|
+
var RULE_KEYS = { factors: "factors", ratio: "ratio", step: "step" };
|
|
110
|
+
function selectionFor(axes, globals) {
|
|
111
|
+
const out = {};
|
|
112
|
+
for (const [axis, def] of Object.entries(axes)) {
|
|
113
|
+
const value = globals[axis];
|
|
114
|
+
if (value === void 0) out[axis] = def.default;
|
|
115
|
+
else if (typeof value === "string" && value in def.values) out[axis] = value;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
var EditError = class extends Error {
|
|
120
|
+
};
|
|
121
|
+
function slotOf(root, holder, key, where, selection, depth = 0) {
|
|
122
|
+
if (depth > 16) throw new EditError(`${where} references itself`);
|
|
123
|
+
let h = holder;
|
|
124
|
+
let k = key;
|
|
125
|
+
let w = where;
|
|
126
|
+
let value = h[k];
|
|
127
|
+
while (isByAxis(value)) {
|
|
128
|
+
const axis = value.by;
|
|
129
|
+
const picked = selection[axis];
|
|
130
|
+
if (picked === void 0 || picked === "by" || !(picked in value)) {
|
|
131
|
+
throw new EditError(`${w} differs by ${axis}, and which ${axis} this trial shows is not known`);
|
|
132
|
+
}
|
|
133
|
+
h = value;
|
|
134
|
+
k = picked;
|
|
135
|
+
w = `${w}.${picked}`;
|
|
136
|
+
value = h[k];
|
|
137
|
+
}
|
|
138
|
+
if (typeof value === "number") return { holder: h, key: k, where: w };
|
|
139
|
+
if (typeof value === "string") {
|
|
140
|
+
const ref = SEED_REF.exec(value);
|
|
141
|
+
if (!ref) throw new EditError(`${w} is "${value}", which is not a number or a seed`);
|
|
142
|
+
const name = ref[1];
|
|
143
|
+
const seeds = root.seeds;
|
|
144
|
+
if (!seeds || !(name in seeds)) throw new EditError(`${w} names the seed "${name}", which this theme inherits rather than defines`);
|
|
145
|
+
return slotOf(root, seeds, name, `seeds.${name}`, selection, depth + 1);
|
|
146
|
+
}
|
|
147
|
+
throw new EditError(`${w} is not a number`);
|
|
148
|
+
}
|
|
149
|
+
function factorsOf(holder, where, selection) {
|
|
150
|
+
let value = holder.factors;
|
|
151
|
+
let w = `${where}.factors`;
|
|
152
|
+
while (isByAxis(value)) {
|
|
153
|
+
const picked = selection[value.by];
|
|
154
|
+
if (picked === void 0 || !(picked in value)) {
|
|
155
|
+
throw new EditError(`${w} differs by ${value.by}, and which ${value.by} this trial shows is not known`);
|
|
156
|
+
}
|
|
157
|
+
w = `${w}.${picked}`;
|
|
158
|
+
value = value[picked];
|
|
159
|
+
}
|
|
160
|
+
if (!Array.isArray(value)) throw new EditError(`${w} is not a list`);
|
|
161
|
+
return { list: value, where: w };
|
|
162
|
+
}
|
|
163
|
+
var kindOf = (scale) => scale.factors !== void 0 ? "factors" : scale.ratio !== void 0 ? "ratio" : scale.step !== void 0 ? "step" : null;
|
|
164
|
+
var plainLiteral = (value) => typeof value === "number" || Array.isArray(value) && value.every((v) => typeof v === "number");
|
|
165
|
+
function swapRule(scale, from, to, value) {
|
|
166
|
+
const entries = Object.entries(scale);
|
|
167
|
+
for (const [k] of entries) delete scale[k];
|
|
168
|
+
for (const [k, v] of entries) scale[k === from ? to : k] = k === from ? value : v;
|
|
169
|
+
if (from === null) scale[to] = value;
|
|
170
|
+
}
|
|
171
|
+
function applyScaleEdits(definition, edits, selection) {
|
|
172
|
+
const root = structuredClone(definition);
|
|
173
|
+
const writes = /* @__PURE__ */ new Map();
|
|
174
|
+
const write = (slot, value) => {
|
|
175
|
+
let byKey = writes.get(slot.holder);
|
|
176
|
+
if (!byKey) writes.set(slot.holder, byKey = /* @__PURE__ */ new Map());
|
|
177
|
+
const pending = byKey.get(slot.key);
|
|
178
|
+
if (pending !== void 0 && pending !== value) throw new EditError(`${slot.where} would need to be both ${pending} and ${value}`);
|
|
179
|
+
byKey.set(slot.key, value);
|
|
180
|
+
};
|
|
181
|
+
try {
|
|
182
|
+
for (const [name, edit] of Object.entries(edits)) {
|
|
183
|
+
const scales = root.scales;
|
|
184
|
+
const scale = scales?.[name];
|
|
185
|
+
if (!scales || !scale) throw new EditError(`the theme defines no scale "${name}"`);
|
|
186
|
+
const where = `scales.${name}`;
|
|
187
|
+
write(slotOf(root, scale, "base", `${where}.base`, selection), edit.base);
|
|
188
|
+
const { rule } = edit;
|
|
189
|
+
const had = kindOf(scale);
|
|
190
|
+
if (had !== rule.kind) {
|
|
191
|
+
const from = had ? RULE_KEYS[had] : null;
|
|
192
|
+
if (from && !plainLiteral(scale[from])) {
|
|
193
|
+
throw new EditError(`${where} would lose its ${from} to become a ${rule.kind} scale, and its ${from} is not a plain number`);
|
|
194
|
+
}
|
|
195
|
+
const value = rule.kind === "factors" ? [...rule.factors] : rule.kind === "ratio" ? rule.ratio : rule.step;
|
|
196
|
+
swapRule(scale, from, RULE_KEYS[rule.kind], value);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (rule.kind === "factors") {
|
|
200
|
+
const { list, where: at } = factorsOf(scale, where, selection);
|
|
201
|
+
if (list.length !== rule.factors.length) {
|
|
202
|
+
throw new EditError(`${at} has ${list.length} factors, and the edit ${rule.factors.length}`);
|
|
203
|
+
}
|
|
204
|
+
rule.factors.forEach((factor, i) => write(slotOf(root, list, i, `${at}.${i}`, selection), factor));
|
|
205
|
+
} else {
|
|
206
|
+
const key = RULE_KEYS[rule.kind];
|
|
207
|
+
write(slotOf(root, scale, key, `${where}.${key}`, selection), rule.kind === "ratio" ? rule.ratio : rule.step);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
if (e instanceof EditError) return { ok: false, message: `Cannot save: ${e.message}.` };
|
|
212
|
+
throw e;
|
|
213
|
+
}
|
|
214
|
+
for (const [holder, byKey] of writes) for (const [key, value] of byKey) holder[key] = value;
|
|
215
|
+
return { ok: true, definition: root };
|
|
153
216
|
}
|
|
154
217
|
var TABS = [
|
|
155
218
|
{ value: "theme", label: "Theme" },
|
|
156
219
|
{ value: "story", label: "Story" }
|
|
157
220
|
];
|
|
158
221
|
var MANIFEST = new Map(TOKEN_MANIFEST.map((token) => [token.name, token]));
|
|
222
|
+
var THEME_NAME = "weasel";
|
|
223
|
+
var literal = (v) => typeof v === "number";
|
|
224
|
+
var SCALE_SOURCES = Object.entries(THEME_SOURCES[THEME_NAME]?.scales ?? {}).flatMap(([name, def]) => {
|
|
225
|
+
if (!Array.isArray(def.steps)) return [];
|
|
226
|
+
const steps = def.steps;
|
|
227
|
+
const tokens = steps.map((step) => `--wzl-${name}-${step}`);
|
|
228
|
+
const group = MANIFEST.get(tokens[0] ?? "")?.group;
|
|
229
|
+
if (!group) return [];
|
|
230
|
+
const factors = Array.isArray(def.factors) && def.factors.every(literal) ? def.factors : null;
|
|
231
|
+
const rule = def.factors !== void 0 ? factors ? { kind: "factors", factors } : "factors" : def.ratio !== void 0 ? literal(def.ratio) ? { kind: "ratio", ratio: def.ratio } : "ratio" : literal(def.step) ? { kind: "step", step: def.step } : "step";
|
|
232
|
+
return [{ name, group, steps, tokens, rule }];
|
|
233
|
+
});
|
|
234
|
+
function scaleValues(source, next) {
|
|
235
|
+
const { rule } = next;
|
|
236
|
+
const values = scale(source.steps, {
|
|
237
|
+
base: next.base,
|
|
238
|
+
...rule.kind === "factors" ? { factors: rule.factors } : rule.kind === "ratio" ? { ratio: rule.ratio } : { step: rule.step }
|
|
239
|
+
});
|
|
240
|
+
return source.steps.flatMap((step, i) => {
|
|
241
|
+
const value = values[step];
|
|
242
|
+
const name = source.tokens[i];
|
|
243
|
+
return value !== void 0 && name !== void 0 ? [[name, value]] : [];
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function standingScale(source, amounts) {
|
|
247
|
+
const seed = { tokens: source.tokens, base: amounts[0] ?? 0, rule: { kind: "step", step: 0 } };
|
|
248
|
+
if (typeof source.rule === "string") return refitScale(seed, source.rule, amounts);
|
|
249
|
+
const { rule } = source;
|
|
250
|
+
if (rule.kind !== "factors") return { ...seed, rule };
|
|
251
|
+
let at = 0;
|
|
252
|
+
rule.factors.forEach((f3, i) => {
|
|
253
|
+
if (Math.abs(f3 - 1) < Math.abs((rule.factors[at] ?? 1) - 1)) at = i;
|
|
254
|
+
});
|
|
255
|
+
const base = (amounts[at] ?? 0) / (rule.factors[at] ?? 1);
|
|
256
|
+
return { ...seed, base: Math.round(base * 100) / 100, rule };
|
|
257
|
+
}
|
|
159
258
|
function groupOf(name) {
|
|
160
259
|
const segments = name.replace(/^--/, "").split("-");
|
|
161
260
|
return segments.length > 1 ? segments.slice(0, -1).join("-") : name.replace(/^--/, "");
|
|
162
261
|
}
|
|
163
|
-
|
|
262
|
+
var NO_GLOBALS = {};
|
|
263
|
+
function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
164
264
|
const trialId = useTrialId();
|
|
165
265
|
const frame = useTrialFrame(trialId);
|
|
166
266
|
const [overrides, setOverrides] = useCssOverrides();
|
|
@@ -169,6 +269,7 @@ function CssVarsPanel() {
|
|
|
169
269
|
{},
|
|
170
270
|
{ scope: "lab" }
|
|
171
271
|
);
|
|
272
|
+
const [edited, setEdited] = usePersistedState("fg-css-vars-scales", {}, { scope: "lab" });
|
|
172
273
|
const [tab, setTab] = useState("theme");
|
|
173
274
|
const [filter, setFilter] = useState("");
|
|
174
275
|
const tokens = useMemo(() => {
|
|
@@ -192,6 +293,18 @@ function CssVarsPanel() {
|
|
|
192
293
|
const override = overrides[token.name];
|
|
193
294
|
return override === void 0 ? token : { ...token, value: override, overridden: true };
|
|
194
295
|
}).filter((token) => !query || token.name.toLowerCase().includes(query) || token.value.toLowerCase().includes(query));
|
|
296
|
+
const scales = useMemo(() => {
|
|
297
|
+
if (tab !== "theme") return void 0;
|
|
298
|
+
const values = new Map(shown.map((t) => [t.name, t.value]));
|
|
299
|
+
const out = {};
|
|
300
|
+
for (const source of SCALE_SOURCES) {
|
|
301
|
+
const amounts = source.tokens.map((name) => Number.parseFloat(values.get(name) ?? ""));
|
|
302
|
+
if (amounts.some(Number.isNaN)) continue;
|
|
303
|
+
const own = source.tokens.some((name) => overrides[name] !== void 0) ? edited[source.group] : void 0;
|
|
304
|
+
out[source.group] = own ?? standingScale(source, amounts);
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}, [tab, shown, overrides, edited]);
|
|
195
308
|
const send = frame.send;
|
|
196
309
|
const write = useCallback(
|
|
197
310
|
(name, value) => {
|
|
@@ -203,6 +316,68 @@ function CssVarsPanel() {
|
|
|
203
316
|
},
|
|
204
317
|
[setOverrides, send]
|
|
205
318
|
);
|
|
319
|
+
const onScaleChange = useCallback(
|
|
320
|
+
(group, next) => {
|
|
321
|
+
const source = SCALE_SOURCES.find((sc) => sc.group === group);
|
|
322
|
+
if (!source) return;
|
|
323
|
+
setEdited((prev) => ({ ...prev, [group]: next }));
|
|
324
|
+
for (const [name, value] of scaleValues(source, next)) write(name, value);
|
|
325
|
+
},
|
|
326
|
+
[setEdited, write]
|
|
327
|
+
);
|
|
328
|
+
const unsaved = useMemo(
|
|
329
|
+
() => SCALE_SOURCES.flatMap((source) => {
|
|
330
|
+
const edit = edited[source.group];
|
|
331
|
+
if (!edit || !source.tokens.some((name) => overrides[name] !== void 0)) return [];
|
|
332
|
+
const values = scaleValues(source, edit);
|
|
333
|
+
return values.every(([name, value]) => overrides[name] === value) ? [{ source, edit }] : [];
|
|
334
|
+
}),
|
|
335
|
+
[edited, overrides]
|
|
336
|
+
);
|
|
337
|
+
const [saving, setSaving] = useState(false);
|
|
338
|
+
const [held, setHeld] = useState(null);
|
|
339
|
+
const notice = held?.trialId === trialId ? held : null;
|
|
340
|
+
const setNotice = (next) => setHeld(next && { ...next, trialId });
|
|
341
|
+
const save = async () => {
|
|
342
|
+
setSaving(true);
|
|
343
|
+
setNotice(null);
|
|
344
|
+
try {
|
|
345
|
+
const api = themeApi ?? httpThemeApi();
|
|
346
|
+
const stored = await api.get(THEME_NAME);
|
|
347
|
+
const file = `themes/${THEME_NAME}.json`;
|
|
348
|
+
if (JSON.stringify(stored.definition) !== JSON.stringify(THEME_SOURCES[THEME_NAME])) {
|
|
349
|
+
setNotice({ tone: "error", text: `${file} has changed since this page loaded. Reload to edit against it.` });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const edits = Object.fromEntries(
|
|
353
|
+
unsaved.map(({ source, edit }) => [source.name, { base: edit.base, rule: edit.rule }])
|
|
354
|
+
);
|
|
355
|
+
const next = applyScaleEdits(stored.definition, edits, selectionFor(stored.definition.axes ?? {}, globals));
|
|
356
|
+
if (!next.ok) {
|
|
357
|
+
setNotice({ tone: "error", text: next.message });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const result = await api.put(THEME_NAME, next.definition, stored.hash);
|
|
361
|
+
if (result.status === "conflict") {
|
|
362
|
+
setNotice({ tone: "error", text: `${file} changed on disk while saving. Nothing was written; reload to edit against it.` });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (result.status === "invalid") {
|
|
366
|
+
setNotice({ tone: "error", text: result.message });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
for (const { source } of unsaved) for (const name of source.tokens) write(name, null);
|
|
370
|
+
const groups = new Set(unsaved.map(({ source }) => source.group));
|
|
371
|
+
setEdited((prev) => Object.fromEntries(Object.entries(prev).filter(([group]) => !groups.has(group))));
|
|
372
|
+
setNotice(
|
|
373
|
+
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("; ")}` }
|
|
374
|
+
);
|
|
375
|
+
} catch (e) {
|
|
376
|
+
setNotice({ tone: "error", text: `Saving needs forge's dev server, which did not answer: ${e.message}` });
|
|
377
|
+
} finally {
|
|
378
|
+
setSaving(false);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
206
381
|
const onCollapsedChange = useCallback(
|
|
207
382
|
(category, collapsed) => setFolds((prev) => ({ ...prev, [category]: collapsed })),
|
|
208
383
|
[setFolds]
|
|
@@ -210,7 +385,7 @@ function CssVarsPanel() {
|
|
|
210
385
|
return /* @__PURE__ */ jsxs("div", { className: "fg-css-vars", children: [
|
|
211
386
|
/* @__PURE__ */ jsxs("div", { className: "fg-css-vars__controls", children: [
|
|
212
387
|
/* @__PURE__ */ jsx(
|
|
213
|
-
ToggleBar,
|
|
388
|
+
ToggleBar$1,
|
|
214
389
|
{
|
|
215
390
|
ariaLabel: "Variables",
|
|
216
391
|
variant: "flat",
|
|
@@ -221,19 +396,42 @@ function CssVarsPanel() {
|
|
|
221
396
|
}
|
|
222
397
|
}
|
|
223
398
|
),
|
|
224
|
-
/* @__PURE__ */ jsx(Input, { "aria-label": "Filter", placeholder: "Filter by name or value", value: filter, onChange: setFilter })
|
|
399
|
+
/* @__PURE__ */ jsx(Input, { "aria-label": "Filter", placeholder: "Filter by name or value", value: filter, onChange: setFilter }),
|
|
400
|
+
tab === "theme" && (unsaved.length > 0 || notice) ? /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__save", children: [
|
|
401
|
+
unsaved.length > 0 ? /* @__PURE__ */ jsx(Button$1, { variant: "primary", size: "sm", loading: saving, disabled: saving, onClick: () => void save(), children: "Save scales to theme" }) : null,
|
|
402
|
+
notice ? /* @__PURE__ */ jsx(
|
|
403
|
+
"p",
|
|
404
|
+
{
|
|
405
|
+
className: `fg-css-vars__notice fg-css-vars__notice--${notice.tone}`,
|
|
406
|
+
role: notice.tone === "error" ? "alert" : "status",
|
|
407
|
+
children: notice.text
|
|
408
|
+
}
|
|
409
|
+
) : null
|
|
410
|
+
] }) : null
|
|
225
411
|
] }),
|
|
226
|
-
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(
|
|
412
|
+
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(
|
|
413
|
+
TokenPanel,
|
|
414
|
+
{
|
|
415
|
+
tokens: shown,
|
|
416
|
+
onChange: write,
|
|
417
|
+
scales,
|
|
418
|
+
onScaleChange,
|
|
419
|
+
collapsed: folds,
|
|
420
|
+
onCollapsedChange
|
|
421
|
+
}
|
|
422
|
+
)
|
|
227
423
|
] });
|
|
228
424
|
}
|
|
229
425
|
function FocusedCssVars() {
|
|
230
426
|
const { focusedTrialId, trials, instruments } = useLabContext();
|
|
427
|
+
const labGlobals2 = useContext(StoryGlobalsContext);
|
|
231
428
|
const record = trials.find((trial) => trial.id === focusedTrialId);
|
|
232
429
|
if (!record) return null;
|
|
430
|
+
const pins = record.config?.[GLOBALS_KEY];
|
|
233
431
|
const instrument = instruments.find((i) => i.name === record.instrumentName);
|
|
234
432
|
return /* @__PURE__ */ jsxs("section", { className: "fg-css-vars-focus", "aria-label": "CSS Vars", children: [
|
|
235
433
|
/* @__PURE__ */ jsx("p", { className: "fg-css-vars__trial", children: record.title ?? instrument?.title ?? record.instrumentName }),
|
|
236
|
-
/* @__PURE__ */ jsx(TrialIdContext.Provider, { value: record.id, children: /* @__PURE__ */ jsx(CssVarsPanel, {}) })
|
|
434
|
+
/* @__PURE__ */ jsx(TrialIdContext.Provider, { value: record.id, children: /* @__PURE__ */ jsx(CssVarsPanel, { globals: effectiveGlobals(labGlobals2, pins) }) })
|
|
237
435
|
] });
|
|
238
436
|
}
|
|
239
437
|
var CSS_VARS_SECTION = {
|
|
@@ -241,45 +439,294 @@ var CSS_VARS_SECTION = {
|
|
|
241
439
|
region: "aside",
|
|
242
440
|
item: { title: "CSS Vars", body: /* @__PURE__ */ jsx(FocusedCssVars, {}) }
|
|
243
441
|
};
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
442
|
+
|
|
443
|
+
// src/shell/breadcrumb.ts
|
|
444
|
+
function breadcrumb(title, name) {
|
|
445
|
+
const segments = [...title.split("/"), name].filter((segment, i) => i === 0 || segment.toLowerCase() !== "index");
|
|
446
|
+
return segments.join(" > ");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/shell/tree/buildComponents.ts
|
|
450
|
+
var LIBRARIES = [
|
|
451
|
+
["/packages/ui/", "ui"],
|
|
452
|
+
["/packages/labkit/", "labkit"],
|
|
453
|
+
["/packages/forge/", "forge"],
|
|
454
|
+
["/apps/draw/", "draw"],
|
|
455
|
+
["/apps/forge/", "forge"]
|
|
456
|
+
];
|
|
457
|
+
function libraryOf(entry) {
|
|
458
|
+
const file = entry.file.replaceAll("\\", "/");
|
|
459
|
+
for (const [marker, label] of LIBRARIES) {
|
|
460
|
+
if (file.includes(marker)) return label;
|
|
461
|
+
}
|
|
462
|
+
return entry.title.split("/")[0];
|
|
463
|
+
}
|
|
464
|
+
function buildComponents(index) {
|
|
465
|
+
const byTitle = /* @__PURE__ */ new Map();
|
|
466
|
+
for (const entry of index) {
|
|
467
|
+
let row = byTitle.get(entry.title);
|
|
468
|
+
if (!row) {
|
|
469
|
+
const segments = entry.title.split("/");
|
|
470
|
+
row = {
|
|
471
|
+
path: entry.title,
|
|
472
|
+
label: segments[segments.length - 1],
|
|
473
|
+
library: libraryOf(entry),
|
|
474
|
+
index: indexEntries([entry])[0],
|
|
475
|
+
entries: []
|
|
476
|
+
};
|
|
477
|
+
byTitle.set(entry.title, row);
|
|
478
|
+
}
|
|
479
|
+
row.entries.push(entry);
|
|
480
|
+
}
|
|
481
|
+
const rows = [...byTitle.values()];
|
|
482
|
+
disambiguate(rows);
|
|
483
|
+
return rows.sort(
|
|
484
|
+
(a, b) => a.label.localeCompare(b.label) || a.library.localeCompare(b.library) || a.path.localeCompare(b.path)
|
|
255
485
|
);
|
|
256
486
|
}
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
487
|
+
function disambiguate(rows) {
|
|
488
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
489
|
+
for (const row of rows) {
|
|
490
|
+
const key = `${row.library}\0${row.label}`;
|
|
491
|
+
const group = byLabel.get(key);
|
|
492
|
+
if (group) group.push(row);
|
|
493
|
+
else byLabel.set(key, [row]);
|
|
494
|
+
}
|
|
495
|
+
for (const group of byLabel.values()) {
|
|
496
|
+
if (group.length < 2) continue;
|
|
497
|
+
for (const row of group) {
|
|
498
|
+
const segments = row.path.split("/");
|
|
499
|
+
for (let take = 2; take <= segments.length; take++) {
|
|
500
|
+
row.label = segments.slice(-take).join("/");
|
|
501
|
+
if (group.every((other) => other === row || !other.path.endsWith(`/${row.label}`))) break;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
268
504
|
}
|
|
269
|
-
return rest;
|
|
270
505
|
}
|
|
271
|
-
function
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
506
|
+
function filterComponents(rows, query) {
|
|
507
|
+
const needle = query.trim().toLowerCase();
|
|
508
|
+
if (!needle) return [...rows];
|
|
509
|
+
const out = [];
|
|
510
|
+
for (const row of rows) {
|
|
511
|
+
if (`${row.path}/${row.library}`.toLowerCase().includes(needle)) {
|
|
512
|
+
out.push(row);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const entries = row.entries.filter((entry) => entry.name.toLowerCase().includes(needle));
|
|
516
|
+
if (entries.length > 0) out.push({ path: row.path, label: row.label, library: row.library, entries });
|
|
517
|
+
}
|
|
518
|
+
return out;
|
|
519
|
+
}
|
|
520
|
+
function componentNodes(rows) {
|
|
521
|
+
const root = [];
|
|
522
|
+
const folders = /* @__PURE__ */ new Map();
|
|
523
|
+
for (const row of rows) {
|
|
524
|
+
const labels = row.label.split("/");
|
|
525
|
+
const title = row.path.split("/");
|
|
526
|
+
let siblings = root;
|
|
527
|
+
let tag = row.library;
|
|
528
|
+
for (let i = 0; i < labels.length - 1; i++) {
|
|
529
|
+
const path = title.slice(0, title.length - labels.length + i + 1).join("/");
|
|
530
|
+
let folder = folders.get(path);
|
|
531
|
+
if (!folder) {
|
|
532
|
+
folder = { kind: "folder", label: labels[i], path, tag, children: [] };
|
|
533
|
+
folders.set(path, folder);
|
|
534
|
+
siblings.push(folder);
|
|
535
|
+
}
|
|
536
|
+
siblings = folder.children;
|
|
537
|
+
tag = void 0;
|
|
538
|
+
}
|
|
539
|
+
siblings.push({
|
|
540
|
+
kind: "folder",
|
|
541
|
+
label: labels[labels.length - 1],
|
|
542
|
+
path: row.path,
|
|
543
|
+
tag,
|
|
544
|
+
...row.index ? { index: row.index } : {},
|
|
545
|
+
children: row.entries.map((entry) => ({ kind: "story", entry }))
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
return root;
|
|
549
|
+
}
|
|
550
|
+
function librariesIn(index) {
|
|
551
|
+
return [...new Set(index.map(libraryOf))].sort((a, b) => a.localeCompare(b));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/shell/info/dossier.ts
|
|
555
|
+
var GLOBALS = "$globals";
|
|
556
|
+
function leaves(shape, prefix = "") {
|
|
557
|
+
const out = [];
|
|
558
|
+
for (const [key, entry] of Object.entries(shape)) {
|
|
559
|
+
if (prefix === "" && key === GLOBALS) continue;
|
|
560
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
561
|
+
if (isConfigBranch(entry)) out.push(...leaves(entry.children, path));
|
|
562
|
+
else if (!entry.annotations.hidden) out.push({ path, node: entry });
|
|
563
|
+
}
|
|
564
|
+
return out;
|
|
565
|
+
}
|
|
566
|
+
function storyDossier(input) {
|
|
567
|
+
const { entry, instrument, config, ready = true } = input;
|
|
568
|
+
const schema = ready ? instrument?.config : void 0;
|
|
569
|
+
const args = schema ? leaves(schema.nodes).map(({ path, node }) => {
|
|
570
|
+
const value = valueAtPath(config, path);
|
|
571
|
+
return {
|
|
572
|
+
path,
|
|
573
|
+
label: node.annotations.name ?? path.split(".").pop() ?? path,
|
|
574
|
+
kind: node.kind,
|
|
575
|
+
default: node.default,
|
|
576
|
+
value: value === void 0 ? node.default : value,
|
|
577
|
+
...node.annotations.description === void 0 ? {} : { description: node.annotations.description }
|
|
578
|
+
};
|
|
579
|
+
}) : [];
|
|
580
|
+
return {
|
|
581
|
+
id: entry.id,
|
|
582
|
+
title: entry.title,
|
|
583
|
+
name: entry.name,
|
|
584
|
+
exportName: entry.exportName,
|
|
585
|
+
file: entry.file,
|
|
586
|
+
library: libraryOf(entry),
|
|
587
|
+
...entry.componentName === void 0 ? {} : { componentName: entry.componentName },
|
|
588
|
+
...entry.description === void 0 ? {} : { description: entry.description },
|
|
589
|
+
...entry.componentDescription === void 0 ? {} : { componentDescription: entry.componentDescription },
|
|
590
|
+
args,
|
|
591
|
+
argsPending: !schema
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function printed(value) {
|
|
595
|
+
if (value === void 0 || value === null) return "\u2014";
|
|
596
|
+
if (typeof value === "string") return value === "" ? '""' : value;
|
|
597
|
+
if (typeof value === "function") return "\u0192";
|
|
598
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
599
|
+
return String(value);
|
|
600
|
+
}
|
|
601
|
+
function Row({ label, children }) {
|
|
602
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-info__row", children: [
|
|
603
|
+
/* @__PURE__ */ jsx("dt", { className: "fg-info__key", children: label }),
|
|
604
|
+
/* @__PURE__ */ jsx("dd", { className: "fg-info__value", children })
|
|
605
|
+
] });
|
|
606
|
+
}
|
|
607
|
+
function ArgRows({ args }) {
|
|
608
|
+
return /* @__PURE__ */ jsxs("table", { className: "fg-info__args", children: [
|
|
609
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
610
|
+
/* @__PURE__ */ jsx("th", { scope: "col", children: "Name" }),
|
|
611
|
+
/* @__PURE__ */ jsx("th", { scope: "col", children: "Kind" }),
|
|
612
|
+
/* @__PURE__ */ jsx("th", { scope: "col", children: "Value" }),
|
|
613
|
+
/* @__PURE__ */ jsx("th", { scope: "col", children: "Default" })
|
|
614
|
+
] }) }),
|
|
615
|
+
/* @__PURE__ */ jsx("tbody", { children: args.map((arg) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
616
|
+
/* @__PURE__ */ jsxs("th", { scope: "row", children: [
|
|
617
|
+
/* @__PURE__ */ jsx("span", { className: "fg-info__arg-name", children: arg.label }),
|
|
618
|
+
arg.description ? /* @__PURE__ */ jsx("span", { className: "fg-info__arg-doc", children: arg.description }) : null
|
|
619
|
+
] }),
|
|
620
|
+
/* @__PURE__ */ jsx("td", { className: "fg-info__arg-kind", children: arg.kind ?? "\u2014" }),
|
|
621
|
+
/* @__PURE__ */ jsx("td", { className: "fg-info__value-cell", children: printed(arg.value) }),
|
|
622
|
+
/* @__PURE__ */ jsx("td", { className: "fg-info__value-cell", children: printed(arg.default) })
|
|
623
|
+
] }, arg.path)) })
|
|
624
|
+
] });
|
|
625
|
+
}
|
|
626
|
+
function StoryDossier({ dossier }) {
|
|
627
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-info", children: [
|
|
628
|
+
dossier.description || dossier.componentDescription ? /* @__PURE__ */ jsxs("section", { className: "fg-info__section", "aria-label": "Docs", children: [
|
|
629
|
+
dossier.description ? /* @__PURE__ */ jsx("p", { className: "fg-info__doc", children: dossier.description }) : null,
|
|
630
|
+
dossier.componentDescription ? /* @__PURE__ */ jsx("p", { className: "fg-info__doc fg-info__doc--component", children: dossier.componentDescription }) : null
|
|
631
|
+
] }) : null,
|
|
632
|
+
/* @__PURE__ */ jsx("section", { className: "fg-info__section", "aria-label": "Identity", children: /* @__PURE__ */ jsxs("dl", { className: "fg-info__rows", children: [
|
|
633
|
+
/* @__PURE__ */ jsx(Row, { label: "Story", children: dossier.name }),
|
|
634
|
+
/* @__PURE__ */ jsx(Row, { label: "Component", children: dossier.componentName ?? dossier.title.split("/").pop() }),
|
|
635
|
+
/* @__PURE__ */ jsx(Row, { label: "Library", children: dossier.library }),
|
|
636
|
+
/* @__PURE__ */ jsx(Row, { label: "Title", children: dossier.title }),
|
|
637
|
+
/* @__PURE__ */ jsx(Row, { label: "Export", children: /* @__PURE__ */ jsx("code", { children: dossier.exportName }) }),
|
|
638
|
+
/* @__PURE__ */ jsx(Row, { label: "Id", children: /* @__PURE__ */ jsx("code", { children: dossier.id }) }),
|
|
639
|
+
/* @__PURE__ */ jsx(Row, { label: "File", children: /* @__PURE__ */ jsx("code", { className: "fg-info__path", children: dossier.file }) })
|
|
640
|
+
] }) }),
|
|
641
|
+
/* @__PURE__ */ jsxs("section", { className: "fg-info__section", "aria-label": "Args", children: [
|
|
642
|
+
/* @__PURE__ */ jsx("h3", { className: "fg-info__heading", children: "Args" }),
|
|
643
|
+
dossier.argsPending ? /* @__PURE__ */ jsx("p", { className: "fg-info__empty", children: "Open this story to read its args." }) : dossier.args.length === 0 ? /* @__PURE__ */ jsx("p", { className: "fg-info__empty", children: "This story takes no args." }) : /* @__PURE__ */ jsx(ArgRows, { args: dossier.args })
|
|
644
|
+
] })
|
|
645
|
+
] });
|
|
646
|
+
}
|
|
647
|
+
function StoryInfoDialog({ index, isReady, isOpen, onOpenChange }) {
|
|
648
|
+
const { focusedTrialId, trials, instruments } = useLabContext();
|
|
649
|
+
const trial = trials.find((record) => record.id === focusedTrialId);
|
|
650
|
+
const entry = trial ? index.find((candidate) => candidate.id === trial.instrumentName) : void 0;
|
|
651
|
+
if (!entry) return null;
|
|
652
|
+
const dossier = storyDossier({
|
|
653
|
+
entry,
|
|
654
|
+
instrument: instruments.find((candidate) => candidate.name === entry.id),
|
|
655
|
+
config: trial?.config,
|
|
656
|
+
ready: isReady(entry.id)
|
|
657
|
+
});
|
|
658
|
+
return /* @__PURE__ */ jsx(
|
|
659
|
+
Dialog,
|
|
660
|
+
{
|
|
661
|
+
isOpen,
|
|
662
|
+
onOpenChange,
|
|
663
|
+
title: breadcrumb(dossier.title, dossier.name),
|
|
664
|
+
className: "fg-info-dialog",
|
|
665
|
+
children: /* @__PURE__ */ jsx(StoryDossier, { dossier })
|
|
666
|
+
}
|
|
279
667
|
);
|
|
280
|
-
const group = f.group(leaves).label("").section("Globals", { layout: "inline", pack: "pairs" });
|
|
281
|
-
return f.schema({ ...schema.nodes, [GLOBALS_KEY]: group });
|
|
282
668
|
}
|
|
669
|
+
function canMoveFrames() {
|
|
670
|
+
return typeof Element !== "undefined" && "moveBefore" in Element.prototype;
|
|
671
|
+
}
|
|
672
|
+
function moveFrame(host, frame) {
|
|
673
|
+
host.moveBefore(frame, host.firstChild);
|
|
674
|
+
}
|
|
675
|
+
var whenIdle = (fn) => {
|
|
676
|
+
if (typeof requestIdleCallback === "function") {
|
|
677
|
+
const handle2 = requestIdleCallback(fn, { timeout: 500 });
|
|
678
|
+
return () => cancelIdleCallback(handle2);
|
|
679
|
+
}
|
|
680
|
+
const handle = setTimeout(fn, 200);
|
|
681
|
+
return () => clearTimeout(handle);
|
|
682
|
+
};
|
|
683
|
+
function createFramePool(frameUrl, size = 2) {
|
|
684
|
+
const shelf = document.createElement("div");
|
|
685
|
+
shelf.className = "fg-frame-pool";
|
|
686
|
+
shelf.setAttribute("aria-hidden", "true");
|
|
687
|
+
document.body.append(shelf);
|
|
688
|
+
const warm = [];
|
|
689
|
+
let loading = 0;
|
|
690
|
+
let cancelRefill = null;
|
|
691
|
+
let disposed = false;
|
|
692
|
+
const load = () => {
|
|
693
|
+
loading += 1;
|
|
694
|
+
const frame = document.createElement("iframe");
|
|
695
|
+
frame.tabIndex = -1;
|
|
696
|
+
frame.src = frameUrl;
|
|
697
|
+
const onMessage = (event) => {
|
|
698
|
+
if (event.source !== frame.contentWindow || event.origin !== location.origin) return;
|
|
699
|
+
if (event.data?.type !== FRAME_HELLO) return;
|
|
700
|
+
window.removeEventListener("message", onMessage);
|
|
701
|
+
loading -= 1;
|
|
702
|
+
warm.push(frame);
|
|
703
|
+
};
|
|
704
|
+
window.addEventListener("message", onMessage);
|
|
705
|
+
shelf.append(frame);
|
|
706
|
+
};
|
|
707
|
+
const refill = () => {
|
|
708
|
+
if (disposed || cancelRefill) return;
|
|
709
|
+
cancelRefill = whenIdle(() => {
|
|
710
|
+
cancelRefill = null;
|
|
711
|
+
while (!disposed && warm.length + loading < size) load();
|
|
712
|
+
});
|
|
713
|
+
};
|
|
714
|
+
if (canMoveFrames()) refill();
|
|
715
|
+
return {
|
|
716
|
+
claim() {
|
|
717
|
+
if (!canMoveFrames()) return null;
|
|
718
|
+
const frame = warm.shift() ?? null;
|
|
719
|
+
refill();
|
|
720
|
+
return frame;
|
|
721
|
+
},
|
|
722
|
+
dispose() {
|
|
723
|
+
disposed = true;
|
|
724
|
+
cancelRefill?.();
|
|
725
|
+
shelf.remove();
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
var FramePoolContext = createContext(null);
|
|
283
730
|
var RECORD = "fg-globals";
|
|
284
731
|
function useLabGlobals(declarations) {
|
|
285
732
|
const [stored, setStored] = usePersistedState(RECORD, null, { scope: "lab" });
|
|
@@ -290,19 +737,65 @@ function useLabGlobals(declarations) {
|
|
|
290
737
|
);
|
|
291
738
|
return [values, set];
|
|
292
739
|
}
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
return /* @__PURE__ */ jsx("div", { className: "fg-globals", role: "toolbar", "aria-label": "Globals", children: Object.entries(declarations).map(([key, declaration]) => /* @__PURE__ */ jsx(
|
|
740
|
+
function GlobalSelect({ name, declaration, values, set, labeled = false }) {
|
|
741
|
+
return /* @__PURE__ */ jsx(
|
|
296
742
|
Select,
|
|
297
743
|
{
|
|
298
|
-
"aria-label": declaration.label,
|
|
299
|
-
width: "fit",
|
|
300
|
-
selectedKey: String(values[
|
|
301
|
-
onSelectionChange: (value) => set(
|
|
744
|
+
...labeled ? { label: declaration.label } : { "aria-label": declaration.label },
|
|
745
|
+
width: labeled ? "fill" : "fit",
|
|
746
|
+
selectedKey: String(values[name]),
|
|
747
|
+
onSelectionChange: (value) => set(name, value),
|
|
302
748
|
options: declaration.options
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
749
|
+
}
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
function GlobalsToolbar({ declarations }) {
|
|
753
|
+
const [values, set] = useLabGlobals(declarations);
|
|
754
|
+
const entries = Object.entries(declarations);
|
|
755
|
+
return /* @__PURE__ */ jsx("div", { className: "fg-globals", role: "toolbar", "aria-label": "Globals", children: entries.filter(([, declaration]) => declaration.under === void 0).map(([key, declaration]) => {
|
|
756
|
+
const nested = entries.filter(([, other]) => other.under === key);
|
|
757
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-globals__item", children: [
|
|
758
|
+
/* @__PURE__ */ jsx(GlobalSelect, { name: key, declaration, values, set }),
|
|
759
|
+
nested.length > 0 ? /* @__PURE__ */ jsx(NestedGlobals, { label: declaration.label, nested, values, set }) : null
|
|
760
|
+
] }, key);
|
|
761
|
+
}) });
|
|
762
|
+
}
|
|
763
|
+
function NestedGlobals({
|
|
764
|
+
label,
|
|
765
|
+
nested,
|
|
766
|
+
values,
|
|
767
|
+
set
|
|
768
|
+
}) {
|
|
769
|
+
const anchor = useRef(null);
|
|
770
|
+
const [open, setOpen] = useState(false);
|
|
771
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
772
|
+
/* @__PURE__ */ jsx(
|
|
773
|
+
Button$1,
|
|
774
|
+
{
|
|
775
|
+
ref: anchor,
|
|
776
|
+
variant: "ghost",
|
|
777
|
+
size: "sm",
|
|
778
|
+
iconOnly: true,
|
|
779
|
+
ariaLabel: `More ${label} settings`,
|
|
780
|
+
pressed: open,
|
|
781
|
+
onClick: () => setOpen((was) => !was),
|
|
782
|
+
children: /* @__PURE__ */ jsx(TuneIcon, { size: 16 })
|
|
783
|
+
}
|
|
784
|
+
),
|
|
785
|
+
/* @__PURE__ */ jsx(
|
|
786
|
+
Callout,
|
|
787
|
+
{
|
|
788
|
+
triggerRef: anchor,
|
|
789
|
+
isOpen: open,
|
|
790
|
+
onOpenChange: setOpen,
|
|
791
|
+
placement: "bottom end",
|
|
792
|
+
showCloseButton: false,
|
|
793
|
+
title: label,
|
|
794
|
+
className: "fg-globals__popover",
|
|
795
|
+
children: nested.map(([key, declaration]) => /* @__PURE__ */ jsx(GlobalSelect, { name: key, declaration, values, set, labeled: true }, key))
|
|
796
|
+
}
|
|
797
|
+
)
|
|
798
|
+
] });
|
|
306
799
|
}
|
|
307
800
|
function LabGlobals({
|
|
308
801
|
declarations,
|
|
@@ -312,7 +805,6 @@ function LabGlobals({
|
|
|
312
805
|
useEffect(() => onChange(values), [values, onChange]);
|
|
313
806
|
return null;
|
|
314
807
|
}
|
|
315
|
-
var StoryGlobalsContext = createContext({});
|
|
316
808
|
|
|
317
809
|
// src/shell/revealTrial.ts
|
|
318
810
|
var FLASH_MS = 600;
|
|
@@ -339,33 +831,51 @@ function readRoute() {
|
|
|
339
831
|
if (!hash.startsWith("#/")) return null;
|
|
340
832
|
return decodeURIComponent(hash.slice(2)) || null;
|
|
341
833
|
}
|
|
834
|
+
function readRouteEntry() {
|
|
835
|
+
const state = history.state;
|
|
836
|
+
const entry = state?.forgeRoute;
|
|
837
|
+
return typeof entry?.step === "number" ? entry : null;
|
|
838
|
+
}
|
|
839
|
+
function crossesInPlace(from, to) {
|
|
840
|
+
if (!from || !to || from.step === to.step) return false;
|
|
841
|
+
return to.step < from.step ? from.inPlace : to.inPlace;
|
|
842
|
+
}
|
|
843
|
+
function stamp(entry, hash) {
|
|
844
|
+
history.replaceState({ ...history.state ?? {}, forgeRoute: entry }, "", hash);
|
|
845
|
+
}
|
|
846
|
+
function setRoute(id, options = {}) {
|
|
847
|
+
const hash = `#/${encodeURIComponent(id)}`;
|
|
848
|
+
if (readRoute() === id) {
|
|
849
|
+
history.replaceState(history.state, "", hash);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const from = readRouteEntry() ?? { step: 0, inPlace: false };
|
|
853
|
+
stamp(from, location.hash);
|
|
854
|
+
location.hash = hash;
|
|
855
|
+
stamp({ step: from.step + 1, inPlace: options.inPlace ?? false }, hash);
|
|
856
|
+
}
|
|
342
857
|
function useRoute() {
|
|
343
|
-
const [route,
|
|
858
|
+
const [route, setCurrent] = useState(readRoute);
|
|
344
859
|
useEffect(() => {
|
|
345
|
-
const sync = () =>
|
|
860
|
+
const sync = () => setCurrent(readRoute());
|
|
346
861
|
window.addEventListener("hashchange", sync);
|
|
347
862
|
sync();
|
|
348
863
|
return () => window.removeEventListener("hashchange", sync);
|
|
349
864
|
}, []);
|
|
350
|
-
|
|
351
|
-
const hash = `#/${encodeURIComponent(id)}`;
|
|
352
|
-
if (readRoute() === id) history.replaceState(null, "", hash);
|
|
353
|
-
else location.hash = hash;
|
|
354
|
-
}, []);
|
|
355
|
-
return [route, go];
|
|
865
|
+
return [route, setRoute];
|
|
356
866
|
}
|
|
357
867
|
|
|
358
868
|
// src/shell/tree/buildTree.ts
|
|
359
869
|
function sortFolder(nodes) {
|
|
360
870
|
const folders = nodes.filter((n) => n.kind === "folder");
|
|
361
|
-
const stories = nodes.filter((n) => n.kind === "story");
|
|
362
871
|
folders.sort((a, b) => a.label.localeCompare(b.label));
|
|
363
872
|
for (const folder of folders) folder.children = sortFolder(folder.children);
|
|
364
|
-
return [...folders, ...
|
|
873
|
+
return [...folders, ...nodes.filter((n) => n.kind === "story")];
|
|
365
874
|
}
|
|
366
875
|
function buildTree(index) {
|
|
367
876
|
const root = [];
|
|
368
877
|
const folders = /* @__PURE__ */ new Map();
|
|
878
|
+
const pages = new Map(indexEntries(index).map((page) => [page.title, page]));
|
|
369
879
|
for (const entry of index) {
|
|
370
880
|
let siblings = root;
|
|
371
881
|
let path = "";
|
|
@@ -373,7 +883,8 @@ function buildTree(index) {
|
|
|
373
883
|
path = path ? `${path}/${label}` : label;
|
|
374
884
|
let folder = folders.get(path);
|
|
375
885
|
if (!folder) {
|
|
376
|
-
|
|
886
|
+
const page = pages.get(path);
|
|
887
|
+
folder = { kind: "folder", label, path, children: [], ...page ? { index: page } : {} };
|
|
377
888
|
folders.set(path, folder);
|
|
378
889
|
siblings.push(folder);
|
|
379
890
|
}
|
|
@@ -391,10 +902,16 @@ function filterTree(nodes, query) {
|
|
|
391
902
|
return `${node.entry.title}/${node.entry.name}`.toLowerCase().includes(needle) ? [node] : [];
|
|
392
903
|
}
|
|
393
904
|
const children = walk(node.children);
|
|
394
|
-
|
|
905
|
+
if (children.length === 0) return [];
|
|
906
|
+
const { index: page, ...folder } = node;
|
|
907
|
+
return [page && node.path.toLowerCase().includes(needle) ? { ...folder, children, index: page } : { ...folder, children }];
|
|
395
908
|
});
|
|
396
909
|
return walk(nodes);
|
|
397
910
|
}
|
|
911
|
+
var VIEWS = [
|
|
912
|
+
{ value: "tree", label: "Tree" },
|
|
913
|
+
{ value: "components", label: "Components" }
|
|
914
|
+
];
|
|
398
915
|
var keyOf = (node) => node.kind === "folder" ? `folder:${node.path}` : `story:${node.entry.id}`;
|
|
399
916
|
function ancestorsOf(entry) {
|
|
400
917
|
const paths = /* @__PURE__ */ new Set();
|
|
@@ -406,16 +923,34 @@ function ancestorsOf(entry) {
|
|
|
406
923
|
}
|
|
407
924
|
return paths;
|
|
408
925
|
}
|
|
926
|
+
var LIBRARY_TONES = {
|
|
927
|
+
ui: "color-mix(in oklab, var(--wzl-swatch-blue), var(--wzl-fg) 35%)",
|
|
928
|
+
labkit: "color-mix(in oklab, var(--wzl-swatch-teal), var(--wzl-fg) 45%)",
|
|
929
|
+
forge: "color-mix(in oklab, var(--wzl-swatch-amber), var(--wzl-fg) 40%)",
|
|
930
|
+
draw: "color-mix(in oklab, var(--wzl-swatch-rose), var(--wzl-fg) 35%)"
|
|
931
|
+
};
|
|
932
|
+
function LibraryBadge({ library }) {
|
|
933
|
+
return /* @__PURE__ */ jsx(Badge, { status: "muted", tone: LIBRARY_TONES[library], variant: "subtle", size: "sm", className: "fg-tree__tag", children: library });
|
|
934
|
+
}
|
|
409
935
|
function StoryTree({ ctx, index }) {
|
|
410
936
|
const headingId = useId();
|
|
411
|
-
const [route,
|
|
937
|
+
const [route, setRoute2] = useRoute();
|
|
412
938
|
const [query, setQuery] = useState("");
|
|
413
939
|
const [folds, setFolds] = usePersistedState("fg-tree-open", {}, { scope: "lab" });
|
|
940
|
+
const [view, setView] = usePersistedState("fg-tree-view", "components", { scope: "lab" });
|
|
941
|
+
const [hidden, setHidden] = usePersistedState("fg-tree-libraries", {}, { scope: "lab" });
|
|
414
942
|
const [focusKey, setFocusKey] = useState(null);
|
|
415
943
|
const items = useRef(/* @__PURE__ */ new Map());
|
|
416
|
-
const
|
|
417
|
-
const
|
|
418
|
-
const
|
|
944
|
+
const libraries = useMemo(() => librariesIn(index), [index]);
|
|
945
|
+
const kept = useMemo(() => index.filter((entry) => !hidden[libraryOf(entry)]), [index, hidden]);
|
|
946
|
+
const tree = useMemo(() => buildTree(kept), [kept]);
|
|
947
|
+
const components = useMemo(() => buildComponents(kept), [kept]);
|
|
948
|
+
const shown = useMemo(
|
|
949
|
+
() => view === "components" ? componentNodes(filterComponents(components, query)) : filterTree(tree, query),
|
|
950
|
+
[view, components, tree, query]
|
|
951
|
+
);
|
|
952
|
+
const routable = useMemo(() => [...kept, ...indexEntries(kept)], [kept]);
|
|
953
|
+
const routed = routable.find((entry) => entry.id === route);
|
|
419
954
|
const routeAncestors = useMemo(() => ancestorsOf(routed), [routed]);
|
|
420
955
|
const filtering = query.trim() !== "";
|
|
421
956
|
const isOpen = (path) => filtering || (folds[path] ?? routeAncestors.has(path));
|
|
@@ -441,8 +976,22 @@ function StoryTree({ ctx, index }) {
|
|
|
441
976
|
const focused = ctx.trials.find((trial) => trial.id === ctx.focusedTrialId);
|
|
442
977
|
if (another || !focused) ctx.addTrial(entry.id);
|
|
443
978
|
else if (focused.instrumentName === entry.id) revealTrial(focused.id);
|
|
444
|
-
else
|
|
445
|
-
|
|
979
|
+
else {
|
|
980
|
+
ctx.swapTrial(focused.id, entry.id);
|
|
981
|
+
setRoute2(entry.id, { inPlace: true });
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
setRoute2(entry.id);
|
|
985
|
+
};
|
|
986
|
+
const openFolder = (node, another) => {
|
|
987
|
+
if (!node.index) {
|
|
988
|
+
setOpen(node.path, !isOpen(node.path));
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (folds[node.path] === false) {
|
|
992
|
+
setFolds(({ [node.path]: _dropped, ...rest }) => rest);
|
|
993
|
+
}
|
|
994
|
+
activate(node.index, another);
|
|
446
995
|
};
|
|
447
996
|
const onKeyDown = (event) => {
|
|
448
997
|
const at = rows.findIndex((row2) => row2.key === active);
|
|
@@ -473,8 +1022,8 @@ function StoryTree({ ctx, index }) {
|
|
|
473
1022
|
break;
|
|
474
1023
|
case "Enter":
|
|
475
1024
|
case " ":
|
|
476
|
-
if (node.kind === "
|
|
477
|
-
else
|
|
1025
|
+
if (node.kind === "story") activate(node.entry, event.shiftKey);
|
|
1026
|
+
else openFolder(node, event.shiftKey);
|
|
478
1027
|
break;
|
|
479
1028
|
default:
|
|
480
1029
|
return;
|
|
@@ -506,9 +1055,35 @@ function StoryTree({ ctx, index }) {
|
|
|
506
1055
|
...itemProps(node, level),
|
|
507
1056
|
"aria-expanded": open,
|
|
508
1057
|
"aria-label": node.label,
|
|
1058
|
+
"aria-current": node.index && openIds.has(node.index.id) ? "true" : void 0,
|
|
509
1059
|
className: "fg-tree__node",
|
|
510
1060
|
children: [
|
|
511
|
-
/* @__PURE__ */
|
|
1061
|
+
/* @__PURE__ */ jsxs(
|
|
1062
|
+
"div",
|
|
1063
|
+
{
|
|
1064
|
+
className: "fg-tree__item fg-tree__folder",
|
|
1065
|
+
onClick: (event) => {
|
|
1066
|
+
setFocusKey(keyOf(node));
|
|
1067
|
+
openFolder(node, event.shiftKey);
|
|
1068
|
+
},
|
|
1069
|
+
children: [
|
|
1070
|
+
/* @__PURE__ */ jsx(
|
|
1071
|
+
"span",
|
|
1072
|
+
{
|
|
1073
|
+
"aria-hidden": "true",
|
|
1074
|
+
className: "fg-tree__fold",
|
|
1075
|
+
onClick: (event) => {
|
|
1076
|
+
event.stopPropagation();
|
|
1077
|
+
setOpen(node.path, !open);
|
|
1078
|
+
},
|
|
1079
|
+
children: /* @__PURE__ */ jsx(DisclosureMark, { open })
|
|
1080
|
+
}
|
|
1081
|
+
),
|
|
1082
|
+
/* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: node.label }),
|
|
1083
|
+
node.tag ? /* @__PURE__ */ jsx(LibraryBadge, { library: node.tag }) : null
|
|
1084
|
+
]
|
|
1085
|
+
}
|
|
1086
|
+
),
|
|
512
1087
|
open ? /* @__PURE__ */ jsx("div", { role: "group", className: "fg-tree__group", children: renderNodes(node.children, level + 1) }) : null
|
|
513
1088
|
]
|
|
514
1089
|
},
|
|
@@ -516,7 +1091,7 @@ function StoryTree({ ctx, index }) {
|
|
|
516
1091
|
);
|
|
517
1092
|
}
|
|
518
1093
|
const { entry } = node;
|
|
519
|
-
return /* @__PURE__ */
|
|
1094
|
+
return /* @__PURE__ */ jsxs(
|
|
520
1095
|
"a",
|
|
521
1096
|
{
|
|
522
1097
|
...itemProps(node, level),
|
|
@@ -529,7 +1104,10 @@ function StoryTree({ ctx, index }) {
|
|
|
529
1104
|
setFocusKey(keyOf(node));
|
|
530
1105
|
activate(entry, event.shiftKey);
|
|
531
1106
|
},
|
|
532
|
-
children:
|
|
1107
|
+
children: [
|
|
1108
|
+
/* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: node.label ?? entry.name }),
|
|
1109
|
+
node.tag ? /* @__PURE__ */ jsx(LibraryBadge, { library: node.tag }) : null
|
|
1110
|
+
]
|
|
533
1111
|
},
|
|
534
1112
|
keyOf(node)
|
|
535
1113
|
);
|
|
@@ -540,6 +1118,30 @@ function StoryTree({ ctx, index }) {
|
|
|
540
1118
|
"No story ",
|
|
541
1119
|
/* @__PURE__ */ jsx("code", { children: route })
|
|
542
1120
|
] }) : null,
|
|
1121
|
+
/* @__PURE__ */ jsx(
|
|
1122
|
+
ToggleBar,
|
|
1123
|
+
{
|
|
1124
|
+
ariaLabel: "Sidebar view",
|
|
1125
|
+
variant: "flat",
|
|
1126
|
+
items: VIEWS,
|
|
1127
|
+
value: view,
|
|
1128
|
+
onChange: (next) => {
|
|
1129
|
+
if (next) setView(next);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
),
|
|
1133
|
+
/* @__PURE__ */ jsxs("fieldset", { className: "fg-tree__libraries", children: [
|
|
1134
|
+
/* @__PURE__ */ jsx("legend", { className: "fg-tree__libraries-legend", children: "Packages" }),
|
|
1135
|
+
libraries.map((library) => /* @__PURE__ */ jsx(
|
|
1136
|
+
Checkbox,
|
|
1137
|
+
{
|
|
1138
|
+
isSelected: !hidden[library],
|
|
1139
|
+
onChange: (on) => setHidden((prev) => ({ ...prev, [library]: !on })),
|
|
1140
|
+
children: /* @__PURE__ */ jsx(LibraryBadge, { library })
|
|
1141
|
+
},
|
|
1142
|
+
library
|
|
1143
|
+
))
|
|
1144
|
+
] }),
|
|
543
1145
|
/* @__PURE__ */ jsx(
|
|
544
1146
|
"input",
|
|
545
1147
|
{
|
|
@@ -597,10 +1199,90 @@ function createAnswerBook() {
|
|
|
597
1199
|
}
|
|
598
1200
|
};
|
|
599
1201
|
}
|
|
1202
|
+
function IndexTrial({ bundle, setup, config }) {
|
|
1203
|
+
const lab = useLabContext();
|
|
1204
|
+
const trialId = useContext(TrialIdContext);
|
|
1205
|
+
const open = (id) => {
|
|
1206
|
+
if (!trialId || !lab.instruments.some((instrument) => instrument.name === id)) return;
|
|
1207
|
+
lab.swapTrial(trialId, id);
|
|
1208
|
+
setRoute(id, { inPlace: true });
|
|
1209
|
+
};
|
|
1210
|
+
const render = (globals) => {
|
|
1211
|
+
const env = {
|
|
1212
|
+
title: bundle.title,
|
|
1213
|
+
...bundle.description === void 0 ? {} : { description: bundle.description },
|
|
1214
|
+
stories: bundle.stories,
|
|
1215
|
+
descriptions: bundle.descriptions,
|
|
1216
|
+
globals,
|
|
1217
|
+
decorators: setup.decorators ?? [],
|
|
1218
|
+
open,
|
|
1219
|
+
// A story that throws shows its error in its own cell; the rest of the page is still worth reading.
|
|
1220
|
+
onError: (error) => console.error(error)
|
|
1221
|
+
};
|
|
1222
|
+
return /* @__PURE__ */ jsx(IndexPage, { env, render: bundle.render });
|
|
1223
|
+
};
|
|
1224
|
+
return /* @__PURE__ */ jsx(TrialHost, { layout: "fullscreen", setup, config, children: render });
|
|
1225
|
+
}
|
|
1226
|
+
function annotationsOn(frames) {
|
|
1227
|
+
return {
|
|
1228
|
+
targets: (_state, _config, trial) => {
|
|
1229
|
+
const ref = frames.hostRef(trial.id);
|
|
1230
|
+
const frame = frames.get(trial.id);
|
|
1231
|
+
const box = frame.size ?? { width: ref.current?.clientWidth ?? 0, height: ref.current?.clientHeight ?? 0 };
|
|
1232
|
+
const capture = frame.capture;
|
|
1233
|
+
return [
|
|
1234
|
+
{
|
|
1235
|
+
id: "story",
|
|
1236
|
+
ref,
|
|
1237
|
+
content: { w: Math.max(1, box.width), h: Math.max(1, box.height) },
|
|
1238
|
+
...capture ? { base: () => capture() } : {}
|
|
1239
|
+
}
|
|
1240
|
+
];
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
function documentInstrument(options) {
|
|
1245
|
+
const { entry, loaded, setup, globals = {}, frames } = options;
|
|
1246
|
+
const schema = loaded.kind === "story" ? loaded.story.config : f.schema({});
|
|
1247
|
+
const config = withGlobals(schema, globals);
|
|
1248
|
+
const initialState = loaded.kind === "story" ? loaded.story.initialState : null;
|
|
1249
|
+
return {
|
|
1250
|
+
name: entry.id,
|
|
1251
|
+
title: breadcrumb(entry.title, entry.name),
|
|
1252
|
+
config,
|
|
1253
|
+
defaultConfig: () => config.defaults(),
|
|
1254
|
+
initialState: (c) => initialState?.(c) ?? null,
|
|
1255
|
+
render: (ctx) => loaded.kind === "story" ? /* @__PURE__ */ jsx(StoryTrial, { story: loaded.story, setup, ctx }) : /* @__PURE__ */ jsx(IndexTrial, { bundle: loaded.bundle, setup, config: ctx.config }),
|
|
1256
|
+
...frames ? { annotations: annotationsOn(frames) } : {},
|
|
1257
|
+
...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {}
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
function StoryLoad({ entry, load, fault }) {
|
|
1261
|
+
useEffect(() => {
|
|
1262
|
+
if (!fault) load(entry);
|
|
1263
|
+
}, [entry, load, fault]);
|
|
1264
|
+
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: [
|
|
1265
|
+
/* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: "import" }),
|
|
1266
|
+
/* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
|
|
1267
|
+
] }) : null });
|
|
1268
|
+
}
|
|
1269
|
+
function pendingInstrument(options) {
|
|
1270
|
+
const { entry, globals = {} } = options;
|
|
1271
|
+
const config = withGlobals(f.schema({}), globals);
|
|
1272
|
+
return {
|
|
1273
|
+
name: entry.id,
|
|
1274
|
+
title: breadcrumb(entry.title, entry.name),
|
|
1275
|
+
config,
|
|
1276
|
+
defaultConfig: () => config.defaults(),
|
|
1277
|
+
initialState: () => null,
|
|
1278
|
+
render: () => /* @__PURE__ */ jsx(StoryLoad, { ...options }, options.generation ?? 0)
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
600
1281
|
|
|
601
1282
|
// src/shell/readyKey.ts
|
|
602
1283
|
var readyKey = (ready) => stableStringify({ schema: ready.schema, layout: ready.layout, viewport: ready.viewport });
|
|
603
1284
|
var START_TIMEOUT_MS = 1e4;
|
|
1285
|
+
var HELLO_TIMEOUT_MS = 3e4;
|
|
604
1286
|
var requests = 0;
|
|
605
1287
|
var IN_VIEW_MARGIN = "50%";
|
|
606
1288
|
function useInView(ref) {
|
|
@@ -644,13 +1326,17 @@ function FrameView(props) {
|
|
|
644
1326
|
const trialId = useContext(TrialIdContext);
|
|
645
1327
|
const frames = useContext(TrialFramesContext);
|
|
646
1328
|
const [overrides] = useCssOverrides();
|
|
1329
|
+
const pool = useContext(FramePoolContext);
|
|
1330
|
+
const lab = useLabContext();
|
|
647
1331
|
const src = `${frameUrl}#${entry.id}`;
|
|
1332
|
+
const title = breadcrumb(entry.title, entry.name);
|
|
648
1333
|
const iframeRef = useRef(null);
|
|
649
1334
|
const hostRef = useRef(null);
|
|
1335
|
+
const slotRef = useRef(null);
|
|
650
1336
|
const inView = useInView(hostRef);
|
|
651
1337
|
const link = useRef(null);
|
|
652
|
-
const latest = useRef({ ...props, globals, overrides });
|
|
653
|
-
latest.current = { ...props, globals, overrides };
|
|
1338
|
+
const latest = useRef({ ...props, globals, overrides, pool, lab, title });
|
|
1339
|
+
latest.current = { ...props, globals, overrides, pool, lab, title };
|
|
654
1340
|
const [fault, setFault] = useState(null);
|
|
655
1341
|
const [pending, setPending] = useState(true);
|
|
656
1342
|
const init = (current) => {
|
|
@@ -707,6 +1393,13 @@ function FrameView(props) {
|
|
|
707
1393
|
else current.pending.get(msg.id)?.fail(new Error(msg.message));
|
|
708
1394
|
current.pending.delete(msg.id);
|
|
709
1395
|
break;
|
|
1396
|
+
case "open": {
|
|
1397
|
+
const { lab: liveLab } = latest.current;
|
|
1398
|
+
if (!trialId || !liveLab.instruments.some((instrument) => instrument.name === msg.id)) break;
|
|
1399
|
+
liveLab.swapTrial(trialId, msg.id);
|
|
1400
|
+
setRoute(msg.id, { inPlace: true });
|
|
1401
|
+
break;
|
|
1402
|
+
}
|
|
710
1403
|
case "fault":
|
|
711
1404
|
if (msg.phase === "render" && msg.seq !== void 0 && msg.seq < current.inputs) break;
|
|
712
1405
|
clearTimeout(current.timer);
|
|
@@ -714,10 +1407,10 @@ function FrameView(props) {
|
|
|
714
1407
|
break;
|
|
715
1408
|
}
|
|
716
1409
|
};
|
|
717
|
-
const
|
|
1410
|
+
const connect = (frame) => {
|
|
718
1411
|
closeLink(link);
|
|
719
1412
|
setPending(true);
|
|
720
|
-
const target =
|
|
1413
|
+
const target = frame.contentWindow;
|
|
721
1414
|
if (!target) return;
|
|
722
1415
|
const { port1, port2 } = new MessageChannel();
|
|
723
1416
|
const timer = setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), START_TIMEOUT_MS);
|
|
@@ -769,15 +1462,53 @@ function FrameView(props) {
|
|
|
769
1462
|
};
|
|
770
1463
|
link.current = current;
|
|
771
1464
|
channel.on((msg) => receive(current, msg));
|
|
772
|
-
target.postMessage({ type: PORT_HANDOFF }, location.origin, [
|
|
1465
|
+
target.postMessage({ type: PORT_HANDOFF, id: latest.current.entry.id }, location.origin, [
|
|
1466
|
+
port2
|
|
1467
|
+
]);
|
|
773
1468
|
};
|
|
774
|
-
|
|
1469
|
+
const connectRef = useRef(connect);
|
|
1470
|
+
connectRef.current = connect;
|
|
775
1471
|
useEffect(() => {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1472
|
+
const slot = slotRef.current;
|
|
1473
|
+
if (!inView || !slot) return;
|
|
1474
|
+
const claimed = latest.current.pool?.claim() ?? null;
|
|
1475
|
+
const frame = claimed ?? document.createElement("iframe");
|
|
1476
|
+
frame.className = "fg-frame-view";
|
|
1477
|
+
frame.title = latest.current.title;
|
|
1478
|
+
frame.removeAttribute("tabindex");
|
|
1479
|
+
frame.toggleAttribute("data-pending", true);
|
|
1480
|
+
iframeRef.current = frame;
|
|
1481
|
+
const unheard = claimed ? void 0 : setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), HELLO_TIMEOUT_MS);
|
|
1482
|
+
const onMessage = (event) => {
|
|
1483
|
+
if (event.source !== frame.contentWindow || event.origin !== location.origin) return;
|
|
1484
|
+
if (event.data?.type !== FRAME_HELLO) return;
|
|
1485
|
+
clearTimeout(unheard);
|
|
1486
|
+
connectRef.current(frame);
|
|
1487
|
+
};
|
|
1488
|
+
window.addEventListener("message", onMessage);
|
|
1489
|
+
if (claimed) {
|
|
1490
|
+
moveFrame(slot, claimed);
|
|
1491
|
+
connectRef.current(claimed);
|
|
1492
|
+
} else {
|
|
1493
|
+
frame.src = src;
|
|
1494
|
+
slot.append(frame);
|
|
1495
|
+
}
|
|
1496
|
+
return () => {
|
|
1497
|
+
window.removeEventListener("message", onMessage);
|
|
1498
|
+
clearTimeout(unheard);
|
|
1499
|
+
closeLink(link);
|
|
1500
|
+
frame.remove();
|
|
1501
|
+
iframeRef.current = null;
|
|
1502
|
+
setPending(true);
|
|
1503
|
+
setFault(null);
|
|
1504
|
+
};
|
|
1505
|
+
}, [inView, src]);
|
|
1506
|
+
useEffect(() => {
|
|
1507
|
+
iframeRef.current?.toggleAttribute("data-pending", pending && !fault);
|
|
1508
|
+
}, [pending, fault, inView, src]);
|
|
1509
|
+
useEffect(() => {
|
|
1510
|
+
if (iframeRef.current) iframeRef.current.title = title;
|
|
1511
|
+
}, [title]);
|
|
781
1512
|
useEffect(() => answers.hold(ctx.config), [answers, ctx.config]);
|
|
782
1513
|
useEffect(() => {
|
|
783
1514
|
const current = link.current;
|
|
@@ -819,17 +1550,7 @@ function FrameView(props) {
|
|
|
819
1550
|
},
|
|
820
1551
|
className: "fg-frame-host",
|
|
821
1552
|
children: [
|
|
822
|
-
|
|
823
|
-
"iframe",
|
|
824
|
-
{
|
|
825
|
-
ref: iframeRef,
|
|
826
|
-
className: "fg-frame-view",
|
|
827
|
-
src,
|
|
828
|
-
title: `${entry.title} / ${entry.name}`,
|
|
829
|
-
onLoad,
|
|
830
|
-
"data-pending": pending && !fault ? "" : void 0
|
|
831
|
-
}
|
|
832
|
-
) : null,
|
|
1553
|
+
/* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
|
|
833
1554
|
fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
|
|
834
1555
|
fault.phase ? /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: fault.phase }) : null,
|
|
835
1556
|
/* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
|
|
@@ -838,7 +1559,7 @@ function FrameView(props) {
|
|
|
838
1559
|
}
|
|
839
1560
|
);
|
|
840
1561
|
}
|
|
841
|
-
function
|
|
1562
|
+
function annotationsOn2(frames) {
|
|
842
1563
|
return {
|
|
843
1564
|
targets: (_state, _config, trial) => {
|
|
844
1565
|
const ref = frames.hostRef(trial.id);
|
|
@@ -865,7 +1586,7 @@ function storyInstrument(options) {
|
|
|
865
1586
|
const descriptionKey = ready ? readyKey(ready) : null;
|
|
866
1587
|
return {
|
|
867
1588
|
name: entry.id,
|
|
868
|
-
title:
|
|
1589
|
+
title: breadcrumb(entry.title, entry.name),
|
|
869
1590
|
config,
|
|
870
1591
|
defaultConfig: () => config.defaults(),
|
|
871
1592
|
initialState: () => null,
|
|
@@ -880,17 +1601,22 @@ function storyInstrument(options) {
|
|
|
880
1601
|
ctx
|
|
881
1602
|
}
|
|
882
1603
|
),
|
|
883
|
-
...frames ? { annotations:
|
|
1604
|
+
...frames ? { annotations: annotationsOn2(frames) } : {},
|
|
884
1605
|
...ready?.viewport ? { stage: { size: { width: ready.viewport.width, height: ready.viewport.height } } } : {}
|
|
885
1606
|
};
|
|
886
1607
|
}
|
|
887
1608
|
|
|
888
1609
|
// src/shell/useStoryRegistry.ts
|
|
889
|
-
var
|
|
1610
|
+
var NO_GLOBALS2 = {};
|
|
1611
|
+
var NO_SETUP = {};
|
|
890
1612
|
var pruned = (map, ids) => [...map.keys()].every((id) => ids.has(id)) ? map : new Map([...map].filter(([id]) => ids.has(id)));
|
|
1613
|
+
var toError = (error) => error instanceof Error ? error : new Error(String(error));
|
|
891
1614
|
function useStoryRegistry(index, options) {
|
|
892
|
-
const { frameUrl, globals =
|
|
1615
|
+
const { frameUrl, globals = NO_GLOBALS2, frames, importers, setup = NO_SETUP, load = loadStories } = options;
|
|
893
1616
|
const [readies, setReadies] = useState(() => /* @__PURE__ */ new Map());
|
|
1617
|
+
const [loaded, setLoaded] = useState(() => /* @__PURE__ */ new Map());
|
|
1618
|
+
const [faults, setFaults] = useState(() => /* @__PURE__ */ new Map());
|
|
1619
|
+
const [generations, setGenerations] = useState(() => /* @__PURE__ */ new Map());
|
|
894
1620
|
const [revisions, setRevisions] = useState(() => /* @__PURE__ */ new Map());
|
|
895
1621
|
const onReady = useCallback((entry, ready) => {
|
|
896
1622
|
setReadies((prev) => {
|
|
@@ -902,8 +1628,93 @@ function useStoryRegistry(index, options) {
|
|
|
902
1628
|
useEffect(() => {
|
|
903
1629
|
const ids = new Set(index.map((entry) => entry.id));
|
|
904
1630
|
setReadies((prev) => pruned(prev, ids));
|
|
1631
|
+
setLoaded((prev) => pruned(prev, ids));
|
|
1632
|
+
setFaults((prev) => pruned(prev, ids));
|
|
905
1633
|
setRevisions((prev) => pruned(prev, ids));
|
|
906
1634
|
}, [index]);
|
|
1635
|
+
const imports = useRef(/* @__PURE__ */ new Map());
|
|
1636
|
+
const live = useRef({ index, importers, setup, load });
|
|
1637
|
+
live.current = { index, importers, setup, load };
|
|
1638
|
+
const importOf = useCallback((file) => {
|
|
1639
|
+
let loading = imports.current.get(file);
|
|
1640
|
+
if (!loading) {
|
|
1641
|
+
const importer = live.current.importers?.[file];
|
|
1642
|
+
loading = importer ? importer() : Promise.reject(new Error(`No importer for ${file}`));
|
|
1643
|
+
imports.current.set(file, loading);
|
|
1644
|
+
}
|
|
1645
|
+
return loading;
|
|
1646
|
+
}, []);
|
|
1647
|
+
const request = useCallback(
|
|
1648
|
+
(entry) => {
|
|
1649
|
+
const { setup: current, load: loadModule } = live.current;
|
|
1650
|
+
const settle2 = (ids2, result) => {
|
|
1651
|
+
setLoaded((prev) => {
|
|
1652
|
+
const next = new Map(prev);
|
|
1653
|
+
for (const [id, value] of result) next.set(id, value);
|
|
1654
|
+
return next;
|
|
1655
|
+
});
|
|
1656
|
+
setFaults((prev) => ids2.some((id) => prev.has(id)) ? pruned(prev, new Set([...prev.keys()].filter((id) => !ids2.includes(id)))) : prev);
|
|
1657
|
+
};
|
|
1658
|
+
const fail = (ids2, error) => {
|
|
1659
|
+
const fault = toError(error);
|
|
1660
|
+
setFaults((prev) => {
|
|
1661
|
+
const next = new Map(prev);
|
|
1662
|
+
for (const id of ids2) next.set(id, fault);
|
|
1663
|
+
return next;
|
|
1664
|
+
});
|
|
1665
|
+
};
|
|
1666
|
+
if (isIndexId(entry.id)) {
|
|
1667
|
+
const entries = live.current.index.filter((e) => !isIndexId(e.id) && indexId(e.title) === entry.id);
|
|
1668
|
+
const files = [...new Set(entries.map((e) => e.file))];
|
|
1669
|
+
void Promise.all(files.map(importOf)).then(async (mods) => {
|
|
1670
|
+
const byFile = new Map(files.map((file, i) => [file, loadModule(mods[i] ?? {}, entry.title, current.parameters)]));
|
|
1671
|
+
const stories = entries.flatMap(
|
|
1672
|
+
(e) => byFile.get(e.file)?.find((s) => s.exportName === e.exportName) ?? []
|
|
1673
|
+
);
|
|
1674
|
+
for (const story of stories) await current.prepare?.(story);
|
|
1675
|
+
const result = /* @__PURE__ */ new Map();
|
|
1676
|
+
result.set(entry.id, {
|
|
1677
|
+
kind: "index",
|
|
1678
|
+
bundle: {
|
|
1679
|
+
title: entry.title,
|
|
1680
|
+
...entry.componentDescription === void 0 ? {} : { description: entry.componentDescription },
|
|
1681
|
+
stories,
|
|
1682
|
+
descriptions: Object.fromEntries(entries.flatMap((e) => e.description ? [[e.id, e.description]] : [])),
|
|
1683
|
+
render: mods.map(indexRenderOf).find((render) => render !== null) ?? null
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
settle2([entry.id], result);
|
|
1687
|
+
}).catch((error) => fail([entry.id], error));
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
const siblings = live.current.index.filter((e) => e.file === entry.file && !isIndexId(e.id));
|
|
1691
|
+
const ids = siblings.map((e) => e.id);
|
|
1692
|
+
void importOf(entry.file).then(async (mod) => {
|
|
1693
|
+
const stories = loadModule(mod, entry.title, current.parameters);
|
|
1694
|
+
const result = /* @__PURE__ */ new Map();
|
|
1695
|
+
for (const sibling of siblings) {
|
|
1696
|
+
const story = stories.find((s) => s.exportName === sibling.exportName);
|
|
1697
|
+
if (!story) continue;
|
|
1698
|
+
await current.prepare?.(story);
|
|
1699
|
+
result.set(sibling.id, { kind: "story", story });
|
|
1700
|
+
}
|
|
1701
|
+
if (!result.has(entry.id)) throw new Error(`${entry.file} has no story export "${entry.exportName}"`);
|
|
1702
|
+
settle2(ids, result);
|
|
1703
|
+
}).catch((error) => fail(ids, error));
|
|
1704
|
+
},
|
|
1705
|
+
[importOf]
|
|
1706
|
+
);
|
|
1707
|
+
const reload = useCallback(
|
|
1708
|
+
(file) => {
|
|
1709
|
+
imports.current.delete(file);
|
|
1710
|
+
const affected = live.current.index.filter((e) => e.file === file || isIndexId(e.id) && live.current.index.some((s) => s.file === file && indexId(s.title) === e.id));
|
|
1711
|
+
const ids = new Set(affected.map((e) => e.id));
|
|
1712
|
+
setLoaded((prev) => pruned(prev, new Set([...prev.keys()].filter((id) => !ids.has(id)))));
|
|
1713
|
+
setFaults((prev) => pruned(prev, new Set([...prev.keys()].filter((id) => !ids.has(id)))));
|
|
1714
|
+
setGenerations((prev) => new Map(prev).set(file, (prev.get(file) ?? 0) + 1));
|
|
1715
|
+
},
|
|
1716
|
+
[]
|
|
1717
|
+
);
|
|
907
1718
|
const committed = useRef({ built: /* @__PURE__ */ new Map(), books: /* @__PURE__ */ new Map(), list: [] });
|
|
908
1719
|
const cache = useMemo(() => {
|
|
909
1720
|
const previous = committed.current;
|
|
@@ -911,26 +1722,35 @@ function useStoryRegistry(index, options) {
|
|
|
911
1722
|
const books = /* @__PURE__ */ new Map();
|
|
912
1723
|
const list = index.map((entry) => {
|
|
913
1724
|
const ready = readies.get(entry.id);
|
|
1725
|
+
const inDocument = importers !== void 0 && entry.isolate === void 0;
|
|
1726
|
+
const had = loaded.get(entry.id);
|
|
1727
|
+
const fault = faults.get(entry.id);
|
|
1728
|
+
const generation = generations.get(entry.file) ?? 0;
|
|
914
1729
|
const revision = revisions.get(entry.id) ?? 0;
|
|
915
1730
|
const entryKey = stableStringify(entry);
|
|
916
1731
|
const answers = previous.books.get(entry.id) ?? createAnswerBook();
|
|
917
1732
|
books.set(entry.id, answers);
|
|
918
1733
|
const held = previous.built.get(entry.id);
|
|
919
|
-
const kept = held && held.entryKey === entryKey && held.ready === ready && held.frameUrl === frameUrl && held.globals === globals && held.frames === frames && held.revision === revision ? held : {
|
|
1734
|
+
const kept = held && held.entryKey === entryKey && held.ready === ready && held.loaded === had && held.fault === fault && held.frameUrl === frameUrl && held.globals === globals && held.frames === frames && held.setup === setup && held.inDocument === inDocument && held.generation === generation && held.revision === revision ? held : {
|
|
920
1735
|
entryKey,
|
|
921
1736
|
ready,
|
|
1737
|
+
loaded: had,
|
|
1738
|
+
fault,
|
|
922
1739
|
frameUrl,
|
|
923
1740
|
globals,
|
|
924
1741
|
frames,
|
|
1742
|
+
setup,
|
|
1743
|
+
inDocument,
|
|
1744
|
+
generation,
|
|
925
1745
|
revision,
|
|
926
|
-
instrument: storyInstrument({ entry, ready, answers, frameUrl, onReady, globals, ...frames ? { frames } : {} })
|
|
1746
|
+
instrument: !inDocument ? storyInstrument({ entry, ready, answers, frameUrl, onReady, globals, ...frames ? { frames } : {} }) : had ? documentInstrument({ entry, loaded: had, setup, globals, ...frames ? { frames } : {} }) : pendingInstrument({ entry, globals, load: request, generation, ...fault ? { fault } : {} })
|
|
927
1747
|
};
|
|
928
1748
|
built.set(entry.id, kept);
|
|
929
1749
|
return kept.instrument;
|
|
930
1750
|
});
|
|
931
1751
|
const unchanged = list.length === previous.list.length && list.every((i, n) => i === previous.list[n]);
|
|
932
1752
|
return { built, books, list: unchanged ? previous.list : list };
|
|
933
|
-
}, [index, readies, revisions, frameUrl, globals, frames, onReady]);
|
|
1753
|
+
}, [index, readies, loaded, faults, generations, revisions, frameUrl, globals, frames, setup, importers, onReady, request]);
|
|
934
1754
|
useLayoutEffect(() => {
|
|
935
1755
|
committed.current = cache;
|
|
936
1756
|
}, [cache]);
|
|
@@ -942,29 +1762,75 @@ function useStoryRegistry(index, options) {
|
|
|
942
1762
|
for (const off of offs) off();
|
|
943
1763
|
};
|
|
944
1764
|
}, [cache.books]);
|
|
945
|
-
|
|
1765
|
+
const isReady = useCallback((id) => readies.has(id) || loaded.has(id), [readies, loaded]);
|
|
1766
|
+
return { instruments: cache.list, isReady, onReady, reload };
|
|
946
1767
|
}
|
|
947
1768
|
var NO_DECLARATIONS = {};
|
|
948
|
-
function initialStory(
|
|
1769
|
+
function initialStory(entries, fallback) {
|
|
949
1770
|
const route = readRoute();
|
|
950
|
-
return route !== null &&
|
|
1771
|
+
return route !== null && entries.some((entry) => entry.id === route) ? route : fallback;
|
|
951
1772
|
}
|
|
952
1773
|
function RouteOpener({ index }) {
|
|
953
1774
|
const lab = useLabContext();
|
|
954
1775
|
const [route] = useRoute();
|
|
955
1776
|
const handled = useRef(null);
|
|
1777
|
+
const last = useRef({ route: readRoute(), entry: readRouteEntry() });
|
|
956
1778
|
useEffect(() => {
|
|
957
|
-
|
|
1779
|
+
const from = last.current;
|
|
1780
|
+
if (route !== from.route) last.current = { route, entry: readRouteEntry() };
|
|
1781
|
+
if (route === null || handled.current === route || !index.some((e) => e.id === route)) return;
|
|
958
1782
|
handled.current = route;
|
|
959
|
-
if (
|
|
1783
|
+
if (lab.trials.some((trial) => trial.instrumentName === route)) return;
|
|
1784
|
+
const previous = lab.trials.find((trial) => trial.instrumentName === from.route);
|
|
1785
|
+
const target = (previous && crossesInPlace(from.entry, last.current.entry) ? previous : void 0) ?? lab.trials.find((trial) => trial.id === lab.focusedTrialId) ?? lab.trials[0];
|
|
1786
|
+
if (target) lab.swapTrial(target.id, route);
|
|
1787
|
+
else lab.addTrial(route);
|
|
960
1788
|
}, [route, index, lab]);
|
|
961
1789
|
return null;
|
|
962
1790
|
}
|
|
963
|
-
function
|
|
1791
|
+
function useInfoShortcut(open) {
|
|
1792
|
+
useEffect(() => {
|
|
1793
|
+
const onKeyDown = (event) => {
|
|
1794
|
+
if (event.key !== "i" && event.key !== "I") return;
|
|
1795
|
+
if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return;
|
|
1796
|
+
const target = event.target;
|
|
1797
|
+
if (target?.isContentEditable || /^(input|textarea|select)$/i.test(target?.tagName ?? "")) return;
|
|
1798
|
+
event.preventDefault();
|
|
1799
|
+
open(true);
|
|
1800
|
+
};
|
|
1801
|
+
window.addEventListener("keydown", onKeyDown);
|
|
1802
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
1803
|
+
}, [open]);
|
|
1804
|
+
}
|
|
1805
|
+
function Workshop({ index, frameUrl, importers, setup, changes, config, stories = [], storageKey, storage }) {
|
|
964
1806
|
const declarations = config?.globals ?? NO_DECLARATIONS;
|
|
965
1807
|
const [frames] = useState(createTrialFrames);
|
|
966
|
-
const
|
|
1808
|
+
const [pool, setPool] = useState(null);
|
|
1809
|
+
const framed = importers === void 0 || index.some((entry) => entry.isolate !== void 0);
|
|
1810
|
+
useEffect(() => {
|
|
1811
|
+
if (!framed) return;
|
|
1812
|
+
const next = createFramePool(frameUrl);
|
|
1813
|
+
setPool(next);
|
|
1814
|
+
return () => {
|
|
1815
|
+
next.dispose();
|
|
1816
|
+
setPool(null);
|
|
1817
|
+
};
|
|
1818
|
+
}, [frameUrl, framed]);
|
|
1819
|
+
const entries = useMemo(() => [...index, ...indexEntries(index)], [index]);
|
|
1820
|
+
const registry = useStoryRegistry(entries, {
|
|
1821
|
+
frameUrl,
|
|
1822
|
+
globals: declarations,
|
|
1823
|
+
frames,
|
|
1824
|
+
...importers ? { importers } : {},
|
|
1825
|
+
...setup ? { setup } : {}
|
|
1826
|
+
});
|
|
1827
|
+
const reload = registry.reload;
|
|
1828
|
+
useEffect(() => changes?.subscribe(reload), [changes, reload]);
|
|
967
1829
|
const [labValues, setLabValues] = useState(() => labGlobals(declarations, void 0));
|
|
1830
|
+
const themeFor = config?.labTheme;
|
|
1831
|
+
const labTheme = useMemo(() => themeFor?.(labValues), [themeFor, labValues]);
|
|
1832
|
+
const [infoOpen, setInfoOpen] = useState(false);
|
|
1833
|
+
useInfoShortcut(setInfoOpen);
|
|
968
1834
|
const reportLabValues = useCallback(
|
|
969
1835
|
(next) => setLabValues((prev) => stableStringify(prev) === stableStringify(next) ? prev : next),
|
|
970
1836
|
[]
|
|
@@ -972,6 +1838,11 @@ function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }
|
|
|
972
1838
|
const labChrome = useMemo(
|
|
973
1839
|
() => [
|
|
974
1840
|
{ id: "fg-stories", region: "sidebar", render: (ctx) => /* @__PURE__ */ jsx(StoryTree, { ctx, index }) },
|
|
1841
|
+
{
|
|
1842
|
+
id: "fg-info",
|
|
1843
|
+
region: "palette",
|
|
1844
|
+
item: { icon: InfoIcon, label: "Info", shortcut: "\u2318I", onActivate: () => setInfoOpen(true) }
|
|
1845
|
+
},
|
|
975
1846
|
CSS_VARS_SECTION,
|
|
976
1847
|
A11Y_SECTION,
|
|
977
1848
|
...Object.keys(declarations).length > 0 ? [{ id: "fg-globals", region: "header", render: () => /* @__PURE__ */ jsx(GlobalsToolbar, { declarations }) }] : [],
|
|
@@ -986,12 +1857,14 @@ function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }
|
|
|
986
1857
|
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
|
|
987
1858
|
] });
|
|
988
1859
|
}
|
|
989
|
-
return /* @__PURE__ */ jsx(StoryGlobalsContext.Provider, { value: labValues, children: /* @__PURE__ */ jsx(TrialFramesContext.Provider, { value: frames, children: /* @__PURE__ */ jsxs(
|
|
1860
|
+
return /* @__PURE__ */ jsx(StoryGlobalsContext.Provider, { value: labValues, children: /* @__PURE__ */ jsx(FramePoolContext.Provider, { value: pool, children: /* @__PURE__ */ jsx(TrialFramesContext.Provider, { value: frames, children: /* @__PURE__ */ jsxs(
|
|
990
1861
|
Lab,
|
|
991
1862
|
{
|
|
992
1863
|
title: "weaselforge",
|
|
1864
|
+
density: "roomy",
|
|
1865
|
+
...labTheme ? { theme: labTheme } : {},
|
|
993
1866
|
instruments: registry.instruments,
|
|
994
|
-
defaultInstrument: initialStory(
|
|
1867
|
+
defaultInstrument: initialStory(entries, first.id),
|
|
995
1868
|
storageKey: storageKey ?? "weaselforge",
|
|
996
1869
|
...storage ? { storage } : {},
|
|
997
1870
|
labChrome,
|
|
@@ -1000,27 +1873,42 @@ function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }
|
|
|
1000
1873
|
...config?.pages ? { pages: config.pages } : {},
|
|
1001
1874
|
...config?.path !== void 0 ? { path: config.path } : {},
|
|
1002
1875
|
children: [
|
|
1003
|
-
/* @__PURE__ */ jsx(RouteOpener, { index }),
|
|
1876
|
+
/* @__PURE__ */ jsx(RouteOpener, { index: entries }),
|
|
1877
|
+
/* @__PURE__ */ jsx(
|
|
1878
|
+
StoryInfoDialog,
|
|
1879
|
+
{
|
|
1880
|
+
index,
|
|
1881
|
+
isReady: registry.isReady,
|
|
1882
|
+
isOpen: infoOpen,
|
|
1883
|
+
onOpenChange: setInfoOpen
|
|
1884
|
+
}
|
|
1885
|
+
),
|
|
1004
1886
|
/* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues })
|
|
1005
1887
|
]
|
|
1006
1888
|
}
|
|
1007
|
-
) }) });
|
|
1889
|
+
) }) }) });
|
|
1008
1890
|
}
|
|
1009
1891
|
function mountWorkshop({ container, ...props }) {
|
|
1010
1892
|
const target = container ?? document.getElementById("root");
|
|
1011
1893
|
if (!target) throw new Error("mountWorkshop: no container given and no #root element");
|
|
1012
1894
|
const root = createRoot(target);
|
|
1013
|
-
|
|
1895
|
+
const changes = createStoryChanges();
|
|
1896
|
+
let current = { ...props, changes };
|
|
1014
1897
|
root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
|
|
1015
1898
|
return {
|
|
1016
1899
|
setIndex(index) {
|
|
1017
1900
|
current = { ...current, index };
|
|
1018
1901
|
root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
|
|
1019
1902
|
},
|
|
1903
|
+
setImporters(importers) {
|
|
1904
|
+
current = { ...current, importers };
|
|
1905
|
+
root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
|
|
1906
|
+
},
|
|
1907
|
+
reloadStory: (file) => changes.emit(file),
|
|
1020
1908
|
unmount: () => root.unmount()
|
|
1021
1909
|
};
|
|
1022
1910
|
}
|
|
1023
1911
|
|
|
1024
|
-
export { A11Y_SECTION, A11yPanel,
|
|
1912
|
+
export { A11Y_SECTION, A11yPanel, Workshop, mountWorkshop, useStoryRegistry };
|
|
1025
1913
|
//# sourceMappingURL=index.js.map
|
|
1026
1914
|
//# sourceMappingURL=index.js.map
|