Files
awoooi/apps/web/src/components/layout/header.tsx
Your Name 716ed5a77c
Some checks failed
CD Pipeline / tests (push) Successful in 1m19s
Code Review / ai-code-review (push) Successful in 22s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
fix(web): 收斂 IwoooS 單一資安入口
2026-05-31 17:55:02 +08:00

235 lines
9.5 KiB
TypeScript

'use client'
/**
* 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
*/
import { useCallback } from 'react'
import { useTranslations } from 'next-intl'
import { usePathname } from 'next/navigation'
import { Z_INDEX } from '@/lib/constants/z-index'
// =============================================================================
// Types
// =============================================================================
interface HeaderProps {
locale: string
sidebarCollapsed?: boolean
compact?: boolean
className?: string
}
// =============================================================================
// Component
// =============================================================================
export function Header({
locale,
sidebarCollapsed = false,
compact = false,
className,
}: HeaderProps) {
const t = useTranslations('locale')
const tDashboard = useTranslations('dashboard')
const pathname = usePathname()
const switchLocale = useCallback((newLocale: string) => {
const newPath = pathname.replace(`/${locale}`, `/${newLocale}`)
window.location.href = newPath
}, [locale, pathname])
const brandWidth = compact ? 64 : sidebarCollapsed ? 64 : 224
return (
<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,
}}
>
{/* Brand Area — 224px 固定寬,與 sidebar 對齊 */}
<div style={{
width: brandWidth,
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>
{/* Brand text: A + wooo + I (figma 混合字體) */}
{!compact && !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>
{/* Header Right — page title + lang switcher + avatar */}
<div style={{
flex: 1,
minWidth: 0,
display: 'flex',
alignItems: 'center',
padding: compact ? '0 10px' : '0 24px',
gap: compact ? 8 : 16,
background: '#faf9f3',
}}>
{/* Page title */}
<span style={{
fontFamily: 'var(--font-heading), Syne, sans-serif',
fontSize: compact ? 20 : 26,
fontWeight: 800,
color: '#141413',
flex: 1,
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
letterSpacing: 0,
}}>
{tDashboard('title')}
</span>
{/* ⌘K Command Palette 入口提示 */}
<button
onClick={() => {
const e = new KeyboardEvent('keydown', { key: 'k', metaKey: true, bubbles: true })
window.dispatchEvent(e)
}}
style={{
display: compact ? 'none' : 'flex', alignItems: 'center', gap: 6,
padding: '5px 12px',
background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 8,
cursor: 'pointer', fontSize: 11, color: '#87867f',
transition: 'all 0.15s',
}}
onMouseEnter={e => { (e.currentTarget as HTMLButtonElement).style.borderColor = '#4A90D9'; (e.currentTarget as HTMLButtonElement).style.color = '#4A90D9' }}
onMouseLeave={e => { (e.currentTarget as HTMLButtonElement).style.borderColor = '#e0ddd4'; (e.currentTarget as HTMLButtonElement).style.color = '#87867f' }}
>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<span style={{ fontFamily: 'var(--font-body), monospace' }}>{tDashboard('searchPlaceholderShort')}</span>
<kbd style={{ fontSize: 9, background: '#f0ede5', borderRadius: 3, padding: '1px 5px', fontFamily: 'monospace', color: '#87867f' }}>K</kbd>
</button>
{/* Language switcher — pill style */}
<button
onClick={() => switchLocale('zh-TW')}
style={{
display: compact ? 'none' : 'block',
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={{
display: compact ? 'none' : 'block',
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={{
display: compact ? 'none' : 'flex',
width: 34,
height: 34,
borderRadius: '50%',
background: '#d97757',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
color: '#fff',
fontWeight: 700,
flexShrink: 0,
fontFamily: 'var(--font-body), monospace',
}}>
OG
</div>
</div>
</header>
)
}