@weasel-js/forge 1.8.0 → 1.8.1

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 (46) hide show
  1. package/README.md +78 -0
  2. package/dist/{FrameController-RdGggdGA.d.ts → FrameController-BH8xJHZ8.d.ts} +36 -1
  3. package/dist/{chunk-NYBMOSSR.js → chunk-5N4YP5HS.js} +166 -10
  4. package/dist/chunk-5N4YP5HS.js.map +1 -0
  5. package/dist/chunk-CJX3W5HY.js +151 -0
  6. package/dist/chunk-CJX3W5HY.js.map +1 -0
  7. package/dist/{chunk-SEK6EKHO.js → chunk-GHTVXHNJ.js} +72 -10
  8. package/dist/chunk-GHTVXHNJ.js.map +1 -0
  9. package/dist/{chunk-KIFCYY4F.js → chunk-JQD3MVUQ.js} +4 -4
  10. package/dist/{chunk-KIFCYY4F.js.map → chunk-JQD3MVUQ.js.map} +1 -1
  11. package/dist/chunk-KJGXJD3U.js +15 -0
  12. package/dist/chunk-KJGXJD3U.js.map +1 -0
  13. package/dist/chunk-L5XM5VKB.js +47 -0
  14. package/dist/chunk-L5XM5VKB.js.map +1 -0
  15. package/dist/{chunk-YKN3IAEX.js → chunk-MGQWCPYE.js} +4 -24
  16. package/dist/chunk-MGQWCPYE.js.map +1 -0
  17. package/dist/{chunk-JNJGIUYU.js → chunk-NI5FYQHW.js} +8 -5
  18. package/dist/chunk-NI5FYQHW.js.map +1 -0
  19. package/dist/{config-BJIgUhiI.d.ts → config-DoLWwEb2.d.ts} +5 -1
  20. package/dist/csf/preview-api.js +2 -1
  21. package/dist/csf/preview-api.js.map +1 -1
  22. package/dist/frame/index.css +1 -0
  23. package/dist/frame/index.css.map +1 -1
  24. package/dist/frame/index.d.ts +2 -2
  25. package/dist/frame/index.js +5 -4
  26. package/dist/frame/index.js.map +1 -1
  27. package/dist/index.d.ts +79 -4
  28. package/dist/index.js +4 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/shell/index.css +14 -0
  31. package/dist/shell/index.css.map +1 -1
  32. package/dist/shell/index.d.ts +2 -2
  33. package/dist/shell/index.js +225 -56
  34. package/dist/shell/index.js.map +1 -1
  35. package/dist/test/index.d.ts +1 -1
  36. package/dist/test/index.js +6 -5
  37. package/dist/test/index.js.map +1 -1
  38. package/dist/vite/index.js +160 -16
  39. package/dist/vite/index.js.map +1 -1
  40. package/package.json +5 -5
  41. package/dist/chunk-JNJGIUYU.js.map +0 -1
  42. package/dist/chunk-NYBMOSSR.js.map +0 -1
  43. package/dist/chunk-SEK6EKHO.js.map +0 -1
  44. package/dist/chunk-Y3WZ2EWR.js +0 -15
  45. package/dist/chunk-Y3WZ2EWR.js.map +0 -1
  46. package/dist/chunk-YKN3IAEX.js.map +0 -1
@@ -1,18 +1,20 @@
1
- import { isIndexId, indexId, libraryOf, indexEntries, schemaFromDescription, LibraryBadge, openChannel, IndexPage } from '../chunk-KIFCYY4F.js';
1
+ import { parseRoute, paramsToKnobs, knobPaths, sameKnob, knobsToParams, formatRoute, PLAYHEAD_PARAM, reservedParams } from '../chunk-CJX3W5HY.js';
2
+ import { isIndexId, indexId, libraryOf, indexEntries, schemaFromDescription, LibraryBadge, openChannel, IndexPage } from '../chunk-JQD3MVUQ.js';
2
3
  import { isGallery } from '../chunk-WEQSLPKT.js';
