@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,10 @@
1
1
  import { describe, expect, it } from 'vitest';
2
+ import { auto } from '../config/auto';
3
+ import { f } from '../config/builder';
4
+ import { defineInstrument } from '../instrument/defineInstrument';
2
5
  import type { Instrument } from '../instrument/types';
3
6
  import type { TrialRecord } from '../state/types';
4
- import { addTrial, cloneTrial, closeTrial, reorderTrials, resetTrial } from './trialOps';
7
+ import { addTrial, cloneTrial, closeTrial, reorderTrials, resetTrial, swapTrial } from './trialOps';
5
8
 
6
9
  interface CounterState {
7
10
  count: number;
@@ -38,10 +41,22 @@ const subjectInstrument: Instrument<{ label: string }, SubjectConfig> = {
38
41
  render: () => null,
39
42
  };
40
43
 
44
+ const schemaInstrument = defineInstrument({
45
+ name: 'Schemed',
46
+ config: f.schema({
47
+ cols: f.number(3).initial(auto),
48
+ gap: f.number(12),
49
+ grid: f.group({ size: f.number(4) }),
50
+ }),
51
+ initialState: () => ({}),
52
+ render: () => null,
53
+ });
54
+
41
55
  const instruments: Instrument[] = [
42
56
  counter as Instrument,
43
57
  counterWithCanvas as Instrument,
44
58
  subjectInstrument as Instrument,
59
+ schemaInstrument as Instrument,
45
60
  ];
46
61
 
47
62
  function head<T>(arr: T[]): T {
@@ -107,6 +122,38 @@ describe('addTrial', () => {
107
122
  const out = addTrial([], instruments, 'Subject', { config: { subject: undefined } });
108
123
  expect(out[0]?.config).toEqual({ subject: 'none', zoom: 1 });
109
124
  });
125
+
126
+ it('opens a trial with the schema-declared auto paths already unpinned', () => {
127
+ const out = addTrial([], instruments, 'Schemed');
128
+ expect(out[0]?.auto).toEqual(['cols']);
129
+ });
130
+
131
+ it('honors auto in a seed config and keeps the sentinel out of the record', () => {
132
+ const out = addTrial([], instruments, 'Schemed', { config: { gap: auto } });
133
+ expect(out[0]?.auto).toContain('gap');
134
+ expect(out[0]?.autoSeed).toEqual(['gap']);
135
+ expect((out[0]?.config as { gap: number }).gap).toBe(12);
136
+ expect(out[0]?.configSeed).not.toHaveProperty('gap');
137
+ });
138
+
139
+ it('keeps a seed value that is an object but not a config branch', () => {
140
+ const when = new Date('2020-01-01T00:00:00Z');
141
+ const out = addTrial([], instruments, 'Counter', {
142
+ config: { at: when } as unknown as Partial<CounterConfig>,
143
+ });
144
+ expect((out[0]?.config as { at: unknown }).at).toBe(when);
145
+ });
146
+
147
+ it('honors auto at a nested seed path', () => {
148
+ const out = addTrial([], instruments, 'Schemed', { config: { grid: { size: auto } } });
149
+ expect(out[0]?.auto).toContain('grid.size');
150
+ expect((out[0]?.config as { grid: { size: number } }).grid.size).toBe(4);
151
+ });
152
+
153
+ it('leaves a trial with no auto field when nothing is unpinned', () => {
154
+ const out = addTrial([], instruments, 'Counter');
155
+ expect(out[0]?.auto).toBeUndefined();
156
+ });
110
157
  });
111
158
 
112
159
  describe('cloneTrial', () => {
@@ -187,6 +234,20 @@ describe('resetTrial', () => {
187
234
  expect(reset[0]?.state).toEqual({ label: 'ONE' });
188
235
  expect(reset[1]?.config).toEqual({ subject: 'two', zoom: 1 });
189
236
  });
237
+
238
+ it('un-pins the paths the trial opened auto again, from schema and from seed', () => {
239
+ const arr = addTrial([], instruments, 'Schemed', { config: { gap: auto } });
240
+ head(arr).auto = [];
241
+ const reset = resetTrial(arr, head(arr).id, instruments);
242
+ expect([...(reset[0]?.auto ?? [])].sort()).toEqual(['cols', 'gap']);
243
+ });
244
+
245
+ it('pins a path the user un-pinned that the trial did not open on', () => {
246
+ const arr = addTrial([], instruments, 'Schemed');
247
+ head(arr).auto = ['cols', 'grid.size'];
248
+ const reset = resetTrial(arr, head(arr).id, instruments);
249
+ expect(reset[0]?.auto).toEqual(['cols']);
250
+ });
190
251
  });
