veza/apps/web/src/components/ui/avatar.tsx

204 lines
4.6 KiB
TypeScript

import React from 'react';
import { cn } from '@/lib/utils';
/**
* 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';
/**
* Classes CSS personnalisées
*/
className?: string;
/**
* Fonction appelée lors du clic sur l'avatar
*/
onClick?: () => void;
}
/**
* Avatar - Composant d'avatar avec design system Kodo
*
* 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 src="/avatar.jpg" alt="John Doe" />
*
* // Avatar avec initiales
* <Avatar fallback="John Doe" size="lg" />
*
* // Avatar avec statut
* <Avatar
* src="/avatar.jpg"
* alt="Jane Doe"
* status="online"
* />
* ```
*
* @component
* @param {AvatarProps} props - Propriétés du composant
* @returns {JSX.Element} Avatar circulaire avec image ou initiales
*/
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
(
{
src,
alt = 'Avatar',
fallback,
size = 'md',
status,
className = '',
onClick,
},
ref,
) => {
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-kodo-lime',
offline: 'bg-kodo-steel',
away: 'bg-kodo-gold',
idle: 'bg-kodo-gold',
busy: 'bg-kodo-red',
dnd: 'bg-kodo-red',
};
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 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);
return (
<div
ref={ref}
className={cn(
`relative inline-block`,
className,
onClick ? 'cursor-pointer' : '',
)}
onClick={onClick}
>
<div
className={cn(
`${sizeClasses[size]} rounded-full overflow-hidden bg-kodo-graphite border border-kodo-steel flex items-center justify-center relative`,
)}
>
{src ? (
<img src={src} alt={alt} className="w-full h-full object-cover" />
) : (
<span className="font-bold text-kodo-content-dim">{initials}</span>
)}
</div>
{status && (
<span
className={cn(
`absolute bottom-0 right-0 rounded-full border-kodo-void`,
statusColors[status],
statusSize[size],
)}
></span>
)}
</div>
);
},
);
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 <img src={src} alt={alt} className={className} />;
};
export const AvatarFallback: React.FC<{
children: React.ReactNode;
className?: string;
}> = ({ children, className }) => {
return <span className={className}>{children}</span>;
};
export const AvatarRoot: React.FC<{
children: React.ReactNode;
className?: string;
}> = ({ children, className }) => {
return <div className={className}>{children}</div>;
};