import { forwardRef, useEffect, useMemo, type ReactNode } from 'react'; import { Image, Text, View, type ImageProps, type TextProps, type ViewProps } from 'react-native'; import { AvatarRoot as AvatarRootPrimitive, Slot, dataAttributes, mergeDataAttributes, useAvatarContext, type IAvatarImageProps, type IAvatarProps, } from '@cdx-ui/primitives'; import { cn, ParentContext, useParentContext, useStyleContext } from '@cdx-ui/utils'; import { Icon as IconComponent, type IconProps } from '../Icon'; import { type AvatarVariantProps, avatarBadgeVariants, avatarIconVariants, avatarTextVariants, avatarImageVariants, avatarRootVariants, } from './styles'; const SCOPE = 'AVATAR'; const useAvatarStyleContext = () => useStyleContext(SCOPE) as AvatarVariantProps; // ============================================================================= // UTILITIES // ============================================================================= function getInitials(name: string): string { const trimmed = name.trim(); const words = trimmed.split(/\s+/); if (words.length === 1 && trimmed.length <= 2) { return trimmed.toUpperCase(); } let result = ''; for (const word of words) { if (word.length > 0) { result += word.charAt(0); } if (result.length >= 2) break; } return result.toUpperCase(); } // ============================================================================= // AVATAR ROOT // ============================================================================= export interface AvatarProps extends ViewProps, IAvatarProps, AvatarVariantProps { className?: string; children?: ReactNode; } const AvatarRoot = forwardRef( ({ size = 'md', className, children, style, ...props }, ref) => { const computedClassName = cn(avatarRootVariants({ size }), className); const parent = useParentContext(); const ctx = useMemo(() => ({ ...parent, [SCOPE]: { size } }), [parent, size]); return ( {children} ); }, ); AvatarRoot.displayName = 'Avatar'; // ============================================================================= // AVATAR IMAGE // ============================================================================= export interface AvatarImageProps extends Omit, IAvatarImageProps { asChild?: boolean; className?: string; } const AvatarImage = forwardRef( ({ asChild, className, style, src, source, onLoad, onError, ...props }, ref) => { const { imageError, setImageLoaded, setImageError } = useAvatarContext(); const computedClassName = cn(avatarImageVariants(), className); const resolvedSource = useMemo(() => (src ? { uri: src } : source), [src, source]); useEffect(() => { if (!resolvedSource) { setImageError(true); } else { setImageError(false); setImageLoaded(false); } }, [resolvedSource, setImageError, setImageLoaded]); if (!resolvedSource || imageError) { return null; } const Comp = asChild ? Slot : Image; return ( { setImageLoaded(true); if (onLoad) (onLoad as (e: unknown) => void)(e); }} onError={(e: unknown) => { setImageError(true); if (onError) (onError as (e: unknown) => void)(e); }} {...props} {...mergeDataAttributes(props, dataAttributes({ slot: 'avatar-image' }))} /> ); }, ); AvatarImage.displayName = 'Avatar.Image'; // ============================================================================= // AVATAR TEXT // ============================================================================= export interface AvatarTextProps extends TextProps { asChild?: boolean; className?: string; children?: ReactNode; } const AvatarText = forwardRef( ({ asChild, className, children, style, ...props }, ref) => { const { size } = useAvatarStyleContext(); const { imageLoaded, imageError } = useAvatarContext(); const visible = !imageLoaded || imageError; const computedClassName = cn(avatarTextVariants({ size }), className); const text = typeof children === 'string' ? getInitials(children) : children; const Comp = asChild ? Slot : Text; return ( {text} ); }, ); AvatarText.displayName = 'Avatar.Text'; // ============================================================================= // AVATAR ICON // ============================================================================= export interface AvatarIconProps extends Omit { asChild?: boolean; className?: string; } const AvatarIcon = ({ asChild, className, style, ...props }: AvatarIconProps) => { const { size } = useAvatarStyleContext(); const { imageLoaded, imageError } = useAvatarContext(); const visible = !imageLoaded || imageError; const computedClassName = cn(avatarIconVariants({ size }), className); if (asChild) { return ( ); } return ( ); }; AvatarIcon.displayName = 'Avatar.Icon'; // ============================================================================= // AVATAR BADGE // ============================================================================= /** * @deprecated Use the generic `Badge` component instead. * Wrap your `Avatar` in `` for equivalent behavior. */ export interface AvatarBadgeProps extends ViewProps { asChild?: boolean; className?: string; children?: ReactNode; } /** * @deprecated Use the generic `Badge` component instead. * Wrap your `Avatar` in `` for equivalent behavior. */ const AvatarBadge = forwardRef( ({ asChild, className, children, style, ...props }, ref) => { const { size } = useAvatarStyleContext(); const computedClassName = cn(avatarBadgeVariants({ size }), className); const Comp = asChild ? Slot : View; return ( {children} ); }, ); AvatarBadge.displayName = 'Avatar.Badge'; // ============================================================================= // COMPOUND COMPONENT // ============================================================================= type AvatarCompoundComponent = typeof AvatarRoot & { Image: typeof AvatarImage; Text: typeof AvatarText; Icon: typeof AvatarIcon; Badge: typeof AvatarBadge; }; export const Avatar = Object.assign(AvatarRoot, { Image: AvatarImage, Text: AvatarText, Icon: AvatarIcon, Badge: AvatarBadge, }) as AvatarCompoundComponent; export type { AvatarVariantProps };