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:
2026-10-12 09:23:41 +02:00
co-authored by Claude Opus 5.5
parent c3211d0fe2
commit 871bb3a77c
75 changed files with 10155 additions and 15 deletions
+45
View File
@@ -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);
});
}
});