191
252
 
192
253
  describe('reorderTrials', () => {
@@ -236,3 +297,42 @@ describe('a size-aware initial view', () => {
236
297
  expect(trial?.view).toBeNull();
237
298
  });
238
299
  });
300
+
301
+ describe('swapTrial', () => {
302
+ const instruments = [counter, subjectInstrument];
303
+ const three = (): TrialRecord[] =>
304
+ addTrial(
305
+ addTrial(addTrial([], instruments, 'Counter'), instruments, 'Counter'),
306
+ instruments,
307
+ 'Counter',
308
+ );
309
+
310
+ it('puts a fresh trial of the other instrument in the same place', () => {
311
+ const trials = three();
312
+ const target = trials[1] as TrialRecord;
313
+ const next = swapTrial(trials, instruments, target.id, 'Subject', {
314
+ config: { subject: 'moon' },
315
+ });
316
+ expect(next).toHaveLength(3);
317
+ expect(next[0]).toBe(trials[0]);
318
+ expect(next[2]).toBe(trials[2]);
319
+ const swapped = next[1] as TrialRecord;
320
+ expect(swapped.id).not.toBe(target.id);
321
+ expect(swapped.instrumentName).toBe('Subject');
322
+ expect(swapped.config).toEqual({ subject: 'moon', zoom: 1 });
323
+ expect(swapped.state).toEqual({ label: 'MOON' });
324
+ expect(swapped.configSeed).toEqual({ subject: 'moon' });
325
+ expect(swapped.undoStack).toEqual({ past: [], future: [] });
326
+ });
327
+
328
+ it('keeps the width the trial’s sidebar was dragged to', () => {
329
+ const trials = three().map((t, i) => (i === 1 ? { ...t, sidebarWidth: 312 } : t));
330
+ const next = swapTrial(trials, instruments, (trials[1] as TrialRecord).id, 'Subject');
331
+ expect(next[1]?.sidebarWidth).toBe(312);
332
+ });
333
+
334
+ it('returns the list untouched for an id it does not hold', () => {
335
+ const trials = three();
336
+ expect(swapTrial(trials, instruments, 'nope', 'Subject')).toBe(trials);
337
+ });
338
+ });
@@ -1,7 +1,10 @@
1
- import { fillConfigDefaults } from '../config/path';
1
+ import { isAuto } from '../config/auto';
2
+ import { autoPathsOf } from '../config/autoConfig';
3
+ import { fillConfigDefaults, isRecord } from '../config/path';
4
+ import { unruledConfigSchema } from '../instrument/serializers';
2
5
  import type { Instrument, InstrumentList } from '../instrument/types';
3
- import type { TrialRecord } from '../state/types';
4
6
  import { newId } from '../state/helpers';
7
+ import type { TrialRecord } from '../state/types';
5
8
 
6
9
  const DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } } as const;
7
10
 
@@ -14,10 +17,12 @@ function findInstrument(instruments: InstrumentList, name: string): Instrument {
14
17
  }
15
18
 
16
19
  function initialView(instrument: Instrument): TrialRecord['view'] {
17
- const declared = instrument.canvas?.initialView;
20
+ const stage = instrument.canvas ? undefined : instrument.stage;
21
+ const declared = instrument.canvas?.initialView ?? stage?.initialView;
18
22
  // A function needs the canvas size, which no trial has at creation. `null`
19
23
  // is the unplaced marker; `Trial` resolves it on the first non-empty size.
20
- if (typeof declared === 'function') return null;
24
+ // A stage that names no view is fitted, which needs that size too.
25
+ if (typeof declared === 'function' || (stage && declared === undefined)) return null;
21
26
  return declared ? structuredClone(declared) : { ...DEFAULT_VIEW, pan: { ...DEFAULT_VIEW.pan } };
22
27
  }
23
28
 
@@ -35,6 +40,35 @@ function seedConfig<TC>(defaults: TC, seed: Partial<TC> | undefined): TC {
35
40
  return seed ? fillConfigDefaults(seed, defaults) : defaults;
36
41
  }
37
42
 
