feat(client): 字体可注入 + 启用 golden 视觉回归闸

- pangolin_theme: PangolinFonts 加 useBundled 开关 + sora/manrope/jetBrainsMono/
  manropeTextTheme 封装(生产走 google_fonts / 测试走 bundled family);
  PangolinText 与 _build 改调封装,移除散落的 GoogleFonts 直接调用
- test/flutter_test_config.dart: 全测试前置,FontLoader 注册 test/fonts 真实
  Sora/Manrope/JetBrainsMono + Lucide 图标字体,置 useBundled=true
- 启用 test/golden(去 @Skip):连接键三态+推荐卡+额度卡×明暗 12 基准图
- tool/visual-diff.md: 标注 golden 严格闸已落地

测试: flutter test 76 通过(原 64+1skip → 76 无 skip), analyze 0 error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-16 11:36:50 +08:00
parent 281a6261fb
commit 1340b477a7
16 changed files with 96 additions and 36 deletions
+47 -17
View File
@@ -143,15 +143,45 @@ extension PangolinContext on BuildContext {
PangolinScheme get pangolin => Theme.of(this).extension<PangolinScheme>()!;
}
/// ── Font families (via google_fonts at runtime) ─────────────────────
/// ── Font families ───────────────────────────────────────────────────
/// 生产用 google_fonts 运行时加载;测试态设 [useBundled]=true,改用本地
/// FontLoader 注册的同名 family(避免 google_fonts 在无网/禁拉取时抛异常,
/// 让 golden 截图回归可跑)。封装函数是 PangolinText/_build 的唯一字体入口。
class PangolinFonts {
PangolinFonts._();
// String constants used as display labels only.
// Actual font loading is done via google_fonts in PangolinText getters.
static const display = 'Sora';
static const sans = 'Manrope';
static const cjk = 'Noto Sans SC';
static const mono = 'JetBrains Mono';
/// 测试钩子:true 时用 bundled familytest/fonts/ 经 FontLoader 注册),不走 google_fonts。
static bool useBundled = false;
static TextStyle sora({double? fontSize, FontWeight? fontWeight, double? height, double? letterSpacing}) =>
useBundled
? TextStyle(fontFamily: display, fontSize: fontSize, fontWeight: fontWeight, height: height, letterSpacing: letterSpacing)
: GoogleFonts.sora(fontSize: fontSize, fontWeight: fontWeight, height: height, letterSpacing: letterSpacing);
static TextStyle manrope(
{double? fontSize, FontWeight? fontWeight, double? height, double? letterSpacing, List<FontFeature>? fontFeatures}) =>
useBundled
? TextStyle(
fontFamily: sans,
fontSize: fontSize,
fontWeight: fontWeight,
height: height,
letterSpacing: letterSpacing,
fontFeatures: fontFeatures)
: GoogleFonts.manrope(
fontSize: fontSize, fontWeight: fontWeight, height: height, letterSpacing: letterSpacing, fontFeatures: fontFeatures);
static TextStyle jetBrainsMono({double? fontSize, FontWeight? fontWeight, List<FontFeature>? fontFeatures}) =>
useBundled
? TextStyle(fontFamily: mono, fontSize: fontSize, fontWeight: fontWeight, fontFeatures: fontFeatures)
: GoogleFonts.jetBrainsMono(fontSize: fontSize, fontWeight: fontWeight, fontFeatures: fontFeatures);
static TextTheme manropeTextTheme(TextTheme base) =>
useBundled ? base.apply(fontFamily: sans) : GoogleFonts.manropeTextTheme(base);
}
/// ── Type scale ──────────────────────────────────────────────────────
@@ -161,27 +191,27 @@ class PangolinFonts {
class PangolinText {
PangolinText._();
static TextStyle get displayXl => GoogleFonts.sora(
static TextStyle get displayXl => PangolinFonts.sora(
fontSize: 48, fontWeight: FontWeight.w700, height: 1.15, letterSpacing: -0.96);
static TextStyle get display => GoogleFonts.sora(
static TextStyle get display => PangolinFonts.sora(
fontSize: 36, fontWeight: FontWeight.w700, height: 1.15, letterSpacing: -0.72);
static TextStyle get h1 => GoogleFonts.sora(
static TextStyle get h1 => PangolinFonts.sora(
fontSize: 30, fontWeight: FontWeight.w600, height: 1.3, letterSpacing: -0.3);
static TextStyle get h2 => GoogleFonts.sora(
static TextStyle get h2 => PangolinFonts.sora(
fontSize: 24, fontWeight: FontWeight.w600, height: 1.3, letterSpacing: -0.24);
static TextStyle get h3 => GoogleFonts.manrope(
static TextStyle get h3 => PangolinFonts.manrope(
fontSize: 20, fontWeight: FontWeight.w600, height: 1.3);
static TextStyle get bodyLg => GoogleFonts.manrope(
static TextStyle get bodyLg => PangolinFonts.manrope(
fontSize: 18, fontWeight: FontWeight.w400, height: 1.65);
static TextStyle get body => GoogleFonts.manrope(
static TextStyle get body => PangolinFonts.manrope(
fontSize: 16, fontWeight: FontWeight.w400, height: 1.5);
static TextStyle get sm => GoogleFonts.manrope(
static TextStyle get sm => PangolinFonts.manrope(
fontSize: 14, fontWeight: FontWeight.w400, height: 1.5);
static TextStyle get caption => GoogleFonts.manrope(
static TextStyle get caption => PangolinFonts.manrope(
fontSize: 12, fontWeight: FontWeight.w500, height: 1.5);
static TextStyle get overline => GoogleFonts.manrope(
static TextStyle get overline => PangolinFonts.manrope(
fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 0.96);
static TextStyle get mono => GoogleFonts.jetBrainsMono(
static TextStyle get mono => PangolinFonts.jetBrainsMono(
fontSize: 14, fontWeight: FontWeight.w400,
fontFeatures: const [FontFeature.tabularFigures()]);
}
@@ -210,8 +240,8 @@ class PangolinTheme {
brightness: b,
colorScheme: scheme,
scaffoldBackgroundColor: s.bg,
// Use google_fonts Manrope as app-wide base font
textTheme: GoogleFonts.manropeTextTheme(
// Manrope app-wide base font(生产 google_fonts / 测试 bundled
textTheme: PangolinFonts.manropeTextTheme(
ThemeData(brightness: b).textTheme,
),
extensions: [s],
@@ -219,7 +249,7 @@ class PangolinTheme {
style: FilledButton.styleFrom(
backgroundColor: s.accent,
foregroundColor: s.fgOnAccent,
textStyle: GoogleFonts.manrope(fontWeight: FontWeight.w600, fontSize: 15),
textStyle: PangolinFonts.manrope(fontWeight: FontWeight.w600, fontSize: 15),
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14),
),