feat(ui): header/sidebar/openclaw 完整對齊 figma-v2
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:
OG T
2026-04-03 11:36:38 +08:00
parent 741a8f4917
commit cbe528b5c6
5 changed files with 306 additions and 383 deletions

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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>
)
}

View File

@@ -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>
)

View File

@@ -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 ? (