@weasel-js/labkit 1.2.0 → 1.3.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) 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/index-DgcNdEdh.d.ts +314 -0
  4. package/dist/_dts/types-1Sdxy_Pv.d.ts +90 -0
  5. package/dist/_dts/types-AHlQxBNN.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 +2 -1
  9. package/dist/chrome/index.d.ts +86 -0
  10. package/dist/chrome/index.js +8 -0
  11. package/dist/chrome/index.js.map +1 -0
  12. package/dist/{chunk-VUU5UXHE.js → chunk-4TMMVIDM.js} +8 -5
  13. package/dist/chunk-4TMMVIDM.js.map +1 -0
  14. package/dist/{chunk-THBG7FQZ.js → chunk-HFDVGF4X.js} +15 -12
  15. package/dist/chunk-HFDVGF4X.js.map +1 -0
  16. package/dist/{chunk-574LJAV4.js → chunk-NS54R52R.js} +31 -21
  17. package/dist/chunk-NS54R52R.js.map +1 -0
  18. package/dist/{chunk-G5TJVQQT.js → chunk-O2BULFHX.js} +5 -14
  19. package/dist/chunk-O2BULFHX.js.map +1 -0
  20. package/dist/chunk-O7NVSCLN.js +266 -0
  21. package/dist/chunk-O7NVSCLN.js.map +1 -0
  22. package/dist/chunk-PO6L3NSH.js +366 -0
  23. package/dist/chunk-PO6L3NSH.js.map +1 -0
  24. package/dist/chunk-PQJ5B2U2.js +27 -0
  25. package/dist/chunk-PQJ5B2U2.js.map +1 -0
  26. package/dist/chunk-RVNN4CHQ.js +212 -0
  27. package/dist/chunk-RVNN4CHQ.js.map +1 -0
  28. package/dist/chunk-TLGRYALP.js +6151 -0
  29. package/dist/chunk-TLGRYALP.js.map +1 -0
  30. package/dist/{chunk-SFL7NFKN.js → chunk-V7PRSIRQ.js} +15 -7
  31. package/dist/chunk-V7PRSIRQ.js.map +1 -0
  32. package/dist/{chunk-BOHF3PQO.js → chunk-Z6HJ5FEM.js} +8111 -5190
  33. package/dist/chunk-Z6HJ5FEM.js.map +1 -0
  34. package/dist/chunk-ZZAYVX4X.js +505 -0
  35. package/dist/chunk-ZZAYVX4X.js.map +1 -0
  36. package/dist/controls/index.d.ts +19 -5
  37. package/dist/controls/index.js +3 -1
  38. package/dist/dragdrop/index.d.ts +4 -2
  39. package/dist/index.d.ts +246 -106
  40. package/dist/index.js +673 -559
  41. package/dist/index.js.map +1 -1
  42. package/dist/layers/index.d.ts +5 -3
  43. package/dist/layers/index.js +2 -1
  44. package/dist/passthrough/weasel-canvas.d.ts +69 -13
  45. package/dist/passthrough/weasel-canvas.js +1 -1
  46. package/dist/passthrough/weasel-ui.d.ts +3799 -3672
  47. package/dist/passthrough/weasel-ui.js +2 -2
  48. package/dist/primitives/index.d.ts +102 -8
  49. package/dist/primitives/index.js +4 -1
  50. package/dist/state/index.d.ts +3 -2
  51. package/dist/state/index.js +2 -2
  52. package/dist/styles.css +542 -153
  53. package/dist/surface/index.js +2 -1
  54. package/dist/ui/layers/index.js +4 -3
  55. package/dist/undo/index.d.ts +4 -2
  56. package/package.json +16 -7
  57. package/src/canvas/AGENTS.md +4 -0
  58. package/src/canvas/CanvasStack.tsx +6 -2
  59. package/src/canvas/useLayerScheduler.ts +22 -15
  60. package/src/canvas/usePanZoom.test.ts +80 -0
  61. package/src/canvas/usePanZoom.ts +16 -2
  62. package/src/chrome/ChromeRegions.stories.tsx +106 -0
  63. package/src/chrome/builtins.test.ts +153 -0
  64. package/src/chrome/builtins.tsx +205 -0
  65. package/src/chrome/index.ts +25 -0
  66. package/src/chrome/merge.test.ts +51 -0
  67. package/src/chrome/merge.ts +52 -0
  68. package/src/chrome/regions/PaletteRegion.less +14 -0
  69. package/src/chrome/regions/PaletteRegion.test.tsx +36 -0
  70. package/src/chrome/regions/PaletteRegion.tsx +41 -0
  71. package/src/chrome/regions/SidebarRegion.tsx +56 -0
  72. package/src/chrome/regions/StatusRegion.tsx +31 -0
  73. package/src/chrome/regions/TitleBarRegion.tsx +41 -0
  74. package/src/chrome/regions/ToolbarRegion.test.tsx +82 -0
  75. package/src/chrome/regions/ToolbarRegion.tsx +71 -0
  76. package/src/chrome/regions/ViewportRegion.less +42 -0
  77. package/src/chrome/regions/ViewportRegion.tsx +37 -0
  78. package/src/chrome/regions/regions.test.tsx +81 -0
  79. package/src/chrome/types.ts +127 -0
  80. package/src/config/builder.test.ts +87 -0
  81. package/src/config/builder.ts +224 -0
  82. package/src/config/fromConfigField.test.ts +93 -0
  83. package/src/config/fromConfigField.ts +74 -0
  84. package/src/config/index.ts +22 -0
  85. package/src/config/resolve.test.ts +120 -0
  86. package/src/config/resolve.ts +67 -0
  87. package/src/config/rules.ts +68 -0
  88. package/src/config/types.ts +108 -0
  89. package/src/config/useConfigSchema.ts +30 -0
  90. package/src/config/visible.ts +20 -0
  91. package/src/controls/ControlPanel.less +4 -58
  92. package/src/controls/ControlPanel.stories.tsx +86 -0
  93. package/src/controls/ControlPanel.test.tsx +223 -10
  94. package/src/controls/ControlPanel.tsx +202 -203
  95. package/src/dragdrop/DragDropRuntime.tsx +36 -18
  96. package/src/dragdrop/DragGhost.less +1 -1
  97. package/src/dragdrop/Palette.less +1 -1
  98. package/src/index.ts +25 -3
  99. package/src/instrument/SineWave.smoke.test.tsx +8 -1
  100. package/src/instrument/defineInstrument.test.ts +38 -1
  101. package/src/instrument/defineInstrument.ts +31 -4
  102. package/src/instrument/index.ts +1 -0
  103. package/src/instrument/types.ts +27 -4
  104. package/src/instrument/validateConfigSchema.test.ts +13 -3
  105. package/src/instrument/validateConfigSchema.ts +7 -10
  106. package/src/lab/Lab.chrome.test.tsx +65 -0
  107. package/src/lab/Lab.tsx +58 -15
  108. package/src/lab/LabContext.ts +6 -0
  109. package/src/lab/LabFullChrome.stories.tsx +308 -0
  110. package/src/lab/LabHeader.test.tsx +42 -0
  111. package/src/lab/LabHeader.tsx +57 -0
  112. package/src/lab/LabPalette.test.tsx +35 -0
  113. package/src/lab/LabPalette.tsx +34 -0
  114. package/src/lab/LabShell.less +50 -3
  115. package/src/lab/Workspace.less +1 -30
  116. package/src/lab/Workspace.test.tsx +20 -10
  117. package/src/lab/Workspace.tsx +5 -8
  118. package/src/lab/index.ts +2 -0
  119. package/src/layers/AGENTS.md +1 -1
  120. package/src/layers/LayerList.less +31 -7
  121. package/src/layers/LayerList.tsx +17 -4
  122. package/src/passthrough/weasel-ui.ts +23 -0
  123. package/src/primitives/DragHandleGlyph.tsx +29 -0
  124. package/src/primitives/FloatingPanel.less +28 -0
  125. package/src/primitives/FloatingPanel.stories.tsx +46 -0
  126. package/src/primitives/FloatingPanel.test.tsx +240 -0
  127. package/src/primitives/FloatingPanel.tsx +190 -0
  128. package/src/primitives/FpsMeter.test.tsx +44 -0
  129. package/src/primitives/FpsMeter.tsx +20 -9
  130. package/src/primitives/JobProgress.less +69 -0
  131. package/src/primitives/JobProgress.stories.tsx +48 -0
  132. package/src/primitives/JobProgress.tsx +65 -0
  133. package/src/primitives/Legend.less +47 -0
  134. package/src/primitives/Legend.stories.tsx +32 -0
  135. package/src/primitives/Legend.test.tsx +57 -0
  136. package/src/primitives/Legend.tsx +40 -0
  137. package/src/primitives/Sidebar.less +32 -2
  138. package/src/primitives/StatusBar.less +7 -0
  139. package/src/primitives/StatusBar.tsx +10 -3
  140. package/src/primitives/Toolbar.less +50 -3
  141. package/src/primitives/Toolbar.test.tsx +67 -1
  142. package/src/primitives/Toolbar.tsx +72 -6
  143. package/src/primitives/ZoomControl.less +31 -0
  144. package/src/primitives/ZoomControl.tsx +76 -0
  145. package/src/primitives/floating.entry.test.tsx +14 -0
  146. package/src/primitives/index.ts +10 -2
  147. package/src/primitives/useRovingTabIndex.test.ts +27 -0
  148. package/src/primitives/useRovingTabIndex.ts +79 -0
  149. package/src/state/store.ts +11 -3
  150. package/src/state/toolSlot.test.ts +36 -0
  151. package/src/state/types.ts +4 -0
  152. package/src/styles.less +6 -0
  153. package/src/surface/useTiledSurface.ts +14 -10
  154. package/src/test-setup.ts +26 -0
  155. package/src/theme/base.less +19 -2
  156. package/src/theme/interstellar.tokens.json +6 -2
  157. package/src/tools/types.ts +24 -0
  158. package/src/trial/Trial.config.test.tsx +113 -0
  159. package/src/trial/Trial.job.test.tsx +13 -15
  160. package/src/trial/Trial.less +115 -10
  161. package/src/trial/Trial.stories.tsx +1 -0
  162. package/src/trial/Trial.test.tsx +67 -96
  163. package/src/trial/Trial.tsx +80 -39
  164. package/src/trial/TrialChrome.tsx +99 -80
  165. package/src/trial/TrialDragContext.ts +10 -0
  166. package/src/trial/TrialTitleBar.tsx +41 -0
  167. package/src/trial/index.ts +1 -15
  168. package/src/trial/trialChrome.borders.test.ts +38 -0
  169. package/src/ui/layers/LayerStack.less +9 -9
  170. package/src/ui/layers/LayerStack.tsx +1 -13
  171. package/src/ui/properties/CurveField.less +2 -2
  172. package/src/ui/properties/CurveField.test.tsx +11 -11
  173. package/src/ui/properties/PropertyGroup.less +2 -2
  174. package/src/ui/properties/PropertyPanel.less +55 -41
  175. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +4 -2
  176. package/src/ui/properties/storyLayouts.tsx +2 -2
  177. package/dist/_dts/index-JFAYj5Tv.d.ts +0 -144
  178. package/dist/_dts/types-x92Kfeme.d.ts +0 -62
  179. package/dist/chunk-574LJAV4.js.map +0 -1
  180. package/dist/chunk-5R2ATYPJ.js +0 -201
  181. package/dist/chunk-5R2ATYPJ.js.map +0 -1
  182. package/dist/chunk-73KA7WBO.js +0 -5992
  183. package/dist/chunk-73KA7WBO.js.map +0 -1
  184. package/dist/chunk-BOHF3PQO.js.map +0 -1
  185. package/dist/chunk-G5TJVQQT.js.map +0 -1
  186. package/dist/chunk-PMAU3SEE.js +0 -126
  187. package/dist/chunk-PMAU3SEE.js.map +0 -1
  188. package/dist/chunk-SFL7NFKN.js.map +0 -1
  189. package/dist/chunk-THBG7FQZ.js.map +0 -1
  190. package/dist/chunk-VUU5UXHE.js.map +0 -1
  191. package/src/instrument/capabilityDetector.test.ts +0 -64
  192. package/src/instrument/capabilityDetector.ts +0 -20
  193. package/src/trial/DefaultSidebar.tsx +0 -29
  194. package/src/trial/DefaultStatusBar.tsx +0 -19
  195. package/src/trial/DefaultToolbar.tsx +0 -92
  196. package/src/trial/slotTypes.ts +0 -54
