@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/dist/surface/index.d.ts
CHANGED
|
@@ -53,6 +53,9 @@ interface SurfaceHandle {
|
|
|
53
53
|
invalidate: (id: string) => void;
|
|
54
54
|
/** Mark every tile — what a resize or a tile-set change means. */
|
|
55
55
|
invalidateAll: () => void;
|
|
56
|
+
/** Mark every tile overlapping `box`, in viewport px as `getBoundingClientRect`
|
|
57
|
+
* gives them: what something drawn over the tiles leaves behind when it moves. */
|
|
58
|
+
invalidateBox: (box: Box) => void;
|
|
56
59
|
/** Re-measure before the next frame. The escape hatch for a host that knows it
|
|
57
60
|
* moved something a ResizeObserver cannot see. */
|
|
58
61
|
invalidateRects: () => void;
|
|
@@ -71,6 +74,15 @@ interface SurfaceHandle {
|
|
|
71
74
|
/** The element tile rects are measured against, and so the one a tile's own
|
|
72
75
|
* chrome positions itself inside. Null before the owner attaches it. */
|
|
73
76
|
getContainer: () => HTMLElement | null;
|
|
77
|
+
/** Narrow the surface to the tiles inside `el`, or widen it back with
|
|
78
|
+
* `null`. A tile outside drops out of the frame's `rects` and is not
|
|
79
|
+
* painted, and the frame is retiled so the buffer is cleared of it — what
|
|
80
|
+
* an expanded tile needs, with the buffers lifted over the rest. */
|
|
81
|
+
scope: (el: HTMLElement | null) => void;
|
|
82
|
+
/** Whether the tile is inside the current scope. Everything is, unscoped. */
|
|
83
|
+
inScope: (id: string) => boolean;
|
|
84
|
+
/** Called whenever the scope changes. Returns the unsubscribe. */
|
|
85
|
+
subscribeScope: (listener: () => void) => () => void;
|
|
74
86
|
}
|
|
75
87
|
/** What a tile's painter is handed: where it sits on the surface now, and the
|
|
76
88
|
* frame that dirtied it. */
|
|
@@ -148,6 +160,13 @@ declare function useTileId(id: string): string;
|
|
|
148
160
|
* The frame reports it under `useTileId(id)`, not under `id`.
|
|
149
161
|
*/
|
|
150
162
|
declare function useSurfaceTile(id: string): (el: HTMLElement | null) => void;
|
|
163
|
+
/**
|
|
164
|
+
* Whether the tile registered under `tileId` — the key `useTileId` returns —
|
|
165
|
+
* is inside the surface's scope. A tenant with chrome of its own on the
|
|
166
|
+
* surface hides it while this is false, since nothing paints that tile then.
|
|
167
|
+
* True with no surface above.
|
|
168
|
+
*/
|
|
169
|
+
declare function useTileInScope(tileId: string): boolean;
|
|
151
170
|
|
|
152
|
-
export { SurfaceCanvasContext, SurfaceContext, composeRects, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTileId, useTiledSurface };
|
|
171
|
+
export { SurfaceCanvasContext, SurfaceContext, composeRects, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTileId, useTileInScope, useTiledSurface };
|
|
153
172
|
export type { Box, Rect, SurfaceCanvases, SurfaceClear, SurfaceFrame, SurfaceHandle, SurfaceLayer, TilePainter, UseTiledSurfaceOptions };
|
package/dist/surface/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { composeRects, toDeviceRect, useTiledSurface } from '../chunk-
|
|
2
|
-
export { SurfaceCanvasContext, SurfaceContext, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTileId } from '../chunk-
|
|
1
|
+
export { composeRects, toDeviceRect, useTiledSurface } from '../chunk-QMAY74TP.js';
|
|
2
|
+
export { SurfaceCanvasContext, SurfaceContext, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTileId, useTileInScope } from '../chunk-6IIKLXBZ.js';
|
|
3
3
|
import '../chunk-2YILQ7Y5.js';
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
package/dist/undo/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { au as UndoCapability } from '../_dts/index-8geQNQet.js';
|
|
2
2
|
import '@weasel-js/ui';
|
|
3
3
|
import '@weasel-js/core';
|
|
4
4
|
import 'react';
|
|
5
5
|
import '@weasel-js/theme';
|
|
6
|
-
import '../_dts/types-
|
|
6
|
+
import '../_dts/types-DUfQ0isU.js';
|
|
7
7
|
import '@weasel-js/quantity';
|
|
8
8
|
import '@weasel-js/geom';
|
|
9
9
|
import '../_dts/types-DJ79Tg5J.js';
|
|
@@ -20,31 +20,42 @@ interface EventBus {
|
|
|
20
20
|
/** Build an empty event bus. */
|
|
21
21
|
declare function createEventBus(): EventBus;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated The whole module. A trial's undo runs on weasel-history
|
|
25
|
+
* (`TrialRecord.history`); for an undo of your own, `createHistory` from
|
|
26
|
+
* `@weasel-js/core`. Kept until sherpa's studio, its last user, moves off it.
|
|
27
|
+
*/
|
|
23
28
|
/** Undo history as two stacks of state snapshots: what has been done, and
|
|
24
|
-
* what has been undone. The present state is held elsewhere, not here.
|
|
29
|
+
* what has been undone. The present state is held elsewhere, not here.
|
|
30
|
+
* @deprecated See the module comment. */
|
|
25
31
|
interface UndoStack {
|
|
26
32
|
past: unknown[];
|
|
27
33
|
future: unknown[];
|
|
28
34
|
}
|
|
29
|
-
/** A fresh, empty history.
|
|
35
|
+
/** A fresh, empty history.
|
|
36
|
+
* @deprecated See the module comment. */
|
|
30
37
|
declare function emptyStack(): UndoStack;
|
|
31
38
|
/** Record a new snapshot, discarding the redo branch and the oldest entry
|
|
32
|
-
* once `maxDepth` is reached.
|
|
39
|
+
* once `maxDepth` is reached.
|
|
40
|
+
* @deprecated See the module comment. */
|
|
33
41
|
declare function pushSnapshot(stack: UndoStack, snapshot: unknown, maxDepth: number): UndoStack;
|
|
34
42
|
/** Step back one snapshot, returning the new history and the state to restore.
|
|
35
43
|
* `null` when there is nothing to undo. The caller supplies `current` so it
|
|
36
|
-
* can be redone.
|
|
44
|
+
* can be redone.
|
|
45
|
+
* @deprecated See the module comment. */
|
|
37
46
|
declare function undo(stack: UndoStack, current: unknown): {
|
|
38
47
|
stack: UndoStack;
|
|
39
48
|
snapshot: unknown;
|
|
40
49
|
} | null;
|
|
41
50
|
/** Step forward one snapshot, returning the new history and the state to
|
|
42
|
-
* restore. `null` when there is nothing to redo.
|
|
51
|
+
* restore. `null` when there is nothing to redo.
|
|
52
|
+
* @deprecated See the module comment. */
|
|
43
53
|
declare function redo(stack: UndoStack, current: unknown): {
|
|
44
54
|
stack: UndoStack;
|
|
45
55
|
snapshot: unknown;
|
|
46
56
|
} | null;
|
|
47
|
-
/** Discard the whole history.
|
|
57
|
+
/** Discard the whole history.
|
|
58
|
+
* @deprecated See the module comment. */
|
|
48
59
|
declare function clearUndo(_stack: UndoStack): UndoStack;
|
|
49
60
|
|
|
50
61
|
export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo };
|
package/dist/undo/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from '../chunk-
|
|
1
|
+
export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from '../chunk-UX66USTN.js';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/labkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "React widgets for building self-contained interactive lab pages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "orochi235",
|
|
@@ -115,18 +115,18 @@
|
|
|
115
115
|
"dev:3d": "LABKIT_EXAMPLE=3d-lab vite"
|
|
116
116
|
},
|
|
117
117
|
"peerDependencies": {
|
|
118
|
-
"@weasel-js/core": "1.
|
|
119
|
-
"@weasel-js/theme": "1.
|
|
118
|
+
"@weasel-js/core": "1.8.1",
|
|
119
|
+
"@weasel-js/theme": "1.8.1",
|
|
120
120
|
"react": "^19.0.0",
|
|
121
121
|
"react-dom": "^19.0.0"
|
|
122
122
|
},
|
|
123
123
|
"dependencies": {
|
|
124
|
-
"@weasel-js/geom": "1.
|
|
125
|
-
"@weasel-js/kernel3d": "1.
|
|
126
|
-
"@weasel-js/loupe": "1.
|
|
127
|
-
"@weasel-js/quantity": "1.
|
|
128
|
-
"@weasel-js/svg": "1.
|
|
129
|
-
"@weasel-js/ui": "1.
|
|
124
|
+
"@weasel-js/geom": "1.8.1",
|
|
125
|
+
"@weasel-js/kernel3d": "1.8.1",
|
|
126
|
+
"@weasel-js/loupe": "1.8.1",
|
|
127
|
+
"@weasel-js/quantity": "1.8.1",
|
|
128
|
+
"@weasel-js/svg": "1.8.1",
|
|
129
|
+
"@weasel-js/ui": "1.8.1",
|
|
130
130
|
"earcut": "3.2.3",
|
|
131
131
|
"polygon-clipping": "^0.15.7",
|
|
132
132
|
"react-aria-components": "^1.5.0",
|
|
@@ -136,8 +136,8 @@
|
|
|
136
136
|
"devDependencies": {
|
|
137
137
|
"@biomejs/biome": "^2.4.12",
|
|
138
138
|
"@testing-library/jest-dom": "^7.0.1",
|
|
139
|
-
"@weasel-js/guides": "1.
|
|
140
|
-
"@weasel-js/modes": "1.
|
|
139
|
+
"@weasel-js/guides": "1.8.1",
|
|
140
|
+
"@weasel-js/modes": "1.8.1",
|
|
141
141
|
"less": "^4.2.0",
|
|
142
142
|
"rollup-plugin-dts": "^6.4.1",
|
|
143
143
|
"tsx": "^4.19.0",
|
|
@@ -65,6 +65,7 @@ function surfaceFor(clears: SurfaceClear[]): SurfaceHandle {
|
|
|
65
65
|
invalidate: () => {},
|
|
66
66
|
invalidateAll: () => {},
|
|
67
67
|
invalidateRects: () => {},
|
|
68
|
+
invalidateBox: () => {},
|
|
68
69
|
registerTile: () => {},
|
|
69
70
|
registerClear: (_id, clear) => {
|
|
70
71
|
clears.push(clear);
|
|
@@ -76,6 +77,9 @@ function surfaceFor(clears: SurfaceClear[]): SurfaceHandle {
|
|
|
76
77
|
},
|
|
77
78
|
containerRef: () => {},
|
|
78
79
|
getContainer: () => container,
|
|
80
|
+
scope: () => {},
|
|
81
|
+
inScope: () => true,
|
|
82
|
+
subscribeScope: () => () => {},
|
|
79
83
|
} as SurfaceHandle;
|
|
80
84
|
}
|
|
81
85
|
|
|
@@ -20,7 +20,12 @@ import {
|
|
|
20
20
|
import { createPortal, flushSync } from 'react-dom';
|
|
21
21
|
import { CameraWheelContext } from '../canvas/CameraWheelContext';
|
|
22
22
|
import type { Rect } from '../surface/rect';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
useSurfaceCanvas,
|
|
25
|
+
useSurfaceOptional,
|
|
26
|
+
useTileId,
|
|
27
|
+
useTileInScope,
|
|
28
|
+
} from '../surface/useSurfaceTile';
|
|
24
29
|
import { createMarkDrawOne } from './drawOne';
|
|
25
30
|
import { boundsOf, roundPoints, toShape, type WorldRect } from './frac';
|
|
26
31
|
import { POINT_MARK_SHAPE } from './paint';
|
|
@@ -124,6 +129,9 @@ export function AnnotationOverlay({
|
|
|
124
129
|
// instrument declares the same target ids — so the tile is registered under
|
|
125
130
|
// the trial-scoped key, not under `id`.
|
|
126
131
|
const tileId = useTileId(id);
|
|
132
|
+
// Out of scope while another trial is expanded: nothing paints this tile
|
|
133
|
+
// then, and its box would sit over the expanded one.
|
|
134
|
+
const inScope = useTileInScope(tileId);
|
|
127
135
|
const lastTile = useRef<HTMLElement | null>(null);
|
|
128
136
|
useEffect(() => {
|
|
129
137
|
const el = target.ref.current ?? null;
|
|
@@ -248,6 +256,7 @@ export function AnnotationOverlay({
|
|
|
248
256
|
// Pass-through while the rail holds a tool that makes no marks, so the
|
|
249
257
|
// instrument under the overlay stays clickable.
|
|
250
258
|
data-idle={annotationToolInfo(activeToolId) ? undefined : ''}
|
|
259
|
+
hidden={!inScope}
|
|
251
260
|
role="application"
|
|
252
261
|
aria-label={`Annotations on ${id}`}
|
|
253
262
|
// biome-ignore lint/a11y/noNoninteractiveTabindex: role="application" IS the interactive case — a region with its own keymap. The rule's allowed-role list stops at the native widget roles and does not include it, and the gesture dispatcher listens on this element, so without focus no tool's keyboard binding ever fires.
|
|
@@ -44,12 +44,15 @@ function Harness({
|
|
|
44
44
|
toolId = 'rect',
|
|
45
45
|
onRegisterClear,
|
|
46
46
|
canvas = null,
|
|
47
|
+
onSurface,
|
|
47
48
|
}: {
|
|
48
49
|
toolId?: string;
|
|
49
50
|
/** Told each tile a clear is registered for, and handed the clear. */
|
|
50
51
|
onRegisterClear?: (id: string, clear: SurfaceClear) => void;
|
|
51
52
|
/** The shared buffer the overlays paint into. */
|
|
52
53
|
canvas?: HTMLCanvasElement | null;
|
|
54
|
+
/** Handed the surface, for a test that drives it. */
|
|
55
|
+
onSurface?: (surface: SurfaceHandle) => void;
|
|
53
56
|
}) {
|
|
54
57
|
const a = useRef<HTMLDivElement | null>(null);
|
|
55
58
|
const b = useRef<HTMLDivElement | null>(null);
|
|
@@ -68,6 +71,7 @@ function Harness({
|
|
|
68
71
|
[tiled, onRegisterClear],
|
|
69
72
|
);
|
|
70
73
|
const annotations = useRef(createAnnotationStore({ targets: () => [] })).current;
|
|
74
|
+
onSurface?.(surface);
|
|
71
75
|
|
|
72
76
|
const capability: AnnotationsCapability = {
|
|
73
77
|
targets: (): AnnotationTarget[] => [
|
|
@@ -178,6 +182,31 @@ describe('<AnnotationTargets>', () => {
|
|
|
178
182
|
}
|
|
179
183
|
});
|
|
180
184
|
|
|
185
|
+
it('hides the box of a target the surface is scoped away from', () => {
|
|
186
|
+
let surface: SurfaceHandle | null = null;
|
|
187
|
+
const { container, getByTestId } = render(
|
|
188
|
+
<Harness
|
|
189
|
+
onSurface={(s) => {
|
|
190
|
+
surface = s;
|
|
191
|
+
}}
|
|
192
|
+
/>,
|
|
193
|
+
{ wrapper: StrictMode },
|
|
194
|
+
);
|
|
195
|
+
act(() => {
|
|
196
|
+
vi.advanceTimersByTime(64);
|
|
197
|
+
});
|
|
198
|
+
const box = (id: string) =>
|
|
199
|
+
container.querySelector<HTMLElement>(`.lk-annotate__input[data-annotation-target="${id}"]`);
|
|
200
|
+
act(() => surface?.scope(getByTestId('a')));
|
|
201
|
+
expect(box('pane:a')?.hidden).toBe(false);
|
|
202
|
+
expect(box('pane:b')?.hidden).toBe(true);
|
|
203
|
+
act(() => surface?.scope(null));
|
|
204
|
+
expect(box('pane:b')?.hidden).toBe(false);
|
|
205
|
+
act(() => {
|
|
206
|
+
vi.advanceTimersByTime(64);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
181
210
|
it('hands a wheel over a target to the trial camera', () => {
|
|
182
211
|
// The input box is portalled into the surface container, so the wheel
|
|
183
212
|
// would otherwise bubble past the stage the picture sits on.
|
|
@@ -131,3 +131,38 @@ describe('<CanvasStack> zoom invariant', () => {
|
|
|
131
131
|
vi.restoreAllMocks();
|
|
132
132
|
});
|
|
133
133
|
});
|
|
134
|
+
|
|
135
|
+
describe('<CanvasStack ticks>', () => {
|
|
136
|
+
it('repaints only the timed layers on a tick', async () => {
|
|
137
|
+
const still: CanvasLayerDescriptor = { id: 'still', visible: true, render: vi.fn() };
|
|
138
|
+
const moving: CanvasLayerDescriptor = {
|
|
139
|
+
id: 'moving',
|
|
140
|
+
visible: true,
|
|
141
|
+
timed: true,
|
|
142
|
+
render: vi.fn(),
|
|
143
|
+
};
|
|
144
|
+
const listeners = new Set<() => void>();
|
|
145
|
+
const ticks = (fn: () => void) => {
|
|
146
|
+
listeners.add(fn);
|
|
147
|
+
return () => {
|
|
148
|
+
listeners.delete(fn);
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
const nextFrame = () => new Promise((r) => requestAnimationFrame(() => r(null)));
|
|
152
|
+
render(
|
|
153
|
+
<CanvasStack
|
|
154
|
+
layers={[still, moving]}
|
|
155
|
+
view={{ zoom: 1, pan: { x: 0, y: 0 } }}
|
|
156
|
+
onViewChange={vi.fn()}
|
|
157
|
+
ticks={ticks}
|
|
158
|
+
/>,
|
|
159
|
+
);
|
|
160
|
+
await nextFrame();
|
|
161
|
+
vi.mocked(still.render).mockClear();
|
|
162
|
+
vi.mocked(moving.render).mockClear();
|
|
163
|
+
for (const fn of listeners) fn();
|
|
164
|
+
await nextFrame();
|
|
165
|
+
expect(moving.render).toHaveBeenCalledTimes(1);
|
|
166
|
+
expect(still.render).not.toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -30,6 +30,8 @@ export interface CanvasStackProps {
|
|
|
30
30
|
/** A yoke to join, from `useYoke()`: cameras on one yoke share their tool
|
|
31
31
|
* and gesture in flight. Omitted, this one keeps its own. */
|
|
32
32
|
yoke?: Yoke;
|
|
33
|
+
/** Frames after each of which every `timed` layer repaints — a trial clock's. */
|
|
34
|
+
ticks?: (fn: () => void) => () => void;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/** Stacks one `<canvas>` per layer and drives them from a shared view, so a
|
|
@@ -50,6 +52,7 @@ export function CanvasStack({
|
|
|
50
52
|
onHitTest,
|
|
51
53
|
children,
|
|
52
54
|
yoke,
|
|
55
|
+
ticks,
|
|
53
56
|
}: CanvasStackProps) {
|
|
54
57
|
const view = normalize2DView(viewProp);
|
|
55
58
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -101,7 +104,15 @@ export function CanvasStack({
|
|
|
101
104
|
() => ({ view: camera, frame, element: () => containerRef.current }),
|
|
102
105
|
[camera, frame],
|
|
103
106
|
);
|
|
104
|
-
useLayerScheduler({
|
|
107
|
+
useLayerScheduler({
|
|
108
|
+
layers,
|
|
109
|
+
view,
|
|
110
|
+
frame,
|
|
111
|
+
canvasRefs: canvasMap,
|
|
112
|
+
size,
|
|
113
|
+
host: containerRef,
|
|
114
|
+
ticks,
|
|
115
|
+
});
|
|
105
116
|
|
|
106
117
|
const ctxValue = useMemo(
|
|
107
118
|
() => ({
|
|
@@ -8,6 +8,8 @@ import { DEFAULT_FRAME, type WorldFrame } from './worldSpec';
|
|
|
8
8
|
export interface CanvasLayerDescriptor {
|
|
9
9
|
id: string;
|
|
10
10
|
visible: boolean;
|
|
11
|
+
/** Repaint on every `ticks` frame, not only when `render` or the view change. */
|
|
12
|
+
timed?: boolean;
|
|
11
13
|
render: (ctx: CanvasRenderingContext2D, view: ViewTransform, frame: WorldFrame) => void;
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -21,6 +23,9 @@ interface SchedulerOptions {
|
|
|
21
23
|
/** The element the stack occupies. Given one, the scheduler also stops while
|
|
22
24
|
* the stack sits outside the viewport. */
|
|
23
25
|
host?: RefObject<Element | null>;
|
|
26
|
+
/** Subscribes to frames — a trial clock's — after each of which every timed
|
|
27
|
+
* layer repaints. Returns the unsubscribe. */
|
|
28
|
+
ticks?: (fn: () => void) => () => void;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export function useLayerScheduler({
|
|
@@ -30,6 +35,7 @@ export function useLayerScheduler({
|
|
|
30
35
|
canvasRefs,
|
|
31
36
|
size,
|
|
32
37
|
host,
|
|
38
|
+
ticks,
|
|
33
39
|
}: SchedulerOptions): void {
|
|
34
40
|
const dirty = useRef<Set<string>>(new Set());
|
|
35
41
|
const lastRenderRef = useRef<Map<string, CanvasLayerDescriptor['render']>>(new Map());
|
|
@@ -82,4 +88,12 @@ export function useLayerScheduler({
|
|
|
82
88
|
useEffect(() => {
|
|
83
89
|
if (dirty.current.size > 0) frameLoop.request();
|
|
84
90
|
});
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!ticks || !layers.some((l) => l.timed)) return;
|
|
94
|
+
return ticks(() => {
|
|
95
|
+
for (const layer of layers) if (layer.timed) dirty.current.add(layer.id);
|
|
96
|
+
frameLoop.request();
|
|
97
|
+
});
|
|
98
|
+
}, [ticks, layers, frameLoop]);
|
|
85
99
|
}
|
|
@@ -26,6 +26,9 @@ const ctx: TrialChromeContext = {
|
|
|
26
26
|
hasLoupe: false,
|
|
27
27
|
loupeOn: false,
|
|
28
28
|
toggleLoupe: () => {},
|
|
29
|
+
canExpand: false,
|
|
30
|
+
expanded: false,
|
|
31
|
+
toggleExpanded: () => {},
|
|
29
32
|
configFields: [],
|
|
30
33
|
configSchema: fromConfigFields([]),
|
|
31
34
|
config: {},
|
|
@@ -73,6 +76,17 @@ describe('builtinContributions', () => {
|
|
|
73
76
|
expect(ids({ ...bare, undo: {} })).toContain('redo');
|
|
74
77
|
});
|
|
75
78
|
|
|
79
|
+
it('contributes the expand toggle only inside a lightbox, ahead of the trial actions', () => {
|
|
80
|
+
expect(ids(bare)).not.toContain('expand');
|
|
81
|
+
expect(ids(bare, { ...ctx, canExpand: true })).toEqual([
|
|
82
|
+
'expand',
|
|
83
|
+
'clone',
|
|
84
|
+
'reset',
|
|
85
|
+
'snapshot',
|
|
86
|
+
'close',
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
|
|
76
90
|
it('contributes the loupe toggle only while a lens is mounted', () => {
|
|
77
91
|
expect(ids(bare)).not.toContain('loupe');
|
|
78
92
|
const mounted = builtinContributions(bare, { ...ctx, hasLoupe: true });
|
package/src/chrome/builtins.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CloneIcon,
|
|
3
3
|
CloseIcon,
|
|
4
|
+
CollapseIcon,
|
|
4
5
|
FitIcon,
|
|
6
|
+
FullscreenIcon,
|
|
5
7
|
LoupeIcon,
|
|
6
8
|
RedoIcon,
|
|
7
9
|
ResetIcon,
|
|
@@ -40,8 +42,9 @@ function groupFolds(
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/** Built-in ids contributed only while runtime state says so — the loupe
|
|
43
|
-
* toggle appears once a lens mounts, after the trial's first render
|
|
44
|
-
|
|
45
|
+
* toggle appears once a lens mounts, after the trial's first render, and the
|
|
46
|
+
* expand toggle only inside a lightbox. */
|
|
47
|
+
export const TRANSIENT_BUILTINS: ReadonlySet<string> = new Set(['loupe', 'expand']);
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
50
|
* The contributions a trial gets from what its instrument declared. This is
|
|
@@ -221,6 +224,18 @@ export function builtinContributions(
|
|
|
221
224
|
),
|
|
222
225
|
});
|
|
223
226
|
}
|
|
227
|
+
if (ctx.canExpand) {
|
|
228
|
+
out.push({
|
|
229
|
+
id: 'expand',
|
|
230
|
+
region: 'titlebar',
|
|
231
|
+
end: true,
|
|
232
|
+
item: {
|
|
233
|
+
icon: ctx.expanded ? CollapseIcon : FullscreenIcon,
|
|
234
|
+
label: ctx.expanded ? 'Close expanded view' : 'Expand trial',
|
|
235
|
+
onActivate: ctx.toggleExpanded,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
224
239
|
out.push({
|
|
225
240
|
id: 'clone',
|
|
226
241
|
region: 'titlebar',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { StatusBar } from '
|
|
1
|
+
import { StatusBar, StatusBarItem, StatusBarSpacer } from '@weasel-js/ui';
|
|
2
|
+
import { Fragment, type ReactNode } from 'react';
|
|
2
3
|
import type { RegionContribution, StatusReadout, TrialChromeContext } from '../types';
|
|
3
4
|
|
|
4
5
|
/** Props for `<StatusRegion>`. */
|
|
@@ -9,28 +10,33 @@ export interface StatusRegionProps<TCtx = TrialChromeContext> {
|
|
|
9
10
|
region?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
/** Lays a bar's readouts out.
|
|
13
|
+
/** Lays a bar's readouts out. The first one marked `end`, and every one after
|
|
14
|
+
* it, sits at the far end. */
|
|
13
15
|
export function StatusRegion<TCtx = TrialChromeContext>({
|
|
14
16
|
contributions,
|
|
15
17
|
ctx,
|
|
16
18
|
region = 'status',
|
|
17
19
|
}: StatusRegionProps<TCtx>) {
|
|
18
20
|
if (contributions.length === 0) return null;
|
|
21
|
+
let spaced = false;
|
|
19
22
|
return (
|
|
20
|
-
<StatusBar>
|
|
23
|
+
<StatusBar divided>
|
|
21
24
|
{contributions.map((c) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
let body: ReactNode;
|
|
26
|
+
let title: string | undefined;
|
|
27
|
+
if (c.render) body = c.render(ctx);
|
|
28
|
+
else if (c.region === region && c.item) {
|
|
29
|
+
const item = c.item as StatusReadout;
|
|
30
|
+
body = item.text;
|
|
31
|
+
title = item.title;
|
|
32
|
+
} else return null;
|
|
33
|
+
const spacer = c.end && !spaced;
|
|
34
|
+
if (spacer) spaced = true;
|
|
30
35
|
return (
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
|
|
36
|
+
<Fragment key={c.id}>
|
|
37
|
+
{spacer && <StatusBarSpacer />}
|
|
38
|
+
<StatusBarItem title={title}>{body}</StatusBarItem>
|
|
39
|
+
</Fragment>
|
|
34
40
|
);
|
|
35
41
|
})}
|
|
36
42
|
</StatusBar>
|
|
@@ -92,6 +92,34 @@ describe('StatusRegion', () => {
|
|
|
92
92
|
);
|
|
93
93
|
expect(screen.getByText('150%')).toBeInTheDocument();
|
|
94
94
|
});
|
|
95
|
+
|
|
96
|
+
it('pushes the first readout marked end, and the ones after it, past one spacer', () => {
|
|
97
|
+
const { container } = render(
|
|
98
|
+
<StatusRegion
|
|
99
|
+
contributions={[
|
|
100
|
+
{ id: 'a', region: 'status', item: { text: 'A' } },
|
|
101
|
+
{ id: 'b', region: 'status', end: true, item: { text: 'B' } },
|
|
102
|
+
{ id: 'c', region: 'status', end: true, item: { text: 'C' } },
|
|
103
|
+
]}
|
|
104
|
+
ctx={ctx}
|
|
105
|
+
/>,
|
|
106
|
+
);
|
|
107
|
+
const bar = container.querySelector('footer') as HTMLElement;
|
|
108
|
+
const kids = [...bar.children].map((el) =>
|
|
109
|
+
el.getAttribute('aria-hidden') === 'true' ? '|' : el.textContent,
|
|
110
|
+
);
|
|
111
|
+
expect(kids).toEqual(['A', '|', 'B', 'C']);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('keeps a readout title as its hover text', () => {
|
|
115
|
+
render(
|
|
116
|
+
<StatusRegion
|
|
117
|
+
contributions={[{ id: 'z', region: 'status', item: { text: '150%', title: 'zoom' } }]}
|
|
118
|
+
ctx={ctx}
|
|
119
|
+
/>,
|
|
120
|
+
);
|
|
121
|
+
expect(screen.getByText('150%').closest('[title]')?.getAttribute('title')).toBe('zoom');
|
|
122
|
+
});
|
|
95
123
|
});
|
|
96
124
|
|
|
97
125
|
describe('ViewportRegion', () => {
|
package/src/chrome/types.ts
CHANGED
|
@@ -169,6 +169,14 @@ export interface TrialChromeContext extends ToolSlotContext, SidebarSlotContext
|
|
|
169
169
|
loupeOn: boolean;
|
|
170
170
|
toggleLoupe: () => void;
|
|
171
171
|
|
|
172
|
+
/** Whether the trial sits in a `<Lightbox>` — every `<Workspace>` tile does
|
|
173
|
+
* unless the workspace turned it off. The expand toggle is offered only
|
|
174
|
+
* while it does. */
|
|
175
|
+
canExpand: boolean;
|
|
176
|
+
/** Whether the trial is showing in its lightbox. */
|
|
177
|
+
expanded: boolean;
|
|
178
|
+
toggleExpanded: () => void;
|
|
179
|
+
|
|
172
180
|
/** The instrument's controls, resolved against the lab's rules. Always
|
|
173
181
|
* populated: a legacy `configSchema()` is adapted into the same shape. */
|
|
174
182
|
configSchema: ResolvedConfig;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import '@weasel-js/theme/tokens.css';
|
|
2
|
+
import 'windease/styles.css';
|
|
3
|
+
import '../styles.less';
|
|
4
|
+
import './clock.browser.test.less';
|
|
5
|
+
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
6
|
+
import { afterEach, expect, test } from 'vitest';
|
|
7
|
+
import type { Instrument } from '../instrument/types';
|
|
8
|
+
import { Lab } from '../lab/Lab';
|
|
9
|
+
|
|
10
|
+
// The lab's frame loop syncing a trial clock and a timed layer repainting from
|
|
11
|
+
// it: real frames and real pixels, which only a browser has.
|
|
12
|
+
|
|
13
|
+
afterEach(cleanup);
|
|
14
|
+
|
|
15
|
+
/** Fills the view with a red that steps with the clock, 1 a frame-ish. */
|
|
16
|
+
const Ticking: Instrument = {
|
|
17
|
+
name: 'Ticking',
|
|
18
|
+
defaultConfig: () => ({}),
|
|
19
|
+
initialState: () => ({}),
|
|
20
|
+
clock: { rate: 1 },
|
|
21
|
+
canvas: {
|
|
22
|
+
layers: [
|
|
23
|
+
{
|
|
24
|
+
id: 'tick',
|
|
25
|
+
timed: true,
|
|
26
|
+
draw: (ctx, { elapsed }) => {
|
|
27
|
+
ctx.fillStyle = `rgb(${Math.floor(elapsed / 16) % 256}, 0, 0)`;
|
|
28
|
+
ctx.fillRect(-1e5, -1e5, 2e5, 2e5);
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
render: (ctx) => (
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={() => {
|
|
37
|
+
if (ctx.trial.clock) ctx.trial.clock.rate = 0;
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
pause
|
|
41
|
+
</button>
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The red channel at the middle of the layer's canvas. */
|
|
46
|
+
function red(): number {
|
|
47
|
+
const canvas = document.querySelector(
|
|
48
|
+
'.lk-clock-frame .lk-canvas-stack__canvas',
|
|
49
|
+
) as HTMLCanvasElement;
|
|
50
|
+
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
|
51
|
+
return ctx.getImageData(canvas.width >> 1, canvas.height >> 1, 1, 1).data[0] ?? -1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
55
|
+
|
|
56
|
+
test('a timed layer repaints as the clock plays, and holds once it pauses', async () => {
|
|
57
|
+
render(
|
|
58
|
+
<div className="lk-clock-frame">
|
|
59
|
+
<Lab title="T" instruments={[Ticking]} defaultInstrument="Ticking" />
|
|
60
|
+
</div>,
|
|
61
|
+
);
|
|
62
|
+
// Layout settles first — the first measurement places the view and repaints
|
|
63
|
+
// every layer — so only the clock can repaint after it.
|
|
64
|
+
await wait(500);
|
|
65
|
+
const a = red();
|
|
66
|
+
await wait(150);
|
|
67
|
+
const b = red();
|
|
68
|
+
await wait(150);
|
|
69
|
+
expect(b).not.toBe(a);
|
|
70
|
+
expect(red()).not.toBe(b);
|
|
71
|
+
|
|
72
|
+
fireEvent.click(screen.getByRole('button', { name: 'pause' }));
|
|
73
|
+
await wait(100);
|
|
74
|
+
const held = red();
|
|
75
|
+
await wait(300);
|
|
76
|
+
expect(red()).toBe(held);
|
|
77
|
+
});
|