@weasel-js/labkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/README.md +72 -0
  2. package/dist/_dts/fitViewToBounds-rWrBI4a6.d.ts +870 -0
  3. package/dist/_dts/index-C6Yze7sQ.d.ts +93 -0
  4. package/dist/_dts/types-Si4Fw-1F.d.ts +48 -0
  5. package/dist/_dts/useExperimentState-eSvpwV_P.d.ts +102 -0
  6. package/dist/canvas/index.d.ts +34 -0
  7. package/dist/canvas/index.js +4 -0
  8. package/dist/canvas/index.js.map +1 -0
  9. package/dist/chunk-2QNYYL3V.js +86 -0
  10. package/dist/chunk-2QNYYL3V.js.map +1 -0
  11. package/dist/chunk-54IQ2DX7.js +222 -0
  12. package/dist/chunk-54IQ2DX7.js.map +1 -0
  13. package/dist/chunk-6FO7G5BB.js +6095 -0
  14. package/dist/chunk-6FO7G5BB.js.map +1 -0
  15. package/dist/chunk-7BKDG73Z.js +54 -0
  16. package/dist/chunk-7BKDG73Z.js.map +1 -0
  17. package/dist/chunk-B5ZUYRKN.js +23330 -0
  18. package/dist/chunk-B5ZUYRKN.js.map +1 -0
  19. package/dist/chunk-HXZHVU4G.js +126 -0
  20. package/dist/chunk-HXZHVU4G.js.map +1 -0
  21. package/dist/chunk-PWC7AQZM.js +8 -0
  22. package/dist/chunk-PWC7AQZM.js.map +1 -0
  23. package/dist/chunk-SD3HDXR7.js +161 -0
  24. package/dist/chunk-SD3HDXR7.js.map +1 -0
  25. package/dist/chunk-SSMHPMDV.js +350 -0
  26. package/dist/chunk-SSMHPMDV.js.map +1 -0
  27. package/dist/chunk-VLAHRJOC.js +201 -0
  28. package/dist/chunk-VLAHRJOC.js.map +1 -0
  29. package/dist/controls/index.d.ts +13 -0
  30. package/dist/controls/index.js +3 -0
  31. package/dist/controls/index.js.map +1 -0
  32. package/dist/dragdrop/index.d.ts +3 -0
  33. package/dist/dragdrop/index.js +3 -0
  34. package/dist/dragdrop/index.js.map +1 -0
  35. package/dist/fonts/oswald-latin-variable.woff2 +0 -0
  36. package/dist/index.d.ts +410 -0
  37. package/dist/index.js +1425 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/layers/index.d.ts +17 -0
  40. package/dist/layers/index.js +3 -0
  41. package/dist/layers/index.js.map +1 -0
  42. package/dist/passthrough/weasel-canvas.d.ts +258 -0
  43. package/dist/passthrough/weasel-canvas.js +3 -0
  44. package/dist/passthrough/weasel-canvas.js.map +1 -0
  45. package/dist/passthrough/weasel-ui.d.ts +2826 -0
  46. package/dist/passthrough/weasel-ui.js +4 -0
  47. package/dist/passthrough/weasel-ui.js.map +1 -0
  48. package/dist/primitives/index.d.ts +57 -0
  49. package/dist/primitives/index.js +4 -0
  50. package/dist/primitives/index.js.map +1 -0
  51. package/dist/state/index.d.ts +33 -0
  52. package/dist/state/index.js +34 -0
  53. package/dist/state/index.js.map +1 -0
  54. package/dist/styles.css +1224 -0
  55. package/dist/theme-interstellar.css +25 -0
  56. package/dist/theme-light.css +13 -0
  57. package/dist/ui/layers/index.d.ts +42 -0
  58. package/dist/ui/layers/index.js +5 -0
  59. package/dist/ui/layers/index.js.map +1 -0
  60. package/dist/undo/index.d.ts +30 -0
  61. package/dist/undo/index.js +3 -0
  62. package/dist/undo/index.js.map +1 -0
  63. package/package.json +99 -0
  64. package/src/canvas/AGENTS.md +72 -0
  65. package/src/canvas/CanvasStack.less +22 -0
  66. package/src/canvas/CanvasStack.stories.tsx +63 -0
  67. package/src/canvas/CanvasStack.test.tsx +82 -0
  68. package/src/canvas/CanvasStack.tsx +108 -0
  69. package/src/canvas/CanvasStackContext.ts +8 -0
  70. package/src/canvas/canvasCoords.test.ts +30 -0
  71. package/src/canvas/canvasCoords.ts +15 -0
  72. package/src/canvas/index.ts +6 -0
  73. package/src/canvas/useLayerScheduler.ts +72 -0
  74. package/src/canvas/usePanZoom.ts +98 -0
  75. package/src/controls/ControlPanel.less +61 -0
  76. package/src/controls/ControlPanel.stories.tsx +77 -0
  77. package/src/controls/ControlPanel.test.tsx +149 -0
  78. package/src/controls/ControlPanel.tsx +249 -0
  79. package/src/controls/index.ts +12 -0
  80. package/src/controls/types.ts +61 -0
  81. package/src/dragdrop/DragDropRuntime.tsx +135 -0
  82. package/src/dragdrop/DragGhost.less +17 -0
  83. package/src/dragdrop/DragGhost.tsx +24 -0
  84. package/src/dragdrop/Palette.less +34 -0
  85. package/src/dragdrop/Palette.tsx +27 -0
  86. package/src/dragdrop/dragDrop.test.tsx +145 -0
  87. package/src/dragdrop/index.ts +1 -0
  88. package/src/fonts/oswald-latin-variable.woff2 +0 -0
  89. package/src/index.ts +70 -0
  90. package/src/instrument/SineWave.smoke.test.tsx +94 -0
  91. package/src/instrument/capabilityDetector.test.ts +64 -0
  92. package/src/instrument/capabilityDetector.ts +17 -0
  93. package/src/instrument/defineInstrument.test.ts +27 -0
  94. package/src/instrument/defineInstrument.ts +5 -0
  95. package/src/instrument/index.ts +19 -0
  96. package/src/instrument/types.ts +65 -0
  97. package/src/instrument/validateConfigSchema.test.ts +125 -0
  98. package/src/instrument/validateConfigSchema.ts +65 -0
  99. package/src/lab/Lab.less +3 -0
  100. package/src/lab/Lab.stories.tsx +51 -0
  101. package/src/lab/Lab.test.tsx +126 -0
  102. package/src/lab/Lab.tsx +196 -0
  103. package/src/lab/LabContext.test.ts +11 -0
  104. package/src/lab/LabContext.ts +28 -0
  105. package/src/lab/LabShell.less +45 -0
  106. package/src/lab/LabShell.stories.tsx +39 -0
  107. package/src/lab/LabShell.test.tsx +59 -0
  108. package/src/lab/LabShell.tsx +33 -0
  109. package/src/lab/WorkspaceGrid.less +8 -0
  110. package/src/lab/WorkspaceGrid.stories.tsx +61 -0
  111. package/src/lab/WorkspaceGrid.test.tsx +40 -0
  112. package/src/lab/WorkspaceGrid.tsx +20 -0
  113. package/src/lab/gridDims.test.ts +35 -0
  114. package/src/lab/gridDims.ts +11 -0
  115. package/src/lab/index.ts +10 -0
  116. package/src/layers/AGENTS.md +65 -0
  117. package/src/layers/LayerList.less +53 -0
  118. package/src/layers/LayerList.test.tsx +49 -0
  119. package/src/layers/LayerList.tsx +105 -0
  120. package/src/layers/index.ts +3 -0
  121. package/src/passthrough/weasel-canvas.test.ts +30 -0
  122. package/src/passthrough/weasel-canvas.ts +23 -0
  123. package/src/passthrough/weasel-ui.test.ts +79 -0
  124. package/src/passthrough/weasel-ui.ts +150 -0
  125. package/src/primitives/FpsMeter.less +14 -0
  126. package/src/primitives/FpsMeter.stories.tsx +14 -0
  127. package/src/primitives/FpsMeter.test.tsx +38 -0
  128. package/src/primitives/FpsMeter.tsx +36 -0
  129. package/src/primitives/ScaleIndicator.less +25 -0
  130. package/src/primitives/ScaleIndicator.stories.tsx +16 -0
  131. package/src/primitives/ScaleIndicator.test.tsx +46 -0
  132. package/src/primitives/ScaleIndicator.tsx +48 -0
  133. package/src/primitives/Sidebar.less +57 -0
  134. package/src/primitives/Sidebar.stories.tsx +38 -0
  135. package/src/primitives/Sidebar.test.tsx +74 -0
  136. package/src/primitives/Sidebar.tsx +32 -0
  137. package/src/primitives/StatusBar.less +21 -0
  138. package/src/primitives/StatusBar.stories.tsx +24 -0
  139. package/src/primitives/StatusBar.test.tsx +25 -0
  140. package/src/primitives/StatusBar.tsx +18 -0
  141. package/src/primitives/Toolbar.less +43 -0
  142. package/src/primitives/Toolbar.stories.tsx +36 -0
  143. package/src/primitives/Toolbar.test.tsx +65 -0
  144. package/src/primitives/Toolbar.tsx +44 -0
  145. package/src/primitives/fpsAverage.test.ts +21 -0
  146. package/src/primitives/fpsAverage.ts +5 -0
  147. package/src/primitives/index.ts +9 -0
  148. package/src/state/SingletonExperiment.test.tsx +101 -0
  149. package/src/state/SingletonExperiment.tsx +52 -0
  150. package/src/state/adapters.test.ts +94 -0
  151. package/src/state/adapters.ts +102 -0
  152. package/src/state/context.tsx +42 -0
  153. package/src/state/helpers.test.ts +89 -0
  154. package/src/state/helpers.ts +61 -0
  155. package/src/state/index.ts +40 -0
  156. package/src/state/store.test.ts +243 -0
  157. package/src/state/store.ts +225 -0
  158. package/src/state/types.ts +53 -0
  159. package/src/state/useExperimentState.test.tsx +102 -0
  160. package/src/state/useExperimentState.ts +28 -0
  161. package/src/styles.less +20 -0
  162. package/src/test-setup.ts +17 -0
  163. package/src/theme/Interstellar.stories.less +92 -0
  164. package/src/theme/Interstellar.stories.tsx +243 -0
  165. package/src/theme/base.less +122 -0
  166. package/src/theme/interstellar.less +34 -0
  167. package/src/theme/light.less +14 -0
  168. package/src/theme/tokens.less +85 -0
  169. package/src/ui/format.ts +19 -0
  170. package/src/ui/layers/LayerStack.less +143 -0
  171. package/src/ui/layers/LayerStack.stories.tsx +45 -0
  172. package/src/ui/layers/LayerStack.test.tsx +170 -0
  173. package/src/ui/layers/LayerStack.tsx +215 -0
  174. package/src/ui/layers/index.ts +1 -0
  175. package/src/ui/properties/CheckboxRow.stories.tsx +27 -0
  176. package/src/ui/properties/ColorRow.stories.tsx +106 -0
  177. package/src/ui/properties/CurveField.less +48 -0
  178. package/src/ui/properties/CurveField.stories.tsx +20 -0
  179. package/src/ui/properties/CurveField.test.tsx +127 -0
  180. package/src/ui/properties/CurveField.tsx +181 -0
  181. package/src/ui/properties/EffectCard.tsx +312 -0
  182. package/src/ui/properties/Gallery.stories.tsx +178 -0
  183. package/src/ui/properties/NumberRow.stories.tsx +65 -0
  184. package/src/ui/properties/PropertyGroup.less +50 -0
  185. package/src/ui/properties/PropertyGroup.stories.tsx +34 -0
  186. package/src/ui/properties/PropertyGroup.test.tsx +33 -0
  187. package/src/ui/properties/PropertyGroup.tsx +44 -0
  188. package/src/ui/properties/PropertyList.stories.tsx +83 -0
  189. package/src/ui/properties/PropertyPanel.less +592 -0
  190. package/src/ui/properties/PropertyPanel.stories.tsx +191 -0
  191. package/src/ui/properties/PropertyPanel.test.tsx +193 -0
  192. package/src/ui/properties/PropertyPanel.tsx +441 -0
  193. package/src/ui/properties/PropertyRow.stories.tsx +53 -0
  194. package/src/ui/properties/SelectRow.stories.tsx +58 -0
  195. package/src/ui/properties/SliderRow.stories.tsx +74 -0
  196. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +396 -0
  197. package/src/ui/properties/TextRow.stories.tsx +62 -0
  198. package/src/ui/properties/ToggleRow.stories.tsx +57 -0
  199. package/src/ui/properties/index.ts +39 -0
  200. package/src/ui/properties/storyLayouts.tsx +36 -0
  201. package/src/undo/eventBus.test.ts +49 -0
  202. package/src/undo/eventBus.ts +32 -0
  203. package/src/undo/index.ts +5 -0
  204. package/src/undo/undoStack.test.ts +70 -0
  205. package/src/undo/undoStack.ts +38 -0
  206. package/src/workspace/DefaultSidebar.tsx +26 -0
  207. package/src/workspace/DefaultStatusBar.tsx +15 -0
  208. package/src/workspace/DefaultToolbar.tsx +88 -0
  209. package/src/workspace/Workspace.less +58 -0
  210. package/src/workspace/Workspace.stories.tsx +78 -0
  211. package/src/workspace/Workspace.test.tsx +186 -0
  212. package/src/workspace/Workspace.tsx +255 -0
  213. package/src/workspace/WorkspaceChrome.tsx +150 -0
  214. package/src/workspace/index.ts +24 -0
  215. package/src/workspace/slotTypes.ts +44 -0
  216. package/src/workspace/workspaceOps.test.ts +142 -0
  217. package/src/workspace/workspaceOps.ts +78 -0
