# UI 重设计 P1：设计系统 Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 将主题系统切换到「明亮 Bento 现代风」新设计令牌（双主题），移除自定义字体，硬编码颜色收归 palette，并重写 MASTER.md 使设计文档与实现一致。

**Architecture:** 只改 `src/theme/`、`src/app/_layout.tsx`（字体加载）、`src/domain/channelConfig.ts`（品牌色引用）、`src/components/CategoryPicker.tsx`（色板引用）、`design-system/`。Token 名不变只改值，组件接口向后兼容，现有页面自动"粗换皮"。

**Tech Stack:** React Native + Expo + TypeScript + Vitest。

**Spec:** `docs/ui-redesign-design.md` §3（视觉语言）、§10（P1 验收）。

**验收标准：** `npm test` 全绿；`npm run typecheck` 通过；`src/` 下不再引用 `@expo-google-fonts/*`；`presets.ts` 的 typography 无 `fontFamily`。

---

### Task 1: 新设计令牌的失败测试

**Files:**
- Modify: `tests/theme.test.ts`
- Create: `tests/palette.test.ts`

- [ ] **Step 1: 重写 tests/theme.test.ts 的断言以匹配新令牌**

完整替换文件内容为：

```typescript
import { describe, expect, it } from 'vitest';
import { lightTheme, darkTheme, presetThemes } from '../src/theme/presets';
import { createTheme } from '../src/theme/createTheme';
import type { ThemeTokens } from '../src/theme/tokens';

describe('预置主题（明亮 Bento 设计系统）', () => {
  it('lightTheme 与 darkTheme 关键色彩不同', () => {
    expect(lightTheme.colors.bgPrimary).not.toBe(darkTheme.colors.bgPrimary);
    expect(lightTheme.colors.fgPrimary).not.toBe(darkTheme.colors.fgPrimary);
  });

  it('浅色背景为 #F6F7F9，暗色为 OLED 纯黑 #040508', () => {
    expect(lightTheme.colors.bgPrimary).toBe('#F6F7F9');
    expect(darkTheme.colors.bgPrimary).toBe('#040508');
  });

  it('accent 为近黑白单色：浅色 #111318，暗色反转为 #F3F4F6', () => {
    expect(lightTheme.colors.accent).toBe('#111318');
    expect(darkTheme.colors.accent).toBe('#F3F4F6');
  });

  it('accentLight 为 accent 的半透明底色（不再是靛蓝族）', () => {
    expect(lightTheme.colors.accentLight).toBe('rgba(17,19,24,0.08)');
    expect(darkTheme.colors.accentLight).toBe('rgba(243,244,246,0.10)');
  });

  it('财务语义色：暗色整体提亮一档', () => {
    expect(lightTheme.colors.financial.income).toBe('#10B981');
    expect(lightTheme.colors.financial.expense).toBe('#EF4444');
    expect(lightTheme.colors.financial.transfer).toBe('#3B82F6');
    expect(darkTheme.colors.financial.income).toBe('#34D399');
    expect(darkTheme.colors.financial.expense).toBe('#F87171');
    expect(darkTheme.colors.financial.transfer).toBe('#60A5FA');
  });

  it('圆角含 xl(24)，卡片默认大圆角', () => {
    for (const theme of [lightTheme, darkTheme]) {
      expect(theme.radii.sm).toBe(8);
      expect(theme.radii.md).toBe(12);
      expect(theme.radii.lg).toBe(16);
      expect(theme.radii.xl).toBe(24);
      expect(theme.radii.full).toBe(9999);
    }
  });

  it('字阶含 display(34)，且不指定 fontFamily（系统字体）', () => {
    for (const theme of [lightTheme, darkTheme] as ThemeTokens[]) {
      expect(theme.typography.display.fontSize).toBe(34);
      expect(theme.typography.h1.fontSize).toBe(28);
      expect(theme.typography.h2.fontSize).toBe(22);
      expect(theme.typography.h3.fontSize).toBe(17);
      for (const key of ['display', 'h1', 'h2', 'h3', 'body', 'bodySmall', 'caption'] as const) {
        expect(theme.typography[key].fontFamily).toBeUndefined();
      }
    }
  });

  it('presetThemes 注册表含 light 与 dark', () => {
    expect(presetThemes.light).toBe(lightTheme);
    expect(presetThemes.dark).toBe(darkTheme);
  });

  it('spacing/shadows 结构完整', () => {
    for (const theme of [lightTheme, darkTheme] as ThemeTokens[]) {
      expect(theme.spacing).toHaveProperty('xs');
      expect(theme.spacing).toHaveProperty('xl');
      expect(theme.shadows).toHaveProperty('sm');
      expect(theme.shadows).toHaveProperty('lg');
    }
  });
});

describe('createTheme 自定义主题工厂', () => {
  it('未覆盖时等价于 lightTheme', () => {
    const custom = createTheme({});
    expect(custom.colors.bgPrimary).toBe(lightTheme.colors.bgPrimary);
    expect(custom.spacing).toEqual(lightTheme.spacing);
  });

  it('覆盖 accent 颜色时其他颜色保留', () => {
    const custom = createTheme({ colors: { ...lightTheme.colors, accent: '#FF5722' } });
    expect(custom.colors.accent).toBe('#FF5722');
    expect(custom.colors.bgPrimary).toBe(lightTheme.colors.bgPrimary);
    expect(custom.colors.success).toBe(lightTheme.colors.success);
  });

  it('覆盖 financial 子对象时深度合并', () => {
    const custom = createTheme({ colors: { ...lightTheme.colors, financial: { ...lightTheme.colors.financial, income: '#000' } } });
    expect(custom.colors.financial.income).toBe('#000');
    expect(custom.colors.financial.expense).toBe(lightTheme.colors.financial.expense);
  });

  it('覆盖 spacing 部分键时其他键保留', () => {
    const custom = createTheme({ spacing: { ...lightTheme.spacing, lg: 32 } });
    expect(custom.spacing.lg).toBe(32);
    expect(custom.spacing.md).toBe(lightTheme.spacing.md);
  });
});
```

