@weasel-js/labkit 1.5.2 → 1.6.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 (217) hide show
  1. package/README.md +3 -7
  2. package/dist/_dts/{CanvasStackContext-CMMo180l.d.ts → CanvasStackContext-Wo6OwIVM.d.ts} +1 -1
  3. package/dist/_dts/FloatingPanel-7j0IcXwz.d.ts +30 -0
  4. package/dist/_dts/LayerList.d-BoELAMsM.d.ts +83 -0
  5. package/dist/_dts/{PrefsForm.d-DPy9I8It.d.ts → PrefsRow.d-DFpPIeIC.d.ts} +16 -1
  6. package/dist/_dts/PropertyField.d-D0ndfIIt.d.ts +404 -0
  7. package/dist/_dts/{Select.d-ZTiwzYyT.d.ts → Select.d-Cr8-zoup.d.ts} +105 -15
  8. package/dist/_dts/{usePanZoom-SSTZe410.d.ts → Stage-DWBBi8IU.d.ts} +80 -35
  9. package/dist/_dts/{index-DlKolEIZ.d.ts → index-CKyX0ME_.d.ts} +12 -13
  10. package/dist/_dts/{frac-B4hFJMiN.d.ts → types-B7yBTTF9.d.ts} +709 -651
  11. package/dist/_dts/{types-Db-3NNk1.d.ts → types-DTuViBvc.d.ts} +21 -2
  12. package/dist/_dts/{useTrialState-pnAE-UBD.d.ts → useTrialState-CMaV6UD4.d.ts} +2 -3
  13. package/dist/canvas/index.d.ts +41 -8
  14. package/dist/canvas/index.js +5 -3
  15. package/dist/chrome/index.d.ts +6 -6
  16. package/dist/chrome/index.js +9 -7
  17. package/dist/chunk-24W5VEJW.js +18 -0
  18. package/dist/chunk-24W5VEJW.js.map +1 -0
  19. package/dist/chunk-2RVCB2PT.js +3 -0
  20. package/dist/chunk-2RVCB2PT.js.map +1 -0
  21. package/dist/chunk-3RY6WL5T.js +38 -0
  22. package/dist/chunk-3RY6WL5T.js.map +1 -0
  23. package/dist/{chunk-CKUKF72R.js → chunk-4TT3PNPR.js} +68 -55
  24. package/dist/chunk-4TT3PNPR.js.map +1 -0
  25. package/dist/{chunk-KJCUERQF.js → chunk-6FLXL3I7.js} +9 -164
  26. package/dist/chunk-6FLXL3I7.js.map +1 -0
  27. package/dist/chunk-ASOK76ZH.js +431 -0
  28. package/dist/chunk-ASOK76ZH.js.map +1 -0
  29. package/dist/chunk-AWQG7FCC.js +19 -0
  30. package/dist/chunk-AWQG7FCC.js.map +1 -0
  31. package/dist/chunk-CGHF6RZV.js +300 -0
  32. package/dist/chunk-CGHF6RZV.js.map +1 -0
  33. package/dist/chunk-DWMSB4CA.js +228 -0
  34. package/dist/chunk-DWMSB4CA.js.map +1 -0
  35. package/dist/chunk-GHGO52SO.js +92 -0
  36. package/dist/chunk-GHGO52SO.js.map +1 -0
  37. package/dist/{chunk-LRI3MBGR.js → chunk-HKV7LRHY.js} +4 -4
  38. package/dist/{chunk-LRI3MBGR.js.map → chunk-HKV7LRHY.js.map} +1 -1
  39. package/dist/{chunk-EMXAIRUJ.js → chunk-IRTGUQ5O.js} +5 -4
  40. package/dist/chunk-IRTGUQ5O.js.map +1 -0
  41. package/dist/{chunk-H6ZAOWNE.js → chunk-JVBYV6TL.js} +3 -3
  42. package/dist/{chunk-H6ZAOWNE.js.map → chunk-JVBYV6TL.js.map} +1 -1
  43. package/dist/chunk-KHK5FGER.js +145 -0
  44. package/dist/chunk-KHK5FGER.js.map +1 -0
  45. package/dist/chunk-NEXR37YJ.js +144 -0
  46. package/dist/chunk-NEXR37YJ.js.map +1 -0
  47. package/dist/{chunk-A2ACAGW6.js → chunk-O6FFFOHS.js} +32 -7
  48. package/dist/chunk-O6FFFOHS.js.map +1 -0
  49. package/dist/chunk-QGMOA2YE.js +731 -0
  50. package/dist/chunk-QGMOA2YE.js.map +1 -0
  51. package/dist/{chunk-ERFMC6WU.js → chunk-S4NCI32V.js} +19 -10
  52. package/dist/chunk-S4NCI32V.js.map +1 -0
  53. package/dist/{chunk-753Y6HE6.js → chunk-SLAQPUI4.js} +4 -4
  54. package/dist/chunk-SLAQPUI4.js.map +1 -0
  55. package/dist/{chunk-REOBGYMC.js → chunk-TJ77A4A3.js} +3 -5
  56. package/dist/chunk-TJ77A4A3.js.map +1 -0
  57. package/dist/chunk-TYN6MLXX.js +13864 -0
  58. package/dist/chunk-TYN6MLXX.js.map +1 -0
  59. package/dist/{chunk-RS3HRQWU.js → chunk-WPLAANH4.js} +4 -3
  60. package/dist/chunk-WPLAANH4.js.map +1 -0
  61. package/dist/config/index.d.ts +51 -7
  62. package/dist/config/index.js +4 -5
  63. package/dist/controls/index.d.ts +92 -5
  64. package/dist/controls/index.js +5 -4
  65. package/dist/dragdrop/index.d.ts +9 -6
  66. package/dist/dragdrop/index.js +3 -2
  67. package/dist/index.d.ts +108 -154
  68. package/dist/index.js +456 -600
  69. package/dist/index.js.map +1 -1
  70. package/dist/layers/index.d.ts +5 -35
  71. package/dist/layers/index.js +3 -3
  72. package/dist/loupe/index.d.ts +21 -17
  73. package/dist/loupe/index.js +5 -3
  74. package/dist/overview/index.d.ts +56 -0
  75. package/dist/overview/index.js +288 -0
  76. package/dist/overview/index.js.map +1 -0
  77. package/dist/passthrough/weasel-ui.d.ts +434 -231
  78. package/dist/passthrough/weasel-ui.js +2 -1
  79. package/dist/primitives/index.d.ts +19 -54
  80. package/dist/primitives/index.js +6 -4
  81. package/dist/state/index.d.ts +8 -7
  82. package/dist/state/index.js +4 -5
  83. package/dist/state/index.js.map +1 -1
  84. package/dist/styles.css +360 -174
  85. package/dist/undo/index.d.ts +5 -4
  86. package/package.json +13 -13
  87. package/src/annotations/frac.test.ts +7 -0
  88. package/src/annotations/index.ts +1 -0
  89. package/src/annotations/paint.test.ts +21 -0
  90. package/src/annotations/paint.ts +12 -0
  91. package/src/annotations/store.test.ts +57 -0
  92. package/src/annotations/store.ts +25 -6
  93. package/src/annotations/toolMap.test.ts +37 -2
  94. package/src/annotations/toolMap.ts +13 -0
  95. package/src/annotations/types.ts +30 -2
  96. package/src/canvas/AGENTS.md +8 -10
  97. package/src/canvas/CameraInput.test.tsx +136 -0
  98. package/src/canvas/CameraInput.tsx +311 -0
  99. package/src/canvas/CanvasStack.tsx +42 -58
  100. package/src/canvas/LinkedCursor.less +16 -0
  101. package/src/canvas/LinkedCursor.tsx +54 -0
  102. package/src/canvas/Stage.test.tsx +18 -0
  103. package/src/canvas/Stage.tsx +42 -37
  104. package/src/canvas/cameraView.test.ts +60 -0
  105. package/src/canvas/cameraView.ts +61 -0
  106. package/src/canvas/index.ts +11 -2
  107. package/src/chrome/regions/SidebarRegion.less +197 -0
  108. package/src/chrome/regions/SidebarRegion.test.tsx +27 -0
  109. package/src/chrome/regions/SidebarRegion.tsx +7 -2
  110. package/src/chrome/types.ts +5 -2
  111. package/src/config/autoConfig.ts +2 -2
  112. package/src/config/builder.ts +21 -2
  113. package/src/config/entry.test.ts +10 -0
  114. package/src/config/fromConfigField.ts +1 -0
  115. package/src/config/index.ts +5 -0
  116. package/src/config/resolve.ts +11 -1
  117. package/src/config/rules.ts +2 -0
  118. package/src/config/sectionTree.test.ts +152 -0
  119. package/src/config/sectionTree.ts +134 -0
  120. package/src/config/types.ts +21 -0
  121. package/src/controls/ControlPanel.less +4 -0
  122. package/src/controls/ControlPanel.stories.tsx +55 -2
  123. package/src/controls/ControlPanel.test.tsx +40 -3
  124. package/src/controls/ControlPanel.tsx +134 -336
  125. package/src/controls/inDialog.test.tsx +100 -0
  126. package/src/controls/inDialog.tsx +38 -0
  127. package/src/controls/index.ts +1 -0
  128. package/src/index.test.ts +1 -1
  129. package/src/index.ts +18 -29
  130. package/src/instrument/SineWave.smoke.test.tsx +2 -0
  131. package/src/instrument/types.ts +15 -3
  132. package/src/lab/Lab.nebula.test.tsx +40 -0
  133. package/src/lab/Lab.test.tsx +3 -1
  134. package/src/lab/Lab.tsx +42 -19
  135. package/src/lab/LabBoundary.test.tsx +66 -0
  136. package/src/lab/LabBoundary.tsx +56 -0
  137. package/src/lab/LabHeader.tsx +4 -23
  138. package/src/lab/LabRoot.tsx +2 -3
  139. package/src/lab/LabShell.less +0 -1
  140. package/src/lab/LabShell.stories.tsx +15 -0
  141. package/src/lab/LabShell.test.tsx +18 -0
  142. package/src/lab/LabShell.tsx +14 -1
  143. package/src/lab/LabSwitcher.less +1 -1
  144. package/src/lab/LabSwitcher.test.tsx +41 -0
  145. package/src/lab/LabSwitcher.tsx +30 -8
  146. package/src/lab/index.ts +1 -0
  147. package/src/layers/AGENTS.md +18 -74
  148. package/src/layers/index.ts +4 -2
  149. package/src/loupe/AGENTS.md +7 -6
  150. package/src/loupe/LoupeGestures.tsx +14 -9
  151. package/src/loupe/TrialLoupe.tsx +5 -5
  152. package/src/loupe/index.ts +1 -0
  153. package/src/loupe/loupeActions.ts +6 -2
  154. package/src/overview/AGENTS.md +38 -0
  155. package/src/overview/OverviewMarks.test.tsx +100 -0
  156. package/src/overview/OverviewMarks.tsx +93 -0
  157. package/src/overview/TrialOverview.less +53 -0
  158. package/src/overview/TrialOverview.stories.tsx +50 -0
  159. package/src/overview/TrialOverview.test.tsx +116 -0
  160. package/src/overview/TrialOverview.tsx +374 -0
  161. package/src/overview/index.ts +2 -0
  162. package/src/passthrough/weasel-ui.ts +32 -10
  163. package/src/primitives/index.ts +0 -2
  164. package/src/specimen/Specimen.tsx +59 -56
  165. package/src/state/labRecords.test.ts +4 -1
  166. package/src/state/store.test.ts +4 -4
  167. package/src/state/types.ts +2 -1
  168. package/src/state/useTrialState.test.tsx +1 -1
  169. package/src/styles.less +6 -2
  170. package/src/theme/Interstellar.stories.tsx +8 -13
  171. package/src/theme/base.less +3 -20
  172. package/src/theme/interstellar.test.ts +6 -3
  173. package/src/theme/interstellar.theme.json +0 -56
  174. package/src/theme/interstellar.ts +2 -2
  175. package/src/tools/labTool.ts +6 -2
  176. package/src/trial/Trial.annotations.test.tsx +16 -0
  177. package/src/trial/Trial.canvas.test.tsx +1 -1
  178. package/src/trial/Trial.loupe.test.tsx +73 -1
  179. package/src/trial/Trial.stories.tsx +2 -0
  180. package/src/trial/Trial.tsx +47 -9
  181. package/src/trial/trialOps.test.ts +4 -4
  182. package/dist/_dts/index-8LiKe3A-.d.ts +0 -410
  183. package/dist/chunk-2AYGEN57.js +0 -32
  184. package/dist/chunk-2AYGEN57.js.map +0 -1
  185. package/dist/chunk-6BUQ77ZV.js +0 -18
  186. package/dist/chunk-6BUQ77ZV.js.map +0 -1
  187. package/dist/chunk-753Y6HE6.js.map +0 -1
  188. package/dist/chunk-A2ACAGW6.js.map +0 -1
  189. package/dist/chunk-CKUKF72R.js.map +0 -1
  190. package/dist/chunk-D35PFTS7.js +0 -170
  191. package/dist/chunk-D35PFTS7.js.map +0 -1
  192. package/dist/chunk-EMXAIRUJ.js.map +0 -1
  193. package/dist/chunk-ERFMC6WU.js.map +0 -1
  194. package/dist/chunk-FKLT3ITC.js +0 -590
  195. package/dist/chunk-FKLT3ITC.js.map +0 -1
  196. package/dist/chunk-KJCUERQF.js.map +0 -1
  197. package/dist/chunk-LASCBWW7.js +0 -13456
  198. package/dist/chunk-LASCBWW7.js.map +0 -1
  199. package/dist/chunk-REOBGYMC.js.map +0 -1
  200. package/dist/chunk-RS3HRQWU.js.map +0 -1
  201. package/dist/chunk-TT5C5S6R.js +0 -368
  202. package/dist/chunk-TT5C5S6R.js.map +0 -1
  203. package/dist/ui/layers/index.d.ts +0 -66
  204. package/dist/ui/layers/index.js +0 -3
  205. package/dist/ui/layers/index.js.map +0 -1
  206. package/src/canvas/usePanZoom.test.ts +0 -244
  207. package/src/canvas/usePanZoom.ts +0 -147
  208. package/src/lab/useSystemMode.ts +0 -30
  209. package/src/layers/LayerList.less +0 -93
  210. package/src/layers/LayerList.stories.tsx +0 -66
  211. package/src/layers/LayerList.test.tsx +0 -232
  212. package/src/layers/LayerList.tsx +0 -268
  213. package/src/primitives/Sidebar.less +0 -112
  214. package/src/primitives/Sidebar.stories.tsx +0 -38
  215. package/src/primitives/Sidebar.test.tsx +0 -74
  216. package/src/primitives/Sidebar.tsx +0 -35
  217. package/src/ui/layers/index.ts +0 -5
