From de5dacfa3b832a96f4c246060a0818135abde8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sun, 30 Nov 2025 00:51:08 +0100 Subject: [PATCH] feat(ui): add responsive mobile menu with animated hamburger icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- assets/css/main.css | 40 ++++++++++ layouts/partials/header.html | 147 ++++++++++++++++++++++++++--------- static/css/main.css | 129 +++++++++++++++++++++++++----- 3 files changed, 260 insertions(+), 56 deletions(-) diff --git a/assets/css/main.css b/assets/css/main.css index ec3dfe6..8f09d45 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -161,6 +161,46 @@ html { transform-origin: left; } + /* Hamburger Menu Icon */ + .hamburger { + width: 20px; + height: 14px; + position: relative; + cursor: pointer; + } + .hamburger span { + display: block; + position: absolute; + width: 100%; + height: 2px; + background: var(--color-text-primary); + border-radius: var(--radius-full); + transition: all var(--duration-normal) var(--ease-out); + } + .hamburger span:nth-child(1) { + top: 0; + } + .hamburger span:nth-child(2) { + top: 50%; + transform: translateY(-50%); + } + .hamburger span:nth-child(3) { + bottom: 0; + } + /* Active state - transforms to X */ + .hamburger.is-active span:nth-child(1) { + top: 50%; + transform: translateY(-50%) rotate(45deg); + } + .hamburger.is-active span:nth-child(2) { + opacity: 0; + transform: translateX(10px); + } + .hamburger.is-active span:nth-child(3) { + bottom: 50%; + transform: translateY(50%) rotate(-45deg); + } + /* Audio Player */ .audio-player { background: var(--color-surface-1); diff --git a/layouts/partials/header.html b/layouts/partials/header.html index 443af98..3d3b37e 100755 --- a/layouts/partials/header.html +++ b/layouts/partials/header.html @@ -1,46 +1,121 @@ -
- -
+ + {{ .Name }} + + + {{- end }} + + + + {{/* Spacer for fixed header */}} diff --git a/static/css/main.css b/static/css/main.css index 4ca1faf..6d3e194 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -34,9 +34,12 @@ --leading-relaxed: 1.625; --radius-sm: 0.25rem; --radius-lg: 0.5rem; + --ease-in: cubic-bezier(0.4, 0, 1, 1); --ease-out: cubic-bezier(0, 0, 0.2, 1); --animate-bounce: bounce 1s infinite; --blur-md: 12px; + --blur-lg: 16px; + --blur-xl: 24px; --default-transition-duration: 150ms; --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); --default-font-family: var(--font-sans); @@ -238,6 +241,9 @@ .z-10 { z-index: 10; } + .z-\[300\] { + z-index: 300; + } .z-player { z-index: 400; } @@ -403,6 +409,14 @@ .flex-shrink-0 { flex-shrink: 0; } + .translate-y-0 { + --tw-translate-y: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .translate-y-4 { + --tw-translate-y: calc(var(--spacing) * 4); + translate: var(--tw-translate-x) var(--tw-translate-y); + } .scale-90 { --tw-scale-x: 90%; --tw-scale-y: 90%; @@ -451,6 +465,9 @@ .gap-6 { gap: calc(var(--spacing) * 6); } + .gap-8 { + gap: calc(var(--spacing) * 8); + } .truncate { overflow: hidden; text-overflow: ellipsis; @@ -501,6 +518,12 @@ background-color: color-mix(in oklab, var(--color-surface-0) 80%, transparent); } } + .bg-surface-0\/98 { + background-color: var(--color-surface-0); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-surface-0) 98%, transparent); + } + } .bg-surface-2 { background-color: var(--color-surface-2); } @@ -649,6 +672,9 @@ .opacity-10 { opacity: 10%; } + .opacity-100 { + opacity: 100%; + } .shadow { --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); @@ -669,6 +695,11 @@ -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); } + .backdrop-blur-xl { + --tw-backdrop-blur: blur(var(--blur-xl)); + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } .transition { transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); @@ -694,6 +725,14 @@ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } + .duration-150 { + --tw-duration: 150ms; + transition-duration: 150ms; + } + .duration-200 { + --tw-duration: 200ms; + transition-duration: 200ms; + } .duration-300 { --tw-duration: 300ms; transition-duration: 300ms; @@ -702,6 +741,14 @@ --tw-duration: 500ms; transition-duration: 500ms; } + .ease-in { + --tw-ease: var(--ease-in); + transition-timing-function: var(--ease-in); + } + .ease-out { + --tw-ease: var(--ease-out); + transition-timing-function: var(--ease-out); + } .group-hover\:translate-x-1 { &:is(:where(.group):hover *) { @media (hover: hover) { @@ -900,6 +947,11 @@ display: flex; } } + .md\:hidden { + @media (width >= 48rem) { + display: none; + } + } .md\:grid-cols-2 { @media (width >= 48rem) { grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -915,11 +967,6 @@ flex-direction: row; } } - .md\:gap-8 { - @media (width >= 48rem) { - gap: calc(var(--spacing) * 8); - } - } .md\:text-4xl { @media (width >= 48rem) { font-size: var(--text-4xl); @@ -1057,6 +1104,43 @@ html { transform: scaleX(1); transform-origin: left; } + .hamburger { + width: 20px; + height: 14px; + position: relative; + cursor: pointer; + } + .hamburger span { + display: block; + position: absolute; + width: 100%; + height: 2px; + background: var(--color-text-primary); + border-radius: var(--radius-full); + transition: all var(--duration-normal) var(--ease-out); + } + .hamburger span:nth-child(1) { + top: 0; + } + .hamburger span:nth-child(2) { + top: 50%; + transform: translateY(-50%); + } + .hamburger span:nth-child(3) { + bottom: 0; + } + .hamburger.is-active span:nth-child(1) { + top: 50%; + transform: translateY(-50%) rotate(45deg); + } + .hamburger.is-active span:nth-child(2) { + opacity: 0; + transform: translateX(10px); + } + .hamburger.is-active span:nth-child(3) { + bottom: 50%; + transform: translateY(50%) rotate(-45deg); + } .audio-player { background: var(--color-surface-1); border-top: 1px solid var(--color-border); @@ -1186,6 +1270,21 @@ html { transition-duration: 0.01ms !important; } } +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} @property --tw-scale-x { syntax: "*"; inherits: false; @@ -1458,20 +1557,9 @@ html { syntax: "*"; inherits: false; } -@property --tw-translate-x { +@property --tw-ease { syntax: "*"; inherits: false; - initial-value: 0; -} -@property --tw-translate-y { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-translate-z { - syntax: "*"; - inherits: false; - initial-value: 0; } @property --tw-content { syntax: "*"; @@ -1491,6 +1579,9 @@ html { @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-scale-z: 1; @@ -1554,9 +1645,7 @@ html { --tw-backdrop-saturate: initial; --tw-backdrop-sepia: initial; --tw-duration: initial; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-translate-z: 0; + --tw-ease: initial; --tw-content: ""; } }