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>
This commit is contained in:
OG T
2026-03-23 13:02:21 +08:00
parent eee4ab9b36
commit 7db5108a1f
4 changed files with 195 additions and 126 deletions

View File

@@ -0,0 +1,29 @@
'use client'
/**
* Authorizations Page - 授權中心
* ==============================
* Phase 7.0: 防禦性路由佔位
*
* Nothing.tech 空態設計:
* - 畫面正中央極簡文字
* - 等寬字體 + 淺灰色
*/
import { AppLayout } from '@/components/layout'
export default function AuthorizationsPage({
params,
}: {
params: { locale: string }
}) {
return (
<AppLayout locale={params.locale}>
<div className="flex items-center justify-center min-h-[60vh]">
<p className="font-mono text-sm text-neutral-400 tracking-wider">
[ / AUTHORIZATIONS_MODULE_UNDER_CONSTRUCTION ]
</p>
</div>
</AppLayout>
)
}

View File

@@ -0,0 +1,29 @@
'use client'
/**
* Knowledge Base Page - 知識殿堂
* ==============================
* Phase 7.0: 防禦性路由佔位
*
* Nothing.tech 空態設計:
* - 畫面正中央極簡文字
* - 等寬字體 + 淺灰色
*/
import { AppLayout } from '@/components/layout'
export default function KnowledgeBasePage({
params,
}: {
params: { locale: string }
}) {
return (
<AppLayout locale={params.locale}>
<div className="flex items-center justify-center min-h-[60vh]">
<p className="font-mono text-sm text-neutral-400 tracking-wider">
[ 殿 / KNOWLEDGE_BASE_MODULE_UNDER_CONSTRUCTION ]
</p>
</div>
</AppLayout>
)
}

View File

@@ -0,0 +1,29 @@
'use client'
/**
* Settings Page - 系統設定
* ========================
* Phase 7.0: 防禦性路由佔位
*
* Nothing.tech 空態設計:
* - 畫面正中央極簡文字
* - 等寬字體 + 淺灰色
*/
import { AppLayout } from '@/components/layout'
export default function SettingsPage({
params,
}: {
params: { locale: string }
}) {
return (
<AppLayout locale={params.locale}>
<div className="flex items-center justify-center min-h-[60vh]">
<p className="font-mono text-sm text-neutral-400 tracking-wider">
[ / SETTINGS_MODULE_UNDER_CONSTRUCTION ]
</p>
</div>
</AppLayout>
)
}

View File