package/dist/index.js CHANGED
@@ -1,29 +1,33 @@
1
- import { useSurfaceOptional } from './chunk-THBG7FQZ.js';
2
- export { SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceOptional, useSurfaceTile, useTiledSurface } from './chunk-THBG7FQZ.js';
1
+ import { useSurfaceOptional } from './chunk-HFDVGF4X.js';
2
+ export { SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceOptional, useSurfaceTile, useTiledSurface } from './chunk-HFDVGF4X.js';
3
3
  import { useJob } from './chunk-LN6JDUGB.js';
4
4
  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';
5
+ export { LayerStack } from './chunk-O2BULFHX.js';
6
+ import { suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, SidebarRegion, ViewportRegion, StatusRegion } from './chunk-PO6L3NSH.js';
7
+ export { PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, builtinContributions, mergeContributions, suppressContributions } from './chunk-PO6L3NSH.js';
8
+ import { JobProgress } from './chunk-RVNN4CHQ.js';
9
+ export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-RVNN4CHQ.js';
10
+ export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-O7NVSCLN.js';
11
+ import { LabStoreContext, createLabStore, noneAdapter } from './chunk-4TMMVIDM.js';
12
+ export { CURRENT_DOCUMENT_VERSION, LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, createMemoryAdapter, labDocumentKey, localStorageAdapter, noneAdapter, quarantineKey, sessionStorageAdapter, urlHashAdapter, useLabStore, useTrialId, useTrialState } from './chunk-4TMMVIDM.js';
13
+ import { fromConfigFields } from './chunk-ZZAYVX4X.js';
14
+ export { CheckboxRow, ColorRow, ControlPanel, NumberRow, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SelectRow, SliderRow, TextRow, ToggleRow, fromConfigFields, isLeafVisible } from './chunk-ZZAYVX4X.js';
15
+ import { p, F, vt, p2, r } from './chunk-TLGRYALP.js';
16
+ import { CanvasStack, screenToWorld } from './chunk-NS54R52R.js';
17
+ export { CanvasStack, screenToWorld, worldToScreen } from './chunk-NS54R52R.js';
16
18
  export { CanvasStackContext } from './chunk-CPUJ3QXL.js';
17
- import { LayerList } from './chunk-SFL7NFKN.js';
18
- export { LayerList } from './chunk-SFL7NFKN.js';
19
+ import { useVisibleRaf } from './chunk-Z6HJ5FEM.js';
20
+ import { LayerList } from './chunk-V7PRSIRQ.js';
21
+ export { LayerList } from './chunk-V7PRSIRQ.js';
22
+ import './chunk-PQJ5B2U2.js';
19
23
  import { createEventBus, redo, undo, pushSnapshot } from './chunk-RL2LOLNI.js';
20
24
  export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
21
25
  import { createContext, useRef, useCallback, useContext, useMemo, useState, Children, useLayoutEffect, useEffect } from 'react';
22
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
26
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
23
27
  import { useStore } from 'zustand/react';
24
28
  import { createPortal } from 'react-dom';
29
+ import { Provider, StrategyRegistryProvider, DragProvider, Container, useDragHandle } from 'windease/react';
25
30
  import { asNodeId, Store, createNode, gridStrategy } from 'windease';
26
- import { DragHandle, Provider, StrategyRegistryProvider, DragProvider, Container } from 'windease/react';
27
31
 
28
32
  var PITCH_LIMIT = Math.PI / 2 - 0.01;
29
33
  var YAW_PER_PX = 8e-3;
