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 (
-