feat: add validated appearance palettes

This commit is contained in:
2026-08-20 06:26:52 +02:00
parent fd90b60430
commit 6643c8fc1e
11 changed files with 196 additions and 17 deletions
+27
View File
@@ -12,9 +12,24 @@ assert.match(tokens, /:root\[data-theme="dark"\]/, "dark token overrides are req
assert.match(tokens, /color-scheme:\s*dark/, "native controls must receive the dark color scheme");
assert.match(app, /matchMedia\?\.\("\(prefers-color-scheme: dark\)"\)/, "system theme must follow OS changes");
assert.match(app, /dataset\.themePreference/, "the selected preference must remain inspectable");
assert.match(app, /dataset\.palette/, "the selected palette must be applied at the document root");
for (const theme of ["system", "light", "dark"]) {
assert.match(settings, new RegExp(`value:\\s*"${theme}"`), `Settings must expose ${theme}`);
}
for (const palette of ["default", "civic_blue", "forest", "plum"]) {
assert.match(settings, new RegExp(`value:\\s*"${palette}"`), `Settings must expose ${palette}`);
}
const paletteContrasts = [
{ selector: "civic_blue", accent: "#245f91", foreground: "#ffffff" },
{ selector: "forest", accent: "#276749", foreground: "#ffffff" },
{ selector: "plum", accent: "#704b78", foreground: "#ffffff" }
];
for (const palette of paletteContrasts) {
assert.match(tokens, new RegExp(`data-palette="${palette.selector}"[\\s\\S]*?--accent:\\s*${palette.accent}`, "i"));
assert.ok(contrastRatio(palette.accent, palette.foreground) >= 4.5, `${palette.selector} accent contrast must meet WCAG AA`);
}
assert.ok(contrastRatio("#ef6b3a", "#242424") >= 4.5, "default accent badge contrast must meet WCAG AA");
const representativeModules = [
"govoplan-campaign/webui/src/styles/campaign-workspace.css",
@@ -28,3 +43,15 @@ for (const relativePath of representativeModules) {
}
console.log("Theme contract passed for core and representative module surfaces.");
function contrastRatio(first, second) {
const left = relativeLuminance(first);
const right = relativeLuminance(second);
return (Math.max(left, right) + 0.05) / (Math.min(left, right) + 0.05);
}
function relativeLuminance(color) {
const channels = color.slice(1).match(/../g).map((value) => parseInt(value, 16) / 255);
const linear = channels.map((value) => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4);
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
}