@weasel-js/forge 1.5.1 → 1.5.2

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.
@@ -1,11 +1,13 @@
1
1
  import { schemaFromDescription, openChannel } from '../chunk-2JDYOBWM.js';
2
2
  import { stableStringify, PORT_HANDOFF, PROTOCOL_VERSION } from '../chunk-FTFR2YPM.js';
3
- import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, inferTokenType, ToggleBar, Input, TokenPanel } from '@weasel-js/labkit';
3
+ import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, inferTokenType, ToggleBar as ToggleBar$1, Input, TokenPanel, refitScale } from '@weasel-js/labkit';
4
4
  import { createContext, useContext, useCallback, useSyncExternalStore, useState, useRef, useEffect, useMemo, useLayoutEffect, useId } from 'react';
5
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
6
  import { createRoot } from 'react-dom/client';
7
- import { TOKEN_MANIFEST } from '@weasel-js/theme';
8
- import { f } from '@weasel-js/labkit/config';
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';
10
+ import { f, valueAtPath, isConfigBranch } from '@weasel-js/labkit/config';
9
11
 
10
12
  var NO_FRAME = { send: null, vars: [], audit: null, a11y: null, capture: null, size: null };
11
13
  function createTrialFrames() {
@@ -156,6 +158,29 @@ var TABS = [
156
158
  { value: "story", label: "Story" }
157
159
  ];
158
160
  var MANIFEST = new Map(TOKEN_MANIFEST.map((token) => [token.name, token]));
161
+ var literal = (v) => typeof v === "number";
162
+ var SCALE_SOURCES = Object.entries(THEME_SOURCES.weasel?.scales ?? {}).flatMap(([name, def]) => {
163
+ if (!Array.isArray(def.steps)) return [];
164
+ const steps = def.steps;
165
+ const tokens = steps.map((step) => `--wzl-${name}-${step}`);
166
+ const group = MANIFEST.get(tokens[0] ?? "")?.group;
167
+ if (!group) return [];
168
+ const factors = Array.isArray(def.factors) && def.factors.every(literal) ? def.factors : null;
169
+ 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
+ return [{ name, group, steps, tokens, rule }];
171
+ });
172
+ function standingScale(source, amounts) {
173
+ const seed = { tokens: source.tokens, base: amounts[0] ?? 0, rule: { kind: "step", step: 0 } };
174
+ if (typeof source.rule === "string") return refitScale(seed, source.rule, amounts);
175
+ const { rule } = source;
176
+ if (rule.kind !== "factors") return { ...seed, rule };
177
+ let at = 0;
178
+ rule.factors.forEach((f3, i) => {
179
+ if (Math.abs(f3 - 1) < Math.abs((rule.factors[at] ?? 1) - 1)) at = i;
180
+ });
181
+ const base = (amounts[at] ?? 0) / (rule.factors[at] ?? 1);
182
+ return { ...seed, base: Math.round(base * 100) / 100, rule };
183
+ }
159
184
  function groupOf(name) {
160
185
  const segments = name.replace(/^--/, "").split("-");
161
186
  return segments.length > 1 ? segments.slice(0, -1).join("-") : name.replace(/^--/, "");
@@ -169,6 +194,7 @@ function CssVarsPanel() {
169
194
  {},
170
195
  { scope: "lab" }
171
196
  );
197
+ const [edited, setEdited] = usePersistedState("fg-css-vars-scales", {}, { scope: "lab" });
172
198
  const [tab, setTab] = useState("theme");
173
199
  const [filter, setFilter] = useState("");
174
200
  const tokens = useMemo(() => {
@@ -192,6 +218,18 @@ function CssVarsPanel() {
192
218
  const override = overrides[token.name];
193
219
  return override === void 0 ? token : { ...token, value: override, overridden: true };
194
220
  }).filter((token) => !query || token.name.toLowerCase().includes(query) || token.value.toLowerCase().includes(query));
221
+ const scales = useMemo(() => {
222
+ if (tab !== "theme") return void 0;
223
+ const values = new Map(shown.map((t) => [t.name, t.value]));
224
+ const out = {};
225
+ for (const source of SCALE_SOURCES) {
226
+ const amounts = source.tokens.map((name) => Number.parseFloat(values.get(name) ?? ""));
227
+ if (amounts.some(Number.isNaN)) continue;
228
+ const own = source.tokens.some((name) => overrides[name] !== void 0) ? edited[source.group] : void 0;
229
+ out[source.group] = own ?? standingScale(source, amounts);
230
+ }
231
+ return out;
232
+ }, [tab, shown, overrides, edited]);
195
233
  const send = frame.send;
196
234
  const write = useCallback(
197
235
  (name, value) => {
@@ -203,6 +241,24 @@ function CssVarsPanel() {
203
241
  },
204
242
  [setOverrides, send]
205
243
  );
244
+ const onScaleChange = useCallback(
245
+ (group, next) => {
246
+ const source = SCALE_SOURCES.find((sc) => sc.group === group);
247
+ if (!source) return;
248
+ setEdited((prev) => ({ ...prev, [group]: next }));
249
+ const { rule } = next;
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
+ });
259
+ },
260
+ [setEdited, write]
261
+ );
206
262
  const onCollapsedChange = useCallback(
207
263
  (category, collapsed) => setFolds((prev) => ({ ...prev, [category]: collapsed })),
208
264
  [setFolds]
@@ -210,7 +266,7 @@ function CssVarsPanel() {
210
266
  return /* @__PURE__ */ jsxs("div", { className: "fg-css-vars", children: [
211
267
  /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__controls", children: [
212
268
  /* @__PURE__ */ jsx(
213
- ToggleBar,
269
+ ToggleBar$1,
214
270
  {
215
271
  ariaLabel: "Variables",
216
272
  variant: "flat",
@@ -223,7 +279,17 @@ function CssVarsPanel() {
223
279
  ),
224
280
  /* @__PURE__ */ jsx(Input, { "aria-label": "Filter", placeholder: "Filter by name or value", value: filter, onChange: setFilter })
225
281
  ] }),
226
- shown.length === 0 ? /* @__PURE__ */ jsx("p", { className: "fg-css-vars__empty", children: tokens.length === 0 ? "The story\u2019s frame has reported no variables." : "No variables match." }) : /* @__PURE__ */ jsx(TokenPanel, { tokens: shown, onChange: write, collapsed: folds, onCollapsedChange })
282
+ 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
+ TokenPanel,
284
+ {
285
+ tokens: shown,
286
+ onChange: write,
287
+ scales,
288
+ onScaleChange,
289
+ collapsed: folds,
290
+ onCollapsedChange
291
+ }
292
+ )
227
293
  ] });
228
294
  }
229
295
  function FocusedCssVars() {
@@ -241,6 +307,206 @@ var CSS_VARS_SECTION = {
241
307
  region: "aside",
242
308
  item: { title: "CSS Vars", body: /* @__PURE__ */ jsx(FocusedCssVars, {}) }
243
309
  };
310
+
311
+ // src/shell/tree/buildComponents.ts
312
+ var LIBRARIES = [
313
+ ["/packages/ui/", "weasel-ui"],
314
+ ["/packages/labkit/", "labkit"],
315
+ ["/packages/forge/", "forge"],
316
+ ["/apps/draw/", "draw"],
317
+ ["/apps/forge/", "forge"]
318
+ ];
319
+ function libraryOf(entry) {
320
+ const file = entry.file.replaceAll("\\", "/");
321
+ for (const [marker, label] of LIBRARIES) {
322
+ if (file.includes(marker)) return label;
323
+ }
324
+ return entry.title.split("/")[0];
325
+ }
326
+ function buildComponents(index) {
327
+ const byTitle = /* @__PURE__ */ new Map();
328
+ for (const entry of index) {
329
+ let row = byTitle.get(entry.title);
330
+ if (!row) {
331
+ const segments = entry.title.split("/");
332
+ row = {
333
+ path: entry.title,
334
+ label: segments[segments.length - 1],
335
+ library: libraryOf(entry),
336
+ entries: []
337
+ };
338
+ byTitle.set(entry.title, row);
339
+ }
340
+ row.entries.push(entry);
341
+ }
342
+ const rows = [...byTitle.values()];
343
+ disambiguate(rows);
344
+ return rows.sort(
345
+ (a, b) => a.label.localeCompare(b.label) || a.library.localeCompare(b.library) || a.path.localeCompare(b.path)
346
+ );
347
+ }
348
+ function disambiguate(rows) {
349
+ const byLabel = /* @__PURE__ */ new Map();
350
+ for (const row of rows) {
351
+ const group = byLabel.get(row.label);
352
+ if (group) group.push(row);
353
+ else byLabel.set(row.label, [row]);
354
+ }
355
+ for (const group of byLabel.values()) {
356
+ if (group.length < 2) continue;
357
+ for (const row of group) {
358
+ const segments = row.path.split("/");
359
+ for (let take = 2; take <= segments.length; take++) {
360
+ row.label = segments.slice(-take).join("/");
361
+ if (group.every((other) => other === row || !other.path.endsWith(`/${row.label}`))) break;
362
+ }
363
+ }
364
+ }
365
+ }
366
+ function filterComponents(rows, query) {
367
+ const needle = query.trim().toLowerCase();
368
+ if (!needle) return [...rows];
369
+ const out = [];
370
+ for (const row of rows) {
371
+ if (`${row.path}/${row.library}`.toLowerCase().includes(needle)) {
372
+ out.push(row);
373
+ continue;
374
+ }
375
+ const entries = row.entries.filter((entry) => entry.name.toLowerCase().includes(needle));
376
+ if (entries.length > 0) out.push({ ...row, entries });
377
+ }
378
+ return out;
379
+ }
380
+ function componentNodes(rows) {
381
+ return rows.map(
382
+ (row) => row.entries.length === 1 ? { kind: "story", entry: row.entries[0], tag: row.library, label: row.label } : {
383
+ kind: "folder",
384
+ label: row.label,
385
+ path: row.path,
386
+ tag: row.library,
387
+ children: row.entries.map((entry) => ({ kind: "story", entry }))
388
+ }
389
+ );
390
+ }
391
+ function librariesIn(index) {
392
+ return [...new Set(index.map(libraryOf))].sort((a, b) => a.localeCompare(b));
393
+ }
394
+
395
+ // src/shell/info/dossier.ts
396
+ var GLOBALS = "$globals";
397
+ function leaves(shape, prefix = "") {
398
+ const out = [];
399
+ for (const [key, entry] of Object.entries(shape)) {
400
+ if (prefix === "" && key === GLOBALS) continue;
401
+ const path = prefix ? `${prefix}.${key}` : key;
402
+ if (isConfigBranch(entry)) out.push(...leaves(entry.children, path));
403
+ else if (!entry.annotations.hidden) out.push({ path, node: entry });
404
+ }
405
+ return out;
406
+ }
407
+ function storyDossier(input) {
408
+ const { entry, instrument, config, ready = true } = input;
409
+ const schema = ready ? instrument?.config : void 0;
410
+ const args = schema ? leaves(schema.nodes).map(({ path, node }) => {
411
+ const value = valueAtPath(config, path);
412
+ return {
413
+ path,
414
+ label: node.annotations.name ?? path.split(".").pop() ?? path,
415
+ kind: node.kind,
416
+ default: node.default,
417
+ value: value === void 0 ? node.default : value,
418
+ ...node.annotations.description === void 0 ? {} : { description: node.annotations.description }
419
+ };
420
+ }) : [];
421
+ return {
422
+ id: entry.id,
423
+ title: entry.title,
424
+ name: entry.name,
425
+ exportName: entry.exportName,
426
+ file: entry.file,
427
+ library: libraryOf(entry),
428
+ ...entry.componentName === void 0 ? {} : { componentName: entry.componentName },
429
+ ...entry.description === void 0 ? {} : { description: entry.description },
430
+ ...entry.componentDescription === void 0 ? {} : { componentDescription: entry.componentDescription },
431
+ args,
432
+ argsPending: !schema
433
+ };
434
+ }
435
+ function printed(value) {
436
+ if (value === void 0 || value === null) return "\u2014";
437
+ if (typeof value === "string") return value === "" ? '""' : value;
438
+ if (typeof value === "function") return "\u0192";
439
+ if (typeof value === "object") return JSON.stringify(value);
440
+ return String(value);
441
+ }
442
+ function Row({ label, children }) {
443
+ return /* @__PURE__ */ jsxs("div", { className: "fg-info__row", children: [
444
+ /* @__PURE__ */ jsx("dt", { className: "fg-info__key", children: label }),
445
+ /* @__PURE__ */ jsx("dd", { className: "fg-info__value", children })
446
+ ] });
447
+ }
448
+ function ArgRows({ args }) {
449
+ return /* @__PURE__ */ jsxs("table", { className: "fg-info__args", children: [
450
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
451
+ /* @__PURE__ */ jsx("th", { scope: "col", children: "Name" }),
452
+ /* @__PURE__ */ jsx("th", { scope: "col", children: "Kind" }),
453
+ /* @__PURE__ */ jsx("th", { scope: "col", children: "Value" }),
454
+ /* @__PURE__ */ jsx("th", { scope: "col", children: "Default" })
455
+ ] }) }),
456
+ /* @__PURE__ */ jsx("tbody", { children: args.map((arg) => /* @__PURE__ */ jsxs("tr", { children: [
457
+ /* @__PURE__ */ jsxs("th", { scope: "row", children: [
458
+ /* @__PURE__ */ jsx("span", { className: "fg-info__arg-name", children: arg.label }),
459
+ arg.description ? /* @__PURE__ */ jsx("span", { className: "fg-info__arg-doc", children: arg.description }) : null
460
+ ] }),
461
+ /* @__PURE__ */ jsx("td", { className: "fg-info__arg-kind", children: arg.kind ?? "\u2014" }),
462
+ /* @__PURE__ */ jsx("td", { className: "fg-info__value-cell", children: printed(arg.value) }),
463
+ /* @__PURE__ */ jsx("td", { className: "fg-info__value-cell", children: printed(arg.default) })
464
+ ] }, arg.path)) })
465
+ ] });
466
+ }
467
+ function StoryDossier({ dossier }) {
468
+ return /* @__PURE__ */ jsxs("div", { className: "fg-info", children: [
469
+ dossier.description || dossier.componentDescription ? /* @__PURE__ */ jsxs("section", { className: "fg-info__section", "aria-label": "Docs", children: [
470
+ dossier.description ? /* @__PURE__ */ jsx("p", { className: "fg-info__doc", children: dossier.description }) : null,
471
+ dossier.componentDescription ? /* @__PURE__ */ jsx("p", { className: "fg-info__doc fg-info__doc--component", children: dossier.componentDescription }) : null
472
+ ] }) : null,
473
+ /* @__PURE__ */ jsx("section", { className: "fg-info__section", "aria-label": "Identity", children: /* @__PURE__ */ jsxs("dl", { className: "fg-info__rows", children: [
474
+ /* @__PURE__ */ jsx(Row, { label: "Story", children: dossier.name }),
475
+ /* @__PURE__ */ jsx(Row, { label: "Component", children: dossier.componentName ?? dossier.title.split("/").pop() }),
476
+ /* @__PURE__ */ jsx(Row, { label: "Library", children: dossier.library }),
477
+ /* @__PURE__ */ jsx(Row, { label: "Title", children: dossier.title }),
478
+ /* @__PURE__ */ jsx(Row, { label: "Export", children: /* @__PURE__ */ jsx("code", { children: dossier.exportName }) }),
479
+ /* @__PURE__ */ jsx(Row, { label: "Id", children: /* @__PURE__ */ jsx("code", { children: dossier.id }) }),
480
+ /* @__PURE__ */ jsx(Row, { label: "File", children: /* @__PURE__ */ jsx("code", { className: "fg-info__path", children: dossier.file }) })
481
+ ] }) }),
482
+ /* @__PURE__ */ jsxs("section", { className: "fg-info__section", "aria-label": "Args", children: [
483
+ /* @__PURE__ */ jsx("h3", { className: "fg-info__heading", children: "Args" }),
484
+ dossier.argsPending ? /* @__PURE__ */ jsx("p", { className: "fg-info__empty", children: "Open this story to read its args." }) : dossier.args.length === 0 ? /* @__PURE__ */ jsx("p", { className: "fg-info__empty", children: "This story takes no args." }) : /* @__PURE__ */ jsx(ArgRows, { args: dossier.args })
485
+ ] })
486
+ ] });
487
+ }
488
+ function StoryInfoDialog({ index, isReady, isOpen, onOpenChange }) {
489
+ const { focusedTrialId, trials, instruments } = useLabContext();
490
+ const trial = trials.find((record) => record.id === focusedTrialId);
491
+ const entry = trial ? index.find((candidate) => candidate.id === trial.instrumentName) : void 0;
492
+ if (!entry) return null;
493
+ const dossier = storyDossier({
494
+ entry,
495
+ instrument: instruments.find((candidate) => candidate.name === entry.id),
496
+ config: trial?.config,
497
+ ready: isReady(entry.id)
498
+ });
499
+ return /* @__PURE__ */ jsx(
500
+ Dialog,
501
+ {
502
+ isOpen,
503
+ onOpenChange,
504
+ title: `${dossier.title} / ${dossier.name}`,
505
+ className: "fg-info-dialog",
506
+ children: /* @__PURE__ */ jsx(StoryDossier, { dossier })
507
+ }
508
+ );
509
+ }
244
510
  var GLOBALS_KEY = "$globals";
