@weasel-js/labkit 1.4.3 → 1.5.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 (229) hide show
  1. package/README.md +13 -2
  2. package/dist/_dts/{CanvasStackContext-Dde8_1my.d.ts → CanvasStackContext-CqkxkFJW.d.ts} +1 -1
  3. package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +116 -0
  4. package/dist/_dts/{frac-B5mB8LsJ.d.ts → frac-8R6UmlvN.d.ts} +256 -33
  5. package/dist/_dts/index-B8uu2ba7.d.ts +237 -0
  6. package/dist/_dts/{index-DelFfeV0.d.ts → index-retnAut7.d.ts} +45 -12
  7. package/dist/_dts/{types-B9_zrHmb.d.ts → types-Ca2LCnPq.d.ts} +65 -2
  8. package/dist/_dts/usePanZoom-nS798rOF.d.ts +160 -0
  9. package/dist/_dts/useTrialState-jcx_TdU6.d.ts +212 -0
  10. package/dist/canvas/index.d.ts +17 -128
  11. package/dist/canvas/index.js +4 -2
  12. package/dist/chrome/index.d.ts +4 -92
  13. package/dist/chrome/index.js +9 -5
  14. package/dist/chunk-67SJLMC7.js +704 -0
  15. package/dist/chunk-67SJLMC7.js.map +1 -0
  16. package/dist/chunk-6ZDGOZQV.js +310 -0
  17. package/dist/chunk-6ZDGOZQV.js.map +1 -0
  18. package/dist/{chunk-QAGAHJKZ.js → chunk-EKIICY6X.js} +111 -25
  19. package/dist/chunk-EKIICY6X.js.map +1 -0
  20. package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
  21. package/dist/chunk-H6ZAOWNE.js.map +1 -0
  22. package/dist/{chunk-2RQPGOQF.js → chunk-KJALCDWE.js} +9 -7
  23. package/dist/chunk-KJALCDWE.js.map +1 -0
  24. package/dist/{chunk-ISCOFAUO.js → chunk-MUKOW3TC.js} +3 -3
  25. package/dist/{chunk-ISCOFAUO.js.map → chunk-MUKOW3TC.js.map} +1 -1
  26. package/dist/chunk-NDRYLVOW.js +15 -0
  27. package/dist/chunk-NDRYLVOW.js.map +1 -0
  28. package/dist/{chunk-C42T6DJT.js → chunk-NS64DXMN.js} +226 -159
  29. package/dist/chunk-NS64DXMN.js.map +1 -0
  30. package/dist/{chunk-I6JCVE24.js → chunk-PGETSEDK.js} +135 -13
  31. package/dist/chunk-PGETSEDK.js.map +1 -0
  32. package/dist/{chunk-FQMJUVHJ.js → chunk-QLSV2N3G.js} +32 -37
  33. package/dist/chunk-QLSV2N3G.js.map +1 -0
  34. package/dist/chunk-RWNDDKOH.js +682 -0
  35. package/dist/chunk-RWNDDKOH.js.map +1 -0
  36. package/dist/chunk-S2SHZRD7.js +7681 -0
  37. package/dist/chunk-S2SHZRD7.js.map +1 -0
  38. package/dist/{chunk-ASWKLRKJ.js → chunk-TQBBYTAF.js} +19 -82
  39. package/dist/chunk-TQBBYTAF.js.map +1 -0
  40. package/dist/chunk-UU3NJO6N.js +72 -0
  41. package/dist/chunk-UU3NJO6N.js.map +1 -0
  42. package/dist/chunk-WIG6XHJ7.js +35 -0
  43. package/dist/chunk-WIG6XHJ7.js.map +1 -0
  44. package/dist/{chunk-TJ7QY3OC.js → chunk-ZPH5WTWF.js} +21 -15
  45. package/dist/chunk-ZPH5WTWF.js.map +1 -0
  46. package/dist/config/index.d.ts +232 -0
  47. package/dist/config/index.js +6 -0
  48. package/dist/config/index.js.map +1 -0
  49. package/dist/controls/index.d.ts +2 -3
  50. package/dist/controls/index.js +3 -2
  51. package/dist/dragdrop/index.d.ts +2 -4
  52. package/dist/index.d.ts +123 -348
  53. package/dist/index.js +794 -837
  54. package/dist/index.js.map +1 -1
  55. package/dist/layers/index.d.ts +3 -5
  56. package/dist/layers/index.js +2 -2
  57. package/dist/loupe/index.d.ts +6 -8
  58. package/dist/loupe/index.js +2 -2
  59. package/dist/passthrough/weasel-ui.d.ts +169 -117
  60. package/dist/passthrough/weasel-ui.js +1 -1
  61. package/dist/primitives/index.d.ts +53 -6
  62. package/dist/primitives/index.js +6 -4
  63. package/dist/state/index.d.ts +12 -7
  64. package/dist/state/index.js +16 -13
  65. package/dist/state/index.js.map +1 -1
  66. package/dist/styles.css +163 -62
  67. package/dist/surface/index.d.ts +44 -7
  68. package/dist/surface/index.js +2 -1
  69. package/dist/ui/layers/index.js +1 -1
  70. package/dist/undo/index.d.ts +2 -4
  71. package/package.json +15 -8
  72. package/src/annotations/AnnotationOverlay.tsx +53 -5
  73. package/src/annotations/Annotations.overlay.test.tsx +85 -6
  74. package/src/annotations/drawOne.test.ts +1 -1
  75. package/src/annotations/drawOne.ts +2 -2
  76. package/src/annotations/frac.test.ts +4 -4
  77. package/src/annotations/frac.ts +3 -2
  78. package/src/annotations/index.ts +1 -1
  79. package/src/annotations/paint.test.ts +2 -2
  80. package/src/annotations/paint.ts +1 -1
  81. package/src/annotations/preload.ts +6 -0
  82. package/src/annotations/store.test.ts +18 -0
  83. package/src/annotations/store.ts +7 -6
  84. package/src/annotations/svgNodes.test.ts +2 -2
  85. package/src/annotations/svgNodes.ts +1 -2
  86. package/src/annotations/toolMap.ts +3 -3
  87. package/src/annotations/types.ts +6 -3
  88. package/src/canvas/CameraWheelContext.ts +12 -0
  89. package/src/canvas/CanvasStack.test.tsx +25 -0
  90. package/src/canvas/CanvasStack.tsx +31 -4
  91. package/src/canvas/Stage.less +18 -0
  92. package/src/canvas/Stage.test.tsx +186 -0
  93. package/src/canvas/Stage.tsx +134 -0
  94. package/src/canvas/camera.test.ts +17 -2
  95. package/src/canvas/camera.ts +9 -9
  96. package/src/canvas/index.ts +4 -0
  97. package/src/canvas/usePanZoom.ts +10 -5
  98. package/src/chrome/ChromeRegions.stories.tsx +0 -3
  99. package/src/chrome/LabChrome.tsx +114 -0
  100. package/src/chrome/builtins.tsx +20 -31
  101. package/src/chrome/index.ts +15 -0
  102. package/src/chrome/labRegions.test.tsx +208 -0
  103. package/src/chrome/labTypes.ts +50 -0
  104. package/src/chrome/merge.ts +7 -9
  105. package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
  106. package/src/chrome/regions/PaletteRegion.tsx +21 -9
  107. package/src/chrome/regions/SidebarRegion.tsx +25 -13
  108. package/src/chrome/regions/StatusRegion.tsx +15 -8
  109. package/src/chrome/regions/ToolbarRegion.tsx +30 -16
  110. package/src/chrome/types.ts +47 -20
  111. package/src/config/builder.test.ts +4 -0
  112. package/src/config/builder.ts +16 -10
  113. package/src/config/declarationEmit.fixture.ts +11 -0
  114. package/src/config/declarationEmit.test.ts +51 -0
  115. package/src/config/entry.test.ts +46 -0
  116. package/src/config/index.ts +13 -1
  117. package/src/config/nodeClasses.test.ts +18 -0
  118. package/src/config/types.ts +3 -1
  119. package/src/controls/ControlPanel.test.tsx +16 -0
  120. package/src/controls/ControlPanel.tsx +5 -2
  121. package/src/fake-indexeddb-auto.d.ts +5 -0
  122. package/src/index.test.ts +7 -0
  123. package/src/index.ts +39 -6
  124. package/src/instrument/serializers.test.ts +22 -0
  125. package/src/instrument/serializers.ts +11 -0
  126. package/src/instrument/types.ts +25 -1
  127. package/src/lab/Lab.less +4 -13
  128. package/src/lab/Lab.persist.test.tsx +147 -0
  129. package/src/lab/Lab.stories.tsx +0 -2
  130. package/src/lab/Lab.surface.test.tsx +66 -3
  131. package/src/lab/Lab.test.tsx +91 -17
  132. package/src/lab/Lab.tsx +337 -124
  133. package/src/lab/LabFit.stories.less +18 -0
  134. package/src/lab/LabFit.stories.tsx +219 -0
  135. package/src/lab/LabFullChrome.stories.tsx +0 -3
  136. package/src/lab/LabHeader.test.tsx +40 -9
  137. package/src/lab/LabHeader.tsx +7 -13
  138. package/src/lab/LabPalette.tsx +13 -26
  139. package/src/lab/LabShell.less +49 -12
  140. package/src/lab/LabShell.tsx +14 -2
  141. package/src/lab/LabSwitcher.less +92 -0
  142. package/src/lab/LabSwitcher.test.tsx +96 -0
  143. package/src/lab/LabSwitcher.tsx +112 -0
  144. package/src/lab/Workspace.surface.test.tsx +2 -1
  145. package/src/lab/fitCheck.test.ts +57 -0
  146. package/src/lab/fitCheck.ts +125 -0
  147. package/src/lab/index.ts +2 -0
  148. package/src/loupe/AGENTS.md +1 -1
  149. package/src/loupe/Loupe.less +1 -1
  150. package/src/loupe/canvasLens.ts +1 -1
  151. package/src/loupe/types.ts +1 -1
  152. package/src/loupe/useLoupe.test.tsx +1 -1
  153. package/src/loupe/useLoupe.ts +1 -1
  154. package/src/passthrough/weasel-ui.ts +9 -0
  155. package/src/primitives/FloatingPanel.stories.tsx +13 -1
  156. package/src/primitives/FloatingPanel.test.tsx +47 -16
  157. package/src/primitives/FloatingPanel.tsx +11 -25
  158. package/src/primitives/Split.less +14 -0
  159. package/src/primitives/Split.test.tsx +108 -0
  160. package/src/primitives/Split.tsx +159 -0
  161. package/src/primitives/ZoomControl.tsx +5 -2
  162. package/src/primitives/index.ts +2 -0
  163. package/src/state/Persistence.stories.tsx +75 -0
  164. package/src/state/Persistence.tsx +33 -0
  165. package/src/state/SingletonExperiment.test.tsx +47 -53
  166. package/src/state/SingletonExperiment.tsx +24 -13
  167. package/src/state/adapterContract.ts +116 -0
  168. package/src/state/adapters.test.ts +113 -61
  169. package/src/state/adapters.ts +328 -69
  170. package/src/state/document.test.ts +71 -96
  171. package/src/state/document.ts +66 -52
  172. package/src/state/helpers.test.ts +20 -0
  173. package/src/state/helpers.ts +16 -0
  174. package/src/state/index.ts +8 -0
  175. package/src/state/labRecords.test.ts +105 -0
  176. package/src/state/labRecords.ts +160 -0
  177. package/src/state/openLabStore.test.ts +658 -0
  178. package/src/state/openLabStore.ts +403 -0
  179. package/src/state/records.test.ts +234 -0
  180. package/src/state/records.ts +219 -0
  181. package/src/state/store.test.ts +104 -635
  182. package/src/state/store.ts +117 -158
  183. package/src/state/toolSlot.test.ts +1 -1
  184. package/src/state/types.ts +39 -15
  185. package/src/state/useOpenOnce.ts +68 -0
  186. package/src/state/usePersistedState.test.tsx +125 -0
  187. package/src/state/usePersistedState.ts +66 -0
  188. package/src/state/useTrialState.test.tsx +2 -3
  189. package/src/state/view.test.ts +49 -15
  190. package/src/state/view.ts +18 -1
  191. package/src/styles.less +3 -0
  192. package/src/surface/AGENTS.md +39 -7
  193. package/src/surface/SurfaceContext.ts +26 -3
  194. package/src/surface/index.ts +2 -0
  195. package/src/surface/useSurfaceTile.test.tsx +2 -1
  196. package/src/surface/useSurfaceTile.ts +6 -5
  197. package/src/surface/useTiledSurface.test.tsx +175 -0
  198. package/src/surface/useTiledSurface.ts +54 -5
  199. package/src/theme/Interstellar.stories.tsx +7 -7
  200. package/src/theme/base.less +4 -4
  201. package/src/theme/interstellar.test.ts +1 -1
  202. package/src/theme/interstellar.tokens.json +2 -2
  203. package/src/tools/labTool.ts +11 -0
  204. package/src/trial/Trial.annotations.persist.test.tsx +100 -58
  205. package/src/trial/Trial.annotations.test.tsx +47 -4
  206. package/src/trial/Trial.job.test.tsx +3 -3
  207. package/src/trial/Trial.less +1 -26
  208. package/src/trial/Trial.stories.tsx +1 -2
  209. package/src/trial/Trial.test.tsx +28 -4
  210. package/src/trial/Trial.trialId.test.tsx +23 -1
  211. package/src/trial/Trial.tsx +118 -38
  212. package/src/trial/TrialBody.tsx +17 -141
  213. package/src/trial/TrialChrome.tsx +7 -4
  214. package/src/trial/trialOps.ts +7 -4
  215. package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
  216. package/dist/_dts/types-x92Kfeme.d.ts +0 -62
  217. package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
  218. package/dist/chunk-2RQPGOQF.js.map +0 -1
  219. package/dist/chunk-5TROQVQ2.js +0 -547
  220. package/dist/chunk-5TROQVQ2.js.map +0 -1
  221. package/dist/chunk-ASWKLRKJ.js.map +0 -1
  222. package/dist/chunk-C42T6DJT.js.map +0 -1
  223. package/dist/chunk-CTRKTLYZ.js.map +0 -1
  224. package/dist/chunk-FQMJUVHJ.js.map +0 -1
  225. package/dist/chunk-I6JCVE24.js.map +0 -1
  226. package/dist/chunk-MHAGC6VD.js +0 -7320
  227. package/dist/chunk-MHAGC6VD.js.map +0 -1
  228. package/dist/chunk-QAGAHJKZ.js.map +0 -1
  229. package/dist/chunk-TJ7QY3OC.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,54 +1,62 @@
1
+ import { resolveLoupe, TrialLoupe } from './chunk-ZPH5WTWF.js';
2
+ export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from './chunk-ZPH5WTWF.js';
1
3
  import { createEventBus, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
2
4
  export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
3
5
  import { useDragDrop, DragOverlay, Palette } from './chunk-RS3HRQWU.js';
4
- import { useSurfaceOptional, useSurfaceCanvas, useTileId, useTiledSurface, SurfaceContext, SurfaceCanvasContext } from './chunk-FQMJUVHJ.js';
5
- export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTileId, useTiledSurface } from './chunk-FQMJUVHJ.js';
6
+ import { useTiledSurface } from './chunk-QLSV2N3G.js';
7
+ export { composeRects, rectsEqual, toDeviceRect, useTiledSurface } from './chunk-QLSV2N3G.js';
6
8
  import { useJob } from './chunk-HXXTULDN.js';
7
9
  export { useJob } from './chunk-HXXTULDN.js';
8
- import { ANNOTATION_WEASEL_TOOLS, annotationToolInfo, suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, ViewportRegion, SidebarRegion, StatusRegion, AnnotationsContext } from './chunk-C42T6DJT.js';
9
- export { ANNOTATION_TOOLS, ANNOTATION_WEASEL_TOOLS, AnnotationsContext, ExportMenu, MarkList, PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, annotationToolInfo, builtinContributions, mergeContributions, suppressContributions, useAnnotations, useAnnotationsOptional } from './chunk-C42T6DJT.js';
10
- import { JobProgress } from './chunk-QAGAHJKZ.js';
11
- export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-QAGAHJKZ.js';
12
- export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-2RQPGOQF.js';
13
- import { createLabStore, panelKey, noneAdapter } from './chunk-5TROQVQ2.js';
14
- export { CURRENT_DOCUMENT_VERSION, createMemoryAdapter, dockPanel, labDocumentKey, localStorageAdapter, noneAdapter, panelKey, quarantineKey, sessionStorageAdapter, undockPanel, urlHashAdapter, useTrialState } from './chunk-5TROQVQ2.js';
15
- import { LabStoreContext, TrialIdProvider } from './chunk-2YILQ7Y5.js';
16
- export { LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, useLabStore, useTrialId } from './chunk-2YILQ7Y5.js';
17
- import { fromConfigFields } from './chunk-ASWKLRKJ.js';
18
- export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-ASWKLRKJ.js';
10
+ import { suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, ViewportRegion, SidebarRegion, StatusRegion, resolveLabTool, AnnotationsContext, useLabChromeContext, labContributions, LabHeaderRegion, LabFooterRegion, LabSidebarRegion } from './chunk-NS64DXMN.js';
11
+ export { AnnotationsContext, ExportMenu, LabFooterRegion, LabHeaderRegion, LabSidebarRegion, MarkList, PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, builtinContributions, contributionsIn, labContributions, mergeContributions, suppressContributions, toolContributions, useAnnotations, useAnnotationsOptional, useLabChromeContext } from './chunk-NS64DXMN.js';
12
+ import { Split, JobProgress } from './chunk-EKIICY6X.js';
13
+ export { FloatingPanel, JobProgress, Legend, Sidebar, Split } from './chunk-EKIICY6X.js';
14
+ export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-KJALCDWE.js';
15
+ import { createLabStore, openLabStore, panelKey } from './chunk-67SJLMC7.js';
16
+ export { CURRENT_DOCUMENT_VERSION, dockPanel, labDocumentKey, openLabStore, panelKey, quarantineKey, undockPanel, useTrialState } from './chunk-67SJLMC7.js';
17
+ import { newId, useWarnIgnoredChange, useOpenOnce, PersistenceContext, createRecordCache, noneAdapter, defaultStorage, usePersistedState } from './chunk-RWNDDKOH.js';
18
+ export { Persistence, createIndexedDbAdapter, createMemoryAdapter, indexedDbAdapter, localStorageAdapter, noneAdapter, sessionStorageAdapter, urlHashAdapter, usePersistedState } from './chunk-RWNDDKOH.js';
19
+ export { ControlPanel } from './chunk-TQBBYTAF.js';
20
+ import { useConfigSchema } from './chunk-6ZDGOZQV.js';
21
+ export { BaseNode, BooleanNode, ColorNode, CustomNode, EnumNode, GroupNode, NumberNode, StringNode, ValueNode, applyRules, builtinRules, f, isConfigBranch, resolveConfigSchema, titleCase, useConfigSchema } from './chunk-6ZDGOZQV.js';
22
+ import { useLabContext, LabContext } from './chunk-NDRYLVOW.js';
23
+ export { LabContext, useLabContext } from './chunk-NDRYLVOW.js';
24
+ export { fromConfigFields, isLeafVisible } from './chunk-UU3NJO6N.js';
19
25
  import { hasConfigPath, schemaNodeAtPath, withValueAtPath, fillConfigDefaults } from './chunk-E44SU6XS.js';
