@spear-ai/spectral 1.14.1 → 1.14.3

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 (77) hide show
  1. package/dist/Button.d.ts +4 -0
  2. package/dist/Button.d.ts.map +1 -1
  3. package/dist/Button.js +4 -2
  4. package/dist/Button.js.map +1 -1
  5. package/dist/Checkbox.d.ts +4 -0
  6. package/dist/Checkbox.d.ts.map +1 -1
  7. package/dist/Checkbox.js +7 -3
  8. package/dist/Checkbox.js.map +1 -1
  9. package/dist/Combobox.d.ts +2 -0
  10. package/dist/Combobox.d.ts.map +1 -1
  11. package/dist/Combobox.js +7 -3
  12. package/dist/Combobox.js.map +1 -1
  13. package/dist/ControlGroup/ControlGroupSelect.d.ts +5 -1
  14. package/dist/ControlGroup/ControlGroupSelect.d.ts.map +1 -1
  15. package/dist/ControlGroup/ControlGroupSelect.js +3 -1
  16. package/dist/ControlGroup/ControlGroupSelect.js.map +1 -1
  17. package/dist/ControlGroup.d.ts +4 -0
  18. package/dist/ControlGroup.d.ts.map +1 -1
  19. package/dist/ControlGroup.js +7 -3
  20. package/dist/ControlGroup.js.map +1 -1
  21. package/dist/DateTimePicker.d.ts +4 -0
  22. package/dist/DateTimePicker.d.ts.map +1 -1
  23. package/dist/DateTimePicker.js +4 -2
  24. package/dist/DateTimePicker.js.map +1 -1
  25. package/dist/FormFieldMessage.d.ts +8 -2
  26. package/dist/FormFieldMessage.d.ts.map +1 -1
  27. package/dist/FormFieldMessage.js +13 -7
  28. package/dist/FormFieldMessage.js.map +1 -1
  29. package/dist/Input.js +7 -3
  30. package/dist/Input.js.map +1 -1
  31. package/dist/InputNumeric.d.ts +3 -1
  32. package/dist/InputNumeric.d.ts.map +1 -1
  33. package/dist/InputNumeric.js.map +1 -1
  34. package/dist/InputOTP.d.ts +4 -0
  35. package/dist/InputOTP.d.ts.map +1 -1
  36. package/dist/InputOTP.js +4 -2
  37. package/dist/InputOTP.js.map +1 -1
  38. package/dist/MultiSelect/MultiSelectBase.d.ts +4 -0
  39. package/dist/MultiSelect/MultiSelectBase.d.ts.map +1 -1
  40. package/dist/MultiSelect/MultiSelectBase.js +7 -3
  41. package/dist/MultiSelect/MultiSelectBase.js.map +1 -1
  42. package/dist/RadioButton.d.ts +2 -0
  43. package/dist/RadioButton.d.ts.map +1 -1
  44. package/dist/RadioButton.js +4 -3
  45. package/dist/RadioButton.js.map +1 -1
  46. package/dist/RadioButtonGroup/RadioButtonGroupBase.d.ts +3 -1
  47. package/dist/RadioButtonGroup/RadioButtonGroupBase.d.ts.map +1 -1
  48. package/dist/RadioButtonGroup/RadioButtonGroupBase.js +8 -5
  49. package/dist/RadioButtonGroup/RadioButtonGroupBase.js.map +1 -1
  50. package/dist/RadioGroup.d.ts +2 -0
  51. package/dist/RadioGroup.d.ts.map +1 -1
  52. package/dist/RadioGroup.js +7 -3
  53. package/dist/RadioGroup.js.map +1 -1
  54. package/dist/Select.js +7 -3
  55. package/dist/Select.js.map +1 -1
  56. package/dist/Switch.d.ts +4 -0
  57. package/dist/Switch.d.ts.map +1 -1
  58. package/dist/Switch.js +7 -3
  59. package/dist/Switch.js.map +1 -1
  60. package/dist/Textarea.d.ts +3 -1
  61. package/dist/Textarea.d.ts.map +1 -1
  62. package/dist/Textarea.js +7 -3
  63. package/dist/Textarea.js.map +1 -1
  64. package/dist/ToggleGroup/ToggleGroupBase.d.ts +6 -3
  65. package/dist/ToggleGroup/ToggleGroupBase.d.ts.map +1 -1
  66. package/dist/ToggleGroup/ToggleGroupBase.js +9 -1
  67. package/dist/ToggleGroup/ToggleGroupBase.js.map +1 -1
  68. package/dist/ToggleGroup/ToggleGroupItem.js +1 -1
  69. package/dist/ToggleGroup/ToggleGroupItem.js.map +1 -1
  70. package/dist/ToggleGroup.js +1 -1
  71. package/dist/ToggleGroup.js.map +1 -1
  72. package/dist/styles/horizon/colors.css +1 -0
  73. package/dist/styles/spectral.css +1 -1
  74. package/dist/utils/formFieldUtils.d.ts +2 -0
  75. package/dist/utils/formFieldUtils.d.ts.map +1 -1
  76. package/dist/utils/formFieldUtils.js.map +1 -1
  77. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"DateTimePicker.js","names":[],"sources":["../src/components/DateTimePicker/DateTimePicker.tsx"],"sourcesContent":["import { CalendarIcon } from '@components/Icons'\nimport { Popover, PopoverContent, PopoverTrigger } from '@components/Popover/Popover'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { ErrorMessage, getErrorMessageId, useFormFieldId } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useMemo, type ComponentProps } from 'react'\nimport { type Locale } from 'react-day-picker'\nimport { Calendar, type CalendarProps } from './Calendar'\nimport { DateTimeDisplayInput } from './DateTimeDisplayInput'\nimport { detectHourFormat, getResolvedLocale, type HourFormat, type Period, type TimePickerTranslations } from './DateTimeUtils'\nimport { TimePicker } from './TimePicker'\n\nexport interface DateTimePickerProps extends Omit<ComponentProps<'div'>, 'onChange' | 'defaultValue'> {\n calendarProps?: Omit<CalendarProps, 'mode' | 'selected' | 'onSelect' | 'disablePastDates' | 'locale'>\n defaultValue?: Date\n disabled?: boolean\n disablePastDates?: boolean\n errorMessage?: string | string[] | Record<string, unknown> | null\n hourFormat?: HourFormat\n label?: string\n locale?: Partial<Locale>\n onChange?: (date: Date | undefined) => void\n showTimePicker?: boolean\n state?: 'default' | 'disabled' | 'error'\n timeLocale?: string\n /** Override translation strings for time picker ARIA labels */\n timeTranslations?: Partial<TimePickerTranslations>\n value?: Date\n}\n\nexport const DateTimePicker = ({\n calendarProps,\n className,\n defaultValue,\n disabled = false,\n disablePastDates = true,\n errorMessage,\n hourFormat,\n label,\n locale,\n onChange,\n showTimePicker = true,\n state = 'default',\n timeLocale,\n timeTranslations,\n value,\n id,\n ...props\n}: DateTimePickerProps) => {\n const fieldId = useFormFieldId(id)\n const errorMessageId = getErrorMessageId(fieldId)\n const describedBy = state === 'error' && errorMessage ? errorMessageId : undefined\n const [date, setDate] = useUncontrolledState<Date | undefined>({\n defaultValue,\n onChange,\n value,\n })\n\n // Resolve time locale with fallback chain\n const resolvedTimeLocale = useMemo(() => getResolvedLocale(timeLocale), [timeLocale])\n\n // Auto-detect hour format from locale if not explicitly provided\n const effectiveHourFormat = useMemo(() => hourFormat ?? detectHourFormat(resolvedTimeLocale), [hourFormat, resolvedTimeLocale])\n\n const handleDateSelect = (selectedDate: Date | undefined) => {\n if (!selectedDate) {\n setDate(undefined)\n return\n }\n\n // Preserve the time from the existing date\n if (date) {\n selectedDate.setHours(date.getHours(), date.getMinutes(), 0, 0)\n }\n\n setDate(selectedDate)\n }\n\n const handleTimeChange = useCallback(\n (newDate: Date | undefined) => {\n setDate(newDate)\n },\n [setDate],\n )\n\n const handlePeriodChange = (period: Period) => {\n // Period change is already handled in TimePicker\n // This callback is for external consumers who want to react to period changes\n void period\n }\n\n const handleInputDateChange = (newDate: Date | undefined) => {\n setDate(newDate)\n }\n\n return (\n <Popover>\n <div className={cn('w-full', className)} data-slot='datetime-picker' data-testid='spectral-datetime-picker' {...props}>\n <DateTimeDisplayInput\n aria-describedby={describedBy}\n aria-invalid={state === 'error'}\n className={cn('gap-4 pr-12 flex w-full justify-start', !date && 'text-text-secondary')}\n data-testid='spectral-datetime-picker-input'\n disabled={disabled}\n endIcon={\n <PopoverTrigger asChild disabled={disabled}>\n <CalendarIcon\n aria-label='Open date picker'\n className={cn('right-4 text-input-icon hover:text-input-icon--hover absolute top-1/2 -translate-y-1/2 cursor-pointer focus:outline-none', disabled ? 'pointer-events-none cursor-not-allowed opacity-50' : 'hover:text-input-icon--hover cursor-pointer')}\n data-testid='spectral-datetime-picker-trigger'\n disabled={disabled}\n />\n </PopoverTrigger>\n }\n hourFormat={effectiveHourFormat}\n id={fieldId}\n label={label}\n onChange={handleInputDateChange}\n showTime={showTimePicker}\n state={state}\n value={date}\n />\n <ErrorMessage dataTestId='spectral-datetime-picker-error-message' id={errorMessageId} message={state === 'error' ? errorMessage : null} />\n </div>\n\n <PopoverContent\n align='start'\n className={cn('rounded-lg py-4 px-6 flex', !showTimePicker && 'w-[330px]', showTimePicker && effectiveHourFormat === '24' && 'w-[486px]', showTimePicker && effectiveHourFormat === '12' && 'w-[560px]')}\n data-testid='spectral-datetime-picker-popover'\n onOpenAutoFocus={(e) => e.preventDefault()}\n >\n <Calendar {...calendarProps} disablePastDates={disablePastDates} locale={locale} mode='single' onSelect={handleDateSelect} selected={date} />\n {showTimePicker && (\n <div className='pl-6 border-l border-border-secondary'>\n <TimePicker date={date} hourFormat={effectiveHourFormat} locale={resolvedTimeLocale} onChange={handleTimeChange} onPeriodChange={handlePeriodChange} translations={timeTranslations} />\n </div>\n )}\n </PopoverContent>\n </Popover>\n )\n}\n\nDateTimePicker.displayName = 'DateTimePicker'\n\n// Re-export sub-components for granular usage\nexport { Calendar } from './Calendar'\nexport { DateTimeDisplayInput } from './DateTimeDisplayInput'\nexport { DateTimeInput } from './DateTimeInput'\nexport { TimePeriodSelect } from './TimePeriodSelect'\nexport { TimePicker } from './TimePicker'\nexport * from './DateTimeUtils'\nexport { type Locale } from 'react-day-picker'\n"],"mappings":";;;;;;;;;;;;;;;;;;AA8BA,MAAa,kBAAkB,EAC7B,eACA,WACA,cACA,WAAW,OACX,mBAAmB,MACnB,cACA,YACA,OACA,QACA,UACA,iBAAiB,MACjB,QAAQ,WACR,YACA,kBACA,OACA,IACA,GAAG,YACsB;CACzB,MAAM,UAAU,eAAe,GAAG;CAClC,MAAM,iBAAiB,kBAAkB,QAAQ;CACjD,MAAM,cAAc,UAAU,WAAW,eAAe,iBAAiB;CACzE,MAAM,CAAC,MAAM,WAAW,qBAAuC;EAC7D;EACA;EACA;EACD,CAAC;CAGF,MAAM,qBAAqB,cAAc,kBAAkB,WAAW,EAAE,CAAC,WAAW,CAAC;CAGrF,MAAM,sBAAsB,cAAc,cAAc,iBAAiB,mBAAmB,EAAE,CAAC,YAAY,mBAAmB,CAAC;CAE/H,MAAM,oBAAoB,iBAAmC;AAC3D,MAAI,CAAC,cAAc;AACjB,WAAQ,OAAU;AAClB;;AAIF,MAAI,KACF,cAAa,SAAS,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,GAAG,EAAE;AAGjE,UAAQ,aAAa;;CAGvB,MAAM,mBAAmB,aACtB,YAA8B;AAC7B,UAAQ,QAAQ;IAElB,CAAC,QAAQ,CACV;CAED,MAAM,sBAAsB,WAAmB;CAM/C,MAAM,yBAAyB,YAA8B;AAC3D,UAAQ,QAAQ;;AAGlB,QACE,qBAAC,SAAD,aACE,qBAAC,OAAD;EAAK,WAAW,GAAG,UAAU,UAAU;EAAE,aAAU;EAAkB,eAAY;EAA2B,GAAI;YAAhH,CACE,oBAAC,sBAAD;GACE,oBAAkB;GAClB,gBAAc,UAAU;GACxB,WAAW,GAAG,yCAAyC,CAAC,QAAQ,sBAAsB;GACtF,eAAY;GACF;GACV,SACE,oBAAC,gBAAD;IAAgB;IAAkB;cAChC,oBAAC,cAAD;KACE,cAAW;KACX,WAAW,GAAG,4HAA4H,WAAW,sDAAsD,8CAA8C;KACzP,eAAY;KACF;KACV;IACa;GAEnB,YAAY;GACZ,IAAI;GACG;GACP,UAAU;GACV,UAAU;GACH;GACP,OAAO;GACP,GACF,oBAAC,cAAD;GAAc,YAAW;GAAyC,IAAI;GAAgB,SAAS,UAAU,UAAU,eAAe;GAAQ,EACtI;KAEN,qBAAC,gBAAD;EACE,OAAM;EACN,WAAW,GAAG,6BAA6B,CAAC,kBAAkB,aAAa,kBAAkB,wBAAwB,QAAQ,aAAa,kBAAkB,wBAAwB,QAAQ,YAAY;EACxM,eAAY;EACZ,kBAAkB,MAAM,EAAE,gBAAgB;YAJ5C,CAME,oBAAC,UAAD;GAAU,GAAI;GAAiC;GAA0B;GAAQ,MAAK;GAAS,UAAU;GAAkB,UAAU;GAAQ,GAC5I,kBACC,oBAAC,OAAD;GAAK,WAAU;aACb,oBAAC,YAAD;IAAkB;IAAM,YAAY;IAAqB,QAAQ;IAAoB,UAAU;IAAkB,gBAAgB;IAAoB,cAAc;IAAoB;GACnL,EAEO;IACT;;AAId,eAAe,cAAc"}
