feat: add mobile nav drawer

This commit is contained in:
cita
2026-03-10 15:00:43 +08:00
parent 2d8e1b9019
commit d91a160455
2 changed files with 75 additions and 25 deletions
+13
View File
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
describe('App mobile sidebar', () => {
it('renders hamburger trigger for mobile navigation', () => {
const source = readFileSync(resolve(process.cwd(), 'src/web/App.tsx'), 'utf8');
expect(source).toContain('aria-label="Open navigation"');
expect(source).toContain('MobileDrawer');
expect(source).toContain('mobile-nav');
});
});
+37
View File
@@ -17,6 +17,7 @@ import { resolveLoginErrorMessage } from './loginError.js';
import { SITE_DOCS_URL } from './docsLink.js'; import { SITE_DOCS_URL } from './docsLink.js';
import { useAnimatedVisibility } from './components/useAnimatedVisibility.js'; import { useAnimatedVisibility } from './components/useAnimatedVisibility.js';
import { useIsMobile } from './components/useIsMobile.js'; import { useIsMobile } from './components/useIsMobile.js';
import { MobileDrawer } from './components/MobileDrawer.js';
const Dashboard = lazy(() => import('./pages/Dashboard.js')); const Dashboard = lazy(() => import('./pages/Dashboard.js'));
const Sites = lazy(() => import('./pages/Sites.js')); const Sites = lazy(() => import('./pages/Sites.js'));
const Accounts = lazy(() => import('./pages/Accounts.js')); const Accounts = lazy(() => import('./pages/Accounts.js'));
@@ -378,6 +379,7 @@ function AppShell() {
const [showProfileModal, setShowProfileModal] = useState(false); const [showProfileModal, setShowProfileModal] = useState(false);
const [showSearch, setShowSearch] = useState(false); const [showSearch, setShowSearch] = useState(false);
const [showNotifications, setShowNotifications] = useState(false); const [showNotifications, setShowNotifications] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const themeMenuPresence = useAnimatedVisibility(showThemeMenu, 160); const themeMenuPresence = useAnimatedVisibility(showThemeMenu, 160);
const userMenuPresence = useAnimatedVisibility(showUserMenu, 160); const userMenuPresence = useAnimatedVisibility(showUserMenu, 160);
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
@@ -551,6 +553,17 @@ function AppShell() {
return ( return (
<> <>
<header className="topbar"> <header className="topbar">
{isMobile && (
<button
className="topbar-icon-btn"
aria-label="Open navigation"
onClick={() => setDrawerOpen(true)}
>
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
)}
<div className="topbar-logo"> <div className="topbar-logo">
<img src="/logo.png" alt="Metapi" style={{ width: 28, height: 28, borderRadius: 6 }} /> <img src="/logo.png" alt="Metapi" style={{ width: 28, height: 28, borderRadius: 6 }} />
<span className="topbar-logo-text">Metapi</span> <span className="topbar-logo-text">Metapi</span>
@@ -673,6 +686,29 @@ function AppShell() {
</header> </header>
<div className="app-layout"> <div className="app-layout">
{isMobile ? (
<MobileDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)}>
<nav className="mobile-nav">
{sidebarGroups.map((group) => (
<div key={group.label} className="mobile-nav-group">
<div className="mobile-nav-label">{t(group.label)}</div>
{group.items.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.to === '/' || item.to === '/settings'}
className={({ isActive }) => `mobile-nav-item ${isActive ? 'active' : ''}`}
onClick={() => setDrawerOpen(false)}
>
{item.icon}
<span>{t(item.label)}</span>
</NavLink>
))}
</div>
))}
</nav>
</MobileDrawer>
) : (
<aside className={`sidebar ${sidebarCollapsed ? 'collapsed' : ''}`}> <aside className={`sidebar ${sidebarCollapsed ? 'collapsed' : ''}`}>
{sidebarGroups.map((group) => ( {sidebarGroups.map((group) => (
<div key={group.label} className="sidebar-group"> <div key={group.label} className="sidebar-group">
@@ -699,6 +735,7 @@ function AppShell() {
{!sidebarCollapsed && <span>{t('收起侧边栏')}</span>} {!sidebarCollapsed && <span>{t('收起侧边栏')}</span>}
</button> </button>
</aside> </aside>
)}
<main className="main-content"> <main className="main-content">
<PageTransition> <PageTransition>