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

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 (157) 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/Accordion.theme.js +2 -2
  8. package/dist/components/core/Accordion/AccordionTrigger.js +28 -1
  9. package/dist/components/core/Alert/Alert.js +2 -1
  10. package/dist/components/core/AlertDialog/AlertDialog.js +1 -1
  11. package/dist/components/core/Badge/Badge.js +6 -17
  12. package/dist/components/core/Breadcrumbs/Breadcrumbs.js +17 -28
  13. package/dist/components/core/Breadcrumbs/Breadcrumbs.theme.js +2 -2
  14. package/dist/components/core/Calendar/Calendar.js +1 -1
  15. package/dist/components/core/Calendar/CalendarMini/CalendarMini.js +4 -2
  16. package/dist/components/core/Calendar/CalendarMini/CalendarMini.theme.js +1 -1
  17. package/dist/components/core/Carousel/CarouselBody.js +6 -1
  18. package/dist/components/core/ContextMenu/ContextMenuBody.js +6 -3
  19. package/dist/components/core/ContextMenu/ContextMenuSubBody.js +6 -3
  20. package/dist/components/core/DragDrop/DragDrop.js +170 -17
  21. package/dist/components/core/DragDrop/DragDrop.types.d.ts +6 -0
  22. package/dist/components/core/Drawer/Drawer.d.ts +1 -1
  23. package/dist/components/core/Drawer/Drawer.js +2 -0
  24. package/dist/components/core/Drawer/Drawer.types.d.ts +2 -0
  25. package/dist/components/core/Drawer/DrawerTitle.d.ts +1 -1
  26. package/dist/components/core/Drawer/DrawerTitle.js +2 -2
  27. package/dist/components/core/Dropdown/DropdownMenu.js +2 -30
  28. package/dist/components/core/Empty/Empty.theme.js +1 -1
  29. package/dist/components/core/Link/Link.js +1 -1
  30. package/dist/components/core/Masonry/Masonry.js +7 -2
  31. package/dist/components/core/Modal/Modal.d.ts +1 -1
  32. package/dist/components/core/Modal/Modal.js +3 -3
  33. package/dist/components/core/Modal/Modal.types.d.ts +2 -0
  34. package/dist/components/core/Modal/ModalTitle.d.ts +1 -1
  35. package/dist/components/core/Modal/ModalTitle.js +2 -2
  36. package/dist/components/core/NavItem/NavItem.js +13 -8
  37. package/dist/components/core/Pagination/Pagination.js +1 -1
  38. package/dist/components/core/Popover/PopoverBody.js +3 -27
  39. package/dist/components/core/Popover/PopoverTrigger.js +1 -1
  40. package/dist/components/core/Progress/Progress.js +4 -4
  41. package/dist/components/core/Rating/Rating.js +1 -1
  42. package/dist/components/core/ScrollArea/ScrollArea.js +3 -2
  43. package/dist/components/core/Segmented/Segmented.js +6 -6
  44. package/dist/components/core/Segmented/SegmentedItem.js +28 -2
  45. package/dist/components/core/Skeleton/Skeleton.js +2 -2
  46. package/dist/components/core/Sortable/Sortable.d.ts +1 -1
  47. package/dist/components/core/Sortable/Sortable.js +20 -4
  48. package/dist/components/core/Sortable/Sortable.types.d.ts +2 -0
  49. package/dist/components/core/Tabs/Tabs.js +6 -4
  50. package/dist/components/core/Tabs/Tabs.types.d.ts +2 -0
  51. package/dist/components/core/Tabs/TabsBody.js +3 -3
  52. package/dist/components/core/Tabs/TabsList.js +19 -21
  53. package/dist/components/core/Tabs/TabsTrigger.js +47 -28
  54. package/dist/components/core/Toast/Toast.js +1 -1
  55. package/dist/components/core/Toast/ToastProvider.js +3 -2
  56. package/dist/components/core/Tooltip/Tooltip.js +25 -34
  57. package/dist/components/core/Tree/Tree.js +178 -8
  58. package/dist/components/data-display/GanttChart/GanttSplitter.js +15 -1
  59. package/dist/components/data-display/Table/Table.js +2 -2
  60. package/dist/components/data-display/Table/Table.types.d.ts +2 -0
  61. package/dist/components/data-display/Table/TableFilter.js +21 -18
  62. package/dist/components/data-display/Timeline/Timeline.animations.d.ts +0 -7
  63. package/dist/components/data-display/Timeline/Timeline.animations.js +1 -27
  64. package/dist/components/data-display/Timeline/Timeline.js +3 -4
  65. package/dist/components/data-display/Timeline/Timeline.types.d.ts +0 -13
  66. package/dist/components/data-display/Timeline/TimelineContent.js +3 -7
  67. package/dist/components/data-display/Timeline/TimelineItem.js +4 -17
  68. package/dist/components/data-display/Timeline/index.d.ts +2 -2
  69. package/dist/components/effects/CustomCursor/CustomCursor.js +3 -7
  70. package/dist/components/effects/Overlay/Overlay.js +4 -9
  71. package/dist/components/effects/PageTransition/PageTransition.js +17 -6
  72. package/dist/components/effects/SpotlightBorder/SpotlightBorder.js +3 -4
  73. package/dist/components/effects/index.d.ts +7 -0
  74. package/dist/components/effects/index.js +3 -0
  75. package/dist/components/forms/Checkbox/Checkbox.js +11 -1
  76. package/dist/components/forms/Checkbox/Checkbox.theme.js +4 -1
  77. package/dist/components/forms/Checkbox/Checkbox.types.d.ts +7 -5
  78. package/dist/components/forms/Checkbox/CheckboxGroup.js +9 -4
  79. package/dist/components/forms/ColorPicker/ColorPicker.js +7 -4
  80. package/dist/components/forms/ColorPicker/ColorPicker.types.d.ts +2 -2
  81. package/dist/components/forms/ColorPicker/ColorPickerBody.js +5 -3
  82. package/dist/components/forms/ColorPicker/ColorPickerEyeDropper.js +3 -3
  83. package/dist/components/forms/ColorPicker/ColorPickerGradient.js +8 -8
  84. package/dist/components/forms/ColorPicker/ColorPickerInput.js +1 -1
  85. package/dist/components/forms/ColorPicker/index.d.ts +1 -1
  86. package/dist/components/forms/Combobox/Combobox.js +20 -45
  87. package/dist/components/forms/Combobox/Combobox.types.d.ts +4 -2
  88. package/dist/components/forms/CronInput/CronInput.js +10 -4
  89. package/dist/components/forms/CronInput/CronInput.types.d.ts +12 -1
  90. package/dist/components/forms/DatePicker/DatePicker.js +2 -2
  91. package/dist/components/forms/DatePicker/DatePicker.types.d.ts +4 -2
  92. package/dist/components/forms/DatePicker/DatePickerBody.js +1 -29
  93. package/dist/components/forms/DatePicker/DatePickerTrigger.js +2 -2
  94. package/dist/components/forms/DateRangePicker/DateRangePicker.js +3 -3
  95. package/dist/components/forms/DateRangePicker/DateRangePicker.types.d.ts +4 -2
  96. package/dist/components/forms/DateRangePicker/DateRangePickerBody.js +1 -28
  97. package/dist/components/forms/DateRangePicker/DateRangePickerTrigger.js +2 -2
  98. package/dist/components/forms/FileUpload/FileUpload.js +14 -10
  99. package/dist/components/forms/FileUpload/FileUpload.types.d.ts +15 -0
  100. package/dist/components/forms/FormLabel/FormLabel.js +2 -2
  101. package/dist/components/forms/Input/Input.js +5 -3
  102. package/dist/components/forms/Input/Input.types.d.ts +2 -0
  103. package/dist/components/forms/InputAddress/InputAddress.d.ts +1 -1
  104. package/dist/components/forms/InputAddress/InputAddress.js +8 -9
  105. package/dist/components/forms/InputCounter/InputCounter.js +25 -18
  106. package/dist/components/forms/InputCounter/InputCounter.types.d.ts +8 -1
  107. package/dist/components/forms/InputCreditCard/InputCreditCard.d.ts +1 -1
  108. package/dist/components/forms/InputCreditCard/InputCreditCard.js +27 -9
  109. package/dist/components/forms/InputCreditCard/InputCreditCard.types.d.ts +5 -0
  110. package/dist/components/forms/InputOTP/InputOTP.js +26 -17
  111. package/dist/components/forms/InputOTP/InputOTP.types.d.ts +21 -4
  112. package/dist/components/forms/InputTag/InputTag.js +12 -39
  113. package/dist/components/forms/InputTag/InputTag.types.d.ts +2 -2
  114. package/dist/components/forms/Mention/Mention.js +13 -6
  115. package/dist/components/forms/Mention/Mention.types.d.ts +9 -0
  116. package/dist/components/forms/Radio/Radio.js +12 -2
  117. package/dist/components/forms/Radio/Radio.theme.js +5 -1
  118. package/dist/components/forms/Radio/Radio.types.d.ts +7 -5
  119. package/dist/components/forms/Radio/RadioGroup.js +8 -3
  120. package/dist/components/forms/RichTextEditor/RichTextEditor.js +21 -21
  121. package/dist/components/forms/RichTextEditor/RichTextEditor.types.d.ts +2 -2
  122. package/dist/components/forms/Select/Select.d.ts +2 -0
  123. package/dist/components/forms/Select/Select.js +27 -57
  124. package/dist/components/forms/Select/Select.types.d.ts +14 -0
  125. package/dist/components/forms/Signature/Signature.js +1 -1
  126. package/dist/components/forms/Signature/Signature.types.d.ts +3 -3
  127. package/dist/components/forms/Slider/Slider.js +18 -4
  128. package/dist/components/forms/Slider/Slider.types.d.ts +2 -2
  129. package/dist/components/forms/Switch/Switch.js +12 -19
  130. package/dist/components/forms/Switch/Switch.theme.js +12 -2
  131. package/dist/components/forms/Switch/Switch.types.d.ts +20 -2
  132. package/dist/components/forms/Textarea/Textarea.js +8 -3
  133. package/dist/components/forms/Textarea/Textarea.types.d.ts +2 -0
  134. package/dist/components/forms/TimePicker/TimePicker.js +1 -1
  135. package/dist/components/forms/TimePicker/TimePicker.types.d.ts +6 -2
  136. package/dist/components/forms/TimePicker/TimePickerContent.js +2 -29
  137. package/dist/components/forms/TimePicker/TimePickerTrigger.js +2 -2
  138. package/dist/components/forms/index.js +1 -1
  139. package/dist/components/layout/index.js +4 -0
  140. package/dist/hooks/index.d.ts +1 -0
  141. package/dist/hooks/useClickOutside.d.ts +10 -4
  142. package/dist/hooks/useClickOutside.js +44 -24
  143. package/dist/hooks/useDarkMode.d.ts +1 -0
  144. package/dist/hooks/useDarkMode.js +42 -0
  145. package/dist/hooks/useFormStateMachine.d.ts +1 -1
  146. package/dist/hooks/useNetworkStatus.js +1 -1
  147. package/dist/hooks/useOverlay.js +4 -0
  148. package/dist/hooks/useScaleBackground.js +5 -2
  149. package/dist/hooks/useSelectPortal.js +14 -5
  150. package/dist/index.d.ts +0 -2
  151. package/dist/index.js +1 -57
  152. package/dist/shadcn-compat.css +80 -0
  153. package/dist/styles.css +1 -1
  154. package/dist/utils/stateMachine.d.ts +4 -4
  155. package/package.json +11 -10
  156. package/tailwind.preset.cjs +32 -3
  157. /package/{src/theme-plugin.css → dist/theme.css} +0 -0