245
511
  var FOLLOW_LAB = "lab";
246
512
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -271,13 +537,13 @@ function storyConfig(config, previous) {
271
537
  function withGlobals(schema, declarations) {
272
538
  const entries = Object.entries(declarations);
273
539
  if (entries.length === 0) return schema;
274
- const leaves = Object.fromEntries(
540
+ const leaves2 = Object.fromEntries(
275
541
  entries.map(([key, declaration]) => [
276
542
  key,
277
543
  f.enum(FOLLOW_LAB, [{ value: FOLLOW_LAB, label: "Lab" }, ...declaration.options]).label(declaration.label)
278
544
  ])
279
545
  );
280
- const group = f.group(leaves).label("").section("Globals", { layout: "inline", pack: "pairs" });
546
+ const group = f.group(leaves2).label("").section("Globals", { layout: "inline", pack: "pairs" });
281
547
  return f.schema({ ...schema.nodes, [GLOBALS_KEY]: group });
282
548
  }
283
549
  var RECORD = "fg-globals";
@@ -395,6 +661,10 @@ function filterTree(nodes, query) {
395
661
  });
396
662
  return walk(nodes);
397
663
  }
664
+ var VIEWS = [
665
+ { value: "tree", label: "Tree" },
666
+ { value: "components", label: "Components" }
667
+ ];
398
668
  var keyOf = (node) => node.kind === "folder" ? `folder:${node.path}` : `story:${node.entry.id}`;
