Files
fin-tracker/app/lib/core/theme/app_theme.dart
T
Dmitry bea239ba2d feat(app): токены темы — тёмный сине-индиго фон, единый синий акцент
ColorScheme в AppTheme.light()/.dark() строится из одного seed вместо
двух параллельных наборов чисел для тёмной и светлой темы — акцент
сведён к ChartColors.slot1Blue, чтобы фирменный цвет интерфейса и
основной цвет графиков совпадали. cardTheme/navigationRailTheme/
navigationBarTheme заданы централизованно и каскадом меняют вид
существующих карточек и навигации на всех экранах.
2026-09-19 15:31:16 +03:00

76 lines
3.1 KiB
Dart

import 'package:flutter/material.dart';
/// Design tokens for fin-tracker's visual language (`docs/ai/design-system.md`): dark
/// surfaces tinted blue-indigo rather than neutral grey, flat bordered cards instead of
/// Material's default elevation shadow, and one accent seed shared with
/// `ChartColors.slot1Blue` so UI chrome and the primary data series read as the same color.
///
/// Both brightnesses share this seed and this shape language — only the surface tones
/// differ — so the dark theme is not a special case bolted onto a default light Material
/// theme, and switching `themeModeProvider` never changes which widgets exist.
class AppTheme {
const AppTheme._();
/// Matches `ChartColors.slot1Blue`.
static const seed = Color(0xFF2A78D6);
static ThemeData light() => _build(Brightness.light);
static ThemeData dark() => _build(Brightness.dark);
static ThemeData _build(Brightness brightness) {
final scheme = ColorScheme.fromSeed(seedColor: seed, brightness: brightness);
final outline = scheme.outlineVariant.withValues(alpha: brightness == Brightness.dark ? 0.4 : 0.7);
return ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: scheme,
scaffoldBackgroundColor: scheme.surface,
appBarTheme: AppBarTheme(
backgroundColor: scheme.surface,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
),
cardTheme: CardThemeData(
elevation: 0,
color: scheme.surfaceContainer,
surfaceTintColor: Colors.transparent,
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: outline),
),
),
dividerTheme: DividerThemeData(color: outline, space: 1, thickness: 1),
navigationRailTheme: NavigationRailThemeData(
backgroundColor: scheme.surfaceContainerLow,
indicatorColor: scheme.primaryContainer,
useIndicator: true,
selectedIconTheme: IconThemeData(color: scheme.primary),
unselectedIconTheme: IconThemeData(color: scheme.onSurfaceVariant),
selectedLabelTextStyle: TextStyle(color: scheme.primary, fontWeight: FontWeight.w600),
unselectedLabelTextStyle: TextStyle(color: scheme.onSurfaceVariant),
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: scheme.surfaceContainerLow,
indicatorColor: scheme.primaryContainer,
elevation: 0,
iconTheme: WidgetStateProperty.resolveWith(
(states) => IconThemeData(
color: states.contains(WidgetState.selected) ? scheme.primary : scheme.onSurfaceVariant,
),
),
labelTextStyle: WidgetStateProperty.resolveWith(
(states) => TextStyle(
fontSize: 12,
fontWeight: states.contains(WidgetState.selected) ? FontWeight.w600 : FontWeight.w400,
color: states.contains(WidgetState.selected) ? scheme.primary : scheme.onSurfaceVariant,
),
),
),
);
}
}