@@ -69,6 +69,22 @@ describe('an instrument that declares annotations', () => {
69
69
  }
70
70
  });
71
71
 
72
+ it('gets only the tools it names', () => {
73
+ const narrow = defineInstrument<Record<string, never>, Record<string, never>>({
74
+ ...annotating,
75
+ name: 'Narrow',
76
+ annotations: {
77
+ targets: () => [{ id: 'pane', ref: { current: null }, content: { w: 200, h: 100 } }],
78
+ tools: ['pointer', 'select'],
79
+ },
80
+ });
81
+ render(<Lab instruments={[narrow]} defaultInstrument="Narrow" />);
82
+ expect(screen.getByRole('button', { name: 'Interact' })).toBeTruthy();
83
+ expect(screen.getByRole('button', { name: 'Select' })).toBeTruthy();
84
+ expect(screen.queryByRole('button', { name: 'Freehand' })).toBeNull();
85
+ expect(screen.queryByRole('button', { name: 'Rectangle' })).toBeNull();
86
+ });
87
+
72
88
  it('puts the store in reach of its own render', () => {
73
89
  render(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
74
90
  expect(screen.getByTestId('pane').dataset.marks).toBe('0');
@@ -185,7 +185,7 @@ describe('LabProps.instruments', () => {
185
185
  render(<Lab instruments={[instrument]} defaultInstrument="Probe" />);
186
186
  expect([...seen]).toEqual(['grid', 'glyph']);
187
187
 
188
- await userEvent.click(screen.getByRole('checkbox', { name: 'Toggle grid' }));
188
+ await userEvent.click(screen.getByRole('checkbox', { name: 'Show grid' }));
189
189
  expect([...seen]).toEqual(['glyph']);
190
190
  });
191
191
  });
@@ -1,4 +1,4 @@
1
- import { fireEvent, render, screen } from '@testing-library/react';
1
+ import { act, fireEvent, render, screen } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { beforeAll, describe, expect, it, vi } from 'vitest';
4
4
  import { defineInstrument } from '../instrument/defineInstrument';
@@ -126,3 +126,75 @@ describe('the loupe capability', () => {
126
126
  expect(lens(container)).toBeNull();
127
127
  });
128
128
  });