399
669
  function ancestorsOf(entry) {
400
670
  const paths = /* @__PURE__ */ new Set();
@@ -411,14 +681,22 @@ function StoryTree({ ctx, index }) {
411
681
  const [route, setRoute] = useRoute();
412
682
  const [query, setQuery] = useState("");
413
683
  const [folds, setFolds] = usePersistedState("fg-tree-open", {}, { scope: "lab" });
684
+ const [view, setView] = usePersistedState("fg-tree-view", "tree", { scope: "lab" });
685
+ const [hidden, setHidden] = usePersistedState("fg-tree-libraries", {}, { scope: "lab" });
414
686
  const [focusKey, setFocusKey] = useState(null);
415
687
  const items = useRef(/* @__PURE__ */ new Map());
416
- const tree = useMemo(() => buildTree(index), [index]);
417
- const shown = useMemo(() => filterTree(tree, query), [tree, query]);
418
- const routed = index.find((entry) => entry.id === route);
688
+ const libraries = useMemo(() => librariesIn(index), [index]);
689
+ const kept = useMemo(() => index.filter((entry) => !hidden[libraryOf(entry)]), [index, hidden]);
690
+ const tree = useMemo(() => buildTree(kept), [kept]);
691
+ const components = useMemo(() => buildComponents(kept), [kept]);
692
+ const shown = useMemo(
693
+ () => view === "components" ? componentNodes(filterComponents(components, query)) : filterTree(tree, query),
694
+ [view, components, tree, query]
695
+ );
696
+ const routed = kept.find((entry) => entry.id === route);
419
697
  const routeAncestors = useMemo(() => ancestorsOf(routed), [routed]);
420
698
  const filtering = query.trim() !== "";
421
- const isOpen = (path) => filtering || (folds[path] ?? routeAncestors.has(path));
699
+ const isOpen = (path) => filtering || (folds[path] ?? (view === "tree" && routeAncestors.has(path)));
422
700
  const rows = [];
423
701
  const collect = (nodes, parent) => {
424
702
  for (const node of nodes) {
@@ -508,7 +786,10 @@ function StoryTree({ ctx, index }) {
508
786
  "aria-label": node.label,
509
787
  className: "fg-tree__node",
510
788
  children: [
511
- /* @__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 }) }),
789
+ /* @__PURE__ */ jsxs("div", { className: "fg-tree__item fg-tree__folder", onClick: () => setOpen(node.path, !open), children: [
790
+ /* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: node.label }),
791
+ node.tag ? /* @__PURE__ */ jsx("span", { className: "fg-tree__tag", children: node.tag }) : null
792
+ ] }),
512
793
  open ? /* @__PURE__ */ jsx("div", { role: "group", className: "fg-tree__group", children: renderNodes(node.children, level + 1) }) : null
513
794
  ]
514
795
  },
@@ -516,7 +797,7 @@ function StoryTree({ ctx, index }) {
516
797
  );
517
798
  }
518
799
  const { entry } = node;
519
- return /* @__PURE__ */ jsx(
800
+ return /* @__PURE__ */ jsxs(
520
801
  "a",
521
802
  {
522
803
  ...itemProps(node, level),
@@ -529,7 +810,10 @@ function StoryTree({ ctx, index }) {
529
810
  setFocusKey(keyOf(node));
530
811
  activate(entry, event.shiftKey);
531
812
  },
532
- children: /* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: entry.name })
813
+ children: [
814
+ /* @__PURE__ */ jsx("span", { className: "fg-tree__label", children: node.label ?? entry.name }),
815
+ node.tag ? /* @__PURE__ */ jsx("span", { className: "fg-tree__tag", children: node.tag }) : null
816
+ ]
533
817
  },
534
818
  keyOf(node)
535
819
  );
@@ -540,6 +824,30 @@ function StoryTree({ ctx, index }) {
540
824
  "No story ",
541
825
  /* @__PURE__ */ jsx("code", { children: route })
542
826
  ] }) : null,
827
+ /* @__PURE__ */ jsx(
828
+ ToggleBar,
829
+ {
830
+ ariaLabel: "Sidebar view",
831
+ variant: "flat",
832
+ items: VIEWS,
833
+ value: view,
834
+ onChange: (next) => {
835
+ if (next) setView(next);
836
+ }
837
+ }
838
+ ),
839
+ /* @__PURE__ */ jsxs("fieldset", { className: "fg-tree__libraries", children: [
840
+ /* @__PURE__ */ jsx("legend", { className: "fg-tree__libraries-legend", children: "Packages" }),
841
+ libraries.map((library) => /* @__PURE__ */ jsx(
842
+ Checkbox,
843
+ {
844
+ isSelected: !hidden[library],
845
+ onChange: (on) => setHidden((prev) => ({ ...prev, [library]: !on })),
846
+ children: library
847
+ },
848
+ library
849
+ ))
850
+ ] }),
543
851
  /* @__PURE__ */ jsx(
544
852
  "input",
545
853
  {
@@ -942,7 +1250,8 @@ function useStoryRegistry(index, options) {
942
1250
  for (const off of offs) off();
943
1251
  };
944
1252
  }, [cache.books]);
945
- return { instruments: cache.list, onReady };
1253
+ const isReady = useCallback((id) => readies.has(id), [readies]);
1254
+ return { instruments: cache.list, isReady, onReady };
946
1255
  }
