()
| 72 | // ─── PresenceAvatars ────────────────────────────────────────────────────────── |
| 73 | |
| 74 | export function PresenceAvatars() { |
| 75 | const ctx = useCollaborationContextOptional(); |
| 76 | if (!ctx) return null; |
| 77 | |
| 78 | const { isConnected, otherUsers, currentUser } = ctx; |
| 79 | // Show at most 4 avatars + overflow badge |
| 80 | const MAX_VISIBLE = 4; |
| 81 | const allUsers = [currentUser, ...otherUsers]; |
| 82 | const visible = allUsers.slice(0, MAX_VISIBLE); |
| 83 | const overflow = allUsers.length - MAX_VISIBLE; |
| 84 | |
| 85 | return ( |
| 86 | <div className="flex items-center gap-2"> |
| 87 | {/* Connection indicator */} |
| 88 | <div className="flex items-center gap-1.5"> |
| 89 | {isConnected ? ( |
| 90 | <Wifi className="w-3.5 h-3.5 text-green-400" /> |
| 91 | ) : ( |
| 92 | <WifiOff className="w-3.5 h-3.5 text-surface-500 animate-pulse" /> |
| 93 | )} |
| 94 | <span className="text-xs text-surface-500 hidden sm:inline"> |
| 95 | {isConnected |
| 96 | ? `${allUsers.length} online` |
| 97 | : "Reconnecting…"} |
| 98 | </span> |
| 99 | </div> |
| 100 | |
| 101 | {/* Stacked avatars */} |
| 102 | <div className="flex items-center"> |
| 103 | <AnimatePresence> |
| 104 | {visible.map((user, i) => ( |
| 105 | <motion.div |
| 106 | key={user.id} |
| 107 | initial={{ opacity: 0, scale: 0.5, x: -8 }} |
| 108 | animate={{ opacity: 1, scale: 1, x: 0 }} |
| 109 | exit={{ opacity: 0, scale: 0.5 }} |
| 110 | transition={{ duration: 0.2, delay: i * 0.04 }} |
| 111 | style={{ zIndex: visible.length - i, marginLeft: i === 0 ? 0 : -8 }} |
| 112 | > |
| 113 | <UserAvatar |
| 114 | name={user.id === currentUser.id ? `${user.name} (you)` : user.name} |
| 115 | color={user.color} |
| 116 | avatar={user.avatar} |
| 117 | role={user.role} |
| 118 | /> |
| 119 | </motion.div> |
| 120 | ))} |
| 121 | </AnimatePresence> |
| 122 | |
| 123 | {overflow > 0 && ( |
| 124 | <div |
| 125 | className={cn( |
| 126 | "w-7 h-7 rounded-full -ml-2 z-0 flex items-center justify-center", |
| 127 | "bg-surface-700 border-2 border-surface-900 text-[10px] font-medium text-surface-300" |
| 128 | )} |
| 129 | > |
| 130 | +{overflow} |
| 131 | </div> |
nothing calls this directly
no test coverage detected