@weasel-js/labkit 1.6.1 → 1.7.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 +3 -3
- package/dist/_dts/{CanvasStackContext-Wo6OwIVM.d.ts → CanvasStackContext-Cjy6I-Iv.d.ts} +4 -1
- package/dist/_dts/{Stage-DWBBi8IU.d.ts → Stage-D6t7o0pF.d.ts} +59 -18
- package/dist/_dts/{index-CKyX0ME_.d.ts → index-BTmaRy6a.d.ts} +10 -6
- package/dist/_dts/{types-B7yBTTF9.d.ts → index-DCMA4Fy1.d.ts} +59 -113
- package/dist/_dts/{types-DTuViBvc.d.ts → types-BUBSIZuV.d.ts} +18 -8
- package/dist/_dts/{useTrialState-CMaV6UD4.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 +6 -4
- package/dist/chrome/index.js +11 -10
- 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-4KSC7NXL.js +15491 -0
- package/dist/chunk-4KSC7NXL.js.map +1 -0
- package/dist/{chunk-SLAQPUI4.js → chunk-CEGFXTBD.js} +10 -63
- package/dist/chunk-CEGFXTBD.js.map +1 -0
- package/dist/chunk-CZZPXUOU.js +3 -0
- package/dist/{chunk-SBXNQW4G.js.map → chunk-CZZPXUOU.js.map} +1 -1
- package/dist/{chunk-QGMOA2YE.js → chunk-E23Q6A3Y.js} +296 -104
- package/dist/chunk-E23Q6A3Y.js.map +1 -0
- package/dist/{chunk-GHGO52SO.js → chunk-E6NARGU7.js} +5 -5
- package/dist/{chunk-GHGO52SO.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-6FLXL3I7.js → chunk-FB4MIZOO.js} +77 -45
- package/dist/chunk-FB4MIZOO.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-4TT3PNPR.js → chunk-GAHELLJT.js} +95 -95
- package/dist/chunk-GAHELLJT.js.map +1 -0
- package/dist/{chunk-HKV7LRHY.js → chunk-IBVRMHW5.js} +5 -5
- package/dist/{chunk-HKV7LRHY.js.map → chunk-IBVRMHW5.js.map} +1 -1
- package/dist/{chunk-ASOK76ZH.js → chunk-JFOJESJF.js} +72 -29
- package/dist/chunk-JFOJESJF.js.map +1 -0
- package/dist/{chunk-RWNDDKOH.js → chunk-PMLZP7RT.js} +18 -15
- package/dist/chunk-PMLZP7RT.js.map +1 -0
- package/dist/{chunk-TJ77A4A3.js → chunk-READTDQX.js} +5 -9
- package/dist/chunk-READTDQX.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-CGHF6RZV.js → chunk-T6DLGEJB.js} +18 -18
- package/dist/chunk-T6DLGEJB.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-DWMSB4CA.js → chunk-VYVGVBGY.js} +95 -32
- package/dist/chunk-VYVGVBGY.js.map +1 -0
- package/dist/{chunk-IRTGUQ5O.js → chunk-WLMDTI2Q.js} +16 -6
- package/dist/chunk-WLMDTI2Q.js.map +1 -0
- package/dist/{chunk-NEXR37YJ.js → chunk-XE5HJOPL.js} +6 -8
- package/dist/chunk-XE5HJOPL.js.map +1 -0
- package/dist/chunk-XVUKDSYV.js +38 -0
- package/dist/chunk-XVUKDSYV.js.map +1 -0
- package/dist/{chunk-O6FFFOHS.js → chunk-YCFKW5RX.js} +16 -8
- 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 +28 -13
- package/dist/config/index.js +4 -4
- package/dist/controls/index.d.ts +4 -7
- package/dist/controls/index.js +6 -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 +72 -487
- package/dist/index.js +603 -441
- 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 +20 -18
- 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 -2701
- package/dist/passthrough/weasel-ui.js +4 -3
- package/dist/primitives/index.d.ts +37 -2
- package/dist/primitives/index.js +9 -8
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +6 -6
- package/dist/styles.css +150 -126
- 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 +14 -13
- package/src/annotations/AnnotationOverlay.abandon.test.tsx +143 -0
- package/src/annotations/AnnotationOverlay.tsx +52 -33
- package/src/annotations/Annotations.less +0 -10
- package/src/annotations/Annotations.overlay.test.tsx +31 -0
- package/src/annotations/MarkList.tsx +4 -8
- package/src/annotations/capture.browser.test.ts +41 -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 +76 -46
- package/src/annotations/paint.ts +65 -43
- package/src/annotations/store.test.ts +212 -2
- package/src/annotations/store.ts +130 -27
- package/src/annotations/svgNodes.test.ts +28 -19
- package/src/annotations/svgNodes.ts +6 -6
- package/src/annotations/types.ts +29 -10
- package/src/canvas/AGENTS.md +1 -0
- package/src/canvas/CameraInput.test.tsx +30 -0
- package/src/canvas/CameraInput.tsx +60 -48
- package/src/canvas/CanvasStack.tsx +11 -6
- package/src/canvas/CanvasStackContext.ts +4 -1
- package/src/canvas/LinkedCursor.less +1 -1
- package/src/canvas/Stage.test.tsx +33 -4
- package/src/canvas/Stage.tsx +9 -4
- package/src/canvas/cameraRegistry.test.ts +55 -0
- package/src/canvas/cameraRegistry.ts +87 -0
- package/src/canvas/cameraZoom.test.ts +46 -0
- package/src/canvas/cameraZoom.ts +24 -0
- package/src/canvas/index.ts +14 -2
- package/src/canvas/sharedScope.test.tsx +94 -0
- package/src/canvas/worldSpec.ts +3 -1
- package/src/chrome/ChromeRegions.stories.tsx +1 -0
- 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/merge.test.ts +5 -0
- package/src/chrome/merge.ts +4 -2
- package/src/chrome/regions/PaletteRegion.less +0 -7
- package/src/chrome/regions/PaletteRegion.test.tsx +2 -3
- package/src/chrome/regions/PaletteRegion.tsx +3 -4
- package/src/chrome/regions/SidebarRegion.less +12 -2
- 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.test.ts +2 -1
- package/src/config/builder.ts +18 -5
- package/src/config/fromConfigField.test.ts +14 -0
- package/src/config/fromConfigField.ts +18 -4
- package/src/config/path.ts +2 -5
- package/src/config/resolve.ts +4 -0
- package/src/config/types.ts +14 -9
- package/src/controls/ControlPanel.less +1 -1
- package/src/controls/ControlPanel.stories.tsx +3 -2
- package/src/controls/ControlPanel.test.tsx +98 -11
- package/src/controls/ControlPanel.tsx +61 -2
- package/src/dragdrop/DragDropRuntime.tsx +22 -13
- package/src/dragdrop/DragGhost.tsx +3 -0
- package/src/dragdrop/Palette.tsx +2 -0
- package/src/index.test.ts +5 -0
- package/src/index.ts +16 -2
- package/src/instrument/types.ts +0 -6
- package/src/job/useJob.ts +14 -9
- package/src/lab/Lab.tsx +88 -73
- package/src/lab/{LabFit.stories.less → LabFit.browser.test.less} +1 -1
- package/src/lab/{LabFit.stories.tsx → LabFit.browser.test.tsx} +41 -63
- package/src/lab/LabFullChrome.stories.tsx +1 -0
- package/src/lab/LabHeader.test.tsx +24 -14
- package/src/lab/LabHeader.tsx +11 -12
- package/src/lab/LabShell.less +9 -17
- package/src/lab/LabSwitcher.browser.test.tsx +27 -0
- package/src/lab/LabSwitcher.less +8 -6
- package/src/lab/LabZoom.test.tsx +223 -0
- package/src/lab/LabZoom.tsx +170 -0
- package/src/lab/Workspace.tsx +48 -52
- package/src/lab/index.ts +1 -0
- 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/LoupeGestures.tsx +1 -1
- 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.tsx +12 -4
- package/src/overview/TrialOverview.tsx +10 -8
- package/src/passthrough/weasel-ui.test.ts +2 -0
- package/src/passthrough/weasel-ui.ts +17 -5
- package/src/primitives/FloatingPanel.test.tsx +11 -0
- package/src/primitives/FloatingPanel.tsx +8 -7
- package/src/primitives/JobProgress.less +2 -1
- package/src/primitives/Legend.stories.tsx +1 -0
- package/src/primitives/Readout.browser.test.tsx +137 -0
- package/src/primitives/Readout.less +12 -0
- package/src/primitives/Readout.stories.tsx +58 -0
- package/src/primitives/Readout.test.tsx +93 -0
- package/src/primitives/Readout.tsx +56 -0
- package/src/primitives/Split.tsx +69 -41
- package/src/primitives/Toolbar.tsx +3 -3
- package/src/primitives/ZoomControl.less +1 -2
- package/src/primitives/index.ts +2 -0
- package/src/specimen/Specimen.stories.tsx +1 -0
- package/src/specimen/Specimen.tsx +2 -3
- package/src/state/Persistence.browser.test.tsx +47 -0
- package/src/state/store.test.ts +5 -1
- package/src/state/undock.ts +3 -0
- package/src/state/useOpenOnce.ts +20 -16
- package/src/state/usePersistedState.test.tsx +1 -1
- package/src/state/view.ts +1 -0
- package/src/styles.less +1 -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/theme/Interstellar.stories.tsx +3 -1
- package/src/theme/base.less +21 -47
- package/src/theme/interstellar.test.ts +1 -1
- package/src/theme/interstellar.theme.json +10 -3
- package/src/trial/Trial.annotations.test.tsx +73 -0
- package/src/trial/Trial.less +0 -29
- package/src/trial/Trial.loupe.test.tsx +28 -11
- package/src/trial/Trial.tsx +85 -67
- package/src/trial/TrialChrome.tsx +27 -6
- package/src/trial/index.ts +2 -0
- package/src/trial/loupeSwitch.ts +15 -0
- package/dist/_dts/LayerList.d-BoELAMsM.d.ts +0 -83
- package/dist/_dts/PrefsRow.d-DFpPIeIC.d.ts +0 -39
- package/dist/_dts/PropertyField.d-D0ndfIIt.d.ts +0 -404
- package/dist/_dts/Select.d-Cr8-zoup.d.ts +0 -569
- package/dist/chunk-4TT3PNPR.js.map +0 -1
- package/dist/chunk-6FLXL3I7.js.map +0 -1
- package/dist/chunk-ASOK76ZH.js.map +0 -1
- package/dist/chunk-CGHF6RZV.js.map +0 -1
- package/dist/chunk-CQLQPQ4P.js.map +0 -1
- package/dist/chunk-DWMSB4CA.js.map +0 -1
- package/dist/chunk-HXXTULDN.js.map +0 -1
- package/dist/chunk-IJJUK4FU.js.map +0 -1
- package/dist/chunk-IRTGUQ5O.js.map +0 -1
- package/dist/chunk-KHK5FGER.js +0 -145
- package/dist/chunk-KHK5FGER.js.map +0 -1
- package/dist/chunk-NEXR37YJ.js.map +0 -1
- package/dist/chunk-O6FFFOHS.js.map +0 -1
- package/dist/chunk-QGMOA2YE.js.map +0 -1
- package/dist/chunk-RWNDDKOH.js.map +0 -1
- package/dist/chunk-S4NCI32V.js +0 -549
- package/dist/chunk-S4NCI32V.js.map +0 -1
- package/dist/chunk-SBXNQW4G.js +0 -3
- package/dist/chunk-SLAQPUI4.js.map +0 -1
- package/dist/chunk-TJ77A4A3.js.map +0 -1
- package/dist/chunk-TYN6MLXX.js +0 -13864
- package/dist/chunk-TYN6MLXX.js.map +0 -1
- package/dist/chunk-WPLAANH4.js.map +0 -1
- package/src/primitives/useRovingTabIndex.test.ts +0 -48
- package/src/primitives/useRovingTabIndex.ts +0 -88
- package/src/state/Persistence.stories.tsx +0 -75
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
|
}
|
|
@@ -109,7 +109,7 @@ describe('usePersistedState', () => {
|
|
|
109
109
|
);
|
|
110
110
|
await screen.findByLabelText('a');
|
|
111
111
|
act(() => screen.getByText('set a').click());
|
|
112
|
-
expect(shown('b')).toBe('color');
|
|
112
|
+
await waitFor(() => expect(shown('b')).toBe('color'));
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
it("re-renders with another tab's write", async () => {
|
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
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
@import './primitives/FpsMeter.less';
|
|
15
15
|
@import './primitives/ScaleIndicator.less';
|
|
16
16
|
@import './primitives/Legend.less';
|
|
17
|
+
@import './primitives/Readout.less';
|
|
17
18
|
@import './primitives/FloatingPanel.less';
|
|
18
19
|
@import './lab/Lab.less';
|
|
19
20
|
@import './trial/Trial.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;
|
|
@@ -3,9 +3,11 @@ import type { TokenName } from '@weasel-js/theme';
|
|
|
3
3
|
import { useTheme } from '@weasel-js/theme/react';
|
|
4
4
|
import { PropertyField, PropertyList, PropertyPanel } from '@weasel-js/ui';
|
|
5
5
|
import './Interstellar.stories.less';
|
|
6
|
+
import { decimal } from '@weasel-js/quantity';
|
|
6
7
|
|
|
7
8
|
const meta: Meta = {
|
|
8
9
|
title: 'labkit/Themes/Interstellar',
|
|
10
|
+
tags: ['gallery'],
|
|
9
11
|
parameters: {
|
|
10
12
|
docs: {
|
|
11
13
|
description: {
|
|
@@ -150,7 +152,7 @@ function LivePanelPreview() {
|
|
|
150
152
|
max={1}
|
|
151
153
|
step={0.01}
|
|
152
154
|
onChange={() => {}}
|
|
153
|
-
|
|
155
|
+
display={decimal({ places: 2 })}
|
|
154
156
|
/>
|
|
155
157
|
<PropertyField kind="color" label="Fill" value="#b08adb" onChange={() => {}} />
|
|
156
158
|
<PropertyField kind="color" label="Stroke" value="#1a1428" onChange={() => {}} />
|
package/src/theme/base.less
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// The Oswald and Inter faces come from @weasel-js/theme/faces.css, imported at
|
|
2
2
|
// the top of styles.less.
|
|
3
3
|
|
|
4
|
+
// weasel-ui's native range skin, read for `:extend` below and emitted only
|
|
5
|
+
// through it.
|
|
6
|
+
@import (reference, less) '../../../ui/src/components/range.module.css';
|
|
7
|
+
|
|
4
8
|
// Resets and global element defaults.
|
|
5
9
|
.lk-root {
|
|
6
10
|
font-family: var(--wzl-font-ui);
|
|
@@ -21,7 +25,10 @@
|
|
|
21
25
|
// Deliberate overrides of a weasel-ui component go the other way — see the
|
|
22
26
|
// `.lk-root` prefix convention below.
|
|
23
27
|
:where(.lk-root) {
|
|
24
|
-
|
|
28
|
+
// Only a button with no class or a labkit one. Specificity cannot keep this
|
|
29
|
+
// off a weasel-ui button: whatever its module leaves unset — a height, a
|
|
30
|
+
// backdrop blur — the default fills in.
|
|
31
|
+
:where(button:not([class]), button[class^='lk-'], button[class*=' lk-']) {
|
|
25
32
|
font: inherit;
|
|
26
33
|
color: inherit;
|
|
27
34
|
background: var(--wzl-surface-raised);
|
|
@@ -33,63 +40,32 @@
|
|
|
33
40
|
backdrop-filter: blur(var(--wzl-glass-blur));
|
|
34
41
|
-webkit-backdrop-filter: blur(var(--wzl-glass-blur));
|
|
35
42
|
|
|
36
|
-
&:hover {
|
|
43
|
+
&:where(:hover) {
|
|
37
44
|
border-color: var(--wzl-accent);
|
|
38
45
|
color: var(--wzl-accent-fg);
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
&:focus-visible {
|
|
48
|
+
&:where(:focus-visible) {
|
|
42
49
|
outline: 2px solid var(--wzl-focus-ring);
|
|
43
50
|
outline-offset: 1px;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
&:disabled {
|
|
53
|
+
&:where(:disabled) {
|
|
47
54
|
color: var(--wzl-fg-subtle);
|
|
48
55
|
cursor: not-allowed;
|
|
49
56
|
}
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
:where(input[type="range"]) {
|
|
56
|
-
appearance: none;
|
|
57
|
-
-webkit-appearance: none;
|
|
58
|
-
height: 12px;
|
|
59
|
-
background: transparent;
|
|
60
|
-
cursor: pointer;
|
|
61
|
-
|
|
62
|
-
&::-webkit-slider-runnable-track {
|
|
63
|
-
height: var(--wzl-slider-track-h);
|
|
64
|
-
border-radius: var(--wzl-radius-sm);
|
|
65
|
-
background: color-mix(in srgb, var(--wzl-accent) var(--wzl-slider-track-mix), transparent);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
&::-moz-range-track {
|
|
69
|
-
height: var(--wzl-slider-track-h);
|
|
70
|
-
border-radius: var(--wzl-radius-sm);
|
|
71
|
-
background: color-mix(in srgb, var(--wzl-accent) var(--wzl-slider-track-mix), transparent);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
&::-webkit-slider-thumb {
|
|
75
|
-
appearance: none;
|
|
76
|
-
-webkit-appearance: none;
|
|
77
|
-
width: var(--wzl-slider-thumb-size);
|
|
78
|
-
height: var(--wzl-slider-thumb-size);
|
|
79
|
-
border: none;
|
|
80
|
-
border-radius: 50%;
|
|
81
|
-
background: color-mix(in srgb, var(--wzl-accent) var(--wzl-slider-thumb-mix), transparent);
|
|
82
|
-
margin-top: calc((var(--wzl-slider-track-h) - var(--wzl-slider-thumb-size)) / 2);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
&::-moz-range-thumb {
|
|
86
|
-
width: var(--wzl-slider-thumb-size);
|
|
87
|
-
height: var(--wzl-slider-thumb-size);
|
|
88
|
-
border: none;
|
|
89
|
-
border-radius: 50%;
|
|
90
|
-
background: color-mix(in srgb, var(--wzl-accent) var(--wzl-slider-thumb-mix), transparent);
|
|
91
|
-
}
|
|
59
|
+
// The browser sets these in its own `monospace`, which no font token reaches.
|
|
60
|
+
:where(code, kbd, samp, pre) {
|
|
61
|
+
font-family: var(--wzl-font-mono);
|
|
92
62
|
}
|
|
63
|
+
|
|
64
|
+
// A consumer's bare range wears weasel-ui's range skin, extended from its one
|
|
65
|
+
// definition rather than copied. The skin keeps its pseudo-classes inside
|
|
66
|
+
// :where(), so nothing this emits outranks a single class and a weasel-ui
|
|
67
|
+
// component's own module class still wins.
|
|
68
|
+
:where(input[type="range"]):extend(.range all) {}
|
|
93
69
|
}
|
|
94
70
|
|
|
95
71
|
// The `.lk-root` prefix convention: a labkit class handed to a weasel-ui
|
|
@@ -151,9 +127,7 @@
|
|
|
151
127
|
}
|
|
152
128
|
|
|
153
129
|
// Headline / display text uses Oswald light at tighter line-height.
|
|
154
|
-
.lk-lab h1,
|
|
155
|
-
.lk-lab h2,
|
|
156
|
-
.lk-lab h3 {
|
|
130
|
+
:where(.lk-lab) :where(h1, h2, h3) {
|
|
157
131
|
font-family: var(--wzl-font-display);
|
|
158
132
|
font-weight: var(--wzl-font-weight-light);
|
|
159
133
|
letter-spacing: 0.01em;
|
|
@@ -18,7 +18,7 @@ describe('interstellarTheme', () => {
|
|
|
18
18
|
|
|
19
19
|
// Values carried over verbatim from the retired Less.
|
|
20
20
|
expect(dark['--wzl-surface']).toBe('#0a0a14');
|
|
21
|
-
expect(dark['--wzl-accent']).toBe('#
|
|
21
|
+
expect(dark['--wzl-accent']).toBe('#4f46e5');
|
|
22
22
|
// Light mode is weasel's own: interstellar restyles the dark only.
|
|
23
23
|
const weasel = resolveTheme(weaselTheme, { mode: 'light' });
|
|
24
24
|
expect(light['--wzl-surface']).toBe(weasel['--wzl-surface']);
|
|
@@ -81,21 +81,28 @@
|
|
|
81
81
|
"accent": {
|
|
82
82
|
"by": "mode",
|
|
83
83
|
"dark": {
|
|
84
|
-
"value": "#
|
|
84
|
+
"value": "#4f46e5",
|
|
85
85
|
"type": "color"
|
|
86
86
|
}
|
|
87
87
|
},
|
|
88
88
|
"accent-hover": {
|
|
89
89
|
"by": "mode",
|
|
90
90
|
"dark": {
|
|
91
|
-
"value": "#
|
|
91
|
+
"value": "#6d5dfc",
|
|
92
|
+
"type": "color"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"accent-strong": {
|
|
96
|
+
"by": "mode",
|
|
97
|
+
"dark": {
|
|
98
|
+
"value": "#7474fe",
|
|
92
99
|
"type": "color"
|
|
93
100
|
}
|
|
94
101
|
},
|
|
95
102
|
"focus-ring": {
|
|
96
103
|
"by": "mode",
|
|
97
104
|
"dark": {
|
|
98
|
-
"value": "rgba(
|
|
105
|
+
"value": "rgba(116, 116, 254, 0.5)",
|
|
99
106
|
"type": "color"
|
|
100
107
|
}
|
|
101
108
|
},
|
|
@@ -180,6 +180,79 @@ describe('the trial undo chrome over marks', () => {
|
|
|
180
180
|
});
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
+
function GrowingPane({ n, grow }: { n: number; grow: () => void }) {
|
|
184
|
+
const api = useAnnotations();
|
|
185
|
+
return (
|
|
186
|
+
<button type="button" data-testid="growing" data-targets={api.targets().length} onClick={grow}>
|
|
187
|
+
{n}
|
|
188
|
+
</button>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
describe('targets that follow the trial state', () => {
|
|
193
|
+
it('reach a reader of the store once the change commits', () => {
|
|
194
|
+
// The store reads the committed state, so a reader rendering in the same
|
|
195
|
+
// pass as the change sees the one before; the trial tells it to read again.
|
|
196
|
+
const growing = defineInstrument<{ n: number }, Record<string, never>>({
|
|
197
|
+
name: 'Growing',
|
|
198
|
+
defaultConfig: () => ({}),
|
|
199
|
+
initialState: () => ({ n: 1 }),
|
|
200
|
+
render: (ctx) => (
|
|
201
|
+
<GrowingPane n={ctx.state.n} grow={() => ctx.setState((s) => ({ n: s.n + 1 }))} />
|
|
202
|
+
),
|
|
203
|
+
annotations: {
|
|
204
|
+
targets: (state) =>
|
|
205
|
+
Array.from({ length: state.n }, (_, i) => ({
|
|
206
|
+
id: `t${i}`,
|
|
207
|
+
ref: { current: null },
|
|
208
|
+
content: { w: 10, h: 10 },
|
|
209
|
+
})),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
render(<Lab instruments={[growing]} defaultInstrument="Growing" />);
|
|
213
|
+
expect(screen.getByTestId('growing').dataset.targets).toBe('1');
|
|
214
|
+
act(() => {
|
|
215
|
+
fireEvent.click(screen.getByTestId('growing'));
|
|
216
|
+
});
|
|
217
|
+
expect(screen.getByTestId('growing').textContent).toBe('2');
|
|
218
|
+
expect(screen.getByTestId('growing').dataset.targets).toBe('2');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("the capability's selection mode", () => {
|
|
223
|
+
const twoTargets = (selection?: 'per-target') =>
|
|
224
|
+
defineInstrument<Record<string, never>, Record<string, never>>({
|
|
225
|
+
...annotating,
|
|
226
|
+
name: 'Two',
|
|
227
|
+
annotations: {
|
|
228
|
+
targets: () => [
|
|
229
|
+
{ id: 'a', ref: { current: null }, content: { w: 200, h: 100 } },
|
|
230
|
+
{ id: 'b', ref: { current: null }, content: { w: 200, h: 100 } },
|
|
231
|
+
],
|
|
232
|
+
...(selection ? { selection } : {}),
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
const selectBoth = (): readonly string[] => {
|
|
236
|
+
const onA = marks().add({ target: 'a', kind: 'rect', frac: { x: 0, y: 0, w: 0.1, h: 0.1 } });
|
|
237
|
+
const onB = marks().add({ target: 'b', kind: 'rect', frac: { x: 0, y: 0, w: 0.1, h: 0.1 } });
|
|
238
|
+
act(() => marks().setSelection([onA, onB]));
|
|
239
|
+
return [onA, onB];
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
it('keeps the selection to one target by default', () => {
|
|
243
|
+
render(<Lab instruments={[twoTargets()]} defaultInstrument="Two" />);
|
|
244
|
+
const [onA] = selectBoth();
|
|
245
|
+
expect(marks().selection()).toEqual([onA]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("reaches the trial's store when the instrument asks for per-target", () => {
|
|
249
|
+
render(<Lab instruments={[twoTargets('per-target')]} defaultInstrument="Two" />);
|
|
250
|
+
expect(marks().selection()).toEqual([]);
|
|
251
|
+
const both = selectBoth();
|
|
252
|
+
expect(marks().selection()).toEqual(both);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
183
256
|
describe('an instrument that declares none', () => {
|
|
184
257
|
it('gets no store and no palette', () => {
|
|
185
258
|
render(<Lab instruments={[plain]} defaultInstrument="Plain" />);
|
package/src/trial/Trial.less
CHANGED
|
@@ -76,22 +76,6 @@
|
|
|
76
76
|
height: 100%;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
// A DOM instrument's own box, so the lens has something to be positioned
|
|
80
|
-
// against. Reproduces the content well's column so the instrument still
|
|
81
|
-
// stretches to it rather than collapsing to its intrinsic height.
|
|
82
|
-
&__loupe-host {
|
|
83
|
-
position: relative;
|
|
84
|
-
display: flex;
|
|
85
|
-
flex-direction: column;
|
|
86
|
-
min-height: 0;
|
|
87
|
-
height: 100%;
|
|
88
|
-
|
|
89
|
-
> * {
|
|
90
|
-
flex: 1 1 auto;
|
|
91
|
-
min-height: 0;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
79
|
&__toolbar {
|
|
96
80
|
flex-shrink: 0;
|
|
97
81
|
}
|
|
@@ -109,19 +93,6 @@
|
|
|
109
93
|
}
|
|
110
94
|
}
|
|
111
95
|
|
|
112
|
-
.lk-toolbar__zoom-label {
|
|
113
|
-
font-size: var(--wzl-font-size-sm);
|
|
114
|
-
color: var(--wzl-fg-muted);
|
|
115
|
-
padding: 0 var(--wzl-space-sm);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.lk-sidebar__placeholder {
|
|
119
|
-
padding: var(--wzl-space-sm);
|
|
120
|
-
color: var(--wzl-fg-muted);
|
|
121
|
-
font-size: var(--wzl-font-size-sm);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
96
|
.lk-trial__titlebar {
|
|
126
97
|
display: flex;
|
|
127
98
|
align-items: center;
|
|
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
|
|
|
3
3
|
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
5
5
|
import { Lab } from '../lab/Lab';
|
|
6
|
+
import { TrialLoupe } from '../loupe';
|
|
6
7
|
|
|
7
8
|
beforeAll(() => {
|
|
8
9
|
HTMLCanvasElement.prototype.getContext = vi.fn(() => ({
|
|
@@ -21,17 +22,19 @@ const canvasInstrument = defineInstrument<Record<string, never>, Record<string,
|
|
|
21
22
|
name: 'Drawn',
|
|
22
23
|
defaultConfig: () => ({}),
|
|
23
24
|
initialState: () => ({}),
|
|
24
|
-
render: () =>
|
|
25
|
+
render: () => <TrialLoupe />,
|
|
25
26
|
canvas: { layers: [{ id: 'main', draw: () => undefined }] },
|
|
26
|
-
loupe: true,
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
const domInstrument = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
30
30
|
name: 'Written',
|
|
31
31
|
defaultConfig: () => ({}),
|
|
32
32
|
initialState: () => ({}),
|
|
33
|
-
render: () =>
|
|
34
|
-
|
|
33
|
+
render: () => (
|
|
34
|
+
<TrialLoupe render={({ view }) => <p data-testid="lens-body">zoom {view.zoom}</p>}>
|
|
35
|
+
<p data-testid="body">the content</p>
|
|
36
|
+
</TrialLoupe>
|
|
37
|
+
),
|
|
35
38
|
});
|
|
36
39
|
|
|
37
40
|
const noLoupe = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
@@ -49,7 +52,7 @@ function pointAt(host: Element, x: number, y: number): void {
|
|
|
49
52
|
fireEvent.pointerMove(host, { clientX: x, clientY: y });
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
describe('the
|
|
55
|
+
describe('a lens the instrument mounts', () => {
|
|
53
56
|
it('gives a trial a toolbar toggle, and no lens until it is on', () => {
|
|
54
57
|
const { container } = render(
|
|
55
58
|
<Lab instruments={[canvasInstrument]} defaultInstrument="Drawn" />,
|
|
@@ -58,7 +61,7 @@ describe('the loupe capability', () => {
|
|
|
58
61
|
expect(lens(container)).toBeNull();
|
|
59
62
|
});
|
|
60
63
|
|
|
61
|
-
it('offers nothing to an instrument that
|
|
64
|
+
it('offers nothing to an instrument that mounts none', () => {
|
|
62
65
|
render(<Lab instruments={[noLoupe]} defaultInstrument="Plain" />);
|
|
63
66
|
expect(screen.queryByRole('button', { name: 'Loupe' })).toBeNull();
|
|
64
67
|
});
|
|
@@ -95,7 +98,7 @@ describe('the loupe capability', () => {
|
|
|
95
98
|
const { container } = render(<Lab instruments={[domInstrument]} defaultInstrument="Written" />);
|
|
96
99
|
await user.click(screen.getByRole('button', { name: 'Loupe' }));
|
|
97
100
|
|
|
98
|
-
const host = container.querySelector('.lk-
|
|
101
|
+
const host = container.querySelector('.lk-loupe-host');
|
|
99
102
|
if (!host) throw new Error('no loupe host');
|
|
100
103
|
pointAt(host, 60, 40);
|
|
101
104
|
|
|
@@ -105,9 +108,24 @@ describe('the loupe capability', () => {
|
|
|
105
108
|
expect(screen.getByTestId('body')).toBeInTheDocument();
|
|
106
109
|
});
|
|
107
110
|
|
|
108
|
-
it('wraps
|
|
111
|
+
it('wraps nothing for an instrument that mounts no lens', () => {
|
|
109
112
|
const { container } = render(<Lab instruments={[noLoupe]} defaultInstrument="Plain" />);
|
|
110
|
-
expect(container.querySelector('.lk-
|
|
113
|
+
expect(container.querySelector('.lk-loupe-host')).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('raises a lens told `enabled` without the trial toggle', () => {
|
|
117
|
+
const always = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
118
|
+
name: 'Always',
|
|
119
|
+
defaultConfig: () => ({}),
|
|
120
|
+
initialState: () => ({}),
|
|
121
|
+
render: () => <TrialLoupe enabled />,
|
|
122
|
+
canvas: { layers: [{ id: 'main', draw: () => undefined }] },
|
|
123
|
+
});
|
|
124
|
+
const { container } = render(<Lab instruments={[always]} defaultInstrument="Always" />);
|
|
125
|
+
const stack = container.querySelector('.lk-canvas-stack');
|
|
126
|
+
if (!stack) throw new Error('no canvas stack');
|
|
127
|
+
pointAt(stack, 60, 40);
|
|
128
|
+
expect(lens(container)).not.toBeNull();
|
|
111
129
|
});
|
|
112
130
|
|
|
113
131
|
it('puts the lens away again when the toggle goes off', async () => {
|
|
@@ -136,7 +154,7 @@ describe('the loupe and the trial camera share one dispatcher', () => {
|
|
|
136
154
|
name: 'Zoomed',
|
|
137
155
|
defaultConfig: () => ({}),
|
|
138
156
|
initialState: () => ({}),
|
|
139
|
-
render: () =>
|
|
157
|
+
render: () => <TrialLoupe />,
|
|
140
158
|
canvas: {
|
|
141
159
|
layers: [
|
|
142
160
|
{
|
|
@@ -147,7 +165,6 @@ describe('the loupe and the trial camera share one dispatcher', () => {
|
|
|
147
165
|
},
|
|
148
166
|
],
|
|
149
167
|
},
|
|
150
|
-
loupe: true,
|
|
151
168
|
});
|
|
152
169
|
const r = render(<Lab instruments={[drawn]} defaultInstrument="Zoomed" />);
|
|
153
170
|
const stack = r.container.querySelector('.lk-canvas-stack');
|