947
1256
  var NO_DECLARATIONS = {};
948
1257
  function initialStory(index, fallback) {
@@ -960,11 +1269,27 @@ function RouteOpener({ index }) {
960
1269
  }, [route, index, lab]);
961
1270
  return null;
962
1271
  }
1272
+ function useInfoShortcut(open) {
1273
+ useEffect(() => {
1274
+ const onKeyDown = (event) => {
1275
+ if (event.key !== "i" && event.key !== "I") return;
1276
+ if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return;
1277
+ const target = event.target;
1278
+ if (target?.isContentEditable || /^(input|textarea|select)$/i.test(target?.tagName ?? "")) return;
1279
+ event.preventDefault();
1280
+ open(true);
1281
+ };
1282
+ window.addEventListener("keydown", onKeyDown);
1283
+ return () => window.removeEventListener("keydown", onKeyDown);
1284
+ }, [open]);
1285
+ }
963
1286
  function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }) {
964
1287
  const declarations = config?.globals ?? NO_DECLARATIONS;
965
1288
  const [frames] = useState(createTrialFrames);
966
1289
  const registry = useStoryRegistry(index, { frameUrl, globals: declarations, frames });
967
1290
  const [labValues, setLabValues] = useState(() => labGlobals(declarations, void 0));
1291
+ const [infoOpen, setInfoOpen] = useState(false);
1292
+ useInfoShortcut(setInfoOpen);
968
1293
  const reportLabValues = useCallback(
969
1294
  (next) => setLabValues((prev) => stableStringify(prev) === stableStringify(next) ? prev : next),
970
1295
  []
@@ -972,6 +1297,11 @@ function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }
972
1297
  const labChrome = useMemo(
973
1298
  () => [
974
1299
  { id: "fg-stories", region: "sidebar", render: (ctx) => /* @__PURE__ */ jsx(StoryTree, { ctx, index }) },
1300
+ {
1301
+ id: "fg-info",
1302
+ region: "palette",
1303
+ item: { icon: InfoIcon, label: "Info", shortcut: "\u2318I", onActivate: () => setInfoOpen(true) }
1304
+ },
975
1305
  CSS_VARS_SECTION,
976
1306
  A11Y_SECTION,
977
1307
  ...Object.keys(declarations).length > 0 ? [{ id: "fg-globals", region: "header", render: () => /* @__PURE__ */ jsx(GlobalsToolbar, { declarations }) }] : [],
@@ -990,6 +1320,7 @@ function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }
990
1320
  Lab,
991
1321
  {
992
1322
  title: "weaselforge",
1323
+ density: "roomy",
993
1324
  instruments: registry.instruments,
994
1325
  defaultInstrument: initialStory(index, first.id),
995
1326
  storageKey: storageKey ?? "weaselforge",
@@ -1001,6 +1332,15 @@ function Workshop({ index, frameUrl, config, stories = [], storageKey, storage }
1001
1332
  ...config?.path !== void 0 ? { path: config.path } : {},
1002
1333
  children: [
1003
1334
  /* @__PURE__ */ jsx(RouteOpener, { index }),
1335
+ /* @__PURE__ */ jsx(
1336
+ StoryInfoDialog,
1337
+ {
1338
+ index,
1339
+ isReady: registry.isReady,
1340
+ isOpen: infoOpen,
1341
+ onOpenChange: setInfoOpen
1342
+ }
1343
+ ),
1004
1344
  /* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues })
1005
1345
  ]
1006
1346
  }