Files
MosaicIQ/MosaicIQ/src/components/Settings/ValidatedInput.tsx
2026-04-05 00:17:26 -04:00

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>
);
};