@weasel-js/labkit 1.4.4 → 1.5.1

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 (278) hide show
  1. package/README.md +54 -6
  2. package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CJNMS6mz.d.ts} +1 -1
  3. package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
  4. package/dist/_dts/Select.d-Gwr8fLpK.d.ts +453 -0
  5. package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-B4okC2lX.d.ts} +291 -31
  6. package/dist/_dts/{index-Blv9uQzu.d.ts → index-BNdz-tNd.d.ts} +105 -17
  7. package/dist/_dts/index-CSJoj4LC.d.ts +248 -0
  8. package/dist/_dts/{types-B9_zrHmb.d.ts → types-DzoNY1pO.d.ts} +91 -4
  9. package/dist/_dts/usePanZoom-Pzf9f2jh.d.ts +160 -0
  10. package/dist/_dts/useTrialState-uLlmaccn.d.ts +212 -0
  11. package/dist/canvas/index.d.ts +18 -129
  12. package/dist/canvas/index.js +4 -2
  13. package/dist/chrome/index.d.ts +5 -93
  14. package/dist/chrome/index.js +10 -6
  15. package/dist/chunk-7PFH3CNM.js +150 -0
  16. package/dist/chunk-7PFH3CNM.js.map +1 -0
  17. package/dist/{chunk-I6JCVE24.js → chunk-CEIUBPPW.js} +143 -17
  18. package/dist/chunk-CEIUBPPW.js.map +1 -0
  19. package/dist/chunk-EH7UEQV7.js +590 -0
  20. package/dist/chunk-EH7UEQV7.js.map +1 -0
  21. package/dist/{chunk-MOM3GOVY.js → chunk-EHGVROVV.js} +125 -27
  22. package/dist/chunk-EHGVROVV.js.map +1 -0
  23. package/dist/{chunk-TJ7QY3OC.js → chunk-ERFMC6WU.js} +160 -78
  24. package/dist/chunk-ERFMC6WU.js.map +1 -0
  25. package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
  26. package/dist/chunk-H6ZAOWNE.js.map +1 -0
  27. package/dist/{chunk-64ZCN3DA.js → chunk-IHUVQ3TX.js} +250 -160
  28. package/dist/chunk-IHUVQ3TX.js.map +1 -0
  29. package/dist/{chunk-FQMJUVHJ.js → chunk-IJJUK4FU.js} +53 -46
  30. package/dist/chunk-IJJUK4FU.js.map +1 -0
  31. package/dist/chunk-IXTQI7JX.js +737 -0
  32. package/dist/chunk-IXTQI7JX.js.map +1 -0
  33. package/dist/{chunk-BKFVHKJH.js → chunk-MDVTK5U3.js} +4 -4
  34. package/dist/{chunk-BKFVHKJH.js.map → chunk-MDVTK5U3.js.map} +1 -1
  35. package/dist/chunk-PHK4W562.js +72 -0
  36. package/dist/chunk-PHK4W562.js.map +1 -0
  37. package/dist/chunk-PJ7WEQBJ.js +18 -0
  38. package/dist/chunk-PJ7WEQBJ.js.map +1 -0
  39. package/dist/chunk-RWNDDKOH.js +682 -0
  40. package/dist/chunk-RWNDDKOH.js.map +1 -0
  41. package/dist/chunk-T3EC2DXD.js +12628 -0
  42. package/dist/chunk-T3EC2DXD.js.map +1 -0
  43. package/dist/{chunk-SMHP6XZ4.js → chunk-UN4U4P63.js} +10 -8
  44. package/dist/chunk-UN4U4P63.js.map +1 -0
  45. package/dist/chunk-WIG6XHJ7.js +35 -0
  46. package/dist/chunk-WIG6XHJ7.js.map +1 -0
  47. package/dist/chunk-Z76KEZAX.js +15 -0
  48. package/dist/chunk-Z76KEZAX.js.map +1 -0
  49. package/dist/chunk-ZDHY47EV.js +363 -0
  50. package/dist/chunk-ZDHY47EV.js.map +1 -0
  51. package/dist/config/index.d.ts +276 -0
  52. package/dist/config/index.js +8 -0
  53. package/dist/config/index.js.map +1 -0
  54. package/dist/controls/index.d.ts +3 -4
  55. package/dist/controls/index.js +4 -3
  56. package/dist/dragdrop/index.d.ts +3 -5
  57. package/dist/index.d.ts +266 -413
  58. package/dist/index.js +1775 -2251
  59. package/dist/index.js.map +1 -1
  60. package/dist/layers/index.d.ts +4 -6
  61. package/dist/layers/index.js +2 -2
  62. package/dist/loupe/index.d.ts +69 -20
  63. package/dist/loupe/index.js +2 -2
  64. package/dist/passthrough/weasel-ui.d.ts +175 -309
  65. package/dist/passthrough/weasel-ui.js +1 -1
  66. package/dist/primitives/index.d.ts +55 -6
  67. package/dist/primitives/index.js +6 -4
  68. package/dist/state/index.d.ts +13 -8
  69. package/dist/state/index.js +18 -13
  70. package/dist/state/index.js.map +1 -1
  71. package/dist/styles.css +223 -76
  72. package/dist/surface/index.d.ts +44 -7
  73. package/dist/surface/index.js +2 -1
  74. package/dist/ui/layers/index.js +1 -1
  75. package/dist/undo/index.d.ts +3 -5
  76. package/package.json +15 -8
  77. package/src/annotations/AnnotationOverlay.tsx +56 -5
  78. package/src/annotations/Annotations.less +4 -0
  79. package/src/annotations/Annotations.overlay.test.tsx +105 -6
  80. package/src/annotations/drawOne.test.ts +1 -1
  81. package/src/annotations/drawOne.ts +2 -2
  82. package/src/annotations/paint.test.ts +2 -2
  83. package/src/annotations/paint.ts +1 -1
  84. package/src/annotations/preload.ts +6 -0
  85. package/src/annotations/store.test.ts +18 -0
  86. package/src/annotations/store.ts +5 -4
  87. package/src/annotations/svgNodes.test.ts +2 -2
  88. package/src/annotations/svgNodes.ts +1 -0
  89. package/src/annotations/toolMap.test.ts +9 -5
  90. package/src/annotations/toolMap.ts +6 -3
  91. package/src/annotations/types.ts +6 -3
  92. package/src/canvas/CameraWheelContext.ts +12 -0
  93. package/src/canvas/CanvasStack.stories.tsx +1 -1
  94. package/src/canvas/CanvasStack.test.tsx +25 -0
  95. package/src/canvas/CanvasStack.tsx +31 -4
  96. package/src/canvas/Stage.less +18 -0
  97. package/src/canvas/Stage.test.tsx +186 -0
  98. package/src/canvas/Stage.tsx +134 -0
  99. package/src/canvas/camera.test.ts +17 -2
  100. package/src/canvas/camera.ts +9 -9
  101. package/src/canvas/index.ts +4 -0
  102. package/src/canvas/usePanZoom.test.ts +28 -0
  103. package/src/canvas/usePanZoom.ts +21 -9
  104. package/src/chrome/ChromeRegions.stories.tsx +1 -4
  105. package/src/chrome/LabChrome.tsx +130 -0
  106. package/src/chrome/builtins.test.ts +1 -0
  107. package/src/chrome/builtins.tsx +21 -31
  108. package/src/chrome/index.ts +16 -0
  109. package/src/chrome/labRegions.test.tsx +208 -0
  110. package/src/chrome/labTypes.ts +50 -0
  111. package/src/chrome/merge.ts +7 -9
  112. package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
  113. package/src/chrome/regions/PaletteRegion.tsx +16 -9
  114. package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
  115. package/src/chrome/regions/SidebarRegion.tsx +25 -13
  116. package/src/chrome/regions/StatusRegion.tsx +15 -8
  117. package/src/chrome/regions/ToolbarRegion.tsx +34 -16
  118. package/src/chrome/types.ts +50 -20
  119. package/src/config/auto.test.ts +12 -0
  120. package/src/config/auto.ts +15 -0
  121. package/src/config/autoConfig.test.ts +107 -0
  122. package/src/config/autoConfig.ts +153 -0
  123. package/src/config/builder.test.ts +42 -0
  124. package/src/config/builder.ts +68 -17
  125. package/src/config/declarationEmit.fixture.ts +11 -0
  126. package/src/config/declarationEmit.test.ts +51 -0
  127. package/src/config/entry.test.ts +46 -0
  128. package/src/config/index.ts +16 -1
  129. package/src/config/nodeClasses.test.ts +18 -0
  130. package/src/config/path.ts +7 -2
  131. package/src/config/resolve.test.ts +25 -0
  132. package/src/config/resolve.ts +17 -1
  133. package/src/config/types.ts +34 -2
  134. package/src/config/useResolvedConfig.ts +26 -0
  135. package/src/controls/AutoControls.stories.tsx +88 -0
  136. package/src/controls/ControlPanel.stories.tsx +42 -1
  137. package/src/controls/ControlPanel.test.tsx +530 -4
  138. package/src/controls/ControlPanel.tsx +562 -82
  139. package/src/fake-indexeddb-auto.d.ts +5 -0
  140. package/src/index.test.ts +7 -0
  141. package/src/index.ts +66 -5
  142. package/src/instrument/serializers.test.ts +22 -0
  143. package/src/instrument/serializers.ts +40 -1
  144. package/src/instrument/types.ts +25 -1
  145. package/src/lab/Lab.chrome.test.tsx +16 -0
  146. package/src/lab/Lab.less +4 -13
  147. package/src/lab/Lab.persist.test.tsx +147 -0
  148. package/src/lab/Lab.stories.tsx +1 -3
  149. package/src/lab/Lab.surface.test.tsx +71 -3
  150. package/src/lab/Lab.test.tsx +168 -16
  151. package/src/lab/Lab.tsx +434 -129
  152. package/src/lab/LabContext.ts +8 -0
  153. package/src/lab/LabFit.stories.less +18 -0
  154. package/src/lab/LabFit.stories.tsx +219 -0
  155. package/src/lab/LabFullChrome.stories.tsx +1 -4
  156. package/src/lab/LabHeader.test.tsx +40 -9
  157. package/src/lab/LabHeader.tsx +7 -14
  158. package/src/lab/LabPalette.tsx +13 -26
  159. package/src/lab/LabRoot.test.tsx +56 -0
  160. package/src/lab/LabRoot.tsx +47 -0
  161. package/src/lab/LabShell.less +73 -15
  162. package/src/lab/LabShell.stories.tsx +50 -1
  163. package/src/lab/LabShell.tsx +18 -27
  164. package/src/lab/LabSwitcher.less +18 -11
  165. package/src/lab/LabSwitcher.test.tsx +14 -9
  166. package/src/lab/LabSwitcher.tsx +8 -4
  167. package/src/lab/Workspace.less +1 -1
  168. package/src/lab/Workspace.stories.tsx +1 -1
  169. package/src/lab/Workspace.surface.test.tsx +1 -0
  170. package/src/lab/fitCheck.test.ts +57 -0
  171. package/src/lab/fitCheck.ts +125 -0
  172. package/src/lab/index.ts +2 -0
  173. package/src/layers/LayerList.stories.tsx +1 -1
  174. package/src/loupe/AGENTS.md +17 -5
  175. package/src/loupe/Loupe.less +1 -1
  176. package/src/loupe/LoupeGestures.tsx +67 -0
  177. package/src/loupe/TrialLoupe.tsx +43 -28
  178. package/src/loupe/canvasLens.ts +1 -1
  179. package/src/loupe/index.ts +4 -0
  180. package/src/loupe/loupeActions.ts +98 -0
  181. package/src/loupe/types.ts +1 -1
  182. package/src/loupe/useLoupe.test.tsx +60 -3
  183. package/src/loupe/useLoupe.ts +29 -49
  184. package/src/passthrough/weasel-ui.ts +9 -0
  185. package/src/primitives/FloatingPanel.stories.tsx +14 -2
  186. package/src/primitives/FloatingPanel.test.tsx +47 -16
  187. package/src/primitives/FloatingPanel.tsx +11 -25
  188. package/src/primitives/FpsMeter.stories.tsx +1 -1
  189. package/src/primitives/JobProgress.stories.tsx +1 -1
  190. package/src/primitives/Legend.stories.tsx +1 -1
  191. package/src/primitives/ScaleIndicator.stories.tsx +1 -1
  192. package/src/primitives/Sidebar.less +7 -0
  193. package/src/primitives/Sidebar.stories.tsx +1 -1
  194. package/src/primitives/Split.less +14 -0
  195. package/src/primitives/Split.test.tsx +133 -0
  196. package/src/primitives/Split.tsx +175 -0
  197. package/src/primitives/StatusBar.stories.tsx +1 -1
  198. package/src/primitives/Toolbar.stories.tsx +1 -1
  199. package/src/primitives/ZoomControl.tsx +5 -2
  200. package/src/primitives/index.ts +2 -0
  201. package/src/specimen/Specimen.less +95 -0
  202. package/src/specimen/Specimen.stories.tsx +13 -0
  203. package/src/specimen/Specimen.test.tsx +13 -0
  204. package/src/specimen/Specimen.tsx +888 -0
  205. package/src/state/Persistence.stories.tsx +75 -0
  206. package/src/state/Persistence.tsx +33 -0
  207. package/src/state/SingletonExperiment.test.tsx +47 -53
  208. package/src/state/SingletonExperiment.tsx +24 -13
  209. package/src/state/adapterContract.ts +116 -0
  210. package/src/state/adapters.test.ts +113 -61
  211. package/src/state/adapters.ts +328 -69
  212. package/src/state/document.test.ts +71 -96
  213. package/src/state/document.ts +66 -52
  214. package/src/state/helpers.test.ts +1 -1
  215. package/src/state/index.ts +8 -0
  216. package/src/state/labRecords.test.ts +105 -0
  217. package/src/state/labRecords.ts +160 -0
  218. package/src/state/openLabStore.test.ts +658 -0
  219. package/src/state/openLabStore.ts +403 -0
  220. package/src/state/records.test.ts +234 -0
  221. package/src/state/records.ts +219 -0
  222. package/src/state/store.test.ts +169 -635
  223. package/src/state/store.ts +146 -163
  224. package/src/state/toolSlot.test.ts +1 -1
  225. package/src/state/types.ts +65 -17
  226. package/src/state/useOpenOnce.ts +68 -0
  227. package/src/state/usePersistedState.test.tsx +125 -0
  228. package/src/state/usePersistedState.ts +66 -0
  229. package/src/state/useTrialState.test.tsx +87 -3
  230. package/src/state/useTrialState.ts +9 -1
  231. package/src/state/view.test.ts +49 -15
  232. package/src/state/view.ts +18 -1
  233. package/src/styles.less +3 -0
  234. package/src/surface/AGENTS.md +39 -7
  235. package/src/surface/SurfaceContext.ts +26 -3
  236. package/src/surface/index.ts +2 -0
  237. package/src/surface/useSurfaceTile.test.tsx +1 -0
  238. package/src/surface/useSurfaceTile.ts +6 -5
  239. package/src/surface/useTiledSurface.test.tsx +231 -1
  240. package/src/surface/useTiledSurface.ts +76 -15
  241. package/src/theme/Interstellar.stories.tsx +9 -9
  242. package/src/theme/interstellar.test.ts +17 -6
  243. package/src/theme/interstellar.theme.json +180 -0
  244. package/src/theme/interstellar.ts +8 -5
  245. package/src/tools/labTool.ts +11 -0
  246. package/src/trial/Trial.annotations.persist.test.tsx +100 -58
  247. package/src/trial/Trial.annotations.test.tsx +59 -7
  248. package/src/trial/Trial.canvas.test.tsx +32 -0
  249. package/src/trial/Trial.config.test.tsx +75 -2
  250. package/src/trial/Trial.job.test.tsx +3 -3
  251. package/src/trial/Trial.less +5 -26
  252. package/src/trial/Trial.stories.tsx +5 -3
  253. package/src/trial/Trial.test.tsx +31 -4
  254. package/src/trial/Trial.trialId.test.tsx +23 -1
  255. package/src/trial/Trial.tsx +139 -52
  256. package/src/trial/TrialBody.tsx +17 -141
  257. package/src/trial/TrialChrome.tsx +24 -7
  258. package/src/trial/trialOps.test.ts +101 -1
  259. package/src/trial/trialOps.ts +67 -7
  260. package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
  261. package/dist/_dts/types-x92Kfeme.d.ts +0 -62
  262. package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
  263. package/dist/chunk-2P6PP5N4.js +0 -553
  264. package/dist/chunk-2P6PP5N4.js.map +0 -1
  265. package/dist/chunk-64ZCN3DA.js.map +0 -1
  266. package/dist/chunk-CTRKTLYZ.js.map +0 -1
  267. package/dist/chunk-E44SU6XS.js +0 -50
  268. package/dist/chunk-E44SU6XS.js.map +0 -1
  269. package/dist/chunk-FQMJUVHJ.js.map +0 -1
  270. package/dist/chunk-I6JCVE24.js.map +0 -1
  271. package/dist/chunk-MOM3GOVY.js.map +0 -1
  272. package/dist/chunk-SMHP6XZ4.js.map +0 -1
  273. package/dist/chunk-TJ7QY3OC.js.map +0 -1
  274. package/dist/chunk-U3IYHIAE.js +0 -343
  275. package/dist/chunk-U3IYHIAE.js.map +0 -1
  276. package/dist/chunk-W3ECWC2K.js +0 -7335
  277. package/dist/chunk-W3ECWC2K.js.map +0 -1
  278. package/src/theme/interstellar.tokens.json +0 -61
