@spear-ai/spectral 1.20.1 → 1.21.0
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/Accordion.js +0 -4
- package/dist/Accordion.js.map +1 -1
- package/dist/Alert/AlertBase.d.ts +0 -1
- package/dist/Alert/AlertBase.d.ts.map +1 -1
- package/dist/Alert/AlertBase.js +14 -17
- package/dist/Alert/AlertBase.js.map +1 -1
- package/dist/Alert.js +16 -4
- package/dist/Alert.js.map +1 -1
- package/dist/AlertDialog.js +3 -3
- package/dist/AlertDialog.js.map +1 -1
- package/dist/Avatar.js +1 -9
- package/dist/Avatar.js.map +1 -1
- package/dist/Badge.js +0 -1
- package/dist/Badge.js.map +1 -1
- package/dist/Button.js +0 -3
- package/dist/Button.js.map +1 -1
- package/dist/ButtonGroup.js +0 -4
- package/dist/ButtonGroup.js.map +1 -1
- package/dist/ButtonIcon.js +0 -1
- package/dist/ButtonIcon.js.map +1 -1
- package/dist/ButtonIconSlideout.js +0 -3
- package/dist/ButtonIconSlideout.js.map +1 -1
- package/dist/Checkbox.d.ts.map +1 -1
- package/dist/Checkbox.js +5 -8
- package/dist/Checkbox.js.map +1 -1
- package/dist/Combobox.d.ts +1 -0
- package/dist/Combobox.d.ts.map +1 -1
- package/dist/Combobox.js +0 -4
- package/dist/Combobox.js.map +1 -1
- package/dist/ControlGroup/ControlGroupSelect.js +0 -5
- package/dist/ControlGroup/ControlGroupSelect.js.map +1 -1
- package/dist/DataCard/Card.js +0 -6
- package/dist/DataCard/Card.js.map +1 -1
- package/dist/DataCard.js +1 -7
- package/dist/DataCard.js.map +1 -1
- package/dist/DateTimePicker/Calendar.js +0 -1
- package/dist/DateTimePicker/Calendar.js.map +1 -1
- package/dist/DateTimePicker/DateTimeInput.js +0 -1
- package/dist/DateTimePicker/DateTimeInput.js.map +1 -1
- package/dist/DateTimePicker/TimePeriodSelect.js +0 -4
- package/dist/DateTimePicker/TimePeriodSelect.js.map +1 -1
- package/dist/DateTimePicker/TimePicker.js +0 -3
- package/dist/DateTimePicker/TimePicker.js.map +1 -1
- package/dist/DateTimePicker.js +0 -4
- package/dist/DateTimePicker.js.map +1 -1
- package/dist/Dialog.js +0 -16
- package/dist/Dialog.js.map +1 -1
- package/dist/DirectionalColorWheel/DirectionalColorWheelDisclosure.js +0 -2
- package/dist/DirectionalColorWheel/DirectionalColorWheelDisclosure.js.map +1 -1
- package/dist/DirectionalColorWheel/DirectionalColorWheelGlyph.js +0 -1
- package/dist/DirectionalColorWheel/DirectionalColorWheelGlyph.js.map +1 -1
- package/dist/DirectionalColorWheel.js +9 -21
- package/dist/DirectionalColorWheel.js.map +1 -1
- package/dist/Drawer.js +6 -29
- package/dist/Drawer.js.map +1 -1
- package/dist/DropdownMenu.js +1 -9
- package/dist/DropdownMenu.js.map +1 -1
- package/dist/FormFieldMessage.js +0 -1
- package/dist/FormFieldMessage.js.map +1 -1
- package/dist/HoverCard.js +0 -3
- package/dist/HoverCard.js.map +1 -1
- package/dist/Input.js +0 -10
- package/dist/Input.js.map +1 -1
- package/dist/InputOTP.js +0 -4
- package/dist/InputOTP.js.map +1 -1
- package/dist/InputSearch.js +3 -15
- package/dist/InputSearch.js.map +1 -1
- package/dist/Kbd.js +0 -2
- package/dist/Kbd.js.map +1 -1
- package/dist/Meter.d.ts +23 -0
- package/dist/Meter.d.ts.map +1 -0
- package/dist/Meter.js +45 -0
- package/dist/Meter.js.map +1 -0
- package/dist/MultiSelect/MultiSelectBase.js +1 -16
- package/dist/MultiSelect/MultiSelectBase.js.map +1 -1
- package/dist/Popover.js +0 -3
- package/dist/Popover.js.map +1 -1
- package/dist/RadialMenu.js +1 -7
- package/dist/RadialMenu.js.map +1 -1
- package/dist/RadioButtonGroup/RadioButtonGroupBase.d.ts.map +1 -1
- package/dist/RadioButtonGroup/RadioButtonGroupBase.js +1 -4
- package/dist/RadioButtonGroup/RadioButtonGroupBase.js.map +1 -1
- package/dist/RadioButtonGroup.js +1 -1
- package/dist/RadioButtonGroup.js.map +1 -1
- package/dist/RadioGroup.js +0 -6
- package/dist/RadioGroup.js.map +1 -1
- package/dist/Select.js +12 -41
- package/dist/Select.js.map +1 -1
- package/dist/Slider.js +2 -11
- package/dist/Slider.js.map +1 -1
- package/dist/Switch.js +0 -6
- package/dist/Switch.js.map +1 -1
- package/dist/Tabs/TabsBase.js +0 -5
- package/dist/Tabs/TabsBase.js.map +1 -1
- package/dist/Textarea.js +0 -4
- package/dist/Textarea.js.map +1 -1
- package/dist/Toast.d.ts +2 -0
- package/dist/Toast.d.ts.map +1 -1
- package/dist/Toast.js +9 -8
- package/dist/Toast.js.map +1 -1
- package/dist/Toggle.js +0 -1
- package/dist/Toggle.js.map +1 -1
- package/dist/ToggleGroup/ToggleGroupItem.js +0 -1
- package/dist/ToggleGroup/ToggleGroupItem.js.map +1 -1
- package/dist/ToggleGroup.js +0 -1
- package/dist/ToggleGroup.js.map +1 -1
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip.js +4 -5
- package/dist/Tooltip.js.map +1 -1
- package/dist/Tray.js +1 -9
- package/dist/Tray.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/styles/horizon/base.css +31 -16
- package/dist/styles/horizon/colors.css +37 -21
- package/dist/styles/horizon/theme.css +15 -7
- package/dist/styles/horizon/utilities.css +19 -45
- package/dist/styles/spectral.css +1 -1
- package/package.json +4 -1
|
@@ -370,7 +370,6 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
370
370
|
className: cn("relative inline-grid select-none", disabled && "pointer-events-none opacity-50", className),
|
|
371
371
|
"data-disabled": disabled || void 0,
|
|
372
372
|
"data-slot": "directional-color-wheel",
|
|
373
|
-
"data-testid": dataTestId,
|
|
374
373
|
ref: handleRef,
|
|
375
374
|
role: "group",
|
|
376
375
|
style: {
|
|
@@ -384,7 +383,6 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
384
383
|
"data-rotatable": canRotate || void 0,
|
|
385
384
|
"data-rotating": rotatingRing || void 0,
|
|
386
385
|
"data-slot": "directional-color-wheel-sectors",
|
|
387
|
-
"data-testid": `${dataTestId}-sectors`,
|
|
388
386
|
style: { filter: `saturate(${saturation})` },
|
|
389
387
|
children: sectors.map((sector) => {
|
|
390
388
|
const isEnabled = !disabledSet.has(sector.index);
|
|
@@ -394,7 +392,6 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
394
392
|
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"),
|
|
395
393
|
"data-sector-id": sector.id,
|
|
396
394
|
"data-slot": "directional-color-wheel-sector",
|
|
397
|
-
"data-testid": `${dataTestId}-sector-${sector.index}`,
|
|
398
395
|
disabled,
|
|
399
396
|
onBlur: () => {
|
|
400
397
|
setFocusedSectorIndex((current) => current === sector.index ? null : current);
|
|
@@ -447,7 +444,6 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
447
444
|
"aria-hidden": true,
|
|
448
445
|
className: "pointer-events-none z-10 col-start-1 row-start-1 size-full overflow-visible",
|
|
449
446
|
"data-slot": "directional-color-wheel-overlay",
|
|
450
|
-
"data-testid": `${dataTestId}-overlay`,
|
|
451
447
|
viewBox: "0 0 100 100",
|
|
452
448
|
children: [
|
|
453
449
|
/* @__PURE__ */ jsx("circle", {
|
|
@@ -471,18 +467,15 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
471
467
|
r: TICK_OUTER_RADIUS,
|
|
472
468
|
strokeWidth: .4
|
|
473
469
|
}),
|
|
474
|
-
/* @__PURE__ */ jsx("g", {
|
|
475
|
-
"
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
y2: tick.outer.y
|
|
484
|
-
}, tick.bearing))
|
|
485
|
-
}),
|
|
470
|
+
/* @__PURE__ */ jsx("g", { children: DEGREE_TICKS.map((tick) => /* @__PURE__ */ jsx("line", {
|
|
471
|
+
className: tick.isMajor ? "stroke-text-secondary" : "stroke-text-disabled",
|
|
472
|
+
strokeLinecap: "round",
|
|
473
|
+
strokeWidth: tick.isMajor ? .8 : .5,
|
|
474
|
+
x1: tick.inner.x,
|
|
475
|
+
x2: tick.outer.x,
|
|
476
|
+
y1: tick.inner.y,
|
|
477
|
+
y2: tick.outer.y
|
|
478
|
+
}, tick.bearing)) }),
|
|
486
479
|
hoverHighlightIndex !== null && sectors[hoverHighlightIndex] ? /* @__PURE__ */ jsx("path", {
|
|
487
480
|
className: "fill-text-primary/15",
|
|
488
481
|
d: annularSectorPath(CENTER, CENTER, INNER_RADIUS, OUTER_RADIUS, sectors[hoverHighlightIndex].startBearing, sectors[hoverHighlightIndex].endBearing)
|
|
@@ -506,7 +499,6 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
506
499
|
/* @__PURE__ */ jsxs("div", {
|
|
507
500
|
className: cn("relative z-20 col-start-1 row-start-1 m-auto flex touch-none items-center justify-center rounded-full border border-border-primary shadow-elevation-2", draggingDial ? "cursor-grabbing" : "cursor-grab"),
|
|
508
501
|
"data-slot": "directional-color-wheel-threshold-dial",
|
|
509
|
-
"data-testid": `${dataTestId}-threshold-dial`,
|
|
510
502
|
onLostPointerCapture: () => {
|
|
511
503
|
setDraggingDial(false);
|
|
512
504
|
},
|
|
@@ -534,7 +526,6 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
534
526
|
"aria-hidden": true,
|
|
535
527
|
className: "absolute left-1/2 size-[15%] -translate-x-1/2 -translate-y-1/2 rounded-full border border-border-primary bg-danger-400 shadow-elevation-1",
|
|
536
528
|
"data-slot": "directional-color-wheel-threshold-indicator",
|
|
537
|
-
"data-testid": `${dataTestId}-threshold-indicator`,
|
|
538
529
|
style: { top: `${dotTopPercent}%` }
|
|
539
530
|
}),
|
|
540
531
|
/* @__PURE__ */ jsx("input", {
|
|
@@ -542,7 +533,6 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
542
533
|
"aria-valuetext": `${Math.round(clampedThreshold * 100)} percent`,
|
|
543
534
|
className: "sr-only",
|
|
544
535
|
"data-slot": "directional-color-wheel-threshold-input",
|
|
545
|
-
"data-testid": `${dataTestId}-threshold`,
|
|
546
536
|
disabled,
|
|
547
537
|
max: 1,
|
|
548
538
|
min: 0,
|
|
@@ -592,7 +582,6 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
592
582
|
strokeWidth: FOCUS_STROKE_WIDTH
|
|
593
583
|
}) : null,
|
|
594
584
|
hasNeedle ? /* @__PURE__ */ jsxs("g", {
|
|
595
|
-
"data-testid": `${dataTestId}-needle`,
|
|
596
585
|
filter: `url(#${needleShadowId})`,
|
|
597
586
|
children: [/* @__PURE__ */ jsx("polygon", {
|
|
598
587
|
className: "fill-danger-500",
|
|
@@ -606,7 +595,6 @@ const DirectionalColorWheel = ({ accessibleName = "Directional bearing color whe
|
|
|
606
595
|
}) : null,
|
|
607
596
|
hasNeedle ? /* @__PURE__ */ jsxs("span", {
|
|
608
597
|
className: "sr-only",
|
|
609
|
-
"data-testid": `${dataTestId}-bearing-value`,
|
|
610
598
|
children: [
|
|
611
599
|
"Bearing ",
|
|
612
600
|
Math.round(needleBearing),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DirectionalColorWheel.js","names":[],"sources":["../src/components/DirectionalColorWheel/DirectionalColorWheel.tsx"],"sourcesContent":["import { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useId, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type Ref } from 'react'\nimport { annularSectorPath, normalizeBearing, polarToCartesian, resolveSectorColor, sectorBearingRange, sectorClipPath, sectorId, thresholdToSaturation, type DirectionalColorStop, type DirectionalSectorCount } from './directionalColorWheelMath'\n\nexport interface DirectionalColorWheelProps {\n /** Accessible name for the wheel group. */\n accessibleName?: string\n /**\n * Read-only bearing (degrees) the needle points at, using the compositor's `atan2(sine, cosine)`\n * convention (0° = N, clockwise). The consumer drives it (e.g. from the feed hover cursor) so the\n * needle follows the cursor; the wheel only displays it — there is no drag interaction.\n * `null`/`undefined` hides the needle — keep it nullable so Horizon can gate the feature flag with\n * a single `null` (PAT-028).\n */\n bearing?: number | null\n /** Layout-only class extension (margins/padding). */\n className?: string\n /**\n * \"Color Angle\": degrees the colours are rotated clockwise around the ring. Controlled+uncontrolled\n * (pair it with `defaultColorAngle` / `onColorAngleChange`). The colours can also be spun directly by\n * dragging the colour ring — that gesture reports through `onColorAngleChange` and is only active when\n * the value can take effect (uncontrolled, or controlled *with* an `onColorAngleChange` handler). Sector\n * geometry and the N/E/S/W bezel stay fixed to compass bearings; only the colours rotate.\n */\n colorAngle?: number\n /**\n * Advanced parity hook: resolver mapping a bearing in `[0, 360)` to a CSS colour. When\n * provided it wins over `colorStops`; pass the same resolver the feed colours by so the\n * legend samples the identical colours.\n */\n colorForBearing?: ((bearingDegrees: number) => string) | null\n /** Palette stops (position `0..1`). Defaults to the directional DIFAR palette. */\n colorStops?: DirectionalColorStop[] | null\n dataTestId?: string\n /** Uncontrolled initial Color Angle (degrees). */\n defaultColorAngle?: number\n /** Uncontrolled initial set of disabled (hidden) sector indices. */\n defaultDisabledSectors?: number[]\n /** Uncontrolled initial threshold. */\n defaultThreshold?: number\n disabled?: boolean\n /** Controlled set of disabled (hidden) sector indices, in `[0, sectorCount)`. */\n disabledSectors?: number[] | null\n /** Called with the next Color Angle (degrees, `[0, 360)`) as the colour ring is dragged. */\n onColorAngleChange?: (colorAngle: number) => void\n /** Called with the next set of disabled sector indices whenever a wedge is toggled. */\n onDisabledSectorsChange?: (disabledSectors: number[]) => void\n /** Called when a single wedge is toggled: its index and whether it is now disabled. */\n onSectorToggle?: (sectorIndex: number, nextDisabled: boolean) => void\n /** Called with the threshold (`0..1`) as the centre dial changes. */\n onThresholdChange?: (threshold: number) => void\n ref?: Ref<HTMLDivElement>\n /** Number of toggleable wedges. */\n sectorCount?: DirectionalSectorCount\n /** Diameter in pixels. */\n size?: number\n /**\n * Coherence threshold (`0..1`). Coherence is how reliable the bearing estimate is; this dial\n * maps it to saturation — higher threshold desaturates the legend. The component only reports\n * the value via `onThresholdChange`; the consumer decides what it drives.\n */\n threshold?: number\n /** Keyboard step for the threshold dial. */\n thresholdStep?: number\n}\n\n// viewBox is a 0..100 square; every radius is a fraction of the wheel. The ring sits well\n// inside the box so the N/E/S/W labels have clear breathing room outside it.\nconst CENTER = 50\nconst OUTER_RADIUS = 38\nconst INNER_RADIUS = 22\nconst LABEL_RADIUS = 47\n// The focus ring sits just inside the wedge with a minimal gap (~1-2px at typical sizes) so the\n// stroke never grazes the shared wedge edge or bleeds onto the neighbour (which left a focus remnant\n// when a wedge was re-enabled), while staying tight enough to read as that wedge's outline.\nconst FOCUS_RADIAL_INSET = 0.7\nconst FOCUS_ANGULAR_INSET = 1.1\n// The threshold dial's focus ring is an SVG circle just outside the hub, stroked with the same\n// width and viewBox units as the sector focus ring, so the two are identical at any `size`.\nconst THRESHOLD_FOCUS_RADIUS = (INNER_RADIUS * 1.7) / 2 + 0.8\nconst FOCUS_STROKE_WIDTH = 0.5\n// Read-only needle: a slim lance across the colour ring whose tip points at the bearing. It's\n// split down its centreline into a lighter and darker red facet (a compass-needle highlight) and\n// lifted off the ring with a soft drop-shadow rather than a hard outline.\nconst NEEDLE_TIP_RADIUS = 39\nconst NEEDLE_BASE_RADIUS = INNER_RADIUS\nconst NEEDLE_HALF_WIDTH_DEGREES = 3\n// Watch-style degree bezel just outside the colour ring: a tick every TICK_STEP_DEGREES, longer\n// at each 30° (which line up with the N/E/S/W labels). Labels sit further out (LABEL_RADIUS) so\n// the ticks never crowd them.\nconst TICK_OUTER_RADIUS = 43\nconst TICK_INNER_MAJOR = 39\nconst TICK_INNER_MINOR = 41\nconst TICK_STEP_DEGREES = 10\n// The threshold is shown as a concentric ring inside the hub: radius 0 (a point at the centre)\n// at threshold 0, growing to this fraction of the hub radius at threshold 1.\nconst THRESHOLD_TRACK_FRACTION = 0.82\n// 0° = North (top), increasing clockwise — the compass convention the colours also use.\nconst CARDINAL_LABELS: Record<number, string> = { 0: 'N', 90: 'E', 180: 'S', 270: 'W' }\n// Precomputed once (geometry only): the degree-bezel tick lines, longer at each 30°.\nconst DEGREE_TICKS = Array.from({ length: 360 / TICK_STEP_DEGREES }, (_, index) => {\n const bearing = index * TICK_STEP_DEGREES\n const isMajor = bearing % 30 === 0\n return {\n bearing,\n inner: polarToCartesian(CENTER, CENTER, isMajor ? TICK_INNER_MAJOR : TICK_INNER_MINOR, bearing),\n isMajor,\n outer: polarToCartesian(CENTER, CENTER, TICK_OUTER_RADIUS, bearing),\n }\n})\n// One label per 30° major tick: the cardinal letter (N/E/S/W) where there is one, otherwise the\n// bearing in degrees. Sits at LABEL_RADIUS, outside the tick bezel.\nconst BEZEL_LABELS = Array.from({ length: 12 }, (_, index) => {\n const bearing = index * 30\n const cardinal = CARDINAL_LABELS[bearing]\n return {\n at: polarToCartesian(CENTER, CENTER, LABEL_RADIUS, bearing),\n bearing,\n isCardinal: cardinal !== undefined,\n text: cardinal ?? String(bearing),\n }\n})\n\nconst clampUnit = (value: number): number => Math.max(0, Math.min(1, value))\n\n// Threshold from how far the pointer sits *above* the hub centre, as a fraction of the ring's\n// max radius: 0 at/below the centre, 1 at the top of the track. Signed-and-clamped (not radial\n// distance) so the ring shrinks to a point and stops dead at the centre instead of bouncing\n// back out the other side. `setThresholdClamped` floors the negative (below-centre) values at 0.\nconst pointerThreshold = (clientY: number, rect: DOMRect): number => {\n const deltaY = clientY - (rect.top + rect.height / 2)\n const maxRadiusPx = ((INNER_RADIUS * 1.7) / 100 / 2) * rect.width * THRESHOLD_TRACK_FRACTION\n return -deltaY / maxRadiusPx\n}\n\n// Drag distance (px) below which a press is a sector tap, not a ring rotation.\nconst ROTATE_SLOP_PX = 4\n\n// Pointer's compass bearing about the wheel centre, matching `polarToCartesian` (atan2(dx, −dy)).\nconst pointerBearing = (clientX: number, clientY: number, rect: DOMRect): number => {\n const deltaX = clientX - (rect.left + rect.width / 2)\n const deltaY = clientY - (rect.top + rect.height / 2)\n return normalizeBearing((Math.atan2(deltaX, -deltaY) * 180) / Math.PI)\n}\n\n// Which sector a compass bearing falls in, for `count` equal sectors starting at 0° (matches `sectorBearingRange`).\nconst sectorIndexForBearing = (bearing: number, count: number): number => Math.floor(normalizeBearing(bearing) / (360 / count)) % count\n\nexport const DirectionalColorWheel = ({\n accessibleName = 'Directional bearing color wheel',\n bearing,\n className,\n colorAngle,\n colorForBearing,\n colorStops,\n dataTestId = 'spectral-directional-color-wheel',\n defaultColorAngle,\n defaultDisabledSectors,\n defaultThreshold = 0,\n disabled = false,\n disabledSectors,\n onColorAngleChange,\n onDisabledSectorsChange,\n onSectorToggle,\n onThresholdChange,\n ref,\n sectorCount = 12,\n size = 240,\n threshold,\n thresholdStep = 0.05,\n}: DirectionalColorWheelProps) => {\n const rootRef = useRef<HTMLDivElement>(null)\n const wedgeRefs = useRef<(HTMLButtonElement | null)[]>([])\n const thresholdInputRef = useRef<HTMLInputElement>(null)\n const dialRectRef = useRef<DOMRect | null>(null)\n const needleShadowId = `directional-needle-shadow-${useId().replaceAll(':', '')}`\n\n const [thresholdValue, setThresholdValue] = useUncontrolledState<number>({ defaultValue: clampUnit(defaultThreshold), onChange: onThresholdChange, value: threshold })\n const [disabledValue, setDisabledValue] = useUncontrolledState<number[]>({ defaultValue: defaultDisabledSectors ?? [], onChange: onDisabledSectorsChange, value: disabledSectors ?? undefined })\n const [colorAngleValue, setColorAngleValue] = useUncontrolledState<number>({ defaultValue: defaultColorAngle ?? 0, onChange: onColorAngleChange, value: colorAngle })\n\n const [activeSectorIndex, setActiveSectorIndex] = useState(0)\n const [focusedSectorIndex, setFocusedSectorIndex] = useState<number | null>(null)\n const [hoveredSectorIndex, setHoveredSectorIndex] = useState<number | null>(null)\n const [draggingDial, setDraggingDial] = useState(false)\n const [dialFocused, setDialFocused] = useState(false)\n const [rotatingRing, setRotatingRing] = useState(false)\n const [brushing, setBrushing] = useState(false)\n // The sectors swept by the in-progress brush stroke — accent-ringed as one active selection.\n const [brushedIndices, setBrushedIndices] = useState<number[]>([])\n\n // Live rotation drag (a ref so rapid moves don't hinge on re-render timing).\n const rotationRef = useRef<{ angle: number; lastBearing: number; moved: boolean; pointerId: number; rect: DOMRect; startX: number; startY: number; wedge: HTMLButtonElement } | null>(null)\n // Live shift+drag brush: paint every wedge the pointer crosses to one target state. The working\n // disabled set + the set swept so far live in a ref so the stroke stays self-consistent even before\n // state echoes back; the cached rect + last bearing let each move fill in every sector swept since\n // the previous sample (so a fast drag never skips a wedge — the gesture is geometric, not per-wedge\n // `pointerenter`).\n const brushRef = useRef<{ brushed: Set<number>; disabled: Set<number>; lastBearing: number; pointerId: number; rect: DOMRect; targetDisabled: boolean } | null>(null)\n const suppressClickRef = useRef(false)\n\n // Dragging is enabled only when the value can take effect (uncontrolled, or controlled with a handler).\n const canRotate = !disabled && (colorAngle === undefined || onColorAngleChange !== undefined)\n\n const disabledSet = useMemo(() => new Set(disabledValue.filter((index) => index >= 0 && index < sectorCount)), [disabledValue, sectorCount])\n\n // Flat GRAMS-style wedges, palette sampled at each wedge centre; no divider stroke → seamless ring.\n // Geometry-only, so memoised (not rebuilt on every threshold/hover/focus render).\n const sectors = useMemo(\n () =>\n Array.from({ length: sectorCount }, (_, index) => {\n const range = sectorBearingRange(index, sectorCount)\n return {\n ...range,\n clipPath: sectorClipPath(INNER_RADIUS, OUTER_RADIUS, range.startBearing, range.endBearing),\n color: resolveSectorColor({ colorForBearing, colorStops }, (range.startBearing + range.endBearing) / 2, colorAngleValue),\n id: sectorId(index, sectorCount),\n index,\n }\n }),\n [colorAngleValue, colorForBearing, colorStops, sectorCount],\n )\n\n const commitDisabled = useCallback(\n (index: number) => {\n if (disabled) return\n const next = disabledSet.has(index) ? disabledValue.filter((value) => value !== index) : [...disabledValue.filter((value) => value >= 0 && value < sectorCount), index].sort((first, second) => first - second)\n setDisabledValue(next)\n onSectorToggle?.(index, !disabledSet.has(index))\n },\n [disabled, disabledSet, disabledValue, onSectorToggle, sectorCount, setDisabledValue],\n )\n\n // Write the next disabled-sector set out through the controlled/uncontrolled channel.\n const commitDisabledSet = useCallback(\n (next: Set<number>) => {\n setDisabledValue([...next].filter((value) => value >= 0 && value < sectorCount).sort((first, second) => first - second))\n },\n [sectorCount, setDisabledValue],\n )\n\n // Set a single sector's disabled state (idempotent — only commits/reports a real change). Backs\n // both the keyboard Shift+Arrow brush and the pointer brush.\n const setSectorDisabled = useCallback(\n (index: number, nextDisabled: boolean) => {\n if (disabled || disabledSet.has(index) === nextDisabled) return\n const next = new Set(disabledSet)\n if (nextDisabled) next.add(index)\n else next.delete(index)\n commitDisabledSet(next)\n onSectorToggle?.(index, nextDisabled)\n },\n [commitDisabledSet, disabled, disabledSet, onSectorToggle],\n )\n\n // Paint a single sector to the active brush's target state. Marks it as swept (for the section\n // highlight) regardless, but only commits/reports when its state actually changes.\n const paintBrushSector = useCallback(\n (brush: NonNullable<typeof brushRef.current>, index: number) => {\n brush.brushed.add(index)\n if (brush.disabled.has(index) === brush.targetDisabled) return\n if (brush.targetDisabled) brush.disabled.add(index)\n else brush.disabled.delete(index)\n commitDisabledSet(brush.disabled)\n onSectorToggle?.(index, brush.targetDisabled)\n },\n [commitDisabledSet, onSectorToggle],\n )\n\n // Shift+drag brush: the first wedge toggles, and every wedge the pointer sweeps over is painted to\n // *that* wedge's new state (one consistent direction), so a single stroke hides/shows a run of\n // sectors. The pointer is captured and each move fills in every sector between the last sample and\n // the current bearing — geometric, so a fast drag never skips a wedge.\n const startBrush = useCallback(\n (event: ReactPointerEvent<HTMLButtonElement>, index: number, rect: DOMRect) => {\n if (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 targetDisabled = !disabledSet.has(index)\n const working = new Set(disabledSet)\n if (targetDisabled) working.add(index)\n else working.delete(index)\n brushRef.current = { brushed: new Set([index]), disabled: working, lastBearing: pointerBearing(event.clientX, event.clientY, rect), pointerId: event.pointerId, rect, targetDisabled }\n // Swallow the trailing click so a shift-click that paints one wedge isn't toggled straight back.\n suppressClickRef.current = true\n setBrushing(true)\n setBrushedIndices([index])\n commitDisabledSet(working)\n onSectorToggle?.(index, targetDisabled)\n },\n [commitDisabledSet, disabled, disabledSet, onSectorToggle],\n )\n\n const endBrush = useCallback(() => {\n if (brushRef.current === null) return\n brushRef.current = null\n setBrushing(false)\n // Drop the stroke's section highlight + the focus accent on the start wedge, so once the stroke\n // is done the dimming (uniform across the run) is the only mark — no single wedge singled out.\n setBrushedIndices([])\n setFocusedSectorIndex(null)\n }, [])\n\n const handleSectorPointerDown = useCallback(\n (event: ReactPointerEvent<HTMLButtonElement>, index: number) => {\n // Reset so a prior drag/brush never swallows this interaction's click.\n suppressClickRef.current = false\n const rect = rootRef.current?.getBoundingClientRect()\n if (!rect) return\n // Shift+drag paints sectors (multi-sector brush) instead of spinning the colour ring.\n if (event.shiftKey) {\n startBrush(event, index, rect)\n return\n }\n if (!canRotate) 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 rotationRef.current = { angle: colorAngleValue, lastBearing: pointerBearing(event.clientX, event.clientY, rect), moved: false, pointerId: event.pointerId, rect, startX: event.clientX, startY: event.clientY, wedge: event.currentTarget }\n },\n [canRotate, colorAngleValue, startBrush],\n )\n\n const handleRingPointerMove = useCallback(\n (event: ReactPointerEvent<HTMLButtonElement>) => {\n const brush = brushRef.current\n if (brush !== null) {\n if (brush.pointerId !== event.pointerId) return\n const bearingNow = pointerBearing(event.clientX, event.clientY, brush.rect)\n // The gesture is locked to \"brush\" at pointer-down (Shift was held then); the whole drag\n // paints, so we don't re-check `event.shiftKey` per move — releasing Shift mid-drag would\n // otherwise silently stop the stroke after the first wedge.\n // Step from the last sampled sector to the current one in the direction of travel, painting\n // each — so even a sample that jumps several sectors fills every wedge in between.\n let delta = bearingNow - brush.lastBearing\n if (delta > 180) delta -= 360\n if (delta < -180) delta += 360\n const step = delta >= 0 ? 1 : -1\n const toIndex = sectorIndexForBearing(bearingNow, sectorCount)\n let index = sectorIndexForBearing(brush.lastBearing, sectorCount)\n while (index !== toIndex) {\n index = (index + step + sectorCount) % sectorCount\n paintBrushSector(brush, index)\n }\n brush.lastBearing = bearingNow\n setBrushedIndices([...brush.brushed])\n return\n }\n const drag = rotationRef.current\n if (drag === null || drag.pointerId !== event.pointerId) return\n // Cached rect (the wheel doesn't move mid-drag) avoids a layout read per move.\n const bearingNow = pointerBearing(event.clientX, event.clientY, drag.rect)\n // Signed, wrap-safe step from the last sample (supports multi-turn spins).\n let step = bearingNow - drag.lastBearing\n if (step > 180) step -= 360\n if (step < -180) step += 360\n drag.lastBearing = bearingNow\n drag.angle = normalizeBearing(drag.angle + step)\n if (!drag.moved) {\n if (Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) <= ROTATE_SLOP_PX) return\n drag.moved = true\n suppressClickRef.current = true\n setRotatingRing(true)\n // Drop the wedge focus so no accent ring lingers on a sector while the ring spins.\n drag.wedge.blur()\n }\n setColorAngleValue(drag.angle)\n },\n [paintBrushSector, sectorCount, setColorAngleValue],\n )\n\n const endRingRotation = useCallback(() => {\n rotationRef.current = null\n setRotatingRing(false)\n }, [])\n\n const focusSector = useCallback((index: number) => {\n setActiveSectorIndex(index)\n wedgeRefs.current[index]?.focus()\n }, [])\n\n const handleWedgeKeyDown = useCallback(\n (event: ReactKeyboardEvent<HTMLButtonElement>, index: number) => {\n if (disabled) return\n // Keyboard brush: holding Shift while arrowing carries the focused wedge's state onto each\n // wedge stepped into (the keyboard analogue of shift+drag) — Space/Enter sets the anchor's\n // state first, then Shift+Arrow paints a run to match it.\n const moveFocus = (next: number) => {\n if (event.shiftKey) setSectorDisabled(next, disabledSet.has(index))\n focusSector(next)\n }\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowDown': {\n event.preventDefault()\n moveFocus((index + 1) % sectorCount)\n break\n }\n case 'ArrowLeft':\n case 'ArrowUp': {\n event.preventDefault()\n moveFocus((index - 1 + sectorCount) % sectorCount)\n break\n }\n case 'Home': {\n event.preventDefault()\n focusSector(0)\n break\n }\n case 'End': {\n event.preventDefault()\n focusSector(sectorCount - 1)\n break\n }\n default: {\n break\n }\n }\n },\n [disabled, disabledSet, focusSector, sectorCount, setSectorDisabled],\n )\n\n const setThresholdClamped = useCallback(\n (next: number) => {\n setThresholdValue(clampUnit(next))\n },\n [setThresholdValue],\n )\n\n const handleThresholdKeyDown = useCallback(\n (event: ReactKeyboardEvent<HTMLInputElement>) => {\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowUp': {\n event.preventDefault()\n setThresholdClamped(thresholdValue + thresholdStep)\n break\n }\n case 'ArrowLeft':\n case 'ArrowDown': {\n event.preventDefault()\n setThresholdClamped(thresholdValue - thresholdStep)\n break\n }\n case 'PageUp': {\n event.preventDefault()\n setThresholdClamped(thresholdValue + thresholdStep * 10)\n break\n }\n case 'PageDown': {\n event.preventDefault()\n setThresholdClamped(thresholdValue - thresholdStep * 10)\n break\n }\n case 'Home': {\n event.preventDefault()\n setThresholdClamped(0)\n break\n }\n case 'End': {\n event.preventDefault()\n setThresholdClamped(1)\n break\n }\n default: {\n break\n }\n }\n },\n [setThresholdClamped, thresholdStep, thresholdValue],\n )\n\n const handleDialPointerDown = useCallback(\n (event: ReactPointerEvent<HTMLDivElement>) => {\n if (disabled) return\n // Suppress the default focus shift (a mousedown on this non-focusable div would otherwise\n // pull focus to the body) so the slider focus below sticks.\n event.preventDefault()\n event.currentTarget.setPointerCapture(event.pointerId)\n setDraggingDial(true)\n // Focus the (hidden) slider so the value is immediately arrow-key adjustable after a click.\n thresholdInputRef.current?.focus()\n // Cache the rect for the drag (the wheel doesn't move mid-drag) to avoid a layout read per move.\n const rect = rootRef.current?.getBoundingClientRect() ?? null\n dialRectRef.current = rect\n if (rect) setThresholdClamped(pointerThreshold(event.clientY, rect))\n },\n [disabled, setThresholdClamped],\n )\n\n const handleDialPointerMove = useCallback(\n (event: ReactPointerEvent<HTMLDivElement>) => {\n const rect = dialRectRef.current\n if (!draggingDial || !rect) return\n setThresholdClamped(pointerThreshold(event.clientY, rect))\n },\n [draggingDial, setThresholdClamped],\n )\n\n const handleRef = useCallback(\n (node: HTMLDivElement | null) => {\n rootRef.current = node\n if (typeof ref === 'function') {\n ref(node)\n } else if (ref) {\n ref.current = node\n }\n },\n [ref],\n )\n\n // Ring + handle in hub-local % (centre 50,50): the ring's diameter grows with the value; the\n // dot sits at its top. Threshold 0 → diameter 0 (dot at the centre); 1 → the track edge.\n // Clamp for rendering: a controlled `threshold` outside [0,1] must not overflow the ring/dot\n // or desync the native input (which clamps) from the announced aria-valuetext.\n const clampedThreshold = clampUnit(thresholdValue)\n const ringDiameterPercent = clampedThreshold * 100 * THRESHOLD_TRACK_FRACTION\n const dotTopPercent = 50 - clampedThreshold * 50 * THRESHOLD_TRACK_FRACTION\n // Hover fill is for the hovered wedge only — never the focused one, which already shows the\n // accent focus ring. Otherwise a just-toggled wedge (focused + hovered) gets fill + ring, which\n // reads as a doubled outline.\n const hoverHighlightIndex = !rotatingRing && hoveredSectorIndex !== null && hoveredSectorIndex !== focusedSectorIndex ? hoveredSectorIndex : null\n // Accent-ringed wedges: during a brush, the whole swept section (so it reads as one active\n // selection); otherwise just the focused wedge.\n const ringIndices = brushing ? brushedIndices : focusedSectorIndex !== null && !rotatingRing ? [focusedSectorIndex] : []\n // The roving-tabindex anchor must stay valid if sectorCount shrinks below it, or the whole\n // wedge group would drop out of the tab order (no button matching a stale index).\n const safeActiveIndex = Math.min(activeSectorIndex, sectorCount - 1)\n const saturation = thresholdToSaturation(clampedThreshold)\n\n // Read-only needle: shown only when a finite bearing is supplied (consumer-driven, e.g. the feed\n // hover cursor). A slim triangle pointing out to the rim at that compass bearing.\n const hasNeedle = typeof bearing === 'number' && Number.isFinite(bearing)\n const needleBearing = hasNeedle ? normalizeBearing(bearing as number) : 0\n const needleTip = polarToCartesian(CENTER, CENTER, NEEDLE_TIP_RADIUS, needleBearing)\n const needleBaseMid = polarToCartesian(CENTER, CENTER, NEEDLE_BASE_RADIUS, needleBearing)\n const needleBaseLeft = polarToCartesian(CENTER, CENTER, NEEDLE_BASE_RADIUS, needleBearing - NEEDLE_HALF_WIDTH_DEGREES)\n const needleBaseRight = polarToCartesian(CENTER, CENTER, NEEDLE_BASE_RADIUS, needleBearing + NEEDLE_HALF_WIDTH_DEGREES)\n\n return (\n <div\n aria-disabled={disabled || undefined}\n aria-label={accessibleName}\n className={cn('relative inline-grid select-none', disabled && 'pointer-events-none opacity-50', className)}\n data-disabled={disabled || undefined}\n data-slot='directional-color-wheel'\n data-testid={dataTestId}\n ref={handleRef}\n role='group'\n style={{ height: size, width: size }}\n >\n {/* Flat coloured sector wedges (the GRAMS look): clipped, toggleable buttons. The\n coherence→saturation preview is one `saturate()` on the whole group (not per wedge), so the\n ring is a single stacking context the overlay can paint over — disabled wedges are grey, so\n the filter is a no-op on them. */}\n <div\n className='col-start-1 row-start-1 grid size-full'\n data-brushing={brushing || undefined}\n data-rotatable={canRotate || undefined}\n data-rotating={rotatingRing || undefined}\n data-slot='directional-color-wheel-sectors'\n data-testid={`${dataTestId}-sectors`}\n style={{ filter: `saturate(${saturation})` }}\n >\n {sectors.map((sector) => {\n const isEnabled = !disabledSet.has(sector.index)\n return (\n <button\n aria-label={`Bearing sector ${Math.round(sector.startBearing)}° to ${Math.round(sector.endBearing)}°`}\n aria-pressed={isEnabled}\n className={cn(\n 'col-start-1 row-start-1 size-full cursor-pointer outline-none motion-safe:transition-opacity motion-safe:duration-150',\n !isEnabled && 'opacity-25',\n canRotate && 'touch-none',\n canRotate && (rotatingRing ? 'cursor-grabbing' : 'cursor-grab'),\n brushing && 'cursor-crosshair',\n )}\n data-sector-id={sector.id}\n data-slot='directional-color-wheel-sector'\n data-testid={`${dataTestId}-sector-${sector.index}`}\n disabled={disabled}\n key={sector.id}\n onBlur={() => {\n setFocusedSectorIndex((current) => (current === sector.index ? null : current))\n }}\n onClick={() => {\n // Swallow the click after a rotation drag so it doesn't also toggle.\n if (suppressClickRef.current) {\n suppressClickRef.current = false\n return\n }\n commitDisabled(sector.index)\n }}\n onFocus={() => {\n setActiveSectorIndex(sector.index)\n setFocusedSectorIndex(sector.index)\n }}\n onKeyDown={(event) => {\n handleWedgeKeyDown(event, sector.index)\n }}\n onLostPointerCapture={() => {\n endRingRotation()\n endBrush()\n }}\n onPointerDown={(event) => {\n handleSectorPointerDown(event, sector.index)\n }}\n onPointerEnter={() => {\n setHoveredSectorIndex(sector.index)\n }}\n onPointerLeave={() => {\n setHoveredSectorIndex((current) => (current === sector.index ? null : current))\n }}\n onPointerMove={handleRingPointerMove}\n onPointerUp={() => {\n endRingRotation()\n endBrush()\n }}\n ref={(node) => {\n wedgeRefs.current[sector.index] = node\n }}\n style={{ background: isEnabled ? sector.color : 'var(--color-level-one)', clipPath: sector.clipPath }}\n tabIndex={disabled || sector.index !== safeActiveIndex ? -1 : 0}\n type='button'\n />\n )\n })}\n </div>\n\n {/* Decorative + interactive overlay, painted above the wedge group (z-10): ring outlines,\n degree bezel, hover/focus highlight, and the compass/degree labels. */}\n <svg\n aria-hidden\n className='pointer-events-none z-10 col-start-1 row-start-1 size-full overflow-visible'\n data-slot='directional-color-wheel-overlay'\n data-testid={`${dataTestId}-overlay`}\n viewBox='0 0 100 100'\n >\n {/* Rings/bezel step up from `border-primary` (neutral-800, nearly invisible) to the mid\n neutrals — `border-secondary` (700) for the rings, `text-disabled` (600) for the minor\n ticks — so the notches read clearly while staying well short of the near-white\n `text-secondary` (300) used for the major ticks + N/E/S/W labels. */}\n <circle\n className='fill-none stroke-border-secondary'\n cx={CENTER}\n cy={CENTER}\n r={OUTER_RADIUS}\n strokeWidth={0.7}\n />\n <circle\n className='fill-none stroke-border-secondary'\n cx={CENTER}\n cy={CENTER}\n r={INNER_RADIUS}\n strokeWidth={0.7}\n />\n <circle\n className='fill-none stroke-border-secondary/60'\n cx={CENTER}\n cy={CENTER}\n r={TICK_OUTER_RADIUS}\n strokeWidth={0.4}\n />\n <g data-testid={`${dataTestId}-degree-ticks`}>\n {DEGREE_TICKS.map((tick) => (\n <line\n className={tick.isMajor ? 'stroke-text-secondary' : 'stroke-text-disabled'}\n key={tick.bearing}\n strokeLinecap='round'\n strokeWidth={tick.isMajor ? 0.8 : 0.5}\n x1={tick.inner.x}\n x2={tick.outer.x}\n y1={tick.inner.y}\n y2={tick.outer.y}\n />\n ))}\n </g>\n {hoverHighlightIndex !== null && sectors[hoverHighlightIndex] ? (\n <path\n className='fill-text-primary/15'\n d={annularSectorPath(CENTER, CENTER, INNER_RADIUS, OUTER_RADIUS, sectors[hoverHighlightIndex].startBearing, sectors[hoverHighlightIndex].endBearing)}\n />\n ) : null}\n {ringIndices.map((ringIndex) =>\n sectors[ringIndex] ? (\n <path\n className='fill-none stroke-accent'\n 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)}\n key={ringIndex}\n strokeLinejoin='round'\n strokeWidth={FOCUS_STROKE_WIDTH}\n />\n ) : null,\n )}\n {BEZEL_LABELS.map((label) => (\n <text\n className={label.isCardinal ? 'font-medium fill-text-secondary text-[6px]' : 'fill-text-secondary text-[4px] tabular-nums'}\n dominantBaseline='central'\n key={label.bearing}\n textAnchor='middle'\n x={label.at.x}\n y={label.at.y}\n >\n {label.text}\n </text>\n ))}\n </svg>\n\n {/* Threshold: a gray hub holding a concentric ring whose radius is the value. Drag the red\n handle dot out (or arrow-key the hidden slider) to grow the ring; it shrinks to a point\n and stops at the centre (0). The track edge is 1. */}\n <div\n className={cn('relative z-20 col-start-1 row-start-1 m-auto flex touch-none items-center justify-center rounded-full border border-border-primary shadow-elevation-2', draggingDial ? 'cursor-grabbing' : 'cursor-grab')}\n data-slot='directional-color-wheel-threshold-dial'\n data-testid={`${dataTestId}-threshold-dial`}\n onLostPointerCapture={() => {\n setDraggingDial(false)\n }}\n onPointerDown={handleDialPointerDown}\n onPointerMove={handleDialPointerMove}\n onPointerUp={() => {\n setDraggingDial(false)\n }}\n style={{ background: 'radial-gradient(circle at 38% 30%, var(--color-level-four), var(--color-level-one) 82%)', height: `${INNER_RADIUS * 1.7}%`, width: `${INNER_RADIUS * 1.7}%` }}\n >\n <span\n aria-hidden\n className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full border border-text-secondary/40'\n data-slot='directional-color-wheel-threshold-ring'\n style={{ height: `${ringDiameterPercent}%`, width: `${ringDiameterPercent}%` }}\n />\n <span\n aria-hidden\n className='absolute left-1/2 size-[15%] -translate-x-1/2 -translate-y-1/2 rounded-full border border-border-primary bg-danger-400 shadow-elevation-1'\n data-slot='directional-color-wheel-threshold-indicator'\n data-testid={`${dataTestId}-threshold-indicator`}\n style={{ top: `${dotTopPercent}%` }}\n />\n <input\n aria-label='Coherence threshold'\n aria-valuetext={`${Math.round(clampedThreshold * 100)} percent`}\n className='sr-only'\n data-slot='directional-color-wheel-threshold-input'\n data-testid={`${dataTestId}-threshold`}\n disabled={disabled}\n max={1}\n min={0}\n onBlur={() => {\n setDialFocused(false)\n }}\n onChange={(event) => {\n setThresholdClamped(Number(event.target.value))\n }}\n onFocus={() => {\n setDialFocused(true)\n }}\n onKeyDown={handleThresholdKeyDown}\n ref={thresholdInputRef}\n step={thresholdStep}\n tabIndex={disabled ? -1 : 0}\n type='range'\n value={clampedThreshold}\n />\n </div>\n\n {/* Top overlay above the hub (z-30): the threshold focus ring (an SVG circle matching the\n sector focus ring's stroke width/units exactly) and the read-only bearing needle. */}\n {dialFocused || hasNeedle ? (\n <svg\n aria-hidden\n className='pointer-events-none z-30 col-start-1 row-start-1 size-full overflow-visible'\n viewBox='0 0 100 100'\n >\n {hasNeedle ? (\n <defs>\n <filter\n filterUnits='userSpaceOnUse'\n height='100'\n id={needleShadowId}\n width='100'\n x='0'\n y='0'\n >\n <feDropShadow\n dx='0'\n dy='0.3'\n floodColor='var(--color-level-one)'\n floodOpacity='0.55'\n stdDeviation='0.5'\n />\n </filter>\n </defs>\n ) : null}\n {dialFocused ? (\n <circle\n className='fill-none stroke-accent'\n cx={CENTER}\n cy={CENTER}\n r={THRESHOLD_FOCUS_RADIUS}\n strokeWidth={FOCUS_STROKE_WIDTH}\n />\n ) : null}\n {hasNeedle ? (\n <g\n data-testid={`${dataTestId}-needle`}\n filter={`url(#${needleShadowId})`}\n >\n {/* Read-only bearing needle (consumer-driven; HZN-2658 follows the cursor). Two facets\n split along the centreline give it a lit/shadowed compass look; the drop-shadow\n lifts it off any wedge colour. Decorative — the consumer surfaces the numeric\n bearing in the hover label. */}\n <polygon\n className='fill-danger-500'\n points={`${needleTip.x},${needleTip.y} ${needleBaseLeft.x},${needleBaseLeft.y} ${needleBaseMid.x},${needleBaseMid.y}`}\n />\n <polygon\n className='fill-danger-400'\n points={`${needleTip.x},${needleTip.y} ${needleBaseMid.x},${needleBaseMid.y} ${needleBaseRight.x},${needleBaseRight.y}`}\n />\n </g>\n ) : null}\n </svg>\n ) : null}\n\n {/* The needle SVG is decorative (aria-hidden), so expose the bearing as text for assistive tech. */}\n {hasNeedle ? (\n <span\n className='sr-only'\n data-testid={`${dataTestId}-bearing-value`}\n >\n Bearing {Math.round(needleBearing)} degrees\n </span>\n ) : null}\n </div>\n )\n}\n\nDirectionalColorWheel.displayName = 'DirectionalColorWheel'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DirectionalColorWheel.js","names":[],"sources":["../src/components/DirectionalColorWheel/DirectionalColorWheel.tsx"],"sourcesContent":["import { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useId, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type Ref } from 'react'\nimport { annularSectorPath, normalizeBearing, polarToCartesian, resolveSectorColor, sectorBearingRange, sectorClipPath, sectorId, thresholdToSaturation, type DirectionalColorStop, type DirectionalSectorCount } from './directionalColorWheelMath'\n\nexport interface DirectionalColorWheelProps {\n /** Accessible name for the wheel group. */\n accessibleName?: string\n /**\n * Read-only bearing (degrees) the needle points at, using the compositor's `atan2(sine, cosine)`\n * convention (0° = N, clockwise). The consumer drives it (e.g. from the feed hover cursor) so the\n * needle follows the cursor; the wheel only displays it — there is no drag interaction.\n * `null`/`undefined` hides the needle — keep it nullable so Horizon can gate the feature flag with\n * a single `null` (PAT-028).\n */\n bearing?: number | null\n /** Layout-only class extension (margins/padding). */\n className?: string\n /**\n * \"Color Angle\": degrees the colours are rotated clockwise around the ring. Controlled+uncontrolled\n * (pair it with `defaultColorAngle` / `onColorAngleChange`). The colours can also be spun directly by\n * dragging the colour ring — that gesture reports through `onColorAngleChange` and is only active when\n * the value can take effect (uncontrolled, or controlled *with* an `onColorAngleChange` handler). Sector\n * geometry and the N/E/S/W bezel stay fixed to compass bearings; only the colours rotate.\n */\n colorAngle?: number\n /**\n * Advanced parity hook: resolver mapping a bearing in `[0, 360)` to a CSS colour. When\n * provided it wins over `colorStops`; pass the same resolver the feed colours by so the\n * legend samples the identical colours.\n */\n colorForBearing?: ((bearingDegrees: number) => string) | null\n /** Palette stops (position `0..1`). Defaults to the directional DIFAR palette. */\n colorStops?: DirectionalColorStop[] | null\n dataTestId?: string\n /** Uncontrolled initial Color Angle (degrees). */\n defaultColorAngle?: number\n /** Uncontrolled initial set of disabled (hidden) sector indices. */\n defaultDisabledSectors?: number[]\n /** Uncontrolled initial threshold. */\n defaultThreshold?: number\n disabled?: boolean\n /** Controlled set of disabled (hidden) sector indices, in `[0, sectorCount)`. */\n disabledSectors?: number[] | null\n /** Called with the next Color Angle (degrees, `[0, 360)`) as the colour ring is dragged. */\n onColorAngleChange?: (colorAngle: number) => void\n /** Called with the next set of disabled sector indices whenever a wedge is toggled. */\n onDisabledSectorsChange?: (disabledSectors: number[]) => void\n /** Called when a single wedge is toggled: its index and whether it is now disabled. */\n onSectorToggle?: (sectorIndex: number, nextDisabled: boolean) => void\n /** Called with the threshold (`0..1`) as the centre dial changes. */\n onThresholdChange?: (threshold: number) => void\n ref?: Ref<HTMLDivElement>\n /** Number of toggleable wedges. */\n sectorCount?: DirectionalSectorCount\n /** Diameter in pixels. */\n size?: number\n /**\n * Coherence threshold (`0..1`). Coherence is how reliable the bearing estimate is; this dial\n * maps it to saturation — higher threshold desaturates the legend. The component only reports\n * the value via `onThresholdChange`; the consumer decides what it drives.\n */\n threshold?: number\n /** Keyboard step for the threshold dial. */\n thresholdStep?: number\n}\n\n// viewBox is a 0..100 square; every radius is a fraction of the wheel. The ring sits well\n// inside the box so the N/E/S/W labels have clear breathing room outside it.\nconst CENTER = 50\nconst OUTER_RADIUS = 38\nconst INNER_RADIUS = 22\nconst LABEL_RADIUS = 47\n// The focus ring sits just inside the wedge with a minimal gap (~1-2px at typical sizes) so the\n// stroke never grazes the shared wedge edge or bleeds onto the neighbour (which left a focus remnant\n// when a wedge was re-enabled), while staying tight enough to read as that wedge's outline.\nconst FOCUS_RADIAL_INSET = 0.7\nconst FOCUS_ANGULAR_INSET = 1.1\n// The threshold dial's focus ring is an SVG circle just outside the hub, stroked with the same\n// width and viewBox units as the sector focus ring, so the two are identical at any `size`.\nconst THRESHOLD_FOCUS_RADIUS = (INNER_RADIUS * 1.7) / 2 + 0.8\nconst FOCUS_STROKE_WIDTH = 0.5\n// Read-only needle: a slim lance across the colour ring whose tip points at the bearing. It's\n// split down its centreline into a lighter and darker red facet (a compass-needle highlight) and\n// lifted off the ring with a soft drop-shadow rather than a hard outline.\nconst NEEDLE_TIP_RADIUS = 39\nconst NEEDLE_BASE_RADIUS = INNER_RADIUS\nconst NEEDLE_HALF_WIDTH_DEGREES = 3\n// Watch-style degree bezel just outside the colour ring: a tick every TICK_STEP_DEGREES, longer\n// at each 30° (which line up with the N/E/S/W labels). Labels sit further out (LABEL_RADIUS) so\n// the ticks never crowd them.\nconst TICK_OUTER_RADIUS = 43\nconst TICK_INNER_MAJOR = 39\nconst TICK_INNER_MINOR = 41\nconst TICK_STEP_DEGREES = 10\n// The threshold is shown as a concentric ring inside the hub: radius 0 (a point at the centre)\n// at threshold 0, growing to this fraction of the hub radius at threshold 1.\nconst THRESHOLD_TRACK_FRACTION = 0.82\n// 0° = North (top), increasing clockwise — the compass convention the colours also use.\nconst CARDINAL_LABELS: Record<number, string> = { 0: 'N', 90: 'E', 180: 'S', 270: 'W' }\n// Precomputed once (geometry only): the degree-bezel tick lines, longer at each 30°.\nconst DEGREE_TICKS = Array.from({ length: 360 / TICK_STEP_DEGREES }, (_, index) => {\n const bearing = index * TICK_STEP_DEGREES\n const isMajor = bearing % 30 === 0\n return {\n bearing,\n inner: polarToCartesian(CENTER, CENTER, isMajor ? TICK_INNER_MAJOR : TICK_INNER_MINOR, bearing),\n isMajor,\n outer: polarToCartesian(CENTER, CENTER, TICK_OUTER_RADIUS, bearing),\n }\n})\n// One label per 30° major tick: the cardinal letter (N/E/S/W) where there is one, otherwise the\n// bearing in degrees. Sits at LABEL_RADIUS, outside the tick bezel.\nconst BEZEL_LABELS = Array.from({ length: 12 }, (_, index) => {\n const bearing = index * 30\n const cardinal = CARDINAL_LABELS[bearing]\n return {\n at: polarToCartesian(CENTER, CENTER, LABEL_RADIUS, bearing),\n bearing,\n isCardinal: cardinal !== undefined,\n text: cardinal ?? String(bearing),\n }\n})\n\nconst clampUnit = (value: number): number => Math.max(0, Math.min(1, value))\n\n// Threshold from how far the pointer sits *above* the hub centre, as a fraction of the ring's\n// max radius: 0 at/below the centre, 1 at the top of the track. Signed-and-clamped (not radial\n// distance) so the ring shrinks to a point and stops dead at the centre instead of bouncing\n// back out the other side. `setThresholdClamped` floors the negative (below-centre) values at 0.\nconst pointerThreshold = (clientY: number, rect: DOMRect): number => {\n const deltaY = clientY - (rect.top + rect.height / 2)\n const maxRadiusPx = ((INNER_RADIUS * 1.7) / 100 / 2) * rect.width * THRESHOLD_TRACK_FRACTION\n return -deltaY / maxRadiusPx\n}\n\n// Drag distance (px) below which a press is a sector tap, not a ring rotation.\nconst ROTATE_SLOP_PX = 4\n\n// Pointer's compass bearing about the wheel centre, matching `polarToCartesian` (atan2(dx, −dy)).\nconst pointerBearing = (clientX: number, clientY: number, rect: DOMRect): number => {\n const deltaX = clientX - (rect.left + rect.width / 2)\n const deltaY = clientY - (rect.top + rect.height / 2)\n return normalizeBearing((Math.atan2(deltaX, -deltaY) * 180) / Math.PI)\n}\n\n// Which sector a compass bearing falls in, for `count` equal sectors starting at 0° (matches `sectorBearingRange`).\nconst sectorIndexForBearing = (bearing: number, count: number): number => Math.floor(normalizeBearing(bearing) / (360 / count)) % count\n\nexport const DirectionalColorWheel = ({\n accessibleName = 'Directional bearing color wheel',\n bearing,\n className,\n colorAngle,\n colorForBearing,\n colorStops,\n dataTestId = 'spectral-directional-color-wheel',\n defaultColorAngle,\n defaultDisabledSectors,\n defaultThreshold = 0,\n disabled = false,\n disabledSectors,\n onColorAngleChange,\n onDisabledSectorsChange,\n onSectorToggle,\n onThresholdChange,\n ref,\n sectorCount = 12,\n size = 240,\n threshold,\n thresholdStep = 0.05,\n}: DirectionalColorWheelProps) => {\n const rootRef = useRef<HTMLDivElement>(null)\n const wedgeRefs = useRef<(HTMLButtonElement | null)[]>([])\n const thresholdInputRef = useRef<HTMLInputElement>(null)\n const dialRectRef = useRef<DOMRect | null>(null)\n const needleShadowId = `directional-needle-shadow-${useId().replaceAll(':', '')}`\n\n const [thresholdValue, setThresholdValue] = useUncontrolledState<number>({ defaultValue: clampUnit(defaultThreshold), onChange: onThresholdChange, value: threshold })\n const [disabledValue, setDisabledValue] = useUncontrolledState<number[]>({ defaultValue: defaultDisabledSectors ?? [], onChange: onDisabledSectorsChange, value: disabledSectors ?? undefined })\n const [colorAngleValue, setColorAngleValue] = useUncontrolledState<number>({ defaultValue: defaultColorAngle ?? 0, onChange: onColorAngleChange, value: colorAngle })\n\n const [activeSectorIndex, setActiveSectorIndex] = useState(0)\n const [focusedSectorIndex, setFocusedSectorIndex] = useState<number | null>(null)\n const [hoveredSectorIndex, setHoveredSectorIndex] = useState<number | null>(null)\n const [draggingDial, setDraggingDial] = useState(false)\n const [dialFocused, setDialFocused] = useState(false)\n const [rotatingRing, setRotatingRing] = useState(false)\n const [brushing, setBrushing] = useState(false)\n // The sectors swept by the in-progress brush stroke — accent-ringed as one active selection.\n const [brushedIndices, setBrushedIndices] = useState<number[]>([])\n\n // Live rotation drag (a ref so rapid moves don't hinge on re-render timing).\n const rotationRef = useRef<{ angle: number; lastBearing: number; moved: boolean; pointerId: number; rect: DOMRect; startX: number; startY: number; wedge: HTMLButtonElement } | null>(null)\n // Live shift+drag brush: paint every wedge the pointer crosses to one target state. The working\n // disabled set + the set swept so far live in a ref so the stroke stays self-consistent even before\n // state echoes back; the cached rect + last bearing let each move fill in every sector swept since\n // the previous sample (so a fast drag never skips a wedge — the gesture is geometric, not per-wedge\n // `pointerenter`).\n const brushRef = useRef<{ brushed: Set<number>; disabled: Set<number>; lastBearing: number; pointerId: number; rect: DOMRect; targetDisabled: boolean } | null>(null)\n const suppressClickRef = useRef(false)\n\n // Dragging is enabled only when the value can take effect (uncontrolled, or controlled with a handler).\n const canRotate = !disabled && (colorAngle === undefined || onColorAngleChange !== undefined)\n\n const disabledSet = useMemo(() => new Set(disabledValue.filter((index) => index >= 0 && index < sectorCount)), [disabledValue, sectorCount])\n\n // Flat GRAMS-style wedges, palette sampled at each wedge centre; no divider stroke → seamless ring.\n // Geometry-only, so memoised (not rebuilt on every threshold/hover/focus render).\n const sectors = useMemo(\n () =>\n Array.from({ length: sectorCount }, (_, index) => {\n const range = sectorBearingRange(index, sectorCount)\n return {\n ...range,\n clipPath: sectorClipPath(INNER_RADIUS, OUTER_RADIUS, range.startBearing, range.endBearing),\n color: resolveSectorColor({ colorForBearing, colorStops }, (range.startBearing + range.endBearing) / 2, colorAngleValue),\n id: sectorId(index, sectorCount),\n index,\n }\n }),\n [colorAngleValue, colorForBearing, colorStops, sectorCount],\n )\n\n const commitDisabled = useCallback(\n (index: number) => {\n if (disabled) return\n const next = disabledSet.has(index) ? disabledValue.filter((value) => value !== index) : [...disabledValue.filter((value) => value >= 0 && value < sectorCount), index].sort((first, second) => first - second)\n setDisabledValue(next)\n onSectorToggle?.(index, !disabledSet.has(index))\n },\n [disabled, disabledSet, disabledValue, onSectorToggle, sectorCount, setDisabledValue],\n )\n\n // Write the next disabled-sector set out through the controlled/uncontrolled channel.\n const commitDisabledSet = useCallback(\n (next: Set<number>) => {\n setDisabledValue([...next].filter((value) => value >= 0 && value < sectorCount).sort((first, second) => first - second))\n },\n [sectorCount, setDisabledValue],\n )\n\n // Set a single sector's disabled state (idempotent — only commits/reports a real change). Backs\n // both the keyboard Shift+Arrow brush and the pointer brush.\n const setSectorDisabled = useCallback(\n (index: number, nextDisabled: boolean) => {\n if (disabled || disabledSet.has(index) === nextDisabled) return\n const next = new Set(disabledSet)\n if (nextDisabled) next.add(index)\n else next.delete(index)\n commitDisabledSet(next)\n onSectorToggle?.(index, nextDisabled)\n },\n [commitDisabledSet, disabled, disabledSet, onSectorToggle],\n )\n\n // Paint a single sector to the active brush's target state. Marks it as swept (for the section\n // highlight) regardless, but only commits/reports when its state actually changes.\n const paintBrushSector = useCallback(\n (brush: NonNullable<typeof brushRef.current>, index: number) => {\n brush.brushed.add(index)\n if (brush.disabled.has(index) === brush.targetDisabled) return\n if (brush.targetDisabled) brush.disabled.add(index)\n else brush.disabled.delete(index)\n commitDisabledSet(brush.disabled)\n onSectorToggle?.(index, brush.targetDisabled)\n },\n [commitDisabledSet, onSectorToggle],\n )\n\n // Shift+drag brush: the first wedge toggles, and every wedge the pointer sweeps over is painted to\n // *that* wedge's new state (one consistent direction), so a single stroke hides/shows a run of\n // sectors. The pointer is captured and each move fills in every sector between the last sample and\n // the current bearing — geometric, so a fast drag never skips a wedge.\n const startBrush = useCallback(\n (event: ReactPointerEvent<HTMLButtonElement>, index: number, rect: DOMRect) => {\n if (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 targetDisabled = !disabledSet.has(index)\n const working = new Set(disabledSet)\n if (targetDisabled) working.add(index)\n else working.delete(index)\n brushRef.current = { brushed: new Set([index]), disabled: working, lastBearing: pointerBearing(event.clientX, event.clientY, rect), pointerId: event.pointerId, rect, targetDisabled }\n // Swallow the trailing click so a shift-click that paints one wedge isn't toggled straight back.\n suppressClickRef.current = true\n setBrushing(true)\n setBrushedIndices([index])\n commitDisabledSet(working)\n onSectorToggle?.(index, targetDisabled)\n },\n [commitDisabledSet, disabled, disabledSet, onSectorToggle],\n )\n\n const endBrush = useCallback(() => {\n if (brushRef.current === null) return\n brushRef.current = null\n setBrushing(false)\n // Drop the stroke's section highlight + the focus accent on the start wedge, so once the stroke\n // is done the dimming (uniform across the run) is the only mark — no single wedge singled out.\n setBrushedIndices([])\n setFocusedSectorIndex(null)\n }, [])\n\n const handleSectorPointerDown = useCallback(\n (event: ReactPointerEvent<HTMLButtonElement>, index: number) => {\n // Reset so a prior drag/brush never swallows this interaction's click.\n suppressClickRef.current = false\n const rect = rootRef.current?.getBoundingClientRect()\n if (!rect) return\n // Shift+drag paints sectors (multi-sector brush) instead of spinning the colour ring.\n if (event.shiftKey) {\n startBrush(event, index, rect)\n return\n }\n if (!canRotate) 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 rotationRef.current = { angle: colorAngleValue, lastBearing: pointerBearing(event.clientX, event.clientY, rect), moved: false, pointerId: event.pointerId, rect, startX: event.clientX, startY: event.clientY, wedge: event.currentTarget }\n },\n [canRotate, colorAngleValue, startBrush],\n )\n\n const handleRingPointerMove = useCallback(\n (event: ReactPointerEvent<HTMLButtonElement>) => {\n const brush = brushRef.current\n if (brush !== null) {\n if (brush.pointerId !== event.pointerId) return\n const bearingNow = pointerBearing(event.clientX, event.clientY, brush.rect)\n // The gesture is locked to \"brush\" at pointer-down (Shift was held then); the whole drag\n // paints, so we don't re-check `event.shiftKey` per move — releasing Shift mid-drag would\n // otherwise silently stop the stroke after the first wedge.\n // Step from the last sampled sector to the current one in the direction of travel, painting\n // each — so even a sample that jumps several sectors fills every wedge in between.\n let delta = bearingNow - brush.lastBearing\n if (delta > 180) delta -= 360\n if (delta < -180) delta += 360\n const step = delta >= 0 ? 1 : -1\n const toIndex = sectorIndexForBearing(bearingNow, sectorCount)\n let index = sectorIndexForBearing(brush.lastBearing, sectorCount)\n while (index !== toIndex) {\n index = (index + step + sectorCount) % sectorCount\n paintBrushSector(brush, index)\n }\n brush.lastBearing = bearingNow\n setBrushedIndices([...brush.brushed])\n return\n }\n const drag = rotationRef.current\n if (drag === null || drag.pointerId !== event.pointerId) return\n // Cached rect (the wheel doesn't move mid-drag) avoids a layout read per move.\n const bearingNow = pointerBearing(event.clientX, event.clientY, drag.rect)\n // Signed, wrap-safe step from the last sample (supports multi-turn spins).\n let step = bearingNow - drag.lastBearing\n if (step > 180) step -= 360\n if (step < -180) step += 360\n drag.lastBearing = bearingNow\n drag.angle = normalizeBearing(drag.angle + step)\n if (!drag.moved) {\n if (Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) <= ROTATE_SLOP_PX) return\n drag.moved = true\n suppressClickRef.current = true\n setRotatingRing(true)\n // Drop the wedge focus so no accent ring lingers on a sector while the ring spins.\n drag.wedge.blur()\n }\n setColorAngleValue(drag.angle)\n },\n [paintBrushSector, sectorCount, setColorAngleValue],\n )\n\n const endRingRotation = useCallback(() => {\n rotationRef.current = null\n setRotatingRing(false)\n }, [])\n\n const focusSector = useCallback((index: number) => {\n setActiveSectorIndex(index)\n wedgeRefs.current[index]?.focus()\n }, [])\n\n const handleWedgeKeyDown = useCallback(\n (event: ReactKeyboardEvent<HTMLButtonElement>, index: number) => {\n if (disabled) return\n // Keyboard brush: holding Shift while arrowing carries the focused wedge's state onto each\n // wedge stepped into (the keyboard analogue of shift+drag) — Space/Enter sets the anchor's\n // state first, then Shift+Arrow paints a run to match it.\n const moveFocus = (next: number) => {\n if (event.shiftKey) setSectorDisabled(next, disabledSet.has(index))\n focusSector(next)\n }\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowDown': {\n event.preventDefault()\n moveFocus((index + 1) % sectorCount)\n break\n }\n case 'ArrowLeft':\n case 'ArrowUp': {\n event.preventDefault()\n moveFocus((index - 1 + sectorCount) % sectorCount)\n break\n }\n case 'Home': {\n event.preventDefault()\n focusSector(0)\n break\n }\n case 'End': {\n event.preventDefault()\n focusSector(sectorCount - 1)\n break\n }\n default: {\n break\n }\n }\n },\n [disabled, disabledSet, focusSector, sectorCount, setSectorDisabled],\n )\n\n const setThresholdClamped = useCallback(\n (next: number) => {\n setThresholdValue(clampUnit(next))\n },\n [setThresholdValue],\n )\n\n const handleThresholdKeyDown = useCallback(\n (event: ReactKeyboardEvent<HTMLInputElement>) => {\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowUp': {\n event.preventDefault()\n setThresholdClamped(thresholdValue + thresholdStep)\n break\n }\n case 'ArrowLeft':\n case 'ArrowDown': {\n event.preventDefault()\n setThresholdClamped(thresholdValue - thresholdStep)\n break\n }\n case 'PageUp': {\n event.preventDefault()\n setThresholdClamped(thresholdValue + thresholdStep * 10)\n break\n }\n case 'PageDown': {\n event.preventDefault()\n setThresholdClamped(thresholdValue - thresholdStep * 10)\n break\n }\n case 'Home': {\n event.preventDefault()\n setThresholdClamped(0)\n break\n }\n case 'End': {\n event.preventDefault()\n setThresholdClamped(1)\n break\n }\n default: {\n break\n }\n }\n },\n [setThresholdClamped, thresholdStep, thresholdValue],\n )\n\n const handleDialPointerDown = useCallback(\n (event: ReactPointerEvent<HTMLDivElement>) => {\n if (disabled) return\n // Suppress the default focus shift (a mousedown on this non-focusable div would otherwise\n // pull focus to the body) so the slider focus below sticks.\n event.preventDefault()\n event.currentTarget.setPointerCapture(event.pointerId)\n setDraggingDial(true)\n // Focus the (hidden) slider so the value is immediately arrow-key adjustable after a click.\n thresholdInputRef.current?.focus()\n // Cache the rect for the drag (the wheel doesn't move mid-drag) to avoid a layout read per move.\n const rect = rootRef.current?.getBoundingClientRect() ?? null\n dialRectRef.current = rect\n if (rect) setThresholdClamped(pointerThreshold(event.clientY, rect))\n },\n [disabled, setThresholdClamped],\n )\n\n const handleDialPointerMove = useCallback(\n (event: ReactPointerEvent<HTMLDivElement>) => {\n const rect = dialRectRef.current\n if (!draggingDial || !rect) return\n setThresholdClamped(pointerThreshold(event.clientY, rect))\n },\n [draggingDial, setThresholdClamped],\n )\n\n const handleRef = useCallback(\n (node: HTMLDivElement | null) => {\n rootRef.current = node\n if (typeof ref === 'function') {\n ref(node)\n } else if (ref) {\n ref.current = node\n }\n },\n [ref],\n )\n\n // Ring + handle in hub-local % (centre 50,50): the ring's diameter grows with the value; the\n // dot sits at its top. Threshold 0 → diameter 0 (dot at the centre); 1 → the track edge.\n // Clamp for rendering: a controlled `threshold` outside [0,1] must not overflow the ring/dot\n // or desync the native input (which clamps) from the announced aria-valuetext.\n const clampedThreshold = clampUnit(thresholdValue)\n const ringDiameterPercent = clampedThreshold * 100 * THRESHOLD_TRACK_FRACTION\n const dotTopPercent = 50 - clampedThreshold * 50 * THRESHOLD_TRACK_FRACTION\n // Hover fill is for the hovered wedge only — never the focused one, which already shows the\n // accent focus ring. Otherwise a just-toggled wedge (focused + hovered) gets fill + ring, which\n // reads as a doubled outline.\n const hoverHighlightIndex = !rotatingRing && hoveredSectorIndex !== null && hoveredSectorIndex !== focusedSectorIndex ? hoveredSectorIndex : null\n // Accent-ringed wedges: during a brush, the whole swept section (so it reads as one active\n // selection); otherwise just the focused wedge.\n const ringIndices = brushing ? brushedIndices : focusedSectorIndex !== null && !rotatingRing ? [focusedSectorIndex] : []\n // The roving-tabindex anchor must stay valid if sectorCount shrinks below it, or the whole\n // wedge group would drop out of the tab order (no button matching a stale index).\n const safeActiveIndex = Math.min(activeSectorIndex, sectorCount - 1)\n const saturation = thresholdToSaturation(clampedThreshold)\n\n // Read-only needle: shown only when a finite bearing is supplied (consumer-driven, e.g. the feed\n // hover cursor). A slim triangle pointing out to the rim at that compass bearing.\n const hasNeedle = typeof bearing === 'number' && Number.isFinite(bearing)\n const needleBearing = hasNeedle ? normalizeBearing(bearing as number) : 0\n const needleTip = polarToCartesian(CENTER, CENTER, NEEDLE_TIP_RADIUS, needleBearing)\n const needleBaseMid = polarToCartesian(CENTER, CENTER, NEEDLE_BASE_RADIUS, needleBearing)\n const needleBaseLeft = polarToCartesian(CENTER, CENTER, NEEDLE_BASE_RADIUS, needleBearing - NEEDLE_HALF_WIDTH_DEGREES)\n const needleBaseRight = polarToCartesian(CENTER, CENTER, NEEDLE_BASE_RADIUS, needleBearing + NEEDLE_HALF_WIDTH_DEGREES)\n\n return (\n <div\n aria-disabled={disabled || undefined}\n aria-label={accessibleName}\n className={cn('relative inline-grid select-none', disabled && 'pointer-events-none opacity-50', className)}\n data-disabled={disabled || undefined}\n data-slot='directional-color-wheel'\n data-testid={dataTestId}\n ref={handleRef}\n role='group'\n style={{ height: size, width: size }}\n >\n {/* Flat coloured sector wedges (the GRAMS look): clipped, toggleable buttons. The\n coherence→saturation preview is one `saturate()` on the whole group (not per wedge), so the\n ring is a single stacking context the overlay can paint over — disabled wedges are grey, so\n the filter is a no-op on them. */}\n <div\n className='col-start-1 row-start-1 grid size-full'\n data-brushing={brushing || undefined}\n data-rotatable={canRotate || undefined}\n data-rotating={rotatingRing || undefined}\n data-slot='directional-color-wheel-sectors'\n data-testid={`${dataTestId}-sectors`}\n style={{ filter: `saturate(${saturation})` }}\n >\n {sectors.map((sector) => {\n const isEnabled = !disabledSet.has(sector.index)\n return (\n <button\n aria-label={`Bearing sector ${Math.round(sector.startBearing)}° to ${Math.round(sector.endBearing)}°`}\n aria-pressed={isEnabled}\n className={cn(\n 'col-start-1 row-start-1 size-full cursor-pointer outline-none motion-safe:transition-opacity motion-safe:duration-150',\n !isEnabled && 'opacity-25',\n canRotate && 'touch-none',\n canRotate && (rotatingRing ? 'cursor-grabbing' : 'cursor-grab'),\n brushing && 'cursor-crosshair',\n )}\n data-sector-id={sector.id}\n data-slot='directional-color-wheel-sector'\n data-testid={`${dataTestId}-sector-${sector.index}`}\n disabled={disabled}\n key={sector.id}\n onBlur={() => {\n setFocusedSectorIndex((current) => (current === sector.index ? null : current))\n }}\n onClick={() => {\n // Swallow the click after a rotation drag so it doesn't also toggle.\n if (suppressClickRef.current) {\n suppressClickRef.current = false\n return\n }\n commitDisabled(sector.index)\n }}\n onFocus={() => {\n setActiveSectorIndex(sector.index)\n setFocusedSectorIndex(sector.index)\n }}\n onKeyDown={(event) => {\n handleWedgeKeyDown(event, sector.index)\n }}\n onLostPointerCapture={() => {\n endRingRotation()\n endBrush()\n }}\n onPointerDown={(event) => {\n handleSectorPointerDown(event, sector.index)\n }}\n onPointerEnter={() => {\n setHoveredSectorIndex(sector.index)\n }}\n onPointerLeave={() => {\n setHoveredSectorIndex((current) => (current === sector.index ? null : current))\n }}\n onPointerMove={handleRingPointerMove}\n onPointerUp={() => {\n endRingRotation()\n endBrush()\n }}\n ref={(node) => {\n wedgeRefs.current[sector.index] = node\n }}\n style={{ background: isEnabled ? sector.color : 'var(--color-level-one)', clipPath: sector.clipPath }}\n tabIndex={disabled || sector.index !== safeActiveIndex ? -1 : 0}\n type='button'\n />\n )\n })}\n </div>\n\n {/* Decorative + interactive overlay, painted above the wedge group (z-10): ring outlines,\n degree bezel, hover/focus highlight, and the compass/degree labels. */}\n <svg\n aria-hidden\n className='pointer-events-none z-10 col-start-1 row-start-1 size-full overflow-visible'\n data-slot='directional-color-wheel-overlay'\n data-testid={`${dataTestId}-overlay`}\n viewBox='0 0 100 100'\n >\n {/* Rings/bezel step up from `border-primary` (neutral-800, nearly invisible) to the mid\n neutrals — `border-secondary` (700) for the rings, `text-disabled` (600) for the minor\n ticks — so the notches read clearly while staying well short of the near-white\n `text-secondary` (300) used for the major ticks + N/E/S/W labels. */}\n <circle\n className='fill-none stroke-border-secondary'\n cx={CENTER}\n cy={CENTER}\n r={OUTER_RADIUS}\n strokeWidth={0.7}\n />\n <circle\n className='fill-none stroke-border-secondary'\n cx={CENTER}\n cy={CENTER}\n r={INNER_RADIUS}\n strokeWidth={0.7}\n />\n <circle\n className='fill-none stroke-border-secondary/60'\n cx={CENTER}\n cy={CENTER}\n r={TICK_OUTER_RADIUS}\n strokeWidth={0.4}\n />\n <g data-testid={`${dataTestId}-degree-ticks`}>\n {DEGREE_TICKS.map((tick) => (\n <line\n className={tick.isMajor ? 'stroke-text-secondary' : 'stroke-text-disabled'}\n key={tick.bearing}\n strokeLinecap='round'\n strokeWidth={tick.isMajor ? 0.8 : 0.5}\n x1={tick.inner.x}\n x2={tick.outer.x}\n y1={tick.inner.y}\n y2={tick.outer.y}\n />\n ))}\n </g>\n {hoverHighlightIndex !== null && sectors[hoverHighlightIndex] ? (\n <path\n className='fill-text-primary/15'\n d={annularSectorPath(CENTER, CENTER, INNER_RADIUS, OUTER_RADIUS, sectors[hoverHighlightIndex].startBearing, sectors[hoverHighlightIndex].endBearing)}\n />\n ) : null}\n {ringIndices.map((ringIndex) =>\n sectors[ringIndex] ? (\n <path\n className='fill-none stroke-accent'\n 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)}\n key={ringIndex}\n strokeLinejoin='round'\n strokeWidth={FOCUS_STROKE_WIDTH}\n />\n ) : null,\n )}\n {BEZEL_LABELS.map((label) => (\n <text\n className={label.isCardinal ? 'font-medium fill-text-secondary text-[6px]' : 'fill-text-secondary text-[4px] tabular-nums'}\n dominantBaseline='central'\n key={label.bearing}\n textAnchor='middle'\n x={label.at.x}\n y={label.at.y}\n >\n {label.text}\n </text>\n ))}\n </svg>\n\n {/* Threshold: a gray hub holding a concentric ring whose radius is the value. Drag the red\n handle dot out (or arrow-key the hidden slider) to grow the ring; it shrinks to a point\n and stops at the centre (0). The track edge is 1. */}\n <div\n className={cn('relative z-20 col-start-1 row-start-1 m-auto flex touch-none items-center justify-center rounded-full border border-border-primary shadow-elevation-2', draggingDial ? 'cursor-grabbing' : 'cursor-grab')}\n data-slot='directional-color-wheel-threshold-dial'\n data-testid={`${dataTestId}-threshold-dial`}\n onLostPointerCapture={() => {\n setDraggingDial(false)\n }}\n onPointerDown={handleDialPointerDown}\n onPointerMove={handleDialPointerMove}\n onPointerUp={() => {\n setDraggingDial(false)\n }}\n style={{ background: 'radial-gradient(circle at 38% 30%, var(--color-level-four), var(--color-level-one) 82%)', height: `${INNER_RADIUS * 1.7}%`, width: `${INNER_RADIUS * 1.7}%` }}\n >\n <span\n aria-hidden\n className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full border border-text-secondary/40'\n data-slot='directional-color-wheel-threshold-ring'\n style={{ height: `${ringDiameterPercent}%`, width: `${ringDiameterPercent}%` }}\n />\n <span\n aria-hidden\n className='absolute left-1/2 size-[15%] -translate-x-1/2 -translate-y-1/2 rounded-full border border-border-primary bg-danger-400 shadow-elevation-1'\n data-slot='directional-color-wheel-threshold-indicator'\n data-testid={`${dataTestId}-threshold-indicator`}\n style={{ top: `${dotTopPercent}%` }}\n />\n <input\n aria-label='Coherence threshold'\n aria-valuetext={`${Math.round(clampedThreshold * 100)} percent`}\n className='sr-only'\n data-slot='directional-color-wheel-threshold-input'\n data-testid={`${dataTestId}-threshold`}\n disabled={disabled}\n max={1}\n min={0}\n onBlur={() => {\n setDialFocused(false)\n }}\n onChange={(event) => {\n setThresholdClamped(Number(event.target.value))\n }}\n onFocus={() => {\n setDialFocused(true)\n }}\n onKeyDown={handleThresholdKeyDown}\n ref={thresholdInputRef}\n step={thresholdStep}\n tabIndex={disabled ? -1 : 0}\n type='range'\n value={clampedThreshold}\n />\n </div>\n\n {/* Top overlay above the hub (z-30): the threshold focus ring (an SVG circle matching the\n sector focus ring's stroke width/units exactly) and the read-only bearing needle. */}\n {dialFocused || hasNeedle ? (\n <svg\n aria-hidden\n className='pointer-events-none z-30 col-start-1 row-start-1 size-full overflow-visible'\n viewBox='0 0 100 100'\n >\n {hasNeedle ? (\n <defs>\n <filter\n filterUnits='userSpaceOnUse'\n height='100'\n id={needleShadowId}\n width='100'\n x='0'\n y='0'\n >\n <feDropShadow\n dx='0'\n dy='0.3'\n floodColor='var(--color-level-one)'\n floodOpacity='0.55'\n stdDeviation='0.5'\n />\n </filter>\n </defs>\n ) : null}\n {dialFocused ? (\n <circle\n className='fill-none stroke-accent'\n cx={CENTER}\n cy={CENTER}\n r={THRESHOLD_FOCUS_RADIUS}\n strokeWidth={FOCUS_STROKE_WIDTH}\n />\n ) : null}\n {hasNeedle ? (\n <g\n data-testid={`${dataTestId}-needle`}\n filter={`url(#${needleShadowId})`}\n >\n {/* Read-only bearing needle (consumer-driven; HZN-2658 follows the cursor). Two facets\n split along the centreline give it a lit/shadowed compass look; the drop-shadow\n lifts it off any wedge colour. Decorative — the consumer surfaces the numeric\n bearing in the hover label. */}\n <polygon\n className='fill-danger-500'\n points={`${needleTip.x},${needleTip.y} ${needleBaseLeft.x},${needleBaseLeft.y} ${needleBaseMid.x},${needleBaseMid.y}`}\n />\n <polygon\n className='fill-danger-400'\n points={`${needleTip.x},${needleTip.y} ${needleBaseMid.x},${needleBaseMid.y} ${needleBaseRight.x},${needleBaseRight.y}`}\n />\n </g>\n ) : null}\n </svg>\n ) : null}\n\n {/* The needle SVG is decorative (aria-hidden), so expose the bearing as text for assistive tech. */}\n {hasNeedle ? (\n <span\n className='sr-only'\n data-testid={`${dataTestId}-bearing-value`}\n >\n Bearing {Math.round(needleBearing)} degrees\n </span>\n ) : null}\n </div>\n )\n}\n\nDirectionalColorWheel.displayName = 'DirectionalColorWheel'\n"],"mappings":"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"}
|
package/dist/Drawer.js
CHANGED
|
@@ -84,15 +84,11 @@ const Drawer = ({ children, className, defaultOpen = false, description, directi
|
|
|
84
84
|
const popup = /* @__PURE__ */ jsxs(Popup, {
|
|
85
85
|
"aria-label": !hasTitle && !hasDescription ? "Drawer" : void 0,
|
|
86
86
|
className: cn("z-50 flex flex-col overscroll-contain bg-drawer-bg **:box-border focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent", directionClassName, swipeClassName, className),
|
|
87
|
-
"data-testid": "spectral-drawer-content",
|
|
88
87
|
render: /* @__PURE__ */ jsx("div", { className: "min-h-0 flex h-full flex-col" }),
|
|
89
88
|
style,
|
|
90
89
|
children: [
|
|
91
90
|
/* @__PURE__ */ jsx(Close, {}),
|
|
92
|
-
shouldRenderHandle && /* @__PURE__ */ jsx("div", {
|
|
93
|
-
className: "mt-2 mb-1 h-1.5 w-12 mx-auto shrink-0 cursor-grab touch-none rounded-full bg-text-secondary/40 active:cursor-grabbing",
|
|
94
|
-
"data-testid": "spectral-drawer-handle"
|
|
95
|
-
}),
|
|
91
|
+
shouldRenderHandle && /* @__PURE__ */ jsx("div", { className: "mt-2 mb-1 h-1.5 w-12 mx-auto shrink-0 cursor-grab touch-none rounded-full bg-text-secondary/40 active:cursor-grabbing" }),
|
|
96
92
|
/* @__PURE__ */ jsxs("div", {
|
|
97
93
|
className: "min-h-0 flex flex-1 flex-col",
|
|
98
94
|
"data-base-ui-swipe-ignore": "",
|
|
@@ -100,36 +96,21 @@ const Drawer = ({ children, className, defaultOpen = false, description, directi
|
|
|
100
96
|
hasTitle && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
101
97
|
isTextTitle && /* @__PURE__ */ jsx(Title, {
|
|
102
98
|
className: "px-3 pt-4 text-xl font-semibold text-text-primary",
|
|
103
|
-
"data-testid": "spectral-drawer-title",
|
|
104
99
|
children: title
|
|
105
100
|
}),
|
|
106
|
-
!isTextTitle && isCustomTitleElement && /* @__PURE__ */ jsx(Title, {
|
|
107
|
-
|
|
108
|
-
render: title
|
|
109
|
-
}),
|
|
110
|
-
!isTextTitle && !isCustomTitleElement && /* @__PURE__ */ jsx(Title, {
|
|
111
|
-
"data-testid": "spectral-drawer-title",
|
|
112
|
-
children: title
|
|
113
|
-
})
|
|
101
|
+
!isTextTitle && isCustomTitleElement && /* @__PURE__ */ jsx(Title, { render: title }),
|
|
102
|
+
!isTextTitle && !isCustomTitleElement && /* @__PURE__ */ jsx(Title, { children: title })
|
|
114
103
|
] }),
|
|
115
104
|
hasDescription && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
116
105
|
isTextDescription && /* @__PURE__ */ jsx(Description, {
|
|
117
106
|
className: "mb-2 px-3 text-xs! text-text-secondary! uppercase",
|
|
118
|
-
"data-testid": "spectral-drawer-description",
|
|
119
107
|
children: description
|
|
120
108
|
}),
|
|
121
|
-
!isTextDescription && isCustomDescriptionElement && /* @__PURE__ */ jsx(Description, {
|
|
122
|
-
|
|
123
|
-
render: description
|
|
124
|
-
}),
|
|
125
|
-
!isTextDescription && !isCustomDescriptionElement && /* @__PURE__ */ jsx(Description, {
|
|
126
|
-
"data-testid": "spectral-drawer-description",
|
|
127
|
-
children: description
|
|
128
|
-
})
|
|
109
|
+
!isTextDescription && isCustomDescriptionElement && /* @__PURE__ */ jsx(Description, { render: description }),
|
|
110
|
+
!isTextDescription && !isCustomDescriptionElement && /* @__PURE__ */ jsx(Description, { children: description })
|
|
129
111
|
] }),
|
|
130
112
|
/* @__PURE__ */ jsx("div", {
|
|
131
113
|
className: "py-2 px-3 min-w-0 [&>*]:min-w-0 [&_*]:min-w-0 min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain *:box-border",
|
|
132
|
-
"data-testid": "spectral-drawer-body",
|
|
133
114
|
children
|
|
134
115
|
})
|
|
135
116
|
]
|
|
@@ -139,13 +120,9 @@ const Drawer = ({ children, className, defaultOpen = false, description, directi
|
|
|
139
120
|
return /* @__PURE__ */ jsx(SpectralProvider, { children: /* @__PURE__ */ jsxs(Root, {
|
|
140
121
|
...rootProps,
|
|
141
122
|
children: [/* @__PURE__ */ jsx(Trigger, {
|
|
142
|
-
"data-testid": "spectral-drawer-trigger",
|
|
143
123
|
nativeButton,
|
|
144
124
|
render: trigger
|
|
145
|
-
}), /* @__PURE__ */ jsxs(Portal, { children: [/* @__PURE__ */ jsx(Backdrop, {
|
|
146
|
-
className: "inset-0 fixed bg-transparent",
|
|
147
|
-
"data-testid": "spectral-drawer-overlay"
|
|
148
|
-
}), usesGesture ? /* @__PURE__ */ jsx(Viewport, {
|
|
125
|
+
}), /* @__PURE__ */ jsxs(Portal, { children: [/* @__PURE__ */ jsx(Backdrop, { className: "inset-0 fixed bg-transparent" }), usesGesture ? /* @__PURE__ */ jsx(Viewport, {
|
|
149
126
|
className: "inset-0 fixed z-50",
|
|
150
127
|
children: popup
|
|
151
128
|
}) : popup] })]
|
package/dist/Drawer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Drawer.js","names":["DrawerGesture"],"sources":["../src/components/Drawer/Drawer.tsx"],"sourcesContent":["import { Dialog } from '@base-ui/react/dialog'\nimport { Drawer as DrawerGesture } from '@base-ui/react/drawer'\nimport { SpectralProvider } from '@components/SpectralProvider/SpectralProvider'\nimport { cn } from '@utils/twUtils'\nimport { Fragment, isValidElement, type CSSProperties, type ElementType, type ReactElement, type ReactNode } from 'react'\n\nexport interface DrawerProps {\n className?: string\n children?: ReactNode\n defaultOpen?: boolean\n description?: ReactNode\n direction?: 'left' | 'right' | 'top' | 'bottom'\n dismissible?: boolean\n dragBehavior?: 'surface' | 'handle' | 'none'\n modal?: boolean\n nativeButton?: boolean\n onOpenChange?: (open: boolean) => void\n open?: boolean\n size?: string\n title?: ReactNode\n trigger: ReactNode\n}\n\nconst hasRenderableText = (value: ReactNode) => (typeof value === 'string' ? value.trim().length > 0 : typeof value === 'number')\n\nconst hasRenderableContent = (value: ReactNode) => {\n if (hasRenderableText(value)) {\n return true\n }\n\n if (value === null || value === undefined || typeof value === 'boolean' || typeof value === 'string') {\n return false\n }\n\n return true\n}\n\nconst isElementAndNotFragment = (element: ReactNode) => isValidElement(element) && element.type !== Fragment\n\n// Dismissal reasons that should be blocked when `dismissible` is false. The\n// close button (`close-press`), trigger, and imperative actions stay allowed.\nconst BLOCKED_DISMISS_REASONS = new Set(['outside-press', 'escape-key', 'focus-out', 'swipe', 'close-watcher'])\n\nconst swipeDirectionByDirection = {\n left: 'left',\n right: 'right',\n top: 'up',\n bottom: 'down',\n} as const\n\nexport const Drawer = ({ children, className, defaultOpen = false, description, direction = 'right', dismissible = true, dragBehavior = 'surface', modal = true, nativeButton = true, onOpenChange, open, size = '380px', title, trigger }: DrawerProps) => {\n const baseStyles = 'font-sans! fixed transition-transform duration-300 ease-out motion-reduce:transition-none'\n const hasTitle = hasRenderableContent(title)\n const hasDescription = hasRenderableContent(description)\n const isTextTitle = hasRenderableText(title)\n const isTextDescription = hasRenderableText(description)\n const isCustomTitleElement = isElementAndNotFragment(title)\n const isCustomDescriptionElement = isElementAndNotFragment(description)\n // Only the handle variant keeps a drag-to-close gesture; everything else is a\n // positioned Dialog so content interaction and text selection never dismiss it.\n const usesGesture = dragBehavior === 'handle'\n const shouldRenderHandle = dragBehavior === 'handle'\n\n const directionStyles = {\n left: {\n className: cn(baseStyles, 'top-0 bottom-0 left-0 shadow-[20px_0_20px_var(--color-black-40)] data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full'),\n style: { width: size },\n },\n right: {\n className: cn(baseStyles, 'top-0 bottom-0 right-0 shadow-[-20px_0_20px_var(--color-black-40)] data-[ending-style]:translate-x-full data-[starting-style]:translate-x-full'),\n style: { width: size },\n },\n top: {\n className: cn(baseStyles, 'top-0 left-0 right-0 shadow-[0_20px_20px_var(--color-black-40)] data-[ending-style]:-translate-y-full data-[starting-style]:-translate-y-full'),\n style: { height: size },\n },\n bottom: {\n className: cn(baseStyles, 'bottom-0 left-0 right-0 shadow-[0_-20px_20px_var(--color-black-40)] data-[ending-style]:translate-y-full data-[starting-style]:translate-y-full'),\n style: { height: size },\n },\n }\n\n const { className: directionClassName, style } = directionStyles[direction]\n // During a handle swipe, follow the pointer live (Base UI exposes the offset\n // via CSS variables) and disable the open/close transition.\n const swipeClassName = usesGesture ? 'data-[swiping]:transition-none data-[swiping]:[transform:translate(var(--drawer-swipe-movement-x,0px),var(--drawer-swipe-movement-y,0px))]' : ''\n\n const handleOpenChange = (nextOpen: boolean, eventDetails: { reason?: string; cancel: () => void }) => {\n if (!dismissible && !nextOpen && eventDetails.reason && BLOCKED_DISMISS_REASONS.has(eventDetails.reason)) {\n eventDetails.cancel()\n return\n }\n\n onOpenChange?.(nextOpen)\n }\n\n const Primitive = usesGesture ? DrawerGesture : Dialog\n const Root = Primitive.Root as ElementType\n const Trigger = Primitive.Trigger as ElementType\n const Portal = Primitive.Portal as ElementType\n const Backdrop = Primitive.Backdrop as ElementType\n const Popup = Primitive.Popup as ElementType\n const Title = Primitive.Title as ElementType\n const Description = Primitive.Description as ElementType\n const Close = Primitive.Close as ElementType\n const Viewport = DrawerGesture.Viewport as ElementType\n\n const rootProps = {\n defaultOpen,\n disablePointerDismissal: !dismissible,\n modal,\n onOpenChange: handleOpenChange,\n open,\n ...(usesGesture ? { swipeDirection: swipeDirectionByDirection[direction] } : {}),\n }\n\n const popup = (\n <Popup\n aria-label={!hasTitle && !hasDescription ? 'Drawer' : undefined}\n className={cn('z-50 flex flex-col overscroll-contain bg-drawer-bg **:box-border focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent', directionClassName, swipeClassName, className)}\n data-testid='spectral-drawer-content'\n render={<div className='min-h-0 flex h-full flex-col' />}\n style={style as CSSProperties}\n >\n <Close />\n {shouldRenderHandle && (\n <div\n className='mt-2 mb-1 h-1.5 w-12 mx-auto shrink-0 cursor-grab touch-none rounded-full bg-text-secondary/40 active:cursor-grabbing'\n data-testid='spectral-drawer-handle'\n />\n )}\n {/* Opt content out of swipe dismissal so interaction and text selection\n never close the drawer; only the handle (above) initiates a drag. */}\n <div\n className='min-h-0 flex flex-1 flex-col'\n data-base-ui-swipe-ignore=''\n >\n {hasTitle && (\n <>\n {isTextTitle && (\n <Title\n className='px-3 pt-4 text-xl font-semibold text-text-primary'\n data-testid='spectral-drawer-title'\n >\n {title}\n </Title>\n )}\n {!isTextTitle && isCustomTitleElement && (\n <Title\n data-testid='spectral-drawer-title'\n render={title as ReactElement}\n />\n )}\n {!isTextTitle && !isCustomTitleElement && <Title data-testid='spectral-drawer-title'>{title}</Title>}\n </>\n )}\n {hasDescription && (\n <>\n {isTextDescription && (\n <Description\n className='mb-2 px-3 text-xs! text-text-secondary! uppercase'\n data-testid='spectral-drawer-description'\n >\n {description}\n </Description>\n )}\n {!isTextDescription && isCustomDescriptionElement && (\n <Description\n data-testid='spectral-drawer-description'\n render={description as ReactElement}\n />\n )}\n {!isTextDescription && !isCustomDescriptionElement && <Description data-testid='spectral-drawer-description'>{description}</Description>}\n </>\n )}\n <div\n className='py-2 px-3 min-w-0 [&>*]:min-w-0 [&_*]:min-w-0 min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain *:box-border'\n data-testid='spectral-drawer-body'\n >\n {children}\n </div>\n </div>\n </Popup>\n )\n\n return (\n <SpectralProvider>\n <Root {...rootProps}>\n <Trigger\n data-testid='spectral-drawer-trigger'\n nativeButton={nativeButton}\n render={trigger as ReactElement}\n />\n <Portal>\n <Backdrop\n className='inset-0 fixed bg-transparent'\n data-testid='spectral-drawer-overlay'\n />\n {usesGesture ? <Viewport className='inset-0 fixed z-50'>{popup}</Viewport> : popup}\n </Portal>\n </Root>\n </SpectralProvider>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Drawer.js","names":[],"sources":["../src/components/Drawer/Drawer.tsx"],"sourcesContent":["import { Dialog } from '@base-ui/react/dialog'\nimport { Drawer as DrawerGesture } from '@base-ui/react/drawer'\nimport { SpectralProvider } from '@components/SpectralProvider/SpectralProvider'\nimport { cn } from '@utils/twUtils'\nimport { Fragment, isValidElement, type CSSProperties, type ElementType, type ReactElement, type ReactNode } from 'react'\n\nexport interface DrawerProps {\n className?: string\n children?: ReactNode\n defaultOpen?: boolean\n description?: ReactNode\n direction?: 'left' | 'right' | 'top' | 'bottom'\n dismissible?: boolean\n dragBehavior?: 'surface' | 'handle' | 'none'\n modal?: boolean\n nativeButton?: boolean\n onOpenChange?: (open: boolean) => void\n open?: boolean\n size?: string\n title?: ReactNode\n trigger: ReactNode\n}\n\nconst hasRenderableText = (value: ReactNode) => (typeof value === 'string' ? value.trim().length > 0 : typeof value === 'number')\n\nconst hasRenderableContent = (value: ReactNode) => {\n if (hasRenderableText(value)) {\n return true\n }\n\n if (value === null || value === undefined || typeof value === 'boolean' || typeof value === 'string') {\n return false\n }\n\n return true\n}\n\nconst isElementAndNotFragment = (element: ReactNode) => isValidElement(element) && element.type !== Fragment\n\n// Dismissal reasons that should be blocked when `dismissible` is false. The\n// close button (`close-press`), trigger, and imperative actions stay allowed.\nconst BLOCKED_DISMISS_REASONS = new Set(['outside-press', 'escape-key', 'focus-out', 'swipe', 'close-watcher'])\n\nconst swipeDirectionByDirection = {\n left: 'left',\n right: 'right',\n top: 'up',\n bottom: 'down',\n} as const\n\nexport const Drawer = ({ children, className, defaultOpen = false, description, direction = 'right', dismissible = true, dragBehavior = 'surface', modal = true, nativeButton = true, onOpenChange, open, size = '380px', title, trigger }: DrawerProps) => {\n const baseStyles = 'font-sans! fixed transition-transform duration-300 ease-out motion-reduce:transition-none'\n const hasTitle = hasRenderableContent(title)\n const hasDescription = hasRenderableContent(description)\n const isTextTitle = hasRenderableText(title)\n const isTextDescription = hasRenderableText(description)\n const isCustomTitleElement = isElementAndNotFragment(title)\n const isCustomDescriptionElement = isElementAndNotFragment(description)\n // Only the handle variant keeps a drag-to-close gesture; everything else is a\n // positioned Dialog so content interaction and text selection never dismiss it.\n const usesGesture = dragBehavior === 'handle'\n const shouldRenderHandle = dragBehavior === 'handle'\n\n const directionStyles = {\n left: {\n className: cn(baseStyles, 'top-0 bottom-0 left-0 shadow-[20px_0_20px_var(--color-black-40)] data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full'),\n style: { width: size },\n },\n right: {\n className: cn(baseStyles, 'top-0 bottom-0 right-0 shadow-[-20px_0_20px_var(--color-black-40)] data-[ending-style]:translate-x-full data-[starting-style]:translate-x-full'),\n style: { width: size },\n },\n top: {\n className: cn(baseStyles, 'top-0 left-0 right-0 shadow-[0_20px_20px_var(--color-black-40)] data-[ending-style]:-translate-y-full data-[starting-style]:-translate-y-full'),\n style: { height: size },\n },\n bottom: {\n className: cn(baseStyles, 'bottom-0 left-0 right-0 shadow-[0_-20px_20px_var(--color-black-40)] data-[ending-style]:translate-y-full data-[starting-style]:translate-y-full'),\n style: { height: size },\n },\n }\n\n const { className: directionClassName, style } = directionStyles[direction]\n // During a handle swipe, follow the pointer live (Base UI exposes the offset\n // via CSS variables) and disable the open/close transition.\n const swipeClassName = usesGesture ? 'data-[swiping]:transition-none data-[swiping]:[transform:translate(var(--drawer-swipe-movement-x,0px),var(--drawer-swipe-movement-y,0px))]' : ''\n\n const handleOpenChange = (nextOpen: boolean, eventDetails: { reason?: string; cancel: () => void }) => {\n if (!dismissible && !nextOpen && eventDetails.reason && BLOCKED_DISMISS_REASONS.has(eventDetails.reason)) {\n eventDetails.cancel()\n return\n }\n\n onOpenChange?.(nextOpen)\n }\n\n const Primitive = usesGesture ? DrawerGesture : Dialog\n const Root = Primitive.Root as ElementType\n const Trigger = Primitive.Trigger as ElementType\n const Portal = Primitive.Portal as ElementType\n const Backdrop = Primitive.Backdrop as ElementType\n const Popup = Primitive.Popup as ElementType\n const Title = Primitive.Title as ElementType\n const Description = Primitive.Description as ElementType\n const Close = Primitive.Close as ElementType\n const Viewport = DrawerGesture.Viewport as ElementType\n\n const rootProps = {\n defaultOpen,\n disablePointerDismissal: !dismissible,\n modal,\n onOpenChange: handleOpenChange,\n open,\n ...(usesGesture ? { swipeDirection: swipeDirectionByDirection[direction] } : {}),\n }\n\n const popup = (\n <Popup\n aria-label={!hasTitle && !hasDescription ? 'Drawer' : undefined}\n className={cn('z-50 flex flex-col overscroll-contain bg-drawer-bg **:box-border focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent', directionClassName, swipeClassName, className)}\n data-testid='spectral-drawer-content'\n render={<div className='min-h-0 flex h-full flex-col' />}\n style={style as CSSProperties}\n >\n <Close />\n {shouldRenderHandle && (\n <div\n className='mt-2 mb-1 h-1.5 w-12 mx-auto shrink-0 cursor-grab touch-none rounded-full bg-text-secondary/40 active:cursor-grabbing'\n data-testid='spectral-drawer-handle'\n />\n )}\n {/* Opt content out of swipe dismissal so interaction and text selection\n never close the drawer; only the handle (above) initiates a drag. */}\n <div\n className='min-h-0 flex flex-1 flex-col'\n data-base-ui-swipe-ignore=''\n >\n {hasTitle && (\n <>\n {isTextTitle && (\n <Title\n className='px-3 pt-4 text-xl font-semibold text-text-primary'\n data-testid='spectral-drawer-title'\n >\n {title}\n </Title>\n )}\n {!isTextTitle && isCustomTitleElement && (\n <Title\n data-testid='spectral-drawer-title'\n render={title as ReactElement}\n />\n )}\n {!isTextTitle && !isCustomTitleElement && <Title data-testid='spectral-drawer-title'>{title}</Title>}\n </>\n )}\n {hasDescription && (\n <>\n {isTextDescription && (\n <Description\n className='mb-2 px-3 text-xs! text-text-secondary! uppercase'\n data-testid='spectral-drawer-description'\n >\n {description}\n </Description>\n )}\n {!isTextDescription && isCustomDescriptionElement && (\n <Description\n data-testid='spectral-drawer-description'\n render={description as ReactElement}\n />\n )}\n {!isTextDescription && !isCustomDescriptionElement && <Description data-testid='spectral-drawer-description'>{description}</Description>}\n </>\n )}\n <div\n className='py-2 px-3 min-w-0 [&>*]:min-w-0 [&_*]:min-w-0 min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain *:box-border'\n data-testid='spectral-drawer-body'\n >\n {children}\n </div>\n </div>\n </Popup>\n )\n\n return (\n <SpectralProvider>\n <Root {...rootProps}>\n <Trigger\n data-testid='spectral-drawer-trigger'\n nativeButton={nativeButton}\n render={trigger as ReactElement}\n />\n <Portal>\n <Backdrop\n className='inset-0 fixed bg-transparent'\n data-testid='spectral-drawer-overlay'\n />\n {usesGesture ? <Viewport className='inset-0 fixed z-50'>{popup}</Viewport> : popup}\n </Portal>\n </Root>\n </SpectralProvider>\n )\n}\n"],"mappings":"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"}
|
package/dist/DropdownMenu.js
CHANGED
|
@@ -53,13 +53,9 @@ const DropdownMenu = ({ align = "start", asChild = true, alignOffset = 0, avoidC
|
|
|
53
53
|
setOpen(false);
|
|
54
54
|
};
|
|
55
55
|
const renderOption = (option, index) => {
|
|
56
|
-
if (option.type === "separator") return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Separator, {
|
|
57
|
-
className: "-mx-1 my-1 h-px bg-border-secondary",
|
|
58
|
-
"data-testid": "spectral-dropdown-menu-separator"
|
|
59
|
-
}, option.id ?? `separator-${index}`);
|
|
56
|
+
if (option.type === "separator") return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Separator, { className: "-mx-1 my-1 h-px bg-border-secondary" }, option.id ?? `separator-${index}`);
|
|
60
57
|
if (option.type === "label") return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Label, {
|
|
61
58
|
className: "px-2 py-1.5 text-base font-semibold text-text-primary",
|
|
62
|
-
"data-testid": "spectral-dropdown-menu-label",
|
|
63
59
|
children: option.label
|
|
64
60
|
}, option.id ?? `label-${index}`);
|
|
65
61
|
const isSelected = isOptionSelected(option.value);
|
|
@@ -67,7 +63,6 @@ const DropdownMenu = ({ align = "start", asChild = true, alignOffset = 0, avoidC
|
|
|
67
63
|
if (selectionMode === "multiple" || option.type === "checkbox") return /* @__PURE__ */ jsxs(DropdownMenuPrimitive.CheckboxItem, {
|
|
68
64
|
checked: isSelected,
|
|
69
65
|
className: itemClassName,
|
|
70
|
-
"data-testid": "spectral-dropdown-menu-checkbox-item",
|
|
71
66
|
disabled: option.disabled,
|
|
72
67
|
onCheckedChange: () => handleValueChange(option.value),
|
|
73
68
|
onSelect: (event) => event.preventDefault(),
|
|
@@ -95,7 +90,6 @@ const DropdownMenu = ({ align = "start", asChild = true, alignOffset = 0, avoidC
|
|
|
95
90
|
}, option.value);
|
|
96
91
|
return /* @__PURE__ */ jsxs(DropdownMenuPrimitive.Item, {
|
|
97
92
|
className: itemClassName,
|
|
98
|
-
"data-testid": "spectral-dropdown-menu-item",
|
|
99
93
|
disabled: option.disabled,
|
|
100
94
|
onSelect: () => handleValueChange(option.value),
|
|
101
95
|
children: [
|
|
@@ -122,7 +116,6 @@ const DropdownMenu = ({ align = "start", asChild = true, alignOffset = 0, avoidC
|
|
|
122
116
|
children: [/* @__PURE__ */ jsx(DropdownMenuPrimitive.Trigger, {
|
|
123
117
|
asChild,
|
|
124
118
|
className: cn(className, "transition-opacity duration-200 hover:cursor-pointer hover:opacity-80"),
|
|
125
|
-
"data-testid": dataTestId ?? "spectral-dropdown-menu-trigger",
|
|
126
119
|
disabled: isDisabled,
|
|
127
120
|
children: trigger
|
|
128
121
|
}), /* @__PURE__ */ jsx(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx(DropdownMenuPrimitive.Content, {
|
|
@@ -134,7 +127,6 @@ const DropdownMenu = ({ align = "start", asChild = true, alignOffset = 0, avoidC
|
|
|
134
127
|
collisionPadding,
|
|
135
128
|
"data-dropdown-width-mode": dropdownWidthMode,
|
|
136
129
|
"data-dropdown-width-value": dropdownWidthMode === "custom" ? dropdownWidth : void 0,
|
|
137
|
-
"data-testid": "spectral-dropdown-menu-content",
|
|
138
130
|
ref: (node) => {
|
|
139
131
|
setDropdownElement(node);
|
|
140
132
|
if (dropdownContentRef) dropdownContentRef.current = node;
|
package/dist/DropdownMenu.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DropdownMenu.js","names":[],"sources":["../src/components/DropdownMenu/DropdownMenu.tsx"],"sourcesContent":["import { CheckmarkIcon } from '@components/Icons'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'\nimport { useAutoDropdownHorizontalShift } from '@utils/dropdownPositioning'\nimport { EmptyState, getDropdownWidthStyles, getDropdownSurfaceClasses, getOptionClasses, LoadingState, type DropdownWidth } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { type ComponentPropsWithoutRef, type RefObject, type ReactNode } from 'react'\n\ntype Align = 'start' | 'center' | 'end'\ntype Side = 'top' | 'bottom' | 'left' | 'right'\ntype DropdownValue = string | string[]\n\ninterface BaseDropdownOption {\n disabled?: boolean\n label: string\n shortcut?: ReactNode\n value: string\n}\n\nexport interface DropdownMenuItemOption extends BaseDropdownOption {\n type?: 'item'\n}\n\nexport interface DropdownMenuCheckboxOption extends BaseDropdownOption {\n type: 'checkbox'\n}\n\nexport interface DropdownMenuLabelOption {\n id?: string\n label: string\n type: 'label'\n}\n\nexport interface DropdownMenuSeparatorOption {\n id?: string\n type: 'separator'\n}\n\nexport type DropdownMenuOption = DropdownMenuItemOption | DropdownMenuCheckboxOption | DropdownMenuLabelOption | DropdownMenuSeparatorOption\n\nexport interface DropdownMenuProps extends Omit<ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Root>, 'defaultOpen' | 'onOpenChange' | 'open'> {\n align?: Align\n asChild?: boolean\n alignOffset?: number\n avoidCollisions?: boolean\n collisionBoundary?: Element | Element[] | null\n collisionPadding?: number | Partial<Record<Side, number>>\n className?: string\n contentClassName?: string\n dataTestId?: string\n defaultOpen?: boolean\n defaultValue?: DropdownValue\n disabled?: boolean\n dropdownContentRef?: RefObject<HTMLDivElement | null>\n dropdownWidth?: DropdownWidth\n emptyMessage?: ReactNode\n loadingMessage?: string\n onOpenChange?: (open: boolean) => void\n onValueChange?: (value: DropdownValue) => void\n open?: boolean\n options: DropdownMenuOption[]\n selectionMode?: 'single' | 'multiple'\n side?: Side\n sideOffset?: number\n state?: 'default' | 'loading'\n trigger: ReactNode\n value?: DropdownValue\n}\n\nconst normalizeValue = (value: DropdownValue | undefined, selectionMode: 'single' | 'multiple') => {\n if (selectionMode === 'multiple') {\n if (Array.isArray(value)) return value\n return value ? [value] : []\n }\n\n if (Array.isArray(value)) {\n return value[0] ?? ''\n }\n\n return value ?? ''\n}\n\nconst isSelectableOption = (option: DropdownMenuOption): option is DropdownMenuItemOption | DropdownMenuCheckboxOption => {\n return option.type !== 'separator' && option.type !== 'label'\n}\n\nexport const DropdownMenu = ({\n align = 'start',\n asChild = true,\n alignOffset = 0,\n avoidCollisions = true,\n className,\n collisionBoundary,\n collisionPadding = 10,\n contentClassName,\n dataTestId,\n defaultOpen,\n defaultValue,\n disabled = false,\n dropdownContentRef,\n dropdownWidth = 'content',\n emptyMessage = 'No options found',\n loadingMessage = 'Loading…',\n modal = true,\n onOpenChange,\n onValueChange,\n open: openProp,\n options,\n selectionMode = 'single',\n side = 'bottom',\n sideOffset = 4,\n state = 'default',\n trigger,\n value: valueProp,\n}: DropdownMenuProps) => {\n const [open, setOpen] = useUncontrolledState<boolean>({\n value: openProp,\n defaultValue: defaultOpen ?? false,\n onChange: onOpenChange,\n })\n\n const { dropdownShiftStyle, setDropdownElement } = useAutoDropdownHorizontalShift(open)\n\n const resolvedDefaultValue = normalizeValue(defaultValue, selectionMode)\n const resolvedControlledValue = valueProp === undefined ? undefined : normalizeValue(valueProp, selectionMode)\n const [value, setValue] = useUncontrolledState<DropdownValue>({\n value: resolvedControlledValue,\n defaultValue: resolvedDefaultValue,\n onChange: onValueChange,\n })\n\n const { dropdownWidthMode, dropdownWidthStyle } = getDropdownWidthStyles({\n dropdownWidth,\n triggerWidth: 'var(--radix-dropdown-menu-trigger-width)',\n })\n\n const isDisabled = disabled || state === 'loading'\n const selectableOptions = options.filter(isSelectableOption)\n\n const isOptionSelected = (optionValue: string) => {\n if (selectionMode === 'multiple') {\n const values = Array.isArray(value) ? value : []\n return values.includes(optionValue)\n }\n\n return value === optionValue\n }\n\n const handleValueChange = (nextValue: string) => {\n if (selectionMode === 'multiple') {\n const values = Array.isArray(value) ? value : []\n const nextValues = values.includes(nextValue) ? values.filter((entry) => entry !== nextValue) : [...values, nextValue]\n setValue(nextValues)\n return\n }\n\n setValue(nextValue)\n setOpen(false)\n }\n\n const renderOption = (option: DropdownMenuOption, index: number) => {\n if (option.type === 'separator') {\n return <DropdownMenuPrimitive.Separator className='-mx-1 my-1 h-px bg-border-secondary' data-testid='spectral-dropdown-menu-separator' key={option.id ?? `separator-${index}`} />\n }\n\n if (option.type === 'label') {\n return (\n <DropdownMenuPrimitive.Label className='px-2 py-1.5 text-base font-semibold text-text-primary' data-testid='spectral-dropdown-menu-label' key={option.id ?? `label-${index}`}>\n {option.label}\n </DropdownMenuPrimitive.Label>\n )\n }\n\n const isSelected = isOptionSelected(option.value)\n const itemClassName = cn(getOptionClasses(Boolean(option.disabled), false, isSelected), 'group/spectral-dropdown-menu-item pr-2 gap-4 relative flex w-full justify-between')\n\n if (selectionMode === 'multiple' || option.type === 'checkbox') {\n return (\n <DropdownMenuPrimitive.CheckboxItem\n checked={isSelected}\n className={itemClassName}\n data-testid='spectral-dropdown-menu-checkbox-item'\n disabled={option.disabled}\n key={option.value}\n onCheckedChange={() => handleValueChange(option.value)}\n onSelect={(event) => event.preventDefault()}\n >\n <span className='min-w-0 flex-1 truncate whitespace-nowrap'>{option.label}</span>\n {option.shortcut && <span className='text-xs tracking-widest ml-auto text-input-text-placeholder'>{option.shortcut}</span>}\n <span aria-hidden='true' className='ml-2 size-4 flex shrink-0 items-center justify-center'>\n <DropdownMenuPrimitive.ItemIndicator asChild>\n <span className='size-4 flex items-center justify-center'>\n <CheckmarkIcon size={16} />\n </span>\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n </DropdownMenuPrimitive.CheckboxItem>\n )\n }\n\n return (\n <DropdownMenuPrimitive.Item className={itemClassName} data-testid='spectral-dropdown-menu-item' disabled={option.disabled} key={option.value} onSelect={() => handleValueChange(option.value)}>\n <span className='min-w-0 flex-1 truncate whitespace-nowrap'>{option.label}</span>\n {option.shortcut && <span className='text-xs tracking-widest ml-auto text-input-text-placeholder'>{option.shortcut}</span>}\n <span aria-hidden='true' className='ml-2 size-4 flex shrink-0 items-center justify-center'>\n {isSelected && <CheckmarkIcon size={16} />}\n </span>\n </DropdownMenuPrimitive.Item>\n )\n }\n\n return (\n <DropdownMenuPrimitive.Root modal={modal} onOpenChange={setOpen} open={open}>\n <DropdownMenuPrimitive.Trigger asChild={asChild} className={cn(className, 'transition-opacity duration-200 hover:cursor-pointer hover:opacity-80')} data-testid={dataTestId ?? 'spectral-dropdown-menu-trigger'} disabled={isDisabled}>\n {trigger}\n </DropdownMenuPrimitive.Trigger>\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n align={align}\n alignOffset={alignOffset}\n avoidCollisions={avoidCollisions}\n className={cn(\n 'p-1 relative z-50 motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:animate-in',\n getDropdownSurfaceClasses(),\n 'motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=open]:zoom-in-95',\n 'max-h-[min(var(--radix-dropdown-menu-content-available-height),300px)] motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=top]:slide-in-from-bottom-2',\n 'min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto',\n contentClassName,\n )}\n collisionBoundary={collisionBoundary}\n collisionPadding={collisionPadding}\n data-dropdown-width-mode={dropdownWidthMode}\n data-dropdown-width-value={dropdownWidthMode === 'custom' ? dropdownWidth : undefined}\n data-testid='spectral-dropdown-menu-content'\n ref={(node) => {\n setDropdownElement(node)\n if (dropdownContentRef) {\n dropdownContentRef.current = node\n }\n }}\n side={side}\n sideOffset={sideOffset}\n style={{\n ...dropdownWidthStyle,\n ...dropdownShiftStyle,\n }}\n >\n {state === 'loading' ? <LoadingState message={loadingMessage} /> : selectableOptions.length === 0 ? <EmptyState message={emptyMessage} /> : options.map(renderOption)}\n </DropdownMenuPrimitive.Content>\n </DropdownMenuPrimitive.Portal>\n </DropdownMenuPrimitive.Root>\n )\n}\n\nDropdownMenu.displayName = 'DropdownMenu'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DropdownMenu.js","names":[],"sources":["../src/components/DropdownMenu/DropdownMenu.tsx"],"sourcesContent":["import { CheckmarkIcon } from '@components/Icons'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'\nimport { useAutoDropdownHorizontalShift } from '@utils/dropdownPositioning'\nimport { EmptyState, getDropdownWidthStyles, getDropdownSurfaceClasses, getOptionClasses, LoadingState, type DropdownWidth } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { type ComponentPropsWithoutRef, type RefObject, type ReactNode } from 'react'\n\ntype Align = 'start' | 'center' | 'end'\ntype Side = 'top' | 'bottom' | 'left' | 'right'\ntype DropdownValue = string | string[]\n\ninterface BaseDropdownOption {\n disabled?: boolean\n label: string\n shortcut?: ReactNode\n value: string\n}\n\nexport interface DropdownMenuItemOption extends BaseDropdownOption {\n type?: 'item'\n}\n\nexport interface DropdownMenuCheckboxOption extends BaseDropdownOption {\n type: 'checkbox'\n}\n\nexport interface DropdownMenuLabelOption {\n id?: string\n label: string\n type: 'label'\n}\n\nexport interface DropdownMenuSeparatorOption {\n id?: string\n type: 'separator'\n}\n\nexport type DropdownMenuOption = DropdownMenuItemOption | DropdownMenuCheckboxOption | DropdownMenuLabelOption | DropdownMenuSeparatorOption\n\nexport interface DropdownMenuProps extends Omit<ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Root>, 'defaultOpen' | 'onOpenChange' | 'open'> {\n align?: Align\n asChild?: boolean\n alignOffset?: number\n avoidCollisions?: boolean\n collisionBoundary?: Element | Element[] | null\n collisionPadding?: number | Partial<Record<Side, number>>\n className?: string\n contentClassName?: string\n dataTestId?: string\n defaultOpen?: boolean\n defaultValue?: DropdownValue\n disabled?: boolean\n dropdownContentRef?: RefObject<HTMLDivElement | null>\n dropdownWidth?: DropdownWidth\n emptyMessage?: ReactNode\n loadingMessage?: string\n onOpenChange?: (open: boolean) => void\n onValueChange?: (value: DropdownValue) => void\n open?: boolean\n options: DropdownMenuOption[]\n selectionMode?: 'single' | 'multiple'\n side?: Side\n sideOffset?: number\n state?: 'default' | 'loading'\n trigger: ReactNode\n value?: DropdownValue\n}\n\nconst normalizeValue = (value: DropdownValue | undefined, selectionMode: 'single' | 'multiple') => {\n if (selectionMode === 'multiple') {\n if (Array.isArray(value)) return value\n return value ? [value] : []\n }\n\n if (Array.isArray(value)) {\n return value[0] ?? ''\n }\n\n return value ?? ''\n}\n\nconst isSelectableOption = (option: DropdownMenuOption): option is DropdownMenuItemOption | DropdownMenuCheckboxOption => {\n return option.type !== 'separator' && option.type !== 'label'\n}\n\nexport const DropdownMenu = ({\n align = 'start',\n asChild = true,\n alignOffset = 0,\n avoidCollisions = true,\n className,\n collisionBoundary,\n collisionPadding = 10,\n contentClassName,\n dataTestId,\n defaultOpen,\n defaultValue,\n disabled = false,\n dropdownContentRef,\n dropdownWidth = 'content',\n emptyMessage = 'No options found',\n loadingMessage = 'Loading…',\n modal = true,\n onOpenChange,\n onValueChange,\n open: openProp,\n options,\n selectionMode = 'single',\n side = 'bottom',\n sideOffset = 4,\n state = 'default',\n trigger,\n value: valueProp,\n}: DropdownMenuProps) => {\n const [open, setOpen] = useUncontrolledState<boolean>({\n value: openProp,\n defaultValue: defaultOpen ?? false,\n onChange: onOpenChange,\n })\n\n const { dropdownShiftStyle, setDropdownElement } = useAutoDropdownHorizontalShift(open)\n\n const resolvedDefaultValue = normalizeValue(defaultValue, selectionMode)\n const resolvedControlledValue = valueProp === undefined ? undefined : normalizeValue(valueProp, selectionMode)\n const [value, setValue] = useUncontrolledState<DropdownValue>({\n value: resolvedControlledValue,\n defaultValue: resolvedDefaultValue,\n onChange: onValueChange,\n })\n\n const { dropdownWidthMode, dropdownWidthStyle } = getDropdownWidthStyles({\n dropdownWidth,\n triggerWidth: 'var(--radix-dropdown-menu-trigger-width)',\n })\n\n const isDisabled = disabled || state === 'loading'\n const selectableOptions = options.filter(isSelectableOption)\n\n const isOptionSelected = (optionValue: string) => {\n if (selectionMode === 'multiple') {\n const values = Array.isArray(value) ? value : []\n return values.includes(optionValue)\n }\n\n return value === optionValue\n }\n\n const handleValueChange = (nextValue: string) => {\n if (selectionMode === 'multiple') {\n const values = Array.isArray(value) ? value : []\n const nextValues = values.includes(nextValue) ? values.filter((entry) => entry !== nextValue) : [...values, nextValue]\n setValue(nextValues)\n return\n }\n\n setValue(nextValue)\n setOpen(false)\n }\n\n const renderOption = (option: DropdownMenuOption, index: number) => {\n if (option.type === 'separator') {\n return <DropdownMenuPrimitive.Separator className='-mx-1 my-1 h-px bg-border-secondary' data-testid='spectral-dropdown-menu-separator' key={option.id ?? `separator-${index}`} />\n }\n\n if (option.type === 'label') {\n return (\n <DropdownMenuPrimitive.Label className='px-2 py-1.5 text-base font-semibold text-text-primary' data-testid='spectral-dropdown-menu-label' key={option.id ?? `label-${index}`}>\n {option.label}\n </DropdownMenuPrimitive.Label>\n )\n }\n\n const isSelected = isOptionSelected(option.value)\n const itemClassName = cn(getOptionClasses(Boolean(option.disabled), false, isSelected), 'group/spectral-dropdown-menu-item pr-2 gap-4 relative flex w-full justify-between')\n\n if (selectionMode === 'multiple' || option.type === 'checkbox') {\n return (\n <DropdownMenuPrimitive.CheckboxItem\n checked={isSelected}\n className={itemClassName}\n data-testid='spectral-dropdown-menu-checkbox-item'\n disabled={option.disabled}\n key={option.value}\n onCheckedChange={() => handleValueChange(option.value)}\n onSelect={(event) => event.preventDefault()}\n >\n <span className='min-w-0 flex-1 truncate whitespace-nowrap'>{option.label}</span>\n {option.shortcut && <span className='text-xs tracking-widest ml-auto text-input-text-placeholder'>{option.shortcut}</span>}\n <span aria-hidden='true' className='ml-2 size-4 flex shrink-0 items-center justify-center'>\n <DropdownMenuPrimitive.ItemIndicator asChild>\n <span className='size-4 flex items-center justify-center'>\n <CheckmarkIcon size={16} />\n </span>\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n </DropdownMenuPrimitive.CheckboxItem>\n )\n }\n\n return (\n <DropdownMenuPrimitive.Item className={itemClassName} data-testid='spectral-dropdown-menu-item' disabled={option.disabled} key={option.value} onSelect={() => handleValueChange(option.value)}>\n <span className='min-w-0 flex-1 truncate whitespace-nowrap'>{option.label}</span>\n {option.shortcut && <span className='text-xs tracking-widest ml-auto text-input-text-placeholder'>{option.shortcut}</span>}\n <span aria-hidden='true' className='ml-2 size-4 flex shrink-0 items-center justify-center'>\n {isSelected && <CheckmarkIcon size={16} />}\n </span>\n </DropdownMenuPrimitive.Item>\n )\n }\n\n return (\n <DropdownMenuPrimitive.Root modal={modal} onOpenChange={setOpen} open={open}>\n <DropdownMenuPrimitive.Trigger asChild={asChild} className={cn(className, 'transition-opacity duration-200 hover:cursor-pointer hover:opacity-80')} data-testid={dataTestId ?? 'spectral-dropdown-menu-trigger'} disabled={isDisabled}>\n {trigger}\n </DropdownMenuPrimitive.Trigger>\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n align={align}\n alignOffset={alignOffset}\n avoidCollisions={avoidCollisions}\n className={cn(\n 'p-1 relative z-50 motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:animate-in',\n getDropdownSurfaceClasses(),\n 'motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=open]:zoom-in-95',\n 'max-h-[min(var(--radix-dropdown-menu-content-available-height),300px)] motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=top]:slide-in-from-bottom-2',\n 'min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto',\n contentClassName,\n )}\n collisionBoundary={collisionBoundary}\n collisionPadding={collisionPadding}\n data-dropdown-width-mode={dropdownWidthMode}\n data-dropdown-width-value={dropdownWidthMode === 'custom' ? dropdownWidth : undefined}\n data-testid='spectral-dropdown-menu-content'\n ref={(node) => {\n setDropdownElement(node)\n if (dropdownContentRef) {\n dropdownContentRef.current = node\n }\n }}\n side={side}\n sideOffset={sideOffset}\n style={{\n ...dropdownWidthStyle,\n ...dropdownShiftStyle,\n }}\n >\n {state === 'loading' ? <LoadingState message={loadingMessage} /> : selectableOptions.length === 0 ? <EmptyState message={emptyMessage} /> : options.map(renderOption)}\n </DropdownMenuPrimitive.Content>\n </DropdownMenuPrimitive.Portal>\n </DropdownMenuPrimitive.Root>\n )\n}\n\nDropdownMenu.displayName = 'DropdownMenu'\n"],"mappings":"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"}
|
package/dist/FormFieldMessage.js
CHANGED
|
@@ -33,7 +33,6 @@ const FormFieldMessage = ({ ariaLive, className, containerClassName, dataTestId,
|
|
|
33
33
|
"aria-atomic": isVisible ? "true" : void 0,
|
|
34
34
|
"aria-live": isVisible ? ariaLive : void 0,
|
|
35
35
|
className: cn("m-0! text-sm leading-5 overflow-hidden", toneClasses, className),
|
|
36
|
-
"data-testid": isVisible ? dataTestId : void 0,
|
|
37
36
|
id,
|
|
38
37
|
role: isVisible ? role : void 0,
|
|
39
38
|
children: content
|