Team Challenge
alphav0.2.1Cooperative team challenge card — one shared goal, collective progress, a whole-team reward, and penalty-free opt-in.
Context
The Relatedness surface of the gamification pack (E3). Renders one Challenge for one Team — label + member stack, a collective progress meter (current / target, never per-member), a whole-team reward chip (system D-08), and a team-level opt-in control where opted-out is a first-class, neutral, joinable state (the CRITICAL never-forced rule made literal: joining is a prominent invite, leaving is one click with no confirm/guilt, and a visible no-penalty hint states refusing is cost-free). On done, a lightweight inline earned acknowledgement (a 'Completed together' pill, non-blocking, no modal) — the heavy celebration overlay belongs to team-feedback-loop (E6), composed alongside; neither triggers the other. Opt-in is controlled-only; omitting onOptInChange collapses to a read-only card. Ships as a light shadcn-style compound — headless TeamChallengeRoot (controlled echo + telemetry chokepoint) + flat parts (Header, Progress, Reward, OptIn) + context-free primitives (ChallengeProgressMeter, ChallengeRewardChip, OptInToggle, TeamMemberStack, Skeleton) + the TeamChallenge assembly — so a card-body-only or opt-in-control-only subset falls out. Emits challenge.opened (first mount, double-emit-guarded) + challenge.opt-in via onEvent. Portable: zero next/*, own types.ts slice, no other registry import; SSR-safe. Third 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-challengeAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/team-challenge-fixturesPreview
Interactive — join / leave (penalty-free)
Controlled opt-in: joining is a prominent invite, leaving is one click with no guilt. Emits challenge.opened + challenge.opt-in.
All 5 members commit a task this morning
Team Aurora
Optional — no penalty for sitting this one out
Joinable (opted-out)
A neutral, first-class invitation — the goal + reward shown as available if you join, never greyed-as-failure.
All 5 members commit a task this morning
Team Aurora
Optional — no penalty for sitting this one out
Active + Completed
Opted-in and in motion → whole-team earned treatment on done (lightweight inline ack, no modal).
Close every stale review by Friday
Team Aurora
Optional — no penalty for sitting this one out
Ship the onboarding flow together
Team Aurora
Optional — no penalty for sitting this one out
Read-only (omit onOptInChange)
Capability-gating — with no handler the opt-in control hides; a pure progress + reward card falls out for free.
Close every stale review by Friday
Team Aurora
Composed / lighter (bare parts, no card chrome)
Just the penalty-free toggle + the avatar pile — the compound tree-shakes to a subset (no Root, no card).
Optional — no penalty for sitting this one out
Team sizes
Single-member (no +N) ↔ overflowing stack (+N chip).
Close every stale review by Friday
Team Nimbus
Optional — no penalty for sitting this one out
All 5 members commit a task this morning
Team Meridian
Optional — no penalty for sitting this one out
Edge cases
No reward (chip hides) · target 0 (0 / 0, never NaN) · long label (truncates) · loading skeleton.
Pair on one tricky bug this week
Team Aurora
Optional — no penalty for sitting this one out
Warm-up challenge (no target set yet)
Team Aurora
Optional — no penalty for sitting this one out
Every member records at least one asynchronous stand-up update before noon so nobody is blocked over the long weekend
Team Meridian
Optional — no penalty for sitting this one out
Demo source
Usage
When to use
Reach for TeamChallenge when a team app wants a safe-by-design cooperative challenge — one shared goal, collective progress, and a whole-team reward, with a penalty-free opt-in. It is the Relatedness surface of the gamification pack. The single rule it exists to protect: never-forced — opting in is a deliberate team choice, refusing is cost-free and neutral, and the reward belongs to the whole team equally, never an individual.
Basic example
import { TeamChallenge } from "@/components/team-challenge"
export function TeamChallengeZone({ challenge, team }) {
return (
<TeamChallenge
challenge={challenge} // { label, optedIn, progress, reward, done }
team={team} // { name, members }
onOptInChange={(optedIn) => persistOptIn(team.id, challenge.id, optedIn)}
onEvent={(e) => analytics.track(e.type, e)} // host adds the envelope
/>
)
}Controlled opt-in (never forced)
challenge.optedIn is the value — the host owns it. Joining is a prominent invite; leaving is one click, no confirm, no guilt. Omit onOptInChange and the control hides entirely — a read-only progress + reward card falls out for free.
Compose a lighter version
It ships as a light shadcn-style compound. Drop the card chrome and keep only the penalty-free toggle (or the bare avatar pile):
import { OptInToggle, TeamMemberStack } from "@/components/team-challenge"
// A context-free toggle, droppable on any surface:
<OptInToggle optedIn={challenge.optedIn} onOptInChange={join} />
// …or just the identity pile:
<TeamMemberStack members={team.members} />Notes
- Collective progress only. One bar +
current / target— never a per-member breakdown or ranking. The avatar stack is identity, not a progress display. - Whole-team reward. The reward reads as the team earns / earned — never first-person, never per-member.
- Done is a lightweight inline ack. A Completed together pill (non-blocking, no modal). The heavy celebration overlay is
team-feedback-loop(E6) — compose it alongside; neither triggers the other. - Telemetry.
onEventemitschallenge.openedonce on first mount (StrictMode-safe) andchallenge.opt-inon toggle. No SDK, nonext/*. - Portable. Own
types.tsslice, no other registry import — only the shadcnprogress,avatar,button,badge, andskeletonprimitives.
Features
- One shared team goal — label + member stack, collective progress (current / target), whole-team reward
- Never-forced opt-in: opted-out is a neutral, first-class, cost-free invitation — never greyed-as-failure
- Penalty-free leave: one click, no confirm dialog, no guilt copy; a visible no-penalty hint in both states
- Collective progress only — never a per-member breakdown or ranking; the avatar stack is identity, not progress
- Whole-team reward framing (the team earns / earned) — never first-person, never per-member
- Done = a lightweight inline 'Completed together' ack (non-blocking, no modal); heavy celebration deferred to E6
- challenge.opened + challenge.opt-in telemetry via onEvent — no SDK, no next/*
- Light compound: headless Root + flat parts + context-free primitives; drop to a card-body-only or toggle-only subset