Skip to content

Commit 9096adc

Browse files
committed
improve mobile menu
1 parent bdbfe93 commit 9096adc

File tree

5 files changed

+175
-100
lines changed

5 files changed

+175
-100
lines changed

src/components/Header.astro

Lines changed: 2 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -80,81 +80,12 @@ import { navConfig } from '@/config/navigation'
8080
</svg>
8181
</button>
8282

83-
<!-- Three dots menu -->
84-
<div class="relative">
85-
<button
86-
id="mobile-menu-trigger"
87-
class="p-2 text-muted-foreground hover:text-foreground transition-colors"
88-
>
89-
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
90-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"></path>
91-
</svg>
92-
</button>
93-
94-
<!-- Dropdown menu -->
95-
<div
96-
id="mobile-menu-dropdown"
97-
class="absolute right-0 mt-2 w-48 bg-background border border-border rounded-md shadow-lg opacity-0 invisible transition-all duration-200"
98-
>
99-
<div class="py-1">
100-
{navConfig.topNav.map((item) => (
101-
<a
102-
href={item.href}
103-
class="block px-4 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
104-
target={item.external ? "_blank" : undefined}
105-
rel={item.external ? "noopener noreferrer" : undefined}
106-
style="text-decoration: none"
107-
>
108-
{item.title}
109-
</a>
110-
))}
111-
112-
<a
113-
href="/getting-started"
114-
class="block px-4 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
115-
style="text-decoration: none"
116-
>
117-
Get Started
118-
</a>
119-
</div>
120-
</div>
121-
</div>
83+
<!-- Mobile Menu Component -->
84+
<MobileMenu client:load />
12285
</div>
12386
</div>
12487
</div>
12588
</header>
12689

12790
<Search />
12891

129-
<script>
130-
document.addEventListener('DOMContentLoaded', () => {
131-
const trigger = document.getElementById('mobile-menu-trigger');
132-
const dropdown = document.getElementById('mobile-menu-dropdown');
133-
134-
if (trigger && dropdown) {
135-
trigger.addEventListener('click', (e) => {
136-
e.stopPropagation();
137-
const isOpen = dropdown.classList.contains('opacity-100');
138-
139-
if (isOpen) {
140-
dropdown.classList.remove('opacity-100', 'visible');
141-
dropdown.classList.add('opacity-0', 'invisible');
142-
} else {
143-
dropdown.classList.remove('opacity-0', 'invisible');
144-
dropdown.classList.add('opacity-100', 'visible');
145-
}
146-
});
147-
148-
// Close dropdown when clicking outside
149-
document.addEventListener('click', () => {
150-
dropdown.classList.remove('opacity-100', 'visible');
151-
dropdown.classList.add('opacity-0', 'invisible');
152-
});
153-
154-
// Prevent dropdown from closing when clicking inside
155-
dropdown.addEventListener('click', (e) => {
156-
e.stopPropagation();
157-
});
158-
}
159-
});
160-
</script>

src/components/Icon.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
interface IconProps {
2+
name: string
3+
className?: string
4+
}
5+
6+
export default function Icon({ name, className }: IconProps) {
7+
const icons = {
8+
website: (
9+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
10+
<circle cx="12" cy="12" r="10"/>
11+
<line x1="2" y1="12" x2="22" y2="12"/>
12+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
13+
</svg>
14+
),
15+
github: (
16+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
17+
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/>
18+
<path d="M9 18c-4.51 2-5-2-7-2"/>
19+
</svg>
20+
),
21+
discord: (
22+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className={className}>
23+
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419-.0003 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9554 2.4189-2.1568 2.4189Z"/>
24+
</svg>
25+
),
26+
reddit: (
27+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className={className}>
28+
<path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/>
29+
</svg>
30+
),
31+
pypi: (
32+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className={className}>
33+
<path d="M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.77l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.17l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05-.05-1.23.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.18l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09zm13.09 3.95l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08z"/>
34+
</svg>
35+
),
36+
}
37+
38+
return icons[name as keyof typeof icons] || null
39+
}

src/components/MobileMenu.tsx

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
1-
import { useState } from 'react'
2-
import { navConfig } from '@/config/navigation'
1+
import { useState, useEffect } from 'react'
2+
import NavMenu from './NavMenu'
33

