/** * Overlapping avatar stack — mobile equivalent of web's * `packages/ui/components/ui/avatar.tsx` `AvatarGroup`. Mobile cannot import * the web component (sharing rules in apps/mobile/CLAUDE.md), so this is a * native re-implementation built on top of ActorAvatar. * * Dedupes input by `${type}:${id}` before slicing — multiple active tasks * from the same agent collapse to a single avatar (otherwise the stack * misrepresents how many distinct actors are involved). */ import { View } from "react-native"; import { ActorAvatar } from "@/components/ui/actor-avatar"; import { Text } from "@/components/ui/text"; export interface StackActor { type: "member" | "agent" | null | undefined; id: string | null | undefined; } interface Props { actors: StackActor[]; /** Max distinct avatars rendered before collapsing to `+N`. Default 3. */ max?: number; /** Avatar diameter in pt. Default 24 (tight enough for a header row). */ size?: number; } export function AvatarStack({ actors, max = 3, size = 24 }: Props) { const deduped = dedupe(actors); const visible = deduped.slice(0, max); const overflow = deduped.length - visible.length; return ( {visible.map((actor, i) => ( ))} {overflow > 0 ? ( +{overflow} ) : null} ); } /** Wraps each avatar in a ring of `bg-background` so overlaps read clearly * against the underlying surface. `marginLeft` does the overlap (web uses * Tailwind's `-space-x-2`; RN doesn't compile that, so we set it inline). */ function Ring({ size, offset, children, }: { size: number; offset: number; children: React.ReactNode; }) { return ( {children} ); } function dedupe(actors: StackActor[]): StackActor[] { const seen = new Set(); const out: StackActor[] = []; for (const a of actors) { const key = `${a.type ?? "none"}:${a.id ?? "none"}`; if (seen.has(key)) continue; seen.add(key); out.push(a); } return out; }