"use client"; import { usePathname } from "next/navigation"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { cn } from "@/lib/utils"; import { flags, legal as legalDe, nav as navDe } from "@/lib/site"; import { routesByLang, type Lang } from "@/lib/i18n"; import { chrome as chromeDe, type Chrome } from "@/content/chrome"; import { Wordmark } from "@/components/brand/Wordmark"; import { TransitionLink } from "@/components/motion/TransitionLink"; import { subscribe } from "@/components/motion/transition-store"; import { Container } from "./Container"; import { MENU_TOGGLE_ID, OverlayMenu } from "./OverlayMenu"; import { LangSwitch } from "./LangSwitch"; export interface HeaderProps { /** The 28 px monogram – layout.tsx passes `` (Server Component). */ mark?: ReactNode; /** Texte der Hülle. Ohne Angabe die deutsche Fassung. */ chrome?: Chrome; /** Die drei Leistungslinks der jeweiligen Sprache. */ nav?: readonly { href: string; label: string }[]; /** Aktuelle Sprache – steuert den Umschalter und das Ziel der Wortmarke. */ lang?: Lang; /** Impressum/Datenschutz der jeweiligen Sprache – für das Overlay-Menü. */ legal?: readonly { href: string; label: string }[]; } const navText = "font-sans text-[0.75rem] uppercase tracking-[0.14em]"; const underline = "relative after:absolute after:-bottom-1 after:left-0 after:h-px after:w-full after:origin-left after:scale-x-0 after:bg-accent after:transition-transform after:duration-300 hover:after:scale-x-100 focus-visible:after:scale-x-100 aria-[current=page]:after:scale-x-100"; function samePath(a: string, b: string) { const n = (s: string) => (s.endsWith("/") ? s : `${s}/`); return n(a) === n(b); } /** * Fixed header, single row (the Masthead lives in each page hero). * < 1024: Wordmark | „Menü“. 1024–1279: Wordmark | Sprachumschalter + „Menü“ (the * three nav links need ~450 px and would collide with the wordmark). * 1280–1439: Wordmark | 3 nav links (tighter caps) | one CTA – the „Kontakt“ * text link steps aside so the wordmark keeps its air and the CTA can carry * the one label the site uses for this action (spec §5.1). * ≥ 1440: Wordmark | 3 nav links | „Kontakt“ + outline CTA. * Ink chrome (the page below breathes, the bar does not); after 80 px: * 92 % ground + blur(8px) + hairline. While it is still transparent it reads * on the page's first block, so on an ivory-first page (Impressum) globals.css * flips it to ink type via `[data-scrolled]`. Writes `--header-h` on . */ export function Header({ mark, chrome = chromeDe, nav = navDe, legal = legalDe, lang = "de" }: HeaderProps) { const ref = useRef(null); const pathname = usePathname(); const [scrolled, setScrolled] = useState(false); const [open, setOpen] = useState(false); useEffect(() => { let raf = 0; const update = () => { raf = 0; setScrolled(window.scrollY > 80); }; const onScroll = () => { if (!raf) raf = requestAnimationFrame(update); }; update(); window.addEventListener("scroll", onScroll, { passive: true }); return () => { window.removeEventListener("scroll", onScroll); if (raf) cancelAnimationFrame(raf); }; }, []); useEffect(() => { const el = ref.current; if (!el) return; const write = () => document.documentElement.style.setProperty("--header-h", `${el.offsetHeight}px`); write(); const ro = new ResizeObserver(write); ro.observe(el); return () => { ro.disconnect(); document.documentElement.style.removeProperty("--header-h"); }; }, []); // Expose the open state on (scroll-progress hides itself while the menu is open). useEffect(() => { document.documentElement.toggleAttribute("data-menu-open", open); return () => document.documentElement.removeAttribute("data-menu-open"); }, [open]); // Close the menu when a page transition starts (route changes are handled by OverlayMenu). useEffect(() => subscribe((s) => s.phase === "cover" && setOpen(false)), []); return ( <>
{/* Steht an der Stelle, an der bis zum 06.09.2026 „Kontakt“ stand. Zwei Formen, weil neben Wortmarke und „Menü“ auf einem Telefon kein Paar Platz hat: unter 1024 px nur die Zielsprache als ein Knopf, darüber beide. Im Overlay-Menü steht er nicht mehr – dort fiel er auf kurzen Geräten unter die Kante, ohne dass man ihm ansah, dass da noch etwas kommt. */} {flags.showLangSwitch && } {flags.showLangSwitch && } {/* One label for this action everywhere – the hero button says the same (spec §5.1). */} {chrome.header.cta.label} {/* WCAG 2.5.3: the visible label stays „Menü“ in both states, so the accessible name has to contain it – „Schließen“ alone made „Klick Menü“ impossible for a speech-input user once the sheet was open. The STATE is on `aria-expanded`, not on the name. The id makes the toggle part of the dialog's focus trap. */}
setOpen(false)} chrome={chrome} legal={legal} /> ); } export default Header;