- [ ] **Step 2: 新建 tests/palette.test.ts（分类色板 + 渠道品牌色）**

```typescript
import { describe, expect, it } from 'vitest';
import { CATEGORY_PALETTE, getCategoryColor, CHANNEL_BRAND_COLORS } from '../src/theme/palette';

describe('categoryPalette', () => {
  it('色板为 12 色循环', () => {
    expect(CATEGORY_PALETTE).toHaveLength(12);
    for (const c of CATEGORY_PALETTE) expect(c).toMatch(/^#[0-9A-Fa-f]{6}$/);
  });

  it('内置分类有具名颜色（与原 CategoryPicker 一致）', () => {
    expect(getCategoryColor('food')).toBe('#F59E0B');
    expect(getCategoryColor('transport')).toBe('#3B82F6');
    expect(getCategoryColor('salary')).toBe('#22C55E');
  });

  it('未知分类 id 走哈希循环，结果确定且在色板内', () => {
    const a = getCategoryColor('user_custom_abc');
    expect(CATEGORY_PALETTE).toContain(a);
    expect(getCategoryColor('user_custom_abc')).toBe(a); // 幂等
  });

  it('空字符串 id 不崩溃', () => {
    expect(CATEGORY_PALETTE).toContain(getCategoryColor(''));
  });

  it('渠道品牌色常量', () => {
    expect(CHANNEL_BRAND_COLORS.alipay).toBe('#1677FF');
    expect(CHANNEL_BRAND_COLORS.wechat).toBe('#07C160');
    expect(CHANNEL_BRAND_COLORS.bank).toBe('#E53935');
  });
});
```

- [ ] **Step 3: 运行测试确认失败**

