fix(dashboard): paint the dose ring in the medication hue, not green

The 'Doses today' ring pinned the ring system's green arc — a colour
that vocabulary reserves for band semantics. Every medication surface
in Insights paints --primary (the compliance progress bars, the
compliance trend line, the drug-level and dose-strength curves), so the
ring now rides a new constant 'meds' hue on that token: flat by design
(both gradient stops on --primary), never a band gradient over pending
morning doses. Contrast, computed: dark #bd93f9 — 6.78:1 card / 5.90:1
bg; light #644ac9 — 6.00:1 card / 5.60:1 bg; both clear the 3:1
graphics floor with room. The guard now windows the assertion to the
dose ring's own SVG and pins primary present, ring-green absent.
This commit is contained in:
Marc-André Bombeck committed 2026-07-05 22:11:26 +02:00
1 parent 72ad228f3e
commit ec586ba75c
3 files changed
+44 -13

No files matched your search

@@ -395,12 +395,20 @@ describe("<DashboardHero> — ring row (v1.27.7)", () => {
expect(rings).toHaveLength(3);
// Labels (de locale): the derived ring reuses its wellness-strip
// title; the dose ring names today's tally and shows it as
// taken/scheduled over the constant green arc — never the band
// gradient (a pending morning dose is not an alert state).
// taken/scheduled over the constant med-family arc (`--primary`,
// the hue the medication surfaces in Insights paint) — never the
// band gradient (a pending morning dose is not an alert state) and
// never the ring system's green (reserved for band semantics).
expect(html).toContain("Bereitschaft");
expect(html).toContain("Dosen heute");
expect(html).toContain(">1/3<");
expect(html).toContain("var(--ring-green-from)");
// Window the assertion to the dose ring's own SVG (its gradient defs
// close with the svg) so the trailing health-score ring's band-green
// gradient can't mask a regression.
const medIdx = html.indexOf('data-ring="MED_COMPLIANCE"');
const doseRing = html.slice(medIdx, html.indexOf("</svg>", medIdx));
expect(doseRing).toContain("var(--primary)");
expect(doseRing).not.toContain("var(--ring-green-from)");
expect(html).not.toContain("var(--ring-yellow-from)");
});
@@ -101,16 +101,18 @@ const CTA_LABEL_KEY: Partial<Record<DashboardVerdictVariant, string>> = {
/**
* Per-ring hue for the selected score rings — the wellness-strip
* vocabulary, so every hero ring paints EXACTLY the hue its insights
* sibling paints. MED_COMPLIANCE carries no per-metric hue entry: the
* dose ring pins the ring system's green arc (`band="green"`, constant)
* — the same green family the medication compliance surfaces in
* Insights paint for a taken dose — instead of a band gradient that
* would flash yellow/red over pending morning doses.
* sibling paints. MED_COMPLIANCE rides the `meds` hue: the medication
* family in Insights paints `--primary` on every surface (compliance
* bars, compliance trend line, drug-level / dose-strength curves), so
* the dose ring holds that one constant tone — never a band gradient
* that would flash yellow/red over pending morning doses, and not the
* green arc the ring system reserves for band semantics.
*/
const RING_HUE_BY_ID: Partial<Record<ScoreRingId, RingHue>> = {
READINESS: "readiness",
RECOVERY_SCORE: "recovery",
SLEEP_SCORE: "sleep",
MED_COMPLIANCE: "meds",
};
/** Per-ring label key — reuses the existing score labels; the dose ring
@@ -328,13 +330,19 @@ export function DashboardHero({
className="flex shrink-0 items-center"
>
{/* Dose ring — today's tally ("1/3") over the constant
green arc; the arc still sweeps on the 0..100
progress `score`. Falls through to the score render
when a cached pre-doses snapshot carries no tally. */}
med-family arc (`hue="meds"` = --primary, the tone
every medication surface in Insights paints); the arc
still sweeps on the 0..100 progress `score`.
`band="green"` stays as the stable data-band anchor —
a pending morning dose is not an alert state — while
the hue owns the paint. Falls through to the score
render when a cached pre-doses snapshot carries no
tally. */}
{ring.id === "MED_COMPLIANCE" && ring.doses ? (
<ScoreRing
score={ring.score}
band="green"
hue="meds"
valueText={`${ring.doses.taken}/${ring.doses.scheduled}`}
ariaLabel={t("dashboard.hero.ringDosesAria", {
taken: ring.doses.taken,
+17 -2
View File
@@ -22,8 +22,17 @@
import type { ScoreBand } from "./band-tokens";
/** The per-metric hue keys the wellness strip passes to `<ScoreRing>`. */
export type RingHue = "readiness" | "sleep" | "recovery" | "stress" | "strain";
/** The per-metric hue keys the wellness strip passes to `<ScoreRing>`.
* `meds` is the dashboard dose ring: the medication family in Insights
* paints `--primary` everywhere (the compliance Progress bars, the
* compliance trend line, the drug-level and dose-strength curves), so
* the ring rides the same token — constant, never a band gradient.
* Contrast (computed): dark #bd93f9 — 6.78:1 card / 5.90:1 bg; light
* #644ac9 — 6.00:1 card / 5.60:1 bg — far above the 3:1 graphics
* floor in both themes, and a future `--primary` retune carries the
* ring along. */
export type RingHue =
"readiness" | "sleep" | "recovery" | "stress" | "strain" | "meds";
/**
* Near-flat single-hue arc `[from, to]` per metric, plus a band fallback for
@@ -38,6 +47,9 @@ export const RING_GRADIENT: Record<RingHue | ScoreBand, [string, string]> = {
sleep: ["var(--ring-sleep-from)", "var(--ring-sleep-to)"],
stress: ["var(--ring-stress-from)", "var(--ring-stress-to)"],
strain: ["var(--ring-strain-from)", "var(--ring-strain-to)"],
// Perfectly flat by design (both stops on the one token) — the med
// family's `--primary`, not a hand-tuned from/to pair.
meds: ["var(--primary)", "var(--primary)"],
// Band fallback for the anatomy detail view (no per-metric hue passed):
green: ["var(--ring-green-from)", "var(--ring-green-to)"],
yellow: ["var(--ring-yellow-from)", "var(--ring-yellow-to)"],
@@ -54,4 +66,7 @@ export const TILE_HUE: Record<RingHue, string> = {
recovery: "var(--tile-recovery)",
stress: "var(--tile-stress)",
strain: "var(--tile-strain)",
// No wellness tile carries the meds hue; the dose ring lives on the
// dashboard hero only. Primary keeps the record total.
meds: "var(--primary)",
};