@weasel-js/forge 1.10.0 → 1.11.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.
Files changed (38) hide show
  1. package/README.md +1 -3
  2. package/dist/{FrameController-UeTY7ubF.d.ts → FrameController-BRhShqut.d.ts} +5 -0
  3. package/dist/{chunk-FL2MUPUL.js → chunk-2OJAV522.js} +2 -2
  4. package/dist/chunk-2OJAV522.js.map +1 -0
  5. package/dist/{chunk-DFKCLZKS.js → chunk-7BQPHMA2.js} +2 -2
  6. package/dist/chunk-7BQPHMA2.js.map +1 -0
  7. package/dist/{chunk-DKWHGSBZ.js → chunk-DPTECNYC.js} +3 -3
  8. package/dist/{chunk-DKWHGSBZ.js.map → chunk-DPTECNYC.js.map} +1 -1
  9. package/dist/{chunk-ENXU34PI.js → chunk-J3GYSNCK.js} +4 -4
  10. package/dist/{chunk-ENXU34PI.js.map → chunk-J3GYSNCK.js.map} +1 -1
  11. package/dist/{chunk-4NM3RTYK.js → chunk-NVNTQ4AT.js} +89 -11
  12. package/dist/chunk-NVNTQ4AT.js.map +1 -0
  13. package/dist/{chunk-V4RI7GWD.js → chunk-R5F44ECM.js} +17 -7
  14. package/dist/chunk-R5F44ECM.js.map +1 -0
  15. package/dist/{chunk-GB6PRJET.js → chunk-SQ4KCIEH.js} +3 -3
  16. package/dist/{chunk-GB6PRJET.js.map → chunk-SQ4KCIEH.js.map} +1 -1
  17. package/dist/{config-Cz7ZyGF-.d.ts → config-BwScQukB.d.ts} +5 -4
  18. package/dist/csf/preview-api.js +2 -2
  19. package/dist/frame/index.d.ts +3 -2
  20. package/dist/frame/index.js +7 -8
  21. package/dist/frame/index.js.map +1 -1
  22. package/dist/index.d.ts +5 -4
  23. package/dist/index.js +3 -3
  24. package/dist/shell/index.css +8 -0
  25. package/dist/shell/index.css.map +1 -1
  26. package/dist/shell/index.d.ts +4 -3
  27. package/dist/shell/index.js +278 -128
  28. package/dist/shell/index.js.map +1 -1
  29. package/dist/test/index.d.ts +2 -1
  30. package/dist/test/index.js +5 -5
  31. package/dist/vite/index.d.ts +13 -1
  32. package/dist/vite/index.js +11 -2
  33. package/dist/vite/index.js.map +1 -1
  34. package/package.json +8 -6
  35. package/dist/chunk-4NM3RTYK.js.map +0 -1
  36. package/dist/chunk-DFKCLZKS.js.map +0 -1
  37. package/dist/chunk-FL2MUPUL.js.map +0 -1
  38. package/dist/chunk-V4RI7GWD.js.map +0 -1
@@ -1,26 +1,27 @@
1
- import { parseRoute, formatRoute, paramsToKnobs, knobPaths, sameKnob, knobsToParams, PLAYHEAD_PARAM, reservedParams } from '../chunk-DKWHGSBZ.js';
1
+ import { parseRoute, formatRoute, paramsToKnobs, knobPaths, sameKnob, knobsToParams, PLAYHEAD_PARAM, reservedParams } from '../chunk-DPTECNYC.js';
2
2
  import { DEFAULT_TITLE } from '../chunk-5DXUNLV2.js';
3
- import { TITLE_LIBRARIES, isIndexId, indexId, indexEntries, librariesFrom, LibrariesContext, schemaFromDescription, LibraryBadge, openChannel, IndexPage } from '../chunk-V4RI7GWD.js';
3
+ import { TITLE_LIBRARIES, isIndexId, indexId, indexEntries, librariesFrom, LibrariesContext, createKitRoot, schemaFromDescription, LibraryBadge, openChannel, IndexPage } from '../chunk-R5F44ECM.js';
4
4
  import { isGallery } from '../chunk-WEQSLPKT.js';
5
- import { useTrialFrame, createTrialFrames, createTrialClocks, StoryGlobalsContext, TrialFramesContext, TrialClocksContext, StoryTrial, useCssOverrides, useTrialClock, TrialHost } from '../chunk-ENXU34PI.js';
6
- export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-ENXU34PI.js';
7
- import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, PLAYHEAD_KEY, labOptions, isGlobalsPath } from '../chunk-DFKCLZKS.js';
8
- import { loadStories, indexRenderOf, useNearViewport, IN_VIEW_MARGIN } from '../chunk-4NM3RTYK.js';
5
+ import { useTrialFrame, createTrialFrames, createTrialClocks, StoryGlobalsContext, TrialFramesContext, TrialClocksContext, StoryTrial, useCssOverrides, useTrialClock, TrialHost } from '../chunk-J3GYSNCK.js';
6
+ export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-J3GYSNCK.js';
7
+ import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, PLAYHEAD_KEY, labOptions, isGlobalsPath } from '../chunk-7BQPHMA2.js';
8
+ import { loadStories, indexRenderOf, useNearViewport, IN_VIEW_MARGIN } from '../chunk-NVNTQ4AT.js';
9
9
  import '../chunk-L5XM5VKB.js';
10
10
  import '../chunk-ZAWXMUGZ.js';