Run: `npx vitest run tests/theme.test.ts tests/palette.test.ts`
Expected: FAIL —— `Cannot find module '../src/theme/palette'`，且 theme 断言多项不通过（旧色板）。

- [ ] **Step 4: Commit**

```bash
git add tests/theme.test.ts tests/palette.test.ts
git commit -m "test: P1 设计系统新令牌与色板的失败测试"
```

---

### Task 2: tokens.ts — display 字阶 + radii.xl

**Files:**
- Modify: `src/theme/tokens.ts`

- [ ] **Step 1: 修改 ThemeRadii 与 ThemeTypography**

`ThemeRadii` 改为（加 `xl`）：

```typescript
export interface ThemeRadii {
  sm: number;
  md: number;
  lg: number;
  xl: number;   // 卡片默认大圆角（24）
  full: number;
}
```

`ThemeTypographyEntry` 的 `fontFamily` 标记废弃（保留字段以兼容现存 `theme.typography.X.fontFamily` 引用，后续阶段逐页清理）：

```typescript
export interface ThemeTypographyEntry {
  fontSize: number;
  fontWeight: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
  lineHeight: number;
  /** @deprecated 设计系统已切换为系统字体，预置主题不再设置该字段；引用处将在 P4/P5 逐页移除。 */
  fontFamily?: string;
}
```

`ThemeTypography` 加 `display`：

```typescript
export interface ThemeTypography {
  display: ThemeTypographyEntry;  // 34/800，净资产等大数字
  h1: ThemeTypographyEntry;
  h2: ThemeTypographyEntry;
  h3: ThemeTypographyEntry;
  body: ThemeTypographyEntry;
  bodySmall: ThemeTypographyEntry;
  caption: ThemeTypographyEntry;
}
```

- [ ] **Step 2: 运行 typecheck 确认报错点**

Run: `npm run typecheck`
Expected: FAIL —— `presets.ts` 缺少 `display` 与 `radii.xl`（Task 3 修复）。

- [ ] **Step 3: 暂不 commit（与 Task 3 一起）**

---

### Task 3: presets.ts — 新双主题色板

**Files:**
- Modify: `src/theme/presets.ts`

- [ ] **Step 1: 完整替换 presets.ts**

