@weasel-js/labkit 1.2.0 → 1.3.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 (227) hide show
  1. package/dist/_dts/{DrawCommand-C_XboUpz.d.ts → DrawCommand-DBB45NfN.d.ts} +196 -52
  2. package/dist/_dts/PrefsForm-BYa6cWnO.d.ts +201 -0
  3. package/dist/_dts/types-1Sdxy_Pv.d.ts +90 -0
  4. package/dist/_dts/types-BttTIent.d.ts +314 -0
  5. package/dist/_dts/types-DCGEcMO_.d.ts +162 -0
  6. package/dist/_dts/{useTrialState-BMNIx3Cy.d.ts → useTrialState-CsGMjhu9.d.ts} +5 -87
  7. package/dist/canvas/index.d.ts +7 -3
  8. package/dist/canvas/index.js +3 -1
  9. package/dist/chrome/index.d.ts +86 -0
  10. package/dist/chrome/index.js +9 -0
  11. package/dist/chrome/index.js.map +1 -0
  12. package/dist/chunk-3XXPU73K.js +138 -0
  13. package/dist/chunk-3XXPU73K.js.map +1 -0
  14. package/dist/{chunk-VUU5UXHE.js → chunk-4TMMVIDM.js} +8 -5
  15. package/dist/chunk-4TMMVIDM.js.map +1 -0
  16. package/dist/chunk-6X5RBC6G.js +366 -0
  17. package/dist/chunk-6X5RBC6G.js.map +1 -0
  18. package/dist/{chunk-574LJAV4.js → chunk-BDJXIBRZ.js} +33 -35
  19. package/dist/chunk-BDJXIBRZ.js.map +1 -0
  20. package/dist/{chunk-SFL7NFKN.js → chunk-BMW4TDP5.js} +15 -7
  21. package/dist/chunk-BMW4TDP5.js.map +1 -0
  22. package/dist/chunk-JDROYQM3.js +266 -0
  23. package/dist/chunk-JDROYQM3.js.map +1 -0
  24. package/dist/{chunk-THBG7FQZ.js → chunk-LGKRIUSW.js} +15 -12
  25. package/dist/chunk-LGKRIUSW.js.map +1 -0
  26. package/dist/{chunk-BOHF3PQO.js → chunk-QCICIHEO.js} +8129 -5196
  27. package/dist/chunk-QCICIHEO.js.map +1 -0
  28. package/dist/chunk-QRYPXSGP.js +212 -0
  29. package/dist/chunk-QRYPXSGP.js.map +1 -0
  30. package/dist/chunk-RGT4EJIB.js +6961 -0
  31. package/dist/chunk-RGT4EJIB.js.map +1 -0
  32. package/dist/chunk-SBXNQW4G.js +3 -0
  33. package/dist/chunk-SBXNQW4G.js.map +1 -0
  34. package/dist/chunk-T66HU3HX.js +17 -0
  35. package/dist/chunk-T66HU3HX.js.map +1 -0
  36. package/dist/chunk-WTI26YTM.js +266 -0
  37. package/dist/chunk-WTI26YTM.js.map +1 -0
  38. package/dist/controls/index.d.ts +19 -5
  39. package/dist/controls/index.js +3 -1
  40. package/dist/dragdrop/index.d.ts +46 -3
  41. package/dist/dragdrop/index.js +3 -1
  42. package/dist/index.d.ts +735 -565
  43. package/dist/index.js +661 -1019
  44. package/dist/index.js.map +1 -1
  45. package/dist/layers/index.d.ts +5 -3
  46. package/dist/layers/index.js +4 -1
  47. package/dist/passthrough/weasel-canvas.d.ts +69 -13
  48. package/dist/passthrough/weasel-canvas.js +1 -1
  49. package/dist/passthrough/weasel-ui.d.ts +3811 -3671
  50. package/dist/passthrough/weasel-ui.js +3 -2
  51. package/dist/primitives/index.d.ts +102 -8
  52. package/dist/primitives/index.js +5 -1
  53. package/dist/state/index.d.ts +3 -2
  54. package/dist/state/index.js +2 -2
  55. package/dist/styles.css +490 -803
  56. package/dist/surface/index.js +2 -1
  57. package/dist/ui/layers/index.d.ts +8 -2
  58. package/dist/ui/layers/index.js +2 -3
  59. package/dist/undo/index.d.ts +4 -2
  60. package/package.json +16 -7
  61. package/src/canvas/AGENTS.md +4 -0
  62. package/src/canvas/CanvasStack.tsx +6 -2
  63. package/src/canvas/useLayerScheduler.ts +22 -15
  64. package/src/canvas/usePanZoom.test.ts +80 -0
  65. package/src/canvas/usePanZoom.ts +16 -2
  66. package/src/chrome/ChromeRegions.stories.tsx +106 -0
  67. package/src/chrome/builtins.test.ts +153 -0
  68. package/src/chrome/builtins.tsx +205 -0
  69. package/src/chrome/index.ts +25 -0
  70. package/src/chrome/merge.test.ts +51 -0
  71. package/src/chrome/merge.ts +52 -0
  72. package/src/chrome/regions/PaletteRegion.less +14 -0
  73. package/src/chrome/regions/PaletteRegion.test.tsx +36 -0
  74. package/src/chrome/regions/PaletteRegion.tsx +41 -0
  75. package/src/chrome/regions/SidebarRegion.tsx +56 -0
  76. package/src/chrome/regions/StatusRegion.tsx +31 -0
  77. package/src/chrome/regions/TitleBarRegion.tsx +41 -0
  78. package/src/chrome/regions/ToolbarRegion.test.tsx +82 -0
  79. package/src/chrome/regions/ToolbarRegion.tsx +71 -0
  80. package/src/chrome/regions/ViewportRegion.less +42 -0
  81. package/src/chrome/regions/ViewportRegion.tsx +37 -0
  82. package/src/chrome/regions/regions.test.tsx +81 -0
  83. package/src/chrome/types.ts +127 -0
  84. package/src/config/builder.test.ts +93 -0
  85. package/src/config/builder.ts +220 -0
  86. package/src/config/fromConfigField.test.ts +93 -0
  87. package/src/config/fromConfigField.ts +74 -0
  88. package/src/config/index.ts +22 -0
  89. package/src/config/resolve.test.ts +119 -0
  90. package/src/config/resolve.ts +67 -0
  91. package/src/config/rules.ts +68 -0
  92. package/src/config/types.ts +108 -0
  93. package/src/config/useConfigSchema.ts +30 -0
  94. package/src/config/visible.ts +20 -0
  95. package/src/controls/ControlPanel.less +4 -58
  96. package/src/controls/ControlPanel.stories.tsx +125 -0
  97. package/src/controls/ControlPanel.test.tsx +245 -10
  98. package/src/controls/ControlPanel.tsx +236 -203
  99. package/src/dragdrop/DragDropRuntime.tsx +36 -18
  100. package/src/dragdrop/DragGhost.less +1 -1
  101. package/src/dragdrop/Palette.less +1 -1
  102. package/src/dragdrop/index.ts +11 -0
  103. package/src/index.ts +74 -5
  104. package/src/instrument/SineWave.smoke.test.tsx +8 -1
  105. package/src/instrument/defineInstrument.test.ts +38 -1
  106. package/src/instrument/defineInstrument.ts +31 -4
  107. package/src/instrument/index.ts +1 -0
  108. package/src/instrument/types.ts +27 -4
  109. package/src/instrument/validateConfigSchema.test.ts +13 -3
  110. package/src/instrument/validateConfigSchema.ts +7 -10
  111. package/src/lab/Lab.chrome.test.tsx +65 -0
  112. package/src/lab/Lab.tsx +58 -15
  113. package/src/lab/LabContext.ts +6 -0
  114. package/src/lab/LabFullChrome.stories.tsx +308 -0
  115. package/src/lab/LabHeader.test.tsx +42 -0
  116. package/src/lab/LabHeader.tsx +57 -0
  117. package/src/lab/LabPalette.test.tsx +35 -0
  118. package/src/lab/LabPalette.tsx +34 -0
  119. package/src/lab/LabShell.less +50 -3
  120. package/src/lab/Workspace.less +1 -30
  121. package/src/lab/Workspace.test.tsx +20 -10
  122. package/src/lab/Workspace.tsx +5 -8
  123. package/src/lab/index.ts +3 -1
  124. package/src/layers/AGENTS.md +1 -1
  125. package/src/layers/LayerList.less +31 -7
  126. package/src/layers/LayerList.tsx +17 -4
  127. package/src/passthrough/weasel-ui.ts +28 -0
  128. package/src/primitives/FloatingPanel.less +28 -0
  129. package/src/primitives/FloatingPanel.stories.tsx +46 -0
  130. package/src/primitives/FloatingPanel.test.tsx +240 -0
  131. package/src/primitives/FloatingPanel.tsx +190 -0
  132. package/src/primitives/FpsMeter.test.tsx +44 -0
  133. package/src/primitives/FpsMeter.tsx +20 -9
  134. package/src/primitives/JobProgress.less +69 -0
  135. package/src/primitives/JobProgress.stories.tsx +48 -0
  136. package/src/primitives/JobProgress.tsx +65 -0
  137. package/src/primitives/Legend.less +47 -0
  138. package/src/primitives/Legend.stories.tsx +32 -0
  139. package/src/primitives/Legend.test.tsx +57 -0
  140. package/src/primitives/Legend.tsx +40 -0
  141. package/src/primitives/Sidebar.less +32 -2
  142. package/src/primitives/StatusBar.less +7 -0
  143. package/src/primitives/StatusBar.tsx +10 -3
  144. package/src/primitives/Toolbar.less +50 -3
  145. package/src/primitives/Toolbar.test.tsx +67 -1
  146. package/src/primitives/Toolbar.tsx +72 -6
  147. package/src/primitives/ZoomControl.less +31 -0
  148. package/src/primitives/ZoomControl.tsx +76 -0
  149. package/src/primitives/floating.entry.test.tsx +14 -0
  150. package/src/primitives/index.ts +10 -2
  151. package/src/primitives/useRovingTabIndex.test.ts +27 -0
  152. package/src/primitives/useRovingTabIndex.ts +79 -0
  153. package/src/state/store.ts +11 -3
  154. package/src/state/toolSlot.test.ts +36 -0
  155. package/src/state/types.ts +4 -0
  156. package/src/styles.less +6 -4
  157. package/src/surface/useTiledSurface.ts +14 -10
  158. package/src/test-setup.ts +26 -0
  159. package/src/theme/Interstellar.stories.tsx +1 -1
  160. package/src/theme/base.less +19 -2
  161. package/src/theme/interstellar.tokens.json +6 -2
  162. package/src/tools/types.ts +24 -0
  163. package/src/trial/Trial.config.test.tsx +108 -0
  164. package/src/trial/Trial.job.test.tsx +13 -15
  165. package/src/trial/Trial.less +115 -10
  166. package/src/trial/Trial.stories.tsx +1 -0
  167. package/src/trial/Trial.test.tsx +67 -96
  168. package/src/trial/Trial.tsx +80 -39
  169. package/src/trial/TrialChrome.tsx +99 -80
  170. package/src/trial/TrialDragContext.ts +10 -0
  171. package/src/trial/TrialTitleBar.tsx +41 -0
  172. package/src/trial/index.ts +1 -15
  173. package/src/trial/trialChrome.borders.test.ts +38 -0
  174. package/src/ui/layers/index.ts +5 -1
  175. package/dist/_dts/index-JFAYj5Tv.d.ts +0 -144
  176. package/dist/_dts/types-x92Kfeme.d.ts +0 -62
  177. package/dist/chunk-574LJAV4.js.map +0 -1
  178. package/dist/chunk-5R2ATYPJ.js +0 -201
  179. package/dist/chunk-5R2ATYPJ.js.map +0 -1
  180. package/dist/chunk-73KA7WBO.js +0 -5992
  181. package/dist/chunk-73KA7WBO.js.map +0 -1
  182. package/dist/chunk-BOHF3PQO.js.map +0 -1
  183. package/dist/chunk-G5TJVQQT.js +0 -161
  184. package/dist/chunk-G5TJVQQT.js.map +0 -1
  185. package/dist/chunk-PMAU3SEE.js +0 -126
  186. package/dist/chunk-PMAU3SEE.js.map +0 -1
  187. package/dist/chunk-SFL7NFKN.js.map +0 -1
  188. package/dist/chunk-THBG7FQZ.js.map +0 -1
  189. package/dist/chunk-VUU5UXHE.js.map +0 -1
  190. package/src/instrument/capabilityDetector.test.ts +0 -64
  191. package/src/instrument/capabilityDetector.ts +0 -20
  192. package/src/trial/DefaultSidebar.tsx +0 -29
  193. package/src/trial/DefaultStatusBar.tsx +0 -19
  194. package/src/trial/DefaultToolbar.tsx +0 -92
  195. package/src/trial/slotTypes.ts +0 -54
  196. package/src/ui/format.test.ts +0 -33
  197. package/src/ui/format.ts +0 -30
  198. package/src/ui/layers/LayerStack.less +0 -143
  199. package/src/ui/layers/LayerStack.stories.tsx +0 -45
  200. package/src/ui/layers/LayerStack.test.tsx +0 -170
  201. package/src/ui/layers/LayerStack.tsx +0 -220
  202. package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
  203. package/src/ui/properties/ColorRow.stories.tsx +0 -106
  204. package/src/ui/properties/CurveField.less +0 -48
  205. package/src/ui/properties/CurveField.stories.tsx +0 -20
  206. package/src/ui/properties/CurveField.test.tsx +0 -127
  207. package/src/ui/properties/CurveField.tsx +0 -184
  208. package/src/ui/properties/EffectCard.tsx +0 -324
  209. package/src/ui/properties/Gallery.stories.tsx +0 -178
  210. package/src/ui/properties/NumberRow.stories.tsx +0 -65
  211. package/src/ui/properties/PropertyGroup.less +0 -50
  212. package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
  213. package/src/ui/properties/PropertyGroup.test.tsx +0 -33
  214. package/src/ui/properties/PropertyGroup.tsx +0 -45
  215. package/src/ui/properties/PropertyList.stories.tsx +0 -83
  216. package/src/ui/properties/PropertyPanel.less +0 -592
  217. package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
  218. package/src/ui/properties/PropertyPanel.test.tsx +0 -193
  219. package/src/ui/properties/PropertyPanel.tsx +0 -469
  220. package/src/ui/properties/PropertyRow.stories.tsx +0 -53
  221. package/src/ui/properties/SelectRow.stories.tsx +0 -58
  222. package/src/ui/properties/SliderRow.stories.tsx +0 -74
  223. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -396
  224. package/src/ui/properties/TextRow.stories.tsx +0 -62
  225. package/src/ui/properties/ToggleRow.stories.tsx +0 -57
  226. package/src/ui/properties/index.ts +0 -39
  227. package/src/ui/properties/storyLayouts.tsx +0 -36
