import { UX4GColors } from '../../foundation/colors'; import React from 'react'; import { View, Text, Image, Pressable, StyleSheet, StyleProp, ViewStyle, TextStyle, } from 'react-native'; import { useUx4gTheme } from '../../theme/Ux4gThemeContext'; import { Ux4gIcons } from '../../foundation/icons'; import { Ux4gBadge } from '../badge/Badge'; let Svg: any = null; let Path: any = null; let Circle: any = null; let Rect: any = null; try { const RNSvg = require('react-native-svg'); Svg = RNSvg.Svg || RNSvg.default; Path = RNSvg.Path; Circle = RNSvg.Circle; Rect = RNSvg.Rect; } catch (e) { // react-native-svg not present; fallback to text/emoji } export type Ux4gAvatarSize = 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl' | 'xxxl'; const AVATAR_SIZE_MAP: Record = { xs: { size: 24, fontSize: 10, iconSize: 14 }, s: { size: 32, fontSize: 14, iconSize: 16 }, m: { size: 40, fontSize: 16, iconSize: 20 }, l: { size: 48, fontSize: 20, iconSize: 24 }, xl: { size: 64, fontSize: 24, iconSize: 32 }, xxl: { size: 80, fontSize: 32, iconSize: 40 }, xxxl: { size: 120, fontSize: 48, iconSize: 60 }, }; function PersonIcon({ size, color }: { size: number; color: string }) { if (Svg && Path && Circle) { return ( ); } return 👤; } function EditIcon({ size, color }: { size: number; color: string }) { if (Svg && Path) { return ( ); } return ✏️; } function CameraIcon({ size, color }: { size: number; color: string }) { if (Svg && Path && Rect) { return ( ); } return 📷; } function RemoveIcon({ size, color }: { size: number; color: string }) { if (Svg && Path) { return ( ); } return −; } const ICON_RENDERERS = { person: PersonIcon, edit: EditIcon, camera: CameraIcon, remove: RemoveIcon, } as const; function getBadgeVariantIndicatorSize(size: Ux4gAvatarSize): number { const map: Record = { xs: 12, s: 14, m: 16, l: 18, xl: 24, xxl: 30, xxxl: 42, }; return map[size] ?? 16; } function getStatusIndicatorSize(size: Ux4gAvatarSize): number { const map: Record = { xs: 10, s: 12, m: 14, l: 16, xl: 20, xxl: 24, xxxl: 34, }; return map[size] ?? 14; } function getStatusColor( variant: Ux4gStatusVariant, isDark: boolean, colors: { success?: string; onSurface?: string; error?: string; warning?: string } ): string { switch (variant) { case 'online': case 'success': return colors.success ?? UX4GColors.green500; case 'offline': { const base = colors.onSurface ?? UX4GColors.neutral500; return isDark ? `${base}80` : `${base}80`; } case 'busy': case 'error': return colors.error ?? UX4GColors.red500; case 'warning': return colors.warning ?? UX4GColors.orange500; } } function computeCircleOffset(avatarSize: number, indicatorSize: number): number { const r = avatarSize / 2; const circleOffset = r - r / 1.41421356; return circleOffset - indicatorSize / 2; } // ─── Ux4gAvatar ────────────────────────────────────────────────────────────── export type Ux4gAvatarShape = 'circle' | 'rounded' | 'square'; export interface Ux4gAvatarProps { size?: Ux4gAvatarSize; shape?: Ux4gAvatarShape; imageUrl?: string; initials?: string; icon?: React.ReactNode; containerColor?: string; contentColor?: string; iconColor?: string; containerStyle?: StyleProp; testID?: string; } export const Ux4gAvatar: React.FC = ({ size = 'm', shape = 'circle', imageUrl, initials, icon, containerColor, contentColor, iconColor, containerStyle, testID, }) => { const theme = useUx4gTheme(); const colors = theme.colors; const typography = theme.typography; const dims = AVATAR_SIZE_MAP[size] ?? AVATAR_SIZE_MAP.m; const resolvedPrimary = colors.primary; const finalContainerColor = containerColor ?? `${resolvedPrimary}1A`; const finalContentColor = contentColor ?? resolvedPrimary; const finalIconColor = iconColor ?? resolvedPrimary; const borderRadius = shape === 'circle' ? dims.size / 2 : shape === 'rounded' ? 8 : 0; const [imageFailed, setImageFailed] = React.useState(false); const renderContent = () => { if (imageUrl && !imageFailed) { return ( setImageFailed(true)} /> ); } if (initials && initials.length > 0) { const display = initials.substring(0, 2).toUpperCase(); const textStyle: TextStyle = { fontSize: dims.fontSize, fontWeight: '500', color: finalContentColor, textAlign: 'center', }; return ( {display} ); } const iconEl = icon ?? ; if (React.isValidElement(iconEl)) { return iconEl; } return {String(iconEl)}; }; return ( {renderContent()} ); }; // ─── Ux4gAvatarGroupItem ───────────────────────────────────────────────────── export interface Ux4gAvatarGroupItem { imageUrl?: string; initials?: string; icon?: React.ReactNode; onPress?: () => void; } // ─── Ux4gAvatarGroup ───────────────────────────────────────────────────────── export interface Ux4gAvatarGroupProps { items: Ux4gAvatarGroupItem[]; size?: Ux4gAvatarSize; maxLimit?: number; collapsed?: boolean; borderColor?: string; borderWidth?: number; onRemainingPress?: () => void; containerStyle?: StyleProp; testID?: string; } export const Ux4gAvatarGroup: React.FC = ({ items, size = 'm', maxLimit, collapsed = true, borderColor, borderWidth = 2, onRemainingPress, containerStyle, testID, }) => { const theme = useUx4gTheme(); const colors = theme.colors; const dims = AVATAR_SIZE_MAP[size] ?? AVATAR_SIZE_MAP.m; if (items.length === 0) return null; const resolvedBorderColor = borderColor ?? colors.surface; const actualLimit = maxLimit ?? items.length; const exceedsLimit = items.length > actualLimit; const visibleItems = exceedsLimit ? items.slice(0, actualLimit - 1) : items; const remainingCount = exceedsLimit ? items.length - (actualLimit - 1) : 0; const overlap = collapsed ? -(dims.size * 0.25) : 8; const renderAvatarWithBorder = (avatar: React.ReactElement) => { if (!collapsed) return avatar; return ( {avatar} ); }; const itemStyle = (idx: number) => { if (idx === 0) return undefined; return { marginLeft: overlap }; }; return ( {visibleItems.map((item, idx) => ( {renderAvatarWithBorder( )} ))} {remainingCount > 0 && ( {renderAvatarWithBorder( )} )} ); }; // ─── Profile Badge/Action Types ────────────────────────────────────────────── export type Ux4gProfileBadge = 'verified' | 'star' | 'admin'; export type Ux4gProfileAction = 'edit' | 'camera' | 'remove'; // ─── Ux4gProfileAvatar ─────────────────────────────────────────────────────── export interface Ux4gProfileAvatarProps { size?: Ux4gAvatarSize; shape?: Ux4gAvatarShape; imageUrl?: string; initials?: string; avatarIcon?: React.ReactNode; variant?: Ux4gProfileBadge | Ux4gProfileAction; badgeSize?: number; onVariantPress?: () => void; containerStyle?: StyleProp; testID?: string; } export const Ux4gProfileAvatar: React.FC = ({ size = 'm', shape = 'circle', imageUrl, initials, avatarIcon, variant, badgeSize, onVariantPress, containerStyle, testID, }) => { const dims = AVATAR_SIZE_MAP[size] ?? AVATAR_SIZE_MAP.m; const indicatorSize = badgeSize ?? getBadgeVariantIndicatorSize(size); const offset = computeCircleOffset(dims.size, indicatorSize); const renderVariant = () => { if (!variant) return null; if (variant === 'verified') { return ( {Ux4gIcons.verification({ size: indicatorSize + 4 })} {Ux4gIcons.verification({ size: indicatorSize })} ); } if (variant === 'star') { return ( {Ux4gIcons.star({ size: indicatorSize + 4 })} {Ux4gIcons.star({ size: indicatorSize })} ); } if (variant === 'admin') { const containerDims = indicatorSize + 4; return ( {Ux4gIcons.shield({ size: indicatorSize * 0.6, color: UX4GColors.white })} ); } const icon = ICON_RENDERERS[variant]; const containerDims = indicatorSize + 4; const bgColor = variant === 'remove' ? UX4GColors.red500 : UX4GColors.primary600; return ( {icon({ size: indicatorSize * 0.6, color: UX4GColors.white })} ); }; const variantWidget = renderVariant(); return ( {variantWidget && ( {variantWidget} )} ); }; // ─── Ux4gStatusVariant ─────────────────────────────────────────────────────── export type Ux4gStatusVariant = 'online' | 'offline' | 'busy' | 'success' | 'error' | 'warning'; // ─── Ux4gStatusAvatar ──────────────────────────────────────────────────────── export interface Ux4gStatusAvatarProps { size?: Ux4gAvatarSize; shape?: Ux4gAvatarShape; imageUrl?: string; initials?: string; avatarIcon?: React.ReactNode; variant?: Ux4gStatusVariant; statusSize?: number; containerStyle?: StyleProp; testID?: string; } export const Ux4gStatusAvatar: React.FC = ({ size = 'm', shape = 'circle', imageUrl, initials, avatarIcon, variant = 'online', statusSize, containerStyle, testID, }) => { const theme = useUx4gTheme(); const colors = theme.colors; const dims = AVATAR_SIZE_MAP[size] ?? AVATAR_SIZE_MAP.m; const indicatorSize = statusSize ?? getStatusIndicatorSize(size); const offset = computeCircleOffset(dims.size, indicatorSize); const statusColor = getStatusColor(variant, theme.isDark, colors); return ( ); }; const styles = StyleSheet.create({ stackContainer: { position: 'relative', alignSelf: 'flex-start', overflow: 'visible', }, variantPosition: { position: 'absolute', zIndex: 1, overflow: 'visible', }, });