@weasel-js/forge 1.5.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/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/FrameController-Ba3LaWfq.d.ts +196 -0
- package/dist/chunk-2GXKNXTJ.js +15 -0
- package/dist/chunk-2GXKNXTJ.js.map +1 -0
- package/dist/chunk-IOBG7GZQ.js +13 -0
- package/dist/chunk-IOBG7GZQ.js.map +1 -0
- package/dist/chunk-KKF7ZX5E.js +572 -0
- package/dist/chunk-KKF7ZX5E.js.map +1 -0
- package/dist/chunk-KKVTFMWV.js +59 -0
- package/dist/chunk-KKVTFMWV.js.map +1 -0
- package/dist/chunk-O5OQZQRZ.js +118 -0
- package/dist/chunk-O5OQZQRZ.js.map +1 -0
- package/dist/chunk-UZCXH724.js +24 -0
- package/dist/chunk-UZCXH724.js.map +1 -0
- package/dist/cli.js +107 -0
- package/dist/config-DdumOfyd.d.ts +28 -0
- package/dist/csf/preview-api.d.ts +9 -0
- package/dist/csf/preview-api.js +48 -0
- package/dist/csf/preview-api.js.map +1 -0
- package/dist/frame/index.css +35 -0
- package/dist/frame/index.css.map +1 -0
- package/dist/frame/index.d.ts +24 -0
- package/dist/frame/index.js +8 -0
- package/dist/frame/index.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/shell/index.css +198 -0
- package/dist/shell/index.css.map +1 -0
- package/dist/shell/index.d.ts +51 -0
- package/dist/shell/index.js +836 -0
- package/dist/shell/index.js.map +1 -0
- package/dist/test/index.d.ts +17 -0
- package/dist/test/index.js +132 -0
- package/dist/test/index.js.map +1 -0
- package/dist/vite/index.d.ts +29 -0
- package/dist/vite/index.js +392 -0
- package/dist/vite/index.js.map +1 -0
- package/package.json +87 -0
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
import { schemaFromDescription, openChannel } from '../chunk-O5OQZQRZ.js';
|
|
2
|
+
import { stableStringify, PORT_HANDOFF, PROTOCOL_VERSION } from '../chunk-IOBG7GZQ.js';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { PropertyList, TextRow, ColorRow, Button, Lab, usePersistedState, useLabContext, TrialIdContext, useTrialId, ToggleBar } from '@weasel-js/labkit';
|
|
5
|
+
import { createContext, memo, useMemo, useCallback, useState, useEffect, useRef, useLayoutEffect, useId, useContext, useSyncExternalStore } from 'react';
|
|
6
|
+
import { TOKEN_MANIFEST } from '@weasel-js/theme';
|
|
7
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
8
|
+
import { f } from '@weasel-js/labkit/config';
|
|
9
|
+
|
|
10
|
+
// src/shell/cssVars/color.ts
|
|
11
|
+
var HEX = /^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i;
|
|
12
|
+
var RGB = /^rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})\b/i;
|
|
13
|
+
var COLOR_FUNCTION = /^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color|color-mix)\(.+\)$/i;
|
|
14
|
+
var KEYWORDS = /* @__PURE__ */ new Set([
|
|
15
|
+
"transparent",
|
|
16
|
+
"currentcolor",
|
|
17
|
+
"black",
|
|
18
|
+
"white",
|
|
19
|
+
"gray",
|
|
20
|
+
"silver",
|
|
21
|
+
"red",
|
|
22
|
+
"orange",
|
|
23
|
+
"yellow",
|
|
24
|
+
"gold",
|
|
25
|
+
"lime",
|
|
26
|
+
"green",
|
|
27
|
+
"teal",
|
|
28
|
+
"cyan",
|
|
29
|
+
"blue",
|
|
30
|
+
"navy",
|
|
31
|
+
"purple",
|
|
32
|
+
"magenta",
|
|
33
|
+
"pink",
|
|
34
|
+
"brown"
|
|
35
|
+
]);
|
|
36
|
+
function browserSupports() {
|
|
37
|
+
return typeof CSS !== "undefined" && typeof CSS.supports === "function" ? (value) => CSS.supports("color", value) : null;
|
|
38
|
+
}
|
|
39
|
+
function parsesAsColor(value, supports = browserSupports()) {
|
|
40
|
+
const v = value.trim();
|
|
41
|
+
if (!v || v.includes("var(")) return false;
|
|
42
|
+
if (supports) return supports(v);
|
|
43
|
+
return HEX.test(v) || COLOR_FUNCTION.test(v) || KEYWORDS.has(v.toLowerCase());
|
|
44
|
+
}
|
|
45
|
+
function hexOf(v) {
|
|
46
|
+
if (HEX.test(v)) {
|
|
47
|
+
const digits = v.slice(1).toLowerCase();
|
|
48
|
+
return `#${digits.length <= 4 ? [...digits.slice(0, 3)].map((d) => d + d).join("") : digits.slice(0, 6)}`;
|
|
49
|
+
}
|
|
50
|
+
const rgb = RGB.exec(v);
|
|
51
|
+
return rgb ? `#${rgb.slice(1, 4).map((n) => Math.min(255, Number(n)).toString(16).padStart(2, "0")).join("")}` : null;
|
|
52
|
+
}
|
|
53
|
+
var paint;
|
|
54
|
+
function serialized(value) {
|
|
55
|
+
if (paint === void 0) {
|
|
56
|
+
try {
|
|
57
|
+
paint = document.createElement("canvas").getContext("2d");
|
|
58
|
+
} catch {
|
|
59
|
+
paint = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!paint) return null;
|
|
63
|
+
paint.fillStyle = "#000000";
|
|
64
|
+
paint.fillStyle = value;
|
|
65
|
+
const onBlack = String(paint.fillStyle);
|
|
66
|
+
paint.fillStyle = "#ffffff";
|
|
67
|
+
paint.fillStyle = value;
|
|
68
|
+
return onBlack === String(paint.fillStyle) ? onBlack : null;
|
|
69
|
+
}
|
|
70
|
+
function toHex(value) {
|
|
71
|
+
const v = value.trim();
|
|
72
|
+
const direct = hexOf(v);
|
|
73
|
+
if (direct) return direct;
|
|
74
|
+
const browser = typeof document === "undefined" ? null : serialized(v);
|
|
75
|
+
return browser && browser !== v ? hexOf(browser) : null;
|
|
76
|
+
}
|
|
77
|
+
var NONE = {};
|
|
78
|
+
function useCssOverrides() {
|
|
79
|
+
return usePersistedState("fg-css-overrides", NONE, { scope: "trial" });
|
|
80
|
+
}
|
|
81
|
+
var NO_FRAME = { send: null, vars: [] };
|
|
82
|
+
function createTrialFrames() {
|
|
83
|
+
const frames = /* @__PURE__ */ new Map();
|
|
84
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
85
|
+
const changed = () => {
|
|
86
|
+
for (const listener of [...listeners]) listener();
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
connect(trialId, send) {
|
|
90
|
+
frames.set(trialId, { send, vars: [] });
|
|
91
|
+
changed();
|
|
92
|
+
return () => {
|
|
93
|
+
if (frames.get(trialId)?.send !== send) return;
|
|
94
|
+
frames.delete(trialId);
|
|
95
|
+
changed();
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
report(trialId, vars) {
|
|
99
|
+
const frame = frames.get(trialId);
|
|
100
|
+
if (!frame) return;
|
|
101
|
+
frames.set(trialId, { ...frame, vars });
|
|
102
|
+
changed();
|
|
103
|
+
},
|
|
104
|
+
get: (trialId) => frames.get(trialId) ?? NO_FRAME,
|
|
105
|
+
subscribe(listener) {
|
|
106
|
+
listeners.add(listener);
|
|
107
|
+
return () => listeners.delete(listener);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
var TrialFramesContext = createContext(null);
|
|
112
|
+
function useTrialFrame(trialId) {
|
|
113
|
+
const frames = useContext(TrialFramesContext);
|
|
114
|
+
const subscribe = useCallback((listener) => frames?.subscribe(listener) ?? (() => {
|
|
115
|
+
}), [frames]);
|
|
116
|
+
const read = () => frames?.get(trialId) ?? NO_FRAME;
|
|
117
|
+
return useSyncExternalStore(subscribe, read, read);
|
|
118
|
+
}
|
|
119
|
+
var TABS = [
|
|
120
|
+
{ value: "theme", label: "Theme" },
|
|
121
|
+
{ value: "story", label: "Story" }
|
|
122
|
+
];
|
|
123
|
+
var VarRow = memo(function VarRow2({ name, value, overridden, write }) {
|
|
124
|
+
const hex = useMemo(() => parsesAsColor(value) ? toHex(value) : null, [value]);
|
|
125
|
+
const onChange = useCallback((next) => write(name, next), [write, name]);
|
|
126
|
+
const reset = useCallback(() => write(name, null), [write, name]);
|
|
127
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-css-var", role: "group", "aria-label": name, children: [
|
|
128
|
+
/* @__PURE__ */ jsxs(PropertyList, { className: "fg-css-var__rows", pack: "one-up", density: "tight", children: [
|
|
129
|
+
/* @__PURE__ */ jsx(TextRow, { label: name, value, onChange }),
|
|
130
|
+
hex ? /* @__PURE__ */ jsx(ColorRow, { label: "Color", value: hex, onChange }) : null
|
|
131
|
+
] }),
|
|
132
|
+
overridden ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", ariaLabel: `Reset ${name}`, onClick: reset, children: "Reset" }) : null
|
|
133
|
+
] });
|
|
134
|
+
});
|
|
135
|
+
function CssVarsPanel() {
|
|
136
|
+
const trialId = useTrialId();
|
|
137
|
+
const frame = useTrialFrame(trialId);
|
|
138
|
+
const [overrides, setOverrides] = useCssOverrides();
|
|
139
|
+
const [tab, setTab] = useState("theme");
|
|
140
|
+
const [filter, setFilter] = useState("");
|
|
141
|
+
const rows = useMemo(() => {
|
|
142
|
+
if (tab === "story") return frame.vars.map(({ name, value }) => ({ name, value }));
|
|
143
|
+
const reported = new Map(frame.vars.map((v) => [v.name, v.value]));
|
|
144
|
+
return TOKEN_MANIFEST.map((token) => ({ name: token.name, value: reported.get(token.name) ?? token.defaultValue }));
|
|
145
|
+
}, [tab, frame.vars]);
|
|
146
|
+
const query = filter.trim().toLowerCase();
|
|
147
|
+
const shown = rows.map((row) => ({ ...row, value: overrides[row.name] ?? row.value })).filter((row) => !query || row.name.toLowerCase().includes(query) || row.value.toLowerCase().includes(query));
|
|
148
|
+
const send = frame.send;
|
|
149
|
+
const write = useCallback(
|
|
150
|
+
(name, value) => {
|
|
151
|
+
setOverrides((prev) => {
|
|
152
|
+
const { [name]: _dropped, ...rest } = prev;
|
|
153
|
+
return value === null ? rest : { ...rest, [name]: value };
|
|
154
|
+
});
|
|
155
|
+
send?.({ type: "vars.set", name, value });
|
|
156
|
+
},
|
|
157
|
+
[setOverrides, send]
|
|
158
|
+
);
|
|
159
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-css-vars", children: [
|
|
160
|
+
/* @__PURE__ */ jsx(
|
|
161
|
+
ToggleBar,
|
|
162
|
+
{
|
|
163
|
+
ariaLabel: "Variables",
|
|
164
|
+
size: "sm",
|
|
165
|
+
items: TABS,
|
|
166
|
+
value: tab,
|
|
167
|
+
onChange: (next) => {
|
|
168
|
+
if (next) setTab(next);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
),
|
|
172
|
+
/* @__PURE__ */ jsx(PropertyList, { pack: "one-up", density: "tight", children: /* @__PURE__ */ jsx(TextRow, { label: "Filter", value: filter, placeholder: "Name or value", onChange: setFilter }) }),
|
|
173
|
+
shown.length === 0 ? /* @__PURE__ */ jsx("p", { className: "fg-css-vars__empty", children: rows.length === 0 ? "The story\u2019s frame has reported no variables." : "No variables match." }) : /* @__PURE__ */ jsx("div", { className: "fg-css-vars__list", children: shown.map((row) => /* @__PURE__ */ jsx(
|
|
174
|
+
VarRow,
|
|
175
|
+
{
|
|
176
|
+
name: row.name,
|
|
177
|
+
value: row.value,
|
|
178
|
+
overridden: row.name in overrides,
|
|
179
|
+
write
|
|
180
|
+
},
|
|
181
|
+
row.name
|
|
182
|
+
)) })
|
|
183
|
+
] });
|
|
184
|
+
}
|
|
185
|
+
var CSS_VARS_SECTION = {
|
|
186
|
+
id: "fg-css-vars",
|
|
187
|
+
region: "sidebar",
|
|
188
|
+
item: { title: "CSS Vars", body: /* @__PURE__ */ jsx(CssVarsPanel, {}) }
|
|
189
|
+
};
|
|
190
|
+
var GLOBALS_KEY = "$globals";
|
|
191
|
+
var FOLLOW_LAB = "lab";
|
|
192
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
193
|
+
var isGlobalsPath = (path) => path === GLOBALS_KEY || path.startsWith(`${GLOBALS_KEY}.`);
|
|
194
|
+
function labGlobals(declarations, stored) {
|
|
195
|
+
const from = isRecord(stored) ? stored : {};
|
|
196
|
+
return Object.fromEntries(
|
|
197
|
+
Object.entries(declarations).map(([key, declaration]) => {
|
|
198
|
+
const value = from[key];
|
|
199
|
+
return [key, declaration.options.some((option) => option.value === value) ? value : declaration.default];
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
function effectiveGlobals(lab, pins) {
|
|
204
|
+
if (!isRecord(pins)) return lab;
|
|
205
|
+
const pinned = Object.entries(pins).filter(([key, value]) => key in lab && value !== FOLLOW_LAB);
|
|
206
|
+
return pinned.length === 0 ? lab : { ...lab, ...Object.fromEntries(pinned) };
|
|
207
|
+
}
|
|
208
|
+
function storyConfig(config, previous) {
|
|
209
|
+
if (!isRecord(config) || !(GLOBALS_KEY in config)) return config;
|
|
210
|
+
const rest = Object.fromEntries(Object.entries(config).filter(([key]) => key !== GLOBALS_KEY));
|
|
211
|
+
if (isRecord(previous)) {
|
|
212
|
+
const keys = Object.keys(rest);
|
|
213
|
+
if (keys.length === Object.keys(previous).length && keys.every((key) => rest[key] === previous[key])) return previous;
|
|
214
|
+
}
|
|
215
|
+
return rest;
|
|
216
|
+
}
|
|
217
|
+
function withGlobals(schema, declarations) {
|
|
218
|
+
const entries = Object.entries(declarations);
|
|
219
|
+
if (entries.length === 0) return schema;
|
|
220
|
+
const leaves = Object.fromEntries(
|
|
221
|
+
entries.map(([key, declaration]) => [
|
|
222
|
+
key,
|
|
223
|
+
f.enum(FOLLOW_LAB, [{ value: FOLLOW_LAB, label: "Lab" }, ...declaration.options]).label(declaration.label)
|
|
224
|
+
])
|
|
225
|
+
);
|
|
226
|
+
const group = f.group(leaves).label("").section("Globals");
|
|
227
|
+
return f.schema({ ...schema.nodes, [GLOBALS_KEY]: group });
|
|
228
|
+
}
|
|
229
|
+
var RECORD = "fg-globals";
|
|
230
|
+
function useLabGlobals(declarations) {
|
|
231
|
+
const [stored, setStored] = usePersistedState(RECORD, null, { scope: "lab" });
|
|
232
|
+
const values = useMemo(() => labGlobals(declarations, stored), [declarations, stored]);
|
|
233
|
+
const set = useCallback(
|
|
234
|
+
(key, value) => setStored((prev) => ({ ...labGlobals(declarations, prev), [key]: value })),
|
|
235
|
+
[declarations, setStored]
|
|
236
|
+
);
|
|
237
|
+
return [values, set];
|
|
238
|
+
}
|
|
239
|
+
function GlobalsToolbar({ declarations }) {
|
|
240
|
+
const [values, set] = useLabGlobals(declarations);
|
|
241
|
+
return /* @__PURE__ */ jsx("div", { className: "fg-globals", role: "toolbar", "aria-label": "Globals", children: Object.entries(declarations).map(([key, declaration]) => /* @__PURE__ */ jsx(
|
|
242
|
+
"select",
|
|
243
|
+
{
|
|
244
|
+
className: "fg-globals__select",
|
|
245
|
+
"aria-label": declaration.label,
|
|
246
|
+
value: String(values[key]),
|
|
247
|
+
onChange: (event) => set(key, event.target.value),
|
|
248
|
+
children: declaration.options.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
|
|
249
|
+
},
|
|
250
|
+
key
|
|
251
|
+
)) });
|
|
252
|
+
}
|
|
253
|
+
function LabGlobals({
|
|
254
|
+
declarations,
|
|
255
|
+
onChange
|
|
256
|
+
}) {
|
|
257
|
+
const [values] = useLabGlobals(declarations);
|
|
258
|
+
useEffect(() => onChange(values), [values, onChange]);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
var StoryGlobalsContext = createContext({});
|
|
262
|
+
|
|
263
|
+
// src/shell/revealTrial.ts
|
|
264
|
+
var FLASH_MS = 600;
|
|
265
|
+
var flashes = /* @__PURE__ */ new WeakMap();
|
|
266
|
+
function revealTrial(trialId, root = document) {
|
|
267
|
+
const trial = root.querySelector(`.lk-trial[data-trial-id="${CSS.escape(trialId)}"]`);
|
|
268
|
+
if (!trial) return false;
|
|
269
|
+
trial.scrollIntoView({ block: "nearest" });
|
|
270
|
+
clearTimeout(flashes.get(trial));
|
|
271
|
+
trial.classList.remove("fg-flash");
|
|
272
|
+
void trial.offsetWidth;
|
|
273
|
+
trial.classList.add("fg-flash");
|
|
274
|
+
flashes.set(
|
|
275
|
+
trial,
|
|
276
|
+
setTimeout(() => {
|
|
277
|
+
trial.classList.remove("fg-flash");
|
|
278
|
+
flashes.delete(trial);
|
|
279
|
+
}, FLASH_MS)
|
|
280
|
+
);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
function readRoute() {
|
|
284
|
+
const { hash } = location;
|
|
285
|
+
if (!hash.startsWith("#/")) return null;
|
|
286
|
+
return decodeURIComponent(hash.slice(2)) || null;
|
|
287
|
+
}
|
|
288
|
+
function useRoute() {
|
|
289
|
+
const [route, setRoute] = useState(readRoute);
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
const sync = () => setRoute(readRoute());
|
|
292
|
+
window.addEventListener("hashchange", sync);
|
|
293
|
+
sync();
|
|
294
|
+
return () => window.removeEventListener("hashchange", sync);
|
|
295
|
+
}, []);
|
|
296
|
+
const go = useCallback((id) => {
|
|
297
|
+
const hash = `#/${encodeURIComponent(id)}`;
|
|
298
|
+
if (readRoute() === id) history.replaceState(null, "", hash);
|
|
299
|
+
else location.hash = hash;
|
|
300
|
+
}, []);
|
|
301
|
+
return [route, go];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/shell/tree/buildTree.ts
|
|
305
|
+
function sortFolder(nodes) {
|
|
306
|
+
const folders = nodes.filter((n) => n.kind === "folder");
|
|
307
|
+
const stories = nodes.filter((n) => n.kind === "story");
|
|
308
|
+
folders.sort((a, b) => a.label.localeCompare(b.label));
|
|
309
|
+
for (const folder of folders) folder.children = sortFolder(folder.children);
|
|
310
|
+
return [...folders, ...stories];
|
|
311
|
+
}
|
|
312
|
+
function buildTree(index) {
|
|
313
|
+
const root = [];
|
|
314
|
+
const folders = /* @__PURE__ */ new Map();
|
|
315
|
+
for (const entry of index) {
|
|
316
|
+
let siblings = root;
|
|
317
|
+
let path = "";
|
|
318
|
+
for (const label of entry.title.split("/")) {
|
|
319
|
+
path = path ? `${path}/${label}` : label;
|
|
320
|
+
let folder = folders.get(path);
|
|
321
|
+
if (!folder) {
|
|
322
|
+
folder = { kind: "folder", label, path, children: [] };
|
|
323
|
+
folders.set(path, folder);
|
|
324
|
+
siblings.push(folder);
|
|
325
|
+
}
|
|
326
|
+
siblings = folder.children;
|
|
327
|
+
}
|
|
328
|
+
siblings.push({ kind: "story", entry });
|
|
329
|
+
}
|
|
330
|
+
return sortFolder(root);
|
|
331
|
+
}
|
|
332
|
+
function filterTree(nodes, query) {
|
|
333
|
+
const needle = query.trim().toLowerCase();
|
|
334
|
+
if (!needle) return nodes;
|
|
335
|
+
const walk = (list) => list.flatMap((node) => {
|
|
336
|
+
if (node.kind === "story") {
|
|
337
|
+
return `${node.entry.title}/${node.entry.name}`.toLowerCase().includes(needle) ? [node] : [];
|
|
338
|
+
}
|
|
339
|
+
const children = walk(node.children);
|
|
340
|
+
return children.length > 0 ? [{ ...node, children }] : [];
|
|
341
|
+
});
|
|
342
|
+
return walk(nodes);
|
|
343
|
+
}
|
|
344
|
+
var keyOf = (node) => node.kind === "folder" ? `folder:${node.path}` : `story:${node.entry.id}`;
|
|
345
|
+
function ancestorsOf(entry) {
|
|
346
|
+
const paths = /* @__PURE__ */ new Set();
|
|
347
|
+
if (!entry) return paths;
|
|
348
|
+
let path = "";
|
|
349
|
+
for (const label of entry.title.split("/")) {
|
|
350
|
+
path = path ? `${path}/${label}` : label;
|
|
351
|
+
paths.add(path);
|
|
352
|
+
}
|
|
353
|
+
return paths;
|
|
354
|
+
}
|
|
355
|
+
function StoryTree({ ctx, index }) {
|
|
356
|
+
const headingId = useId();
|
|
357
|
+
const [route, setRoute] = useRoute();
|
|
358
|
+
const [query, setQuery] = useState("");
|
|
359
|
+
const [folds, setFolds] = usePersistedState("fg-tree-open", {}, { scope: "lab" });
|
|
360
|
+
const [focusKey, setFocusKey] = useState(null);
|
|
361
|
+
const items = useRef(/* @__PURE__ */ new Map());
|
|
362
|
+
const tree = useMemo(() => buildTree(index), [index]);
|
|
363
|
+
const shown = useMemo(() => filterTree(tree, query), [tree, query]);
|
|
364
|
+
const routed = index.find((entry) => entry.id === route);
|
|
365
|
+
const routeAncestors = useMemo(() => ancestorsOf(routed), [routed]);
|
|
366
|
+
const filtering = query.trim() !== "";
|
|
367
|
+
const isOpen = (path) => filtering || (folds[path] ?? routeAncestors.has(path));
|
|
368
|
+
const rows = [];
|
|
369
|
+
const collect = (nodes, parent) => {
|
|
370
|
+
for (const node of nodes) {
|
|
371
|
+
rows.push({ key: keyOf(node), node, parent });
|
|
372
|
+
if (node.kind === "folder" && isOpen(node.path)) collect(node.children, keyOf(node));
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
collect(shown, null);
|
|
376
|
+
const active = rows.some((row) => row.key === focusKey) ? focusKey : rows[0]?.key ?? null;
|
|
377
|
+
const openIds = new Set(ctx.trials.map((trial) => trial.instrumentName));
|
|
378
|
+
const setOpen = (path, open) => {
|
|
379
|
+
if (!filtering) setFolds((prev) => ({ ...prev, [path]: open }));
|
|
380
|
+
};
|
|
381
|
+
const moveTo = (key) => {
|
|
382
|
+
if (key === void 0) return;
|
|
383
|
+
setFocusKey(key);
|
|
384
|
+
items.current.get(key)?.focus();
|
|
385
|
+
};
|
|
386
|
+
const activate = (entry, fresh) => {
|
|
387
|
+
const open = ctx.trials.find((trial) => trial.instrumentName === entry.id);
|
|
388
|
+
if (open && !fresh) revealTrial(open.id);
|
|
389
|
+
else ctx.addTrial(entry.id);
|
|
390
|
+
setRoute(entry.id);
|
|
391
|
+
};
|
|
392
|
+
const onKeyDown = (event) => {
|
|
393
|
+
const at = rows.findIndex((row2) => row2.key === active);
|
|
394
|
+
const row = rows[at];
|
|
395
|
+
if (!row) return;
|
|
396
|
+
const { node } = row;
|
|
397
|
+
switch (event.key) {
|
|
398
|
+
case "ArrowDown":
|
|
399
|
+
moveTo(rows[at + 1]?.key);
|
|
400
|
+
break;
|
|
401
|
+
case "ArrowUp":
|
|
402
|
+
moveTo(rows[at - 1]?.key);
|
|
403
|
+
break;
|
|
404
|
+
case "Home":
|
|
405
|
+
moveTo(rows[0]?.key);
|
|
406
|
+
break;
|
|
407
|
+
case "End":
|
|
408
|
+
moveTo(rows[rows.length - 1]?.key);
|
|
409
|
+
break;
|
|
410
|
+
case "ArrowRight":
|
|
411
|
+
if (node.kind !== "folder") break;
|
|
412
|
+
if (!isOpen(node.path)) setOpen(node.path, true);
|
|
413
|
+
else if (rows[at + 1]?.parent === row.key) moveTo(rows[at + 1]?.key);
|
|
414
|
+
break;
|
|
415
|
+
case "ArrowLeft":
|
|
416
|
+
if (node.kind === "folder" && isOpen(node.path) && !filtering) setOpen(node.path, false);
|
|
417
|
+
else moveTo(row.parent ?? void 0);
|
|
418
|
+
break;
|
|
419
|
+
case "Enter":
|
|
420
|
+
case " ":
|
|
421
|
+
if (node.kind === "folder") setOpen(node.path, !isOpen(node.path));
|
|
422
|
+
else activate(node.entry, event.metaKey || event.ctrlKey);
|
|
423
|
+
break;
|
|
424
|
+
default:
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
event.preventDefault();
|
|
428
|
+
};
|
|
429
|
+
const itemProps = (node, level) => {
|
|
430
|
+
const key = keyOf(node);
|
|
431
|
+
return {
|
|
432
|
+
role: "treeitem",
|
|
433
|
+
"aria-level": level,
|
|
434
|
+
tabIndex: key === active ? 0 : -1,
|
|
435
|
+
ref: (el) => {
|
|
436
|
+
if (el) items.current.set(key, el);
|
|
437
|
+
else items.current.delete(key);
|
|
438
|
+
},
|
|
439
|
+
// A folder item contains its children, so their focus bubbles through it.
|
|
440
|
+
onFocus: (event) => {
|
|
441
|
+
if (event.target === event.currentTarget) setFocusKey(key);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
};
|
|
445
|
+
const renderNodes = (nodes, level) => nodes.map((node) => {
|
|
446
|
+
if (node.kind === "folder") {
|
|
447
|
+
const open = isOpen(node.path);
|
|
448
|
+
return /* @__PURE__ */ jsxs(
|
|
449
|
+
"div",
|
|
450
|
+
{
|
|
451
|
+
...itemProps(node, level),
|
|
452
|
+
"aria-expanded": open,
|
|
453
|
+
"aria-label": node.label,
|
|
454
|
+
className: "fg-tree__node",
|
|
455
|
+
children: [
|
|
456
|
+
/* @__PURE__ */ jsx("div", { className: "fg-tree__item fg-tree__folder", onClick: () => setOpen(node.path, !open), children: /* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: node.label }) }),
|
|
457
|
+
open ? /* @__PURE__ */ jsx("div", { role: "group", className: "fg-tree__group", children: renderNodes(node.children, level + 1) }) : null
|
|
458
|
+
]
|
|
459
|
+
},
|
|
460
|
+
keyOf(node)
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
const { entry } = node;
|
|
464
|
+
return /* @__PURE__ */ jsx(
|
|
465
|
+
"a",
|
|
466
|
+
{
|
|
467
|
+
...itemProps(node, level),
|
|
468
|
+
href: `#/${encodeURIComponent(entry.id)}`,
|
|
469
|
+
"aria-current": openIds.has(entry.id) ? "true" : void 0,
|
|
470
|
+
className: "fg-tree__item fg-tree__story",
|
|
471
|
+
onClick: (event) => {
|
|
472
|
+
if (event.button !== 0 || event.shiftKey || event.altKey) return;
|
|
473
|
+
event.preventDefault();
|
|
474
|
+
setFocusKey(keyOf(node));
|
|
475
|
+
activate(entry, event.metaKey || event.ctrlKey);
|
|
476
|
+
},
|
|
477
|
+
children: /* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: entry.name })
|
|
478
|
+
},
|
|
479
|
+
keyOf(node)
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
return /* @__PURE__ */ jsxs("section", { className: "fg-tree", children: [
|
|
483
|
+
/* @__PURE__ */ jsx("h2", { id: headingId, className: "fg-tree__heading", children: "Stories" }),
|
|
484
|
+
route && !routed ? /* @__PURE__ */ jsxs("p", { className: "fg-route-miss", children: [
|
|
485
|
+
"No story ",
|
|
486
|
+
/* @__PURE__ */ jsx("code", { children: route })
|
|
487
|
+
] }) : null,
|
|
488
|
+
/* @__PURE__ */ jsx(
|
|
489
|
+
"input",
|
|
490
|
+
{
|
|
491
|
+
type: "search",
|
|
492
|
+
className: "fg-tree__filter",
|
|
493
|
+
"aria-label": "Filter stories",
|
|
494
|
+
value: query,
|
|
495
|
+
onChange: (event) => setQuery(event.target.value)
|
|
496
|
+
}
|
|
497
|
+
),
|
|
498
|
+
/* @__PURE__ */ jsx("div", { role: "tree", "aria-labelledby": headingId, className: "fg-tree__items", onKeyDown, children: renderNodes(shown, 1) })
|
|
499
|
+
] });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/shell/answers.ts
|
|
503
|
+
var KEPT_CONFIGS = 50;
|
|
504
|
+
var UNANSWERED = stableStringify({ hidden: [], errors: {} });
|
|
505
|
+
function createAnswerBook() {
|
|
506
|
+
const byConfig = /* @__PURE__ */ new Map();
|
|
507
|
+
let errors = {};
|
|
508
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
509
|
+
return {
|
|
510
|
+
record(answers) {
|
|
511
|
+
const key = stableStringify({ hidden: answers.hidden, errors: answers.errors });
|
|
512
|
+
const changed = key !== (byConfig.get(answers.configKey)?.key ?? UNANSWERED);
|
|
513
|
+
byConfig.delete(answers.configKey);
|
|
514
|
+
byConfig.set(answers.configKey, { hidden: answers.hidden, key });
|
|
515
|
+
if (byConfig.size > KEPT_CONFIGS) {
|
|
516
|
+
const oldest = byConfig.keys().next().value;
|
|
517
|
+
if (oldest !== void 0) byConfig.delete(oldest);
|
|
518
|
+
}
|
|
519
|
+
errors = answers.errors;
|
|
520
|
+
if (changed) for (const fn of listeners) fn();
|
|
521
|
+
},
|
|
522
|
+
hidden(path, config) {
|
|
523
|
+
const hidden = byConfig.get(stableStringify(storyConfig(config)))?.hidden;
|
|
524
|
+
return hidden?.some((h) => path === h || path.startsWith(`${h}.`)) ?? false;
|
|
525
|
+
},
|
|
526
|
+
errors: (path) => errors[path] ?? [],
|
|
527
|
+
subscribe(fn) {
|
|
528
|
+
listeners.add(fn);
|
|
529
|
+
return () => {
|
|
530
|
+
listeners.delete(fn);
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/shell/readyKey.ts
|
|
537
|
+
var readyKey = (ready) => stableStringify({ schema: ready.schema, layout: ready.layout, viewport: ready.viewport });
|
|
538
|
+
var START_TIMEOUT_MS = 1e4;
|
|
539
|
+
function closeLink(link) {
|
|
540
|
+
if (!link.current) return;
|
|
541
|
+
clearTimeout(link.current.timer);
|
|
542
|
+
link.current.disconnect();
|
|
543
|
+
link.current.channel.close();
|
|
544
|
+
link.current = null;
|
|
545
|
+
}
|
|
546
|
+
function mismatchMessage(mismatch) {
|
|
547
|
+
return mismatch.reason === "version" ? `Frame speaks protocol ${String(mismatch.version)}; this workshop speaks ${PROTOCOL_VERSION}` : "Frame sent a message that is not a forge envelope";
|
|
548
|
+
}
|
|
549
|
+
function FrameView(props) {
|
|
550
|
+
const { entry, frameUrl, descriptionKey, ctx } = props;
|
|
551
|
+
const labGlobals2 = useContext(StoryGlobalsContext);
|
|
552
|
+
const pins = ctx.config?.[GLOBALS_KEY];
|
|
553
|
+
const globals = useMemo(() => effectiveGlobals(labGlobals2, pins), [labGlobals2, pins]);
|
|
554
|
+
const trialId = useContext(TrialIdContext);
|
|
555
|
+
const frames = useContext(TrialFramesContext);
|
|
556
|
+
const [overrides] = useCssOverrides();
|
|
557
|
+
const src = `${frameUrl}#${entry.id}`;
|
|
558
|
+
const iframeRef = useRef(null);
|
|
559
|
+
const link = useRef(null);
|
|
560
|
+
const latest = useRef({ ...props, globals, overrides });
|
|
561
|
+
latest.current = { ...props, globals, overrides };
|
|
562
|
+
const [fault, setFault] = useState(null);
|
|
563
|
+
const init = (current) => {
|
|
564
|
+
const { ctx: live, globals: liveGlobals } = latest.current;
|
|
565
|
+
current.awaiting = null;
|
|
566
|
+
current.sent = { config: storyConfig(live.config), state: live.state, globals: liveGlobals };
|
|
567
|
+
current.inputs += 1;
|
|
568
|
+
current.channel.send({ type: "init", ...current.sent });
|
|
569
|
+
};
|
|
570
|
+
const receive = (current, msg) => {
|
|
571
|
+
const { ctx: live, answers, onReady } = latest.current;
|
|
572
|
+
switch (msg.type) {
|
|
573
|
+
case "ready": {
|
|
574
|
+
clearTimeout(current.timer);
|
|
575
|
+
setFault(null);
|
|
576
|
+
for (const [name, value] of Object.entries(latest.current.overrides)) {
|
|
577
|
+
current.channel.send({ type: "vars.set", name, value });
|
|
578
|
+
}
|
|
579
|
+
const key = readyKey(msg);
|
|
580
|
+
current.awaiting = key;
|
|
581
|
+
onReady(latest.current.entry, msg);
|
|
582
|
+
if (current.awaiting === key && key === latest.current.descriptionKey) init(current);
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
case "setConfig":
|
|
586
|
+
if (!isGlobalsPath(msg.path)) live.setConfig(msg.path, msg.value);
|
|
587
|
+
break;
|
|
588
|
+
case "setState":
|
|
589
|
+
if (current.sent) current.sent.state = msg.state;
|
|
590
|
+
live.setState(msg.state);
|
|
591
|
+
break;
|
|
592
|
+
case "answers":
|
|
593
|
+
answers.record(msg.answers);
|
|
594
|
+
break;
|
|
595
|
+
case "vars":
|
|
596
|
+
if (trialId) frames?.report(trialId, msg.vars);
|
|
597
|
+
break;
|
|
598
|
+
case "fault":
|
|
599
|
+
if (msg.phase === "render" && msg.seq !== void 0 && msg.seq < current.inputs) break;
|
|
600
|
+
clearTimeout(current.timer);
|
|
601
|
+
setFault({ phase: msg.phase, message: msg.message });
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
const onLoad = () => {
|
|
606
|
+
closeLink(link);
|
|
607
|
+
const target = iframeRef.current?.contentWindow;
|
|
608
|
+
if (!target) return;
|
|
609
|
+
const { port1, port2 } = new MessageChannel();
|
|
610
|
+
const timer = setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), START_TIMEOUT_MS);
|
|
611
|
+
const channel = openChannel(port1, {
|
|
612
|
+
onMismatch: (mismatch) => {
|
|
613
|
+
clearTimeout(timer);
|
|
614
|
+
setFault({ phase: "protocol", message: mismatchMessage(mismatch) });
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
const disconnect = frames && trialId ? frames.connect(trialId, channel.send) : () => {
|
|
618
|
+
};
|
|
619
|
+
const current = { channel, timer, awaiting: null, sent: null, inputs: 0, disconnect };
|
|
620
|
+
link.current = current;
|
|
621
|
+
channel.on((msg) => receive(current, msg));
|
|
622
|
+
target.postMessage({ type: PORT_HANDOFF }, location.origin, [port2]);
|
|
623
|
+
};
|
|
624
|
+
useEffect(() => () => closeLink(link), []);
|
|
625
|
+
useEffect(() => {
|
|
626
|
+
const current = link.current;
|
|
627
|
+
if (!current) return;
|
|
628
|
+
if (current.awaiting !== null) {
|
|
629
|
+
if (current.awaiting === descriptionKey) init(current);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const sent = current.sent;
|
|
633
|
+
if (!sent) return;
|
|
634
|
+
let input = false;
|
|
635
|
+
const config = storyConfig(ctx.config, sent.config);
|
|
636
|
+
if (config !== sent.config) {
|
|
637
|
+
sent.config = config;
|
|
638
|
+
current.channel.send({ type: "config", config });
|
|
639
|
+
current.inputs += 1;
|
|
640
|
+
input = true;
|
|
641
|
+
}
|
|
642
|
+
if (ctx.state !== sent.state) {
|
|
643
|
+
sent.state = ctx.state;
|
|
644
|
+
current.channel.send({ type: "state", state: ctx.state });
|
|
645
|
+
current.inputs += 1;
|
|
646
|
+
input = true;
|
|
647
|
+
}
|
|
648
|
+
if (globals !== sent.globals && stableStringify(globals) !== stableStringify(sent.globals)) {
|
|
649
|
+
sent.globals = globals;
|
|
650
|
+
current.channel.send({ type: "globals", globals });
|
|
651
|
+
current.inputs += 1;
|
|
652
|
+
input = true;
|
|
653
|
+
}
|
|
654
|
+
if (input) setFault((shown) => shown?.phase === "render" ? null : shown);
|
|
655
|
+
}, [ctx.config, ctx.state, globals, descriptionKey]);
|
|
656
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-frame-host", children: [
|
|
657
|
+
/* @__PURE__ */ jsx(
|
|
658
|
+
"iframe",
|
|
659
|
+
{
|
|
660
|
+
ref: iframeRef,
|
|
661
|
+
className: "fg-frame-view",
|
|
662
|
+
src,
|
|
663
|
+
title: `${entry.title} / ${entry.name}`,
|
|
664
|
+
onLoad
|
|
665
|
+
}
|
|
666
|
+
),
|
|
667
|
+
fault ? /* @__PURE__ */ jsxs("div", { className: "fg-fault", role: "alert", children: [
|
|
668
|
+
fault.phase ? /* @__PURE__ */ jsx("span", { className: "fg-fault__phase", children: fault.phase }) : null,
|
|
669
|
+
/* @__PURE__ */ jsx("p", { className: "fg-fault__message", children: fault.message })
|
|
670
|
+
] }) : null
|
|
671
|
+
] });
|
|
672
|
+
}
|
|
673
|
+
function storyInstrument(options) {
|
|
674
|
+
const { entry, ready, answers, frameUrl, onReady, globals = {} } = options;
|
|
675
|
+
const config = withGlobals(
|
|
676
|
+
ready ? schemaFromDescription(ready.schema, answers) : f.schema({}),
|
|
677
|
+
globals
|
|
678
|
+
);
|
|
679
|
+
const descriptionKey = ready ? readyKey(ready) : null;
|
|
680
|
+
return {
|
|
681
|
+
name: entry.id,
|
|
682
|
+
title: `${entry.title} / ${entry.name}`,
|
|
683
|
+
config,
|
|
684
|
+
defaultConfig: () => config.defaults(),
|
|
685
|
+
initialState: () => null,
|
|
686
|
+
render: (ctx) => /* @__PURE__ */ jsx(
|
|
687
|
+
FrameView,
|
|
688
|
+
{
|
|
689
|
+
entry,
|
|
690
|
+
frameUrl,
|
|
691
|
+
answers,
|
|
692
|
+
onReady,
|
|
693
|
+
descriptionKey,
|
|
694
|
+
ctx
|
|
695
|
+
}
|
|
696
|
+
),
|
|
697
|
+
...ready?.viewport ? { stage: { size: { width: ready.viewport.width, height: ready.viewport.height } } } : {}
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/shell/useStoryRegistry.ts
|
|
702
|
+
var NO_GLOBALS = {};
|
|
703
|
+
var pruned = (map, ids) => [...map.keys()].every((id) => ids.has(id)) ? map : new Map([...map].filter(([id]) => ids.has(id)));
|
|
704
|
+
function useStoryRegistry(index, options) {
|
|
705
|
+
const { frameUrl, globals = NO_GLOBALS } = options;
|
|
706
|
+
const [readies, setReadies] = useState(() => /* @__PURE__ */ new Map());
|
|
707
|
+
const [revisions, setRevisions] = useState(() => /* @__PURE__ */ new Map());
|
|
708
|
+
const onReady = useCallback((entry, ready) => {
|
|
709
|
+
setReadies((prev) => {
|
|
710
|
+
const held = prev.get(entry.id);
|
|
711
|
+
if (held && readyKey(held) === readyKey(ready)) return prev;
|
|
712
|
+
return new Map(prev).set(entry.id, ready);
|
|
713
|
+
});
|
|
714
|
+
}, []);
|
|
715
|
+
useEffect(() => {
|
|
716
|
+
const ids = new Set(index.map((entry) => entry.id));
|
|
717
|
+
setReadies((prev) => pruned(prev, ids));
|
|
718
|
+
setRevisions((prev) => pruned(prev, ids));
|
|
719
|
+
}, [index]);
|
|
720
|
+
const committed = useRef({ built: /* @__PURE__ */ new Map(), books: /* @__PURE__ */ new Map(), list: [] });
|
|
721
|
+
const cache = useMemo(() => {
|
|
722
|
+
const previous = committed.current;
|
|
723
|
+
const built = /* @__PURE__ */ new Map();
|
|
724
|
+
const books = /* @__PURE__ */ new Map();
|
|
725
|
+
const list = index.map((entry) => {
|
|
726
|
+
const ready = readies.get(entry.id);
|
|
727
|
+
const revision = revisions.get(entry.id) ?? 0;
|
|
728
|
+
const entryKey = stableStringify(entry);
|
|
729
|
+
const answers = previous.books.get(entry.id) ?? createAnswerBook();
|
|
730
|
+
books.set(entry.id, answers);
|
|
731
|
+
const held = previous.built.get(entry.id);
|
|
732
|
+
const kept = held && held.entryKey === entryKey && held.ready === ready && held.frameUrl === frameUrl && held.globals === globals && held.revision === revision ? held : {
|
|
733
|
+
entryKey,
|
|
734
|
+
ready,
|
|
735
|
+
frameUrl,
|
|
736
|
+
globals,
|
|
737
|
+
revision,
|
|
738
|
+
instrument: storyInstrument({ entry, ready, answers, frameUrl, onReady, globals })
|
|
739
|
+
};
|
|
740
|
+
built.set(entry.id, kept);
|
|
741
|
+
return kept.instrument;
|
|
742
|
+
});
|
|
743
|
+
const unchanged = list.length === previous.list.length && list.every((i, n) => i === previous.list[n]);
|
|
744
|
+
return { built, books, list: unchanged ? previous.list : list };
|
|
745
|
+
}, [index, readies, revisions, frameUrl, globals, onReady]);
|
|
746
|
+
useLayoutEffect(() => {
|
|
747
|
+
committed.current = cache;
|
|
748
|
+
}, [cache]);
|
|
749
|
+
useEffect(() => {
|
|
750
|
+
const offs = [...cache.books].map(
|
|
751
|
+
([id, book]) => book.subscribe(() => setRevisions((prev) => new Map(prev).set(id, (prev.get(id) ?? 0) + 1)))
|
|
752
|
+
);
|
|
753
|
+
return () => {
|
|
754
|
+
for (const off of offs) off();
|
|
755
|
+
};
|
|
756
|
+
}, [cache.books]);
|
|
757
|
+
return { instruments: cache.list, onReady };
|
|
758
|
+
}
|
|
759
|
+
var NO_DECLARATIONS = {};
|
|
760
|
+
var TRIAL_CHROME = [CSS_VARS_SECTION];
|
|
761
|
+
function initialStory(index, fallback) {
|
|
762
|
+
const route = readRoute();
|
|
763
|
+
return route !== null && index.some((entry) => entry.id === route) ? route : fallback;
|
|
764
|
+
}
|
|
765
|
+
function RouteOpener({ index }) {
|
|
766
|
+
const lab = useLabContext();
|
|
767
|
+
const [route] = useRoute();
|
|
768
|
+
const handled = useRef(null);
|
|
769
|
+
useEffect(() => {
|
|
770
|
+
if (route === null || handled.current === route || !index.some((entry) => entry.id === route)) return;
|
|
771
|
+
handled.current = route;
|
|
772
|
+
if (!lab.trials.some((trial) => trial.instrumentName === route)) lab.addTrial(route);
|
|
773
|
+
}, [route, index, lab]);
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }) {
|
|
777
|
+
const declarations = config?.globals ?? NO_DECLARATIONS;
|
|
778
|
+
const registry = useStoryRegistry(index, { frameUrl, globals: declarations });
|
|
779
|
+
const [frames] = useState(createTrialFrames);
|
|
780
|
+
const [labValues, setLabValues] = useState(() => labGlobals(declarations, void 0));
|
|
781
|
+
const reportLabValues = useCallback(
|
|
782
|
+
(next) => setLabValues((prev) => stableStringify(prev) === stableStringify(next) ? prev : next),
|
|
783
|
+
[]
|
|
784
|
+
);
|
|
785
|
+
const labChrome = useMemo(
|
|
786
|
+
() => [
|
|
787
|
+
{ id: "fg-stories", region: "sidebar", render: (ctx) => /* @__PURE__ */ jsx(StoryTree, { ctx, index }) },
|
|
788
|
+
...Object.keys(declarations).length > 0 ? [{ id: "fg-globals", region: "header", render: () => /* @__PURE__ */ jsx(GlobalsToolbar, { declarations }) }] : [],
|
|
789
|
+
...config?.labChrome ?? []
|
|
790
|
+
],
|
|
791
|
+
[index, declarations, config?.labChrome]
|
|
792
|
+
);
|
|
793
|
+
const first = index[0];
|
|
794
|
+
if (!first) {
|
|
795
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-empty", children: [
|
|
796
|
+
/* @__PURE__ */ jsx("p", { className: "fg-empty__title", children: "No stories found" }),
|
|
797
|
+
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
|
|
798
|
+
] });
|
|
799
|
+
}
|
|
800
|
+
return /* @__PURE__ */ jsx(StoryGlobalsContext.Provider, { value: labValues, children: /* @__PURE__ */ jsx(TrialFramesContext.Provider, { value: frames, children: /* @__PURE__ */ jsxs(
|
|
801
|
+
Lab,
|
|
802
|
+
{
|
|
803
|
+
title: "weaselforge",
|
|
804
|
+
instruments: registry.instruments,
|
|
805
|
+
defaultInstrument: initialStory(index, first.id),
|
|
806
|
+
storageKey: storageKey ?? "weaselforge",
|
|
807
|
+
...storage ? { storage } : {},
|
|
808
|
+
labChrome,
|
|
809
|
+
chrome: TRIAL_CHROME,
|
|
810
|
+
addTrial: false,
|
|
811
|
+
...config?.controls ? { controls: config.controls } : {},
|
|
812
|
+
children: [
|
|
813
|
+
/* @__PURE__ */ jsx(RouteOpener, { index }),
|
|
814
|
+
/* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues })
|
|
815
|
+
]
|
|
816
|
+
}
|
|
817
|
+
) }) });
|
|
818
|
+
}
|
|
819
|
+
function mountWorkshop({ container, ...props }) {
|
|
820
|
+
const target = container ?? document.getElementById("root");
|
|
821
|
+
if (!target) throw new Error("mountWorkshop: no container given and no #root element");
|
|
822
|
+
const root = createRoot(target);
|
|
823
|
+
let current = props;
|
|
824
|
+
root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
|
|
825
|
+
return {
|
|
826
|
+
setIndex(index) {
|
|
827
|
+
current = { ...current, index };
|
|
828
|
+
root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));
|
|
829
|
+
},
|
|
830
|
+
unmount: () => root.unmount()
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export { StoryGlobalsContext, Workshop, mountWorkshop, useStoryRegistry };
|
|
835
|
+
//# sourceMappingURL=index.js.map
|
|
836
|
+
//# sourceMappingURL=index.js.map
|