diff --git a/frontend/web/src/App.tsx b/frontend/web/src/App.tsx index a0f35e9..343cc7d 100644 --- a/frontend/web/src/App.tsx +++ b/frontend/web/src/App.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { TopBar } from "./components/TopBar"; -import { Sidebar } from "./components/Nav"; +import { Sidebar, MobileNav } from "./components/Nav"; import { ModeRouter } from "./modes/ModeRouter"; import { AdminApp } from "./admin/AdminApp"; import { parseAdminHash } from "./admin/router"; @@ -13,6 +13,7 @@ export default function App(): React.ReactNode { // Apply the persisted (dark-first) theme and open the realtime channel only // when we hold a token — otherwise the gateway rejects the handshake with 401. const [route, setRoute] = useState(() => parseAdminHash(window.location.hash)); + const [menuOpen, setMenuOpen] = useState(false); const setCurrentPersona = useAppStore((s) => s.setCurrentPersona); useEffect(() => { @@ -67,13 +68,14 @@ export default function App(): React.ReactNode { return (
- + setMenuOpen(true)} />
+ setMenuOpen(false)} />
); } diff --git a/frontend/web/src/components/Icon.tsx b/frontend/web/src/components/Icon.tsx index 6b8a267..cf171c6 100644 --- a/frontend/web/src/components/Icon.tsx +++ b/frontend/web/src/components/Icon.tsx @@ -46,7 +46,8 @@ export type IconName = | "arrow" // → direction (arrow_forward_rounded) | "terminal" // 🖥 code (terminal_rounded) | "image" // 🖼 attachment/vision (image_rounded) - | "phone" // 📞 voice call / hang up (call_rounded); + | "phone" // 📞 voice call / hang up (call_rounded) + | "menu" // ☰ drawer/open nav (menu_rounded); const ICON_PATHS: Record = { chart: @@ -125,6 +126,8 @@ const ICON_PATHS: Record = { "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2ZM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5Z", phone: "M6.6 10.8a15.1 15.1 0 0 0 6.6 6.6l2.2-2.2c.3-.3.7-.4 1-.2 1.1.4 2.3.6 3.6.6.6 0 1 .4 1 1V20c0 .6-.4 1-1 1C10.6 21 3 13.4 3 4c0-.6.4-1 1-1h3.5c.6 0 1 .4 1 1 0 1.3.2 2.5.6 3.6.1.3 0 .7-.2 1l-2.3 2.2Z", + menu: + "M4 6h16v2H4V6Zm0 5h16v2H4v-2Zm0 5h16v2H4v-2Z", }; export interface IconProps { diff --git a/frontend/web/src/components/Nav.tsx b/frontend/web/src/components/Nav.tsx index cb2f268..85bd1b2 100644 --- a/frontend/web/src/components/Nav.tsx +++ b/frontend/web/src/components/Nav.tsx @@ -9,8 +9,15 @@ import { Tag } from "./primitives"; * Left rail — NOT a behavior-mode list (design/01 §3: mode switching is driven * at runtime by the agent / user, and rendered per message). This is a small * navigation rail: brand + connection status + theme + admin/settings entry. + * + * Two surface variants share the same NavContent: + * - Sidebar (desktop): fixed rail, `hidden md:flex`. + * - MobileNav(mobile): drawer overlay opened from TopBar's hamburger button + * (`md:hidden`), with a scrim + a spring slide-in panel. Content is shared + * so desktop and mobile never diverge. */ -export function Sidebar(): React.ReactNode { + +function NavContent({ onNavigate }: { onNavigate: () => void }): React.ReactNode { const wsStatus = useAppStore((s) => s.wsStatus); const mood = useAppStore((s) => s.mood); const [theme, setTheme] = useState(readTheme); @@ -24,7 +31,7 @@ export function Sidebar(): React.ReactNode { }; return ( -