fix: improve mobile sidebar UX with smooth transitions and proper touch handling

Add backdrop overlay with tap-to-dismiss, enforce 44px touch targets on the
toggle button, auto-close on route change, clean up z-index layering, remove
duplicate translate logic, and respect safe-area insets for standalone PWA mode.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-04-02 21:21:10 +02:00
parent 80a56a993b
commit 3daf720447
No known key found for this signature in database
2 changed files with 45 additions and 10 deletions

View file

@ -1,7 +1,7 @@
import { Button } from "@xtablo/ui/components/button";
import { MenuIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
import { MenuIcon, XIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Outlet, useLocation } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { SideNavigation } from "./NavigationBar";
import { OnboardingModal } from "./OnboardingModal";
@ -12,6 +12,7 @@ const ONBOARDING_STORAGE_KEY = "xtablo-onboarding-completed";
export function Layout() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [showOnboarding, setShowOnboarding] = useState(false);
const location = useLocation();
useEffect(() => {
// Check if user has completed onboarding
@ -21,30 +22,64 @@ export function Layout() {
}
}, []);
// Close mobile menu on route change
useEffect(() => {
setIsMobileMenuOpen(false);
}, [location.pathname]);
const handleOnboardingComplete = () => {
localStorage.setItem(ONBOARDING_STORAGE_KEY, "true");
setShowOnboarding(false);
};
const closeMobileMenu = useCallback(() => {
setIsMobileMenuOpen(false);
}, []);
return (
<div className="flex h-screen">
<OnboardingModal open={showOnboarding} onComplete={handleOnboardingComplete} />
{/* Mobile menu toggle button - 44px min touch target */}
<Button
variant="ghost"
size="icon"
className={twMerge(
"fixed z-50 md:hidden",
isMobileMenuOpen ? "top-2 left-55" : "top-2 left-4"
"fixed z-[60] md:hidden",
"min-w-[44px] min-h-[44px] w-11 h-11",
"top-2 left-2",
"safe-area-inset-left"
)}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
aria-expanded={isMobileMenuOpen}
>
<MenuIcon className="h-6 w-6" />
{isMobileMenuOpen ? (
<XIcon className="h-6 w-6" />
) : (
<MenuIcon className="h-6 w-6" />
)}
</Button>
{/* Mobile backdrop overlay */}
<div
className={twMerge(
"fixed md:relative transition-all duration-300 z-40",
"fixed inset-0 z-40 bg-black/50 md:hidden",
"transition-opacity duration-300 ease-in-out",
isMobileMenuOpen
? "opacity-100 pointer-events-auto"
: "opacity-0 pointer-events-none"
)}
onClick={closeMobileMenu}
aria-hidden="true"
/>
{/* Sidebar */}
<div
className={twMerge(
"fixed md:relative z-50 h-full",
"transition-transform duration-300 ease-in-out",
"md:transition-none",
isMobileMenuOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
)}
>

View file

@ -291,11 +291,11 @@ export const SideNavigation = ({ isMobileMenuOpen }: { isMobileMenuOpen: boolean
aria-label="Main navigation"
className={twMerge(
"group isolate flex flex-col overflow-y-auto overflow-x-hidden bg-navbar-background transition-all duration-300",
"fixed md:relative h-[calc(100vh-2rem)] md:h-screen z-50",
"h-full md:h-screen",
isCollapsed ? "w-16" : "w-48",
"md:flex",
"transform md:transform-none",
isMobileMenuOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
// On mobile in standalone mode, respect safe area insets
"pl-[env(safe-area-inset-left,0px)] pt-[env(safe-area-inset-top,0px)] pb-[env(safe-area-inset-bottom,0px)]"
)}
>
<div className="relative flex flex-col items-center px-2 py-3 w-full">