3
- import { useTrialFrame, createTrialFrames, StoryGlobalsContext, TrialFramesContext, StoryTrial, useCssOverrides, TrialHost } from '../chunk-SEK6EKHO.js';
4
- export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-SEK6EKHO.js';
5
- import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, labOptions, isGlobalsPath } from '../chunk-JNJGIUYU.js';
6
- import { loadStories, indexRenderOf } from '../chunk-NYBMOSSR.js';
7
- import '../chunk-Y3WZ2EWR.js';
4
+ import { useTrialFrame, createTrialFrames, createTrialClocks, StoryGlobalsContext, TrialFramesContext, TrialClocksContext, StoryTrial, useCssOverrides, useTrialClock, TrialHost } from '../chunk-GHTVXHNJ.js';
5
+ export { StoryGlobalsContext, StoryTrial, TrialFramesContext, TrialHost, useTrialFrame } from '../chunk-GHTVXHNJ.js';
6
+ import { labGlobals, withGlobals, followApp, storyConfig, GLOBALS_KEY, effectiveGlobals, PLAYHEAD_KEY, labOptions, isGlobalsPath } from '../chunk-NI5FYQHW.js';
7
+ import { loadStories, indexRenderOf } from '../chunk-5N4YP5HS.js';
8
+ import '../chunk-L5XM5VKB.js';
8
9
  import '../chunk-HRGPFQBW.js';
9
- import { stableStringify, PORT_HANDOFF, FRAME_HELLO, PROTOCOL_VERSION } from '../chunk-YKN3IAEX.js';
10
- import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, inferTokenType, ToggleBar as ToggleBar$1, Input, TokenPanel, refitScale } from '@weasel-js/labkit';
10
+ import '../chunk-MGQWCPYE.js';
11
+ import { stableStringify, PORT_HANDOFF, FRAME_HELLO, PROTOCOL_VERSION } from '../chunk-KJGXJD3U.js';
12
+ import { useTrialId, Button, Lab, useLabContext, TrialIdContext, usePersistedState, Select, useTrialState, inferTokenType, ToggleBar as ToggleBar$1, Input, TokenPanel, refitScale } from '@weasel-js/labkit';
11
13
  import num from '@weasel-js/theme/numeric.module.css';
12
- import { createContext, useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect, useId, useContext } from 'react';
14
+ import { createContext, useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect, useId, useContext, useSyncExternalStore } from 'react';
13
15
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
14
16
  import { createRoot } from 'react-dom/client';
15
- import { InfoIcon, ToggleBar, Checkbox, Dialog, DisclosureMark, Button as Button$1, TuneIcon, Callout } from '@weasel-js/ui';
17
+ import { InfoIcon, ToggleBar, Checkbox, Dialog, DisclosureMark, Button as Button$1, TuneIcon, Callout, Slider, Transport } from '@weasel-js/ui';
16
18
  import { TOKEN_MANIFEST, THEME_SOURCES, isByAxis } from '@weasel-js/theme';
17
19
  import { scale, httpThemeApi } from '@weasel-js/theme/engine';
18
20
  import { f, valueAtPath, isConfigBranch } from '@weasel-js/labkit/config';
@@ -625,6 +627,120 @@ function createFramePool(frameUrl, size = 2) {
625
627
  };
626
628
  }
627
629
  var FramePoolContext = createContext(null);
