b04cef3cc4
- 新增 design/codegen/gen_flutter_tokens.mjs: 从 colors_and_type.css codegen 出 client/lib/pangolin_tokens.gen.dart 覆盖 PangolinColors/Spacing/Radius/Motion/Shadow(纯数值层) - 重构 client/lib/pangolin_theme.dart: 删除手抄数值,import+export pangolin_tokens.gen.dart 保留实现层 PangolinScheme/PangolinText/PangolinTheme/PangolinContext 19 个引用方零改动,对外 API 完全不变 - 新增 web/usercenter/scripts/build-tokens.mjs: 仿 website 样板,prebuild/predev 自动从 css 生成 public/colors_and_type.css 去除 usercenter 手抄副本的漂移风险 - 更新 CLAUDE.md:补 token codegen 使用说明与单源模型 - 更新 design/SKILL.md:移除已删 flutter/ 引用,补 codegen 入口 验证:改 clay-500 → 三端 codegen 同步变化;flutter analyze 无新增 error。 注:design/flutter/ fork 手动删除(git rm 需用户执行) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
250 lines
10 KiB
Dart
250 lines
10 KiB
Dart
// pangolin_theme.dart
|
|
// 穿山甲 · Pangolin — Flutter theme implementation
|
|
//
|
|
// Token 数值层(颜色/间距/圆角/动效/阴影)由 codegen 自动维护,见 pangolin_tokens.gen.dart。
|
|
// 本文件只保留 Flutter 实现层:
|
|
// · PangolinScheme — ThemeExtension(语义色映射 + light/dark 实例)
|
|
// · PangolinContext — BuildContext 扩展
|
|
// · PangolinFonts — 字族标签常量
|
|
// · PangolinText — google_fonts 运行时字型
|
|
// · PangolinTheme — ThemeData 组装
|
|
//
|
|
// Usage:
|
|
// MaterialApp(
|
|
// theme: PangolinTheme.light,
|
|
// darkTheme: PangolinTheme.dark,
|
|
// themeMode: ThemeMode.system,
|
|
// );
|
|
// // tokens: context.pangolin.accent, PangolinSpacing.x4, PangolinRadius.lg ...
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
// token 数值层:import 供本文件使用,export 让调用方 import pangolin_theme.dart 时也能访问。
|
|
import 'pangolin_tokens.gen.dart';
|
|
export 'pangolin_tokens.gen.dart';
|
|
|
|
/// ── Semantic tokens, resolved per brightness ───────────────────────
|
|
@immutable
|
|
class PangolinScheme extends ThemeExtension<PangolinScheme> {
|
|
const PangolinScheme({
|
|
required this.bg,
|
|
required this.bgSubtle,
|
|
required this.surface,
|
|
required this.surface2,
|
|
required this.overlay,
|
|
required this.fg1,
|
|
required this.fg2,
|
|
required this.fg3,
|
|
required this.fgOnAccent,
|
|
required this.accent,
|
|
required this.accentHover,
|
|
required this.accentPress,
|
|
required this.accentSubtle,
|
|
required this.accentBorder,
|
|
required this.border,
|
|
required this.borderStrong,
|
|
required this.ring,
|
|
required this.success,
|
|
required this.successSubtle,
|
|
required this.warning,
|
|
required this.warningSubtle,
|
|
required this.danger,
|
|
required this.dangerSubtle,
|
|
});
|
|
|
|
final Color bg, bgSubtle, surface, surface2, overlay;
|
|
final Color fg1, fg2, fg3, fgOnAccent;
|
|
final Color accent, accentHover, accentPress, accentSubtle, accentBorder;
|
|
final Color border, borderStrong, ring;
|
|
final Color success, successSubtle, warning, warningSubtle, danger, dangerSubtle;
|
|
|
|
static const light = PangolinScheme(
|
|
bg: PangolinColors.sand50,
|
|
bgSubtle: PangolinColors.sand100,
|
|
surface: PangolinColors.white,
|
|
surface2: PangolinColors.sand50,
|
|
overlay: Color(0x731F1C18),
|
|
fg1: PangolinColors.sand900,
|
|
fg2: PangolinColors.sand600,
|
|
fg3: PangolinColors.sand500,
|
|
fgOnAccent: PangolinColors.white,
|
|
accent: PangolinColors.clay500,
|
|
accentHover: PangolinColors.clay600,
|
|
accentPress: PangolinColors.clay700,
|
|
accentSubtle: PangolinColors.clay50,
|
|
accentBorder: PangolinColors.clay200,
|
|
border: PangolinColors.sand200,
|
|
borderStrong: PangolinColors.sand300,
|
|
ring: Color(0x59B96A3D),
|
|
success: PangolinColors.green500,
|
|
successSubtle: Color(0xFFE9F0E6),
|
|
warning: PangolinColors.amber500,
|
|
warningSubtle: Color(0xFFF8EED6),
|
|
danger: PangolinColors.red500,
|
|
dangerSubtle: Color(0xFFF6E1DA),
|
|
);
|
|
|
|
static const dark = PangolinScheme(
|
|
bg: PangolinColors.sand950,
|
|
bgSubtle: PangolinColors.sand900,
|
|
surface: Color(0xFF221E19),
|
|
surface2: Color(0xFF2A251F),
|
|
overlay: Color(0x99000000),
|
|
fg1: Color(0xFFF4EFE8),
|
|
fg2: Color(0xFFB6AC9C),
|
|
fg3: Color(0xFF897F6F),
|
|
fgOnAccent: PangolinColors.sand900,
|
|
accent: PangolinColors.clay400,
|
|
accentHover: PangolinColors.clay300,
|
|
accentPress: PangolinColors.clay500,
|
|
accentSubtle: Color(0x24CC8B5C),
|
|
accentBorder: Color(0x4DCC8B5C),
|
|
border: Color(0x1AF2EEE7),
|
|
borderStrong: Color(0x2EF2EEE7),
|
|
ring: Color(0x73CC8B5C),
|
|
success: PangolinColors.green400,
|
|
successSubtle: Color(0x297FB07A),
|
|
warning: PangolinColors.amber400,
|
|
warningSubtle: Color(0x29E2B05A),
|
|
danger: PangolinColors.red400,
|
|
dangerSubtle: Color(0x29D4715A),
|
|
);
|
|
|
|
@override
|
|
PangolinScheme copyWith({Color? bg, Color? fg1, Color? accent}) => PangolinScheme(
|
|
bg: bg ?? this.bg, bgSubtle: bgSubtle, surface: surface, surface2: surface2,
|
|
overlay: overlay, fg1: fg1 ?? this.fg1, fg2: fg2, fg3: fg3, fgOnAccent: fgOnAccent,
|
|
accent: accent ?? this.accent, accentHover: accentHover, accentPress: accentPress,
|
|
accentSubtle: accentSubtle, accentBorder: accentBorder, border: border,
|
|
borderStrong: borderStrong, ring: ring, success: success, successSubtle: successSubtle,
|
|
warning: warning, warningSubtle: warningSubtle, danger: danger, dangerSubtle: dangerSubtle,
|
|
);
|
|
|
|
@override
|
|
PangolinScheme lerp(ThemeExtension<PangolinScheme>? other, double t) {
|
|
if (other is! PangolinScheme) return this;
|
|
Color c(Color a, Color b) => Color.lerp(a, b, t)!;
|
|
return PangolinScheme(
|
|
bg: c(bg, other.bg), bgSubtle: c(bgSubtle, other.bgSubtle), surface: c(surface, other.surface),
|
|
surface2: c(surface2, other.surface2), overlay: c(overlay, other.overlay),
|
|
fg1: c(fg1, other.fg1), fg2: c(fg2, other.fg2), fg3: c(fg3, other.fg3), fgOnAccent: c(fgOnAccent, other.fgOnAccent),
|
|
accent: c(accent, other.accent), accentHover: c(accentHover, other.accentHover), accentPress: c(accentPress, other.accentPress),
|
|
accentSubtle: c(accentSubtle, other.accentSubtle), accentBorder: c(accentBorder, other.accentBorder),
|
|
border: c(border, other.border), borderStrong: c(borderStrong, other.borderStrong), ring: c(ring, other.ring),
|
|
success: c(success, other.success), successSubtle: c(successSubtle, other.successSubtle),
|
|
warning: c(warning, other.warning), warningSubtle: c(warningSubtle, other.warningSubtle),
|
|
danger: c(danger, other.danger), dangerSubtle: c(dangerSubtle, other.dangerSubtle),
|
|
);
|
|
}
|
|
}
|
|
|
|
extension PangolinContext on BuildContext {
|
|
PangolinScheme get pangolin => Theme.of(this).extension<PangolinScheme>()!;
|
|
}
|
|
|
|
/// ── Font families (via google_fonts at runtime) ─────────────────────
|
|
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';
|
|
}
|
|
|
|
/// ── Type scale ──────────────────────────────────────────────────────
|
|
/// Uses google_fonts for runtime font loading (no local ttf required).
|
|
/// All PangolinText members are static getters (not const) so they can
|
|
/// reference google_fonts, which registers fonts dynamically.
|
|
class PangolinText {
|
|
PangolinText._();
|
|
|
|
static TextStyle get displayXl => GoogleFonts.sora(
|
|
fontSize: 48, fontWeight: FontWeight.w700, height: 1.15, letterSpacing: -0.96);
|
|
static TextStyle get display => GoogleFonts.sora(
|
|
fontSize: 36, fontWeight: FontWeight.w700, height: 1.15, letterSpacing: -0.72);
|
|
static TextStyle get h1 => GoogleFonts.sora(
|
|
fontSize: 30, fontWeight: FontWeight.w600, height: 1.3, letterSpacing: -0.3);
|
|
static TextStyle get h2 => GoogleFonts.sora(
|
|
fontSize: 24, fontWeight: FontWeight.w600, height: 1.3, letterSpacing: -0.24);
|
|
static TextStyle get h3 => GoogleFonts.manrope(
|
|
fontSize: 20, fontWeight: FontWeight.w600, height: 1.3);
|
|
static TextStyle get bodyLg => GoogleFonts.manrope(
|
|
fontSize: 18, fontWeight: FontWeight.w400, height: 1.65);
|
|
static TextStyle get body => GoogleFonts.manrope(
|
|
fontSize: 16, fontWeight: FontWeight.w400, height: 1.5);
|
|
static TextStyle get sm => GoogleFonts.manrope(
|
|
fontSize: 14, fontWeight: FontWeight.w400, height: 1.5);
|
|
static TextStyle get caption => GoogleFonts.manrope(
|
|
fontSize: 12, fontWeight: FontWeight.w500, height: 1.5);
|
|
static TextStyle get overline => GoogleFonts.manrope(
|
|
fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 0.96);
|
|
static TextStyle get mono => GoogleFonts.jetBrainsMono(
|
|
fontSize: 14, fontWeight: FontWeight.w400,
|
|
fontFeatures: const [FontFeature.tabularFigures()]);
|
|
}
|
|
|
|
/// ── Ready-to-use ThemeData ──────────────────────────────────────────
|
|
class PangolinTheme {
|
|
PangolinTheme._();
|
|
|
|
static ThemeData get light => _build(PangolinScheme.light, Brightness.light);
|
|
static ThemeData get dark => _build(PangolinScheme.dark, Brightness.dark);
|
|
|
|
static ThemeData _build(PangolinScheme s, Brightness b) {
|
|
final scheme = ColorScheme(
|
|
brightness: b,
|
|
primary: s.accent,
|
|
onPrimary: s.fgOnAccent,
|
|
secondary: s.accent,
|
|
onSecondary: s.fgOnAccent,
|
|
error: s.danger,
|
|
onError: PangolinColors.white,
|
|
surface: s.surface,
|
|
onSurface: s.fg1,
|
|
);
|
|
return ThemeData(
|
|
useMaterial3: true,
|
|
brightness: b,
|
|
colorScheme: scheme,
|
|
scaffoldBackgroundColor: s.bg,
|
|
// Use google_fonts Manrope as app-wide base font
|
|
textTheme: GoogleFonts.manropeTextTheme(
|
|
ThemeData(brightness: b).textTheme,
|
|
),
|
|
extensions: [s],
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: s.accent,
|
|
foregroundColor: s.fgOnAccent,
|
|
textStyle: GoogleFonts.manrope(fontWeight: FontWeight.w600, fontSize: 15),
|
|
shape: const StadiumBorder(),
|
|
padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14),
|
|
),
|
|
),
|
|
cardTheme: CardThemeData(
|
|
color: s.surface,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
side: BorderSide(color: s.border),
|
|
borderRadius: BorderRadius.circular(PangolinRadius.lg),
|
|
),
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: s.bg,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(PangolinRadius.md),
|
|
borderSide: BorderSide(color: s.borderStrong, width: 1.5),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(PangolinRadius.md),
|
|
borderSide: BorderSide(color: s.accent, width: 1.5),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|