@teamofsilicons/silicon-ui 0.2.0 → 0.4.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.
Files changed (163) hide show
  1. package/README.md +4 -3
  2. package/check.mjs +14 -5
  3. package/components/activity-heatmap/activity-heatmap.module.css +4 -4
  4. package/components/alert/alert.module.css +1 -1
  5. package/components/animated-counter/animated-counter.js +1 -1
  6. package/components/announcement-bar/announcement-bar.module.css +6 -6
  7. package/components/avatar/avatar.d.ts +2 -2
  8. package/components/avatar/avatar.js +1 -1
  9. package/components/avatar/avatar.module.css +5 -2
  10. package/components/avatar-group/avatar-group.module.css +4 -4
  11. package/components/badge/badge.module.css +4 -4
  12. package/components/bar-chart/bar-chart.js +1 -1
  13. package/components/bar-chart/bar-chart.module.css +4 -4
  14. package/components/billing-toggle/billing-toggle.js +2 -2
  15. package/components/billing-toggle/billing-toggle.module.css +10 -10
  16. package/components/bottom-sheet/bottom-sheet.js +1 -1
  17. package/components/bottom-sheet/bottom-sheet.module.css +4 -5
  18. package/components/breadcrumb/breadcrumb.d.ts +8 -2
  19. package/components/breadcrumb/breadcrumb.js +21 -9
  20. package/components/breadcrumb/breadcrumb.module.css +7 -3
  21. package/components/brush-chart/brush-chart.module.css +9 -9
  22. package/components/button/button.d.ts +7 -2
  23. package/components/button/button.js +3 -1
  24. package/components/button/button.module.css +10 -1
  25. package/components/button-group/button-group.module.css +7 -7
  26. package/components/calendar/calendar.js +15 -5
  27. package/components/calendar/calendar.module.css +13 -10
  28. package/components/card/card.js +4 -4
  29. package/components/card/card.module.css +2 -2
  30. package/components/card-stack/card-stack.js +2 -2
  31. package/components/card-stack/card-stack.module.css +2 -3
  32. package/components/carousel/carousel.module.css +3 -3
  33. package/components/chat-thread/chat-thread.module.css +41 -41
  34. package/components/checkbox/checkbox.module.css +1 -1
  35. package/components/chip-group/chip-group.js +1 -1
  36. package/components/chip-group/chip-group.module.css +2 -2
  37. package/components/code-block/code-block.module.css +7 -14
  38. package/components/color-picker/color-picker.module.css +20 -21
  39. package/components/combobox/combobox.js +5 -1
  40. package/components/combobox/combobox.module.css +2 -2
  41. package/components/command-palette/command-palette.js +18 -11
  42. package/components/command-palette/command-palette.module.css +18 -18
  43. package/components/comment-thread/comment-thread.module.css +35 -35
  44. package/components/confirm-morph/confirm-morph.js +5 -5
  45. package/components/confirm-morph/confirm-morph.module.css +9 -9
  46. package/components/context-menu/context-menu.js +10 -28
  47. package/components/context-menu/context-menu.module.css +3 -3
  48. package/components/copy-button/copy-button.js +4 -4
  49. package/components/countdown/countdown.module.css +4 -4
  50. package/components/date-picker/date-picker.js +3 -3
  51. package/components/date-picker/date-picker.module.css +7 -7
  52. package/components/date-range-picker/date-range-picker.js +13 -10
  53. package/components/date-range-picker/date-range-picker.module.css +23 -22
  54. package/components/dialog/dialog.module.css +2 -2
  55. package/components/donut-chart/donut-chart.module.css +5 -5
  56. package/components/drawer/drawer.module.css +7 -7
  57. package/components/dropdown-menu/dropdown-menu.d.ts +71 -1
  58. package/components/dropdown-menu/dropdown-menu.js +100 -6
  59. package/components/dropdown-menu/dropdown-menu.module.css +15 -5
  60. package/components/elastic-slider/elastic-slider.js +2 -2
  61. package/components/elastic-slider/elastic-slider.module.css +4 -4
  62. package/components/expandable-card/expandable-card.js +1 -1
  63. package/components/expandable-card/expandable-card.module.css +1 -1
  64. package/components/expanding-button-group/expanding-button-group.module.css +3 -3
  65. package/components/expanding-search/expanding-search.js +3 -3
  66. package/components/expanding-search/expanding-search.module.css +9 -9
  67. package/components/file-dropzone/file-dropzone.js +3 -3
  68. package/components/file-dropzone/file-dropzone.module.css +21 -21
  69. package/components/file-upload/file-upload.module.css +7 -7
  70. package/components/filter-toolbar/filter-toolbar.js +7 -3
  71. package/components/filter-toolbar/filter-toolbar.module.css +9 -9
  72. package/components/floating-button-group/floating-button-group.module.css +7 -7
  73. package/components/gauge/gauge.js +1 -1
  74. package/components/gauge/gauge.module.css +2 -2
  75. package/components/hover-card/hover-card.js +2 -2
  76. package/components/hover-card/hover-card.module.css +2 -2
  77. package/components/image-compare/image-compare.module.css +4 -4
  78. package/components/inline-edit/inline-edit.module.css +12 -12
  79. package/components/json-viewer/json-viewer.module.css +12 -12
  80. package/components/line-chart/line-chart.js +1 -1
  81. package/components/line-chart/line-chart.module.css +8 -8
  82. package/components/mention-input/mention-input.js +4 -2
  83. package/components/mention-input/mention-input.module.css +8 -8
  84. package/components/metric-card/metric-card.module.css +1 -1
  85. package/components/money-input/money-input.js +6 -3
  86. package/components/money-input/money-input.module.css +10 -10
  87. package/components/morph-select/morph-select.js +7 -5
  88. package/components/morph-select/morph-select.module.css +13 -13
  89. package/components/multi-select/multi-select.js +4 -2
  90. package/components/multi-select/multi-select.module.css +6 -6
  91. package/components/nav-list/nav-list.d.ts +64 -0
  92. package/components/nav-list/nav-list.js +96 -0
  93. package/components/nav-list/nav-list.module.css +25 -0
  94. package/components/notification-center/notification-center.module.css +27 -27
  95. package/components/number-field/number-field.js +3 -3
  96. package/components/number-field/number-field.module.css +3 -3
  97. package/components/otp-input/otp-input.module.css +4 -4
  98. package/components/pagination/pagination.module.css +1 -1
  99. package/components/password-field/password-field.module.css +2 -2
  100. package/components/password-strength/password-strength.module.css +8 -8
  101. package/components/phone-input/phone-input.js +9 -7
  102. package/components/phone-input/phone-input.module.css +19 -19
  103. package/components/popover/popover.module.css +2 -2
  104. package/components/progress/progress.module.css +1 -1
  105. package/components/radio-cards/radio-cards.module.css +8 -8
  106. package/components/radio-group/radio-group.js +4 -1
  107. package/components/radio-group/radio-group.module.css +4 -3
  108. package/components/resizable-panels/resizable-panels.module.css +5 -5
  109. package/components/rich-text-editor/rich-text-editor.js +3 -3
  110. package/components/rich-text-editor/rich-text-editor.module.css +12 -12
  111. package/components/ridgeline/ridgeline.module.css +7 -7
  112. package/components/scroll-area/scroll-area.module.css +8 -8
  113. package/components/segmented-control/segmented-control.d.ts +3 -1
  114. package/components/segmented-control/segmented-control.js +5 -2
  115. package/components/segmented-control/segmented-control.module.css +9 -4
  116. package/components/select/select.module.css +5 -5
  117. package/components/shortcut-recorder/shortcut-recorder.js +5 -5
  118. package/components/shortcut-recorder/shortcut-recorder.module.css +16 -16
  119. package/components/signature-pad/signature-pad.module.css +11 -11
  120. package/components/slider/slider.module.css +4 -4
  121. package/components/slope-chart/slope-chart.module.css +7 -7
  122. package/components/slot-text/slot-text.js +2 -1
  123. package/components/sortable-data-table/sortable-data-table.module.css +4 -4
  124. package/components/sparkline/sparkline.js +1 -1
  125. package/components/split-button/split-button.module.css +6 -6
  126. package/components/stepper/stepper.module.css +3 -3
  127. package/components/streamgraph/streamgraph.module.css +7 -7
  128. package/components/swipe-actions/swipe-actions.js +2 -2
  129. package/components/swipe-actions/swipe-actions.module.css +5 -5
  130. package/components/switch/switch.js +2 -2
  131. package/components/switch/switch.module.css +1 -1
  132. package/components/tabs/tabs.d.ts +9 -2
  133. package/components/tabs/tabs.js +11 -5
  134. package/components/tabs/tabs.module.css +10 -3
  135. package/components/tag-input/tag-input.module.css +4 -4
  136. package/components/text-reveal/text-reveal.js +1 -1
  137. package/components/time-picker/time-picker.js +4 -2
  138. package/components/time-picker/time-picker.module.css +3 -3
  139. package/components/timeline/timeline.js +4 -4
  140. package/components/timeline/timeline.module.css +3 -3
  141. package/components/toast/toast.js +1 -1
  142. package/components/toast/toast.module.css +5 -5
  143. package/components/toast-stack/toast-stack.js +1 -1
  144. package/components/toast-stack/toast-stack.module.css +5 -5
  145. package/components/tooltip/tooltip.module.css +1 -1
  146. package/components/tree-view/tree-view.js +16 -12
  147. package/components/tree-view/tree-view.module.css +10 -8
  148. package/components/treemap/treemap.module.css +13 -13
  149. package/components/usage-meter/usage-meter.js +1 -1
  150. package/components/usage-meter/usage-meter.module.css +5 -5
  151. package/components/user-menu/user-menu.js +16 -14
  152. package/components/user-menu/user-menu.module.css +22 -22
  153. package/components/waffle-chart/waffle-chart.module.css +7 -7
  154. package/components.json +416 -1
  155. package/eslint.js +35 -3
  156. package/lib/fluid-hover.d.ts +83 -0
  157. package/lib/fluid-hover.js +184 -0
  158. package/lib/menu-highlight.d.ts +24 -11
  159. package/lib/menu-highlight.js +29 -28
  160. package/lib/motion-tokens.d.ts +76 -18
  161. package/lib/motion-tokens.js +45 -10
  162. package/package.json +1 -1
  163. package/styles.css +50 -7
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useSta
4
4
  import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion, useTransform } from "motion/react";
