Skip to content
ilinxa/pro-ui

Team Quest Log

alphav0.2.1

Team quest overlay — an editable skippable quest name plus a milestone chapter timeline with done, current, and upcoming beats.

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

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

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
Install the component
pnpm dlx shadcn@latest add @ilinxa/team-quest-log

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/team-quest-log-fixtures

CLI 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

Where it beganProject kickoffCompleted 3 Feb 2026
It moves!First playable buildCompleted 18 Mar 2026
Into the wild (beta)Closed beta
Doors openPublic launch
Looking back1.0 retrospective

All done vs none done

All done → every beat completed, no 'current'. None done → the first beat is 'current'.

The Road to Launch

Where it beganProject kickoffCompleted 3 Feb 2026
It moves!First playable buildCompleted 18 Mar 2026
Into the wild (beta)Closed betaCompleted 1 May 2026
Doors openPublic launchCompleted 1 May 2026
Looking back1.0 retrospectiveCompleted 1 May 2026

Fresh Start

Where it beganProject kickoff
It moves!First playable build
Into the wild (beta)Closed beta
Doors openPublic launch
Looking back1.0 retrospective

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

Where it beganProject kickoffCompleted 3 Feb 2026
It moves!First playable buildCompleted 18 Mar 2026
Into the wild (beta)Closed beta
Doors openPublic launch
Looking back1.0 retrospective
The lost chapter

Composed / lighter — timeline-only + name-only subsets

Hand-assembled from Root + one part each — the compound tree-shakes to a subset.

Where it beganProject kickoffCompleted 3 Feb 2026
It moves!First playable buildCompleted 18 Mar 2026
Into the wild (beta)Closed beta
Doors openPublic launch
Looking back1.0 retrospective

Team Aurora

Demo source

demo.tsxtsx
"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. onEvent emits narrative.chapter-viewed once per chapter per mount, when a beat first scrolls into view (IntersectionObserver). Omit onEvent and no observer attaches.
  • Imperative handle. A ref exposes focusNameEditor() and scrollToChapter(id) for onboarding / deep-link hosts.
  • Team-scoped.Only this team's narrative — no comparison, no other team's data.
  • Portable. No next/*, own types.ts slice, no other registry import; only the shadcn input / button primitives + 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

Tags

team-quest-loggamificationnarrativemilestonestimelineteamcooperativetelemetry

Dependencies

shadcn primitives: button, input
npm peer deps: lucide-react@^1.11.0