feat(ui): add responsive mobile menu with animated hamburger icon
- Add hamburger icon that morphs to X when menu is open - Full-screen mobile menu overlay with backdrop blur - Staggered fade-in animation for menu items - Desktop navigation unchanged (hidden on mobile) - Close menu via X button, backdrop click, or ESC key 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,46 +1,121 @@
|
||||
<header class="fixed top-0 left-0 right-0 z-sticky bg-surface-0/80 backdrop-blur-md border-b border-border">
|
||||
<nav class="container-wide flex items-center justify-between h-16">
|
||||
{{/* Logo */}}
|
||||
<div class="flex items-center gap-3 group">
|
||||
<canvas
|
||||
id="logo-canvas"
|
||||
hx-preserve="true"
|
||||
class="w-8 h-8 cursor-pointer"
|
||||
width="32"
|
||||
height="32"
|
||||
aria-hidden="true"
|
||||
@click="window.__pivoine?.visualizer?.nextVisualizer()"
|
||||
title="Click to change visualizer"
|
||||
></canvas>
|
||||
<a
|
||||
href="{{ "/" | relURL }}"
|
||||
class="text-lg font-medium tracking-tight group-hover:text-accent transition-colors"
|
||||
aria-label="{{ .Site.Title }} - Home"
|
||||
<div x-data="{ menuOpen: false }" @keydown.escape.window="menuOpen = false">
|
||||
<header class="fixed top-0 left-0 right-0 z-sticky bg-surface-0/80 backdrop-blur-md border-b border-border">
|
||||
<nav class="container-wide flex items-center justify-between h-16">
|
||||
{{/* Logo */}}
|
||||
<div class="flex items-center gap-3 group">
|
||||
<canvas
|
||||
id="logo-canvas"
|
||||
hx-preserve="true"
|
||||
class="w-8 h-8 cursor-pointer"
|
||||
width="32"
|
||||
height="32"
|
||||
aria-hidden="true"
|
||||
@click="window.__pivoine?.visualizer?.nextVisualizer()"
|
||||
title="Click to change visualizer"
|
||||
></canvas>
|
||||
<a
|
||||
href="{{ "/" | relURL }}"
|
||||
class="text-lg font-medium tracking-tight group-hover:text-accent transition-colors"
|
||||
aria-label="{{ .Site.Title }} - Home"
|
||||
>
|
||||
VALKNAR'S
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{/* Desktop Navigation */}}
|
||||
<ul
|
||||
class="hidden md:flex items-center gap-8"
|
||||
x-data="{ path: window.location.pathname }"
|
||||
@htmx:after-settle.window="path = window.location.pathname"
|
||||
>
|
||||
VALKNAR'S
|
||||
</a>
|
||||
{{- range .Site.Menus.main }}
|
||||
<li>
|
||||
<a
|
||||
href="{{ .URL }}"
|
||||
class="text-sm transition-colors"
|
||||
:class="path.startsWith('{{ .URL }}') ? 'text-text-primary border-b border-text-primary' : 'text-text-secondary hover:text-text-primary link-hover'"
|
||||
>
|
||||
{{ .Name }}
|
||||
</a>
|
||||
</li>
|
||||
{{- end }}
|
||||
</ul>
|
||||
|
||||
{{/* Mobile Hamburger Button */}}
|
||||
<button
|
||||
class="md:hidden relative w-8 h-8 flex items-center justify-center"
|
||||
@click="menuOpen = !menuOpen"
|
||||
:aria-expanded="menuOpen"
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
<div class="hamburger" :class="{ 'is-active': menuOpen }">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{{/* Mobile Menu Overlay - Full Screen */}}
|
||||
<div
|
||||
class="md:hidden fixed inset-0 z-[300]"
|
||||
x-show="menuOpen"
|
||||
x-cloak
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@click="menuOpen = false"
|
||||
>
|
||||
{{/* Backdrop */}}
|
||||
<div class="absolute inset-0 bg-surface-0/98 backdrop-blur-xl"></div>
|
||||
|
||||
{{/* Close Button - Top right corner */}}
|
||||
<div class="absolute top-0 right-0 h-16 container-wide flex items-center justify-end z-10">
|
||||
<button
|
||||
class="w-8 h-8 flex items-center justify-center"
|
||||
@click.stop="menuOpen = false"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<div class="hamburger is-active">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{/* Navigation */}}
|
||||
<ul
|
||||
class="flex items-center gap-4 md:gap-8"
|
||||
{{/* Menu Content */}}
|
||||
<nav
|
||||
class="relative h-full flex flex-col items-center justify-center"
|
||||
x-data="{ path: window.location.pathname }"
|
||||
@htmx:after-settle.window="path = window.location.pathname"
|
||||
@click.stop
|
||||
>
|
||||
{{- range .Site.Menus.main }}
|
||||
<li>
|
||||
<a
|
||||
href="{{ .URL }}"
|
||||
class="text-sm transition-colors"
|
||||
:class="path.startsWith('{{ .URL }}') ? 'text-text-primary border-b border-text-primary' : 'text-text-secondary hover:text-text-primary link-hover'"
|
||||
<ul class="flex flex-col items-center gap-8">
|
||||
{{- range $i, $item := .Site.Menus.main }}
|
||||
<li
|
||||
class="transform transition-all duration-300 ease-out"
|
||||
:class="menuOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
|
||||
style="transition-delay: {{ mul $i 50 }}ms"
|
||||
>
|
||||
{{ .Name }}
|
||||
</a>
|
||||
</li>
|
||||
{{- end }}
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<a
|
||||
href="{{ .URL }}"
|
||||
class="text-2xl font-medium tracking-wide transition-colors"
|
||||
:class="path.startsWith('{{ .URL }}') ? 'text-text-primary' : 'text-text-secondary hover:text-text-primary'"
|
||||
@click="menuOpen = false"
|
||||
>
|
||||
{{ .Name }}
|
||||
</a>
|
||||
</li>
|
||||
{{- end }}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* Spacer for fixed header */}}
|
||||
<div class="h-16" aria-hidden="true"></div>
|
||||
|
||||
Reference in New Issue
Block a user