veza/apps/web/src/components/ui/LoadingState.tsx
senke a7ccd06042 refactor: LoadingState delegates all spinner rendering to LoadingSpinner
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 22:23:16 +01:00

196 lines
4.9 KiB
TypeScript

import React from 'react';
import { cn } from '@/lib/utils';
import { LoadingSpinner } from './loading-spinner';
/**
* CRITIQUE FIX #20: Composant LoadingState standardisé pour uniformiser les états de chargement
*
* Ce composant fournit une interface cohérente pour afficher les états de chargement
* à travers toute l'application, améliorant l'UX et la cohérence visuelle.
*/
export interface LoadingStateProps {
/**
* Si true, affiche l'état de chargement
*/
isLoading?: boolean;
/**
* Type de chargement à afficher
* - 'spinner': Spinner centré avec texte optionnel (par défaut)
* - 'inline': Spinner inline avec texte à côté
* - 'skeleton': Skeleton loader pour le contenu
* - 'minimal': Spinner minimal sans conteneur
*/
variant?: 'spinner' | 'inline' | 'skeleton' | 'minimal';
/**
* Taille du spinner
*/
size?: 'sm' | 'md' | 'lg';
/**
* Texte à afficher pendant le chargement
*/
text?: string;
/**
* Message à afficher si aucun texte n'est fourni
*/
defaultText?: string;
/**
* Classes CSS personnalisées
*/
className?: string;
/**
* Contenu à afficher quand isLoading est false
*/
children?: React.ReactNode;
/**
* Si true, affiche un skeleton loader au lieu d'un spinner
* (utilisé avec variant='skeleton')
*/
showSkeleton?: boolean;
}
/**
* LoadingState - Composant standardisé pour les états de chargement
*
* CRITIQUE FIX #20: Standardise tous les états de chargement dans l'application
*
* @example
* ```tsx
* // Spinner centré (par défaut)
* <LoadingState isLoading={isLoading} text="Chargement des données..." />
*
* // Spinner inline
* <LoadingState isLoading={isLoading} variant="inline" text="Sauvegarde..." />
*
* // Skeleton loader
* <LoadingState isLoading={isLoading} variant="skeleton">
* <div>Contenu à charger</div>
* </LoadingState>
*
* // Avec contenu conditionnel
* <LoadingState isLoading={isLoading} text="Chargement...">
* <div>Contenu chargé</div>
* </LoadingState>
* ```
*/
export function LoadingState({
isLoading = false,
variant = 'spinner',
size = 'md',
text,
defaultText = 'Chargement...',
className,
children,
showSkeleton = false,
}: LoadingStateProps) {
// Si pas de chargement, afficher le contenu
if (!isLoading && !showSkeleton) {
return <>{children}</>;
}
const displayText = text || defaultText;
// Variant: spinner (par défaut) - Spinner centré avec texte
if (variant === 'spinner') {
return (
<div
className={cn('flex flex-col items-center justify-center', className)}
>
<LoadingSpinner size={size} text={displayText} />
</div>
);
}
// Variant: inline - Spinner avec texte à côté (delegates to LoadingSpinner inline)
if (variant === 'inline') {
return (
<div className={cn('flex items-center gap-2', className)}>
<LoadingSpinner inline size={size} variant="muted" aria-label={displayText} />
{displayText && (
<span className="text-sm text-muted-foreground dark:text-muted-foreground">
{displayText}
</span>
)}
</div>
);
}
// Variant: skeleton - Skeleton loader pour le contenu
if (variant === 'skeleton' || showSkeleton) {
return (
<div className={cn('animate-pulse space-y-4', className)}>
{children || (
<>
<div className="h-4 bg-muted rounded w-3/4" />
<div className="h-4 bg-muted rounded w-1/2" />
<div className="h-4 bg-muted rounded w-5/6" />
</>
)}
</div>
);
}
// Variant: minimal - Spinner minimal sans conteneur (delegates to LoadingSpinner inline)
return (
<span role="status" className={className}>
<LoadingSpinner inline size={size} variant="muted" aria-label={displayText} />
</span>
);
}
/**
* LoadingStateWrapper - Wrapper pour afficher un état de chargement autour du contenu
*
* CRITIQUE FIX #20: Wrapper pratique pour les composants qui chargent des données
*
* @example
* ```tsx
* <LoadingStateWrapper isLoading={isLoading} text="Chargement des utilisateurs...">
* <UserList users={users} />
* </LoadingStateWrapper>
* ```
*/
export interface LoadingStateWrapperProps
extends Omit<LoadingStateProps, 'children'> {
/**
* Contenu à afficher quand isLoading est false
*/
children: React.ReactNode;
/**
* Variant à utiliser pour l'état de chargement
*/
loadingVariant?: LoadingStateProps['variant'];
}
export function LoadingStateWrapper({
isLoading,
children,
loadingVariant = 'spinner',
text,
defaultText,
className,
size,
}: LoadingStateWrapperProps) {
if (isLoading) {
return (
<LoadingState
isLoading={true}
variant={loadingVariant}
text={text}
defaultText={defaultText}
className={className}
size={size}
/>
);
}
return <>{children}</>;
}