203 lines
7.7 KiB
TypeScript
203 lines
7.7 KiB
TypeScript
'use client'
|
||
|
||
/**
|
||
* Header - 頂部狀態列
|
||
* ====================
|
||
* CPO-102: Nothing.tech 明亮工業風頂部導航
|
||
*
|
||
* Features:
|
||
* - 玻璃效果 + 固定定位
|
||
* - 連線狀態指示器
|
||
* - 語系切換器
|
||
* - 使用者選單
|
||
*
|
||
* Phase 19: 使用 Z_INDEX.HEADER (30)
|
||
* @see lib/constants/z-index.ts
|
||
*/
|
||
|
||
import { useCallback } from 'react'
|
||
import { useTranslations } from 'next-intl'
|
||
import { usePathname } from 'next/navigation'
|
||
import { StatusOrb } from '@/components/ui/status-orb'
|
||
import { useConnectionStatus, useMockMode } from '@/stores/dashboard.store'
|
||
import { useApprovalCount } from '@/stores/approval.store'
|
||
import { cn } from '@/lib/utils'
|
||
import { Z_INDEX } from '@/lib/constants/z-index'
|
||
|
||
// =============================================================================
|
||
// Types
|
||
// =============================================================================
|
||
|
||
interface HeaderProps {
|
||
locale: string
|
||
sidebarCollapsed?: boolean
|
||
className?: string
|
||
}
|
||
|
||
// =============================================================================
|
||
// Component
|
||
// =============================================================================
|
||
|
||
export function Header({
|
||
locale,
|
||
sidebarCollapsed = false,
|
||
className,
|
||
}: HeaderProps) {
|
||
const t = useTranslations('locale')
|
||
const tDashboard = useTranslations('dashboard')
|
||
const pathname = usePathname()
|
||
|
||
const connectionStatus = useConnectionStatus()
|
||
const mockMode = useMockMode()
|
||
const pendingApprovals = useApprovalCount()
|
||
|
||
const switchLocale = useCallback((newLocale: string) => {
|
||
const newPath = pathname.replace(`/${locale}`, `/${newLocale}`)
|
||
window.location.href = newPath
|
||
}, [locale, pathname])
|
||
|
||
const tConnection = useTranslations('connection')
|
||
const connectionLabel = tConnection(connectionStatus)
|
||
|
||
const connectionOrbStatus = {
|
||
disconnected: 'idle',
|
||
connecting: 'syncing',
|
||
connected: 'healthy',
|
||
reconnecting: 'warning',
|
||
error: 'critical',
|
||
}[connectionStatus] as 'idle' | 'syncing' | 'healthy' | 'warning' | 'critical'
|
||
|
||
return (
|
||
<header
|
||
className={cn(
|
||
'fixed top-0 right-0 h-16',
|
||
'flex items-center justify-between px-6',
|
||
// 玻璃效果
|
||
'bg-white/85 backdrop-blur-[20px]',
|
||
'border-b border-black/[0.06]',
|
||
// 寬度根據 sidebar 調整
|
||
'transition-all duration-300 ease-out',
|
||
sidebarCollapsed ? 'left-16' : 'left-64',
|
||
className
|
||
)}
|
||
style={{ zIndex: Z_INDEX.HEADER }}
|
||
>
|
||
{/* 左側 Logo */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||
{/* NemoClaw Logo SVG (34×34,viewBox 0 0 140 140,深色底) */}
|
||
<div style={{
|
||
width: 34, height: 34,
|
||
background: '#0d1117',
|
||
borderRadius: 6,
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
flexShrink: 0,
|
||
}}>
|
||
<svg width="28" height="28" viewBox="0 0 140 140" xmlns="http://www.w3.org/2000/svg">
|
||
<defs>
|
||
<linearGradient id="ceramic3d" x1="0%" y1="0%" x2="100%" y2="100%">
|
||
<stop offset="0%" stopColor="#e8eef5" />
|
||
<stop offset="50%" stopColor="#c8d8e8" />
|
||
<stop offset="100%" stopColor="#8aaac8" />
|
||
</linearGradient>
|
||
<radialGradient id="ledCore" cx="40%" cy="35%" r="60%">
|
||
<stop offset="0%" stopColor="#7AB8F5" />
|
||
<stop offset="100%" stopColor="#2B6CB0" />
|
||
</radialGradient>
|
||
</defs>
|
||
{/* 5 主臂(白瓷 ceramic3d)*/}
|
||
<line x1="70" y1="70" x2="70" y2="18" stroke="url(#ceramic3d)" strokeWidth="8" strokeLinecap="round"/>
|
||
<line x1="70" y1="70" x2="120" y2="95" stroke="url(#ceramic3d)" strokeWidth="8" strokeLinecap="round"/>
|
||
<line x1="70" y1="70" x2="99" y2="128" stroke="url(#ceramic3d)" strokeWidth="8" strokeLinecap="round"/>
|
||
<line x1="70" y1="70" x2="41" y2="128" stroke="url(#ceramic3d)" strokeWidth="8" strokeLinecap="round"/>
|
||
<line x1="70" y1="70" x2="20" y2="95" stroke="url(#ceramic3d)" strokeWidth="8" strokeLinecap="round"/>
|
||
{/* 爪尖 */}
|
||
<circle cx="70" cy="18" r="5" fill="#c8d8e8" />
|
||
<circle cx="120" cy="95" r="5" fill="#c8d8e8" />
|
||
<circle cx="99" cy="128" r="5" fill="#c8d8e8" />
|
||
<circle cx="41" cy="128" r="5" fill="#c8d8e8" />
|
||
<circle cx="20" cy="95" r="5" fill="#c8d8e8" />
|
||
{/* 旋轉虛線軌道 */}
|
||
<circle cx="70" cy="70" r="42" fill="none" stroke="#4A90D9"
|
||
strokeWidth="1" strokeDasharray="6 6" opacity="0.5"
|
||
style={{ animation: 'ring-spin 8s linear infinite', transformOrigin: '70px 70px' }}
|
||
/>
|
||
{/* LED 核心脈動 */}
|
||
<circle cx="70" cy="70" r="16" fill="url(#ledCore)"
|
||
style={{ animation: 'orb-pulse 1.5s ease-in-out infinite', transformOrigin: '70px 70px' }}
|
||
/>
|
||
<circle cx="65" cy="65" r="5" fill="white" opacity="0.4" />
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: 13, fontWeight: 800, color: '#141413', letterSpacing: '2px', fontFamily: 'monospace' }}>
|
||
AWOOOI
|
||
</div>
|
||
<div style={{ fontSize: 8, color: '#87867f', letterSpacing: '1px' }}>
|
||
{tDashboard('title')} · AI OPERATIONS
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right: Status + Locale + User */}
|
||
<div className="flex items-center gap-6">
|
||
{/* Pending Approvals Badge */}
|
||
{pendingApprovals > 0 && (
|
||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-status-critical/10 border border-status-critical/30">
|
||
<StatusOrb status="critical" size="sm" pulse />
|
||
<span className="text-sm font-mono text-status-critical">
|
||
{pendingApprovals} {tDashboard('pendingApprovals')}
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* Connection Status */}
|
||
<div className="flex items-center gap-2">
|
||
<StatusOrb status={connectionOrbStatus} size="md" glow />
|
||
<span className={cn(
|
||
'font-mono text-sm',
|
||
connectionStatus === 'connected' ? 'text-status-healthy' : 'text-nothing-gray-500'
|
||
)}>
|
||
{connectionLabel}
|
||
</span>
|
||
{mockMode && (
|
||
<span className="px-2 py-0.5 text-xs font-mono rounded bg-status-warning/10 text-status-warning border border-status-warning/30">
|
||
MOCK
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Locale Switcher */}
|
||
<div className="flex items-center gap-1 bg-nothing-gray-100 rounded-lg p-1">
|
||
<button
|
||
onClick={() => switchLocale('zh-TW')}
|
||
className={cn(
|
||
'px-3 py-1.5 rounded-md font-mono text-sm transition-all',
|
||
locale === 'zh-TW'
|
||
? 'bg-nothing-black text-white'
|
||
: 'text-nothing-gray-600 hover:text-nothing-black'
|
||
)}
|
||
>
|
||
{t('zhTW')}
|
||
</button>
|
||
<button
|
||
onClick={() => switchLocale('en')}
|
||
className={cn(
|
||
'px-3 py-1.5 rounded-md font-mono text-sm transition-all',
|
||
locale === 'en'
|
||
? 'bg-nothing-black text-white'
|
||
: 'text-nothing-gray-600 hover:text-nothing-black'
|
||
)}
|
||
>
|
||
{t('en')}
|
||
</button>
|
||
</div>
|
||
|
||
{/* User Avatar */}
|
||
<button className="w-9 h-9 rounded-full bg-nothing-gray-200 flex items-center justify-center hover:bg-nothing-gray-300 transition-colors">
|
||
<span className="text-sm font-medium text-nothing-gray-700">A</span>
|
||
</button>
|
||
</div>
|
||
</header>
|
||
)
|
||
}
|