@weasel-js/labkit 1.0.4 → 1.1.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 (85) hide show
  1. package/README.md +6 -6
  2. package/dist/_dts/{index-CFlDPeZh.d.ts → index-iAP6XbH3.d.ts} +17 -7
  3. package/dist/_dts/{useExperimentState-D7EQnnwJ.d.ts → useTrialState-DYe2vUwN.d.ts} +60 -36
  4. package/dist/canvas/index.d.ts +1 -1
  5. package/dist/chunk-3TYUJR7Z.js +488 -0
  6. package/dist/chunk-3TYUJR7Z.js.map +1 -0
  7. package/dist/{chunk-BAPZPDDA.js → chunk-C6GJKPUI.js} +365 -340
  8. package/dist/chunk-C6GJKPUI.js.map +1 -0
  9. package/dist/{chunk-DWV7SFKR.js → chunk-DJLDIRFN.js} +28 -12
  10. package/dist/chunk-DJLDIRFN.js.map +1 -0
  11. package/dist/{chunk-DEWXYFEU.js → chunk-NRKWVTVT.js} +3 -3
  12. package/dist/{chunk-DEWXYFEU.js.map → chunk-NRKWVTVT.js.map} +1 -1
  13. package/dist/dragdrop/index.d.ts +1 -1
  14. package/dist/index.d.ts +149 -149
  15. package/dist/index.js +164 -159
  16. package/dist/index.js.map +1 -1
  17. package/dist/layers/index.d.ts +2 -2
  18. package/dist/passthrough/weasel-canvas.js +1 -1
  19. package/dist/passthrough/weasel-ui.d.ts +9 -2
  20. package/dist/passthrough/weasel-ui.js +2 -2
  21. package/dist/state/index.d.ts +12 -12
  22. package/dist/state/index.js +5 -5
  23. package/dist/state/index.js.map +1 -1
  24. package/dist/styles.css +21 -13
  25. package/dist/ui/layers/index.js +3 -3
  26. package/dist/undo/index.d.ts +1 -1
  27. package/package.json +1 -1
  28. package/src/canvas/AGENTS.md +6 -2
  29. package/src/dragdrop/dragDrop.test.tsx +4 -7
  30. package/src/index.test.ts +52 -0
  31. package/src/index.ts +11 -7
  32. package/src/instrument/SineWave.smoke.test.tsx +1 -1
  33. package/src/instrument/capabilityDetector.ts +1 -1
  34. package/src/instrument/types.ts +18 -6
  35. package/src/lab/Lab.less +16 -0
  36. package/src/lab/Lab.stories.tsx +5 -5
  37. package/src/lab/Lab.test.tsx +25 -25
  38. package/src/lab/Lab.tsx +50 -50
  39. package/src/lab/LabContext.ts +12 -12
  40. package/src/lab/LabShell.tsx +1 -1
  41. package/src/lab/{WorkspaceGrid.less → Workspace.less} +2 -2
  42. package/src/lab/{WorkspaceGrid.stories.tsx → Workspace.stories.tsx} +13 -13
  43. package/src/lab/{WorkspaceGrid.test.tsx → Workspace.test.tsx} +24 -24
  44. package/src/lab/{WorkspaceGrid.tsx → Workspace.tsx} +18 -18
  45. package/src/lab/index.ts +2 -2
  46. package/src/layers/AGENTS.md +3 -3
  47. package/src/primitives/Toolbar.stories.tsx +2 -2
  48. package/src/state/SingletonExperiment.test.tsx +8 -8
  49. package/src/state/SingletonExperiment.tsx +9 -9
  50. package/src/state/context.tsx +12 -12
  51. package/src/state/document.test.ts +369 -0
  52. package/src/state/document.ts +194 -0
  53. package/src/state/helpers.test.ts +74 -46
  54. package/src/state/helpers.ts +21 -29
  55. package/src/state/index.ts +16 -8
  56. package/src/state/store.test.ts +440 -41
  57. package/src/state/store.ts +159 -102
  58. package/src/state/types.ts +29 -12
  59. package/src/state/{useExperimentState.test.tsx → useTrialState.test.tsx} +18 -18
  60. package/src/state/useTrialState.ts +29 -0
  61. package/src/styles.less +2 -2
  62. package/src/theme/base.less +1 -1
  63. package/src/{workspace → trial}/DefaultSidebar.tsx +3 -3
  64. package/src/{workspace → trial}/DefaultStatusBar.tsx +5 -4
  65. package/src/{workspace → trial}/DefaultToolbar.tsx +9 -8
  66. package/src/trial/Trial.canvas.test.tsx +80 -0
  67. package/src/{workspace/Workspace.less → trial/Trial.less} +1 -1
  68. package/src/{workspace/Workspace.stories.tsx → trial/Trial.stories.tsx} +13 -18
  69. package/src/{workspace/Workspace.test.tsx → trial/Trial.test.tsx} +26 -26
  70. package/src/{workspace/Workspace.tsx → trial/Trial.tsx} +48 -43
  71. package/src/{workspace/WorkspaceChrome.tsx → trial/TrialChrome.tsx} +42 -42
  72. package/src/{workspace → trial}/index.ts +13 -13
  73. package/src/{workspace → trial}/slotTypes.ts +16 -16
  74. package/src/{workspace/workspaceOps.test.ts → trial/trialOps.test.ts} +38 -44
  75. package/src/trial/trialOps.ts +99 -0
  76. package/src/ui/format.test.ts +33 -0
  77. package/src/ui/format.ts +11 -0
  78. package/src/ui/properties/CurveField.tsx +6 -6
  79. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +3 -3
  80. package/dist/chunk-BAPZPDDA.js.map +0 -1
  81. package/dist/chunk-DWV7SFKR.js.map +0 -1
  82. package/dist/chunk-KSTEW2AF.js +0 -367
  83. package/dist/chunk-KSTEW2AF.js.map +0 -1
  84. package/src/state/useExperimentState.ts +0 -31
  85. package/src/workspace/workspaceOps.ts +0 -102
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- export { LayerStack } from './chunk-DEWXYFEU.js';
2
- import { p, F } from './chunk-BAPZPDDA.js';
3
- import './chunk-DWV7SFKR.js';
1
+ export { LayerStack } from './chunk-NRKWVTVT.js';
2
+ import { p, F } from './chunk-C6GJKPUI.js';
3
+ import './chunk-DJLDIRFN.js';
4
4
  import { Sidebar, StatusBar, Toolbar } from './chunk-PMAU3SEE.js';
