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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user