Files
awoooi/apps/web/src/components/layout/header.tsx
2026-04-01 20:11:39 +08:00

203 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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×34viewBox 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>
)
}