"use client"; /** * Sidebar — 左側導航 * * 規格來源: * - .autoflow/03-design/components.md §3.1(雲端版 Sidebar 變更) * - .autoflow/03-design/pages.md(頁面總覽) * * 設計重點: * - 使用 Lucide Icon(對齊 design-review.md C1 建議,取代 local-tool 的單字母佔位) * - 當前路徑用 usePathname() 自動偵測('/' 嚴格比對、其他用 startsWith) * - active state 採用 `bg-sidebar-accent text-sidebar-accent-foreground` * — 利用既有的 sidebar-* Design Tokens(globals.css 已定義 Light/Dark 兩套) * - Desktop 固定寬度 w-60;Mobile 先隱藏(AppShell 負責),未來 F 任務再補 drawer * - 版本號顯示於底部(暫以 package.json version 等效字串) * * 未做(保留給後續任務): * - Mobile drawer / Sheet(F6 之後視需要) * - UserMenu — 依 F4 任務規格已改放到 Header 右側(不在 Sidebar 底部) */ import Link from "next/link"; import { usePathname } from "next/navigation"; import { Boxes, Cable, LayoutDashboard, Network, Play, Settings, type LucideIcon, } from "lucide-react"; import { useT, type TranslateFn } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; interface NavItem { href: string; /** i18n key,由 t() 展開 */ labelKey: string; icon: LucideIcon; } /** * 導航項目 — 對齊 pages.md 頁面總覽與 components.md §3.1 的 icon 指定。 * 以函式產生讓 test 也能以同一份設定做 assertion(若需要)。 */ const NAV_ITEMS: readonly NavItem[] = [ { href: "/", labelKey: "nav.dashboard", icon: LayoutDashboard }, { href: "/devices", labelKey: "nav.devices", icon: Cable }, { href: "/models", labelKey: "nav.models", icon: Boxes }, { href: "/workspace", labelKey: "nav.workspace", icon: Play }, { href: "/clusters", labelKey: "nav.clusters", icon: Network }, { href: "/settings", labelKey: "nav.settings", icon: Settings }, ] as const; /** * 是否為當前 active 導航項目。 * 匯出給 test 使用,避免在測試中重造 active 判定邏輯。 */ export function isNavActive(itemHref: string, pathname: string): boolean { if (itemHref === "/") return pathname === "/"; // startsWith 比對,使得 /devices/pair 也能讓 /devices 維持 active 狀態 return pathname === itemHref || pathname.startsWith(`${itemHref}/`); } interface SidebarProps { className?: string; } export function Sidebar({ className }: SidebarProps) { const pathname = usePathname(); const t = useT(); return ( ); } interface NavItemLinkProps { item: NavItem; active: boolean; t: TranslateFn; } function NavItemLink({ item, active, t }: NavItemLinkProps) { const Icon = item.icon; const label = t(item.labelKey); return (