feat(ui): Huelle mit Seitenleiste und Platzhalterseiten (#002)
Die Huelle bekommt Benutzer und optional den aktiven Pfad als Props und kennt weder Sitzung noch Datenbank; ohne Pfad ermittelt die Navigation ihn per usePathname(), weil ein Layout den Pfad nicht kennt. Die Benutzerermittlung steckt in huellenBenutzerErmitteln(), damit #004 nur diese Funktion ersetzen muss. Unter 1024 px klappt die Leiste ein; geschlossen ist sie invisible und damit auch nicht per Tab erreichbar. Alle Navigationsziele haben eine Platzhalterseite, damit nichts auf eine 404 fuehrt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import { Bildmarke, Kurzname, Marke, Wortmarke } from "@/components/marke";
|
||||
import { VERSION } from "@/lib/version";
|
||||
import { Navigation } from "./Navigation";
|
||||
import { SeitenleistenRahmen } from "./SeitenleistenRahmen";
|
||||
import { ROLLEN_TEXT, type HuellenBenutzer } from "./typen";
|
||||
|
||||
/**
|
||||
* Grundlayout (#002): feste Seitenleiste links (248 px), Hauptbereich rechts.
|
||||
* Benutzer und aktiver Pfad kommen als Props – die Hülle kennt weder Sitzung noch
|
||||
* Datenbank, damit sie in Tests und Prototypen ohne Anmeldung rendert.
|
||||
*/
|
||||
export function Huelle({
|
||||
benutzer,
|
||||
aktiverPfad,
|
||||
children,
|
||||
}: {
|
||||
benutzer: HuellenBenutzer;
|
||||
/** Ohne Angabe ermittelt die Navigation den Pfad im Browser. */
|
||||
aktiverPfad?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-bg min-h-dvh lg:flex">
|
||||
<a
|
||||
href="#inhalt"
|
||||
className="bg-accent text-bg rounded-box sr-only z-[60] px-4 py-3 font-semibold focus:not-sr-only focus:fixed focus:top-3 focus:left-3"
|
||||
>
|
||||
Zum Inhalt springen
|
||||
</a>
|
||||
|
||||
<SeitenleistenRahmen
|
||||
kopf={
|
||||
<Link href="/" className="rounded-box flex min-h-11 items-center gap-2 px-1">
|
||||
<Bildmarke groesse={28} />
|
||||
<Wortmarke groesse={19} />
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<div className="flex h-full flex-col overflow-y-auto px-4 pt-6 pb-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="rounded-box mb-8 flex items-center px-2 py-1"
|
||||
aria-label="SSHKeyVault – Übersicht"
|
||||
>
|
||||
<Marke bildGroesse={34} textGroesse={22} />
|
||||
</Link>
|
||||
|
||||
<Navigation aktiverPfad={aktiverPfad} />
|
||||
|
||||
<div className="mt-auto pt-6">
|
||||
<BenutzerKasten benutzer={benutzer} />
|
||||
<p className="text-muted mt-3 px-2 font-mono text-[11.5px]">
|
||||
<Kurzname /> v{VERSION}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SeitenleistenRahmen>
|
||||
|
||||
<main id="inhalt" tabIndex={-1} className="min-w-0 flex-1 outline-none">
|
||||
<div className="mx-auto max-w-[1440px] px-4 py-6 sm:px-8 sm:py-8 xl:px-10">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BenutzerKasten({ benutzer }: { benutzer: HuellenBenutzer }) {
|
||||
const kuerzel = benutzer.name
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((t) => t[0]?.toUpperCase())
|
||||
.join("");
|
||||
return (
|
||||
<div className="border-border bg-surface rounded-box flex items-center gap-3 border p-3">
|
||||
<span
|
||||
aria-hidden
|
||||
className="bg-raised text-accent font-display flex size-9 shrink-0 items-center justify-center rounded-full text-[13px] font-semibold"
|
||||
>
|
||||
{kuerzel}
|
||||
</span>
|
||||
<div className="min-w-0 text-[13px] leading-snug">
|
||||
<p className="text-text truncate text-[14px] font-medium">{benutzer.name}</p>
|
||||
<p className="text-muted truncate">
|
||||
{ROLLEN_TEXT[benutzer.rolle]} · über {benutzer.herkunft}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user