Files
photo-live/apps/mobile/lib/app/theme.dart
T

270 lines
10 KiB
Dart
Raw Normal View History

/// UI-01 主题(视觉规范 §3.1 精确落地)。
///
/// 浅色基准:primary #1A73E8 / surface #FFFFFF / 页面背景 #F6F8FB /
/// 主文字 #172033 / 次文字 #5E6878 / 分隔线 #E5EAF1;等待色 #996300 承载
/// ColorScheme.secondary、错误 #B42318 承载 error。成功色**不进**
/// ColorScheme(Material 无该语义角色),以独立令牌提供(见 ui/tokens.dart,
/// 成功/等待/错误均另有深色提亮变体)。
///
/// 深色:语义对应色(深表面 + 浅文字 + 提亮的主色/等待色/错误色);图片
/// 与内容不做反色处理,沿用跟随系统的深浅色切换。容器色(secondary/
/// primaryContainer 等)由种子色派生,与 #1A73E8 同系。
///
/// 字阶(fontSize 走 TextScaler,随系统字体缩放;UI-03 §3 校准):24
/// 页面标题 / 17 分组标题 / 16-14 正文 / 13-12 辅助。组件:主按钮高 48
/// (区间 48-52)、输入框与按钮圆角 12、卡片圆角 16、Dialog 圆角 16、
/// BottomSheet 顶部圆角 16。动效时长常量见 ui/tokens.dart。
library;
import 'package:flutter/material.dart';
import '../ui/tokens.dart';
abstract final class AppTheme {
/// Material 种子色(容器色派生基准)。
static const Color seedColor = Color(0xFF1A73E8);
// ── 浅色基准值(方案 §3.1)──────────────────────────────────────────
static const Color _primary = Color(0xFF1A73E8);
static const Color _onPrimary = Color(0xFFFFFFFF);
static const Color _surface = Color(0xFFFFFFFF);
static const Color _onSurface = Color(0xFF172033);
static const Color _background = Color(0xFFF6F8FB);
static const Color _textSecondary = Color(0xFF69778E); // UI06: app-preview --muted
static const Color _divider = Color(0xFFE5EAF1);
static const Color _waiting = Color(0xFF996300);
static const Color _onWaiting = Color(0xFFFFFFFF);
static const Color _error = Color(0xFFB42318);
// ── 深色对应语义值(同语义、深色表面提亮)──────────────────────────
static const Color _primaryDark = Color(0xFF8AB4F8);
static const Color _onPrimaryDark = Color(0xFF0B2E6F);
static const Color _surfaceDark = Color(0xFF1F2530);
static const Color _onSurfaceDark = Color(0xFFE6EAF2);
static const Color _backgroundDark = Color(0xFF12161D);
static const Color _textSecondaryDark = Color(0xFFA6B0C0);
static const Color _dividerDark = Color(0xFF333B47);
static const Color _waitingDark = Color(0xFFFCD34D);
static const Color _onWaitingDark = Color(0xFF402E00);
static const Color _errorDark = Color(0xFFF87171);
static const Color _onErrorDark = Color(0xFF5C1A15);
static ThemeData get light => _build(Brightness.light);
static ThemeData get dark => _build(Brightness.dark);
static ThemeData _build(Brightness brightness) {
final dark = brightness == Brightness.dark;
final colorScheme =
ColorScheme.fromSeed(
seedColor: seedColor,
brightness: brightness,
).copyWith(
primary: dark ? _primaryDark : _primary,
onPrimary: dark ? _onPrimaryDark : _onPrimary,
secondary: dark ? _waitingDark : _waiting,
onSecondary: dark ? _onWaitingDark : _onWaiting,
error: dark ? _errorDark : _error,
onError: dark ? _onErrorDark : Colors.white,
surface: dark ? _surfaceDark : _surface,
onSurface: dark ? _onSurfaceDark : _onSurface,
surfaceContainerHighest: dark ? _dividerDark : _divider,
onSurfaceVariant: dark ? _textSecondaryDark : _textSecondary,
);
final textTheme = _textTheme(colorScheme);
final buttonBorder = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(kRadiusInput),
);
return ThemeData(
brightness: brightness,
colorScheme: colorScheme,
scaffoldBackgroundColor: dark ? _backgroundDark : _background,
textTheme: textTheme,
// AppBar:干净(零海拔 + 页面背景同色),标题用 titleLarge。
appBarTheme: AppBarTheme(
backgroundColor: dark ? _backgroundDark : _background,
surfaceTintColor: Colors.transparent,
foregroundColor: colorScheme.onSurface,
titleTextStyle: textTheme.titleLarge,
elevation: 0,
scrolledUnderElevation: 1,
centerTitle: false,
),
// 按钮:主/次操作统一高 48(规格 48-52)、圆角 12。
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size(64, 48),
shape: buttonBorder,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(64, 48),
shape: buttonBorder,
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size(48, 48),
shape: buttonBorder,
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
minimumSize: const Size(48, 48),
shape: buttonBorder,
),
),
// 输入框:圆角 12,轻分隔线边框,聚焦主色 2px。
inputDecorationTheme: InputDecorationTheme(
border: _inputBorder(colorScheme, colorScheme.outline),
enabledBorder: _inputBorder(
colorScheme,
dark ? _dividerDark : _divider,
),
focusedBorder: _inputBorder(colorScheme, colorScheme.primary, width: 2),
errorBorder: _inputBorder(colorScheme, colorScheme.error),
focusedErrorBorder: _inputBorder(
colorScheme,
colorScheme.error,
width: 2,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: kSpace16,
vertical: kSpace12,
),
),
// 卡片:白色表面、圆角 16、细边框无阴影(app-preview .card 形态)。
cardTheme: CardThemeData(
color: colorScheme.surface,
surfaceTintColor: Colors.transparent,
elevation: 0,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(kRadiusCard),
side: BorderSide(color: dark ? _dividerDark : kColorCardBorder),
),
),
// Dialog 圆角 16;BottomSheet 仅顶部圆角 16。
dialogTheme: DialogThemeData(
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(kRadiusCard),
),
),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(kRadiusCard),
),
),
),
// Snackbar:浮动 + inverseSurface(自动随深浅色),动作色 inversePrimary。
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
backgroundColor: colorScheme.inverseSurface,
contentTextStyle: textTheme.bodyMedium?.copyWith(
color: colorScheme.onInverseSurface,
),
actionTextColor: colorScheme.inversePrimary,
),
// 底部导航(app-preview .nav):白底 + 顶部细线 + **无胶囊指示器**;
// 选中 primary 蓝图标/文字,未选次文字色。
navigationBarTheme: NavigationBarThemeData(
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
indicatorColor: Colors.transparent,
height: 68,
iconTheme: WidgetStateProperty.resolveWith(
(states) => IconThemeData(
size: 24,
color: states.contains(WidgetState.selected)
? colorScheme.primary
: (dark ? _textSecondaryDark : kColorTextSecondary),
),
),
labelTextStyle: WidgetStateProperty.resolveWith(
(states) => textTheme.labelMedium!.copyWith(
color: states.contains(WidgetState.selected)
? colorScheme.primary
: (dark ? _textSecondaryDark : kColorTextSecondary),
fontWeight: FontWeight.w500,
),
),
),
// 轻分隔线(不用粗边框)。
dividerTheme: DividerThemeData(
color: dark ? _dividerDark : _divider,
thickness: 1,
space: 1,
),
);
}
static OutlineInputBorder _inputBorder(
ColorScheme scheme,
Color color, {
double width = 1,
}) => OutlineInputBorder(
borderRadius: BorderRadius.circular(kRadiusField),
borderSide: BorderSide(color: color, width: width),
);
/// 字阶(UI-03 §3):24 页面标题 / 17 分组 / 16-14 正文 / 13-12 辅助。
///
/// 只声明 fontSize/字重/颜色,字体族与缩放交给默认排版 + TextScaler
/// (fontSize 随系统字体大小缩放)。
static TextTheme _textTheme(ColorScheme scheme) {
final onSurface = scheme.onSurface;
final onSecondaryText = scheme.onSurfaceVariant;
return TextTheme(
titleLarge: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: onSurface,
),
titleMedium: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: onSurface,
),
titleSmall: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: onSurface,
),
bodyLarge: TextStyle(fontSize: 16, color: onSurface),
bodyMedium: TextStyle(fontSize: 14, color: onSurface),
bodySmall: TextStyle(fontSize: 13, color: onSecondaryText),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: onSurface,
),
labelMedium: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: onSurface,
),
labelSmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: onSecondaryText,
),
);
}
}