feat(client): 新增 AppTokens ThemeExtension 类型

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-24 21:30:31 +08:00
parent 021c4f17cc
commit 5b9ac47362
2 changed files with 126 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
@immutable
class AppTokens extends ThemeExtension<AppTokens> {
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<AppTokens>? 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,
);
}
}
+38
View File
@@ -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);
});
}