11
- import '../chunk-GB6PRJET.js';
12
- import { stableStringify, PORT_HANDOFF, FRAME_HELLO, PROTOCOL_VERSION } from '../chunk-FL2MUPUL.js';
13
- import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, useTrialState, inferTokenType, ToggleBar as ToggleBar$1, Input, TokenPanel, refitScale } from '@weasel-js/labkit';
11
+ import '../chunk-SQ4KCIEH.js';
12
+ import { stableStringify, PORT_HANDOFF, FRAME_HELLO, PROTOCOL_VERSION } from '../chunk-2OJAV522.js';
13
+ import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, useTrialState, ToggleBar as ToggleBar$1, Input, TokenPanel, inferTokenType, refitScale } from '@weasel-js/labkit';
14
14
  import num from '@weasel-js/theme/numeric.module.css';
15
15
  import { createContext, useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect, useId, useContext, Fragment as Fragment$1, useSyncExternalStore } from 'react';
16
16
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
17
- import { createRoot } from 'react-dom/client';
18
17
  import { createEmitter } from '@weasel-js/registry';
19
18
  import { f, valueAtPath, isConfigBranch } from '@weasel-js/labkit/config';
20
- import { InfoIcon, ToggleBar, Checkbox, Dialog, TOKEN_CATEGORIES, Icon, DisclosureMark, Button as Button$1, Callout, Slider, Transport, SearchIcon } from '@weasel-js/ui';
21
- import { TOKEN_MANIFEST, THEME_SOURCES, isByAxis } from '@weasel-js/theme';
19
+ import { formatShortcut, ToggleBar, Checkbox, Dialog, TOKEN_CATEGORIES, Icon, DisclosureMark, Button as Button$1, Callout, Slider, Transport } from '@weasel-js/ui';
20
+ import { TOKEN_MANIFEST, themeAxes, weaselTheme, THEME_SOURCES, resolveTheme, isColorModePreference, isByAxis } from '@weasel-js/theme';
22
21
  import { scale, httpThemeApi } from '@weasel-js/theme/engine';
23
22
  import { useLatest, historyKey, createHistory, swapOp, isPlainObject } from '@weasel-js/core';
23
+ import { useResolvedColorMode } from '@weasel-js/theme/react';
24
+ import { pref } from '@weasel-js/prefs';
24
25
 
