From a643b5f67fd9871bd2c6b5e65ee594dcf6e4bead Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Sat, 16 May 2026 05:54:33 +0000 Subject: [PATCH] feat(mobile): viewport hook + sidebar drawer + hamburger headers - useViewport: matchMedia-based hook (no resize polling). Breakpoints mobile <768 / tablet 768-1023 / desktop >=1024. SSR-safe. - useSidebarDrawer: Context provider with open/setOpen/toggle + auto-close on useLocation().pathname change. - App.tsx: wraps SidebarDrawerProvider around AppShell, renders a MobileBackdrop (z-30) when the drawer is open on mobile. - ProjectSidebar: aside is fixed/translate-x-full off-screen on mobile, slides in (z-40, 200ms transform) when drawerOpen. Inline column on desktop, unchanged. - Session.tsx + Project.tsx: hamburger (Menu icon, >=44x44 min) on mobile opens the drawer. Headers gain paddingTop: max(0.75rem, env(safe-area-inset-top)) for notch devices. Home.tsx left alone (sidebar content duplicates the home page). Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/src/App.tsx | 20 +++++++++- apps/web/src/components/ProjectSidebar.tsx | 19 ++++++++- apps/web/src/hooks/useSidebarDrawer.tsx | 36 +++++++++++++++++ apps/web/src/hooks/useViewport.ts | 45 ++++++++++++++++++++++ apps/web/src/pages/Project.tsx | 37 +++++++++++++----- apps/web/src/pages/Session.tsx | 18 ++++++++- 6 files changed, 162 insertions(+), 13 deletions(-) create mode 100644 apps/web/src/hooks/useSidebarDrawer.tsx create mode 100644 apps/web/src/hooks/useViewport.ts diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index e488e28..af68dea 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -8,6 +8,8 @@ import { Project } from '@/pages/Project'; import { Session } from '@/pages/Session'; import { Toaster } from '@/components/ui/sonner'; import { useUserEvents } from '@/hooks/useUserEvents'; +import { SidebarDrawerProvider, useSidebarDrawer } from '@/hooks/useSidebarDrawer'; +import { useViewport } from '@/hooks/useViewport'; function SessionRightRail() { const { id } = useParams<{ id: string }>(); @@ -27,11 +29,25 @@ function RightRailForSession({ sessionId }: { sessionId: string }) { return ; } +function MobileBackdrop() { + const { open, setOpen } = useSidebarDrawer(); + const { isMobile } = useViewport(); + if (!isMobile || !open) return null; + return ( +
setOpen(false)} + aria-hidden="true" + /> + ); +} + function AppShell() { useUserEvents(); return (
+
} /> @@ -50,7 +66,9 @@ function AppShell() { export default function App() { return ( - + + + ); } diff --git a/apps/web/src/components/ProjectSidebar.tsx b/apps/web/src/components/ProjectSidebar.tsx index 8c9159f..9d26f2c 100644 --- a/apps/web/src/components/ProjectSidebar.tsx +++ b/apps/web/src/components/ProjectSidebar.tsx @@ -20,6 +20,8 @@ import { import { AddProjectModal } from './AddProjectModal'; import { api } from '@/api/client'; import { useSidebar } from '@/hooks/useSidebar'; +import { useSidebarDrawer } from '@/hooks/useSidebarDrawer'; +import { useViewport } from '@/hooks/useViewport'; import type { SidebarProject } from '@/api/types'; import { giteaUrlFor } from '@/lib/projectUrls'; import { cn } from '@/lib/utils'; @@ -195,8 +197,23 @@ export function ProjectSidebar() { const rowCls = (active: boolean) => active ? 'bg-sidebar-accent text-sidebar-accent-foreground' : 'hover:bg-sidebar-accent/60'; + const { open: drawerOpen } = useSidebarDrawer(); + const { isMobile } = useViewport(); + + // On mobile the sidebar is a slide-in drawer (fixed, z-40, off-screen by + // default). On desktop it sits inline as a normal flex column. The + // backdrop is rendered by AppShell; drawer-open state lives in + // SidebarDrawerProvider. + const asideCls = isMobile + ? cn( + 'fixed inset-y-0 left-0 z-40 w-60 border-r bg-sidebar text-sidebar-foreground flex flex-col', + 'transition-transform duration-200 ease-out', + drawerOpen ? 'translate-x-0' : '-translate-x-full', + ) + : 'w-60 shrink-0 border-r bg-sidebar text-sidebar-foreground flex flex-col h-screen'; + return ( -