@spear-ai/spectral 1.20.2 → 1.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Checkbox.js +3 -3
- package/dist/Checkbox.js.map +1 -1
- package/dist/Combobox.d.ts.map +1 -1
- package/dist/Combobox.js +2 -2
- package/dist/Combobox.js.map +1 -1
- package/dist/DateTimePicker/Calendar.d.ts +13 -1
- package/dist/DateTimePicker/Calendar.d.ts.map +1 -1
- package/dist/DateTimePicker/Calendar.js +11 -4
- package/dist/DateTimePicker/Calendar.js.map +1 -1
- package/dist/DateTimePicker/DateTimeDisplayInput.d.ts +1 -1
- package/dist/DateTimePicker/DateTimeDisplayInput.d.ts.map +1 -1
- package/dist/DateTimePicker/DateTimeDisplayInput.js +3 -3
- package/dist/DateTimePicker/DateTimeDisplayInput.js.map +1 -1
- package/dist/DateTimePicker.d.ts +13 -2
- package/dist/DateTimePicker.d.ts.map +1 -1
- package/dist/DateTimePicker.js +9 -5
- package/dist/DateTimePicker.js.map +1 -1
- package/dist/Input.d.ts +1 -1
- package/dist/Input.d.ts.map +1 -1
- package/dist/Input.js +24 -17
- package/dist/Input.js.map +1 -1
- package/dist/InputOTP.d.ts +2 -0
- package/dist/InputOTP.d.ts.map +1 -1
- package/dist/InputOTP.js +52 -40
- package/dist/InputOTP.js.map +1 -1
- package/dist/InputSearch.d.ts.map +1 -1
- package/dist/InputSearch.js +3 -2
- package/dist/InputSearch.js.map +1 -1
- package/dist/MultiSelect/MultiSelectBase.js +5 -4
- package/dist/MultiSelect/MultiSelectBase.js.map +1 -1
- package/dist/RadialMenu.d.ts.map +1 -1
- package/dist/RadialMenu.js.map +1 -1
- package/dist/RadioButtonGroup/RadioButtonGroupBase.d.ts +1 -1
- package/dist/RadioButtonGroup/RadioButtonGroupBase.d.ts.map +1 -1
- package/dist/RadioButtonGroup/RadioButtonGroupBase.js +3 -3
- package/dist/RadioButtonGroup/RadioButtonGroupBase.js.map +1 -1
- package/dist/RadioButtonGroup.d.ts +1 -1
- package/dist/RadioGroup.d.ts +1 -1
- package/dist/RadioGroup.d.ts.map +1 -1
- package/dist/RadioGroup.js +3 -3
- package/dist/RadioGroup.js.map +1 -1
- package/dist/Select.d.ts.map +1 -1
- package/dist/Select.js +4 -4
- package/dist/Select.js.map +1 -1
- package/dist/Slider.js +1 -1
- package/dist/Slider.js.map +1 -1
- package/dist/Switch.d.ts +2 -2
- package/dist/Switch.d.ts.map +1 -1
- package/dist/Switch.js +4 -4
- package/dist/Switch.js.map +1 -1
- package/dist/Textarea.d.ts +3 -3
- package/dist/Textarea.d.ts.map +1 -1
- package/dist/Textarea.js +4 -3
- package/dist/Textarea.js.map +1 -1
- package/dist/Toast.d.ts +2 -0
- package/dist/Toast.d.ts.map +1 -1
- package/dist/Toast.js +8 -5
- package/dist/Toast.js.map +1 -1
- package/dist/primitives/input.d.ts.map +1 -1
- package/dist/primitives/input.js +2 -0
- package/dist/primitives/input.js.map +1 -1
- package/dist/primitives/textarea.d.ts.map +1 -1
- package/dist/primitives/textarea.js +2 -0
- package/dist/primitives/textarea.js.map +1 -1
- package/dist/styles/spectral.css +1 -1
- package/dist/utils/formFieldUtils.d.ts +9 -1
- package/dist/utils/formFieldUtils.d.ts.map +1 -1
- package/dist/utils/formFieldUtils.js +19 -2
- package/dist/utils/formFieldUtils.js.map +1 -1
- package/package.json +1 -1
|
@@ -56,11 +56,19 @@ declare const getInputClasses: (state: FormFieldState, className?: string) => st
|
|
|
56
56
|
declare const getTextareaClasses: (state: FormFieldState, className?: string) => string;
|
|
57
57
|
declare const useFormFieldId: (id?: string, name?: string) => string;
|
|
58
58
|
declare const getErrorMessageId: (fieldId: string) => string;
|
|
59
|
+
declare const getWarningMessageId: (fieldId: string) => string;
|
|
59
60
|
declare const getAriaProps: (state: FormFieldState, ariaDescribedBy?: string, ariaRequired?: boolean, messageId?: string) => {
|
|
60
61
|
'aria-invalid': boolean;
|
|
61
62
|
'aria-required': boolean | undefined;
|
|
62
63
|
'aria-describedby': string | undefined;
|
|
63
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* Opt-out attributes that prevent password managers (LastPass, 1Password,
|
|
67
|
+
* Dashlane, Bitwarden, Proton Pass) from injecting their icons/overlays.
|
|
68
|
+
* Skipped for credential fields (`password`/`email`) so genuine autofill keeps working.
|
|
69
|
+
* Spread these before `{...props}` so consumers can still override per-field.
|
|
70
|
+
*/
|
|
71
|
+
declare const getPasswordManagerIgnoreProps: (type?: string) => Record<string, string>;
|
|
64
72
|
declare const getFormFieldCSSProperties: (customProps?: Record<string, string>) => {
|
|
65
73
|
'--field-border-radius': string;
|
|
66
74
|
'--field-height': string;
|
|
@@ -100,5 +108,5 @@ declare const EmptyState: ({
|
|
|
100
108
|
declare const getOptionClasses: (isDisabled: boolean | undefined, isFocused: boolean, isSelected: boolean) => string;
|
|
101
109
|
declare const scrollIntoView: (element: HTMLElement | null) => void;
|
|
102
110
|
//#endregion
|
|
103
|
-
export { BaseFormFieldProps, BaseOption, DropdownWidth, DropdownWidthMode, EmptyState, ErrorMessage, type FormFieldMessageValue, FormFieldState, LoadingState, WarningMessage, getAriaProps, getDropdownClasses, getDropdownSurfaceClasses, getDropdownWidthStyles, getErrorMessageId, getFormFieldCSSProperties, getInputClasses, getOptionClasses, getStateClasses, getTextareaClasses, getTriggerClasses, groupOptions, scrollIntoView, useClickOutside, useFormFieldId, useFormFieldState, useKeyboardNavigation };
|
|
111
|
+
export { BaseFormFieldProps, BaseOption, DropdownWidth, DropdownWidthMode, EmptyState, ErrorMessage, type FormFieldMessageValue, FormFieldState, LoadingState, WarningMessage, getAriaProps, getDropdownClasses, getDropdownSurfaceClasses, getDropdownWidthStyles, getErrorMessageId, getFormFieldCSSProperties, getInputClasses, getOptionClasses, getPasswordManagerIgnoreProps, getStateClasses, getTextareaClasses, getTriggerClasses, getWarningMessageId, groupOptions, scrollIntoView, useClickOutside, useFormFieldId, useFormFieldState, useKeyboardNavigation };
|
|
104
112
|
//# sourceMappingURL=formFieldUtils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formFieldUtils.d.ts","names":[],"sources":["../../src/utils/formFieldUtils.tsx"],"mappings":";;;;;;;KAIY,cAAA;AAAA,UAEK,kBAAA;EACf,QAAA;EACA,YAAA,uBAAmC,MAAA;EACnC,EAAA;EACA,KAAA;EACA,mBAAA;EACA,mBAAA;EACA,IAAA;EACA,QAAA,IAAY,KAAA;EACZ,QAAA;EACA,KAAA,GAAQ,cAAA;EACR,kBAAA;EACA,eAAA;EACA,cAAA;EACA,YAAA;EACA,eAAA;AAAA;AAAA,UAGe,UAAA;EACf,QAAA;EACA,KAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,KAGU,aAAA;AAAA,KACA,iBAAA;AAAA,UAEF,wBAAA;EACR,aAAA,EAAe,aAAA;EACf,iBAAA;EACA,YAAA;EACA,gBAAA;AAAA;AAAA,cAGW,sBAAA;EAAsB,aAAA;EAAA,iBAAA;EAAA,YAAA;EAAA;AAAA,GAKhC,wBAAA;EACD,qBAAA,EAAuB,aAAA;EACvB,iBAAA,EAAmB,iBAAA;EACnB,kBAAA,EAAoB,aAAA;EACpB,qBAAA;AAAA;AAAA,cA6BW,eAAA,GAAe,KAAA,EAAW,cAAA;AAAA,cAY1B,iBAAA,GAAiB,MAAA,uBAAmB,KAAA,EAAiB,cAAA,EAAc,SAAA;AAAA,cAWnE,kBAAA,GAAkB,MAAA,WAAmB,QAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"formFieldUtils.d.ts","names":[],"sources":["../../src/utils/formFieldUtils.tsx"],"mappings":";;;;;;;KAIY,cAAA;AAAA,UAEK,kBAAA;EACf,QAAA;EACA,YAAA,uBAAmC,MAAA;EACnC,EAAA;EACA,KAAA;EACA,mBAAA;EACA,mBAAA;EACA,IAAA;EACA,QAAA,IAAY,KAAA;EACZ,QAAA;EACA,KAAA,GAAQ,cAAA;EACR,kBAAA;EACA,eAAA;EACA,cAAA;EACA,YAAA;EACA,eAAA;AAAA;AAAA,UAGe,UAAA;EACf,QAAA;EACA,KAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,KAGU,aAAA;AAAA,KACA,iBAAA;AAAA,UAEF,wBAAA;EACR,aAAA,EAAe,aAAA;EACf,iBAAA;EACA,YAAA;EACA,gBAAA;AAAA;AAAA,cAGW,sBAAA;EAAsB,aAAA;EAAA,iBAAA;EAAA,YAAA;EAAA;AAAA,GAKhC,wBAAA;EACD,qBAAA,EAAuB,aAAA;EACvB,iBAAA,EAAmB,iBAAA;EACnB,kBAAA,EAAoB,aAAA;EACpB,qBAAA;AAAA;AAAA,cA6BW,eAAA,GAAe,KAAA,EAAW,cAAA;AAAA,cAY1B,iBAAA,GAAiB,MAAA,uBAAmB,KAAA,EAAiB,cAAA,EAAc,SAAA;AAAA,cAWnE,kBAAA,GAAkB,MAAA,WAAmB,QAAA;AAAA,cAOrC,yBAAA;AAAA,cAIA,eAAA,GAAe,KAAA,EAAW,cAAA,EAAc,SAAA;AAAA,cAWxC,kBAAA,GAAkB,KAAA,EAAW,cAAA,EAAc,SAAA;AAAA,cAW3C,cAAA,GAAc,EAAA,WAAe,IAAA;AAAA,cAW7B,iBAAA,GAAiB,OAAA;AAAA,cACjB,mBAAA,GAAmB,OAAA;AAAA,cAEnB,YAAA,GAAY,KAAA,EAAW,cAAA,EAAc,eAAA,WAA0B,YAAA,YAAwB,SAAA;EAClG,cAAA;EACA,eAAA;EACA,kBAAA;AAAA;;AAxH8D;;;;;cAiInD,6BAAA,GAA6B,IAAA,cAAoB,MAAA;AAAA,cAWjD,yBAAA,GAAyB,WAAA,GAAiB,MAAA;;;;;cAO1C,iBAAA,GAAiB,QAAA,YAAsB,KAAA,GAAS,cAAA;EAC3D,UAAA;EACA,SAAA;EACA,SAAA;AAAA;AAAA,cAIW,eAAA,aAA6B,WAAA,EAAW,QAAA,iBAAsB,OAAA,CAAA,SAAA,CAAA,CAAA;AAAA,cAiB9D,qBAAA,aAAmC,UAAA,EAAU,MAAA,WAAiB,WAAA,uBAAsB,OAAA,cAA6B,QAAA,GAAa,KAAA,mBAAsB,OAAA,EAAW,CAAA,IAAG,aAAA;;oCAAF,OAAA,CAAA,cAAA;yBAwCjK,aAAA,CAAc,WAAA;;;cA6Db,YAAA,aAA0B,UAAA,EAAU,OAAA,EAAW,CAAA;;;;;cAsB/C,YAAA;EAAY,SAAA;EAAA;AAAA;EAA2C,SAAA;EAAoB,OAAA;AAAA,MAAkB,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,cAM7F,UAAA;EAAU,SAAA;EAAA;AAAA;EAAmD,SAAA;EAAoB,OAAA,GAAU,SAAA;AAAA,MAAW,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,cAMtG,gBAAA,GAAgB,UAAA,uBAAuB,SAAA,WAA4B,UAAA;AAAA,cASnE,cAAA,GAAc,OAAA,EAAa,WAAA"}
|
|
@@ -30,7 +30,7 @@ const getStateClasses = (state) => {
|
|
|
30
30
|
error: "border-danger-400 hover:border-danger-500 focus-visible:border-danger-400 focus-visible:outline-danger-400",
|
|
31
31
|
warning: "border-warning-400 hover:border-warning-400 focus-visible:border-warning-400 focus-visible:outline-warning-400",
|
|
32
32
|
loading: "cursor-wait border-input-border--disabled pointer-events-none",
|
|
33
|
-
disabled: "
|
|
33
|
+
disabled: "border-input-border--disabled text-input-text--disabled pointer-events-none",
|
|
34
34
|
default: ""
|
|
35
35
|
}[state];
|
|
36
36
|
};
|
|
@@ -58,11 +58,28 @@ const useFormFieldId = (id, name) => {
|
|
|
58
58
|
return safeName ? `${safeName}-${generatedId}` : generatedId;
|
|
59
59
|
};
|
|
60
60
|
const getErrorMessageId = (fieldId) => `${fieldId}-error`;
|
|
61
|
+
const getWarningMessageId = (fieldId) => `${fieldId}-warning`;
|
|
61
62
|
const getAriaProps = (state, ariaDescribedBy, ariaRequired, messageId) => ({
|
|
62
63
|
"aria-invalid": state === "error",
|
|
63
64
|
"aria-required": ariaRequired,
|
|
64
65
|
"aria-describedby": cn((state === "error" || state === "warning") && messageId, ariaDescribedBy) || void 0
|
|
65
66
|
});
|
|
67
|
+
/**
|
|
68
|
+
* Opt-out attributes that prevent password managers (LastPass, 1Password,
|
|
69
|
+
* Dashlane, Bitwarden, Proton Pass) from injecting their icons/overlays.
|
|
70
|
+
* Skipped for credential fields (`password`/`email`) so genuine autofill keeps working.
|
|
71
|
+
* Spread these before `{...props}` so consumers can still override per-field.
|
|
72
|
+
*/
|
|
73
|
+
const getPasswordManagerIgnoreProps = (type) => {
|
|
74
|
+
if (type === "password" || type === "email") return {};
|
|
75
|
+
return {
|
|
76
|
+
"data-1p-ignore": "true",
|
|
77
|
+
"data-bwignore": "true",
|
|
78
|
+
"data-form-type": "other",
|
|
79
|
+
"data-lpignore": "true",
|
|
80
|
+
"data-protonpass-ignore": "true"
|
|
81
|
+
};
|
|
82
|
+
};
|
|
66
83
|
const getFormFieldCSSProperties = (customProps = {}) => ({
|
|
67
84
|
"--field-border-radius": "0.5rem",
|
|
68
85
|
"--field-height": "3rem",
|
|
@@ -209,5 +226,5 @@ const scrollIntoView = (element) => {
|
|
|
209
226
|
};
|
|
210
227
|
|
|
211
228
|
//#endregion
|
|
212
|
-
export { EmptyState, ErrorMessage, LoadingState, WarningMessage, getAriaProps, getDropdownClasses, getDropdownSurfaceClasses, getDropdownWidthStyles, getErrorMessageId, getFormFieldCSSProperties, getInputClasses, getOptionClasses, getStateClasses, getTextareaClasses, getTriggerClasses, groupOptions, scrollIntoView, useClickOutside, useFormFieldId, useFormFieldState, useKeyboardNavigation };
|
|
229
|
+
export { EmptyState, ErrorMessage, LoadingState, WarningMessage, getAriaProps, getDropdownClasses, getDropdownSurfaceClasses, getDropdownWidthStyles, getErrorMessageId, getFormFieldCSSProperties, getInputClasses, getOptionClasses, getPasswordManagerIgnoreProps, getStateClasses, getTextareaClasses, getTriggerClasses, getWarningMessageId, groupOptions, scrollIntoView, useClickOutside, useFormFieldId, useFormFieldState, useKeyboardNavigation };
|
|
213
230
|
//# sourceMappingURL=formFieldUtils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formFieldUtils.js","names":[],"sources":["../../src/utils/formFieldUtils.tsx"],"sourcesContent":["import { cn } from '@utils/twUtils'\nimport { type CSSProperties, type KeyboardEvent, type ReactNode, useCallback, useEffect, useId, useRef, useState } from 'react'\nexport { ErrorMessage, WarningMessage, type FormFieldMessageValue } from '@components/FormFieldMessage/FormFieldMessage'\n\nexport type FormFieldState = 'default' | 'error' | 'warning' | 'disabled' | 'loading' | 'success'\n\nexport interface BaseFormFieldProps {\n disabled?: boolean\n errorMessage?: string | string[] | Record<string, unknown> | null\n id?: string\n label?: string\n messageReserveLines?: number\n messageReserveSpace?: boolean\n name?: string\n onChange?: (value: string) => void\n required?: boolean\n state?: FormFieldState\n 'aria-describedby'?: string\n 'aria-disabled'?: boolean\n 'aria-invalid'?: boolean\n 'aria-label'?: string\n 'aria-required'?: boolean\n}\n\nexport interface BaseOption {\n disabled?: boolean\n group?: string\n label: string\n value: string\n}\n\nexport type DropdownWidth = 'trigger' | 'content' | (string & {})\nexport type DropdownWidthMode = 'trigger' | 'content' | 'custom'\n\ninterface DropdownWidthStyleConfig {\n dropdownWidth: DropdownWidth\n triggerStyleWidth?: string\n triggerWidth: string\n viewportMaxWidth?: string\n}\n\nexport const getDropdownWidthStyles = ({\n dropdownWidth,\n triggerStyleWidth,\n triggerWidth,\n viewportMaxWidth = 'calc(100vw - 2rem)',\n}: DropdownWidthStyleConfig): {\n dropdownOverflowStyle: CSSProperties\n dropdownWidthMode: DropdownWidthMode\n dropdownWidthStyle: CSSProperties\n resolvedDropdownWidth: string\n} => {\n const dropdownWidthMode: DropdownWidthMode = dropdownWidth === 'trigger' ? 'trigger' : dropdownWidth === 'content' ? 'content' : 'custom'\n\n const resolvedDropdownWidth = dropdownWidth === 'trigger' ? triggerWidth : dropdownWidth === 'content' ? 'max-content' : dropdownWidth\n\n const dropdownOverflowStyle: CSSProperties = {\n maxWidth: viewportMaxWidth,\n overflowX: 'auto',\n overflowY: 'auto',\n }\n\n const dropdownWidthStyle: CSSProperties =\n dropdownWidth === 'trigger'\n ? { width: triggerStyleWidth ?? triggerWidth }\n : {\n width: resolvedDropdownWidth,\n ...(dropdownWidth === 'content' ? { minWidth: 'max-content' } : {}),\n ...dropdownOverflowStyle,\n }\n\n return {\n dropdownOverflowStyle,\n dropdownWidthMode,\n dropdownWidthStyle,\n resolvedDropdownWidth,\n }\n}\n\nexport const getStateClasses = (state: FormFieldState): string => {\n const stateClassMap: Record<FormFieldState, string> = {\n success: 'border-success-400 hover:border-success-500 focus-visible:border-success-400 focus-visible:outline-success-400',\n error: 'border-danger-400 hover:border-danger-500 focus-visible:border-danger-400 focus-visible:outline-danger-400',\n warning: 'border-warning-400 hover:border-warning-400 focus-visible:border-warning-400 focus-visible:outline-warning-400',\n loading: 'cursor-wait border-input-border--disabled pointer-events-none',\n disabled: 'opacity-50 border-input-border--disabled text-input-text--disabled',\n default: '',\n }\n return stateClassMap[state]\n}\n\nexport const getTriggerClasses = (isOpen: boolean = false, state: FormFieldState, className?: string): string => {\n return cn(\n 'h-12 rounded-lg px-4 text-base flex w-full items-center justify-between border-2 border-input-border bg-input-bg',\n 'text-input-text transition duration-200 hover:border-input-border--hover focus:border-input-border--focus focus:outline-none',\n 'disabled:cursor-not-allowed disabled:border-input-border--disabled disabled:bg-input-bg--disabled disabled:text-input-text--disabled',\n isOpen && 'border-input-border--focus hover:border-input-border--focus',\n getStateClasses(state),\n className,\n )\n}\n\nexport const getDropdownClasses = (isOpen: boolean, position: 'top' | 'bottom' = 'bottom'): string => {\n const positionClasses = position === 'bottom' ? 'top-full mt-1 origin-top' : 'bottom-full mb-1 origin-bottom'\n\n const animationClasses = position === 'bottom' ? (isOpen ? 'scale-100 opacity-100 translate-y-0' : 'scale-95 opacity-0 -translate-y-1 pointer-events-none') : isOpen ? 'scale-100 opacity-100 translate-y-0' : 'scale-95 opacity-0 translate-y-1 pointer-events-none'\n\n return cn('left-0 right-0 absolute z-50', getDropdownSurfaceClasses(), 'transition-[opacity,transform] duration-150 motion-reduce:transform-none motion-reduce:transition-none', 'will-change-[opacity,transform]', positionClasses, animationClasses)\n}\n\nexport const getDropdownSurfaceClasses = (): string => {\n return 'rounded-lg border border-input-border bg-input-bg shadow-md'\n}\n\nexport const getInputClasses = (state: FormFieldState, className?: string): string => {\n return cn(\n 'peer h-12 rounded-md ps-3 pe-12 text-base flex w-full border-2 border-input-border bg-input-bg transition duration-200',\n 'placeholder:text-input-text-placeholder hover:border-input-border--hover focus:border-input-border--focus focus:outline-none',\n 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-input-border--focus',\n 'disabled:pointer-events-none disabled:border-input-border--disabled disabled:bg-input-bg--disabled disabled:text-input-text--disabled',\n getStateClasses(state),\n className,\n )\n}\n\nexport const getTextareaClasses = (state: FormFieldState, className?: string): string => {\n return cn(\n 'peer h-24 rounded-md p-3 text-base flex w-full resize-none border-2 border-input-border bg-input-bg transition duration-200',\n 'placeholder:text-input-text-placeholder hover:border-input-border--hover focus:border-input-border--focus focus:outline-none',\n 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-input-border--focus',\n 'disabled:pointer-events-none disabled:border-input-border--disabled disabled:bg-input-bg--disabled disabled:text-input-text--disabled',\n getStateClasses(state),\n className,\n )\n}\n\nexport const useFormFieldId = (id?: string, name?: string): string => {\n const generatedId = useId()\n if (id) return id\n const safeName = name\n ?.trim()\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9\\-_.:]/g, '')\n return safeName ? `${safeName}-${generatedId}` : generatedId\n}\n\nexport const getErrorMessageId = (fieldId: string): string => `${fieldId}-error`\n\nexport const getAriaProps = (state: FormFieldState, ariaDescribedBy?: string, ariaRequired?: boolean, messageId?: string) => ({\n 'aria-invalid': state === 'error',\n 'aria-required': ariaRequired,\n 'aria-describedby': cn((state === 'error' || state === 'warning') && messageId, ariaDescribedBy) || undefined,\n})\n\nexport const getFormFieldCSSProperties = (customProps: Record<string, string> = {}) => ({\n '--field-border-radius': '0.5rem',\n '--field-height': '3rem',\n '--field-padding': '1rem',\n ...customProps,\n})\n\nexport const useFormFieldState = (disabled?: boolean, state: FormFieldState = 'default') => ({\n isDisabled: disabled ?? state === 'disabled',\n isLoading: state === 'loading',\n isInvalid: state === 'error',\n // isSuccess: state === 'success', // For future use\n})\n\nexport const useClickOutside = <T extends HTMLElement>(callback: () => void) => {\n const ref = useRef<T>(null)\n\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n if (ref.current && !ref.current.contains(event.target as Node)) {\n callback()\n }\n }\n\n document.addEventListener('mousedown', handleClickOutside)\n return () => document.removeEventListener('mousedown', handleClickOutside)\n }, [callback])\n\n return ref\n}\n\nexport const useKeyboardNavigation = <T extends BaseOption>(isOpen: boolean, multiSelect: boolean = false, onClose: () => void, onSelect: (value: string) => void, options: T[], selectedValue: string | string[] | undefined) => {\n const [focusedIndex, setFocusedIndex] = useState(-1)\n\n // Reset focused index when opening/closing\n useEffect(() => {\n if (isOpen) {\n if (multiSelect) {\n setFocusedIndex(0)\n } else {\n const selectedIndex = options.findIndex((option) => option.value === selectedValue)\n setFocusedIndex(selectedIndex >= 0 ? selectedIndex : 0)\n }\n } else {\n setFocusedIndex(-1)\n }\n }, [isOpen, selectedValue, options, multiSelect])\n\n const getNextEnabledIndex = useCallback(\n (currentIndex: number, direction: 'up' | 'down'): number => {\n const increment = direction === 'down' ? 1 : -1\n let nextIndex = currentIndex + increment\n\n // Boundary checks\n if (direction === 'down') {\n nextIndex = Math.min(nextIndex, options.length - 1)\n } else {\n nextIndex = Math.max(nextIndex, 0)\n }\n\n const nextOption = options[nextIndex]\n if (nextOption?.disabled && nextIndex !== currentIndex) {\n return getNextEnabledIndex(nextIndex, direction)\n }\n\n return nextIndex\n },\n [options],\n )\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLElement>): 'open' | void => {\n if (!isOpen) {\n if (['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(event.key)) {\n event.preventDefault()\n return 'open'\n }\n return\n }\n\n const keyHandlers = {\n ArrowDown: () => {\n event.preventDefault()\n setFocusedIndex((prev) => getNextEnabledIndex(prev, 'down'))\n },\n ArrowUp: () => {\n event.preventDefault()\n setFocusedIndex((prev) => getNextEnabledIndex(prev, 'up'))\n },\n Enter: () => {\n event.preventDefault()\n if (focusedIndex >= 0 && options[focusedIndex] && !options[focusedIndex].disabled) {\n onSelect(options[focusedIndex].value)\n if (!multiSelect) {\n onClose()\n }\n }\n },\n ' ': () => {\n event.preventDefault()\n if (focusedIndex >= 0 && options[focusedIndex] && !options[focusedIndex].disabled) {\n onSelect(options[focusedIndex].value)\n if (!multiSelect) {\n onClose()\n }\n }\n },\n Escape: () => {\n event.preventDefault()\n onClose()\n },\n Home: () => {\n event.preventDefault()\n setFocusedIndex(0)\n },\n End: () => {\n event.preventDefault()\n setFocusedIndex(options.length - 1)\n },\n }\n\n const handler = keyHandlers[event.key as keyof typeof keyHandlers]\n if (handler) {\n handler()\n }\n },\n [options, focusedIndex, onSelect, onClose, isOpen, multiSelect, getNextEnabledIndex],\n )\n\n return { focusedIndex, setFocusedIndex, handleKeyDown, isOpen }\n}\n\nexport const groupOptions = <T extends BaseOption>(options: T[]) => {\n const groups: Record<string, T[]> = {}\n const ungrouped: T[] = []\n\n options.forEach((option) => {\n if (option.group) {\n if (!groups[option.group]) {\n groups[option.group] = []\n }\n groups[option.group].push(option)\n } else {\n ungrouped.push(option)\n }\n })\n\n return {\n groups,\n ungrouped,\n hasGroups: Object.keys(groups).length > 0,\n }\n}\n\nexport const LoadingState = ({ className, message = 'Loading…' }: { className?: string; message?: string }) => (\n <div aria-live='polite' className={cn('gap-2 py-6 text-base text-input-text-secondary flex items-center justify-center', className)} role='status'>\n {message}\n </div>\n)\n\nexport const EmptyState = ({ className, message = 'No options found' }: { className?: string; message?: ReactNode }) => (\n <div className={cn('py-6 text-base text-center text-text-secondary', className)} role='status'>\n {message}\n </div>\n)\n\nexport const getOptionClasses = (isDisabled: boolean = false, isFocused: boolean, isSelected: boolean): string => {\n return cn(\n 'my-0.5 first:mt-0 last:mb-0 rounded-sm py-1.5 pl-2 pr-8 text-base relative flex w-full cursor-pointer items-center outline-none select-none hover:bg-input-bg--hover data-[highlighted]:bg-input-bg--hover data-[highlighted]:text-input-text',\n isFocused && 'bg-input-bg--hover',\n isSelected && 'bg-input-bg--selected text-input-text',\n isDisabled && 'pointer-events-none text-input-text--disabled',\n )\n}\n\nexport const scrollIntoView = (element: HTMLElement | null) => {\n if (element) {\n element.scrollIntoView({ block: 'nearest', behavior: 'smooth' })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"formFieldUtils.js","names":[],"sources":["../../src/utils/formFieldUtils.tsx"],"sourcesContent":["import { cn } from '@utils/twUtils'\nimport { type CSSProperties, type KeyboardEvent, type ReactNode, useCallback, useEffect, useId, useRef, useState } from 'react'\nexport { ErrorMessage, WarningMessage, type FormFieldMessageValue } from '@components/FormFieldMessage/FormFieldMessage'\n\nexport type FormFieldState = 'default' | 'error' | 'warning' | 'disabled' | 'loading' | 'success'\n\nexport interface BaseFormFieldProps {\n disabled?: boolean\n errorMessage?: string | string[] | Record<string, unknown> | null\n id?: string\n label?: string\n messageReserveLines?: number\n messageReserveSpace?: boolean\n name?: string\n onChange?: (value: string) => void\n required?: boolean\n state?: FormFieldState\n 'aria-describedby'?: string\n 'aria-disabled'?: boolean\n 'aria-invalid'?: boolean\n 'aria-label'?: string\n 'aria-required'?: boolean\n}\n\nexport interface BaseOption {\n disabled?: boolean\n group?: string\n label: string\n value: string\n}\n\nexport type DropdownWidth = 'trigger' | 'content' | (string & {})\nexport type DropdownWidthMode = 'trigger' | 'content' | 'custom'\n\ninterface DropdownWidthStyleConfig {\n dropdownWidth: DropdownWidth\n triggerStyleWidth?: string\n triggerWidth: string\n viewportMaxWidth?: string\n}\n\nexport const getDropdownWidthStyles = ({\n dropdownWidth,\n triggerStyleWidth,\n triggerWidth,\n viewportMaxWidth = 'calc(100vw - 2rem)',\n}: DropdownWidthStyleConfig): {\n dropdownOverflowStyle: CSSProperties\n dropdownWidthMode: DropdownWidthMode\n dropdownWidthStyle: CSSProperties\n resolvedDropdownWidth: string\n} => {\n const dropdownWidthMode: DropdownWidthMode = dropdownWidth === 'trigger' ? 'trigger' : dropdownWidth === 'content' ? 'content' : 'custom'\n\n const resolvedDropdownWidth = dropdownWidth === 'trigger' ? triggerWidth : dropdownWidth === 'content' ? 'max-content' : dropdownWidth\n\n const dropdownOverflowStyle: CSSProperties = {\n maxWidth: viewportMaxWidth,\n overflowX: 'auto',\n overflowY: 'auto',\n }\n\n const dropdownWidthStyle: CSSProperties =\n dropdownWidth === 'trigger'\n ? { width: triggerStyleWidth ?? triggerWidth }\n : {\n width: resolvedDropdownWidth,\n ...(dropdownWidth === 'content' ? { minWidth: 'max-content' } : {}),\n ...dropdownOverflowStyle,\n }\n\n return {\n dropdownOverflowStyle,\n dropdownWidthMode,\n dropdownWidthStyle,\n resolvedDropdownWidth,\n }\n}\n\nexport const getStateClasses = (state: FormFieldState): string => {\n const stateClassMap: Record<FormFieldState, string> = {\n success: 'border-success-400 hover:border-success-500 focus-visible:border-success-400 focus-visible:outline-success-400',\n error: 'border-danger-400 hover:border-danger-500 focus-visible:border-danger-400 focus-visible:outline-danger-400',\n warning: 'border-warning-400 hover:border-warning-400 focus-visible:border-warning-400 focus-visible:outline-warning-400',\n loading: 'cursor-wait border-input-border--disabled pointer-events-none',\n disabled: 'border-input-border--disabled text-input-text--disabled pointer-events-none',\n default: '',\n }\n return stateClassMap[state]\n}\n\nexport const getTriggerClasses = (isOpen: boolean = false, state: FormFieldState, className?: string): string => {\n return cn(\n 'h-12 rounded-lg px-4 text-base flex w-full items-center justify-between border-2 border-input-border bg-input-bg',\n 'text-input-text transition duration-200 hover:border-input-border--hover focus:border-input-border--focus focus:outline-none',\n 'disabled:cursor-not-allowed disabled:border-input-border--disabled disabled:bg-input-bg--disabled disabled:text-input-text--disabled',\n isOpen && 'border-input-border--focus hover:border-input-border--focus',\n getStateClasses(state),\n className,\n )\n}\n\nexport const getDropdownClasses = (isOpen: boolean, position: 'top' | 'bottom' = 'bottom'): string => {\n const positionClasses = position === 'bottom' ? 'top-full mt-1 origin-top' : 'bottom-full mb-1 origin-bottom'\n const animationClasses = position === 'bottom' ? (isOpen ? 'scale-100 opacity-100 translate-y-0' : 'scale-95 opacity-0 -translate-y-1 pointer-events-none') : isOpen ? 'scale-100 opacity-100 translate-y-0' : 'scale-95 opacity-0 translate-y-1 pointer-events-none'\n\n return cn('left-0 right-0 absolute z-50', getDropdownSurfaceClasses(), 'transition-[opacity,transform] duration-150 motion-reduce:transform-none motion-reduce:transition-none', 'will-change-[opacity,transform]', positionClasses, animationClasses)\n}\n\nexport const getDropdownSurfaceClasses = (): string => {\n return 'rounded-lg border border-input-border bg-input-bg shadow-md'\n}\n\nexport const getInputClasses = (state: FormFieldState, className?: string): string => {\n return cn(\n 'peer h-12 rounded-md ps-3 pe-12 text-base flex w-full border-2 border-input-border bg-input-bg transition duration-200',\n 'placeholder:text-input-text-placeholder hover:border-input-border--hover focus:border-input-border--focus focus:outline-none',\n 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-input-border--focus',\n 'disabled:pointer-events-none disabled:border-input-border--disabled disabled:bg-input-bg--disabled disabled:text-input-text--disabled',\n getStateClasses(state),\n className,\n )\n}\n\nexport const getTextareaClasses = (state: FormFieldState, className?: string): string => {\n return cn(\n 'peer h-24 rounded-md p-3 text-base flex w-full resize-none border-2 border-input-border bg-input-bg transition duration-200',\n 'placeholder:text-input-text-placeholder hover:border-input-border--hover focus:border-input-border--focus focus:outline-none',\n 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-input-border--focus',\n 'disabled:pointer-events-none disabled:border-input-border--disabled disabled:bg-input-bg--disabled disabled:text-input-text--disabled',\n getStateClasses(state),\n className,\n )\n}\n\nexport const useFormFieldId = (id?: string, name?: string): string => {\n const generatedId = useId()\n if (id) return id\n const safeName = name\n ?.trim()\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9\\-_.:]/g, '')\n return safeName ? `${safeName}-${generatedId}` : generatedId\n}\n\nexport const getErrorMessageId = (fieldId: string): string => `${fieldId}-error`\nexport const getWarningMessageId = (fieldId: string): string => `${fieldId}-warning`\n\nexport const getAriaProps = (state: FormFieldState, ariaDescribedBy?: string, ariaRequired?: boolean, messageId?: string) => ({\n 'aria-invalid': state === 'error',\n 'aria-required': ariaRequired,\n 'aria-describedby': cn((state === 'error' || state === 'warning') && messageId, ariaDescribedBy) || undefined,\n})\n\n/**\n * Opt-out attributes that prevent password managers (LastPass, 1Password,\n * Dashlane, Bitwarden, Proton Pass) from injecting their icons/overlays.\n * Skipped for credential fields (`password`/`email`) so genuine autofill keeps working.\n * Spread these before `{...props}` so consumers can still override per-field.\n */\nexport const getPasswordManagerIgnoreProps = (type?: string): Record<string, string> => {\n if (type === 'password' || type === 'email') return {}\n return {\n 'data-1p-ignore': 'true',\n 'data-bwignore': 'true',\n 'data-form-type': 'other',\n 'data-lpignore': 'true',\n 'data-protonpass-ignore': 'true',\n }\n}\n\nexport const getFormFieldCSSProperties = (customProps: Record<string, string> = {}) => ({\n '--field-border-radius': '0.5rem',\n '--field-height': '3rem',\n '--field-padding': '1rem',\n ...customProps,\n})\n\nexport const useFormFieldState = (disabled?: boolean, state: FormFieldState = 'default') => ({\n isDisabled: disabled ?? state === 'disabled',\n isLoading: state === 'loading',\n isInvalid: state === 'error',\n // isSuccess: state === 'success', // For future use\n})\n\nexport const useClickOutside = <T extends HTMLElement>(callback: () => void) => {\n const ref = useRef<T>(null)\n\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n if (ref.current && !ref.current.contains(event.target as Node)) {\n callback()\n }\n }\n\n document.addEventListener('mousedown', handleClickOutside)\n return () => document.removeEventListener('mousedown', handleClickOutside)\n }, [callback])\n\n return ref\n}\n\nexport const useKeyboardNavigation = <T extends BaseOption>(isOpen: boolean, multiSelect: boolean = false, onClose: () => void, onSelect: (value: string) => void, options: T[], selectedValue: string | string[] | undefined) => {\n const [focusedIndex, setFocusedIndex] = useState(-1)\n\n // Reset focused index when opening/closing\n useEffect(() => {\n if (isOpen) {\n if (multiSelect) {\n setFocusedIndex(0)\n } else {\n const selectedIndex = options.findIndex((option) => option.value === selectedValue)\n setFocusedIndex(selectedIndex >= 0 ? selectedIndex : 0)\n }\n } else {\n setFocusedIndex(-1)\n }\n }, [isOpen, selectedValue, options, multiSelect])\n\n const getNextEnabledIndex = useCallback(\n (currentIndex: number, direction: 'up' | 'down'): number => {\n const increment = direction === 'down' ? 1 : -1\n let nextIndex = currentIndex + increment\n\n // Boundary checks\n if (direction === 'down') {\n nextIndex = Math.min(nextIndex, options.length - 1)\n } else {\n nextIndex = Math.max(nextIndex, 0)\n }\n\n const nextOption = options[nextIndex]\n if (nextOption?.disabled && nextIndex !== currentIndex) {\n return getNextEnabledIndex(nextIndex, direction)\n }\n\n return nextIndex\n },\n [options],\n )\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLElement>): 'open' | void => {\n if (!isOpen) {\n if (['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(event.key)) {\n event.preventDefault()\n return 'open'\n }\n return\n }\n\n const keyHandlers = {\n ArrowDown: () => {\n event.preventDefault()\n setFocusedIndex((prev) => getNextEnabledIndex(prev, 'down'))\n },\n ArrowUp: () => {\n event.preventDefault()\n setFocusedIndex((prev) => getNextEnabledIndex(prev, 'up'))\n },\n Enter: () => {\n event.preventDefault()\n if (focusedIndex >= 0 && options[focusedIndex] && !options[focusedIndex].disabled) {\n onSelect(options[focusedIndex].value)\n if (!multiSelect) {\n onClose()\n }\n }\n },\n ' ': () => {\n event.preventDefault()\n if (focusedIndex >= 0 && options[focusedIndex] && !options[focusedIndex].disabled) {\n onSelect(options[focusedIndex].value)\n if (!multiSelect) {\n onClose()\n }\n }\n },\n Escape: () => {\n event.preventDefault()\n onClose()\n },\n Home: () => {\n event.preventDefault()\n setFocusedIndex(0)\n },\n End: () => {\n event.preventDefault()\n setFocusedIndex(options.length - 1)\n },\n }\n\n const handler = keyHandlers[event.key as keyof typeof keyHandlers]\n if (handler) {\n handler()\n }\n },\n [options, focusedIndex, onSelect, onClose, isOpen, multiSelect, getNextEnabledIndex],\n )\n\n return { focusedIndex, setFocusedIndex, handleKeyDown, isOpen }\n}\n\nexport const groupOptions = <T extends BaseOption>(options: T[]) => {\n const groups: Record<string, T[]> = {}\n const ungrouped: T[] = []\n\n options.forEach((option) => {\n if (option.group) {\n if (!groups[option.group]) {\n groups[option.group] = []\n }\n groups[option.group].push(option)\n } else {\n ungrouped.push(option)\n }\n })\n\n return {\n groups,\n ungrouped,\n hasGroups: Object.keys(groups).length > 0,\n }\n}\n\nexport const LoadingState = ({ className, message = 'Loading…' }: { className?: string; message?: string }) => (\n <div aria-live='polite' className={cn('gap-2 py-6 text-base text-input-text-secondary flex items-center justify-center', className)} role='status'>\n {message}\n </div>\n)\n\nexport const EmptyState = ({ className, message = 'No options found' }: { className?: string; message?: ReactNode }) => (\n <div className={cn('py-6 text-base text-center text-text-secondary', className)} role='status'>\n {message}\n </div>\n)\n\nexport const getOptionClasses = (isDisabled: boolean = false, isFocused: boolean, isSelected: boolean): string => {\n return cn(\n 'my-0.5 first:mt-0 last:mb-0 rounded-sm py-1.5 pl-2 pr-8 text-base relative flex w-full cursor-pointer items-center outline-none select-none hover:bg-input-bg--hover data-[highlighted]:bg-input-bg--hover data-[highlighted]:text-input-text',\n isFocused && 'bg-input-bg--hover',\n isSelected && 'bg-input-bg--selected text-input-text',\n isDisabled && 'pointer-events-none text-input-text--disabled',\n )\n}\n\nexport const scrollIntoView = (element: HTMLElement | null) => {\n if (element) {\n element.scrollIntoView({ block: 'nearest', behavior: 'smooth' })\n }\n}\n"],"mappings":"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"}
|