44
export default function MobileMenu() {
55
const [isOpen, setIsOpen] = useState(false)
6+
const [isVisible, setIsVisible] = useState(false)
7+
8+
useEffect(() => {
9+
if (isOpen) {
10+
// Small delay to ensure the DOM is ready before animating
11+
setTimeout(() => setIsVisible(true), 10)
12+
// Add blur effect to background content
13+
document.body.style.overflow = 'hidden'
14+
document.documentElement.classList.add('mobile-menu-open')
15+
} else {
16+
setIsVisible(false)
17+
// Remove blur effect
18+
document.body.style.overflow = ''
19+
document.documentElement.classList.remove('mobile-menu-open')
20+
}
21+
}, [isOpen])
622

723
return (
824
<div className="md:hidden">
@@ -28,34 +44,41 @@ export default function MobileMenu() {
2844
</svg>
2945
</button>
3046

47+
{/* Transparent overlay */}
3148
{isOpen && (
32-
<div className="fixed inset-0 top-16 z-50 bg-background md:hidden">
33-
<nav className="h-full overflow-y-auto px-6 py-6">
34-
<div className="space-y-6">
35-
{navConfig.sidebar.map((section) => (
36-
<div key={section.title} className="space-y-3">
37-
<h4 className="text-sm font-medium leading-none tracking-tight">
38-
{section.title}
39-
</h4>
40-
<ul className="space-y-1">
41-
{section.items.map((item) => (
42-
<li key={item.href}>
43-
<a
44-
href={item.href}
45-
onClick={() => setIsOpen(false)}
46-
className="block rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
47-
>
48-
{item.title}
49-
</a>
50-
</li>
51-
))}
52-
</ul>
53-
</div>
54-
))}
55-
</div>
56-
</nav>
57-
</div>
49+
<div
50+
className={`fixed inset-0 z-40 bg-black/30 transition-opacity duration-300 ease-in-out md:hidden ${
51+
isVisible ? 'opacity-100' : 'opacity-0'
52+
}`}
53+
onClick={() => setIsOpen(false)}
54+
/>
5855
)}
56+
57+
{/* Menu panel - always rendered but translated off-screen when closed */}
58+
<div
59+
className={`fixed top-0 left-0 h-screen w-80 z-50 bg-white dark:bg-gray-900 shadow-2xl transform transition-transform duration-300 ease-in-out md:hidden ${
60+
isOpen && isVisible ? 'translate-x-0' : '-translate-x-full'
61+
}`}
62+
style={{ maxWidth: '80vw' }}
63+
>
64+
{/* Menu header */}
65+
<div className="flex items-center justify-end px-4 py-2">
66+
<button
67+
onClick={() => setIsOpen(false)}
68+
className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
69+
aria-label="Close menu"
70+
>
71+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
72+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
73+
</svg>
74+
</button>
75+
</div>
76+
77+
{/* Menu content */}
78+
<nav className="h-[calc(100vh-48px)] overflow-y-auto px-6 pb-16">
79+
<NavMenu onLinkClick={() => setIsOpen(false)} />
80+
</nav>
81+
</div>
5982
</div>
6083
)
61-
}
84+
}

src/components/NavMenu.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { navConfig } from '@/config/navigation'
2+
import Icon from './Icon'
3+
4+
interface NavMenuProps {
5+
currentPath?: string
6+
onLinkClick?: () => void
7+
}
8+
9+
export default function NavMenu({ currentPath, onLinkClick }: NavMenuProps) {
10+
return (
11+
<div className="space-y-6">
12+
{/* Top links section */}
13+
<div className="space-y-2">
14+
{navConfig.sidebarTopLinks.map((link) => (
15+
<a
16+
key={link.href}
17+
href={link.href}
18+
target={link.external ? "_blank" : undefined}
19+
rel={link.external ? "noopener noreferrer" : undefined}
20+
onClick={onLinkClick}
21+
className="flex items-center gap-3 px-3 py-1.5 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors no-underline group"
22+
style={{ textDecoration: 'none' }}
23+
>
24+
{link.icon && (
25+
<div className="flex items-center justify-center w-6 h-6 rounded-md border border-gray-200 dark:border-gray-700 group-hover:bg-black dark:group-hover:bg-white group-hover:border-black dark:group-hover:border-white transition-all">
26+
<Icon name={link.icon} className="w-4 h-4 text-gray-400 dark:text-gray-500 group-hover:text-white dark:group-hover:text-black" />
27+
</div>
28+
)}
29+
<span>{link.title}</span>
30+
</a>
31+
))}
32+
</div>
33+
34+
{/* Regular navigation sections */}
35+
{navConfig.sidebar.map((section) => (
36+
<div key={section.title}>
37+
<h4 className="text-sm font-medium leading-none tracking-tight text-gray-900 dark:text-gray-100 px-3 mb-4 mt-10">
38+
{section.title}
39+
</h4>
40+
<ul className="space-y-1 list-none m-0 p-0">
41+
{section.items.map((item) => (
42+
<li key={item.href}>
43+
<a
44+
href={item.href}
45+
onClick={onLinkClick}
46+
className={`block text-sm transition-colors hover:text-gray-900 dark:hover:text-gray-100 rounded-xl ${
47+
currentPath === item.href
48+
? 'text-gray-900 dark:text-gray-100 font-medium bg-[#e8e8e8] dark:bg-gray-800 px-3.5 py-2'
49+
: 'text-gray-600 dark:text-gray-400 font-normal hover:bg-gray-50 dark:hover:bg-gray-800/50 px-3 py-1.5'
50+
}`}
51+
style={{ textDecoration: 'none' }}
52+
>
53+
{item.title}
54+
</a>
55+
</li>
56+
))}
57+
</ul>
58+
</div>
59+
))}
60+
</div>
61+
)
62+
}

src/styles/global.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,24 @@
329329
stroke: #cccccc !important;
330330
}
331331

332+
/* Mobile menu blur effect - only blur specific content areas */
333+
.mobile-menu-open main {
334+
filter: blur(4px);
335+
transition: filter 300ms ease-in-out;
336+
}
337+
338+
.mobile-menu-open header .flex-shrink-0 {
339+
filter: blur(4px);
340+
transition: filter 300ms ease-in-out;
341+
}
342+
343+
.mobile-menu-open header .hidden.md\\:flex {
344+
filter: blur(4px);
345+
transition: filter 300ms ease-in-out;
346+
}
347+
348+
.mobile-menu-open body {
349+
overflow: hidden;
350+
}
351+
332352
}

0 commit comments

Comments
 (0)