Files
awoooi/apps/web/src/components/layout/sidebar.tsx
OG T 7db5108a1f feat(web): Phase 7.0 minimalist 5-pillar navigation
- Refactor sidebar to Nothing.tech visual compliance
- Add defensive route stubs for /authorizations, /knowledge-base, /settings
- Dynamic badge for pending approvals count
- Ultra-minimal borders (0.5px), no shadows

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-23 13:02:21 +08:00

222 lines
6.8 KiB
TypeScript

'use client'
/**
* Sidebar - Phase 7.0 極簡五柱導航
* =================================
* Nothing.tech 視覺憲法:
* - 純白背景 (bg-white)
* - 極細右邊框 (border-r-[0.5px] border-neutral-200)
* - 無陰影
* - 單色圖示
*
* 5 大核心樞紐:
* 1. 全局戰情室 (/)
* 2. 授權中心 (/authorizations) - 含動態徽章
* 3. 行動日誌 (/action-logs)
* 4. 知識殿堂 (/knowledge-base)
* 5. 系統設定 (/settings)
*/
import { useEffect, useState } from 'react'
import { useTranslations } from 'next-intl'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
import { cn } from '@/lib/utils'
import {
LayoutDashboard,
ShieldCheck,
Zap,
BookOpen,
Settings,
ChevronLeft,
ChevronRight,
} from 'lucide-react'
import { apiClient } from '@/lib/api-client'
// =============================================================================
// Types
// =============================================================================
interface SidebarProps {
locale: string
collapsed?: boolean
onToggle?: () => void
className?: string
}
type NavItemConfig = {
id: string
href: string
labelKey: string
Icon: typeof LayoutDashboard
badge?: boolean // 是否顯示動態徽章
}
// =============================================================================
// Phase 7.0: 5 大核心樞紐
// =============================================================================
const NAV_ITEMS: NavItemConfig[] = [
{ id: 'warroom', href: '/', labelKey: 'dashboard', Icon: LayoutDashboard },
{ id: 'authorizations', href: '/authorizations', labelKey: 'approvals', Icon: ShieldCheck, badge: true },
{ id: 'action-logs', href: '/action-logs', labelKey: 'actions', Icon: Zap },
{ id: 'knowledge-base', href: '/knowledge-base', labelKey: 'knowledge', Icon: BookOpen },
{ id: 'settings', href: '/settings', labelKey: 'settings', Icon: Settings },
]
// =============================================================================
// Component
// =============================================================================
export function Sidebar({
locale,
collapsed = false,
onToggle,
className,
}: SidebarProps) {
const t = useTranslations('nav')
const tBrand = useTranslations('brand')
const pathname = usePathname()
const [pendingCount, setPendingCount] = useState(0)
// 載入待簽核數量 (動態徽章)
useEffect(() => {
const fetchPendingCount = async () => {
try {
const data = await apiClient.getPendingApprovals()
setPendingCount(data.count || 0)
} catch {
setPendingCount(0)
}
}
fetchPendingCount()
// 每 30 秒刷新
const interval = setInterval(fetchPendingCount, 30000)
return () => clearInterval(interval)
}, [])
const isActive = (href: string) => {
const fullHref = `/${locale}${href === '/' ? '' : href}`
if (href === '/') {
return pathname === `/${locale}` || pathname === `/${locale}/`
}
return pathname === fullHref || pathname.startsWith(fullHref + '/')
}
return (
<aside
className={cn(
'fixed left-0 top-0 h-screen z-40',
'flex flex-col',
// Phase 7.0: 純白極簡
'bg-white',
'border-r-[0.5px] border-neutral-200',
// 寬度動畫
'transition-all duration-300 ease-out',
collapsed ? 'w-16' : 'w-56',
className
)}
>
{/* Logo 區 - 極簡化 */}
<div className={cn(
'flex items-center border-b-[0.5px] border-neutral-200',
'transition-all duration-300',
collapsed ? 'h-16 justify-center px-2' : 'h-16 px-4'
)}>
{collapsed ? (
<span className="font-mono text-lg font-bold text-neutral-900 tracking-tighter">
A
</span>
) : (
<span className="font-mono text-sm font-bold text-neutral-900 tracking-widest uppercase">
{tBrand('name')}
</span>
)}
</div>
{/* 導航列表 - 5 大核心樞紐 */}
<nav className="flex-1 py-4 overflow-y-auto">
<ul className="space-y-0.5 px-2">
{NAV_ITEMS.map((item) => {
const active = isActive(item.href)
return (
<li key={item.id}>
<Link
href={`/${locale}${item.href === '/' ? '' : item.href}`}
className={cn(
'flex items-center gap-3 px-3 py-2.5 rounded-sm',
'transition-all duration-150',
'relative',
// Phase 7.0 視覺規範
active
? 'font-bold text-black bg-neutral-100'
: 'text-neutral-500 hover:bg-neutral-50 hover:text-black',
collapsed && 'justify-center px-2'
)}
>
<item.Icon className={cn(
'w-[18px] h-[18px] flex-shrink-0',
active ? 'text-black' : 'text-neutral-400'
)} />
{!collapsed && (
<span className="text-[13px] tracking-wide truncate">
{t(item.labelKey)}
</span>
)}
{/* 動態徽章 - 授權中心 */}
{item.badge && pendingCount > 0 && (
<span className={cn(
'flex items-center justify-center',
'min-w-[18px] h-[18px] px-1',
'text-[10px] font-mono font-bold',
'bg-neutral-900 text-white rounded-sm',
collapsed ? 'absolute top-1 right-1' : 'ml-auto'
)}>
{pendingCount > 99 ? '99+' : pendingCount}
</span>
)}
</Link>
</li>
)
})}
</ul>
</nav>
{/* 底部區域 - 極簡版本號 */}
<div className={cn(
'border-t-[0.5px] border-neutral-200 py-3',
collapsed ? 'px-2 text-center' : 'px-4'
)}>
{!collapsed && (
<span className="text-[10px] font-mono text-neutral-300 uppercase tracking-widest">
v1.0.0
</span>
)}
</div>
{/* 折疊按鈕 */}
<button
onClick={onToggle}
className={cn(
'absolute -right-3 top-20',
'w-6 h-6 rounded-full',
'bg-white border-[0.5px] border-neutral-200',
'flex items-center justify-center',
'hover:bg-neutral-50',
'transition-all duration-150'
)}
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
>
{collapsed ? (
<ChevronRight className="w-3 h-3 text-neutral-400" />
) : (
<ChevronLeft className="w-3 h-3 text-neutral-400" />
)}
</button>
</aside>
)
}