a96df75473
.personal-report-reader declared --report-paper, --report-rule and --report-accent on itself. A custom property resolves from the nearest element that declares it, so an element-local declaration beats :root regardless of media queries or source order — the dark override added with the theme was dead the moment it was written. The inks did follow the theme, because they live on :root. So dark mode put --color-ink (#f2f0ea) on paper that stayed #f8f5ee: 1.05:1 for headings and 1.22:1 for body. The report was effectively invisible. Printing had the same defect from the other side: the print block forces white backgrounds but never reset the inks, so printing from a dark screen produced a blank page. It now pins the palette back to light — paper is always paper. Every report ink now clears AA on report paper in both themes, and the new contract test forbids re-declaring the report palette anywhere but :root, which is the trap that caused this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
71 lines
3.2 KiB
TypeScript
71 lines
3.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
|
|
|
const luminance = (hex: string) => {
|
|
const channel = (value: number) => {
|
|
const c = value / 255;
|
|
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
};
|
|
const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
|
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
};
|
|
const contrast = (a: string, b: string) => {
|
|
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
return (hi + 0.05) / (lo + 0.05);
|
|
};
|
|
|
|
function blockTokens(opener: string): Map<string, string> {
|
|
const start = globalStyles.indexOf(opener);
|
|
assert.notEqual(start, -1, `missing block: ${opener}`);
|
|
const end = globalStyles.indexOf("\n}", start);
|
|
const body = globalStyles.slice(start, end);
|
|
return new Map([...body.matchAll(/--([\w-]+)\s*:\s*([^;]+);/g)]
|
|
.map((hit) => [hit[1], hit[2].trim()]));
|
|
}
|
|
|
|
test("the report palette is declared on :root, never on the reader element", () => {
|
|
// A custom property declared on .personal-report-reader wins over :root no
|
|
// matter what a media query says. Keeping it local made the dark override
|
|
// dead: the paper stayed light while --color-ink went light with the theme,
|
|
// which is white text on white paper.
|
|
const reader = globalStyles.slice(globalStyles.indexOf(".personal-report-reader {"));
|
|
const readerBody = reader.slice(0, reader.indexOf("\n}"));
|
|
assert.doesNotMatch(readerBody, /--report-(paper|rule|accent)\s*:/);
|
|
for (const token of ["report-paper", "report-rule", "report-accent"]) {
|
|
assert.ok(blockTokens(":root {\n color-scheme: light;").has(token), `--${token} must live on :root`);
|
|
}
|
|
});
|
|
|
|
test("report ink clears AA on report paper in both themes", () => {
|
|
const light = blockTokens(":root {\n color-scheme: light;");
|
|
const dark = blockTokens(':root[data-theme="dark"] {');
|
|
for (const [name, palette] of [["light", light], ["dark", dark]] as const) {
|
|
const paper = palette.get("report-paper")!;
|
|
assert.match(paper, /^#[0-9a-f]{6}$/i);
|
|
for (const ink of ["color-ink", "color-ink-strong", "color-ink-secondary", "color-ink-tertiary", "report-accent"]) {
|
|
const value = palette.get(ink)!;
|
|
const ratio = contrast(value, paper);
|
|
assert.ok(
|
|
ratio >= 4.5,
|
|
`${name}: --${ink} (${value}) on --report-paper (${paper}) is ${ratio.toFixed(2)}:1`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("print pins the palette back to light so paper is always paper", () => {
|
|
// Printing while the screen is dark forced white backgrounds but left every
|
|
// ink light, which comes out of the printer blank.
|
|
const print = globalStyles.slice(globalStyles.indexOf("@media print {"));
|
|
assert.match(print, /:root \{\s*\n\s*color-scheme: light;/);
|
|
const printRoot = blockTokens("@media print {\n /* Paper is always paper");
|
|
for (const token of ["color-ink", "color-ink-strong", "report-paper", "report-accent"]) {
|
|
assert.ok(printRoot.has(token), `@media print must reset --${token}`);
|
|
}
|
|
assert.equal(printRoot.get("color-ink"), "#1d1d1f");
|
|
assert.equal(printRoot.get("report-paper"), "#f8f5ee");
|
|
});
|