feat(nav): navgroup-Dropdowns im Header + Fix Overflow-Clipping
Deploy / verify (push) Successful in 1m3s
Deploy / deploy (push) Successful in 1m12s

- layout.server.ts: NavLink um optional children?: NavLink[] erweitert.
  navHeader-Resolve von 'links' auf 'all' (navgroup-sub-links werden mit
  aufgelöst). Resolver-Loop erkennt navgroup-Entries (_type==='navgroup'
  oder Shape label+links) und baut NavLink mit Children + Parent-URL.
- Header.svelte: Desktop-Dropdown mit Hover/Focus-Open + ARIA (haspopup,
  expanded, role=menu/menuitem). Mobile: collapsible Submenu via
  openSubmenu-State, slide-animiert; "– Übersicht"-Link wenn Parent
  selbst eine URL hat. isActiveTree(): Parent gilt aktiv wenn ein Child
  aktiv ist.
- Overflow-Fix: .gradient-blob-bg setzt overflow:hidden → clipte den
  Desktop-Dropdown. Neue Variante .gradient-blob-bg-inner als absoluter
  Inner-Layer (eigenes overflow-hidden), Header selbst bleibt
  overflow-visible. Bestehende .gradient-blob-bg-Nutzungen (z.B.
  DeadlineBannerBlock) unverändert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-05-30 08:50:51 +02:00
