Composants UI
Documentation des composants UI de Company Manager.
Composants UI
Documentation complète des composants UI de Company Manager.
Composants de Base
Button
// components/ui/Button.tsx
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
icon?: React.ReactNode;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
loading,
icon,
children,
...props
}) => {
return (
<button
className={cn(
'inline-flex items-center justify-center',
'rounded-md font-medium transition-colors',
'focus:outline-none focus:ring-2',
'disabled:opacity-50 disabled:pointer-events-none',
variants[variant],
sizes[size]
)}
disabled={loading || props.disabled}
{...props}
>
{loading && <Spinner className="mr-2" />}
{icon && !loading && <span className="mr-2">{icon}</span>}
{children}
</button>
);
};Input
// components/ui/Input.tsx
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
hint?: string;
}
export const Input: React.FC<InputProps> = ({
label,
error,
hint,
...props
}) => {
return (
<div className="space-y-1">
{label && (
<label className="text-sm font-medium">
{label}
</label>
)}
<input
className={cn(
'w-full rounded-md border',
'px-3 py-2',
'focus:outline-none focus:ring-2',
error ? 'border-red-500' : 'border-gray-300'
)}
{...props}
/>
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
{hint && !error && (
<p className="text-sm text-gray-500">{hint}</p>
)}
</div>
);
};Composants de Données
DataTable
// components/ui/DataTable.tsx
interface DataTableProps<T> {
data: T[];
columns: Column<T>[];
loading?: boolean;
pagination?: {
page: number;
pageSize: number;
total: number;
onPageChange: (page: number) => void;
};
sorting?: {
sortBy: string;
sortOrder: 'asc' | 'desc';
onSort: (field: string) => void;
};
}
export const DataTable = <T extends {}>({
data,
columns,
loading,
pagination,
sorting,
}: DataTableProps<T>) => {
return (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr>
{columns.map(column => (
<th
key={column.key}
onClick={() => sorting?.onSort(column.key)}
className={cn(
'px-4 py-2 text-left',
sorting && 'cursor-pointer'
)}
>
{column.title}
{sorting?.sortBy === column.key && (
<SortIcon order={sorting.sortOrder} />
)}
</th>
))}
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={columns.length}>
<LoadingState />
</td>
</tr>
) : (
data.map((row, index) => (
<tr key={index}>
{columns.map(column => (
<td key={column.key} className="px-4 py-2">
{column.render
? column.render(row)
: row[column.key]}
</td>
))}
</tr>
))
)}
</tbody>
</table>
{pagination && (
<Pagination
page={pagination.page}
pageSize={pagination.pageSize}
total={pagination.total}
onChange={pagination.onPageChange}
/>
)}
</div>
);
};Form
// components/ui/Form.tsx
interface FormProps<T> extends UseFormReturn<T> {
onSubmit: SubmitHandler<T>;
children: React.ReactNode;
}
export const Form = <T extends {}>({
onSubmit,
children,
...form
}: FormProps<T>) => {
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{children}
</form>
</FormProvider>
);
};
// Exemple d'utilisation
const UserForm = () => {
const form = useForm<User>({
defaultValues: {
name: '',
email: '',
role: 'USER',
},
});
const onSubmit = async (data: User) => {
await createUser(data);
};
return (
<Form {...form} onSubmit={onSubmit}>
<Input name="name" label="Nom" />
<Input name="email" label="Email" type="email" />
<Select
name="role"
label="Rôle"
options={[
{ value: 'USER', label: 'Utilisateur' },
{ value: 'ADMIN', label: 'Administrateur' },
]}
/>
<Button type="submit">Créer</Button>
</Form>
);
};Composants de Feedback
Toast
// components/ui/Toast.tsx
import { toast } from 'sonner';
interface ToastProps {
title: string;
description?: string;
type?: 'success' | 'error' | 'warning' | 'info';
}
export const showToast = ({
title,
description,
type = 'info',
}: ToastProps) => {
toast[type](title, {
description,
duration: 5000,
});
};
// Exemple d'utilisation
const handleSave = async () => {
try {
await saveData();
showToast({
type: 'success',
title: 'Sauvegardé',
description: 'Les modifications ont été enregistrées.',
});
} catch (error) {
showToast({
type: 'error',
title: 'Erreur',
description: error.message,
});
}
};Dialog
// components/ui/Dialog.tsx
interface DialogProps {
open: boolean;
onClose: () => void;
title: string;
description?: string;
children: React.ReactNode;
}
export const Dialog: React.FC<DialogProps> = ({
open,
onClose,
title,
description,
children,
}) => {
return (
<DialogPrimitive.Root open={open} onOpenChange={onClose}>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="fixed inset-0 bg-black/50" />
<DialogPrimitive.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<div className="bg-white rounded-lg shadow-lg p-6 w-[32rem] max-w-full">
<DialogPrimitive.Title className="text-lg font-medium">
{title}
</DialogPrimitive.Title>
{description && (
<DialogPrimitive.Description className="mt-2 text-gray-500">
{description}
</DialogPrimitive.Description>
)}
<div className="mt-4">{children}</div>
</div>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
);
};Composants de Navigation
Tabs
// components/ui/Tabs.tsx
interface Tab {
key: string;
title: string;
content: React.ReactNode;
}
interface TabsProps {
tabs: Tab[];
defaultTab?: string;
onChange?: (key: string) => void;
}
export const Tabs: React.FC<TabsProps> = ({
tabs,
defaultTab,
onChange,
}) => {
const [activeTab, setActiveTab] = useState(defaultTab || tabs[0].key);
const handleChange = (key: string) => {
setActiveTab(key);
onChange?.(key);
};
return (
<div>
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
{tabs.map(tab => (
<button
key={tab.key}
onClick={() => handleChange(tab.key)}
className={cn(
'py-4 px-1 border-b-2 font-medium text-sm',
activeTab === tab.key
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
)}
>
{tab.title}
</button>
))}
</nav>
</div>
<div className="mt-4">
{tabs.find(tab => tab.key === activeTab)?.content}
</div>
</div>
);
};Bonnes Pratiques
Accessibilité
- Utilisation des attributs ARIA appropriés
- Support du clavier
- Contraste des couleurs suffisant
- Messages d'erreur explicites
Performance
- Code splitting des composants
- Lazy loading des images
- Optimisation des re-renders
- Utilisation de memo quand nécessaire
Maintenance
- Documentation des props
- Tests des composants
- Storybook pour le développement
- Thèmes et styles cohérents