|
1 |
+"use client"; |
|
2 |
+ |
|
3 |
+import Link from "next/link"; |
|
4 |
+import { useEffect, useState } from "react"; |
|
5 |
+ |
|
6 |
+export function MobileMenu({ items }: { items: ReadonlyArray<readonly [string, string]> }): JSX.Element { |
|
7 |
+ const [open, setOpen] = useState(false); |
|
8 |
+ |
|
9 |
+ // Close on Escape; lock scroll while open. |
|
10 |
+ useEffect(() => { |
|
11 |
+ if (!open) return; |
|
12 |
+ const onKey = (event: KeyboardEvent) => { |
|
13 |
+ if (event.key === "Escape") setOpen(false); |
|
14 |
+ }; |
|
15 |
+ document.addEventListener("keydown", onKey); |
|
16 |
+ document.body.style.overflow = "hidden"; |
|
17 |
+ return () => { |
|
18 |
+ document.removeEventListener("keydown", onKey); |
|
19 |
+ document.body.style.overflow = ""; |
|
20 |
+ }; |
|
21 |
+ }, [open]); |
|
22 |
+ |
|
23 |
+ return ( |
|
24 |
+ <div className="sm:hidden"> |
|
25 |
+ <button |
|
26 |
+ type="button" |
|
27 |
+ aria-expanded={open} |
|
28 |
+ aria-label={open ? "Close menu" : "Open menu"} |
|
29 |
+ onClick={() => setOpen((value) => !value)} |
|
30 |
+ className="flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--border)] bg-[var(--surface-1)] text-[var(--ink)]" |
|
31 |
+ > |
|
32 |
+ {open ? ( |
|
33 |
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"> |
|
34 |
+ <path d="M3 3l10 10M13 3L3 13" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" /> |
|
35 |
+ </svg> |
|
36 |
+ ) : ( |
|
37 |
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"> |
|
38 |
+ <path d="M2 4.5h12M2 8h12M2 11.5h12" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" /> |
|
39 |
+ </svg> |
|
40 |
+ )} |
|
41 |
+ </button> |
|
42 |
+ |
|
43 |
+ {open && ( |
|
44 |
+ <> |
|
45 |
+ <button |
|
46 |
+ type="button" |
|
47 |
+ aria-label="Close menu" |
|
48 |
+ onClick={() => setOpen(false)} |
|
49 |
+ className="fixed inset-0 top-[57px] z-40 cursor-default bg-black/20 backdrop-blur-[2px]" |
|
50 |
+ /> |
|
51 |
+ <nav className="absolute inset-x-0 top-full z-50 border-b border-[var(--border)] bg-[var(--surface-1)] shadow-lg"> |
|
52 |
+ <ul className="mx-auto max-w-5xl px-4 py-2"> |
|
53 |
+ {items.map(([href, label]) => ( |
|
54 |
+ <li key={href} className="border-b border-[var(--grid)] last:border-b-0"> |
|
55 |
+ <Link |
|
56 |
+ href={href} |
|
57 |
+ onClick={() => setOpen(false)} |
|
58 |
+ className="block py-3.5 text-base font-medium text-[var(--ink)] active:bg-[var(--wash)]" |
|
59 |
+ > |
|
60 |
+ {label} |
|
61 |
+ </Link> |
|
62 |
+ </li> |
|
63 |
+ ))} |
|
64 |
+ </ul> |
|
65 |
+ </nav> |
|
66 |
+ </> |
|
67 |
+ )} |
|
68 |
+ </div> |
|
69 |
+ ); |
|
70 |
+} |
|
71 |
|