Commit Graph

Free

A contribution heatmap paged by two-week sprints, drillable to the day.

Commit activity

1

84 commits in sprints 22 to 26

Sprint 26 began Thu, Sep 24, day 6 of 14

LessMore

npx shadcn@latest add @ui-shrushank/commit-graph

The install writes a single file to components/ui/CommitGraph.tsx, which exports CommitGraph.

tsx
import { CommitGraph } from "@/components/ui/CommitGraph";
tsx
<CommitGraph />

Needs framer-motion and @phosphor-icons/react installed. The shadcn CLI adds them for you.

A GitHub-style contribution heatmap, cut into the unit teams actually review against.

  • Five swappable colour themes (swatch picker included), a documented exception to the two-hue rule
  • A streak badge that only shows once there's an actual streak
  • Clicking Commit round-trips through a simulated ~500ms backend call before today's cell levels up (the site is a static export with no server), and the returned count persists to localStorage the way a server response would be cached, so it survives a reload
  • A production version swaps the mock for a real POST. Each sprint group carries its own total and is a real <button> that opens a detail panel for that sprint alone.

The simulated latency + localStorage persistence makes the demo behave like a real server-backed feature, and the note documents exactly what the production swap is.

Every line below was read out of the file you are about to copy, so you can check each one against the source at the bottom of this page.

  • Uses the native role="status", role="group", role="radiogroup", role="radio" semantics rather than a styled div, so assistive technology announces what the control is and how it behaves.
  • Carries aria-checked, aria-describedby, aria-expanded, aria-invalid, aria-label, aria-live, aria-pressed, so its state is exposed and not left to the visual treatment alone.
  • Announces its own state changes through an aria-live region, so a change that is only visual is still heard.
  • Has an explicit prefers-reduced-motion branch: the animation is dropped for an instant state change, and the component stays fully usable without it.
  • Handles keys directly, so every pointer gesture it supports has a keyboard equivalent.
  • Draws a focus-visible ring that is separate from its hover treatment, so keyboard users get an affordance mouse users do not take away.
  • Ships screen-reader-only text for the part of its affordance that is purely visual.
  • Pairs every input with a real <label>, so the field keeps its name after the placeholder disappears.

A sprint click is a navigation, not an expansion.
Five totals answer "which sprint was heavy"; they can't answer "what happened inside it". The overview is replaced by that sprint's own heat map: the same fourteen days redrawn as a 7x2 calendar at 44px with the exact count in every cell, weekday headers and W1/W2 row labels.
The commit log sits under the sprint's calendar.
Commits are grouped by day with a time and a short SHA, and any day cell filters the log to itself. A count tells you Tuesday was busy; the log tells you what Tuesday was.
The number inside a cell needs two colours, not one.
The ramp runs dark-on-light in light mode and light-on-dark in dark mode.
Commit is hidden inside past sprints.
It lands on today, and an action that changes a number the open view can't show is worse than no action.
Focus follows the navigation both ways.
Escape or "All sprints" comes back out to the sprint you opened.
The log's row count is the sprint total by construction.
Both read the same seededCount, so the headline can never drift from the list under it.
A sprint is a start and an end date, on any day of the week.
Real teams slip a planning meeting or lose a Monday to a holiday, so sprints drift off a fixed weekday within a few months. Sprints never overlap but may leave days between them (a holiday week, a hardening week); those days belong to no sprint, and the next sprint says so ("Starts 3 days after Sprint 25 ended"). The newest sprint runs to the later of its end date and today, which keeps today's commits visible even when nobody has started the next sprint yet ("3 days past its end date").
New sprint defaults to today and takes any other dates.
Most sprints are created the day they start; the start field covers the ones created a few days late (backdate it) or early (schedule it up to four weeks ahead, where it shows as Planned with dashed upcoming days). The end follows the start at 14 days until it is picked by hand. Creating a sprint closes the newest one the day before, and a consequence line says so before anything is saved: "Sprint 26 closes Sep 25 after 2 days".
Every sprint's dates can be edited, and editing moves nothing else.
"Edit dates" in a sprint's own view opens the same start and end fields, bounded by the sprints either side, so an edit can never take a day that belongs to another sprint. Rejected dates name the neighbour in the way ("Sprint 26 starts Thu, Sep 24, so this one has to end by Sep 23") rather than just refusing, and the consequence line reports any gap the edit opens. The alternative, letting an end date push the next sprint's start along, was rejected: one edit quietly rewriting a second sprint is the kind of change nobody notices until the totals look wrong.
The overview is calendar-aligned; the detail view is sprint-aligned.
In the overview, rows are Sunday to Saturday as on GitHub, so a sprint that began on a Thursday opens four cells down its first column and its start date sits under the block. Inside a sprint, the headers rotate to start on the sprint's own first weekday (Thu to Wed), so fourteen days are two full rows.
Counts are pinned to calendar dates, not grid positions.
seededCount(day) takes a day number, so Sep 24 has the same count whichever sprint it lands in after a date moves. Weekends run at a third of weekdays, which gives the weekday rows a real signal. Today comes from useSyncExternalStore with a fixed server date, and month names are hand-written rather than toLocaleDateString, so the server and client never disagree on text.
History is 26 sprints deep with five on screen.
Chevrons either side of the window page back a year, and a second pair inside a sprint steps to its neighbours. The window follows you, so coming back out of Sprint 3 lands on the page holding Sprint 3 rather than back at the newest.
The subtitle counts the window, not the year.
"98 commits in sprints 17 to 21", for the same reason the log row count matches the sprint total: a headline should count what is actually on screen. Keeping a year costs nothing because seededCount is a pure function of the date, so the 21 sprints behind the window are the same arithmetic as the five in it. The window is counted back from the newest page, so adding a sprint keeps you on the newest page.

One file, no runtime package. Copy it, or install it with the command at the top of this page.

