diff --git a/src/layouts/PublicLayout.tsx b/src/layouts/PublicLayout.tsx index e3afdcb..bdae3b9 100644 --- a/src/layouts/PublicLayout.tsx +++ b/src/layouts/PublicLayout.tsx @@ -6,7 +6,7 @@ import { X, } from "lucide-react"; import { AnimatePresence, m } from "framer-motion"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { Link, useLocation, useNavigate, useOutlet } from "react-router-dom"; import { pageTransition } from "../motion"; import { ArkLogoMark } from "../components/ArkLogoMark"; @@ -309,6 +309,17 @@ export function PublicLayout() { const desktopMenuButtonRef = useRef(null); const desktopSearchRef = useRef(null); const desktopSearchPanelRef = useRef(null); + // Runtime fit detection for the desktop nav. Different languages and page + // titles change the natural width of the nav and brand, so a fixed CSS + // breakpoint (e.g. `xl:flex`) clips or overlaps text in some translations. + // Instead we render a hidden “ghost” nav, measure its scrollWidth against + // the space left after the brand + right-side actions, and toggle to the + // burger menu the moment it would no longer fit. + const [showInlineNav, setShowInlineNav] = useState(true); + const headerRowRef = useRef(null); + const headerBrandRef = useRef(null); + const headerRightRef = useRef(null); + const ghostNavRef = useRef(null); const nav = useNavigate(); const lp = useLocalizedPath(); @@ -387,6 +398,50 @@ export function PublicLayout() { }, [lang]); const popularHref = lp("/browse?sort=popular"); + // Decide whether the inline nav fits next to the brand and right-side + // actions for the current language. Hysteresis (60px) on the + // burger -> inline transition keeps the layout from oscillating when the + // right-side width shrinks slightly after we hide the burger button. + useLayoutEffect(() => { + const row = headerRowRef.current; + const brand = headerBrandRef.current; + const right = headerRightRef.current; + const ghost = ghostNavRef.current; + if (!row || !brand || !right || !ghost) return; + + const measure = () => { + const rowWidth = row.clientWidth; + if (rowWidth === 0) return; + const rowStyle = window.getComputedStyle(row); + const rowGap = parseFloat(rowStyle.columnGap || rowStyle.gap || "0"); + const totalGap = rowGap * 2; // brand <-> nav <-> right + const available = + rowWidth - brand.offsetWidth - right.offsetWidth - totalGap; + const needed = ghost.scrollWidth; + + setShowInlineNav((current) => { + if (current) return needed <= available; + // Need real breathing room before switching back, to avoid flicker + // when the right-side width shrinks after the burger button hides. + return needed + 60 <= available; + }); + }; + + measure(); + const ro = new ResizeObserver(measure); + ro.observe(row); + ro.observe(ghost); + ro.observe(brand); + ro.observe(right); + return () => ro.disconnect(); + }, [lang, pageTitle, t]); + + // When the layout grows enough that we move back to the inline nav, the + // burger drawer would otherwise stay stuck open with no visible toggle. + useEffect(() => { + if (showInlineNav) setOpen(false); + }, [showInlineNav]); + const goSearch = () => { const s = q.trim(); if (!s) return; @@ -611,12 +666,18 @@ export function PublicLayout() {
- {/* Single row (md+): logo | nav | search + language. Header now spans - the full viewport so wide monitors get more breathing room, and - the nav switches to the burger menu at `xl` so it never has to - clip / horizontally-scroll the labels. */} -
-
+ {/* Single row (md+): logo | nav | search + language. The nav and the + burger toggle are driven by a runtime fit measurement (see + useLayoutEffect above) instead of a fixed Tailwind breakpoint, + so each language collapses to the burger at its own width. */} +
+
{/* Logo → home; page-name text → scroll to top of the current page. */}
+ {/* Hidden measurement copy of the nav. Always rendered so the + ResizeObserver can ask the browser “how wide would the inline + nav need to be?” without flickering the visible layout. */} -
+ {showInlineNav ? ( + + ) : ( +
+ )} + +
+ {showInlineNav ? null : ( + + )}
@@ -746,7 +835,7 @@ export function PublicLayout() { {open ? (