@weasel-js/labkit 1.7.0 → 1.7.2
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 +3 -3
- package/dist/_dts/{CanvasStackContext-D2jnIwfk.d.ts → CanvasStackContext-Cjy6I-Iv.d.ts} +4 -1
- package/dist/_dts/{Stage-CCWfQQWM.d.ts → Stage-D6t7o0pF.d.ts} +3 -2
- package/dist/_dts/{index-CsYqUHD3.d.ts → index-BTmaRy6a.d.ts} +10 -6
- package/dist/_dts/{types-CSm9jgol.d.ts → index-DCMA4Fy1.d.ts} +59 -113
- package/dist/_dts/{types-CRPSGFAp.d.ts → types-BUBSIZuV.d.ts} +8 -7
- package/dist/_dts/{useTrialState-CTZPgeZ7.d.ts → useTrialState-Bh8bzFgE.d.ts} +1 -1
- package/dist/canvas/index.d.ts +8 -6
- package/dist/canvas/index.js +6 -6
- package/dist/chrome/index.d.ts +7 -5
- package/dist/chrome/index.js +10 -9
- package/dist/chunk-2ES4W47N.js +8 -0
- package/dist/chunk-2ES4W47N.js.map +1 -0
- package/dist/{chunk-WPLAANH4.js → chunk-2N7UBSS2.js} +12 -12
- package/dist/chunk-2N7UBSS2.js.map +1 -0
- package/dist/{chunk-275JRRVD.js → chunk-E23Q6A3Y.js} +232 -37
- package/dist/chunk-E23Q6A3Y.js.map +1 -0
- package/dist/{chunk-GOZLNM4L.js → chunk-E6NARGU7.js} +5 -5
- package/dist/{chunk-GOZLNM4L.js.map → chunk-E6NARGU7.js.map} +1 -1
- package/dist/{chunk-IJJUK4FU.js → chunk-EESJUSYR.js} +5 -6
- package/dist/chunk-EESJUSYR.js.map +1 -0
- package/dist/{chunk-HXXTULDN.js → chunk-EFXRM6XQ.js} +8 -11
- package/dist/chunk-EFXRM6XQ.js.map +1 -0
- package/dist/chunk-F4ACIK7Z.js +210 -0
- package/dist/chunk-F4ACIK7Z.js.map +1 -0
- package/dist/{chunk-AWQG7FCC.js → chunk-FG3EXZDV.js} +3 -3
- package/dist/{chunk-AWQG7FCC.js.map → chunk-FG3EXZDV.js.map} +1 -1
- package/dist/{chunk-YT75VYRD.js → chunk-IBVRMHW5.js} +5 -5
- package/dist/{chunk-YT75VYRD.js.map → chunk-IBVRMHW5.js.map} +1 -1
- package/dist/{chunk-RWNDDKOH.js → chunk-PMLZP7RT.js} +18 -15
- package/dist/chunk-PMLZP7RT.js.map +1 -0
- package/dist/{chunk-Y23G2VZL.js → chunk-READTDQX.js} +2 -2
- package/dist/chunk-READTDQX.js.map +1 -0
- package/dist/{chunk-I5DUQ5HM.js → chunk-RODIY7HI.js} +75 -72
- package/dist/chunk-RODIY7HI.js.map +1 -0
- package/dist/{chunk-JVBYV6TL.js → chunk-ROGJWDQA.js} +3 -3
- package/dist/{chunk-JVBYV6TL.js.map → chunk-ROGJWDQA.js.map} +1 -1
- package/dist/{chunk-ZIFPHIWZ.js → chunk-T6DLGEJB.js} +12 -14
- package/dist/chunk-T6DLGEJB.js.map +1 -0
- package/dist/chunk-U6UQVGFP.js +15583 -0
- package/dist/chunk-U6UQVGFP.js.map +1 -0
- package/dist/{chunk-24W5VEJW.js → chunk-ULYYNWGP.js} +2 -2
- package/dist/{chunk-24W5VEJW.js.map → chunk-ULYYNWGP.js.map} +1 -1
- package/dist/{chunk-UNAZSLWE.js → chunk-VYVGVBGY.js} +17 -20
- package/dist/chunk-VYVGVBGY.js.map +1 -0
- package/dist/chunk-WFALF2X2.js +302 -0
- package/dist/chunk-WFALF2X2.js.map +1 -0
- package/dist/{chunk-KMJ7RWD3.js → chunk-WLMDTI2Q.js} +3 -3
- package/dist/{chunk-KMJ7RWD3.js.map → chunk-WLMDTI2Q.js.map} +1 -1
- package/dist/{chunk-T5IBSPAM.js → chunk-WWJDPNZZ.js} +43 -33
- package/dist/chunk-WWJDPNZZ.js.map +1 -0
- package/dist/{chunk-J7OWWTCW.js → chunk-XE5HJOPL.js} +5 -6
- package/dist/chunk-XE5HJOPL.js.map +1 -0
- package/dist/{chunk-7BO7HNGF.js → chunk-XRX5S5NT.js} +8 -61
- package/dist/chunk-XRX5S5NT.js.map +1 -0
- package/dist/chunk-XVUKDSYV.js +38 -0
- package/dist/chunk-XVUKDSYV.js.map +1 -0
- package/dist/{chunk-FMK7UQ6V.js → chunk-XZDCEMEL.js} +64 -47
- package/dist/chunk-XZDCEMEL.js.map +1 -0
- package/dist/{chunk-FZ7YFQN7.js → chunk-YCFKW5RX.js} +3 -3
- package/dist/chunk-YCFKW5RX.js.map +1 -0
- package/dist/{chunk-CQLQPQ4P.js → chunk-ZQIHJXP4.js} +2 -2
- package/dist/chunk-ZQIHJXP4.js.map +1 -0
- package/dist/config/index.d.ts +18 -10
- package/dist/config/index.js +4 -4
- package/dist/controls/index.d.ts +58 -9
- package/dist/controls/index.js +7 -5
- package/dist/dragdrop/index.d.ts +23 -3
- package/dist/dragdrop/index.js +4 -3
- package/dist/fonts/oswald-tabular-digits.woff2 +0 -0
- package/dist/index.d.ts +61 -715
- package/dist/index.js +456 -432
- package/dist/index.js.map +1 -1
- package/dist/job/index.d.ts +8 -0
- package/dist/job/index.js +1 -1
- package/dist/layers/index.d.ts +6 -5
- package/dist/layers/index.js +3 -2
- package/dist/loupe/index.d.ts +100 -30
- package/dist/loupe/index.js +585 -6
- package/dist/loupe/index.js.map +1 -1
- package/dist/overview/index.d.ts +4 -2
- package/dist/overview/index.js +15 -14
- package/dist/overview/index.js.map +1 -1
- package/dist/passthrough/weasel-canvas.d.ts +2 -0
- package/dist/passthrough/weasel-ui.d.ts +2 -2871
- package/dist/passthrough/weasel-ui.js +3 -2
- package/dist/primitives/index.d.ts +213 -3
- package/dist/primitives/index.js +8 -7
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +6 -6
- package/dist/styles.css +284 -51
- package/dist/surface/index.d.ts +10 -2
- package/dist/surface/index.js +1 -1
- package/dist/undo/index.d.ts +5 -3
- package/package.json +11 -11
- package/src/annotations/AnnotationOverlay.abandon.test.tsx +143 -0
- package/src/annotations/AnnotationOverlay.tsx +50 -31
- package/src/annotations/Annotations.overlay.test.tsx +31 -0
- package/src/annotations/capture.browser.test.ts +47 -0
- package/src/annotations/capture.ts +14 -7
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/frac.test.ts +44 -1
- package/src/annotations/frac.ts +46 -18
- package/src/annotations/index.ts +1 -0
- package/src/annotations/paint.test.ts +74 -33
- package/src/annotations/paint.ts +70 -32
- package/src/annotations/store.test.ts +212 -2
- package/src/annotations/store.ts +130 -27
- package/src/annotations/svgNodes.test.ts +22 -10
- package/src/annotations/svgNodes.ts +3 -3
- package/src/annotations/types.ts +29 -10
- package/src/canvas/CameraInput.test.tsx +30 -0
- package/src/canvas/CameraInput.tsx +16 -15
- package/src/canvas/CanvasStack.tsx +5 -5
- package/src/canvas/CanvasStackContext.ts +4 -1
- package/src/canvas/Stage.test.tsx +34 -25
- package/src/canvas/Stage.tsx +3 -3
- package/src/canvas/cameraRegistry.ts +1 -0
- package/src/canvas/worldSpec.ts +3 -1
- package/src/chrome/builtins.test.ts +7 -6
- package/src/chrome/builtins.tsx +5 -1
- package/src/chrome/index.ts +1 -1
- package/src/chrome/labRegions.test.tsx +37 -26
- package/src/chrome/merge.test.ts +5 -0
- package/src/chrome/merge.ts +4 -2
- package/src/chrome/regions/PaletteRegion.test.tsx +4 -5
- package/src/chrome/regions/PaletteRegion.tsx +3 -4
- package/src/chrome/regions/ViewportRegion.browser.test.less +6 -0
- package/src/chrome/regions/ViewportRegion.browser.test.tsx +38 -0
- package/src/chrome/regions/ViewportRegion.less +6 -0
- package/src/chrome/regions/ViewportRegion.tsx +10 -4
- package/src/chrome/types.ts +4 -2
- package/src/config/auto.ts +1 -0
- package/src/config/builder.ts +6 -0
- package/src/config/types.ts +2 -1
- package/src/controls/ControlMatrix.less +245 -0
- package/src/controls/ControlMatrix.stories.less +13 -0
- package/src/controls/ControlMatrix.stories.tsx +135 -0
- package/src/controls/ControlMatrix.test.tsx +206 -0
- package/src/controls/ControlMatrix.tsx +446 -0
- package/src/controls/ControlPanel.test.tsx +37 -3
- package/src/controls/ControlPanel.tsx +16 -5
- package/src/controls/index.ts +6 -0
- package/src/dragdrop/DragDropRuntime.tsx +22 -13
- package/src/dragdrop/DragGhost.tsx +3 -0
- package/src/dragdrop/Palette.tsx +2 -0
- package/src/dragdrop/dragDrop.test.tsx +34 -33
- package/src/index.test.ts +5 -0
- package/src/index.ts +7 -1
- package/src/instrument/types.ts +0 -6
- package/src/job/useJob.ts +14 -9
- package/src/lab/Lab.chrome.test.tsx +15 -10
- package/src/lab/Lab.nebula.test.tsx +5 -5
- package/src/lab/Lab.surface.test.tsx +30 -21
- package/src/lab/Lab.test.tsx +75 -74
- package/src/lab/Lab.tsx +5 -1
- package/src/lab/Lab.undock.test.tsx +15 -10
- package/src/lab/LabHeader.test.tsx +15 -14
- package/src/lab/LabPalette.test.tsx +12 -7
- package/src/lab/LabShell.less +1 -1
- package/src/lab/LabSwitcher.browser.test.tsx +27 -0
- package/src/lab/LabSwitcher.less +2 -0
- package/src/lab/LabZoom.test.tsx +55 -44
- package/src/lab/LabZoom.tsx +4 -4
- package/src/lab/Workspace.surface.test.tsx +16 -13
- package/src/lab/Workspace.test.tsx +32 -27
- package/src/lab/Workspace.tsx +48 -52
- package/src/loupe/AGENTS.md +28 -15
- package/src/loupe/CanvasLoupe.tsx +2 -3
- package/src/loupe/DomLoupe.tsx +1 -7
- package/src/loupe/Loupe.less +16 -0
- package/src/loupe/TrialLoupe.tsx +120 -57
- package/src/loupe/index.ts +1 -6
- package/src/loupe/types.test.ts +3 -3
- package/src/loupe/types.ts +24 -27
- package/src/loupe/useLoupe.test.tsx +29 -4
- package/src/loupe/useLoupe.ts +20 -17
- package/src/overview/OverviewMarks.test.tsx +9 -8
- package/src/overview/OverviewMarks.tsx +12 -4
- package/src/overview/TrialOverview.test.tsx +18 -17
- package/src/overview/TrialOverview.tsx +3 -3
- package/src/passthrough/weasel-ui.test.ts +1 -1
- package/src/passthrough/weasel-ui.ts +1 -1
- package/src/primitives/FloatingPanel.tsx +7 -3
- package/src/primitives/FpsMeter.less +1 -2
- package/src/primitives/JobProgress.less +1 -1
- package/src/primitives/Readout.test.tsx +2 -2
- package/src/primitives/ScaleIndicator.less +1 -1
- package/src/primitives/Split.tsx +69 -41
- package/src/primitives/StatusBar.less +1 -1
- package/src/primitives/Toolbar.tsx +3 -3
- package/src/primitives/ZoomControl.less +1 -2
- package/src/specimen/Specimen.less +1 -0
- package/src/specimen/Specimen.tsx +0 -1
- package/src/state/undock.ts +3 -0
- package/src/state/useOpenOnce.ts +20 -16
- package/src/state/view.ts +1 -0
- package/src/styles.less +3 -0
- package/src/surface/index.ts +1 -1
- package/src/surface/useTiledSurface.test.tsx +14 -0
- package/src/surface/useTiledSurface.ts +11 -3
- package/src/trial/Trial.annotations.persist.test.tsx +20 -9
- package/src/trial/Trial.annotations.test.tsx +105 -27
- package/src/trial/Trial.canvas.test.tsx +21 -17
- package/src/trial/Trial.config.test.tsx +34 -27
- package/src/trial/Trial.less +0 -16
- package/src/trial/Trial.loupe.test.tsx +51 -25
- package/src/trial/Trial.targets.test.tsx +9 -8
- package/src/trial/Trial.test.tsx +51 -46
- package/src/trial/Trial.trialId.test.tsx +7 -6
- package/src/trial/Trial.tsx +74 -62
- package/src/trial/TrialChrome.tsx +6 -3
- package/src/trial/index.ts +2 -0
- package/src/trial/loupeSwitch.ts +15 -0
- package/dist/_dts/LayerList.d-DiuEArKq.d.ts +0 -83
- package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +0 -79
- package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +0 -141
- package/dist/_dts/Select.d-Bensr_MT.d.ts +0 -579
- package/dist/_dts/index-B2Yj5aA0.d.ts +0 -274
- package/dist/chunk-275JRRVD.js.map +0 -1
- package/dist/chunk-4LJSC7MB.js +0 -143
- package/dist/chunk-4LJSC7MB.js.map +0 -1
- package/dist/chunk-7BO7HNGF.js.map +0 -1
- package/dist/chunk-CQLQPQ4P.js.map +0 -1
- package/dist/chunk-FMK7UQ6V.js.map +0 -1
- package/dist/chunk-FZ7YFQN7.js.map +0 -1
- package/dist/chunk-HXXTULDN.js.map +0 -1
- package/dist/chunk-I5DUQ5HM.js.map +0 -1
- package/dist/chunk-IJJUK4FU.js.map +0 -1
- package/dist/chunk-J7OWWTCW.js.map +0 -1
- package/dist/chunk-M7HP3FTK.js +0 -549
- package/dist/chunk-M7HP3FTK.js.map +0 -1
- package/dist/chunk-MIM5R4ZT.js +0 -14972
- package/dist/chunk-MIM5R4ZT.js.map +0 -1
- package/dist/chunk-RWNDDKOH.js.map +0 -1
- package/dist/chunk-T5IBSPAM.js.map +0 -1
- package/dist/chunk-UNAZSLWE.js.map +0 -1
- package/dist/chunk-WPLAANH4.js.map +0 -1
- package/dist/chunk-Y23G2VZL.js.map +0 -1
- package/dist/chunk-ZIFPHIWZ.js.map +0 -1
- package/src/primitives/useRovingTabIndex.test.ts +0 -48
- package/src/primitives/useRovingTabIndex.ts +0 -88
|
@@ -2,11 +2,14 @@ import { type ReactPortal, useEffect, useState } from 'react';
|
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import type { PaletteItem, Point } from '../instrument/types';
|
|
4
4
|
|
|
5
|
+
/** Props for `DragGhost`. `screenPos` is in client pixels. */
|
|
5
6
|
export interface DragGhostProps {
|
|
6
7
|
item: PaletteItem;
|
|
7
8
|
screenPos: Point;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
/** The dragged item's label, portaled to `document.body` just below-right of
|
|
12
|
+
* `screenPos`. Renders nothing until mounted, so it is SSR-safe. */
|
|
10
13
|
export function DragGhost({ item, screenPos }: DragGhostProps): ReactPortal | null {
|
|
11
14
|
const [mounted, setMounted] = useState(false);
|
|
12
15
|
useEffect(() => setMounted(true), []);
|
package/src/dragdrop/Palette.tsx
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { PointerEvent as ReactPointerEvent } from 'react';
|
|
2
2
|
import type { PaletteItem } from '../instrument/types';
|
|
3
3
|
|
|
4
|
+
/** Props for `Palette`. `onDragStart` is usually `useDragDrop`'s `startDrag`. */
|
|
4
5
|
export interface PaletteProps {
|
|
5
6
|
items: PaletteItem[];
|
|
6
7
|
onDragStart: (item: PaletteItem, e: ReactPointerEvent) => void;
|
|
7
8
|
className?: string;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
/** One button per item, each starting a drag on pointerdown. */
|
|
10
12
|
export function Palette({ items, onDragStart, className }: PaletteProps) {
|
|
11
13
|
return (
|
|
12
14
|
<div className={className ? `lk-palette ${className}` : 'lk-palette'}>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { act, fireEvent,
|
|
1
|
+
import { act, fireEvent, screen } from '@testing-library/react';
|
|
2
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
2
3
|
import { useContext } from 'react';
|
|
3
4
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
5
|
import { useStore } from 'zustand/react';
|
|
@@ -51,8 +52,8 @@ const centredInstrument = defineInstrument<DropState, Record<string, never>>({
|
|
|
51
52
|
},
|
|
52
53
|
});
|
|
53
54
|
|
|
54
|
-
function renderLab(probe?: (state: DropState) => void, instrument = testInstrument) {
|
|
55
|
-
return
|
|
55
|
+
async function renderLab(probe?: (state: DropState) => void, instrument = testInstrument) {
|
|
56
|
+
return renderSettled(
|
|
56
57
|
<Lab instruments={[instrument]} defaultInstrument={instrument.name}>
|
|
57
58
|
{probe ? <StateProbe onState={probe} /> : null}
|
|
58
59
|
</Lab>,
|
|
@@ -88,104 +89,104 @@ function pointerDown(el: Element, x: number, y: number) {
|
|
|
88
89
|
|
|
89
90
|
/** The session listens on the document, so that is where the rest of the
|
|
90
91
|
* gesture has to arrive — a dispatch on `window` reaches nothing. */
|
|
91
|
-
function dispatchOnDocument(type: string, init: PointerEventInit) {
|
|
92
|
-
act(() => {
|
|
92
|
+
async function dispatchOnDocument(type: string, init: PointerEventInit) {
|
|
93
|
+
await act(async () => {
|
|
93
94
|
document.dispatchEvent(new PointerEvent(type, { pointerId: 1, bubbles: true, ...init }));
|
|
94
95
|
});
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
function pointerMove(x: number, y: number) {
|
|
98
|
-
dispatchOnDocument('pointermove', { buttons: 1, clientX: x, clientY: y });
|
|
98
|
+
async function pointerMove(x: number, y: number) {
|
|
99
|
+
await dispatchOnDocument('pointermove', { buttons: 1, clientX: x, clientY: y });
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function pointerUp(x: number, y: number) {
|
|
102
|
-
dispatchOnDocument('pointerup', { clientX: x, clientY: y });
|
|
102
|
+
async function pointerUp(x: number, y: number) {
|
|
103
|
+
await dispatchOnDocument('pointerup', { clientX: x, clientY: y });
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
describe('DragDrop integration', () => {
|
|
106
|
-
it('renders palette item in the sidebar', () => {
|
|
107
|
-
renderLab();
|
|
107
|
+
it('renders palette item in the sidebar', async () => {
|
|
108
|
+
await renderLab();
|
|
108
109
|
expect(screen.getByRole('button', { name: 'Item A' })).toBeInTheDocument();
|
|
109
110
|
});
|
|
110
111
|
|
|
111
|
-
it('drops an item onto the canvas and appends it to state', () => {
|
|
112
|
+
it('drops an item onto the canvas and appends it to state', async () => {
|
|
112
113
|
let latest: DropState = { items: [] };
|
|
113
|
-
renderLab((s) => {
|
|
114
|
+
await renderLab((s) => {
|
|
114
115
|
latest = s;
|
|
115
116
|
});
|
|
116
117
|
const palette = screen.getByRole('button', { name: 'Item A' });
|
|
117
118
|
|
|
118
|
-
act(() => {
|
|
119
|
+
await act(async () => {
|
|
119
120
|
pointerDown(palette, 0, 0);
|
|
120
121
|
});
|
|
121
|
-
pointerUp(250, 250);
|
|
122
|
+
await pointerUp(250, 250);
|
|
122
123
|
|
|
123
124
|
expect(latest.items).toHaveLength(1);
|
|
124
125
|
expect(latest.items[0]?.id).toBe('a');
|
|
125
126
|
});
|
|
126
127
|
|
|
127
|
-
it('drop outside the canvas leaves state unchanged', () => {
|
|
128
|
+
it('drop outside the canvas leaves state unchanged', async () => {
|
|
128
129
|
let latest: DropState = { items: [] };
|
|
129
|
-
renderLab((s) => {
|
|
130
|
+
await renderLab((s) => {
|
|
130
131
|
latest = s;
|
|
131
132
|
});
|
|
132
133
|
const palette = screen.getByRole('button', { name: 'Item A' });
|
|
133
134
|
|
|
134
|
-
act(() => {
|
|
135
|
+
await act(async () => {
|
|
135
136
|
pointerDown(palette, 0, 0);
|
|
136
137
|
});
|
|
137
|
-
pointerMove(10, 10);
|
|
138
|
-
pointerUp(10, 10);
|
|
138
|
+
await pointerMove(10, 10);
|
|
139
|
+
await pointerUp(10, 10);
|
|
139
140
|
|
|
140
141
|
expect(latest.items).toHaveLength(0);
|
|
141
142
|
expect(document.querySelector('.lk-drag-ghost')).toBeNull();
|
|
142
143
|
});
|
|
143
144
|
|
|
144
|
-
it('shows drag ghost during drag and clears on drop', () => {
|
|
145
|
-
renderLab();
|
|
145
|
+
it('shows drag ghost during drag and clears on drop', async () => {
|
|
146
|
+
await renderLab();
|
|
146
147
|
const palette = screen.getByRole('button', { name: 'Item A' });
|
|
147
148
|
|
|
148
|
-
act(() => {
|
|
149
|
+
await act(async () => {
|
|
149
150
|
pointerDown(palette, 5, 5);
|
|
150
151
|
});
|
|
151
152
|
expect(document.querySelector('.lk-drag-ghost')).not.toBeNull();
|
|
152
153
|
|
|
153
|
-
pointerUp(250, 250);
|
|
154
|
+
await pointerUp(250, 250);
|
|
154
155
|
expect(document.querySelector('.lk-drag-ghost')).toBeNull();
|
|
155
156
|
});
|
|
156
157
|
|
|
157
|
-
it('a cancelled gesture drops nothing and clears the ghost', () => {
|
|
158
|
+
it('a cancelled gesture drops nothing and clears the ghost', async () => {
|
|
158
159
|
let latest: DropState = { items: [] };
|
|
159
|
-
renderLab((s) => {
|
|
160
|
+
await renderLab((s) => {
|
|
160
161
|
latest = s;
|
|
161
162
|
});
|
|
162
163
|
const palette = screen.getByRole('button', { name: 'Item A' });
|
|
163
164
|
|
|
164
|
-
act(() => {
|
|
165
|
+
await act(async () => {
|
|
165
166
|
pointerDown(palette, 0, 0);
|
|
166
167
|
});
|
|
167
|
-
pointerMove(250, 250);
|
|
168
|
+
await pointerMove(250, 250);
|
|
168
169
|
expect(document.querySelector('.lk-drag-ghost')).not.toBeNull();
|
|
169
170
|
|
|
170
|
-
dispatchOnDocument('pointercancel', {});
|
|
171
|
+
await dispatchOnDocument('pointercancel', {});
|
|
171
172
|
|
|
172
173
|
expect(latest.items).toHaveLength(0);
|
|
173
174
|
expect(document.querySelector('.lk-drag-ghost')).toBeNull();
|
|
174
175
|
});
|
|
175
176
|
|
|
176
|
-
it('drops through the instrument world spec, not the canvas top-left', () => {
|
|
177
|
+
it('drops through the instrument world spec, not the canvas top-left', async () => {
|
|
177
178
|
let latest: DropState = { items: [] };
|
|
178
|
-
renderLab((s) => {
|
|
179
|
+
await renderLab((s) => {
|
|
179
180
|
latest = s;
|
|
180
181
|
}, centredInstrument);
|
|
181
182
|
const palette = screen.getByRole('button', { name: 'Item A' });
|
|
182
183
|
|
|
183
|
-
act(() => {
|
|
184
|
+
await act(async () => {
|
|
184
185
|
pointerDown(palette, 0, 0);
|
|
185
186
|
});
|
|
186
187
|
// The host spans (100,100)-(500,500), so its centre is at (300,300).
|
|
187
188
|
// Dropping 50px right of centre and 50px above it, at zoom 1, y up.
|
|
188
|
-
pointerUp(350, 250);
|
|
189
|
+
await pointerUp(350, 250);
|
|
189
190
|
|
|
190
191
|
expect(latest.items[0]).toMatchObject({ x: 50, y: 50 });
|
|
191
192
|
});
|
package/src/index.test.ts
CHANGED
|
@@ -49,6 +49,11 @@ describe('public export surface', () => {
|
|
|
49
49
|
expect(labkit).toHaveProperty('useTrialState');
|
|
50
50
|
expect(labkit).toHaveProperty('useTrialId');
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
it('leaves the loupe to its own entry, so a lab without one does not load it', () => {
|
|
54
|
+
expect(labkit).not.toHaveProperty('TrialLoupe');
|
|
55
|
+
expect(labkit).not.toHaveProperty('useLoupe');
|
|
56
|
+
});
|
|
52
57
|
});
|
|
53
58
|
|
|
54
59
|
describe('ui re-exports', () => {
|
package/src/index.ts
CHANGED
|
@@ -80,6 +80,7 @@ export type {
|
|
|
80
80
|
AnnotationMeaning,
|
|
81
81
|
AnnotationPatch,
|
|
82
82
|
AnnotationQuery,
|
|
83
|
+
AnnotationSelectionMode,
|
|
83
84
|
AnnotationStatus,
|
|
84
85
|
AnnotationStoreOptions,
|
|
85
86
|
AnnotationsApi,
|
|
@@ -228,6 +229,12 @@ export type {
|
|
|
228
229
|
export { useConfigSchema } from './config/useConfigSchema';
|
|
229
230
|
export { useResolvedConfig } from './config/useResolvedConfig';
|
|
230
231
|
export { isLeafVisible } from './config/visible';
|
|
232
|
+
export {
|
|
233
|
+
ControlMatrix,
|
|
234
|
+
type ControlMatrixColumn,
|
|
235
|
+
type ControlMatrixProps,
|
|
236
|
+
type ControlMatrixRow,
|
|
237
|
+
} from './controls/ControlMatrix';
|
|
231
238
|
export { ControlPanel } from './controls/ControlPanel';
|
|
232
239
|
export { type InDialogOptions, inDialog, summarizeValue } from './controls/inDialog';
|
|
233
240
|
export type {
|
|
@@ -267,7 +274,6 @@ export * from './job';
|
|
|
267
274
|
export * from './lab';
|
|
268
275
|
export type { LayerListItem, LayerListProps, LayerMove } from './layers';
|
|
269
276
|
export { LayerList, moveLayers } from './layers';
|
|
270
|
-
export * from './loupe';
|
|
271
277
|
export * from './primitives';
|
|
272
278
|
export { SPECIMEN_SECTIONS, Specimen } from './specimen/Specimen';
|
|
273
279
|
export {
|
package/src/instrument/types.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { TrialContribution } from '../chrome/types';
|
|
|
6
6
|
import type { ConfigPath, ConfigSchema } from '../config/types';
|
|
7
7
|
import type { ConfigField } from '../controls/types';
|
|
8
8
|
import type { JobCapability, JobHandle } from '../job/types';
|
|
9
|
-
import type { LoupeDeclaration } from '../loupe/types';
|
|
10
9
|
import type { TrialInfo } from '../state/types';
|
|
11
10
|
import type { ToolCapability } from '../tools/types';
|
|
12
11
|
|
|
@@ -182,11 +181,6 @@ export interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
|
|
|
182
181
|
* is allowed to mean. Declaring it is what makes the trial provide the
|
|
183
182
|
* annotation overlay and its chrome. */
|
|
184
183
|
annotations?: AnnotationsCapability<TS, TC>;
|
|
185
|
-
/** Magnification. `true` takes every default and re-draws the instrument's
|
|
186
|
-
* canvas layers through a zoomed camera; an instrument whose content is DOM
|
|
187
|
-
* gives a `render` that draws it again at a camera it is handed. A function
|
|
188
|
-
* is re-read as the config changes, so a setting can drive the lens. */
|
|
189
|
-
loupe?: LoupeDeclaration<TS, TC> | ((config: TC) => LoupeDeclaration<TS, TC>);
|
|
190
184
|
/** Chrome this instrument contributes beyond what its capabilities imply. */
|
|
191
185
|
chrome?: TrialContribution[];
|
|
192
186
|
/** Work too slow to do during a render. The runtime starts it, aborts it on
|
package/src/job/useJob.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { useLatest } from '@weasel-js/core';
|
|
1
2
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
3
|
import type { JobCapability, JobFailure, JobHandle, JobStatus } from './types';
|
|
3
4
|
|
|
5
|
+
/** Inputs to `useJob`. `setState` receives each item folded in by the
|
|
6
|
+
* capability's `onItem`. */
|
|
4
7
|
export interface UseJobOptions<TS, TC, TItem> {
|
|
5
8
|
capability: JobCapability<TS, TC, TItem>;
|
|
6
9
|
config: TC;
|
|
@@ -23,6 +26,12 @@ function sameKey(a: readonly unknown[] | null, b: readonly unknown[] | null): bo
|
|
|
23
26
|
return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Drive a `JobCapability` and report its progress. `start` cancels any run in
|
|
31
|
+
* flight and begins again; results from a superseded or canceled run are
|
|
32
|
+
* dropped. With `auto`, it starts on mount and again whenever `key` changes
|
|
33
|
+
* element-wise. Unmounting aborts the run.
|
|
34
|
+
*/
|
|
26
35
|
export function useJob<TS, TC, TItem>({
|
|
27
36
|
capability,
|
|
28
37
|
config,
|
|
@@ -38,14 +47,10 @@ export function useJob<TS, TC, TItem>({
|
|
|
38
47
|
|
|
39
48
|
// Read through refs: `run` is called once per run and must see the values as of
|
|
40
49
|
// that moment rather than re-subscribing on every render.
|
|
41
|
-
const capRef =
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const stateRef = useRef(state);
|
|
46
|
-
stateRef.current = state;
|
|
47
|
-
const setStateRef = useRef(setState);
|
|
48
|
-
setStateRef.current = setState;
|
|
50
|
+
const capRef = useLatest(capability);
|
|
51
|
+
const configRef = useLatest(config);
|
|
52
|
+
const stateRef = useLatest(state);
|
|
53
|
+
const setStateRef = useLatest(setState);
|
|
49
54
|
|
|
50
55
|
const cancel = useCallback(() => {
|
|
51
56
|
token.current += 1;
|
|
@@ -96,7 +101,7 @@ export function useJob<TS, TC, TItem>({
|
|
|
96
101
|
}));
|
|
97
102
|
}
|
|
98
103
|
})();
|
|
99
|
-
}, []);
|
|
104
|
+
}, [capRef, configRef, setStateRef, stateRef]);
|
|
100
105
|
|
|
101
106
|
// Re-run when the declared key changes. `auto` covers the first mount too.
|
|
102
107
|
const lastKey = useRef<readonly unknown[] | null>(null);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
2
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import type { TrialContribution } from '../chrome/types';
|
|
4
5
|
import type { Instrument } from '../instrument/types';
|
|
@@ -19,23 +20,27 @@ function silenceRenderError(): void {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
describe('<Lab> chrome', () => {
|
|
22
|
-
it('renders a consumer contribution', () => {
|
|
23
|
+
it('renders a consumer contribution', async () => {
|
|
23
24
|
const extra: TrialContribution = {
|
|
24
25
|
id: 'export',
|
|
25
26
|
region: 'toolbar',
|
|
26
27
|
item: { icon: Glyph, label: 'Export', onActivate: () => {} },
|
|
27
28
|
};
|
|
28
|
-
|
|
29
|
+
await renderSettled(
|
|
30
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" chrome={[extra]} />,
|
|
31
|
+
);
|
|
29
32
|
expect(screen.getByRole('button', { name: 'Export' })).toBeInTheDocument();
|
|
30
33
|
});
|
|
31
34
|
|
|
32
|
-
it('suppresses a built-in by id', () => {
|
|
33
|
-
|
|
35
|
+
it('suppresses a built-in by id', async () => {
|
|
36
|
+
await renderSettled(
|
|
37
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" suppress={['snapshot']} />,
|
|
38
|
+
);
|
|
34
39
|
expect(screen.queryByRole('button', { name: 'Save snapshot' })).toBeNull();
|
|
35
40
|
expect(screen.getByRole('button', { name: 'Clone trial' })).toBeInTheDocument();
|
|
36
41
|
});
|
|
37
42
|
|
|
38
|
-
it('throws when a consumer id collides with a built-in', () => {
|
|
43
|
+
it('throws when a consumer id collides with a built-in', async () => {
|
|
39
44
|
silenceRenderError();
|
|
40
45
|
const clash: TrialContribution = {
|
|
41
46
|
id: 'clone',
|
|
@@ -47,24 +52,24 @@ describe('<Lab> chrome', () => {
|
|
|
47
52
|
).toThrow(/duplicate contribution id "clone"/);
|
|
48
53
|
});
|
|
49
54
|
|
|
50
|
-
it('throws when suppressing an id that is not there', () => {
|
|
55
|
+
it('throws when suppressing an id that is not there', async () => {
|
|
51
56
|
silenceRenderError();
|
|
52
57
|
expect(() =>
|
|
53
58
|
render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" suppress={['nope']} />),
|
|
54
59
|
).toThrow(/cannot suppress "nope"/);
|
|
55
60
|
});
|
|
56
61
|
|
|
57
|
-
it('renders an instrument-declared contribution', () => {
|
|
62
|
+
it('renders an instrument-declared contribution', async () => {
|
|
58
63
|
const withChrome: Instrument = {
|
|
59
64
|
...bare,
|
|
60
65
|
chrome: [{ id: 'mine', region: 'status', item: { text: 'ready' } }],
|
|
61
66
|
};
|
|
62
|
-
|
|
67
|
+
await renderSettled(<Lab title="T" instruments={[withChrome]} defaultInstrument="Bare" />);
|
|
63
68
|
expect(screen.getByText('ready')).toBeInTheDocument();
|
|
64
69
|
});
|
|
65
70
|
|
|
66
|
-
it('renders an aside contribution in its own pane, after the workspace', () => {
|
|
67
|
-
|
|
71
|
+
it('renders an aside contribution in its own pane, after the workspace', async () => {
|
|
72
|
+
await renderSettled(
|
|
68
73
|
<Lab
|
|
69
74
|
title="T"
|
|
70
75
|
instruments={[bare]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
2
2
|
import { resolveTheme } from '@weasel-js/theme';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
import type { Instrument } from '../instrument/types';
|
|
@@ -18,8 +18,8 @@ const backdrop = (container: HTMLElement) =>
|
|
|
18
18
|
.find(Boolean) ?? '';
|
|
19
19
|
|
|
20
20
|
describe('<Lab nebula>', () => {
|
|
21
|
-
it('draws one blob per literal color', () => {
|
|
22
|
-
const { container } =
|
|
21
|
+
it('draws one blob per literal color', async () => {
|
|
22
|
+
const { container } = await renderSettled(
|
|
23
23
|
<Lab
|
|
24
24
|
instruments={[bare]}
|
|
25
25
|
defaultInstrument="Bare"
|
|
@@ -30,8 +30,8 @@ describe('<Lab nebula>', () => {
|
|
|
30
30
|
expect(backdrop(container).match(/radial-gradient/g)).toHaveLength(3);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
it('resolves a theme ramp against the lab’s theme', () => {
|
|
34
|
-
const { container } =
|
|
33
|
+
it('resolves a theme ramp against the lab’s theme', async () => {
|
|
34
|
+
const { container } = await renderSettled(
|
|
35
35
|
<Lab instruments={[bare]} defaultInstrument="Bare" mode="dark" nebula={{ ramp: 'swatch' }} />,
|
|
36
36
|
);
|
|
37
37
|
const sky = resolveTheme(interstellarTheme, { mode: 'dark' })['--wzl-swatch-sky'];
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* workspace: `.lk-lab__body > .lk-workspace { min-width: 0 }` is a direct-child
|
|
8
8
|
* selector, so an extra element between them silently drops that rule.
|
|
9
9
|
*/
|
|
10
|
-
import { act
|
|
10
|
+
import { act } from '@testing-library/react';
|
|
11
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
11
12
|
import type { ReactNode } from 'react';
|
|
12
13
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
14
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
@@ -75,16 +76,18 @@ const probeInstrument = defineInstrument<Record<string, never>, Record<string, n
|
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
describe('<Lab> surface provider', () => {
|
|
78
|
-
it('puts a surface handle in reach of an instrument', () => {
|
|
79
|
+
it('puts a surface handle in reach of an instrument', async () => {
|
|
79
80
|
seen = 'not-rendered';
|
|
80
|
-
|
|
81
|
+
await renderSettled(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
|
|
81
82
|
expect(seen).not.toBe('not-rendered');
|
|
82
83
|
expect(seen).not.toBeNull();
|
|
83
84
|
expect(typeof (seen as unknown as SurfaceHandle).registerTile).toBe('function');
|
|
84
85
|
});
|
|
85
86
|
|
|
86
|
-
it('stacks an under and an over buffer inside the lab body', () => {
|
|
87
|
-
const { container } =
|
|
87
|
+
it('stacks an under and an over buffer inside the lab body', async () => {
|
|
88
|
+
const { container } = await renderSettled(
|
|
89
|
+
<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />,
|
|
90
|
+
);
|
|
88
91
|
const canvases = container.querySelectorAll('.lk-lab__body > canvas.lk-lab__surface');
|
|
89
92
|
expect(canvases).toHaveLength(2);
|
|
90
93
|
// Source order, because the z-index that separates them is in a stylesheet
|
|
@@ -96,24 +99,28 @@ describe('<Lab> surface provider', () => {
|
|
|
96
99
|
]);
|
|
97
100
|
});
|
|
98
101
|
|
|
99
|
-
it('hands a tenant whichever of the two it asks for', () => {
|
|
100
|
-
const { container } =
|
|
102
|
+
it('hands a tenant whichever of the two it asks for', async () => {
|
|
103
|
+
const { container } = await renderSettled(
|
|
104
|
+
<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />,
|
|
105
|
+
);
|
|
101
106
|
const [underEl, overEl] = [...container.querySelectorAll('canvas.lk-lab__surface')];
|
|
102
107
|
expect(seenCanvases.over).toBe(overEl);
|
|
103
108
|
expect(seenCanvases.under).toBe(underEl);
|
|
104
109
|
expect(overEl).not.toBe(underEl);
|
|
105
110
|
});
|
|
106
111
|
|
|
107
|
-
it('defaults a tenant to the buffer over the trials', () => {
|
|
112
|
+
it('defaults a tenant to the buffer over the trials', async () => {
|
|
108
113
|
// Every tenant predating the split wants the over one: the surface was
|
|
109
114
|
// built for marks that annotate an instrument.
|
|
110
|
-
const { container } =
|
|
115
|
+
const { container } = await renderSettled(
|
|
116
|
+
<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />,
|
|
117
|
+
);
|
|
111
118
|
const [underEl, overEl] = [...container.querySelectorAll('canvas.lk-lab__surface')];
|
|
112
119
|
expect(seenCanvases.byDefault).toBe(overEl);
|
|
113
120
|
expect(seenCanvases.byDefault).not.toBe(underEl);
|
|
114
121
|
});
|
|
115
122
|
|
|
116
|
-
it('mounts no buffer of its own when a host already owns the surface', () => {
|
|
123
|
+
it('mounts no buffer of its own when a host already owns the surface', async () => {
|
|
117
124
|
function Host({ children }: { children: ReactNode }) {
|
|
118
125
|
const surface = useTiledSurface({ onFrame: () => {} });
|
|
119
126
|
return (
|
|
@@ -122,7 +129,7 @@ describe('<Lab> surface provider', () => {
|
|
|
122
129
|
</SurfaceContext.Provider>
|
|
123
130
|
);
|
|
124
131
|
}
|
|
125
|
-
const { container } =
|
|
132
|
+
const { container } = await renderSettled(
|
|
126
133
|
<Host>
|
|
127
134
|
<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />
|
|
128
135
|
</Host>,
|
|
@@ -130,11 +137,13 @@ describe('<Lab> surface provider', () => {
|
|
|
130
137
|
expect(container.querySelectorAll('canvas.lk-lab__surface')).toHaveLength(0);
|
|
131
138
|
});
|
|
132
139
|
|
|
133
|
-
it('keeps the workspace a direct child of the lab body', () => {
|
|
140
|
+
it('keeps the workspace a direct child of the lab body', async () => {
|
|
134
141
|
// The min-width:0 rule that keeps the flex row from overflowing is
|
|
135
142
|
// `.lk-lab__body > .lk-workspace`. An element inserted between them drops
|
|
136
143
|
// it, and jsdom cannot see the overflow that follows.
|
|
137
|
-
const { container } =
|
|
144
|
+
const { container } = await renderSettled(
|
|
145
|
+
<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />,
|
|
146
|
+
);
|
|
138
147
|
const body = container.querySelector('.lk-lab__body');
|
|
139
148
|
expect(body).not.toBeNull();
|
|
140
149
|
expect(body?.querySelector(':scope > .lk-workspace')).not.toBeNull();
|
|
@@ -155,7 +164,7 @@ describe('<Lab> tile round trip', () => {
|
|
|
155
164
|
vi.restoreAllMocks();
|
|
156
165
|
});
|
|
157
166
|
|
|
158
|
-
it('publishes a rect for a tile an instrument registered', () => {
|
|
167
|
+
it('publishes a rect for a tile an instrument registered', async () => {
|
|
159
168
|
const frames: SurfaceFrame[] = [];
|
|
160
169
|
let handle: SurfaceHandle | null = null;
|
|
161
170
|
|
|
@@ -177,7 +186,7 @@ describe('<Lab> tile round trip', () => {
|
|
|
177
186
|
);
|
|
178
187
|
}
|
|
179
188
|
|
|
180
|
-
|
|
189
|
+
await renderSettled(
|
|
181
190
|
<Host>
|
|
182
191
|
<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />
|
|
183
192
|
</Host>,
|
|
@@ -189,11 +198,11 @@ describe('<Lab> tile round trip', () => {
|
|
|
189
198
|
expect(pane).not.toBeNull();
|
|
190
199
|
stubBox(pane, 240, 100, 320, 200);
|
|
191
200
|
|
|
192
|
-
act(() => {
|
|
201
|
+
await act(async () => {
|
|
193
202
|
handle?.invalidateRects();
|
|
194
203
|
handle?.invalidate(paneTileId);
|
|
195
204
|
});
|
|
196
|
-
act(() => {
|
|
205
|
+
await act(async () => {
|
|
197
206
|
vi.advanceTimersByTime(64);
|
|
198
207
|
});
|
|
199
208
|
|
|
@@ -208,8 +217,8 @@ describe('<Lab> tile round trip', () => {
|
|
|
208
217
|
});
|
|
209
218
|
|
|
210
219
|
describe('<Lab> pages', () => {
|
|
211
|
-
it("gives the title a way back to the project's other labs", () => {
|
|
212
|
-
const { getByRole } =
|
|
220
|
+
it("gives the title a way back to the project's other labs", async () => {
|
|
221
|
+
const { getByRole } = await renderSettled(
|
|
213
222
|
<Lab
|
|
214
223
|
instruments={[probeInstrument]}
|
|
215
224
|
defaultInstrument="Probe"
|
|
@@ -225,8 +234,8 @@ describe('<Lab> pages', () => {
|
|
|
225
234
|
expect(getByRole('button', { name: /kliegsminister/ })).toBeTruthy();
|
|
226
235
|
});
|
|
227
236
|
|
|
228
|
-
it('leaves the title a plain heading when there are no other labs', () => {
|
|
229
|
-
const { getByRole, queryByRole } =
|
|
237
|
+
it('leaves the title a plain heading when there are no other labs', async () => {
|
|
238
|
+
const { getByRole, queryByRole } = await renderSettled(
|
|
230
239
|
<Lab instruments={[probeInstrument]} defaultInstrument="Probe" title="solo" />,
|
|
231
240
|
);
|
|
232
241
|
expect(getByRole('heading', { name: 'solo' })).toBeTruthy();
|