25
26
  function Finding({ finding }) {
26
27
  return /* @__PURE__ */ jsxs("li", { className: "fg-a11y__finding", children: [
@@ -339,12 +340,30 @@ function missingInstrument(id, globals = {}) {
339
340
  };
340
341
  }
341
342
  var ICONS = {
342
- color: "drop",
343
- type: "type",
344
- size: "measure",
345
- motion: "curveEaseInOut",
346
- depth: "layers",
347
- other: "more"
343
+ color: (
344
+ /* @icon */
345
+ "ui:drop"
346
+ ),
347
+ type: (
348
+ /* @icon */
349
+ "ui:type"
350
+ ),
351
+ size: (
352
+ /* @icon */
353
+ "core:measure"
354
+ ),
355
+ motion: (
356
+ /* @icon */
357
+ "ui:curveEaseInOut"
358
+ ),
359
+ depth: (
360
+ /* @icon */
361
+ "ui:layers"
362
+ ),
363
+ other: (
364
+ /* @icon */
365
+ "ui:more"
366
+ )
348
367
  };
349
368
  var PANEL = ".fg-css-vars";
350
369
  var HOST = ".lk-sidebar-section, .lk-panel-tile";
@@ -553,15 +572,24 @@ function applyTokenEdits(definition, edits, selection) {
553
572
  }
554
573
  return { ok: true, definition: root };
555
574
  }
575
+ function useTrialSelection(axes, globals) {
576
+ const { mode } = globals;
577
+ const osMode = useResolvedColorMode(isColorModePreference(mode) ? mode : "auto");
578
+ return useMemo(() => {
579
+ const selection = selectionFor(axes, globals);
580
+ return axes.mode && osMode in axes.mode.values ? { mode: osMode, ...selection } : selection;
581
+ }, [axes, globals, osMode]);
582
+ }
556
583
 
557
584
  // src/shell/cssVars/useThemeSave.ts
558
585
  var THEME_NAME = "weasel";
559
586
  var TOKEN_PREFIX = "--wzl-";
587
+ var NO_AXES = {};
560
588
  function useThemeSave({ trialId, globals, themeApi, scales, overrides, history: history2 }) {
589
+ const definition = THEME_SOURCES[THEME_NAME];
590
+ const selection = useTrialSelection(definition?.axes ?? NO_AXES, globals);
561
591
  const tokens = useMemo(() => {
562
- const definition = THEME_SOURCES[THEME_NAME];
563
592
  if (!definition) return {};
564
- const selection = selectionFor(definition.axes ?? {}, globals);
565
593
  const generated = new Set(scales.flatMap((scale) => scale.tokens));
566
594
  return Object.fromEntries(
567
595
  Object.entries(overrides).flatMap(([name, value]) => {
@@ -570,7 +598,7 @@ function useThemeSave({ trialId, globals, themeApi, scales, overrides, history:
570
598
  return writtenOut(definition, token, selection) ? [[token, value]] : [];
571
599
  })
572
600
  );
573
- }, [globals, scales, overrides]);
601
+ }, [definition, selection, scales, overrides]);
574
602
  const [saving, setSaving] = useState(false);
575
603
  const [held, setHeld] = useState(null);
576
604
  const setNotice = (next) => setHeld(next && { ...next, trialId });
@@ -585,7 +613,6 @@ function useThemeSave({ trialId, globals, themeApi, scales, overrides, history:
585
613
  setNotice({ tone: "error", text: `${file} has changed since this page loaded. Reload to edit against it.` });
586
614
  return;
587
615
  }
588
- const selection = selectionFor(stored.definition.axes ?? {}, globals);
589
616
  const scaled = applyScaleEdits(stored.definition, Object.fromEntries(scales.map((s) => [s.name, s.edit])), selection);
590
617
  const next = scaled.ok ? applyTokenEdits(scaled.definition, tokens, selection) : scaled;
591
618
  if (!next.ok) {
@@ -627,6 +654,39 @@ function useThemeSave({ trialId, globals, themeApi, scales, overrides, history:
627
654
  save
628
655
  };
629
656
  }
657
+ var MANIFEST = new Map(TOKEN_MANIFEST.map((token) => [token.name, token]));
658
+ function groupOf(name) {
659
+ const segments = name.replace(/^--/, "").split("-");
660
+ return segments.length > 1 ? segments.slice(0, -1).join("-") : name.replace(/^--/, "");
661
+ }
662
+ function useStoryTokens(vars) {
663
+ const [showUnused, setShowUnused] = usePersistedState("fg-css-vars-unused", false, { scope: "lab" });
664
+ const tokens = useMemo(
665
+ () => vars.flatMap(({ name, value, used }) => {
666
+ if (!used && !showUnused) return [];
667
+ const known = MANIFEST.get(name);
668
+ return [{ name, type: known?.type ?? inferTokenType(value), group: known?.group ?? groupOf(name), value }];
669
+ }),
670
+ [vars, showUnused]
671
+ );
672
+ const emptyText = vars.length > 0 && !showUnused ? "The story uses no variables." : "The story\u2019s frame has reported no variables.";
673
+ return { tokens, showUnused, setShowUnused, emptyText };
674
+ }
675
+ var AXES = themeAxes(weaselTheme);
676
+ function useThemeTokens(globals, vars) {
677
+ const selection = useTrialSelection(AXES, globals);
678
+ const themed = useMemo(() => resolveTheme(weaselTheme, selection), [selection]);
679
+ return useMemo(() => {
680
+ const reported = new Map(vars.map((v) => [v.name, v.value]));
681
+ return TOKEN_MANIFEST.map((token) => ({
682
+ name: token.name,
683
+ type: token.type,
684
+ group: token.group,
685
+ value: reported.get(token.name) ?? themed[token.name] ?? token.defaultValue,
686
+ description: token.description
687
+ }));
688
+ }, [vars, themed]);
689
+ }
630
690
  var SWAP = "fg.vars.swap";
631
691
  var COALESCE_MS = 800;
632
692
  var LIMIT = 100;
@@ -690,13 +750,13 @@ var TABS = [
690
750
  { value: "theme", label: "Theme" },
691
751
  { value: "story", label: "Story" }
692
752
  ];
693
- var MANIFEST = new Map(TOKEN_MANIFEST.map((token) => [token.name, token]));
753
+ var MANIFEST2 = new Map(TOKEN_MANIFEST.map((token) => [token.name, token]));
694
754
  var literal = (v) => typeof v === "number";
695
755
  var SCALE_SOURCES = Object.entries(THEME_SOURCES[THEME_NAME]?.scales ?? {}).flatMap(([name, def]) => {
696
756
  if (!Array.isArray(def.steps)) return [];
697
757
  const steps = def.steps;
698
758
  const tokens = steps.map((step) => `${TOKEN_PREFIX}${name}-${step}`);
699
- const group = MANIFEST.get(tokens[0] ?? "")?.group;
759
+ const group = MANIFEST2.get(tokens[0] ?? "")?.group;
700
760
  if (!group) return [];
701
761
  const factors = Array.isArray(def.factors) && def.factors.every(literal) ? def.factors : null;
702
762
  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";
@@ -726,10 +786,6 @@ function standingScale(source, amounts) {
726
786
  const base = (amounts[at] ?? 0) / (rule.factors[at] ?? 1);
727
787
  return { ...seed, base: Math.round(base * 100) / 100, rule };
728
788
  }
729
- function groupOf(name) {
730
- const segments = name.replace(/^--/, "").split("-");
731
- return segments.length > 1 ? segments.slice(0, -1).join("-") : name.replace(/^--/, "");
732
- }
733
789
  var NO_GLOBALS = {};
734
790
  function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
735
791
  const trialId = useTrialId();
@@ -743,22 +799,9 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
743
799
  const [edited, setEdited] = usePersistedState("fg-css-vars-scales", {}, { scope: "trial" });
744
800
  const [tab, setTab] = useState("theme");
745
801
  const [filter, setFilter] = useState("");
746
- const tokens = useMemo(() => {
747
- if (tab === "story") {
748
- return frame.vars.map(({ name, value }) => {
749
- const known = MANIFEST.get(name);
750
- return { name, type: known?.type ?? inferTokenType(value), group: known?.group ?? groupOf(name), value };
751
- });
752
- }
753
- const reported = new Map(frame.vars.map((v) => [v.name, v.value]));
754
- return TOKEN_MANIFEST.map((token) => ({
755
- name: token.name,
756
- type: token.type,
757
- group: token.group,
758
- value: reported.get(token.name) ?? token.defaultValue,
759
- description: token.description
760
- }));
761
- }, [tab, frame.vars]);
802
+ const themeTokens = useThemeTokens(globals, frame.vars);
803
+ const story = useStoryTokens(frame.vars);
804
+ const tokens = tab === "theme" ? themeTokens : story.tokens;
762
805
  const query = filter.trim().toLowerCase();
763
806
  const shown = tokens.map((token) => {
764
807
  const override = overrides[token.name];
@@ -850,27 +893,27 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
850
893
  placeholder: "Filter",
851
894
  value: filter,
852
895
  onChange: setFilter,
853
- leadingAdornment: /* @__PURE__ */ jsx(SearchIcon, { size: 12 })
896
+ leadingAdornment: /* @__PURE__ */ jsx(Icon, { name: (
897
+ /* @icon */
898
+ "ui:search"
899
+ ), size: 12 })
854
900
  }
855
901
  ),
856
902
  /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__steps", role: "group", "aria-label": "History", children: [
857
- /* @__PURE__ */ jsx(Button$1, { iconOnly: true, ariaLabel: "Undo", variant: "ghost", size: "sm", disabled: !history2.canUndo, onClick: () => history2.step("undo"), children: /* @__PURE__ */ jsx(Icon, { name: "undo", size: 14 }) }),
858
- /* @__PURE__ */ jsx(Button$1, { iconOnly: true, ariaLabel: "Redo", variant: "ghost", size: "sm", disabled: !history2.canRedo, onClick: () => history2.step("redo"), children: /* @__PURE__ */ jsx(Icon, { name: "redo", size: 14 }) })
903
+ /* @__PURE__ */ jsx(Button$1, { iconOnly: true, ariaLabel: "Undo", variant: "ghost", size: "sm", disabled: !history2.canUndo, onClick: () => history2.step("undo"), children: /* @__PURE__ */ jsx(Icon, { name: (
904
+ /* @icon */
905
+ "core:undo"
906
+ ), size: 14 }) }),
907
+ /* @__PURE__ */ jsx(Button$1, { iconOnly: true, ariaLabel: "Redo", variant: "ghost", size: "sm", disabled: !history2.canRedo, onClick: () => history2.step("redo"), children: /* @__PURE__ */ jsx(Icon, { name: (
908
+ /* @icon */
909
+ "core:redo"
910
+ ), size: 14 }) })
859
911
  ] })
860
912
  ] }),
861
- tab === "theme" && (pending || notice) ? /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__save", children: [
862
- pending ? /* @__PURE__ */ jsx(Button$1, { variant: "primary", size: "sm", loading: saving, disabled: saving, onClick: () => void save(), children: "Save to theme" }) : null,
863
- notice ? /* @__PURE__ */ jsx(
864
- "p",
865
- {
866
- className: `fg-css-vars__notice fg-css-vars__notice--${notice.tone}`,
867
- role: notice.tone === "error" ? "alert" : "status",
868
- children: notice.text
869
- }
870
- ) : null
871
- ] }) : null
913
+ tab === "story" ? /* @__PURE__ */ jsx(Checkbox, { className: "fg-css-vars__unused", isSelected: story.showUnused, onChange: story.setShowUnused, children: "Show variables the story doesn\u2019t use" }) : null,
914
+ tab === "theme" ? /* @__PURE__ */ jsx(SaveRow, { pending, saving, notice, onSave: () => void save() }) : null
872
915
  ] }),