630
+
631
+ // src/shell/routedTrial.ts
632
+ function routedTrial(trials, focusedTrialId, route) {
633
+ return trials.find((t) => t.id === focusedTrialId && t.instrumentName === route) ?? trials.find((t) => t.instrumentName === route);
634
+ }
635
+ function readRoute() {
636
+ return parseRoute(location.hash).story;
637
+ }
638
+ function readRouteParams() {
639
+ return { ...parseRoute(location.hash).params };
640
+ }
641
+ function replaceRouteParams(update) {
642
+ const { story, params } = parseRoute(location.hash);
643
+ if (story === null) return;
644
+ const next = formatRoute({ story, params: update({ ...params }) });
645
+ if (next !== location.hash) history.replaceState(history.state, "", next);
646
+ }
647
+ function readRouteEntry() {
648
+ const state = history.state;
649
+ const entry = state?.forgeRoute;
650
+ return typeof entry?.step === "number" ? entry : null;
651
+ }
652
+ function crossesInPlace(from, to) {
653
+ if (!from || !to || from.step === to.step) return false;
654
+ return to.step < from.step ? from.inPlace : to.inPlace;
655
+ }
656
+ function stamp(entry, hash) {
657
+ history.replaceState({ ...history.state ?? {}, forgeRoute: entry }, "", hash);
658
+ }
659
+ function setRoute(id, options = {}) {
660
+ if (readRoute() === id) {
661
+ history.replaceState(history.state, "", formatRoute({ story: id, params: readRouteParams() }));
662
+ return;
663
+ }
664
+ const hash = formatRoute({ story: id, params: {} });
665
+ const from = readRouteEntry() ?? { step: 0, inPlace: false };
666
+ stamp(from, location.hash);
667
+ location.hash = hash;
668
+ stamp({ step: from.step + 1, inPlace: options.inPlace ?? false }, hash);
669
+ }
670
+ function useRoute() {
671
+ const [route, setCurrent] = useState(readRoute);
672
+ useEffect(() => {
673
+ const sync = () => setCurrent(readRoute());
674
+ window.addEventListener("hashchange", sync);
675
+ sync();
676
+ return () => window.removeEventListener("hashchange", sync);
677
+ }, []);
678
+ return [route, setRoute];
679
+ }
680
+
681
+ // src/timeline/playheadUrl.ts
682
+ function formatSeconds(ms) {
683
+ const rounded = Math.round(ms) / 1e3;
684
+ return String(rounded === 0 ? 0 : rounded);
685
+ }
686
+ function parseSeconds(text) {
687
+ if (text == null || !/^\s*-?(\d+\.?\d*|\.\d+)\s*$/.test(text)) return null;
688
+ return Math.round(Number(text) * 1e3);
689
+ }
690
+ function readPlayheadParam() {
691
+ return parseSeconds(readRouteParams()[PLAYHEAD_PARAM]);
692
+ }
693
+ function writePlayheadParam(ms) {
694
+ replaceRouteParams(
695
+ ({ [PLAYHEAD_PARAM]: _held, ...rest }) => ms === null ? rest : { ...rest, [PLAYHEAD_PARAM]: formatSeconds(ms) }
696
+ );
697
+ }
698
+ function PlayheadSync({ trialId, route, applied }) {
699
+ const clock = useTrialClock(trialId);
700
+ useEffect(() => {
701
+ if (!clock) return;
702
+ const fromUrl = () => {
703
+ const t = readPlayheadParam();
704
+ if (t === null || t === clock.get().time) return;
705
+ clock.request(t);
706
+ };
707
+ let held;
708
+ const toUrl = () => {
709
+ if (clock.pending() !== null) return;
710
+ const { playing, time, span } = clock.get();
711
+ const next = playing || time === span.start ? null : time;
712
+ if (next === held) return;
713
+ held = next;
714
+ writePlayheadParam(next);
715
+ };
716
+ if (applied.current !== route) {
717
+ applied.current = route;
718
+ fromUrl();
719
+ }
720
+ toUrl();
721
+ const off = clock.subscribe(toUrl);
722
+ const onHashChange = () => {
723
+ if (readRoute() !== route) return;
724
+ held = void 0;
725
+ fromUrl();
726
+ toUrl();
727
+ };
728
+ window.addEventListener("hashchange", onHashChange);
729
+ return () => {
730
+ off();
731
+ window.removeEventListener("hashchange", onHashChange);
732
+ };
733
+ }, [clock, route, applied]);
734
+ return null;
735
+ }
736
+ function RoutePlayhead() {
737
+ const lab = useLabContext();
738
+ const [route] = useRoute();
739
+ const applied = useRef(null);
740
+ const trial = route === null ? void 0 : routedTrial(lab.trials, lab.focusedTrialId, route);
741
+ if (!trial || route === null) return null;
742
+ return /* @__PURE__ */ jsx(PlayheadSync, { trialId: trial.id, route, applied }, `${trial.id} ${route}`);
743
+ }
628
744
  var RECORD = "fg-globals";
