@weasel-js/labkit 1.3.0-pre.0 → 1.4.0-pre.0
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.
- package/dist/_dts/CanvasStackContext-DtAn_3O-.d.ts +42 -0
- package/dist/_dts/DrawCommand-B3bskUsC.d.ts +564 -0
- package/dist/_dts/{PrefsForm-BYa6cWnO.d.ts → PrefsForm-BkUJZx0A.d.ts} +4 -1
- package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +21 -0
- package/dist/_dts/frac-Cp3NivlC.d.ts +728 -0
- package/dist/_dts/index-BDVzvRzQ.d.ts +236 -0
- package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +87 -0
- package/dist/_dts/{DrawCommand-DBB45NfN.d.ts → types-C-gh9Ap-.d.ts} +1 -582
- package/dist/_dts/{types-AHlQxBNN.d.ts → types-D6s4b7if.d.ts} +2 -2
- package/dist/_dts/{types-1Sdxy_Pv.d.ts → types-DYMaEvM5.d.ts} +26 -1
- package/dist/_dts/{useTrialState-CsGMjhu9.d.ts → useTrialState-gmMvZqPc.d.ts} +7 -2
- package/dist/canvas/index.d.ts +43 -25
- package/dist/canvas/index.js +5 -3
- package/dist/chrome/index.d.ts +9 -6
- package/dist/chrome/index.js +7 -6
- package/dist/chunk-2AYGEN57.js +32 -0
- package/dist/chunk-2AYGEN57.js.map +1 -0
- package/dist/{chunk-RVNN4CHQ.js → chunk-3P5TTTA7.js} +16 -8
- package/dist/chunk-3P5TTTA7.js.map +1 -0
- package/dist/{chunk-NS54R52R.js → chunk-3ZELRRBV.js} +45 -40
- package/dist/chunk-3ZELRRBV.js.map +1 -0
- package/dist/{chunk-HFDVGF4X.js → chunk-4EY67BKJ.js} +32 -7
- package/dist/chunk-4EY67BKJ.js.map +1 -0
- package/dist/chunk-5TCEVMPI.js +140 -0
- package/dist/chunk-5TCEVMPI.js.map +1 -0
- package/dist/{chunk-O7NVSCLN.js → chunk-6JMU56CU.js} +133 -134
- package/dist/chunk-6JMU56CU.js.map +1 -0
- package/dist/{chunk-CPUJ3QXL.js → chunk-CQLQPQ4P.js} +2 -2
- package/dist/chunk-CQLQPQ4P.js.map +1 -0
- package/dist/chunk-CTRKTLYZ.js +33 -0
- package/dist/chunk-CTRKTLYZ.js.map +1 -0
- package/dist/chunk-EQIB2PGC.js +458 -0
- package/dist/chunk-EQIB2PGC.js.map +1 -0
- package/dist/chunk-HEORBLVT.js +7128 -0
- package/dist/chunk-HEORBLVT.js.map +1 -0
- package/dist/{chunk-V7PRSIRQ.js → chunk-JDDIZSLL.js} +4 -4
- package/dist/chunk-JDDIZSLL.js.map +1 -0
- package/dist/chunk-LITE5YFB.js +669 -0
- package/dist/chunk-LITE5YFB.js.map +1 -0
- package/dist/{chunk-Z6HJ5FEM.js → chunk-ROQDRXKW.js} +732 -315
- package/dist/chunk-ROQDRXKW.js.map +1 -0
- package/dist/chunk-SBXNQW4G.js +3 -0
- package/dist/chunk-SBXNQW4G.js.map +1 -0
- package/dist/{chunk-4TMMVIDM.js → chunk-XJ6N32QP.js} +50 -6
- package/dist/chunk-XJ6N32QP.js.map +1 -0
- package/dist/chunk-YQJNT4XQ.js +293 -0
- package/dist/chunk-YQJNT4XQ.js.map +1 -0
- package/dist/controls/index.d.ts +4 -28
- package/dist/controls/index.js +3 -3
- package/dist/dragdrop/index.d.ts +50 -5
- package/dist/dragdrop/index.js +3 -1
- package/dist/index.d.ts +660 -302
- package/dist/index.js +1793 -587
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +6 -5
- package/dist/layers/index.js +4 -2
- package/dist/loupe/index.d.ts +187 -0
- package/dist/loupe/index.js +7 -0
- package/dist/loupe/index.js.map +1 -0
- package/dist/passthrough/weasel-canvas.d.ts +3 -1
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +151 -91
- package/dist/passthrough/weasel-ui.js +3 -2
- package/dist/primitives/index.d.ts +3 -1
- package/dist/primitives/index.js +6 -5
- package/dist/state/index.d.ts +3 -3
- package/dist/state/index.js +2 -2
- package/dist/styles.css +229 -703
- package/dist/surface/index.d.ts +18 -2
- package/dist/surface/index.js +2 -2
- package/dist/ui/layers/index.d.ts +8 -2
- package/dist/ui/layers/index.js +2 -4
- package/dist/undo/index.d.ts +5 -4
- package/package.json +14 -7
- package/src/annotations/AnnotationOverlay.tsx +205 -0
- package/src/annotations/AnnotationTargets.tsx +37 -0
- package/src/annotations/Annotations.less +129 -0
- package/src/annotations/Annotations.meaning.test.tsx +111 -0
- package/src/annotations/Annotations.overlay.test.tsx +142 -0
- package/src/annotations/AnnotationsContext.ts +49 -0
- package/src/annotations/ExportMenu.test.tsx +86 -0
- package/src/annotations/ExportMenu.tsx +182 -0
- package/src/annotations/MarkList.tsx +75 -0
- package/src/annotations/capture.test.ts +140 -0
- package/src/annotations/capture.ts +237 -0
- package/src/annotations/drawOne.test.ts +56 -0
- package/src/annotations/drawOne.ts +47 -0
- package/src/annotations/frac.test.ts +59 -0
- package/src/annotations/frac.ts +66 -0
- package/src/annotations/history.test.ts +82 -0
- package/src/annotations/history.ts +118 -0
- package/src/annotations/index.ts +37 -0
- package/src/annotations/paint.test.ts +125 -0
- package/src/annotations/paint.ts +130 -0
- package/src/annotations/staleness.test.ts +56 -0
- package/src/annotations/staleness.ts +41 -0
- package/src/annotations/store.test.ts +175 -0
- package/src/annotations/store.ts +317 -0
- package/src/annotations/svgNodes.test.ts +77 -0
- package/src/annotations/svgNodes.ts +74 -0
- package/src/annotations/toolMap.test.ts +36 -0
- package/src/annotations/toolMap.ts +54 -0
- package/src/annotations/types.ts +222 -0
- package/src/annotations/view.test.ts +37 -0
- package/src/annotations/view.ts +40 -0
- package/src/canvas/AGENTS.md +55 -5
- package/src/canvas/CanvasStack.test.tsx +27 -1
- package/src/canvas/CanvasStack.tsx +29 -4
- package/src/canvas/CanvasStackContext.ts +23 -4
- package/src/canvas/camera.test.ts +78 -0
- package/src/canvas/camera.ts +62 -0
- package/src/canvas/canvasCoords.test.ts +26 -0
- package/src/canvas/canvasCoords.ts +20 -7
- package/src/canvas/index.ts +5 -1
- package/src/canvas/useLayerScheduler.ts +7 -3
- package/src/canvas/usePanZoom.test.ts +36 -5
- package/src/canvas/usePanZoom.ts +20 -15
- package/src/canvas/worldSpec.test.ts +63 -0
- package/src/canvas/worldSpec.ts +48 -0
- package/src/chrome/builtins.test.ts +31 -1
- package/src/chrome/builtins.tsx +63 -19
- package/src/chrome/regions/PaletteRegion.test.tsx +22 -1
- package/src/chrome/regions/PaletteRegion.tsx +4 -0
- package/src/chrome/regions/SidebarRegion.tsx +37 -13
- package/src/chrome/regions/ToolbarRegion.tsx +2 -1
- package/src/chrome/regions/ViewportRegion.tsx +9 -1
- package/src/chrome/regions/regions.test.tsx +6 -1
- package/src/chrome/types.ts +20 -0
- package/src/config/builder.test.ts +7 -1
- package/src/config/builder.ts +1 -5
- package/src/config/resolve.test.ts +1 -2
- package/src/controls/ControlPanel.stories.tsx +41 -2
- package/src/controls/ControlPanel.test.tsx +69 -9
- package/src/controls/ControlPanel.tsx +106 -25
- package/src/dragdrop/DragDropRuntime.tsx +8 -2
- package/src/dragdrop/dragDrop.test.tsx +29 -2
- package/src/dragdrop/index.ts +11 -0
- package/src/index.test.ts +22 -0
- package/src/index.ts +136 -4
- package/src/instrument/SineWave.smoke.test.tsx +1 -0
- package/src/instrument/types.ts +24 -1
- package/src/lab/Lab.surface.test.tsx +162 -0
- package/src/lab/Lab.tsx +102 -17
- package/src/lab/Lab.undock.test.tsx +85 -0
- package/src/lab/LabFullChrome.stories.tsx +20 -0
- package/src/lab/LabShell.less +11 -0
- package/src/lab/Workspace.less +73 -0
- package/src/lab/Workspace.surface.test.tsx +2 -0
- package/src/lab/Workspace.tsx +97 -8
- package/src/lab/index.ts +1 -1
- package/src/lab/panelHost.ts +37 -0
- package/src/layers/AGENTS.md +1 -1
- package/src/layers/LayerList.tsx +1 -1
- package/src/loupe/AGENTS.md +49 -0
- package/src/loupe/CanvasLoupe.tsx +83 -0
- package/src/loupe/DomLoupe.tsx +62 -0
- package/src/loupe/Loupe.less +38 -0
- package/src/loupe/LoupeBubble.tsx +32 -0
- package/src/loupe/TrialLoupe.tsx +93 -0
- package/src/loupe/canvasLens.test.ts +205 -0
- package/src/loupe/canvasLens.ts +141 -0
- package/src/loupe/index.ts +19 -0
- package/src/loupe/types.test.ts +29 -0
- package/src/loupe/types.ts +90 -0
- package/src/loupe/useHostSize.ts +30 -0
- package/src/loupe/useLoupe.test.tsx +197 -0
- package/src/loupe/useLoupe.ts +189 -0
- package/src/passthrough/weasel-ui.ts +25 -17
- package/src/primitives/FloatingPanel.test.tsx +76 -1
- package/src/primitives/FloatingPanel.tsx +19 -3
- package/src/primitives/ScaleIndicator.test.tsx +7 -2
- package/src/primitives/Sidebar.less +18 -0
- package/src/primitives/Toolbar.less +8 -0
- package/src/primitives/Toolbar.tsx +5 -0
- package/src/primitives/useRovingTabIndex.test.ts +21 -0
- package/src/primitives/useRovingTabIndex.ts +26 -17
- package/src/state/document.test.ts +23 -0
- package/src/state/document.ts +14 -2
- package/src/state/index.ts +2 -0
- package/src/state/store.test.ts +24 -0
- package/src/state/store.ts +35 -1
- package/src/state/types.ts +11 -0
- package/src/state/undock.test.ts +40 -0
- package/src/state/undock.ts +39 -0
- package/src/styles.less +2 -4
- package/src/surface/SurfaceContext.ts +4 -0
- package/src/surface/index.ts +8 -3
- package/src/surface/useSurfaceTile.test.tsx +2 -0
- package/src/surface/useSurfaceTile.ts +7 -1
- package/src/surface/useTiledSurface.test.tsx +58 -0
- package/src/surface/useTiledSurface.ts +38 -3
- package/src/theme/Interstellar.stories.tsx +1 -1
- package/src/trial/Trial.annotations.persist.test.tsx +166 -0
- package/src/trial/Trial.annotations.test.tsx +121 -0
- package/src/trial/Trial.canvas.test.tsx +76 -1
- package/src/trial/Trial.config.test.tsx +3 -8
- package/src/trial/Trial.less +19 -0
- package/src/trial/Trial.loupe.test.tsx +128 -0
- package/src/trial/Trial.stories.tsx +1 -0
- package/src/trial/Trial.test.tsx +12 -0
- package/src/trial/Trial.trialId.test.tsx +36 -0
- package/src/trial/Trial.tsx +215 -40
- package/src/trial/TrialChrome.tsx +35 -0
- package/src/trial/UndockedSections.tsx +65 -0
- package/src/trial/trialOps.test.ts +21 -0
- package/src/trial/trialOps.ts +5 -3
- package/src/ui/layers/index.ts +5 -1
- package/dist/_dts/index-DgcNdEdh.d.ts +0 -314
- package/dist/chunk-4TMMVIDM.js.map +0 -1
- package/dist/chunk-CPUJ3QXL.js.map +0 -1
- package/dist/chunk-HFDVGF4X.js.map +0 -1
- package/dist/chunk-NS54R52R.js.map +0 -1
- package/dist/chunk-O2BULFHX.js +0 -152
- package/dist/chunk-O2BULFHX.js.map +0 -1
- package/dist/chunk-O7NVSCLN.js.map +0 -1
- package/dist/chunk-PO6L3NSH.js +0 -366
- package/dist/chunk-PO6L3NSH.js.map +0 -1
- package/dist/chunk-PQJ5B2U2.js +0 -27
- package/dist/chunk-PQJ5B2U2.js.map +0 -1
- package/dist/chunk-RVNN4CHQ.js.map +0 -1
- package/dist/chunk-TLGRYALP.js +0 -6151
- package/dist/chunk-TLGRYALP.js.map +0 -1
- package/dist/chunk-V7PRSIRQ.js.map +0 -1
- package/dist/chunk-Z6HJ5FEM.js.map +0 -1
- package/dist/chunk-ZZAYVX4X.js +0 -505
- package/dist/chunk-ZZAYVX4X.js.map +0 -1
- package/src/primitives/DragHandleGlyph.tsx +0 -29
- package/src/ui/format.test.ts +0 -33
- package/src/ui/format.ts +0 -30
- package/src/ui/layers/LayerStack.less +0 -143
- package/src/ui/layers/LayerStack.stories.tsx +0 -45
- package/src/ui/layers/LayerStack.test.tsx +0 -170
- package/src/ui/layers/LayerStack.tsx +0 -208
- package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
- package/src/ui/properties/ColorRow.stories.tsx +0 -106
- package/src/ui/properties/CurveField.less +0 -48
- package/src/ui/properties/CurveField.stories.tsx +0 -20
- package/src/ui/properties/CurveField.test.tsx +0 -127
- package/src/ui/properties/CurveField.tsx +0 -184
- package/src/ui/properties/EffectCard.tsx +0 -324
- package/src/ui/properties/Gallery.stories.tsx +0 -178
- package/src/ui/properties/NumberRow.stories.tsx +0 -65
- package/src/ui/properties/PropertyGroup.less +0 -50
- package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
- package/src/ui/properties/PropertyGroup.test.tsx +0 -33
- package/src/ui/properties/PropertyGroup.tsx +0 -45
- package/src/ui/properties/PropertyList.stories.tsx +0 -83
- package/src/ui/properties/PropertyPanel.less +0 -606
- package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
- package/src/ui/properties/PropertyPanel.test.tsx +0 -193
- package/src/ui/properties/PropertyPanel.tsx +0 -469
- package/src/ui/properties/PropertyRow.stories.tsx +0 -53
- package/src/ui/properties/SelectRow.stories.tsx +0 -58
- package/src/ui/properties/SliderRow.stories.tsx +0 -74
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -398
- package/src/ui/properties/TextRow.stories.tsx +0 -62
- package/src/ui/properties/ToggleRow.stories.tsx +0 -57
- package/src/ui/properties/index.ts +0 -39
- package/src/ui/properties/storyLayouts.tsx +0 -36
package/src/trial/Trial.tsx
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { type ReactNode, useContext, useMemo, useRef, useState } from 'react';
|
|
1
|
+
import { type ReactNode, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { useStore } from 'zustand/react';
|
|
3
|
+
import { AnnotationsContext } from '../annotations/AnnotationsContext';
|
|
4
|
+
import { AnnotationTargets } from '../annotations/AnnotationTargets';
|
|
5
|
+
import { annotationsFromJSON } from '../annotations/store';
|
|
6
|
+
import type { AnnotationTargetInfo } from '../annotations/types';
|
|
3
7
|
import { CanvasStack } from '../canvas/CanvasStack';
|
|
4
8
|
import type { CanvasLayerDescriptor } from '../canvas/useLayerScheduler';
|
|
9
|
+
import { applyCamera, type ViewportSize } from '../canvas/worldSpec';
|
|
5
10
|
import type { TrialContribution } from '../chrome/types';
|
|
6
11
|
import { DragOverlay, useDragDrop } from '../dragdrop/DragDropRuntime';
|
|
7
12
|
import { Palette } from '../dragdrop/Palette';
|
|
@@ -9,13 +14,15 @@ import type { Instrument, LayerDescriptor, PaletteItem, RenderContext } from '..
|
|
|
9
14
|
import { useJob } from '../job/useJob';
|
|
10
15
|
import { useLabContext } from '../lab/LabContext';
|
|
11
16
|
import { LayerList } from '../layers/LayerList';
|
|
12
|
-
import {
|
|
17
|
+
import { TrialLoupe } from '../loupe/TrialLoupe';
|
|
18
|
+
import { resolveLoupe } from '../loupe/types';
|
|
19
|
+
import { LabStoreContext, TrialIdProvider } from '../state/context';
|
|
13
20
|
import type { LabStore } from '../state/store';
|
|
14
21
|
import type { TrialRecord } from '../state/types';
|
|
15
22
|
import { as2DView, DEFAULT_VIEW } from '../state/view';
|
|
16
23
|
import { createEventBus, type EventBus } from '../undo/eventBus';
|
|
17
24
|
import { pushSnapshot, redo as undoRedo, undo as undoUndo } from '../undo/undoStack';
|
|
18
|
-
import type { UndoBindings } from './TrialChrome';
|
|
25
|
+
import type { LoupeBindings, UndoBindings } from './TrialChrome';
|
|
19
26
|
import { TrialChrome } from './TrialChrome';
|
|
20
27
|
|
|
21
28
|
/** Props for `<Trial>`. */
|
|
@@ -67,10 +74,12 @@ interface TrialRuntimeProps {
|
|
|
67
74
|
|
|
68
75
|
function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: TrialRuntimeProps) {
|
|
69
76
|
const canvasContainerRef = useRef<HTMLDivElement | null>(null);
|
|
77
|
+
const loupeHostRef = useRef<HTMLDivElement | null>(null);
|
|
70
78
|
const updateTrialState = useStore(store, (s) => s.updateTrialState);
|
|
71
79
|
const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
|
|
72
80
|
const updateTrialView = useStore(store, (s) => s.updateTrialView);
|
|
73
81
|
const updateTrialUndoStack = useStore(store, (s) => s.updateTrialUndoStack);
|
|
82
|
+
const updateTrialAnnotations = useStore(store, (s) => s.updateTrialAnnotations);
|
|
74
83
|
const labToolId = useStore(store, (s) => s.activeToolId);
|
|
75
84
|
const setLabTool = useStore(store, (s) => s.setLabTool);
|
|
76
85
|
const setTrialTool = useStore(store, (s) => s.setTrialTool);
|
|
@@ -81,6 +90,15 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
81
90
|
|
|
82
91
|
const [layerVisibility, setLayerVisibility] = useState<Record<string, boolean>>({});
|
|
83
92
|
const [layerOrder, setLayerOrder] = useState<string[] | null>(null);
|
|
93
|
+
const [loupeOn, setLoupeOn] = useState(false);
|
|
94
|
+
|
|
95
|
+
const visibleLayers = useMemo(
|
|
96
|
+
() =>
|
|
97
|
+
(instrument.canvas?.layers ?? [])
|
|
98
|
+
.filter((l) => layerVisibility[l.id] !== false)
|
|
99
|
+
.map((l) => l.id),
|
|
100
|
+
[instrument.canvas, layerVisibility],
|
|
101
|
+
);
|
|
84
102
|
|
|
85
103
|
const undoCap = instrument.undo;
|
|
86
104
|
const undoEvents = useMemo(() => new Set(undoCap?.snapshotOn ?? ['state.change']), [undoCap]);
|
|
@@ -96,11 +114,101 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
96
114
|
|
|
97
115
|
const setView = (v: unknown): void => updateTrialView(record.id, v);
|
|
98
116
|
|
|
117
|
+
// A function `initialView` needs the canvas size, so `trialOps` leaves the
|
|
118
|
+
// view null and the first measurement resolves it. Reset nulls it again,
|
|
119
|
+
// which re-frames — so the guard is the null itself, not a "have I run" flag.
|
|
120
|
+
const placeView = (size: ViewportSize): void => {
|
|
121
|
+
const declared = instrument.canvas?.initialView;
|
|
122
|
+
if (typeof declared !== 'function') return;
|
|
123
|
+
if (record.view != null) return;
|
|
124
|
+
setView(declared(size));
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// The capability's own `targets` thunk, re-read on every call: a target
|
|
128
|
+
// resizing or gaining a dependency must not need the store rebuilt. Held in
|
|
129
|
+
// a ref because the store is built once and closes over it.
|
|
130
|
+
const annotationsCap = instrument.annotations;
|
|
131
|
+
const targetsRef = useRef<() => readonly AnnotationTargetInfo[]>(() => []);
|
|
132
|
+
targetsRef.current = () =>
|
|
133
|
+
annotationsCap ? annotationsCap.targets(record.state, record.config) : [];
|
|
134
|
+
|
|
135
|
+
// One store for the trial's lifetime. Marks do not survive a reload yet —
|
|
136
|
+
// the storage slot is 3d.
|
|
137
|
+
const annotationsRef = useRef<ReturnType<typeof annotationsFromJSON> | null>(null);
|
|
138
|
+
// Read through refs for the same reason `targets` is: the store is built
|
|
139
|
+
// once, and an export must draw against the config the trial holds now.
|
|
140
|
+
const configRef = useRef<unknown>(record.config);
|
|
141
|
+
configRef.current = record.config;
|
|
142
|
+
if (annotationsRef.current === null) {
|
|
143
|
+
// Seeded from wherever the marks were kept: the instrument's own store if
|
|
144
|
+
// it declared one, else this trial's slot.
|
|
145
|
+
const kept = annotationsCap?.storage ? annotationsCap.storage.load() : record.annotations;
|
|
146
|
+
annotationsRef.current = annotationsFromJSON(kept, () => targetsRef.current(), {
|
|
147
|
+
meaning: annotationsCap?.meaning,
|
|
148
|
+
config: () => configRef.current,
|
|
149
|
+
onCapture: (result) => annotationsCap?.onCapture?.(result),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const annotations = annotationsRef.current;
|
|
153
|
+
|
|
154
|
+
// Written on a trailing debounce rather than per notification: a scene
|
|
155
|
+
// mutates every frame of a drag, and a zustand write per frame re-renders
|
|
156
|
+
// every trial in the lab. The unmount flush is not an optimization — the
|
|
157
|
+
// last mark before a trial closes is otherwise lost.
|
|
158
|
+
const saveCap = annotationsCap?.storage;
|
|
159
|
+
const saveRef = useRef<(doc: unknown) => void>(() => {});
|
|
160
|
+
saveRef.current = (doc) => {
|
|
161
|
+
if (saveCap) saveCap.save(doc as never);
|
|
162
|
+
else updateTrialAnnotations(record.id, doc);
|
|
163
|
+
};
|
|
164
|
+
// Only when the pair flips, which is rare: bumping React state on every
|
|
165
|
+
// scene notification would re-render the trial on every frame of a drag,
|
|
166
|
+
// which is the thing the debounce below exists to avoid.
|
|
167
|
+
const [markMoves, setMarkMoves] = useState({ undo: false, redo: false });
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (!annotationsCap) return;
|
|
170
|
+
const read = (): void => {
|
|
171
|
+
const next = { undo: annotations.canUndo(), redo: annotations.canRedo() };
|
|
172
|
+
setMarkMoves((prev) => (prev.undo === next.undo && prev.redo === next.redo ? prev : next));
|
|
173
|
+
};
|
|
174
|
+
read();
|
|
175
|
+
return annotations.subscribe(read);
|
|
176
|
+
}, [annotations, annotationsCap]);
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (!annotationsCap) return;
|
|
180
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
181
|
+
let dirty = false;
|
|
182
|
+
const flush = (): void => {
|
|
183
|
+
if (!dirty) return;
|
|
184
|
+
dirty = false;
|
|
185
|
+
saveRef.current(annotations.toJSON());
|
|
186
|
+
};
|
|
187
|
+
const off = annotations.subscribe(() => {
|
|
188
|
+
dirty = true;
|
|
189
|
+
if (timer) clearTimeout(timer);
|
|
190
|
+
timer = setTimeout(() => {
|
|
191
|
+
timer = null;
|
|
192
|
+
flush();
|
|
193
|
+
}, 250);
|
|
194
|
+
});
|
|
195
|
+
return () => {
|
|
196
|
+
off();
|
|
197
|
+
if (timer) clearTimeout(timer);
|
|
198
|
+
flush();
|
|
199
|
+
};
|
|
200
|
+
}, [annotations, annotationsCap]);
|
|
201
|
+
|
|
99
202
|
// A trial gets its own slot when its instrument declares tools; otherwise it
|
|
100
203
|
// reads the lab's. Which slot a change writes follows from the same thing.
|
|
101
|
-
|
|
204
|
+
// Annotation tools are a trial's own for the same reason: two trials
|
|
205
|
+
// annotating different pictures must not share one active tool.
|
|
206
|
+
const declaresTools = instrument.tools != null || instrument.annotations != null;
|
|
102
207
|
const resolvedToolId = declaresTools
|
|
103
|
-
? (record.activeToolId ??
|
|
208
|
+
? (record.activeToolId ??
|
|
209
|
+
instrument.tools?.initial ??
|
|
210
|
+
instrument.tools?.tools[0]?.id ??
|
|
211
|
+
(instrument.annotations ? 'select' : null))
|
|
104
212
|
: labToolId;
|
|
105
213
|
const setActiveTool = (id: string): void => {
|
|
106
214
|
if (declaresTools) setTrialTool(record.id, id);
|
|
@@ -147,6 +255,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
147
255
|
updateTrialView(record.id, { ...view2d, zoom: z });
|
|
148
256
|
},
|
|
149
257
|
activeToolId: resolvedToolId,
|
|
258
|
+
visibleLayers,
|
|
150
259
|
},
|
|
151
260
|
emit: (event) => {
|
|
152
261
|
snapshotIfNeeded(event);
|
|
@@ -155,23 +264,46 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
155
264
|
job: jobCap ? job : undefined,
|
|
156
265
|
};
|
|
157
266
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
267
|
+
// Marks are undone through weasel history, not through labkit's snapshot
|
|
268
|
+
// stack — the spec's rule, and the reason a mark scene is the truth. A trial
|
|
269
|
+
// declaring both takes the marks first: the most recent thing the user did
|
|
270
|
+
// is what undo is for, and only a mark change moves the mark history.
|
|
271
|
+
const undoState = () => {
|
|
272
|
+
const result = undoUndo(record.undoStack, structuredClone(record.state));
|
|
273
|
+
if (!result) return;
|
|
274
|
+
updateTrialState(record.id, result.snapshot as never);
|
|
275
|
+
updateTrialUndoStack(record.id, result.stack);
|
|
276
|
+
};
|
|
277
|
+
const redoState = () => {
|
|
278
|
+
const result = undoRedo(record.undoStack, structuredClone(record.state));
|
|
279
|
+
if (!result) return;
|
|
280
|
+
updateTrialState(record.id, result.snapshot as never);
|
|
281
|
+
updateTrialUndoStack(record.id, result.stack);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const undoBindings: UndoBindings | undefined =
|
|
285
|
+
undoCap || annotationsCap
|
|
286
|
+
? {
|
|
287
|
+
canUndo: markMoves.undo || (undoCap ? record.undoStack.past.length > 0 : false),
|
|
288
|
+
canRedo: markMoves.redo || (undoCap ? record.undoStack.future.length > 0 : false),
|
|
289
|
+
undo: () => {
|
|
290
|
+
if (annotationsCap && annotations.undo()) return;
|
|
291
|
+
if (undoCap) undoState();
|
|
292
|
+
},
|
|
293
|
+
redo: () => {
|
|
294
|
+
if (annotationsCap && annotations.redo()) return;
|
|
295
|
+
if (undoCap) redoState();
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
: undefined;
|
|
299
|
+
|
|
300
|
+
const loupeCap = useMemo(() => {
|
|
301
|
+
const declared = instrument.loupe;
|
|
302
|
+
if (declared == null) return null;
|
|
303
|
+
return resolveLoupe(typeof declared === 'function' ? declared(record.config) : declared);
|
|
304
|
+
}, [instrument.loupe, record.config]);
|
|
305
|
+
const loupeBindings: LoupeBindings | undefined = loupeCap
|
|
306
|
+
? { on: loupeOn, toggle: () => setLoupeOn((v) => !v) }
|
|
175
307
|
: undefined;
|
|
176
308
|
|
|
177
309
|
const canvasLayers: CanvasLayerDescriptor[] = useMemo(() => {
|
|
@@ -183,11 +315,10 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
183
315
|
return ordered.map((layer) => ({
|
|
184
316
|
id: layer.id,
|
|
185
317
|
visible: layerVisibility[layer.id] !== false,
|
|
186
|
-
render: (ctx, view) => {
|
|
318
|
+
render: (ctx, view, frame) => {
|
|
187
319
|
// Camera applied here, so a layer draws in world coordinates. `zoom`
|
|
188
320
|
// stays in the args for line widths, which must not scale with it.
|
|
189
|
-
ctx
|
|
190
|
-
ctx.scale(view.zoom, view.zoom);
|
|
321
|
+
applyCamera(ctx, view, frame);
|
|
191
322
|
layer.draw(ctx, { state: record.state, config: record.config, zoom: view.zoom });
|
|
192
323
|
},
|
|
193
324
|
}));
|
|
@@ -202,6 +333,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
202
333
|
capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
|
|
203
334
|
canvasContainerRef,
|
|
204
335
|
view: view2d ?? DEFAULT_VIEW,
|
|
336
|
+
worldSpec: instrument.canvas?.worldSpec,
|
|
205
337
|
state: record.state,
|
|
206
338
|
config: record.config,
|
|
207
339
|
setState: (next) => {
|
|
@@ -245,6 +377,20 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
245
377
|
];
|
|
246
378
|
}, [canvasLayers, dragDropResult.drag]);
|
|
247
379
|
|
|
380
|
+
// The lens tracks the pointer over whatever box holds the content: the canvas
|
|
381
|
+
// stack's own element, which it takes from context, or the wrapper below.
|
|
382
|
+
const lens = loupeCap ? (
|
|
383
|
+
<TrialLoupe
|
|
384
|
+
capability={loupeCap}
|
|
385
|
+
enabled={loupeOn}
|
|
386
|
+
state={record.state}
|
|
387
|
+
config={record.config}
|
|
388
|
+
view={view2d ?? DEFAULT_VIEW}
|
|
389
|
+
worldSpec={instrument.canvas?.worldSpec}
|
|
390
|
+
hostRef={loupeHostRef}
|
|
391
|
+
/>
|
|
392
|
+
) : null;
|
|
393
|
+
|
|
248
394
|
let body: ReactNode;
|
|
249
395
|
if (instrument.canvas) {
|
|
250
396
|
body = (
|
|
@@ -253,14 +399,24 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
253
399
|
layers={layersWithFeedback}
|
|
254
400
|
view={view2d ?? DEFAULT_VIEW}
|
|
255
401
|
onViewChange={setView}
|
|
402
|
+
worldSpec={instrument.canvas.worldSpec}
|
|
403
|
+
onResize={placeView}
|
|
256
404
|
minZoom={instrument.canvas.minZoom}
|
|
257
405
|
maxZoom={instrument.canvas.maxZoom}
|
|
258
406
|
>
|
|
259
407
|
{instrument.render(renderCtx)}
|
|
408
|
+
{lens}
|
|
260
409
|
</CanvasStack>
|
|
261
410
|
<DragOverlay drag={dragDropResult.drag} />
|
|
262
411
|
</div>
|
|
263
412
|
);
|
|
413
|
+
} else if (lens) {
|
|
414
|
+
body = (
|
|
415
|
+
<div ref={loupeHostRef} className="lk-trial__loupe-host">
|
|
416
|
+
{instrument.render(renderCtx)}
|
|
417
|
+
{lens}
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
264
420
|
} else {
|
|
265
421
|
body = instrument.render(renderCtx);
|
|
266
422
|
}
|
|
@@ -310,21 +466,40 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
310
466
|
return out;
|
|
311
467
|
}, [paletteNode, layerListNode]);
|
|
312
468
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
chrome={chrome}
|
|
323
|
-
suppress={suppress}
|
|
469
|
+
// Rendered beside the body rather than inside it: an overlay portals itself
|
|
470
|
+
// into the surface container, so where it sits in this tree decides only
|
|
471
|
+
// when its effects run — after the instrument's refs have attached.
|
|
472
|
+
const annotationOverlays = annotationsCap ? (
|
|
473
|
+
<AnnotationTargets
|
|
474
|
+
capability={annotationsCap}
|
|
475
|
+
state={record.state}
|
|
476
|
+
config={record.config}
|
|
477
|
+
annotations={annotations}
|
|
324
478
|
activeToolId={resolvedToolId}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
479
|
+
/>
|
|
480
|
+
) : null;
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
<TrialIdProvider trialId={record.id}>
|
|
484
|
+
<AnnotationsContext.Provider value={annotationsCap ? annotations : null}>
|
|
485
|
+
<TrialChrome
|
|
486
|
+
job={jobCap ? job : undefined}
|
|
487
|
+
loupe={loupeBindings}
|
|
488
|
+
trialId={record.id}
|
|
489
|
+
record={record}
|
|
490
|
+
instrument={instrument}
|
|
491
|
+
isLastTrial={isLast}
|
|
492
|
+
undoBindings={undoBindings}
|
|
493
|
+
trialChrome={extraChrome}
|
|
494
|
+
chrome={chrome}
|
|
495
|
+
suppress={suppress}
|
|
496
|
+
activeToolId={resolvedToolId}
|
|
497
|
+
setActiveTool={setActiveTool}
|
|
498
|
+
>
|
|
499
|
+
{body}
|
|
500
|
+
{annotationOverlays}
|
|
501
|
+
</TrialChrome>
|
|
502
|
+
</AnnotationsContext.Provider>
|
|
503
|
+
</TrialIdProvider>
|
|
329
504
|
);
|
|
330
505
|
}
|
|
@@ -18,6 +18,13 @@ import { LabStoreContext } from '../state/context';
|
|
|
18
18
|
import type { TrialRecord } from '../state/types';
|
|
19
19
|
import { as2DView } from '../state/view';
|
|
20
20
|
import { TrialTitleBar } from './TrialTitleBar';
|
|
21
|
+
import { UndockedSections } from './UndockedSections';
|
|
22
|
+
|
|
23
|
+
/** The trial's loupe as its chrome sees it: a switch and its position. */
|
|
24
|
+
export interface LoupeBindings {
|
|
25
|
+
on: boolean;
|
|
26
|
+
toggle: () => void;
|
|
27
|
+
}
|
|
21
28
|
|
|
22
29
|
export interface UndoBindings {
|
|
23
30
|
canUndo: boolean;
|
|
@@ -35,6 +42,9 @@ export interface TrialChromeProps {
|
|
|
35
42
|
undoBindings?: UndoBindings;
|
|
36
43
|
/** Supplied when the instrument declares a `job`. */
|
|
37
44
|
job?: JobHandle;
|
|
45
|
+
/** Supplied when the instrument declares a `loupe`. The lens itself is the
|
|
46
|
+
* trial's to mount; the chrome only offers the switch. */
|
|
47
|
+
loupe?: LoupeBindings;
|
|
38
48
|
/** Contributions the trial runtime itself adds — the drag palette and the
|
|
39
49
|
* layer list, which depend on runtime state the instrument cannot reach. */
|
|
40
50
|
trialChrome?: readonly TrialContribution[];
|
|
@@ -61,6 +71,7 @@ export function TrialChrome({
|
|
|
61
71
|
isLastTrial,
|
|
62
72
|
undoBindings,
|
|
63
73
|
job,
|
|
74
|
+
loupe,
|
|
64
75
|
trialChrome,
|
|
65
76
|
chrome,
|
|
66
77
|
suppress,
|
|
@@ -74,6 +85,16 @@ export function TrialChrome({
|
|
|
74
85
|
const updateTrialView = useStore(storeCtx.store, (s) => s.updateTrialView);
|
|
75
86
|
const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
|
|
76
87
|
const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
|
|
88
|
+
const undockPanelAction = useStore(storeCtx.store, (s) => s.undockPanel);
|
|
89
|
+
const dockPanelAction = useStore(storeCtx.store, (s) => s.dockPanel);
|
|
90
|
+
const undockedPanels = useStore(storeCtx.store, (s) => s.undockedPanels);
|
|
91
|
+
const undockedIds = useMemo(
|
|
92
|
+
() =>
|
|
93
|
+
Object.values(undockedPanels)
|
|
94
|
+
.filter((p) => p.trialId === trialId)
|
|
95
|
+
.map((p) => p.sectionId),
|
|
96
|
+
[undockedPanels, trialId],
|
|
97
|
+
);
|
|
77
98
|
|
|
78
99
|
// The trial view is opaque to labkit, so the zoom chrome asks for the 2D shape
|
|
79
100
|
// and goes inert when the trial holds something else — an orbit, say.
|
|
@@ -96,6 +117,8 @@ export function TrialChrome({
|
|
|
96
117
|
canRedo: undoBindings?.canRedo ?? false,
|
|
97
118
|
undo: undoBindings?.undo ?? NO_OP,
|
|
98
119
|
redo: undoBindings?.redo ?? NO_OP,
|
|
120
|
+
loupeOn: loupe?.on ?? false,
|
|
121
|
+
toggleLoupe: loupe?.toggle ?? NO_OP,
|
|
99
122
|
configSchema,
|
|
100
123
|
configFields: instrument.config ? [] : (instrument.configSchema?.() ?? []),
|
|
101
124
|
config: record.config,
|
|
@@ -113,6 +136,9 @@ export function TrialChrome({
|
|
|
113
136
|
updateTrialState(trialId, nextState as never);
|
|
114
137
|
}
|
|
115
138
|
},
|
|
139
|
+
undockedPanels: undockedIds,
|
|
140
|
+
undockPanel: (sectionId, as) => undockPanelAction(trialId, sectionId, as),
|
|
141
|
+
dockPanel: (sectionId) => dockPanelAction(trialId, sectionId),
|
|
116
142
|
savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
|
|
117
143
|
saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
|
|
118
144
|
loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
|
|
@@ -132,10 +158,14 @@ export function TrialChrome({
|
|
|
132
158
|
updateTrialConfig,
|
|
133
159
|
updateTrialState,
|
|
134
160
|
undoBindings,
|
|
161
|
+
loupe,
|
|
135
162
|
view2d,
|
|
136
163
|
activeToolId,
|
|
137
164
|
setActiveTool,
|
|
138
165
|
configSchema,
|
|
166
|
+
undockedIds,
|
|
167
|
+
undockPanelAction,
|
|
168
|
+
dockPanelAction,
|
|
139
169
|
]);
|
|
140
170
|
|
|
141
171
|
const contributions = useMemo(
|
|
@@ -185,6 +215,11 @@ export function TrialChrome({
|
|
|
185
215
|
<PaletteRegion contributions={inRegion('palette')} ctx={ctx} />
|
|
186
216
|
<div className="lk-trial__sidebar">
|
|
187
217
|
<SidebarRegion contributions={inRegion('sidebar')} ctx={ctx} />
|
|
218
|
+
<UndockedSections
|
|
219
|
+
trialId={trialId}
|
|
220
|
+
sections={inRegion('sidebar').filter((c) => undockedIds.includes(c.id))}
|
|
221
|
+
onDock={ctx.dockPanel}
|
|
222
|
+
/>
|
|
188
223
|
</div>
|
|
189
224
|
<div className={`lk-trial__content${instrument.canvas ? ' lk-trial__content--flush' : ''}`}>
|
|
190
225
|
{children}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import type { SidebarSection, TrialContribution } from '../chrome/types';
|
|
4
|
+
import { usePanelHosts } from '../lab/panelHost';
|
|
5
|
+
import { panelKey } from '../state/undock';
|
|
6
|
+
|
|
7
|
+
/** Props for `<UndockedSections>`. */
|
|
8
|
+
export interface UndockedSectionsProps {
|
|
9
|
+
trialId: string;
|
|
10
|
+
/** The trial's sidebar contributions that are currently torn out. */
|
|
11
|
+
sections: readonly TrialContribution[];
|
|
12
|
+
onDock: (sectionId: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function Portalled({
|
|
16
|
+
trialId,
|
|
17
|
+
contribution,
|
|
18
|
+
onDock,
|
|
19
|
+
}: {
|
|
20
|
+
trialId: string;
|
|
21
|
+
contribution: TrialContribution;
|
|
22
|
+
onDock: (sectionId: string) => void;
|
|
23
|
+
}) {
|
|
24
|
+
const hosts = usePanelHosts();
|
|
25
|
+
const key = panelKey(trialId, contribution.id);
|
|
26
|
+
const host = useSyncExternalStore(
|
|
27
|
+
(fn) => hosts?.subscribe(fn) ?? (() => {}),
|
|
28
|
+
() => hosts?.get(key) ?? null,
|
|
29
|
+
() => null,
|
|
30
|
+
);
|
|
31
|
+
if (!host || !contribution.item) return null;
|
|
32
|
+
const item = contribution.item as SidebarSection;
|
|
33
|
+
return createPortal(
|
|
34
|
+
<>
|
|
35
|
+
<div className="lk-panel-tile__titlebar">
|
|
36
|
+
<span className="lk-panel-tile__title">{item.title}</span>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
className="lk-panel-tile__dock"
|
|
40
|
+
aria-label={`Dock ${item.title}`}
|
|
41
|
+
title={`Dock ${item.title}`}
|
|
42
|
+
onClick={() => onDock(contribution.id)}
|
|
43
|
+
>
|
|
44
|
+
⇤
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
{item.body}
|
|
48
|
+
</>,
|
|
49
|
+
host,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Renders a torn-out section's body into the workspace panel holding it. The
|
|
54
|
+
* body stays mounted in the trial's React tree — only its DOM moves — so it
|
|
55
|
+
* keeps the trial's context and its own state across an undock. */
|
|
56
|
+
export function UndockedSections({ trialId, sections, onDock }: UndockedSectionsProps) {
|
|
57
|
+
if (sections.length === 0) return null;
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
{sections.map((c) => (
|
|
61
|
+
<Portalled key={c.id} trialId={trialId} contribution={c} onDock={onDock} />
|
|
62
|
+
))}
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -167,3 +167,24 @@ describe('reorderTrials', () => {
|
|
|
167
167
|
expect(next.map((w) => w.id)).toEqual(['c', 'a', 'b']);
|
|
168
168
|
});
|
|
169
169
|
});
|
|
170
|
+
|
|
171
|
+
describe('a size-aware initial view', () => {
|
|
172
|
+
const sized = {
|
|
173
|
+
name: 'Sized',
|
|
174
|
+
defaultConfig: () => ({}),
|
|
175
|
+
initialState: () => ({}),
|
|
176
|
+
render: () => null,
|
|
177
|
+
canvas: {
|
|
178
|
+
initialView: ({ width, height }: { width: number; height: number }) => ({
|
|
179
|
+
zoom: 1,
|
|
180
|
+
pan: { x: width / 2, y: height / 2 },
|
|
181
|
+
}),
|
|
182
|
+
layers: [],
|
|
183
|
+
},
|
|
184
|
+
} as unknown as Instrument;
|
|
185
|
+
|
|
186
|
+
it('leaves the view unplaced, because the canvas has no size yet', () => {
|
|
187
|
+
const [trial] = addTrial([], [sized], 'Sized');
|
|
188
|
+
expect(trial?.view).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
});
|
package/src/trial/trialOps.ts
CHANGED
|
@@ -12,9 +12,11 @@ function findInstrument(instruments: InstrumentList, name: string): Instrument {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function initialView(instrument: Instrument): TrialRecord['view'] {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const declared = instrument.canvas?.initialView;
|
|
16
|
+
// A function needs the canvas size, which no trial has at creation. `null`
|
|
17
|
+
// is the unplaced marker; `Trial` resolves it on the first non-empty size.
|
|
18
|
+
if (typeof declared === 'function') return null;
|
|
19
|
+
return declared ? structuredClone(declared) : { ...DEFAULT_VIEW, pan: { ...DEFAULT_VIEW.pan } };
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/** Append a new trial running `instrumentName`, at that instrument's
|
package/src/ui/layers/index.ts
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// LayerStack lives in `@weasel-js/ui` now; labkit re-exports it so existing
|
|
2
|
+
// `@weasel-js/labkit/ui/layers` imports keep resolving. Named, not `export *`
|
|
3
|
+
// — a star re-export of an external package emits no binding in the bundle.
|
|
4
|
+
export type { LayerStackItem, LayerStackProps } from '@weasel-js/ui';
|
|
5
|
+
export { LayerStack } from '@weasel-js/ui';
|