From 1a634aca857e85185970126d8354a8ae6b6ee396 Mon Sep 17 00:00:00 2001 From: Summer Schrader Date: Fri, 7 Aug 2026 11:31:45 -0400 Subject: [PATCH 1/4] Dark mode: remap the semantic tokens onto a purpose-built palette 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) --- package-lock.json | 30 + package.json | 5 +- scripts/color-audit.mjs | 181 ++++ src/components/@types/custom.d.ts | 6 + .../AsthmaActionPlanManager.css | 4 +- .../AsthmaAirQualities/AsthmaAirQualities.css | 4 +- .../AsthmaLogEntryDetails.css | 2 +- .../asthma/helpers/asthma-functions.ts | 2 +- .../BloodPressureVisualization.css | 6 +- .../container/ConnectDevice/ConnectDevice.css | 4 +- .../ConnectDevicesMenu/ConnectDevicesMenu.css | 4 +- .../container/ConnectEhr/ConnectEhr.css | 4 +- .../ConnectedDevices/ConnectedDevices.css | 6 +- .../container/DailyDataGoal/DailyDataGoal.tsx | 4 +- .../DeviceDataMonthChart.tsx | 2 +- .../container/EhrNewsFeed/EhrNewsFeed.css | 2 +- .../ExternalAccountConnectionAlert.tsx | 2 +- .../container/GlucoseChart/GlucoseChart.css | 2 +- .../container/GlucoseChart/GlucoseChart.tsx | 8 +- .../GlucoseDayCoordinator.tsx | 2 +- src/components/container/Goal/Goal.tsx | 4 +- .../container/MealEditor/MealEditor.css | 12 +- .../container/MicroTrend/MicroTrend.tsx | 4 +- .../MostRecentNotification.css | 2 +- .../ProjectSupport/ProjectSupport.css | 4 +- .../ProviderSearch/ProviderSearch.css | 4 +- .../RecentDailyDataBarChart.tsx | 2 +- .../RelativeActivity/RelativeActivity.tsx | 2 +- .../RelativeActivityDayCoordinator.tsx | 2 +- .../SurveyAnswerChart/SurveyAnswerChart.tsx | 2 +- .../presentational/Action/Action.css | 2 +- .../ActivityMeter/ActivityMeter.css | 2 +- .../presentational/Button/Button.tsx | 4 +- src/components/presentational/Chat/Chat.css | 2 +- .../DateRangeNavigator/DateRangeNavigator.css | 4 +- .../DiscreteScale/DiscreteScale.tsx | 2 +- .../DumbbellChart/DumbbellChart.css | 12 +- .../GlucoseStats/GlucoseStats.tsx | 8 +- .../presentational/Histogram/Histogram.css | 2 +- .../LabResultWithSparkline.css | 2 +- .../LabResultWithSparkline.tsx | 4 +- .../presentational/Layout/Layout.tsx | 16 +- .../MealAnalysis/MealAnalysis.css | 4 +- .../NavigationBar/NavigationBar.css | 2 +- .../presentational/NotesInput/NotesInput.css | 2 +- .../ProgressRing/ProgressRing.css | 2 +- .../presentational/Resource/Resource.css | 2 +- .../SegmentedControl/SegmentedControl.css | 6 +- .../SingleDataPoint/SingleDataPoint.css | 2 +- .../SingleExternalAccount.css | 2 +- .../presentational/SingleMeal/SingleMeal.tsx | 2 +- .../SingleSurveyTask/SingleSurveyTask.css | 6 +- .../TermInformationButton.tsx | 2 +- .../ValueSelector/ValueSelector.css | 2 +- src/components/step/StepLayout/StepLayout.css | 2 +- .../container/ReportBuilder/ReportBuilder.css | 2 +- .../presentational/Calendar/Calendar.css | 2 +- .../NotesTimeline/NotesTimeline.css | 2 +- .../DocumentDetailView/DocumentDetailView.css | 2 +- .../DocumentDetailView/DocumentDetailView.tsx | 2 +- .../view/NewPointsView/NewPointsView.css | 6 +- src/helpers/chartHelpers.tsx | 18 +- src/helpers/globalCss.ts | 864 ++++++++++++------ src/helpers/legacyCeColors.ts | 260 ++++++ .../news-feed/eventTypeDefinitions.tsx | 4 +- src/stories/foundation/colorDocs.css | 166 ++-- src/stories/foundation/colorDocs.tsx | 449 +++++---- 67 files changed, 1569 insertions(+), 622 deletions(-) create mode 100644 scripts/color-audit.mjs create mode 100644 src/helpers/legacyCeColors.ts diff --git a/package-lock.json b/package-lock.json index 38ea6e7f8..f7c18cbd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,8 +60,10 @@ "@types/markdown-it": "^12.2.3", "@types/react": "^18.0.33", "@types/uuid": "^10.0.0", + "apca-w3": "^0.1.9", "babel-jest": "^29.7.0", "babel-loader": "^9.1.3", + "colorjs.io": "^0.6.1", "jest": "^29.7.0", "jest-environment-jsdom": "^30.3.0", "mkdirp": "^3.0.1", @@ -6378,6 +6380,16 @@ "node": ">= 8" } }, + "node_modules/apca-w3": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/apca-w3/-/apca-w3-0.1.9.tgz", + "integrity": "sha512-Zrf6AeBeQjNe/fxK7U1jCo5zfdjDl6T4/kdw5Xlky3G7u+EJTZkyItjMYQGtwf9pkftsINxcYyOpuLkzKf1ITQ==", + "dev": true, + "license": "Limited W3 License", + "dependencies": { + "colorparsley": "^0.1.8" + } + }, "node_modules/append-transform": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", @@ -7439,6 +7451,24 @@ "dev": true, "license": "MIT" }, + "node_modules/colorjs.io": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.6.1.tgz", + "integrity": "sha512-8lyR2wHzuIykCpqHKgluGsqQi5iDm3/a2IgP2GBZrasn2sBRkE4NOGsglZxWLs/jZQoNkmA/KM/8NV16rLUdBg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/color" + } + }, + "node_modules/colorparsley": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/colorparsley/-/colorparsley-0.1.8.tgz", + "integrity": "sha512-rObESTTTE6G5qO5WFwFxWPggpw4KfpxnLC6Ssl8bITBnNVRhDsyCeTRAUxWQNVTx2pRknKlO2nddYTxjkdNFaw==", + "dev": true, + "license": "AGPL v3" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", diff --git a/package.json b/package.json index 117f8e6ca..35c5386dc 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "storybook": "tsx scripts/prepare-storybook.ts && set NODE_OPTIONS=--openssl-legacy-provider && storybook dev -p 6006", "build-storybook": "tsx scripts/prepare-storybook.ts && set NODE_OPTIONS=--openssl-legacy-provider && storybook build", "test": "jest", - "test-storybook": "test-storybook" + "test-storybook": "test-storybook", + "audit:colors": "node scripts/color-audit.mjs semantic --scheme dark --ci" }, "repository": { "type": "git", @@ -69,8 +70,10 @@ "@types/markdown-it": "^12.2.3", "@types/react": "^18.0.33", "@types/uuid": "^10.0.0", + "apca-w3": "^0.1.9", "babel-jest": "^29.7.0", "babel-loader": "^9.1.3", + "colorjs.io": "^0.6.1", "jest": "^29.7.0", "jest-environment-jsdom": "^30.3.0", "mkdirp": "^3.0.1", diff --git a/scripts/color-audit.mjs b/scripts/color-audit.mjs new file mode 100644 index 000000000..0d819e1ec --- /dev/null +++ b/scripts/color-audit.mjs @@ -0,0 +1,181 @@ +#!/usr/bin/env node +// Color audit: WCAG 2.x ratio + APCA Lc (both polarities) for the --mdh-* ramp and the +// --mdhui-* semantic mapping, checked against the dual-conformance role floors +// (TEXT_ROLES below). +// +// Usage: +// node scripts/color-audit.mjs semantic --scheme dark [--ci] # audit semantic role pairs +// node scripts/color-audit.mjs semantic --scheme light [--ci] +// node scripts/color-audit.mjs ramp [--hue red] [--json] # per-hue delta -> min Lc/ratio +// node scripts/color-audit.mjs pair "#fff" "#1d1c22" # ad-hoc one pair +// +// Exit code: 0 = all required pairs pass, 1 = a required pair fails (CI-friendly), 2 = usage. +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { calcAPCA } from 'apca-w3'; +import Color from 'colorjs.io'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const GLOBALCSS = join(ROOT, 'src/helpers/globalCss.ts'); +const src = readFileSync(GLOBALCSS, 'utf8'); + +// ---- parse the --mdh-* ramp (literal hexes) ---- +const ramp = {}; +for (const m of src.matchAll(/(--mdh-[a-z0-9-]+)\s*:\s*(#[0-9A-Fa-f]{3,8})/g)) ramp[m[1]] = m[2]; + +// ---- resolve a semantic value to a hex (follows one var(--mdh-*) hop; raw hex passes through) ---- +function resolve(val) { + val = val.trim(); + const v = val.match(/^var\((--[a-z0-9-]+)\)$/); + if (v) return ramp[v[1]] ?? null; + if (/^#[0-9A-Fa-f]{3,8}$/.test(val)) return val; + if (/^rgb/i.test(val)) return val; + return null; // gradients / nested / unresolvable +} + +// ---- parse a semantic style object (lightColorStyle / darkColorStyle) ---- +function semanticTokens(scheme) { + const name = scheme === 'dark' ? 'darkColorStyle' : 'lightColorStyle'; + const start = src.indexOf(`export const ${name}`); + if (start < 0) throw new Error(`${name} not found`); + const open = src.indexOf('{', start); + let depth = 0, end = -1; + for (let i = open; i < src.length; i++) { if (src[i] === '{') depth++; else if (src[i] === '}') { if (--depth === 0) { end = i; break; } } } + const block = src.slice(open, end); + const toks = {}; + for (const m of block.matchAll(/'(--mdhui-[a-z0-9-]+)'\s*:\s*'([^']+)'/g)) toks[m[1]] = m[2]; + return toks; +} + +// ---- metrics ---- +const wcag = (fg, bg) => new Color(fg).contrast(bg, 'WCAG21'); +const apca = (fg, bg) => calcAPCA(fg, bg); // signed Lc; negative = light-on-dark +const minLc = (a, b) => Math.min(Math.abs(apca(a, b)), Math.abs(apca(b, a))); // weaker polarity + +// ---- conformance floors (dark/light polarity as computed) ---- +const TEXT_ROLES = [ + { role: 'primary text', tokens: ['--mdhui-text-color-0', '--mdhui-text-color-1'], wcag: 4.5, apca: 75 }, + { role: 'secondary text', tokens: ['--mdhui-text-color-2'], wcag: 4.5, apca: 60 }, + { role: 'muted text', tokens: ['--mdhui-text-color-3'], wcag: 3.0, apca: 45 }, + { role: 'disabled text', tokens: ['--mdhui-text-color-4'], wcag: 0, apca: 30, exempt: true }, +]; +const SURFACES = ['--mdhui-background-color-0', '--mdhui-background-color-1', '--mdhui-background-color-2']; +const BORDERS = ['--mdhui-border-color-0', '--mdhui-border-color-1', '--mdhui-border-color-2']; + +// Accents and signals carry two duties and are checked against different floors: +// base — fills (buttons, chips) and chart marks. A filled control with contrasting text +// on it needs no boundary contrast (WCAG 1.4.11 exemption), so the base token is +// checked for the white text it carries, not against the surface behind it. +// Marks are non-text functional, so they do get the 3:1 floor vs the surface. +// -text — the same role as foreground (links, status text, icons that must be seen). +const ACCENTS = ['primary', 'success', 'warning', 'danger']; +const SIGNALS = ['glucose', 'heart-rate', 'activity', 'sleep', 'air-quality']; +const FILL_INK = '#fff'; // what the accent fills carry +// -text is used at many sizes/weights (links, 15px/600 headings, status text): WCAG normal-text +// floor plus the APCA muted/large tier. Aim higher for small body-size foregrounds. +const FOREGROUND_FLOORS = { wcag: 4.5, apca: 45 }; +const MARK_FLOORS = { wcag: 3.0, apca: 15 }; + +function auditSemantic(scheme, ci) { + const t = semanticTokens(scheme); + const hex = k => (k.startsWith('#') ? k : resolve(t[k] ?? '')); + const rows = []; + let fails = 0; +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; } + const w = wcag(fH, bH), lc = Math.abs(apca(fH, bH)); + const passW = exempt || w >= wFloor, passA = exempt || lc >= aFloor; + const pass = passW && passA; + if (!pass && !exempt) fails++; + rows.push({ label, w, lc, wFloor, aFloor, pass, exempt, aExtra: lc >= aFloor }); + }; + // text on each surface + for (const rr of TEXT_ROLES) for (const tok of rr.tokens) for (const s of SURFACES) + pushPair(`${rr.role} (${tok.replace('--mdhui-text-color-', 't')}) on ${s.replace('--mdhui-background-color-', 'bg')}`, tok, s, rr.wcag, rr.apca, rr.exempt); + // borders on the matching surface (border-N on bg-N) — reported, not gating: all + // --mdhui-border-* uses are structural, which WCAG 1.4.11 exempts from a floor. + BORDERS.forEach((b, i) => pushPair(`border ${i} on bg${i}`, b, SURFACES[Math.min(i, SURFACES.length - 1)], 0, 15, true)); + const bgName = s => s.replace('--mdhui-background-color-', 'bg'); + for (const a of ACCENTS) { + pushPair(`#fff on ${a} fill`, FILL_INK, `--mdhui-color-${a}`, 4.5, 45, false); + for (const s of SURFACES) { + // fills are 1.4.11-exempt against the surface (their ink identifies them) — reported only + pushPair(`${a} fill vs ${bgName(s)}`, `--mdhui-color-${a}`, s, 3.0, 15, true); + pushPair(`${a}-text on ${bgName(s)}`, `--mdhui-color-${a}-text`, s, FOREGROUND_FLOORS.wcag, FOREGROUND_FLOORS.apca, false); + } + } + 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); + } + } + // adjacent elevation surfaces — target Lc 15 (or border-assisted; reported, not gating) + const surfPairs = [[0, 1], [1, 2]]; + const surfaceNotes = surfPairs.map(([a, b]) => { + const A = hex(SURFACES[a]), B = hex(SURFACES[b]); + return { label: `surface bg${a} vs bg${b}`, w: wcag(A, B), lc: minLc(A, B) }; + }); + // text-3 vs text-4 must differ (spec acceptance) + const t3 = hex('--mdhui-text-color-3'), t4 = hex('--mdhui-text-color-4'); + const distinct34 = t3 && t4 && t3.toLowerCase() !== t4.toLowerCase(); + + // ---- report ---- + console.log(`\n=== SEMANTIC AUDIT — ${scheme.toUpperCase()} scheme (role floors) ===\n`); + console.log(' role pair'.padEnd(40) + 'WCAG'.padStart(8) + 'APCA Lc'.padStart(10) + ' floors verdict'); + for (const r of rows) { + if (r.skip) { console.log(' ' + r.label.padEnd(38) + ' ' + r.note); continue; } + const v = r.exempt ? (r.aExtra ? 'exempt ✓vis' : 'exempt') : (r.pass ? 'PASS' : 'FAIL'); + console.log(' ' + r.label.padEnd(38) + fmt(r.w, 1, ':1').padStart(8) + fmt(r.lc, 1).padStart(10) + ` ${r.wFloor}/${r.aFloor}`.padEnd(12) + ' ' + v); + } + console.log('\n surfaces & separation:'); + for (const s of surfaceNotes) console.log(' ' + s.label.padEnd(38) + fmt(s.w, 2, ':1').padStart(8) + fmt(s.lc, 1).padStart(10) + ` (target Lc 15 or border)`); + console.log(` text-3 ≠ text-4: ${distinct34 ? 'yes ✓' : 'NO ✗ (spec requires distinct muted vs disabled)'}`); + const ok = fails === 0 && distinct34; + console.log(`\n ${fails} required-pair failure(s). ${ok ? 'PASS' : 'FAIL'}\n`); + if (ci) process.exit(ok ? 0 : 1); + return ok; +} + +function auditRamp(hueFilter, json) { + // per hue: for each grade pair, WCAG + min-Lc; summarize delta -> min guaranteed Lc/ratio + const hues = {}; + for (const k of Object.keys(ramp)) { + const m = k.match(/^--mdh-([a-z-]+)-(\d+)$/); if (!m) continue; + (hues[m[1]] ??= []).push({ g: +m[2], hex: ramp[k] }); + } + const out = {}; + for (const [hue, grades] of Object.entries(hues)) { + if (hueFilter && hue !== hueFilter) continue; + grades.sort((a, b) => a.g - b.g); + const byDelta = {}; + for (let i = 0; i < grades.length; i++) for (let j = i + 1; j < grades.length; j++) { + const d = grades[j].g - grades[i].g; + const lc = minLc(grades[i].hex, grades[j].hex), w = wcag(grades[i].hex, grades[j].hex); + (byDelta[d] ??= { minLc: Infinity, minW: Infinity }); byDelta[d].minLc = Math.min(byDelta[d].minLc, lc); byDelta[d].minW = Math.min(byDelta[d].minW, w); + } + out[hue] = byDelta; + } + if (json) { console.log(JSON.stringify(out, null, 2)); return true; } + for (const [hue, byDelta] of Object.entries(out)) { + console.log(`\n${hue}: delta -> min guaranteed (Lc / WCAG) across all pairs at that grade gap`); + for (const d of Object.keys(byDelta).map(Number).sort((a, b) => a - b)) + console.log(` Δ${String(d).padStart(3)} Lc ${fmt(byDelta[d].minLc, 1).padStart(6)} ${fmt(byDelta[d].minW, 2).padStart(6)}:1`); + } + return true; +} + +function fmt(n, dp, suf = '') { return (n == null || !isFinite(n)) ? '—' : n.toFixed(dp) + suf; } + +// ---- CLI ---- +const [, , mode, ...rest] = process.argv; +const flag = n => rest.includes(n); +const opt = n => { const i = rest.indexOf(n); return i >= 0 ? rest[i + 1] : undefined; }; +try { + if (mode === 'semantic') auditSemantic(opt('--scheme') || 'dark', flag('--ci')); + else if (mode === 'ramp') auditRamp(opt('--hue'), flag('--json')); + else if (mode === 'pair') { const [fg, bg] = rest; console.log(`WCAG ${fmt(wcag(fg, bg), 2)}:1 | APCA Lc ${fmt(apca(fg, bg), 1)} (fg-on-bg) / ${fmt(apca(bg, fg), 1)} (bg-on-fg) | min |Lc| ${fmt(minLc(fg, bg), 1)}`); } + else { console.error('usage: color-audit.mjs [...]'); process.exit(2); } +} catch (e) { console.error('error:', e.message); process.exit(2); } diff --git a/src/components/@types/custom.d.ts b/src/components/@types/custom.d.ts index 7b8d003d7..ad4ed9777 100644 --- a/src/components/@types/custom.d.ts +++ b/src/components/@types/custom.d.ts @@ -6,4 +6,10 @@ declare module "*.svg" { declare module "*.png" { const content: any; export default content; +} + +// apca-w3 ships no type definitions; only the pieces used by colorDocs/scripts are declared. +declare module "apca-w3" { + export function sRGBtoY(rgb: number[]): number; + export function APCAcontrast(textY: number, backgroundY: number, places?: number): number; } \ No newline at end of file diff --git a/src/components/asthma/components/AsthmaActionPlanManager/AsthmaActionPlanManager.css b/src/components/asthma/components/AsthmaActionPlanManager/AsthmaActionPlanManager.css index 7c10629eb..f6294d184 100644 --- a/src/components/asthma/components/AsthmaActionPlanManager/AsthmaActionPlanManager.css +++ b/src/components/asthma/components/AsthmaActionPlanManager/AsthmaActionPlanManager.css @@ -12,7 +12,7 @@ .mdhui-unstyled-button.mdhui-asthma-action-plan-manager-learn-more { font-size: 0.8em; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); margin-bottom: 16px; cursor: pointer; } @@ -53,7 +53,7 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); font-weight: bold; } diff --git a/src/components/asthma/components/AsthmaAirQualities/AsthmaAirQualities.css b/src/components/asthma/components/AsthmaAirQualities/AsthmaAirQualities.css index c0542d4d5..0676e5454 100644 --- a/src/components/asthma/components/AsthmaAirQualities/AsthmaAirQualities.css +++ b/src/components/asthma/components/AsthmaAirQualities/AsthmaAirQualities.css @@ -17,7 +17,7 @@ text-align: right; line-height: 32px; font-size: 20px; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); margin-bottom: 16px; } @@ -42,7 +42,7 @@ .mdhui-asthma-air-qualities-setup-button { font-size: 1.3em; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } .mdhui-asthma-air-qualities-setup-label { diff --git a/src/components/asthma/components/AsthmaLogEntryDetails/AsthmaLogEntryDetails.css b/src/components/asthma/components/AsthmaLogEntryDetails/AsthmaLogEntryDetails.css index 5aa344e29..8364f0b83 100644 --- a/src/components/asthma/components/AsthmaLogEntryDetails/AsthmaLogEntryDetails.css +++ b/src/components/asthma/components/AsthmaLogEntryDetails/AsthmaLogEntryDetails.css @@ -40,7 +40,7 @@ text-align: right; line-height: 32px; font-size: 20px; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } .mdhui-asthma-log-entry-details-component { diff --git a/src/components/asthma/helpers/asthma-functions.ts b/src/components/asthma/helpers/asthma-functions.ts index f38382eac..4b4477f69 100644 --- a/src/components/asthma/helpers/asthma-functions.ts +++ b/src/components/asthma/helpers/asthma-functions.ts @@ -90,7 +90,7 @@ export const getAsthmaDataStatusText = (status: AsthmaDataStatus): string => { export const getAsthmaDataStatusColor = (status: AsthmaDataStatus): ColorDefinition => { if (status === 'out-of-range') return '#9D5BED'; if (status === 'in-range') return '#188A83'; - if (status === 'offline') return 'var(--mdhui-color-warning)'; + if (status === 'offline') return 'var(--mdhui-color-warning-text)'; return 'var(--mdhui-text-color-3)'; }; diff --git a/src/components/container/BloodPressureVisualization/BloodPressureVisualization.css b/src/components/container/BloodPressureVisualization/BloodPressureVisualization.css index c68af160d..4ade7d881 100644 --- a/src/components/container/BloodPressureVisualization/BloodPressureVisualization.css +++ b/src/components/container/BloodPressureVisualization/BloodPressureVisualization.css @@ -53,7 +53,7 @@ line-height: 15px; letter-spacing: 0em; text-align: left; - color: var(--mdhui-color-danger); + color: var(--mdhui-color-danger-text); } .mdhui-blood-pressure-metrics .mdhui-blood-pressure-metrics-rows @@ -69,10 +69,10 @@ .mdhui-blood-pressure-metrics .mdhui-blood-pressure-metrics-rows .mdhui-blood-pressure-metrics-block .mdhui-blood-pressure-metric-normal{ font-size: 17px; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } .mdhui-blood-pressure-metrics .mdhui-blood-pressure-metrics-rows .mdhui-blood-pressure-metrics-block .mdhui-blood-pressure-metric-not-normal{ - color: var(--mdhui-color-danger); + color: var(--mdhui-color-danger-text); } \ No newline at end of file diff --git a/src/components/container/ConnectDevice/ConnectDevice.css b/src/components/container/ConnectDevice/ConnectDevice.css index 728d1dc1d..5c3ca6db6 100644 --- a/src/components/container/ConnectDevice/ConnectDevice.css +++ b/src/components/container/ConnectDevice/ConnectDevice.css @@ -3,11 +3,11 @@ } .mdhui-connect-device .subtitle.reconnect { - color: var(--mdhui-color-danger); + color: var(--mdhui-color-danger-text); } .mdhui-connect-device .subtitle.success svg { - color: var(--mdhui-color-success); + color: var(--mdhui-color-success-text); } .mdhui-connect-device .mdhui-button { diff --git a/src/components/container/ConnectDevicesMenu/ConnectDevicesMenu.css b/src/components/container/ConnectDevicesMenu/ConnectDevicesMenu.css index 33077d9ab..e51487084 100644 --- a/src/components/container/ConnectDevicesMenu/ConnectDevicesMenu.css +++ b/src/components/container/ConnectDevicesMenu/ConnectDevicesMenu.css @@ -19,11 +19,11 @@ } .mdhui-connect-devices-menu .mdhui-connect-devices-menu-connect { - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } .mdhui-connect-devices-menu .mdhui-connect-devices-menu-reconnect { - color: var(--mdhui-color-danger); + color: var(--mdhui-color-danger-text); } .mdhui-connect-devices-menu .mdhui-connect-devices-menu-device .mdhui-connect-devices-menu-help { diff --git a/src/components/container/ConnectEhr/ConnectEhr.css b/src/components/container/ConnectEhr/ConnectEhr.css index 3be46cb4a..0f0cd2b73 100644 --- a/src/components/container/ConnectEhr/ConnectEhr.css +++ b/src/components/container/ConnectEhr/ConnectEhr.css @@ -7,11 +7,11 @@ } .mdhui-connect-ehr .connection-status .success svg { - color: var(--mdhui-color-success); + color: var(--mdhui-color-success-text); } .mdhui-connect-ehr .connection-status .warning { - color: var(--mdhui-color-danger); + color: var(--mdhui-color-danger-text); } .mdhui-connect-ehr .mdhui-connect-ehr-action { diff --git a/src/components/container/ConnectedDevices/ConnectedDevices.css b/src/components/container/ConnectedDevices/ConnectedDevices.css index 801d68745..ca2436831 100644 --- a/src/components/container/ConnectedDevices/ConnectedDevices.css +++ b/src/components/container/ConnectedDevices/ConnectedDevices.css @@ -37,13 +37,13 @@ .mdhui-connected-devices .connected-device .battery-icon.battery-empty, .mdhui-connected-devices .connected-device .battery-icon.battery-low { - color: var(--mdhui-color-danger); + color: var(--mdhui-color-danger-text); } .mdhui-connected-devices .connected-device .battery-icon.battery-medium { - color: var(--mdhui-color-warning); + color: var(--mdhui-color-warning-text); } .mdhui-connected-devices .connected-device .battery-icon.battery-high { - color: var(--mdhui-color-success); + color: var(--mdhui-color-success-text); } diff --git a/src/components/container/DailyDataGoal/DailyDataGoal.tsx b/src/components/container/DailyDataGoal/DailyDataGoal.tsx index 987d0bb2f..5fba67fcb 100644 --- a/src/components/container/DailyDataGoal/DailyDataGoal.tsx +++ b/src/components/container/DailyDataGoal/DailyDataGoal.tsx @@ -41,8 +41,8 @@ export default function (props: DailyDataGoalProps) { }); }, ["externalAccountSyncComplete"], [dateRangeContext?.intervalStart]); - const goalCompleteColor = props.goalCompleteColor || "var(--mdhui-color-success)"; - const goalIncompleteColor = props.goalIncompleteColor || "var(--mdhui-color-primary)"; + const goalCompleteColor = props.goalCompleteColor || "var(--mdhui-color-success-text)"; + const goalIncompleteColor = props.goalIncompleteColor || "var(--mdhui-color-primary-text)"; let goalProgress: number | null = dailyValue == null ? null : dailyValue / props.goal; if (goalProgress != null && goalProgress > 1) goalProgress = 1; diff --git a/src/components/container/DeviceDataMonthChart/DeviceDataMonthChart.tsx b/src/components/container/DeviceDataMonthChart/DeviceDataMonthChart.tsx index a462d0ddb..b7ed150e3 100644 --- a/src/components/container/DeviceDataMonthChart/DeviceDataMonthChart.tsx +++ b/src/components/container/DeviceDataMonthChart/DeviceDataMonthChart.tsx @@ -240,7 +240,7 @@ export default function (props: DeviceDataMonthChartProps) { {props.lines.map((line) => - + )} } /> diff --git a/src/components/container/EhrNewsFeed/EhrNewsFeed.css b/src/components/container/EhrNewsFeed/EhrNewsFeed.css index 50216f479..787196295 100644 --- a/src/components/container/EhrNewsFeed/EhrNewsFeed.css +++ b/src/components/container/EhrNewsFeed/EhrNewsFeed.css @@ -39,7 +39,7 @@ } .mdhui-news-feed .mdhui-news-feed-list-item .mdhui-news-feed-list-item-more { - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); font-weight: bold; } diff --git a/src/components/container/ExternalAccountConnectionAlert/ExternalAccountConnectionAlert.tsx b/src/components/container/ExternalAccountConnectionAlert/ExternalAccountConnectionAlert.tsx index 9e1e8b24b..c7f886621 100644 --- a/src/components/container/ExternalAccountConnectionAlert/ExternalAccountConnectionAlert.tsx +++ b/src/components/container/ExternalAccountConnectionAlert/ExternalAccountConnectionAlert.tsx @@ -61,7 +61,7 @@ export default function (props: ExternalAccountConnectionAlertProps) { } return } + icon={} subtitle={language("connect-ehr-needs-attention")} onClick={props.onClick} /> } \ No newline at end of file diff --git a/src/components/container/GlucoseChart/GlucoseChart.css b/src/components/container/GlucoseChart/GlucoseChart.css index b9a68c33c..c03a78455 100644 --- a/src/components/container/GlucoseChart/GlucoseChart.css +++ b/src/components/container/GlucoseChart/GlucoseChart.css @@ -5,7 +5,7 @@ .mdhui-glucose-chart .mdhui-glucose-chart-title { font-size: .88em; font-weight: 600; - color: rgb(196, 41, 28); + color: var(--mdhui-color-glucose-text); margin: 16px 16px -8px; } diff --git a/src/components/container/GlucoseChart/GlucoseChart.tsx b/src/components/container/GlucoseChart/GlucoseChart.tsx index 30425514d..32127442c 100644 --- a/src/components/container/GlucoseChart/GlucoseChart.tsx +++ b/src/components/container/GlucoseChart/GlucoseChart.tsx @@ -228,11 +228,11 @@ export default function (props: GlucoseChartProps) { {props.variant !== 'minimal' && glucoseContext?.recentAverage !== undefined && } {props.variant !== 'minimal' && - + }
{props.emptyText ?? language('glucose-chart-no-data')}
diff --git a/src/components/container/GlucoseDayCoordinator/GlucoseDayCoordinator.tsx b/src/components/container/GlucoseDayCoordinator/GlucoseDayCoordinator.tsx index 751ddcfcf..1925cbb78 100644 --- a/src/components/container/GlucoseDayCoordinator/GlucoseDayCoordinator.tsx +++ b/src/components/container/GlucoseDayCoordinator/GlucoseDayCoordinator.tsx @@ -43,7 +43,7 @@ export default function (props: GlucoseDayCoordinatorProps) { const dayRenderer = (dayKey: string): React.JSX.Element | null => { let sparkRanges: SparkRangeChartRange[] = []; if (glucoseRangeLookup?.hasOwnProperty(dayKey)) { - sparkRanges.push({ ...(glucoseRangeLookup[dayKey]) }); + sparkRanges.push({ ...(glucoseRangeLookup[dayKey]), color: 'var(--mdhui-color-glucose-text)' }); } return
diff --git a/src/components/container/Goal/Goal.tsx b/src/components/container/Goal/Goal.tsx index 0ba06c532..df69e977b 100644 --- a/src/components/container/Goal/Goal.tsx +++ b/src/components/container/Goal/Goal.tsx @@ -79,8 +79,8 @@ export default function Goal(props: GoalProps) { }, [], [props.previewState, props.targetValue, props.maxValue, props.valueProvider]); const defaultColor = 'var(--mdhui-text-color-3)'; - const defaultInProgressColor = 'var(--mdhui-color-primary)'; - const defaultCompletedColor = 'var(--mdhui-color-success)'; + const defaultInProgressColor = 'var(--mdhui-color-primary-text)'; + const defaultCompletedColor = 'var(--mdhui-color-success-text)'; // These next few blocks of code are for backward compatibility. When necessary, they map individual ColorDefinitions onto // the new GoalColorConfiguration model in a way that retains backward compatible coloring behavior. diff --git a/src/components/container/MealEditor/MealEditor.css b/src/components/container/MealEditor/MealEditor.css index d69af20f4..9134c4827 100644 --- a/src/components/container/MealEditor/MealEditor.css +++ b/src/components/container/MealEditor/MealEditor.css @@ -16,7 +16,7 @@ } .mdhui-meal-editor-header-delete-action { - color: var(--mdhui-color-danger); + color: var(--mdhui-color-danger-text); width: 28px; text-align: right; } @@ -76,7 +76,7 @@ .mdhui-meal-editor-image-add { margin-top: 8px; background: var(--mdhui-background-color-1); - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } .mdhui-meal-editor-image-loading { @@ -128,11 +128,11 @@ text-align: center; background: var(--mdhui-meal-editor-image-action-background-color); border-radius: 5px; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } .mdhui-meal-editor-image-action-remove { - color: var(--mdhui-color-danger); + color: var(--mdhui-color-danger-text); } .mdhui-meal-editor-items { @@ -191,7 +191,7 @@ } .mdhui-meal-editor-item-delete-icon { - color: var(--mdhui-color-danger); + color: var(--mdhui-color-danger-text); font-size: 0.85em; } @@ -208,7 +208,7 @@ .mdhui-meal-editor-error { margin-top: 16px; - color: var(--mdhui-color-danger); + color: var(--mdhui-color-danger-text); font-size: 0.9em; } diff --git a/src/components/container/MicroTrend/MicroTrend.tsx b/src/components/container/MicroTrend/MicroTrend.tsx index c54566521..bf8879fb0 100644 --- a/src/components/container/MicroTrend/MicroTrend.tsx +++ b/src/components/container/MicroTrend/MicroTrend.tsx @@ -99,7 +99,7 @@ export default function MicroTrend(props: MicroTrendProps) { const dayValue = results?.[dayKey]?.value ?? 0; const noData = dayValue === 0; - const color = noData ? "var(--mdhui-text-color-3)" : resolveColor(layoutContext.colorScheme, props.dataType.color) ?? "var(--mdhui-color-primary)"; + const color = noData ? "var(--mdhui-text-color-3)" : resolveColor(layoutContext.colorScheme, props.dataType.color) ?? "var(--mdhui-color-primary-text)"; const getBars = (): SparkBarChartBar[] => { const bars: SparkBarChartBar[] = []; @@ -111,7 +111,7 @@ export default function MicroTrend(props: MicroTrendProps) { const fillPercent = dayResult?.fillPercent ?? 0; bars.push({ barFillPercent: fillPercent, - color: (value > threshold ? (dataType.overThresholdColor ?? dataType.color) : dataType.color) || "var(--mdhui-color-primary)", + color: (value > threshold ? (dataType.overThresholdColor ?? dataType.color) : dataType.color) || "var(--mdhui-color-primary-text)", opacity: (i === 0 ? 1 : 0.4) }); } diff --git a/src/components/container/MostRecentNotification/MostRecentNotification.css b/src/components/container/MostRecentNotification/MostRecentNotification.css index 313aea655..6dd5fabce 100644 --- a/src/components/container/MostRecentNotification/MostRecentNotification.css +++ b/src/components/container/MostRecentNotification/MostRecentNotification.css @@ -3,5 +3,5 @@ } .mdhui-most-recent-notification .mdhui-action .subtitle { - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } \ No newline at end of file diff --git a/src/components/container/ProjectSupport/ProjectSupport.css b/src/components/container/ProjectSupport/ProjectSupport.css index caacf8869..4572ad0fb 100644 --- a/src/components/container/ProjectSupport/ProjectSupport.css +++ b/src/components/container/ProjectSupport/ProjectSupport.css @@ -5,10 +5,10 @@ } .mdhui-project-support .mdhui-action { - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); border-bottom: solid 1px var(--mdhui-border-color-0); } .mdhui-project-support .mdhui-action .subtitle { - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } \ No newline at end of file diff --git a/src/components/container/ProviderSearch/ProviderSearch.css b/src/components/container/ProviderSearch/ProviderSearch.css index 2a9d6bdc7..59fd5198e 100644 --- a/src/components/container/ProviderSearch/ProviderSearch.css +++ b/src/components/container/ProviderSearch/ProviderSearch.css @@ -70,11 +70,11 @@ } .mdhui-provider-search .search-results .provider .provider-status.error-status { - color: var(--mdhui-color-danger); + color: var(--mdhui-color-danger-text); } .mdhui-provider-search .search-results .provider .provider-status.connected-status { - color: var(--mdhui-color-success); + color: var(--mdhui-color-success-text); } .mdhui-provider-search .search-results .provider.disabled { diff --git a/src/components/container/RecentDailyDataBarChart/RecentDailyDataBarChart.tsx b/src/components/container/RecentDailyDataBarChart/RecentDailyDataBarChart.tsx index 68fa22865..3500090f3 100644 --- a/src/components/container/RecentDailyDataBarChart/RecentDailyDataBarChart.tsx +++ b/src/components/container/RecentDailyDataBarChart/RecentDailyDataBarChart.tsx @@ -121,7 +121,7 @@ export default function (props: RecentDailyDataBarChartProps) { const labelContent = ({x, y, width, value, index}: any) => { if (!value) return undefined; - let textColor = 'var(--mdhui-color-primary)'; + let textColor = 'var(--mdhui-color-primary-text)'; if (props.highlight && props.highlight(data[index].rawValue)) { textColor = '#6731AA'; } diff --git a/src/components/container/RelativeActivity/RelativeActivity.tsx b/src/components/container/RelativeActivity/RelativeActivity.tsx index ee00e40d2..86e5d4b26 100644 --- a/src/components/container/RelativeActivity/RelativeActivity.tsx +++ b/src/components/container/RelativeActivity/RelativeActivity.tsx @@ -82,7 +82,7 @@ export default function (props: RelativeActivityProps) { if (dataType.threshold !== undefined && dataType.overThresholdColor && fillPercent > .5) { return dataType.overThresholdColor; } - return dataType.color || "var(--mdhui-color-primary)"; + return dataType.color || "var(--mdhui-color-primary-text)"; } function getThresholdLabel(dataType: RelativeActivityDataType, threshold: number) { diff --git a/src/components/container/RelativeActivityDayCoordinator/RelativeActivityDayCoordinator.tsx b/src/components/container/RelativeActivityDayCoordinator/RelativeActivityDayCoordinator.tsx index 99b900ad8..c77a0aeb1 100644 --- a/src/components/container/RelativeActivityDayCoordinator/RelativeActivityDayCoordinator.tsx +++ b/src/components/container/RelativeActivityDayCoordinator/RelativeActivityDayCoordinator.tsx @@ -49,7 +49,7 @@ export default function RelativeActivityDateRangeCoordinator(props: RelativeActi const dataForDay = relativeActivityData?.[dataType.dailyDataType]?.[dayKey]; const value = dataForDay?.value ?? 0; - let color = dataType.color || 'var(--mdhui-color-primary)'; + let color = dataType.color || 'var(--mdhui-color-primary-text)'; if (dataType.threshold !== undefined && dataType.threshold !== '30DayAverage' && value > dataType.threshold && dataType.overThresholdColor) { color = dataType.overThresholdColor; } diff --git a/src/components/container/SurveyAnswerChart/SurveyAnswerChart.tsx b/src/components/container/SurveyAnswerChart/SurveyAnswerChart.tsx index 885d2c71f..7ed521b9c 100644 --- a/src/components/container/SurveyAnswerChart/SurveyAnswerChart.tsx +++ b/src/components/container/SurveyAnswerChart/SurveyAnswerChart.tsx @@ -136,7 +136,7 @@ export default function SurveyAnswerChart(props: SurveyAnswerChartProps) { {payload.map((series: any, index: number) => { - const seriesColor = resolveColor(layoutContext.colorScheme, props.series[index].color) ?? 'var(--mdhui-color-primary)'; + const seriesColor = resolveColor(layoutContext.colorScheme, props.series[index].color) ?? 'var(--mdhui-color-primary-text)'; return diff --git a/src/components/presentational/Action/Action.css b/src/components/presentational/Action/Action.css index 39987a101..21085fd47 100644 --- a/src/components/presentational/Action/Action.css +++ b/src/components/presentational/Action/Action.css @@ -49,7 +49,7 @@ } .mdhui-action .mdhui-action-indicator-value { - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); font-weight: bold; } diff --git a/src/components/presentational/ActivityMeter/ActivityMeter.css b/src/components/presentational/ActivityMeter/ActivityMeter.css index 7dc84a90b..72b34b361 100644 --- a/src/components/presentational/ActivityMeter/ActivityMeter.css +++ b/src/components/presentational/ActivityMeter/ActivityMeter.css @@ -11,7 +11,7 @@ text-align: center; left: 16px; top: calc(50% - 8px); - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); font-size: 1em; } diff --git a/src/components/presentational/Button/Button.tsx b/src/components/presentational/Button/Button.tsx index 5a429c3a0..ece75c5dc 100644 --- a/src/components/presentational/Button/Button.tsx +++ b/src/components/presentational/Button/Button.tsx @@ -39,11 +39,11 @@ export default function Button(props: ButtonProps) { let backgroundColor = resolveColor(context?.colorScheme, props.color); let textColor = "#FFF"; if (props.variant === "subtle") { - textColor = resolveColor(context?.colorScheme, props.color) || "var(--mdhui-color-primary)"; + textColor = resolveColor(context?.colorScheme, props.color) || "var(--mdhui-color-primary-text)"; backgroundColor = "transparent"; } else if (props.variant === "light") { - textColor = resolveColor(context?.colorScheme, props.color) || "var(--mdhui-color-primary)"; + textColor = resolveColor(context?.colorScheme, props.color) || "var(--mdhui-color-primary-text)"; backgroundColor = "var(--mdhui-background-color-1)"; } diff --git a/src/components/presentational/Chat/Chat.css b/src/components/presentational/Chat/Chat.css index 5685812b4..6804d4dc9 100644 --- a/src/components/presentational/Chat/Chat.css +++ b/src/components/presentational/Chat/Chat.css @@ -108,7 +108,7 @@ } .mdhui-chat input:focus-visible { - outline: 1px solid var(--mdhui-color-primary); + outline: 1px solid var(--mdhui-color-primary-text); } .mdhui-chat .mdhui-chat-suggestions { diff --git a/src/components/presentational/DateRangeNavigator/DateRangeNavigator.css b/src/components/presentational/DateRangeNavigator/DateRangeNavigator.css index 8316c7317..c1e14d4e8 100644 --- a/src/components/presentational/DateRangeNavigator/DateRangeNavigator.css +++ b/src/components/presentational/DateRangeNavigator/DateRangeNavigator.css @@ -23,7 +23,7 @@ position: absolute; top: 0; padding: 16px; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); height: 100%; } @@ -53,7 +53,7 @@ width: 36px; height: 36px; background: var(--mdhui-background-color-0); - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); border-radius: 31px; cursor: pointer; position: absolute; diff --git a/src/components/presentational/DiscreteScale/DiscreteScale.tsx b/src/components/presentational/DiscreteScale/DiscreteScale.tsx index f46f513d6..e2ecb99a7 100644 --- a/src/components/presentational/DiscreteScale/DiscreteScale.tsx +++ b/src/components/presentational/DiscreteScale/DiscreteScale.tsx @@ -23,7 +23,7 @@ export default function (props: DiscreteScaleProps) { if (selectedValue !== undefined && selectedValue < 0) selectedValue = 0; if (selectedValue !== undefined && selectedValue >= props.tickCount) selectedValue = props.tickCount - 1; - let sliderColor = props.sliderColor ? resolveColor(layoutContext.colorScheme, props.sliderColor) : 'var(--mdhui-color-primary)'; + let sliderColor = props.sliderColor ? resolveColor(layoutContext.colorScheme, props.sliderColor) : 'var(--mdhui-color-primary-text)'; let sliderStop = selectedValue ? `calc((((100% - 16px) / ${(props.tickCount - 1)}) * ${selectedValue}) + 8px)` : '0%'; let sliderStyle = { background: `linear-gradient(to right, ${sliderColor} 0%, ${sliderColor} ${sliderStop}, var(--mdhui-background-color-2) ${sliderStop}, var(--mdhui-background-color-2) 100%)` diff --git a/src/components/presentational/DumbbellChart/DumbbellChart.css b/src/components/presentational/DumbbellChart/DumbbellChart.css index e049dbe2b..5b04bd502 100644 --- a/src/components/presentational/DumbbellChart/DumbbellChart.css +++ b/src/components/presentational/DumbbellChart/DumbbellChart.css @@ -70,8 +70,8 @@ .mdhui-dumbbell-visualization .mdhui-dumbbell-chart .mdhui-dumbbell .mdhui-dumbbell-full-out-of-range-pill{ - background-color: var(--mdhui-color-danger); - border-color:var(--mdhui-color-danger); + background-color: var(--mdhui-color-danger-text); + border-color:var(--mdhui-color-danger-text); position: absolute; width: 9px; height: 9px; @@ -126,12 +126,12 @@ .mdhui-dumbbell-visualization .mdhui-dumbbell-chart .mdhui-dumbbell .mdhui-dumbbell-in-range { - background-color: var(--mdhui-color-primary); - border-color: var(--mdhui-color-primary); + background-color: var(--mdhui-color-primary-text); + border-color: var(--mdhui-color-primary-text); } .mdhui-dumbbell-visualization .mdhui-dumbbell-chart .mdhui-dumbbell .mdhui-dumbbell-out-of-range { - background-color: var(--mdhui-color-danger); - border-color: var(--mdhui-color-danger); + background-color: var(--mdhui-color-danger-text); + border-color: var(--mdhui-color-danger-text); } \ No newline at end of file diff --git a/src/components/presentational/GlucoseStats/GlucoseStats.tsx b/src/components/presentational/GlucoseStats/GlucoseStats.tsx index 1ef766ac3..a301d8261 100644 --- a/src/components/presentational/GlucoseStats/GlucoseStats.tsx +++ b/src/components/presentational/GlucoseStats/GlucoseStats.tsx @@ -45,7 +45,7 @@ export default function (props: GlucoseStatsProps) { loading={props.loading} label={language(props.variant === 'minimal' ? 'glucose-stats-range-label-minimal' : 'glucose-stats-range-label')} icon={props.variant === 'minimal' ? undefined : faDroplet} - iconColor="#d36540" + iconColor="var(--mdhui-color-glucose-text)" value={(minGlucose && maxGlucose) ? `${formatNumberForLocale(minGlucose)} - ${formatNumberForLocale(maxGlucose)} mg/dL` : undefined} invert={props.variant === 'minimal'} /> @@ -53,7 +53,7 @@ export default function (props: GlucoseStatsProps) { loading={props.loading} label={language(props.variant === 'minimal' ? 'glucose-stats-avg-label-minimal' : 'glucose-stats-avg-label')} icon={props.variant === 'minimal' ? undefined : faDroplet} - iconColor="#d36540" + iconColor="var(--mdhui-color-glucose-text)" value={avgGlucose ? `${formatNumberForLocale(avgGlucose)} mg/dL` : undefined} invert={props.variant === 'minimal'} /> @@ -63,14 +63,14 @@ export default function (props: GlucoseStatsProps) { loading={props.loading} label={language('glucose-stats-steps-label')} icon={faShoePrints} - iconColor="#f5b722" + iconColor="var(--mdhui-color-activity-text)" value={steps ? `${formatNumberForLocale(steps)}` : undefined} /> diff --git a/src/components/presentational/Histogram/Histogram.css b/src/components/presentational/Histogram/Histogram.css index ba7e1c002..fc4eef77b 100644 --- a/src/components/presentational/Histogram/Histogram.css +++ b/src/components/presentational/Histogram/Histogram.css @@ -17,7 +17,7 @@ .mdhui-histogram .mdhui-histogram-entry.mdhui-histogram-entry-clickable .mdhui-histogram-entry-label { text-decoration: underline; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } .mdhui-histogram .mdhui-histogram-entry-bar-wrapper diff --git a/src/components/presentational/LabResultWithSparkline/LabResultWithSparkline.css b/src/components/presentational/LabResultWithSparkline/LabResultWithSparkline.css index 58fdcebbf..a423636b7 100644 --- a/src/components/presentational/LabResultWithSparkline/LabResultWithSparkline.css +++ b/src/components/presentational/LabResultWithSparkline/LabResultWithSparkline.css @@ -60,7 +60,7 @@ } .mdhui-lab-result-with-sparkline .mdhui-lab-result-with-sparkline-term-info { - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); font-size: 24px; opacity: .8; position: absolute; diff --git a/src/components/presentational/LabResultWithSparkline/LabResultWithSparkline.tsx b/src/components/presentational/LabResultWithSparkline/LabResultWithSparkline.tsx index aa03ee51c..231a87d61 100644 --- a/src/components/presentational/LabResultWithSparkline/LabResultWithSparkline.tsx +++ b/src/components/presentational/LabResultWithSparkline/LabResultWithSparkline.tsx @@ -75,7 +75,7 @@ export default function LabResultWithSparkline(props: LabResultWithSparklineProp y={props.labResultValue.NormalRangeBottomY * sparklineYRange} width={sparklineXRange} height={props.labResultValue.NormalRangeBottomY * sparklineYRange} - fill="var(--mdhui-color-primary)" /> + fill="var(--mdhui-color-primary-text)" /> } {props.labResultValue.SparklinePoints.slice(0, props.labResultValue.SparklinePoints.length - 1).map((point: any, index: number) => + fill="var(--mdhui-color-primary-text)" /> } {!!props.labResultValue.TermInformation && diff --git a/src/components/presentational/Layout/Layout.tsx b/src/components/presentational/Layout/Layout.tsx index 0a79e39e7..b689a7205 100644 --- a/src/components/presentational/Layout/Layout.tsx +++ b/src/components/presentational/Layout/Layout.tsx @@ -61,19 +61,23 @@ export default function Layout(props: LayoutProps) { return ( + {context.colorScheme == "dark" && + + } + {context.colorScheme == "light" && + + } + {/* after the scheme styles: both write --mdhui-color-primary at :root, so a + customer's color only survives if it lands last. -text carries it too, since + the scheme layer would otherwise pair a custom fill with the stock foreground. */} {props.primaryColor && } - {context.colorScheme == "dark" && - - } - {context.colorScheme == "light" && - - } {!props.noGlobalStyles && } diff --git a/src/components/presentational/MealAnalysis/MealAnalysis.css b/src/components/presentational/MealAnalysis/MealAnalysis.css index b9b15d729..274a434d8 100644 --- a/src/components/presentational/MealAnalysis/MealAnalysis.css +++ b/src/components/presentational/MealAnalysis/MealAnalysis.css @@ -12,7 +12,7 @@ } .mdhui-meal-analysis-title { - color: var(--mdhui-color-warning); + color: var(--mdhui-color-warning-text); font-weight: bold; font-size: 0.9em; flex: 1; @@ -46,7 +46,7 @@ } .mdhui-meal-analysis-item-add-icon { - color: var(--mdhui-color-success); + color: var(--mdhui-color-success-text); font-size: 1em; } diff --git a/src/components/presentational/NavigationBar/NavigationBar.css b/src/components/presentational/NavigationBar/NavigationBar.css index 46a51897f..1b4b2f023 100644 --- a/src/components/presentational/NavigationBar/NavigationBar.css +++ b/src/components/presentational/NavigationBar/NavigationBar.css @@ -53,7 +53,7 @@ font-weight: normal; font-size: .88em; cursor: pointer; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } .mdhui-navigation-bar .back-button { diff --git a/src/components/presentational/NotesInput/NotesInput.css b/src/components/presentational/NotesInput/NotesInput.css index 845916aec..e0084c90e 100644 --- a/src/components/presentational/NotesInput/NotesInput.css +++ b/src/components/presentational/NotesInput/NotesInput.css @@ -11,7 +11,7 @@ border-radius: var(--mdhui-card-border-radius); padding: 16px; box-sizing: border-box; - border: solid 1px var(--mdhui-color-primary); + border: solid 1px var(--mdhui-color-primary-text); overflow: hidden; -webkit-appearance: none; box-shadow: none !important; diff --git a/src/components/presentational/ProgressRing/ProgressRing.css b/src/components/presentational/ProgressRing/ProgressRing.css index 276af928f..650b0e062 100644 --- a/src/components/presentational/ProgressRing/ProgressRing.css +++ b/src/components/presentational/ProgressRing/ProgressRing.css @@ -26,7 +26,7 @@ } .mdhui-progress-ring > svg circle:nth-child(n+2) { - stroke: var(--mdhui-color-primary); + stroke: var(--mdhui-color-primary-text); animation-fill-mode: forwards; } diff --git a/src/components/presentational/Resource/Resource.css b/src/components/presentational/Resource/Resource.css index ea79a85ef..5c59669c4 100644 --- a/src/components/presentational/Resource/Resource.css +++ b/src/components/presentational/Resource/Resource.css @@ -34,7 +34,7 @@ } .mdhui-resource-link { - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); font-weight: 500; margin: 16px 0 0; } \ No newline at end of file diff --git a/src/components/presentational/SegmentedControl/SegmentedControl.css b/src/components/presentational/SegmentedControl/SegmentedControl.css index 14c5debe6..f48e4240c 100644 --- a/src/components/presentational/SegmentedControl/SegmentedControl.css +++ b/src/components/presentational/SegmentedControl/SegmentedControl.css @@ -1,6 +1,6 @@ .mdhui-segmented-control { border-radius: 32px; - border: solid 1px var(--mdhui-color-primary); + border: solid 1px var(--mdhui-color-primary-text); overflow: hidden; padding: 1px; background: var(--mdhui-background-color-highest-contrast); @@ -14,7 +14,7 @@ padding: 8px; box-sizing: border-box; font-weight: bold; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); position: relative; border-radius: 32px; overflow: hidden; @@ -55,7 +55,7 @@ .mdhui-segmented-control.mdhui-segmented-control-options-vertical .mdhui-segment { width: 100% !important; - border-bottom: solid 1px var(--mdhui-color-primary); + border-bottom: solid 1px var(--mdhui-color-primary-text); } .mdhui-segmented-control.mdhui-segmented-control-options-vertical .mdhui-segment:last-child { diff --git a/src/components/presentational/SingleDataPoint/SingleDataPoint.css b/src/components/presentational/SingleDataPoint/SingleDataPoint.css index eb9e3b5a5..83b47b21a 100644 --- a/src/components/presentational/SingleDataPoint/SingleDataPoint.css +++ b/src/components/presentational/SingleDataPoint/SingleDataPoint.css @@ -20,7 +20,7 @@ .mdhui-single-data-point-setup { font-size: 1.3em; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } .mdhui-single-data-point-value { diff --git a/src/components/presentational/SingleExternalAccount/SingleExternalAccount.css b/src/components/presentational/SingleExternalAccount/SingleExternalAccount.css index 5323466e1..613bec34a 100644 --- a/src/components/presentational/SingleExternalAccount/SingleExternalAccount.css +++ b/src/components/presentational/SingleExternalAccount/SingleExternalAccount.css @@ -56,5 +56,5 @@ } .mdhui-single-external-account .external-account-status .error { - color: var(--mdhui-color-danger); + color: var(--mdhui-color-danger-text); } \ No newline at end of file diff --git a/src/components/presentational/SingleMeal/SingleMeal.tsx b/src/components/presentational/SingleMeal/SingleMeal.tsx index 0e39a7e97..5110ce344 100644 --- a/src/components/presentational/SingleMeal/SingleMeal.tsx +++ b/src/components/presentational/SingleMeal/SingleMeal.tsx @@ -76,7 +76,7 @@ export default function (props: SingleMealProps) {
{getMealTypeDisplayText(props.meal.type)}  - {props.selected && } + {props.selected && }
{props.displayDateWithTime ? getDateAndTimeString(props.meal.timestamp) : getTimeOfDayString(props.meal.timestamp)} diff --git a/src/components/presentational/SingleSurveyTask/SingleSurveyTask.css b/src/components/presentational/SingleSurveyTask/SingleSurveyTask.css index 76f8a503a..6927017f2 100644 --- a/src/components/presentational/SingleSurveyTask/SingleSurveyTask.css +++ b/src/components/presentational/SingleSurveyTask/SingleSurveyTask.css @@ -44,15 +44,15 @@ .mdhui-single-survey-task .due-date { font-size: .88em; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } .mdhui-single-survey-task .due-date.warning { - color: var(--mdhui-color-warning); + color: var(--mdhui-color-warning-text); font-weight: bold; } .mdhui-single-survey-task .due-date.danger { - color: var(--mdhui-color-danger); + color: var(--mdhui-color-danger-text); font-weight: bold; } \ No newline at end of file diff --git a/src/components/presentational/TermInformationButton/TermInformationButton.tsx b/src/components/presentational/TermInformationButton/TermInformationButton.tsx index 44c0c8059..9c46e0de9 100644 --- a/src/components/presentational/TermInformationButton/TermInformationButton.tsx +++ b/src/components/presentational/TermInformationButton/TermInformationButton.tsx @@ -43,7 +43,7 @@ export default function TermInformationButton(props: TermInformationButtonProps) return
{(props.termInformation || props.labObservationID) && - + }
; diff --git a/src/components/presentational/ValueSelector/ValueSelector.css b/src/components/presentational/ValueSelector/ValueSelector.css index 1320ab932..acadcf68e 100644 --- a/src/components/presentational/ValueSelector/ValueSelector.css +++ b/src/components/presentational/ValueSelector/ValueSelector.css @@ -6,7 +6,7 @@ --mdhui-value-selector-button-value-selected-background-color: var(--mdhui-color-primary); --mdhui-value-selector-button-value-selected-text-color: #fff; --mdhui-value-selector-checkbox-color: var(--mdhui-text-color-4); - --mdhui-value-selector-checkbox-selected-color: var(--mdhui-color-primary); + --mdhui-value-selector-checkbox-selected-color: var(--mdhui-color-primary-text); padding: 16px; } diff --git a/src/components/step/StepLayout/StepLayout.css b/src/components/step/StepLayout/StepLayout.css index 65cb87249..168f47b85 100644 --- a/src/components/step/StepLayout/StepLayout.css +++ b/src/components/step/StepLayout/StepLayout.css @@ -29,7 +29,7 @@ } .mdhui-step-container a { - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); text-decoration: none; } diff --git a/src/components/symptom-shark/container/ReportBuilder/ReportBuilder.css b/src/components/symptom-shark/container/ReportBuilder/ReportBuilder.css index 58956ed81..e789ade80 100644 --- a/src/components/symptom-shark/container/ReportBuilder/ReportBuilder.css +++ b/src/components/symptom-shark/container/ReportBuilder/ReportBuilder.css @@ -37,7 +37,7 @@ } .ss-report-builder .ss-report-builder-items .ss-report-builder-item .fa-circle-check { - color: var(--mdhui-color-success); + color: var(--mdhui-color-success-text); } .ss-report-builder .ss-report-builder-items .ss-report-builder-item.ss-report-builder-month-selector { diff --git a/src/components/symptom-shark/presentational/Calendar/Calendar.css b/src/components/symptom-shark/presentational/Calendar/Calendar.css index abbd77762..504447d72 100644 --- a/src/components/symptom-shark/presentational/Calendar/Calendar.css +++ b/src/components/symptom-shark/presentational/Calendar/Calendar.css @@ -16,7 +16,7 @@ } .mdhui-ss-calendar .mdhui-ss-notes-icon { - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); position: absolute; right: calc(50% - 19px); bottom: 2px; diff --git a/src/components/symptom-shark/presentational/NotesTimeline/NotesTimeline.css b/src/components/symptom-shark/presentational/NotesTimeline/NotesTimeline.css index 70e241d0c..3dac7018e 100644 --- a/src/components/symptom-shark/presentational/NotesTimeline/NotesTimeline.css +++ b/src/components/symptom-shark/presentational/NotesTimeline/NotesTimeline.css @@ -59,7 +59,7 @@ /* Default color for PDF Rendering, since pdf renderer does not support CSS variables */ color: #429bdf; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } .notes-timeline .ss-notes-timeline-day .ss-notes-timeline-day-title { diff --git a/src/components/view/DocumentDetailView/DocumentDetailView.css b/src/components/view/DocumentDetailView/DocumentDetailView.css index 32c6ec561..b255f3d2b 100644 --- a/src/components/view/DocumentDetailView/DocumentDetailView.css +++ b/src/components/view/DocumentDetailView/DocumentDetailView.css @@ -16,7 +16,7 @@ body:has(.mdhui-document-detail-view) { display: flex; align-items: center; justify-content: center; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); font-size: 1.2em; cursor: pointer; } diff --git a/src/components/view/DocumentDetailView/DocumentDetailView.tsx b/src/components/view/DocumentDetailView/DocumentDetailView.tsx index b6c9d2657..edc19db9d 100644 --- a/src/components/view/DocumentDetailView/DocumentDetailView.tsx +++ b/src/components/view/DocumentDetailView/DocumentDetailView.tsx @@ -137,7 +137,7 @@ export default function DocumentDetailView(props: DocumentDetailViewProps) { }
-
diff --git a/src/components/view/NewPointsView/NewPointsView.css b/src/components/view/NewPointsView/NewPointsView.css index d95d79239..8c7d7555f 100644 --- a/src/components/view/NewPointsView/NewPointsView.css +++ b/src/components/view/NewPointsView/NewPointsView.css @@ -12,7 +12,7 @@ font-size: 40px; height: 50px; line-height: 50px; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } .mdhui-new-points-content { @@ -53,7 +53,7 @@ .mdhui-new-points-entry-points { width: 50px; - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); font-size: 1.1em; font-weight: bold; text-align: end; @@ -65,7 +65,7 @@ } .mdhui-new-points-next-reward-points { - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); font-weight: bold; } diff --git a/src/helpers/chartHelpers.tsx b/src/helpers/chartHelpers.tsx index 6aeace1e7..5aff160f8 100644 --- a/src/helpers/chartHelpers.tsx +++ b/src/helpers/chartHelpers.tsx @@ -55,7 +55,7 @@ export function createLineChartDefs( return {series.map((s, i) => { - let lineColor = colorOrDefault(s.color, "var(--mdhui-color-primary"); + let lineColor = colorOrDefault(s.color, "var(--mdhui-color-primary-text"); return {createStopsFromThresholds(lineColor, options?.thresholds, dataKeys[i])} @@ -73,14 +73,14 @@ export function createBarChartDefs(layoutContext: LayoutContext, gradientKey: st return {series.map((s, i) => - - + + )} {options?.thresholds?.map((threshold, index) => - - + + )} ; @@ -96,12 +96,12 @@ export function createAreaChartDefs(layoutContext: LayoutContext, gradientKey: s {series.map((s, i) => <> - - + + - - + + )} diff --git a/src/helpers/globalCss.ts b/src/helpers/globalCss.ts index 2a8da6b7e..fae7329d6 100644 --- a/src/helpers/globalCss.ts +++ b/src/helpers/globalCss.ts @@ -1,5 +1,6 @@ import { css, CSSObject } from '@emotion/react'; import { CSSProperties } from 'react'; +import { legacyCeColors } from './legacyCeColors'; export const core = css` :root { @@ -7,6 +8,14 @@ export const core = css` --mdhui-color-success: #00AE42; --mdhui-color-warning: rgb(232, 124, 0); --mdhui-color-danger: rgb(239, 55, 36); + /* Text tokens (mapping table): the accents as FOREGROUND (links, icons, headings, + status text). Default to the base accents, so light mode is unchanged; dark + overrides them to the grade-40 ladder. Base tokens keep the FILL duty (buttons, + pills, chips) — white-text elements keep the light-mode value in dark. */ + --mdhui-color-primary-text: var(--mdhui-color-primary); + --mdhui-color-success-text: var(--mdhui-color-success); + --mdhui-color-warning-text: var(--mdhui-color-warning); + --mdhui-color-danger-text: var(--mdhui-color-danger); --mdhui-fadein-animation: fadein 1s; --mdhui-card-border-radius: 12px; --mdhui-font-family: ui-rounded, -apple-system, Helvetica, Arial, sans-serif; @@ -57,260 +66,535 @@ export const core = css` :root { /* BASE-COLORS */ - --ce-grey-1: rgb(251, 252, 252); - --ce-grey-2: rgb(247, 248, 249); - --ce-grey-3: rgb(244, 245, 246); - --ce-grey-4: rgb(242, 243, 245); - --ce-grey-5: rgb(237, 238, 240); - --ce-grey-10: rgb(224, 225, 230); - --ce-grey-20: rgb(200, 201, 210); - --ce-grey-30: rgb(172, 173, 186); - --ce-grey-40: rgb(143, 144, 162); - --ce-grey-50: rgb(115, 116, 139); - --ce-grey-55: rgb(107, 107, 129); - --ce-grey-60: rgb(90, 90, 108); - --ce-grey-70: rgb(68, 67, 81); - --ce-grey-80: rgb(46, 45, 53); - --ce-grey-90: rgb(29, 28, 34); - --ce-grey-95: rgb(27, 26, 31); - --ce-grey-96: rgb(25, 25, 29); - --ce-grey-97: rgb(22, 21, 25); - --ce-grey-98: rgb(20, 19, 23); - --ce-grey-99: rgb(17, 16, 19); - - --ce-yellow-1: rgb(255, 252, 230); - --ce-yellow-2: rgb(255, 249, 207); - --ce-yellow-3: rgb(255, 247, 195); - --ce-yellow-4: rgb(255, 245, 183); - --ce-yellow-5: rgb(255, 240, 148); - --ce-yellow-10: rgb(255, 224, 43); - --ce-yellow-20: rgb(246, 196, 0); - --ce-yellow-30: rgb(211, 168, 0); - --ce-yellow-40: rgb(177, 141, 0); - --ce-yellow-50: rgb(144, 114, 0); - --ce-yellow-55: rgb(134, 105, 0); - --ce-yellow-60: rgb(112, 89, 0); - --ce-yellow-70: rgb(85, 67, 0); - --ce-yellow-80: rgb(57, 44, 0); - --ce-yellow-90: rgb(37, 28, 0); - --ce-yellow-95: rgb(34, 26, 0); - --ce-yellow-96: rgb(31, 24, 0); - --ce-yellow-97: rgb(28, 21, 0); - --ce-yellow-98: rgb(25, 20, 0); - --ce-yellow-99: rgb(21, 17, 0); - - --ce-orange-1: rgb(255, 251, 246); - --ce-orange-2: rgb(255, 246, 237); - --ce-orange-3: rgb(255, 244, 232); - --ce-orange-4: rgb(255, 241, 227); - --ce-orange-5: rgb(255, 236, 217); - --ce-orange-10: rgb(255, 219, 184); - --ce-orange-20: rgb(255, 188, 122); - --ce-orange-30: rgb(255, 146, 41); - --ce-orange-40: rgb(236, 108, 5); - --ce-orange-45: #E35C33; - --ce-orange-50: rgb(191, 87, 4); - --ce-orange-55: rgb(177, 81, 3); - --ce-orange-60: rgb(149, 68, 3); - --ce-orange-70: rgb(112, 51, 2); - --ce-orange-80: rgb(76, 34, 1); - --ce-orange-90: rgb(49, 22, 0); - --ce-orange-95: rgb(46, 20, 0); - --ce-orange-96: rgb(42, 19, 0); - --ce-orange-97: rgb(38, 17, 0); - --ce-orange-98: rgb(31, 14, 0); - --ce-orange-99: rgb(29, 13, 0); - - --ce-red-1: rgb(255, 250, 250); - --ce-red-2: rgb(254, 245, 245); - --ce-red-3: rgb(253, 243, 244); - --ce-red-4: rgb(253, 239, 240); - --ce-red-5: rgb(252, 234, 234); - --ce-red-10: rgb(249, 217, 218); - --ce-red-20: rgb(244, 187, 189); - --ce-red-30: rgb(237, 150, 153); - --ce-red-40: rgb(230, 107, 111); - --ce-red-50: rgb(220, 52, 58); - --ce-red-55: rgb(206, 44, 50); - --ce-red-60: rgb(174, 37, 42); - --ce-red-70: rgb(132, 28, 32); - --ce-red-80: rgb(89, 20, 22); - --ce-red-90: rgb(58, 13, 14); - --ce-red-95: rgb(54, 12, 13); - --ce-red-96: rgb(51, 11, 12); - --ce-red-97: rgb(45, 10, 11); - --ce-red-98: rgb(41, 9, 10); - --ce-red-99: rgb(35, 8, 9); - - --ce-red-vivid-1: rgb(255, 250, 250); - --ce-red-vivid-2: rgb(255, 246, 247); - --ce-red-vivid-3: rgb(254, 243, 243); - --ce-red-vivid-4: rgb(254, 240, 240); - --ce-red-vivid-5: rgb(254, 233, 233); - --ce-red-vivid-10: rgb(252, 215, 217); - --ce-red-vivid-20: rgb(250, 186, 188); - --ce-red-vivid-30: rgb(247, 145, 148); - --ce-red-vivid-40: rgb(243, 97, 102); - --ce-red-vivid-50: rgb(236, 6, 14); - --ce-red-vivid-55: rgb(220, 0, 7); - --ce-red-vivid-60: rgb(187, 0, 6); - --ce-red-vivid-70: rgb(143, 0, 4); - --ce-red-vivid-80: rgb(100, 0, 3); - --ce-red-vivid-90: rgb(67, 0, 2); - --ce-red-vivid-95: rgb(64, 0, 2); - --ce-red-vivid-96: rgb(60, 0, 2); - --ce-red-vivid-97: rgb(54, 0, 2); - --ce-red-vivid-98: rgb(50, 0, 2); - --ce-red-vivid-99: rgb(45, 0, 1); - - --ce-violet-1: rgb(254, 251, 255); - --ce-violet-2: rgb(252, 246, 255); - --ce-violet-3: rgb(252, 243, 255); - --ce-violet-4: rgb(250, 238, 255); - --ce-violet-5: rgb(249, 233, 255); - --ce-violet-10: rgb(244, 214, 255); - --ce-violet-20: rgb(235, 183, 255); - --ce-violet-30: rgb(224, 141, 255); - --ce-violet-40: rgb(207, 94, 255); - --ce-violet-50: rgb(174, 50, 246); - --ce-violet-55: rgb(162, 42, 233); - --ce-violet-60: rgb(137, 35, 197); - --ce-violet-70: rgb(104, 25, 150); - --ce-violet-80: rgb(71, 16, 104); - --ce-violet-90: rgb(47, 9, 69); - --ce-violet-95: rgb(44, 8, 65); - --ce-violet-96: rgb(41, 8, 60); - --ce-violet-97: rgb(36, 7, 54); - --ce-violet-98: rgb(34, 6, 50); - --ce-violet-99: rgb(29, 6, 42); - - --ce-blue-1: rgb(250, 252, 253); - --ce-blue-2: rgb(244, 249, 252); - --ce-blue-3: rgb(238, 246, 250); - --ce-blue-4: rgb(235, 244, 249); - --ce-blue-5: rgb(230, 241, 248); - --ce-blue-10: rgb(206, 227, 242); - --ce-blue-20: rgb(170, 206, 232); - --ce-blue-30: rgb(125, 180, 220); - --ce-blue-40: rgb(76, 152, 207); - --ce-blue-50: rgb(24, 122, 193); - --ce-blue-55: rgb(11, 112, 186); - --ce-blue-60: rgb(9, 94, 159); - --ce-blue-70: rgb(5, 70, 122); - --ce-blue-80: rgb(2, 47, 86); - --ce-blue-90: rgb(0, 29, 60); - --ce-blue-95: rgb(0, 27, 56); - --ce-blue-96: rgb(0, 25, 52); - --ce-blue-97: rgb(0, 23, 46); - --ce-blue-98: rgb(0, 20, 42); - --ce-blue-99: rgb(0, 18, 36); - - --ce-blue-vivid-1: rgb(250, 252, 255); - --ce-blue-vivid-2: rgb(244, 248, 255); - --ce-blue-vivid-3: rgb(241, 246, 255); - --ce-blue-vivid-4: rgb(235, 243, 254); - --ce-blue-vivid-5: rgb(229, 239, 254); - --ce-blue-vivid-10: rgb(209, 226, 253); - --ce-blue-vivid-20: rgb(205, 227, 255); - --ce-blue-vivid-30: rgb(125, 175, 250); - --ce-blue-vivid-40: rgb(76, 145, 248); - --ce-blue-vivid-50: rgb(20, 110, 246); - --ce-blue-vivid-55: rgb(12, 100, 235); - --ce-blue-vivid-60: rgb(9, 84, 200); - --ce-blue-vivid-70: rgb(6, 62, 152); - --ce-blue-vivid-80: rgb(3, 42, 106); - --ce-blue-vivid-90: rgb(0, 26, 72); - --ce-blue-vivid-95: rgb(0, 24, 67); - --ce-blue-vivid-96: rgb(0, 23, 63); - --ce-blue-vivid-97: rgb(0, 20, 56); - --ce-blue-vivid-98: rgb(0, 18, 50); - --ce-blue-vivid-99: rgb(0, 16, 45); - - --ce-indigo-1: rgb(251, 252, 255); - --ce-indigo-2: rgb(247, 248, 255); - --ce-indigo-3: rgb(244, 245, 255); - --ce-indigo-4: rgb(241, 241, 255); - --ce-indigo-5: rgb(237, 237, 255); - --ce-indigo-10: rgb(222, 221, 255); - --ce-indigo-20: rgb(200, 196, 253); - --ce-indigo-30: rgb(172, 163, 250); - --ce-indigo-40: rgb(143, 130, 246); - --ce-indigo-50: rgb(112, 94, 243); - --ce-indigo-55: rgb(102, 83, 240); - --ce-indigo-60: rgb(83, 61, 224); - --ce-indigo-70: rgb(54, 28, 201); - --ce-indigo-80: rgb(36, 16, 142); - --ce-indigo-90: rgb(22, 8, 98); - --ce-indigo-95: rgb(21, 7, 92); - --ce-indigo-96: rgb(19, 7, 86); - --ce-indigo-97: rgb(17, 6, 78); - --ce-indigo-98: rgb(16, 6, 71); - --ce-indigo-99: rgb(14, 5, 63); - - --ce-cyan-1: rgb(247, 253, 254); - --ce-cyan-2: rgb(237, 251, 253); - --ce-cyan-3: rgb(228, 249, 252); - --ce-cyan-4: rgb(219, 247, 251); - --ce-cyan-5: rgb(209, 245, 250); - --ce-cyan-10: rgb(168, 235, 246); - --ce-cyan-20: rgb(93, 218, 239); - --ce-cyan-30: rgb(63, 188, 212); - --ce-cyan-40: rgb(34, 159, 185); - --ce-cyan-50: rgb(4, 128, 158); - --ce-cyan-55: rgb(10, 118, 148); - --ce-cyan-60: rgb(10, 99, 124); - --ce-cyan-70: rgb(9, 74, 94); - --ce-cyan-80: rgb(8, 50, 65); - --ce-cyan-90: rgb(8, 31, 42); - --ce-cyan-95: rgb(8, 29, 39); - --ce-cyan-96: rgb(7, 27, 36); - --ce-cyan-97: rgb(6, 24, 32); - --ce-cyan-98: rgb(6, 22, 28); - --ce-cyan-99: rgb(5, 18, 24); - - --ce-green-1: rgb(248, 253, 251); - --ce-green-2: rgb(241, 251, 246); - --ce-green-3: rgb(234, 249, 242); - --ce-green-4: rgb(230, 247, 240); - --ce-green-5: rgb(219, 244, 233); - --ce-green-10: rgb(189, 234, 215); - --ce-green-20: rgb(138, 217, 184); - --ce-green-30: rgb(69, 194, 143); - --ce-green-40: rgb(35, 164, 130); - --ce-green-50: rgb(1, 132, 116); - --ce-green-55: rgb(0, 123, 108); - --ce-green-60: rgb(0, 103, 91); - --ce-green-70: rgb(0, 78, 69); - --ce-green-80: rgb(0, 52, 47); - --ce-green-90: rgb(0, 33, 30); - --ce-green-95: rgb(0, 31, 29); - --ce-green-96: rgb(0, 29, 27); - --ce-green-97: rgb(0, 26, 23); - --ce-green-98: rgb(0, 23, 21); - --ce-green-99: rgb(0, 20, 19); + --mdh-grey-0: #FFFFFF; + --mdh-grey-1: #FCFCFC; + --mdh-grey-2: #F8F8F8; + --mdh-grey-3: #F4F5F6; + --mdh-grey-4: #F2F3F4; + --mdh-grey-5: #EEEFF1; + --mdh-grey-10: #E0E2E5; + --mdh-grey-20: #C7CBD1; + --mdh-grey-30: #A9AFB7; + --mdh-grey-35: #9AA1AA; + --mdh-grey-40: #8B939D; + --mdh-grey-50: #6E7683; + --mdh-grey-55: #636C78; + --mdh-grey-60: #545C69; + --mdh-grey-70: #3D4551; + --mdh-grey-80: #262E3A; + --mdh-grey-85: #1D2530; + --mdh-grey-90: #151D28; + --mdh-grey-95: #0B131D; + --mdh-grey-99: #050A14; + --mdh-blue-grey-1: #FBFCFD; + --mdh-blue-grey-2: #F6F8FB; + --mdh-blue-grey-3: #F2F5FA; + --mdh-blue-grey-4: #EFF4F9; + --mdh-blue-grey-5: #E8EFF7; + --mdh-blue-grey-10: #D4E3F2; + --mdh-blue-grey-20: #BAD2EB; + --mdh-blue-grey-30: #96BBE1; + --mdh-blue-grey-35: #7BA9D8; + --mdh-blue-grey-40: #6098D2; + --mdh-blue-grey-50: #4A79AA; + --mdh-blue-grey-55: #436F9B; + --mdh-blue-grey-60: #3D6690; + --mdh-blue-grey-70: #35577B; + --mdh-blue-grey-80: #2A435E; + --mdh-blue-grey-85: #1E3751; + --mdh-blue-grey-90: #1B2C3E; + --mdh-blue-grey-95: #142230; + --mdh-blue-grey-99: #16222F; + --mdh-blue-1: #FAFCFF; + --mdh-blue-2: #F5F9FE; + --mdh-blue-3: #F0F6FF; + --mdh-blue-4: #EBF3FF; + --mdh-blue-5: #E4EFFF; + --mdh-blue-10: #CFE3FF; + --mdh-blue-20: #B1D1FF; + --mdh-blue-30: #88B9FE; + --mdh-blue-35: #69A7FE; + --mdh-blue-40: #4795FE; + --mdh-blue-50: #0271E8; + --mdh-blue-55: #0268D6; + --mdh-blue-60: #0260C7; + --mdh-blue-70: #0352AB; + --mdh-blue-80: #033F85; + --mdh-blue-85: #04336C; + --mdh-blue-90: #022859; + --mdh-blue-95: #021F48; + --mdh-blue-99: #021F46; + --mdh-cobalt-1: #FBFCFF; + --mdh-cobalt-2: #F7F9FE; + --mdh-cobalt-3: #F1F5FF; + --mdh-cobalt-4: #EEF3FF; + --mdh-cobalt-5: #E8EEFF; + --mdh-cobalt-10: #D6E1FF; + --mdh-cobalt-20: #BCCEFF; + --mdh-cobalt-30: #9BB5FE; + --mdh-cobalt-35: #82A1FE; + --mdh-cobalt-40: #6C8FFE; + --mdh-cobalt-50: #4667FE; + --mdh-cobalt-55: #3A57FC; + --mdh-cobalt-60: #334AFE; + --mdh-cobalt-70: #2A29FC; + --mdh-cobalt-80: #200ED4; + --mdh-cobalt-85: #181AA4; + --mdh-cobalt-90: #130992; + --mdh-cobalt-95: #0D0676; + --mdh-cobalt-99: #0D0974; + --mdh-indigo-1: #FBFCFF; + --mdh-indigo-2: #F8F9FE; + --mdh-indigo-3: #F3F5FF; + --mdh-indigo-4: #F0F2FF; + --mdh-indigo-5: #EBEEFF; + --mdh-indigo-10: #DBE0FF; + --mdh-indigo-20: #C4CCFF; + --mdh-indigo-30: #A8B2FE; + --mdh-indigo-35: #949DFF; + --mdh-indigo-40: #8289FE; + --mdh-indigo-50: #6560FE; + --mdh-indigo-55: #5C4EFC; + --mdh-indigo-60: #573FFE; + --mdh-indigo-70: #4F11FC; + --mdh-indigo-80: #3C0CC7; + --mdh-indigo-85: #2F189A; + --mdh-indigo-90: #270889; + --mdh-indigo-95: #1D056F; + --mdh-indigo-99: #1D086D; + --mdh-iris-1: #FBFBFF; + --mdh-iris-2: #F8F8FE; + --mdh-iris-3: #F4F4FF; + --mdh-iris-4: #F3F2FF; + --mdh-iris-5: #EDEDFF; + --mdh-iris-10: #E0DEFF; + --mdh-iris-20: #CEC9FF; + --mdh-iris-30: #B6AEFE; + --mdh-iris-35: #A497FE; + --mdh-iris-40: #9783FE; + --mdh-iris-50: #7F56FE; + --mdh-iris-55: #7741FC; + --mdh-iris-60: #732DFE; + --mdh-iris-70: #670AEC; + --mdh-iris-80: #4F0BB9; + --mdh-iris-85: #3E1690; + --mdh-iris-90: #34077E; + --mdh-iris-95: #280566; + --mdh-iris-99: #280765; + --mdh-violet-1: #FCFBFF; + --mdh-violet-2: #FAF8FE; + --mdh-violet-3: #F6F3FF; + --mdh-violet-4: #F5F1FF; + --mdh-violet-5: #F1ECFF; + --mdh-violet-10: #E6DCFF; + --mdh-violet-20: #D7C6FF; + --mdh-violet-30: #C4A9FE; + --mdh-violet-35: #B692FC; + --mdh-violet-40: #AB7BFE; + --mdh-violet-50: #9747FE; + --mdh-violet-55: #902CFC; + --mdh-violet-60: #8C08FC; + --mdh-violet-70: #7809D8; + --mdh-violet-80: #5D0AA9; + --mdh-violet-85: #491484; + --mdh-violet-90: #3E0674; + --mdh-violet-95: #31045D; + --mdh-violet-99: #31065C; + --mdh-purple-1: #FDFBFF; + --mdh-purple-2: #FCF8FE; + --mdh-purple-3: #F9F2FF; + --mdh-purple-4: #F8F0FF; + --mdh-purple-5: #F6EAFF; + --mdh-purple-10: #EFD9FF; + --mdh-purple-20: #E6C1FF; + --mdh-purple-30: #D9A0FE; + --mdh-purple-35: #D085FE; + --mdh-purple-40: #C86BFE; + --mdh-purple-50: #B918FE; + --mdh-purple-55: #AB07EB; + --mdh-purple-60: #9F06DC; + --mdh-purple-70: #8907BD; + --mdh-purple-80: #6A0894; + --mdh-purple-85: #531373; + --mdh-purple-90: #470564; + --mdh-purple-95: #390351; + --mdh-purple-99: #390550; + --mdh-magenta-1: #FDFBFF; + --mdh-magenta-2: #FBF8FF; + --mdh-magenta-3: #F9F3FF; + --mdh-magenta-4: #F7F0FF; + --mdh-magenta-5: #F5EAFF; + --mdh-magenta-10: #EFD9FF; + --mdh-magenta-20: #EABFFF; + --mdh-magenta-30: #E999FF; + --mdh-magenta-35: #E677FE; + --mdh-magenta-40: #E84DFF; + --mdh-magenta-50: #CF00D7; + --mdh-magenta-55: #BD00C5; + --mdh-magenta-60: #B000B7; + --mdh-magenta-70: #98009E; + --mdh-magenta-80: #77007C; + --mdh-magenta-85: #5D1161; + --mdh-magenta-90: #510055; + --mdh-magenta-95: #410044; + --mdh-magenta-99: #410144; + --mdh-fuchsia-1: #FEFBFF; + --mdh-fuchsia-2: #FEF7FE; + --mdh-fuchsia-3: #FEF1FF; + --mdh-fuchsia-4: #FEEEFF; + --mdh-fuchsia-5: #FEE7FF; + --mdh-fuchsia-10: #FFD4FD; + --mdh-fuchsia-20: #FFB8F7; + --mdh-fuchsia-30: #FE90EC; + --mdh-fuchsia-35: #FE6DE5; + --mdh-fuchsia-40: #FE3FDE; + --mdh-fuchsia-50: #DA08B2; + --mdh-fuchsia-55: #C705A2; + --mdh-fuchsia-60: #B90597; + --mdh-fuchsia-70: #9F0682; + --mdh-fuchsia-80: #7C0765; + --mdh-fuchsia-85: #621150; + --mdh-fuchsia-90: #540444; + --mdh-fuchsia-95: #440336; + --mdh-fuchsia-99: #420435; + --mdh-pink-1: #FFFBFD; + --mdh-pink-2: #FFF7FB; + --mdh-pink-3: #FFF1F9; + --mdh-pink-4: #FFEEF8; + --mdh-pink-5: #FFE8F5; + --mdh-pink-10: #FFD6EC; + --mdh-pink-20: #FFBBDD; + --mdh-pink-30: #FF96C8; + --mdh-pink-35: #FE76B8; + --mdh-pink-40: #FF50A9; + --mdh-pink-50: #E50084; + --mdh-pink-55: #D00078; + --mdh-pink-60: #C30070; + --mdh-pink-70: #A8005F; + --mdh-pink-80: #84004A; + --mdh-pink-85: #680F3C; + --mdh-pink-90: #5A0031; + --mdh-pink-95: #490026; + --mdh-pink-99: #490026; + --mdh-red-1: #FFFBFC; + --mdh-red-2: #FFF7F9; + --mdh-red-3: #FFF2F6; + --mdh-red-4: #FFEFF3; + --mdh-red-5: #FFE9EE; + --mdh-red-10: #FFD7E1; + --mdh-red-20: #FFBECA; + --mdh-red-30: #FF9AAA; + --mdh-red-35: #FD7C90; + --mdh-red-40: #FF5A76; + --mdh-red-50: #EC0046; + --mdh-red-55: #D8003F; + --mdh-red-60: #C9003A; + --mdh-red-70: #AD0030; + --mdh-red-80: #880024; + --mdh-red-85: #6B1020; + --mdh-red-90: #5D0016; + --mdh-red-95: #4B000F; + --mdh-red-99: #4B0010; + --mdh-red-muted-1: #FEFBFB; + --mdh-red-muted-2: #FEF8F8; + --mdh-red-muted-3: #FDF3F3; + --mdh-red-muted-4: #FDF0F0; + --mdh-red-muted-5: #FCEAEA; + --mdh-red-muted-10: #FBDAD9; + --mdh-red-muted-20: #F8C1C2; + --mdh-red-muted-30: #F5A0A2; + --mdh-red-muted-35: #F18589; + --mdh-red-muted-40: #F16772; + --mdh-red-muted-50: #DB364F; + --mdh-red-muted-55: #C73046; + --mdh-red-muted-60: #BA2C41; + --mdh-red-muted-70: #A02638; + --mdh-red-muted-80: #7D1D2A; + --mdh-red-muted-85: #6B1120; + --mdh-red-muted-90: #55111A; + --mdh-red-muted-95: #440C13; + --mdh-red-muted-99: #430C13; + --mdh-blue-muted-1: #F9FCFD; + --mdh-blue-muted-2: #F6FAFD; + --mdh-blue-muted-3: #F0F7FB; + --mdh-blue-muted-4: #EAF3FA; + --mdh-blue-muted-5: #E4F0F8; + --mdh-blue-muted-10: #CEE4F4; + --mdh-blue-muted-20: #A8CFEC; + --mdh-blue-muted-30: #75B4E1; + --mdh-blue-muted-35: #58A7DF; + --mdh-blue-muted-40: #4698D3; + --mdh-blue-muted-50: #187AC1; + --mdh-blue-muted-55: #0A6FB9; + --mdh-blue-muted-60: #005DA6; + --mdh-blue-muted-70: #004483; + --mdh-blue-muted-80: #002E5B; + --mdh-blue-muted-85: #042547; + --mdh-blue-muted-90: #001D3B; + --mdh-blue-muted-95: #001B38; + --mdh-blue-muted-99: #001224; + --mdh-red-orange-1: #FFFBFA; + --mdh-red-orange-2: #FFF8F6; + --mdh-red-orange-3: #FFF2EE; + --mdh-red-orange-4: #FFEFEB; + --mdh-red-orange-5: #FFEAE3; + --mdh-red-orange-10: #FFD9CE; + --mdh-red-orange-20: #FFC0AE; + --mdh-red-orange-30: #FF9E82; + --mdh-red-orange-35: #FE7F5B; + --mdh-red-orange-40: #FF6030; + --mdh-red-orange-50: #DC3800; + --mdh-red-orange-55: #C63800; + --mdh-red-orange-60: #B93400; + --mdh-red-orange-70: #9E2B00; + --mdh-red-orange-80: #7D2000; + --mdh-red-orange-85: #631E08; + --mdh-red-orange-90: #551300; + --mdh-red-orange-95: #430D00; + --mdh-red-orange-99: #420D00; + --mdh-orange-1: #FFFBF8; + --mdh-orange-2: #FFF8F2; + --mdh-orange-3: #FFF3E8; + --mdh-orange-4: #FFF0E2; + --mdh-orange-5: #FFEBD8; + --mdh-orange-10: #FFDBBC; + --mdh-orange-20: #FFC294; + --mdh-orange-30: #FFA05D; + --mdh-orange-35: #FE831A; + --mdh-orange-40: #EC7200; + --mdh-orange-50: #C45500; + --mdh-orange-55: #B44D00; + --mdh-orange-60: #A74700; + --mdh-orange-70: #8F3C00; + --mdh-orange-80: #6F2D00; + --mdh-orange-85: #592607; + --mdh-orange-90: #4C1C00; + --mdh-orange-95: #3D1500; + --mdh-orange-99: #3D1600; + --mdh-amber-1: #FFFCF7; + --mdh-amber-2: #FEF9ED; + --mdh-amber-3: #FFF4DE; + --mdh-amber-4: #FFF2D9; + --mdh-amber-5: #FFECC9; + --mdh-amber-10: #FFDDA1; + --mdh-amber-20: #FFC568; + --mdh-amber-30: #F8A60B; + --mdh-amber-35: #E39404; + --mdh-amber-40: #CF8407; + --mdh-amber-50: #AA6704; + --mdh-amber-55: #9B5D02; + --mdh-amber-60: #905603; + --mdh-amber-70: #7B4903; + --mdh-amber-80: #5F3803; + --mdh-amber-85: #4C2E09; + --mdh-amber-90: #402402; + --mdh-amber-95: #321B02; + --mdh-amber-99: #311B02; + --mdh-gold-1: #FFFCEF; + --mdh-gold-2: #FEFAE3; + --mdh-gold-3: #FFF6CC; + --mdh-gold-4: #FEF3BE; + --mdh-gold-5: #FFEFA7; + --mdh-gold-10: #FFE05F; + --mdh-gold-20: #F5CB0B; + --mdh-gold-30: #DCB30B; + --mdh-gold-35: #C8A109; + --mdh-gold-40: #B59007; + --mdh-gold-50: #937204; + --mdh-gold-55: #866702; + --mdh-gold-60: #7D6003; + --mdh-gold-70: #6A5103; + --mdh-gold-80: #533F04; + --mdh-gold-85: #433306; + --mdh-gold-90: #372902; + --mdh-gold-95: #2B1F01; + --mdh-gold-99: #291E02; + --mdh-yellow-1: #FCFDEF; + --mdh-yellow-2: #FAFBE4; + --mdh-yellow-3: #F7F8CD; + --mdh-yellow-4: #F5F6C0; + --mdh-yellow-5: #F3F3AA; + --mdh-yellow-10: #EAE767; + --mdh-yellow-20: #D9D40B; + --mdh-yellow-30: #C0BC0B; + --mdh-yellow-35: #ADA90D; + --mdh-yellow-40: #9C9907; + --mdh-yellow-50: #7C7904; + --mdh-yellow-55: #726F03; + --mdh-yellow-60: #696703; + --mdh-yellow-70: #5A5703; + --mdh-yellow-80: #454403; + --mdh-yellow-85: #383709; + --mdh-yellow-90: #2E2C02; + --mdh-yellow-95: #232201; + --mdh-yellow-99: #232202; + --mdh-chartreuse-1: #F8FCEF; + --mdh-chartreuse-2: #F4FBE5; + --mdh-chartreuse-3: #EFFCD3; + --mdh-chartreuse-4: #EBFBC8; + --mdh-chartreuse-5: #E2F7B2; + --mdh-chartreuse-10: #CEEE78; + --mdh-chartreuse-20: #B6DE0B; + --mdh-chartreuse-30: #A0C40A; + --mdh-chartreuse-35: #90B106; + --mdh-chartreuse-40: #82A007; + --mdh-chartreuse-50: #677F04; + --mdh-chartreuse-55: #5D7402; + --mdh-chartreuse-60: #576C03; + --mdh-chartreuse-70: #4A5C03; + --mdh-chartreuse-80: #384603; + --mdh-chartreuse-85: #2D3903; + --mdh-chartreuse-90: #242E02; + --mdh-chartreuse-95: #1C2401; + --mdh-chartreuse-99: #1C2402; + --mdh-lime-1: #F5FCF1; + --mdh-lime-2: #F0FCE8; + --mdh-lime-3: #E7FED8; + --mdh-lime-4: #DDFACB; + --mdh-lime-5: #D3F9B9; + --mdh-lime-10: #B6F288; + --mdh-lime-20: #88E60B; + --mdh-lime-30: #78CD0A; + --mdh-lime-35: #6BB803; + --mdh-lime-40: #61A606; + --mdh-lime-50: #4C8404; + --mdh-lime-55: #457903; + --mdh-lime-60: #3F7003; + --mdh-lime-70: #366003; + --mdh-lime-80: #294A03; + --mdh-lime-85: #203B04; + --mdh-lime-90: #182F02; + --mdh-lime-95: #132602; + --mdh-lime-99: #132502; + --mdh-green-1: #F4FEF3; + --mdh-green-2: #ECFEEB; + --mdh-green-3: #DDFDDB; + --mdh-green-4: #D5FDD3; + --mdh-green-5: #C6FCC4; + --mdh-green-10: #9DF69B; + --mdh-green-20: #0DEF35; + --mdh-green-30: #0DD42F; + --mdh-green-35: #11BF2B; + --mdh-green-40: #08AC24; + --mdh-green-50: #058A1B; + --mdh-green-55: #047E18; + --mdh-green-60: #027414; + --mdh-green-70: #046311; + --mdh-green-80: #044D0C; + --mdh-green-85: #043F09; + --mdh-green-90: #033306; + --mdh-green-95: #012803; + --mdh-green-99: #032805; + --mdh-emerald-1: #F2FEF6; + --mdh-emerald-2: #E9FEF0; + --mdh-emerald-3: #D4FEE3; + --mdh-emerald-4: #C8FEDC; + --mdh-emerald-5: #B6FED1; + --mdh-emerald-10: #7EF9B3; + --mdh-emerald-20: #0EEC91; + --mdh-emerald-30: #0DD180; + --mdh-emerald-35: #06BE73; + --mdh-emerald-40: #08AA67; + --mdh-emerald-50: #058851; + --mdh-emerald-55: #047C4A; + --mdh-emerald-60: #037344; + --mdh-emerald-70: #046239; + --mdh-emerald-80: #044C2C; + --mdh-emerald-85: #053E23; + --mdh-emerald-90: #03321B; + --mdh-emerald-95: #012714; + --mdh-emerald-99: #032715; + --mdh-teal-1: #F0FEFB; + --mdh-teal-2: #E5FEF8; + --mdh-teal-3: #CDFEF3; + --mdh-teal-4: #C0FEF1; + --mdh-teal-5: #AAFEED; + --mdh-teal-10: #41FBDF; + --mdh-teal-20: #0EE8CC; + --mdh-teal-30: #0DCEB4; + --mdh-teal-35: #04B9A2; + --mdh-teal-40: #08A792; + --mdh-teal-50: #058574; + --mdh-teal-55: #047A6A; + --mdh-teal-60: #037162; + --mdh-teal-70: #046053; + --mdh-teal-80: #044A40; + --mdh-teal-85: #0A3C34; + --mdh-teal-90: #03312A; + --mdh-teal-95: #012620; + --mdh-teal-99: #032620; + --mdh-cyan-1: #F6FDFF; + --mdh-cyan-2: #EDFBFE; + --mdh-cyan-3: #E0F9FF; + --mdh-cyan-4: #DAF8FF; + --mdh-cyan-5: #CAF6FE; + --mdh-cyan-10: #9BEEFF; + --mdh-cyan-20: #2DE2FF; + --mdh-cyan-30: #0DC9E3; + --mdh-cyan-35: #18B5CC; + --mdh-cyan-40: #08A3B9; + --mdh-cyan-50: #058294; + --mdh-cyan-55: #047788; + --mdh-cyan-60: #036E7D; + --mdh-cyan-70: #045E6B; + --mdh-cyan-80: #044852; + --mdh-cyan-85: #0B3B42; + --mdh-cyan-90: #033037; + --mdh-cyan-95: #01252B; + --mdh-cyan-99: #03252B; + --mdh-azure-1: #F8FCFF; + --mdh-azure-2: #F4FAFF; + --mdh-azure-3: #EDF6FF; + --mdh-azure-4: #E8F4FF; + --mdh-azure-5: #E0F1FF; + --mdh-azure-10: #C7E5FF; + --mdh-azure-20: #A2D4FF; + --mdh-azure-30: #6DBDFF; + --mdh-azure-35: #4CAAF4; + --mdh-azure-40: #419ADF; + --mdh-azure-50: #327BB3; + --mdh-azure-55: #2D70A4; + --mdh-azure-60: #245E8A; + --mdh-azure-70: #1C4D73; + --mdh-azure-80: #164060; + --mdh-azure-85: #0A3555; + --mdh-azure-90: #0E2C44; + --mdh-azure-95: #092336; + --mdh-azure-99: #0B2335; + + + + + + + + + + /* Gradients */ - --ce-gradient-alpine-overlook-dark: linear-gradient(180deg, rgba(12, 100, 235), rgba(76, 152, 207)); - --ce-gradient-alpine-overlook: linear-gradient(180deg, rgba(117, 180, 225), rgba(246, 250, 253)); - --ce-gradient-cold-front-dark: linear-gradient(180deg, rgba(143, 130, 246), rgba(230, 107, 111)); - --ce-gradient-cold-front: linear-gradient(180deg, rgba(244, 215, 255), rgba(252, 217, 218)); - --ce-gradient-early-bird-dark: linear-gradient(180deg, rgba(34, 159, 185), rgba(143, 130, 246)); - --ce-gradient-early-bird: linear-gradient(180deg, rgba(199, 196, 255), rgba(255, 245, 156)); - --ce-gradient-evergreen-petrichor-dark: linear-gradient(180deg, rgba(34, 159, 185), rgba(0, 123, 108)); - --ce-gradient-evergreen-petrichor: linear-gradient(180deg, rgba(137, 218, 183), rgba(255, 248, 184)); - --ce-gradient-high-noon-dark: linear-gradient(180deg, rgba(236, 108, 5), rgba(206, 44, 50)); - --ce-gradient-high-noon: linear-gradient(180deg, rgba(255, 242, 128), rgba(255, 146, 40)); - --ce-gradient-midnight-moon-dark: linear-gradient(180deg, rgba(207, 94, 255), rgba(102, 83, 240)); - --ce-gradient-midnight-moon: linear-gradient(180deg, rgba(169, 163, 254), rgba(222, 221, 255)); - --ce-gradient-open-horizon-dark: linear-gradient(180deg, rgba(76, 145, 248), rgba(207, 94, 255)); - --ce-gradient-open-horizon: linear-gradient(180deg, rgba(117, 180, 225), rgba(252, 217, 218)); - --ce-gradient-summer-solstice-dark: linear-gradient(180deg, rgba(255, 146, 41), rgba(143, 130, 246)); - --ce-gradient-summer-solstice: linear-gradient(180deg, rgba(255, 146, 40), rgba(199, 196, 255)); - --ce-gradient-summit-sunset-dark: linear-gradient(180deg, rgba(207, 94, 255), rgba(230, 107, 111)); - --ce-gradient-summit-sunset: linear-gradient(180deg, rgba(244, 215, 255), rgba(246, 145, 148)); - --ce-gradient-tropical-mist-dark: linear-gradient(180deg, rgba(76, 145, 248), rgba(35, 164, 130)); - --ce-gradient-tropical-mist: linear-gradient(180deg, rgba(117, 180, 225), rgba(137, 218, 183)); + --mdh-gradient-alpine-overlook-dark: linear-gradient(180deg, rgba(12, 100, 235), rgba(76, 152, 207)); + --mdh-gradient-alpine-overlook: linear-gradient(180deg, rgba(117, 180, 225), rgba(246, 250, 253)); + --mdh-gradient-cold-front-dark: linear-gradient(180deg, rgba(143, 130, 246), rgba(230, 107, 111)); + --mdh-gradient-cold-front: linear-gradient(180deg, rgba(244, 215, 255), rgba(252, 217, 218)); + --mdh-gradient-early-bird-dark: linear-gradient(180deg, rgba(34, 159, 185), rgba(143, 130, 246)); + --mdh-gradient-early-bird: linear-gradient(180deg, rgba(199, 196, 255), rgba(255, 245, 156)); + --mdh-gradient-evergreen-petrichor-dark: linear-gradient(180deg, rgba(34, 159, 185), rgba(0, 123, 108)); + --mdh-gradient-evergreen-petrichor: linear-gradient(180deg, rgba(137, 218, 183), rgba(255, 248, 184)); + --mdh-gradient-high-noon-dark: linear-gradient(180deg, rgba(236, 108, 5), rgba(206, 44, 50)); + --mdh-gradient-high-noon: linear-gradient(180deg, rgba(255, 242, 128), rgba(255, 146, 40)); + --mdh-gradient-midnight-moon-dark: linear-gradient(180deg, rgba(207, 94, 255), rgba(102, 83, 240)); + --mdh-gradient-midnight-moon: linear-gradient(180deg, rgba(169, 163, 254), rgba(222, 221, 255)); + --mdh-gradient-open-horizon-dark: linear-gradient(180deg, rgba(76, 145, 248), rgba(207, 94, 255)); + --mdh-gradient-open-horizon: linear-gradient(180deg, rgba(117, 180, 225), rgba(252, 217, 218)); + --mdh-gradient-summer-solstice-dark: linear-gradient(180deg, rgba(255, 146, 41), rgba(143, 130, 246)); + --mdh-gradient-summer-solstice: linear-gradient(180deg, rgba(255, 146, 40), rgba(199, 196, 255)); + --mdh-gradient-summit-sunset-dark: linear-gradient(180deg, rgba(207, 94, 255), rgba(230, 107, 111)); + --mdh-gradient-summit-sunset: linear-gradient(180deg, rgba(244, 215, 255), rgba(246, 145, 148)); + --mdh-gradient-tropical-mist-dark: linear-gradient(180deg, rgba(76, 145, 248), rgba(35, 164, 130)); + --mdh-gradient-tropical-mist: linear-gradient(180deg, rgba(117, 180, 225), rgba(137, 218, 183)); + ${legacyCeColors} }`; export const lightColorStyle: CSSProperties = { @@ -329,6 +613,20 @@ export const lightColorStyle: CSSProperties = { '--mdhui-text-color-3': '#999', '--mdhui-text-color-4': '#bbb', + /* Signal colors: base token = marks (bars/pins/dots); "-text" = the signal as + foreground, since CVD-optimized mark colors can be too weak as text. Light keeps + the hexes components previously hardcoded; dark overrides. */ + '--mdhui-color-glucose': '#c4291c', + '--mdhui-color-glucose-text': '#c4291c', + '--mdhui-color-heart-rate': '#e35c33', + '--mdhui-color-activity': '#f5b722', + '--mdhui-color-activity-text': '#f5b722', + '--mdhui-color-sleep': '#7b88c6', + '--mdhui-color-sleep-text': '#7b88c6', + // No shipped hex to mirror — this signal is new, so it comes from the ramp. Grade 55 + // is the lightest teal that still clears AA on the 1-5 backgrounds. + '--mdhui-color-air-quality': 'var(--mdh-teal-55)', + '--mdhui-box-shadow-color-0': 'rgba(0, 0, 0, 0.05)', '--mdhui-box-shadow-color-1': '#aaa', @@ -343,30 +641,56 @@ export const lightColorScheme = css({ ':root': lightColorStyle } as CSSObject); +// Dark values are palette references so pairings inherit the ramp's audited WCAG+APCA +// guarantees — validate changes with `npm run audit:colors`. export const darkColorStyle: CSSProperties = { - '--mdhui-background-color-0': '#2c2c2d', - '--mdhui-background-color-1': '#1c1c1d', - '--mdhui-background-color-2': '#000', - '--mdhui-background-color-highest-contrast': '#000', - - '--mdhui-border-color-0': '#444446', - '--mdhui-border-color-1': '#444', - '--mdhui-border-color-2': '#555', - - '--mdhui-text-color-0': '#FFF', - '--mdhui-text-color-1': '#FFF', - '--mdhui-text-color-2': '#9e9ea5', - '--mdhui-text-color-3': '#59595d', - '--mdhui-text-color-4': '#59595d', - - '--mdhui-box-shadow-color-0': '#333', - '--mdhui-box-shadow-color-1': '#666', + '--mdhui-background-color-0': 'var(--mdh-grey-85)', + '--mdhui-background-color-1': 'var(--mdh-grey-95)', + '--mdhui-background-color-2': 'var(--mdh-grey-99)', + '--mdhui-background-color-highest-contrast': 'var(--mdh-grey-99)', + + '--mdhui-border-color-0': 'var(--mdh-grey-70)', + '--mdhui-border-color-1': 'var(--mdh-grey-70)', + '--mdhui-border-color-2': 'var(--mdh-grey-60)', + + '--mdhui-text-color-0': 'var(--mdh-grey-0)', + '--mdhui-text-color-1': 'var(--mdh-grey-1)', + '--mdhui-text-color-2': 'var(--mdh-grey-10)', + '--mdhui-text-color-3': 'var(--mdh-grey-30)', + '--mdhui-text-color-4': 'var(--mdh-grey-40)', + + '--mdhui-color-glucose': 'var(--mdh-red-orange-40)', + // grade 30 rather than the mark's 40: as foreground on grey-80 the 40 lands at Lc 41, + // well under what APCA wants for text, while 30 reaches Lc 59. + '--mdhui-color-glucose-text': 'var(--mdh-red-orange-35)', + '--mdhui-color-heart-rate': 'var(--mdh-red-orange-40)', + '--mdhui-color-activity': 'var(--mdh-gold-20)', + '--mdhui-color-activity-text': 'var(--mdh-gold-20)', + '--mdhui-color-sleep': 'var(--mdh-indigo-40)', + '--mdhui-color-sleep-text': 'var(--mdh-indigo-35)', + '--mdhui-color-air-quality': 'var(--mdh-teal-35)', + + // Accent base tokens keep the light-mode fill grades (fills carry white text); + // "-text" = foreground-tuned grades for links/status text. + '--mdhui-color-primary': 'var(--mdh-blue-50)', + '--mdhui-color-success': 'var(--mdh-green-50)', + '--mdhui-color-warning': 'var(--mdh-red-orange-50)', + '--mdhui-color-danger': 'var(--mdh-red-50)', + '--mdhui-color-primary-text': 'var(--mdh-blue-30)', + '--mdhui-color-success-text': 'var(--mdh-green-35)', + '--mdhui-color-warning-text': 'var(--mdh-orange-35)', + // red-35 on the grade-85 cards is gap 50 — band-licensed AA text with real chroma; + // the 35/85 rows exist for exactly this pairing (like 55 exists for light backgrounds). + '--mdhui-color-danger-text': 'var(--mdh-red-35)', + + '--mdhui-box-shadow-color-0': 'var(--mdh-grey-80)', + '--mdhui-box-shadow-color-1': 'var(--mdh-grey-60)', '--mdhui-box-shadow-0': '0px 0px 1px 1px var(--mdhui-border-color-0)', '--mdhui-box-shadow-1': '0px 0px 1px 1px var(--mdhui-border-color-0)', '--mdhui-overlay-gradient': 'linear-gradient(to left, rgba(0, 0, 0, 0) 0%, rgba(0, 11, 15, 0.2) 100%) !important', - color: "#FFF" + color: 'var(--mdh-grey-5)' } as CSSProperties; export const darkColorScheme = css({ @@ -375,7 +699,7 @@ export const darkColorScheme = css({ export const global = css` a { - color: var(--mdhui-color-primary); + color: var(--mdhui-color-primary-text); } html { diff --git a/src/helpers/legacyCeColors.ts b/src/helpers/legacyCeColors.ts new file mode 100644 index 000000000..2a303cebf --- /dev/null +++ b/src/helpers/legacyCeColors.ts @@ -0,0 +1,260 @@ +// DEPRECATED. Frozen at pre-redesign values so downstream content that references +// --ce-* vars (e.g. ViewBuilder custom views) keeps rendering unchanged. New code uses +// the --mdhui-* tokens. +export const legacyCeColors = ` + /* BASE-COLORS */ + --ce-grey-1: rgb(251, 252, 252); + --ce-grey-2: rgb(247, 248, 249); + --ce-grey-3: rgb(244, 245, 246); + --ce-grey-4: rgb(242, 243, 245); + --ce-grey-5: rgb(237, 238, 240); + --ce-grey-10: rgb(224, 225, 230); + --ce-grey-20: rgb(200, 201, 210); + --ce-grey-30: rgb(172, 173, 186); + --ce-grey-40: rgb(143, 144, 162); + --ce-grey-50: rgb(115, 116, 139); + --ce-grey-55: rgb(107, 107, 129); + --ce-grey-60: rgb(90, 90, 108); + --ce-grey-70: rgb(68, 67, 81); + --ce-grey-80: rgb(46, 45, 53); + --ce-grey-90: rgb(29, 28, 34); + --ce-grey-95: rgb(27, 26, 31); + --ce-grey-96: rgb(25, 25, 29); + --ce-grey-97: rgb(22, 21, 25); + --ce-grey-98: rgb(20, 19, 23); + --ce-grey-99: rgb(17, 16, 19); + + --ce-yellow-1: rgb(255, 252, 230); + --ce-yellow-2: rgb(255, 249, 207); + --ce-yellow-3: rgb(255, 247, 195); + --ce-yellow-4: rgb(255, 245, 183); + --ce-yellow-5: rgb(255, 240, 148); + --ce-yellow-10: rgb(255, 224, 43); + --ce-yellow-20: rgb(246, 196, 0); + --ce-yellow-30: rgb(211, 168, 0); + --ce-yellow-40: rgb(177, 141, 0); + --ce-yellow-50: rgb(144, 114, 0); + --ce-yellow-55: rgb(134, 105, 0); + --ce-yellow-60: rgb(112, 89, 0); + --ce-yellow-70: rgb(85, 67, 0); + --ce-yellow-80: rgb(57, 44, 0); + --ce-yellow-90: rgb(37, 28, 0); + --ce-yellow-95: rgb(34, 26, 0); + --ce-yellow-96: rgb(31, 24, 0); + --ce-yellow-97: rgb(28, 21, 0); + --ce-yellow-98: rgb(25, 20, 0); + --ce-yellow-99: rgb(21, 17, 0); + + --ce-orange-1: rgb(255, 251, 246); + --ce-orange-2: rgb(255, 246, 237); + --ce-orange-3: rgb(255, 244, 232); + --ce-orange-4: rgb(255, 241, 227); + --ce-orange-5: rgb(255, 236, 217); + --ce-orange-10: rgb(255, 219, 184); + --ce-orange-20: rgb(255, 188, 122); + --ce-orange-30: rgb(255, 146, 41); + --ce-orange-40: rgb(236, 108, 5); + --ce-orange-45: #E35C33; + --ce-orange-50: rgb(191, 87, 4); + --ce-orange-55: rgb(177, 81, 3); + --ce-orange-60: rgb(149, 68, 3); + --ce-orange-70: rgb(112, 51, 2); + --ce-orange-80: rgb(76, 34, 1); + --ce-orange-90: rgb(49, 22, 0); + --ce-orange-95: rgb(46, 20, 0); + --ce-orange-96: rgb(42, 19, 0); + --ce-orange-97: rgb(38, 17, 0); + --ce-orange-98: rgb(31, 14, 0); + --ce-orange-99: rgb(29, 13, 0); + + --ce-red-1: rgb(255, 250, 250); + --ce-red-2: rgb(254, 245, 245); + --ce-red-3: rgb(253, 243, 244); + --ce-red-4: rgb(253, 239, 240); + --ce-red-5: rgb(252, 234, 234); + --ce-red-10: rgb(249, 217, 218); + --ce-red-20: rgb(244, 187, 189); + --ce-red-30: rgb(237, 150, 153); + --ce-red-40: rgb(230, 107, 111); + --ce-red-50: rgb(220, 52, 58); + --ce-red-55: rgb(206, 44, 50); + --ce-red-60: rgb(174, 37, 42); + --ce-red-70: rgb(132, 28, 32); + --ce-red-80: rgb(89, 20, 22); + --ce-red-90: rgb(58, 13, 14); + --ce-red-95: rgb(54, 12, 13); + --ce-red-96: rgb(51, 11, 12); + --ce-red-97: rgb(45, 10, 11); + --ce-red-98: rgb(41, 9, 10); + --ce-red-99: rgb(35, 8, 9); + + --ce-red-vivid-1: rgb(255, 250, 250); + --ce-red-vivid-2: rgb(255, 246, 247); + --ce-red-vivid-3: rgb(254, 243, 243); + --ce-red-vivid-4: rgb(254, 240, 240); + --ce-red-vivid-5: rgb(254, 233, 233); + --ce-red-vivid-10: rgb(252, 215, 217); + --ce-red-vivid-20: rgb(250, 186, 188); + --ce-red-vivid-30: rgb(247, 145, 148); + --ce-red-vivid-40: rgb(243, 97, 102); + --ce-red-vivid-50: rgb(236, 6, 14); + --ce-red-vivid-55: rgb(220, 0, 7); + --ce-red-vivid-60: rgb(187, 0, 6); + --ce-red-vivid-70: rgb(143, 0, 4); + --ce-red-vivid-80: rgb(100, 0, 3); + --ce-red-vivid-90: rgb(67, 0, 2); + --ce-red-vivid-95: rgb(64, 0, 2); + --ce-red-vivid-96: rgb(60, 0, 2); + --ce-red-vivid-97: rgb(54, 0, 2); + --ce-red-vivid-98: rgb(50, 0, 2); + --ce-red-vivid-99: rgb(45, 0, 1); + + --ce-violet-1: rgb(254, 251, 255); + --ce-violet-2: rgb(252, 246, 255); + --ce-violet-3: rgb(252, 243, 255); + --ce-violet-4: rgb(250, 238, 255); + --ce-violet-5: rgb(249, 233, 255); + --ce-violet-10: rgb(244, 214, 255); + --ce-violet-20: rgb(235, 183, 255); + --ce-violet-30: rgb(224, 141, 255); + --ce-violet-40: rgb(207, 94, 255); + --ce-violet-50: rgb(174, 50, 246); + --ce-violet-55: rgb(162, 42, 233); + --ce-violet-60: rgb(137, 35, 197); + --ce-violet-70: rgb(104, 25, 150); + --ce-violet-80: rgb(71, 16, 104); + --ce-violet-90: rgb(47, 9, 69); + --ce-violet-95: rgb(44, 8, 65); + --ce-violet-96: rgb(41, 8, 60); + --ce-violet-97: rgb(36, 7, 54); + --ce-violet-98: rgb(34, 6, 50); + --ce-violet-99: rgb(29, 6, 42); + + --ce-blue-1: rgb(250, 252, 253); + --ce-blue-2: rgb(244, 249, 252); + --ce-blue-3: rgb(238, 246, 250); + --ce-blue-4: rgb(235, 244, 249); + --ce-blue-5: rgb(230, 241, 248); + --ce-blue-10: rgb(206, 227, 242); + --ce-blue-20: rgb(170, 206, 232); + --ce-blue-30: rgb(125, 180, 220); + --ce-blue-40: rgb(76, 152, 207); + --ce-blue-50: rgb(24, 122, 193); + --ce-blue-55: rgb(11, 112, 186); + --ce-blue-60: rgb(9, 94, 159); + --ce-blue-70: rgb(5, 70, 122); + --ce-blue-80: rgb(2, 47, 86); + --ce-blue-90: rgb(0, 29, 60); + --ce-blue-95: rgb(0, 27, 56); + --ce-blue-96: rgb(0, 25, 52); + --ce-blue-97: rgb(0, 23, 46); + --ce-blue-98: rgb(0, 20, 42); + --ce-blue-99: rgb(0, 18, 36); + + --ce-blue-vivid-1: rgb(250, 252, 255); + --ce-blue-vivid-2: rgb(244, 248, 255); + --ce-blue-vivid-3: rgb(241, 246, 255); + --ce-blue-vivid-4: rgb(235, 243, 254); + --ce-blue-vivid-5: rgb(229, 239, 254); + --ce-blue-vivid-10: rgb(209, 226, 253); + --ce-blue-vivid-20: rgb(205, 227, 255); + --ce-blue-vivid-30: rgb(125, 175, 250); + --ce-blue-vivid-40: rgb(76, 145, 248); + --ce-blue-vivid-50: rgb(20, 110, 246); + --ce-blue-vivid-55: rgb(12, 100, 235); + --ce-blue-vivid-60: rgb(9, 84, 200); + --ce-blue-vivid-70: rgb(6, 62, 152); + --ce-blue-vivid-80: rgb(3, 42, 106); + --ce-blue-vivid-90: rgb(0, 26, 72); + --ce-blue-vivid-95: rgb(0, 24, 67); + --ce-blue-vivid-96: rgb(0, 23, 63); + --ce-blue-vivid-97: rgb(0, 20, 56); + --ce-blue-vivid-98: rgb(0, 18, 50); + --ce-blue-vivid-99: rgb(0, 16, 45); + + --ce-indigo-1: rgb(251, 252, 255); + --ce-indigo-2: rgb(247, 248, 255); + --ce-indigo-3: rgb(244, 245, 255); + --ce-indigo-4: rgb(241, 241, 255); + --ce-indigo-5: rgb(237, 237, 255); + --ce-indigo-10: rgb(222, 221, 255); + --ce-indigo-20: rgb(200, 196, 253); + --ce-indigo-30: rgb(172, 163, 250); + --ce-indigo-40: rgb(143, 130, 246); + --ce-indigo-50: rgb(112, 94, 243); + --ce-indigo-55: rgb(102, 83, 240); + --ce-indigo-60: rgb(83, 61, 224); + --ce-indigo-70: rgb(54, 28, 201); + --ce-indigo-80: rgb(36, 16, 142); + --ce-indigo-90: rgb(22, 8, 98); + --ce-indigo-95: rgb(21, 7, 92); + --ce-indigo-96: rgb(19, 7, 86); + --ce-indigo-97: rgb(17, 6, 78); + --ce-indigo-98: rgb(16, 6, 71); + --ce-indigo-99: rgb(14, 5, 63); + + --ce-cyan-1: rgb(247, 253, 254); + --ce-cyan-2: rgb(237, 251, 253); + --ce-cyan-3: rgb(228, 249, 252); + --ce-cyan-4: rgb(219, 247, 251); + --ce-cyan-5: rgb(209, 245, 250); + --ce-cyan-10: rgb(168, 235, 246); + --ce-cyan-20: rgb(93, 218, 239); + --ce-cyan-30: rgb(63, 188, 212); + --ce-cyan-40: rgb(34, 159, 185); + --ce-cyan-50: rgb(4, 128, 158); + --ce-cyan-55: rgb(10, 118, 148); + --ce-cyan-60: rgb(10, 99, 124); + --ce-cyan-70: rgb(9, 74, 94); + --ce-cyan-80: rgb(8, 50, 65); + --ce-cyan-90: rgb(8, 31, 42); + --ce-cyan-95: rgb(8, 29, 39); + --ce-cyan-96: rgb(7, 27, 36); + --ce-cyan-97: rgb(6, 24, 32); + --ce-cyan-98: rgb(6, 22, 28); + --ce-cyan-99: rgb(5, 18, 24); + + --ce-green-1: rgb(248, 253, 251); + --ce-green-2: rgb(241, 251, 246); + --ce-green-3: rgb(234, 249, 242); + --ce-green-4: rgb(230, 247, 240); + --ce-green-5: rgb(219, 244, 233); + --ce-green-10: rgb(189, 234, 215); + --ce-green-20: rgb(138, 217, 184); + --ce-green-30: rgb(69, 194, 143); + --ce-green-40: rgb(35, 164, 130); + --ce-green-50: rgb(1, 132, 116); + --ce-green-55: rgb(0, 123, 108); + --ce-green-60: rgb(0, 103, 91); + --ce-green-70: rgb(0, 78, 69); + --ce-green-80: rgb(0, 52, 47); + --ce-green-90: rgb(0, 33, 30); + --ce-green-95: rgb(0, 31, 29); + --ce-green-96: rgb(0, 29, 27); + --ce-green-97: rgb(0, 26, 23); + --ce-green-98: rgb(0, 23, 21); + --ce-green-99: rgb(0, 20, 19); + + + /* Gradients */ + --ce-gradient-alpine-overlook-dark: linear-gradient(180deg, rgba(12, 100, 235), rgba(76, 152, 207)); + --ce-gradient-alpine-overlook: linear-gradient(180deg, rgba(117, 180, 225), rgba(246, 250, 253)); + --ce-gradient-cold-front-dark: linear-gradient(180deg, rgba(143, 130, 246), rgba(230, 107, 111)); + --ce-gradient-cold-front: linear-gradient(180deg, rgba(244, 215, 255), rgba(252, 217, 218)); + --ce-gradient-early-bird-dark: linear-gradient(180deg, rgba(34, 159, 185), rgba(143, 130, 246)); + --ce-gradient-early-bird: linear-gradient(180deg, rgba(199, 196, 255), rgba(255, 245, 156)); + --ce-gradient-evergreen-petrichor-dark: linear-gradient(180deg, rgba(34, 159, 185), rgba(0, 123, 108)); + --ce-gradient-evergreen-petrichor: linear-gradient(180deg, rgba(137, 218, 183), rgba(255, 248, 184)); + --ce-gradient-high-noon-dark: linear-gradient(180deg, rgba(236, 108, 5), rgba(206, 44, 50)); + --ce-gradient-high-noon: linear-gradient(180deg, rgba(255, 242, 128), rgba(255, 146, 40)); + --ce-gradient-midnight-moon-dark: linear-gradient(180deg, rgba(207, 94, 255), rgba(102, 83, 240)); + --ce-gradient-midnight-moon: linear-gradient(180deg, rgba(169, 163, 254), rgba(222, 221, 255)); + --ce-gradient-open-horizon-dark: linear-gradient(180deg, rgba(76, 145, 248), rgba(207, 94, 255)); + --ce-gradient-open-horizon: linear-gradient(180deg, rgba(117, 180, 225), rgba(252, 217, 218)); + --ce-gradient-summer-solstice-dark: linear-gradient(180deg, rgba(255, 146, 41), rgba(143, 130, 246)); + --ce-gradient-summer-solstice: linear-gradient(180deg, rgba(255, 146, 40), rgba(199, 196, 255)); + --ce-gradient-summit-sunset-dark: linear-gradient(180deg, rgba(207, 94, 255), rgba(230, 107, 111)); + --ce-gradient-summit-sunset: linear-gradient(180deg, rgba(244, 215, 255), rgba(246, 145, 148)); + --ce-gradient-tropical-mist-dark: linear-gradient(180deg, rgba(76, 145, 248), rgba(35, 164, 130)); + --ce-gradient-tropical-mist: linear-gradient(180deg, rgba(117, 180, 225), rgba(137, 218, 183)); +`; diff --git a/src/helpers/news-feed/eventTypeDefinitions.tsx b/src/helpers/news-feed/eventTypeDefinitions.tsx index a289382ee..6d1c64db8 100644 --- a/src/helpers/news-feed/eventTypeDefinitions.tsx +++ b/src/helpers/news-feed/eventTypeDefinitions.tsx @@ -95,12 +95,12 @@ let labReportHandler: EventTypeDefinition = { let acuityElement: ReactNode | undefined; if (labObservation.AcuityHighlight === "High") { style.fontWeight = "bold"; - style.color = "var(--mdhui-color-danger)"; + style.color = "var(--mdhui-color-danger-text)"; acuityElement =
H
; } if (labObservation.AcuityHighlight === "Low") { style.fontWeight = "bold"; - style.color = "var(--mdhui-color-primary)"; + style.color = "var(--mdhui-color-primary-text)"; acuityElement =
L
; } return
{labObservation.Value} {labObservation.Units} {acuityElement}
diff --git a/src/stories/foundation/colorDocs.css b/src/stories/foundation/colorDocs.css index b43431264..ace9b650e 100644 --- a/src/stories/foundation/colorDocs.css +++ b/src/stories/foundation/colorDocs.css @@ -1,8 +1,9 @@ -/* Highlight color variables for border and label */ +/* Highlight color variables for border and label; numbers match the magic-number + bands (40 AA-large / 50 AA / 70 AAA) */ :root { - --color-highlight-40: var(--ce-grey-30); - --color-highlight-50: var(--ce-green-40); - --color-highlight-70: var(--ce-cyan-50); + --color-highlight-40: var(--mdh-grey-30); + --color-highlight-50: var(--mdh-green-40); + --color-highlight-70: var(--mdh-cyan-50); color: rgb(46, 52, 56); /* to match storybook's css-in-js for dark mode */ } * { @@ -20,9 +21,10 @@ h3 { h4 { margin-top: 16px !important; } +/* Semantic tokens (not raw --mdh-*) so the accordion flips with the theme toggle. */ details { - background: var(--ce-grey-2); - border: 1px solid var(--ce-grey-10); + background: var(--mdhui-background-color-1); + border: 1px solid var(--mdhui-border-color-0); border-radius: 6px; margin-bottom: 16px; } @@ -30,101 +32,85 @@ summary { font-weight: 600; cursor: pointer; padding: 10px 15px; - color: rgb(46, 52, 56); /* to match storybook's css-in-js for dark mode */ + color: var(--mdhui-text-color-1); } details .details-content { padding: 0 20px 10px 20px !important; } table { - background: #fff; + background: var(--mdhui-background-color-0); } + +/* "Which magic number to use" */ +.magicTableWrap { overflow-x: auto; max-width: 700px; } +.magicTable { border-collapse: collapse; width: 100%; margin: 8px 0 14px; font-size: 14px; } +.magicTable th, .magicTable td { border: 1px solid var(--mdhui-border-color-1); padding: 8px 10px; text-align: left; vertical-align: top; } +.magicTable thead th { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--mdhui-text-color-3); white-space: nowrap; } +.magicTable thead th span { display: block; font-size: 10px; text-transform: none; letter-spacing: 0; font-weight: 400; white-space: normal; max-width: 15ch; } +/* the row most callers want; semantic tokens so it tints correctly in both schemes */ +.magicTable tbody tr.magicTable-default th, .magicTable tbody tr.magicTable-default td { background: var(--mdhui-background-color-1); } +.magicTable tbody th { font-weight: 600; } +.magicTable tbody th span { display: block; font-weight: 400; font-size: 12px; color: var(--mdhui-text-color-3); margin-top: 2px; } +.magicTable tbody td { font-weight: 700; white-space: nowrap; } td, th { line-height: 1.2; } .emphasis-text { - color: var(--ce-cyan-60); - background: var(--ce-cyan-4); + color: var(--mdh-cyan-60); + background: var(--mdh-cyan-4); padding: 2px 4px; border-radius: 2px; font-weight: 600; } -/* Data Viz Use Case tiles */ -.data-viz-use-cases { - display: flex; - flex-wrap: wrap; - gap: 10px; - padding-left: 0 !important; -} -.data-viz-use-cases li { - list-style: none; - padding: 6px 12px; - border-radius: 6px; - display: flex; - align-items: center; - justify-content: center; -} -.data-viz-air { - background-color: var(--ce-cyan-50); - color: #fff !important; -} -.data-viz-sleep { - background-color: var(--ce-indigo-50); - color: #fff !important; -} -.data-viz-heart { - background-color: var(--ce-orange-40); - color: #fff !important; -} -.data-viz-activity { - background-color: var(--ce-yellow-10); - color: #000 !important; -} - - /* Color Cell Rows */ .colorHeader { display: flex; - align-items: center; - min-width: 190px; - margin-bottom: 10px; - gap: 5px; + flex-direction: column; + justify-content: center; + align-items: flex-start; + width: 120px; + min-width: 120px; + margin-bottom: 0; + gap: 2px; + padding-right: 10px; } .colorRow h3, .colorRow p { margin: 0 !important; } +.colorRow h3 { + font-size: 13px !important; +} .colorHeader code { - font-size: 11px !important; + font-size: 10px !important; } .colorRow .colors { display: flex; align-items: flex-start; - flex-wrap: wrap; - gap: 3px; - width: 100%; + flex-wrap: nowrap; + gap: 0; + flex: 1; } .colorCell-overlay { position: absolute; - left: 0; - right: 0; - bottom: 0; - font-size: 10px; - padding: 2px 4px; - text-align: center; + inset: 0; z-index: 12; pointer-events: none; display: flex; - justify-content: space-between; + justify-content: center; align-items: center; - gap: 4px; width: 100%; } +.colorCell-gradeLabel { + font-size: 12px; + font-weight: 600; +} .colorCell-overlay-light { color: #222; } @@ -169,21 +155,21 @@ td, th { .colorCell .color { min-width: 40px; width: 100%; - height: 60px; + height: 46px; margin: 0 auto; } .colorCell .shade { - color: var(--mdhui-grey-99); + color: var(--mdhui-text-color-1); font-size: 12px; } .colorRow { display: flex; - align-items: flex-start; - padding-top: 20px; - flex-direction: column; + align-items: stretch; + padding-top: 0; + flex-direction: row; } @@ -255,27 +241,28 @@ td, th { color: rgb(46, 52, 56); /* to match storybook's css-in-js for dark mode */ } -/* hover borders */ -.colorCell { - border: 2px solid transparent; +/* ramp legend — rings drawn the same way as the hover highlight below */ +.rampLegend { display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: center; margin: 6px 0 10px; font-size: 13px; } +.rampLegend-item { display: inline-flex; align-items: center; gap: 7px; } +.rampLegend-ring { width: 22px; height: 22px; border-radius: 3px; background: var(--mdhui-background-color-2); } +.rampLegend-ring40 { box-shadow: inset 0 0 0 3px var(--color-highlight-40); } +.rampLegend-ring50 { box-shadow: inset 0 0 0 3px var(--color-highlight-50); } +.rampLegend-ring70 { box-shadow: inset 0 0 0 3px var(--color-highlight-70); } + +/* hover highlight — inset so cells stay gapless with no layout shift */ +.colorCell-highlight40 .color { + box-shadow: inset 0 0 0 3px var(--color-highlight-40); } -.colorCell-highlight40 { - border: 2px solid var(--color-highlight-40) !important; - box-shadow: 0 0 0 2px var(--color-highlight-40); - z-index: 2; +.colorCell-highlight50 .color { + box-shadow: inset 0 0 0 3px var(--color-highlight-50); } -.colorCell-highlight50 { - border: 2px solid var(--color-highlight-50) !important; - box-shadow: 0 0 0 2px var(--color-highlight-50); - z-index: 2; +.colorCell-highlight70 .color { + box-shadow: inset 0 0 0 3px var(--color-highlight-70); } -.colorCell-highlight70 { - border: 2px solid var(--color-highlight-70) !important; - box-shadow: 0 0 0 2px var(--color-highlight-70); +.colorCell-highlight40, .colorCell-highlight50, .colorCell-highlight70 { z-index: 2; } - /* Gradients */ .gradients { @@ -284,9 +271,13 @@ td, th { .gradients .colorHeader { flex-direction: column; align-items: flex-start; + /* undo the 120px hue-row header width so the intro wraps like body text */ + width: auto; + min-width: 0; + max-width: 700px; } .gradients .colorRow { - border-top: 1px solid var(--mdhui-grey-30); + border-top: 1px solid var(--mdhui-border-color-1); } .gradientCells .colorCell .color { @@ -299,3 +290,18 @@ td, th { gap: 10px; align-items: flex-start; } + +/* Tokens in context — one card rendered under each scheme via scoped token styles */ +.tokensCtx { display: flex; gap: 14px; flex-wrap: wrap; margin: 8px 0 16px; } +.tokensCtx-col { flex: 1 1 320px; min-width: 300px; max-width: 430px; } +.tokensCtx-colLabel { font: 700 11px ui-monospace, monospace; color: var(--mdhui-text-color-3); margin: 0 0 6px; text-transform: uppercase; } +.tokensCtx-surface { background: var(--mdhui-background-color-1); border-radius: 14px; padding: 14px; color: var(--mdhui-text-color-1); } +.tokensCtx-card { background: var(--mdhui-background-color-0); border: 1px solid var(--mdhui-border-color-0); border-radius: 12px; padding: 16px 18px; font-size: 15px; } +.tokensCtx-ann { font: 10.5px ui-monospace, monospace; color: var(--mdhui-text-color-3); } +/* one sample per row, its token name in a fixed right-hand gutter */ +.tokensCtx-line { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center; padding: 3px 0; } +.tokensCtx-divider { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center; margin: 12px 0 8px; } +.tokensCtx-caption { font: 700 9.5px ui-monospace, monospace; text-transform: uppercase; letter-spacing: .05em; color: var(--mdhui-text-color-3); margin: 2px 0 4px; } +.tokensCtx-grid { display: grid; grid-template-columns: 110px 1fr; gap: 10px; align-items: center; padding: 3px 0; } +.tokensCtx-track { background: var(--mdhui-background-color-2); border-radius: 99px; height: 8px; } +.tokensCtx-fill { width: 45%; height: 8px; border-radius: 99px; background: var(--mdhui-color-primary); } diff --git a/src/stories/foundation/colorDocs.tsx b/src/stories/foundation/colorDocs.tsx index 81e76a5c1..b2cd000a7 100644 --- a/src/stories/foundation/colorDocs.tsx +++ b/src/stories/foundation/colorDocs.tsx @@ -3,22 +3,40 @@ import './colorDocs.css'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCopy as faCopyRegular } from '@fortawesome/free-regular-svg-icons'; import { faCopy as faCopySolid } from '@fortawesome/free-solid-svg-icons'; +import { APCAcontrast, sRGBtoY } from 'apca-w3'; +import { darkColorStyle, lightColorStyle } from '../../helpers/globalCss'; +// A walk around the wheel: each muted family sits beside its vivid parent. const colors: string[] = [ 'grey', + 'blue-grey', + 'blue-muted', 'blue', - 'blue-vivid', + 'cobalt', 'indigo', + 'iris', 'violet', - 'cyan', - 'green', + 'purple', + 'magenta', + 'fuchsia', + 'pink', + 'red-muted', 'red', - 'red-vivid', + 'red-orange', 'orange', + 'amber', + 'gold', 'yellow', + 'chartreuse', + 'lime', + 'green', + 'emerald', + 'teal', + 'cyan', + 'azure', ]; -const grades = [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 55, 60, 70, 80, 90, 99]; +const grades = [1, 2, 3, 4, 5, 10, 20, 30, 35, 40, 50, 55, 60, 70, 80, 85, 90, 95, 99]; const gradientsNames: string[] = [ 'alpine-overlook', @@ -83,8 +101,7 @@ function hexToRgb(hex: string) { return [num >> 16, (num >> 8) & 255, num & 255]; } -// TODO: THESE HELPERS MIGHT NOT BE ACCURATE -// Helper: WCAG 2.0 contrast ratio +// Helper: WCAG 2.x contrast ratio function luminance([r, g, b]: number[]) { const a = [r, g, b].map(function (v) { v /= 255; @@ -97,49 +114,44 @@ function wcagContrast(rgb1: number[], rgb2: number[]) { const l2 = luminance(rgb2); return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); } -// Helper: APCA contrast (simplified, not 100% spec, but close for demo) -function apcaContrast(txt: number[], bg: number[]) { - // https://github.com/Myndex/SAPC-APCA/blob/master/documentation/APCA_calculation_steps.md - // This is a simplified version for demo purposes - const Ybg = luminance(bg); - const Ytxt = luminance(txt); - let contrast = 0; - if (Ybg > Ytxt) { - contrast = (Ybg - Ytxt) * 1.14 * 100; - } else { - contrast = (Ytxt - Ybg) * 1.14 * 100 * -1; - } - return Math.round(contrast); +// APCA-4g Lc via the official apca-w3 package +// The sign encodes polarity: positive = dark text on light bg, negative = light on dark. +function apcaLc(text: number[], background: number[]) { + return APCAcontrast(sRGBtoY(text), sRGBtoY(background)); } -// Label for contrast level (A, AA, AAA) with color +// Label for a conformance level: WCAG A/AA/AAA, or an APCA text-role band function ContrastLevelLabel({ level }: { level: string }) { - let colorVar = ''; - let label = ''; - if (!level) { - colorVar = 'var(--ce-grey-40)'; - label = 'N/A'; - } else { - label = level; - if (level === 'A') colorVar = 'var(--color-highlight-40)'; - else if (level === 'AA') colorVar = 'var(--color-highlight-50)'; - else if (level === 'AAA') colorVar = 'var(--color-highlight-70)'; - } - return {label}; + const colorByLevel: { [level: string]: string } = { + 'AA large': 'var(--color-highlight-40)', + 'AA': 'var(--color-highlight-50)', + 'AAA': 'var(--color-highlight-70)', + 'muted': 'var(--color-highlight-40)', + 'body': 'var(--color-highlight-50)', + 'primary': 'var(--color-highlight-70)', + 'non-text': 'var(--mdh-grey-50)', + }; + return ( + {level || 'fails'} + ); } -// Helper: Get WCAG level for a contrast ratio +// Helper: Get WCAG level for a contrast ratio. 3:1 is not Level A — it's the AA floor +// for large text and for non-text elements. function getWcagLevel(contrast: number) { if (contrast >= 7) return 'AAA'; if (contrast >= 4.5) return 'AA'; - if (contrast >= 3) return 'A'; + if (contrast >= 3) return 'AA large'; return ''; } -// Helper: Get APCA level for a contrast value (absolute) -function getApcaLevel(contrast: number) { - const abs = Math.abs(contrast); - if (abs >= 75) return 'AAA'; - if (abs >= 60) return 'AA'; - if (abs >= 40) return 'A'; +// APCA has no A/AA/AAA; label with the strongest text role the |Lc| supports. Each floor +// assumes a minimum font size and weight (75 = 18px/400, 60 = 24px/400, 45 = 36px/400), +// so these labels are a best case. +function getApcaRole(lc: number) { + const abs = Math.abs(lc); + if (abs >= 75) return 'primary'; + if (abs >= 60) return 'body'; + if (abs >= 45) return 'muted'; + if (abs >= 15) return 'non-text'; return ''; } @@ -150,40 +162,15 @@ const ColorCell: React.FC void; }> = ({ hue, grade, hoveredGrade, highlight40, highlight50, highlight70, onHover }) => { - const color = `var(--ce-${hue}-${grade})`; + const color = `var(--mdh-${hue}-${grade})`; const colorDivRef = useRef(null); const [copied, setCopied] = React.useState(false); const [showIcon, setShowIcon] = React.useState(false); - const [hex, setHex] = React.useState(''); - const [wcag, setWcag] = React.useState(''); - const [apca, setApca] = React.useState(''); - - React.useEffect(() => { - if (colorDivRef.current) { - const computed = window.getComputedStyle(colorDivRef.current); - const bgColor = computed.backgroundColor; - const hexVal = rgbToHex(bgColor); - setHex(hexVal); - // Contrast against white and black, show the higher - const rgb = hexToRgb(hexVal); - const wcagWhite = wcagContrast(rgb, [255, 255, 255]); - const wcagBlack = wcagContrast(rgb, [0, 0, 0]); - setWcag(wcagWhite > wcagBlack ? wcagWhite.toFixed(2) + ' (white)' : wcagBlack.toFixed(2) + ' (black)'); - const apcaWhite = apcaContrast(rgb, [255, 255, 255]); - const apcaBlack = apcaContrast(rgb, [0, 0, 0]); - setApca(Math.abs(apcaWhite) > Math.abs(apcaBlack) ? apcaWhite + ' (white)' : apcaBlack + ' (black)'); - } - }, [colorDivRef, color]); const handleClick = async () => { - if (colorDivRef.current) { - const computed = window.getComputedStyle(colorDivRef.current); - const bgColor = computed.backgroundColor; - const hex = rgbToHex(bgColor); - await navigator.clipboard.writeText(hex); - setCopied(true); - setTimeout(() => setCopied(false), 1200); - } + await navigator.clipboard.writeText(color); + setCopied(true); + setTimeout(() => setCopied(false), 1200); }; const isLight = grade < 50; @@ -196,7 +183,7 @@ const ColorCell: React.FC { setShowIcon(true); onHover && onHover(grade); }} onMouseLeave={() => { setShowIcon(false); onHover && onHover(null); }} @@ -205,19 +192,14 @@ const ColorCell: React.FC
-
- {/* {apca.replace(/\s*\(.*\)/, '')} */} -
-
- {wcag.replace(/\s*\(.*\)/, '')} -
+ {grade}
{showIcon && (
-
WCAG 2.0
+
WCAG 2.x
{(() => { const colorVal = colorDivRef.current ? rgbToHex(window.getComputedStyle(colorDivRef.current).backgroundColor) : ''; @@ -238,36 +220,32 @@ const ColorCell: React.FC
-
APCA (WCAG 3.0)
+
APCA Lc (as text / as background)
{(() => { const colorVal = colorDivRef.current ? rgbToHex(window.getComputedStyle(colorDivRef.current).backgroundColor) : ''; const rgb = colorVal ? hexToRgb(colorVal) : [0, 0, 0]; - const apcaWhite = apcaContrast(rgb, [255, 255, 255]); - const apcaBlack = apcaContrast(rgb, [0, 0, 0]); - return <> - - #fff: {apcaWhite} - - - - #000: {apcaBlack} - - - - ; + // The label reflects the weaker direction — what the palette guarantees. + return ([['#fff', [255, 255, 255]], ['#000', [0, 0, 0]]] as [string, number[]][]).map(([label, other]) => { + const asText = apcaLc(rgb, other); + const asBackground = apcaLc(other, rgb); + const weaker = Math.min(Math.abs(asText), Math.abs(asBackground)); + return ( + + {label}: {Math.round(asText)} / {Math.round(asBackground)} + + + ); + }); })()}
)}
-
- {grade} -
{copied && ( -
Copied hex to clipboard
+
Copied {color}
)} ); @@ -280,26 +258,15 @@ interface ColorRowProps { const ColorRow: React.FC = ({ hue }) => { const [hoveredGrade, setHoveredGrade] = React.useState(null); - // Find the first color on each side that is at least X away from hoveredGrade - const getHighlightIndexes = (distance: number) => { + // Bands are the magic-number guarantees: 40 -> AA large, 50 -> AA, 70 -> AAA. + const getHighlightIndexes = (band: number) => { if (hoveredGrade == null) return { left: null, right: null }; let left: number | null = null; let right: number | null = null; - for (let i = grades.length - 1; i >= 0; i--) { - if (grades[i] < hoveredGrade - distance) { - left = i + 1 < grades.length ? i + 1 : null; - break; - } - } for (let i = 0; i < grades.length; i++) { - if (grades[i] > hoveredGrade + distance) { - right = i - 1 >= 0 ? i - 1 : null; - break; - } + if (grades[i] <= hoveredGrade - band) left = i; + if (right === null && grades[i] >= hoveredGrade + band) right = i; } - // If no such grade, pick the edge - if (left === null && hoveredGrade - distance >= grades[0]) left = 0; - if (right === null && hoveredGrade + distance <= grades[grades.length - 1]) right = grades.length - 1; return { left, right }; }; const highlight40 = getHighlightIndexes(40); @@ -311,7 +278,7 @@ const ColorRow: React.FC = ({ hue }) => {

{hue}

- var(--ce-{hue}-##) + var(--mdh-{hue}-##)

@@ -340,15 +307,15 @@ const GradientSection: React.FC = () => {

Gradients

- There are both regular and dark version of the gradients. The regular version is designed for black text on top, while the dark version is designed for white text on top. - var(--ce-gradient-##) + Each gradient comes in a regular and a dark version: regular is made for black text on top, dark for white text. + var(--mdh-gradient-##)

Light (for dark text)

{lightGradients.map((gradient) => (
-
+
{gradient}
@@ -359,7 +326,7 @@ const GradientSection: React.FC = () => {
{darkGradients.map((gradient) => (
-
+
{gradient}
@@ -370,63 +337,229 @@ const GradientSection: React.FC = () => { ) } +// Anchors are the References list at the bottom; cvdPalette.tsx has a matching copy. +const Ref: React.FC<{ n: number }> = ({ n }) => ( + [{n}] +); + +// Scoping the real style objects per column means the preview can't drift from the tokens. +// Base token = fills, -text = the signal as foreground; heart-rate and air-quality have +// no -text token and fall back to the mark color. Ink is per value: air quality's light +// grade needs white where every other fill takes near-black. +const DARK_INK = '#12151b'; +const LIGHT_INK = '#fff'; +type SignalSwatch = { value: string; ink: string }; +const SIGNALS: { name: string; fill: string; text: string; light: SignalSwatch; dark: SignalSwatch }[] = [ + { name: 'Sleep', fill: '--mdhui-color-sleep', text: '--mdhui-color-sleep-text', light: { value: '#7b88c6', ink: DARK_INK }, dark: { value: 'indigo-40', ink: DARK_INK } }, + { name: 'Heart rate', fill: '--mdhui-color-heart-rate', text: '--mdhui-color-heart-rate', light: { value: '#e35c33', ink: DARK_INK }, dark: { value: 'red-orange-40', ink: DARK_INK } }, + { name: 'Activity', fill: '--mdhui-color-activity', text: '--mdhui-color-activity-text', light: { value: '#f5b722', ink: DARK_INK }, dark: { value: 'gold-20', ink: DARK_INK } }, + { name: 'Air quality', fill: '--mdhui-color-air-quality', text: '--mdhui-color-air-quality', light: { value: 'teal-55', ink: LIGHT_INK }, dark: { value: 'teal-30', ink: DARK_INK } }, +]; + +// Every sample sits in one column and its token name in the gutter beside it, so the +// card reads as plausible content on the left and a token index on the right. +const Line: React.FC<{ token: string; children: React.ReactNode }> = ({ token, children }) => ( +
+
{children}
+ {token} +
+); +const Divider: React.FC<{ token: string; color: string }> = ({ token, color }) => ( +
+
+ {token} +
+); +const Caption: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +
{children}
+); + +const TokenContextCard: React.FC<{ scheme: 'light' | 'dark' }> = ({ scheme }) => ( +
+
+
+ 118 mg/dL + Blood glucose, last 7 days + Your readings stayed in range. + Updated 2 hours ago + Sync unavailable + + + View details + In range + Running low + Overdue + Log reading + 3 due +
+ +
+ {SIGNALS.map(signal => { + const swatch = scheme === 'light' ? signal.light : signal.dark; + return ( +
+
{signal.name}
+
{swatch.value}
+
+ ); + })} + +
surface background-color-1 · card background-color-0 · card edge border-color-0
+ +); + +const TokensInContext: React.FC = () => ( +
+
+
Light
+
+
+
+
Dark
+
+
+
+); + const ColorDocs: React.FC = () => { return (
-

The "Magic Number" Color System

-

In design systems compliant with WCAG 2.0 (and Section 508), the term "magic number" refers to the specific luminance difference required - between two colors to ensure they meet minimum contrast standards for accessibility. - The higher the magic number, the greater the contrast and accessibility. -

-
    -
  • Magic number of 40+: Meets WCAG 2.0 AA Large Text (18pt/14pt bold or larger) contrast. For example, grey-90 and indigo-50.
  • -
  • Magic number of 50+: Meets WCAG 2.0 AA contrast or AAA Large Text contrast. For example, grey-90 and red-40. We recommend this level of contrast for most web content.
  • -
  • Magic number of 70+: Meets WCAG 2.0 AAA contrast. For example, grey-10 and red-80.
  • -
-

Additionally, a grade of 50 will meet the Section 508 AA contrast requirement against both pure white (#000) and pure black (#fff).

+

The "magic number" color system

+

The MyDataHelps color system makes accessible color the default, in light mode and dark. It + takes the U.S. Web Design System's approach, gives it more vibrant hues, and adds + APCA: a newer way of measuring contrast that dark mode needs, and the one WCAG 3 + is expected to adopt.

+

Each color's name carries a number, and that number tells you how light or dark the color is. + The gap between two of those numbers is the "magic number": the wider the gap, + the more contrast the pair is guaranteed to have, whichever two hues you picked. Choosing a + readable pair comes down to subtraction, so nobody has to run colors through a contrast checker + to know the answer.

+

Usage

+

color: var(--mdh-[hue]-[grade]) Example: color: var(--mdh-blue-40)

+

The palette is 25 hues plus a grey, each in 19 grades from 1 + (near white) to 99 (near black), published as --mdh-* CSS custom properties. Ten + gradients cover decorative surfaces, and the color-blind-safe palettes cover charts.

+

In component code, always go through the semantic --mdhui-* tokens rather than the + raw palette — the raw --mdh-* vars exist to define them, and the semantic layer is + what keeps light and dark mode both correct.

+

Which magic number to use

+

Find the row for what you're coloring. WCAG is the legal floor that Section 508 + requires; APCA is the perceptual model drafted for WCAG 3, which asks for a bigger + gap to reach the same readability. Both apply in either color scheme, so the APCA number is the one + to reach for when you want a single figure to remember — dark mode is audited against it, and light + pairs that only meet the WCAG minimum can still land short of it.

+
+
{series.dataKey} {formatNumberForLocale(parseFloat(series.value), 2)}
textaccents — as text, then as fillsignals — name in its text color, fill shows its value
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
What you're coloringWCAGminimumAPCArecommended, especially in dark mode
Body text16–18px regular — the default for most content50+80+
Bold or emphasized text16px bold — too small to count as large text50+70+
Large text24px regular (18pt) — WCAG's large-text tier starts here40+70+
Headlines36px regular, or 24px bold40+60+
Highest-contrast textWCAG AAA at body size70+80+
Functional elementsinput borders, focus rings, checkbox and toggle boundaries, icons, chart marks40+40+
Structural elementscard edges, separators, dividers — 10 and 20 in light, 70 and 60 in darkno minimum
Backgroundsthe ends of the ramp — in dark: cards grey-85, app background grey-95, wells grey-991-5 light · 85-99 dark
+
+

Why dark mode needs the bigger gaps. The same gap buys less perceptual contrast + at the dark end of the ramp: a 50-gap measures Lc 67 near the light end but only Lc 42 near the dark + end, and WCAG rates that dark pair higher (5.5:1 vs 4.6:1) — the discrepancy APCA exists to + catch. Dark answers don't mirror light ones either: text on tinted backgrounds needs 55 on the light + end but 40 on the dark end — a band further out than symmetry would suggest — and status accents + ride the 35 half-step on the grade-85 cards.

- More about A, AA, and AAA + The standards behind the numbers — WCAG and APCA
-

Always aim for AA. In some cases, A may be acceptable:

+

WCAG levels. Level A doesn't say anything about contrast; the minimums start at AA:

    -
  • A: Minimum level. Text must have a contrast ratio of at least 3:1 for large text (18pt/14pt bold or larger) and 4.5:1 for normal text against its background. This level is the basic requirement for accessibility, ensuring that text is readable for most users with mild visual impairments.
  • -
  • AA: Mid-range level. Text must have a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text. This is the recommended level for most web content and is required for legal compliance in many regions (such as Section 508 in the US).
  • -
  • AAA: Highest level. Text must have a contrast ratio of at least 7:1 for normal text and 4.5:1 for large text. This level is intended for content that needs to be accessible to the widest possible audience, including users with significant visual impairments. AAA is not required for most content, but is encouraged where possible.
  • +
  • AA (SC 1.4.3): at least 4.5:1 for normal text and 3:1 for large text (18pt, or 14pt bold). This is the level to hit for most web content, and it's what Section 508 requires in the US.
  • +
  • AAA (SC 1.4.6): at least 7:1 for normal text and 4.5:1 for large. Aim here when you want the widest possible audience; it's encouraged rather than required.
  • +
  • Non-text elements (SC 1.4.11, Level AA): anything functional — input borders, focus rings, checkbox and toggle boundaries, icons, chart marks — needs at least 3:1 against what's next to it. Purely structural pieces (card edges, separators, dividers) are exempt.
-

- Source: W3C: Understanding WCAG 2.1 Contrast (Minimum) -

-
-
-
- Will this change with WCAG 3.0? -
-

While WCAG 2.0 uses relative luminance to determine color contrast, WCAG 3.0 (potentially Dec 2025) is expected to use APCA (Accessible Perceptual Contrast Algorithm). - APCA is a new method for evaluating color contrast that takes into account human perception more effectively than previous methods.

-

The MyDataHelps color palette will be updated as necessary to comply with WCAG 3.0.

+

APCA. WCAG 2.x measures contrast as a ratio of relative luminance; APCA + (Accessible Perceptual Contrast Algorithm), drafted for WCAG 3, models how contrast actually + looks to people. The difference shows up most in dark mode, where the WCAG 2.x + math is too optimistic about dark-on-dark pairs. Every pairing here is measured under both, + taking the weaker of the two text/background directions, and APCA's text tiers need bigger gaps:

+
    +
  • Gap 60+: muted and large text (APCA floor Lc 45; palette worst case 47).
  • +
  • Gap 70+: body text (floor Lc 60; worst case 68).
  • +
  • Gap 80+: primary text (floor Lc 75; worst case 81).
  • +
+

WCAG 3 is still a draft and APCA isn't legally required yet, so the palette conforms to both: + WCAG 2.x ratios stay the hard floor, the APCA measurements add the perceptual picture on top, + and the dark-mode semantic tokens are built at the APCA gaps.

-

Usage

-

color: var(--ce-[hue]-[grade]) Example: color: var(--ce-blue-40)

-

UI Use Cases

-
    -
  • The 5 lightest (1-5) and darkest (95-99) grades are typically used as background colors.
  • -
  • The 55 grade is the lightest legible grade against backgrounds 1-5.
  • -
  • The 10 and 20 are typically used for border colors.
  • -
-

Data Visualization Use Cases

-
    -
  • Air Quality: Cyan
  • -
  • Sleep: Indigo
  • -
  • Heart rate: Red-Orange
  • -
  • Activity: Yellow
  • -
+

Tokens in context

+

The same card under each scheme. Read each row left to right: the sample on the left is drawn with + the token named on the right. Every color here is a semantic --mdhui-* token, so one set + of rules produces both cards; in dark mode those tokens map onto the grey ramp and brighter accent + grades, and npm run audit:colors checks every pairing against the role floors.

+ +

The signal rows are the data-visualization colors: heart rate on red-orange (glucose shares it), + activity on gold, sleep on indigo, air quality on teal.

+

The full ramp

+

Every hue, lightest to darkest. Hover a swatch and the ramp marks the closest + grade on either side that's far enough away to clear each band — so you can read a safe pairing + straight off the row:

+
+ 40+ · AA large text + 50+ · AA body text + 70+ · AAA +
+

The same hover shows what that color measures against white and black — the WCAG 2.x ratio and + the APCA Lc in both directions, each with the strongest role it supports. Click a + swatch to copy its variable.

{colors.map((hue) => ( ))}
+

References

+
    +
  1. U.S. Web Design System: color tokens — the grade-and-magic-number approach this system follows.
  2. +
  3. W3C: Understanding WCAG 2.1 — Contrast (Minimum) — the AA and AAA text floors.
  4. +
  5. W3C: Understanding WCAG 2.1 — Non-text Contrast — the 3:1 functional floor and the structural exemption.
  6. +
  7. APCA in a Nutshell — the perceptual contrast model and its Lc tiers.
  8. +
); }; From ef2b91890d988c74e647a4c177dc289089ada904 Mon Sep 17 00:00:00 2001 From: Summer Schrader Date: Fri, 7 Aug 2026 13:27:16 -0400 Subject: [PATCH 2/4] Dark mode fixes: brand-color scheme-toggle ordering, -mark signal tokens, 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 --- .../BasicPointsForBadges.tsx | 2 +- .../container/GlucoseChart/GlucoseChart.tsx | 2 +- .../GlucoseDayCoordinator.tsx | 2 +- .../presentational/Layout/Layout.tsx | 28 +++++++---------- .../ProgressBar/ProgressBar.tsx | 2 +- src/helpers/globalCss.ts | 30 ++++++++++--------- src/stories/foundation/colorDocs.tsx | 21 ++++++------- 7 files changed, 42 insertions(+), 45 deletions(-) diff --git a/src/components/container/BasicPointsForBadges/BasicPointsForBadges.tsx b/src/components/container/BasicPointsForBadges/BasicPointsForBadges.tsx index b4a100c04..5ed853ca4 100644 --- a/src/components/container/BasicPointsForBadges/BasicPointsForBadges.tsx +++ b/src/components/container/BasicPointsForBadges/BasicPointsForBadges.tsx @@ -94,7 +94,7 @@ export default function BasicPointsForBadges(props: BasicPointsForBadgesProps) { diff --git a/src/components/container/GlucoseDayCoordinator/GlucoseDayCoordinator.tsx b/src/components/container/GlucoseDayCoordinator/GlucoseDayCoordinator.tsx index 1925cbb78..e49499869 100644 --- a/src/components/container/GlucoseDayCoordinator/GlucoseDayCoordinator.tsx +++ b/src/components/container/GlucoseDayCoordinator/GlucoseDayCoordinator.tsx @@ -43,7 +43,7 @@ export default function (props: GlucoseDayCoordinatorProps) { const dayRenderer = (dayKey: string): React.JSX.Element | null => { let sparkRanges: SparkRangeChartRange[] = []; if (glucoseRangeLookup?.hasOwnProperty(dayKey)) { - sparkRanges.push({ ...(glucoseRangeLookup[dayKey]), color: 'var(--mdhui-color-glucose-text)' }); + sparkRanges.push({ ...(glucoseRangeLookup[dayKey]), color: 'var(--mdhui-color-glucose-mark)' }); } return
diff --git a/src/components/presentational/Layout/Layout.tsx b/src/components/presentational/Layout/Layout.tsx index b689a7205..eedf00d0e 100644 --- a/src/components/presentational/Layout/Layout.tsx +++ b/src/components/presentational/Layout/Layout.tsx @@ -58,26 +58,20 @@ export default function Layout(props: LayoutProps) { let paddingBottom = props.flex ? "0" : "env(safe-area-inset-bottom)"; + // One Global for scheme + brand: Emotion orders tags by mount order, so a runtime + // scheme toggle would land the scheme tag after a separate brand tag and clobber it. + // -text gets the brand too, so a custom fill never pairs with the stock foreground. + const schemeStyles = context.colorScheme == "dark" ? darkColorScheme : lightColorScheme; + const brandStyles = props.primaryColor ? css` + :root { + --mdhui-color-primary: ${resolveColor(colorScheme, props.primaryColor)}; + --mdhui-color-primary-text: ${resolveColor(colorScheme, props.primaryColor)}; + }` : undefined; + return ( - {context.colorScheme == "dark" && - - } - {context.colorScheme == "light" && - - } - {/* after the scheme styles: both write --mdhui-color-primary at :root, so a - customer's color only survives if it lands last. -text carries it too, since - the scheme layer would otherwise pair a custom fill with the stock foreground. */} - {props.primaryColor && - - } + {!props.noGlobalStyles && } diff --git a/src/components/presentational/ProgressBar/ProgressBar.tsx b/src/components/presentational/ProgressBar/ProgressBar.tsx index cee88cc2b..6f59f3c43 100644 --- a/src/components/presentational/ProgressBar/ProgressBar.tsx +++ b/src/components/presentational/ProgressBar/ProgressBar.tsx @@ -21,7 +21,7 @@ export interface ProgressBarProps { ProgressBar.defaultProps = { backgroundColor: "var(--mdhui-background-color-0)", - fillColor: "var(--mdhui-color-primary)", + fillColor: "var(--mdhui-color-primary-text)", fillPercent: 0 } diff --git a/src/helpers/globalCss.ts b/src/helpers/globalCss.ts index fae7329d6..e33a37285 100644 --- a/src/helpers/globalCss.ts +++ b/src/helpers/globalCss.ts @@ -613,19 +613,21 @@ export const lightColorStyle: CSSProperties = { '--mdhui-text-color-3': '#999', '--mdhui-text-color-4': '#bbb', - /* Signal colors: base token = marks (bars/pins/dots); "-text" = the signal as + /* 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 - the hexes components previously hardcoded; dark overrides. */ - '--mdhui-color-glucose': '#c4291c', + one canonical hex per signal, picked from the values components hardcoded — where + components disagreed (GlucoseStats' #d36540/#8287bb), they shift to it; dark + overrides. */ + '--mdhui-color-glucose-mark': '#c4291c', '--mdhui-color-glucose-text': '#c4291c', - '--mdhui-color-heart-rate': '#e35c33', - '--mdhui-color-activity': '#f5b722', + '--mdhui-color-heart-rate-mark': '#e35c33', + '--mdhui-color-activity-mark': '#f5b722', '--mdhui-color-activity-text': '#f5b722', - '--mdhui-color-sleep': '#7b88c6', + '--mdhui-color-sleep-mark': '#7b88c6', '--mdhui-color-sleep-text': '#7b88c6', // No shipped hex to mirror — this signal is new, so it comes from the ramp. Grade 55 // is the lightest teal that still clears AA on the 1-5 backgrounds. - '--mdhui-color-air-quality': 'var(--mdh-teal-55)', + '--mdhui-color-air-quality-mark': 'var(--mdh-teal-55)', '--mdhui-box-shadow-color-0': 'rgba(0, 0, 0, 0.05)', '--mdhui-box-shadow-color-1': '#aaa', @@ -659,16 +661,16 @@ export const darkColorStyle: CSSProperties = { '--mdhui-text-color-3': 'var(--mdh-grey-30)', '--mdhui-text-color-4': 'var(--mdh-grey-40)', - '--mdhui-color-glucose': 'var(--mdh-red-orange-40)', - // grade 30 rather than the mark's 40: as foreground on grey-80 the 40 lands at Lc 41, - // well under what APCA wants for text, while 30 reaches Lc 59. + '--mdhui-color-glucose-mark': 'var(--mdh-red-orange-40)', + // grade 35 rather than the mark's 40: as foreground on the grey-85 cards the 40 lands + // at Lc 43, under what APCA wants for text, while 35 reaches Lc 51. '--mdhui-color-glucose-text': 'var(--mdh-red-orange-35)', - '--mdhui-color-heart-rate': 'var(--mdh-red-orange-40)', - '--mdhui-color-activity': 'var(--mdh-gold-20)', + '--mdhui-color-heart-rate-mark': 'var(--mdh-red-orange-40)', + '--mdhui-color-activity-mark': 'var(--mdh-gold-20)', '--mdhui-color-activity-text': 'var(--mdh-gold-20)', - '--mdhui-color-sleep': 'var(--mdh-indigo-40)', + '--mdhui-color-sleep-mark': 'var(--mdh-indigo-40)', '--mdhui-color-sleep-text': 'var(--mdh-indigo-35)', - '--mdhui-color-air-quality': 'var(--mdh-teal-35)', + '--mdhui-color-air-quality-mark': 'var(--mdh-teal-35)', // Accent base tokens keep the light-mode fill grades (fills carry white text); // "-text" = foreground-tuned grades for links/status text. diff --git a/src/stories/foundation/colorDocs.tsx b/src/stories/foundation/colorDocs.tsx index b2cd000a7..4fe927184 100644 --- a/src/stories/foundation/colorDocs.tsx +++ b/src/stories/foundation/colorDocs.tsx @@ -343,17 +343,18 @@ const Ref: React.FC<{ n: number }> = ({ n }) => ( ); // Scoping the real style objects per column means the preview can't drift from the tokens. -// Base token = fills, -text = the signal as foreground; heart-rate and air-quality have -// no -text token and fall back to the mark color. Ink is per value: air quality's light -// grade needs white where every other fill takes near-black. +// "-mark" = the signal as chart marks, "-text" = as foreground; heart-rate and air-quality have +// no -text token and fall back to the mark color. Ink is per value: glucose's and air +// quality's light values need white where the other fills take near-black. const DARK_INK = '#12151b'; const LIGHT_INK = '#fff'; type SignalSwatch = { value: string; ink: string }; -const SIGNALS: { name: string; fill: string; text: string; light: SignalSwatch; dark: SignalSwatch }[] = [ - { name: 'Sleep', fill: '--mdhui-color-sleep', text: '--mdhui-color-sleep-text', light: { value: '#7b88c6', ink: DARK_INK }, dark: { value: 'indigo-40', ink: DARK_INK } }, - { name: 'Heart rate', fill: '--mdhui-color-heart-rate', text: '--mdhui-color-heart-rate', light: { value: '#e35c33', ink: DARK_INK }, dark: { value: 'red-orange-40', ink: DARK_INK } }, - { name: 'Activity', fill: '--mdhui-color-activity', text: '--mdhui-color-activity-text', light: { value: '#f5b722', ink: DARK_INK }, dark: { value: 'gold-20', ink: DARK_INK } }, - { name: 'Air quality', fill: '--mdhui-color-air-quality', text: '--mdhui-color-air-quality', light: { value: 'teal-55', ink: LIGHT_INK }, dark: { value: 'teal-30', ink: DARK_INK } }, +const SIGNALS: { name: string; mark: string; text: string; light: SignalSwatch; dark: SignalSwatch }[] = [ + { name: 'Glucose', mark: '--mdhui-color-glucose-mark', text: '--mdhui-color-glucose-text', light: { value: '#c4291c', ink: LIGHT_INK }, dark: { value: 'red-orange-40', ink: DARK_INK } }, + { name: 'Sleep', mark: '--mdhui-color-sleep-mark', text: '--mdhui-color-sleep-text', light: { value: '#7b88c6', ink: DARK_INK }, dark: { value: 'indigo-40', ink: DARK_INK } }, + { name: 'Heart rate', mark: '--mdhui-color-heart-rate-mark', text: '--mdhui-color-heart-rate-mark', light: { value: '#e35c33', ink: DARK_INK }, dark: { value: 'red-orange-40', ink: DARK_INK } }, + { name: 'Activity', mark: '--mdhui-color-activity-mark', text: '--mdhui-color-activity-text', light: { value: '#f5b722', ink: DARK_INK }, dark: { value: 'gold-20', ink: DARK_INK } }, + { name: 'Air quality', mark: '--mdhui-color-air-quality-mark', text: '--mdhui-color-air-quality-mark', light: { value: 'teal-55', ink: LIGHT_INK }, dark: { value: 'teal-35', ink: DARK_INK } }, ]; // Every sample sits in one column and its token name in the gutter beside it, so the @@ -393,14 +394,14 @@ const TokenContextCard: React.FC<{ scheme: 'light' | 'dark' }> = ({ scheme }) => 3 due
- signals — name in its text color, fill shows its value + signals — name in its text color, mark shows its value {SIGNALS.map(signal => { const swatch = scheme === 'light' ? signal.light : signal.dark; return (
{signal.name}
{swatch.value}
From c3698e33cf06c3916796041d208033000046e11b Mon Sep 17 00:00:00 2001 From: Summer Schrader Date: Fri, 7 Aug 2026 23:26:16 -0400 Subject: [PATCH 3/4] Address Copilot review: make the color audit honest, fix the chart var() typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- scripts/color-audit.mjs | 46 ++++++++++++++++++++++------ src/helpers/chartHelpers.tsx | 2 +- src/stories/foundation/colorDocs.tsx | 9 ++++-- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/scripts/color-audit.mjs b/scripts/color-audit.mjs index 0d819e1ec..abcaf934b 100644 --- a/scripts/color-audit.mjs +++ b/scripts/color-audit.mjs @@ -24,17 +24,37 @@ const src = readFileSync(GLOBALCSS, 'utf8'); const ramp = {}; for (const m of src.matchAll(/(--mdh-[a-z0-9-]+)\s*:\s*(#[0-9A-Fa-f]{3,8})/g)) ramp[m[1]] = m[2]; -// ---- resolve a semantic value to a hex (follows one var(--mdh-*) hop; raw hex passes through) ---- -function resolve(val) { - val = val.trim(); +// ---- resolve a semantic value to a hex (follows var() hops through --mdhui aliases and +// into the --mdh ramp; raw hex passes through) ---- +function resolve(val, toks = {}, depth = 0) { + val = (val ?? '').trim(); + if (depth > 4) return null; // cycle guard const v = val.match(/^var\((--[a-z0-9-]+)\)$/); - if (v) return ramp[v[1]] ?? null; + if (v) { + if (ramp[v[1]]) return ramp[v[1]]; + if (toks[v[1]]) return resolve(toks[v[1]], toks, depth + 1); + return null; + } if (/^#[0-9A-Fa-f]{3,8}$/.test(val)) return val; if (/^rgb/i.test(val)) return val; return null; // gradients / nested / unresolvable } -// ---- parse a semantic style object (lightColorStyle / darkColorStyle) ---- +// ---- parse the :root declarations in `core` ---- +// The accent bases and their -text defaults are declared once here, and a scheme block only +// overrides what it changes: dark redefines them, light inherits. Reading core underneath the +// scheme is what makes the light audit see accents at all. +function coreTokens() { + const start = src.indexOf('export const core'); + if (start < 0) throw new Error('core not found'); + const next = src.indexOf('export const', start + 'export const core'.length); + const block = src.slice(start, next < 0 ? src.length : next); + const toks = {}; + for (const m of block.matchAll(/(--mdhui-[a-z0-9-]+)\s*:\s*([^;]+);/g)) toks[m[1]] = m[2].trim(); + return toks; +} + +// ---- parse a semantic style object (lightColorStyle / darkColorStyle), over core ---- function semanticTokens(scheme) { const name = scheme === 'dark' ? 'darkColorStyle' : 'lightColorStyle'; const start = src.indexOf(`export const ${name}`); @@ -45,7 +65,7 @@ function semanticTokens(scheme) { const block = src.slice(open, end); const toks = {}; for (const m of block.matchAll(/'(--mdhui-[a-z0-9-]+)'\s*:\s*'([^']+)'/g)) toks[m[1]] = m[2]; - return toks; + return { ...coreTokens(), ...toks }; } // ---- metrics ---- @@ -79,12 +99,18 @@ const MARK_FLOORS = { wcag: 3.0, apca: 15 }; function auditSemantic(scheme, ci) { const t = semanticTokens(scheme); - const hex = k => (k.startsWith('#') ? k : resolve(t[k] ?? '')); + const hex = k => (k.startsWith('#') ? k : resolve(t[k] ?? '', t)); const rows = []; let fails = 0; 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; } + // An unresolved token fails rather than skipping: a renamed or misspelled variable would + // otherwise drop its pair from the audit silently, and the run would still report PASS. + if (!fH || !bH) { + if (!exempt) fails++; + rows.push({ label, note: `unresolved (${t[fg]} / ${t[bg]})${exempt ? '' : ' — FAIL'}`, skip: true }); + return; + } const w = wcag(fH, bH), lc = Math.abs(apca(fH, bH)); const passW = exempt || w >= wFloor, passA = exempt || lc >= aFloor; const pass = passW && passA; @@ -108,7 +134,9 @@ const pushPair = (label, fg, bg, wFloor, aFloor, exempt) => { } 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); + // -mark is required for every signal, so a missing one fails. -text is optional: + // heart-rate and air-quality intentionally ship none and fall back to the mark color. + pushPair(`${g}-mark on ${bgName(s)}`, `--mdhui-color-${g}-mark`, 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); } } diff --git a/src/helpers/chartHelpers.tsx b/src/helpers/chartHelpers.tsx index 5aff160f8..6daf8b0a5 100644 --- a/src/helpers/chartHelpers.tsx +++ b/src/helpers/chartHelpers.tsx @@ -55,7 +55,7 @@ export function createLineChartDefs( return {series.map((s, i) => { - let lineColor = colorOrDefault(s.color, "var(--mdhui-color-primary-text"); + let lineColor = colorOrDefault(s.color, "var(--mdhui-color-primary-text)"); return {createStopsFromThresholds(lineColor, options?.thresholds, dataKeys[i])} diff --git a/src/stories/foundation/colorDocs.tsx b/src/stories/foundation/colorDocs.tsx index 4fe927184..65b15ac2a 100644 --- a/src/stories/foundation/colorDocs.tsx +++ b/src/stories/foundation/colorDocs.tsx @@ -519,10 +519,13 @@ const ColorDocs: React.FC = () => { math is too optimistic about dark-on-dark pairs. Every pairing here is measured under both, taking the weaker of the two text/background directions, and APCA's text tiers need bigger gaps:

    -
  • Gap 60+: muted and large text (APCA floor Lc 45; palette worst case 47).
  • -
  • Gap 70+: body text (floor Lc 60; worst case 68).
  • -
  • Gap 80+: primary text (floor Lc 75; worst case 81).
  • +
  • Gap 60+ clears Lc 45, APCA's floor for large or secondary text (palette worst case 47).
  • +
  • Gap 70+ clears Lc 60, its floor for body text (worst case 68).
  • +
  • Gap 80+ clears Lc 75, its preferred level for body text (worst case 81).
+

Those are APCA's own minimums. The table above is deliberately a step stricter for running + text — it puts body text at gap 80 rather than the Lc 60 floor, so the default sits with + headroom instead of right on the line.

WCAG 3 is still a draft and APCA isn't legally required yet, so the palette conforms to both: WCAG 2.x ratios stay the hard floor, the APCA measurements add the perceptual picture on top, and the dark-mode semantic tokens are built at the APCA gaps.

From 376527e42ce26d8665a389c97ad1c60f1317c299 Mon Sep 17 00:00:00 2001 From: Summer Schrader Date: Mon, 10 Aug 2026 20:06:07 -0400 Subject: [PATCH 4/4] Review round 2: brand fills-only in dark, heart-rate-text, audit honesty, duty fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../presentational/Layout/Layout.test.tsx | 52 +++++++++++++++++++ scripts/color-audit.mjs | 30 +++++++---- .../container/DailyDataGoal/DailyDataGoal.tsx | 8 ++- .../ActivityMeter/ActivityMeter.css | 2 +- .../presentational/Layout/Layout.stories.tsx | 29 ++++++++++- .../presentational/Layout/Layout.tsx | 12 +++-- src/helpers/globalCss.ts | 3 ++ src/stories/foundation/colorDocs.css | 2 +- src/stories/foundation/colorDocs.tsx | 37 ++++++++----- 9 files changed, 145 insertions(+), 30 deletions(-) create mode 100644 __tests__/components/presentational/Layout/Layout.test.tsx diff --git a/__tests__/components/presentational/Layout/Layout.test.tsx b/__tests__/components/presentational/Layout/Layout.test.tsx new file mode 100644 index 000000000..ed39f77b7 --- /dev/null +++ b/__tests__/components/presentational/Layout/Layout.test.tsx @@ -0,0 +1,52 @@ +/** + * @jest-environment jsdom + */ +import React from 'react'; +import Layout from '../../../../src/components/presentational/Layout/Layout'; +import { act, render, cleanup } from '@testing-library/react'; + +jest.mock('@careevolution/mydatahelps-js', () => ({ + __esModule: true, + default: { setStatusBarStyle: jest.fn(), on: jest.fn(), off: jest.fn() } +})); + +describe('Layout brand color tokens', () => { + // Emotion runs in speedy mode here, so injected CSS lives in CSSOM rules rather than + // the style tags' text. Reading the rules back is the observable behavior — which + // tokens the brand drives per scheme. + const globalCss = () => + Array.from(document.querySelectorAll('style')) + .flatMap(s => s.sheet ? Array.from(s.sheet.cssRules).map(r => r.cssText) : []) + .join('\n'); + + const renderLayout = async (element: React.ReactElement) => act(async () => { render(element); }); + + afterEach(() => { + cleanup(); + document.querySelectorAll('style').forEach(s => s.remove()); + }); + + it('drives fill and foreground in light', async () => { + await renderLayout(); + expect(globalCss()).toContain('--mdhui-color-primary: #00693E'); + expect(globalCss()).toContain('--mdhui-color-primary-text: #00693E'); + }); + + it('drives only the fill in dark for a plain-string brand', async () => { + await renderLayout(); + expect(globalCss()).toContain('--mdhui-color-primary: #00693E'); + expect(globalCss()).not.toContain('--mdhui-color-primary-text: #00693E'); + }); + + it('puts an explicit dark value on fills only — dark links keep the scheme grade', async () => { + await renderLayout(); + expect(globalCss()).toContain('--mdhui-color-primary: #00B068'); + expect(globalCss()).not.toContain('--mdhui-color-primary-text: #00B068'); + }); + + it('treats an object without a dark value the same way in dark', async () => { + await renderLayout(); + expect(globalCss()).toContain('--mdhui-color-primary: #00693E'); + expect(globalCss()).not.toContain('--mdhui-color-primary-text: #00693E'); + }); +}); diff --git a/scripts/color-audit.mjs b/scripts/color-audit.mjs index abcaf934b..d51fdbca7 100644 --- a/scripts/color-audit.mjs +++ b/scripts/color-audit.mjs @@ -41,16 +41,20 @@ function resolve(val, toks = {}, depth = 0) { } // ---- parse the :root declarations in `core` ---- -// The accent bases and their -text defaults are declared once here, and a scheme block only -// overrides what it changes: dark redefines them, light inherits. Reading core underneath the -// scheme is what makes the light audit see accents at all. +// Accents are declared once in core's :root; dark overrides them, light inherits. Without +// core, the light audit would never see an accent. Scan only :root blocks — core also holds +// @keyframes and @media blocks, and a media-conditional value must not beat the :root one. function coreTokens() { const start = src.indexOf('export const core'); if (start < 0) throw new Error('core not found'); const next = src.indexOf('export const', start + 'export const core'.length); const block = src.slice(start, next < 0 ? src.length : next); const toks = {}; - for (const m of block.matchAll(/(--mdhui-[a-z0-9-]+)\s*:\s*([^;]+);/g)) toks[m[1]] = m[2].trim(); + for (const m of block.matchAll(/:root\s*\{/g)) { + let depth = 1, i = m.index + m[0].length; const from = i; + for (; i < block.length && depth > 0; i++) { if (block[i] === '{') depth++; else if (block[i] === '}') depth--; } + for (const d of block.slice(from, i - 1).matchAll(/(--mdhui-[a-z0-9-]+)\s*:\s*([^;]+);/g)) toks[d[1]] = d[2].trim(); + } return toks; } @@ -91,6 +95,10 @@ const BORDERS = ['--mdhui-border-color-0', '--mdhui-border-color-1', '--mdhui-bo // -text — the same role as foreground (links, status text, icons that must be seen). const ACCENTS = ['primary', 'success', 'warning', 'danger']; const SIGNALS = ['glucose', 'heart-rate', 'activity', 'sleep', 'air-quality']; +// Signals whose -mark doubles as their foreground color. Named explicitly (not probed for) +// so a typo'd -text token can't skip its own check; the mark is then audited at the +// foreground floors, keeping "the mark can carry text" honest. +const TEXT_VIA_MARK = new Set(['air-quality']); const FILL_INK = '#fff'; // what the accent fills carry // -text is used at many sizes/weights (links, 15px/600 headings, status text): WCAG normal-text // floor plus the APCA muted/large tier. Aim higher for small body-size foregrounds. @@ -104,8 +112,10 @@ function auditSemantic(scheme, ci) { let fails = 0; const pushPair = (label, fg, bg, wFloor, aFloor, exempt) => { const fH = hex(fg), bH = hex(bg); - // An unresolved token fails rather than skipping: a renamed or misspelled variable would - // otherwise drop its pair from the audit silently, and the run would still report PASS. + // A token that doesn't resolve FAILS instead of skipping — otherwise a rename would + // silently drop its checks and the run would still say PASS. Exempt pairs only report, + // so a renamed border token still slips through (text-color-4 would too, but the + // distinct34 check below catches it). if (!fH || !bH) { if (!exempt) fails++; rows.push({ label, note: `unresolved (${t[fg]} / ${t[bg]})${exempt ? '' : ' — FAIL'}`, skip: true }); @@ -134,10 +144,12 @@ const pushPair = (label, fg, bg, wFloor, aFloor, exempt) => { } for (const g of SIGNALS) { for (const s of SURFACES) { - // -mark is required for every signal, so a missing one fails. -text is optional: - // heart-rate and air-quality intentionally ship none and fall back to the mark color. + // Every signal is checked for both duties; nothing opts out of the foreground floor. pushPair(`${g}-mark on ${bgName(s)}`, `--mdhui-color-${g}-mark`, 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); + if (TEXT_VIA_MARK.has(g)) + pushPair(`${g}-mark as text on ${bgName(s)}`, `--mdhui-color-${g}-mark`, s, FOREGROUND_FLOORS.wcag, FOREGROUND_FLOORS.apca, false); + else + pushPair(`${g}-text on ${bgName(s)}`, `--mdhui-color-${g}-text`, s, FOREGROUND_FLOORS.wcag, FOREGROUND_FLOORS.apca, false); } } // adjacent elevation surfaces — target Lc 15 (or border-assisted; reported, not gating) diff --git a/src/components/container/DailyDataGoal/DailyDataGoal.tsx b/src/components/container/DailyDataGoal/DailyDataGoal.tsx index 5fba67fcb..a4c74f4a4 100644 --- a/src/components/container/DailyDataGoal/DailyDataGoal.tsx +++ b/src/components/container/DailyDataGoal/DailyDataGoal.tsx @@ -41,14 +41,20 @@ export default function (props: DailyDataGoalProps) { }); }, ["externalAccountSyncComplete"], [dateRangeContext?.intervalStart]); + // The message is text, but the circle fill renders at opacity .3 over a light track, + // where the light -text grades wash out — so the fill defaults to the stronger base + // accent. An explicit color prop still drives both. const goalCompleteColor = props.goalCompleteColor || "var(--mdhui-color-success-text)"; const goalIncompleteColor = props.goalIncompleteColor || "var(--mdhui-color-primary-text)"; + const goalCompleteFill = props.goalCompleteColor || "var(--mdhui-color-success)"; + const goalIncompleteFill = props.goalIncompleteColor || "var(--mdhui-color-primary)"; let goalProgress: number | null = dailyValue == null ? null : dailyValue / props.goal; if (goalProgress != null && goalProgress > 1) goalProgress = 1; const formattedProgress = goalProgress == null ? "0%" : Math.round(goalProgress * 100) + "%"; const color = goalProgress === 1 ? goalCompleteColor : goalIncompleteColor; + const fillColor = goalProgress === 1 ? goalCompleteFill : goalIncompleteFill; let message: string | undefined = undefined; if (props.messages && dailyValue != null) { @@ -63,7 +69,7 @@ export default function (props: DailyDataGoalProps) { return
-
+
{goalProgress === 1 && diff --git a/src/components/presentational/ActivityMeter/ActivityMeter.css b/src/components/presentational/ActivityMeter/ActivityMeter.css index 72b34b361..c0ab3d780 100644 --- a/src/components/presentational/ActivityMeter/ActivityMeter.css +++ b/src/components/presentational/ActivityMeter/ActivityMeter.css @@ -50,7 +50,7 @@ .mdhui-activity-meter .mdhui-activity-meter-meter .mdhui-activity-meter-fill { height: 16px; - background: var(--mdhui-color-primary); + background: var(--mdhui-color-primary-text); position: relative; border-radius: 8px; } diff --git a/src/components/presentational/Layout/Layout.stories.tsx b/src/components/presentational/Layout/Layout.stories.tsx index 067475a4d..1898b2e8d 100644 --- a/src/components/presentational/Layout/Layout.stories.tsx +++ b/src/components/presentational/Layout/Layout.stories.tsx @@ -3,6 +3,7 @@ import Layout from "../Layout"; import { LayoutProps } from "./Layout"; import TextBlock from "../TextBlock"; import Card from "../Card"; +import Button from "../Button"; import { Meta, StoryObj } from "@storybook/react"; const meta: Meta = { @@ -69,4 +70,30 @@ export const CustomBackgroundLightDark: Story = { bodyBackgroundColor: { darkMode: "#23395d", lightMode: "#ffffff" } }, render: render -}; \ No newline at end of file +}; +// Both duties a brand color can drive, using real components: the default Button is a fill +// (brand background, white text), the subtle Button is foreground (brand-colored text). +const brandRender = (args: LayoutProps) => + + + + + + +; + +export const BrandColor: Story = { + args: { + colorScheme: "auto", + primaryColor: "#00693E" + }, + render: brandRender +}; + +export const BrandColorLightDark: Story = { + args: { + colorScheme: "auto", + primaryColor: { lightMode: "#00693E", darkMode: "#00B068" } + }, + render: brandRender +}; diff --git a/src/components/presentational/Layout/Layout.tsx b/src/components/presentational/Layout/Layout.tsx index eedf00d0e..46e2bafa4 100644 --- a/src/components/presentational/Layout/Layout.tsx +++ b/src/components/presentational/Layout/Layout.tsx @@ -60,12 +60,16 @@ export default function Layout(props: LayoutProps) { // One Global for scheme + brand: Emotion orders tags by mount order, so a runtime // scheme toggle would land the scheme tag after a separate brand tag and clobber it. - // -text gets the brand too, so a custom fill never pairs with the stock foreground. const schemeStyles = context.colorScheme == "dark" ? darkColorScheme : lightColorScheme; - const brandStyles = props.primaryColor ? css` + // The brand color renders exactly as given; the library never alters it. In light it + // drives both tokens, as it always has. In dark it drives only fills, and links keep + // the scheme's readable grade: no single dark value can be both a fill under white + // text and legible text on the card, so text needs its own color, not this one. + const brand = props.primaryColor ? resolveColor(colorScheme, props.primaryColor) : undefined; + const brandStyles = brand ? css` :root { - --mdhui-color-primary: ${resolveColor(colorScheme, props.primaryColor)}; - --mdhui-color-primary-text: ${resolveColor(colorScheme, props.primaryColor)}; + --mdhui-color-primary: ${brand}; + ${colorScheme === "light" ? `--mdhui-color-primary-text: ${brand};` : ""} }` : undefined; return ( diff --git a/src/helpers/globalCss.ts b/src/helpers/globalCss.ts index e33a37285..bff62cfb0 100644 --- a/src/helpers/globalCss.ts +++ b/src/helpers/globalCss.ts @@ -621,6 +621,7 @@ export const lightColorStyle: CSSProperties = { '--mdhui-color-glucose-mark': '#c4291c', '--mdhui-color-glucose-text': '#c4291c', '--mdhui-color-heart-rate-mark': '#e35c33', + '--mdhui-color-heart-rate-text': '#e35c33', '--mdhui-color-activity-mark': '#f5b722', '--mdhui-color-activity-text': '#f5b722', '--mdhui-color-sleep-mark': '#7b88c6', @@ -666,6 +667,8 @@ export const darkColorStyle: CSSProperties = { // at Lc 43, under what APCA wants for text, while 35 reaches Lc 51. '--mdhui-color-glucose-text': 'var(--mdh-red-orange-35)', '--mdhui-color-heart-rate-mark': 'var(--mdh-red-orange-40)', + // grade 35 like glucose-text: the mark's 40 falls just under the Lc 45 text floor on cards. + '--mdhui-color-heart-rate-text': 'var(--mdh-red-orange-35)', '--mdhui-color-activity-mark': 'var(--mdh-gold-20)', '--mdhui-color-activity-text': 'var(--mdh-gold-20)', '--mdhui-color-sleep-mark': 'var(--mdh-indigo-40)', diff --git a/src/stories/foundation/colorDocs.css b/src/stories/foundation/colorDocs.css index ace9b650e..fa75c351b 100644 --- a/src/stories/foundation/colorDocs.css +++ b/src/stories/foundation/colorDocs.css @@ -304,4 +304,4 @@ td, th { .tokensCtx-caption { font: 700 9.5px ui-monospace, monospace; text-transform: uppercase; letter-spacing: .05em; color: var(--mdhui-text-color-3); margin: 2px 0 4px; } .tokensCtx-grid { display: grid; grid-template-columns: 110px 1fr; gap: 10px; align-items: center; padding: 3px 0; } .tokensCtx-track { background: var(--mdhui-background-color-2); border-radius: 99px; height: 8px; } -.tokensCtx-fill { width: 45%; height: 8px; border-radius: 99px; background: var(--mdhui-color-primary); } +.tokensCtx-fill { width: 45%; height: 8px; border-radius: 99px; background: var(--mdhui-color-primary-text); } diff --git a/src/stories/foundation/colorDocs.tsx b/src/stories/foundation/colorDocs.tsx index 65b15ac2a..7445d22ba 100644 --- a/src/stories/foundation/colorDocs.tsx +++ b/src/stories/foundation/colorDocs.tsx @@ -343,16 +343,16 @@ const Ref: React.FC<{ n: number }> = ({ n }) => ( ); // Scoping the real style objects per column means the preview can't drift from the tokens. -// "-mark" = the signal as chart marks, "-text" = as foreground; heart-rate and air-quality have -// no -text token and fall back to the mark color. Ink is per value: glucose's and air -// quality's light values need white where the other fills take near-black. +// -mark = chart marks, -text = foreground. Air quality alone has no -text — its mark doubles +// as text, and the audit checks it at the text floors. Ink is per value: glucose and air +// quality need white in light where the other fills take near-black. const DARK_INK = '#12151b'; const LIGHT_INK = '#fff'; type SignalSwatch = { value: string; ink: string }; const SIGNALS: { name: string; mark: string; text: string; light: SignalSwatch; dark: SignalSwatch }[] = [ { name: 'Glucose', mark: '--mdhui-color-glucose-mark', text: '--mdhui-color-glucose-text', light: { value: '#c4291c', ink: LIGHT_INK }, dark: { value: 'red-orange-40', ink: DARK_INK } }, { name: 'Sleep', mark: '--mdhui-color-sleep-mark', text: '--mdhui-color-sleep-text', light: { value: '#7b88c6', ink: DARK_INK }, dark: { value: 'indigo-40', ink: DARK_INK } }, - { name: 'Heart rate', mark: '--mdhui-color-heart-rate-mark', text: '--mdhui-color-heart-rate-mark', light: { value: '#e35c33', ink: DARK_INK }, dark: { value: 'red-orange-40', ink: DARK_INK } }, + { name: 'Heart rate', mark: '--mdhui-color-heart-rate-mark', text: '--mdhui-color-heart-rate-text', light: { value: '#e35c33', ink: DARK_INK }, dark: { value: 'red-orange-40', ink: DARK_INK } }, { name: 'Activity', mark: '--mdhui-color-activity-mark', text: '--mdhui-color-activity-text', light: { value: '#f5b722', ink: DARK_INK }, dark: { value: 'gold-20', ink: DARK_INK } }, { name: 'Air quality', mark: '--mdhui-color-air-quality-mark', text: '--mdhui-color-air-quality-mark', light: { value: 'teal-55', ink: LIGHT_INK }, dark: { value: 'teal-35', ink: DARK_INK } }, ]; @@ -392,7 +392,7 @@ const TokenContextCard: React.FC<{ scheme: 'light' | 'dark' }> = ({ scheme }) => Overdue Log reading 3 due -
+
signals — name in its text color, mark shows its value {SIGNALS.map(signal => { @@ -519,13 +519,21 @@ const ColorDocs: React.FC = () => { math is too optimistic about dark-on-dark pairs. Every pairing here is measured under both, taking the weaker of the two text/background directions, and APCA's text tiers need bigger gaps:

    -
  • Gap 60+ clears Lc 45, APCA's floor for large or secondary text (palette worst case 47).
  • -
  • Gap 70+ clears Lc 60, its floor for body text (worst case 68).
  • -
  • Gap 80+ clears Lc 75, its preferred level for body text (worst case 81).
  • +
  • Gap 60+ clears Lc 45 — APCA's minimum for large, heavy text such as headlines: 36px regular or 24px bold (ramp floor 49.3).
  • +
  • Gap 70+ clears Lc 60 — its minimum for content text that is not body, column, or block text (floor 70.9).
  • +
  • Gap 80+ clears Lc 75 — its minimum for columns of body text, at fonts no smaller than 24px/300, 18px/400, 16px/500 or 14px/700 (floor 81.5).
-

Those are APCA's own minimums. The table above is deliberately a step stricter for running - text — it puts body text at gap 80 rather than the Lc 60 floor, so the default sits with - headroom instead of right on the line.

+

Those are APCA's minimums, and the text rows of the table above derive from them — read + that column as a floor, never as headroom. One caveat the table understates: its body row spans + “16–18px regular”, but Lc 75 covers 18px regular and 16px medium; + 16px regular falls into APCA's Lc 90 tier, which no gap in the table reaches. Set body copy at + 18px, or carry 16px at medium weight.

+

Lc 90 is APCA's preferred level for running text, and widening the gap is not a reliable way + to reach it: the guarantee is not monotone in gap size. Gap 82 clears Lc 90, but 85 and 89 do + not (both 84.0). Individual pairs go well past it — white on a dark card measures + Lc 102 in the weaker direction — so measure the pair you are actually using rather than + reasoning from the gap. node scripts/color-audit.mjs ramp --json prints the + guaranteed floor at every gap.

WCAG 3 is still a draft and APCA isn't legally required yet, so the palette conforms to both: WCAG 2.x ratios stay the hard floor, the APCA measurements add the perceptual picture on top, and the dark-mode semantic tokens are built at the APCA gaps.

@@ -537,8 +545,11 @@ const ColorDocs: React.FC = () => { of rules produces both cards; in dark mode those tokens map onto the grey ramp and brighter accent grades, and npm run audit:colors checks every pairing against the role floors.

-

The signal rows are the data-visualization colors: heart rate on red-orange (glucose shares it), - activity on gold, sleep on indigo, air quality on teal.

+

The signal rows are the data-visualization colors: glucose and heart rate on red-orange, + activity on gold, sleep on indigo, air quality on teal. Glucose and heart rate resolve to the + same grade in dark for both duties, and in light they sit only Lc 12 apart — under the Lc 15 + this page asks of chart marks. Don't separate them by color alone in either scheme; use shape, + position, or a label.

The full ramp

Every hue, lightest to darkest. Hover a swatch and the ramp marks the closest grade on either side that's far enough away to clear each band — so you can read a safe pairing