@@ -1,7 +1,5 @@
1
1
  import { ComponentType, ReactNode, RefObject } from 'react';
2
- import { R as ResolvedConfig, a as ConfigSchema, C as ConfigPath } from './types-B9_zrHmb.js';
3
- import { C as ConfigField } from './types-x92Kfeme.js';
4
- import { c as SavedSnapshot, i as TrialInfo } from './types-BP2OCcpg.js';
2
+ import { R as ResolvedConfig, C as ConfigPath, V as ValueAtPath, a as ConfigField, b as ConfigSchema } from './types-DzoNY1pO.js';
5
3
  import { J as JobHandle, a as JobCapability } from './types-DJ79Tg5J.js';
6
4
  import { Scene } from '@weasel-js/core';
7
5
 
@@ -33,15 +31,223 @@ declare function resolveFrame(spec: WorldSpec | undefined, size: ViewportSize):
33
31
  * `draw` works in them. Must stay the exact inverse of `screenToWorld`. */
34
32
  declare function applyCamera(ctx: CanvasRenderingContext2D, view: ViewTransform, frame?: WorldFrame): void;
35
33
 
34
+ /**
35
+ * Written at a config path to mean "do not pin this — let the instrument
36
+ * decide". Accepted by `setConfig`, by a trial's `configSeed`, and by
37
+ * `.initial()` in a schema. It is normalized into the trial's set of unpinned
38
+ * paths on the way into the store and is never itself stored, so a serialized
39
+ * trial contains no sentinel and the last pinned value survives.
40
+ */
41
+ declare const auto: unique symbol;
42
+ /** The type of the `auto` sentinel, for widening a value parameter. */
43
+ type Auto = typeof auto;
44
+ declare function isAuto(value: unknown): value is Auto;
45
+
46
+ /** A sidebar section torn out of its trial, and where it went. */
47
+ interface UndockedPanel {
48
+ trialId: string;
49
+ sectionId: string;
50
+ as: 'tile' | 'floating';
51
+ }
52
+ /** Undocked panels, keyed by {@link panelKey}. */
53
+ type UndockedPanels = Record<string, UndockedPanel>;
54
+ declare function panelKey(trialId: string, sectionId: string): string;
55
+ declare function undockPanel(panels: UndockedPanels, trialId: string, sectionId: string, as?: UndockedPanel['as']): UndockedPanels;
56
+ /** Dock one section back, or — with no `sectionId` — every panel the trial
57
+ * owns, which is what closing a trial wants. */
58
+ declare function dockPanel(panels: UndockedPanels, trialId: string, sectionId?: string): UndockedPanels;
59
+
60
+ /** A trial's undo history, as snapshots of its state either side of the
61
+ * present. */
62
+ interface UndoStack {
63
+ past: unknown[];
64
+ future: unknown[];
65
+ }
66
+ /** The trial a per-trial call is coming from. `id` is the id `useTileId`
67
+ * scopes a surface tile under, so a consumer keying its own per-trial
68
+ * registry and labkit's tiles agree on the key. */
69
+ interface TrialInfo<TV = unknown> {
70
+ id: string;
71
+ /** The trial's view, in whatever shape the instrument keeps it. */
72
+ view: TV;
73
+ }
74
+ /** One trial as the store holds it: which instrument it runs, that
75
+ * instrument's config and state, the camera, and the undo history. */
76
+ interface TrialRecord<TS = unknown, TC = unknown, TV = unknown> {
77
+ id: string;
78
+ instrumentName: string;
79
+ config: TC;
80
+ state: TS;
81
+ /** Opaque to labkit: persisted, restored on Reset and handed to the instrument,
82
+ * but never read into. A 3D lab puts an orbit here and keeps all three. */
83
+ view: TV;
84
+ /** The config `addTrial` opened this trial on, overlaid on the instrument's
85
+ * defaults. Kept so Reset restores the trial's own subject rather than the
86
+ * bare defaults. */
87
+ configSeed?: Partial<TC>;
88
+ /** Dotted paths the trial's seed config opened as auto. Kept beside
89
+ * `configSeed`, which cannot carry them: the sentinel is stripped on the way
90
+ * in, so Reset would otherwise have no way back to the state the trial
91
+ * opened in. Absent means none. */
92
+ autoSeed?: readonly string[];
93
+ /** Dotted paths this trial has unpinned. The value at such a path stays in
94
+ * `config` — it is what the field pins back to — but the instrument reads
95
+ * the resolver's value, or `undefined`, instead. Absent means none. */
96
+ auto?: readonly string[];
97
+ /** This trial's own tool slot. Undefined means it reads the lab's. */
98
+ activeToolId?: string | null;
99
+ /** The extent the trial's sidebar was last dragged to, in pixels. Undefined
100
+ * until someone moves the seam. */
101
+ sidebarWidth?: number;
102
+ /** What the title bar reads. Undefined — the state a trial opens in, and the
103
+ * one `setTitle(null)` returns it to — means the instrument's name. */
104
+ title?: string | null;
105
+ /** Which of the trial's collapsible sections are folded, keyed as
106
+ * `TrialChromeContext.collapsedSections` describes. A key that is absent
107
+ * takes the section's own default. */
108
+ collapsedSections?: Record<string, boolean>;
109
+ /** The marks on this trial's annotation targets, as `AnnotationsApi.toJSON`
110
+ * wrote them. Opaque here, and absent for a trial whose instrument declares
111
+ * no `annotations` — or declares its own `storage`. Not in `state`, which
112
+ * belongs to the instrument and is typed as such. */
113
+ annotations?: unknown;
114
+ undoStack: UndoStack;
115
+ }
116
+ /** A named, saved copy of a trial's config and state, restorable into any
117
+ * trial running the same instrument. */
118
+ interface SavedSnapshot {
119
+ id: string;
120
+ name: string;
121
+ trialId: string;
122
+ instrumentName: string;
123
+ config: unknown;
124
+ state: unknown;
125
+ /** The trial's `auto` paths when saved; empty means every field was pinned.
126
+ * Absent on a snapshot saved before snapshots carried them, which loads
127
+ * over the trial's current set rather than guessing one. */
128
+ auto?: readonly string[];
129
+ savedAt: number;
130
+ }
131
+ /** `auto` follows the OS; the other two are an explicit choice. */
132
+ type LabMode = 'auto' | 'light' | 'dark';
133
+ /** Everything a lab persists: its trials, its saved snapshots, and the
134
+ * chosen color mode. */
135
+ interface LabStoreState {
136
+ trials: TrialRecord[];
137
+ savedSnapshots: SavedSnapshot[];
138
+ mode: LabMode;
139
+ /** The lab's tool slot — what a trial with no slot of its own resolves to. */
140
+ activeToolId: string | null;
141
+ /** Per-trial tile extents, keyed by trial id. Opaque here — the
142
+ * shape belongs to whatever lays the trials out. */
143
+ layout: Record<string, unknown>;
144
+ /** Sidebar sections torn out of their trial. A panel here is not rendered in
145
+ * its trial's sidebar; the workspace renders it instead. */
146
+ undockedPanels: UndockedPanels;
147
+ /** The instruments this store serializes, migrates and fills configs for.
148
+ * Null for a store built without them, whose trials look their instrument
149
+ * up on the lab instead. Not persisted. */
150
+ instruments: InstrumentList | null;
151
+ }
152
+ /** A change someone else made to one record: its new value, or `undefined`
153
+ * when it was deleted. */
154
+ type StorageChange = [key: string, value: unknown];
155
+ /** Where a lab persists itself: asynchronous keyed storage of
156
+ * structured-clone values, so IndexedDB, the URL, memory or a server can all
157
+ * back one. */
158
+ interface StorageAdapter {
159
+ /** `undefined` when the key is absent. */
160
+ get(key: string): Promise<unknown>;
161
+ list(prefix: string): Promise<[string, unknown][]>;
162
+ /** Rejects when the value did not land. */
163
+ set(key: string, value: unknown): Promise<void>;
164
+ delete(key: string): Promise<void>;
165
+ /** Reports writes under `prefix` made by anyone but this adapter — another
166
+ * tab, another instance, a server. Omit when the substrate cannot tell. */
167
+ subscribe?(prefix: string, on: (changes: StorageChange[]) => void): () => void;
168
+ }
169
+ /** What `useTrialState` hands an instrument: its state and config, with
170
+ * a setter for each. */
171
+ interface TrialStateHandle<TS, TC> {
172
+ state: TS;
173
+ setState: (next: TS | ((prev: TS) => TS)) => void;
174
+ config: TC;
175
+ /** The config as stored, before any auto path is resolved. What the control
176
+ * panel renders, so a ghosted control sits at the value it pins back to. */
177
+ raw: TC;
178
+ /** Dotted paths this trial has unpinned. */
179
+ auto: ReadonlySet<string>;
180
+ /** Write one config value, by dotted path — `'grid.size'` for a leaf under
181
+ * an `f.group`, `'cellSize'` for one at the root. */
182
+ setConfig: <P extends ConfigPath<TC> & string>(path: P, value: ValueAtPath<TC, P> | Auto) => void;
183
+ }
184
+ /** Options for `createLabStore`, which knows nothing about storage. */
185
+ interface CreateLabStoreOptions {
186
+ /** A document already read and migrated — what `openLabStore` hands over. */
187
+ initial?: LabDocument;
188
+ initialMode?: LabMode;
189
+ /** Each instrument's default config, keyed by instrument name, used to fill
190
+ * the gaps in a stored one. A config saved before its schema grew a branch
191
+ * arrives holding that branch's defaults rather than `undefined`, and keeps
192
+ * whatever keys the schema has since stopped naming. */
193
+ configDefaults?: Record<string, () => unknown>;
194
+ /** Each instrument's `migrateConfig`, keyed by instrument name, run on a
195
+ * stored config before its defaults fill it. */
196
+ configMigrations?: Record<string, (stored: unknown) => unknown>;
197
+ /** Each instrument's resolved config schema, keyed by instrument name. What
198
+ * a trial's auto paths are read against, away from the lab that would
199
+ * otherwise supply the rules. */
200
+ configSchemas?: Record<string, ResolvedConfig>;
201
+ /** How each instrument's state survives a reload. Hydration is the first
202
+ * thing `createLabStore` does, so these have to arrive with the store. */
203
+ serializers?: InstrumentSerializers;
204
+ /** The instruments to serialize, migrate and fill configs for. Given, it
205
+ * supplies all three and the three fields above are ignored. */
206
+ instruments?: InstrumentList;
207
+ }
208
+ /** What a store does per instrument on the way in and out. */
209
+ interface InstrumentHooks {
210
+ serializers: InstrumentSerializers;
211
+ configDefaults: Record<string, () => unknown>;
212
+ configMigrations: Record<string, (stored: unknown) => unknown>;
213
+ configSchemas: Record<string, ResolvedConfig>;
214
+ }
215
+ /** Per-instrument serialize/deserialize hooks, keyed by instrument name. An
216
+ * instrument whose state is already JSON-safe needs no entry. `deserialize`
217
+ * is handed the config the state was saved against — a trial's own for a
218
+ * reload, the snapshot's for a load — since a state rebuilt without it can
219
+ * disagree with the settings sitting next to it. */
220
+ type InstrumentSerializers = Record<string, {
221
+ serialize?: (state: unknown) => unknown;
222
+ deserialize?: (data: unknown, config: unknown) => unknown;
223
+ } | undefined>;
224
+ /** A trial as it is persisted: everything but the undo history, which is
225
+ * session-only. */
226
+ type SerializedTrial = Omit<TrialRecord, 'undoStack'>;
227
+ /** Everything a lab persists, under one key, at a known version. */
228
+ interface LabDocument {
229
+ version: number;
230
+ trials: SerializedTrial[];
231
+ saves: SavedSnapshot[];
232
+ layout: Record<string, unknown>;
233
+ /** Sidebar sections torn out of their trial, keyed by trial and section. */
234
+ undockedPanels: UndockedPanels;
235
+ mode: LabMode;
236
+ }
237
+ /** Migrates a document one version forward. Index `i` in the chain takes a
238
+ * version-`i` document to version `i + 1`. */
239
+ type Migration = (doc: Record<string, unknown>) => Record<string, unknown>;
240
+
36
241
  /** A named position in a trial's chrome. Content is not a region — that is
37
- * the instrument. */
242
+ * the instrument. The lab's own boxes are named in `LabRegion`. */
38
243
  type TrialRegion = 'titlebar' | 'toolbar' | 'palette' | 'sidebar' | 'viewport' | 'status';