```typescript
import type { ThemeTokens } from './tokens';

/**
 * 预置主题 —— 「明亮 Bento 现代风」（docs/ui-redesign-design.md §3）。
 * 浅色为默认；暗色为 OLED 纯黑完整对等主题。
 * accent 为近黑白单色：品牌感靠排版与财务语义色表达。
 */

export const lightTheme: ThemeTokens = {
  colors: {
    bgPrimary: '#F6F7F9',
    bgSecondary: '#FFFFFF',
    bgTertiary: '#EFF1F4',
    fgPrimary: '#111318',
    fgSecondary: '#6B7280',
    fgInverse: '#FFFFFF',
    accent: '#111318',                 // 近黑：按钮/选中态
    accentLight: 'rgba(17,19,24,0.08)', // accent 8% 底色（选中高亮）
    accentDark: '#000000',              // 按压加深态
    success: '#10B981',
    warning: '#F59E0B',
    error: '#EF4444',
    info: '#3B82F6',
    financial: {
      income: '#10B981',
      expense: '#EF4444',
      transfer: '#3B82F6',
    },
    border: '#E5E7EB',
    divider: '#F0F1F3',
    overlay: 'rgba(17,19,24,0.4)',
    skeleton: '#E5E7EB',
    progressBg: 'rgba(17,19,24,0.06)',
  },
  spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
  radii: { sm: 8, md: 12, lg: 16, xl: 24, full: 9999 },
  typography: {
    display: { fontSize: 34, fontWeight: '800', lineHeight: 42 },
    h1: { fontSize: 28, fontWeight: '700', lineHeight: 36 },
    h2: { fontSize: 22, fontWeight: '700', lineHeight: 30 },
    h3: { fontSize: 17, fontWeight: '600', lineHeight: 24 },
    body: { fontSize: 16, fontWeight: '400', lineHeight: 24 },
    bodySmall: { fontSize: 14, fontWeight: '400', lineHeight: 20 },
    caption: { fontSize: 12, fontWeight: '400', lineHeight: 16 },
  },
  shadows: {
    sm: { shadowColor: '#111318', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.04, shadowRadius: 4, elevation: 1 },
    md: { shadowColor: '#111318', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.06, shadowRadius: 8, elevation: 2 },
    lg: { shadowColor: '#111318', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.10, shadowRadius: 12, elevation: 4 },
  },
};

export const darkTheme: ThemeTokens = {
  ...lightTheme,
  colors: {
    bgPrimary: '#040508',               // OLED 纯黑
    bgSecondary: '#101218',
    bgTertiary: '#1A1D24',
    fgPrimary: '#F3F4F6',
    fgSecondary: '#9CA3AF',
    fgInverse: '#111318',
    accent: '#F3F4F6',                  // 反转为白
    accentLight: 'rgba(243,244,246,0.10)',
    accentDark: '#FFFFFF',
    success: '#34D399',
    warning: '#FBBF24',
    error: '#F87171',
    info: '#60A5FA',
    financial: {
      income: '#34D399',                // 提亮一档保证对比度
      expense: '#F87171',
      transfer: '#60A5FA',
    },
    border: 'rgba(255,255,255,0.08)',
    divider: 'rgba(255,255,255,0.04)',
    overlay: 'rgba(0,0,0,0.7)',
    skeleton: '#1A1D24',
    progressBg: 'rgba(243,244,246,0.08)',
  },
  shadows: {
    sm: { shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.4, shadowRadius: 2, elevation: 1 },
    md: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.5, shadowRadius: 4, elevation: 3 },
    lg: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.6, shadowRadius: 8, elevation: 5 },
  },
};

/** 预置主题注册表。 */
export const presetThemes: Record<string, ThemeTokens> = {
  light: lightTheme,
  dark: darkTheme,
};
```

- [ ] **Step 2: 运行 Task 1 的测试**

Run: `npx vitest run tests/theme.test.ts`
Expected: PASS（palette.test.ts 仍 FAIL，Task 4 修复）。

- [ ] **Step 3: 运行 typecheck**

Run: `npm run typecheck`
Expected: PASS。

- [ ] **Step 4: Commit**

```bash
git add src/theme/tokens.ts src/theme/presets.ts
git commit -m "feat(theme): P1 明亮 Bento 令牌 —— 近黑 accent、xl 圆角、display 字阶、系统字体"
```

---

### Task 4: theme/palette.ts — 分类色板 + 渠道品牌色

**Files:**
- Create: `src/theme/palette.ts`
- Modify: `src/domain/channelConfig.ts:32,40,48`
- Modify: `src/components/CategoryPicker.tsx:31-48,59`

- [ ] **Step 1: 创建 src/theme/palette.ts（纯 TS，无 RN/domain 依赖，可被双方向引用）**

