From cc3eecacacce46bf77314372fe645b42be2290bc Mon Sep 17 00:00:00 2001 From: Aska Date: Mon, 7 Sep 2026 03:13:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E7=A7=BB=E5=8A=A8=E7=AB=AF?= =?UTF-8?q?=E9=80=82=E9=85=8D=E2=80=94=E2=80=94=E9=A1=B6=E6=A0=8F=E9=98=B2?= =?UTF-8?q?=E6=8D=A2=E8=A1=8C=20+=20=E6=8A=BD=E5=B1=89=E5=BC=8F=E4=BE=A7?= =?UTF-8?q?=E8=BE=B9=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TopBar: 标题区 min-w-0+truncate 防换行;移动端(sm 下)隐藏心情/WS文字tag、管理/主题按钮shrink-0;新增汉堡按钮(md:hidden) - Nav: 抽共享 NavContent;桌面 Sidebar(dd:flex) 保持;移动端新增 MobileNav 抽屉(遮罩+弹簧滑入 md:hidden) - App: 加 menuOpen state,渲染 MobileNav + TopBar 传 onMenu - Icon: 新增 menu 图标;index.css: 加 mu-drawer-in 滑入动画 - npm run build 绿(77模块);后端未动 --- frontend/web/src/App.tsx | 6 ++- frontend/web/src/components/Icon.tsx | 5 +- frontend/web/src/components/Nav.tsx | 63 ++++++++++++++++++++++++-- frontend/web/src/components/TopBar.tsx | 31 ++++++++----- frontend/web/src/styles/index.css | 10 ++++ 5 files changed, 96 insertions(+), 19 deletions(-) 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 ( -