feat(web): 移动端适配——顶栏防换行 + 抽屉式侧边栏
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 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模块);后端未动
This commit is contained in:
Aska committed 2026-09-07 03:13:24 +08:00
1 parent c42aae7797
commit cc3eecacac
5 files changed
+96 -19

No files matched your search

+58 -5
View File
@@ -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<ThemeMode>(readTheme);
@@ -24,7 +31,7 @@ export function Sidebar(): React.ReactNode {
};
return (
<nav className="glass hidden h-full w-[var(--mu-sidebar-w)] shrink-0 flex-col gap-1 border-r border-[var(--mu-glass-border)] p-3 md:flex">
<>
<div className="flex items-center gap-2 px-2 py-2">
<img src="/icon.svg" alt="Cyrene" className="h-7 w-7" />
<div className="leading-tight">
@@ -38,7 +45,7 @@ export function Sidebar(): React.ReactNode {
active={connected}
icon="channel"
label={connected ? "实时连接" : "未连接"}
onClick={() => {}}
onClick={onNavigate}
hint={wsStatus}
/>
@@ -52,12 +59,18 @@ export function Sidebar(): React.ReactNode {
<NavItem
icon="settings"
label="设置"
onClick={() => { window.location.hash = "#/admin/system"; }}
onClick={() => {
window.location.hash = "#/admin/system";
onNavigate();
}}
/>
<NavItem
icon="shield"
label="管理面板"
onClick={() => { window.location.hash = "#/admin"; }}
onClick={() => {
window.location.hash = "#/admin";
onNavigate();
}}
/>
<NavItem
icon={theme === "dark" ? "sun" : "moon"}
@@ -66,10 +79,50 @@ export function Sidebar(): React.ReactNode {
/>
{!hasToken ? <Tag tone="warn">未登录</Tag> : null}
</div>
</>
);
}
/** Desktop left rail. */
export function Sidebar(): React.ReactNode {
return (
<nav className="glass hidden h-full w-[var(--mu-sidebar-w)] shrink-0 flex-col gap-1 border-r border-[var(--mu-glass-border)] p-3 md:flex">
<NavContent onNavigate={() => {}} />
</nav>
);
}
/** Mobile drawer — overlay + scrim + spring slide-in panel. */
export function MobileNav({ open, onClose }: { open: boolean; onClose: () => void }): React.ReactNode {
if (!open) return null;
return (
<div className="fixed inset-0 z-40 md:hidden" role="dialog" aria-modal="true">
{/* scrim */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-[2px]"
onClick={onClose}
aria-label="关闭侧边栏"
/>
{/* panel */}
<nav className="glass absolute inset-y-0 left-0 flex w-[var(--mu-sidebar-w)] max-w-[80vw] flex-col gap-1 border-r border-[var(--mu-glass-border)] p-3"
style={{ animation: "mu-drawer-in 0.3s var(--mu-spring) both" }}
>
<div className="flex items-center justify-between px-2 pb-1">
<span className="text-xs font-medium text-[var(--mu-text-lo)]">菜单</span>
<button
onClick={onClose}
aria-label="关闭"
className="inline-flex h-8 w-8 items-center justify-center rounded-[var(--mu-radius)] text-[var(--mu-text-mid)] transition-all duration-200 hover:bg-[var(--mu-surface-4)] active:scale-95"
>
<Icon name="close" size={18} />
</button>
</div>
<NavContent onNavigate={onClose} />
</nav>
</div>
);
}
function NavItem({
icon,
label,