feat(ui): header/sidebar/openclaw 完整對齊 figma-v2
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 6m57s
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 6m57s
- 移除 OpenClaw "AWOOOI v1.0.0 | 正式環境" header - 語言按鈕標籤改為 繁/EN (pill 樣式) - header/sidebar 視覺對齊 figma-v2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -63,7 +63,7 @@
|
||||
"locale": {
|
||||
"switch": "Switch Language",
|
||||
"zhTW": "繁體中文",
|
||||
"en": "English"
|
||||
"en": "EN"
|
||||
},
|
||||
"demo": {
|
||||
"title": "AWOOOI Demo",
|
||||
@@ -646,7 +646,7 @@
|
||||
"saved": "Saved",
|
||||
"zhTW": "繁體中文",
|
||||
"zhTWSub": "Traditional Chinese",
|
||||
"en": "English",
|
||||
"en": "EN",
|
||||
"enSub": "English (US)"
|
||||
},
|
||||
"autoRepair": {
|
||||
|
||||
@@ -62,8 +62,8 @@
|
||||
},
|
||||
"locale": {
|
||||
"switch": "切換語系",
|
||||
"zhTW": "繁體中文",
|
||||
"en": "English"
|
||||
"zhTW": "繁",
|
||||
"en": "EN"
|
||||
},
|
||||
"demo": {
|
||||
"title": "AWOOOI 展示",
|
||||
@@ -645,9 +645,9 @@
|
||||
"phase": "Phase",
|
||||
"save": "儲存設定",
|
||||
"saved": "已儲存",
|
||||
"zhTW": "繁體中文",
|
||||
"zhTW": "繁",
|
||||
"zhTWSub": "Traditional Chinese",
|
||||
"en": "English",
|
||||
"en": "EN",
|
||||
"enSub": "English (US)"
|
||||
},
|
||||
"autoRepair": {
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* OpenClawPanel - 賽博維運風格 AI 面板
|
||||
* =====================================
|
||||
* Phase 5: OpenClaw 實體化升級
|
||||
*
|
||||
* Features:
|
||||
* - 3D 骨架機械爪視覺化 (CSS Art)
|
||||
* - 核心藍色 LED 脈衝動畫
|
||||
* - 點陣字體狀態顯示
|
||||
* - AI 思考流過渡動畫
|
||||
* - 高通透度 awoooi-glass 效果
|
||||
* OpenClawPanel - figma-v2 設計對齊版
|
||||
* ======================================
|
||||
* @updated 2026-04-03 Claude Code — 完整對齊 figma-v2
|
||||
* - 移除 "AWOOOI v1.0.0 | 正式環境" header
|
||||
* - W●●●Claw 混合字體 (DM Mono + VT323 #d97757)
|
||||
* - WOODCLAW PIPELINE badge (9px, rgba(74,144,217,0.1), #4A90D9)
|
||||
* - 3個脈衝點動畫
|
||||
* - 保留 NemoClaw SVG (68px)
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
@@ -36,119 +33,44 @@ export interface OpenClawPanelProps {
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NemoClaw 3D Ceramic SVG Component (Lab-White Style)
|
||||
// NemoClaw 3D Ceramic SVG Component (68px, figma-v2 spec)
|
||||
// =============================================================================
|
||||
|
||||
function NemoClaw({ isActive, isPulsing }: { isActive: boolean; isPulsing: boolean }) {
|
||||
return (
|
||||
<svg viewBox="0 0 140 140" className="w-full h-full drop-shadow-lg" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="68" height="68" viewBox="0 0 140 140" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="nc-ceramic3d" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<linearGradient id="oc2-ceramic" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#FFFFFF" />
|
||||
<stop offset="40%" stopColor="#F8F8F8" />
|
||||
<stop offset="70%" stopColor="#E8E8E8" />
|
||||
<stop offset="100%" stopColor="#D8D8D8" />
|
||||
</linearGradient>
|
||||
<filter id="nc-coreGlow" x="-100%" y="-100%" width="300%" height="300%">
|
||||
<feGaussianBlur stdDeviation="6" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
<filter id="nc-pulseGlow" x="-100%" y="-100%" width="300%" height="300%">
|
||||
<feGaussianBlur stdDeviation="8" result="blur">
|
||||
<animate attributeName="stdDeviation" values="6;10;6" dur="1.5s" repeatCount="indefinite" />
|
||||
</feGaussianBlur>
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
<filter id="nc-shadow3d" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="2" dy="4" stdDeviation="3" floodOpacity="0.15" />
|
||||
</filter>
|
||||
<radialGradient id="oc2-led" cx="40%" cy="35%" r="60%">
|
||||
<stop offset="0%" stopColor="#7AB8F5" />
|
||||
<stop offset="100%" stopColor="#2B6CB0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
{/* Base shadow */}
|
||||
<ellipse cx="70" cy="125" rx="35" ry="8" fill="rgba(0,0,0,0.08)" />
|
||||
{/* Main body - 3D ceramic sphere */}
|
||||
<circle cx="70" cy="70" r="32" fill="url(#nc-ceramic3d)" filter="url(#nc-shadow3d)" stroke="#E0E0E0" strokeWidth="1" />
|
||||
{/* Inner ring */}
|
||||
<circle cx="70" cy="70" r="26" fill="none" stroke="#D0D0D0" strokeWidth="1" opacity="0.5" />
|
||||
{/* Core LED */}
|
||||
<circle cx="70" cy="70" r="16" fill={isActive ? '#4A90D9' : '#B0B0B0'} filter={isPulsing ? 'url(#nc-pulseGlow)' : 'url(#nc-coreGlow)'} className="transition-all duration-500">
|
||||
{isPulsing && <animate attributeName="r" values="14;17;14" dur="1s" repeatCount="indefinite" />}
|
||||
<circle cx="70" cy="70" r="32" fill="url(#oc2-ceramic)" stroke="#E0E0E0" strokeWidth="1" />
|
||||
<circle cx="70" cy="70" r="16" fill={isActive ? '#4A90D9' : 'url(#oc2-led)'}>
|
||||
{isPulsing && <animate attributeName="r" values="14;17;14" dur="2s" repeatCount="indefinite" />}
|
||||
</circle>
|
||||
<circle cx="70" cy="70" r="8" fill="white" opacity="0.8" />
|
||||
<path d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6" stroke="url(#oc2-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<path d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6" stroke="#4A90D9" strokeWidth="3" strokeLinecap="round" fill="none" opacity="0.5" />
|
||||
<path d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82" stroke="url(#oc2-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<path d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82" stroke="#4A90D9" strokeWidth="3" strokeLinecap="round" fill="none" opacity="0.5" />
|
||||
<path d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82" stroke="url(#oc2-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<path d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82" stroke="#4A90D9" strokeWidth="3" strokeLinecap="round" fill="none" opacity="0.5" />
|
||||
<path d="M 48 92 L 28 112 L 16 116" stroke="url(#oc2-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<path d="M 92 92 L 112 112 L 124 116" stroke="url(#oc2-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<circle cx="70" cy="70" r="42" fill="none" stroke="#4A90D9" strokeWidth="1" strokeDasharray="6 6" opacity="0.3">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 70 70" to="360 70 70" dur="8s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="70" cy="70" r="8" fill="white" opacity={isActive ? 0.9 : 0.4} className="transition-opacity duration-300" />
|
||||
{/* Top arm */}
|
||||
<g filter="url(#nc-shadow3d)">
|
||||
<path d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6" stroke="url(#nc-ceramic3d)" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
<path d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6" stroke={isActive ? '#4A90D9' : '#C0C0C0'} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" fill="none" opacity="0.6" />
|
||||
<circle cx="58" cy="6" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
|
||||
<circle cx="82" cy="6" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
|
||||
</g>
|
||||
{/* Left arm */}
|
||||
<g filter="url(#nc-shadow3d)">
|
||||
<path d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82" stroke="url(#nc-ceramic3d)" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
<path d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82" stroke={isActive ? '#4A90D9' : '#C0C0C0'} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" fill="none" opacity="0.6" />
|
||||
<circle cx="6" cy="58" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
|
||||
<circle cx="6" cy="82" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
|
||||
</g>
|
||||
{/* Right arm */}
|
||||
<g filter="url(#nc-shadow3d)">
|
||||
<path d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82" stroke="url(#nc-ceramic3d)" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
<path d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82" stroke={isActive ? '#4A90D9' : '#C0C0C0'} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" fill="none" opacity="0.6" />
|
||||
<circle cx="134" cy="58" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
|
||||
<circle cx="134" cy="82" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
|
||||
</g>
|
||||
{/* Bottom left arm */}
|
||||
<g filter="url(#nc-shadow3d)">
|
||||
<path d="M 48 92 L 28 112 L 16 116" stroke="url(#nc-ceramic3d)" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
<path d="M 48 92 L 28 112 L 16 116" stroke={isActive ? '#4A90D9' : '#C0C0C0'} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" fill="none" opacity="0.6" />
|
||||
<circle cx="16" cy="116" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
|
||||
</g>
|
||||
{/* Bottom right arm */}
|
||||
<g filter="url(#nc-shadow3d)">
|
||||
<path d="M 92 92 L 112 112 L 124 116" stroke="url(#nc-ceramic3d)" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
<path d="M 92 92 L 112 112 L 124 116" stroke={isActive ? '#4A90D9' : '#C0C0C0'} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" fill="none" opacity="0.6" />
|
||||
<circle cx="124" cy="116" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
|
||||
</g>
|
||||
{/* Orbit ring when active */}
|
||||
{isActive && (
|
||||
<circle cx="70" cy="70" r="42" fill="none" stroke="#4A90D9" strokeWidth="1" strokeDasharray="6 6" opacity="0.4"
|
||||
className="animate-spin" style={{ animationDuration: '8s', transformOrigin: '70px 70px' }} />
|
||||
)}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Status Messages (Dot Matrix Style)
|
||||
// =============================================================================
|
||||
|
||||
// 2026-04-02 Claude Code: STATUS_MESSAGES 已移除,改用 i18n 鍵值 openclawPanel.*
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Typewriter Hook
|
||||
// =============================================================================
|
||||
|
||||
function useTypewriter(text: string, speed: number = 50) {
|
||||
const [displayText, setDisplayText] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
let index = 0
|
||||
setDisplayText('')
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (index < text.length) {
|
||||
setDisplayText(text.slice(0, index + 1))
|
||||
index++
|
||||
} else {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, speed)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [text, speed])
|
||||
|
||||
return displayText
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Component
|
||||
// =============================================================================
|
||||
@@ -160,15 +82,10 @@ export function OpenClawPanel({
|
||||
className,
|
||||
}: OpenClawPanelProps) {
|
||||
const tPanel = useTranslations('openclawPanel')
|
||||
const tBrand = useTranslations('brand')
|
||||
// Phase 8.0 #16: 移除 cursorVisible state,改用 CSS animate-pulse
|
||||
|
||||
const isActive = status !== 'patrolling'
|
||||
const isPulsing = status === 'intercepting' || status === 'analyzing'
|
||||
|
||||
const statusMessage = tPanel(status)
|
||||
const displayText = useTypewriter(statusMessage, 40)
|
||||
|
||||
// Notify when complete
|
||||
useEffect(() => {
|
||||
if (status === 'complete') {
|
||||
@@ -181,104 +98,101 @@ export function OpenClawPanel({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative overflow-hidden',
|
||||
'bg-white',
|
||||
'p-4',
|
||||
className
|
||||
)}
|
||||
className={className}
|
||||
style={{
|
||||
padding: 14,
|
||||
display: 'flex',
|
||||
gap: 14,
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
{/* Scan line animation when active */}
|
||||
{isActive && (
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute h-[1px] w-full bg-gradient-to-r from-transparent via-claw-blue/50 to-transparent animate-scan" />
|
||||
</div>
|
||||
)}
|
||||
{/* NemoClaw SVG — 68px */}
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<NemoClaw isActive={isActive} isPulsing={isPulsing} />
|
||||
</div>
|
||||
|
||||
{/* Header - Dot Matrix Style */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-dot-matrix text-sm text-nothing-gray-700 tracking-wider">
|
||||
AWOOOI v1.0.0
|
||||
</span>
|
||||
<span className="font-dot-matrix text-xs text-nothing-gray-400">
|
||||
| {tBrand('environment')}
|
||||
</span>
|
||||
{isActive && (
|
||||
<span className="w-2 h-2 rounded-full bg-claw-blue animate-ping" />
|
||||
)}
|
||||
{/* 右側文字欄 */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{/* W●●●Claw 混合字體名稱 */}
|
||||
<div style={{ marginBottom: 3 }}>
|
||||
<span style={{
|
||||
fontFamily: 'var(--font-body), "DM Mono", monospace',
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
color: '#141413',
|
||||
letterSpacing: '-0.5px',
|
||||
}}>W</span>
|
||||
<span style={{
|
||||
fontFamily: 'var(--font-dot-matrix), "VT323", monospace',
|
||||
fontSize: 26,
|
||||
color: '#d97757',
|
||||
letterSpacing: 2,
|
||||
lineHeight: 1,
|
||||
}}>ooo</span>
|
||||
<span style={{
|
||||
fontFamily: 'var(--font-body), "DM Mono", monospace',
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
color: '#141413',
|
||||
letterSpacing: '-0.5px',
|
||||
}}>Claw</span>
|
||||
</div>
|
||||
{alertType && (
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-dot-matrix bg-status-critical/10 text-status-critical border border-status-critical/20">
|
||||
{alertType}
|
||||
|
||||
{/* WOODCLAW PIPELINE badge */}
|
||||
<div style={{
|
||||
display: 'inline-block',
|
||||
fontSize: 9,
|
||||
padding: '2px 7px',
|
||||
background: 'rgba(74,144,217,0.1)',
|
||||
color: '#4A90D9',
|
||||
borderRadius: 2,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '1.5px',
|
||||
marginBottom: 8,
|
||||
}}>
|
||||
WoooClaw Pipeline
|
||||
</div>
|
||||
|
||||
{/* 狀態文字 + 脈衝點 */}
|
||||
<div style={{ fontSize: 12, color: '#87867f', lineHeight: 1.5 }}>
|
||||
{tPanel(status)}
|
||||
{/* 脈衝點動畫 */}
|
||||
<style>{`
|
||||
@keyframes oc-dot-pulse {
|
||||
0%, 60%, 100% { opacity: 0.3; }
|
||||
30% { opacity: 1; }
|
||||
}
|
||||
`}</style>
|
||||
<span style={{ display: 'inline-flex', gap: 3, marginLeft: 4, verticalAlign: 'middle' }}>
|
||||
{[0, 0.2, 0.4].map((delay, i) => (
|
||||
<span key={i} style={{
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: '50%',
|
||||
background: '#4A90D9',
|
||||
display: 'inline-block',
|
||||
animation: `oc-dot-pulse 1.4s ease-in-out ${delay}s infinite`,
|
||||
}} />
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* alertType badge (optional) */}
|
||||
{alertType && (
|
||||
<div style={{ marginTop: 6 }}>
|
||||
<span style={{
|
||||
fontSize: 10,
|
||||
padding: '2px 8px',
|
||||
background: 'rgba(204,34,0,0.1)',
|
||||
color: '#cc2200',
|
||||
borderRadius: 4,
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
{alertType}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* NemoClaw 3D Ceramic Visualization */}
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: '10px 12px' }}>
|
||||
<div className="relative w-20 h-20 flex-shrink-0">
|
||||
<NemoClaw isActive={isActive} isPulsing={isPulsing} />
|
||||
</div>
|
||||
|
||||
{/* 右側文字 */}
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#141413', marginBottom: 2 }}>
|
||||
WoooClaw
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#87867f', marginBottom: 6 }}>
|
||||
{tPanel(status)}
|
||||
</div>
|
||||
<span style={{
|
||||
fontSize: 11, padding: '2px 8px',
|
||||
background: 'rgba(74,144,217,0.08)',
|
||||
border: '0.5px solid rgba(74,144,217,0.3)',
|
||||
borderRadius: 10,
|
||||
color: '#4A90D9',
|
||||
}}>
|
||||
WoooClaw Pipeline
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Display - VT323 Dot Matrix Font */}
|
||||
<div className="text-center bg-nothing-gray-50/50 rounded-lg py-2 px-3">
|
||||
<div className={cn(
|
||||
'font-dot-matrix text-base tracking-wide',
|
||||
isActive ? 'text-claw-blue' : 'text-nothing-gray-600'
|
||||
)}>
|
||||
<span>{displayText}</span>
|
||||
{/* Phase 8.0 #16: CSS cursor blink */}
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block w-2 h-4 ml-1 -mb-0.5 animate-pulse',
|
||||
isActive ? 'bg-claw-blue' : 'bg-nothing-gray-400'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress indicator when analyzing */}
|
||||
{(status === 'analyzing' || status === 'generating') && (
|
||||
<div className="mt-3 flex justify-center gap-1">
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="w-1.5 h-1.5 rounded-full bg-claw-blue animate-pulse"
|
||||
style={{ animationDelay: `${i * 150}ms` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Complete indicator */}
|
||||
{status === 'complete' && (
|
||||
<div className="mt-3 flex justify-center">
|
||||
<span className="px-2 py-0.5 rounded text-[9px] font-body bg-status-healthy/10 text-status-healthy border border-status-healthy/20">
|
||||
READY
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Header - 頂部狀態列
|
||||
* ====================
|
||||
* CPO-102: Nothing.tech 明亮工業風頂部導航
|
||||
*
|
||||
* Features:
|
||||
* - 玻璃效果 + 固定定位
|
||||
* - 連線狀態指示器
|
||||
* - 語系切換器
|
||||
* - 使用者選單
|
||||
* Header - figma-v2 品牌導航列
|
||||
* ==============================
|
||||
* @updated 2026-04-03 Claude Code — 完整對齊 figma-v2 設計
|
||||
* - brand-area: A + wooo(VT323) + I 混合字體 logo
|
||||
* - page-title: Syne 26px 800
|
||||
* - lang-btn: pill 樣式 (border-radius 20px)
|
||||
* - avatar: 34px 圓形 #d97757
|
||||
*
|
||||
* Phase 19: 使用 Z_INDEX.HEADER (30)
|
||||
* @see lib/constants/z-index.ts
|
||||
@@ -18,10 +16,6 @@
|
||||
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'
|
||||
|
||||
// =============================================================================
|
||||
@@ -47,106 +41,161 @@ export function Header({
|
||||
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 }}
|
||||
className={className}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: 68,
|
||||
background: '#faf9f3',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
zIndex: Z_INDEX.HEADER,
|
||||
}}
|
||||
>
|
||||
{/* 左側:頁面標題 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="font-heading text-xl font-bold text-nothing-black tracking-tight">
|
||||
{tDashboard('title')}
|
||||
</h1>
|
||||
</div>
|
||||
{/* Brand Area — 224px 固定寬,與 sidebar 對齊 */}
|
||||
<div style={{
|
||||
width: sidebarCollapsed ? 64 : 224,
|
||||
height: 68,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 14px',
|
||||
gap: 10,
|
||||
flexShrink: 0,
|
||||
borderRight: '0.5px solid #e0ddd4',
|
||||
background: '#faf9f3',
|
||||
overflow: 'hidden',
|
||||
transition: 'width 0.3s ease',
|
||||
}}>
|
||||
{/* NemoClaw mini SVG */}
|
||||
<div style={{ width: 36, height: 36, flexShrink: 0 }}>
|
||||
<svg width="36" height="36" viewBox="0 0 140 140" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="hdr-ceramic" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#FFFFFF" />
|
||||
<stop offset="40%" stopColor="#F8F8F8" />
|
||||
<stop offset="70%" stopColor="#E8E8E8" />
|
||||
<stop offset="100%" stopColor="#D8D8D8" />
|
||||
</linearGradient>
|
||||
<radialGradient id="hdr-led" cx="40%" cy="35%" r="60%">
|
||||
<stop offset="0%" stopColor="#7AB8F5" />
|
||||
<stop offset="100%" stopColor="#2B6CB0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<circle cx="70" cy="70" r="32" fill="url(#hdr-ceramic)" stroke="#E0E0E0" strokeWidth="1" />
|
||||
<circle cx="70" cy="70" r="16" fill="url(#hdr-led)">
|
||||
<animate attributeName="r" values="14;17;14" dur="2s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="70" cy="70" r="8" fill="white" opacity="0.8" />
|
||||
<path d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6" stroke="url(#hdr-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<path d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6" stroke="#4A90D9" strokeWidth="3" strokeLinecap="round" fill="none" opacity="0.5" />
|
||||
<path d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82" stroke="url(#hdr-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<path d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82" stroke="#4A90D9" strokeWidth="3" strokeLinecap="round" fill="none" opacity="0.5" />
|
||||
<path d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82" stroke="url(#hdr-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<path d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82" stroke="#4A90D9" strokeWidth="3" strokeLinecap="round" fill="none" opacity="0.5" />
|
||||
<path d="M 48 92 L 28 112 L 16 116" stroke="url(#hdr-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<path d="M 92 92 L 112 112 L 124 116" stroke="url(#hdr-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<circle cx="70" cy="70" r="42" fill="none" stroke="#4A90D9" strokeWidth="1" strokeDasharray="6 6" opacity="0.3">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 70 70" to="360 70 70" dur="8s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
</svg>
|
||||
</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-body text-status-critical">
|
||||
{pendingApprovals} {tDashboard('pendingApprovals')}
|
||||
</span>
|
||||
{/* Brand text: A + wooo + I (figma 混合字體) */}
|
||||
{!sidebarCollapsed && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 0, lineHeight: 1 }}>
|
||||
<span style={{ fontFamily: 'var(--font-body), "DM Mono", monospace', fontSize: 22, fontWeight: 700, color: '#141413', letterSpacing: '-0.5px' }}>A</span>
|
||||
<span style={{ fontFamily: 'var(--font-dot-matrix), "VT323", monospace', fontSize: 30, color: '#d97757', letterSpacing: 2, lineHeight: 1 }}>wooo</span>
|
||||
<span style={{ fontFamily: 'var(--font-body), "DM Mono", monospace', fontSize: 22, fontWeight: 700, color: '#141413', letterSpacing: '-0.5px' }}>I</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Connection Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusOrb status={connectionOrbStatus} size="md" glow />
|
||||
<span className={cn(
|
||||
'font-body text-sm',
|
||||
connectionStatus === 'connected' ? 'text-status-healthy' : 'text-nothing-gray-500'
|
||||
)}>
|
||||
{connectionLabel}
|
||||
</span>
|
||||
{mockMode && (
|
||||
<span className="px-2 py-0.5 text-xs font-body rounded bg-status-warning/10 text-status-warning border border-status-warning/30">
|
||||
MOCK
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Header Right — page title + lang switcher + avatar */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 24px',
|
||||
gap: 16,
|
||||
background: '#faf9f3',
|
||||
}}>
|
||||
{/* Page title */}
|
||||
<span style={{
|
||||
fontFamily: 'var(--font-heading), Syne, sans-serif',
|
||||
fontSize: 26,
|
||||
fontWeight: 800,
|
||||
color: '#141413',
|
||||
flex: 1,
|
||||
letterSpacing: '-0.5px',
|
||||
}}>
|
||||
{tDashboard('title')}
|
||||
</span>
|
||||
|
||||
{/* 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-body 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-body 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>
|
||||
{/* Language switcher — pill style */}
|
||||
<button
|
||||
onClick={() => switchLocale('zh-TW')}
|
||||
style={{
|
||||
padding: '5px 12px',
|
||||
fontFamily: 'var(--font-body), "DM Mono", monospace',
|
||||
fontSize: 11,
|
||||
border: '0.5px solid',
|
||||
borderRadius: 20,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
...(locale === 'zh-TW'
|
||||
? { background: '#141413', color: '#fff', borderColor: '#141413' }
|
||||
: { background: '#fff', color: '#87867f', borderColor: '#e0ddd4' }),
|
||||
}}
|
||||
>
|
||||
{t('zhTW')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => switchLocale('en')}
|
||||
style={{
|
||||
padding: '5px 12px',
|
||||
fontFamily: 'var(--font-body), "DM Mono", monospace',
|
||||
fontSize: 11,
|
||||
border: '0.5px solid',
|
||||
borderRadius: 20,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
...(locale === 'en'
|
||||
? { background: '#141413', color: '#fff', borderColor: '#141413' }
|
||||
: { background: '#fff', color: '#87867f', borderColor: '#e0ddd4' }),
|
||||
}}
|
||||
>
|
||||
{t('en')}
|
||||
</button>
|
||||
|
||||
{/* Avatar — 34px 圓形 #d97757 */}
|
||||
<div style={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: '50%',
|
||||
background: '#d97757',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 12,
|
||||
color: '#fff',
|
||||
fontWeight: 700,
|
||||
flexShrink: 0,
|
||||
fontFamily: 'var(--font-body), monospace',
|
||||
}}>
|
||||
OG
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -160,68 +160,20 @@ export function Sidebar({
|
||||
className={cn(
|
||||
'fixed left-0 top-0 h-screen',
|
||||
'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',
|
||||
// figma-v2: #faf9f3 背景
|
||||
className
|
||||
)}
|
||||
style={{ zIndex: Z_INDEX.SIDEBAR }}
|
||||
style={{
|
||||
zIndex: Z_INDEX.SIDEBAR,
|
||||
background: '#faf9f3',
|
||||
borderRight: '0.5px solid #e0ddd4',
|
||||
width: collapsed ? 64 : 224,
|
||||
transition: 'width 0.3s ease',
|
||||
}}
|
||||
>
|
||||
{/* Logo 區 - NemoClaw + 品牌名稱 (VT323 點陣字體) */}
|
||||
<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 gap-3'
|
||||
)}>
|
||||
{/* NemoClaw Mini SVG */}
|
||||
<div style={{ width: 36, height: 36, flexShrink: 0 }}>
|
||||
<svg width="36" height="36" viewBox="0 0 140 140" className="drop-shadow-sm" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="sb-ceramic" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#FFFFFF" />
|
||||
<stop offset="40%" stopColor="#F8F8F8" />
|
||||
<stop offset="70%" stopColor="#E8E8E8" />
|
||||
<stop offset="100%" stopColor="#D8D8D8" />
|
||||
</linearGradient>
|
||||
<radialGradient id="sb-led" cx="40%" cy="35%" r="60%">
|
||||
<stop offset="0%" stopColor="#7AB8F5" />
|
||||
<stop offset="100%" stopColor="#2B6CB0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
{/* Body */}
|
||||
<circle cx="70" cy="70" r="32" fill="url(#sb-ceramic)" stroke="#E0E0E0" strokeWidth="1" />
|
||||
<circle cx="70" cy="70" r="16" fill="url(#sb-led)">
|
||||
<animate attributeName="r" values="14;17;14" dur="2s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="70" cy="70" r="8" fill="white" opacity="0.8" />
|
||||
{/* Top arm */}
|
||||
<path d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6" stroke="url(#sb-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<path d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6" stroke="#4A90D9" strokeWidth="3" strokeLinecap="round" fill="none" opacity="0.5" />
|
||||
{/* Left arm */}
|
||||
<path d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82" stroke="url(#sb-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<path d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82" stroke="#4A90D9" strokeWidth="3" strokeLinecap="round" fill="none" opacity="0.5" />
|
||||
{/* Right arm */}
|
||||
<path d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82" stroke="url(#sb-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<path d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82" stroke="#4A90D9" strokeWidth="3" strokeLinecap="round" fill="none" opacity="0.5" />
|
||||
{/* Bottom arms */}
|
||||
<path d="M 48 92 L 28 112 L 16 116" stroke="url(#sb-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
<path d="M 92 92 L 112 112 L 124 116" stroke="url(#sb-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
|
||||
{/* Orbit ring */}
|
||||
<circle cx="70" cy="70" r="42" fill="none" stroke="#4A90D9" strokeWidth="1" strokeDasharray="6 6" opacity="0.3">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 70 70" to="360 70 70" dur="8s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
</svg>
|
||||
</div>
|
||||
{/* Brand name - VT323 點陣字體 (hidden when collapsed) */}
|
||||
{!collapsed && (
|
||||
<span className="font-dot-matrix text-xl text-neutral-900 tracking-widest uppercase">
|
||||
{tBrand('name')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Logo 區 - figma-v2: 與 header brand-area 完全一致,但此處因 header 佔滿,sidebar 不重複顯示 logo */}
|
||||
{/* sidebar top spacer - 68px 高度對齊 header */}
|
||||
<div style={{ height: 68, flexShrink: 0 }} />
|
||||
|
||||
{/* 導航列表 - AI中心 v6 4分區 */}
|
||||
<nav className="flex-1 py-2 overflow-y-auto">
|
||||
@@ -317,13 +269,14 @@ export function Sidebar({
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* 底部區域 - 極簡版本號 */}
|
||||
<div className={cn(
|
||||
'border-t-[0.5px] border-neutral-200 py-3',
|
||||
collapsed ? 'px-2 text-center' : 'px-4'
|
||||
)}>
|
||||
{/* 底部區域 - 版本號 */}
|
||||
<div style={{
|
||||
borderTop: '0.5px solid #e0ddd4',
|
||||
padding: collapsed ? '8px 0' : '8px 12px',
|
||||
textAlign: collapsed ? 'center' : 'left',
|
||||
}}>
|
||||
{!collapsed && (
|
||||
<span className="text-[10px] font-body text-muted uppercase tracking-widest">
|
||||
<span style={{ fontSize: 10, color: '#b0ad9f', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
||||
v1.0.0
|
||||
</span>
|
||||
)}
|
||||
@@ -332,14 +285,21 @@ export function Sidebar({
|
||||
{/* 折疊按鈕 */}
|
||||
<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'
|
||||
)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: -12,
|
||||
top: 80,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
background: '#faf9f3',
|
||||
border: '0.5px solid #e0ddd4',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
aria-label={collapsed ? tSidebar('expand') : tSidebar('collapse')}
|
||||
>
|
||||
{collapsed ? (
|
||||
|
||||
Reference in New Issue
Block a user