@weasel-js/labkit 1.1.0 → 1.3.0-pre.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 (226) hide show
  1. package/README.md +27 -0
  2. package/dist/_dts/{DrawCommand-BkZztJsW.d.ts → DrawCommand-DBB45NfN.d.ts} +244 -58
  3. package/dist/_dts/PrefsForm-BYa6cWnO.d.ts +201 -0
  4. package/dist/_dts/index-DgcNdEdh.d.ts +314 -0
  5. package/dist/_dts/types-1Sdxy_Pv.d.ts +90 -0
  6. package/dist/_dts/types-AHlQxBNN.d.ts +162 -0
  7. package/dist/_dts/types-DJ79Tg5J.d.ts +56 -0
  8. package/dist/_dts/{useTrialState-DYe2vUwN.d.ts → useTrialState-CsGMjhu9.d.ts} +6 -92
  9. package/dist/canvas/index.d.ts +8 -3
  10. package/dist/canvas/index.js +2 -1
  11. package/dist/chrome/index.d.ts +86 -0
  12. package/dist/chrome/index.js +8 -0
  13. package/dist/chrome/index.js.map +1 -0
  14. package/dist/{chunk-3TYUJR7Z.js → chunk-4TMMVIDM.js} +9 -3
  15. package/dist/chunk-4TMMVIDM.js.map +1 -0
  16. package/dist/chunk-HFDVGF4X.js +170 -0
  17. package/dist/chunk-HFDVGF4X.js.map +1 -0
  18. package/dist/chunk-LN6JDUGB.js +106 -0
  19. package/dist/chunk-LN6JDUGB.js.map +1 -0
  20. package/dist/{chunk-574LJAV4.js → chunk-NS54R52R.js} +31 -21
  21. package/dist/chunk-NS54R52R.js.map +1 -0
  22. package/dist/{chunk-NRKWVTVT.js → chunk-O2BULFHX.js} +5 -14
  23. package/dist/chunk-O2BULFHX.js.map +1 -0
  24. package/dist/chunk-O7NVSCLN.js +266 -0
  25. package/dist/chunk-O7NVSCLN.js.map +1 -0
  26. package/dist/chunk-PO6L3NSH.js +366 -0
  27. package/dist/chunk-PO6L3NSH.js.map +1 -0
  28. package/dist/chunk-PQJ5B2U2.js +27 -0
  29. package/dist/chunk-PQJ5B2U2.js.map +1 -0
  30. package/dist/chunk-RVNN4CHQ.js +212 -0
  31. package/dist/chunk-RVNN4CHQ.js.map +1 -0
  32. package/dist/chunk-TLGRYALP.js +6151 -0
  33. package/dist/chunk-TLGRYALP.js.map +1 -0
  34. package/dist/{chunk-SFL7NFKN.js → chunk-V7PRSIRQ.js} +15 -7
  35. package/dist/chunk-V7PRSIRQ.js.map +1 -0
  36. package/dist/{chunk-DJLDIRFN.js → chunk-Z6HJ5FEM.js} +9626 -6050
  37. package/dist/chunk-Z6HJ5FEM.js.map +1 -0
  38. package/dist/chunk-ZZAYVX4X.js +505 -0
  39. package/dist/chunk-ZZAYVX4X.js.map +1 -0
  40. package/dist/controls/index.d.ts +19 -5
  41. package/dist/controls/index.js +3 -1
  42. package/dist/dragdrop/index.d.ts +5 -2
  43. package/dist/index.d.ts +307 -107
  44. package/dist/index.js +797 -540
  45. package/dist/index.js.map +1 -1
  46. package/dist/job/index.d.ts +13 -0
  47. package/dist/job/index.js +3 -0
  48. package/dist/job/index.js.map +1 -0
  49. package/dist/layers/index.d.ts +6 -3
  50. package/dist/layers/index.js +2 -1
  51. package/dist/passthrough/weasel-canvas.d.ts +69 -13
  52. package/dist/passthrough/weasel-canvas.js +1 -1
  53. package/dist/passthrough/weasel-ui.d.ts +3804 -3661
  54. package/dist/passthrough/weasel-ui.js +2 -2
  55. package/dist/primitives/index.d.ts +102 -8
  56. package/dist/primitives/index.js +4 -1
  57. package/dist/state/index.d.ts +3 -2
  58. package/dist/state/index.js +2 -2
  59. package/dist/styles.css +543 -144
  60. package/dist/surface/index.d.ts +77 -0
  61. package/dist/surface/index.js +4 -0
  62. package/dist/surface/index.js.map +1 -0
  63. package/dist/ui/layers/index.js +4 -3
  64. package/dist/undo/index.d.ts +5 -2
  65. package/package.json +24 -7
  66. package/src/canvas/AGENTS.md +12 -0
  67. package/src/canvas/CanvasStack.tsx +6 -2
  68. package/src/canvas/useLayerScheduler.ts +22 -15
  69. package/src/canvas/useOrbit.test.ts +71 -0
  70. package/src/canvas/useOrbit.ts +149 -0
  71. package/src/canvas/usePanZoom.test.ts +80 -0
  72. package/src/canvas/usePanZoom.ts +16 -2
  73. package/src/chrome/ChromeRegions.stories.tsx +106 -0
  74. package/src/chrome/builtins.test.ts +153 -0
  75. package/src/chrome/builtins.tsx +205 -0
  76. package/src/chrome/index.ts +25 -0
  77. package/src/chrome/merge.test.ts +51 -0
  78. package/src/chrome/merge.ts +52 -0
  79. package/src/chrome/regions/PaletteRegion.less +14 -0
  80. package/src/chrome/regions/PaletteRegion.test.tsx +36 -0
  81. package/src/chrome/regions/PaletteRegion.tsx +41 -0
  82. package/src/chrome/regions/SidebarRegion.tsx +56 -0
  83. package/src/chrome/regions/StatusRegion.tsx +31 -0
  84. package/src/chrome/regions/TitleBarRegion.tsx +41 -0
  85. package/src/chrome/regions/ToolbarRegion.test.tsx +82 -0
  86. package/src/chrome/regions/ToolbarRegion.tsx +71 -0
  87. package/src/chrome/regions/ViewportRegion.less +42 -0
  88. package/src/chrome/regions/ViewportRegion.tsx +37 -0
  89. package/src/chrome/regions/regions.test.tsx +81 -0
  90. package/src/chrome/types.ts +127 -0
  91. package/src/config/builder.test.ts +87 -0
  92. package/src/config/builder.ts +224 -0
  93. package/src/config/fromConfigField.test.ts +93 -0
  94. package/src/config/fromConfigField.ts +74 -0
  95. package/src/config/index.ts +22 -0
  96. package/src/config/resolve.test.ts +120 -0
  97. package/src/config/resolve.ts +67 -0
  98. package/src/config/rules.ts +68 -0
  99. package/src/config/types.ts +108 -0
  100. package/src/config/useConfigSchema.ts +30 -0
  101. package/src/config/visible.ts +20 -0
  102. package/src/controls/ControlPanel.less +4 -58
  103. package/src/controls/ControlPanel.stories.tsx +86 -0
  104. package/src/controls/ControlPanel.test.tsx +223 -10
  105. package/src/controls/ControlPanel.tsx +202 -203
  106. package/src/dragdrop/DragDropRuntime.tsx +36 -18
  107. package/src/dragdrop/DragGhost.less +1 -1
  108. package/src/dragdrop/Palette.less +1 -1
  109. package/src/index.test.ts +18 -0
  110. package/src/index.ts +38 -3
  111. package/src/instrument/SineWave.smoke.test.tsx +9 -1
  112. package/src/instrument/defineInstrument.test.ts +38 -1
  113. package/src/instrument/defineInstrument.ts +31 -4
  114. package/src/instrument/index.ts +1 -0
  115. package/src/instrument/types.ts +38 -3
  116. package/src/instrument/validateConfigSchema.test.ts +13 -3
  117. package/src/instrument/validateConfigSchema.ts +7 -10
  118. package/src/job/index.ts +3 -0
  119. package/src/job/types.ts +47 -0
  120. package/src/job/useJob.test.tsx +210 -0
  121. package/src/job/useJob.ts +134 -0
  122. package/src/lab/Lab.chrome.test.tsx +65 -0
  123. package/src/lab/Lab.stories.tsx +1 -2
  124. package/src/lab/Lab.tsx +58 -15
  125. package/src/lab/LabContext.ts +6 -0
  126. package/src/lab/LabFullChrome.stories.tsx +308 -0
  127. package/src/lab/LabHeader.test.tsx +42 -0
  128. package/src/lab/LabHeader.tsx +57 -0
  129. package/src/lab/LabPalette.test.tsx +35 -0
  130. package/src/lab/LabPalette.tsx +34 -0
  131. package/src/lab/LabShell.less +50 -3
  132. package/src/lab/Workspace.less +1 -30
  133. package/src/lab/Workspace.surface.test.tsx +49 -0
  134. package/src/lab/Workspace.test.tsx +20 -10
  135. package/src/lab/Workspace.tsx +19 -9
  136. package/src/lab/index.ts +2 -0
  137. package/src/layers/AGENTS.md +1 -1
  138. package/src/layers/LayerList.less +31 -7
  139. package/src/layers/LayerList.tsx +17 -4
  140. package/src/passthrough/weasel-ui.ts +23 -0
  141. package/src/primitives/DragHandleGlyph.tsx +29 -0
  142. package/src/primitives/FloatingPanel.less +28 -0
  143. package/src/primitives/FloatingPanel.stories.tsx +46 -0
  144. package/src/primitives/FloatingPanel.test.tsx +240 -0
  145. package/src/primitives/FloatingPanel.tsx +190 -0
  146. package/src/primitives/FpsMeter.test.tsx +44 -0
  147. package/src/primitives/FpsMeter.tsx +20 -9
  148. package/src/primitives/JobProgress.less +69 -0
  149. package/src/primitives/JobProgress.stories.tsx +48 -0
  150. package/src/primitives/JobProgress.tsx +65 -0
  151. package/src/primitives/Legend.less +47 -0
  152. package/src/primitives/Legend.stories.tsx +32 -0
  153. package/src/primitives/Legend.test.tsx +57 -0
  154. package/src/primitives/Legend.tsx +40 -0
  155. package/src/primitives/Sidebar.less +32 -2
  156. package/src/primitives/StatusBar.less +7 -0
  157. package/src/primitives/StatusBar.tsx +10 -3
  158. package/src/primitives/Toolbar.less +50 -3
  159. package/src/primitives/Toolbar.test.tsx +67 -1
  160. package/src/primitives/Toolbar.tsx +72 -6
  161. package/src/primitives/ZoomControl.less +31 -0
  162. package/src/primitives/ZoomControl.tsx +76 -0
  163. package/src/primitives/floating.entry.test.tsx +14 -0
  164. package/src/primitives/index.ts +10 -2
  165. package/src/primitives/useRovingTabIndex.test.ts +27 -0
  166. package/src/primitives/useRovingTabIndex.ts +79 -0
  167. package/src/state/store.ts +16 -2
  168. package/src/state/toolSlot.test.ts +36 -0
  169. package/src/state/types.ts +8 -2
  170. package/src/state/view.test.ts +127 -0
  171. package/src/state/view.ts +18 -0
  172. package/src/styles.less +6 -0
  173. package/src/surface/AGENTS.md +64 -0
  174. package/src/surface/SurfaceContext.ts +5 -0
  175. package/src/surface/composeRects.test.ts +50 -0
  176. package/src/surface/composeRects.ts +19 -0
  177. package/src/surface/deviceRect.test.ts +40 -0
  178. package/src/surface/deviceRect.ts +19 -0
  179. package/src/surface/index.ts +7 -0
  180. package/src/surface/rect.ts +16 -0
  181. package/src/surface/useSurfaceTile.test.tsx +67 -0
  182. package/src/surface/useSurfaceTile.ts +32 -0
  183. package/src/surface/useTiledSurface.test.tsx +231 -0
  184. package/src/surface/useTiledSurface.ts +161 -0
  185. package/src/test-setup.ts +26 -0
  186. package/src/theme/base.less +19 -2
  187. package/src/theme/interstellar.tokens.json +6 -2
  188. package/src/tools/types.ts +24 -0
  189. package/src/trial/Trial.config.test.tsx +113 -0
  190. package/src/trial/Trial.job.test.tsx +73 -0
  191. package/src/trial/Trial.less +121 -4
  192. package/src/trial/Trial.stories.tsx +9 -1
  193. package/src/trial/Trial.test.tsx +67 -96
  194. package/src/trial/Trial.tsx +112 -45
  195. package/src/trial/TrialChrome.tsx +110 -64
  196. package/src/trial/TrialDragContext.ts +10 -0
  197. package/src/trial/TrialTitleBar.tsx +41 -0
  198. package/src/trial/index.ts +1 -15
  199. package/src/trial/trialChrome.borders.test.ts +38 -0
  200. package/src/ui/layers/LayerStack.less +9 -9
  201. package/src/ui/layers/LayerStack.tsx +1 -13
  202. package/src/ui/properties/CurveField.less +2 -2
  203. package/src/ui/properties/CurveField.test.tsx +11 -11
  204. package/src/ui/properties/PropertyGroup.less +2 -2
  205. package/src/ui/properties/PropertyPanel.less +55 -41
  206. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +4 -2
  207. package/src/ui/properties/storyLayouts.tsx +2 -2
  208. package/dist/_dts/index-iAP6XbH3.d.ts +0 -132
  209. package/dist/_dts/types-x92Kfeme.d.ts +0 -62
  210. package/dist/chunk-3TYUJR7Z.js.map +0 -1
  211. package/dist/chunk-574LJAV4.js.map +0 -1
  212. package/dist/chunk-5R2ATYPJ.js +0 -201
  213. package/dist/chunk-5R2ATYPJ.js.map +0 -1
  214. package/dist/chunk-C6GJKPUI.js +0 -5986
  215. package/dist/chunk-C6GJKPUI.js.map +0 -1
  216. package/dist/chunk-DJLDIRFN.js.map +0 -1
  217. package/dist/chunk-NRKWVTVT.js.map +0 -1
  218. package/dist/chunk-PMAU3SEE.js +0 -126
  219. package/dist/chunk-PMAU3SEE.js.map +0 -1
  220. package/dist/chunk-SFL7NFKN.js.map +0 -1
  221. package/src/instrument/capabilityDetector.test.ts +0 -64
  222. package/src/instrument/capabilityDetector.ts +0 -20
  223. package/src/trial/DefaultSidebar.tsx +0 -29
  224. package/src/trial/DefaultStatusBar.tsx +0 -19
  225. package/src/trial/DefaultToolbar.tsx +0 -92
  226. package/src/trial/slotTypes.ts +0 -53