```typescript
/**
 * 分类/渠道颜色统一色板（docs/ui-redesign-design.md §3.2）。
 *
 * 纯 TS 模块，不依赖 React Native 与 domain 层，因此：
 * - components/（CategoryPicker 等）用它给分类配色；
 * - domain/channelConfig.ts 用它引用渠道品牌色；
 * 全工程的颜色字面量只允许存在于 presets.ts 与本文件（P5 grep 审计）。
 */

/** 12 色循环色板：未知/用户自定义分类按 id 哈希取色。 */
export const CATEGORY_PALETTE = [
  '#F59E0B', // Amber
  '#3B82F6', // Blue
  '#EC4899', // Pink
  '#06B6D4', // Cyan
  '#6366F1', // Indigo
  '#8B5CF6', // Purple
  '#10B981', // Emerald
  '#64748B', // Slate
  '#F43F5E', // Rose
  '#14B8A6', // Teal
  '#EF4444', // Red
  '#84CC16', // Lime
] as const;

/** 内置分类的具名颜色（沿用原 CategoryPicker CATEGORY_COLORS 的映射，视觉不变）。 */
const NAMED_CATEGORY_COLORS: Record<string, string> = {
  food: '#F59E0B',
  transport: '#3B82F6',
  shopping: '#EC4899',
  housing_utility: '#06B6D4',
  housing_rent: '#6366F1',
  housing_communication: '#8B5CF6',
  entertainment: '#10B981',
  services: '#64748B',
  personal_care: '#F43F5E',
  clothing: '#14B8A6',
  health: '#EF4444',
  learning: '#84CC16',
  salary: '#22C55E',
  income_activity: '#EF4444',
  income_investment: '#F59E0B',
};

/**
 * 本地 FNV-1a 哈希。
 * 注：domain/ledger.ts 也有 hash()，但 theme 层不反向依赖 domain 解析器，故保留这份 8 行实现。
 */
function fnv1a(input: string): number {
  let h = 0x811c9dc5;
  for (let i = 0; i < input.length; i++) {
    h ^= input.charCodeAt(i);
    h = Math.imul(h, 0x01000193);
  }
  return h >>> 0;
}

/** 取分类颜色：具名映射优先，否则按 id 哈希在 12 色板内确定性循环。 */
export function getCategoryColor(categoryId: string): string {
  const named = NAMED_CATEGORY_COLORS[categoryId];
  if (named) return named;
  return CATEGORY_PALETTE[fnv1a(categoryId) % CATEGORY_PALETTE.length];
}

/** 渠道品牌色（供 domain/channelConfig.ts 引用）。 */
export const CHANNEL_BRAND_COLORS = {
  alipay: '#1677FF',
  wechat: '#07C160',
  bank: '#E53935',
} as const;
```

- [ ] **Step 2: channelConfig.ts 改用品牌色常量**

文件顶部加 import：

```typescript
import { CHANNEL_BRAND_COLORS } from '../theme/palette';
```

三处字面量替换：`color: '#1677FF'` → `color: CHANNEL_BRAND_COLORS.alipay`，`color: '#07C160'` → `color: CHANNEL_BRAND_COLORS.wechat`，`color: '#E53935'` → `color: CHANNEL_BRAND_COLORS.bank`。

- [ ] **Step 3: CategoryPicker.tsx 改用 getCategoryColor**

删除第 31–48 行的 `CATEGORY_COLORS` 常量（含 `fallback`），文件顶部加：

```typescript
import { getCategoryColor } from '../theme/palette';
```

第 59 行 `const color = CATEGORY_COLORS[cat.id] || CATEGORY_COLORS.fallback;` 改为：

```typescript
const color = getCategoryColor(cat.id);
```

- [ ] **Step 4: 运行测试**

Run: `npx vitest run tests/palette.test.ts tests/channelConfig.test.ts`
Expected: PASS（渠道色值未变，channelConfig 测试不受影响）。

- [ ] **Step 5: Commit**

```bash
git add src/theme/palette.ts src/domain/channelConfig.ts src/components/CategoryPicker.tsx
git commit -m "feat(theme): 分类/渠道颜色收归 theme/palette，消除组件与 domain 硬编码色"
```

---

### Task 5: 移除自定义字体（Caveat/Quicksand）

**Files:**
- Modify: `src/app/_layout.tsx:27-39,70-79,370`
- Modify: `package.json:14-15`

- [ ] **Step 1: 删除 _layout.tsx 的字体导入**

删除第 27–39 行两个 `import { useFonts, ... } from '@expo-google-fonts/...'` 块。

- [ ] **Step 2: 删除 useFonts 调用**

