Archived
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:
1 parent
900d3a9ab5
commit
8d3424967c
2 files changed
+75
-7
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const NotFound = () => {
|
||||
@@ -9,13 +9,13 @@ const NotFound = () => {
|
||||
}, [location.pathname]);
|
||||
|
||||
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">
|
||||
<h1 className="mb-4 text-4xl font-bold">404</h1>
|
||||
<p className="mb-4 text-xl text-muted-foreground">Oops! Page not found</p>
|
||||
<a href="/" className="text-primary underline hover:text-primary/90">
|
||||
Return to Home
|
||||
</a>
|
||||
<h1 className="mb-4 text-4xl font-bold">Seite nicht gefunden</h1>
|
||||
<p className="mb-6 text-xl text-muted-foreground">Diese Seite gibt es nicht oder nicht mehr.</p>
|
||||
<Link to="/" className="text-foreground underline underline-offset-4 hover:no-underline">
|
||||
Zur Startseite
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -282,3 +282,71 @@ describe("C1 Footer auf allen Unterseiten", () => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user