feat(app): токены темы — тёмный сине-индиго фон, единый синий акцент

ColorScheme в AppTheme.light()/.dark() строится из одного seed вместо
двух параллельных наборов чисел для тёмной и светлой темы — акцент
сведён к ChartColors.slot1Blue, чтобы фирменный цвет интерфейса и
основной цвет графиков совпадали. cardTheme/navigationRailTheme/
navigationBarTheme заданы централизованно и каскадом меняют вид
существующих карточек и навигации на всех экранах.
This commit is contained in:
Dmitry
2026-09-19 15:31:16 +03:00
parent 52d07e1503
commit bea239ba2d
2 changed files with 78 additions and 6 deletions
+3 -6
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/theme/app_theme.dart';
import 'core/theme/theme_controller.dart';
import 'router.dart';
@@ -16,12 +17,8 @@ class FinTrackerApp extends ConsumerWidget {
title: 'fin-tracker',
routerConfig: router,
themeMode: themeMode,
theme: ThemeData(colorSchemeSeed: const Color(0xFF2E6F5E), useMaterial3: true),
darkTheme: ThemeData(
colorSchemeSeed: const Color(0xFF2E6F5E),
brightness: Brightness.dark,
useMaterial3: true,
),
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
locale: const Locale('ru'),
supportedLocales: const [Locale('ru'), Locale('en')],
localizationsDelegates: GlobalMaterialLocalizations.delegates,
+75
View File
@@ -0,0 +1,75 @@
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,
),
),
),
);
}
}