@weasel-js/labkit 1.7.0 → 1.7.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 (208) hide show
  1. package/README.md +3 -3
  2. package/dist/_dts/{CanvasStackContext-D2jnIwfk.d.ts → CanvasStackContext-Cjy6I-Iv.d.ts} +4 -1
  3. package/dist/_dts/{Stage-CCWfQQWM.d.ts → Stage-D6t7o0pF.d.ts} +3 -2
  4. package/dist/_dts/{index-CsYqUHD3.d.ts → index-BTmaRy6a.d.ts} +10 -6
  5. package/dist/_dts/{types-CSm9jgol.d.ts → index-DCMA4Fy1.d.ts} +59 -113
  6. package/dist/_dts/{types-CRPSGFAp.d.ts → types-BUBSIZuV.d.ts} +8 -7
  7. package/dist/_dts/{useTrialState-CTZPgeZ7.d.ts → useTrialState-Bh8bzFgE.d.ts} +1 -1
  8. package/dist/canvas/index.d.ts +8 -6
  9. package/dist/canvas/index.js +6 -6
  10. package/dist/chrome/index.d.ts +7 -5
  11. package/dist/chrome/index.js +10 -9
  12. package/dist/chunk-2ES4W47N.js +8 -0
  13. package/dist/chunk-2ES4W47N.js.map +1 -0
  14. package/dist/{chunk-WPLAANH4.js → chunk-2N7UBSS2.js} +12 -12
  15. package/dist/chunk-2N7UBSS2.js.map +1 -0
  16. package/dist/chunk-4KSC7NXL.js +15491 -0
  17. package/dist/chunk-4KSC7NXL.js.map +1 -0
  18. package/dist/{chunk-7BO7HNGF.js → chunk-CEGFXTBD.js} +8 -61
  19. package/dist/chunk-CEGFXTBD.js.map +1 -0
  20. package/dist/{chunk-275JRRVD.js → chunk-E23Q6A3Y.js} +232 -37
  21. package/dist/chunk-E23Q6A3Y.js.map +1 -0
  22. package/dist/{chunk-GOZLNM4L.js → chunk-E6NARGU7.js} +5 -5
  23. package/dist/{chunk-GOZLNM4L.js.map → chunk-E6NARGU7.js.map} +1 -1
  24. package/dist/{chunk-IJJUK4FU.js → chunk-EESJUSYR.js} +5 -6
  25. package/dist/chunk-EESJUSYR.js.map +1 -0
  26. package/dist/{chunk-HXXTULDN.js → chunk-EFXRM6XQ.js} +8 -11
  27. package/dist/chunk-EFXRM6XQ.js.map +1 -0
  28. package/dist/chunk-F4ACIK7Z.js +210 -0
  29. package/dist/chunk-F4ACIK7Z.js.map +1 -0
  30. package/dist/{chunk-FMK7UQ6V.js → chunk-FB4MIZOO.js} +61 -44
  31. package/dist/chunk-FB4MIZOO.js.map +1 -0
  32. package/dist/{chunk-AWQG7FCC.js → chunk-FG3EXZDV.js} +3 -3
  33. package/dist/{chunk-AWQG7FCC.js.map → chunk-FG3EXZDV.js.map} +1 -1
  34. package/dist/{chunk-I5DUQ5HM.js → chunk-GAHELLJT.js} +75 -72
  35. package/dist/chunk-GAHELLJT.js.map +1 -0
  36. package/dist/{chunk-YT75VYRD.js → chunk-IBVRMHW5.js} +5 -5
  37. package/dist/{chunk-YT75VYRD.js.map → chunk-IBVRMHW5.js.map} +1 -1
  38. package/dist/{chunk-T5IBSPAM.js → chunk-JFOJESJF.js} +33 -23
  39. package/dist/chunk-JFOJESJF.js.map +1 -0
  40. package/dist/{chunk-RWNDDKOH.js → chunk-PMLZP7RT.js} +18 -15
  41. package/dist/chunk-PMLZP7RT.js.map +1 -0
  42. package/dist/{chunk-Y23G2VZL.js → chunk-READTDQX.js} +2 -2
  43. package/dist/chunk-READTDQX.js.map +1 -0
  44. package/dist/{chunk-JVBYV6TL.js → chunk-ROGJWDQA.js} +3 -3
  45. package/dist/{chunk-JVBYV6TL.js.map → chunk-ROGJWDQA.js.map} +1 -1
  46. package/dist/{chunk-ZIFPHIWZ.js → chunk-T6DLGEJB.js} +12 -14
  47. package/dist/chunk-T6DLGEJB.js.map +1 -0
  48. package/dist/{chunk-24W5VEJW.js → chunk-ULYYNWGP.js} +2 -2
  49. package/dist/{chunk-24W5VEJW.js.map → chunk-ULYYNWGP.js.map} +1 -1
  50. package/dist/{chunk-UNAZSLWE.js → chunk-VYVGVBGY.js} +17 -20
  51. package/dist/chunk-VYVGVBGY.js.map +1 -0
  52. package/dist/{chunk-KMJ7RWD3.js → chunk-WLMDTI2Q.js} +3 -3
  53. package/dist/{chunk-KMJ7RWD3.js.map → chunk-WLMDTI2Q.js.map} +1 -1
  54. package/dist/{chunk-J7OWWTCW.js → chunk-XE5HJOPL.js} +5 -6
  55. package/dist/chunk-XE5HJOPL.js.map +1 -0
  56. package/dist/chunk-XVUKDSYV.js +38 -0
  57. package/dist/chunk-XVUKDSYV.js.map +1 -0
  58. package/dist/{chunk-FZ7YFQN7.js → chunk-YCFKW5RX.js} +3 -3
  59. package/dist/chunk-YCFKW5RX.js.map +1 -0
  60. package/dist/{chunk-CQLQPQ4P.js → chunk-ZQIHJXP4.js} +2 -2
  61. package/dist/chunk-ZQIHJXP4.js.map +1 -0
  62. package/dist/config/index.d.ts +18 -10
  63. package/dist/config/index.js +4 -4
  64. package/dist/controls/index.d.ts +4 -7
  65. package/dist/controls/index.js +6 -5
  66. package/dist/dragdrop/index.d.ts +23 -3
  67. package/dist/dragdrop/index.js +4 -3
  68. package/dist/fonts/oswald-tabular-digits.woff2 +0 -0
  69. package/dist/index.d.ts +60 -714
  70. package/dist/index.js +357 -334
  71. package/dist/index.js.map +1 -1
  72. package/dist/job/index.d.ts +8 -0
  73. package/dist/job/index.js +1 -1
  74. package/dist/layers/index.d.ts +6 -5
  75. package/dist/layers/index.js +3 -2
  76. package/dist/loupe/index.d.ts +100 -30
  77. package/dist/loupe/index.js +585 -6
  78. package/dist/loupe/index.js.map +1 -1
  79. package/dist/overview/index.d.ts +4 -2
  80. package/dist/overview/index.js +15 -14
  81. package/dist/overview/index.js.map +1 -1
  82. package/dist/passthrough/weasel-canvas.d.ts +2 -0
  83. package/dist/passthrough/weasel-ui.d.ts +2 -2871
  84. package/dist/passthrough/weasel-ui.js +3 -2
  85. package/dist/primitives/index.d.ts +213 -3
  86. package/dist/primitives/index.js +8 -7
  87. package/dist/state/index.d.ts +7 -5
  88. package/dist/state/index.js +6 -6
  89. package/dist/styles.css +60 -44
  90. package/dist/surface/index.d.ts +10 -2
  91. package/dist/surface/index.js +1 -1
  92. package/dist/undo/index.d.ts +5 -3
  93. package/package.json +11 -11
  94. package/src/annotations/AnnotationOverlay.abandon.test.tsx +143 -0
  95. package/src/annotations/AnnotationOverlay.tsx +50 -31
  96. package/src/annotations/Annotations.overlay.test.tsx +31 -0
  97. package/src/annotations/capture.browser.test.ts +41 -0
  98. package/src/annotations/capture.ts +14 -7
  99. package/src/annotations/drawOne.ts +2 -2
  100. package/src/annotations/frac.test.ts +44 -1
  101. package/src/annotations/frac.ts +46 -18
  102. package/src/annotations/index.ts +1 -0
  103. package/src/annotations/paint.test.ts +74 -33
  104. package/src/annotations/paint.ts +70 -32
  105. package/src/annotations/store.test.ts +212 -2
  106. package/src/annotations/store.ts +130 -27
  107. package/src/annotations/svgNodes.test.ts +22 -10
  108. package/src/annotations/svgNodes.ts +3 -3
  109. package/src/annotations/types.ts +29 -10
  110. package/src/canvas/CameraInput.test.tsx +30 -0
  111. package/src/canvas/CameraInput.tsx +16 -15
  112. package/src/canvas/CanvasStack.tsx +5 -5
  113. package/src/canvas/CanvasStackContext.ts +4 -1
  114. package/src/canvas/Stage.tsx +3 -3
  115. package/src/canvas/cameraRegistry.ts +1 -0
  116. package/src/canvas/worldSpec.ts +3 -1
  117. package/src/chrome/builtins.test.ts +7 -6
  118. package/src/chrome/builtins.tsx +5 -1
  119. package/src/chrome/index.ts +1 -1
  120. package/src/chrome/merge.test.ts +5 -0
  121. package/src/chrome/merge.ts +4 -2
  122. package/src/chrome/regions/PaletteRegion.test.tsx +2 -3
  123. package/src/chrome/regions/PaletteRegion.tsx +3 -4
  124. package/src/chrome/regions/ViewportRegion.browser.test.less +6 -0
  125. package/src/chrome/regions/ViewportRegion.browser.test.tsx +38 -0
  126. package/src/chrome/regions/ViewportRegion.less +6 -0
  127. package/src/chrome/regions/ViewportRegion.tsx +10 -4
  128. package/src/chrome/types.ts +4 -2
  129. package/src/config/auto.ts +1 -0
  130. package/src/config/builder.ts +6 -0
  131. package/src/config/types.ts +2 -1
  132. package/src/controls/ControlPanel.test.tsx +37 -3
  133. package/src/controls/ControlPanel.tsx +12 -3
  134. package/src/dragdrop/DragDropRuntime.tsx +22 -13
  135. package/src/dragdrop/DragGhost.tsx +3 -0
  136. package/src/dragdrop/Palette.tsx +2 -0
  137. package/src/index.test.ts +5 -0
  138. package/src/index.ts +1 -1
  139. package/src/instrument/types.ts +0 -6
  140. package/src/job/useJob.ts +14 -9
  141. package/src/lab/Lab.tsx +5 -1
  142. package/src/lab/LabShell.less +1 -0
  143. package/src/lab/LabSwitcher.browser.test.tsx +27 -0
  144. package/src/lab/LabSwitcher.less +2 -0
  145. package/src/lab/LabZoom.tsx +4 -4
  146. package/src/lab/Workspace.tsx +48 -52
  147. package/src/loupe/AGENTS.md +28 -15
  148. package/src/loupe/CanvasLoupe.tsx +2 -3
  149. package/src/loupe/DomLoupe.tsx +1 -7
  150. package/src/loupe/Loupe.less +16 -0
  151. package/src/loupe/TrialLoupe.tsx +120 -57
  152. package/src/loupe/index.ts +1 -6
  153. package/src/loupe/types.test.ts +3 -3
  154. package/src/loupe/types.ts +24 -27
  155. package/src/loupe/useLoupe.test.tsx +29 -4
  156. package/src/loupe/useLoupe.ts +20 -17
  157. package/src/overview/OverviewMarks.tsx +12 -4
  158. package/src/overview/TrialOverview.tsx +3 -3
  159. package/src/passthrough/weasel-ui.test.ts +1 -1
  160. package/src/passthrough/weasel-ui.ts +1 -1
  161. package/src/primitives/FloatingPanel.tsx +7 -3
  162. package/src/primitives/JobProgress.less +1 -0
  163. package/src/primitives/Readout.test.tsx +2 -2
  164. package/src/primitives/Split.tsx +69 -41
  165. package/src/primitives/Toolbar.tsx +3 -3
  166. package/src/primitives/ZoomControl.less +1 -2
  167. package/src/specimen/Specimen.tsx +0 -1
  168. package/src/state/undock.ts +3 -0
  169. package/src/state/useOpenOnce.ts +20 -16
  170. package/src/state/view.ts +1 -0
  171. package/src/surface/index.ts +1 -1
  172. package/src/surface/useTiledSurface.test.tsx +14 -0
  173. package/src/surface/useTiledSurface.ts +11 -3
  174. package/src/trial/Trial.annotations.test.tsx +73 -0
  175. package/src/trial/Trial.less +0 -16
  176. package/src/trial/Trial.loupe.test.tsx +28 -11
  177. package/src/trial/Trial.tsx +74 -62
  178. package/src/trial/TrialChrome.tsx +6 -3
  179. package/src/trial/index.ts +2 -0
  180. package/src/trial/loupeSwitch.ts +15 -0
  181. package/dist/_dts/LayerList.d-DiuEArKq.d.ts +0 -83
  182. package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +0 -79
  183. package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +0 -141
  184. package/dist/_dts/Select.d-Bensr_MT.d.ts +0 -579
  185. package/dist/_dts/index-B2Yj5aA0.d.ts +0 -274
  186. package/dist/chunk-275JRRVD.js.map +0 -1
  187. package/dist/chunk-4LJSC7MB.js +0 -143
  188. package/dist/chunk-4LJSC7MB.js.map +0 -1
  189. package/dist/chunk-7BO7HNGF.js.map +0 -1
  190. package/dist/chunk-CQLQPQ4P.js.map +0 -1
  191. package/dist/chunk-FMK7UQ6V.js.map +0 -1
  192. package/dist/chunk-FZ7YFQN7.js.map +0 -1
  193. package/dist/chunk-HXXTULDN.js.map +0 -1
  194. package/dist/chunk-I5DUQ5HM.js.map +0 -1
  195. package/dist/chunk-IJJUK4FU.js.map +0 -1
  196. package/dist/chunk-J7OWWTCW.js.map +0 -1
  197. package/dist/chunk-M7HP3FTK.js +0 -549
  198. package/dist/chunk-M7HP3FTK.js.map +0 -1
  199. package/dist/chunk-MIM5R4ZT.js +0 -14972
  200. package/dist/chunk-MIM5R4ZT.js.map +0 -1
  201. package/dist/chunk-RWNDDKOH.js.map +0 -1
  202. package/dist/chunk-T5IBSPAM.js.map +0 -1
  203. package/dist/chunk-UNAZSLWE.js.map +0 -1
  204. package/dist/chunk-WPLAANH4.js.map +0 -1
  205. package/dist/chunk-Y23G2VZL.js.map +0 -1
  206. package/dist/chunk-ZIFPHIWZ.js.map +0 -1
  207. package/src/primitives/useRovingTabIndex.test.ts +0 -48
  208. package/src/primitives/useRovingTabIndex.ts +0 -88
