/**
* 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;
}