import React, { useState } from 'react'; import { cn } from '@/lib/utils'; /** * Badge configuration for the Avatar component */ interface AvatarBadge { /** * Number to display in the badge (hidden if dot is true) */ count?: number; /** * Color variant of the badge * @default 'primary' */ color?: 'primary' | 'destructive' | 'success'; /** * Show as a small dot instead of a numbered badge * @default false */ dot?: boolean; } /** * AvatarProps - Propriétés du composant Avatar * * @interface AvatarProps */ interface AvatarProps { /** * URL de l'image de l'avatar */ src?: string; /** * Texte alternatif de l'avatar * * @default 'Avatar' */ alt?: string; /** * Texte de fallback utilisé pour générer les initiales si pas d'image */ fallback?: string; /** * Taille de l'avatar * * - `xs`: 6x6 (24px) * - `sm`: 8x8 (32px) * - `md`: 10x10 (40px) - par défaut * - `lg`: 12x12 (48px) * - `xl`: 16x16 (64px) * - `2xl`: 24x24 (96px) * - `3xl`: 32x32 (128px) * * @default 'md' */ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'; /** * Statut de présence à afficher (indicateur coloré) * * - `online`: Vert (lime) * - `offline`: Gris * - `away`: Or * - `idle`: Or * - `busy`: Rouge * - `dnd`: Rouge (do not disturb) */ status?: 'online' | 'offline' | 'away' | 'busy' | 'idle' | 'dnd'; /** * Badge overlay configuration */ badge?: AvatarBadge; /** * Classes CSS personnalisées */ className?: string; /** * Fonction appelée lors du clic sur l'avatar */ onClick?: () => void; } /** * Avatar - Composant d'avatar avec design system SUMI * * Composant d'avatar pour afficher des images de profil ou des initiales. * Supporte les statuts de présence et plusieurs tailles. * * @example * ```tsx * // Avatar avec image * * * // Avatar avec initiales * * * // Avatar avec statut * * * // Avatar avec badge * * ``` * * @component * @param {AvatarProps} props - Propriétés du composant * @returns {JSX.Element} Avatar circulaire avec image ou initiales */ export const Avatar = React.forwardRef( ( { src, alt = 'Avatar', fallback, size = 'md', status, badge, className = '', onClick, }, ref, ) => { const [isLoaded, setIsLoaded] = useState(false); const [hasError, setHasError] = useState(false); const sizeClasses = { xs: 'w-6 h-6 text-[10px]', sm: 'w-8 h-8 text-xs', md: 'w-10 h-10 text-sm', lg: 'w-12 h-12 text-base', xl: 'w-16 h-16 text-lg', '2xl': 'w-24 h-24 text-xl', '3xl': 'w-32 h-32 text-2xl', }; const statusColors = { online: 'bg-success', offline: 'bg-muted', away: 'bg-warning', idle: 'bg-warning', busy: 'bg-destructive', dnd: 'bg-destructive', }; const statusSize = { xs: 'w-1.5 h-1.5 border', sm: 'w-2 h-2 border', md: 'w-2.5 h-2.5 border-2', lg: 'w-3 h-3 border-2', xl: 'w-4 h-4 border-2', '2xl': 'w-5 h-5 border-4', '3xl': 'w-6 h-6 border-4', }; const badgeColors = { primary: 'bg-primary text-primary-foreground', destructive: 'bg-destructive text-destructive-foreground', success: 'bg-success text-success-foreground', }; const badgeSizeClasses = { xs: 'w-2.5 h-2.5 text-[6px]', sm: 'w-3 h-3 text-[7px]', md: 'w-4 h-4 text-[8px]', lg: 'w-4.5 h-4.5 text-[9px]', xl: 'w-5 h-5 text-[10px]', '2xl': 'w-6 h-6 text-xs', '3xl': 'w-8 h-8 text-sm', }; const badgeDotSizeClasses = { xs: 'w-1.5 h-1.5', sm: 'w-2 h-2', md: 'w-2.5 h-2.5', lg: 'w-3 h-3', xl: 'w-3.5 h-3.5', '2xl': 'w-4 h-4', '3xl': 'w-5 h-5', }; const getInitials = (name?: string): string => { if (!name) return '?'; const parts = name.trim().split(' '); if (parts.length >= 2) { return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); } return name.substring(0, 2).toUpperCase(); }; const initials = getInitials(fallback || alt); const showFallback = !src || hasError; return (
{showFallback ? ( {initials} ) : ( <> {!isLoaded && ( {status && ( )} {badge && ( {!badge.dot && badge.count != null ? badge.count : null} )}
); }, ); Avatar.displayName = 'Avatar'; // Compatibility exports (for existing code using AvatarImage, AvatarFallback) export const AvatarImage: React.FC<{ src?: string; alt?: string; className?: string; }> = ({ src, alt, className }) => { if (!src) return null; return {alt}; }; export const AvatarFallback: React.FC<{ children: React.ReactNode; className?: string; }> = ({ children, className }) => { return {children}; }; export const AvatarRoot: React.FC<{ children: React.ReactNode; className?: string; }> = ({ children, className }) => { return
{children}
; };