@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
@@ -44,9 +44,9 @@ export interface DateRangePickerProps extends Omit<React.HTMLAttributes<HTMLDivE
44
44
  /** Whether the field is required */
45
45
  required?: boolean;
46
46
  /** Label for the field */
47
- label?: string;
47
+ label?: React.ReactNode;
48
48
  /** Helper text below the field */
49
- helperText?: string;
49
+ helperText?: React.ReactNode;
50
50
  /** Callback fired when a range is applied */
51
51
  onChange?: (range: DateRange | null) => void;
52
52
  /** Array of preset date ranges */
@@ -103,6 +103,8 @@ export interface DateRangePickerTriggerProps {
103
103
  onFocus?: (event: React.FocusEvent<HTMLButtonElement>) => void;
104
104
  onBlur?: (event: React.FocusEvent<HTMLButtonElement>) => void;
105
105
  children?: React.ReactNode;
106
+ "aria-invalid"?: boolean;
107
+ "aria-describedby"?: string;
106
108
  }
107
109
  /**
108
110
  * DateRangePicker body props
@@ -13,35 +13,8 @@ import { formatDateCustom } from '../../../utils/dateUtils.js';
13
13
  import { createDropdownAnimations } from '../DatePicker/DatePicker.animations.js';
14
14
  import { useIsClient } from '../../../hooks/useIsClient.js';
15
15
  import { useMediaQuery } from '../../../hooks/useMediaQuery.js';
16
+ import { useDarkMode } from '../../../hooks/useDarkMode.js';
16
17
 
17
- /**
18
- * Hook to detect dark mode state from document
19
- */
20
- const useDarkMode = () => {
21
- const [isDark, setIsDark] = useState(false);
22
- useEffect(() => {
23
- if (typeof document === "undefined")
24
- return;
25
- const checkDarkMode = () => {
26
- const hasDarkClass = document.documentElement.classList.contains("dark") ||
27
- document.body.classList.contains("dark") ||
28
- document.querySelector(".dark") !== null;
29
- setIsDark(hasDarkClass);
30
- };
31
- checkDarkMode();
32
- const observer = new MutationObserver(checkDarkMode);
33
- observer.observe(document.documentElement, {
34
- attributes: true,
35
- attributeFilter: ["class"],
36
- });
37
- observer.observe(document.body, {
38
- attributes: true,
39
- attributeFilter: ["class"],
40
- });
41
- return () => observer.disconnect();
42
- }, []);
43
- return isDark;
44
- };
45
18
  /**
46
19
  * DateRangePicker Body Component — dual calendars + presets sidebar + footer
47
20
  */
@@ -7,7 +7,7 @@ import { DateRangePickerContext } from './DateRangePicker.js';
7
7
  /**
8
8
  * DateRangePicker Trigger Component — styled like form input
9
9
  */