@@ -1,24 +1,26 @@
'use client'
/**
* Sidebar - 高透光玻璃側邊欄
* ==========================
* CPO-102: Nothing.tech 明亮工業風側邊導航
* Phase 2.5: lucide-react 純代碼視覺 (符合第七章憲法)
* Sidebar - Phase 7.0 極簡五柱導航
* =================================
* Nothing.tech 視覺憲法:
* - 純白背景 (bg-white)
* - 極細右邊框 (border-r-[0.5px] border-neutral-200)
* - 無陰影
* - 單色圖示
*
* Features:
* - backdrop-blur 毛玻璃效果
* - 固定寬度 (w-64)
* - 折疊/展開動畫 - 機械爪開合意象
* - i18n 導航標籤
* - 當前路由高亮
* - lucide-react 圖示 (無實體圖片依賴)
* 5 大核心樞紐:
* 1. 全局戰情室 (/)
* 2. 授權中心 (/authorizations) - 含動態徽章
* 3. 行動日誌 (/action-logs)
* 4. 知識殿堂 (/knowledge-base)
* 5. 系統設定 (/settings)
*/
import { useCallback } from 'react'
import { useEffect, useState } from 'react'
import { useTranslations } from 'next-intl'
import { usePathname } from 'next/navigation'
import { StatusOrb } from '@/components/ui/status-orb'
import Link from 'next/link'
import { cn } from '@/lib/utils'
import {
LayoutDashboard,
@@ -26,12 +28,10 @@ import {
Zap,
BookOpen,
Settings,
Bot,
Eye,
ChevronLeft,
ChevronRight,
Sparkles,
} from 'lucide-react'
import { apiClient } from '@/lib/api-client'
// =============================================================================
// Types
@@ -44,22 +44,23 @@ interface SidebarProps {
className?: string
}
// =============================================================================
// Navigation Items (using lucide-react icons)
// =============================================================================
type NavItemConfig = {
id: string
href: string
labelKey: string
Icon: typeof LayoutDashboard
badge?: boolean // 是否顯示動態徽章
}
// =============================================================================
// Phase 7.0: 5 大核心樞紐
// =============================================================================
const NAV_ITEMS: NavItemConfig[] = [
{ id: 'dashboard', href: '/demo', labelKey: 'dashboard', Icon: LayoutDashboard },
{ id: 'approvals', href: '/approvals', labelKey: 'approvals', Icon: ShieldCheck },
{ id: 'actions', href: '/action-logs', labelKey: 'actions', Icon: Zap },
{ id: 'knowledge', href: '/knowledge', labelKey: 'knowledge', Icon: BookOpen },
{ 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 },
]
@@ -75,11 +76,31 @@ export function Sidebar({
}: SidebarProps) {
const t = useTranslations('nav')
const tBrand = useTranslations('brand')
const tClawbot = useTranslations('openclaw')
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}`
const fullHref = `/${locale}${href === '/' ? '' : href}`
if (href === '/') {
return pathname === `/${locale}` || pathname === `/${locale}/`
}
return pathname === fullHref || pathname.startsWith(fullHref + '/')
}
@@ -88,150 +109,111 @@ export function Sidebar({
className={cn(
'fixed left-0 top-0 h-screen z-40',
'flex flex-col',
// 玻璃效果
'bg-white/70 backdrop-blur-[20px]',
'border-r border-black/[0.06]',
// Phase 7.0: 純白極簡
'bg-white',
'border-r-[0.5px] border-neutral-200',
// 寬度動畫
'transition-all duration-300 ease-out',
collapsed ? 'w-16' : 'w-64',
collapsed ? 'w-16' : 'w-56',
className
)}
>
{/* Logo - lucide-react + CSS Typography (第七章規範) */}
{/* Logo 區 - 極簡化 */}
<div className={cn(
'flex items-center border-b border-black/[0.04]',
'flex items-center border-b-[0.5px] border-neutral-200',
'transition-all duration-300',
collapsed ? 'h-20 justify-center px-3' : 'h-24 px-6'
collapsed ? 'h-16 justify-center px-2' : 'h-16 px-4'
)}>
<div className={cn(
'flex items-center gap-3',
collapsed ? 'flex-col' : ''
)}>
{/* Bot Icon + 呼吸燈效果 */}
<div className="relative">
<div className={cn(
'w-10 h-10 rounded-xl flex items-center justify-center',
'bg-gradient-to-br from-claw-blue/20 to-claw-blue/10',
'border border-claw-blue/30',
'transition-all duration-300'
)}>
<Eye className="w-5 h-5 text-claw-blue" />
</div>
{/* Breathing indicator */}
<div className="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-claw-blue opacity-75" />
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-claw-blue" />
</div>
</div>
{/* 品牌字體排版 - i18n 規範 */}
{!collapsed && (
<div className="flex flex-col">
<span className="text-xl font-black tracking-widest text-nothing-black uppercase font-mono">
{tBrand('name')}
</span>
<span className="text-[9px] text-nothing-gray-500 tracking-widest font-mono font-medium uppercase">
{tBrand('version')} | {tBrand('tagline')}
</span>
</div>
)}
</div>
{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>
{/* Navigation - lucide-react Icons */}
<nav className="flex-1 py-6 overflow-y-auto">
<ul className="space-y-1.5 px-3">
{/* 導航列表 - 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}>
<a
href={`/${locale}${item.href}`}
<Link
href={`/${locale}${item.href === '/' ? '' : item.href}`}
className={cn(
'flex items-center gap-3.5 px-3.5 py-3 rounded-xl',
'transition-all duration-200',
'group relative',
'flex items-center gap-3 px-3 py-2.5 rounded-sm',
'transition-all duration-150',
'relative',
// Phase 7.0 視覺規範
active
? 'bg-nothing-black text-white shadow-lg shadow-black/20'
: 'text-nothing-gray-600 hover:bg-nothing-gray-100/80 hover:text-nothing-black',
? 'font-bold text-black bg-neutral-100'
: 'text-neutral-500 hover:bg-neutral-50 hover:text-black',
collapsed && 'justify-center px-2'
)}
>
{/* Active indicator - 左側條紋 */}
{active && !collapsed && (
<span className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-white/30 rounded-r-full" />
)}
<item.Icon className={cn(
'w-5 h-5 flex-shrink-0 transition-all duration-200',
active ? 'opacity-100' : 'opacity-60 group-hover:opacity-100',
'group-hover:scale-105'
'w-[18px] h-[18px] flex-shrink-0',
active ? 'text-black' : 'text-neutral-400'
)} />
{!collapsed && (
<span className="font-medium text-[13px] tracking-wide truncate">
<span className="text-[13px] tracking-wide truncate">
{t(item.labelKey)}
</span>
)}
</a>
{/* 動態徽章 - 授權中心 */}
{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>
{/* OpenClaw Status - lucide-react Bot Icon */}
{/* 底部區域 - 極簡版本號 */}
<div className={cn(
'border-t border-black/[0.04] p-5',
collapsed ? 'flex justify-center' : ''
'border-t-[0.5px] border-neutral-200 py-3',
collapsed ? 'px-2 text-center' : 'px-4'
)}>
<div className={cn(
'flex items-center',
collapsed ? 'flex-col gap-2' : 'gap-3.5'
)}>
{/* Bot Icon with glow */}
<div className="relative">
<div className={cn(
'w-10 h-10 rounded-xl flex items-center justify-center',
'bg-gradient-to-br from-status-thinking/20 to-status-thinking/10',
'border border-status-thinking/30'
)}>
<Bot className="w-5 h-5 text-status-thinking" />
</div>
{/* Sparkle effect */}
<Sparkles className="absolute -top-1 -right-1 w-3 h-3 text-status-thinking animate-pulse" />
</div>
{!collapsed && (
<div className="flex flex-col gap-0.5">
<span className="text-sm font-bold text-nothing-black tracking-tight">
{tClawbot('name')}
</span>
<span className="text-[9px] text-status-thinking font-mono uppercase tracking-widest">
{tClawbot('monitoring')}
</span>
</div>
)}
</div>
{!collapsed && (
<span className="text-[10px] font-mono text-neutral-300 uppercase tracking-widest">
v1.0.0
</span>
)}
</div>
{/* Collapse Toggle - lucide-react Chevron */}
{/* 折疊按鈕 */}
<button
onClick={onToggle}
className={cn(
'absolute -right-4 top-24',
'w-8 h-8 rounded-full',
'bg-white border-2 border-nothing-gray-200/80',
'shadow-md shadow-black/5',
'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-nothing-gray-50 hover:border-nothing-gray-300',
'hover:shadow-lg hover:shadow-black/10',
'active:scale-95',
'transition-all duration-200'
'hover:bg-neutral-50',
'transition-all duration-150'
)}
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
>
{collapsed ? (
<ChevronRight className="w-4 h-4 text-nothing-gray-600" />
<ChevronRight className="w-3 h-3 text-neutral-400" />
) : (
<ChevronLeft className="w-4 h-4 text-nothing-gray-600" />
<ChevronLeft className="w-3 h-3 text-neutral-400" />
)}
</button>
</aside>