@weasel-js/labkit 1.1.0 → 1.3.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/README.md +27 -0
- package/dist/_dts/{DrawCommand-BkZztJsW.d.ts → DrawCommand-DBB45NfN.d.ts} +244 -58
- package/dist/_dts/PrefsForm-BYa6cWnO.d.ts +201 -0
- package/dist/_dts/index-DgcNdEdh.d.ts +314 -0
- package/dist/_dts/types-1Sdxy_Pv.d.ts +90 -0
- package/dist/_dts/types-AHlQxBNN.d.ts +162 -0
- package/dist/_dts/types-DJ79Tg5J.d.ts +56 -0
- package/dist/_dts/{useTrialState-DYe2vUwN.d.ts → useTrialState-CsGMjhu9.d.ts} +6 -92
- package/dist/canvas/index.d.ts +8 -3
- package/dist/canvas/index.js +2 -1
- package/dist/chrome/index.d.ts +86 -0
- package/dist/chrome/index.js +8 -0
- package/dist/chrome/index.js.map +1 -0
- package/dist/{chunk-3TYUJR7Z.js → chunk-4TMMVIDM.js} +9 -3
- package/dist/chunk-4TMMVIDM.js.map +1 -0
- package/dist/chunk-HFDVGF4X.js +170 -0
- package/dist/chunk-HFDVGF4X.js.map +1 -0
- package/dist/chunk-LN6JDUGB.js +106 -0
- package/dist/chunk-LN6JDUGB.js.map +1 -0
- package/dist/{chunk-574LJAV4.js → chunk-NS54R52R.js} +31 -21
- package/dist/chunk-NS54R52R.js.map +1 -0
- package/dist/{chunk-NRKWVTVT.js → chunk-O2BULFHX.js} +5 -14
- package/dist/chunk-O2BULFHX.js.map +1 -0
- package/dist/chunk-O7NVSCLN.js +266 -0
- package/dist/chunk-O7NVSCLN.js.map +1 -0
- package/dist/chunk-PO6L3NSH.js +366 -0
- package/dist/chunk-PO6L3NSH.js.map +1 -0
- package/dist/chunk-PQJ5B2U2.js +27 -0
- package/dist/chunk-PQJ5B2U2.js.map +1 -0
- package/dist/chunk-RVNN4CHQ.js +212 -0
- package/dist/chunk-RVNN4CHQ.js.map +1 -0
- package/dist/chunk-TLGRYALP.js +6151 -0
- package/dist/chunk-TLGRYALP.js.map +1 -0
- package/dist/{chunk-SFL7NFKN.js → chunk-V7PRSIRQ.js} +15 -7
- package/dist/chunk-V7PRSIRQ.js.map +1 -0
- package/dist/{chunk-DJLDIRFN.js → chunk-Z6HJ5FEM.js} +9626 -6050
- package/dist/chunk-Z6HJ5FEM.js.map +1 -0
- package/dist/chunk-ZZAYVX4X.js +505 -0
- package/dist/chunk-ZZAYVX4X.js.map +1 -0
- package/dist/controls/index.d.ts +19 -5
- package/dist/controls/index.js +3 -1
- package/dist/dragdrop/index.d.ts +5 -2
- package/dist/index.d.ts +307 -107
- package/dist/index.js +797 -540
- package/dist/index.js.map +1 -1
- package/dist/job/index.d.ts +13 -0
- package/dist/job/index.js +3 -0
- package/dist/job/index.js.map +1 -0
- package/dist/layers/index.d.ts +6 -3
- package/dist/layers/index.js +2 -1
- package/dist/passthrough/weasel-canvas.d.ts +69 -13
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +3804 -3661
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/primitives/index.d.ts +102 -8
- package/dist/primitives/index.js +4 -1
- package/dist/state/index.d.ts +3 -2
- package/dist/state/index.js +2 -2
- package/dist/styles.css +543 -144
- package/dist/surface/index.d.ts +77 -0
- package/dist/surface/index.js +4 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/ui/layers/index.js +4 -3
- package/dist/undo/index.d.ts +5 -2
- package/package.json +24 -7
- package/src/canvas/AGENTS.md +12 -0
- package/src/canvas/CanvasStack.tsx +6 -2
- package/src/canvas/useLayerScheduler.ts +22 -15
- package/src/canvas/useOrbit.test.ts +71 -0
- package/src/canvas/useOrbit.ts +149 -0
- package/src/canvas/usePanZoom.test.ts +80 -0
- package/src/canvas/usePanZoom.ts +16 -2
- package/src/chrome/ChromeRegions.stories.tsx +106 -0
- package/src/chrome/builtins.test.ts +153 -0
- package/src/chrome/builtins.tsx +205 -0
- package/src/chrome/index.ts +25 -0
- package/src/chrome/merge.test.ts +51 -0
- package/src/chrome/merge.ts +52 -0
- package/src/chrome/regions/PaletteRegion.less +14 -0
- package/src/chrome/regions/PaletteRegion.test.tsx +36 -0
- package/src/chrome/regions/PaletteRegion.tsx +41 -0
- package/src/chrome/regions/SidebarRegion.tsx +56 -0
- package/src/chrome/regions/StatusRegion.tsx +31 -0
- package/src/chrome/regions/TitleBarRegion.tsx +41 -0
- package/src/chrome/regions/ToolbarRegion.test.tsx +82 -0
- package/src/chrome/regions/ToolbarRegion.tsx +71 -0
- package/src/chrome/regions/ViewportRegion.less +42 -0
- package/src/chrome/regions/ViewportRegion.tsx +37 -0
- package/src/chrome/regions/regions.test.tsx +81 -0
- package/src/chrome/types.ts +127 -0
- package/src/config/builder.test.ts +87 -0
- package/src/config/builder.ts +224 -0
- package/src/config/fromConfigField.test.ts +93 -0
- package/src/config/fromConfigField.ts +74 -0
- package/src/config/index.ts +22 -0
- package/src/config/resolve.test.ts +120 -0
- package/src/config/resolve.ts +67 -0
- package/src/config/rules.ts +68 -0
- package/src/config/types.ts +108 -0
- package/src/config/useConfigSchema.ts +30 -0
- package/src/config/visible.ts +20 -0
- package/src/controls/ControlPanel.less +4 -58
- package/src/controls/ControlPanel.stories.tsx +86 -0
- package/src/controls/ControlPanel.test.tsx +223 -10
- package/src/controls/ControlPanel.tsx +202 -203
- package/src/dragdrop/DragDropRuntime.tsx +36 -18
- package/src/dragdrop/DragGhost.less +1 -1
- package/src/dragdrop/Palette.less +1 -1
- package/src/index.test.ts +18 -0
- package/src/index.ts +38 -3
- package/src/instrument/SineWave.smoke.test.tsx +9 -1
- package/src/instrument/defineInstrument.test.ts +38 -1
- package/src/instrument/defineInstrument.ts +31 -4
- package/src/instrument/index.ts +1 -0
- package/src/instrument/types.ts +38 -3
- package/src/instrument/validateConfigSchema.test.ts +13 -3
- package/src/instrument/validateConfigSchema.ts +7 -10
- package/src/job/index.ts +3 -0
- package/src/job/types.ts +47 -0
- package/src/job/useJob.test.tsx +210 -0
- package/src/job/useJob.ts +134 -0
- package/src/lab/Lab.chrome.test.tsx +65 -0
- package/src/lab/Lab.stories.tsx +1 -2
- package/src/lab/Lab.tsx +58 -15
- package/src/lab/LabContext.ts +6 -0
- package/src/lab/LabFullChrome.stories.tsx +308 -0
- package/src/lab/LabHeader.test.tsx +42 -0
- package/src/lab/LabHeader.tsx +57 -0
- package/src/lab/LabPalette.test.tsx +35 -0
- package/src/lab/LabPalette.tsx +34 -0
- package/src/lab/LabShell.less +50 -3
- package/src/lab/Workspace.less +1 -30
- package/src/lab/Workspace.surface.test.tsx +49 -0
- package/src/lab/Workspace.test.tsx +20 -10
- package/src/lab/Workspace.tsx +19 -9
- package/src/lab/index.ts +2 -0
- package/src/layers/AGENTS.md +1 -1
- package/src/layers/LayerList.less +31 -7
- package/src/layers/LayerList.tsx +17 -4
- package/src/passthrough/weasel-ui.ts +23 -0
- package/src/primitives/DragHandleGlyph.tsx +29 -0
- package/src/primitives/FloatingPanel.less +28 -0
- package/src/primitives/FloatingPanel.stories.tsx +46 -0
- package/src/primitives/FloatingPanel.test.tsx +240 -0
- package/src/primitives/FloatingPanel.tsx +190 -0
- package/src/primitives/FpsMeter.test.tsx +44 -0
- package/src/primitives/FpsMeter.tsx +20 -9
- package/src/primitives/JobProgress.less +69 -0
- package/src/primitives/JobProgress.stories.tsx +48 -0
- package/src/primitives/JobProgress.tsx +65 -0
- package/src/primitives/Legend.less +47 -0
- package/src/primitives/Legend.stories.tsx +32 -0
- package/src/primitives/Legend.test.tsx +57 -0
- package/src/primitives/Legend.tsx +40 -0
- package/src/primitives/Sidebar.less +32 -2
- package/src/primitives/StatusBar.less +7 -0
- package/src/primitives/StatusBar.tsx +10 -3
- package/src/primitives/Toolbar.less +50 -3
- package/src/primitives/Toolbar.test.tsx +67 -1
- package/src/primitives/Toolbar.tsx +72 -6
- package/src/primitives/ZoomControl.less +31 -0
- package/src/primitives/ZoomControl.tsx +76 -0
- package/src/primitives/floating.entry.test.tsx +14 -0
- package/src/primitives/index.ts +10 -2
- package/src/primitives/useRovingTabIndex.test.ts +27 -0
- package/src/primitives/useRovingTabIndex.ts +79 -0
- package/src/state/store.ts +16 -2
- package/src/state/toolSlot.test.ts +36 -0
- package/src/state/types.ts +8 -2
- package/src/state/view.test.ts +127 -0
- package/src/state/view.ts +18 -0
- package/src/styles.less +6 -0
- package/src/surface/AGENTS.md +64 -0
- package/src/surface/SurfaceContext.ts +5 -0
- package/src/surface/composeRects.test.ts +50 -0
- package/src/surface/composeRects.ts +19 -0
- package/src/surface/deviceRect.test.ts +40 -0
- package/src/surface/deviceRect.ts +19 -0
- package/src/surface/index.ts +7 -0
- package/src/surface/rect.ts +16 -0
- package/src/surface/useSurfaceTile.test.tsx +67 -0
- package/src/surface/useSurfaceTile.ts +32 -0
- package/src/surface/useTiledSurface.test.tsx +231 -0
- package/src/surface/useTiledSurface.ts +161 -0
- package/src/test-setup.ts +26 -0
- package/src/theme/base.less +19 -2
- package/src/theme/interstellar.tokens.json +6 -2
- package/src/tools/types.ts +24 -0
- package/src/trial/Trial.config.test.tsx +113 -0
- package/src/trial/Trial.job.test.tsx +73 -0
- package/src/trial/Trial.less +121 -4
- package/src/trial/Trial.stories.tsx +9 -1
- package/src/trial/Trial.test.tsx +67 -96
- package/src/trial/Trial.tsx +112 -45
- package/src/trial/TrialChrome.tsx +110 -64
- package/src/trial/TrialDragContext.ts +10 -0
- package/src/trial/TrialTitleBar.tsx +41 -0
- package/src/trial/index.ts +1 -15
- package/src/trial/trialChrome.borders.test.ts +38 -0
- package/src/ui/layers/LayerStack.less +9 -9
- package/src/ui/layers/LayerStack.tsx +1 -13
- package/src/ui/properties/CurveField.less +2 -2
- package/src/ui/properties/CurveField.test.tsx +11 -11
- package/src/ui/properties/PropertyGroup.less +2 -2
- package/src/ui/properties/PropertyPanel.less +55 -41
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +4 -2
- package/src/ui/properties/storyLayouts.tsx +2 -2
- package/dist/_dts/index-iAP6XbH3.d.ts +0 -132
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/chunk-3TYUJR7Z.js.map +0 -1
- package/dist/chunk-574LJAV4.js.map +0 -1
- package/dist/chunk-5R2ATYPJ.js +0 -201
- package/dist/chunk-5R2ATYPJ.js.map +0 -1
- package/dist/chunk-C6GJKPUI.js +0 -5986
- package/dist/chunk-C6GJKPUI.js.map +0 -1
- package/dist/chunk-DJLDIRFN.js.map +0 -1
- package/dist/chunk-NRKWVTVT.js.map +0 -1
- package/dist/chunk-PMAU3SEE.js +0 -126
- package/dist/chunk-PMAU3SEE.js.map +0 -1
- package/dist/chunk-SFL7NFKN.js.map +0 -1
- package/src/instrument/capabilityDetector.test.ts +0 -64
- package/src/instrument/capabilityDetector.ts +0 -20
- package/src/trial/DefaultSidebar.tsx +0 -29
- package/src/trial/DefaultStatusBar.tsx +0 -19
- package/src/trial/DefaultToolbar.tsx +0 -92
- package/src/trial/slotTypes.ts +0 -53
package/src/trial/Trial.tsx
CHANGED
|
@@ -2,20 +2,17 @@ import { type ReactNode, useContext, useMemo, useRef, useState } from 'react';
|
|
|
2
2
|
import { useStore } from 'zustand/react';
|
|
3
3
|
import { CanvasStack } from '../canvas/CanvasStack';
|
|
4
4
|
import type { CanvasLayerDescriptor } from '../canvas/useLayerScheduler';
|
|
5
|
+
import type { TrialContribution } from '../chrome/types';
|
|
5
6
|
import { DragOverlay, useDragDrop } from '../dragdrop/DragDropRuntime';
|
|
6
7
|
import { Palette } from '../dragdrop/Palette';
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
LayerDescriptor,
|
|
10
|
-
PaletteItem,
|
|
11
|
-
RenderContext,
|
|
12
|
-
ViewTransform,
|
|
13
|
-
} from '../instrument/types';
|
|
8
|
+
import type { Instrument, LayerDescriptor, PaletteItem, RenderContext } from '../instrument/types';
|
|
9
|
+
import { useJob } from '../job/useJob';
|
|
14
10
|
import { useLabContext } from '../lab/LabContext';
|
|
15
11
|
import { LayerList } from '../layers/LayerList';
|
|
16
12
|
import { LabStoreContext } from '../state/context';
|
|
17
13
|
import type { LabStore } from '../state/store';
|
|
18
14
|
import type { TrialRecord } from '../state/types';
|
|
15
|
+
import { as2DView, DEFAULT_VIEW } from '../state/view';
|
|
19
16
|
import { createEventBus, type EventBus } from '../undo/eventBus';
|
|
20
17
|
import { pushSnapshot, redo as undoRedo, undo as undoUndo } from '../undo/undoStack';
|
|
21
18
|
import type { UndoBindings } from './TrialChrome';
|
|
@@ -24,11 +21,16 @@ import { TrialChrome } from './TrialChrome';
|
|
|
24
21
|
/** Props for `<Trial>`. */
|
|
25
22
|
export interface TrialProps {
|
|
26
23
|
id: string;
|
|
24
|
+
/** Contributions the lab adds to this trial's chrome, merged after the
|
|
25
|
+
* instrument's own. */
|
|
26
|
+
chrome?: readonly TrialContribution[];
|
|
27
|
+
/** Built-in contribution ids to drop. Throws on an id that is not there. */
|
|
28
|
+
suppress?: readonly string[];
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/** Renders one trial from the lab store: looks up its record and
|
|
30
32
|
* instrument, and mounts the instrument inside the trial chrome. */
|
|
31
|
-
export function Trial({ id }: TrialProps) {
|
|
33
|
+
export function Trial({ id, chrome, suppress }: TrialProps) {
|
|
32
34
|
const lab = useLabContext();
|
|
33
35
|
const storeCtx = useContext(LabStoreContext);
|
|
34
36
|
if (!storeCtx) throw new Error('[labkit] <Trial> requires <LabStoreProvider>');
|
|
@@ -48,6 +50,8 @@ export function Trial({ id }: TrialProps) {
|
|
|
48
50
|
instrument={instrument}
|
|
49
51
|
store={storeCtx.store}
|
|
50
52
|
isLast={lab.trials.length <= 1}
|
|
53
|
+
chrome={chrome}
|
|
54
|
+
suppress={suppress}
|
|
51
55
|
/>
|
|
52
56
|
);
|
|
53
57
|
}
|
|
@@ -57,14 +61,19 @@ interface TrialRuntimeProps {
|
|
|
57
61
|
instrument: Instrument;
|
|
58
62
|
store: LabStore;
|
|
59
63
|
isLast: boolean;
|
|
64
|
+
chrome?: readonly TrialContribution[];
|
|
65
|
+
suppress?: readonly string[];
|
|
60
66
|
}
|
|
61
67
|
|
|
62
|
-
function TrialRuntime({ record, instrument, store, isLast }: TrialRuntimeProps) {
|
|
68
|
+
function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: TrialRuntimeProps) {
|
|
63
69
|
const canvasContainerRef = useRef<HTMLDivElement | null>(null);
|
|
64
70
|
const updateTrialState = useStore(store, (s) => s.updateTrialState);
|
|
65
71
|
const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
|
|
66
72
|
const updateTrialView = useStore(store, (s) => s.updateTrialView);
|
|
67
73
|
const updateTrialUndoStack = useStore(store, (s) => s.updateTrialUndoStack);
|
|
74
|
+
const labToolId = useStore(store, (s) => s.activeToolId);
|
|
75
|
+
const setLabTool = useStore(store, (s) => s.setLabTool);
|
|
76
|
+
const setTrialTool = useStore(store, (s) => s.setTrialTool);
|
|
68
77
|
|
|
69
78
|
const busRef = useRef<EventBus | null>(null);
|
|
70
79
|
if (busRef.current === null) busRef.current = createEventBus();
|
|
@@ -85,7 +94,32 @@ function TrialRuntime({ record, instrument, store, isLast }: TrialRuntimeProps)
|
|
|
85
94
|
updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
|
|
86
95
|
};
|
|
87
96
|
|
|
88
|
-
const setView = (v:
|
|
97
|
+
const setView = (v: unknown): void => updateTrialView(record.id, v);
|
|
98
|
+
|
|
99
|
+
// A trial gets its own slot when its instrument declares tools; otherwise it
|
|
100
|
+
// reads the lab's. Which slot a change writes follows from the same thing.
|
|
101
|
+
const declaresTools = instrument.tools != null;
|
|
102
|
+
const resolvedToolId = declaresTools
|
|
103
|
+
? (record.activeToolId ?? instrument.tools?.initial ?? instrument.tools?.tools[0]?.id ?? null)
|
|
104
|
+
: labToolId;
|
|
105
|
+
const setActiveTool = (id: string): void => {
|
|
106
|
+
if (declaresTools) setTrialTool(record.id, id);
|
|
107
|
+
else setLabTool(id);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// `CanvasStack` and the zoom controls are 2D; a trial holding another view shape
|
|
111
|
+
// gets the default here and simply never renders them.
|
|
112
|
+
const view2d = as2DView(record.view);
|
|
113
|
+
|
|
114
|
+
const jobCap = instrument.job;
|
|
115
|
+
// Hooks cannot be conditional, so a trial without the capability still calls
|
|
116
|
+
// this — with a runner that yields nothing and is never started.
|
|
117
|
+
const job = useJob({
|
|
118
|
+
capability: jobCap ?? { run: async function* () {}, onItem: (_i, st) => st },
|
|
119
|
+
config: record.config,
|
|
120
|
+
state: record.state,
|
|
121
|
+
setState: (next) => updateTrialState(record.id, next as never),
|
|
122
|
+
});
|
|
89
123
|
|
|
90
124
|
const renderCtx: RenderContext<unknown, unknown> = {
|
|
91
125
|
state: record.state,
|
|
@@ -105,13 +139,20 @@ function TrialRuntime({ record, instrument, store, isLast }: TrialRuntimeProps)
|
|
|
105
139
|
},
|
|
106
140
|
trial: {
|
|
107
141
|
id: record.id,
|
|
108
|
-
|
|
109
|
-
|
|
142
|
+
view: record.view,
|
|
143
|
+
setView,
|
|
144
|
+
zoom: view2d?.zoom ?? 1,
|
|
145
|
+
setZoom: (z) => {
|
|
146
|
+
if (!view2d) return;
|
|
147
|
+
updateTrialView(record.id, { ...view2d, zoom: z });
|
|
148
|
+
},
|
|
149
|
+
activeToolId: resolvedToolId,
|
|
110
150
|
},
|
|
111
151
|
emit: (event) => {
|
|
112
152
|
snapshotIfNeeded(event);
|
|
113
153
|
bus.emit(event);
|
|
114
154
|
},
|
|
155
|
+
job: jobCap ? job : undefined,
|
|
115
156
|
};
|
|
116
157
|
|
|
117
158
|
const undoBindings: UndoBindings | undefined = undoCap
|
|
@@ -154,13 +195,13 @@ function TrialRuntime({ record, instrument, store, isLast }: TrialRuntimeProps)
|
|
|
154
195
|
|
|
155
196
|
const layerDescriptors: LayerDescriptor[] = useMemo(() => {
|
|
156
197
|
if (!instrument.layers) return [];
|
|
157
|
-
return instrument.layers.ids.map((
|
|
198
|
+
return instrument.layers.ids.map((l) => (typeof l === 'string' ? { id: l, label: l } : l));
|
|
158
199
|
}, [instrument.layers]);
|
|
159
200
|
|
|
160
201
|
const dragDropResult = useDragDrop({
|
|
161
202
|
capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
|
|
162
203
|
canvasContainerRef,
|
|
163
|
-
view:
|
|
204
|
+
view: view2d ?? DEFAULT_VIEW,
|
|
164
205
|
state: record.state,
|
|
165
206
|
config: record.config,
|
|
166
207
|
setState: (next) => {
|
|
@@ -207,12 +248,14 @@ function TrialRuntime({ record, instrument, store, isLast }: TrialRuntimeProps)
|
|
|
207
248
|
let body: ReactNode;
|
|
208
249
|
if (instrument.canvas) {
|
|
209
250
|
body = (
|
|
210
|
-
<div
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
251
|
+
<div ref={canvasContainerRef} className="lk-trial__canvas-host">
|
|
252
|
+
<CanvasStack
|
|
253
|
+
layers={layersWithFeedback}
|
|
254
|
+
view={view2d ?? DEFAULT_VIEW}
|
|
255
|
+
onViewChange={setView}
|
|
256
|
+
minZoom={instrument.canvas.minZoom}
|
|
257
|
+
maxZoom={instrument.canvas.maxZoom}
|
|
258
|
+
>
|
|
216
259
|
{instrument.render(renderCtx)}
|
|
217
260
|
</CanvasStack>
|
|
218
261
|
<DragOverlay drag={dragDropResult.drag} />
|
|
@@ -222,40 +265,64 @@ function TrialRuntime({ record, instrument, store, isLast }: TrialRuntimeProps)
|
|
|
222
265
|
body = instrument.render(renderCtx);
|
|
223
266
|
}
|
|
224
267
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
268
|
+
const startDrag = dragDropResult.startDrag;
|
|
269
|
+
const paletteNode = useMemo(
|
|
270
|
+
() =>
|
|
271
|
+
paletteItems.length > 0 ? <Palette items={paletteItems} onDragStart={startDrag} /> : null,
|
|
272
|
+
[paletteItems, startDrag],
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const layerListNode = useMemo(
|
|
276
|
+
() =>
|
|
277
|
+
instrument.layers && layerDescriptors.length > 0 ? (
|
|
278
|
+
<LayerList
|
|
279
|
+
layers={layerDescriptors}
|
|
280
|
+
visibility={layerVisibility}
|
|
281
|
+
onReorder={(next) => {
|
|
282
|
+
setLayerOrder(next.map((l) => l.id));
|
|
283
|
+
bus.emit('layers.reorder');
|
|
284
|
+
}}
|
|
285
|
+
onToggle={(lid, visible) => {
|
|
286
|
+
setLayerVisibility((prev) => ({ ...prev, [lid]: visible }));
|
|
287
|
+
bus.emit('layers.toggle');
|
|
288
|
+
}}
|
|
289
|
+
/>
|
|
290
|
+
) : null,
|
|
291
|
+
[instrument.layers, layerDescriptors, layerVisibility, bus],
|
|
292
|
+
);
|
|
229
293
|
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
294
|
+
const extraChrome = useMemo<TrialContribution[]>(() => {
|
|
295
|
+
const out: TrialContribution[] = [];
|
|
296
|
+
if (paletteNode) {
|
|
297
|
+
out.push({
|
|
298
|
+
id: 'dragdrop-palette',
|
|
299
|
+
region: 'sidebar',
|
|
300
|
+
item: { title: 'Parts', body: paletteNode },
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (layerListNode) {
|
|
304
|
+
out.push({
|
|
305
|
+
id: 'layer-list',
|
|
306
|
+
region: 'sidebar',
|
|
307
|
+
item: { title: 'Layers', body: layerListNode },
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}, [paletteNode, layerListNode]);
|
|
245
312
|
|
|
246
313
|
return (
|
|
247
314
|
<TrialChrome
|
|
315
|
+
job={jobCap ? job : undefined}
|
|
248
316
|
trialId={record.id}
|
|
249
317
|
record={record}
|
|
250
318
|
instrument={instrument}
|
|
251
319
|
isLastTrial={isLast}
|
|
252
320
|
undoBindings={undoBindings}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
321
|
+
trialChrome={extraChrome}
|
|
322
|
+
chrome={chrome}
|
|
323
|
+
suppress={suppress}
|
|
324
|
+
activeToolId={resolvedToolId}
|
|
325
|
+
setActiveTool={setActiveTool}
|
|
259
326
|
>
|
|
260
327
|
{body}
|
|
261
328
|
</TrialChrome>
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { type KeyboardEvent, type ReactNode, useContext, useMemo } from 'react';
|
|
2
2
|
import { useStore } from 'zustand/react';
|
|
3
|
+
import { builtinContributions } from '../chrome/builtins';
|
|
4
|
+
import { mergeContributions, suppressContributions } from '../chrome/merge';
|
|
5
|
+
import { PaletteRegion } from '../chrome/regions/PaletteRegion';
|
|
6
|
+
import { SidebarRegion } from '../chrome/regions/SidebarRegion';
|
|
7
|
+
import { StatusRegion } from '../chrome/regions/StatusRegion';
|
|
8
|
+
import { TitleBarRegion } from '../chrome/regions/TitleBarRegion';
|
|
9
|
+
import { ToolbarRegion } from '../chrome/regions/ToolbarRegion';
|
|
10
|
+
import { ViewportRegion } from '../chrome/regions/ViewportRegion';
|
|
11
|
+
import type { TrialChromeContext, TrialContribution, TrialRegion } from '../chrome/types';
|
|
12
|
+
import { useConfigSchema } from '../config/useConfigSchema';
|
|
3
13
|
import type { Instrument } from '../instrument/types';
|
|
14
|
+
import type { JobHandle } from '../job/types';
|
|
4
15
|
import { useLabContext } from '../lab/LabContext';
|
|
16
|
+
import { JobProgress } from '../primitives/JobProgress';
|
|
5
17
|
import { LabStoreContext } from '../state/context';
|
|
6
18
|
import type { TrialRecord } from '../state/types';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { DefaultToolbar } from './DefaultToolbar';
|
|
10
|
-
import type {
|
|
11
|
-
SidebarSlot,
|
|
12
|
-
StatusBarSlot,
|
|
13
|
-
ToolbarSlot,
|
|
14
|
-
TrialSidebarContext,
|
|
15
|
-
TrialStatusBarContext,
|
|
16
|
-
TrialToolbarContext,
|
|
17
|
-
} from './slotTypes';
|
|
19
|
+
import { as2DView } from '../state/view';
|
|
20
|
+
import { TrialTitleBar } from './TrialTitleBar';
|
|
18
21
|
|
|
19
22
|
export interface UndoBindings {
|
|
20
23
|
canUndo: boolean;
|
|
@@ -29,27 +32,40 @@ export interface TrialChromeProps {
|
|
|
29
32
|
record: TrialRecord;
|
|
30
33
|
instrument: Instrument;
|
|
31
34
|
isLastTrial: boolean;
|
|
32
|
-
toolbar?: ToolbarSlot;
|
|
33
|
-
sidebar?: SidebarSlot;
|
|
34
|
-
statusBar?: StatusBarSlot;
|
|
35
35
|
undoBindings?: UndoBindings;
|
|
36
|
-
|
|
36
|
+
/** Supplied when the instrument declares a `job`. */
|
|
37
|
+
job?: JobHandle;
|
|
38
|
+
/** Contributions the trial runtime itself adds — the drag palette and the
|
|
39
|
+
* layer list, which depend on runtime state the instrument cannot reach. */
|
|
40
|
+
trialChrome?: readonly TrialContribution[];
|
|
41
|
+
/** Contributions from the lab, merged last. */
|
|
42
|
+
chrome?: readonly TrialContribution[];
|
|
43
|
+
/** Built-in contribution ids to drop. Throws on an id that is not there. */
|
|
44
|
+
suppress?: readonly string[];
|
|
45
|
+
/** The trial's resolved tool slot, and the setter that writes whichever
|
|
46
|
+
* slot it resolved to. `Trial` owns the resolution. */
|
|
47
|
+
activeToolId?: string | null;
|
|
48
|
+
setActiveTool?: (id: string) => void;
|
|
37
49
|
children: ReactNode;
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
const NO_OP = (): void => {};
|
|
53
|
+
|
|
54
|
+
/** The frame around a running instrument. Builds one chrome context, assembles
|
|
55
|
+
* the contributions the instrument, the runtime and the lab declare, and hands
|
|
56
|
+
* each region its slice. */
|
|
43
57
|
export function TrialChrome({
|
|
44
58
|
trialId,
|
|
45
59
|
record,
|
|
46
60
|
instrument,
|
|
47
61
|
isLastTrial,
|
|
48
|
-
toolbar,
|
|
49
|
-
sidebar,
|
|
50
|
-
statusBar,
|
|
51
62
|
undoBindings,
|
|
52
|
-
|
|
63
|
+
job,
|
|
64
|
+
trialChrome,
|
|
65
|
+
chrome,
|
|
66
|
+
suppress,
|
|
67
|
+
activeToolId = null,
|
|
68
|
+
setActiveTool = NO_OP,
|
|
53
69
|
children,
|
|
54
70
|
}: TrialChromeProps) {
|
|
55
71
|
const lab = useLabContext();
|
|
@@ -59,39 +75,29 @@ export function TrialChrome({
|
|
|
59
75
|
const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
|
|
60
76
|
const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
|
|
61
77
|
|
|
62
|
-
|
|
78
|
+
// The trial view is opaque to labkit, so the zoom chrome asks for the 2D shape
|
|
79
|
+
// and goes inert when the trial holds something else — an orbit, say.
|
|
80
|
+
const view2d = as2DView(record.view);
|
|
81
|
+
|
|
82
|
+
const configSchema = useConfigSchema(instrument);
|
|
83
|
+
|
|
84
|
+
const ctx = useMemo<TrialChromeContext>(() => {
|
|
63
85
|
const setZoom = (z: number): void => {
|
|
64
|
-
|
|
86
|
+
if (!view2d) return;
|
|
87
|
+
updateTrialView(trialId, { ...view2d, zoom: z });
|
|
65
88
|
};
|
|
66
89
|
return {
|
|
67
90
|
trialId,
|
|
68
91
|
instrumentName: record.instrumentName,
|
|
69
|
-
|
|
92
|
+
isLastTrial,
|
|
93
|
+
zoom: view2d ? view2d.zoom : null,
|
|
94
|
+
setZoom,
|
|
70
95
|
canUndo: undoBindings?.canUndo ?? false,
|
|
71
96
|
canRedo: undoBindings?.canRedo ?? false,
|
|
72
|
-
undo: undoBindings?.undo ??
|
|
73
|
-
redo: undoBindings?.redo ??
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
zoomIn: () => setZoom(record.view.zoom * 1.25),
|
|
77
|
-
zoomOut: () => setZoom(record.view.zoom * 0.8),
|
|
78
|
-
resetZoom: () => setZoom(1),
|
|
79
|
-
hasCanvas: instrument.canvas != null,
|
|
80
|
-
savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
|
|
81
|
-
saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
|
|
82
|
-
loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
|
|
83
|
-
clone: () => lab.cloneTrial(trialId),
|
|
84
|
-
reset: () => lab.resetTrial(trialId),
|
|
85
|
-
close: () => lab.closeTrial(trialId),
|
|
86
|
-
isLastTrial,
|
|
87
|
-
};
|
|
88
|
-
}, [trialId, record, instrument, lab, isLastTrial, updateTrialView, undoBindings]);
|
|
89
|
-
|
|
90
|
-
const sidebarCtx = useMemo<TrialSidebarContext>(
|
|
91
|
-
() => ({
|
|
92
|
-
trialId,
|
|
93
|
-
instrumentName: record.instrumentName,
|
|
94
|
-
configFields: instrument.configSchema?.() ?? [],
|
|
97
|
+
undo: undoBindings?.undo ?? NO_OP,
|
|
98
|
+
redo: undoBindings?.redo ?? NO_OP,
|
|
99
|
+
configSchema,
|
|
100
|
+
configFields: instrument.config ? [] : (instrument.configSchema?.() ?? []),
|
|
95
101
|
config: record.config,
|
|
96
102
|
setConfig: (key, value) => {
|
|
97
103
|
const prevConfig = record.config as Record<string, unknown>;
|
|
@@ -107,26 +113,58 @@ export function TrialChrome({
|
|
|
107
113
|
updateTrialState(trialId, nextState as never);
|
|
108
114
|
}
|
|
109
115
|
},
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
|
|
117
|
+
saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
|
|
118
|
+
loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
|
|
119
|
+
clone: () => lab.cloneTrial(trialId),
|
|
120
|
+
reset: () => lab.resetTrial(trialId),
|
|
121
|
+
close: () => lab.closeTrial(trialId),
|
|
122
|
+
activeToolId,
|
|
123
|
+
setActiveTool,
|
|
124
|
+
};
|
|
125
|
+
}, [
|
|
126
|
+
trialId,
|
|
127
|
+
record,
|
|
128
|
+
instrument,
|
|
129
|
+
lab,
|
|
130
|
+
isLastTrial,
|
|
131
|
+
updateTrialView,
|
|
132
|
+
updateTrialConfig,
|
|
133
|
+
updateTrialState,
|
|
134
|
+
undoBindings,
|
|
135
|
+
view2d,
|
|
136
|
+
activeToolId,
|
|
137
|
+
setActiveTool,
|
|
138
|
+
configSchema,
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const contributions = useMemo(
|
|
142
|
+
() =>
|
|
143
|
+
suppressContributions(
|
|
144
|
+
mergeContributions(
|
|
145
|
+
builtinContributions(instrument, ctx, lab.controls),
|
|
146
|
+
[...(instrument.chrome ?? [])],
|
|
147
|
+
[...(trialChrome ?? [])],
|
|
148
|
+
[...(chrome ?? [])],
|
|
149
|
+
),
|
|
150
|
+
suppress ?? [],
|
|
151
|
+
),
|
|
152
|
+
[instrument, ctx, trialChrome, chrome, suppress, lab.controls],
|
|
112
153
|
);
|
|
113
154
|
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
instrumentName: record.instrumentName,
|
|
117
|
-
zoom: record.view.zoom,
|
|
118
|
-
};
|
|
155
|
+
const inRegion = (region: TrialRegion): TrialContribution[] =>
|
|
156
|
+
contributions.filter((c) => c.region === region);
|
|
119
157
|
|
|
120
|
-
const handleKeyDown = (e: KeyboardEvent<
|
|
158
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLElement>): void => {
|
|
121
159
|
const mod = e.metaKey || e.ctrlKey;
|
|
122
160
|
if (!mod) return;
|
|
123
161
|
if (e.key === 'z' || e.key === 'Z') {
|
|
124
162
|
e.preventDefault();
|
|
125
|
-
if (e.shiftKey)
|
|
126
|
-
else
|
|
163
|
+
if (e.shiftKey) ctx.redo();
|
|
164
|
+
else ctx.undo();
|
|
127
165
|
} else if (e.key === 's' || e.key === 'S') {
|
|
128
166
|
e.preventDefault();
|
|
129
|
-
|
|
167
|
+
ctx.saveSnapshot();
|
|
130
168
|
}
|
|
131
169
|
};
|
|
132
170
|
|
|
@@ -134,20 +172,28 @@ export function TrialChrome({
|
|
|
134
172
|
<section
|
|
135
173
|
className="lk-trial"
|
|
136
174
|
aria-label={`Trial ${record.instrumentName}`}
|
|
175
|
+
tabIndex={-1}
|
|
137
176
|
onKeyDown={handleKeyDown}
|
|
138
177
|
>
|
|
178
|
+
<TrialTitleBar title={record.instrumentName}>
|
|
179
|
+
<TitleBarRegion contributions={inRegion('titlebar')} ctx={ctx} />
|
|
180
|
+
</TrialTitleBar>
|
|
139
181
|
<div className="lk-trial__toolbar">
|
|
140
|
-
{toolbar
|
|
182
|
+
<ToolbarRegion contributions={inRegion('toolbar')} ctx={ctx} />
|
|
141
183
|
</div>
|
|
142
184
|
<div className="lk-trial__body">
|
|
185
|
+
<PaletteRegion contributions={inRegion('palette')} ctx={ctx} />
|
|
143
186
|
<div className="lk-trial__sidebar">
|
|
144
|
-
{sidebar
|
|
145
|
-
|
|
187
|
+
<SidebarRegion contributions={inRegion('sidebar')} ctx={ctx} />
|
|
188
|
+
</div>
|
|
189
|
+
<div className={`lk-trial__content${instrument.canvas ? ' lk-trial__content--flush' : ''}`}>
|
|
190
|
+
{children}
|
|
191
|
+
<ViewportRegion contributions={inRegion('viewport')} ctx={ctx} />
|
|
146
192
|
</div>
|
|
147
|
-
<div className="lk-trial__content">{children}</div>
|
|
148
193
|
</div>
|
|
149
194
|
<div className="lk-trial__status">
|
|
150
|
-
{
|
|
195
|
+
{job ? <JobProgress job={job} /> : null}
|
|
196
|
+
<StatusRegion contributions={inRegion('status')} ctx={ctx} />
|
|
151
197
|
</div>
|
|
152
198
|
</section>
|
|
153
199
|
);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { NodeId } from 'windease';
|
|
3
|
+
|
|
4
|
+
/** Supplied by `<Workspace>` when trials are reorderable, so a trial's title
|
|
5
|
+
* bar can be the drag surface. Null when reordering is off. */
|
|
6
|
+
export const TrialDragContext = createContext<{ nodeId: NodeId } | null>(null);
|
|
7
|
+
|
|
8
|
+
export function useTrialDrag(): { nodeId: NodeId } | null {
|
|
9
|
+
return useContext(TrialDragContext);
|
|
10
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { NodeId } from 'windease';
|
|
3
|
+
import { useDragHandle } from 'windease/react';
|
|
4
|
+
import { useTrialDrag } from './TrialDragContext';
|
|
5
|
+
|
|
6
|
+
/** Props for `<TrialTitleBar>`. */
|
|
7
|
+
export interface TrialTitleBarProps {
|
|
8
|
+
title: string;
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// `useDragHandle` needs a node id and windease's DragProvider, so the draggable
|
|
13
|
+
// form is a separate component rather than a conditional hook.
|
|
14
|
+
function Draggable({ nodeId, title, children }: TrialTitleBarProps & { nodeId: NodeId }) {
|
|
15
|
+
const handlers = useDragHandle(nodeId);
|
|
16
|
+
return (
|
|
17
|
+
<div className="lk-trial__titlebar lk-trial__titlebar--draggable" {...handlers}>
|
|
18
|
+
<span className="lk-trial__title">{title}</span>
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A trial's title bar. When the workspace allows reordering the whole bar is
|
|
25
|
+
* the drag surface — there is no separate grip, because a window's title bar
|
|
26
|
+
* is already the thing you expect to drag. */
|
|
27
|
+
export function TrialTitleBar({ title, children }: TrialTitleBarProps) {
|
|
28
|
+
const drag = useTrialDrag();
|
|
29
|
+
if (drag)
|
|
30
|
+
return (
|
|
31
|
+
<Draggable nodeId={drag.nodeId} title={title}>
|
|
32
|
+
{children}
|
|
33
|
+
</Draggable>
|
|
34
|
+
);
|
|
35
|
+
return (
|
|
36
|
+
<div className="lk-trial__titlebar">
|
|
37
|
+
<span className="lk-trial__title">{title}</span>
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
package/src/trial/index.ts
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
export type { DefaultSidebarProps } from './DefaultSidebar';
|
|
2
|
-
export { DefaultSidebar } from './DefaultSidebar';
|
|
3
|
-
export type { DefaultStatusBarProps } from './DefaultStatusBar';
|
|
4
|
-
export { DefaultStatusBar } from './DefaultStatusBar';
|
|
5
|
-
export type { DefaultToolbarProps } from './DefaultToolbar';
|
|
6
|
-
export { DefaultToolbar } from './DefaultToolbar';
|
|
7
|
-
export type {
|
|
8
|
-
SidebarSlot,
|
|
9
|
-
StatusBarSlot,
|
|
10
|
-
ToolbarSlot,
|
|
11
|
-
TrialSidebarContext,
|
|
12
|
-
TrialStatusBarContext,
|
|
13
|
-
TrialToolbarContext,
|
|
14
|
-
} from './slotTypes';
|
|
15
1
|
export type { TrialProps } from './Trial';
|
|
16
2
|
export { Trial } from './Trial';
|
|
17
|
-
export type { TrialChromeProps } from './TrialChrome';
|
|
3
|
+
export type { TrialChromeProps, UndoBindings } from './TrialChrome';
|
|
18
4
|
export { TrialChrome } from './TrialChrome';
|
|
19
5
|
export {
|
|
20
6
|
addTrial,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
const less = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'Trial.less'), 'utf8');
|
|
7
|
+
|
|
8
|
+
/** The body of one rule, by selector. Resolves Less's `&__suffix` nesting and
|
|
9
|
+
* matches braces, so a nested block cannot end the slice early. */
|
|
10
|
+
function rule(selector: string): string {
|
|
11
|
+
const nested = selector.replace(/^\.[a-z-]+?(__|--)/, '&$1');
|
|
12
|
+
const start = [selector, nested].map((s) => less.indexOf(`${s} {`)).find((i) => i !== -1);
|
|
13
|
+
if (start === undefined) throw new Error(`no rule for ${selector}`);
|
|
14
|
+
const open = less.indexOf('{', start);
|
|
15
|
+
let depth = 0;
|
|
16
|
+
for (let i = open; i < less.length; i += 1) {
|
|
17
|
+
if (less[i] === '{') depth += 1;
|
|
18
|
+
else if (less[i] === '}') {
|
|
19
|
+
depth -= 1;
|
|
20
|
+
if (depth === 0) return less.slice(open + 1, i);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`unterminated rule for ${selector}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('trial chrome seams', () => {
|
|
27
|
+
// A `not.toMatch` passes on an empty string, so pin that `rule()` found the
|
|
28
|
+
// rule it was asked for.
|
|
29
|
+
it('isolates the rule it is asked for', () => {
|
|
30
|
+
expect(rule('.lk-trial__toolbar')).toMatch(/flex-shrink/);
|
|
31
|
+
expect(rule('.lk-trial__status')).toMatch(/flex-shrink/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('lets the component draw its own border, not the wrapper', () => {
|
|
35
|
+
expect(rule('.lk-trial__toolbar')).not.toMatch(/border-bottom:\s*[^;]*solid/);
|
|
36
|
+
expect(rule('.lk-trial__status')).not.toMatch(/border-top:\s*[^;]*solid/);
|
|
37
|
+
});
|
|
38
|
+
});
|