20
26
  export { fillConfigDefaults, hasConfigPath, schemaNodeAtPath, valueAtPath, withValueAtPath } from './chunk-E44SU6XS.js';
21
- import { CanvasStack } from './chunk-I6JCVE24.js';
22
- export { CanvasStack, PITCH_LIMIT, clampPitch, orbitAfterDrag, orbitAfterWheel, useOrbit, usePanZoom, wrapYaw } from './chunk-I6JCVE24.js';
23
- import { LayerList } from './chunk-ISCOFAUO.js';
24
- export { LayerList } from './chunk-ISCOFAUO.js';
25
- import './chunk-SBXNQW4G.js';
26
- import { E, w, T, C, g, r6 } from './chunk-MHAGC6VD.js';
27
- export { M2 as CheckboxRow, j2 as ColorRow, y3 as CurveField, R as EffectCard, B as EffectCardList, c3 as LayerStack, P2 as NumberRow, V as PropertyGroup, T2 as PropertyList, w2 as PropertyPanel, D as PropertyRow, E2 as PropertySpan, F as SelectRow, k as SliderRow, L2 as Subpanel, N2 as TextRow, I as ToggleRow } from './chunk-MHAGC6VD.js';
28
- import { resolveLoupe, TrialLoupe } from './chunk-TJ7QY3OC.js';
29
- export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from './chunk-TJ7QY3OC.js';
30
- export { centerOn, zoomAt } from './chunk-CTRKTLYZ.js';
27
+ import { CameraWheelContext, as2DView, DEFAULT_VIEW, CanvasStack, Stage, withZoom, fitStage } from './chunk-PGETSEDK.js';
28
+ export { CanvasStack, DEFAULT_VIEW, PITCH_LIMIT, Stage, as2DView, clampPitch, fitStage, orbitAfterDrag, orbitAfterWheel, useOrbit, usePanZoom, wrapYaw } from './chunk-PGETSEDK.js';
29
+ export { centerOn, zoomAt } from './chunk-H6ZAOWNE.js';
31
30
  import { applyCamera } from './chunk-2AYGEN57.js';
32
31
  export { DEFAULT_FRAME, applyCamera, resolveFrame, screenToWorld, worldToScreen } from './chunk-2AYGEN57.js';
32
+ import { useSurfaceOptional, useSurfaceCanvas, useTileId, SurfaceContext, SurfaceCanvasContext } from './chunk-WIG6XHJ7.js';
33
+ export { SurfaceCanvasContext, SurfaceContext, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTileId } from './chunk-WIG6XHJ7.js';
33
34
  export { CanvasStackContext } from './chunk-CQLQPQ4P.js';
34
- import { textCommand, linePath, markerDrawCommands, ellipsePath, rectPath, createScene, asNodeId as asNodeId$1, WeaselProvider, SceneCanvas, sceneFromJSON, PathBuilder, renderSceneToPixels, useActiveToolContext, resolveStrokeWidth, getMarker, getPaintKind, boundsOfPath, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, SCRIPT_METRICS } from '@weasel-js/core';
35
+ import { LabStoreContext, TrialIdProvider } from './chunk-2YILQ7Y5.js';
36
+ export { LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, useLabStore, useTrialId } from './chunk-2YILQ7Y5.js';
37
+ import { LayerList } from './chunk-MUKOW3TC.js';
38
+ export { LayerList } from './chunk-MUKOW3TC.js';
39
+ import './chunk-SBXNQW4G.js';
40
+ import { E, w, T, p, u, c, n, f, r2, h, C, l5, r6 } from './chunk-S2SHZRD7.js';
41
+ export { a3 as Button, P3 as CheckboxRow, N2 as ColorRow, b3 as CurveField, B as EffectCard, H as EffectCardList, c3 as LayerStack, I as NumberRow, U2 as PropertyGroup, E2 as PropertyList, T2 as PropertyPanel, O as PropertyRow, D as PropertySpan, L as SelectRow, j2 as SliderRow, z as Subpanel, F as TextRow, r6 as ToggleBar, R as ToggleRow } from './chunk-S2SHZRD7.js';
42
+ import { textCommand, linePath, markerDrawCommands, ellipsePath, rectPath, createScene, asNodeId as asNodeId$1, WeaselProvider, SceneCanvas, sceneFromJSON, PathBuilder, renderSceneToPixels, useActiveToolContext, getMarker, getPaintKind, boundsOfPath, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, resolveAlign, SCRIPT_METRICS } from '@weasel-js/core';
35
43
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
36
- import { createContext, useState, useRef, useEffect, useContext, useMemo, Children, useLayoutEffect, useCallback, useSyncExternalStore } from 'react';
37
- import { createPortal } from 'react-dom';
44
+ import { createContext, useState, useRef, useEffect, useCallback, useContext, useMemo, useId, Children, useLayoutEffect, useSyncExternalStore } from 'react';
45
+ import { flushSync, createPortal } from 'react-dom';
38
46
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
39
47
  import { useStore } from 'zustand/react';
40
- import { asNodeId, Store, createNode, stripStrategy, floatingStrategy, gridStrategy } from 'windease';
41
- import { Provider, StrategyRegistryProvider, Container, DragProvider, useDragHandle } from 'windease/react';
48
+ import { Provider, StrategyRegistryProvider, DragProvider, Container, useDragHandle } from 'windease/react';
49
+ import { asNodeId, Store, createNode, floatingStrategy, gridStrategy } from 'windease';
42
50
 
43
51
  var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