@@ -106,23 +110,260 @@ function useOrbit({
106
110
  return { onWheel, onPointerDown, onPointerMove, onPointerUp, onDoubleClick, isDragging };
107
111
  }
108
112
 
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
- };
113
+ // src/config/builder.ts
114
+ var BaseNode = class {
115
+ constructor(defaultValue, annotations = {}, options = {}) {
116
+ this.annotations = annotations;
117
+ this.options = options;
118
+ this.default = defaultValue;
119
+ }
120
+ annotations;
121
+ options;
122
+ // `default` is a reserved word in a parameter position, so the field is
123
+ // declared rather than taken as a parameter property.
124
+ default;
125
+ /** Clone into the same subclass. Subclasses with extra constructor
126
+ * arguments override this. */
127
+ with(annotations, options) {
128
+ const Ctor = this.constructor;
129
+ return new Ctor(this.default, annotations, options);
130
+ }
131
+ ann(patch) {
132
+ return this.with({ ...this.annotations, ...patch }, this.options);
133
+ }
134
+ opt(patch) {
135
+ return this.with(this.annotations, { ...this.options, ...patch });
136
+ }
137
+ /** Human-readable label. Defaults to the key, title-cased. */
138
+ label(name) {
139
+ return this.ann({ name });
140
+ }
141
+ /** Longer help text, surfaced as a tooltip on the row label. */
142
+ describe(description) {
143
+ return this.ann({ description });
144
+ }
145
+ /** Omit from the panel unless it is asked to show hidden leaves. */
146
+ hidden() {
147
+ return this.ann({ hidden: true });
148
+ }
149
+ /** Pair with a sibling sharing this id, side-by-side on one row. */
150
+ pair(pair) {
151
+ return this.ann({ pair });
152
+ }
153
+ /** Render under a named section heading. */
154
+ section(section) {
155
+ return this.opt({ section });
156
+ }
157
+ /** Show this row only while the predicate holds. Presentational — the value
158
+ * stays in config and the instrument still reads it. */
159
+ showIf(predicate) {
160
+ return this.opt({ showIf: predicate });
161
+ }
162
+ /** Draw this one row yourself, keeping the kind, default and validation. */
163
+ render(renderer) {
164
+ return this.opt({ render: renderer });
165
+ }
166
+ };
167
+ var NumberNode = class extends BaseNode {
168
+ kind = "number";
169
+ /** Bound the value. A number with both bounds renders as a slider. */
170
+ range(min, max) {
171
+ return this.ann({ min, max });
172
+ }
173
+ step(step) {
174
+ return this.ann({ step });
175
+ }
176
+ /** Force a slider even without both bounds. */
177
+ slider() {
178
+ return this.ann({ control: "slider" });
179
+ }
180
+ /** Force a typed input even when both bounds are set. */
181
+ input() {
182
+ return this.ann({ control: "input" });
183
+ }
184
+ };
185
+ var BooleanNode = class extends BaseNode {
186
+ kind = "boolean";
187
+ /** Ask for a switch. `ControlPanel` still draws a checkbox; weasel-ui's
188
+ * `PrefsForm` honors the distinction. */
189
+ toggle() {
190
+ return this.ann({ control: "switch" });
191
+ }
192
+ };
193
+ var StringNode = class extends BaseNode {
194
+ kind = "string";
195
+ placeholder(placeholder) {
196
+ return this.ann({ placeholder });
197
+ }
198
+ maxLength(maxLength) {
199
+ return this.ann({ maxLength });
200
+ }
201
+ /** Milliseconds to debounce live writes. 0 commits every keystroke. */
202
+ debounce(debounceMs) {
203
+ return this.ann({ debounceMs });
204
+ }
205
+ };
206
+ var ColorNode = class extends BaseNode {
207
+ kind = "color";
208
+ };
209
+ var EnumNode = class extends BaseNode {
210
+ kind = "enum";
211
+ /** Render as a segmented control rather than a select. */
212
+ radio() {
213
+ return this.ann({ control: "radio" });
214
+ }
215
+ /** Relabel the choices. Named `labels` rather than `options` because
216
+ * `options` is the node's own extras bag. */
217
+ labels(options) {
218
+ return this.ann({ options });
219
+ }
220
+ };
221
+ var ValueNode = class extends BaseNode {
222
+ kind = null;
223
+ };
224
+ var CustomNode = class _CustomNode extends BaseNode {
225
+ constructor(kind, defaultValue, annotations = {}, options = {}) {
226
+ super(defaultValue, annotations, options);
227
+ this.kind = kind;
228
+ }
229
+ kind;
230
+ with(annotations, options) {
231
+ return new _CustomNode(this.kind, this.default, annotations, options);
232
+ }
233
+ };
234
+ var expand = (options) => options.map((o) => typeof o === "string" ? { value: o, label: o } : o);
235
+ var f = {
236
+ number: (def) => new NumberNode(def),
237
+ boolean: (def) => new BooleanNode(def),
238
+ string: (def) => new StringNode(def),
239
+ color: (def) => new ColorNode(def),
240
+ /** A fixed set of choices. The default's literal type flows into the config
241
+ * type, so `f.enum('fast', ['fast', 'accurate'])` gives
242
+ * `'fast' | 'accurate'` with no `as const`. */
243
+ enum: (def, options) => new EnumNode(def, { options: expand(options) }),
244
+ /** A leaf with no declared kind: the rule chain decides, which is how a lab
245
+ * states a convention like "every `*Color` key is a color picker". */
246
+ value: (def) => new ValueNode(def),
247
+ /** A leaf of a kind a lab supplies the control for, through `controls`. */
248
+ custom: (kind, def, validate) => new CustomNode(kind, def, {}, validate ? { validate } : {}),
249
+ /** Collect leaves into an instrument's config. */
250
+ schema(nodes) {
251
+ return {
252
+ nodes,
253
+ defaults: () => {
254
+ const out = {};
255
+ for (const [key, node] of Object.entries(nodes)) out[key] = node.default;
256
+ return out;
257
+ }
258
+ };
259
+ }
260
+ };
261
+
262
+ // src/config/rules.ts
263
+ function titleCase(key) {
264
+ const words = key.replace(/[_-]+/g, " ").split(/(?<=[a-z0-9])(?=[A-Z])/).join(" ").trim().toLowerCase();
265
+ return words.length === 0 ? words : words[0].toUpperCase() + words.slice(1);
266
+ }
267
+ var kindFromValue = (ctx) => {
268
+ const t = typeof ctx.default;
269
+ if (t === "boolean" || t === "number" || t === "string") return { kind: t };
270
+ return null;
271
+ };
272
+ var labelFromKey = (ctx) => ({ name: titleCase(ctx.key) });
273
+ var sliderWhenBounded = (ctx) => {
274
+ if (ctx.leaf.kind !== "number") return null;
275
+ const bounded = ctx.leaf.min !== void 0 && ctx.leaf.max !== void 0;
276
+ return bounded ? { control: "slider" } : null;
277
+ };
278
+ var descriptionDefault = () => ({ description: "" });
279
+ var builtinRules = [
280
+ kindFromValue,
281
+ labelFromKey,
282
+ sliderWhenBounded,
283
+ descriptionDefault
284
+ ];
285
+ function applyRules(seed, ctx, rules) {
286
+ const acc = { ...seed };
287
+ for (const rule of rules) {
288
+ const patch = rule({ ...ctx, leaf: acc });
289
+ if (patch === null) continue;
290
+ for (const [k, v] of Object.entries(patch)) {
291
+ if (v !== void 0 && acc[k] === void 0) {
292
+ acc[k] = v;
293
+ }
294
+ }
295
+ }
296
+ return acc;
297
+ }
298
+
299
+ // src/config/resolve.ts
300
+ function defined(annotations) {
301
+ const out = {};
302
+ for (const [k, v] of Object.entries(annotations)) if (v !== void 0) out[k] = v;
303
+ return out;
304
+ }
305
+ function resolveConfigSchema(schema, rules = []) {
306
+ const children = {};
307
+ const sectionOrder = [];
308
+ const sectionPaths = /* @__PURE__ */ new Map();
309
+ const showIf = /* @__PURE__ */ new Map();
310
+ const renderers = {};
311
+ const chain2 = [...rules, ...builtinRules];
312
+ for (const [key, node] of Object.entries(schema.nodes)) {
313
+ const seed = defined({
314
+ ...node.annotations,
315
+ ...node.kind === null ? {} : { kind: node.kind }
316
+ });
317
+ const patch = applyRules(seed, { key, path: key, default: node.default }, chain2);
318
+ children[key] = { ...patch, default: node.default };
319
+ const { section, showIf: predicate, render } = node.options;
320
+ if (section !== void 0) {
321
+ if (!sectionPaths.has(section)) {
322
+ sectionOrder.push(section);
323
+ sectionPaths.set(section, []);
324
+ }
325
+ sectionPaths.get(section)?.push(key);
326
+ }
327
+ if (predicate) showIf.set(key, predicate);
328
+ if (render) renderers[key] = render;
329
+ }
330
+ const group = { name: "", children };
331
+ const sections = sectionOrder.map((label) => ({
332
+ label,
333
+ paths: sectionPaths.get(label) ?? []
334
+ }));
335
+ return { group, sections, showIf, renderers };
336
+ }
337
+ var LabContext = createContext(null);
338
+ function useLabContext() {
339
+ const ctx = useContext(LabContext);
340
+ if (ctx === null) {
341
+ throw new Error("useLabContext must be used inside <Lab>");
342
+ }
343
+ return ctx;
344
+ }
345
+
346
+ // src/config/useConfigSchema.ts
347
+ var NO_RULES = [];
348
+ function useConfigSchema(instrument) {
349
+ const lab = useContext(LabContext);
350
+ const rules = lab?.configRules ?? NO_RULES;
351
+ return useMemo(
352
+ () => instrument.config ? resolveConfigSchema(instrument.config, rules) : fromConfigFields(instrument.configSchema?.() ?? []),
353
+ [instrument, rules]
354
+ );
117
355
  }