39
244
  /** An icon component taking a pixel size, as `@weasel-js/ui` glyphs do. */
40
245
  type IconComponent = ComponentType<{
41
246
  size?: number;
42
247
  }>;
43
- /** A button in the trial toolbar. */
44
- interface ToolbarItem {
248
+ /** A button in a toolbar. `TCtx` is the chrome context its `onActivate` is
249
+ * handed — a trial's, or the lab's for a lab-level bar. */
250
+ interface ToolbarItem<TCtx = TrialChromeContext> {
45
251
  icon: IconComponent;
46
252
  label: string;
47
253
  /** Shown in the tooltip. Not bound here — the trial owns its keymap. */
@@ -54,10 +260,10 @@ interface ToolbarItem {
54
260
  pressed?: boolean;
55
261
  /** Render the label beside the glyph rather than only in the tooltip. */
56
262
  showLabel?: boolean;
57
- /** Handed the trial's chrome context, so a contribution declared from the
58
- * lab can reach `ctx.saveSnapshot()` and the rest without the `render`
59
- * escape. A zero-argument handler stays valid. */
60
- onActivate: (ctx: TrialChromeContext) => void;
263
+ /** Handed the chrome context it was declared against, so a contribution can
264
+ * reach `ctx.saveSnapshot()` and the rest without the `render` escape. A
265
+ * zero-argument handler stays valid. */
266
+ onActivate: (ctx: TCtx) => void;
61
267
  }
62
268
  /** A selectable tool in the palette region. */
63
269
  interface ToolItem {
@@ -79,12 +285,12 @@ interface SidebarSection {
79
285
  body: ReactNode;
80
286
  }
81
287
  /** A control acting on the view of the trial, not on the trial. */
82
- interface ViewportControl {
288
+ interface ViewportControl<TCtx = TrialChromeContext> {
83
289
  icon: IconComponent;
84
290
  label: string;
85
291
  disabled?: boolean;
86
292
  /** Handed the trial's chrome context, as `ToolbarItem.onActivate` is. */
87
- onActivate: (ctx: TrialChromeContext) => void;
293
+ onActivate: (ctx: TCtx) => void;
88
294
  }
89
295
  /** A readout in the status bar. */
90
296
  interface StatusReadout {
@@ -93,7 +299,7 @@ interface StatusReadout {
93
299
  /** Tooltip. */
94
300
  title?: string;
95
301
  }
96
- /** What every contribution shares. */
302
+ /** What every contribution shares, whichever chrome it is declared against. */
97
303
  interface ContributionBase {
98
304
  id: string;
99
305
  /** Groups sort by first appearance; items sort within a group by
@@ -136,18 +342,46 @@ type TrialContribution = (ContributionBase & {
136
342
  item?: never;
137
343
  render: (ctx: TrialChromeContext) => ReactNode;
138
344
  });
345
+ /**
346
+ * A tool slot a region can reflect and write. Both chrome contexts carry one:
347
+ * a trial's resolves to the lab's when its instrument declares no tools.
348
+ */
349
+ interface ToolSlotContext {
350
+ activeToolId: string | null;
351
+ setActiveTool: (id: string) => void;
352
+ }
353
+ /** What a sidebar region reflects and writes: which sections are folded, and —
354
+ * where the chrome can tear a section out — where it goes. A trial supplies
355
+ * all four; a lab supplies the fold state only. */
356
+ interface SidebarSlotContext {
357
+ collapsedSections: Readonly<Record<string, boolean>>;
358
+ setSectionCollapsed: (key: string, collapsed: boolean) => void;
359
+ undockedPanels?: readonly string[];
360
+ undockPanel?: (sectionId: string, as?: 'tile' | 'floating') => void;
361
+ }
362
+ /**
363
+ * A contribution as a region renderer sees it. A renderer checks the region
364
+ * name against its own and narrows `item` itself, which is what lets one
365
+ * renderer serve both chromes — the trial's and the lab's — without knowing
366
+ * which context it was handed.
367
+ */
368
+ interface RegionContribution<TCtx> extends ContributionBase {
369
+ region: string;
370
+ item?: unknown;
371
+ render?: (ctx: TCtx) => ReactNode;
372
+ }
139
373
  /**
140
374
  * Everything a contribution can read about the trial it is being rendered
141
375
  * into. Replaces the three separate slot contexts, which each carried a
142
376
  * hand-picked subset.
143
377
  */
144
- interface TrialChromeContext {
378
+ interface TrialChromeContext extends ToolSlotContext, SidebarSlotContext {
145
379
  trialId: string;
146
380
  instrumentName: string;
147
- /** What the title bar reads, which is the instrument's name until something
148
- * calls `setTitle`. */
381
+ /** What the title bar reads, which is the instrument's title (or, lacking
382
+ * one, its name) until something calls `setTitle`. */
149
383
  title: string;
150
- /** Retitle this trial; `null` restores the instrument name. Persisted with
384
+ /** Retitle this trial; `null` restores the instrument's title. Persisted with
151
385
  * the trial, so a title survives a reload. */
152
386
  setTitle: (title: string | null) => void;
153
387
  isLastTrial: boolean;
@@ -169,6 +403,9 @@ interface TrialChromeContext {
169
403
  * `config`, since a builder schema has no `ConfigField[]` form. */
170
404
  configFields: ConfigField[];
171
405
  config: unknown;
406
+ /** The trial's unpinned paths. `config` stays raw, so a control drawn from
407
+ * it sits at the value un-pinning writes back. */
408
+ auto: ReadonlySet<string>;
172
409
  setConfig: (key: string, value: unknown) => void;
173
410
  /** Which of this trial's collapsible sections are folded. A sidebar
174
411
  * section's key is its contribution id; a section *inside* a contribution —
@@ -189,10 +426,6 @@ interface TrialChromeContext {
189
426
  clone: () => void;
190
427
  reset: () => void;
191
428
  close: () => void;
192
- /** Resolved active tool: the trial's slot, or the lab's when the trial has
193
- * none. Null when neither holds one. */
194
- activeToolId: string | null;
195
- setActiveTool: (id: string) => void;
196
429
  }
197
430
 
198
431
  /** A point on the magnified surface, in its own CSS pixels. */
@@ -245,7 +478,7 @@ interface LoupeCapability<TS = unknown, TC = unknown> {
245
478
  /** Held for a momentary peek while the loupe is off. Default `'Alt'`; `null`
246
479
  * turns hold-to-peek off. Matched against `KeyboardEvent.key`. */
247
480
  peekKey?: string | null;
248
- /** Called with the colour under the aim, wherever the surface can say. The
481
+ /** Called with the color under the aim, wherever the surface can say. The
249
482
  * canvas painter reads it back; a DOM loupe has no pixels to sample. */
250
483
  onColorChange?: (hex: string) => void;
251
484
  }
@@ -353,6 +586,18 @@ interface CanvasCapability<TS = unknown, TC = unknown> {
353
586
  minZoom?: number;
354
587
  maxZoom?: number;
355
588
  }
589
+ /** Declares that an instrument's DOM is content of a fixed size, which the
590
+ * trial pans and zooms the way it does a canvas's layers. Not combined with
591
+ * `canvas`: there the DOM is an unzoomed overlay, and `canvas` wins. */
592
+ interface StageCapability {
593
+ /** The content's own size in CSS pixels, at zoom 1. */
594
+ size: ViewportSize;
595
+ /** Where the view starts. Omitted, the content opens centered and shrunk to
596
+ * fit (`fitStage`). A function is called once the stage knows its size. */
597
+ initialView?: ViewTransform | ((viewport: ViewportSize) => ViewTransform);
598
+ minZoom?: number;
599
+ maxZoom?: number;
600
+ }
356
601
  /** Declares which of an instrument's layers the trial should offer
357
602
  * show/hide controls for. */
358
603
  interface LayerCapability {
@@ -391,7 +636,8 @@ type HitResult = {
391
636
  layerId?: string;
392
637
  pointId?: string;
393
638
  };
394
- /** A trial's camera. */
639
+ /** A trial's camera. `zoom` is always positive and finite once labkit holds
640
+ * it — see `normalize2DView`. */
395
641
  type ViewTransform = {
396
642
  zoom: number;
397
643
  pan: Point;
@@ -424,7 +670,10 @@ type DragFeedback = {
424
670
  * is what makes the trial provide the corresponding chrome.
425
671
  */
426
672
  interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
673
+ /** The id a trial record names its instrument by. */
427
674
  name: string;
675
+ /** What a trial of this instrument and the add-trial menu read. Default: `name`. */
676
+ title?: string;
428
677
  defaultConfig: () => TC;
429
678
  initialState: (config: TC) => TS;
430
679
  /** The instrument's config, declared once: values, types and controls.
@@ -438,9 +687,16 @@ interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
438
687
  * layers rather than instead of them; return `null` for canvas only. */
439
688
  render: (ctx: RenderContext<TS, TC>) => ReactNode;
440
689
  onConfigChange?: (config: TC, prev: TC, state: TS) => TS;
690
+ /** Moves a stored config's values to where the current schema keeps them —
691
+ * `gridSize` to `grid.size` — before the defaults fill its gaps, so it
692
+ * returns only what it moved and leaves the rest to them. It runs on every
693
+ * read of a stored config, including one already moved, so it must hand
694
+ * back a current config unchanged. */
695
+ migrateConfig?: (stored: unknown) => unknown;
441
696
  serialize?: (state: TS) => unknown;
442
697
  deserialize?: (data: unknown, config: TC) => TS;
443
698
  canvas?: CanvasCapability<TS, TC>;
699
+ stage?: StageCapability;
444
700
  layers?: LayerCapability;
445
701
  dragDrop?: DragDropCapability<TS, TC>;
446
702
  undo?: UndoCapability;
@@ -479,9 +735,10 @@ interface AnnotationStoreOptions {
479
735
  targets: () => readonly AnnotationTargetInfo[];
480
736
  /** Serialized scenes from a previous `toJSON`, keyed by target. */
481
737
  restore?: Readonly<Record<string, unknown>>;
482
- /** The instrument's vocabulary, so an export draws a mark in the colour its
483
- * status gives it. */
484
- meaning?: AnnotationMeaning;
738
+ /** The instrument's vocabulary, so an export draws a mark in the color its
739
+ * status gives it. A thunk is read at each capture, for a vocabulary that
740
+ * changes under a store built once. */
741
+ meaning?: AnnotationMeaning | (() => AnnotationMeaning | undefined);
485
742
  /** The trial's live config. A getter for the same reason `targets` is: the
486
743
  * store is built once and the config changes under it. */
487
744
  config?: () => unknown;
@@ -535,7 +792,7 @@ interface AnnotationStatus {
535
792
  id: string;
536
793
  label: string;
537
794
  /** What a mark in this status is drawn in. Omitted, it takes the default
538
- * mark colour — a status is allowed to be a label and nothing more. */
795
+ * mark color — a status is allowed to be a label and nothing more. */
539
796
  color?: string;
540
797
  }
541
798
  /** The optional meaning tier: what a mark means, as opposed to where it is.
@@ -651,9 +908,10 @@ interface AnnotationTarget extends AnnotationTargetInfo {
651
908
  /** Where an instrument keeps its own marks. Declaring this means labkit never
652
909
  * writes its trial slot — for an instrument whose marks belong in a format it
653
910
  * already owns. Both halves are called outside React; `save` is already
654
- * debounced by the time it arrives. */
911
+ * debounced by the time it arrives. A trial shows an empty body until `load`
912
+ * settles. */
655
913
  interface AnnotationStorage {
656
- load: () => SerializedAnnotations | null | undefined;
914
+ load: () => Promise<SerializedAnnotations | null | undefined>;
657
915
  save: (doc: SerializedAnnotations) => void;
658
916
  }
659
917
  /** Declares that an instrument accepts marks: which regions take them,
@@ -663,6 +921,8 @@ interface AnnotationsCapability<TS = unknown, TC = unknown> {
663
921
  * is called once per trial, and its targets are that trial's own. */
664
922
  targets: (state: TS, config: TC, trial: TrialInfo) => readonly AnnotationTarget[];
665
923
  meaning?: AnnotationMeaning;
924
+ /** An instrument replacing this one under a live trial must pass the same
925
+ * object: the trial keeps the marks it loaded from the first. */
666
926
  storage?: AnnotationStorage;
667
927
  /** Fires after every finished export, labkit's own chrome included. A
668
928
  * notification, not an interception: a host wanting its own flow calls
@@ -758,5 +1018,5 @@ declare function fracContains(box: FracRect, pt: FracPoint, tol?: number): boole
758
1018
  * answers false for two rects that merely overlap. */
759
1019
  declare function fracEncloses(outer: FracRect, inner: FracRect): boolean;
760
1020
 
761
- export { LOUPE_DEFAULTS as L, annotationsFromJSON as a3, applyCamera as a4, createAnnotationScene as a5, createAnnotationStore as a6, fracContains as a7, fracEncloses as a8, fracToWorld as a9, resolveFrame as aa, resolveLoupe as ab, roundFrac as ac, worldToFrac as ad, DEFAULT_FRAME as x };
762
- export type { TrialRegion as $, AnnotationData as A, LayerCapability as B, CaptureSource as C, DragFeedback as D, LayerDescriptor as E, FracPoint as F, LoupeCapability as G, HitResult as H, Instrument as I, LoupeDeclaration as J, LoupeRenderArgs as K, MarkScene as M, ResolvedLoupe as N, SidebarSection as O, PaletteItem as P, StatusReadout as Q, RenderContext as R, SerializedAnnotations as S, TrialTool as T, SystemEvent as U, ViewTransform as V, WorldFrame as W, ToolCapability as X, ToolItem as Y, ToolbarItem as Z, TrialChromeContext as _, Point as a, UndoCapability as a0, ViewportControl as a1, ViewportSize as a2, LoupePoint as ae, LoupeMode as af, DragDropCapability as b, WorldSpec as c, WorldRect as d, AnnotationMeaning as e, CaptureOptions as f, CaptureResult as g, AnnotationTarget as h, AnnotationsApi as i, AnnotationsCapability as j, AnnotationKind as k, InstrumentList as l, TrialContribution as m, Annotation as n, AnnotationInit as o, AnnotationPatch as p, AnnotationQuery as q, AnnotationStatus as r, AnnotationStoreOptions as s, AnnotationTargetInfo as t, CanvasCapability as u, CanvasLayer as v, CaptureDeps as w, FracRect as y, IconComponent as z };
1021
+ export { DEFAULT_FRAME as D, LOUPE_DEFAULTS as a8, fracEncloses as aA, fracToWorld as aB, isAuto as aC, resolveLoupe as aD, roundFrac as aE, worldToFrac as aF, annotationsFromJSON as av, auto as aw, createAnnotationScene as ax, createAnnotationStore as ay, fracContains as az, applyCamera as b, dockPanel as j, panelKey as p, resolveFrame as r, undockPanel as u };
1022
+ export type { Auto as $, AnnotationData as A, AnnotationTarget as B, CreateLabStoreOptions as C, AnnotationsApi as E, AnnotationsCapability as F, TrialInfo as G, TrialTool as H, InstrumentSerializers as I, AnnotationKind as J, Instrument as K, LabDocument as L, Migration as M, TrialContribution as N, Annotation as O, PaletteItem as P, AnnotationInit as Q, AnnotationPatch as R, SerializedTrial as S, TrialRecord as T, UndoStack as U, ViewportSize as V, WorldFrame as W, AnnotationQuery as X, AnnotationStatus as Y, AnnotationStoreOptions as Z, AnnotationTargetInfo as _, WorldSpec as a, CanvasCapability as a0, CanvasLayer as a1, CaptureDeps as a2, ContributionBase as a3, FracPoint as a4, FracRect as a5, HitResult as a6, IconComponent as a7, LayerCapability as a9, LoupePoint as aG, LoupeMode as aH, LayerDescriptor as aa, LoupeCapability as ab, LoupeDeclaration as ac, LoupeRenderArgs as ad, RegionContribution as ae, RenderContext as af, ResolvedLoupe as ag, SerializedAnnotations as ah, SidebarSection as ai, SidebarSlotContext as aj, StageCapability as ak, StatusReadout as al, SystemEvent as am, ToolCapability as an, ToolItem as ao, ToolSlotContext as ap, ToolbarItem as aq, TrialChromeContext as ar, TrialRegion as as, UndoCapability as at, ViewportControl as au, StorageAdapter as c, LabStoreState as d, SavedSnapshot as e, StorageChange as f, TrialStateHandle as g, UndockedPanel as h, UndockedPanels as i, ViewTransform as k, Point as l, DragFeedback as m, DragDropCapability as n, LabMode as o, InstrumentList as q, InstrumentHooks as s, WorldRect as t, AnnotationMeaning as v, MarkScene as w, CaptureSource as x, CaptureOptions as y, CaptureResult as z };