44
- function multiply(a, b) {
52
+ function multiply(a2, b2) {
45
53
  return [
46
- a[0] * b[0] + a[2] * b[1],
47
- a[1] * b[0] + a[3] * b[1],
48
- a[0] * b[2] + a[2] * b[3],
49
- a[1] * b[2] + a[3] * b[3],
50
- a[0] * b[4] + a[2] * b[5] + a[4],
51
- a[1] * b[4] + a[3] * b[5] + a[5]
54
+ a2[0] * b2[0] + a2[2] * b2[1],
55
+ a2[1] * b2[0] + a2[3] * b2[1],
56
+ a2[0] * b2[2] + a2[2] * b2[3],
57
+ a2[1] * b2[2] + a2[3] * b2[3],
58
+ a2[0] * b2[4] + a2[2] * b2[5] + a2[4],
59
+ a2[1] * b2[4] + a2[3] * b2[5] + a2[5]
52
60
  ];
53
61
  }
54
62
  function isIdentity(m, eps = 1e-9) {
@@ -56,13 +64,17 @@ function isIdentity(m, eps = 1e-9) {
56
64
  }
57
65
  function formatMatrix(m) {
58
66
  if (isIdentity(m)) return null;
59
- return `matrix(${m.map((n) => trimNumber(n)).join(" ")})`;
67
+ return `matrix(${m.map((n2) => trimNumber(n2)).join(" ")})`;
68
+ }
69
+ function trimNumber(n2) {
70
+ if (!Number.isFinite(n2)) return "0";
71
+ const r3 = Math.round(n2 * 1e6) / 1e6;
72
+ if (Number.isInteger(r3)) return String(r3);
73
+ return String(r3);
60
74
  }
61
- function trimNumber(n) {
62
- if (!Number.isFinite(n)) return "0";
63
- const r2 = Math.round(n * 1e6) / 1e6;
64
- if (Number.isInteger(r2)) return String(r2);
65
- return String(r2);
75
+ function anchorOffset(style, width) {
76
+ const edge = resolveAlign(style?.align ?? "left", style?.direction ?? "ltr");
77
+ return edge === "center" ? width / 2 : edge === "right" ? width : 0;
66
78
  }
67
79
  function patternSpecOf(paint) {
68
80
  return "tile" in paint.pattern ? paint.pattern : null;
@@ -110,32 +122,32 @@ function escapeAttr(s) {
110
122
  }
111
123
  function serializePathD(path) {
112
124
  if (path.kind === "rect") {
113
- const { x, y: y2, width, height } = path;
114
- return `M${trimNumber(x)} ${trimNumber(y2)}h${trimNumber(width)}v${trimNumber(height)}h${trimNumber(-width)}Z`;
125
+ const { x, y, width, height } = path;
126
+ return `M${trimNumber(x)} ${trimNumber(y)}h${trimNumber(width)}v${trimNumber(height)}h${trimNumber(-width)}Z`;
115
127
  }
116
128
  const cmds = path.commands;
117
129
  const xs = path.coords;
118
130
  const out = [];
119
131
  let ci = 0;
120
132
  for (let i = 0; i < cmds.length; i++) {
121
- const c2 = cmds[i];
122
- if (c2 === PATH_M) {
133
+ const c3 = cmds[i];
134
+ if (c3 === PATH_M) {
123
135
  out.push(`M${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
124
136
  ci += 2;
125
- } else if (c2 === PATH_L) {
137
+ } else if (c3 === PATH_L) {
126
138
  out.push(`L${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
127
139
  ci += 2;
128
- } else if (c2 === PATH_C) {
140
+ } else if (c3 === PATH_C) {
129
141
  out.push(
130
142
  `C${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])} ${trimNumber(xs[ci + 4])} ${trimNumber(xs[ci + 5])}`
131
143
  );
132
144
  ci += 6;
133
- } else if (c2 === PATH_Q) {
145
+ } else if (c3 === PATH_Q) {
134
146
  out.push(
135
147
  `Q${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])}`
136
148
  );
137
149
  ci += 4;
138
- } else if (c2 === PATH_Z) {
150
+ } else if (c3 === PATH_Z) {
139
151
  out.push("Z");
140
152
  }
141
153
  }
@@ -259,7 +271,7 @@ function serializeSvg(nodes, opts = {}) {
259
271
  }
260
272
  }
261
273
  const defsXml = registry.toDefsXml(opts.onWarn);
262
- const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces)).join("");
274
+ const bodyXml = nodes.map((n2) => nodeXml(n2, registry, namespaces)).join("");
263
275
  const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
264
276
  return `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
265
277
  }
@@ -304,18 +316,18 @@ function metaElementsXml(meta, namespaces) {
304
316
  return out;
305
317
  }
306
318
  function registerPaintServers(nodes, registry) {
307
- for (const n of nodes) {
308
- if (n.kind === "group") {
309
- registerPaintServers(n.children, registry);
310
- } else if (n.kind === "path") {
311
- if (n.fill.kind === "gradient") registry.register(n.fill.paint);
312
- if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
313
- registerMarkers(n.stroke, registry);
314
- } else if (n.kind === "text") {
315
- registerTextPaint(n.fill, registry);
316
- registerTextPaint(n.stroke?.paint, registry);
317
- registerMarkers(n.stroke, registry);
318
- for (const run of n.runs ?? []) {
319
+ for (const n2 of nodes) {
320
+ if (n2.kind === "group") {
321
+ registerPaintServers(n2.children, registry);
322
+ } else if (n2.kind === "path") {
323
+ if (n2.fill.kind === "gradient") registry.register(n2.fill.paint);
324
+ if (n2.stroke && n2.stroke.paint.kind === "gradient") registry.register(n2.stroke.paint.paint);
325
+ registerMarkers(n2.stroke, registry);
326
+ } else if (n2.kind === "text") {
327
+ registerTextPaint(n2.fill, registry);
328
+ registerTextPaint(n2.stroke?.paint, registry);
329
+ registerMarkers(n2.stroke, registry);
330
+ for (const run of n2.runs ?? []) {
319
331
  registerTextPaint(run.fill, registry);
320
332
  registerTextPaint(run.stroke?.paint, registry);
321
333
  }
@@ -377,19 +389,19 @@ function groupXml(node, registry, namespaces) {
377
389
  }
378
390
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
379
391
  const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
380
- const body = node.children.map((c2) => nodeXml(c2, registry, namespaces)).join("");
392
+ const body = node.children.map((c3) => nodeXml(c3, registry, namespaces)).join("");
381
393
  return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
382
394
  }
383
395
  function pathXml(node, registry, namespaces) {
384
396
  const attrs = [`d="${serializePathD(node.path)}"`];
385
397
  const fillAttrs = paintAttrs(node.fill, "fill", registry);
386
- for (const a of fillAttrs) attrs.push(a);
398
+ for (const a2 of fillAttrs) attrs.push(a2);
387
399
  if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
388
400
  attrs.push(`fill-rule="evenodd"`);
389
401
  }
390
402
  if (node.stroke) {
391
403
  const strokeAttrs = strokeAttrsFor(node.stroke, registry);
392
- for (const a of strokeAttrs) attrs.push(a);
404
+ for (const a2 of strokeAttrs) attrs.push(a2);
393
405
  } else {
394
406
  attrs.push('stroke="none"');
395
407
  }
@@ -397,9 +409,9 @@ function pathXml(node, registry, namespaces) {
397
409
  attrs.push(`opacity="${trimNumber(node.opacity)}"`);
398
410
  }
399
411
  if (node.rotation != null && node.rotation !== 0) {
400
- const b = pathBounds(node.path);
401
- const cx = b.minX + (b.maxX - b.minX) / 2;
402
- const cy = b.minY + (b.maxY - b.minY) / 2;
412
+ const b2 = pathBounds(node.path);
413
+ const cx = b2.minX + (b2.maxX - b2.minX) / 2;
414
+ const cy = b2.minY + (b2.maxY - b2.minY) / 2;
403
415
  const deg = node.rotation * 180 / Math.PI;
404
416
  attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
405
417
  }
@@ -422,11 +434,15 @@ function paintAttrs(paint, name, registry, includeOpacity = true) {
422
434
  const id = registry.register(paint.paint);
423
435
  return [`${name}="url(#${id})"`];
424
436
  }
437
+ function strokeWidthAttrs(width) {
438
+ if (typeof width === "number") return [`stroke-width="${trimNumber(width)}"`];
439
+ return [`stroke-width="${trimNumber(width.px)}"`, 'vector-effect="non-scaling-stroke"'];
440
+ }
425
441
  function coreStrokeAttrs(stroke, registry) {
426
442
  const paint = stroke?.paint;
427
443
  if (!stroke || !paint) return [];
428
- const width = resolveStrokeWidth(stroke.width ?? 1, 1);
429
- if (!(width > 0)) return [];
444
+ const width = stroke.width ?? 1;
445
+ if (!((typeof width === "object" ? width.px : width) > 0)) return [];
430
446
  const attrs = [];
431
447
  if ("color" in paint) {
432
448
  attrs.push(`stroke="${paint.color}"`);
@@ -436,7 +452,7 @@ function coreStrokeAttrs(stroke, registry) {
436
452
  } else {
437
453
  attrs.push(`stroke="url(#${registry.register(paint)})"`);
438
454
  }
439
- attrs.push(`stroke-width="${trimNumber(width)}"`);
455
+ attrs.push(...strokeWidthAttrs(width));
440
456
  if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
441
457
  if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
442
458
  if (stroke.dash && stroke.dash.length > 0) {
@@ -459,7 +475,7 @@ function coreStrokeAttrs(stroke, registry) {
459
475
  }
460
476
  function strokeAttrsFor(stroke, registry) {
461
477
  const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
462
- attrs.push(`stroke-width="${trimNumber(stroke.width)}"`);
478
+ attrs.push(...strokeWidthAttrs(stroke.width));
463
479
  const opacity = stroke.opacity ?? (stroke.paint.kind === "solid" ? stroke.paint.opacity : void 0);
464
480
  if (opacity != null && opacity !== 1) {
465
481
  attrs.push(`stroke-opacity="${trimNumber(opacity)}"`);
@@ -491,42 +507,42 @@ function strokeAttrsFor(stroke, registry) {
491
507
  }
492
508
  function computeBounds(nodes) {
493
509
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
494
- const visit = (n, ctm) => {
495
- if (n.kind === "group") {
496
- const childCtm = n.transform ? multiply(ctm, n.transform) : ctm;
497
- n.children.forEach((c2) => visit(c2, childCtm));
510
+ const visit = (n2, ctm) => {
511
+ if (n2.kind === "group") {
512
+ const childCtm = n2.transform ? multiply(ctm, n2.transform) : ctm;
513
+ n2.children.forEach((c3) => visit(c3, childCtm));
498
514
  return;
499
515
  }
500
- const b = n.kind === "text" || n.kind === "image" ? { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height } : pathBounds(n.path);
501
- const local = n.rotation ? rotateAboutCenter(ctm, b, n.rotation) : ctm;
502
- for (const [x, y2] of corners(b)) {
503
- const px = local[0] * x + local[2] * y2 + local[4];
504
- const py = local[1] * x + local[3] * y2 + local[5];
516
+ const b2 = n2.kind === "text" || n2.kind === "image" ? { minX: n2.x, minY: n2.y, maxX: n2.x + n2.width, maxY: n2.y + n2.height } : pathBounds(n2.path);
517
+ const local = n2.rotation ? rotateAboutCenter(ctm, b2, n2.rotation) : ctm;
518
+ for (const [x, y] of corners(b2)) {
519
+ const px = local[0] * x + local[2] * y + local[4];
520
+ const py = local[1] * x + local[3] * y + local[5];
505
521
  if (px < minX) minX = px;
506
522
  if (py < minY) minY = py;
507
523
  if (px > maxX) maxX = px;
508
524
  if (py > maxY) maxY = py;
509
525
  }
510
526
  };
511
- nodes.forEach((n) => visit(n, IDENTITY_MATRIX));
527
+ nodes.forEach((n2) => visit(n2, IDENTITY_MATRIX));
512
528
  if (!Number.isFinite(minX)) {
513
529
  return { x: 0, y: 0, width: 0, height: 0 };
514
530
  }
515
531
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
516
532
  }
517
- function corners(b) {
518
- return [[b.minX, b.minY], [b.maxX, b.minY], [b.maxX, b.maxY], [b.minX, b.maxY]];
533
+ function corners(b2) {
534
+ return [[b2.minX, b2.minY], [b2.maxX, b2.minY], [b2.maxX, b2.maxY], [b2.minX, b2.maxY]];
519
535
  }
520
- function rotateAboutCenter(ctm, b, angle) {
521
- const cx = (b.minX + b.maxX) / 2;
522
- const cy = (b.minY + b.maxY) / 2;
523
- const c2 = Math.cos(angle);
536
+ function rotateAboutCenter(ctm, b2, angle) {
537
+ const cx = (b2.minX + b2.maxX) / 2;
538
+ const cy = (b2.minY + b2.maxY) / 2;
539
+ const c3 = Math.cos(angle);
524
540
  const s = Math.sin(angle);
525
- return multiply(ctm, [c2, s, -s, c2, cx - cx * c2 + cy * s, cy - cx * s - cy * c2]);
541
+ return multiply(ctm, [c3, s, -s, c3, cx - cx * c3 + cy * s, cy - cx * s - cy * c3]);
526
542
  }
527
543
  function textXml(node, registry, namespaces) {
528
544
  const attrs = [
529
- `x="${trimNumber(node.x)}"`,
545
+ `x="${trimNumber(node.x + anchorOffset(node.style, node.width))}"`,
530
546
  `y="${trimNumber(node.y)}"`,
531
547
  `dominant-baseline="text-before-edge"`,
532
548
  // The runs model stores real line breaks; SVG's default whitespace
@@ -552,6 +568,7 @@ function textXml(node, registry, namespaces) {
552
568
  }
553
569
  const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
554
570
  if (decoration) attrs.push(`text-decoration="${decoration}"`);
571
+ if (style?.wrap) attrs.push('data-weasel-wrap="true"');
555
572
  if (node.fill === null) {
556
573
  attrs.push('fill="none"');
557
574
  } else if (node.fill) {
@@ -565,7 +582,7 @@ function textXml(node, registry, namespaces) {
565
582
  attrs.push(`fill="url(#${id})"`);
566
583
  }
567
584
  }
568
- for (const a of coreStrokeAttrs(node.stroke, registry)) attrs.push(a);
585
+ for (const a2 of coreStrokeAttrs(node.stroke, registry)) attrs.push(a2);
569
586
  if (node.opacity != null && node.opacity !== 1) {
570
587
  attrs.push(`opacity="${trimNumber(node.opacity)}"`);
571
588
  }
@@ -575,7 +592,7 @@ function textXml(node, registry, namespaces) {
575
592
  const deg = node.rotation * 180 / Math.PI;
576
593
  attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
577
594
  }
578
- const body = node.runs && node.runs.length > 0 ? node.runs.map((r2) => runXml(r2, registry)).join("") : escapeText(node.text);
595
+ const body = node.runs && node.runs.length > 0 ? node.runs.map((r3) => runXml(r3, registry)).join("") : escapeText(node.text);
579
596
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
580
597
  const metaEls = metaElementsXml(node.meta, namespaces);
581
598
  return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
@@ -607,7 +624,7 @@ function runXml(run, registry) {
607
624
  attrs.push(`fill="url(#${id})"`);
608
625
  }
609
626
  }
610
- for (const a of coreStrokeAttrs(run.stroke, registry)) attrs.push(a);
627
+ for (const a2 of coreStrokeAttrs(run.stroke, registry)) attrs.push(a2);
611
628
  const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
612
629
  return `${head}${escapeText(run.text)}</tspan>`;
613
630
  }
@@ -633,32 +650,32 @@ function pathBounds(path) {
633
650
  maxY: path.y + path.height
634
651
  };
635
652
  }
636
- const b = boundsOfPath(path);
637
- return { minX: b.x, minY: b.y, maxX: b.x + b.width, maxY: b.y + b.height };
653
+ const b2 = boundsOfPath(path);
654
+ return { minX: b2.x, minY: b2.y, maxX: b2.x + b2.width, maxY: b2.y + b2.height };
638
655
  }
639
656
  var MARK_COLOR = "#e5484d";
640
657
  var MARK_WIDTH = 2;
641
658
  var TEXT_SIZE = 14;
642
- var toWorld = (p, content) => ({
643
- x: p.x * content.w,
644
- y: p.y * content.h
659
+ var toWorld = (p2, content) => ({
660
+ x: p2.x * content.w,
661
+ y: p2.y * content.h
645
662
  });
646
663
  function vertices(m, content) {
647
664
  const stored = m.data.points;
648
- if (stored && stored.length >= 2) return stored.map((p) => toWorld(p, content));
649
- const { x, y: y2, width, height } = m.pose;
665
+ if (stored && stored.length >= 2) return stored.map((p2) => toWorld(p2, content));
666
+ const { x, y, width, height } = m.pose;
650
667
  return [
651
- { x, y: y2 },
652
- { x: x + width, y: y2 + height }
668
+ { x, y },
669
+ { x: x + width, y: y + height }
653
670
  ];
654
671
  }
655
672
  function polyline(points) {
656
- const b = new PathBuilder();
673
+ const b2 = new PathBuilder();
657
674
  const first = points[0];
658
675
  if (!first) return rectPath(0, 0, 0, 0);
659
- b.moveTo(first.x, first.y);
660
- for (const p of points.slice(1)) b.lineTo(p.x, p.y);
661
- return b.build();
676
+ b2.moveTo(first.x, first.y);
677
+ for (const p2 of points.slice(1)) b2.lineTo(p2.x, p2.y);
678
+ return b2.build();
662
679
  }
663
680
  function markCommands(m, content, style = {}) {
664
681
  const color = style.color ?? MARK_COLOR;
@@ -677,13 +694,13 @@ function markCommands(m, content, style = {}) {
677
694
  case "ellipse":
678
695
  return [{ kind: "path", path: ellipsePath(m.pose), stroke }];
679
696
  case "line": {
680
- const [a, b] = vertices(m, content);
681
- return [{ kind: "path", path: linePath(a, b), stroke }];
697
+ const [a2, b2] = vertices(m, content);
698
+ return [{ kind: "path", path: linePath(a2, b2), stroke }];
682
699
  }
683
700
  case "arrow": {
684
- const [a, b] = vertices(m, content);
701
+ const [a2, b2] = vertices(m, content);
685
702
  const marked = { ...stroke, markerEnd: "arrow" };
686
- const path = linePath(a, b);
703
+ const path = linePath(a2, b2);
687
704
  return [
688
705
  { kind: "path", path, stroke: marked },
689
706
  ...markerDrawCommands(path, marked, MARK_WIDTH, void 0)
@@ -717,8 +734,8 @@ function seenFrom(config, keys) {
717
734
  const out = {};
718
735
  if (config === null || typeof config !== "object") return out;
719
736
  const src = config;
720
- for (const k2 of keys) {
721
- if (k2 in src) out[k2] = src[k2];
737
+ for (const k of keys) {
738
+ if (k in src) out[k] = src[k];
722
739
  }
723
740
  return out;
724
741
  }
@@ -726,9 +743,9 @@ function isStale(seen, config, keys) {
726
743
  if (!seen || keys.length === 0) return false;
727
744
  if (config === null || typeof config !== "object") return false;
728
745
  const src = config;
729
- for (const k2 of keys) {
730
- if (!(k2 in seen)) continue;
731
- if (!Object.is(seen[k2], src[k2])) return true;
746
+ for (const k of keys) {
747
+ if (!(k in seen)) continue;
748
+ if (!Object.is(seen[k], src[k])) return true;
732
749
  }
733
750
  return false;
734
751
  }
@@ -745,6 +762,8 @@ function resolveMarkStyle(data, opts) {
745
762
  stale: isStale(data.seen, opts.config, opts.positionDependsOn ?? [])
746
763
  };
747
764
  }
765
+
766
+ // src/annotations/svgNodes.ts
748
767
  var NO_PAINT = { kind: "none" };
749
768
  function toSvgPaint(paint) {
750
769
  if (!paint) return NO_PAINT;
@@ -760,7 +779,7 @@ function toSvgPaint(paint) {
760
779
  function toSvgStroke(stroke) {
761
780
  return {
762
781
  paint: toSvgPaint(stroke.paint),
763
- width: resolveStrokeWidth(stroke.width ?? 1, 1),
782
+ width: stroke.width ?? 1,
764
783
  ...stroke.cap ? { cap: stroke.cap } : {},
765
784
  ...stroke.join ? { join: stroke.join } : {},
766
785
  ...stroke.dash ? { dash: [...stroke.dash] } : {},
@@ -800,7 +819,7 @@ function capturePlan(base, format) {
800
819
  if (format === "svg") return "svg-document";
801
820
  return base?.kind === "svg" ? "svg-document" : "raster-stack";
802
821
  }
803
- function nestBaseSvg(markup, w3, h) {
822
+ function nestBaseSvg(markup, w2, h2) {
804
823
  const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
805
824
  const root = doc.documentElement;
806
825
  if (doc.getElementsByTagName("parsererror").length > 0 || root.localName !== "svg") {
@@ -808,17 +827,17 @@ function nestBaseSvg(markup, w3, h) {
808
827
  }
809
828
  root.setAttribute("x", "0");
810
829
  root.setAttribute("y", "0");
811
- root.setAttribute("width", String(w3));
812
- root.setAttribute("height", String(h));
830
+ root.setAttribute("width", String(w2));
831
+ root.setAttribute("height", String(h2));
813
832
  return new XMLSerializer().serializeToString(root);
814
833
  }
815
- function embedRasterBase(base, w3, h, onWarn) {
834
+ function embedRasterBase(base, w2, h2, onWarn) {
816
835
  const href = base.kind === "canvas" ? base.canvas.toDataURL() : base.kind === "image" ? base.src : "";
817
836
  if (base.kind === "image" && !href.startsWith("data:")) {
818
837
  onWarn?.(`base image '${href}' is a reference, so this SVG is not self-contained`);
819
838
  }
820
- return serializeSvg([{ kind: "image", href, x: 0, y: 0, width: w3, height: h }], {
821
- viewBox: { x: 0, y: 0, width: w3, height: h }
839
+ return serializeSvg([{ kind: "image", href, x: 0, y: 0, width: w2, height: h2 }], {
840
+ viewBox: { x: 0, y: 0, width: w2, height: h2 }
822
841
  });
823
842
  }
824
843
  function markNodesOf(scene, draw) {
@@ -834,15 +853,15 @@ function markNodesOf(scene, draw) {
834
853
  }
835
854
  function composeCaptureSvg(args) {
836
855
  const { base, scene, draw, scale, onWarn } = args;
837
- const { w: w3, h } = draw.content;
838
- const baseXml = base === void 0 ? "" : base.kind === "svg" ? nestBaseSvg(base.markup, w3, h) : embedRasterBase(base, w3, h, onWarn);
856
+ const { w: w2, h: h2 } = draw.content;
857
+ const baseXml = base === void 0 ? "" : base.kind === "svg" ? nestBaseSvg(base.markup, w2, h2) : embedRasterBase(base, w2, h2, onWarn);
839
858
  const marksXml = serializeSvg(markNodesOf(scene, draw), {
840
- viewBox: { x: 0, y: 0, width: w3, height: h },
859
+ viewBox: { x: 0, y: 0, width: w2, height: h2 },
841
860
  onWarn
842
861
  });
843
- const outW = Math.max(1, Math.round(w3 * scale));
844
- const outH = Math.max(1, Math.round(h * scale));
845
- return `<svg xmlns="${SVG_NS}" width="${outW}" height="${outH}" viewBox="0 0 ${w3} ${h}">${baseXml}${marksXml}</svg>`;
862
+ const outW = Math.max(1, Math.round(w2 * scale));
863
+ const outH = Math.max(1, Math.round(h2 * scale));
864
+ return `<svg xmlns="${SVG_NS}" width="${outW}" height="${outH}" viewBox="0 0 ${w2} ${h2}">${baseXml}${marksXml}</svg>`;
846
865
  }
847
866
  function context2d(width, height) {
848
867
  const canvas = document.createElement("canvas");
@@ -936,32 +955,32 @@ async function captureTarget(args, opts = {}) {
936
955
  }
937
956
 
938
957
  // src/annotations/frac.ts
939
- function fracToWorld(f2, content) {
958
+ function fracToWorld(f3, content) {
940
959
  return {
941
- x: f2.x * content.w,
942
- y: f2.y * content.h,
943
- width: f2.w * content.w,
944
- height: f2.h * content.h
960
+ x: f3.x * content.w,
961
+ y: f3.y * content.h,
962
+ width: f3.w * content.w,
963
+ height: f3.h * content.h
945
964
  };
946
965
  }
947
- function worldToFrac(r2, content) {
966
+ function worldToFrac(r3, content) {
948
967
  const sx = content.w === 0 ? 0 : 1 / content.w;
949
968
  const sy = content.h === 0 ? 0 : 1 / content.h;
950
- return { x: r2.x * sx, y: r2.y * sy, w: r2.width * sx, h: r2.height * sy };
969
+ return { x: r3.x * sx, y: r3.y * sy, w: r3.width * sx, h: r3.height * sy };
951
970
  }
952
971
  var DP = 1e4;
953
- function roundFrac(f2) {
972
+ function roundFrac(f3) {
954
973
  return {
955
- x: Math.round(f2.x * DP) / DP,
956
- y: Math.round(f2.y * DP) / DP,
957
- w: Math.round(f2.w * DP) / DP,
958
- h: Math.round(f2.h * DP) / DP
974
+ x: Math.round(f3.x * DP) / DP,
975
+ y: Math.round(f3.y * DP) / DP,
976
+ w: Math.round(f3.w * DP) / DP,
977
+ h: Math.round(f3.h * DP) / DP
959
978
  };
960
979
  }
961
980
  function fracContains(box, pt, tol = 0) {
962
981
  return pt.x >= box.x - tol && pt.x <= box.x + box.w + tol && pt.y >= box.y - tol && pt.y <= box.y + box.h + tol;
963
982
  }
964
- function fracIntersects(outer, inner) {
983
+ function fracEncloses(outer, inner) {
965
984
  return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.w <= outer.x + outer.w && inner.y + inner.h <= outer.y + outer.h;
966
985
  }
967
986
 
@@ -1098,21 +1117,21 @@ function createAnnotationStore(opts) {
1098
1117
  if (!scene) return [];
1099
1118
  const out = [];
1100
1119
  for (const id of scene.renderOrder()) {
1101
- const a = project(target, String(id));
1102
- if (a) out.push(a);
1120
+ const a2 = project(target, String(id));
1121
+ if (a2) out.push(a2);
1103
1122
  }
1104
1123
  return out;
1105
1124
  };
1106
1125
  const all = () => liveTargets().flatMap(marksOn);
1107
- const matches = (a, q) => {
1108
- if (q.target !== void 0 && a.target !== q.target) return false;
1109
- if (q.kind !== void 0 && a.kind !== q.kind) return false;
1110
- if (q.status !== void 0 && a.status !== q.status) return false;
1126
+ const matches = (a2, q) => {
1127
+ if (q.target !== void 0 && a2.target !== q.target) return false;
1128
+ if (q.kind !== void 0 && a2.kind !== q.kind) return false;
1129
+ if (q.status !== void 0 && a2.status !== q.status) return false;
1111
1130
  if (q.tags !== void 0) {
1112
- const have = a.tags ?? [];
1131
+ const have = a2.tags ?? [];
1113
1132
  for (const t of q.tags) if (!have.includes(t)) return false;
1114
1133
  }
1115
- if (q.where && !q.where(a)) return false;
1134
+ if (q.where && !q.where(a2)) return false;
1116
1135
  return true;
1117
1136
  };
1118
1137
  const nodeOf = (id) => {
@@ -1130,17 +1149,17 @@ function createAnnotationStore(opts) {
1130
1149
  return parts ? project(parts.target, parts.node) : void 0;
1131
1150
  },
1132
1151
  query(q) {
1133
- return q ? all().filter((a) => matches(a, q)) : all();
1152
+ return q ? all().filter((a2) => matches(a2, q)) : all();
1134
1153
  },
1135
1154
  hitTest(target, pt, tol = 0) {
1136
- return marksOn(target).filter((a) => fracContains(a.frac, pt, tol)).reverse();
1155
+ return marksOn(target).filter((a2) => fracContains(a2.frac, pt, tol)).reverse();
1137
1156
  },
1138
1157
  within(target, box) {
1139
- return marksOn(target).filter((a) => fracIntersects(box, a.frac));
1158
+ return marksOn(target).filter((a2) => fracEncloses(box, a2.frac));
1140
1159
  },
1141
- isStale(a, config) {
1142
- const keys = targetOf(a.target)?.positionDependsOn ?? [];
1143
- return isStale(a.seen, config, keys);
1160
+ isStale(a2, config) {
1161
+ const keys = targetOf(a2.target)?.positionDependsOn ?? [];
1162
+ return isStale(a2.seen, config, keys);
1144
1163
  },
1145
1164
  canUndo() {
1146
1165
  return history.canUndo(historySceneAt);
@@ -1178,7 +1197,7 @@ function createAnnotationStore(opts) {
1178
1197
  for (const [target, scene] of scenes) {
1179
1198
  const next = wanted.get(target) ?? [];
1180
1199
  const now = scene.getSelection();
1181
- if (now.length === next.length && next.every((n, i) => String(now[i]) === n)) continue;
1200
+ if (now.length === next.length && next.every((n2, i) => String(now[i]) === n2)) continue;
1182
1201
  scene.setSelection(next.map(asNodeId$1));
1183
1202
  }
1184
1203
  },
@@ -1240,7 +1259,7 @@ function createAnnotationStore(opts) {
1240
1259
  content: info.content,
1241
1260
  positionDependsOn: info.positionDependsOn,
1242
1261
  config: opts.config?.(),
1243
- meaning: opts.meaning
1262
+ meaning: typeof opts.meaning === "function" ? opts.meaning() : opts.meaning
1244
1263
  },
1245
1264
  base: info.base,
1246
1265
  onWarn: (message) => console.warn(`[labkit] capture: ${message}`)
@@ -1263,6 +1282,32 @@ function annotationsFromJSON(raw, targets, rest = {}) {
1263
1282
  return createAnnotationStore({ targets, restore: doc.scenes, ...rest });
1264
1283
  }
1265
1284
 
1285
+ // src/annotations/toolMap.ts
1286
+ var TOOLS = {
1287
+ select: { weaselTool: "select" },
1288
+ stroke: { weaselTool: "pencil", kind: "stroke" },
1289
+ line: { weaselTool: "line", kind: "line" },
1290
+ arrow: { weaselTool: "line", kind: "arrow" },
1291
+ rect: { weaselTool: "rect", kind: "rect" },
1292
+ ellipse: { weaselTool: "ellipse", kind: "ellipse" },
1293
+ text: { weaselTool: "text", kind: "text" }
1294
+ };
1295
+ var ANNOTATION_TOOLS = [
1296
+ { id: "select", label: "Select", icon: p, group: "annotate" },
1297
+ { id: "stroke", label: "Freehand", icon: u, group: "annotate" },
1298
+ { id: "line", label: "Line", icon: c, group: "annotate" },
1299
+ { id: "arrow", label: "Arrow", icon: n, group: "annotate" },
1300
+ { id: "rect", label: "Rectangle", icon: f, group: "annotate" },
1301
+ { id: "ellipse", label: "Ellipse", icon: r2, group: "annotate" },
1302
+ { id: "text", label: "Text", icon: h, group: "annotate" }
1303
+ ];
1304
+ function annotationToolInfo(id) {
1305
+ return id === null ? void 0 : TOOLS[id];
1306
+ }
1307
+ var ANNOTATION_WEASEL_TOOLS = [
1308
+ ...new Set(Object.values(TOOLS).map((t) => t.weaselTool))
1309
+ ];
1310
+
1266
1311
  // src/annotations/view.ts
1267
1312
  function toWeaselView(v) {
1268
1313
  return { x: v.pan.x, y: v.pan.y, scale: { x: v.zoom, y: v.zoom } };
@@ -1274,7 +1319,7 @@ function fitView(content, pane) {
1274
1319
  const zoom = content.w > 0 && content.h > 0 ? Math.min(pane.w / content.w, pane.h / content.h) : 1;
1275
1320
  return { zoom: Number.isFinite(zoom) && zoom > 0 ? zoom : 1, pan: { x: 0, y: 0 } };
1276
1321
  }
1277
- var sameRect = (a, b) => a !== null && a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
1322
+ var sameRect = (a2, b2) => a2 !== null && a2.x === b2.x && a2.y === b2.y && a2.w === b2.w && a2.h === b2.h;
1278
1323
  function ToolBridge({ toolId }) {
1279
1324
  const tools = useActiveToolContext();
1280
1325
  const setActive = tools.setActive;
@@ -1284,9 +1329,9 @@ function ToolBridge({ toolId }) {
1284
1329
  return null;
1285
1330
  }
1286
1331
  function pointsOf(extras, content) {
1287
- const toFrac = (p) => ({
1288
- x: content.w === 0 ? 0 : p.x / content.w,
1289
- y: content.h === 0 ? 0 : p.y / content.h
1332
+ const toFrac = (p2) => ({
1333
+ x: content.w === 0 ? 0 : p2.x / content.w,
1334
+ y: content.h === 0 ? 0 : p2.y / content.h
1290
1335
  });
1291
1336
  if (extras.kind === "line") {
1292
1337
  return [toFrac(extras.a), toFrac(extras.b)];
@@ -1328,9 +1373,40 @@ function AnnotationOverlay({
1328
1373
  },
1329
1374
  [surface, tileId]
1330
1375
  );
1376
+ const painted = useRef(false);
1377
+ const canvasRef = useRef(canvas);
1378
+ if (canvasRef.current !== canvas) painted.current = false;
1379
+ canvasRef.current = canvas;
1380
+ const unsubscribeFrame = useRef(null);
1381
+ const attachSceneCanvas = useCallback((api) => {
1382
+ sceneCanvas.current = api;
1383
+ unsubscribeFrame.current?.();
1384
+ unsubscribeFrame.current = api ? api.subscribeFrame(() => {
1385
+ painted.current = true;
1386
+ }) : null;
1387
+ }, []);
1388
+ useEffect(() => {
1389
+ if (!surface) return;
1390
+ return surface.registerClear(tileId, (size, dpr) => {
1391
+ if (!painted.current) return;
1392
+ const gl = canvasRef.current?.getContext("webgl2");
1393
+ if (!gl) return;
1394
+ gl.disable(gl.SCISSOR_TEST);
1395
+ gl.viewport(0, 0, Math.round(size.width * dpr), Math.round(size.height * dpr));
1396
+ gl.clearColor(0, 0, 0, 0);
1397
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
1398
+ });
1399
+ }, [surface, tileId]);
1400
+ const wheelSlot = useContext(CameraWheelContext);
1401
+ useEffect(() => {
1402
+ if (!input || !wheelSlot) return;
1403
+ const wheel = (e) => wheelSlot.current?.(e);
1404
+ input.addEventListener("wheel", wheel, { passive: false });
1405
+ return () => input.removeEventListener("wheel", wheel);
1406
+ }, [input, wheelSlot]);
1331
1407
  useEffect(
1332
1408
  () => surface?.registerPainter(tileId, (next) => {
1333
- setRect((prev) => sameRect(prev, next) ? prev : next);
1409
+ flushSync(() => setRect((prev) => sameRect(prev, next) ? prev : next));
1334
1410
  sceneCanvas.current?.requestRedraw();
1335
1411
  }),
1336
1412
  [surface, tileId]
@@ -1388,7 +1464,7 @@ function AnnotationOverlay({
1388
1464
  /* @__PURE__ */ jsx(
1389
1465
  SceneCanvas,
1390
1466
  {
1391
- ref: sceneCanvas,
1467
+ ref: attachSceneCanvas,
1392
1468
  scene,
1393
1469
  width: rect.w,
1394
1470
  height: rect.h,
@@ -1428,314 +1504,6 @@ function AnnotationTargets({
1428
1504
  )) });
1429
1505
  }
1430
1506
 
1431
- // src/config/builder.ts
1432
- function isConfigBranch(entry) {
1433
- return "children" in entry;
1434
- }
1435
- var BaseNode = class {
1436
- constructor(defaultValue, annotations = {}, options = {}) {
1437
- this.annotations = annotations;
1438
- this.options = options;
1439
- this.default = defaultValue;
1440
- }
1441
- annotations;
1442
- options;
1443
- // `default` is a reserved word in a parameter position, so the field is
1444
- // declared rather than taken as a parameter property.
1445
- default;
1446
- /** Clone into the same subclass. Subclasses with extra constructor
1447
- * arguments override this. */
1448
- with(annotations, options) {
1449
- const Ctor = this.constructor;
1450
- return new Ctor(this.default, annotations, options);
1451
- }
1452
- ann(patch) {
1453
- return this.with({ ...this.annotations, ...patch }, this.options);
1454
- }
1455
- opt(patch) {
1456
- return this.with(this.annotations, { ...this.options, ...patch });
1457
- }
1458
- /** Human-readable label. Defaults to the key, title-cased. */
1459
- label(name) {
1460
- return this.ann({ name });
1461
- }
1462
- /** Longer help text, surfaced as a tooltip on the row label. */
1463
- describe(description) {
1464
- return this.ann({ description });
1465
- }
1466
- /** Omit from the panel unless it is asked to show hidden leaves. */
1467
- hidden() {
1468
- return this.ann({ hidden: true });
1469
- }
1470
- /** Pair with a sibling sharing this id, side-by-side on one row. */
1471
- pair(pair) {
1472
- return this.ann({ pair });
1473
- }
1474
- /** Render under a named section heading. `collapsed` opens the section
1475
- * folded — say it on any one of the section's leaves. */
1476
- section(label, opts = {}) {
1477
- return this.opt({ section: { label, ...opts } });
1478
- }
1479
- /** Show this row only while the predicate holds. Presentational — the value
1480
- * stays in config and the instrument still reads it. */
1481
- showIf(predicate) {
1482
- return this.opt({ showIf: predicate });
1483
- }
1484
- /** Draw this one row yourself, keeping the kind, default and validation. */
1485
- render(renderer) {
1486
- return this.opt({ render: renderer });
1487
- }
1488
- };
1489
- var NumberNode = class extends BaseNode {
1490
- kind = "number";
1491
- /** Bound the value. A number with both bounds renders as a slider. */
1492
- range(min, max) {
1493
- return this.ann({ min, max });
1494
- }
1495
- step(step) {
1496
- return this.ann({ step });
1497
- }
1498
- /** A display suffix for the value — `'px'`, `'ms'`, `'°'`. Presentation only:
1499
- * it is never parsed, and the stored value stays a plain number. */
1500
- suffix(suffix) {
1501
- return this.ann({ suffix });
1502
- }
1503
- /** Force a slider even without both bounds. */
1504
- slider() {
1505
- return this.ann({ control: "slider" });
1506
- }
1507
- /** Force a typed input even when both bounds are set. */
1508
- input() {
1509
- return this.ann({ control: "input" });
1510
- }
1511
- };
1512
- var BooleanNode = class extends BaseNode {
1513
- kind = "boolean";
1514
- /** Ask for a switch. `ControlPanel` still draws a checkbox; weasel-ui's
1515
- * `PrefsForm` honors the distinction. */
1516
- toggle() {
1517
- return this.ann({ control: "switch" });
1518
- }
1519
- };
1520
- var StringNode = class extends BaseNode {
1521
- kind = "string";
1522
- placeholder(placeholder) {
1523
- return this.ann({ placeholder });
1524
- }
1525
- maxLength(maxLength) {
1526
- return this.ann({ maxLength });
1527
- }
1528
- /** Milliseconds to debounce live writes. 0 commits every keystroke. */
1529
- debounce(debounceMs) {
1530
- return this.ann({ debounceMs });
1531
- }
1532
- };
1533
- var ColorNode = class extends BaseNode {
1534
- kind = "color";
1535
- };
1536
- var EnumNode = class extends BaseNode {
1537
- kind = "enum";
1538
- /** Render as a segmented control rather than a select. */
1539
- radio() {
1540
- return this.ann({ control: "radio" });
1541
- }
1542
- /** Relabel the choices. Named `labels` rather than `options` because
1543
- * `options` is the node's own extras bag. */
1544
- labels(options) {
1545
- return this.ann({ options });
1546
- }
1547
- };
1548
- var ValueNode = class extends BaseNode {
1549
- kind = null;
1550
- };
1551
- var CustomNode = class _CustomNode extends BaseNode {
1552
- constructor(kind, defaultValue, annotations = {}, options = {}) {
1553
- super(defaultValue, annotations, options);
1554
- this.kind = kind;
1555
- }
1556
- kind;
1557
- with(annotations, options) {
1558
- return new _CustomNode(this.kind, this.default, annotations, options);
1559
- }
1560
- };
1561
- var GroupNode = class _GroupNode {
1562
- constructor(children, annotations = {}, options = {}) {
1563
- this.children = children;
1564
- this.annotations = annotations;
1565
- this.options = options;
1566
- }
1567
- children;
1568
- annotations;
1569
- options;
1570
- with(annotations, options) {
1571
- return new _GroupNode(this.children, annotations, options);
1572
- }
1573
- /** Heading for the group's rows. Defaults to the key, title-cased. */
1574
- label(name) {
1575
- return this.with({ ...this.annotations, name }, this.options);
1576
- }
1577
- /** Longer help text. Carried on the resolved `PrefGroup`, where weasel-ui's
1578
- * `PrefsForm` draws it under the heading; `ControlPanel` has no place for
1579
- * it yet and shows the heading alone. */
1580
- describe(description) {
1581
- return this.with({ ...this.annotations, description }, this.options);
1582
- }
1583
- /** Render this whole group under a named section heading. */
1584
- section(label, opts = {}) {
1585
- return this.with(this.annotations, { ...this.options, section: { label, ...opts } });
1586
- }
1587
- /** Show this group only while the predicate holds. Presentational — every
1588
- * value beneath it stays in config and the instrument still reads it. */
1589
- showIf(predicate) {
1590
- return this.with(this.annotations, { ...this.options, showIf: predicate });
1591
- }
1592
- };
1593
- function shapeDefaults(shape) {
1594
- const out = {};
1595
- for (const [key, entry] of Object.entries(shape)) {
1596
- out[key] = isConfigBranch(entry) ? shapeDefaults(entry.children) : entry.default;
1597
- }
1598
- return out;
1599
- }
1600
- var expand = (options) => options.map((o) => typeof o === "string" ? { value: o, label: o } : o);
1601
- var f = {
1602
- number: (def) => new NumberNode(def),
1603
- boolean: (def) => new BooleanNode(def),
1604
- string: (def) => new StringNode(def),
1605
- color: (def) => new ColorNode(def),
1606
- /** A fixed set of choices. The default's literal type flows into the config
1607
- * type, so `f.enum('fast', ['fast', 'accurate'])` gives
1608
- * `'fast' | 'accurate'` with no `as const`. */
1609
- enum: (def, options) => new EnumNode(def, { options: expand(options) }),
1610
- /** A leaf with no declared kind: the rule chain decides, which is how a lab
1611
- * states a convention like "every `*Color` key is a color picker". */
1612
- value: (def) => new ValueNode(def),
1613
- /** A branch: its children nest under this key, so `grid: f.group({ size })`
1614
- * puts the value at `config.grid.size` and addresses it as `'grid.size'`.
1615
- * Groups nest as deeply as the schema wants. */
1616
- group: (children) => new GroupNode(children),
1617
- /** A leaf of a kind a lab supplies the control for, through `controls`. */
1618
- custom: (kind, def, validate) => new CustomNode(kind, def, {}, validate ? { validate } : {}),
1619
- /** Collect leaves and groups into an instrument's config. */
1620
- schema(nodes) {
1621
- return { nodes, defaults: () => shapeDefaults(nodes) };
1622
- }
1623
- };
1624
-
1625
- // src/config/rules.ts
1626
- function titleCase(key) {
1627
- const words = key.replace(/[_-]+/g, " ").split(/(?<=[a-z0-9])(?=[A-Z])/).join(" ").trim().toLowerCase();
1628
- return words.length === 0 ? words : words[0].toUpperCase() + words.slice(1);
1629
- }
1630
- var kindFromValue = (ctx) => {
1631
- const t = typeof ctx.default;
1632
- if (t === "boolean" || t === "number" || t === "string") return { kind: t };
1633
- return null;
1634
- };
1635
- var labelFromKey = (ctx) => ({ name: titleCase(ctx.key) });
1636
- var sliderWhenBounded = (ctx) => {
1637
- if (ctx.leaf.kind !== "number") return null;
1638
- const bounded = ctx.leaf.min !== void 0 && ctx.leaf.max !== void 0;
1639
- return bounded ? { control: "slider" } : null;
1640
- };
1641
- var descriptionDefault = () => ({ description: "" });
1642
- var builtinRules = [
1643
- kindFromValue,
1644
- labelFromKey,
1645
- sliderWhenBounded,
1646
- descriptionDefault
1647
- ];
1648
- function applyRules(seed, ctx, rules) {
1649
- const acc = { ...seed };
1650
- for (const rule of rules) {
1651
- const patch = rule({ ...ctx, leaf: acc });
1652
- if (patch === null) continue;
1653
- for (const [k2, v] of Object.entries(patch)) {
1654
- if (v !== void 0 && acc[k2] === void 0) {
1655
- acc[k2] = v;
1656
- }
1657
- }
1658
- }
1659
- return acc;
1660
- }
1661
-
1662
- // src/config/resolve.ts
1663
- function defined(annotations) {
1664
- const out = {};
1665
- for (const [k2, v] of Object.entries(annotations)) if (v !== void 0) out[k2] = v;
1666
- return out;
1667
- }
1668
- function resolveConfigSchema(schema, rules = []) {
1669
- const sink = {
1670
- sections: [],
1671
- showIf: /* @__PURE__ */ new Map(),
1672
- renderers: {},
1673
- chain: [...rules, ...builtinRules]
1674
- };
1675
- const group = resolveShape(schema.nodes, "", "", sink);
1676
- return { group, sections: sink.sections, showIf: sink.showIf, renderers: sink.renderers };
1677
- }
1678
- function resolveShape(shape, at, name, sink) {
1679
- const children = {};
1680
- const sections = /* @__PURE__ */ new Map();
1681
- for (const [key, entry] of Object.entries(shape)) {
1682
- const path = at === "" ? key : `${at}.${key}`;
1683
- children[key] = resolveEntry(entry, key, path, sink);
1684
- const { section, showIf: predicate } = entry.options;
1685
- if (section !== void 0) {
1686
- let spec = sections.get(section.label);
1687
- if (!spec) {
1688
- spec = { at, label: section.label, paths: [] };
1689
- sections.set(section.label, spec);
1690
- sink.sections.push(spec);
1691
- }
1692
- spec.paths.push(path);
1693
- if (section.collapsed !== void 0) {
1694
- spec.collapsed = (spec.collapsed ?? false) || section.collapsed;
1695
- }
1696
- }
1697
- if (predicate) sink.showIf.set(path, predicate);
1698
- if (!isConfigBranch(entry) && entry.options.render) sink.renderers[path] = entry.options.render;
1699
- }
1700
- return { name, children };
1701
- }
1702
- function resolveEntry(entry, key, path, sink) {
1703
- if (isConfigBranch(entry)) {
1704
- const group = resolveShape(
1705
- entry.children,
1706
- path,
1707
- entry.annotations.name ?? titleCase(key),
1708
- sink
1709
- );
1710
- return entry.annotations.description === void 0 ? group : { ...group, description: entry.annotations.description };
1711
- }
1712
- const seed = defined({
1713
- ...entry.annotations,
1714
- ...entry.kind === null ? {} : { kind: entry.kind }
1715
- });
1716
- const patch = applyRules(seed, { key, path, default: entry.default }, sink.chain);
1717
- return { ...patch, default: entry.default };
1718
- }
1719
- var LabContext = createContext(null);
1720
- function useLabContext() {
1721
- const ctx = useContext(LabContext);
1722
- if (ctx === null) {
1723
- throw new Error("useLabContext must be used inside <Lab>");
1724
- }
1725
- return ctx;
1726
- }
1727
-
1728
- // src/config/useConfigSchema.ts
1729
- var NO_RULES = [];
1730
- function useConfigSchema(instrument) {
1731
- const lab = useContext(LabContext);
1732
- const rules = lab?.configRules ?? NO_RULES;
1733
- return useMemo(
1734
- () => instrument.config ? resolveConfigSchema(instrument.config, rules) : fromConfigFields(instrument.configSchema?.() ?? []),
1735
- [instrument, rules]
1736
- );
1737
- }
1738
-
1739
1507
  // src/instrument/defineInstrument.ts
1740
1508
  function defineInstrument(spec) {
1741
1509
  if (spec.config && !("defaultConfig" in spec && spec.defaultConfig)) {
@@ -1794,7 +1562,7 @@ function validateConfigSchema(fields) {
1794
1562
  return { valid: errors.length === 0, errors };
1795
1563
  }
1796
1564
 
1797
- // ../theme/dist/chunk-CXVFAOTM.js
1565
+ // ../theme/dist/chunk-VKZ4JIVX.js
1798
1566
  var THEME_SOURCES = {
1799
1567
  "weasel": {
1800
1568
  "primitives": {
@@ -1931,46 +1699,46 @@ var THEME_SOURCES = {
1931
1699
  "alpha": 0.18,
1932
1700
  "description": "Pressed wash over a raised surface."
1933
1701
  },
1934
- "swatch-green": {
1702
+ "swatch-fuchsia": {
1935
1703
  "type": "color",
1936
- "value": "#2fdd18",
1937
- "description": "First of ten categorical spot colors for plotting, status and data viz. Ordered \u2014 take them in sequence. Mode-invariant on purpose: a series keeps its color when the mode flips."
1704
+ "value": "#f641f7",
1705
+ "description": "First of ten categorical spot colors for plotting, status and data viz. Take them in sequence: the order is farthest-point over hue, so the first N are as separated as N colors can be (first three 87\xB0 apart, all ten 32\xB0). Mode-invariant on purpose \u2014 a series keeps its color when the mode flips, which is why they are tuned for a dark surface and clear only 1.5:1 on a light one."
1938
1706
  },
1939
- "swatch-pink": {
1707
+ "swatch-green": {
1940
1708
  "type": "color",
1941
- "value": "#ff5885"
1709
+ "value": "#48e628"
1942
1710
  },
1943
- "swatch-cyan": {
1711
+ "swatch-sky": {
1944
1712
  "type": "color",
1945
- "value": "#00dfff"
1713
+ "value": "#36bff2"
1946
1714
  },
1947
- "swatch-gold": {
1715
+ "swatch-amber": {
1948
1716
  "type": "color",
1949
- "value": "#dcb700"
1717
+ "value": "#e69628"
1950
1718
  },
1951
- "swatch-amber": {
1719
+ "swatch-teal": {
1952
1720
  "type": "color",
1953
- "value": "#ff8c00"
1721
+ "value": "#3ee1cb"
1954
1722
  },
1955
- "swatch-violet": {
1723
+ "swatch-red": {
1956
1724
  "type": "color",
1957
- "value": "#a497ff"
1725
+ "value": "#f75034"
1958
1726
  },
1959
- "swatch-mint": {
1727
+ "swatch-blue": {
1960
1728
  "type": "color",
1961
- "value": "#00e7af"
1729
+ "value": "#407ef7"
1962
1730
  },
1963
- "swatch-sky": {
1731
+ "swatch-citron": {
1964
1732
  "type": "color",
1965
- "value": "#00b8ff"
1733
+ "value": "#d5d23a"
1966
1734
  },
1967
- "swatch-orange": {
1735
+ "swatch-rose": {
1968
1736
  "type": "color",
1969
- "value": "#ff6b00"
1737
+ "value": "#fa4590"
1970
1738
  },
1971
- "swatch-magenta": {
1739
+ "swatch-violet": {
1972
1740
  "type": "color",
1973
- "value": "#ff6eff"
1741
+ "value": "#955cf9"
1974
1742
  },
1975
1743
  "radius-sm": {
1976
1744
  "type": "dimension",
@@ -2023,15 +1791,15 @@ var THEME_SOURCES = {
2023
1791
  "value": "8px",
2024
1792
  "description": "Diameter of a linear range thumb."
2025
1793
  },
2026
- "slider-track-tint": {
1794
+ "slider-track-mix": {
2027
1795
  "type": "dimension",
2028
1796
  "value": "18%",
2029
- "description": "Accent proportion in a range track. Mixed into a real property, never into another custom property."
1797
+ "description": "Accent proportion in a range track, as a color-mix percentage \u2014 not a color. Mixed into a real property, never into another custom property."
2030
1798
  },
2031
- "slider-thumb-tint": {
1799
+ "slider-thumb-mix": {
2032
1800
  "type": "dimension",
2033
1801
  "value": "70%",
2034
- "description": "Accent proportion in a range thumb."
1802
+ "description": "Accent proportion in a range thumb, as a color-mix percentage \u2014 not a color."
2035
1803
  },
2036
1804
  "field-pad-x": {
2037
1805
  "type": "dimension",
@@ -2271,7 +2039,8 @@ var THEME_SOURCES = {
2271
2039
  },
2272
2040
  "border-strong": {
2273
2041
  "type": "color",
2274
- "value": "{color.gray-600}"
2042
+ "value": "{color.gray-400}",
2043
+ "description": "The line that has to be seen: a control's edge, a raised surface's outline. Clears WCAG 1.4.11's 3:1 against every surface in both modes."
2275
2044
  },
2276
2045
  "accent-fg": {
2277
2046
  "type": "color",
@@ -2287,11 +2056,6 @@ var THEME_SOURCES = {
2287
2056
  "type": "color",
2288
2057
  "value": "rgba(0, 0, 0, 0.6)",
2289
2058
  "description": "Elevation. Always darker than any surface \u2014 never derived from fg, which is near-white on dark."
2290
- },
2291
- "border-raised": {
2292
- "type": "color",
2293
- "value": "{color.gray-400}",
2294
- "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."
2295
2059
  }
2296
2060
  },
2297
2061
  "light": {
@@ -2325,7 +2089,7 @@ var THEME_SOURCES = {
2325
2089
  },
2326
2090
  "border-strong": {
2327
2091
  "type": "color",
2328
- "value": "{color.gray-300}"
2092
+ "value": "{color.gray-500}"
2329
2093
  },
2330
2094
  "accent-fg": {
2331
2095
  "type": "color",
@@ -2340,10 +2104,6 @@ var THEME_SOURCES = {
2340
2104
  "shadow": {
2341
2105
  "type": "color",
2342
2106
  "value": "rgba(0, 0, 0, 0.18)"
2343
- },
2344
- "border-raised": {
2345
- "type": "color",
2346
- "value": "{color.gray-400}"
2347
2107
  }
2348
2108
  }
2349
2109
  }
@@ -2360,11 +2120,11 @@ function hexToRgba(hex, alpha) {
2360
2120
  const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
2361
2121
  if (!m) throw new Error(`Expected a hex color, got "${hex}"`);
2362
2122
  const body = m[1];
2363
- const full = body.length === 3 ? body.replace(/./g, (c2) => c2 + c2) : body;
2364
- const r2 = Number.parseInt(full.slice(0, 2), 16);
2365
- const g2 = Number.parseInt(full.slice(2, 4), 16);
2366
- const b = Number.parseInt(full.slice(4, 6), 16);
2367
- return `rgba(${r2}, ${g2}, ${b}, ${alpha})`;
2123
+ const full = body.length === 3 ? body.replace(/./g, (c3) => c3 + c3) : body;
2124
+ const r3 = Number.parseInt(full.slice(0, 2), 16);
2125
+ const g = Number.parseInt(full.slice(2, 4), 16);
2126
+ const b2 = Number.parseInt(full.slice(4, 6), 16);
2127
+ return `rgba(${r3}, ${g}, ${b2}, ${alpha})`;
2368
2128
  }
2369
2129
  var REF = /^\{([^}]+)\}$/;
2370
2130
  function refTarget(value) {
@@ -2376,7 +2136,7 @@ function refTarget(value) {
2376
2136
  return dot === -1 ? path : path.slice(dot + 1);
2377
2137
  }
2378
2138
  function fontStack(value) {
2379
- return value.map((f2) => typeof f2 === "string" && /\s/.test(f2) ? `'${f2}'` : String(f2)).join(", ");
2139
+ return value.map((f3) => typeof f3 === "string" && /\s/.test(f3) ? `'${f3}'` : String(f3)).join(", ");
2380
2140
  }
2381
2141
  function serialize(type, value) {
2382
2142
  if (Array.isArray(value)) {
@@ -2501,25 +2261,7 @@ function ThemeProvider({
2501
2261
  function useThemeOptional() {
2502
2262
  return useContext(ThemeContext);
2503
2263
  }
2504
-
2505
- // src/instrument/serializers.ts
2506
- function configDefaultsOf(instruments) {
2507
- const out = {};
2508
- for (const instrument of instruments) out[instrument.name] = () => instrument.defaultConfig();
2509
- return out;
2510
- }
2511
- function serializersOf(instruments) {
2512
- const out = {};
2513
- for (const instrument of instruments) {
2514
- const { name, serialize: serialize2, deserialize } = instrument;
2515
- if (!serialize2 && !deserialize) continue;
2516
- out[name] = {
2517
- ...serialize2 ? { serialize: (state) => serialize2(state) } : {},
2518
- ...deserialize ? { deserialize: (data, config) => deserialize(data, config) } : {}
2519
- };
2520
- }
2521
- return out;
2522
- }
2264
+ var AnnotationPreloadContext = createContext(null);
2523
2265
 
2524
2266
  // ../theme/dist/index.js
2525
2267
  var ALPHA_EXT = "com.weasel.alpha";
@@ -2529,9 +2271,9 @@ function flattenTokens(doc) {
2529
2271
  for (const [groupName, group] of Object.entries(doc)) {
2530
2272
  if (groupName.startsWith("$")) continue;
2531
2273
  if (typeof group !== "object" || group === null) continue;
2532
- const g2 = group;
2533
- const groupType = g2.$type;
2534
- for (const [leaf, node] of Object.entries(g2)) {
2274
+ const g = group;
2275
+ const groupType = g.$type;
2276
+ for (const [leaf, node] of Object.entries(g)) {
2535
2277
  if (leaf.startsWith("$")) continue;
2536
2278
  if (!isToken(node)) continue;
2537
2279
  const type = node.$type ?? groupType;
@@ -2604,7 +2346,7 @@ var interstellar_tokens_default = {
2604
2346
  "accent-hover": { $value: "#c6a3ee" },
2605
2347
  "focus-ring": { $value: "rgba(176, 138, 219, 0.45)" },
2606
2348
  shadow: { $value: "rgba(0, 0, 0, 0.7)" },
2607
- "border-raised": { $value: "rgba(255, 255, 255, 0.36)" }
2349
+ "border-strong": { $value: "rgba(255, 255, 255, 0.36)" }
2608
2350
  },
2609
2351
  gradient: {
2610
2352
  $type: "gradient",
@@ -2628,7 +2370,7 @@ var interstellar_tokens_default = {
2628
2370
  "accent-hover": { $value: "#c2854f" },
2629
2371
  "focus-ring": { $value: "rgba(168, 111, 60, 0.35)" },
2630
2372
  shadow: { $value: "rgba(80, 60, 40, 0.18)" },
2631
- "border-raised": { $value: "#9e8b71" }
2373
+ "border-strong": { $value: "#9e8b71" }
2632
2374
  }
2633
2375
  }
2634
2376
  }
@@ -2636,114 +2378,18 @@ var interstellar_tokens_default = {
2636
2378
 
2637
2379
  // src/theme/interstellar.ts
2638
2380
  var interstellarTheme = loadDTCG(interstellar_tokens_default);
2639
-
2640
- // src/state/view.ts
2641
- var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
2642
- function as2DView(view) {
2643
- if (typeof view !== "object" || view === null) return null;
2644
- const v = view;
2645
- if (typeof v.zoom !== "number") return null;
2646
- if (typeof v.pan !== "object" || v.pan === null) return null;
2647
- if (typeof v.pan.x !== "number" || typeof v.pan.y !== "number") return null;
2648
- return { zoom: v.zoom, pan: { x: v.pan.x, y: v.pan.y } };
2649
- }
2650
- var ZONE_ID = asNodeId("lk-trial-body");
2651
- var SIDEBAR_ID = asNodeId("lk-trial-sidebar");
2652
- var CONTENT_ID = asNodeId("lk-trial-content");
2653
- var STRATEGIES = { strip: stripStrategy };
2654
- function TrialBody({
2655
- sidebar,
2656
- children,
2657
- contentClassName,
2658
- width,
2659
- defaultWidth = 320,
2660
- minWidth = 140,
2661
- maxWidth = 720,
2662
- contentMinWidth = 160,
2663
- onWidthChange,
2664
- viewport
2665
- }) {
2666
- const storeRef = useRef(null);
2667
- if (storeRef.current === null) {
2668
- const store2 = new Store();
2669
- store2.registerNode(
2670
- createNode({
2671
- kind: "zone",
2672
- id: ZONE_ID,
2673
- container: { strategyId: "strip", config: { axis: "x", resizable: true } }
2674
- })
2675
- );
2676
- store2.registerNode(
2677
- createNode({
2678
- kind: "sidebar",
2679
- id: SIDEBAR_ID,
2680
- parentId: ZONE_ID,
2681
- meta: { title: "Sidebar" },
2682
- placement: { size: { w: width ?? defaultWidth } },
2683
- hints: { minSize: { w: minWidth, h: 0 }, maxSize: { w: maxWidth, h: 0 } }
2684
- })
2685
- );
2686
- store2.registerNode(
2687
- createNode({
2688
- kind: "content",
2689
- id: CONTENT_ID,
2690
- parentId: ZONE_ID,
2691
- meta: { title: "Content" },
2692
- hints: { minSize: { w: contentMinWidth, h: 0 } }
2693
- })
2694
- );
2695
- store2.showNode(SIDEBAR_ID);
2696
- store2.showNode(CONTENT_ID);
2697
- storeRef.current = store2;
2698
- }
2699
- const store = storeRef.current;
2700
- const widthRef = useRef(width ?? defaultWidth);
2701
- useEffect(() => {
2702
- store.setHints(SIDEBAR_ID, {
2703
- minSize: { w: minWidth, h: 0 },
2704
- maxSize: { w: maxWidth, h: 0 }
2705
- });
2706
- store.setHints(CONTENT_ID, { minSize: { w: contentMinWidth, h: 0 } });
2707
- }, [store, minWidth, maxWidth, contentMinWidth]);
2708
- useEffect(() => {
2709
- if (width === void 0 || width === widthRef.current) return;
2710
- widthRef.current = width;
2711
- store.patchPlacement(SIDEBAR_ID, { size: { w: width } });
2712
- }, [store, width]);
2713
- const onWidthChangeRef = useRef(onWidthChange);
2714
- onWidthChangeRef.current = onWidthChange;
2715
- useEffect(
2716
- () => store.events.on("node.placementChanged", () => {
2717
- const next = store.getPlacement(SIDEBAR_ID)?.size?.w;
2718
- if (typeof next !== "number" || next === widthRef.current) return;
2719
- widthRef.current = next;
2720
- onWidthChangeRef.current?.(next);
2721
- }),
2722
- [store]
2723
- );
2724
- const chrome = useMemo(
2725
- () => ({
2726
- sidebar: () => /* @__PURE__ */ jsx("div", { className: "lk-trial__sidebar", children: sidebar }),
2727
- content: () => /* @__PURE__ */ jsx(
2728
- "div",
2729
- {
2730
- className: contentClassName ? `lk-trial__content ${contentClassName}` : "lk-trial__content",
2731
- children
2732
- }
2733
- )
2734
- }),
2735
- [sidebar, children, contentClassName]
2736
- );
2737
- return /* @__PURE__ */ jsx(Provider, { store, children: /* @__PURE__ */ jsx(StrategyRegistryProvider, { strategies: STRATEGIES, children: /* @__PURE__ */ jsx(
2738
- Container,
2381
+ function TrialBody({ sidebar, children, contentClassName, ...rest }) {
2382
+ return /* @__PURE__ */ jsx(
2383
+ Split,
2739
2384
  {
2740
- parentId: ZONE_ID,
2741
- chrome,
2742
- className: "lk-trial__panes windease-zone",
2743
- affordances: true,
2744
- viewport
2385
+ ...rest,
2386
+ sidebar,
2387
+ className: "lk-trial__panes",
2388
+ sidebarClassName: "lk-trial__sidebar",
2389
+ contentClassName: contentClassName ? `lk-trial__content ${contentClassName}` : "lk-trial__content",
2390
+ children
2745
2391
  }
2746
- ) }) });
2392
+ );
2747
2393
  }
2748
2394
  var TrialDragContext = createContext(null);
2749
2395
  function useTrialDrag() {
@@ -2828,7 +2474,7 @@ function Portalled({
2828
2474
  }
2829
2475
  function UndockedSections({ trialId, sections, onDock }) {
2830
2476
  if (sections.length === 0) return null;
2831
- return /* @__PURE__ */ jsx(Fragment, { children: sections.map((c2) => /* @__PURE__ */ jsx(Portalled, { trialId, contribution: c2, onDock }, c2.id)) });
2477
+ return /* @__PURE__ */ jsx(Fragment, { children: sections.map((c3) => /* @__PURE__ */ jsx(Portalled, { trialId, contribution: c3, onDock }, c3.id)) });
2832
2478
  }
2833
2479
  var NO_OP = () => {
2834
2480
  };
@@ -2861,17 +2507,17 @@ function TrialChrome({
2861
2507
  const dockPanelAction = useStore(storeCtx.store, (s) => s.dockPanel);
2862
2508
  const undockedPanels = useStore(storeCtx.store, (s) => s.undockedPanels);
2863
2509
  const undockedIds = useMemo(
2864
- () => Object.values(undockedPanels).filter((p) => p.trialId === trialId).map((p) => p.sectionId),
2510
+ () => Object.values(undockedPanels).filter((p2) => p2.trialId === trialId).map((p2) => p2.sectionId),
2865
2511
  [undockedPanels, trialId]
2866
2512
  );
2867
2513
  const view2d = as2DView(record.view);
2868
2514
  const configSchema = useConfigSchema(instrument);
2869
- const title = record.title ?? record.instrumentName;
2515
+ const title = record.title ?? instrument.title ?? instrument.name;
2870
2516
  const collapsedSections = record.collapsedSections ?? NO_SECTIONS;
2871
2517
  const ctx = useMemo(() => {
2872
- const setZoom = (z) => {
2518
+ const setZoom = (z2) => {
2873
2519
  if (!view2d) return;
2874
- updateTrialView(trialId, { ...view2d, zoom: z });
2520
+ updateTrialView(trialId, withZoom(view2d, z2));
2875
2521
  };
2876
2522
  return {
2877
2523
  trialId,
@@ -2953,7 +2599,7 @@ function TrialChrome({
2953
2599
  ),
2954
2600
  [instrument, ctx, trialChrome, chrome, suppress, lab.controls]
2955
2601
  );
2956
- const inRegion = (region) => contributions.filter((c2) => c2.region === region);
2602
+ const inRegion = (region) => contributions.filter((c3) => c3.region === region);
2957
2603
  const handleKeyDown = (e) => {
2958
2604
  const mod = e.metaKey || e.ctrlKey;
2959
2605
  if (!mod) return;
@@ -2970,6 +2616,7 @@ function TrialChrome({
2970
2616
  "section",
2971
2617
  {
2972
2618
  className: "lk-trial",
2619
+ "data-trial-id": trialId,
2973
2620
  "aria-label": `Trial ${title}`,
2974
2621
  tabIndex: -1,
2975
2622
  onKeyDown: handleKeyDown,
@@ -2989,15 +2636,15 @@ function TrialChrome({
2989
2636
  TrialBody,
2990
2637
  {
2991
2638
  width: record.sidebarWidth,
2992
- onWidthChange: (w3) => updateTrialSidebarWidth(trialId, w3),
2993
- contentClassName: instrument.canvas ? "lk-trial__content--flush" : void 0,
2639
+ onWidthChange: (w2) => updateTrialSidebarWidth(trialId, w2),
2640
+ contentClassName: instrument.canvas || instrument.stage ? "lk-trial__content--flush" : void 0,
2994
2641
  sidebar: /* @__PURE__ */ jsxs(Fragment, { children: [
2995
2642
  /* @__PURE__ */ jsx(SidebarRegion, { contributions: inRegion("sidebar"), ctx }),
2996
2643
  /* @__PURE__ */ jsx(
2997
2644
  UndockedSections,
2998
2645
  {
2999
2646
  trialId,
3000
- sections: inRegion("sidebar").filter((c2) => undockedIds.includes(c2.id)),
2647
+ sections: inRegion("sidebar").filter((c3) => undockedIds.includes(c3.id)),
3001
2648
  onDock: ctx.dockPanel
3002
2649
  }
3003
2650
  )
@@ -3021,25 +2668,30 @@ function Trial({ id, chrome, suppress }) {
3021
2668
  const lab = useLabContext();
3022
2669
  const storeCtx = useContext(LabStoreContext);
3023
2670
  if (!storeCtx) throw new Error("[labkit] <Trial> requires <LabStoreProvider>");
3024
- const record = useStore(storeCtx.store, (s) => s.trials.find((w3) => w3.id === id));
2671
+ const instruments = useStore(storeCtx.store, (s) => s.instruments) ?? lab.instruments;
2672
+ const record = useStore(storeCtx.store, (s) => s.trials.find((w2) => w2.id === id));
2673
+ const instrument = record ? instruments.find((i) => i.name === record.instrumentName) : void 0;
2674
+ const kept = useKeptMarks(instrument?.annotations?.storage, id);
3025
2675
  if (!record) {
3026
2676
  return /* @__PURE__ */ jsxs("div", { className: "lk-trial lk-trial--unknown", children: [
3027
2677
  "Trial not found: ",
3028
2678
  id
3029
2679
  ] });
3030
2680
  }
3031
- const instrument = lab.instruments.find((i) => i.name === record.instrumentName);
3032
2681
  if (!instrument) {
3033
2682
  return /* @__PURE__ */ jsxs("div", { className: "lk-trial lk-trial--unknown", children: [
3034
2683
  "Unknown instrument: ",
3035
2684
  record.instrumentName
3036
2685
  ] });
3037
2686
  }
2687
+ if (!kept.ready) return /* @__PURE__ */ jsx("div", { className: "lk-trial lk-trial--loading" });
3038
2688
  return /* @__PURE__ */ jsx(
3039
2689
  TrialRuntime,
3040
2690
  {
2691
+ keptMarks: kept.value,
3041
2692
  record,
3042
2693
  instrument,
2694
+ instruments,
3043
2695
  store: storeCtx.store,
3044
2696
  isLast: lab.trials.length <= 1,
3045
2697
  chrome,
@@ -3047,7 +2699,41 @@ function Trial({ id, chrome, suppress }) {
3047
2699
  }
3048
2700
  );
3049
2701
  }
3050
- function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
2702
+ function useKeptMarks(storage, trialId) {
2703
+ const preload = useContext(AnnotationPreloadContext);
2704
+ const [loaded, setLoaded] = useState(
2705
+ () => storage && preload?.has(trialId) ? { value: preload.get(trialId) } : null
2706
+ );
2707
+ useEffect(() => {
2708
+ if (!storage || loaded) return;
2709
+ let live = true;
2710
+ storage.load().then(
2711
+ (value) => {
2712
+ if (live) setLoaded({ value });
2713
+ },
2714
+ (error) => {
2715
+ console.warn(`[labkit] could not load the marks of trial "${trialId}"`, error);
2716
+ if (live) setLoaded({ value: null });
2717
+ }
2718
+ );
2719
+ return () => {
2720
+ live = false;
2721
+ };
2722
+ }, [storage, loaded, trialId]);
2723
+ if (!storage) return { ready: true, value: void 0 };
2724
+ return loaded ? { ready: true, value: loaded.value } : { ready: false, value: void 0 };
2725
+ }
2726
+ function TrialRuntime({
2727
+ keptMarks,
2728
+ record,
2729
+ instrument,
2730
+ instruments,
2731
+ store,
2732
+ isLast,
2733
+ chrome,
2734
+ suppress
2735
+ }) {
2736
+ const wheelSlot = useRef(null);
3051
2737
  const canvasContainerRef = useRef(null);
3052
2738
  const loupeHostRef = useRef(null);
3053
2739
  const updateTrialState = useStore(store, (s) => s.updateTrialState);
@@ -3065,7 +2751,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3065
2751
  const [layerOrder, setLayerOrder] = useState(null);
3066
2752
  const [loupeOn, setLoupeOn] = useState(false);
3067
2753
  const visibleLayers = useMemo(
3068
- () => (instrument.canvas?.layers ?? []).filter((l) => layerVisibility[l.id] !== false).map((l) => l.id),
2754
+ () => (instrument.canvas?.layers ?? []).filter((l2) => layerVisibility[l2.id] !== false).map((l2) => l2.id),
3069
2755
  [instrument.canvas, layerVisibility]
3070
2756
  );
3071
2757
  const undoCap = instrument.undo;
@@ -3073,14 +2759,15 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3073
2759
  const maxDepth = undoCap?.maxDepth ?? 50;
3074
2760
  const snapshotIfNeeded = (event) => {
3075
2761
  if (!undoCap || !undoEvents.has(event)) return;
3076
- const current = store.getState().trials.find((w3) => w3.id === record.id);
2762
+ const current = store.getState().trials.find((w2) => w2.id === record.id);
3077
2763
  if (!current) return;
3078
2764
  const snap = structuredClone(current.state);
3079
2765
  updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
3080
2766
  };
3081
2767
  const setView = (v) => updateTrialView(record.id, v);
2768
+ const stage = instrument.canvas ? void 0 : instrument.stage;
3082
2769
  const placeView = (size) => {
3083
- const declared = instrument.canvas?.initialView;
2770
+ const declared = instrument.canvas?.initialView ?? (stage ? stage.initialView ?? ((vp) => fitStage(stage.size, vp)) : void 0);
3084
2771
  if (typeof declared !== "function") return;
3085
2772
  if (record.view != null) return;
3086
2773
  setView(declared(size));
@@ -3091,12 +2778,14 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3091
2778
  const annotationsRef = useRef(null);
3092
2779
  const configRef = useRef(record.config);
3093
2780
  configRef.current = record.config;
2781
+ const capRef = useRef(annotationsCap);
2782
+ capRef.current = annotationsCap;
3094
2783
  if (annotationsRef.current === null) {
3095
- const kept = annotationsCap?.storage ? annotationsCap.storage.load() : record.annotations;
2784
+ const kept = annotationsCap?.storage ? keptMarks : record.annotations;
3096
2785
  annotationsRef.current = annotationsFromJSON(kept, () => targetsRef.current(), {
3097
- meaning: annotationsCap?.meaning,
2786
+ meaning: () => capRef.current?.meaning,
3098
2787
  config: () => configRef.current,
3099
- onCapture: (result) => annotationsCap?.onCapture?.(result)
2788
+ onCapture: (result) => capRef.current?.onCapture?.(result)
3100
2789
  });
3101
2790
  }
3102
2791
  const annotations = annotationsRef.current;
@@ -3140,8 +2829,9 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3140
2829
  flush();
3141
2830
  };
3142
2831
  }, [annotations, annotationsCap]);
3143
- const declaresTools = instrument.tools != null || instrument.annotations != null;
3144
- const resolvedToolId = declaresTools ? record.activeToolId ?? instrument.tools?.initial ?? instrument.tools?.tools[0]?.id ?? (instrument.annotations ? "select" : null) : labToolId;
2832
+ const declaresTools = instrument.tools != null;
2833
+ const labTool = resolveLabTool(labToolId, instruments);
2834
+ const resolvedToolId = declaresTools ? record.activeToolId ?? instrument.tools?.initial ?? instrument.tools?.tools[0]?.id ?? null : labTool;
3145
2835
  const setActiveTool = (id) => {
3146
2836
  if (declaresTools) setTrialTool(record.id, id);
3147
2837
  else setLabTool(id);
@@ -3176,9 +2866,9 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3176
2866
  view: record.view,
3177
2867
  setView,
3178
2868
  zoom: view2d?.zoom ?? 1,
3179
- setZoom: (z) => {
2869
+ setZoom: (z2) => {
3180
2870
  if (!view2d) return;
3181
- updateTrialView(record.id, { ...view2d, zoom: z });
2871
+ updateTrialView(record.id, withZoom(view2d, z2));
3182
2872
  },
3183
2873
  activeToolId: resolvedToolId,
3184
2874
  visibleLayers
@@ -3222,7 +2912,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3222
2912
  const canvasLayers = useMemo(() => {
3223
2913
  if (!instrument.canvas) return [];
3224
2914
  const baseLayers = instrument.canvas.layers;
3225
- const ordered = layerOrder ? [...baseLayers].sort((a, b) => layerOrder.indexOf(a.id) - layerOrder.indexOf(b.id)) : baseLayers;
2915
+ const ordered = layerOrder ? [...baseLayers].sort((a2, b2) => layerOrder.indexOf(a2.id) - layerOrder.indexOf(b2.id)) : baseLayers;
3226
2916
  return ordered.map((layer) => ({
3227
2917
  id: layer.id,
3228
2918
  visible: layerVisibility[layer.id] !== false,
@@ -3234,7 +2924,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3234
2924
  }, [instrument.canvas, record.state, record.config, layerVisibility, layerOrder]);
3235
2925
  const layerDescriptors = useMemo(() => {
3236
2926
  if (!instrument.layers) return [];
3237
- return instrument.layers.ids.map((l) => typeof l === "string" ? { id: l, label: l } : l);
2927
+ return instrument.layers.ids.map((l2) => typeof l2 === "string" ? { id: l2, label: l2 } : l2);
3238
2928
  }, [instrument.layers]);
3239
2929
  const dragDropResult = useDragDrop({
3240
2930
  capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
@@ -3254,8 +2944,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3254
2944
  });
3255
2945
  const paletteItems = useMemo(() => {
3256
2946
  if (!instrument.dragDrop) return [];
3257
- const p = instrument.dragDrop.palette;
3258
- return typeof p === "function" ? p(record.state, record.config) : p;
2947
+ const p2 = instrument.dragDrop.palette;
2948
+ return typeof p2 === "function" ? p2(record.state, record.config) : p2;
3259
2949
  }, [instrument.dragDrop, record.state, record.config]);
3260
2950
  const layersWithFeedback = useMemo(() => {
3261
2951
  if (!dragDropResult.drag?.feedback) return canvasLayers;
@@ -3269,12 +2959,12 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3269
2959
  render: (ctx) => {
3270
2960
  const el = canvasContainerRef.current;
3271
2961
  if (!el) return;
3272
- const r2 = el.getBoundingClientRect();
2962
+ const r3 = el.getBoundingClientRect();
3273
2963
  ctx.save();
3274
2964
  ctx.strokeStyle = fb.ok ? "#3a7" : "#c44";
3275
2965
  ctx.lineWidth = 2;
3276
2966
  ctx.beginPath();
3277
- ctx.arc(screen.x - r2.left, screen.y - r2.top, 16, 0, Math.PI * 2);
2967
+ ctx.arc(screen.x - r3.left, screen.y - r3.top, 16, 0, Math.PI * 2);
3278
2968
  ctx.stroke();
3279
2969
  ctx.restore();
3280
2970
  }
@@ -3314,6 +3004,21 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3314
3004
  ),
3315
3005
  /* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
3316
3006
  ] });
3007
+ } else if (stage) {
3008
+ body = /* @__PURE__ */ jsx(
3009
+ Stage,
3010
+ {
3011
+ size: stage.size,
3012
+ view: view2d ?? DEFAULT_VIEW,
3013
+ onViewChange: setView,
3014
+ onResize: placeView,
3015
+ minZoom: stage.minZoom,
3016
+ maxZoom: stage.maxZoom,
3017
+ hostRef: loupeHostRef,
3018
+ overlay: lens,
3019
+ children: instrument.render(renderCtx)
3020
+ }
3021
+ );
3317
3022
  } else if (lens) {
3318
3023
  body = /* @__PURE__ */ jsxs("div", { ref: loupeHostRef, className: "lk-trial__loupe-host", children: [
3319
3024
  instrument.render(renderCtx),
@@ -3334,7 +3039,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3334
3039
  layers: layerDescriptors,
3335
3040
  visibility: layerVisibility,
3336
3041
  onReorder: (next) => {
3337
- setLayerOrder(next.map((l) => l.id));
3042
+ setLayerOrder(next.map((l2) => l2.id));
3338
3043
  bus.emit("layers.reorder");
3339
3044
  },
3340
3045
  onToggle: (lid, visible) => {
@@ -3371,10 +3076,10 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3371
3076
  config: record.config,
3372
3077
  trial: { id: record.id, view: record.view },
3373
3078
  annotations,
3374
- activeToolId: resolvedToolId
3079
+ activeToolId: labTool
3375
3080
  }
3376
3081
  ) : null;
3377
- return /* @__PURE__ */ jsx(TrialIdProvider, { trialId: record.id, children: /* @__PURE__ */ jsx(AnnotationsContext.Provider, { value: annotationsCap ? annotations : null, children: /* @__PURE__ */ jsxs(
3082
+ return /* @__PURE__ */ jsx(TrialIdProvider, { trialId: record.id, children: /* @__PURE__ */ jsx(AnnotationsContext.Provider, { value: annotationsCap ? annotations : null, children: /* @__PURE__ */ jsx(CameraWheelContext.Provider, { value: wheelSlot, children: /* @__PURE__ */ jsxs(
3378
3083
  TrialChrome,
3379
3084
  {
3380
3085
  job: jobCap ? job : void 0,
@@ -3394,7 +3099,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3394
3099
  annotationOverlays
3395
3100
  ]
3396
3101
  }
3397
- ) }) });
3102
+ ) }) }) });
3398
3103
  }
3399
3104
 
3400
3105
  // src/trial/trialOps.ts
@@ -3407,8 +3112,9 @@ function findInstrument(instruments, name) {
3407
3112
  return found;
3408
3113
  }
3409
3114
  function initialView(instrument) {
3410
- const declared = instrument.canvas?.initialView;
3411
- if (typeof declared === "function") return null;
3115
+ const stage = instrument.canvas ? void 0 : instrument.stage;
3116
+ const declared = instrument.canvas?.initialView ?? stage?.initialView;
3117
+ if (typeof declared === "function" || stage && declared === void 0) return null;
3412
3118
  return declared ? structuredClone(declared) : { ...DEFAULT_VIEW2, pan: { ...DEFAULT_VIEW2.pan } };
3413
3119
  }
3414
3120
  function seedConfig(defaults, seed) {
@@ -3419,7 +3125,7 @@ function addTrial(trials, instruments, instrumentName, options = {}) {
3419
3125
  const config = seedConfig(instrument.defaultConfig(), options.config);
3420
3126
  const state = instrument.initialState(config);
3421
3127
  const record = {
3422
- id: crypto.randomUUID(),
3128
+ id: newId(),
3423
3129
  instrumentName,
3424
3130
  config,
3425
3131
  state,
@@ -3430,12 +3136,12 @@ function addTrial(trials, instruments, instrumentName, options = {}) {
3430
3136
  return [...trials, record];
3431
3137
  }
3432
3138
  function cloneTrial(trials, id) {
3433
- const sourceIdx = trials.findIndex((w3) => w3.id === id);
3139
+ const sourceIdx = trials.findIndex((w2) => w2.id === id);
3434
3140
  const source = trials[sourceIdx];
3435
3141
  if (!source) return trials;
3436
3142
  const clone = {
3437
3143
  ...source,
3438
- id: crypto.randomUUID(),
3144
+ id: newId(),
3439
3145
  config: structuredClone(source.config),
3440
3146
  state: structuredClone(source.state),
3441
3147
  view: structuredClone(source.view),
@@ -3445,11 +3151,11 @@ function cloneTrial(trials, id) {
3445
3151
  }
3446
3152
  function closeTrial(trials, id) {
3447
3153
  if (trials.length <= 1) return trials;
3448
- const next = trials.filter((w3) => w3.id !== id);
3154
+ const next = trials.filter((w2) => w2.id !== id);
3449
3155
  return next.length === trials.length ? trials : next;
3450
3156
  }
3451
3157
  function resetTrial(trials, id, instruments) {
3452
- const idx = trials.findIndex((w3) => w3.id === id);
3158
+ const idx = trials.findIndex((w2) => w2.id === id);
3453
3159
  const current = trials[idx];
3454
3160
  if (!current) return trials;
3455
3161
  const instrument = findInstrument(instruments, current.instrumentName);
@@ -3464,12 +3170,92 @@ function resetTrial(trials, id, instruments) {
3464
3170
  return [...trials.slice(0, idx), reset, ...trials.slice(idx + 1)];
3465
3171
  }
3466
3172
  function reorderTrials(trials, ids) {
3467
- const byId = new Map(trials.map((w3) => [w3.id, w3]));
3468
- const named = ids.map((id) => byId.get(id)).filter((w3) => w3 !== void 0);
3469
- const seen = new Set(named.map((w3) => w3.id));
3470
- const rest = trials.filter((w3) => !seen.has(w3.id));
3173
+ const byId = new Map(trials.map((w2) => [w2.id, w2]));
3174
+ const named = ids.map((id) => byId.get(id)).filter((w2) => w2 !== void 0);
3175
+ const seen = new Set(named.map((w2) => w2.id));
3176
+ const rest = trials.filter((w2) => !seen.has(w2.id));
3471
3177
  return [...named, ...rest];
3472
3178
  }
3179
+ function findOvershoot(bound, items, tolerance = 1) {
3180
+ let worst = null;
3181
+ for (const { item, edges } of items) {
3182
+ const y = edges.bottom - bound.bottom;
3183
+ const x = edges.right - bound.right;
3184
+ const by = Math.max(x, y);
3185
+ if (by <= tolerance) continue;
3186
+ if (!worst || by > worst.by) worst = { item, by, axis: y >= x ? "y" : "x" };
3187
+ }
3188
+ return worst;
3189
+ }
3190
+ function tagAndClasses(el) {
3191
+ return el.tagName.toLowerCase() + [...el.classList].map((c3) => `.${c3}`).join("");
3192
+ }
3193
+ function describeElement(el) {
3194
+ if (el.classList.length > 0) return tagAndClasses(el);
3195
+ let up = el.parentElement;
3196
+ while (up && up.classList.length === 0) up = up.parentElement;
3197
+ return up ? `${tagAndClasses(el)} in ${tagAndClasses(up)}` : tagAndClasses(el);
3198
+ }
3199
+ function edgesOf(el) {
3200
+ const r3 = el.getBoundingClientRect();
3201
+ return { top: r3.top, left: r3.left, bottom: r3.bottom, right: r3.right };
3202
+ }
3203
+ function* candidates(root) {
3204
+ yield { item: root, edges: edgesOf(root) };
3205
+ for (const el of root.querySelectorAll("*")) yield { item: el, edges: edgesOf(el) };
3206
+ }
3207
+ function overflows(el) {
3208
+ return el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1;
3209
+ }
3210
+ function explainLabScroll(lab, shellBody) {
3211
+ if (overflows(shellBody)) {
3212
+ const r4 = shellBody.getBoundingClientRect();
3213
+ const bound2 = {
3214
+ top: r4.top,
3215
+ left: r4.left,
3216
+ bottom: r4.top + shellBody.clientHeight,
3217
+ right: r4.left + shellBody.clientWidth
3218
+ };
3219
+ const hit2 = findOvershoot(bound2, candidates(shellBody));
3220
+ return hit2 ? `.lk-shell-body scrolls: ${describeElement(hit2.item)} reaches ${Math.round(hit2.by)}px past it (${hit2.axis})` : ".lk-shell-body scrolls";
3221
+ }
3222
+ const doc = document.scrollingElement ?? document.documentElement;
3223
+ if (!overflows(doc)) return null;
3224
+ const r3 = lab.getBoundingClientRect();
3225
+ const bound = {
3226
+ top: r3.top,
3227
+ left: r3.left,
3228
+ bottom: r3.top + Math.min(r3.height, doc.clientHeight),
3229
+ right: r3.left + Math.min(r3.width, doc.clientWidth)
3230
+ };
3231
+ const hit = findOvershoot(bound, candidates(lab));
3232
+ return hit ? `the page scrolls: ${describeElement(hit.item)} reaches ${Math.round(hit.by)}px past the lab's box (${hit.axis})` : null;
3233
+ }
3234
+ var SETTLE_MS = 300;
3235
+ function useLabFitWarning(labBody) {
3236
+ useEffect(() => {
3237
+ if (process.env.NODE_ENV === "production") return;
3238
+ if (!labBody || typeof ResizeObserver === "undefined") return;
3239
+ const lab = labBody.closest(".lk-lab");
3240
+ const shellBody = labBody.closest(".lk-shell-body");
3241
+ if (!lab || !shellBody) return;
3242
+ let timer;
3243
+ const observer = new ResizeObserver(() => {
3244
+ clearTimeout(timer);
3245
+ timer = setTimeout(() => {
3246
+ const why = explainLabScroll(lab, shellBody);
3247
+ if (!why) return;
3248
+ observer.disconnect();
3249
+ console.warn(`[labkit] <Lab> should fit its container without scrolling, but ${why}.`);
3250
+ }, SETTLE_MS);
3251
+ });
3252
+ for (const el of [document.documentElement, lab, shellBody, labBody]) observer.observe(el);
3253
+ return () => {
3254
+ clearTimeout(timer);
3255
+ observer.disconnect();
3256
+ };
3257
+ }, [labBody]);
3258
+ }
3473
3259
  var MODES = [
3474
3260
  { value: "auto", label: "Auto", glyph: /* @__PURE__ */ jsx(E, { size: 14 }) },
3475
3261
  { value: "light", label: "Light", glyph: /* @__PURE__ */ jsx(w, { size: 14 }) },
@@ -3480,11 +3266,11 @@ var MODE_ITEMS = MODES.map(({ value, label, glyph }) => ({
3480
3266
  label: glyph,
3481
3267
  ariaLabel: label
3482
3268
  }));
3483
- function LabHeader() {
3269
+ function LabHeader({ addTrial: addTrial2 = true }) {
3484
3270
  const lab = useLabContext();
3485
3271
  const only = lab.instruments.length === 1 ? lab.instruments[0] : null;
3486
3272
  return /* @__PURE__ */ jsxs(Fragment, { children: [
3487
- only ? /* @__PURE__ */ jsxs(
3273
+ !addTrial2 ? null : only ? /* @__PURE__ */ jsxs(
3488
3274
  "button",
3489
3275
  {
3490
3276
  type: "button",
@@ -3495,22 +3281,14 @@ function LabHeader() {
3495
3281
  /* @__PURE__ */ jsx("span", { children: "Add trial" })
3496
3282
  ]
3497
3283
  }
3498
- ) : (
3499
- // Held at null so the control stays an "add one" action rather than
3500
- // reading as the current instrument.
3501
- /* @__PURE__ */ jsx(
3502
- g,
3503
- {
3504
- className: "lk-lab-header__add-select",
3505
- "aria-label": "Add trial",
3506
- placeholder: "Add trial\u2026",
3507
- selectedKey: null,
3508
- options: lab.instruments.map((i) => ({ value: i.name, label: i.name })),
3509
- onSelectionChange: (name) => {
3510
- if (name != null) lab.addTrial(String(name));
3511
- }
3512
- }
3513
- )
3284
+ ) : /* @__PURE__ */ jsx(
3285
+ l5,
3286
+ {
3287
+ "aria-label": "Add trial",
3288
+ label: "Add trial\u2026",
3289
+ items: lab.instruments.map((i) => ({ value: i.name, label: i.title ?? i.name })),
3290
+ onAction: (name) => lab.addTrial(name)
3291
+ }
3514
3292
  ),
3515
3293
  /* @__PURE__ */ jsx(
3516
3294
  r6,
@@ -3528,22 +3306,71 @@ function LabHeader() {
3528
3306
  )
3529
3307
  ] });
3530
3308
  }
3531
- function LabPalette({ tools }) {
3532
- const storeCtx = useContext(LabStoreContext);
3533
- if (!storeCtx) throw new Error("[labkit] LabPalette requires <LabStoreProvider>");
3534
- const activeToolId = useStore(storeCtx.store, (s) => s.activeToolId);
3535
- const setLabTool = useStore(storeCtx.store, (s) => s.setLabTool);
3536
- const contributions = tools.map((t) => ({
3537
- id: t.id,
3538
- region: "palette",
3539
- group: t.group,
3540
- item: { icon: t.icon, label: t.label, shortcut: t.shortcut }
3541
- }));
3542
- const ctx = {
3543
- activeToolId,
3544
- setActiveTool: setLabTool
3545
- };
3546
- return /* @__PURE__ */ jsx(PaletteRegion, { contributions, ctx });
3309
+ function LabPalette({ tools, contributions }) {
3310
+ const ctx = useLabChromeContext();
3311
+ const all = labContributions(tools, contributions);
3312
+ return /* @__PURE__ */ jsx(PaletteRegion, { contributions: all.filter((c3) => c3.region === "palette"), ctx });
3313
+ }
3314
+ function currentPage(path, pages) {
3315
+ const here = path.split("?")[0].split("#")[0].replace(/\/$/, "").replace(/\.html$/, "");
3316
+ return pages.findIndex((p2) => here.endsWith(p2.href.replace(/\.html$/, "")));
3317
+ }
3318
+ function LabSwitcher({ title, pages, path, className }) {
3319
+ const [open, setOpen] = useState(false);
3320
+ const root = useRef(null);
3321
+ const menuId = useId();
3322
+ const here = currentPage(path ?? (typeof window === "undefined" ? "" : window.location.pathname), pages);
3323
+ const close = useCallback(() => setOpen(false), []);
3324
+ useEffect(() => {
3325
+ if (!open) return;
3326
+ const onKey = (e) => {
3327
+ if (e.key === "Escape") {
3328
+ close();
3329
+ root.current?.querySelector("button")?.focus();
3330
+ }
3331
+ };
3332
+ const onDown = (e) => {
3333
+ if (!root.current?.contains(e.target)) close();
3334
+ };
3335
+ document.addEventListener("keydown", onKey);
3336
+ document.addEventListener("pointerdown", onDown);
3337
+ return () => {
3338
+ document.removeEventListener("keydown", onKey);
3339
+ document.removeEventListener("pointerdown", onDown);
3340
+ };
3341
+ }, [open, close]);
3342
+ if (pages.length < 2) {
3343
+ return /* @__PURE__ */ jsx("h1", { className: `lk-switcher-title${className ? ` ${className}` : ""}`, children: title });
3344
+ }
3345
+ return /* @__PURE__ */ jsxs("div", { className: `lk-switcher${className ? ` ${className}` : ""}`, ref: root, children: [
3346
+ /* @__PURE__ */ jsx("h1", { className: "lk-switcher-title", children: /* @__PURE__ */ jsxs(
3347
+ "button",
3348
+ {
3349
+ type: "button",
3350
+ className: "lk-switcher-trigger",
3351
+ "aria-expanded": open,
3352
+ "aria-haspopup": "menu",
3353
+ "aria-controls": open ? menuId : void 0,
3354
+ onClick: () => setOpen((was) => !was),
3355
+ children: [
3356
+ title,
3357
+ /* @__PURE__ */ jsx("span", { className: "lk-switcher-caret", "aria-hidden": "true" })
3358
+ ]
3359
+ }
3360
+ ) }),
3361
+ open && /* @__PURE__ */ jsx("div", { className: "lk-switcher-menu", id: menuId, role: "menu", children: pages.map((page, i) => /* @__PURE__ */ jsx(
3362
+ "a",
3363
+ {
3364
+ href: page.href,
3365
+ role: "menuitem",
3366
+ className: "lk-switcher-item",
3367
+ "aria-current": i === here ? "page" : void 0,
3368
+ onClick: close,
3369
+ children: page.label
3370
+ },
3371
+ page.href
3372
+ )) })
3373
+ ] });
3547
3374
  }
3548
3375
  function query() {
3549
3376
  if (typeof window === "undefined" || !window.matchMedia) return null;
@@ -3563,7 +3390,15 @@ function useResolvedMode(mode) {
3563
3390
  }, [mode]);
3564
3391
  return mode === "auto" ? system : mode;
3565
3392
  }
3566
- function LabShell({ title, children, header, footer, mode = "auto" }) {
3393
+ function LabShell({
3394
+ title,
3395
+ children,
3396
+ header,
3397
+ footer,
3398
+ mode = "auto",
3399
+ pages,
3400
+ path
3401
+ }) {
3567
3402
  const resolved = useResolvedMode(mode);
3568
3403
  const outer = useThemeOptional();
3569
3404
  const shell = (
@@ -3571,7 +3406,15 @@ function LabShell({ title, children, header, footer, mode = "auto" }) {
3571
3406
  // carries the element defaults a lab's bare markup is styled by.
3572
3407
  /* @__PURE__ */ jsxs("div", { className: "lk-root lk-shell", "data-wzl-portal-host": "", children: [
3573
3408
  /* @__PURE__ */ jsxs("header", { className: "lk-shell-header", children: [
3574
- /* @__PURE__ */ jsx("h1", { className: "lk-shell-title", children: title }),
3409
+ pages && pages.length > 0 ? /* @__PURE__ */ jsx(
3410
+ LabSwitcher,
3411
+ {
3412
+ title,
3413
+ pages,
3414
+ path,
3415
+ className: "lk-shell-title"
3416
+ }
3417
+ ) : /* @__PURE__ */ jsx("h1", { className: "lk-shell-title", children: title }),
3575
3418
  header && /* @__PURE__ */ jsx("div", { className: "lk-shell-header-actions", children: header })
3576
3419
  ] }),
3577
3420
  /* @__PURE__ */ jsx("main", { className: "lk-shell-body", children }),
@@ -3580,17 +3423,17 @@ function LabShell({ title, children, header, footer, mode = "auto" }) {
3580
3423
  );
3581
3424
  return outer ? shell : /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolved, children: shell });
3582
3425
  }
3583
- var ZONE_ID2 = asNodeId("lk-workspace");
3584
- var STRATEGIES2 = { grid: gridStrategy, floating: floatingStrategy };
3426
+ var ZONE_ID = asNodeId("lk-workspace");
3427
+ var STRATEGIES = { grid: gridStrategy, floating: floatingStrategy };
3585
3428
  var FLOAT_ZONE_ID = asNodeId("lk-workspace-floating");
3586
3429
  var PANEL_KIND = "panel";
3587
3430
  var KIND = "trial";
3588
3431
  function extentOf(store, id) {
3589
- const p = store.getNode(id)?.membership?.placement;
3590
- if (!p) return null;
3432
+ const p2 = store.getNode(id)?.membership?.placement;
3433
+ if (!p2) return null;
3591
3434
  const out = {};
3592
- if (p.size) out.size = p.size;
3593
- if (p.span) out.span = p.span;
3435
+ if (p2.size) out.size = p2.size;
3436
+ if (p2.span) out.span = p2.span;
3594
3437
  return Object.keys(out).length > 0 ? out : null;
3595
3438
  }
3596
3439
  function PanelFrame({ panel, floating = false }) {
@@ -3611,11 +3454,11 @@ function Workspace({
3611
3454
  viewport
3612
3455
  }) {
3613
3456
  const items = Children.toArray(children);
3614
- const tilePanels = useMemo(() => (panels ?? []).filter((p) => p.as === "tile"), [panels]);
3615
- const floatPanels = useMemo(() => (panels ?? []).filter((p) => p.as === "floating"), [panels]);
3457
+ const tilePanels = useMemo(() => (panels ?? []).filter((p2) => p2.as === "tile"), [panels]);
3458
+ const floatPanels = useMemo(() => (panels ?? []).filter((p2) => p2.as === "floating"), [panels]);
3616
3459
  const wantedIds = [
3617
3460
  ...items.map((_, i) => ids?.[i] ?? `lk-ws-${i}`),
3618
- ...tilePanels.map((p) => `lk-panel-${p.key}`)
3461
+ ...tilePanels.map((p2) => `lk-panel-${p2.key}`)
3619
3462
  ];
3620
3463
  const idKey = wantedIds.join(",");
3621
3464
  const idKeyRef = useRef(null);
@@ -3635,7 +3478,7 @@ function Workspace({
3635
3478
  store2.registerNode(
3636
3479
  createNode({
3637
3480
  kind: "zone",
3638
- id: ZONE_ID2,
3481
+ id: ZONE_ID,
3639
3482
  container: { strategyId: "grid", config: { resizable, gap, padding } }
3640
3483
  })
3641
3484
  );
@@ -3651,10 +3494,10 @@ function Workspace({
3651
3494
  const store = storeRef.current;
3652
3495
  const surface = useSurfaceOptional();
3653
3496
  useLayoutEffect(() => {
3654
- store.updateContainerConfig(ZONE_ID2, { resizable, gap, padding });
3497
+ store.updateContainerConfig(ZONE_ID, { resizable, gap, padding });
3655
3498
  }, [store, resizable, gap, padding]);
3656
3499
  useLayoutEffect(() => {
3657
- const present = new Set(store.getContainerView(ZONE_ID2)?.childOrder ?? []);
3500
+ const present = new Set(store.getContainerView(ZONE_ID)?.childOrder ?? []);
3658
3501
  const wanted = new Set(nodeIds);
3659
3502
  for (const id of present) {
3660
3503
  if (!wanted.has(id)) store.unregisterNode(id);
@@ -3662,12 +3505,12 @@ function Workspace({
3662
3505
  for (const id of nodeIds) {
3663
3506
  if (present.has(id)) continue;
3664
3507
  const kind = String(id).startsWith("lk-panel-") ? PANEL_KIND : KIND;
3665
- store.registerNode(createNode({ kind, id, parentId: ZONE_ID2, focus: true }));
3508
+ store.registerNode(createNode({ kind, id, parentId: ZONE_ID, focus: true }));
3666
3509
  store.showNode(id);
3667
3510
  const saved = layoutRef.current?.[id];
3668
3511
  if (saved) store.patchPlacement(id, saved);
3669
3512
  }
3670
- store.setChildOrder(ZONE_ID2, [...nodeIds]);
3513
+ store.setChildOrder(ZONE_ID, [...nodeIds]);
3671
3514
  }, [store, nodeIds]);
3672
3515
  useEffect(() => {
3673
3516
  if (!surface) return;
@@ -3678,7 +3521,7 @@ function Workspace({
3678
3521
  if (!onLayoutChange) return;
3679
3522
  return store.events.on("node.placementChanged", () => {
3680
3523
  const next = {};
3681
- for (const child of store.getChildren(ZONE_ID2)) {
3524
+ for (const child of store.getChildren(ZONE_ID)) {
3682
3525
  const extent = extentOf(store, child.id);
3683
3526
  if (extent) next[child.id] = extent;
3684
3527
  }
@@ -3692,7 +3535,7 @@ function Workspace({
3692
3535
  const chrome = useMemo(() => {
3693
3536
  const byId = new Map(nodeIds.map((id, i) => [id, items[i]]));
3694
3537
  const panelByNode = new Map(
3695
- tilePanels.map((p) => [`lk-panel-${p.key}`, p])
3538
+ tilePanels.map((p2) => [`lk-panel-${p2.key}`, p2])
3696
3539
  );
3697
3540
  return {
3698
3541
  [PANEL_KIND]: ({ node }) => {
@@ -3710,11 +3553,11 @@ function Workspace({
3710
3553
  }
3711
3554
  };
3712
3555
  }, [nodeIds, items, reorderable, tilePanels]);
3713
- const floatingLayer = floatPanels.length === 0 ? null : /* @__PURE__ */ jsx("div", { className: "lk-workspace__floating", children: floatPanels.map((p) => /* @__PURE__ */ jsx(PanelFrame, { panel: p, floating: true }, p.key)) });
3556
+ const floatingLayer = floatPanels.length === 0 ? null : /* @__PURE__ */ jsx("div", { className: "lk-workspace__floating", children: floatPanels.map((p2) => /* @__PURE__ */ jsx(PanelFrame, { panel: p2, floating: true }, p2.key)) });
3714
3557
  const grid = /* @__PURE__ */ jsx(
3715
3558
  Container,
3716
3559
  {
3717
- parentId: ZONE_ID2,
3560
+ parentId: ZONE_ID,
3718
3561
  chrome,
3719
3562
  className: "lk-workspace windease-zone",
3720
3563
  affordances: resizable,
@@ -3722,7 +3565,7 @@ function Workspace({
3722
3565
  onChildOrderChange: reorderable ? commitOrder : void 0
3723
3566
  }
3724
3567
  );
3725
- return /* @__PURE__ */ jsx(Provider, { store, children: /* @__PURE__ */ jsx(StrategyRegistryProvider, { strategies: STRATEGIES2, children: floatingLayer ? (
3568
+ return /* @__PURE__ */ jsx(Provider, { store, children: /* @__PURE__ */ jsx(StrategyRegistryProvider, { strategies: STRATEGIES, children: floatingLayer ? (
3726
3569
  // The floating layer insets against the workspace, so it needs a
3727
3570
  // positioned box around both. Without one it insets against the page
3728
3571
  // and lands over the lab header.
@@ -3732,23 +3575,59 @@ function Workspace({
3732
3575
  ] })
3733
3576
  ) : reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid }) });
3734
3577
  }
3735
- function buildStore(instruments, defaultInstrument, storage, storageKey, initialMode) {
3736
- const store = createLabStore({
3578
+ function seedDefaultTrial(store, instruments, defaultInstrument) {
3579
+ if (store.getState().trials.length > 0) return;
3580
+ const record = addTrial([], instruments, defaultInstrument)[0];
3581
+ if (!record) return;
3582
+ const { undoStack: _undoStack, ...rest } = record;
3583
+ store.getState().addTrial(rest);
3584
+ }
3585
+ function openUnstoredLab({ instruments, defaultInstrument, mode }) {
3586
+ const store = createLabStore({ initialMode: mode ?? "auto", instruments });
3587
+ seedDefaultTrial(store, instruments, defaultInstrument);
3588
+ const records = createRecordCache({ storage: noneAdapter, prefix: "", writable: false });
3589
+ return { store, records, close: () => records.close(), marks: /* @__PURE__ */ new Map() };
3590
+ }
3591
+ async function openStoredLab({ instruments, defaultInstrument, mode }, storageKey, storage) {
3592
+ const opened = await openLabStore({
3737
3593
  storageKey,
3738
- storage,
3739
- initialMode,
3740
- serializers: serializersOf(instruments),
3741
- configDefaults: configDefaultsOf(instruments)
3594
+ storage: storage ?? await defaultStorage(),
3595
+ initialMode: mode ?? "auto",
3596
+ instruments
3742
3597
  });
3743
- if (store.getState().trials.length === 0) {
3744
- const seeded = addTrial([], instruments, defaultInstrument);
3745
- const record = seeded[0];
3746
- if (record) {
3747
- const { undoStack: _undoStack, ...rest } = record;
3748
- store.getState().addTrial(rest);
3598
+ seedDefaultTrial(opened.store, instruments, defaultInstrument);
3599
+ const marks = /* @__PURE__ */ new Map();
3600
+ await Promise.all(
3601
+ opened.store.getState().trials.map(async (trial) => {
3602
+ const kept = instruments.find((i) => i.name === trial.instrumentName)?.annotations?.storage;
3603
+ if (!kept) return;
3604
+ try {
3605
+ marks.set(trial.id, await kept.load());
3606
+ } catch (error) {
3607
+ console.warn(`[labkit] could not load the marks of trial "${trial.id}"`, error);
3608
+ marks.set(trial.id, null);
3609
+ }
3610
+ })
3611
+ );
3612
+ return { ...opened, marks };
3613
+ }
3614
+ function LabFallback({
3615
+ title,
3616
+ mode,
3617
+ pages,
3618
+ path
3619
+ }) {
3620
+ const resolvedMode = useResolvedMode(mode ?? "auto");
3621
+ return /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolvedMode, className: "lk-lab", children: /* @__PURE__ */ jsx(
3622
+ LabShell,
3623
+ {
3624
+ title: title ?? "Labkit",
3625
+ mode,
3626
+ ...pages ? { pages } : {},
3627
+ ...path !== void 0 ? { path } : {},
3628
+ children: null
3749
3629
  }
3750
- }
3751
- return store;
3630
+ ) });
3752
3631
  }
3753
3632
  var NEBULA_SLOTS = [
3754
3633
  { cx: "18%", cy: "30%", sx: "60%", sy: "80%", stop: "70%" },
@@ -3758,43 +3637,77 @@ var NEBULA_SLOTS = [
3758
3637
  { cx: "85%", cy: "18%", sx: "55%", sy: "80%", stop: "70%" }
3759
3638
  ];
3760
3639
  function buildNebula(colors) {
3761
- const blobs = colors.map((c2, i) => {
3762
- const p = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
3763
- return `radial-gradient(ellipse ${p.sx} ${p.sy} at ${p.cx} ${p.cy}, color-mix(in srgb, ${c2} 22%, transparent), transparent ${p.stop})`;
3640
+ const blobs = colors.map((c3, i) => {
3641
+ const p2 = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
3642
+ return `radial-gradient(ellipse ${p2.sx} ${p2.sy} at ${p2.cx} ${p2.cy}, color-mix(in srgb, ${c3} 22%, transparent), transparent ${p2.stop})`;
3764
3643
  });
3765
3644
  blobs.push("radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%)");
3766
3645
  return blobs.join(", ");
3767
3646
  }
3768
- function Lab({
3647
+ function LabPanes({
3648
+ contributions,
3649
+ children
3650
+ }) {
3651
+ const surface = useSurfaceOptional();
3652
+ const [sidebarWidth, setSidebarWidth] = usePersistedState(
3653
+ "lk-lab-sidebar-width",
3654
+ void 0,
3655
+ { scope: "lab" }
3656
+ );
3657
+ return /* @__PURE__ */ jsx(
3658
+ Split,
3659
+ {
3660
+ className: "lk-lab__panes",
3661
+ sidebarClassName: "lk-lab__sidebar",
3662
+ contentClassName: "lk-lab__main",
3663
+ label: "Lab sidebar",
3664
+ defaultWidth: 260,
3665
+ width: sidebarWidth,
3666
+ onWidthChange: (w2) => {
3667
+ setSidebarWidth(w2);
3668
+ surface?.invalidateRects();
3669
+ },
3670
+ sidebar: /* @__PURE__ */ jsx(LabSidebarRegion, { contributions }),
3671
+ children
3672
+ }
3673
+ );
3674
+ }
3675
+ function Lab(props) {
3676
+ if (process.env.NODE_ENV !== "production" && props.instruments.length === 0) {
3677
+ throw new Error("[labkit] <Lab> requires a non-empty `instruments` array");
3678
+ }
3679
+ const { storageKey, storage } = props;
3680
+ useWarnIgnoredChange("<Lab>", { storageKey, storage });
3681
+ const opened = useOpenOnce(
3682
+ () => storageKey === void 0 ? openUnstoredLab(props) : openStoredLab(props, storageKey, storage)
3683
+ );
3684
+ if (!opened) {
3685
+ return props.fallback !== void 0 ? /* @__PURE__ */ jsx(Fragment, { children: props.fallback }) : /* @__PURE__ */ jsx(LabFallback, { title: props.title, mode: props.mode, pages: props.pages, path: props.path });
3686
+ }
3687
+ return /* @__PURE__ */ jsx(LabRuntime, { ...props, opened });
3688
+ }
3689
+ function LabRuntime({
3769
3690
  instruments,
3770
- defaultInstrument,
3771
- storage,
3772
- storageKey,
3773
3691
  mode,
3774
3692
  nebula,
3775
3693
  title,
3694
+ pages,
3695
+ path,
3776
3696
  footer,
3777
3697
  chrome,
3698
+ labChrome,
3778
3699
  suppress,
3700
+ addTrial: addTrial2,
3779
3701
  tools,
3780
3702
  configRules,
3781
3703
  controls,
3782
- children
3704
+ children,
3705
+ opened
3783
3706
  }) {
3784
- if (process.env.NODE_ENV !== "production" && instruments.length === 0) {
3785
- throw new Error("[labkit] <Lab> requires a non-empty `instruments` array");
3786
- }
3787
- const storeRef = useRef(null);
3788
- if (storeRef.current === null) {
3789
- storeRef.current = buildStore(
3790
- instruments,
3791
- defaultInstrument,
3792
- storage ?? noneAdapter,
3793
- storageKey ?? "labkit",
3794
- mode ?? "auto"
3795
- );
3796
- }
3797
- const store = storeRef.current;
3707
+ const { store } = opened;
3708
+ useLayoutEffect(() => {
3709
+ store.getState().setInstruments(instruments);
3710
+ }, [instruments, store]);
3798
3711
  const trials = useStore(store, (s) => s.trials);
3799
3712
  const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
3800
3713
  const modeValue = useStore(store, (s) => s.mode);
@@ -3803,28 +3716,48 @@ function Lab({
3803
3716
  const panelHostsRef = useRef(null);
3804
3717
  if (panelHostsRef.current === null) panelHostsRef.current = createPanelHostRegistry();
3805
3718
  const outerSurface = useSurfaceOptional();
3806
- const outerCanvas = useSurfaceCanvas();
3807
- const [ownCanvas, setOwnCanvas] = useState(null);
3808
- const ownCanvasRef = useRef(null);
3719
+ const outerOver = useSurfaceCanvas("over");
3720
+ const outerUnder = useSurfaceCanvas("under");
3721
+ const [ownOver, setOwnOver] = useState(null);
3722
+ const [ownUnder, setOwnUnder] = useState(null);
3723
+ const ownOverRef = useRef(null);
3724
+ const ownUnderRef = useRef(null);
3809
3725
  const bufferRef = useRef({ w: 0, h: 0 });
3810
3726
  const surfaceRef = useRef(null);
3811
3727
  const onFrame = useCallback((frame) => {
3812
- const c2 = ownCanvasRef.current;
3813
- if (!c2) return;
3814
- const w3 = Math.round(frame.size.width * frame.dpr);
3815
- const h = Math.round(frame.size.height * frame.dpr);
3816
- if (bufferRef.current.w === w3 && bufferRef.current.h === h) return;
3817
- bufferRef.current = { w: w3, h };
3818
- c2.width = w3;
3819
- c2.height = h;
3820
- c2.style.width = `${frame.size.width}px`;
3821
- c2.style.height = `${frame.size.height}px`;
3822
- surfaceRef.current?.invalidateAll();
3728
+ const canvases = [ownUnderRef.current, ownOverRef.current].filter((c3) => c3 !== null);
3729
+ if (canvases.length === 0) return;
3730
+ const w2 = Math.round(frame.size.width * frame.dpr);
3731
+ const h2 = Math.round(frame.size.height * frame.dpr);
3732
+ if (bufferRef.current.w !== w2 || bufferRef.current.h !== h2) {
3733
+ bufferRef.current = { w: w2, h: h2 };
3734
+ for (const c3 of canvases) {
3735
+ c3.width = w2;
3736
+ c3.height = h2;
3737
+ c3.style.width = `${frame.size.width}px`;
3738
+ c3.style.height = `${frame.size.height}px`;
3739
+ }
3740
+ surfaceRef.current?.invalidateAll();
3741
+ return;
3742
+ }
3823
3743
  }, []);
3824
3744
  const ownSurface = useTiledSurface({ onFrame });
3825
3745
  surfaceRef.current = ownSurface;
3746
+ const [labBody, setLabBody] = useState(null);
3747
+ const attachOwnSurface = ownSurface.containerRef;
3748
+ const labBodyRef = useCallback(
3749
+ (el) => {
3750
+ setLabBody(el);
3751
+ if (!outerSurface) attachOwnSurface(el);
3752
+ },
3753
+ [outerSurface, attachOwnSurface]
3754
+ );
3755
+ useLabFitWarning(labBody);
3826
3756
  const surface = outerSurface ?? ownSurface;
3827
- const surfaceCanvas = outerSurface ? outerCanvas : ownCanvas;
3757
+ const surfaceCanvases = useMemo(
3758
+ () => outerSurface ? { over: outerOver, under: outerUnder } : { over: ownOver, under: ownUnder },
3759
+ [outerSurface, outerOver, outerUnder, ownOver, ownUnder]
3760
+ );
3828
3761
  const workspacePanels = useMemo(
3829
3762
  () => Object.entries(undockedPanels).map(([key, panel]) => ({
3830
3763
  key,
@@ -3834,6 +3767,13 @@ function Lab({
3834
3767
  [undockedPanels]
3835
3768
  );
3836
3769
  const resolvedMode = useResolvedMode(modeValue);
3770
+ const annotates = instruments.some((i) => i.annotations != null);
3771
+ const labChromeAll = useMemo(
3772
+ () => labContributions([...annotates ? ANNOTATION_TOOLS : [], ...tools ?? []], labChrome),
3773
+ [annotates, tools, labChrome]
3774
+ );
3775
+ const hasFooterChrome = labChromeAll.some((c3) => c3.region === "footer");
3776
+ const hasSidebarChrome = labChromeAll.some((c3) => c3.region === "sidebar");
3837
3777
  useEffect(() => {
3838
3778
  if (mode && mode !== store.getState().mode) {
3839
3779
  store.getState().setMode(mode);
@@ -3841,10 +3781,9 @@ function Lab({
3841
3781
  }, [mode, store]);
3842
3782
  const contextValue = useMemo(() => {
3843
3783
  const replaceTrials = (next) => {
3844
- const currentById = new Map(store.getState().trials.map((w3) => [w3.id, w3]));
3845
- const merged = next.map((w3) => currentById.get(w3.id) ?? w3);
3784
+ const currentById = new Map(store.getState().trials.map((w2) => [w2.id, w2]));
3785
+ const merged = next.map((w2) => currentById.get(w2.id) ?? w2);
3846
3786
  store.setState({ trials: merged });
3847
- store.getState().setMode(store.getState().mode);
3848
3787
  };
3849
3788
  return {
3850
3789
  instruments,
@@ -3866,14 +3805,13 @@ function Lab({
3866
3805
  },
3867
3806
  resetTrial: (id) => {
3868
3807
  const next = resetTrial(store.getState().trials, id, instruments);
3869
- const record = next.find((w3) => w3.id === id);
3808
+ const record = next.find((w2) => w2.id === id);
3870
3809
  if (!record) return;
3871
3810
  store.setState((s) => ({
3872
3811
  trials: s.trials.map(
3873
- (w3) => w3.id === id ? { ...w3, config: record.config, state: record.state, view: record.view } : w3
3812
+ (w2) => w2.id === id ? { ...w2, config: record.config, state: record.state, view: record.view } : w2
3874
3813
  )
3875
3814
  }));
3876
- store.getState().updateTrialView(id, record.view);
3877
3815
  },
3878
3816
  savedSnapshots,
3879
3817
  saveSnapshot: (trialId, name) => {
@@ -3895,7 +3833,20 @@ function Lab({
3895
3833
  };
3896
3834
  }, [instruments, trials, savedSnapshots, modeValue, store, configRules, controls]);
3897
3835
  const backdropStyle = resolvedMode === "dark" && nebula && nebula.length > 0 ? { ["--wzl-backdrop"]: buildNebula(nebula) } : void 0;
3898
- return /* @__PURE__ */ jsx(LabStoreContext.Provider, { value: { store }, children: /* @__PURE__ */ jsx(LabContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
3836
+ const workspace = /* @__PURE__ */ jsx(
3837
+ Workspace,
3838
+ {
3839
+ panels: workspacePanels,
3840
+ ids: trials.map((w2) => w2.id),
3841
+ resizable: true,
3842
+ reorderable: true,
3843
+ onReorder: (ids) => contextValue.reorderTrials(ids),
3844
+ layout,
3845
+ onLayoutChange: (next) => store.getState().setLayout(next),
3846
+ children: trials.map((w2) => /* @__PURE__ */ jsx(Trial, { id: w2.id, chrome, suppress }, w2.id))
3847
+ }
3848
+ );
3849
+ return /* @__PURE__ */ jsx(LabStoreContext.Provider, { value: { store }, children: /* @__PURE__ */ jsx(PersistenceContext.Provider, { value: opened.records, children: /* @__PURE__ */ jsx(AnnotationPreloadContext.Provider, { value: opened.marks, children: /* @__PURE__ */ jsx(LabContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
3899
3850
  ThemeProvider,
3900
3851
  {
3901
3852
  theme: interstellarTheme,
@@ -3907,55 +3858,61 @@ function Lab({
3907
3858
  {
3908
3859
  title: title ?? "Labkit",
3909
3860
  mode: modeValue,
3910
- footer,
3861
+ ...pages ? { pages } : {},
3862
+ ...path !== void 0 ? { path } : {},
3863
+ footer: hasFooterChrome ? /* @__PURE__ */ jsxs(Fragment, { children: [
3864
+ footer,
3865
+ /* @__PURE__ */ jsx(LabFooterRegion, { contributions: labChromeAll })
3866
+ ] }) : footer,
3911
3867
  header: /* @__PURE__ */ jsxs(Fragment, { children: [
3912
- /* @__PURE__ */ jsx(LabHeader, {}),
3913
- children
3868
+ /* @__PURE__ */ jsx(LabHeader, { ...addTrial2 !== void 0 ? { addTrial: addTrial2 } : {} }),
3869
+ children,
3870
+ /* @__PURE__ */ jsx(LabHeaderRegion, { contributions: labChromeAll })
3914
3871
  ] }),
3915
- children: /* @__PURE__ */ jsx(PanelHostContext.Provider, { value: panelHostsRef.current, children: /* @__PURE__ */ jsx(SurfaceContext.Provider, { value: surface, children: /* @__PURE__ */ jsx(SurfaceCanvasContext.Provider, { value: surfaceCanvas, children: /* @__PURE__ */ jsxs(
3916
- "div",
3917
- {
3918
- className: "lk-lab__body",
3919
- ref: outerSurface ? void 0 : ownSurface.containerRef,
3920
- children: [
3921
- outerSurface ? null : (
3922
- // Above the trials and inert: the marks a tile paints have
3923
- // to sit over the instrument's own DOM, and nothing on this
3924
- // buffer takes input — each tile has its own input box.
3925
- /* @__PURE__ */ jsx(
3926
- "canvas",
3927
- {
3928
- className: "lk-lab__surface",
3929
- ref: (el) => {
3930
- ownCanvasRef.current = el;
3931
- setOwnCanvas(el);
3932
- }
3872
+ children: /* @__PURE__ */ jsx(PanelHostContext.Provider, { value: panelHostsRef.current, children: /* @__PURE__ */ jsx(SurfaceContext.Provider, { value: surface, children: /* @__PURE__ */ jsx(SurfaceCanvasContext.Provider, { value: surfaceCanvases, children: /* @__PURE__ */ jsxs("div", { className: "lk-lab__body", ref: labBodyRef, children: [
3873
+ outerSurface ? null : (
3874
+ // Two buffers stacked around the trials, both inert —
3875
+ // each tile takes input from its own box. A tile's marks
3876
+ // annotate the instrument's DOM from over it; an opaque
3877
+ // renderer sits under it, so the pane can still hold a
3878
+ // label. The under one is first so it paints first.
3879
+ /* @__PURE__ */ jsxs(Fragment, { children: [
3880
+ /* @__PURE__ */ jsx(
3881
+ "canvas",
3882
+ {
3883
+ className: "lk-lab__surface lk-lab__surface--under",
3884
+ ref: (el) => {
3885
+ ownUnderRef.current = el;
3886
+ setOwnUnder(el);
3933
3887
  }
3934
- )
3888
+ }
3935
3889
  ),
3936
- tools ? /* @__PURE__ */ jsx(LabPalette, { tools }) : null,
3937
3890
  /* @__PURE__ */ jsx(
3938
- Workspace,
3891
+ "canvas",
3939
3892
  {
3940
- panels: workspacePanels,
3941
- ids: trials.map((w3) => w3.id),
3942
- resizable: true,
3943
- reorderable: true,
3944
- onReorder: (ids) => contextValue.reorderTrials(ids),
3945
- layout,
3946
- onLayoutChange: (next) => store.getState().setLayout(next),
3947
- children: trials.map((w3) => /* @__PURE__ */ jsx(Trial, { id: w3.id, chrome, suppress }, w3.id))
3893
+ className: "lk-lab__surface lk-lab__surface--over",
3894
+ ref: (el) => {
3895
+ ownOverRef.current = el;
3896
+ setOwnOver(el);
3897
+ }
3948
3898
  }
3949
3899
  )
3950
- ]
3951
- }
3952
- ) }) }) })
3900
+ ] })
3901
+ ),
3902
+ hasSidebarChrome ? /* @__PURE__ */ jsxs(LabPanes, { contributions: labChromeAll, children: [
3903
+ /* @__PURE__ */ jsx(LabPalette, { contributions: labChromeAll }),
3904
+ workspace
3905
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3906
+ /* @__PURE__ */ jsx(LabPalette, { contributions: labChromeAll }),
3907
+ workspace
3908
+ ] })
3909
+ ] }) }) }) })
3953
3910
  }
3954
3911
  )
3955
3912
  }
3956
- ) }) });
3913
+ ) }) }) }) });
3957
3914
  }
3958
3915
 
3959
- export { AnnotationOverlay, AnnotationTargets, DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell, Trial, TrialBody, TrialChrome, Workspace, addTrial, annotationsFromJSON, applyRules, as2DView, builtinRules, capturePlan, captureTarget, cloneTrial, closeTrial, composeCaptureSvg, createAnnotationScene, createAnnotationStore, createMarkDrawOne, defineInstrument, f, fitView, fracContains, fracIntersects, fracToWorld, fromWeaselView, interstellarTheme, isConfigBranch, isStale, markCommands, markSvgNodes, reorderTrials, resetTrial, resolveConfigSchema, resolveMarkStyle, roundFrac, seenFrom, titleCase, toWeaselView, useConfigSchema, useLabContext, validateConfigSchema, worldToFrac };
3916
+ export { ANNOTATION_TOOLS, ANNOTATION_WEASEL_TOOLS, AnnotationOverlay, AnnotationTargets, Lab, LabPalette, LabShell, LabSwitcher, Trial, TrialBody, TrialChrome, Workspace, addTrial, annotationToolInfo, annotationsFromJSON, capturePlan, captureTarget, cloneTrial, closeTrial, composeCaptureSvg, createAnnotationScene, createAnnotationStore, createMarkDrawOne, currentPage, defineInstrument, fitView, fracContains, fracEncloses, fracToWorld, fromWeaselView, interstellarTheme, isStale, markCommands, markSvgNodes, reorderTrials, resetTrial, resolveMarkStyle, roundFrac, seenFrom, toWeaselView, validateConfigSchema, worldToFrac };
3960
3917
  //# sourceMappingURL=index.js.map
3961
3918
  //# sourceMappingURL=index.js.map