style(web): dunkle App-Leiste und Fluent-Seitenleiste

Kopfzeile 48 px dunkel mit Seitentitel statt immer Startseite,
Dashboard-Reiter mit Akzent-Unterstrich; Seitenleiste mit
Auswahlpille, Modul-Kacheln, aufgeklappten Kategorien und gefuelltem
Suchfeld; eingeklappt nur Kacheln. Einheitlicher PageHeader.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-25 14:32:37 +02:00
parent 7192be223a
commit b80db49c5e
11 changed files with 389 additions and 150 deletions
+37 -21
View File
@@ -12,6 +12,7 @@ import { ThemeToggle } from '@/components/theme-toggle';
import { BugReportButton } from '@/components/bug-report/bug-report-button';
import { HEADER_CENTER_SLOT_ID } from '@/components/layout/header-slot';
import { usePathname } from 'next/navigation';
import { resolvePageTitle, useNavStore } from '@/lib/stores/nav-store';
export function Header() {
const t = useTranslations();
@@ -95,14 +96,25 @@ export function Header() {
const roleLabel = user ? tHeader(`role.${user.role}`) : '';
// Seitentitel in der Mitte (Design „Mosaik“): statt immer „Startseite“
// der Name der aktuellen Seite bzw. des Moduls.
const navModules = useNavStore((s) => s.modules);
const pageTitle = resolvePageTitle(pathname, navModules);
const pageTitleText = pageTitle
? 'key' in pageTitle
? t(pageTitle.key)
: pageTitle.text
: null;
return (
<header className="sticky top-0 z-50 flex h-[var(--header-height)] items-center border-b border-border bg-background px-4 shadow-sm">
{/* Left: Logo + Mobile hamburger */}
<div className="flex shrink-0 items-center gap-3">
<header className="sticky top-0 z-50 flex h-[var(--header-height)] items-center bg-appbar px-2 text-appbar-foreground sm:px-3 dark:shadow-[inset_0_-1px_0_oklch(1_0_0/0.07)]">
{/* Links: Menue (mobil) + Bildmarke. Dunkle App-Leiste wie bei GitHub
bzw. Microsoft 365 — in beiden Erscheinungsbildern dunkel. */}
<div className="flex shrink-0 items-center gap-2">
<button
type="button"
onClick={() => setMobileOpen(true)}
className="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-muted hover:text-foreground transition-colors md:hidden"
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-appbar-muted hover:bg-appbar-hover hover:text-appbar-foreground transition-colors md:hidden"
aria-label={t('common.menu')}
>
<svg
@@ -122,17 +134,21 @@ export function Header() {
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
<TesseraLogo
variant="horizontal"
size={28}
// Gelb traegt nur auf dunklem Grund: gemessen 13,1:1 im dunklen
// Erscheinungsbild gegen 1,26:1 im hellen (WCAG verlangt 3:1 fuer
// grossen fetten Text). Im hellen Modus erbt der Schriftzug daher
// die Vordergrundfarbe, im dunklen steht die Markenfarbe.
// Unter 640 px nur die Bildmarke: der Platz gehoert dort den
// Dashboard-Reitern in der Kopfzeilenmitte (260924-h7x).
wordmarkClassName="hidden sm:inline dark:text-primary"
/>
<Link
href="/"
className="flex items-center rounded-md px-1 py-1 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-appbar-foreground"
>
<TesseraLogo
variant="horizontal"
size={26}
plateOutline="always"
className="gap-2.5"
// Schriftzug weiss auf der dunklen Leiste (in beiden
// Erscheinungsbildern). Unter 640 px nur die Bildmarke: der Platz
// gehoert dort den Dashboard-Reitern (260924-h7x).
wordmarkClassName="hidden sm:inline text-[15px] font-semibold tracking-normal text-appbar-foreground"
/>
</Link>
</div>
{/* Mitte: Einhaengepunkt fuer die Dashboard-Reiter (quick-260924-h7x).
@@ -143,15 +159,15 @@ export function Header() {
und Aktionen rechts auch bei schmalem Fenster Platz behalten. */}
<div className="flex min-w-0 flex-1 items-center justify-center px-2 text-center md:px-4">
<div id={HEADER_CENTER_SLOT_ID} className="flex min-w-0 max-w-full justify-center" />
{pathname !== '/' && (
<span className="text-sm text-muted-foreground truncate">
{tHeader('breadcrumb.home')}
{pageTitleText !== null && (
<span className="truncate text-sm font-semibold text-appbar-foreground">
{pageTitleText}
</span>
)}
</div>
{/* Right: Actions */}
<div className="flex shrink-0 items-center gap-2">
<div className="flex shrink-0 items-center gap-1">
<BugReportButton />
<ThemeToggle />
@@ -160,7 +176,7 @@ export function Header() {
<button
type="button"
onClick={() => setDropdownOpen(!dropdownOpen)}
className="h-8 w-8 rounded-full overflow-hidden bg-primary flex items-center justify-center text-xs font-medium text-primary-foreground hover:opacity-90 transition-opacity"
className="ml-1 flex h-8 w-8 items-center justify-center overflow-hidden rounded-full bg-primary text-xs font-semibold text-primary-foreground ring-1 ring-white/15 transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-appbar-foreground"
aria-label={tHeader('userMenu')}
>
{user?.hasAvatar && !avatarError ? (
@@ -178,7 +194,7 @@ export function Header() {
{/* Dropdown menu */}
{dropdownOpen && (
<div className="absolute right-0 top-full mt-2 w-56 rounded-md border border-border bg-card shadow-lg py-2 z-50">
<div className="absolute right-0 top-full z-50 mt-2 w-60 rounded-lg border border-border bg-card py-1.5 text-card-foreground shadow-lg">
{/* User info */}
<div className="px-4 py-2 border-b border-border">
<p className="text-sm font-medium text-foreground truncate">