Files
awoooi/.agents/skills/01-awoooi-frontend-aesthetics.md
OG T c180bdaaac docs: Sprint 5R 前端重構批准 — ADR-065 + 設計稿 + Skills + LOGBOOK
- ADR-065: Sprint 5R 前端重構決策(版本 A 批准)
- sprint5r-approved-design.html: 統帥批准的設計稿存檔
- Skills 01 v1.7: 品牌 Logo/AwoooI 一致性鐵律
- LOGBOOK: Sprint 5R 開始實施

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 15:15:43 +08:00

11 KiB
Raw Blame History

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 前端重構 — 品牌一致性鐵律 + 設計稿對齊規範

🔴🔴🔴 前端建置禁止內網 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 時,必須確認:

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-111viewBox 0 0 140 140
  • 漸層:陶瓷白 + 藍色 LED + 觸鬚 + 旋轉虛線圓
  • 禁止簡化、禁止替代、禁止自創

AwoooI 品牌文字

  • ADM Mono 20px fw-700 #141413 margin-right:-4px
  • woooVT323 26px #d97757 letterSpacing:0 margin:0 -2px
  • IDM 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 零容忍硬編碼

// ❌ 絕對禁止
<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-intluseTranslations()
  • 字典檔案位置: apps/web/messages/{locale}.json

3. Zustand SSE 狀態管理

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

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

4. 禁止 Mock Data 掩蓋錯誤

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

5. ADR-013 代碼註解規範

強制 JSDoc 場景:

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

強制 data-testid:

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

強制驗收程序 (Mandatory Validation)

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

# 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 無錯誤但行為異常

根因

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

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

// 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 動態渲染機制

// 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 卡片開發規範

// 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 已建立

快捷鍵規範

// 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

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

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

// ✅ 正確: 直接操作 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 (樂觀更新)

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

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

Pattern 3: AbortController (請求取消)

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

// ✅ 正確: 每次請求前取消前一次
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 重試

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 卡片