desktop: Share the avatar stack

Board cards show assignees the way the repository header shows
delegates, overlapping and fanning out on hover, so the stack moves
into its own component.
This commit is contained in:
Rūdolfs Ošiņš
2026-11-06 18:21:33 +01:00
parent 244e2dc44c
commit 787be14d73
2 changed files with 61 additions and 43 deletions
+59
View File
@@ -0,0 +1,59 @@
<script lang="ts">
import type { Author } from "@bindings/cob/Author";
import { authorForNodeId } from "@app/lib/utils";
import NodeId from "@app/components/NodeId.svelte";
interface Props {
people: Author[];
avatarSize?: string;
}
const { people, avatarSize = undefined }: Props = $props();
// The cards are portaled out of the stack, so moving onto one ends the
// stack's hover. Keep it fanned out while any card is still open.
let hovered = $state(false);
const cardsOpen: Record<string, boolean> = $state({});
const fanned = $derived(hovered || Object.values(cardsOpen).some(Boolean));
</script>
<style>
.avatars {
display: flex;
align-items: center;
}
/* Positioned so each avatar paints together with its ring; otherwise every
ring is painted before any image and the one below covers it. */
.avatar {
position: relative;
display: flex;
border-radius: 2px;
box-shadow: 0 0 0 2px var(--color-surface-canvas);
transition: margin-left 150ms ease;
}
.avatar + .avatar {
margin-left: -0.625rem;
}
.avatars.fanned .avatar + .avatar {
margin-left: 0.25rem;
}
</style>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="avatars"
class:fanned
onmouseenter={() => (hovered = true)}
onmouseleave={() => (hovered = false)}>
{#each people as person, index (person.did)}
<div class="avatar" style:z-index={people.length - index}>
<NodeId
{...authorForNodeId(person)}
avatarOnly
{avatarSize}
oncardtoggle={expanded => (cardsOpen[person.did] = expanded)} />
</div>
{/each}
</div>
+2 -43
View File
@@ -3,10 +3,9 @@
import type { RepoInfo } from "@bindings/repo/RepoInfo";
import * as router from "@app/lib/router";
import { authorForNodeId } from "@app/lib/utils";
import AvatarStack from "@app/components/AvatarStack.svelte";
import CheckoutRepoButton from "@app/components/CheckoutRepoButton.svelte";
import NodeId from "@app/components/NodeId.svelte";
import ShareButton from "@app/components/ShareButton.svelte";
import VisibilityBadge from "@app/components/VisibilityBadge.svelte";
@@ -18,12 +17,6 @@
const { repo, config }: Props = $props();
const project = $derived(repo.payloads["xyz.radicle.project"]!);
// The cards are portaled out of the stack, so moving onto one ends the
// stack's hover. Keep it fanned out while any card is still open.
let hovered = $state(false);
const cardsOpen: Record<string, boolean> = $state({});
const fanned = $derived(hovered || Object.values(cardsOpen).some(Boolean));
</script>
<style>
@@ -82,25 +75,6 @@
.meta-value {
color: var(--color-text-primary);
}
.avatars {
display: flex;
align-items: center;
}
/* Positioned so each avatar paints together with its ring; otherwise every
ring is painted before any image and the one below covers it. */
.avatar {
position: relative;
display: flex;
border-radius: 2px;
box-shadow: 0 0 0 2px var(--color-surface-canvas);
transition: margin-left 150ms ease;
}
.avatar + .avatar {
margin-left: -0.625rem;
}
.avatars.fanned .avatar + .avatar {
margin-left: 0.25rem;
}
.actions {
display: flex;
align-items: center;
@@ -133,22 +107,7 @@
<span class="meta-label">Delegates</span>
<span class="meta-value">{repo.threshold}/{repo.delegates.length}</span>
</button>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="avatars"
class:fanned
onmouseenter={() => (hovered = true)}
onmouseleave={() => (hovered = false)}>
{#each repo.delegates as delegate, index (delegate.did)}
<div class="avatar" style:z-index={repo.delegates.length - index}>
<NodeId
{...authorForNodeId(delegate)}
avatarOnly
avatarSize="1.25rem"
oncardtoggle={expanded => (cardsOpen[delegate.did] = expanded)} />
</div>
{/each}
</div>
<AvatarStack people={repo.delegates} avatarSize="1.25rem" />
</div>
</div>