/** * Chart colours. * * These are the validated data-viz slots, not the brand accent: the UI is warm * ember on paper, but a chart has to stay readable for colour-blind readers in * both themes, so identity colours come from the reference palette. Focus takes * the orange slot (nearest the brand's warmth), breaks take aqua, and anything * that encodes *magnitude* uses the single-hue blue ramp — this ordering was run * through the palette validator for all pairs in both modes. */ export interface VizPalette { work: string; shortBreak: string; longBreak: string; sequential: string[]; grid: string; axis: string; empty: string; } const LIGHT: VizPalette = { work: "#eb6834", shortBreak: "#2a78d6", longBreak: "#1baf7a", // blue 100 → 600: "more pomodoros" reads as "darker". sequential: ["#cde2fb", "#9ec5f4", "#6da7ec", "#3987e5", "#256abf", "#184f95"], grid: "rgba(23, 21, 15, 0.12)", axis: "rgba(23, 21, 15, 0.45)", empty: "rgba(23, 21, 15, 0.07)", }; const DARK: VizPalette = { work: "#d95926", shortBreak: "#3987e5", longBreak: "#199e70", sequential: ["#104281", "#184f95", "#256abf", "#3987e5", "#6da7ec", "#9ec5f4"], grid: "rgba(244, 241, 234, 0.14)", axis: "rgba(244, 241, 234, 0.55)", empty: "rgba(244, 241, 234, 0.08)", }; export function vizPalette(dark: boolean): VizPalette { return dark ? DARK : LIGHT; } export function kindColor(kind: string, palette: VizPalette): string { if (kind === "work") return palette.work; if (kind === "long_break") return palette.longBreak; return palette.shortBreak; } /** Pick a ramp step for a count; null means "nothing happened". */ export function rampStep(count: number, goal: number, palette: VizPalette): string | null { if (count <= 0) return null; const ratio = goal > 0 ? count / goal : count / 8; const index = Math.min( palette.sequential.length - 1, Math.max(0, Math.round(ratio * (palette.sequential.length - 1))), ); return palette.sequential[index]; }