# Skill 01: AWOOOI Frontend Aesthetics
# 前端視覺主權守護者

> **管轄範圍**: `apps/web/` (Next.js, Zustand, Tailwind)
> **觸發條件**: 任何 `.tsx`, `.ts`, `.css` 檔案修改

---

## 文件資訊

| 欄位 | 值 |
|------|-----|
| **版本** | v1.6 |
| **建立日期** | 2026-03-20 (台北) |
| **建立者** | Claude Code |
| **最後修改** | 2026-03-31 (台北) |
| **修改者** | Claude Code (首席架構師) |

### 變更紀錄

| 版本 | 日期 | 執行者 | 變更內容 |
|------|------|--------|----------|
| v1.0 | 2026-03-20 | Claude Code | 初始建立 |
| v1.1 | 2026-03-23 | Claude Code | Props Mapping 完整性檢查 |
| v1.2 | 2026-03-25 | Claude Code | 加入文件資訊區塊 |
| v1.3 | 2026-03-27 | Claude Code | Phase 19 Z-Index/GenUI/快捷鍵規範 |
| v1.4 | 2026-03-28 | Claude Code | ✅ Phase 19 Wave 0-5 完成 (~95% + Telemetry 整合) |
| v1.5 | 2026-03-30 | Claude Code | 🔴🔴🔴 前端建置禁止內網 IP (瀏覽器權限事故) |
| v1.6 | 2026-03-31 | Claude Code | 🚀 ADR-042 效能優化模式 (DOM Bypass + Optimistic Updates) |
| v1.7 | 2026-04-09 | Claude Opus 4.6 | 🔴 Sprint 5R 前端重構 — 品牌一致性鐵律 + 設計稿對齊規範 |
| v1.8 | 2026-04-10 | Claude Opus 4.6 | ✅ Sprint 5R 實施完成 — 7 新元件 + 骨架屏 + 60:40 雙欄 |

---

## 🔴🔴🔴 前端建置禁止內網 IP (2026-03-30)

> **血的教訓**: CD 使用 `http://192.168.0.125:32334` 建置，導致瀏覽器彈出「存取區域網路」權限對話框

### 禁止清單

| 變數 | 禁止值 | 正確值 |
|------|--------|--------|
| `NEXT_PUBLIC_API_URL` | `http://192.168.0.*` | `https://awoooi.wooo.work` |
| `NEXT_PUBLIC_SENTRY_DSN` | 包含內網 IP | 使用 Sentry Tunnel |

### 原理

`NEXT_PUBLIC_*` 是 **build-time** 變數，會被打包進 JS Bundle。Runtime 的 K8s ConfigMap 無法覆蓋。

### 檢查點

修改 CD Pipeline 時，必須確認：
```bash
grep "NEXT_PUBLIC" .gitea/workflows/cd.yaml | grep -v "192.168"
# 應該看到所有 NEXT_PUBLIC_* 都使用公網域名
```

---

## 🔴🔴 品牌 Logo 與文字一致性 (2026-04-09)

> **統帥多次糾正**: 所有設計稿和頁面中的 Logo SVG 和 AwoooI 文字必須與正式環境完全一致

### Logo SVG（螺旋眼睛）
- 來源：`header.tsx` L82-111，viewBox `0 0 140 140`
- 漸層：陶瓷白 + 藍色 LED + 觸鬚 + 旋轉虛線圓
- 禁止簡化、禁止替代、禁止自創

### AwoooI 品牌文字
- `A`：DM Mono 20px fw-700 #141413 margin-right:-4px
- `wooo`：VT323 26px #d97757 letterSpacing:0 margin:0 -2px
- `I`：DM Mono 20px fw-700 #141413 margin-left:-3px
- 字母間必須緊湊，整體像一個字

### 設計稿 HTML Mockup
- 直接從 header.tsx 複製 SVG 和文字結構
- OpenClaw 面板也用同款螺旋眼睛 SVG

### 流程圖 icon
- 使用 dashboardicons.com OpenClaw PNG（取代圓圈，不是浮動）
- URL: `https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/openclaw.png`

---

## 核心約束 (Iron Laws)

### 1. Nothing.tech 純白工業風 (絕對標準)

| 元素 | 規範 | 違規處置 |
|------|------|----------|
| 背景 | `bg-white/70 backdrop-blur-[20px]` | 立即修正 |
| 字體 | `VT323` 點陣風格 | 禁止其他字體 |
| 發光色 | `#4A90D9` (Glacier Blue) | 唯一許可強調色 |
| 陰影 | `shadow-lg/20` 白玻璃效果 | 禁止深色陰影 |

### 2. i18n 零容忍硬編碼

