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) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 05:54:33 +00:00
parent 57c883b775
commit a643b5f67f
6 changed files with 162 additions and 13 deletions

View File

@@ -1,10 +1,12 @@
import { useEffect, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { ChevronRight } from 'lucide-react';
import { ChevronRight, Menu } from 'lucide-react';
import { api } from '@/api/client';
import type { Project, Session as SessionType } from '@/api/types';
import { sessionEvents } from '@/hooks/sessionEvents';
import { useActivePane } from '@/hooks/useActivePane';
import { useSidebarDrawer } from '@/hooks/useSidebarDrawer';
import { useViewport } from '@/hooks/useViewport';
import { Workspace } from '@/components/Workspace';
import { ModelPicker } from '@/components/ModelPicker';
@@ -16,6 +18,8 @@ export function Session() {
const [name, setName] = useState('');
const [editingName, setEditingName] = useState(false);
const active = useActivePane();
const { setOpen: setDrawerOpen } = useSidebarDrawer();
const { isMobile } = useViewport();
useEffect(() => {
if (!id) return;
@@ -83,7 +87,17 @@ export function Session() {
return (
<div className="flex-1 flex flex-col min-h-0">
<header className="border-b px-4 py-2 flex items-center gap-1.5 shrink-0 text-sm">
<header className="border-b px-4 py-2 flex items-center gap-1.5 shrink-0 text-sm" style={{ paddingTop: 'max(0.5rem, env(safe-area-inset-top))' }}>
{isMobile && (
<button
type="button"
onClick={() => setDrawerOpen(true)}
className="inline-flex items-center justify-center -ml-1 mr-1 min-w-[44px] min-h-[44px] rounded text-muted-foreground hover:bg-muted hover:text-foreground"
aria-label="Open sidebar"
>
<Menu className="size-5" />
</button>
)}
<Link to="/" className="text-muted-foreground hover:text-foreground">
Projects
</Link>