118
356
 
119
357
  // src/instrument/defineInstrument.ts
120
358
  function defineInstrument(spec) {
359
+ if (spec.config && !("defaultConfig" in spec && spec.defaultConfig)) {
360
+ const schema = spec.config;
361
+ return { ...spec, defaultConfig: () => schema.defaults() };
362
+ }
121
363
  return spec;
122
364
  }
123
365
 
124
366
  // src/instrument/validateConfigSchema.ts
125
- var KNOWN_TYPES = ["slider", "checkbox", "select", "number", "text", "color"];
126
367
  function validateConfigSchema(fields) {
127
368
  const errors = [];
128
369
  const seenKeys = /* @__PURE__ */ new Set();
@@ -137,10 +378,6 @@ function validateConfigSchema(fields) {
137
378
  if (field.label === "") {
138
379
  errors.push(`Field "${field.key}" has empty label`);
139
380
  }
140
- if (!KNOWN_TYPES.includes(field.type)) {
141
- errors.push(`Unknown field type: "${field.type}" on key "${field.key}"`);
142
- continue;
143
- }
144
381
  if (field.type === "slider") {
145
382
  if (!Number.isFinite(field.default)) {
146
383
  errors.push(`Field "${field.key}": default must be a finite number`);
@@ -175,7 +412,7 @@ function validateConfigSchema(fields) {
175
412
  return { valid: errors.length === 0, errors };
176
413
  }
177
414
 
178
- // ../theme/dist/chunk-E3FOFGMD.js
415
+ // ../theme/dist/chunk-O32P3G22.js
179
416
  var THEME_SOURCES = {
180
417
  "weasel": {
181
418
  "primitives": {
@@ -361,6 +598,15 @@ var THEME_SOURCES = {
361
598
  "type": "dimension",
362
599
  "value": "5px"
363
600
  },
601
+ "radius-lg": {
602
+ "type": "dimension",
603
+ "value": "14px"
604
+ },
605
+ "radius-pill": {
606
+ "type": "dimension",
607
+ "value": "999px",
608
+ "description": "Fully rounded ends \u2014 segmented controls, progress tracks."
609
+ },
364
610
  "border-w": {
365
611
  "type": "dimension",
366
612
  "value": "1px"
@@ -378,29 +624,47 @@ var THEME_SOURCES = {
378
624
  "tb-height": {
379
625
  "type": "dimension",
380
626
  "value": "28px",
381
- "description": "Height of a horizontal toolbar strip (ActionsBar, OptionsBar, ToggleBar, ToolOptionsBar)."
382
- },
383
- "radius-lg": {
384
- "type": "dimension",
385
- "value": "14px"
627
+ "description": "Height of a horizontal toolbar strip (ActionsBar, OptionsBar, ToolOptionsBar). Taller than control-h so the controls inside a strip are not flush with its edges."
386
628
  },
387
629
  "control-h": {
388
630
  "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."
631
+ "value": "24px",
632
+ "description": "Height of an interactive control \u2014 button, input, select, segmented control. Distinct from tb-height, which sizes the strip a row of them sits in."
391
633
  },
392
634
  "glass-blur": {
393
635
  "type": "dimension",
394
636
  "value": "3px",
395
637
  "description": "Backdrop blur radius for frosted surfaces. Pairs with glass-tint."
396
638
  },
397
- "font-size": {
639
+ "font-size-2xs": {
640
+ "type": "dimension",
641
+ "value": "9px",
642
+ "description": "Shortcut keys and index badges. The floor for chrome text."
643
+ },
644
+ "font-size-xs": {
398
645
  "type": "dimension",
399
- "value": "13px"
646
+ "value": "10px",
647
+ "description": "Tool-button labels under an icon."
400
648
  },
401
649
  "font-size-sm": {
402
650
  "type": "dimension",
403
- "value": "11px"
651
+ "value": "11px",
652
+ "description": "Chrome labels, status readouts, section headings."
653
+ },
654
+ "font-size": {
655
+ "type": "dimension",
656
+ "value": "13px",
657
+ "description": "Controls and body text."
658
+ },
659
+ "font-size-lg": {
660
+ "type": "dimension",
661
+ "value": "16px",
662
+ "description": "Panel and dialog titles."
663
+ },
664
+ "font-size-xl": {
665
+ "type": "dimension",
666
+ "value": "20px",
667
+ "description": "The largest chrome text \u2014 a lab or app title."
404
668
  },
405
669
  "space-xs": {
406
670
  "type": "dimension",
@@ -418,6 +682,20 @@ var THEME_SOURCES = {
418
682
  "type": "dimension",
419
683
  "value": "16px"
420
684
  },
685
+ "tracking-none": {
686
+ "type": "dimension",
687
+ "value": "0"
688
+ },
689
+ "tracking-wide": {
690
+ "type": "dimension",
691
+ "value": "0.06em",
692
+ "description": "Uppercase section headings."
693
+ },
694
+ "tracking-wider": {
695
+ "type": "dimension",
696
+ "value": "0.08em",
697
+ "description": "Uppercase at 11px and below."
698
+ },
421
699
  "z-toolbar": {
422
700
  "type": "number",
423
701
  "value": 10
@@ -430,6 +708,21 @@ var THEME_SOURCES = {
430
708
  "type": "number",
431
709
  "value": 30
432
710
  },
711
+ "leading-tight": {
712
+ "type": "number",
713
+ "value": 1,
714
+ "description": "Single-line controls; the box sets the height."
715
+ },
716
+ "leading-snug": {
717
+ "type": "number",
718
+ "value": 1.2,
719
+ "description": "Headings and two-line labels."
720
+ },
721
+ "leading": {
722
+ "type": "number",
723
+ "value": 1.4,
724
+ "description": "Body and anything that wraps."
725
+ },
433
726
  "backdrop": {
434
727
  "type": "gradient",
435
728
  "value": "none",
@@ -582,6 +875,16 @@ var THEME_SOURCES = {
582
875
  "type": "color",
583
876
  "value": "{color.gray-900}",
584
877
  "description": "Text drawn on a filled foreground-colored element (e.g. a slider thumb). Flips with the mode, so it is a semantic, not an alias to a primitive."
878
+ },
879
+ "shadow": {
880
+ "type": "color",
881
+ "value": "rgba(0, 0, 0, 0.6)",
882
+ "description": "Elevation. Always darker than any surface \u2014 never derived from fg, which is near-white on dark."
883
+ },
884
+ "border-raised": {
885
+ "type": "color",
886
+ "value": "{color.gray-400}",
887
+ "description": "The line around a raised surface. Clears 3:1 against surface and surface-raised in either mode, which the general-purpose border does not; over surface-sunken it does not, so a panel on a sunken field needs its own answer."
585
888
  }
586
889
  },
587
890
  "light": {
@@ -626,6 +929,14 @@ var THEME_SOURCES = {
626
929
  "type": "color",
627
930
  "value": "{color.gray-50}",
628
931
  "description": "Flips with the mode \u2014 see the dark layer."
932
+ },
933
+ "shadow": {
934
+ "type": "color",
935
+ "value": "rgba(0, 0, 0, 0.18)"
936
+ },
937
+ "border-raised": {
938
+ "type": "color",
939
+ "value": "{color.gray-400}"
629
940
  }
630
941
  }
631
942
  }
@@ -643,10 +954,10 @@ function hexToRgba(hex, alpha) {
643
954
  if (!m) throw new Error(`Expected a hex color, got "${hex}"`);
644
955
  const body = m[1];
645
956
  const full = body.length === 3 ? body.replace(/./g, (c) => c + c) : body;
646
- const r = Number.parseInt(full.slice(0, 2), 16);
957
+ const r2 = Number.parseInt(full.slice(0, 2), 16);
647
958
  const g = Number.parseInt(full.slice(2, 4), 16);
648
959
  const b = Number.parseInt(full.slice(4, 6), 16);
649
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
960
+ return `rgba(${r2}, ${g}, ${b}, ${alpha})`;
650
961
  }
651
962
  var REF = /^\{([^}]+)\}$/;
652
963
  function refTarget(value) {
@@ -658,7 +969,7 @@ function refTarget(value) {
658
969
  return dot === -1 ? path : path.slice(dot + 1);
659
970
  }
660
971
  function fontStack(value) {
661
- return value.map((f) => typeof f === "string" && /\s/.test(f) ? `'${f}'` : String(f)).join(", ");
972
+ return value.map((f2) => typeof f2 === "string" && /\s/.test(f2) ? `'${f2}'` : String(f2)).join(", ");
662
973
  }
663
974
  function serialize(type, value) {
664
975
  if (Array.isArray(value)) {
@@ -865,7 +1176,9 @@ var interstellar_tokens_default = {
865
1176
  "fg-subtle": { $value: "#5a5a78" },
866
1177
  accent: { $value: "#b08adb" },
867
1178
  "accent-hover": { $value: "#c6a3ee" },
868
- "focus-ring": { $value: "rgba(176, 138, 219, 0.45)" }
1179
+ "focus-ring": { $value: "rgba(176, 138, 219, 0.45)" },
1180
+ shadow: { $value: "rgba(0, 0, 0, 0.7)" },
1181
+ "border-raised": { $value: "rgba(255, 255, 255, 0.36)" }
869
1182
  },
870
1183
  gradient: {
871
1184
  $type: "gradient",
@@ -887,7 +1200,9 @@ var interstellar_tokens_default = {
887
1200
  "fg-subtle": { $value: "#a89b85" },
888
1201
  accent: { $value: "#a86f3c" },
889
1202
  "accent-hover": { $value: "#c2854f" },
890
- "focus-ring": { $value: "rgba(168, 111, 60, 0.35)" }
1203
+ "focus-ring": { $value: "rgba(168, 111, 60, 0.35)" },
1204
+ shadow: { $value: "rgba(80, 60, 40, 0.18)" },
1205
+ "border-raised": { $value: "#9e8b71" }
891
1206
  }
892
1207
  }
893
1208
  }
@@ -920,13 +1235,13 @@ function useDragDrop({
920
1235
  const [drag, setDrag] = useState(null);
921
1236
  const dragRef = useRef(null);
922
1237
  dragRef.current = drag;
923
- const rafThrottle = useRef(null);
1238
+ const pendingPos = useRef(null);
924
1239
  const isOverCanvas = useCallback(
925
1240
  (screenPos) => {
926
1241
  const el = canvasContainerRef.current;
927
1242
  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;
1243
+ const r2 = el.getBoundingClientRect();
1244
+ return screenPos.x >= r2.left && screenPos.x <= r2.right && screenPos.y >= r2.top && screenPos.y <= r2.bottom;
930
1245
  },
931
1246
  [canvasContainerRef]
932
1247
  );
@@ -934,42 +1249,44 @@ function useDragDrop({
934
1249
  (screenPos) => {
935
1250
  const el = canvasContainerRef.current;
936
1251
  if (!el) return null;
937
- const r = el.getBoundingClientRect();
938
- return screenToWorld({ x: screenPos.x - r.left, y: screenPos.y - r.top }, view);
1252
+ const r2 = el.getBoundingClientRect();
1253
+ return screenToWorld({ x: screenPos.x - r2.left, y: screenPos.y - r2.top }, view);
939
1254
  },
940
1255
  [canvasContainerRef, view]
941
1256
  );
1257
+ const frameLoop = useVisibleRaf(() => {
1258
+ const screenPos = pendingPos.current;
1259
+ pendingPos.current = null;
1260
+ const active = dragRef.current;
1261
+ if (!active || !screenPos) return;
1262
+ let feedback = null;
1263
+ if (capability.onDragOver && isOverCanvas(screenPos)) {
1264
+ const world = screenToWorldFromContainer(screenPos);
1265
+ if (world) feedback = capability.onDragOver(world, active.item, state, config);
1266
+ }
1267
+ setDrag({ ...active, screenPos, feedback });
1268
+ });
942
1269
  useEffect(() => {
943
1270
  if (!drag) return;
944
1271
  const handleMove = (e) => {
945
1272
  const screenPos = { x: e.clientX, y: e.clientY };
946
1273
  const current = dragRef.current;
947
1274
  if (!current) return;
948
- if (rafThrottle.current !== null) {
1275
+ if (pendingPos.current !== null) {
1276
+ pendingPos.current = screenPos;
949
1277
  dragRef.current = { ...current, screenPos };
950
1278
  setDrag(dragRef.current);
951
1279
  return;
952
1280
  }
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
- });
1281
+ pendingPos.current = screenPos;
1282
+ frameLoop.request();
964
1283
  };
965
1284
  const handleUp = (e) => {
966
1285
  const screenPos = { x: e.clientX, y: e.clientY };
967
1286
  const active = dragRef.current;
968
1287
  if (!active) return;
969
- if (rafThrottle.current !== null) {
970
- cancelAnimationFrame(rafThrottle.current);
971
- rafThrottle.current = null;
972
- }
1288
+ pendingPos.current = null;
1289
+ frameLoop.cancel();
973
1290
  if (isOverCanvas(screenPos)) {
974
1291
  const world = screenToWorldFromContainer(screenPos);
975
1292
  if (world) {
@@ -986,7 +1303,17 @@ function useDragDrop({
986
1303
  window.removeEventListener("pointermove", handleMove);
987
1304
  window.removeEventListener("pointerup", handleUp);
988
1305
  };
989
- }, [drag, capability, state, config, setState, emit, isOverCanvas, screenToWorldFromContainer]);
1306
+ }, [
1307
+ drag,
1308
+ capability,
1309
+ state,
1310
+ config,
1311
+ setState,
1312
+ emit,
1313
+ frameLoop,
1314
+ isOverCanvas,
1315
+ screenToWorldFromContainer
1316
+ ]);
990
1317
  const startDrag = useCallback((item, originScreenPos) => {
991
1318
  setDrag({ item, screenPos: originScreenPos, feedback: null });
992
1319
  }, []);
@@ -1011,14 +1338,6 @@ function Palette({ items, onDragStart, className }) {
1011
1338
  item.id
1012
1339
  )) });
1013
1340
  }
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
1341
 
1023
1342
  // src/state/view.ts
1024
1343
  var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
@@ -1030,112 +1349,40 @@ function as2DView(view) {
1030
1349
  if (typeof v.pan.x !== "number" || typeof v.pan.y !== "number") return null;
1031
1350
  return { zoom: v.zoom, pan: { x: v.pan.x, y: v.pan.y } };
1032
1351
  }
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;
1352
+ var TrialDragContext = createContext(null);
1353
+ function useTrialDrag() {
1354
+ return useContext(TrialDragContext);
1056
1355
  }
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) })
1356
+ function Draggable({ nodeId, title, children }) {
1357
+ const handlers = useDragHandle(nodeId);
1358
+ return /* @__PURE__ */ jsxs("div", { className: "lk-trial__titlebar lk-trial__titlebar--draggable", ...handlers, children: [
1359
+ /* @__PURE__ */ jsx("span", { className: "lk-trial__title", children: title }),
1360
+ children
1070
1361
  ] });
1071
1362
  }
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
- )
1363
+ function TrialTitleBar({ title, children }) {
1364
+ const drag = useTrialDrag();
1365
+ if (drag)
1366
+ return /* @__PURE__ */ jsx(Draggable, { nodeId: drag.nodeId, title, children });
1367
+ return /* @__PURE__ */ jsxs("div", { className: "lk-trial__titlebar", children: [
1368
+ /* @__PURE__ */ jsx("span", { className: "lk-trial__title", children: title }),
1369
+ children
1126
1370
  ] });
1127
1371
  }
1372
+ var NO_OP = () => {
1373
+ };
1128
1374
  function TrialChrome({
1129
1375
  trialId,
1130
1376
  record,
1131
1377
  instrument,
1132
1378
  isLastTrial,
1133
- toolbar,
1134
- sidebar,
1135
- statusBar,
1136
1379
  undoBindings,
1137
1380
  job,
1138
- sidebarExtras,
1381
+ trialChrome,
1382
+ chrome,
1383
+ suppress,
1384
+ activeToolId = null,
1385
+ setActiveTool = NO_OP,
1139
1386
  children
1140
1387
  }) {
1141
1388
  const lab = useLabContext();
@@ -1145,7 +1392,8 @@ function TrialChrome({
1145
1392
  const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
1146
1393
  const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
1147
1394
  const view2d = as2DView(record.view);
1148
- const toolbarCtx = useMemo(() => {
1395
+ const configSchema = useConfigSchema(instrument);
1396
+ const ctx = useMemo(() => {
1149
1397
  const setZoom = (z) => {
1150
1398
  if (!view2d) return;
1151
1399
  updateTrialView(trialId, { ...view2d, zoom: z });
@@ -1153,33 +1401,15 @@ function TrialChrome({
1153
1401
  return {
1154
1402
  trialId,
1155
1403
  instrumentName: record.instrumentName,
1156
- hasUndo: instrument.undo != null,
1404
+ isLastTrial,
1405
+ zoom: view2d ? view2d.zoom : null,
1406
+ setZoom,
1157
1407
  canUndo: undoBindings?.canUndo ?? false,
1158
1408
  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?.() ?? [],
1409
+ undo: undoBindings?.undo ?? NO_OP,
1410
+ redo: undoBindings?.redo ?? NO_OP,
1411
+ configSchema,
1412
+ configFields: instrument.config ? [] : instrument.configSchema?.() ?? [],
1183
1413
  config: record.config,
1184
1414
  setConfig: (key, value) => {
1185
1415
  const prevConfig = record.config;
@@ -1194,25 +1424,54 @@ function TrialChrome({
1194
1424
  const nextState = instrument.onConfigChange(nextConfig, prevConfig, record.state);
1195
1425
  updateTrialState(trialId, nextState);
1196
1426
  }
1197
- }
1198
- }),
1199
- [trialId, record, instrument, updateTrialConfig, updateTrialState]
1200
- );
1201
- const statusCtx = {
1427
+ },
1428
+ savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
1429
+ saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
1430
+ loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
1431
+ clone: () => lab.cloneTrial(trialId),
1432
+ reset: () => lab.resetTrial(trialId),
1433
+ close: () => lab.closeTrial(trialId),
1434
+ activeToolId,
1435
+ setActiveTool
1436
+ };
1437
+ }, [
1202
1438
  trialId,
1203
- instrumentName: record.instrumentName,
1204
- zoom: view2d ? view2d.zoom : null
1205
- };
1439
+ record,
1440
+ instrument,
1441
+ lab,
1442
+ isLastTrial,
1443
+ updateTrialView,
1444
+ updateTrialConfig,
1445
+ updateTrialState,
1446
+ undoBindings,
1447
+ view2d,
1448
+ activeToolId,
1449
+ setActiveTool,
1450
+ configSchema
1451
+ ]);
1452
+ const contributions = useMemo(
1453
+ () => suppressContributions(
1454
+ mergeContributions(
1455
+ builtinContributions(instrument, ctx, lab.controls),
1456
+ [...instrument.chrome ?? []],
1457
+ [...trialChrome ?? []],
1458
+ [...chrome ?? []]
1459
+ ),
1460
+ suppress ?? []
1461
+ ),
1462
+ [instrument, ctx, trialChrome, chrome, suppress, lab.controls]
1463
+ );
1464
+ const inRegion = (region) => contributions.filter((c) => c.region === region);
1206
1465
  const handleKeyDown = (e) => {
1207
1466
  const mod = e.metaKey || e.ctrlKey;
1208
1467
  if (!mod) return;
1209
1468
  if (e.key === "z" || e.key === "Z") {
1210
1469
  e.preventDefault();
1211
- if (e.shiftKey) toolbarCtx.redo();
1212
- else toolbarCtx.undo();
1470
+ if (e.shiftKey) ctx.redo();
1471
+ else ctx.undo();
1213
1472
  } else if (e.key === "s" || e.key === "S") {
1214
1473
  e.preventDefault();
1215
- toolbarCtx.saveSnapshot();
1474
+ ctx.saveSnapshot();
1216
1475
  }
1217
1476
  };
1218
1477
  return /* @__PURE__ */ jsxs(
@@ -1220,36 +1479,28 @@ function TrialChrome({
1220
1479
  {
1221
1480
  className: "lk-trial",
1222
1481
  "aria-label": `Trial ${record.instrumentName}`,
1482
+ tabIndex: -1,
1223
1483
  onKeyDown: handleKeyDown,
1224
1484
  children: [
1225
- /* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: toolbar ? toolbar(toolbarCtx) : /* @__PURE__ */ jsx(DefaultToolbar, { ctx: toolbarCtx }) }),
1485
+ /* @__PURE__ */ jsx(TrialTitleBar, { title: record.instrumentName, children: /* @__PURE__ */ jsx(TitleBarRegion, { contributions: inRegion("titlebar"), ctx }) }),
1486
+ /* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: /* @__PURE__ */ jsx(ToolbarRegion, { contributions: inRegion("toolbar"), ctx }) }),
1226
1487
  /* @__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 })
1488
+ /* @__PURE__ */ jsx(PaletteRegion, { contributions: inRegion("palette"), ctx }),
1489
+ /* @__PURE__ */ jsx("div", { className: "lk-trial__sidebar", children: /* @__PURE__ */ jsx(SidebarRegion, { contributions: inRegion("sidebar"), ctx }) }),
1490
+ /* @__PURE__ */ jsxs("div", { className: `lk-trial__content${instrument.canvas ? " lk-trial__content--flush" : ""}`, children: [
1491
+ children,
1492
+ /* @__PURE__ */ jsx(ViewportRegion, { contributions: inRegion("viewport"), ctx })
1493
+ ] })
1232
1494
  ] }),
1233
1495
  /* @__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 })
1496
+ job ? /* @__PURE__ */ jsx(JobProgress, { job }) : null,
1497
+ /* @__PURE__ */ jsx(StatusRegion, { contributions: inRegion("status"), ctx })
1247
1498
  ] })
1248
1499
  ]
1249
1500
  }
1250
1501
  );
1251
1502
  }
1252
- function Trial({ id }) {
1503
+ function Trial({ id, chrome, suppress }) {
1253
1504
  const lab = useLabContext();
1254
1505
  const storeCtx = useContext(LabStoreContext);
1255
1506
  if (!storeCtx) throw new Error("[labkit] <Trial> requires <LabStoreProvider>");
@@ -1273,16 +1524,21 @@ function Trial({ id }) {
1273
1524
  record,
1274
1525
  instrument,
1275
1526
  store: storeCtx.store,
1276
- isLast: lab.trials.length <= 1
1527
+ isLast: lab.trials.length <= 1,
1528
+ chrome,
1529
+ suppress
1277
1530
  }
1278
1531
  );
1279
1532
  }
1280
- function TrialRuntime({ record, instrument, store, isLast }) {
1533
+ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1281
1534
  const canvasContainerRef = useRef(null);
1282
1535
  const updateTrialState = useStore(store, (s) => s.updateTrialState);
1283
1536
  const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
1284
1537
  const updateTrialView = useStore(store, (s) => s.updateTrialView);
1285
1538
  const updateTrialUndoStack = useStore(store, (s) => s.updateTrialUndoStack);
1539
+ const labToolId = useStore(store, (s) => s.activeToolId);
1540
+ const setLabTool = useStore(store, (s) => s.setLabTool);
1541
+ const setTrialTool = useStore(store, (s) => s.setTrialTool);
1286
1542
  const busRef = useRef(null);
1287
1543
  if (busRef.current === null) busRef.current = createEventBus();
1288
1544
  const bus = busRef.current;
@@ -1299,6 +1555,12 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1299
1555
  updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
1300
1556
  };
1301
1557
  const setView = (v) => updateTrialView(record.id, v);
1558
+ const declaresTools = instrument.tools != null;
1559
+ const resolvedToolId = declaresTools ? record.activeToolId ?? instrument.tools?.initial ?? instrument.tools?.tools[0]?.id ?? null : labToolId;
1560
+ const setActiveTool = (id) => {
1561
+ if (declaresTools) setTrialTool(record.id, id);
1562
+ else setLabTool(id);
1563
+ };
1302
1564
  const view2d = as2DView(record.view);
1303
1565
  const jobCap = instrument.job;
1304
1566
  const job = useJob({
@@ -1332,7 +1594,8 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1332
1594
  setZoom: (z) => {
1333
1595
  if (!view2d) return;
1334
1596
  updateTrialView(record.id, { ...view2d, zoom: z });
1335
- }
1597
+ },
1598
+ activeToolId: resolvedToolId
1336
1599
  },
1337
1600
  emit: (event) => {
1338
1601
  snapshotIfNeeded(event);
@@ -1372,7 +1635,7 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1372
1635
  }, [instrument.canvas, record.state, record.config, layerVisibility, layerOrder]);
1373
1636
  const layerDescriptors = useMemo(() => {
1374
1637
  if (!instrument.layers) return [];
1375
- return instrument.layers.ids.map((lid) => ({ id: lid, label: lid }));
1638
+ return instrument.layers.ids.map((l) => typeof l === "string" ? { id: l, label: l } : l);
1376
1639
  }, [instrument.layers]);
1377
1640
  const dragDropResult = useDragDrop({
1378
1641
  capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
@@ -1391,8 +1654,8 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1391
1654
  });
1392
1655
  const paletteItems = useMemo(() => {
1393
1656
  if (!instrument.dragDrop) return [];
1394
- const p2 = instrument.dragDrop.palette;
1395
- return typeof p2 === "function" ? p2(record.state, record.config) : p2;
1657
+ const p3 = instrument.dragDrop.palette;
1658
+ return typeof p3 === "function" ? p3(record.state, record.config) : p3;
1396
1659
  }, [instrument.dragDrop, record.state, record.config]);
1397
1660
  const layersWithFeedback = useMemo(() => {
1398
1661
  if (!dragDropResult.drag?.feedback) return canvasLayers;
@@ -1406,12 +1669,12 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1406
1669
  render: (ctx) => {
1407
1670
  const el = canvasContainerRef.current;
1408
1671
  if (!el) return;
1409
- const r = el.getBoundingClientRect();
1672
+ const r2 = el.getBoundingClientRect();
1410
1673
  ctx.save();
1411
1674
  ctx.strokeStyle = fb.ok ? "#3a7" : "#c44";
1412
1675
  ctx.lineWidth = 2;
1413
1676
  ctx.beginPath();
1414
- ctx.arc(screen.x - r.left, screen.y - r.top, 16, 0, Math.PI * 2);
1677
+ ctx.arc(screen.x - r2.left, screen.y - r2.top, 16, 0, Math.PI * 2);
1415
1678
  ctx.stroke();
1416
1679
  ctx.restore();
1417
1680
  }
@@ -1420,45 +1683,64 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1420
1683
  }, [canvasLayers, dragDropResult.drag]);
1421
1684
  let body;
1422
1685
  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
- );
1686
+ body = /* @__PURE__ */ jsxs("div", { ref: canvasContainerRef, className: "lk-trial__canvas-host", children: [
1687
+ /* @__PURE__ */ jsx(
1688
+ CanvasStack,
1689
+ {
1690
+ layers: layersWithFeedback,
1691
+ view: view2d ?? DEFAULT_VIEW,
1692
+ onViewChange: setView,
1693
+ minZoom: instrument.canvas.minZoom,
1694
+ maxZoom: instrument.canvas.maxZoom,
1695
+ children: instrument.render(renderCtx)
1696
+ }
1697
+ ),
1698
+ /* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
1699
+ ] });
1443
1700
  } else {
1444
1701
  body = instrument.render(renderCtx);
1445
1702
  }
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");
1703
+ const startDrag = dragDropResult.startDrag;
1704
+ const paletteNode = useMemo(
1705
+ () => paletteItems.length > 0 ? /* @__PURE__ */ jsx(Palette, { items: paletteItems, onDragStart: startDrag }) : null,
1706
+ [paletteItems, startDrag]
1707
+ );
1708
+ const layerListNode = useMemo(
1709
+ () => instrument.layers && layerDescriptors.length > 0 ? /* @__PURE__ */ jsx(
1710
+ LayerList,
1711
+ {
1712
+ layers: layerDescriptors,
1713
+ visibility: layerVisibility,
1714
+ onReorder: (next) => {
1715
+ setLayerOrder(next.map((l) => l.id));
1716
+ bus.emit("layers.reorder");
1717
+ },
1718
+ onToggle: (lid, visible) => {
1719
+ setLayerVisibility((prev) => ({ ...prev, [lid]: visible }));
1720
+ bus.emit("layers.toggle");
1721
+ }
1459
1722
  }
1723
+ ) : null,
1724
+ [instrument.layers, layerDescriptors, layerVisibility, bus]
1725
+ );
1726
+ const extraChrome = useMemo(() => {
1727
+ const out = [];
1728
+ if (paletteNode) {
1729
+ out.push({
1730
+ id: "dragdrop-palette",
1731
+ region: "sidebar",
1732
+ item: { title: "Parts", body: paletteNode }
1733
+ });
1734
+ }
1735
+ if (layerListNode) {
1736
+ out.push({
1737
+ id: "layer-list",
1738
+ region: "sidebar",
1739
+ item: { title: "Layers", body: layerListNode }
1740
+ });
1460
1741
  }
1461
- ) : null;
1742
+ return out;
1743
+ }, [paletteNode, layerListNode]);
1462
1744
  return /* @__PURE__ */ jsx(
1463
1745
  TrialChrome,
1464
1746
  {
@@ -1468,10 +1750,11 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1468
1750
  instrument,
1469
1751
  isLastTrial: isLast,
1470
1752
  undoBindings,
1471
- sidebarExtras: /* @__PURE__ */ jsxs(Fragment, { children: [
1472
- paletteNode,
1473
- layerListNode
1474
- ] }),
1753
+ trialChrome: extraChrome,
1754
+ chrome,
1755
+ suppress,
1756
+ activeToolId: resolvedToolId,
1757
+ setActiveTool,
1475
1758
  children: body
1476
1759
  }
1477
1760
  );
@@ -1544,6 +1827,74 @@ function reorderTrials(trials, ids) {
1544
1827
  const rest = trials.filter((w) => !seen.has(w.id));
1545
1828
  return [...named, ...rest];
1546
1829
  }
1830
+ var MODES = [
1831
+ { value: "auto", label: "Auto" },
1832
+ { value: "light", label: "Light" },
1833
+ { value: "dark", label: "Dark" }
1834
+ ];
1835
+ function LabHeader() {
1836
+ const lab = useLabContext();
1837
+ const only = lab.instruments.length === 1 ? lab.instruments[0] : null;
1838
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1839
+ only ? /* @__PURE__ */ jsxs(
1840
+ "button",
1841
+ {
1842
+ type: "button",
1843
+ className: "lk-lab-header__add",
1844
+ onClick: () => lab.addTrial(only.name),
1845
+ children: [
1846
+ /* @__PURE__ */ jsx(vt, { size: 16 }),
1847
+ /* @__PURE__ */ jsx("span", { children: "Add trial" })
1848
+ ]
1849
+ }
1850
+ ) : (
1851
+ // Held at null so the control stays an "add one" action rather than
1852
+ // reading as the current instrument.
1853
+ /* @__PURE__ */ jsx(
1854
+ p2,
1855
+ {
1856
+ className: "lk-lab-header__add-select",
1857
+ "aria-label": "Add trial",
1858
+ placeholder: "Add trial\u2026",
1859
+ selectedKey: null,
1860
+ options: lab.instruments.map((i) => ({ value: i.name, label: i.name })),
1861
+ onSelectionChange: (name) => {
1862
+ if (name != null) lab.addTrial(String(name));
1863
+ }
1864
+ }
1865
+ )
1866
+ ),
1867
+ /* @__PURE__ */ jsx(
1868
+ r,
1869
+ {
1870
+ className: "lk-lab-header__mode",
1871
+ ariaLabel: "Color mode",
1872
+ items: MODES,
1873
+ value: lab.mode,
1874
+ onChange: (next) => {
1875
+ if (next) lab.setMode(next);
1876
+ }
1877
+ }
1878
+ )
1879
+ ] });
1880
+ }
1881
+ function LabPalette({ tools }) {
1882
+ const storeCtx = useContext(LabStoreContext);
1883
+ if (!storeCtx) throw new Error("[labkit] LabPalette requires <LabStoreProvider>");
1884
+ const activeToolId = useStore(storeCtx.store, (s) => s.activeToolId);
1885
+ const setLabTool = useStore(storeCtx.store, (s) => s.setLabTool);
1886
+ const contributions = tools.map((t) => ({
1887
+ id: t.id,
1888
+ region: "palette",
1889
+ group: t.group,
1890
+ item: { icon: t.icon, label: t.label, shortcut: t.shortcut }
1891
+ }));
1892
+ const ctx = {
1893
+ activeToolId,
1894
+ setActiveTool: setLabTool
1895
+ };
1896
+ return /* @__PURE__ */ jsx(PaletteRegion, { contributions, ctx });
1897
+ }
1547
1898
  function query() {
1548
1899
  if (typeof window === "undefined" || !window.matchMedia) return null;
1549
1900
  return window.matchMedia("(prefers-color-scheme: light)");
@@ -1579,11 +1930,11 @@ var ZONE_ID = asNodeId("lk-workspace");
1579
1930
  var STRATEGIES = { grid: gridStrategy };
1580
1931
  var KIND = "trial";
1581
1932
  function extentOf(store, id) {
1582
- const p2 = store.getNode(id)?.membership?.placement;
1583
- if (!p2) return null;
1933
+ const p3 = store.getNode(id)?.membership?.placement;
1934
+ if (!p3) return null;
1584
1935
  const out = {};
1585
- if (p2.size) out.size = p2.size;
1586
- if (p2.span) out.span = p2.span;
1936
+ if (p3.size) out.size = p3.size;
1937
+ if (p3.span) out.span = p3.span;
1587
1938
  return Object.keys(out).length > 0 ? out : null;
1588
1939
  }
1589
1940
  function Workspace({
@@ -1663,10 +2014,10 @@ function Workspace({
1663
2014
  [KIND]: ({ node }) => {
1664
2015
  const content = byId.get(node.id) ?? null;
1665
2016
  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
- ] });
2017
+ return (
2018
+ // No grip strip: the trial's own title bar is the drag surface.
2019
+ /* @__PURE__ */ jsx(TrialDragContext.Provider, { value: { nodeId: node.id }, children: /* @__PURE__ */ jsx("div", { className: "lk-trial-tile", children: content }) })
2020
+ );
1670
2021
  }
1671
2022
  };
1672
2023
  }, [nodeIds, items, reorderable]);