@@ -1,11 +1,25 @@
1
- import { createPointerStore, PointerContextProvider, WeaselProvider } from '@weasel-js/core';
2
- import { type ReactNode, useContext, useEffect, useMemo, useRef, useState } from 'react';
1
+ import {
2
+ createPointerStore,
3
+ PointerContextProvider,
4
+ useLatest,
5
+ WeaselProvider,
6
+ } from '@weasel-js/core';
7
+ import {
8
+ type ReactNode,
9
+ useCallback,
10
+ useContext,
11
+ useEffect,
12
+ useLayoutEffect,
13
+ useMemo,
14
+ useRef,
15
+ useState,
16
+ } from 'react';
3
17
  import { useStore } from 'zustand/react';
4
18
  import { AnnotationsContext } from '../annotations/AnnotationsContext';
5
19
  import { AnnotationTargets } from '../annotations/AnnotationTargets';
6
20
  import { AnnotationPreloadContext } from '../annotations/preload';
7
21
  import { annotationsFromJSON } from '../annotations/store';
8
- import type { AnnotationStorage, AnnotationTargetInfo } from '../annotations/types';
22
+ import type { AnnotationStorage, AnnotationsApi, AnnotationTargetInfo } from '../annotations/types';
9
23
  import type { CameraView } from '../canvas/CameraInput';
