project-bifrost-platform/src/components/AvatarPile.astro
Jonathan Hvid b0e6d7e18b feat(page): /events + /events/past + AvatarPile component
/events:
- Header: EVENTS · 'Where the council gathers.' · one-line subtitle
- Hero invitation card on --ink for the soonest non-office_hours event:
  NEXT UP · MEMBERS ONLY / INVITATION BY HAND eyebrow strip, two-column
  date/detail body separated by a 0.5px vertical line, foot strip with
  '{capacity} seats · {confirmed} confirmed' + AvatarPile of confirmed
  attendees and the RSVP CTA. The RSVP button toggles between cream-on-ink
  'Save your seat →' and outlined 'You're confirmed ✓ Change'. Empty-state
  card retains the visual weight when no upcoming non-office_hours event.
- ALSO COMING UP — every other upcoming event including office_hours.
  Three-column rows; the right column uses event.action_label or falls back
  to defaultActionLabel(kind). Studio hours surfaces with 'Book a slot →'.
- PAST GATHERINGS — two-column grid. Each card has a 56px thumb: photo_url
  if set, else a copper-tinted notes square when notes_url is present, else
  a deterministic two-pigment gradient block. View all → links to /events/past.

/events/past — same card component, full list of starts_at < now() events.
No boolean past flag column; filter is purely date-based.

AvatarPile (src/components/AvatarPile.astro) — reusable. Overlapping circle
slots with a 1.5px border in a caller-provided colour (defaults to surface,
the hero card overrides to --ink so circles read on dark). Stacks z-index
so leftmost is on top; +N overflow chip at the end.

format.ts: adds eventKindLabel (office_hours → 'Studio hours') and
defaultActionLabel per kind.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:05:47 +02:00

56 lines
1.5 KiB
Text

---
import Avatar from './Avatar.astro';
import type { UserPublic } from '../lib/db';
interface Props {
users: UserPublic[];
max?: number;
size?: number;
/** Border color between overlapping avatars — defaults to --surface for cream surfaces. */
borderColor?: string;
}
const { users, max = 5, size = 22, borderColor = 'var(--surface)' } = Astro.props;
const shown = users.slice(0, max);
const overflow = Math.max(0, users.length - shown.length);
---
<div class="pile" style={`--pile-size: ${size}px; --pile-border: ${borderColor};`}>
{shown.map((u, i) => (
<span class="pile-slot" style={`z-index: ${shown.length - i}`}>
<Avatar id={u.id} name={u.name} size={size} />
</span>
))}
{overflow > 0 && (
<span class="pile-slot pile-overflow label-sm" aria-label={`${overflow} more`}>
+{overflow}
</span>
)}
</div>
<style>
.pile {
display: inline-flex;
align-items: center;
}
.pile-slot {
display: inline-flex;
border-radius: 50%;
box-shadow: 0 0 0 1.5px var(--pile-border);
}
.pile-slot:not(:first-child) {
margin-left: calc(var(--pile-size) * -0.32);
}
.pile-overflow {
width: var(--pile-size);
height: var(--pile-size);
background: var(--ink-muted);
color: var(--ink);
align-items: center;
justify-content: center;
font-family: var(--font-sans);
letter-spacing: var(--tracking-wide);
font-weight: 600;
font-size: calc(var(--pile-size) * 0.36);
}
</style>