veza/apps/web/src/components/ui/LoadingState.tsx
senke 7113d35a4a ui(tokens): complete text-kodo-content-dim → text-muted-foreground migration (52 files)
Eliminate all remaining text-kodo-content-dim from user-facing source files.
This legacy token (hardcoded Gray-400) is now fully replaced by the
theme-aware text-muted-foreground token across UI primitives, settings,
social features, playlists, modals, inventory, and admin views.

Only story files (.stories.tsx) retain the old token for reference.
Total migration: ~345 instances across 87 files (this + previous commit).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 00:04:51 +01:00

214 lines
5.2 KiB
TypeScript

import React from 'react';
import { cn } from '@/lib/utils';
import { LoadingSpinner } from './loading-spinner';
import { Loader2 } from 'lucide-react';
/**
* 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é
if (variant === 'inline') {
const sizeClasses = {
sm: 'h-4 w-4',
md: 'h-5 w-5',
lg: 'h-6 w-6',
};
return (
<div className={cn('flex items-center gap-2', className)}>
<Loader2
className={cn('animate-spin text-muted-foreground', sizeClasses[size])}
aria-hidden="true"
/>
{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-kodo-slate dark:bg-kodo-steel rounded w-3/4"></div>
<div className="h-4 bg-kodo-slate dark:bg-kodo-steel rounded w-1/2"></div>
<div className="h-4 bg-kodo-slate dark:bg-kodo-steel rounded w-5/6"></div>
</>
)}
</div>
);
}
// Variant: minimal - Spinner minimal sans conteneur
const sizeClasses = {
sm: 'h-4 w-4',
md: 'h-5 w-5',
lg: 'h-6 w-6',
};
return (
<Loader2
className={cn('animate-spin text-muted-foreground', sizeClasses[size], className)}
aria-label={displayText}
role="status"
/>
);
}
/**
* 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}</>;
}