package/dist/index.js CHANGED
@@ -1,43 +1,369 @@
1
- export { LayerStack } from './chunk-NRKWVTVT.js';
2
- import { p, F } from './chunk-C6GJKPUI.js';
3
- import './chunk-DJLDIRFN.js';
4
- import { Sidebar, StatusBar, Toolbar } from './chunk-PMAU3SEE.js';
5
- export { FpsMeter, ScaleIndicator, Sidebar, StatusBar, Toolbar } from './chunk-PMAU3SEE.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
- import { ControlPanel } from './chunk-5R2ATYPJ.js';
9
- export { ControlPanel } from './chunk-5R2ATYPJ.js';
10
- import { CanvasStack, screenToWorld } from './chunk-574LJAV4.js';
11
- export { CanvasStack, screenToWorld, worldToScreen } from './chunk-574LJAV4.js';
1
+ import { useSurfaceOptional } from './chunk-HFDVGF4X.js';
2
+ export { SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceOptional, useSurfaceTile, useTiledSurface } from './chunk-HFDVGF4X.js';
3
+ import { useJob } from './chunk-LN6JDUGB.js';
4
+ export { useJob } from './chunk-LN6JDUGB.js';
5
+ export { LayerStack } from './chunk-O2BULFHX.js';
6
+ import { suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, SidebarRegion, ViewportRegion, StatusRegion } from './chunk-PO6L3NSH.js';
7
+ export { PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, builtinContributions, mergeContributions, suppressContributions } from './chunk-PO6L3NSH.js';
8
+ import { JobProgress } from './chunk-RVNN4CHQ.js';
9
+ export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-RVNN4CHQ.js';
10
+ export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-O7NVSCLN.js';
11
+ import { LabStoreContext, createLabStore, noneAdapter } from './chunk-4TMMVIDM.js';
12
+ export { CURRENT_DOCUMENT_VERSION, LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, createMemoryAdapter, labDocumentKey, localStorageAdapter, noneAdapter, quarantineKey, sessionStorageAdapter, urlHashAdapter, useLabStore, useTrialId, useTrialState } from './chunk-4TMMVIDM.js';
13
+ import { fromConfigFields } from './chunk-ZZAYVX4X.js';
14
+ export { CheckboxRow, ColorRow, ControlPanel, NumberRow, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SelectRow, SliderRow, TextRow, ToggleRow, fromConfigFields, isLeafVisible } from './chunk-ZZAYVX4X.js';
15
+ import { p, F, vt, p2, r } from './chunk-TLGRYALP.js';
16
+ import { CanvasStack, screenToWorld } from './chunk-NS54R52R.js';
17
+ export { CanvasStack, screenToWorld, worldToScreen } from './chunk-NS54R52R.js';
12
18
  export { CanvasStackContext } from './chunk-CPUJ3QXL.js';
13
- import { LayerList } from './chunk-SFL7NFKN.js';
14
- export { LayerList } from './chunk-SFL7NFKN.js';
19
+ import { useVisibleRaf } from './chunk-Z6HJ5FEM.js';
20
+ import { LayerList } from './chunk-V7PRSIRQ.js';
21
+ export { LayerList } from './chunk-V7PRSIRQ.js';
22
+ import './chunk-PQJ5B2U2.js';
15
23
  import { createEventBus, redo, undo, pushSnapshot } from './chunk-RL2LOLNI.js';
16
24
  export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
17
- import { createContext, useContext, useMemo, useRef, useState, Children, useLayoutEffect, useEffect, useCallback } from 'react';
18
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
25
+ import { createContext, useRef, useCallback, useContext, useMemo, useState, Children, useLayoutEffect, useEffect } from 'react';
26
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
19
27
  import { useStore } from 'zustand/react';
20
28
  import { createPortal } from 'react-dom';
29
+ import { Provider, StrategyRegistryProvider, DragProvider, Container, useDragHandle } from 'windease/react';
21
30
  import { asNodeId, Store, createNode, gridStrategy } from 'windease';
22
- import { DragHandle, Provider, StrategyRegistryProvider, DragProvider, Container } from 'windease/react';
23
31
 
