mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Stack team card avatars
This commit is contained in:
@@ -115,6 +115,7 @@ function TeamAvatarRow({
|
||||
}) {
|
||||
const visiblePersonas = personas.slice(0, MAX_VISIBLE_MEMBER_AVATARS);
|
||||
const overflowCount = Math.max(0, memberCount - visiblePersonas.length);
|
||||
const stackItemCount = visiblePersonas.length + (overflowCount > 0 ? 1 : 0);
|
||||
|
||||
if (visiblePersonas.length === 0 && overflowCount === 0) {
|
||||
return (
|
||||
@@ -130,16 +131,26 @@ function TeamAvatarRow({
|
||||
<div className="absolute inset-x-0 top-0 bottom-12 flex items-center justify-center">
|
||||
<div
|
||||
aria-label={`${teamName} member avatars`}
|
||||
className="flex max-w-full items-center justify-center gap-2 px-4"
|
||||
className="flex max-w-full items-center justify-center px-4"
|
||||
role="img"
|
||||
>
|
||||
{visiblePersonas.map((persona, index) => (
|
||||
<TeamAvatarItem index={index} key={persona.id} persona={persona} />
|
||||
<TeamAvatarItem
|
||||
index={index}
|
||||
isFollowedByAnother={index < stackItemCount - 1}
|
||||
key={persona.id}
|
||||
persona={persona}
|
||||
/>
|
||||
))}
|
||||
{overflowCount > 0 ? (
|
||||
<span className="flex h-14 w-14 items-center justify-center rounded-full border-[3px] border-background bg-card text-sm font-semibold text-muted-foreground shadow-sm">
|
||||
+{overflowCount}
|
||||
</span>
|
||||
<div
|
||||
className={visiblePersonas.length > 0 ? "-ml-5" : ""}
|
||||
style={{ zIndex: stackItemCount }}
|
||||
>
|
||||
<span className="flex h-14 w-14 items-center justify-center rounded-full border-[3px] border-background bg-card text-sm font-semibold text-muted-foreground shadow-sm">
|
||||
+{overflowCount}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,15 +159,28 @@ function TeamAvatarRow({
|
||||
|
||||
function TeamAvatarItem({
|
||||
index,
|
||||
isFollowedByAnother,
|
||||
persona,
|
||||
}: {
|
||||
index: number;
|
||||
isFollowedByAnother: boolean;
|
||||
persona: AgentPersona;
|
||||
}) {
|
||||
const avatarUrl = persona.avatarUrl?.trim() ?? null;
|
||||
|
||||
return (
|
||||
<div className="h-14 w-14" data-team-member-avatar="avatar">
|
||||
<div
|
||||
className={`h-14 w-14 ${index > 0 ? "-ml-5" : ""}`}
|
||||
data-team-member-avatar="avatar"
|
||||
style={{
|
||||
zIndex: index + 1,
|
||||
...(isFollowedByAnother && {
|
||||
mask: "radial-gradient(circle 32px at calc(100% + 8px) 50%, transparent 99%, #fff 100%)",
|
||||
WebkitMask:
|
||||
"radial-gradient(circle 32px at calc(100% + 8px) 50%, transparent 99%, #fff 100%)",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<ProfileAvatar
|
||||
avatarUrl={avatarUrl}
|
||||
|
||||
@@ -413,6 +413,56 @@ test("the new team card offers create and import", async ({ page }) => {
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("team cards use the thread-style overlapping avatar stack", async ({
|
||||
page,
|
||||
}) => {
|
||||
const personaIds = ["custom:design", "custom:build", "custom:ship"];
|
||||
await installMockBridge(page, {
|
||||
personas: [
|
||||
{
|
||||
id: personaIds[0],
|
||||
displayName: "Design",
|
||||
systemPrompt: "You design interfaces.",
|
||||
},
|
||||
{
|
||||
id: personaIds[1],
|
||||
displayName: "Build",
|
||||
systemPrompt: "You build interfaces.",
|
||||
},
|
||||
{
|
||||
id: personaIds[2],
|
||||
displayName: "Ship",
|
||||
systemPrompt: "You ship interfaces.",
|
||||
},
|
||||
],
|
||||
teams: [
|
||||
{
|
||||
name: "Product crew",
|
||||
personaIds,
|
||||
},
|
||||
],
|
||||
});
|
||||
await gotoApp(page);
|
||||
await page.getByTestId("open-agents-view").click();
|
||||
|
||||
const stack = page.getByLabel("Product crew member avatars");
|
||||
const avatars = stack.locator('[data-team-member-avatar="avatar"]');
|
||||
await expect(avatars).toHaveCount(3);
|
||||
await expect(avatars.nth(1)).toHaveClass(/-ml-5/);
|
||||
await expect(avatars.nth(2)).toHaveClass(/-ml-5/);
|
||||
|
||||
const boxes = await avatars.evaluateAll((elements) =>
|
||||
elements.map((element) => {
|
||||
const box = element.getBoundingClientRect();
|
||||
return { left: box.left, right: box.right };
|
||||
}),
|
||||
);
|
||||
expect(boxes[1]?.left).toBeLessThan(boxes[0]?.right ?? 0);
|
||||
expect(boxes[2]?.left).toBeLessThan(boxes[1]?.right ?? 0);
|
||||
await expect(avatars.first()).not.toHaveCSS("mask-image", "none");
|
||||
await expect(avatars.last()).toHaveCSS("mask-image", "none");
|
||||
});
|
||||
|
||||
test("agent defaults stays in the header without an actions menu", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user