From b53ab36c4b5381d98fc51f78df5941c8aa6649ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 31 Jul 2026 08:27:19 +0200 Subject: [PATCH] Separate the nap band from the stage beneath it with a visible edge The nap fill and a wake bout measured 1.52:1 in the dark theme and 1.16:1 in the light one, so on any night with a wake bout the two bands read as a single block. That defeats the point of pulling the nap out in the first place. No fill colour can fix this. To clear the 3:1 asked between neighbouring shapes, a nap colour would have to beat all five stage colours and the card behind them, and in both themes those two demands point in opposite directions with nothing left in between. The band now carries a card- coloured outline instead, which is the usual answer for stacked bars and leaves the stage colours exactly where they were. The tightest pair it leaves is 4.81:1. The legend and the tooltip already name the nap, so the distinction never rests on colour alone. --- .../insights/sleep-stage-stacked-bar.tsx | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/components/insights/sleep-stage-stacked-bar.tsx b/src/components/insights/sleep-stage-stacked-bar.tsx index fc2569782..15e50bb0b 100644 --- a/src/components/insights/sleep-stage-stacked-bar.tsx +++ b/src/components/insights/sleep-stage-stacked-bar.tsx @@ -106,6 +106,36 @@ export const STAGE_ORDER = ["DEEP", "REM", "CORE", "ASLEEP", "AWAKE"] as const; */ const NAP_KEY = "NAP"; +/** + * The nap band is drawn with a card-coloured outline, and that outline is + * load-bearing rather than decoration. + * + * No flat fill can carry this on its own. To clear the 3:1 that WCAG 1.4.11 + * asks between adjacent graphical objects, a nap colour would have to beat + * all five stage colours AND the card it sits on. In the dark theme the five + * stages span luminance 0.385–0.890, which forces the nap below 0.095, while + * clearing the card (0.014) forces it above 0.143 — no value satisfies both. + * The light theme is the same bind mirrored: the stages sit at 0.088–0.161, + * forcing the nap above 0.583, while the card (0.966) forces it below 0.289. + * Both are empty ranges, so this is arithmetic, not a shortage of tokens. + * + * A separator is the sanctioned way out, and it measures comfortably: + * + * stroke vs DEEP REM CORE ASLEEP AWAKE nap fill + * dark 6.78 6.86 11.82 11.92 14.65 9.60 + * light 6.04 7.37 5.80 5.90 4.81 5.60 + * + * Lowest pair 4.81:1, against a light-theme wake bout — the neighbour the + * nap most often lands on, since AWAKE is the top of the stage stack. + * + * The stage tokens themselves are untouched; they are shared with the + * hypnogram and the charts-visual-identity rule keeps them put. + */ +const NAP_SEPARATOR = { + stroke: "var(--card)", + strokeWidth: 2, +} as const; + /** * Dracula stage palette. Exported so the last-night hypnogram * (`sleep-hypnogram.tsx`) reuses the exact same tokens — per the @@ -436,9 +466,18 @@ export function SleepStageStackedBar({ breakdown }: SleepStageStackedBarProps) { dataKey={NAP_KEY} stackId="stages" fill={STAGE_COLORS[NAP_KEY]} + // The outline is what separates the nap from the stage + // beneath it; the two fills alone measure as low as + // 1.04:1. See NAP_SEPARATOR. + stroke={NAP_SEPARATOR.stroke} + strokeWidth={NAP_SEPARATOR.strokeWidth} isAnimationActive={false} > - + )}