删除第 70–79 行的 `const [fontsLoaded, fontError] = useFonts({...});`。

- [ ] **Step 3: 简化加载门禁**

第 370 行：

```typescript
if (phase === 'loading' || (!fontsLoaded && !fontError)) {
```

改为：

```typescript
if (phase === 'loading') {
```

- [ ] **Step 4: 移除 package.json 依赖并重装**

删除 `package.json` 中 `"@expo-google-fonts/caveat"` 与 `"@expo-google-fonts/quicksand"` 两行，然后：

Run: `npm install --legacy-peer-deps`
Expected: lockfile 更新，无字体包残留。

- [ ] **Step 5: 验证无残留引用**

Run: `grep -rn "expo-google-fonts\|Caveat_\|Quicksand_" src/ package.json`
Expected: 无输出（typography 中 `theme.typography.X.fontFamily` 的运行时引用返回 undefined，RN 回退系统字体，无需在 P1 清理）。

- [ ] **Step 6: 全量测试 + typecheck**

Run: `npm test && npm run typecheck`
Expected: 全绿。

- [ ] **Step 7: Commit**

```bash
git add src/app/_layout.tsx package.json package-lock.json
git commit -m "feat(theme): 移除 Caveat/Quicksand 自定义字体，切换系统字体"
```

---

### Task 6: commonStyles 适配新圆角

**Files:**
- Modify: `src/theme/commonStyles.ts`

- [ ] **Step 1: modalCard 圆角 lg → xl**

`modalCard` 中 `borderRadius: theme.radii.lg, // 弹窗使用大圆角 lg (16px)` 改为：

```typescript
borderRadius: theme.radii.xl, // 弹窗使用卡片级大圆角 xl (24px)
```

（`input` 的 `theme.radii.md` 现在即 12px，无需改动，仅更新注释 `// 输入框标准 radii 为 md (12px)`。）

- [ ] **Step 2: 验证**

Run: `npm run typecheck`
Expected: PASS。

- [ ] **Step 3: Commit**

```bash
git add src/theme/commonStyles.ts
git commit -m "feat(theme): commonStyles 弹窗圆角升级为 xl(24)"
```

---

### Task 7: 重写 MASTER.md 使其与实现一致

**Files:**
- Modify: `design-system/beancount-mobile/MASTER.md`

- [ ] **Step 1: 完整替换 MASTER.md**

```markdown
# Beancount Mobile 设计系统（MASTER）

> 本文件与 `src/theme/presets.ts` 一一对应，是实现的事实描述而非平行标准。
> 修改配色/字阶/圆角时必须先改 presets.ts，再同步本文件。

## 设计方向：明亮 Bento 现代风

浅色为主、大圆角卡片（Bento）、近黑白单色 + 财务语义色点缀。
品牌感靠排版与财务色表达，不依赖彩色 accent。暗色为 OLED 纯黑完整对等主题。

## 色板（= presets.ts）

### 浅色（默认）
| Token | 值 | 用途 |
|---|---|---|
| bgPrimary | `#F6F7F9` | 页面背景 |
| bgSecondary | `#FFFFFF` | 卡片 |
| bgTertiary | `#EFF1F4` | 输入框 / chip |
| fgPrimary | `#111318` | 主文字 |
| fgSecondary | `#6B7280` | 次要文字 |
| fgInverse | `#FFFFFF` | 反色文字 |
| accent | `#111318` | 按钮 / 选中态（近黑） |
| accentLight | `rgba(17,19,24,0.08)` | 选中高亮底色 |
| accentDark | `#000000` | 按压态 |
| financial.income | `#10B981` | 收入 |
| financial.expense | `#EF4444` | 支出 |
| financial.transfer | `#3B82F6` | 转账 |
| border | `#E5E7EB` | 边框 |
| overlay | `rgba(17,19,24,0.4)` | 遮罩 |