629
745
  function useLabGlobals(declarations) {
630
746
  const [stored, setStored] = usePersistedState(RECORD, null, { scope: "lab" });
@@ -706,6 +822,46 @@ function LabGlobals({
706
822
  useEffect(() => onChange(resolved), [resolved, onChange]);
707
823
  return null;
708
824
  }
825
+ function KnobSync({ schema, config }) {
826
+ const { setConfig } = useTrialState();
827
+ const reading = useRef(true);
828
+ const [typed, setTyped] = useState(0);
829
+ useEffect(() => {
830
+ const onHashChange = () => {
831
+ reading.current = true;
832
+ setTyped((n) => n + 1);
833
+ };
834
+ window.addEventListener("hashchange", onHashChange);
835
+ return () => window.removeEventListener("hashchange", onHashChange);
836
+ }, []);
837
+ useEffect(() => {
838
+ if (reading.current) {
839
+ reading.current = false;
840
+ const knobs2 = paramsToKnobs(schema, readRouteParams());
841
+ let wrote = false;
842
+ for (const { path, leaf } of knobPaths(schema)) {
843
+ const target = path in knobs2 ? knobs2[path] : leaf.default;
844
+ const current = valueAtPath(config, path);
845
+ if (sameKnob(current === void 0 ? leaf.default : current, target)) continue;
846
+ setConfig(path, target);
847
+ wrote = true;
848
+ }
849
+ if (wrote) return;
850
+ }
851
+ const knobs = knobsToParams(schema, config);
852
+ replaceRouteParams((params) => ({ ...knobs, ...reservedParams(params) }));
853
+ }, [schema, config, setConfig, typed]);
854
+ return null;
855
+ }
856
+ function RouteKnobs({ instruments, isReady }) {
857
+ const lab = useLabContext();
858
+ const [route] = useRoute();
859
+ if (route === null || !isReady(route)) return null;
860
+ const trial = routedTrial(lab.trials, lab.focusedTrialId, route);
861
+ const schema = instruments.find((instrument) => instrument.name === route)?.config;
862
+ if (!trial || !schema) return null;
863
+ return /* @__PURE__ */ jsx(TrialIdContext.Provider, { value: trial.id, children: /* @__PURE__ */ jsx(KnobSync, { schema, config: trial.config }, `${trial.id} ${route}`) });
864
+ }
709
865
 
710
866
  // src/shell/revealTrial.ts
711
867
  var FLASH_MS = 600;
@@ -727,44 +883,6 @@ function revealTrial(trialId, root = document) {
727
883
  );
728
884
  return true;
729
885
  }
730
- function readRoute() {
731
- const { hash } = location;
732
- if (!hash.startsWith("#/")) return null;
733
- return decodeURIComponent(hash.slice(2)) || null;
734
- }
735
- function readRouteEntry() {
736
- const state = history.state;
737
- const entry = state?.forgeRoute;
738
- return typeof entry?.step === "number" ? entry : null;
739
- }
740
- function crossesInPlace(from, to) {
741
- if (!from || !to || from.step === to.step) return false;
742
- return to.step < from.step ? from.inPlace : to.inPlace;
743
- }
744
- function stamp(entry, hash) {
745
- history.replaceState({ ...history.state ?? {}, forgeRoute: entry }, "", hash);
746
- }
747
- function setRoute(id, options = {}) {
748
- const hash = `#/${encodeURIComponent(id)}`;
749
- if (readRoute() === id) {
750
- history.replaceState(history.state, "", hash);
751
- return;
752
- }
753
- const from = readRouteEntry() ?? { step: 0, inPlace: false };
754
- stamp(from, location.hash);
755
- location.hash = hash;
756
- stamp({ step: from.step + 1, inPlace: options.inPlace ?? false }, hash);
757
- }
758
- function useRoute() {
759
- const [route, setCurrent] = useState(readRoute);
760
- useEffect(() => {
761
- const sync = () => setCurrent(readRoute());
762
- window.addEventListener("hashchange", sync);
763
- sync();
764
- return () => window.removeEventListener("hashchange", sync);
765
- }, []);
766
- return [route, setRoute];
767
- }
768
886
 
769
887
  // src/shell/tree/buildTree.ts
770
888
  function sortFolder(nodes) {
@@ -1212,6 +1330,44 @@ function createAnswerBook() {
1212
1330
  }
1213
1331
  };
1214
1332
  }
