2025-12-03 21:56:50 +00:00
|
|
|
import React from 'react';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
2026-01-16 01:11:21 +00:00
|
|
|
import { Input as DesignSystemInput } from './input';
|
|
|
|
|
import { Textarea as DesignSystemTextarea } from './textarea';
|
|
|
|
|
import { Select as DesignSystemSelect } from './select';
|
2025-12-03 21:56:50 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* FormFieldProps - Propriétés du composant FormField
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @interface FormFieldProps
|
|
|
|
|
*/
|
2025-12-03 21:56:50 +00:00
|
|
|
interface FormFieldProps {
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Label du champ de formulaire
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
|
|
|
|
* <FormField label="Email">
|
|
|
|
|
* <Input type="email" />
|
|
|
|
|
* </FormField>
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
2025-12-03 21:56:50 +00:00
|
|
|
label: string;
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Message d'erreur à afficher sous le champ
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
|
|
|
|
* <FormField label="Email" error={errors.email}>
|
|
|
|
|
* <Input type="email" />
|
|
|
|
|
* </FormField>
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
2025-12-03 21:56:50 +00:00
|
|
|
error?: string;
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Si `true`, affiche un indicateur requis (*)
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @default false
|
|
|
|
|
*/
|
2025-12-03 21:56:50 +00:00
|
|
|
required?: boolean;
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Champ de formulaire enfant (Input, Textarea, Select, etc.)
|
|
|
|
|
*/
|
2025-12-03 21:56:50 +00:00
|
|
|
children: React.ReactNode;
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Classes CSS personnalisées
|
|
|
|
|
*/
|
2025-12-03 21:56:50 +00:00
|
|
|
className?: string;
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Texte d'aide à afficher sous le champ (si pas d'erreur)
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
2026-01-13 18:47:57 +00:00
|
|
|
* <FormField
|
2026-01-07 09:31:02 +00:00
|
|
|
* label="Mot de passe"
|
|
|
|
|
* helpText="Minimum 8 caractères"
|
|
|
|
|
* >
|
|
|
|
|
* <Input type="password" />
|
|
|
|
|
* </FormField>
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
2025-12-03 21:56:50 +00:00
|
|
|
helpText?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* FormField - Composant de champ de formulaire avec label et validation
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* Composant wrapper pour les champs de formulaire avec support pour :
|
|
|
|
|
* - Label avec indicateur requis
|
|
|
|
|
* - Message d'erreur
|
|
|
|
|
* - Texte d'aide
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
|
|
|
|
* // Champ simple
|
|
|
|
|
* <FormField label="Nom">
|
|
|
|
|
* <Input type="text" />
|
|
|
|
|
* </FormField>
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* // Champ requis avec erreur
|
2026-01-13 18:47:57 +00:00
|
|
|
* <FormField
|
|
|
|
|
* label="Email"
|
|
|
|
|
* required
|
2026-01-07 09:31:02 +00:00
|
|
|
* error={errors.email}
|
|
|
|
|
* >
|
|
|
|
|
* <Input type="email" />
|
|
|
|
|
* </FormField>
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* // Champ avec texte d'aide
|
2026-01-13 18:47:57 +00:00
|
|
|
* <FormField
|
2026-01-07 09:31:02 +00:00
|
|
|
* label="Description"
|
|
|
|
|
* helpText="Maximum 500 caractères"
|
|
|
|
|
* >
|
|
|
|
|
* <Textarea />
|
|
|
|
|
* </FormField>
|
|
|
|
|
* ```
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @component
|
|
|
|
|
* @param {FormFieldProps} props - Propriétés du composant
|
|
|
|
|
* @returns {JSX.Element} Élément div contenant le label, le champ et les messages
|
|
|
|
|
*/
|
|
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
export const FormField: React.FC<FormFieldProps> = ({
|
|
|
|
|
label,
|
|
|
|
|
error,
|
|
|
|
|
required = false,
|
|
|
|
|
children,
|
|
|
|
|
className,
|
|
|
|
|
helpText,
|
|
|
|
|
}) => {
|
|
|
|
|
return (
|
|
|
|
|
<div className={cn('space-y-2', className)}>
|
2026-01-16 00:56:37 +00:00
|
|
|
<label className="text-sm font-medium text-kodo-text-main dark:text-kodo-text-main">
|
2025-12-03 21:56:50 +00:00
|
|
|
{label}
|
2026-01-16 00:56:37 +00:00
|
|
|
{required && <span className="text-kodo-red ml-1">*</span>}
|
2025-12-03 21:56:50 +00:00
|
|
|
</label>
|
|
|
|
|
{children}
|
|
|
|
|
{helpText && !error && (
|
2026-02-08 23:04:51 +00:00
|
|
|
<p className="text-xs text-muted-foreground dark:text-muted-foreground">{helpText}</p>
|
2025-12-03 21:56:50 +00:00
|
|
|
)}
|
|
|
|
|
{error && (
|
2026-01-16 00:56:37 +00:00
|
|
|
<p className="text-xs text-kodo-red dark:text-kodo-red">{error}</p>
|
2025-12-03 21:56:50 +00:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
|
|
|
error?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 02:34:34 +00:00
|
|
|
export const Input: React.FC<InputProps> = ({
|
|
|
|
|
error = false,
|
|
|
|
|
className,
|
|
|
|
|
...props
|
2025-12-03 21:56:50 +00:00
|
|
|
}) => {
|
|
|
|
|
return (
|
2026-01-16 01:11:21 +00:00
|
|
|
<DesignSystemInput
|
2025-12-03 21:56:50 +00:00
|
|
|
className={cn(
|
2026-01-16 01:11:21 +00:00
|
|
|
error && 'border-kodo-red focus-visible:border-kodo-red',
|
2025-12-13 02:34:34 +00:00
|
|
|
className,
|
2025-12-03 21:56:50 +00:00
|
|
|
)}
|
|
|
|
|
{...props}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-13 02:34:34 +00:00
|
|
|
interface TextareaProps
|
|
|
|
|
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
2025-12-03 21:56:50 +00:00
|
|
|
error?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 02:34:34 +00:00
|
|
|
export const Textarea: React.FC<TextareaProps> = ({
|
|
|
|
|
error = false,
|
|
|
|
|
className,
|
|
|
|
|
...props
|
2025-12-03 21:56:50 +00:00
|
|
|
}) => {
|
|
|
|
|
return (
|
2026-01-16 01:11:21 +00:00
|
|
|
<DesignSystemTextarea
|
2025-12-03 21:56:50 +00:00
|
|
|
className={cn(
|
2026-01-16 01:11:21 +00:00
|
|
|
error && 'border-kodo-red focus-visible:border-kodo-red',
|
2025-12-13 02:34:34 +00:00
|
|
|
className,
|
2025-12-03 21:56:50 +00:00
|
|
|
)}
|
|
|
|
|
{...props}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-16 01:11:21 +00:00
|
|
|
interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'onChange'> {
|
2025-12-03 21:56:50 +00:00
|
|
|
error?: boolean;
|
|
|
|
|
options: Array<{ value: string; label: string }>;
|
2026-01-16 01:11:21 +00:00
|
|
|
placeholder?: string;
|
|
|
|
|
onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-13 02:34:34 +00:00
|
|
|
export const Select: React.FC<SelectProps> = ({
|
|
|
|
|
error = false,
|
|
|
|
|
className,
|
2025-12-03 21:56:50 +00:00
|
|
|
options,
|
2026-01-16 01:11:21 +00:00
|
|
|
value,
|
|
|
|
|
onChange,
|
|
|
|
|
placeholder,
|
|
|
|
|
disabled,
|
|
|
|
|
'aria-label': ariaLabel,
|
|
|
|
|
'aria-labelledby': ariaLabelledBy,
|
2025-12-03 21:56:50 +00:00
|
|
|
}) => {
|
2026-01-16 01:11:21 +00:00
|
|
|
const selectValue = value as string | undefined;
|
|
|
|
|
const handleChange = (newValue: string | string[]) => {
|
|
|
|
|
if (onChange) {
|
|
|
|
|
const event = {
|
|
|
|
|
target: { value: Array.isArray(newValue) ? newValue[0] : newValue },
|
|
|
|
|
} as React.ChangeEvent<HTMLSelectElement>;
|
|
|
|
|
onChange(event);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
return (
|
2026-01-16 01:11:21 +00:00
|
|
|
<DesignSystemSelect
|
|
|
|
|
options={options.map((opt) => ({ value: opt.value, label: opt.label }))}
|
|
|
|
|
value={selectValue}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
placeholder={placeholder || 'Select an option...'}
|
2025-12-03 21:56:50 +00:00
|
|
|
className={cn(
|
2026-01-16 01:11:21 +00:00
|
|
|
error && 'border-kodo-red',
|
2025-12-13 02:34:34 +00:00
|
|
|
className,
|
2025-12-03 21:56:50 +00:00
|
|
|
)}
|
2026-01-16 01:11:21 +00:00
|
|
|
aria-label={ariaLabel}
|
|
|
|
|
aria-labelledby={ariaLabelledBy}
|
|
|
|
|
/>
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
};
|