package/dist/index.js CHANGED
@@ -1,29 +1,34 @@
1
- import { useSurfaceOptional } from './chunk-THBG7FQZ.js';
2
- export { SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceOptional, useSurfaceTile, useTiledSurface } from './chunk-THBG7FQZ.js';
1
+ import { useDragDrop, DragOverlay, Palette } from './chunk-3XXPU73K.js';
2
+ import { useSurfaceOptional } from './chunk-LGKRIUSW.js';
3
+ export { SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceOptional, useSurfaceTile, useTiledSurface } from './chunk-LGKRIUSW.js';
3
4
  import { useJob } from './chunk-LN6JDUGB.js';
4
5
  export { useJob } from './chunk-LN6JDUGB.js';
5
- export { LayerStack } from './chunk-G5TJVQQT.js';
6
- import { p, F } from './chunk-73KA7WBO.js';
7
- import './chunk-BOHF3PQO.js';
8
- import { Sidebar, StatusBar, Toolbar } from './chunk-PMAU3SEE.js';
9
- export { FpsMeter, ScaleIndicator, Sidebar, StatusBar, Toolbar } from './chunk-PMAU3SEE.js';
10
- import { LabStoreContext, createLabStore, noneAdapter } from './chunk-VUU5UXHE.js';
11
- export { CURRENT_DOCUMENT_VERSION, LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, createMemoryAdapter, labDocumentKey, localStorageAdapter, noneAdapter, quarantineKey, sessionStorageAdapter, urlHashAdapter, useLabStore, useTrialId, useTrialState } from './chunk-VUU5UXHE.js';
12
- import { ControlPanel } from './chunk-5R2ATYPJ.js';
13
- export { ControlPanel } from './chunk-5R2ATYPJ.js';
14
- import { CanvasStack, screenToWorld } from './chunk-574LJAV4.js';
15
- export { CanvasStack, screenToWorld, worldToScreen } from './chunk-574LJAV4.js';
6
+ import { suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, SidebarRegion, ViewportRegion, StatusRegion } from './chunk-6X5RBC6G.js';
7
+ export { PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, builtinContributions, mergeContributions, suppressContributions } from './chunk-6X5RBC6G.js';
8
+ import { JobProgress } from './chunk-QRYPXSGP.js';
9
+ export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-QRYPXSGP.js';
10
+ export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-JDROYQM3.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-WTI26YTM.js';
14
+ export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-WTI26YTM.js';
15
+ import { CanvasStack } from './chunk-BDJXIBRZ.js';
16
+ export { CanvasStack } from './chunk-BDJXIBRZ.js';
17
+ export { screenToWorld, worldToScreen } from './chunk-T66HU3HX.js';
16
18
  export { CanvasStackContext } from './chunk-CPUJ3QXL.js';
