veza/apps/web/src/components/ui/avatar.tsx
senke bfdc785ccc refactor(tokens): complete design token migration to semantic system
Sprint 3.1 — Default colors → semantic (~15 files, ~99 replacements):
- lime-500 → success, red-500 → destructive, cyan-500 → primary

Sprint 3.2 — Hardcoded colors → semantic (~13 files, ~99 replacements):
- text-white → text-foreground, bg-black → bg-background, bg-white → bg-card

Sprint 3.3 — Legacy kodo-* → semantic (~27 files, ~122 replacements):
- bg-kodo-ink → bg-card, bg-kodo-void → bg-background, text-kodo-steel → text-muted-foreground
- Preserved kodo-cyan/magenta/lime/gold palette accents and gradients

Sprint 3.4 — Arbitrary values → Tailwind scale (5 replacements):
- min-h-[600px] → min-h-layout-page, min-h-[400px] → min-h-layout-page-sm
- left-[50%] → left-1/2, min-h-[80px] → min-h-20, min-h-[40px] → min-h-10

Sprint 3.5 — Border-radius standardization (4 replacements):
- Modal/dialog skeletons: rounded-lg → rounded-xl (convention)

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

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-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 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-card border border-border flex items-center justify-center relative`,
)}
>
{src ? (
<img src={src} alt={alt} className="w-full h-full object-cover" />
) : (
<span className="font-bold text-muted-foreground">{initials}</span>
)}
</div>
{status && (
<span
className={cn(
`absolute bottom-0 right-0 rounded-full border-background`,
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>;
};