5
5
  import { CalendarDays, ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
6
6
  import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import { FluidHoverHighlight, useFluidHover } from "../../lib/fluid-hover.js";
7
8
  import { useToday } from "../../lib/use-today.js";
8
9
  import styles from "./date-range-picker.module.css";
9
10
  const DAY = 864e5;
@@ -50,21 +51,21 @@ const GROW = physical(.48, .12), SHRINK = physical(.38, 0), STRETCH = physical(.
50
51
  const TRIGGER_RADIUS = 18, PANEL_RADIUS = 26, WIDE_CELL = 36, WIDE_MIN = 712, EDGE = 8;
51
52
  const roll = {
52
53
  enter: (direction) => ({ opacity: 0, y: `${direction * .45}em`, filter: `blur(${blur.subtle}px)` }),
53
- center: { opacity: 1, y: "0em", filter: "blur(0px)", transition: { duration: .26, ease: enterEase } },
54
- exit: (direction) => ({ opacity: 0, y: `${direction * -.45}em`, filter: `blur(${blur.subtle}px)`, transition: { duration: .14, ease: standardEase } }),
54
+ center: { opacity: 1, y: "0em", filter: "blur(0px)", transition: { duration: motionTokens.duration.exit, ease: enterEase } },
55
+ exit: (direction) => ({ opacity: 0, y: `${direction * -.45}em`, filter: `blur(${blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: standardEase } }),
55
56
  };
56
- const fade = { enter: { opacity: 0 }, center: { opacity: 1, y: "0em", filter: "blur(0px)", transition: { duration: .12 } }, exit: { opacity: 0, transition: { duration: .08 } } };
57
+ const fade = { enter: { opacity: 0 }, center: { opacity: 1, y: "0em", filter: "blur(0px)", transition: { duration: motionTokens.duration.instant } }, exit: { opacity: 0, transition: { duration: motionTokens.duration.instant } } };
57
58
  /** Panel content arrives just behind the growing surface and leaves ahead of it. */
58
59
  const faceIn = {
59
60
  hidden: { opacity: 0, y: -4, filter: `blur(${blur.soft}px)` },
60
- shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: .26, ease: enterEase, delay: .07 } },
61
- gone: { opacity: 0, y: -2, filter: `blur(${blur.soft}px)`, transition: { duration: .12, ease: standardEase } },
61
+ shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.exit, ease: enterEase, delay: motionTokens.duration.instant } },
62
+ gone: { opacity: 0, y: -2, filter: `blur(${blur.soft}px)`, transition: { duration: motionTokens.duration.instant, ease: standardEase } },
62
63
  };
63
- const faceFade = { hidden: { opacity: 0 }, shown: { opacity: 1, transition: { duration: .12 } }, gone: { opacity: 0, transition: { duration: .08 } } };
64
+ const faceFade = { hidden: { opacity: 0 }, shown: { opacity: 1, transition: { duration: motionTokens.duration.instant } }, gone: { opacity: 0, transition: { duration: motionTokens.duration.instant } } };
64
65
  const slide = {
65
66
  enter: (direction) => ({ opacity: 0, x: direction * 48, filter: `blur(${blur.soft}px)` }),
66
- center: { opacity: 1, x: 0, filter: "blur(0px)", transition: { x: SLIDE, opacity: { duration: .22, ease: enterEase }, filter: { duration: .24, ease: enterEase } } },
67
- exit: (direction) => ({ opacity: 0, x: direction * -36, filter: `blur(${blur.soft}px)`, transition: { x: SLIDE, opacity: { duration: .14, ease: standardEase }, filter: { duration: .14, ease: standardEase } } }),
67
+ center: { opacity: 1, x: 0, filter: "blur(0px)", transition: { x: SLIDE, opacity: { duration: motionTokens.duration.exit, ease: enterEase }, filter: { duration: motionTokens.duration.exit, ease: enterEase } } },
68
+ exit: (direction) => ({ opacity: 0, x: direction * -36, filter: `blur(${blur.soft}px)`, transition: { x: SLIDE, opacity: { duration: motionTokens.duration.fast, ease: standardEase }, filter: { duration: motionTokens.duration.fast, ease: standardEase } } }),
68
69
  };
69
70
  /** Words roll one by one: a later date rises from below, an earlier one drops from above; words that stay the same hold still. */
70
71
  function Rolling({ text, direction, reduced }) {
@@ -216,6 +217,8 @@ export function DateRangePicker({ value, defaultValue = null, onChange, label =
216
217
  const triggerRef = useRef(null);
217
218
  const panelRef = useRef(null);
218
219
  const railRef = useRef(null);
220
+ // Fluid hover on the presets: the nearest one is lit, gaps and padding included, and a click there picks it. Focus has its own fill.
221
+ const presetHover = useFluidHover(railRef, { items: "[data-preset]", axis: "xy", followFocus: false });
219
222
  const pendingFocus = useRef(null);
220
223
  /* ---------- One surface: the trigger's box springs to the panel's box and back ---------- */
221
224
  // Before the first measurement the surface simply fills the root and the root wraps the trigger, so server markup already looks right.
@@ -524,10 +527,10 @@ export function DateRangePicker({ value, defaultValue = null, onChange, label =
524
527
  const layoutId = reduced ? undefined : `${uid}-value`;
525
528
  const cellSize = compact && panelWidth ? Math.max(32, Math.min(42, Math.floor((panelWidth - 24) / 7))) : WIDE_CELL;
526
529
  const quiet = open ? (reduced ? { opacity: 0 } : { opacity: 0, filter: `blur(${blur.subtle}px)` }) : { opacity: 1, filter: "blur(0px)" };
527
- const quietTransition = open ? { duration: .12, ease: standardEase } : { duration: .22, ease: enterEase, delay: reduced ? 0 : .1 };
530
+ const quietTransition = open ? { duration: motionTokens.duration.instant, ease: standardEase } : { duration: motionTokens.duration.exit, ease: enterEase, delay: reduced ? 0 : .1 };
528
531
  return _jsxs(motion.div, { ref: rootRef, className: [styles.root, className].filter(Boolean).join(" "), style: { width: rootWidth }, "data-open": open || undefined, onKeyDown: onRootKey, onBlur: onRootBlur, children: [_jsxs("button", { ref: triggerRef, type: "button", className: styles.trigger, "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": open ? `${uid}-panel` : undefined, "aria-label": `${label}: ${committedText}`, disabled: !today, inert: open || undefined, onClick: openPanel, children: [_jsx(motion.span, { className: styles.triggerIcon, initial: false, animate: quiet, transition: quietTransition, children: _jsx(CalendarDays, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), open || !layoutId
529
532
  ? _jsx(motion.span, { className: styles.value, "data-empty": !committed || undefined, initial: false, animate: open ? { opacity: 0 } : { opacity: 1 }, transition: quietTransition, children: _jsx(Rolling, { text: committedText, direction: committedDirection, reduced: reduced }) }, "resting")
530
- : _jsx(motion.span, { layoutId: layoutId, layout: "position", className: styles.value, "data-empty": !committed || undefined, transition: SHRINK, children: _jsx(Rolling, { text: committedText, direction: committedDirection, reduced: reduced }) }, "shared"), _jsx(motion.span, { className: styles.chevron, initial: false, animate: quiet, transition: quietTransition, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }), _jsx(motion.div, { className: styles.surface, style: { width, height, borderRadius: radius, x }, children: _jsx(AnimatePresence, { children: open && today && _jsxs(motion.div, { ref: panelRef, id: `${uid}-panel`, className: styles.panel, role: "dialog", "aria-label": label, "data-compact": compact || undefined, style: { "--cell": `${cellSize}px`, width: compact && panelWidth ? panelWidth : undefined }, exit: { opacity: 1, transition: { duration: .14 } }, children: [_jsxs(motion.div, { className: styles.body, variants: reduced ? faceFade : faceIn, initial: "hidden", animate: "shown", exit: "gone", children: [_jsxs("div", { ref: railRef, className: styles.rail, role: "group", "aria-label": "Presets", onKeyDown: onRailKey, children: [_jsx(motion.span, { className: styles.railHighlight, style: { x: gx, y: gy, width: gw, height: gh, opacity: go }, "aria-hidden": "true" }), presets.map((preset, index) => _jsx("button", { type: "button", className: styles.preset, "data-preset": index, "aria-pressed": index === activePreset, onClick: () => choosePreset(preset), children: preset.label }, preset.label))] }), _jsxs("div", { className: styles.calendars, onPointerLeave: () => { if (anchor)
533
+ : _jsx(motion.span, { layoutId: layoutId, layout: "position", className: styles.value, "data-empty": !committed || undefined, transition: SHRINK, children: _jsx(Rolling, { text: committedText, direction: committedDirection, reduced: reduced }) }, "shared"), _jsx(motion.span, { className: styles.chevron, initial: false, animate: quiet, transition: quietTransition, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }), _jsx(motion.div, { className: styles.surface, style: { width, height, borderRadius: radius, x }, children: _jsx(AnimatePresence, { children: open && today && _jsxs(motion.div, { ref: panelRef, id: `${uid}-panel`, className: styles.panel, role: "dialog", "aria-label": label, "data-compact": compact || undefined, style: { "--cell": `${cellSize}px`, width: compact && panelWidth ? panelWidth : undefined }, exit: { opacity: 1, transition: { duration: motionTokens.duration.fast } }, children: [_jsxs(motion.div, { className: styles.body, variants: reduced ? faceFade : faceIn, initial: "hidden", animate: "shown", exit: "gone", children: [_jsxs("div", { ref: railRef, className: styles.rail, role: "group", "aria-label": "Presets", onKeyDown: onRailKey, ...presetHover.handlers, children: [_jsx(FluidHoverHighlight, { hover: presetHover, className: styles.railHover }), _jsx(motion.span, { className: styles.railHighlight, style: { x: gx, y: gy, width: gw, height: gh, opacity: go }, "aria-hidden": "true" }), presets.map((preset, index) => _jsx("button", { type: "button", className: styles.preset, "data-preset": index, "aria-pressed": index === activePreset, onClick: () => choosePreset(preset), children: preset.label }, preset.label))] }), _jsxs("div", { className: styles.calendars, onPointerLeave: () => { if (anchor)
531
534
  setHover(null); }, children: [_jsx("button", { type: "button", className: styles.navButton, "data-side": "prev", "aria-label": "Previous month", onClick: () => goTo(addMonths(view, -1)), disabled: Boolean(minDate && monthDiff(view, minDate) <= 0), children: _jsx(ChevronLeft, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsx("button", { type: "button", className: styles.navButton, "data-side": "next", "aria-label": "Next month", onClick: () => goTo(addMonths(view, 1)), disabled: Boolean(maxDate && monthDiff(addMonths(view, count - 1), maxDate) >= 0), children: _jsx(ChevronRight, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsx("div", { className: styles.viewport, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", custom: direction, children: _jsx(Months, { direction: direction, reduced: reduced, children: visible.map(month => _jsx(Month, { month: month, range: shown, tabbable: tabbable, today: today, minDate: minDate, maxDate: maxDate, weekStartsOn: weekStartsOn, reduced: reduced, formatters: formatters, idBase: uid, onPick: pick, onHover: date => { if (anchor)
532
535
  setHover(date); }, onKey: onDayKey, onFocusDay: date => setFocusKey(keyOf(date)) }, keyOf(month))) }, `${keyOf(view)}-${count}`) }) })] })] }), _jsxs("div", { className: styles.footer, children: [_jsxs("div", { className: styles.summary, children: [_jsx(motion.span, { layoutId: layoutId, layout: layoutId ? "position" : undefined, className: styles.summaryValue, transition: GROW, variants: layoutId ? undefined : faceFade, initial: layoutId ? undefined : "hidden", animate: layoutId ? undefined : "shown", exit: layoutId ? undefined : "gone", children: _jsx(Rolling, { text: shownText, direction: shownDirection, reduced: reduced }) }), _jsx(motion.span, { className: styles.summaryCount, variants: reduced ? faceFade : faceIn, initial: "hidden", animate: "shown", exit: "gone", children: countText && _jsx(Rolling, { text: anchor && sameDay(anchor, hover) ? "Pick an end date" : countText, direction: shownDirection, reduced: reduced }) })] }), _jsxs(motion.div, { className: styles.actions, variants: reduced ? faceFade : faceIn, initial: "hidden", animate: "shown", exit: "gone", children: [_jsx("button", { type: "button", className: styles.ghost, onClick: () => close(true), children: "Cancel" }), _jsx("button", { type: "button", className: styles.primary, onClick: apply, disabled: !shown, children: "Apply" })] })] }), _jsx("p", { className: styles.srOnly, "aria-live": "polite", children: status })] }, "panel") }) })] });
533
536
  }
@@ -11,7 +11,7 @@
11
11
  line-height: var(--leading-body);
12
12
  }
13
13
  .root { z-index: 0; transition: z-index 0s linear .5s; }
14
- .root[data-open] { z-index: 30; transition-delay: 0s; }
14
+ .root[data-open] { z-index: var(--layer-popover); transition-delay: 0s; }
15
15
  .root:not([data-open]) .surface { z-index: 0; }
16
16
  .root *, .root *::before, .root *::after { box-sizing: border-box; }
17
17
 
@@ -38,8 +38,8 @@
38
38
  width: max-content;
39
39
  height: var(--control-height-md);
40
40
  align-items: center;
41
- gap: 10px;
42
- padding: 0 12px 0 14px;
41
+ gap: var(--space-group);
42
+ padding: 0 var(--space-group) 0 var(--space-inset);
43
43
  border: 0;
44
44
  background: transparent;
45
45
  color: var(--foreground);
@@ -62,10 +62,12 @@
62
62
  .wordInner { display: inline-block; }
63
63
 
64
64
  /* Panel */
65
- .panel { position: absolute; top: 0; left: 0; width: max-content; padding: 10px; --cell: 36px; }
66
- .body { display: flex; gap: 10px; }
65
+ .panel { position: absolute; top: 0; left: 0; width: max-content; padding: var(--space-group); --cell: 36px; }
66
+ .body { display: flex; gap: var(--space-group); }
67
67
 
68
- .rail { position: relative; display: flex; width: 9.25rem; flex: none; flex-direction: column; gap: 2px; padding-right: 10px; border-right: 1px solid var(--border-subtle); }
68
+ .rail { position: relative; display: flex; width: 9.25rem; flex: none; flex-direction: column; gap: var(--space-hair); padding-right: var(--space-group); border-right: 1px solid var(--border-subtle); }
69
+ /* The hover fill glides between presets, under the chosen preset's highlight. */
70
+ .railHover { border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--foreground) 4%, transparent); }
69
71
  .railHighlight { position: absolute; top: 0; left: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--foreground) 6.5%, transparent); pointer-events: none; }
70
72
  .preset {
71
73
  position: relative;
@@ -73,7 +75,7 @@
73
75
  height: 32px;
74
76
  flex: none;
75
77
  align-items: center;
76
- padding: 0 12px;
78
+ padding: 0 var(--space-group);
77
79
  border: 0;
78
80
  border-radius: var(--radius-control); corner-shape: var(--corner-shape);
79
81
  background: transparent;
@@ -87,7 +89,7 @@
87
89
  }
88
90
  .preset[aria-pressed="true"] { color: var(--foreground); font-weight: 500; }
89
91
 
90
- .calendars { position: relative; padding: 0 2px; }
92
+ .calendars { position: relative; padding: 0 var(--space-hair); }
91
93
  .navButton {
92
94
  position: absolute;
93
95
  z-index: 2;
@@ -105,14 +107,14 @@
105
107
  -webkit-tap-highlight-color: transparent;
106
108
  transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
107
109
  }
108
- .navButton[data-side="prev"] { left: 2px; }
109
- .navButton[data-side="next"] { right: 2px; }
110
+ .navButton[data-side="prev"] { left: var(--space-hair); }
111
+ .navButton[data-side="next"] { right: var(--space-hair); }
110
112
  .navButton:active:not(:disabled) { transform: scale(.92); }
111
113
  .navButton:disabled { opacity: .35; cursor: default; }
112
114
 
113
115
  /* Months slide horizontally inside a clipped window that keeps a little room for the blur. */
114
116
  .viewport { position: relative; overflow: clip; overflow-clip-margin: 2px; }
115
- .months { display: flex; gap: 24px; }
117
+ .months { display: flex; gap: var(--space-section); }
116
118
  .month { width: calc(var(--cell) * 7); }
117
119
  .monthTitle { height: 32px; margin: 0; color: var(--foreground); font-weight: 500; line-height: 32px; text-align: center; white-space: nowrap; }
118
120
  .grid { display: grid; }
@@ -125,7 +127,7 @@
125
127
  .layer { position: absolute; inset: 0; pointer-events: none; }
126
128
  .bar { position: absolute; height: calc(var(--cell) - 6px); border-radius: var(--radius-full); corner-shape: round; background: var(--accent-subtle); }
127
129
  .thumb { position: absolute; top: 0; left: 0; width: var(--cell); height: var(--cell); }
128
- .thumb::before { content: ""; position: absolute; inset: 3px; border-radius: 50%; corner-shape: round; background: var(--accent); }
130
+ .thumb::before { content: ""; position: absolute; inset: var(--space-tight); border-radius: 50%; corner-shape: round; background: var(--accent); }
129
131
 
130
132
  .day {
131
133
  position: relative;
@@ -148,23 +150,23 @@
148
150
  .day > span { position: relative; display: grid; width: calc(100% - 6px); height: calc(100% - 6px); place-items: center; border-radius: 50%; corner-shape: round; transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
149
151
  .day:active:not(:disabled) > span { transform: scale(.9); }
150
152
  .day[data-edge] { color: var(--accent-foreground); font-weight: 500; transition-delay: 40ms; }
151
- .day[data-today]::after { content: ""; position: absolute; bottom: 5px; left: 50%; width: 4px; height: 4px; margin-left: -2px; border-radius: 50%; corner-shape: round; background: var(--accent); }
153
+ .day[data-today]::after { content: ""; position: absolute; bottom: var(--space-tight); left: 50%; width: 4px; height: 4px; margin-left: calc(var(--space-hair) * -1); border-radius: 50%; corner-shape: round; background: var(--accent); }
152
154
  .day[data-today][data-edge]::after { background: var(--accent-foreground); }
153
155
  .day:disabled { color: var(--text-muted); cursor: default; opacity: .5; }
154
156
  .day:focus-visible:not([data-edge]) > span { background: color-mix(in oklab, var(--foreground) 7%, transparent); }
155
157
 
156
158
  /* Footer: the formatted range and the two decisions. */
157
- .footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; padding: 10px 2px 0 12px; border-top: 1px solid var(--border-subtle); }
159
+ .footer { display: flex; align-items: center; justify-content: space-between; gap: var(--space-group); margin-top: var(--space-group); padding: var(--space-group) var(--space-hair) 0 var(--space-group); border-top: 1px solid var(--border-subtle); }
158
160
  .summary { display: grid; min-width: 0; gap: 1px; }
159
161
  .summaryValue { display: inline-flex; width: max-content; font-weight: 500; font-variant-numeric: tabular-nums; }
160
162
  .summaryCount { display: inline-flex; min-height: 1.4em; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
161
- .actions { display: flex; flex: none; align-items: center; gap: 6px; }
163
+ .actions { display: flex; flex: none; align-items: center; gap: var(--space-related); }
162
164
  .ghost, .primary {
163
165
  display: inline-flex;
164
166
  height: var(--control-height-sm);
165
167
  align-items: center;
166
168
  justify-content: center;
167
- padding: 0 14px;
169
+ padding: 0 var(--space-inset);
168
170
  border: 0;
169
171
  border-radius: var(--radius-control); corner-shape: var(--corner-shape);
170
172
  cursor: pointer;
@@ -184,13 +186,13 @@
184
186
  .srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
185
187
 
186
188
  /* Compact: one month under a scrolling row of presets. */
187
- .panel[data-compact] { padding: 8px; }
188
- .panel[data-compact] .body { flex-direction: column; gap: 8px; }
189
- .panel[data-compact] .rail { width: auto; flex-direction: row; gap: 2px; margin: 0 -8px; padding: 0 8px 8px; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--border-subtle); overscroll-behavior-x: contain; scrollbar-width: none; }
189
+ .panel[data-compact] { padding: var(--space-related); }
190
+ .panel[data-compact] .body { flex-direction: column; gap: var(--space-related); }
191
+ .panel[data-compact] .rail { width: auto; flex-direction: row; gap: var(--space-hair); margin: 0 calc(var(--space-related) * -1); padding: 0 var(--space-related) var(--space-related); overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--border-subtle); overscroll-behavior-x: contain; scrollbar-width: none; }
190
192
  .panel[data-compact] .rail::-webkit-scrollbar { display: none; }
191
- .panel[data-compact] .preset { padding: 0 11px; }
193
+ .panel[data-compact] .preset { padding: 0 var(--space-group); }
192
194
  .panel[data-compact] .calendars { align-self: center; }
193
- .panel[data-compact] .footer { flex-wrap: wrap; margin-top: 6px; padding-left: 11px; }
195
+ .panel[data-compact] .footer { flex-wrap: wrap; margin-top: var(--space-related); padding-left: var(--space-group); }
194
196
  .panel[data-compact] .actions { margin-left: auto; }
195
197
  .panel[data-compact] .summary { flex: 1 1 8rem; }
196
198
 
@@ -198,7 +200,6 @@
198
200
  .root:not([data-open]):has(> .trigger:hover:not(:disabled)) .surface { background: color-mix(in oklab, var(--foreground) 3%, var(--surface-raised)); }
199
201
  .root:not([data-open]):has(> .trigger:hover:not(:disabled)) .surface::after { border-color: var(--border-strong); }
200
202
  .preset:hover { color: var(--foreground); }
201
- .preset:not([aria-pressed="true"]):hover { background: color-mix(in oklab, var(--foreground) 4%, transparent); }
202
203
  .navButton:hover:not(:disabled) { background: color-mix(in oklab, var(--foreground) 6.5%, transparent); color: var(--foreground); }
203
204
  .day:hover:not(:disabled):not([data-edge]) > span { background: color-mix(in oklab, var(--foreground) 7%, transparent); }
204
205
  .ghost:hover { background: var(--surface-muted); color: var(--foreground); }
@@ -1,6 +1,6 @@
1
- .overlay { position: fixed; inset: 0; z-index: 50; background: oklch(10% 0 0 / .46); backdrop-filter: blur(7px); }
1
+ .overlay { position: fixed; inset: 0; z-index: var(--layer-overlay); background: var(--scrim); backdrop-filter: blur(7px); }
2
2
  /* Centered with auto margins (like a native modal dialog) so transform stays free for the entrance spring. */
3
- .content { position: fixed; z-index: 51; inset: 0; width: min(calc(100vw - var(--space-block)), 440px); height: fit-content; max-height: calc(100vh - var(--space-block)); max-height: calc(100dvh - var(--space-block)); margin: auto; overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-overlay); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); }
3
+ .content { position: fixed; z-index: var(--layer-dialog); inset: 0; width: min(calc(100vw - var(--space-block)), 440px); height: fit-content; max-height: calc(100vh - var(--space-block)); max-height: calc(100dvh - var(--space-block)); margin: auto; overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-overlay); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); }
4
4
  /* Motion animates both layers. Keyframes only run when DialogContent sits under a bare Radix root. */
5
5
  .keyframes.overlay[data-state="open"] { animation: overlay-in var(--duration-standard) var(--ease-enter) both; }
6
6
  .keyframes.overlay[data-state="closed"] { animation: overlay-out var(--duration-fast) var(--ease-standard) both; }
@@ -10,7 +10,7 @@
10
10
  .svg { display: block; width: 100%; height: 100%; overflow: visible; cursor: default; }
11
11
  .svg[data-hovering] { cursor: pointer; }
12
12
  /* The track shows only before the first sweep and when there is nothing to draw, so the gaps between segments stay the page colour. */
13
- .track { fill: none; stroke: var(--surface-muted); transition: opacity var(--duration-considered, .48s) var(--ease-standard); opacity: 0; }
13
+ .track { fill: none; stroke: var(--surface-muted); transition: opacity var(--duration-expressive, .48s) var(--ease-standard); opacity: 0; }
14
14
  .track[data-idle] { opacity: 1; }
15
15
  /* Shapes are written every frame from script; only colour and dimming ease here, on the compositor. */
16
16
  .segment { fill: var(--slice); shape-rendering: geometricPrecision; transition: fill var(--duration-standard) var(--ease-standard), opacity var(--duration-standard) var(--ease-standard); }
@@ -18,7 +18,7 @@
18
18
 
19
19
  /* The centre reads the active segment, or the total at rest. Old and new readouts share one grid cell, so a swap never moves or resizes anything. */
20
20
  .center { position: absolute; top: 50%; left: 50%; display: grid; width: calc(var(--inner) - 16px); transform: translate(-50%, -50%); pointer-events: none; }
21
- .readout { grid-area: 1 / 1; display: grid; min-width: 0; justify-items: center; gap: 2px; text-align: center; }
21
+ .readout { grid-area: 1 / 1; display: grid; min-width: 0; justify-items: center; gap: var(--space-hair); text-align: center; }
22
22
  .centerLabel, .centerMeta { display: block; width: 100%; min-height: calc(1em * var(--leading-body)); overflow: hidden; font-size: var(--text-xs); line-height: var(--leading-body); text-overflow: ellipsis; white-space: nowrap; }
23
23
  .centerLabel { color: var(--text-secondary); }
24
24
  .centerMeta { color: var(--text-muted); font-variant-numeric: tabular-nums; }
@@ -26,10 +26,10 @@
26
26
 
27
27
  /* Legend rows mirror the segments: hovering either lights both. */
28
28
  /* Rows carry their own 2px spacing so a row closing its height takes its gap with it; the list's negative margin drops the last one. */
29
- .legend { display: grid; margin: 0 0 -2px; padding: 0; list-style: none; }
29
+ .legend { display: grid; margin: 0 0 calc(var(--space-hair) * -1); padding: 0; list-style: none; }
30
30
  .item { overflow: hidden; }
31
- .item > .row { margin-bottom: 2px; }
32
- .row { display: grid; width: 100%; min-height: 40px; grid-template-columns: 10px minmax(0, 1fr) auto 3.25em; align-items: center; column-gap: 10px; padding: 6px 10px; border: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); cursor: pointer; font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); text-align: left; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
31
+ .item > .row { margin-bottom: var(--space-hair); }
32
+ .row { display: grid; width: 100%; min-height: 40px; grid-template-columns: 10px minmax(0, 1fr) auto 3.25em; align-items: center; column-gap: var(--space-group); padding: var(--space-related) var(--space-group); border: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); cursor: pointer; font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); text-align: left; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
33
33
  .row:active { transform: scale(.985); }
34
34
  .row[data-active], .row[data-pinned] { background: color-mix(in oklab, var(--foreground) 5%, transparent); }
35
35
  .dot { width: 10px; height: 10px; border-radius: 50%; corner-shape: round; background: var(--slice); box-shadow: inset 0 0 0 1.5px var(--slice); transition: background-color var(--duration-standard) var(--ease-standard), box-shadow var(--duration-standard) var(--ease-standard); }
@@ -1,8 +1,8 @@
1
1
  .overlay {
2
2
  position: fixed;
3
3
  inset: 0;
4
- z-index: 50;
5
- background: oklch(10% 0 0 / .38);
4
+ z-index: var(--layer-overlay);
5
+ background: var(--scrim);
6
6
  backdrop-filter: blur(4px);
7
7
  }
8
8
 
@@ -12,7 +12,7 @@
12
12
  --ease-drawer: cubic-bezier(.32,.72,0,1);
13
13
  --drawer-from: translateX(100%);
14
14
  position: fixed;
15
- z-index: 51;
15
+ z-index: var(--layer-dialog);
16
16
  display: flex;
17
17
  flex-direction: column;
18
18
  width: var(--drawer-size);
@@ -32,7 +32,7 @@
32
32
  .content[data-side="left"] { inset: 0 auto 0 0; border-width: 1px 1px 1px 0; border-radius: 0 var(--radius-container) var(--radius-container) 0; corner-shape: var(--corner-shape); }
33
33
  .content[data-side="top"] { inset: 0 0 auto; width: 100%; max-width: none; max-height: min(32rem, 100dvh); border-width: 0 0 1px; border-radius: 0 0 var(--radius-container) var(--radius-container); corner-shape: var(--corner-shape); }
34
34
  .content[data-side="bottom"] { inset: auto 0 0; width: 100%; max-width: none; max-height: min(32rem, 100dvh); border-width: 1px 0 0; border-radius: var(--radius-container) var(--radius-container) 0 0; corner-shape: var(--corner-shape); }
35
- .content[data-side="bottom"]::before { position: absolute; top: 9px; left: 50%; width: 2.25rem; height: 4px; border-radius: var(--radius-full); corner-shape: round; background: var(--border-strong); content: ""; opacity: .75; transform: translateX(-50%); }
35
+ .content[data-side="bottom"]::before { position: absolute; top: var(--space-related); left: 50%; width: 2.25rem; height: 4px; border-radius: var(--radius-full); corner-shape: round; background: var(--border-strong); content: ""; opacity: .75; transform: translateX(-50%); }
36
36
 
37
37
  /* Motion springs the panel from its edge. Keyframes only run when DrawerContent sits under a bare Radix root. */
38
38
  .content[data-side="left"] { --drawer-from: translateX(-100%); }
@@ -45,7 +45,7 @@
45
45
  .content:focus { outline: none; }
46
46
  /* Inside a container the layers anchor to that element instead of the viewport. */
47
47
  .overlay[data-contained], .content[data-contained] { position: absolute; }
48
- .overlay[data-contained] { z-index: 1; background: oklch(10% 0 0 / .16); backdrop-filter: none; }
48
+ .overlay[data-contained] { z-index: 1; background: var(--scrim-soft); backdrop-filter: none; }
49
49
  .content[data-contained] { --drawer-size: min(22rem, calc(100% - var(--space-block))); z-index: 2; max-height: 100%; box-shadow: none; }
50
50
  .content[data-contained][data-side="top"], .content[data-contained][data-side="bottom"] { max-height: min(32rem, calc(100% - var(--space-block))); }
51
51
  /* The container already draws the outer edges, so only the inner edge keeps a border. */
@@ -89,9 +89,9 @@
89
89
 
90
90
  @media (prefers-reduced-motion: reduce) {
91
91
  .keyframes.overlay[data-state="open"] { animation: overlay-in var(--duration-instant) linear both; }
92
- .keyframes.overlay[data-state="closed"] { animation: overlay-out 100ms linear both; }
92
+ .keyframes.overlay[data-state="closed"] { animation: overlay-out var(--duration-fast) linear both; }
93
93
  .keyframes.content[data-state="open"] { animation: drawer-fade-in var(--duration-instant) linear both; }
94
- .keyframes.content[data-state="closed"] { animation: drawer-fade-out 100ms linear both; }
94
+ .keyframes.content[data-state="closed"] { animation: drawer-fade-out var(--duration-fast) linear both; }
95
95
  .close, .close:active { transition: none; }
96
96
  .close:active { transform: none; }
97
97
  }
@@ -1,16 +1,86 @@
1
- import type { ReactNode } from "react";
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import * as DropdownPrimitive from "@radix-ui/react-dropdown-menu";
3
+ /** The menu's state. Uncontrolled by default; pass `open` and `onOpenChange` to control it. */
4
+ export declare function DropdownMenuRoot(props: ComponentPropsWithoutRef<typeof DropdownPrimitive.Root>): import("react").JSX.Element;
5
+ export interface DropdownMenuTriggerProps extends ComponentPropsWithoutRef<typeof DropdownPrimitive.Trigger> {
6
+ /** Render your own trigger (an icon-only Button with an aria-label, an avatar and name) instead of the default one. */
7
+ asChild?: boolean;
8
+ }
9
+ /** What opens the menu. Without `asChild` it is the default outlined trigger with a chevron. */
10
+ export declare const DropdownMenuTrigger: import("react").ForwardRefExoticComponent<DropdownMenuTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
11
+ export interface DropdownMenuContentProps extends ComponentPropsWithoutRef<typeof DropdownPrimitive.Content> {
12
+ /** Which side of the trigger it opens on (default bottom). */
13
+ side?: "top" | "right" | "bottom" | "left";
14
+ /** How it lines up with the trigger (default end). */
15
+ align?: "start" | "center" | "end";
16
+ }
17
+ /** The menu itself. One highlight glides between items for the pointer and jumps instantly for the keyboard. */
18
+ export declare const DropdownMenuContent: import("react").ForwardRefExoticComponent<DropdownMenuContentProps & import("react").RefAttributes<HTMLDivElement>>;
19
+ export interface DropdownMenuItemProps extends ComponentPropsWithoutRef<typeof DropdownPrimitive.Item> {
20
+ /** A 16px icon before the label. */
21
+ icon?: ReactNode;
22
+ /** A keyboard shortcut hint after the label, such as "⌘D". Shown only; bind the keys yourself. */
23
+ shortcut?: string;
24
+ /** A destructive action (delete, remove): shown in the danger colour. */
25
+ destructive?: boolean;
26
+ /** Make your own element the item, such as a link: `<DropdownMenuItem asChild><a href="/q3">Q3 2026</a></DropdownMenuItem>`.
27
+ * Its children become the label, and Enter follows the link. */
28
+ asChild?: boolean;
29
+ }
30
+ /** One action, or with `asChild` one link. */
31
+ export declare const DropdownMenuItem: import("react").ForwardRefExoticComponent<DropdownMenuItemProps & import("react").RefAttributes<HTMLDivElement>>;
32
+ export interface DropdownMenuCheckboxItemProps extends ComponentPropsWithoutRef<typeof DropdownPrimitive.CheckboxItem> {
33
+ /** A keyboard shortcut hint after the label. */
34
+ shortcut?: string;
35
+ }
36
+ /** An on/off setting, such as "Full width". A check shows in the icon column while it is on. */
37
+ export declare const DropdownMenuCheckboxItem: import("react").ForwardRefExoticComponent<DropdownMenuCheckboxItemProps & import("react").RefAttributes<HTMLDivElement>>;
38
+ /** One choice out of several (the current space, a sort order). Wrap DropdownMenuRadioItems in it. */
39
+ export declare const DropdownMenuRadioGroup: import("react").ForwardRefExoticComponent<DropdownPrimitive.DropdownMenuRadioGroupProps & import("react").RefAttributes<HTMLDivElement>>;
40
+ export interface DropdownMenuRadioItemProps extends ComponentPropsWithoutRef<typeof DropdownPrimitive.RadioItem> {
41
+ /** A 16px icon or avatar before the label; the check shows at the end while this is the current choice. */
42
+ icon?: ReactNode;
43
+ }
44
+ /** One of the choices in a DropdownMenuRadioGroup. The current choice shows a check at the end. */
45
+ export declare const DropdownMenuRadioItem: import("react").ForwardRefExoticComponent<DropdownMenuRadioItemProps & import("react").RefAttributes<HTMLDivElement>>;
46
+ /** A section heading inside the menu, such as "Spaces". Not focusable. */
47
+ export declare const DropdownMenuLabel: import("react").ForwardRefExoticComponent<Omit<DropdownPrimitive.DropdownMenuLabelProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
48
+ /** A hairline between groups of items. */
49
+ export declare const DropdownMenuSeparator: import("react").ForwardRefExoticComponent<Omit<DropdownPrimitive.DropdownMenuSeparatorProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
50
+ /** Groups items for assistive technology, without visual change. */
51
+ export declare const DropdownMenuGroup: import("react").ForwardRefExoticComponent<DropdownPrimitive.DropdownMenuGroupProps & import("react").RefAttributes<HTMLDivElement>>;
52
+ /** A nested menu: a DropdownMenuSubTrigger and its DropdownMenuSubContent. */
53
+ export declare const DropdownMenuSub: import("react").FC<DropdownPrimitive.DropdownMenuSubProps>;
54
+ export interface DropdownMenuSubTriggerProps extends ComponentPropsWithoutRef<typeof DropdownPrimitive.SubTrigger> {
55
+ /** A 16px icon before the label. */
56
+ icon?: ReactNode;
57
+ }
58
+ /** The item that opens a submenu (on hover, click, or the right arrow key). */
59
+ export declare const DropdownMenuSubTrigger: import("react").ForwardRefExoticComponent<DropdownMenuSubTriggerProps & import("react").RefAttributes<HTMLDivElement>>;
60
+ /** The submenu's own menu, opening beside its trigger. */
61
+ export declare const DropdownMenuSubContent: import("react").ForwardRefExoticComponent<Omit<DropdownPrimitive.DropdownMenuSubContentProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
2
62
  export interface DropdownItem {
63
+ /** The item's text. */
3
64
  label: string;
65
+ /** Runs when the item is chosen. */
4
66
  onSelect?: () => void;
67
+ /** Shown but not choosable. */
5
68
  disabled?: boolean;
69
+ /** A 16px icon before the label. */
6
70
  icon?: ReactNode;
71
+ /** A destructive action, shown in the danger colour. */
7
72
  destructive?: boolean;
73
+ /** Draws a separator above this item. */
8
74
  separatorBefore?: boolean;
9
75
  }
10
76
  export interface DropdownMenuProps {
77
+ /** The trigger's text; when it names the current choice, a new label rises in and the trigger springs to its width. */
11
78
  label: string;
79
+ /** The actions, in order. */
12
80
  items: DropdownItem[];
81
+ /** An icon before the trigger's text. */
13
82
  icon?: ReactNode;
14
83
  }
84
+ /** The shorthand: a labelled trigger and a flat list of actions, built from the parts above. */
15
85
  export declare function DropdownMenu({ label, items, icon }: DropdownMenuProps): import("react").JSX.Element;
16
86
  export default DropdownMenu;
@@ -1,12 +1,107 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Fragment, useLayoutEffect, useRef, useState } from "react";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * Dropdown menu: a list of actions behind a trigger.
5
+ *
6
+ * Two ways to use it:
7
+ *
8
+ * // Shorthand: a labelled trigger and a flat list.
9
+ * <DropdownMenu label="Manage silicon" items={[{ label: "Rename", onSelect: rename }, …]} />
10
+ *
11
+ * // Composable (experimental): any trigger, labels, checked and radio items, shortcuts and submenus.
12
+ * <DropdownMenuRoot>
13
+ * <DropdownMenuTrigger asChild><Button variant="ghost" aria-label="Page options"><Ellipsis /></Button></DropdownMenuTrigger>
14
+ * <DropdownMenuContent align="end">
15
+ * <DropdownMenuLabel>Page</DropdownMenuLabel>
16
+ * <DropdownMenuItem icon={<Copy />} shortcut="⌘D" onSelect={duplicate}>Duplicate</DropdownMenuItem>
17
+ * <DropdownMenuCheckboxItem checked={wide} onCheckedChange={setWide}>Full width</DropdownMenuCheckboxItem>
18
+ * <DropdownMenuSub>
19
+ * <DropdownMenuSubTrigger icon={<FolderInput />}>Move to</DropdownMenuSubTrigger>
20
+ * <DropdownMenuSubContent>…</DropdownMenuSubContent>
21
+ * </DropdownMenuSub>
22
+ * <DropdownMenuSeparator />
23
+ * <DropdownMenuItem destructive icon={<Trash2 />} onSelect={remove}>Delete</DropdownMenuItem>
24
+ * </DropdownMenuContent>
25
+ * </DropdownMenuRoot>
26
+ *
27
+ * Every part shares one look: the same surface, the gliding highlight, the entrance that grows from the trigger.
28
+ */
29
+ import { Fragment, cloneElement, forwardRef, isValidElement, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
4
30
  import * as DropdownPrimitive from "@radix-ui/react-dropdown-menu";
5
31
  import { AnimatePresence, motion, useReducedMotion } from "motion/react";
6
- import { ChevronDown } from "lucide-react";
32
+ import { Check, ChevronDown, ChevronRight } from "lucide-react";
7
33
  import { MenuHighlight, useMenuHighlight } from "../../lib/menu-highlight.js";
8
34
  import { motionTokens } from "../../lib/motion-tokens.js";
9
35
  import styles from "./dropdown-menu.module.css";
36
+ const ITEMS = '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
37
+ const cx = (...names) => names.filter(Boolean).join(" ");
38
+ /** A menu surface's own ref, also handed to the caller's ref. It numbers the items in order, so their entrance
39
+ * staggers whatever groups and parts they sit in. */
40
+ function useSurfaceRef(forwarded) {
41
+ const own = useRef(null);
42
+ useImperativeHandle(forwarded, () => own.current, []);
43
+ useLayoutEffect(() => {
44
+ own.current?.querySelectorAll(ITEMS).forEach((item, index) => item.style.setProperty("--i", String(index)));
45
+ });
46
+ return own;
47
+ }
48
+ /* ------------------------------------------------------------------------------------------------------------------ */
49
+ /* Composable parts */
50
+ /* ------------------------------------------------------------------------------------------------------------------ */
51
+ /** The menu's state. Uncontrolled by default; pass `open` and `onOpenChange` to control it. */
52
+ export function DropdownMenuRoot(props) {
53
+ return _jsx(DropdownPrimitive.Root, { ...props });
54
+ }
55
+ /** What opens the menu. Without `asChild` it is the default outlined trigger with a chevron. */
56
+ export const DropdownMenuTrigger = forwardRef(function DropdownMenuTrigger({ asChild, className, children, ...props }, ref) {
57
+ if (asChild)
58
+ return _jsx(DropdownPrimitive.Trigger, { ...props, ref: ref, asChild: true, children: children });
59
+ return (_jsxs(DropdownPrimitive.Trigger, { ...props, ref: ref, type: "button", className: cx(styles.trigger, className), children: [children, _jsx(ChevronDown, { className: styles.chevron, size: 15, strokeWidth: 1.8, "aria-hidden": "true" })] }));
60
+ });
61
+ /** The menu itself. One highlight glides between items for the pointer and jumps instantly for the keyboard. */
62
+ export const DropdownMenuContent = forwardRef(function DropdownMenuContent({ className, children, side = "bottom", align = "end", sideOffset = 6, collisionPadding = 12, ...props }, ref) {
63
+ const { highlight, contentProps } = useMenuHighlight(ITEMS);
64
+ const surface = useSurfaceRef(ref);
65
+ return (_jsx(DropdownPrimitive.Portal, { children: _jsxs(DropdownPrimitive.Content, { ...props, ...contentProps, ref: surface, side: side, align: align, sideOffset: sideOffset, collisionPadding: collisionPadding, loop: true, className: cx(styles.menu, className), children: [_jsx(MenuHighlight, { state: highlight, className: styles.highlight }), children] }) }));
66
+ });
67
+ /** One action, or with `asChild` one link. */
68
+ export const DropdownMenuItem = forwardRef(function DropdownMenuItem({ icon, shortcut, destructive, asChild, className, children, ...props }, ref) {
69
+ const content = (label) => _jsxs(_Fragment, { children: [icon ? _jsx("span", { className: styles.icon, "aria-hidden": "true", children: icon }) : null, _jsx("span", { className: styles.itemLabel, children: label }), shortcut ? _jsx("kbd", { className: styles.shortcut, children: shortcut }) : null] });
70
+ const own = asChild && isValidElement(children) ? children : null;
71
+ return (_jsx(DropdownPrimitive.Item, { ...props, ref: ref, asChild: !!own, className: cx(styles.item, destructive && styles.destructive, className), "data-tone": destructive ? "danger" : undefined, children: own ? cloneElement(own, undefined, content(own.props.children)) : content(children) }));
72
+ });
73
+ /** An on/off setting, such as "Full width". A check shows in the icon column while it is on. */
74
+ export const DropdownMenuCheckboxItem = forwardRef(function DropdownMenuCheckboxItem({ shortcut, className, children, ...props }, ref) {
75
+ return (_jsxs(DropdownPrimitive.CheckboxItem, { ...props, ref: ref, className: cx(styles.item, className), children: [_jsx("span", { className: styles.icon, "aria-hidden": "true", children: _jsx(DropdownPrimitive.ItemIndicator, { children: _jsx(Check, { size: 16, strokeWidth: 2 }) }) }), _jsx("span", { className: styles.itemLabel, children: children }), shortcut ? _jsx("kbd", { className: styles.shortcut, children: shortcut }) : null] }));
76
+ });
77
+ /** One choice out of several (the current space, a sort order). Wrap DropdownMenuRadioItems in it. */
78
+ export const DropdownMenuRadioGroup = DropdownPrimitive.RadioGroup;
79
+ /** One of the choices in a DropdownMenuRadioGroup. The current choice shows a check at the end. */
80
+ export const DropdownMenuRadioItem = forwardRef(function DropdownMenuRadioItem({ icon, className, children, ...props }, ref) {
81
+ return (_jsxs(DropdownPrimitive.RadioItem, { ...props, ref: ref, className: cx(styles.item, className), children: [icon ? _jsx("span", { className: styles.icon, "aria-hidden": "true", children: icon }) : null, _jsx("span", { className: styles.itemLabel, children: children }), _jsx("span", { className: styles.indicator, "aria-hidden": "true", children: _jsx(DropdownPrimitive.ItemIndicator, { children: _jsx(Check, { size: 16, strokeWidth: 2 }) }) })] }));
82
+ });
83
+ /** A section heading inside the menu, such as "Spaces". Not focusable. */
84
+ export const DropdownMenuLabel = forwardRef(function DropdownMenuLabel({ className, ...props }, ref) {
85
+ return _jsx(DropdownPrimitive.Label, { ...props, ref: ref, className: cx(styles.sectionLabel, className) });
86
+ });
87
+ /** A hairline between groups of items. */
88
+ export const DropdownMenuSeparator = forwardRef(function DropdownMenuSeparator({ className, ...props }, ref) {
89
+ return _jsx(DropdownPrimitive.Separator, { ...props, ref: ref, className: cx(styles.separator, className) });
90
+ });
91
+ /** Groups items for assistive technology, without visual change. */
92
+ export const DropdownMenuGroup = DropdownPrimitive.Group;
93
+ /** A nested menu: a DropdownMenuSubTrigger and its DropdownMenuSubContent. */
94
+ export const DropdownMenuSub = DropdownPrimitive.Sub;
95
+ /** The item that opens a submenu (on hover, click, or the right arrow key). */
96
+ export const DropdownMenuSubTrigger = forwardRef(function DropdownMenuSubTrigger({ icon, className, children, ...props }, ref) {
97
+ return (_jsxs(DropdownPrimitive.SubTrigger, { ...props, ref: ref, className: cx(styles.item, styles.subTrigger, className), children: [icon ? _jsx("span", { className: styles.icon, "aria-hidden": "true", children: icon }) : null, _jsx("span", { className: styles.itemLabel, children: children }), _jsx(ChevronRight, { className: styles.subChevron, size: 15, strokeWidth: 1.8, "aria-hidden": "true" })] }));
98
+ });
99
+ /** The submenu's own menu, opening beside its trigger. */
100
+ export const DropdownMenuSubContent = forwardRef(function DropdownMenuSubContent({ className, children, sideOffset = 6, collisionPadding = 12, ...props }, ref) {
101
+ const { highlight, contentProps } = useMenuHighlight(ITEMS);
102
+ const surface = useSurfaceRef(ref);
103
+ return (_jsx(DropdownPrimitive.Portal, { children: _jsxs(DropdownPrimitive.SubContent, { ...props, ...contentProps, ref: surface, sideOffset: sideOffset, collisionPadding: collisionPadding, loop: true, className: cx(styles.menu, className), children: [_jsx(MenuHighlight, { state: highlight, className: styles.highlight }), children] }) }));
104
+ });
10
105
  /** A new trigger label rises in while the old one leaves, and the trigger width springs to the measured text instead of snapping. */
11
106
  function TriggerLabel({ text }) {
12
107
  const reduced = useReducedMotion();
@@ -31,9 +126,8 @@ function TriggerLabel({ text }) {
31
126
  }, []);
32
127
  return _jsxs(motion.span, { className: styles.label, initial: false, animate: { width: size.width }, transition: size.animate && !reduced ? motionTokens.spring.morph : { duration: 0 }, children: [_jsx("span", { ref: measure, className: styles.labelMeasure, "aria-hidden": "true", children: text }), _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(motion.span, { className: styles.labelText, initial: reduced ? false : { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } }, transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }, children: text }, text) })] });
33
128
  }
129
+ /** The shorthand: a labelled trigger and a flat list of actions, built from the parts above. */
34
130
  export function DropdownMenu({ label, items, icon }) {
35
- const { highlight, reset, contentProps } = useMenuHighlight();
36
- return _jsxs(DropdownPrimitive.Root, { onOpenChange: open => { if (open)
37
- reset(); }, children: [_jsxs(DropdownPrimitive.Trigger, { className: styles.trigger, type: "button", children: [icon && _jsx("span", { className: styles.triggerIcon, "aria-hidden": "true", children: icon }), _jsx(TriggerLabel, { text: label }), _jsx(ChevronDown, { className: styles.chevron, size: 15, strokeWidth: 1.8, "aria-hidden": "true" })] }), _jsx(DropdownPrimitive.Portal, { children: _jsxs(DropdownPrimitive.Content, { className: styles.menu, sideOffset: 6, align: "end", collisionPadding: 12, loop: true, ...contentProps, children: [_jsx(MenuHighlight, { state: highlight, className: styles.highlight }), items.map((item, index) => _jsxs(Fragment, { children: [item.separatorBefore && _jsx(DropdownPrimitive.Separator, { className: styles.separator }), _jsxs(DropdownPrimitive.Item, { className: [styles.item, item.destructive ? styles.destructive : ""].filter(Boolean).join(" "), "data-tone": item.destructive ? "danger" : undefined, style: { "--i": index }, disabled: item.disabled, onSelect: item.onSelect, children: [item.icon && _jsx("span", { className: styles.icon, "aria-hidden": "true", children: item.icon }), item.label] })] }, item.label))] }) })] });
131
+ return (_jsxs(DropdownMenuRoot, { children: [_jsxs(DropdownMenuTrigger, { children: [icon ? _jsx("span", { className: styles.triggerIcon, "aria-hidden": "true", children: icon }) : null, _jsx(TriggerLabel, { text: label })] }), _jsx(DropdownMenuContent, { children: items.map(item => (_jsxs(Fragment, { children: [item.separatorBefore ? _jsx(DropdownMenuSeparator, {}) : null, _jsx(DropdownMenuItem, { icon: item.icon, destructive: item.destructive, disabled: item.disabled, onSelect: item.onSelect, children: item.label })] }, item.label))) })] }));
38
132
  }
39
133
  export default DropdownMenu;
@@ -12,7 +12,7 @@
12
12
  .trigger[data-state="open"] .chevron { transform: rotate(180deg); }
13
13
  /* The menu grows from the trigger edge: offset toward the trigger, scale from the Radix origin, fade quickly.
14
14
  Transitions instead of keyframes, so a close that interrupts the open (or a reopen during the close) reverses from where it is. */
15
- .menu { --menu-x: 0px; --menu-y: -5px; position: relative; z-index: 60; min-width: min(12rem, var(--radix-dropdown-menu-content-available-width)); max-width: var(--radix-dropdown-menu-content-available-width); padding: 5px; border: 1px solid var(--border); border-radius: var(--radius-container); background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: var(--radix-dropdown-menu-content-transform-origin); transition: opacity var(--duration-fast) var(--ease-enter), transform var(--duration-spring) var(--ease-spring); }
15
+ .menu { --menu-x: 0px; --menu-y: -5px; position: relative; z-index: var(--layer-dropdown); min-width: min(12rem, var(--radix-dropdown-menu-content-available-width)); max-width: var(--radix-dropdown-menu-content-available-width); padding: var(--space-tight); border: 1px solid var(--border); border-radius: var(--radius-container); background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: var(--radix-dropdown-menu-content-transform-origin); transition: opacity var(--duration-fast) var(--ease-enter), transform var(--duration-spring) var(--ease-spring); }
16
16
  .menu[data-side="top"] { --menu-y: 5px; }
17
17
  .menu[data-side="left"] { --menu-x: 5px; --menu-y: 0px; }
18
18
  .menu[data-side="right"] { --menu-x: -5px; --menu-y: 0px; }
@@ -21,11 +21,21 @@
21
21
  .menu[data-state="open"] .item { opacity: 0; transform: translate(calc(var(--menu-x) * .4), calc(var(--menu-y) * .4)); }
22
22
  }
23
23
  /* Radix unmounts when the exit animation ends, so a no-op keyframe times the exit while the transition does the visual work. */
24
- .menu[data-state="closed"] { opacity: 0; transform: translate(calc(var(--menu-x) * .5), calc(var(--menu-y) * .5)) scale(.985); transition: opacity 130ms var(--ease-standard), transform 130ms var(--ease-standard); animation: menu-exit 130ms linear both; pointer-events: none; }
25
- .highlight { position: absolute; top: 0; right: 5px; left: 5px; border-radius: calc(var(--radius-container) - 6px); background: var(--surface-muted); opacity: 0; pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); }
24
+ .menu[data-state="closed"] { opacity: 0; transform: translate(calc(var(--menu-x) * .5), calc(var(--menu-y) * .5)) scale(.985); transition: opacity var(--duration-instant) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); animation: menu-exit var(--duration-instant) linear both; pointer-events: none; }
25
+ .highlight { position: absolute; top: 0; right: var(--space-tight); left: var(--space-tight); border-radius: calc(var(--radius-container) - var(--space-related)); background: var(--surface-muted); opacity: 0; pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); }
26
26
  .highlight[data-tone="danger"] { background: color-mix(in oklab, var(--danger) 8%, var(--surface)); }
27
- .item { position: relative; display: flex; min-height: 36px; align-items: center; gap: 10px; padding: 0 11px; border-radius: calc(var(--radius-container) - 6px); color: var(--foreground); font-size: var(--text-sm); cursor: pointer; outline: none; transition: color var(--duration-fast) var(--ease-standard), opacity var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * 35ms), transform var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * 35ms); }
28
- .icon { display: inline-flex; width: 17px; color: var(--text-secondary); }
27
+ /* An item may be a link (asChild), which keeps the item's look. */
28
+ .item { position: relative; display: flex; min-height: 36px; align-items: center; gap: var(--space-group); padding: 0 var(--space-group); border-radius: calc(var(--radius-container) - var(--space-related)); color: var(--foreground); font-size: var(--text-sm); text-decoration: none; cursor: pointer; outline: none; transition: color var(--duration-fast) var(--ease-standard), opacity var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * var(--stagger-item)), transform var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * var(--stagger-item)); }
29
+ /* At least the 16px icon's column; wider content (an avatar) takes the room it needs. */
30
+ .icon { display: inline-flex; min-width: 17px; flex: none; color: var(--text-secondary); }
31
+ /* The composable parts. The label takes the free width, so a shortcut or check sits at the end of the row. */
32
+ .itemLabel { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
33
+ .shortcut { flex: none; padding-left: var(--space-group); color: var(--text-muted); font: inherit; font-size: var(--text-xs); letter-spacing: var(--tracking-body); }
34
+ .indicator { display: inline-flex; flex: none; color: var(--foreground); }
35
+ .sectionLabel { padding: var(--space-related) var(--space-group) var(--space-tight); color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; }
36
+ .subChevron { flex: none; color: var(--text-muted); }
37
+ /* While its submenu is open, the item that opened it keeps a quiet fill, so the path stays visible. */
38
+ .subTrigger[data-state="open"] { background: var(--surface-muted); }
29
39
  .separator { height: 1px; margin: var(--space-tight) calc(var(--space-tight) * -1); background: var(--border-subtle); }
30
40
  .destructive { color: var(--danger); }
31
41
  .destructive .icon { color: var(--danger); }
@@ -109,7 +109,7 @@ export function ElasticSlider({ label, value, defaultValue, onValueChange, onVal
109
109
  function pulse(mark, haptic) {
110
110
  const node = tickRefs.current.get(mark);
111
111
  if (node && !reduced)
112
- animate(node, { scale: [1, 2.1, 1] }, { duration: .34, ease: enterEase });
112
+ animate(node, { scale: [1, 2.1, 1] }, { duration: motionTokens.duration.standard, ease: enterEase });
113
113
  if (haptic && typeof navigator !== "undefined" && "vibrate" in navigator)
114
114
  navigator.vibrate?.(6);
115
115
  }
@@ -255,7 +255,7 @@ export function ElasticSlider({ label, value, defaultValue, onValueChange, onVal
255
255
  return _jsxs("div", { className: [styles.root, className].filter(Boolean).join(" "), "data-disabled": disabled || undefined, "data-dragging": dragging || undefined, "data-tag": bubble || undefined, "data-marks": labelled.length > 0 || undefined, children: [_jsxs("div", { className: styles.header, children: [_jsx("span", { id: labelId, className: styles.label, children: label }), showValue && _jsx("span", { className: styles.readout, "aria-hidden": "true", children: text })] }), _jsxs("div", { className: styles.body, children: [startIcon && _jsx(motion.span, { className: styles.start, style: { x: startShift }, "aria-hidden": "true", children: startIcon }), _jsxs("div", { className: styles.control, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerEnd, onPointerCancel: onPointerEnd, onLostPointerCapture: onPointerEnd, onMouseDown: event => event.preventDefault(), children: [_jsxs("div", { ref: trackRef, className: styles.rail, children: [_jsx(motion.div, { className: styles.track, style: { scaleX, scaleY, originX }, children: _jsx(motion.div, { className: styles.fill, style: { clipPath } }) }), markList.filter(mark => mark.value > min && mark.value < max).map(mark => _jsx("span", { ref: node => { if (node)
256
256
  tickRefs.current.set(mark.value, node);
257
257
  else
258
- tickRefs.current.delete(mark.value); }, className: styles.tick, "data-on": mark.value <= current || undefined, style: { left: `${pct(mark.value)}%` } }, mark.value))] }), _jsxs("div", { className: styles.thumbs, children: [_jsx(motion.div, { className: styles.layer, style: { x: bubbleX }, children: _jsx(motion.span, { className: styles.bubbleAnchor, style: { x: over, "--at": bubbleAt }, children: _jsx(AnimatePresence, { children: bubble && _jsx(motion.span, { className: styles.bubble, "aria-hidden": "true", style: { rotate }, initial: reduced ? { opacity: 0 } : bubbleIn, animate: bubbleRest, exit: reduced ? { opacity: 0, transition: { duration: duration.instant } } : bubbleOut, transition: reduced ? { duration: .15 } : bubbleEnter, children: text }, "bubble") }) }) }), _jsx(motion.div, { className: styles.layer, style: { x: thumbX }, children: _jsx(motion.div, { className: styles.shift, style: { x: over }, children: _jsx(motion.div, { ref: thumbRef, role: "slider", "data-thumb": "", className: styles.thumb, tabIndex: disabled ? -1 : 0, "aria-labelledby": labelId, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": current, "aria-valuetext": text, "aria-orientation": "horizontal", "aria-disabled": disabled || undefined, initial: false, animate: { scale: dragging ? 1.28 : 1 }, transition: reduced ? { duration: 0 } : spring.snappy, onKeyDown: onKeyDown, onBlur: () => setKeyed(false) }) }) })] })] }), endIcon && _jsx(motion.span, { className: styles.end, style: { x: endShift }, "aria-hidden": "true", children: endIcon }), labelled.length > 0 && _jsx("div", { className: styles.marks, "aria-hidden": "true", children: labelled.map(mark => _jsx("span", { className: styles.markLabel, "data-on": mark.value === current || undefined, "data-edge": mark.value === min ? "start" : mark.value === max ? "end" : undefined, style: { left: `${pct(mark.value)}%` }, onClick: () => { if (disabled || mark.value === latest.current)
258
+ tickRefs.current.delete(mark.value); }, className: styles.tick, "data-on": mark.value <= current || undefined, style: { left: `${pct(mark.value)}%` } }, mark.value))] }), _jsxs("div", { className: styles.thumbs, children: [_jsx(motion.div, { className: styles.layer, style: { x: bubbleX }, children: _jsx(motion.span, { className: styles.bubbleAnchor, style: { x: over, "--at": bubbleAt }, children: _jsx(AnimatePresence, { children: bubble && _jsx(motion.span, { className: styles.bubble, "aria-hidden": "true", style: { rotate }, initial: reduced ? { opacity: 0 } : bubbleIn, animate: bubbleRest, exit: reduced ? { opacity: 0, transition: { duration: duration.instant } } : bubbleOut, transition: reduced ? { duration: motionTokens.duration.fast } : bubbleEnter, children: text }, "bubble") }) }) }), _jsx(motion.div, { className: styles.layer, style: { x: thumbX }, children: _jsx(motion.div, { className: styles.shift, style: { x: over }, children: _jsx(motion.div, { ref: thumbRef, role: "slider", "data-thumb": "", className: styles.thumb, tabIndex: disabled ? -1 : 0, "aria-labelledby": labelId, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": current, "aria-valuetext": text, "aria-orientation": "horizontal", "aria-disabled": disabled || undefined, initial: false, animate: { scale: dragging ? 1.28 : 1 }, transition: reduced ? { duration: 0 } : spring.snappy, onKeyDown: onKeyDown, onBlur: () => setKeyed(false) }) }) })] })] }), endIcon && _jsx(motion.span, { className: styles.end, style: { x: endShift }, "aria-hidden": "true", children: endIcon }), labelled.length > 0 && _jsx("div", { className: styles.marks, "aria-hidden": "true", children: labelled.map(mark => _jsx("span", { className: styles.markLabel, "data-on": mark.value === current || undefined, "data-edge": mark.value === min ? "start" : mark.value === max ? "end" : undefined, style: { left: `${pct(mark.value)}%` }, onClick: () => { if (disabled || mark.value === latest.current)
259
259
  return; emit(mark.value); pulse(mark.value, false); onValueCommit?.(mark.value); }, children: mark.label }, mark.value)) })] }), name && _jsx("input", { type: "hidden", name: name, value: current, disabled: disabled })] });
260
260
  }
261
261
  export default ElasticSlider;