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, + ), + ), + ), + ); + } +}