import 'package:flutter/material.dart'; /// 语义色 token(ADR-005:以 AI宠物_iOS_UI设计稿.html 珊瑚橙暖色系为品牌正典, /// 详见 iteration-1-reports/04-ui-login-design-spec.md §1.1)。 abstract final class AppColors { /// 品牌珊瑚橙:图标、装饰、渐变起点;不用于承载文字的实心填充(对比度不足)。 static const primary = Color(0xFFFF6F4C); /// 加深珊瑚:实心按钮填充、可点击文字链接(白字约 4.5:1,WCAG AA)。 static const primaryStrong = Color(0xFFD6431A); /// 浅色底上的强调文字、按钮反白替代。 static const primaryDark = Color(0xFF7A2E12); /// amber:渐变终点、徽章、会员/促销 CTA。 static const accent = Color(0xFFFFB648); /// amber 底上的文字。 static const accentDark = Color(0xFF7A4B0A); /// 页面背景(奶油底)。 static const canvas = Color(0xFFFFF7ED); /// 卡片、输入框背景。 static const surface = Color(0xFFFFFFFF); /// peach:图标底、占位块、选中指示。 static const surfaceTint = Color(0xFFFFE8D6); /// 主文字。 static const ink = Color(0xFF3E2A1F); /// 承载信息的次级文字(日期、元数据、字段名;正典 feed-caption 色, /// 白底 6.59:1 达 AA)。muted 仅限占位符、禁用态、纯装饰(05 号规范 DEBT-2)。 static const inkSoft = Color(0xFF6B5A4A); /// 次级文字、占位符。 static const muted = Color(0xFF9C8977); /// 描边、分隔线。 static const border = Color(0xFFF0DCC8); /// sage:成功提示的图标/描边。 static const success = Color(0xFF7FA88A); /// 成功提示底上的文字。 static const successInk = Color(0xFF3F5744); /// 成功提示底色。 static const successSurface = Color(0xFFE8F0E8); static const warning = Color(0xFFF59E0B); /// 校验错误文字、错误描边、失败提示(白底上约 5.4:1,AA 达标)。 static const error = Color(0xFFD0342C); /// error 淡底(8% tint)上的文字(error 本身在自家淡底仅 4.44:1 贴线不过; /// 与 primary→primaryStrong 同构加深,05 号规范 §2/D8)。 static const errorDark = Color(0xFFB02C25); /// 品牌渐变:hero 区、头像环、装饰性大面积(不承载正文文字)。 static const brandGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [primary, accent], ); } /// 圆角 token(设计规范 §1.3)。 abstract final class AppRadius { /// 小按钮、内嵌 CTA、错误横幅。 static const sm = 12.0; /// 主按钮、SnackBar、菜单组。 static const md = 16.0; /// 输入框、feed 卡。 static const lg = 18.0; /// 大卡片。 static const xl = 24.0; /// 芯片、徽章。 static const pill = 999.0; } ThemeData buildAppTheme() { final scheme = ColorScheme.fromSeed( seedColor: AppColors.primary, brightness: Brightness.light, surface: AppColors.surface, ).copyWith(error: AppColors.error, onError: Colors.white); return ThemeData( useMaterial3: true, colorScheme: scheme, scaffoldBackgroundColor: AppColors.canvas, fontFamilyFallback: const [ 'Noto Sans CJK SC', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', 'sans-serif', ], textTheme: const TextTheme( headlineSmall: TextStyle( color: AppColors.ink, fontSize: 22, fontWeight: FontWeight.w800, ), titleLarge: TextStyle( color: AppColors.ink, fontSize: 18, fontWeight: FontWeight.w800, ), titleMedium: TextStyle( color: AppColors.ink, fontSize: 15, fontWeight: FontWeight.w700, ), bodyMedium: TextStyle(color: AppColors.ink, fontSize: 14, height: 1.5), bodySmall: TextStyle(color: AppColors.muted, fontSize: 12, height: 1.4), ), cardTheme: const CardThemeData( color: AppColors.surface, elevation: 0, margin: EdgeInsets.zero, shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(AppRadius.xl)), side: BorderSide(color: AppColors.border), ), ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( backgroundColor: AppColors.primaryStrong, foregroundColor: Colors.white, disabledBackgroundColor: AppColors.ink.withAlpha(31), disabledForegroundColor: AppColors.ink.withAlpha(97), minimumSize: const Size(64, 52), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(AppRadius.md)), ), textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700), ), ), inputDecorationTheme: const InputDecorationTheme( filled: true, fillColor: AppColors.surface, contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(AppRadius.lg)), borderSide: BorderSide(color: AppColors.border), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(AppRadius.lg)), borderSide: BorderSide(color: AppColors.border), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(AppRadius.lg)), borderSide: BorderSide(color: AppColors.primary, width: 1.5), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(AppRadius.lg)), borderSide: BorderSide(color: AppColors.error, width: 1.5), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(AppRadius.lg)), borderSide: BorderSide(color: AppColors.error, width: 1.5), ), errorStyle: TextStyle(color: AppColors.error, fontSize: 12), helperStyle: TextStyle(color: AppColors.muted, fontSize: 12), ), snackBarTheme: const SnackBarThemeData( behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(AppRadius.md)), ), ), datePickerTheme: _datePickerTheme, navigationBarTheme: NavigationBarThemeData( backgroundColor: AppColors.surface, indicatorColor: AppColors.surfaceTint, height: 72, labelTextStyle: WidgetStateProperty.resolveWith((states) { return TextStyle( fontSize: 11, fontWeight: states.contains(WidgetState.selected) ? FontWeight.w700 : FontWeight.w500, color: states.contains(WidgetState.selected) ? AppColors.primaryStrong : AppColors.muted, ); }), ), ); } /// 日期选择器主题(M3.5-01)。 /// /// 根因备忘:此前未定制,`showDatePicker` 完全走 `ColorScheme.fromSeed` /// 由珊瑚橙 `#FF6F4C` 派生出的 M3 调和色(选中日为暗红棕实底), /// 与全 app 品牌色脱节。这里只复用 05 号规范(iteration-2/05、iteration-3/05) /// 已审计过的色对,不新造色值: /// /// | 位置 | 色对 | 对比度 | 来源 | /// | --- | --- | --- | --- | /// | 头部(帮助文字 + 标题 + 模式切换图标) | `surfaceTint` 底 + `primaryDark` | 7.98:1 | 选中 chip 同款(iteration-2/05 §3 D7) | /// | 选中日 / 选中年 | `primaryStrong` 底 + 白字 | 4.49:1 | FAB / 头像徽标同款(同 §2) | /// | 今日(未选中)描边与文字 | 白底 + `primaryStrong` | 4.49:1 | 同上 | /// | 未选中日 / 年 | 白底 + `ink` | ≥12:1 | 正文主色 | /// | 星期表头 | 白底 + `inkSoft` | 6.59:1 | 承载信息的次级文字(§3 DEBT-2) | /// | 越界不可选日 | 白底 + `muted` | 3.36:1 | 禁用态,DEBT-2 允许的 `muted` 用途 | /// | 确定 / 取消按钮 | 白底 + `primaryStrong` 文字 | 4.49:1 | 可点击文字链接(§1.1 token 注释) | final DatePickerThemeData _datePickerTheme = DatePickerThemeData( backgroundColor: AppColors.surface, elevation: 0, surfaceTintColor: Colors.transparent, shadowColor: Colors.transparent, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(AppRadius.xl)), side: BorderSide(color: AppColors.border), ), dividerColor: AppColors.border, headerBackgroundColor: AppColors.surfaceTint, headerForegroundColor: AppColors.primaryDark, headerHelpStyle: const TextStyle( color: AppColors.primaryDark, fontSize: 12, fontWeight: FontWeight.w600, ), headerHeadlineStyle: const TextStyle( color: AppColors.primaryDark, // 22(默认 32 偏大):中文 `formatMediumDate` 是「9月10日周四」5~6 字, // 横屏侧栏头部宽度下 26 起就会折行,22 一行放得下。 fontSize: 22, fontWeight: FontWeight.w800, ), // 年份下拉 / 上下月箭头一行:与头部同族深色,白底 8.74:1。 subHeaderForegroundColor: AppColors.primaryDark, weekdayStyle: const TextStyle( color: AppColors.inkSoft, fontSize: 12, fontWeight: FontWeight.w600, ), dayStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), dayForegroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.disabled)) return AppColors.muted; if (states.contains(WidgetState.selected)) return Colors.white; return AppColors.ink; }), dayBackgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) return AppColors.primaryStrong; return null; }), dayOverlayColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) return Colors.white24; return AppColors.primary.withAlpha(31); }), todayForegroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.disabled)) return AppColors.muted; if (states.contains(WidgetState.selected)) return Colors.white; return AppColors.primaryStrong; }), todayBackgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) return AppColors.primaryStrong; return null; }), // 今日始终带描边(选中态由实底承载,未选中态靠 1.5px 描边定位): // `primaryStrong` 白底 4.49:1,超过非文字元素 3:1 门槛。 todayBorder: const BorderSide(color: AppColors.primaryStrong, width: 1.5), yearStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), yearForegroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.disabled)) return AppColors.muted; if (states.contains(WidgetState.selected)) return Colors.white; return AppColors.ink; }), yearBackgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) return AppColors.primaryStrong; return null; }), yearOverlayColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) return Colors.white24; return AppColors.primary.withAlpha(31); }), cancelButtonStyle: TextButton.styleFrom( foregroundColor: AppColors.inkSoft, minimumSize: const Size(64, 44), textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), ), confirmButtonStyle: TextButton.styleFrom( foregroundColor: AppColors.primaryStrong, minimumSize: const Size(64, 44), textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700), ), );