Team Trophy Shelf
alphav0.2.1Gallery of earned team badges with honest locked slots, an optional count header, and a brief skippable reveal for new badges.
Context
The "what has this team accomplished?" surface the progress bar can't express — recognition + shared pride, not status. Takes Badge[] (awardedAt is the single earned/locked discriminator) + the owning team; lays earned tokens and locked slots in a responsive grid, shows an awarded-date tooltip on hover, and plays a diff-driven award reveal when a controlled badges update flips a badge's awardedAt on (SSR-safe — nothing animates on load; respects prefers-reduced-motion). Ships as a shadcn-style compound — headless TeamTrophyShelfRoot + flat parts (Grid, Header, Empty) + the standalone Tier-C TeamMilestoneBadge token + a React.lazy BadgeAwardOverlay — so the bare token falls out for free and animateAward=false / the bare-token path never load the award chunk. D-16 (celebration ownership): a host routing badge events to team-feedback-loop sets animateAward=false so the moment isn't celebrated twice; neither component triggers the other. Cooperative-only and team-scoped by design (D-08): no per-individual, inter-team, or public affordance. Portable: zero next/*, no app context, SSR-safe, imports no other registry component. Second component of the gamification-system.
Installation
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/team-trophy-shelfAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/team-trophy-shelf-fixturesPreview
The trophy shelf
Earned + locked slots, header count, awarded-date on hover. Emits badges.viewed on first view.
Team Aurora trophies
4 / 9Award reveal (diff-driven, non-blocking)
Flip a locked badge → earned to play the < 1s in-place reveal. Toggle animateAward or your OS reduced-motion setting to suppress it.
Team Aurora trophies
4 / 9- Kickoff
- First playable build
- Internal playtest
- Vertical slice
- Beta milestone
- Content complete
- Localization pass finished
- Launch candidate
- Shipped
States
Empty · all-earned · earned-only (showLocked=false)
Team Aurora trophies
0 / 0No trophies yet
Milestones your team completes will land here.
Team Aurora trophies
9 / 9- Kickoff
- First playable build
- Internal playtest
- Vertical slice
- Beta milestone
- Content complete
- Localization pass finished
- Launch candidate
- Shipped
Team Aurora trophies
4 / 9- Kickoff
- First playable build
- Internal playtest
- Vertical slice
Bare token, inline
TeamMilestoneBadge alone — no shelf chrome, no award overlay in the bundle.
- Kickoff
- First playable build
- Internal playtest
- Vertical slice
Composed / lighter (custom layout, no header)
Hand-assembled Root + Grid with animateAward={false} — the lazy award chunk never loads.
Team trophies
- Kickoff
- First playable build
- Internal playtest
- Vertical slice
- Beta milestone
- Content complete
- Localization pass finished
- Launch candidate
- Shipped
Demo source
Usage
When to use
Reach for TeamTrophyShelf when a team board needs a durable gallery of this team's earned milestone badges— the artifacts of progress, alongside honest locked slots for what's ahead. It complements the progress bar (the live rate) with recognition + shared pride. Badges belong to the team, not individuals, and the shelf renders only on the team board — never a public or inter-team surface, never a ranking.
Data
awardedAt is the single discriminator: present → earned (drives the awarded date + the diff-based reveal); absent → a locked slot.
interface Badge {
id: string
label: string
awardedAt?: string // ISO 8601; undefined → not yet earned (locked)
milestoneId?: string // the milestone that earned it (optional link)
}Basic example
import { TeamTrophyShelf } from "@/components/team-trophy-shelf"
<TeamTrophyShelf
team={{ id: team.id, name: team.name }}
badges={badges} // earned + not-yet-earned slots
showLocked // show the journey, not just the wins
onEvent={track} // { type: "badges.viewed", teamId, badgeId? }
onBadgeOpen={(b) => openMilestone(b.milestoneId)}
/>When the host flips a badge's awardedAt from undefinedto a timestamp, that badge plays a brief (< 1s), skippable, non-blocking reveal in place — no toast, no modal, no blocked input.
The bare token + lighter builds
import { TeamMilestoneBadge } from "@/components/team-trophy-shelf"
// Inline next to a milestone — no shelf chrome, no award chunk.
<TeamMilestoneBadge badge={badge} size="sm" />It ships as a shadcn-style compound: TeamTrophyShelfRoot + TeamTrophyShelfGrid / TeamTrophyShelfHeader / TeamTrophyShelfEmpty + the Tier-C TeamMilestoneBadge. The award burst (BadgeAwardOverlay) is React.lazy, so the bare-token path and animateAward={false} never pull its chunk.
Notes
- D-16 (celebration ownership): if you also route badge events to a
team-feedback-loopoverlay, setanimateAward={false}here so the moment isn't celebrated twice. Neither component triggers the other. - Always visible + honest:
showLocked(default true) renders the journey ahead; empty renders an encouraging state, not a dead panel. - SSR-safe: no badge animates on load; the reveal only plays after a controlled
badgesupdate. Respectsprefers-reduced-motion. - Portable + team-scoped: no
next/*, no per-member or inter-team affordance; only the shadcntooltip/badge/separatorprimitives + lucide glyphs.
Features
- Responsive gallery of a team's earned milestone badges + honest locked slots (showLocked)
- awardedAt is the single earned/locked discriminator; awarded-date on hover (tooltip)
- Diff-driven, SSR-safe award reveal (<1s, skippable, non-blocking, reduced-motion-aware) — nothing animates on load
- Standalone TeamMilestoneBadge token — usable inline with zero shelf scaffolding
- badges.viewed telemetry (once on first view; with badgeId on open)
- Cooperative + team-scoped: team-owned only, no ranking / per-member / inter-team surface — ever
- Compound: headless Root + flat parts + React.lazy award overlay; bare-token path & animateAward=false drop the award chunk
- D-16 celebration ownership — set animateAward=false to defer the moment to team-feedback-loop