43
+ /** Splits a seed config into its ordinary values and the dotted paths it wrote
44
+ * as `auto`, so the sentinel never reaches the record. */
45
+ function splitAutoSeed(
46
+ seed: Record<string, unknown> | undefined,
47
+ at = '',
48
+ ): { config: Record<string, unknown>; autoPaths: string[] } {
49
+ const config: Record<string, unknown> = {};
50
+ const autoPaths: string[] = [];
51
+ for (const [key, value] of Object.entries(seed ?? {})) {
52
+ const path = at === '' ? key : `${at}.${key}`;
53
+ if (isAuto(value)) {
54
+ autoPaths.push(path);
55
+ } else if (isRecord(value)) {
56
+ const inner = splitAutoSeed(value as Record<string, unknown>, path);
57
+ config[key] = inner.config;
58
+ autoPaths.push(...inner.autoPaths);
59
+ } else {
60
+ config[key] = value;
61
+ }
62
+ }
63
+ return { config, autoPaths };
64
+ }
65
+
66
+ /** The paths an instrument's own schema declares as starting auto. */
67
+ function schemaAutoPaths(instrument: Instrument): string[] {
68
+ const schema = unruledConfigSchema(instrument);
69
+ return schema ? autoPathsOf(schema) : [];
70
+ }
71
+
38
72
  /** Append a new trial running `instrumentName`, at that instrument's default
39
73
  * config — with `options.config` written over it — and the initial state
40
74
  * that config produces. */
