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 ? (
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 (