10
24
  import { CameraWheelContext, type CameraWheelSlot } from '../canvas/CameraWheelContext';
11
25
  import { CanvasStack } from '../canvas/CanvasStack';
@@ -27,8 +41,6 @@ import type {
27
41
  } from '../instrument/types';
28
42
  import { useJob } from '../job/useJob';
29
43
  import { useLabContext } from '../lab/LabContext';
30
- import { TrialLoupe } from '../loupe/TrialLoupe';
31
- import { resolveLoupe } from '../loupe/types';
32
44
  import { LayerList, type LayerListItem, moveLayers } from '../passthrough/weasel-ui';
33
45
  import { LabStoreContext, TrialIdProvider } from '../state/context';
34
46
  import type { LabStore } from '../state/store';
@@ -37,6 +49,7 @@ import { as2DView, DEFAULT_VIEW, withZoom } from '../state/view';
37
49
  import { resolveLabTool } from '../tools/labTool';
38
50
  import { createEventBus, type EventBus } from '../undo/eventBus';
39
51
  import { pushSnapshot, redo as undoRedo, undo as undoUndo } from '../undo/undoStack';
52
+ import { LoupeSwitchContext } from './loupeSwitch';
40
53
  import type { LoupeBindings, UndoBindings } from './TrialChrome';
