shadimage
ui-shrushank
componentcommit-graphsource: ok · refreshed

Commit Graph

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

npx shadcn add "https://raw.githubusercontent.com/Shrootiiii/ui/main/r/commit-graph.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationframer-motion
icons@phosphor-icons/react

Files1 · source cached 10/8/2026, 1:49:20 AM

components/ui/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>
  );
}

More from ui-shrushank

Browse all components in this library →