115 lines
3 KiB
JavaScript
115 lines
3 KiB
JavaScript
|
|
import { forwardRef, useId } from 'react';
|
||
|
|
|
||
|
|
const Input = forwardRef(function Input(
|
||
|
|
{
|
||
|
|
label,
|
||
|
|
helperText,
|
||
|
|
error,
|
||
|
|
leadingIcon,
|
||
|
|
trailingIcon,
|
||
|
|
trailingAction,
|
||
|
|
id: idProp,
|
||
|
|
type = 'text',
|
||
|
|
className = '',
|
||
|
|
style,
|
||
|
|
...rest
|
||
|
|
},
|
||
|
|
ref
|
||
|
|
) {
|
||
|
|
const reactId = useId();
|
||
|
|
const id = idProp ?? reactId;
|
||
|
|
const describedBy = [];
|
||
|
|
if (helperText && !error) describedBy.push(`${id}-helper`);
|
||
|
|
if (error) describedBy.push(`${id}-error`);
|
||
|
|
|
||
|
|
const inputBorder = error
|
||
|
|
? '1px solid #dc2626'
|
||
|
|
: '1px solid var(--border)';
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={className}>
|
||
|
|
{label && (
|
||
|
|
<label
|
||
|
|
htmlFor={id}
|
||
|
|
className="block text-sm font-medium mb-2"
|
||
|
|
style={{ color: 'var(--text-primary)' }}
|
||
|
|
>
|
||
|
|
{label}
|
||
|
|
</label>
|
||
|
|
)}
|
||
|
|
<div className="relative">
|
||
|
|
{leadingIcon && (
|
||
|
|
<span
|
||
|
|
aria-hidden="true"
|
||
|
|
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 inline-flex"
|
||
|
|
style={{ color: 'var(--text-secondary)' }}
|
||
|
|
>
|
||
|
|
{leadingIcon}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
<input
|
||
|
|
ref={ref}
|
||
|
|
id={id}
|
||
|
|
type={type}
|
||
|
|
aria-invalid={error ? 'true' : undefined}
|
||
|
|
aria-describedby={describedBy.length > 0 ? describedBy.join(' ') : undefined}
|
||
|
|
className={[
|
||
|
|
'w-full rounded-xl px-4 py-3 transition-all duration-200',
|
||
|
|
'focus:outline-none focus-visible:ring-2',
|
||
|
|
leadingIcon ? 'pl-10' : '',
|
||
|
|
trailingIcon || trailingAction ? 'pr-10' : '',
|
||
|
|
]
|
||
|
|
.filter(Boolean)
|
||
|
|
.join(' ')}
|
||
|
|
style={{
|
||
|
|
background: 'var(--glass-surface-high)',
|
||
|
|
backdropFilter:
|
||
|
|
'blur(var(--glass-blur-low)) saturate(var(--glass-saturate))',
|
||
|
|
WebkitBackdropFilter:
|
||
|
|
'blur(var(--glass-blur-low)) saturate(var(--glass-saturate))',
|
||
|
|
border: inputBorder,
|
||
|
|
color: 'var(--text-primary)',
|
||
|
|
'--tw-ring-color': error ? '#dc2626' : 'var(--accent-ember)',
|
||
|
|
...style,
|
||
|
|
}}
|
||
|
|
{...rest}
|
||
|
|
/>
|
||
|
|
{trailingIcon && (
|
||
|
|
<span
|
||
|
|
aria-hidden="true"
|
||
|
|
className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 inline-flex"
|
||
|
|
style={{ color: 'var(--text-secondary)' }}
|
||
|
|
>
|
||
|
|
{trailingIcon}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
{trailingAction && (
|
||
|
|
<span className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex">
|
||
|
|
{trailingAction}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
{error && (
|
||
|
|
<p
|
||
|
|
id={`${id}-error`}
|
||
|
|
className="mt-1 text-xs"
|
||
|
|
style={{ color: '#dc2626' }}
|
||
|
|
>
|
||
|
|
{error}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
{!error && helperText && (
|
||
|
|
<p
|
||
|
|
id={`${id}-helper`}
|
||
|
|
className="mt-1 text-xs"
|
||
|
|
style={{ color: 'var(--text-secondary)' }}
|
||
|
|
>
|
||
|
|
{helperText}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
export default Input;
|