);
}
export function PublicLayout() {
const { t, lang, setLang } = useI18n();
const { pathname, search, hash } = useLocation();
const outlet = useOutlet();
const [open, setOpen] = useState(false);
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const [q, setQ] = useState("");
const menuRef = useRef(null);
const mobileMenuButtonRef = useRef(null);
const desktopMenuButtonRef = useRef(null);
const nav = useNavigate();
const na = (which: PublicNavWhich) =>
navIsActive(pathname, search, hash, which);
const isHome = pathname === "/";
const footerInContentFlow = pathname === "/browse";
// Current page name shown in the header brand slot (falls back to the brand).
const pageTitle = usePageTitle();
const popularHref = "/browse?sort=popular";
const goSearch = () => {
const s = q.trim();
if (!s) return;
nav(`/browse?q=${encodeURIComponent(s)}`);
setOpen(false);
setMobileSearchOpen(false);
};
useEffect(() => {
window.dispatchEvent(
new CustomEvent(publicMenuOpenChangeEvent, { detail: open }),
);
}, [open]);
useEffect(() => {
if (!open) return;
const closeOnOutside = (event: MouseEvent | TouchEvent) => {
const target = event.target as Node;
if (
menuRef.current?.contains(target) ||
mobileMenuButtonRef.current?.contains(target) ||
desktopMenuButtonRef.current?.contains(target)
) {
return;
}
setOpen(false);
};
const closeOnScroll = () => setOpen(false);
document.addEventListener("mousedown", closeOnOutside);
document.addEventListener("touchstart", closeOnOutside);
window.addEventListener("scroll", closeOnScroll);
return () => {
document.removeEventListener("mousedown", closeOnOutside);
document.removeEventListener("touchstart", closeOnOutside);
window.removeEventListener("scroll", closeOnScroll);
};
}, [open]);
// Lock background scroll while the mobile search overlay is open.
// Uses the iOS-compatible position-fixed pattern so the underlying page
// doesn't move at all (overflow:hidden alone is not enough on iOS Safari).
useEffect(() => {
if (!mobileSearchOpen) 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);
};
}, [mobileSearchOpen]);
return (
{/* Logo → home; page-name text → scroll to top of the current page. */}
{
if (isHome) {
e.preventDefault();
window.scrollTo({ top: 0, behavior: "smooth" });
}
}}
className="shrink-0 rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070c]"
>
{/* Single row (md+): logo | scrollable nav (左對齊,可橫向滑動) | 搜尋 + 語言 */}
{/* Logo → home; page-name text → scroll to top of the current page. */}
{
if (isHome) {
e.preventDefault();
window.scrollTo({ top: 0, behavior: "smooth" });
}
}}
className="shrink-0 rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg"
>