1333
+ function TransportBar({ clock }) {
1334
+ const { time, playing, loop, rate, span } = useSyncExternalStore(clock.subscribe, clock.get);
1335
+ return /* @__PURE__ */ jsxs("div", { className: "fg-transport", children: [
1336
+ /* @__PURE__ */ jsx(
1337
+ Slider,
1338
+ {
1339
+ className: "fg-transport__scrub",
1340
+ ariaLabel: "Scrub",
1341
+ density: "slim",
1342
+ readoutPlacement: "none",
1343
+ min: span.start,
1344
+ max: span.end > span.start ? span.end : span.start + 1,
1345
+ thumbs: [{ value: time, valueText: `${formatSeconds(time)} seconds` }],
1346
+ onInput: ([thumb]) => {
1347
+ if (thumb) clock.seek(thumb.value);
1348
+ }
1349
+ }
1350
+ ),
1351
+ /* @__PURE__ */ jsx(
1352
+ Transport,
1353
+ {
1354
+ paused: !playing,
1355
+ loop,
1356
+ rate,
1357
+ playhead: time,
1358
+ duration: span.end,
1359
+ onPlay: clock.play,
1360
+ onPause: clock.pause,
1361
+ onLoopChange: (next) => clock.setLoop(next !== false && next !== 0),
1362
+ onRateChange: clock.setRate
1363
+ }
1364
+ )
1365
+ ] });
1366
+ }
1367
+ function TrialTransport({ trialId }) {
1368
+ const clock = useTrialClock(trialId);
1369
+ return clock ? /* @__PURE__ */ jsx(TransportBar, { clock }) : null;
1370
+ }
1215
1371
  function IndexTrial({ bundle, setup, config }) {
1216
1372
  const lab = useLabContext();
1217
1373
  const trialId = useContext(TrialIdContext);
@@ -1243,10 +1399,14 @@ function IndexTrial({ bundle, setup, config }) {
1243
1399
  };
1244
1400
  return /* @__PURE__ */ jsx(TrialHost, { layout: "fullscreen", setup, config, children: render });
1245
1401
  }
1402
+ function withPlayhead(schema) {
1403
+ return f.schema({ ...schema.nodes, [PLAYHEAD_KEY]: f.custom("forge-playhead", null).hidden() });
1404
+ }
1246
1405
  function documentInstrument(options) {
1247
1406
  const { entry, loaded, setup, globals = {} } = options;
1248
1407
  const schema = loaded.kind === "story" ? loaded.story.config : f.schema({});
1249
- const config = withGlobals(schema, globals);
1408
+ const timed = loaded.kind === "story" && loaded.story.timeline !== null;
1409
+ const config = withGlobals(timed ? withPlayhead(schema) : schema, globals);
1250
1410
  const initialState = loaded.kind === "story" ? loaded.story.initialState : null;
1251
1411
  return {
1252
1412
  name: entry.id,
@@ -1254,8 +1414,9 @@ function documentInstrument(options) {
1254
1414
  config,
1255
1415
  defaultConfig: () => config.defaults(),
1256
1416
  initialState: (c) => initialState?.(c) ?? null,
1257
- render: (ctx) => loaded.kind === "story" ? /* @__PURE__ */ jsx(StoryTrial, { story: loaded.story, setup, ctx }) : /* @__PURE__ */ jsx(IndexTrial, { bundle: loaded.bundle, setup, config: ctx.config }),
1258
- ...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {}
1417
+ render: (ctx) => loaded.kind === "story" ? /* @__PURE__ */ jsx(StoryTrial, { story: loaded.story, setup, ctx }, loaded.story.id) : /* @__PURE__ */ jsx(IndexTrial, { bundle: loaded.bundle, setup, config: ctx.config }),
1418
+ ...loaded.kind === "story" && loaded.story.viewport ? { stage: { size: { width: loaded.story.viewport.width, height: loaded.story.viewport.height } } } : {},
1419
+ ...timed ? { chrome: [{ id: "fg-transport", region: "status", render: (c) => /* @__PURE__ */ jsx(TrialTransport, { trialId: c.trialId }) }] } : {}
1259
1420
  };
1260
1421
  }
1261
1422
  function StoryLoad({ entry, load, fault }) {
@@ -1747,6 +1908,7 @@ function useStoryRegistry(index, options) {
1747
1908
  return { instruments: cache.list, isReady, onReady, reload };
1748
1909
  }
1749
1910
  var NO_DECLARATIONS = {};
1911
+ var DEFAULT_TITLE = "weaselforge";
1750
1912
  function initialStory(entries, fallback) {
1751
1913
  const route = readRoute();
1752
1914
  return route !== null && entries.some((entry) => entry.id === route) ? route : fallback;
@@ -1785,7 +1947,12 @@ function useInfoShortcut(open) {
1785
1947
  }
1786
1948
  function Workshop({ index, frameUrl, importers, setup, changes, config, stories = [], storageKey, storage, dependencies }) {
1787
1949
  const declarations = config?.globals ?? NO_DECLARATIONS;
1950
+ const title = config?.title ?? DEFAULT_TITLE;
1951
+ useEffect(() => {
1952
+ document.title = title;
1953
+ }, [title]);
1788
1954
  const [frames] = useState(createTrialFrames);
1955
+ const [clocks] = useState(createTrialClocks);
1789
1956
  const [pool, setPool] = useState(null);
1790
1957
  const framed = importers === void 0 || index.some((entry) => entry.isolate !== void 0);
1791
1958
  useEffect(() => {
@@ -1823,12 +1990,12 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
1823
1990
  region: "palette",
1824
1991
  item: { icon: InfoIcon, label: "Info", shortcut: "\u2318I", onActivate: () => setInfoOpen(true) }
1825
1992
  },
1826
- CSS_VARS_SECTION,
1993
+ ...config?.cssVars === false ? [] : [CSS_VARS_SECTION],
1827
1994
  A11Y_SECTION,
1828
1995
  ...Object.keys(declarations).length > 0 ? [{ id: "fg-globals", region: "header", render: () => /* @__PURE__ */ jsx(GlobalsToolbar, { declarations }) }] : [],
1829
1996
  ...config?.labChrome ?? []
1830
1997
  ],
1831
- [index, declarations, config?.labChrome]
1998
+ [index, declarations, config?.cssVars, config?.labChrome]
1832
1999
  );
1833
2000
  const first = index[0];
1834
2001
  if (!first) {
@@ -1837,10 +2004,10 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
1837
2004
  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
1838
2005
  ] });
1839
2006
  }
