Next.js 16, Prisma 7, Tailwind 4 und Vitest/Playwright, Konfiguration aus Laufzettel übernommen, weil dort derselbe Stack erprobt ist. Die Farben der Branding-Vorgabe stehen als Tokens in globals.css, das helle Theme setzt dieselben Namen neu. Amber und Rot sind dort abgedunkelt, sonst reicht der Kontrast nicht – ein Test prüft beide Themes gegen 4,5:1. Anmeldung mit lokalem Benutzer, Rolle je Anfrage aus der Datenbank, Audit-Log per Trigger nur anhängbar. Geprüft im gebauten Stand auf 1440 px und 390 px. Closes #001 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
46 lines
1.8 KiB
TypeScript
46 lines
1.8 KiB
TypeScript
import { readFileSync } from "node:fs";
|
||
import { describe, expect, it } from "vitest";
|
||
|
||
/**
|
||
* Kontrast mindestens 4,5:1 (WCAG AA) für Text auf Grund und Panel – in beiden Themes.
|
||
* Liest die Tokens direkt aus globals.css, damit eine Farbänderung hier auffällt.
|
||
*/
|
||
const css = readFileSync("src/app/globals.css", "utf8");
|
||
|
||
function block(selektor: string): Record<string, string> {
|
||
const anfang = css.indexOf(selektor);
|
||
const inhalt = css.slice(css.indexOf("{", anfang) + 1, css.indexOf("}", anfang));
|
||
return Object.fromEntries(
|
||
[...inhalt.matchAll(/--farbe-([a-z-]+):\s*(#[0-9a-f]{6})/gi)].map((t) => [t[1]!, t[2]!]),
|
||
);
|
||
}
|
||
|
||
function leuchtdichte(hex: string): number {
|
||
const kanal = (i: number) => {
|
||
const c = parseInt(hex.slice(i, i + 2), 16) / 255;
|
||
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
||
};
|
||
return 0.2126 * kanal(1) + 0.7152 * kanal(3) + 0.0722 * kanal(5);
|
||
}
|
||
|
||
export function kontrast(a: string, b: string): number {
|
||
const [hell, dunkel] = [leuchtdichte(a), leuchtdichte(b)].sort((x, y) => y - x) as [number, number];
|
||
return (hell + 0.05) / (dunkel + 0.05);
|
||
}
|
||
|
||
const THEMES = { dunkel: block('[data-theme="dunkel"]'), hell: block('[data-theme="hell"]') };
|
||
const TEXTFARBEN = ["text", "muted", "akzent", "laeuft", "fehler"];
|
||
|
||
describe("Kontrast der Theme-Farben", () => {
|
||
for (const [name, farben] of Object.entries(THEMES)) {
|
||
for (const flaeche of ["grund", "panel", "panel-hoch"]) {
|
||
it.each(TEXTFARBEN)(`${name}: %s auf ${flaeche} ≥ 4,5:1`, (textfarbe) => {
|
||
expect(kontrast(farben[textfarbe]!, farben[flaeche]!)).toBeGreaterThanOrEqual(4.5);
|
||
});
|
||
}
|
||
it(`${name}: Text auf Akzentfläche ≥ 4,5:1`, () => {
|
||
expect(kontrast(farben["akzent-text"]!, farben["akzent"]!)).toBeGreaterThanOrEqual(4.5);
|
||
});
|
||
}
|
||
});
|