From 5b9ac4736205fc272c2cd7f8e57454ebbefba47b Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Wed, 24 Jun 2026 21:30:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(client):=20=E6=96=B0=E5=A2=9E=20AppTokens?= =?UTF-8?q?=20ThemeExtension=20=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- client/lib/core/theme/app_tokens.dart | 88 ++++++++++++++++++++++++++ client/test/theme/app_tokens_test.dart | 38 +++++++++++ 2 files changed, 126 insertions(+) create mode 100644 client/lib/core/theme/app_tokens.dart create mode 100644 client/test/theme/app_tokens_test.dart diff --git a/client/lib/core/theme/app_tokens.dart b/client/lib/core/theme/app_tokens.dart new file mode 100644 index 0000000..c758bb7 --- /dev/null +++ b/client/lib/core/theme/app_tokens.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +@immutable +class AppTokens extends ThemeExtension { + final Color page, bg, surface, border, borderSubtle, text, heading, title, muted, faint; + final Color primary, primaryDark, onPrimary, brand400, brand50, accent; + final Color success, danger, warn, infoSoft, okSoft, accentSoft, successBg, dangerBg, warnBg; + final Color thBg, rowHover, shadow; + final Color topBg, topFg, topFaint, topBorder, topControl, topMarkBg, topMarkFg; + final Color sideBg, sideFg, sideMuted, sideActiveBg, sideActiveFg, sideBorder, sideHover; + final Color menuUserBg, menuUserFg, menuUserHover; + final bool isDark; + + const AppTokens({ + required this.page, required this.bg, required this.surface, required this.border, + required this.borderSubtle, required this.text, required this.heading, required this.title, + required this.muted, required this.faint, required this.primary, required this.primaryDark, + required this.onPrimary, required this.brand400, required this.brand50, required this.accent, + required this.success, required this.danger, required this.warn, required this.infoSoft, + required this.okSoft, required this.accentSoft, required this.successBg, required this.dangerBg, + required this.warnBg, required this.thBg, required this.rowHover, required this.shadow, + required this.topBg, required this.topFg, required this.topFaint, required this.topBorder, + required this.topControl, required this.topMarkBg, required this.topMarkFg, required this.sideBg, + required this.sideFg, required this.sideMuted, required this.sideActiveBg, required this.sideActiveFg, + required this.sideBorder, required this.sideHover, required this.menuUserBg, required this.menuUserFg, + required this.menuUserHover, required this.isDark, + }); + + @override + AppTokens copyWith({ + Color? page, Color? bg, Color? surface, Color? border, Color? borderSubtle, Color? text, + Color? heading, Color? title, Color? muted, Color? faint, Color? primary, Color? primaryDark, + Color? onPrimary, Color? brand400, Color? brand50, Color? accent, Color? success, Color? danger, + Color? warn, Color? infoSoft, Color? okSoft, Color? accentSoft, Color? successBg, Color? dangerBg, + Color? warnBg, Color? thBg, Color? rowHover, Color? shadow, Color? topBg, Color? topFg, + Color? topFaint, Color? topBorder, Color? topControl, Color? topMarkBg, Color? topMarkFg, + Color? sideBg, Color? sideFg, Color? sideMuted, Color? sideActiveBg, Color? sideActiveFg, + Color? sideBorder, Color? sideHover, Color? menuUserBg, Color? menuUserFg, Color? menuUserHover, + bool? isDark, + }) { + return AppTokens( + page: page ?? this.page, bg: bg ?? this.bg, surface: surface ?? this.surface, + border: border ?? this.border, borderSubtle: borderSubtle ?? this.borderSubtle, + text: text ?? this.text, heading: heading ?? this.heading, title: title ?? this.title, + muted: muted ?? this.muted, faint: faint ?? this.faint, primary: primary ?? this.primary, + primaryDark: primaryDark ?? this.primaryDark, onPrimary: onPrimary ?? this.onPrimary, + brand400: brand400 ?? this.brand400, brand50: brand50 ?? this.brand50, accent: accent ?? this.accent, + success: success ?? this.success, danger: danger ?? this.danger, warn: warn ?? this.warn, + infoSoft: infoSoft ?? this.infoSoft, okSoft: okSoft ?? this.okSoft, accentSoft: accentSoft ?? this.accentSoft, + successBg: successBg ?? this.successBg, dangerBg: dangerBg ?? this.dangerBg, warnBg: warnBg ?? this.warnBg, + thBg: thBg ?? this.thBg, rowHover: rowHover ?? this.rowHover, shadow: shadow ?? this.shadow, + topBg: topBg ?? this.topBg, topFg: topFg ?? this.topFg, topFaint: topFaint ?? this.topFaint, + topBorder: topBorder ?? this.topBorder, topControl: topControl ?? this.topControl, + topMarkBg: topMarkBg ?? this.topMarkBg, topMarkFg: topMarkFg ?? this.topMarkFg, + sideBg: sideBg ?? this.sideBg, sideFg: sideFg ?? this.sideFg, sideMuted: sideMuted ?? this.sideMuted, + sideActiveBg: sideActiveBg ?? this.sideActiveBg, sideActiveFg: sideActiveFg ?? this.sideActiveFg, + sideBorder: sideBorder ?? this.sideBorder, sideHover: sideHover ?? this.sideHover, + menuUserBg: menuUserBg ?? this.menuUserBg, menuUserFg: menuUserFg ?? this.menuUserFg, + menuUserHover: menuUserHover ?? this.menuUserHover, isDark: isDark ?? this.isDark, + ); + } + + @override + AppTokens lerp(ThemeExtension? other, double t) { + if (other is! AppTokens) return this; + Color c(Color a, Color b) => Color.lerp(a, b, t)!; + return AppTokens( + page: c(page, other.page), bg: c(bg, other.bg), surface: c(surface, other.surface), + border: c(border, other.border), borderSubtle: c(borderSubtle, other.borderSubtle), + text: c(text, other.text), heading: c(heading, other.heading), title: c(title, other.title), + muted: c(muted, other.muted), faint: c(faint, other.faint), primary: c(primary, other.primary), + primaryDark: c(primaryDark, other.primaryDark), onPrimary: c(onPrimary, other.onPrimary), + brand400: c(brand400, other.brand400), brand50: c(brand50, other.brand50), accent: c(accent, other.accent), + success: c(success, other.success), danger: c(danger, other.danger), warn: c(warn, other.warn), + infoSoft: c(infoSoft, other.infoSoft), okSoft: c(okSoft, other.okSoft), accentSoft: c(accentSoft, other.accentSoft), + successBg: c(successBg, other.successBg), dangerBg: c(dangerBg, other.dangerBg), warnBg: c(warnBg, other.warnBg), + thBg: c(thBg, other.thBg), rowHover: c(rowHover, other.rowHover), shadow: c(shadow, other.shadow), + topBg: c(topBg, other.topBg), topFg: c(topFg, other.topFg), topFaint: c(topFaint, other.topFaint), + topBorder: c(topBorder, other.topBorder), topControl: c(topControl, other.topControl), + topMarkBg: c(topMarkBg, other.topMarkBg), topMarkFg: c(topMarkFg, other.topMarkFg), + sideBg: c(sideBg, other.sideBg), sideFg: c(sideFg, other.sideFg), sideMuted: c(sideMuted, other.sideMuted), + sideActiveBg: c(sideActiveBg, other.sideActiveBg), sideActiveFg: c(sideActiveFg, other.sideActiveFg), + sideBorder: c(sideBorder, other.sideBorder), sideHover: c(sideHover, other.sideHover), + menuUserBg: c(menuUserBg, other.menuUserBg), menuUserFg: c(menuUserFg, other.menuUserFg), + menuUserHover: c(menuUserHover, other.menuUserHover), isDark: t < 0.5 ? isDark : other.isDark, + ); + } +} diff --git a/client/test/theme/app_tokens_test.dart b/client/test/theme/app_tokens_test.dart new file mode 100644 index 0000000..55d0810 --- /dev/null +++ b/client/test/theme/app_tokens_test.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jiu_client/core/theme/app_tokens.dart'; + +AppTokens _sample() => const AppTokens( + page: Color(0xFF111111), bg: Color(0xFF222222), surface: Color(0xFF333333), + border: Color(0xFF444444), borderSubtle: Color(0xFF555555), text: Color(0xFF666666), + heading: Color(0xFF777777), title: Color(0xFF888888), muted: Color(0xFF999999), + faint: Color(0xFFAAAAAA), primary: Color(0xFF2563AC), primaryDark: Color(0xFF154072), + onPrimary: Color(0xFFFFFFFF), brand400: Color(0xFF4F86C6), brand50: Color(0xFFEEF4FB), + accent: Color(0xFF8B2331), success: Color(0xFF2E8B57), danger: Color(0xFFD14343), + warn: Color(0xFFE08E00), infoSoft: Color(0xFFEEF4FB), okSoft: Color(0xFFE8F5EE), + accentSoft: Color(0xFFFAEEF0), successBg: Color(0xFFE8F5EE), dangerBg: Color(0xFFFDECEC), + warnBg: Color(0xFFFFF4DB), thBg: Color(0xFFF7F9FC), rowHover: Color(0xFFEEF4FB), + shadow: Color(0x33142846), topBg: Color(0xFF2563AC), topFg: Color(0xFFFFFFFF), + topFaint: Color(0xB8FFFFFF), topBorder: Color(0xFF1E5594), topControl: Color(0x29FFFFFF), + topMarkBg: Color(0x38FFFFFF), topMarkFg: Color(0xFFFFFFFF), sideBg: Color(0xFF163A6C), + sideFg: Color(0xFFC4D6ED), sideMuted: Color(0xFF7C9FCB), sideActiveBg: Color(0xFF2C6BB3), + sideActiveFg: Color(0xFFFFFFFF), sideBorder: Color(0xFF1E4A86), sideHover: Color(0xFF1C4680), + menuUserBg: Color(0xFF3C79C0), menuUserFg: Color(0xFFFFFFFF), menuUserHover: Color(0xFF4D88CC), + isDark: false, + ); + +void main() { + test('copyWith overrides one field, keeps others', () { + final t = _sample().copyWith(primary: const Color(0xFF000000)); + expect(t.primary, const Color(0xFF000000)); + expect(t.surface, const Color(0xFF333333)); + expect(t.isDark, false); + }); + + test('lerp at t=0 returns first, t=1 returns second', () { + final a = _sample(); + final b = _sample().copyWith(primary: const Color(0xFFFFFFFF)); + expect(a.lerp(b, 0).primary, a.primary); + expect(a.lerp(b, 1).primary, b.primary); + }); +}