@weasel-js/labkit 1.4.4 → 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 (219) hide show
  1. package/README.md +13 -2
  2. package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CqkxkFJW.d.ts} +1 -1
  3. package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +116 -0
  4. package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-8R6UmlvN.d.ts} +253 -31
  5. package/dist/_dts/index-B8uu2ba7.d.ts +237 -0
  6. package/dist/_dts/{index-Blv9uQzu.d.ts → index-retnAut7.d.ts} +18 -10
  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-MOM3GOVY.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-SMHP6XZ4.js → chunk-KJALCDWE.js} +9 -7
  23. package/dist/chunk-KJALCDWE.js.map +1 -0
  24. package/dist/{chunk-BKFVHKJH.js → chunk-MUKOW3TC.js} +3 -3
  25. package/dist/{chunk-BKFVHKJH.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-64ZCN3DA.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-U3IYHIAE.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 +110 -381
  53. package/dist/index.js +691 -817
  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 +164 -122
  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 +84 -50
  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/paint.test.ts +2 -2
  77. package/src/annotations/paint.ts +1 -1
  78. package/src/annotations/preload.ts +6 -0
  79. package/src/annotations/store.test.ts +18 -0
  80. package/src/annotations/store.ts +5 -4
  81. package/src/annotations/svgNodes.test.ts +2 -2
  82. package/src/annotations/toolMap.ts +3 -3
  83. package/src/annotations/types.ts +6 -3
  84. package/src/canvas/CameraWheelContext.ts +12 -0
  85. package/src/canvas/CanvasStack.test.tsx +25 -0
  86. package/src/canvas/CanvasStack.tsx +31 -4
  87. package/src/canvas/Stage.less +18 -0
  88. package/src/canvas/Stage.test.tsx +186 -0
  89. package/src/canvas/Stage.tsx +134 -0
  90. package/src/canvas/camera.test.ts +17 -2
  91. package/src/canvas/camera.ts +9 -9
  92. package/src/canvas/index.ts +4 -0
  93. package/src/canvas/usePanZoom.ts +10 -5
  94. package/src/chrome/ChromeRegions.stories.tsx +0 -3
  95. package/src/chrome/LabChrome.tsx +114 -0
  96. package/src/chrome/builtins.tsx +20 -31
  97. package/src/chrome/index.ts +15 -0
  98. package/src/chrome/labRegions.test.tsx +208 -0
  99. package/src/chrome/labTypes.ts +50 -0
  100. package/src/chrome/merge.ts +7 -9
  101. package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
  102. package/src/chrome/regions/PaletteRegion.tsx +21 -9
  103. package/src/chrome/regions/SidebarRegion.tsx +25 -13
  104. package/src/chrome/regions/StatusRegion.tsx +15 -8
  105. package/src/chrome/regions/ToolbarRegion.tsx +30 -16
  106. package/src/chrome/types.ts +47 -20
  107. package/src/config/builder.test.ts +4 -0
  108. package/src/config/builder.ts +16 -10
  109. package/src/config/declarationEmit.fixture.ts +11 -0
  110. package/src/config/declarationEmit.test.ts +51 -0
  111. package/src/config/entry.test.ts +46 -0
  112. package/src/config/index.ts +13 -1
  113. package/src/config/nodeClasses.test.ts +18 -0
  114. package/src/config/types.ts +3 -1
  115. package/src/controls/ControlPanel.test.tsx +16 -0
  116. package/src/controls/ControlPanel.tsx +5 -2
  117. package/src/fake-indexeddb-auto.d.ts +5 -0
  118. package/src/index.test.ts +7 -0
  119. package/src/index.ts +38 -5
  120. package/src/instrument/serializers.test.ts +22 -0
  121. package/src/instrument/serializers.ts +11 -0
  122. package/src/instrument/types.ts +25 -1
  123. package/src/lab/Lab.less +4 -13
  124. package/src/lab/Lab.persist.test.tsx +147 -0
  125. package/src/lab/Lab.stories.tsx +0 -2
  126. package/src/lab/Lab.surface.test.tsx +66 -3
  127. package/src/lab/Lab.test.tsx +91 -17
  128. package/src/lab/Lab.tsx +337 -124
  129. package/src/lab/LabFit.stories.less +18 -0
  130. package/src/lab/LabFit.stories.tsx +219 -0
  131. package/src/lab/LabFullChrome.stories.tsx +0 -3
  132. package/src/lab/LabHeader.test.tsx +40 -9
  133. package/src/lab/LabHeader.tsx +7 -13
  134. package/src/lab/LabPalette.tsx +13 -26
  135. package/src/lab/LabShell.less +49 -12
  136. package/src/lab/LabSwitcher.less +8 -1
  137. package/src/lab/Workspace.surface.test.tsx +2 -1
  138. package/src/lab/fitCheck.test.ts +57 -0
  139. package/src/lab/fitCheck.ts +125 -0
  140. package/src/loupe/AGENTS.md +1 -1
  141. package/src/loupe/Loupe.less +1 -1
  142. package/src/loupe/canvasLens.ts +1 -1
  143. package/src/loupe/types.ts +1 -1
  144. package/src/loupe/useLoupe.test.tsx +1 -1
  145. package/src/loupe/useLoupe.ts +1 -1
  146. package/src/passthrough/weasel-ui.ts +9 -0
  147. package/src/primitives/FloatingPanel.stories.tsx +13 -1
  148. package/src/primitives/FloatingPanel.test.tsx +47 -16
  149. package/src/primitives/FloatingPanel.tsx +11 -25
  150. package/src/primitives/Split.less +14 -0
  151. package/src/primitives/Split.test.tsx +108 -0
  152. package/src/primitives/Split.tsx +159 -0
  153. package/src/primitives/ZoomControl.tsx +5 -2
  154. package/src/primitives/index.ts +2 -0
  155. package/src/state/Persistence.stories.tsx +75 -0
  156. package/src/state/Persistence.tsx +33 -0
  157. package/src/state/SingletonExperiment.test.tsx +47 -53
  158. package/src/state/SingletonExperiment.tsx +24 -13
  159. package/src/state/adapterContract.ts +116 -0
  160. package/src/state/adapters.test.ts +113 -61
  161. package/src/state/adapters.ts +328 -69
  162. package/src/state/document.test.ts +71 -96
  163. package/src/state/document.ts +66 -52
  164. package/src/state/helpers.test.ts +1 -1
  165. package/src/state/index.ts +8 -0
  166. package/src/state/labRecords.test.ts +105 -0
  167. package/src/state/labRecords.ts +160 -0
  168. package/src/state/openLabStore.test.ts +658 -0
  169. package/src/state/openLabStore.ts +403 -0
  170. package/src/state/records.test.ts +234 -0
  171. package/src/state/records.ts +219 -0
  172. package/src/state/store.test.ts +104 -635
  173. package/src/state/store.ts +116 -157
  174. package/src/state/toolSlot.test.ts +1 -1
  175. package/src/state/types.ts +39 -15
  176. package/src/state/useOpenOnce.ts +68 -0
  177. package/src/state/usePersistedState.test.tsx +125 -0
  178. package/src/state/usePersistedState.ts +66 -0
  179. package/src/state/useTrialState.test.tsx +2 -3
  180. package/src/state/view.test.ts +49 -15
  181. package/src/state/view.ts +18 -1
  182. package/src/styles.less +2 -0
  183. package/src/surface/AGENTS.md +39 -7
  184. package/src/surface/SurfaceContext.ts +26 -3
  185. package/src/surface/index.ts +2 -0
  186. package/src/surface/useSurfaceTile.test.tsx +2 -1
  187. package/src/surface/useSurfaceTile.ts +6 -5
  188. package/src/surface/useTiledSurface.test.tsx +175 -0
  189. package/src/surface/useTiledSurface.ts +54 -5
  190. package/src/theme/Interstellar.stories.tsx +7 -7
  191. package/src/theme/interstellar.test.ts +1 -1
  192. package/src/theme/interstellar.tokens.json +2 -2
  193. package/src/tools/labTool.ts +11 -0
  194. package/src/trial/Trial.annotations.persist.test.tsx +100 -58
  195. package/src/trial/Trial.annotations.test.tsx +47 -4
  196. package/src/trial/Trial.job.test.tsx +3 -3
  197. package/src/trial/Trial.less +1 -26
  198. package/src/trial/Trial.stories.tsx +1 -2
  199. package/src/trial/Trial.test.tsx +28 -4
  200. package/src/trial/Trial.trialId.test.tsx +23 -1
  201. package/src/trial/Trial.tsx +118 -38
  202. package/src/trial/TrialBody.tsx +17 -141
  203. package/src/trial/TrialChrome.tsx +7 -4
  204. package/src/trial/trialOps.ts +5 -3
  205. package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
  206. package/dist/_dts/types-x92Kfeme.d.ts +0 -62
  207. package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
  208. package/dist/chunk-2P6PP5N4.js +0 -553
  209. package/dist/chunk-2P6PP5N4.js.map +0 -1
  210. package/dist/chunk-64ZCN3DA.js.map +0 -1
  211. package/dist/chunk-CTRKTLYZ.js.map +0 -1
  212. package/dist/chunk-FQMJUVHJ.js.map +0 -1
  213. package/dist/chunk-I6JCVE24.js.map +0 -1
  214. package/dist/chunk-MOM3GOVY.js.map +0 -1
  215. package/dist/chunk-SMHP6XZ4.js.map +0 -1
  216. package/dist/chunk-TJ7QY3OC.js.map +0 -1
  217. package/dist/chunk-U3IYHIAE.js.map +0 -1
  218. package/dist/chunk-W3ECWC2K.js +0 -7335
  219. package/dist/chunk-W3ECWC2K.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-64ZCN3DA.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-64ZCN3DA.js';
10
- import { JobProgress } from './chunk-MOM3GOVY.js';
11
- export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-MOM3GOVY.js';
12
- export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-SMHP6XZ4.js';
13
- import { newId, createLabStore, panelKey, noneAdapter } from './chunk-2P6PP5N4.js';
14
- export { CURRENT_DOCUMENT_VERSION, createMemoryAdapter, dockPanel, labDocumentKey, localStorageAdapter, noneAdapter, panelKey, quarantineKey, sessionStorageAdapter, undockPanel, urlHashAdapter, useTrialState } from './chunk-2P6PP5N4.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-U3IYHIAE.js';
18
- export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-U3IYHIAE.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-BKFVHKJH.js';
24
- export { LayerList } from './chunk-BKFVHKJH.js';
25
- import './chunk-SBXNQW4G.js';
26
- import { E, w, T, C, g, r6 } from './chunk-W3ECWC2K.js';
27
- export { N2 as CheckboxRow, M2 as ColorRow, y3 as CurveField, z as EffectCard, V as EffectCardList, c3 as LayerStack, F as NumberRow, H as PropertyGroup, T2 as PropertyList, w2 as PropertyPanel, D as PropertyRow, E2 as PropertySpan, I as SelectRow, A2 as SliderRow, R as Subpanel, P2 as TextRow, L2 as ToggleRow } from './chunk-W3ECWC2K.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, 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, useId, useCallback, Children, useLayoutEffect, 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(" ")})`;
60
68
  }
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);
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);
74
+ }
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
  }
