// The pages' colours, held to WCAG 2.2's AA contrast (1.4.3): text and // what it is drawn on at least 4.5:1, in every theme web/src/tokens.css // defines: light, dark as the system asks for it and as chosen, and each // other. A colour is only ever text on some surfaces, which PAIRS names; // one drawn over another, as a diff's changed word is over its line, is // composed as the browser would. A new colour that is text goes in PAIRS. // // node harness/contrast.mjs // // The pairs and how they are measured are web/src/lib/contrast.ts's, which // the theme editor holds a theme of someone's own to as well. // // Prints each pair that falls short as `theme: text on surface ratio`, // failing if any, and that the two copies of dark agree. import { readFileSync } from 'node:fs' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { AA, block, blocks, shortfalls } from '../web/src/lib/contrast.ts' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const css = readFileSync(join(root, 'web/src/tokens.css'), 'utf8') const { light, dark, chosenDark, shared } = blocks(css) const themes = { light, dark } for (const [, id] of css.matchAll(/^\[data-theme='([\w-]+)'\] \{/gm)) if (id !== 'light' && id !== 'dark') themes[id] = { ...light, ...shared, ...block(css, `[data-theme='${id}']`) } const problems = [] for (const [id, env] of Object.entries(themes)) for (const s of shortfalls(env)) problems.push(`${id}: ${s.text} on ${s.on} ${s.ratio.toFixed(2)}:1, under ${AA}:1`) for (const [name, value] of Object.entries(dark)) if (chosenDark[name] !== value) problems.push(`dark: --${name} is ${value} as the system asks, ${chosenDark[name]} as chosen`) for (const p of problems) console.log(p) if (problems.length) process.exit(1) console.log( `contrast: ${Object.keys(themes).length} themes, every text at least ${AA}:1 on what it is drawn on`, )