- Bulk replace text-white → text-foreground across 116 component files (preserving text-white/ opacity variants) - Remove hover-glow-cyan, shadow-card-glow-cyan, shadow-button-primary-glow classes from all components - Replace --duration-normal/--duration-immersive/--duration-slow with --sumi-duration-normal/--sumi-duration-slow across 130+ files - Replace --ease-out/--ease-in-out with --sumi-ease-out/--sumi-ease-in-out - Replace focus:ring-blue-500 → focus:ring-primary (4 files) - Remove hover:scale-105/110 and hover:-translate-y-1/0.5 transforms (SUMI anti-pattern: no scale on hover) - Clean up stale kodo- references in comments Co-authored-by: Cursor <cursoragent@cursor.com>
96 lines
3.5 KiB
TypeScript
96 lines
3.5 KiB
TypeScript
import React, { useId, useState } from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
import { Eye, EyeOff } from 'lucide-react';
|
|
|
|
interface AuthInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
error?: string;
|
|
label?: string;
|
|
showPasswordToggle?: boolean;
|
|
}
|
|
|
|
export function AuthInput({
|
|
error,
|
|
label,
|
|
className,
|
|
id,
|
|
showPasswordToggle,
|
|
type,
|
|
...props
|
|
}: AuthInputProps) {
|
|
// CRITIQUE FIX #5: Utiliser useId() de React pour générer un ID stable
|
|
// qui ne change pas entre les renders, contrairement à Math.random()
|
|
const generatedId = useId();
|
|
const inputId = id || generatedId;
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const inputType = type === 'password' && showPassword ? 'text' : type;
|
|
const hasToggle = type === 'password' && showPasswordToggle;
|
|
|
|
return (
|
|
<div className="w-full">
|
|
{label && (
|
|
<label
|
|
htmlFor={inputId}
|
|
className="block text-sm font-medium text-foreground mb-1"
|
|
>
|
|
{label}
|
|
</label>
|
|
)}
|
|
<div className="relative">
|
|
<input
|
|
id={inputId}
|
|
type={inputType}
|
|
className={cn(
|
|
'w-full px-4 py-2.5 border rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 transition-all duration-[var(--sumi-duration-slow)] ease-in-out',
|
|
// Focus glow
|
|
'focus-visible:shadow-[0_0_0_3px_oklch(var(--primary)/0.15),0_0_12px_oklch(var(--primary)/0.1)]',
|
|
'bg-card border-border text-foreground placeholder:text-muted-foreground',
|
|
error
|
|
? 'border-destructive focus-visible:border-destructive'
|
|
: 'focus-visible:border-primary',
|
|
hasToggle ? 'pr-10' : '',
|
|
className,
|
|
)}
|
|
aria-invalid={error ? 'true' : 'false'}
|
|
aria-describedby={error ? `${inputId}-error` : undefined}
|
|
aria-required={props.required ? 'true' : undefined}
|
|
{...props}
|
|
// CRITIQUE FIX #4: Définir autoComplete APRÈS le spread pour éviter qu'il soit écrasé
|
|
// Préférer props.autoComplete s'il est défini, sinon utiliser une valeur par défaut basée sur le type
|
|
autoComplete={
|
|
props.autoComplete !== undefined
|
|
? props.autoComplete
|
|
: type === 'email'
|
|
? 'email'
|
|
: type === 'password'
|
|
? 'current-password'
|
|
: undefined
|
|
}
|
|
// CRITIQUE FIX #6: S'assurer que l'attribut HTML5 required est présent si props.required est true
|
|
// Le spread {...props} devrait déjà inclure required, mais on le définit explicitement pour être sûr
|
|
required={props.required}
|
|
/>
|
|
{/* Password visibility toggle */}
|
|
{hasToggle && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-opacity transition-colors duration-[var(--duration-fast)]"
|
|
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
|
tabIndex={-1}
|
|
>
|
|
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
</button>
|
|
)}
|
|
</div>
|
|
{error && (
|
|
<p
|
|
id={`${inputId}-error`}
|
|
className="mt-1 text-sm text-destructive animate-shake"
|
|
role="alert"
|
|
>
|
|
{error}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|