Dark mode: remap the semantic tokens onto a purpose-built palette - #657
Open
n400 wants to merge 4 commits into
Open
Dark mode: remap the semantic tokens onto a purpose-built palette#657n400 wants to merge 4 commits into
n400 wants to merge 4 commits into
Conversation
WCAG 2.x ratios are calibrated for dark text on light backgrounds; APCA, drafted for WCAG 3, is the recommended measure for dark mode. The dark tokens are now chosen and audited under both. Adds the --mdh-* palette: a color's grade encodes its lightness, so the gap between two grades sets a contrast floor for any pair of hues (40 for 3:1, 50 for 4.5:1, 70 for 7:1). Half-steps at 35 and 85 give dark status text a home that the existing grades couldn't provide. The prefix is --mdh- because DesignerUI already uses --ce-* for a different palette. The old values move to legacyCeColors.ts unchanged and are still emitted, so ViewBuilder content renders as before. Accent tokens split by duty: the base token is for fills, which carry white text, and a -text variant is for foreground. Component references were routed to whichever they need. scripts/color-audit.mjs checks the token definitions against their role floors under both models; npm run audit:colors gates dark mode in CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ens, doc corrections - Layout: render scheme + brand primaryColor in a single Emotion Global so a runtime scheme toggle can't reorder the tags and clobber the customer color - Rename the signal family's bare tokens to explicit -mark duty (glucose/heart-rate/activity/sleep/air-quality); no bare signal names remain - colorDocs: add the missing Glucose row, correct the air-quality dark label to teal-35, adopt the -mark vocabulary - globalCss: correct the glucose-text comment to the shipped pair (red-orange-35 on grey-85, measured Lc), stop claiming light hexes were kept verbatim - Standardize progress fills on primary-text (ProgressBar default, BasicPointsForBadges) to match ProgressRing; move GlucoseChart's steps overlay and GlucoseDayCoordinator's spark ranges onto mark tokens Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Rebuilds the dark-mode color system around an expanded palette while preserving legacy ViewBuilder colors and light-mode behavior.
Changes:
- Adds dark-mode semantic tokens and updates component color roles.
- Preserves legacy
--ce-*variables and branded runtime themes. - Adds color documentation, APCA tooling, and a contrast audit.
Reviewed changes
Copilot reviewed 67 out of 69 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/stories/foundation/colorDocs.tsx |
Rewrites color-system documentation. |
src/stories/foundation/colorDocs.css |
Styles the new palette documentation. |
src/helpers/news-feed/eventTypeDefinitions.tsx |
Uses foreground status tokens. |
src/helpers/legacyCeColors.ts |
Preserves legacy --ce-* colors. |
src/helpers/globalCss.ts |
Defines the palette and semantic mappings. |
src/helpers/chartHelpers.tsx |
Updates default chart colors. |
src/components/view/NewPointsView/NewPointsView.css |
Updates primary foreground colors. |
src/components/view/DocumentDetailView/DocumentDetailView.tsx |
Updates delete-button color. |
src/components/view/DocumentDetailView/DocumentDetailView.css |
Updates primary foreground color. |
src/components/symptom-shark/presentational/NotesTimeline/NotesTimeline.css |
Updates timeline accent color. |
src/components/symptom-shark/presentational/Calendar/Calendar.css |
Updates notes-icon color. |
src/components/symptom-shark/container/ReportBuilder/ReportBuilder.css |
Updates success foreground color. |
src/components/step/StepLayout/StepLayout.css |
Updates link color. |
src/components/presentational/ValueSelector/ValueSelector.css |
Updates selected checkbox color. |
src/components/presentational/TermInformationButton/TermInformationButton.tsx |
Updates information-icon color. |
src/components/presentational/SingleSurveyTask/SingleSurveyTask.css |
Updates due-date status colors. |
src/components/presentational/SingleMeal/SingleMeal.tsx |
Updates selected-meal icon color. |
src/components/presentational/SingleExternalAccount/SingleExternalAccount.css |
Updates error text color. |
src/components/presentational/SingleDataPoint/SingleDataPoint.css |
Updates setup text color. |
src/components/presentational/SegmentedControl/SegmentedControl.css |
Updates control foreground colors. |
src/components/presentational/Resource/Resource.css |
Updates resource-link color. |
src/components/presentational/ProgressRing/ProgressRing.css |
Updates progress-ring stroke. |
src/components/presentational/ProgressBar/ProgressBar.tsx |
Updates default progress color. |
src/components/presentational/NotesInput/NotesInput.css |
Updates input border color. |
src/components/presentational/NavigationBar/NavigationBar.css |
Updates navigation color. |
src/components/presentational/MealAnalysis/MealAnalysis.css |
Updates meal status colors. |
src/components/presentational/Layout/Layout.tsx |
Combines scheme and brand styles. |
src/components/presentational/LabResultWithSparkline/LabResultWithSparkline.tsx |
Updates sparkline colors. |
src/components/presentational/LabResultWithSparkline/LabResultWithSparkline.css |
Updates term-info color. |
src/components/presentational/Histogram/Histogram.css |
Updates clickable-label color. |
src/components/presentational/GlucoseStats/GlucoseStats.tsx |
Applies signal color tokens. |
src/components/presentational/DumbbellChart/DumbbellChart.css |
Updates chart-mark colors. |
src/components/presentational/DiscreteScale/DiscreteScale.tsx |
Updates default slider color. |
src/components/presentational/DateRangeNavigator/DateRangeNavigator.css |
Updates navigator colors. |
src/components/presentational/Chat/Chat.css |
Updates focus outline color. |
src/components/presentational/Button/Button.tsx |
Updates light/subtle foregrounds. |
src/components/presentational/ActivityMeter/ActivityMeter.css |
Updates activity text color. |
src/components/presentational/Action/Action.css |
Updates indicator color. |
src/components/container/SurveyAnswerChart/SurveyAnswerChart.tsx |
Updates tooltip series color. |
src/components/container/RelativeActivityDayCoordinator/RelativeActivityDayCoordinator.tsx |
Updates activity chart color. |
src/components/container/RelativeActivity/RelativeActivity.tsx |
Updates relative-activity color. |
src/components/container/RecentDailyDataBarChart/RecentDailyDataBarChart.tsx |
Updates label color. |
src/components/container/ProviderSearch/ProviderSearch.css |
Updates provider status colors. |
src/components/container/ProjectSupport/ProjectSupport.css |
Updates support-action colors. |
src/components/container/MostRecentNotification/MostRecentNotification.css |
Updates notification subtitle color. |
src/components/container/MicroTrend/MicroTrend.tsx |
Updates trend chart colors. |
src/components/container/MealEditor/MealEditor.css |
Updates editor action/status colors. |
src/components/container/Goal/Goal.tsx |
Updates goal-state colors. |
src/components/container/GlucoseDayCoordinator/GlucoseDayCoordinator.tsx |
Applies glucose mark token. |
src/components/container/GlucoseChart/GlucoseChart.tsx |
Applies semantic chart tokens. |
src/components/container/GlucoseChart/GlucoseChart.css |
Updates glucose title color. |
src/components/container/ExternalAccountConnectionAlert/ExternalAccountConnectionAlert.tsx |
Updates warning-icon color. |
src/components/container/EhrNewsFeed/EhrNewsFeed.css |
Updates feed action color. |
src/components/container/DeviceDataMonthChart/DeviceDataMonthChart.tsx |
Updates line-chart color. |
src/components/container/DailyDataGoal/DailyDataGoal.tsx |
Updates goal progress colors. |
src/components/container/ConnectEhr/ConnectEhr.css |
Updates connection status colors. |
src/components/container/ConnectedDevices/ConnectedDevices.css |
Updates battery status colors. |
src/components/container/ConnectDevicesMenu/ConnectDevicesMenu.css |
Updates connection action colors. |
src/components/container/ConnectDevice/ConnectDevice.css |
Updates device status colors. |
src/components/container/BloodPressureVisualization/BloodPressureVisualization.css |
Updates metric status colors. |
src/components/container/BasicPointsForBadges/BasicPointsForBadges.tsx |
Updates progress-bar color. |
src/components/asthma/helpers/asthma-functions.ts |
Updates offline status color. |
src/components/asthma/components/AsthmaLogEntryDetails/AsthmaLogEntryDetails.css |
Updates action color. |
src/components/asthma/components/AsthmaAirQualities/AsthmaAirQualities.css |
Updates setup/action colors. |
src/components/asthma/components/AsthmaActionPlanManager/AsthmaActionPlanManager.css |
Updates action-plan colors. |
src/components/@types/custom.d.ts |
Declares APCA package types. |
scripts/color-audit.mjs |
Adds contrast-audit tooling. |
package.json |
Adds audit command and dependencies. |
package-lock.json |
Locks new color dependencies. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+109
to
+114
| for (const g of SIGNALS) { | ||
| for (const s of SURFACES) { | ||
| if (t[`--mdhui-color-${g}`]) pushPair(`${g} mark on ${bgName(s)}`, `--mdhui-color-${g}`, s, MARK_FLOORS.wcag, MARK_FLOORS.apca, false); | ||
| if (t[`--mdhui-color-${g}-text`]) pushPair(`${g}-text on ${bgName(s)}`, `--mdhui-color-${g}-text`, s, FOREGROUND_FLOORS.wcag, FOREGROUND_FLOORS.apca, false); | ||
| } | ||
| } |
Comment on lines
+85
to
+87
| const pushPair = (label, fg, bg, wFloor, aFloor, exempt) => { | ||
| const fH = hex(fg), bH = hex(bg); | ||
| if (!fH || !bH) { rows.push({ label, note: `unresolved (${t[fg]} / ${t[bg]})`, skip: true }); return; } |
Comment on lines
+25
to
+26
| "test-storybook": "test-storybook", | ||
| "audit:colors": "node scripts/color-audit.mjs semantic --scheme dark --ci" |
Comment on lines
+616
to
+620
| /* Signal colors: "-mark" = chart marks (bars/pins/dots); "-text" = the signal as | ||
| foreground, since CVD-optimized mark colors can be too weak as text. Light keeps | ||
| one canonical hex per signal, picked from the values components hardcoded — where | ||
| components disagreed (GlucoseStats' #d36540/#8287bb), they shift to it; dark | ||
| overrides. */ |
Comment on lines
+522
to
+524
| <li><strong>Gap 60+:</strong> muted and large text (APCA floor Lc 45; palette worst case 47).</li> | ||
| <li><strong>Gap 70+:</strong> body text (floor Lc 60; worst case 68).</li> | ||
| <li><strong>Gap 80+:</strong> primary text (floor Lc 75; worst case 81).</li> |
…r() typo - color-audit: check the signal `-mark` tokens. The loop composed bare names, so the -mark rename left every signal-mark lookup undefined and the guard skipped it — 15 checks silently vanished and the run still reported PASS. -text stays optional, since heart-rate and air-quality intentionally ship none. - color-audit: an unresolved required token now fails instead of skipping, so a renamed or misspelled variable can't drop its pair from the audit unnoticed. - color-audit: read the `core` :root declarations underneath the scheme block, and follow var() hops through --mdhui aliases. The accent bases live in core and only dark overrides them, so the light audit had never resolved an accent at all — it reported 10 failures while 21 pairs were invisible. Light now resolves everything and reports 31 real failures (PR 4's scope); dark passes with all 15 mark rows genuinely checked. - chartHelpers: close the `var(--mdhui-color-primary-text)` fallback. The missing paren predates this branch, but the line is touched here and the invalid value meant uncolored line-chart series lost their gradient color entirely. - colorDocs: the APCA bullets labelled gap 70 as "body text" while the table above requires gap 80 for it. State them as APCA's own floors and say why the table is deliberately a step stricter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sty, duty fixes Brand color (Layout): renders exactly as configured, never transformed. In light it drives fills and foreground, as production always has. In dark it drives fills only; links keep the scheme's readable grade. An explicitly-set darkMode value also stays fill-only: no single dark value can carry white text (needs luminance <= 0.18) and read on the grade-85 card (needs >= 0.26), and ViewBuilder's ColorPicker has been storing darkMode values since 3.x — reinterpreting them as text colors would break saved views on upgrade. A dedicated text-color field is the follow-up. New Layout stories exercise both cases; new tests pin all four. Signals: heart-rate gains a real -text (red-orange-35 dark) — its mark grade is Lc 43.1 as foreground, under the 45 floor. Air-quality's mark is now audited AT the foreground floors (TEXT_VIA_MARK), which surfaced a real light-mode shortfall (3.9:1 on bg2) that the old probe-based guard silently skipped. Audit: core parsing is :root-scoped (a prefers-reduced-motion value was shadowing the :root one); unresolved required tokens fail instead of skipping; a typo'd signal token now breaks the run instead of deleting its own checks. Duty fixes: DailyDataGoal splits its one color into text default (-text) and fill default (base) — the -text grade washed out to 1.13:1 at opacity .3 over the track; ActivityMeter's meter fill matches ProgressBar on primary-text; colorDocs' progress preview matches the shipped default. Docs: APCA tiers restated from the spec with measured ramp floors (49.3/70.9/81.5); the non-monotone Lc-90 note (gap 82 clears it, 85 and 89 do not); the glucose/ heart-rate collision warning; heart-rate row wired to its own -text token. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Expands the MyDataHelps color palette to better support APCA (WCAG 3), dark mode, and color vision
differences.
The differences should be dark-mode-only, and barely noticeable--just for readability. Here are some examples: https://claude.ai/code/artifact/361ee3d2-6cb8-4f48-9de0-0f8b3f05ce86
One deliberate exception in light mode: GlucoseStats' glucose and sleep icons pick up the shared token values (
#d36540→#c4291c,#8287bb→#7b88c6). It was the only component using either hex — GlucoseChart, Histogram and DayTrackerSymbol already used#c4291c, and ActivityMeter, SparkBarChart and DayTrackerSymbol already used#7b88c6— so this converges an outlier rather than changing the palette. Before/after: https://claude.ai/code/artifact/41a4c046-0335-48de-aa1f-39100cb4f0cbWCAG 2.x ratios are calibrated for dark text on light backgrounds, while APCA, the perceptual model
drafted for WCAG 3, is the recommended measure for dark mode, so the dark tokens are chosen and
audited under both.
half-steps at 35 and 85 that give dark status text a legal home. The magic-number contract itself
is unchanged: a grade encodes lightness, and the gap between two grades sets the contrast floor for
any pair of hues (40 → 3:1, 50 → 4.5:1, 70 → 7:1). Every pair in the expanded set is verified
against it.
--mdh-, not--ce-: DesignerUI already uses--ce-*for a more muted palette. The old valuesare frozen in
src/helpers/legacyCeColors.tsand still emitted, so ViewBuilder content rendersunchanged.
grey-85over agrey-95background, and accentssplit by duty — the base token for fills that carry white text, a new
-textvariant forforeground. Signal colors (glucose, heart rate, activity, sleep, air quality) ship only as
explicit
-mark(chart marks) /-textpairs, so there is no bare signal name to grab for thewrong duty. Component references were rerouted to whichever one they actually need.
primaryColorin a single Emotion Global. Emotion orders style tags by mount order, not JSXorder, so with separate Globals a toggle into dark re-mounted the scheme tag after the brand tag
and reverted every primary-colored element to stock blue for the rest of the session.
npm run audit:colorschecks the token definitions: every semantic pairing against its rolefloor under WCAG 2.x and APCA. It's a standalone tool, not a build step — run it after any token
change. It reads the token file only, so it confirms the tokens are sound, not that a given
component reached for the right one.
Start/ColorsStorybook page rewritten for the new system.Stacked on #646 (the Storybook theme toggle), which needs to land first.
Follow-ups
Security
CSS custom properties, component style references, a build-time audit script, and a Storybook page.
No participant data, credentials, or API surface. Two new dependencies (
colorjs.io,apca-w3) runonly at build and test time.
Testing
What changed, in plain terms: dark mode has a rebuilt color set. Light mode should look the same
as it does today — if a light-mode screen looks different from production, that's a bug.
Important: this PR converts the shared color system, but a number of screens still have colors
written directly into them. Those are already known and are being fixed in a follow-up PR. Please
don't treat them as blockers here — but please do write them down (see "Two kinds of problems"
below), because that list is exactly what the follow-up needs.
How to test (Storybook):
and dark.
as the reference for what correct dark mode looks like — ten components, light on the left, dark
on the right.
green — just lighter, so they show up on dark cards.
every time. If a story sets a customer brand color, that color must survive the toggle rather
than reverting to the standard blue.
Two kinds of problems, and how to tell them apart. Switch the story between light and dark and
watch the element in question:
Known to be in the second bucket already, so no need to re-report unless something looks worse than
"stuck in light mode": the Asthma screens (control calendar, log entry details, provider report),
Symptom Shark (notes timeline, severity chart, monthly report, entry icons), and the project header,
badges, daily data goal, blood pressure readings, and recent-data bar chart. Anything outside that
list is worth writing down to make sure we catch it in the follow-up PR.
In the apps (iOS / Android / Web): the OS dark mode setting is a real input that Storybook can
only simulate, so a short pass on device is needed.
app follow, and if a customer brand color is set, does it stay the brand color?
Already verified (no manual steps needed): a standalone contrast audit was run over every color pairing,
checking each against both the current accessibility standard and the newer one
designed for dark mode; the full unit-test suite passes (131 suites, 621 tests); the gallery above
was generated from this exact branch; and the older
--ce-*colors used by ViewBuilder content areunchanged byte-for-byte.
Documentation
@CareEvolution/api-docs.Reviewers
To be assigned.