diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 5535a72c..808189f8 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1837,7 +1837,9 @@ }, "sidebar": { "expand": "展開側欄", - "collapse": "收合側欄" + "collapse": "收合側欄", + "openNavigation": "開啟導航選單", + "closeNavigation": "關閉導航選單" }, "settings": { "title": "系統設定", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 5535a72c..808189f8 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1837,7 +1837,9 @@ }, "sidebar": { "expand": "展開側欄", - "collapse": "收合側欄" + "collapse": "收合側欄", + "openNavigation": "開啟導航選單", + "closeNavigation": "關閉導航選單" }, "settings": { "title": "系統設定", diff --git a/apps/web/src/components/layout/app-layout.tsx b/apps/web/src/components/layout/app-layout.tsx index 4d89f2c0..102db462 100644 --- a/apps/web/src/components/layout/app-layout.tsx +++ b/apps/web/src/components/layout/app-layout.tsx @@ -15,9 +15,11 @@ */ import { useState, useEffect } from 'react' +import { useTranslations } from 'next-intl' import { Sidebar } from './sidebar' import { Header } from './header' import { DotMatrixBg } from '@/components/ui/dot-matrix-bg' +import { Z_INDEX } from '@/lib/constants/z-index' import { cn } from '@/lib/utils' import { useDashboardStore } from '@/stores/dashboard.store' @@ -50,8 +52,10 @@ export function AppLayout({ showBackground = true, fullBleed = false, }: AppLayoutProps) { + const tSidebar = useTranslations('sidebar') const [collapsed, setCollapsed] = useState(false) const [mobileShell, setMobileShell] = useState(false) + const [mobileNavigationOpen, setMobileNavigationOpen] = useState(false) const [mounted, setMounted] = useState(false) // Phase 19 修復: 全局 SSE 連接 @@ -73,6 +77,8 @@ export function AppLayout({ setMobileShell(isMobile) if (isMobile) { setCollapsed(false) + } else { + setMobileNavigationOpen(false) } } @@ -100,6 +106,11 @@ export function AppLayout({ // Save collapsed state to localStorage const handleToggle = () => { + if (mobileShell) { + setMobileNavigationOpen(value => !value) + return + } + const newState = !collapsed setCollapsed(newState) localStorage.setItem(SIDEBAR_STATE_KEY, String(newState)) @@ -110,8 +121,25 @@ export function AppLayout({ const effectiveCollapsed = mounted ? collapsed : false const shellCollapsed = effectiveCollapsed const contentOffsetClass = shellCollapsed - ? mobileShell ? 'ml-[48px]' : 'ml-[64px]' - : 'ml-[224px]' + ? 'ml-0 md:ml-[64px]' + : 'ml-0 md:ml-[224px]' + + useEffect(() => { + if (!mobileNavigationOpen) { + return + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setMobileNavigationOpen(false) + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => { + window.removeEventListener('keydown', handleKeyDown) + } + }, [mobileNavigationOpen]) return (
@@ -131,13 +159,49 @@ export function AppLayout({ collapsed={shellCollapsed} compact={mobileShell} onToggle={handleToggle} + className="hidden md:flex" /> + {mobileNavigationOpen && ( +
+
+ )} + {/* Header */}
setMobileNavigationOpen(true)} /> {/* Main Content */} diff --git a/apps/web/src/components/layout/header.tsx b/apps/web/src/components/layout/header.tsx index 59a0a0af..1bad8204 100644 --- a/apps/web/src/components/layout/header.tsx +++ b/apps/web/src/components/layout/header.tsx @@ -17,7 +17,7 @@ import { useCallback } from 'react' import { useTranslations } from 'next-intl' import { usePathname } from 'next/navigation' -import { ShieldCheck } from 'lucide-react' +import { Menu, ShieldCheck } from 'lucide-react' import { Z_INDEX } from '@/lib/constants/z-index' // ============================================================================= @@ -28,6 +28,7 @@ interface HeaderProps { locale: string sidebarCollapsed?: boolean compact?: boolean + onOpenNavigation?: () => void className?: string } @@ -39,11 +40,13 @@ export function Header({ locale, sidebarCollapsed = false, compact = false, + onOpenNavigation, className, }: HeaderProps) { const t = useTranslations('locale') const tBrand = useTranslations('brand') const tDashboard = useTranslations('dashboard') + const tSidebar = useTranslations('sidebar') const pathname = usePathname() const switchLocale = useCallback((newLocale: string) => { @@ -51,7 +54,7 @@ export function Header({ window.location.href = newPath }, [locale, pathname]) - const brandWidth = sidebarCollapsed ? 64 : 224 + const brandWidth = compact ? 64 : sidebarCollapsed ? 64 : 224 return (
+ {compact && ( + + )} + {/* Page title */} void + onNavigate?: () => void className?: string } @@ -64,7 +67,10 @@ export function Sidebar({ locale, collapsed = false, compact = false, + width, + showToggle = true, onToggle, + onNavigate, className, }: SidebarProps) { const t = useTranslations('nav') @@ -98,13 +104,13 @@ export function Sidebar({ item.aliases?.some(alias => isRouteActive(alias)) === true || item.relatedPaths?.some(path => isRouteActive(path)) === true ) - const sidebarWidth = compact && collapsed ? 48 : collapsed ? 64 : 224 + const sidebarWidth = width ?? (compact && collapsed ? 48 : collapsed ? 64 : 224) return ( ) }