404-Seite auf Deutsch mit lesbaren Theme-Farben

Überschrift "Seite nicht gefunden", Hinweis "Diese Seite gibt es nicht
oder nicht mehr." und Link "Zur Startseite" statt der englischen Vorlage.
bg-background/text-foreground statt festem grauen Hintergrund, damit der
Text in Hell und Dunkel lesbar ist. Der HTTP-Status bleibt 200 (Serveränderung
außerhalb dieses Pakets).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Sonnet 5.5 committed 2026-10-01 10:41:54 +00:00
1 parent 900d3a9ab5
commit 8d3424967c
2 files changed
+75 -7

No files matched your search

+7 -7
View File
@@ -1,4 +1,4 @@
import { useLocation } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import { useEffect } from "react"; import { useEffect } from "react";
const NotFound = () => { const NotFound = () => {
@@ -9,13 +9,13 @@ const NotFound = () => {
}, [location.pathname]); }, [location.pathname]);
return ( return (
<div className="flex min-h-screen items-center justify-center bg-muted"> <div className="flex min-h-[70vh] items-center justify-center bg-background px-6 text-foreground">
<div className="text-center"> <div className="text-center">
<h1 className="mb-4 text-4xl font-bold">404</h1> <h1 className="mb-4 text-4xl font-bold">Seite nicht gefunden</h1>
<p className="mb-4 text-xl text-muted-foreground">Oops! Page not found</p> <p className="mb-6 text-xl text-muted-foreground">Diese Seite gibt es nicht oder nicht mehr.</p>
<a href="/" className="text-primary underline hover:text-primary/90"> <Link to="/" className="text-foreground underline underline-offset-4 hover:no-underline">
Return to Home Zur Startseite
</a> </Link>
</div> </div>
</div> </div>
); );
+68
View File
@@ -282,3 +282,71 @@ describe("C1 Footer auf allen Unterseiten", () => {
expect(inhalt.compareDocumentPosition(footer) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); expect(inhalt.compareDocumentPosition(footer) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
}); });
}); });
// ── Kontrast-Hilfen (WCAG 2.x), gelesen werden die Farb-Token aus src/index.css ──
type RGB = [number, number, number];
const rgbAusHsl = (h: number, s: number, l: number): RGB => {
s /= 100;
l /= 100;
const k = (n: number) => (n + h / 30) % 12;
const a = s * Math.min(l, 1 - l);
const f = (n: number) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
return [f(0) * 255, f(8) * 255, f(4) * 255];
};
const leuchtdichte = (rgb: RGB) => {
const [r, g, b] = rgb.map((v) => {
const c = v / 255;
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const kontrast = (a: RGB, b: RGB) => {
const [hell, dunkel] = [leuchtdichte(a), leuchtdichte(b)].sort((x, y) => y - x);
return (hell + 0.05) / (dunkel + 0.05);
};
/** "#120F17" oder "hsl(0 0% 6%)" als RGB. */
const farbe = (wert: string): RGB => {
const hex = wert.trim().match(/^#([0-9a-f]{6})$/i);
if (hex) return [0, 2, 4].map((i) => parseInt(hex[1].slice(i, i + 2), 16)) as RGB;
const hsl = wert.trim().match(/^hsl\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%\s*\)$/);
if (hsl) return rgbAusHsl(Number(hsl[1]), Number(hsl[2]), Number(hsl[3]));
throw new Error(`Unbekanntes Farbformat: ${wert}`);
};
type Thema = "hell" | "dunkel";
/** Farb-Token ("--foreground: 0 0% 9%") eines Themes aus index.css. */
const tokenFarbe = (thema: Thema, name: string): RGB => {
const css = lies("src/index.css");
const block = css.match(thema === "dunkel" ? /\.dark\s*\{([^}]*)\}/ : /:root\s*\{([^}]*)\}/)?.[1] ?? "";
const wert = block.match(new RegExp(`--${name}:\\s*([\\d.]+)\\s+([\\d.]+)%\\s+([\\d.]+)%`));
if (!wert) throw new Error(`Token --${name} fehlt im Theme ${thema}`);
return rgbAusHsl(Number(wert[1]), Number(wert[2]), Number(wert[3]));
};
const THEMEN: Thema[] = ["hell", "dunkel"];
describe("C5 404-Seite", () => {
it("zeigt Überschrift, Erklärung und Link zur Startseite auf Deutsch", () => {
vi.spyOn(console, "error").mockImplementation(() => {});
seite("/gibt-es-nicht");
expect(screen.getByRole("heading", { level: 1, name: "Seite nicht gefunden" })).toBeInTheDocument();
expect(screen.getByText("Diese Seite gibt es nicht oder nicht mehr.")).toBeInTheDocument();
expect(screen.getByRole("link", { name: "Zur Startseite" })).toHaveAttribute("href", "/");
expect(screen.queryByText(/Oops|Return to Home|Page not found/)).toBeNull();
});
it("nutzt die Theme-Farben statt eines festen grauen Hintergrunds", () => {
vi.spyOn(console, "error").mockImplementation(() => {});
const { container } = seite("/gibt-es-nicht");
expect(container.querySelector(".bg-background.text-foreground")).not.toBeNull();
expect(container.querySelector(".bg-muted")).toBeNull();
expect(screen.getByRole("link", { name: "Zur Startseite" })).toHaveClass("text-foreground");
});
it("Textfarben der Seite erreichen auf dem Hintergrund in beiden Themes mindestens 4,5:1", () => {
for (const thema of THEMEN) {
for (const name of ["foreground", "muted-foreground"]) {
expect(kontrast(tokenFarbe(thema, name), tokenFarbe(thema, "background"))).toBeGreaterThanOrEqual(4.5);
}
}
});
});