129
+
130
+ describe('the loupe and the trial camera share one dispatcher', () => {
131
+ const frame = () => new Promise<void>((r) => requestAnimationFrame(() => r()));
132
+
133
+ function mountDrawn() {
134
+ const zooms: number[] = [];
135
+ const drawn = defineInstrument<Record<string, never>, Record<string, never>>({
136
+ name: 'Zoomed',
137
+ defaultConfig: () => ({}),
138
+ initialState: () => ({}),
139
+ render: () => null,
140
+ canvas: {
141
+ layers: [
142
+ {
143
+ id: 'main',
144
+ draw: (_ctx, { zoom }) => {
145
+ zooms.push(zoom);
146
+ },
147
+ },
148
+ ],
149
+ },
150
+ loupe: true,
151
+ });
152
+ const r = render(<Lab instruments={[drawn]} defaultInstrument="Zoomed" />);
153
+ const stack = r.container.querySelector('.lk-canvas-stack');
154
+ if (!stack) throw new Error('no canvas stack');
155
+ return { ...r, stack, zooms };
156
+ }
157
+
158
+ const wheel = (el: Element) => {
159
+ const e = new WheelEvent('wheel', {
160
+ deltaY: -300,
161
+ clientX: 50,
162
+ clientY: 50,
163
+ bubbles: true,
164
+ cancelable: true,
165
+ });
166
+ el.dispatchEvent(e);
167
+ return e;
168
+ };
169
+
170
+ it('zooms the camera while the lens is down', async () => {
171
+ const h = mountDrawn();
172
+ await act(async () => {
173
+ await frame();
174
+ });
175
+ await act(async () => {
176
+ wheel(h.stack);
177
+ await frame();
178
+ });
179
+ expect(h.zooms.at(-1)).toBeGreaterThan(1);
180
+ });
181
+
182
+ it('gives the wheel to the lens while it is up, and leaves the camera alone', async () => {
183
+ const user = userEvent.setup();
184
+ const h = mountDrawn();
185
+ await user.click(screen.getByRole('button', { name: 'Loupe' }));
186
+ pointAt(h.stack, 60, 40);
187
+ expect(lens(h.container)).not.toBeNull();
188
+ await act(async () => {
189
+ await frame();
190
+ });
191
+ const before = h.zooms.at(-1);
192
+ let e!: WheelEvent;
193
+ await act(async () => {
194
+ e = wheel(h.stack);
195
+ await frame();
196
+ });
197
+ expect(e.defaultPrevented).toBe(true);
198
+ expect(h.zooms.at(-1)).toBe(before);
199
+ });
200
+ });
@@ -1,3 +1,4 @@
1
+ import { createPointerStore } from '@weasel-js/core';
1
2
  import type { Meta, StoryObj } from '@weasel-js/forge';
