@weasel-js/forge 1.5.2 → 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-eZM3hlcE.d.ts → FrameController-BspC9Zr5.d.ts} +61 -4
- package/dist/chunk-A4JOEGYQ.js +325 -0
- package/dist/chunk-A4JOEGYQ.js.map +1 -0
- package/dist/{chunk-5YHEO26U.js → chunk-FWPBKKYB.js} +58 -240
- 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-CSVP6duU.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 +96 -19
- package/dist/shell/index.css.map +1 -1
- package/dist/shell/index.d.ts +75 -15
- package/dist/shell/index.js +762 -214
- 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 +43 -5
- 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-5YHEO26U.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,77 +1,19 @@
|
|
|
1
|
-
import { schemaFromDescription, openChannel } from '../chunk-
|
|
2
|
-
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';
|
|
3
8
|
import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, inferTokenType, ToggleBar as ToggleBar$1, Input, TokenPanel, refitScale } from '@weasel-js/labkit';
|
|
4
|
-
import { createContext,
|
|
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 { InfoIcon, ToggleBar, Checkbox, Dialog } from '@weasel-js/ui';
|
|
8
|
-
import { TOKEN_MANIFEST, THEME_SOURCES } from '@weasel-js/theme';
|
|
9
|
-
import { scale } from '@weasel-js/theme/engine';
|
|
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';
|
|
10
15
|
import { f, valueAtPath, isConfigBranch } from '@weasel-js/labkit/config';
|
|
11
16
|
|
|
12
|
-
var NO_FRAME = { send: null, vars: [], audit: null, a11y: null, capture: null, size: null };
|
|
13
|
-
function createTrialFrames() {
|
|
14
|
-
const frames = /* @__PURE__ */ new Map();
|
|
15
|
-
const hosts = /* @__PURE__ */ new Map();
|
|
16
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
17
|
-
const changed = () => {
|
|
18
|
-
for (const listener of [...listeners]) listener();
|
|
19
|
-
};
|
|
20
|
-
const patch = (trialId, next) => {
|
|
21
|
-
const frame = frames.get(trialId);
|
|
22
|
-
if (!frame) return;
|
|
23
|
-
frames.set(trialId, { ...frame, ...next });
|
|
24
|
-
changed();
|
|
25
|
-
};
|
|
26
|
-
return {
|
|
27
|
-
connect(trialId, calls) {
|
|
28
|
-
const held = frames.get(trialId);
|
|
29
|
-
frames.set(trialId, {
|
|
30
|
-
send: calls.send,
|
|
31
|
-
vars: [],
|
|
32
|
-
audit: calls.audit,
|
|
33
|
-
a11y: null,
|
|
34
|
-
capture: calls.capture,
|
|
35
|
-
size: held?.size ?? null
|
|
36
|
-
});
|
|
37
|
-
changed();
|
|
38
|
-
return () => {
|
|
39
|
-
if (frames.get(trialId)?.send !== calls.send) return;
|
|
40
|
-
frames.delete(trialId);
|
|
41
|
-
changed();
|
|
42
|
-
};
|
|
43
|
-
},
|
|
44
|
-
report: (trialId, vars) => patch(trialId, { vars }),
|
|
45
|
-
reportA11y: (trialId, a11y) => patch(trialId, { a11y }),
|
|
46
|
-
reportSize(trialId, size) {
|
|
47
|
-
const frame = frames.get(trialId) ?? NO_FRAME;
|
|
48
|
-
if (frame.size?.width === size.width && frame.size?.height === size.height) return;
|
|
49
|
-
frames.set(trialId, { ...frame, size });
|
|
50
|
-
changed();
|
|
51
|
-
},
|
|
52
|
-
hostRef(trialId) {
|
|
53
|
-
let ref = hosts.get(trialId);
|
|
54
|
-
if (!ref) {
|
|
55
|
-
ref = { current: null };
|
|
56
|
-
hosts.set(trialId, ref);
|
|
57
|
-
}
|
|
58
|
-
return ref;
|
|
59
|
-
},
|
|
60
|
-
get: (trialId) => frames.get(trialId) ?? NO_FRAME,
|
|
61
|
-
subscribe(listener) {
|
|
62
|
-
listeners.add(listener);
|
|
63
|
-
return () => listeners.delete(listener);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
var TrialFramesContext = createContext(null);
|
|
68
|
-
function useTrialFrame(trialId) {
|
|
69
|
-
const frames = useContext(TrialFramesContext);
|
|
70
|
-
const subscribe = useCallback((listener) => frames?.subscribe(listener) ?? (() => {
|
|
71
|
-
}), [frames]);
|
|
72
|
-
const read = () => frames?.get(trialId) ?? NO_FRAME;
|
|
73
|
-
return useSyncExternalStore(subscribe, read, read);
|
|
74
|
-
}
|
|
75
17
|
function Finding({ finding }) {
|
|
76
18
|
return /* @__PURE__ */ jsxs("li", { className: "fg-a11y__finding", children: [
|
|
77
19
|
/* @__PURE__ */ jsxs("p", { className: "fg-a11y__help", children: [
|
|
@@ -149,17 +91,137 @@ var A11Y_SECTION = {
|
|
|
149
91
|
region: "aside",
|
|
150
92
|
item: { title: "Accessibility", body: /* @__PURE__ */ jsx(FocusedA11y, {}) }
|
|
151
93
|
};
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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 };
|
|
155
216
|
}
|
|
156
217
|
var TABS = [
|
|
157
218
|
{ value: "theme", label: "Theme" },
|
|
158
219
|
{ value: "story", label: "Story" }
|
|
159
220
|
];
|
|
160
221
|
var MANIFEST = new Map(TOKEN_MANIFEST.map((token) => [token.name, token]));
|
|
222
|
+
var THEME_NAME = "weasel";
|
|
161
223
|
var literal = (v) => typeof v === "number";
|
|
162
|
-
var SCALE_SOURCES = Object.entries(THEME_SOURCES
|
|
224
|
+
var SCALE_SOURCES = Object.entries(THEME_SOURCES[THEME_NAME]?.scales ?? {}).flatMap(([name, def]) => {
|
|
163
225
|
if (!Array.isArray(def.steps)) return [];
|
|
164
226
|
const steps = def.steps;
|
|
165
227
|
const tokens = steps.map((step) => `--wzl-${name}-${step}`);
|
|
@@ -169,6 +231,18 @@ var SCALE_SOURCES = Object.entries(THEME_SOURCES.weasel?.scales ?? {}).flatMap((
|
|
|
169
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";
|
|
170
232
|
return [{ name, group, steps, tokens, rule }];
|
|
171
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
|
+
}
|
|
172
246
|
function standingScale(source, amounts) {
|
|
173
247
|
const seed = { tokens: source.tokens, base: amounts[0] ?? 0, rule: { kind: "step", step: 0 } };
|
|
174
248
|
if (typeof source.rule === "string") return refitScale(seed, source.rule, amounts);
|
|
@@ -185,7 +259,8 @@ function groupOf(name) {
|
|
|
185
259
|
const segments = name.replace(/^--/, "").split("-");
|
|
186
260
|
return segments.length > 1 ? segments.slice(0, -1).join("-") : name.replace(/^--/, "");
|
|
187
261
|
}
|
|
188
|
-
|
|
262
|
+
var NO_GLOBALS = {};
|
|
263
|
+
function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
|
|
189
264
|
const trialId = useTrialId();
|
|
190
265
|
const frame = useTrialFrame(trialId);
|
|
191
266
|
const [overrides, setOverrides] = useCssOverrides();
|
|
@@ -246,19 +321,63 @@ function CssVarsPanel() {
|
|
|
246
321
|
const source = SCALE_SOURCES.find((sc) => sc.group === group);
|
|
247
322
|
if (!source) return;
|
|
248
323
|
setEdited((prev) => ({ ...prev, [group]: next }));
|
|
249
|
-
const
|
|
250
|
-
const values = scale(source.steps, {
|
|
251
|
-
base: next.base,
|
|
252
|
-
...rule.kind === "factors" ? { factors: rule.factors } : rule.kind === "ratio" ? { ratio: rule.ratio } : { step: rule.step }
|
|
253
|
-
});
|
|
254
|
-
source.steps.forEach((step, i) => {
|
|
255
|
-
const value = values[step];
|
|
256
|
-
const name = source.tokens[i];
|
|
257
|
-
if (value !== void 0 && name !== void 0) write(name, value);
|
|
258
|
-
});
|
|
324
|
+
for (const [name, value] of scaleValues(source, next)) write(name, value);
|
|
259
325
|
},
|
|
260
326
|
[setEdited, write]
|
|
261
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
|
+
};
|
|
262
381
|
const onCollapsedChange = useCallback(
|
|
263
382
|
(category, collapsed) => setFolds((prev) => ({ ...prev, [category]: collapsed })),
|
|
264
383
|
[setFolds]
|
|
@@ -277,7 +396,18 @@ function CssVarsPanel() {
|
|
|
277
396
|
}
|
|
278
397
|
}
|
|
279
398
|
),
|
|
280
|
-
/* @__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
|
|
281
411
|
] }),
|
|
282
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(
|
|
283
413
|
TokenPanel,
|
|
@@ -294,12 +424,14 @@ function CssVarsPanel() {
|
|
|
294
424
|
}
|
|
295
425
|
function FocusedCssVars() {
|
|
296
426
|
const { focusedTrialId, trials, instruments } = useLabContext();
|
|
427
|
+
const labGlobals2 = useContext(StoryGlobalsContext);
|
|
297
428
|
const record = trials.find((trial) => trial.id === focusedTrialId);
|
|
298
429
|
if (!record) return null;
|
|
430
|
+
const pins = record.config?.[GLOBALS_KEY];
|
|
299
431
|
const instrument = instruments.find((i) => i.name === record.instrumentName);
|
|
300
432
|
return /* @__PURE__ */ jsxs("section", { className: "fg-css-vars-focus", "aria-label": "CSS Vars", children: [
|
|
301
433
|
/* @__PURE__ */ jsx("p", { className: "fg-css-vars__trial", children: record.title ?? instrument?.title ?? record.instrumentName }),
|
|
302
|
-
/* @__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) }) })
|
|
303
435
|
] });
|
|
304
436
|
}
|
|
305
437
|
var CSS_VARS_SECTION = {
|
|
@@ -308,9 +440,15 @@ var CSS_VARS_SECTION = {
|
|
|
308
440
|
item: { title: "CSS Vars", body: /* @__PURE__ */ jsx(FocusedCssVars, {}) }
|
|
309
441
|
};
|
|
310
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
|
+
|
|
311
449
|
// src/shell/tree/buildComponents.ts
|
|
312
450
|
var LIBRARIES = [
|
|
313
|
-
["/packages/ui/", "
|
|
451
|
+
["/packages/ui/", "ui"],
|
|
314
452
|
["/packages/labkit/", "labkit"],
|
|
315
453
|
["/packages/forge/", "forge"],
|
|
316
454
|
["/apps/draw/", "draw"],
|
|
@@ -333,6 +471,7 @@ function buildComponents(index) {
|
|
|
333
471
|
path: entry.title,
|
|
334
472
|
label: segments[segments.length - 1],
|
|
335
473
|
library: libraryOf(entry),
|
|
474
|
+
index: indexEntries([entry])[0],
|
|
336
475
|
entries: []
|
|
337
476
|
};
|
|
338
477
|
byTitle.set(entry.title, row);
|
|
@@ -348,9 +487,10 @@ function buildComponents(index) {
|
|
|
348
487
|
function disambiguate(rows) {
|
|
349
488
|
const byLabel = /* @__PURE__ */ new Map();
|
|
350
489
|
for (const row of rows) {
|
|
351
|
-
const
|
|
490
|
+
const key = `${row.library}\0${row.label}`;
|
|
491
|
+
const group = byLabel.get(key);
|
|
352
492
|
if (group) group.push(row);
|
|
353
|
-
else byLabel.set(
|
|
493
|
+
else byLabel.set(key, [row]);
|
|
354
494
|
}
|
|
355
495
|
for (const group of byLabel.values()) {
|
|
356
496
|
if (group.length < 2) continue;
|
|
@@ -373,20 +513,39 @@ function filterComponents(rows, query) {
|
|
|
373
513
|
continue;
|
|
374
514
|
}
|
|
375
515
|
const entries = row.entries.filter((entry) => entry.name.toLowerCase().includes(needle));
|
|
376
|
-
if (entries.length > 0) out.push({
|
|
516
|
+
if (entries.length > 0) out.push({ path: row.path, label: row.label, library: row.library, entries });
|
|
377
517
|
}
|
|
378
518
|
return out;
|
|
379
519
|
}
|
|
380
520
|
function componentNodes(rows) {
|
|
381
|
-
|
|
382
|
-
|
|
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({
|
|
383
540
|
kind: "folder",
|
|
384
|
-
label:
|
|
541
|
+
label: labels[labels.length - 1],
|
|
385
542
|
path: row.path,
|
|
386
|
-
tag
|
|
543
|
+
tag,
|
|
544
|
+
...row.index ? { index: row.index } : {},
|
|
387
545
|
children: row.entries.map((entry) => ({ kind: "story", entry }))
|
|
388
|
-
}
|
|
389
|
-
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
return root;
|
|
390
549
|
}
|
|
391
550
|
function librariesIn(index) {
|
|
392
551
|
return [...new Set(index.map(libraryOf))].sort((a, b) => a.localeCompare(b));
|
|
@@ -501,51 +660,73 @@ function StoryInfoDialog({ index, isReady, isOpen, onOpenChange }) {
|
|
|
501
660
|
{
|
|
502
661
|
isOpen,
|
|
503
662
|
onOpenChange,
|
|
504
|
-
title:
|
|
663
|
+
title: breadcrumb(dossier.title, dossier.name),
|
|
505
664
|
className: "fg-info-dialog",
|
|
506
665
|
children: /* @__PURE__ */ jsx(StoryDossier, { dossier })
|
|
507
666
|
}
|
|
508
667
|
);
|
|
509
668
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
513
|
-
var isGlobalsPath = (path) => path === GLOBALS_KEY || path.startsWith(`${GLOBALS_KEY}.`);
|
|
514
|
-
function labGlobals(declarations, stored) {
|
|
515
|
-
const from = isRecord(stored) ? stored : {};
|
|
516
|
-
return Object.fromEntries(
|
|
517
|
-
Object.entries(declarations).map(([key, declaration]) => {
|
|
518
|
-
const value = from[key];
|
|
519
|
-
return [key, declaration.options.some((option) => option.value === value) ? value : declaration.default];
|
|
520
|
-
})
|
|
521
|
-
);
|
|
669
|
+
function canMoveFrames() {
|
|
670
|
+
return typeof Element !== "undefined" && "moveBefore" in Element.prototype;
|
|
522
671
|
}
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
const pinned = Object.entries(pins).filter(([key, value]) => key in lab && value !== FOLLOW_LAB);
|
|
526
|
-
return pinned.length === 0 ? lab : { ...lab, ...Object.fromEntries(pinned) };
|
|
527
|
-
}
|
|
528
|
-
function storyConfig(config, previous) {
|
|
529
|
-
if (!isRecord(config) || !(GLOBALS_KEY in config)) return config;
|
|
530
|
-
const rest = Object.fromEntries(Object.entries(config).filter(([key]) => key !== GLOBALS_KEY));
|
|
531
|
-
if (isRecord(previous)) {
|
|
532
|
-
const keys = Object.keys(rest);
|
|
533
|
-
if (keys.length === Object.keys(previous).length && keys.every((key) => rest[key] === previous[key])) return previous;
|
|
534
|
-
}
|
|
535
|
-
return rest;
|
|
672
|
+
function moveFrame(host, frame) {
|
|
673
|
+
host.moveBefore(frame, host.firstChild);
|
|
536
674
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
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
|
+
};
|
|
548
728
|
}
|
|
729
|
+
var FramePoolContext = createContext(null);
|
|
549
730
|
var RECORD = "fg-globals";
|
|
550
731
|
function useLabGlobals(declarations) {
|
|
551
732
|
const [stored, setStored] = usePersistedState(RECORD, null, { scope: "lab" });
|
|
@@ -556,19 +737,65 @@ function useLabGlobals(declarations) {
|
|
|
556
737
|
);
|
|
557
738
|
return [values, set];
|
|
558
739
|
}
|
|
559
|
-
function
|
|
560
|
-
|
|
561
|
-
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(
|
|
562
742
|
Select,
|
|
563
743
|
{
|
|
564
|
-
"aria-label": declaration.label,
|
|
565
|
-
width: "fit",
|
|
566
|
-
selectedKey: String(values[
|
|
567
|
-
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),
|
|
568
748
|
options: declaration.options
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
|
|
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
|
+
] });
|
|
572
799
|
}
|
|
573
800
|
function LabGlobals({
|
|
574
801
|
declarations,
|
|
@@ -578,7 +805,6 @@ function LabGlobals({
|
|
|
578
805
|
useEffect(() => onChange(values), [values, onChange]);
|
|
579
806
|
return null;
|
|
580
807
|
}
|
|
581
|
-
var StoryGlobalsContext = createContext({});
|
|
582
808
|
|
|
583
809
|
// src/shell/revealTrial.ts
|
|
584
810
|
var FLASH_MS = 600;
|
|
@@ -605,33 +831,51 @@ function readRoute() {
|
|
|
605
831
|
if (!hash.startsWith("#/")) return null;
|
|
606
832
|
return decodeURIComponent(hash.slice(2)) || null;
|
|
607
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
|
+
}
|
|
608
857
|
function useRoute() {
|
|
609
|
-
const [route,
|
|
858
|
+
const [route, setCurrent] = useState(readRoute);
|
|
610
859
|
useEffect(() => {
|
|
611
|
-
const sync = () =>
|
|
860
|
+
const sync = () => setCurrent(readRoute());
|
|
612
861
|
window.addEventListener("hashchange", sync);
|
|
613
862
|
sync();
|
|
614
863
|
return () => window.removeEventListener("hashchange", sync);
|
|
615
864
|
}, []);
|
|
616
|
-
|
|
617
|
-
const hash = `#/${encodeURIComponent(id)}`;
|
|
618
|
-
if (readRoute() === id) history.replaceState(null, "", hash);
|
|
619
|
-
else location.hash = hash;
|
|
620
|
-
}, []);
|
|
621
|
-
return [route, go];
|
|
865
|
+
return [route, setRoute];
|
|
622
866
|
}
|
|
623
867
|
|
|
624
868
|
// src/shell/tree/buildTree.ts
|
|
625
869
|
function sortFolder(nodes) {
|
|
626
870
|
const folders = nodes.filter((n) => n.kind === "folder");
|
|
627
|
-
const stories = nodes.filter((n) => n.kind === "story");
|
|
628
871
|
folders.sort((a, b) => a.label.localeCompare(b.label));
|
|
629
872
|
for (const folder of folders) folder.children = sortFolder(folder.children);
|
|
630
|
-
return [...folders, ...
|
|
873
|
+
return [...folders, ...nodes.filter((n) => n.kind === "story")];
|
|
631
874
|
}
|
|
632
875
|
function buildTree(index) {
|
|
633
876
|
const root = [];
|
|
634
877
|
const folders = /* @__PURE__ */ new Map();
|
|
878
|
+
const pages = new Map(indexEntries(index).map((page) => [page.title, page]));
|
|
635
879
|
for (const entry of index) {
|
|
636
880
|
let siblings = root;
|
|
637
881
|
let path = "";
|
|
@@ -639,7 +883,8 @@ function buildTree(index) {
|
|
|
639
883
|
path = path ? `${path}/${label}` : label;
|
|
640
884
|
let folder = folders.get(path);
|
|
641
885
|
if (!folder) {
|
|
642
|
-
|
|
886
|
+
const page = pages.get(path);
|
|
887
|
+
folder = { kind: "folder", label, path, children: [], ...page ? { index: page } : {} };
|
|
643
888
|
folders.set(path, folder);
|
|
644
889
|
siblings.push(folder);
|
|
645
890
|
}
|
|
@@ -657,7 +902,9 @@ function filterTree(nodes, query) {
|
|
|
657
902
|
return `${node.entry.title}/${node.entry.name}`.toLowerCase().includes(needle) ? [node] : [];
|
|
658
903
|
}
|
|
659
904
|
const children = walk(node.children);
|
|
660
|
-
|
|
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 }];
|
|
661
908
|
});
|
|
662
909
|
return walk(nodes);
|
|
663
910
|
}
|
|
@@ -676,12 +923,21 @@ function ancestorsOf(entry) {
|
|
|
676
923
|
}
|
|
677
924
|
return paths;
|
|
678
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
|
+
}
|
|
679
935
|
function StoryTree({ ctx, index }) {
|
|
680
936
|
const headingId = useId();
|
|
681
|
-
const [route,
|
|
937
|
+
const [route, setRoute2] = useRoute();
|
|
682
938
|
const [query, setQuery] = useState("");
|
|
683
939
|
const [folds, setFolds] = usePersistedState("fg-tree-open", {}, { scope: "lab" });
|
|
684
|
-
const [view, setView] = usePersistedState("fg-tree-view", "
|
|
940
|
+
const [view, setView] = usePersistedState("fg-tree-view", "components", { scope: "lab" });
|
|
685
941
|
const [hidden, setHidden] = usePersistedState("fg-tree-libraries", {}, { scope: "lab" });
|
|
686
942
|
const [focusKey, setFocusKey] = useState(null);
|
|
687
943
|
const items = useRef(/* @__PURE__ */ new Map());
|
|
@@ -693,10 +949,11 @@ function StoryTree({ ctx, index }) {
|
|
|
693
949
|
() => view === "components" ? componentNodes(filterComponents(components, query)) : filterTree(tree, query),
|
|
694
950
|
[view, components, tree, query]
|
|
695
951
|
);
|
|
696
|
-
const
|
|
952
|
+
const routable = useMemo(() => [...kept, ...indexEntries(kept)], [kept]);
|
|
953
|
+
const routed = routable.find((entry) => entry.id === route);
|
|
697
954
|
const routeAncestors = useMemo(() => ancestorsOf(routed), [routed]);
|
|
698
955
|
const filtering = query.trim() !== "";
|
|
699
|
-
const isOpen = (path) => filtering || (folds[path] ??
|
|
956
|
+
const isOpen = (path) => filtering || (folds[path] ?? routeAncestors.has(path));
|
|
700
957
|
const rows = [];
|
|
701
958
|
const collect = (nodes, parent) => {
|
|
702
959
|
for (const node of nodes) {
|
|
@@ -719,8 +976,22 @@ function StoryTree({ ctx, index }) {
|
|
|
719
976
|
const focused = ctx.trials.find((trial) => trial.id === ctx.focusedTrialId);
|
|
720
977
|
if (another || !focused) ctx.addTrial(entry.id);
|
|
721
978
|
else if (focused.instrumentName === entry.id) revealTrial(focused.id);
|
|
722
|
-
else
|
|
723
|
-
|
|
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);
|
|
724
995
|
};
|
|
725
996
|
const onKeyDown = (event) => {
|
|
726
997
|
const at = rows.findIndex((row2) => row2.key === active);
|
|
@@ -751,8 +1022,8 @@ function StoryTree({ ctx, index }) {
|
|
|
751
1022
|
break;
|
|
752
1023
|
case "Enter":
|
|
753
1024
|
case " ":
|
|
754
|
-
if (node.kind === "
|
|
755
|
-
else
|
|
1025
|
+
if (node.kind === "story") activate(node.entry, event.shiftKey);
|
|
1026
|
+
else openFolder(node, event.shiftKey);
|
|
756
1027
|
break;
|
|
757
1028
|
default:
|
|
758
1029
|
return;
|
|
@@ -784,12 +1055,35 @@ function StoryTree({ ctx, index }) {
|
|
|
784
1055
|
...itemProps(node, level),
|
|
785
1056
|
"aria-expanded": open,
|
|
786
1057
|
"aria-label": node.label,
|
|
1058
|
+
"aria-current": node.index && openIds.has(node.index.id) ? "true" : void 0,
|
|
787
1059
|
className: "fg-tree__node",
|
|
788
1060
|
children: [
|
|
789
|
-
/* @__PURE__ */ jsxs(
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
+
),
|
|
793
1087
|
open ? /* @__PURE__ */ jsx("div", { role: "group", className: "fg-tree__group", children: renderNodes(node.children, level + 1) }) : null
|
|
794
1088
|
]
|
|
795
1089
|
},
|
|
@@ -812,7 +1106,7 @@ function StoryTree({ ctx, index }) {
|
|
|
812
1106
|
},
|
|
813
1107
|
children: [
|
|
814
1108
|
/* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: node.label ?? entry.name }),
|
|
815
|
-
node.tag ? /* @__PURE__ */ jsx(
|
|
1109
|
+
node.tag ? /* @__PURE__ */ jsx(LibraryBadge, { library: node.tag }) : null
|
|
816
1110
|
]
|
|
817
1111
|
},
|
|
818
1112
|
keyOf(node)
|
|
@@ -843,7 +1137,7 @@ function StoryTree({ ctx, index }) {
|
|
|
843
1137
|
{
|
|
844
1138
|
isSelected: !hidden[library],
|
|
845
1139
|
onChange: (on) => setHidden((prev) => ({ ...prev, [library]: !on })),
|
|
846
|
-
children: library
|
|
1140
|
+
children: /* @__PURE__ */ jsx(LibraryBadge, { library })
|
|
847
1141
|
},
|
|
848
1142
|
library
|
|
849
1143
|
))
|
|
@@ -905,10 +1199,90 @@ function createAnswerBook() {
|
|
|
905
1199
|
}
|
|
906
1200
|
};
|
|
907
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
|
+
}
|
|
908
1281
|
|
|
909
1282
|
// src/shell/readyKey.ts
|
|
910
1283
|
var readyKey = (ready) => stableStringify({ schema: ready.schema, layout: ready.layout, viewport: ready.viewport });
|
|
911
1284
|
var START_TIMEOUT_MS = 1e4;
|
|
1285
|
+
var HELLO_TIMEOUT_MS = 3e4;
|
|
912
1286
|
var requests = 0;
|
|
913
1287
|
var IN_VIEW_MARGIN = "50%";
|
|
914
1288
|
function useInView(ref) {
|
|
@@ -952,13 +1326,17 @@ function FrameView(props) {
|
|
|
952
1326
|
const trialId = useContext(TrialIdContext);
|
|
953
1327
|
const frames = useContext(TrialFramesContext);
|
|
954
1328
|
const [overrides] = useCssOverrides();
|
|
1329
|
+
const pool = useContext(FramePoolContext);
|
|
1330
|
+
const lab = useLabContext();
|
|
955
1331
|
const src = `${frameUrl}#${entry.id}`;
|
|
1332
|
+
const title = breadcrumb(entry.title, entry.name);
|
|
956
1333
|
const iframeRef = useRef(null);
|
|
957
1334
|
const hostRef = useRef(null);
|
|
1335
|
+
const slotRef = useRef(null);
|
|
958
1336
|
const inView = useInView(hostRef);
|
|
959
1337
|
const link = useRef(null);
|
|
960
|
-
const latest = useRef({ ...props, globals, overrides });
|
|
961
|
-
latest.current = { ...props, globals, overrides };
|
|
1338
|
+
const latest = useRef({ ...props, globals, overrides, pool, lab, title });
|
|
1339
|
+
latest.current = { ...props, globals, overrides, pool, lab, title };
|
|
962
1340
|
const [fault, setFault] = useState(null);
|
|
963
1341
|
const [pending, setPending] = useState(true);
|
|
964
1342
|
const init = (current) => {
|
|
@@ -1015,6 +1393,13 @@ function FrameView(props) {
|
|
|
1015
1393
|
else current.pending.get(msg.id)?.fail(new Error(msg.message));
|
|
1016
1394
|
current.pending.delete(msg.id);
|
|
1017
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
|
+
}
|
|
1018
1403
|
case "fault":
|
|
1019
1404
|
if (msg.phase === "render" && msg.seq !== void 0 && msg.seq < current.inputs) break;
|
|
1020
1405
|
clearTimeout(current.timer);
|
|
@@ -1022,10 +1407,10 @@ function FrameView(props) {
|
|
|
1022
1407
|
break;
|
|
1023
1408
|
}
|
|
1024
1409
|
};
|
|
1025
|
-
const
|
|
1410
|
+
const connect = (frame) => {
|
|
1026
1411
|
closeLink(link);
|
|
1027
1412
|
setPending(true);
|
|
1028
|
-
const target =
|
|
1413
|
+
const target = frame.contentWindow;
|
|
1029
1414
|
if (!target) return;
|
|
1030
1415
|
const { port1, port2 } = new MessageChannel();
|
|
1031
1416
|
const timer = setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), START_TIMEOUT_MS);
|
|
@@ -1077,15 +1462,53 @@ function FrameView(props) {
|
|
|
1077
1462
|
};
|
|
1078
1463
|
link.current = current;
|
|
1079
1464
|
channel.on((msg) => receive(current, msg));
|
|
1080
|
-
target.postMessage({ type: PORT_HANDOFF }, location.origin, [
|
|
1465
|
+
target.postMessage({ type: PORT_HANDOFF, id: latest.current.entry.id }, location.origin, [
|
|
1466
|
+
port2
|
|
1467
|
+
]);
|
|
1081
1468
|
};
|
|
1082
|
-
|
|
1469
|
+
const connectRef = useRef(connect);
|
|
1470
|
+
connectRef.current = connect;
|
|
1083
1471
|
useEffect(() => {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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]);
|
|
1089
1512
|
useEffect(() => answers.hold(ctx.config), [answers, ctx.config]);
|
|
1090
1513
|
useEffect(() => {
|
|
1091
1514
|
const current = link.current;
|
|
@@ -1127,17 +1550,7 @@ function FrameView(props) {
|
|
|
1127
1550
|
},
|
|
1128
1551
|
className: "fg-frame-host",
|
|
1129
1552
|
children: [
|
|
1130
|
-
|
|
1131
|
-
"iframe",
|
|
1132
|
-
{
|
|
1133
|
-
ref: iframeRef,
|
|
1134
|
-
className: "fg-frame-view",
|
|
1135
|
-
src,
|
|
1136
|
-
title: `${entry.title} / ${entry.name}`,
|
|
1137
|
-
onLoad,
|
|
1138
|
-
"data-pending": pending && !fault ? "" : void 0
|
|
1139
|
-
}
|
|
1140
|
-
) : null,
|
|
1553
|
+
/* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
|
|
1141
1554
|
fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
|
|
1142
1555
|
fault.phase ? /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: fault.phase }) : null,
|
|
1143
1556
|
/* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
|
|
@@ -1146,7 +1559,7 @@ function FrameView(props) {
|
|
|
1146
1559
|
}
|
|
1147
1560
|
);
|
|
1148
1561
|
}
|
|
1149
|
-
function
|
|
1562
|
+
function annotationsOn2(frames) {
|
|
1150
1563
|
return {
|
|
1151
1564
|
targets: (_state, _config, trial) => {
|
|
1152
1565
|
const ref = frames.hostRef(trial.id);
|
|
@@ -1173,7 +1586,7 @@ function storyInstrument(options) {
|
|
|
1173
1586
|
const descriptionKey = ready ? readyKey(ready) : null;
|
|
1174
1587
|
return {
|
|
1175
1588
|
name: entry.id,
|
|
1176
|
-
title:
|
|
1589
|
+
title: breadcrumb(entry.title, entry.name),
|
|
1177
1590
|
config,
|
|
1178
1591
|
defaultConfig: () => config.defaults(),
|
|
1179
1592
|
initialState: () => null,
|
|
@@ -1188,17 +1601,22 @@ function storyInstrument(options) {
|
|
|
1188
1601
|
ctx
|
|
1189
1602
|
}
|
|
1190
1603
|
),
|
|
1191
|
-
...frames ? { annotations:
|
|
1604
|
+
...frames ? { annotations: annotationsOn2(frames) } : {},
|
|
1192
1605
|
...ready?.viewport ? { stage: { size: { width: ready.viewport.width, height: ready.viewport.height } } } : {}
|
|
1193
1606
|
};
|
|
1194
1607
|
}
|
|
1195
1608
|
|
|
1196
1609
|
// src/shell/useStoryRegistry.ts
|
|
1197
|
-
var
|
|
1610
|
+
var NO_GLOBALS2 = {};
|
|
1611
|
+
var NO_SETUP = {};
|
|
1198
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));
|
|
1199
1614
|
function useStoryRegistry(index, options) {
|
|
1200
|
-
const { frameUrl, globals =
|
|
1615
|
+
const { frameUrl, globals = NO_GLOBALS2, frames, importers, setup = NO_SETUP, load = loadStories } = options;
|
|
1201
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());
|
|
1202
1620
|
const [revisions, setRevisions] = useState(() => /* @__PURE__ */ new Map());
|
|
1203
1621
|
const onReady = useCallback((entry, ready) => {
|
|
1204
1622
|
setReadies((prev) => {
|
|
@@ -1210,8 +1628,93 @@ function useStoryRegistry(index, options) {
|
|
|
1210
1628
|
useEffect(() => {
|
|
1211
1629
|
const ids = new Set(index.map((entry) => entry.id));
|
|
1212
1630
|
setReadies((prev) => pruned(prev, ids));
|
|
1631
|
+
setLoaded((prev) => pruned(prev, ids));
|
|
1632
|
+
setFaults((prev) => pruned(prev, ids));
|
|
1213
1633
|
setRevisions((prev) => pruned(prev, ids));
|
|
1214
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
|
+
);
|
|
1215
1718
|
const committed = useRef({ built: /* @__PURE__ */ new Map(), books: /* @__PURE__ */ new Map(), list: [] });
|
|
1216
1719
|
const cache = useMemo(() => {
|
|
1217
1720
|
const previous = committed.current;
|
|
@@ -1219,26 +1722,35 @@ function useStoryRegistry(index, options) {
|
|
|
1219
1722
|
const books = /* @__PURE__ */ new Map();
|
|
1220
1723
|
const list = index.map((entry) => {
|
|
1221
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;
|
|
1222
1729
|
const revision = revisions.get(entry.id) ?? 0;
|
|
1223
1730
|
const entryKey = stableStringify(entry);
|
|
1224
1731
|
const answers = previous.books.get(entry.id) ?? createAnswerBook();
|
|
1225
1732
|
books.set(entry.id, answers);
|
|
1226
1733
|
const held = previous.built.get(entry.id);
|
|
1227
|
-
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 : {
|
|
1228
1735
|
entryKey,
|
|
1229
1736
|
ready,
|
|
1737
|
+
loaded: had,
|
|
1738
|
+
fault,
|
|
1230
1739
|
frameUrl,
|
|
1231
1740
|
globals,
|
|
1232
1741
|
frames,
|
|
1742
|
+
setup,
|
|
1743
|
+
inDocument,
|
|
1744
|
+
generation,
|
|
1233
1745
|
revision,
|
|
1234
|
-
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 } : {} })
|
|
1235
1747
|
};
|
|
1236
1748
|
built.set(entry.id, kept);
|
|
1237
1749
|
return kept.instrument;
|
|
1238
1750
|
});
|
|
1239
1751
|
const unchanged = list.length === previous.list.length && list.every((i, n) => i === previous.list[n]);
|
|
1240
1752
|
return { built, books, list: unchanged ? previous.list : list };
|
|
1241
|
-
}, [index, readies, revisions, frameUrl, globals, frames, onReady]);
|
|
1753
|
+
}, [index, readies, loaded, faults, generations, revisions, frameUrl, globals, frames, setup, importers, onReady, request]);
|
|
1242
1754
|
useLayoutEffect(() => {
|
|
1243
1755
|
committed.current = cache;
|
|
1244
1756
|
}, [cache]);
|
|
@@ -1250,22 +1762,29 @@ function useStoryRegistry(index, options) {
|
|
|
1250
1762
|
for (const off of offs) off();
|
|
1251
1763
|
};
|
|
1252
1764
|
}, [cache.books]);
|
|
1253
|
-
const isReady = useCallback((id) => readies.has(id), [readies]);
|
|
1254
|
-
return { instruments: cache.list, isReady, onReady };
|
|
1765
|
+
const isReady = useCallback((id) => readies.has(id) || loaded.has(id), [readies, loaded]);
|
|
1766
|
+
return { instruments: cache.list, isReady, onReady, reload };
|
|
1255
1767
|
}
|
|
1256
1768
|
var NO_DECLARATIONS = {};
|
|
1257
|
-
function initialStory(
|
|
1769
|
+
function initialStory(entries, fallback) {
|
|
1258
1770
|
const route = readRoute();
|
|
1259
|
-
return route !== null &&
|
|
1771
|
+
return route !== null && entries.some((entry) => entry.id === route) ? route : fallback;
|
|
1260
1772
|
}
|
|
1261
1773
|
function RouteOpener({ index }) {
|
|
1262
1774
|
const lab = useLabContext();
|
|
1263
1775
|
const [route] = useRoute();
|
|
1264
1776
|
const handled = useRef(null);
|
|
1777
|
+
const last = useRef({ route: readRoute(), entry: readRouteEntry() });
|
|
1265
1778
|
useEffect(() => {
|
|
1266
|
-
|
|
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;
|
|
1267
1782
|
handled.current = route;
|
|
1268
|
-
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);
|
|
1269
1788
|
}, [route, index, lab]);
|
|
1270
1789
|
return null;
|
|
1271
1790
|
}
|
|
@@ -1283,11 +1802,33 @@ function useInfoShortcut(open) {
|
|
|
1283
1802
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
1284
1803
|
}, [open]);
|
|
1285
1804
|
}
|
|
1286
|
-
function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }) {
|
|
1805
|
+
function Workshop({ index, frameUrl, importers, setup, changes, config, stories = [], storageKey, storage }) {
|
|
1287
1806
|
const declarations = config?.globals ?? NO_DECLARATIONS;
|
|
1288
1807
|
const [frames] = useState(createTrialFrames);
|
|
1289
|
-
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]);
|
|
1290
1829
|
const [labValues, setLabValues] = useState(() => labGlobals(declarations, void 0));
|
|
1830
|
+
const themeFor = config?.labTheme;
|
|
1831
|
+
const labTheme = useMemo(() => themeFor?.(labValues), [themeFor, labValues]);
|
|
1291
1832
|
const [infoOpen, setInfoOpen] = useState(false);
|
|
1292
1833
|
useInfoShortcut(setInfoOpen);
|
|
1293
1834
|
const reportLabValues = useCallback(
|
|
@@ -1316,13 +1857,14 @@ function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }
|
|
|
1316
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
|
|
1317
1858
|
] });
|
|
1318
1859
|
}
|
|
1319
|
-
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(
|
|
1320
1861
|
Lab,
|
|
1321
1862
|
{
|
|
1322
1863
|
title: "weaselforge",
|
|
1323
1864
|
density: "roomy",
|
|
1865
|
+
...labTheme ? { theme: labTheme } : {},
|
|
1324
1866
|
instruments: registry.instruments,
|
|
1325
|
-
defaultInstrument: initialStory(
|
|
1867
|
+
defaultInstrument: initialStory(entries, first.id),
|
|
1326
1868
|
storageKey: storageKey ?? "weaselforge",
|
|
1327
1869
|
...storage ? { storage } : {},
|
|
1328
1870
|
labChrome,
|
|
@@ -1331,7 +1873,7 @@ function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }
|
|
|
1331
1873
|
...config?.pages ? { pages: config.pages } : {},
|
|
1332
1874
|
...config?.path !== void 0 ? { path: config.path } : {},
|
|
1333
1875
|
children: [
|
|
1334
|
-
/* @__PURE__ */ jsx(RouteOpener, { index }),
|
|
1876
|
+
/* @__PURE__ */ jsx(RouteOpener, { index: entries }),
|
|
1335
1877
|
/* @__PURE__ */ jsx(
|
|
1336
1878
|
StoryInfoDialog,
|
|
1337
1879
|
{
|
|
@@ -1344,23 +1886,29 @@ function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }
|
|
|
1344
1886
|
/* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues })
|
|
1345
1887
|
]
|
|
1346
1888
|
}
|
|
1347
|
-
) }) });
|
|
1889
|
+
) }) }) });
|
|
1348
1890
|
}
|
|
1349
1891
|
function mountWorkshop({ container, ...props }) {
|
|
1350
1892
|
const target = container ?? document.getElementById("root");
|
|
1351
1893
|
if (!target) throw new Error("mountWorkshop: no container given and no #root element");
|
|
1352
1894
|
const root = createRoot(target);
|
|
1353
|
-
|
|
1895
|
+
const changes = createStoryChanges();
|
|
1896
|
+
let current = { ...props, changes };
|
|
1354
1897
|
root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
|
|
1355
1898
|
return {
|
|
1356
1899
|
setIndex(index) {
|
|
1357
1900
|
current = { ...current, index };
|
|
1358
1901
|
root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
|
|
1359
1902
|
},
|
|
1903
|
+
setImporters(importers) {
|
|
1904
|
+
current = { ...current, importers };
|
|
1905
|
+
root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
|
|
1906
|
+
},
|
|
1907
|
+
reloadStory: (file) => changes.emit(file),
|
|
1360
1908
|
unmount: () => root.unmount()
|
|
1361
1909
|
};
|
|
1362
1910
|
}
|
|
1363
1911
|
|
|
1364
|
-
export { A11Y_SECTION, A11yPanel,
|
|
1912
|
+
export { A11Y_SECTION, A11yPanel, Workshop, mountWorkshop, useStoryRegistry };
|
|
1365
1913
|
//# sourceMappingURL=index.js.map
|
|
1366
1914
|
//# sourceMappingURL=index.js.map
|