Team Feedback Loop
alphav0.2.1Non-blocking celebration layer — a brief skippable overlay when team progress advances, plus a gentle dismissible next-task nudge.
Context
The gamification-system's feedback layer (E6, Competence) — closes the engagement + progression loops. The host pushes a FeedbackEvent (controlled event prop OR imperative celebrate() — both funnel into one reducer, newest wins, never stack) and the component renders a brief celebration band and a standing next-task nudge. The cardinal constraint is D-10 NON-BLOCKING: the board stays fully interactive during a celebration (pointer-events:none except the skip button), focus is never moved or trapped, and celebrationDurationMs is clamped to <1000ms so a lingering modal is impossible; skip via ✕ or Esc. Reduced-motion is a real static branch (no movement, no confetti, still time-boxed + skippable). The default flourish is token CSS reveal-up (zero library); an opt-in canvas-confetti burst is React.lazy for milestone/badge only, so the default consumer never loads it. Ships as a shadcn-style compound — headless TeamFeedbackLoopRoot (the reducer + timer + reduced-motion + imperative handle) + flat parts (TeamFeedbackCelebration, TeamFeedbackNudge) + Tier-C primitives (CelebrationOverlay, NextTaskNudge, lazy ConfettiBurst). D-16: neither this nor team-trophy-shelf triggers the other; the host routes each event kind to exactly one celebrator (set the shelf's animateAward=false to let this own it). Cooperative + team-scoped (D-08): no individual-subject copy, no per-member call-out, no inter-team/public surface. Portable: zero next/*, SSR-safe, imports no other registry component. 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-feedback-loopAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/team-feedback-loop-fixturesPreview
Fire a celebration
Imperative celebrate() — a brief (<1s), skippable, NON-BLOCKING band at the bottom. Click behind it: the page stays fully interactive. Press Esc or ✕ to skip.
Options
Confetti is opt-in + lazy (milestone/badge only, never under reduced motion). Toggle your OS reduced-motion setting to see the static branch.
Live loop (celebration + nudge)
The inline nudge renders here; accept/dismiss are penalty-free. Watch the console for callbacks.
Composed / lighter (nudge only)
Hand-assembled Root + only TeamFeedbackNudge — no celebration, and the confetti chunk never loads.
Demo source
Usage
When to use
Reach for TeamFeedbackLoop to close the cooperative feedback loops: a brief, skippable, NON-BLOCKING celebration when the host says "team progress just advanced," followed by a gentle next-task nudge. It owns no milestone/badge/task state — the host triggers it, it renders the moment and gets out of the way. Every reward is about the team, never an individual (D-08).
The cardinal constraint: non-blocking (D-10)
- The board stays fully interactive during a celebration — clicks pass through.
- Never moves or traps focus; the skip button is the only clickable element.
- Auto-dismisses in < 1s —
celebrationDurationMsis clamped to[200, 999), so a lingering modal is impossible. - Skip with a click on ✕ or Esc at any time.
Two trigger paths (one reducer)
import { TeamFeedbackLoop } from "@/components/team-feedback-loop"
// Controlled (declarative hosts): set event → open; null → close.
<TeamFeedbackLoop
teamId={team.id}
event={lastEvent} // { kind: "milestone", title: "Your team…" }
enableConfetti
nextTask={nextTask} // { taskId, label: "Pick up: wire the win screen" }
onNextTask={(s) => openTask(s.taskId)}
/>
// Imperative (event-driven hosts): fire from a callback.
const ref = useRef<TeamFeedbackLoopHandle>(null)
ref.current?.celebrate({ kind: "task-complete", title: "Your team cleared the column" })Both funnel into one internal reducer — identical behavior regardless of entry point. Newest event wins; overlays never stack.
Confetti + reduced motion
enableConfetti(default off) adds a lazycanvas-confettiburst formilestone/badgeonly. The default CSS flourish keeps the confetti chunk out of the bundle entirely.- Under
prefers-reduced-motion: reducethe celebration renders static — no movement, no confetti — still time-boxed and skippable.
D-16 celebration ownership
If you also use team-trophy-shelf, route each event kind to exactly one celebrator: set the shelf's animateAward={false} and push badge/milestonehere, OR let the shelf own the in-place reveal and don't push that kind here. Neither component triggers the other.
Notes
- Compound: drop
TeamFeedbackCelebrationorTeamFeedbackNudgefor a subset.onEventis accepted for symmetry but E6 emits nothing. - Portable + SSR-safe: no
next/*, no animation on first paint; only the shadcnbuttonprimitive + lucide + the lazy confetti dep.
Features
- Brief (<1s), skippable, NON-BLOCKING celebration overlay — clamped timer, click-through, no focus trap (D-10)
- Two trigger paths (controlled event prop + imperative celebrate()) funnel into one reducer — newest wins, never stack
- Gentle, penalty-free next-task nudge (inline or corner), independent of the celebration lifecycle
- Reduced-motion static branch — no movement, no confetti, still time-boxed + skippable
- Opt-in canvas-confetti burst, React.lazy for milestone/badge — the default CSS flourish keeps it out of the bundle
- Team-scoped copy only — never an individual (D-08); onEvent accepted for symmetry, emits nothing (E6)
- Compound: headless Root + flat parts + lazy confetti; drop either surface for a subset
- D-16 celebration ownership — set the trophy shelf's animateAward=false to defer the moment here