### 暗色（OLED）
| Token | 值 |
|---|---|
| bgPrimary | `#040508` |
| bgSecondary | `#101218` |
| bgTertiary | `#1A1D24` |
| fgPrimary / accent | `#F3F4F6` |
| fgSecondary | `#9CA3AF` |
| fgInverse | `#111318` |
| financial.income | `#34D399`（提亮） |
| financial.expense | `#F87171`（提亮） |
| financial.transfer | `#60A5FA`（提亮） |
| border | `rgba(255,255,255,0.08)` |

分类/渠道颜色见 `src/theme/palette.ts`（12 色循环 + 具名映射 + 渠道品牌色）。
全工程颜色字面量只允许存在于 `presets.ts` 与 `palette.ts`。

## 字体

系统字体（iOS SF / Android Roboto），不加载自定义字体。
金额数字一律 `fontVariant: ['tabular-nums']` 等宽对齐。

字阶：display 34/800 · h1 28/700 · h2 22/700 · h3 17/600 · body 16/400 · bodySmall 14/400 · caption 12/400。

## 圆角 / 间距 / 阴影

- 圆角：sm 8 · md 12 · lg 16 · **xl 24（卡片与弹窗默认）** · full
- 间距：xs 4 · sm 8 · md 16 · lg 24 · xl 32
- 阴影：浅色 4–12px 弥散轻阴影；暗色以 1px 半透边框代替阴影

## 图标

统一 Ionicons（`@expo/vector-icons`）。禁止用 emoji 充当图标；
分类图标 = Ionicon + 圆形彩色底（CategoryIcon 组件，P2 落地）。

## 反模式

- 禁止在组件中写颜色字面量（`#xxxxxx` / `rgba(...)`），必须走 token
- 禁止低对比文字（< 4.5:1）
- 禁止瞬间状态变化，交互反馈 150–300ms
- 禁止绕过 commonStyles 重复造 input/chip/modal 样式
```

- [ ] **Step 2: Commit**

```bash
git add design-system/beancount-mobile/MASTER.md
git commit -m "docs: MASTER.md 重写为与实现一致的明亮 Bento 设计系统"
```

---

### Task 8: P1 全量验收

- [ ] **Step 1: 全量测试**

Run: `npm test`
Expected: 全部通过（30+ 文件，含新 theme/palette 测试）。

- [ ] **Step 2: typecheck**

Run: `npm run typecheck`
Expected: 无错误。

- [ ] **Step 3: 审计**

Run: `grep -rn "expo-google-fonts" src/ package.json` → 无输出
Run: `grep -rn "4F46E5\|EEF2FF\|3730A3" src/` → 无输出（靛蓝族清零）

已知遗留（不在 P1 处理，已在后续阶段计划内）：`src/app/transaction/new.tsx:219` 的 `#2196F3`（P3 重写 new.tsx 时消除）；`src/app/(tabs)/index.tsx` Hero 卡硬编码白字（P4 首页重写时消除）；组件内 `fontFamily: theme.typography.X.fontFamily` 引用（P4/P5 逐页清理）。

- [ ] **Step 4: 手工走查（需要设备/模拟器）**

Run: `npm run android`（或 expo start）
走查清单：浅色/暗色各过一遍四个 Tab —— 页面背景为米白/纯黑；chip 选中态为黑底白字/白底黑字；卡片圆角变大；无字体加载等待。

---

## 后续计划（不在本文件）

- **P2 组件库**：ScreenHeader / StatCard / AppTabBar / CategoryIcon / DatePickerField / FilterSheet / ManagementScreen
- **P3 录入闭环**：NumpadSheet + 双腿账户选择 + 全局＋ + transaction/new 重写 + SpeedDial 退役
- **P4 四个 Tab 页**：首页待办条 / 交易时间线 / 报表 anchor 统一 / 我的 4 分组
- **P5 管理页模板化 + 清零审计**

每阶段完成后基于实际代码编写下一阶段计划。