@@ -1704,8 +2055,8 @@ var NEBULA_SLOTS = [
1704
2055
  ];
1705
2056
  function buildNebula(colors) {
1706
2057
  const blobs = colors.map((c, i) => {
1707
- 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})`;
2058
+ const p3 = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
2059
+ return `radial-gradient(ellipse ${p3.sx} ${p3.sy} at ${p3.cx} ${p3.cy}, color-mix(in srgb, ${c} 22%, transparent), transparent ${p3.stop})`;
1709
2060
  });
1710
2061
  blobs.push("radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%)");
1711
2062
  return blobs.join(", ");
@@ -1718,6 +2069,12 @@ function Lab({
1718
2069
  mode,
1719
2070
  nebula,
1720
2071
  title,
2072
+ footer,
2073
+ chrome,
2074
+ suppress,
2075
+ tools,
2076
+ configRules,
2077
+ controls,
1721
2078
  children
1722
2079
  }) {
1723
2080
  if (process.env.NODE_ENV !== "production" && instruments.length === 0) {
@@ -1782,7 +2139,8 @@ function Lab({
1782
2139
  },
1783
2140
  savedSnapshots,
1784
2141
  saveSnapshot: (trialId, name) => {
1785
- store.getState().saveSnapshot(trialId, name ?? `Save ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
2142
+ const taken = store.getState().savedSnapshots.filter((s) => s.trialId === trialId).length;
2143
+ store.getState().saveSnapshot(trialId, name ?? `Snapshot ${taken + 1}`);
1786
2144
  },