24
- // src/instrument/capabilityDetector.ts
25
- function detectCapabilities(instrument) {
32
+ var PITCH_LIMIT = Math.PI / 2 - 0.01;
33
+ var YAW_PER_PX = 8e-3;
34
+ var PITCH_PER_PX = 8e-3;
35
+ var DISTANCE_PER_NOTCH = 15e-4;
36
+ var DRAG_THRESHOLD = 3;
37
+ function clampPitch(pitch) {
38
+ return Math.min(PITCH_LIMIT, Math.max(-PITCH_LIMIT, pitch));
39
+ }
40
+ function wrapYaw(yaw) {
41
+ const wrapped = ((yaw + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) - Math.PI;
42
+ return wrapped === -Math.PI ? Math.PI : wrapped;
43
+ }
44
+ function orbitAfterDrag(start, dx, dy) {
45
+ return {
46
+ ...start,
47
+ yaw: wrapYaw(start.yaw + dx * YAW_PER_PX),
48
+ pitch: clampPitch(start.pitch + dy * PITCH_PER_PX)
49
+ };
50
+ }
51
+ function orbitAfterWheel(view, deltaY, minDistance, maxDistance) {
52
+ const factor = Math.exp(deltaY * DISTANCE_PER_NOTCH);
26
53
  return {
27
- hasCanvas: instrument.canvas != null,
28
- hasLayers: instrument.layers != null,
29
- hasDragDrop: instrument.dragDrop != null,
30
- hasUndo: instrument.undo != null
54
+ ...view,
55
+ distance: Math.min(maxDistance, Math.max(minDistance, view.distance * factor))
31
56
  };
32
57
  }
58
+ function useOrbit({
59
+ view,
60
+ onViewChange,
61
+ home,
62
+ minDistance = 0.1,
63
+ maxDistance = 1e3
64
+ }) {
65
+ const dragRef = useRef(null);
66
+ const viewRef = useRef(view);
67
+ viewRef.current = view;
68
+ const homeRef = useRef(home ?? view);
69
+ if (home) homeRef.current = home;
70
+ const onWheel = useCallback(
71
+ (e) => {
72
+ e.preventDefault();
73
+ onViewChange(orbitAfterWheel(viewRef.current, e.deltaY, minDistance, maxDistance));
74
+ },
75
+ [onViewChange, minDistance, maxDistance]
76
+ );
77
+ const onPointerDown = useCallback((e) => {
78
+ if (e.button !== 0) return;
79
+ e.currentTarget.setPointerCapture(e.pointerId);
80
+ dragRef.current = {
81
+ pointerId: e.pointerId,
82
+ startX: e.clientX,
83
+ startY: e.clientY,
84
+ startView: viewRef.current,
85
+ moved: false
86
+ };
87
+ }, []);
88
+ const onPointerMove = useCallback(
89
+ (e) => {
90
+ const drag = dragRef.current;
91
+ if (!drag || drag.pointerId !== e.pointerId) return;
92
+ const dx = e.clientX - drag.startX;
93
+ const dy = e.clientY - drag.startY;
94
+ if (!drag.moved && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
95
+ drag.moved = true;
96
+ onViewChange(orbitAfterDrag(drag.startView, dx, dy));
97
+ },
98
+ [onViewChange]
99
+ );
100
+ const onPointerUp = useCallback((e) => {
101
+ const drag = dragRef.current;
102
+ if (!drag || drag.pointerId !== e.pointerId) return;
103
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
104
+ e.currentTarget.releasePointerCapture(e.pointerId);
105
+ }
106
+ dragRef.current = null;
107
+ }, []);
108
+ const onDoubleClick = useCallback(() => onViewChange(homeRef.current), [onViewChange]);
109
+ const isDragging = useCallback(() => dragRef.current?.moved === true, []);
110
+ return { onWheel, onPointerDown, onPointerMove, onPointerUp, onDoubleClick, isDragging };
111
+ }
112
+
113
+ // src/config/builder.ts
114
+ var BaseNode = class {
115
+ constructor(defaultValue, annotations = {}, options = {}) {
116
+ this.annotations = annotations;
117
+ this.options = options;
118
+ this.default = defaultValue;
119
+ }
120
+ annotations;
121
+ options;
122
+ // `default` is a reserved word in a parameter position, so the field is
123
+ // declared rather than taken as a parameter property.
124
+ default;
125
+ /** Clone into the same subclass. Subclasses with extra constructor
126
+ * arguments override this. */
127
+ with(annotations, options) {
128
+ const Ctor = this.constructor;
129
+ return new Ctor(this.default, annotations, options);
130
+ }
131
+ ann(patch) {
132
+ return this.with({ ...this.annotations, ...patch }, this.options);
133
+ }
134
+ opt(patch) {
135
+ return this.with(this.annotations, { ...this.options, ...patch });
136
+ }
137
+ /** Human-readable label. Defaults to the key, title-cased. */
138
+ label(name) {
139
+ return this.ann({ name });
140
+ }
141
+ /** Longer help text, surfaced as a tooltip on the row label. */
142
+ describe(description) {
143
+ return this.ann({ description });
144
+ }
145
+ /** Omit from the panel unless it is asked to show hidden leaves. */
146
+ hidden() {
147
+ return this.ann({ hidden: true });
148
+ }
149
+ /** Pair with a sibling sharing this id, side-by-side on one row. */
150
+ pair(pair) {
151
+ return this.ann({ pair });
152
+ }
153
+ /** Render under a named section heading. */
154
+ section(section) {
155
+ return this.opt({ section });
156
+ }
157
+ /** Show this row only while the predicate holds. Presentational — the value
158
+ * stays in config and the instrument still reads it. */
159
+ showIf(predicate) {
160
+ return this.opt({ showIf: predicate });
161
+ }
162
+ /** Draw this one row yourself, keeping the kind, default and validation. */
163
+ render(renderer) {
164
+ return this.opt({ render: renderer });
165
+ }
166
+ };
167
+ var NumberNode = class extends BaseNode {
168
+ kind = "number";
169
+ /** Bound the value. A number with both bounds renders as a slider. */
170
+ range(min, max) {
171
+ return this.ann({ min, max });
172
+ }
173
+ step(step) {
174
+ return this.ann({ step });
175
+ }
176
+ /** Force a slider even without both bounds. */
177
+ slider() {
178
+ return this.ann({ control: "slider" });
179
+ }
180
+ /** Force a typed input even when both bounds are set. */
181
+ input() {
182
+ return this.ann({ control: "input" });
183
+ }
184
+ };
185
+ var BooleanNode = class extends BaseNode {
186
+ kind = "boolean";
187
+ /** Ask for a switch. `ControlPanel` still draws a checkbox; weasel-ui's
188
+ * `PrefsForm` honors the distinction. */
189
+ toggle() {
190
+ return this.ann({ control: "switch" });
191
+ }
192
+ };
193
+ var StringNode = class extends BaseNode {
194
+ kind = "string";
195
+ placeholder(placeholder) {
196
+ return this.ann({ placeholder });
197
+ }
198
+ maxLength(maxLength) {
199
+ return this.ann({ maxLength });
200
+ }
201
+ /** Milliseconds to debounce live writes. 0 commits every keystroke. */
202
+ debounce(debounceMs) {
203
+ return this.ann({ debounceMs });
204
+ }
205
+ };
206
+ var ColorNode = class extends BaseNode {
207
+ kind = "color";
208
+ };
209
+ var EnumNode = class extends BaseNode {
210
+ kind = "enum";
211
+ /** Render as a segmented control rather than a select. */
212
+ radio() {
213
+ return this.ann({ control: "radio" });
214
+ }
215
+ /** Relabel the choices. Named `labels` rather than `options` because
216
+ * `options` is the node's own extras bag. */
217
+ labels(options) {
218
+ return this.ann({ options });
219
+ }
220
+ };
221
+ var ValueNode = class extends BaseNode {
222
+ kind = null;
223
+ };
224
+ var CustomNode = class _CustomNode extends BaseNode {
225
+ constructor(kind, defaultValue, annotations = {}, options = {}) {
226
+ super(defaultValue, annotations, options);
227
+ this.kind = kind;
228
+ }
229
+ kind;
230
+ with(annotations, options) {
231
+ return new _CustomNode(this.kind, this.default, annotations, options);
232
+ }
233
+ };
234
+ var expand = (options) => options.map((o) => typeof o === "string" ? { value: o, label: o } : o);
235
+ var f = {
236
+ number: (def) => new NumberNode(def),
237
+ boolean: (def) => new BooleanNode(def),
238
+ string: (def) => new StringNode(def),
239
+ color: (def) => new ColorNode(def),
240
+ /** A fixed set of choices. The default's literal type flows into the config
241
+ * type, so `f.enum('fast', ['fast', 'accurate'])` gives
242
+ * `'fast' | 'accurate'` with no `as const`. */
243
+ enum: (def, options) => new EnumNode(def, { options: expand(options) }),
244
+ /** A leaf with no declared kind: the rule chain decides, which is how a lab
245
+ * states a convention like "every `*Color` key is a color picker". */
246
+ value: (def) => new ValueNode(def),
247
+ /** A leaf of a kind a lab supplies the control for, through `controls`. */
248
+ custom: (kind, def, validate) => new CustomNode(kind, def, {}, validate ? { validate } : {}),
249
+ /** Collect leaves into an instrument's config. */
250
+ schema(nodes) {
251
+ return {
252
+ nodes,
253
+ defaults: () => {
254
+ const out = {};
255
+ for (const [key, node] of Object.entries(nodes)) out[key] = node.default;
256
+ return out;
257
+ }
258
+ };
259
+ }
260
+ };
261
+
262
+ // src/config/rules.ts
263
+ function titleCase(key) {
264
+ const words = key.replace(/[_-]+/g, " ").split(/(?<=[a-z0-9])(?=[A-Z])/).join(" ").trim().toLowerCase();
265
+ return words.length === 0 ? words : words[0].toUpperCase() + words.slice(1);
266
+ }
267
+ var kindFromValue = (ctx) => {
268
+ const t = typeof ctx.default;
269
+ if (t === "boolean" || t === "number" || t === "string") return { kind: t };
270
+ return null;
271
+ };
272
+ var labelFromKey = (ctx) => ({ name: titleCase(ctx.key) });
273
+ var sliderWhenBounded = (ctx) => {
274
+ if (ctx.leaf.kind !== "number") return null;
275
+ const bounded = ctx.leaf.min !== void 0 && ctx.leaf.max !== void 0;
276
+ return bounded ? { control: "slider" } : null;
277
+ };
278
+ var descriptionDefault = () => ({ description: "" });
279
+ var builtinRules = [
280
+ kindFromValue,
281
+ labelFromKey,
282
+ sliderWhenBounded,
283
+ descriptionDefault
284
+ ];
285
+ function applyRules(seed, ctx, rules) {
286
+ const acc = { ...seed };
287
+ for (const rule of rules) {
288
+ const patch = rule({ ...ctx, leaf: acc });
289
+ if (patch === null) continue;
290
+ for (const [k, v] of Object.entries(patch)) {
291
+ if (v !== void 0 && acc[k] === void 0) {
292
+ acc[k] = v;
293
+ }
294
+ }
295
+ }
296
+ return acc;
297
+ }
298
+
299
+ // src/config/resolve.ts
300
+ function defined(annotations) {
301
+ const out = {};
302
+ for (const [k, v] of Object.entries(annotations)) if (v !== void 0) out[k] = v;
303
+ return out;
304
+ }
305
+ function resolveConfigSchema(schema, rules = []) {
306
+ const children = {};
307
+ const sectionOrder = [];
308
+ const sectionPaths = /* @__PURE__ */ new Map();
309
+ const showIf = /* @__PURE__ */ new Map();
310
+ const renderers = {};
311
+ const chain2 = [...rules, ...builtinRules];
312
+ for (const [key, node] of Object.entries(schema.nodes)) {
313
+ const seed = defined({
314
+ ...node.annotations,
315
+ ...node.kind === null ? {} : { kind: node.kind }
316
+ });
317
+ const patch = applyRules(seed, { key, path: key, default: node.default }, chain2);
318
+ children[key] = { ...patch, default: node.default };
319
+ const { section, showIf: predicate, render } = node.options;
320
+ if (section !== void 0) {
321
+ if (!sectionPaths.has(section)) {
322
+ sectionOrder.push(section);
323
+ sectionPaths.set(section, []);
324
+ }
325
+ sectionPaths.get(section)?.push(key);
326
+ }
327
+ if (predicate) showIf.set(key, predicate);
328
+ if (render) renderers[key] = render;
329
+ }
330
+ const group = { name: "", children };
331
+ const sections = sectionOrder.map((label) => ({
332
+ label,
333
+ paths: sectionPaths.get(label) ?? []
334
+ }));
335
+ return { group, sections, showIf, renderers };
336
+ }
337
+ var LabContext = createContext(null);
338
+ function useLabContext() {
339
+ const ctx = useContext(LabContext);
340
+ if (ctx === null) {
341
+ throw new Error("useLabContext must be used inside <Lab>");
342
+ }
343
+ return ctx;
344
+ }
345
+
346
+ // src/config/useConfigSchema.ts
347
+ var NO_RULES = [];
348
+ function useConfigSchema(instrument) {
349
+ const lab = useContext(LabContext);
350
+ const rules = lab?.configRules ?? NO_RULES;
351
+ return useMemo(
352
+ () => instrument.config ? resolveConfigSchema(instrument.config, rules) : fromConfigFields(instrument.configSchema?.() ?? []),
353
+ [instrument, rules]
354
+ );
355
+ }
33
356
 
34
357
  // src/instrument/defineInstrument.ts
35
358
  function defineInstrument(spec) {
359
+ if (spec.config && !("defaultConfig" in spec && spec.defaultConfig)) {
360
+ const schema = spec.config;
361
+ return { ...spec, defaultConfig: () => schema.defaults() };
362
+ }
36
363
  return spec;
37
364
  }
38
365
 
39
366
  // src/instrument/validateConfigSchema.ts
40
- var KNOWN_TYPES = ["slider", "checkbox", "select", "number", "text", "color"];
41
367
  function validateConfigSchema(fields) {
42
368
  const errors = [];
43
369
  const seenKeys = /* @__PURE__ */ new Set();
@@ -52,10 +378,6 @@ function validateConfigSchema(fields) {
52
378
  if (field.label === "") {
53
379
  errors.push(`Field "${field.key}" has empty label`);
54
380
  }
55
- if (!KNOWN_TYPES.includes(field.type)) {
56
- errors.push(`Unknown field type: "${field.type}" on key "${field.key}"`);
57
- continue;
58
- }
59
381
  if (field.type === "slider") {
60
382
  if (!Number.isFinite(field.default)) {
61
383
  errors.push(`Field "${field.key}": default must be a finite number`);
@@ -90,7 +412,7 @@ function validateConfigSchema(fields) {
90
412
  return { valid: errors.length === 0, errors };
91
413
  }
92
414
 
93
- // ../theme/dist/chunk-E3FOFGMD.js
415
+ // ../theme/dist/chunk-O32P3G22.js
94
416
  var THEME_SOURCES = {
95
417
  "weasel": {
96
418
  "primitives": {
@@ -276,6 +598,15 @@ var THEME_SOURCES = {
276
598
  "type": "dimension",
277
599
  "value": "5px"
278
600
  },
601
+ "radius-lg": {
602
+ "type": "dimension",
603
+ "value": "14px"
604
+ },
605
+ "radius-pill": {
606
+ "type": "dimension",
607
+ "value": "999px",
608
+ "description": "Fully rounded ends \u2014 segmented controls, progress tracks."
609
+ },
279
610
  "border-w": {
280
611
  "type": "dimension",
281
612
  "value": "1px"
@@ -293,29 +624,47 @@ var THEME_SOURCES = {
293
624
  "tb-height": {
294
625
  "type": "dimension",
295
626
  "value": "28px",
296
- "description": "Height of a horizontal toolbar strip (ActionsBar, OptionsBar, ToggleBar, ToolOptionsBar)."
297
- },
298
- "radius-lg": {
299
- "type": "dimension",
300
- "value": "14px"
627
+ "description": "Height of a horizontal toolbar strip (ActionsBar, OptionsBar, ToolOptionsBar). Taller than control-h so the controls inside a strip are not flush with its edges."
301
628
  },
302
629
  "control-h": {
303
630
  "type": "dimension",
304
- "value": "28px",
305
- "description": "Height of an interactive control \u2014 button, input, select. Distinct from tb-height, which sizes the strip a row of them sits in."
631
+ "value": "24px",
632
+ "description": "Height of an interactive control \u2014 button, input, select, segmented control. Distinct from tb-height, which sizes the strip a row of them sits in."
306
633
  },
307
634
  "glass-blur": {
308
635
  "type": "dimension",
309
636
  "value": "3px",
310
637
  "description": "Backdrop blur radius for frosted surfaces. Pairs with glass-tint."
311
638
  },
312
- "font-size": {
639
+ "font-size-2xs": {
640
+ "type": "dimension",
641
+ "value": "9px",
642
+ "description": "Shortcut keys and index badges. The floor for chrome text."
643
+ },
644
+ "font-size-xs": {
313
645
  "type": "dimension",
314
- "value": "13px"
646
+ "value": "10px",
647
+ "description": "Tool-button labels under an icon."
315
648
  },
316
649
  "font-size-sm": {
317
650
  "type": "dimension",
318
- "value": "11px"
651
+ "value": "11px",
652
+ "description": "Chrome labels, status readouts, section headings."
653
+ },
654
+ "font-size": {
655
+ "type": "dimension",
656
+ "value": "13px",
657
+ "description": "Controls and body text."
658
+ },
659
+ "font-size-lg": {
660
+ "type": "dimension",
661
+ "value": "16px",
662
+ "description": "Panel and dialog titles."
663
+ },
664
+ "font-size-xl": {
665
+ "type": "dimension",
666
+ "value": "20px",
667
+ "description": "The largest chrome text \u2014 a lab or app title."
319
668
  },
320
669
  "space-xs": {
321
670
  "type": "dimension",
@@ -333,6 +682,20 @@ var THEME_SOURCES = {
333
682
  "type": "dimension",
334
683
  "value": "16px"
335
684
  },
685
+ "tracking-none": {
686
+ "type": "dimension",
687
+ "value": "0"
688
+ },
689
+ "tracking-wide": {
690
+ "type": "dimension",
691
+ "value": "0.06em",
692
+ "description": "Uppercase section headings."
693
+ },
694
+ "tracking-wider": {
695
+ "type": "dimension",
696
+ "value": "0.08em",
697
+ "description": "Uppercase at 11px and below."
698
+ },
336
699
  "z-toolbar": {
337
700
  "type": "number",
338
701
  "value": 10
@@ -345,6 +708,21 @@ var THEME_SOURCES = {
345
708
  "type": "number",
346
709
  "value": 30
347
710
  },
711
+ "leading-tight": {
712
+ "type": "number",
713
+ "value": 1,
714
+ "description": "Single-line controls; the box sets the height."
715
+ },
716
+ "leading-snug": {
717
+ "type": "number",
718
+ "value": 1.2,
719
+ "description": "Headings and two-line labels."
720
+ },
721
+ "leading": {
722
+ "type": "number",
723
+ "value": 1.4,
724
+ "description": "Body and anything that wraps."
725
+ },
348
726
  "backdrop": {
349
727
  "type": "gradient",
350
728
  "value": "none",
@@ -497,6 +875,16 @@ var THEME_SOURCES = {
497
875
  "type": "color",
498
876
  "value": "{color.gray-900}",
499
877
  "description": "Text drawn on a filled foreground-colored element (e.g. a slider thumb). Flips with the mode, so it is a semantic, not an alias to a primitive."
878
+ },
879
+ "shadow": {
880
+ "type": "color",
881
+ "value": "rgba(0, 0, 0, 0.6)",
882
+ "description": "Elevation. Always darker than any surface \u2014 never derived from fg, which is near-white on dark."
883
+ },
884
+ "border-raised": {
885
+ "type": "color",
886
+ "value": "{color.gray-400}",
887
+ "description": "The line around a raised surface. Clears 3:1 against surface and surface-raised in either mode, which the general-purpose border does not; over surface-sunken it does not, so a panel on a sunken field needs its own answer."
500
888
  }
501
889
  },
502
890
  "light": {
@@ -541,6 +929,14 @@ var THEME_SOURCES = {
541
929
  "type": "color",
542
930
  "value": "{color.gray-50}",
543
931
  "description": "Flips with the mode \u2014 see the dark layer."
932
+ },
933
+ "shadow": {
934
+ "type": "color",
935
+ "value": "rgba(0, 0, 0, 0.18)"
936
+ },
937
+ "border-raised": {
938
+ "type": "color",
939
+ "value": "{color.gray-400}"
544
940
  }
545
941
  }
546
942
  }
@@ -558,10 +954,10 @@ function hexToRgba(hex, alpha) {
558
954
  if (!m) throw new Error(`Expected a hex color, got "${hex}"`);
559
955
  const body = m[1];
560
956
  const full = body.length === 3 ? body.replace(/./g, (c) => c + c) : body;
561
- const r = Number.parseInt(full.slice(0, 2), 16);
957
+ const r2 = Number.parseInt(full.slice(0, 2), 16);
562
958
  const g = Number.parseInt(full.slice(2, 4), 16);
563
959
  const b = Number.parseInt(full.slice(4, 6), 16);
564
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
960
+ return `rgba(${r2}, ${g}, ${b}, ${alpha})`;
565
961
  }
566
962
  var REF = /^\{([^}]+)\}$/;
567
963
  function refTarget(value) {
@@ -573,7 +969,7 @@ function refTarget(value) {
573
969
  return dot === -1 ? path : path.slice(dot + 1);
574
970
  }
575
971
  function fontStack(value) {
576
- return value.map((f) => typeof f === "string" && /\s/.test(f) ? `'${f}'` : String(f)).join(", ");
972
+ return value.map((f2) => typeof f2 === "string" && /\s/.test(f2) ? `'${f2}'` : String(f2)).join(", ");
577
973
  }
578
974
  function serialize(type, value) {
579
975
  if (Array.isArray(value)) {
@@ -780,7 +1176,9 @@ var interstellar_tokens_default = {
780
1176
  "fg-subtle": { $value: "#5a5a78" },
781
1177
  accent: { $value: "#b08adb" },
782
1178
  "accent-hover": { $value: "#c6a3ee" },
783
- "focus-ring": { $value: "rgba(176, 138, 219, 0.45)" }
1179
+ "focus-ring": { $value: "rgba(176, 138, 219, 0.45)" },
1180
+ shadow: { $value: "rgba(0, 0, 0, 0.7)" },
1181
+ "border-raised": { $value: "rgba(255, 255, 255, 0.36)" }
784
1182
  },
785
1183
  gradient: {
786
1184
  $type: "gradient",
@@ -802,7 +1200,9 @@ var interstellar_tokens_default = {
802
1200
  "fg-subtle": { $value: "#a89b85" },
803
1201
  accent: { $value: "#a86f3c" },
804
1202
  "accent-hover": { $value: "#c2854f" },
805
- "focus-ring": { $value: "rgba(168, 111, 60, 0.35)" }
1203
+ "focus-ring": { $value: "rgba(168, 111, 60, 0.35)" },
1204
+ shadow: { $value: "rgba(80, 60, 40, 0.18)" },
1205
+ "border-raised": { $value: "#9e8b71" }
806
1206
  }
807
1207
  }
808
1208
  }
@@ -835,13 +1235,13 @@ function useDragDrop({
835
1235
  const [drag, setDrag] = useState(null);
836
1236
  const dragRef = useRef(null);
837
1237
  dragRef.current = drag;
838
- const rafThrottle = useRef(null);
1238
+ const pendingPos = useRef(null);
839
1239
  const isOverCanvas = useCallback(
840
1240
  (screenPos) => {
841
1241
  const el = canvasContainerRef.current;
842
1242
  if (!el) return false;
843
- const r = el.getBoundingClientRect();
844
- return screenPos.x >= r.left && screenPos.x <= r.right && screenPos.y >= r.top && screenPos.y <= r.bottom;
1243
+ const r2 = el.getBoundingClientRect();
1244
+ return screenPos.x >= r2.left && screenPos.x <= r2.right && screenPos.y >= r2.top && screenPos.y <= r2.bottom;
845
1245
  },
846
1246
  [canvasContainerRef]
847
1247
  );
@@ -849,42 +1249,44 @@ function useDragDrop({
849
1249
  (screenPos) => {
850
1250
  const el = canvasContainerRef.current;
851
1251
  if (!el) return null;
852
- const r = el.getBoundingClientRect();
853
- return screenToWorld({ x: screenPos.x - r.left, y: screenPos.y - r.top }, view);
1252
+ const r2 = el.getBoundingClientRect();
1253
+ return screenToWorld({ x: screenPos.x - r2.left, y: screenPos.y - r2.top }, view);
854
1254
  },
855
1255
  [canvasContainerRef, view]
856
1256
  );
1257
+ const frameLoop = useVisibleRaf(() => {
1258
+ const screenPos = pendingPos.current;
1259
+ pendingPos.current = null;
1260
+ const active = dragRef.current;
1261
+ if (!active || !screenPos) return;
1262
+ let feedback = null;
1263
+ if (capability.onDragOver && isOverCanvas(screenPos)) {
1264
+ const world = screenToWorldFromContainer(screenPos);
1265
+ if (world) feedback = capability.onDragOver(world, active.item, state, config);
1266
+ }
1267
+ setDrag({ ...active, screenPos, feedback });
1268
+ });
857
1269
  useEffect(() => {
858
1270
  if (!drag) return;
859
1271
  const handleMove = (e) => {
860
1272
  const screenPos = { x: e.clientX, y: e.clientY };
861
1273
  const current = dragRef.current;
862
1274
  if (!current) return;
863
- if (rafThrottle.current !== null) {
1275
+ if (pendingPos.current !== null) {
1276
+ pendingPos.current = screenPos;
864
1277
  dragRef.current = { ...current, screenPos };
865
1278
  setDrag(dragRef.current);
866
1279
  return;
867
1280
  }
868
- rafThrottle.current = requestAnimationFrame(() => {
869
- rafThrottle.current = null;
870
- const active = dragRef.current;
871
- if (!active) return;
872
- let feedback = null;
873
- if (capability.onDragOver && isOverCanvas(screenPos)) {
874
- const world = screenToWorldFromContainer(screenPos);
875
- if (world) feedback = capability.onDragOver(world, active.item, state, config);
876
- }
877
- setDrag({ ...active, screenPos, feedback });
878
- });
1281
+ pendingPos.current = screenPos;
1282
+ frameLoop.request();
879
1283
  };
880
1284
  const handleUp = (e) => {
881
1285
  const screenPos = { x: e.clientX, y: e.clientY };
882
1286
  const active = dragRef.current;
883
1287
  if (!active) return;
884
- if (rafThrottle.current !== null) {
885
- cancelAnimationFrame(rafThrottle.current);
886
- rafThrottle.current = null;
887
- }
1288
+ pendingPos.current = null;
1289
+ frameLoop.cancel();
888
1290
  if (isOverCanvas(screenPos)) {
889
1291
  const world = screenToWorldFromContainer(screenPos);
890
1292
  if (world) {
@@ -901,7 +1303,17 @@ function useDragDrop({
901
1303
  window.removeEventListener("pointermove", handleMove);
902
1304
  window.removeEventListener("pointerup", handleUp);
903
1305
  };
904
- }, [drag, capability, state, config, setState, emit, isOverCanvas, screenToWorldFromContainer]);
1306
+ }, [
1307
+ drag,
1308
+ capability,
1309
+ state,
1310
+ config,
1311
+ setState,
1312
+ emit,
1313
+ frameLoop,
1314
+ isOverCanvas,
1315
+ screenToWorldFromContainer
1316
+ ]);
905
1317
  const startDrag = useCallback((item, originScreenPos) => {
906
1318
  setDrag({ item, screenPos: originScreenPos, feedback: null });
907
1319
  }, []);
@@ -926,119 +1338,51 @@ function Palette({ items, onDragStart, className }) {
926
1338
  item.id
927
1339
  )) });
928
1340
  }
929
- var LabContext = createContext(null);
930
- function useLabContext() {
931
- const ctx = useContext(LabContext);
932
- if (ctx === null) {
933
- throw new Error("useLabContext must be used inside <Lab>");
934
- }
935
- return ctx;
936
- }
937
- function DefaultSidebar({ ctx }) {
938
- if (ctx.configFields.length === 0) {
939
- return /* @__PURE__ */ jsx(Sidebar, { title: ctx.instrumentName, children: /* @__PURE__ */ jsxs("div", { className: "lk-sidebar__placeholder", children: [
940
- ctx.instrumentName,
941
- " has no config fields."
942
- ] }) });
943
- }
944
- return /* @__PURE__ */ jsx(Sidebar, { title: ctx.instrumentName, children: /* @__PURE__ */ jsx(
945
- ControlPanel,
946
- {
947
- fields: ctx.configFields,
948
- config: ctx.config,
949
- setConfig: (key, value) => ctx.setConfig(key, value)
950
- }
951
- ) });
952
- }
953
1341
 
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, "-"));
1342
+ // src/state/view.ts
1343
+ var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
1344
+ function as2DView(view) {
1345
+ if (typeof view !== "object" || view === null) return null;
1346
+ const v = view;
1347
+ if (typeof v.zoom !== "number") return null;
1348
+ if (typeof v.pan !== "object" || v.pan === null) return null;
1349
+ if (typeof v.pan.x !== "number" || typeof v.pan.y !== "number") return null;
1350
+ return { zoom: v.zoom, pan: { x: v.pan.x, y: v.pan.y } };
963
1351
  }
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`;
1352
+ var TrialDragContext = createContext(null);
1353
+ function useTrialDrag() {
1354
+ return useContext(TrialDragContext);
969
1355
  }
970
- function DefaultStatusBar({ ctx }) {
971
- return /* @__PURE__ */ jsxs(StatusBar, { children: [
972
- /* @__PURE__ */ jsx(StatusBar.Section, { children: ctx.instrumentName }),
973
- /* @__PURE__ */ jsx(StatusBar.Section, { children: formatZoom(ctx.zoom) })
1356
+ function Draggable({ nodeId, title, children }) {
1357
+ const handlers = useDragHandle(nodeId);
1358
+ return /* @__PURE__ */ jsxs("div", { className: "lk-trial__titlebar lk-trial__titlebar--draggable", ...handlers, children: [
1359
+ /* @__PURE__ */ jsx("span", { className: "lk-trial__title", children: title }),
1360
+ children
974
1361
  ] });
975
1362
  }
976
- function DefaultToolbar({ ctx }) {
977
- return /* @__PURE__ */ jsxs(Toolbar, { children: [
978
- ctx.hasUndo && /* @__PURE__ */ jsxs(Fragment, { children: [
979
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.undo, disabled: !ctx.canUndo, title: "Undo (Cmd/Ctrl+Z)", children: "Undo" }),
980
- /* @__PURE__ */ jsx(
981
- Toolbar.Button,
982
- {
983
- onClick: ctx.redo,
984
- disabled: !ctx.canRedo,
985
- title: "Redo (Cmd/Ctrl+Shift+Z)",
986
- children: "Redo"
987
- }
988
- ),
989
- /* @__PURE__ */ jsx(Toolbar.Spacer, {})
990
- ] }),
991
- ctx.hasCanvas && /* @__PURE__ */ jsxs(Fragment, { children: [
992
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.zoomOut, title: "Zoom out", children: "\u2212" }),
993
- /* @__PURE__ */ jsx("span", { className: "lk-toolbar__zoom-label", children: formatZoom(ctx.zoom) }),
994
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.zoomIn, title: "Zoom in", children: "+" }),
995
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.resetZoom, title: "Reset zoom", children: "1:1" }),
996
- /* @__PURE__ */ jsx(Toolbar.Spacer, {})
997
- ] }),
998
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: () => ctx.saveSnapshot(), title: "Save snapshot (Cmd/Ctrl+S)", children: "Save" }),
999
- ctx.savedSnapshots.length > 0 && /* @__PURE__ */ jsxs(
1000
- "select",
1001
- {
1002
- className: "lk-toolbar__load-select",
1003
- "aria-label": "Load snapshot",
1004
- defaultValue: "",
1005
- onChange: (e) => {
1006
- const id = e.target.value;
1007
- if (id) {
1008
- ctx.loadSnapshot(id);
1009
- e.target.value = "";
1010
- }
1011
- },
1012
- children: [
1013
- /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Load\u2026" }),
1014
- ctx.savedSnapshots.map((sn) => /* @__PURE__ */ jsx("option", { value: sn.id, children: sn.name }, sn.id))
1015
- ]
1016
- }
1017
- ),
1018
- /* @__PURE__ */ jsx(Toolbar.Spacer, {}),
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" }),
1021
- /* @__PURE__ */ jsx(
1022
- Toolbar.Button,
1023
- {
1024
- onClick: ctx.close,
1025
- disabled: ctx.isLastTrial,
1026
- title: ctx.isLastTrial ? "Cannot close the last trial" : "Close trial",
1027
- children: "Close"
1028
- }
1029
- )
1363
+ function TrialTitleBar({ title, children }) {
1364
+ const drag = useTrialDrag();
1365
+ if (drag)
1366
+ return /* @__PURE__ */ jsx(Draggable, { nodeId: drag.nodeId, title, children });
1367
+ return /* @__PURE__ */ jsxs("div", { className: "lk-trial__titlebar", children: [
1368
+ /* @__PURE__ */ jsx("span", { className: "lk-trial__title", children: title }),
1369
+ children
1030
1370
  ] });
1031
1371
  }
1372
+ var NO_OP = () => {
1373
+ };
1032
1374
  function TrialChrome({
1033
1375
  trialId,
1034
1376
  record,
1035
1377
  instrument,
1036
1378
  isLastTrial,
1037
- toolbar,
1038
- sidebar,
1039
- statusBar,
1040
1379
  undoBindings,
1041
- sidebarExtras,
1380
+ job,
1381
+ trialChrome,
1382
+ chrome,
1383
+ suppress,
1384
+ activeToolId = null,
1385
+ setActiveTool = NO_OP,
1042
1386
  children
1043
1387
  }) {
1044
1388
  const lab = useLabContext();
@@ -1047,40 +1391,25 @@ function TrialChrome({
1047
1391
  const updateTrialView = useStore(storeCtx.store, (s) => s.updateTrialView);
1048
1392
  const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
1049
1393
  const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
1050
- const toolbarCtx = useMemo(() => {
1394
+ const view2d = as2DView(record.view);
1395
+ const configSchema = useConfigSchema(instrument);
1396
+ const ctx = useMemo(() => {
1051
1397
  const setZoom = (z) => {
1052
- updateTrialView(trialId, { ...record.view, zoom: z });
1398
+ if (!view2d) return;
1399
+ updateTrialView(trialId, { ...view2d, zoom: z });
1053
1400
  };
1054
1401
  return {
1055
1402
  trialId,
1056
1403
  instrumentName: record.instrumentName,
1057
- hasUndo: instrument.undo != null,
1404
+ isLastTrial,
1405
+ zoom: view2d ? view2d.zoom : null,
1406
+ setZoom,
1058
1407
  canUndo: undoBindings?.canUndo ?? false,
1059
1408
  canRedo: undoBindings?.canRedo ?? false,
1060
- undo: undoBindings?.undo ?? (() => {
1061
- }),
1062
- redo: undoBindings?.redo ?? (() => {
1063
- }),
1064
- zoom: record.view.zoom,
1065
- setZoom,
1066
- zoomIn: () => setZoom(record.view.zoom * 1.25),
1067
- zoomOut: () => setZoom(record.view.zoom * 0.8),
1068
- resetZoom: () => setZoom(1),
1069
- hasCanvas: instrument.canvas != null,
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
1077
- };
1078
- }, [trialId, record, instrument, lab, isLastTrial, updateTrialView, undoBindings]);
1079
- const sidebarCtx = useMemo(
1080
- () => ({
1081
- trialId,
1082
- instrumentName: record.instrumentName,
1083
- configFields: instrument.configSchema?.() ?? [],
1409
+ undo: undoBindings?.undo ?? NO_OP,
1410
+ redo: undoBindings?.redo ?? NO_OP,
1411
+ configSchema,
1412
+ configFields: instrument.config ? [] : instrument.configSchema?.() ?? [],
1084
1413
  config: record.config,
1085
1414
  setConfig: (key, value) => {
1086
1415
  const prevConfig = record.config;
@@ -1095,25 +1424,54 @@ function TrialChrome({
1095
1424
  const nextState = instrument.onConfigChange(nextConfig, prevConfig, record.state);
1096
1425
  updateTrialState(trialId, nextState);
1097
1426
  }
1098
- }
1099
- }),
1100
- [trialId, record, instrument, updateTrialConfig, updateTrialState]
1101
- );
1102
- const statusCtx = {
1427
+ },
1428
+ savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
1429
+ saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
1430
+ loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
1431
+ clone: () => lab.cloneTrial(trialId),
1432
+ reset: () => lab.resetTrial(trialId),
1433
+ close: () => lab.closeTrial(trialId),
1434
+ activeToolId,
1435
+ setActiveTool
1436
+ };
1437
+ }, [
1103
1438
  trialId,
1104
- instrumentName: record.instrumentName,
1105
- zoom: record.view.zoom
1106
- };
1439
+ record,
1440
+ instrument,
1441
+ lab,
1442
+ isLastTrial,
1443
+ updateTrialView,
1444
+ updateTrialConfig,
1445
+ updateTrialState,
1446
+ undoBindings,
1447
+ view2d,
1448
+ activeToolId,
1449
+ setActiveTool,
1450
+ configSchema
1451
+ ]);
1452
+ const contributions = useMemo(
1453
+ () => suppressContributions(
1454
+ mergeContributions(
1455
+ builtinContributions(instrument, ctx, lab.controls),
1456
+ [...instrument.chrome ?? []],
1457
+ [...trialChrome ?? []],
1458
+ [...chrome ?? []]
1459
+ ),
1460
+ suppress ?? []
1461
+ ),
1462
+ [instrument, ctx, trialChrome, chrome, suppress, lab.controls]
1463
+ );
1464
+ const inRegion = (region) => contributions.filter((c) => c.region === region);
1107
1465
  const handleKeyDown = (e) => {
1108
1466
  const mod = e.metaKey || e.ctrlKey;
1109
1467
  if (!mod) return;
1110
1468
  if (e.key === "z" || e.key === "Z") {
1111
1469
  e.preventDefault();
1112
- if (e.shiftKey) toolbarCtx.redo();
1113
- else toolbarCtx.undo();
1470
+ if (e.shiftKey) ctx.redo();
1471
+ else ctx.undo();
1114
1472
  } else if (e.key === "s" || e.key === "S") {
1115
1473
  e.preventDefault();
1116
- toolbarCtx.saveSnapshot();
1474
+ ctx.saveSnapshot();
1117
1475
  }
1118
1476
  };
1119
1477
  return /* @__PURE__ */ jsxs(
@@ -1121,22 +1479,28 @@ function TrialChrome({
1121
1479
  {
1122
1480
  className: "lk-trial",
1123
1481
  "aria-label": `Trial ${record.instrumentName}`,
1482
+ tabIndex: -1,
1124
1483
  onKeyDown: handleKeyDown,
1125
1484
  children: [
1126
- /* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: toolbar ? toolbar(toolbarCtx) : /* @__PURE__ */ jsx(DefaultToolbar, { ctx: toolbarCtx }) }),
1485
+ /* @__PURE__ */ jsx(TrialTitleBar, { title: record.instrumentName, children: /* @__PURE__ */ jsx(TitleBarRegion, { contributions: inRegion("titlebar"), ctx }) }),
1486
+ /* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: /* @__PURE__ */ jsx(ToolbarRegion, { contributions: inRegion("toolbar"), ctx }) }),
1127
1487
  /* @__PURE__ */ jsxs("div", { className: "lk-trial__body", children: [
1128
- /* @__PURE__ */ jsxs("div", { className: "lk-trial__sidebar", children: [
1129
- sidebar ? sidebar(sidebarCtx) : /* @__PURE__ */ jsx(DefaultSidebar, { ctx: sidebarCtx }),
1130
- sidebarExtras
1131
- ] }),
1132
- /* @__PURE__ */ jsx("div", { className: "lk-trial__content", children })
1488
+ /* @__PURE__ */ jsx(PaletteRegion, { contributions: inRegion("palette"), ctx }),
1489
+ /* @__PURE__ */ jsx("div", { className: "lk-trial__sidebar", children: /* @__PURE__ */ jsx(SidebarRegion, { contributions: inRegion("sidebar"), ctx }) }),
1490
+ /* @__PURE__ */ jsxs("div", { className: `lk-trial__content${instrument.canvas ? " lk-trial__content--flush" : ""}`, children: [
1491
+ children,
1492
+ /* @__PURE__ */ jsx(ViewportRegion, { contributions: inRegion("viewport"), ctx })
1493
+ ] })
1133
1494
  ] }),
1134
- /* @__PURE__ */ jsx("div", { className: "lk-trial__status", children: statusBar ? statusBar(statusCtx) : /* @__PURE__ */ jsx(DefaultStatusBar, { ctx: statusCtx }) })
1495
+ /* @__PURE__ */ jsxs("div", { className: "lk-trial__status", children: [
1496
+ job ? /* @__PURE__ */ jsx(JobProgress, { job }) : null,
1497
+ /* @__PURE__ */ jsx(StatusRegion, { contributions: inRegion("status"), ctx })
1498
+ ] })
1135
1499
  ]
1136
1500
  }
1137
1501
  );
1138
1502
  }
1139
- function Trial({ id }) {
1503
+ function Trial({ id, chrome, suppress }) {
1140
1504
  const lab = useLabContext();
1141
1505
  const storeCtx = useContext(LabStoreContext);
1142
1506
  if (!storeCtx) throw new Error("[labkit] <Trial> requires <LabStoreProvider>");
@@ -1160,16 +1524,21 @@ function Trial({ id }) {
1160
1524
  record,
1161
1525
  instrument,
1162
1526
  store: storeCtx.store,
1163
- isLast: lab.trials.length <= 1
1527
+ isLast: lab.trials.length <= 1,
1528
+ chrome,
1529
+ suppress
1164
1530
  }
1165
1531
  );
1166
1532
  }
1167
- function TrialRuntime({ record, instrument, store, isLast }) {
1533
+ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1168
1534
  const canvasContainerRef = useRef(null);
1169
1535
  const updateTrialState = useStore(store, (s) => s.updateTrialState);
1170
1536
  const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
1171
1537
  const updateTrialView = useStore(store, (s) => s.updateTrialView);
1172
1538
  const updateTrialUndoStack = useStore(store, (s) => s.updateTrialUndoStack);
1539
+ const labToolId = useStore(store, (s) => s.activeToolId);
1540
+ const setLabTool = useStore(store, (s) => s.setLabTool);
1541
+ const setTrialTool = useStore(store, (s) => s.setTrialTool);
1173
1542
  const busRef = useRef(null);
1174
1543
  if (busRef.current === null) busRef.current = createEventBus();
1175
1544
  const bus = busRef.current;
@@ -1186,6 +1555,21 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1186
1555
  updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
1187
1556
  };
1188
1557
  const setView = (v) => updateTrialView(record.id, v);
1558
+ const declaresTools = instrument.tools != null;
1559
+ const resolvedToolId = declaresTools ? record.activeToolId ?? instrument.tools?.initial ?? instrument.tools?.tools[0]?.id ?? null : labToolId;
1560
+ const setActiveTool = (id) => {
1561
+ if (declaresTools) setTrialTool(record.id, id);
1562
+ else setLabTool(id);
1563
+ };
1564
+ const view2d = as2DView(record.view);
1565
+ const jobCap = instrument.job;
1566
+ const job = useJob({
1567
+ capability: jobCap ?? { run: async function* () {
1568
+ }, onItem: (_i, st) => st },
1569
+ config: record.config,
1570
+ state: record.state,
1571
+ setState: (next) => updateTrialState(record.id, next)
1572
+ });
1189
1573
  const renderCtx = {
1190
1574
  state: record.state,
1191
1575
  config: record.config,
@@ -1204,13 +1588,20 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1204
1588
  },
1205
1589
  trial: {
1206
1590
  id: record.id,
1207
- zoom: record.view.zoom,
1208
- setZoom: (z) => updateTrialView(record.id, { ...record.view, zoom: z })
1591
+ view: record.view,
1592
+ setView,
1593
+ zoom: view2d?.zoom ?? 1,
1594
+ setZoom: (z) => {
1595
+ if (!view2d) return;
1596
+ updateTrialView(record.id, { ...view2d, zoom: z });
1597
+ },
1598
+ activeToolId: resolvedToolId
1209
1599
  },
1210
1600
  emit: (event) => {
1211
1601
  snapshotIfNeeded(event);
1212
1602
  bus.emit(event);
1213
- }
1603
+ },
1604
+ job: jobCap ? job : void 0
1214
1605
  };
1215
1606
  const undoBindings = undoCap ? {
1216
1607
  canUndo: record.undoStack.past.length > 0,
@@ -1244,12 +1635,12 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1244
1635
  }, [instrument.canvas, record.state, record.config, layerVisibility, layerOrder]);
1245
1636
  const layerDescriptors = useMemo(() => {
1246
1637
  if (!instrument.layers) return [];
1247
- return instrument.layers.ids.map((lid) => ({ id: lid, label: lid }));
1638
+ return instrument.layers.ids.map((l) => typeof l === "string" ? { id: l, label: l } : l);
1248
1639
  }, [instrument.layers]);
1249
1640
  const dragDropResult = useDragDrop({
1250
1641
  capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
1251
1642
  canvasContainerRef,
1252
- view: record.view,
1643
+ view: view2d ?? DEFAULT_VIEW,
1253
1644
  state: record.state,
1254
1645
  config: record.config,
1255
1646
  setState: (next) => {
@@ -1263,8 +1654,8 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1263
1654
  });
1264
1655
  const paletteItems = useMemo(() => {
1265
1656
  if (!instrument.dragDrop) return [];
1266
- const p2 = instrument.dragDrop.palette;
1267
- return typeof p2 === "function" ? p2(record.state, record.config) : p2;
1657
+ const p3 = instrument.dragDrop.palette;
1658
+ return typeof p3 === "function" ? p3(record.state, record.config) : p3;
1268
1659
  }, [instrument.dragDrop, record.state, record.config]);
1269
1660
  const layersWithFeedback = useMemo(() => {
1270
1661
  if (!dragDropResult.drag?.feedback) return canvasLayers;
@@ -1278,12 +1669,12 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1278
1669
  render: (ctx) => {
1279
1670
  const el = canvasContainerRef.current;
1280
1671
  if (!el) return;
1281
- const r = el.getBoundingClientRect();
1672
+ const r2 = el.getBoundingClientRect();
1282
1673
  ctx.save();
1283
1674
  ctx.strokeStyle = fb.ok ? "#3a7" : "#c44";
1284
1675
  ctx.lineWidth = 2;
1285
1676
  ctx.beginPath();
1286
- ctx.arc(screen.x - r.left, screen.y - r.top, 16, 0, Math.PI * 2);
1677
+ ctx.arc(screen.x - r2.left, screen.y - r2.top, 16, 0, Math.PI * 2);
1287
1678
  ctx.stroke();
1288
1679
  ctx.restore();
1289
1680
  }
@@ -1292,56 +1683,85 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1292
1683
  }, [canvasLayers, dragDropResult.drag]);
1293
1684
  let body;
1294
1685
  if (instrument.canvas) {
1295
- body = /* @__PURE__ */ jsxs(
1296
- "div",
1297
- {
1298
- ref: canvasContainerRef,
1299
- className: "lk-trial__canvas-host",
1300
- style: { width: "100%", height: "100%", position: "relative" },
1301
- children: [
1302
- /* @__PURE__ */ jsx(CanvasStack, { layers: layersWithFeedback, view: record.view, onViewChange: setView, children: instrument.render(renderCtx) }),
1303
- /* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
1304
- ]
1305
- }
1306
- );
1686
+ body = /* @__PURE__ */ jsxs("div", { ref: canvasContainerRef, className: "lk-trial__canvas-host", children: [
1687
+ /* @__PURE__ */ jsx(
1688
+ CanvasStack,
1689
+ {
1690
+ layers: layersWithFeedback,
1691
+ view: view2d ?? DEFAULT_VIEW,
1692
+ onViewChange: setView,
1693
+ minZoom: instrument.canvas.minZoom,
1694
+ maxZoom: instrument.canvas.maxZoom,
1695
+ children: instrument.render(renderCtx)
1696
+ }
1697
+ ),
1698
+ /* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
1699
+ ] });
1307
1700
  } else {
1308
1701
  body = instrument.render(renderCtx);
1309
1702
  }
1310
- const paletteNode = paletteItems.length > 0 ? /* @__PURE__ */ jsx(Palette, { items: paletteItems, onDragStart: dragDropResult.startDrag }) : null;
1311
- const layerListNode = instrument.layers && layerDescriptors.length > 0 ? /* @__PURE__ */ jsx(
1312
- LayerList,
1313
- {
1314
- layers: layerDescriptors,
1315
- visibility: layerVisibility,
1316
- onReorder: (next) => {
1317
- setLayerOrder(next.map((l) => l.id));
1318
- bus.emit("layers.reorder");
1319
- },
1320
- onToggle: (lid, visible) => {
1321
- setLayerVisibility((prev) => ({ ...prev, [lid]: visible }));
1322
- bus.emit("layers.toggle");
1703
+ const startDrag = dragDropResult.startDrag;
1704
+ const paletteNode = useMemo(
1705
+ () => paletteItems.length > 0 ? /* @__PURE__ */ jsx(Palette, { items: paletteItems, onDragStart: startDrag }) : null,
1706
+ [paletteItems, startDrag]
1707
+ );
1708
+ const layerListNode = useMemo(
1709
+ () => instrument.layers && layerDescriptors.length > 0 ? /* @__PURE__ */ jsx(
1710
+ LayerList,
1711
+ {
1712
+ layers: layerDescriptors,
1713
+ visibility: layerVisibility,
1714
+ onReorder: (next) => {
1715
+ setLayerOrder(next.map((l) => l.id));
1716
+ bus.emit("layers.reorder");
1717
+ },
1718
+ onToggle: (lid, visible) => {
1719
+ setLayerVisibility((prev) => ({ ...prev, [lid]: visible }));
1720
+ bus.emit("layers.toggle");
1721
+ }
1323
1722
  }
1723
+ ) : null,
1724
+ [instrument.layers, layerDescriptors, layerVisibility, bus]
1725
+ );
1726
+ const extraChrome = useMemo(() => {
1727
+ const out = [];
1728
+ if (paletteNode) {
1729
+ out.push({
1730
+ id: "dragdrop-palette",
1731
+ region: "sidebar",
1732
+ item: { title: "Parts", body: paletteNode }
1733
+ });
1324
1734
  }
1325
- ) : null;
1735
+ if (layerListNode) {
1736
+ out.push({
1737
+ id: "layer-list",
1738
+ region: "sidebar",
1739
+ item: { title: "Layers", body: layerListNode }
1740
+ });
1741
+ }
1742
+ return out;
1743
+ }, [paletteNode, layerListNode]);
1326
1744
  return /* @__PURE__ */ jsx(
1327
1745
  TrialChrome,
1328
1746
  {
1747
+ job: jobCap ? job : void 0,
1329
1748
  trialId: record.id,
1330
1749
  record,
1331
1750
  instrument,
1332
1751
  isLastTrial: isLast,
1333
1752
  undoBindings,
1334
- sidebarExtras: /* @__PURE__ */ jsxs(Fragment, { children: [
1335
- paletteNode,
1336
- layerListNode
1337
- ] }),
1753
+ trialChrome: extraChrome,
1754
+ chrome,
1755
+ suppress,
1756
+ activeToolId: resolvedToolId,
1757
+ setActiveTool,
1338
1758
  children: body
1339
1759
  }
1340
1760
  );
1341
1761
  }
1342
1762
 
1343
1763
  // src/trial/trialOps.ts
1344
- var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
1764
+ var DEFAULT_VIEW2 = { zoom: 1, pan: { x: 0, y: 0 } };
1345
1765
  function findInstrument(instruments, name) {
1346
1766
  const found = instruments.find((i) => i.name === name);
1347
1767
  if (!found) {
@@ -1350,7 +1770,7 @@ function findInstrument(instruments, name) {
1350
1770
  return found;
1351
1771
  }
1352
1772
  function initialView(instrument) {
1353
- return instrument.canvas?.initialView ? structuredClone(instrument.canvas.initialView) : { ...DEFAULT_VIEW, pan: { ...DEFAULT_VIEW.pan } };
1773
+ return instrument.canvas?.initialView ? structuredClone(instrument.canvas.initialView) : { ...DEFAULT_VIEW2, pan: { ...DEFAULT_VIEW2.pan } };
1354
1774
  }
1355
1775
  function addTrial(trials, instruments, instrumentName) {
1356
1776
  const instrument = findInstrument(instruments, instrumentName);
@@ -1407,6 +1827,74 @@ function reorderTrials(trials, ids) {
1407
1827
  const rest = trials.filter((w) => !seen.has(w.id));
1408
1828
  return [...named, ...rest];
1409
1829
  }
1830
+ var MODES = [
1831
+ { value: "auto", label: "Auto" },
1832
+ { value: "light", label: "Light" },
1833
+ { value: "dark", label: "Dark" }
1834
+ ];
1835
+ function LabHeader() {
1836
+ const lab = useLabContext();
1837
+ const only = lab.instruments.length === 1 ? lab.instruments[0] : null;
1838
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1839
+ only ? /* @__PURE__ */ jsxs(
1840
+ "button",
1841
+ {
1842
+ type: "button",
1843
+ className: "lk-lab-header__add",
1844
+ onClick: () => lab.addTrial(only.name),
1845
+ children: [
1846
+ /* @__PURE__ */ jsx(vt, { size: 16 }),
1847
+ /* @__PURE__ */ jsx("span", { children: "Add trial" })
1848
+ ]
1849
+ }
1850
+ ) : (
1851
+ // Held at null so the control stays an "add one" action rather than
1852
+ // reading as the current instrument.
1853
+ /* @__PURE__ */ jsx(
1854
+ p2,
1855
+ {
1856
+ className: "lk-lab-header__add-select",
1857
+ "aria-label": "Add trial",
1858
+ placeholder: "Add trial\u2026",
1859
+ selectedKey: null,
1860
+ options: lab.instruments.map((i) => ({ value: i.name, label: i.name })),
1861
+ onSelectionChange: (name) => {
1862
+ if (name != null) lab.addTrial(String(name));
1863
+ }
1864
+ }
1865
+ )
1866
+ ),
1867
+ /* @__PURE__ */ jsx(
1868
+ r,
1869
+ {
1870
+ className: "lk-lab-header__mode",
1871
+ ariaLabel: "Color mode",
1872
+ items: MODES,
1873
+ value: lab.mode,
1874
+ onChange: (next) => {
1875
+ if (next) lab.setMode(next);
1876
+ }
1877
+ }
1878
+ )
1879
+ ] });
1880
+ }
1881
+ function LabPalette({ tools }) {
1882
+ const storeCtx = useContext(LabStoreContext);
1883
+ if (!storeCtx) throw new Error("[labkit] LabPalette requires <LabStoreProvider>");
1884
+ const activeToolId = useStore(storeCtx.store, (s) => s.activeToolId);
1885
+ const setLabTool = useStore(storeCtx.store, (s) => s.setLabTool);
1886
+ const contributions = tools.map((t) => ({
1887
+ id: t.id,
1888
+ region: "palette",
1889
+ group: t.group,
1890
+ item: { icon: t.icon, label: t.label, shortcut: t.shortcut }
1891
+ }));
1892
+ const ctx = {
1893
+ activeToolId,
1894
+ setActiveTool: setLabTool
1895
+ };
1896
+ return /* @__PURE__ */ jsx(PaletteRegion, { contributions, ctx });
1897
+ }
1410
1898
  function query() {
1411
1899
  if (typeof window === "undefined" || !window.matchMedia) return null;
1412
1900
  return window.matchMedia("(prefers-color-scheme: light)");
@@ -1442,11 +1930,11 @@ var ZONE_ID = asNodeId("lk-workspace");
1442
1930
  var STRATEGIES = { grid: gridStrategy };
1443
1931
  var KIND = "trial";
1444
1932
  function extentOf(store, id) {
1445
- const p2 = store.getNode(id)?.membership?.placement;
1446
- if (!p2) return null;
1933
+ const p3 = store.getNode(id)?.membership?.placement;
1934
+ if (!p3) return null;
1447
1935
  const out = {};
1448
- if (p2.size) out.size = p2.size;
1449
- if (p2.span) out.span = p2.span;
1936
+ if (p3.size) out.size = p3.size;
1937
+ if (p3.span) out.span = p3.span;
1450
1938
  return Object.keys(out).length > 0 ? out : null;
1451
1939
  }
1452
1940
  function Workspace({
@@ -1481,6 +1969,7 @@ function Workspace({
1481
1969
  storeRef.current = store2;
1482
1970
  }
1483
1971
  const store = storeRef.current;
1972
+ const surface = useSurfaceOptional();
1484
1973
  useLayoutEffect(() => {
1485
1974
  store.updateContainerConfig(ZONE_ID, { resizable, gap, padding });
1486
1975
  }, [store, resizable, gap, padding]);
@@ -1499,6 +1988,11 @@ function Workspace({
1499
1988
  }
1500
1989
  store.setChildOrder(ZONE_ID, [...nodeIds]);
1501
1990
  }, [store, nodeIds]);
1991
+ useEffect(() => {
1992
+ if (!surface) return;
1993
+ surface.invalidateRects();
1994
+ return store.events.on("node.placementChanged", () => surface.invalidateRects());
1995
+ }, [store, surface, nodeIds]);
1502
1996
  useEffect(() => {
1503
1997
  if (!onLayoutChange) return;
1504
1998
  return store.events.on("node.placementChanged", () => {
@@ -1520,10 +2014,10 @@ function Workspace({
1520
2014
  [KIND]: ({ node }) => {
1521
2015
  const content = byId.get(node.id) ?? null;
1522
2016
  if (!reorderable) return 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 })
1526
- ] });
2017
+ return (
2018
+ // No grip strip: the trial's own title bar is the drag surface.
2019
+ /* @__PURE__ */ jsx(TrialDragContext.Provider, { value: { nodeId: node.id }, children: /* @__PURE__ */ jsx("div", { className: "lk-trial-tile", children: content }) })
2020
+ );
1527
2021
  }
1528
2022
  };
1529
2023
  }, [nodeIds, items, reorderable]);
@@ -1561,8 +2055,8 @@ var NEBULA_SLOTS = [
1561
2055
  ];
1562
2056
  function buildNebula(colors) {
1563
2057
  const blobs = colors.map((c, i) => {
1564
- const p2 = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
1565
- return `radial-gradient(ellipse ${p2.sx} ${p2.sy} at ${p2.cx} ${p2.cy}, color-mix(in srgb, ${c} 22%, transparent), transparent ${p2.stop})`;
2058
+ const p3 = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
2059
+ return `radial-gradient(ellipse ${p3.sx} ${p3.sy} at ${p3.cx} ${p3.cy}, color-mix(in srgb, ${c} 22%, transparent), transparent ${p3.stop})`;
1566
2060
  });
1567
2061
  blobs.push("radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%)");
1568
2062
  return blobs.join(", ");
@@ -1575,6 +2069,12 @@ function Lab({
1575
2069
  mode,
1576
2070
  nebula,
1577
2071
  title,
2072
+ footer,
2073
+ chrome,
2074
+ suppress,
2075
+ tools,
2076
+ configRules,
2077
+ controls,
1578
2078
  children
1579
2079
  }) {
1580
2080
  if (process.env.NODE_ENV !== "production" && instruments.length === 0) {
@@ -1639,7 +2139,8 @@ function Lab({
1639
2139
  },
1640
2140
  savedSnapshots,
1641
2141
  saveSnapshot: (trialId, name) => {
1642
- store.getState().saveSnapshot(trialId, name ?? `Save ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
2142
+ const taken = store.getState().savedSnapshots.filter((s) => s.trialId === trialId).length;
2143
+ store.getState().saveSnapshot(trialId, name ?? `Snapshot ${taken + 1}`);
1643
2144
  },
1644
2145
  loadSnapshot: (trialId, snapshotId) => {
1645
2146
  store.getState().loadSnapshot(snapshotId, trialId);
@@ -1650,9 +2151,11 @@ function Lab({
1650
2151
  mode: modeValue,
1651
2152
  setMode: (m) => {
1652
2153
  store.getState().setMode(m);
1653
- }
2154
+ },
2155
+ configRules,
2156
+ controls
1654
2157
  };
1655
- }, [instruments, trials, savedSnapshots, modeValue, store]);
2158
+ }, [instruments, trials, savedSnapshots, modeValue, store, configRules, controls]);
1656
2159
  const backdropStyle = resolvedMode === "dark" && nebula && nebula.length > 0 ? { ["--wzl-backdrop"]: buildNebula(nebula) } : void 0;
1657
2160
  return /* @__PURE__ */ jsx(LabStoreContext.Provider, { value: { store }, children: /* @__PURE__ */ jsx(LabContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
1658
2161
  ThemeProvider,
@@ -1661,18 +2164,33 @@ function Lab({
1661
2164
  mode: resolvedMode,
1662
2165
  className: "lk-lab",
1663
2166
  style: backdropStyle,
1664
- children: /* @__PURE__ */ jsx(LabShell, { title: title ?? "Labkit", mode: modeValue, header: children, children: /* @__PURE__ */ jsx(
1665
- Workspace,
2167
+ children: /* @__PURE__ */ jsx(
2168
+ LabShell,
1666
2169
  {
1667
- ids: trials.map((w) => w.id),
1668
- resizable: true,
1669
- reorderable: true,
1670
- onReorder: (ids) => contextValue.reorderTrials(ids),
1671
- layout,
1672
- onLayoutChange: (next) => store.getState().setLayout(next),
1673
- children: trials.map((w) => /* @__PURE__ */ jsx(Trial, { id: w.id }, w.id))
2170
+ title: title ?? "Labkit",
2171
+ mode: modeValue,
2172
+ footer,
2173
+ header: /* @__PURE__ */ jsxs(Fragment, { children: [
2174
+ /* @__PURE__ */ jsx(LabHeader, {}),
2175
+ children
2176
+ ] }),
2177
+ children: /* @__PURE__ */ jsxs("div", { className: "lk-lab__body", children: [
2178
+ tools ? /* @__PURE__ */ jsx(LabPalette, { tools }) : null,
2179
+ /* @__PURE__ */ jsx(
2180
+ Workspace,
2181
+ {
2182
+ ids: trials.map((w) => w.id),
2183
+ resizable: true,
2184
+ reorderable: true,
2185
+ onReorder: (ids) => contextValue.reorderTrials(ids),
2186
+ layout,
2187
+ onLayoutChange: (next) => store.getState().setLayout(next),
2188
+ children: trials.map((w) => /* @__PURE__ */ jsx(Trial, { id: w.id, chrome, suppress }, w.id))
2189
+ }
2190
+ )
2191
+ ] })
1674
2192
  }
1675
- ) })
2193
+ )
1676
2194
  }
1677
2195
  ) }) });
1678
2196
  }
@@ -1717,7 +2235,7 @@ function CurveField({
1717
2235
  }
1718
2236
  flipped.sort((a, b) => a.x - b.x);
1719
2237
  const out = [];
1720
- for (const p2 of flipped) out.push(p2.x, p2.y);
2238
+ for (const p3 of flipped) out.push(p3.x, p3.y);
1721
2239
  onChange(out);
1722
2240
  }, [values, onChange]);
1723
2241
  const handleFlipVertical = useCallback(() => {
@@ -2021,268 +2539,7 @@ function EffectCardList({
2021
2539
  ] }, String(item.id));
2022
2540
  }) });
2023
2541
  }
2024
- function PropertyGroup({
2025
- title,
2026
- hidden,
2027
- children,
2028
- className,
2029
- pack = "auto-color"
2030
- }) {
2031
- if (hidden) return null;
2032
- const packClass = pack === "pairs" ? " lk-property-group--pairs" : "";
2033
- const cls = `lk-property-group${packClass}${className ? ` ${className}` : ""}`;
2034
- return /* @__PURE__ */ jsxs("div", { className: cls, children: [
2035
- /* @__PURE__ */ jsxs("h3", { className: "lk-property-group__title", children: [
2036
- /* @__PURE__ */ jsx("hr", {}),
2037
- /* @__PURE__ */ jsx("span", { children: title }),
2038
- /* @__PURE__ */ jsx("hr", {})
2039
- ] }),
2040
- /* @__PURE__ */ jsx("div", { className: "lk-property-group__body", children })
2041
- ] });
2042
- }
2043
- function PropertyPanel({ title, children, className }) {
2044
- const cls = className ? `lk-property-panel ${className}` : "lk-property-panel";
2045
- return /* @__PURE__ */ jsxs("div", { className: cls, children: [
2046
- title != null && /* @__PURE__ */ jsx("h2", { className: "lk-property-panel__title", children: title }),
2047
- children
2048
- ] });
2049
- }
2050
- function PropertyList({ children, className, pack = "auto-color" }) {
2051
- const packClass = pack === "pairs" ? " lk-property-list--pairs" : "";
2052
- const cls = `lk-property-list${packClass}${className ? ` ${className}` : ""}`;
2053
- return /* @__PURE__ */ jsx("div", { className: cls, children });
2054
- }
2055
- function PropertyRow({
2056
- label,
2057
- readout,
2058
- variant = "default",
2059
- layout = "block",
2060
- children,
2061
- htmlFor,
2062
- className
2063
- }) {
2064
- const variantClass = variant === "default" ? "" : ` lk-property-row--${variant}`;
2065
- const layoutClass = variant === "default" && layout === "inline" ? " lk-property-row--inline" : "";
2066
- const cls = `lk-property-row${variantClass}${layoutClass}${className ? ` ${className}` : ""}`;
2067
- return /* @__PURE__ */ jsxs("label", { className: cls, htmlFor, children: [
2068
- /* @__PURE__ */ jsxs("span", { className: "lk-property-row__label", children: [
2069
- label,
2070
- readout != null && /* @__PURE__ */ jsx("em", { className: "lk-property-row__readout", children: readout })
2071
- ] }),
2072
- children
2073
- ] });
2074
- }
2075
- function SliderRow({
2076
- label,
2077
- value,
2078
- min,
2079
- max,
2080
- step = 1,
2081
- onChange,
2082
- format,
2083
- unit,
2084
- layout
2085
- }) {
2086
- const decimals = step >= 1 ? 0 : Math.min(6, Math.max(0, Math.ceil(-Math.log10(step))));
2087
- const effectiveFormat = format ?? ((n) => formatNumber(n, decimals));
2088
- return /* @__PURE__ */ jsx(
2089
- PropertyRow,
2090
- {
2091
- label,
2092
- readout: /* @__PURE__ */ jsx(
2093
- EditableReadout,
2094
- {
2095
- value,
2096
- min,
2097
- max,
2098
- format: effectiveFormat,
2099
- unit,
2100
- onCommit: onChange
2101
- }
2102
- ),
2103
- layout,
2104
- children: /* @__PURE__ */ jsx(
2105
- "input",
2106
- {
2107
- type: "range",
2108
- tabIndex: -1,
2109
- min,
2110
- max,
2111
- step,
2112
- value,
2113
- onChange: (e) => {
2114
- const v = Number(e.target.value);
2115
- p("property-panel", "slider", { label, value: v });
2116
- onChange(v);
2117
- }
2118
- }
2119
- )
2120
- }
2121
- );
2122
- }
2123
- function EditableReadout({ value, min, max, format, unit, onCommit }) {
2124
- const [draft, setDraft] = useState(null);
2125
- const fmt = (n) => format ? format(n) : formatNumber(n);
2126
- const displayValue = draft !== null ? draft : (() => {
2127
- const formatted = fmt(value);
2128
- return typeof formatted === "string" ? formatted : String(formatted);
2129
- })();
2130
- const suffix = unit == null ? null : typeof unit === "string" ? /* @__PURE__ */ jsx("span", { className: "lk-property-row__readout-unit", children: unit }) : unit;
2131
- const commit = () => {
2132
- if (draft !== null) {
2133
- const n = parseSignedNumber(draft);
2134
- if (Number.isFinite(n)) onCommit(Math.min(max, Math.max(min, n)));
2135
- }
2136
- setDraft(null);
2137
- };
2138
- return /* @__PURE__ */ jsxs("span", { className: "lk-property-row__readout-group", children: [
2139
- /* @__PURE__ */ jsx(
2140
- "input",
2141
- {
2142
- type: "text",
2143
- inputMode: "decimal",
2144
- className: "lk-property-row__readout-input",
2145
- value: displayValue,
2146
- onFocus: (e) => {
2147
- const formatted = fmt(value);
2148
- setDraft(typeof formatted === "string" ? formatted : String(formatted));
2149
- e.currentTarget.select();
2150
- },
2151
- onChange: (e) => setDraft(e.target.value.replace(/-/g, "\u2212")),
2152
- onBlur: commit,
2153
- onClick: (e) => {
2154
- e.preventDefault();
2155
- e.stopPropagation();
2156
- e.currentTarget.focus();
2157
- },
2158
- onMouseDown: (e) => e.stopPropagation(),
2159
- onKeyDown: (e) => {
2160
- if (e.key === "Enter") {
2161
- commit();
2162
- e.currentTarget.blur();
2163
- } else if (e.key === "Escape") {
2164
- setDraft(null);
2165
- e.currentTarget.blur();
2166
- }
2167
- }
2168
- }
2169
- ),
2170
- suffix
2171
- ] });
2172
- }
2173
- function ColorRow({
2174
- label,
2175
- value,
2176
- onChange,
2177
- alpha,
2178
- onAlphaChange,
2179
- alphaDisabled
2180
- }) {
2181
- const showAlpha = alpha != null;
2182
- const className = alphaDisabled ? "lk-property-row--alpha-disabled" : void 0;
2183
- return /* @__PURE__ */ jsxs(PropertyRow, { label, variant: "color", className, children: [
2184
- /* @__PURE__ */ jsx("input", { type: "color", value, onChange: (e) => onChange(e.target.value) }),
2185
- showAlpha && /* @__PURE__ */ jsx(
2186
- "input",
2187
- {
2188
- type: "range",
2189
- className: "lk-property-row__alpha",
2190
- min: 0,
2191
- max: 1,
2192
- step: 0.01,
2193
- value: alpha,
2194
- disabled: alphaDisabled,
2195
- onChange: (e) => onAlphaChange?.(Number(e.target.value))
2196
- }
2197
- )
2198
- ] });
2199
- }
2200
- function CheckboxRow({ label, value, onChange }) {
2201
- return /* @__PURE__ */ jsx(PropertyRow, { label, variant: "checkbox", children: /* @__PURE__ */ jsx("input", { type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked) }) });
2202
- }
2203
- function TextRow({ label, value, onChange, placeholder, maxLength, layout }) {
2204
- return /* @__PURE__ */ jsx(PropertyRow, { label, layout, children: /* @__PURE__ */ jsx(
2205
- "input",
2206
- {
2207
- type: "text",
2208
- value,
2209
- placeholder,
2210
- maxLength,
2211
- onChange: (e) => onChange(e.target.value)
2212
- }
2213
- ) });
2214
- }
2215
- function NumberRow({
2216
- label,
2217
- value,
2218
- onChange,
2219
- min,
2220
- max,
2221
- step,
2222
- placeholder,
2223
- layout
2224
- }) {
2225
- return /* @__PURE__ */ jsx(PropertyRow, { label, layout, children: /* @__PURE__ */ jsx(
2226
- "input",
2227
- {
2228
- type: "number",
2229
- value,
2230
- min,
2231
- max,
2232
- step,
2233
- placeholder,
2234
- onChange: (e) => {
2235
- const raw = e.target.value;
2236
- if (raw === "") return;
2237
- const n = Number(raw);
2238
- if (Number.isFinite(n)) onChange(n);
2239
- }
2240
- }
2241
- ) });
2242
- }
2243
- function SelectRow({
2244
- label,
2245
- value,
2246
- options,
2247
- onChange,
2248
- layout
2249
- }) {
2250
- return /* @__PURE__ */ jsx(PropertyRow, { label, layout, children: /* @__PURE__ */ jsx(
2251
- "select",
2252
- {
2253
- value,
2254
- onChange: (e) => {
2255
- const v = e.target.value;
2256
- p("property-panel", "select", { label, value: v });
2257
- onChange(v);
2258
- },
2259
- children: options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
2260
- }
2261
- ) });
2262
- }
2263
- function ToggleRow({
2264
- label,
2265
- value,
2266
- options,
2267
- onChange,
2268
- layout
2269
- }) {
2270
- return /* @__PURE__ */ jsx(PropertyRow, { label, layout, children: /* @__PURE__ */ jsx("div", { className: "lk-property-row__toggle", children: options.map((opt) => {
2271
- const selected = opt.value === value;
2272
- return /* @__PURE__ */ jsx(
2273
- "button",
2274
- {
2275
- type: "button",
2276
- "aria-pressed": selected,
2277
- className: selected ? "lk-property-row__toggle-button lk-property-row__toggle-button--selected" : "lk-property-row__toggle-button",
2278
- onClick: () => onChange(opt.value),
2279
- children: opt.label
2280
- },
2281
- opt.value
2282
- );
2283
- }) }) });
2284
- }
2285
2542
 
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 };
2543
+ export { CurveField, DEFAULT_VIEW, EffectCard, EffectCardList, Lab, LabContext, LabPalette, LabShell, PITCH_LIMIT, Subpanel, Trial, TrialChrome, Workspace, addTrial, applyRules, as2DView, builtinRules, clampPitch, cloneTrial, closeTrial, defineInstrument, f, interstellarTheme, orbitAfterDrag, orbitAfterWheel, reorderTrials, resetTrial, resolveConfigSchema, titleCase, useConfigSchema, useLabContext, useOrbit, validateConfigSchema, wrapYaw };
2287
2544
  //# sourceMappingURL=index.js.map
2288
2545
  //# sourceMappingURL=index.js.map