Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 1 | /** |
| 2 | * A person's last year on their profile: a square a day, a column a week, | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 3 | * shaded in the accent by how much they did (commits pushed to a default |
| 4 | * branch, issues and pull requests opened, reviews given) on repositories | |
| 5 | * the viewer can see. The layout is | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 6 | * lib/contribution-calendar.ts; the counts come from the work service's |
| 7 | * `contributions`, which never counts work the viewer could not open. | |
| 8 | * | |
| 9 | * One hint serves every square: it follows the pointer, so a year of days | |
| 10 | * costs one tooltip, not 365. | |
| 11 | */ | |
| 12 | import { type PointerEvent, useEffect, useMemo, useRef, useState } from "react"; | |
| 13 | ||
| 14 | import { type Level, buildCalendar, dayLabel, totalLabel } from "../lib/contribution-calendar"; | |
| 15 | import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; | |
| 16 | ||
| 17 | /** A square's side and the space between squares, in pixels. */ | |
| 18 | const CELL = 10; | |
| 19 | const GAP = 3; | |
| 20 | const STEP = CELL + GAP; | |
| 21 | ||
| 22 | /** Each level's shade: the raised gray for nothing, then the accent, stronger with more. */ | |
| 23 | const SHADES: Record<Level, string> = { | |
| 24 | 0: "bg-raised", | |
| 25 | 1: "bg-accent/25", | |
| 26 | 2: "bg-accent/45", | |
| 27 | 3: "bg-accent/70", | |
| 28 | 4: "bg-accent", | |
| 29 | }; | |
| 30 | ||
| 31 | const WEEKDAYS = ["", "Mon", "", "Wed", "", "Fri", ""]; | |
| 32 | ||
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 33 | type Hovered = { date: string; count: number; commits: number; left: number; top: number }; |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 34 | |
| 35 | export function ContributionCalendar({ | |
| 36 | days, | |
| 37 | total, | |
| 38 | today, | |
| 39 | from, | |
| 40 | }: { | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 41 | days: readonly { date: string; count: number; commits?: number }[]; |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 42 | total: number; |
| 43 | /** `YYYY-MM-DD`, UTC: the last day shown. */ | |
| 44 | today: string; | |
| 45 | /** The first day counted; a year before `today` when absent. */ | |
| 46 | from?: string; | |
| 47 | }) { | |
| 48 | const calendar = useMemo(() => buildCalendar(days, today, from), [days, today, from]); | |
| 49 | const scroller = useRef<HTMLDivElement>(null); | |
| 50 | const [hovered, setHovered] = useState<Hovered | null>(null); | |
| 51 | ||
| 52 | // On a narrow screen the calendar scrolls inside its card; start at today. | |
| 53 | useEffect(() => { | |
| 54 | const element = scroller.current; | |
| 55 | if (element) element.scrollLeft = element.scrollWidth; | |
| 56 | }, []); | |
| 57 | ||
| 58 | const over = (event: PointerEvent<HTMLDivElement>) => { | |
| 59 | const cell = (event.target as HTMLElement).closest<HTMLElement>("[data-date]"); | |
| 60 | if (!cell) return setHovered(null); | |
| 61 | const date = cell.dataset.date!; | |
| 62 | if (hovered?.date === date) return; | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 63 | setHovered({ date, count: Number(cell.dataset.count ?? 0), commits: Number(cell.dataset.commits ?? 0), left: cell.offsetLeft, top: cell.offsetTop }); |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 64 | }; |
| 65 | ||
| 66 | const heading = totalLabel(total); | |
| 67 | return ( | |
| 68 | <section aria-label="Contributions" className="rounded-xl border border-line bg-surface/50 px-4 pt-3.5 pb-3"> | |
| 69 | <h2 className="text-sm font-medium">{heading}</h2> | |
| 70 | <div ref={scroller} className="mt-3 overflow-x-auto overflow-y-hidden pb-1 [scrollbar-width:thin]"> | |
| 71 | <div className="flex w-max text-[0.625rem] leading-none text-faint"> | |
| 72 | {/* The weekdays, under the months' row. */} | |
| 73 | <div aria-hidden="true" className="mr-1.5 flex shrink-0 flex-col" style={{ gap: GAP, paddingTop: 16 }}> | |
| 74 | {WEEKDAYS.map((label, index) => ( | |
| 75 | <span key={index} className="flex items-center" style={{ height: CELL }}> | |
| 76 | {label} | |
| 77 | </span> | |
| 78 | ))} | |
| 79 | </div> | |
| 80 | <div | |
| 81 | role="img" | |
| 82 | aria-label={heading} | |
| 83 | className="relative" | |
| 84 | onPointerOver={over} | |
| 85 | // A tap's pointer leaves as the finger lifts: its hint stays until the next tap elsewhere. | |
| 86 | onPointerLeave={(event) => event.pointerType === "mouse" && setHovered(null)} | |
| 87 | > | |
| 88 | <div aria-hidden="true" className="relative h-4" style={{ width: calendar.weeks.length * STEP - GAP }}> | |
| 89 | {calendar.months.map((month) => ( | |
| 90 | <span key={`${month.week}:${month.label}`} className="absolute top-0" style={{ left: month.week * STEP }}> | |
| 91 | {month.label} | |
| 92 | </span> | |
| 93 | ))} | |
| 94 | </div> | |
| 95 | <div className="flex" style={{ gap: GAP }}> | |
| 96 | {calendar.weeks.map((week, column) => ( | |
| 97 | <div key={column} className="flex flex-col" style={{ gap: GAP }}> | |
| 98 | {week.map((day, row) => | |
| 99 | day ? ( | |
| 100 | <span | |
| 101 | key={row} | |
| 102 | data-date={day.date} | |
| 103 | data-count={day.count} | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 104 | data-commits={day.commits} |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 105 | className={`block rounded-[2px] ${SHADES[day.level]} ${hovered?.date === day.date ? "ring-1 ring-fg/60" : ""}`} |
| 106 | style={{ width: CELL, height: CELL }} | |
| 107 | /> | |
| 108 | ) : ( | |
| 109 | <span key={row} className="block" style={{ width: CELL, height: CELL }} /> | |
| 110 | ), | |
| 111 | )} | |
| 112 | </div> | |
| 113 | ))} | |
| 114 | </div> | |
| 115 | {/* The one hint, over whichever square the pointer is on. */} | |
| 116 | <Tooltip open={hovered != null} onOpenChange={(open) => !open && setHovered(null)}> | |
| 117 | <TooltipTrigger asChild> | |
| 118 | <span | |
| 119 | aria-hidden="true" | |
| 120 | className="pointer-events-none absolute" | |
| 121 | style={{ left: hovered?.left ?? 0, top: hovered?.top ?? 0, width: CELL, height: CELL }} | |
| 122 | /> | |
| 123 | </TooltipTrigger> | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 124 | <TooltipContent side="top">{hovered ? dayLabel(hovered.count, hovered.date, hovered.commits) : null}</TooltipContent> |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 125 | </Tooltip> |
| 126 | </div> | |
| 127 | </div> | |
| 128 | </div> | |
| 129 | <div aria-hidden="true" className="mt-2 flex items-center justify-end gap-1 text-[0.6875rem] text-faint"> | |
| 130 | <span className="mr-1">Less</span> | |
| 131 | {([0, 1, 2, 3, 4] as Level[]).map((level) => ( | |
| 132 | <span key={level} className={`block rounded-[2px] ${SHADES[level]}`} style={{ width: CELL, height: CELL }} /> | |
| 133 | ))} | |
| 134 | <span className="ml-1">More</span> | |
| 135 | </div> | |
| 136 | </section> | |
| 137 | ); | |
| 138 | } |