@weasel-js/labkit 1.7.3 → 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-Cjy6I-Iv.d.ts → CanvasStackContext-B05ZM04l.d.ts} +3 -1
- package/dist/_dts/{Stage-D6t7o0pF.d.ts → Stage-CIJlbjrE.d.ts} +5 -3
- package/dist/_dts/{index-DCMA4Fy1.d.ts → index-8geQNQet.d.ts} +84 -12
- package/dist/_dts/{index-BTmaRy6a.d.ts → index-DRi3V5sN.d.ts} +6 -4
- package/dist/_dts/{types-BUBSIZuV.d.ts → types-DUfQ0isU.d.ts} +5 -1
- package/dist/_dts/{useTrialState-Bh8bzFgE.d.ts → useTrialState-Ar0AFpCe.d.ts} +94 -87
- package/dist/canvas/index.d.ts +5 -5
- package/dist/canvas/index.js +2 -2
- package/dist/chrome/index.d.ts +3 -3
- package/dist/chrome/index.js +7 -7
- package/dist/{chunk-HBGH344L.js → chunk-4RCVWX4M.js} +55 -18
- 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-LPGBC63T.js → chunk-KHB3KRRQ.js} +7 -7
- package/dist/chunk-KHB3KRRQ.js.map +1 -0
- package/dist/{chunk-IBVRMHW5.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-4X44DMRS.js → chunk-RN7T7OBO.js} +46 -24
- package/dist/chunk-RN7T7OBO.js.map +1 -0
- package/dist/{chunk-4TJ34FXA.js → chunk-RXO5UEGT.js} +5 -12
- package/dist/chunk-RXO5UEGT.js.map +1 -0
- package/dist/{chunk-E6NARGU7.js → chunk-SQX35X5V.js} +3 -3
- package/dist/{chunk-E6NARGU7.js.map → chunk-SQX35X5V.js.map} +1 -1
- package/dist/{chunk-YCFKW5RX.js → chunk-T5OEUNCB.js} +16 -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-IRMPZIYU.js → chunk-V4SMGEYY.js} +6732 -6507
- 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-XSJJTXNA.js → chunk-YZA73U4A.js} +19 -19
- package/dist/chunk-YZA73U4A.js.map +1 -0
- package/dist/config/index.d.ts +12 -5
- package/dist/config/index.js +2 -2
- package/dist/controls/index.d.ts +2 -2
- package/dist/controls/index.js +5 -5
- package/dist/dragdrop/index.d.ts +2 -2
- package/dist/dragdrop/index.js +2 -2
- package/dist/index.d.ts +158 -16
- package/dist/index.js +1036 -415
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +2 -2
- package/dist/layers/index.js +3 -3
- package/dist/loupe/index.d.ts +176 -28
- package/dist/loupe/index.js +547 -141
- package/dist/loupe/index.js.map +1 -1
- package/dist/overview/index.d.ts +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 +6 -8
- package/dist/state/index.js +5 -5
- package/dist/styles.css +181 -44
- package/dist/surface/index.d.ts +20 -1
- package/dist/surface/index.js +2 -2
- package/dist/undo/index.d.ts +19 -8
- 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/builder.ts +11 -1
- package/src/config/resolve.test.ts +12 -0
- package/src/config/resolve.ts +5 -0
- package/src/config/types.ts +5 -1
- package/src/controls/ControlMatrix.test.tsx +10 -0
- package/src/controls/ControlMatrix.tsx +110 -58
- package/src/controls/ControlPanel.test.tsx +30 -0
- 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/theme/interstellar.theme.json +0 -14
- 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-4TJ34FXA.js.map +0 -1
- package/dist/chunk-4X44DMRS.js.map +0 -1
- package/dist/chunk-EESJUSYR.js.map +0 -1
- package/dist/chunk-HBGH344L.js.map +0 -1
- package/dist/chunk-IBVRMHW5.js.map +0 -1
- package/dist/chunk-IRMPZIYU.js.map +0 -1
- package/dist/chunk-LPGBC63T.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-XSJJTXNA.js.map +0 -1
- package/dist/chunk-YCFKW5RX.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
package/src/loupe/useLoupe.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { type RefObject, useCallback, useEffect, useReducer, useRef } from 'react';
|
|
9
9
|
import type { LoupeInputApi } from './loupeActions';
|
|
10
10
|
import { WHEEL_RATE } from './loupeActions';
|
|
11
|
-
import type { ResolvedLoupe } from './types';
|
|
11
|
+
import type { LoupeLens, ResolvedLoupe } from './types';
|
|
12
12
|
|
|
13
13
|
/** Options for {@link useLoupe}. */
|
|
14
14
|
export interface UseLoupeOptions {
|
|
@@ -20,6 +20,12 @@ export interface UseLoupeOptions {
|
|
|
20
20
|
/** Hex color at a host point. Omitted, the loupe reports no color — which
|
|
21
21
|
* is the honest answer for a surface with no pixels to read. */
|
|
22
22
|
sample?: (p: LoupePoint) => string | null;
|
|
23
|
+
/** Run `fn` whenever what `sample` reads changes without the aim moving — a
|
|
24
|
+
* canvas that animates, or a frame that arrives after the aim did; returns
|
|
25
|
+
* an unsubscribe. While the lens is up, each call samples the aim again, on
|
|
26
|
+
* top of the sample every aim takes at once. Omitted, color changes only on
|
|
27
|
+
* an aim. */
|
|
28
|
+
subscribeResample?: (fn: () => void) => () => void;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
/** A loupe as a React view reads it. */
|
|
@@ -28,11 +34,16 @@ export interface LoupeState {
|
|
|
28
34
|
visible: boolean;
|
|
29
35
|
/** Where it is aimed, in the host's own pixels. */
|
|
30
36
|
aim: LoupePoint;
|
|
37
|
+
/** The wheel's magnification. What the lens shows at is `lens.factor`. */
|
|
31
38
|
factor: number;
|
|
39
|
+
/** The box the lens is drawn in, and what it shows. */
|
|
40
|
+
lens: LoupeLens;
|
|
32
41
|
mode: LoupeMode;
|
|
33
42
|
color: string | null;
|
|
34
43
|
setMode: (mode: LoupeMode) => void;
|
|
35
44
|
setFactor: (factor: number) => void;
|
|
45
|
+
/** Sample the aim again, for pixels that changed under a still aim. */
|
|
46
|
+
resample: () => void;
|
|
36
47
|
/** Sample what the lens shows at a point inside it. */
|
|
37
48
|
pick: (p?: LoupePoint) => string | null;
|
|
38
49
|
/** What `<LoupeGestures>` drives the lens through. Stable for the life of
|
|
@@ -56,10 +67,19 @@ function lensShown(over: boolean, enabled: boolean, peeking: boolean): boolean {
|
|
|
56
67
|
* has no continuous-motion entry, and inventing one to carry the lens' aim
|
|
57
68
|
* would be an input taxonomy change, not a loupe change.
|
|
58
69
|
*/
|
|
59
|
-
export function useLoupe({
|
|
70
|
+
export function useLoupe({
|
|
71
|
+
options,
|
|
72
|
+
hostRef,
|
|
73
|
+
enabled,
|
|
74
|
+
sample,
|
|
75
|
+
subscribeResample,
|
|
76
|
+
}: UseLoupeOptions): LoupeState {
|
|
60
77
|
const [, bump] = useReducer((n: number) => n + 1, 0);
|
|
61
78
|
|
|
62
79
|
const overRef = useRef(false);
|
|
80
|
+
// Where the pointer last was over the host, in client px. The model ignores
|
|
81
|
+
// aims while the lens is down, so this is what the lens opens at.
|
|
82
|
+
const pointerRef = useRef<{ clientX: number; clientY: number } | null>(null);
|
|
63
83
|
const peekingRef = useRef(false);
|
|
64
84
|
const enabledRef = useLatest(enabled);
|
|
65
85
|
const sampleRef = useLatest(sample);
|
|
@@ -81,9 +101,15 @@ export function useLoupe({ options, hostRef, enabled, sample }: UseLoupeOptions)
|
|
|
81
101
|
onColorChange: (hex) => onColorChangeRef.current?.(hex),
|
|
82
102
|
surface: {
|
|
83
103
|
lens: () => {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
104
|
+
const { center, shows, width, height, factor } = lensRef.current;
|
|
105
|
+
return {
|
|
106
|
+
x: center.x - width / 2,
|
|
107
|
+
y: center.y - height / 2,
|
|
108
|
+
w: width,
|
|
109
|
+
h: height,
|
|
110
|
+
shows,
|
|
111
|
+
factor,
|
|
112
|
+
};
|
|
87
113
|
},
|
|
88
114
|
// The lens follows the pointer and is painted over the host rather than
|
|
89
115
|
// into it, so it is never part of the picture being magnified.
|
|
@@ -97,7 +123,6 @@ export function useLoupe({ options, hostRef, enabled, sample }: UseLoupeOptions)
|
|
|
97
123
|
}
|
|
98
124
|
const model = modelRef.current;
|
|
99
125
|
|
|
100
|
-
const diameterRef = useLatest(options.diameter);
|
|
101
126
|
const goneRef = useRef(false);
|
|
102
127
|
|
|
103
128
|
// The model holds no resources, and `dispose` is one-way — so unmounting only
|
|
@@ -111,11 +136,25 @@ export function useLoupe({ options, hostRef, enabled, sample }: UseLoupeOptions)
|
|
|
111
136
|
};
|
|
112
137
|
}, []);
|
|
113
138
|
|
|
139
|
+
// Aim at the pointer's last spot over the host, for a lens that has just
|
|
140
|
+
// come up under a pointer that has not moved since. Stable, because `input`
|
|
141
|
+
// captures it once.
|
|
142
|
+
const hostRefRef = useLatest(hostRef);
|
|
143
|
+
const aimAtPointer = useCallback((): void => {
|
|
144
|
+
const host = hostRefRef.current.current;
|
|
145
|
+
const p = pointerRef.current;
|
|
146
|
+
if (!host || !p) return;
|
|
147
|
+
const rect = host.getBoundingClientRect();
|
|
148
|
+
modelRef.current?.aimAt({ x: p.clientX - rect.left, y: p.clientY - rect.top });
|
|
149
|
+
}, [hostRefRef]);
|
|
150
|
+
|
|
114
151
|
// Turning the loupe off with the pointer still inside must put the lens away,
|
|
115
|
-
// and the model only reconsiders on an aim
|
|
152
|
+
// and the model only reconsiders on an aim; turning it on must open the lens
|
|
153
|
+
// where the pointer already is.
|
|
116
154
|
useEffect(() => {
|
|
117
|
-
if (
|
|
118
|
-
|
|
155
|
+
if (enabled) aimAtPointer();
|
|
156
|
+
else if (!peekingRef.current) bump();
|
|
157
|
+
}, [enabled, aimAtPointer]);
|
|
119
158
|
|
|
120
159
|
useEffect(() => {
|
|
121
160
|
const host = hostRef.current;
|
|
@@ -123,6 +162,7 @@ export function useLoupe({ options, hostRef, enabled, sample }: UseLoupeOptions)
|
|
|
123
162
|
|
|
124
163
|
const onPointerMove = (e: PointerEvent): void => {
|
|
125
164
|
overRef.current = true;
|
|
165
|
+
pointerRef.current = { clientX: e.clientX, clientY: e.clientY };
|
|
126
166
|
const rect = host.getBoundingClientRect();
|
|
127
167
|
model.aimAt({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
|
128
168
|
if (!shown()) return;
|
|
@@ -130,6 +170,7 @@ export function useLoupe({ options, hostRef, enabled, sample }: UseLoupeOptions)
|
|
|
130
170
|
};
|
|
131
171
|
const onPointerLeave = (): void => {
|
|
132
172
|
overRef.current = false;
|
|
173
|
+
pointerRef.current = null;
|
|
133
174
|
bump();
|
|
134
175
|
};
|
|
135
176
|
|
|
@@ -141,6 +182,58 @@ export function useLoupe({ options, hostRef, enabled, sample }: UseLoupeOptions)
|
|
|
141
182
|
};
|
|
142
183
|
}, [hostRef, model, shown]);
|
|
143
184
|
|
|
185
|
+
// Subscribed while the lens is up, and before any effect the caller declares
|
|
186
|
+
// after this hook runs — so a frame its own effect provokes is not missed.
|
|
187
|
+
// This render's `enabled`: `enabledRef` holds the last committed one.
|
|
188
|
+
const visible = lensShown(overRef.current, enabled, peekingRef.current);
|
|
189
|
+
|
|
190
|
+
// Asked only while the lens is up: a host's `place` may measure its layout.
|
|
191
|
+
const placed = visible
|
|
192
|
+
? (options.place?.({ aim: model.aim, factor: model.factor }) ?? null)
|
|
193
|
+
: null;
|
|
194
|
+
const lens: LoupeLens = placed
|
|
195
|
+
? {
|
|
196
|
+
center: placed.center,
|
|
197
|
+
shows: placed.shows ?? placed.center,
|
|
198
|
+
width: placed.width,
|
|
199
|
+
height: placed.height,
|
|
200
|
+
factor: placed.factor ?? model.factor,
|
|
201
|
+
shape: options.shape,
|
|
202
|
+
}
|
|
203
|
+
: {
|
|
204
|
+
center: model.aim,
|
|
205
|
+
shows: model.aim,
|
|
206
|
+
width: options.diameter,
|
|
207
|
+
height: options.diameter,
|
|
208
|
+
factor: model.factor,
|
|
209
|
+
shape: options.shape,
|
|
210
|
+
};
|
|
211
|
+
const lensRef = useLatest(lens);
|
|
212
|
+
|
|
213
|
+
// Told on a change in value, not on every render: a host drawing into this
|
|
214
|
+
// box would otherwise redraw for each one.
|
|
215
|
+
const onLensRef = useLatest(options.onLens);
|
|
216
|
+
const reportedRef = useRef<string>(JSON.stringify(null));
|
|
217
|
+
const report = useCallback(
|
|
218
|
+
(next: LoupeLens | null): void => {
|
|
219
|
+
const key = JSON.stringify(next);
|
|
220
|
+
if (key === reportedRef.current) return;
|
|
221
|
+
reportedRef.current = key;
|
|
222
|
+
onLensRef.current?.(next);
|
|
223
|
+
},
|
|
224
|
+
[onLensRef],
|
|
225
|
+
);
|
|
226
|
+
const shownLens = visible ? lens : null;
|
|
227
|
+
const shownKey = JSON.stringify(shownLens);
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
report(JSON.parse(shownKey) as LoupeLens | null);
|
|
230
|
+
}, [shownKey, report]);
|
|
231
|
+
useEffect(() => () => report(null), [report]);
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (!visible || !subscribeResample) return;
|
|
234
|
+
return subscribeResample(() => model.resample());
|
|
235
|
+
}, [visible, subscribeResample, model]);
|
|
236
|
+
|
|
144
237
|
const inputRef = useRef<LoupeInputApi | null>(null);
|
|
145
238
|
if (inputRef.current === null) {
|
|
146
239
|
inputRef.current = {
|
|
@@ -148,6 +241,7 @@ export function useLoupe({ options, hostRef, enabled, sample }: UseLoupeOptions)
|
|
|
148
241
|
setPeeking: (on) => {
|
|
149
242
|
if (peekingRef.current === on) return;
|
|
150
243
|
peekingRef.current = on;
|
|
244
|
+
if (on) aimAtPointer();
|
|
151
245
|
bump();
|
|
152
246
|
},
|
|
153
247
|
magnifyBy: (deltaY) => {
|
|
@@ -158,14 +252,15 @@ export function useLoupe({ options, hostRef, enabled, sample }: UseLoupeOptions)
|
|
|
158
252
|
}
|
|
159
253
|
|
|
160
254
|
return {
|
|
161
|
-
|
|
162
|
-
visible: lensShown(overRef.current, enabled, peekingRef.current),
|
|
255
|
+
visible,
|
|
163
256
|
aim: model.aim,
|
|
164
257
|
factor: model.factor,
|
|
258
|
+
lens,
|
|
165
259
|
mode: model.mode,
|
|
166
260
|
color: model.color,
|
|
167
261
|
setMode: model.setMode,
|
|
168
262
|
setFactor: model.setFactor,
|
|
263
|
+
resample: model.resample,
|
|
169
264
|
pick: model.pick,
|
|
170
265
|
input: inputRef.current,
|
|
171
266
|
};
|
package/src/primitives/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type { StatusBarItemProps, StatusBarProps } from '@weasel-js/ui';
|
|
2
|
+
export { StatusBar, StatusBarItem, StatusBarSpacer } from '@weasel-js/ui';
|
|
1
3
|
export type { FloatingPanelProps } from './FloatingPanel';
|
|
2
4
|
export { FloatingPanel } from './FloatingPanel';
|
|
3
5
|
export { FpsMeter } from './FpsMeter';
|
|
@@ -11,8 +13,6 @@ export type { ScaleIndicatorProps } from './ScaleIndicator';
|
|
|
11
13
|
export { ScaleIndicator } from './ScaleIndicator';
|
|
12
14
|
export type { SplitProps } from './Split';
|
|
13
15
|
export { Split } from './Split';
|
|
14
|
-
export type { StatusBarProps, StatusBarSectionProps } from './StatusBar';
|
|
15
|
-
export { StatusBar } from './StatusBar';
|
|
16
16
|
export type { ToolbarButtonProps, ToolbarGroupProps, ToolbarProps } from './Toolbar';
|
|
17
17
|
export { Toolbar } from './Toolbar';
|
|
18
18
|
export type { ZoomControlProps } from './ZoomControl';
|
|
@@ -72,7 +72,6 @@ import { JobProgress } from '../primitives/JobProgress';
|
|
|
72
72
|
import { Legend, type LegendEntry } from '../primitives/Legend';
|
|
73
73
|
import { ScaleIndicator } from '../primitives/ScaleIndicator';
|
|
74
74
|
import { Split } from '../primitives/Split';
|
|
75
|
-
import { StatusBar } from '../primitives/StatusBar';
|
|
76
75
|
import { Toolbar } from '../primitives/Toolbar';
|
|
77
76
|
import { ZoomControl } from '../primitives/ZoomControl';
|
|
78
77
|
|
|
@@ -831,12 +830,6 @@ function LabChrome() {
|
|
|
831
830
|
</Toolbar.Group>
|
|
832
831
|
</Toolbar>
|
|
833
832
|
</Cell>
|
|
834
|
-
<Cell label="StatusBar">
|
|
835
|
-
<StatusBar>
|
|
836
|
-
<StatusBar.Section>Items: 12</StatusBar.Section>
|
|
837
|
-
<StatusBar.Section>Zoom: 100%</StatusBar.Section>
|
|
838
|
-
</StatusBar>
|
|
839
|
-
</Cell>
|
|
840
833
|
<Cell label="Legend, JobProgress">
|
|
841
834
|
<Legend entries={LEGEND} />
|
|
842
835
|
<JobProgress job={JOB} />
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { createHistory } from '@weasel-js/core';
|
|
1
2
|
import { describe, expect, it } from 'vitest';
|
|
2
3
|
import {
|
|
3
4
|
decodeUrlHash,
|
|
4
5
|
deserializeTrials,
|
|
5
|
-
emptyUndoStack,
|
|
6
6
|
encodeUrlHash,
|
|
7
7
|
labStorageKey,
|
|
8
8
|
newId,
|
|
@@ -29,12 +29,6 @@ describe('encodeUrlHash / decodeUrlHash', () => {
|
|
|
29
29
|
});
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
describe('emptyUndoStack', () => {
|
|
33
|
-
it('returns an empty stack', () => {
|
|
34
|
-
expect(emptyUndoStack()).toEqual({ past: [], future: [] });
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
32
|
describe('serializeTrials', () => {
|
|
39
33
|
it('returns records with the undo stack dropped', () => {
|
|
40
34
|
const records = serializeTrials(
|
|
@@ -45,7 +39,7 @@ describe('serializeTrials', () => {
|
|
|
45
39
|
config: { a: 1 },
|
|
46
40
|
state: { b: 2 },
|
|
47
41
|
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
48
|
-
|
|
42
|
+
history: createHistory(null),
|
|
49
43
|
},
|
|
50
44
|
],
|
|
51
45
|
{},
|
|
@@ -70,7 +64,6 @@ describe('serializeTrials', () => {
|
|
|
70
64
|
config: {},
|
|
71
65
|
state: { n: 2 },
|
|
72
66
|
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
73
|
-
undoStack: { past: [], future: [] },
|
|
74
67
|
},
|
|
75
68
|
],
|
|
76
69
|
{ Test: { serialize: (s) => ({ doubled: (s as { n: number }).n * 2 }) } },
|
|
@@ -80,7 +73,7 @@ describe('serializeTrials', () => {
|
|
|
80
73
|
});
|
|
81
74
|
|
|
82
75
|
describe('deserializeTrials', () => {
|
|
83
|
-
it('rebuilds records with
|
|
76
|
+
it('rebuilds records with no undo history', () => {
|
|
84
77
|
const out = deserializeTrials(
|
|
85
78
|
[
|
|
86
79
|
{
|
|
@@ -93,7 +86,7 @@ describe('deserializeTrials', () => {
|
|
|
93
86
|
],
|
|
94
87
|
{},
|
|
95
88
|
);
|
|
96
|
-
expect(out[0].
|
|
89
|
+
expect(out[0].history).toBeUndefined();
|
|
97
90
|
});
|
|
98
91
|
|
|
99
92
|
it('runs the instrument deserializer over the state', () => {
|
package/src/state/helpers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InstrumentSerializers, SerializedTrial, TrialRecord
|
|
1
|
+
import type { InstrumentSerializers, SerializedTrial, TrialRecord } from './types';
|
|
2
2
|
|
|
3
3
|
/** The storage key a lab writes one of its buckets under. Namespaced by
|
|
4
4
|
* `storageKey` so two labs sharing an origin do not collide. */
|
|
@@ -41,11 +41,6 @@ export function newId(): string {
|
|
|
41
41
|
return `lk-${Date.now().toString(36)}-${idCounter++}`;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** A fresh, empty undo history. */
|
|
45
|
-
export function emptyUndoStack(): UndoStack {
|
|
46
|
-
return { past: [], future: [] };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
44
|
/** Serialize trials for storage, running each instrument's own serializer
|
|
50
45
|
* over its state. Undo history is deliberately dropped — it does not survive
|
|
51
46
|
* a reload. */
|
|
@@ -53,7 +48,7 @@ export function serializeTrials(
|
|
|
53
48
|
trials: TrialRecord[],
|
|
54
49
|
serializers: InstrumentSerializers,
|
|
55
50
|
): SerializedTrial[] {
|
|
56
|
-
return trials.map(({
|
|
51
|
+
return trials.map(({ history: _history, ...w }) => {
|
|
57
52
|
const s = serializers[w.instrumentName];
|
|
58
53
|
return { ...w, state: s?.serialize ? s.serialize(w.state) : w.state };
|
|
59
54
|
});
|
|
@@ -75,7 +70,6 @@ export function deserializeTrials(
|
|
|
75
70
|
return {
|
|
76
71
|
...r,
|
|
77
72
|
state: d?.deserialize ? d.deserialize(r.state, r.config) : r.state,
|
|
78
|
-
undoStack: emptyUndoStack(),
|
|
79
73
|
};
|
|
80
74
|
});
|
|
81
75
|
}
|
package/src/state/index.ts
CHANGED
|
@@ -24,7 +24,6 @@ export {
|
|
|
24
24
|
export {
|
|
25
25
|
decodeUrlHash,
|
|
26
26
|
deserializeTrials,
|
|
27
|
-
emptyUndoStack,
|
|
28
27
|
encodeUrlHash,
|
|
29
28
|
labStorageKey,
|
|
30
29
|
serializeTrials,
|
|
@@ -50,7 +49,6 @@ export type {
|
|
|
50
49
|
StorageChange,
|
|
51
50
|
TrialRecord,
|
|
52
51
|
TrialStateHandle,
|
|
53
|
-
UndoStack,
|
|
54
52
|
} from './types';
|
|
55
53
|
export type { UndockedPanel, UndockedPanels } from './undock';
|
|
56
54
|
export { dockPanel, panelKey, undockPanel } from './undock';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHistory } from '@weasel-js/core';
|
|
1
2
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
3
|
import type { Instrument } from '../instrument/types';
|
|
3
4
|
import { createMemoryAdapter } from './adapters';
|
|
@@ -61,13 +62,13 @@ describe('openLabStore — round trips', () => {
|
|
|
61
62
|
const backing = new Map<string, unknown>();
|
|
62
63
|
const first = await open(backing);
|
|
63
64
|
first.store.getState().addTrial(trial('w1', { state: { n: 7 } }));
|
|
64
|
-
first.store.getState().
|
|
65
|
+
first.store.getState().setTrialHistory('w1', createHistory(null));
|
|
65
66
|
await first.close();
|
|
66
67
|
|
|
67
68
|
const again = await open(backing);
|
|
68
69
|
expect(again.store.getState().trials).toHaveLength(1);
|
|
69
70
|
expect(again.store.getState().trials[0]).toMatchObject({ id: 'w1', state: { n: 7 } });
|
|
70
|
-
expect(again.store.getState().trials[0]?.
|
|
71
|
+
expect(again.store.getState().trials[0]?.history).toBeUndefined();
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
// An instrument holding a Map, a Set or anything else storage cannot keep
|
|
@@ -321,7 +322,7 @@ describe('openLabStore — what a change writes', () => {
|
|
|
321
322
|
storeLab(backing, { trials: [trial('a')] });
|
|
322
323
|
const { storage, set } = counting(backing);
|
|
323
324
|
const opened = await openLabStore({ storageKey: 'test', storage });
|
|
324
|
-
opened.store.getState().
|
|
325
|
+
opened.store.getState().setTrialHistory('a', createHistory(null));
|
|
325
326
|
await opened.close();
|
|
326
327
|
expect(set).not.toHaveBeenCalled();
|
|
327
328
|
});
|
|
@@ -380,7 +381,7 @@ describe('openLabStore — trial order', () => {
|
|
|
380
381
|
storeLab(backing, { trials: [trial('a'), trial('c')] });
|
|
381
382
|
const opened = await open(backing);
|
|
382
383
|
const [a, c] = opened.store.getState().trials as [TrialRecord, TrialRecord];
|
|
383
|
-
const b: TrialRecord =
|
|
384
|
+
const b: TrialRecord = trial('b');
|
|
384
385
|
opened.store.setState({ trials: [a, b, c] });
|
|
385
386
|
await opened.close();
|
|
386
387
|
const ids = (await open(backing)).store.getState().trials.map((t) => t.id);
|
|
@@ -409,8 +410,9 @@ describe('openLabStore — two tabs', () => {
|
|
|
409
410
|
storeLab(backing, { trials: [trial('t1', { state: { n: 0 } }), trial('t2')] });
|
|
410
411
|
const tabA = await open(backing);
|
|
411
412
|
const tabB = await open(backing);
|
|
412
|
-
|
|
413
|
-
tabA.store.getState().
|
|
413
|
+
const kept = createHistory(null);
|
|
414
|
+
tabA.store.getState().setTrialHistory('t1', createHistory(null));
|
|
415
|
+
tabA.store.getState().setTrialHistory('t2', kept);
|
|
414
416
|
|
|
415
417
|
tabB.store.getState().updateTrialState('t1', { n: 5 });
|
|
416
418
|
await tabB.records.flush();
|
|
@@ -418,8 +420,8 @@ describe('openLabStore — two tabs', () => {
|
|
|
418
420
|
|
|
419
421
|
const [t1, t2] = tabA.store.getState().trials;
|
|
420
422
|
expect(t1?.state).toEqual({ n: 5 });
|
|
421
|
-
expect(t1?.
|
|
422
|
-
expect(t2?.
|
|
423
|
+
expect(t1?.history).toBeUndefined();
|
|
424
|
+
expect(t2?.history).toBe(kept);
|
|
423
425
|
});
|
|
424
426
|
|
|
425
427
|
it("applies another tab's delete, snapshot, layout and mode without writing them back", async () => {
|
|
@@ -212,7 +212,7 @@ function persistedFieldsDiffer(a: TrialRecord, b: TrialRecord): boolean {
|
|
|
212
212
|
const left = a as unknown as Record<string, unknown>;
|
|
213
213
|
const right = b as unknown as Record<string, unknown>;
|
|
214
214
|
for (const key of new Set([...Object.keys(left), ...Object.keys(right)])) {
|
|
215
|
-
if (key !== '
|
|
215
|
+
if (key !== 'history' && !Object.is(left[key], right[key])) return true;
|
|
216
216
|
}
|
|
217
217
|
return false;
|
|
218
218
|
}
|
package/src/state/store.test.ts
CHANGED
|
@@ -29,7 +29,7 @@ describe('createLabStore — initial state', () => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
describe('addTrial', () => {
|
|
32
|
-
it('adds a trial with
|
|
32
|
+
it('adds a trial with no undo history', () => {
|
|
33
33
|
const s = makeStore();
|
|
34
34
|
s.getState().addTrial({
|
|
35
35
|
id: 'w1',
|
|
@@ -39,7 +39,7 @@ describe('addTrial', () => {
|
|
|
39
39
|
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
40
40
|
});
|
|
41
41
|
expect(s.getState().trials).toHaveLength(1);
|
|
42
|
-
expect(s.getState().trials[0]?.
|
|
42
|
+
expect(s.getState().trials[0]?.history).toBeUndefined();
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
45
|
|
package/src/state/store.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { History } from '@weasel-js/core';
|
|
1
2
|
import { createStore, type StoreApi } from 'zustand/vanilla';
|
|
2
3
|
import { applyConfigWrite } from '../config/autoConfig';
|
|
3
4
|
import { fillConfigDefaults } from '../config/path';
|
|
@@ -9,7 +10,7 @@ import {
|
|
|
9
10
|
} from '../instrument/serializers';
|
|
10
11
|
import type { InstrumentList } from '../instrument/types';
|
|
11
12
|
import { emptyDocument } from './document';
|
|
12
|
-
import { deserializeTrials,
|
|
13
|
+
import { deserializeTrials, newId } from './helpers';
|
|
13
14
|
import type {
|
|
14
15
|
CreateLabStoreOptions,
|
|
15
16
|
InstrumentHooks,
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
/** Every mutation a lab store supports: managing trials, saving and
|
|
30
31
|
* restoring snapshots, and setting the color mode. */
|
|
31
32
|
export interface LabStoreActions {
|
|
32
|
-
addTrial: (record: Omit<TrialRecord, '
|
|
33
|
+
addTrial: (record: Omit<TrialRecord, 'history'>) => void;
|
|
33
34
|
removeTrial: (id: string) => void;
|
|
34
35
|
updateTrialState: <TS>(id: string, next: TS | ((prev: TS) => TS)) => void;
|
|
35
36
|
/** Write one config value. `path` is dotted for a value nested under an
|
|
@@ -42,10 +43,10 @@ export interface LabStoreActions {
|
|
|
42
43
|
/** Fold or unfold one of a trial's sections. */
|
|
43
44
|
setTrialSectionCollapsed: (id: string, key: string, collapsed: boolean) => void;
|
|
44
45
|
updateTrialAnnotations: (id: string, doc: unknown) => void;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
) => void;
|
|
46
|
+
/** Give a trial the undo history it made on its first snapshot. */
|
|
47
|
+
setTrialHistory: (id: string, history: History) => void;
|
|
48
|
+
/** Record where a trial's clock stands. */
|
|
49
|
+
updateTrialClock: (id: string, clock: { elapsed: number; rate: number }) => void;
|
|
49
50
|
setTrialInstrument: (id: string, instrumentName: string) => void;
|
|
50
51
|
saveSnapshot: (trialId: string, name: string) => void;
|
|
51
52
|
loadSnapshot: (snapshotId: string, trialId: string) => void;
|
|
@@ -96,7 +97,7 @@ export function createLabStore(options: CreateLabStoreOptions = {}): LabStore {
|
|
|
96
97
|
|
|
97
98
|
addTrial: (record) => {
|
|
98
99
|
set((s) => ({
|
|
99
|
-
trials: [...s.trials,
|
|
100
|
+
trials: [...s.trials, record],
|
|
100
101
|
}));
|
|
101
102
|
},
|
|
102
103
|
|
|
@@ -182,16 +183,14 @@ export function createLabStore(options: CreateLabStoreOptions = {}): LabStore {
|
|
|
182
183
|
}));
|
|
183
184
|
},
|
|
184
185
|
|
|
185
|
-
|
|
186
|
+
updateTrialClock: (id, clock) => {
|
|
186
187
|
set((s) => ({
|
|
187
|
-
trials: s.trials.map((w) => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return { ...w, undoStack };
|
|
194
|
-
}),
|
|
188
|
+
trials: s.trials.map((w) => (w.id === id ? { ...w, clock } : w)),
|
|
189
|
+
}));
|
|
190
|
+
},
|
|
191
|
+
setTrialHistory: (id, history) => {
|
|
192
|
+
set((s) => ({
|
|
193
|
+
trials: s.trials.map((w) => (w.id === id ? { ...w, history } : w)),
|
|
195
194
|
}));
|
|
196
195
|
},
|
|
197
196
|
|
package/src/state/types.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
+
import type { History } from '@weasel-js/core';
|
|
1
2
|
import type { ColorModePreference } from '@weasel-js/theme';
|
|
2
3
|
import type { Auto } from '../config/auto';
|
|
3
4
|
import type { ConfigPath, ResolvedConfig, ValueAtPath } from '../config/types';
|
|
4
5
|
import type { InstrumentList } from '../instrument/types';
|
|
5
6
|
import type { UndockedPanels } from './undock';
|
|
6
|
-
/** A trial's undo history, as snapshots of its state either side of the
|
|
7
|
-
* present. */
|
|
8
|
-
export interface UndoStack {
|
|
9
|
-
past: unknown[];
|
|
10
|
-
future: unknown[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
7
|
/** The trial a per-trial call is coming from. `id` is the id `useTileId`
|
|
14
8
|
* scopes a surface tile under, so a consumer keying its own per-trial
|
|
15
9
|
* registry and labkit's tiles agree on the key. */
|
|
@@ -59,7 +53,13 @@ export interface TrialRecord<TS = unknown, TC = unknown, TV = unknown> {
|
|
|
59
53
|
* no `annotations` — or declares its own `storage`. Not in `state`, which
|
|
60
54
|
* belongs to the instrument and is typed as such. */
|
|
61
55
|
annotations?: unknown;
|
|
62
|
-
|
|
56
|
+
/** This trial's undo history over `state`, made by the trial on its first
|
|
57
|
+
* snapshot. Session-only: absent on a new, cloned, swapped or reloaded
|
|
58
|
+
* trial, which is how each starts with an empty history. */
|
|
59
|
+
history?: History;
|
|
60
|
+
/** Where the trial's clock stood at its last rate or seek change, for a
|
|
61
|
+
* trial whose instrument declares `clock`. */
|
|
62
|
+
clock?: { elapsed: number; rate: number };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/** A named, saved copy of a trial's config and state, restorable into any
|
|
@@ -190,7 +190,7 @@ export type InstrumentSerializers = Record<
|
|
|
190
190
|
|
|
191
191
|
/** A trial as it is persisted: everything but the undo history, which is
|
|
192
192
|
* session-only. */
|
|
193
|
-
export type SerializedTrial = Omit<TrialRecord, '
|
|
193
|
+
export type SerializedTrial = Omit<TrialRecord, 'history'>;
|
|
194
194
|
|
|
195
195
|
/** Everything a lab persists, under one key, at a known version. */
|
|
196
196
|
export interface LabDocument {
|
package/src/styles.less
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
@import (reference, less) '@weasel-js/theme/inherited.module.css';
|
|
9
9
|
@import './theme/base.less';
|
|
10
10
|
@import './lab/Workspace.less';
|
|
11
|
+
@import './lightbox/Lightbox.less';
|
|
11
12
|
@import './lab/LabShell.less';
|
|
12
13
|
@import './lab/LabSwitcher.less';
|
|
13
14
|
@import './primitives/Toolbar.less';
|
|
14
15
|
@import './primitives/Split.less';
|
|
15
16
|
@import './primitives/ZoomControl.less';
|
|
16
|
-
@import './primitives/StatusBar.less';
|
|
17
17
|
@import './primitives/JobProgress.less';
|
|
18
18
|
@import './primitives/FpsMeter.less';
|
|
19
19
|
@import './primitives/ScaleIndicator.less';
|
|
@@ -36,3 +36,4 @@
|
|
|
36
36
|
@import './dragdrop/Palette.less';
|
|
37
37
|
@import './dragdrop/DragGhost.less';
|
|
38
38
|
@import './specimen/Specimen.less';
|
|
39
|
+
@import './lab/presentation.less';
|
package/src/surface/AGENTS.md
CHANGED
|
@@ -58,6 +58,19 @@ was the only trial open.
|
|
|
58
58
|
trial may register one, or none: a trial holding a drawn pane beside an undrawn
|
|
59
59
|
one contributes a single rect, and a trial with nothing to draw contributes none.
|
|
60
60
|
|
|
61
|
+
## An expanded tile
|
|
62
|
+
|
|
63
|
+
A `<Lightbox>` lifts its tile into the browser's top layer, which takes it out
|
|
64
|
+
from under the buffers. So the lab declares its two buffer layers with
|
|
65
|
+
`<LightboxLayers>` (`src/lab/LabSurface.tsx`), and an open lightbox lifts the
|
|
66
|
+
under layer, then itself, then the over layer — the same stacking as before,
|
|
67
|
+
each covering the window. The over layer is the surface container, so the
|
|
68
|
+
surface re-measures against the window and the buffers resize. The lightbox
|
|
69
|
+
also calls `scope(el)`: tiles outside it leave `rects`, are not painted, and
|
|
70
|
+
the retile clears them; `useTileInScope` tells a tenant's own chrome on the
|
|
71
|
+
surface (the annotation input box) to hide. A host owning its own surface does
|
|
72
|
+
the same for its buffers, or an expanded tile shows without them.
|
|
73
|
+
|
|
61
74
|
## Traps
|
|
62
75
|
|
|
63
76
|
- **`preserveDrawingBuffer` is the consumer's job and is usually required.** A
|
package/src/surface/index.ts
CHANGED
|
@@ -9,11 +9,15 @@ function fakeHandle(): SurfaceHandle {
|
|
|
9
9
|
invalidate: vi.fn(),
|
|
10
10
|
invalidateAll: vi.fn(),
|
|
11
11
|
invalidateRects: vi.fn(),
|
|
12
|
+
invalidateBox: vi.fn(),
|
|
12
13
|
registerTile: vi.fn(),
|
|
13
14
|
registerClear: vi.fn(() => () => {}),
|
|
14
15
|
registerPainter: vi.fn(() => () => {}),
|
|
15
16
|
containerRef: vi.fn(),
|
|
16
17
|
getContainer: vi.fn(() => null),
|
|
18
|
+
scope: vi.fn(),
|
|
19
|
+
inScope: vi.fn(() => true),
|
|
20
|
+
subscribeScope: vi.fn(() => () => {}),
|
|
17
21
|
};
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useContext } from 'react';
|
|
1
|
+
import { useCallback, useContext, useSyncExternalStore } from 'react';
|
|
2
2
|
import { TrialIdContext } from '../state/context';
|
|
3
3
|
import { SurfaceCanvasContext, SurfaceContext, type SurfaceLayer } from './SurfaceContext';
|
|
4
4
|
import type { SurfaceHandle } from './useTiledSurface';
|
|
@@ -55,3 +55,19 @@ export function useSurfaceTile(id: string): (el: HTMLElement | null) => void {
|
|
|
55
55
|
[surface, tileId],
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
const noSubscribe = (): (() => void) => () => {};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether the tile registered under `tileId` — the key `useTileId` returns —
|
|
63
|
+
* is inside the surface's scope. A tenant with chrome of its own on the
|
|
64
|
+
* surface hides it while this is false, since nothing paints that tile then.
|
|
65
|
+
* True with no surface above.
|
|
66
|
+
*/
|
|
67
|
+
export function useTileInScope(tileId: string): boolean {
|
|
68
|
+
const surface = useSurfaceOptional();
|
|
69
|
+
return useSyncExternalStore(
|
|
70
|
+
surface?.subscribeScope ?? noSubscribe,
|
|
71
|
+
() => surface?.inScope(tileId) ?? true,
|
|
72
|
+
);
|
|
73
|
+
}
|