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>
204 lines
4.6 KiB
TypeScript
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>;
|
|
};
|