diff --git a/api/src/charts.ts b/api/src/charts.ts
index 35ddf65..37ba631 100644
--- a/api/src/charts.ts
+++ b/api/src/charts.ts
@@ -8,15 +8,26 @@
* Everything is string-built: no chart library, no runtime imports. The only
* dynamic values entering the markup are numbers, percentages, and dates the
* Worker itself computes, plus the fixed phase/stage labels — so nothing here
- * needs XML escaping. GitHub renders these on white; no dark-mode variants.
- * Every function renders a valid (if empty-looking) SVG on a zero-row DB.
+ * needs XML escaping. Styling is shadcn dark zinc to match the README's
+ * shieldcn badges: rounded #09090b cards, #fafafa/#a1a1aa text, GitHub
+ * dark-mode green ramp. Every function renders valid SVG on a zero-row DB.
*/
// D1Database comes from the generated worker-configuration.d.ts runtime types.
const FONT = "Verdana,DejaVu Sans,sans-serif";
-// Green ramp shared by the heatmap and the ladder (GitHub contribution hues).
-const GREENS = ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"];
+// shadcn dark-zinc palette, matching the README's shieldcn badges.
+const BG = "#09090b"; // zinc-950 card
+const FG = "#fafafa"; // zinc-50 text
+const MUTED = "#a1a1aa"; // zinc-400 secondary text
+const LINE = "#27272a"; // zinc-800 structure / empty
+const GREEN = "#16a34a"; // green-600 (core / pass)
+const BLUE = "#2563eb"; // blue-600 (optional)
+const RED = "#f87171"; // red-400 (fail, on dark)
+
+// Green ramp shared by the heatmap and the ladder (GitHub dark-mode
+// contribution hues; level 0 is the empty zinc cell).
+const GREENS = ["#27272a", "#0e4429", "#006d32", "#26a641", "#39d353"];
// Labels are stored SVG-ready: phase II's "&" is pre-escaped since these
// strings go straight into markup and nothing else here needs escaping.
@@ -32,12 +43,12 @@ const STAGES = ["new", "+2", "+5", "+10", "retired"];
// ── svg helpers ──────────────────────────────────────────────────
-/** Opening tag plus the white background rect every chart starts with. */
+/** Opening tag plus the rounded dark card every chart starts with. */
function svgOpen(width: number, height: number): string {
return (
`");
@@ -153,7 +164,7 @@ export async function ladderChart(db: D1Database): Promise {
const width = 640;
const height = 220;
- const plotTop = 34;
+ const plotTop = 26;
const plotBottom = height - 32;
const plotH = plotBottom - plotTop;
const slotW = (width - 80) / STAGES.length;
@@ -161,7 +172,6 @@ export async function ladderChart(db: D1Database): Promise {
const scale = Math.max(1, ...counts);
const parts = [svgOpen(width, height)];
- parts.push(text(40, 20, "review ladder", { size: 13, weight: "bold" }));
counts.forEach((count, i) => {
const cx = 40 + slotW * i + slotW / 2;
@@ -172,11 +182,11 @@ export async function ladderChart(db: D1Database): Promise {
``,
);
}
- parts.push(text(cx, y - 6, String(count), { size: 12, anchor: "middle", weight: "bold" }));
- parts.push(text(cx, plotBottom + 18, STAGES[i]!, { size: 12, fill: "#57606a", anchor: "middle" }));
+ parts.push(text(cx, Math.max(y - 6, 16), String(count), { size: 12, anchor: "middle", weight: "bold" }));
+ parts.push(text(cx, plotBottom + 18, STAGES[i]!, { size: 12, fill: MUTED, anchor: "middle" }));
});
- parts.push(``);
+ parts.push(``);
parts.push("");
return parts.join("");
}
@@ -211,7 +221,7 @@ export async function heatmapChart(db: D1Database, start: string, days: number):
// Week numbers across the top, Mon/Wed/Fri down the left.
for (let w = 0; w < weeks; w++) {
- parts.push(text(gridX + w * step + cell / 2, gridY - 7, `W${w + 1}`, { size: 10, fill: "#57606a", anchor: "middle" }));
+ parts.push(text(gridX + w * step + cell / 2, gridY - 7, `W${w + 1}`, { size: 10, fill: MUTED, anchor: "middle" }));
}
const weekdays: [number, string][] = [
[0, "Mon"],
@@ -219,7 +229,7 @@ export async function heatmapChart(db: D1Database, start: string, days: number):
[4, "Fri"],
];
for (const [row, label] of weekdays) {
- parts.push(text(gridX - 6, gridY + row * step + cell - 4, label, { size: 10, fill: "#57606a", anchor: "end" }));
+ parts.push(text(gridX - 6, gridY + row * step + cell - 4, label, { size: 10, fill: MUTED, anchor: "end" }));
}
// One cell per campaign day; the start date is a Monday, so day i sits at
@@ -239,17 +249,17 @@ export async function heatmapChart(db: D1Database, start: string, days: number):
// Less → More ramp fills the space right of the grid.
const legendX = gridX + weeks * step + 40;
const legendY = gridY + 3 * step;
- parts.push(text(legendX - 6, legendY + cell - 4, "Less", { size: 10, fill: "#57606a", anchor: "end" }));
+ parts.push(text(legendX - 6, legendY + cell - 4, "Less", { size: 10, fill: MUTED, anchor: "end" }));
GREENS.forEach((color, i) => {
parts.push(``);
});
- parts.push(text(legendX + GREENS.length * step + 3, legendY + cell - 4, "More", { size: 10, fill: "#57606a" }));
+ parts.push(text(legendX + GREENS.length * step + 3, legendY + cell - 4, "More", { size: 10, fill: MUTED }));
parts.push("");
return parts.join("");
}
-// ── gate badge: shields.io flat pill ─────────────────────────────
+// ── gate badge: shieldcn-style dark pill ─────────────────────────
export async function gateBadge(db: D1Database): Promise {
const row = await db
@@ -258,31 +268,21 @@ export async function gateBadge(db: D1Database): Promise {
const label = "gate";
const value = row ? `${Math.round(row.pass_rate * 100)}%` : "none yet";
- const color = row ? (row.pass_rate >= 0.7 ? "#4c1" : "#e05d44") : "#9f9f9f";
+ const valueFill = row ? (row.pass_rate >= 0.7 ? "#4ade80" : RED) : MUTED;
- // Shields flat geometry: height 20, rx 3, verdana 11px, ~6.5px per char
- // plus 5px padding each side. Each caption is drawn twice — a dark
- // 30%-opacity copy 1px low as the shadow, then white on top.
- const labelW = Math.round(label.length * 6.5) + 10;
- const valueW = Math.round(value.length * 6.5) + 10;
- const total = labelW + valueW;
- const labelX = labelW / 2;
- const valueX = labelW + valueW / 2;
+ // shieldcn geometry: height 32, rx 6, one flat zinc-900 pill. Verdana 13px
+ // ≈ 7.5px per char; 12px outer padding, 8px between label and value.
+ const labelW = Math.round(label.length * 7.5);
+ const valueW = Math.round(value.length * 7.5);
+ const total = 12 + labelW + 8 + valueW + 12;
return (
- `