@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
package/src/lab/Lab.tsx CHANGED
@@ -4,22 +4,38 @@ import {
4
4
  type ReactNode,
5
5
  useCallback,
6
6
  useEffect,
7
+ useLayoutEffect,
7
8
  useMemo,
8
9
  useRef,
9
10
  useState,
10
11
  } from 'react';
11
12
  import { useStore } from 'zustand/react';
13
+ import { AnnotationPreloadContext } from '../annotations/preload';
14
+ import { ANNOTATION_TOOLS } from '../annotations/toolMap';
15
+ import {
16
+ LabAsideRegion,
17
+ LabFooterRegion,
18
+ LabHeaderRegion,
19
+ LabSidebarRegion,
20
+ labContributions,
21
+ } from '../chrome/LabChrome';
22
+ import type { LabContribution } from '../chrome/labTypes';
12
23
  import type { TrialContribution } from '../chrome/types';
13
24
  import type { ConfigRule, ControlRenderer } from '../config/types';
14
- import { configDefaultsOf, serializersOf } from '../instrument/serializers';
15
25
  import type { InstrumentList } from '../instrument/types';
16
- import { noneAdapter } from '../state/adapters';
26
+ import { Split } from '../primitives/Split';
27
+ import { defaultStorage, noneAdapter } from '../state/adapters';
17
28
  import { LabStoreContext } from '../state/context';
29
+ import { type OpenedLabStore, openLabStore } from '../state/openLabStore';
30
+ import { PersistenceContext } from '../state/Persistence';
31
+ import { createRecordCache } from '../state/records';
18
32
  import { createLabStore, type LabStore } from '../state/store';
19
33
  import type { LabMode, StorageAdapter, TrialRecord } from '../state/types';
34
+ import { useOpenOnce, useWarnIgnoredChange } from '../state/useOpenOnce';
35
+ import { usePersistedState } from '../state/usePersistedState';
20
36
  import { SurfaceCanvasContext, SurfaceContext } from '../surface/SurfaceContext';
21
37
  import { useSurfaceCanvas, useSurfaceOptional } from '../surface/useSurfaceTile';
22
- import { useTiledSurface } from '../surface/useTiledSurface';
38
+ import { type SurfaceFrame, useTiledSurface } from '../surface/useTiledSurface';
23
39
  import { interstellarTheme } from '../theme/interstellar';
24
40
  import type { TrialTool } from '../tools/types';
25
41
  import { Trial } from '../trial/Trial';
@@ -29,22 +45,27 @@ import {
29
45
  closeTrial as closeTrialOp,
30
46
  reorderTrials as reorderTrialsOp,
31
47
  resetTrial as resetTrialOp,
48
+ swapTrial as swapTrialOp,
32
49
  } from '../trial/trialOps';
50
+ import { useLabFitWarning } from './fitCheck';
33
51
  import { LabContext, type LabContextValue } from './LabContext';
34
52
  import { LabHeader } from './LabHeader';
35
53
  import { LabPalette } from './LabPalette';
36
54
  import { LabShell } from './LabShell';
55
+ import type { LabPage } from './LabSwitcher';
37
56
  import { createPanelHostRegistry, PanelHostContext } from './panelHost';
38
57
  import { useResolvedMode } from './useSystemMode';
39
58
  import { type PanelDescriptor, type TrialLayout, Workspace } from './Workspace';
40
59
 
