Files
boocode/apps/web/src/App.tsx
2026-05-20 14:56:02 +00:00

108 lines
3.7 KiB
TypeScript

import { useEffect, useState } from 'react';
import { BrowserRouter, Routes, Route, useParams } from 'react-router-dom';
import { api } from '@/api/client';
import { ProjectSidebar } from '@/components/ProjectSidebar';
import { RightRail } from '@/components/RightRail';
import { Home } from '@/pages/Home';
import { Project } from '@/pages/Project';
import { Session } from '@/pages/Session';
import { Settings } from '@/pages/Settings';
import { Toaster } from '@/components/ui/sonner';
import { useUserEvents } from '@/hooks/useUserEvents';
import { useTheme } from '@/lib/theme';
import { SidebarDrawerProvider, useSidebarDrawer } from '@/hooks/useSidebarDrawer';
import { RightRailDrawerProvider, useRightRailDrawer } from '@/hooks/useRightRailDrawer';
import { useViewport } from '@/hooks/useViewport';
function SessionRightRail() {
const { id } = useParams<{ id: string }>();
if (!id) return null;
return <RightRailForSession sessionId={id} />;
}
function RightRailForSession({ sessionId }: { sessionId: string }) {
const [projectId, setProjectId] = useState<string | null>(null);
useEffect(() => {
api.sessions
.get(sessionId)
.then((s) => setProjectId(s.project_id))
.catch((err) => console.warn('RightRail: failed to fetch session', err));
}, [sessionId]);
if (!projectId) return null;
// v1.6.2: rendered on all viewports. On mobile, RightRail itself renders as
// a right-side drawer toggled by the header's FolderTree button (via
// useRightRailDrawer). On desktop, it renders inline as before with its
// own internal open/close state.
return <RightRail projectId={projectId} />;
}
function MobileBackdrop() {
const { open, setOpen } = useSidebarDrawer();
const { isMobile } = useViewport();
if (!isMobile || !open) return null;
return (
<div
className="fixed inset-0 z-30 bg-black/40 md:hidden"
onClick={() => setOpen(false)}
aria-hidden="true"
/>
);
}
function MobileRightRailBackdrop() {
const { open, setOpen } = useRightRailDrawer();
const { isMobile } = useViewport();
if (!isMobile || !open) return null;
return (
<div
className="fixed inset-0 z-30 bg-black/40 md:hidden"
onClick={() => setOpen(false)}
aria-hidden="true"
/>
);
}
function AppShell() {
// themes-v1: useTheme() owns the matchMedia subscription for system mode
// and reconciles cache with /api/settings on mount. Mounted first so the
// theme class on <html> is correct before any child renders.
useTheme();
useUserEvents();
// v1.10.8c: h-dvh (dynamic viewport) instead of h-screen (100vh) so the
// root height excludes the iOS URL-bar overlay area. Without this, every
// descendant — including the terminal pane — measures itself against a
// height that extends behind the URL bar, and xterm allocates extra rows
// that scroll out of reach on iPhone.
return (
<div className="h-dvh flex bg-background text-foreground">
<ProjectSidebar />
<MobileBackdrop />
<main className="flex-1 flex flex-col min-w-0">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/project/:id" element={<Project />} />
<Route path="/session/:id" element={<Session />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</main>
<MobileRightRailBackdrop />
<Routes>
<Route path="/session/:id" element={<SessionRightRail />} />
</Routes>
<Toaster position="bottom-right" />
</div>
);
}
export default function App() {
return (
<BrowserRouter>
<SidebarDrawerProvider>
<RightRailDrawerProvider>
<AppShell />
</RightRailDrawerProvider>
</SidebarDrawerProvider>
</BrowserRouter>
);
}