Components · Events
The RSVP affordance and the "who's coming" read-outs. RsvpControl is presentational — the parent holds the authoritative status and reacts to onChange, with pending greying the controls mid-mutation. CapacityMeter escalates tone (accent → warn → bad) as the event fills, and the avatar cluster caps with a "+N" overflow chip.
Maybe → I'm going is pinned :hover. The control owns no server state: each button reports the requested next status via onChange; pending disables the row at 45% opacity while the parent's mutation lands.
fillTone(ratio): < 0.75 accent · < 1 warn · ≥ 1 bad. An uncapped event (capacity == null) renders a running count instead of a meter.
The DS AvatarGroup caps at max visible avatars (default 5, each with a 2px surface ring) and collapses the rest into a +N chip; pass total to drive the overflow when the list is a sampled head (here 5 of 188).
status: going|maybe|declined|null; onChange(next); waitlisted swaps the "Going" badge for the Waitlisted chip; pending greys + disables the row. Holds no server state.confirmed + capacity (null/0 = uncapped → plain count); waitlist surfaced once full; hideLabel. Tone escalates accent → warn → bad. Built on the DS Meter.position renders "Waitlisted · #3". Surfaced when a "going" RSVP lands past capacity.attendees: { id, name, avatarUrl? }[]; max caps visible avatars; total overrides the "+N"; size; label. Renders nothing when empty.Source · ds/events/RsvpControl.tsx · CapacityMeter.tsx · WaitlistBadge.tsx · AttendeeAvatars.tsx (DS Avatar / AvatarGroup / Meter / Badge / Button).