@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
@@ -0,0 +1,5 @@
1
+ // fake-indexeddb's `./auto` export declares no `types` condition, so under
2
+ // `moduleResolution: Bundler` tsc finds no declaration for it — the ambient one
3
+ // the package ships at its root is never part of the compilation. The subpath
4
+ // is side-effect-only, so a bare declaration is the whole of it.
5
+ declare module 'fake-indexeddb/auto';
package/src/index.test.ts CHANGED
@@ -51,6 +51,13 @@ describe('public export surface', () => {
51
51
  });
52
52
  });
53
53
 
54
+ describe('ui re-exports', () => {
55
+ it('carries ToggleBar and Button from the package root', () => {
56
+ expect(labkit).toHaveProperty('ToggleBar');
57
+ expect(labkit).toHaveProperty('Button');
58
+ });
59
+ });
60
+
54
61
  describe('surface, job and orbit entry points', () => {
55
62
  it('are reachable from the package root', async () => {
56
63
  const kit = await import('./index');
package/src/index.ts CHANGED
@@ -1,8 +1,13 @@
1
- // The property-panel family and LayerStack live in `@weasel-js/ui` now; labkit
2
- // re-exports them so existing `@weasel-js/labkit` imports keep resolving. Named,
3
- // not `export *` — a star re-export of an external package emits no binding in
4
- // the bundle.
1
+ // The property-panel family, LayerStack, Input, Select, TokenPanel, ToggleBar and Button live in
2
+ // `@weasel-js/ui`; labkit re-exports them so chrome built on labkit needs no
3
+ // direct ui dependency. Named, not `export *` — a star re-export of an external
4
+ // package emits no binding in the bundle.
5
+ // The playback glyphs ride along for the same reason: chrome that labels a control with
6
+ // one should not need `@weasel-js/ui` in its manifest to draw a play button.
5
7
  export type {
8
+ ButtonProps,
9
+ ButtonSize,
10
+ ButtonVariant,
6
11
  CheckboxRowProps,
7
12
  ColorRowProps,
8
13
  CurveFieldProps,
@@ -10,6 +15,9 @@ export type {
10
15
  EffectCardListItem,
11
16
  EffectCardListProps,
12
17
  EffectCardProps,
18
+ IconProps,
19
+ InputProps,
20
+ JogProps,
13
21
  LayerStackItem,
14
22
  LayerStackProps,
15
23
  NumberRowProps,
@@ -22,30 +30,55 @@ export type {
22
30
  PropertyRowProps,
23
31
  PropertyRowVariant,
24
32
  PropertySpanProps,
33
+ SelectProps,
25
34
  SelectRowProps,
26
35
  SliderRowProps,
27
36
  SubpanelProps,
28
37
  TextRowProps,
38
+ ToggleBarItem,
39
+ ToggleBarProps,
40
+ ToggleBarSize,
41
+ ToggleBarVariant,
29
42
  ToggleRowProps,
43
+ TokenCategory,
44
+ TokenEntry,
45
+ TokenPanelProps,
30
46
  } from '@weasel-js/ui';
31
47
  export {
48
+ Button,
32
49
  CheckboxRow,
50
+ CloseIcon,
51
+ CollapseIcon,
33
52
  ColorRow,
34
53
  CurveField,
35
54
  EffectCard,
36
55
  EffectCardList,
56
+ ExpandIcon,
57
+ Icon,
58
+ Input,
59
+ inferTokenType,
60
+ Jog,
37
61
  LayerStack,
38
62
  NumberRow,
63
+ PauseIcon,
64
+ PlayIcon,
39
65
  PropertyGroup,
40
66
  PropertyList,
41
67
  PropertyPanel,
42
68
  PropertyRow,
43
69
  PropertySpan,
70
+ ResetIcon,
71
+ Select,
44
72
  SelectRow,
45
73
  SliderRow,
74
+ StepBackIcon,
75
+ StepForwardIcon,
76
+ StopIcon,
46
77
  Subpanel,
47
78
  TextRow,
79
+ ToggleBar,
48
80
  ToggleRow,
81
+ TokenPanel,
49
82
  } from '@weasel-js/ui';
50
83
  export type {
51
84
  Annotation,
@@ -118,6 +151,7 @@ export type {
118
151
  OrbitHandlers,
119
152
  OrbitView,
120
153
  PanZoomHandlers,
154
+ StageProps,
121
155
  UseOrbitOptions,
122
156
  UsePanZoomOptions,
123
157
  Vec3,
@@ -133,10 +167,12 @@ export {
133
167
  centerOn,
134
168
  clampPitch,
135
169
  DEFAULT_FRAME,
170
+ fitStage,
136
171
  orbitAfterDrag,
137
172
  orbitAfterWheel,
138
173
  PITCH_LIMIT,
139
174
  resolveFrame,
175
+ Stage,
140
176
  screenToWorld,
141
177
  useOrbit,
142
178
  usePanZoom,
@@ -145,7 +181,21 @@ export {
145
181
  zoomAt,
146
182
  } from './canvas';
147
183
  export * from './chrome';
148
- export { f, isConfigBranch } from './config/builder';
184
+ export { type Auto, auto, isAuto } from './config/auto';
185
+ export { autoPathsOf, resolveAutoConfig } from './config/autoConfig';
186
+ export {
187
+ BaseNode,
188
+ BooleanNode,
189
+ ColorNode,
190
+ CustomNode,
191
+ EnumNode,
192
+ f,
193
+ GroupNode,
194
+ isConfigBranch,
195
+ NumberNode,
196
+ StringNode,
197
+ ValueNode,
198
+ } from './config/builder';
149
199
  export { fromConfigFields } from './config/fromConfigField';
150
200
  export {
151
201
  fillConfigDefaults,
@@ -181,6 +231,7 @@ export type {
181
231
  ValueAtPath,
182
232
  } from './config/types';
183
233
  export { useConfigSchema } from './config/useConfigSchema';
234
+ export { useResolvedConfig } from './config/useResolvedConfig';
184
235
  export { isLeafVisible } from './config/visible';
185
236
  export { ControlPanel } from './controls/ControlPanel';
186
237
  export type {
@@ -209,6 +260,7 @@ export type {
209
260
  PaletteItem,
210
261
  Point,
211
262
  RenderContext,
263
+ StageCapability,
212
264
  SystemEvent,
213
265
  UndoCapability,
214
266
  ViewTransform,
@@ -221,8 +273,12 @@ export type { LayerListProps, LayerTreeNode } from './layers';
221
273
  export { LayerList } from './layers';
222
274
  export * from './loupe';
223
275
  export * from './primitives';
276
+ export { SPECIMEN_SECTIONS, Specimen } from './specimen/Specimen';
224
277
  export {
278
+ createIndexedDbAdapter,
225
279
  createMemoryAdapter,
280
+ type IndexedDbAdapterOptions,
281
+ indexedDbAdapter,
226
282
  localStorageAdapter,
227
283
  noneAdapter,
228
284
  sessionStorageAdapter,
@@ -237,6 +293,9 @@ export {
237
293
  useTrialId,
238
294
  } from './state/context';
239
295
  export { CURRENT_DOCUMENT_VERSION, labDocumentKey, quarantineKey } from './state/document';
296
+ export { type OpenedLabStore, type OpenLabStoreOptions, openLabStore } from './state/openLabStore';
297
+ export { Persistence, type PersistenceProps } from './state/Persistence';
298
+ export type { RecordCache, RecordChange } from './state/records';
240
299
  export type {
241
300
  CreateLabStoreOptions,
242
301
  LabDocument,
@@ -245,6 +304,7 @@ export type {
245
304
  SavedSnapshot,
246
305
  SerializedTrial,
247
306
  StorageAdapter,
307
+ StorageChange,
248
308
  TrialInfo,
249
309
  TrialRecord,
250
310
  TrialStateHandle,
@@ -252,6 +312,7 @@ export type {
252
312
  } from './state/types';
253
313
  export type { UndockedPanel, UndockedPanels } from './state/undock';
254
314
  export { dockPanel, panelKey, undockPanel } from './state/undock';
315
+ export { type PersistedStateOptions, usePersistedState } from './state/usePersistedState';
255
316
  export { useTrialState } from './state/useTrialState';
256
317
  export type { ViewTransform2D } from './state/view';
257
318
  export { as2DView, DEFAULT_VIEW } from './state/view';
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { configMigrationsOf } from './serializers';
3
+ import type { InstrumentList } from './types';
4
+
5
+ const instrument = (name: string, extra: Record<string, unknown> = {}) => ({
6
+ name,
7
+ defaultConfig: () => ({}),
8
+ initialState: () => ({}),
9
+ render: () => null,
10
+ ...extra,
11
+ });
12
+
13
+ describe('configMigrationsOf', () => {
14
+ it('keys each declared migration by instrument name, and skips the rest', () => {
15
+ const migrations = configMigrationsOf([
16
+ instrument('moves', { migrateConfig: (c: unknown) => ({ moved: c }) }),
17
+ instrument('stays'),
18
+ ] as InstrumentList);
19
+ expect(Object.keys(migrations)).toEqual(['moves']);
20
+ expect(migrations.moves?.({ a: 1 })).toEqual({ moved: { a: 1 } });
21
+ });
22
+ });
@@ -1,5 +1,33 @@
1
+ import { resolveConfigSchema } from '../config/resolve';
2
+ import type { ResolvedConfig } from '../config/types';
1
3
  import type { InstrumentSerializers } from '../state/types';
2
- import type { InstrumentList } from './types';
4
+ import type { Instrument, InstrumentList } from './types';
5
+
6
+ /**
7
+ * One instrument's config schema, resolved without the lab's rules — which
8
+ * neither the store nor a trial composed outside a lab has access to. No rule
9
+ * can reach `autoResolve`, `unpinned` or `manual`, so for reading those the
10
+ * two resolutions agree; anything reading the rendered leaves wants
11
+ * `useConfigSchema` instead. `undefined` for a legacy `configSchema()`
12
+ * instrument, which declares no auto annotations at all.
13
+ */
14
+ export function unruledConfigSchema(
15
+ // biome-ignore lint/suspicious/noExplicitAny: instruments are stored contravariantly; see InstrumentList
16
+ instrument: Instrument<any, any, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
17
+ ): ResolvedConfig | undefined {
18
+ return instrument.config ? resolveConfigSchema(instrument.config, []) : undefined;
19
+ }
20
+
21
+ /** Each instrument's unruled config schema, keyed by name — what the store
22
+ * reads a leaf's auto annotations off. */
23
+ export function configSchemasOf(instruments: InstrumentList): Record<string, ResolvedConfig> {
24
+ const out: Record<string, ResolvedConfig> = {};
25
+ for (const instrument of instruments) {
26
+ const schema = unruledConfigSchema(instrument);
27
+ if (schema) out[instrument.name] = schema;
28
+ }
29
+ return out;
30
+ }
3
31
 
4
32
  /**
5
33
  * Each instrument's default config, keyed by name — what `createLabStore`
@@ -12,6 +40,17 @@ export function configDefaultsOf(instruments: InstrumentList): Record<string, ()
12
40
  return out;
13
41
  }
14
42
 
43
+ /** Each instrument's `migrateConfig`, keyed by name, for those that declare one. */
44
+ export function configMigrationsOf(
45
+ instruments: InstrumentList,
46
+ ): Record<string, (stored: unknown) => unknown> {
47
+ const out: Record<string, (stored: unknown) => unknown> = {};
48
+ for (const { name, migrateConfig } of instruments) {
49
+ if (migrateConfig) out[name] = (stored) => migrateConfig(stored);
50
+ }
51
+ return out;
52
+ }
53
+
15
54
  /**
16
55
  * The `serialize` / `deserialize` an instrument list declares, keyed by
17
56
  * instrument name — what `createLabStore` needs to rebuild a trial's state
@@ -67,6 +67,19 @@ export interface CanvasCapability<TS = unknown, TC = unknown> {
67
67
  maxZoom?: number;
68
68
  }
69
69
 
70
+ /** Declares that an instrument's DOM is content of a fixed size, which the
71
+ * trial pans and zooms the way it does a canvas's layers. Not combined with
72
+ * `canvas`: there the DOM is an unzoomed overlay, and `canvas` wins. */
73
+ export interface StageCapability {
74
+ /** The content's own size in CSS pixels, at zoom 1. */
75
+ size: ViewportSize;
76
+ /** Where the view starts. Omitted, the content opens centered and shrunk to
77
+ * fit (`fitStage`). A function is called once the stage knows its size. */
78
+ initialView?: ViewTransform | ((viewport: ViewportSize) => ViewTransform);
79
+ minZoom?: number;
80
+ maxZoom?: number;
81
+ }
82
+
70
83
  /** Declares which of an instrument's layers the trial should offer
71
84
  * show/hide controls for. */
72
85
  export interface LayerCapability {
@@ -99,7 +112,8 @@ export type SystemEvent = string;
99
112
  export type Point = { x: number; y: number };
100
113
  /** What a hit-test found, and where. */
101
114
  export type HitResult = { hit: boolean; layerId?: string; pointId?: string };
102
- /** A trial's camera. */
115
+ /** A trial's camera. `zoom` is always positive and finite once labkit holds
116
+ * it — see `normalize2DView`. */
103
117
  export type ViewTransform = { zoom: number; pan: Point };
104
118
  /** A layer as the layer list shows it. `alwaysOn` layers cannot be hidden. */
105
119
  export type LayerDescriptor = { id: string; label: string; alwaysOn?: boolean };
@@ -119,7 +133,10 @@ export type DragFeedback = { ok: boolean; reason?: string };
119
133
  * is what makes the trial provide the corresponding chrome.
120
134
  */
121
135
  export interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
136
+ /** The id a trial record names its instrument by. */
122
137
  name: string;
138
+ /** What a trial of this instrument and the add-trial menu read. Default: `name`. */
139
+ title?: string;
123
140
  defaultConfig: () => TC;
124
141
  initialState: (config: TC) => TS;
125
142
  /** The instrument's config, declared once: values, types and controls.
@@ -133,9 +150,16 @@ export interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
133
150
  * layers rather than instead of them; return `null` for canvas only. */
134
151
  render: (ctx: RenderContext<TS, TC>) => ReactNode;
135
152
  onConfigChange?: (config: TC, prev: TC, state: TS) => TS;
153
+ /** Moves a stored config's values to where the current schema keeps them —
154
+ * `gridSize` to `grid.size` — before the defaults fill its gaps, so it
155
+ * returns only what it moved and leaves the rest to them. It runs on every
156
+ * read of a stored config, including one already moved, so it must hand
157
+ * back a current config unchanged. */
158
+ migrateConfig?: (stored: unknown) => unknown;
136
159
  serialize?: (state: TS) => unknown;
137
160
  deserialize?: (data: unknown, config: TC) => TS;
138
161
  canvas?: CanvasCapability<TS, TC>;
162
+ stage?: StageCapability;
139
163
  layers?: LayerCapability;
140
164
  dragDrop?: DragDropCapability<TS, TC>;
141
165
  undo?: UndoCapability;
@@ -62,4 +62,20 @@ describe('<Lab> chrome', () => {
62
62
  render(<Lab title="T" instruments={[withChrome]} defaultInstrument="Bare" />);
63
63
  expect(screen.getByText('ready')).toBeInTheDocument();
64
64
  });
65
+
66
+ it('renders an aside contribution in its own pane, after the workspace', () => {
67
+ render(
68
+ <Lab
69
+ title="T"
70
+ instruments={[bare]}
71
+ defaultInstrument="Bare"
72
+ labChrome={[{ id: 'notes', region: 'aside', item: { title: 'Notes', body: <p>jot</p> } }]}
73
+ />,
74
+ );
75
+ const body = screen.getByText('jot');
76
+ const trial = screen.getByRole('region', { name: /trial/i });
77
+ expect(body.closest('.lk-lab__aside')).not.toBeNull();
78
+ expect(trial.compareDocumentPosition(body) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
79
+ expect(screen.getByRole('separator', { name: /Lab aside/ })).toBeInTheDocument();
80
+ });
65
81
  });
package/src/lab/Lab.less CHANGED
@@ -1,17 +1,8 @@
1
- // `height: 100%` resolves against the containing block, so the lab needs an
2
- // unbroken chain of sized ancestors — and a host that supplies the height but
3
- // not the margin reset gets a page 16px taller than the viewport, which reads
4
- // as a stuck canvas rather than as overflow. Scoped with `:has` so a page that
5
- // mounts no lab is untouched, and stopping at the lab's own parent so an
6
- // embedded lab cannot resize its host's layout.
7
- html:has(.lk-lab),
8
- body:has(.lk-lab) {
1
+ // A fullscreen lab's page is the viewport, and body's default margin would
2
+ // scroll it. `<LabShell>` alone fills the viewport too. `:has` leaves a page
3
+ // that mounts neither untouched.
4
+ body:has(.lk-lab, .lk-shell) {
9
5
  margin: 0;
10
- height: 100%;
11
- }
12
-
13
- body:has(.lk-lab) > :has(> .lk-lab) {
14
- height: 100%;
15
6
  }
16
7
 
17
8
  .lk-lab {
@@ -0,0 +1,147 @@
1
+ import 'fake-indexeddb/auto';
2
+ import { act, render, screen, waitFor } from '@testing-library/react';
3
+ import { StrictMode } from 'react';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import type { Instrument } from '../instrument/types';
6
+ import { createMemoryAdapter, indexedDbAdapter, resetDefaultStorage } from '../state/adapters';
7
+ import { labPrefix } from '../state/labRecords';
8
+ import type { StorageAdapter } from '../state/types';
9
+ import { Lab } from './Lab';
10
+ import { LabContext, type LabContextValue } from './LabContext';
11
+
12
+ const stub: Instrument = {
13
+ name: 'Stub',
14
+ defaultConfig: () => ({}),
15
+ initialState: () => ({}),
16
+ render: () => <div data-testid="stub-content">stub</div>,
17
+ };
18
+
19
+ let labRef: LabContextValue | null = null;
20
+ function CaptureLab() {
21
+ return (
22
+ <LabContext.Consumer>
23
+ {(value) => {
24
+ labRef = value;
25
+ return null;
26
+ }}
27
+ </LabContext.Consumer>
28
+ );
29
+ }
30
+
31
+ /** A memory adapter whose first `list` waits until released. */
32
+ function gated() {
33
+ const memory = createMemoryAdapter();
34
+ let release = (): void => {};
35
+ const opened = new Promise<void>((resolve) => {
36
+ release = resolve;
37
+ });
38
+ const storage: StorageAdapter = {
39
+ ...memory,
40
+ list: async (prefix) => {
41
+ await opened;
42
+ return memory.list(prefix);
43
+ },
44
+ };
45
+ return { storage, release };
46
+ }
47
+
48
+ afterEach(() => {
49
+ resetDefaultStorage();
50
+ vi.restoreAllMocks();
51
+ });
52
+
53
+ describe('<Lab> with a storageKey', () => {
54
+ it('renders its fallback until the lab has loaded', async () => {
55
+ const { storage, release } = gated();
56
+ render(
57
+ <Lab
58
+ instruments={[stub]}
59
+ defaultInstrument="Stub"
60
+ storageKey="gate"
61
+ storage={storage}
62
+ fallback={<p>loading</p>}
63
+ />,
64
+ );
65
+ expect(screen.getByText('loading')).toBeInTheDocument();
66
+ expect(screen.queryByTestId('stub-content')).toBeNull();
67
+ await act(async () => release());
68
+ expect(await screen.findByTestId('stub-content')).toBeInTheDocument();
69
+ expect(screen.queryByText('loading')).toBeNull();
70
+ });
71
+
72
+ it("falls back to the lab's empty shell by default", () => {
73
+ const { storage } = gated();
74
+ const { container } = render(
75
+ <Lab
76
+ instruments={[stub]}
77
+ defaultInstrument="Stub"
78
+ storageKey="gate"
79
+ storage={storage}
80
+ title="Waiting"
81
+ />,
82
+ );
83
+ expect(container.querySelector('.lk-lab .lk-shell')).not.toBeNull();
84
+ expect(screen.getByText('Waiting')).toBeInTheDocument();
85
+ expect(screen.queryByRole('region', { name: /trial/i })).toBeNull();
86
+ });
87
+
88
+ it('opens the lab once under StrictMode', async () => {
89
+ const storage = createMemoryAdapter();
90
+ const list = vi.spyOn(storage, 'list');
91
+ render(
92
+ <StrictMode>
93
+ <Lab instruments={[stub]} defaultInstrument="Stub" storageKey="strict" storage={storage} />
94
+ </StrictMode>,
95
+ );
96
+ await screen.findByTestId('stub-content');
97
+ expect(list).toHaveBeenCalledTimes(1);
98
+ });
99
+
100
+ it('persists to IndexedDB when given no storage', async () => {
101
+ const view = render(
102
+ <Lab instruments={[stub]} defaultInstrument="Stub" storageKey="idb-lab">
103
+ <CaptureLab />
104
+ </Lab>,
105
+ );
106
+ await screen.findByTestId('stub-content');
107
+ act(() => labRef?.setMode('light'));
108
+ view.unmount();
109
+ await waitFor(async () => {
110
+ const records = new Map(await indexedDbAdapter.list(labPrefix('idb-lab')));
111
+ expect(records.get(`${labPrefix('idb-lab')}meta`)).toMatchObject({ mode: 'light' });
112
+ });
113
+ });
114
+
115
+ it('warns when its storageKey changes after mount, and keeps the first', async () => {
116
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
117
+ const storage = createMemoryAdapter();
118
+ const view = render(
119
+ <Lab instruments={[stub]} defaultInstrument="Stub" storageKey="one" storage={storage} />,
120
+ );
121
+ await screen.findByTestId('stub-content');
122
+ view.rerender(
123
+ <Lab instruments={[stub]} defaultInstrument="Stub" storageKey="two" storage={storage} />,
124
+ );
125
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('`storageKey`'));
126
+ });
127
+
128
+ it('closes the lab on unmount, sending what was queued', async () => {
129
+ const backing = new Map<string, unknown>();
130
+ const view = render(
131
+ <Lab
132
+ instruments={[stub]}
133
+ defaultInstrument="Stub"
134
+ storageKey="close"
135
+ storage={createMemoryAdapter(backing)}
136
+ >
137
+ <CaptureLab />
138
+ </Lab>,
139
+ );
140
+ await screen.findByTestId('stub-content');
141
+ act(() => labRef?.setMode('dark'));
142
+ view.unmount();
143
+ await waitFor(() =>
144
+ expect(backing.get(`${labPrefix('close')}meta`)).toMatchObject({ mode: 'dark' }),
145
+ );
146
+ });
147
+ });
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
1
+ import type { Meta, StoryObj } from '@weasel-js/forge';
2
2
  import { useEffect } from 'react';
3
3
  import type { Instrument } from '../instrument/types';
4
4
  import { Lab } from './Lab';
@@ -27,7 +27,6 @@ export const Default: Story = {
27
27
  instruments: [StubInstrument],
28
28
  defaultInstrument: 'Stub',
29
29
  title: 'Default Lab',
30
- storage: null,
31
30
  },
32
31
  };
33
32
 
@@ -44,7 +43,6 @@ export const TwoTrials: Story = {
44
43
  instruments: [StubInstrument],
45
44
  defaultInstrument: 'Stub',
46
45
  title: 'Two Trials',
47
- storage: null,
48
46
  children: <AddSecondTrial />,
49
47
  },
50
48
  };
@@ -12,7 +12,12 @@ import type { ReactNode } from 'react';
12
12
  import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
13
13
  import { defineInstrument } from '../instrument/defineInstrument';
14
14
  import { SurfaceContext } from '../surface/SurfaceContext';
15
- import { useSurfaceOptional, useSurfaceTile, useTileId } from '../surface/useSurfaceTile';
15
+ import {
16
+ useSurfaceCanvas,
17
+ useSurfaceOptional,
18
+ useSurfaceTile,
19
+ useTileId,
20
+ } from '../surface/useSurfaceTile';
16
21
  import type { SurfaceFrame, SurfaceHandle } from '../surface/useTiledSurface';
17
22
  import { useTiledSurface } from '../surface/useTiledSurface';
18
23
  import { Lab } from './Lab';
@@ -43,9 +48,21 @@ let seen: SurfaceHandle | null | 'not-rendered' = 'not-rendered';
43
48
  * trial is scoped by it, and the trial's id is minted by the store. */
44
49
  let paneTileId = 'pane';
45
50
 
51
+ interface SeenCanvases {
52
+ over: HTMLCanvasElement | null;
53
+ under: HTMLCanvasElement | null;
54
+ byDefault: HTMLCanvasElement | null;
55
+ }
56
+ let seenCanvases: SeenCanvases = { over: null, under: null, byDefault: null };
57
+
46
58
  function Probe() {
47
59
  seen = useSurfaceOptional();
48
60
  paneTileId = useTileId('pane');
61
+ seenCanvases = {
62
+ over: useSurfaceCanvas('over'),
63
+ under: useSurfaceCanvas('under'),
64
+ byDefault: useSurfaceCanvas(),
65
+ };
49
66
  const tile = useSurfaceTile('pane');
50
67
  return <div data-testid="pane" ref={tile} />;
51
68
  }
@@ -66,10 +83,34 @@ describe('<Lab> surface provider', () => {
66
83
  expect(typeof (seen as unknown as SurfaceHandle).registerTile).toBe('function');
67
84
  });
68
85
 
69
- it('mounts one inert buffer inside the lab body', () => {
86
+ it('stacks an under and an over buffer inside the lab body', () => {
70
87
  const { container } = render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
71
88
  const canvases = container.querySelectorAll('.lk-lab__body > canvas.lk-lab__surface');
72
- expect(canvases).toHaveLength(1);
89
+ expect(canvases).toHaveLength(2);
90
+ // Source order, because the z-index that separates them is in a stylesheet
91
+ // jsdom does not resolve — this is a proxy for the stacking, not proof of
92
+ // it. The real check is a browser.
93
+ expect([...canvases].map((c) => c.className)).toEqual([
94
+ 'lk-lab__surface lk-lab__surface--under',
95
+ 'lk-lab__surface lk-lab__surface--over',
96
+ ]);
97
+ });
98
+
99
+ it('hands a tenant whichever of the two it asks for', () => {
100
+ const { container } = render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
101
+ const [underEl, overEl] = [...container.querySelectorAll('canvas.lk-lab__surface')];
102
+ expect(seenCanvases.over).toBe(overEl);
103
+ expect(seenCanvases.under).toBe(underEl);
104
+ expect(overEl).not.toBe(underEl);
105
+ });
106
+
107
+ it('defaults a tenant to the buffer over the trials', () => {
108
+ // Every tenant predating the split wants the over one: the surface was
109
+ // built for marks that annotate an instrument.
110
+ const { container } = render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
111
+ const [underEl, overEl] = [...container.querySelectorAll('canvas.lk-lab__surface')];
112
+ expect(seenCanvases.byDefault).toBe(overEl);
113
+ expect(seenCanvases.byDefault).not.toBe(underEl);
73
114
  });
74
115
 
75
116
  it('mounts no buffer of its own when a host already owns the surface', () => {
@@ -165,3 +206,30 @@ describe('<Lab> tile round trip', () => {
165
206
  expect(rect).toEqual({ x: 240, y: 100, w: 320, h: 200 });
166
207
  });
167
208
  });
209
+
210
+ describe('<Lab> pages', () => {
211
+ it("gives the title a way back to the project's other labs", () => {
212
+ const { getByRole } = render(
213
+ <Lab
214
+ instruments={[probeInstrument]}
215
+ defaultInstrument="Probe"
216
+ title="kliegsminister"
217
+ path="/kliegsminister"
218
+ pages={[
219
+ { href: '/kliegsminister', label: 'kliegsminister' },
220
+ { href: '/stats', label: 'corpus stats' },
221
+ ]}
222
+ />,
223
+ );
224
+ // A switcher, not a bare heading: the lab it came from has to be reachable.
225
+ expect(getByRole('button', { name: /kliegsminister/ })).toBeTruthy();
226
+ });
227
+
228
+ it('leaves the title a plain heading when there are no other labs', () => {
229
+ const { getByRole, queryByRole } = render(
230
+ <Lab instruments={[probeInstrument]} defaultInstrument="Probe" title="solo" />,
231
+ );
232
+ expect(getByRole('heading', { name: 'solo' })).toBeTruthy();
233
+ expect(queryByRole('button', { name: /solo/ })).toBeNull();
234
+ });
235
+ });