```typescript
// ❌ 絕對禁止
<span>確認執行</span>
<span>STATE: IDLE</span>

// ✅ 唯一許可
<span>{t('actions.confirm')}</span>
<span>{t('agent.state')}: {t('agent.idle')}</span>
```

**語言選擇標準** (詳見 `feedback_i18n_language_strategy.md`):
| 元素 | 語言 | 範例 |
|------|------|------|
| UI 文字/狀態/按鈕 | **繁體中文** | 「系統穩定」「待命」 |
| 技術標識/版本 | 英文 | `P0`, `v1.0.0` |
| 產品名稱 | 英文 | `OpenClaw`, `AWOOOI` |

- 所有 UI 文字必須透過 `next-intl` 的 `useTranslations()`
- 字典檔案位置: `apps/web/messages/{locale}.json`

### 3. Zustand SSE 狀態管理

```typescript
// ✅ 正確模式
const buffer = useSSEStore((s) => s.buffer);
const appendChunk = useSSEStore((s) => s.appendChunk);

// ❌ 禁止模式
useState() // 管理 SSE 串流狀態
```

### 4. 禁止 Mock Data 掩蓋錯誤

- 所有 API 調用必須處理真實回應
- 無數據時顯示 `"--"` 或空狀態組件
- 禁止使用假數據填充 UI

### 5. ADR-013 代碼註解規範

**強制 JSDoc 場景**:
```typescript
/**
 * 簽核待審批請求
 * @param id - Approval UUID
 * @param signerId - 簽核者 ID (需有對應 Tier 權限)
 * @returns 簽核結果，包含是否觸發執行
 * @throws {UnauthorizedError} 簽核者權限不足
 */
async function signApproval(id: string, signerId: string): Promise<ApprovalResult>
```

**強制 data-testid**:
```tsx
// 命名: <component>-<element>[-<action>]
<button data-testid="approval-card-approve-btn">
<input data-testid="search-input-filter">
```

---

## 強制驗收程序 (Mandatory Validation)

### 修改任何 `.tsx` 後必須執行:

```bash
# Step 1: TypeScript 靜態檢查
cd apps/web && pnpm exec tsc --noEmit

# Step 2: 生產建置測試
pnpm run build

# Step 3: Hydration 錯誤檢測 (如有疑慮)
pnpm run dev & sleep 5 && curl -s http://localhost:3000 | grep -i "hydration"
```

### 驗收標準

| 檢查項目 | 通過條件 |
|----------|----------|
| tsc --noEmit | Exit code 0, 無錯誤 |
| pnpm build | 成功完成, 無警告 |
| Hydration | 無 "Hydration mismatch" 錯誤 |

---

## 常見陷阱 (Known Pitfalls)

1. **SSR vs Client State**: 使用 `useEffect` 包裝瀏覽器專屬 API
2. **apiBaseUrl 空值**: 確保 `NEXT_PUBLIC_API_URL` 在 build-time 注入
3. **Tailwind Purge**: 動態類名需完整拼寫，禁止字串拼接

---

## 🚨 Polling + 操作 Race Condition (2026-03-23 教訓)

> **事故**: 簽核卡片按下後消失，所有卡片消失，又全部出現。原因是 Zustand Polling 與 signApproval API 競爭

### 症狀

- UI 元素閃爍、消失又出現
- 操作後狀態被覆蓋
- Console 無錯誤但行為異常

### 根因

```typescript
// ❌ 問題模式: Polling 與操作同時進行
const signApproval = async () => {
  await fetch('/api/sign')  // 操作 API
  // ⚠️ 此時 Polling (5秒一次) 可能已用舊數據覆蓋 state
}
```

### ✅ 正確模式: 操作期間暫停 Polling

```typescript
// apps/web/src/stores/approval.store.ts
signApproval: async (id, signerId, signerName, comment) => {
  // 🔧 暫停 Polling
  const wasPolling = pollingTimer !== null
  if (wasPolling) {
    clearInterval(pollingTimer!)
    pollingTimer = null
  }

  try {
    const response = await fetch(`/api/v1/approvals/${id}/sign`, {...})
    // ... 處理回應 ...
  } finally {
    // 🔧 延遲 1 秒後恢復 Polling (讓後端有時間更新)
    if (wasPolling) {
      setTimeout(() => {
        get().startPolling(get().pollingInterval || 5000)
      }, 1000)
    }
  }
}
```

### 適用場景

- 簽核/拒絕 (Approval)
- 任何會修改後端狀態的操作
- 樂觀更新 (Optimistic Update) 模式

---

## Phase 19: Z-Index 與 GenUI 規範 (2026-03-27)

### Z-Index 7-Tier 系統

