@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
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* centering puts the pressed world point at the stage's origin instead.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { act
|
|
10
|
+
import { act } from '@testing-library/react';
|
|
11
11
|
import type { PointerContextValue } from '@weasel-js/core';
|
|
12
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
12
13
|
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
13
14
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
14
15
|
import { Lab } from '../lab/Lab';
|
|
@@ -40,7 +41,7 @@ function pointer(el: Element, type: string, x: number, y: number, buttons = 1) {
|
|
|
40
41
|
|
|
41
42
|
/** Content 400×300 in a 200×150 box, 6px in: scale 0.46, so box (100, 75)
|
|
42
43
|
* is content (200, 150). */
|
|
43
|
-
function mount() {
|
|
44
|
+
async function mount() {
|
|
44
45
|
let store: PointerContextValue | null = null;
|
|
45
46
|
const pictured = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
46
47
|
name: 'Pictured',
|
|
@@ -57,7 +58,7 @@ function mount() {
|
|
|
57
58
|
),
|
|
58
59
|
},
|
|
59
60
|
});
|
|
60
|
-
const r =
|
|
61
|
+
const r = await renderSettled(<Lab instruments={[pictured]} defaultInstrument="Pictured" />);
|
|
61
62
|
const box = r.container.querySelector('.lk-overview__box');
|
|
62
63
|
const content = r.container.querySelector('.lk-stage__content') as HTMLElement | null;
|
|
63
64
|
if (!box || !content) throw new Error('no overview or stage');
|
|
@@ -67,15 +68,15 @@ function mount() {
|
|
|
67
68
|
const px = (el: HTMLElement, name: string) => Number.parseFloat(el.style.getPropertyValue(name));
|
|
68
69
|
|
|
69
70
|
describe('<TrialOverview>', () => {
|
|
70
|
-
it('draws the instrument-supplied content, not the instrument itself', () => {
|
|
71
|
-
const h = mount();
|
|
71
|
+
it('draws the instrument-supplied content, not the instrument itself', async () => {
|
|
72
|
+
const h = await mount();
|
|
72
73
|
expect(h.box.querySelector('[data-testid="thumb"]')).not.toBeNull();
|
|
73
74
|
expect(h.box.querySelector('[data-testid="picture"]')).toBeNull();
|
|
74
75
|
});
|
|
75
76
|
|
|
76
|
-
it('moves the stage camera to the pressed point', () => {
|
|
77
|
-
const h = mount();
|
|
78
|
-
act(() => {
|
|
77
|
+
it('moves the stage camera to the pressed point', async () => {
|
|
78
|
+
const h = await mount();
|
|
79
|
+
await act(async () => {
|
|
79
80
|
pointer(h.box, 'pointerdown', 100, 75);
|
|
80
81
|
pointer(h.box, 'pointerup', 100, 75, 0);
|
|
81
82
|
});
|
|
@@ -83,9 +84,9 @@ describe('<TrialOverview>', () => {
|
|
|
83
84
|
expect(px(h.content, '--lk-stage-y')).toBeCloseTo(-150);
|
|
84
85
|
});
|
|
85
86
|
|
|
86
|
-
it('follows a drag with the camera, and never pans the stage by it', () => {
|
|
87
|
-
const h = mount();
|
|
88
|
-
act(() => {
|
|
87
|
+
it('follows a drag with the camera, and never pans the stage by it', async () => {
|
|
88
|
+
const h = await mount();
|
|
89
|
+
await act(async () => {
|
|
89
90
|
pointer(h.box, 'pointerdown', 100, 75);
|
|
90
91
|
pointer(h.box, 'pointermove', 110, 75);
|
|
91
92
|
pointer(h.box, 'pointermove', 123, 75);
|
|
@@ -96,9 +97,9 @@ describe('<TrialOverview>', () => {
|
|
|
96
97
|
expect(px(h.content, '--lk-stage-x')).toBeCloseTo(-250);
|
|
97
98
|
});
|
|
98
99
|
|
|
99
|
-
it("publishes the pointer over it as the overview's, and the stage draws it", () => {
|
|
100
|
-
const h = mount();
|
|
101
|
-
act(() => pointer(h.box, 'pointermove', 100, 75, 0));
|
|
100
|
+
it("publishes the pointer over it as the overview's, and the stage draws it", async () => {
|
|
101
|
+
const h = await mount();
|
|
102
|
+
await act(async () => pointer(h.box, 'pointermove', 100, 75, 0));
|
|
102
103
|
const p = h.store()?.get();
|
|
103
104
|
expect(p?.viewId).toBe('overview');
|
|
104
105
|
expect(p?.worldX).toBeCloseTo(200);
|
|
@@ -106,10 +107,10 @@ describe('<TrialOverview>', () => {
|
|
|
106
107
|
expect(h.container.querySelector('.lk-stage .lk-linked-cursor')).not.toBeNull();
|
|
107
108
|
});
|
|
108
109
|
|
|
109
|
-
it("publishes the stage's pointer as the stage's, so the stage draws none", () => {
|
|
110
|
-
const h = mount();
|
|
110
|
+
it("publishes the stage's pointer as the stage's, so the stage draws none", async () => {
|
|
111
|
+
const h = await mount();
|
|
111
112
|
const host = h.container.querySelector('.lk-stage') as HTMLElement;
|
|
112
|
-
act(() => pointer(host, 'pointermove', 10, 10, 0));
|
|
113
|
+
await act(async () => pointer(host, 'pointermove', 10, 10, 0));
|
|
113
114
|
expect(h.store()?.get()?.viewId).toBe('stage');
|
|
114
115
|
expect(h.container.querySelector('.lk-stage .lk-linked-cursor')).toBeNull();
|
|
115
116
|
});
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
useActionsRegistry,
|
|
21
21
|
useDepSource,
|
|
22
22
|
useGestureDispatcher,
|
|
23
|
+
useLatest,
|
|
23
24
|
usePointerPosition,
|
|
24
25
|
type View,
|
|
25
26
|
type ViewApi,
|
|
@@ -314,8 +315,7 @@ function OverviewDispatch({
|
|
|
314
315
|
camera: CameraContextValue;
|
|
315
316
|
}) {
|
|
316
317
|
const registry = useActionsRegistry();
|
|
317
|
-
const fitRef =
|
|
318
|
-
fitRef.current = fit;
|
|
318
|
+
const fitRef = useLatest(fit);
|
|
319
319
|
const root = camera.view;
|
|
320
320
|
useDepSource('rootView', () => root as ViewApi);
|
|
321
321
|
|
|
@@ -336,7 +336,7 @@ function OverviewDispatch({
|
|
|
336
336
|
y: (cy - (r?.top ?? 0)) / f.scale.y + f.y,
|
|
337
337
|
};
|
|
338
338
|
},
|
|
339
|
-
[boxRef],
|
|
339
|
+
[boxRef, fitRef],
|
|
340
340
|
);
|
|
341
341
|
|
|
342
342
|
useGestureDispatcher({
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
openPointerSession,
|
|
3
|
+
type PointerSession,
|
|
4
|
+
pastDragThreshold,
|
|
5
|
+
useLatest,
|
|
6
|
+
} from '@weasel-js/core';
|
|
2
7
|
import type React from 'react';
|
|
3
8
|
import { type CSSProperties, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
9
|
import {
|
|
@@ -119,8 +124,7 @@ export function FloatingPanel({
|
|
|
119
124
|
// A drag outlives the render that started it, so the reducer's inputs are
|
|
120
125
|
// read here and not from that render's closure — a container resized
|
|
121
126
|
// mid-drag would otherwise place the panel against its old box.
|
|
122
|
-
const layout =
|
|
123
|
-
layout.current = { container, options, item };
|
|
127
|
+
const layout = useLatest({ container, options, item });
|
|
124
128
|
|
|
125
129
|
useEffect(() => () => session.current?.cancel(), []);
|
|
126
130
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
.lk-fps-meter {
|
|
2
|
+
.numeric();
|
|
2
3
|
display: inline-flex;
|
|
3
4
|
align-items: center;
|
|
4
5
|
white-space: nowrap;
|
|
5
6
|
gap: var(--wzl-space-xs);
|
|
6
|
-
font-family: var(--wzl-font-mono);
|
|
7
7
|
font-size: var(--wzl-font-size-sm);
|
|
8
8
|
color: var(--wzl-fg-muted);
|
|
9
9
|
}
|
|
@@ -14,6 +14,5 @@
|
|
|
14
14
|
a 240Hz display reads three digits, and a readout that grows into them
|
|
15
15
|
pushes whatever shares its row. */
|
|
16
16
|
min-width: 3ch;
|
|
17
|
-
font-variant-numeric: tabular-nums;
|
|
18
17
|
text-align: right;
|
|
19
18
|
}
|
|
@@ -81,8 +81,8 @@ describe('Readout', () => {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
it('merges a consumer className on its root', () => {
|
|
84
|
-
const { container } = render(<Readout className="mine" rows={[]} />);
|
|
85
|
-
expect(must(container.firstElementChild).className).toMatch(/\blk-readout\b.*\
|
|
84
|
+
const { container } = render(<Readout className="is-mine" rows={[]} />);
|
|
85
|
+
expect(must(container.firstElementChild).className).toMatch(/\blk-readout\b.*\bis-mine\b/);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
it('is exported from the labkit barrel, with the list it is built on', () => {
|
package/src/primitives/Split.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLatest } from '@weasel-js/core';
|
|
1
2
|
import { type ReactNode, useEffect, useMemo, useRef } from 'react';
|
|
2
3
|
import { asNodeId, createNode, Store, stripStrategy } from 'windease';
|
|
3
4
|
import { type ChromeMap, Container, Provider, StrategyRegistryProvider } from 'windease/react';
|
|
@@ -42,6 +43,65 @@ export interface SplitProps {
|
|
|
42
43
|
viewport?: { w: number; h: number };
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
interface SplitInit {
|
|
47
|
+
side: 'start' | 'end';
|
|
48
|
+
label: string;
|
|
49
|
+
sidebarWidth: number;
|
|
50
|
+
minWidth: number;
|
|
51
|
+
maxWidth: number;
|
|
52
|
+
contentMinWidth: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A strip holding the sidebar and the content pane, sized as the first render asks. */
|
|
56
|
+
function createSplitStore({
|
|
57
|
+
side,
|
|
58
|
+
label,
|
|
59
|
+
sidebarWidth,
|
|
60
|
+
minWidth,
|
|
61
|
+
maxWidth,
|
|
62
|
+
contentMinWidth,
|
|
63
|
+
}: SplitInit): Store {
|
|
64
|
+
const store = new Store();
|
|
65
|
+
store.registerNode(
|
|
66
|
+
createNode({
|
|
67
|
+
kind: 'zone',
|
|
68
|
+
id: ZONE_ID,
|
|
69
|
+
// A strip's seam resizes the pane before it, which on the end side is the
|
|
70
|
+
// content; 'neighbor' has the seam size the sidebar after it too.
|
|
71
|
+
container: {
|
|
72
|
+
strategyId: 'strip',
|
|
73
|
+
config: {
|
|
74
|
+
axis: 'x',
|
|
75
|
+
resizable: true,
|
|
76
|
+
...(side === 'end' ? { resizeMode: 'neighbor' } : {}),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
const sidebarNode = createNode({
|
|
82
|
+
kind: 'sidebar',
|
|
83
|
+
id: SIDEBAR_ID,
|
|
84
|
+
parentId: ZONE_ID,
|
|
85
|
+
meta: { title: label },
|
|
86
|
+
placement: { size: { w: sidebarWidth } },
|
|
87
|
+
hints: { minSize: { w: minWidth, h: 0 }, maxSize: { w: maxWidth, h: 0 } },
|
|
88
|
+
});
|
|
89
|
+
const contentNode = createNode({
|
|
90
|
+
kind: 'content',
|
|
91
|
+
id: CONTENT_ID,
|
|
92
|
+
parentId: ZONE_ID,
|
|
93
|
+
meta: { title: 'Content' },
|
|
94
|
+
hints: { minSize: { w: contentMinWidth, h: 0 } },
|
|
95
|
+
});
|
|
96
|
+
// A strip lays its children out in registration order.
|
|
97
|
+
for (const node of side === 'end' ? [contentNode, sidebarNode] : [sidebarNode, contentNode]) {
|
|
98
|
+
store.registerNode(node);
|
|
99
|
+
}
|
|
100
|
+
store.showNode(SIDEBAR_ID);
|
|
101
|
+
store.showNode(CONTENT_ID);
|
|
102
|
+
return store;
|
|
103
|
+
}
|
|
104
|
+
|
|
45
105
|
/**
|
|
46
106
|
* A sidebar and a content pane as a two-pane strip with a draggable seam.
|
|
47
107
|
*
|
|
@@ -70,45 +130,14 @@ export function Split({
|
|
|
70
130
|
// stretches its panes to the container's height and never reads `h`.
|
|
71
131
|
const storeRef = useRef<Store | null>(null);
|
|
72
132
|
if (storeRef.current === null) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
container: {
|
|
81
|
-
strategyId: 'strip',
|
|
82
|
-
config: {
|
|
83
|
-
axis: 'x',
|
|
84
|
-
resizable: true,
|
|
85
|
-
...(side === 'end' ? { resizeMode: 'neighbor' } : {}),
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
}),
|
|
89
|
-
);
|
|
90
|
-
const sidebarNode = createNode({
|
|
91
|
-
kind: 'sidebar',
|
|
92
|
-
id: SIDEBAR_ID,
|
|
93
|
-
parentId: ZONE_ID,
|
|
94
|
-
meta: { title: label },
|
|
95
|
-
placement: { size: { w: width ?? defaultWidth } },
|
|
96
|
-
hints: { minSize: { w: minWidth, h: 0 }, maxSize: { w: maxWidth, h: 0 } },
|
|
97
|
-
});
|
|
98
|
-
const contentNode = createNode({
|
|
99
|
-
kind: 'content',
|
|
100
|
-
id: CONTENT_ID,
|
|
101
|
-
parentId: ZONE_ID,
|
|
102
|
-
meta: { title: 'Content' },
|
|
103
|
-
hints: { minSize: { w: contentMinWidth, h: 0 } },
|
|
133
|
+
storeRef.current = createSplitStore({
|
|
134
|
+
side,
|
|
135
|
+
label,
|
|
136
|
+
sidebarWidth: width ?? defaultWidth,
|
|
137
|
+
minWidth,
|
|
138
|
+
maxWidth,
|
|
139
|
+
contentMinWidth,
|
|
104
140
|
});
|
|
105
|
-
// A strip lays its children out in registration order.
|
|
106
|
-
for (const node of side === 'end' ? [contentNode, sidebarNode] : [sidebarNode, contentNode]) {
|
|
107
|
-
store.registerNode(node);
|
|
108
|
-
}
|
|
109
|
-
store.showNode(SIDEBAR_ID);
|
|
110
|
-
store.showNode(CONTENT_ID);
|
|
111
|
-
storeRef.current = store;
|
|
112
141
|
}
|
|
113
142
|
const store = storeRef.current;
|
|
114
143
|
|
|
@@ -134,8 +163,7 @@ export function Split({
|
|
|
134
163
|
store.patchPlacement(SIDEBAR_ID, { size: { w: width } });
|
|
135
164
|
}, [store, width]);
|
|
136
165
|
|
|
137
|
-
const onWidthChangeRef =
|
|
138
|
-
onWidthChangeRef.current = onWidthChange;
|
|
166
|
+
const onWidthChangeRef = useLatest(onWidthChange);
|
|
139
167
|
useEffect(
|
|
140
168
|
() =>
|
|
141
169
|
store.events.on('node.placementChanged', () => {
|
|
@@ -148,7 +176,7 @@ export function Split({
|
|
|
148
176
|
widthRef.current = next;
|
|
149
177
|
onWidthChangeRef.current?.(next);
|
|
150
178
|
}),
|
|
151
|
-
[store],
|
|
179
|
+
[onWidthChangeRef, store],
|
|
152
180
|
);
|
|
153
181
|
|
|
154
182
|
const chrome = useMemo<ChromeMap>(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
.lk-status-bar {
|
|
2
|
+
.numeric();
|
|
2
3
|
display: flex;
|
|
3
4
|
align-items: center;
|
|
4
5
|
/* One line, always. A bar that grows a second line takes it from the canvas
|
|
@@ -11,7 +12,6 @@
|
|
|
11
12
|
border-top: 1px solid var(--wzl-border);
|
|
12
13
|
font-size: var(--wzl-font-size-sm);
|
|
13
14
|
color: var(--wzl-fg-muted);
|
|
14
|
-
font-family: var(--wzl-font-mono);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
.lk-status-bar-section {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MouseEventHandler, ReactNode } from 'react';
|
|
2
|
-
import { useRovingTabIndex } from '
|
|
2
|
+
import { useRovingTabIndex } from '../passthrough/weasel-ui';
|
|
3
3
|
|
|
4
4
|
/** Props for `<Toolbar>`. */
|
|
5
5
|
export interface ToolbarProps {
|
|
@@ -12,10 +12,10 @@ export interface ToolbarProps {
|
|
|
12
12
|
/** A horizontal bar of controls. Fill it with `<Toolbar.Title>`,
|
|
13
13
|
* `<Toolbar.Group>`, `<Toolbar.Button>` and `<Toolbar.Spacer>`. */
|
|
14
14
|
export function Toolbar({ children, 'aria-label': ariaLabel }: ToolbarProps) {
|
|
15
|
-
const {
|
|
15
|
+
const { rootRef, onKeyDown } = useRovingTabIndex<HTMLDivElement>({ orientation: 'horizontal' });
|
|
16
16
|
return (
|
|
17
17
|
<div
|
|
18
|
-
ref={
|
|
18
|
+
ref={rootRef}
|
|
19
19
|
className="lk-toolbar"
|
|
20
20
|
role="toolbar"
|
|
21
21
|
aria-label={ariaLabel}
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
// See the `.lk-root` prefix convention in theme/base.less.
|
|
16
|
+
// Under `.lk-root` to outrank theme/base.less's element defaults; see its prefix convention.
|
|
18
17
|
.lk-root {
|
|
19
18
|
.lk-zoom {
|
|
20
19
|
// Narrow enough to sit in a trial toolbar, wide enough for the octave
|
package/src/state/undock.ts
CHANGED
|
@@ -8,10 +8,13 @@ export interface UndockedPanel {
|
|
|
8
8
|
/** Undocked panels, keyed by {@link panelKey}. */
|
|
9
9
|
export type UndockedPanels = Record<string, UndockedPanel>;
|
|
10
10
|
|
|
11
|
+
/** The `UndockedPanels` key for one trial's section. */
|
|
11
12
|
export function panelKey(trialId: string, sectionId: string): string {
|
|
12
13
|
return `${trialId}::${sectionId}`;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/** `panels` with the section marked undocked `as` a tile or a floating panel,
|
|
17
|
+
* replacing any earlier entry for it. Returns a new record. */
|
|
15
18
|
export function undockPanel(
|
|
16
19
|
panels: UndockedPanels,
|
|
17
20
|
trialId: string,
|
package/src/state/useOpenOnce.ts
CHANGED
|
@@ -10,6 +10,13 @@ interface Entry<T> {
|
|
|
10
10
|
mounts: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function openEntry<T>(open: () => T | Promise<T>): Entry<T> {
|
|
14
|
+
const result = open();
|
|
15
|
+
return result instanceof Promise
|
|
16
|
+
? { value: null, pending: result, mounts: 0 }
|
|
17
|
+
: { value: result, pending: null, mounts: 0 };
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
21
|
* Open something once for a component's lifetime: at once when `open` returns
|
|
15
22
|
* a value, after it settles when it returns a promise. Closed on unmount — but
|
|
@@ -18,13 +25,7 @@ interface Entry<T> {
|
|
|
18
25
|
*/
|
|
19
26
|
export function useOpenOnce<T extends Closable>(open: () => T | Promise<T>): T | null {
|
|
20
27
|
const entry = useRef<Entry<T> | null>(null);
|
|
21
|
-
if (entry.current === null)
|
|
22
|
-
const result = open();
|
|
23
|
-
entry.current =
|
|
24
|
-
result instanceof Promise
|
|
25
|
-
? { value: null, pending: result, mounts: 0 }
|
|
26
|
-
: { value: result, pending: null, mounts: 0 };
|
|
27
|
-
}
|
|
28
|
+
if (entry.current === null) entry.current = openEntry(open);
|
|
28
29
|
const [value, setValue] = useState<T | null>(entry.current.value);
|
|
29
30
|
|
|
30
31
|
useEffect(() => {
|
|
@@ -56,13 +57,16 @@ export function useOpenOnce<T extends Closable>(open: () => T | Promise<T>): T |
|
|
|
56
57
|
export function useWarnIgnoredChange(component: string, props: Record<string, unknown>): void {
|
|
57
58
|
const first = useRef(props);
|
|
58
59
|
const warned = useRef(false);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
// In an effect, so only props that committed are compared.
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (process.env.NODE_ENV === 'production' || warned.current) return;
|
|
63
|
+
for (const key of Object.keys(props)) {
|
|
64
|
+
if (Object.is(first.current[key], props[key])) continue;
|
|
65
|
+
warned.current = true;
|
|
66
|
+
console.warn(
|
|
67
|
+
`[labkit] ${component} reads \`${key}\` once, at mount; a later change is ignored`,
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
68
72
|
}
|
package/src/state/view.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { ViewTransform } from '../instrument/types';
|
|
|
5
5
|
* other. Re-exported under its own name so a consumer can say which it means. */
|
|
6
6
|
export type ViewTransform2D = ViewTransform;
|
|
7
7
|
|
|
8
|
+
/** Zoom 1, no pan. */
|
|
8
9
|
export const DEFAULT_VIEW: ViewTransform2D = { zoom: 1, pan: { x: 0, y: 0 } };
|
|
9
10
|
|
|
10
11
|
/** A trial's view is opaque to labkit, so anything that needs the 2D shape — the
|
package/src/styles.less
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Left as a CSS @import for the consumer's bundler to resolve; build-css.mjs
|
|
3
3
|
// swaps it for the faces themselves, pointed at dist/fonts/.
|
|
4
4
|
@import (css) '@weasel-js/theme/faces.css';
|
|
5
|
+
// The numeric face's one definition, taken as a mixin: `.numeric();`.
|
|
6
|
+
@import (reference, less) '@weasel-js/theme/numeric.module.css';
|
|
5
7
|
@import './theme/base.less';
|
|
6
8
|
@import './lab/Workspace.less';
|
|
7
9
|
@import './lab/LabShell.less';
|
|
@@ -22,6 +24,7 @@
|
|
|
22
24
|
@import './chrome/regions/PaletteRegion.less';
|
|
23
25
|
@import './chrome/regions/SidebarRegion.less';
|
|
24
26
|
@import './controls/ControlPanel.less';
|
|
27
|
+
@import './controls/ControlMatrix.less';
|
|
25
28
|
@import './canvas/CanvasStack.less';
|
|
26
29
|
@import './canvas/Stage.less';
|
|
27
30
|
@import './canvas/LinkedCursor.less';
|
package/src/surface/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { act, render, renderHook } from '@testing-library/react';
|
|
2
|
+
import { renderThenAbandon } from '@weasel-js/react/testing/abandonRender';
|
|
2
3
|
import { useEffect } from 'react';
|
|
3
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import type { SurfaceFrame, SurfaceHandle } from './useTiledSurface';
|
|
@@ -505,6 +506,19 @@ describe('useTiledSurface', () => {
|
|
|
505
506
|
expect(clear).toHaveBeenCalledTimes(1);
|
|
506
507
|
});
|
|
507
508
|
});
|
|
509
|
+
it("hands a frame to the committed owner, never an abandoned render's", () => {
|
|
510
|
+
const committed: SurfaceFrame[] = [];
|
|
511
|
+
const abandoned: SurfaceFrame[] = [];
|
|
512
|
+
const onHandle = (): void => {};
|
|
513
|
+
renderThenAbandon<HarnessProps>(
|
|
514
|
+
{ frames: committed, onHandle },
|
|
515
|
+
{ frames: abandoned, onHandle },
|
|
516
|
+
(p) => <Harness {...p} />,
|
|
517
|
+
);
|
|
518
|
+
flushFrames();
|
|
519
|
+
expect(abandoned).toHaveLength(0);
|
|
520
|
+
expect(committed.length).toBeGreaterThan(0);
|
|
521
|
+
});
|
|
508
522
|
});
|
|
509
523
|
|
|
510
524
|
describe('useTiledSurface handle identity', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useVisibleRaf } from '@weasel-js/core';
|
|
1
|
+
import { useLatest, useVisibleRaf } from '@weasel-js/core';
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
3
3
|
import { composeRects, rectsEqual } from './composeRects';
|
|
4
4
|
import type { Box, Rect } from './rect';
|
|
@@ -55,10 +55,19 @@ export type TilePainter = (rect: Rect, frame: SurfaceFrame) => void;
|
|
|
55
55
|
* viewport takes. */
|
|
56
56
|
export type SurfaceClear = (size: { width: number; height: number }, dpr: number) => void;
|
|
57
57
|
|
|
58
|
+
/** Inputs to `useTiledSurface`. `onFrame` runs once per frame that has dirty
|
|
59
|
+
* tiles, before any tile paints — the place to size the shared buffer. */
|
|
58
60
|
export interface UseTiledSurfaceOptions {
|
|
59
61
|
onFrame: (frame: SurfaceFrame) => void;
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Share one drawing buffer among tiles laid out by the DOM. Tiles register
|
|
66
|
+
* their elements; the hook measures them against the container, re-measures on
|
|
67
|
+
* resize, and batches invalidations into one visible-only frame that calls
|
|
68
|
+
* `onFrame`, then (on a retiled frame) the registered clears, then each dirty
|
|
69
|
+
* tile's painter.
|
|
70
|
+
*/
|
|
62
71
|
export function useTiledSurface({ onFrame }: UseTiledSurfaceOptions): SurfaceHandle {
|
|
63
72
|
const container = useRef<HTMLElement | null>(null);
|
|
64
73
|
const tiles = useRef(new Map<string, HTMLElement>());
|
|
@@ -72,8 +81,7 @@ export function useTiledSurface({ onFrame }: UseTiledSurfaceOptions): SurfaceHan
|
|
|
72
81
|
|
|
73
82
|
// Held in a ref so a caller passing an inline closure does not re-create every
|
|
74
83
|
// callback below on each render.
|
|
75
|
-
const onFrameRef =
|
|
76
|
-
onFrameRef.current = onFrame;
|
|
84
|
+
const onFrameRef = useLatest(onFrame);
|
|
77
85
|
|
|
78
86
|
const measure = useCallback((): boolean => {
|
|
79
87
|
const el = container.current;
|