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>
214 lines
5.2 KiB
TypeScript
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}</>;
|
|
}
|