@flikk/ui 1.0.0-beta.23 → 1.0.0-beta.24

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 (142) hide show
  1. package/dist/components/charts/AreaChart/AreaChart.types.d.ts +2 -2
  2. package/dist/components/charts/BarChart/BarChart.types.d.ts +2 -2
  3. package/dist/components/charts/ComboChart/ComboChart.types.d.ts +2 -2
  4. package/dist/components/charts/LineChart/LineChart.types.d.ts +2 -2
  5. package/dist/components/charts/ScatterPlot/ScatterPlot.types.d.ts +2 -2
  6. package/dist/components/charts/StackedBarChart/StackedBarChart.types.d.ts +2 -2
  7. package/dist/components/core/Accordion/AccordionTrigger.js +28 -1
  8. package/dist/components/core/Alert/Alert.js +2 -1
  9. package/dist/components/core/AlertDialog/AlertDialog.js +1 -1
  10. package/dist/components/core/Badge/Badge.js +6 -17
  11. package/dist/components/core/Breadcrumbs/Breadcrumbs.js +17 -28
  12. package/dist/components/core/Calendar/Calendar.js +1 -1
  13. package/dist/components/core/Carousel/CarouselBody.js +6 -1
  14. package/dist/components/core/ContextMenu/ContextMenuBody.js +6 -3
  15. package/dist/components/core/ContextMenu/ContextMenuSubBody.js +6 -3
  16. package/dist/components/core/DragDrop/DragDrop.js +170 -17
  17. package/dist/components/core/DragDrop/DragDrop.types.d.ts +6 -0
  18. package/dist/components/core/Drawer/Drawer.d.ts +1 -1
  19. package/dist/components/core/Drawer/Drawer.js +2 -0
  20. package/dist/components/core/Drawer/Drawer.types.d.ts +2 -0
  21. package/dist/components/core/Drawer/DrawerTitle.d.ts +1 -1
  22. package/dist/components/core/Drawer/DrawerTitle.js +2 -2
  23. package/dist/components/core/Dropdown/DropdownMenu.js +2 -30
  24. package/dist/components/core/Masonry/Masonry.js +7 -2
  25. package/dist/components/core/Modal/Modal.d.ts +1 -1
  26. package/dist/components/core/Modal/Modal.js +3 -3
  27. package/dist/components/core/Modal/Modal.types.d.ts +2 -0
  28. package/dist/components/core/Modal/ModalTitle.d.ts +1 -1
  29. package/dist/components/core/Modal/ModalTitle.js +2 -2
  30. package/dist/components/core/NavItem/NavItem.js +13 -8
  31. package/dist/components/core/Pagination/Pagination.js +1 -1
  32. package/dist/components/core/Popover/PopoverBody.js +3 -27
  33. package/dist/components/core/Popover/PopoverTrigger.js +1 -1
  34. package/dist/components/core/Progress/Progress.js +4 -4
  35. package/dist/components/core/Rating/Rating.js +1 -1
  36. package/dist/components/core/ScrollArea/ScrollArea.js +3 -2
  37. package/dist/components/core/Segmented/Segmented.js +6 -6
  38. package/dist/components/core/Segmented/SegmentedItem.js +28 -2
  39. package/dist/components/core/Skeleton/Skeleton.js +2 -2
  40. package/dist/components/core/Sortable/Sortable.d.ts +1 -1
  41. package/dist/components/core/Sortable/Sortable.js +20 -4
  42. package/dist/components/core/Sortable/Sortable.types.d.ts +2 -0
  43. package/dist/components/core/Tabs/Tabs.js +6 -4
  44. package/dist/components/core/Tabs/Tabs.types.d.ts +2 -0
  45. package/dist/components/core/Tabs/TabsBody.js +3 -3
  46. package/dist/components/core/Tabs/TabsList.js +19 -21
  47. package/dist/components/core/Tabs/TabsTrigger.js +47 -28
  48. package/dist/components/core/Toast/Toast.js +1 -1
  49. package/dist/components/core/Toast/ToastProvider.js +3 -2
  50. package/dist/components/core/Tooltip/Tooltip.js +25 -34
  51. package/dist/components/core/Tree/Tree.js +178 -8
  52. package/dist/components/data-display/GanttChart/GanttSplitter.js +15 -1
  53. package/dist/components/data-display/Table/Table.js +2 -2
  54. package/dist/components/data-display/Table/Table.types.d.ts +2 -0
  55. package/dist/components/data-display/Table/TableFilter.js +21 -18
  56. package/dist/components/effects/CustomCursor/CustomCursor.js +3 -7
  57. package/dist/components/effects/Overlay/Overlay.js +4 -9
  58. package/dist/components/effects/PageTransition/PageTransition.js +17 -6
  59. package/dist/components/effects/SpotlightBorder/SpotlightBorder.js +3 -4
  60. package/dist/components/forms/Checkbox/Checkbox.js +11 -1
  61. package/dist/components/forms/Checkbox/Checkbox.theme.js +4 -1
  62. package/dist/components/forms/Checkbox/Checkbox.types.d.ts +7 -5
  63. package/dist/components/forms/Checkbox/CheckboxGroup.js +9 -4
  64. package/dist/components/forms/ColorPicker/ColorPicker.js +7 -4
  65. package/dist/components/forms/ColorPicker/ColorPicker.types.d.ts +2 -2
  66. package/dist/components/forms/ColorPicker/ColorPickerBody.js +5 -3
  67. package/dist/components/forms/ColorPicker/ColorPickerEyeDropper.js +3 -3
  68. package/dist/components/forms/ColorPicker/ColorPickerGradient.js +8 -8
  69. package/dist/components/forms/ColorPicker/ColorPickerInput.js +1 -1
  70. package/dist/components/forms/ColorPicker/index.d.ts +1 -1
  71. package/dist/components/forms/Combobox/Combobox.js +20 -45
  72. package/dist/components/forms/Combobox/Combobox.types.d.ts +4 -2
  73. package/dist/components/forms/CronInput/CronInput.js +10 -4
  74. package/dist/components/forms/CronInput/CronInput.types.d.ts +12 -1
  75. package/dist/components/forms/DatePicker/DatePicker.js +2 -2
  76. package/dist/components/forms/DatePicker/DatePicker.types.d.ts +4 -2
  77. package/dist/components/forms/DatePicker/DatePickerBody.js +1 -29
  78. package/dist/components/forms/DatePicker/DatePickerTrigger.js +2 -2
  79. package/dist/components/forms/DateRangePicker/DateRangePicker.js +3 -3
  80. package/dist/components/forms/DateRangePicker/DateRangePicker.types.d.ts +4 -2
  81. package/dist/components/forms/DateRangePicker/DateRangePickerBody.js +1 -28
  82. package/dist/components/forms/DateRangePicker/DateRangePickerTrigger.js +2 -2
  83. package/dist/components/forms/FileUpload/FileUpload.js +14 -10
  84. package/dist/components/forms/FileUpload/FileUpload.types.d.ts +15 -0
  85. package/dist/components/forms/FormLabel/FormLabel.js +2 -2
  86. package/dist/components/forms/Input/Input.js +5 -3
  87. package/dist/components/forms/Input/Input.types.d.ts +2 -0
  88. package/dist/components/forms/InputAddress/InputAddress.d.ts +1 -1
  89. package/dist/components/forms/InputAddress/InputAddress.js +8 -9
  90. package/dist/components/forms/InputCounter/InputCounter.js +25 -18
  91. package/dist/components/forms/InputCounter/InputCounter.types.d.ts +8 -1
  92. package/dist/components/forms/InputCreditCard/InputCreditCard.d.ts +1 -1
  93. package/dist/components/forms/InputCreditCard/InputCreditCard.js +27 -9
  94. package/dist/components/forms/InputCreditCard/InputCreditCard.types.d.ts +5 -0
  95. package/dist/components/forms/InputOTP/InputOTP.js +26 -17
  96. package/dist/components/forms/InputOTP/InputOTP.types.d.ts +21 -4
  97. package/dist/components/forms/InputTag/InputTag.js +12 -39
  98. package/dist/components/forms/InputTag/InputTag.types.d.ts +2 -2
  99. package/dist/components/forms/Mention/Mention.js +13 -6
  100. package/dist/components/forms/Mention/Mention.types.d.ts +9 -0
  101. package/dist/components/forms/Radio/Radio.js +12 -2
  102. package/dist/components/forms/Radio/Radio.theme.js +5 -1
  103. package/dist/components/forms/Radio/Radio.types.d.ts +7 -5
  104. package/dist/components/forms/Radio/RadioGroup.js +8 -3
  105. package/dist/components/forms/RichTextEditor/RichTextEditor.js +21 -21
  106. package/dist/components/forms/RichTextEditor/RichTextEditor.types.d.ts +2 -2
  107. package/dist/components/forms/Select/Select.d.ts +2 -0
  108. package/dist/components/forms/Select/Select.js +26 -56
  109. package/dist/components/forms/Select/Select.types.d.ts +14 -0
  110. package/dist/components/forms/Signature/Signature.js +1 -1
  111. package/dist/components/forms/Signature/Signature.types.d.ts +3 -3
  112. package/dist/components/forms/Slider/Slider.js +18 -4
  113. package/dist/components/forms/Slider/Slider.types.d.ts +2 -2
  114. package/dist/components/forms/Switch/Switch.js +12 -19
  115. package/dist/components/forms/Switch/Switch.theme.js +12 -2
  116. package/dist/components/forms/Switch/Switch.types.d.ts +20 -2
  117. package/dist/components/forms/Textarea/Textarea.js +8 -3
  118. package/dist/components/forms/Textarea/Textarea.types.d.ts +2 -0
  119. package/dist/components/forms/TimePicker/TimePicker.js +1 -1
  120. package/dist/components/forms/TimePicker/TimePicker.types.d.ts +6 -2
  121. package/dist/components/forms/TimePicker/TimePickerContent.js +2 -29
  122. package/dist/components/forms/TimePicker/TimePickerTrigger.js +2 -2
  123. package/dist/components/forms/index.js +1 -1
  124. package/dist/components/layout/index.js +4 -0
  125. package/dist/hooks/index.d.ts +1 -0
  126. package/dist/hooks/useClickOutside.d.ts +10 -4
  127. package/dist/hooks/useClickOutside.js +44 -24
  128. package/dist/hooks/useDarkMode.d.ts +1 -0
  129. package/dist/hooks/useDarkMode.js +42 -0
  130. package/dist/hooks/useFormStateMachine.d.ts +1 -1
  131. package/dist/hooks/useNetworkStatus.js +1 -1
  132. package/dist/hooks/useOverlay.js +4 -0
  133. package/dist/hooks/useScaleBackground.js +5 -2
  134. package/dist/hooks/useSelectPortal.js +14 -5
  135. package/dist/index.d.ts +0 -2
  136. package/dist/index.js +1 -57
  137. package/dist/shadcn-compat.css +80 -0
  138. package/dist/styles.css +1 -1
  139. package/dist/utils/stateMachine.d.ts +4 -4
  140. package/package.json +11 -10
  141. package/tailwind.preset.cjs +32 -3
  142. /package/{src/theme-plugin.css → dist/theme.css} +0 -0
