feat(nav): navgroup-Dropdowns im Header + Fix Overflow-Clipping
- 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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user