1
+ {"version":3,"file":"DateTimePicker.js","names":[],"sources":["../src/components/DateTimePicker/DateTimePicker.tsx"],"sourcesContent":["import { CalendarIcon } from '@components/Icons'\nimport { Popover, PopoverContent, PopoverTrigger } from '@components/Popover/Popover'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { ErrorMessage, getErrorMessageId, useFormFieldId } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useMemo, type ComponentProps } from 'react'\nimport { type Locale } from 'react-day-picker'\nimport { Calendar, type CalendarProps } from './Calendar'\nimport { DateTimeDisplayInput } from './DateTimeDisplayInput'\nimport { detectHourFormat, getResolvedLocale, type HourFormat, type Period, type TimePickerTranslations } from './DateTimeUtils'\nimport { TimePicker } from './TimePicker'\n\nexport interface DateTimePickerProps extends Omit<ComponentProps<'div'>, 'onChange' | 'defaultValue'> {\n calendarProps?: Omit<CalendarProps, 'mode' | 'selected' | 'onSelect' | 'disablePastDates' | 'locale'>\n defaultValue?: Date\n disabled?: boolean\n disablePastDates?: boolean\n errorMessage?: string | string[] | Record<string, unknown> | null\n hourFormat?: HourFormat\n label?: string\n locale?: Partial<Locale>\n messageReserveLines?: number\n messageReserveSpace?: boolean\n onChange?: (date: Date | undefined) => void\n showTimePicker?: boolean\n state?: 'default' | 'disabled' | 'error'\n timeLocale?: string\n /** Override translation strings for time picker ARIA labels */\n timeTranslations?: Partial<TimePickerTranslations>\n value?: Date\n}\n\nexport const DateTimePicker = ({\n calendarProps,\n className,\n defaultValue,\n disabled = false,\n disablePastDates = true,\n errorMessage,\n hourFormat,\n label,\n locale,\n messageReserveLines = 1,\n messageReserveSpace = true,\n onChange,\n showTimePicker = true,\n state = 'default',\n timeLocale,\n timeTranslations,\n value,\n id,\n ...props\n}: DateTimePickerProps) => {\n const fieldId = useFormFieldId(id)\n const errorMessageId = getErrorMessageId(fieldId)\n const describedBy = state === 'error' && errorMessage ? errorMessageId : undefined\n const [date, setDate] = useUncontrolledState<Date | undefined>({\n defaultValue,\n onChange,\n value,\n })\n\n // Resolve time locale with fallback chain\n const resolvedTimeLocale = useMemo(() => getResolvedLocale(timeLocale), [timeLocale])\n\n // Auto-detect hour format from locale if not explicitly provided\n const effectiveHourFormat = useMemo(() => hourFormat ?? detectHourFormat(resolvedTimeLocale), [hourFormat, resolvedTimeLocale])\n\n const handleDateSelect = (selectedDate: Date | undefined) => {\n if (!selectedDate) {\n setDate(undefined)\n return\n }\n\n // Preserve the time from the existing date\n if (date) {\n selectedDate.setHours(date.getHours(), date.getMinutes(), 0, 0)\n }\n\n setDate(selectedDate)\n }\n\n const handleTimeChange = useCallback(\n (newDate: Date | undefined) => {\n setDate(newDate)\n },\n [setDate],\n )\n\n const handlePeriodChange = (period: Period) => {\n // Period change is already handled in TimePicker\n // This callback is for external consumers who want to react to period changes\n void period\n }\n\n const handleInputDateChange = (newDate: Date | undefined) => {\n setDate(newDate)\n }\n\n return (\n <Popover>\n <div className={cn('w-full', className)} data-slot='datetime-picker' data-testid='spectral-datetime-picker' {...props}>\n <DateTimeDisplayInput\n aria-describedby={describedBy}\n aria-invalid={state === 'error'}\n className={cn('gap-4 pr-12 flex w-full justify-start', !date && 'text-text-secondary')}\n data-testid='spectral-datetime-picker-input'\n disabled={disabled}\n endIcon={\n <PopoverTrigger asChild disabled={disabled}>\n <CalendarIcon\n aria-label='Open date picker'\n className={cn('right-4 text-input-icon hover:text-input-icon--hover absolute top-1/2 -translate-y-1/2 cursor-pointer focus:outline-none', disabled ? 'pointer-events-none cursor-not-allowed opacity-50' : 'hover:text-input-icon--hover cursor-pointer')}\n data-testid='spectral-datetime-picker-trigger'\n disabled={disabled}\n />\n </PopoverTrigger>\n }\n hourFormat={effectiveHourFormat}\n id={fieldId}\n label={label}\n onChange={handleInputDateChange}\n showTime={showTimePicker}\n state={state}\n value={date}\n />\n <ErrorMessage dataTestId='spectral-datetime-picker-error-message' id={errorMessageId} message={state === 'error' ? errorMessage : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} />\n </div>\n\n <PopoverContent\n align='start'\n className={cn('rounded-lg py-4 px-6 flex', !showTimePicker && 'w-[330px]', showTimePicker && effectiveHourFormat === '24' && 'w-[486px]', showTimePicker && effectiveHourFormat === '12' && 'w-[560px]')}\n data-testid='spectral-datetime-picker-popover'\n onOpenAutoFocus={(e) => e.preventDefault()}\n >\n <Calendar {...calendarProps} disablePastDates={disablePastDates} locale={locale} mode='single' onSelect={handleDateSelect} selected={date} />\n {showTimePicker && (\n <div className='pl-6 border-l border-border-secondary'>\n <TimePicker date={date} hourFormat={effectiveHourFormat} locale={resolvedTimeLocale} onChange={handleTimeChange} onPeriodChange={handlePeriodChange} translations={timeTranslations} />\n </div>\n )}\n </PopoverContent>\n </Popover>\n )\n}\n\nDateTimePicker.displayName = 'DateTimePicker'\n\n// Re-export sub-components for granular usage\nexport { Calendar } from './Calendar'\nexport { DateTimeDisplayInput } from './DateTimeDisplayInput'\nexport { DateTimeInput } from './DateTimeInput'\nexport { TimePeriodSelect } from './TimePeriodSelect'\nexport { TimePicker } from './TimePicker'\nexport * from './DateTimeUtils'\nexport { type Locale } from 'react-day-picker'\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgCA,MAAa,kBAAkB,EAC7B,eACA,WACA,cACA,WAAW,OACX,mBAAmB,MACnB,cACA,YACA,OACA,QACA,sBAAsB,GACtB,sBAAsB,MACtB,UACA,iBAAiB,MACjB,QAAQ,WACR,YACA,kBACA,OACA,IACA,GAAG,YACsB;CACzB,MAAM,UAAU,eAAe,GAAG;CAClC,MAAM,iBAAiB,kBAAkB,QAAQ;CACjD,MAAM,cAAc,UAAU,WAAW,eAAe,iBAAiB;CACzE,MAAM,CAAC,MAAM,WAAW,qBAAuC;EAC7D;EACA;EACA;EACD,CAAC;CAGF,MAAM,qBAAqB,cAAc,kBAAkB,WAAW,EAAE,CAAC,WAAW,CAAC;CAGrF,MAAM,sBAAsB,cAAc,cAAc,iBAAiB,mBAAmB,EAAE,CAAC,YAAY,mBAAmB,CAAC;CAE/H,MAAM,oBAAoB,iBAAmC;AAC3D,MAAI,CAAC,cAAc;AACjB,WAAQ,OAAU;AAClB;;AAIF,MAAI,KACF,cAAa,SAAS,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,GAAG,EAAE;AAGjE,UAAQ,aAAa;;CAGvB,MAAM,mBAAmB,aACtB,YAA8B;AAC7B,UAAQ,QAAQ;IAElB,CAAC,QAAQ,CACV;CAED,MAAM,sBAAsB,WAAmB;CAM/C,MAAM,yBAAyB,YAA8B;AAC3D,UAAQ,QAAQ;;AAGlB,QACE,qBAAC,SAAD,aACE,qBAAC,OAAD;EAAK,WAAW,GAAG,UAAU,UAAU;EAAE,aAAU;EAAkB,eAAY;EAA2B,GAAI;YAAhH,CACE,oBAAC,sBAAD;GACE,oBAAkB;GAClB,gBAAc,UAAU;GACxB,WAAW,GAAG,yCAAyC,CAAC,QAAQ,sBAAsB;GACtF,eAAY;GACF;GACV,SACE,oBAAC,gBAAD;IAAgB;IAAkB;cAChC,oBAAC,cAAD;KACE,cAAW;KACX,WAAW,GAAG,4HAA4H,WAAW,sDAAsD,8CAA8C;KACzP,eAAY;KACF;KACV;IACa;GAEnB,YAAY;GACZ,IAAI;GACG;GACP,UAAU;GACV,UAAU;GACH;GACP,OAAO;GACP,GACF,oBAAC,cAAD;GAAc,YAAW;GAAyC,IAAI;GAAgB,SAAS,UAAU,UAAU,eAAe;GAA2B;GAA0C;GAAuB,EAC1N;KAEN,qBAAC,gBAAD;EACE,OAAM;EACN,WAAW,GAAG,6BAA6B,CAAC,kBAAkB,aAAa,kBAAkB,wBAAwB,QAAQ,aAAa,kBAAkB,wBAAwB,QAAQ,YAAY;EACxM,eAAY;EACZ,kBAAkB,MAAM,EAAE,gBAAgB;YAJ5C,CAME,oBAAC,UAAD;GAAU,GAAI;GAAiC;GAA0B;GAAQ,MAAK;GAAS,UAAU;GAAkB,UAAU;GAAQ,GAC5I,kBACC,oBAAC,OAAD;GAAK,WAAU;aACb,oBAAC,YAAD;IAAkB;IAAM,YAAY;IAAqB,QAAQ;IAAoB,UAAU;IAAkB,gBAAgB;IAAoB,cAAc;IAAoB;GACnL,EAEO;IACT;;AAId,eAAe,cAAc"}
@@ -9,20 +9,26 @@ interface BaseFieldMessageProps {
9
9
  dataTestId?: string;
10
10
  id: string;
11
11
  message: FormFieldMessageValue;
12
+ messageReserveLines?: number;
13
+ messageReserveSpace?: boolean;
12
14
  }
13
15
  declare const ErrorMessage: ({
14
16
  className,
15
17
  containerClassName,
16
18
  dataTestId,
17
19
  id,
18
- message
20
+ message,
21
+ messageReserveLines,
22
+ messageReserveSpace
19
23
  }: BaseFieldMessageProps) => _$react_jsx_runtime0.JSX.Element;
20
24
  declare const WarningMessage: ({
21
25
  className,
22
26
  containerClassName,
23
27
  dataTestId,
24
28
  id,
25
- message
29
+ message,
30
+ messageReserveLines,
31
+ messageReserveSpace
26
32
  }: BaseFieldMessageProps) => _$react_jsx_runtime0.JSX.Element;
27
33
  //#endregion
28
34
  export { ErrorMessage, FormFieldMessageValue, WarningMessage };
@@ -1 +1 @@
1
- {"version":3,"file":"FormFieldMessage.d.ts","names":[],"sources":["../src/components/FormFieldMessage/FormFieldMessage.tsx"],"mappings":";;;;KAGY,qBAAA,uBAA4C,MAAA;AAAA,UAE9C,qBAAA;EACR,SAAA;EACA,kBAAA;EACA,UAAA;EACA,EAAA;EACA,OAAA,EAAS,qBAAA;AAAA;AAAA,cAuDE,YAAA;EAAY,SAAA;EAAA,kBAAA;EAAA,UAAA;EAAA,EAAA;EAAA;AAAA,GAAsG,qBAAA,KAAqB,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,cAIvI,cAAA;EAAc,SAAA;EAAA,kBAAA;EAAA,UAAA;EAAA,EAAA;EAAA;AAAA,GAAwG,qBAAA,KAAqB,oBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"FormFieldMessage.d.ts","names":[],"sources":["../src/components/FormFieldMessage/FormFieldMessage.tsx"],"mappings":";;;;KAGY,qBAAA,uBAA4C,MAAA;AAAA,UAE9C,qBAAA;EACR,SAAA;EACA,kBAAA;EACA,UAAA;EACA,EAAA;EACA,OAAA,EAAS,qBAAA;EACT,mBAAA;EACA,mBAAA;AAAA;AAAA,cAoEW,YAAA;EAAY,SAAA;EAAA,kBAAA;EAAA,UAAA;EAAA,EAAA;EAAA,OAAA;EAAA,mBAAA;EAAA;AAAA,GAAgJ,qBAAA,KAAqB,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,cAIjL,cAAA;EAAc,SAAA;EAAA,kBAAA;EAAA,UAAA;EAAA,EAAA;EAAA,OAAA;EAAA,mBAAA;EAAA;AAAA,GAAkJ,qBAAA,KAAqB,oBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -20,17 +20,19 @@ const renderFieldMessageContent = (message) => {
20
20
  }
21
21
  return String(message);
22
22
  };
23
- const FormFieldMessage = ({ ariaLive, className, containerClassName, dataTestId, id, message, role, tone }) => {
23
+ const FormFieldMessage = ({ ariaLive, className, containerClassName, dataTestId, id, message, messageReserveLines = 1, messageReserveSpace = true, role, tone }) => {
24
24
  const content = renderFieldMessageContent(message);
25
25
  const isVisible = Boolean(content);
26
+ const reservedHeight = messageReserveSpace && messageReserveLines > 0 ? `${Math.max(messageReserveLines, 1) * 1.25}rem` : void 0;
26
27
  const toneClasses = tone === "danger" ? "text-danger-400!" : "text-warning-400!";
27
28
  return /* @__PURE__ */ jsx("div", {
28
29
  "aria-hidden": !isVisible,
29
- className: cn("transition-[opacity,transform,padding] duration-300 ease-out motion-reduce:transition-none", isVisible ? "translate-y-0 py-2 opacity-100" : "-translate-y-1 py-0 opacity-0", containerClassName),
30
+ className: cn("transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-none", messageReserveSpace ? "pt-1" : "pt-0", isVisible ? "translate-y-0 opacity-100" : "-translate-y-0.5 opacity-0", !messageReserveSpace && !isVisible && "hidden", containerClassName),
31
+ style: reservedHeight ? { minHeight: reservedHeight } : void 0,
30
32
  children: /* @__PURE__ */ jsx("p", {
31
33
  "aria-atomic": isVisible ? "true" : void 0,
32
34
  "aria-live": isVisible ? ariaLive : void 0,
33
- className: cn("m-0! min-h-0 text-sm overflow-hidden", toneClasses, className),
35
+ className: cn("m-0! text-sm leading-5 overflow-hidden", toneClasses, className),
34
36
  "data-testid": isVisible ? dataTestId : void 0,
35
37
  id,
36
38
  role: isVisible ? role : void 0,
@@ -38,19 +40,21 @@ const FormFieldMessage = ({ ariaLive, className, containerClassName, dataTestId,
38
40
  })
39
41
  });
40
42
  };
41
- const ErrorMessage = ({ className, containerClassName, dataTestId = "spectral-form-field-error-message", id, message }) => {
43
+ const ErrorMessage = ({ className, containerClassName, dataTestId = "spectral-form-field-error-message", id, message, messageReserveLines, messageReserveSpace }) => {
42
44
  return /* @__PURE__ */ jsx(FormFieldMessage, {
43
- ariaLive: "assertive",
45
+ ariaLive: "polite",
44
46
  className,
45
47
  containerClassName,
46
48
  dataTestId,
47
49
  id,
48
50
  message,
49
- role: "alert",
51
+ messageReserveLines,
52
+ messageReserveSpace,
53
+ role: "status",
50
54
  tone: "danger"
51
55
  });
52
56
  };
53
- const WarningMessage = ({ className, containerClassName, dataTestId = "spectral-form-field-warning-message", id, message }) => {
57
+ const WarningMessage = ({ className, containerClassName, dataTestId = "spectral-form-field-warning-message", id, message, messageReserveLines, messageReserveSpace }) => {
54
58
  return /* @__PURE__ */ jsx(FormFieldMessage, {
55
59
  ariaLive: "polite",
56
60
  className,
@@ -58,6 +62,8 @@ const WarningMessage = ({ className, containerClassName, dataTestId = "spectral-
58
62
  dataTestId,
59
63
  id,
60
64
  message,
65
+ messageReserveLines,
66
+ messageReserveSpace,
61
67
  role: "status",
62
68
  tone: "warning"
63
69
  });
@@ -1 +1 @@
1
- {"version":3,"file":"FormFieldMessage.js","names":[],"sources":["../src/components/FormFieldMessage/FormFieldMessage.tsx"],"sourcesContent":["import { cn } from '@utils/twUtils'\nimport { type ReactNode } from 'react'\n\nexport type FormFieldMessageValue = string | string[] | Record<string, unknown> | null | undefined\n\ninterface BaseFieldMessageProps {\n className?: string\n containerClassName?: string\n dataTestId?: string\n id: string\n message: FormFieldMessageValue\n}\n\nconst renderFieldMessageContent = (message: FormFieldMessageValue): ReactNode => {\n if (!message) return null\n\n if (typeof message === 'string') {\n return message\n }\n\n if (Array.isArray(message)) {\n return message.join(', ')\n }\n\n if (typeof message === 'object') {\n if ('message' in message && typeof message.message === 'string') return message.message\n if ('error' in message && typeof message.error === 'string') return message.error\n if ('details' in message && typeof message.details === 'string') return message.details\n try {\n return JSON.stringify(message)\n } catch {\n return '[Circular]'\n }\n }\n\n return String(message)\n}\n\nconst FormFieldMessage = ({\n ariaLive,\n className,\n containerClassName,\n dataTestId,\n id,\n message,\n role,\n tone,\n}: BaseFieldMessageProps & {\n ariaLive: 'assertive' | 'polite'\n role: 'alert' | 'status'\n tone: 'danger' | 'warning'\n}) => {\n const content = renderFieldMessageContent(message)\n const isVisible = Boolean(content)\n const toneClasses = tone === 'danger' ? 'text-danger-400!' : 'text-warning-400!'\n\n return (\n <div aria-hidden={!isVisible} className={cn('transition-[opacity,transform,padding] duration-300 ease-out motion-reduce:transition-none', isVisible ? 'translate-y-0 py-2 opacity-100' : '-translate-y-1 py-0 opacity-0', containerClassName)}>\n <p aria-atomic={isVisible ? 'true' : undefined} aria-live={isVisible ? ariaLive : undefined} className={cn('m-0! min-h-0 text-sm overflow-hidden', toneClasses, className)} data-testid={isVisible ? dataTestId : undefined} id={id} role={isVisible ? role : undefined}>\n {content}\n </p>\n </div>\n )\n}\n\nexport const ErrorMessage = ({ className, containerClassName, dataTestId = 'spectral-form-field-error-message', id, message }: BaseFieldMessageProps) => {\n return <FormFieldMessage ariaLive='assertive' className={className} containerClassName={containerClassName} dataTestId={dataTestId} id={id} message={message} role='alert' tone='danger' />\n}\n\nexport const WarningMessage = ({ className, containerClassName, dataTestId = 'spectral-form-field-warning-message', id, message }: BaseFieldMessageProps) => {\n return <FormFieldMessage ariaLive='polite' className={className} containerClassName={containerClassName} dataTestId={dataTestId} id={id} message={message} role='status' tone='warning' />\n}\n"],"mappings":";;;;;;AAaA,MAAM,6BAA6B,YAA8C;AAC/E,KAAI,CAAC,QAAS,QAAO;AAErB,KAAI,OAAO,YAAY,SACrB,QAAO;AAGT,KAAI,MAAM,QAAQ,QAAQ,CACxB,QAAO,QAAQ,KAAK,KAAK;AAG3B,KAAI,OAAO,YAAY,UAAU;AAC/B,MAAI,aAAa,WAAW,OAAO,QAAQ,YAAY,SAAU,QAAO,QAAQ;AAChF,MAAI,WAAW,WAAW,OAAO,QAAQ,UAAU,SAAU,QAAO,QAAQ;AAC5E,MAAI,aAAa,WAAW,OAAO,QAAQ,YAAY,SAAU,QAAO,QAAQ;AAChF,MAAI;AACF,UAAO,KAAK,UAAU,QAAQ;UACxB;AACN,UAAO;;;AAIX,QAAO,OAAO,QAAQ;;AAGxB,MAAM,oBAAoB,EACxB,UACA,WACA,oBACA,YACA,IACA,SACA,MACA,WAKI;CACJ,MAAM,UAAU,0BAA0B,QAAQ;CAClD,MAAM,YAAY,QAAQ,QAAQ;CAClC,MAAM,cAAc,SAAS,WAAW,qBAAqB;AAE7D,QACE,oBAAC,OAAD;EAAK,eAAa,CAAC;EAAW,WAAW,GAAG,8FAA8F,YAAY,mCAAmC,iCAAiC,mBAAmB;YAC3O,oBAAC,KAAD;GAAG,eAAa,YAAY,SAAS;GAAW,aAAW,YAAY,WAAW;GAAW,WAAW,GAAG,wCAAwC,aAAa,UAAU;GAAE,eAAa,YAAY,aAAa;GAAe;GAAI,MAAM,YAAY,OAAO;aAC3P;GACC;EACA;;AAIV,MAAa,gBAAgB,EAAE,WAAW,oBAAoB,aAAa,qCAAqC,IAAI,cAAqC;AACvJ,QAAO,oBAAC,kBAAD;EAAkB,UAAS;EAAuB;EAA+B;EAAgC;EAAgB;EAAa;EAAS,MAAK;EAAQ,MAAK;EAAW;;AAG7L,MAAa,kBAAkB,EAAE,WAAW,oBAAoB,aAAa,uCAAuC,IAAI,cAAqC;AAC3J,QAAO,oBAAC,kBAAD;EAAkB,UAAS;EAAoB;EAA+B;EAAgC;EAAgB;EAAa;EAAS,MAAK;EAAS,MAAK;EAAY"}
1
+ {"version":3,"file":"FormFieldMessage.js","names":[],"sources":["../src/components/FormFieldMessage/FormFieldMessage.tsx"],"sourcesContent":["import { cn } from '@utils/twUtils'\nimport { type ReactNode } from 'react'\n\nexport type FormFieldMessageValue = string | string[] | Record<string, unknown> | null | undefined\n\ninterface BaseFieldMessageProps {\n className?: string\n containerClassName?: string\n dataTestId?: string\n id: string\n message: FormFieldMessageValue\n messageReserveLines?: number\n messageReserveSpace?: boolean\n}\n\nconst renderFieldMessageContent = (message: FormFieldMessageValue): ReactNode => {\n if (!message) return null\n\n if (typeof message === 'string') {\n return message\n }\n\n if (Array.isArray(message)) {\n return message.join(', ')\n }\n\n if (typeof message === 'object') {\n if ('message' in message && typeof message.message === 'string') return message.message\n if ('error' in message && typeof message.error === 'string') return message.error\n if ('details' in message && typeof message.details === 'string') return message.details\n try {\n return JSON.stringify(message)\n } catch {\n return '[Circular]'\n }\n }\n\n return String(message)\n}\n\nconst FormFieldMessage = ({\n ariaLive,\n className,\n containerClassName,\n dataTestId,\n id,\n message,\n messageReserveLines = 1,\n messageReserveSpace = true,\n role,\n tone,\n}: BaseFieldMessageProps & {\n ariaLive: 'assertive' | 'polite'\n role: 'alert' | 'status'\n tone: 'danger' | 'warning'\n}) => {\n const content = renderFieldMessageContent(message)\n const isVisible = Boolean(content)\n const reservedHeight = messageReserveSpace && messageReserveLines > 0 ? `${Math.max(messageReserveLines, 1) * 1.25}rem` : undefined\n const toneClasses = tone === 'danger' ? 'text-danger-400!' : 'text-warning-400!'\n\n return (\n <div\n aria-hidden={!isVisible}\n className={cn(\n 'transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-none',\n messageReserveSpace ? 'pt-1' : 'pt-0',\n isVisible ? 'translate-y-0 opacity-100' : '-translate-y-0.5 opacity-0',\n !messageReserveSpace && !isVisible && 'hidden',\n containerClassName,\n )}\n style={reservedHeight ? { minHeight: reservedHeight } : undefined}\n >\n <p aria-atomic={isVisible ? 'true' : undefined} aria-live={isVisible ? ariaLive : undefined} className={cn('m-0! text-sm leading-5 overflow-hidden', toneClasses, className)} data-testid={isVisible ? dataTestId : undefined} id={id} role={isVisible ? role : undefined}>\n {content}\n </p>\n </div>\n )\n}\n\nexport const ErrorMessage = ({ className, containerClassName, dataTestId = 'spectral-form-field-error-message', id, message, messageReserveLines, messageReserveSpace }: BaseFieldMessageProps) => {\n return <FormFieldMessage ariaLive='polite' className={className} containerClassName={containerClassName} dataTestId={dataTestId} id={id} message={message} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} role='status' tone='danger' />\n}\n\nexport const WarningMessage = ({ className, containerClassName, dataTestId = 'spectral-form-field-warning-message', id, message, messageReserveLines, messageReserveSpace }: BaseFieldMessageProps) => {\n return <FormFieldMessage ariaLive='polite' className={className} containerClassName={containerClassName} dataTestId={dataTestId} id={id} message={message} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} role='status' tone='warning' />\n}\n"],"mappings":";;;;;;AAeA,MAAM,6BAA6B,YAA8C;AAC/E,KAAI,CAAC,QAAS,QAAO;AAErB,KAAI,OAAO,YAAY,SACrB,QAAO;AAGT,KAAI,MAAM,QAAQ,QAAQ,CACxB,QAAO,QAAQ,KAAK,KAAK;AAG3B,KAAI,OAAO,YAAY,UAAU;AAC/B,MAAI,aAAa,WAAW,OAAO,QAAQ,YAAY,SAAU,QAAO,QAAQ;AAChF,MAAI,WAAW,WAAW,OAAO,QAAQ,UAAU,SAAU,QAAO,QAAQ;AAC5E,MAAI,aAAa,WAAW,OAAO,QAAQ,YAAY,SAAU,QAAO,QAAQ;AAChF,MAAI;AACF,UAAO,KAAK,UAAU,QAAQ;UACxB;AACN,UAAO;;;AAIX,QAAO,OAAO,QAAQ;;AAGxB,MAAM,oBAAoB,EACxB,UACA,WACA,oBACA,YACA,IACA,SACA,sBAAsB,GACtB,sBAAsB,MACtB,MACA,WAKI;CACJ,MAAM,UAAU,0BAA0B,QAAQ;CAClD,MAAM,YAAY,QAAQ,QAAQ;CAClC,MAAM,iBAAiB,uBAAuB,sBAAsB,IAAI,GAAG,KAAK,IAAI,qBAAqB,EAAE,GAAG,KAAK,OAAO;CAC1H,MAAM,cAAc,SAAS,WAAW,qBAAqB;AAE7D,QACE,oBAAC,OAAD;EACE,eAAa,CAAC;EACd,WAAW,GACT,sFACA,sBAAsB,SAAS,QAC/B,YAAY,8BAA8B,8BAC1C,CAAC,uBAAuB,CAAC,aAAa,UACtC,mBACD;EACD,OAAO,iBAAiB,EAAE,WAAW,gBAAgB,GAAG;YAExD,oBAAC,KAAD;GAAG,eAAa,YAAY,SAAS;GAAW,aAAW,YAAY,WAAW;GAAW,WAAW,GAAG,0CAA0C,aAAa,UAAU;GAAE,eAAa,YAAY,aAAa;GAAe;GAAI,MAAM,YAAY,OAAO;aAC7P;GACC;EACA;;AAIV,MAAa,gBAAgB,EAAE,WAAW,oBAAoB,aAAa,qCAAqC,IAAI,SAAS,qBAAqB,0BAAiD;AACjM,QAAO,oBAAC,kBAAD;EAAkB,UAAS;EAAoB;EAA+B;EAAgC;EAAgB;EAAa;EAA8B;EAA0C;EAAqB,MAAK;EAAS,MAAK;EAAW;;AAG/Q,MAAa,kBAAkB,EAAE,WAAW,oBAAoB,aAAa,uCAAuC,IAAI,SAAS,qBAAqB,0BAAiD;AACrM,QAAO,oBAAC,kBAAD;EAAkB,UAAS;EAAoB;EAA+B;EAAgC;EAAgB;EAAa;EAA8B;EAA0C;EAAqB,MAAK;EAAS,MAAK;EAAY"}
package/dist/Input.js CHANGED
@@ -38,7 +38,7 @@ const getAutoCompleteValue = (type) => {
38
38
  }[type] || "off";
39
39
  };
40
40
  const Input = (allProps) => {
41
- const { className, clearOnFocus = false, defaultValue, disabled, endIcon, errorMessage, id, label, labelClassName, name, onBlur, onChange, onFocus, placeholder, prefix, ref, required, showClearButton = false, showStateIcon = true, startIcon, state = "default", suppressHydrationWarning = true, type = "text", value: valueProp, warningMessage, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...props } = allProps;
41
+ const { className, clearOnFocus = false, defaultValue, disabled, endIcon, errorMessage, id, label, labelClassName, messageReserveLines = 1, messageReserveSpace = true, name, onBlur, onChange, onFocus, placeholder, prefix, ref, required, showClearButton = false, showStateIcon = true, startIcon, state = "default", suppressHydrationWarning = true, type = "text", value: valueProp, warningMessage, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...props } = allProps;
42
42
  const inputId = useFormFieldId(id, name);
43
43
  const errorMessageId = `${inputId}-error`;
44
44
  const warningMessageId = `${inputId}-warning`;
@@ -183,12 +183,16 @@ const Input = (allProps) => {
183
183
  /* @__PURE__ */ jsx(ErrorMessage, {
184
184
  dataTestId: "spectral-input-error-message",
185
185
  id: errorMessageId,
186
- message: isInvalid ? errorMessage ?? null : null
186
+ message: isInvalid ? errorMessage ?? null : null,
187
+ messageReserveLines,
188
+ messageReserveSpace: messageReserveSpace && state === "error"
187
189
  }),
188
190
  /* @__PURE__ */ jsx(WarningMessage, {
189
191
  dataTestId: "spectral-input-warning-message",
190
192
  id: warningMessageId,
191
- message: state === "warning" ? warningMessage ?? null : null
193
+ message: state === "warning" ? warningMessage ?? null : null,
194
+ messageReserveLines,
195
+ messageReserveSpace: messageReserveSpace && state === "warning"
192
196
  })
193
197
  ]
194
198
  })]
package/dist/Input.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"Input.js","names":[],"sources":["../src/components/Input/Input.tsx"],"sourcesContent":["import { CheckCircleIcon, CloseCircleIcon, ErrorIcon, EyeClosedIcon, EyeOpenIcon, LoaderIcon, WarningIcon } from '@components/Icons'\nimport { Label } from '@components/Label/Label'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { ErrorMessage, WarningMessage, getAriaProps, getFormFieldCSSProperties, getInputClasses, useFormFieldId, useFormFieldState, type BaseFormFieldProps } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useRef, type ChangeEvent, type CSSProperties, type FocusEvent, type InputHTMLAttributes, type ReactElement, type Ref } from 'react'\nimport { useClearOnFocus, usePasswordVisibility, usePrefixWidth } from './InputUtils'\n\nexport type InputType = 'text' | 'email' | 'url' | 'tel' | 'password' | 'number' | 'date' | 'datetime-local'\n\nexport type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'id' | 'onChange'> &\n BaseFormFieldProps & {\n className?: string\n clearOnFocus?: boolean\n endIcon?: ReactElement\n labelClassName?: string\n onBlur?: (e: FocusEvent<HTMLInputElement>) => void\n onChange?: (value: string) => void\n onFocus?: (e: FocusEvent<HTMLInputElement>) => void\n placeholder?: string\n prefix?: string\n showClearButton?: boolean\n showStateIcon?: boolean\n startIcon?: ReactElement\n suppressHydrationWarning?: boolean\n type?: InputType\n value?: string\n warningMessage?: BaseFormFieldProps['errorMessage']\n }\n\nconst mergeRefs = <T,>(...refs: (Ref<T> | undefined)[]): Ref<T> => {\n return (value: T | null) => {\n refs.forEach((ref) => {\n if (!ref) return\n if (typeof ref === 'function') {\n ref(value)\n } else {\n ;(ref as { current: T | null }).current = value\n }\n })\n }\n}\n\nconst getAutoCompleteValue = (type: InputType): string => {\n const autoCompleteMap: Record<InputType, string> = {\n date: 'off',\n email: 'email',\n number: 'off',\n password: 'current-password',\n tel: 'tel',\n text: 'off',\n url: 'url',\n 'datetime-local': 'off',\n }\n return autoCompleteMap[type] || 'off'\n}\n\nexport const Input = (\n allProps: InputProps & {\n ref?: Ref<HTMLInputElement>\n },\n): ReactElement => {\n const {\n className,\n clearOnFocus = false,\n defaultValue,\n disabled,\n endIcon,\n errorMessage,\n id,\n label,\n labelClassName,\n name,\n onBlur,\n onChange,\n onFocus,\n placeholder,\n prefix,\n ref,\n required,\n showClearButton = false,\n showStateIcon = true,\n startIcon,\n state = 'default',\n suppressHydrationWarning = true,\n type = 'text',\n value: valueProp,\n warningMessage,\n 'aria-label': ariaLabel,\n 'aria-describedby': ariaDescribedBy,\n ...props\n } = allProps\n const inputId = useFormFieldId(id, name)\n const errorMessageId = `${inputId}-error`\n const warningMessageId = `${inputId}-warning`\n const { isDisabled, isLoading, isInvalid } = useFormFieldState(disabled, state)\n const messageId = state === 'error' ? errorMessageId : state === 'warning' && warningMessage ? warningMessageId : undefined\n const ariaProps = getAriaProps(state, ariaDescribedBy, required, messageId)\n const normalizedDefaultValue = typeof defaultValue === 'string' ? defaultValue : defaultValue !== undefined && defaultValue !== null ? String(defaultValue) : ''\n const [value, setValue] = useUncontrolledState<string>({\n value: valueProp,\n defaultValue: normalizedDefaultValue,\n onChange,\n })\n\n const internalRef = useRef<HTMLInputElement>(null)\n const inputRef = mergeRefs(ref, internalRef)\n\n const { isVisible, toggleVisibility, inputType } = usePasswordVisibility()\n const { prefixWidth, prefixRef } = usePrefixWidth(prefix)\n const { handleFocus: clearOnFocusHandler } = useClearOnFocus(clearOnFocus, (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value))\n\n const handleBlur = useCallback(\n (e: FocusEvent<HTMLInputElement>): void => {\n onBlur?.(e)\n },\n [onBlur],\n )\n\n const handleFocus = useCallback(\n (e: FocusEvent<HTMLInputElement>): void => {\n clearOnFocusHandler(e, onFocus)\n },\n [clearOnFocusHandler, onFocus],\n )\n\n const handleChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>): void => {\n const newValue = e.target.value\n setValue(newValue)\n },\n [setValue],\n )\n\n const handleClear = useCallback((): void => {\n const element = internalRef.current\n if (element) {\n setValue('')\n element.focus()\n }\n }, [setValue])\n\n const showClearButtonNow = showClearButton && value.length > 0\n\n const getEndIcon = (): ReactElement | null => {\n const iconClasses = 'absolute right-4 top-1/2 -translate-y-1/2 text-input-icon hover:text-input-icon--hover focus:outline-none cursor-pointer'\n\n const iconComponents = {\n password: () => (\n <button aria-controls={inputId} aria-label={isVisible ? `Hide ${label ?? 'password'}` : `Show ${label ?? 'password'}`} aria-pressed={isVisible} className={iconClasses} data-testid='spectral-input-password-toggle' onClick={toggleVisibility} type='button'>\n {isVisible ? <EyeClosedIcon size={22} /> : <EyeOpenIcon size={22} />}\n </button>\n ),\n clear: () => (\n <button aria-label={String(`Clear ${label ?? 'input'}`)} className={iconClasses} data-testid='spectral-input-clear-button' onClick={handleClear} type='button'>\n <CloseCircleIcon size={24} />\n </button>\n ),\n loading: () => (\n <div className='right-4 text-input-icon absolute top-1/2 -translate-y-1/2' data-testid='spectral-input-loading-icon'>\n <LoaderIcon size={24} />\n </div>\n ),\n error: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-danger-400' data-testid='spectral-input-error-icon'>\n <ErrorIcon size={24} />\n </div>\n ),\n success: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-success-400' data-testid='spectral-input-success-icon'>\n <CheckCircleIcon size={24} />\n </div>\n ),\n warning: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-warning-400' data-testid='spectral-input-warning-icon'>\n <WarningIcon size={24} />\n </div>\n ),\n }\n\n if (endIcon) return <div className='right-4 text-input-icon absolute top-1/2 -translate-y-1/2'>{endIcon}</div>\n if (type === 'password') return iconComponents.password()\n if (showClearButtonNow) return iconComponents.clear()\n if (isLoading) return iconComponents.loading()\n if (!showStateIcon && (state === 'success' || state === 'warning' || state === 'error')) return null\n if (state === 'success') return iconComponents.success()\n if (state === 'warning') return iconComponents.warning()\n if (state === 'error') return iconComponents.error()\n\n return null\n }\n\n const getStartIcon = (): ReactElement | null => {\n if (startIcon) return startIcon\n return null\n }\n\n const usesTabularNumbers = type === 'number' || type === 'date' || type === 'datetime-local'\n const inputClasses = cn(getInputClasses(state, className), '[text-indent:var(--prefix-width)]', showClearButtonNow && 'pr-10', usesTabularNumbers && 'tabular-nums')\n\n const prefixClasses = cn('inset-y-0 left-4 text-base pointer-events-none absolute flex items-center text-input-text-prefix opacity-100 peer-disabled:opacity-50')\n\n return (\n <div className='space-y-1.5 w-full' data-testid='spectral-input-container'>\n {label && (\n <Label className={cn('mb-2 block', labelClassName, isDisabled && 'cursor-not-allowed text-input-text--disabled placeholder:text-input-text-placeholder')} data-testid='spectral-input-label' htmlFor={inputId}>\n {label}\n </Label>\n )}\n <div className='relative' data-testid='spectral-input-wrapper'>\n <div className='relative'>\n {getStartIcon()}\n {prefix && (\n <span ref={prefixRef} className={prefixClasses}>\n {prefix}\n </span>\n )}\n <input\n aria-label={ariaLabel ?? label}\n autoComplete={getAutoCompleteValue(type)}\n className={inputClasses}\n data-state={state}\n data-testid='spectral-input'\n disabled={isDisabled}\n id={inputId}\n name={name}\n onBlur={handleBlur}\n onChange={handleChange}\n onFocus={handleFocus}\n placeholder={placeholder ?? label}\n ref={inputRef}\n required={required}\n style={\n getFormFieldCSSProperties({\n '--prefix-width': prefix ? `${prefixWidth}px` : '0',\n }) as CSSProperties\n }\n suppressHydrationWarning={suppressHydrationWarning}\n type={type === 'password' ? inputType : type}\n value={value}\n {...ariaProps}\n {...props}\n />\n {getEndIcon()}\n </div>\n\n <ErrorMessage dataTestId='spectral-input-error-message' id={errorMessageId} message={isInvalid ? (errorMessage ?? null) : null} />\n <WarningMessage dataTestId='spectral-input-warning-message' id={warningMessageId} message={state === 'warning' ? (warningMessage ?? null) : null} />\n </div>\n </div>\n )\n}\nInput.displayName = 'Input'\n"],"mappings":";;;;;;;;;;;;;;;;;;AA8BA,MAAM,aAAiB,GAAG,SAAyC;AACjE,SAAQ,UAAoB;AAC1B,OAAK,SAAS,QAAQ;AACpB,OAAI,CAAC,IAAK;AACV,OAAI,OAAO,QAAQ,WACjB,KAAI,MAAM;OAET,CAAC,IAA8B,UAAU;IAE5C;;;AAIN,MAAM,wBAAwB,SAA4B;AAWxD,QAAO;EATL,MAAM;EACN,OAAO;EACP,QAAQ;EACR,UAAU;EACV,KAAK;EACL,MAAM;EACN,KAAK;EACL,kBAAkB;EAEE,CAAC,SAAS;;AAGlC,MAAa,SACX,aAGiB;CACjB,MAAM,EACJ,WACA,eAAe,OACf,cACA,UACA,SACA,cACA,IACA,OACA,gBACA,MACA,QACA,UACA,SACA,aACA,QACA,KACA,UACA,kBAAkB,OAClB,gBAAgB,MAChB,WACA,QAAQ,WACR,2BAA2B,MAC3B,OAAO,QACP,OAAO,WACP,gBACA,cAAc,WACd,oBAAoB,iBACpB,GAAG,UACD;CACJ,MAAM,UAAU,eAAe,IAAI,KAAK;CACxC,MAAM,iBAAiB,GAAG,QAAQ;CAClC,MAAM,mBAAmB,GAAG,QAAQ;CACpC,MAAM,EAAE,YAAY,WAAW,cAAc,kBAAkB,UAAU,MAAM;CAE/E,MAAM,YAAY,aAAa,OAAO,iBAAiB,UADrC,UAAU,UAAU,iBAAiB,UAAU,aAAa,iBAAiB,mBAAmB,OACvC;CAE3E,MAAM,CAAC,OAAO,YAAY,qBAA6B;EACrD,OAAO;EACP,cAH6B,OAAO,iBAAiB,WAAW,eAAe,iBAAiB,UAAa,iBAAiB,OAAO,OAAO,aAAa,GAAG;EAI5J;EACD,CAAC;CAEF,MAAM,cAAc,OAAyB,KAAK;CAClD,MAAM,WAAW,UAAU,KAAK,YAAY;CAE5C,MAAM,EAAE,WAAW,kBAAkB,cAAc,uBAAuB;CAC1E,MAAM,EAAE,aAAa,cAAc,eAAe,OAAO;CACzD,MAAM,EAAE,aAAa,wBAAwB,gBAAgB,eAAe,MAAqC,SAAS,EAAE,OAAO,MAAM,CAAC;CAE1I,MAAM,aAAa,aAChB,MAA0C;AACzC,WAAS,EAAE;IAEb,CAAC,OAAO,CACT;CAED,MAAM,cAAc,aACjB,MAA0C;AACzC,sBAAoB,GAAG,QAAQ;IAEjC,CAAC,qBAAqB,QAAQ,CAC/B;CAED,MAAM,eAAe,aAClB,MAA2C;EAC1C,MAAM,WAAW,EAAE,OAAO;AAC1B,WAAS,SAAS;IAEpB,CAAC,SAAS,CACX;CAED,MAAM,cAAc,kBAAwB;EAC1C,MAAM,UAAU,YAAY;AAC5B,MAAI,SAAS;AACX,YAAS,GAAG;AACZ,WAAQ,OAAO;;IAEhB,CAAC,SAAS,CAAC;CAEd,MAAM,qBAAqB,mBAAmB,MAAM,SAAS;CAE7D,MAAM,mBAAwC;EAC5C,MAAM,cAAc;EAEpB,MAAM,iBAAiB;GACrB,gBACE,oBAAC,UAAD;IAAQ,iBAAe;IAAS,cAAY,YAAY,QAAQ,SAAS,eAAe,QAAQ,SAAS;IAAc,gBAAc;IAAW,WAAW;IAAa,eAAY;IAAiC,SAAS;IAAkB,MAAK;cAClP,YAAY,oBAAC,eAAD,EAAe,MAAM,IAAM,IAAG,oBAAC,aAAD,EAAa,MAAM,IAAM;IAC7D;GAEX,aACE,oBAAC,UAAD;IAAQ,cAAY,OAAO,SAAS,SAAS,UAAU;IAAE,WAAW;IAAa,eAAY;IAA8B,SAAS;IAAa,MAAK;cACpJ,oBAAC,iBAAD,EAAiB,MAAM,IAAM;IACtB;GAEX,eACE,oBAAC,OAAD;IAAK,WAAU;IAA4D,eAAY;cACrF,oBAAC,YAAD,EAAY,MAAM,IAAM;IACpB;GAER,aACE,oBAAC,OAAD;IAAK,WAAU;IAA4D,eAAY;cACrF,oBAAC,WAAD,EAAW,MAAM,IAAM;IACnB;GAER,eACE,oBAAC,OAAD;IAAK,WAAU;IAA6D,eAAY;cACtF,oBAAC,iBAAD,EAAiB,MAAM,IAAM;IACzB;GAER,eACE,oBAAC,OAAD;IAAK,WAAU;IAA6D,eAAY;cACtF,oBAAC,aAAD,EAAa,MAAM,IAAM;IACrB;GAET;AAED,MAAI,QAAS,QAAO,oBAAC,OAAD;GAAK,WAAU;aAA6D;GAAc;AAC9G,MAAI,SAAS,WAAY,QAAO,eAAe,UAAU;AACzD,MAAI,mBAAoB,QAAO,eAAe,OAAO;AACrD,MAAI,UAAW,QAAO,eAAe,SAAS;AAC9C,MAAI,CAAC,kBAAkB,UAAU,aAAa,UAAU,aAAa,UAAU,SAAU,QAAO;AAChG,MAAI,UAAU,UAAW,QAAO,eAAe,SAAS;AACxD,MAAI,UAAU,UAAW,QAAO,eAAe,SAAS;AACxD,MAAI,UAAU,QAAS,QAAO,eAAe,OAAO;AAEpD,SAAO;;CAGT,MAAM,qBAA0C;AAC9C,MAAI,UAAW,QAAO;AACtB,SAAO;;CAGT,MAAM,qBAAqB,SAAS,YAAY,SAAS,UAAU,SAAS;CAC5E,MAAM,eAAe,GAAG,gBAAgB,OAAO,UAAU,EAAE,qCAAqC,sBAAsB,SAAS,sBAAsB,eAAe;CAEpK,MAAM,gBAAgB,GAAG,wIAAwI;AAEjK,QACE,qBAAC,OAAD;EAAK,WAAU;EAAqB,eAAY;YAAhD,CACG,SACC,oBAAC,OAAD;GAAO,WAAW,GAAG,cAAc,gBAAgB,cAAc,uFAAuF;GAAE,eAAY;GAAuB,SAAS;aACnM;GACK,GAEV,qBAAC,OAAD;GAAK,WAAU;GAAW,eAAY;aAAtC;IACE,qBAAC,OAAD;KAAK,WAAU;eAAf;MACG,cAAc;MACd,UACC,oBAAC,QAAD;OAAM,KAAK;OAAW,WAAW;iBAC9B;OACI;MAET,oBAAC,SAAD;OACE,cAAY,aAAa;OACzB,cAAc,qBAAqB,KAAK;OACxC,WAAW;OACX,cAAY;OACZ,eAAY;OACZ,UAAU;OACV,IAAI;OACE;OACN,QAAQ;OACR,UAAU;OACV,SAAS;OACT,aAAa,eAAe;OAC5B,KAAK;OACK;OACV,OACE,0BAA0B,EACxB,kBAAkB,SAAS,GAAG,YAAY,MAAM,KACjD,CAAC;OAEsB;OAC1B,MAAM,SAAS,aAAa,YAAY;OACjC;OACP,GAAI;OACJ,GAAI;OACJ;MACD,YAAY;MACT;;IAEN,oBAAC,cAAD;KAAc,YAAW;KAA+B,IAAI;KAAgB,SAAS,YAAa,gBAAgB,OAAQ;KAAQ;IAClI,oBAAC,gBAAD;KAAgB,YAAW;KAAiC,IAAI;KAAkB,SAAS,UAAU,YAAa,kBAAkB,OAAQ;KAAQ;IAChJ;KACF;;;AAGV,MAAM,cAAc"}
1
+ {"version":3,"file":"Input.js","names":[],"sources":["../src/components/Input/Input.tsx"],"sourcesContent":["import { CheckCircleIcon, CloseCircleIcon, ErrorIcon, EyeClosedIcon, EyeOpenIcon, LoaderIcon, WarningIcon } from '@components/Icons'\nimport { Label } from '@components/Label/Label'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { ErrorMessage, WarningMessage, getAriaProps, getFormFieldCSSProperties, getInputClasses, useFormFieldId, useFormFieldState, type BaseFormFieldProps } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useRef, type ChangeEvent, type CSSProperties, type FocusEvent, type InputHTMLAttributes, type ReactElement, type Ref } from 'react'\nimport { useClearOnFocus, usePasswordVisibility, usePrefixWidth } from './InputUtils'\n\nexport type InputType = 'text' | 'email' | 'url' | 'tel' | 'password' | 'number' | 'date' | 'datetime-local'\n\nexport type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'id' | 'onChange'> &\n BaseFormFieldProps & {\n className?: string\n clearOnFocus?: boolean\n endIcon?: ReactElement\n labelClassName?: string\n onBlur?: (e: FocusEvent<HTMLInputElement>) => void\n onChange?: (value: string) => void\n onFocus?: (e: FocusEvent<HTMLInputElement>) => void\n placeholder?: string\n prefix?: string\n showClearButton?: boolean\n showStateIcon?: boolean\n startIcon?: ReactElement\n suppressHydrationWarning?: boolean\n type?: InputType\n value?: string\n warningMessage?: BaseFormFieldProps['errorMessage']\n }\n\nconst mergeRefs = <T,>(...refs: (Ref<T> | undefined)[]): Ref<T> => {\n return (value: T | null) => {\n refs.forEach((ref) => {\n if (!ref) return\n if (typeof ref === 'function') {\n ref(value)\n } else {\n ;(ref as { current: T | null }).current = value\n }\n })\n }\n}\n\nconst getAutoCompleteValue = (type: InputType): string => {\n const autoCompleteMap: Record<InputType, string> = {\n date: 'off',\n email: 'email',\n number: 'off',\n password: 'current-password',\n tel: 'tel',\n text: 'off',\n url: 'url',\n 'datetime-local': 'off',\n }\n return autoCompleteMap[type] || 'off'\n}\n\nexport const Input = (\n allProps: InputProps & {\n ref?: Ref<HTMLInputElement>\n },\n): ReactElement => {\n const {\n className,\n clearOnFocus = false,\n defaultValue,\n disabled,\n endIcon,\n errorMessage,\n id,\n label,\n labelClassName,\n messageReserveLines = 1,\n messageReserveSpace = true,\n name,\n onBlur,\n onChange,\n onFocus,\n placeholder,\n prefix,\n ref,\n required,\n showClearButton = false,\n showStateIcon = true,\n startIcon,\n state = 'default',\n suppressHydrationWarning = true,\n type = 'text',\n value: valueProp,\n warningMessage,\n 'aria-label': ariaLabel,\n 'aria-describedby': ariaDescribedBy,\n ...props\n } = allProps\n const inputId = useFormFieldId(id, name)\n const errorMessageId = `${inputId}-error`\n const warningMessageId = `${inputId}-warning`\n const { isDisabled, isLoading, isInvalid } = useFormFieldState(disabled, state)\n const messageId = state === 'error' ? errorMessageId : state === 'warning' && warningMessage ? warningMessageId : undefined\n const ariaProps = getAriaProps(state, ariaDescribedBy, required, messageId)\n const normalizedDefaultValue = typeof defaultValue === 'string' ? defaultValue : defaultValue !== undefined && defaultValue !== null ? String(defaultValue) : ''\n const [value, setValue] = useUncontrolledState<string>({\n value: valueProp,\n defaultValue: normalizedDefaultValue,\n onChange,\n })\n\n const internalRef = useRef<HTMLInputElement>(null)\n const inputRef = mergeRefs(ref, internalRef)\n\n const { isVisible, toggleVisibility, inputType } = usePasswordVisibility()\n const { prefixWidth, prefixRef } = usePrefixWidth(prefix)\n const { handleFocus: clearOnFocusHandler } = useClearOnFocus(clearOnFocus, (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value))\n\n const handleBlur = useCallback(\n (e: FocusEvent<HTMLInputElement>): void => {\n onBlur?.(e)\n },\n [onBlur],\n )\n\n const handleFocus = useCallback(\n (e: FocusEvent<HTMLInputElement>): void => {\n clearOnFocusHandler(e, onFocus)\n },\n [clearOnFocusHandler, onFocus],\n )\n\n const handleChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>): void => {\n const newValue = e.target.value\n setValue(newValue)\n },\n [setValue],\n )\n\n const handleClear = useCallback((): void => {\n const element = internalRef.current\n if (element) {\n setValue('')\n element.focus()\n }\n }, [setValue])\n\n const showClearButtonNow = showClearButton && value.length > 0\n\n const getEndIcon = (): ReactElement | null => {\n const iconClasses = 'absolute right-4 top-1/2 -translate-y-1/2 text-input-icon hover:text-input-icon--hover focus:outline-none cursor-pointer'\n\n const iconComponents = {\n password: () => (\n <button aria-controls={inputId} aria-label={isVisible ? `Hide ${label ?? 'password'}` : `Show ${label ?? 'password'}`} aria-pressed={isVisible} className={iconClasses} data-testid='spectral-input-password-toggle' onClick={toggleVisibility} type='button'>\n {isVisible ? <EyeClosedIcon size={22} /> : <EyeOpenIcon size={22} />}\n </button>\n ),\n clear: () => (\n <button aria-label={String(`Clear ${label ?? 'input'}`)} className={iconClasses} data-testid='spectral-input-clear-button' onClick={handleClear} type='button'>\n <CloseCircleIcon size={24} />\n </button>\n ),\n loading: () => (\n <div className='right-4 text-input-icon absolute top-1/2 -translate-y-1/2' data-testid='spectral-input-loading-icon'>\n <LoaderIcon size={24} />\n </div>\n ),\n error: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-danger-400' data-testid='spectral-input-error-icon'>\n <ErrorIcon size={24} />\n </div>\n ),\n success: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-success-400' data-testid='spectral-input-success-icon'>\n <CheckCircleIcon size={24} />\n </div>\n ),\n warning: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-warning-400' data-testid='spectral-input-warning-icon'>\n <WarningIcon size={24} />\n </div>\n ),\n }\n\n if (endIcon) return <div className='right-4 text-input-icon absolute top-1/2 -translate-y-1/2'>{endIcon}</div>\n if (type === 'password') return iconComponents.password()\n if (showClearButtonNow) return iconComponents.clear()\n if (isLoading) return iconComponents.loading()\n if (!showStateIcon && (state === 'success' || state === 'warning' || state === 'error')) return null\n if (state === 'success') return iconComponents.success()\n if (state === 'warning') return iconComponents.warning()\n if (state === 'error') return iconComponents.error()\n\n return null\n }\n\n const getStartIcon = (): ReactElement | null => {\n if (startIcon) return startIcon\n return null\n }\n\n const usesTabularNumbers = type === 'number' || type === 'date' || type === 'datetime-local'\n const inputClasses = cn(getInputClasses(state, className), '[text-indent:var(--prefix-width)]', showClearButtonNow && 'pr-10', usesTabularNumbers && 'tabular-nums')\n\n const prefixClasses = cn('inset-y-0 left-4 text-base pointer-events-none absolute flex items-center text-input-text-prefix opacity-100 peer-disabled:opacity-50')\n\n return (\n <div className='space-y-1.5 w-full' data-testid='spectral-input-container'>\n {label && (\n <Label className={cn('mb-2 block', labelClassName, isDisabled && 'cursor-not-allowed text-input-text--disabled placeholder:text-input-text-placeholder')} data-testid='spectral-input-label' htmlFor={inputId}>\n {label}\n </Label>\n )}\n <div className='relative' data-testid='spectral-input-wrapper'>\n <div className='relative'>\n {getStartIcon()}\n {prefix && (\n <span ref={prefixRef} className={prefixClasses}>\n {prefix}\n </span>\n )}\n <input\n aria-label={ariaLabel ?? label}\n autoComplete={getAutoCompleteValue(type)}\n className={inputClasses}\n data-state={state}\n data-testid='spectral-input'\n disabled={isDisabled}\n id={inputId}\n name={name}\n onBlur={handleBlur}\n onChange={handleChange}\n onFocus={handleFocus}\n placeholder={placeholder ?? label}\n ref={inputRef}\n required={required}\n style={\n getFormFieldCSSProperties({\n '--prefix-width': prefix ? `${prefixWidth}px` : '0',\n }) as CSSProperties\n }\n suppressHydrationWarning={suppressHydrationWarning}\n type={type === 'password' ? inputType : type}\n value={value}\n {...ariaProps}\n {...props}\n />\n {getEndIcon()}\n </div>\n\n <ErrorMessage dataTestId='spectral-input-error-message' id={errorMessageId} message={isInvalid ? (errorMessage ?? null) : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace && state === 'error'} />\n <WarningMessage dataTestId='spectral-input-warning-message' id={warningMessageId} message={state === 'warning' ? (warningMessage ?? null) : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace && state === 'warning'} />\n </div>\n </div>\n )\n}\nInput.displayName = 'Input'\n"],"mappings":";;;;;;;;;;;;;;;;;;AA8BA,MAAM,aAAiB,GAAG,SAAyC;AACjE,SAAQ,UAAoB;AAC1B,OAAK,SAAS,QAAQ;AACpB,OAAI,CAAC,IAAK;AACV,OAAI,OAAO,QAAQ,WACjB,KAAI,MAAM;OAET,CAAC,IAA8B,UAAU;IAE5C;;;AAIN,MAAM,wBAAwB,SAA4B;AAWxD,QAAO;EATL,MAAM;EACN,OAAO;EACP,QAAQ;EACR,UAAU;EACV,KAAK;EACL,MAAM;EACN,KAAK;EACL,kBAAkB;EAEE,CAAC,SAAS;;AAGlC,MAAa,SACX,aAGiB;CACjB,MAAM,EACJ,WACA,eAAe,OACf,cACA,UACA,SACA,cACA,IACA,OACA,gBACA,sBAAsB,GACtB,sBAAsB,MACtB,MACA,QACA,UACA,SACA,aACA,QACA,KACA,UACA,kBAAkB,OAClB,gBAAgB,MAChB,WACA,QAAQ,WACR,2BAA2B,MAC3B,OAAO,QACP,OAAO,WACP,gBACA,cAAc,WACd,oBAAoB,iBACpB,GAAG,UACD;CACJ,MAAM,UAAU,eAAe,IAAI,KAAK;CACxC,MAAM,iBAAiB,GAAG,QAAQ;CAClC,MAAM,mBAAmB,GAAG,QAAQ;CACpC,MAAM,EAAE,YAAY,WAAW,cAAc,kBAAkB,UAAU,MAAM;CAE/E,MAAM,YAAY,aAAa,OAAO,iBAAiB,UADrC,UAAU,UAAU,iBAAiB,UAAU,aAAa,iBAAiB,mBAAmB,OACvC;CAE3E,MAAM,CAAC,OAAO,YAAY,qBAA6B;EACrD,OAAO;EACP,cAH6B,OAAO,iBAAiB,WAAW,eAAe,iBAAiB,UAAa,iBAAiB,OAAO,OAAO,aAAa,GAAG;EAI5J;EACD,CAAC;CAEF,MAAM,cAAc,OAAyB,KAAK;CAClD,MAAM,WAAW,UAAU,KAAK,YAAY;CAE5C,MAAM,EAAE,WAAW,kBAAkB,cAAc,uBAAuB;CAC1E,MAAM,EAAE,aAAa,cAAc,eAAe,OAAO;CACzD,MAAM,EAAE,aAAa,wBAAwB,gBAAgB,eAAe,MAAqC,SAAS,EAAE,OAAO,MAAM,CAAC;CAE1I,MAAM,aAAa,aAChB,MAA0C;AACzC,WAAS,EAAE;IAEb,CAAC,OAAO,CACT;CAED,MAAM,cAAc,aACjB,MAA0C;AACzC,sBAAoB,GAAG,QAAQ;IAEjC,CAAC,qBAAqB,QAAQ,CAC/B;CAED,MAAM,eAAe,aAClB,MAA2C;EAC1C,MAAM,WAAW,EAAE,OAAO;AAC1B,WAAS,SAAS;IAEpB,CAAC,SAAS,CACX;CAED,MAAM,cAAc,kBAAwB;EAC1C,MAAM,UAAU,YAAY;AAC5B,MAAI,SAAS;AACX,YAAS,GAAG;AACZ,WAAQ,OAAO;;IAEhB,CAAC,SAAS,CAAC;CAEd,MAAM,qBAAqB,mBAAmB,MAAM,SAAS;CAE7D,MAAM,mBAAwC;EAC5C,MAAM,cAAc;EAEpB,MAAM,iBAAiB;GACrB,gBACE,oBAAC,UAAD;IAAQ,iBAAe;IAAS,cAAY,YAAY,QAAQ,SAAS,eAAe,QAAQ,SAAS;IAAc,gBAAc;IAAW,WAAW;IAAa,eAAY;IAAiC,SAAS;IAAkB,MAAK;cAClP,YAAY,oBAAC,eAAD,EAAe,MAAM,IAAM,IAAG,oBAAC,aAAD,EAAa,MAAM,IAAM;IAC7D;GAEX,aACE,oBAAC,UAAD;IAAQ,cAAY,OAAO,SAAS,SAAS,UAAU;IAAE,WAAW;IAAa,eAAY;IAA8B,SAAS;IAAa,MAAK;cACpJ,oBAAC,iBAAD,EAAiB,MAAM,IAAM;IACtB;GAEX,eACE,oBAAC,OAAD;IAAK,WAAU;IAA4D,eAAY;cACrF,oBAAC,YAAD,EAAY,MAAM,IAAM;IACpB;GAER,aACE,oBAAC,OAAD;IAAK,WAAU;IAA4D,eAAY;cACrF,oBAAC,WAAD,EAAW,MAAM,IAAM;IACnB;GAER,eACE,oBAAC,OAAD;IAAK,WAAU;IAA6D,eAAY;cACtF,oBAAC,iBAAD,EAAiB,MAAM,IAAM;IACzB;GAER,eACE,oBAAC,OAAD;IAAK,WAAU;IAA6D,eAAY;cACtF,oBAAC,aAAD,EAAa,MAAM,IAAM;IACrB;GAET;AAED,MAAI,QAAS,QAAO,oBAAC,OAAD;GAAK,WAAU;aAA6D;GAAc;AAC9G,MAAI,SAAS,WAAY,QAAO,eAAe,UAAU;AACzD,MAAI,mBAAoB,QAAO,eAAe,OAAO;AACrD,MAAI,UAAW,QAAO,eAAe,SAAS;AAC9C,MAAI,CAAC,kBAAkB,UAAU,aAAa,UAAU,aAAa,UAAU,SAAU,QAAO;AAChG,MAAI,UAAU,UAAW,QAAO,eAAe,SAAS;AACxD,MAAI,UAAU,UAAW,QAAO,eAAe,SAAS;AACxD,MAAI,UAAU,QAAS,QAAO,eAAe,OAAO;AAEpD,SAAO;;CAGT,MAAM,qBAA0C;AAC9C,MAAI,UAAW,QAAO;AACtB,SAAO;;CAGT,MAAM,qBAAqB,SAAS,YAAY,SAAS,UAAU,SAAS;CAC5E,MAAM,eAAe,GAAG,gBAAgB,OAAO,UAAU,EAAE,qCAAqC,sBAAsB,SAAS,sBAAsB,eAAe;CAEpK,MAAM,gBAAgB,GAAG,wIAAwI;AAEjK,QACE,qBAAC,OAAD;EAAK,WAAU;EAAqB,eAAY;YAAhD,CACG,SACC,oBAAC,OAAD;GAAO,WAAW,GAAG,cAAc,gBAAgB,cAAc,uFAAuF;GAAE,eAAY;GAAuB,SAAS;aACnM;GACK,GAEV,qBAAC,OAAD;GAAK,WAAU;GAAW,eAAY;aAAtC;IACE,qBAAC,OAAD;KAAK,WAAU;eAAf;MACG,cAAc;MACd,UACC,oBAAC,QAAD;OAAM,KAAK;OAAW,WAAW;iBAC9B;OACI;MAET,oBAAC,SAAD;OACE,cAAY,aAAa;OACzB,cAAc,qBAAqB,KAAK;OACxC,WAAW;OACX,cAAY;OACZ,eAAY;OACZ,UAAU;OACV,IAAI;OACE;OACN,QAAQ;OACR,UAAU;OACV,SAAS;OACT,aAAa,eAAe;OAC5B,KAAK;OACK;OACV,OACE,0BAA0B,EACxB,kBAAkB,SAAS,GAAG,YAAY,MAAM,KACjD,CAAC;OAEsB;OAC1B,MAAM,SAAS,aAAa,YAAY;OACjC;OACP,GAAI;OACJ,GAAI;OACJ;MACD,YAAY;MACT;;IAEN,oBAAC,cAAD;KAAc,YAAW;KAA+B,IAAI;KAAgB,SAAS,YAAa,gBAAgB,OAAQ;KAA2B;KAAqB,qBAAqB,uBAAuB,UAAU;KAAW;IAC3O,oBAAC,gBAAD;KAAgB,YAAW;KAAiC,IAAI;KAAkB,SAAS,UAAU,YAAa,kBAAkB,OAAQ;KAA2B;KAAqB,qBAAqB,uBAAuB,UAAU;KAAa;IAC3P;KACF;;;AAGV,MAAM,cAAc"}
@@ -6,7 +6,9 @@ import { ReactElement } from "react";
6
6
  type InputNumericProps = Omit<InputProps, 'inputMode' | 'onChange' | 'pattern' | 'type'> & {
7
7
  allowDecimal?: boolean;
8
8
  allowNegative?: boolean;
9
- locale?: string;
9
+ locale?: string; /** Number of message lines to reserve (default: 1 via Input). */
10
+ messageReserveLines?: number; /** Whether to keep message space reserved when hidden (default: true via Input). */
11
+ messageReserveSpace?: boolean;
10
12
  onChange?: (value: string) => void;
11
13
  value?: string;
12
14
  };
@@ -1 +1 @@
1
- {"version":3,"file":"InputNumeric.d.ts","names":[],"sources":["../src/components/InputNumeric/InputNumeric.tsx"],"mappings":";;;;;KA2FY,iBAAA,GAAoB,IAAA,CAAK,UAAA;EACnC,YAAA;EACA,aAAA;EACA,MAAA;EACA,QAAA,IAAY,KAAA;EACZ,KAAA;AAAA;AAAA;EAG6B,YAAA;EAAqB,aAAA;EAAuB,SAAA;EAAW,YAAA;EAAmB,MAAA;EAAQ,GAAA;EAAK,GAAA;EAAK,QAAA;EAAU,SAAA;EAAW,OAAA;EAAS,IAAA;EAAM,KAAA,EAAO,SAAA;EAAA,GAAc;AAAA,GAAS,iBAAA,GAAoB,YAAA;AAAA"}
1
+ {"version":3,"file":"InputNumeric.d.ts","names":[],"sources":["../src/components/InputNumeric/InputNumeric.tsx"],"mappings":";;;;;KA2FY,iBAAA,GAAoB,IAAA,CAAK,UAAA;EACnC,YAAA;EACA,aAAA;EACA,MAAA;EAEA,mBAAA,WAL8B;EAO9B,mBAAA;EACA,QAAA,IAAY,KAAA;EACZ,KAAA;AAAA;AAAA;EAG6B,YAAA;EAAqB,aAAA;EAAuB,SAAA;EAAW,YAAA;EAAmB,MAAA;EAAQ,GAAA;EAAK,GAAA;EAAK,QAAA;EAAU,SAAA;EAAW,OAAA;EAAS,IAAA;EAAM,KAAA,EAAO,SAAA;EAAA,GAAc;AAAA,GAAS,iBAAA,GAAoB,YAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"InputNumeric.js","names":[],"sources":["../src/components/InputNumeric/InputNumeric.tsx"],"sourcesContent":["import { Input, type InputProps } from '@components/Input/Input'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, type ClipboardEvent, type KeyboardEvent, type ReactElement } from 'react'\n\ntype NumericKeyDownEvent = KeyboardEvent<HTMLInputElement>\ntype NumericPasteEvent = ClipboardEvent<HTMLInputElement>\n\nconst DIGIT_REGEX = /^\\d$/\nconst DISALLOWED_SPECIAL_KEYS = new Set(['e', 'E', '+'])\nconst ALLOWED_CONTROL_KEYS = new Set(['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Tab', 'Enter', 'Escape'])\n\nconst getDecimalSeparator = (locale?: string): string => {\n try {\n const formattedNumber = new Intl.NumberFormat(locale).formatToParts(1.1)\n return formattedNumber.find((part) => part.type === 'decimal')?.value ?? '.'\n } catch {\n return '.'\n }\n}\n\nconst normalizeDecimalSeparator = (value: string, decimalSeparator: string): string => {\n if (decimalSeparator === '.') return value\n return value.replaceAll(decimalSeparator, '.')\n}\n\nconst sanitizeNumericValue = (value: string, allowDecimal: boolean, allowNegative: boolean, decimalSeparator: string): string => {\n if (!value) return ''\n\n let sanitized = normalizeDecimalSeparator(value, decimalSeparator).replace(/[^\\d.-]/g, '')\n\n if (!allowNegative) {\n sanitized = sanitized.replace(/-/g, '')\n } else {\n sanitized = sanitized.replace(/(?!^)-/g, '')\n if (sanitized.startsWith('--')) {\n sanitized = `-${sanitized.replace(/-/g, '')}`\n }\n }\n\n if (!allowDecimal) {\n sanitized = sanitized.replace(/\\./g, '')\n } else {\n const firstDecimalIndex = sanitized.indexOf('.')\n if (firstDecimalIndex >= 0) {\n const integerPart = sanitized.slice(0, firstDecimalIndex + 1)\n const fractionalPart = sanitized.slice(firstDecimalIndex + 1).replace(/\\./g, '')\n sanitized = `${integerPart}${fractionalPart}`\n }\n }\n\n return sanitized\n}\n\nconst shouldAllowDecimalInput = (target: HTMLInputElement): boolean => {\n if (target.selectionStart === null || target.selectionEnd === null) {\n return !target.value.includes('.')\n }\n\n const selectedText = target.value.slice(target.selectionStart, target.selectionEnd)\n return !target.value.includes('.') || selectedText.includes('.')\n}\n\nconst shouldAllowNegativeInput = (target: HTMLInputElement): boolean => {\n if (target.selectionStart === null || target.selectionEnd === null) {\n return target.value.length === 0\n }\n\n const isAtStart = target.selectionStart === 0\n const selectedText = target.value.slice(target.selectionStart, target.selectionEnd)\n return isAtStart && (!target.value.includes('-') || selectedText.includes('-'))\n}\n\nconst parseNumericProp = (value: number | string | undefined): number | undefined => {\n if (value === undefined || value === null || value === '') return undefined\n const parsed = typeof value === 'number' ? value : Number.parseFloat(value)\n return Number.isFinite(parsed) ? parsed : undefined\n}\n\nconst getDecimalPlaces = (value: number): number => {\n const valueString = value.toString()\n if (!valueString.includes('.')) return 0\n return valueString.split('.')[1]?.length ?? 0\n}\n\nconst roundToPrecision = (value: number, precision: number): number => {\n if (precision <= 0) return value\n const factor = 10 ** precision\n return Math.round(value * factor) / factor\n}\n\nexport type InputNumericProps = Omit<InputProps, 'inputMode' | 'onChange' | 'pattern' | 'type'> & {\n allowDecimal?: boolean\n allowNegative?: boolean\n locale?: string\n onChange?: (value: string) => void\n value?: string\n}\n\nexport const InputNumeric = ({ allowDecimal = true, allowNegative = false, className, defaultValue = '', locale, max, min, onChange, onKeyDown, onPaste, step, value: valueProp, ...props }: InputNumericProps): ReactElement => {\n const normalizedDefaultValue = typeof defaultValue === 'string' ? defaultValue : defaultValue !== undefined && defaultValue !== null ? String(defaultValue) : ''\n const [value, setValue] = useUncontrolledState<string>({\n value: valueProp,\n defaultValue: normalizedDefaultValue,\n onChange,\n })\n const decimalSeparator = getDecimalSeparator(locale)\n const parsedMin = parseNumericProp(min)\n const parsedMax = parseNumericProp(max)\n const parsedStep = parseNumericProp(step)\n const stepValue = parsedStep && parsedStep > 0 ? parsedStep : 1\n const effectiveMin = parsedMin ?? (!allowNegative ? 0 : undefined)\n\n const handleKeyDown = useCallback(\n (event: NumericKeyDownEvent): void => {\n onKeyDown?.(event)\n if (event.defaultPrevented || event.nativeEvent.isComposing) return\n\n if (event.metaKey || event.ctrlKey || event.altKey) return\n if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {\n event.preventDefault()\n\n const direction = event.key === 'ArrowUp' ? 1 : -1\n const normalizedValue = normalizeDecimalSeparator(value, decimalSeparator)\n const currentValue = Number.parseFloat(normalizedValue)\n const hasCurrentValue = Number.isFinite(currentValue)\n const baseValue = hasCurrentValue ? currentValue : (effectiveMin ?? 0)\n\n let nextValue = baseValue + direction * stepValue\n if (effectiveMin !== undefined) {\n nextValue = Math.max(effectiveMin, nextValue)\n }\n if (parsedMax !== undefined) {\n nextValue = Math.min(parsedMax, nextValue)\n }\n\n if (!allowDecimal) {\n nextValue = Math.trunc(nextValue)\n }\n\n const precision = allowDecimal ? getDecimalPlaces(stepValue) : 0\n const roundedValue = roundToPrecision(nextValue, precision)\n const safeValue = Object.is(roundedValue, -0) ? 0 : roundedValue\n const nextString = precision > 0 ? safeValue.toFixed(precision).replace(/\\.?0+$/, '') : safeValue.toString()\n setValue(sanitizeNumericValue(nextString, allowDecimal, allowNegative, decimalSeparator))\n return\n }\n\n if (ALLOWED_CONTROL_KEYS.has(event.key)) return\n if (DIGIT_REGEX.test(event.key)) return\n\n if (DISALLOWED_SPECIAL_KEYS.has(event.key)) {\n event.preventDefault()\n return\n }\n\n const isDecimalKey = event.key === '.' || event.key === decimalSeparator\n\n if (isDecimalKey) {\n if (!allowDecimal || !shouldAllowDecimalInput(event.currentTarget)) {\n event.preventDefault()\n }\n return\n }\n\n if (event.key === '-') {\n if (!allowNegative || !shouldAllowNegativeInput(event.currentTarget)) {\n event.preventDefault()\n }\n return\n }\n\n event.preventDefault()\n },\n [allowDecimal, allowNegative, decimalSeparator, effectiveMin, onKeyDown, parsedMax, setValue, stepValue, value],\n )\n\n const handlePaste = useCallback(\n (event: NumericPasteEvent): void => {\n onPaste?.(event)\n },\n [onPaste],\n )\n\n const handleChange = useCallback(\n (newValue: string): void => {\n setValue(sanitizeNumericValue(newValue, allowDecimal, allowNegative, decimalSeparator))\n },\n [allowDecimal, allowNegative, decimalSeparator, setValue],\n )\n\n const inputMode = allowDecimal ? 'decimal' : 'numeric'\n const pattern = allowDecimal ? '[0-9]*[.]?[0-9]*' : '[0-9]*'\n const normalizedValue = normalizeDecimalSeparator(value, decimalSeparator)\n const ariaValueNow = normalizedValue === '' || normalizedValue === '-' || normalizedValue === '.' ? undefined : Number.parseFloat(normalizedValue)\n\n return (\n <Input\n {...props}\n aria-valuemax={parsedMax}\n aria-valuemin={effectiveMin}\n aria-valuenow={Number.isFinite(ariaValueNow) ? ariaValueNow : undefined}\n className={cn('tabular-nums', className)}\n inputMode={inputMode}\n max={max}\n min={min}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n onPaste={handlePaste}\n pattern={pattern}\n role='spinbutton'\n step={step}\n type='text'\n value={value}\n />\n )\n}\n\nInputNumeric.displayName = 'InputNumeric'\n"],"mappings":"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"}
1
+ {"version":3,"file":"InputNumeric.js","names":[],"sources":["../src/components/InputNumeric/InputNumeric.tsx"],"sourcesContent":["import { Input, type InputProps } from '@components/Input/Input'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, type ClipboardEvent, type KeyboardEvent, type ReactElement } from 'react'\n\ntype NumericKeyDownEvent = KeyboardEvent<HTMLInputElement>\ntype NumericPasteEvent = ClipboardEvent<HTMLInputElement>\n\nconst DIGIT_REGEX = /^\\d$/\nconst DISALLOWED_SPECIAL_KEYS = new Set(['e', 'E', '+'])\nconst ALLOWED_CONTROL_KEYS = new Set(['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Tab', 'Enter', 'Escape'])\n\nconst getDecimalSeparator = (locale?: string): string => {\n try {\n const formattedNumber = new Intl.NumberFormat(locale).formatToParts(1.1)\n return formattedNumber.find((part) => part.type === 'decimal')?.value ?? '.'\n } catch {\n return '.'\n }\n}\n\nconst normalizeDecimalSeparator = (value: string, decimalSeparator: string): string => {\n if (decimalSeparator === '.') return value\n return value.replaceAll(decimalSeparator, '.')\n}\n\nconst sanitizeNumericValue = (value: string, allowDecimal: boolean, allowNegative: boolean, decimalSeparator: string): string => {\n if (!value) return ''\n\n let sanitized = normalizeDecimalSeparator(value, decimalSeparator).replace(/[^\\d.-]/g, '')\n\n if (!allowNegative) {\n sanitized = sanitized.replace(/-/g, '')\n } else {\n sanitized = sanitized.replace(/(?!^)-/g, '')\n if (sanitized.startsWith('--')) {\n sanitized = `-${sanitized.replace(/-/g, '')}`\n }\n }\n\n if (!allowDecimal) {\n sanitized = sanitized.replace(/\\./g, '')\n } else {\n const firstDecimalIndex = sanitized.indexOf('.')\n if (firstDecimalIndex >= 0) {\n const integerPart = sanitized.slice(0, firstDecimalIndex + 1)\n const fractionalPart = sanitized.slice(firstDecimalIndex + 1).replace(/\\./g, '')\n sanitized = `${integerPart}${fractionalPart}`\n }\n }\n\n return sanitized\n}\n\nconst shouldAllowDecimalInput = (target: HTMLInputElement): boolean => {\n if (target.selectionStart === null || target.selectionEnd === null) {\n return !target.value.includes('.')\n }\n\n const selectedText = target.value.slice(target.selectionStart, target.selectionEnd)\n return !target.value.includes('.') || selectedText.includes('.')\n}\n\nconst shouldAllowNegativeInput = (target: HTMLInputElement): boolean => {\n if (target.selectionStart === null || target.selectionEnd === null) {\n return target.value.length === 0\n }\n\n const isAtStart = target.selectionStart === 0\n const selectedText = target.value.slice(target.selectionStart, target.selectionEnd)\n return isAtStart && (!target.value.includes('-') || selectedText.includes('-'))\n}\n\nconst parseNumericProp = (value: number | string | undefined): number | undefined => {\n if (value === undefined || value === null || value === '') return undefined\n const parsed = typeof value === 'number' ? value : Number.parseFloat(value)\n return Number.isFinite(parsed) ? parsed : undefined\n}\n\nconst getDecimalPlaces = (value: number): number => {\n const valueString = value.toString()\n if (!valueString.includes('.')) return 0\n return valueString.split('.')[1]?.length ?? 0\n}\n\nconst roundToPrecision = (value: number, precision: number): number => {\n if (precision <= 0) return value\n const factor = 10 ** precision\n return Math.round(value * factor) / factor\n}\n\nexport type InputNumericProps = Omit<InputProps, 'inputMode' | 'onChange' | 'pattern' | 'type'> & {\n allowDecimal?: boolean\n allowNegative?: boolean\n locale?: string\n /** Number of message lines to reserve (default: 1 via Input). */\n messageReserveLines?: number\n /** Whether to keep message space reserved when hidden (default: true via Input). */\n messageReserveSpace?: boolean\n onChange?: (value: string) => void\n value?: string\n}\n\nexport const InputNumeric = ({ allowDecimal = true, allowNegative = false, className, defaultValue = '', locale, max, min, onChange, onKeyDown, onPaste, step, value: valueProp, ...props }: InputNumericProps): ReactElement => {\n const normalizedDefaultValue = typeof defaultValue === 'string' ? defaultValue : defaultValue !== undefined && defaultValue !== null ? String(defaultValue) : ''\n const [value, setValue] = useUncontrolledState<string>({\n value: valueProp,\n defaultValue: normalizedDefaultValue,\n onChange,\n })\n const decimalSeparator = getDecimalSeparator(locale)\n const parsedMin = parseNumericProp(min)\n const parsedMax = parseNumericProp(max)\n const parsedStep = parseNumericProp(step)\n const stepValue = parsedStep && parsedStep > 0 ? parsedStep : 1\n const effectiveMin = parsedMin ?? (!allowNegative ? 0 : undefined)\n\n const handleKeyDown = useCallback(\n (event: NumericKeyDownEvent): void => {\n onKeyDown?.(event)\n if (event.defaultPrevented || event.nativeEvent.isComposing) return\n\n if (event.metaKey || event.ctrlKey || event.altKey) return\n if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {\n event.preventDefault()\n\n const direction = event.key === 'ArrowUp' ? 1 : -1\n const normalizedValue = normalizeDecimalSeparator(value, decimalSeparator)\n const currentValue = Number.parseFloat(normalizedValue)\n const hasCurrentValue = Number.isFinite(currentValue)\n const baseValue = hasCurrentValue ? currentValue : (effectiveMin ?? 0)\n\n let nextValue = baseValue + direction * stepValue\n if (effectiveMin !== undefined) {\n nextValue = Math.max(effectiveMin, nextValue)\n }\n if (parsedMax !== undefined) {\n nextValue = Math.min(parsedMax, nextValue)\n }\n\n if (!allowDecimal) {\n nextValue = Math.trunc(nextValue)\n }\n\n const precision = allowDecimal ? getDecimalPlaces(stepValue) : 0\n const roundedValue = roundToPrecision(nextValue, precision)\n const safeValue = Object.is(roundedValue, -0) ? 0 : roundedValue\n const nextString = precision > 0 ? safeValue.toFixed(precision).replace(/\\.?0+$/, '') : safeValue.toString()\n setValue(sanitizeNumericValue(nextString, allowDecimal, allowNegative, decimalSeparator))\n return\n }\n\n if (ALLOWED_CONTROL_KEYS.has(event.key)) return\n if (DIGIT_REGEX.test(event.key)) return\n\n if (DISALLOWED_SPECIAL_KEYS.has(event.key)) {\n event.preventDefault()\n return\n }\n\n const isDecimalKey = event.key === '.' || event.key === decimalSeparator\n\n if (isDecimalKey) {\n if (!allowDecimal || !shouldAllowDecimalInput(event.currentTarget)) {\n event.preventDefault()\n }\n return\n }\n\n if (event.key === '-') {\n if (!allowNegative || !shouldAllowNegativeInput(event.currentTarget)) {\n event.preventDefault()\n }\n return\n }\n\n event.preventDefault()\n },\n [allowDecimal, allowNegative, decimalSeparator, effectiveMin, onKeyDown, parsedMax, setValue, stepValue, value],\n )\n\n const handlePaste = useCallback(\n (event: NumericPasteEvent): void => {\n onPaste?.(event)\n },\n [onPaste],\n )\n\n const handleChange = useCallback(\n (newValue: string): void => {\n setValue(sanitizeNumericValue(newValue, allowDecimal, allowNegative, decimalSeparator))\n },\n [allowDecimal, allowNegative, decimalSeparator, setValue],\n )\n\n const inputMode = allowDecimal ? 'decimal' : 'numeric'\n const pattern = allowDecimal ? '[0-9]*[.]?[0-9]*' : '[0-9]*'\n const normalizedValue = normalizeDecimalSeparator(value, decimalSeparator)\n const ariaValueNow = normalizedValue === '' || normalizedValue === '-' || normalizedValue === '.' ? undefined : Number.parseFloat(normalizedValue)\n\n return (\n <Input\n {...props}\n aria-valuemax={parsedMax}\n aria-valuemin={effectiveMin}\n aria-valuenow={Number.isFinite(ariaValueNow) ? ariaValueNow : undefined}\n className={cn('tabular-nums', className)}\n inputMode={inputMode}\n max={max}\n min={min}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n onPaste={handlePaste}\n pattern={pattern}\n role='spinbutton'\n step={step}\n type='text'\n value={value}\n />\n )\n}\n\nInputNumeric.displayName = 'InputNumeric'\n"],"mappings":"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"}
@@ -10,6 +10,8 @@ interface InputOTPBaseProps extends Omit<OTPInputProps, 'textAlign' | 'pushPassw
10
10
  className?: string;
11
11
  errorMessage?: string | undefined;
12
12
  inputMode?: 'numeric' | 'text' | 'decimal' | 'tel' | 'search' | 'email' | 'url';
13
+ messageReserveLines?: number;
14
+ messageReserveSpace?: boolean;
13
15
  /**
14
16
  * Regex pattern string to restrict allowed characters.
15
17
  * When `inputMode="numeric"`, defaults to digits-only pattern.
@@ -40,6 +42,8 @@ declare const InputOTP: {
40
42
  errorMessage,
41
43
  id,
42
44
  inputMode,
45
+ messageReserveLines,
46
+ messageReserveSpace,
43
47
  maxLength,
44
48
  name,
45
49
  onChange,
@@ -1 +1 @@
1
- {"version":3,"file":"InputOTP.d.ts","names":[],"sources":["../src/components/InputOTP/InputOTP.tsx"],"mappings":";;;;;;;UAMiB,iBAAA,SAA0B,IAAA,CAAK,aAAA;EAC9C,UAAA,OAAiB,IAAA;EACjB,SAAA;EACA,YAAA;EACA,SAAA;;;;;;EAMA,OAAA;EACA,SAAA;EACA,KAAA,GAAQ,cAAA;EACR,OAAA;AAAA;AAAA,KAGU,aAAA,GAAgB,iBAAA;EAAuB,KAAA;EAAwB,QAAA,GAAW,QAAA;AAAA;EAAyC,KAAA;EAAe,QAAA;AAAA;AAAA,UAwJpI,UAAA;EACR,SAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,cA0DW,QAAA;EAAA;;;;;;;;;;;;;;;;;KAlLV,aAAA;IACD,GAAA,GAAM,GAAA,CAAI,YAAA,QAAoB,QAAA;EAAA,IAC/B,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;OAqEE,wBAAA;MACD,GAAA,GAAM,GAAA,CAAI,YAAA;IAAA,IACX,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;;;OAQE,wBAAA;MACD,KAAA;MACA,GAAA,GAAM,GAAA,CAAI,YAAA;IAAA,IACX,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;;OA+D+C,UAAA,GAAU,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;OAkBvD,wBAAA;MACD,GAAA,GAAM,GAAA,CAAI,YAAA;IAAA,IACX,oBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"InputOTP.d.ts","names":[],"sources":["../src/components/InputOTP/InputOTP.tsx"],"mappings":";;;;;;;UAMiB,iBAAA,SAA0B,IAAA,CAAK,aAAA;EAC9C,UAAA,OAAiB,IAAA;EACjB,SAAA;EACA,YAAA;EACA,SAAA;EACA,mBAAA;EACA,mBAAA;EAQQ;;;;;EAFR,OAAA;EACA,SAAA;EACA,KAAA,GAAQ,cAAA;EACR,OAAA;AAAA;AAAA,KAGU,aAAA,GAAgB,iBAAA;EAAuB,KAAA;EAAwB,QAAA,GAAW,QAAA;AAAA;EAAyC,KAAA;EAAe,QAAA;AAAA;AAAA,UA0JpI,UAAA;EACR,SAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,cA0DW,QAAA;EAAA;;;;;;;;;;;;;;;;;;;KAlLV,aAAA;IACD,GAAA,GAAM,GAAA,CAAI,YAAA,QAAoB,QAAA;EAAA,IAC/B,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;OAqEE,wBAAA;MACD,GAAA,GAAM,GAAA,CAAI,YAAA;IAAA,IACX,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;;;OAQE,wBAAA;MACD,KAAA;MACA,GAAA,GAAM,GAAA,CAAI,YAAA;IAAA,IACX,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;;OA+D+C,UAAA,GAAU,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;OAkBvD,wBAAA;MACD,GAAA,GAAM,GAAA,CAAI,YAAA;IAAA,IACX,oBAAA,CAAA,GAAA,CAAA,OAAA"}
package/dist/InputOTP.js CHANGED
@@ -15,7 +15,7 @@ const useRoot = () => {
15
15
  if (!context) throw new Error("useRoot must be used within an InputOTP");
16
16
  return context;
17
17
  };
18
- const Root = ({ autoFocus = false, children, className, errorMessage, id, inputMode = "numeric", maxLength, name, onChange, onComplete, pattern, ref, state = "default", value, variant = "outlined", ...props }) => {
18
+ const Root = ({ autoFocus = false, children, className, errorMessage, id, inputMode = "numeric", messageReserveLines = 1, messageReserveSpace = true, maxLength, name, onChange, onComplete, pattern, ref, state = "default", value, variant = "outlined", ...props }) => {
19
19
  const inputId = useFormFieldId(id, name);
20
20
  const errorMessageId = getErrorMessageId(inputId);
21
21
  const { isInvalid } = useFormFieldState(false, state);
@@ -64,7 +64,9 @@ const Root = ({ autoFocus = false, children, className, errorMessage, id, inputM
64
64
  }), /* @__PURE__ */ jsx(ErrorMessage, {
65
65
  dataTestId: "spectral-input-otp-error-message",
66
66
  id: errorMessageId,
67
- message: isInvalid ? errorMessage : null
67
+ message: isInvalid ? errorMessage : null,
68
+ messageReserveLines,
69
+ messageReserveSpace
68
70
  })]
69
71
  })
70
72
  });
