From bea239ba2d361dcf66544e5d249575d0fb7ee196 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sat, 19 Sep 2026 15:31:16 +0300 Subject: [PATCH] =?UTF-8?q?feat(app):=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D1=82=D0=B5=D0=BC=D1=8B=20=E2=80=94=20=D1=82=D1=91?= =?UTF-8?q?=D0=BC=D0=BD=D1=8B=D0=B9=20=D1=81=D0=B8=D0=BD=D0=B5-=D0=B8?= =?UTF-8?q?=D0=BD=D0=B4=D0=B8=D0=B3=D0=BE=20=D1=84=D0=BE=D0=BD,=20=D0=B5?= =?UTF-8?q?=D0=B4=D0=B8=D0=BD=D1=8B=D0=B9=20=D1=81=D0=B8=D0=BD=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=B0=D0=BA=D1=86=D0=B5=D0=BD=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ColorScheme в AppTheme.light()/.dark() строится из одного seed вместо двух параллельных наборов чисел для тёмной и светлой темы — акцент сведён к ChartColors.slot1Blue, чтобы фирменный цвет интерфейса и основной цвет графиков совпадали. cardTheme/navigationRailTheme/ navigationBarTheme заданы централизованно и каскадом меняют вид существующих карточек и навигации на всех экранах. --- app/lib/app.dart | 9 ++-- app/lib/core/theme/app_theme.dart | 75 +++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 app/lib/core/theme/app_theme.dart diff --git a/app/lib/app.dart b/app/lib/app.dart index 01cef7a..1542501 100644 --- a/app/lib/app.dart +++ b/app/lib/app.dart @@ -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, diff --git a/app/lib/core/theme/app_theme.dart b/app/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..6169dca --- /dev/null +++ b/app/lib/core/theme/app_theme.dart @@ -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, + ), + ), + ), + ); + } +}