DriftLedger/docs/ui-redesign-p1-plan.md
fengmengqi 2e73b5a2c6 feat: UI 全面重设计(Bento 明亮风)+ 记一笔 NumpadSheet + 管理页模板 + OCR v5→v6 升级
UI 重设计(P1-P5):
- 主题从靛蓝 accent 切换为近黑白单色(#111318),品牌感靠排版与财务语义色
- 暗色模式适配 OLED 纯黑,财务色整体提亮一档保证对比度
- 圆角体系新增 xl(24),卡片/弹窗统一大圆角
- 移除 Caveat/Quicksand 自定义字体,切换为系统字体
- 新增 display(34/800) 字阶用于净资产等大数字展示
- 分类/标签/渠道颜色统一到 palette.ts 色板(12 色循环 + FNV 哈希)
记一笔 NumpadSheet(P3 录入闭环):
- 全新计算器风格录入面板:方向 chip → 大金额 → 账户 chip → 分类快捷行 → 数字键盘
- 支持表达式输入(20+15-5.5)与字符串十进制求值,避免浮点精度问题
- 高级模式保留多行分录编辑器,简单模式自动推断双腿
- postingLegs.ts 实现 buildPostings 的逆操作(编辑/草稿预填)
- 未保存守卫:整页 beforeRemove + modal 脏状态上报
- 全局弹层(NumpadSheetHost)由+按钮唤起,可通过设置开关
Tab 栏重构:
- AppTabBar 替换默认 tab bar,中央+按钮唤起全局记账面板
- Tab 从 5 个精简为 4 个(首页/交易/报表/设置),+按钮独立
管理页模板 ManagementScreen:
- 统一「列表 + 新增 + 点按编辑 + 长按删除 + FormModal」模式
- 预算/分类/标签/信用卡等管理页消除手写样板
其他:
- OCR 模型从 PP-OCRv5 升级到 v6(det/rec/dict 全部替换)
- GBK 解码器 import 修复(text-encoding-gbk ESM 兼容)
- 批量确认失败数计算修正(用 failedPayees.length 替代 store 快照差值)
- 新增 diagnostics 诊断页、TodoStrip 首页待办条、SwipeableTransactionCard 左滑操作
- periodNav.ts 报表周期导航(anchor+period 统一状态管理,本地时区运算)
- 测试覆盖 numpadExpression/postingLegs/periodNav/palette/search/transactionGroups/categoryIcons/dateGrid
2026-07-22 13:33:29 +08:00

679 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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视觉语言、§10P1 验收)。
**验收标准:** `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: PASSpalette.test.ts 仍 FAILTask 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**
删除第 3148 行的 `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 的字体导入**
删除第 2739 行两个 `import { useFonts, ... } from '@expo-google-fonts/...'` 块。
- [ ] **Step 2: 删除 useFonts 调用**
删除第 7079 行的 `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` 的运行时引用返回 undefinedRN 回退系统字体,无需在 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
- 阴影:浅色 412px 弥散轻阴影;暗色以 1px 半透边框代替阴影
## 图标
统一 Ionicons`@expo/vector-icons`)。禁止用 emoji 充当图标;
分类图标 = Ionicon + 圆形彩色底CategoryIcon 组件P2 落地)。
## 反模式
- 禁止在组件中写颜色字面量(`#xxxxxx` / `rgba(...)`),必须走 token
- 禁止低对比文字(< 4.5:1
- 禁止瞬间状态变化交互反馈 150300ms
- 禁止绕过 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 管理页模板化 + 清零审计**
每阶段完成后基于实际代码编写下一阶段计划。