parent c58f418c1a
commit bd67f0c568
3 changed files with 194 additions and 29 deletions
+120 -24
View File
@@ -19,6 +19,12 @@
interface NavLink {
href: string;
label: string;
children?: NavLink[];
}
function isActiveTree(link: NavLink, pathname: string): boolean {
if (isActiveLink(link.href, pathname)) return true;
return (link.children ?? []).some((c) => isActiveLink(c.href, pathname));
}
interface SocialLink {
@@ -46,6 +52,11 @@
}
let menuOpen = $state(false);
let searchOpen = $state(false);
let openSubmenu = $state<string | null>(null);
function toggleSubmenu(label: string) {
openSubmenu = openSubmenu === label ? null : label;
}
function toggleMenu() {
if (menuOpen) {
@@ -100,9 +111,10 @@
<header
id="horizontal-navigation"
class="gradient-blob-bg sticky top-0 z-20 border-b border-white/30 shadow-2xl backdrop-blur-md bg-wald-50"
class="relative sticky top-0 z-20 border-b border-white/30 shadow-2xl backdrop-blur-md bg-wald-50"
>
<div class="flex items-center justify-between container-custom py-2">
<span class="gradient-blob-bg-inner" aria-hidden="true"></span>
<div class="relative flex items-center justify-between container-custom py-2">
<a
href="/"
aria-label={t(T.site_name_fallback)}
@@ -126,14 +138,60 @@
aria-label={t(T.header_nav_aria)}
>
{#each links as link}
{@const active = isActiveLink(link.href, $page.url.pathname)}
<a
href={link.href}
class="text-[.7rem] py-1 {active ? 'font-bold ' : ''}"
aria-current={active ? "page" : undefined}
>
{link.label}
</a>
{@const active = isActiveTree(link, $page.url.pathname)}
{#if link.children && link.children.length > 0}
<div class="relative group">
{#if link.href && link.href !== "#"}
<a
href={link.href}
class="text-[.7rem] py-1 inline-flex items-center gap-0.5 {active ? 'font-bold' : ''}"
aria-haspopup="menu"
aria-expanded="false"
>
{link.label}
<Icon icon="mdi:chevron-down" class="size-3" aria-hidden="true" />
</a>
{:else}
<button
type="button"
class="text-[.7rem] py-1 inline-flex items-center gap-0.5 {active ? 'font-bold' : ''}"
aria-haspopup="menu"
aria-expanded="false"
>
{link.label}
<Icon icon="mdi:chevron-down" class="size-3" aria-hidden="true" />
</button>
{/if}
<div
role="menu"
class="absolute left-0 top-full pt-1 min-w-[10rem] opacity-0 invisible translate-y-1 transition-all group-hover:opacity-100 group-hover:visible group-hover:translate-y-0 group-focus-within:opacity-100 group-focus-within:visible group-focus-within:translate-y-0 z-50"
>
<ul class="bg-stein-0 shadow-lg rounded border border-stein-200 py-1 text-stein-800">
{#each link.children as child}
{@const cActive = isActiveLink(child.href, $page.url.pathname)}
<li role="none">
<a
role="menuitem"
href={child.href}
class="block px-3 py-1.5 text-xs hover:bg-wald-50 {cActive ? 'font-bold text-wald-700' : ''}"
aria-current={cActive ? "page" : undefined}
>
{child.label}
</a>
</li>
{/each}
</ul>
</div>
</div>
{:else}
<a
href={link.href}
class="text-[.7rem] py-1 {active ? 'font-bold ' : ''}"
aria-current={active ? "page" : undefined}
>
{link.label}
</a>
{/if}
{/each}
<div class="w-1 h-4"></div>
{#each socialLinks as social}
@@ -241,20 +299,58 @@
aria-label={t(T.header_nav_aria)}
>
{#each links as link}
{@const active = isActiveLink(
link.href,
$page.url.pathname,
)}
<a
href={link.href}
class="block py-4 px-3 rounded-xs transition-colors text-base {active
? 'font-bold text-wald-300 bg-wald-700/20 border-l-4 border-wald-400'
: 'text-stein-100 hover:text-stein-0 hover:bg-stein-0/10 border-l-4 border-transparent'}"
aria-current={active ? "page" : undefined}
onclick={closeMenu}
>
{link.label}
</a>
{@const active = isActiveTree(link, $page.url.pathname)}
{#if link.children && link.children.length > 0}
{@const expanded = openSubmenu === link.label}
<div>
<button
type="button"
class="flex w-full items-center justify-between py-4 px-3 rounded-xs transition-colors text-base text-stein-100 hover:bg-stein-0/10 border-l-4 {active ? 'border-wald-400 font-bold text-wald-300' : 'border-transparent'}"
aria-expanded={expanded}
onclick={() => toggleSubmenu(link.label)}
>
<span>{link.label}</span>
<Icon icon={expanded ? "mdi:chevron-up" : "mdi:chevron-down"} class="size-4" aria-hidden="true" />
</button>
{#if expanded}
<div class="ml-6 border-l border-stein-0/10" in:slide={{ duration: 150 }} out:slide={{ duration: 100 }}>
{#if link.href && link.href !== "#"}
{@const pActive = isActiveLink(link.href, $page.url.pathname)}
<a
href={link.href}
class="block py-2 px-3 text-sm {pActive ? 'font-bold text-wald-300' : 'text-stein-200 hover:text-stein-0'}"
aria-current={pActive ? "page" : undefined}
onclick={closeMenu}
>
{link.label} Übersicht
</a>
{/if}
{#each link.children as child}
{@const cActive = isActiveLink(child.href, $page.url.pathname)}
<a
href={child.href}
class="block py-2 px-3 text-sm {cActive ? 'font-bold text-wald-300' : 'text-stein-200 hover:text-stein-0'}"
aria-current={cActive ? "page" : undefined}
onclick={closeMenu}
>
{child.label}
</a>
{/each}
</div>
{/if}
</div>
{:else}
<a
href={link.href}
class="block py-4 px-3 rounded-xs transition-colors text-base {active
? 'font-bold text-wald-300 bg-wald-700/20 border-l-4 border-wald-400'
: 'text-stein-100 hover:text-stein-0 hover:bg-stein-0/10 border-l-4 border-transparent'}"
aria-current={active ? "page" : undefined}
onclick={closeMenu}
>
{link.label}
</a>
{/if}
{/each}
{#if socialLinks.length > 0}
<div