@spear-ai/spectral 1.19.1 → 1.20.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/dist/DirectionalColorWheel/DirectionalColorWheelDisclosure.d.ts +35 -2
- package/dist/DirectionalColorWheel/DirectionalColorWheelDisclosure.d.ts.map +1 -1
- package/dist/DirectionalColorWheel/DirectionalColorWheelDisclosure.js +92 -3
- package/dist/DirectionalColorWheel/DirectionalColorWheelDisclosure.js.map +1 -1
- package/dist/DirectionalColorWheel.d.ts.map +1 -1
- package/dist/DirectionalColorWheel.js +130 -21
- package/dist/DirectionalColorWheel.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/styles/spectral.css +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { CSSProperties, ReactNode, Ref } from "react";
|
|
2
|
+
import { CSSProperties, ReactNode, Ref, RefObject } from "react";
|
|
3
3
|
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/components/DirectionalColorWheel/DirectionalColorWheelDisclosure.d.ts
|
|
6
6
|
type DisclosureAlign = 'start' | 'center' | 'end';
|
|
7
7
|
type DisclosureSide = 'top' | 'right' | 'bottom' | 'left';
|
|
8
|
+
/** Pixel offset of the draggable disclosure from where it is mounted (default `{ x: 0, y: 0 }`). */
|
|
9
|
+
interface DirectionalColorWheelDisclosurePosition {
|
|
10
|
+
/** Horizontal offset in px (positive moves right). */
|
|
11
|
+
x: number;
|
|
12
|
+
/** Vertical offset in px (positive moves down). */
|
|
13
|
+
y: number;
|
|
14
|
+
}
|
|
8
15
|
interface DirectionalColorWheelDisclosureProps {
|
|
9
16
|
/** Accessible name for the collapsed trigger button (`aria-expanded` conveys the open state). */
|
|
10
17
|
accessibleName?: string;
|
|
@@ -22,9 +29,23 @@ interface DirectionalColorWheelDisclosureProps {
|
|
|
22
29
|
/** Min distance (px) from the viewport edge before the panel flips/shifts. */
|
|
23
30
|
collisionPadding?: number;
|
|
24
31
|
dataTestId?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Element the drag is clamped to so the glyph can't be parked off the feed. Pass a ref to the
|
|
34
|
+
* container the disclosure is mounted over (e.g. the plot surface). `undefined`/`null` = no clamp
|
|
35
|
+
* (free drag). Only used when `draggable`.
|
|
36
|
+
*/
|
|
37
|
+
dragBoundsRef?: RefObject<HTMLElement | null> | null;
|
|
25
38
|
/** Uncontrolled initial expanded state. */
|
|
26
39
|
defaultExpanded?: boolean;
|
|
40
|
+
/** Uncontrolled initial position offset (px) from the mount point. Defaults to the corner it is mounted at. */
|
|
41
|
+
defaultPosition?: DirectionalColorWheelDisclosurePosition;
|
|
27
42
|
disabled?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* When `true`, the operator can drag the collapsed glyph to reposition the whole widget (the
|
|
45
|
+
* expanded panel reflows with it in real time). A tap still toggles; only a drag past a small
|
|
46
|
+
* slop repositions. Defaults to `false` (fixed at its mount point).
|
|
47
|
+
*/
|
|
48
|
+
draggable?: boolean;
|
|
28
49
|
/**
|
|
29
50
|
* When `false`, the panel stays open during outside interaction and Esc — it toggles only via the
|
|
30
51
|
* trigger (or a controlled `expanded`). Use this for a feed-mounted wheel so clicking/scrubbing the
|
|
@@ -38,6 +59,13 @@ interface DirectionalColorWheelDisclosureProps {
|
|
|
38
59
|
expanded?: boolean | null;
|
|
39
60
|
/** Called whenever the disclosure expands or collapses (click, Esc, or outside dismiss). */
|
|
40
61
|
onExpandedChange?: (expanded: boolean) => void;
|
|
62
|
+
/** Called as the glyph is dragged with the next position offset (px). Pair with `position` to persist it. */
|
|
63
|
+
onPositionChange?: (position: DirectionalColorWheelDisclosurePosition) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Controlled position offset (px) from the mount point. `undefined`/`null` leaves it uncontrolled,
|
|
66
|
+
* so Horizon can gate the feature with a single nullable value (PAT-028). Requires `draggable` to move.
|
|
67
|
+
*/
|
|
68
|
+
position?: DirectionalColorWheelDisclosurePosition | null;
|
|
41
69
|
ref?: Ref<HTMLButtonElement>;
|
|
42
70
|
/** Which side of the trigger the panel grows toward. Defaults to `top` (icon at a bottom corner). */
|
|
43
71
|
side?: DisclosureSide;
|
|
@@ -63,10 +91,15 @@ declare function DirectionalColorWheelDisclosure({
|
|
|
63
91
|
collisionPadding,
|
|
64
92
|
dataTestId,
|
|
65
93
|
defaultExpanded,
|
|
94
|
+
defaultPosition,
|
|
66
95
|
disabled,
|
|
67
96
|
dismissOnInteractOutside,
|
|
97
|
+
draggable,
|
|
98
|
+
dragBoundsRef,
|
|
68
99
|
expanded,
|
|
69
100
|
onExpandedChange,
|
|
101
|
+
onPositionChange,
|
|
102
|
+
position,
|
|
70
103
|
ref,
|
|
71
104
|
side,
|
|
72
105
|
sideOffset,
|
|
@@ -78,5 +111,5 @@ declare namespace DirectionalColorWheelDisclosure {
|
|
|
78
111
|
var displayName: string;
|
|
79
112
|
}
|
|
80
113
|
//#endregion
|
|
81
|
-
export { DirectionalColorWheelDisclosure, DirectionalColorWheelDisclosureProps };
|
|
114
|
+
export { DirectionalColorWheelDisclosure, DirectionalColorWheelDisclosurePosition, DirectionalColorWheelDisclosureProps };
|
|
82
115
|
//# sourceMappingURL=DirectionalColorWheelDisclosure.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DirectionalColorWheelDisclosure.d.ts","names":[],"sources":["../../src/components/DirectionalColorWheel/DirectionalColorWheelDisclosure.tsx"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"DirectionalColorWheelDisclosure.d.ts","names":[],"sources":["../../src/components/DirectionalColorWheel/DirectionalColorWheelDisclosure.tsx"],"mappings":";;;;;KAOK,eAAA;AAAA,KACA,cAAA;;UAGY,uCAAA;EAJG;EAMlB,CAAA;EANkB;EAQlB,CAAA;AAAA;AAAA,UAOe,oCAAA;;EAEf,cAAA;EAhBiB;EAkBjB,KAAA,GAAQ,eAAA;EAf8C;EAiBtD,QAAA,EAAU,SAAA;EAfV;EAiBA,SAAA;EARe;;;;EAaf,aAAA,GAAgB,SAAA;EAAA;EAEhB,gBAAA;EACA,UAAA;EAUkB;;;;;EAJlB,aAAA,GAAgB,SAAA,CAAU,WAAA;EA0ClB;EAxCR,eAAA;EAwCqB;EAtCrB,eAAA,GAAkB,uCAAA;EAClB,QAAA;EAvBQ;;;;;EA6BR,SAAA;EAlBA;;;;;EAwBA,wBAAA;EAbA;;;;EAkBA,QAAA;EAAA;EAEA,gBAAA,IAAoB,QAAA;EAAA;EAEpB,gBAAA,IAAoB,QAAA,EAAU,uCAAA;EAAA;;;;EAK9B,QAAA,GAAW,uCAAA;EACX,GAAA,GAAM,GAAA,CAAI,iBAAA;EAAA;EAEV,IAAA,GAAO,cAAA;EAAA;EAEP,UAAA;EAMA;;;;;EAAA,KAAA,GAAQ,aAAA;;EAER,gBAAA;;EAEA,KAAA;AAAA;AAAA;EAoBA,cAAA;EACA,KAAA;EACA,QAAA;EACA,SAAA;EACA,aAAA;EACA,gBAAA;EACA,UAAA;EACA,eAAA;EACA,eAAA;EACA,QAAA;EACA,wBAAA;EACA,SAAA;EACA,aAAA;EACA,QAAA;EACA,gBAAA;EACA,gBAAA;EACA,QAAA;EACA,GAAA;EACA,IAAA;EACA,UAAA;EACA,KAAA;EACA,gBAAA;EACA;AAAA,GACC,oCAAA,GAAoC,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA"}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { cn } from "../utils/twUtils.js";
|
|
3
|
+
import { useUncontrolledState } from "../hooks/useUncontrolledState.js";
|
|
3
4
|
import { Card } from "../DataCard/Card.js";
|
|
4
5
|
import { Popover, PopoverContent, PopoverTrigger } from "../Popover.js";
|
|
5
6
|
import { DirectionalColorWheelGlyph } from "./DirectionalColorWheelGlyph.js";
|
|
6
|
-
import { useRef } from "react";
|
|
7
|
+
import { useCallback, useRef, useState } from "react";
|
|
7
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
|
|
9
10
|
//#region src/components/DirectionalColorWheel/DirectionalColorWheelDisclosure.tsx
|
|
11
|
+
const DRAG_SLOP_PX = 4;
|
|
12
|
+
const ORIGIN = {
|
|
13
|
+
x: 0,
|
|
14
|
+
y: 0
|
|
15
|
+
};
|
|
10
16
|
/**
|
|
11
17
|
* Collapse/expand affordance for the {@link DirectionalColorWheel}, built on the Spectral
|
|
12
18
|
* {@link Popover} primitive so it inherits corner anchoring, click-outside/Esc dismissal, focus
|
|
@@ -16,10 +22,83 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
16
22
|
* panel, growing from the icon with a `transform-origin`-anchored zoom+fade that is automatically
|
|
17
23
|
* skipped under `prefers-reduced-motion`. `className` extends the Card panel's classes and `style`
|
|
18
24
|
* applies inline overrides to it (background, border, shadow, …).
|
|
25
|
+
*
|
|
26
|
+
* Pass `draggable` to let the operator drag the glyph to reposition the whole widget on a feed; the
|
|
27
|
+
* expanded panel reflows with it in real time, and `dragBoundsRef` clamps it inside its container so
|
|
28
|
+
* it can't be parked off the feed. Position is controlled+uncontrolled via
|
|
29
|
+
* `position` / `defaultPosition` / `onPositionChange` (a nullable px offset from the mount point,
|
|
30
|
+
* PAT-028) so a consumer can persist where the operator parked it.
|
|
19
31
|
*/
|
|
20
|
-
const DirectionalColorWheelDisclosure = ({ accessibleName = "Directional color legend", align = "start", children, className, collapsedIcon, collisionPadding = 8, dataTestId = "spectral-directional-color-wheel-disclosure", defaultExpanded, disabled = false, dismissOnInteractOutside = true, expanded, onExpandedChange, ref, side = "top", sideOffset = 8, style, triggerClassName, width = "fit-content" }) => {
|
|
32
|
+
const DirectionalColorWheelDisclosure = ({ accessibleName = "Directional color legend", align = "start", children, className, collapsedIcon, collisionPadding = 8, dataTestId = "spectral-directional-color-wheel-disclosure", defaultExpanded, defaultPosition, disabled = false, dismissOnInteractOutside = true, draggable = false, dragBoundsRef, expanded, onExpandedChange, onPositionChange, position, ref, side = "top", sideOffset = 8, style, triggerClassName, width = "fit-content" }) => {
|
|
21
33
|
const panelRef = useRef(null);
|
|
22
34
|
const panelWidth = typeof width === "number" ? `${width}px` : width;
|
|
35
|
+
const [positionValue, setPositionValue] = useUncontrolledState({
|
|
36
|
+
defaultValue: defaultPosition ?? ORIGIN,
|
|
37
|
+
onChange: onPositionChange,
|
|
38
|
+
value: position ?? void 0
|
|
39
|
+
});
|
|
40
|
+
const [dragging, setDragging] = useState(false);
|
|
41
|
+
const dragRef = useRef(null);
|
|
42
|
+
const suppressClickRef = useRef(false);
|
|
43
|
+
const handleTriggerPointerDown = useCallback((event) => {
|
|
44
|
+
suppressClickRef.current = false;
|
|
45
|
+
if (!draggable || disabled) return;
|
|
46
|
+
try {
|
|
47
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
48
|
+
} catch {}
|
|
49
|
+
const triggerRect = event.currentTarget.getBoundingClientRect();
|
|
50
|
+
dragRef.current = {
|
|
51
|
+
baseX: positionValue.x,
|
|
52
|
+
baseY: positionValue.y,
|
|
53
|
+
bounds: dragBoundsRef?.current?.getBoundingClientRect() ?? null,
|
|
54
|
+
height: triggerRect.height,
|
|
55
|
+
homeLeft: triggerRect.left - positionValue.x,
|
|
56
|
+
homeTop: triggerRect.top - positionValue.y,
|
|
57
|
+
moved: false,
|
|
58
|
+
pointerId: event.pointerId,
|
|
59
|
+
startX: event.clientX,
|
|
60
|
+
startY: event.clientY,
|
|
61
|
+
width: triggerRect.width
|
|
62
|
+
};
|
|
63
|
+
}, [
|
|
64
|
+
disabled,
|
|
65
|
+
draggable,
|
|
66
|
+
dragBoundsRef,
|
|
67
|
+
positionValue.x,
|
|
68
|
+
positionValue.y
|
|
69
|
+
]);
|
|
70
|
+
const handleTriggerPointerMove = useCallback((event) => {
|
|
71
|
+
const drag = dragRef.current;
|
|
72
|
+
if (drag === null || drag.pointerId !== event.pointerId) return;
|
|
73
|
+
const deltaX = event.clientX - drag.startX;
|
|
74
|
+
const deltaY = event.clientY - drag.startY;
|
|
75
|
+
if (!drag.moved) {
|
|
76
|
+
if (Math.hypot(deltaX, deltaY) <= DRAG_SLOP_PX) return;
|
|
77
|
+
drag.moved = true;
|
|
78
|
+
suppressClickRef.current = true;
|
|
79
|
+
setDragging(true);
|
|
80
|
+
}
|
|
81
|
+
let nextX = drag.baseX + deltaX;
|
|
82
|
+
let nextY = drag.baseY + deltaY;
|
|
83
|
+
if (drag.bounds !== null) {
|
|
84
|
+
nextX = Math.min(Math.max(nextX, drag.bounds.left - drag.homeLeft), drag.bounds.right - drag.homeLeft - drag.width);
|
|
85
|
+
nextY = Math.min(Math.max(nextY, drag.bounds.top - drag.homeTop), drag.bounds.bottom - drag.homeTop - drag.height);
|
|
86
|
+
}
|
|
87
|
+
setPositionValue({
|
|
88
|
+
x: nextX,
|
|
89
|
+
y: nextY
|
|
90
|
+
});
|
|
91
|
+
}, [setPositionValue]);
|
|
92
|
+
const endDrag = useCallback(() => {
|
|
93
|
+
dragRef.current = null;
|
|
94
|
+
setDragging(false);
|
|
95
|
+
}, []);
|
|
96
|
+
const handleTriggerClick = useCallback((event) => {
|
|
97
|
+
if (suppressClickRef.current) {
|
|
98
|
+
suppressClickRef.current = false;
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
}
|
|
101
|
+
}, []);
|
|
23
102
|
return /* @__PURE__ */ jsxs(Popover, {
|
|
24
103
|
defaultOpen: defaultExpanded,
|
|
25
104
|
onOpenChange: onExpandedChange,
|
|
@@ -28,11 +107,20 @@ const DirectionalColorWheelDisclosure = ({ accessibleName = "Directional color l
|
|
|
28
107
|
asChild: true,
|
|
29
108
|
children: /* @__PURE__ */ jsx("button", {
|
|
30
109
|
"aria-label": accessibleName,
|
|
31
|
-
className: cn("inline-flex items-center justify-center rounded-full text-text-primary outline-none hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent disabled:pointer-events-none disabled:opacity-50 motion-safe:transition-opacity motion-safe:duration-150", triggerClassName),
|
|
110
|
+
className: cn("inline-flex items-center justify-center rounded-full text-text-primary outline-none hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent disabled:pointer-events-none disabled:opacity-50 motion-safe:transition-opacity motion-safe:duration-150", draggable && "touch-none", draggable && (dragging ? "cursor-grabbing" : "cursor-grab"), triggerClassName),
|
|
32
111
|
"data-slot": "directional-color-wheel-disclosure-trigger",
|
|
33
112
|
"data-testid": `${dataTestId}-trigger`,
|
|
34
113
|
disabled,
|
|
114
|
+
onClick: handleTriggerClick,
|
|
115
|
+
onLostPointerCapture: endDrag,
|
|
116
|
+
onPointerDown: handleTriggerPointerDown,
|
|
117
|
+
onPointerMove: handleTriggerPointerMove,
|
|
118
|
+
onPointerUp: endDrag,
|
|
35
119
|
ref,
|
|
120
|
+
style: draggable ? {
|
|
121
|
+
transform: `translate(${positionValue.x}px, ${positionValue.y}px)`,
|
|
122
|
+
willChange: dragging ? "transform" : void 0
|
|
123
|
+
} : void 0,
|
|
36
124
|
type: "button",
|
|
37
125
|
children: collapsedIcon ?? /* @__PURE__ */ jsx(DirectionalColorWheelGlyph, {})
|
|
38
126
|
})
|
|
@@ -50,6 +138,7 @@ const DirectionalColorWheelDisclosure = ({ accessibleName = "Directional color l
|
|
|
50
138
|
},
|
|
51
139
|
side,
|
|
52
140
|
sideOffset,
|
|
141
|
+
updatePositionStrategy: dragging ? "always" : "optimized",
|
|
53
142
|
width: "fit-content",
|
|
54
143
|
children: /* @__PURE__ */ jsx(Card, {
|
|
55
144
|
className: cn("outline-none", className),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DirectionalColorWheelDisclosure.js","names":[],"sources":["../../src/components/DirectionalColorWheel/DirectionalColorWheelDisclosure.tsx"],"sourcesContent":["import { Card } from '@components/DataCard/Card'\nimport { Popover, PopoverContent, PopoverTrigger } from '@components/Popover/Popover'\nimport { cn } from '@utils/twUtils'\nimport { useRef, type CSSProperties, type ReactNode, type Ref } from 'react'\nimport { DirectionalColorWheelGlyph } from './DirectionalColorWheelGlyph'\n\ntype DisclosureAlign = 'start' | 'center' | 'end'\ntype DisclosureSide = 'top' | 'right' | 'bottom' | 'left'\n\nexport interface DirectionalColorWheelDisclosureProps {\n /** Accessible name for the collapsed trigger button (`aria-expanded` conveys the open state). */\n accessibleName?: string\n /** How the panel aligns to the trigger. Defaults to `start` for a corner-anchored feed widget. */\n align?: DisclosureAlign\n /** The expandable content: the bare wheel, or the wheel framed by the GRAMS controls. */\n children: ReactNode\n /** Layout-only class extension for the expanded panel. */\n className?: string\n /**\n * Override the collapsed affordance. Defaults to a {@link DirectionalColorWheelGlyph} mini\n * compass; pass a palette-matched glyph (or any node) to mirror the expanded wheel's colours.\n */\n collapsedIcon?: ReactNode\n /** Min distance (px) from the viewport edge before the panel flips/shifts. */\n collisionPadding?: number\n dataTestId?: string\n /** Uncontrolled initial expanded state. */\n defaultExpanded?: boolean\n disabled?: boolean\n /**\n * When `false`, the panel stays open during outside interaction and Esc — it toggles only via the\n * trigger (or a controlled `expanded`). Use this for a feed-mounted wheel so clicking/scrubbing the\n * feed underneath doesn't dismiss the legend. Defaults to `true` (standard popover dismissal).\n */\n dismissOnInteractOutside?: boolean\n /**\n * Controlled expanded state. `undefined`/`null` leaves the disclosure uncontrolled, so Horizon\n * can gate the feature with a single nullable value (PAT-028).\n */\n expanded?: boolean | null\n /** Called whenever the disclosure expands or collapses (click, Esc, or outside dismiss). */\n onExpandedChange?: (expanded: boolean) => void\n ref?: Ref<HTMLButtonElement>\n /** Which side of the trigger the panel grows toward. Defaults to `top` (icon at a bottom corner). */\n side?: DisclosureSide\n /** Gap (px) between the trigger and the panel. */\n sideOffset?: number\n /**\n * Inline styles merged onto the expanded panel (the {@link Card}). Use this for visual overrides\n * — background, border, shadow — that the `card-effects` base class doesn't expose to `className`\n * utilities. The default panel width is applied first, so a `width` set here wins over the `width` prop.\n */\n style?: CSSProperties\n /** Layout-only class extension for the collapsed trigger button. */\n triggerClassName?: string\n /** Panel width: a pixel number or a CSS width string. Defaults to `'fit-content'` (hugs content). */\n width?: number | string\n}\n\n/**\n * Collapse/expand affordance for the {@link DirectionalColorWheel}, built on the Spectral\n * {@link Popover} primitive so it inherits corner anchoring, click-outside/Esc dismissal, focus\n * management, and `aria-expanded`/`aria-controls` for free. Collapsed it is a small compass glyph\n * (a real `<button>`); expanded it reveals whatever `children` it wraps — the bare wheel, or the\n * wheel plus the GRAMS Color Angle / Threshold / Sectors controls — inside a Spectral {@link Card}\n * panel, growing from the icon with a `transform-origin`-anchored zoom+fade that is automatically\n * skipped under `prefers-reduced-motion`. `className` extends the Card panel's classes and `style`\n * applies inline overrides to it (background, border, shadow, …).\n */\nexport const DirectionalColorWheelDisclosure = ({\n accessibleName = 'Directional color legend',\n align = 'start',\n children,\n className,\n collapsedIcon,\n collisionPadding = 8,\n dataTestId = 'spectral-directional-color-wheel-disclosure',\n defaultExpanded,\n disabled = false,\n dismissOnInteractOutside = true,\n expanded,\n onExpandedChange,\n ref,\n side = 'top',\n sideOffset = 8,\n style,\n triggerClassName,\n width = 'fit-content',\n}: DirectionalColorWheelDisclosureProps) => {\n const panelRef = useRef<HTMLDivElement>(null)\n // `width` sizes the Card panel; the popover hugs it (fit-content) so there is no empty gap.\n const panelWidth = typeof width === 'number' ? `${width}px` : width\n\n return (\n <Popover\n defaultOpen={defaultExpanded}\n onOpenChange={onExpandedChange}\n open={expanded ?? undefined}\n >\n <PopoverTrigger asChild>\n <button\n aria-label={accessibleName}\n className={cn(\n 'inline-flex items-center justify-center rounded-full text-text-primary outline-none hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent disabled:pointer-events-none disabled:opacity-50 motion-safe:transition-opacity motion-safe:duration-150',\n triggerClassName,\n )}\n data-slot='directional-color-wheel-disclosure-trigger'\n data-testid={`${dataTestId}-trigger`}\n disabled={disabled}\n ref={ref}\n type='button'\n >\n {collapsedIcon ?? <DirectionalColorWheelGlyph />}\n </button>\n </PopoverTrigger>\n <PopoverContent\n align={align}\n // Chrome-less positioning/animation layer; the visible panel is the Spectral Card below.\n className='p-0 flex items-center justify-center overflow-visible border-none bg-transparent shadow-none'\n collisionPadding={collisionPadding}\n data-slot='directional-color-wheel-disclosure-content'\n data-testid={`${dataTestId}-content`}\n // When dismissal is off, swallow Radix's outside-pointer/Esc dismiss so the panel stays open\n // while the user interacts with whatever sits under it (e.g. the feed); the trigger still toggles.\n onEscapeKeyDown={dismissOnInteractOutside ? undefined : (event) => event.preventDefault()}\n onInteractOutside={dismissOnInteractOutside ? undefined : (event) => event.preventDefault()}\n onOpenAutoFocus={(event) => {\n // Focus the panel, not the first sector wedge (which would paint an accent ring on a sector).\n event.preventDefault()\n panelRef.current?.focus()\n }}\n side={side}\n sideOffset={sideOffset}\n width='fit-content'\n >\n <Card\n className={cn('outline-none', className)}\n data-slot='directional-color-wheel-disclosure-panel'\n ref={panelRef}\n // The default width is applied first so a caller-supplied `style.width` (or any other\n // visual override the `card-effects` base class locks down) takes precedence.\n style={{ width: panelWidth, ...style }}\n tabIndex={-1}\n >\n {children}\n </Card>\n </PopoverContent>\n </Popover>\n )\n}\n\nDirectionalColorWheelDisclosure.displayName = 'DirectionalColorWheelDisclosure'\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAqEA,MAAa,mCAAmC,EAC9C,iBAAiB,4BACjB,QAAQ,SACR,UACA,WACA,eACA,mBAAmB,GACnB,aAAa,+CACb,iBACA,WAAW,OACX,2BAA2B,MAC3B,UACA,kBACA,KACA,OAAO,OACP,aAAa,GACb,OACA,kBACA,QAAQ,oBACkC;CAC1C,MAAM,WAAW,OAAuB,KAAK;CAE7C,MAAM,aAAa,OAAO,UAAU,WAAW,GAAG,MAAM,MAAM;AAE9D,QACE,qBAAC,SAAD;EACE,aAAa;EACb,cAAc;EACd,MAAM,YAAY;YAHpB,CAKE,oBAAC,gBAAD;GAAgB;aACd,oBAAC,UAAD;IACE,cAAY;IACZ,WAAW,GACT,qSACA,iBACD;IACD,aAAU;IACV,eAAa,GAAG,WAAW;IACjB;IACL;IACL,MAAK;cAEJ,iBAAiB,oBAAC,4BAAD,EAA8B;IACzC;GACM,GACjB,oBAAC,gBAAD;GACS;GAEP,WAAU;GACQ;GAClB,aAAU;GACV,eAAa,GAAG,WAAW;GAG3B,iBAAiB,2BAA2B,UAAa,UAAU,MAAM,gBAAgB;GACzF,mBAAmB,2BAA2B,UAAa,UAAU,MAAM,gBAAgB;GAC3F,kBAAkB,UAAU;AAE1B,UAAM,gBAAgB;AACtB,aAAS,SAAS,OAAO;;GAErB;GACM;GACZ,OAAM;aAEN,oBAAC,MAAD;IACE,WAAW,GAAG,gBAAgB,UAAU;IACxC,aAAU;IACV,KAAK;IAGL,OAAO;KAAE,OAAO;KAAY,GAAG;KAAO;IACtC,UAAU;IAET;IACI;GACQ,EACT;;;AAId,gCAAgC,cAAc"}
|
|
1
|
+
{"version":3,"file":"DirectionalColorWheelDisclosure.js","names":[],"sources":["../../src/components/DirectionalColorWheel/DirectionalColorWheelDisclosure.tsx"],"sourcesContent":["import { Card } from '@components/DataCard/Card'\nimport { Popover, PopoverContent, PopoverTrigger } from '@components/Popover/Popover'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useRef, useState, type CSSProperties, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode, type Ref, type RefObject } from 'react'\nimport { DirectionalColorWheelGlyph } from './DirectionalColorWheelGlyph'\n\ntype DisclosureAlign = 'start' | 'center' | 'end'\ntype DisclosureSide = 'top' | 'right' | 'bottom' | 'left'\n\n/** Pixel offset of the draggable disclosure from where it is mounted (default `{ x: 0, y: 0 }`). */\nexport interface DirectionalColorWheelDisclosurePosition {\n /** Horizontal offset in px (positive moves right). */\n x: number\n /** Vertical offset in px (positive moves down). */\n y: number\n}\n\n// Drag distance (px) below which a press on the glyph is a tap (toggle), not a reposition drag.\nconst DRAG_SLOP_PX = 4\nconst ORIGIN: DirectionalColorWheelDisclosurePosition = { x: 0, y: 0 }\n\nexport interface DirectionalColorWheelDisclosureProps {\n /** Accessible name for the collapsed trigger button (`aria-expanded` conveys the open state). */\n accessibleName?: string\n /** How the panel aligns to the trigger. Defaults to `start` for a corner-anchored feed widget. */\n align?: DisclosureAlign\n /** The expandable content: the bare wheel, or the wheel framed by the GRAMS controls. */\n children: ReactNode\n /** Layout-only class extension for the expanded panel. */\n className?: string\n /**\n * Override the collapsed affordance. Defaults to a {@link DirectionalColorWheelGlyph} mini\n * compass; pass a palette-matched glyph (or any node) to mirror the expanded wheel's colours.\n */\n collapsedIcon?: ReactNode\n /** Min distance (px) from the viewport edge before the panel flips/shifts. */\n collisionPadding?: number\n dataTestId?: string\n /**\n * Element the drag is clamped to so the glyph can't be parked off the feed. Pass a ref to the\n * container the disclosure is mounted over (e.g. the plot surface). `undefined`/`null` = no clamp\n * (free drag). Only used when `draggable`.\n */\n dragBoundsRef?: RefObject<HTMLElement | null> | null\n /** Uncontrolled initial expanded state. */\n defaultExpanded?: boolean\n /** Uncontrolled initial position offset (px) from the mount point. Defaults to the corner it is mounted at. */\n defaultPosition?: DirectionalColorWheelDisclosurePosition\n disabled?: boolean\n /**\n * When `true`, the operator can drag the collapsed glyph to reposition the whole widget (the\n * expanded panel reflows with it in real time). A tap still toggles; only a drag past a small\n * slop repositions. Defaults to `false` (fixed at its mount point).\n */\n draggable?: boolean\n /**\n * When `false`, the panel stays open during outside interaction and Esc — it toggles only via the\n * trigger (or a controlled `expanded`). Use this for a feed-mounted wheel so clicking/scrubbing the\n * feed underneath doesn't dismiss the legend. Defaults to `true` (standard popover dismissal).\n */\n dismissOnInteractOutside?: boolean\n /**\n * Controlled expanded state. `undefined`/`null` leaves the disclosure uncontrolled, so Horizon\n * can gate the feature with a single nullable value (PAT-028).\n */\n expanded?: boolean | null\n /** Called whenever the disclosure expands or collapses (click, Esc, or outside dismiss). */\n onExpandedChange?: (expanded: boolean) => void\n /** Called as the glyph is dragged with the next position offset (px). Pair with `position` to persist it. */\n onPositionChange?: (position: DirectionalColorWheelDisclosurePosition) => void\n /**\n * Controlled position offset (px) from the mount point. `undefined`/`null` leaves it uncontrolled,\n * so Horizon can gate the feature with a single nullable value (PAT-028). Requires `draggable` to move.\n */\n position?: DirectionalColorWheelDisclosurePosition | null\n ref?: Ref<HTMLButtonElement>\n /** Which side of the trigger the panel grows toward. Defaults to `top` (icon at a bottom corner). */\n side?: DisclosureSide\n /** Gap (px) between the trigger and the panel. */\n sideOffset?: number\n /**\n * Inline styles merged onto the expanded panel (the {@link Card}). Use this for visual overrides\n * — background, border, shadow — that the `card-effects` base class doesn't expose to `className`\n * utilities. The default panel width is applied first, so a `width` set here wins over the `width` prop.\n */\n style?: CSSProperties\n /** Layout-only class extension for the collapsed trigger button. */\n triggerClassName?: string\n /** Panel width: a pixel number or a CSS width string. Defaults to `'fit-content'` (hugs content). */\n width?: number | string\n}\n\n/**\n * Collapse/expand affordance for the {@link DirectionalColorWheel}, built on the Spectral\n * {@link Popover} primitive so it inherits corner anchoring, click-outside/Esc dismissal, focus\n * management, and `aria-expanded`/`aria-controls` for free. Collapsed it is a small compass glyph\n * (a real `<button>`); expanded it reveals whatever `children` it wraps — the bare wheel, or the\n * wheel plus the GRAMS Color Angle / Threshold / Sectors controls — inside a Spectral {@link Card}\n * panel, growing from the icon with a `transform-origin`-anchored zoom+fade that is automatically\n * skipped under `prefers-reduced-motion`. `className` extends the Card panel's classes and `style`\n * applies inline overrides to it (background, border, shadow, …).\n *\n * Pass `draggable` to let the operator drag the glyph to reposition the whole widget on a feed; the\n * expanded panel reflows with it in real time, and `dragBoundsRef` clamps it inside its container so\n * it can't be parked off the feed. Position is controlled+uncontrolled via\n * `position` / `defaultPosition` / `onPositionChange` (a nullable px offset from the mount point,\n * PAT-028) so a consumer can persist where the operator parked it.\n */\nexport const DirectionalColorWheelDisclosure = ({\n accessibleName = 'Directional color legend',\n align = 'start',\n children,\n className,\n collapsedIcon,\n collisionPadding = 8,\n dataTestId = 'spectral-directional-color-wheel-disclosure',\n defaultExpanded,\n defaultPosition,\n disabled = false,\n dismissOnInteractOutside = true,\n draggable = false,\n dragBoundsRef,\n expanded,\n onExpandedChange,\n onPositionChange,\n position,\n ref,\n side = 'top',\n sideOffset = 8,\n style,\n triggerClassName,\n width = 'fit-content',\n}: DirectionalColorWheelDisclosureProps) => {\n const panelRef = useRef<HTMLDivElement>(null)\n // `width` sizes the Card panel; the popover hugs it (fit-content) so there is no empty gap.\n const panelWidth = typeof width === 'number' ? `${width}px` : width\n\n const [positionValue, setPositionValue] = useUncontrolledState<DirectionalColorWheelDisclosurePosition>({ defaultValue: defaultPosition ?? ORIGIN, onChange: onPositionChange, value: position ?? undefined })\n const [dragging, setDragging] = useState(false)\n // Live reposition drag (a ref so rapid moves don't hinge on re-render timing). `bounds` is the\n // clamp rect (the dragBoundsRef element); `homeLeft`/`homeTop` are the trigger's viewport position\n // at translate 0, so the allowed offset range keeps the glyph fully inside `bounds`.\n const dragRef = useRef<{ baseX: number; baseY: number; bounds: DOMRect | null; height: number; homeLeft: number; homeTop: number; moved: boolean; pointerId: number; startX: number; startY: number; width: number } | null>(null)\n // Set when a drag passes the slop so the trailing click is swallowed (preventDefault → Radix's\n // composed trigger toggle bails) instead of also expanding/collapsing the panel.\n const suppressClickRef = useRef(false)\n\n const handleTriggerPointerDown = useCallback(\n (event: ReactPointerEvent<HTMLButtonElement>) => {\n suppressClickRef.current = false\n if (!draggable || disabled) return\n try {\n event.currentTarget.setPointerCapture(event.pointerId)\n } catch {\n // setPointerCapture can throw without an active pointer (e.g. synthetic test events).\n }\n const triggerRect = event.currentTarget.getBoundingClientRect()\n dragRef.current = {\n baseX: positionValue.x,\n baseY: positionValue.y,\n bounds: dragBoundsRef?.current?.getBoundingClientRect() ?? null,\n height: triggerRect.height,\n // Subtract the active translate to recover the trigger's home (translate-0) position.\n homeLeft: triggerRect.left - positionValue.x,\n homeTop: triggerRect.top - positionValue.y,\n moved: false,\n pointerId: event.pointerId,\n startX: event.clientX,\n startY: event.clientY,\n width: triggerRect.width,\n }\n },\n [disabled, draggable, dragBoundsRef, positionValue.x, positionValue.y],\n )\n\n const handleTriggerPointerMove = useCallback(\n (event: ReactPointerEvent<HTMLButtonElement>) => {\n const drag = dragRef.current\n if (drag === null || drag.pointerId !== event.pointerId) return\n const deltaX = event.clientX - drag.startX\n const deltaY = event.clientY - drag.startY\n if (!drag.moved) {\n if (Math.hypot(deltaX, deltaY) <= DRAG_SLOP_PX) return\n drag.moved = true\n suppressClickRef.current = true\n // Promote the trigger to its own compositor layer and poll the popover position every frame\n // (`updatePositionStrategy='always'`) so the panel reflows with the glyph rather than snapping.\n setDragging(true)\n }\n let nextX = drag.baseX + deltaX\n let nextY = drag.baseY + deltaY\n // Clamp the offset so the glyph stays fully inside the bounds element (can't be lost off the feed).\n if (drag.bounds !== null) {\n nextX = Math.min(Math.max(nextX, drag.bounds.left - drag.homeLeft), drag.bounds.right - drag.homeLeft - drag.width)\n nextY = Math.min(Math.max(nextY, drag.bounds.top - drag.homeTop), drag.bounds.bottom - drag.homeTop - drag.height)\n }\n setPositionValue({ x: nextX, y: nextY })\n },\n [setPositionValue],\n )\n\n const endDrag = useCallback(() => {\n dragRef.current = null\n setDragging(false)\n }, [])\n\n const handleTriggerClick = useCallback((event: ReactMouseEvent<HTMLButtonElement>) => {\n if (suppressClickRef.current) {\n suppressClickRef.current = false\n event.preventDefault()\n }\n }, [])\n\n return (\n <Popover\n defaultOpen={defaultExpanded}\n onOpenChange={onExpandedChange}\n open={expanded ?? undefined}\n >\n <PopoverTrigger asChild>\n <button\n aria-label={accessibleName}\n className={cn(\n 'inline-flex items-center justify-center rounded-full text-text-primary outline-none hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent disabled:pointer-events-none disabled:opacity-50 motion-safe:transition-opacity motion-safe:duration-150',\n draggable && 'touch-none',\n draggable && (dragging ? 'cursor-grabbing' : 'cursor-grab'),\n triggerClassName,\n )}\n data-slot='directional-color-wheel-disclosure-trigger'\n data-testid={`${dataTestId}-trigger`}\n disabled={disabled}\n onClick={handleTriggerClick}\n onLostPointerCapture={endDrag}\n onPointerDown={handleTriggerPointerDown}\n onPointerMove={handleTriggerPointerMove}\n onPointerUp={endDrag}\n ref={ref}\n // A pure translate (no left/top) keeps the reposition on the compositor; `will-change`\n // promotes it for the duration of the drag.\n style={draggable ? { transform: `translate(${positionValue.x}px, ${positionValue.y}px)`, willChange: dragging ? 'transform' : undefined } : undefined}\n type='button'\n >\n {collapsedIcon ?? <DirectionalColorWheelGlyph />}\n </button>\n </PopoverTrigger>\n <PopoverContent\n align={align}\n // Chrome-less positioning/animation layer; the visible panel is the Spectral Card below.\n className='p-0 flex items-center justify-center overflow-visible border-none bg-transparent shadow-none'\n collisionPadding={collisionPadding}\n data-slot='directional-color-wheel-disclosure-content'\n data-testid={`${dataTestId}-content`}\n // When dismissal is off, swallow Radix's outside-pointer/Esc dismiss so the panel stays open\n // while the user interacts with whatever sits under it (e.g. the feed); the trigger still toggles.\n onEscapeKeyDown={dismissOnInteractOutside ? undefined : (event) => event.preventDefault()}\n onInteractOutside={dismissOnInteractOutside ? undefined : (event) => event.preventDefault()}\n onOpenAutoFocus={(event) => {\n // Focus the panel, not the first sector wedge (which would paint an accent ring on a sector).\n event.preventDefault()\n panelRef.current?.focus()\n }}\n side={side}\n sideOffset={sideOffset}\n // While dragging, poll the anchor position every animation frame so the panel tracks the\n // glyph in real time; back to the cheaper 'optimized' strategy at rest.\n updatePositionStrategy={dragging ? 'always' : 'optimized'}\n width='fit-content'\n >\n <Card\n className={cn('outline-none', className)}\n data-slot='directional-color-wheel-disclosure-panel'\n ref={panelRef}\n // The default width is applied first so a caller-supplied `style.width` (or any other\n // visual override the `card-effects` base class locks down) takes precedence.\n style={{ width: panelWidth, ...style }}\n tabIndex={-1}\n >\n {children}\n </Card>\n </PopoverContent>\n </Popover>\n )\n}\n\nDirectionalColorWheelDisclosure.displayName = 'DirectionalColorWheelDisclosure'\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DirectionalColorWheel.d.ts","names":[],"sources":["../src/components/DirectionalColorWheel/DirectionalColorWheel.tsx"],"mappings":";;;;;;UAKiB,0BAAA;;EAEf,cAAA;EAFe;;;;;;;EAUf,OAAA;EAuCoC;EArCpC,SAAA;EAFA;;;;;;;EAUA,UAAA;EAWA;;;;;EALA,eAAA,KAAoB,cAAA;EAcE;EAZtB,UAAA,GAAa,oBAAA;EACb,UAAA;EAeA;EAbA,iBAAA;EAauC;EAXvC,sBAAA;EAaqB;EAXrB,gBAAA;EACA,QAAA;EAWU;EATV,eAAA;EAWc;EATd,kBAAA,IAAsB,UAAA;EAiBtB;EAfA,uBAAA,IAA2B,eAAA;EAiBd;EAfb,cAAA,IAAkB,WAAA,UAAqB,YAAA;;EAEvC,iBAAA,IAAqB,SAAA;EACrB,GAAA,GAAM,GAAA,CAAI,cAAA;
|
|
1
|
+
{"version":3,"file":"DirectionalColorWheel.d.ts","names":[],"sources":["../src/components/DirectionalColorWheel/DirectionalColorWheel.tsx"],"mappings":";;;;;;UAKiB,0BAAA;;EAEf,cAAA;EAFe;;;;;;;EAUf,OAAA;EAuCoC;EArCpC,SAAA;EAFA;;;;;;;EAUA,UAAA;EAWA;;;;;EALA,eAAA,KAAoB,cAAA;EAcE;EAZtB,UAAA,GAAa,oBAAA;EACb,UAAA;EAeA;EAbA,iBAAA;EAauC;EAXvC,sBAAA;EAaqB;EAXrB,gBAAA;EACA,QAAA;EAWU;EATV,eAAA;EAWc;EATd,kBAAA,IAAsB,UAAA;EAiBtB;EAfA,uBAAA,IAA2B,eAAA;EAiBd;EAfb,cAAA,IAAkB,WAAA,UAAqB,YAAA;;EAEvC,iBAAA,IAAqB,SAAA;EACrB,GAAA,GAAM,GAAA,CAAI,cAAA;EAkGV;EAhGA,WAAA,GAAc,sBAAA;EAkGd;EAhGA,IAAA;EAkGA;;;;;EA5FA,SAAA;EAkGA;EAhGA,aAAA;AAAA;AAAA;EAsFA,cAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EACA,eAAA;EACA,UAAA;EACA,UAAA;EACA,iBAAA;EACA,sBAAA;EACA,gBAAA;EACA,QAAA;EACA,eAAA;EACA,kBAAA;EACA,uBAAA;EACA,cAAA;EACA,iBAAA;EACA,GAAA;EACA,WAAA;EACA,IAAA;EACA,SAAA;EACA;AAAA,GACC,0BAAA,GAA0B,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA"}
|
|
@@ -60,6 +60,7 @@ const pointerBearing = (clientX, clientY, rect) => {
|
|
|
60
60
|
const deltaY = clientY - (rect.top + rect.height / 2);
|
|
61
61
|
return normalizeBearing(Math.atan2(deltaX, -deltaY) * 180 / Math.PI);
|
|
62
62
|
};
|
|
63
|
+
const sectorIndexForBearing = (bearing, count) => Math.floor(normalizeBearing(bearing) / (360 / count)) % count;
|
|
63
64
|
const DirectionalColorWheel = ({ accessibleName = "Directional bearing color wheel", bearing, className, colorAngle, colorForBearing, colorStops, dataTestId = "spectral-directional-color-wheel", defaultColorAngle, defaultDisabledSectors, defaultThreshold = 0, disabled = false, disabledSectors, onColorAngleChange, onDisabledSectorsChange, onSectorToggle, onThresholdChange, ref, sectorCount = 12, size = 240, threshold, thresholdStep = .05 }) => {
|
|
64
65
|
const rootRef = useRef(null);
|
|
65
66
|
const wedgeRefs = useRef([]);
|
|
@@ -87,7 +88,10 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
87
88
|
const [draggingDial, setDraggingDial] = useState(false);
|
|
88
89
|
const [dialFocused, setDialFocused] = useState(false);
|
|
89
90
|
const [rotatingRing, setRotatingRing] = useState(false);
|
|
91
|
+
const [brushing, setBrushing] = useState(false);
|
|
92
|
+
const [brushedIndices, setBrushedIndices] = useState([]);
|
|
90
93
|
const rotationRef = useRef(null);
|
|
94
|
+
const brushRef = useRef(null);
|
|
91
95
|
const suppressClickRef = useRef(false);
|
|
92
96
|
const canRotate = !disabled && (colorAngle === void 0 || onColorAngleChange !== void 0);
|
|
93
97
|
const disabledSet = useMemo(() => new Set(disabledValue.filter((index) => index >= 0 && index < sectorCount)), [disabledValue, sectorCount]);
|
|
@@ -121,11 +125,74 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
121
125
|
sectorCount,
|
|
122
126
|
setDisabledValue
|
|
123
127
|
]);
|
|
124
|
-
const
|
|
128
|
+
const commitDisabledSet = useCallback((next) => {
|
|
129
|
+
setDisabledValue([...next].filter((value) => value >= 0 && value < sectorCount).sort((first, second) => first - second));
|
|
130
|
+
}, [sectorCount, setDisabledValue]);
|
|
131
|
+
const setSectorDisabled = useCallback((index, nextDisabled) => {
|
|
132
|
+
if (disabled || disabledSet.has(index) === nextDisabled) return;
|
|
133
|
+
const next = new Set(disabledSet);
|
|
134
|
+
if (nextDisabled) next.add(index);
|
|
135
|
+
else next.delete(index);
|
|
136
|
+
commitDisabledSet(next);
|
|
137
|
+
onSectorToggle?.(index, nextDisabled);
|
|
138
|
+
}, [
|
|
139
|
+
commitDisabledSet,
|
|
140
|
+
disabled,
|
|
141
|
+
disabledSet,
|
|
142
|
+
onSectorToggle
|
|
143
|
+
]);
|
|
144
|
+
const paintBrushSector = useCallback((brush, index) => {
|
|
145
|
+
brush.brushed.add(index);
|
|
146
|
+
if (brush.disabled.has(index) === brush.targetDisabled) return;
|
|
147
|
+
if (brush.targetDisabled) brush.disabled.add(index);
|
|
148
|
+
else brush.disabled.delete(index);
|
|
149
|
+
commitDisabledSet(brush.disabled);
|
|
150
|
+
onSectorToggle?.(index, brush.targetDisabled);
|
|
151
|
+
}, [commitDisabledSet, onSectorToggle]);
|
|
152
|
+
const startBrush = useCallback((event, index, rect) => {
|
|
153
|
+
if (disabled) return;
|
|
154
|
+
try {
|
|
155
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
156
|
+
} catch {}
|
|
157
|
+
const targetDisabled = !disabledSet.has(index);
|
|
158
|
+
const working = new Set(disabledSet);
|
|
159
|
+
if (targetDisabled) working.add(index);
|
|
160
|
+
else working.delete(index);
|
|
161
|
+
brushRef.current = {
|
|
162
|
+
brushed: new Set([index]),
|
|
163
|
+
disabled: working,
|
|
164
|
+
lastBearing: pointerBearing(event.clientX, event.clientY, rect),
|
|
165
|
+
pointerId: event.pointerId,
|
|
166
|
+
rect,
|
|
167
|
+
targetDisabled
|
|
168
|
+
};
|
|
169
|
+
suppressClickRef.current = true;
|
|
170
|
+
setBrushing(true);
|
|
171
|
+
setBrushedIndices([index]);
|
|
172
|
+
commitDisabledSet(working);
|
|
173
|
+
onSectorToggle?.(index, targetDisabled);
|
|
174
|
+
}, [
|
|
175
|
+
commitDisabledSet,
|
|
176
|
+
disabled,
|
|
177
|
+
disabledSet,
|
|
178
|
+
onSectorToggle
|
|
179
|
+
]);
|
|
180
|
+
const endBrush = useCallback(() => {
|
|
181
|
+
if (brushRef.current === null) return;
|
|
182
|
+
brushRef.current = null;
|
|
183
|
+
setBrushing(false);
|
|
184
|
+
setBrushedIndices([]);
|
|
185
|
+
setFocusedSectorIndex(null);
|
|
186
|
+
}, []);
|
|
187
|
+
const handleSectorPointerDown = useCallback((event, index) => {
|
|
125
188
|
suppressClickRef.current = false;
|
|
126
|
-
if (!canRotate) return;
|
|
127
189
|
const rect = rootRef.current?.getBoundingClientRect();
|
|
128
190
|
if (!rect) return;
|
|
191
|
+
if (event.shiftKey) {
|
|
192
|
+
startBrush(event, index, rect);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!canRotate) return;
|
|
129
196
|
try {
|
|
130
197
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
131
198
|
} catch {}
|
|
@@ -139,8 +206,30 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
139
206
|
startY: event.clientY,
|
|
140
207
|
wedge: event.currentTarget
|
|
141
208
|
};
|
|
142
|
-
}, [
|
|
209
|
+
}, [
|
|
210
|
+
canRotate,
|
|
211
|
+
colorAngleValue,
|
|
212
|
+
startBrush
|
|
213
|
+
]);
|
|
143
214
|
const handleRingPointerMove = useCallback((event) => {
|
|
215
|
+
const brush = brushRef.current;
|
|
216
|
+
if (brush !== null) {
|
|
217
|
+
if (brush.pointerId !== event.pointerId) return;
|
|
218
|
+
const bearingNow = pointerBearing(event.clientX, event.clientY, brush.rect);
|
|
219
|
+
let delta = bearingNow - brush.lastBearing;
|
|
220
|
+
if (delta > 180) delta -= 360;
|
|
221
|
+
if (delta < -180) delta += 360;
|
|
222
|
+
const step = delta >= 0 ? 1 : -1;
|
|
223
|
+
const toIndex = sectorIndexForBearing(bearingNow, sectorCount);
|
|
224
|
+
let index = sectorIndexForBearing(brush.lastBearing, sectorCount);
|
|
225
|
+
while (index !== toIndex) {
|
|
226
|
+
index = (index + step + sectorCount) % sectorCount;
|
|
227
|
+
paintBrushSector(brush, index);
|
|
228
|
+
}
|
|
229
|
+
brush.lastBearing = bearingNow;
|
|
230
|
+
setBrushedIndices([...brush.brushed]);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
144
233
|
const drag = rotationRef.current;
|
|
145
234
|
if (drag === null || drag.pointerId !== event.pointerId) return;
|
|
146
235
|
const bearingNow = pointerBearing(event.clientX, event.clientY, drag.rect);
|
|
@@ -157,7 +246,11 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
157
246
|
drag.wedge.blur();
|
|
158
247
|
}
|
|
159
248
|
setColorAngleValue(drag.angle);
|
|
160
|
-
}, [
|
|
249
|
+
}, [
|
|
250
|
+
paintBrushSector,
|
|
251
|
+
sectorCount,
|
|
252
|
+
setColorAngleValue
|
|
253
|
+
]);
|
|
161
254
|
const endRingRotation = useCallback(() => {
|
|
162
255
|
rotationRef.current = null;
|
|
163
256
|
setRotatingRing(false);
|
|
@@ -168,16 +261,20 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
168
261
|
}, []);
|
|
169
262
|
const handleWedgeKeyDown = useCallback((event, index) => {
|
|
170
263
|
if (disabled) return;
|
|
264
|
+
const moveFocus = (next) => {
|
|
265
|
+
if (event.shiftKey) setSectorDisabled(next, disabledSet.has(index));
|
|
266
|
+
focusSector(next);
|
|
267
|
+
};
|
|
171
268
|
switch (event.key) {
|
|
172
269
|
case "ArrowRight":
|
|
173
270
|
case "ArrowDown":
|
|
174
271
|
event.preventDefault();
|
|
175
|
-
|
|
272
|
+
moveFocus((index + 1) % sectorCount);
|
|
176
273
|
break;
|
|
177
274
|
case "ArrowLeft":
|
|
178
275
|
case "ArrowUp":
|
|
179
276
|
event.preventDefault();
|
|
180
|
-
|
|
277
|
+
moveFocus((index - 1 + sectorCount) % sectorCount);
|
|
181
278
|
break;
|
|
182
279
|
case "Home":
|
|
183
280
|
event.preventDefault();
|
|
@@ -191,8 +288,10 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
191
288
|
}
|
|
192
289
|
}, [
|
|
193
290
|
disabled,
|
|
291
|
+
disabledSet,
|
|
194
292
|
focusSector,
|
|
195
|
-
sectorCount
|
|
293
|
+
sectorCount,
|
|
294
|
+
setSectorDisabled
|
|
196
295
|
]);
|
|
197
296
|
const setThresholdClamped = useCallback((next) => {
|
|
198
297
|
setThresholdValue(clampUnit(next));
|
|
@@ -256,6 +355,7 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
256
355
|
const ringDiameterPercent = clampedThreshold * 100 * THRESHOLD_TRACK_FRACTION;
|
|
257
356
|
const dotTopPercent = 50 - clampedThreshold * 50 * THRESHOLD_TRACK_FRACTION;
|
|
258
357
|
const hoverHighlightIndex = !rotatingRing && hoveredSectorIndex !== null && hoveredSectorIndex !== focusedSectorIndex ? hoveredSectorIndex : null;
|
|
358
|
+
const ringIndices = brushing ? brushedIndices : focusedSectorIndex !== null && !rotatingRing ? [focusedSectorIndex] : [];
|
|
259
359
|
const safeActiveIndex = Math.min(activeSectorIndex, sectorCount - 1);
|
|
260
360
|
const saturation = thresholdToSaturation(clampedThreshold);
|
|
261
361
|
const hasNeedle = typeof bearing === "number" && Number.isFinite(bearing);
|
|
@@ -280,6 +380,7 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
280
380
|
children: [
|
|
281
381
|
/* @__PURE__ */ jsx("div", {
|
|
282
382
|
className: "col-start-1 row-start-1 grid size-full",
|
|
383
|
+
"data-brushing": brushing || void 0,
|
|
283
384
|
"data-rotatable": canRotate || void 0,
|
|
284
385
|
"data-rotating": rotatingRing || void 0,
|
|
285
386
|
"data-slot": "directional-color-wheel-sectors",
|
|
@@ -290,7 +391,7 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
290
391
|
return /* @__PURE__ */ jsx("button", {
|
|
291
392
|
"aria-label": `Bearing sector ${Math.round(sector.startBearing)}° to ${Math.round(sector.endBearing)}°`,
|
|
292
393
|
"aria-pressed": isEnabled,
|
|
293
|
-
className: cn("col-start-1 row-start-1 size-full cursor-pointer outline-none motion-safe:transition-opacity motion-safe:duration-150", !isEnabled && "opacity-25", canRotate && "touch-none", canRotate && (rotatingRing ? "cursor-grabbing" : "cursor-grab")),
|
|
394
|
+
className: cn("col-start-1 row-start-1 size-full cursor-pointer outline-none motion-safe:transition-opacity motion-safe:duration-150", !isEnabled && "opacity-25", canRotate && "touch-none", canRotate && (rotatingRing ? "cursor-grabbing" : "cursor-grab"), brushing && "cursor-crosshair"),
|
|
294
395
|
"data-sector-id": sector.id,
|
|
295
396
|
"data-slot": "directional-color-wheel-sector",
|
|
296
397
|
"data-testid": `${dataTestId}-sector-${sector.index}`,
|
|
@@ -312,8 +413,13 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
312
413
|
onKeyDown: (event) => {
|
|
313
414
|
handleWedgeKeyDown(event, sector.index);
|
|
314
415
|
},
|
|
315
|
-
onLostPointerCapture:
|
|
316
|
-
|
|
416
|
+
onLostPointerCapture: () => {
|
|
417
|
+
endRingRotation();
|
|
418
|
+
endBrush();
|
|
419
|
+
},
|
|
420
|
+
onPointerDown: (event) => {
|
|
421
|
+
handleSectorPointerDown(event, sector.index);
|
|
422
|
+
},
|
|
317
423
|
onPointerEnter: () => {
|
|
318
424
|
setHoveredSectorIndex(sector.index);
|
|
319
425
|
},
|
|
@@ -321,7 +427,10 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
321
427
|
setHoveredSectorIndex((current) => current === sector.index ? null : current);
|
|
322
428
|
},
|
|
323
429
|
onPointerMove: handleRingPointerMove,
|
|
324
|
-
onPointerUp:
|
|
430
|
+
onPointerUp: () => {
|
|
431
|
+
endRingRotation();
|
|
432
|
+
endBrush();
|
|
433
|
+
},
|
|
325
434
|
ref: (node) => {
|
|
326
435
|
wedgeRefs.current[sector.index] = node;
|
|
327
436
|
},
|
|
@@ -342,21 +451,21 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
342
451
|
viewBox: "0 0 100 100",
|
|
343
452
|
children: [
|
|
344
453
|
/* @__PURE__ */ jsx("circle", {
|
|
345
|
-
className: "fill-none stroke-border-
|
|
454
|
+
className: "fill-none stroke-border-secondary",
|
|
346
455
|
cx: CENTER,
|
|
347
456
|
cy: CENTER,
|
|
348
457
|
r: OUTER_RADIUS,
|
|
349
|
-
strokeWidth: .
|
|
458
|
+
strokeWidth: .7
|
|
350
459
|
}),
|
|
351
460
|
/* @__PURE__ */ jsx("circle", {
|
|
352
|
-
className: "fill-none stroke-border-
|
|
461
|
+
className: "fill-none stroke-border-secondary",
|
|
353
462
|
cx: CENTER,
|
|
354
463
|
cy: CENTER,
|
|
355
464
|
r: INNER_RADIUS,
|
|
356
|
-
strokeWidth: .
|
|
465
|
+
strokeWidth: .7
|
|
357
466
|
}),
|
|
358
467
|
/* @__PURE__ */ jsx("circle", {
|
|
359
|
-
className: "fill-none stroke-border-
|
|
468
|
+
className: "fill-none stroke-border-secondary/60",
|
|
360
469
|
cx: CENTER,
|
|
361
470
|
cy: CENTER,
|
|
362
471
|
r: TICK_OUTER_RADIUS,
|
|
@@ -365,9 +474,9 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
365
474
|
/* @__PURE__ */ jsx("g", {
|
|
366
475
|
"data-testid": `${dataTestId}-degree-ticks`,
|
|
367
476
|
children: DEGREE_TICKS.map((tick) => /* @__PURE__ */ jsx("line", {
|
|
368
|
-
className: tick.isMajor ? "stroke-text-secondary" : "stroke-
|
|
477
|
+
className: tick.isMajor ? "stroke-text-secondary" : "stroke-text-disabled",
|
|
369
478
|
strokeLinecap: "round",
|
|
370
|
-
strokeWidth: tick.isMajor ? .8 : .
|
|
479
|
+
strokeWidth: tick.isMajor ? .8 : .5,
|
|
371
480
|
x1: tick.inner.x,
|
|
372
481
|
x2: tick.outer.x,
|
|
373
482
|
y1: tick.inner.y,
|
|
@@ -378,12 +487,12 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
378
487
|
className: "fill-text-primary/15",
|
|
379
488
|
d: annularSectorPath(CENTER, CENTER, INNER_RADIUS, OUTER_RADIUS, sectors[hoverHighlightIndex].startBearing, sectors[hoverHighlightIndex].endBearing)
|
|
380
489
|
}) : null,
|
|
381
|
-
|
|
490
|
+
ringIndices.map((ringIndex) => sectors[ringIndex] ? /* @__PURE__ */ jsx("path", {
|
|
382
491
|
className: "fill-none stroke-accent",
|
|
383
|
-
d: annularSectorPath(CENTER, CENTER, INNER_RADIUS + FOCUS_RADIAL_INSET, OUTER_RADIUS - FOCUS_RADIAL_INSET, sectors[
|
|
492
|
+
d: annularSectorPath(CENTER, CENTER, INNER_RADIUS + FOCUS_RADIAL_INSET, OUTER_RADIUS - FOCUS_RADIAL_INSET, sectors[ringIndex].startBearing + FOCUS_ANGULAR_INSET, sectors[ringIndex].endBearing - FOCUS_ANGULAR_INSET),
|
|
384
493
|
strokeLinejoin: "round",
|
|
385
494
|
strokeWidth: FOCUS_STROKE_WIDTH
|
|
386
|
-
}) : null,
|
|
495
|
+
}, ringIndex) : null),
|
|
387
496
|
BEZEL_LABELS.map((label) => /* @__PURE__ */ jsx("text", {
|
|
388
497
|
className: label.isCardinal ? "font-medium fill-text-secondary text-[6px]" : "fill-text-secondary text-[4px] tabular-nums",
|
|
389
498
|
dominantBaseline: "central",
|