diff --git a/app/layout.tsx b/app/layout.tsx index 43c8ca6..f102972 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -11,7 +11,7 @@ import { MapPin, Boxes, } from "lucide-react"; -import { Sidebar, type NavItem } from "@/components/Sidebar"; +import { Sidebar, MobileNav, type NavItem } from "@/components/Sidebar"; import { ThemeProvider } from "@/components/ThemeProvider"; import { ThemeToggle } from "@/components/ThemeToggle"; import { GlobalSearch } from "@/components/GlobalSearch"; @@ -120,7 +120,15 @@ export default async function RootLayout({ children }: { children: React.ReactNo the Plant Map scrolled straight over it. Must stay above 1000, and below the mobile nav drawer (z-[1200]) in Sidebar.tsx. */}
-
+ {/* gap-2, not gap-3: the 44px trigger eats ~32px of the header row + after its -ml-3, and the search field is the only thing that + yields. Recovering 4px per gap keeps it usable at 375px. */} +
+ {/* The hamburger must live HERE, in the full-bleed header — not in + the content flex row below, where it indented every page on + mobile. See MobileNav for the portal detail. */} + + , pathname: stri ); } -export function Sidebar({ items }: { items: NavItem[] }) { +// The desktop rail and the mobile drawer render the same list from this one +// component, so the two navs can never drift apart. +function NavList({ items, onNavigate }: { items: NavItem[]; onNavigate?: () => void }) { const pathname = usePathname(); - const [mobileOpen, setMobileOpen] = useState(false); - - // Close drawer on navigation - useEffect(() => { - setMobileOpen(false); - }, [pathname]); - - // Lock body scroll when drawer is open - useEffect(() => { - document.body.style.overflow = mobileOpen ? "hidden" : ""; - return () => { - document.body.style.overflow = ""; - }; - }, [mobileOpen]); - - const renderNav = (onNavigate?: () => void) => ( + return ( ); +} + +/** + * Desktop rail only. It is `hidden lg:block`, so it contributes ZERO width below + * `lg` and must stay a pure sibling of `
` — anything visible on mobile + * rendered in this flex row steals width from the page. The mobile trigger lives + * in the header instead; see `MobileNav`. + */ +export function Sidebar({ items }: { items: NavItem[] }) { + return ( + + ); +} + +/** + * Mobile nav: the hamburger plus the drawer it opens. Rendered INSIDE the header, + * which is the whole point — the old version put the button in the content flex + * row, where its 36px was subtracted from every page below `lg` and indented the + * entire site on phones and tablets. + * + * The header is full-bleed, so a 44px trigger there costs no content width. + * `-ml-3` pulls the hit area into the header's own `px-4`, which lands the icon's + * left edge exactly on the logo's (16px) without changing the logo's position. + */ +export function MobileNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + const [open, setOpen] = useState(false); + const [mounted, setMounted] = useState(false); + + // Close drawer on navigation + useEffect(() => { + setOpen(false); + }, [pathname]); + + // Scroll lock, plus the SSR gate for the portal below — `document` does not + // exist while this component is server-rendered. + useEffect(() => { + setMounted(true); + document.body.style.overflow = open ? "hidden" : ""; + return () => { + document.body.style.overflow = ""; + }; + }, [open]); return ( <> - {/* Desktop: persistent sidebar (hidden on mobile) */} - - - {/* Mobile: hamburger in the top bar triggers a drawer */} - {/* The drawer must out-rank Leaflet. Leaflet's own `.leaflet-top` / - `.leaflet-bottom` control panes are `z-index: 1000` and its map panes - run 400–700, all in the root stacking context — so the old z-40/z-50 - pair lost and the Plant Map painted straight over the open menu. - Keep these values above 1000, and above the header's z-[1100]. */} - {mobileOpen && ( -
setMobileOpen(false)} - /> - )} - ); }