@weasel-js/labkit 1.4.4 → 1.5.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 +13 -2
- package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CqkxkFJW.d.ts} +1 -1
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +116 -0
- package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-8R6UmlvN.d.ts} +253 -31
- package/dist/_dts/index-B8uu2ba7.d.ts +237 -0
- package/dist/_dts/{index-Blv9uQzu.d.ts → index-retnAut7.d.ts} +18 -10
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-Ca2LCnPq.d.ts} +65 -2
- package/dist/_dts/usePanZoom-nS798rOF.d.ts +160 -0
- package/dist/_dts/useTrialState-jcx_TdU6.d.ts +212 -0
- package/dist/canvas/index.d.ts +17 -128
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +4 -92
- package/dist/chrome/index.js +9 -5
- package/dist/chunk-67SJLMC7.js +704 -0
- package/dist/chunk-67SJLMC7.js.map +1 -0
- package/dist/chunk-6ZDGOZQV.js +310 -0
- package/dist/chunk-6ZDGOZQV.js.map +1 -0
- package/dist/{chunk-MOM3GOVY.js → chunk-EKIICY6X.js} +111 -25
- package/dist/chunk-EKIICY6X.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-SMHP6XZ4.js → chunk-KJALCDWE.js} +9 -7
- package/dist/chunk-KJALCDWE.js.map +1 -0
- package/dist/{chunk-BKFVHKJH.js → chunk-MUKOW3TC.js} +3 -3
- package/dist/{chunk-BKFVHKJH.js.map → chunk-MUKOW3TC.js.map} +1 -1
- package/dist/chunk-NDRYLVOW.js +15 -0
- package/dist/chunk-NDRYLVOW.js.map +1 -0
- package/dist/{chunk-64ZCN3DA.js → chunk-NS64DXMN.js} +226 -159
- package/dist/chunk-NS64DXMN.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-PGETSEDK.js} +135 -13
- package/dist/chunk-PGETSEDK.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-QLSV2N3G.js} +32 -37
- package/dist/chunk-QLSV2N3G.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-S2SHZRD7.js +7681 -0
- package/dist/chunk-S2SHZRD7.js.map +1 -0
- package/dist/{chunk-U3IYHIAE.js → chunk-TQBBYTAF.js} +19 -82
- package/dist/chunk-TQBBYTAF.js.map +1 -0
- package/dist/chunk-UU3NJO6N.js +72 -0
- package/dist/chunk-UU3NJO6N.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ZPH5WTWF.js} +21 -15
- package/dist/chunk-ZPH5WTWF.js.map +1 -0
- package/dist/config/index.d.ts +232 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +2 -3
- package/dist/controls/index.js +3 -2
- package/dist/dragdrop/index.d.ts +2 -4
- package/dist/index.d.ts +110 -381
- package/dist/index.js +691 -817
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -5
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +6 -8
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +164 -122
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +53 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +12 -7
- package/dist/state/index.js +16 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +84 -50
- 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 +2 -4
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +53 -5
- package/src/annotations/Annotations.overlay.test.tsx +85 -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/toolMap.ts +3 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- 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.ts +10 -5
- package/src/chrome/ChromeRegions.stories.tsx +0 -3
- package/src/chrome/LabChrome.tsx +114 -0
- package/src/chrome/builtins.tsx +20 -31
- package/src/chrome/index.ts +15 -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 +21 -9
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +30 -16
- package/src/chrome/types.ts +47 -20
- package/src/config/builder.test.ts +4 -0
- package/src/config/builder.ts +16 -10
- 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 +13 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/types.ts +3 -1
- package/src/controls/ControlPanel.test.tsx +16 -0
- package/src/controls/ControlPanel.tsx +5 -2
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +38 -5
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +11 -0
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +0 -2
- package/src/lab/Lab.surface.test.tsx +66 -3
- package/src/lab/Lab.test.tsx +91 -17
- package/src/lab/Lab.tsx +337 -124
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +0 -3
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -13
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabShell.less +49 -12
- package/src/lab/LabSwitcher.less +8 -1
- package/src/lab/Workspace.surface.test.tsx +2 -1
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/loupe/AGENTS.md +1 -1
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +1 -1
- package/src/loupe/useLoupe.ts +1 -1
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +13 -1
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +108 -0
- package/src/primitives/Split.tsx +159 -0
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -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 +104 -635
- package/src/state/store.ts +116 -157
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +39 -15
- 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 +2 -3
- package/src/state/view.test.ts +49 -15
- package/src/state/view.ts +18 -1
- package/src/styles.less +2 -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 +2 -1
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +175 -0
- package/src/surface/useTiledSurface.ts +54 -5
- package/src/theme/Interstellar.stories.tsx +7 -7
- package/src/theme/interstellar.test.ts +1 -1
- package/src/theme/interstellar.tokens.json +2 -2
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +47 -4
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +1 -26
- package/src/trial/Trial.stories.tsx +1 -2
- package/src/trial/Trial.test.tsx +28 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +118 -38
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +7 -4
- package/src/trial/trialOps.ts +5 -3
- 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-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.map +0 -1
- package/dist/chunk-W3ECWC2K.js +0 -7335
- package/dist/chunk-W3ECWC2K.js.map +0 -1
package/src/lab/Lab.tsx
CHANGED
|
@@ -4,22 +4,37 @@ 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
|
+
LabFooterRegion,
|
|
17
|
+
LabHeaderRegion,
|
|
18
|
+
LabSidebarRegion,
|
|
19
|
+
labContributions,
|
|
20
|
+
} from '../chrome/LabChrome';
|
|
21
|
+
import type { LabContribution } from '../chrome/labTypes';
|
|
12
22
|
import type { TrialContribution } from '../chrome/types';
|
|
13
23
|
import type { ConfigRule, ControlRenderer } from '../config/types';
|
|
14
|
-
import { configDefaultsOf, serializersOf } from '../instrument/serializers';
|
|
15
24
|
import type { InstrumentList } from '../instrument/types';
|
|
16
|
-
import {
|
|
25
|
+
import { Split } from '../primitives/Split';
|
|
26
|
+
import { defaultStorage, noneAdapter } from '../state/adapters';
|
|
17
27
|
import { LabStoreContext } from '../state/context';
|
|
28
|
+
import { type OpenedLabStore, openLabStore } from '../state/openLabStore';
|
|
29
|
+
import { PersistenceContext } from '../state/Persistence';
|
|
30
|
+
import { createRecordCache } from '../state/records';
|
|
18
31
|
import { createLabStore, type LabStore } from '../state/store';
|
|
19
32
|
import type { LabMode, StorageAdapter, TrialRecord } from '../state/types';
|
|
33
|
+
import { useOpenOnce, useWarnIgnoredChange } from '../state/useOpenOnce';
|
|
34
|
+
import { usePersistedState } from '../state/usePersistedState';
|
|
20
35
|
import { SurfaceCanvasContext, SurfaceContext } from '../surface/SurfaceContext';
|
|
21
36
|
import { useSurfaceCanvas, useSurfaceOptional } from '../surface/useSurfaceTile';
|
|
22
|
-
import { useTiledSurface } from '../surface/useTiledSurface';
|
|
37
|
+
import { type SurfaceFrame, useTiledSurface } from '../surface/useTiledSurface';
|
|
23
38
|
import { interstellarTheme } from '../theme/interstellar';
|
|
24
39
|
import type { TrialTool } from '../tools/types';
|
|
25
40
|
import { Trial } from '../trial/Trial';
|
|
@@ -30,21 +45,25 @@ import {
|
|
|
30
45
|
reorderTrials as reorderTrialsOp,
|
|
31
46
|
resetTrial as resetTrialOp,
|
|
32
47
|
} from '../trial/trialOps';
|
|
48
|
+
import { useLabFitWarning } from './fitCheck';
|
|
33
49
|
import { LabContext, type LabContextValue } from './LabContext';
|
|
34
50
|
import { LabHeader } from './LabHeader';
|
|
35
51
|
import { LabPalette } from './LabPalette';
|
|
36
52
|
import { LabShell } from './LabShell';
|
|
53
|
+
import type { LabPage } from './LabSwitcher';
|
|
37
54
|
import { createPanelHostRegistry, PanelHostContext } from './panelHost';
|
|
38
55
|
import { useResolvedMode } from './useSystemMode';
|
|
39
56
|
import { type PanelDescriptor, type TrialLayout, Workspace } from './Workspace';
|
|
40
57
|
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
interface LabBaseProps {
|
|
59
|
+
/** May change while mounted. An instrument that is a different object from
|
|
60
|
+
* the last render counts as replaced and its open trials' configs are
|
|
61
|
+
* refilled, so hoist or memoize the list. */
|
|
43
62
|
instruments: InstrumentList;
|
|
44
63
|
defaultInstrument: string;
|
|
45
|
-
storage?: StorageAdapter | null;
|
|
46
|
-
storageKey?: string;
|
|
47
64
|
mode?: LabMode;
|
|
65
|
+
/** Rendered while a stored lab loads. Default: the lab's empty shell. */
|
|
66
|
+
fallback?: ReactNode;
|
|
48
67
|
/**
|
|
49
68
|
* Optional list of CSS colors used to compose the interstellar theme's
|
|
50
69
|
* cosmic backdrop. Each color becomes one radial-gradient blob on the
|
|
@@ -53,12 +72,27 @@ export interface LabProps {
|
|
|
53
72
|
*/
|
|
54
73
|
nebula?: readonly string[];
|
|
55
74
|
title?: string;
|
|
75
|
+
/** The project's other labs. Given two or more, the title becomes the way to
|
|
76
|
+
* reach them — the same switcher `<LabShell>` renders, so a lab reached from
|
|
77
|
+
* one is not a dead end. */
|
|
78
|
+
pages?: readonly LabPage[];
|
|
79
|
+
/** The path the switcher marks as open. Defaults to the current location. */
|
|
80
|
+
path?: string;
|
|
56
81
|
/** Rendered in the shell's footer, below the workspace. */
|
|
57
82
|
footer?: ReactNode;
|
|
58
83
|
/** Contributions added to every trial's chrome, after the instrument's own. */
|
|
59
84
|
chrome?: readonly TrialContribution[];
|
|
85
|
+
/** Contributions to the lab's own chrome — its header bar, its sidebar, its
|
|
86
|
+
* tool rail and its footer — rather than to every trial's. Rendered after
|
|
87
|
+
* `children` and `footer`, which stay the way a lab drops arbitrary content
|
|
88
|
+
* into those same two boxes. Going from no sidebar contributions to some
|
|
89
|
+
* remounts the workspace and every trial, dropping unpersisted trial state. */
|
|
90
|
+
labChrome?: readonly LabContribution[];
|
|
60
91
|
/** Built-in contribution ids to drop. Throws on an id that is not there. */
|
|
61
92
|
suppress?: readonly string[];
|
|
93
|
+
/** Offer the header's add-trial control. Default `true`; a lab that opens
|
|
94
|
+
* trials some other way passes `false`. */
|
|
95
|
+
addTrial?: boolean;
|
|
62
96
|
/** Tools offered lab-wide. A trial whose instrument declares none of its own
|
|
63
97
|
* reflects and writes this slot. */
|
|
64
98
|
tools?: readonly TrialTool[];
|
|
@@ -72,31 +106,89 @@ export interface LabProps {
|
|
|
72
106
|
children?: ReactNode;
|
|
73
107
|
}
|
|
74
108
|
|
|
75
|
-
|
|
109
|
+
/** Props for `<Lab>`. With a `storageKey` the lab persists — to IndexedDB
|
|
110
|
+
* unless `storage` names another substrate — and both are read once, at
|
|
111
|
+
* mount. Without one, nothing persists. */
|
|
112
|
+
export type LabProps = LabBaseProps &
|
|
113
|
+
(
|
|
114
|
+
| { storageKey?: undefined; storage?: undefined }
|
|
115
|
+
| { storageKey: string; storage?: StorageAdapter }
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
interface OpenedLab extends OpenedLabStore {
|
|
119
|
+
marks: ReadonlyMap<string, unknown>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function seedDefaultTrial(
|
|
123
|
+
store: LabStore,
|
|
76
124
|
instruments: InstrumentList,
|
|
77
125
|
defaultInstrument: string,
|
|
78
|
-
|
|
126
|
+
): void {
|
|
127
|
+
if (store.getState().trials.length > 0) return;
|
|
128
|
+
const record = addTrialOp([], instruments, defaultInstrument)[0];
|
|
129
|
+
if (!record) return;
|
|
130
|
+
const { undoStack: _undoStack, ...rest } = record;
|
|
131
|
+
store.getState().addTrial(rest);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A lab with nothing to load renders at once. Its records hold
|
|
135
|
+
* `usePersistedState` values for the session and write nothing. */
|
|
136
|
+
function openUnstoredLab({ instruments, defaultInstrument, mode }: LabProps): OpenedLab {
|
|
137
|
+
const store = createLabStore({ initialMode: mode ?? 'auto', instruments });
|
|
138
|
+
seedDefaultTrial(store, instruments, defaultInstrument);
|
|
139
|
+
const records = createRecordCache({ storage: noneAdapter, prefix: '', writable: false });
|
|
140
|
+
return { store, records, close: () => records.close(), marks: new Map() };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function openStoredLab(
|
|
144
|
+
{ instruments, defaultInstrument, mode }: LabProps,
|
|
79
145
|
storageKey: string,
|
|
80
|
-
|
|
81
|
-
):
|
|
82
|
-
//
|
|
83
|
-
// being
|
|
84
|
-
const
|
|
146
|
+
storage: StorageAdapter | undefined,
|
|
147
|
+
): Promise<OpenedLab> {
|
|
148
|
+
// The store reads the instruments' serializers and defaults while it is being
|
|
149
|
+
// built, so they go in with it rather than being pushed onto it afterwards.
|
|
150
|
+
const opened = await openLabStore({
|
|
85
151
|
storageKey,
|
|
86
|
-
storage,
|
|
87
|
-
initialMode,
|
|
88
|
-
|
|
89
|
-
configDefaults: configDefaultsOf(instruments),
|
|
152
|
+
storage: storage ?? (await defaultStorage()),
|
|
153
|
+
initialMode: mode ?? 'auto',
|
|
154
|
+
instruments,
|
|
90
155
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
156
|
+
seedDefaultTrial(opened.store, instruments, defaultInstrument);
|
|
157
|
+
const marks = new Map<string, unknown>();
|
|
158
|
+
await Promise.all(
|
|
159
|
+
opened.store.getState().trials.map(async (trial) => {
|
|
160
|
+
const kept = instruments.find((i) => i.name === trial.instrumentName)?.annotations?.storage;
|
|
161
|
+
if (!kept) return;
|
|
162
|
+
try {
|
|
163
|
+
marks.set(trial.id, await kept.load());
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.warn(`[labkit] could not load the marks of trial "${trial.id}"`, error);
|
|
166
|
+
marks.set(trial.id, null);
|
|
167
|
+
}
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
return { ...opened, marks };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function LabFallback({
|
|
174
|
+
title,
|
|
175
|
+
mode,
|
|
176
|
+
pages,
|
|
177
|
+
path,
|
|
178
|
+
}: Pick<LabBaseProps, 'title' | 'mode' | 'pages' | 'path'>) {
|
|
179
|
+
const resolvedMode = useResolvedMode(mode ?? 'auto');
|
|
180
|
+
return (
|
|
181
|
+
<ThemeProvider theme={interstellarTheme} mode={resolvedMode} className="lk-lab">
|
|
182
|
+
<LabShell
|
|
183
|
+
title={title ?? 'Labkit'}
|
|
184
|
+
mode={mode}
|
|
185
|
+
{...(pages ? { pages } : {})}
|
|
186
|
+
{...(path !== undefined ? { path } : {})}
|
|
187
|
+
>
|
|
188
|
+
{null}
|
|
189
|
+
</LabShell>
|
|
190
|
+
</ThemeProvider>
|
|
191
|
+
);
|
|
100
192
|
}
|
|
101
193
|
|
|
102
194
|
// Fixed spread of nebula blob positions / sizes / fall-off stops. Colors
|
|
@@ -120,39 +212,87 @@ function buildNebula(colors: readonly string[]): string {
|
|
|
120
212
|
return blobs.join(', ');
|
|
121
213
|
}
|
|
122
214
|
|
|
123
|
-
/** The
|
|
215
|
+
/** The strip holding the sidebar beside the tool rail and workspace. Its own component
|
|
216
|
+
* because the width is a persisted value, and `LabRuntime` renders the
|
|
217
|
+
* persistence provider it reads from. */
|
|
218
|
+
function LabPanes({
|
|
219
|
+
contributions,
|
|
220
|
+
children,
|
|
221
|
+
}: {
|
|
222
|
+
contributions: readonly LabContribution[];
|
|
223
|
+
children: ReactNode;
|
|
224
|
+
}) {
|
|
225
|
+
const surface = useSurfaceOptional();
|
|
226
|
+
const [sidebarWidth, setSidebarWidth] = usePersistedState<number | undefined>(
|
|
227
|
+
'lk-lab-sidebar-width',
|
|
228
|
+
undefined,
|
|
229
|
+
{ scope: 'lab' },
|
|
230
|
+
);
|
|
231
|
+
return (
|
|
232
|
+
<Split
|
|
233
|
+
className="lk-lab__panes"
|
|
234
|
+
sidebarClassName="lk-lab__sidebar"
|
|
235
|
+
contentClassName="lk-lab__main"
|
|
236
|
+
label="Lab sidebar"
|
|
237
|
+
defaultWidth={260}
|
|
238
|
+
width={sidebarWidth}
|
|
239
|
+
onWidthChange={(w) => {
|
|
240
|
+
setSidebarWidth(w);
|
|
241
|
+
surface?.invalidateRects();
|
|
242
|
+
}}
|
|
243
|
+
sidebar={<LabSidebarRegion contributions={contributions} />}
|
|
244
|
+
>
|
|
245
|
+
{children}
|
|
246
|
+
</Split>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The lab runtime: opens the store, provides it, and renders one trial
|
|
124
251
|
* per record in a grid. Each trial runs one of `instruments`. */
|
|
125
|
-
export function Lab({
|
|
252
|
+
export function Lab(props: LabProps) {
|
|
253
|
+
if (process.env.NODE_ENV !== 'production' && props.instruments.length === 0) {
|
|
254
|
+
throw new Error('[labkit] <Lab> requires a non-empty `instruments` array');
|
|
255
|
+
}
|
|
256
|
+
const { storageKey, storage } = props;
|
|
257
|
+
useWarnIgnoredChange('<Lab>', { storageKey, storage });
|
|
258
|
+
const opened = useOpenOnce<OpenedLab>(() =>
|
|
259
|
+
storageKey === undefined ? openUnstoredLab(props) : openStoredLab(props, storageKey, storage),
|
|
260
|
+
);
|
|
261
|
+
if (!opened) {
|
|
262
|
+
return props.fallback !== undefined ? (
|
|
263
|
+
<>{props.fallback}</>
|
|
264
|
+
) : (
|
|
265
|
+
<LabFallback title={props.title} mode={props.mode} pages={props.pages} path={props.path} />
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return <LabRuntime {...props} opened={opened} />;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function LabRuntime({
|
|
126
272
|
instruments,
|
|
127
|
-
defaultInstrument,
|
|
128
|
-
storage,
|
|
129
|
-
storageKey,
|
|
130
273
|
mode,
|
|
131
274
|
nebula,
|
|
132
275
|
title,
|
|
276
|
+
pages,
|
|
277
|
+
path,
|
|
133
278
|
footer,
|
|
134
279
|
chrome,
|
|
280
|
+
labChrome,
|
|
135
281
|
suppress,
|
|
282
|
+
addTrial,
|
|
136
283
|
tools,
|
|
137
284
|
configRules,
|
|
138
285
|
controls,
|
|
139
286
|
children,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
287
|
+
opened,
|
|
288
|
+
}: LabProps & { opened: OpenedLab }) {
|
|
289
|
+
const { store } = opened;
|
|
144
290
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
storage ?? noneAdapter,
|
|
151
|
-
storageKey ?? 'labkit',
|
|
152
|
-
mode ?? 'auto',
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
const store = storeRef.current;
|
|
291
|
+
// The store opened against the list <Lab> mounted with; a later list reaches
|
|
292
|
+
// it here, before paint.
|
|
293
|
+
useLayoutEffect(() => {
|
|
294
|
+
store.getState().setInstruments(instruments);
|
|
295
|
+
}, [instruments, store]);
|
|
156
296
|
|
|
157
297
|
const trials = useStore(store, (s) => s.trials);
|
|
158
298
|
const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
|
|
@@ -167,38 +307,64 @@ export function Lab({
|
|
|
167
307
|
if (panelHostsRef.current === null) panelHostsRef.current = createPanelHostRegistry();
|
|
168
308
|
|
|
169
309
|
// One shared drawing surface for the whole lab, anchored to the body — tile
|
|
170
|
-
// rects compose against it, and
|
|
310
|
+
// rects compose against it, and both buffers compose against the same rects.
|
|
171
311
|
// `Workspace` invalidates rects when the grid moves something a
|
|
172
312
|
// ResizeObserver cannot see. A host that already owns a surface keeps it: a
|
|
173
313
|
// lab embedded in a larger shared-surface app must not open a second GL
|
|
174
314
|
// tenancy, and mounts no buffer of its own.
|
|
175
315
|
const outerSurface = useSurfaceOptional();
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const
|
|
316
|
+
const outerOver = useSurfaceCanvas('over');
|
|
317
|
+
const outerUnder = useSurfaceCanvas('under');
|
|
318
|
+
const [ownOver, setOwnOver] = useState<HTMLCanvasElement | null>(null);
|
|
319
|
+
const [ownUnder, setOwnUnder] = useState<HTMLCanvasElement | null>(null);
|
|
320
|
+
const ownOverRef = useRef<HTMLCanvasElement | null>(null);
|
|
321
|
+
const ownUnderRef = useRef<HTMLCanvasElement | null>(null);
|
|
179
322
|
const bufferRef = useRef({ w: 0, h: 0 });
|
|
180
323
|
const surfaceRef = useRef<ReturnType<typeof useTiledSurface> | null>(null);
|
|
181
324
|
|
|
182
325
|
// 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
|
-
|
|
326
|
+
// only the one whose move triggered the measurement. Both buffers are sized
|
|
327
|
+
// together off one comparison: they are the same box, so a tenant of either
|
|
328
|
+
// is looking at the same rects, and one invalidateAll covers both.
|
|
329
|
+
const onFrame = useCallback((frame: SurfaceFrame) => {
|
|
330
|
+
const canvases = [ownUnderRef.current, ownOverRef.current].filter((c) => c !== null);
|
|
331
|
+
if (canvases.length === 0) return;
|
|
187
332
|
const w = Math.round(frame.size.width * frame.dpr);
|
|
188
333
|
const h = Math.round(frame.size.height * frame.dpr);
|
|
189
|
-
if (bufferRef.current.w
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
334
|
+
if (bufferRef.current.w !== w || bufferRef.current.h !== h) {
|
|
335
|
+
bufferRef.current = { w, h };
|
|
336
|
+
for (const c of canvases) {
|
|
337
|
+
c.width = w;
|
|
338
|
+
c.height = h;
|
|
339
|
+
c.style.width = `${frame.size.width}px`;
|
|
340
|
+
c.style.height = `${frame.size.height}px`;
|
|
341
|
+
}
|
|
342
|
+
surfaceRef.current?.invalidateAll();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// A same-size re-tile does not go through here: assigning `width` its own
|
|
346
|
+
// value resizes nothing, so it clears nothing. The tenants clear, through
|
|
347
|
+
// `registerClear`.
|
|
196
348
|
}, []);
|
|
197
349
|
|
|
198
350
|
const ownSurface = useTiledSurface({ onFrame });
|
|
199
351
|
surfaceRef.current = ownSurface;
|
|
352
|
+
const [labBody, setLabBody] = useState<HTMLDivElement | null>(null);
|
|
353
|
+
const attachOwnSurface = ownSurface.containerRef;
|
|
354
|
+
const labBodyRef = useCallback(
|
|
355
|
+
(el: HTMLDivElement | null) => {
|
|
356
|
+
setLabBody(el);
|
|
357
|
+
if (!outerSurface) attachOwnSurface(el);
|
|
358
|
+
},
|
|
359
|
+
[outerSurface, attachOwnSurface],
|
|
360
|
+
);
|
|
361
|
+
useLabFitWarning(labBody);
|
|
200
362
|
const surface = outerSurface ?? ownSurface;
|
|
201
|
-
const
|
|
363
|
+
const surfaceCanvases = useMemo(
|
|
364
|
+
() =>
|
|
365
|
+
outerSurface ? { over: outerOver, under: outerUnder } : { over: ownOver, under: ownUnder },
|
|
366
|
+
[outerSurface, outerOver, outerUnder, ownOver, ownUnder],
|
|
367
|
+
);
|
|
202
368
|
|
|
203
369
|
const workspacePanels = useMemo<PanelDescriptor[]>(
|
|
204
370
|
() =>
|
|
@@ -211,6 +377,19 @@ export function Lab({
|
|
|
211
377
|
);
|
|
212
378
|
const resolvedMode = useResolvedMode(modeValue);
|
|
213
379
|
|
|
380
|
+
// Tools and lab contributions share one id namespace, so they merge once
|
|
381
|
+
// here — before any region renders — and a collision throws rather than one
|
|
382
|
+
// of them silently losing.
|
|
383
|
+
// Declaring `annotations` on any instrument puts the drawing tools here, in
|
|
384
|
+
// the lab's rail: one tool is armed across every trial.
|
|
385
|
+
const annotates = instruments.some((i) => i.annotations != null);
|
|
386
|
+
const labChromeAll = useMemo(
|
|
387
|
+
() => labContributions([...(annotates ? ANNOTATION_TOOLS : []), ...(tools ?? [])], labChrome),
|
|
388
|
+
[annotates, tools, labChrome],
|
|
389
|
+
);
|
|
390
|
+
const hasFooterChrome = labChromeAll.some((c) => c.region === 'footer');
|
|
391
|
+
const hasSidebarChrome = labChromeAll.some((c) => c.region === 'sidebar');
|
|
392
|
+
|
|
214
393
|
useEffect(() => {
|
|
215
394
|
if (mode && mode !== store.getState().mode) {
|
|
216
395
|
store.getState().setMode(mode);
|
|
@@ -222,8 +401,6 @@ export function Lab({
|
|
|
222
401
|
const currentById = new Map(store.getState().trials.map((w) => [w.id, w]));
|
|
223
402
|
const merged = next.map((w) => currentById.get(w.id) ?? w);
|
|
224
403
|
store.setState({ trials: merged });
|
|
225
|
-
// Trigger persistence flush via a tracked action.
|
|
226
|
-
store.getState().setMode(store.getState().mode);
|
|
227
404
|
};
|
|
228
405
|
|
|
229
406
|
return {
|
|
@@ -255,7 +432,6 @@ export function Lab({
|
|
|
255
432
|
: w,
|
|
256
433
|
),
|
|
257
434
|
}));
|
|
258
|
-
store.getState().updateTrialView(id, record.view);
|
|
259
435
|
},
|
|
260
436
|
savedSnapshots,
|
|
261
437
|
saveSnapshot: (trialId, name) => {
|
|
@@ -284,66 +460,103 @@ export function Lab({
|
|
|
284
460
|
? ({ ['--wzl-backdrop' as string]: buildNebula(nebula) } as CSSProperties)
|
|
285
461
|
: undefined;
|
|
286
462
|
|
|
463
|
+
const workspace = (
|
|
464
|
+
<Workspace
|
|
465
|
+
panels={workspacePanels}
|
|
466
|
+
ids={trials.map((w) => w.id)}
|
|
467
|
+
resizable
|
|
468
|
+
reorderable
|
|
469
|
+
onReorder={(ids) => contextValue.reorderTrials(ids)}
|
|
470
|
+
layout={layout as TrialLayout}
|
|
471
|
+
onLayoutChange={(next) => store.getState().setLayout(next)}
|
|
472
|
+
>
|
|
473
|
+
{trials.map((w) => (
|
|
474
|
+
<Trial key={w.id} id={w.id} chrome={chrome} suppress={suppress} />
|
|
475
|
+
))}
|
|
476
|
+
</Workspace>
|
|
477
|
+
);
|
|
478
|
+
|
|
287
479
|
return (
|
|
288
480
|
<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
|
-
|
|
481
|
+
<PersistenceContext.Provider value={opened.records}>
|
|
482
|
+
<AnnotationPreloadContext.Provider value={opened.marks}>
|
|
483
|
+
<LabContext.Provider value={contextValue}>
|
|
484
|
+
<ThemeProvider
|
|
485
|
+
theme={interstellarTheme}
|
|
486
|
+
mode={resolvedMode}
|
|
487
|
+
className="lk-lab"
|
|
488
|
+
style={backdropStyle}
|
|
489
|
+
>
|
|
490
|
+
<LabShell
|
|
491
|
+
title={title ?? 'Labkit'}
|
|
492
|
+
mode={modeValue}
|
|
493
|
+
{...(pages ? { pages } : {})}
|
|
494
|
+
{...(path !== undefined ? { path } : {})}
|
|
495
|
+
footer={
|
|
496
|
+
hasFooterChrome ? (
|
|
497
|
+
<>
|
|
498
|
+
{footer}
|
|
499
|
+
<LabFooterRegion contributions={labChromeAll} />
|
|
500
|
+
</>
|
|
501
|
+
) : (
|
|
502
|
+
footer
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
header={
|
|
506
|
+
<>
|
|
507
|
+
<LabHeader {...(addTrial !== undefined ? { addTrial } : {})} />
|
|
508
|
+
{children}
|
|
509
|
+
<LabHeaderRegion contributions={labChromeAll} />
|
|
510
|
+
</>
|
|
511
|
+
}
|
|
512
|
+
>
|
|
513
|
+
<PanelHostContext.Provider value={panelHostsRef.current}>
|
|
514
|
+
<SurfaceContext.Provider value={surface}>
|
|
515
|
+
<SurfaceCanvasContext.Provider value={surfaceCanvases}>
|
|
516
|
+
<div className="lk-lab__body" ref={labBodyRef}>
|
|
517
|
+
{outerSurface ? null : (
|
|
518
|
+
// Two buffers stacked around the trials, both inert —
|
|
519
|
+
// each tile takes input from its own box. A tile's marks
|
|
520
|
+
// annotate the instrument's DOM from over it; an opaque
|
|
521
|
+
// renderer sits under it, so the pane can still hold a
|
|
522
|
+
// label. The under one is first so it paints first.
|
|
523
|
+
<>
|
|
524
|
+
<canvas
|
|
525
|
+
className="lk-lab__surface lk-lab__surface--under"
|
|
526
|
+
ref={(el) => {
|
|
527
|
+
ownUnderRef.current = el;
|
|
528
|
+
setOwnUnder(el);
|
|
529
|
+
}}
|
|
530
|
+
/>
|
|
531
|
+
<canvas
|
|
532
|
+
className="lk-lab__surface lk-lab__surface--over"
|
|
533
|
+
ref={(el) => {
|
|
534
|
+
ownOverRef.current = el;
|
|
535
|
+
setOwnOver(el);
|
|
536
|
+
}}
|
|
537
|
+
/>
|
|
538
|
+
</>
|
|
539
|
+
)}
|
|
540
|
+
{hasSidebarChrome ? (
|
|
541
|
+
<LabPanes contributions={labChromeAll}>
|
|
542
|
+
<LabPalette contributions={labChromeAll} />
|
|
543
|
+
{workspace}
|
|
544
|
+
</LabPanes>
|
|
545
|
+
) : (
|
|
546
|
+
<>
|
|
547
|
+
<LabPalette contributions={labChromeAll} />
|
|
548
|
+
{workspace}
|
|
549
|
+
</>
|
|
550
|
+
)}
|
|
551
|
+
</div>
|
|
552
|
+
</SurfaceCanvasContext.Provider>
|
|
553
|
+
</SurfaceContext.Provider>
|
|
554
|
+
</PanelHostContext.Provider>
|
|
555
|
+
</LabShell>
|
|
556
|
+
</ThemeProvider>
|
|
557
|
+
</LabContext.Provider>
|
|
558
|
+
</AnnotationPreloadContext.Provider>
|
|
559
|
+
</PersistenceContext.Provider>
|
|
347
560
|
</LabStoreContext.Provider>
|
|
348
561
|
);
|
|
349
562
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Host pages for LabFit.stories. Each is scoped by its own class, so a story
|
|
2
|
+
// that mounts no fixture leaves the page untouched.
|
|
3
|
+
|
|
4
|
+
// The mount every example documents: `html, body, #root { margin: 0; height: 100% }`.
|
|
5
|
+
html:has(> body > .lk-fit-host--reset),
|
|
6
|
+
body:has(> .lk-fit-host--reset),
|
|
7
|
+
.lk-fit-host--reset {
|
|
8
|
+
margin: 0;
|
|
9
|
+
height: 100%;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.lk-fit-heading {
|
|
13
|
+
height: 40px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.lk-fit-frame {
|
|
17
|
+
height: 560px;
|
|
18
|
+
}
|