diff --git a/ui/test/disk-health-contrast.test.mjs b/ui/test/disk-health-contrast.test.mjs new file mode 100644 index 00000000..08978e7b --- /dev/null +++ b/ui/test/disk-health-contrast.test.mjs @@ -0,0 +1,37 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; + +const page = fs.readFileSync(new URL('../widgets/disk-health/index.html', import.meta.url), 'utf8'); + +const luminance = rgb => + rgb + .map(v => v / 255) + .map(v => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4)) + .reduce((sum, v, i) => sum + v * [0.2126, 0.7152, 0.0722][i], 0); +const hex = h => [1, 3, 5].map(i => parseInt(h.slice(i, i + 2), 16)); +const ratio = (alpha, bg) => { + const fg = bg.map(v => 255 * alpha + v * (1 - alpha)); + return (luminance(fg) + 0.05) / (luminance(bg) + 0.05); +}; + +const rule = selector => { + const at = page.indexOf(`\n ${selector} {`); + assert.ok(at >= 0, `${selector} moved`); + return page.slice(at, page.indexOf('}', at)); +}; +const ink = selector => Number(/color:rgba\(255,255,255,([\d.]+)\)/.exec(rule(selector))?.[1]); +const fill = selector => hex(/background(?:-color)?:(#[0-9A-Fa-f]{6})/.exec(rule(selector))?.[1] ?? ''); + +/* SC 1.4.3: 11px text needs 4.5:1. */ +for (const [text, on, bg] of [ + ['bay name', '.bay-name', '.bay'], + ['bay name on a pressed bay', '.bay-name', '.bay.assigned:hover,\n .bay.assigned.touch-active'], + ['unread bay name', '.bay.unread .bay-name', '.bay.unread'], + ['bay number', '#nums span', '#panel'], +]) { + test(`the ${text} clears 4.5:1`, () => { + const r = ratio(ink(on), fill(bg)); + assert.ok(r >= 4.5, `${text} reads at ${r.toFixed(2)}:1`); + }); +} diff --git a/ui/widgets/disk-health/index.html b/ui/widgets/disk-health/index.html index 4a28dc01..2fc4488e 100644 --- a/ui/widgets/disk-health/index.html +++ b/ui/widgets/disk-health/index.html @@ -54,7 +54,7 @@ } .bay.empty { background-color:#2C2C2E; } .bay.unread { background-color:#2C2C2E; box-shadow: inset 0 0 0 1.5px rgba(255,255,255,0.22); } - .bay.unread .bay-name { color:rgba(255,255,255,0.28); } + .bay.unread .bay-name { color:rgba(255,255,255,0.5); } .bay.assigned { cursor:pointer; } #root.size-medium .bay { border-width:1px; border-radius:8px; } .bay.assigned:hover, @@ -71,7 +71,7 @@ display:flex; align-items:center; justify-content:center; writing-mode:vertical-rl; font-size:11px; font-weight:500; letter-spacing:0.08em; - color:rgba(255,255,255,0.34); + color:rgba(255,255,255,0.62); white-space:nowrap; overflow:hidden; pointer-events:none; } .led { @@ -84,7 +84,7 @@ .led.unread { background:transparent; box-shadow: inset 0 0 0 1.5px #8E8E93; } #nums { height:16px; flex-shrink:0; display:flex; align-items:center; padding:3px 4px 0; } - #nums span { flex:1; text-align:center; font-size:11px; font-weight:500; color:rgba(255,255,255,0.4); } + #nums span { flex:1; text-align:center; font-size:11px; font-weight:500; color:rgba(255,255,255,0.5); } #alert { display:flex; align-items:center; gap:5px; padding:5px 2px 0; font-size:11px; font-weight:600; line-height:1.2; color:rgba(255,255,255,0.92); } #alert::before { content:''; width:6px; height:6px; border-radius:50%; flex-shrink:0; background:#FF4245; }