2
3
  import type { Instrument } from '../instrument/types';
3
4
  import { LabContext, type LabContextValue } from '../lab/LabContext';
@@ -63,6 +64,7 @@ function Harness() {
63
64
  setZoom: () => {},
64
65
  activeToolId: null,
65
66
  visibleLayers: [],
67
+ pointer: createPointerStore(),
66
68
  },
67
69
  emit: () => {},
68
70
  })}
@@ -1,3 +1,4 @@
1
+ import { createPointerStore, PointerContextProvider, WeaselProvider } from '@weasel-js/core';
1
2
  import { type ReactNode, useContext, useEffect, useMemo, useRef, useState } from 'react';
2
3
  import { useStore } from 'zustand/react';
3
4
  import { AnnotationsContext } from '../annotations/AnnotationsContext';
@@ -5,6 +6,7 @@ import { AnnotationTargets } from '../annotations/AnnotationTargets';
5
6
  import { AnnotationPreloadContext } from '../annotations/preload';
6
7
  import { annotationsFromJSON } from '../annotations/store';
7
8
  import type { AnnotationStorage, AnnotationTargetInfo } from '../annotations/types';
9
+ import { CameraScopeContext } from '../canvas/CameraInput';
8
10
  import { CameraWheelContext, type CameraWheelSlot } from '../canvas/CameraWheelContext';
