@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/loupe/types.ts
CHANGED
|
@@ -3,11 +3,9 @@ import type { ReactNode } from 'react';
|
|
|
3
3
|
import type { ViewportSize } from '../canvas/worldSpec';
|
|
4
4
|
import type { ViewTransform } from '../instrument/types';
|
|
5
5
|
|
|
6
|
-
/** What a DOM
|
|
7
|
-
*
|
|
8
|
-
export interface LoupeRenderArgs
|
|
9
|
-
state: TS;
|
|
10
|
-
config: TC;
|
|
6
|
+
/** What a DOM loupe's `render` is handed: the camera to draw the content
|
|
7
|
+
* through again. */
|
|
8
|
+
export interface LoupeRenderArgs {
|
|
11
9
|
/** The trial's own camera composed with the magnification, about the aimed
|
|
12
10
|
* point — so drawing the same content through it magnifies in place. */
|
|
13
11
|
view: ViewTransform;
|
|
@@ -21,14 +19,14 @@ export interface LoupeRenderArgs<TS = unknown, TC = unknown> {
|
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
/**
|
|
24
|
-
*
|
|
22
|
+
* How a lens magnifies.
|
|
25
23
|
*
|
|
26
|
-
* With no `render`, the
|
|
27
|
-
* zoomed camera, so it stays sharp at any factor.
|
|
28
|
-
*
|
|
24
|
+
* With no `render`, the lens re-draws the canvas stack's layers through a
|
|
25
|
+
* zoomed camera, so it stays sharp at any factor. Over DOM content, `render`
|
|
26
|
+
* draws it instead: given a camera, draw me again.
|
|
29
27
|
*/
|
|
30
|
-
export interface
|
|
31
|
-
render?: (args: LoupeRenderArgs
|
|
28
|
+
export interface LoupeOptions {
|
|
29
|
+
render?: (args: LoupeRenderArgs) => ReactNode;
|
|
32
30
|
/** Opening magnification, clamped to the bounds below. Default 6. */
|
|
33
31
|
factor?: number;
|
|
34
32
|
/** What the wheel clamps to. Defaults 2 and 32. */
|
|
@@ -48,12 +46,9 @@ export interface LoupeCapability<TS = unknown, TC = unknown> {
|
|
|
48
46
|
onColorChange?: (hex: string) => void;
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
/**
|
|
52
|
-
export
|
|
53
|
-
|
|
54
|
-
/** A {@link LoupeCapability} with every default filled in. */
|
|
55
|
-
export interface ResolvedLoupe<TS = unknown, TC = unknown> {
|
|
56
|
-
render?: (args: LoupeRenderArgs<TS, TC>) => ReactNode;
|
|
49
|
+
/** {@link LoupeOptions} with every default filled in. */
|
|
50
|
+
export interface ResolvedLoupe {
|
|
51
|
+
render?: (args: LoupeRenderArgs) => ReactNode;
|
|
57
52
|
onColorChange?: (hex: string) => void;
|
|
58
53
|
factor: number;
|
|
59
54
|
minFactor: number;
|
|
@@ -63,6 +58,7 @@ export interface ResolvedLoupe<TS = unknown, TC = unknown> {
|
|
|
63
58
|
peekKey: string | null;
|
|
64
59
|
}
|
|
65
60
|
|
|
61
|
+
/** What `resolveLoupe` fills in for anything a declaration leaves unset. */
|
|
66
62
|
export const LOUPE_DEFAULTS = {
|
|
67
63
|
factor: 6,
|
|
68
64
|
minFactor: 2,
|
|
@@ -72,19 +68,20 @@ export const LOUPE_DEFAULTS = {
|
|
|
72
68
|
peekKey: 'Alt',
|
|
73
69
|
} as const satisfies Omit<ResolvedLoupe, 'render' | 'onColorChange'>;
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
71
|
+
/** `options` with every default filled in and `factor` clamped to
|
|
72
|
+
* `[minFactor, maxFactor]`. A loupe with its own `render` is always `'vector'`. */
|
|
73
|
+
export function resolveLoupe(options: LoupeOptions = {}): ResolvedLoupe {
|
|
74
|
+
const minFactor = options.minFactor ?? LOUPE_DEFAULTS.minFactor;
|
|
75
|
+
const maxFactor = options.maxFactor ?? LOUPE_DEFAULTS.maxFactor;
|
|
79
76
|
return {
|
|
80
|
-
render:
|
|
81
|
-
onColorChange:
|
|
77
|
+
render: options.render,
|
|
78
|
+
onColorChange: options.onColorChange,
|
|
82
79
|
minFactor,
|
|
83
80
|
maxFactor,
|
|
84
|
-
factor: Math.min(maxFactor, Math.max(minFactor,
|
|
85
|
-
diameter:
|
|
86
|
-
peekKey:
|
|
81
|
+
factor: Math.min(maxFactor, Math.max(minFactor, options.factor ?? LOUPE_DEFAULTS.factor)),
|
|
82
|
+
diameter: options.diameter ?? LOUPE_DEFAULTS.diameter,
|
|
83
|
+
peekKey: options.peekKey === undefined ? LOUPE_DEFAULTS.peekKey : options.peekKey,
|
|
87
84
|
// A DOM loupe has no framebuffer, so `pixel` would have nothing to enlarge.
|
|
88
|
-
mode:
|
|
85
|
+
mode: options.render ? 'vector' : (options.mode ?? LOUPE_DEFAULTS.mode),
|
|
89
86
|
};
|
|
90
87
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import { WeaselProvider } from '@weasel-js/core';
|
|
3
|
+
import { renderThenAbandon } from '@weasel-js/react/testing/abandonRender';
|
|
3
4
|
import { type RefObject, StrictMode, useRef } from 'react';
|
|
4
5
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
6
|
import { LoupeGestures } from './LoupeGestures';
|
|
@@ -23,12 +24,12 @@ interface HarnessProps {
|
|
|
23
24
|
* cases below exercise the real dispatcher route rather than a stand-in. */
|
|
24
25
|
function Harness({ enabled = true, seen, sample, ...rest }: HarnessProps) {
|
|
25
26
|
const hostRef = useRef<HTMLDivElement | null>(null);
|
|
26
|
-
const
|
|
27
|
+
const options = resolveLoupe(rest);
|
|
27
28
|
const loupe = useLoupe({
|
|
28
29
|
hostRef: hostRef as RefObject<HTMLElement | null>,
|
|
29
30
|
enabled,
|
|
30
31
|
sample,
|
|
31
|
-
|
|
32
|
+
options,
|
|
32
33
|
});
|
|
33
34
|
seen?.(loupe);
|
|
34
35
|
return (
|
|
@@ -37,7 +38,7 @@ function Harness({ enabled = true, seen, sample, ...rest }: HarnessProps) {
|
|
|
37
38
|
<LoupeGestures
|
|
38
39
|
hostRef={hostRef as RefObject<HTMLElement | null>}
|
|
39
40
|
input={loupe.input}
|
|
40
|
-
peekKey={
|
|
41
|
+
peekKey={options.peekKey ?? null}
|
|
41
42
|
/>
|
|
42
43
|
</WeaselProvider>
|
|
43
44
|
<span data-testid="visible">{String(loupe.visible)}</span>
|
|
@@ -92,7 +93,7 @@ describe('useLoupe', () => {
|
|
|
92
93
|
expect(read('visible')).toBe('false');
|
|
93
94
|
});
|
|
94
95
|
|
|
95
|
-
it('does not peek when
|
|
96
|
+
it('does not peek when its options turned the key off', () => {
|
|
96
97
|
render(<Harness enabled={false} peekKey={null} />);
|
|
97
98
|
move(40, 25);
|
|
98
99
|
act(() => {
|
|
@@ -251,4 +252,28 @@ describe('useLoupe', () => {
|
|
|
251
252
|
});
|
|
252
253
|
expect(read('mode')).toBe('pixel');
|
|
253
254
|
});
|
|
255
|
+
|
|
256
|
+
it('aims as the committed `enabled` allows, not an abandoned render', () => {
|
|
257
|
+
const seen: LoupeState[] = [];
|
|
258
|
+
// Reads the aim back through the model: the DOM is not re-rendered while
|
|
259
|
+
// the abandoned transition is still pending.
|
|
260
|
+
const aimOf = (p: { x: number; y: number }) => `${p.x},${p.y}`;
|
|
261
|
+
renderThenAbandon({ enabled: false }, { enabled: true }, (p) => (
|
|
262
|
+
<Harness enabled={p.enabled} sample={aimOf} seen={(l) => seen.push(l)} />
|
|
263
|
+
));
|
|
264
|
+
const before = seen[0]?.pick();
|
|
265
|
+
move(40, 25);
|
|
266
|
+
expect(seen[0]?.pick()).toBe(before);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('samples through the committed `sample`, not an abandoned render', () => {
|
|
270
|
+
const seen: LoupeState[] = [];
|
|
271
|
+
const committed = vi.fn(() => '#111111');
|
|
272
|
+
const abandoned = vi.fn(() => '#222222');
|
|
273
|
+
renderThenAbandon({ sample: committed }, { sample: abandoned }, (p) => (
|
|
274
|
+
<Harness sample={p.sample} seen={(l) => seen.push(l)} />
|
|
275
|
+
));
|
|
276
|
+
expect(seen[0]?.pick()).toBe('#111111');
|
|
277
|
+
expect(abandoned).not.toHaveBeenCalled();
|
|
278
|
+
});
|
|
254
279
|
});
|
package/src/loupe/useLoupe.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLatest } from '@weasel-js/core';
|
|
1
2
|
import {
|
|
2
3
|
createLoupeModel,
|
|
3
4
|
type LoupeMode,
|
|
@@ -11,7 +12,7 @@ import type { ResolvedLoupe } from './types';
|
|
|
11
12
|
|
|
12
13
|
/** Options for {@link useLoupe}. */
|
|
13
14
|
export interface UseLoupeOptions {
|
|
14
|
-
|
|
15
|
+
options: ResolvedLoupe;
|
|
15
16
|
/** The element the lens tracks the pointer across. */
|
|
16
17
|
hostRef: RefObject<HTMLElement | null>;
|
|
17
18
|
/** Whether the loupe is turned on. Hold-to-peek shows it regardless. */
|
|
@@ -39,6 +40,11 @@ export interface LoupeState {
|
|
|
39
40
|
input: LoupeInputApi;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/** Whether the lens is up: the pointer is over the host, and the loupe is on or peeked. */
|
|
44
|
+
function lensShown(over: boolean, enabled: boolean, peeking: boolean): boolean {
|
|
45
|
+
return over && (enabled || peeking);
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
/**
|
|
43
49
|
* Binds `@weasel-js/loupe`'s model to a host element and tracks the pointer
|
|
44
50
|
* across it.
|
|
@@ -50,31 +56,28 @@ export interface LoupeState {
|
|
|
50
56
|
* has no continuous-motion entry, and inventing one to carry the lens' aim
|
|
51
57
|
* would be an input taxonomy change, not a loupe change.
|
|
52
58
|
*/
|
|
53
|
-
export function useLoupe({
|
|
59
|
+
export function useLoupe({ options, hostRef, enabled, sample }: UseLoupeOptions): LoupeState {
|
|
54
60
|
const [, bump] = useReducer((n: number) => n + 1, 0);
|
|
55
61
|
|
|
56
62
|
const overRef = useRef(false);
|
|
57
63
|
const peekingRef = useRef(false);
|
|
58
|
-
const enabledRef =
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
sampleRef.current = sample;
|
|
62
|
-
const onColorChangeRef = useRef(capability.onColorChange);
|
|
63
|
-
onColorChangeRef.current = capability.onColorChange;
|
|
64
|
+
const enabledRef = useLatest(enabled);
|
|
65
|
+
const sampleRef = useLatest(sample);
|
|
66
|
+
const onColorChangeRef = useLatest(options.onColorChange);
|
|
64
67
|
|
|
65
68
|
// Reads nothing but refs, so it is stable and an effect may depend on it.
|
|
66
69
|
const shown = useCallback(
|
|
67
|
-
(): boolean => overRef.current
|
|
68
|
-
[],
|
|
70
|
+
(): boolean => lensShown(overRef.current, enabledRef.current, peekingRef.current),
|
|
71
|
+
[enabledRef],
|
|
69
72
|
);
|
|
70
73
|
|
|
71
74
|
const modelRef = useRef<LoupeModel | null>(null);
|
|
72
75
|
if (modelRef.current === null) {
|
|
73
76
|
modelRef.current = createLoupeModel({
|
|
74
|
-
mode:
|
|
75
|
-
factor:
|
|
76
|
-
minFactor:
|
|
77
|
-
maxFactor:
|
|
77
|
+
mode: options.mode,
|
|
78
|
+
factor: options.factor,
|
|
79
|
+
minFactor: options.minFactor,
|
|
80
|
+
maxFactor: options.maxFactor,
|
|
78
81
|
onColorChange: (hex) => onColorChangeRef.current?.(hex),
|
|
79
82
|
surface: {
|
|
80
83
|
lens: () => {
|
|
@@ -94,8 +97,7 @@ export function useLoupe({ capability, hostRef, enabled, sample }: UseLoupeOptio
|
|
|
94
97
|
}
|
|
95
98
|
const model = modelRef.current;
|
|
96
99
|
|
|
97
|
-
const diameterRef =
|
|
98
|
-
diameterRef.current = capability.diameter;
|
|
100
|
+
const diameterRef = useLatest(options.diameter);
|
|
99
101
|
const goneRef = useRef(false);
|
|
100
102
|
|
|
101
103
|
// The model holds no resources, and `dispose` is one-way — so unmounting only
|
|
@@ -156,7 +158,8 @@ export function useLoupe({ capability, hostRef, enabled, sample }: UseLoupeOptio
|
|
|
156
158
|
}
|
|
157
159
|
|
|
158
160
|
return {
|
|
159
|
-
|
|
161
|
+
// This render's `enabled`: `enabledRef` holds the last committed one.
|
|
162
|
+
visible: lensShown(overRef.current, enabled, peekingRef.current),
|
|
160
163
|
aim: model.aim,
|
|
161
164
|
factor: model.factor,
|
|
162
165
|
mode: model.mode,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Every annotation target's marks, drawn read-only on the overview through the
|
|
3
|
-
* export's own vector path: `markSvgNodes`, then `serializeSvg
|
|
3
|
+
* export's own vector path: `markSvgNodes`, then `serializeSvg` — synchronous
|
|
4
|
+
* with no `warmSvg`, since marks export nothing that loads on demand.
|
|
4
5
|
*/
|
|
5
6
|
import type { View } from '@weasel-js/core';
|
|
6
7
|
import { serializeSvg } from '@weasel-js/svg';
|
|
@@ -40,10 +41,17 @@ function elementOf(target: object): HTMLElement | null {
|
|
|
40
41
|
return (target as { ref?: RefObject<HTMLElement | null> }).ref?.current ?? null;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function markupOf(
|
|
44
|
+
function markupOf(
|
|
45
|
+
api: AnnotationsApi,
|
|
46
|
+
target: string,
|
|
47
|
+
content: { w: number; h: number },
|
|
48
|
+
shown: { w: number; h: number },
|
|
49
|
+
): string {
|
|
44
50
|
const painted = api.paintedMarks(target);
|
|
45
51
|
if (painted.length === 0) return '';
|
|
46
|
-
|
|
52
|
+
// The document is shown at `shown` px, fitted like any SVG viewBox.
|
|
53
|
+
const k = Math.min(shown.w / content.w, shown.h / content.h) || 1;
|
|
54
|
+
const nodes = painted.flatMap(({ mark, style }) => markSvgNodes(mark, style, { x: k, y: k }));
|
|
47
55
|
return serializeSvg(nodes, { viewBox: { x: 0, y: 0, width: content.w, height: content.h } });
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -69,7 +77,7 @@ export function OverviewMarks({ fit, camera }: OverviewMarksProps) {
|
|
|
69
77
|
{api.targets().map((t) => {
|
|
70
78
|
const el = elementOf(t);
|
|
71
79
|
const box = el ? localRectOf(el, camera) : null;
|
|
72
|
-
const markup = box ? markupOf(api, t.id, t.content) : '';
|
|
80
|
+
const markup = box ? markupOf(api, t.id, t.content, { w: box.w * s, h: box.h * s }) : '';
|
|
73
81
|
if (!box || !markup) return null;
|
|
74
82
|
const place = {
|
|
75
83
|
['--lk-overview-mx' as string]: `${(box.x - fit.x) * s}px`,
|
|
@@ -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,
|
|
@@ -163,11 +164,13 @@ function Overview({
|
|
|
163
164
|
}: TrialOverviewProps & { camera: CameraContextValue }) {
|
|
164
165
|
const size = useMemo(() => ({ width, height }), [width, height]);
|
|
165
166
|
const content = bounds ? toLocalRect(bounds, camera.frame) : camera.content;
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
// `content` is rebuilt every render, so the fit keys on its numbers.
|
|
168
|
+
const framed = content !== null && content !== undefined;
|
|
169
|
+
const { x = 0, y = 0, width: w = 0, height: h = 0 } = content ?? {};
|
|
168
170
|
const fit = useMemo(
|
|
169
|
-
() =>
|
|
170
|
-
|
|
171
|
+
() =>
|
|
172
|
+
framed ? fitRect({ x, y, width: w, height: h }, size) : { x: 0, y: 0, scale: { x: 1, y: 1 } },
|
|
173
|
+
[framed, x, y, w, h, size],
|
|
171
174
|
);
|
|
172
175
|
const boxRef = useRef<HTMLDivElement | null>(null);
|
|
173
176
|
|
|
@@ -312,8 +315,7 @@ function OverviewDispatch({
|
|
|
312
315
|
camera: CameraContextValue;
|
|
313
316
|
}) {
|
|
314
317
|
const registry = useActionsRegistry();
|
|
315
|
-
const fitRef =
|
|
316
|
-
fitRef.current = fit;
|
|
318
|
+
const fitRef = useLatest(fit);
|
|
317
319
|
const root = camera.view;
|
|
318
320
|
useDepSource('rootView', () => root as ViewApi);
|
|
319
321
|
|
|
@@ -334,14 +336,14 @@ function OverviewDispatch({
|
|
|
334
336
|
y: (cy - (r?.top ?? 0)) / f.scale.y + f.y,
|
|
335
337
|
};
|
|
336
338
|
},
|
|
337
|
-
[boxRef],
|
|
339
|
+
[boxRef, fitRef],
|
|
338
340
|
);
|
|
339
341
|
|
|
340
342
|
useGestureDispatcher({
|
|
341
343
|
canvasRef: boxRef,
|
|
342
344
|
// Always present: `OverviewInput` mounts the provider directly above.
|
|
343
345
|
actions: registry as NonNullable<typeof registry>,
|
|
344
|
-
|
|
346
|
+
entriesById: TOOLS_BY_ID,
|
|
345
347
|
clientToWorld: clientToLocal,
|
|
346
348
|
keyboard: false,
|
|
347
349
|
channels: CHANNELS,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
Sidebar,
|
|
25
25
|
Slider,
|
|
26
26
|
Switch,
|
|
27
|
+
sampleByInterpolation,
|
|
27
28
|
Tabs,
|
|
28
29
|
ToolPalette,
|
|
29
30
|
useReorderDragList,
|
|
@@ -38,6 +39,7 @@ describe('weasel-ui passthrough', () => {
|
|
|
38
39
|
expect(typeof useReorderDragList).toBe('function');
|
|
39
40
|
expect(typeof paintGradientTrack).toBe('function');
|
|
40
41
|
expect(typeof oklchToHex).toBe('function');
|
|
42
|
+
expect(typeof sampleByInterpolation).toBe('function');
|
|
41
43
|
expect(typeof chromaAt).toBe('function');
|
|
42
44
|
});
|
|
43
45
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* future without consumer churn.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
export { formatNumber, MINUS_SIGN } from '@weasel-js/quantity';
|
|
8
9
|
export {
|
|
9
10
|
ActionBar,
|
|
10
11
|
type ActionBarProps,
|
|
@@ -21,6 +22,7 @@ export {
|
|
|
21
22
|
type BadgeVariant,
|
|
22
23
|
type BoundsCtx,
|
|
23
24
|
type BuiltInEdgeName,
|
|
25
|
+
type BuiltInProfileName,
|
|
24
26
|
type BuiltinPref,
|
|
25
27
|
Button,
|
|
26
28
|
ButtonBar,
|
|
@@ -38,6 +40,9 @@ export {
|
|
|
38
40
|
type CheckboxProps,
|
|
39
41
|
type ChromaCurve,
|
|
40
42
|
type ChromaCurvePoint,
|
|
43
|
+
CloseButton,
|
|
44
|
+
type CloseButtonProps,
|
|
45
|
+
COLOR_MODE_OPTIONS,
|
|
41
46
|
ColorModeControl,
|
|
42
47
|
type ColorModeControlProps,
|
|
43
48
|
ComboBox,
|
|
@@ -65,8 +70,8 @@ export {
|
|
|
65
70
|
type DisclosureRowProps,
|
|
66
71
|
DragGhost,
|
|
67
72
|
type DragGhostProps,
|
|
68
|
-
|
|
69
|
-
type
|
|
73
|
+
DragGrip,
|
|
74
|
+
type DragGripProps,
|
|
70
75
|
detectPlatform,
|
|
71
76
|
dlog,
|
|
72
77
|
EDGE_PROFILES,
|
|
@@ -78,11 +83,13 @@ export {
|
|
|
78
83
|
type FieldProps,
|
|
79
84
|
type FillSettings,
|
|
80
85
|
fieldClasses,
|
|
81
|
-
formatNumber,
|
|
82
86
|
formatShortcut,
|
|
83
87
|
formatShortcutParts,
|
|
88
|
+
GestureRoute,
|
|
89
|
+
type GestureRouteProps,
|
|
84
90
|
type GradientTrackOpts,
|
|
85
91
|
type GridSettings,
|
|
92
|
+
gestureRouteSegments,
|
|
86
93
|
ICON_PATHS,
|
|
87
94
|
Icon,
|
|
88
95
|
type IconName,
|
|
@@ -98,6 +105,7 @@ export {
|
|
|
98
105
|
type KeyCapVariant,
|
|
99
106
|
type KeycapKind,
|
|
100
107
|
KeySequence,
|
|
108
|
+
type KeySequenceJoins,
|
|
101
109
|
type KeySequenceProps,
|
|
102
110
|
type KeySpec,
|
|
103
111
|
keyGlyph,
|
|
@@ -115,7 +123,6 @@ export {
|
|
|
115
123
|
MenuButton,
|
|
116
124
|
type MenuButtonItem,
|
|
117
125
|
type MenuButtonProps,
|
|
118
|
-
MINUS_SIGN,
|
|
119
126
|
moveLayers,
|
|
120
127
|
NumberField,
|
|
121
128
|
type NumberFieldProps,
|
|
@@ -138,6 +145,7 @@ export {
|
|
|
138
145
|
type PowerlineSegment,
|
|
139
146
|
type PrefBoolean,
|
|
140
147
|
type PrefBooleanControl,
|
|
148
|
+
type PrefBooleanEncoding,
|
|
141
149
|
type PrefColor,
|
|
142
150
|
type PrefCustom,
|
|
143
151
|
type PrefEnum,
|
|
@@ -174,7 +182,7 @@ export {
|
|
|
174
182
|
type ReorderDragHandlers,
|
|
175
183
|
type ReorderDragState,
|
|
176
184
|
type ReorderGhost,
|
|
177
|
-
type
|
|
185
|
+
type RovingOrientation,
|
|
178
186
|
type RovingTabIndex,
|
|
179
187
|
type SegmentTooltipFields,
|
|
180
188
|
Select,
|
|
@@ -191,6 +199,7 @@ export {
|
|
|
191
199
|
type StanceProps,
|
|
192
200
|
Switch,
|
|
193
201
|
type SwitchProps,
|
|
202
|
+
sampleByInterpolation,
|
|
194
203
|
Tab,
|
|
195
204
|
TabList,
|
|
196
205
|
type TabListProps,
|
|
@@ -199,6 +208,9 @@ export {
|
|
|
199
208
|
type TabProps,
|
|
200
209
|
Tabs,
|
|
201
210
|
type TabsProps,
|
|
211
|
+
ThemeSwitcher,
|
|
212
|
+
type ThemeSwitcherOption,
|
|
213
|
+
type ThemeSwitcherProps,
|
|
202
214
|
type Thumb,
|
|
203
215
|
type ThumbRenderCtx,
|
|
204
216
|
type ThumbShape,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { DRAG_THRESHOLD_PX } from '@weasel-js/core';
|
|
2
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import { createMemoryAdapter } from '../state/adapters';
|
|
4
5
|
import { labPrefix } from '../state/labRecords';
|
|
@@ -130,6 +131,16 @@ describe('FloatingPanel dragging', () => {
|
|
|
130
131
|
expect(panel.dataset.dragging).toBeUndefined();
|
|
131
132
|
});
|
|
132
133
|
|
|
134
|
+
it('becomes a drag at the kit drag threshold, not before', () => {
|
|
135
|
+
const { panel } = renderPanel(<FloatingPanel>x</FloatingPanel>);
|
|
136
|
+
fireEvent.pointerDown(panel, { clientX: 100, clientY: 100 });
|
|
137
|
+
fireEvent.pointerMove(panel, { clientX: 100 + DRAG_THRESHOLD_PX - 0.5, clientY: 100 });
|
|
138
|
+
expect(panel.dataset.dragging).toBeUndefined();
|
|
139
|
+
fireEvent.pointerMove(panel, { clientX: 100 + DRAG_THRESHOLD_PX, clientY: 100 });
|
|
140
|
+
expect(panel.dataset.dragging).toBe('true');
|
|
141
|
+
fireEvent.pointerUp(panel);
|
|
142
|
+
});
|
|
143
|
+
|
|
133
144
|
it('a press that never moves is not a drag', () => {
|
|
134
145
|
const { panel } = renderPanel(<FloatingPanel anchor="top-left">x</FloatingPanel>);
|
|
135
146
|
const before = panel.style.left;
|
|
@@ -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 {
|
|
@@ -41,9 +46,6 @@ function borderBoxOf(entry: ResizeObserverEntry): { w: number; h: number } {
|
|
|
41
46
|
return { w: entry.contentRect.width, h: entry.contentRect.height };
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
/** How far the pointer must travel before a press becomes a drag. */
|
|
45
|
-
const DRAG_THRESHOLD = 3;
|
|
46
|
-
|
|
47
49
|
/** A pointerdown on one of these is the child's, not a drag. */
|
|
48
50
|
function isInteractive(target: EventTarget | null): boolean {
|
|
49
51
|
if (!(target instanceof Element)) return false;
|
|
@@ -122,8 +124,7 @@ export function FloatingPanel({
|
|
|
122
124
|
// A drag outlives the render that started it, so the reducer's inputs are
|
|
123
125
|
// read here and not from that render's closure — a container resized
|
|
124
126
|
// mid-drag would otherwise place the panel against its old box.
|
|
125
|
-
const layout =
|
|
126
|
-
layout.current = { container, options, item };
|
|
127
|
+
const layout = useLatest({ container, options, item });
|
|
127
128
|
|
|
128
129
|
useEffect(() => () => session.current?.cancel(), []);
|
|
129
130
|
|
|
@@ -158,7 +159,7 @@ export function FloatingPanel({
|
|
|
158
159
|
{
|
|
159
160
|
onMove: (ev) => {
|
|
160
161
|
if (!dragging.current) {
|
|
161
|
-
if (
|
|
162
|
+
if (!pastDragThreshold({ clientX: start.x, clientY: start.y }, ev)) return;
|
|
162
163
|
dragging.current = { ...start };
|
|
163
164
|
setDragging(true);
|
|
164
165
|
origin.setPointerCapture?.(pointerId);
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
height: 100%;
|
|
19
19
|
background: var(--wzl-accent);
|
|
20
20
|
transform-origin: left center;
|
|
21
|
-
transition: transform
|
|
21
|
+
transition: transform var(--wzl-motion-fast) linear;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// A job that never reports a total sweeps instead of filling, so the bar
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
&__count {
|
|
33
33
|
font-variant-numeric: tabular-nums;
|
|
34
|
+
font-family: var(--wzl-font-numeric);
|
|
34
35
|
white-space: nowrap;
|
|
35
36
|
}
|
|
36
37
|
|