873
- shown.length === 0 ? /* @__PURE__ */ jsx("p", { className: "fg-css-vars__empty", children: tokens.length === 0 ? "The story\u2019s frame has reported no variables." : "No variables match." }) : /* @__PURE__ */ jsx("div", { className: "fg-css-vars__list", children: /* @__PURE__ */ jsx(
916
+ shown.length === 0 ? /* @__PURE__ */ jsx("p", { className: "fg-css-vars__empty", children: tokens.length > 0 ? "No variables match." : tab === "story" ? story.emptyText : "The story\u2019s frame has reported no variables." }) : /* @__PURE__ */ jsx("div", { className: "fg-css-vars__list", children: /* @__PURE__ */ jsx(
874
917
  TokenPanel,
875
918
  {
876
919
  tokens: shown,
@@ -887,6 +930,13 @@ function CssVarsPanel({ globals = NO_GLOBALS, themeApi } = {}) {
887
930
  )
888
931
  );
889
932
  }
933
+ function SaveRow({ pending, saving, notice, onSave }) {
934
+ if (!pending && !notice) return null;
935
+ return /* @__PURE__ */ jsxs("div", { className: "fg-css-vars__save", children: [
936
+ pending ? /* @__PURE__ */ jsx(Button$1, { variant: "primary", size: "sm", loading: saving, disabled: saving, onClick: onSave, children: "Save to theme" }) : null,
937
+ notice ? /* @__PURE__ */ jsx("p", { className: `fg-css-vars__notice fg-css-vars__notice--${notice.tone}`, role: notice.tone === "error" ? "alert" : "status", children: notice.text }) : null
938
+ ] });
939
+ }
890
940
  function FocusedCssVars() {
891
941
  const { focusedTrialId, trials, instruments } = useLabContext();
892
942
  const labGlobals2 = useContext(StoryGlobalsContext);
@@ -904,6 +954,19 @@ var CSS_VARS_SECTION = {
904
954
  region: "aside",
905
955
  item: { title: "CSS Vars", actions: /* @__PURE__ */ jsx(SectionJump, {}), fill: true, body: /* @__PURE__ */ jsx(FocusedCssVars, {}) }
906
956
  };
957
+ var KEYS = { kind: "key", key: "i", mods: { mod: true } };
958
+ function infoItem(open) {
959
+ return {
960
+ icon: (
961
+ /* @icon */
962
+ "ui:info"
963
+ ),
964
+ label: "Info",
965
+ shortcut: formatShortcut({ key: KEYS.key, mod: KEYS.mods.mod }),
966
+ keys: KEYS,
967
+ onActivate: open
968
+ };
969
+ }
907
970
  var GLOBALS = "$globals";
908
971
  function leaves(shape, prefix = "") {
909
972
  const out = [];
@@ -1036,10 +1099,10 @@ var whenIdle = (fn) => {
1036
1099
  return () => clearTimeout(handle);
1037
1100
  };
1038
1101
  function createFramePool(frameUrl, size = 2) {
1039
- const shelf = document.createElement("div");
1040
- shelf.className = "fg-frame-pool";
1041
- shelf.setAttribute("aria-hidden", "true");
1042
- document.body.append(shelf);
1102
+ const shelf2 = document.createElement("div");
1103
+ shelf2.className = "fg-frame-pool";
1104
+ shelf2.setAttribute("aria-hidden", "true");
1105
+ document.body.append(shelf2);
1043
1106
  const warm = [];
1044
1107
  const waits = /* @__PURE__ */ new Set();
1045
1108
  let cancelRefill = null;
@@ -1066,7 +1129,7 @@ function createFramePool(frameUrl, size = 2) {
1066
1129
  }, HELLO_TIMEOUT_MS);
1067
1130
  waits.add(stop);
1068
1131
  window.addEventListener("message", onMessage);
1069
- shelf.append(frame);
1132
+ shelf2.append(frame);
1070
1133
  };
1071
1134
  const refill = () => {
1072
1135
  if (disposed || cancelRefill) return;
@@ -1087,7 +1150,7 @@ function createFramePool(frameUrl, size = 2) {
1087
1150
  disposed = true;
1088
1151
  cancelRefill?.();
1089
1152
  for (const stop of [...waits]) stop();
1090
- shelf.remove();
1153
+ shelf2.remove();
1091
1154
  }
1092
1155
  };
1093
1156
  }
@@ -1097,7 +1160,7 @@ var FramePoolContext = createContext(null);
1097
1160
  function routedTrial(trials, focusedTrialId, route) {
1098
1161
  return trials.find((t) => t.id === focusedTrialId && t.instrumentName === route) ?? trials.find((t) => t.instrumentName === route);
1099
1162
  }
1100
- function PlayheadSync({ trialId, route, applied }) {
1163
+ function PlayheadSync({ trialId, route, appliedRef }) {
1101
1164
  const clock = useTrialClock(trialId);
1102
1165
  useEffect(() => {
1103
1166
  if (!clock) return;
@@ -1115,8 +1178,8 @@ function PlayheadSync({ trialId, route, applied }) {
1115
1178
  held = next;
1116
1179
  writePlayheadParam(next);
1117
1180
  };
1118
- if (applied.current !== route) {
1119
- applied.current = route;
1181
+ if (appliedRef.current !== route) {
1182
+ appliedRef.current = route;
1120
1183
  fromUrl();
1121
1184
  }
1122
1185
  toUrl();
@@ -1132,7 +1195,7 @@ function PlayheadSync({ trialId, route, applied }) {
1132
1195
  off();
1133
1196
  window.removeEventListener("hashchange", onHashChange);
1134
1197
  };
1135
- }, [clock, route, applied]);
1198
+ }, [clock, route, appliedRef]);
1136
1199
  return null;
1137
1200
  }
1138
1201
  function RoutePlayhead() {
@@ -1141,7 +1204,7 @@ function RoutePlayhead() {
1141
1204
  const applied = useRef(null);
1142
1205
  const trial = route === null ? void 0 : routedTrial(lab.trials, lab.focusedTrialId, route);
1143
1206
  if (!trial || route === null) return null;
1144
- return /* @__PURE__ */ jsx(PlayheadSync, { trialId: trial.id, route, applied }, `${trial.id} ${route}`);
1207
+ return /* @__PURE__ */ jsx(PlayheadSync, { trialId: trial.id, route, appliedRef: applied }, `${trial.id} ${route}`);
1145
1208
  }
1146
1209
  var RECORD = "fg-globals";
1147
1210
  function useLabGlobals(declarations) {
@@ -1177,7 +1240,8 @@ function GlobalsToolbar({ declarations }) {
1177
1240
  NestedGlobals,
1178
1241
  {
1179
1242
  label: declaration.label,
1180
- icon: declaration.icon ?? "tune",
1243
+ icon: declaration.icon ?? /* @icon */
1244
+ "core:tune",
1181
1245
  nested,
1182
1246
  values,
1183
1247
  set
@@ -1235,6 +1299,41 @@ function LabGlobals({
1235
1299
  useEffect(() => onChange(resolved), [resolved, onChange]);
1236
1300
  return null;
1237
1301
  }
1302
+ var leafPath = (n) => `globals.d${n}.v`;
1303
+ function globalsMenu(declarations, values, set) {
1304
+ const entries = Object.entries(declarations);
1305
+ if (entries.length === 0) return void 0;
1306
+ const submenus = {};
1307
+ const current = {};
1308
+ const keys = /* @__PURE__ */ new Map();
1309
+ entries.forEach(([key, declaration], n) => {
1310
+ const options = labOptions(declaration, values);
1311
+ submenus[`d${n}`] = pref.group(
1312
+ declaration.label,
1313
+ { v: pref.enum(declaration.label, declaration.default, options) },
1314
+ { as: "menu" }
1315
+ );
1316
+ current[leafPath(n)] = values[key];
1317
+ keys.set(leafPath(n), key);
1318
+ });
1319
+ return {
1320
+ schema: pref.group("", { globals: pref.group("Globals", submenus, { as: "menu" }) }),
1321
+ values: current,
1322
+ onChange: (path, value) => {
1323
+ const key = keys.get(path);
1324
+ if (key !== void 0 && typeof value === "string") set(key, value);
1325
+ }
1326
+ };
1327
+ }
1328
+ function GlobalsMenus({
1329
+ declarations,
1330
+ onMenus
1331
+ }) {
1332
+ const [values, set] = useLabGlobals(declarations);
1333
+ const menus = useMemo(() => globalsMenu(declarations, values, set), [declarations, values, set]);
1334
+ useEffect(() => onMenus(menus), [menus, onMenus]);
1335
+ return null;
1336
+ }
1238
1337
  function KnobSync({ schema, config }) {
1239
1338
  const { setConfig } = useTrialState();
1240
1339
  const reading = useRef(true);
@@ -1481,13 +1580,13 @@ function StoryTree({ ctx, index }) {
1481
1580
  }, [index]);
1482
1581
  const enabled = (value) => views.some((item) => item.value === value && !item.disabled);
1483
1582
  const view = [chosen, "components", "gallery"].find(enabled) ?? chosen;
1484
- const kept = useMemo(() => index.filter((entry) => !hidden[table.of(entry)]), [index, hidden, table]);
1583
+ const kept2 = useMemo(() => index.filter((entry) => !hidden[table.of(entry)]), [index, hidden, table]);
1485
1584
  const [galleryEntries, rest] = useMemo(() => {
1486
1585
  const yes = [];
1487
1586
  const no = [];
1488
- for (const entry of kept) (isGallery(entry) ? yes : no).push(entry);
1587
+ for (const entry of kept2) (isGallery(entry) ? yes : no).push(entry);
1489
1588
  return [yes, no];
1490
- }, [kept]);
1589
+ }, [kept2]);
1491
1590
  const tree = useMemo(() => buildTree(rest), [rest]);
1492
1591
  const components = useMemo(() => buildComponents(rest, table), [rest, table]);
1493
1592
  const galleries = useMemo(() => buildComponents(galleryEntries, table), [galleryEntries, table]);
@@ -1495,7 +1594,7 @@ function StoryTree({ ctx, index }) {
1495
1594
  () => view === "tree" ? filterTree(tree, query) : componentNodes(filterComponents(view === "gallery" ? galleries : components, query)),
1496
1595
  [view, components, galleries, tree, query]
1497
1596
  );
1498
- const routable = useMemo(() => [...kept, ...indexEntries(kept)], [kept]);
1597
+ const routable = useMemo(() => [...kept2, ...indexEntries(kept2)], [kept2]);
1499
1598
  const routed = routable.find((entry) => entry.id === route);
1500
1599
  const routeAncestors = useMemo(() => ancestorsOf(routed), [routed]);
1501
1600
  const filtering = query.trim() !== "";
@@ -1786,6 +1885,54 @@ function createAnswerBook() {
1786
1885
 
1787
1886
  // src/shell/readyKey.ts
1788
1887
  var readyKey = (ready) => stableStringify({ schema: ready.schema, layout: ready.layout, viewport: ready.viewport });
1888
+ var kept = /* @__PURE__ */ new Map();
1889
+ var shelf = null;
1890
+ function shelfElement() {
1891
+ if (!shelf) {
1892
+ shelf = document.createElement("div");
1893
+ shelf.className = "fg-frame-pool";
1894
+ shelf.setAttribute("aria-hidden", "true");
1895
+ }
1896
+ if (!shelf.isConnected) document.body.append(shelf);
1897
+ return shelf;
1898
+ }
1899
+ function adopt({ key, slot, frame, link, onAdopt }) {
1900
+ const waiting = key === null ? void 0 : kept.get(key);
1901
+ if (key === null || !waiting || !slot.current) return;
1902
+ kept.delete(key);
1903
+ moveFrame(slot.current, waiting.frame);
1904
+ frame.current = waiting.frame;
1905
+ link.current = waiting.link;
1906
+ onAdopt(waiting.link);
1907
+ }
1908
+ function shelve(options) {
1909
+ const { key, frame, link, onDrop } = options.current;
1910
+ const element = frame.current;
1911
+ const current = link.current;
1912
+ if (key === null || !element?.isConnected || !current || !canMoveFrames()) return false;
1913
+ if (current.awaiting === null && current.sent === null) return false;
1914
+ const entry = { frame: element, link: current };
1915
+ moveFrame(shelfElement(), element);
1916
+ kept.set(key, entry);
1917
+ queueMicrotask(() => {
1918
+ if (kept.get(key) !== entry) return;
1919
+ kept.delete(key);
1920
+ onDrop();
1921
+ element.remove();
1922
+ });
1923
+ return true;
1924
+ }
1925
+ function useKeptFrame(options) {
1926
+ const handedOn = useRef(false);
1927
+ const latest = useLatest(options);
1928
+ useLayoutEffect(() => {
1929
+ adopt(latest.current);
1930
+ return () => {
1931
+ if (shelve(latest)) handedOn.current = true;
1932
+ };
1933
+ }, [latest]);
1934
+ return useCallback(() => handedOn.current, []);
1935
+ }
1789
1936
  var START_TIMEOUT_MS = 1e4;
1790
1937
  var requests = 0;
1791
1938
  function closeLink(link) {
@@ -1800,6 +1947,27 @@ function closeLink(link) {
1800
1947
  function mismatchMessage(mismatch) {
1801
1948
  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";
1802
1949
  }
1950
+ function sendInputs(current, sent, next) {
1951
+ const before = current.inputs;
1952
+ const { state, globals } = next;
1953
+ const config = storyConfig(next.config, sent.config);
1954
+ if (config !== sent.config) {
1955
+ sent.config = config;
1956
+ current.channel.send({ type: "config", config });
1957
+ current.inputs += 1;
1958
+ }
1959
+ if (state !== sent.state) {
1960
+ sent.state = state;
1961
+ current.channel.send({ type: "state", state });
1962
+ current.inputs += 1;
1963
+ }
1964
+ if (globals !== sent.globals && stableStringify(globals) !== stableStringify(sent.globals)) {
1965
+ sent.globals = globals;
1966
+ current.channel.send({ type: "globals", globals });
1967
+ current.inputs += 1;
1968
+ }
1969
+ return current.inputs !== before;
1970
+ }
1803
1971
  function FrameView(props) {
1804
1972
  const { entry, frameUrl, answers, descriptionKey, ctx } = props;
1805
1973
  const labGlobals2 = useContext(StoryGlobalsContext);
@@ -1906,6 +2074,7 @@ function FrameView(props) {
1906
2074
  };
1907
2075
  const current = {
1908
2076
  channel,
2077
+ deliver: (msg) => receive(current, msg),
1909
2078
  timer,
1910
2079
  awaiting: null,
1911
2080
  sent: null,
@@ -1916,23 +2085,35 @@ function FrameView(props) {
1916
2085
  queued: []
1917
2086
  };
1918
2087
  link.current = current;
1919
- channel.on((msg) => receive(current, msg));
2088
+ channel.on((msg) => current.deliver(msg));
1920
2089
  target.postMessage({ type: PORT_HANDOFF, id: latestRef.current.entry.id }, location.origin, [
1921
2090
  port2
1922
2091
  ]);
1923
2092
  };
1924
2093
  const connectRef = useLatest(connect);
2094
+ const handedOn = useKeptFrame({
2095
+ key: trialId ? `${trialId} ${src}` : null,
2096
+ slot: slotRef,
2097
+ frame: iframeRef,
2098
+ link,
2099
+ onAdopt: (adopted) => {
2100
+ adopted.deliver = (msg) => receive(adopted, msg);
2101
+ setPending(!adopted.hasRendered);
2102
+ },
2103
+ onDrop: () => closeLink(link)
2104
+ });
1925
2105
  useEffect(() => {
1926
2106
  const slot = slotRef.current;
1927
2107
  if (!inView || !slot) return;
1928
- const claimed = latestRef.current.pool?.claim() ?? null;
1929
- const frame = claimed ?? document.createElement("iframe");
2108
+ const adopted = iframeRef.current;
2109
+ const claimed = adopted ? null : latestRef.current.pool?.claim() ?? null;
2110
+ const frame = adopted ?? claimed ?? document.createElement("iframe");
1930
2111
  frame.className = "fg-frame-view";
1931
2112
  frame.title = latestRef.current.title;
1932
2113
  frame.removeAttribute("tabindex");
1933
2114
  frame.toggleAttribute("data-pending", true);
1934
2115
  iframeRef.current = frame;
1935
- const unheard = claimed ? void 0 : setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), HELLO_TIMEOUT_MS);
2116
+ const unheard = adopted || claimed ? void 0 : setTimeout(() => setFault({ phase: null, message: `Frame did not start: ${src}` }), HELLO_TIMEOUT_MS);
1936
2117
  const onMessage = (event) => {
1937
2118
  if (event.source !== frame.contentWindow || event.origin !== location.origin) return;
1938
2119
  if (event.data?.type !== FRAME_HELLO) return;
@@ -1943,20 +2124,21 @@ function FrameView(props) {
1943
2124
  if (claimed) {
1944
2125
  moveFrame(slot, claimed);
1945
2126
  connectRef.current(claimed);
1946
- } else {
2127
+ } else if (!adopted) {
1947
2128
  frame.src = src;
1948
2129
  slot.append(frame);
1949
2130
  }
1950
2131
  return () => {
1951
2132
  window.removeEventListener("message", onMessage);
1952
2133
  clearTimeout(unheard);
2134
+ if (handedOn()) return;
1953
2135
  closeLink(link);
1954
2136
  frame.remove();
1955
2137
  iframeRef.current = null;
1956
2138
  setPending(true);
1957
2139
  setFault(null);
1958
2140
  };
1959
- }, [inView, src, connectRef, latestRef]);
2141
+ }, [inView, src, connectRef, latestRef, handedOn]);
1960
2142
  useEffect(() => {
1961
2143
  iframeRef.current?.toggleAttribute("data-pending", pending && !fault);
1962
2144
  }, [pending, fault, inView, src]);
@@ -1971,29 +2153,9 @@ function FrameView(props) {
1971
2153
  if (current.awaiting === descriptionKey) init(current);
1972
2154
  return;
1973
2155
  }
1974
- const sent = current.sent;
1975
- if (!sent) return;
1976
- let input = false;
1977
- const config = storyConfig(ctx.config, sent.config);
1978
- if (config !== sent.config) {
1979
- sent.config = config;
1980
- current.channel.send({ type: "config", config });
1981
- current.inputs += 1;
1982
- input = true;
1983
- }
1984
- if (ctx.state !== sent.state) {
1985
- sent.state = ctx.state;
1986
- current.channel.send({ type: "state", state: ctx.state });
1987
- current.inputs += 1;
1988
- input = true;
2156
+ if (current.sent && sendInputs(current, current.sent, { config: ctx.config, state: ctx.state, globals })) {
2157
+ setFault((shown) => shown?.phase === "render" ? null : shown);
1989
2158
  }
1990
- if (globals !== sent.globals && stableStringify(globals) !== stableStringify(sent.globals)) {
1991
- sent.globals = globals;
1992
- current.channel.send({ type: "globals", globals });
1993
- current.inputs += 1;
1994
- input = true;
1995
- }
1996
- if (input) setFault((shown) => shown?.phase === "render" ? null : shown);
1997
2159
  }, [ctx.config, ctx.state, globals, descriptionKey, init]);
1998
2160
  return /* @__PURE__ */ jsxs("div", { ref: hostRef, className: "fg-frame-host", children: [
1999
2161
  /* @__PURE__ */ jsx("div", { ref: slotRef, className: "fg-frame-slot" }),
@@ -2172,7 +2334,7 @@ function useStoryRegistry(index, options) {
2172
2334
  const answers = previous.books.get(entry.id) ?? createAnswerBook();
2173
2335
  books.set(entry.id, answers);
2174
2336
  const held = previous.built.get(entry.id);
2175
- const kept = held && held.entryKey === entryKey && held.ready === ready && held.loaded === had && held.fault === fault && held.frameUrl === frameUrl && held.globals === globals && held.setup === setup && held.inDocument === inDocument && held.generation === generation && held.revision === revision ? held : {
2337
+ const kept2 = held && held.entryKey === entryKey && held.ready === ready && held.loaded === had && held.fault === fault && held.frameUrl === frameUrl && held.globals === globals && held.setup === setup && held.inDocument === inDocument && held.generation === generation && held.revision === revision ? held : {
2176
2338
  entryKey,
2177
2339
  ready,
2178
2340
  loaded: had,
@@ -2185,8 +2347,8 @@ function useStoryRegistry(index, options) {
2185
2347
  revision,
2186
2348
  instrument: !inDocument ? storyInstrument({ entry, ready, answers, frameUrl, onReady, globals }) : had ? documentInstrument({ entry, loaded: had, setup, globals }) : pendingInstrument({ entry, globals, load: request, generation, ...fault ? { fault } : {} })
2187
2349
  };
2188
- built.set(entry.id, kept);
2189
- return kept.instrument;
2350
+ built.set(entry.id, kept2);
2351
+ return kept2.instrument;
2190
2352
  });
2191
2353
  const unchanged = list.length === previous.list.length && list.every((i, n) => i === previous.list[n]);
2192
2354
  return { built, books, list: unchanged ? previous.list : list };
@@ -2224,26 +2386,15 @@ function RouteOpener() {
2224
2386
  }, [route, lab]);
2225
2387
  return null;
2226
2388
  }
2227
- function useInfoShortcut(open) {
2389
+ function useDocumentTitle(title) {
2228
2390
  useEffect(() => {
2229
- const onKeyDown = (event) => {
2230
- if (event.key !== "i" && event.key !== "I") return;
2231
- if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return;
2232
- const target = event.target;
2233
- if (target?.isContentEditable || /^(input|textarea|select)$/i.test(target?.tagName ?? "")) return;
2234
- event.preventDefault();
2235
- open(true);
2236
- };
2237
- window.addEventListener("keydown", onKeyDown);
2238
- return () => window.removeEventListener("keydown", onKeyDown);
2239
- }, [open]);
2391
+ document.title = title;
2392
+ }, [title]);
2240
2393
  }
2241
2394
  function Workshop({ index, frameUrl, importers, setup, changes, config, stories = [], storageKey, storage, dependencies }) {
2242
2395
  const declarations = config?.globals ?? NO_DECLARATIONS;
2243
2396
  const title = config?.title ?? DEFAULT_TITLE;
2244
- useEffect(() => {
2245
- document.title = title;
2246
- }, [title]);
2397
+ useDocumentTitle(title);
2247
2398
  const [frames] = useState(createTrialFrames);
2248
2399
  const [clocks] = useState(createTrialClocks);
2249
2400
  const [pool, setPool] = useState(null);
@@ -2279,15 +2430,11 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2279
2430
  const themeFor = config?.labTheme;
2280
2431
  const labTheme = useMemo(() => themeFor?.(labValues), [themeFor, labValues]);
2281
2432
  const [infoOpen, setInfoOpen] = useState(false);
2282
- useInfoShortcut(setInfoOpen);
2283
2433
  const reportLabValues = useCallback(
2284
2434
  (next) => setLabValues((prev) => stableStringify(prev) === stableStringify(next) ? prev : next),
2285
2435
  []
2286
2436
  );
2287
- const infoCommand = useMemo(
2288
- () => ({ icon: InfoIcon, label: "Info", shortcut: "\u2318I", onActivate: () => setInfoOpen(true) }),
2289
- []
2290
- );
2437
+ const infoCommand = useMemo(() => infoItem(() => setInfoOpen(true)), []);
2291
2438
  const labChrome = useMemo(
2292
2439
  () => [
2293
2440
  { id: "fg-stories", region: "sidebar", render: (ctx) => /* @__PURE__ */ jsx(StoryTree, { ctx, index }) },
@@ -2299,6 +2446,7 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2299
2446
  ],
2300
2447
  [index, infoCommand, declarations, config?.cssVars, config?.labChrome]
2301
2448
  );
2449
+ const [menus, setMenus] = useState();
2302
2450
  const first = index[0];
2303
2451
  if (!first) {
2304
2452
  return /* @__PURE__ */ jsxs("div", { className: "fg-empty", children: [
@@ -2318,6 +2466,7 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2318
2466
  ...storage ? { storage } : {},
2319
2467
  labChrome,
2320
2468
  addTrial: false,
2469
+ ...menus ? { menus } : {},
2321
2470
  ...config?.controls ? { controls: config.controls } : {},
2322
2471
  ...config?.pages ? { pages: config.pages } : {},
2323
2472
  ...config?.path !== void 0 ? { path: config.path } : {},
@@ -2334,7 +2483,8 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2334
2483
  onOpenChange: setInfoOpen
2335
2484
  }
2336
2485
  ),
2337
- /* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues })
2486
+ /* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues }),
2487
+ /* @__PURE__ */ jsx(GlobalsMenus, { declarations, onMenus: setMenus })
2338
2488
  ]
2339
2489
  }
2340
2490
  ) }) }) }) }) }) });
@@ -2342,7 +2492,7 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
2342
2492
  function mountWorkshop({ container, ...props }) {
2343
2493
  const target = container ?? document.getElementById("root");
2344
2494
  if (!target) throw new Error("mountWorkshop: no container given and no #root element");
2345
- const root = createRoot(target);
2495
+ const root = createKitRoot(target, props.setup?.icons);
2346
2496
  const changes = createStoryChanges();
2347
2497
  let current = { ...props, changes };
2348
2498
  root.render(/* @__PURE__ */ jsx(Workshop, { ...current }));