9
11
  import { CanvasStack } from '../canvas/CanvasStack';
10
12
  import { fitStage, Stage } from '../canvas/Stage';
@@ -24,9 +26,9 @@ import type {
24
26
  } from '../instrument/types';
25
27
  import { useJob } from '../job/useJob';
26
28
  import { useLabContext } from '../lab/LabContext';
27
- import { LayerList } from '../layers/LayerList';
28
29
  import { TrialLoupe } from '../loupe/TrialLoupe';
29
30
  import { resolveLoupe } from '../loupe/types';
31
+ import { LayerList, type LayerListItem, moveLayers } from '../passthrough/weasel-ui';
30
32
  import { LabStoreContext, TrialIdProvider } from '../state/context';
31
33
  import type { LabStore } from '../state/store';
32
34
  import type { TrialRecord } from '../state/types';
@@ -134,6 +136,7 @@ function TrialRuntime({
134
136
  suppress,
135
137
  }: TrialRuntimeProps) {
136
138
  const wheelSlot = useRef<CameraWheelSlot['current']>(null);
139
+ const [pointer] = useState(createPointerStore);
137
140
  const canvasContainerRef = useRef<HTMLDivElement | null>(null);
138
141
  const loupeHostRef = useRef<HTMLDivElement | null>(null);
139
142
  const updateTrialState = useStore(store, (s) => s.updateTrialState);
@@ -328,6 +331,7 @@ function TrialRuntime({
328
331
  },
329
332
  activeToolId: resolvedToolId,
330
333
  visibleLayers,
334
+ pointer,
331
335
  },
332
336
  emit: (event) => {
333
337
  snapshotIfNeeded(event);
@@ -401,6 +405,24 @@ function TrialRuntime({
401
405
  return instrument.layers.ids.map((l) => (typeof l === 'string' ? { id: l, label: l } : l));
402
406
  }, [instrument.layers]);
403
407
 
408
+ // In the order the canvas draws them, with the always-on layers pinned
409
+ // below the rest, where a drag cannot reach past them.
410
+ const layerItems: LayerListItem[] = useMemo(() => {
411
+ const ordered = layerOrder
412
+ ? [...layerDescriptors].sort((a, b) => layerOrder.indexOf(a.id) - layerOrder.indexOf(b.id))
413
+ : layerDescriptors;
414
+ const pinnedLast = [
415
+ ...ordered.filter((l) => !l.alwaysOn),
416
+ ...ordered.filter((l) => l.alwaysOn),
417
+ ];
418
+ return pinnedLast.map((l) => ({
419
+ id: l.id,
420
+ label: l.label,
421
+ locked: l.alwaysOn === true,
422
+ visible: l.alwaysOn === true || layerVisibility[l.id] !== false,
423
+ }));
424
+ }, [layerDescriptors, layerOrder, layerVisibility]);
425
+
404
426
  const dragDropResult = useDragDrop({
405
427
  capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
406
428
  canvasContainerRef,
@@ -492,7 +514,12 @@ function TrialRuntime({
492
514
  minZoom={stage.minZoom}
493
515
  maxZoom={stage.maxZoom}
494
516
  hostRef={loupeHostRef}
495
- overlay={lens}
517
+ overlay={
518
+ <>
519
+ {stage.overlay?.(renderCtx)}
520
+ {lens}
521
+ </>
522
+ }
496
523
  >
497
524
  {instrument.render(renderCtx)}
498
525
  </Stage>
@@ -519,19 +546,18 @@ function TrialRuntime({
519
546
  () =>
520
547
  instrument.layers && layerDescriptors.length > 0 ? (
521
548
  <LayerList
522
- layers={layerDescriptors}
523
- visibility={layerVisibility}
524
- onReorder={(next) => {
525
- setLayerOrder(next.map((l) => l.id));
549
+ items={layerItems}
550
+ onReorder={(move) => {
551
+ setLayerOrder(moveLayers(layerItems, move).map((l) => l.id));
526
552
  bus.emit('layers.reorder');
527
553
  }}
528
- onToggle={(lid, visible) => {
554
+ onVisibilityChange={(lid, visible) => {
529
555
  setLayerVisibility((prev) => ({ ...prev, [lid]: visible }));
530
556
  bus.emit('layers.toggle');
531
557
  }}
532
558
  />
533
559
  ) : null,
534
- [instrument.layers, layerDescriptors, layerVisibility, bus],
560
+ [instrument.layers, layerDescriptors, layerItems, bus],
535
561
  );
536
562
 
537
563
  const extraChrome = useMemo<TrialContribution[]>(() => {
@@ -567,6 +593,18 @@ function TrialRuntime({
567
593
  />
568
594
  ) : null;
569
595
 
596
+ // One input scope for the trial's body: the camera, the loupe and an
597
+ // overview share its actions, its deps and its pointer. Isolated, because an
598
+ // actions registry holds one dispatcher and the trial beside this one has
599
+ // its own.
600
+ const scopedBody = (
601
+ <WeaselProvider isolate>
602
+ <PointerContextProvider store={pointer}>
603
+ <CameraScopeContext.Provider value={true}>{body}</CameraScopeContext.Provider>
604
+ </PointerContextProvider>
605
+ </WeaselProvider>
606
+ );
607
+
570
608
  return (
571
609
  <TrialIdProvider trialId={record.id}>
572
610
  <AnnotationsContext.Provider value={annotationsCap ? annotations : null}>
@@ -585,7 +623,7 @@ function TrialRuntime({
585
623
  activeToolId={resolvedToolId}
586
624
  setActiveTool={setActiveTool}
587
625
  >
588
- {body}
626
+ {scopedBody}
589
627
  {annotationOverlays}
590
628
  </TrialChrome>
591
629
  </CameraWheelContext.Provider>
@@ -132,7 +132,7 @@ describe('addTrial', () => {
132
132
  const out = addTrial([], instruments, 'Schemed', { config: { gap: auto } });
133
133
  expect(out[0]?.auto).toContain('gap');
134
134
  expect(out[0]?.autoSeed).toEqual(['gap']);
135
- expect((out[0]?.config as { gap: number }).gap).toBe(12);
135
+ expect((out[0]?.config as { gap: number } | undefined)?.gap).toBe(12);
136
136
  expect(out[0]?.configSeed).not.toHaveProperty('gap');
137
137
  });
138
138
 
@@ -141,13 +141,13 @@ describe('addTrial', () => {
141
141
  const out = addTrial([], instruments, 'Counter', {
142
142
  config: { at: when } as unknown as Partial<CounterConfig>,
143
143
  });
144
- expect((out[0]?.config as { at: unknown }).at).toBe(when);
144
+ expect((out[0]?.config as { at: unknown } | undefined)?.at).toBe(when);
145
145
  });
146
146
 
147
147
  it('honors auto at a nested seed path', () => {
148
148
  const out = addTrial([], instruments, 'Schemed', { config: { grid: { size: auto } } });
149
149
  expect(out[0]?.auto).toContain('grid.size');
150
- expect((out[0]?.config as { grid: { size: number } }).grid.size).toBe(4);
150
+ expect((out[0]?.config as { grid: { size: number } } | undefined)?.grid.size).toBe(4);
151
151
  });
152
152
 
153
153
  it('leaves a trial with no auto field when nothing is unpinned', () => {
@@ -222,7 +222,7 @@ describe('resetTrial', () => {
222
222
  const arr = addTrial([], instruments, 'Counter');
223
223
  head(arr).config = { step: 99 } as CounterConfig;
224
224
  resetTrial(arr, head(arr).id, instruments);
225
- expect((arr[0]?.config as CounterConfig).step).toBe(99);
225
+ expect((arr[0]?.config as CounterConfig | undefined)?.step).toBe(99);
226
226
  });
227
227
 
228
228
  it('restores the seed the trial was opened with, not the bare defaults', () => {