@@ -1 +1 @@
1
- {"version":3,"file":"InputOTP.js","names":[],"sources":["../src/components/InputOTP/InputOTP.tsx"],"sourcesContent":["import { MinusIcon } from '@components/Icons'\nimport { ErrorMessage, getErrorMessageId, useFormFieldId, useFormFieldState, type FormFieldState } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { OTPInput, REGEXP_ONLY_DIGITS, type OTPInputProps } from 'input-otp'\nimport { createContext, useContext, type ClipboardEvent, type ComponentPropsWithoutRef, type ComponentRef, type Ref } from 'react'\n\nexport interface InputOTPBaseProps extends Omit<OTPInputProps, 'textAlign' | 'pushPasswordManagerStrategy' | 'pasteTransformer' | 'noScriptCSSFallback' | 'placeholder' | 'containerClassName' | 'render' | 'pattern'> {\n onComplete?: (...args: unknown[]) => void\n className?: string\n errorMessage?: string | undefined\n inputMode?: 'numeric' | 'text' | 'decimal' | 'tel' | 'search' | 'email' | 'url'\n /**\n * Regex pattern string to restrict allowed characters.\n * When `inputMode=\"numeric\"`, defaults to digits-only pattern.\n * Set to `undefined` to allow any characters.\n */\n pattern?: string | undefined\n separator?: boolean\n state?: FormFieldState\n variant?: 'outlined' | 'filled'\n}\n\nexport type InputOTPProps = InputOTPBaseProps & ({ value: number | string; onChange: (newValue: number | string) => void } | { value?: never; onChange?: never })\n\nconst InputOTPStateContext = createContext<{ isInvalid?: boolean }>({})\n\nconst OTPInputContext = createContext<{\n maxLength?: number\n slots?: { char: string | null; hasFakeCaret: boolean; isActive: boolean }[]\n variant?: 'outlined' | 'filled'\n} | null>(null)\n\nconst useRoot = () => {\n const context = useContext(OTPInputContext)\n if (!context) {\n throw new Error('useRoot must be used within an InputOTP')\n }\n return context\n}\n\nconst Root = ({\n autoFocus = false,\n children,\n className,\n errorMessage,\n id,\n inputMode = 'numeric',\n maxLength,\n name,\n onChange,\n onComplete,\n pattern,\n ref,\n state = 'default',\n value,\n variant = 'outlined',\n ...props\n}: InputOTPProps & {\n ref?: Ref<ComponentRef<typeof OTPInput>>\n}) => {\n const inputId = useFormFieldId(id, name)\n const errorMessageId = getErrorMessageId(inputId)\n const { isInvalid } = useFormFieldState(false, state)\n\n // Apply digits-only pattern when inputMode is numeric (unless explicitly overridden)\n const effectivePattern = pattern ?? (inputMode === 'numeric' ? REGEXP_ONLY_DIGITS : undefined)\n\n const handlePaste = (e: ClipboardEvent<HTMLDivElement>): void => {\n let pasteData = e.clipboardData.getData('text/plain').trim().replaceAll('-', '')\n\n // Filter to digits only when in numeric mode\n if (inputMode === 'numeric') {\n pasteData = pasteData.replace(/\\D/g, '')\n }\n\n if (pasteData.length === maxLength && typeof onChange === 'function') {\n onChange(pasteData)\n }\n }\n\n return (\n <InputOTPStateContext.Provider value={{ isInvalid }}>\n <div className='gap-y-1 flex w-max flex-col'>\n <OTPInput\n /* eslint-disable-next-line jsx-a11y/no-autofocus -- intentional: consumers can opt in for OTP-first flows; defaults to false */\n autoFocus={autoFocus}\n containerClassName={cn('gap-2 flex items-center disabled:cursor-not-allowed has-[disabled]:opacity-50', className)}\n data-1p-ignore='true'\n data-dashlane-disabled-on-field='true'\n data-lpignore='true'\n data-protonpass-ignore='true'\n data-testid='spectral-input-otp'\n id={inputId}\n inputMode={inputMode}\n maxLength={maxLength}\n onChange={onChange}\n onComplete={onComplete}\n onPaste={handlePaste}\n pasteTransformer={(pasted) => pasted.replaceAll('-', '')}\n pattern={effectivePattern}\n pushPasswordManagerStrategy='none'\n ref={ref}\n aria-describedby={isInvalid && errorMessage ? errorMessageId : undefined}\n aria-invalid={isInvalid}\n role='textbox'\n textAlign='center'\n value={value}\n {...props}\n render={({ slots }) => (\n <OTPInputContext.Provider value={{ slots, variant, maxLength }}>\n {children ?? (\n <Group>\n <Slots />\n </Group>\n )}\n </OTPInputContext.Provider>\n )}\n />\n <ErrorMessage dataTestId='spectral-input-otp-error-message' id={errorMessageId} message={isInvalid ? errorMessage : null} />\n </div>\n </InputOTPStateContext.Provider>\n )\n}\nRoot.displayName = 'InputOTP'\n\nconst Group = ({\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n ref?: Ref<ComponentRef<'div'>>\n}) => <div className='gap-x-2 flex items-center justify-center' data-testid='spectral-input-otp-group' ref={ref} {...props} />\nGroup.displayName = 'InputOTP.Group'\n\nconst Slot = ({\n className,\n index,\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n index: number\n ref?: Ref<ComponentRef<'div'>>\n}) => {\n const { variant = 'outlined', slots = [] } = useRoot()\n const { isInvalid } = useContext(InputOTPStateContext)\n const slot = slots[index] || { char: '', hasFakeCaret: true, isActive: false }\n\n return (\n <div\n className={cn(\n 'h-12 w-10 relative z-10 flex items-center justify-center rounded-[8px] border tabular-nums transition duration-200 focus:outline-none',\n variant === 'filled' ? 'border-level-one bg-level-one' : 'border-input-otp-border bg-transparent',\n !isInvalid && 'border',\n isInvalid && 'border-2 border-danger-400',\n slot.isActive && !isInvalid && 'z-10 border-input-otp-border--focus',\n slot.isActive && isInvalid && 'z-10 border-danger-400 focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-danger-400',\n className,\n )}\n data-index={index}\n data-testid='spectral-input-otp-slot'\n data-variant={variant}\n ref={ref}\n {...props}\n >\n {slot.char}\n {slot.hasFakeCaret && (\n <div className='inset-0 pointer-events-none absolute flex items-center justify-center motion-safe:animate-caret-blink'>\n <div className='h-8 w-px bg-input-otp-caret' />\n </div>\n )}\n </div>\n )\n}\nSlot.displayName = 'InputOTP.Slot'\n\ninterface SlotsProps {\n className?: string\n count?: number\n start?: number\n}\n\n/**\n * Helper component that automatically renders multiple InputOTP.Slot components.\n * Uses the maxLength from the parent InputOTP to determine how many slots to render.\n *\n * @example\n * // Render all 6 slots\n * <InputOTP maxLength={6}>\n * <InputOTP.Group>\n * <InputOTP.Slots />\n * </InputOTP.Group>\n * </InputOTP>\n *\n * @example\n * // Render slots in groups with a separator (3-3 split)\n * <InputOTP maxLength={6}>\n * <InputOTP.Group>\n * <InputOTP.Slots count={3} />\n * </InputOTP.Group>\n * <InputOTP.Separator />\n * <InputOTP.Group>\n * <InputOTP.Slots start={3} />\n * </InputOTP.Group>\n * </InputOTP>\n */\nconst Slots = ({ start = 0, count, className }: SlotsProps) => {\n const { maxLength = 0 } = useRoot()\n const end = count !== undefined ? start + count : maxLength\n const indices = Array.from({ length: end - start }, (_, i) => start + i)\n\n return (\n <>\n {indices.map((index) => (\n <Slot key={index} index={index} className={className} />\n ))}\n </>\n )\n}\nSlots.displayName = 'InputOTP.Slots'\n\nconst Separator = ({\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n ref?: Ref<ComponentRef<'div'>>\n}) => {\n const { variant = 'outlined' } = useRoot()\n\n return (\n <div ref={ref} role='separator' {...props} data-testid='spectral-input-otp-separator' data-variant={variant}>\n <MinusIcon size={24} color={variant === 'filled' ? 'var(--color-input-otp-filled-separator)' : 'var(--color-input-otp-border)'} />\n </div>\n )\n}\nSeparator.displayName = 'InputOTP.Separator'\n\nexport const InputOTP = Object.assign(Root, {\n Group,\n Slot,\n Slots,\n Separator,\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"InputOTP.js","names":[],"sources":["../src/components/InputOTP/InputOTP.tsx"],"sourcesContent":["import { MinusIcon } from '@components/Icons'\nimport { ErrorMessage, getErrorMessageId, useFormFieldId, useFormFieldState, type FormFieldState } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { OTPInput, REGEXP_ONLY_DIGITS, type OTPInputProps } from 'input-otp'\nimport { createContext, useContext, type ClipboardEvent, type ComponentPropsWithoutRef, type ComponentRef, type Ref } from 'react'\n\nexport interface InputOTPBaseProps extends Omit<OTPInputProps, 'textAlign' | 'pushPasswordManagerStrategy' | 'pasteTransformer' | 'noScriptCSSFallback' | 'placeholder' | 'containerClassName' | 'render' | 'pattern'> {\n onComplete?: (...args: unknown[]) => void\n className?: string\n errorMessage?: string | undefined\n inputMode?: 'numeric' | 'text' | 'decimal' | 'tel' | 'search' | 'email' | 'url'\n messageReserveLines?: number\n messageReserveSpace?: boolean\n /**\n * Regex pattern string to restrict allowed characters.\n * When `inputMode=\"numeric\"`, defaults to digits-only pattern.\n * Set to `undefined` to allow any characters.\n */\n pattern?: string | undefined\n separator?: boolean\n state?: FormFieldState\n variant?: 'outlined' | 'filled'\n}\n\nexport type InputOTPProps = InputOTPBaseProps & ({ value: number | string; onChange: (newValue: number | string) => void } | { value?: never; onChange?: never })\n\nconst InputOTPStateContext = createContext<{ isInvalid?: boolean }>({})\n\nconst OTPInputContext = createContext<{\n maxLength?: number\n slots?: { char: string | null; hasFakeCaret: boolean; isActive: boolean }[]\n variant?: 'outlined' | 'filled'\n} | null>(null)\n\nconst useRoot = () => {\n const context = useContext(OTPInputContext)\n if (!context) {\n throw new Error('useRoot must be used within an InputOTP')\n }\n return context\n}\n\nconst Root = ({\n autoFocus = false,\n children,\n className,\n errorMessage,\n id,\n inputMode = 'numeric',\n messageReserveLines = 1,\n messageReserveSpace = true,\n maxLength,\n name,\n onChange,\n onComplete,\n pattern,\n ref,\n state = 'default',\n value,\n variant = 'outlined',\n ...props\n}: InputOTPProps & {\n ref?: Ref<ComponentRef<typeof OTPInput>>\n}) => {\n const inputId = useFormFieldId(id, name)\n const errorMessageId = getErrorMessageId(inputId)\n const { isInvalid } = useFormFieldState(false, state)\n\n // Apply digits-only pattern when inputMode is numeric (unless explicitly overridden)\n const effectivePattern = pattern ?? (inputMode === 'numeric' ? REGEXP_ONLY_DIGITS : undefined)\n\n const handlePaste = (e: ClipboardEvent<HTMLDivElement>): void => {\n let pasteData = e.clipboardData.getData('text/plain').trim().replaceAll('-', '')\n\n // Filter to digits only when in numeric mode\n if (inputMode === 'numeric') {\n pasteData = pasteData.replace(/\\D/g, '')\n }\n\n if (pasteData.length === maxLength && typeof onChange === 'function') {\n onChange(pasteData)\n }\n }\n\n return (\n <InputOTPStateContext.Provider value={{ isInvalid }}>\n <div className='gap-y-1 flex w-max flex-col'>\n <OTPInput\n /* eslint-disable-next-line jsx-a11y/no-autofocus -- intentional: consumers can opt in for OTP-first flows; defaults to false */\n autoFocus={autoFocus}\n containerClassName={cn('gap-2 flex items-center disabled:cursor-not-allowed has-[disabled]:opacity-50', className)}\n data-1p-ignore='true'\n data-dashlane-disabled-on-field='true'\n data-lpignore='true'\n data-protonpass-ignore='true'\n data-testid='spectral-input-otp'\n id={inputId}\n inputMode={inputMode}\n maxLength={maxLength}\n onChange={onChange}\n onComplete={onComplete}\n onPaste={handlePaste}\n pasteTransformer={(pasted) => pasted.replaceAll('-', '')}\n pattern={effectivePattern}\n pushPasswordManagerStrategy='none'\n ref={ref}\n aria-describedby={isInvalid && errorMessage ? errorMessageId : undefined}\n aria-invalid={isInvalid}\n role='textbox'\n textAlign='center'\n value={value}\n {...props}\n render={({ slots }) => (\n <OTPInputContext.Provider value={{ slots, variant, maxLength }}>\n {children ?? (\n <Group>\n <Slots />\n </Group>\n )}\n </OTPInputContext.Provider>\n )}\n />\n <ErrorMessage dataTestId='spectral-input-otp-error-message' id={errorMessageId} message={isInvalid ? errorMessage : null} messageReserveLines={messageReserveLines} messageReserveSpace={messageReserveSpace} />\n </div>\n </InputOTPStateContext.Provider>\n )\n}\nRoot.displayName = 'InputOTP'\n\nconst Group = ({\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n ref?: Ref<ComponentRef<'div'>>\n}) => <div className='gap-x-2 flex items-center justify-center' data-testid='spectral-input-otp-group' ref={ref} {...props} />\nGroup.displayName = 'InputOTP.Group'\n\nconst Slot = ({\n className,\n index,\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n index: number\n ref?: Ref<ComponentRef<'div'>>\n}) => {\n const { variant = 'outlined', slots = [] } = useRoot()\n const { isInvalid } = useContext(InputOTPStateContext)\n const slot = slots[index] || { char: '', hasFakeCaret: true, isActive: false }\n\n return (\n <div\n className={cn(\n 'h-12 w-10 relative z-10 flex items-center justify-center rounded-[8px] border tabular-nums transition duration-200 focus:outline-none',\n variant === 'filled' ? 'border-level-one bg-level-one' : 'border-input-otp-border bg-transparent',\n !isInvalid && 'border',\n isInvalid && 'border-2 border-danger-400',\n slot.isActive && !isInvalid && 'z-10 border-input-otp-border--focus',\n slot.isActive && isInvalid && 'z-10 border-danger-400 focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-danger-400',\n className,\n )}\n data-index={index}\n data-testid='spectral-input-otp-slot'\n data-variant={variant}\n ref={ref}\n {...props}\n >\n {slot.char}\n {slot.hasFakeCaret && (\n <div className='inset-0 pointer-events-none absolute flex items-center justify-center motion-safe:animate-caret-blink'>\n <div className='h-8 w-px bg-input-otp-caret' />\n </div>\n )}\n </div>\n )\n}\nSlot.displayName = 'InputOTP.Slot'\n\ninterface SlotsProps {\n className?: string\n count?: number\n start?: number\n}\n\n/**\n * Helper component that automatically renders multiple InputOTP.Slot components.\n * Uses the maxLength from the parent InputOTP to determine how many slots to render.\n *\n * @example\n * // Render all 6 slots\n * <InputOTP maxLength={6}>\n * <InputOTP.Group>\n * <InputOTP.Slots />\n * </InputOTP.Group>\n * </InputOTP>\n *\n * @example\n * // Render slots in groups with a separator (3-3 split)\n * <InputOTP maxLength={6}>\n * <InputOTP.Group>\n * <InputOTP.Slots count={3} />\n * </InputOTP.Group>\n * <InputOTP.Separator />\n * <InputOTP.Group>\n * <InputOTP.Slots start={3} />\n * </InputOTP.Group>\n * </InputOTP>\n */\nconst Slots = ({ start = 0, count, className }: SlotsProps) => {\n const { maxLength = 0 } = useRoot()\n const end = count !== undefined ? start + count : maxLength\n const indices = Array.from({ length: end - start }, (_, i) => start + i)\n\n return (\n <>\n {indices.map((index) => (\n <Slot key={index} index={index} className={className} />\n ))}\n </>\n )\n}\nSlots.displayName = 'InputOTP.Slots'\n\nconst Separator = ({\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n ref?: Ref<ComponentRef<'div'>>\n}) => {\n const { variant = 'outlined' } = useRoot()\n\n return (\n <div ref={ref} role='separator' {...props} data-testid='spectral-input-otp-separator' data-variant={variant}>\n <MinusIcon size={24} color={variant === 'filled' ? 'var(--color-input-otp-filled-separator)' : 'var(--color-input-otp-border)'} />\n </div>\n )\n}\nSeparator.displayName = 'InputOTP.Separator'\n\nexport const InputOTP = Object.assign(Root, {\n Group,\n Slot,\n Slots,\n Separator,\n})\n"],"mappings":"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"}
@@ -21,6 +21,8 @@ interface MultiSelectBaseProps extends Omit<ButtonHTMLAttributes<HTMLButtonEleme
21
21
  label?: string;
22
22
  loadingMessage?: string;
23
23
  maxCount?: number;
24
+ messageReserveLines?: number;
25
+ messageReserveSpace?: boolean;
24
26
  name?: string;
25
27
  defaultValue?: string[];
26
28
  onChange?: (value: string[]) => void;
@@ -51,6 +53,8 @@ declare function MultiSelectBase({
51
53
  id,
52
54
  label,
53
55
  loadingMessage,
56
+ messageReserveLines,
57
+ messageReserveSpace,
54
58
  maxCount,
55
59
  name,
56
60
  onChange,
@@ -1 +1 @@
1
- {"version":3,"file":"MultiSelectBase.d.ts","names":[],"sources":["../../src/components/MultiSelect/MultiSelectBase.tsx"],"mappings":";;;;;;KASY,gBAAA,GAAmB,OAAA,CAAQ,cAAA;AAAA,UAEtB,iBAAA;EACf,QAAA;EACA,KAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,oBAAA,SAA6B,IAAA,CAAK,oBAAA,CAAqB,iBAAA;EACtE,aAAA;EACA,aAAA;EACA,aAAA,GAAgB,aAAA;EAChB,YAAA;EACA,YAAA,GAAe,kBAAA;EACf,EAAA;EACA,KAAA;EACA,cAAA;EACA,QAAA;EACA,IAAA;EACA,YAAA;EACA,QAAA,IAAY,KAAA;EACZ,OAAA,EAAS,iBAAA;EACT,WAAA;EACA,QAAA;EACA,iBAAA;EACA,YAAA;EACA,UAAA;EACA,aAAA;EACA,cAAA;EACA,kBAAA;EACA,KAAA,GAAQ,gBAAA;EACR,KAAA;EACA,cAAA,GAAiB,kBAAA;EACjB,YAAA;EACA,kBAAA;AAAA;AAAA;EA6MA,SAAA;EACA,aAAA;EACA,aAAA;EACA,aAAA;EACA,YAAA;EACA,YAAA;EACA,YAAA;EACA,QAAA;EACA,EAAA;EACA,KAAA;EACA,cAAA;EACA,QAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,WAAA;EACA,GAAA;EACA,iBAAA;EACA,cAAA;EACA,YAAA;EACA,UAAA;EACA,aAAA;EACA,kBAAA;EACA,KAAA;EACA,KAAA,EAAO,SAAA;EACP,cAAA;EAAA,cACc,SAAA;EAAA,oBACM,eAAA;EAAA,GACjB;AAAA,GACF,oBAAA;EACD,GAAA,GAAM,GAAA,CAAI,iBAAA;AAAA,IACX,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA"}
1
+ {"version":3,"file":"MultiSelectBase.d.ts","names":[],"sources":["../../src/components/MultiSelect/MultiSelectBase.tsx"],"mappings":";;;;;;KASY,gBAAA,GAAmB,OAAA,CAAQ,cAAA;AAAA,UAEtB,iBAAA;EACf,QAAA;EACA,KAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,oBAAA,SAA6B,IAAA,CAAK,oBAAA,CAAqB,iBAAA;EACtE,aAAA;EACA,aAAA;EACA,aAAA,GAAgB,aAAA;EAChB,YAAA;EACA,YAAA,GAAe,kBAAA;EACf,EAAA;EACA,KAAA;EACA,cAAA;EACA,QAAA;EACA,mBAAA;EACA,mBAAA;EACA,IAAA;EACA,YAAA;EACA,QAAA,IAAY,KAAA;EACZ,OAAA,EAAS,iBAAA;EACT,WAAA;EACA,QAAA;EACA,iBAAA;EACA,YAAA;EACA,UAAA;EACA,aAAA;EACA,cAAA;EACA,kBAAA;EACA,KAAA,GAAQ,gBAAA;EACR,KAAA;EACA,cAAA,GAAiB,kBAAA;EACjB,YAAA;EACA,kBAAA;AAAA;AAAA;EA6MA,SAAA;EACA,aAAA;EACA,aAAA;EACA,aAAA;EACA,YAAA;EACA,YAAA;EACA,YAAA;EACA,QAAA;EACA,EAAA;EACA,KAAA;EACA,cAAA;EACA,mBAAA;EACA,mBAAA;EACA,QAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,WAAA;EACA,GAAA;EACA,iBAAA;EACA,cAAA;EACA,YAAA;EACA,UAAA;EACA,aAAA;EACA,kBAAA;EACA,KAAA;EACA,KAAA,EAAO,SAAA;EACP,cAAA;EAAA,cACc,SAAA;EAAA,oBACM,eAAA;EAAA,GACjB;AAAA,GACF,oBAAA;EACD,GAAA,GAAM,GAAA,CAAI,iBAAA;AAAA,IACX,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA"}
@@ -136,7 +136,7 @@ const useKeyboardNavigation = (options, onClearAll, onClose, onSelect, onSelectA
136
136
  }, [focusedIndex, focusableItems])
137
137
  };
138
138
  };
139
- const MultiSelectBase = ({ className, clearAllLabel = "Clear all", closeOnSelect = false, dropdownWidth = "trigger", emptyMessage = "No options found", errorMessage, defaultValue = [], disabled, id, label, loadingMessage = "Loading options…", maxCount = 3, name, onChange, options = [], placeholder = "Select options", ref, searchPlaceholder = "Search options…", selectAllLabel = "Select all", showClearAll = true, showSearch = true, showSelectAll = true, sortAlphabetically = false, state = "default", value: valueProp, warningMessage, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...props }) => {
139
+ const MultiSelectBase = ({ className, clearAllLabel = "Clear all", closeOnSelect = false, dropdownWidth = "trigger", emptyMessage = "No options found", errorMessage, defaultValue = [], disabled, id, label, loadingMessage = "Loading options…", messageReserveLines = 1, messageReserveSpace = true, maxCount = 3, name, onChange, options = [], placeholder = "Select options", ref, searchPlaceholder = "Search options…", selectAllLabel = "Select all", showClearAll = true, showSearch = true, showSelectAll = true, sortAlphabetically = false, state = "default", value: valueProp, warningMessage, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...props }) => {
140
140
  const generatedId = useId();
141
141
  const multiSelectId = useFormFieldId(id, name ?? `multiselect-${generatedId}`);
142
142
  const listboxId = `${multiSelectId}-listbox`;
@@ -430,12 +430,16 @@ const MultiSelectBase = ({ className, clearAllLabel = "Clear all", closeOnSelect
430
430
  /* @__PURE__ */ jsx(ErrorMessage, {
431
431
  dataTestId: "spectral-multiselect-error-message",
432
432
  id: errorMessageId,
433
- message: state === "error" ? errorMessage : null
433
+ message: state === "error" ? errorMessage : null,
434
+ messageReserveLines,
435
+ messageReserveSpace: messageReserveSpace && state === "error"
434
436
  }),
435
437
  /* @__PURE__ */ jsx(WarningMessage, {
436
438
  dataTestId: "spectral-multiselect-warning-message",
437
439
  id: warningMessageId,
438
- message: state === "warning" ? warningMessage : null
440
+ message: state === "warning" ? warningMessage : null,
441
+ messageReserveLines,
442
+ messageReserveSpace: messageReserveSpace && state === "warning"
439
443
  })
440
444
  ]
441
445
  });