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

@@ -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 (
<aside className="w-60 shrink-0 border-r bg-sidebar text-sidebar-foreground flex flex-col h-screen">
<aside className={asideCls}>
<div className="px-4 py-3 border-b flex items-center justify-between">
<NavLink to="/" className="font-semibold tracking-tight text-base">
BooCode