@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
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { getAlpha01, isBuiltinToolPref, toHex8, withAlpha01 } from '@weasel-js/core';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Focusable,
|
|
5
|
+
isPrefLeaf,
|
|
6
|
+
type PrefLeaf,
|
|
7
|
+
type PropertyControlProps,
|
|
8
|
+
PropertyHelp,
|
|
9
|
+
PropertyList,
|
|
10
|
+
PropertyPanel,
|
|
11
|
+
prefFieldProps,
|
|
12
|
+
type StanceProps,
|
|
13
|
+
Tooltip,
|
|
14
|
+
TooltipTrigger,
|
|
15
|
+
useOverlayPortal,
|
|
16
|
+
} from '@weasel-js/ui';
|
|
17
|
+
import {
|
|
18
|
+
type CSSProperties,
|
|
19
|
+
type DOMAttributes,
|
|
20
|
+
type MouseEvent,
|
|
21
|
+
type ReactElement,
|
|
22
|
+
type ReactNode,
|
|
23
|
+
useRef,
|
|
24
|
+
useState,
|
|
25
|
+
} from 'react';
|
|
26
|
+
import { Dialog as RACDialog, Popover as RACPopover } from 'react-aria-components';
|
|
27
|
+
import { auto as autoValue } from '../config/auto';
|
|
28
|
+
import { schemaNodeAtPath, valueAtPath } from '../config/path';
|
|
29
|
+
import type { ResolvedConfig } from '../config/types';
|
|
30
|
+
import { ControlRow } from './ControlPanel';
|
|
31
|
+
import { summarizeValue } from './inDialog';
|
|
32
|
+
|
|
33
|
+
/** One column of a {@link ControlMatrix}: a config group, every one the same shape. */
|
|
34
|
+
export interface ControlMatrixColumn {
|
|
35
|
+
/** Config path prefix of this column's group, e.g. `'looks.window'`. */
|
|
36
|
+
key: string;
|
|
37
|
+
/** Short header shown at narrow width, e.g. `'Win'`. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** Full name, for the header's tooltip and accessible name, e.g. `'Window'`. */
|
|
40
|
+
title?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One row of a {@link ControlMatrix}: a setting every column's group may hold. */
|
|
44
|
+
export interface ControlMatrixRow {
|
|
45
|
+
/** Path of the setting inside each column's group, e.g. `'background'` or `'fill.colorBy'`. */
|
|
46
|
+
key: string;
|
|
47
|
+
/** Row label. Defaults to the leaf's own name from the schema. */
|
|
48
|
+
label?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ControlMatrixProps extends StanceProps {
|
|
52
|
+
/** Heads the matrix. With `title`, `stance` or `tone` given it sits in a
|
|
53
|
+
* `<PropertyPanel>`, as a `ControlPanel` does. */
|
|
54
|
+
title?: ReactNode;
|
|
55
|
+
/** The whole resolved schema. A cell's leaf is looked up at
|
|
56
|
+
* `${column.key}.${row.key}`; a cell with none draws a dash. */
|
|
57
|
+
schema: ResolvedConfig;
|
|
58
|
+
/** The scopes. The first is the fallback the others inherit from: its cells
|
|
59
|
+
* never inherit, and its header is emphasized. */
|
|
60
|
+
columns: readonly ControlMatrixColumn[];
|
|
61
|
+
rows: readonly ControlMatrixRow[];
|
|
62
|
+
/** Values to show, already resolved: an inherited cell's value is the one it
|
|
63
|
+
* inherits. */
|
|
64
|
+
config: Record<string, unknown>;
|
|
65
|
+
/** Full dotted paths whose cells inherit, drawn ghosted. */
|
|
66
|
+
auto: ReadonlySet<string>;
|
|
67
|
+
/** Writes a value, which pins the cell, or labkit's `auto` sentinel, which
|
|
68
|
+
* unpins it. */
|
|
69
|
+
setConfig: (path: string, value: unknown) => void;
|
|
70
|
+
/** Clicking a column header. Without it the headers are plain text. */
|
|
71
|
+
onColumnClick?: (column: ControlMatrixColumn) => void;
|
|
72
|
+
/** What an inherited cell's tooltip says. Defaults to `from <first column>`. */
|
|
73
|
+
inheritHint?: (column: ControlMatrixColumn) => string;
|
|
74
|
+
className?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const NO_AUTO: ReadonlySet<string> = new Set();
|
|
78
|
+
const noop = (): void => {};
|
|
79
|
+
|
|
80
|
+
const nameOf = (column: ControlMatrixColumn): string => column.title ?? column.label;
|
|
81
|
+
|
|
82
|
+
/** The open popover: which cell, and the element it hangs off. */
|
|
83
|
+
interface OpenCell {
|
|
84
|
+
path: string;
|
|
85
|
+
leaf: PrefLeaf;
|
|
86
|
+
column: ControlMatrixColumn;
|
|
87
|
+
first: boolean;
|
|
88
|
+
label: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* One setting across several config groups of the same shape: settings down
|
|
93
|
+
* the side, scopes across the top, a compact read-only cell at each crossing.
|
|
94
|
+
* A cell inherits from the first column until it is pinned; inherited cells
|
|
95
|
+
* draw ghosted. Click a cell to edit it with the control a `ControlPanel` would
|
|
96
|
+
* draw (a boolean flips and a color opens its picker in place); editing pins,
|
|
97
|
+
* and the popover's Inherit button — or Option/Alt-click on the cell — unpins.
|
|
98
|
+
*/
|
|
99
|
+
export function ControlMatrix({
|
|
100
|
+
title,
|
|
101
|
+
stance,
|
|
102
|
+
tone,
|
|
103
|
+
schema,
|
|
104
|
+
columns,
|
|
105
|
+
rows,
|
|
106
|
+
config,
|
|
107
|
+
auto,
|
|
108
|
+
setConfig,
|
|
109
|
+
onColumnClick,
|
|
110
|
+
inheritHint,
|
|
111
|
+
className,
|
|
112
|
+
}: ControlMatrixProps): ReactElement {
|
|
113
|
+
const fallback = columns[0];
|
|
114
|
+
const hintFor =
|
|
115
|
+
inheritHint ?? ((_: ControlMatrixColumn) => (fallback ? `from ${nameOf(fallback)}` : ''));
|
|
116
|
+
const [open, setOpen] = useState<OpenCell | null>(null);
|
|
117
|
+
const triggerRef = useRef<HTMLElement | null>(null);
|
|
118
|
+
const { anchor, portalProps } = useOverlayPortal();
|
|
119
|
+
|
|
120
|
+
const leafAt = (path: string): PrefLeaf | undefined => {
|
|
121
|
+
const node = schemaNodeAtPath(schema.group, path);
|
|
122
|
+
return node && isPrefLeaf(node) ? node : undefined;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const rowSpecs = rows.map((row) => {
|
|
126
|
+
const leaves = columns.map((column) => leafAt(`${column.key}.${row.key}`));
|
|
127
|
+
const named = leaves.find((leaf) => leaf !== undefined);
|
|
128
|
+
return { row, leaves, label: row.label ?? named?.name ?? row.key, about: named?.description };
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const table = (
|
|
132
|
+
<div className={className ? `lk-control-matrix ${className}` : 'lk-control-matrix'}>
|
|
133
|
+
{anchor}
|
|
134
|
+
<table className="lk-control-matrix__table">
|
|
135
|
+
<thead>
|
|
136
|
+
<tr>
|
|
137
|
+
<td className="lk-control-matrix__corner" />
|
|
138
|
+
{columns.map((column, i) => (
|
|
139
|
+
<th
|
|
140
|
+
key={column.key}
|
|
141
|
+
scope="col"
|
|
142
|
+
className={i === 0 ? 'lk-control-matrix__head is-first' : 'lk-control-matrix__head'}
|
|
143
|
+
>
|
|
144
|
+
<ColumnHeader column={column} onClick={onColumnClick} />
|
|
145
|
+
</th>
|
|
146
|
+
))}
|
|
147
|
+
</tr>
|
|
148
|
+
</thead>
|
|
149
|
+
<tbody>
|
|
150
|
+
{rowSpecs.map(({ row, leaves, label, about }) => (
|
|
151
|
+
<tr key={row.key}>
|
|
152
|
+
<th scope="row" className="lk-control-matrix__label">
|
|
153
|
+
<span className="lk-control-matrix__label-text">{label}</span>
|
|
154
|
+
{about ? <PropertyHelp label={label} description={about} /> : null}
|
|
155
|
+
</th>
|
|
156
|
+
{columns.map((column, i) => {
|
|
157
|
+
const path = `${column.key}.${row.key}`;
|
|
158
|
+
const leaf = leaves[i];
|
|
159
|
+
if (!leaf) {
|
|
160
|
+
return (
|
|
161
|
+
<td key={column.key} className="lk-control-matrix__cell is-missing">
|
|
162
|
+
<span role="img" aria-label={`${nameOf(column)} has no ${label}`}>
|
|
163
|
+
—
|
|
164
|
+
</span>
|
|
165
|
+
</td>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const first = i === 0;
|
|
169
|
+
const inherited = !first && auto.has(path);
|
|
170
|
+
return (
|
|
171
|
+
<td
|
|
172
|
+
key={column.key}
|
|
173
|
+
className={
|
|
174
|
+
inherited ? 'lk-control-matrix__cell is-inherited' : 'lk-control-matrix__cell'
|
|
175
|
+
}
|
|
176
|
+
>
|
|
177
|
+
<Cell
|
|
178
|
+
path={path}
|
|
179
|
+
leaf={leaf}
|
|
180
|
+
value={valueAtPath(config, path)}
|
|
181
|
+
name={`${nameOf(column)} ${label}`}
|
|
182
|
+
hint={inherited ? hintFor(column) : undefined}
|
|
183
|
+
canUnpin={!first && !inherited}
|
|
184
|
+
setConfig={setConfig}
|
|
185
|
+
onOpen={(el) => {
|
|
186
|
+
triggerRef.current = el;
|
|
187
|
+
setOpen({ path, leaf, column, first, label });
|
|
188
|
+
}}
|
|
189
|
+
/>
|
|
190
|
+
</td>
|
|
191
|
+
);
|
|
192
|
+
})}
|
|
193
|
+
</tr>
|
|
194
|
+
))}
|
|
195
|
+
</tbody>
|
|
196
|
+
</table>
|
|
197
|
+
<RACPopover
|
|
198
|
+
triggerRef={triggerRef}
|
|
199
|
+
isOpen={open !== null}
|
|
200
|
+
onOpenChange={(next) => {
|
|
201
|
+
if (!next) setOpen(null);
|
|
202
|
+
}}
|
|
203
|
+
placement="bottom"
|
|
204
|
+
className="lk-control-matrix__popover"
|
|
205
|
+
data-weasel-overlay=""
|
|
206
|
+
{...portalProps}
|
|
207
|
+
>
|
|
208
|
+
{open ? (
|
|
209
|
+
<RACDialog
|
|
210
|
+
className="lk-control-matrix__editor"
|
|
211
|
+
aria-label={`${nameOf(open.column)} ${open.label}`}
|
|
212
|
+
>
|
|
213
|
+
<PropertyList pack="one-up">
|
|
214
|
+
<ControlRow
|
|
215
|
+
path={open.path}
|
|
216
|
+
leaf={open.leaf}
|
|
217
|
+
resolved={schema}
|
|
218
|
+
config={config}
|
|
219
|
+
shown={config}
|
|
220
|
+
setConfig={setConfig}
|
|
221
|
+
pack="one-up"
|
|
222
|
+
auto={NO_AUTO}
|
|
223
|
+
setRowAuto={noop}
|
|
224
|
+
/>
|
|
225
|
+
</PropertyList>
|
|
226
|
+
{clearable(open.leaf) ? (
|
|
227
|
+
<div className="lk-control-matrix__editor-actions">
|
|
228
|
+
<Button
|
|
229
|
+
size="sm"
|
|
230
|
+
variant="ghost"
|
|
231
|
+
disabled={!valueAtPath(config, open.path) || auto.has(open.path)}
|
|
232
|
+
onClick={() => setConfig(open.path, null)}
|
|
233
|
+
>
|
|
234
|
+
Clear
|
|
235
|
+
</Button>
|
|
236
|
+
</div>
|
|
237
|
+
) : null}
|
|
238
|
+
{open.first ? null : (
|
|
239
|
+
<div className="lk-control-matrix__editor-actions">
|
|
240
|
+
<Button
|
|
241
|
+
size="sm"
|
|
242
|
+
variant="ghost"
|
|
243
|
+
disabled={auto.has(open.path)}
|
|
244
|
+
tooltip={`Inherit ${hintFor(open.column)}`}
|
|
245
|
+
onClick={() => setConfig(open.path, autoValue)}
|
|
246
|
+
>
|
|
247
|
+
Inherit
|
|
248
|
+
</Button>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
</RACDialog>
|
|
252
|
+
) : null}
|
|
253
|
+
</RACPopover>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (title === undefined && stance === undefined && tone === undefined) return table;
|
|
258
|
+
return (
|
|
259
|
+
<PropertyPanel title={title} stance={stance} tone={tone}>
|
|
260
|
+
{table}
|
|
261
|
+
</PropertyPanel>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function ColumnHeader({
|
|
266
|
+
column,
|
|
267
|
+
onClick,
|
|
268
|
+
}: {
|
|
269
|
+
column: ControlMatrixColumn;
|
|
270
|
+
onClick?: (column: ControlMatrixColumn) => void;
|
|
271
|
+
}) {
|
|
272
|
+
const name = nameOf(column);
|
|
273
|
+
if (!onClick) {
|
|
274
|
+
return column.title && column.title !== column.label ? (
|
|
275
|
+
<abbr title={column.title} className="lk-control-matrix__head-text">
|
|
276
|
+
{column.label}
|
|
277
|
+
</abbr>
|
|
278
|
+
) : (
|
|
279
|
+
<span className="lk-control-matrix__head-text">{column.label}</span>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return (
|
|
283
|
+
<TooltipTrigger>
|
|
284
|
+
<Focusable>
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
className="lk-control-matrix__head-button"
|
|
288
|
+
aria-label={name}
|
|
289
|
+
onClick={() => onClick(column)}
|
|
290
|
+
>
|
|
291
|
+
{column.label}
|
|
292
|
+
</button>
|
|
293
|
+
</Focusable>
|
|
294
|
+
<Tooltip>{name}</Tooltip>
|
|
295
|
+
</TooltipTrigger>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** A color leaf whose default is empty: unset is a value of its own, not black. */
|
|
300
|
+
const clearable = (leaf: PrefLeaf): boolean => leaf.kind === 'color' && leaf.default === '';
|
|
301
|
+
|
|
302
|
+
/** The number of decimals a step implies: `0.05` shows two, `1` none. */
|
|
303
|
+
function decimalsOf(step: number | undefined): number {
|
|
304
|
+
if (step === undefined || !Number.isFinite(step) || step <= 0) return 0;
|
|
305
|
+
const text = String(step);
|
|
306
|
+
const exp = /e-(\d+)$/.exec(text);
|
|
307
|
+
if (exp) return Number(exp[1]);
|
|
308
|
+
const dot = text.indexOf('.');
|
|
309
|
+
return dot === -1 ? 0 : text.length - dot - 1;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** What a cell shows of a value it opens a popover to edit. */
|
|
313
|
+
function summaryOf(field: PropertyControlProps | null, value: unknown): ReactNode {
|
|
314
|
+
if (field?.kind === 'number') {
|
|
315
|
+
return typeof field.value === 'number' ? field.value.toFixed(decimalsOf(field.step)) : '—';
|
|
316
|
+
}
|
|
317
|
+
if (field?.kind === 'enum') {
|
|
318
|
+
const option = field.options.find((o) => o.value === field.value);
|
|
319
|
+
if (!option) return '—';
|
|
320
|
+
return typeof option.glyph === 'string' ? option.glyph : option.label;
|
|
321
|
+
}
|
|
322
|
+
if (field?.kind === 'string') return field.value || '—';
|
|
323
|
+
return summarizeValue(value);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function Cell({
|
|
327
|
+
path,
|
|
328
|
+
leaf,
|
|
329
|
+
value,
|
|
330
|
+
name,
|
|
331
|
+
hint,
|
|
332
|
+
canUnpin,
|
|
333
|
+
setConfig,
|
|
334
|
+
onOpen,
|
|
335
|
+
}: {
|
|
336
|
+
path: string;
|
|
337
|
+
leaf: PrefLeaf;
|
|
338
|
+
value: unknown;
|
|
339
|
+
/** The cell's accessible name: column and row. */
|
|
340
|
+
name: string;
|
|
341
|
+
/** Present while the cell inherits: where from. */
|
|
342
|
+
hint?: string;
|
|
343
|
+
canUnpin: boolean;
|
|
344
|
+
setConfig: (path: string, value: unknown) => void;
|
|
345
|
+
onOpen: (el: HTMLElement) => void;
|
|
346
|
+
}) {
|
|
347
|
+
const shown = value ?? leaf.default;
|
|
348
|
+
const write = (next: unknown): void => setConfig(path, next);
|
|
349
|
+
const field = isBuiltinToolPref(leaf)
|
|
350
|
+
? prefFieldProps(leaf, { value: shown, setValue: write })
|
|
351
|
+
: null;
|
|
352
|
+
const label = hint ? `${name}, ${hint}` : name;
|
|
353
|
+
|
|
354
|
+
// Option/Alt-click hands a pinned cell back to the fallback, and does nothing
|
|
355
|
+
// else on any cell — it must not also flip or open what it lands on.
|
|
356
|
+
const unpinned = (e: MouseEvent): boolean => {
|
|
357
|
+
if (!e.altKey) return false;
|
|
358
|
+
e.preventDefault();
|
|
359
|
+
if (canUnpin) setConfig(path, autoValue);
|
|
360
|
+
return true;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// The shape `Focusable` asks of the element it makes a tooltip trigger.
|
|
364
|
+
let control: ReactElement<DOMAttributes<HTMLElement>, string>;
|
|
365
|
+
if (field?.kind === 'boolean') {
|
|
366
|
+
const on = field.value === true;
|
|
367
|
+
control = (
|
|
368
|
+
<button
|
|
369
|
+
type="button"
|
|
370
|
+
role="switch"
|
|
371
|
+
aria-checked={on}
|
|
372
|
+
aria-label={label}
|
|
373
|
+
className="lk-control-matrix__switch"
|
|
374
|
+
onClick={(e) => {
|
|
375
|
+
if (!unpinned(e)) field.onChange(!on);
|
|
376
|
+
}}
|
|
377
|
+
>
|
|
378
|
+
<span className="lk-control-matrix__track" aria-hidden="true">
|
|
379
|
+
<span className="lk-control-matrix__thumb" />
|
|
380
|
+
</span>
|
|
381
|
+
</button>
|
|
382
|
+
);
|
|
383
|
+
} else if (field?.kind === 'color' && !clearable(leaf)) {
|
|
384
|
+
const stored =
|
|
385
|
+
typeof field.value === 'string' && field.value !== '' ? toHex8(field.value) : '#000000ff';
|
|
386
|
+
control = (
|
|
387
|
+
<input
|
|
388
|
+
type="color"
|
|
389
|
+
aria-label={label}
|
|
390
|
+
className="lk-control-matrix__swatch"
|
|
391
|
+
value={stored.slice(0, 7)}
|
|
392
|
+
onClick={(e) => {
|
|
393
|
+
unpinned(e);
|
|
394
|
+
}}
|
|
395
|
+
onChange={(e) => {
|
|
396
|
+
const rgb = e.currentTarget.value;
|
|
397
|
+
field.onChange(field.alpha ? withAlpha01(rgb, getAlpha01(stored)) : rgb);
|
|
398
|
+
}}
|
|
399
|
+
/>
|
|
400
|
+
);
|
|
401
|
+
} else if (field?.kind === 'color') {
|
|
402
|
+
// A color that may be unset opens the popover, which is the only place it can be cleared.
|
|
403
|
+
const set = typeof field.value === 'string' && field.value !== '';
|
|
404
|
+
control = (
|
|
405
|
+
<button
|
|
406
|
+
type="button"
|
|
407
|
+
aria-label={set ? label : `${label}, unset`}
|
|
408
|
+
aria-haspopup="dialog"
|
|
409
|
+
className="lk-control-matrix__value"
|
|
410
|
+
onClick={(e) => {
|
|
411
|
+
if (!unpinned(e)) onOpen(e.currentTarget);
|
|
412
|
+
}}
|
|
413
|
+
>
|
|
414
|
+
<span
|
|
415
|
+
className={set ? 'lk-control-matrix__chip' : 'lk-control-matrix__chip is-unset'}
|
|
416
|
+
style={set ? ({ '--lk-chip': field.value } as CSSProperties) : undefined}
|
|
417
|
+
aria-hidden="true"
|
|
418
|
+
/>
|
|
419
|
+
</button>
|
|
420
|
+
);
|
|
421
|
+
} else {
|
|
422
|
+
const valueClass =
|
|
423
|
+
field?.kind === 'number' ? 'lk-control-matrix__value is-number' : 'lk-control-matrix__value';
|
|
424
|
+
control = (
|
|
425
|
+
<button
|
|
426
|
+
type="button"
|
|
427
|
+
aria-label={label}
|
|
428
|
+
aria-haspopup="dialog"
|
|
429
|
+
className={valueClass}
|
|
430
|
+
onClick={(e) => {
|
|
431
|
+
if (!unpinned(e)) onOpen(e.currentTarget);
|
|
432
|
+
}}
|
|
433
|
+
>
|
|
434
|
+
{summaryOf(field, shown)}
|
|
435
|
+
</button>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!hint) return control;
|
|
440
|
+
return (
|
|
441
|
+
<TooltipTrigger>
|
|
442
|
+
<Focusable>{control}</Focusable>
|
|
443
|
+
<Tooltip>{hint}</Tooltip>
|
|
444
|
+
</TooltipTrigger>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
@@ -919,6 +919,30 @@ describe('<ControlPanel> option icons', () => {
|
|
|
919
919
|
expect(ok.textContent).toBe('');
|
|
920
920
|
expect(screen.getByRole('radio', { name: 'bad' }).textContent).toBe('bad');
|
|
921
921
|
});
|
|
922
|
+
|
|
923
|
+
it("draws an option's icon beside its label in a select's trigger and list", () => {
|
|
924
|
+
const status = f.enum('ok', [
|
|
925
|
+
{ value: 'ok', label: 'ok', icon: 'statusSuccess' },
|
|
926
|
+
{ value: 'bad', label: 'bad' },
|
|
927
|
+
]);
|
|
928
|
+
render(
|
|
929
|
+
<ControlPanel
|
|
930
|
+
schema={resolveConfigSchema(f.schema({ status }), [])}
|
|
931
|
+
config={{ status: 'ok' }}
|
|
932
|
+
setConfig={() => {}}
|
|
933
|
+
/>,
|
|
934
|
+
);
|
|
935
|
+
const trigger = screen.getByRole('button', { name: /ok/ });
|
|
936
|
+
expect(trigger.querySelector('[aria-hidden="true"] > svg')).not.toBeNull();
|
|
937
|
+
expect(trigger).toHaveTextContent('ok');
|
|
938
|
+
act(() => {
|
|
939
|
+
fireEvent.click(trigger);
|
|
940
|
+
});
|
|
941
|
+
expect(
|
|
942
|
+
screen.getByRole('option', { name: 'ok' }).querySelector('[aria-hidden="true"] > svg'),
|
|
943
|
+
).not.toBeNull();
|
|
944
|
+
expect(screen.getByRole('option', { name: 'bad' }).querySelectorAll('svg')).toHaveLength(1);
|
|
945
|
+
});
|
|
922
946
|
});
|
|
923
947
|
|
|
924
948
|
describe('<ControlPanel> pair', () => {
|
|
@@ -1129,14 +1153,20 @@ describe('<ControlPanel> resettable group', () => {
|
|
|
1129
1153
|
const schema = resolveConfigSchema(
|
|
1130
1154
|
f.schema({
|
|
1131
1155
|
name: f.string('a'),
|
|
1132
|
-
modes: f
|
|
1156
|
+
modes: f
|
|
1157
|
+
.group({ one: f.number(0), two: f.number(0), inner: f.group({ three: f.number(1) }) })
|
|
1158
|
+
.resettable(),
|
|
1133
1159
|
plain: f.group({ four: f.number(0) }),
|
|
1134
1160
|
}),
|
|
1135
1161
|
);
|
|
1136
1162
|
|
|
1137
1163
|
it('puts a reset button on the group heading, disabled while nothing has changed', () => {
|
|
1138
1164
|
render(
|
|
1139
|
-
<ControlPanel
|
|
1165
|
+
<ControlPanel
|
|
1166
|
+
schema={schema}
|
|
1167
|
+
config={{ name: 'a', modes: { one: 0, two: 0, inner: { three: 1 } }, plain: { four: 0 } }}
|
|
1168
|
+
setConfig={vi.fn()}
|
|
1169
|
+
/>,
|
|
1140
1170
|
);
|
|
1141
1171
|
expect(screen.getByRole('button', { name: 'Reset Modes' })).toBeDisabled();
|
|
1142
1172
|
expect(screen.queryByRole('button', { name: 'Reset Plain' })).toBeNull();
|
|
@@ -1145,7 +1175,11 @@ describe('<ControlPanel> resettable group', () => {
|
|
|
1145
1175
|
it('writes the default of every changed value beneath the group and nothing else', () => {
|
|
1146
1176
|
const setConfig = vi.fn();
|
|
1147
1177
|
render(
|
|
1148
|
-
<ControlPanel
|
|
1178
|
+
<ControlPanel
|
|
1179
|
+
schema={schema}
|
|
1180
|
+
config={{ name: 'b', modes: { one: 2, two: 0, inner: { three: 5 } }, plain: { four: 3 } }}
|
|
1181
|
+
setConfig={setConfig}
|
|
1182
|
+
/>,
|
|
1149
1183
|
);
|
|
1150
1184
|
fireEvent.click(screen.getByRole('button', { name: 'Reset Modes' }));
|
|
1151
1185
|
expect(setConfig.mock.calls).toEqual([
|
|
@@ -123,7 +123,9 @@ const NO_AUTO: ReadonlySet<string> = new Set();
|
|
|
123
123
|
function leavesUnder(group: PrefGroup, at: string): [string, unknown][] {
|
|
124
124
|
return Object.entries(group.children).flatMap(([key, child]) => {
|
|
125
125
|
const path = at === '' ? key : `${at}.${key}`;
|
|
126
|
-
return isPrefLeaf(child)
|
|
126
|
+
return isPrefLeaf(child)
|
|
127
|
+
? [[path, child.default] as [string, unknown]]
|
|
128
|
+
: leavesUnder(child, path);
|
|
127
129
|
});
|
|
128
130
|
}
|
|
129
131
|
|
|
@@ -140,7 +142,9 @@ function ResetGroup({
|
|
|
140
142
|
config: Record<string, unknown>;
|
|
141
143
|
setConfig: (path: string, value: unknown) => void;
|
|
142
144
|
}) {
|
|
143
|
-
const changed = leavesUnder(group, path).filter(
|
|
145
|
+
const changed = leavesUnder(group, path).filter(
|
|
146
|
+
([p, d]) => !Object.is(valueAtPath(config, p), d),
|
|
147
|
+
);
|
|
144
148
|
return (
|
|
145
149
|
<Button
|
|
146
150
|
iconOnly
|
|
@@ -267,7 +271,12 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
267
271
|
span
|
|
268
272
|
actions={
|
|
269
273
|
resolved.resettable?.has(path) ? (
|
|
270
|
-
<ResetGroup
|
|
274
|
+
<ResetGroup
|
|
275
|
+
group={found}
|
|
276
|
+
path={path}
|
|
277
|
+
config={config as Record<string, unknown>}
|
|
278
|
+
setConfig={setConfig}
|
|
279
|
+
/>
|
|
271
280
|
) : undefined
|
|
272
281
|
}
|
|
273
282
|
{...fold(path, undefined, rows.grid)}
|
|
@@ -390,7 +399,7 @@ function sectionKey(section: SectionSpec): string {
|
|
|
390
399
|
return section.at === '' ? section.label : `${section.at}.${section.label}`;
|
|
391
400
|
}
|
|
392
401
|
|
|
393
|
-
interface ControlRowProps<TC extends Record<string, unknown>> {
|
|
402
|
+
export interface ControlRowProps<TC extends Record<string, unknown>> {
|
|
394
403
|
path: string;
|
|
395
404
|
leaf: PrefLeaf;
|
|
396
405
|
resolved: ResolvedConfig;
|
|
@@ -423,7 +432,9 @@ function extra<T>(leaf: PrefLeaf, key: string): T | undefined {
|
|
|
423
432
|
return (leaf as unknown as Record<string, T | undefined>)[key];
|
|
424
433
|
}
|
|
425
434
|
|
|
426
|
-
|
|
435
|
+
/** One leaf's row, as the panel draws it. Shared with `ControlMatrix`, whose
|
|
436
|
+
* popover edits one cell with exactly this row. */
|
|
437
|
+
export function ControlRow<TC extends Record<string, unknown>>({
|
|
427
438
|
path,
|
|
428
439
|
leaf,
|
|
429
440
|
resolved,
|
package/src/controls/index.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ControlMatrix,
|
|
3
|
+
type ControlMatrixColumn,
|
|
4
|
+
type ControlMatrixProps,
|
|
5
|
+
type ControlMatrixRow,
|
|
6
|
+
} from './ControlMatrix';
|
|
1
7
|
export { ControlPanel } from './ControlPanel';
|
|
2
8
|
export { type InDialogOptions, inDialog, summarizeValue } from './inDialog';
|
|
3
9
|
export type {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { openPointerSession, type PointerSession, useVisibleRaf } from '@weasel-js/core';
|
|
1
|
+
import { openPointerSession, type PointerSession, useLatest, useVisibleRaf } from '@weasel-js/core';
|
|
2
|
+
import { boxContainsPoint } from '@weasel-js/geom';
|
|
2
3
|
import {
|
|
3
4
|
type PointerEvent as ReactPointerEvent,
|
|
4
5
|
type RefObject,
|
|
@@ -18,12 +19,16 @@ import type {
|
|
|
18
19
|
} from '../instrument/types';
|
|
19
20
|
import { DragGhost } from './DragGhost';
|
|
20
21
|
|
|
22
|
+
/** A palette drag in progress. `screenPos` is in client pixels; `feedback` is
|
|
23
|
+
* the last `onDragOver` answer — `null` off the canvas or without one. */
|
|
21
24
|
export interface DragState {
|
|
22
25
|
item: PaletteItem;
|
|
23
26
|
screenPos: Point;
|
|
24
27
|
feedback: DragFeedback | null;
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
/** Inputs to `useDragDrop`: the capability, the element that counts as the
|
|
31
|
+
* drop target, and the trial state a drop rewrites. */
|
|
27
32
|
export interface UseDragDropArgs<TS, TC> {
|
|
28
33
|
capability: DragDropCapability<TS, TC>;
|
|
29
34
|
canvasContainerRef: RefObject<HTMLElement | null>;
|
|
@@ -37,11 +42,18 @@ export interface UseDragDropArgs<TS, TC> {
|
|
|
37
42
|
emit: (event: string) => void;
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
/** The live drag, and the handler a palette item calls on pointerdown. */
|
|
40
46
|
export interface UseDragDropResult {
|
|
41
47
|
drag: DragState | null;
|
|
42
48
|
startDrag: (item: PaletteItem, e: ReactPointerEvent) => void;
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Run palette-to-canvas drags for a `DragDropCapability`. Releasing over the
|
|
53
|
+
* container converts the pointer to world coordinates, applies `onDrop`'s
|
|
54
|
+
* state and emits `'canvas.itemAdded'`; releasing anywhere else, or a canceled
|
|
55
|
+
* gesture, drops nothing. `onDragOver` is probed at most once per frame.
|
|
56
|
+
*/
|
|
45
57
|
export function useDragDrop<TS, TC>({
|
|
46
58
|
capability,
|
|
47
59
|
canvasContainerRef,
|
|
@@ -53,8 +65,8 @@ export function useDragDrop<TS, TC>({
|
|
|
53
65
|
worldSpec,
|
|
54
66
|
}: UseDragDropArgs<TS, TC>): UseDragDropResult {
|
|
55
67
|
const [drag, setDrag] = useState<DragState | null>(null);
|
|
68
|
+
// Owned by the handlers below, which write it alongside every `setDrag`.
|
|
56
69
|
const dragRef = useRef<DragState | null>(null);
|
|
57
|
-
dragRef.current = drag;
|
|
58
70
|
/** The move the next frame will resolve. Doubles as the throttle's flag:
|
|
59
71
|
* non-null means a frame is already queued. */
|
|
60
72
|
const pendingPos = useRef<Point | null>(null);
|
|
@@ -62,20 +74,14 @@ export function useDragDrop<TS, TC>({
|
|
|
62
74
|
|
|
63
75
|
/** A session keeps the closure it was opened with, so everything the drop
|
|
64
76
|
* reads has to come from here rather than from that closure. */
|
|
65
|
-
const liveRef =
|
|
66
|
-
liveRef.current = { capability, state, config, setState, emit, view, worldSpec };
|
|
77
|
+
const liveRef = useLatest({ capability, state, config, setState, emit, view, worldSpec });
|
|
67
78
|
|
|
68
79
|
const isOverCanvas = useCallback(
|
|
69
80
|
(screenPos: Point): boolean => {
|
|
70
81
|
const el = canvasContainerRef.current;
|
|
71
82
|
if (!el) return false;
|
|
72
83
|
const r = el.getBoundingClientRect();
|
|
73
|
-
return (
|
|
74
|
-
screenPos.x >= r.left &&
|
|
75
|
-
screenPos.x <= r.right &&
|
|
76
|
-
screenPos.y >= r.top &&
|
|
77
|
-
screenPos.y <= r.bottom
|
|
78
|
-
);
|
|
84
|
+
return boxContainsPoint([r.left, r.top, r.right, r.bottom], screenPos.x, screenPos.y);
|
|
79
85
|
},
|
|
80
86
|
[canvasContainerRef],
|
|
81
87
|
);
|
|
@@ -92,7 +98,7 @@ export function useDragDrop<TS, TC>({
|
|
|
92
98
|
frame,
|
|
93
99
|
);
|
|
94
100
|
},
|
|
95
|
-
[canvasContainerRef],
|
|
101
|
+
[canvasContainerRef, liveRef],
|
|
96
102
|
);
|
|
97
103
|
|
|
98
104
|
const frameLoop = useVisibleRaf(() => {
|
|
@@ -106,7 +112,8 @@ export function useDragDrop<TS, TC>({
|
|
|
106
112
|
const world = screenToWorldFromContainer(screenPos);
|
|
107
113
|
if (world) feedback = live.capability.onDragOver(world, active.item, live.state, live.config);
|
|
108
114
|
}
|
|
109
|
-
|
|
115
|
+
dragRef.current = { ...active, screenPos, feedback };
|
|
116
|
+
setDrag(dragRef.current);
|
|
110
117
|
});
|
|
111
118
|
|
|
112
119
|
const clearDrag = useCallback(() => {
|
|
@@ -154,7 +161,7 @@ export function useDragDrop<TS, TC>({
|
|
|
154
161
|
onCancel: clearDrag,
|
|
155
162
|
});
|
|
156
163
|
},
|
|
157
|
-
[clearDrag, frameLoop, isOverCanvas, screenToWorldFromContainer],
|
|
164
|
+
[clearDrag, frameLoop, isOverCanvas, liveRef, screenToWorldFromContainer],
|
|
158
165
|
);
|
|
159
166
|
|
|
160
167
|
useEffect(() => () => sessionRef.current?.cancel(), []);
|
|
@@ -162,10 +169,12 @@ export function useDragDrop<TS, TC>({
|
|
|
162
169
|
return { drag, startDrag };
|
|
163
170
|
}
|
|
164
171
|
|
|
172
|
+
/** Props for `DragOverlay`. */
|
|
165
173
|
export interface DragOverlayProps {
|
|
166
174
|
drag: DragState | null;
|
|
167
175
|
}
|
|
168
176
|
|
|
177
|
+
/** The ghost for `drag`, or nothing when no drag is live. */
|
|
169
178
|
export function DragOverlay({ drag }: DragOverlayProps) {
|
|
170
179
|
if (!drag) return null;
|
|
171
180
|
return <DragGhost item={drag.item} screenPos={drag.screenPos} />;
|