"use client"; /** * Figma: "Asset / Avatar" (18628:32840). Circular (or rounded-square) image * with initials or a person mark when `src` is missing or fails to load. */ import { memo, useLayoutEffect, useRef, useState } from "react"; import type { AvatarShapeValue, AvatarSizeValue, AvatarVariantValue, } from "../../../../lib/propNormalization"; export type { AvatarShapeValue, AvatarSizeValue, AvatarVariantValue }; export interface AvatarProps { src?: string; alt: string; size?: AvatarSizeValue; /** 1–2 characters, or a name (first letters of the first two words). */ initials?: string; shape?: AvatarShapeValue; /** Fallback chrome: cream fill (default) or yellow outline. */ variant?: AvatarVariantValue; className?: string; onClick?: () => void; } const SIZE_CLASS: Record = { small: "h-[var(--spacing-scale-016)] w-[var(--spacing-scale-016)] text-xx-small-label", medium: "h-[var(--spacing-scale-018)] w-[var(--spacing-scale-018)] text-xx-small-label", large: "h-[var(--spacing-scale-024)] w-[var(--spacing-scale-024)] text-xx-small-label", xlarge: "h-[var(--spacing-scale-032)] w-[var(--spacing-scale-032)] text-x-small-label", }; function formatAvatarInitials(value: string): string { const trimmed = value.trim(); if (!trimmed) return ""; const words = trimmed.split(/\s+/); if (words.length >= 2) { return `${words[0]?.[0] ?? ""}${words[1]?.[0] ?? ""}`.toUpperCase(); } return trimmed.slice(0, 2).toUpperCase(); } function AvatarPersonIcon() { return ( ); } const Avatar = memo( ({ src, alt, size = "small", initials, shape = "circle", variant = "filled", className = "", onClick, }) => { const imgRef = useRef(null); const [failed, setFailed] = useState(false); const [loaded, setLoaded] = useState(false); const trimmedSrc = src?.trim() ?? ""; const hasSrc = trimmedSrc.length > 0; const showImage = hasSrc && !failed; const mark = initials ? formatAvatarInitials(initials) : ""; const decorative = alt.trim().length === 0; useLayoutEffect(() => { setFailed(false); const img = imgRef.current; setLoaded(Boolean(img?.complete && img.naturalWidth > 0)); }, [trimmedSrc]); const radiusClass = shape === "rounded-square" ? "rounded-[var(--measures-radius-200,8px)]" : "rounded-[var(--radius-measures-radius-full)]"; const fallbackChrome = variant === "outlined" ? "border-[1.5px] border-solid border-[var(--color-content-default-brand-primary)] bg-transparent text-[var(--color-content-default-brand-primary)]" : "bg-[var(--color-surface-invert-brand-primary)] text-[var(--color-content-default-primary)]"; // White 30% ring on small photos: no DS token (opacity scale is black-alpha). const smallImageBorder = size === "small" && showImage && loaded ? "border-[1.5px] border-solid border-[#FFFFFF4D]" : ""; const interactiveClass = onClick ? `cursor-pointer appearance-none bg-transparent p-0 ${smallImageBorder ? "" : "border-0"} transition-opacity hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-border-invert-primary)]` : ""; const shellClass = `relative inline-flex box-border shrink-0 items-center justify-center overflow-hidden ${SIZE_CLASS[size]} ${radiusClass} ${smallImageBorder} ${interactiveClass} ${className}`.trim(); const showFallback = !showImage; const showSkeleton = showImage && !loaded; const visual = ( <> {showFallback ? ( {mark ? mark : } ) : null} {showSkeleton ? ( ) : null} {showImage ? ( /* eslint-disable-next-line @next/next/no-img-element -- avatar image from URL */ {onClick setLoaded(true)} onError={() => { setFailed(true); setLoaded(false); }} /> ) : null} ); if (onClick) { return ( ); } return ( {visual} ); }, ); Avatar.displayName = "Avatar"; export default Avatar;