5
5
  export { FpsMeter, ScaleIndicator, Sidebar, StatusBar, Toolbar } from './chunk-PMAU3SEE.js';
6
- import { LabStoreContext, createLabStore, noneAdapter } from './chunk-KSTEW2AF.js';
7
- export { LabStoreContext, LabStoreProvider, WorkspaceIdContext, WorkspaceIdProvider, createMemoryAdapter, localStorageAdapter, noneAdapter, sessionStorageAdapter, urlHashAdapter, useExperimentState, useLabStore, useWorkspaceId } from './chunk-KSTEW2AF.js';
6
+ import { LabStoreContext, createLabStore, noneAdapter } from './chunk-3TYUJR7Z.js';
7
+ export { CURRENT_DOCUMENT_VERSION, LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, createMemoryAdapter, labDocumentKey, localStorageAdapter, noneAdapter, quarantineKey, sessionStorageAdapter, urlHashAdapter, useLabStore, useTrialId, useTrialState } from './chunk-3TYUJR7Z.js';
8
8
  import { ControlPanel } from './chunk-5R2ATYPJ.js';
9
9
  export { ControlPanel } from './chunk-5R2ATYPJ.js';
10
10
  import { CanvasStack, screenToWorld } from './chunk-574LJAV4.js';
@@ -90,7 +90,7 @@ function validateConfigSchema(fields) {
90
90
  return { valid: errors.length === 0, errors };
91
91
  }
92
92
 
93
- // ../theme/dist/chunk-STFENPNX.js
93
+ // ../theme/dist/chunk-E3FOFGMD.js
94
94
  var THEME_SOURCES = {
95
95
  "weasel": {
96
96
  "primitives": {
@@ -630,19 +630,20 @@ function resolveTheme(theme, mode) {
630
630
  return out;
631
631
  }
632
632
  var STYLE_ID = "wzl-themes";
633
- var emitted = /* @__PURE__ */ new Set();
633
+ var emitted = /* @__PURE__ */ new Map();
634
634
  var sheet = null;
635
635
  var styleEl = null;
636
636
  function canAdopt() {
637
637
  return typeof CSSStyleSheet !== "undefined" && "replaceSync" in CSSStyleSheet.prototype && Array.isArray(document.adoptedStyleSheets);
638
638
  }
639
- function appendRule(css) {
639
+ function flushRules() {
640
+ const css = [...emitted.values()].join("\n");
640
641
  if (canAdopt()) {
641
642
  if (!sheet) {
642
643
  sheet = new CSSStyleSheet();
643
644
  document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
644
645
  }
645
- sheet.insertRule(css, sheet.cssRules.length);
646
+ sheet.replaceSync(css);
646
647
  return;
647
648
  }
648
649
  if (!styleEl) {
@@ -650,17 +651,17 @@ function appendRule(css) {
650
651
  styleEl.id = STYLE_ID;
651
652
  document.head.appendChild(styleEl);
652
653
  }
653
- styleEl.textContent += `${css}
654
- `;
654
+ styleEl.textContent = css;
655
655
  }
656
656
  function applyTheme(el, theme, mode) {
657
657
  if (typeof document === "undefined") return;
658
658
  const key = `${theme.name}::${mode}`;
659
- if (!emitted.has(key)) {
660
- const resolved = resolveTheme(theme, mode);
661
- const body = Object.entries(resolved).map(([name, value]) => `${name}: ${value};`).join(" ");
662
- appendRule(`[data-wzl-theme='${theme.name}'][data-wzl-mode='${mode}'] { ${body} }`);
663
- emitted.add(key);
659
+ const resolved = resolveTheme(theme, mode);
660
+ const body = Object.entries(resolved).map(([name, value]) => `${name}: ${value};`).join(" ");
661
+ const rule = `[data-wzl-theme='${theme.name}'][data-wzl-mode='${mode}'] { ${body} }`;
662
+ if (emitted.get(key) !== rule) {
663
+ emitted.set(key, rule);
664
+ flushRules();
664
665
  }
665
666
  el.setAttribute("data-wzl-theme", theme.name);
666
667
  el.setAttribute("data-wzl-mode", mode);
@@ -949,13 +950,27 @@ function DefaultSidebar({ ctx }) {
949
950
  }
950
951
  ) });
951
952
  }
953
+
954
+ // src/ui/format.ts
955
+ var MINUS = "\u2212";
956
+ function formatNumber(n, fractionDigits) {
957
+ if (!Number.isFinite(n)) return String(n);
958
+ const s = fractionDigits == null ? String(n) : n.toFixed(fractionDigits);
959
+ return s.startsWith("-") ? MINUS + s.slice(1) : s;
960
+ }
961
+ function parseSignedNumber(s) {
962
+ return Number(s.replace(MINUS, "-"));
963
+ }
964
+ function formatZoom(zoom) {
965
+ if (!Number.isFinite(zoom)) return String(zoom);
966
+ if (zoom <= 2) return `${formatNumber(Math.round(zoom * 100))}%`;
967
+ if (zoom >= 100) return `${Math.round(zoom).toLocaleString("en-US")}x`;
968
+ return `${formatNumber(Math.round(zoom * 10) / 10)}x`;
969
+ }
952
970
  function DefaultStatusBar({ ctx }) {
953
971
  return /* @__PURE__ */ jsxs(StatusBar, { children: [
954
972
  /* @__PURE__ */ jsx(StatusBar.Section, { children: ctx.instrumentName }),
955
- /* @__PURE__ */ jsxs(StatusBar.Section, { children: [
956
- Math.round(ctx.zoom * 100),
957
- "%"
958
- ] })
973
+ /* @__PURE__ */ jsx(StatusBar.Section, { children: formatZoom(ctx.zoom) })
959
974
  ] });
960
975
  }
961
976
  function DefaultToolbar({ ctx }) {
@@ -975,10 +990,7 @@ function DefaultToolbar({ ctx }) {
975
990
  ] }),
976
991
  ctx.hasCanvas && /* @__PURE__ */ jsxs(Fragment, { children: [
977
992
  /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.zoomOut, title: "Zoom out", children: "\u2212" }),
978
- /* @__PURE__ */ jsxs("span", { className: "lk-toolbar__zoom-label", children: [
979
- Math.round(ctx.zoom * 100),
980
- "%"
981
- ] }),
993
+ /* @__PURE__ */ jsx("span", { className: "lk-toolbar__zoom-label", children: formatZoom(ctx.zoom) }),
982
994
  /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.zoomIn, title: "Zoom in", children: "+" }),
983
995
  /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.resetZoom, title: "Reset zoom", children: "1:1" }),
