// Chart primitives + UI atoms — refined editorial palette.
// Theme: cream paper background, deep ink text, terracotta accent for PM,
// indigo accent for Researcher. Health gradient stays sequential rust→amber→sage.
function healthColor(score, alpha = 1) {
// 1 (poor) → muted rust; 5 (excellent) → muted sage
const t = Math.max(0, Math.min(1, (score - 1) / 4));
const hue = 25 + t * 115;
const chroma = 0.10 + t * 0.04;
const light = 0.58 + t * 0.04;
return `oklch(${light} ${chroma} ${hue} / ${alpha})`;
}
function riskColor(risk) {
if (risk === "critical") return "oklch(0.58 0.15 28)";
if (risk === "watch") return "oklch(0.72 0.13 75)";
return "oklch(0.62 0.10 145)";
}
function riskLabel(risk) {
if (risk === "critical") return "Critical";
if (risk === "watch") return "Watch";
return "Healthy";
}
function signedN(n, d=1){ const s = n.toFixed(d); return n>=0?`+${s}`:s; }
function avg(a){ return a.length ? a.reduce((s,v)=>s+v,0)/a.length : 0; }
function stddev(arr) { const m = avg(arr); return Math.sqrt(arr.reduce((s,v)=>s+(v-m)*(v-m),0)/arr.length); }
function fmtDate(iso) {
const d = new Date(iso + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
function fmtDateLong(iso) {
const d = new Date(iso + "T00:00:00");
return d.toLocaleDateString("en-US", { weekday: "short", month: "long", day: "numeric", year: "numeric" });
}
const INK = "oklch(0.22 0.02 250)";
const SUBINK = "oklch(0.45 0.015 250)";
const FAINT = "oklch(0.72 0.01 250)";
const RULE = "oklch(0.88 0.005 250)";
const PAPER = "oklch(0.985 0.005 80)";
const PAPER_2 = "oklch(0.97 0.006 80)";
function Sparkline({ data, width = 120, height = 32, min = 1, max = 5, stroke, fill }) {
if (!data || !data.length) return null;
const pad = 2;
const w = width - pad * 2, h = height - pad * 2;
const dx = w / (data.length - 1 || 1);
const pts = data.map((v, i) => [pad + i * dx, pad + h - ((v - min) / (max - min)) * h]);
const path = pts.map(([x, y], i) => (i === 0 ? `M${x},${y}` : `L${x},${y}`)).join(" ");
const last = data[data.length - 1];
const c = stroke || healthColor(last);
const f = fill || healthColor(last, 0.16);
const area = `${path} L${pts[pts.length-1][0]},${pad+h} L${pts[0][0]},${pad+h} Z`;
return (
);
}
function TrendLine({ data, width = 720, height = 220, min = 1, max = 5, compare, compareLabel, label }) {
const padL = 38, padR = 14, padT = 18, padB = 30;
const w = width - padL - padR, h = height - padT - padB;
const xs = (i) => padL + (i * w) / (data.length - 1 || 1);
const ys = (v) => padT + h - ((v - min) / (max - min)) * h;
const path = data.map((d, i) => `${i ? "L" : "M"}${xs(i)},${ys(d.score)}`).join(" ");
const area = `${path} L${xs(data.length-1)},${padT+h} L${xs(0)},${padT+h} Z`;
const cmpPath = compare && compare.length === data.length ? compare.map((d, i) => `${i ? "L" : "M"}${xs(i)},${ys(d.score)}`).join(" ") : null;
const last = data[data.length - 1];
return (
);
}
function Radar({ dimensions, size = 280, max = 5, compare }) {
const cx = size/2, cy = size/2;
const radius = size/2 - 42;
const n = dimensions.length;
const ang = (i) => -Math.PI/2 + (i*2*Math.PI)/n;
const pt = (i, v) => { const r = (v/max)*radius; return [cx+Math.cos(ang(i))*r, cy+Math.sin(ang(i))*r]; };
const polyD = (vals) => vals.map((v,i)=>pt(i,v)).map(([x,y],i)=>`${i?"L":"M"}${x},${y}`).join(" ") + " Z";
const teamVals = dimensions.map(d=>d.current);
const teamAvg = avg(teamVals);
return (
);
}
function Heatmap({ teams, dimensions, onTeamClick }) {
const cellH = 30, headerH = 32, labelW = 220, cellW = 64;
const width = labelW + dimensions.length * cellW + 80;
const height = headerH + teams.length * cellH;
return (
);
}
function DistBar({ buckets, width = 140, height = 36 }) {
const segW = width/5;
const max = Math.max(...buckets, 1);
return (
);
}
function ResponseBar({ weekly, width = 720, height = 90, teamSize, onPick, selectedWeek }) {
const padL = 36, padR = 12, padT = 10, padB = 28;
const w = width-padL-padR, h = height-padT-padB;
const max = teamSize || Math.max(...weekly.map(d=>d.responses), 1);
const slot = w/weekly.length;
const barW = slot - 8;
return (
);
}
function Bar({ value, max=5, width=120, height=8, color }) {
return (
);
}
// Refined UI atoms
function Card({ className = "", children, ...rest }) {
return ;
}
function CardHeader({ title, subtitle, action, eyebrow }) {
return (
{eyebrow &&
{eyebrow}
}
{title &&
{title}
}
{subtitle &&
{subtitle}
}
{action}
);
}
function CardBody({ className = "", children }) {
return {children}
;
}
function Pill({ children, color = "neutral", className = "" }) {
const palette = {
neutral: { bg: "oklch(0.95 0.005 250)", fg: INK, border: RULE },
healthy: { bg: "oklch(0.96 0.04 145)", fg: "oklch(0.4 0.10 145)", border: "oklch(0.86 0.06 145)" },
watch: { bg: "oklch(0.97 0.05 75)", fg: "oklch(0.45 0.10 75)", border: "oklch(0.88 0.08 75)" },
critical:{ bg: "oklch(0.96 0.04 28)", fg: "oklch(0.45 0.13 28)", border: "oklch(0.85 0.08 28)" },
accent: { bg: "oklch(0.96 0.04 270)", fg: "oklch(0.42 0.13 270)", border: "oklch(0.86 0.08 270)" },
pm: { bg: "oklch(0.96 0.04 38)", fg: "oklch(0.45 0.13 38)", border: "oklch(0.85 0.09 38)" },
};
const p = palette[color];
return {children};
}
function Segmented({ value, onChange, options }) {
return (
{options.map(o => (
))}
);
}
function Select({ value, onChange, options, className = "" }) {
return (
);
}
// Calendar dropdown for week selection
function WeekPicker({ weeks, value, onChange, startDate = "2026-04-27" }) {
const [open, setOpen] = React.useState(false);
const ref = React.useRef(null);
React.useEffect(() => {
const onClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener("mousedown", onClick);
return () => document.removeEventListener("mousedown", onClick);
}, []);
const weekDates = React.useMemo(() => {
const start = new Date(startDate + "T00:00:00");
return weeks.map((w) => {
const d = new Date(start); d.setDate(start.getDate() + (w.week - 1) * 7);
return { ...w, date: d, label: d.toLocaleDateString("en-US", { month: "short", day: "numeric" }) };
});
}, [weeks, startDate]);
const sel = weekDates.find(w => w.week === value) || weekDates[weekDates.length - 1];
return (
{open && (
Pulse week
{weekDates[0].label} – {weekDates[weekDates.length-1].label}, 2026
{weekDates.map((w) => (
))}
Latest · W{weekDates[weekDates.length-1].week}
)}
);
}
Object.assign(window, {
healthColor, riskColor, riskLabel, signedN, avg, stddev, fmtDate, fmtDateLong,
INK, SUBINK, FAINT, RULE, PAPER, PAPER_2,
Sparkline, TrendLine, Radar, Heatmap, DistBar, ResponseBar, Bar,
Card, CardHeader, CardBody, Pill, Segmented, Select, WeekPicker,
});