feat: mobile hamburger menu with full-screen overlay for categories
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import { Search } from 'lucide-react'
|
import { Search, Menu, X } from 'lucide-react'
|
||||||
import type { Category } from '@/lib/types'
|
import type { Category } from '@/lib/types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -11,50 +12,125 @@ interface Props {
|
|||||||
|
|
||||||
export default function NavbarClient({ categories }: Props) {
|
export default function NavbarClient({ categories }: Props) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMenuOpen(false)
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (menuOpen) {
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
return () => { document.body.style.overflow = '' }
|
||||||
|
}, [menuOpen])
|
||||||
|
|
||||||
function openSearch() {
|
function openSearch() {
|
||||||
window.dispatchEvent(new CustomEvent('kotobane:open-search'))
|
window.dispatchEvent(new CustomEvent('kotobane:open-search'))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bg-bg-elevated border-b border-border sticky top-0 z-40">
|
<>
|
||||||
<div className="max-w-[1200px] mx-auto px-6 h-14 flex items-center gap-6">
|
<nav className="bg-bg-elevated border-b border-border sticky top-0 z-40">
|
||||||
<Link href="/" className="text-lg font-black text-accent tracking-[3px] shrink-0">
|
<div className="max-w-[1200px] mx-auto px-4 h-14 flex items-center">
|
||||||
言羽
|
{/* Mobile hamburger */}
|
||||||
</Link>
|
<button
|
||||||
|
onClick={() => setMenuOpen(true)}
|
||||||
|
className="sm:hidden mr-3 text-text-muted hover:text-text-secondary transition-colors"
|
||||||
|
aria-label="Open menu"
|
||||||
|
>
|
||||||
|
<Menu size={20} />
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className="w-px h-4 bg-border shrink-0" />
|
{/* Logo — center on mobile, left on desktop */}
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-lg font-black text-accent tracking-[3px] shrink-0 sm:mr-6 sm:ml-0 mx-auto sm:mx-0"
|
||||||
|
>
|
||||||
|
言羽
|
||||||
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center gap-1 overflow-x-auto">
|
{/* Desktop nav links */}
|
||||||
{categories.map((cat) => {
|
<div className="hidden sm:flex items-center gap-1 flex-1 overflow-x-auto">
|
||||||
const isActive = pathname.startsWith(`/${cat.slug}`)
|
<div className="w-px h-4 bg-border shrink-0 mr-4" />
|
||||||
return (
|
{categories.map((cat) => {
|
||||||
<Link
|
const isActive = pathname.startsWith(`/${cat.slug}`)
|
||||||
key={cat.slug}
|
return (
|
||||||
href={`/${cat.slug}`}
|
<Link
|
||||||
className={`text-xs font-medium px-3 py-1 rounded-sm whitespace-nowrap transition-colors ${
|
key={cat.slug}
|
||||||
isActive
|
href={`/${cat.slug}`}
|
||||||
? 'text-accent'
|
className={`text-xs font-medium px-3 py-1 rounded-sm whitespace-nowrap transition-colors outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg-elevated ${
|
||||||
: 'text-text-muted hover:text-text-secondary'
|
isActive
|
||||||
}`}
|
? 'text-accent'
|
||||||
>
|
: 'text-text-muted hover:text-text-secondary'
|
||||||
{cat.name}
|
}`}
|
||||||
</Link>
|
>
|
||||||
)
|
{cat.name}
|
||||||
})}
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<button
|
||||||
|
onClick={openSearch}
|
||||||
|
className="ml-auto sm:ml-0 flex items-center gap-2 text-text-muted hover:text-text-secondary transition-colors shrink-0"
|
||||||
|
aria-label="Open search"
|
||||||
|
>
|
||||||
|
<Search size={16} />
|
||||||
|
<kbd className="hidden sm:inline-flex bg-bg-card border border-border rounded px-1.5 py-0.5 text-[10px] font-mono">
|
||||||
|
⌘K
|
||||||
|
</kbd>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<button
|
{/* Mobile overlay */}
|
||||||
onClick={openSearch}
|
{menuOpen && (
|
||||||
className="ml-auto flex items-center gap-2 text-text-muted hover:text-text-secondary transition-colors shrink-0"
|
<div className="fixed inset-0 z-50 sm:hidden">
|
||||||
aria-label="Open search"
|
<div className="absolute inset-0 bg-black/60" onClick={() => setMenuOpen(false)} />
|
||||||
>
|
<div className="relative w-full h-full bg-bg-elevated flex flex-col">
|
||||||
<Search size={16} />
|
{/* Overlay header */}
|
||||||
<kbd className="hidden sm:inline-flex bg-bg-card border border-border rounded px-1.5 py-0.5 text-[10px] font-mono">
|
<div className="flex items-center justify-between px-4 h-14 border-b border-border">
|
||||||
⌘K
|
<span className="text-lg font-black text-accent tracking-[3px]">言羽</span>
|
||||||
</kbd>
|
<button
|
||||||
</button>
|
onClick={() => setMenuOpen(false)}
|
||||||
</div>
|
className="text-text-muted hover:text-text-secondary transition-colors"
|
||||||
</nav>
|
aria-label="Close menu"
|
||||||
|
>
|
||||||
|
<X size={22} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category list */}
|
||||||
|
<div className="flex-1 overflow-y-auto py-4">
|
||||||
|
{categories.map((cat) => {
|
||||||
|
const isActive = pathname.startsWith(`/${cat.slug}`)
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={cat.slug}
|
||||||
|
href={`/${cat.slug}`}
|
||||||
|
className={`block px-6 py-3.5 text-sm font-medium transition-colors ${
|
||||||
|
isActive
|
||||||
|
? 'text-accent bg-bg-card border-l-2 border-accent'
|
||||||
|
: 'text-text-muted hover:text-text-secondary hover:bg-bg-card'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cat.name}
|
||||||
|
{cat.description && (
|
||||||
|
<span className="block text-[11px] text-text-disabled font-normal mt-0.5">
|
||||||
|
{cat.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user