package/dist/index.js ADDED
@@ -0,0 +1,1425 @@
1
+ export { LayerStack } from './chunk-SD3HDXR7.js';
2
+ import { Ae, ao } from './chunk-6FO7G5BB.js';
3
+ import './chunk-B5ZUYRKN.js';
4
+ import { Sidebar, StatusBar, Toolbar } from './chunk-HXZHVU4G.js';
5
+ export { FpsMeter, ScaleIndicator, Sidebar, StatusBar, Toolbar } from './chunk-HXZHVU4G.js';
6
+ import { LabStoreContext, createLabStore, noneAdapter } from './chunk-SSMHPMDV.js';
7
+ export { LabStoreContext, LabStoreProvider, WorkspaceIdContext, WorkspaceIdProvider, createMemoryAdapter, localStorageAdapter, noneAdapter, sessionStorageAdapter, urlHashAdapter, useExperimentState, useLabStore, useWorkspaceId } from './chunk-SSMHPMDV.js';
8
+ import { ControlPanel } from './chunk-VLAHRJOC.js';
9
+ export { ControlPanel } from './chunk-VLAHRJOC.js';
10
+ import { CanvasStack, screenToWorld } from './chunk-54IQ2DX7.js';
11
+ export { CanvasStack, screenToWorld, worldToScreen } from './chunk-54IQ2DX7.js';
12
+ export { CanvasStackContext } from './chunk-PWC7AQZM.js';
13
+ import { LayerList } from './chunk-2QNYYL3V.js';
14
+ export { LayerList } from './chunk-2QNYYL3V.js';
15
+ import { createEventBus, redo, undo, pushSnapshot } from './chunk-7BKDG73Z.js';
16
+ export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-7BKDG73Z.js';
17
+ import { createContext, useContext, useMemo, useRef, useState, Children, useEffect, useCallback } from 'react';
18
+ import { useStore } from 'zustand/react';
19
+ import { createPortal } from 'react-dom';
20
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
21
+
22
+ // src/instrument/capabilityDetector.ts
23
+ function detectCapabilities(instrument) {
24
+ return {
25
+ hasCanvas: instrument.canvas != null,
26
+ hasLayers: instrument.layers != null,
27
+ hasDragDrop: instrument.dragDrop != null,
28
+ hasUndo: instrument.undo != null
29
+ };
30
+ }
31
+
32
+ // src/instrument/defineInstrument.ts
33
+ function defineInstrument(spec) {
34
+ return spec;
35
+ }
36
+
37
+ // src/instrument/validateConfigSchema.ts
38
+ var KNOWN_TYPES = ["slider", "checkbox", "select", "number", "text", "color"];
39
+ function validateConfigSchema(fields) {
40
+ const errors = [];
41
+ const seenKeys = /* @__PURE__ */ new Set();
42
+ for (const field of fields) {
43
+ if (field.key === "") {
44
+ errors.push("Field has empty key");
45
+ } else if (seenKeys.has(field.key)) {
46
+ errors.push(`Duplicate config key: "${field.key}"`);
47
+ } else {
48
+ seenKeys.add(field.key);
49
+ }
50
+ if (field.label === "") {
51
+ errors.push(`Field "${field.key}" has empty label`);
52
+ }
53
+ if (!KNOWN_TYPES.includes(field.type)) {
54
+ errors.push(`Unknown field type: "${field.type}" on key "${field.key}"`);
55
+ continue;
56
+ }
57
+ if (field.type === "slider") {
58
+ if (!Number.isFinite(field.default)) {
59
+ errors.push(`Field "${field.key}": default must be a finite number`);
60
+ }
61
+ if (field.min >= field.max) {
62
+ errors.push(`Slider "${field.key}": min must be < max`);
63
+ } else if (field.default < field.min || field.default > field.max) {
64
+ errors.push(`Slider "${field.key}": default ${field.default} is outside [min, max]`);
65
+ }
66
+ } else if (field.type === "number") {
67
+ if (!Number.isFinite(field.default)) {
68
+ errors.push(`Field "${field.key}": default must be a finite number`);
69
+ }
70
+ } else if (field.type === "select") {
71
+ if (field.options.length === 0) {
72
+ errors.push(`Select "${field.key}": options array must not be empty`);
73
+ } else {
74
+ const seenValues = /* @__PURE__ */ new Set();
75
+ for (const opt of field.options) {
76
+ if (seenValues.has(opt.value)) {
77
+ errors.push(`Select "${field.key}": duplicate option value "${opt.value}"`);
78
+ } else {
79
+ seenValues.add(opt.value);
80
+ }
81
+ }
82
+ if (!seenValues.has(field.default)) {
83
+ errors.push(`Select "${field.key}": default "${field.default}" is not among options`);
84
+ }
85
+ }
86
+ }
87
+ }
88
+ return { valid: errors.length === 0, errors };
89
+ }
90
+
91
+ // src/lab/gridDims.ts
92
+ function gridDims(count) {
93
+ if (count <= 1) return { cols: 1, rows: 1 };
94
+ const cols = Math.ceil(Math.sqrt(count));
95
+ const rows = Math.ceil(count / cols);
96
+ return { cols, rows };
97
+ }
98
+ function DragGhost({ item, screenPos }) {
99
+ const [mounted, setMounted] = useState(false);
100
+ useEffect(() => setMounted(true), []);
101
+ if (!mounted || typeof document === "undefined") return null;
102
+ const style = {
103
+ left: `${screenPos.x + 12}px`,
104
+ top: `${screenPos.y + 12}px`
105
+ };
106
+ return createPortal(
107
+ /* @__PURE__ */ jsx("div", { className: "lk-drag-ghost", style, children: /* @__PURE__ */ jsx("span", { className: "lk-drag-ghost__label", children: item.label }) }),
108
+ document.body
109
+ );
110
+ }
111
+ function useDragDrop({
112
+ capability,
113
+ canvasContainerRef,
114
+ view,
115
+ state,
116
+ config,
117
+ setState,
118
+ emit
119
+ }) {
120
+ const [drag, setDrag] = useState(null);
121
+ const dragRef = useRef(null);
122
+ dragRef.current = drag;
123
+ const rafThrottle = useRef(null);
124
+ const isOverCanvas = useCallback(
125
+ (screenPos) => {
126
+ const el = canvasContainerRef.current;
127
+ if (!el) return false;
128
+ const r = el.getBoundingClientRect();
129
+ return screenPos.x >= r.left && screenPos.x <= r.right && screenPos.y >= r.top && screenPos.y <= r.bottom;
130
+ },
131
+ [canvasContainerRef]
132
+ );
133
+ const screenToWorldFromContainer = useCallback(
134
+ (screenPos) => {
135
+ const el = canvasContainerRef.current;
136
+ if (!el) return null;
137
+ const r = el.getBoundingClientRect();
138
+ return screenToWorld({ x: screenPos.x - r.left, y: screenPos.y - r.top }, view);
139
+ },
140
+ [canvasContainerRef, view]
141
+ );
142
+ useEffect(() => {
143
+ if (!drag) return;
144
+ const handleMove = (e) => {
145
+ const screenPos = { x: e.clientX, y: e.clientY };
146
+ const current = dragRef.current;
147
+ if (!current) return;
148
+ if (rafThrottle.current !== null) {
149
+ dragRef.current = { ...current, screenPos };
150
+ setDrag(dragRef.current);
151
+ return;
152
+ }
153
+ rafThrottle.current = requestAnimationFrame(() => {
154
+ rafThrottle.current = null;
155
+ const active = dragRef.current;
156
+ if (!active) return;
157
+ let feedback = null;
158
+ if (capability.onDragOver && isOverCanvas(screenPos)) {
159
+ const world = screenToWorldFromContainer(screenPos);
160
+ if (world) feedback = capability.onDragOver(world, active.item, state, config);
161
+ }
162
+ setDrag({ ...active, screenPos, feedback });
163
+ });
164
+ };
165
+ const handleUp = (e) => {
166
+ const screenPos = { x: e.clientX, y: e.clientY };
167
+ const active = dragRef.current;
168
+ if (!active) return;
169
+ if (rafThrottle.current !== null) {
170
+ cancelAnimationFrame(rafThrottle.current);
171
+ rafThrottle.current = null;
172
+ }
173
+ if (isOverCanvas(screenPos)) {
174
+ const world = screenToWorldFromContainer(screenPos);
175
+ if (world) {
176
+ const nextState = capability.onDrop(world, active.item, state, config);
177
+ setState(nextState);
178
+ emit("canvas.itemAdded");
179
+ }
180
+ }
181
+ setDrag(null);
182
+ };
183
+ window.addEventListener("pointermove", handleMove);
184
+ window.addEventListener("pointerup", handleUp);
185
+ return () => {
186
+ window.removeEventListener("pointermove", handleMove);
187
+ window.removeEventListener("pointerup", handleUp);
188
+ };
189
+ }, [drag, capability, state, config, setState, emit, isOverCanvas, screenToWorldFromContainer]);
190
+ const startDrag = useCallback((item, originScreenPos) => {
191
+ setDrag({ item, screenPos: originScreenPos, feedback: null });
192
+ }, []);
193
+ return { drag, startDrag };
194
+ }
195
+ function DragOverlay({ drag }) {
196
+ if (!drag) return null;
197
+ return /* @__PURE__ */ jsx(DragGhost, { item: drag.item, screenPos: drag.screenPos });
198
+ }
199
+ function Palette({ items, onDragStart, className }) {
200
+ return /* @__PURE__ */ jsx("div", { className: className ? `lk-palette ${className}` : "lk-palette", children: items.map((item) => /* @__PURE__ */ jsx(
201
+ "button",
202
+ {
203
+ type: "button",
204
+ className: "lk-palette__item",
205
+ onPointerDown: (e) => {
206
+ e.preventDefault();
207
+ onDragStart(item, { x: e.clientX, y: e.clientY });
208
+ },
209
+ children: /* @__PURE__ */ jsx("span", { className: "lk-palette__label", children: item.label })
210
+ },
211
+ item.id
212
+ )) });
213
+ }
214
+ var LabContext = createContext(null);
215
+ function useLabContext() {
216
+ const ctx = useContext(LabContext);
217
+ if (ctx === null) {
218
+ throw new Error("useLabContext must be used inside <Lab>");
219
+ }
220
+ return ctx;
221
+ }
222
+ function DefaultSidebar({ ctx }) {
223
+ if (ctx.configFields.length === 0) {
224
+ return /* @__PURE__ */ jsx(Sidebar, { title: ctx.instrumentName, children: /* @__PURE__ */ jsxs("div", { className: "lk-sidebar__placeholder", children: [
225
+ ctx.instrumentName,
226
+ " has no config fields."
227
+ ] }) });
228
+ }
229
+ return /* @__PURE__ */ jsx(Sidebar, { title: ctx.instrumentName, children: /* @__PURE__ */ jsx(
230
+ ControlPanel,
231
+ {
232
+ fields: ctx.configFields,
233
+ config: ctx.config,
234
+ setConfig: (key, value) => ctx.setConfig(key, value)
235
+ }
236
+ ) });
237
+ }
238
+ function DefaultStatusBar({ ctx }) {
239
+ return /* @__PURE__ */ jsxs(StatusBar, { children: [
240
+ /* @__PURE__ */ jsx(StatusBar.Section, { children: ctx.instrumentName }),
241
+ /* @__PURE__ */ jsxs(StatusBar.Section, { children: [
242
+ Math.round(ctx.zoom * 100),
243
+ "%"
244
+ ] })
245
+ ] });
246
+ }
247
+ function DefaultToolbar({ ctx }) {
248
+ return /* @__PURE__ */ jsxs(Toolbar, { children: [
249
+ ctx.hasUndo && /* @__PURE__ */ jsxs(Fragment, { children: [
250
+ /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.undo, disabled: !ctx.canUndo, title: "Undo (Cmd/Ctrl+Z)", children: "Undo" }),
251
+ /* @__PURE__ */ jsx(
252
+ Toolbar.Button,
253
+ {
254
+ onClick: ctx.redo,
255
+ disabled: !ctx.canRedo,
256
+ title: "Redo (Cmd/Ctrl+Shift+Z)",
257
+ children: "Redo"
258
+ }
259
+ ),
260
+ /* @__PURE__ */ jsx(Toolbar.Spacer, {})
261
+ ] }),
262
+ ctx.hasCanvas && /* @__PURE__ */ jsxs(Fragment, { children: [
263
+ /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.zoomOut, title: "Zoom out", children: "\u2212" }),
264
+ /* @__PURE__ */ jsxs("span", { className: "lk-toolbar__zoom-label", children: [
265
+ Math.round(ctx.zoom * 100),
266
+ "%"
267
+ ] }),
268
+ /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.zoomIn, title: "Zoom in", children: "+" }),
269
+ /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.resetZoom, title: "Reset zoom", children: "1:1" }),
270
+ /* @__PURE__ */ jsx(Toolbar.Spacer, {})
271
+ ] }),
272
+ /* @__PURE__ */ jsx(Toolbar.Button, { onClick: () => ctx.saveSnapshot(), title: "Save snapshot (Cmd/Ctrl+S)", children: "Save" }),
273
+ ctx.savedSnapshots.length > 0 && /* @__PURE__ */ jsxs(
274
+ "select",
275
+ {
276
+ className: "lk-toolbar__load-select",
277
+ "aria-label": "Load snapshot",
278
+ defaultValue: "",
279
+ onChange: (e) => {
280
+ const id = e.target.value;
281
+ if (id) {
282
+ ctx.loadSnapshot(id);
283
+ e.target.value = "";
284
+ }
285
+ },
286
+ children: [
287
+ /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Load\u2026" }),
288
+ ctx.savedSnapshots.map((sn) => /* @__PURE__ */ jsx("option", { value: sn.id, children: sn.name }, sn.id))
289
+ ]
290
+ }
291
+ ),
292
+ /* @__PURE__ */ jsx(Toolbar.Spacer, {}),
293
+ /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.clone, title: "Clone workspace", children: "Clone" }),
294
+ /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.reset, title: "Reset workspace", children: "Reset" }),
295
+ /* @__PURE__ */ jsx(
296
+ Toolbar.Button,
297
+ {
298
+ onClick: ctx.close,
299
+ disabled: ctx.isLastWorkspace,
300
+ title: ctx.isLastWorkspace ? "Cannot close the last workspace" : "Close workspace",
301
+ children: "Close"
302
+ }
303
+ )
304
+ ] });
305
+ }
306
+ function WorkspaceChrome({
307
+ workspaceId,
308
+ record,
309
+ instrument,
310
+ isLastWorkspace,
311
+ toolbar,
312
+ sidebar,
313
+ statusBar,
314
+ undoBindings,
315
+ sidebarExtras,
316
+ children
317
+ }) {
318
+ const lab = useLabContext();
319
+ const storeCtx = useContext(LabStoreContext);
320
+ if (!storeCtx) throw new Error("[labkit] WorkspaceChrome requires <LabStoreProvider>");
321
+ const updateWorkspaceView = useStore(storeCtx.store, (s) => s.updateWorkspaceView);
322
+ const updateWorkspaceConfig = useStore(storeCtx.store, (s) => s.updateWorkspaceConfig);
323
+ const updateWorkspaceState = useStore(storeCtx.store, (s) => s.updateWorkspaceState);
324
+ const toolbarCtx = useMemo(() => {
325
+ const setZoom = (z) => {
326
+ updateWorkspaceView(workspaceId, { ...record.view, zoom: z });
327
+ };
328
+ return {
329
+ workspaceId,
330
+ instrumentName: record.instrumentName,
331
+ hasUndo: instrument.undo != null,
332
+ canUndo: undoBindings?.canUndo ?? false,
333
+ canRedo: undoBindings?.canRedo ?? false,
334
+ undo: undoBindings?.undo ?? (() => {
335
+ }),
336
+ redo: undoBindings?.redo ?? (() => {
337
+ }),
338
+ zoom: record.view.zoom,
339
+ setZoom,
340
+ zoomIn: () => setZoom(record.view.zoom * 1.25),
341
+ zoomOut: () => setZoom(record.view.zoom * 0.8),
342
+ resetZoom: () => setZoom(1),
343
+ hasCanvas: instrument.canvas != null,
344
+ savedSnapshots: lab.savedSnapshots.filter((s) => s.workspaceId === workspaceId),
345
+ saveSnapshot: (name) => lab.saveSnapshot(workspaceId, name),
346
+ loadSnapshot: (snapshotId) => lab.loadSnapshot(workspaceId, snapshotId),
347
+ clone: () => lab.cloneWorkspace(workspaceId),
348
+ reset: () => lab.resetWorkspace(workspaceId),
349
+ close: () => lab.closeWorkspace(workspaceId),
350
+ isLastWorkspace
351
+ };
352
+ }, [workspaceId, record, instrument, lab, isLastWorkspace, updateWorkspaceView, undoBindings]);
353
+ const sidebarCtx = useMemo(
354
+ () => ({
355
+ workspaceId,
356
+ instrumentName: record.instrumentName,
357
+ configFields: instrument.configSchema?.() ?? [],
358
+ config: record.config,
359
+ setConfig: (key, value) => {
360
+ const prevConfig = record.config;
361
+ if (process.env.NODE_ENV !== "production" && !(key in prevConfig)) {
362
+ console.warn(
363
+ `[labkit] setConfig: unknown key "${key}" for instrument "${record.instrumentName}"`
364
+ );
365
+ }
366
+ updateWorkspaceConfig(workspaceId, key, value);
367
+ if (instrument.onConfigChange) {
368
+ const nextConfig = { ...prevConfig, [key]: value };
369
+ const nextState = instrument.onConfigChange(nextConfig, prevConfig, record.state);
370
+ updateWorkspaceState(workspaceId, nextState);
371
+ }
372
+ }
373
+ }),
374
+ [workspaceId, record, instrument, updateWorkspaceConfig, updateWorkspaceState]
375
+ );
376
+ const statusCtx = {
377
+ workspaceId,
378
+ instrumentName: record.instrumentName,
379
+ zoom: record.view.zoom
380
+ };
381
+ const handleKeyDown = (e) => {
382
+ const mod = e.metaKey || e.ctrlKey;
383
+ if (!mod) return;
384
+ if (e.key === "z" || e.key === "Z") {
385
+ e.preventDefault();
386
+ if (e.shiftKey) toolbarCtx.redo();
387
+ else toolbarCtx.undo();
388
+ } else if (e.key === "s" || e.key === "S") {
389
+ e.preventDefault();
390
+ toolbarCtx.saveSnapshot();
391
+ }
392
+ };
393
+ return /* @__PURE__ */ jsxs(
394
+ "section",
395
+ {
396
+ className: "lk-workspace",
397
+ "aria-label": `Workspace ${record.instrumentName}`,
398
+ onKeyDown: handleKeyDown,
399
+ children: [
400
+ /* @__PURE__ */ jsx("div", { className: "lk-workspace__toolbar", children: toolbar ? toolbar(toolbarCtx) : /* @__PURE__ */ jsx(DefaultToolbar, { ctx: toolbarCtx }) }),
401
+ /* @__PURE__ */ jsxs("div", { className: "lk-workspace__body", children: [
402
+ /* @__PURE__ */ jsxs("div", { className: "lk-workspace__sidebar", children: [
403
+ sidebar ? sidebar(sidebarCtx) : /* @__PURE__ */ jsx(DefaultSidebar, { ctx: sidebarCtx }),
404
+ sidebarExtras
405
+ ] }),
406
+ /* @__PURE__ */ jsx("div", { className: "lk-workspace__content", children })
407
+ ] }),
408
+ /* @__PURE__ */ jsx("div", { className: "lk-workspace__status", children: statusBar ? statusBar(statusCtx) : /* @__PURE__ */ jsx(DefaultStatusBar, { ctx: statusCtx }) })
409
+ ]
410
+ }
411
+ );
412
+ }
413
+ function Workspace({ id }) {
414
+ const lab = useLabContext();
415
+ const storeCtx = useContext(LabStoreContext);
416
+ if (!storeCtx) throw new Error("[labkit] <Workspace> requires <LabStoreProvider>");
417
+ const record = useStore(storeCtx.store, (s) => s.workspaces.find((w) => w.id === id));
418
+ if (!record) {
419
+ return /* @__PURE__ */ jsxs("div", { className: "lk-workspace lk-workspace--unknown", children: [
420
+ "Workspace not found: ",
421
+ id
422
+ ] });
423
+ }
424
+ const instrument = lab.instruments.find((i) => i.name === record.instrumentName);
425
+ if (!instrument) {
426
+ return /* @__PURE__ */ jsxs("div", { className: "lk-workspace lk-workspace--unknown", children: [
427
+ "Unknown instrument: ",
428
+ record.instrumentName
429
+ ] });
430
+ }
431
+ return /* @__PURE__ */ jsx(
432
+ WorkspaceRuntime,
433
+ {
434
+ record,
435
+ instrument,
436
+ store: storeCtx.store,
437
+ isLast: lab.workspaces.length <= 1
438
+ }
439
+ );
440
+ }
441
+ function WorkspaceRuntime({ record, instrument, store, isLast }) {
442
+ const canvasContainerRef = useRef(null);
443
+ const updateWorkspaceState = useStore(store, (s) => s.updateWorkspaceState);
444
+ const updateWorkspaceConfig = useStore(store, (s) => s.updateWorkspaceConfig);
445
+ const updateWorkspaceView = useStore(store, (s) => s.updateWorkspaceView);
446
+ const updateWorkspaceUndoStack = useStore(store, (s) => s.updateWorkspaceUndoStack);
447
+ const busRef = useRef(null);
448
+ if (busRef.current === null) busRef.current = createEventBus();
449
+ const bus = busRef.current;
450
+ const [layerVisibility, setLayerVisibility] = useState({});
451
+ const [layerOrder, setLayerOrder] = useState(null);
452
+ const undoCap = instrument.undo;
453
+ const undoEvents = useMemo(() => new Set(undoCap?.snapshotOn ?? ["state.change"]), [undoCap]);
454
+ const maxDepth = undoCap?.maxDepth ?? 50;
455
+ const snapshotIfNeeded = (event) => {
456
+ if (!undoCap || !undoEvents.has(event)) return;
457
+ const current = store.getState().workspaces.find((w) => w.id === record.id);
458
+ if (!current) return;
459
+ const snap = structuredClone(current.state);
460
+ updateWorkspaceUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
461
+ };
462
+ const setView = (v) => updateWorkspaceView(record.id, v);
463
+ const renderCtx = {
464
+ state: record.state,
465
+ config: record.config,
466
+ setState: (next) => {
467
+ snapshotIfNeeded("state.change");
468
+ updateWorkspaceState(record.id, next);
469
+ bus.emit("state.change");
470
+ },
471
+ setConfig: (key, value) => {
472
+ const evt = `config.change:${String(key)}`;
473
+ snapshotIfNeeded("config.change");
474
+ snapshotIfNeeded(evt);
475
+ updateWorkspaceConfig(record.id, key, value);
476
+ bus.emit("config.change");
477
+ bus.emit(evt);
478
+ },
479
+ workspace: {
480
+ id: record.id,
481
+ zoom: record.view.zoom,
482
+ setZoom: (z) => updateWorkspaceView(record.id, { ...record.view, zoom: z })
483
+ },
484
+ emit: (event) => {
485
+ snapshotIfNeeded(event);
486
+ bus.emit(event);
487
+ }
488
+ };
489
+ const undoBindings = undoCap ? {
490
+ canUndo: record.undoStack.past.length > 0,
491
+ canRedo: record.undoStack.future.length > 0,
492
+ undo: () => {
493
+ const result = undo(record.undoStack, structuredClone(record.state));
494
+ if (!result) return;
495
+ updateWorkspaceState(record.id, result.snapshot);
496
+ updateWorkspaceUndoStack(record.id, result.stack);
497
+ },
498
+ redo: () => {
499
+ const result = redo(record.undoStack, structuredClone(record.state));
500
+ if (!result) return;
501
+ updateWorkspaceState(record.id, result.snapshot);
502
+ updateWorkspaceUndoStack(record.id, result.stack);
503
+ }
504
+ } : void 0;
505
+ const canvasLayers = useMemo(() => {
506
+ if (!instrument.canvas) return [];
507
+ const baseLayers = instrument.canvas.layers;
508
+ const ordered = layerOrder ? [...baseLayers].sort((a, b) => layerOrder.indexOf(a.id) - layerOrder.indexOf(b.id)) : baseLayers;
509
+ return ordered.map((layer) => ({
510
+ id: layer.id,
511
+ visible: layerVisibility[layer.id] !== false,
512
+ render: (ctx, view) => layer.draw(ctx, { state: record.state, config: record.config, zoom: view.zoom })
513
+ }));
514
+ }, [instrument.canvas, record.state, record.config, layerVisibility, layerOrder]);
515
+ const layerDescriptors = useMemo(() => {
516
+ if (!instrument.layers) return [];
517
+ return instrument.layers.ids.map((lid) => ({ id: lid, label: lid }));
518
+ }, [instrument.layers]);
519
+ const dragDropResult = useDragDrop({
520
+ capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
521
+ canvasContainerRef,
522
+ view: record.view,
523
+ state: record.state,
524
+ config: record.config,
525
+ setState: (next) => {
526
+ snapshotIfNeeded("canvas.itemAdded");
527
+ updateWorkspaceState(record.id, next);
528
+ },
529
+ emit: (evt) => {
530
+ snapshotIfNeeded(evt);
531
+ bus.emit(evt);
532
+ }
533
+ });
534
+ const paletteItems = useMemo(() => {
535
+ if (!instrument.dragDrop) return [];
536
+ const p = instrument.dragDrop.palette;
537
+ return typeof p === "function" ? p(record.state, record.config) : p;
538
+ }, [instrument.dragDrop, record.state, record.config]);
539
+ const layersWithFeedback = useMemo(() => {
540
+ if (!dragDropResult.drag?.feedback) return canvasLayers;
541
+ const fb = dragDropResult.drag.feedback;
542
+ const screen = dragDropResult.drag.screenPos;
543
+ return [
544
+ ...canvasLayers,
545
+ {
546
+ id: "__lk_drag_feedback",
547
+ visible: true,
548
+ render: (ctx) => {
549
+ const el = canvasContainerRef.current;
550
+ if (!el) return;
551
+ const r = el.getBoundingClientRect();
552
+ ctx.save();
553
+ ctx.strokeStyle = fb.ok ? "#3a7" : "#c44";
554
+ ctx.lineWidth = 2;
555
+ ctx.beginPath();
556
+ ctx.arc(screen.x - r.left, screen.y - r.top, 16, 0, Math.PI * 2);
557
+ ctx.stroke();
558
+ ctx.restore();
559
+ }
560
+ }
561
+ ];
562
+ }, [canvasLayers, dragDropResult.drag]);
563
+ let body;
564
+ if (instrument.canvas) {
565
+ body = /* @__PURE__ */ jsxs(
566
+ "div",
567
+ {
568
+ ref: canvasContainerRef,
569
+ className: "lk-workspace__canvas-host",
570
+ style: { width: "100%", height: "100%", position: "relative" },
571
+ children: [
572
+ /* @__PURE__ */ jsx(CanvasStack, { layers: layersWithFeedback, view: record.view, onViewChange: setView }),
573
+ /* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
574
+ ]
575
+ }
576
+ );
577
+ } else {
578
+ body = instrument.render(renderCtx);
579
+ }
580
+ const paletteNode = paletteItems.length > 0 ? /* @__PURE__ */ jsx(Palette, { items: paletteItems, onDragStart: dragDropResult.startDrag }) : null;
581
+ const layerListNode = instrument.layers && layerDescriptors.length > 0 ? /* @__PURE__ */ jsx(
582
+ LayerList,
583
+ {
584
+ layers: layerDescriptors,
585
+ visibility: layerVisibility,
586
+ onReorder: (next) => {
587
+ setLayerOrder(next.map((l) => l.id));
588
+ bus.emit("layers.reorder");
589
+ },
590
+ onToggle: (lid, visible) => {
591
+ setLayerVisibility((prev) => ({ ...prev, [lid]: visible }));
592
+ bus.emit("layers.toggle");
593
+ }
594
+ }
595
+ ) : null;
596
+ return /* @__PURE__ */ jsx(
597
+ WorkspaceChrome,
598
+ {
599
+ workspaceId: record.id,
600
+ record,
601
+ instrument,
602
+ isLastWorkspace: isLast,
603
+ undoBindings,
604
+ sidebarExtras: /* @__PURE__ */ jsxs(Fragment, { children: [
605
+ paletteNode,
606
+ layerListNode
607
+ ] }),
608
+ children: body
609
+ }
610
+ );
611
+ }
612
+
613
+ // src/workspace/workspaceOps.ts
614
+ var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
615
+ function findInstrument(instruments, name) {
616
+ const found = instruments.find((i) => i.name === name);
617
+ if (!found) {
618
+ throw new Error(`[labkit] Unknown instrument: "${name}"`);
619
+ }
620
+ return found;
621
+ }
622
+ function initialView(instrument) {
623
+ return instrument.canvas?.initialView ? structuredClone(instrument.canvas.initialView) : { ...DEFAULT_VIEW, pan: { ...DEFAULT_VIEW.pan } };
624
+ }
625
+ function addWorkspace(workspaces, instruments, instrumentName) {
626
+ const instrument = findInstrument(instruments, instrumentName);
627
+ const config = instrument.defaultConfig();
628
+ const state = instrument.initialState(config);
629
+ const record = {
630
+ id: crypto.randomUUID(),
631
+ instrumentName,
632
+ config,
633
+ state,
634
+ view: initialView(instrument),
635
+ undoStack: { past: [], future: [] }
636
+ };
637
+ return [...workspaces, record];
638
+ }
639
+ function cloneWorkspace(workspaces, id) {
640
+ const sourceIdx = workspaces.findIndex((w) => w.id === id);
641
+ const source = workspaces[sourceIdx];
642
+ if (!source) return workspaces;
643
+ const clone = {
644
+ ...source,
645
+ id: crypto.randomUUID(),
646
+ config: structuredClone(source.config),
647
+ state: structuredClone(source.state),
648
+ view: structuredClone(source.view),
649
+ undoStack: { past: [], future: [] }
650
+ };
651
+ return [...workspaces.slice(0, sourceIdx + 1), clone, ...workspaces.slice(sourceIdx + 1)];
652
+ }
653
+ function closeWorkspace(workspaces, id) {
654
+ if (workspaces.length <= 1) return workspaces;
655
+ const next = workspaces.filter((w) => w.id !== id);
656
+ return next.length === workspaces.length ? workspaces : next;
657
+ }
658
+ function resetWorkspace(workspaces, id, instruments) {
659
+ const idx = workspaces.findIndex((w) => w.id === id);
660
+ const current = workspaces[idx];
661
+ if (!current) return workspaces;
662
+ const instrument = findInstrument(instruments, current.instrumentName);
663
+ const config = instrument.defaultConfig();
664
+ const state = instrument.initialState(config);
665
+ const reset = {
666
+ ...current,
667
+ config,
668
+ state,
669
+ view: initialView(instrument)
670
+ };
671
+ return [...workspaces.slice(0, idx), reset, ...workspaces.slice(idx + 1)];
672
+ }
673
+ function LabShell({ title, children, header, footer, theme = "auto" }) {
674
+ const themeClass = theme === "light" ? " lk-theme-light" : theme === "interstellar" ? " lk-theme-interstellar" : "";
675
+ return /* @__PURE__ */ jsxs("div", { className: `lk-root lk-shell${themeClass}`, children: [
676
+ /* @__PURE__ */ jsxs("header", { className: "lk-shell-header", children: [
677
+ /* @__PURE__ */ jsx("h1", { className: "lk-shell-title", children: title }),
678
+ header && /* @__PURE__ */ jsx("div", { className: "lk-shell-header-actions", children: header })
679
+ ] }),
680
+ /* @__PURE__ */ jsx("main", { className: "lk-shell-body", children }),
681
+ footer && /* @__PURE__ */ jsx("footer", { className: "lk-shell-footer", children: footer })
682
+ ] });
683
+ }
684
+ function WorkspaceGrid({ children }) {
685
+ const count = Children.count(children);
686
+ const { cols, rows } = gridDims(count);
687
+ const style = {
688
+ "--lk-grid-cols": String(cols),
689
+ "--lk-grid-rows": String(rows)
690
+ };
691
+ return /* @__PURE__ */ jsx("div", { className: "lk-workspace-grid", style, children });
692
+ }
693
+ function buildStore(instruments, defaultInstrument, storage, storageKey, initialTheme) {
694
+ const store = createLabStore({ storageKey, storage, initialTheme });
695
+ if (store.getState().workspaces.length === 0) {
696
+ const seeded = addWorkspace([], instruments, defaultInstrument);
697
+ const record = seeded[0];
698
+ if (record) {
699
+ const { undoStack: _undoStack, ...rest } = record;
700
+ store.getState().addWorkspace(rest);
701
+ }
702
+ }
703
+ return store;
704
+ }
705
+ var NEBULA_SLOTS = [
706
+ { cx: "18%", cy: "30%", sx: "60%", sy: "80%", stop: "70%" },
707
+ { cx: "78%", cy: "70%", sx: "55%", sy: "90%", stop: "65%" },
708
+ { cx: "50%", cy: "50%", sx: "50%", sy: "70%", stop: "75%" },
709
+ { cx: "12%", cy: "82%", sx: "50%", sy: "70%", stop: "70%" },
710
+ { cx: "85%", cy: "18%", sx: "55%", sy: "80%", stop: "70%" }
711
+ ];
712
+ function buildNebula(colors) {
713
+ const blobs = colors.map((c, i) => {
714
+ const p = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
715
+ return `radial-gradient(ellipse ${p.sx} ${p.sy} at ${p.cx} ${p.cy}, color-mix(in srgb, ${c} 22%, transparent), transparent ${p.stop})`;
716
+ });
717
+ blobs.push("radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%)");
718
+ return blobs.join(", ");
719
+ }
720
+ function Lab({
721
+ instruments,
722
+ defaultInstrument,
723
+ storage,
724
+ storageKey,
725
+ theme,
726
+ nebula,
727
+ title,
728
+ children
729
+ }) {
730
+ if (process.env.NODE_ENV !== "production" && instruments.length === 0) {
731
+ throw new Error("[labkit] <Lab> requires a non-empty `instruments` array");
732
+ }
733
+ const storeRef = useRef(null);
734
+ if (storeRef.current === null) {
735
+ storeRef.current = buildStore(
736
+ instruments,
737
+ defaultInstrument,
738
+ storage ?? noneAdapter,
739
+ storageKey ?? "labkit",
740
+ theme ?? "auto"
741
+ );
742
+ }
743
+ const store = storeRef.current;
744
+ const workspaces = useStore(store, (s) => s.workspaces);
745
+ const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
746
+ const themeValue = useStore(store, (s) => s.theme);
747
+ useEffect(() => {
748
+ if (theme && theme !== store.getState().theme) {
749
+ store.getState().setTheme(theme);
750
+ }
751
+ }, [theme, store]);
752
+ const contextValue = useMemo(() => {
753
+ const replaceWorkspaces = (next) => {
754
+ const currentById = new Map(store.getState().workspaces.map((w) => [w.id, w]));
755
+ const merged = next.map((w) => currentById.get(w.id) ?? w);
756
+ store.setState({ workspaces: merged });
757
+ store.getState().setTheme(store.getState().theme);
758
+ };
759
+ return {
760
+ instruments,
761
+ workspaces,
762
+ addWorkspace: (instrumentName) => {
763
+ const next = addWorkspace(store.getState().workspaces, instruments, instrumentName);
764
+ replaceWorkspaces(next);
765
+ },
766
+ cloneWorkspace: (id) => {
767
+ const next = cloneWorkspace(store.getState().workspaces, id);
768
+ replaceWorkspaces(next);
769
+ },
770
+ closeWorkspace: (id) => {
771
+ const next = closeWorkspace(store.getState().workspaces, id);
772
+ replaceWorkspaces(next);
773
+ },
774
+ resetWorkspace: (id) => {
775
+ const next = resetWorkspace(store.getState().workspaces, id, instruments);
776
+ const record = next.find((w) => w.id === id);
777
+ if (!record) return;
778
+ store.setState((s) => ({
779
+ workspaces: s.workspaces.map(
780
+ (w) => w.id === id ? { ...w, config: record.config, state: record.state, view: record.view } : w
781
+ )
782
+ }));
783
+ store.getState().updateWorkspaceView(id, record.view);
784
+ },
785
+ savedSnapshots,
786
+ saveSnapshot: (workspaceId, name) => {
787
+ store.getState().saveSnapshot(workspaceId, name ?? `Save ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
788
+ },
789
+ loadSnapshot: (workspaceId, snapshotId) => {
790
+ store.getState().loadSnapshot(snapshotId, workspaceId);
791
+ },
792
+ deleteSnapshot: (snapshotId) => {
793
+ store.getState().deleteSnapshot(snapshotId);
794
+ },
795
+ theme: themeValue,
796
+ setTheme: (t) => {
797
+ store.getState().setTheme(t);
798
+ }
799
+ };
800
+ }, [instruments, workspaces, savedSnapshots, themeValue, store]);
801
+ const themeClass = themeValue === "light" ? "lk-theme-light" : themeValue === "interstellar" ? "lk-theme-interstellar" : "";
802
+ const nebulaStyle = themeValue === "interstellar" && nebula && nebula.length > 0 ? { ["--lk-space-nebula"]: buildNebula(nebula) } : void 0;
803
+ return /* @__PURE__ */ jsx(LabStoreContext.Provider, { value: { store }, children: /* @__PURE__ */ jsx(LabContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { className: `lk-lab ${themeClass}`.trim(), style: nebulaStyle, children: /* @__PURE__ */ jsx(LabShell, { title: title ?? "Labkit", theme: themeValue, header: children, children: /* @__PURE__ */ jsx(WorkspaceGrid, { children: workspaces.map((w) => /* @__PURE__ */ jsx(Workspace, { id: w.id }, w.id)) }) }) }) }) });
804
+ }
805
+ function CurveField({
806
+ values,
807
+ min,
808
+ max,
809
+ step,
810
+ width,
811
+ height = 110,
812
+ defaults,
813
+ onChange,
814
+ marks
815
+ }) {
816
+ const points = useMemo(() => {
817
+ const out = [];
818
+ for (let i = 0; i + 1 < values.length; i += 2) out.push({ x: values[i], y: values[i + 1] });
819
+ return out;
820
+ }, [values]);
821
+ const handleChange = useCallback(
822
+ (next) => {
823
+ const flat = new Array(next.length * 2);
824
+ for (let i = 0; i < next.length; i++) {
825
+ const ySnap = Math.round(next[i].y / step) * step;
826
+ const y = Math.max(min, Math.min(max, ySnap));
827
+ flat[i * 2] = next[i].x;
828
+ flat[i * 2 + 1] = y;
829
+ }
830
+ Ae("curve-field", "handleChange", {
831
+ nLen: next.length,
832
+ flatLen: flat.length,
833
+ sample: flat.slice(0, 4)
834
+ });
835
+ onChange(flat);
836
+ },
837
+ [onChange, min, max, step]
838
+ );
839
+ const handleFlip = useCallback(() => {
840
+ const flipped = [];
841
+ for (let i = 0; i + 1 < values.length; i += 2) {
842
+ flipped.push({ x: 1 - values[i], y: values[i + 1] });
843
+ }
844
+ flipped.sort((a, b) => a.x - b.x);
845
+ const out = [];
846
+ for (const p of flipped) out.push(p.x, p.y);
847
+ onChange(out);
848
+ }, [values, onChange]);
849
+ const handleFlipVertical = useCallback(() => {
850
+ const mid = (min + max) / 2;
851
+ const flipped = new Array(values.length);
852
+ for (let i = 0; i + 1 < values.length; i += 2) {
853
+ flipped[i] = values[i];
854
+ flipped[i + 1] = 2 * mid - values[i + 1];
855
+ }
856
+ onChange(flipped);
857
+ }, [values, min, max, onChange]);
858
+ return /* @__PURE__ */ jsxs("div", { className: "lk-curve-field", children: [
859
+ /* @__PURE__ */ jsxs("div", { className: "lk-curve-field__plot", children: [
860
+ /* @__PURE__ */ jsx(
861
+ ao,
862
+ {
863
+ value: points,
864
+ onChange: handleChange,
865
+ domain: "1d",
866
+ constrain: "function",
867
+ xRange: [0, 1],
868
+ yRange: [min, max],
869
+ width,
870
+ height,
871
+ endpoints: "pinned-x",
872
+ addPointMode: "click-curve",
873
+ minPoints: 2,
874
+ grid: {},
875
+ history: false
876
+ }
877
+ ),
878
+ marks && marks.length > 0 && /* @__PURE__ */ jsx(
879
+ "svg",
880
+ {
881
+ className: "lk-curve-field__marks",
882
+ width,
883
+ height,
884
+ viewBox: `0 0 ${width} ${height}`,
885
+ "aria-hidden": "true",
886
+ children: marks.map((m) => {
887
+ const color = m.color ?? "#ffcc00";
888
+ if (m.kind === "band") {
889
+ const x0 = Math.max(0, Math.min(1, m.x[0])) * width;
890
+ const x1 = Math.max(0, Math.min(1, m.x[1])) * width;
891
+ const left = Math.min(x0, x1);
892
+ const w = Math.abs(x1 - x0);
893
+ return /* @__PURE__ */ jsxs("g", { children: [
894
+ /* @__PURE__ */ jsx("rect", { x: left, y: 0, width: w, height, fill: color, fillOpacity: 0.4 }),
895
+ /* @__PURE__ */ jsx(
896
+ "line",
897
+ {
898
+ x1: left,
899
+ y1: 0,
900
+ x2: left,
901
+ y2: height,
902
+ stroke: color,
903
+ strokeOpacity: 0.85,
904
+ strokeWidth: 1
905
+ }
906
+ ),
907
+ /* @__PURE__ */ jsx(
908
+ "line",
909
+ {
910
+ x1: left + w,
911
+ y1: 0,
912
+ x2: left + w,
913
+ y2: height,
914
+ stroke: color,
915
+ strokeOpacity: 0.85,
916
+ strokeWidth: 1
917
+ }
918
+ )
919
+ ] }, `band-${m.x[0]}-${m.x[1]}-${color}`);
920
+ }
921
+ const x = Math.max(0, Math.min(1, m.x)) * width;
922
+ return /* @__PURE__ */ jsx(
923
+ "line",
924
+ {
925
+ x1: x,
926
+ y1: 0,
927
+ x2: x,
928
+ y2: height,
929
+ stroke: color,
930
+ strokeWidth: 1
931
+ },
932
+ `mark-${m.x}-${color}`
933
+ );
934
+ })
935
+ }
936
+ )
937
+ ] }),
938
+ /* @__PURE__ */ jsxs("div", { className: "lk-curve-field__actions", children: [
939
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lk-curve-field__action", onClick: handleFlip, children: "Flip horizontally" }),
940
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lk-curve-field__action", onClick: handleFlipVertical, children: "Flip vertically" }),
941
+ defaults && /* @__PURE__ */ jsx(
942
+ "button",
943
+ {
944
+ type: "button",
945
+ className: "lk-curve-field__action",
946
+ onClick: () => onChange([...defaults]),
947
+ children: "Reset"
948
+ }
949
+ )
950
+ ] })
951
+ ] });
952
+ }
953
+ function Subpanel({ title, children, className }) {
954
+ const cls = className ? `lk-subpanel ${className}` : "lk-subpanel";
955
+ return /* @__PURE__ */ jsxs("div", { className: cls, children: [
956
+ /* @__PURE__ */ jsxs("h4", { className: "lk-subpanel__title", children: [
957
+ /* @__PURE__ */ jsx("span", { children: title }),
958
+ /* @__PURE__ */ jsx("hr", {})
959
+ ] }),
960
+ children
961
+ ] });
962
+ }
963
+ function EffectCard({
964
+ title,
965
+ primary,
966
+ accent,
967
+ index,
968
+ expanded = true,
969
+ onToggleExpanded,
970
+ onRemove,
971
+ draggable,
972
+ onPressHandle,
973
+ onReleaseHandle,
974
+ onDragStart,
975
+ onDragEnd,
976
+ onDragOver,
977
+ onDragLeave,
978
+ onDrop,
979
+ dragging,
980
+ children,
981
+ className
982
+ }) {
983
+ const stateClass = `${expanded ? "is-expanded" : "is-collapsed"}${dragging ? " is-dragging" : ""}${accent ? " has-accent" : ""}`;
984
+ const cls = `lk-effect-card ${stateClass}${className ? ` ${className}` : ""}`;
985
+ const style = accent ? { "--lk-panel-accent": accent } : void 0;
986
+ const headProps = onToggleExpanded ? {
987
+ role: "button",
988
+ tabIndex: 0,
989
+ onClick: onToggleExpanded,
990
+ onKeyDown: (e) => {
991
+ if (e.key === "Enter" || e.key === " ") {
992
+ e.preventDefault();
993
+ onToggleExpanded();
994
+ }
995
+ }
996
+ } : {};
997
+ return (
998
+ // 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.
999
+ /* @__PURE__ */ jsxs(
1000
+ "div",
1001
+ {
1002
+ className: cls,
1003
+ style,
1004
+ draggable,
1005
+ onDragStart,
1006
+ onDragEnd,
1007
+ onDragOver,
1008
+ onDragLeave,
1009
+ onDrop,
1010
+ children: [
1011
+ /* @__PURE__ */ jsxs("div", { className: "lk-effect-card__head", ...headProps, children: [
1012
+ index != null ? /* @__PURE__ */ jsx(
1013
+ "button",
1014
+ {
1015
+ type: "button",
1016
+ className: "lk-effect-card__index-badge lk-effect-card__handle",
1017
+ title: "Drag to reorder \xB7 click to toggle",
1018
+ "aria-label": `Item ${index + 1} \u2014 drag to reorder, click to toggle`,
1019
+ onMouseDown: onPressHandle,
1020
+ onMouseUp: onReleaseHandle,
1021
+ onClick: (e) => {
1022
+ e.stopPropagation();
1023
+ onToggleExpanded?.();
1024
+ },
1025
+ children: index + 1
1026
+ }
1027
+ ) : /* @__PURE__ */ jsx(
1028
+ "span",
1029
+ {
1030
+ className: "lk-effect-card__handle",
1031
+ "aria-hidden": "true",
1032
+ title: "Drag to reorder",
1033
+ onMouseDown: onPressHandle,
1034
+ onMouseUp: onReleaseHandle,
1035
+ children: /* @__PURE__ */ jsx(DragHandleIcon, {})
1036
+ }
1037
+ ),
1038
+ /* @__PURE__ */ jsx("span", { className: "lk-effect-card__title", children: title }),
1039
+ primary != null && /* @__PURE__ */ jsx("span", { className: "lk-effect-card__primary", children: primary }),
1040
+ onRemove && /* @__PURE__ */ jsx(
1041
+ "button",
1042
+ {
1043
+ type: "button",
1044
+ className: "lk-effect-card__remove",
1045
+ onClick: (e) => {
1046
+ e.stopPropagation();
1047
+ onRemove();
1048
+ },
1049
+ title: "Remove",
1050
+ "aria-label": "Remove",
1051
+ children: "\u2715"
1052
+ }
1053
+ )
1054
+ ] }),
1055
+ expanded && /* @__PURE__ */ jsx("div", { className: "lk-effect-card__body", children })
1056
+ ]
1057
+ }
1058
+ )
1059
+ );
1060
+ }
1061
+ function DragHandleIcon() {
1062
+ return /* @__PURE__ */ jsxs("svg", { width: "10", height: "14", viewBox: "0 0 10 14", "aria-hidden": "true", children: [
1063
+ /* @__PURE__ */ jsx("circle", { cx: "3", cy: "3", r: "1.3", fill: "currentColor" }),
1064
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "3", r: "1.3", fill: "currentColor" }),
1065
+ /* @__PURE__ */ jsx("circle", { cx: "3", cy: "7", r: "1.3", fill: "currentColor" }),
1066
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "7", r: "1.3", fill: "currentColor" }),
1067
+ /* @__PURE__ */ jsx("circle", { cx: "3", cy: "11", r: "1.3", fill: "currentColor" }),
1068
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "11", r: "1.3", fill: "currentColor" })
1069
+ ] });
1070
+ }
1071
+ function EffectCardList({
1072
+ items,
1073
+ renderItem,
1074
+ onReorder,
1075
+ empty,
1076
+ defaultExpandedIds
1077
+ }) {
1078
+ const [expanded, setExpanded] = useState(() => new Set(defaultExpandedIds ?? []));
1079
+ const [draggingId, setDraggingId] = useState(null);
1080
+ const [pressedId, setPressedId] = useState(null);
1081
+ const [dropHint, setDropHint] = useState(
1082
+ null
1083
+ );
1084
+ if (items.length === 0) {
1085
+ return /* @__PURE__ */ jsx(Fragment, { children: empty ?? /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list__empty", children: "No items." }) });
1086
+ }
1087
+ return /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list", children: items.map((item) => {
1088
+ const isExpanded = expanded.has(item.id);
1089
+ const isDragging = draggingId === item.id;
1090
+ const isPressed = pressedId === item.id;
1091
+ const showHintBefore = dropHint && dropHint.id === item.id && dropHint.position === "before";
1092
+ const showHintAfter = dropHint && dropHint.id === item.id && dropHint.position === "after";
1093
+ const toggleExpanded = () => setExpanded((prev) => {
1094
+ const next = new Set(prev);
1095
+ if (next.has(item.id)) next.delete(item.id);
1096
+ else next.add(item.id);
1097
+ return next;
1098
+ });
1099
+ return /* @__PURE__ */ jsxs("div", { className: "lk-effect-card-list__wrap", children: [
1100
+ showHintBefore && /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list__drop-hint" }),
1101
+ renderItem(item, {
1102
+ isExpanded,
1103
+ isDragging,
1104
+ toggleExpanded,
1105
+ cardProps: {
1106
+ expanded: isExpanded,
1107
+ dragging: isDragging,
1108
+ draggable: isPressed,
1109
+ onToggleExpanded: toggleExpanded,
1110
+ onPressHandle: () => setPressedId(item.id),
1111
+ onReleaseHandle: () => setPressedId(null),
1112
+ onDragStart: (e) => {
1113
+ e.dataTransfer.effectAllowed = "move";
1114
+ e.dataTransfer.setData("text/plain", String(item.id));
1115
+ setDraggingId(item.id);
1116
+ },
1117
+ onDragEnd: () => {
1118
+ setDraggingId(null);
1119
+ setPressedId(null);
1120
+ setDropHint(null);
1121
+ },
1122
+ onDragOver: (e) => {
1123
+ if (draggingId === null || draggingId === item.id) return;
1124
+ e.preventDefault();
1125
+ e.dataTransfer.dropEffect = "move";
1126
+ const rect = e.currentTarget.getBoundingClientRect();
1127
+ const position = e.clientY < rect.top + rect.height / 2 ? "before" : "after";
1128
+ setDropHint({ id: item.id, position });
1129
+ },
1130
+ onDragLeave: () => {
1131
+ setDropHint((h) => h && h.id === item.id ? null : h);
1132
+ },
1133
+ onDrop: (e) => {
1134
+ e.preventDefault();
1135
+ if (draggingId === null || draggingId === item.id) {
1136
+ setDropHint(null);
1137
+ return;
1138
+ }
1139
+ const rect = e.currentTarget.getBoundingClientRect();
1140
+ const position = e.clientY < rect.top + rect.height / 2 ? "before" : "after";
1141
+ onReorder(draggingId, item.id, position);
1142
+ setDropHint(null);
1143
+ }
1144
+ }
1145
+ }),
1146
+ showHintAfter && /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list__drop-hint" })
1147
+ ] }, String(item.id));
1148
+ }) });
1149
+ }
1150
+ function PropertyGroup({
1151
+ title,
1152
+ hidden,
1153
+ children,
1154
+ className,
1155
+ pack = "auto-color"
1156
+ }) {
1157
+ if (hidden) return null;
1158
+ const packClass = pack === "pairs" ? " lk-property-group--pairs" : "";
1159
+ const cls = `lk-property-group${packClass}${className ? ` ${className}` : ""}`;
1160
+ return /* @__PURE__ */ jsxs("div", { className: cls, children: [
1161
+ /* @__PURE__ */ jsxs("h3", { className: "lk-property-group__title", children: [
1162
+ /* @__PURE__ */ jsx("hr", {}),
1163
+ /* @__PURE__ */ jsx("span", { children: title }),
1164
+ /* @__PURE__ */ jsx("hr", {})
1165
+ ] }),
1166
+ /* @__PURE__ */ jsx("div", { className: "lk-property-group__body", children })
1167
+ ] });
1168
+ }
1169
+
1170
+ // src/ui/format.ts
1171
+ var MINUS = "\u2212";
1172
+ function formatNumber(n, fractionDigits) {
1173
+ if (!Number.isFinite(n)) return String(n);
1174
+ const s = fractionDigits == null ? String(n) : n.toFixed(fractionDigits);
1175
+ return s.startsWith("-") ? MINUS + s.slice(1) : s;
1176
+ }
1177
+ function parseSignedNumber(s) {
1178
+ return Number(s.replace(MINUS, "-"));
1179
+ }
1180
+ function PropertyPanel({ title, children, className }) {
1181
+ const cls = className ? `lk-property-panel ${className}` : "lk-property-panel";
1182
+ return /* @__PURE__ */ jsxs("div", { className: cls, children: [
1183
+ title != null && /* @__PURE__ */ jsx("h2", { className: "lk-property-panel__title", children: title }),
1184
+ children
1185
+ ] });
1186
+ }
1187
+ function PropertyList({ children, className, pack = "auto-color" }) {
1188
+ const packClass = pack === "pairs" ? " lk-property-list--pairs" : "";
1189
+ const cls = `lk-property-list${packClass}${className ? ` ${className}` : ""}`;
1190
+ return /* @__PURE__ */ jsx("div", { className: cls, children });
1191
+ }
1192
+ function PropertyRow({
1193
+ label,
1194
+ readout,
1195
+ variant = "default",
1196
+ layout = "block",
1197
+ children,
1198
+ htmlFor,
1199
+ className
1200
+ }) {
1201
+ const variantClass = variant === "default" ? "" : ` lk-property-row--${variant}`;
1202
+ const layoutClass = variant === "default" && layout === "inline" ? " lk-property-row--inline" : "";
1203
+ const cls = `lk-property-row${variantClass}${layoutClass}${className ? ` ${className}` : ""}`;
1204
+ return /* @__PURE__ */ jsxs("label", { className: cls, htmlFor, children: [
1205
+ /* @__PURE__ */ jsxs("span", { className: "lk-property-row__label", children: [
1206
+ label,
1207
+ readout != null && /* @__PURE__ */ jsx("em", { className: "lk-property-row__readout", children: readout })
1208
+ ] }),
1209
+ children
1210
+ ] });
1211
+ }
1212
+ function SliderRow({
1213
+ label,
1214
+ value,
1215
+ min,
1216
+ max,
1217
+ step = 1,
1218
+ onChange,
1219
+ format,
1220
+ unit,
1221
+ layout
1222
+ }) {
1223
+ const decimals = step >= 1 ? 0 : Math.min(6, Math.max(0, Math.ceil(-Math.log10(step))));
1224
+ const effectiveFormat = format ?? ((n) => formatNumber(n, decimals));
1225
+ return /* @__PURE__ */ jsx(
1226
+ PropertyRow,
1227
+ {
1228
+ label,
1229
+ readout: /* @__PURE__ */ jsx(
1230
+ EditableReadout,
1231
+ {
1232
+ value,
1233
+ min,
1234
+ max,
1235
+ format: effectiveFormat,
1236
+ unit,
1237
+ onCommit: onChange
1238
+ }
1239
+ ),
1240
+ layout,
1241
+ children: /* @__PURE__ */ jsx(
1242
+ "input",
1243
+ {
1244
+ type: "range",
1245
+ tabIndex: -1,
1246
+ min,
1247
+ max,
1248
+ step,
1249
+ value,
1250
+ onChange: (e) => {
1251
+ const v = Number(e.target.value);
1252
+ Ae("property-panel", "slider", { label, value: v });
1253
+ onChange(v);
1254
+ }
1255
+ }
1256
+ )
1257
+ }
1258
+ );
1259
+ }
1260
+ function EditableReadout({ value, min, max, format, unit, onCommit }) {
1261
+ const [draft, setDraft] = useState(null);
1262
+ const fmt = (n) => format ? format(n) : formatNumber(n);
1263
+ const displayValue = draft !== null ? draft : (() => {
1264
+ const formatted = fmt(value);
1265
+ return typeof formatted === "string" ? formatted : String(formatted);
1266
+ })();
1267
+ const suffix = unit == null ? null : typeof unit === "string" ? /* @__PURE__ */ jsx("span", { className: "lk-property-row__readout-unit", children: unit }) : unit;
1268
+ const commit = () => {
1269
+ if (draft !== null) {
1270
+ const n = parseSignedNumber(draft);
1271
+ if (Number.isFinite(n)) onCommit(Math.min(max, Math.max(min, n)));
1272
+ }
1273
+ setDraft(null);
1274
+ };
1275
+ return /* @__PURE__ */ jsxs("span", { className: "lk-property-row__readout-group", children: [
1276
+ /* @__PURE__ */ jsx(
1277
+ "input",
1278
+ {
1279
+ type: "text",
1280
+ inputMode: "decimal",
1281
+ className: "lk-property-row__readout-input",
1282
+ value: displayValue,
1283
+ onFocus: (e) => {
1284
+ const formatted = fmt(value);
1285
+ setDraft(typeof formatted === "string" ? formatted : String(formatted));
1286
+ e.currentTarget.select();
1287
+ },
1288
+ onChange: (e) => setDraft(e.target.value.replace(/-/g, "\u2212")),
1289
+ onBlur: commit,
1290
+ onClick: (e) => {
1291
+ e.preventDefault();
1292
+ e.stopPropagation();
1293
+ e.currentTarget.focus();
1294
+ },
1295
+ onMouseDown: (e) => e.stopPropagation(),
1296
+ onKeyDown: (e) => {
1297
+ if (e.key === "Enter") {
1298
+ commit();
1299
+ e.currentTarget.blur();
1300
+ } else if (e.key === "Escape") {
1301
+ setDraft(null);
1302
+ e.currentTarget.blur();
1303
+ }
1304
+ }
1305
+ }
1306
+ ),
1307
+ suffix
1308
+ ] });
1309
+ }
1310
+ function ColorRow({
1311
+ label,
1312
+ value,
1313
+ onChange,
1314
+ alpha,
1315
+ onAlphaChange,
1316
+ alphaDisabled
1317
+ }) {
1318
+ const showAlpha = alpha != null;
1319
+ const className = alphaDisabled ? "lk-property-row--alpha-disabled" : void 0;
1320
+ return /* @__PURE__ */ jsxs(PropertyRow, { label, variant: "color", className, children: [
1321
+ /* @__PURE__ */ jsx("input", { type: "color", value, onChange: (e) => onChange(e.target.value) }),
1322
+ showAlpha && /* @__PURE__ */ jsx(
1323
+ "input",
1324
+ {
1325
+ type: "range",
1326
+ className: "lk-property-row__alpha",
1327
+ min: 0,
1328
+ max: 1,
1329
+ step: 0.01,
1330
+ value: alpha,
1331
+ disabled: alphaDisabled,
1332
+ onChange: (e) => onAlphaChange?.(Number(e.target.value))
1333
+ }
1334
+ )
1335
+ ] });
1336
+ }
1337
+ function CheckboxRow({ label, value, onChange }) {
1338
+ return /* @__PURE__ */ jsx(PropertyRow, { label, variant: "checkbox", children: /* @__PURE__ */ jsx("input", { type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked) }) });
1339
+ }
1340
+ function TextRow({ label, value, onChange, placeholder, maxLength, layout }) {
1341
+ return /* @__PURE__ */ jsx(PropertyRow, { label, layout, children: /* @__PURE__ */ jsx(
1342
+ "input",
1343
+ {
1344
+ type: "text",
1345
+ value,
1346
+ placeholder,
1347
+ maxLength,
1348
+ onChange: (e) => onChange(e.target.value)
1349
+ }
1350
+ ) });
1351
+ }
1352
+ function NumberRow({
1353
+ label,
1354
+ value,
1355
+ onChange,
1356
+ min,
1357
+ max,
1358
+ step,
1359
+ placeholder,
1360
+ layout
1361
+ }) {
1362
+ return /* @__PURE__ */ jsx(PropertyRow, { label, layout, children: /* @__PURE__ */ jsx(
1363
+ "input",
1364
+ {
1365
+ type: "number",
1366
+ value,
1367
+ min,
1368
+ max,
1369
+ step,
1370
+ placeholder,
1371
+ onChange: (e) => {
1372
+ const raw = e.target.value;
1373
+ if (raw === "") return;
1374
+ const n = Number(raw);
1375
+ if (Number.isFinite(n)) onChange(n);
1376
+ }
1377
+ }
1378
+ ) });
1379
+ }
1380
+ function SelectRow({
1381
+ label,
1382
+ value,
1383
+ options,
1384
+ onChange,
1385
+ layout
1386
+ }) {
1387
+ return /* @__PURE__ */ jsx(PropertyRow, { label, layout, children: /* @__PURE__ */ jsx(
1388
+ "select",
1389
+ {
1390
+ value,
1391
+ onChange: (e) => {
1392
+ const v = e.target.value;
1393
+ Ae("property-panel", "select", { label, value: v });
1394
+ onChange(v);
1395
+ },
1396
+ children: options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
1397
+ }
1398
+ ) });
1399
+ }
1400
+ function ToggleRow({
1401
+ label,
1402
+ value,
1403
+ options,
1404
+ onChange,
1405
+ layout
1406
+ }) {
1407
+ return /* @__PURE__ */ jsx(PropertyRow, { label, layout, children: /* @__PURE__ */ jsx("div", { className: "lk-property-row__toggle", children: options.map((opt) => {
1408
+ const selected = opt.value === value;
1409
+ return /* @__PURE__ */ jsx(
1410
+ "button",
1411
+ {
1412
+ type: "button",
1413
+ "aria-pressed": selected,
1414
+ className: selected ? "lk-property-row__toggle-button lk-property-row__toggle-button--selected" : "lk-property-row__toggle-button",
1415
+ onClick: () => onChange(opt.value),
1416
+ children: opt.label
1417
+ },
1418
+ opt.value
1419
+ );
1420
+ }) }) });
1421
+ }
1422
+
1423
+ export { CheckboxRow, ColorRow, CurveField, DefaultSidebar, DefaultStatusBar, DefaultToolbar, EffectCard, EffectCardList, Lab, LabContext, LabShell, NumberRow, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SelectRow, SliderRow, Subpanel, TextRow, ToggleRow, Workspace, WorkspaceChrome, WorkspaceGrid, addWorkspace, cloneWorkspace, closeWorkspace, defineInstrument, detectCapabilities, gridDims, resetWorkspace, useLabContext, validateConfigSchema };
1424
+ //# sourceMappingURL=index.js.map
1425
+ //# sourceMappingURL=index.js.map