@@ -35,7 +35,7 @@ const PROGRESS_SPRING_TRANSITION = {
35
35
  * // Indeterminate (loading)
36
36
  * <Progress />
37
37
  */
38
- const Progress = React__default.forwardRef(({ value, size = "md", showLabel = false, labelPosition = "right", formatLabel, animated = true, wrapperClassName, className, theme: customTheme, ...props }, ref) => {
38
+ const Progress = React__default.forwardRef(({ value, size = "md", showLabel = false, labelPosition = "right", formatLabel, animated = true, wrapperClassName, className, theme: customTheme, 'aria-label': consumerAriaLabel, ...props }, ref) => {
39
39
  const shouldReduceMotion = useReducedMotion();
40
40
  // Merge custom theme with default theme
41
41
  const theme = useMemo(() => {
@@ -98,11 +98,11 @@ const Progress = React__default.forwardRef(({ value, size = "md", showLabel = fa
98
98
  const renderTrack = () => (jsxs("div", { className: cn(theme.trackStyle, theme.sizes[size], "relative", wrapperClassName), children: [isIndeterminate ? (
99
99
  // Indeterminate animation
100
100
  jsx("div", { className: cn(theme.fillStyle, "absolute", theme.indeterminateStyle, className) })) : shouldAnimate ? (
101
- // Animated determinate progress with spring animation
102
- jsx(motion.div, { className: cn(theme.fillStyle, className), initial: { width: 0 }, animate: { width: `${percentage}%` }, transition: PROGRESS_SPRING_TRANSITION })) : (
101
+ // Animated determinate progress with spring animation (scaleX avoids layout recalculation)
102
+ jsx(motion.div, { className: cn(theme.fillStyle, className), style: { width: '100%', transformOrigin: 'left' }, initial: { scaleX: 0 }, animate: { scaleX: percentage / 100 }, transition: PROGRESS_SPRING_TRANSITION })) : (
103
103
  // Static determinate progress
104
104
  jsx("div", { className: cn(theme.fillStyle, className), style: { width: `${percentage}%` } })), showLabel && labelPosition === "inside" && size === "lg" && !isIndeterminate && (jsx("span", { className: theme.insideLabelStyle, children: label }))] }));
105
- return (jsxs("div", { ref: ref, className: cn(theme.containerStyle, containerLayoutClass), role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": isIndeterminate ? undefined : percentage, "aria-label": isIndeterminate ? "Loading" : `${percentage}% complete`, ...props, children: [labelPosition === "top" && renderLabel(), labelPosition === "right" ? (jsxs(Fragment, { children: [jsx("div", { className: "flex-1", children: renderTrack() }), renderLabel()] })) : (renderTrack()), labelPosition === "bottom" && renderLabel()] }));
105
+ return (jsxs("div", { ref: ref, className: cn(theme.containerStyle, containerLayoutClass), role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": isIndeterminate ? undefined : percentage, "aria-label": consumerAriaLabel !== null && consumerAriaLabel !== void 0 ? consumerAriaLabel : (isIndeterminate ? "Loading" : `${percentage}% complete`), ...props, children: [labelPosition === "top" && renderLabel(), labelPosition === "right" ? (jsxs(Fragment, { children: [jsx("div", { className: "flex-1", children: renderTrack() }), renderLabel()] })) : (renderTrack()), labelPosition === "bottom" && renderLabel()] }));
106
106
  });
107
107
  Progress.displayName = "Progress";
108
108
 
@@ -160,7 +160,7 @@ const Rating = React__default.memo(React__default.forwardRef(({ value: controlle
160
160
  return (jsx("span", { className: "inline-flex items-center justify-center", children: renderStarIcon() }, starIndex));
161
161
  }
162
162
  // Interactive: render as a button
163
- return (jsx("button", { type: "button", className: cn(theme.starStyle, "hover:scale-110 active:scale-95"), onClick: (e) => handleStarClick(starIndex, e), onMouseMove: (e) => handleStarMouseMove(starIndex, e), "aria-label": `Rate ${starIndex} out of ${max}`, tabIndex: -1, children: renderStarIcon() }, starIndex));
163
+ return (jsx("button", { type: "button", className: cn(theme.starStyle, "hover:scale-110 active:scale-95"), onClick: (e) => handleStarClick(starIndex, e), onMouseMove: (e) => handleStarMouseMove(starIndex, e), "aria-label": `Rate ${starIndex} out of ${max}`, tabIndex: -1, "aria-hidden": "true", children: renderStarIcon() }, starIndex));
164
164
  };
165
165
  // Build array of star indices (1-based)
166
166
  const stars = useMemo(() => Array.from({ length: max }, (_, i) => i + 1), [max]);
@@ -50,8 +50,9 @@ const ScrollArea = React__default.forwardRef(({ children, type = 'hover', size =
50
50
  const smoothAPI = useSmoothScroll(smooth ? internalRef : null, smoothScrollConfig, smoothOrientation);
51
51
  // Expose extended API via ref
52
52
  useImperativeHandle(ref, () => {
53
- if (!internalRef.current)
54
- return null;
53
+ // internalRef.current is always set by the time the imperative handle is
54
+ // accessed (refs are resolved after mount). The non-null assertion avoids
55
+ // the unsafe `null as unknown as ScrollAreaRef` double-cast.
55
56
  const element = internalRef.current;
56
57
  // Create extended ref object
57
58
  const extendedRef = element;
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
- import React__default, { useRef, useState, useEffect, useMemo } from 'react';
2
+ import React__default, { useRef, useState, useCallback, useEffect, useMemo } from 'react';
3
3
  import { cn } from '../../../utils/cn.js';
4
4
  import { debounce } from '../../../utils/debounce.js';
5
5
  import { segmentedTheme } from './Segmented.theme.js';
@@ -10,11 +10,11 @@ import { SegmentedItem } from './SegmentedItem.js';
10
10
  * Segmented component for selecting one option from a set of segments
11
11
  * Uses compound component pattern with Segmented.Item
12
12
  */
13
- const SegmentedComponent = React__default.forwardRef(({ value, onChange, size = "md", disabled = false, block = false, children, className, slidingBackgroundClassName, theme, ...props }, ref) => {
13
+ const SegmentedComponent = React__default.forwardRef(({ value, onChange, size = "md", disabled = false, block = false, children, className, slidingBackgroundClassName, theme, "aria-label": ariaLabel, ...props }, ref) => {
14
14
  var _a;
15
15
  const containerRef = useRef(null);
16
16
  const [activeStyle, setActiveStyle] = useState({});
17
- const updateActiveStyle = () => {
17
+ const updateActiveStyle = useCallback(() => {
18
18
  if (!containerRef.current || !value)
19
19
  return;
20
20
  const activeButton = containerRef.current.querySelector(`[data-value="${value}"]`);
@@ -28,10 +28,10 @@ const SegmentedComponent = React__default.forwardRef(({ value, onChange, size =
28
28
  transform: `translateX(${buttonRect.left - containerRect.left}px)`,
29
29
  });
30
30
  }
31
- };
31
+ }, [value]);
32
32
  useEffect(() => {
33
33
  updateActiveStyle();
34
- }, [value]);
34
+ }, [value, updateActiveStyle]);
35
35
  // Debounced resize handler for better performance
36
36
  const debouncedUpdateActiveStyle = useMemo(() => debounce(updateActiveStyle, 100), [updateActiveStyle]);
37
37
  useEffect(() => {
@@ -52,7 +52,7 @@ const SegmentedComponent = React__default.forwardRef(({ value, onChange, size =
52
52
  else if (ref) {
53
53
  ref.current = node;
54
54
  }
55
- }, role: "tablist", "aria-orientation": "horizontal", className: cn((theme === null || theme === void 0 ? void 0 : theme.baseStyle) || segmentedTheme.baseStyle, ((_a = theme === null || theme === void 0 ? void 0 : theme.sizes) === null || _a === void 0 ? void 0 : _a[size]) || segmentedTheme.sizes[size], block && ((theme === null || theme === void 0 ? void 0 : theme.blockStyle) || segmentedTheme.blockStyle), disabled && ((theme === null || theme === void 0 ? void 0 : theme.disabledStyle) || segmentedTheme.disabledStyle), className), "data-size": size, "data-disabled": disabled, "data-block": block, ...props, children: [value && (jsx("div", { className: cn((theme === null || theme === void 0 ? void 0 : theme.slidingBackgroundStyle) ||
55
+ }, role: "radiogroup", "aria-label": ariaLabel, className: cn((theme === null || theme === void 0 ? void 0 : theme.baseStyle) || segmentedTheme.baseStyle, ((_a = theme === null || theme === void 0 ? void 0 : theme.sizes) === null || _a === void 0 ? void 0 : _a[size]) || segmentedTheme.sizes[size], block && ((theme === null || theme === void 0 ? void 0 : theme.blockStyle) || segmentedTheme.blockStyle), disabled && ((theme === null || theme === void 0 ? void 0 : theme.disabledStyle) || segmentedTheme.disabledStyle), className), "data-size": size, "data-disabled": disabled, "data-block": block, ...props, children: [value && (jsx("div", { className: cn((theme === null || theme === void 0 ? void 0 : theme.slidingBackgroundStyle) ||
56
56
  segmentedTheme.slidingBackgroundStyle, slidingBackgroundClassName), style: activeStyle })), children] }) }));
57
57
  });
58
58
  // Set display name for dev tools and Storybook
@@ -18,10 +18,36 @@ const SegmentedItem = React__default.forwardRef(({ value, disabled = false, chil
18
18
  onChange === null || onChange === void 0 ? void 0 : onChange(value);
19
19
  onClick === null || onClick === void 0 ? void 0 : onClick(event);
20
20
  };
21
- return (jsx("button", { ref: ref, type: "button", role: "tab", "aria-selected": isSelected, disabled: isDisabled, className: cn(segmentedItemTheme.baseStyle, segmentedItemTheme.sizes[size], isSelected
21
+ const handleKeyDown = (event) => {
22
+ if (isDisabled)
23
+ return;
24
+ const radioGroup = event.currentTarget.parentElement;
25
+ if (!radioGroup)
26
+ return;
27
+ const radios = Array.from(radioGroup.querySelectorAll('[role="radio"]:not([disabled])'));
28
+ const currentIndex = radios.indexOf(event.currentTarget);
29
+ let targetRadio;
30
+ if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
31
+ event.preventDefault();
32
+ targetRadio = radios[(currentIndex + 1) % radios.length];
33
+ }
34
+ else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
35
+ event.preventDefault();
36
+ targetRadio = radios[(currentIndex - 1 + radios.length) % radios.length];
37
+ }
38
+ if (targetRadio) {
39
+ targetRadio.focus();
40
+ // Read the value from the data attribute and trigger selection
41
+ const targetValue = targetRadio.getAttribute('data-value');
42
+ if (targetValue) {
43
+ onChange === null || onChange === void 0 ? void 0 : onChange(targetValue);
44
+ }
45
+ }
46
+ };
47
+ return (jsx("button", { ref: ref, type: "button", role: "radio", "aria-checked": isSelected, tabIndex: isSelected ? 0 : -1, disabled: isDisabled, className: cn(segmentedItemTheme.baseStyle, segmentedItemTheme.sizes[size], isSelected
22
48
  ? segmentedItemTheme.selectedStyle
23
49
  : segmentedItemTheme.unselectedStyle, isDisabled && segmentedItemTheme.disabledStyle, className // User overrides take precedence
24
- ), onClick: handleClick, "data-value": value, "data-selected": isSelected, "data-disabled": isDisabled, ...props, children: children }));
50
+ ), onClick: handleClick, onKeyDown: handleKeyDown, "data-value": value, "data-selected": isSelected, "data-disabled": isDisabled, ...props, children: children }));
25
51
  });
26
52
  // Set display name for dev tools and Storybook
27
53
  SegmentedItem.displayName = "Segmented.Item";
@@ -50,11 +50,11 @@ const Skeleton = React__default.forwardRef(({ variant = 'rectangle', width, heig
50
50
  // Make last line 75% width for more natural look
51
51
  const isLastLine = index === lines - 1;
52
52
  const lineWidth = isLastLine && !width ? '75%' : computedWidth;
53
- return (jsx("div", { className: baseClasses, style: { ...computedStyle, width: lineWidth } }, index));
53
+ return (jsx("div", { className: baseClasses, style: { ...computedStyle, width: lineWidth }, "aria-hidden": "true" }, index));
54
54
  }) }));
55
55
  }
56
56
  // Single skeleton
57
- return (jsx("div", { ref: ref, className: baseClasses, style: computedStyle, "aria-busy": "true", "aria-live": "polite", ...props }));
57
+ return (jsx("div", { ref: ref, className: baseClasses, style: computedStyle, "aria-hidden": "true", ...props }));
58
58
  });
59
59
  Skeleton.displayName = 'Skeleton';
60
60
 
@@ -1,4 +1,4 @@
1
1
  import React from 'react';
2
2
  import { SortableProps, SortableItemProps } from './Sortable.types';
3
3
  export declare const Sortable: React.ForwardRefExoticComponent<SortableProps & React.RefAttributes<HTMLUListElement>>;
4
- export declare const SortableItem: React.ForwardRefExoticComponent<SortableItemProps & React.RefAttributes<HTMLDivElement>>;
4
+ export declare const SortableItem: React.ForwardRefExoticComponent<SortableItemProps & React.RefAttributes<HTMLLIElement>>;
@@ -1,5 +1,5 @@
1
- import { jsx, jsxs } from 'react/jsx-runtime';
2
- import React__default, { createContext, useState, useRef, useMemo, useCallback, useEffect, useContext } from 'react';
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import React__default, { createContext, useState, useRef, useId, useMemo, useCallback, useEffect, useContext } from 'react';
3
3
  import { Reorder, useDragControls, motion } from 'motion/react';
4
4
  import { LockClosedIcon, Bars3Icon } from '@heroicons/react/24/outline';
5
5
  import { cn } from '../../../utils/cn.js';
@@ -19,6 +19,8 @@ const Sortable = React__default.forwardRef(({ children, onReorder, className, th
19
19
  const [dropPosition, setDropPosition] = useState(null);
20
20
  const itemRefs = useRef(new Map());
21
21
  const [draggedItemContent, setDraggedItemContent] = useState(null);
22
+ const [liveMessage, setLiveMessage] = useState('');
23
+ const liveRegionId = useId();
22
24
  const theme = {
23
25
  ...sortableTheme,
24
26
  ...customTheme,
@@ -63,8 +65,19 @@ const Sortable = React__default.forwardRef(({ children, onReorder, className, th
63
65
  itemRefs,
64
66
  draggedItemContent,
65
67
  setDraggedItemContent,
68
+ setLiveMessage,
66
69
  }), [direction, showDragHandle, theme, onReorder, onSortDragStart, onSortDragEnd, draggedItem, dropPosition, draggedItemContent]);
67
- return (jsx(SortableContext.Provider, { value: contextValue, children: jsx(Reorder.Group, { ref: ref, axis: direction === 'horizontal' ? 'x' : 'y', values: internalValues, onReorder: handleReorder, className: cn(theme.containerStyle, direction === 'horizontal' ? 'flex flex-row' : 'flex flex-col', className), as: "div", children: children }) }));
70
+ return (jsxs(SortableContext.Provider, { value: contextValue, children: [jsx("div", { id: liveRegionId, "aria-live": "assertive", "aria-atomic": "true", style: {
71
+ position: 'absolute',
72
+ width: '1px',
73
+ height: '1px',
74
+ padding: 0,
75
+ margin: '-1px',
76
+ overflow: 'hidden',
77
+ clip: 'rect(0, 0, 0, 0)',
78
+ whiteSpace: 'nowrap',
79
+ border: 0,
80
+ }, children: liveMessage }), jsx(Reorder.Group, { ref: ref, axis: direction === 'horizontal' ? 'x' : 'y', values: internalValues, onReorder: handleReorder, className: cn(theme.containerStyle, direction === 'horizontal' ? 'flex flex-row' : 'flex flex-col', className), as: "ul", children: children })] }));
68
81
  });
69
82
  Sortable.displayName = 'Sortable';
70
83
  const SortableItem = React__default.forwardRef(({ id, children, className, theme: customTheme = {}, disabled = false, value, }, forwardedRef) => {
@@ -136,6 +149,9 @@ const SortableItem = React__default.forwardRef(({ id, children, className, theme
136
149
  }
137
150
  if (targetIndex !== currentIndex) {
138
151
  context.onReorder(currentIndex, targetIndex);
152
+ // Announce the reorder to screen readers via the live region
153
+ const itemLabel = typeof value === 'string' ? value : id;
154
+ context.setLiveMessage(`${itemLabel} moved to position ${targetIndex + 1} of ${items.length}`);
139
155
  // Focus the item at new position after reorder
140
156
  setTimeout(() => {
141
157
  const targetItem = items[targetIndex];
@@ -185,7 +201,7 @@ const SortableItem = React__default.forwardRef(({ id, children, className, theme
185
201
  dragControls.start(event);
186
202
  }, [disabled, dragControls]);
187
203
  const isBeingDragged = context.draggedItem === id;
188
- return (jsxs(Reorder.Item, { ref: (node) => {
204
+ return (jsxs(Reorder.Item, { as: "li", ref: (node) => {
189
205
  itemRef.current = node;
190
206
  if (typeof forwardedRef === 'function') {
191
207
  forwardedRef(node);
@@ -70,4 +70,6 @@ export interface SortableContextValue {
70
70
  itemRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
71
71
  draggedItemContent: React.ReactNode | null;
72
72
  setDraggedItemContent: (content: React.ReactNode | null) => void;
73
+ /** Update the visually-hidden live region with a reorder announcement */
74
+ setLiveMessage: (message: string) => void;
73
75
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import React__default, { useState, useRef, useCallback, useMemo } from 'react';
2
+ import React__default, { useState, useId, useRef, useCallback, useMemo } from 'react';
3
3
  import { cn } from '../../../utils/cn.js';
4
4
  import { tabsTheme } from './Tabs.theme.js';
5
5
  import { TabsContext } from './TabsContext.js';
@@ -14,6 +14,8 @@ import { TabsBody } from './TabsBody.js';
14
14
  */
15
15
  const TabsComponent = React__default.forwardRef(({ value, defaultValue, onValueChange, orientation = "horizontal", size = "md", disabled = false, children, className, darkMode = false, ...props }, ref) => {
16
16
  const [currentValue, setCurrentValue] = useState(value || defaultValue || "");
17
+ // Unique instance prefix to scope all DOM IDs to this Tabs instance
18
+ const instanceId = useId();
17
19
  // Track trigger elements for underline positioning
18
20
  const triggerRefs = useRef(new Map());
19
21
  // Handle controlled vs uncontrolled
@@ -68,15 +70,15 @@ const TabsComponent = React__default.forwardRef(({ value, defaultValue, onValueC
68
70
  disabled,
69
71
  registerTrigger,
70
72
  unregisterTrigger,
71
- }), [activeValue, handleValueChange, orientation, size, disabled, registerTrigger, unregisterTrigger]);
73
+ instanceId,
74
+ }), [activeValue, handleValueChange, orientation, size, disabled, registerTrigger, unregisterTrigger, instanceId]);
72
75
  // Memoize theme classes for performance
73
76
  const themeClasses = useMemo(() => ({
74
77
  baseStyle: tabsTheme.baseStyle,
75
78
  disabledStyle: disabled ? "opacity-50 cursor-not-allowed pointer-events-none" : "",
76
79
  }), [disabled]);
77
80
  return (jsx(TabsContext.Provider, { value: contextValue, children: jsx("div", { ref: ref, className: cn(themeClasses.baseStyle, themeClasses.disabledStyle, darkMode && "dark", className // User overrides take precedence
78
- ), "data-orientation": orientation, "data-size": size, "data-disabled": disabled, role: "tablist" // Add semantic role for screen readers
79
- , "aria-orientation": orientation, ...props, children: children }) }));
81
+ ), "data-orientation": orientation, "data-size": size, "data-disabled": disabled, ...props, children: children }) }));
80
82
  });
81
83
  // Set display name for dev tools and Storybook
82
84
  TabsComponent.displayName = "Tabs";
@@ -139,4 +139,6 @@ export interface TabsContextValue {
139
139
  registerTrigger: (value: string, element: HTMLButtonElement) => void;
140
140
  /** Unregister a tab trigger when it unmounts */
141
141
  unregisterTrigger: (value: string) => void;
142
+ /** Unique prefix scoping DOM IDs to this Tabs instance */
143
+ instanceId: string;
142
144
  }
@@ -10,7 +10,7 @@ import { getTabsBodyVariants } from './Tabs.animations.js';
10
10
  * TabsBody component displays content panels for tabs with directional fade animations
11
11
  */
12
12
  const TabsBody = React__default.forwardRef(({ value, forceMount = false, children, className, ...props }, ref) => {
13
- const { value: selectedValue, orientation } = useTabsContext();
13
+ const { value: selectedValue, orientation, instanceId } = useTabsContext();
14
14
  const shouldReduceMotion = useReducedMotion();
15
15
  // Check if we're in Storybook and should disable animations
16
16
  const isInStorybook = typeof window !== 'undefined' && window.__STORYBOOK_DISABLE_ANIMATIONS__;
@@ -21,7 +21,7 @@ const TabsBody = React__default.forwardRef(({ value, forceMount = false, childre
21
21
  const contentVariants = useMemo(() => getTabsBodyVariants(shouldDisableAnimations), [shouldDisableAnimations]);
22
22
  // For forceMount, we handle visibility differently to preserve DOM
23
23
  if (forceMount) {
24
- return (jsx(motion.div, { ref: ref, role: "tabpanel", "aria-labelledby": `${value}-tab`, id: `${value}-content`, tabIndex: 0, className: cn(tabsTheme.bodyStyle, !isSelected && "hidden", // Hide when not selected but force mounted
24
+ return (jsx(motion.div, { ref: ref, role: "tabpanel", "aria-labelledby": `${instanceId}-${value}-tab`, id: `${instanceId}-${value}-content`, tabIndex: 0, className: cn(tabsTheme.bodyStyle, !isSelected && "hidden", // Hide when not selected but force mounted
25
25
  className // User overrides take precedence
26
26
  ), "data-state": isSelected ? "active" : "inactive", "data-orientation": orientation, style: {
27
27
  // Ensure proper stacking context for animations
@@ -39,7 +39,7 @@ const TabsBody = React__default.forwardRef(({ value, forceMount = false, childre
39
39
  exit: "exit",
40
40
  variants: contentVariants,
41
41
  };
42
- return (jsx(AnimatePresence, { mode: "wait", children: isSelected && (jsx(motion.div, { ref: ref, role: "tabpanel", "aria-labelledby": `${value}-tab`, id: `${value}-content`, tabIndex: 0, className: cn(tabsTheme.bodyStyle, className // User overrides take precedence
42
+ return (jsx(AnimatePresence, { mode: "wait", children: isSelected && (jsx(motion.div, { ref: ref, role: "tabpanel", "aria-labelledby": `${instanceId}-${value}-tab`, id: `${instanceId}-${value}-content`, tabIndex: 0, className: cn(tabsTheme.bodyStyle, className // User overrides take precedence
43
43
  ), "data-state": "active", "data-orientation": orientation, style: {
44
44
  // Ensure proper stacking context for animations
45
45
  position: "relative",
@@ -1,5 +1,5 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import React__default, { useState, useRef, useMemo, useEffect } from 'react';
2
+ import React__default, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
3
3
  import { useReducedMotion, motion } from 'motion/react';
4
4
  import { cn } from '../../../utils/cn.js';
5
5
  import { tabsTheme } from './Tabs.theme.js';
@@ -18,26 +18,24 @@ const TabsList = React__default.forwardRef(({ children, className, ...props }, r
18
18
  // Ref to access the list element for calculations
19
19
  const listRef = useRef(null);
20
20
  // Calculate underline position based on active tab
21
- const updateUnderlinePosition = useMemo(() => {
22
- return () => {
23
- if (!activeValue || !listRef.current) {
24
- setShowUnderline(false);
25
- return;
26
- }
27
- // Find the active trigger element
28
- const activeTrigger = listRef.current.querySelector(`[data-value="${activeValue}"]`);
29
- if (!activeTrigger) {
30
- setShowUnderline(false);
31
- return;
32
- }
33
- // Get the position relative to the list container
34
- const listRect = listRef.current.getBoundingClientRect();
35
- const triggerRect = activeTrigger.getBoundingClientRect();
36
- const left = triggerRect.left - listRect.left;
37
- const width = triggerRect.width;
38
- setUnderlinePosition({ left, width });
39
- setShowUnderline(true);
40
- };
21
+ const updateUnderlinePosition = useCallback(() => {
22
+ if (!activeValue || !listRef.current) {
23
+ setShowUnderline(false);
24
+ return;
25
+ }
26
+ // Find the active trigger element
27
+ const activeTrigger = listRef.current.querySelector(`[data-value="${activeValue}"]`);
28
+ if (!activeTrigger) {
29
+ setShowUnderline(false);
30
+ return;
31
+ }
32
+ // Get the position relative to the list container
33
+ const listRect = listRef.current.getBoundingClientRect();
34
+ const triggerRect = activeTrigger.getBoundingClientRect();
35
+ const left = triggerRect.left - listRect.left;
36
+ const width = triggerRect.width;
37
+ setUnderlinePosition({ left, width });
38
+ setShowUnderline(true);
41
39
  }, [activeValue]);
42
40
  // Update underline position when active value changes or on mount
43
41
  useEffect(() => {
@@ -1,5 +1,5 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import React__default, { useMemo, useCallback, useEffect } from 'react';
2
+ import React__default, { useRef, useEffect, useMemo, useCallback } from 'react';
3
3
  import { useReducedMotion, motion } from 'motion/react';
4
4
  import { cn } from '../../../utils/cn.js';
5
5
  import { tabsTheme } from './Tabs.theme.js';
@@ -10,11 +10,14 @@ import { getTabsTriggerVariants } from './Tabs.animations.js';
10
10
  * TabsTrigger component displays individual tab headers with optional animations
11
11
  */
12
12
  const TabsTrigger = React__default.forwardRef(({ value, disabled = false, children, className, onClick, onKeyDown, ...props }, ref) => {
13
- const { value: selectedValue, onValueChange, orientation, size, disabled: contextDisabled, registerTrigger, unregisterTrigger } = useTabsContext();
13
+ const { value: selectedValue, onValueChange, orientation, size, disabled: contextDisabled, registerTrigger, unregisterTrigger, instanceId } = useTabsContext();
14
14
  const shouldReduceMotion = useReducedMotion();
15
- // Check if we're in Storybook and should disable animations
16
- const isInStorybook = typeof window !== 'undefined' && window.__STORYBOOK_DISABLE_ANIMATIONS__;
17
- const shouldDisableAnimations = !!shouldReduceMotion || !!isInStorybook;
15
+ // Check if we're in Storybook evaluated after mount to avoid SSR hydration mismatch
16
+ const isInStorybookRef = useRef(false);
17
+ useEffect(() => {
18
+ isInStorybookRef.current = typeof window !== 'undefined' && !!window.__STORYBOOK_DISABLE_ANIMATIONS__;
19
+ }, []);
20
+ const shouldDisableAnimations = !!shouldReduceMotion || !!isInStorybookRef.current;
18
21
  // Memoize derived state flags for performance
19
22
  const derivedState = useMemo(() => ({
20
23
  isSelected: value === selectedValue,
@@ -51,7 +54,6 @@ const TabsTrigger = React__default.forwardRef(({ value, disabled = false, childr
51
54
  }, [isDisabled, onValueChange, value, onClick]);
52
55
  // Enhanced keyboard navigation handler with error handling
53
56
  const handleKeyDown = useCallback((event) => {
54
- var _a, _b;
55
57
  // Handle keyboard navigation for tabs
56
58
  if (!isDisabled) {
57
59
  try {
@@ -78,20 +80,32 @@ const TabsTrigger = React__default.forwardRef(({ value, disabled = false, childr
78
80
  break;
79
81
  case "Home":
80
82
  event.preventDefault();
81
- // Move to first tab
82
- const firstTrigger = (_a = event.currentTarget.parentElement) === null || _a === void 0 ? void 0 : _a.firstElementChild;
83
- if (firstTrigger && firstTrigger.tagName === 'BUTTON' && !firstTrigger.disabled) {
84
- firstTrigger.focus();
85
- firstTrigger.click();
83
+ // Move to first tab — use role="tab" to skip the sliding underline element
84
+ {
85
+ const tabList = event.currentTarget.parentElement;
86
+ const allTabs = tabList
87
+ ? Array.from(tabList.querySelectorAll('[role="tab"]:not([disabled])'))
88
+ : [];
89
+ const firstTrigger = allTabs[0];
90
+ if (firstTrigger) {
91
+ firstTrigger.focus();
92
+ firstTrigger.click();
93
+ }
86
94
  }
87
95
  break;
88
96
  case "End":
89
97
  event.preventDefault();
90
- // Move to last tab
91
- const lastTrigger = (_b = event.currentTarget.parentElement) === null || _b === void 0 ? void 0 : _b.lastElementChild;
92
- if (lastTrigger && lastTrigger.tagName === 'BUTTON' && !lastTrigger.disabled) {
93
- lastTrigger.focus();
94
- lastTrigger.click();
98
+ // Move to last tab — use role="tab" to skip the sliding underline element
99
+ {
100
+ const tabList = event.currentTarget.parentElement;
101
+ const allTabs = tabList
102
+ ? Array.from(tabList.querySelectorAll('[role="tab"]:not([disabled])'))
103
+ : [];
104
+ const lastTrigger = allTabs[allTabs.length - 1];
105
+ if (lastTrigger) {
106
+ lastTrigger.focus();
107
+ lastTrigger.click();
108
+ }
95
109
  }
96
110
  break;
97
111
  }
@@ -114,18 +128,23 @@ const TabsTrigger = React__default.forwardRef(({ value, disabled = false, childr
114
128
  }, [size]);
115
129
  // Generate default aria-label for icon-only triggers if not provided (fallback only)
116
130
  const ariaLabel = props["aria-label"] || undefined;
117
- // Register/unregister this trigger for underline positioning
118
- useEffect(() => {
119
- // Register with a slight delay to ensure DOM is ready
120
- const timeoutId = setTimeout(() => {
121
- if (ref && typeof ref === 'object' && ref.current) {
122
- registerTrigger(value, ref.current);
123
- }
124
- }, 0);
125
- return () => {
126
- clearTimeout(timeoutId);
131
+ // Callback ref that registers/unregisters the trigger for underline positioning.
132
+ // Using a callback ref ensures registration fires whenever the DOM node mounts,
133
+ // not just when the ref object identity changes.
134
+ const triggerRef = useCallback((node) => {
135
+ if (node) {
136
+ registerTrigger(value, node);
137
+ }
138
+ else {
127
139
  unregisterTrigger(value);
128
- };
140
+ }
141
+ // Forward to the outer ref passed by the consumer, if any
142
+ if (typeof ref === 'function') {
143
+ ref(node);
144
+ }
145
+ else if (ref) {
146
+ ref.current = node;
147
+ }
129
148
  }, [value, registerTrigger, unregisterTrigger, ref]);
130
149
  // Determine animation props (user props will override due to spread order)
131
150
  const animationProps = shouldDisableAnimations ? {} : {
@@ -133,7 +152,7 @@ const TabsTrigger = React__default.forwardRef(({ value, disabled = false, childr
133
152
  whileTap: !isDisabled ? "tap" : undefined,
134
153
  variants: triggerVariants,
135
154
  };
136
- return (jsx(motion.button, { ref: ref, type: "button", role: "tab", "aria-selected": isSelected, "aria-controls": `${value}-content`, id: `${value}-tab`, tabIndex: isSelected ? 0 : -1, disabled: isDisabled, className: cn(themeClasses.triggerStyle, themeClasses.sizeStyle, className // User overrides take precedence
155
+ return (jsx(motion.button, { ref: triggerRef, type: "button", role: "tab", "aria-selected": isSelected, "aria-controls": `${instanceId}-${value}-content`, id: `${instanceId}-${value}-tab`, tabIndex: isSelected ? 0 : -1, disabled: isDisabled, className: cn(themeClasses.triggerStyle, themeClasses.sizeStyle, className // User overrides take precedence
137
156
  ), onClick: handleClick, onKeyDown: handleKeyDown, "data-value": value, "data-state": isSelected ? "active" : "inactive", "data-disabled": isDisabled, "data-orientation": orientation, "aria-disabled": isDisabled || undefined, "aria-label": ariaLabel, ...animationProps, ...props, children: children }));
138
157
  });
139
158
  TabsTrigger.displayName = "TabsTrigger";
@@ -84,7 +84,7 @@ const Toast = React__default.forwardRef(({ toast, className, onDismiss, index =
84
84
  }, [stackingStyle.zIndex, index]);
85
85
  const toastNode = (jsxs(motion.div, { ref: setRefs, layout: true, initial: shouldReduceMotion ? false : toastAnimations.initial, animate: animateValue, exit: shouldReduceMotion
86
86
  ? { ...toastAnimations.exit, transition: { duration: 0 } }
87
- : toastAnimations.exit, transition: shouldReduceMotion ? { duration: 0 } : toastAnimations.transition, className: cn(toastTheme.baseStyle, isTextOnly && "items-center", ((_a = toastTheme.variants) === null || _a === void 0 ? void 0 : _a[variant]) || "", "pointer-events-auto", index > 0 && !isExpanded && "absolute top-0 left-0", className), style: styleValue, role: "alert", "aria-live": "polite", children: [jsx("div", { className: cn(toastTheme.iconStyle, !isTextOnly && "mt-0.5"), children: resolvedIcon }), jsxs("div", { className: "flex-1 min-w-0", children: [title &&
87
+ : toastAnimations.exit, transition: shouldReduceMotion ? { duration: 0 } : toastAnimations.transition, className: cn(toastTheme.baseStyle, isTextOnly && "items-center", ((_a = toastTheme.variants) === null || _a === void 0 ? void 0 : _a[variant]) || "", "pointer-events-auto", index > 0 && !isExpanded && "absolute top-0 left-0", className), style: styleValue, role: "status", "aria-live": "polite", children: [jsx("div", { className: cn(toastTheme.iconStyle, !isTextOnly && "mt-0.5"), children: resolvedIcon }), jsxs("div", { className: "flex-1 min-w-0", children: [title &&
88
88
  (typeof title === "string" ? (jsx("div", { className: cn(toastTheme.titleStyle, !isTextOnly && "font-semibold"), children: title })) : (title)), subtitle &&
89
89
  (typeof subtitle === "string" ? (jsx("div", { className: toastTheme.subtitleStyle, children: subtitle })) : (subtitle)), action && (jsx("div", { className: "mt-4", children: isToastAction(action) ? (jsx(Button, { variant: "link", color: "primary", onClick: handleActionClick, children: action.label })) : (action) })), children] }), dismissible && (jsx("button", { type: "button", className: cn(toastTheme.closeButtonStyle, !isTextOnly && "mt-0.5"), onClick: handleDismiss, "aria-label": "Dismiss notification", children: jsx(XMarkIcon, { className: "size-5" }) }))] }));
90
90
  if (darkMode) {
@@ -24,7 +24,7 @@ const ToastProvider = ({ children, position = 'top-right', maxToasts = 5, defaul
24
24
  const hoverTimeoutRef = React__default.useRef(null);
25
25
  const addToast = useCallback((toastData) => {
26
26
  var _a;
27
- const id = Math.random().toString(36).substring(2, 9);
27
+ const id = crypto.randomUUID();
28
28
  const newToast = {
29
29
  ...toastData,
30
30
  id,
@@ -91,7 +91,8 @@ const ToastProvider = ({ children, position = 'top-right', maxToasts = 5, defaul
91
91
  removeToast,
92
92
  removeAllToasts,
93
93
  }), [toasts, addToast, removeToast, removeAllToasts]);
94
- return (jsxs(ToastContext.Provider, { value: contextValue, children: [children, isClient && createPortal(jsx("div", { className: cn(toastTheme.containerStyle, positionStyles[position]), "aria-live": "polite", "aria-label": "Notifications", children: jsx("div", { className: "relative w-80", style: { paddingBottom: isHovered && toasts.length > 1 ? totalExpandedHeight : undefined }, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: jsx(AnimatePresence, { mode: "popLayout", children: toasts.map((toast, index) => (jsx(Toast, { toast: toast, onDismiss: removeToast, index: index, isExpanded: isHovered, totalToasts: toasts.length, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onHeightChange: handleToastHeight, expandedOffset: getExpandedOffset(index), darkMode: darkMode }, toast.id))) }) }) }), document.body)] }));
94
+ const portalTarget = isClient ? document.body : null;
95
+ return (jsxs(ToastContext.Provider, { value: contextValue, children: [children, portalTarget ? createPortal(jsx("div", { className: cn(toastTheme.containerStyle, positionStyles[position]), "aria-live": "polite", "aria-label": "Notifications", children: jsx("div", { className: "relative w-80", style: { paddingBottom: isHovered && toasts.length > 1 ? totalExpandedHeight : undefined }, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: jsx(AnimatePresence, { mode: "popLayout", children: toasts.map((toast, index) => (jsx(Toast, { toast: toast, onDismiss: removeToast, index: index, isExpanded: isHovered, totalToasts: toasts.length, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onHeightChange: handleToastHeight, expandedOffset: getExpandedOffset(index), darkMode: darkMode }, toast.id))) }) }) }), portalTarget) : null] }));
95
96
  };
96
97
  const useToastContext = () => {
97
98
  const context = useContext(ToastContext);
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
- import { useState, useRef, useCallback, useEffect } from 'react';
2
+ import React__default, { useState, useRef, useCallback, useEffect } from 'react';
3
3
  import { useReducedMotion, AnimatePresence, motion } from 'motion/react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { cn } from '../../../utils/cn.js';
@@ -7,36 +7,8 @@ import { tooltipTheme } from './Tooltip.theme.js';
7
7
  import { getTooltipVariants } from './Tooltip.animations.js';
8
8
  import { useSelectPortal } from '../../../hooks/useSelectPortal.js';
9
9
  import { useIsClient } from '../../../hooks/useIsClient.js';
10
+ import { useDarkMode } from '../../../hooks/useDarkMode.js';
10
11
 
11
- /**
12
- * Hook to detect dark mode state from document
13
- */
14
- const useDarkMode = () => {
15
- const [isDark, setIsDark] = useState(false);
16
- useEffect(() => {
17
- if (typeof document === "undefined")
18
- return;
19
- const checkDarkMode = () => {
20
- const hasDarkClass = document.documentElement.classList.contains("dark") ||
21
- document.body.classList.contains("dark") ||
22
- document.querySelector(".dark") !== null;
23
- setIsDark(hasDarkClass);
24
- };
25
- checkDarkMode();
26
- // Observe class changes on html and body
27
- const observer = new MutationObserver(checkDarkMode);
28
- observer.observe(document.documentElement, {
29
- attributes: true,
30
- attributeFilter: ["class"],
31
- });
32
- observer.observe(document.body, {
33
- attributes: true,
34
- attributeFilter: ["class"],
35
- });
36
- return () => observer.disconnect();
37
- }, []);
38
- return isDark;
39
- };
40
12
  const Tooltip = ({ content, children, placement = "top", delay = 300, disabled = false, className, triggerClassName, maxWidth = "360px", minWidth = "fit-content", showOnFocus = true, wrapText = false, id, theme: themeOverrides, "aria-label": ariaLabel, role = "tooltip", offset = 8, darkMode = false, }) => {
41
13
  const [isVisible, setIsVisible] = useState(false);
42
14
  const tooltipRef = useRef(null);
@@ -91,11 +63,14 @@ const Tooltip = ({ content, children, placement = "top", delay = 300, disabled =
91
63
  handleHideTooltip();
92
64
  }
93
65
  }, [handleHideTooltip]);
94
- // Event handlers for trigger element
66
+ // Determine whether the child can receive focus events via cloneElement.
67
+ // If so, focus/blur are forwarded to the child; otherwise fall back to the wrapper.
68
+ const childCanReceiveFocus = showOnFocus && React__default.isValidElement(children);
69
+ // Event handlers for the wrapper div (focus/blur only go here as fallback)
95
70
  const triggerHandlers = {
96
71
  onMouseEnter: handleShowTooltip,
97
72
  onMouseLeave: handleHideTooltip,
98
- ...(showOnFocus && {
73
+ ...(showOnFocus && !childCanReceiveFocus && {
99
74
  onFocus: handleShowTooltip,
100
75
  onBlur: handleHideTooltip,
101
76
  }),
@@ -122,8 +97,24 @@ const Tooltip = ({ content, children, placement = "top", delay = 300, disabled =
122
97
  ...cssVariables,
123
98
  top: `${position.top}px`,
124
99
  left: `${position.left}px`,
125
- }, className: cn(theme.baseStyle, widthClasses, className), "aria-hidden": !isVisible, "aria-label": ariaLabel, "aria-live": role === "tooltip" ? "polite" : undefined, variants: shouldReduceMotion ? {} : getTooltipVariants(placement), initial: shouldReduceMotion ? undefined : "initial", animate: shouldReduceMotion ? undefined : "animate", exit: shouldReduceMotion ? undefined : "exit", children: content })) }));
126
- return (jsxs("div", { className: cn("relative inline-block", triggerClassName), ref: triggerRef, ...triggerHandlers, tabIndex: showOnFocus ? 0 : undefined, "aria-describedby": isVisible && id ? id : undefined, children: [children, isClient &&
100
+ }, className: cn(theme.baseStyle, widthClasses, className), "aria-hidden": !isVisible, "aria-label": ariaLabel, variants: shouldReduceMotion ? {} : getTooltipVariants(placement), initial: shouldReduceMotion ? undefined : "initial", animate: shouldReduceMotion ? undefined : "animate", exit: shouldReduceMotion ? undefined : "exit", children: content })) }));
101
+ // When the child is a valid element, forward focus/blur events directly onto it
102
+ // instead of adding tabIndex to the wrapper (which would create a double Tab stop).
103
+ const childWithFocusHandlers = childCanReceiveFocus
104
+ ? React__default.cloneElement(children, {
105
+ onFocus: (e) => {
106
+ var _a, _b;
107
+ handleShowTooltip();
108
+ (_b = (_a = children.props).onFocus) === null || _b === void 0 ? void 0 : _b.call(_a, e);
109
+ },
110
+ onBlur: (e) => {
111
+ var _a, _b;
112
+ handleHideTooltip();
113
+ (_b = (_a = children.props).onBlur) === null || _b === void 0 ? void 0 : _b.call(_a, e);
114
+ },
115
+ })
116
+ : children;
117
+ return (jsxs("div", { className: cn("relative inline-block", triggerClassName), ref: triggerRef, ...triggerHandlers, "aria-describedby": isVisible && id ? id : undefined, children: [childWithFocusHandlers, isClient &&
127
118
  createPortal(jsx("div", { className: darkMode || isDarkMode ? "dark" : undefined, children: tooltipContent }), document.body)] }));
128
119
  };
129
120
  // Set display name for dev tools and Storybook