refactor(insights): route chart and classification colors through theme tokens

Reference-band fills, classification tones, and the trend-chart palette
carried hardcoded hex that rendered identically in both themes and
glared on the light surface. Route them through the semantic and chart
CSS variables, which already hold per-theme values, so the light theme
gets the muted set and the dark theme stays byte-identical.
This commit is contained in:
Marc-André Bombeck committed 2026-07-01 23:44:55 +02:00
1 parent f219072881
commit c6dea5eb56
8 files changed
+180 -85

No files matched your search

+76 -32
View File
@@ -13,35 +13,39 @@ export interface BmiClassification {
export function classifyBMI(bmi: number): BmiClassification {
if (bmi < 18.5) {
return { category: "Underweight", color: "#f1fa8c", severity: "warning" };
return {
category: "Underweight",
color: "var(--dracula-yellow)",
severity: "warning",
};
}
if (bmi < 25) {
return { category: "Normal", color: "#50fa7b", severity: "normal" };
return { category: "Normal", color: "var(--success)", severity: "normal" };
}
if (bmi < 30) {
return {
category: "Overweight",
color: "#ffb86c",
color: "var(--warning)",
severity: "warning",
};
}
if (bmi < 35) {
return {
category: "Obesity Grade I",
color: "#ff79c6",
color: "var(--chart-3)",
severity: "danger",
};
}
if (bmi < 40) {
return {
category: "Obesity Grade II",
color: "#ff5555",
color: "var(--destructive)",
severity: "danger",
};
}
return {
category: "Obesity Grade III",
color: "#ff5555",
color: "var(--destructive)",
severity: "danger",
};
}
@@ -56,35 +60,35 @@ export interface BpClassification {
export function classifyBP(sys: number, dia: number): BpClassification {
if (sys < 120 && dia < 80) {
return { category: "Optimal", color: "#50fa7b", severity: "normal" };
return { category: "Optimal", color: "var(--success)", severity: "normal" };
}
if (sys < 130 && dia < 85) {
return { category: "Normal", color: "#8be9fd", severity: "normal" };
return { category: "Normal", color: "var(--info)", severity: "normal" };
}
if (sys < 140 && dia < 90) {
return {
category: "High-normal",
color: "#f1fa8c",
color: "var(--dracula-yellow)",
severity: "elevated",
};
}
if (sys < 160 && dia < 100) {
return {
category: "Hypertension Grade 1",
color: "#ffb86c",
color: "var(--warning)",
severity: "warning",
};
}
if (sys < 180 && dia < 110) {
return {
category: "Hypertension Grade 2",
color: "#ff79c6",
color: "var(--chart-3)",
severity: "danger",
};
}
return {
category: "Hypertension Grade 3",
color: "#ff5555",
color: "var(--destructive)",
severity: "danger",
};
}
@@ -99,15 +103,23 @@ export interface PulseClassification {
export function classifyPulse(bpm: number): PulseClassification {
if (bpm < 60) {
return { category: "Bradycardia", color: "#8be9fd", severity: "info" };
return { category: "Bradycardia", color: "var(--info)", severity: "info" };
}
if (bpm <= 100) {
return { category: "Normal", color: "#50fa7b", severity: "normal" };
return { category: "Normal", color: "var(--success)", severity: "normal" };
}
if (bpm <= 120) {
return { category: "Elevated", color: "#ffb86c", severity: "warning" };
return {
category: "Elevated",
color: "var(--warning)",
severity: "warning",
};
}
return { category: "Tachycardia", color: "#ff5555", severity: "danger" };
return {
category: "Tachycardia",
color: "var(--destructive)",
severity: "danger",
};
}
// ── Sleep Duration Classification (AASM/SRS) ───────────
@@ -124,22 +136,34 @@ export function classifySleepDuration(
if (hours < 6) {
return {
category: "Far too short",
color: "#ff5555",
color: "var(--destructive)",
severity: "warning",
};
}
if (hours < 7) {
return { category: "Too short", color: "#ffb86c", severity: "warning" };
return {
category: "Too short",
color: "var(--warning)",
severity: "warning",
};
}
if (hours <= 9) {
return { category: "On target", color: "#50fa7b", severity: "normal" };
return {
category: "On target",
color: "var(--success)",
severity: "normal",
};
}
if (hours <= 10) {
return { category: "Slightly long", color: "#8be9fd", severity: "info" };
return {
category: "Slightly long",
color: "var(--info)",
severity: "info",
};
}
return {
category: "Far too long",
color: "#ff79c6",
color: "var(--chart-3)",
severity: "warning",
};
}
@@ -220,23 +244,35 @@ export function classifyBodyFat(
// hormonal / cardiac risk in chronic depletion).
return {
category: "Below essential",
color: "#ff5555",
color: "var(--destructive)",
severity: "danger",
};
}
if (pct <= essentialMax) {
return { category: "Essential", color: "#f1fa8c", severity: "warning" };
return {
category: "Essential",
color: "var(--dracula-yellow)",
severity: "warning",
};
}
if (pct <= athleteMax) {
return { category: "Athletic", color: "#50fa7b", severity: "normal" };
return {
category: "Athletic",
color: "var(--success)",
severity: "normal",
};
}
if (pct <= fitnessMax) {
return { category: "Fitness", color: "#50fa7b", severity: "normal" };
return { category: "Fitness", color: "var(--success)", severity: "normal" };
}
if (pct <= acceptableMax) {
return { category: "Acceptable", color: "#ffb86c", severity: "warning" };
return {
category: "Acceptable",
color: "var(--warning)",
severity: "warning",
};
}
return { category: "Obese", color: "#ff5555", severity: "danger" };
return { category: "Obese", color: "var(--destructive)", severity: "danger" };
}
// ── Activity Steps Classification ───────────────────────
@@ -259,24 +295,32 @@ export function classifySteps(steps: number): StepsClassification {
if (steps < 3000) {
return {
category: "Very low",
color: "#ff5555",
color: "var(--destructive)",
severity: "warning",
};
}
if (steps < 5000) {
return { category: "Low active", color: "#ffb86c", severity: "warning" };
return {
category: "Low active",
color: "var(--warning)",
severity: "warning",
};
}
if (steps < 7500) {
return {
category: "Moderately active",
color: "#f1fa8c",
color: "var(--dracula-yellow)",
severity: "info",
};
}
if (steps < 10000) {
return { category: "Active", color: "#50fa7b", severity: "normal" };
return { category: "Active", color: "var(--success)", severity: "normal" };
}
return { category: "Very active", color: "#50fa7b", severity: "normal" };
return {
category: "Very active",
color: "var(--success)",
severity: "normal",
};
}
/**
+9 -5
View File
@@ -110,30 +110,34 @@ export function classifyPulseByTarget(
if (bpm < target.orangeMin) {
return {
category: "Significantly low",
color: "#8be9fd",
color: "var(--info)",
severity: "info",
};
}
if (bpm < target.greenMin) {
return {
category: "Slightly low",
color: "#ffb86c",
color: "var(--warning)",
severity: "warning",
};
}
if (bpm <= target.greenMax) {
return { category: "On target", color: "#50fa7b", severity: "normal" };
return {
category: "On target",
color: "var(--success)",
severity: "normal",
};
}
if (bpm <= target.orangeMax) {
return {
category: "Slightly elevated",
color: "#ffb86c",
color: "var(--warning)",
severity: "warning",
};
}
return {
category: "Significantly elevated",
color: "#ff5555",
color: "var(--destructive)",
severity: "danger",
};
}
+30 -10
View File
@@ -39,11 +39,21 @@ export function buildTrafficLightBands(
const upperBound = options.upperBound ?? orangeHigh + Math.max(span, 1) * 2;
const bands: ValueBand[] = [
{ min: lowerBound, max: orangeLow, color: "#ff5555", opacity: 0.16 },
{ min: orangeLow, max: min, color: "#ffb86c", opacity: 0.18 },
{ min, max, color: "#50fa7b", opacity: 0.2 },
{ min: max, max: orangeHigh, color: "#ffb86c", opacity: 0.18 },
{ min: orangeHigh, max: upperBound, color: "#ff5555", opacity: 0.16 },
{
min: lowerBound,
max: orangeLow,
color: "var(--destructive)",
opacity: 0.16,
},
{ min: orangeLow, max: min, color: "var(--warning)", opacity: 0.18 },
{ min, max, color: "var(--success)", opacity: 0.2 },
{ min: max, max: orangeHigh, color: "var(--warning)", opacity: 0.18 },
{
min: orangeHigh,
max: upperBound,
color: "var(--destructive)",
opacity: 0.16,
},
];
return bands.filter((band) => band.max > band.min);
@@ -88,26 +98,36 @@ export function buildWeightBandsFromHeight(
const upperBound = options.upperBound ?? 250;
return [
{ min: lowerBound, max: range.orangeMin, color: "#ff5555", opacity: 0.16 },
{
min: lowerBound,
max: range.orangeMin,
color: "var(--destructive)",
opacity: 0.16,
},
{
min: range.orangeMin,
max: range.greenMin,
color: "#ffb86c",
color: "var(--warning)",
opacity: 0.18,
},
{
min: range.greenMin,
max: range.greenMax,
color: "#50fa7b",
color: "var(--success)",
opacity: 0.2,
},
{
min: range.greenMax,
max: range.orangeMax,
color: "#ffb86c",
color: "var(--warning)",
opacity: 0.18,
},
{ min: range.orangeMax, max: upperBound, color: "#ff5555", opacity: 0.16 },
{
min: range.orangeMax,
max: upperBound,
color: "var(--destructive)",
opacity: 0.16,
},
].filter((band) => band.max > band.min);
}
@@ -58,26 +58,36 @@ function clientBands(profile: {
orangeMax: pulseTarget.orangeMax,
},
pulseBands: [
{ min: 30, max: pulseTarget.orangeMin, color: "#ff5555", opacity: 0.16 },
{
min: 30,
max: pulseTarget.orangeMin,
color: "var(--destructive)",
opacity: 0.16,
},
{
min: pulseTarget.orangeMin,
max: pulseTarget.greenMin,
color: "#ffb86c",
color: "var(--warning)",
opacity: 0.18,
},
{
min: pulseTarget.greenMin,
max: pulseTarget.greenMax,
color: "#50fa7b",
color: "var(--success)",
opacity: 0.2,
},
{
min: pulseTarget.greenMax,
max: pulseTarget.orangeMax,
color: "#ffb86c",
color: "var(--warning)",
opacity: 0.18,
},
{ min: pulseTarget.orangeMax, max: 220, color: "#ff5555", opacity: 0.16 },
{
min: pulseTarget.orangeMax,
max: 220,
color: "var(--destructive)",
opacity: 0.16,
},
].filter((b) => b.max > b.min),
bodyFatRange,
bodyFatBands: buildTrafficLightBands(bodyFatRange.min, bodyFatRange.max, {
+15 -5
View File
@@ -88,26 +88,36 @@ export function buildDashboardBands(profile: {
? buildTrafficRange(bpTargets.diaLow, bpTargets.diaHigh)
: null;
const pulseBands = [
{ min: 30, max: pulseTarget.orangeMin, color: "#ff5555", opacity: 0.16 },
{
min: 30,
max: pulseTarget.orangeMin,
color: "var(--destructive)",
opacity: 0.16,
},
{
min: pulseTarget.orangeMin,
max: pulseTarget.greenMin,
color: "#ffb86c",
color: "var(--warning)",
opacity: 0.18,
},
{
min: pulseTarget.greenMin,
max: pulseTarget.greenMax,
color: "#50fa7b",
color: "var(--success)",
opacity: 0.2,
},
{
min: pulseTarget.greenMax,
max: pulseTarget.orangeMax,
color: "#ffb86c",
color: "var(--warning)",
opacity: 0.18,
},
{ min: pulseTarget.orangeMax, max: 220, color: "#ff5555", opacity: 0.16 },
{
min: pulseTarget.orangeMax,
max: 220,
color: "var(--destructive)",
opacity: 0.16,
},
].filter((band) => band.max > band.min);
const bodyFatBands = buildTrafficLightBands(
bodyFatRange.min,
+12 -12
View File
@@ -100,7 +100,7 @@ export const TREND_CHART_CONFIG: Record<
metric: "bp",
kind: "health-chart",
types: ["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"],
colors: ["#ff79c6", "#8be9fd"],
colors: ["var(--chart-3)", "var(--chart-4)"],
unit: "mmHg",
yAxisUnit: "mmHg",
titleKey: "charts.bloodPressure",
@@ -112,7 +112,7 @@ export const TREND_CHART_CONFIG: Record<
metric: "weight",
kind: "health-chart",
types: ["WEIGHT"],
colors: ["#bd93f9"],
colors: ["var(--chart-1)"],
unit: "kg",
titleKey: "charts.weight",
captionKey: "insights.trendsRow.caption.weight",
@@ -123,7 +123,7 @@ export const TREND_CHART_CONFIG: Record<
metric: "pulse",
kind: "health-chart",
types: ["PULSE"],
colors: ["#50fa7b"],
colors: ["var(--chart-2)"],
unit: "bpm",
yAxisUnit: "bpm",
titleKey: "charts.pulse",
@@ -144,7 +144,7 @@ export const TREND_CHART_CONFIG: Record<
metric: "sleep",
kind: "health-chart",
types: ["SLEEP_DURATION"],
colors: ["#8be9fd"],
colors: ["var(--chart-4)"],
unit: "h",
yAxisUnit: "h",
titleKey: "charts.sleep",
@@ -155,7 +155,7 @@ export const TREND_CHART_CONFIG: Record<
metric: "steps",
kind: "health-chart",
types: ["ACTIVITY_STEPS"],
colors: ["#50fa7b"],
colors: ["var(--chart-2)"],
unit: "steps",
yAxisUnit: "steps",
titleKey: "charts.steps",
@@ -166,7 +166,7 @@ export const TREND_CHART_CONFIG: Record<
metric: "hrv",
kind: "health-chart",
types: ["HEART_RATE_VARIABILITY"],
colors: ["#bd93f9"],
colors: ["var(--chart-1)"],
unit: "ms",
yAxisUnit: "ms",
titleKey: "charts.hrv",
@@ -177,7 +177,7 @@ export const TREND_CHART_CONFIG: Record<
metric: "resting_hr",
kind: "health-chart",
types: ["RESTING_HEART_RATE"],
colors: ["#ff79c6"],
colors: ["var(--chart-3)"],
unit: "bpm",
yAxisUnit: "bpm",
titleKey: "charts.restingHeartRate",
@@ -188,7 +188,7 @@ export const TREND_CHART_CONFIG: Record<
metric: "active_energy",
kind: "health-chart",
types: ["ACTIVE_ENERGY_BURNED"],
colors: ["#ffb86c"],
colors: ["var(--chart-5)"],
unit: "kcal",
yAxisUnit: "kcal",
titleKey: "charts.activeEnergy",
@@ -199,7 +199,7 @@ export const TREND_CHART_CONFIG: Record<
metric: "flights",
kind: "health-chart",
types: ["FLIGHTS_CLIMBED"],
colors: ["#f1fa8c"],
colors: ["var(--dracula-yellow)"],
unit: "flights",
yAxisUnit: "flights",
titleKey: "charts.flights",
@@ -210,7 +210,7 @@ export const TREND_CHART_CONFIG: Record<
metric: "distance",
kind: "health-chart",
types: ["WALKING_RUNNING_DISTANCE"],
colors: ["#8be9fd"],
colors: ["var(--chart-4)"],
unit: "km",
yAxisUnit: "km",
titleKey: "charts.distance",
@@ -221,7 +221,7 @@ export const TREND_CHART_CONFIG: Record<
metric: "vo2_max",
kind: "health-chart",
types: ["VO2_MAX"],
colors: ["#50fa7b"],
colors: ["var(--chart-2)"],
unit: "mL/kg·min",
yAxisUnit: "mL/kg·min",
titleKey: "charts.vo2Max",
@@ -232,7 +232,7 @@ export const TREND_CHART_CONFIG: Record<
metric: "body_temp",
kind: "health-chart",
types: ["BODY_TEMPERATURE"],
colors: ["#ff5555"],
colors: ["var(--destructive)"],
unit: "°C",
yAxisUnit: "°C",
titleKey: "charts.bodyTemperature",
+5 -1
View File
@@ -171,7 +171,11 @@ export const SIGNALS: Record<string, SignalDefinition> = {
archetype: "physiological-vital",
normalRange: { low: 50, high: 100 },
referenceMetric: "RESTING_HEART_RATE",
chart: { overlayKey: "restingHr", color: "#ff5555", yAxisUnit: "bpm" },
chart: {
overlayKey: "restingHr",
color: "var(--destructive)",
yAxisUnit: "bpm",
},
surfaces: {
detailPage: true,
correlationEligible: false,
+18 -15
View File
@@ -828,10 +828,10 @@ export async function buildTargetsResponse(user: AuthedUser) {
classification:
bpInTargetRate != null
? bpInTargetRate >= 70
? { category: "Good", color: "#50fa7b" }
? { category: "Good", color: "var(--success)" }
: bpInTargetRate >= 40
? { category: "Moderate", color: "#f1fa8c" }
: { category: "Low", color: "#ff5555" }
? { category: "Moderate", color: "var(--dracula-yellow)" }
: { category: "Low", color: "var(--destructive)" }
: null,
source: "ESH 2023",
...inTargetConsistency,
@@ -1233,10 +1233,10 @@ export async function buildTargetsResponse(user: AuthedUser) {
classification:
complianceRate7 != null
? complianceRate7 >= 90
? { category: "Very good", color: "#50fa7b" }
? { category: "Very good", color: "var(--success)" }
: complianceRate7 >= 70
? { category: "Good", color: "#f1fa8c" }
: { category: "Low", color: "#ff5555" }
? { category: "Good", color: "var(--dracula-yellow)" }
: { category: "Low", color: "var(--destructive)" }
: null,
source: "7-day",
details: {
@@ -1358,10 +1358,10 @@ export async function buildTargetsResponse(user: AuthedUser) {
const moodClassification =
latestMoodScore != null
? latestMoodScore >= 3.5
? { category: "Good", color: "#50fa7b" }
? { category: "Good", color: "var(--success)" }
: latestMoodScore >= 2
? { category: "Moderate", color: "#f1fa8c" }
: { category: "Low", color: "#ff5555" }
? { category: "Moderate", color: "var(--dracula-yellow)" }
: { category: "Low", color: "var(--destructive)" }
: null;
const moodRange = { min: 3.5, max: 5 };
@@ -1397,10 +1397,10 @@ export async function buildTargetsResponse(user: AuthedUser) {
const stabilityClassification =
stdDev <= 0.5
? { category: "Very stable", color: "#50fa7b" }
? { category: "Very stable", color: "var(--success)" }
: stdDev <= 1.0
? { category: "Stable", color: "#f1fa8c" }
: { category: "Fluctuating", color: "#ff5555" };
? { category: "Stable", color: "var(--dracula-yellow)" }
: { category: "Fluctuating", color: "var(--destructive)" };
// Mood stability is computed from the same recent-mood window;
// consistency mirrors MOOD_SCORE so the strip aligns with the
@@ -1476,11 +1476,14 @@ export async function buildTargetsResponse(user: AuthedUser) {
let classification: { category: string; color: string } | null = null;
if (range && effRange) {
if (latest >= range.min && latest <= range.max) {
classification = { category: "Optimal", color: "#50fa7b" };
classification = { category: "Optimal", color: "var(--success)" };
} else if (latest >= effRange.orangeMin && latest <= effRange.orangeMax) {
classification = { category: "Elevated", color: "#f1fa8c" };
classification = {
category: "Elevated",
color: "var(--dracula-yellow)",
};
} else {
classification = { category: "High", color: "#ff5555" };
classification = { category: "High", color: "var(--destructive)" };
}
}