@flikk/ui 1.0.0-beta.22 → 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.
- package/dist/components/ai/PromptInput/PromptInput.js +139 -7
- package/dist/components/ai/PromptInput/VoiceRecorder.js +84 -0
- package/dist/components/charts/AreaChart/AreaChart.types.d.ts +2 -2
- package/dist/components/charts/BarChart/BarChart.types.d.ts +2 -2
- package/dist/components/charts/ComboChart/ComboChart.types.d.ts +2 -2
- package/dist/components/charts/LineChart/LineChart.types.d.ts +2 -2
- package/dist/components/charts/ScatterPlot/ScatterPlot.types.d.ts +2 -2
- package/dist/components/charts/StackedBarChart/StackedBarChart.types.d.ts +2 -2
- package/dist/components/core/Accordion/AccordionTrigger.js +28 -1
- package/dist/components/core/Alert/Alert.js +2 -1
- package/dist/components/core/AlertDialog/AlertDialog.js +1 -1
- package/dist/components/core/Badge/Badge.js +6 -17
- package/dist/components/core/Breadcrumbs/Breadcrumbs.js +17 -28
- package/dist/components/core/Calendar/Calendar.js +1 -1
- package/dist/components/core/Carousel/CarouselBody.js +6 -1
- package/dist/components/core/ContextMenu/ContextMenuBody.js +6 -3
- package/dist/components/core/ContextMenu/ContextMenuSubBody.js +6 -3
- package/dist/components/core/DragDrop/DragDrop.js +170 -17
- package/dist/components/core/DragDrop/DragDrop.types.d.ts +6 -0
- package/dist/components/core/Drawer/Drawer.d.ts +1 -1
- package/dist/components/core/Drawer/Drawer.js +2 -0
- package/dist/components/core/Drawer/Drawer.types.d.ts +2 -0
- package/dist/components/core/Drawer/DrawerTitle.d.ts +1 -1
- package/dist/components/core/Drawer/DrawerTitle.js +2 -2
- package/dist/components/core/Dropdown/DropdownMenu.js +2 -30
- package/dist/components/core/Masonry/Masonry.js +7 -2
- package/dist/components/core/Modal/Modal.d.ts +1 -1
- package/dist/components/core/Modal/Modal.js +3 -3
- package/dist/components/core/Modal/Modal.types.d.ts +2 -0
- package/dist/components/core/Modal/ModalTitle.d.ts +1 -1
- package/dist/components/core/Modal/ModalTitle.js +2 -2
- package/dist/components/core/NavItem/NavItem.js +21 -6
- package/dist/components/core/NavItem/NavItem.theme.js +2 -2
- package/dist/components/core/Pagination/Pagination.js +1 -1
- package/dist/components/core/Popover/PopoverBody.js +3 -27
- package/dist/components/core/Popover/PopoverTrigger.js +1 -1
- package/dist/components/core/Progress/Progress.js +4 -4
- package/dist/components/core/Rating/Rating.js +1 -1
- package/dist/components/core/ScrollArea/ScrollArea.js +3 -2
- package/dist/components/core/Segmented/Segmented.js +6 -6
- package/dist/components/core/Segmented/SegmentedItem.js +28 -2
- package/dist/components/core/Sidebar/Sidebar.d.ts +1 -0
- package/dist/components/core/Sidebar/Sidebar.js +5 -3
- package/dist/components/core/Sidebar/Sidebar.types.d.ts +7 -1
- package/dist/components/core/Sidebar/SidebarContent.js +1 -1
- package/dist/components/core/Sidebar/SidebarFooter.js +1 -1
- package/dist/components/core/Sidebar/SidebarHeader.js +5 -2
- package/dist/components/core/Sidebar/SidebarLogo.d.ts +3 -0
- package/dist/components/core/Sidebar/SidebarLogo.js +17 -0
- package/dist/components/core/Sidebar/SidebarNavGroup.js +2 -1
- package/dist/components/core/Sidebar/SidebarToggle.js +2 -4
- package/dist/components/core/Sidebar/SidebarUserProfile.js +3 -6
- package/dist/components/core/Sidebar/index.d.ts +2 -1
- package/dist/components/core/Skeleton/Skeleton.js +2 -2
- package/dist/components/core/Sortable/Sortable.d.ts +1 -1
- package/dist/components/core/Sortable/Sortable.js +20 -4
- package/dist/components/core/Sortable/Sortable.types.d.ts +2 -0
- package/dist/components/core/Tabs/Tabs.js +6 -4
- package/dist/components/core/Tabs/Tabs.types.d.ts +2 -0
- package/dist/components/core/Tabs/TabsBody.js +3 -3
- package/dist/components/core/Tabs/TabsList.js +19 -21
- package/dist/components/core/Tabs/TabsTrigger.js +47 -28
- package/dist/components/core/Toast/Toast.js +1 -1
- package/dist/components/core/Toast/ToastProvider.js +3 -2
- package/dist/components/core/Tooltip/Tooltip.d.ts +1 -1
- package/dist/components/core/Tooltip/Tooltip.js +26 -35
- package/dist/components/core/Tooltip/Tooltip.types.d.ts +5 -1
- package/dist/components/core/Tree/Tree.js +178 -8
- package/dist/components/core/index.js +1 -0
- package/dist/components/data-display/GanttChart/GanttSplitter.js +15 -1
- package/dist/components/data-display/Table/Table.js +2 -2
- package/dist/components/data-display/Table/Table.types.d.ts +2 -0
- package/dist/components/data-display/Table/TableFilter.js +21 -18
- package/dist/components/effects/CustomCursor/CustomCursor.js +3 -7
- package/dist/components/effects/Overlay/Overlay.js +4 -9
- package/dist/components/effects/PageTransition/PageTransition.js +17 -6
- package/dist/components/effects/SpotlightBorder/SpotlightBorder.js +3 -4
- package/dist/components/forms/Checkbox/Checkbox.js +11 -1
- package/dist/components/forms/Checkbox/Checkbox.theme.js +4 -1
- package/dist/components/forms/Checkbox/Checkbox.types.d.ts +7 -5
- package/dist/components/forms/Checkbox/CheckboxGroup.js +9 -4
- package/dist/components/forms/ColorPicker/ColorPicker.js +7 -4
- package/dist/components/forms/ColorPicker/ColorPicker.types.d.ts +2 -2
- package/dist/components/forms/ColorPicker/ColorPickerBody.js +5 -3
- package/dist/components/forms/ColorPicker/ColorPickerEyeDropper.js +3 -3
- package/dist/components/forms/ColorPicker/ColorPickerGradient.js +8 -8
- package/dist/components/forms/ColorPicker/ColorPickerInput.js +1 -1
- package/dist/components/forms/ColorPicker/index.d.ts +1 -1
- package/dist/components/forms/Combobox/Combobox.js +20 -45
- package/dist/components/forms/Combobox/Combobox.types.d.ts +4 -2
- package/dist/components/forms/CronInput/CronInput.js +10 -4
- package/dist/components/forms/CronInput/CronInput.types.d.ts +12 -1
- package/dist/components/forms/DatePicker/DatePicker.js +2 -2
- package/dist/components/forms/DatePicker/DatePicker.types.d.ts +4 -2
- package/dist/components/forms/DatePicker/DatePickerBody.js +1 -29
- package/dist/components/forms/DatePicker/DatePickerTrigger.js +2 -2
- package/dist/components/forms/DateRangePicker/DateRangePicker.js +3 -3
- package/dist/components/forms/DateRangePicker/DateRangePicker.types.d.ts +4 -2
- package/dist/components/forms/DateRangePicker/DateRangePickerBody.js +1 -28
- package/dist/components/forms/DateRangePicker/DateRangePickerTrigger.js +2 -2
- package/dist/components/forms/FileUpload/FileUpload.js +99 -10
- package/dist/components/forms/FileUpload/FileUpload.types.d.ts +15 -0
- package/dist/components/forms/FormLabel/FormLabel.js +2 -2
- package/dist/components/forms/Input/Input.js +5 -3
- package/dist/components/forms/Input/Input.types.d.ts +2 -0
- package/dist/components/forms/InputAddress/InputAddress.d.ts +1 -1
- package/dist/components/forms/InputAddress/InputAddress.js +8 -9
- package/dist/components/forms/InputCounter/InputCounter.js +25 -18
- package/dist/components/forms/InputCounter/InputCounter.types.d.ts +8 -1
- package/dist/components/forms/InputCreditCard/InputCreditCard.d.ts +1 -1
- package/dist/components/forms/InputCreditCard/InputCreditCard.js +27 -9
- package/dist/components/forms/InputCreditCard/InputCreditCard.types.d.ts +5 -0
- package/dist/components/forms/InputOTP/InputOTP.js +26 -17
- package/dist/components/forms/InputOTP/InputOTP.types.d.ts +21 -4
- package/dist/components/forms/InputTag/InputTag.js +12 -39
- package/dist/components/forms/InputTag/InputTag.types.d.ts +2 -2
- package/dist/components/forms/Mention/Mention.js +13 -6
- package/dist/components/forms/Mention/Mention.types.d.ts +9 -0
- package/dist/components/forms/Radio/Radio.js +12 -2
- package/dist/components/forms/Radio/Radio.theme.js +5 -1
- package/dist/components/forms/Radio/Radio.types.d.ts +7 -5
- package/dist/components/forms/Radio/RadioGroup.js +8 -3
- package/dist/components/forms/RichTextEditor/RichTextEditor.js +21 -21
- package/dist/components/forms/RichTextEditor/RichTextEditor.types.d.ts +2 -2
- package/dist/components/forms/Select/Select.d.ts +2 -0
- package/dist/components/forms/Select/Select.js +26 -56
- package/dist/components/forms/Select/Select.types.d.ts +14 -0
- package/dist/components/forms/Signature/Signature.js +1 -1
- package/dist/components/forms/Signature/Signature.types.d.ts +3 -3
- package/dist/components/forms/Slider/Slider.js +18 -4
- package/dist/components/forms/Slider/Slider.types.d.ts +2 -2
- package/dist/components/forms/Switch/Switch.js +12 -19
- package/dist/components/forms/Switch/Switch.theme.js +12 -2
- package/dist/components/forms/Switch/Switch.types.d.ts +20 -2
- package/dist/components/forms/Textarea/Textarea.js +8 -3
- package/dist/components/forms/Textarea/Textarea.types.d.ts +2 -0
- package/dist/components/forms/TimePicker/TimePicker.js +1 -1
- package/dist/components/forms/TimePicker/TimePicker.types.d.ts +6 -2
- package/dist/components/forms/TimePicker/TimePickerContent.js +88 -30
- package/dist/components/forms/TimePicker/TimePickerTrigger.js +2 -2
- package/dist/components/forms/index.js +86 -1
- package/dist/components/layout/FormLayout/FormLayout.js +87 -1
- package/dist/components/layout/index.js +4 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useClickOutside.d.ts +10 -4
- package/dist/hooks/useClickOutside.js +44 -24
- package/dist/hooks/useDarkMode.d.ts +1 -0
- package/dist/hooks/useDarkMode.js +42 -0
- package/dist/hooks/useFormStateMachine.d.ts +1 -1
- package/dist/hooks/useNetworkStatus.js +1 -1
- package/dist/hooks/useOverlay.js +4 -0
- package/dist/hooks/useScaleBackground.js +5 -2
- package/dist/hooks/useSelectPortal.js +14 -5
- package/dist/index.d.ts +0 -2
- package/dist/index.js +174 -223
- package/dist/shadcn-compat.css +80 -0
- package/dist/styles.css +1 -1
- package/dist/utils/stateMachine.d.ts +4 -4
- package/package.json +12 -10
- package/tailwind.preset.cjs +32 -3
- /package/{src/theme-plugin.css → dist/theme.css} +0 -0
|
@@ -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<
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
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 (
|
|
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,
|
|
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 (
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
159
|
+
isDisabled,
|
|
156
160
|
onChange,
|
|
157
161
|
handleIncrement,
|
|
158
162
|
handleDecrement,
|
|
159
163
|
]);
|
|
160
164
|
// Check if buttons should be disabled
|
|
161
|
-
const canDecrement = !
|
|
162
|
-
const canIncrement = !
|
|
163
|
-
|
|
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
|
|
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<
|
|
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, {
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
const
|
|
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] && !
|
|
41
|
+
if (autoFocus && inputRefs.current[0] && !isDisabled) {
|
|
37
42
|
inputRefs.current[0].focus();
|
|
38
43
|
}
|
|
39
|
-
}, [autoFocus,
|
|
44
|
+
}, [autoFocus, isDisabled]);
|
|
40
45
|
// Handle input change
|
|
41
46
|
const handleChange = useCallback((index, newValue) => {
|
|
42
47
|
var _a;
|
|
43
|
-
if (
|
|
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
|
-
}, [
|
|
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 (
|
|
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
|
-
}, [
|
|
107
|
+
}, [isDisabled, valueArray, length, handleChange]);
|
|
103
108
|
// Handle paste
|
|
104
109
|
const handlePaste = useCallback((event) => {
|
|
105
110
|
var _a;
|
|
106
|
-
if (
|
|
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
|
-
}, [
|
|
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 =
|
|
134
|
+
const stateClass = isInvalid
|
|
130
135
|
? "state-invalid"
|
|
131
|
-
:
|
|
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:
|
|
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))) })) }),
|
|
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
|
-
|
|
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?:
|
|
43
|
+
label?: React.ReactNode;
|
|
28
44
|
/**
|
|
29
45
|
* Helper text below the inputs
|
|
30
46
|
*/
|
|
31
|
-
helperText?:
|
|
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
|
/**
|