41
- /** Props for `<Lab>`. */
42
- export interface LabProps {
60
+ interface LabBaseProps {
61
+ /** May change while mounted. An instrument that is a different object from
62
+ * the last render counts as replaced and its open trials' configs are
63
+ * refilled, so hoist or memoize the list. */
43
64
  instruments: InstrumentList;
44
65
  defaultInstrument: string;
45
- storage?: StorageAdapter | null;
46
- storageKey?: string;
47
66
  mode?: LabMode;
67
+ /** Rendered while a stored lab loads. Default: the lab's empty shell. */
68
+ fallback?: ReactNode;
48
69
  /**
49
70
  * Optional list of CSS colors used to compose the interstellar theme's
50
71
  * cosmic backdrop. Each color becomes one radial-gradient blob on the
@@ -53,12 +74,27 @@ export interface LabProps {
53
74
  */
54
75
  nebula?: readonly string[];
55
76
  title?: string;
77
+ /** The project's other labs. Given two or more, the title becomes the way to
78
+ * reach them — the same switcher `<LabShell>` renders, so a lab reached from
79
+ * one is not a dead end. */
80
+ pages?: readonly LabPage[];
81
+ /** The path the switcher marks as open. Defaults to the current location. */
82
+ path?: string;
56
83
  /** Rendered in the shell's footer, below the workspace. */
57
84
  footer?: ReactNode;
58
85
  /** Contributions added to every trial's chrome, after the instrument's own. */
59
86
  chrome?: readonly TrialContribution[];
87
+ /** Contributions to the lab's own chrome — its header bar, its sidebar, its
88
+ * tool rail and its footer — rather than to every trial's. Rendered after
89
+ * `children` and `footer`, which stay the way a lab drops arbitrary content
90
+ * into those same two boxes. Going from no sidebar contributions to some
91
+ * remounts the workspace and every trial, dropping unpersisted trial state. */
92
+ labChrome?: readonly LabContribution[];
60
93
  /** Built-in contribution ids to drop. Throws on an id that is not there. */
61
94
  suppress?: readonly string[];
95
+ /** Offer the header's add-trial control. Default `true`; a lab that opens
96
+ * trials some other way passes `false`. */
97
+ addTrial?: boolean;
62
98
  /** Tools offered lab-wide. A trial whose instrument declares none of its own
63
99
  * reflects and writes this slot. */
64
100
  tools?: readonly TrialTool[];
@@ -72,31 +108,89 @@ export interface LabProps {
72
108
  children?: ReactNode;
73
109
  }
74
110
 
75
- function buildStore(
111
+ /** Props for `<Lab>`. With a `storageKey` the lab persists — to IndexedDB
112
+ * unless `storage` names another substrate — and both are read once, at
113
+ * mount. Without one, nothing persists. */
114
+ export type LabProps = LabBaseProps &
115
+ (
116
+ | { storageKey?: undefined; storage?: undefined }
117
+ | { storageKey: string; storage?: StorageAdapter }
118
+ );
119
+
120
+ interface OpenedLab extends OpenedLabStore {
121
+ marks: ReadonlyMap<string, unknown>;
122
+ }
123
+
124
+ function seedDefaultTrial(
125
+ store: LabStore,
76
126
  instruments: InstrumentList,
77
127
  defaultInstrument: string,
78
- storage: StorageAdapter,
128
+ ): void {
129
+ if (store.getState().trials.length > 0) return;
130
+ const record = addTrialOp([], instruments, defaultInstrument)[0];
131
+ if (!record) return;
132
+ const { undoStack: _undoStack, ...rest } = record;
133
+ store.getState().addTrial(rest);
134
+ }
135
+
136
+ /** A lab with nothing to load renders at once. Its records hold
137
+ * `usePersistedState` values for the session and write nothing. */
138
+ function openUnstoredLab({ instruments, defaultInstrument, mode }: LabProps): OpenedLab {
139
+ const store = createLabStore({ initialMode: mode ?? 'auto', instruments });
140
+ seedDefaultTrial(store, instruments, defaultInstrument);
141
+ const records = createRecordCache({ storage: noneAdapter, prefix: '', writable: false });
142
+ return { store, records, close: () => records.close(), marks: new Map() };
143
+ }
144
+
145
+ async function openStoredLab(
146
+ { instruments, defaultInstrument, mode }: LabProps,
79
147
  storageKey: string,
80
- initialMode: LabMode,
81
- ): LabStore {
82
- // Hydration reads the serializers, so they go in with the store rather than
83
- // being registered onto it afterwards.
84
- const store = createLabStore({
148
+ storage: StorageAdapter | undefined,
149
+ ): Promise<OpenedLab> {
150
+ // The store reads the instruments' serializers and defaults while it is being
151
+ // built, so they go in with it rather than being pushed onto it afterwards.
152
+ const opened = await openLabStore({
85
153
  storageKey,
86
- storage,
87
- initialMode,
88
- serializers: serializersOf(instruments),
89
- configDefaults: configDefaultsOf(instruments),
154
+ storage: storage ?? (await defaultStorage()),
155
+ initialMode: mode ?? 'auto',
156
+ instruments,
90
157
  });
91
- if (store.getState().trials.length === 0) {
92
- const seeded = addTrialOp([], instruments, defaultInstrument);
93
- const record = seeded[0];
94
- if (record) {
95
- const { undoStack: _undoStack, ...rest } = record;
96
- store.getState().addTrial(rest);
97
- }
98
- }
99
- return store;
158
+ seedDefaultTrial(opened.store, instruments, defaultInstrument);
159
+ const marks = new Map<string, unknown>();
160
+ await Promise.all(
161
+ opened.store.getState().trials.map(async (trial) => {
162
+ const kept = instruments.find((i) => i.name === trial.instrumentName)?.annotations?.storage;
163
+ if (!kept) return;
164
+ try {
165
+ marks.set(trial.id, await kept.load());
166
+ } catch (error) {
167
+ console.warn(`[labkit] could not load the marks of trial "${trial.id}"`, error);
168
+ marks.set(trial.id, null);
169
+ }
170
+ }),
171
+ );
172
+ return { ...opened, marks };
173
+ }
174
+
175
+ function LabFallback({
176
+ title,
177
+ mode,
178
+ pages,
179
+ path,
180
+ }: Pick<LabBaseProps, 'title' | 'mode' | 'pages' | 'path'>) {
181
+ const resolvedMode = useResolvedMode(mode ?? 'auto');
182
+ return (
183
+ <ThemeProvider theme={interstellarTheme} selection={{ mode: resolvedMode }} className="lk-lab">
184
+ <LabShell
185
+ title={title ?? 'Labkit'}
186
+ mode={mode}
187
+ {...(pages ? { pages } : {})}
188
+ {...(path !== undefined ? { path } : {})}
189
+ >
190
+ {null}
191
+ </LabShell>
192
+ </ThemeProvider>
193
+ );
100
194
  }
101
195
 
102
196
  // Fixed spread of nebula blob positions / sizes / fall-off stops. Colors
@@ -120,41 +214,118 @@ function buildNebula(colors: readonly string[]): string {
120
214
  return blobs.join(', ');
121
215
  }
122
216
 
123
- /** The lab runtime: creates the store, provides it, and renders one trial
217
+ /** The strip holding the sidebar beside the tool rail and workspace. Its own component
218
+ * because the width is a persisted value, and `LabRuntime` renders the
219
+ * persistence provider it reads from. */
220
+ function LabPanes({
221
+ contributions,
222
+ children,
223
+ }: {
224
+ contributions: readonly LabContribution[];
225
+ children: ReactNode;
226
+ }) {
227
+ const surface = useSurfaceOptional();
228
+ const [sidebarWidth, setSidebarWidth] = usePersistedState<number | undefined>(
229
+ 'lk-lab-sidebar-width',
230
+ undefined,
231
+ { scope: 'lab' },
232
+ );
233
+ const [asideWidth, setAsideWidth] = usePersistedState<number | undefined>(
234
+ 'lk-lab-aside-width',
235
+ undefined,
236
+ { scope: 'lab' },
237
+ );
238
+ const main = contributions.some((c) => c.region === 'aside') ? (
239
+ <Split
240
+ className="lk-lab__panes"
241
+ side="end"
242
+ sidebarClassName="lk-lab__aside"
243
+ contentClassName="lk-lab__main"
244
+ label="Lab aside"
245
+ defaultWidth={320}
246
+ width={asideWidth}
247
+ onWidthChange={(w) => {
248
+ setAsideWidth(w);
249
+ surface?.invalidateRects();
250
+ }}
251
+ sidebar={<LabAsideRegion contributions={contributions} />}
252
+ >
253
+ {children}
254
+ </Split>
255
+ ) : (
256
+ children
257
+ );
258
+ if (!contributions.some((c) => c.region === 'sidebar')) return main;
259
+ return (
260
+ <Split
261
+ className="lk-lab__panes"
262
+ sidebarClassName="lk-lab__sidebar"
263
+ contentClassName="lk-lab__main"
264
+ label="Lab sidebar"
265
+ defaultWidth={260}
266
+ width={sidebarWidth}
267
+ onWidthChange={(w) => {
268
+ setSidebarWidth(w);
269
+ surface?.invalidateRects();
270
+ }}
271
+ sidebar={<LabSidebarRegion contributions={contributions} />}
272
+ >
273
+ {main}
274
+ </Split>
275
+ );
276
+ }
277
+
278
+ /** The lab runtime: opens the store, provides it, and renders one trial
124
279
  * per record in a grid. Each trial runs one of `instruments`. */
125
- export function Lab({
280
+ export function Lab(props: LabProps) {
281
+ if (process.env.NODE_ENV !== 'production' && props.instruments.length === 0) {
282
+ throw new Error('[labkit] <Lab> requires a non-empty `instruments` array');
283
+ }
284
+ const { storageKey, storage } = props;
285
+ useWarnIgnoredChange('<Lab>', { storageKey, storage });
286
+ const opened = useOpenOnce<OpenedLab>(() =>
287
+ storageKey === undefined ? openUnstoredLab(props) : openStoredLab(props, storageKey, storage),
288
+ );
289
+ if (!opened) {
290
+ if (props.fallback !== undefined) return props.fallback;
291
+ return (
292
+ <LabFallback title={props.title} mode={props.mode} pages={props.pages} path={props.path} />
293
+ );
294
+ }
295
+ return <LabRuntime {...props} opened={opened} />;
296
+ }
297
+
298
+ function LabRuntime({
126
299
  instruments,
127
- defaultInstrument,
128
- storage,
129
- storageKey,
130
300
  mode,
131
301
  nebula,
132
302
  title,
303
+ pages,
304
+ path,
133
305
  footer,
134
306
  chrome,
307
+ labChrome,
135
308
  suppress,
309
+ addTrial,
136
310
  tools,
137
311
  configRules,
138
312
  controls,
139
313
  children,
140
- }: LabProps) {
141
- if (process.env.NODE_ENV !== 'production' && instruments.length === 0) {
142
- throw new Error('[labkit] <Lab> requires a non-empty `instruments` array');
143
- }
314
+ opened,
315
+ }: LabProps & { opened: OpenedLab }) {
316
+ const { store } = opened;
144
317
 
145
- const storeRef = useRef<LabStore | null>(null);
146
- if (storeRef.current === null) {
147
- storeRef.current = buildStore(
148
- instruments,
149
- defaultInstrument,
150
- storage ?? noneAdapter,
151
- storageKey ?? 'labkit',
152
- mode ?? 'auto',
153
- );
154
- }
155
- const store = storeRef.current;
318
+ // The store opened against the list <Lab> mounted with; a later list reaches
319
+ // it here, before paint.
320
+ useLayoutEffect(() => {
321
+ store.getState().setInstruments(instruments);
322
+ }, [instruments, store]);
156
323
 
157
324
  const trials = useStore(store, (s) => s.trials);
325
+ const [focusPick, setFocusPick] = useState<string | null>(null);
326
+ const focusedTrialId = trials.some((t) => t.id === focusPick)
327
+ ? focusPick
328
+ : (trials[0]?.id ?? null);
158
329
  const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
159
330
  const modeValue = useStore(store, (s) => s.mode);
160
331
  const layout = useStore(store, (s) => s.layout);
@@ -167,38 +338,97 @@ export function Lab({
167
338
  if (panelHostsRef.current === null) panelHostsRef.current = createPanelHostRegistry();
168
339
 
169
340
  // One shared drawing surface for the whole lab, anchored to the body — tile
170
- // rects compose against it, and every tile paints into the one buffer.
341
+ // rects compose against it, and both buffers compose against the same rects.
171
342
  // `Workspace` invalidates rects when the grid moves something a
172
343
  // ResizeObserver cannot see. A host that already owns a surface keeps it: a
173
344
  // lab embedded in a larger shared-surface app must not open a second GL
174
345
  // tenancy, and mounts no buffer of its own.
175
346
  const outerSurface = useSurfaceOptional();
176
- const outerCanvas = useSurfaceCanvas();
177
- const [ownCanvas, setOwnCanvas] = useState<HTMLCanvasElement | null>(null);
178
- const ownCanvasRef = useRef<HTMLCanvasElement | null>(null);
347
+ const outerOver = useSurfaceCanvas('over');
348
+ const outerUnder = useSurfaceCanvas('under');
349
+ const [ownOver, setOwnOver] = useState<HTMLCanvasElement | null>(null);
350
+ const [ownUnder, setOwnUnder] = useState<HTMLCanvasElement | null>(null);
351
+ const ownOverRef = useRef<HTMLCanvasElement | null>(null);
352
+ const ownUnderRef = useRef<HTMLCanvasElement | null>(null);
179
353
  const bufferRef = useRef({ w: 0, h: 0 });
180
354
  const surfaceRef = useRef<ReturnType<typeof useTiledSurface> | null>(null);
181
355
 
182
356
  // Sizing the buffer clears all of it, so every tile has to repaint — not
183
- // only the one whose move triggered the measurement.
184
- const onFrame = useCallback((frame: { size: { width: number; height: number }; dpr: number }) => {
185
- const c = ownCanvasRef.current;
186
- if (!c) return;
357
+ // only the one whose move triggered the measurement. Both buffers are sized
358
+ // together off one comparison: they are the same box, so a tenant of either
359
+ // is looking at the same rects, and one invalidateAll covers both.
360
+ const onFrame = useCallback((frame: SurfaceFrame) => {
361
+ const canvases = [ownUnderRef.current, ownOverRef.current].filter((c) => c !== null);
362
+ if (canvases.length === 0) return;
187
363
  const w = Math.round(frame.size.width * frame.dpr);
188
364
  const h = Math.round(frame.size.height * frame.dpr);
189
- if (bufferRef.current.w === w && bufferRef.current.h === h) return;
190
- bufferRef.current = { w, h };
191
- c.width = w;
192
- c.height = h;
193
- c.style.width = `${frame.size.width}px`;
194
- c.style.height = `${frame.size.height}px`;
195
- surfaceRef.current?.invalidateAll();
365
+ if (bufferRef.current.w !== w || bufferRef.current.h !== h) {
366
+ bufferRef.current = { w, h };
367
+ for (const c of canvases) {
368
+ c.width = w;
369
+ c.height = h;
370
+ c.style.width = `${frame.size.width}px`;
371
+ c.style.height = `${frame.size.height}px`;
372
+ }
373
+ surfaceRef.current?.invalidateAll();
374
+ return;
375
+ }
376
+ // A same-size re-tile does not go through here: assigning `width` its own
377
+ // value resizes nothing, so it clears nothing. The tenants clear, through
378
+ // `registerClear`.
196
379
  }, []);
197
380
 
198
381
  const ownSurface = useTiledSurface({ onFrame });
199
382
  surfaceRef.current = ownSurface;
383
+ const [labBody, setLabBody] = useState<HTMLDivElement | null>(null);
384
+ const attachOwnSurface = ownSurface.containerRef;
385
+ const labBodyRef = useCallback(
386
+ (el: HTMLDivElement | null) => {
387
+ setLabBody(el);
388
+ if (!outerSurface) attachOwnSurface(el);
389
+ },
390
+ [outerSurface, attachOwnSurface],
391
+ );
392
+ useLabFitWarning(labBody);
393
+
394
+ useEffect(() => {
395
+ if (!labBody) return;
396
+ const pick = (node: unknown): void => {
397
+ const owned = new Set(store.getState().trials.map((t) => t.id));
398
+ const nearest = (from: Element | null | undefined) =>
399
+ from?.closest<HTMLElement>('.lk-trial[data-trial-id]') ?? null;
400
+ let trial =
401
+ typeof (node as Element | null)?.closest === 'function' ? nearest(node as Element) : null;
402
+ // A trial can hold a lab of its own, whose trials this lab does not own.
403
+ while (trial && !owned.has(trial.dataset.trialId ?? '')) trial = nearest(trial.parentElement);
404
+ if (trial?.dataset.trialId) setFocusPick(trial.dataset.trialId);
405
+ };
406
+ const onInput = (event: Event): void => pick(event.target);
407
+ // Focus moving into a frame fires nothing in this document: the window
408
+ // blurs, and the frame is the active element once it has.
409
+ let pending: ReturnType<typeof setTimeout> | undefined;
410
+ const doc = labBody.ownerDocument;
411
+ const win = doc.defaultView;
412
+ const onBlur = (): void => {
413
+ clearTimeout(pending);
414
+ pending = setTimeout(() => pick(doc.activeElement), 0);
415
+ };
416
+ labBody.addEventListener('pointerdown', onInput, true);
417
+ labBody.addEventListener('focusin', onInput, true);
418
+ win?.addEventListener('blur', onBlur);
419
+ return () => {
420
+ clearTimeout(pending);
421
+ labBody.removeEventListener('pointerdown', onInput, true);
422
+ labBody.removeEventListener('focusin', onInput, true);
423
+ win?.removeEventListener('blur', onBlur);
424
+ };
425
+ }, [labBody, store]);
200
426
  const surface = outerSurface ?? ownSurface;
201
- const surfaceCanvas = outerSurface ? outerCanvas : ownCanvas;
427
+ const surfaceCanvases = useMemo(
428
+ () =>
429
+ outerSurface ? { over: outerOver, under: outerUnder } : { over: ownOver, under: ownUnder },
430
+ [outerSurface, outerOver, outerUnder, ownOver, ownUnder],
431
+ );
202
432
 
203
433
  const workspacePanels = useMemo<PanelDescriptor[]>(
204
434
  () =>
@@ -211,6 +441,19 @@ export function Lab({
211
441
  );
212
442
  const resolvedMode = useResolvedMode(modeValue);
213
443
 
444
+ // Tools and lab contributions share one id namespace, so they merge once
445
+ // here — before any region renders — and a collision throws rather than one
446
+ // of them silently losing.
447
+ // Declaring `annotations` on any instrument puts the drawing tools here, in
448
+ // the lab's rail: one tool is armed across every trial.
449
+ const annotates = instruments.some((i) => i.annotations != null);
450
+ const labChromeAll = useMemo(
451
+ () => labContributions([...(annotates ? ANNOTATION_TOOLS : []), ...(tools ?? [])], labChrome),
452
+ [annotates, tools, labChrome],
453
+ );
454
+ const hasFooterChrome = labChromeAll.some((c) => c.region === 'footer');
455
+ const hasPaneChrome = labChromeAll.some((c) => c.region === 'sidebar' || c.region === 'aside');
456
+
214
457
  useEffect(() => {
215
458
  if (mode && mode !== store.getState().mode) {
216
459
  store.getState().setMode(mode);
@@ -222,20 +465,24 @@ export function Lab({
222
465
  const currentById = new Map(store.getState().trials.map((w) => [w.id, w]));
223
466
  const merged = next.map((w) => currentById.get(w.id) ?? w);
224
467
  store.setState({ trials: merged });
225
- // Trigger persistence flush via a tracked action.
226
- store.getState().setMode(store.getState().mode);
468
+ };
469
+ const replaceAndFocusAdded = (next: TrialRecord[]): void => {
470
+ const before = new Set(store.getState().trials.map((w) => w.id));
471
+ replaceTrials(next);
472
+ const added = next.find((w) => !before.has(w.id));
473
+ if (added) setFocusPick(added.id);
227
474
  };
228
475
 
229
476
  return {
230
477
  instruments,
231
478
  trials,
232
479
  addTrial: (instrumentName, options) => {
233
- const next = addTrialOp(store.getState().trials, instruments, instrumentName, options);
234
- replaceTrials(next);
480
+ replaceAndFocusAdded(
481
+ addTrialOp(store.getState().trials, instruments, instrumentName, options),
482
+ );
235
483
  },
236
484
  cloneTrial: (id) => {
237
- const next = cloneTrialOp(store.getState().trials, id);
238
- replaceTrials(next);
485
+ replaceAndFocusAdded(cloneTrialOp(store.getState().trials, id));
239
486
  },
240
487
  closeTrial: (id) => {
241
488
  const next = closeTrialOp(store.getState().trials, id);
@@ -244,6 +491,19 @@ export function Lab({
244
491
  reorderTrials: (ids) => {
245
492
  replaceTrials(reorderTrialsOp(store.getState().trials, ids));
246
493
  },
494
+ swapTrial: (id, instrumentName, options) => {
495
+ const current = store.getState();
496
+ const next = swapTrialOp(current.trials, instruments, id, instrumentName, options);
497
+ if (next === current.trials) return;
498
+ const record = next[current.trials.findIndex((w) => w.id === id)] as TrialRecord;
499
+ // Written before the tile registers, which is when the grid reads it.
500
+ const { [id]: extent, ...layoutRest } = current.layout as TrialLayout;
501
+ if (extent) current.setLayout({ ...layoutRest, [record.id]: extent });
502
+ replaceTrials(next);
503
+ if (focusedTrialId === id) setFocusPick(record.id);
504
+ },
505
+ focusedTrialId,
506
+ focusTrial: (id) => setFocusPick(id),
247
507
  resetTrial: (id) => {
248
508
  const next = resetTrialOp(store.getState().trials, id, instruments);
249
509
  const record = next.find((w) => w.id === id);
@@ -255,7 +515,6 @@ export function Lab({
255
515
  : w,
256
516
  ),
257
517
  }));
258
- store.getState().updateTrialView(id, record.view);
259
518
  },
260
519
  savedSnapshots,
261
520
  saveSnapshot: (trialId, name) => {
@@ -275,7 +534,16 @@ export function Lab({
275
534
  configRules,
276
535
  controls,
277
536
  };
278
- }, [instruments, trials, savedSnapshots, modeValue, store, configRules, controls]);
537
+ }, [
538
+ instruments,
539
+ trials,
540
+ focusedTrialId,
541
+ savedSnapshots,
542
+ modeValue,
543
+ store,
544
+ configRules,
545
+ controls,
546
+ ]);
279
547
 
280
548
  // Only override the backdrop in dark, where there is one to override.
281
549
  // Setting a CSS custom property is the sanctioned use of inline style.
@@ -284,66 +552,103 @@ export function Lab({
284
552
  ? ({ ['--wzl-backdrop' as string]: buildNebula(nebula) } as CSSProperties)
285
553
  : undefined;
286
554
 
555
+ const workspace = (
556
+ <Workspace
557
+ panels={workspacePanels}
558
+ ids={trials.map((w) => w.id)}
559
+ resizable
560
+ reorderable
561
+ onReorder={(ids) => contextValue.reorderTrials(ids)}
562
+ layout={layout as TrialLayout}
563
+ onLayoutChange={(next) => store.getState().setLayout(next)}
564
+ >
565
+ {trials.map((w) => (
566
+ <Trial key={w.id} id={w.id} chrome={chrome} suppress={suppress} />
567
+ ))}
568
+ </Workspace>
569
+ );
570
+
287
571
  return (
288
572
  <LabStoreContext.Provider value={{ store }}>
289
- <LabContext.Provider value={contextValue}>
290
- <ThemeProvider
291
- theme={interstellarTheme}
292
- mode={resolvedMode}
293
- className="lk-lab"
294
- style={backdropStyle}
295
- >
296
- <LabShell
297
- title={title ?? 'Labkit'}
298
- mode={modeValue}
299
- footer={footer}
300
- header={
301
- <>
302
- <LabHeader />
303
- {children}
304
- </>
305
- }
306
- >
307
- <PanelHostContext.Provider value={panelHostsRef.current}>
308
- <SurfaceContext.Provider value={surface}>
309
- <SurfaceCanvasContext.Provider value={surfaceCanvas}>
310
- <div
311
- className="lk-lab__body"
312
- ref={outerSurface ? undefined : ownSurface.containerRef}
313
- >
314
- {outerSurface ? null : (
315
- // Above the trials and inert: the marks a tile paints have
316
- // to sit over the instrument's own DOM, and nothing on this
317
- // buffer takes input — each tile has its own input box.
318
- <canvas
319
- className="lk-lab__surface"
320
- ref={(el) => {
321
- ownCanvasRef.current = el;
322
- setOwnCanvas(el);
323
- }}
324
- />
325
- )}
326
- {tools ? <LabPalette tools={tools} /> : null}
327
- <Workspace
328
- panels={workspacePanels}
329
- ids={trials.map((w) => w.id)}
330
- resizable
331
- reorderable
332
- onReorder={(ids) => contextValue.reorderTrials(ids)}
333
- layout={layout as TrialLayout}
334
- onLayoutChange={(next) => store.getState().setLayout(next)}
335
- >
336
- {trials.map((w) => (
337
- <Trial key={w.id} id={w.id} chrome={chrome} suppress={suppress} />
338
- ))}
339
- </Workspace>
340
- </div>
341
- </SurfaceCanvasContext.Provider>
342
- </SurfaceContext.Provider>
343
- </PanelHostContext.Provider>
344
- </LabShell>
345
- </ThemeProvider>
346
- </LabContext.Provider>
573
+ <PersistenceContext.Provider value={opened.records}>
574
+ <AnnotationPreloadContext.Provider value={opened.marks}>
575
+ <LabContext.Provider value={contextValue}>
576
+ <ThemeProvider
577
+ theme={interstellarTheme}
578
+ selection={{ mode: resolvedMode }}
579
+ className="lk-lab"
580
+ style={backdropStyle}
581
+ >
582
+ <LabShell
583
+ title={title ?? 'Labkit'}
584
+ mode={modeValue}
585
+ {...(pages ? { pages } : {})}
586
+ {...(path !== undefined ? { path } : {})}
587
+ footer={
588
+ hasFooterChrome ? (
589
+ <>
590
+ {footer}
591
+ <LabFooterRegion contributions={labChromeAll} />
592
+ </>
593
+ ) : (
594
+ footer
595
+ )
596
+ }
597
+ header={
598
+ <>
599
+ <LabHeader {...(addTrial !== undefined ? { addTrial } : {})} />
600
+ {children}
601
+ <LabHeaderRegion contributions={labChromeAll} />
602
+ </>
603
+ }
604
+ >
605
+ <PanelHostContext.Provider value={panelHostsRef.current}>
606
+ <SurfaceContext.Provider value={surface}>
607
+ <SurfaceCanvasContext.Provider value={surfaceCanvases}>
608
+ <div className="lk-lab__body" ref={labBodyRef}>
609
+ {outerSurface ? null : (
610
+ // Two buffers stacked around the trials, both inert —
611
+ // each tile takes input from its own box. A tile's marks
612
+ // annotate the instrument's DOM from over it; an opaque
613
+ // renderer sits under it, so the pane can still hold a
614
+ // label. The under one is first so it paints first.
615
+ <>
616
+ <canvas
617
+ className="lk-lab__surface lk-lab__surface--under"
618
+ ref={(el) => {
619
+ ownUnderRef.current = el;
620
+ setOwnUnder(el);
621
+ }}
622
+ />
623
+ <canvas
624
+ className="lk-lab__surface lk-lab__surface--over"
625
+ ref={(el) => {
626
+ ownOverRef.current = el;
627
+ setOwnOver(el);
628
+ }}
629
+ />
630
+ </>
631
+ )}
632
+ {hasPaneChrome ? (
633
+ <LabPanes contributions={labChromeAll}>
634
+ <LabPalette contributions={labChromeAll} />
635
+ {workspace}
636
+ </LabPanes>
637
+ ) : (
638
+ <>
639
+ <LabPalette contributions={labChromeAll} />
640
+ {workspace}
641
+ </>
642
+ )}
643
+ </div>
644
+ </SurfaceCanvasContext.Provider>
645
+ </SurfaceContext.Provider>
646
+ </PanelHostContext.Provider>
647
+ </LabShell>
648
+ </ThemeProvider>
649
+ </LabContext.Provider>
650
+ </AnnotationPreloadContext.Provider>
651
+ </PersistenceContext.Provider>
347
652
  </LabStoreContext.Provider>
348
653
  );
349
654
  }