@weasel-js/labkit 1.4.4 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -6
- package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CJNMS6mz.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-Gwr8fLpK.d.ts +453 -0
- package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-B4okC2lX.d.ts} +291 -31
- package/dist/_dts/{index-Blv9uQzu.d.ts → index-BNdz-tNd.d.ts} +105 -17
- package/dist/_dts/index-CSJoj4LC.d.ts +248 -0
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-DzoNY1pO.d.ts} +91 -4
- package/dist/_dts/usePanZoom-Pzf9f2jh.d.ts +160 -0
- package/dist/_dts/useTrialState-uLlmaccn.d.ts +212 -0
- package/dist/canvas/index.d.ts +18 -129
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +5 -93
- package/dist/chrome/index.js +10 -6
- package/dist/chunk-7PFH3CNM.js +150 -0
- package/dist/chunk-7PFH3CNM.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-CEIUBPPW.js} +143 -17
- package/dist/chunk-CEIUBPPW.js.map +1 -0
- package/dist/chunk-EH7UEQV7.js +590 -0
- package/dist/chunk-EH7UEQV7.js.map +1 -0
- package/dist/{chunk-MOM3GOVY.js → chunk-EHGVROVV.js} +125 -27
- package/dist/chunk-EHGVROVV.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ERFMC6WU.js} +160 -78
- package/dist/chunk-ERFMC6WU.js.map +1 -0
- package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
- package/dist/chunk-H6ZAOWNE.js.map +1 -0
- package/dist/{chunk-64ZCN3DA.js → chunk-IHUVQ3TX.js} +250 -160
- package/dist/chunk-IHUVQ3TX.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-IJJUK4FU.js} +53 -46
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/chunk-IXTQI7JX.js +737 -0
- package/dist/chunk-IXTQI7JX.js.map +1 -0
- package/dist/{chunk-BKFVHKJH.js → chunk-MDVTK5U3.js} +4 -4
- package/dist/{chunk-BKFVHKJH.js.map → chunk-MDVTK5U3.js.map} +1 -1
- package/dist/chunk-PHK4W562.js +72 -0
- package/dist/chunk-PHK4W562.js.map +1 -0
- package/dist/chunk-PJ7WEQBJ.js +18 -0
- package/dist/chunk-PJ7WEQBJ.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-T3EC2DXD.js +12628 -0
- package/dist/chunk-T3EC2DXD.js.map +1 -0
- package/dist/{chunk-SMHP6XZ4.js → chunk-UN4U4P63.js} +10 -8
- package/dist/chunk-UN4U4P63.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/chunk-Z76KEZAX.js +15 -0
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/chunk-ZDHY47EV.js +363 -0
- package/dist/chunk-ZDHY47EV.js.map +1 -0
- package/dist/config/index.d.ts +276 -0
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +3 -4
- package/dist/controls/index.js +4 -3
- package/dist/dragdrop/index.d.ts +3 -5
- package/dist/index.d.ts +266 -413
- package/dist/index.js +1775 -2251
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -6
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +69 -20
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +175 -309
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +55 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +13 -8
- package/dist/state/index.js +18 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +223 -76
- package/dist/surface/index.d.ts +44 -7
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +3 -5
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +56 -5
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +105 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/paint.test.ts +2 -2
- package/src/annotations/paint.ts +1 -1
- package/src/annotations/preload.ts +6 -0
- package/src/annotations/store.test.ts +18 -0
- package/src/annotations/store.ts +5 -4
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +6 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- package/src/canvas/CanvasStack.test.tsx +25 -0
- package/src/canvas/CanvasStack.tsx +31 -4
- package/src/canvas/Stage.less +18 -0
- package/src/canvas/Stage.test.tsx +186 -0
- package/src/canvas/Stage.tsx +134 -0
- package/src/canvas/camera.test.ts +17 -2
- package/src/canvas/camera.ts +9 -9
- package/src/canvas/index.ts +4 -0
- package/src/canvas/usePanZoom.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +21 -9
- package/src/chrome/ChromeRegions.stories.tsx +1 -4
- package/src/chrome/LabChrome.tsx +130 -0
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +21 -31
- package/src/chrome/index.ts +16 -0
- package/src/chrome/labRegions.test.tsx +208 -0
- package/src/chrome/labTypes.ts +50 -0
- package/src/chrome/merge.ts +7 -9
- package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
- package/src/chrome/regions/PaletteRegion.tsx +16 -9
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +34 -16
- package/src/chrome/types.ts +50 -20
- package/src/config/auto.test.ts +12 -0
- package/src/config/auto.ts +15 -0
- package/src/config/autoConfig.test.ts +107 -0
- package/src/config/autoConfig.ts +153 -0
- package/src/config/builder.test.ts +42 -0
- package/src/config/builder.ts +68 -17
- package/src/config/declarationEmit.fixture.ts +11 -0
- package/src/config/declarationEmit.test.ts +51 -0
- package/src/config/entry.test.ts +46 -0
- package/src/config/index.ts +16 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/path.ts +7 -2
- package/src/config/resolve.test.ts +25 -0
- package/src/config/resolve.ts +17 -1
- package/src/config/types.ts +34 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.stories.tsx +42 -1
- package/src/controls/ControlPanel.test.tsx +530 -4
- package/src/controls/ControlPanel.tsx +562 -82
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +66 -5
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +40 -1
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +1 -3
- package/src/lab/Lab.surface.test.tsx +71 -3
- package/src/lab/Lab.test.tsx +168 -16
- package/src/lab/Lab.tsx +434 -129
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +1 -4
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -14
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +73 -15
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +18 -11
- package/src/lab/LabSwitcher.test.tsx +14 -9
- package/src/lab/LabSwitcher.tsx +8 -4
- package/src/lab/Workspace.less +1 -1
- package/src/lab/Workspace.stories.tsx +1 -1
- package/src/lab/Workspace.surface.test.tsx +1 -0
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.stories.tsx +1 -1
- package/src/loupe/AGENTS.md +17 -5
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +60 -3
- package/src/loupe/useLoupe.ts +29 -49
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +14 -2
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/Legend.stories.tsx +1 -1
- package/src/primitives/ScaleIndicator.stories.tsx +1 -1
- package/src/primitives/Sidebar.less +7 -0
- package/src/primitives/Sidebar.stories.tsx +1 -1
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +133 -0
- package/src/primitives/Split.tsx +175 -0
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- package/src/specimen/Specimen.less +95 -0
- package/src/specimen/Specimen.stories.tsx +13 -0
- package/src/specimen/Specimen.test.tsx +13 -0
- package/src/specimen/Specimen.tsx +888 -0
- package/src/state/Persistence.stories.tsx +75 -0
- package/src/state/Persistence.tsx +33 -0
- package/src/state/SingletonExperiment.test.tsx +47 -53
- package/src/state/SingletonExperiment.tsx +24 -13
- package/src/state/adapterContract.ts +116 -0
- package/src/state/adapters.test.ts +113 -61
- package/src/state/adapters.ts +328 -69
- package/src/state/document.test.ts +71 -96
- package/src/state/document.ts +66 -52
- package/src/state/helpers.test.ts +1 -1
- package/src/state/index.ts +8 -0
- package/src/state/labRecords.test.ts +105 -0
- package/src/state/labRecords.ts +160 -0
- package/src/state/openLabStore.test.ts +658 -0
- package/src/state/openLabStore.ts +403 -0
- package/src/state/records.test.ts +234 -0
- package/src/state/records.ts +219 -0
- package/src/state/store.test.ts +169 -635
- package/src/state/store.ts +146 -163
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +65 -17
- package/src/state/useOpenOnce.ts +68 -0
- package/src/state/usePersistedState.test.tsx +125 -0
- package/src/state/usePersistedState.ts +66 -0
- package/src/state/useTrialState.test.tsx +87 -3
- package/src/state/useTrialState.ts +9 -1
- package/src/state/view.test.ts +49 -15
- package/src/state/view.ts +18 -1
- package/src/styles.less +3 -0
- package/src/surface/AGENTS.md +39 -7
- package/src/surface/SurfaceContext.ts +26 -3
- package/src/surface/index.ts +2 -0
- package/src/surface/useSurfaceTile.test.tsx +1 -0
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +231 -1
- package/src/surface/useTiledSurface.ts +76 -15
- package/src/theme/Interstellar.stories.tsx +9 -9
- package/src/theme/interstellar.test.ts +17 -6
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +59 -7
- package/src/trial/Trial.canvas.test.tsx +32 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +5 -26
- package/src/trial/Trial.stories.tsx +5 -3
- package/src/trial/Trial.test.tsx +31 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +139 -52
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +24 -7
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +67 -7
- package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
- package/dist/chunk-2P6PP5N4.js +0 -553
- package/dist/chunk-2P6PP5N4.js.map +0 -1
- package/dist/chunk-64ZCN3DA.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MOM3GOVY.js.map +0 -1
- package/dist/chunk-SMHP6XZ4.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
- package/dist/chunk-U3IYHIAE.js +0 -343
- package/dist/chunk-U3IYHIAE.js.map +0 -1
- package/dist/chunk-W3ECWC2K.js +0 -7335
- package/dist/chunk-W3ECWC2K.js.map +0 -1
- package/src/theme/interstellar.tokens.json +0 -61
package/src/lab/Lab.tsx
CHANGED
|
@@ -4,22 +4,38 @@ import {
|
|
|
4
4
|
type ReactNode,
|
|
5
5
|
useCallback,
|
|
6
6
|
useEffect,
|
|
7
|
+
useLayoutEffect,
|
|
7
8
|
useMemo,
|
|
8
9
|
useRef,
|
|
9
10
|
useState,
|
|
10
11
|
} from 'react';
|
|
11
12
|
import { useStore } from 'zustand/react';
|
|
13
|
+
import { AnnotationPreloadContext } from '../annotations/preload';
|
|
14
|
+
import { ANNOTATION_TOOLS } from '../annotations/toolMap';
|
|
15
|
+
import {
|
|
16
|
+
LabAsideRegion,
|
|
17
|
+
LabFooterRegion,
|
|
18
|
+
LabHeaderRegion,
|
|
19
|
+
LabSidebarRegion,
|
|
20
|
+
labContributions,
|
|
21
|
+
} from '../chrome/LabChrome';
|
|
22
|
+
import type { LabContribution } from '../chrome/labTypes';
|
|
12
23
|
import type { TrialContribution } from '../chrome/types';
|
|
13
24
|
import type { ConfigRule, ControlRenderer } from '../config/types';
|
|
14
|
-
import { configDefaultsOf, serializersOf } from '../instrument/serializers';
|
|
15
25
|
import type { InstrumentList } from '../instrument/types';
|
|
16
|
-
import {
|
|
26
|
+
import { Split } from '../primitives/Split';
|
|
27
|
+
import { defaultStorage, noneAdapter } from '../state/adapters';
|
|
17
28
|
import { LabStoreContext } from '../state/context';
|
|
29
|
+
import { type OpenedLabStore, openLabStore } from '../state/openLabStore';
|
|
30
|
+
import { PersistenceContext } from '../state/Persistence';
|
|
31
|
+
import { createRecordCache } from '../state/records';
|
|
18
32
|
import { createLabStore, type LabStore } from '../state/store';
|
|
19
33
|
import type { LabMode, StorageAdapter, TrialRecord } from '../state/types';
|
|
34
|
+
import { useOpenOnce, useWarnIgnoredChange } from '../state/useOpenOnce';
|
|
35
|
+
import { usePersistedState } from '../state/usePersistedState';
|
|
20
36
|
import { SurfaceCanvasContext, SurfaceContext } from '../surface/SurfaceContext';
|
|
21
37
|
import { useSurfaceCanvas, useSurfaceOptional } from '../surface/useSurfaceTile';
|
|
22
|
-
import { useTiledSurface } from '../surface/useTiledSurface';
|
|
38
|
+
import { type SurfaceFrame, useTiledSurface } from '../surface/useTiledSurface';
|
|
23
39
|
import { interstellarTheme } from '../theme/interstellar';
|
|
24
40
|
import type { TrialTool } from '../tools/types';
|
|
25
41
|
import { Trial } from '../trial/Trial';
|
|
@@ -29,22 +45,27 @@ import {
|
|
|
29
45
|
closeTrial as closeTrialOp,
|
|
30
46
|
reorderTrials as reorderTrialsOp,
|
|
31
47
|
resetTrial as resetTrialOp,
|
|
48
|
+
swapTrial as swapTrialOp,
|
|
32
49
|
} from '../trial/trialOps';
|
|
50
|
+
import { useLabFitWarning } from './fitCheck';
|
|
33
51
|
import { LabContext, type LabContextValue } from './LabContext';
|
|
34
52
|
import { LabHeader } from './LabHeader';
|
|
35
53
|
import { LabPalette } from './LabPalette';
|
|
36
54
|
import { LabShell } from './LabShell';
|
|
55
|
+
import type { LabPage } from './LabSwitcher';
|
|
37
56
|
import { createPanelHostRegistry, PanelHostContext } from './panelHost';
|
|
38
57
|
import { useResolvedMode } from './useSystemMode';
|
|
39
58
|
import { type PanelDescriptor, type TrialLayout, Workspace } from './Workspace';
|
|
40
59
|
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
interface LabBaseProps {
|
|
61
|
+
/** May change while mounted. An instrument that is a different object from
|
|
62
|
+
* the last render counts as replaced and its open trials' configs are
|
|
63
|
+
* refilled, so hoist or memoize the list. */
|
|
43
64
|
instruments: InstrumentList;
|
|
44
65
|
defaultInstrument: string;
|
|
45
|
-
storage?: StorageAdapter | null;
|
|
46
|
-
storageKey?: string;
|
|
47
66
|
mode?: LabMode;
|
|
67
|
+
/** Rendered while a stored lab loads. Default: the lab's empty shell. */
|
|
68
|
+
fallback?: ReactNode;
|
|
48
69
|
/**
|
|
49
70
|
* Optional list of CSS colors used to compose the interstellar theme's
|
|
50
71
|
* cosmic backdrop. Each color becomes one radial-gradient blob on the
|
|
@@ -53,12 +74,27 @@ export interface LabProps {
|
|
|
53
74
|
*/
|
|
54
75
|
nebula?: readonly string[];
|
|
55
76
|
title?: string;
|
|
77
|
+
/** The project's other labs. Given two or more, the title becomes the way to
|
|
78
|
+
* reach them — the same switcher `<LabShell>` renders, so a lab reached from
|
|
79
|
+
* one is not a dead end. */
|
|
80
|
+
pages?: readonly LabPage[];
|
|
81
|
+
/** The path the switcher marks as open. Defaults to the current location. */
|
|
82
|
+
path?: string;
|
|
56
83
|
/** Rendered in the shell's footer, below the workspace. */
|
|
57
84
|
footer?: ReactNode;
|
|
58
85
|
/** Contributions added to every trial's chrome, after the instrument's own. */
|
|
59
86
|
chrome?: readonly TrialContribution[];
|
|
87
|
+
/** Contributions to the lab's own chrome — its header bar, its sidebar, its
|
|
88
|
+
* tool rail and its footer — rather than to every trial's. Rendered after
|
|
89
|
+
* `children` and `footer`, which stay the way a lab drops arbitrary content
|
|
90
|
+
* into those same two boxes. Going from no sidebar contributions to some
|
|
91
|
+
* remounts the workspace and every trial, dropping unpersisted trial state. */
|
|
92
|
+
labChrome?: readonly LabContribution[];
|
|
60
93
|
/** Built-in contribution ids to drop. Throws on an id that is not there. */
|
|
61
94
|
suppress?: readonly string[];
|
|
95
|
+
/** Offer the header's add-trial control. Default `true`; a lab that opens
|
|
96
|
+
* trials some other way passes `false`. */
|
|
97
|
+
addTrial?: boolean;
|
|
62
98
|
/** Tools offered lab-wide. A trial whose instrument declares none of its own
|
|
63
99
|
* reflects and writes this slot. */
|
|
64
100
|
tools?: readonly TrialTool[];
|
|
@@ -72,31 +108,89 @@ export interface LabProps {
|
|
|
72
108
|
children?: ReactNode;
|
|
73
109
|
}
|
|
74
110
|
|
|
75
|
-
|
|
111
|
+
/** Props for `<Lab>`. With a `storageKey` the lab persists — to IndexedDB
|
|
112
|
+
* unless `storage` names another substrate — and both are read once, at
|
|
113
|
+
* mount. Without one, nothing persists. */
|
|
114
|
+
export type LabProps = LabBaseProps &
|
|
115
|
+
(
|
|
116
|
+
| { storageKey?: undefined; storage?: undefined }
|
|
117
|
+
| { storageKey: string; storage?: StorageAdapter }
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
interface OpenedLab extends OpenedLabStore {
|
|
121
|
+
marks: ReadonlyMap<string, unknown>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function seedDefaultTrial(
|
|
125
|
+
store: LabStore,
|
|
76
126
|
instruments: InstrumentList,
|
|
77
127
|
defaultInstrument: string,
|
|
78
|
-
|
|
128
|
+
): void {
|
|
129
|
+
if (store.getState().trials.length > 0) return;
|
|
130
|
+
const record = addTrialOp([], instruments, defaultInstrument)[0];
|
|
131
|
+
if (!record) return;
|
|
132
|
+
const { undoStack: _undoStack, ...rest } = record;
|
|
133
|
+
store.getState().addTrial(rest);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A lab with nothing to load renders at once. Its records hold
|
|
137
|
+
* `usePersistedState` values for the session and write nothing. */
|
|
138
|
+
function openUnstoredLab({ instruments, defaultInstrument, mode }: LabProps): OpenedLab {
|
|
139
|
+
const store = createLabStore({ initialMode: mode ?? 'auto', instruments });
|
|
140
|
+
seedDefaultTrial(store, instruments, defaultInstrument);
|
|
141
|
+
const records = createRecordCache({ storage: noneAdapter, prefix: '', writable: false });
|
|
142
|
+
return { store, records, close: () => records.close(), marks: new Map() };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function openStoredLab(
|
|
146
|
+
{ instruments, defaultInstrument, mode }: LabProps,
|
|
79
147
|
storageKey: string,
|
|
80
|
-
|
|
81
|
-
):
|
|
82
|
-
//
|
|
83
|
-
// being
|
|
84
|
-
const
|
|
148
|
+
storage: StorageAdapter | undefined,
|
|
149
|
+
): Promise<OpenedLab> {
|
|
150
|
+
// The store reads the instruments' serializers and defaults while it is being
|
|
151
|
+
// built, so they go in with it rather than being pushed onto it afterwards.
|
|
152
|
+
const opened = await openLabStore({
|
|
85
153
|
storageKey,
|
|
86
|
-
storage,
|
|
87
|
-
initialMode,
|
|
88
|
-
|
|
89
|
-
configDefaults: configDefaultsOf(instruments),
|
|
154
|
+
storage: storage ?? (await defaultStorage()),
|
|
155
|
+
initialMode: mode ?? 'auto',
|
|
156
|
+
instruments,
|
|
90
157
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
158
|
+
seedDefaultTrial(opened.store, instruments, defaultInstrument);
|
|
159
|
+
const marks = new Map<string, unknown>();
|
|
160
|
+
await Promise.all(
|
|
161
|
+
opened.store.getState().trials.map(async (trial) => {
|
|
162
|
+
const kept = instruments.find((i) => i.name === trial.instrumentName)?.annotations?.storage;
|
|
163
|
+
if (!kept) return;
|
|
164
|
+
try {
|
|
165
|
+
marks.set(trial.id, await kept.load());
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.warn(`[labkit] could not load the marks of trial "${trial.id}"`, error);
|
|
168
|
+
marks.set(trial.id, null);
|
|
169
|
+
}
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
return { ...opened, marks };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function LabFallback({
|
|
176
|
+
title,
|
|
177
|
+
mode,
|
|
178
|
+
pages,
|
|
179
|
+
path,
|
|
180
|
+
}: Pick<LabBaseProps, 'title' | 'mode' | 'pages' | 'path'>) {
|
|
181
|
+
const resolvedMode = useResolvedMode(mode ?? 'auto');
|
|
182
|
+
return (
|
|
183
|
+
<ThemeProvider theme={interstellarTheme} selection={{ mode: resolvedMode }} className="lk-lab">
|
|
184
|
+
<LabShell
|
|
185
|
+
title={title ?? 'Labkit'}
|
|
186
|
+
mode={mode}
|
|
187
|
+
{...(pages ? { pages } : {})}
|
|
188
|
+
{...(path !== undefined ? { path } : {})}
|
|
189
|
+
>
|
|
190
|
+
{null}
|
|
191
|
+
</LabShell>
|
|
192
|
+
</ThemeProvider>
|
|
193
|
+
);
|
|
100
194
|
}
|
|
101
195
|
|
|
102
196
|
// Fixed spread of nebula blob positions / sizes / fall-off stops. Colors
|
|
@@ -120,41 +214,118 @@ function buildNebula(colors: readonly string[]): string {
|
|
|
120
214
|
return blobs.join(', ');
|
|
121
215
|
}
|
|
122
216
|
|
|
123
|
-
/** The
|
|
217
|
+
/** The strip holding the sidebar beside the tool rail and workspace. Its own component
|
|
218
|
+
* because the width is a persisted value, and `LabRuntime` renders the
|
|
219
|
+
* persistence provider it reads from. */
|
|
220
|
+
function LabPanes({
|
|
221
|
+
contributions,
|
|
222
|
+
children,
|
|
223
|
+
}: {
|
|
224
|
+
contributions: readonly LabContribution[];
|
|
225
|
+
children: ReactNode;
|
|
226
|
+
}) {
|
|
227
|
+
const surface = useSurfaceOptional();
|
|
228
|
+
const [sidebarWidth, setSidebarWidth] = usePersistedState<number | undefined>(
|
|
229
|
+
'lk-lab-sidebar-width',
|
|
230
|
+
undefined,
|
|
231
|
+
{ scope: 'lab' },
|
|
232
|
+
);
|
|
233
|
+
const [asideWidth, setAsideWidth] = usePersistedState<number | undefined>(
|
|
234
|
+
'lk-lab-aside-width',
|
|
235
|
+
undefined,
|
|
236
|
+
{ scope: 'lab' },
|
|
237
|
+
);
|
|
238
|
+
const main = contributions.some((c) => c.region === 'aside') ? (
|
|
239
|
+
<Split
|
|
240
|
+
className="lk-lab__panes"
|
|
241
|
+
side="end"
|
|
242
|
+
sidebarClassName="lk-lab__aside"
|
|
243
|
+
contentClassName="lk-lab__main"
|
|
244
|
+
label="Lab aside"
|
|
245
|
+
defaultWidth={320}
|
|
246
|
+
width={asideWidth}
|
|
247
|
+
onWidthChange={(w) => {
|
|
248
|
+
setAsideWidth(w);
|
|
249
|
+
surface?.invalidateRects();
|
|
250
|
+
}}
|
|
251
|
+
sidebar={<LabAsideRegion contributions={contributions} />}
|
|
252
|
+
>
|
|
253
|
+
{children}
|
|
254
|
+
</Split>
|
|
255
|
+
) : (
|
|
256
|
+
children
|
|
257
|
+
);
|
|
258
|
+
if (!contributions.some((c) => c.region === 'sidebar')) return main;
|
|
259
|
+
return (
|
|
260
|
+
<Split
|
|
261
|
+
className="lk-lab__panes"
|
|
262
|
+
sidebarClassName="lk-lab__sidebar"
|
|
263
|
+
contentClassName="lk-lab__main"
|
|
264
|
+
label="Lab sidebar"
|
|
265
|
+
defaultWidth={260}
|
|
266
|
+
width={sidebarWidth}
|
|
267
|
+
onWidthChange={(w) => {
|
|
268
|
+
setSidebarWidth(w);
|
|
269
|
+
surface?.invalidateRects();
|
|
270
|
+
}}
|
|
271
|
+
sidebar={<LabSidebarRegion contributions={contributions} />}
|
|
272
|
+
>
|
|
273
|
+
{main}
|
|
274
|
+
</Split>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The lab runtime: opens the store, provides it, and renders one trial
|
|
124
279
|
* per record in a grid. Each trial runs one of `instruments`. */
|
|
125
|
-
export function Lab({
|
|
280
|
+
export function Lab(props: LabProps) {
|
|
281
|
+
if (process.env.NODE_ENV !== 'production' && props.instruments.length === 0) {
|
|
282
|
+
throw new Error('[labkit] <Lab> requires a non-empty `instruments` array');
|
|
283
|
+
}
|
|
284
|
+
const { storageKey, storage } = props;
|
|
285
|
+
useWarnIgnoredChange('<Lab>', { storageKey, storage });
|
|
286
|
+
const opened = useOpenOnce<OpenedLab>(() =>
|
|
287
|
+
storageKey === undefined ? openUnstoredLab(props) : openStoredLab(props, storageKey, storage),
|
|
288
|
+
);
|
|
289
|
+
if (!opened) {
|
|
290
|
+
if (props.fallback !== undefined) return props.fallback;
|
|
291
|
+
return (
|
|
292
|
+
<LabFallback title={props.title} mode={props.mode} pages={props.pages} path={props.path} />
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return <LabRuntime {...props} opened={opened} />;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function LabRuntime({
|
|
126
299
|
instruments,
|
|
127
|
-
defaultInstrument,
|
|
128
|
-
storage,
|
|
129
|
-
storageKey,
|
|
130
300
|
mode,
|
|
131
301
|
nebula,
|
|
132
302
|
title,
|
|
303
|
+
pages,
|
|
304
|
+
path,
|
|
133
305
|
footer,
|
|
134
306
|
chrome,
|
|
307
|
+
labChrome,
|
|
135
308
|
suppress,
|
|
309
|
+
addTrial,
|
|
136
310
|
tools,
|
|
137
311
|
configRules,
|
|
138
312
|
controls,
|
|
139
313
|
children,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
314
|
+
opened,
|
|
315
|
+
}: LabProps & { opened: OpenedLab }) {
|
|
316
|
+
const { store } = opened;
|
|
144
317
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
storage ?? noneAdapter,
|
|
151
|
-
storageKey ?? 'labkit',
|
|
152
|
-
mode ?? 'auto',
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
const store = storeRef.current;
|
|
318
|
+
// The store opened against the list <Lab> mounted with; a later list reaches
|
|
319
|
+
// it here, before paint.
|
|
320
|
+
useLayoutEffect(() => {
|
|
321
|
+
store.getState().setInstruments(instruments);
|
|
322
|
+
}, [instruments, store]);
|
|
156
323
|
|
|
157
324
|
const trials = useStore(store, (s) => s.trials);
|
|
325
|
+
const [focusPick, setFocusPick] = useState<string | null>(null);
|
|
326
|
+
const focusedTrialId = trials.some((t) => t.id === focusPick)
|
|
327
|
+
? focusPick
|
|
328
|
+
: (trials[0]?.id ?? null);
|
|
158
329
|
const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
|
|
159
330
|
const modeValue = useStore(store, (s) => s.mode);
|
|
160
331
|
const layout = useStore(store, (s) => s.layout);
|
|
@@ -167,38 +338,97 @@ export function Lab({
|
|
|
167
338
|
if (panelHostsRef.current === null) panelHostsRef.current = createPanelHostRegistry();
|
|
168
339
|
|
|
169
340
|
// One shared drawing surface for the whole lab, anchored to the body — tile
|
|
170
|
-
// rects compose against it, and
|
|
341
|
+
// rects compose against it, and both buffers compose against the same rects.
|
|
171
342
|
// `Workspace` invalidates rects when the grid moves something a
|
|
172
343
|
// ResizeObserver cannot see. A host that already owns a surface keeps it: a
|
|
173
344
|
// lab embedded in a larger shared-surface app must not open a second GL
|
|
174
345
|
// tenancy, and mounts no buffer of its own.
|
|
175
346
|
const outerSurface = useSurfaceOptional();
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const
|
|
347
|
+
const outerOver = useSurfaceCanvas('over');
|
|
348
|
+
const outerUnder = useSurfaceCanvas('under');
|
|
349
|
+
const [ownOver, setOwnOver] = useState<HTMLCanvasElement | null>(null);
|
|
350
|
+
const [ownUnder, setOwnUnder] = useState<HTMLCanvasElement | null>(null);
|
|
351
|
+
const ownOverRef = useRef<HTMLCanvasElement | null>(null);
|
|
352
|
+
const ownUnderRef = useRef<HTMLCanvasElement | null>(null);
|
|
179
353
|
const bufferRef = useRef({ w: 0, h: 0 });
|
|
180
354
|
const surfaceRef = useRef<ReturnType<typeof useTiledSurface> | null>(null);
|
|
181
355
|
|
|
182
356
|
// Sizing the buffer clears all of it, so every tile has to repaint — not
|
|
183
|
-
// only the one whose move triggered the measurement.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
357
|
+
// only the one whose move triggered the measurement. Both buffers are sized
|
|
358
|
+
// together off one comparison: they are the same box, so a tenant of either
|
|
359
|
+
// is looking at the same rects, and one invalidateAll covers both.
|
|
360
|
+
const onFrame = useCallback((frame: SurfaceFrame) => {
|
|
361
|
+
const canvases = [ownUnderRef.current, ownOverRef.current].filter((c) => c !== null);
|
|
362
|
+
if (canvases.length === 0) return;
|
|
187
363
|
const w = Math.round(frame.size.width * frame.dpr);
|
|
188
364
|
const h = Math.round(frame.size.height * frame.dpr);
|
|
189
|
-
if (bufferRef.current.w
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
365
|
+
if (bufferRef.current.w !== w || bufferRef.current.h !== h) {
|
|
366
|
+
bufferRef.current = { w, h };
|
|
367
|
+
for (const c of canvases) {
|
|
368
|
+
c.width = w;
|
|
369
|
+
c.height = h;
|
|
370
|
+
c.style.width = `${frame.size.width}px`;
|
|
371
|
+
c.style.height = `${frame.size.height}px`;
|
|
372
|
+
}
|
|
373
|
+
surfaceRef.current?.invalidateAll();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
// A same-size re-tile does not go through here: assigning `width` its own
|
|
377
|
+
// value resizes nothing, so it clears nothing. The tenants clear, through
|
|
378
|
+
// `registerClear`.
|
|
196
379
|
}, []);
|
|
197
380
|
|
|
198
381
|
const ownSurface = useTiledSurface({ onFrame });
|
|
199
382
|
surfaceRef.current = ownSurface;
|
|
383
|
+
const [labBody, setLabBody] = useState<HTMLDivElement | null>(null);
|
|
384
|
+
const attachOwnSurface = ownSurface.containerRef;
|
|
385
|
+
const labBodyRef = useCallback(
|
|
386
|
+
(el: HTMLDivElement | null) => {
|
|
387
|
+
setLabBody(el);
|
|
388
|
+
if (!outerSurface) attachOwnSurface(el);
|
|
389
|
+
},
|
|
390
|
+
[outerSurface, attachOwnSurface],
|
|
391
|
+
);
|
|
392
|
+
useLabFitWarning(labBody);
|
|
393
|
+
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
if (!labBody) return;
|
|
396
|
+
const pick = (node: unknown): void => {
|
|
397
|
+
const owned = new Set(store.getState().trials.map((t) => t.id));
|
|
398
|
+
const nearest = (from: Element | null | undefined) =>
|
|
399
|
+
from?.closest<HTMLElement>('.lk-trial[data-trial-id]') ?? null;
|
|
400
|
+
let trial =
|
|
401
|
+
typeof (node as Element | null)?.closest === 'function' ? nearest(node as Element) : null;
|
|
402
|
+
// A trial can hold a lab of its own, whose trials this lab does not own.
|
|
403
|
+
while (trial && !owned.has(trial.dataset.trialId ?? '')) trial = nearest(trial.parentElement);
|
|
404
|
+
if (trial?.dataset.trialId) setFocusPick(trial.dataset.trialId);
|
|
405
|
+
};
|
|
406
|
+
const onInput = (event: Event): void => pick(event.target);
|
|
407
|
+
// Focus moving into a frame fires nothing in this document: the window
|
|
408
|
+
// blurs, and the frame is the active element once it has.
|
|
409
|
+
let pending: ReturnType<typeof setTimeout> | undefined;
|
|
410
|
+
const doc = labBody.ownerDocument;
|
|
411
|
+
const win = doc.defaultView;
|
|
412
|
+
const onBlur = (): void => {
|
|
413
|
+
clearTimeout(pending);
|
|
414
|
+
pending = setTimeout(() => pick(doc.activeElement), 0);
|
|
415
|
+
};
|
|
416
|
+
labBody.addEventListener('pointerdown', onInput, true);
|
|
417
|
+
labBody.addEventListener('focusin', onInput, true);
|
|
418
|
+
win?.addEventListener('blur', onBlur);
|
|
419
|
+
return () => {
|
|
420
|
+
clearTimeout(pending);
|
|
421
|
+
labBody.removeEventListener('pointerdown', onInput, true);
|
|
422
|
+
labBody.removeEventListener('focusin', onInput, true);
|
|
423
|
+
win?.removeEventListener('blur', onBlur);
|
|
424
|
+
};
|
|
425
|
+
}, [labBody, store]);
|
|
200
426
|
const surface = outerSurface ?? ownSurface;
|
|
201
|
-
const
|
|
427
|
+
const surfaceCanvases = useMemo(
|
|
428
|
+
() =>
|
|
429
|
+
outerSurface ? { over: outerOver, under: outerUnder } : { over: ownOver, under: ownUnder },
|
|
430
|
+
[outerSurface, outerOver, outerUnder, ownOver, ownUnder],
|
|
431
|
+
);
|
|
202
432
|
|
|
203
433
|
const workspacePanels = useMemo<PanelDescriptor[]>(
|
|
204
434
|
() =>
|
|
@@ -211,6 +441,19 @@ export function Lab({
|
|
|
211
441
|
);
|
|
212
442
|
const resolvedMode = useResolvedMode(modeValue);
|
|
213
443
|
|
|
444
|
+
// Tools and lab contributions share one id namespace, so they merge once
|
|
445
|
+
// here — before any region renders — and a collision throws rather than one
|
|
446
|
+
// of them silently losing.
|
|
447
|
+
// Declaring `annotations` on any instrument puts the drawing tools here, in
|
|
448
|
+
// the lab's rail: one tool is armed across every trial.
|
|
449
|
+
const annotates = instruments.some((i) => i.annotations != null);
|
|
450
|
+
const labChromeAll = useMemo(
|
|
451
|
+
() => labContributions([...(annotates ? ANNOTATION_TOOLS : []), ...(tools ?? [])], labChrome),
|
|
452
|
+
[annotates, tools, labChrome],
|
|
453
|
+
);
|
|
454
|
+
const hasFooterChrome = labChromeAll.some((c) => c.region === 'footer');
|
|
455
|
+
const hasPaneChrome = labChromeAll.some((c) => c.region === 'sidebar' || c.region === 'aside');
|
|
456
|
+
|
|
214
457
|
useEffect(() => {
|
|
215
458
|
if (mode && mode !== store.getState().mode) {
|
|
216
459
|
store.getState().setMode(mode);
|
|
@@ -222,20 +465,24 @@ export function Lab({
|
|
|
222
465
|
const currentById = new Map(store.getState().trials.map((w) => [w.id, w]));
|
|
223
466
|
const merged = next.map((w) => currentById.get(w.id) ?? w);
|
|
224
467
|
store.setState({ trials: merged });
|
|
225
|
-
|
|
226
|
-
|
|
468
|
+
};
|
|
469
|
+
const replaceAndFocusAdded = (next: TrialRecord[]): void => {
|
|
470
|
+
const before = new Set(store.getState().trials.map((w) => w.id));
|
|
471
|
+
replaceTrials(next);
|
|
472
|
+
const added = next.find((w) => !before.has(w.id));
|
|
473
|
+
if (added) setFocusPick(added.id);
|
|
227
474
|
};
|
|
228
475
|
|
|
229
476
|
return {
|
|
230
477
|
instruments,
|
|
231
478
|
trials,
|
|
232
479
|
addTrial: (instrumentName, options) => {
|
|
233
|
-
|
|
234
|
-
|
|
480
|
+
replaceAndFocusAdded(
|
|
481
|
+
addTrialOp(store.getState().trials, instruments, instrumentName, options),
|
|
482
|
+
);
|
|
235
483
|
},
|
|
236
484
|
cloneTrial: (id) => {
|
|
237
|
-
|
|
238
|
-
replaceTrials(next);
|
|
485
|
+
replaceAndFocusAdded(cloneTrialOp(store.getState().trials, id));
|
|
239
486
|
},
|
|
240
487
|
closeTrial: (id) => {
|
|
241
488
|
const next = closeTrialOp(store.getState().trials, id);
|
|
@@ -244,6 +491,19 @@ export function Lab({
|
|
|
244
491
|
reorderTrials: (ids) => {
|
|
245
492
|
replaceTrials(reorderTrialsOp(store.getState().trials, ids));
|
|
246
493
|
},
|
|
494
|
+
swapTrial: (id, instrumentName, options) => {
|
|
495
|
+
const current = store.getState();
|
|
496
|
+
const next = swapTrialOp(current.trials, instruments, id, instrumentName, options);
|
|
497
|
+
if (next === current.trials) return;
|
|
498
|
+
const record = next[current.trials.findIndex((w) => w.id === id)] as TrialRecord;
|
|
499
|
+
// Written before the tile registers, which is when the grid reads it.
|
|
500
|
+
const { [id]: extent, ...layoutRest } = current.layout as TrialLayout;
|
|
501
|
+
if (extent) current.setLayout({ ...layoutRest, [record.id]: extent });
|
|
502
|
+
replaceTrials(next);
|
|
503
|
+
if (focusedTrialId === id) setFocusPick(record.id);
|
|
504
|
+
},
|
|
505
|
+
focusedTrialId,
|
|
506
|
+
focusTrial: (id) => setFocusPick(id),
|
|
247
507
|
resetTrial: (id) => {
|
|
248
508
|
const next = resetTrialOp(store.getState().trials, id, instruments);
|
|
249
509
|
const record = next.find((w) => w.id === id);
|
|
@@ -255,7 +515,6 @@ export function Lab({
|
|
|
255
515
|
: w,
|
|
256
516
|
),
|
|
257
517
|
}));
|
|
258
|
-
store.getState().updateTrialView(id, record.view);
|
|
259
518
|
},
|
|
260
519
|
savedSnapshots,
|
|
261
520
|
saveSnapshot: (trialId, name) => {
|
|
@@ -275,7 +534,16 @@ export function Lab({
|
|
|
275
534
|
configRules,
|
|
276
535
|
controls,
|
|
277
536
|
};
|
|
278
|
-
}, [
|
|
537
|
+
}, [
|
|
538
|
+
instruments,
|
|
539
|
+
trials,
|
|
540
|
+
focusedTrialId,
|
|
541
|
+
savedSnapshots,
|
|
542
|
+
modeValue,
|
|
543
|
+
store,
|
|
544
|
+
configRules,
|
|
545
|
+
controls,
|
|
546
|
+
]);
|
|
279
547
|
|
|
280
548
|
// Only override the backdrop in dark, where there is one to override.
|
|
281
549
|
// Setting a CSS custom property is the sanctioned use of inline style.
|
|
@@ -284,66 +552,103 @@ export function Lab({
|
|
|
284
552
|
? ({ ['--wzl-backdrop' as string]: buildNebula(nebula) } as CSSProperties)
|
|
285
553
|
: undefined;
|
|
286
554
|
|
|
555
|
+
const workspace = (
|
|
556
|
+
<Workspace
|
|
557
|
+
panels={workspacePanels}
|
|
558
|
+
ids={trials.map((w) => w.id)}
|
|
559
|
+
resizable
|
|
560
|
+
reorderable
|
|
561
|
+
onReorder={(ids) => contextValue.reorderTrials(ids)}
|
|
562
|
+
layout={layout as TrialLayout}
|
|
563
|
+
onLayoutChange={(next) => store.getState().setLayout(next)}
|
|
564
|
+
>
|
|
565
|
+
{trials.map((w) => (
|
|
566
|
+
<Trial key={w.id} id={w.id} chrome={chrome} suppress={suppress} />
|
|
567
|
+
))}
|
|
568
|
+
</Workspace>
|
|
569
|
+
);
|
|
570
|
+
|
|
287
571
|
return (
|
|
288
572
|
<LabStoreContext.Provider value={{ store }}>
|
|
289
|
-
<
|
|
290
|
-
<
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
{
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
573
|
+
<PersistenceContext.Provider value={opened.records}>
|
|
574
|
+
<AnnotationPreloadContext.Provider value={opened.marks}>
|
|
575
|
+
<LabContext.Provider value={contextValue}>
|
|
576
|
+
<ThemeProvider
|
|
577
|
+
theme={interstellarTheme}
|
|
578
|
+
selection={{ mode: resolvedMode }}
|
|
579
|
+
className="lk-lab"
|
|
580
|
+
style={backdropStyle}
|
|
581
|
+
>
|
|
582
|
+
<LabShell
|
|
583
|
+
title={title ?? 'Labkit'}
|
|
584
|
+
mode={modeValue}
|
|
585
|
+
{...(pages ? { pages } : {})}
|
|
586
|
+
{...(path !== undefined ? { path } : {})}
|
|
587
|
+
footer={
|
|
588
|
+
hasFooterChrome ? (
|
|
589
|
+
<>
|
|
590
|
+
{footer}
|
|
591
|
+
<LabFooterRegion contributions={labChromeAll} />
|
|
592
|
+
</>
|
|
593
|
+
) : (
|
|
594
|
+
footer
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
header={
|
|
598
|
+
<>
|
|
599
|
+
<LabHeader {...(addTrial !== undefined ? { addTrial } : {})} />
|
|
600
|
+
{children}
|
|
601
|
+
<LabHeaderRegion contributions={labChromeAll} />
|
|
602
|
+
</>
|
|
603
|
+
}
|
|
604
|
+
>
|
|
605
|
+
<PanelHostContext.Provider value={panelHostsRef.current}>
|
|
606
|
+
<SurfaceContext.Provider value={surface}>
|
|
607
|
+
<SurfaceCanvasContext.Provider value={surfaceCanvases}>
|
|
608
|
+
<div className="lk-lab__body" ref={labBodyRef}>
|
|
609
|
+
{outerSurface ? null : (
|
|
610
|
+
// Two buffers stacked around the trials, both inert —
|
|
611
|
+
// each tile takes input from its own box. A tile's marks
|
|
612
|
+
// annotate the instrument's DOM from over it; an opaque
|
|
613
|
+
// renderer sits under it, so the pane can still hold a
|
|
614
|
+
// label. The under one is first so it paints first.
|
|
615
|
+
<>
|
|
616
|
+
<canvas
|
|
617
|
+
className="lk-lab__surface lk-lab__surface--under"
|
|
618
|
+
ref={(el) => {
|
|
619
|
+
ownUnderRef.current = el;
|
|
620
|
+
setOwnUnder(el);
|
|
621
|
+
}}
|
|
622
|
+
/>
|
|
623
|
+
<canvas
|
|
624
|
+
className="lk-lab__surface lk-lab__surface--over"
|
|
625
|
+
ref={(el) => {
|
|
626
|
+
ownOverRef.current = el;
|
|
627
|
+
setOwnOver(el);
|
|
628
|
+
}}
|
|
629
|
+
/>
|
|
630
|
+
</>
|
|
631
|
+
)}
|
|
632
|
+
{hasPaneChrome ? (
|
|
633
|
+
<LabPanes contributions={labChromeAll}>
|
|
634
|
+
<LabPalette contributions={labChromeAll} />
|
|
635
|
+
{workspace}
|
|
636
|
+
</LabPanes>
|
|
637
|
+
) : (
|
|
638
|
+
<>
|
|
639
|
+
<LabPalette contributions={labChromeAll} />
|
|
640
|
+
{workspace}
|
|
641
|
+
</>
|
|
642
|
+
)}
|
|
643
|
+
</div>
|
|
644
|
+
</SurfaceCanvasContext.Provider>
|
|
645
|
+
</SurfaceContext.Provider>
|
|
646
|
+
</PanelHostContext.Provider>
|
|
647
|
+
</LabShell>
|
|
648
|
+
</ThemeProvider>
|
|
649
|
+
</LabContext.Provider>
|
|
650
|
+
</AnnotationPreloadContext.Provider>
|
|
651
|
+
</PersistenceContext.Provider>
|
|
347
652
|
</LabStoreContext.Provider>
|
|
348
653
|
);
|
|
349
654
|
}
|