> **ADR-031**: Omni-Terminal SSE 架構 / **ADR-032**: GenUI 動態渲染機制

```typescript
// lib/constants/z-index.ts - 必須使用此常量
import { Z_INDEX } from '@/lib/constants/z-index'

// ✅ 正確
<div style={{ zIndex: Z_INDEX.OMNI_TERMINAL }}>
<div className={`z-[${Z_INDEX.TOAST}]`}>

// ❌ 禁止: inline z-index (z-50, z-100 等)
<div className="z-50">  // 違規！
```

**層級分配**:
| Tier | 用途 | 值 |
|------|------|-----|
| Tier 0 | 背景 | -10 |
| Tier 1 | 基礎內容 | 0-15 |
| Tier 2 | 導航 (Header/Sidebar) | 30-40 |
| Tier 3 | 浮動面板 (Terminal/Dropdown) | 50-54 |
| Tier 4 | 通知 (Toast/Tooltip) | 60-62 |
| Tier 5 | 模態 (Dialog/Confirm) | 70-75 |
| Tier 6 | 核鑰 (NuclearKey) | 90-99 |
| Tier 7 | DevTools | 100 |

### GenUI 卡片開發規範

```typescript
// genui/cards/NewCard.tsx
interface NewCardProps {
  // Props 必須有嚴格類型定義
}

export const NewCard: React.FC<NewCardProps> = ({ ... }) => {
  return (
    <div className="w-full max-w-2xl bg-white border-2 border-nothing-black">
      {/* Nothing.tech 風格 */}
    </div>
  )
}
```

**新增卡片檢查清單**:
- [ ] Props 定義在 `genui/types.ts`
- [ ] 組件註冊到 `genui/registry.ts`
- [ ] 後端 ALLOWED_COMPONENTS 同步
- [ ] i18n 無硬編碼
- [ ] data-testid 完整
- [ ] Storybook story 已建立

### 快捷鍵規範

```typescript
// contexts/ShortcutContext.tsx
// 快捷鍵必須透過 ShortcutContext 管理

// ✅ 正確
const { registerShortcut } = useShortcutContext()
registerShortcut('terminal.toggle', { key: 'j', modifiers: ['meta'] })

// ❌ 禁止: 直接監聽 keydown
document.addEventListener('keydown', ...)  // 違規！
```

---

## 🚀 效能優化模式 (ADR-042)

> **參考**: [ADR-042-frontend-performance-patterns.md](../../docs/adr/ADR-042-frontend-performance-patterns.md)

### Pattern 1: DOM Bypass (繞過 React 渲染)

**適用**: 高頻更新 (>10/sec)、大量 DOM 節點 (>100 items)

```typescript
// ✅ 正確: 直接操作 DOM，不經過 React state
const containerRef = useRef<HTMLDivElement>(null)

function createLineElement(line: StreamLine): HTMLDivElement {
  const div = document.createElement('div')
  div.textContent = line.content  // XSS 安全
  return div
}

// SSE 直接 append，不用 setState
containerRef.current.appendChild(createLineElement(data))
enforceMaxLines(containerRef.current, 500)  // 記憶體安全
```

### Pattern 2: Optimistic Updates (樂觀更新)

**適用**: 用戶觸發狀態變更、需即時回饋

```typescript
// ✅ 正確: 保存原始 → 樂觀更新 → API → 失敗回滾
const original = [...state.items]
set({ items: updatedItems })  // 0ms UI 回饋
try {
  await api.update(id)
} catch {
  set({ items: original })  // 回滾
}
```

### Pattern 3: AbortController (請求取消)

**適用**: 組件 unmount、新請求覆蓋舊請求

```typescript
// ✅ 正確: 每次請求前取消前一次
const abortRef = useRef<AbortController | null>(null)

const fetchData = async () => {
  abortRef.current?.abort()
  abortRef.current = new AbortController()
  await fetch(url, { signal: abortRef.current.signal })
}

useEffect(() => () => abortRef.current?.abort(), [])  // cleanup
```

### Pattern 4: Exponential Backoff (指數退避)

**適用**: SSE 重連、API 重試

```typescript
const delay = Math.min(BASE * Math.pow(2, attempts), MAX_DELAY)
setTimeout(() => reconnect(), delay)
```

---

## 參考文檔

- ADR-002: Nothing.tech 設計系統
- ADR-031: Omni-Terminal SSE 架構
- ADR-032: GenUI 動態渲染機制
- ADR-042: 前端效能優化模式
- `apps/web/tailwind.config.ts`: 顏色定義
- `apps/web/src/components/ui/`: 原子組件庫
- `apps/web/src/components/genui/`: GenUI 卡片
