veza/apps/web/src/components/ui/loading-spinner.tsx
2026-02-12 22:04:45 +01:00

142 lines
3 KiB
TypeScript

import { Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
/**
* LoadingSpinnerProps - Propriétés du composant LoadingSpinner
*
* @interface LoadingSpinnerProps
*/
interface LoadingSpinnerProps {
/**
* Taille du spinner
*
* - `sm`: Petit (h-4 w-4 en inline, h-4 w-4 en block)
* - `md`: Moyen (h-5 w-5 en inline, h-8 w-8 en block) - par défaut
* - `lg`: Grand (h-6 w-6 en inline, h-12 w-12 en block)
*
* @default 'md'
*/
size?: 'sm' | 'md' | 'lg';
/**
* Classes CSS personnalisées
*/
className?: string;
/**
* Texte à afficher sous le spinner (ignoré en mode inline)
*
* @example
* ```tsx
* <LoadingSpinner text="Chargement en cours..." />
* ```
*/
text?: string;
/**
* Mode inline pour boutons et éléments compacts.
* Rend uniquement l'icône sans conteneur.
*/
inline?: boolean;
/**
* Variant visuel (uniquement en mode inline)
*/
variant?: 'default' | 'muted' | 'white' | 'current';
/**
* Label d'accessibilité
*/
'aria-label'?: string;
}
/**
* LoadingSpinner - Composant de spinner de chargement
*
* Composant de spinner animé pour indiquer un état de chargement.
* Inclut un texte optionnel et supporte plusieurs tailles.
*
* @example
* ```tsx
* // Spinner simple
* <LoadingSpinner />
*
* // Spinner avec texte
* <LoadingSpinner text="Chargement..." />
*
* // Spinner de grande taille
* <LoadingSpinner size="lg" text="Traitement en cours" />
* ```
*
* @component
* @param {LoadingSpinnerProps} props - Propriétés du composant
* @returns {JSX.Element} Spinner animé avec texte optionnel
*/
const variantClasses = {
default: 'text-primary',
muted: 'text-muted-foreground',
white: 'text-foreground',
current: 'text-current',
} as const;
const blockSizeClasses = {
sm: 'h-4 w-4',
md: 'h-8 w-8',
lg: 'h-12 w-12',
};
const inlineSizeClasses = {
sm: 'h-4 w-4',
md: 'h-5 w-5',
lg: 'h-6 w-6',
};
export function LoadingSpinner({
size = 'md',
className,
text,
inline = false,
variant = 'default',
'aria-label': ariaLabel = 'Chargement en cours',
}: LoadingSpinnerProps) {
if (inline) {
return (
<>
<Loader2
className={cn(
'animate-spin',
inlineSizeClasses[size],
variantClasses[variant],
className,
)}
aria-hidden="true"
/>
<span className="sr-only">{ariaLabel}</span>
</>
);
}
return (
<div
className={cn(
'flex flex-col items-center justify-center min-h-48',
className,
)}
>
<div
className={cn(
'animate-spin rounded-full border-2 border-muted border-t-primary',
blockSizeClasses[size],
)}
role="status"
aria-label={ariaLabel}
>
<span className="sr-only">Chargement...</span>
</div>
{text && (
<p className="mt-2 text-sm text-muted-foreground dark:text-muted-foreground">{text}</p>
)}
</div>
);
}