10
- const DateRangePickerTrigger = ({ id, disabled, isOpen, state = "default", size = "md", className, onClick, onFocus, onBlur, children, }) => {
10
+ const DateRangePickerTrigger = ({ id, disabled, isOpen, state = "default", size = "md", className, onClick, onFocus, onBlur, children, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedby, }) => {
11
11
  var _a, _b, _c, _d;
12
12
  const context = useContext(DateRangePickerContext);
13
13
  if (!context) {
@@ -26,7 +26,7 @@ const DateRangePickerTrigger = ({ id, disabled, isOpen, state = "default", size
26
26
  ? focusStateStyle ||
27
27
  "outline-2 outline-primary-600 -outline-offset-1 ring-4 ring-[var(--color-primary)]/10"
28
28
  : "";
29
- return (jsx("div", { className: "relative w-full", children: jsxs("button", { ref: triggerRef, id: id, type: "button", className: cn(baseStyle, sizeStyle, stateStyle, hoverStateStyle, activeFocusState, iconEndPadding, triggerButtonStyle, "text-left", className), onClick: onClick, onFocus: onFocus, onBlur: onBlur, disabled: disabled, "aria-expanded": isOpen, "aria-haspopup": "dialog", "aria-label": "Open date range picker", children: [children, jsx("span", { className: iconEndStyle, children: jsx(CalendarIcon, { className: "h-4 w-4" }) })] }) }));
29
+ return (jsx("div", { className: "relative w-full", children: jsxs("button", { ref: triggerRef, id: id, type: "button", className: cn(baseStyle, sizeStyle, stateStyle, hoverStateStyle, activeFocusState, iconEndPadding, triggerButtonStyle, "text-left", className), onClick: onClick, onFocus: onFocus, onBlur: onBlur, disabled: disabled, "aria-expanded": isOpen, "aria-haspopup": "dialog", "aria-label": "Open date range picker", "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedby, children: [children, jsx("span", { className: iconEndStyle, children: jsx(CalendarIcon, { className: "h-4 w-4" }) })] }) }));
30
30
  };
31
31
 
32
32
  export { DateRangePickerTrigger };
@@ -1,5 +1,5 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import React__default, { useMemo, useRef, useState, useCallback } from 'react';
2
+ import React__default, { useMemo, useId, useRef, useState, useCallback } from 'react';
3
3
  import { fileUploadTheme } from './FileUpload.theme.js';
4
4
  import { cn } from '../../../utils/cn.js';
5
5
  import { CloudArrowUpIcon, DocumentIcon, XMarkIcon } from '@heroicons/react/24/outline';
@@ -89,6 +89,7 @@ import '../../core/ContextMenu/ContextMenu.js';
89
89
  import '../../core/ContextMenu/ContextMenu.theme.js';
90
90
  import '../../core/Pill/Pill.js';
91
91
  import '../../core/Icon/Icon.js';
92
+ import { FormLabel } from '../FormLabel/FormLabel.js';
92
93
 
93
94
  /**
94
95
  * FileUpload component - Drag-and-drop file uploader with previews
@@ -110,11 +111,14 @@ import '../../core/Icon/Icon.js';
110
111
  * onUpload={(files) => console.log(files)}
111
112
  * />
112
113
  */
113
- const FileUpload = React__default.forwardRef(({ accept, multiple = false, maxSize, maxFiles, disabled = false, onUpload, onRemove, onError, showPreview = true, buttonText = "Choose files", helperText = "or drag and drop", description, className, theme: customTheme, ...props }, ref) => {
114
+ const FileUpload = React__default.forwardRef(({ accept, multiple = false, maxSize, maxFiles, state = "default", disabled = false, label, errorMessage, onUpload, onRemove, onError, showPreview = true, buttonText = "Choose files", helperText = "or drag and drop", description, className, theme: customTheme, ...props }, ref) => {
114
115
  // Merge custom theme with default theme
115
116
  const theme = useMemo(() => customTheme
116
117
  ? { ...fileUploadTheme, ...customTheme }
117
118
  : fileUploadTheme, [customTheme]);
119
+ const generatedId = useId();
120
+ const isDisabled = state === "disabled" || disabled;
121
+ const isInvalid = state === "invalid";
118
122
  const inputRef = useRef(null);
119
123
  const [isDragActive, setIsDragActive] = useState(false);
120
124
  const [selectedFiles, setSelectedFiles] = useState([]);
@@ -229,9 +233,9 @@ const FileUpload = React__default.forwardRef(({ accept, multiple = false, maxSiz
229
233
  const handleDragEnter = useCallback((e) => {
230
234
  e.preventDefault();
231
235
  e.stopPropagation();
232
- if (!disabled)
236
+ if (!isDisabled)
233
237
  setIsDragActive(true);
234
- }, [disabled]);
238
+ }, [isDisabled]);
235
239
  const handleDragLeave = useCallback((e) => {
236
240
  e.preventDefault();
237
241
  e.stopPropagation();
@@ -245,18 +249,18 @@ const FileUpload = React__default.forwardRef(({ accept, multiple = false, maxSiz
245
249
  e.preventDefault();
246
250
  e.stopPropagation();
247
251
  setIsDragActive(false);
248
- if (disabled)
252
+ if (isDisabled)
249
253
  return;
250
254
  const files = e.dataTransfer.files;
251
255
  handleFiles(files);
252
- }, [disabled, handleFiles]);
256
+ }, [isDisabled, handleFiles]);
253
257
  // Handle click to select files
254
258
  const handleClick = useCallback(() => {
255
259
  var _a;
256
- if (!disabled) {
260
+ if (!isDisabled) {
257
261
  (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click();
258
262
  }
259
- }, [disabled]);
263
+ }, [isDisabled]);
260
264
  // Handle input change
261
265
  const handleInputChange = useCallback((e) => {
262
266
  handleFiles(e.target.files);
@@ -278,12 +282,12 @@ const FileUpload = React__default.forwardRef(({ accept, multiple = false, maxSiz
278
282
  const i = Math.floor(Math.log(bytes) / Math.log(k));
279
283
  return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
280
284
  };
281
- return (jsxs("div", { ref: ref, className: cn(theme.containerStyle, className), ...props, children: [jsx("input", { ref: inputRef, type: "file", accept: acceptString, multiple: multiple, onChange: handleInputChange, disabled: disabled, className: "sr-only", "aria-label": "File upload" }), jsx("div", { className: cn(theme.dropzoneStyle, isDragActive && theme.dropzoneActiveStyle, disabled && theme.dropzoneDisabledStyle), onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, onClick: handleClick, role: "button", tabIndex: disabled ? -1 : 0, "aria-disabled": disabled, onKeyDown: (e) => {
285
+ return (jsxs("div", { ref: ref, className: cn(theme.containerStyle, className), ...props, children: [label && (typeof label === "string" ? (jsx(FormLabel, { htmlFor: `${generatedId}-input`, state: isDisabled ? "disabled" : isInvalid ? "invalid" : "default", children: label })) : (label)), jsx("input", { ref: inputRef, id: `${generatedId}-input`, type: "file", accept: acceptString, multiple: multiple, onChange: handleInputChange, disabled: isDisabled, className: "sr-only", "aria-label": "File upload" }), jsx("div", { className: cn(theme.dropzoneStyle, isDragActive && theme.dropzoneActiveStyle, isDisabled && theme.dropzoneDisabledStyle, isInvalid && "border-[var(--color-danger)]"), onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, onClick: handleClick, role: "button", tabIndex: isDisabled ? -1 : 0, "aria-disabled": isDisabled, "aria-invalid": isInvalid || undefined, onKeyDown: (e) => {
282
286
  if (e.key === "Enter" || e.key === " ") {
283
287
  e.preventDefault();
284
288
  handleClick();
285
289
  }
286
- }, children: jsxs("div", { className: theme.contentStyle, children: [jsx("div", { className: "size-11 border border-border bg-white rounded-full flex items-center justify-center mb-4", children: jsx(CloudArrowUpIcon, { className: `size-6 text-[var(--color-text-secondary)]` }) }), jsxs("p", { className: "text-base font-medium text-[var(--color-text-primary)]", children: [jsx("span", { className: "text-[var(--color-primary)] font-semibold", children: buttonText }), " ", helperText] }), description && (jsx("p", { className: "text-sm text-[var(--color-text-muted)] mt-1", children: description }))] }) }), showPreview && selectedFiles.length > 0 && (jsx("div", { className: theme.previewContainerStyle, children: selectedFiles.map((file, index) => {
290
+ }, children: jsxs("div", { className: theme.contentStyle, children: [jsx("div", { className: "size-11 border border-border bg-white rounded-full flex items-center justify-center mb-4", children: jsx(CloudArrowUpIcon, { className: `size-6 text-[var(--color-text-secondary)]` }) }), jsxs("p", { className: "text-base font-medium text-[var(--color-text-primary)]", children: [jsx("span", { className: "text-[var(--color-primary)] font-semibold", children: buttonText }), " ", helperText] }), description && (jsx("p", { className: "text-sm text-[var(--color-text-muted)] mt-1", children: description }))] }) }), errorMessage && (jsx("div", { className: "text-sm mt-1 text-[var(--color-danger)]", children: errorMessage })), showPreview && selectedFiles.length > 0 && (jsx("div", { className: theme.previewContainerStyle, children: selectedFiles.map((file, index) => {
287
291
  const previewUrl = previewUrls.get(file);
288
292
  const isImage = file.type.startsWith("image/");
289
293
  return (jsxs("div", { className: theme.previewItemStyle, children: [isImage && previewUrl ? (jsx("img", { src: previewUrl, alt: file.name, className: theme.previewImageStyle })) : (jsxs("div", { className: theme.previewFileStyle, children: [jsx(DocumentIcon, { className: "w-8 h-8 mx-auto mb-2 text-[var(--color-text-muted)]" }), jsx("span", { className: "text-sm", children: file.name }), jsx("span", { className: "text-sm text-[var(--color-text-placeholder)] mt-1", children: formatFileSize(file.size) })] })), jsx(Button, { variant: "filled", color: "danger", size: "sm", iconOnly: true, onClick: (e) => {
@@ -1,3 +1,5 @@
1
+ import React from "react";
2
+ export type FileUploadState = "default" | "disabled" | "invalid";
1
3
  /**
2
4
  * FileUpload component props
3
5
  * Supports both click-to-upload and drag-and-drop
@@ -22,11 +24,24 @@ export interface FileUploadProps extends Omit<React.HTMLAttributes<HTMLDivElemen
22
24
  * Maximum number of files (when multiple is true)
23
25
  */
24
26
  maxFiles?: number;
27
+ /**
28
+ * Validation/interaction state
29
+ * @default "default"
30
+ */
31
+ state?: FileUploadState;
25
32
  /**
26
33
  * Whether the upload area is disabled
27
34
  * @default false
28
35
  */
29
36
  disabled?: boolean;
37
+ /**
38
+ * Field label rendered above the dropzone
39
+ */
40
+ label?: React.ReactNode;
41
+ /**
42
+ * Validation error message rendered below the dropzone (styled in danger color)
43
+ */
44
+ errorMessage?: React.ReactNode;
30
45
  /**
31
46
  * Callback when files are selected/dropped
32
47
  */
@@ -1,4 +1,4 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
1
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
2
  import React__default from 'react';
3
3
  import { formLabelTheme } from './FormLabel.theme.js';
4
4
  import { cn } from '../../../utils/cn.js';
@@ -7,7 +7,7 @@ import { cn } from '../../../utils/cn.js';
7
7
  * FormLabel component for providing consistent labeling for form elements
8
8
  */
9
9
  const FormLabel = React__default.forwardRef(({ htmlFor, children, className, required = false, state = 'default', ...props }, ref) => {
10
- return (jsxs("label", { ref: ref, htmlFor: htmlFor, className: cn(formLabelTheme.baseStyle, formLabelTheme.stateStyles[state], className), ...props, children: [children, required && (jsx("span", { className: formLabelTheme.requiredIndicatorStyle, children: "*" }))] }));
10
+ return (jsxs("label", { ref: ref, htmlFor: htmlFor, className: cn(formLabelTheme.baseStyle, formLabelTheme.stateStyles[state], className), ...props, children: [children, required && (jsxs(Fragment, { children: [jsx("span", { "aria-hidden": "true", className: formLabelTheme.requiredIndicatorStyle, children: "*" }), jsx("span", { className: "sr-only", children: "(required)" })] }))] }));
11
11
  });
12
12
  FormLabel.displayName = "FormLabel";
13
13
 
@@ -62,12 +62,12 @@ const PasswordStrengthIndicator = ({ inline = false, passwordStrengthIndicator,
62
62
  const colorClass = strengthConfig.color || "text-neutral-500";
63
63
  const label = customLabel || strengthConfig.label || "Password";
64
64
  const activeDots = getActiveDots(strength);
65
- return (jsxs("div", { className: `${containerStyle} ${indicatorClassName} ${colorClass}`, children: [jsx("div", { className: labelStyle, children: label }), [...Array(5)].map((_, index) => (jsx("div", { className: `${dotStyle} ${index < activeDots ? dotActiveStyle : dotInactiveStyle}` }, index)))] }));
65
+ return (jsxs("div", { className: `${containerStyle} ${indicatorClassName} ${colorClass}`, children: [jsx("div", { className: labelStyle, children: label }), [...Array(5)].map((_, index) => (jsx("div", { className: `${dotStyle} ${index < activeDots ? dotActiveStyle : dotInactiveStyle}` }, `dot-${index}`)))] }));
66
66
  };
67
67
  // ============================================================================
68
68
  // Main Input Component
69
69
  // ============================================================================
70
- const Input = React__default.forwardRef(({ size = "md", state = "default", className = "", label, labelClassName = "", helperText, helperTextClassName = "", wrapperClassName = "", inputGroupClassName = "", iconStart, iconEnd, contentStart, contentEnd, keyboardShortcut, id, required, type = "text", passwordToggle = type === "password", passwordStrengthIndicator, showNumberControls = false, min, max, step = 1, onChange, onFocus, onBlur, value, currencyDecimalPlaces, darkMode = false, showTypeIcon = true, ...props }, ref) => {
70
+ const Input = React__default.forwardRef(({ size = "md", state = "default", className = "", label, labelClassName = "", helperText, helperTextClassName = "", wrapperClassName = "", inputGroupClassName = "", iconStart, iconEnd, contentStart, contentEnd, keyboardShortcut, id, required, type = "text", passwordToggle = type === "password", passwordStrengthIndicator, showNumberControls = false, min, max, step = 1, onChange, onValueChange, onFocus, onBlur, value, currencyDecimalPlaces, darkMode = false, showTypeIcon = true, ...props }, ref) => {
71
71
  const [isFocused, setIsFocused] = useState(false);
72
72
  const [showPassword, setShowPassword] = useState(false);
73
73
  const generatedId = React__default.useId();
@@ -201,6 +201,7 @@ const Input = React__default.forwardRef(({ size = "md", state = "default", class
201
201
  if (onChange) {
202
202
  onChange(event);
203
203
  }
204
+ onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(newValue.toString());
204
205
  }
205
206
  };
206
207
  // Handle input change
@@ -240,6 +241,7 @@ const Input = React__default.forwardRef(({ size = "md", state = "default", class
240
241
  if (onChange) {
241
242
  onChange(e);
242
243
  }
244
+ onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(e.target.value);
243
245
  };
244
246
  // Theme classes - using simplified approach with design tokens
245
247
  const wrapperClasses = inputTheme.wrapperStyle;
@@ -277,7 +279,7 @@ const Input = React__default.forwardRef(({ size = "md", state = "default", class
277
279
  }, onBlur: (e) => {
278
280
  setIsFocused(false);
279
281
  onBlur === null || onBlur === void 0 ? void 0 : onBlur(e);
280
- }, placeholder: props.placeholder, ...props }), hasLeftIcon && (jsx("div", { className: iconStartClasses, children: finalLeftIcon })), hasRightIcon && !isPasswordField && !displayNumberControls && (jsx("div", { className: iconEndClasses, children: iconEnd })), isPasswordField && (jsx(PasswordToggleButton, { showPassword: showPassword, onToggle: handleTogglePassword, passwordToggleClasses: passwordToggleClasses })), displayNumberControls && (jsx(NumberButtons, { isDisabled: isDisabled, isAtMax: isAtMax, isAtMin: isAtMin, onIncrement: handleIncrement, onDecrement: handleDecrement, numberButtonsContainerClasses: numberButtonsContainerClasses, numberButtonClasses: numberButtonClasses })), hasKeyboardShortcut && (jsx("div", { className: inputTheme.keyboardShortcutStyle, children: keyboardShortcut }))] }), hasContentEnd && (jsx("div", { "data-slot": "content-end", className: cn(contentStyleClasses, contentEndStyle), children: contentEnd }))] }) }), showStrengthBelow && jsx(PasswordStrengthIndicator, { inline: false, passwordStrengthIndicator: passwordStrengthIndicator }), helperText && (typeof helperText === 'string' ? (jsx("div", { id: `${inputId}-helper`, className: cn(helperTextClasses, helperTextClassName), children: helperText })) : (helperText))] }));
282
+ }, placeholder: props.placeholder, ...props }), hasLeftIcon && (jsx("div", { className: iconStartClasses, children: finalLeftIcon })), hasRightIcon && !isPasswordField && !displayNumberControls && (jsx("div", { className: iconEndClasses, children: iconEnd })), isPasswordField && (jsx(PasswordToggleButton, { showPassword: showPassword, onToggle: handleTogglePassword, passwordToggleClasses: passwordToggleClasses })), displayNumberControls && (jsx(NumberButtons, { isDisabled: isDisabled, isAtMax: isAtMax, isAtMin: isAtMin, onIncrement: handleIncrement, onDecrement: handleDecrement, numberButtonsContainerClasses: numberButtonsContainerClasses, numberButtonClasses: numberButtonClasses })), hasKeyboardShortcut && (jsx("div", { className: inputTheme.keyboardShortcutStyle, children: keyboardShortcut }))] }), hasContentEnd && (jsx("div", { "data-slot": "content-end", className: cn(contentStyleClasses, contentEndStyle), children: contentEnd }))] }) }), showStrengthBelow && jsx(PasswordStrengthIndicator, { inline: false, passwordStrengthIndicator: passwordStrengthIndicator }), helperText && (typeof helperText === 'string' ? (jsx("div", { id: `${inputId}-helper`, className: cn(helperTextClasses, helperTextClassName), role: isInvalid ? "alert" : undefined, children: helperText })) : (helperText))] }));
281
283
  });
282
284
  Input.displayName = "Input";
283
285
 
@@ -69,6 +69,8 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
69
69
  * @default false
70
70
  */
71
71
  darkMode?: boolean;
72
+ /** Callback fired with the raw string value on every change — convenience alternative to reading e.target.value from onChange */
73
+ onValueChange?: (value: string) => void;
72
74
  }
73
75
  export type InputSize = 'sm' | 'md' | 'lg';
74
76
  export type InputState = 'default' | 'disabled' | 'invalid';
@@ -16,4 +16,4 @@ import { InputAddressProps } from "./InputAddress.types";
16
16
  * />
17
17
  * ```
18
18
  */
19
- export declare const InputAddress: React.ForwardRefExoticComponent<InputAddressProps & React.RefAttributes<HTMLDivElement>>;
19
+ export declare const InputAddress: React.ForwardRefExoticComponent<InputAddressProps & React.RefAttributes<HTMLFieldSetElement>>;
@@ -1,7 +1,6 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import React__default, { useMemo, useState, useCallback } from 'react';
2
+ import React__default, { useMemo, useState, useCallback, useId } from 'react';
3
3
  import { Input } from '../Input/Input.js';
4
- import { FormLabel } from '../FormLabel/FormLabel.js';
5
4
  import { inputAddressTheme } from './InputAddress.theme.js';
6
5
 
7
6
  /**
@@ -21,6 +20,7 @@ import { inputAddressTheme } from './InputAddress.theme.js';
21
20
  * ```
22
21
  */
23
22
  const InputAddress = React__default.forwardRef(({ availableCountries = [], defaultCountry = "United States", onCountryChange, onPostalCodeChange, countryLabel = "Country", postalCodeLabel = "ZIP / Postal code", legendText = "Billing address", streetInputProps = {}, stateInputProps = {}, countryInputProps = {}, postalCodeInputProps = {}, className = "", theme = {}, }, ref) => {
23
+ var _a;
24
24
  // Merge default theme with provided theme using useMemo for performance
25
25
  const mergedTheme = useMemo(() => ({
26
26
  ...inputAddressTheme,
@@ -56,13 +56,12 @@ const InputAddress = React__default.forwardRef(({ availableCountries = [], defau
56
56
  return availableCountries;
57
57
  }, [availableCountries]);
58
58
  // Create unique IDs for accessibility
59
- useMemo(() => `address-fieldset-${Math.random().toString(36).substring(2, 9)}`, []);
60
- useMemo(() => `address-legend-${Math.random().toString(36).substring(2, 9)}`, []);
61
- const streetId = useMemo(() => `street-${Math.random().toString(36).substring(2, 9)}`, []);
62
- const stateId = useMemo(() => `state-${Math.random().toString(36).substring(2, 9)}`, []);
63
- const countryId = useMemo(() => `country-${Math.random().toString(36).substring(2, 9)}`, []);
64
- const postalCodeId = useMemo(() => `postal-code-${Math.random().toString(36).substring(2, 9)}`, []);
65
- return (jsxs("div", { ref: ref, className: `w-full ${className}`, "data-testid": "input-address-fieldset", children: [jsx(FormLabel, { htmlFor: streetId, className: mergedTheme.legendStyles, children: legendText }), jsxs("div", { className: `grid gap-0 ${mergedTheme.contentStyles}`, "data-testid": "input-address-content", children: [jsx("div", { className: "w-full relative focus-within:z-10", "data-testid": "input-address-street", children: jsx(Input, { id: streetId, type: "text", placeholder: "Street address", "aria-label": "street address", inputGroupClassName: "rounded-b-none", ...streetInputProps }) }), jsx("div", { className: "w-full -mt-[0.5px] relative focus-within:z-10", "data-testid": "input-address-state", children: jsx(Input, { id: stateId, type: "text", placeholder: "State", "aria-label": "state", inputGroupClassName: "rounded-none", ...stateInputProps }) }), jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 w-full -mt-px relative focus-within:z-10", children: [jsx("div", { className: `${mergedTheme.countryWrapperStyles} w-full sm:pr-0 relative focus-within:z-10`, "data-testid": "input-address-country", children: jsx(Input, { id: countryId, type: "text", value: country, onChange: handleCountryChange, placeholder: countryLabel, "aria-label": countryLabel, inputGroupClassName: "sm:rounded-bl-md rounded-t-none sm:rounded-tr-none sm:rounded-br-none", ...countryInputProps }) }), jsx("div", { className: `${mergedTheme.postalCodeWrapperStyles} w-full relative focus-within:z-10`, "data-testid": "input-address-postal", children: jsx(Input, { id: postalCodeId, type: "text", value: postalCode, onChange: handlePostalCodeChange, placeholder: postalCodeLabel, "aria-label": postalCodeLabel, inputGroupClassName: "sm:rounded-br-md rounded-t-none rounded-tl-none rounded-bl-md sm:rounded-bl-none", ...postalCodeInputProps }) })] })] })] }));
59
+ const baseId = useId();
60
+ const streetId = `${baseId}-street`;
61
+ const stateId = `${baseId}-state`;
62
+ const countryId = `${baseId}-country`;
63
+ const postalCodeId = `${baseId}-postal-code`;
64
+ return (jsxs("fieldset", { ref: ref, className: `w-full border-0 m-0 p-0 ${className}`, "data-testid": "input-address-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-address-content", children: [jsx("div", { className: "w-full relative focus-within:z-10", "data-testid": "input-address-street", children: jsx(Input, { id: streetId, type: "text", placeholder: "Street address", "aria-label": "street address", inputGroupClassName: "rounded-b-none", ...streetInputProps }) }), jsx("div", { className: "w-full -mt-[0.5px] relative focus-within:z-10", "data-testid": "input-address-state", children: jsx(Input, { id: stateId, type: "text", placeholder: "State", "aria-label": "state", inputGroupClassName: "rounded-none", ...stateInputProps }) }), jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 w-full -mt-px relative focus-within:z-10", children: [jsx("div", { className: `${mergedTheme.countryWrapperStyles} w-full sm:pr-0 relative focus-within:z-10`, "data-testid": "input-address-country", children: jsx(Input, { id: countryId, type: "text", value: country, onChange: handleCountryChange, placeholder: countryLabel, "aria-label": countryLabel, inputGroupClassName: "sm:rounded-bl-md rounded-t-none sm:rounded-tr-none sm:rounded-br-none", ...countryInputProps }) }), jsx("div", { className: `${mergedTheme.postalCodeWrapperStyles} w-full relative focus-within:z-10`, "data-testid": "input-address-postal", children: jsx(Input, { id: postalCodeId, type: "text", value: postalCode, onChange: handlePostalCodeChange, placeholder: postalCodeLabel, "aria-label": postalCodeLabel, inputGroupClassName: "sm:rounded-br-md rounded-t-none rounded-tl-none rounded-bl-md sm:rounded-bl-none", ...postalCodeInputProps }) })] })] })] }));
66
65
  });
67
66
  InputAddress.displayName = "InputAddress";
68
67
 
@@ -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 */