@@ -1,11 +1,12 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import React__default, { useMemo, useEffect, useCallback } from 'react';
2
+ import React__default, { useId, useMemo, useEffect, useCallback } from 'react';
3
3
  import { useReducedMotion, motion } from 'motion/react';
4
4
  import { inputCounterTheme } from './InputCounter.theme.js';
5
5
  import { Button } from '../../core/Button/Button.js';
6
6
  import { SlidingNumber } from '../../core/SlidingNumber/SlidingNumber.js';
7
7
  import { cn } from '../../../utils/cn.js';
8
8
  import { MinusIcon, PlusIcon } from '@heroicons/react/24/outline';
9
+ import { FormLabel } from '../FormLabel/FormLabel.js';
9
10
 
10
11
  /**
11
12
  * InputCounter component with animated number display
@@ -33,9 +34,12 @@ import { MinusIcon, PlusIcon } from '@heroicons/react/24/outline';
33
34
  * );
34
35
  * ```
35
36
  */
36
- const InputCounter = React__default.forwardRef(({ value, onChange, min, max, step = 1, disabled = false, decimalPlaces, formatNumber = (n) => n.toString(), className, buttonProps, theme, transition = { type: "spring", bounce: 0, stiffness: 300, damping: 30 }, "aria-label": ariaLabel = "Counter", onAnimationStart, onAnimationEnd, onDrag, onDragStart, onDragEnd, ...props }, ref) => {
37
+ const InputCounter = React__default.forwardRef(({ value, onChange, state = "default", label, helperText, min, max, step = 1, disabled = false, decimalPlaces, formatNumber = (n) => n.toString(), className, buttonProps, theme, transition = { type: "spring", bounce: 0, stiffness: 300, damping: 30 }, "aria-label": ariaLabel = "Counter", onAnimationStart, onAnimationEnd, onDrag, onDragStart, onDragEnd, ...props }, ref) => {
37
38
  const shouldReduceMotion = useReducedMotion();
38
39
  const shouldDisableAnimations = !!shouldReduceMotion;
40
+ const generatedId = useId();
41
+ const isDisabled = state === "disabled" || disabled;
42
+ const isInvalid = state === "invalid";
39
43
  // Auto-detect decimal places from step if not explicitly provided
40
44
  const autoDecimalPlaces = useMemo(() => {
41
45
  if (decimalPlaces !== undefined)
@@ -68,12 +72,12 @@ const InputCounter = React__default.forwardRef(({ value, onChange, min, max, ste
68
72
  needsUpdate = true;
69
73
  }
70
74
  if (needsUpdate) {
71
- onChange(clampedValue);
75
+ onChange === null || onChange === void 0 ? void 0 : onChange(clampedValue);
72
76
  }
73
77
  }, [min, max, value, onChange]);
74
78
  // Handle increment
75
79
  const handleIncrement = useCallback(() => {
76
- if (disabled)
80
+ if (isDisabled)
77
81
  return;
78
82
  const newValue = value + step;
79
83
  // Round to avoid floating point precision errors
@@ -86,11 +90,11 @@ const InputCounter = React__default.forwardRef(({ value, onChange, min, max, ste
86
90
  }
87
91
  return;
88
92
  }
89
- onChange(roundedValue);
90
- }, [value, step, max, disabled, onChange]);
93
+ onChange === null || onChange === void 0 ? void 0 : onChange(roundedValue);
94
+ }, [value, step, max, isDisabled, onChange]);
91
95
  // Handle decrement
92
96
  const handleDecrement = useCallback(() => {
93
- if (disabled)
97
+ if (isDisabled)
94
98
  return;
95
99
  const newValue = value - step;
96
100
  // Round to avoid floating point precision errors
@@ -103,12 +107,12 @@ const InputCounter = React__default.forwardRef(({ value, onChange, min, max, ste
103
107
  }
104
108
  return;
105
109
  }
106
- onChange(roundedValue);
107
- }, [value, step, min, disabled, onChange]);
110
+ onChange === null || onChange === void 0 ? void 0 : onChange(roundedValue);
111
+ }, [value, step, min, isDisabled, onChange]);
108
112
  // Keyboard support
109
113
  useEffect(() => {
110
114
  const handleKeyDown = (event) => {
111
- if (disabled)
115
+ if (isDisabled)
112
116
  return;
113
117
  // Check if the counter is focused
114
118
  const target = event.target;
@@ -127,20 +131,20 @@ const InputCounter = React__default.forwardRef(({ value, onChange, min, max, ste
127
131
  event.preventDefault();
128
132
  const pageUpValue = value + step * 10;
129
133
  if (max === undefined || pageUpValue <= max) {
130
- onChange(pageUpValue);
134
+ onChange === null || onChange === void 0 ? void 0 : onChange(pageUpValue);
131
135
  }
132
136
  else if (max !== undefined) {
133
- onChange(max);
137
+ onChange === null || onChange === void 0 ? void 0 : onChange(max);
134
138
  }
135
139
  break;
136
140
  case "PageDown":
137
141
  event.preventDefault();
138
142
  const pageDownValue = value - step * 10;
139
143
  if (min === undefined || pageDownValue >= min) {
140
- onChange(pageDownValue);
144
+ onChange === null || onChange === void 0 ? void 0 : onChange(pageDownValue);
141
145
  }
142
146
  else if (min !== undefined) {
143
- onChange(min);
147
+ onChange === null || onChange === void 0 ? void 0 : onChange(min);
144
148
  }
145
149
  break;
146
150
  }
@@ -152,15 +156,18 @@ const InputCounter = React__default.forwardRef(({ value, onChange, min, max, ste
152
156
  step,
153
157
  min,
154
158
  max,
155
- disabled,
159
+ isDisabled,
156
160
  onChange,
157
161
  handleIncrement,
158
162
  handleDecrement,
159
163
  ]);
160
164
  // Check if buttons should be disabled
161
- const canDecrement = !disabled && (min === undefined || value > min);
162
- const canIncrement = !disabled && (max === undefined || value < max);
163
- return (jsxs(motion.div, { ref: ref, "data-counter": "true", layout: true, transition: shouldDisableAnimations ? undefined : transition, className: cn(mergedTheme.baseStyle, className), role: "spinbutton", "aria-label": ariaLabel, "aria-valuenow": value, "aria-valuemin": min, "aria-valuemax": max, "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, ...props, children: [jsx(motion.div, { className: mergedTheme.buttonWrapperStyle, whileHover: !disabled && !shouldDisableAnimations ? { scale: 1.05 } : undefined, whileTap: !disabled && !shouldDisableAnimations ? { scale: 0.95 } : undefined, children: jsx(Button, { size: "sm", variant: "link", iconOnly: true, ...buttonProps, onClick: handleDecrement, disabled: !canDecrement, "aria-label": "Decrement", children: jsx(MinusIcon, { className: "size-4 text-[var(--color-text-primary)]", strokeWidth: 2 }) }) }), jsx(SlidingNumber, { value: value, decimalPlaces: autoDecimalPlaces, className: mergedTheme.numberStyle, transition: transition }), jsx(motion.div, { className: mergedTheme.buttonWrapperStyle, whileHover: !disabled && !shouldDisableAnimations ? { scale: 1.05 } : undefined, whileTap: !disabled && !shouldDisableAnimations ? { scale: 0.95 } : undefined, children: jsx(Button, { size: "sm", variant: "link", iconOnly: true, ...buttonProps, onClick: handleIncrement, disabled: !canIncrement, "aria-label": "Increment", children: jsx(PlusIcon, { className: "size-4 text-[var(--color-text-primary)]", strokeWidth: 2 }) }) })] }));
165
+ const canDecrement = !isDisabled && (min === undefined || value > min);
166
+ const canIncrement = !isDisabled && (max === undefined || value < max);
167
+ const helperId = `${generatedId}-helper`;
168
+ return (jsxs("div", { className: "flex flex-col", children: [label && (typeof label === "string" ? (jsx(FormLabel, { htmlFor: `${generatedId}-counter`, state: isDisabled ? "disabled" : isInvalid ? "invalid" : "default", className: "mb-1.5", children: label })) : (jsx("div", { className: "mb-1.5", children: label }))), jsxs(motion.div, { ref: ref, id: `${generatedId}-counter`, "data-counter": "true", layout: true, transition: shouldDisableAnimations ? undefined : transition, className: cn(mergedTheme.baseStyle, isInvalid && "ring-1 ring-[var(--color-danger)]", className), role: "spinbutton", "aria-label": ariaLabel, "aria-valuenow": value, "aria-valuemin": min, "aria-valuemax": max, "aria-disabled": isDisabled, "aria-invalid": isInvalid || undefined, "aria-describedby": helperText ? helperId : undefined, tabIndex: isDisabled ? -1 : 0, ...props, children: [jsx(motion.div, { className: mergedTheme.buttonWrapperStyle, whileHover: !isDisabled && !shouldDisableAnimations ? { scale: 1.05 } : undefined, whileTap: !isDisabled && !shouldDisableAnimations ? { scale: 0.95 } : undefined, children: jsx(Button, { size: "sm", variant: "link", iconOnly: true, ...buttonProps, onClick: handleDecrement, disabled: !canDecrement, "aria-label": "Decrement", children: jsx(MinusIcon, { className: "size-4 text-[var(--color-text-primary)]", strokeWidth: 2 }) }) }), jsx(SlidingNumber, { value: value, decimalPlaces: autoDecimalPlaces, className: mergedTheme.numberStyle, transition: transition }), jsx(motion.div, { className: mergedTheme.buttonWrapperStyle, whileHover: !isDisabled && !shouldDisableAnimations ? { scale: 1.05 } : undefined, whileTap: !isDisabled && !shouldDisableAnimations ? { scale: 0.95 } : undefined, children: jsx(Button, { size: "sm", variant: "link", iconOnly: true, ...buttonProps, onClick: handleIncrement, disabled: !canIncrement, "aria-label": "Increment", children: jsx(PlusIcon, { className: "size-4 text-[var(--color-text-primary)]", strokeWidth: 2 }) }) })] }), helperText && (jsx("div", { id: helperId, className: cn("text-sm mt-1", isInvalid
169
+ ? "text-[var(--color-danger)]"
170
+ : "text-[var(--color-text-muted)]"), children: helperText }))] }));
164
171
  });
165
172
  InputCounter.displayName = "InputCounter";
166
173
 
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import type { Transition } from 'motion/react';
3
3
  import { ButtonProps } from '../../core/Button/Button.types';
4
+ export type InputCounterState = 'default' | 'disabled' | 'invalid';
4
5
  /**
5
6
  * InputCounter component props
6
7
  * @property {number} value - Current counter value (controlled)
@@ -18,7 +19,13 @@ export interface InputCounterProps extends Omit<React.HTMLAttributes<HTMLDivElem
18
19
  /** Current counter value (controlled) */
19
20
  value: number;
20
21
  /** Callback when value changes */
21
- onChange: (value: number) => void;
22
+ onChange?: (value: number) => void;
23
+ /** Validation/interaction state */
24
+ state?: InputCounterState;
25
+ /** Field label rendered above the counter */
26
+ label?: React.ReactNode;
27
+ /** Helper text rendered below the counter */
28
+ helperText?: React.ReactNode;
22
29
  /** Minimum allowed value (optional) */
23
30
  min?: number;
24
31
  /** Maximum allowed value (optional) */
@@ -16,4 +16,4 @@ import { InputCreditCardProps } from "./InputCreditCard.types";
16
16
  * />
17
17
  * ```
18
18
  */
19
- export declare const InputCreditCard: React.ForwardRefExoticComponent<InputCreditCardProps & React.RefAttributes<HTMLDivElement>>;
19
+ export declare const InputCreditCard: React.ForwardRefExoticComponent<InputCreditCardProps & React.RefAttributes<HTMLFieldSetElement>>;
@@ -1,8 +1,8 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import React__default, { useMemo, useState, useCallback } from 'react';
2
+ import React__default, { useState, useEffect, useMemo, useCallback, useId } from 'react';
3
3
  import { Input } from '../Input/Input.js';
4
- import { FormLabel } from '../FormLabel/FormLabel.js';
5
4
  import { InputCreditCardTheme } from './InputCreditCard.theme.js';
5
+ import { formsBaseTheme } from '../forms.theme.js';
6
6
 
7
7
  /**
8
8
  * InputCreditCard component for collecting credit card information
@@ -20,7 +20,17 @@ import { InputCreditCardTheme } from './InputCreditCard.theme.js';
20
20
  * />
21
21
  * ```
22
22
  */
23
- const InputCreditCard = React__default.forwardRef(({ onCardNumberChange, onExpiryDateChange, onCvcChange, expiryDateLabel = "MM / YY", cvcLabel = "CVC", legendText = "Payment Information", cardNumberInputProps = {}, expiryDateInputProps = {}, cvcInputProps = {}, className = "", theme = {}, }, ref) => {
23
+ const InputCreditCard = React__default.forwardRef(({ state = 'default', error, onCardNumberChange, onExpiryDateChange, onCvcChange, expiryDateLabel = "MM / YY", cvcLabel = "CVC", legendText = "Payment Information", cardNumberInputProps = {}, expiryDateInputProps = {}, cvcInputProps = {}, className = "", theme = {}, }, ref) => {
24
+ var _a;
25
+ const isDisabled = state === 'disabled';
26
+ // Clear the error visually as soon as the user starts re-entering after an error.
27
+ // Resets when the parent sets state back to 'invalid' (e.g. on re-submit).
28
+ const [isDirtyAfterError, setIsDirtyAfterError] = useState(false);
29
+ useEffect(() => {
30
+ if (state === 'invalid')
31
+ setIsDirtyAfterError(false);
32
+ }, [state]);
33
+ const isInvalid = state === 'invalid' && !isDirtyAfterError;
24
34
  // Merge the default theme with the provided theme overrides
25
35
  const mergedTheme = useMemo(() => ({
26
36
  ...InputCreditCardTheme,
@@ -43,18 +53,21 @@ const InputCreditCard = React__default.forwardRef(({ onCardNumberChange, onExpir
43
53
  const handleCardNumberChange = useCallback((e) => {
44
54
  const newCardNumber = e.target.value;
45
55
  setCardNumber(newCardNumber);
56
+ setIsDirtyAfterError(true);
46
57
  onCardNumberChange === null || onCardNumberChange === void 0 ? void 0 : onCardNumberChange(newCardNumber);
47
58
  }, [onCardNumberChange]);
48
59
  // Handle expiry date change
49
60
  const handleExpiryDateChange = useCallback((e) => {
50
61
  const newExpiryDate = e.target.value;
51
62
  setExpiryDate(newExpiryDate);
63
+ setIsDirtyAfterError(true);
52
64
  onExpiryDateChange === null || onExpiryDateChange === void 0 ? void 0 : onExpiryDateChange(newExpiryDate);
53
65
  }, [onExpiryDateChange]);
54
66
  // Handle CVC change
55
67
  const handleCvcChange = useCallback((e) => {
56
68
  const newCvc = e.target.value;
57
69
  setCvc(newCvc);
70
+ setIsDirtyAfterError(true);
58
71
  onCvcChange === null || onCvcChange === void 0 ? void 0 : onCvcChange(newCvc);
59
72
  }, [onCvcChange]);
60
73
  // Handle focus states
@@ -64,12 +77,17 @@ const InputCreditCard = React__default.forwardRef(({ onCardNumberChange, onExpir
64
77
  const handleBlur = useCallback(() => {
65
78
  setFocused(null);
66
79
  }, []);
67
- // Create unique IDs for accessibility
68
- useMemo(() => `card-fieldset-${Math.random().toString(36).substring(2, 9)}`, []);
69
- const cardNumberId = useMemo(() => `card-number-${Math.random().toString(36).substring(2, 9)}`, []);
70
- const expiryDateId = useMemo(() => `expiry-date-${Math.random().toString(36).substring(2, 9)}`, []);
71
- const cvcId = useMemo(() => `cvc-${Math.random().toString(36).substring(2, 9)}`, []);
72
- return (jsxs("div", { ref: ref, className: `w-full ${className}`, "data-testid": "input-credit-card-fieldset", children: [jsx(FormLabel, { htmlFor: cardNumberId, className: mergedTheme.legendStyles, children: legendText }), jsxs("div", { className: `grid gap-0 ${mergedTheme.contentStyles}`, "data-testid": "input-credit-card-content", children: [jsx("div", { className: "w-full relative focus-within:z-10", "data-testid": "input-card-number", children: jsx(Input, { id: cardNumberId, type: "creditCard", value: cardNumber, onChange: handleCardNumberChange, placeholder: "Card Number", "aria-label": "card number", onFocus: () => handleFocus("cardNumber"), onBlur: handleBlur, inputGroupClassName: "rounded-b-none", ...cardNumberInputProps }) }), jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 w-full -mt-px relative", children: [jsx("div", { className: `${mergedTheme.expiryDateWrapperStyles || ""} w-full sm:pr-0`, "data-testid": "input-expiry-date relative focus-within:z-10", children: jsx(Input, { id: expiryDateId, type: "expiryDate", value: expiryDate, onChange: handleExpiryDateChange, placeholder: expiryDateLabel, "aria-label": expiryDateLabel, onFocus: () => handleFocus("expiryDate"), onBlur: handleBlur, inputGroupClassName: "sm:rounded-bl-md rounded-t-none sm:rounded-tr-none sm:rounded-br-none relative focus-within:z-10", ...expiryDateInputProps }) }), jsx("div", { className: `${mergedTheme.cvcWrapperStyles || ""} w-full`, "data-testid": "input-cvc relative focus-within:z-10", children: jsx(Input, { id: cvcId, type: "tel", maxLength: 3, pattern: "[0-9]*", inputMode: "numeric", value: cvc, onChange: handleCvcChange, placeholder: cvcLabel, "aria-label": cvcLabel, onFocus: () => handleFocus("cvc"), onBlur: handleBlur, inputGroupClassName: " sm:rounded-br-md rounded-t-none rounded-bl-md sm:rounded-bl-none relative focus-within:z-10", ...cvcInputProps }) })] })] })] }));
80
+ const instanceId = useId();
81
+ const cardNumberId = `${instanceId}-card-number`;
82
+ const expiryDateId = `${instanceId}-expiry-date`;
83
+ const cvcId = `${instanceId}-cvc`;
84
+ const fieldState = isInvalid ? 'invalid' : isDisabled ? 'disabled' : 'default';
85
+ // Merge top-level state into each field's props; strip per-field helperText
86
+ // so only the consolidated error message is shown at the bottom.
87
+ const { helperText: _cn, ...cardProps } = { state: fieldState, ...cardNumberInputProps };
88
+ const { helperText: _ex, ...expiryProps } = { state: fieldState, ...expiryDateInputProps };
89
+ const { helperText: _cv, ...cvcProps } = { state: fieldState, ...cvcInputProps };
90
+ return (jsxs("fieldset", { ref: ref, className: `w-full border-0 m-0 p-0 ${isInvalid ? 'state-invalid' : ''} ${className}`, "data-testid": "input-credit-card-fieldset", children: [jsx("legend", { className: `block text-sm/6 font-medium mb-2 text-[var(--color-text-primary)] ${(_a = mergedTheme.legendStyles) !== null && _a !== void 0 ? _a : ""}`, children: legendText }), jsxs("div", { className: `grid gap-0 ${mergedTheme.contentStyles}`, "data-testid": "input-credit-card-content", children: [jsx("div", { className: "w-full relative focus-within:z-10", "data-testid": "input-card-number", children: jsx(Input, { id: cardNumberId, type: "creditCard", value: cardNumber, onChange: handleCardNumberChange, placeholder: "Card Number", "aria-label": "card number", onFocus: () => handleFocus("cardNumber"), onBlur: handleBlur, inputGroupClassName: "rounded-b-none", ...cardProps }) }), jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 w-full -mt-px relative", children: [jsx("div", { className: `${mergedTheme.expiryDateWrapperStyles || ""} w-full sm:pr-0`, "data-testid": "input-expiry-date relative focus-within:z-10", children: jsx(Input, { id: expiryDateId, type: "expiryDate", value: expiryDate, onChange: handleExpiryDateChange, placeholder: expiryDateLabel, "aria-label": expiryDateLabel, onFocus: () => handleFocus("expiryDate"), onBlur: handleBlur, inputGroupClassName: "sm:rounded-bl-md rounded-t-none sm:rounded-tr-none sm:rounded-br-none relative focus-within:z-10", ...expiryProps }) }), jsx("div", { className: `${mergedTheme.cvcWrapperStyles || ""} w-full`, "data-testid": "input-cvc relative focus-within:z-10", children: jsx(Input, { id: cvcId, type: "tel", maxLength: 3, pattern: "[0-9]*", inputMode: "numeric", value: cvc, onChange: handleCvcChange, placeholder: cvcLabel, "aria-label": cvcLabel, onFocus: () => handleFocus("cvc"), onBlur: handleBlur, inputGroupClassName: " sm:rounded-br-md rounded-t-none rounded-bl-md sm:rounded-bl-none relative focus-within:z-10", ...cvcProps }) })] })] }), error && (jsx("p", { className: formsBaseTheme.helperText, children: error }))] }));
73
91
  });
74
92
  InputCreditCard.displayName = "InputCreditCard";
75
93
 
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import { InputProps, InputTheme } from '../Input/Input.types';
2
3
  /**
3
4
  * InputCreditCard component props
@@ -10,6 +11,10 @@ import { InputProps, InputTheme } from '../Input/Input.types';
10
11
  * @property {InputCreditCardThemeOverrides} theme - Theme overrides for the credit card input
11
12
  */
12
13
  export interface InputCreditCardProps {
14
+ /** Visual/validation state of the entire card group */
15
+ state?: 'default' | 'disabled' | 'invalid';
16
+ /** Consolidated error message shown below all fields */
17
+ error?: React.ReactNode;
13
18
  /** Callback when card number changes */
14
19
  onCardNumberChange?: (cardNumber: string) => void;
15
20
  /** Callback when expiry date changes */
@@ -21,11 +21,16 @@ import { inputOTPTheme } from './InputOTP.theme.js';
21
21
  * />
22
22
  * ```
23
23
  */
24
- const InputOTP = React__default.forwardRef(({ length = 6, value = '', onChange, onComplete, disabled = false, label, helperText, error, autoFocus = true, className, ...props }, ref) => {
24
+ const InputOTP = React__default.forwardRef(({ length = 6, value, defaultValue, onChange, onComplete, state = 'default', disabled = false, label, helperText, error, autoFocus = true, className, id: idProp, ...props }, ref) => {
25
25
  const inputRefs = useRef([]);
26
26
  const theme = inputOTPTheme;
27
- // Internal state for uncontrolled usage
28
- const [internalValue, setInternalValue] = useState('');
27
+ const generatedId = React__default.useId();
28
+ const helperTextId = `${idProp !== null && idProp !== void 0 ? idProp : generatedId}-helper`;
29
+ // Derive unified state flags for backward compat
30
+ const isDisabled = state === 'disabled' || disabled;
31
+ const isInvalid = state === 'invalid' || !!error;
32
+ // Internal state for uncontrolled usage — seeded with defaultValue
33
+ const [internalValue, setInternalValue] = useState(defaultValue !== null && defaultValue !== void 0 ? defaultValue : '');
29
34
  // Use controlled value if provided, otherwise use internal state
30
35
  const currentValue = value !== undefined ? value : internalValue;
31
36
  const setValue = onChange || setInternalValue;
@@ -33,14 +38,14 @@ const InputOTP = React__default.forwardRef(({ length = 6, value = '', onChange,
33
38
  const valueArray = currentValue.padEnd(length, '').split('').slice(0, length);
34
39
  // Auto-focus first input on mount
35
40
  useEffect(() => {
36
- if (autoFocus && inputRefs.current[0] && !disabled) {
41
+ if (autoFocus && inputRefs.current[0] && !isDisabled) {
37
42
  inputRefs.current[0].focus();
38
43
  }
39
- }, [autoFocus, disabled]);
44
+ }, [autoFocus, isDisabled]);
40
45
  // Handle input change
41
46
  const handleChange = useCallback((index, newValue) => {
42
47
  var _a;
43
- if (disabled)
48
+ if (isDisabled)
44
49
  return;
45
50
  // Only allow single character (take last character if multiple)
46
51
  const char = newValue.slice(-1);
@@ -58,11 +63,11 @@ const InputOTP = React__default.forwardRef(({ length = 6, value = '', onChange,
58
63
  if (newValueString.length === length) {
59
64
  onComplete === null || onComplete === void 0 ? void 0 : onComplete(newValueString);
60
65
  }
61
- }, [disabled, valueArray, length, setValue, onComplete]);
66
+ }, [isDisabled, valueArray, length, setValue, onComplete]);
62
67
  // Handle key down for navigation and deletion
63
68
  const handleKeyDown = useCallback((index, event) => {
64
69
  var _a, _b, _c, _d, _e;
65
- if (disabled)
70
+ if (isDisabled)
66
71
  return;
67
72
  switch (event.key) {
68
73
  case 'Backspace':
@@ -99,11 +104,11 @@ const InputOTP = React__default.forwardRef(({ length = 6, value = '', onChange,
99
104
  (_e = inputRefs.current[length - 1]) === null || _e === void 0 ? void 0 : _e.focus();
100
105
  break;
101
106
  }
102
- }, [disabled, valueArray, length, handleChange]);
107
+ }, [isDisabled, valueArray, length, handleChange]);
103
108
  // Handle paste
104
109
  const handlePaste = useCallback((event) => {
105
110
  var _a;
106
- if (disabled)
111
+ if (isDisabled)
107
112
  return;
108
113
  event.preventDefault();
109
114
  const pastedData = event.clipboardData.getData('text').slice(0, length);
@@ -117,7 +122,7 @@ const InputOTP = React__default.forwardRef(({ length = 6, value = '', onChange,
117
122
  onComplete === null || onComplete === void 0 ? void 0 : onComplete(pastedData);
118
123
  }
119
124
  }
120
- }, [disabled, length, setValue, onComplete]);
125
+ }, [isDisabled, length, setValue, onComplete]);
121
126
  // Handle focus and blur (simplified since we removed focus state)
122
127
  const handleFocus = useCallback((index) => {
123
128
  // Focus logic can be added here if needed
@@ -126,11 +131,15 @@ const InputOTP = React__default.forwardRef(({ length = 6, value = '', onChange,
126
131
  // Blur logic can be added here if needed
127
132
  }, []);
128
133
  // State class for CSS selectors (matches Input component pattern)
129
- const stateClass = error
134
+ const stateClass = isInvalid
130
135
  ? "state-invalid"
131
- : disabled
136
+ : isDisabled
132
137
  ? "state-disabled"
133
138
  : "state-default";
139
+ // Resolved label state for FormLabel
140
+ const labelState = isInvalid ? 'invalid' : isDisabled ? 'disabled' : 'default';
141
+ // Resolved helper text to display — prefer helperText, fall back to error
142
+ const displayedHelperText = helperText !== null && helperText !== void 0 ? helperText : error;
134
143
  // Build classes with shadcn pattern - user className has highest priority
135
144
  const wrapperClasses = cn(theme.wrapperStyle, stateClass, className);
136
145
  const containerClasses = cn(theme.containerStyle);
@@ -155,20 +164,20 @@ const InputOTP = React__default.forwardRef(({ length = 6, value = '', onChange,
155
164
  });
156
165
  }
157
166
  };
158
- return (jsxs("div", { ref: ref, className: wrapperClasses, ...props, children: [label && (jsx(FormLabel, { htmlFor: `otp-input-0`, state: error ? "invalid" : disabled ? "disabled" : "default", children: label })), jsx("div", { className: containerClasses, children: length === 6 ? (
167
+ return (jsxs("div", { ref: ref, className: wrapperClasses, ...props, children: [label && (typeof label === 'string' ? (jsx(FormLabel, { htmlFor: "otp-input-0", state: labelState, children: label })) : (label)), jsx("div", { className: containerClasses, children: length === 6 ? (
159
168
  // For 6-digit OTP, create two groups of 3
160
169
  jsxs(Fragment, { children: [jsx("div", { className: "flex", children: Array.from({ length: 3 }, (_, i) => (jsx(Input, { ref: (el) => {
161
170
  inputRefs.current[i] = el;
162
- }, id: `otp-input-${i}`, type: "text", value: valueArray[i] || '', onChange: (e) => handleChange(i, e.target.value), onKeyDown: (e) => handleKeyDown(i, e), onFocus: () => handleFocus(i), onBlur: handleBlur, onPaste: i === 0 ? handlePaste : undefined, disabled: disabled, maxLength: 1, className: theme.inputStyle, inputGroupClassName: getInputClasses(i), wrapperClassName: "space-y-0 w-auto" }, i))) }), jsx("span", { className: theme.separatorStyle, children: "\u2014" }), jsx("div", { className: "flex", children: Array.from({ length: 3 }, (_, i) => {
171
+ }, id: `otp-input-${i}`, type: "text", value: valueArray[i] || '', onChange: (e) => handleChange(i, e.target.value), onKeyDown: (e) => handleKeyDown(i, e), onFocus: () => handleFocus(i), onBlur: handleBlur, onPaste: i === 0 ? handlePaste : undefined, disabled: isDisabled, state: isInvalid ? 'invalid' : isDisabled ? 'disabled' : 'default', "aria-describedby": displayedHelperText ? helperTextId : undefined, maxLength: 1, className: theme.inputStyle, inputGroupClassName: getInputClasses(i), wrapperClassName: "space-y-0 w-auto" }, i))) }), jsx("span", { className: theme.separatorStyle, children: "\u2014" }), jsx("div", { className: "flex", children: Array.from({ length: 3 }, (_, i) => {
163
172
  const index = i + 3;
164
173
  return (jsx(Input, { ref: (el) => {
165
174
  inputRefs.current[index] = el;
166
- }, id: `otp-input-${index}`, type: "text", value: valueArray[index] || '', onChange: (e) => handleChange(index, e.target.value), onKeyDown: (e) => handleKeyDown(index, e), onFocus: () => handleFocus(index), onBlur: handleBlur, disabled: disabled, maxLength: 1, className: theme.inputStyle, inputGroupClassName: getInputClasses(index), wrapperClassName: "space-y-0 w-auto" }, index));
175
+ }, id: `otp-input-${index}`, type: "text", value: valueArray[index] || '', onChange: (e) => handleChange(index, e.target.value), onKeyDown: (e) => handleKeyDown(index, e), onFocus: () => handleFocus(index), onBlur: handleBlur, disabled: isDisabled, state: isInvalid ? 'invalid' : isDisabled ? 'disabled' : 'default', "aria-describedby": displayedHelperText ? helperTextId : undefined, maxLength: 1, className: theme.inputStyle, inputGroupClassName: getInputClasses(index), wrapperClassName: "space-y-0 w-auto" }, index));
167
176
  }) })] })) : (
168
177
  // For other lengths, create one group
169
178
  jsx("div", { className: "flex", children: Array.from({ length }, (_, index) => (jsx(Input, { ref: (el) => {
170
179
  inputRefs.current[index] = el;
171
- }, id: `otp-input-${index}`, type: "text", value: valueArray[index] || '', onChange: (e) => handleChange(index, e.target.value), onKeyDown: (e) => handleKeyDown(index, e), onFocus: () => handleFocus(index), onBlur: handleBlur, onPaste: index === 0 ? handlePaste : undefined, disabled: disabled, maxLength: 1, className: theme.inputStyle, inputGroupClassName: getInputClasses(index), wrapperClassName: "space-y-0 w-auto" }, index))) })) }), (error || helperText) && (jsx("div", { className: cn(theme.helperText), children: error || helperText }))] }));
180
+ }, id: `otp-input-${index}`, type: "text", value: valueArray[index] || '', onChange: (e) => handleChange(index, e.target.value), onKeyDown: (e) => handleKeyDown(index, e), onFocus: () => handleFocus(index), onBlur: handleBlur, onPaste: index === 0 ? handlePaste : undefined, disabled: isDisabled, state: isInvalid ? 'invalid' : isDisabled ? 'disabled' : 'default', "aria-describedby": displayedHelperText ? helperTextId : undefined, maxLength: 1, className: theme.inputStyle, inputGroupClassName: getInputClasses(index), wrapperClassName: "space-y-0 w-auto" }, index))) })) }), displayedHelperText && (jsx("div", { id: helperTextId, className: cn(theme.helperText), children: displayedHelperText }))] }));
172
181
  });
173
182
  InputOTP.displayName = 'InputOTP';
174
183
 
@@ -1,13 +1,21 @@
1
- export interface InputOTPProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
1
+ /**
2
+ * State variants for the InputOTP component
3
+ */
4
+ export type InputOTPState = 'default' | 'disabled' | 'invalid';
5
+ export interface InputOTPProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'> {
2
6
  /**
3
7
  * Number of OTP input fields
4
8
  * @default 6
5
9
  */
6
10
  length?: number;
7
11
  /**
8
- * Current OTP value
12
+ * Current OTP value (controlled)
9
13
  */
10
14
  value?: string;
15
+ /**
16
+ * Default OTP value (uncontrolled)
17
+ */
18
+ defaultValue?: string;
11
19
  /**
12
20
  * Callback when OTP value changes
13
21
  */
@@ -16,21 +24,30 @@ export interface InputOTPProps extends Omit<React.HTMLAttributes<HTMLDivElement>
16
24
  * Callback when OTP is complete (all fields filled)
17
25
  */
18
26
  onComplete?: (value: string) => void;
27
+ /**
28
+ * Visual and functional state of the OTP input group.
29
+ * Use 'disabled' instead of the deprecated `disabled` boolean prop.
30
+ * Use 'invalid' instead of the deprecated `error` string prop.
31
+ * @default 'default'
32
+ */
33
+ state?: InputOTPState;
19
34
  /**
20
35
  * Whether the input is disabled
36
+ * @deprecated Use `state='disabled'` instead.
21
37
  * @default false
22
38
  */
23
39
  disabled?: boolean;
24
40
  /**
25
41
  * Label for the OTP input group
26
42
  */
27
- label?: string;
43
+ label?: React.ReactNode;
28
44
  /**
29
45
  * Helper text below the inputs
30
46
  */
31
- helperText?: string;
47
+ helperText?: React.ReactNode;
32
48
  /**
33
49
  * Error message
50
+ * @deprecated Use `state='invalid'` with `helperText` instead.
34
51
  */
35
52
  error?: string;
36
53
  /**
@@ -1,4 +1,4 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
1
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import React__default, { useMemo, useState, useRef, useEffect, useCallback } from 'react';
3
3
  import { AnimatePresence, motion } from 'motion/react';
4
4
  import { createPortal } from 'react-dom';
@@ -11,35 +11,8 @@ import { inputTagTheme } from './InputTag.theme.js';
11
11
  import { useSelectPortal } from '../../../hooks/useSelectPortal.js';
12
12
  import { createDropdownAnimations } from '../Select/Select.animations.js';
13
13
  import { useIsClient } from '../../../hooks/useIsClient.js';
14
+ import { useDarkMode } from '../../../hooks/useDarkMode.js';
14
15
 
15
- /**
16
- * Hook to detect dark mode state from document
17
- */
18
- const useDarkMode = () => {
19
- const [isDark, setIsDark] = useState(false);
20
- useEffect(() => {
21
- if (typeof document === "undefined")
22
- return;
23
- const checkDarkMode = () => {
24
- const hasDarkClass = document.documentElement.classList.contains("dark") ||
25
- document.body.classList.contains("dark") ||
26
- document.querySelector(".dark") !== null;
27
- setIsDark(hasDarkClass);
28
- };
29
- checkDarkMode();
30
- const observer = new MutationObserver(checkDarkMode);
31
- observer.observe(document.documentElement, {
32
- attributes: true,
33
- attributeFilter: ["class"],
34
- });
35
- observer.observe(document.body, {
36
- attributes: true,
37
- attributeFilter: ["class"],
38
- });
39
- return () => observer.disconnect();
40
- }, []);
41
- return isDark;
42
- };
43
16
  /**
44
17
  * Default filter function for suggestions
45
18
  */
@@ -97,16 +70,16 @@ const InputTagOptions = ({ isOpen, onClose, triggerRef, filteredOptions, selecte
97
70
  width: `${position.width}px`,
98
71
  }
99
72
  : {}),
100
- }, tabIndex: -1, variants: animations, initial: "initial", animate: "visible", exit: "exit", children: filteredOptions.length === 0 ? (jsx("div", { className: theme.emptyMessageStyle, children: emptyMessage })) : (filteredOptions.map((option, index) => {
101
- const isSelected = selectedTags.includes(option.value);
102
- const isHighlighted = index === highlightedIndex;
103
- const isCreateOption = option.id === "__create__";
104
- return (jsx("div", { role: "option", "aria-selected": isSelected, "aria-disabled": option.disabled, "data-option-index": index, className: cn(theme.optionStyle, isHighlighted && theme.highlightedOptionStyle, option.disabled && theme.disabledOptionStyle, isCreateOption && theme.createOptionStyle), onMouseEnter: () => !option.disabled && setHighlightedIndex(index), onClick: () => {
105
- if (!option.disabled) {
106
- onSelectOption(option, isCreateOption);
107
- }
108
- }, children: isCreateOption ? (jsxs("span", { className: "flex items-center gap-1", children: [jsx(PlusIcon, { className: "size-4 shrink-0", strokeWidth: 2 }), jsx("span", { className: "block truncate", children: option.label })] })) : (jsxs("span", { className: "flex items-center justify-between gap-2", children: [jsx("span", { className: "block truncate", children: option.label }), showSelectedIndicator && isSelected && (jsx(CheckIcon, { className: "size-4 shrink-0 text-[var(--color-primary)]", strokeWidth: 2 }))] })) }, option.id));
109
- })) })) }));
73
+ }, tabIndex: -1, variants: animations, initial: "initial", animate: "visible", exit: "exit", children: filteredOptions.length === 0 ? (jsx("div", { className: theme.emptyMessageStyle, children: emptyMessage })) : (jsxs(Fragment, { children: [(filteredOptions.length > 100 ? filteredOptions.slice(0, 100) : filteredOptions).map((option, index) => {
74
+ const isSelected = selectedTags.includes(option.value);
75
+ const isHighlighted = index === highlightedIndex;
76
+ const isCreateOption = option.id === "__create__";
77
+ return (jsx("div", { role: "option", "aria-selected": isSelected, "aria-disabled": option.disabled, "data-option-index": index, className: cn(theme.optionStyle, isHighlighted && theme.highlightedOptionStyle, option.disabled && theme.disabledOptionStyle, isCreateOption && theme.createOptionStyle), onMouseEnter: () => !option.disabled && setHighlightedIndex(index), onClick: () => {
78
+ if (!option.disabled) {
79
+ onSelectOption(option, isCreateOption);
80
+ }
81
+ }, children: isCreateOption ? (jsxs("span", { className: "flex items-center gap-1", children: [jsx(PlusIcon, { className: "size-4 shrink-0", strokeWidth: 2 }), jsx("span", { className: "block truncate", children: option.label })] })) : (jsxs("span", { className: "flex items-center justify-between gap-2", children: [jsx("span", { className: "block truncate", children: option.label }), showSelectedIndicator && isSelected && (jsx(CheckIcon, { className: "size-4 shrink-0 text-[var(--color-primary)]", strokeWidth: 2 }))] })) }, option.id));
82
+ }), filteredOptions.length > 100 && (jsxs("div", { className: "px-3 py-2 text-xs text-[var(--color-text-muted)] text-center border-t border-[var(--color-border)]", children: ["Showing 100 of ", filteredOptions.length, " results \u2014 refine your search"] }))] })) })) }));
110
83
  if (portal && isClient) {
111
84
  return createPortal(jsx("div", { className: isDarkMode ? "dark" : undefined, children: optionsContent }), document.body);
112
85
  }
@@ -33,9 +33,9 @@ export interface InputTagProps extends Omit<React.HTMLAttributes<HTMLDivElement>
33
33
  /** The state of the input */
34
34
  state?: InputTagState;
35
35
  /** Label for the input */
36
- label?: string;
36
+ label?: React.ReactNode;
37
37
  /** Helper text displayed below the tags */
38
- helperText?: string;
38
+ helperText?: React.ReactNode;
39
39
  /** Placeholder text for the input */
40
40
  placeholder?: string;
41
41
  /** Whether the input is disabled */
@@ -1,11 +1,12 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
- import React__default, { useMemo, useState, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
2
+ import React__default, { useMemo, useId, useState, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { AnimatePresence, motion } from 'motion/react';
5
5
  import { mentionTheme } from './Mention.theme.js';
6
6
  import { cn } from '../../../utils/cn.js';
7
7
  import { Avatar } from '../../core/Avatar/Avatar.js';
8
8
  import { Tag } from '../../core/Tag/Tag.js';
9
+ import { FormLabel } from '../FormLabel/FormLabel.js';
9
10
  import { useIsClient } from '../../../hooks/useIsClient.js';
10
11
  import { normalizeSegments, setCaretAtOffset, getPlainTextOffset, parseDOMToSegments, serializeSegments, serializeSegmentsMasked, insertMentionIntoSegments, MENTION_ATTR } from './Mention.utils.js';
11
12
 
@@ -73,8 +74,11 @@ const dropdownVariants = {
73
74
  // ---------------------------------------------------------------------------
74
75
  // Mention component
75
76
  // ---------------------------------------------------------------------------
76
- const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions: suggestionsProp, onSearch: onSearchProp, value: controlledValue, defaultValue = "", onChange, onMentionSelect, onMentionsChange, placeholder, size = "md", maxSuggestions = 8, className, wrapperClassName, dropdownClassName, theme: themeOverrides, disabled = false, renderSuggestion, emptyMessage = "No results found", loading = false, mentionTagSize = "sm", mentionTagClassName, rows = 3, ...props }, ref) => {
77
+ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions: suggestionsProp, onSearch: onSearchProp, value: controlledValue, defaultValue = "", onChange, onMentionSelect, onMentionsChange, placeholder, size = "md", maxSuggestions = 8, className, wrapperClassName, dropdownClassName, theme: themeOverrides, state = "default", disabled = false, helperText, required, label, renderSuggestion, emptyMessage = "No results found", loading = false, mentionTagSize = "sm", mentionTagClassName, rows = 3, ...props }, ref) => {
77
78
  const theme = useMemo(() => ({ ...mentionTheme, ...(themeOverrides || {}) }), [themeOverrides]);
79
+ const generatedId = useId();
80
+ const isDisabled = state === "disabled" || disabled;
81
+ const isInvalid = state === "invalid";
78
82
  const triggers = useMemo(() => {
79
83
  if (triggersProp && triggersProp.length > 0)
80
84
  return triggersProp;
@@ -150,7 +154,7 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
150
154
  if (!activeMention || !(activeTriggerConfig === null || activeTriggerConfig === void 0 ? void 0 : activeTriggerConfig.onSearch))
151
155
  return;
152
156
  activeTriggerConfig.onSearch(activeMention.query);
153
- }, [activeMention === null || activeMention === void 0 ? void 0 : activeMention.query, activeMention === null || activeMention === void 0 ? void 0 : activeMention.trigger]); // eslint-disable-line react-hooks/exhaustive-deps
157
+ }, [activeMention === null || activeMention === void 0 ? void 0 : activeMention.query, activeMention === null || activeMention === void 0 ? void 0 : activeMention.trigger, activeTriggerConfig]);
154
158
  // Reset highlighted index when suggestions change
155
159
  useEffect(() => {
156
160
  setHighlightedIndex(filteredSuggestions.length > 0 ? 0 : -1);
@@ -596,11 +600,14 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
596
600
  // -----------------------------------------------------------------------
597
601
  // Render
598
602
  // -----------------------------------------------------------------------
599
- return (jsxs("div", { className: cn(theme.wrapperStyle, wrapperClassName), children: [jsx("div", { ref: wrapperRef, className: cn(theme.inputGroupStyle, isFocused && theme.inputGroupFocusStyle), children: jsx("div", { ref: setEditableRef, contentEditable: !disabled, suppressContentEditableWarning: true, onInput: handleInput, onKeyDown: handleKeyDown, onClick: handleClick, onFocus: handleFocus, onBlur: handleBlur, onPaste: handlePaste, onSelect: handleSelect, onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd, role: "textbox", "aria-multiline": "true", "aria-autocomplete": "list", "aria-expanded": isDropdownOpen, "aria-haspopup": "listbox", "aria-placeholder": placeholder, "data-placeholder": placeholder, "aria-disabled": disabled || undefined, className: cn(theme.editableStyle, theme.sizes[size], isContentEmpty &&
600
- "before:content-[attr(data-placeholder)] before:text-[var(--color-text-placeholder)] before:pointer-events-none before:absolute dark:before:text-[var(--color-neutral-500)]", disabled && "cursor-not-allowed opacity-60", className), style: {
603
+ const helperId = `${generatedId}-helper`;
604
+ return (jsxs("div", { className: cn(theme.wrapperStyle, wrapperClassName), children: [label && (jsx(FormLabel, { htmlFor: generatedId, state: isDisabled ? "disabled" : isInvalid ? "invalid" : "default", required: required, className: "mb-1.5", children: label })), jsx("div", { ref: wrapperRef, className: cn(theme.inputGroupStyle, isFocused && theme.inputGroupFocusStyle, isInvalid && "border-[var(--color-danger)] ring-1 ring-[var(--color-danger)]"), children: jsx("div", { ref: setEditableRef, id: generatedId, contentEditable: !isDisabled, suppressContentEditableWarning: true, onInput: handleInput, onKeyDown: handleKeyDown, onClick: handleClick, onFocus: handleFocus, onBlur: handleBlur, onPaste: handlePaste, onSelect: handleSelect, onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd, role: "textbox", "aria-multiline": "true", "aria-autocomplete": "list", "aria-expanded": isDropdownOpen, "aria-haspopup": "listbox", "aria-placeholder": placeholder, "aria-required": required || undefined, "data-placeholder": placeholder, "aria-disabled": isDisabled || undefined, "aria-invalid": isInvalid || undefined, "aria-describedby": helperText ? helperId : undefined, className: cn(theme.editableStyle, theme.sizes[size], isContentEmpty &&
605
+ "before:content-[attr(data-placeholder)] before:text-[var(--color-text-placeholder)] before:pointer-events-none before:absolute dark:before:text-[var(--color-neutral-500)]", isDisabled && "cursor-not-allowed opacity-60", className), style: {
601
606
  minHeight: rows ? `${rows * 1.5}em` : undefined,
602
607
  position: "relative",
603
- }, ...props, children: renderedSegments }) }), isClient && createPortal(dropdownContent, document.body)] }));
608
+ }, ...props, children: renderedSegments }) }), helperText && (jsx("div", { id: helperId, className: cn("text-sm mt-1", isInvalid
609
+ ? "text-[var(--color-danger)]"
610
+ : "text-[var(--color-text-muted)]"), children: helperText })), isClient && createPortal(dropdownContent, document.body)] }));
604
611
  });
605
612
  Mention.displayName = "Mention";
606
613
 
@@ -100,6 +100,7 @@ export interface MentionTheme {
100
100
  * Partial theme overrides for the Mention component.
101
101
  */
102
102
  export type MentionThemeOverrides = Partial<MentionTheme>;
103
+ export type MentionState = "default" | "disabled" | "invalid";
103
104
  /**
104
105
  * Props for the Mention component.
105
106
  */
@@ -134,8 +135,14 @@ export interface MentionProps extends Omit<React.HTMLAttributes<HTMLDivElement>,
134
135
  dropdownClassName?: string;
135
136
  /** Theme overrides */
136
137
  theme?: MentionThemeOverrides;
138
+ /** Validation/interaction state */
139
+ state?: MentionState;
137
140
  /** Whether the mention input is disabled */
138
141
  disabled?: boolean;
142
+ /** Helper text rendered below the mention input */
143
+ helperText?: React.ReactNode;
144
+ /** Whether the field is required */
145
+ required?: boolean;
139
146
  /** Custom renderer for suggestion items */
140
147
  renderSuggestion?: (suggestion: MentionSuggestion, isHighlighted: boolean, trigger: string) => React.ReactNode;
141
148
  /** Message shown when no suggestions match */
@@ -148,4 +155,6 @@ export interface MentionProps extends Omit<React.HTMLAttributes<HTMLDivElement>,
148
155
  mentionTagClassName?: string;
149
156
  /** Number of visible text rows (controls min-height) */
150
157
  rows?: number;
158
+ /** Label rendered above the mention input */
159
+ label?: string;
151
160
  }