DriftLedger/docs/design/ui-redesign-p1-plan.md
fengmengqi bf04400852 feat: OCR 模型按需下载 + 三层独立开关 + 日志持久化 + 原生浮层主题同步 + 文档重构
OCR 模型按需下载(P8 瘦身):
- 移除 plugins/ppocr/assets/ 内置模型(det 9.8MB + rec 21MB + dict),APK 减包 ~31MB
- 新增 modelDownloader.ts:优先从 HuggingFace/CDN 下载,兜底从 APK assets 拷贝
- OcrModule.kt 新增 setModelDir,支持从 filesystem 加载模型,回退 assets 兼容旧用户
- settingsStore 持久化 ocrModelVersion / ocrModelDir
- 自动化页集成模型状态检查与一键下载 UI

OCR 三层独立控制:
- OcrProcessorConfig 从单一 aiVisionEnabled 拆分为 layer1/2/3 三个独立开关
- 设置页可按层启停(L1 正则规则 / L2 本地 OCR / L3 AI Vision)
- OCR 处理增加耗时与字符数日志

日志系统升级:
- logger.ts 新增 LogFileBackend 抽象,支持磁盘持久化(按日期 app-YYYY-MM-DD.log)
- 新增 logBackend.ts(ExpoLogFileBackend)+ 日志中心页 settings/logs.tsx
- 日志中心:实时缓冲 + 历史文件、4 级过滤、Tag/关键词搜索、JSON 展开、分享导出、7 天过期清理
- _layout.tsx 启动时初始化文件后端 + 日志脱敏(验证码/卡号)

原生浮层 UI 主题同步(P6):
- 新增 floatingUiConfig.ts:JS 侧从 theme tokens + i18n 构建 FloatingUiConfig 推送原生
- 新增 FloatingUiConfigStore.kt:SharedPreferences 存储,三浮层组件读取
- FloatingBillView 重设计:颜色/文案走配置、新增币种 chip、金额校验改 BigDecimal
- FloatingHelper / FloatingTip 同步适配
- _layout.tsx 新增 FloatingUiConfigSyncer,主题/语言切换自动推送

UI 与组件增强:
- FormModal 新增 select/dropdown 控件、行内布局(row/flex)、联动回调 onValuesChange
- 新增 Touchable 通用触摸组件、AccountCreateModal 快速建账弹窗
- 信用卡页展示账单周期/到期还款日/本期应还/剩余可用额度,关联账户改下拉选择
- AI 设置页重做:OpenAI/Gemini/DeepSeek 预设 + 默认 URL/模型
- 引导页新增 Android 权限检查步骤(无障碍/通知/短信/存储/悬浮窗)

去重优化:
- 对手方匹配改为模糊包含(includes),双方均无对手方时判定低置信度重复
- DedupResult 新增 matchedItem 返回匹配对比项

文档重构:
- README.md 重写为入口索引(品牌更新 + 模块概览 + 文档导航表)
- 新增 AGENTS.md(AI 助手贡献指南)、docs/architecture.md(Mermaid 数据流/分层/OCR 级联图)
- 新增 docs/development.md(环境/命令/编码规范/测试/提交规范)、plugins/README.md
- UI 重设计文档(design spec + p1-p8)移入 docs/design/

其他:
- i18n 新增权限/信用卡详情/日志中心/AI 设置等翻译键
- ppocr Config Plugin 修复 import 注入去重;size-optimization 增强
- 新增测试:logger.test.ts、floating-ui-config.test.ts
2026-07-23 19:03:38 +08:00

22 KiB
Raw Permalink Blame History

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 的断言以匹配新令牌

完整替换文件内容为:

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分类色板 + 渠道品牌色)
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
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

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

ThemeTypographyEntryfontFamily 标记废弃(保留字段以兼容现存 theme.typography.X.fontFamily 引用,后续阶段逐页清理):

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

ThemeTypographydisplay

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 缺少 displayradii.xlTask 3 修复)。

  • Step 3: 暂不 commit与 Task 3 一起)

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

Files:

  • Modify: src/theme/presets.ts

  • Step 1: 完整替换 presets.ts

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
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 依赖,可被双方向引用)

/**
 * 分类/渠道颜色统一色板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

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

三处字面量替换:color: '#1677FF'color: CHANNEL_BRAND_COLORS.alipaycolor: '#07C160'color: CHANNEL_BRAND_COLORS.wechatcolor: '#E53935'color: CHANNEL_BRAND_COLORS.bank

  • Step 3: CategoryPicker.tsx 改用 getCategoryColor

删除第 3148 行的 CATEGORY_COLORS 常量(含 fallback),文件顶部加:

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

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

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
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 行:

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

改为:

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

modalCardborderRadius: theme.radii.lg, // 弹窗使用大圆角 lg (16px) 改为:

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

inputtheme.radii.md 现在即 12px无需改动仅更新注释 // 输入框标准 radii 为 md (12px)。)

  • Step 2: 验证

Run: npm run typecheck Expected: PASS。

  • Step 3: Commit
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

# 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
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#2196F3P3 重写 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 管理页模板化 + 清零审计

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