veza/packages/design-system/src/components/Input/Input.tsx

117 lines
5.2 KiB
TypeScript
Raw Normal View History

2026-01-07 18:39:21 +00:00
import React from 'react';
import { Search, Upload as UploadIcon } from 'lucide-react';
import { cn } from '../../utils/cn';
/* -------------------------------------------------------------------------- */
/* Text Input */
/* -------------------------------------------------------------------------- */
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
icon?: React.ReactNode;
}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ label, icon, className, type, autoComplete, required, id, ...props }, ref) => {
// CRITIQUE FIX #4: Générer un ID stable si non fourni pour l'association avec le label
const generatedId = React.useId();
const inputId = id || generatedId;
// CRITIQUE FIX #4: Déterminer autoComplete par défaut basé sur le type
const defaultAutoComplete = autoComplete !== undefined
? autoComplete
: (type === 'email' ? 'email'
: type === 'password' ? 'current-password'
: undefined);
// CRITIQUE FIX #51: S'assurer que aria-describedby et aria-invalid sont correctement passés
const ariaDescribedBy = props['aria-describedby'];
const ariaInvalid = props['aria-invalid'];
return (
<div className="w-full">
{label && (
<label htmlFor={inputId} className="block text-sm font-medium text-gray-400 mb-2 font-body">{label}</label>
)}
<div className="relative">
{icon && (
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none">
{icon}
</div>
)}
<input
ref={ref}
id={inputId}
type={type}
autoComplete={defaultAutoComplete}
required={required}
aria-describedby={ariaDescribedBy}
aria-invalid={ariaInvalid}
className={cn(
'w-full py-3 bg-kodo-graphite border border-kodo-steel text-white placeholder-gray-500 font-body text-base rounded-lg focus:outline-none focus:border-kodo-cyan focus:ring-1 focus:ring-kodo-cyan transition-all duration-200',
icon ? 'pl-11 pr-4' : 'px-4',
className
)}
{...props}
/>
</div>
</div>
);
}
);
Input.displayName = 'Input';
/* -------------------------------------------------------------------------- */
/* Search Input */
/* -------------------------------------------------------------------------- */
export const SearchInput = React.forwardRef<
HTMLInputElement,
React.InputHTMLAttributes<HTMLInputElement>
>((props, ref) => {
return (
<div className="relative w-full group">
<input
ref={ref}
type="search"
className="w-full pl-12 pr-4 py-3 bg-kodo-graphite border border-kodo-steel text-white placeholder-gray-500 rounded-full focus:outline-none focus:border-kodo-cyan focus:ring-1 focus:ring-kodo-cyan focus:shadow-neon-cyan transition-all duration-300"
placeholder="Search platform..."
{...props}
/>
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500 group-focus-within:text-kodo-cyan transition-colors" />
</div>
);
});
SearchInput.displayName = 'SearchInput';
/* -------------------------------------------------------------------------- */
/* File Upload */
/* -------------------------------------------------------------------------- */
export interface FileUploadProps {
onUpload?: (files: FileList) => void;
className?: string;
}
export const FileUpload: React.FC<FileUploadProps> = ({ onUpload, className }) => {
return (
<div
className={cn(
'border-2 border-dashed border-kodo-steel rounded-xl p-8 bg-kodo-graphite/50 hover:bg-kodo-slate/30 hover:border-kodo-cyan/50 transition-all duration-300 cursor-pointer text-center group',
className
)}
>
<div className="w-16 h-16 rounded-full bg-kodo-slate flex items-center justify-center mx-auto mb-4 group-hover:scale-110 group-hover:bg-kodo-steel transition-all">
<UploadIcon className="w-8 h-8 text-kodo-cyan" />
</div>
<h3 className="text-xl font-bold text-white mb-2 font-display">Drop your stems here</h3>
<p className="text-gray-500 text-sm max-w-md mx-auto">
Support for WAV, FLAC, AIFF. Up to 500MB per file.{' '}
<span className="text-kodo-cyan">Premium users</span> get unlimited storage.
</p>
</div>
);
};