@@ -45,7 +79,9 @@ export function addTrial(
45
79
  options: AddTrialOptions = {},
46
80
  ): TrialRecord[] {
47
81
  const instrument = findInstrument(instruments, instrumentName);
48
- const config = seedConfig(instrument.defaultConfig(), options.config);
82
+ const seeded = splitAutoSeed(options.config as Record<string, unknown> | undefined);
83
+ const autoPaths = [...new Set([...schemaAutoPaths(instrument), ...seeded.autoPaths])];
84
+ const config = seedConfig(instrument.defaultConfig(), seeded.config);
49
85
  const state = instrument.initialState(config);
50
86
  const record: TrialRecord = {
51
87
  id: newId(),
@@ -55,7 +91,9 @@ export function addTrial(
55
91
  view: initialView(instrument),
56
92
  undoStack: { past: [], future: [] },
57
93
  };
58
- if (options.config) record.configSeed = options.config;
94
+ if (options.config) record.configSeed = seeded.config;
95
+ if (seeded.autoPaths.length > 0) record.autoSeed = seeded.autoPaths;
96
+ if (autoPaths.length > 0) record.auto = autoPaths;
59
97
  return [...trials, record];
60
98
  }
61
99
 
@@ -97,15 +135,37 @@ export function resetTrial(
97
135
  const instrument = findInstrument(instruments, current.instrumentName);
98
136
  const config = seedConfig(instrument.defaultConfig(), current.configSeed);
99
137
  const state = instrument.initialState(config);
138
+ const autoPaths = [...new Set([...schemaAutoPaths(instrument), ...(current.autoSeed ?? [])])];
139
+ const { auto: _unpinnedNow, ...kept } = current;
100
140
  const reset: TrialRecord = {
101
- ...current,
141
+ ...kept,
102
142
  config,
103
143
  state,
104
144
  view: initialView(instrument),
105
145
  };
146
+ if (autoPaths.length > 0) reset.auto = autoPaths;
106
147
  return [...trials.slice(0, idx), reset, ...trials.slice(idx + 1)];
107
148
  }
108
149
 
150
+ /** Put a fresh trial running `instrumentName` where trial `id` is. It gets a
151
+ * new id, since the history, snapshots and marks keyed by the old one belong
152
+ * to what that trial was running, but keeps the width its sidebar was
153
+ * dragged to. */
154
+ export function swapTrial(
155
+ trials: TrialRecord[],
156
+ instruments: InstrumentList,
157
+ id: string,
158
+ instrumentName: string,
159
+ options: AddTrialOptions = {},
160
+ ): TrialRecord[] {
161
+ const idx = trials.findIndex((w) => w.id === id);
162
+ const current = trials[idx];
163
+ if (!current) return trials;
164
+ const [record] = addTrial([], instruments, instrumentName, options) as [TrialRecord];
165
+ if (current.sidebarWidth !== undefined) record.sidebarWidth = current.sidebarWidth;
166
+ return [...trials.slice(0, idx), record, ...trials.slice(idx + 1)];
167
+ }
168
+
109
169
  /**
110
170
  * Reorder to match `ids`. Ids the list doesn't mention keep their relative
111
171
  * order at the end, and ids it names that no longer exist are dropped — a
@@ -1,155 +0,0 @@
1
- import { C as ConfigPath, V as ValueAtPath } from './types-B9_zrHmb.js';
2
-
3
- /** A sidebar section torn out of its trial, and where it went. */
4
- interface UndockedPanel {
5
- trialId: string;
6
- sectionId: string;
7
- as: 'tile' | 'floating';
8
- }
9
- /** Undocked panels, keyed by {@link panelKey}. */
10
- type UndockedPanels = Record<string, UndockedPanel>;
11
- declare function panelKey(trialId: string, sectionId: string): string;
12
- declare function undockPanel(panels: UndockedPanels, trialId: string, sectionId: string, as?: UndockedPanel['as']): UndockedPanels;
13
- /** Dock one section back, or — with no `sectionId` — every panel the trial
14
- * owns, which is what closing a trial wants. */
15
- declare function dockPanel(panels: UndockedPanels, trialId: string, sectionId?: string): UndockedPanels;
16
-
17
- /** A trial's undo history, as snapshots of its state either side of the
18
- * present. */
19
- interface UndoStack {
20
- past: unknown[];
21
- future: unknown[];
22
- }
23
- /** The trial a per-trial call is coming from. `id` is the id `useTileId`
24
- * scopes a surface tile under, so a consumer keying its own per-trial
25
- * registry and labkit's tiles agree on the key. */
26
- interface TrialInfo<TV = unknown> {
27
- id: string;
28
- /** The trial's view, in whatever shape the instrument keeps it. */
29
- view: TV;
30
- }
31
- /** One trial as the store holds it: which instrument it runs, that
32
- * instrument's config and state, the camera, and the undo history. */
33
- interface TrialRecord<TS = unknown, TC = unknown, TV = unknown> {
34
- id: string;
35
- instrumentName: string;
36
- config: TC;
37
- state: TS;
38
- /** Opaque to labkit: persisted, restored on Reset and handed to the instrument,
39
- * but never read into. A 3D lab puts an orbit here and keeps all three. */
40
- view: TV;
41
- /** The config `addTrial` opened this trial on, overlaid on the instrument's
42
- * defaults. Kept so Reset restores the trial's own subject rather than the
43
- * bare defaults. */
44
- configSeed?: Partial<TC>;
45
- /** This trial's own tool slot. Undefined means it reads the lab's. */
46
- activeToolId?: string | null;
47
- /** The extent the trial's sidebar was last dragged to, in pixels. Undefined
48
- * until someone moves the seam. */
49
- sidebarWidth?: number;
50
- /** What the title bar reads. Undefined — the state a trial opens in, and the
51
- * one `setTitle(null)` returns it to — means the instrument's name. */
52
- title?: string | null;
53
- /** Which of the trial's collapsible sections are folded, keyed as
54
- * `TrialChromeContext.collapsedSections` describes. A key that is absent
55
- * takes the section's own default. */
56
- collapsedSections?: Record<string, boolean>;
57
- /** The marks on this trial's annotation targets, as `AnnotationsApi.toJSON`
58
- * wrote them. Opaque here, and absent for a trial whose instrument declares
59
- * no `annotations` — or declares its own `storage`. Not in `state`, which
60
- * belongs to the instrument and is typed as such. */
61
- annotations?: unknown;
62
- undoStack: UndoStack;
63
- }
64
- /** A named, saved copy of a trial's config and state, restorable into any
65
- * trial running the same instrument. */
66
- interface SavedSnapshot {
67
- id: string;
68
- name: string;
69
- trialId: string;
70
- instrumentName: string;
71
- config: unknown;
72
- state: unknown;
73
- savedAt: number;
74
- }
75
- /** `auto` follows the OS; the other two are an explicit choice. */
76
- type LabMode = 'auto' | 'light' | 'dark';
77
- /** Everything a lab persists: its trials, its saved snapshots, and the
78
- * chosen color mode. */
79
- interface LabStoreState {
80
- trials: TrialRecord[];
81
- savedSnapshots: SavedSnapshot[];
82
- mode: LabMode;
83
- /** The lab's tool slot — what a trial with no slot of its own resolves to. */
84
- activeToolId: string | null;
85
- /** Per-trial tile extents, keyed by trial id. Opaque here — the
86
- * shape belongs to whatever lays the trials out. */
87
- layout: Record<string, unknown>;
88
- /** Sidebar sections torn out of their trial. A panel here is not rendered in
89
- * its trial's sidebar; the workspace renders it instead. */
90
- undockedPanels: UndockedPanels;
91
- }
92
- /** Where a lab persists itself. Implementations are keyed string storage and
93
- * nothing more, so the same store works against localStorage, the URL hash,
94
- * or memory. */
95
- interface StorageAdapter {
96
- read(key: string): string | null;
97
- write(key: string, value: string): void;
98
- delete?(key: string): void;
99
- }
100
- /** What `useTrialState` hands an instrument: its state and config, with
101
- * a setter for each. */
102
- interface TrialStateHandle<TS, TC> {
103
- state: TS;
104
- setState: (next: TS | ((prev: TS) => TS)) => void;
105
- config: TC;
106
- /** Write one config value, by dotted path — `'grid.size'` for a leaf under
107
- * an `f.group`, `'cellSize'` for one at the root. */
108
- setConfig: <P extends ConfigPath<TC> & string>(path: P, value: ValueAtPath<TC, P>) => void;
109
- }
110
- /** Options for `createLabStore`. `storageKey` namespaces the keys written, so
111
- * two labs on one origin do not collide. */
112
- interface CreateLabStoreOptions {
113
- storageKey: string;
114
- storage: StorageAdapter;
115
- initialMode?: LabMode;
116
- /** Each instrument's default config, keyed by instrument name, used to fill
117
- * the gaps in a stored one. A config saved before its schema grew a branch
118
- * arrives holding that branch's defaults rather than `undefined`, and keeps
119
- * whatever keys the schema has since stopped naming. `<Lab>` collects these
120
- * off its `instruments`. */
121
- configDefaults?: Record<string, () => unknown>;
122
- /** How each instrument's state survives a reload. Hydration is the first
123
- * thing `createLabStore` does, so these have to arrive with the store —
124
- * anything registered afterwards is already too late to read the document
125
- * it was built from. `<Lab>` collects them off its `instruments`. */
126
- serializers?: InstrumentSerializers;
127
- }
128
- /** Per-instrument serialize/deserialize hooks, keyed by instrument name. An
129
- * instrument whose state is already JSON-safe needs no entry. `deserialize`
130
- * is handed the config the state was saved against — a trial's own for a
131
- * reload, the snapshot's for a load — since a state rebuilt without it can
132
- * disagree with the settings sitting next to it. */
133
- type InstrumentSerializers = Record<string, {
134
- serialize?: (state: unknown) => unknown;
135
- deserialize?: (data: unknown, config: unknown) => unknown;
136
- } | undefined>;
137
- /** A trial as it is persisted: everything but the undo history, which is
138
- * session-only. */
139
- type SerializedTrial = Omit<TrialRecord, 'undoStack'>;
140
- /** Everything a lab persists, under one key, at a known version. */
141
- interface LabDocument {
142
- version: number;
143
- trials: SerializedTrial[];
144
- saves: SavedSnapshot[];
145
- layout: Record<string, unknown>;
146
- /** Sidebar sections torn out of their trial, keyed by trial and section. */
147
- undockedPanels: UndockedPanels;
148
- mode: LabMode;
149
- }
150
- /** Migrates a document one version forward. Index `i` in the chain takes a
151
- * version-`i` document to version `i + 1`. */
152
- type Migration = (doc: Record<string, unknown>) => Record<string, unknown>;
153
-
154
- export { dockPanel as g, panelKey as p, undockPanel as u };
155
- export type { CreateLabStoreOptions as C, InstrumentSerializers as I, LabDocument as L, Migration as M, SerializedTrial as S, TrialRecord as T, UndoStack as U, StorageAdapter as a, LabStoreState as b, SavedSnapshot as c, TrialStateHandle as d, UndockedPanel as e, UndockedPanels as f, LabMode as h, TrialInfo as i };
@@ -1,62 +0,0 @@
1
- /** Which control a config field is edited with. */
2
- type ConfigFieldType = 'slider' | 'checkbox' | 'select' | 'number' | 'text' | 'color';
3
- /** What every config field carries: the config key it writes, the label shown
4
- * beside it, and which control renders it. */
5
- interface ConfigFieldBase {
6
- key: string;
7
- label: string;
8
- type: ConfigFieldType;
9
- }
10
- /** A bounded number edited by dragging. */
11
- interface SliderField extends ConfigFieldBase {
12
- type: 'slider';
13
- default: number;
14
- min: number;
15
- max: number;
16
- step?: number;
17
- }
18
- /** A boolean. */
19
- interface CheckboxField extends ConfigFieldBase {
20
- type: 'checkbox';
21
- default: boolean;
22
- }
23
- /** One choice in a select field. */
24
- interface SelectOption {
25
- value: string;
26
- label: string;
27
- }
28
- /** A fixed set of labeled choices. */
29
- interface SelectField extends ConfigFieldBase {
30
- type: 'select';
31
- default: string;
32
- options: SelectOption[];
33
- }
34
- /** A number typed directly, optionally bounded. Use a slider field instead
35
- * when the range matters more than the exact value. */
36
- interface NumberField extends ConfigFieldBase {
37
- type: 'number';
38
- default: number;
39
- min?: number;
40
- max?: number;
41
- step?: number;
42
- }
43
- /** A free-text string. Writes are debounced so typing does not re-run the
44
- * instrument on every keystroke. */
45
- interface TextField extends ConfigFieldBase {
46
- type: 'text';
47
- default: string;
48
- placeholder?: string;
49
- maxLength?: number;
50
- /** Milliseconds to debounce live setConfig calls. Default 150 ms. Set to 0 to disable. */
51
- debounceMs?: number;
52
- }
53
- /** A color, as a CSS color string. */
54
- interface ColorField extends ConfigFieldBase {
55
- type: 'color';
56
- default: string;
57
- }
58
- /** One field of an instrument's config schema. The schema is what the control
59
- * panel renders, and what `validateConfigSchema` checks. */
60
- type ConfigField = SliderField | CheckboxField | SelectField | NumberField | TextField | ColorField;
61
-
62
- export type { ConfigField as C, NumberField as N, SelectField as S, TextField as T, CheckboxField as a, ColorField as b, ConfigFieldBase as c, ConfigFieldType as d, SelectOption as e, SliderField as f };
@@ -1,99 +0,0 @@
1
- import { a as StorageAdapter, b as LabStoreState, T as TrialRecord, c as SavedSnapshot, h as LabMode, e as UndockedPanel, C as CreateLabStoreOptions, d as TrialStateHandle } from './types-BP2OCcpg.js';
2
- import * as react from 'react';
3
- import { ReactNode, ReactElement } from 'react';
4
- import { StoreApi } from 'zustand/vanilla';
5
-
6
- /** Persist to `localStorage` — state survives a reload and a new tab. Falls
7
- * back to no-ops when storage is unavailable. */
8
- declare const localStorageAdapter: StorageAdapter;
9
- /** Persist to `sessionStorage` — state survives a reload but not a new tab. */
10
- declare const sessionStorageAdapter: StorageAdapter;
11
- /** Persist into the URL fragment, so the page's link carries its state and
12
- * can be shared or bookmarked. */
13
- declare const urlHashAdapter: StorageAdapter;
14
- /** An in-memory store, discarded on reload. For tests, and for labs that
15
- * should start fresh every time. */
16
- declare function createMemoryAdapter(): StorageAdapter;
17
- /** Persists nothing and reads back nothing. */
18
- declare const noneAdapter: StorageAdapter;
19
-
20
- /** Every mutation a lab store supports: managing trials, saving and
21
- * restoring snapshots, and setting the color mode. */
22
- interface LabStoreActions {
23
- addTrial: (record: Omit<TrialRecord, 'undoStack'>) => void;
24
- removeTrial: (id: string) => void;
25
- updateTrialState: <TS>(id: string, next: TS | ((prev: TS) => TS)) => void;
26
- /** Write one config value. `path` is dotted for a value nested under an
27
- * `f.group`, and the bare key for one at the root. */
28
- updateTrialConfig: (id: string, path: string, value: unknown) => void;
29
- updateTrialView: (id: string, view: unknown) => void;
30
- updateTrialSidebarWidth: (id: string, width: number) => void;
31
- /** Retitle a trial. `null` returns it to its instrument's name. */
32
- setTrialTitle: (id: string, title: string | null) => void;
33
- /** Fold or unfold one of a trial's sections. */
34
- setTrialSectionCollapsed: (id: string, key: string, collapsed: boolean) => void;
35
- updateTrialAnnotations: (id: string, doc: unknown) => void;
36
- updateTrialUndoStack: (id: string, next: TrialRecord['undoStack'] | ((prev: TrialRecord['undoStack']) => TrialRecord['undoStack'])) => void;
37
- setTrialInstrument: (id: string, instrumentName: string) => void;
38
- saveSnapshot: (trialId: string, name: string) => void;
39
- loadSnapshot: (snapshotId: string, trialId: string) => void;
40
- deleteSnapshot: (snapshotId: string) => void;
41
- listSnapshots: (trialId?: string) => SavedSnapshot[];
42
- setMode: (mode: LabMode) => void;
43
- setLabTool: (id: string | null) => void;
44
- setTrialTool: (trialId: string, id: string | null) => void;
45
- setLayout: (layout: Record<string, unknown>) => void;
46
- /** Tear a sidebar section out of its trial. Default target is a workspace tile. */
47
- undockPanel: (trialId: string, sectionId: string, as?: UndockedPanel['as']) => void;
48
- /** Put one section back, or — with no `sectionId` — every panel the trial owns. */
49
- dockPanel: (trialId: string, sectionId?: string) => void;
50
- }
51
- /** A lab's store: its state and its actions. How each instrument's state is
52
- * serialized comes in through `CreateLabStoreOptions.serializers`. */
53
- type LabStore = StoreApi<LabStoreState & LabStoreActions>;
54
- /** Build a lab store, hydrating from storage if anything was saved under the
55
- * same key. Writes back are debounced. */
56
- declare function createLabStore(options: CreateLabStoreOptions): LabStore;
57
-
58
- type LabStoreCtx = {
59
- store: LabStore;
60
- } | null;
61
- /** Context carrying the lab store. Prefer `useLabStore`; this is exported for
62
- * code that needs to read the context without subscribing. */
63
- declare const LabStoreContext: react.Context<LabStoreCtx>;
64
- /** Provides a lab store to its subtree. */
65
- declare function LabStoreProvider({ store, children, }: {
66
- store: LabStore;
67
- children: ReactNode;
68
- }): ReactElement;
69
- /** Subscribe to the whole lab store. Throws outside a `<LabStoreProvider>`. */
70
- declare function useLabStore(): LabStoreState & ReturnType<LabStore['getState']>;
71
- /** Context carrying which trial the subtree belongs to. */
72
- declare const TrialIdContext: react.Context<string | null>;
73
- /** Names the trial its subtree belongs to, so an instrument's hooks can
74
- * find their own record in the store without being passed an id. */
75
- declare function TrialIdProvider({ trialId, children, }: {
76
- trialId: string;
77
- children: ReactNode;
78
- }): ReactElement;
79
- /** The id of the trial this component is inside. Throws outside a
80
- * `<TrialIdProvider>`. */
81
- declare function useTrialId(): string;
82
-
83
- /** Bumped whenever the persisted shape changes; every bump needs a migration. */
84
- declare const CURRENT_DOCUMENT_VERSION = 3;
85
- /** The one key a lab persists under. The `:doc` suffix keeps it out of the
86
- * legacy bucket namespace, where a lab named `a:saves` would otherwise write
87
- * its document over lab `a`'s saves bucket. */
88
- declare function labDocumentKey(storageKey: string): string;
89
- /** Where a document that failed to migrate is set aside, so a bad migration
90
- * loses state loudly rather than silently. */
91
- declare function quarantineKey(storageKey: string): string;
92
-
93
- /** An instrument's own state and config, plus setters. Reads the surrounding
94
- * trial id, so an instrument never has to know which trial it is
95
- * running in. Throws outside a lab store and trial. */
96
- declare function useTrialState<TS = unknown, TC = unknown>(): TrialStateHandle<TS, TC>;
97
-
98
- export { CURRENT_DOCUMENT_VERSION as C, TrialIdContext as T, LabStoreContext as b, LabStoreProvider as c, TrialIdProvider as d, createLabStore as e, createMemoryAdapter as f, localStorageAdapter as g, useLabStore as h, useTrialId as i, useTrialState as j, labDocumentKey as l, noneAdapter as n, quarantineKey as q, sessionStorageAdapter as s, urlHashAdapter as u };
99
- export type { LabStore as L, LabStoreActions as a };