- 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>
11 KiB
11 KiB
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.tsxL82-111,viewBox0 0 140 140 - 漸層:陶瓷白 + 藍色 LED + 觸鬚 + 旋轉虛線圓
- 禁止簡化、禁止替代、禁止自創
AwoooI 品牌文字
A:DM Mono 20px fw-700 #141413 margin-right:-4pxwooo:VT323 26px #d97757 letterSpacing:0 margin:0 -2pxI: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 零容忍硬編碼
// ❌ 絕對禁止
<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 狀態管理
// ✅ 正確模式
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)
- SSR vs Client State: 使用
useEffect包裝瀏覽器專屬 API - apiBaseUrl 空值: 確保
NEXT_PUBLIC_API_URL在 build-time 注入 - 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)
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 卡片