feat: Grundgerüst mit Anmeldung, Layout und Design-Tokens
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>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user