import * as React from 'react'; import { cva } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const SIZES = { xs: 'w-6 h-6 text-[9px]', sm: 'w-7 h-7 text-[10px]', default: 'w-9 h-9 text-[12px]', lg: 'w-11 h-11 text-body-sm', xl: 'w-16 h-16 text-title', '2xl': 'w-20 h-20 text-title-lg', }; const avatarVariants = cva( 'relative inline-grid place-items-center shrink-0 overflow-hidden font-semibold select-none', { variants: { shape: { circle: 'rounded-full', rounded: 'rounded-xl' }, tone: { brand: 'bg-krow-blue text-white', navy: 'bg-krow-navy text-white', soft: 'bg-krow-blue-tint text-krow-blue', neutral: 'bg-surface-sunken text-ink-3', }, }, defaultVariants: { shape: 'circle', tone: 'brand' }, } ); /** First letters of the first two words — "Chef Antoine Dubois" → "CA". */ export function initialsFrom(name = '') { const words = String(name).trim().split(/\s+/).filter(Boolean); if (words.length === 0) return '?'; return words.slice(0, 2).map((w) => w[0]).join('').toUpperCase(); } /** * Avatar — a photo when one exists, initials otherwise. * * The app shows people constantly and most have no photo, so initials are a * first-class state rather than a fallback afterthought. A failed image load * degrades to initials instead of a broken-image icon. */ export const Avatar = React.forwardRef( ({ name, src, size = 'default', shape, tone, status, className, ...props }, ref) => { const [failed, setFailed] = React.useState(false); const showImage = src && !failed; // Reset when the subject changes, so a new src gets its own chance to load. React.useEffect(() => setFailed(false), [src]); return ( {showImage ? ( {name setFailed(true)} className="w-full h-full object-cover" /> ) : ( {initialsFrom(name)} )} {status && ( )} ); } ); Avatar.displayName = 'Avatar'; /** * AvatarGroup — overlapping avatars with a "+N" remainder, for showing several * people in a tight row (applicants on a position, endorsers on a skill). */ export function AvatarGroup({ people = [], max = 4, size = 'sm', className }) { const visible = people.slice(0, max); const overflow = people.length - visible.length; return (
{visible.map((person, i) => ( 0 && '-ml-2')} /> ))} {overflow > 0 && ( +{overflow} )}
); }