@@ -495,42 +507,42 @@ function strokeAttrsFor(stroke, registry) {
495
507
  }
496
508
  function computeBounds(nodes) {
497
509
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
498
- const visit = (n, ctm) => {
499
- if (n.kind === "group") {
500
- const childCtm = n.transform ? multiply(ctm, n.transform) : ctm;
501
- 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));
502
514
  return;
503
515
  }
504
- 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);
505
- const local = n.rotation ? rotateAboutCenter(ctm, b, n.rotation) : ctm;
506
- for (const [x, y2] of corners(b)) {
507
- const px = local[0] * x + local[2] * y2 + local[4];
508
- 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];
509
521
  if (px < minX) minX = px;
510
522
  if (py < minY) minY = py;
511
523
  if (px > maxX) maxX = px;
512
524
  if (py > maxY) maxY = py;
513
525
  }
514
526
  };
515
- nodes.forEach((n) => visit(n, IDENTITY_MATRIX));
527
+ nodes.forEach((n2) => visit(n2, IDENTITY_MATRIX));
516
528
  if (!Number.isFinite(minX)) {
517
529
  return { x: 0, y: 0, width: 0, height: 0 };
518
530
  }
519
531
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
520
532
  }
521
- function corners(b) {
522
- 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]];
523
535
  }
524
- function rotateAboutCenter(ctm, b, angle) {
525
- const cx = (b.minX + b.maxX) / 2;
526
- const cy = (b.minY + b.maxY) / 2;
527
- 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);
528
540
  const s = Math.sin(angle);
529
- 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]);
530
542
  }
531
543
  function textXml(node, registry, namespaces) {
532
544
  const attrs = [
533
- `x="${trimNumber(node.x)}"`,
545
+ `x="${trimNumber(node.x + anchorOffset(node.style, node.width))}"`,
534
546
  `y="${trimNumber(node.y)}"`,
535
547
  `dominant-baseline="text-before-edge"`,
536
548
  // The runs model stores real line breaks; SVG's default whitespace
@@ -556,6 +568,7 @@ function textXml(node, registry, namespaces) {
556
568
  }
557
569
  const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
558
570
  if (decoration) attrs.push(`text-decoration="${decoration}"`);
571
+ if (style?.wrap) attrs.push('data-weasel-wrap="true"');
559
572
  if (node.fill === null) {
560
573
  attrs.push('fill="none"');
561
574
  } else if (node.fill) {
@@ -569,7 +582,7 @@ function textXml(node, registry, namespaces) {
569
582
  attrs.push(`fill="url(#${id})"`);
570
583
  }
571
584
  }
572
- for (const a of coreStrokeAttrs(node.stroke, registry)) attrs.push(a);
585
+ for (const a2 of coreStrokeAttrs(node.stroke, registry)) attrs.push(a2);
573
586
  if (node.opacity != null && node.opacity !== 1) {
574
587
  attrs.push(`opacity="${trimNumber(node.opacity)}"`);
575
588
  }
@@ -579,7 +592,7 @@ function textXml(node, registry, namespaces) {
579
592
  const deg = node.rotation * 180 / Math.PI;
580
593
  attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
581
594
  }
582
- 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);
583
596
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
584
597
  const metaEls = metaElementsXml(node.meta, namespaces);
585
598
  return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
@@ -611,7 +624,7 @@ function runXml(run, registry) {
611
624
  attrs.push(`fill="url(#${id})"`);
612
625
  }
613
626
  }
614
- for (const a of coreStrokeAttrs(run.stroke, registry)) attrs.push(a);
627
+ for (const a2 of coreStrokeAttrs(run.stroke, registry)) attrs.push(a2);
615
628
  const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
616
629
  return `${head}${escapeText(run.text)}</tspan>`;
617
630
  }
@@ -637,32 +650,32 @@ function pathBounds(path) {
637
650
  maxY: path.y + path.height
638
651
  };
639
652
  }
640
- const b = boundsOfPath(path);
641
- 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 };
642
655
  }
643
656
  var MARK_COLOR = "#e5484d";
644
657
  var MARK_WIDTH = 2;
645
658
  var TEXT_SIZE = 14;