41
54
  import { TrialChrome } from './TrialChrome';
42
55
 
@@ -145,7 +158,6 @@ function TrialRuntime({
145
158
  );
146
159
  const [pointer] = useState(createPointerStore);
147
160
  const canvasContainerRef = useRef<HTMLDivElement | null>(null);
148
- const loupeHostRef = useRef<HTMLDivElement | null>(null);
149
161
  const updateTrialState = useStore(store, (s) => s.updateTrialState);
150
162
  const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
151
163
  const updateTrialView = useStore(store, (s) => s.updateTrialView);
@@ -167,6 +179,12 @@ function TrialRuntime({
167
179
  const [layerVisibility, setLayerVisibility] = useState<Record<string, boolean>>({});
168
180
  const [layerOrder, setLayerOrder] = useState<string[] | null>(null);
169
181
  const [loupeOn, setLoupeOn] = useState(false);
182
+ const [lenses, setLenses] = useState(0);
183
+ const mountLens = useCallback(() => {
184
+ setLenses((n) => n + 1);
185
+ return () => setLenses((n) => n - 1);
186
+ }, []);
187
+ const loupeSwitch = useMemo(() => ({ on: loupeOn, mount: mountLens }), [loupeOn, mountLens]);
170
188
 
171
189
  const visibleLayers = useMemo(
172
190
  () =>
@@ -207,43 +225,69 @@ function TrialRuntime({
207
225
  // resizing or gaining a dependency must not need the store rebuilt. Held in
208
226
  // a ref because the store is built once and closes over it.
209
227
  const annotationsCap = instrument.annotations;
210
- const targetsRef = useRef<() => readonly AnnotationTargetInfo[]>(() => []);
211
- targetsRef.current = () =>
228
+ const targetsRef = useLatest((): readonly AnnotationTargetInfo[] =>
212
229
  annotationsCap
213
230
  ? annotationsCap.targets(record.state, config, { id: record.id, view: record.view })
214
- : [];
231
+ : [],
232
+ );
215
233
 
216
234
  // One store for the trial's lifetime.
217
235
  const annotationsRef = useRef<ReturnType<typeof annotationsFromJSON> | null>(null);
218
236
  // Read through refs for the same reason `targets` is: the store is built
219
237
  // once, and an export must draw against the config the trial holds now.
220
- const configRef = useRef<unknown>(config);
221
- configRef.current = config;
238
+ const configRef = useLatest(config);
222
239
  // The instrument can be swapped under a live trial, so the capability is read at use too.
223
- const capRef = useRef(annotationsCap);
224
- capRef.current = annotationsCap;
240
+ const capRef = useLatest(annotationsCap);
225
241
  if (annotationsRef.current === null) {
226
242
  // Seeded from wherever the marks were kept: the instrument's own store if
227
243
  // it declared one, else this trial's slot.
228
- const kept = annotationsCap?.storage ? keptMarks : record.annotations;
229
- annotationsRef.current = annotationsFromJSON(kept, () => targetsRef.current(), {
230
- meaning: () => capRef.current?.meaning,
231
- config: () => configRef.current,
232
- onCapture: (result) => capRef.current?.onCapture?.(result),
233
- });
244
+ annotationsRef.current = annotationsFromJSON(
245
+ annotationsCap?.storage ? keptMarks : record.annotations,
246
+ () => targetsRef.current(),
247
+ {
248
+ meaning: () => capRef.current?.meaning,
249
+ config: () => configRef.current,
250
+ onCapture: (result) => capRef.current?.onCapture?.(result),
251
+ selection: () => capRef.current?.selection,
252
+ },
253
+ );
234
254
  }
235
255
  const annotations = annotationsRef.current;
236
256
 
257
+ // Chrome reads the store while it renders — a mark list, the export menu,
258
+ // the overview — and the store reads the refs above, which hold the last
259
+ // commit. So once a change to them commits, those readers are told to read
260
+ // again. Through the context's `subscribe` alone: the store's own would have
261
+ // the debounce below save marks that did not change.
262
+ const inputsCommitted = useRef(new Set<() => void>());
263
+ // biome-ignore lint/correctness/useExhaustiveDependencies: these are what `targets` and `config` answer from — the signal, not values this reads
264
+ useLayoutEffect(() => {
265
+ for (const fn of inputsCommitted.current) fn();
266
+ }, [record.state, record.view, config, annotationsCap]);
267
+ const provided = useMemo<AnnotationsApi>(
268
+ () => ({
269
+ ...annotations,
270
+ subscribe: (fn) => {
271
+ const off = annotations.subscribe(fn);
272
+ inputsCommitted.current.add(fn);
273
+ return () => {
274
+ off();
275
+ inputsCommitted.current.delete(fn);
276
+ };
277
+ },
278
+ }),
279
+ [annotations],
280
+ );
281
+
237
282
  // Written on a trailing debounce rather than per notification: a scene
238
283
  // mutates every frame of a drag, and a zustand write per frame re-renders
239
284
  // every trial in the lab. The unmount flush is not an optimization — the
240
285
  // last mark before a trial closes is otherwise lost.
241
286
  const saveCap = annotationsCap?.storage;
242
- const saveRef = useRef<(doc: unknown) => void>(() => {});
243
- saveRef.current = (doc) => {
287
+ const saveRef = useLatest((doc: unknown): void => {
244
288
  if (saveCap) saveCap.save(doc as never);
245
289
  else updateTrialAnnotations(record.id, doc);
246
- };
290
+ });
247
291
  // Only when the pair flips, which is rare: bumping React state on every
248
292
  // scene notification would re-render the trial on every frame of a drag,
249
293
  // which is the thing the debounce below exists to avoid.
@@ -280,7 +324,7 @@ function TrialRuntime({
280
324
  if (timer) clearTimeout(timer);
281
325
  flush();
282
326
  };
283
- }, [annotations, annotationsCap]);
327
+ }, [annotations, annotationsCap, saveRef]);
284
328
 
285
329
  // A trial gets its own slot when its instrument declares tools; otherwise it
286
330
  // reads the lab's. Which slot a change writes follows from the same thing.
@@ -380,14 +424,8 @@ function TrialRuntime({
380
424
  }
381
425
  : undefined;
382
426
 
383
- const loupeCap = useMemo(() => {
384
- const declared = instrument.loupe;
385
- if (declared == null) return null;
386
- return resolveLoupe(typeof declared === 'function' ? declared(config) : declared);
387
- }, [instrument.loupe, config]);
388
- const loupeBindings: LoupeBindings | undefined = loupeCap
389
- ? { on: loupeOn, toggle: () => setLoupeOn((v) => !v) }
390
- : undefined;
427
+ const loupeBindings: LoupeBindings | undefined =
428
+ lenses > 0 ? { on: loupeOn, toggle: () => setLoupeOn((v) => !v) } : undefined;
391
429
 
392
430
  const canvasLayers: CanvasLayerDescriptor[] = useMemo(() => {
393
431
  if (!instrument.canvas) return [];
@@ -478,20 +516,6 @@ function TrialRuntime({
478
516
  ];
479
517
  }, [canvasLayers, dragDropResult.drag]);
480
518
 
481
- // The lens tracks the pointer over whatever box holds the content: the canvas
482
- // stack's own element, which it takes from context, or the wrapper below.
483
- const lens = loupeCap ? (
484
- <TrialLoupe
485
- capability={loupeCap}
486
- enabled={loupeOn}
487
- state={record.state}
488
- config={config}
489
- view={view2d ?? DEFAULT_VIEW}
490
- worldSpec={instrument.canvas?.worldSpec}
491
- hostRef={loupeHostRef}
492
- />
493
- ) : null;
494
-
495
519
  let body: ReactNode;
496
520
  if (instrument.canvas) {
497
521
  body = (
@@ -506,7 +530,6 @@ function TrialRuntime({
506
530
  maxZoom={instrument.canvas.maxZoom}
507
531
  >
508
532
  {instrument.render(renderCtx)}
509
- {lens}
510
533
  </CanvasStack>
511
534
  <DragOverlay drag={dragDropResult.drag} />
512
535
  </div>
@@ -520,24 +543,11 @@ function TrialRuntime({
520
543
  onResize={placeView}
521
544
  minZoom={stage.minZoom}
522
545
  maxZoom={stage.maxZoom}
523
- hostRef={loupeHostRef}
524
- overlay={
525
- <>
526
- {stage.overlay?.(renderCtx)}
527
- {lens}
528
- </>
529
- }
546
+ overlay={stage.overlay?.(renderCtx)}
530
547
  >
531
548
  {instrument.render(renderCtx)}
532
549
  </Stage>
533
550
  );
534
- } else if (lens) {
535
- body = (
536
- <div ref={loupeHostRef} className="lk-trial__loupe-host">
537
- {instrument.render(renderCtx)}
538
- {lens}
539
- </div>
540
- );
541
551
  } else {
542
552
  body = instrument.render(renderCtx);
543
553
  }
@@ -606,14 +616,16 @@ function TrialRuntime({
606
616
  const scopedBody = (
607
617
  <WeaselProvider isolate>
608
618
  <PointerContextProvider store={pointer}>
609
- <CameraPublishContext.Provider value={publishCamera}>{body}</CameraPublishContext.Provider>
619
+ <CameraPublishContext.Provider value={publishCamera}>
620
+ <LoupeSwitchContext.Provider value={loupeSwitch}>{body}</LoupeSwitchContext.Provider>
621
+ </CameraPublishContext.Provider>
610
622
  </PointerContextProvider>
611
623
  </WeaselProvider>
612
624
  );
613
625
 
614
626
  return (
615
627
  <TrialIdProvider trialId={record.id}>
616
- <AnnotationsContext.Provider value={annotationsCap ? annotations : null}>
628
+ <AnnotationsContext.Provider value={annotationsCap ? provided : null}>
617
629
  <CameraWheelContext.Provider value={wheelSlot}>
618
630
  <TrialChrome
619
631
  job={jobCap ? job : undefined}
@@ -8,7 +8,7 @@ import {
8
8
  import { useStore } from 'zustand/react';
9
9
  import { CameraRegistryContext } from '../canvas/cameraRegistry';
10
10
  import { zoomCameraTo } from '../canvas/cameraZoom';
11
- import { builtinContributions } from '../chrome/builtins';
11
+ import { builtinContributions, TRANSIENT_BUILTINS } from '../chrome/builtins';
12
12
  import { mergeContributions, suppressContributions } from '../chrome/merge';
13
13
  import { PaletteRegion } from '../chrome/regions/PaletteRegion';
14
14
  import { SidebarRegion } from '../chrome/regions/SidebarRegion';
@@ -38,6 +38,7 @@ export interface LoupeBindings {
38
38
  toggle: () => void;
39
39
  }
40
40
 
41
+ /** The trial's undo history as its chrome sees it. */
41
42
  export interface UndoBindings {
42
43
  canUndo: boolean;
43
44
  canRedo: boolean;
@@ -54,8 +55,8 @@ export interface TrialChromeProps {
54
55
  undoBindings?: UndoBindings;
55
56
  /** Supplied when the instrument declares a `job`. */
56
57
  job?: JobHandle;
57
- /** Supplied when the instrument declares a `loupe`. The lens itself is the
58
- * trial's to mount; the chrome only offers the switch. */
58
+ /** Supplied while a lens is mounted in the trial. The lens itself is the
59
+ * instrument's to mount; the chrome only offers the switch. */
59
60
  loupe?: LoupeBindings;
60
61
  /** Contributions the trial runtime itself adds — the drag palette and the
61
62
  * layer list, which depend on runtime state the instrument cannot reach. */
@@ -157,6 +158,7 @@ export function TrialChrome({
157
158
  canRedo: undoBindings?.canRedo ?? false,
158
159
  undo: undoBindings?.undo ?? NO_OP,
159
160
  redo: undoBindings?.redo ?? NO_OP,
161
+ hasLoupe: loupe != null,
160
162
  loupeOn: loupe?.on ?? false,
161
163
  toggleLoupe: loupe?.toggle ?? NO_OP,
162
164
  configSchema,
@@ -233,6 +235,7 @@ export function TrialChrome({
233
235
  [...(chrome ?? [])],
234
236
  ),
235
237
  suppress ?? [],
238
+ TRANSIENT_BUILTINS,
236
239
  ),
237
240
  [instrument, ctx, trialChrome, chrome, suppress, lab.controls],
238
241
  );
@@ -1,3 +1,5 @@
1
+ export type { LoupeSwitch } from './loupeSwitch';
2
+ export { LoupeSwitchContext } from './loupeSwitch';
1
3
  export type { TrialProps } from './Trial';
2
4
  export { Trial } from './Trial';
3
5
  export type { TrialBodyProps } from './TrialBody';
@@ -0,0 +1,15 @@
1
+ import { createContext } from 'react';
2
+
3
+ /**
4
+ * A trial's loupe switch, as a lens mounted inside the trial finds it. The
5
+ * trial offers its toolbar toggle while at least one lens has called `mount`,
6
+ * and every lens reads `on` as whether it is turned on.
7
+ */
8
+ export interface LoupeSwitch {
9
+ on: boolean;
10
+ /** Reports a lens mounted; the returned function reports it gone. */
11
+ mount: () => () => void;
12
+ }
13
+
14
+ /** The loupe switch of the trial around the caller, or `null` outside one. */
15
+ export const LoupeSwitchContext = createContext<LoupeSwitch | null>(null);
@@ -1,83 +0,0 @@
1
- import * as react from 'react';
2
- import { ReactNode } from 'react';
3
- import { S as StanceProps } from './PrefsRow.d-TOZ7BgD9.js';
4
-
5
- /** One layer. With a body (see `renderBody`) it draws as a card that folds its
6
- * body away; without one, as a one-line row. Either can hold child layers. */
7
- interface LayerListItem {
8
- id: string;
9
- /** The layer's name. Also what its controls are labeled by. */
10
- label: string;
11
- /** Shown in the header in place of `label` — a select that switches the
12
- * layer's kind, say. */
13
- title?: ReactNode;
14
- /** Fixed-size content before the label: a color swatch, a kind icon. */
15
- leading?: ReactNode;
16
- /** Drawn in the drag handle in place of the grip — an ordinal. */
17
- badge?: ReactNode;
18
- /** Which of its list this layer is: an index into the theme's tone list, or
19
- * a color. A card draws its edge and handle in it and recolors its controls. */
20
- tone?: StanceProps['tone'];
21
- /** Cannot be dragged, hidden or removed, walls off drops on either side, and
22
- * selects alone rather than joining a multi-selection. */
23
- locked?: boolean;
24
- /** Shown or not, when the list has `onVisibilityChange`. Default true. */
25
- visible?: boolean;
26
- children?: LayerListItem[];
27
- /** Folds the body and children away until the user opens them.
28
- * `collapsedIds` overrides it. */
29
- defaultCollapsed?: boolean;
30
- }
31
- /**
32
- * A reorder: `ids`, siblings under `parentId` (`null` at the top level), now
33
- * sit as one block in list order starting at `index` of the parent's children.
34
- * A drag never moves a layer to another parent.
35
- */
36
- interface LayerMove {
37
- ids: string[];
38
- parentId: string | null;
39
- index: number;
40
- }
41
- /** Props for `<LayerList>`. */
42
- interface LayerListProps {
43
- items: LayerListItem[];
44
- /** Omit for a list the user cannot reorder. */
45
- onReorder?: (move: LayerMove) => void;
46
- /** Giving both makes rows selectable: a click selects one, shift-click adds
47
- * or removes one, and dragging a selected row drags its selected siblings. */
48
- selectedIds?: readonly string[];
49
- onSelect?: (ids: string[]) => void;
50
- /** Adds a visibility checkbox to every unlocked layer. */
51
- onVisibilityChange?: (id: string, visible: boolean) => void;
52
- /** Adds a remove button to every unlocked layer. */
53
- onRemove?: (id: string) => void;
54
- /** A layer's controls. Returning `null` leaves that layer a one-line row. */
55
- renderBody?: (item: LayerListItem) => ReactNode;
56
- /** Ids whose body and children are folded away. Giving it makes folding
57
- * controlled; `onCollapsedChange` fires either way. */
58
- collapsedIds?: readonly string[];
59
- onCollapsedChange?: (ids: string[]) => void;
60
- /** Heading above the list. */
61
- title?: string;
62
- /** Names the list for assistive tech when it has no `title`. */
63
- label?: string;
64
- /** Kinds the user can add, as buttons beside the title. Shown only with `onAdd`. */
65
- addKinds?: string[];
66
- onAdd?: (kind: string) => void;
67
- /** Shown in place of the list when `items` is empty. */
68
- empty?: ReactNode;
69
- /** Appended to the root's class list — the module's own names are hashed. */
70
- className?: string;
71
- }
72
- /** Returns `items` with `move` applied, for a consumer that holds the tree as state. */
73
- declare function moveLayers(items: LayerListItem[], move: LayerMove): LayerListItem[];
74
- /**
75
- * An ordered, drag-reorderable list of layers — a document's objects, a lab's
76
- * canvases, a fill's effects. Each layer is a one-line row or, with a body, a
77
- * card whose controls fold away; either can nest child layers. Selection,
78
- * visibility, removal and an add palette are each on when their handler is given.
79
- */
80
- declare function LayerList(props: LayerListProps): react.JSX.Element;
81
-
82
- export { LayerList as L, moveLayers as m };
83
- export type { LayerListItem as a, LayerListProps as b, LayerMove as c };
@@ -1,79 +0,0 @@
1
- import { Stance } from '@weasel-js/theme';
2
- import { ReactNode } from 'react';
3
- import { ToolPrefLeaf } from '@weasel-js/core';
4
-
5
- /** What one unit is worth in base units: `base = value * factor + offset`. */
6
- interface UnitScale {
7
- factor: number;
8
- /** Where this unit puts zero, in base units. Absent is 0 — a pure scale. */
9
- offset?: number;
10
- }
11
- /** One unit's conversion. A bare number is the `{ factor }` shorthand. */
12
- type UnitEntry = number | UnitScale;
13
-
14
- /**
15
- * How a number shows, speaks and reads back, as plain data: `{ kind: 'fraction',
16
- * maxDenominator: 64 }`. Data rather than a function so a tagged value keeps its
17
- * presentation through JSON, history snapshots and `structuredClone`. The
18
- * behavior lives on the registered {@link DisplayKind} of the same `kind`.
19
- */
20
- interface Display {
21
- readonly kind: string;
22
- readonly [option: string]: unknown;
23
- }
24
-
25
- /**
26
- * Display-formatter for numbers. Use this anywhere a number is shown to
27
- * a user. The whole point: negative values get prefixed with the real
28
- * MINUS SIGN (U+2212) instead of the ASCII HYPHEN-MINUS (U+002D) that
29
- * `toLocaleString` and template literals produce by default.
30
- *
31
- * U+2212 is the same visual width as `+` and reads as a sign rather
32
- * than a hyphen — columns of signed numbers align cleanly and the
33
- * glyph doesn't get confused with a bullet or list dash.
34
- */
35
- declare const MINUS_SIGN = "\u2212";
36
- /**
37
- * Formats a number for display, substituting {@link MINUS_SIGN} for the ASCII
38
- * hyphen `toLocaleString` emits. Non-finite values stringify as-is.
39
- */
40
- declare function formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
41
- /** Suffixes a person may type, each mapped to its conversion into the shown unit. */
42
- type UnitTable = Record<string, UnitEntry>;
43
-
44
- /**
45
- * What a surface holds, and which of its peers it is. Every surface that takes
46
- * these takes both, and the theme decides how each looks.
47
- */
48
- interface StanceProps {
49
- /** The class of content the surface holds. Omitted, it draws as it always has. */
50
- stance?: Stance;
51
- /**
52
- * Which of its peers this is: an index into the theme's tone list, wrapping,
53
- * or a color given directly. Independent of `stance`, and allowed without one.
54
- */
55
- tone?: number | string;
56
- }
57
-
58
- /** What a {@link PrefRenderer} is given for the leaf it is rendering. */
59
- interface PrefRenderContext {
60
- /** Dotted path of the leaf within the schema root. */
61
- path: string;
62
- /** The schema node. App renderers narrow this to their own kind shape. */
63
- pref: ToolPrefLeaf;
64
- /** Current value — `values` at `path`, falling back to `pref.default`. */
65
- value: unknown;
66
- setValue: (value: unknown) => void;
67
- /** Whether this leaf is currently auto — not pinned, computed by the owner. */
68
- auto: boolean;
69
- /** Toggle this leaf's auto state. */
70
- setAuto: (next: boolean) => void;
71
- }
72
- /**
73
- * Renders the control cell for one preference leaf. Returning `null`
74
- * collapses the row.
75
- */
76
- type PrefRenderer = (ctx: PrefRenderContext) => ReactNode;
77
-
78
- export { MINUS_SIGN as M, formatNumber as f };
79
- export type { Display as D, PrefRenderer as P, StanceProps as S, UnitTable as U, PrefRenderContext as a };
@@ -1,141 +0,0 @@
1
- import * as react from 'react';
2
- import { ReactNode } from 'react';
3
- import { S as StanceProps } from './PrefsRow.d-TOZ7BgD9.js';
4
-
5
- /**
6
- * How much room a container gives its rows — gaps, padding, and field height,
7
- * moved together. Set on any container in the family; it reaches every
8
- * descendant, so an inner group can differ from the panel around it.
9
- */
10
- type PropertyDensity = 'tight' | 'normal' | 'roomy';
11
- /** Where an inline row's label and control sit on the row's cross axis. Set on
12
- * a container to line a whole column of them up. */
13
- type PropertyAlign = 'start' | 'center' | 'end' | 'baseline';
14
- /** Metric props every container in the family takes. Both reach descendants,
15
- * so the nearest container that states one wins. */
16
- interface PropertyMetricProps {
17
- /** Room the rows get. Unset inherits from an outer container, else `normal`. */
18
- density?: PropertyDensity;
19
- /**
20
- * Cross-axis alignment of an inline row's label and control. `baseline` sits
21
- * the control on the label's first-line baseline, which is what lines a
22
- * column of swatches up against labels of different heights. Unset keeps each
23
- * variant's own alignment — a color row centers its label and swatch and
24
- * sinks the pair to the row's bottom edge, which keeps a paired alpha track
25
- * level with its neighbour.
26
- */
27
- align?: PropertyAlign;
28
- }
29
- /** Props for `<PropertyPanel>`. */
30
- interface PropertyPanelProps extends PropertyMetricProps, StanceProps {
31
- title?: ReactNode;
32
- /** Controls on the trailing edge of the title row — a switch, a clear
33
- * button. They sit outside the heading, so they neither take its type nor
34
- * join its accessible name. */
35
- actions?: ReactNode;
36
- /**
37
- * Lets text inside the panel be selected and copied. A panel is chrome by
38
- * default, so a drag across it moves a slider rather than painting a
39
- * selection; a `debug` panel defaults to selectable, since what it shows —
40
- * ids, keys, traces — is there to be copied.
41
- */
42
- selectable?: boolean;
43
- children: ReactNode;
44
- className?: string;
45
- }
46
- /**
47
- * A titled panel holding property rows — the sidebar container the rest of
48
- * this module's components fill.
49
- *
50
- * `stance` says what kind of content it holds and `tone` which of its peers it
51
- * is; the theme's `--wzl-panel-*` and `--wzl-stance-*` slots decide how each looks. A stanced
52
- * panel's title takes the row-label recipe rather than the display title.
53
- */
54
- declare function PropertyPanel({ title, actions, selectable, children, className, density, align, stance, tone, }: PropertyPanelProps): react.JSX.Element;
55
- /** How a property list packs its rows into two columns. */
56
- type PropertyListPack = 'auto-color' | 'pairs' | 'one-up';
57
- /** Props for `<PropertyList>`. */
58
- interface PropertyListProps extends PropertyMetricProps {
59
- children: ReactNode;
60
- className?: string;
61
- /**
62
- * How rows pack into the 2-column grid.
63
- * - `'auto-color'` (default): only color rows pair side-by-side; everything
64
- * else spans the full width. Right for sparse top-level panels.
65
- * - `'pairs'`: every row auto-places into the 2-column grid two-per-row.
66
- * Headers and subpanels still span the full width; wrap any other
67
- * full-width child in `<PropertySpan>`. Right for dense effect bodies.
68
- * - `'one-up'`: every row spans the full width, color rows included. Right
69
- * for a palette — a column of swatches read as a group.
70
- */
71
- pack?: PropertyListPack;
72
- }
73
- /**
74
- * Grid container for PropertyRows. Use standalone for chrome-less layouts, or
75
- * nest inside <PropertyPanel/> for the standard glass card.
76
- */
77
- declare const PropertyList: react.ForwardRefExoticComponent<PropertyListProps & react.RefAttributes<HTMLDivElement>>;
78
- /** Props for `<PropertySpan>`. */
79
- interface PropertySpanProps {
80
- children: ReactNode;
81
- className?: string;
82
- }
83
- /**
84
- * Wrapper that makes an arbitrary child span both columns of a
85
- * `<PropertyList pack="pairs">`, a `<PropertyGroup pack="pairs">` body, or a
86
- * `<Subpanel>`. `<PropertyRow span>` covers the row case; this covers
87
- * everything else a consumer puts in the grid.
88
- */
89
- declare function PropertySpan({ children, className }: PropertySpanProps): react.JSX.Element;
90
- /** Which control shape a row holds, which decides its intrinsic layout. */
91
- type PropertyRowVariant = 'default' | 'color' | 'checkbox';
92
- /** Whether a row's label sits above its control or beside it. */
93
- type PropertyRowLayout = 'block' | 'inline';
94
- /** Props for `<PropertyRow>`. */
95
- interface PropertyRowProps extends PropertyMetricProps {
96
- label: ReactNode;
97
- /** Right-aligned readout shown next to the label (e.g. current value). */
98
- readout?: ReactNode;
99
- /**
100
- * Help text for the row, shown in a tooltip off an ⓘ affordance beside the
101
- * label. Empty or absent renders no affordance.
102
- */
103
- description?: string;
104
- variant?: PropertyRowVariant;
105
- /**
106
- * Label position relative to the control. Unset takes the variant's own
107
- * orientation: `block` — label above control — for the default variant, and
108
- * `inline` for the color and checkbox variants, which read as a row.
109
- */
110
- layout?: PropertyRowLayout;
111
- /**
112
- * Take the full width of the enclosing grid. Only has an effect inside a
113
- * `<PropertyList pack="pairs">` or a `<Subpanel>`; elsewhere rows are already
114
- * full width.
115
- */
116
- span?: boolean;
117
- children: ReactNode;
118
- htmlFor?: string;
119
- /** The control is a group of several (a segmented toggle), not one element.
120
- * The row then renders as a `<div>`: a `<label>` hands a click on its text to
121
- * its first control, which in a group means selecting the first option. Each
122
- * member has to carry its own name. */
123
- group?: boolean;
124
- className?: string;
125
- /** The row is auto: its value is not pinned and the owner computes it. The
126
- * control is hidden — its box kept, so the row does not resize — and the
127
- * row reads out the word instead. */
128
- auto?: boolean;
129
- /** Given, the row's label toggles `auto` when it is clicked. Omitted, the row
130
- * can show an auto state but not change it.
131
- *
132
- * The label then names that toggle, so a control passed as `children` has to
133
- * carry its own accessible name — every `<PropertyField>` does. */
134
- onAutoChange?: (next: boolean) => void;
135
- }
136
- /** The label-plus-control frame `<PropertyField>` draws its rows in. Use it
137
- * directly for a control no field kind covers. */
138
- declare function PropertyRow({ label, readout, description, variant, layout, span, children, htmlFor, group, className, density, align, auto, onAutoChange, }: PropertyRowProps): react.JSX.Element;
139
-
140
- export { PropertyList as c, PropertyPanel as e, PropertyRow as g, PropertySpan as j };
141
- export type { PropertyMetricProps as P, PropertyRowLayout as a, PropertyListPack as b, PropertyListProps as d, PropertyPanelProps as f, PropertyRowProps as h, PropertyRowVariant as i, PropertySpanProps as k, PropertyAlign as l, PropertyDensity as m };