984
996
  /* @__PURE__ */ jsx(Toolbar.Spacer, {})
@@ -1004,24 +1016,24 @@ function DefaultToolbar({ ctx }) {
1004
1016
  }
1005
1017
  ),
1006
1018
  /* @__PURE__ */ jsx(Toolbar.Spacer, {}),
1007
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.clone, title: "Clone workspace", children: "Clone" }),
1008
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.reset, title: "Reset workspace", children: "Reset" }),
1019
+ /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.clone, title: "Clone trial", children: "Clone" }),
1020
+ /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.reset, title: "Reset trial", children: "Reset" }),
1009
1021
  /* @__PURE__ */ jsx(
1010
1022
  Toolbar.Button,
1011
1023
  {
1012
1024
  onClick: ctx.close,
1013
- disabled: ctx.isLastWorkspace,
1014
- title: ctx.isLastWorkspace ? "Cannot close the last workspace" : "Close workspace",
1025
+ disabled: ctx.isLastTrial,
1026
+ title: ctx.isLastTrial ? "Cannot close the last trial" : "Close trial",
1015
1027
  children: "Close"
1016
1028
  }
1017
1029
  )
1018
1030
  ] });
1019
1031
  }
1020
- function WorkspaceChrome({
1021
- workspaceId,
1032
+ function TrialChrome({
1033
+ trialId,
1022
1034
  record,
1023
1035
  instrument,
1024
- isLastWorkspace,
1036
+ isLastTrial,
1025
1037
  toolbar,
1026
1038
  sidebar,
1027
1039
  statusBar,
@@ -1031,16 +1043,16 @@ function WorkspaceChrome({
1031
1043
  }) {
1032
1044
  const lab = useLabContext();
1033
1045
  const storeCtx = useContext(LabStoreContext);
1034
- if (!storeCtx) throw new Error("[labkit] WorkspaceChrome requires <LabStoreProvider>");
1035
- const updateWorkspaceView = useStore(storeCtx.store, (s) => s.updateWorkspaceView);
1036
- const updateWorkspaceConfig = useStore(storeCtx.store, (s) => s.updateWorkspaceConfig);
1037
- const updateWorkspaceState = useStore(storeCtx.store, (s) => s.updateWorkspaceState);
1046
+ if (!storeCtx) throw new Error("[labkit] TrialChrome requires <LabStoreProvider>");
1047
+ const updateTrialView = useStore(storeCtx.store, (s) => s.updateTrialView);
1048
+ const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
1049
+ const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
1038
1050
  const toolbarCtx = useMemo(() => {
1039
1051
  const setZoom = (z) => {
1040
- updateWorkspaceView(workspaceId, { ...record.view, zoom: z });
1052
+ updateTrialView(trialId, { ...record.view, zoom: z });
1041
1053
  };
1042
1054
  return {
1043
- workspaceId,
1055
+ trialId,
1044
1056
  instrumentName: record.instrumentName,
1045
1057
  hasUndo: instrument.undo != null,
1046
1058
  canUndo: undoBindings?.canUndo ?? false,
@@ -1055,18 +1067,18 @@ function WorkspaceChrome({
1055
1067
  zoomOut: () => setZoom(record.view.zoom * 0.8),
1056
1068
  resetZoom: () => setZoom(1),
1057
1069
  hasCanvas: instrument.canvas != null,
1058
- savedSnapshots: lab.savedSnapshots.filter((s) => s.workspaceId === workspaceId),
1059
- saveSnapshot: (name) => lab.saveSnapshot(workspaceId, name),
1060
- loadSnapshot: (snapshotId) => lab.loadSnapshot(workspaceId, snapshotId),
1061
- clone: () => lab.cloneWorkspace(workspaceId),
1062
- reset: () => lab.resetWorkspace(workspaceId),
1063
- close: () => lab.closeWorkspace(workspaceId),
1064
- isLastWorkspace
1070
+ savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
1071
+ saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
1072
+ loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
1073
+ clone: () => lab.cloneTrial(trialId),
1074
+ reset: () => lab.resetTrial(trialId),
1075
+ close: () => lab.closeTrial(trialId),
1076
+ isLastTrial
1065
1077
  };
1066
- }, [workspaceId, record, instrument, lab, isLastWorkspace, updateWorkspaceView, undoBindings]);
1078
+ }, [trialId, record, instrument, lab, isLastTrial, updateTrialView, undoBindings]);
1067
1079
  const sidebarCtx = useMemo(
1068
1080
  () => ({
1069
- workspaceId,
1081
+ trialId,
1070
1082
  instrumentName: record.instrumentName,
1071
1083
  configFields: instrument.configSchema?.() ?? [],
1072
1084
  config: record.config,
@@ -1077,18 +1089,18 @@ function WorkspaceChrome({
1077
1089
  `[labkit] setConfig: unknown key "${key}" for instrument "${record.instrumentName}"`
1078
1090
  );
1079
1091
  }
1080
- updateWorkspaceConfig(workspaceId, key, value);
1092
+ updateTrialConfig(trialId, key, value);
1081
1093
  if (instrument.onConfigChange) {
1082
1094
  const nextConfig = { ...prevConfig, [key]: value };
1083
1095
  const nextState = instrument.onConfigChange(nextConfig, prevConfig, record.state);
1084
- updateWorkspaceState(workspaceId, nextState);
1096
+ updateTrialState(trialId, nextState);
1085
1097
  }
1086
1098
  }
1087
1099
  }),
1088
- [workspaceId, record, instrument, updateWorkspaceConfig, updateWorkspaceState]
1100
+ [trialId, record, instrument, updateTrialConfig, updateTrialState]
1089
1101
  );
1090
1102
  const statusCtx = {
1091
- workspaceId,
1103
+ trialId,
1092
1104
  instrumentName: record.instrumentName,
1093
1105
  zoom: record.view.zoom
1094
1106
  };
@@ -1107,57 +1119,57 @@ function WorkspaceChrome({
1107
1119
  return /* @__PURE__ */ jsxs(
1108
1120
  "section",
1109
1121
  {
1110
- className: "lk-workspace",
1111
- "aria-label": `Workspace ${record.instrumentName}`,
1122
+ className: "lk-trial",
1123
+ "aria-label": `Trial ${record.instrumentName}`,
1112
1124
  onKeyDown: handleKeyDown,
1113
1125
  children: [
1114
- /* @__PURE__ */ jsx("div", { className: "lk-workspace__toolbar", children: toolbar ? toolbar(toolbarCtx) : /* @__PURE__ */ jsx(DefaultToolbar, { ctx: toolbarCtx }) }),
1115
- /* @__PURE__ */ jsxs("div", { className: "lk-workspace__body", children: [
1116
- /* @__PURE__ */ jsxs("div", { className: "lk-workspace__sidebar", children: [
1126
+ /* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: toolbar ? toolbar(toolbarCtx) : /* @__PURE__ */ jsx(DefaultToolbar, { ctx: toolbarCtx }) }),
1127
+ /* @__PURE__ */ jsxs("div", { className: "lk-trial__body", children: [
1128
+ /* @__PURE__ */ jsxs("div", { className: "lk-trial__sidebar", children: [
1117
1129
  sidebar ? sidebar(sidebarCtx) : /* @__PURE__ */ jsx(DefaultSidebar, { ctx: sidebarCtx }),
1118
1130
  sidebarExtras
1119
1131
  ] }),
1120
- /* @__PURE__ */ jsx("div", { className: "lk-workspace__content", children })
1132
+ /* @__PURE__ */ jsx("div", { className: "lk-trial__content", children })
1121
1133
  ] }),
1122
- /* @__PURE__ */ jsx("div", { className: "lk-workspace__status", children: statusBar ? statusBar(statusCtx) : /* @__PURE__ */ jsx(DefaultStatusBar, { ctx: statusCtx }) })
1134
+ /* @__PURE__ */ jsx("div", { className: "lk-trial__status", children: statusBar ? statusBar(statusCtx) : /* @__PURE__ */ jsx(DefaultStatusBar, { ctx: statusCtx }) })
1123
1135
  ]
1124
1136
  }
1125
1137
  );
1126
1138
  }
1127
- function Workspace({ id }) {
1139
+ function Trial({ id }) {
1128
1140
  const lab = useLabContext();
1129
1141
  const storeCtx = useContext(LabStoreContext);
1130
- if (!storeCtx) throw new Error("[labkit] <Workspace> requires <LabStoreProvider>");
1131
- const record = useStore(storeCtx.store, (s) => s.workspaces.find((w) => w.id === id));
1142
+ if (!storeCtx) throw new Error("[labkit] <Trial> requires <LabStoreProvider>");
1143
+ const record = useStore(storeCtx.store, (s) => s.trials.find((w) => w.id === id));
1132
1144
  if (!record) {
1133
- return /* @__PURE__ */ jsxs("div", { className: "lk-workspace lk-workspace--unknown", children: [
1134
- "Workspace not found: ",
1145
+ return /* @__PURE__ */ jsxs("div", { className: "lk-trial lk-trial--unknown", children: [
1146
+ "Trial not found: ",
1135
1147
  id
1136
1148
  ] });
1137
1149
  }
1138
1150
  const instrument = lab.instruments.find((i) => i.name === record.instrumentName);
1139
1151
  if (!instrument) {
1140
- return /* @__PURE__ */ jsxs("div", { className: "lk-workspace lk-workspace--unknown", children: [
1152
+ return /* @__PURE__ */ jsxs("div", { className: "lk-trial lk-trial--unknown", children: [
1141
1153
  "Unknown instrument: ",
1142
1154
  record.instrumentName
1143
1155
  ] });
1144
1156
  }
1145
1157
  return /* @__PURE__ */ jsx(
1146
- WorkspaceRuntime,
1158
+ TrialRuntime,
1147
1159
  {
1148
1160
  record,
1149
1161
  instrument,
1150
1162
  store: storeCtx.store,
1151
- isLast: lab.workspaces.length <= 1
1163
+ isLast: lab.trials.length <= 1
1152
1164
  }
1153
1165
  );
1154
1166
  }
1155
- function WorkspaceRuntime({ record, instrument, store, isLast }) {
1167
+ function TrialRuntime({ record, instrument, store, isLast }) {
1156
1168
  const canvasContainerRef = useRef(null);
1157
- const updateWorkspaceState = useStore(store, (s) => s.updateWorkspaceState);
1158
- const updateWorkspaceConfig = useStore(store, (s) => s.updateWorkspaceConfig);
1159
- const updateWorkspaceView = useStore(store, (s) => s.updateWorkspaceView);
1160
- const updateWorkspaceUndoStack = useStore(store, (s) => s.updateWorkspaceUndoStack);
1169
+ const updateTrialState = useStore(store, (s) => s.updateTrialState);
1170
+ const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
1171
+ const updateTrialView = useStore(store, (s) => s.updateTrialView);
1172
+ const updateTrialUndoStack = useStore(store, (s) => s.updateTrialUndoStack);
1161
1173
  const busRef = useRef(null);
1162
1174
  if (busRef.current === null) busRef.current = createEventBus();
1163
1175
  const bus = busRef.current;
@@ -1168,32 +1180,32 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1168
1180
  const maxDepth = undoCap?.maxDepth ?? 50;
1169
1181
  const snapshotIfNeeded = (event) => {
1170
1182
  if (!undoCap || !undoEvents.has(event)) return;
1171
- const current = store.getState().workspaces.find((w) => w.id === record.id);
1183
+ const current = store.getState().trials.find((w) => w.id === record.id);
1172
1184
  if (!current) return;
1173
1185
  const snap = structuredClone(current.state);
1174
- updateWorkspaceUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
1186
+ updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
1175
1187
  };
1176
- const setView = (v) => updateWorkspaceView(record.id, v);
1188
+ const setView = (v) => updateTrialView(record.id, v);
1177
1189
  const renderCtx = {
1178
1190
  state: record.state,
1179
1191
  config: record.config,
1180
1192
  setState: (next) => {
1181
1193
  snapshotIfNeeded("state.change");
1182
- updateWorkspaceState(record.id, next);
1194
+ updateTrialState(record.id, next);
1183
1195
  bus.emit("state.change");
1184
1196
  },
1185
1197
  setConfig: (key, value) => {
1186
1198
  const evt = `config.change:${String(key)}`;
1187
1199
  snapshotIfNeeded("config.change");
1188
1200
  snapshotIfNeeded(evt);
1189
- updateWorkspaceConfig(record.id, key, value);
1201
+ updateTrialConfig(record.id, key, value);
1190
1202
  bus.emit("config.change");
1191
1203
  bus.emit(evt);
1192
1204
  },
1193
- workspace: {
1205
+ trial: {
1194
1206
  id: record.id,
1195
1207
  zoom: record.view.zoom,
1196
- setZoom: (z) => updateWorkspaceView(record.id, { ...record.view, zoom: z })
1208
+ setZoom: (z) => updateTrialView(record.id, { ...record.view, zoom: z })
1197
1209
  },
1198
1210
  emit: (event) => {
1199
1211
  snapshotIfNeeded(event);
@@ -1206,14 +1218,14 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1206
1218
  undo: () => {
1207
1219
  const result = undo(record.undoStack, structuredClone(record.state));
1208
1220
  if (!result) return;
1209
- updateWorkspaceState(record.id, result.snapshot);
1210
- updateWorkspaceUndoStack(record.id, result.stack);
1221
+ updateTrialState(record.id, result.snapshot);
1222
+ updateTrialUndoStack(record.id, result.stack);
1211
1223
  },
1212
1224
  redo: () => {
1213
1225
  const result = redo(record.undoStack, structuredClone(record.state));
1214
1226
  if (!result) return;
1215
- updateWorkspaceState(record.id, result.snapshot);
1216
- updateWorkspaceUndoStack(record.id, result.stack);
1227
+ updateTrialState(record.id, result.snapshot);
1228
+ updateTrialUndoStack(record.id, result.stack);
1217
1229
  }
1218
1230
  } : void 0;
1219
1231
  const canvasLayers = useMemo(() => {
@@ -1223,7 +1235,11 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1223
1235
  return ordered.map((layer) => ({
1224
1236
  id: layer.id,
1225
1237
  visible: layerVisibility[layer.id] !== false,
1226
- render: (ctx, view) => layer.draw(ctx, { state: record.state, config: record.config, zoom: view.zoom })
1238
+ render: (ctx, view) => {
1239
+ ctx.translate(view.pan.x, view.pan.y);
1240
+ ctx.scale(view.zoom, view.zoom);
1241
+ layer.draw(ctx, { state: record.state, config: record.config, zoom: view.zoom });
1242
+ }
1227
1243
  }));
1228
1244
  }, [instrument.canvas, record.state, record.config, layerVisibility, layerOrder]);
1229
1245
  const layerDescriptors = useMemo(() => {
@@ -1238,7 +1254,7 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1238
1254
  config: record.config,
1239
1255
  setState: (next) => {
1240
1256
  snapshotIfNeeded("canvas.itemAdded");
1241
- updateWorkspaceState(record.id, next);
1257
+ updateTrialState(record.id, next);
1242
1258
  },
1243
1259
  emit: (evt) => {
1244
1260
  snapshotIfNeeded(evt);
@@ -1280,10 +1296,10 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1280
1296
  "div",
1281
1297
  {
1282
1298
  ref: canvasContainerRef,
1283
- className: "lk-workspace__canvas-host",
1299
+ className: "lk-trial__canvas-host",
1284
1300
  style: { width: "100%", height: "100%", position: "relative" },
1285
1301
  children: [
1286
- /* @__PURE__ */ jsx(CanvasStack, { layers: layersWithFeedback, view: record.view, onViewChange: setView }),
1302
+ /* @__PURE__ */ jsx(CanvasStack, { layers: layersWithFeedback, view: record.view, onViewChange: setView, children: instrument.render(renderCtx) }),
1287
1303
  /* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
1288
1304
  ]
1289
1305
  }
@@ -1308,12 +1324,12 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1308
1324
  }
1309
1325
  ) : null;
1310
1326
  return /* @__PURE__ */ jsx(
1311
- WorkspaceChrome,
1327
+ TrialChrome,
1312
1328
  {
1313
- workspaceId: record.id,
1329
+ trialId: record.id,
1314
1330
  record,
1315
1331
  instrument,
1316
- isLastWorkspace: isLast,
1332
+ isLastTrial: isLast,
1317
1333
  undoBindings,
1318
1334
  sidebarExtras: /* @__PURE__ */ jsxs(Fragment, { children: [
1319
1335
  paletteNode,
@@ -1324,7 +1340,7 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1324
1340
  );
1325
1341
  }
1326
1342
 
1327
- // src/workspace/workspaceOps.ts
1343
+ // src/trial/trialOps.ts
1328
1344
  var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
1329
1345
  function findInstrument(instruments, name) {
1330
1346
  const found = instruments.find((i) => i.name === name);
@@ -1336,7 +1352,7 @@ function findInstrument(instruments, name) {
1336
1352
  function initialView(instrument) {
1337
1353
  return instrument.canvas?.initialView ? structuredClone(instrument.canvas.initialView) : { ...DEFAULT_VIEW, pan: { ...DEFAULT_VIEW.pan } };
1338
1354
  }
1339
- function addWorkspace(workspaces, instruments, instrumentName) {
1355
+ function addTrial(trials, instruments, instrumentName) {
1340
1356
  const instrument = findInstrument(instruments, instrumentName);
1341
1357
  const config = instrument.defaultConfig();
1342
1358
  const state = instrument.initialState(config);
@@ -1348,12 +1364,12 @@ function addWorkspace(workspaces, instruments, instrumentName) {
1348
1364
  view: initialView(instrument),
1349
1365
  undoStack: { past: [], future: [] }
1350
1366
  };
1351
- return [...workspaces, record];
1367
+ return [...trials, record];
1352
1368
  }
1353
- function cloneWorkspace(workspaces, id) {
1354
- const sourceIdx = workspaces.findIndex((w) => w.id === id);
1355
- const source = workspaces[sourceIdx];
1356
- if (!source) return workspaces;
1369
+ function cloneTrial(trials, id) {
1370
+ const sourceIdx = trials.findIndex((w) => w.id === id);
1371
+ const source = trials[sourceIdx];
1372
+ if (!source) return trials;
1357
1373
  const clone = {
1358
1374
  ...source,
1359
1375
  id: crypto.randomUUID(),
@@ -1362,17 +1378,17 @@ function cloneWorkspace(workspaces, id) {
1362
1378
  view: structuredClone(source.view),
1363
1379
  undoStack: { past: [], future: [] }
1364
1380
  };
1365
- return [...workspaces.slice(0, sourceIdx + 1), clone, ...workspaces.slice(sourceIdx + 1)];
1381
+ return [...trials.slice(0, sourceIdx + 1), clone, ...trials.slice(sourceIdx + 1)];
1366
1382
  }
1367
- function closeWorkspace(workspaces, id) {
1368
- if (workspaces.length <= 1) return workspaces;
1369
- const next = workspaces.filter((w) => w.id !== id);
1370
- return next.length === workspaces.length ? workspaces : next;
1383
+ function closeTrial(trials, id) {
1384
+ if (trials.length <= 1) return trials;
1385
+ const next = trials.filter((w) => w.id !== id);
1386
+ return next.length === trials.length ? trials : next;
1371
1387
  }
1372
- function resetWorkspace(workspaces, id, instruments) {
1373
- const idx = workspaces.findIndex((w) => w.id === id);
1374
- const current = workspaces[idx];
1375
- if (!current) return workspaces;
1388
+ function resetTrial(trials, id, instruments) {
1389
+ const idx = trials.findIndex((w) => w.id === id);
1390
+ const current = trials[idx];
1391
+ if (!current) return trials;
1376
1392
  const instrument = findInstrument(instruments, current.instrumentName);
1377
1393
  const config = instrument.defaultConfig();
1378
1394
  const state = instrument.initialState(config);
@@ -1382,13 +1398,13 @@ function resetWorkspace(workspaces, id, instruments) {
1382
1398
  state,
1383
1399
  view: initialView(instrument)
1384
1400
  };
1385
- return [...workspaces.slice(0, idx), reset, ...workspaces.slice(idx + 1)];
1401
+ return [...trials.slice(0, idx), reset, ...trials.slice(idx + 1)];
1386
1402
  }
1387
- function reorderWorkspaces(workspaces, ids) {
1388
- const byId = new Map(workspaces.map((w) => [w.id, w]));
1403
+ function reorderTrials(trials, ids) {
1404
+ const byId = new Map(trials.map((w) => [w.id, w]));
1389
1405
  const named = ids.map((id) => byId.get(id)).filter((w) => w !== void 0);
1390
1406
  const seen = new Set(named.map((w) => w.id));
1391
- const rest = workspaces.filter((w) => !seen.has(w.id));
1407
+ const rest = trials.filter((w) => !seen.has(w.id));
1392
1408
  return [...named, ...rest];
1393
1409
  }
1394
1410
  function query() {
@@ -1422,9 +1438,9 @@ function LabShell({ title, children, header, footer, mode = "auto" }) {
1422
1438
  ] });
1423
1439
  return outer ? shell : /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolved, children: shell });
1424
1440
  }
1425
- var ZONE_ID = asNodeId("lk-workspaces");
1441
+ var ZONE_ID = asNodeId("lk-workspace");
1426
1442
  var STRATEGIES = { grid: gridStrategy };
1427
- var KIND = "workspace";
1443
+ var KIND = "trial";
1428
1444
  function extentOf(store, id) {
1429
1445
  const p2 = store.getNode(id)?.membership?.placement;
1430
1446
  if (!p2) return null;
@@ -1433,7 +1449,7 @@ function extentOf(store, id) {
1433
1449
  if (p2.span) out.span = p2.span;
1434
1450
  return Object.keys(out).length > 0 ? out : null;
1435
1451
  }
1436
- function WorkspaceGrid({
1452
+ function Workspace({
1437
1453
  children,
1438
1454
  ids,
1439
1455
  resizable = false,
@@ -1504,9 +1520,9 @@ function WorkspaceGrid({
1504
1520
  [KIND]: ({ node }) => {
1505
1521
  const content = byId.get(node.id) ?? null;
1506
1522
  if (!reorderable) return content;
1507
- return /* @__PURE__ */ jsxs("div", { className: "lk-workspace-tile", children: [
1508
- /* @__PURE__ */ jsx(DragHandle, { nodeId: node.id, className: "lk-workspace-tile__grip", children: /* @__PURE__ */ jsx("span", { className: "lk-workspace-tile__grip-dots", "aria-hidden": "true" }) }),
1509
- /* @__PURE__ */ jsx("div", { className: "lk-workspace-tile__body", children: content })
1523
+ return /* @__PURE__ */ jsxs("div", { className: "lk-trial-tile", children: [
1524
+ /* @__PURE__ */ jsx(DragHandle, { nodeId: node.id, className: "lk-trial-tile__grip", children: /* @__PURE__ */ jsx("span", { className: "lk-trial-tile__grip-dots", "aria-hidden": "true" }) }),
1525
+ /* @__PURE__ */ jsx("div", { className: "lk-trial-tile__body", children: content })
1510
1526
  ] });
1511
1527
  }
1512
1528
  };
@@ -1516,7 +1532,7 @@ function WorkspaceGrid({
1516
1532
  {
1517
1533
  parentId: ZONE_ID,
1518
1534
  chrome,
1519
- className: "lk-workspace-grid windease-zone",
1535
+ className: "lk-workspace windease-zone",
1520
1536
  affordances: resizable,
1521
1537
  viewport,
1522
1538
  onChildOrderChange: reorderable ? commitOrder : void 0
@@ -1526,12 +1542,12 @@ function WorkspaceGrid({
1526
1542
  }
1527
1543
  function buildStore(instruments, defaultInstrument, storage, storageKey, initialMode) {
1528
1544
  const store = createLabStore({ storageKey, storage, initialMode });
1529
- if (store.getState().workspaces.length === 0) {
1530
- const seeded = addWorkspace([], instruments, defaultInstrument);
1545
+ if (store.getState().trials.length === 0) {
1546
+ const seeded = addTrial([], instruments, defaultInstrument);
1531
1547
  const record = seeded[0];
1532
1548
  if (record) {
1533
1549
  const { undoStack: _undoStack, ...rest } = record;
1534
- store.getState().addWorkspace(rest);
1550
+ store.getState().addTrial(rest);
1535
1551
  }
1536
1552
  }
1537
1553
  return store;
@@ -1575,7 +1591,7 @@ function Lab({
1575
1591
  );
1576
1592
  }
1577
1593
  const store = storeRef.current;
1578
- const workspaces = useStore(store, (s) => s.workspaces);
1594
+ const trials = useStore(store, (s) => s.trials);
1579
1595
  const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
1580
1596
  const modeValue = useStore(store, (s) => s.mode);
1581
1597
  const layout = useStore(store, (s) => s.layout);
@@ -1586,47 +1602,47 @@ function Lab({
1586
1602
  }
1587
1603
  }, [mode, store]);
1588
1604
  const contextValue = useMemo(() => {
1589
- const replaceWorkspaces = (next) => {
1590
- const currentById = new Map(store.getState().workspaces.map((w) => [w.id, w]));
1605
+ const replaceTrials = (next) => {
1606
+ const currentById = new Map(store.getState().trials.map((w) => [w.id, w]));
1591
1607
  const merged = next.map((w) => currentById.get(w.id) ?? w);
1592
- store.setState({ workspaces: merged });
1608
+ store.setState({ trials: merged });
1593
1609
  store.getState().setMode(store.getState().mode);
1594
1610
  };
1595
1611
  return {
1596
1612
  instruments,
1597
- workspaces,
1598
- addWorkspace: (instrumentName) => {
1599
- const next = addWorkspace(store.getState().workspaces, instruments, instrumentName);
1600
- replaceWorkspaces(next);
1613
+ trials,
1614
+ addTrial: (instrumentName) => {
1615
+ const next = addTrial(store.getState().trials, instruments, instrumentName);
1616
+ replaceTrials(next);
1601
1617
  },
1602
- cloneWorkspace: (id) => {
1603
- const next = cloneWorkspace(store.getState().workspaces, id);
1604
- replaceWorkspaces(next);
1618
+ cloneTrial: (id) => {
1619
+ const next = cloneTrial(store.getState().trials, id);
1620
+ replaceTrials(next);
1605
1621
  },
1606
- closeWorkspace: (id) => {
1607
- const next = closeWorkspace(store.getState().workspaces, id);
1608
- replaceWorkspaces(next);
1622
+ closeTrial: (id) => {
1623
+ const next = closeTrial(store.getState().trials, id);
1624
+ replaceTrials(next);
1609
1625
  },
1610
- reorderWorkspaces: (ids) => {
1611
- replaceWorkspaces(reorderWorkspaces(store.getState().workspaces, ids));
1626
+ reorderTrials: (ids) => {
1627
+ replaceTrials(reorderTrials(store.getState().trials, ids));
1612
1628
  },
1613
- resetWorkspace: (id) => {
1614
- const next = resetWorkspace(store.getState().workspaces, id, instruments);
1629
+ resetTrial: (id) => {
1630
+ const next = resetTrial(store.getState().trials, id, instruments);
1615
1631
  const record = next.find((w) => w.id === id);
1616
1632
  if (!record) return;
1617
1633
  store.setState((s) => ({
1618
- workspaces: s.workspaces.map(
1634
+ trials: s.trials.map(
1619
1635
  (w) => w.id === id ? { ...w, config: record.config, state: record.state, view: record.view } : w
1620
1636
  )
1621
1637
  }));
1622
- store.getState().updateWorkspaceView(id, record.view);
1638
+ store.getState().updateTrialView(id, record.view);
1623
1639
  },
1624
1640
  savedSnapshots,
1625
- saveSnapshot: (workspaceId, name) => {
1626
- store.getState().saveSnapshot(workspaceId, name ?? `Save ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
1641
+ saveSnapshot: (trialId, name) => {
1642
+ store.getState().saveSnapshot(trialId, name ?? `Save ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
1627
1643
  },
1628
- loadSnapshot: (workspaceId, snapshotId) => {
1629
- store.getState().loadSnapshot(snapshotId, workspaceId);
1644
+ loadSnapshot: (trialId, snapshotId) => {
1645
+ store.getState().loadSnapshot(snapshotId, trialId);
1630
1646
  },
1631
1647
  deleteSnapshot: (snapshotId) => {
1632
1648
  store.getState().deleteSnapshot(snapshotId);
@@ -1636,7 +1652,7 @@ function Lab({
1636
1652
  store.getState().setMode(m);
1637
1653
  }
1638
1654
  };
1639
- }, [instruments, workspaces, savedSnapshots, modeValue, store]);
1655
+ }, [instruments, trials, savedSnapshots, modeValue, store]);
1640
1656
  const backdropStyle = resolvedMode === "dark" && nebula && nebula.length > 0 ? { ["--wzl-backdrop"]: buildNebula(nebula) } : void 0;
1641
1657
  return /* @__PURE__ */ jsx(LabStoreContext.Provider, { value: { store }, children: /* @__PURE__ */ jsx(LabContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
1642
1658
  ThemeProvider,
@@ -1646,15 +1662,15 @@ function Lab({
1646
1662
  className: "lk-lab",
1647
1663
  style: backdropStyle,
1648
1664
  children: /* @__PURE__ */ jsx(LabShell, { title: title ?? "Labkit", mode: modeValue, header: children, children: /* @__PURE__ */ jsx(
1649
- WorkspaceGrid,
1665
+ Workspace,
1650
1666
  {
1651
- ids: workspaces.map((w) => w.id),
1667
+ ids: trials.map((w) => w.id),
1652
1668
  resizable: true,
1653
1669
  reorderable: true,
1654
- onReorder: (ids) => contextValue.reorderWorkspaces(ids),
1670
+ onReorder: (ids) => contextValue.reorderTrials(ids),
1655
1671
  layout,
1656
1672
  onLayoutChange: (next) => store.getState().setLayout(next),
1657
- children: workspaces.map((w) => /* @__PURE__ */ jsx(Workspace, { id: w.id }, w.id))
1673
+ children: trials.map((w) => /* @__PURE__ */ jsx(Trial, { id: w.id }, w.id))
1658
1674
  }
1659
1675
  ) })
1660
1676
  }
@@ -2024,17 +2040,6 @@ function PropertyGroup({
2024
2040
  /* @__PURE__ */ jsx("div", { className: "lk-property-group__body", children })
2025
2041
  ] });
2026
2042
  }
2027
-
2028
- // src/ui/format.ts
2029
- var MINUS = "\u2212";
2030
- function formatNumber(n, fractionDigits) {
2031
- if (!Number.isFinite(n)) return String(n);
2032
- const s = fractionDigits == null ? String(n) : n.toFixed(fractionDigits);
2033
- return s.startsWith("-") ? MINUS + s.slice(1) : s;
2034
- }
2035
- function parseSignedNumber(s) {
2036
- return Number(s.replace(MINUS, "-"));
2037
- }
2038
2043
  function PropertyPanel({ title, children, className }) {
2039
2044
  const cls = className ? `lk-property-panel ${className}` : "lk-property-panel";
2040
2045
  return /* @__PURE__ */ jsxs("div", { className: cls, children: [
@@ -2278,6 +2283,6 @@ function ToggleRow({
2278
2283
  }) }) });
2279
2284
  }
2280
2285
 
2281
- export { CheckboxRow, ColorRow, CurveField, DefaultSidebar, DefaultStatusBar, DefaultToolbar, EffectCard, EffectCardList, Lab, LabContext, LabShell, NumberRow, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SelectRow, SliderRow, Subpanel, TextRow, ToggleRow, Workspace, WorkspaceChrome, WorkspaceGrid, addWorkspace, cloneWorkspace, closeWorkspace, defineInstrument, detectCapabilities, interstellarTheme, reorderWorkspaces, resetWorkspace, useLabContext, validateConfigSchema };
2286
+ export { CheckboxRow, ColorRow, CurveField, DefaultSidebar, DefaultStatusBar, DefaultToolbar, EffectCard, EffectCardList, Lab, LabContext, LabShell, NumberRow, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SelectRow, SliderRow, Subpanel, TextRow, ToggleRow, Trial, TrialChrome, Workspace, addTrial, cloneTrial, closeTrial, defineInstrument, detectCapabilities, interstellarTheme, reorderTrials, resetTrial, useLabContext, validateConfigSchema };
2282
2287
  //# sourceMappingURL=index.js.map
2283
2288
  //# sourceMappingURL=index.js.map