CommitGraph.tsx
"use client";import { useEffect, useId, useMemo, useRef, useState, useSyncExternalStore, type CSSProperties, type FormEvent, type ReactNode } from "react";import { motion, useReducedMotion } from "framer-motion";import {  GitCommitIcon as PhGitCommit,  CircleNotchIcon as PhCircleNotch,  FlameIcon as PhFlame,  ArrowCounterClockwiseIcon as PhReset,  ArrowLeftIcon as PhArrowLeft,  CaretLeftIcon as PhCaretLeft,  CaretRightIcon as PhCaretRight,  PlusIcon as PhPlus,  PencilSimpleIcon as PhPencil,} from "@phosphor-icons/react";// A sprint is a start and an end date, on any weekday. Sprints never overlap,// but they may leave days between them (a holiday week, a hardening week), and// those days belong to no sprint. The newest sprint runs to the later of its// end and today: a team that forgets to start the next one still sees today's// commits land somewhere.const PLANNED_DAYS = 14;// The longest sprint the date fields accept.const MAX_SPRINT_DAYS = 56;// A year of history, five sprints on screen at a time.const SEEDED_SPRINTS = 26;const VISIBLE_SPRINTS = 5;// How far ahead a sprint can be scheduled when it is created early.const MAX_LEAD_DAYS = 28;const DAY_MS = 86_400_000;const DAYS = 7;const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];const COMMITS_KEY = "ui-commit-graph:commits";const SPRINTS_KEY = "ui-commit-graph:sprints";const THEME_KEY = "ui-commit-graph:theme";// The server cannot know what day it is for the viewer, so the first paint// renders against a fixed date and the client swaps in the real one during// hydration (see useToday).const SERVER_TODAY = "2026-09-29";type ThemeId = "emerald" | "blue" | "violet" | "amber" | "rose";interface Theme {  label: string;  swatch: string;  light: [string, string, string, string];  dark: [string, string, string, string];}const THEMES: Record<ThemeId, Theme> = {  emerald: { label: "Emerald", swatch: "#16A34A", light: ["#BBF7D0", "#4ADE80", "#16A34A", "#166534"], dark: ["#14532D", "#15803D", "#22C55E", "#86EFAC"] },  blue: { label: "Blue", swatch: "#2563EB", light: ["#BFDBFE", "#60A5FA", "#2563EB", "#1E3A8A"], dark: ["#1E3A8A", "#1D4ED8", "#3B82F6", "#93C5FD"] },  violet: { label: "Violet", swatch: "#7C3AED", light: ["#DDD6FE", "#A78BFA", "#7C3AED", "#4C1D95"], dark: ["#4C1D95", "#6D28D9", "#8B5CF6", "#C4B5FD"] },  amber: { label: "Amber", swatch: "#D97706", light: ["#FDE68A", "#FBBF24", "#D97706", "#92400E"], dark: ["#78350F", "#B45309", "#F59E0B", "#FCD34D"] },  rose: { label: "Rose", swatch: "#DB2777", light: ["#FBCFE8", "#F472B6", "#DB2777", "#831843"], dark: ["#831843", "#BE185D", "#EC4899", "#F9A8D4"] },};const THEME_IDS = Object.keys(THEMES) as ThemeId[];// Dates are handled as whole UTC day numbers (days since 1970-01-01). Adding a// day is adding one, and no daylight-saving change can make a day 23 hours long.function dayNumber(iso: string): number {  const [y, m, d] = iso.split("-").map(Number);  return Math.round(Date.UTC(y, m - 1, d) / DAY_MS);}function isoFromDay(day: number): string {  return new Date(day * DAY_MS).toISOString().slice(0, 10);}function weekdayOf(day: number): number {  return new Date(day * DAY_MS).getUTCDay();}// Month names by hand rather than toLocaleDateString: the server and the// browser can disagree on locale, and text that differs between them is a// hydration error.function shortDate(day: number): string {  const d = new Date(day * DAY_MS);  return `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;}function longDate(day: number): string {  return `${WEEKDAYS[weekdayOf(day)].slice(0, 3)}, ${shortDate(day)}`;}function relativeDay(day: number, today: number): string {  const diff = day - today;  if (diff === 0) return "today";  if (diff === -1) return "yesterday";  if (diff === 1) return "tomorrow";  return diff < 0 ? `${-diff} days ago` : `in ${diff} days`;}// Pure function of the calendar date, not of when the page is viewed: Sep 24// has the same count today and next month. Weekends run at a third, so the// weekday axis carries a real signal.function seededCount(day: number): number {  const x = Math.sin((day + 11) * 12.9898 + 78.233) * 43758.5453;  const frac = x - Math.floor(x);  let count = 0;  if (frac > 0.94) count = 8 + (Math.floor(frac * 37) % 6);  else if (frac > 0.78) count = 4 + (Math.floor(frac * 23) % 4);  else if (frac > 0.35) count = 1 + (Math.floor(frac * 17) % 3);  return weekdayOf(day) % 6 === 0 ? Math.floor(count / 3) : count;}// Integer hash (Math.imul, exact in every engine), deliberately not the// sin-based seededCount above. A bucketed level survives a 1-ULP difference// between two engines' Math.sin; a message index or a SHA does not, and text// that differs between the server and the client is a hydration error.function hash32(seed: number): number {  let h = Math.imul(seed ^ 0x9e3779b9, 0x85ebca6b);  h ^= h >>> 13;  h = Math.imul(h, 0xc2b2ae35);  h ^= h >>> 16;  return h >>> 0;}interface SprintRange {  start: number;  end: number;}// The demo's history: 26 back-to-back sprints of 12 to 16 days, the newest// started five days ago. Uneven lengths put every sprint on a different// weekday, which is what real teams get once a holiday or a slipped planning// meeting has moved the start once or twice.function seededRanges(today: number): SprintRange[] {  const starts = [today - 5];  for (let i = 1; i < SEEDED_SPRINTS; i++) {    starts.unshift(starts[0] - (12 + (hash32(i * 131 + 17) % 5)));  }  return starts.map((start, i) => ({ start, end: i < starts.length - 1 ? starts[i + 1] - 1 : start + PLANNED_DAYS - 1 }));}// A real deploy reads these from the git log (or the host's API). They are// generated here for the same reason the counts are: the site is a static// export, so the demo has to carry its own history.const SUBJECTS = [  "fix: guard the tick generator against an empty range",  "feat: add orientation to the chart frame",  "refactor: pull the tooltip anchor out of the crosshair",  "fix: stop the axis rounding away a fifth of the plot",  "chore: bump framer-motion to 11.3.2",  "test: cover the quantile legend ticks",  "perf: batch the dot layer into one path",  "fix: round projected coords so SSR and the client agree",  "docs: note why the preview files are generated",  "feat: keyboard roving across the heat map cells",  "fix: dark ramp was two steps too light at level 3",  "refactor: hoist the shared spline into the frame",  "chore: regenerate the preview chunks",  "fix: legend toggle left the y domain in dead space",  "feat: sticky first column on the data table",  "style: align the tick labels to the baseline",  "fix: reset the day filter when the sprint changes",  "test: snapshot the empty state",  "perf: memoize the grid so hover stops re-rendering it",  "fix: focus ring vanished at the light end of the ramp",  "feat: copy button on the code block",  "refactor: drop the wrapper div around every cell",  "chore: pin the node version in CI",  "fix: tooltip clipped inside the scroll container",  "docs: rewrite the install steps for bun",  "feat: reduced-motion path for the reveal",  "fix: aria-label read the count without the day",  "revert: put the old easing back on the drawer",  "chore: delete the unused ramp helper",  "fix: off-by-one in the week boundary",];interface Commit {  sha: string;  time: string;  subject: string;}// Every stride is coprime with SUBJECTS.length, so walking the pool by one of// them visits every subject before repeating: a day never lands the same commit// message twice, which is what gives a hashed message away as fake.const STRIDES = [7, 11, 13, 17, 19, 23];function commitsFor(day: number, count: number): Commit[] {  const base = hash32(day * 31 + 7) % SUBJECTS.length;  const stride = STRIDES[hash32(day * 17 + 5) % STRIDES.length];  return Array.from({ length: count }, (_, i) => {    const h = hash32(day * 97 + i * 7919 + 3);    return { h, minutes: 8 * 60 + (h % 660) };  })    .sort((a, b) => a.minutes - b.minutes)    .map(({ h, minutes }, i) => ({      sha: h.toString(16).padStart(8, "0").slice(0, 7),      time: `${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`,      subject: SUBJECTS[(base + i * stride) % SUBJECTS.length],    }));}function levelFor(count: number): 0 | 1 | 2 | 3 | 4 {  if (count <= 0) return 0;  if (count <= 2) return 1;  if (count <= 5) return 2;  if (count <= 9) return 3;  return 4;}interface Day {  day: number;  count: number;  upcoming: boolean;}interface Sprint {  start: number;  end: number;  days: Day[];  status: "done" | "current" | "planned";  overBy: number;  total: number;  activeDays: number;  elapsedDays: number;  best: number;}// Where the newest sprint actually ends: its own end, or today if it has run// past that with nothing started after it.function effectiveEnd(range: SprintRange, isNewest: boolean, today: number): number {  return isNewest && range.start <= today ? Math.max(range.end, today) : range.end;}function buildSprints(ranges: SprintRange[], today: number, extra: Record<string, number>): Sprint[] {  return ranges.map((range, i) => {    const { start } = range;    const isNewest = i === ranges.length - 1;    const end = effectiveEnd(range, isNewest, today);    const days: Day[] = [];    for (let day = start; day <= end; day++) {      const upcoming = day > today;      days.push({ day, upcoming, count: upcoming ? 0 : seededCount(day) + (extra[isoFromDay(day)] ?? 0) });    }    const elapsed = days.filter((d) => !d.upcoming);    return {      start,      end,      days,      status: start > today ? "planned" : end < today ? "done" : "current",      overBy: end - range.end,      total: elapsed.reduce((sum, d) => sum + d.count, 0),      activeDays: elapsed.filter((d) => d.count > 0).length,      elapsedDays: elapsed.length,      best: elapsed.reduce((max, d) => Math.max(max, d.count), 0),    };  });}// The overview lays each sprint on the calendar, GitHub-style: rows are Sunday// to Saturday, so a sprint that starts on a Wednesday opens three cells down// its first column. Nulls pad the partial weeks at either end.function calendarColumns(days: Day[]): (Day | null)[][] {  const cells: (Day | null)[] = [...Array<null>(weekdayOf(days[0].day)).fill(null), ...days];  while (cells.length % DAYS) cells.push(null);  return Array.from({ length: cells.length / DAYS }, (_, c) => cells.slice(c * DAYS, (c + 1) * DAYS));}// The detail view counts from the sprint's own first day instead, so a// Wednesday sprint reads Wed to Tue across the top and two full rows down.function sprintRows(days: Day[]): (Day | null)[][] {  return Array.from({ length: Math.ceil(days.length / DAYS) }, (_, r) => {    const row: (Day | null)[] = days.slice(r * DAYS, (r + 1) * DAYS);    while (row.length < DAYS) row.push(null);    return row;  });}const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;function parseDate(value: unknown): number | null {  return typeof value === "string" && ISO_DATE.test(value) ? dayNumber(value) : null;}// Stored as [{ start, end }] in ISO dates. Anything overlapping the sprint// before it is dropped rather than guessed at, so a hand-edited value can// never produce two sprints claiming the same day.function parseRanges(raw: string | null): SprintRange[] | null {  if (!raw) return null;  try {    const list: unknown = JSON.parse(raw);    if (!Array.isArray(list)) return null;    const ranges = list      .map((item: unknown) => {        const { start, end } = (item ?? {}) as { start?: unknown; end?: unknown };        const s = parseDate(start);        const e = parseDate(end);        return s !== null && e !== null && e >= s ? { start: s, end: e } : null;      })      .filter((r): r is SprintRange => r !== null)      .sort((a, b) => a.start - b.start)      .filter((r, i, all) => i === 0 || r.start > all[i - 1].end);    return ranges.length > 0 ? ranges : null;  } catch {    return null;  }}function parseCommits(raw: string | null): Record<string, number> {  if (!raw) return {};  try {    const map: unknown = JSON.parse(raw);    return map && typeof map === "object" && !Array.isArray(map) ? (map as Record<string, number>) : {};  } catch {    return {};  }}const subscribeNever = () => () => {};// Read through useSyncExternalStore rather than seeding useState from// localStorage in an effect: the server snapshot is what both the server and// the first client paint render, so there's nothing for hydration to disagree// about, and no cascading setState-in-effect render. Raw strings, not parsed// values, so the snapshot compares equal between reads.function readKey(key: string): string | null {  try {    return localStorage.getItem(key);  } catch {    return null;  }}function getSprintsSnapshot() {  return readKey(SPRINTS_KEY);}function getCommitsSnapshot() {  return readKey(COMMITS_KEY);}function getNullSnapshot() {  return null;}function getThemeSnapshot(): ThemeId {  const stored = readKey(THEME_KEY) as ThemeId | null;  return stored && stored in THEMES ? stored : "emerald";}function getThemeServerSnapshot(): ThemeId {  return "emerald";}// The viewer's local calendar date, not the UTC one: a commit at 9pm in// California belongs to that evening, not to tomorrow.function getTodaySnapshot(): string {  const d = new Date();  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;}function getTodayServerSnapshot(): string {  return SERVER_TODAY;}function writeKey(key: string, value: string | null) {  try {    if (value === null) localStorage.removeItem(key);    else localStorage.setItem(key, value);  } catch {    // no-op  }}function GitCommitIcon() {  return <PhGitCommit size={13} weight="bold" aria-hidden />;}function SpinnerIcon() {  return <PhCircleNotch size={13} weight="bold" aria-hidden />;}function FlameIcon() {  return <PhFlame size={11} weight="fill" aria-hidden />;}function ResetIcon() {  return <PhReset size={11} aria-hidden />;}function BackIcon() {  return <PhArrowLeft size={12} weight="bold" aria-hidden />;}function OlderIcon() {  return <PhCaretLeft size={12} weight="bold" aria-hidden />;}function NewerIcon() {  return <PhCaretRight size={12} weight="bold" aria-hidden />;}function PlusIcon() {  return <PhPlus size={12} weight="bold" aria-hidden />;}function PencilIcon() {  return <PhPencil size={11} weight="bold" aria-hidden />;}const FOCUS =  "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-600 dark:focus-visible:outline-emerald-400";const INPUT =  "h-8 w-full min-w-0 rounded-md border border-stone-300 bg-stone-100 px-3 text-sm tabular-nums text-stone-900 outline-none focus:border-emerald-600 focus:ring-2 focus:ring-emerald-600/25 dark:border-stone-700 dark:bg-stone-950 dark:text-stone-100 dark:[color-scheme:dark] dark:focus:border-emerald-400 dark:focus:ring-emerald-400/25";const EYEBROW = "text-[11px] font-medium uppercase tracking-[0.08em] text-stone-500 dark:text-stone-500";interface DraftError {  field: "start" | "end";  message: string;}interface Bounds {  min: number | null;  max: number;}interface SprintDatesFormProps {  title: string;  start: string;  end: string;  startBounds: Bounds;  endBounds: Bounds;  today: number;  error: DraftError | null;  hint: ReactNode;  submitLabel: string;  onStartChange: (value: string) => void;  onEndChange: (value: string) => void;  onSubmit: () => void;  onCancel: () => void;}// One form for both jobs, creating a sprint and changing an existing sprint's// dates. A new sprint defaults to starting today because most sprints are// created the day they start; the fields are there for the ones that are not.function SprintDatesForm({  title,  start,  end,  startBounds,  endBounds,  today,  error,  hint,  submitLabel,  onStartChange,  onEndChange,  onSubmit,  onCancel,}: SprintDatesFormProps) {  const shouldReduceMotion = useReducedMotion();  const startRef = useRef<HTMLInputElement | null>(null);  const id = useId();  const hintId = `${id}-hint`;  const todayIso = isoFromDay(today);  const canPickToday = start !== todayIso && today <= startBounds.max && (startBounds.min === null || today >= startBounds.min);  useEffect(() => {    startRef.current?.focus({ preventScroll: true });  }, []);  const submit = (event: FormEvent) => {    event.preventDefault();    if (!error) onSubmit();  };  return (    <motion.form      onSubmit={submit}      aria-label={title}      initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: -4 }}      animate={{ opacity: 1, y: 0 }}      transition={{ duration: 0.18, ease: [0.16, 1, 0.3, 1] }}      className="mt-3 rounded-xl border border-stone-200 p-3 dark:border-stone-800"    >      <p className="text-sm font-medium text-stone-900 dark:text-stone-100">{title}</p>      <div className="mt-2 grid grid-cols-2 gap-2">        <div>          <div className="flex h-4 items-center justify-between gap-2">            <label htmlFor={`${id}-start`} className={EYEBROW}>              Starts            </label>            {canPickToday && (              <button                type="button"                onClick={() => onStartChange(todayIso)}                className={`rounded text-[11px] font-medium text-emerald-700 hover:underline dark:text-emerald-400 ${FOCUS}`}              >                Today              </button>            )}          </div>          <input            ref={startRef}            id={`${id}-start`}            type="date"            required            value={start}            min={startBounds.min === null ? undefined : isoFromDay(startBounds.min)}            max={isoFromDay(startBounds.max)}            onChange={(event) => onStartChange(event.target.value)}            aria-invalid={error?.field === "start" ? true : undefined}            aria-describedby={hintId}            className={`mt-1 ${INPUT}`}          />        </div>        <div>          <div className="flex h-4 items-center">            <label htmlFor={`${id}-end`} className={EYEBROW}>              Ends            </label>          </div>          <input            id={`${id}-end`}            type="date"            required            value={end}            min={endBounds.min === null ? undefined : isoFromDay(endBounds.min)}            max={isoFromDay(endBounds.max)}            onChange={(event) => onEndChange(event.target.value)}            aria-invalid={error?.field === "end" ? true : undefined}            aria-describedby={hintId}            className={`mt-1 ${INPUT}`}          />        </div>      </div>      <p id={hintId} className={`mt-2 text-xs ${error ? "text-red-600 dark:text-red-400" : "text-stone-600 dark:text-stone-400"}`}>        {error?.message ?? hint}      </p>      <div className="mt-3 flex justify-end gap-2">        <button          type="button"          onClick={onCancel}          className={`h-8 rounded-md border border-stone-300 px-3 text-xs font-medium text-stone-700 transition-colors duration-150 hover:bg-stone-100 active:scale-[0.98] dark:border-stone-700 dark:text-stone-200 dark:hover:bg-stone-800 ${FOCUS}`}        >          Cancel        </button>        <button          type="submit"          disabled={error !== null}          className={`h-8 rounded-md bg-emerald-700 px-3 text-xs font-medium text-white transition-colors duration-150 hover:bg-emerald-800 active:scale-[0.98] active:bg-emerald-900 disabled:pointer-events-none disabled:opacity-50 ${FOCUS}`}        >          {submitLabel}        </button>      </div>    </motion.form>  );}type Draft = { mode: "create" } | { mode: "edit"; index: number };export function CommitGraph() {  const shouldReduceMotion = useReducedMotion();  const todayIso = useSyncExternalStore(subscribeNever, getTodaySnapshot, getTodayServerSnapshot);  const persistedSprints = useSyncExternalStore(subscribeNever, getSprintsSnapshot, getNullSnapshot);  const persistedCommits = useSyncExternalStore(subscribeNever, getCommitsSnapshot, getNullSnapshot);  const persistedTheme = useSyncExternalStore(subscribeNever, getThemeSnapshot, getThemeServerSnapshot);  // undefined means "no change this session, use what storage holds"; null  // means "cleared", which is different from never having been set.  const [sprintsOverride, setSprintsOverride] = useState<string | null | undefined>(undefined);  const [commitsOverride, setCommitsOverride] = useState<string | null | undefined>(undefined);  const [themeOverride, setThemeOverride] = useState<ThemeId | null>(null);  const [committing, setCommitting] = useState(false);  const [justCommitted, setJustCommitted] = useState(false);  const [selected, setSelected] = useState<number | null>(null);  const [selectedDay, setSelectedDay] = useState<number | null>(null);  // Counted back from the newest page rather than forward from the oldest, so  // adding a sprint keeps you on the newest page instead of one behind it.  const [pagesBack, setPagesBack] = useState(0);  const [pageDir, setPageDir] = useState(0);  const [draft, setDraft] = useState<Draft | null>(null);  const [draftStart, setDraftStart] = useState("");  const [draftEnd, setDraftEnd] = useState("");  // A new sprint's end follows its start at the planned length until the end  // is picked by hand; after that the two move independently.  const [endTouched, setEndTouched] = useState(false);  const [announcement, setAnnouncement] = useState("");  const sprintRefs = useRef<(HTMLButtonElement | null)[]>([]);  const backRef = useRef<HTMLButtonElement | null>(null);  const draftTriggerRef = useRef<HTMLButtonElement | null>(null);  const returnFocusTo = useRef<number | null>(null);  const focusAfterCreate = useRef<number | null>(null);  const wasSelected = useRef<number | null>(null);  const today = dayNumber(todayIso);  const sprintsRaw = sprintsOverride === undefined ? persistedSprints : sprintsOverride;  const commitsRaw = commitsOverride === undefined ? persistedCommits : commitsOverride;  const theme = themeOverride ?? persistedTheme;  const customRanges = useMemo(() => parseRanges(sprintsRaw), [sprintsRaw]);  const ranges = useMemo(() => customRanges ?? seededRanges(today), [customRanges, today]);  const extra = useMemo(() => parseCommits(commitsRaw), [commitsRaw]);  const sprints = useMemo(() => buildSprints(ranges, today, extra), [ranges, today, extra]);  const sprintColumns = useMemo(() => sprints.map((sprint) => calendarColumns(sprint.days)), [sprints]);  const currentIndex = sprints.findIndex((sprint) => sprint.status === "current");  const current = currentIndex === -1 ? null : sprints[currentIndex];  const lastIndex = sprints.length - 1;  const maxStart = Math.max(0, sprints.length - VISIBLE_SPRINTS);  const windowStart = Math.min(Math.max(maxStart - pagesBack, 0), maxStart);  const windowEnd = Math.min(windowStart + VISIBLE_SPRINTS, sprints.length);  const visibleSprints = sprints.slice(windowStart, windowEnd);  // The window's own total, not the year's: the headline should count what is  // actually on screen.  const windowTotal = visibleSprints.reduce((sum, sprint) => sum + sprint.total, 0);  const atOldest = windowStart === 0;  const atNewest = windowStart >= maxStart;  const streak = useMemo(() => {    let count = 0;    for (let day = today; day >= ranges[0].start; day--) {      if (seededCount(day) + (extra[isoFromDay(day)] ?? 0) > 0) count++;      else break;    }    return count;  }, [today, ranges, extra]);  const detail = selected === null ? null : (sprints[selected] ?? null);  const isCurrentSprint = selected !== null && selected === currentIndex;  const delta =    selected === null || selected === 0 || detail?.status === "planned" ? null : sprints[selected].total - sprints[selected - 1].total;  const sprintLog = useMemo(    () => (detail === null ? [] : detail.days.map((d) => ({ ...d, commits: commitsFor(d.day, d.count) }))),    [detail],  );  const visibleDays = selectedDay === null ? sprintLog.filter((d) => d.count > 0) : sprintLog.filter((d) => d.day === selectedDay);  // Focus follows the view, in both directions: into the sprint it lands on the  // way back out, and on return it lands on the sprint you came from rather  // than at the top of the card.  useEffect(() => {    const was = wasSelected.current;    wasSelected.current = selected;    if (selected !== null && was === null) {      backRef.current?.focus({ preventScroll: true });    } else if (selected !== null && document.activeElement === document.body) {      // Stepping onto the first or last sprint disables the chevron that was      // pressed, and a disabled button drops focus to the page.      backRef.current?.focus({ preventScroll: true });    } else if (selected === null && was !== null && returnFocusTo.current !== null) {      sprintRefs.current[returnFocusTo.current]?.focus({ preventScroll: true });      returnFocusTo.current = null;    }  }, [selected]);  // A new sprint's block only exists after the render that adds it.  useEffect(() => {    if (focusAfterCreate.current === null) return;    sprintRefs.current[focusAfterCreate.current]?.focus({ preventScroll: true });    focusAfterCreate.current = null;  }, [sprints]);  const moveWindowTo = (start: number) => setPagesBack(maxStart - Math.min(Math.max(start, 0), maxStart));  // Stepping to a sprint also drags the overview window along, so coming back  // out lands on the sprint you were reading rather than where you started.  const openSprint = (si: number) => {    const target = Math.min(Math.max(si, 0), lastIndex);    returnFocusTo.current = target;    setSelected(target);    setSelectedDay(null);    setDraft(null);    moveWindowTo(Math.min(Math.max(windowStart, target - VISIBLE_SPRINTS + 1), target));  };  const page = (direction: -1 | 1) => {    setPageDir(direction);    moveWindowTo(windowStart + direction * VISIBLE_SPRINTS);  };  const closeDetail = () => {    setSelected(null);    setSelectedDay(null);    setDraft(null);  };  const activeTheme = THEMES[theme];  const ramp = (level: 0 | 1 | 2 | 3 | 4, isDark: boolean) => (level === 0 ? null : (isDark ? activeTheme.dark : activeTheme.light)[level - 1]);  const changeTheme = (id: ThemeId) => {    setThemeOverride(id);    writeKey(THEME_KEY, id);  };  const saveRanges = (next: SprintRange[]) => {    const raw = JSON.stringify(next.map((r) => ({ start: isoFromDay(r.start), end: isoFromDay(r.end) })));    writeKey(SPRINTS_KEY, raw);    setSprintsOverride(raw);  };  const handleCommit = () => {    if (committing) return;    setCommitting(true);    // A real deploy of this component would POST to /api/commits and read the    // updated count back; this demo simulates that round trip's latency and    // persists the result the same place a server response would end up    // caching to, so the count survives a reload without a real backend.    // Keyed by date, so tomorrow's commits land on tomorrow.    const latency = 420 + Math.round(Math.random() * 260);    window.setTimeout(() => {      const raw = JSON.stringify({ ...extra, [todayIso]: (extra[todayIso] ?? 0) + 1 });      writeKey(COMMITS_KEY, raw);      setCommitsOverride(raw);      setCommitting(false);      setJustCommitted(true);      window.setTimeout(() => setJustCommitted(false), 500);    }, latency);  };  const handleReset = () => {    writeKey(COMMITS_KEY, null);    writeKey(SPRINTS_KEY, null);    setCommitsOverride(null);    setSprintsOverride(null);    closeDetail();    setPagesBack(0);    setAnnouncement("Sprints and commits reset to the sample history.");  };  const draftStartDay = ISO_DATE.test(draftStart) ? dayNumber(draftStart) : null;  const draftEndDay = ISO_DATE.test(draftEnd) ? dayNumber(draftEnd) : null;  const newest = ranges[lastIndex];  // Where each field may land. A new sprint has to start after the newest one  // starts (otherwise it would swallow it whole) and at most four weeks out.  // An existing sprint keeps clear of its neighbours: nothing else moves when  // its dates change, so it can never take days that belong to another sprint.  const draftLimits = (() => {    if (draft === null) return null;    const anchor = draftStartDay ?? (draft.mode === "create" ? today : ranges[draft.index].start);    const endMax = (next: SprintRange | undefined) => (next ? Math.min(next.start - 1, anchor + MAX_SPRINT_DAYS - 1) : anchor + MAX_SPRINT_DAYS - 1);    if (draft.mode === "create") {      return {        start: { min: newest.start + 1, max: today + MAX_LEAD_DAYS },        end: { min: draftStartDay, max: endMax(undefined) },      };    }    const i = draft.index;    const prev = ranges[i - 1];    const next = ranges[i + 1];    return {      start: { min: prev ? prev.end + 1 : null, max: next ? next.start - 1 : today + MAX_LEAD_DAYS },      end: { min: draftStartDay, max: endMax(next) },    };  })();  // Creating a sprint closes the newest one the day before it starts. If the  // new one starts later than that, the newest keeps its own end (or today, if  // it had already run past it) and the days between belong to no sprint.  const newestEndAfterCreate = (start: number) => {    const runsTo = effectiveEnd(newest, true, today);    return start <= runsTo + 1 ? start - 1 : runsTo;  };  const draftError = ((): DraftError | null => {    if (draft === null || draftLimits === null) return null;    if (draftStartDay === null) return { field: "start", message: "Pick a date for the sprint to start." };    if (draftEndDay === null) return { field: "end", message: "Pick a date for the sprint to end." };    const { start, end } = draftLimits;    const i = draft.mode === "create" ? lastIndex + 1 : draft.index;    if (start.min !== null && draftStartDay < start.min) {      return draft.mode === "create"        ? { field: "start", message: `Has to be after Sprint ${i} starts on ${longDate(start.min - 1)}.` }        : { field: "start", message: `Sprint ${i} ends ${longDate(start.min - 1)}, so this one can start ${shortDate(start.min)} at the earliest.` };    }    if (draftStartDay > start.max) {      return draft.mode === "edit" && draft.index < lastIndex        ? { field: "start", message: `Sprint ${i + 2} starts ${longDate(start.max + 1)}, so this one has to start before it.` }        : { field: "start", message: `Sprints can be scheduled up to four weeks ahead, ${longDate(start.max)} at the latest.` };    }    if (draftEndDay < draftStartDay) return { field: "end", message: "The end date is before the start date." };    const next = draft.mode === "edit" ? ranges[draft.index + 1] : undefined;    if (next && draftEndDay >= next.start) {      return { field: "end", message: `Sprint ${i + 2} starts ${longDate(next.start)}, so this one has to end by ${shortDate(next.start - 1)}.` };    }    if (draftEndDay > end.max) {      return { field: "end", message: `Sprints run eight weeks at most, so ${longDate(end.max)} at the latest.` };    }    return null;  })();  const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? "" : "s"}`;  // The consequence line. Dates decide more than this sprint's own length: a  // new sprint closes the one before it, and any sprint can leave days that  // belong to no sprint. The form says so before anything is saved.  const draftHint = (() => {    if (draft === null || draftStartDay === null || draftEndDay === null || draftEndDay < draftStartDay) return null;    const parts = [`${plural(draftEndDay - draftStartDay + 1, "day")}, starting ${relativeDay(draftStartDay, today)}.`];    if (draft.mode === "create") {      const closesOn = newestEndAfterCreate(draftStartDay);      if (closesOn !== newest.end) {        const length = closesOn - newest.start + 1;        parts.push(          closesOn < today            ? `Sprint ${lastIndex + 1} closes ${shortDate(closesOn)} after ${plural(length, "day")}.`            : `Sprint ${lastIndex + 1} now ends ${shortDate(closesOn)}, ${plural(length, "day")} in all.`,        );      }      const gap = draftStartDay - closesOn - 1;      if (gap > 0) parts.push(`The ${plural(gap, "day")} between them belong to no sprint.`);    } else {      const i = draft.index;      const prev = ranges[i - 1];      const next = ranges[i + 1];      const gapBefore = prev ? draftStartDay - prev.end - 1 : 0;      const gapAfter = next ? next.start - draftEndDay - 1 : 0;      if (gapBefore > 0) parts.push(`Leaves ${plural(gapBefore, "day")} after Sprint ${i} in no sprint.`);      if (gapAfter > 0) parts.push(`Leaves ${plural(gapAfter, "day")} before Sprint ${i + 2} in no sprint.`);      if (!next && draftStartDay <= today && draftEndDay < today) {        parts.push("It keeps collecting commits past that until the next sprint starts.");      }    }    return parts.join(" ");  })();  const openDraft = (next: Draft, trigger: HTMLButtonElement) => {    draftTriggerRef.current = trigger;    setDraft(next);    if (next.mode === "create") {      setDraftStart(todayIso);      setDraftEnd(isoFromDay(today + PLANNED_DAYS - 1));      setEndTouched(false);    } else {      setDraftStart(isoFromDay(ranges[next.index].start));      setDraftEnd(isoFromDay(ranges[next.index].end));      setEndTouched(true);    }  };  const changeDraftStart = (value: string) => {    setDraftStart(value);    if (!endTouched && ISO_DATE.test(value)) setDraftEnd(isoFromDay(dayNumber(value) + PLANNED_DAYS - 1));  };  const changeDraftEnd = (value: string) => {    setDraftEnd(value);    setEndTouched(true);  };  const cancelDraft = () => {    setDraft(null);    draftTriggerRef.current?.focus({ preventScroll: true });  };  const submitDraft = () => {    if (draft === null || draftStartDay === null || draftEndDay === null || draftError) return;    const range = { start: draftStartDay, end: draftEndDay };    const span = `${longDate(range.start)} to ${longDate(range.end)}`;    if (draft.mode === "create") {      const index = ranges.length;      saveRanges([...ranges.slice(0, -1), { start: newest.start, end: newestEndAfterCreate(range.start) }, range]);      setDraft(null);      setPagesBack(0);      focusAfterCreate.current = index;      setAnnouncement(`Sprint ${index + 1} created, ${span}.`);    } else {      saveRanges(ranges.map((r, i) => (i === draft.index ? range : r)));      setDraft(null);      setSelectedDay(null);      draftTriggerRef.current?.focus({ preventScroll: true });      setAnnouncement(`Sprint ${draft.index + 1} now runs ${span}.`);    }  };  // Commit lands on today, so it is only offered where today is on screen: the  // overview page holding the current sprint, or that sprint's own view.  const currentOnScreen = currentIndex >= windowStart && currentIndex < windowEnd;  const showCommitAction = selected === null ? currentOnScreen : isCurrentSprint;  const showReset = Object.keys(extra).length > 0 || customRanges !== null;  const enter = shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 6 };  const cellFill = (level: 0 | 1 | 2 | 3 | 4, upcoming: boolean, className: string) =>    upcoming ? (      <span className={`absolute inset-0 border border-dashed border-stone-300 dark:border-stone-700 ${className}`} />    ) : (      <>        <span className={`absolute inset-0 block dark:hidden ${className}`} style={{ backgroundColor: ramp(level, false) ?? "rgb(226 232 240)" }} />        <span className={`absolute inset-0 hidden dark:block ${className}`} style={{ backgroundColor: ramp(level, true) ?? "rgb(255 255 255 / 0.08)" }} />      </>    );  return (    <div      onKeyDown={(event) => {        if (event.key !== "Escape") return;        if (draft !== null) cancelDraft();        else if (selected !== null) closeDetail();      }}      className="w-full max-w-md rounded-2xl border border-stone-200 bg-white p-4 dark:border-stone-800 dark:bg-stone-900"    >      <p className="sr-only" role="status" aria-live="polite">        {announcement}      </p>      <div className="flex flex-wrap items-start justify-between gap-3">        {detail === null || selected === null ? (          <div>            <div className="flex items-center gap-1.5">              <p className="text-sm font-semibold text-stone-800 dark:text-stone-100">Commit activity</p>              {streak > 0 && (                <motion.span                  initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.8 }}                  animate={{ opacity: 1, scale: 1 }}                  className="inline-flex items-center gap-0.5 rounded-full bg-orange-500/10 px-1.5 py-0.5 text-[11px] font-semibold text-orange-600 dark:text-orange-400"                >                  <FlameIcon />                  {streak}                </motion.span>              )}            </div>            <p className="text-xs text-stone-500 dark:text-stone-500">              {windowTotal} commits in sprints {windowStart + 1} to {windowEnd}            </p>            <p className="text-[11px] text-stone-500 dark:text-stone-500">              {current                ? `Sprint ${currentIndex + 1} began ${longDate(current.start)}, ${                    current.overBy > 0                      ? `${plural(current.overBy, "day")} past its end date`                      : `day ${today - current.start + 1} of ${current.end - current.start + 1}`                  }`                : sprints[lastIndex].status === "planned"                  ? `Between sprints. Sprint ${lastIndex + 1} starts ${longDate(sprints[lastIndex].start)}`                  : "Between sprints"}            </p>          </div>        ) : (          <div>            <div className="flex items-center gap-1.5">              <button                ref={backRef}                type="button"                onClick={closeDetail}                className={`inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[11px] font-medium text-stone-500 transition-colors hover:bg-black/5 hover:text-stone-700 dark:text-stone-400 dark:hover:bg-white/10 dark:hover:text-stone-200 ${FOCUS}`}              >                <BackIcon />                All sprints              </button>            </div>            <div className="mt-1 flex items-center gap-1.5">              <button                type="button"                onClick={() => openSprint(selected - 1)}                disabled={selected === 0}                aria-label={`Sprint ${selected}, the sprint before this one`}                title={selected === 0 ? "No older sprint" : `Sprint ${selected}`}                className={`inline-flex h-5 w-5 items-center justify-center rounded-full text-stone-500 transition-colors hover:bg-black/5 hover:text-stone-600 disabled:pointer-events-none disabled:opacity-50 dark:hover:bg-white/10 dark:hover:text-stone-300 ${FOCUS}`}              >                <OlderIcon />              </button>              <p className="text-sm font-semibold text-stone-800 dark:text-stone-100">Sprint {selected + 1}</p>              <button                type="button"                onClick={() => openSprint(selected + 1)}                disabled={selected === lastIndex}                aria-label={`Sprint ${selected + 2}, the sprint after this one`}                title={selected === lastIndex ? "No newer sprint" : `Sprint ${selected + 2}`}                className={`inline-flex h-5 w-5 items-center justify-center rounded-full text-stone-500 transition-colors hover:bg-black/5 hover:text-stone-600 disabled:pointer-events-none disabled:opacity-50 dark:hover:bg-white/10 dark:hover:text-stone-300 ${FOCUS}`}              >                <NewerIcon />              </button>              {detail.status !== "done" && (                <span className="rounded-full bg-stone-900/[0.06] px-1.5 py-0.5 text-[11px] font-medium uppercase tracking-[0.08em] text-stone-500 dark:bg-white/10 dark:text-stone-500">                  {detail.status === "current" ? "In progress" : "Planned"}                </span>              )}            </div>            <div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1">              <p className="text-xs text-stone-600 dark:text-stone-400">                {longDate(detail.start)} to {longDate(detail.end)}, {plural(detail.end - detail.start + 1, "day")}                {detail.overBy > 0 && `, ${detail.overBy} past its end date`}              </p>              <button                type="button"                onClick={(event) => (draft?.mode === "edit" ? cancelDraft() : openDraft({ mode: "edit", index: selected }, event.currentTarget))}                aria-expanded={draft?.mode === "edit"}                className={`inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[11px] font-medium text-emerald-700 transition-colors hover:bg-emerald-500/10 dark:text-emerald-400 ${FOCUS}`}              >                <PencilIcon />                Edit dates              </button>            </div>            {selected > 0 && ranges[selected].start - ranges[selected - 1].end > 1 && (              <p className="text-[11px] text-stone-500 dark:text-stone-500">                Starts {plural(ranges[selected].start - ranges[selected - 1].end - 1, "day")} after Sprint {selected} ended              </p>            )}            <p className="text-xs text-stone-500 dark:text-stone-500">              {detail.status === "planned" ? (                `Starts ${relativeDay(detail.start, today)}`              ) : (                <>                  {detail.total} commits, {detail.activeDays} of {detail.elapsedDays} days active, busiest {detail.best}                  {delta !== null && (                    <span className={delta > 0 ? "text-green-600 dark:text-green-400" : undefined}>                      {", "}                      {delta > 0 ? "+" : ""}                      {delta} vs S{selected}                    </span>                  )}                </>              )}            </p>          </div>        )}        <div className="flex items-center gap-1">          {showReset && selected === null && (            <button              type="button"              onClick={handleReset}              aria-label="Reset sprints and commits to the sample history"              title="Reset sprints and commits"              className={`inline-flex h-7 w-7 items-center justify-center rounded-full text-stone-500 transition-colors hover:bg-black/5 hover:text-stone-600 dark:hover:bg-white/10 dark:hover:text-stone-300 ${FOCUS}`}            >              <ResetIcon />            </button>          )}          {selected === null && (            <button              type="button"              onClick={(event) => (draft ? cancelDraft() : openDraft({ mode: "create" }, event.currentTarget))}              aria-expanded={draft?.mode === "create"}              className={`inline-flex items-center gap-1.5 rounded-full border border-stone-300 px-3 py-1.5 text-xs font-medium text-stone-700 transition-colors duration-150 hover:bg-stone-100 active:scale-[0.98] dark:border-stone-700 dark:text-stone-200 dark:hover:bg-stone-800 ${FOCUS}`}            >              <PlusIcon />              New sprint            </button>          )}          {showCommitAction && (            <button              type="button"              onClick={handleCommit}              disabled={committing}              className={`inline-flex items-center gap-1.5 rounded-full bg-emerald-700 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-emerald-800 disabled:pointer-events-none disabled:opacity-50 ${FOCUS}`}            >              {committing ? (                <motion.span animate={{ rotate: 360 }} transition={{ duration: 0.7, repeat: Infinity, ease: "linear" }}>                  <SpinnerIcon />                </motion.span>              ) : (                <GitCommitIcon />              )}              {committing ? "Committing…" : "Commit"}            </button>          )}        </div>      </div>      {draft !== null && draftLimits !== null && (        <SprintDatesForm          key={draft.mode === "create" ? "create" : `edit-${draft.index}`}          title={draft.mode === "create" ? `New sprint: Sprint ${ranges.length + 1}` : `Sprint ${draft.index + 1} dates`}          start={draftStart}          end={draftEnd}          startBounds={draftLimits.start}          endBounds={draftLimits.end}          today={today}          error={draftError}          hint={draftHint}          submitLabel={draft.mode === "create" ? "Create sprint" : "Save dates"}          onStartChange={changeDraftStart}          onEndChange={changeDraftEnd}          onSubmit={submitDraft}          onCancel={cancelDraft}        />      )}      {detail === null || selected === null ? (        <motion.div key="overview" initial={enter} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.22, ease: [0.16, 1, 0.3, 1] }}>          <div className="mt-4 flex items-center gap-1">            <button              type="button"              onClick={() => page(-1)}              disabled={atOldest}              aria-label="Show older sprints"              title="Older sprints"              className={`inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-stone-500 transition-colors hover:bg-black/5 hover:text-stone-600 disabled:pointer-events-none disabled:opacity-50 dark:hover:bg-white/10 dark:hover:text-stone-300 ${FOCUS}`}            >              <OlderIcon />            </button>            <motion.div              key={windowStart}              initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, x: pageDir * 14 }}              animate={{ opacity: 1, x: 0 }}              transition={{ duration: 0.24, ease: [0.16, 1, 0.3, 1] }}              className="flex-1 overflow-x-auto pb-1"              data-lenis-prevent            >              <div className="mx-auto flex w-max items-start gap-1">                {/* Weekday rows, GitHub's Mon / Wed / Fri, offset by the                    sprint buttons' padding so they line up with the cells. */}                <div className="flex flex-col gap-[3px] p-1 pr-0" aria-hidden>                  {WEEKDAYS.map((name, di) => (                    <span key={name} className="h-[11px] text-[11px] leading-[11px] text-stone-500 dark:text-stone-500">                      {di % 2 === 1 && di < 6 ? name.slice(0, 3) : ""}                    </span>                  ))}                </div>                <div className="flex gap-1" role="group" aria-label={`Sprints ${windowStart + 1} to ${windowEnd}`}>                  {visibleSprints.map((sprint, wi) => {                    const si = windowStart + wi;                    const isCurrent = si === currentIndex;                    const isPlanned = sprint.status === "planned";                    return (                      <button                        key={si}                        type="button"                        ref={(node) => {                          sprintRefs.current[si] = node;                        }}                        onClick={() => openSprint(si)}                        aria-label={`Sprint ${si + 1}${isCurrent ? " (current)" : isPlanned ? " (planned)" : ""}, ${longDate(sprint.start)} to ${longDate(sprint.end)}, ${sprint.total} commits. Open its heat map.`}                        title={`Sprint ${si + 1}: ${longDate(sprint.start)} to ${longDate(sprint.end)}`}                        className={`flex min-w-10 flex-col items-center rounded-lg p-1 transition-colors hover:bg-stone-900/[0.03] dark:hover:bg-white/[0.04] ${FOCUS}`}                      >                        <div className="flex gap-[3px]">                          {sprintColumns[si].map((column, ci) => (                            <div key={ci} className="flex flex-col gap-[3px]">                              {column.map((cell, di) => {                                if (cell === null) return <span key={di} aria-hidden className="h-[11px] w-[11px]" />;                                const isToday = cell.day === today;                                return (                                  <motion.div                                    key={di}                                    aria-hidden                                    title={`${isToday ? "Today, " : ""}${longDate(cell.day)}: ${cell.upcoming ? "upcoming" : `${cell.count} commit${cell.count === 1 ? "" : "s"}`}`}                                    initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.4 }}                                    animate={isToday && justCommitted ? { opacity: 1, scale: [1, 1.5, 1] } : { opacity: 1, scale: 1 }}                                    transition={                                      isToday && justCommitted                                        ? { duration: 0.45, ease: [0.16, 1, 0.3, 1] }                                        : { duration: 0.25, delay: Math.min(wi * 4 + ci, 24) * 0.012, ease: [0.16, 1, 0.3, 1] }                                    }                                    className={`relative h-[11px] w-[11px] overflow-hidden rounded-[2.5px] ${isToday ? "ring-1 ring-emerald-500/70 ring-offset-1 ring-offset-white dark:ring-offset-stone-950" : ""}`}                                  >                                    {cellFill(levelFor(cell.count), cell.upcoming, "rounded-[2.5px]")}                                  </motion.div>                                );                              })}                            </div>                          ))}                        </div>                        <span className="mt-2 flex flex-col items-center leading-tight">                          <span                            className={`text-[11px] uppercase tracking-[0.08em] ${                              isCurrent ? "text-stone-500 dark:text-stone-400" : "text-stone-400 dark:text-stone-600"                            }`}                          >                            S{si + 1}                          </span>                          <span                            className={`text-[11px] tabular-nums ${                              isCurrent ? "font-semibold text-stone-700 dark:text-stone-200" : "text-stone-500 dark:text-stone-500"                            }`}                          >                            {isPlanned ? "next" : sprint.total}                          </span>                          <span className="whitespace-nowrap text-[11px] tabular-nums text-stone-400 dark:text-stone-600">                            {shortDate(sprint.start)}                          </span>                        </span>                      </button>                    );                  })}                </div>              </div>            </motion.div>            <button              type="button"              onClick={() => page(1)}              disabled={atNewest}              aria-label="Show newer sprints"              title="Newer sprints"              className={`inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-stone-500 transition-colors hover:bg-black/5 hover:text-stone-600 disabled:pointer-events-none disabled:opacity-50 dark:hover:bg-white/10 dark:hover:text-stone-300 ${FOCUS}`}            >              <NewerIcon />            </button>          </div>        </motion.div>      ) : (        <motion.div          key={`sprint-${selected}-${detail.start}`}          initial={enter}          animate={{ opacity: 1, y: 0 }}          transition={{ duration: 0.22, ease: [0.16, 1, 0.3, 1] }}        >          <div className="mt-4">            {/* Headers start on the sprint's own first weekday, so a sprint                that began on a Wednesday reads Wed to Tue. */}            <div className="flex gap-2 pl-6" aria-hidden>              {Array.from({ length: DAYS }, (_, i) => WEEKDAYS[(weekdayOf(detail.start) + i) % DAYS]).map((day) => (                <span key={day} className="flex-1 text-center text-[11px] text-stone-500 dark:text-stone-500">                  {day.slice(0, 3)}                </span>              ))}            </div>            {sprintRows(detail.days).map((row, ri) => (              <div key={ri} className="mt-2 flex items-center gap-2">                <span className="w-4 shrink-0 text-[11px] uppercase tracking-[0.08em] text-stone-500 dark:text-stone-500" aria-hidden>                  W{ri + 1}                </span>                {row.map((cell, di) => {                  if (cell === null) return <span key={di} aria-hidden className="h-11 flex-1" />;                  const idx = ri * DAYS + di;                  const level = levelFor(cell.count);                  const isDaySelected = selectedDay === cell.day;                  const isToday = cell.day === today;                  const dayLabel = `${longDate(cell.day)}, ${cell.upcoming ? "upcoming" : `${cell.count} commit${cell.count === 1 ? "" : "s"}`}`;                  return (                    <motion.button                      key={di}                      type="button"                      disabled={cell.upcoming}                      onClick={() => setSelectedDay(isDaySelected ? null : cell.day)}                      aria-pressed={cell.upcoming ? undefined : isDaySelected}                      aria-label={`${isToday ? "Today. " : ""}${dayLabel}.${cell.upcoming ? "" : " Show this day's commits."}`}                      title={longDate(cell.day)}                      initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.9 }}                      animate={{ opacity: 1, scale: 1 }}                      transition={{ duration: 0.24, delay: Math.min(idx, 20) * 0.012, ease: [0.16, 1, 0.3, 1] }}                      className={`relative flex h-11 flex-1 items-center justify-center overflow-hidden rounded-lg text-xs font-semibold tabular-nums transition-transform hover:scale-[1.04] disabled:hover:scale-100 ${FOCUS} ${                        isDaySelected                          ? "ring-2 ring-stone-900/40 ring-offset-2 ring-offset-white dark:ring-white/50 dark:ring-offset-stone-950"                          : isToday                            ? "ring-1 ring-emerald-500/70 ring-offset-1 ring-offset-white dark:ring-offset-stone-950"                            : ""                      }`}                    >                      {cellFill(level, cell.upcoming, "rounded-lg")}                      {/* The ramp runs dark-on-light one way and light-on-dark the                          other, so the number needs its own pair, not one colour. */}                      <span className={`relative block dark:hidden ${level >= 3 ? "text-white" : "text-stone-700"}`}>                        {cell.count > 0 ? cell.count : ""}                      </span>                      <span className={`relative hidden dark:block ${level >= 3 ? "text-stone-900" : "text-stone-100"}`}>                        {cell.count > 0 ? cell.count : ""}                      </span>                    </motion.button>                  );                })}              </div>            ))}          </div>          <div className="mt-4 border-t border-stone-200/70 pt-2 dark:border-white/10">            <div className="flex items-center justify-between gap-2">              <p className="text-[11px] uppercase tracking-[0.08em] text-stone-500 dark:text-stone-500">                {selectedDay === null                  ? "Commits, day by day"                  : `${longDate(selectedDay)}, ${visibleDays[0]?.count ?? 0} commit${visibleDays[0]?.count === 1 ? "" : "s"}`}              </p>              {selectedDay !== null && (                <button                  type="button"                  onClick={() => setSelectedDay(null)}                  className={`rounded-full px-1.5 py-0.5 text-[11px] text-stone-500 transition-colors hover:bg-black/5 hover:text-stone-700 dark:text-stone-400 dark:hover:bg-white/10 dark:hover:text-stone-200 ${FOCUS}`}                >                  All days                </button>              )}            </div>            <div className="mt-1 max-h-44 overflow-y-auto pr-1" data-lenis-prevent>              {visibleDays.every((d) => d.count === 0) ? (                <p className="py-3 text-center text-[11px] text-stone-500 dark:text-stone-500">                  {detail.status === "planned"                    ? `Nothing yet. Commits land here from ${longDate(detail.start)}.`                    : selectedDay === null                      ? "No commits in this sprint yet."                      : "No commits on this day."}                </p>              ) : (                <ul className="space-y-2">                  {visibleDays.map((d) => (                    <li key={d.day}>                      {selectedDay === null && (                        <div className="flex items-baseline justify-between gap-2">                          <p className="text-[11px] font-medium uppercase tracking-[0.08em] text-stone-500 dark:text-stone-500">{longDate(d.day)}</p>                          <p className="text-[11px] tabular-nums text-stone-400 dark:text-stone-600">                            {d.count} commit{d.count === 1 ? "" : "s"}                          </p>                        </div>                      )}                      <ul className="mt-1 space-y-1">                        {d.commits.map((commit, ci) => (                          <li key={`${commit.sha}-${ci}`} className="flex items-baseline gap-2 text-[11px]">                            <span className="tabular-nums text-stone-400 dark:text-stone-600">{commit.time}</span>                            <span className="font-mono text-[11px] text-stone-500 dark:text-stone-500">{commit.sha}</span>                            <span className="min-w-0 flex-1 truncate text-stone-600 dark:text-stone-300" title={commit.subject}>                              {commit.subject}                            </span>                          </li>                        ))}                      </ul>                    </li>                  ))}                </ul>              )}            </div>          </div>        </motion.div>      )}      <div className="mt-3 flex items-center justify-between">        <div className="flex items-center gap-1.5" role="radiogroup" aria-label="Color theme">          {THEME_IDS.map((id) => (            <button              key={id}              type="button"              role="radio"              aria-checked={theme === id}              aria-label={THEMES[id].label}              title={THEMES[id].label}              onClick={() => changeTheme(id)}              className={`h-4 w-4 shrink-0 rounded-full transition-[color,background-color,border-color,transform,opacity] duration-150 ${FOCUS} ${                theme === id ? "ring-2 ring-offset-2 ring-offset-white dark:ring-offset-stone-950" : "opacity-60 hover:opacity-100"              }`}              style={{ backgroundColor: THEMES[id].swatch, ...(theme === id ? ({ "--tw-ring-color": THEMES[id].swatch } as CSSProperties) : {}) }}            />          ))}        </div>        <div className="flex items-center gap-1 text-[11px] text-stone-500 dark:text-stone-500">          Less          {([0, 1, 2, 3, 4] as const).map((level) => (            <span              key={level}              aria-hidden              className="h-[9px] w-[9px] rounded-[2px] dark:hidden"              style={{ backgroundColor: ramp(level, false) ?? "rgb(226 232 240)" }}            />          ))}          {([0, 1, 2, 3, 4] as const).map((level) => (            <span              key={level}              aria-hidden              className="hidden h-[9px] w-[9px] rounded-[2px] dark:block"              style={{ backgroundColor: ramp(level, true) ?? "rgb(255 255 255 / 0.08)" }}            />          ))}          More        </div>      </div>    </div>  );}