import { PencilSimple, User } from "@phosphor-icons/react"; import { useRef, type ChangeEvent, type ReactNode } from "react"; import { cn } from "../../lib/cn"; export type AvatarSize = "sm" | "md" | "lg"; export type AvatarEditError = { code: "file-too-large" | "invalid-file-type"; file: File; message: string; }; export type AvatarProps = { accept?: string; alt?: string; asset?: ReactNode; capture?: boolean | "user" | "environment"; className?: string; disabled?: boolean; editable?: boolean; editLabel?: string; imageClassName?: string; initials?: string; inputName?: string; maxFileSize?: number; name?: string; onEdit?: (file: File) => void; onEditError?: (error: AvatarEditError) => void; size?: AvatarSize; src?: string; }; const avatarSizeClasses: Record = { sm: "h-8 w-8 text-xs", md: "h-[var(--uhuru-control-height)] w-[var(--uhuru-control-height)] text-sm", lg: "h-14 w-14 text-base", }; const editButtonSizes: Record = { sm: { button: 16, icon: 9 }, md: { button: 18, icon: 11 }, lg: { button: 20, icon: 13 }, }; function initialsFromName(name?: string) { if (!name) { return ""; } return name .trim() .split(/\s+/) .slice(0, 2) .map((part) => part.charAt(0)) .join("") .toUpperCase(); } function fileMatchesAccept(file: File, accept: string) { if (!accept.trim()) { return true; } return accept .split(",") .map((value) => value.trim().toLowerCase()) .filter(Boolean) .some((value) => { if (value.startsWith(".")) { return file.name.toLowerCase().endsWith(value); } if (value.endsWith("/*")) { return file.type.toLowerCase().startsWith(value.slice(0, -1)); } return file.type.toLowerCase() === value; }); } export function Avatar({ accept = "image/*", alt, asset, capture, className, disabled = false, editable = false, editLabel = "Change avatar", imageClassName, initials, inputName, maxFileSize, name, onEdit, onEditError, size = "md", src, }: AvatarProps) { const inputRef = useRef(null); const fallbackInitials = initials?.trim() || initialsFromName(name); function handleFileChange(event: ChangeEvent) { const file = event.target.files?.[0]; event.target.value = ""; if (!file) { return; } if (!fileMatchesAccept(file, accept)) { onEditError?.({ code: "invalid-file-type", file, message: `The selected file does not match the accepted types: ${accept}.`, }); return; } if (maxFileSize !== undefined && file.size > maxFileSize) { onEditError?.({ code: "file-too-large", file, message: `The selected file exceeds the ${maxFileSize}-byte size limit.`, }); return; } onEdit?.(file); } return ( {asset ? ( {asset} ) : src ? ( {(alt ) : fallbackInitials ? ( fallbackInitials ) : ( {editable ? ( <> ) : null} ); }