1787
2145
  loadSnapshot: (trialId, snapshotId) => {
1788
2146
  store.getState().loadSnapshot(snapshotId, trialId);
@@ -1793,9 +2151,11 @@ function Lab({
1793
2151
  mode: modeValue,
1794
2152
  setMode: (m) => {
1795
2153
  store.getState().setMode(m);
1796
- }
2154
+ },
2155
+ configRules,
2156
+ controls
1797
2157
  };
1798
- }, [instruments, trials, savedSnapshots, modeValue, store]);
2158
+ }, [instruments, trials, savedSnapshots, modeValue, store, configRules, controls]);
1799
2159
  const backdropStyle = resolvedMode === "dark" && nebula && nebula.length > 0 ? { ["--wzl-backdrop"]: buildNebula(nebula) } : void 0;
1800
2160
  return /* @__PURE__ */ jsx(LabStoreContext.Provider, { value: { store }, children: /* @__PURE__ */ jsx(LabContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
1801
2161
  ThemeProvider,
@@ -1804,18 +2164,33 @@ function Lab({
1804
2164
  mode: resolvedMode,
1805
2165
  className: "lk-lab",
1806
2166
  style: backdropStyle,
1807
- children: /* @__PURE__ */ jsx(LabShell, { title: title ?? "Labkit", mode: modeValue, header: children, children: /* @__PURE__ */ jsx(
1808
- Workspace,
2167
+ children: /* @__PURE__ */ jsx(
2168
+ LabShell,
1809
2169
  {
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))
2170
+ title: title ?? "Labkit",
2171
+ mode: modeValue,
2172
+ footer,
2173
+ header: /* @__PURE__ */ jsxs(Fragment, { children: [
2174
+ /* @__PURE__ */ jsx(LabHeader, {}),
2175
+ children
2176
+ ] }),
2177
+ children: /* @__PURE__ */ jsxs("div", { className: "lk-lab__body", children: [
2178
+ tools ? /* @__PURE__ */ jsx(LabPalette, { tools }) : null,
2179
+ /* @__PURE__ */ jsx(
2180
+ Workspace,
2181
+ {
2182
+ ids: trials.map((w) => w.id),
2183
+ resizable: true,
2184
+ reorderable: true,
2185
+ onReorder: (ids) => contextValue.reorderTrials(ids),
2186
+ layout,
2187
+ onLayoutChange: (next) => store.getState().setLayout(next),
2188
+ children: trials.map((w) => /* @__PURE__ */ jsx(Trial, { id: w.id, chrome, suppress }, w.id))
2189
+ }
2190
+ )
2191
+ ] })
1817
2192
  }
1818
- ) })
2193
+ )
1819
2194
  }
1820
2195
  ) }) });