1840
- return /* @__PURE__ */ jsx(StoryGlobalsContext.Provider, { value: labValues, children: /* @__PURE__ */ jsx(DependenciesContext.Provider, { value: dependencies ?? null, children: /* @__PURE__ */ jsx(FramePoolContext.Provider, { value: pool, children: /* @__PURE__ */ jsx(TrialFramesContext.Provider, { value: frames, children: /* @__PURE__ */ jsxs(
2007
+ return /* @__PURE__ */ jsx(StoryGlobalsContext.Provider, { value: labValues, children: /* @__PURE__ */ jsx(DependenciesContext.Provider, { value: dependencies ?? null, children: /* @__PURE__ */ jsx(FramePoolContext.Provider, { value: pool, children: /* @__PURE__ */ jsx(TrialFramesContext.Provider, { value: frames, children: /* @__PURE__ */ jsx(TrialClocksContext.Provider, { value: clocks, children: /* @__PURE__ */ jsxs(
1841
2008
  Lab,
1842
2009
  {
1843
- title: "weaselforge",
2010
+ title,
1844
2011
  density: "roomy",
1845
2012
  ...labTheme ? { theme: labTheme } : {},
1846
2013
  instruments: registry.instruments,
@@ -1854,6 +2021,8 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
1854
2021
  ...config?.path !== void 0 ? { path: config.path } : {},
1855
2022
  children: [
1856
2023
  /* @__PURE__ */ jsx(RouteOpener, { index: entries }),
2024
+ /* @__PURE__ */ jsx(RouteKnobs, { instruments: registry.instruments, isReady: registry.isReady }),
2025
+ /* @__PURE__ */ jsx(RoutePlayhead, {}),
1857
2026
  /* @__PURE__ */ jsx(
1858
2027
  StoryInfoDialog,
1859
2028
  {
@@ -1866,7 +2035,7 @@ function Workshop({ index, frameUrl, importers, setup, changes, config, stories
1866
2035
  /* @__PURE__ */ jsx(LabGlobals, { declarations, onChange: reportLabValues })
1867
2036
  ]
1868
2037
  }
1869
- ) }) }) }) });
2038
+ ) }) }) }) }) });
1870
2039
  }
1871
2040
  function mountWorkshop({ container, ...props }) {
1872
2041
  const target = container ?? document.getElementById("root");