Skip to content
ilinxa/pro-ui

Team Trophy Shelf

alphav0.2.1

Gallery of earned team badges with honest locked slots, an optional count header, and a brief skippable reveal for new badges.

Category: GamificationUpdated: 2026-08-11Created: 2026-07-01Author: ilinxa

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

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/team-trophy-shelf

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/team-trophy-shelf-fixtures

Preview

The trophy shelf

Earned + locked slots, header count, awarded-date on hover. Emits badges.viewed on first view.

Team Aurora trophies

4 / 9

Award 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)

Empty (no badges)

Team Aurora trophies

0 / 0

No trophies yet

Milestones your team completes will land here.

All earned

Team Aurora trophies

9 / 9
  • Kickoff
  • First playable build
  • Internal playtest
  • Vertical slice
  • Beta milestone
  • Content complete
  • Localization pass finished
  • Launch candidate
  • Shipped
Earned only (showLocked=false)

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

demo.tsxtsx

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-loop overlay, set animateAward={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 badges update. Respects prefers-reduced-motion.
  • Portable + team-scoped: no next/*, no per-member or inter-team affordance; only the shadcn tooltip/ badge / separator primitives + 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

Tags

team-trophy-shelfgamificationbadgesmilestonesachievementsteamcooperativetelemetry

Dependencies

shadcn primitives: tooltip, badge, separator
npm peer deps: lucide-react@^1.11.0