@weasel-js/labkit 1.8.0 → 1.8.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 +24 -0
- package/dist/_dts/{CanvasStackContext-C0fzFsQg.d.ts → CanvasStackContext-B05ZM04l.d.ts} +3 -1
- package/dist/_dts/{Stage-Cvwxdnfp.d.ts → Stage-CIJlbjrE.d.ts} +5 -3
- package/dist/_dts/{index-DRaswz91.d.ts → index-8geQNQet.d.ts} +83 -11
- package/dist/_dts/{index-DEHRoTXK.d.ts → index-DRi3V5sN.d.ts} +5 -3
- package/dist/_dts/{useTrialState-CS4TdWPW.d.ts → useTrialState-Ar0AFpCe.d.ts} +94 -87
- package/dist/canvas/index.d.ts +4 -4
- package/dist/canvas/index.js +2 -2
- package/dist/chrome/index.d.ts +2 -2
- package/dist/chrome/index.js +7 -7
- package/dist/{chunk-YX75W6EK.js → chunk-4RCVWX4M.js} +8 -8
- package/dist/chunk-4RCVWX4M.js.map +1 -0
- package/dist/{chunk-WIG6XHJ7.js → chunk-6IIKLXBZ.js} +13 -4
- package/dist/chunk-6IIKLXBZ.js.map +1 -0
- package/dist/{chunk-F4ACIK7Z.js → chunk-6Q6ACJ3H.js} +3 -3
- package/dist/{chunk-F4ACIK7Z.js.map → chunk-6Q6ACJ3H.js.map} +1 -1
- package/dist/{chunk-PMLZP7RT.js → chunk-7ALJOVGP.js} +5 -9
- package/dist/chunk-7ALJOVGP.js.map +1 -0
- package/dist/{chunk-XE5HJOPL.js → chunk-7FMZT2WL.js} +3 -3
- package/dist/{chunk-XE5HJOPL.js.map → chunk-7FMZT2WL.js.map} +1 -1
- package/dist/{chunk-XVUKDSYV.js → chunk-AVOILDN2.js} +2 -2
- package/dist/{chunk-XVUKDSYV.js.map → chunk-AVOILDN2.js.map} +1 -1
- package/dist/{chunk-2N7UBSS2.js → chunk-J5MDJEOZ.js} +3 -3
- package/dist/{chunk-2N7UBSS2.js.map → chunk-J5MDJEOZ.js.map} +1 -1
- package/dist/{chunk-NVKBL7ZE.js → chunk-KHB3KRRQ.js} +7 -7
- package/dist/chunk-KHB3KRRQ.js.map +1 -0
- package/dist/{chunk-XXACAPVU.js → chunk-NHIO2O3O.js} +31 -30
- package/dist/chunk-NHIO2O3O.js.map +1 -0
- package/dist/{chunk-EESJUSYR.js → chunk-QMAY74TP.js} +56 -5
- package/dist/chunk-QMAY74TP.js.map +1 -0
- package/dist/{chunk-YDE6XATS.js → chunk-RN7T7OBO.js} +44 -22
- package/dist/chunk-RN7T7OBO.js.map +1 -0
- package/dist/{chunk-3L4MLLQB.js → chunk-RXO5UEGT.js} +4 -11
- package/dist/chunk-RXO5UEGT.js.map +1 -0
- package/dist/{chunk-MOTDMMMN.js → chunk-SQX35X5V.js} +3 -3
- package/dist/{chunk-MOTDMMMN.js.map → chunk-SQX35X5V.js.map} +1 -1
- package/dist/{chunk-J5HJJDLF.js → chunk-T5OEUNCB.js} +7 -2
- package/dist/chunk-T5OEUNCB.js.map +1 -0
- package/dist/{chunk-RL2LOLNI.js → chunk-UX66USTN.js} +2 -2
- package/dist/chunk-UX66USTN.js.map +1 -0
- package/dist/{chunk-WURZTJGQ.js → chunk-V4SMGEYY.js} +4858 -4806
- package/dist/chunk-V4SMGEYY.js.map +1 -0
- package/dist/{chunk-E23Q6A3Y.js → chunk-VCK5T6EP.js} +3 -3
- package/dist/{chunk-E23Q6A3Y.js.map → chunk-VCK5T6EP.js.map} +1 -1
- package/dist/{chunk-T6DLGEJB.js → chunk-XWR2P6MR.js} +23 -6
- package/dist/chunk-XWR2P6MR.js.map +1 -0
- package/dist/{chunk-7KAETQSS.js → chunk-YZA73U4A.js} +7 -7
- package/dist/chunk-YZA73U4A.js.map +1 -0
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.js +2 -2
- package/dist/controls/index.js +5 -5
- package/dist/dragdrop/index.d.ts +1 -1
- package/dist/dragdrop/index.js +2 -2
- package/dist/index.d.ts +157 -15
- package/dist/index.js +893 -258
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +1 -1
- package/dist/layers/index.js +3 -3
- package/dist/loupe/index.d.ts +175 -27
- package/dist/loupe/index.js +547 -141
- package/dist/loupe/index.js.map +1 -1
- package/dist/overview/index.js +5 -5
- package/dist/passthrough/weasel-ui.js +3 -3
- package/dist/primitives/index.d.ts +4 -19
- package/dist/primitives/index.js +7 -7
- package/dist/state/index.d.ts +5 -7
- package/dist/state/index.js +5 -5
- package/dist/styles.css +173 -38
- package/dist/surface/index.d.ts +20 -1
- package/dist/surface/index.js +2 -2
- package/dist/undo/index.d.ts +18 -7
- package/dist/undo/index.js +1 -1
- package/package.json +11 -11
- package/src/annotations/AnnotationOverlay.abandon.test.tsx +4 -0
- package/src/annotations/AnnotationOverlay.tsx +10 -1
- package/src/annotations/Annotations.less +2 -0
- package/src/annotations/Annotations.overlay.test.tsx +29 -0
- package/src/canvas/CanvasStack.test.tsx +35 -0
- package/src/canvas/CanvasStack.tsx +12 -1
- package/src/canvas/useLayerScheduler.ts +14 -0
- package/src/chrome/builtins.test.ts +14 -0
- package/src/chrome/builtins.tsx +17 -2
- package/src/chrome/regions/StatusRegion.tsx +20 -14
- package/src/chrome/regions/regions.test.tsx +28 -0
- package/src/chrome/types.ts +8 -0
- package/src/clock/clock.browser.test.less +5 -0
- package/src/clock/clock.browser.test.tsx +77 -0
- package/src/clock/clock.integration.test.tsx +95 -0
- package/src/clock/clockRegistry.test.ts +33 -0
- package/src/clock/clockRegistry.ts +46 -0
- package/src/clock/hooks.ts +44 -0
- package/src/clock/index.ts +2 -0
- package/src/clock/trialClock.test.ts +265 -0
- package/src/clock/trialClock.ts +245 -0
- package/src/clock/useClockLoop.ts +45 -0
- package/src/config/resolve.test.ts +12 -0
- package/src/config/resolve.ts +5 -0
- package/src/controls/ControlMatrix.tsx +56 -54
- package/src/index.ts +3 -2
- package/src/instrument/types.ts +21 -1
- package/src/lab/Lab.chrome.test.tsx +8 -1
- package/src/lab/Lab.presentation.test.tsx +113 -0
- package/src/lab/Lab.surface.test.tsx +20 -9
- package/src/lab/Lab.tsx +132 -257
- package/src/lab/LabLightbox.browser.test.less +17 -0
- package/src/lab/LabLightbox.browser.test.tsx +216 -0
- package/src/lab/LabShell.less +10 -3
- package/src/lab/LabSurface.tsx +129 -0
- package/src/lab/Workspace.stories.tsx +18 -0
- package/src/lab/Workspace.surface.test.tsx +4 -0
- package/src/lab/Workspace.test.tsx +37 -1
- package/src/lab/Workspace.tsx +34 -6
- package/src/lab/index.ts +2 -0
- package/src/lab/nebula.ts +30 -0
- package/src/lab/openLab.test.ts +81 -0
- package/src/lab/openLab.ts +119 -0
- package/src/lab/presentation.browser.test.less +5 -0
- package/src/lab/presentation.browser.test.tsx +119 -0
- package/src/lab/presentation.less +64 -0
- package/src/lab/presentation.tsx +47 -0
- package/src/lab/useFocusPick.ts +43 -0
- package/src/lightbox/Lightbox.browser.test.less +14 -0
- package/src/lightbox/Lightbox.browser.test.tsx +113 -0
- package/src/lightbox/Lightbox.less +128 -0
- package/src/lightbox/Lightbox.test.tsx +195 -0
- package/src/lightbox/Lightbox.tsx +205 -0
- package/src/lightbox/LightboxContext.ts +38 -0
- package/src/lightbox/LightboxLayers.tsx +40 -0
- package/src/lightbox/index.ts +8 -0
- package/src/lightbox/modal.ts +94 -0
- package/src/loupe/AGENTS.md +40 -2
- package/src/loupe/CanvasLoupe.tsx +5 -4
- package/src/loupe/DomLoupe.tsx +7 -5
- package/src/loupe/Loupe.less +15 -0
- package/src/loupe/LoupeBubble.tsx +35 -13
- package/src/loupe/SourceLoupe.browser.test.tsx +204 -0
- package/src/loupe/SourceLoupe.tsx +60 -0
- package/src/loupe/TrialLoupe.browser.test.tsx +342 -0
- package/src/loupe/TrialLoupe.tsx +126 -28
- package/src/loupe/canvasLens.ts +20 -17
- package/src/loupe/index.ts +26 -2
- package/src/loupe/sourceLens.test.ts +65 -0
- package/src/loupe/sourceLens.ts +97 -0
- package/src/loupe/types.test.ts +6 -0
- package/src/loupe/types.ts +109 -7
- package/src/loupe/useLoupe.test.tsx +188 -3
- package/src/loupe/useLoupe.ts +106 -11
- package/src/primitives/index.ts +2 -2
- package/src/specimen/Specimen.tsx +0 -7
- package/src/state/helpers.test.ts +4 -11
- package/src/state/helpers.ts +2 -8
- package/src/state/index.ts +0 -2
- package/src/state/openLabStore.test.ts +10 -8
- package/src/state/openLabStore.ts +1 -1
- package/src/state/store.test.ts +2 -2
- package/src/state/store.ts +15 -16
- package/src/state/types.ts +9 -9
- package/src/styles.less +2 -1
- package/src/surface/AGENTS.md +13 -0
- package/src/surface/index.ts +1 -0
- package/src/surface/useSurfaceTile.test.tsx +4 -0
- package/src/surface/useSurfaceTile.ts +17 -1
- package/src/surface/useTiledSurface.test.tsx +65 -0
- package/src/surface/useTiledSurface.ts +68 -1
- package/src/trial/Trial.stories.tsx +0 -1
- package/src/trial/Trial.test.tsx +40 -3
- package/src/trial/Trial.tsx +87 -43
- package/src/trial/TrialChrome.tsx +20 -2
- package/src/trial/trialOps.test.ts +1 -2
- package/src/trial/trialOps.ts +2 -3
- package/src/undo/stateOp.ts +21 -0
- package/src/undo/undoStack.ts +18 -6
- package/dist/chunk-3L4MLLQB.js.map +0 -1
- package/dist/chunk-7KAETQSS.js.map +0 -1
- package/dist/chunk-EESJUSYR.js.map +0 -1
- package/dist/chunk-J5HJJDLF.js.map +0 -1
- package/dist/chunk-NVKBL7ZE.js.map +0 -1
- package/dist/chunk-PMLZP7RT.js.map +0 -1
- package/dist/chunk-RL2LOLNI.js.map +0 -1
- package/dist/chunk-T6DLGEJB.js.map +0 -1
- package/dist/chunk-WIG6XHJ7.js.map +0 -1
- package/dist/chunk-WURZTJGQ.js.map +0 -1
- package/dist/chunk-XXACAPVU.js.map +0 -1
- package/dist/chunk-YDE6XATS.js.map +0 -1
- package/dist/chunk-YX75W6EK.js.map +0 -1
- package/src/primitives/StatusBar.less +0 -34
- package/src/primitives/StatusBar.stories.tsx +0 -24
- package/src/primitives/StatusBar.test.tsx +0 -25
- package/src/primitives/StatusBar.tsx +0 -28
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* `SurfaceContext` in production — so `useSurfaceOptional()` always answered
|
|
4
4
|
* null and a tile registered from inside a trial reached nothing.
|
|
5
5
|
*
|
|
6
|
-
* The surface
|
|
7
|
-
*
|
|
8
|
-
* selector, so an extra element between them silently drops that
|
|
6
|
+
* The surface's layers sit in `.lk-lab__body` beside the workspace rather than
|
|
7
|
+
* wrapped around it: `.lk-lab__body > .lk-workspace { min-width: 0 }` is a
|
|
8
|
+
* direct-child selector, so an extra element between them silently drops that
|
|
9
|
+
* rule.
|
|
9
10
|
*/
|
|
10
11
|
import { act } from '@testing-library/react';
|
|
11
12
|
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
@@ -84,19 +85,29 @@ describe('<Lab> surface provider', () => {
|
|
|
84
85
|
expect(typeof (seen as unknown as SurfaceHandle).registerTile).toBe('function');
|
|
85
86
|
});
|
|
86
87
|
|
|
87
|
-
it('stacks an under and an over buffer
|
|
88
|
+
it('stacks an under and an over buffer, each in its own layer of the lab body', async () => {
|
|
88
89
|
const { container } = await renderSettled(
|
|
89
90
|
<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />,
|
|
90
91
|
);
|
|
91
|
-
const
|
|
92
|
-
expect(canvases).toHaveLength(2);
|
|
92
|
+
const layers = container.querySelectorAll('.lk-lab__body > .lk-lab__layer');
|
|
93
93
|
// Source order, because the z-index that separates them is in a stylesheet
|
|
94
94
|
// jsdom does not resolve — this is a proxy for the stacking, not proof of
|
|
95
95
|
// it. The real check is a browser.
|
|
96
|
-
expect([...
|
|
97
|
-
'lk-
|
|
98
|
-
'lk-
|
|
96
|
+
expect([...layers].map((l) => l.className)).toEqual([
|
|
97
|
+
'lk-lab__layer lk-lab__layer--under',
|
|
98
|
+
'lk-lab__layer lk-lab__layer--over',
|
|
99
99
|
]);
|
|
100
|
+
for (const layer of layers)
|
|
101
|
+
expect(layer.querySelector('canvas.lk-lab__surface')).not.toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('measures tile rects against the over layer', async () => {
|
|
105
|
+
const { container } = await renderSettled(
|
|
106
|
+
<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />,
|
|
107
|
+
);
|
|
108
|
+
expect((seen as SurfaceHandle).getContainer()).toBe(
|
|
109
|
+
container.querySelector('.lk-lab__layer--over'),
|
|
110
|
+
);
|
|
100
111
|
});
|
|
101
112
|
|
|
102
113
|
it('hands a tenant whichever of the two it asks for', async () => {
|
package/src/lab/Lab.tsx
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { type ColorList,
|
|
1
|
+
import { type ColorList, colorCount, type Theme } from '@weasel-js/theme';
|
|
2
2
|
import { ThemeProvider, useResolvedColorMode } from '@weasel-js/theme/react';
|
|
3
3
|
import {
|
|
4
4
|
type CSSProperties,
|
|
5
5
|
type ReactNode,
|
|
6
|
-
useCallback,
|
|
7
6
|
useEffect,
|
|
8
|
-
useInsertionEffect,
|
|
9
7
|
useLayoutEffect,
|
|
10
8
|
useMemo,
|
|
11
9
|
useRef,
|
|
@@ -24,21 +22,17 @@ import {
|
|
|
24
22
|
} from '../chrome/LabChrome';
|
|
25
23
|
import type { LabContribution } from '../chrome/labTypes';
|
|
26
24
|
import type { TrialContribution } from '../chrome/types';
|
|
25
|
+
import { ClockRegistryContext, createClockRegistry } from '../clock/clockRegistry';
|
|
26
|
+
import { useClockLoop } from '../clock/useClockLoop';
|
|
27
27
|
import type { ConfigRule, ControlRenderer } from '../config/types';
|
|
28
28
|
import type { InstrumentList } from '../instrument/types';
|
|
29
29
|
import { Split } from '../primitives/Split';
|
|
30
|
-
import { defaultStorage, noneAdapter } from '../state/adapters';
|
|
31
30
|
import { LabStoreContext } from '../state/context';
|
|
32
|
-
import { type OpenedLabStore, openLabStore } from '../state/openLabStore';
|
|
33
31
|
import { PersistenceContext } from '../state/Persistence';
|
|
34
|
-
import { createRecordCache } from '../state/records';
|
|
35
|
-
import { createLabStore, type LabStore } from '../state/store';
|
|
36
32
|
import type { LabDensity, LabMode, StorageAdapter, TrialRecord } from '../state/types';
|
|
37
33
|
import { useOpenOnce, useWarnIgnoredChange } from '../state/useOpenOnce';
|
|
38
34
|
import { usePersistedState } from '../state/usePersistedState';
|
|
39
|
-
import {
|
|
40
|
-
import { useSurfaceCanvas, useSurfaceOptional } from '../surface/useSurfaceTile';
|
|
41
|
-
import { type SurfaceFrame, useTiledSurface } from '../surface/useTiledSurface';
|
|
35
|
+
import { useSurfaceOptional } from '../surface/useSurfaceTile';
|
|
42
36
|
import { interstellarTheme } from '../theme/interstellar';
|
|
43
37
|
import type { TrialTool } from '../tools/types';
|
|
44
38
|
import { Trial } from '../trial/Trial';
|
|
@@ -55,9 +49,20 @@ import { LabContext, type LabContextValue } from './LabContext';
|
|
|
55
49
|
import { LabHeader, LabThemeSwitcher } from './LabHeader';
|
|
56
50
|
import { LabPalette } from './LabPalette';
|
|
57
51
|
import { LabShell } from './LabShell';
|
|
52
|
+
import { LabSurface } from './LabSurface';
|
|
58
53
|
import type { LabPage } from './LabSwitcher';
|
|
59
54
|
import { LabZoom } from './LabZoom';
|
|
55
|
+
import { buildNebula } from './nebula';
|
|
56
|
+
import {
|
|
57
|
+
type OpenedLab,
|
|
58
|
+
openStoredLab,
|
|
59
|
+
openUnstoredLab,
|
|
60
|
+
type PresentationSeed,
|
|
61
|
+
presentStorageKey,
|
|
62
|
+
} from './openLab';
|
|
60
63
|
import { createPanelHostRegistry, PanelHostContext } from './panelHost';
|
|
64
|
+
import { hasPresentParam, PresentationContext } from './presentation';
|
|
65
|
+
import { useFocusPick } from './useFocusPick';
|
|
61
66
|
import { type PanelDescriptor, type TrialLayout, Workspace } from './Workspace';
|
|
62
67
|
|
|
63
68
|
interface LabBaseProps {
|
|
@@ -104,6 +109,10 @@ interface LabBaseProps {
|
|
|
104
109
|
labChrome?: readonly LabContribution[];
|
|
105
110
|
/** Built-in contribution ids to drop. Throws on an id that is not there. */
|
|
106
111
|
suppress?: readonly string[];
|
|
112
|
+
/** Whether a double-click on a trial also opens it in its lightbox, beside
|
|
113
|
+
* the title bar's expand toggle every trial has. Off by default; a
|
|
114
|
+
* function decides per trial, by its record. */
|
|
115
|
+
expandOnDoubleClick?: boolean | ((trial: TrialRecord) => boolean);
|
|
107
116
|
/** Offer the header's add-trial control. Default `true`; a lab that opens
|
|
108
117
|
* trials some other way passes `false`. */
|
|
109
118
|
addTrial?: boolean;
|
|
@@ -121,6 +130,15 @@ interface LabBaseProps {
|
|
|
121
130
|
* Keys are config paths (checked first) or leaf kinds. Memoize or hoist. */
|
|
122
131
|
controls?: Record<string, ControlRenderer>;
|
|
123
132
|
children?: ReactNode;
|
|
133
|
+
/** Start presenting: one trial, opened on `seed`, without the lab's chrome or
|
|
134
|
+
* its own. `?present` in the page's URL does the same. Read once, at mount.
|
|
135
|
+
* A stored lab presents from its own records, under `storageKey` +
|
|
136
|
+
* `':present'`, so an embed never shows or changes the full lab's trials. */
|
|
137
|
+
present?: boolean;
|
|
138
|
+
/** The trial a lab that starts presenting opens on. A store that already
|
|
139
|
+
* holds a trial opened on this same seed keeps it; a changed seed replaces
|
|
140
|
+
* it. Ignored unless the lab starts presenting. */
|
|
141
|
+
seed?: PresentationSeed;
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
/** Props for `<Lab>`. With a `storageKey` the lab persists — to IndexedDB
|
|
@@ -132,61 +150,6 @@ export type LabProps = LabBaseProps &
|
|
|
132
150
|
| { storageKey: string; storage?: StorageAdapter }
|
|
133
151
|
);
|
|
134
152
|
|
|
135
|
-
interface OpenedLab extends OpenedLabStore {
|
|
136
|
-
marks: ReadonlyMap<string, unknown>;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function seedDefaultTrial(
|
|
140
|
-
store: LabStore,
|
|
141
|
-
instruments: InstrumentList,
|
|
142
|
-
defaultInstrument: string,
|
|
143
|
-
): void {
|
|
144
|
-
if (store.getState().trials.length > 0) return;
|
|
145
|
-
const record = addTrialOp([], instruments, defaultInstrument)[0];
|
|
146
|
-
if (!record) return;
|
|
147
|
-
const { undoStack: _undoStack, ...rest } = record;
|
|
148
|
-
store.getState().addTrial(rest);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** A lab with nothing to load renders at once. Its records hold
|
|
152
|
-
* `usePersistedState` values for the session and write nothing. */
|
|
153
|
-
function openUnstoredLab({ instruments, defaultInstrument, mode }: LabProps): OpenedLab {
|
|
154
|
-
const store = createLabStore({ initialMode: mode ?? 'auto', instruments });
|
|
155
|
-
seedDefaultTrial(store, instruments, defaultInstrument);
|
|
156
|
-
const records = createRecordCache({ storage: noneAdapter, prefix: '', writable: false });
|
|
157
|
-
return { store, records, close: () => records.close(), marks: new Map() };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function openStoredLab(
|
|
161
|
-
{ instruments, defaultInstrument, mode }: LabProps,
|
|
162
|
-
storageKey: string,
|
|
163
|
-
storage: StorageAdapter | undefined,
|
|
164
|
-
): Promise<OpenedLab> {
|
|
165
|
-
// The store reads the instruments' serializers and defaults while it is being
|
|
166
|
-
// built, so they go in with it rather than being pushed onto it afterwards.
|
|
167
|
-
const opened = await openLabStore({
|
|
168
|
-
storageKey,
|
|
169
|
-
storage: storage ?? (await defaultStorage()),
|
|
170
|
-
initialMode: mode ?? 'auto',
|
|
171
|
-
instruments,
|
|
172
|
-
});
|
|
173
|
-
seedDefaultTrial(opened.store, instruments, defaultInstrument);
|
|
174
|
-
const marks = new Map<string, unknown>();
|
|
175
|
-
await Promise.all(
|
|
176
|
-
opened.store.getState().trials.map(async (trial) => {
|
|
177
|
-
const kept = instruments.find((i) => i.name === trial.instrumentName)?.annotations?.storage;
|
|
178
|
-
if (!kept) return;
|
|
179
|
-
try {
|
|
180
|
-
marks.set(trial.id, await kept.load());
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.warn(`[labkit] could not load the marks of trial "${trial.id}"`, error);
|
|
183
|
-
marks.set(trial.id, null);
|
|
184
|
-
}
|
|
185
|
-
}),
|
|
186
|
-
);
|
|
187
|
-
return { ...opened, marks };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
153
|
function LabFallback({
|
|
191
154
|
title,
|
|
192
155
|
mode,
|
|
@@ -214,35 +177,6 @@ function LabFallback({
|
|
|
214
177
|
);
|
|
215
178
|
}
|
|
216
179
|
|
|
217
|
-
// Fixed spread of nebula blob positions / sizes / fall-off stops. Colors
|
|
218
|
-
// supplied via the `nebula` prop are slotted into these slots in order;
|
|
219
|
-
// callers passing more than 5 wrap around (intentional — keeps the look
|
|
220
|
-
// readable and bounded).
|
|
221
|
-
const NEBULA_SLOTS = [
|
|
222
|
-
{ cx: '18%', cy: '30%', sx: '60%', sy: '80%', stop: '70%' },
|
|
223
|
-
{ cx: '78%', cy: '70%', sx: '55%', sy: '90%', stop: '65%' },
|
|
224
|
-
{ cx: '50%', cy: '50%', sx: '50%', sy: '70%', stop: '75%' },
|
|
225
|
-
{ cx: '12%', cy: '82%', sx: '50%', sy: '70%', stop: '70%' },
|
|
226
|
-
{ cx: '85%', cy: '18%', sx: '55%', sy: '80%', stop: '70%' },
|
|
227
|
-
] as const;
|
|
228
|
-
|
|
229
|
-
function buildNebula(
|
|
230
|
-
list: ColorList,
|
|
231
|
-
theme: Theme,
|
|
232
|
-
mode: 'light' | 'dark',
|
|
233
|
-
density: string,
|
|
234
|
-
): string {
|
|
235
|
-
const ctx = { theme, resolved: resolveTheme(theme, { mode, density }) };
|
|
236
|
-
const n = colorCount(list, ctx) ?? NEBULA_SLOTS.length;
|
|
237
|
-
const colors = Array.from({ length: n }, (_, i) => colorAt(list, i, ctx));
|
|
238
|
-
const blobs = colors.map((c, i) => {
|
|
239
|
-
const p = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
|
|
240
|
-
return `radial-gradient(ellipse ${p.sx} ${p.sy} at ${p.cx} ${p.cy}, color-mix(in srgb, ${c} 22%, transparent), transparent ${p.stop})`;
|
|
241
|
-
});
|
|
242
|
-
blobs.push('radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%)');
|
|
243
|
-
return blobs.join(', ');
|
|
244
|
-
}
|
|
245
|
-
|
|
246
180
|
/** The strip holding the sidebar beside the tool rail and workspace. Its own component
|
|
247
181
|
* because the width is a persisted value, and `LabRuntime` renders the
|
|
248
182
|
* persistence provider it reads from. */
|
|
@@ -310,13 +244,22 @@ export function Lab(props: LabProps) {
|
|
|
310
244
|
if (process.env.NODE_ENV !== 'production' && props.instruments.length === 0) {
|
|
311
245
|
throw new Error('[labkit] <Lab> requires a non-empty `instruments` array');
|
|
312
246
|
}
|
|
313
|
-
const { storageKey, storage } = props;
|
|
314
|
-
useWarnIgnoredChange('<Lab>', { storageKey, storage });
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
247
|
+
const { storageKey, storage, present } = props;
|
|
248
|
+
useWarnIgnoredChange('<Lab>', { storageKey, storage, present });
|
|
249
|
+
const [startsPresenting] = useState(() => present === true || hasPresentParam());
|
|
250
|
+
if (process.env.NODE_ENV !== 'production' && props.seed && !startsPresenting) {
|
|
251
|
+
warnSeedIgnored();
|
|
252
|
+
}
|
|
253
|
+
const opened = useOpenOnce<OpenedLab>(() => {
|
|
254
|
+
const seed = startsPresenting ? (props.seed ?? {}) : undefined;
|
|
255
|
+
if (storageKey === undefined) return openUnstoredLab(props, seed);
|
|
256
|
+
const key = startsPresenting ? presentStorageKey(storageKey) : storageKey;
|
|
257
|
+
return openStoredLab(props, key, storage, seed);
|
|
258
|
+
});
|
|
318
259
|
if (!opened) {
|
|
319
260
|
if (props.fallback !== undefined) return props.fallback;
|
|
261
|
+
// An embed shows nothing until its trial is ready, never the lab's shell.
|
|
262
|
+
if (startsPresenting) return null;
|
|
320
263
|
return (
|
|
321
264
|
<LabFallback
|
|
322
265
|
title={props.title}
|
|
@@ -328,7 +271,14 @@ export function Lab(props: LabProps) {
|
|
|
328
271
|
/>
|
|
329
272
|
);
|
|
330
273
|
}
|
|
331
|
-
return <LabRuntime {...props} opened={opened} />;
|
|
274
|
+
return <LabRuntime {...props} opened={opened} startsPresenting={startsPresenting} />;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let seedWarned = false;
|
|
278
|
+
function warnSeedIgnored(): void {
|
|
279
|
+
if (seedWarned) return;
|
|
280
|
+
seedWarned = true;
|
|
281
|
+
console.warn('[labkit] <Lab> reads `seed` only when it starts presenting; it is ignored here');
|
|
332
282
|
}
|
|
333
283
|
|
|
334
284
|
function LabRuntime({
|
|
@@ -344,6 +294,7 @@ function LabRuntime({
|
|
|
344
294
|
chrome,
|
|
345
295
|
labChrome,
|
|
346
296
|
suppress,
|
|
297
|
+
expandOnDoubleClick = false,
|
|
347
298
|
addTrial,
|
|
348
299
|
zoom = true,
|
|
349
300
|
tools,
|
|
@@ -351,7 +302,8 @@ function LabRuntime({
|
|
|
351
302
|
controls,
|
|
352
303
|
children,
|
|
353
304
|
opened,
|
|
354
|
-
|
|
305
|
+
startsPresenting,
|
|
306
|
+
}: LabProps & { opened: OpenedLab; startsPresenting: boolean }) {
|
|
355
307
|
const { store } = opened;
|
|
356
308
|
|
|
357
309
|
// The store opened against the list <Lab> mounted with; a later list reaches
|
|
@@ -362,10 +314,26 @@ function LabRuntime({
|
|
|
362
314
|
|
|
363
315
|
const trials = useStore(store, (s) => s.trials);
|
|
364
316
|
const [cameras] = useState(createCameraRegistry);
|
|
317
|
+
const [clocks] = useState(createClockRegistry);
|
|
318
|
+
useClockLoop(clocks);
|
|
365
319
|
const [focusPick, setFocusPick] = useState<string | null>(null);
|
|
366
320
|
const focusedTrialId = trials.some((t) => t.id === focusPick)
|
|
367
321
|
? focusPick
|
|
368
322
|
: (trials[0]?.id ?? null);
|
|
323
|
+
// `mount` when the lab started presenting, `enter` when asked to since.
|
|
324
|
+
const [presentedBy, setPresentedBy] = useState<'mount' | 'enter' | null>(
|
|
325
|
+
startsPresenting ? 'mount' : null,
|
|
326
|
+
);
|
|
327
|
+
const presenting = presentedBy !== null;
|
|
328
|
+
const presentation = useMemo(
|
|
329
|
+
() => ({
|
|
330
|
+
active: presenting,
|
|
331
|
+
enter: () => setPresentedBy((by) => by ?? 'enter'),
|
|
332
|
+
exit: () => setPresentedBy(null),
|
|
333
|
+
trialId: presenting ? focusedTrialId : null,
|
|
334
|
+
}),
|
|
335
|
+
[presenting, focusedTrialId],
|
|
336
|
+
);
|
|
369
337
|
const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
|
|
370
338
|
const modeValue = useStore(store, (s) => s.mode);
|
|
371
339
|
const layout = useStore(store, (s) => s.layout);
|
|
@@ -377,102 +345,22 @@ function LabRuntime({
|
|
|
377
345
|
const panelHostsRef = useRef<ReturnType<typeof createPanelHostRegistry> | null>(null);
|
|
378
346
|
if (panelHostsRef.current === null) panelHostsRef.current = createPanelHostRegistry();
|
|
379
347
|
|
|
380
|
-
// One shared drawing surface for the whole lab, anchored to the body — tile
|
|
381
|
-
// rects compose against it, and both buffers compose against the same rects.
|
|
382
|
-
// `Workspace` invalidates rects when the grid moves something a
|
|
383
|
-
// ResizeObserver cannot see. A host that already owns a surface keeps it: a
|
|
384
|
-
// lab embedded in a larger shared-surface app must not open a second GL
|
|
385
|
-
// tenancy, and mounts no buffer of its own.
|
|
386
|
-
const outerSurface = useSurfaceOptional();
|
|
387
|
-
const outerOver = useSurfaceCanvas('over');
|
|
388
|
-
const outerUnder = useSurfaceCanvas('under');
|
|
389
|
-
const [ownOver, setOwnOver] = useState<HTMLCanvasElement | null>(null);
|
|
390
|
-
const [ownUnder, setOwnUnder] = useState<HTMLCanvasElement | null>(null);
|
|
391
|
-
const ownOverRef = useRef<HTMLCanvasElement | null>(null);
|
|
392
|
-
const ownUnderRef = useRef<HTMLCanvasElement | null>(null);
|
|
393
|
-
const bufferRef = useRef({ w: 0, h: 0 });
|
|
394
|
-
const surfaceRef = useRef<ReturnType<typeof useTiledSurface> | null>(null);
|
|
395
|
-
|
|
396
|
-
// Sizing the buffer clears all of it, so every tile has to repaint — not
|
|
397
|
-
// only the one whose move triggered the measurement. Both buffers are sized
|
|
398
|
-
// together off one comparison: they are the same box, so a tenant of either
|
|
399
|
-
// is looking at the same rects, and one invalidateAll covers both.
|
|
400
|
-
const onFrame = useCallback((frame: SurfaceFrame) => {
|
|
401
|
-
const canvases = [ownUnderRef.current, ownOverRef.current].filter((c) => c !== null);
|
|
402
|
-
if (canvases.length === 0) return;
|
|
403
|
-
const w = Math.round(frame.size.width * frame.dpr);
|
|
404
|
-
const h = Math.round(frame.size.height * frame.dpr);
|
|
405
|
-
if (bufferRef.current.w !== w || bufferRef.current.h !== h) {
|
|
406
|
-
bufferRef.current = { w, h };
|
|
407
|
-
for (const c of canvases) {
|
|
408
|
-
c.width = w;
|
|
409
|
-
c.height = h;
|
|
410
|
-
c.style.width = `${frame.size.width}px`;
|
|
411
|
-
c.style.height = `${frame.size.height}px`;
|
|
412
|
-
}
|
|
413
|
-
surfaceRef.current?.invalidateAll();
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
// A same-size re-tile does not go through here: assigning `width` its own
|
|
417
|
-
// value resizes nothing, so it clears nothing. The tenants clear, through
|
|
418
|
-
// `registerClear`.
|
|
419
|
-
}, []);
|
|
420
|
-
|
|
421
|
-
const ownSurface = useTiledSurface({ onFrame });
|
|
422
|
-
// Not `useLatest`: `onFrame` reads it, and is needed before the surface exists.
|
|
423
|
-
useInsertionEffect(() => {
|
|
424
|
-
surfaceRef.current = ownSurface;
|
|
425
|
-
}, [ownSurface]);
|
|
426
348
|
const [labBody, setLabBody] = useState<HTMLDivElement | null>(null);
|
|
427
|
-
const attachOwnSurface = ownSurface.containerRef;
|
|
428
|
-
const labBodyRef = useCallback(
|
|
429
|
-
(el: HTMLDivElement | null) => {
|
|
430
|
-
setLabBody(el);
|
|
431
|
-
if (!outerSurface) attachOwnSurface(el);
|
|
432
|
-
},
|
|
433
|
-
[outerSurface, attachOwnSurface],
|
|
434
|
-
);
|
|
435
349
|
useLabFitWarning(labBody);
|
|
436
350
|
|
|
351
|
+
useFocusPick(labBody, store, setFocusPick);
|
|
352
|
+
|
|
353
|
+
// Only a lab asked to present has a workspace to go back to.
|
|
437
354
|
useEffect(() => {
|
|
438
|
-
if (!labBody) return;
|
|
439
|
-
const pick = (node: unknown): void => {
|
|
440
|
-
const owned = new Set(store.getState().trials.map((t) => t.id));
|
|
441
|
-
const nearest = (from: Element | null | undefined) =>
|
|
442
|
-
from?.closest<HTMLElement>('.lk-trial[data-trial-id]') ?? null;
|
|
443
|
-
let trial =
|
|
444
|
-
typeof (node as Element | null)?.closest === 'function' ? nearest(node as Element) : null;
|
|
445
|
-
// A trial can hold a lab of its own, whose trials this lab does not own.
|
|
446
|
-
while (trial && !owned.has(trial.dataset.trialId ?? '')) trial = nearest(trial.parentElement);
|
|
447
|
-
if (trial?.dataset.trialId) setFocusPick(trial.dataset.trialId);
|
|
448
|
-
};
|
|
449
|
-
const onInput = (event: Event): void => pick(event.target);
|
|
450
|
-
// Focus moving into a frame fires nothing in this document: the window
|
|
451
|
-
// blurs, and the frame is the active element once it has.
|
|
452
|
-
let pending: ReturnType<typeof setTimeout> | undefined;
|
|
355
|
+
if (presentedBy !== 'enter' || !labBody) return;
|
|
453
356
|
const doc = labBody.ownerDocument;
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
pending = setTimeout(() => pick(doc.activeElement), 0);
|
|
458
|
-
};
|
|
459
|
-
labBody.addEventListener('pointerdown', onInput, true);
|
|
460
|
-
labBody.addEventListener('focusin', onInput, true);
|
|
461
|
-
win?.addEventListener('blur', onBlur);
|
|
462
|
-
return () => {
|
|
463
|
-
clearTimeout(pending);
|
|
464
|
-
labBody.removeEventListener('pointerdown', onInput, true);
|
|
465
|
-
labBody.removeEventListener('focusin', onInput, true);
|
|
466
|
-
win?.removeEventListener('blur', onBlur);
|
|
357
|
+
const onKeyDown = (event: KeyboardEvent): void => {
|
|
358
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return;
|
|
359
|
+
setPresentedBy(null);
|
|
467
360
|
};
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
() =>
|
|
472
|
-
outerSurface ? { over: outerOver, under: outerUnder } : { over: ownOver, under: ownUnder },
|
|
473
|
-
[outerSurface, outerOver, outerUnder, ownOver, ownUnder],
|
|
474
|
-
);
|
|
475
|
-
|
|
361
|
+
doc.addEventListener('keydown', onKeyDown);
|
|
362
|
+
return () => doc.removeEventListener('keydown', onKeyDown);
|
|
363
|
+
}, [presentedBy, labBody]);
|
|
476
364
|
const workspacePanels = useMemo<PanelDescriptor[]>(
|
|
477
365
|
() =>
|
|
478
366
|
Object.entries(undockedPanels).map(([key, panel]) => ({
|
|
@@ -558,6 +446,7 @@ function LabRuntime({
|
|
|
558
446
|
: w,
|
|
559
447
|
),
|
|
560
448
|
}));
|
|
449
|
+
clocks.get(id)?.reset();
|
|
561
450
|
},
|
|
562
451
|
savedSnapshots,
|
|
563
452
|
saveSnapshot: (trialId, name) => {
|
|
@@ -578,6 +467,7 @@ function LabRuntime({
|
|
|
578
467
|
controls,
|
|
579
468
|
};
|
|
580
469
|
}, [
|
|
470
|
+
clocks,
|
|
581
471
|
instruments,
|
|
582
472
|
trials,
|
|
583
473
|
focusedTrialId,
|
|
@@ -611,6 +501,14 @@ function LabRuntime({
|
|
|
611
501
|
onReorder={(ids) => contextValue.reorderTrials(ids)}
|
|
612
502
|
layout={layout as TrialLayout}
|
|
613
503
|
onLayoutChange={(next) => store.getState().setLayout(next)}
|
|
504
|
+
expandOnDoubleClick={
|
|
505
|
+
typeof expandOnDoubleClick === 'function'
|
|
506
|
+
? (id) => {
|
|
507
|
+
const trial = trials.find((t) => t.id === id);
|
|
508
|
+
return trial ? expandOnDoubleClick(trial) : false;
|
|
509
|
+
}
|
|
510
|
+
: expandOnDoubleClick
|
|
511
|
+
}
|
|
614
512
|
>
|
|
615
513
|
{trials.map((w) => (
|
|
616
514
|
<Trial key={w.id} id={w.id} chrome={chrome} suppress={suppress} />
|
|
@@ -623,65 +521,42 @@ function LabRuntime({
|
|
|
623
521
|
<PersistenceContext.Provider value={opened.records}>
|
|
624
522
|
<AnnotationPreloadContext.Provider value={opened.marks}>
|
|
625
523
|
<LabContext.Provider value={contextValue}>
|
|
626
|
-
<
|
|
627
|
-
<
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
<
|
|
661
|
-
<
|
|
662
|
-
{outerSurface ? null : (
|
|
663
|
-
// Two buffers stacked around the trials, both inert —
|
|
664
|
-
// each tile takes input from its own box. A tile's marks
|
|
665
|
-
// annotate the instrument's DOM from over it; an opaque
|
|
666
|
-
// renderer sits under it, so the pane can still hold a
|
|
667
|
-
// label. The under one is first so it paints first.
|
|
668
|
-
<>
|
|
669
|
-
<canvas
|
|
670
|
-
className="lk-lab__surface lk-lab__surface--under"
|
|
671
|
-
ref={(el) => {
|
|
672
|
-
ownUnderRef.current = el;
|
|
673
|
-
setOwnUnder(el);
|
|
674
|
-
}}
|
|
675
|
-
/>
|
|
676
|
-
<canvas
|
|
677
|
-
className="lk-lab__surface lk-lab__surface--over"
|
|
678
|
-
ref={(el) => {
|
|
679
|
-
ownOverRef.current = el;
|
|
680
|
-
setOwnOver(el);
|
|
681
|
-
}}
|
|
682
|
-
/>
|
|
683
|
-
</>
|
|
684
|
-
)}
|
|
524
|
+
<PresentationContext.Provider value={presentation}>
|
|
525
|
+
<CameraRegistryContext.Provider value={cameras}>
|
|
526
|
+
<ClockRegistryContext.Provider value={clocks}>
|
|
527
|
+
<ThemeProvider
|
|
528
|
+
theme={theme}
|
|
529
|
+
selection={{ mode: resolvedMode, density: density ?? 'comfortable' }}
|
|
530
|
+
className={presenting ? 'lk-lab lk-lab--present' : 'lk-lab'}
|
|
531
|
+
style={backdropStyle}
|
|
532
|
+
>
|
|
533
|
+
<LabShell
|
|
534
|
+
title={title ?? 'Labkit'}
|
|
535
|
+
mode={modeValue}
|
|
536
|
+
{...(pages ? { pages } : {})}
|
|
537
|
+
{...(path !== undefined ? { path } : {})}
|
|
538
|
+
footer={
|
|
539
|
+
hasFooterChrome ? (
|
|
540
|
+
<>
|
|
541
|
+
{footer}
|
|
542
|
+
<LabFooterRegion contributions={labChromeAll} />
|
|
543
|
+
</>
|
|
544
|
+
) : (
|
|
545
|
+
footer
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
header={
|
|
549
|
+
<>
|
|
550
|
+
<LabHeader {...(addTrial !== undefined ? { addTrial } : {})} />
|
|
551
|
+
{zoom && !presenting ? <LabZoom /> : null}
|
|
552
|
+
{children}
|
|
553
|
+
<LabHeaderRegion contributions={labChromeAll} />
|
|
554
|
+
<LabThemeSwitcher />
|
|
555
|
+
</>
|
|
556
|
+
}
|
|
557
|
+
>
|
|
558
|
+
<PanelHostContext.Provider value={panelHostsRef.current}>
|
|
559
|
+
<LabSurface bodyRef={setLabBody}>
|
|
685
560
|
{hasPaneChrome ? (
|
|
686
561
|
<LabPanes contributions={labChromeAll}>
|
|
687
562
|
<LabPalette contributions={labChromeAll} />
|
|
@@ -693,13 +568,13 @@ function LabRuntime({
|
|
|
693
568
|
{workspace}
|
|
694
569
|
</>
|
|
695
570
|
)}
|
|
696
|
-
</
|
|
697
|
-
</
|
|
698
|
-
</
|
|
699
|
-
</
|
|
700
|
-
</
|
|
701
|
-
</
|
|
702
|
-
</
|
|
571
|
+
</LabSurface>
|
|
572
|
+
</PanelHostContext.Provider>
|
|
573
|
+
</LabShell>
|
|
574
|
+
</ThemeProvider>
|
|
575
|
+
</ClockRegistryContext.Provider>
|
|
576
|
+
</CameraRegistryContext.Provider>
|
|
577
|
+
</PresentationContext.Provider>
|
|
703
578
|
</LabContext.Provider>
|
|
704
579
|
</AnnotationPreloadContext.Provider>
|
|
705
580
|
</PersistenceContext.Provider>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.lk-lightbox-lab-host {
|
|
2
|
+
width: 900px;
|
|
3
|
+
height: 560px;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// The target fills its pane in one flat color, so a mark is the only thing
|
|
7
|
+
// that can change a pixel inside it.
|
|
8
|
+
.lk-lightbox-lab-pane {
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: 60%;
|
|
11
|
+
background: rgb(0 0 255);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.lk-lightbox-lab-solid {
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 30%;
|
|
17
|
+
}
|