102 lines
2.9 KiB
TypeScript
102 lines
2.9 KiB
TypeScript
import React from 'react';
|
|
import { Check, X } from 'lucide-react';
|
|
|
|
export type ValidationStatus = 'idle' | 'valid' | 'invalid';
|
|
|
|
export interface ValidatedInputProps extends Omit<React.ComponentProps<'input'>, 'aria-invalid'> {
|
|
label?: string;
|
|
validationStatus?: ValidationStatus;
|
|
errorMessage?: string;
|
|
helperText?: string;
|
|
showValidationIcon?: boolean;
|
|
fullWidth?: boolean;
|
|
}
|
|
|
|
const statusStyles: Record<
|
|
ValidationStatus,
|
|
{ border: string; focus: string; icon: React.ReactNode | null; ariaInvalid: boolean }
|
|
> = {
|
|
idle: {
|
|
border: 'border-[#2a2a2a]',
|
|
focus: 'focus:border-[#58a6ff]',
|
|
icon: null,
|
|
ariaInvalid: false,
|
|
},
|
|
valid: {
|
|
border: 'border-[#214f31]',
|
|
focus: 'focus:border-[#9ee6b3]',
|
|
icon: <Check className="h-4 w-4 text-[#9ee6b3]" />,
|
|
ariaInvalid: false,
|
|
},
|
|
invalid: {
|
|
border: 'border-[#5c2b2b]',
|
|
focus: 'focus:border-[#ffb4b4]',
|
|
icon: <X className="h-4 w-4 text-[#ffb4b4]" />,
|
|
ariaInvalid: true,
|
|
},
|
|
};
|
|
|
|
export const ValidatedInput: React.FC<ValidatedInputProps> = ({
|
|
label,
|
|
validationStatus = 'idle',
|
|
errorMessage,
|
|
helperText,
|
|
showValidationIcon = true,
|
|
fullWidth = true,
|
|
className = '',
|
|
id,
|
|
...props
|
|
}) => {
|
|
const styles = statusStyles[validationStatus];
|
|
const inputId = id ?? `input-${Math.random().toString(36).substring(2, 9)}`;
|
|
const errorId = `${inputId}-error`;
|
|
const helperId = `${inputId}-helper`;
|
|
|
|
const baseClassName =
|
|
'rounded bg-[#111111] px-3 py-2 text-sm font-mono text-[#e0e0e0] outline-none transition-colors';
|
|
const borderClassName = `${styles.border} ${styles.focus}`;
|
|
const widthClassName = fullWidth ? 'w-full' : '';
|
|
const iconWidth = showValidationIcon ? 'pr-10' : '';
|
|
|
|
return (
|
|
<label className={`${fullWidth ? 'block' : ''} ${className}`}>
|
|
{label && (
|
|
<span className="mb-2 block text-xs font-mono text-[#888888]">{label}</span>
|
|
)}
|
|
<div className="relative">
|
|
<input
|
|
id={inputId}
|
|
className={`${baseClassName} ${borderClassName} ${widthClassName} ${iconWidth} ${className}`}
|
|
aria-invalid={styles.ariaInvalid}
|
|
aria-describedby={
|
|
validationStatus === 'invalid' && errorMessage
|
|
? errorId
|
|
: helperText
|
|
? helperId
|
|
: undefined
|
|
}
|
|
{...props}
|
|
/>
|
|
{showValidationIcon && styles.icon && (
|
|
<span
|
|
className="absolute right-3 top-1/2 -translate-y-1/2"
|
|
aria-hidden="true"
|
|
>
|
|
{styles.icon}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{validationStatus === 'invalid' && errorMessage && (
|
|
<p id={errorId} className="mt-1.5 text-xs font-mono text-[#ffb4b4]" role="alert">
|
|
{errorMessage}
|
|
</p>
|
|
)}
|
|
{validationStatus !== 'invalid' && helperText && (
|
|
<p id={helperId} className="mt-1.5 text-xs font-mono text-[#888888]">
|
|
{helperText}
|
|
</p>
|
|
)}
|
|
</label>
|
|
);
|
|
};
|