Team Quest Log
alphav0.2.1Team quest overlay — an editable skippable quest name plus a milestone chapter timeline with done, current, and upcoming beats.
Context
The Team Narrative Framing surface of the gamification pack (E5, Autonomy + Relatedness). Two mountable surfaces over one team + its milestones: (a) a quest-name editor — display the resolved title (questName?.trim() || name) with an inline display↔edit toggle; clearing/saving blank reverts to the literal team name (the skip path, never forced); a quiet 'name your quest' invitation on the default; and (b) a milestone-chapter timeline — one vertical-rail beat per NarrativeChapter, ordered, each framing a milestone, with done / current ('you are here') / upcoming states derived from Milestone.done (current = the first not-done beat; unresolved milestoneId renders gracefully + dev-warns, never crashes). Deliberately lighter than gantt-timeline / event-calendar — chapters, not a time axis; it consumes no TaskItem[] and imports no other registry component. Emits narrative.chapter-viewed once per chapter per mount when a beat first scrolls into view (one IntersectionObserver, fire-once Set, SSR-guarded, click fallback). Ships as a light shadcn-style compound (no React.lazy) — headless TeamQuestLogRoot (title resolution + draft/edit state + beat derivation + telemetry + a forwardRef handle: focusNameEditor / scrollToChapter) + flat parts (TeamQuestNameEditor, TeamQuestChapters) + context-free primitives (QuestTitle, QuestNameField, ChapterRail, ChapterBeat, EmptyNarrative) + the TeamQuestLog assembly — so a timeline-only or name-only subset falls out. Controlled (D-06); portable — zero next/*, own types.ts slice; SSR-safe. Sixth and final component of the gamification-system.
Installation
pnpm dlx shadcn@latest initpnpm dlx shadcn@latest add @ilinxa/team-quest-logAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/team-quest-log-fixturesCLI can't resolve @ilinxa? The namespace is listed in the official shadcn registry directory, so current CLIs need no configuration. If yours can't resolve it (older or pinned versions, self-hosted mirrors), register it manually in components.json:
"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}Preview
Full overlay — live (name your quest)
Starts on the default team name. Name it, edit it, or clear it to revert — never forced. Beats emit narrative.chapter-viewed once when scrolled into view.
Team Aurora
Custom quest name + in-progress narrative
done · done · current (you are here) · upcoming — each visually distinct (icon + color).
The Road to Launch
All done vs none done
All done → every beat completed, no 'current'. None done → the first beat is 'current'.
The Road to Launch
Fresh Start
Edge cases
Empty narrative (quiet placeholder) · an unresolved-milestone beat (renders gracefully).
The Road to Launch
Your team's story will appear here as milestones are reached.
The Road to Launch
Composed / lighter — timeline-only + name-only subsets
Hand-assembled from Root + one part each — the compound tree-shakes to a subset.
Team Aurora
Demo source
"use client"; import * as React from "react"; import { TeamQuestLog } from "./team-quest-log";import { TeamQuestLogRoot } from "./parts/team-quest-log-root";import { TeamQuestChapters } from "./parts/team-quest-chapters";import { TeamQuestNameEditor } from "./parts/team-quest-name-editor";import { CHAPTERS, CHAPTERS_WITH_UNRESOLVED, MILESTONES, MILESTONES_ALL_DONE, MILESTONES_NONE_DONE, TEAM_AURORA, TEAM_AURORA_DEFAULT,} from "./dummy-data";import type { Team } from "./types"; function Section({ title, hint, children,}: { title: string; hint?: string; children: React.ReactNode;}) { return ( <section className="flex flex-col gap-3"> <div className="flex flex-col gap-0.5"> <h3 className="text-sm font-semibold text-foreground">{title}</h3> {hint ? <p className="text-xs text-muted-foreground">{hint}</p> : null} </div> <div className="rounded-lg border border-border bg-card p-5">{children}</div> </section> );} /** Controlled host — proves the skippable quest-name loop (clear → revert). */function LiveQuestLog() { const [team, setTeam] = React.useState<Team>(TEAM_AURORA_DEFAULT); return ( <TeamQuestLog team={team} milestones={MILESTONES} chapters={CHAPTERS} onQuestNameChange={(questName) => setTeam((t) => ({ ...t, questName: questName || undefined })) } onChapterClick={(c) => console.info("[demo] chapter clicked", c.id)} onEvent={(e) => console.info("[demo] gamification event", e)} /> );} export default function TeamQuestLogDemo() { return ( <div className="mx-auto flex w-full max-w-lg flex-col gap-8"> <Section title="Full overlay — live (name your quest)" hint="Starts on the default team name. Name it, edit it, or clear it to revert — never forced. Beats emit narrative.chapter-viewed once when scrolled into view." > <LiveQuestLog /> </Section> <Section title="Custom quest name + in-progress narrative" hint="done · done · current (you are here) · upcoming — each visually distinct (icon + color)." > <TeamQuestLog team={TEAM_AURORA} milestones={MILESTONES} chapters={CHAPTERS} onQuestNameChange={() => {}} /> </Section> <Section title="All done vs none done" hint="All done → every beat completed, no 'current'. None done → the first beat is 'current'." > <div className="flex flex-col gap-6"> <TeamQuestLog team={TEAM_AURORA} milestones={MILESTONES_ALL_DONE} chapters={CHAPTERS} editableName={false} /> <TeamQuestLog team={{ ...TEAM_AURORA, questName: "Fresh Start" }} milestones={MILESTONES_NONE_DONE} chapters={CHAPTERS} editableName={false} /> </div> </Section> <Section title="Edge cases" hint="Empty narrative (quiet placeholder) · an unresolved-milestone beat (renders gracefully)." > <div className="flex flex-col gap-6"> <TeamQuestLog team={TEAM_AURORA} milestones={MILESTONES} chapters={[]} editableName={false} /> <TeamQuestLog team={TEAM_AURORA} milestones={MILESTONES} chapters={CHAPTERS_WITH_UNRESOLVED} editableName={false} /> </div> </Section> <Section title="Composed / lighter — timeline-only + name-only subsets" hint="Hand-assembled from Root + one part each — the compound tree-shakes to a subset." > <div className="flex flex-col gap-6"> <TeamQuestLogRoot team={TEAM_AURORA} milestones={MILESTONES} chapters={CHAPTERS}> <TeamQuestChapters /> </TeamQuestLogRoot> <TeamQuestLogRoot team={TEAM_AURORA_DEFAULT} milestones={[]} chapters={[]} onQuestNameChange={() => {}} > <TeamQuestNameEditor /> </TeamQuestLogRoot> </div> </Section> </div> );} Usage
When to use
Reach for TeamQuestLog to give a team a light sense of story over its journey — a name for the quest and milestone beats framed as chapters. It is the E5 (Autonomy + Relatedness) surface of the gamification pack. The narrative is optional and skippable: a blank quest name falls back to the team's literal name, and the chapter timeline degrades quietly when no chapters are authored. It is deliberately lighter than the Gantt / calendar — chapters, not a time axis.
Basic example
import { TeamQuestLog } from "@/components/team-quest-log"
<TeamQuestLog
team={team} // { id, name, questName? }
milestones={milestones} // host-owned shared spine
chapters={chapters} // { id, title, milestoneId, order }
onQuestNameChange={(name) => saveQuestName(team.id, name)} // "" reverts to team name
onEvent={(e) => analytics.track(e.type, e)} // narrative.chapter-viewed
/>Never forced
The displayed title is questName?.trim() || name. Clearing the field (saving blank) reverts to the literal team name — a first-class skip path, no required-field validation, no nag. On the default name a quiet Name your quest invitation shows (never a blocking prompt).
Beat states
- done — the milestone is complete (check marker).
- current — the first not-done beat by order (the you are here pin).
- upcoming — later not-done beats (hollow marker).
State is derived from milestones and conveyed by icon + color (never color alone). An unresolved milestoneId renders gracefully (as upcoming) and dev-warns — it never crashes.
Compose a lighter version
import { TeamQuestLogRoot, TeamQuestChapters } from "@/components/team-quest-log"
// Timeline-only — the name-editor code never enters this bundle:
<TeamQuestLogRoot team={team} milestones={milestones} chapters={chapters} onEvent={track}>
<TeamQuestChapters />
</TeamQuestLogRoot>Notes
- Telemetry.
onEventemitsnarrative.chapter-viewedonce per chapter per mount, when a beat first scrolls into view (IntersectionObserver). OmitonEventand no observer attaches. - Imperative handle. A ref exposes
focusNameEditor()andscrollToChapter(id)for onboarding / deep-link hosts. - Team-scoped.Only this team's narrative — no comparison, no other team's data.
- Portable. No
next/*, owntypes.tsslice, no other registry import; only the shadcninput/buttonprimitives + lucide.
Features
- Skippable quest name — displayed title = questName?.trim() || name; clearing reverts to the team name (never forced, no validation)
- Milestone-chapter timeline — one vertical-rail beat per chapter, done / current / upcoming derived from milestones
- Deterministic beat states — current = first not-done by order; all-done has no current; unresolved milestone renders gracefully + dev-warns
- narrative.chapter-viewed telemetry — one IntersectionObserver, fire-once per chapter per mount, SSR-guarded with a click fallback
- Non-color state cues (check / you-are-here pin / hollow) + accessible beat names — state reads without color
- Light compound — headless Root + flat TeamQuestNameEditor / TeamQuestChapters + context-free primitives; timeline-only + name-only subsets fall out
- Imperative handle — focusNameEditor() + scrollToChapter(id) for onboarding / deep-link hosts