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:
parent
80a56a993b
commit
3daf720447
2 changed files with 45 additions and 10 deletions
|
|
@ -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"
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue