feat(web): 移动端适配——顶栏防换行 + 抽屉式侧边栏
- 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:
1 parent
c42aae7797
commit
cc3eecacac
5 files changed
+96
-19
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user