diff --git a/src/components/dashboard/bar-list.tsx b/src/components/dashboard/bar-list.tsx
new file mode 100644
index 0000000..f60a798
--- /dev/null
+++ b/src/components/dashboard/bar-list.tsx
@@ -0,0 +1,56 @@
+import type { Tally } from "@/lib/metrics/compute";
+
+/**
+ * Ranked horizontal bars, folded past `limit`.
+ *
+ * Folding rather than truncating: the remainder becomes an "other" row so the
+ * total still adds up. A chart whose bars sum to less than the headline figure
+ * invites exactly the question this dashboard exists to pre-empt.
+ */
+export function BarList({
+ title,
+ items,
+ limit = 6,
+ emptyLabel = "Nothing yet",
+}: {
+ title: string;
+ items: Tally[];
+ limit?: number;
+ emptyLabel?: string;
+}) {
+ const shown = items.slice(0, limit);
+ const rest = items.slice(limit).reduce((sum, item) => sum + item.count, 0);
+ const rows = rest > 0 ? [...shown, { key: "other", count: rest }] : shown;
+ const max = Math.max(1, ...rows.map((r) => r.count));
+
+ return (
+ {emptyLabel}
+ {title}
+
+
+ {rows.length === 0 ? (
+
+ {rows.map((row) => (
+
+ )}
+
{label}
+ {/* The spec requires "estimated" in the UI, not only in a footnote: + these figures are shown to clients and have to survive scrutiny. */} + {estimated &&+ {value} +
+ {sub &&{sub}
} ++ Git records what changed and when, but not how long it took. Hours + here are estimated from + the timing of commits, not measured. +
+ ++ Commits less than{" "} + {sessionGapMinutes}{" "} + minutes apart are treated as one working session. Each session counts + the time between its first and last commit, plus{" "} + {preSessionMinutes}{" "} + minutes before it — reading and planning happen before code is + committed — and{" "} + {postSessionMinutes}{" "} + minutes after, for testing and deploying. +
+ ++ What this cannot see:{" "} + design, meetings, reading code, and debugging that ended without a + commit. The estimate is more likely to be low than high. +
+ ++ Merge commits are excluded — a merge records when a branch was + integrated, not work done. + {metrics.mergeCommits > 0 && ( + <> + {" "} + {metrics.mergeCommits}{" "} + {metrics.mergeCommits === 1 ? "merge was" : "merges were"} left out. + > + )} +
+ ++ Active days are counted against{" "} + workdays — weekdays + excluding South African public holidays — rather than calendar days, + so weekends are not counted as time not worked. +
+