17
- import { LayerList } from './chunk-SFL7NFKN.js';
18
- export { LayerList } from './chunk-SFL7NFKN.js';
19
- import { createEventBus, redo, undo, pushSnapshot } from './chunk-RL2LOLNI.js';
19
+ import { LayerList } from './chunk-BMW4TDP5.js';
20
+ export { LayerList } from './chunk-BMW4TDP5.js';
21
+ import './chunk-SBXNQW4G.js';
22
+ import { Rt, p, r } from './chunk-RGT4EJIB.js';
23
+ export { k as CheckboxRow, O as ColorRow, m2 as CurveField, _2 as EffectCard, y as EffectCardList, c3 as LayerStack, j as NumberRow, b2 as PropertyGroup, S2 as PropertyList, x2 as PropertyPanel, w as PropertyRow, C as PropertySpan, M as SelectRow, E as SliderRow, g as Subpanel, A as TextRow, N as ToggleRow } from './chunk-RGT4EJIB.js';
24
+ import './chunk-QCICIHEO.js';
25
+ import { createEventBus, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
20
26
  export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
21
27
  import { createContext, useRef, useCallback, useContext, useMemo, useState, Children, useLayoutEffect, useEffect } from 'react';
22
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
28
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
23
29
  import { useStore } from 'zustand/react';
24
- import { createPortal } from 'react-dom';
30
+ import { Provider, StrategyRegistryProvider, DragProvider, Container, useDragHandle } from 'windease/react';
25
31
  import { asNodeId, Store, createNode, gridStrategy } from 'windease';
26
- import { DragHandle, Provider, StrategyRegistryProvider, DragProvider, Container } from 'windease/react';
27
32
 
28
33
  var PITCH_LIMIT = Math.PI / 2 - 0.01;
29
34
  var YAW_PER_PX = 8e-3;
@@ -106,23 +111,260 @@ function useOrbit({
106
111
  return { onWheel, onPointerDown, onPointerMove, onPointerUp, onDoubleClick, isDragging };
107
112
  }
108
113
 
109
- // src/instrument/capabilityDetector.ts
110
- function detectCapabilities(instrument) {
111
- return {
112
- hasCanvas: instrument.canvas != null,
113
- hasLayers: instrument.layers != null,
114
- hasDragDrop: instrument.dragDrop != null,
115
- hasUndo: instrument.undo != null
116
- };
114
+ // src/config/builder.ts
115
+ var BaseNode = class {
116
+ constructor(defaultValue, annotations = {}, options = {}) {
117
+ this.annotations = annotations;
118
+ this.options = options;
119
+ this.default = defaultValue;
120
+ }
121
+ annotations;
122
+ options;
123
+ // `default` is a reserved word in a parameter position, so the field is
124
+ // declared rather than taken as a parameter property.
125
+ default;
126
+ /** Clone into the same subclass. Subclasses with extra constructor
127
+ * arguments override this. */
128
+ with(annotations, options) {
129
+ const Ctor = this.constructor;
130
+ return new Ctor(this.default, annotations, options);
131
+ }
132
+ ann(patch) {
133
+ return this.with({ ...this.annotations, ...patch }, this.options);
134
+ }
135
+ opt(patch) {
136
+ return this.with(this.annotations, { ...this.options, ...patch });
137
+ }
138
+ /** Human-readable label. Defaults to the key, title-cased. */
139
+ label(name) {
140
+ return this.ann({ name });
141
+ }
142
+ /** Longer help text, surfaced as a tooltip on the row label. */
143
+ describe(description) {
144
+ return this.ann({ description });
145
+ }
146
+ /** Omit from the panel unless it is asked to show hidden leaves. */
147
+ hidden() {
148
+ return this.ann({ hidden: true });
149
+ }
150
+ /** Pair with a sibling sharing this id, side-by-side on one row. */
151
+ pair(pair) {
152
+ return this.ann({ pair });
153
+ }
154
+ /** Render under a named section heading. */
155
+ section(section) {
156
+ return this.opt({ section });
157
+ }
158
+ /** Show this row only while the predicate holds. Presentational — the value
159
+ * stays in config and the instrument still reads it. */
160
+ showIf(predicate) {
161
+ return this.opt({ showIf: predicate });
162
+ }
163
+ /** Draw this one row yourself, keeping the kind, default and validation. */
164
+ render(renderer) {
165
+ return this.opt({ render: renderer });
166
+ }
167
+ };
168
+ var NumberNode = class extends BaseNode {
169
+ kind = "number";
170
+ /** Bound the value. A number with both bounds renders as a slider. */
171
+ range(min, max) {
172
+ return this.ann({ min, max });
173
+ }
174
+ step(step) {
175
+ return this.ann({ step });
176
+ }
177
+ /** Force a slider even without both bounds. */
178
+ slider() {
179
+ return this.ann({ control: "slider" });
180
+ }
181
+ /** Force a typed input even when both bounds are set. */
182
+ input() {
183
+ return this.ann({ control: "input" });
184
+ }
185
+ };
186
+ var BooleanNode = class extends BaseNode {
187
+ kind = "boolean";
188
+ /** Ask for a switch. `ControlPanel` still draws a checkbox; weasel-ui's
189
+ * `PrefsForm` honors the distinction. */
190
+ toggle() {
191
+ return this.ann({ control: "switch" });
192
+ }
193
+ };
194
+ var StringNode = class extends BaseNode {
195
+ kind = "string";
196
+ placeholder(placeholder) {
197
+ return this.ann({ placeholder });
198
+ }
199
+ maxLength(maxLength) {
200
+ return this.ann({ maxLength });
201
+ }
202
+ /** Milliseconds to debounce live writes. 0 commits every keystroke. */
203
+ debounce(debounceMs) {
204
+ return this.ann({ debounceMs });
205
+ }
206
+ };
207
+ var ColorNode = class extends BaseNode {
208
+ kind = "color";
209
+ };
210
+ var EnumNode = class extends BaseNode {
211
+ kind = "enum";
212
+ /** Render as a segmented control rather than a select. */
213
+ radio() {
214
+ return this.ann({ control: "radio" });
215
+ }
216
+ /** Relabel the choices. Named `labels` rather than `options` because
217
+ * `options` is the node's own extras bag. */
218
+ labels(options) {
219
+ return this.ann({ options });
220
+ }
221
+ };
222
+ var ValueNode = class extends BaseNode {
223
+ kind = null;
224
+ };
225
+ var CustomNode = class _CustomNode extends BaseNode {
226
+ constructor(kind, defaultValue, annotations = {}, options = {}) {
227
+ super(defaultValue, annotations, options);
228
+ this.kind = kind;
229
+ }
230
+ kind;
231
+ with(annotations, options) {
232
+ return new _CustomNode(this.kind, this.default, annotations, options);
233
+ }
234
+ };
235
+ var expand = (options) => options.map((o) => typeof o === "string" ? { value: o, label: o } : o);
236
+ var f = {
237
+ number: (def) => new NumberNode(def),
238
+ boolean: (def) => new BooleanNode(def),
239
+ string: (def) => new StringNode(def),
240
+ color: (def) => new ColorNode(def),
241
+ /** A fixed set of choices. The default's literal type flows into the config
242
+ * type, so `f.enum('fast', ['fast', 'accurate'])` gives
243
+ * `'fast' | 'accurate'` with no `as const`. */
244
+ enum: (def, options) => new EnumNode(def, { options: expand(options) }),
245
+ /** A leaf with no declared kind: the rule chain decides, which is how a lab
246
+ * states a convention like "every `*Color` key is a color picker". */
247
+ value: (def) => new ValueNode(def),
248
+ /** A leaf of a kind a lab supplies the control for, through `controls`. */
249
+ custom: (kind, def, validate) => new CustomNode(kind, def, {}, validate ? { validate } : {}),
250
+ /** Collect leaves into an instrument's config. */
251
+ schema(nodes) {
252
+ return {
253
+ nodes,
254
+ defaults: () => {
255
+ const out = {};
256
+ for (const [key, node] of Object.entries(nodes)) out[key] = node.default;
257
+ return out;
258
+ }
259
+ };
260
+ }
261
+ };
262
+
263
+ // src/config/rules.ts
264
+ function titleCase(key) {
265
+ const words = key.replace(/[_-]+/g, " ").split(/(?<=[a-z0-9])(?=[A-Z])/).join(" ").trim().toLowerCase();
266
+ return words.length === 0 ? words : words[0].toUpperCase() + words.slice(1);
267
+ }
268
+ var kindFromValue = (ctx) => {
269
+ const t = typeof ctx.default;
270
+ if (t === "boolean" || t === "number" || t === "string") return { kind: t };
271
+ return null;
272
+ };
273
+ var labelFromKey = (ctx) => ({ name: titleCase(ctx.key) });
274
+ var sliderWhenBounded = (ctx) => {
275
+ if (ctx.leaf.kind !== "number") return null;
276
+ const bounded = ctx.leaf.min !== void 0 && ctx.leaf.max !== void 0;
277
+ return bounded ? { control: "slider" } : null;
278
+ };
279
+ var descriptionDefault = () => ({ description: "" });
280
+ var builtinRules = [
281
+ kindFromValue,
282
+ labelFromKey,
283
+ sliderWhenBounded,
284
+ descriptionDefault
285
+ ];
286
+ function applyRules(seed, ctx, rules) {
287
+ const acc = { ...seed };
288
+ for (const rule of rules) {
289
+ const patch = rule({ ...ctx, leaf: acc });
290
+ if (patch === null) continue;
291
+ for (const [k2, v] of Object.entries(patch)) {
292
+ if (v !== void 0 && acc[k2] === void 0) {
293
+ acc[k2] = v;
294
+ }
295
+ }
296
+ }
297
+ return acc;
298
+ }
299
+
300
+ // src/config/resolve.ts
301
+ function defined(annotations) {
302
+ const out = {};
303
+ for (const [k2, v] of Object.entries(annotations)) if (v !== void 0) out[k2] = v;
304
+ return out;
305
+ }
306
+ function resolveConfigSchema(schema, rules = []) {
307
+ const children = {};
308
+ const sectionOrder = [];
309
+ const sectionPaths = /* @__PURE__ */ new Map();
310
+ const showIf = /* @__PURE__ */ new Map();
311
+ const renderers = {};
312
+ const chain2 = [...rules, ...builtinRules];
313
+ for (const [key, node] of Object.entries(schema.nodes)) {
314
+ const seed = defined({
315
+ ...node.annotations,
316
+ ...node.kind === null ? {} : { kind: node.kind }
317
+ });
318
+ const patch = applyRules(seed, { key, path: key, default: node.default }, chain2);
319
+ children[key] = { ...patch, default: node.default };
320
+ const { section, showIf: predicate, render } = node.options;
321
+ if (section !== void 0) {
322
+ if (!sectionPaths.has(section)) {
323
+ sectionOrder.push(section);
324
+ sectionPaths.set(section, []);
325
+ }
326
+ sectionPaths.get(section)?.push(key);
327
+ }
328
+ if (predicate) showIf.set(key, predicate);
329
+ if (render) renderers[key] = render;
330
+ }
331
+ const group = { name: "", children };
332
+ const sections = sectionOrder.map((label) => ({
333
+ label,
334
+ paths: sectionPaths.get(label) ?? []
335
+ }));
336
+ return { group, sections, showIf, renderers };
337
+ }
338
+ var LabContext = createContext(null);
339
+ function useLabContext() {
340
+ const ctx = useContext(LabContext);
341
+ if (ctx === null) {
342
+ throw new Error("useLabContext must be used inside <Lab>");
343
+ }
344
+ return ctx;
345
+ }
346
+
347
+ // src/config/useConfigSchema.ts
348
+ var NO_RULES = [];
349
+ function useConfigSchema(instrument) {
350
+ const lab = useContext(LabContext);
351
+ const rules = lab?.configRules ?? NO_RULES;
352
+ return useMemo(
353
+ () => instrument.config ? resolveConfigSchema(instrument.config, rules) : fromConfigFields(instrument.configSchema?.() ?? []),
354
+ [instrument, rules]
355
+ );
117
356
  }
118
357
 
119
358
  // src/instrument/defineInstrument.ts
120
359
  function defineInstrument(spec) {
360
+ if (spec.config && !("defaultConfig" in spec && spec.defaultConfig)) {
361
+ const schema = spec.config;
362
+ return { ...spec, defaultConfig: () => schema.defaults() };
363
+ }
121
364
  return spec;
122
365
  }
123
366
 
124
367
  // src/instrument/validateConfigSchema.ts
125
- var KNOWN_TYPES = ["slider", "checkbox", "select", "number", "text", "color"];
126
368
  function validateConfigSchema(fields) {
127
369
  const errors = [];
128
370
  const seenKeys = /* @__PURE__ */ new Set();
@@ -137,10 +379,6 @@ function validateConfigSchema(fields) {
137
379
  if (field.label === "") {
138
380
  errors.push(`Field "${field.key}" has empty label`);
139
381
  }
140
- if (!KNOWN_TYPES.includes(field.type)) {
141
- errors.push(`Unknown field type: "${field.type}" on key "${field.key}"`);
142
- continue;
143
- }
144
382
  if (field.type === "slider") {
145
383
  if (!Number.isFinite(field.default)) {
146
384
  errors.push(`Field "${field.key}": default must be a finite number`);
@@ -175,7 +413,7 @@ function validateConfigSchema(fields) {
175
413
  return { valid: errors.length === 0, errors };
176
414
  }
177
415
 
178
- // ../theme/dist/chunk-E3FOFGMD.js
416
+ // ../theme/dist/chunk-O32P3G22.js
179
417
  var THEME_SOURCES = {
180
418
  "weasel": {
181
419
  "primitives": {
@@ -361,6 +599,15 @@ var THEME_SOURCES = {
361
599
  "type": "dimension",
362
600
  "value": "5px"
363
601
  },
602
+ "radius-lg": {
603
+ "type": "dimension",
604
+ "value": "14px"
605
+ },
606
+ "radius-pill": {
607
+ "type": "dimension",
608
+ "value": "999px",
609
+ "description": "Fully rounded ends \u2014 segmented controls, progress tracks."
610
+ },
364
611
  "border-w": {
365
612
  "type": "dimension",
366
613
  "value": "1px"
@@ -378,29 +625,47 @@ var THEME_SOURCES = {
378
625
  "tb-height": {
379
626
  "type": "dimension",
380
627
  "value": "28px",
381
- "description": "Height of a horizontal toolbar strip (ActionsBar, OptionsBar, ToggleBar, ToolOptionsBar)."
382
- },
383
- "radius-lg": {
384
- "type": "dimension",
385
- "value": "14px"
628
+ "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."
386
629
  },
387
630
  "control-h": {
388
631
  "type": "dimension",
389
- "value": "28px",
390
- "description": "Height of an interactive control \u2014 button, input, select. Distinct from tb-height, which sizes the strip a row of them sits in."
632
+ "value": "24px",
633
+ "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."
391
634
  },
392
635
  "glass-blur": {
393
636
  "type": "dimension",
394
637
  "value": "3px",
395
638
  "description": "Backdrop blur radius for frosted surfaces. Pairs with glass-tint."
396
639
  },
397
- "font-size": {
640
+ "font-size-2xs": {
641
+ "type": "dimension",
642
+ "value": "9px",
643
+ "description": "Shortcut keys and index badges. The floor for chrome text."
644
+ },
645
+ "font-size-xs": {
398
646
  "type": "dimension",
399
- "value": "13px"
647
+ "value": "10px",
648
+ "description": "Tool-button labels under an icon."
400
649
  },
401
650
  "font-size-sm": {
402
651
  "type": "dimension",
403
- "value": "11px"
652
+ "value": "11px",
653
+ "description": "Chrome labels, status readouts, section headings."
654
+ },
655
+ "font-size": {
656
+ "type": "dimension",
657
+ "value": "13px",
658
+ "description": "Controls and body text."
659
+ },
660
+ "font-size-lg": {
661
+ "type": "dimension",
662
+ "value": "16px",
663
+ "description": "Panel and dialog titles."
664
+ },
665
+ "font-size-xl": {
666
+ "type": "dimension",
667
+ "value": "20px",
668
+ "description": "The largest chrome text \u2014 a lab or app title."
404
669
  },
405
670
  "space-xs": {
406
671
  "type": "dimension",
@@ -418,6 +683,20 @@ var THEME_SOURCES = {
418
683
  "type": "dimension",
419
684
  "value": "16px"
420
685
  },
686
+ "tracking-none": {
687
+ "type": "dimension",
688
+ "value": "0"
689
+ },
690
+ "tracking-wide": {
691
+ "type": "dimension",
692
+ "value": "0.06em",
693
+ "description": "Uppercase section headings."
694
+ },
695
+ "tracking-wider": {
696
+ "type": "dimension",
697
+ "value": "0.08em",
698
+ "description": "Uppercase at 11px and below."
699
+ },
421
700
  "z-toolbar": {
422
701
  "type": "number",
423
702
  "value": 10
@@ -430,6 +709,21 @@ var THEME_SOURCES = {
430
709
  "type": "number",
431
710
  "value": 30
432
711
  },
712
+ "leading-tight": {
713
+ "type": "number",
714
+ "value": 1,
715
+ "description": "Single-line controls; the box sets the height."
716
+ },
717
+ "leading-snug": {
718
+ "type": "number",
719
+ "value": 1.2,
720
+ "description": "Headings and two-line labels."
721
+ },
722
+ "leading": {
723
+ "type": "number",
724
+ "value": 1.4,
725
+ "description": "Body and anything that wraps."
726
+ },
433
727
  "backdrop": {
434
728
  "type": "gradient",
435
729
  "value": "none",
@@ -582,6 +876,16 @@ var THEME_SOURCES = {
582
876
  "type": "color",
583
877
  "value": "{color.gray-900}",
584
878
  "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."
879
+ },
880
+ "shadow": {
881
+ "type": "color",
882
+ "value": "rgba(0, 0, 0, 0.6)",
883
+ "description": "Elevation. Always darker than any surface \u2014 never derived from fg, which is near-white on dark."
884
+ },
885
+ "border-raised": {
886
+ "type": "color",
887
+ "value": "{color.gray-400}",
888
+ "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."
585
889
  }
586
890
  },
587
891
  "light": {
@@ -626,6 +930,14 @@ var THEME_SOURCES = {
626
930
  "type": "color",
627
931
  "value": "{color.gray-50}",
628
932
  "description": "Flips with the mode \u2014 see the dark layer."
933
+ },
934
+ "shadow": {
935
+ "type": "color",
936
+ "value": "rgba(0, 0, 0, 0.18)"
937
+ },
938
+ "border-raised": {
939
+ "type": "color",
940
+ "value": "{color.gray-400}"
629
941
  }
630
942
  }
631
943
  }
@@ -639,26 +951,26 @@ var weaselTheme = {
639
951
  modes: THEME_SOURCES.weasel.modes
640
952
  };
641
953
  function hexToRgba(hex, alpha) {
642
- const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
643
- if (!m) throw new Error(`Expected a hex color, got "${hex}"`);
644
- const body = m[1];
645
- const full = body.length === 3 ? body.replace(/./g, (c) => c + c) : body;
646
- const r = Number.parseInt(full.slice(0, 2), 16);
647
- const g = Number.parseInt(full.slice(2, 4), 16);
648
- const b = Number.parseInt(full.slice(4, 6), 16);
649
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
954
+ const m2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
955
+ if (!m2) throw new Error(`Expected a hex color, got "${hex}"`);
956
+ const body = m2[1];
957
+ const full = body.length === 3 ? body.replace(/./g, (c2) => c2 + c2) : body;
958
+ const r2 = Number.parseInt(full.slice(0, 2), 16);
959
+ const g2 = Number.parseInt(full.slice(2, 4), 16);
960
+ const b2 = Number.parseInt(full.slice(4, 6), 16);
961
+ return `rgba(${r2}, ${g2}, ${b2}, ${alpha})`;
650
962
  }
651
963
  var REF = /^\{([^}]+)\}$/;
652
964
  function refTarget(value) {
653
965
  if (typeof value !== "string") return null;
654
- const m = REF.exec(value.trim());
655
- if (!m) return null;
656
- const path = m[1];
966
+ const m2 = REF.exec(value.trim());
967
+ if (!m2) return null;
968
+ const path = m2[1];
657
969
  const dot = path.indexOf(".");
658
970
  return dot === -1 ? path : path.slice(dot + 1);
659
971
  }
660
972
  function fontStack(value) {
661
- return value.map((f) => typeof f === "string" && /\s/.test(f) ? `'${f}'` : String(f)).join(", ");
973
+ return value.map((f2) => typeof f2 === "string" && /\s/.test(f2) ? `'${f2}'` : String(f2)).join(", ");
662
974
  }
663
975
  function serialize(type, value) {
664
976
  if (Array.isArray(value)) {
@@ -792,9 +1104,9 @@ function flattenTokens(doc) {
792
1104
  for (const [groupName, group] of Object.entries(doc)) {
793
1105
  if (groupName.startsWith("$")) continue;
794
1106
  if (typeof group !== "object" || group === null) continue;
795
- const g = group;
796
- const groupType = g.$type;
797
- for (const [leaf, node] of Object.entries(g)) {
1107
+ const g2 = group;
1108
+ const groupType = g2.$type;
1109
+ for (const [leaf, node] of Object.entries(g2)) {
798
1110
  if (leaf.startsWith("$")) continue;
799
1111
  if (!isToken(node)) continue;
800
1112
  const type = node.$type ?? groupType;
@@ -865,7 +1177,9 @@ var interstellar_tokens_default = {
865
1177
  "fg-subtle": { $value: "#5a5a78" },
866
1178
  accent: { $value: "#b08adb" },
867
1179
  "accent-hover": { $value: "#c6a3ee" },
868
- "focus-ring": { $value: "rgba(176, 138, 219, 0.45)" }
1180
+ "focus-ring": { $value: "rgba(176, 138, 219, 0.45)" },
1181
+ shadow: { $value: "rgba(0, 0, 0, 0.7)" },
1182
+ "border-raised": { $value: "rgba(255, 255, 255, 0.36)" }
869
1183
  },
870
1184
  gradient: {
871
1185
  $type: "gradient",
@@ -887,7 +1201,9 @@ var interstellar_tokens_default = {
887
1201
  "fg-subtle": { $value: "#a89b85" },
888
1202
  accent: { $value: "#a86f3c" },
889
1203
  "accent-hover": { $value: "#c2854f" },
890
- "focus-ring": { $value: "rgba(168, 111, 60, 0.35)" }
1204
+ "focus-ring": { $value: "rgba(168, 111, 60, 0.35)" },
1205
+ shadow: { $value: "rgba(80, 60, 40, 0.18)" },
1206
+ "border-raised": { $value: "#9e8b71" }
891
1207
  }
892
1208
  }
893
1209
  }
@@ -895,130 +1211,6 @@ var interstellar_tokens_default = {
895
1211
 
896
1212
  // src/theme/interstellar.ts
897
1213
  var interstellarTheme = loadDTCG(interstellar_tokens_default);
898
- function DragGhost({ item, screenPos }) {
899
- const [mounted, setMounted] = useState(false);
900
- useEffect(() => setMounted(true), []);
901
- if (!mounted || typeof document === "undefined") return null;
902
- const style = {
903
- left: `${screenPos.x + 12}px`,
904
- top: `${screenPos.y + 12}px`
905
- };
906
- return createPortal(
907
- /* @__PURE__ */ jsx("div", { className: "lk-drag-ghost", style, children: /* @__PURE__ */ jsx("span", { className: "lk-drag-ghost__label", children: item.label }) }),
908
- document.body
909
- );
910
- }
911
- function useDragDrop({
912
- capability,
913
- canvasContainerRef,
914
- view,
915
- state,
916
- config,
917
- setState,
918
- emit
919
- }) {
920
- const [drag, setDrag] = useState(null);
921
- const dragRef = useRef(null);
922
- dragRef.current = drag;
923
- const rafThrottle = useRef(null);
924
- const isOverCanvas = useCallback(
925
- (screenPos) => {
926
- const el = canvasContainerRef.current;
927
- if (!el) return false;
928
- const r = el.getBoundingClientRect();
929
- return screenPos.x >= r.left && screenPos.x <= r.right && screenPos.y >= r.top && screenPos.y <= r.bottom;
930
- },
931
- [canvasContainerRef]
932
- );
933
- const screenToWorldFromContainer = useCallback(
934
- (screenPos) => {
935
- const el = canvasContainerRef.current;
936
- if (!el) return null;
937
- const r = el.getBoundingClientRect();
938
- return screenToWorld({ x: screenPos.x - r.left, y: screenPos.y - r.top }, view);
939
- },
940
- [canvasContainerRef, view]
941
- );
942
- useEffect(() => {
943
- if (!drag) return;
944
- const handleMove = (e) => {
945
- const screenPos = { x: e.clientX, y: e.clientY };
946
- const current = dragRef.current;
947
- if (!current) return;
948
- if (rafThrottle.current !== null) {
949
- dragRef.current = { ...current, screenPos };
950
- setDrag(dragRef.current);
951
- return;
952
- }
953
- rafThrottle.current = requestAnimationFrame(() => {
954
- rafThrottle.current = null;
955
- const active = dragRef.current;
956
- if (!active) return;
957
- let feedback = null;
958
- if (capability.onDragOver && isOverCanvas(screenPos)) {
959
- const world = screenToWorldFromContainer(screenPos);
960
- if (world) feedback = capability.onDragOver(world, active.item, state, config);
961
- }
962
- setDrag({ ...active, screenPos, feedback });
963
- });
964
- };
965
- const handleUp = (e) => {
966
- const screenPos = { x: e.clientX, y: e.clientY };
967
- const active = dragRef.current;
968
- if (!active) return;
969
- if (rafThrottle.current !== null) {
970
- cancelAnimationFrame(rafThrottle.current);
971
- rafThrottle.current = null;
972
- }
973
- if (isOverCanvas(screenPos)) {
974
- const world = screenToWorldFromContainer(screenPos);
975
- if (world) {
976
- const nextState = capability.onDrop(world, active.item, state, config);
977
- setState(nextState);
978
- emit("canvas.itemAdded");
979
- }
980
- }
981
- setDrag(null);
982
- };
983
- window.addEventListener("pointermove", handleMove);
984
- window.addEventListener("pointerup", handleUp);
985
- return () => {
986
- window.removeEventListener("pointermove", handleMove);
987
- window.removeEventListener("pointerup", handleUp);
988
- };
989
- }, [drag, capability, state, config, setState, emit, isOverCanvas, screenToWorldFromContainer]);
990
- const startDrag = useCallback((item, originScreenPos) => {
991
- setDrag({ item, screenPos: originScreenPos, feedback: null });
992
- }, []);
993
- return { drag, startDrag };
994
- }
995
- function DragOverlay({ drag }) {
996
- if (!drag) return null;
997
- return /* @__PURE__ */ jsx(DragGhost, { item: drag.item, screenPos: drag.screenPos });
998
- }
999
- function Palette({ items, onDragStart, className }) {
1000
- return /* @__PURE__ */ jsx("div", { className: className ? `lk-palette ${className}` : "lk-palette", children: items.map((item) => /* @__PURE__ */ jsx(
1001
- "button",
1002
- {
1003
- type: "button",
1004
- className: "lk-palette__item",
1005
- onPointerDown: (e) => {
1006
- e.preventDefault();
1007
- onDragStart(item, { x: e.clientX, y: e.clientY });
1008
- },
1009
- children: /* @__PURE__ */ jsx("span", { className: "lk-palette__label", children: item.label })
1010
- },
1011
- item.id
1012
- )) });
1013
- }
1014
- var LabContext = createContext(null);
1015
- function useLabContext() {
1016
- const ctx = useContext(LabContext);
1017
- if (ctx === null) {
1018
- throw new Error("useLabContext must be used inside <Lab>");
1019
- }
1020
- return ctx;
1021
- }
1022
1214
 
1023
1215
  // src/state/view.ts
1024
1216
  var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
@@ -1030,112 +1222,40 @@ function as2DView(view) {
1030
1222
  if (typeof v.pan.x !== "number" || typeof v.pan.y !== "number") return null;
1031
1223
  return { zoom: v.zoom, pan: { x: v.pan.x, y: v.pan.y } };
1032
1224
  }
1033
- function DefaultSidebar({ ctx }) {
1034
- if (ctx.configFields.length === 0) {
1035
- return /* @__PURE__ */ jsx(Sidebar, { title: ctx.instrumentName, children: /* @__PURE__ */ jsxs("div", { className: "lk-sidebar__placeholder", children: [
1036
- ctx.instrumentName,
1037
- " has no config fields."
1038
- ] }) });
1039
- }
1040
- return /* @__PURE__ */ jsx(Sidebar, { title: ctx.instrumentName, children: /* @__PURE__ */ jsx(
1041
- ControlPanel,
1042
- {
1043
- fields: ctx.configFields,
1044
- config: ctx.config,
1045
- setConfig: (key, value) => ctx.setConfig(key, value)
1046
- }
1047
- ) });
1048
- }
1049
-
1050
- // src/ui/format.ts
1051
- var MINUS = "\u2212";
1052
- function formatNumber(n, fractionDigits) {
1053
- if (!Number.isFinite(n)) return String(n);
1054
- const s = fractionDigits == null ? String(n) : n.toFixed(fractionDigits);
1055
- return s.startsWith("-") ? MINUS + s.slice(1) : s;
1225
+ var TrialDragContext = createContext(null);
1226
+ function useTrialDrag() {
1227
+ return useContext(TrialDragContext);
1056
1228
  }
1057
- function parseSignedNumber(s) {
1058
- return Number(s.replace(MINUS, "-"));
1059
- }
1060
- function formatZoom(zoom) {
1061
- if (!Number.isFinite(zoom)) return String(zoom);
1062
- if (zoom <= 2) return `${formatNumber(Math.round(zoom * 100))}%`;
1063
- if (zoom >= 100) return `${Math.round(zoom).toLocaleString("en-US")}x`;
1064
- return `${formatNumber(Math.round(zoom * 10) / 10)}x`;
1065
- }
1066
- function DefaultStatusBar({ ctx }) {
1067
- return /* @__PURE__ */ jsxs(StatusBar, { children: [
1068
- /* @__PURE__ */ jsx(StatusBar.Section, { children: ctx.instrumentName }),
1069
- ctx.zoom === null ? null : /* @__PURE__ */ jsx(StatusBar.Section, { children: formatZoom(ctx.zoom) })
1229
+ function Draggable({ nodeId, title, children }) {
1230
+ const handlers = useDragHandle(nodeId);
1231
+ return /* @__PURE__ */ jsxs("div", { className: "lk-trial__titlebar lk-trial__titlebar--draggable", ...handlers, children: [
1232
+ /* @__PURE__ */ jsx("span", { className: "lk-trial__title", children: title }),
1233
+ children
1070
1234
  ] });
1071
1235
  }
1072
- function DefaultToolbar({ ctx }) {
1073
- return /* @__PURE__ */ jsxs(Toolbar, { children: [
1074
- ctx.hasUndo && /* @__PURE__ */ jsxs(Fragment, { children: [
1075
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.undo, disabled: !ctx.canUndo, title: "Undo (Cmd/Ctrl+Z)", children: "Undo" }),
1076
- /* @__PURE__ */ jsx(
1077
- Toolbar.Button,
1078
- {
1079
- onClick: ctx.redo,
1080
- disabled: !ctx.canRedo,
1081
- title: "Redo (Cmd/Ctrl+Shift+Z)",
1082
- children: "Redo"
1083
- }
1084
- ),
1085
- /* @__PURE__ */ jsx(Toolbar.Spacer, {})
1086
- ] }),
1087
- ctx.hasCanvas && /* @__PURE__ */ jsxs(Fragment, { children: [
1088
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.zoomOut, title: "Zoom out", children: "\u2212" }),
1089
- /* @__PURE__ */ jsx("span", { className: "lk-toolbar__zoom-label", children: formatZoom(ctx.zoom) }),
1090
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.zoomIn, title: "Zoom in", children: "+" }),
1091
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.resetZoom, title: "Reset zoom", children: "1:1" }),
1092
- /* @__PURE__ */ jsx(Toolbar.Spacer, {})
1093
- ] }),
1094
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: () => ctx.saveSnapshot(), title: "Save snapshot (Cmd/Ctrl+S)", children: "Save" }),
1095
- ctx.savedSnapshots.length > 0 && /* @__PURE__ */ jsxs(
1096
- "select",
1097
- {
1098
- className: "lk-toolbar__load-select",
1099
- "aria-label": "Load snapshot",
1100
- defaultValue: "",
1101
- onChange: (e) => {
1102
- const id = e.target.value;
1103
- if (id) {
1104
- ctx.loadSnapshot(id);
1105
- e.target.value = "";
1106
- }
1107
- },
1108
- children: [
1109
- /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Load\u2026" }),
1110
- ctx.savedSnapshots.map((sn) => /* @__PURE__ */ jsx("option", { value: sn.id, children: sn.name }, sn.id))
1111
- ]
1112
- }
1113
- ),
1114
- /* @__PURE__ */ jsx(Toolbar.Spacer, {}),
1115
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.clone, title: "Clone trial", children: "Clone" }),
1116
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.reset, title: "Reset trial", children: "Reset" }),
1117
- /* @__PURE__ */ jsx(
1118
- Toolbar.Button,
1119
- {
1120
- onClick: ctx.close,
1121
- disabled: ctx.isLastTrial,
1122
- title: ctx.isLastTrial ? "Cannot close the last trial" : "Close trial",
1123
- children: "Close"
1124
- }
1125
- )
1236
+ function TrialTitleBar({ title, children }) {
1237
+ const drag = useTrialDrag();
1238
+ if (drag)
1239
+ return /* @__PURE__ */ jsx(Draggable, { nodeId: drag.nodeId, title, children });
1240
+ return /* @__PURE__ */ jsxs("div", { className: "lk-trial__titlebar", children: [
1241
+ /* @__PURE__ */ jsx("span", { className: "lk-trial__title", children: title }),
1242
+ children
1126
1243
  ] });
1127
1244
  }
1245
+ var NO_OP = () => {
1246
+ };
1128
1247
  function TrialChrome({
1129
1248
  trialId,
1130
1249
  record,
1131
1250
  instrument,
1132
1251
  isLastTrial,
1133
- toolbar,
1134
- sidebar,
1135
- statusBar,
1136
1252
  undoBindings,
1137
1253
  job,
1138
- sidebarExtras,
1254
+ trialChrome,
1255
+ chrome,
1256
+ suppress,
1257
+ activeToolId = null,
1258
+ setActiveTool = NO_OP,
1139
1259
  children
1140
1260
  }) {
1141
1261
  const lab = useLabContext();
@@ -1145,7 +1265,8 @@ function TrialChrome({
1145
1265
  const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
1146
1266
  const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
1147
1267
  const view2d = as2DView(record.view);
1148
- const toolbarCtx = useMemo(() => {
1268
+ const configSchema = useConfigSchema(instrument);
1269
+ const ctx = useMemo(() => {
1149
1270
  const setZoom = (z) => {
1150
1271
  if (!view2d) return;
1151
1272
  updateTrialView(trialId, { ...view2d, zoom: z });
@@ -1153,33 +1274,15 @@ function TrialChrome({
1153
1274
  return {
1154
1275
  trialId,
1155
1276
  instrumentName: record.instrumentName,
1156
- hasUndo: instrument.undo != null,
1277
+ isLastTrial,
1278
+ zoom: view2d ? view2d.zoom : null,
1279
+ setZoom,
1157
1280
  canUndo: undoBindings?.canUndo ?? false,
1158
1281
  canRedo: undoBindings?.canRedo ?? false,
1159
- undo: undoBindings?.undo ?? (() => {
1160
- }),
1161
- redo: undoBindings?.redo ?? (() => {
1162
- }),
1163
- zoom: view2d?.zoom ?? 1,
1164
- setZoom,
1165
- zoomIn: () => setZoom((view2d?.zoom ?? 1) * 1.25),
1166
- zoomOut: () => setZoom((view2d?.zoom ?? 1) * 0.8),
1167
- resetZoom: () => setZoom(1),
1168
- hasCanvas: instrument.canvas != null,
1169
- savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
1170
- saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
1171
- loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
1172
- clone: () => lab.cloneTrial(trialId),
1173
- reset: () => lab.resetTrial(trialId),
1174
- close: () => lab.closeTrial(trialId),
1175
- isLastTrial
1176
- };
1177
- }, [trialId, record, instrument, lab, isLastTrial, updateTrialView, undoBindings, view2d]);
1178
- const sidebarCtx = useMemo(
1179
- () => ({
1180
- trialId,
1181
- instrumentName: record.instrumentName,
1182
- configFields: instrument.configSchema?.() ?? [],
1282
+ undo: undoBindings?.undo ?? NO_OP,
1283
+ redo: undoBindings?.redo ?? NO_OP,
1284
+ configSchema,
1285
+ configFields: instrument.config ? [] : instrument.configSchema?.() ?? [],
1183
1286
  config: record.config,
1184
1287
  setConfig: (key, value) => {
1185
1288
  const prevConfig = record.config;
@@ -1194,25 +1297,54 @@ function TrialChrome({
1194
1297
  const nextState = instrument.onConfigChange(nextConfig, prevConfig, record.state);
1195
1298
  updateTrialState(trialId, nextState);
1196
1299
  }
1197
- }
1198
- }),
1199
- [trialId, record, instrument, updateTrialConfig, updateTrialState]
1200
- );
1201
- const statusCtx = {
1300
+ },
1301
+ savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
1302
+ saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
1303
+ loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
1304
+ clone: () => lab.cloneTrial(trialId),
1305
+ reset: () => lab.resetTrial(trialId),
1306
+ close: () => lab.closeTrial(trialId),
1307
+ activeToolId,
1308
+ setActiveTool
1309
+ };
1310
+ }, [
1202
1311
  trialId,
1203
- instrumentName: record.instrumentName,
1204
- zoom: view2d ? view2d.zoom : null
1205
- };
1312
+ record,
1313
+ instrument,
1314
+ lab,
1315
+ isLastTrial,
1316
+ updateTrialView,
1317
+ updateTrialConfig,
1318
+ updateTrialState,
1319
+ undoBindings,
1320
+ view2d,
1321
+ activeToolId,
1322
+ setActiveTool,
1323
+ configSchema
1324
+ ]);
1325
+ const contributions = useMemo(
1326
+ () => suppressContributions(
1327
+ mergeContributions(
1328
+ builtinContributions(instrument, ctx, lab.controls),
1329
+ [...instrument.chrome ?? []],
1330
+ [...trialChrome ?? []],
1331
+ [...chrome ?? []]
1332
+ ),
1333
+ suppress ?? []
1334
+ ),
1335
+ [instrument, ctx, trialChrome, chrome, suppress, lab.controls]
1336
+ );
1337
+ const inRegion = (region) => contributions.filter((c2) => c2.region === region);
1206
1338
  const handleKeyDown = (e) => {
1207
1339
  const mod = e.metaKey || e.ctrlKey;
1208
1340
  if (!mod) return;
1209
1341
  if (e.key === "z" || e.key === "Z") {
1210
1342
  e.preventDefault();
1211
- if (e.shiftKey) toolbarCtx.redo();
1212
- else toolbarCtx.undo();
1343
+ if (e.shiftKey) ctx.redo();
1344
+ else ctx.undo();
1213
1345
  } else if (e.key === "s" || e.key === "S") {
1214
1346
  e.preventDefault();
1215
- toolbarCtx.saveSnapshot();
1347
+ ctx.saveSnapshot();
1216
1348
  }
1217
1349
  };
1218
1350
  return /* @__PURE__ */ jsxs(
@@ -1220,40 +1352,32 @@ function TrialChrome({
1220
1352
  {
1221
1353
  className: "lk-trial",
1222
1354
  "aria-label": `Trial ${record.instrumentName}`,
1355
+ tabIndex: -1,
1223
1356
  onKeyDown: handleKeyDown,
1224
1357
  children: [
1225
- /* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: toolbar ? toolbar(toolbarCtx) : /* @__PURE__ */ jsx(DefaultToolbar, { ctx: toolbarCtx }) }),
1358
+ /* @__PURE__ */ jsx(TrialTitleBar, { title: record.instrumentName, children: /* @__PURE__ */ jsx(TitleBarRegion, { contributions: inRegion("titlebar"), ctx }) }),
1359
+ /* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: /* @__PURE__ */ jsx(ToolbarRegion, { contributions: inRegion("toolbar"), ctx }) }),
1226
1360
  /* @__PURE__ */ jsxs("div", { className: "lk-trial__body", children: [
1227
- /* @__PURE__ */ jsxs("div", { className: "lk-trial__sidebar", children: [
1228
- sidebar ? sidebar(sidebarCtx) : /* @__PURE__ */ jsx(DefaultSidebar, { ctx: sidebarCtx }),
1229
- sidebarExtras
1230
- ] }),
1231
- /* @__PURE__ */ jsx("div", { className: "lk-trial__content", children })
1361
+ /* @__PURE__ */ jsx(PaletteRegion, { contributions: inRegion("palette"), ctx }),
1362
+ /* @__PURE__ */ jsx("div", { className: "lk-trial__sidebar", children: /* @__PURE__ */ jsx(SidebarRegion, { contributions: inRegion("sidebar"), ctx }) }),
1363
+ /* @__PURE__ */ jsxs("div", { className: `lk-trial__content${instrument.canvas ? " lk-trial__content--flush" : ""}`, children: [
1364
+ children,
1365
+ /* @__PURE__ */ jsx(ViewportRegion, { contributions: inRegion("viewport"), ctx })
1366
+ ] })
1232
1367
  ] }),
1233
1368
  /* @__PURE__ */ jsxs("div", { className: "lk-trial__status", children: [
1234
- job ? /* @__PURE__ */ jsxs("div", { className: "lk-trial__job", children: [
1235
- /* @__PURE__ */ jsxs("span", { className: "lk-trial__job-count", children: [
1236
- job.done,
1237
- job.total === null ? "" : ` / ${job.total}`
1238
- ] }),
1239
- job.failures.length > 0 ? /* @__PURE__ */ jsxs("span", { className: "lk-trial__job-failures", children: [
1240
- job.failures.length,
1241
- " failed"
1242
- ] }) : null,
1243
- job.error ? /* @__PURE__ */ jsx("span", { className: "lk-trial__job-error", children: job.error }) : null,
1244
- job.status === "running" ? /* @__PURE__ */ jsx("button", { type: "button", className: "lk-trial__job-cancel", onClick: job.cancel, children: "Cancel" }) : null
1245
- ] }) : null,
1246
- statusBar ? statusBar(statusCtx) : /* @__PURE__ */ jsx(DefaultStatusBar, { ctx: statusCtx })
1369
+ job ? /* @__PURE__ */ jsx(JobProgress, { job }) : null,
1370
+ /* @__PURE__ */ jsx(StatusRegion, { contributions: inRegion("status"), ctx })
1247
1371
  ] })
1248
1372
  ]
1249
1373
  }
1250
1374
  );
1251
1375
  }
1252
- function Trial({ id }) {
1376
+ function Trial({ id, chrome, suppress }) {
1253
1377
  const lab = useLabContext();
1254
1378
  const storeCtx = useContext(LabStoreContext);
1255
1379
  if (!storeCtx) throw new Error("[labkit] <Trial> requires <LabStoreProvider>");
1256
- const record = useStore(storeCtx.store, (s) => s.trials.find((w) => w.id === id));
1380
+ const record = useStore(storeCtx.store, (s) => s.trials.find((w2) => w2.id === id));
1257
1381
  if (!record) {
1258
1382
  return /* @__PURE__ */ jsxs("div", { className: "lk-trial lk-trial--unknown", children: [
1259
1383
  "Trial not found: ",
@@ -1273,16 +1397,21 @@ function Trial({ id }) {
1273
1397
  record,
1274
1398
  instrument,
1275
1399
  store: storeCtx.store,
1276
- isLast: lab.trials.length <= 1
1400
+ isLast: lab.trials.length <= 1,
1401
+ chrome,
1402
+ suppress
1277
1403
  }
1278
1404
  );
1279
1405
  }
1280
- function TrialRuntime({ record, instrument, store, isLast }) {
1406
+ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1281
1407
  const canvasContainerRef = useRef(null);
1282
1408
  const updateTrialState = useStore(store, (s) => s.updateTrialState);
1283
1409
  const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
1284
1410
  const updateTrialView = useStore(store, (s) => s.updateTrialView);
1285
1411
  const updateTrialUndoStack = useStore(store, (s) => s.updateTrialUndoStack);
1412
+ const labToolId = useStore(store, (s) => s.activeToolId);
1413
+ const setLabTool = useStore(store, (s) => s.setLabTool);
1414
+ const setTrialTool = useStore(store, (s) => s.setTrialTool);
1286
1415
  const busRef = useRef(null);
1287
1416
  if (busRef.current === null) busRef.current = createEventBus();
1288
1417
  const bus = busRef.current;
@@ -1293,12 +1422,18 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1293
1422
  const maxDepth = undoCap?.maxDepth ?? 50;
1294
1423
  const snapshotIfNeeded = (event) => {
1295
1424
  if (!undoCap || !undoEvents.has(event)) return;
1296
- const current = store.getState().trials.find((w) => w.id === record.id);
1425
+ const current = store.getState().trials.find((w2) => w2.id === record.id);
1297
1426
  if (!current) return;
1298
1427
  const snap = structuredClone(current.state);
1299
1428
  updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
1300
1429
  };
1301
1430
  const setView = (v) => updateTrialView(record.id, v);
1431
+ const declaresTools = instrument.tools != null;
1432
+ const resolvedToolId = declaresTools ? record.activeToolId ?? instrument.tools?.initial ?? instrument.tools?.tools[0]?.id ?? null : labToolId;
1433
+ const setActiveTool = (id) => {
1434
+ if (declaresTools) setTrialTool(record.id, id);
1435
+ else setLabTool(id);
1436
+ };
1302
1437
  const view2d = as2DView(record.view);
1303
1438
  const jobCap = instrument.job;
1304
1439
  const job = useJob({
@@ -1332,7 +1467,8 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1332
1467
  setZoom: (z) => {
1333
1468
  if (!view2d) return;
1334
1469
  updateTrialView(record.id, { ...view2d, zoom: z });
1335
- }
1470
+ },
1471
+ activeToolId: resolvedToolId
1336
1472
  },
1337
1473
  emit: (event) => {
1338
1474
  snapshotIfNeeded(event);
@@ -1359,7 +1495,7 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1359
1495
  const canvasLayers = useMemo(() => {
1360
1496
  if (!instrument.canvas) return [];
1361
1497
  const baseLayers = instrument.canvas.layers;
1362
- const ordered = layerOrder ? [...baseLayers].sort((a, b) => layerOrder.indexOf(a.id) - layerOrder.indexOf(b.id)) : baseLayers;
1498
+ const ordered = layerOrder ? [...baseLayers].sort((a, b2) => layerOrder.indexOf(a.id) - layerOrder.indexOf(b2.id)) : baseLayers;
1363
1499
  return ordered.map((layer) => ({
1364
1500
  id: layer.id,
1365
1501
  visible: layerVisibility[layer.id] !== false,
@@ -1372,7 +1508,7 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1372
1508
  }, [instrument.canvas, record.state, record.config, layerVisibility, layerOrder]);
1373
1509
  const layerDescriptors = useMemo(() => {
1374
1510
  if (!instrument.layers) return [];
1375
- return instrument.layers.ids.map((lid) => ({ id: lid, label: lid }));
1511
+ return instrument.layers.ids.map((l) => typeof l === "string" ? { id: l, label: l } : l);
1376
1512
  }, [instrument.layers]);
1377
1513
  const dragDropResult = useDragDrop({
1378
1514
  capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
@@ -1406,12 +1542,12 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1406
1542
  render: (ctx) => {
1407
1543
  const el = canvasContainerRef.current;
1408
1544
  if (!el) return;
1409
- const r = el.getBoundingClientRect();
1545
+ const r2 = el.getBoundingClientRect();
1410
1546
  ctx.save();
1411
1547
  ctx.strokeStyle = fb.ok ? "#3a7" : "#c44";
1412
1548
  ctx.lineWidth = 2;
1413
1549
  ctx.beginPath();
1414
- ctx.arc(screen.x - r.left, screen.y - r.top, 16, 0, Math.PI * 2);
1550
+ ctx.arc(screen.x - r2.left, screen.y - r2.top, 16, 0, Math.PI * 2);
1415
1551
  ctx.stroke();
1416
1552
  ctx.restore();
1417
1553
  }
@@ -1420,45 +1556,64 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1420
1556
  }, [canvasLayers, dragDropResult.drag]);
1421
1557
  let body;
1422
1558
  if (instrument.canvas) {
1423
- body = /* @__PURE__ */ jsxs(
1424
- "div",
1425
- {
1426
- ref: canvasContainerRef,
1427
- className: "lk-trial__canvas-host",
1428
- style: { width: "100%", height: "100%", position: "relative" },
1429
- children: [
1430
- /* @__PURE__ */ jsx(
1431
- CanvasStack,
1432
- {
1433
- layers: layersWithFeedback,
1434
- view: view2d ?? DEFAULT_VIEW,
1435
- onViewChange: setView,
1436
- children: instrument.render(renderCtx)
1437
- }
1438
- ),
1439
- /* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
1440
- ]
1441
- }
1442
- );
1559
+ body = /* @__PURE__ */ jsxs("div", { ref: canvasContainerRef, className: "lk-trial__canvas-host", children: [
1560
+ /* @__PURE__ */ jsx(
1561
+ CanvasStack,
1562
+ {
1563
+ layers: layersWithFeedback,
1564
+ view: view2d ?? DEFAULT_VIEW,
1565
+ onViewChange: setView,
1566
+ minZoom: instrument.canvas.minZoom,
1567
+ maxZoom: instrument.canvas.maxZoom,
1568
+ children: instrument.render(renderCtx)
1569
+ }
1570
+ ),
1571
+ /* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
1572
+ ] });
1443
1573
  } else {
1444
1574
  body = instrument.render(renderCtx);
1445
1575
  }
1446
- const paletteNode = paletteItems.length > 0 ? /* @__PURE__ */ jsx(Palette, { items: paletteItems, onDragStart: dragDropResult.startDrag }) : null;
1447
- const layerListNode = instrument.layers && layerDescriptors.length > 0 ? /* @__PURE__ */ jsx(
1448
- LayerList,
1449
- {
1450
- layers: layerDescriptors,
1451
- visibility: layerVisibility,
1452
- onReorder: (next) => {
1453
- setLayerOrder(next.map((l) => l.id));
1454
- bus.emit("layers.reorder");
1455
- },
1456
- onToggle: (lid, visible) => {
1457
- setLayerVisibility((prev) => ({ ...prev, [lid]: visible }));
1458
- bus.emit("layers.toggle");
1576
+ const startDrag = dragDropResult.startDrag;
1577
+ const paletteNode = useMemo(
1578
+ () => paletteItems.length > 0 ? /* @__PURE__ */ jsx(Palette, { items: paletteItems, onDragStart: startDrag }) : null,
1579
+ [paletteItems, startDrag]
1580
+ );
1581
+ const layerListNode = useMemo(
1582
+ () => instrument.layers && layerDescriptors.length > 0 ? /* @__PURE__ */ jsx(
1583
+ LayerList,
1584
+ {
1585
+ layers: layerDescriptors,
1586
+ visibility: layerVisibility,
1587
+ onReorder: (next) => {
1588
+ setLayerOrder(next.map((l) => l.id));
1589
+ bus.emit("layers.reorder");
1590
+ },
1591
+ onToggle: (lid, visible) => {
1592
+ setLayerVisibility((prev) => ({ ...prev, [lid]: visible }));
1593
+ bus.emit("layers.toggle");
1594
+ }
1459
1595
  }
1596
+ ) : null,
1597
+ [instrument.layers, layerDescriptors, layerVisibility, bus]
1598
+ );
1599
+ const extraChrome = useMemo(() => {
1600
+ const out = [];
1601
+ if (paletteNode) {
1602
+ out.push({
1603
+ id: "dragdrop-palette",
1604
+ region: "sidebar",
1605
+ item: { title: "Parts", body: paletteNode }
1606
+ });
1460
1607
  }
1461
- ) : null;
1608
+ if (layerListNode) {
1609
+ out.push({
1610
+ id: "layer-list",
1611
+ region: "sidebar",
1612
+ item: { title: "Layers", body: layerListNode }
1613
+ });
1614
+ }
1615
+ return out;
1616
+ }, [paletteNode, layerListNode]);
1462
1617
  return /* @__PURE__ */ jsx(
1463
1618
  TrialChrome,
1464
1619
  {
@@ -1468,10 +1623,11 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1468
1623
  instrument,
1469
1624
  isLastTrial: isLast,
1470
1625
  undoBindings,
1471
- sidebarExtras: /* @__PURE__ */ jsxs(Fragment, { children: [
1472
- paletteNode,
1473
- layerListNode
1474
- ] }),
1626
+ trialChrome: extraChrome,
1627
+ chrome,
1628
+ suppress,
1629
+ activeToolId: resolvedToolId,
1630
+ setActiveTool,
1475
1631
  children: body
1476
1632
  }
1477
1633
  );
@@ -1504,7 +1660,7 @@ function addTrial(trials, instruments, instrumentName) {
1504
1660
  return [...trials, record];
1505
1661
  }
1506
1662
  function cloneTrial(trials, id) {
1507
- const sourceIdx = trials.findIndex((w) => w.id === id);
1663
+ const sourceIdx = trials.findIndex((w2) => w2.id === id);
1508
1664
  const source = trials[sourceIdx];
1509
1665
  if (!source) return trials;
1510
1666
  const clone = {
@@ -1519,11 +1675,11 @@ function cloneTrial(trials, id) {
1519
1675
  }
1520
1676
  function closeTrial(trials, id) {
1521
1677
  if (trials.length <= 1) return trials;
1522
- const next = trials.filter((w) => w.id !== id);
1678
+ const next = trials.filter((w2) => w2.id !== id);
1523
1679
  return next.length === trials.length ? trials : next;
1524
1680
  }
1525
1681
  function resetTrial(trials, id, instruments) {
1526
- const idx = trials.findIndex((w) => w.id === id);
1682
+ const idx = trials.findIndex((w2) => w2.id === id);
1527
1683
  const current = trials[idx];
1528
1684
  if (!current) return trials;
1529
1685
  const instrument = findInstrument(instruments, current.instrumentName);
@@ -1538,12 +1694,80 @@ function resetTrial(trials, id, instruments) {
1538
1694
  return [...trials.slice(0, idx), reset, ...trials.slice(idx + 1)];
1539
1695
  }
1540
1696
  function reorderTrials(trials, ids) {
1541
- const byId = new Map(trials.map((w) => [w.id, w]));
1542
- const named = ids.map((id) => byId.get(id)).filter((w) => w !== void 0);
1543
- const seen = new Set(named.map((w) => w.id));
1544
- const rest = trials.filter((w) => !seen.has(w.id));
1697
+ const byId = new Map(trials.map((w2) => [w2.id, w2]));
1698
+ const named = ids.map((id) => byId.get(id)).filter((w2) => w2 !== void 0);
1699
+ const seen = new Set(named.map((w2) => w2.id));
1700
+ const rest = trials.filter((w2) => !seen.has(w2.id));
1545
1701
  return [...named, ...rest];
1546
1702
  }
1703
+ var MODES = [
1704
+ { value: "auto", label: "Auto" },
1705
+ { value: "light", label: "Light" },
1706
+ { value: "dark", label: "Dark" }
1707
+ ];
1708
+ function LabHeader() {
1709
+ const lab = useLabContext();
1710
+ const only = lab.instruments.length === 1 ? lab.instruments[0] : null;
1711
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1712
+ only ? /* @__PURE__ */ jsxs(
1713
+ "button",
1714
+ {
1715
+ type: "button",
1716
+ className: "lk-lab-header__add",
1717
+ onClick: () => lab.addTrial(only.name),
1718
+ children: [
1719
+ /* @__PURE__ */ jsx(Rt, { size: 16 }),
1720
+ /* @__PURE__ */ jsx("span", { children: "Add trial" })
1721
+ ]
1722
+ }
1723
+ ) : (
1724
+ // Held at null so the control stays an "add one" action rather than
1725
+ // reading as the current instrument.
1726
+ /* @__PURE__ */ jsx(
1727
+ p,
1728
+ {
1729
+ className: "lk-lab-header__add-select",
1730
+ "aria-label": "Add trial",
1731
+ placeholder: "Add trial\u2026",
1732
+ selectedKey: null,
1733
+ options: lab.instruments.map((i) => ({ value: i.name, label: i.name })),
1734
+ onSelectionChange: (name) => {
1735
+ if (name != null) lab.addTrial(String(name));
1736
+ }
1737
+ }
1738
+ )
1739
+ ),
1740
+ /* @__PURE__ */ jsx(
1741
+ r,
1742
+ {
1743
+ className: "lk-lab-header__mode",
1744
+ ariaLabel: "Color mode",
1745
+ items: MODES,
1746
+ value: lab.mode,
1747
+ onChange: (next) => {
1748
+ if (next) lab.setMode(next);
1749
+ }
1750
+ }
1751
+ )
1752
+ ] });
1753
+ }
1754
+ function LabPalette({ tools }) {
1755
+ const storeCtx = useContext(LabStoreContext);
1756
+ if (!storeCtx) throw new Error("[labkit] LabPalette requires <LabStoreProvider>");
1757
+ const activeToolId = useStore(storeCtx.store, (s) => s.activeToolId);
1758
+ const setLabTool = useStore(storeCtx.store, (s) => s.setLabTool);
1759
+ const contributions = tools.map((t) => ({
1760
+ id: t.id,
1761
+ region: "palette",
1762
+ group: t.group,
1763
+ item: { icon: t.icon, label: t.label, shortcut: t.shortcut }
1764
+ }));
1765
+ const ctx = {
1766
+ activeToolId,
1767
+ setActiveTool: setLabTool
1768
+ };
1769
+ return /* @__PURE__ */ jsx(PaletteRegion, { contributions, ctx });
1770
+ }
1547
1771
  function query() {
1548
1772
  if (typeof window === "undefined" || !window.matchMedia) return null;
1549
1773
  return window.matchMedia("(prefers-color-scheme: light)");
@@ -1600,7 +1824,7 @@ function Workspace({
1600
1824
  }) {
1601
1825
  const items = Children.toArray(children);
1602
1826
  const idKey = ids ? ids.join(",") : `#${items.length}`;
1603
- const nodeIds = useMemo(() => items.map((_, i) => asNodeId(ids?.[i] ?? `lk-ws-${i}`)), [idKey]);
1827
+ const nodeIds = useMemo(() => items.map((_2, i) => asNodeId(ids?.[i] ?? `lk-ws-${i}`)), [idKey]);
1604
1828
  const layoutRef = useRef(layout);
1605
1829
  layoutRef.current = layout;
1606
1830
  const onLayoutChangeRef = useRef(onLayoutChange);
@@ -1663,10 +1887,10 @@ function Workspace({
1663
1887
  [KIND]: ({ node }) => {
1664
1888
  const content = byId.get(node.id) ?? null;
1665
1889
  if (!reorderable) return content;
1666
- return /* @__PURE__ */ jsxs("div", { className: "lk-trial-tile", children: [
1667
- /* @__PURE__ */ jsx(DragHandle, { nodeId: node.id, className: "lk-trial-tile__grip", children: /* @__PURE__ */ jsx("span", { className: "lk-trial-tile__grip-dots", "aria-hidden": "true" }) }),
1668
- /* @__PURE__ */ jsx("div", { className: "lk-trial-tile__body", children: content })
1669
- ] });
1890
+ return (
1891
+ // No grip strip: the trial's own title bar is the drag surface.
1892
+ /* @__PURE__ */ jsx(TrialDragContext.Provider, { value: { nodeId: node.id }, children: /* @__PURE__ */ jsx("div", { className: "lk-trial-tile", children: content }) })
1893
+ );
1670
1894
  }
1671
1895
  };
1672
1896
  }, [nodeIds, items, reorderable]);
@@ -1703,9 +1927,9 @@ var NEBULA_SLOTS = [
1703
1927
  { cx: "85%", cy: "18%", sx: "55%", sy: "80%", stop: "70%" }
1704
1928
  ];
1705
1929
  function buildNebula(colors) {
1706
- const blobs = colors.map((c, i) => {
1930
+ const blobs = colors.map((c2, i) => {
1707
1931
  const p2 = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
1708
- return `radial-gradient(ellipse ${p2.sx} ${p2.sy} at ${p2.cx} ${p2.cy}, color-mix(in srgb, ${c} 22%, transparent), transparent ${p2.stop})`;
1932
+ return `radial-gradient(ellipse ${p2.sx} ${p2.sy} at ${p2.cx} ${p2.cy}, color-mix(in srgb, ${c2} 22%, transparent), transparent ${p2.stop})`;
1709
1933
  });
1710
1934
  blobs.push("radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%)");
1711
1935
  return blobs.join(", ");
@@ -1718,6 +1942,12 @@ function Lab({
1718
1942
  mode,
1719
1943
  nebula,
1720
1944
  title,
1945
+ footer,
1946
+ chrome,
1947
+ suppress,
1948
+ tools,
1949
+ configRules,
1950
+ controls,
1721
1951
  children
1722
1952
  }) {
1723
1953
  if (process.env.NODE_ENV !== "production" && instruments.length === 0) {
@@ -1746,8 +1976,8 @@ function Lab({
1746
1976
  }, [mode, store]);
1747
1977
  const contextValue = useMemo(() => {
1748
1978
  const replaceTrials = (next) => {
1749
- const currentById = new Map(store.getState().trials.map((w) => [w.id, w]));
1750
- const merged = next.map((w) => currentById.get(w.id) ?? w);
1979
+ const currentById = new Map(store.getState().trials.map((w2) => [w2.id, w2]));
1980
+ const merged = next.map((w2) => currentById.get(w2.id) ?? w2);
1751
1981
  store.setState({ trials: merged });
1752
1982
  store.getState().setMode(store.getState().mode);
1753
1983
  };
@@ -1771,18 +2001,19 @@ function Lab({
1771
2001
  },
1772
2002
  resetTrial: (id) => {
1773
2003
  const next = resetTrial(store.getState().trials, id, instruments);
1774
- const record = next.find((w) => w.id === id);
2004
+ const record = next.find((w2) => w2.id === id);
1775
2005
  if (!record) return;
1776
2006
  store.setState((s) => ({
1777
2007
  trials: s.trials.map(
1778
- (w) => w.id === id ? { ...w, config: record.config, state: record.state, view: record.view } : w
2008
+ (w2) => w2.id === id ? { ...w2, config: record.config, state: record.state, view: record.view } : w2
1779
2009
  )
1780
2010
  }));
1781
2011
  store.getState().updateTrialView(id, record.view);
1782
2012
  },
1783
2013
  savedSnapshots,
1784
2014
  saveSnapshot: (trialId, name) => {
1785
- store.getState().saveSnapshot(trialId, name ?? `Save ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
2015
+ const taken = store.getState().savedSnapshots.filter((s) => s.trialId === trialId).length;
2016
+ store.getState().saveSnapshot(trialId, name ?? `Snapshot ${taken + 1}`);
1786
2017
  },
1787
2018
  loadSnapshot: (trialId, snapshotId) => {
1788
2019
  store.getState().loadSnapshot(snapshotId, trialId);
@@ -1791,11 +2022,13 @@ function Lab({
1791
2022
  store.getState().deleteSnapshot(snapshotId);
1792
2023
  },
1793
2024
  mode: modeValue,
1794
- setMode: (m) => {
1795
- store.getState().setMode(m);
1796
- }
2025
+ setMode: (m2) => {
2026
+ store.getState().setMode(m2);
2027
+ },
2028
+ configRules,
2029
+ controls
1797
2030
  };
1798
- }, [instruments, trials, savedSnapshots, modeValue, store]);
2031
+ }, [instruments, trials, savedSnapshots, modeValue, store, configRules, controls]);
1799
2032
  const backdropStyle = resolvedMode === "dark" && nebula && nebula.length > 0 ? { ["--wzl-backdrop"]: buildNebula(nebula) } : void 0;
1800
2033
  return /* @__PURE__ */ jsx(LabStoreContext.Provider, { value: { store }, children: /* @__PURE__ */ jsx(LabContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
1801
2034
  ThemeProvider,
@@ -1804,628 +2037,37 @@ function Lab({
1804
2037
  mode: resolvedMode,
1805
2038
  className: "lk-lab",
1806
2039
  style: backdropStyle,
1807
- children: /* @__PURE__ */ jsx(LabShell, { title: title ?? "Labkit", mode: modeValue, header: children, children: /* @__PURE__ */ jsx(
1808
- Workspace,
1809
- {
1810
- ids: trials.map((w) => w.id),
1811
- resizable: true,
1812
- reorderable: true,
1813
- onReorder: (ids) => contextValue.reorderTrials(ids),
1814
- layout,
1815
- onLayoutChange: (next) => store.getState().setLayout(next),
1816
- children: trials.map((w) => /* @__PURE__ */ jsx(Trial, { id: w.id }, w.id))
1817
- }
1818
- ) })
1819
- }
1820
- ) }) });
1821
- }
1822
- function CurveField({
1823
- values,
1824
- min,
1825
- max,
1826
- step,
1827
- width,
1828
- height = 110,
1829
- defaults,
1830
- onChange,
1831
- marks
1832
- }) {
1833
- const points = useMemo(() => {
1834
- const out = [];
1835
- for (let i = 0; i + 1 < values.length; i += 2) out.push({ x: values[i], y: values[i + 1] });
1836
- return out;
1837
- }, [values]);
1838
- const handleChange = useCallback(
1839
- (next) => {
1840
- const flat = new Array(next.length * 2);
1841
- for (let i = 0; i < next.length; i++) {
1842
- const ySnap = Math.round(next[i].y / step) * step;
1843
- const y = Math.max(min, Math.min(max, ySnap));
1844
- flat[i * 2] = next[i].x;
1845
- flat[i * 2 + 1] = y;
1846
- }
1847
- p("curve-field", "handleChange", {
1848
- nLen: next.length,
1849
- flatLen: flat.length,
1850
- sample: flat.slice(0, 4)
1851
- });
1852
- onChange(flat);
1853
- },
1854
- [onChange, min, max, step]
1855
- );
1856
- const handleFlip = useCallback(() => {
1857
- const flipped = [];
1858
- for (let i = 0; i + 1 < values.length; i += 2) {
1859
- flipped.push({ x: 1 - values[i], y: values[i + 1] });
1860
- }
1861
- flipped.sort((a, b) => a.x - b.x);
1862
- const out = [];
1863
- for (const p2 of flipped) out.push(p2.x, p2.y);
1864
- onChange(out);
1865
- }, [values, onChange]);
1866
- const handleFlipVertical = useCallback(() => {
1867
- const mid = (min + max) / 2;
1868
- const flipped = new Array(values.length);
1869
- for (let i = 0; i + 1 < values.length; i += 2) {
1870
- flipped[i] = values[i];
1871
- flipped[i + 1] = 2 * mid - values[i + 1];
1872
- }
1873
- onChange(flipped);
1874
- }, [values, min, max, onChange]);
1875
- return /* @__PURE__ */ jsxs("div", { className: "lk-curve-field", children: [
1876
- /* @__PURE__ */ jsxs("div", { className: "lk-curve-field__plot", children: [
1877
- /* @__PURE__ */ jsx(
1878
- F,
1879
- {
1880
- value: points,
1881
- onInput: handleChange,
1882
- domain: "1d",
1883
- constrain: "function",
1884
- xRange: [0, 1],
1885
- yRange: [min, max],
1886
- width,
1887
- height,
1888
- endpoints: "pinned-x",
1889
- addPointMode: "click-curve",
1890
- minPoints: 2,
1891
- grid: {},
1892
- history: false
1893
- }
1894
- ),
1895
- marks && marks.length > 0 && /* @__PURE__ */ jsx(
1896
- "svg",
1897
- {
1898
- className: "lk-curve-field__marks",
1899
- width,
1900
- height,
1901
- viewBox: `0 0 ${width} ${height}`,
1902
- "aria-hidden": "true",
1903
- children: marks.map((m) => {
1904
- const color = m.color ?? "#ffcc00";
1905
- if (m.kind === "band") {
1906
- const x0 = Math.max(0, Math.min(1, m.x[0])) * width;
1907
- const x1 = Math.max(0, Math.min(1, m.x[1])) * width;
1908
- const left = Math.min(x0, x1);
1909
- const w = Math.abs(x1 - x0);
1910
- return /* @__PURE__ */ jsxs("g", { children: [
1911
- /* @__PURE__ */ jsx("rect", { x: left, y: 0, width: w, height, fill: color, fillOpacity: 0.4 }),
1912
- /* @__PURE__ */ jsx(
1913
- "line",
1914
- {
1915
- x1: left,
1916
- y1: 0,
1917
- x2: left,
1918
- y2: height,
1919
- stroke: color,
1920
- strokeOpacity: 0.85,
1921
- strokeWidth: 1
1922
- }
1923
- ),
1924
- /* @__PURE__ */ jsx(
1925
- "line",
1926
- {
1927
- x1: left + w,
1928
- y1: 0,
1929
- x2: left + w,
1930
- y2: height,
1931
- stroke: color,
1932
- strokeOpacity: 0.85,
1933
- strokeWidth: 1
1934
- }
1935
- )
1936
- ] }, `band-${m.x[0]}-${m.x[1]}-${color}`);
1937
- }
1938
- const x = Math.max(0, Math.min(1, m.x)) * width;
1939
- return /* @__PURE__ */ jsx(
1940
- "line",
1941
- {
1942
- x1: x,
1943
- y1: 0,
1944
- x2: x,
1945
- y2: height,
1946
- stroke: color,
1947
- strokeWidth: 1
1948
- },
1949
- `mark-${m.x}-${color}`
1950
- );
1951
- })
1952
- }
1953
- )
1954
- ] }),
1955
- /* @__PURE__ */ jsxs("div", { className: "lk-curve-field__actions", children: [
1956
- /* @__PURE__ */ jsx("button", { type: "button", className: "lk-curve-field__action", onClick: handleFlip, children: "Flip horizontally" }),
1957
- /* @__PURE__ */ jsx("button", { type: "button", className: "lk-curve-field__action", onClick: handleFlipVertical, children: "Flip vertically" }),
1958
- defaults && /* @__PURE__ */ jsx(
1959
- "button",
2040
+ children: /* @__PURE__ */ jsx(
2041
+ LabShell,
1960
2042
  {
1961
- type: "button",
1962
- className: "lk-curve-field__action",
1963
- onClick: () => onChange([...defaults]),
1964
- children: "Reset"
1965
- }
1966
- )
1967
- ] })
1968
- ] });
1969
- }
1970
- function Subpanel({ title, children, className }) {
1971
- const cls = className ? `lk-subpanel ${className}` : "lk-subpanel";
1972
- return /* @__PURE__ */ jsxs("div", { className: cls, children: [
1973
- /* @__PURE__ */ jsxs("h4", { className: "lk-subpanel__title", children: [
1974
- /* @__PURE__ */ jsx("span", { children: title }),
1975
- /* @__PURE__ */ jsx("hr", {})
1976
- ] }),
1977
- children
1978
- ] });
1979
- }
1980
- function EffectCard({
1981
- title,
1982
- primary,
1983
- accent,
1984
- index,
1985
- expanded = true,
1986
- onToggleExpanded,
1987
- onRemove,
1988
- draggable,
1989
- onPressHandle,
1990
- onReleaseHandle,
1991
- onDragStart,
1992
- onDragEnd,
1993
- onDragOver,
1994
- onDragLeave,
1995
- onDrop,
1996
- dragging,
1997
- children,
1998
- className
1999
- }) {
2000
- const stateClass = `${expanded ? "is-expanded" : "is-collapsed"}${dragging ? " is-dragging" : ""}${accent ? " has-accent" : ""}`;
2001
- const cls = `lk-effect-card ${stateClass}${className ? ` ${className}` : ""}`;
2002
- const style = accent ? { "--lk-panel-accent": accent } : void 0;
2003
- const headProps = onToggleExpanded ? {
2004
- role: "button",
2005
- tabIndex: 0,
2006
- onClick: onToggleExpanded,
2007
- onKeyDown: (e) => {
2008
- if (e.key === "Enter" || e.key === " ") {
2009
- e.preventDefault();
2010
- onToggleExpanded();
2011
- }
2012
- }
2013
- } : {};
2014
- return (
2015
- // biome-ignore lint/a11y/noStaticElementInteractions: native HTML5 drag-and-drop container — drag events are not click/keyboard interactions and have no semantic element; the in-card handle and header carry the a11y affordances.
2016
- /* @__PURE__ */ jsxs(
2017
- "div",
2018
- {
2019
- className: cls,
2020
- style,
2021
- draggable,
2022
- onDragStart,
2023
- onDragEnd,
2024
- onDragOver,
2025
- onDragLeave,
2026
- onDrop,
2027
- children: [
2028
- /* @__PURE__ */ jsxs("div", { className: "lk-effect-card__head", ...headProps, children: [
2029
- index != null ? /* @__PURE__ */ jsx(
2030
- "button",
2031
- {
2032
- type: "button",
2033
- className: "lk-effect-card__index-badge lk-effect-card__handle",
2034
- title: "Drag to reorder \xB7 click to toggle",
2035
- "aria-label": `Item ${index + 1} \u2014 drag to reorder, click to toggle`,
2036
- onMouseDown: onPressHandle,
2037
- onMouseUp: onReleaseHandle,
2038
- onClick: (e) => {
2039
- e.stopPropagation();
2040
- onToggleExpanded?.();
2041
- },
2042
- children: index + 1
2043
- }
2044
- ) : /* @__PURE__ */ jsx(
2045
- "span",
2046
- {
2047
- className: "lk-effect-card__handle",
2048
- "aria-hidden": "true",
2049
- title: "Drag to reorder",
2050
- onMouseDown: onPressHandle,
2051
- onMouseUp: onReleaseHandle,
2052
- children: /* @__PURE__ */ jsx(DragHandleIcon, {})
2053
- }
2054
- ),
2055
- /* @__PURE__ */ jsx("span", { className: "lk-effect-card__title", children: title }),
2056
- primary != null && /* @__PURE__ */ jsx("span", { className: "lk-effect-card__primary", children: primary }),
2057
- onRemove && /* @__PURE__ */ jsx(
2058
- "button",
2043
+ title: title ?? "Labkit",
2044
+ mode: modeValue,
2045
+ footer,
2046
+ header: /* @__PURE__ */ jsxs(Fragment, { children: [
2047
+ /* @__PURE__ */ jsx(LabHeader, {}),
2048
+ children
2049
+ ] }),
2050
+ children: /* @__PURE__ */ jsxs("div", { className: "lk-lab__body", children: [
2051
+ tools ? /* @__PURE__ */ jsx(LabPalette, { tools }) : null,
2052
+ /* @__PURE__ */ jsx(
2053
+ Workspace,
2059
2054
  {
2060
- type: "button",
2061
- className: "lk-effect-card__remove",
2062
- onClick: (e) => {
2063
- e.stopPropagation();
2064
- onRemove();
2065
- },
2066
- title: "Remove",
2067
- "aria-label": "Remove",
2068
- children: "\u2715"
2055
+ ids: trials.map((w2) => w2.id),
2056
+ resizable: true,
2057
+ reorderable: true,
2058
+ onReorder: (ids) => contextValue.reorderTrials(ids),
2059
+ layout,
2060
+ onLayoutChange: (next) => store.getState().setLayout(next),
2061
+ children: trials.map((w2) => /* @__PURE__ */ jsx(Trial, { id: w2.id, chrome, suppress }, w2.id))
2069
2062
  }
2070
2063
  )
2071
- ] }),
2072
- expanded && /* @__PURE__ */ jsx("div", { className: "lk-effect-card__body", children })
2073
- ]
2074
- }
2075
- )
2076
- );
2077
- }
2078
- function DragHandleIcon() {
2079
- return /* @__PURE__ */ jsxs("svg", { width: "10", height: "14", viewBox: "0 0 10 14", "aria-hidden": "true", children: [
2080
- /* @__PURE__ */ jsx("circle", { cx: "3", cy: "3", r: "1.3", fill: "currentColor" }),
2081
- /* @__PURE__ */ jsx("circle", { cx: "7", cy: "3", r: "1.3", fill: "currentColor" }),
2082
- /* @__PURE__ */ jsx("circle", { cx: "3", cy: "7", r: "1.3", fill: "currentColor" }),
2083
- /* @__PURE__ */ jsx("circle", { cx: "7", cy: "7", r: "1.3", fill: "currentColor" }),
2084
- /* @__PURE__ */ jsx("circle", { cx: "3", cy: "11", r: "1.3", fill: "currentColor" }),
2085
- /* @__PURE__ */ jsx("circle", { cx: "7", cy: "11", r: "1.3", fill: "currentColor" })
2086
- ] });
2087
- }
2088
- function EffectCardList({
2089
- items,
2090
- renderItem,
2091
- onReorder,
2092
- empty,
2093
- defaultExpandedIds
2094
- }) {
2095
- const [expanded, setExpanded] = useState(() => new Set(defaultExpandedIds ?? []));
2096
- const [draggingId, setDraggingId] = useState(null);
2097
- const [pressedId, setPressedId] = useState(null);
2098
- const [dropHint, setDropHint] = useState(
2099
- null
2100
- );
2101
- if (items.length === 0) {
2102
- return /* @__PURE__ */ jsx(Fragment, { children: empty ?? /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list__empty", children: "No items." }) });
2103
- }
2104
- return /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list", children: items.map((item) => {
2105
- const isExpanded = expanded.has(item.id);
2106
- const isDragging = draggingId === item.id;
2107
- const isPressed = pressedId === item.id;
2108
- const showHintBefore = dropHint && dropHint.id === item.id && dropHint.position === "before";
2109
- const showHintAfter = dropHint && dropHint.id === item.id && dropHint.position === "after";
2110
- const toggleExpanded = () => setExpanded((prev) => {
2111
- const next = new Set(prev);
2112
- if (next.has(item.id)) next.delete(item.id);
2113
- else next.add(item.id);
2114
- return next;
2115
- });
2116
- return /* @__PURE__ */ jsxs("div", { className: "lk-effect-card-list__wrap", children: [
2117
- showHintBefore && /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list__drop-hint" }),
2118
- renderItem(item, {
2119
- isExpanded,
2120
- isDragging,
2121
- toggleExpanded,
2122
- cardProps: {
2123
- expanded: isExpanded,
2124
- dragging: isDragging,
2125
- draggable: isPressed,
2126
- onToggleExpanded: toggleExpanded,
2127
- onPressHandle: () => setPressedId(item.id),
2128
- onReleaseHandle: () => setPressedId(null),
2129
- onDragStart: (e) => {
2130
- e.dataTransfer.effectAllowed = "move";
2131
- e.dataTransfer.setData("text/plain", String(item.id));
2132
- setDraggingId(item.id);
2133
- },
2134
- onDragEnd: () => {
2135
- setDraggingId(null);
2136
- setPressedId(null);
2137
- setDropHint(null);
2138
- },
2139
- onDragOver: (e) => {
2140
- if (draggingId === null || draggingId === item.id) return;
2141
- e.preventDefault();
2142
- e.dataTransfer.dropEffect = "move";
2143
- const rect = e.currentTarget.getBoundingClientRect();
2144
- const position = e.clientY < rect.top + rect.height / 2 ? "before" : "after";
2145
- setDropHint({ id: item.id, position });
2146
- },
2147
- onDragLeave: () => {
2148
- setDropHint((h) => h && h.id === item.id ? null : h);
2149
- },
2150
- onDrop: (e) => {
2151
- e.preventDefault();
2152
- if (draggingId === null || draggingId === item.id) {
2153
- setDropHint(null);
2154
- return;
2155
- }
2156
- const rect = e.currentTarget.getBoundingClientRect();
2157
- const position = e.clientY < rect.top + rect.height / 2 ? "before" : "after";
2158
- onReorder(draggingId, item.id, position);
2159
- setDropHint(null);
2160
- }
2161
- }
2162
- }),
2163
- showHintAfter && /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list__drop-hint" })
2164
- ] }, String(item.id));
2165
- }) });
2166
- }
2167
- function PropertyGroup({
2168
- title,
2169
- hidden,
2170
- children,
2171
- className,
2172
- pack = "auto-color"
2173
- }) {
2174
- if (hidden) return null;
2175
- const packClass = pack === "pairs" ? " lk-property-group--pairs" : "";
2176
- const cls = `lk-property-group${packClass}${className ? ` ${className}` : ""}`;
2177
- return /* @__PURE__ */ jsxs("div", { className: cls, children: [
2178
- /* @__PURE__ */ jsxs("h3", { className: "lk-property-group__title", children: [
2179
- /* @__PURE__ */ jsx("hr", {}),
2180
- /* @__PURE__ */ jsx("span", { children: title }),
2181
- /* @__PURE__ */ jsx("hr", {})
2182
- ] }),
2183
- /* @__PURE__ */ jsx("div", { className: "lk-property-group__body", children })
2184
- ] });
2185
- }
2186
- function PropertyPanel({ title, children, className }) {
2187
- const cls = className ? `lk-property-panel ${className}` : "lk-property-panel";
2188
- return /* @__PURE__ */ jsxs("div", { className: cls, children: [
2189
- title != null && /* @__PURE__ */ jsx("h2", { className: "lk-property-panel__title", children: title }),
2190
- children
2191
- ] });
2192
- }
2193
- function PropertyList({ children, className, pack = "auto-color" }) {
2194
- const packClass = pack === "pairs" ? " lk-property-list--pairs" : "";
2195
- const cls = `lk-property-list${packClass}${className ? ` ${className}` : ""}`;
2196
- return /* @__PURE__ */ jsx("div", { className: cls, children });
2197
- }
2198
- function PropertyRow({
2199
- label,
2200
- readout,
2201
- variant = "default",
2202
- layout = "block",
2203
- children,
2204
- htmlFor,
2205
- className
2206
- }) {
2207
- const variantClass = variant === "default" ? "" : ` lk-property-row--${variant}`;
2208
- const layoutClass = variant === "default" && layout === "inline" ? " lk-property-row--inline" : "";
2209
- const cls = `lk-property-row${variantClass}${layoutClass}${className ? ` ${className}` : ""}`;
2210
- return /* @__PURE__ */ jsxs("label", { className: cls, htmlFor, children: [
2211
- /* @__PURE__ */ jsxs("span", { className: "lk-property-row__label", children: [
2212
- label,
2213
- readout != null && /* @__PURE__ */ jsx("em", { className: "lk-property-row__readout", children: readout })
2214
- ] }),
2215
- children
2216
- ] });
2217
- }
2218
- function SliderRow({
2219
- label,
2220
- value,
2221
- min,
2222
- max,
2223
- step = 1,
2224
- onChange,
2225
- format,
2226
- unit,
2227
- layout
2228
- }) {
2229
- const decimals = step >= 1 ? 0 : Math.min(6, Math.max(0, Math.ceil(-Math.log10(step))));
2230
- const effectiveFormat = format ?? ((n) => formatNumber(n, decimals));
2231
- return /* @__PURE__ */ jsx(
2232
- PropertyRow,
2233
- {
2234
- label,
2235
- readout: /* @__PURE__ */ jsx(
2236
- EditableReadout,
2237
- {
2238
- value,
2239
- min,
2240
- max,
2241
- format: effectiveFormat,
2242
- unit,
2243
- onCommit: onChange
2244
- }
2245
- ),
2246
- layout,
2247
- children: /* @__PURE__ */ jsx(
2248
- "input",
2249
- {
2250
- type: "range",
2251
- tabIndex: -1,
2252
- min,
2253
- max,
2254
- step,
2255
- value,
2256
- onChange: (e) => {
2257
- const v = Number(e.target.value);
2258
- p("property-panel", "slider", { label, value: v });
2259
- onChange(v);
2260
- }
2064
+ ] })
2261
2065
  }
2262
2066
  )
2263
2067
  }
2264
- );
2265
- }
2266
- function EditableReadout({ value, min, max, format, unit, onCommit }) {
2267
- const [draft, setDraft] = useState(null);
2268
- const fmt = (n) => format ? format(n) : formatNumber(n);
2269
- const displayValue = draft !== null ? draft : (() => {
2270
- const formatted = fmt(value);
2271
- return typeof formatted === "string" ? formatted : String(formatted);
2272
- })();
2273
- const suffix = unit == null ? null : typeof unit === "string" ? /* @__PURE__ */ jsx("span", { className: "lk-property-row__readout-unit", children: unit }) : unit;
2274
- const commit = () => {
2275
- if (draft !== null) {
2276
- const n = parseSignedNumber(draft);
2277
- if (Number.isFinite(n)) onCommit(Math.min(max, Math.max(min, n)));
2278
- }
2279
- setDraft(null);
2280
- };
2281
- return /* @__PURE__ */ jsxs("span", { className: "lk-property-row__readout-group", children: [
2282
- /* @__PURE__ */ jsx(
2283
- "input",
2284
- {
2285
- type: "text",
2286
- inputMode: "decimal",
2287
- className: "lk-property-row__readout-input",
2288
- value: displayValue,
2289
- onFocus: (e) => {
2290
- const formatted = fmt(value);
2291
- setDraft(typeof formatted === "string" ? formatted : String(formatted));
2292
- e.currentTarget.select();
2293
- },
2294
- onChange: (e) => setDraft(e.target.value.replace(/-/g, "\u2212")),
2295
- onBlur: commit,
2296
- onClick: (e) => {
2297
- e.preventDefault();
2298
- e.stopPropagation();
2299
- e.currentTarget.focus();
2300
- },
2301
- onMouseDown: (e) => e.stopPropagation(),
2302
- onKeyDown: (e) => {
2303
- if (e.key === "Enter") {
2304
- commit();
2305
- e.currentTarget.blur();
2306
- } else if (e.key === "Escape") {
2307
- setDraft(null);
2308
- e.currentTarget.blur();
2309
- }
2310
- }
2311
- }
2312
- ),
2313
- suffix
2314
- ] });
2315
- }
2316
- function ColorRow({
2317
- label,
2318
- value,
2319
- onChange,
2320
- alpha,
2321
- onAlphaChange,
2322
- alphaDisabled
2323
- }) {
2324
- const showAlpha = alpha != null;
2325
- const className = alphaDisabled ? "lk-property-row--alpha-disabled" : void 0;
2326
- return /* @__PURE__ */ jsxs(PropertyRow, { label, variant: "color", className, children: [
2327
- /* @__PURE__ */ jsx("input", { type: "color", value, onChange: (e) => onChange(e.target.value) }),
2328
- showAlpha && /* @__PURE__ */ jsx(
2329
- "input",
2330
- {
2331
- type: "range",
2332
- className: "lk-property-row__alpha",
2333
- min: 0,
2334
- max: 1,
2335
- step: 0.01,
2336
- value: alpha,
2337
- disabled: alphaDisabled,
2338
- onChange: (e) => onAlphaChange?.(Number(e.target.value))
2339
- }
2340
- )
2341
- ] });
2342
- }
2343
- function CheckboxRow({ label, value, onChange }) {
2344
- return /* @__PURE__ */ jsx(PropertyRow, { label, variant: "checkbox", children: /* @__PURE__ */ jsx("input", { type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked) }) });
2345
- }
2346
- function TextRow({ label, value, onChange, placeholder, maxLength, layout }) {
2347
- return /* @__PURE__ */ jsx(PropertyRow, { label, layout, children: /* @__PURE__ */ jsx(
2348
- "input",
2349
- {
2350
- type: "text",
2351
- value,
2352
- placeholder,
2353
- maxLength,
2354
- onChange: (e) => onChange(e.target.value)
2355
- }
2356
- ) });
2357
- }
2358
- function NumberRow({
2359
- label,
2360
- value,
2361
- onChange,
2362
- min,
2363
- max,
2364
- step,
2365
- placeholder,
2366
- layout
2367
- }) {
2368
- return /* @__PURE__ */ jsx(PropertyRow, { label, layout, children: /* @__PURE__ */ jsx(
2369
- "input",
2370
- {
2371
- type: "number",
2372
- value,
2373
- min,
2374
- max,
2375
- step,
2376
- placeholder,
2377
- onChange: (e) => {
2378
- const raw = e.target.value;
2379
- if (raw === "") return;
2380
- const n = Number(raw);
2381
- if (Number.isFinite(n)) onChange(n);
2382
- }
2383
- }
2384
- ) });
2385
- }
2386
- function SelectRow({
2387
- label,
2388
- value,
2389
- options,
2390
- onChange,
2391
- layout
2392
- }) {
2393
- return /* @__PURE__ */ jsx(PropertyRow, { label, layout, children: /* @__PURE__ */ jsx(
2394
- "select",
2395
- {
2396
- value,
2397
- onChange: (e) => {
2398
- const v = e.target.value;
2399
- p("property-panel", "select", { label, value: v });
2400
- onChange(v);
2401
- },
2402
- children: options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
2403
- }
2404
- ) });
2405
- }
2406
- function ToggleRow({
2407
- label,
2408
- value,
2409
- options,
2410
- onChange,
2411
- layout
2412
- }) {
2413
- return /* @__PURE__ */ jsx(PropertyRow, { label, layout, children: /* @__PURE__ */ jsx("div", { className: "lk-property-row__toggle", children: options.map((opt) => {
2414
- const selected = opt.value === value;
2415
- return /* @__PURE__ */ jsx(
2416
- "button",
2417
- {
2418
- type: "button",
2419
- "aria-pressed": selected,
2420
- className: selected ? "lk-property-row__toggle-button lk-property-row__toggle-button--selected" : "lk-property-row__toggle-button",
2421
- onClick: () => onChange(opt.value),
2422
- children: opt.label
2423
- },
2424
- opt.value
2425
- );
2426
- }) }) });
2068
+ ) }) });
2427
2069
  }
2428
2070
 
2429
- export { CheckboxRow, ColorRow, CurveField, DEFAULT_VIEW, DefaultSidebar, DefaultStatusBar, DefaultToolbar, EffectCard, EffectCardList, Lab, LabContext, LabShell, NumberRow, PITCH_LIMIT, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SelectRow, SliderRow, Subpanel, TextRow, ToggleRow, Trial, TrialChrome, Workspace, addTrial, as2DView, clampPitch, cloneTrial, closeTrial, defineInstrument, detectCapabilities, interstellarTheme, orbitAfterDrag, orbitAfterWheel, reorderTrials, resetTrial, useLabContext, useOrbit, validateConfigSchema, wrapYaw };
2071
+ export { DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell, PITCH_LIMIT, Trial, TrialChrome, Workspace, addTrial, applyRules, as2DView, builtinRules, clampPitch, cloneTrial, closeTrial, defineInstrument, f, interstellarTheme, orbitAfterDrag, orbitAfterWheel, reorderTrials, resetTrial, resolveConfigSchema, titleCase, useConfigSchema, useLabContext, useOrbit, validateConfigSchema, wrapYaw };
2430
2072
  //# sourceMappingURL=index.js.map
2431
2073
  //# sourceMappingURL=index.js.map