feat(layout): full-screen mobile menu drawer matching Figma 4164-5733

Rebuild the mobile hamburger menu as a full-screen drawer that matches
the Figma design 1:1 — five nav items (全部资料 / 资料分类 / 官方推荐 /
最新更新 / 热门资料), transparent item backgrounds over the ark-bg
drawer, hairline dividers at #2B2B37, gold text on the active route,
and the existing WalletButton compact pill as the bottom CTA. Drop the
chevron-right indicators per the rendered Figma frame and remove the
old 收藏 row since it's not in the design.

Also move the drawer JSX out of <header sticky top-0 z-40> and render
it as a sibling at the layout root. The sticky+z-index header was
creating a stacking context that trapped the drawer's z-50 fixed below
the bottom nav at z-40 global, so the drawer never reached the
foreground.

Add the same iOS-safe body scroll lock used for the search overlay so
the underlying page doesn't drift while the drawer is open.
This commit is contained in:
TerryM
2026-06-03 21:59:31 +08:00
parent 2ef26390be
commit 39f9cba8c7
2 changed files with 113 additions and 71 deletions

View File

@@ -1,10 +1,4 @@
import {
ChevronDown,
Heart,
Menu,
Search as SearchIcon,
X,
} from "lucide-react";
import { ChevronDown, Menu, Search as SearchIcon, X } from "lucide-react";
import { AnimatePresence, m } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import { Link, useLocation, useNavigate, useOutlet } from "react-router-dom";
@@ -82,10 +76,6 @@ function navClassName(active: boolean) {
].join(" ");
}
function mobileMenuNavClassName(active: boolean) {
return `${navClassName(active)} w-fit justify-self-start`;
}
const dropdownAnimationClass = "ark-header-popover-enter";
const headerMenuAnimationClass = "ark-header-menu-enter";
@@ -493,6 +483,33 @@ export function PublicLayout() {
};
}, [mobileSearchOpen]);
// Lock background scroll while the full-screen mobile menu drawer is open.
useEffect(() => {
if (!open) return;
const scrollY = window.scrollY;
const body = document.body;
const prev = {
position: body.style.position,
top: body.style.top,
left: body.style.left,
right: body.style.right,
width: body.style.width,
};
body.style.position = "fixed";
body.style.top = `-${scrollY}px`;
body.style.left = "0";
body.style.right = "0";
body.style.width = "100%";
return () => {
body.style.position = prev.position;
body.style.top = prev.top;
body.style.left = prev.left;
body.style.right = prev.right;
body.style.width = prev.width;
window.scrollTo(0, scrollY);
};
}, [open]);
return (
<div className="flex min-h-[100dvh] flex-col bg-ark-bg">
<DocumentMeta />
@@ -691,68 +708,64 @@ export function PublicLayout() {
</div>
</div>
</div>
{open ? (
<div
ref={menuRef}
className={`${headerMenuAnimationClass} fixed inset-x-0 top-[64px] z-50 grid gap-2 bg-[#08070c] px-4 py-3 shadow-2xl shadow-black/50 min-[440px]:px-5 sm:px-6 md:top-[70px] md:px-9 min-[1000px]:hidden`}
>
<Link
to={lp("/browse")}
className={mobileMenuNavClassName(na("browseAll"))}
aria-current={na("browseAll") ? "page" : undefined}
onClick={() => setOpen(false)}
>
{t("all")}
</Link>
<Link
to={lp("/categories")}
className={mobileMenuNavClassName(na("categories"))}
aria-current={na("categories") ? "page" : undefined}
onClick={() => setOpen(false)}
>
{t("categories")}
</Link>
<Link
to={lp("/official-recommendations")}
className={mobileMenuNavClassName(na("browseRecommended"))}
aria-current={na("browseRecommended") ? "page" : undefined}
onClick={() => setOpen(false)}
>
{t("official")}
</Link>
<Link
to={lp("/browse?sort=latest")}
className={mobileMenuNavClassName(na("browseLatest"))}
aria-current={na("browseLatest") ? "page" : undefined}
onClick={() => setOpen(false)}
>
{t("latest")}
</Link>
<Link
to={popularHref}
className={mobileMenuNavClassName(na("browsePopular"))}
aria-current={na("browsePopular") ? "page" : undefined}
onClick={() => setOpen(false)}
>
{t("popular")}
</Link>
<Link
to={lp("/favorites")}
className={`${mobileMenuNavClassName(na("favorites"))} flex items-center gap-2`}
aria-current={na("favorites") ? "page" : undefined}
onClick={() => setOpen(false)}
>
<Heart size={16} strokeWidth={2} />
{t("favorites")}
</Link>
<div className="mt-2 w-full max-w-xs">
<WalletButton compact onOpenLogin={() => setOpen(false)} />
</div>
</div>
) : null}
</header>
{open ? (
<div
ref={menuRef}
className={`${headerMenuAnimationClass} fixed inset-x-0 bottom-0 top-[64px] z-50 flex flex-col bg-ark-bg md:top-[70px] min-[1000px]:hidden`}
>
<nav className="flex-1 overflow-y-auto px-5 pt-2">
{(
[
{
to: lp("/browse"),
label: t("all"),
active: na("browseAll"),
},
{
to: lp("/categories"),
label: t("categories"),
active: na("categories"),
},
{
to: lp("/official-recommendations"),
label: t("official"),
active: na("browseRecommended"),
},
{
to: lp("/browse?sort=latest"),
label: t("latest"),
active: na("browseLatest"),
},
{
to: popularHref,
label: t("popular"),
active: na("browsePopular"),
},
] as const
).map((item) => (
<Link
key={item.to}
to={item.to}
aria-current={item.active ? "page" : undefined}
onClick={() => setOpen(false)}
className={`flex h-[68px] items-center border-b border-[#2B2B37] text-[15px] font-medium leading-[20px] outline-none transition-colors focus-visible:text-ark-gold ${
item.active
? "text-ark-gold"
: "text-[#A8A9AE] [@media(hover:hover)]:hover:text-ark-gold"
}`}
>
{item.label}
</Link>
))}
</nav>
<div className="px-5 pb-[max(env(safe-area-inset-bottom),20px)] pt-4">
<WalletButton compact onOpenLogin={() => setOpen(false)} />
</div>
</div>
) : null}
{mobileSearchOpen ? (
<SearchPanel
lang={lang}