646
- var toWorld = (p, content) => ({
647
- x: p.x * content.w,
648
- y: p.y * content.h
659
+ var toWorld = (p2, content) => ({
660
+ x: p2.x * content.w,
661
+ y: p2.y * content.h
649
662
  });
650
663
  function vertices(m, content) {
651
664
  const stored = m.data.points;
652
- if (stored && stored.length >= 2) return stored.map((p) => toWorld(p, content));
653
- 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;
654
667
  return [
655
- { x, y: y2 },
656
- { x: x + width, y: y2 + height }
668
+ { x, y },
669
+ { x: x + width, y: y + height }
657
670
  ];
658
671
  }
659
672
  function polyline(points) {
660
- const b = new PathBuilder();
673
+ const b2 = new PathBuilder();
661
674
  const first = points[0];
662
675
  if (!first) return rectPath(0, 0, 0, 0);
663
- b.moveTo(first.x, first.y);
664
- for (const p of points.slice(1)) b.lineTo(p.x, p.y);
665
- 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();
666
679
  }
667
680
  function markCommands(m, content, style = {}) {
668
681
  const color = style.color ?? MARK_COLOR;
@@ -681,13 +694,13 @@ function markCommands(m, content, style = {}) {
681
694
  case "ellipse":
682
695
  return [{ kind: "path", path: ellipsePath(m.pose), stroke }];
683
696
  case "line": {
684
- const [a, b] = vertices(m, content);
685
- return [{ kind: "path", path: linePath(a, b), stroke }];
697
+ const [a2, b2] = vertices(m, content);
698
+ return [{ kind: "path", path: linePath(a2, b2), stroke }];
686
699
  }
687
700
  case "arrow": {
688
- const [a, b] = vertices(m, content);
701
+ const [a2, b2] = vertices(m, content);
689
702
  const marked = { ...stroke, markerEnd: "arrow" };
690
- const path = linePath(a, b);
703
+ const path = linePath(a2, b2);
691
704
  return [
692
705
  { kind: "path", path, stroke: marked },
693
706
  ...markerDrawCommands(path, marked, MARK_WIDTH, void 0)
@@ -806,7 +819,7 @@ function capturePlan(base, format) {
806
819
  if (format === "svg") return "svg-document";
807
820
  return base?.kind === "svg" ? "svg-document" : "raster-stack";
808
821
  }
809
- function nestBaseSvg(markup, w3, h) {
822
+ function nestBaseSvg(markup, w2, h2) {
810
823
  const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
811
824
  const root = doc.documentElement;
812
825
  if (doc.getElementsByTagName("parsererror").length > 0 || root.localName !== "svg") {
@@ -814,17 +827,17 @@ function nestBaseSvg(markup, w3, h) {
814
827
  }
815
828
  root.setAttribute("x", "0");
816
829
  root.setAttribute("y", "0");
817
- root.setAttribute("width", String(w3));
818
- root.setAttribute("height", String(h));
830
+ root.setAttribute("width", String(w2));
831
+ root.setAttribute("height", String(h2));
819
832
  return new XMLSerializer().serializeToString(root);
820
833
  }
821
- function embedRasterBase(base, w3, h, onWarn) {
834
+ function embedRasterBase(base, w2, h2, onWarn) {
822
835
  const href = base.kind === "canvas" ? base.canvas.toDataURL() : base.kind === "image" ? base.src : "";
823
836
  if (base.kind === "image" && !href.startsWith("data:")) {
824
837
  onWarn?.(`base image '${href}' is a reference, so this SVG is not self-contained`);
825
838
  }
826
- return serializeSvg([{ kind: "image", href, x: 0, y: 0, width: w3, height: h }], {
827
- 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 }
828
841
  });
829
842
  }
830
843
  function markNodesOf(scene, draw) {
@@ -840,15 +853,15 @@ function markNodesOf(scene, draw) {
840
853
  }
841
854
  function composeCaptureSvg(args) {
842
855
  const { base, scene, draw, scale, onWarn } = args;
843
- const { w: w3, h } = draw.content;
844
- 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);
845
858
  const marksXml = serializeSvg(markNodesOf(scene, draw), {
846
- viewBox: { x: 0, y: 0, width: w3, height: h },
859
+ viewBox: { x: 0, y: 0, width: w2, height: h2 },
847
860
  onWarn
848
861
  });
849
- const outW = Math.max(1, Math.round(w3 * scale));
850
- const outH = Math.max(1, Math.round(h * scale));
851
- 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>`;
852
865
  }
853
866
  function context2d(width, height) {
854
867
  const canvas = document.createElement("canvas");
@@ -942,26 +955,26 @@ async function captureTarget(args, opts = {}) {
942
955
  }
943
956
 
944
957
  // src/annotations/frac.ts
945
- function fracToWorld(f2, content) {
958
+ function fracToWorld(f3, content) {
946
959
  return {
947
- x: f2.x * content.w,
948
- y: f2.y * content.h,
949
- width: f2.w * content.w,
950
- 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
951
964
  };
952
965
  }
953
- function worldToFrac(r2, content) {
966
+ function worldToFrac(r3, content) {
954
967
  const sx = content.w === 0 ? 0 : 1 / content.w;
955
968
  const sy = content.h === 0 ? 0 : 1 / content.h;
956
- 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 };
957
970
  }
958
971
  var DP = 1e4;
959
- function roundFrac(f2) {
972
+ function roundFrac(f3) {
960
973
  return {
961
- x: Math.round(f2.x * DP) / DP,
962
- y: Math.round(f2.y * DP) / DP,
963
- w: Math.round(f2.w * DP) / DP,
964
- 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
965
978
  };
966
979
  }
967
980
  function fracContains(box, pt, tol = 0) {
@@ -1104,21 +1117,21 @@ function createAnnotationStore(opts) {
1104
1117
  if (!scene) return [];
1105
1118
  const out = [];
1106
1119
  for (const id of scene.renderOrder()) {
1107
- const a = project(target, String(id));
1108
- if (a) out.push(a);
1120
+ const a2 = project(target, String(id));
1121
+ if (a2) out.push(a2);
1109
1122
  }
1110
1123
  return out;
1111
1124
  };
1112
1125
  const all = () => liveTargets().flatMap(marksOn);
1113
- const matches = (a, q) => {
1114
- if (q.target !== void 0 && a.target !== q.target) return false;
1115
- if (q.kind !== void 0 && a.kind !== q.kind) return false;
1116
- 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;
1117
1130
  if (q.tags !== void 0) {
1118
- const have = a.tags ?? [];
1131
+ const have = a2.tags ?? [];
1119
1132
  for (const t of q.tags) if (!have.includes(t)) return false;
1120
1133
  }
1121
- if (q.where && !q.where(a)) return false;
1134
+ if (q.where && !q.where(a2)) return false;
1122
1135
  return true;
1123
1136
  };
1124
1137
  const nodeOf = (id) => {
@@ -1136,17 +1149,17 @@ function createAnnotationStore(opts) {
1136
1149
  return parts ? project(parts.target, parts.node) : void 0;
1137
1150
  },
1138
1151
  query(q) {
1139
- return q ? all().filter((a) => matches(a, q)) : all();
1152
+ return q ? all().filter((a2) => matches(a2, q)) : all();
1140
1153
  },
1141
1154
  hitTest(target, pt, tol = 0) {
1142
- return marksOn(target).filter((a) => fracContains(a.frac, pt, tol)).reverse();
1155
+ return marksOn(target).filter((a2) => fracContains(a2.frac, pt, tol)).reverse();
1143
1156
  },
1144
1157
  within(target, box) {
1145
- return marksOn(target).filter((a) => fracEncloses(box, a.frac));
1158
+ return marksOn(target).filter((a2) => fracEncloses(box, a2.frac));
1146
1159
  },
1147
- isStale(a, config) {
1148
- const keys = targetOf(a.target)?.positionDependsOn ?? [];
1149
- return isStale(a.seen, config, keys);
1160
+ isStale(a2, config) {
1161
+ const keys = targetOf(a2.target)?.positionDependsOn ?? [];
1162
+ return isStale(a2.seen, config, keys);
1150
1163
  },
1151
1164
  canUndo() {
1152
1165
  return history.canUndo(historySceneAt);
@@ -1184,7 +1197,7 @@ function createAnnotationStore(opts) {
1184
1197
  for (const [target, scene] of scenes) {
1185
1198
  const next = wanted.get(target) ?? [];
1186
1199
  const now = scene.getSelection();
1187
- 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;
1188
1201
  scene.setSelection(next.map(asNodeId$1));
1189
1202
  }
1190
1203
  },
@@ -1246,7 +1259,7 @@ function createAnnotationStore(opts) {
1246
1259
  content: info.content,
1247
1260
  positionDependsOn: info.positionDependsOn,
1248
1261
  config: opts.config?.(),
1249
- meaning: opts.meaning
1262
+ meaning: typeof opts.meaning === "function" ? opts.meaning() : opts.meaning
1250
1263
  },
1251
1264
  base: info.base,
1252
1265
  onWarn: (message) => console.warn(`[labkit] capture: ${message}`)
@@ -1269,6 +1282,32 @@ function annotationsFromJSON(raw, targets, rest = {}) {
1269
1282
  return createAnnotationStore({ targets, restore: doc.scenes, ...rest });
1270
1283
  }
1271
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
+
1272
1311
  // src/annotations/view.ts
1273
1312
  function toWeaselView(v) {
1274
1313
  return { x: v.pan.x, y: v.pan.y, scale: { x: v.zoom, y: v.zoom } };
@@ -1280,7 +1319,7 @@ function fitView(content, pane) {
1280
1319
  const zoom = content.w > 0 && content.h > 0 ? Math.min(pane.w / content.w, pane.h / content.h) : 1;
1281
1320
  return { zoom: Number.isFinite(zoom) && zoom > 0 ? zoom : 1, pan: { x: 0, y: 0 } };
1282
1321
  }
1283
- 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;
1284
1323
  function ToolBridge({ toolId }) {
1285
1324
  const tools = useActiveToolContext();
1286
1325
  const setActive = tools.setActive;
@@ -1290,9 +1329,9 @@ function ToolBridge({ toolId }) {
1290
1329
  return null;
1291
1330
  }
1292
1331
  function pointsOf(extras, content) {
1293
- const toFrac = (p) => ({
1294
- x: content.w === 0 ? 0 : p.x / content.w,
1295
- 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
1296
1335
  });
1297
1336
  if (extras.kind === "line") {
1298
1337
  return [toFrac(extras.a), toFrac(extras.b)];
@@ -1334,9 +1373,40 @@ function AnnotationOverlay({
1334
1373
  },
1335
1374
  [surface, tileId]
1336
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]);
1337
1407
  useEffect(
1338
1408
  () => surface?.registerPainter(tileId, (next) => {
1339
- setRect((prev) => sameRect(prev, next) ? prev : next);
1409
+ flushSync(() => setRect((prev) => sameRect(prev, next) ? prev : next));
1340
1410
  sceneCanvas.current?.requestRedraw();
1341
1411
  }),
1342
1412
  [surface, tileId]
@@ -1394,7 +1464,7 @@ function AnnotationOverlay({
1394
1464
  /* @__PURE__ */ jsx(
1395
1465
  SceneCanvas,
1396
1466
  {
1397
- ref: sceneCanvas,
1467
+ ref: attachSceneCanvas,
1398
1468
  scene,
1399
1469
  width: rect.w,
1400
1470
  height: rect.h,
@@ -1434,314 +1504,6 @@ function AnnotationTargets({
1434
1504
  )) });
1435
1505
  }
1436
1506
 
1437
- // src/config/builder.ts
1438
- function isConfigBranch(entry) {
1439
- return "children" in entry;
1440
- }
1441
- var BaseNode = class {
1442
- constructor(defaultValue, annotations = {}, options = {}) {
1443
- this.annotations = annotations;
1444
- this.options = options;
1445
- this.default = defaultValue;
1446
- }
1447
- annotations;
1448
- options;
1449
- // `default` is a reserved word in a parameter position, so the field is
1450
- // declared rather than taken as a parameter property.
1451
- default;
1452
- /** Clone into the same subclass. Subclasses with extra constructor
1453
- * arguments override this. */
1454
- with(annotations, options) {
1455
- const Ctor = this.constructor;
1456
- return new Ctor(this.default, annotations, options);
1457
- }
1458
- ann(patch) {
1459
- return this.with({ ...this.annotations, ...patch }, this.options);
1460
- }
1461
- opt(patch) {
1462
- return this.with(this.annotations, { ...this.options, ...patch });
1463
- }
1464
- /** Human-readable label. Defaults to the key, title-cased. */
1465
- label(name) {
1466
- return this.ann({ name });
1467
- }
1468
- /** Longer help text, surfaced as a tooltip on the row label. */
1469
- describe(description) {
1470
- return this.ann({ description });
1471
- }
1472
- /** Omit from the panel unless it is asked to show hidden leaves. */
1473
- hidden() {
1474
- return this.ann({ hidden: true });
1475
- }
1476
- /** Pair with a sibling sharing this id, side-by-side on one row. */
1477
- pair(pair) {
1478
- return this.ann({ pair });
1479
- }
1480
- /** Render under a named section heading. `collapsed` opens the section
1481
- * folded — say it on any one of the section's leaves. */
1482
- section(label, opts = {}) {
1483
- return this.opt({ section: { label, ...opts } });
1484
- }
1485
- /** Show this row only while the predicate holds. Presentational — the value
1486
- * stays in config and the instrument still reads it. */
1487
- showIf(predicate) {
1488
- return this.opt({ showIf: predicate });
1489
- }
1490
- /** Draw this one row yourself, keeping the kind, default and validation. */
1491
- render(renderer) {
1492
- return this.opt({ render: renderer });
1493
- }
1494
- };
1495
- var NumberNode = class extends BaseNode {
1496
- kind = "number";
1497
- /** Bound the value. A number with both bounds renders as a slider. */
1498
- range(min, max) {
1499
- return this.ann({ min, max });
1500
- }
1501
- step(step) {
1502
- return this.ann({ step });
1503
- }
1504
- /** A display suffix for the value — `'px'`, `'ms'`, `'°'`. Presentation only:
1505
- * it is never parsed, and the stored value stays a plain number. */
1506
- suffix(suffix) {
1507
- return this.ann({ suffix });
1508
- }
1509
- /** Force a slider even without both bounds. */
1510
- slider() {
1511
- return this.ann({ control: "slider" });
1512
- }
1513
- /** Force a typed input even when both bounds are set. */
1514
- input() {
1515
- return this.ann({ control: "input" });
1516
- }
1517
- };
1518
- var BooleanNode = class extends BaseNode {
1519
- kind = "boolean";
1520
- /** Ask for a switch. `ControlPanel` still draws a checkbox; weasel-ui's
1521
- * `PrefsForm` honors the distinction. */
1522
- toggle() {
1523
- return this.ann({ control: "switch" });
1524
- }
1525
- };
1526
- var StringNode = class extends BaseNode {
1527
- kind = "string";
1528
- placeholder(placeholder) {
1529
- return this.ann({ placeholder });
1530
- }
1531
- maxLength(maxLength) {
1532
- return this.ann({ maxLength });
1533
- }
1534
- /** Milliseconds to debounce live writes. 0 commits every keystroke. */
1535
- debounce(debounceMs) {
1536
- return this.ann({ debounceMs });
1537
- }
1538
- };
1539
- var ColorNode = class extends BaseNode {
1540
- kind = "color";
1541
- };
1542
- var EnumNode = class extends BaseNode {
1543
- kind = "enum";
1544
- /** Render as a segmented control rather than a select. */
1545
- radio() {
1546
- return this.ann({ control: "radio" });
1547
- }
1548
- /** Relabel the choices. Named `labels` rather than `options` because
1549
- * `options` is the node's own extras bag. */
1550
- labels(options) {
1551
- return this.ann({ options });
1552
- }
1553
- };
1554
- var ValueNode = class extends BaseNode {
1555
- kind = null;
1556
- };
1557
- var CustomNode = class _CustomNode extends BaseNode {
1558
- constructor(kind, defaultValue, annotations = {}, options = {}) {
1559
- super(defaultValue, annotations, options);
1560
- this.kind = kind;
1561
- }
1562
- kind;
1563
- with(annotations, options) {
1564
- return new _CustomNode(this.kind, this.default, annotations, options);
1565
- }
1566
- };
1567
- var GroupNode = class _GroupNode {
1568
- constructor(children, annotations = {}, options = {}) {
1569
- this.children = children;
1570
- this.annotations = annotations;
1571
- this.options = options;
1572
- }
1573
- children;
1574
- annotations;
1575
- options;
1576
- with(annotations, options) {
1577
- return new _GroupNode(this.children, annotations, options);
1578
- }
1579
- /** Heading for the group's rows. Defaults to the key, title-cased. */
1580
- label(name) {
1581
- return this.with({ ...this.annotations, name }, this.options);
1582
- }
1583
- /** Longer help text. Carried on the resolved `PrefGroup`, where weasel-ui's
1584
- * `PrefsForm` draws it under the heading; `ControlPanel` has no place for
1585
- * it yet and shows the heading alone. */
1586
- describe(description) {
1587
- return this.with({ ...this.annotations, description }, this.options);
1588
- }
1589
- /** Render this whole group under a named section heading. */
1590
- section(label, opts = {}) {
1591
- return this.with(this.annotations, { ...this.options, section: { label, ...opts } });
1592
- }
1593
- /** Show this group only while the predicate holds. Presentational — every
1594
- * value beneath it stays in config and the instrument still reads it. */
1595
- showIf(predicate) {
1596
- return this.with(this.annotations, { ...this.options, showIf: predicate });
1597
- }
1598
- };
1599
- function shapeDefaults(shape) {
1600
- const out = {};
1601
- for (const [key, entry] of Object.entries(shape)) {
1602
- out[key] = isConfigBranch(entry) ? shapeDefaults(entry.children) : entry.default;
1603
- }
1604
- return out;
1605
- }
1606
- var expand = (options) => options.map((o) => typeof o === "string" ? { value: o, label: o } : o);
1607
- var f = {
1608
- number: (def) => new NumberNode(def),
1609
- boolean: (def) => new BooleanNode(def),
1610
- string: (def) => new StringNode(def),
1611
- color: (def) => new ColorNode(def),
1612
- /** A fixed set of choices. The default's literal type flows into the config
1613
- * type, so `f.enum('fast', ['fast', 'accurate'])` gives
1614
- * `'fast' | 'accurate'` with no `as const`. */
1615
- enum: (def, options) => new EnumNode(def, { options: expand(options) }),
1616
- /** A leaf with no declared kind: the rule chain decides, which is how a lab
1617
- * states a convention like "every `*Color` key is a color picker". */
1618
- value: (def) => new ValueNode(def),
1619
- /** A branch: its children nest under this key, so `grid: f.group({ size })`
1620
- * puts the value at `config.grid.size` and addresses it as `'grid.size'`.
1621
- * Groups nest as deeply as the schema wants. */
1622
- group: (children) => new GroupNode(children),
1623
- /** A leaf of a kind a lab supplies the control for, through `controls`. */
1624
- custom: (kind, def, validate) => new CustomNode(kind, def, {}, validate ? { validate } : {}),
1625
- /** Collect leaves and groups into an instrument's config. */
1626
- schema(nodes) {
1627
- return { nodes, defaults: () => shapeDefaults(nodes) };
1628
- }
1629
- };
1630
-
1631
- // src/config/rules.ts
1632
- function titleCase(key) {
1633
- const words = key.replace(/[_-]+/g, " ").split(/(?<=[a-z0-9])(?=[A-Z])/).join(" ").trim().toLowerCase();
1634
- return words.length === 0 ? words : words[0].toUpperCase() + words.slice(1);
1635
- }
1636
- var kindFromValue = (ctx) => {
1637
- const t = typeof ctx.default;
1638
- if (t === "boolean" || t === "number" || t === "string") return { kind: t };
1639
- return null;
1640
- };
1641
- var labelFromKey = (ctx) => ({ name: titleCase(ctx.key) });
1642
- var sliderWhenBounded = (ctx) => {
1643
- if (ctx.leaf.kind !== "number") return null;
1644
- const bounded = ctx.leaf.min !== void 0 && ctx.leaf.max !== void 0;
1645
- return bounded ? { control: "slider" } : null;
1646
- };
1647
- var descriptionDefault = () => ({ description: "" });
1648
- var builtinRules = [
1649
- kindFromValue,
1650
- labelFromKey,
1651
- sliderWhenBounded,
1652
- descriptionDefault
1653
- ];
1654
- function applyRules(seed, ctx, rules) {
1655
- const acc = { ...seed };
1656
- for (const rule of rules) {
1657
- const patch = rule({ ...ctx, leaf: acc });
1658
- if (patch === null) continue;
1659
- for (const [k, v] of Object.entries(patch)) {
1660
- if (v !== void 0 && acc[k] === void 0) {
1661
- acc[k] = v;
1662
- }
1663
- }
1664
- }
1665
- return acc;
1666
- }
1667
-
1668
- // src/config/resolve.ts
1669
- function defined(annotations) {
1670
- const out = {};
1671
- for (const [k, v] of Object.entries(annotations)) if (v !== void 0) out[k] = v;
1672
- return out;
1673
- }
1674
- function resolveConfigSchema(schema, rules = []) {
1675
- const sink = {
1676
- sections: [],
1677
- showIf: /* @__PURE__ */ new Map(),
1678
- renderers: {},
1679
- chain: [...rules, ...builtinRules]
1680
- };
1681
- const group = resolveShape(schema.nodes, "", "", sink);
1682
- return { group, sections: sink.sections, showIf: sink.showIf, renderers: sink.renderers };
1683
- }
1684
- function resolveShape(shape, at, name, sink) {
1685
- const children = {};
1686
- const sections = /* @__PURE__ */ new Map();
1687
- for (const [key, entry] of Object.entries(shape)) {
1688
- const path = at === "" ? key : `${at}.${key}`;
1689
- children[key] = resolveEntry(entry, key, path, sink);
1690
- const { section, showIf: predicate } = entry.options;
1691
- if (section !== void 0) {
1692
- let spec = sections.get(section.label);
1693
- if (!spec) {
1694
- spec = { at, label: section.label, paths: [] };
1695
- sections.set(section.label, spec);
1696
- sink.sections.push(spec);
1697
- }
1698
- spec.paths.push(path);
1699
- if (section.collapsed !== void 0) {
1700
- spec.collapsed = (spec.collapsed ?? false) || section.collapsed;
1701
- }
1702
- }
1703
- if (predicate) sink.showIf.set(path, predicate);
1704
- if (!isConfigBranch(entry) && entry.options.render) sink.renderers[path] = entry.options.render;
1705
- }
1706
- return { name, children };
1707
- }
1708
- function resolveEntry(entry, key, path, sink) {
1709
- if (isConfigBranch(entry)) {
1710
- const group = resolveShape(
1711
- entry.children,
1712
- path,
1713
- entry.annotations.name ?? titleCase(key),
1714
- sink
1715
- );
1716
- return entry.annotations.description === void 0 ? group : { ...group, description: entry.annotations.description };
1717
- }
1718
- const seed = defined({
1719
- ...entry.annotations,
1720
- ...entry.kind === null ? {} : { kind: entry.kind }
1721
- });
1722
- const patch = applyRules(seed, { key, path, default: entry.default }, sink.chain);
1723
- return { ...patch, default: entry.default };
1724
- }
1725
- var LabContext = createContext(null);
1726
- function useLabContext() {
1727
- const ctx = useContext(LabContext);
1728
- if (ctx === null) {
1729
- throw new Error("useLabContext must be used inside <Lab>");
1730
- }
1731
- return ctx;
1732
- }
1733
-
1734
- // src/config/useConfigSchema.ts
1735
- var NO_RULES = [];
1736
- function useConfigSchema(instrument) {
1737
- const lab = useContext(LabContext);
1738
- const rules = lab?.configRules ?? NO_RULES;
1739
- return useMemo(
1740
- () => instrument.config ? resolveConfigSchema(instrument.config, rules) : fromConfigFields(instrument.configSchema?.() ?? []),
1741
- [instrument, rules]
1742
- );
1743
- }
1744
-
1745
1507
  // src/instrument/defineInstrument.ts
1746
1508
  function defineInstrument(spec) {
1747
1509
  if (spec.config && !("defaultConfig" in spec && spec.defaultConfig)) {
@@ -1800,7 +1562,7 @@ function validateConfigSchema(fields) {
1800
1562
  return { valid: errors.length === 0, errors };
1801
1563
  }
1802
1564
 
1803
- // ../theme/dist/chunk-D6AFL2JW.js
1565
+ // ../theme/dist/chunk-VKZ4JIVX.js
1804
1566
  var THEME_SOURCES = {
1805
1567
  "weasel": {
1806
1568
  "primitives": {
@@ -1937,46 +1699,46 @@ var THEME_SOURCES = {
1937
1699
  "alpha": 0.18,
1938
1700
  "description": "Pressed wash over a raised surface."
1939
1701
  },
1940
- "swatch-green": {
1702
+ "swatch-fuchsia": {
1941
1703
  "type": "color",
1942
- "value": "#2fdd18",
1943
- "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."
1944
1706
  },
1945
- "swatch-pink": {
1707
+ "swatch-green": {
1946
1708
  "type": "color",
1947
- "value": "#ff5885"
1709
+ "value": "#48e628"
1948
1710
  },
1949
- "swatch-cyan": {
1711
+ "swatch-sky": {
1950
1712
  "type": "color",
1951
- "value": "#00dfff"
1713
+ "value": "#36bff2"
1952
1714
  },
1953
- "swatch-gold": {
1715
+ "swatch-amber": {
1954
1716
  "type": "color",
1955
- "value": "#dcb700"
1717
+ "value": "#e69628"
1956
1718
  },
1957
- "swatch-amber": {
1719
+ "swatch-teal": {
1958
1720
  "type": "color",
1959
- "value": "#ff8c00"
1721
+ "value": "#3ee1cb"
1960
1722
  },
1961
- "swatch-violet": {
1723
+ "swatch-red": {
1962
1724
  "type": "color",
1963
- "value": "#a497ff"
1725
+ "value": "#f75034"
1964
1726
  },
1965
- "swatch-mint": {
1727
+ "swatch-blue": {
1966
1728
  "type": "color",
1967
- "value": "#00e7af"
1729
+ "value": "#407ef7"
1968
1730
  },
1969
- "swatch-sky": {
1731
+ "swatch-citron": {
1970
1732
  "type": "color",
1971
- "value": "#00b8ff"
1733
+ "value": "#d5d23a"
1972
1734
  },
1973
- "swatch-orange": {
1735
+ "swatch-rose": {
1974
1736
  "type": "color",
1975
- "value": "#ff6b00"
1737
+ "value": "#fa4590"
1976
1738
  },
1977
- "swatch-magenta": {
1739
+ "swatch-violet": {
1978
1740
  "type": "color",
1979
- "value": "#ff6eff"
1741
+ "value": "#955cf9"
1980
1742
  },
1981
1743
  "radius-sm": {
1982
1744
  "type": "dimension",
@@ -2277,7 +2039,8 @@ var THEME_SOURCES = {
2277
2039
  },
2278
2040
  "border-strong": {
2279
2041
  "type": "color",
2280
- "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."
2281
2044
  },
2282
2045
  "accent-fg": {
2283
2046
  "type": "color",
@@ -2293,11 +2056,6 @@ var THEME_SOURCES = {
2293
2056
  "type": "color",
2294
2057
  "value": "rgba(0, 0, 0, 0.6)",
2295
2058
  "description": "Elevation. Always darker than any surface \u2014 never derived from fg, which is near-white on dark."
2296
- },
2297
- "border-raised": {
2298
- "type": "color",
2299
- "value": "{color.gray-400}",
2300
- "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."
2301
2059
  }
2302
2060
  },
2303
2061
  "light": {
@@ -2331,7 +2089,7 @@ var THEME_SOURCES = {
2331
2089
  },
2332
2090
  "border-strong": {
2333
2091
  "type": "color",
2334
- "value": "{color.gray-300}"
2092
+ "value": "{color.gray-500}"
2335
2093
  },
2336
2094
  "accent-fg": {
2337
2095
  "type": "color",
@@ -2346,10 +2104,6 @@ var THEME_SOURCES = {
2346
2104
  "shadow": {
2347
2105
  "type": "color",
2348
2106
  "value": "rgba(0, 0, 0, 0.18)"
2349
- },
2350
- "border-raised": {
2351
- "type": "color",
2352
- "value": "{color.gray-400}"
2353
2107
  }
2354
2108
  }
2355
2109
  }
@@ -2366,11 +2120,11 @@ function hexToRgba(hex, alpha) {
2366
2120
  const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
2367
2121
  if (!m) throw new Error(`Expected a hex color, got "${hex}"`);
2368
2122
  const body = m[1];
2369
- const full = body.length === 3 ? body.replace(/./g, (c2) => c2 + c2) : body;
2370
- const r2 = Number.parseInt(full.slice(0, 2), 16);
2371
- const g2 = Number.parseInt(full.slice(2, 4), 16);
2372
- const b = Number.parseInt(full.slice(4, 6), 16);
2373
- 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})`;
2374
2128
  }
2375
2129
  var REF = /^\{([^}]+)\}$/;
2376
2130
  function refTarget(value) {
@@ -2382,7 +2136,7 @@ function refTarget(value) {
2382
2136
  return dot === -1 ? path : path.slice(dot + 1);
2383
2137
  }
2384
2138
  function fontStack(value) {
2385
- 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(", ");
2386
2140
  }
2387
2141
  function serialize(type, value) {
2388
2142
  if (Array.isArray(value)) {
@@ -2507,25 +2261,7 @@ function ThemeProvider({
2507
2261
  function useThemeOptional() {
2508
2262
  return useContext(ThemeContext);
2509
2263
  }
2510
-
2511
- // src/instrument/serializers.ts
2512
- function configDefaultsOf(instruments) {
2513
- const out = {};
2514
- for (const instrument of instruments) out[instrument.name] = () => instrument.defaultConfig();
2515
- return out;
2516
- }
2517
- function serializersOf(instruments) {
2518
- const out = {};
2519
- for (const instrument of instruments) {
2520
- const { name, serialize: serialize2, deserialize } = instrument;
2521
- if (!serialize2 && !deserialize) continue;
2522
- out[name] = {
2523
- ...serialize2 ? { serialize: (state) => serialize2(state) } : {},
2524
- ...deserialize ? { deserialize: (data, config) => deserialize(data, config) } : {}
2525
- };
2526
- }
2527
- return out;
2528
- }
2264
+ var AnnotationPreloadContext = createContext(null);
2529
2265
 
2530
2266
  // ../theme/dist/index.js
2531
2267
  var ALPHA_EXT = "com.weasel.alpha";
@@ -2535,9 +2271,9 @@ function flattenTokens(doc) {
2535
2271
  for (const [groupName, group] of Object.entries(doc)) {
2536
2272
  if (groupName.startsWith("$")) continue;
2537
2273
  if (typeof group !== "object" || group === null) continue;
2538
- const g2 = group;
2539
- const groupType = g2.$type;
2540
- 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)) {
2541
2277
  if (leaf.startsWith("$")) continue;
2542
2278
  if (!isToken(node)) continue;
2543
2279
  const type = node.$type ?? groupType;
@@ -2610,7 +2346,7 @@ var interstellar_tokens_default = {
2610
2346
  "accent-hover": { $value: "#c6a3ee" },
2611
2347
  "focus-ring": { $value: "rgba(176, 138, 219, 0.45)" },
2612
2348
  shadow: { $value: "rgba(0, 0, 0, 0.7)" },
2613
- "border-raised": { $value: "rgba(255, 255, 255, 0.36)" }
2349
+ "border-strong": { $value: "rgba(255, 255, 255, 0.36)" }
2614
2350
  },
2615
2351
  gradient: {
2616
2352
  $type: "gradient",
@@ -2634,7 +2370,7 @@ var interstellar_tokens_default = {
2634
2370
  "accent-hover": { $value: "#c2854f" },
2635
2371
  "focus-ring": { $value: "rgba(168, 111, 60, 0.35)" },
2636
2372
  shadow: { $value: "rgba(80, 60, 40, 0.18)" },
2637
- "border-raised": { $value: "#9e8b71" }
2373
+ "border-strong": { $value: "#9e8b71" }
2638
2374
  }
2639
2375
  }
2640
2376
  }
@@ -2642,114 +2378,18 @@ var interstellar_tokens_default = {
2642
2378
 
2643
2379
  // src/theme/interstellar.ts
2644
2380
  var interstellarTheme = loadDTCG(interstellar_tokens_default);
2645
-
2646
- // src/state/view.ts
2647
- var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
2648
- function as2DView(view) {
2649
- if (typeof view !== "object" || view === null) return null;
2650
- const v = view;
2651
- if (typeof v.zoom !== "number") return null;
2652
- if (typeof v.pan !== "object" || v.pan === null) return null;
2653
- if (typeof v.pan.x !== "number" || typeof v.pan.y !== "number") return null;
2654
- return { zoom: v.zoom, pan: { x: v.pan.x, y: v.pan.y } };
2655
- }
2656
- var ZONE_ID = asNodeId("lk-trial-body");
2657
- var SIDEBAR_ID = asNodeId("lk-trial-sidebar");
2658
- var CONTENT_ID = asNodeId("lk-trial-content");
2659
- var STRATEGIES = { strip: stripStrategy };
2660
- function TrialBody({
2661
- sidebar,
2662
- children,
2663
- contentClassName,
2664
- width,
2665
- defaultWidth = 320,
2666
- minWidth = 140,
2667
- maxWidth = 720,
2668
- contentMinWidth = 160,
2669
- onWidthChange,
2670
- viewport
2671
- }) {
2672
- const storeRef = useRef(null);
2673
- if (storeRef.current === null) {
2674
- const store2 = new Store();
2675
- store2.registerNode(
2676
- createNode({
2677
- kind: "zone",
2678
- id: ZONE_ID,
2679
- container: { strategyId: "strip", config: { axis: "x", resizable: true } }
2680
- })
2681
- );
2682
- store2.registerNode(
2683
- createNode({
2684
- kind: "sidebar",
2685
- id: SIDEBAR_ID,
2686
- parentId: ZONE_ID,
2687
- meta: { title: "Sidebar" },
2688
- placement: { size: { w: width ?? defaultWidth } },
2689
- hints: { minSize: { w: minWidth, h: 0 }, maxSize: { w: maxWidth, h: 0 } }
2690
- })
2691
- );
2692
- store2.registerNode(
2693
- createNode({
2694
- kind: "content",
2695
- id: CONTENT_ID,
2696
- parentId: ZONE_ID,
2697
- meta: { title: "Content" },
2698
- hints: { minSize: { w: contentMinWidth, h: 0 } }
2699
- })
2700
- );
2701
- store2.showNode(SIDEBAR_ID);
2702
- store2.showNode(CONTENT_ID);
2703
- storeRef.current = store2;
2704
- }
2705
- const store = storeRef.current;
2706
- const widthRef = useRef(width ?? defaultWidth);
2707
- useEffect(() => {
2708
- store.setHints(SIDEBAR_ID, {
2709
- minSize: { w: minWidth, h: 0 },
2710
- maxSize: { w: maxWidth, h: 0 }
2711
- });
2712
- store.setHints(CONTENT_ID, { minSize: { w: contentMinWidth, h: 0 } });
2713
- }, [store, minWidth, maxWidth, contentMinWidth]);
2714
- useEffect(() => {
2715
- if (width === void 0 || width === widthRef.current) return;
2716
- widthRef.current = width;
2717
- store.patchPlacement(SIDEBAR_ID, { size: { w: width } });
2718
- }, [store, width]);
2719
- const onWidthChangeRef = useRef(onWidthChange);
2720
- onWidthChangeRef.current = onWidthChange;
2721
- useEffect(
2722
- () => store.events.on("node.placementChanged", () => {
2723
- const next = store.getPlacement(SIDEBAR_ID)?.size?.w;
2724
- if (typeof next !== "number" || next === widthRef.current) return;
2725
- widthRef.current = next;
2726
- onWidthChangeRef.current?.(next);
2727
- }),
2728
- [store]
2729
- );
2730
- const chrome = useMemo(
2731
- () => ({
2732
- sidebar: () => /* @__PURE__ */ jsx("div", { className: "lk-trial__sidebar", children: sidebar }),
2733
- content: () => /* @__PURE__ */ jsx(
2734
- "div",
2735
- {
2736
- className: contentClassName ? `lk-trial__content ${contentClassName}` : "lk-trial__content",
2737
- children
2738
- }
2739
- )
2740
- }),
2741
- [sidebar, children, contentClassName]
2742
- );
2743
- return /* @__PURE__ */ jsx(Provider, { store, children: /* @__PURE__ */ jsx(StrategyRegistryProvider, { strategies: STRATEGIES, children: /* @__PURE__ */ jsx(
2744
- Container,
2381
+ function TrialBody({ sidebar, children, contentClassName, ...rest }) {
2382
+ return /* @__PURE__ */ jsx(
2383
+ Split,
2745
2384
  {
2746
- parentId: ZONE_ID,
2747
- chrome,
2748
- className: "lk-trial__panes windease-zone",
2749
- affordances: true,
2750
- 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
2751
2391
  }
2752
- ) }) });
2392
+ );
2753
2393
  }
2754
2394
  var TrialDragContext = createContext(null);
2755
2395
  function useTrialDrag() {
@@ -2834,7 +2474,7 @@ function Portalled({
2834
2474
  }
2835
2475
  function UndockedSections({ trialId, sections, onDock }) {
2836
2476
  if (sections.length === 0) return null;
2837
- 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)) });
2838
2478
  }
2839
2479
  var NO_OP = () => {
2840
2480
  };
@@ -2867,17 +2507,17 @@ function TrialChrome({
2867
2507
  const dockPanelAction = useStore(storeCtx.store, (s) => s.dockPanel);
2868
2508
  const undockedPanels = useStore(storeCtx.store, (s) => s.undockedPanels);
2869
2509
  const undockedIds = useMemo(
2870
- () => 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),
2871
2511
  [undockedPanels, trialId]
2872
2512
  );
2873
2513
  const view2d = as2DView(record.view);
2874
2514
  const configSchema = useConfigSchema(instrument);
2875
- const title = record.title ?? record.instrumentName;
2515
+ const title = record.title ?? instrument.title ?? instrument.name;
2876
2516
  const collapsedSections = record.collapsedSections ?? NO_SECTIONS;
2877
2517
  const ctx = useMemo(() => {
2878
2518
  const setZoom = (z2) => {
2879
2519
  if (!view2d) return;
2880
- updateTrialView(trialId, { ...view2d, zoom: z2 });
2520
+ updateTrialView(trialId, withZoom(view2d, z2));
2881
2521
  };
2882
2522
  return {
2883
2523
  trialId,
@@ -2959,7 +2599,7 @@ function TrialChrome({
2959
2599
  ),
2960
2600
  [instrument, ctx, trialChrome, chrome, suppress, lab.controls]
2961
2601
  );
2962
- const inRegion = (region) => contributions.filter((c2) => c2.region === region);
2602
+ const inRegion = (region) => contributions.filter((c3) => c3.region === region);
2963
2603
  const handleKeyDown = (e) => {
2964
2604
  const mod = e.metaKey || e.ctrlKey;
2965
2605
  if (!mod) return;
@@ -2976,6 +2616,7 @@ function TrialChrome({
2976
2616
  "section",
2977
2617
  {
2978
2618
  className: "lk-trial",
2619
+ "data-trial-id": trialId,
2979
2620
  "aria-label": `Trial ${title}`,
2980
2621
  tabIndex: -1,
2981
2622
  onKeyDown: handleKeyDown,
@@ -2995,15 +2636,15 @@ function TrialChrome({
2995
2636
  TrialBody,
2996
2637
  {
2997
2638
  width: record.sidebarWidth,
2998
- onWidthChange: (w3) => updateTrialSidebarWidth(trialId, w3),
2999
- 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,
3000
2641
  sidebar: /* @__PURE__ */ jsxs(Fragment, { children: [
3001
2642
  /* @__PURE__ */ jsx(SidebarRegion, { contributions: inRegion("sidebar"), ctx }),
3002
2643
  /* @__PURE__ */ jsx(
3003
2644
  UndockedSections,
3004
2645
  {
3005
2646
  trialId,
3006
- sections: inRegion("sidebar").filter((c2) => undockedIds.includes(c2.id)),
2647
+ sections: inRegion("sidebar").filter((c3) => undockedIds.includes(c3.id)),
3007
2648
  onDock: ctx.dockPanel
3008
2649
  }
3009
2650
  )
@@ -3027,25 +2668,30 @@ function Trial({ id, chrome, suppress }) {
3027
2668
  const lab = useLabContext();
3028
2669
  const storeCtx = useContext(LabStoreContext);
3029
2670
  if (!storeCtx) throw new Error("[labkit] <Trial> requires <LabStoreProvider>");
3030
- 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);
3031
2675
  if (!record) {
3032
2676
  return /* @__PURE__ */ jsxs("div", { className: "lk-trial lk-trial--unknown", children: [
3033
2677
  "Trial not found: ",
3034
2678
  id
3035
2679
  ] });
3036
2680
  }
3037
- const instrument = lab.instruments.find((i) => i.name === record.instrumentName);
3038
2681
  if (!instrument) {
3039
2682
  return /* @__PURE__ */ jsxs("div", { className: "lk-trial lk-trial--unknown", children: [
3040
2683
  "Unknown instrument: ",
3041
2684
  record.instrumentName
3042
2685
  ] });
3043
2686
  }
2687
+ if (!kept.ready) return /* @__PURE__ */ jsx("div", { className: "lk-trial lk-trial--loading" });
3044
2688
  return /* @__PURE__ */ jsx(
3045
2689
  TrialRuntime,
3046
2690
  {
2691
+ keptMarks: kept.value,
3047
2692
  record,
3048
2693
  instrument,
2694
+ instruments,
3049
2695
  store: storeCtx.store,
3050
2696
  isLast: lab.trials.length <= 1,
3051
2697
  chrome,
@@ -3053,7 +2699,41 @@ function Trial({ id, chrome, suppress }) {
3053
2699
  }
3054
2700
  );
3055
2701
  }
3056
- 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);
3057
2737
  const canvasContainerRef = useRef(null);
3058
2738
  const loupeHostRef = useRef(null);
3059
2739
  const updateTrialState = useStore(store, (s) => s.updateTrialState);
@@ -3071,7 +2751,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3071
2751
  const [layerOrder, setLayerOrder] = useState(null);
3072
2752
  const [loupeOn, setLoupeOn] = useState(false);
3073
2753
  const visibleLayers = useMemo(
3074
- () => (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),
3075
2755
  [instrument.canvas, layerVisibility]
3076
2756
  );
3077
2757
  const undoCap = instrument.undo;
@@ -3079,14 +2759,15 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3079
2759
  const maxDepth = undoCap?.maxDepth ?? 50;
3080
2760
  const snapshotIfNeeded = (event) => {
3081
2761
  if (!undoCap || !undoEvents.has(event)) return;
3082
- const current = store.getState().trials.find((w3) => w3.id === record.id);
2762
+ const current = store.getState().trials.find((w2) => w2.id === record.id);
3083
2763
  if (!current) return;
3084
2764
  const snap = structuredClone(current.state);
3085
2765
  updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
3086
2766
  };
3087
2767
  const setView = (v) => updateTrialView(record.id, v);
2768
+ const stage = instrument.canvas ? void 0 : instrument.stage;
3088
2769
  const placeView = (size) => {
3089
- const declared = instrument.canvas?.initialView;
2770
+ const declared = instrument.canvas?.initialView ?? (stage ? stage.initialView ?? ((vp) => fitStage(stage.size, vp)) : void 0);
3090
2771
  if (typeof declared !== "function") return;
3091
2772
  if (record.view != null) return;
3092
2773
  setView(declared(size));
@@ -3097,12 +2778,14 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3097
2778
  const annotationsRef = useRef(null);
3098
2779
  const configRef = useRef(record.config);
3099
2780
  configRef.current = record.config;
2781
+ const capRef = useRef(annotationsCap);
2782
+ capRef.current = annotationsCap;
3100
2783
  if (annotationsRef.current === null) {
3101
- const kept = annotationsCap?.storage ? annotationsCap.storage.load() : record.annotations;
2784
+ const kept = annotationsCap?.storage ? keptMarks : record.annotations;
3102
2785
  annotationsRef.current = annotationsFromJSON(kept, () => targetsRef.current(), {
3103
- meaning: annotationsCap?.meaning,
2786
+ meaning: () => capRef.current?.meaning,
3104
2787
  config: () => configRef.current,
3105
- onCapture: (result) => annotationsCap?.onCapture?.(result)
2788
+ onCapture: (result) => capRef.current?.onCapture?.(result)
3106
2789
  });
3107
2790
  }
3108
2791
  const annotations = annotationsRef.current;
@@ -3146,8 +2829,9 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3146
2829
  flush();
3147
2830
  };
3148
2831
  }, [annotations, annotationsCap]);
3149
- const declaresTools = instrument.tools != null || instrument.annotations != null;
3150
- 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;
3151
2835
  const setActiveTool = (id) => {
3152
2836
  if (declaresTools) setTrialTool(record.id, id);
3153
2837
  else setLabTool(id);
@@ -3184,7 +2868,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3184
2868
  zoom: view2d?.zoom ?? 1,
3185
2869
  setZoom: (z2) => {
3186
2870
  if (!view2d) return;
3187
- updateTrialView(record.id, { ...view2d, zoom: z2 });
2871
+ updateTrialView(record.id, withZoom(view2d, z2));
3188
2872
  },
3189
2873
  activeToolId: resolvedToolId,
3190
2874
  visibleLayers
@@ -3228,7 +2912,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3228
2912
  const canvasLayers = useMemo(() => {
3229
2913
  if (!instrument.canvas) return [];
3230
2914
  const baseLayers = instrument.canvas.layers;
3231
- 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;
3232
2916
  return ordered.map((layer) => ({
3233
2917
  id: layer.id,
3234
2918
  visible: layerVisibility[layer.id] !== false,
@@ -3240,7 +2924,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3240
2924
  }, [instrument.canvas, record.state, record.config, layerVisibility, layerOrder]);
3241
2925
  const layerDescriptors = useMemo(() => {
3242
2926
  if (!instrument.layers) return [];
3243
- 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);
3244
2928
  }, [instrument.layers]);
3245
2929
  const dragDropResult = useDragDrop({
3246
2930
  capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
@@ -3260,8 +2944,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3260
2944
  });
3261
2945
  const paletteItems = useMemo(() => {
3262
2946
  if (!instrument.dragDrop) return [];
3263
- const p = instrument.dragDrop.palette;
3264
- 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;
3265
2949
  }, [instrument.dragDrop, record.state, record.config]);
3266
2950
  const layersWithFeedback = useMemo(() => {
3267
2951
  if (!dragDropResult.drag?.feedback) return canvasLayers;
@@ -3275,12 +2959,12 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3275
2959
  render: (ctx) => {
3276
2960
  const el = canvasContainerRef.current;
3277
2961
  if (!el) return;
3278
- const r2 = el.getBoundingClientRect();
2962
+ const r3 = el.getBoundingClientRect();
3279
2963
  ctx.save();
3280
2964
  ctx.strokeStyle = fb.ok ? "#3a7" : "#c44";
3281
2965
  ctx.lineWidth = 2;
3282
2966
  ctx.beginPath();
3283
- 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);
3284
2968
  ctx.stroke();
3285
2969
  ctx.restore();
3286
2970
  }
@@ -3320,6 +3004,21 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3320
3004
  ),
3321
3005
  /* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
3322
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
+ );
3323
3022
  } else if (lens) {
3324
3023
  body = /* @__PURE__ */ jsxs("div", { ref: loupeHostRef, className: "lk-trial__loupe-host", children: [
3325
3024
  instrument.render(renderCtx),
@@ -3340,7 +3039,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3340
3039
  layers: layerDescriptors,
3341
3040
  visibility: layerVisibility,
3342
3041
  onReorder: (next) => {
3343
- setLayerOrder(next.map((l) => l.id));
3042
+ setLayerOrder(next.map((l2) => l2.id));
3344
3043
  bus.emit("layers.reorder");
3345
3044
  },
3346
3045
  onToggle: (lid, visible) => {
@@ -3377,10 +3076,10 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3377
3076
  config: record.config,
3378
3077
  trial: { id: record.id, view: record.view },
3379
3078
  annotations,
3380
- activeToolId: resolvedToolId
3079
+ activeToolId: labTool
3381
3080
  }
3382
3081
  ) : null;
3383
- 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(
3384
3083
  TrialChrome,
3385
3084
  {
3386
3085
  job: jobCap ? job : void 0,
@@ -3400,7 +3099,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3400
3099
  annotationOverlays
3401
3100
  ]
3402
3101
  }
3403
- ) }) });
3102
+ ) }) }) });
3404
3103
  }
3405
3104
 
3406
3105
  // src/trial/trialOps.ts
@@ -3413,8 +3112,9 @@ function findInstrument(instruments, name) {
3413
3112
  return found;
3414
3113
  }
3415
3114
  function initialView(instrument) {
3416
- const declared = instrument.canvas?.initialView;
3417
- 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;
3418
3118
  return declared ? structuredClone(declared) : { ...DEFAULT_VIEW2, pan: { ...DEFAULT_VIEW2.pan } };
3419
3119
  }
3420
3120
  function seedConfig(defaults, seed) {
@@ -3436,7 +3136,7 @@ function addTrial(trials, instruments, instrumentName, options = {}) {
3436
3136
  return [...trials, record];
3437
3137
  }
3438
3138
  function cloneTrial(trials, id) {
3439
- const sourceIdx = trials.findIndex((w3) => w3.id === id);
3139
+ const sourceIdx = trials.findIndex((w2) => w2.id === id);
3440
3140
  const source = trials[sourceIdx];
3441
3141
  if (!source) return trials;
3442
3142
  const clone = {
@@ -3451,11 +3151,11 @@ function cloneTrial(trials, id) {
3451
3151
  }
3452
3152
  function closeTrial(trials, id) {
3453
3153
  if (trials.length <= 1) return trials;
3454
- const next = trials.filter((w3) => w3.id !== id);
3154
+ const next = trials.filter((w2) => w2.id !== id);
3455
3155
  return next.length === trials.length ? trials : next;
3456
3156
  }
3457
3157
  function resetTrial(trials, id, instruments) {
3458
- const idx = trials.findIndex((w3) => w3.id === id);
3158
+ const idx = trials.findIndex((w2) => w2.id === id);
3459
3159
  const current = trials[idx];
3460
3160
  if (!current) return trials;
3461
3161
  const instrument = findInstrument(instruments, current.instrumentName);
@@ -3470,12 +3170,92 @@ function resetTrial(trials, id, instruments) {
3470
3170
  return [...trials.slice(0, idx), reset, ...trials.slice(idx + 1)];
3471
3171
  }
3472
3172
  function reorderTrials(trials, ids) {
3473
- const byId = new Map(trials.map((w3) => [w3.id, w3]));
3474
- const named = ids.map((id) => byId.get(id)).filter((w3) => w3 !== void 0);
3475
- const seen = new Set(named.map((w3) => w3.id));
3476
- 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));
3477
3177
  return [...named, ...rest];
3478
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
+ }
3479
3259
  var MODES = [
3480
3260
  { value: "auto", label: "Auto", glyph: /* @__PURE__ */ jsx(E, { size: 14 }) },
3481
3261
  { value: "light", label: "Light", glyph: /* @__PURE__ */ jsx(w, { size: 14 }) },
@@ -3486,11 +3266,11 @@ var MODE_ITEMS = MODES.map(({ value, label, glyph }) => ({
3486
3266
  label: glyph,
3487
3267
  ariaLabel: label
3488
3268
  }));
3489
- function LabHeader() {
3269
+ function LabHeader({ addTrial: addTrial2 = true }) {
3490
3270
  const lab = useLabContext();
3491
3271
  const only = lab.instruments.length === 1 ? lab.instruments[0] : null;
3492
3272
  return /* @__PURE__ */ jsxs(Fragment, { children: [
3493
- only ? /* @__PURE__ */ jsxs(
3273
+ !addTrial2 ? null : only ? /* @__PURE__ */ jsxs(
3494
3274
  "button",
3495
3275
  {
3496
3276
  type: "button",
@@ -3501,22 +3281,14 @@ function LabHeader() {
3501
3281
  /* @__PURE__ */ jsx("span", { children: "Add trial" })
3502
3282
  ]
3503
3283
  }
3504
- ) : (
3505
- // Held at null so the control stays an "add one" action rather than
3506
- // reading as the current instrument.
3507
- /* @__PURE__ */ jsx(
3508
- g,
3509
- {
3510
- className: "lk-lab-header__add-select",
3511
- "aria-label": "Add trial",
3512
- placeholder: "Add trial\u2026",
3513
- selectedKey: null,
3514
- options: lab.instruments.map((i) => ({ value: i.name, label: i.name })),
3515
- onSelectionChange: (name) => {
3516
- if (name != null) lab.addTrial(String(name));
3517
- }
3518
- }
3519
- )
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
+ }
3520
3292
  ),
3521
3293
  /* @__PURE__ */ jsx(
3522
3294
  r6,
@@ -3534,26 +3306,14 @@ function LabHeader() {
3534
3306
  )
3535
3307
  ] });
3536
3308
  }
3537
- function LabPalette({ tools }) {
3538
- const storeCtx = useContext(LabStoreContext);
3539
- if (!storeCtx) throw new Error("[labkit] LabPalette requires <LabStoreProvider>");
3540
- const activeToolId = useStore(storeCtx.store, (s) => s.activeToolId);
3541
- const setLabTool = useStore(storeCtx.store, (s) => s.setLabTool);
3542
- const contributions = tools.map((t) => ({
3543
- id: t.id,
3544
- region: "palette",
3545
- group: t.group,
3546
- item: { icon: t.icon, label: t.label, shortcut: t.shortcut }
3547
- }));
3548
- const ctx = {
3549
- activeToolId,
3550
- setActiveTool: setLabTool
3551
- };
3552
- 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 });
3553
3313
  }
3554
3314
  function currentPage(path, pages) {
3555
3315
  const here = path.split("?")[0].split("#")[0].replace(/\/$/, "").replace(/\.html$/, "");
3556
- return pages.findIndex((p) => here.endsWith(p.href.replace(/\.html$/, "")));
3316
+ return pages.findIndex((p2) => here.endsWith(p2.href.replace(/\.html$/, "")));
3557
3317
  }
3558
3318
  function LabSwitcher({ title, pages, path, className }) {
3559
3319
  const [open, setOpen] = useState(false);
@@ -3663,17 +3423,17 @@ function LabShell({
3663
3423
  );
3664
3424
  return outer ? shell : /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolved, children: shell });
3665
3425
  }
3666
- var ZONE_ID2 = asNodeId("lk-workspace");
3667
- var STRATEGIES2 = { grid: gridStrategy, floating: floatingStrategy };
3426
+ var ZONE_ID = asNodeId("lk-workspace");
3427
+ var STRATEGIES = { grid: gridStrategy, floating: floatingStrategy };
3668
3428
  var FLOAT_ZONE_ID = asNodeId("lk-workspace-floating");
3669
3429
  var PANEL_KIND = "panel";
3670
3430
  var KIND = "trial";
3671
3431
  function extentOf(store, id) {
3672
- const p = store.getNode(id)?.membership?.placement;
3673
- if (!p) return null;
3432
+ const p2 = store.getNode(id)?.membership?.placement;
3433
+ if (!p2) return null;
3674
3434
  const out = {};
3675
- if (p.size) out.size = p.size;
3676
- if (p.span) out.span = p.span;
3435
+ if (p2.size) out.size = p2.size;
3436
+ if (p2.span) out.span = p2.span;
3677
3437
  return Object.keys(out).length > 0 ? out : null;
3678
3438
  }
3679
3439
  function PanelFrame({ panel, floating = false }) {
@@ -3694,11 +3454,11 @@ function Workspace({
3694
3454
  viewport
3695
3455
  }) {
3696
3456
  const items = Children.toArray(children);
3697
- const tilePanels = useMemo(() => (panels ?? []).filter((p) => p.as === "tile"), [panels]);
3698
- 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]);
3699
3459
  const wantedIds = [
3700
3460
  ...items.map((_, i) => ids?.[i] ?? `lk-ws-${i}`),
3701
- ...tilePanels.map((p) => `lk-panel-${p.key}`)
3461
+ ...tilePanels.map((p2) => `lk-panel-${p2.key}`)
3702
3462
  ];
3703
3463
  const idKey = wantedIds.join(",");
3704
3464
  const idKeyRef = useRef(null);
@@ -3718,7 +3478,7 @@ function Workspace({
3718
3478
  store2.registerNode(
3719
3479
  createNode({
3720
3480
  kind: "zone",
3721
- id: ZONE_ID2,
3481
+ id: ZONE_ID,
3722
3482
  container: { strategyId: "grid", config: { resizable, gap, padding } }
3723
3483
  })
3724
3484
  );
@@ -3734,10 +3494,10 @@ function Workspace({
3734
3494
  const store = storeRef.current;
3735
3495
  const surface = useSurfaceOptional();
3736
3496
  useLayoutEffect(() => {
3737
- store.updateContainerConfig(ZONE_ID2, { resizable, gap, padding });
3497
+ store.updateContainerConfig(ZONE_ID, { resizable, gap, padding });
3738
3498
  }, [store, resizable, gap, padding]);
3739
3499
  useLayoutEffect(() => {
3740
- const present = new Set(store.getContainerView(ZONE_ID2)?.childOrder ?? []);
3500
+ const present = new Set(store.getContainerView(ZONE_ID)?.childOrder ?? []);
3741
3501
  const wanted = new Set(nodeIds);
3742
3502
  for (const id of present) {
3743
3503
  if (!wanted.has(id)) store.unregisterNode(id);
@@ -3745,12 +3505,12 @@ function Workspace({
3745
3505
  for (const id of nodeIds) {
3746
3506
  if (present.has(id)) continue;
3747
3507
  const kind = String(id).startsWith("lk-panel-") ? PANEL_KIND : KIND;
3748
- store.registerNode(createNode({ kind, id, parentId: ZONE_ID2, focus: true }));
3508
+ store.registerNode(createNode({ kind, id, parentId: ZONE_ID, focus: true }));
3749
3509
  store.showNode(id);
3750
3510
  const saved = layoutRef.current?.[id];
3751
3511
  if (saved) store.patchPlacement(id, saved);
3752
3512
  }
3753
- store.setChildOrder(ZONE_ID2, [...nodeIds]);
3513
+ store.setChildOrder(ZONE_ID, [...nodeIds]);
3754
3514
  }, [store, nodeIds]);
3755
3515
  useEffect(() => {
3756
3516
  if (!surface) return;
@@ -3761,7 +3521,7 @@ function Workspace({
3761
3521
  if (!onLayoutChange) return;
3762
3522
  return store.events.on("node.placementChanged", () => {
3763
3523
  const next = {};
3764
- for (const child of store.getChildren(ZONE_ID2)) {
3524
+ for (const child of store.getChildren(ZONE_ID)) {
3765
3525
  const extent = extentOf(store, child.id);
3766
3526
  if (extent) next[child.id] = extent;
3767
3527
  }
@@ -3775,7 +3535,7 @@ function Workspace({
3775
3535
  const chrome = useMemo(() => {
3776
3536
  const byId = new Map(nodeIds.map((id, i) => [id, items[i]]));
3777
3537
  const panelByNode = new Map(
3778
- tilePanels.map((p) => [`lk-panel-${p.key}`, p])
3538
+ tilePanels.map((p2) => [`lk-panel-${p2.key}`, p2])
3779
3539
  );
3780
3540
  return {
3781
3541
  [PANEL_KIND]: ({ node }) => {
@@ -3793,11 +3553,11 @@ function Workspace({
3793
3553
  }
3794
3554
  };
3795
3555
  }, [nodeIds, items, reorderable, tilePanels]);
3796
- 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)) });
3797
3557
  const grid = /* @__PURE__ */ jsx(
3798
3558
  Container,
3799
3559
  {
3800
- parentId: ZONE_ID2,
3560
+ parentId: ZONE_ID,
3801
3561
  chrome,
3802
3562
  className: "lk-workspace windease-zone",
3803
3563
  affordances: resizable,
@@ -3805,7 +3565,7 @@ function Workspace({
3805
3565
  onChildOrderChange: reorderable ? commitOrder : void 0
3806
3566
  }
3807
3567
  );
3808
- 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 ? (
3809
3569
  // The floating layer insets against the workspace, so it needs a
3810
3570
  // positioned box around both. Without one it insets against the page
3811
3571
  // and lands over the lab header.
@@ -3815,23 +3575,59 @@ function Workspace({
3815
3575
  ] })
3816
3576
  ) : reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid }) });
3817
3577
  }
3818
- function buildStore(instruments, defaultInstrument, storage, storageKey, initialMode) {
3819
- 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({
3820
3593
  storageKey,
3821
- storage,
3822
- initialMode,
3823
- serializers: serializersOf(instruments),
3824
- configDefaults: configDefaultsOf(instruments)
3594
+ storage: storage ?? await defaultStorage(),
3595
+ initialMode: mode ?? "auto",
3596
+ instruments
3825
3597
  });
3826
- if (store.getState().trials.length === 0) {
3827
- const seeded = addTrial([], instruments, defaultInstrument);
3828
- const record = seeded[0];
3829
- if (record) {
3830
- const { undoStack: _undoStack, ...rest } = record;
3831
- 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
3832
3629
  }
3833
- }
3834
- return store;
3630
+ ) });
3835
3631
  }
3836
3632
  var NEBULA_SLOTS = [
3837
3633
  { cx: "18%", cy: "30%", sx: "60%", sy: "80%", stop: "70%" },
@@ -3841,43 +3637,77 @@ var NEBULA_SLOTS = [
3841
3637
  { cx: "85%", cy: "18%", sx: "55%", sy: "80%", stop: "70%" }
3842
3638
  ];
3843
3639
  function buildNebula(colors) {
3844
- const blobs = colors.map((c2, i) => {
3845
- const p = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
3846
- 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})`;
3847
3643
  });
3848
3644
  blobs.push("radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%)");
3849
3645
  return blobs.join(", ");
3850
3646
  }
3851
- 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({
3852
3690
  instruments,
3853
- defaultInstrument,
3854
- storage,
3855
- storageKey,
3856
3691
  mode,
3857
3692
  nebula,
3858
3693
  title,
3694
+ pages,
3695
+ path,
3859
3696
  footer,
3860
3697
  chrome,
3698
+ labChrome,
3861
3699
  suppress,
3700
+ addTrial: addTrial2,
3862
3701
  tools,
3863
3702
  configRules,
3864
3703
  controls,
3865
- children
3704
+ children,
3705
+ opened
3866
3706
  }) {
3867
- if (process.env.NODE_ENV !== "production" && instruments.length === 0) {
3868
- throw new Error("[labkit] <Lab> requires a non-empty `instruments` array");
3869
- }
3870
- const storeRef = useRef(null);
3871
- if (storeRef.current === null) {
3872
- storeRef.current = buildStore(
3873
- instruments,
3874
- defaultInstrument,
3875
- storage ?? noneAdapter,
3876
- storageKey ?? "labkit",
3877
- mode ?? "auto"
3878
- );
3879
- }
3880
- const store = storeRef.current;
3707
+ const { store } = opened;
3708
+ useLayoutEffect(() => {
3709
+ store.getState().setInstruments(instruments);
3710
+ }, [instruments, store]);
3881
3711
  const trials = useStore(store, (s) => s.trials);
3882
3712
  const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
3883
3713
  const modeValue = useStore(store, (s) => s.mode);
@@ -3886,28 +3716,48 @@ function Lab({
3886
3716
  const panelHostsRef = useRef(null);
3887
3717
  if (panelHostsRef.current === null) panelHostsRef.current = createPanelHostRegistry();
3888
3718
  const outerSurface = useSurfaceOptional();
3889
- const outerCanvas = useSurfaceCanvas();
3890
- const [ownCanvas, setOwnCanvas] = useState(null);
3891
- 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);
3892
3725
  const bufferRef = useRef({ w: 0, h: 0 });
3893
3726
  const surfaceRef = useRef(null);
3894
3727
  const onFrame = useCallback((frame) => {
3895
- const c2 = ownCanvasRef.current;
3896
- if (!c2) return;
3897
- const w3 = Math.round(frame.size.width * frame.dpr);
3898
- const h = Math.round(frame.size.height * frame.dpr);
3899
- if (bufferRef.current.w === w3 && bufferRef.current.h === h) return;
3900
- bufferRef.current = { w: w3, h };
3901
- c2.width = w3;
3902
- c2.height = h;
3903
- c2.style.width = `${frame.size.width}px`;
3904
- c2.style.height = `${frame.size.height}px`;
3905
- 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
+ }
3906
3743
  }, []);
3907
3744
  const ownSurface = useTiledSurface({ onFrame });
3908
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);
3909
3756
  const surface = outerSurface ?? ownSurface;
3910
- 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
+ );
3911
3761
  const workspacePanels = useMemo(
3912
3762
  () => Object.entries(undockedPanels).map(([key, panel]) => ({
3913
3763
  key,
@@ -3917,6 +3767,13 @@ function Lab({
3917
3767
  [undockedPanels]
3918
3768
  );
3919
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");
3920
3777
  useEffect(() => {
3921
3778
  if (mode && mode !== store.getState().mode) {
3922
3779
  store.getState().setMode(mode);
@@ -3924,10 +3781,9 @@ function Lab({
3924
3781
  }, [mode, store]);
3925
3782
  const contextValue = useMemo(() => {
3926
3783
  const replaceTrials = (next) => {
3927
- const currentById = new Map(store.getState().trials.map((w3) => [w3.id, w3]));
3928
- 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);
3929
3786
  store.setState({ trials: merged });
3930
- store.getState().setMode(store.getState().mode);
3931
3787
  };
3932
3788
  return {
3933
3789
  instruments,
@@ -3949,14 +3805,13 @@ function Lab({
3949
3805
  },
3950
3806
  resetTrial: (id) => {
3951
3807
  const next = resetTrial(store.getState().trials, id, instruments);
3952
- const record = next.find((w3) => w3.id === id);
3808
+ const record = next.find((w2) => w2.id === id);
3953
3809
  if (!record) return;
3954
3810
  store.setState((s) => ({
3955
3811
  trials: s.trials.map(
3956
- (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
3957
3813
  )
3958
3814
  }));
3959
- store.getState().updateTrialView(id, record.view);
3960
3815
  },
3961
3816
  savedSnapshots,
3962
3817
  saveSnapshot: (trialId, name) => {
@@ -3978,7 +3833,20 @@ function Lab({
3978
3833
  };
3979
3834
  }, [instruments, trials, savedSnapshots, modeValue, store, configRules, controls]);
3980
3835
  const backdropStyle = resolvedMode === "dark" && nebula && nebula.length > 0 ? { ["--wzl-backdrop"]: buildNebula(nebula) } : void 0;
3981
- 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(
3982
3850
  ThemeProvider,
3983
3851
  {
3984
3852
  theme: interstellarTheme,
@@ -3990,55 +3858,61 @@ function Lab({
3990
3858
  {
3991
3859
  title: title ?? "Labkit",
3992
3860
  mode: modeValue,
3993
- 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,
3994
3867
  header: /* @__PURE__ */ jsxs(Fragment, { children: [
3995
- /* @__PURE__ */ jsx(LabHeader, {}),
3996
- children
3868
+ /* @__PURE__ */ jsx(LabHeader, { ...addTrial2 !== void 0 ? { addTrial: addTrial2 } : {} }),
3869
+ children,
3870
+ /* @__PURE__ */ jsx(LabHeaderRegion, { contributions: labChromeAll })
3997
3871
  ] }),
3998
- 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(
3999
- "div",
4000
- {
4001
- className: "lk-lab__body",
4002
- ref: outerSurface ? void 0 : ownSurface.containerRef,
4003
- children: [
4004
- outerSurface ? null : (
4005
- // Above the trials and inert: the marks a tile paints have
4006
- // to sit over the instrument's own DOM, and nothing on this
4007
- // buffer takes input — each tile has its own input box.
4008
- /* @__PURE__ */ jsx(
4009
- "canvas",
4010
- {
4011
- className: "lk-lab__surface",
4012
- ref: (el) => {
4013
- ownCanvasRef.current = el;
4014
- setOwnCanvas(el);
4015
- }
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);
4016
3887
  }
4017
- )
3888
+ }
4018
3889
  ),
4019
- tools ? /* @__PURE__ */ jsx(LabPalette, { tools }) : null,
4020
3890
  /* @__PURE__ */ jsx(
4021
- Workspace,
3891
+ "canvas",
4022
3892
  {
4023
- panels: workspacePanels,
4024
- ids: trials.map((w3) => w3.id),
4025
- resizable: true,
4026
- reorderable: true,
4027
- onReorder: (ids) => contextValue.reorderTrials(ids),
4028
- layout,
4029
- onLayoutChange: (next) => store.getState().setLayout(next),
4030
- 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
+ }
4031
3898
  }
4032
3899
  )
4033
- ]
4034
- }
4035
- ) }) }) })
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
+ ] }) }) }) })
4036
3910
  }
4037
3911
  )
4038
3912
  }
4039
- ) }) });
3913
+ ) }) }) }) });
4040
3914
  }
4041
3915
 
4042
- export { AnnotationOverlay, AnnotationTargets, DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell, LabSwitcher, Trial, TrialBody, TrialChrome, Workspace, addTrial, annotationsFromJSON, applyRules, as2DView, builtinRules, capturePlan, captureTarget, cloneTrial, closeTrial, composeCaptureSvg, createAnnotationScene, createAnnotationStore, createMarkDrawOne, currentPage, defineInstrument, f, fitView, fracContains, fracEncloses, 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 };
4043
3917
  //# sourceMappingURL=index.js.map
4044
3918
  //# sourceMappingURL=index.js.map