1821
2196
  }
@@ -1860,7 +2235,7 @@ function CurveField({
1860
2235
  }
1861
2236
  flipped.sort((a, b) => a.x - b.x);
1862
2237
  const out = [];
1863
- for (const p2 of flipped) out.push(p2.x, p2.y);
2238
+ for (const p3 of flipped) out.push(p3.x, p3.y);
1864
2239
  onChange(out);
1865
2240
  }, [values, onChange]);
1866
2241
  const handleFlipVertical = useCallback(() => {
@@ -2164,268 +2539,7 @@ function EffectCardList({
2164
2539
  ] }, String(item.id));
2165
2540
  }) });
2166
2541
  }
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
- }
2261
- }
2262
- )
2263
- }
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
- }) }) });
2427
- }
2428
2542
 
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 };
2543
+ export { CurveField, DEFAULT_VIEW, EffectCard, EffectCardList, Lab, LabContext, LabPalette, LabShell, PITCH_LIMIT, Subpanel, Trial, TrialChrome, Workspace, addTrial, applyRules, as2DView, builtinRules, clampPitch, cloneTrial, closeTrial, defineInstrument, f, interstellarTheme, orbitAfterDrag, orbitAfterWheel, reorderTrials, resetTrial, resolveConfigSchema, titleCase, useConfigSchema, useLabContext, useOrbit, validateConfigSchema, wrapYaw };
2430
2544
  //# sourceMappingURL=index.js.map
2431
2545
  //# sourceMappingURL=index.js.map