Die Flaechenverteilung war verkehrt herum. Blau lag auf dem Groessten, was die Anmeldeseite zu vergeben hat, und Rosa nur in Andeutungen daneben — bei einer Marke, deren Schriftzug auf Rosa steht und deren Manual Rosa zuerst nennt. Sichtbar wurde es am Logo. Es bringt sein rosa Feld selbst mit, weil §1.2 den Schriftzug nur zur Gaenze auf Rosa zulaesst. Auf einer blauen Flaeche steht dieses Feld als ausgeschnittenes Rechteck darauf. Auf Weiss ist es dagegen richtig — §4.1 verlangt fuer weissen Untergrund genau das, ein rechteckiges rosa Feld. Falsch war also nicht das Logo, sondern die blaue Flaeche darunter. Rosa bekommen jetzt: das Panel der Anmeldeseite und das Band ueber der Anwendung aus Kopfzeile und Logokopf der Seitenleiste. Beide zusammen sind ein durchgehender Streifen, in dem das Logo aufgeht statt darauf zu liegen — was §3.1 mit "in einem grossflaechigen rosa Umfeld eingebettet" meint. Blau bleibt, was es ist: Knopf, Link, Fokusring, ausgewaehlter Eintrag. Das ist dasselbe Verhaeltnis, aus dem der Schriftzug besteht — blaue Lettern auf rosa Feld — und es ist das einzige, das traegt. Weiss auf Rosa sind 2.18:1. Auf den rosa Flaechen steht deshalb nichts Weisses und nichts Gedaempftes mehr. ink kommt auf 7.47:1, brand-700 auf 6.60:1; ink-muted lag bei 2.33:1 und ist aus Kopfzeile, Glocke und Logokopf verschwunden. Gestuft wird ueber Groesse und Gewicht statt ueber Transparenz — eine aufgehellte Schrift ist genau das, was auf dieser Flaeche durchfaellt. Das Raster auf dem Panel ist von Weiss auf ink gewechselt: auf Rosa war es nicht mehr zu sehen. Lint, Typen, Schemaabgleich, 552 Tests und der Build sind sauber. Im Browser nicht gesehen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
4.0 KiB
TypeScript
101 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { BarChart3, Building2, History, LayoutGrid, Network, Upload, Users, X } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { useEffect } from "react";
|
|
import { AppWortmarke } from "@/components/brand/Logo";
|
|
|
|
const NAV_ITEMS = [
|
|
{ href: "/", label: "Übersicht", icon: LayoutGrid },
|
|
{ href: "/employees", label: "Mitarbeiter:innen", icon: Users },
|
|
{ href: "/orgchart", label: "Organigramm", icon: Network },
|
|
{ href: "/positions", label: "Positionen", icon: Building2 },
|
|
{ href: "/reports", label: "Berichte", icon: BarChart3 },
|
|
{ href: "/audit", label: "Audit-Log", icon: History },
|
|
{ href: "/import", label: "Import", icon: Upload },
|
|
] as const;
|
|
|
|
export function isActiveRoute(pathname: string, href: string): boolean {
|
|
return href === "/" ? pathname === "/" : pathname.startsWith(href);
|
|
}
|
|
|
|
// Permanent rail from lg up, slide-in drawer below it. Same markup either
|
|
// way — the breakpoint only changes how the <nav> is positioned — so the
|
|
// nav list has exactly one definition.
|
|
export function Sidebar({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
const pathname = usePathname();
|
|
|
|
// A route change means the user tapped a nav item; the drawer has to get
|
|
// out of the way on its own, since it covers the page it just navigated to.
|
|
useEffect(() => {
|
|
onClose();
|
|
}, [pathname, onClose]);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") onClose();
|
|
};
|
|
window.addEventListener("keydown", onKey);
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
}, [open, onClose]);
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
onClick={onClose}
|
|
aria-hidden
|
|
className={`fixed inset-0 z-40 bg-black/40 transition-opacity lg:hidden ${
|
|
open ? "opacity-100" : "pointer-events-none opacity-0"
|
|
}`}
|
|
/>
|
|
<nav
|
|
aria-label="Hauptnavigation"
|
|
className={`fixed left-0 top-0 z-50 flex h-dvh w-[264px] flex-col border-r border-border bg-white transition-transform duration-200 lg:z-30 lg:w-[236px] lg:translate-x-0 ${
|
|
open ? "translate-x-0" : "-translate-x-full"
|
|
}`}
|
|
>
|
|
{/* pl-2 statt pl-5: der Freiraum des Logos bringt seinen eigenen
|
|
Abstand mit (siehe components/brand/Logo.tsx), und beides
|
|
uebereinander schoebe die Wortmarke sichtbar aus der Flucht der
|
|
Navigationspunkte darunter. */}
|
|
{/* Rosa wie die Kopfzeile daneben: beide zusammen sind ein
|
|
durchgehendes Band ueber der Anwendung, in dem das Logo mit seinem
|
|
eigenen rosa Feld aufgeht statt als Rechteck darauf zu liegen. */}
|
|
<div className="flex h-14 shrink-0 items-center justify-between border-b border-accent-600/40 bg-accent-500 pl-2 pr-3">
|
|
<AppWortmarke hoehe={24} textKlasse="text-brand-700" />
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
aria-label="Navigation schließen"
|
|
className="-mr-1 rounded p-2 text-ink hover:bg-accent-100 lg:hidden"
|
|
>
|
|
<X className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
{/* pb keeps the last item clear of the iOS home indicator. */}
|
|
<ul className="flex-1 space-y-1 overflow-y-auto px-3 py-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
|
|
{NAV_ITEMS.map(({ href, label, icon: Icon }) => {
|
|
const active = isActiveRoute(pathname, href);
|
|
return (
|
|
<li key={href}>
|
|
<Link
|
|
href={href}
|
|
aria-current={active ? "page" : undefined}
|
|
className={`flex items-center gap-3 rounded px-3 py-2.5 text-sm font-semibold transition-colors ${
|
|
active ? "bg-brand-100 text-brand-700" : "text-ink-body hover:bg-surface"
|
|
}`}
|
|
>
|
|
<Icon className="h-4 w-4 shrink-0" />
|
|
{label}
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</nav>
|
|
</>
|
|
);
|
|
}
|