/// Material 3 light and dark themes — charcoal surfaces, mint accent. library; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; /// Builds the [ThemeData] used by [MaterialApp] for light and dark mode. abstract final class AppTheme { /// Mint / emerald accent from the product look. static const Color accent = Color(0xFF4FA894); /// Soft rose for expenses / money out. static const Color expense = Color(0xFFE07A7A); /// Near-black charcoal scaffold (dark). static const Color darkCanvas = Color(0xFF0B0D0C); static const Color darkSurface = Color(0xFF141716); static const Color darkContainer = Color(0xFF1E2321); static const Color darkContainerHigh = Color(0xFF2A302D); static ThemeData light() => _build(Brightness.light); static ThemeData dark() => _build(Brightness.dark); static ColorScheme _scheme(Brightness brightness) { if (brightness == Brightness.dark) { return const ColorScheme.dark( primary: accent, onPrimary: Color(0xFF06201A), primaryContainer: Color(0xFF1F4F44), onPrimaryContainer: Color(0xFFB8E6D8), secondary: Color(0xFF8FB5AB), onSecondary: Color(0xFF0B1F1A), secondaryContainer: Color(0xFF2A403A), onSecondaryContainer: Color(0xFFD0E8E0), tertiary: expense, onTertiary: Color(0xFF2A0F0F), tertiaryContainer: Color(0xFF5C2E2E), onTertiaryContainer: Color(0xFFFFDAD6), error: expense, onError: Color(0xFF2A0F0F), errorContainer: Color(0xFF5C2E2E), onErrorContainer: Color(0xFFFFDAD6), surface: darkSurface, onSurface: Color(0xFFE8EEEC), onSurfaceVariant: Color(0xFFA3AFA9), surfaceContainerLowest: darkCanvas, surfaceContainerLow: Color(0xFF171B19), surfaceContainer: darkContainer, surfaceContainerHigh: darkContainerHigh, surfaceContainerHighest: Color(0xFF343B38), outline: Color(0xFF6B7671), outlineVariant: Color(0xFF3A423E), shadow: Colors.black, scrim: Colors.black, inverseSurface: Color(0xFFE8EEEC), onInverseSurface: Color(0xFF1A1F1D), inversePrimary: Color(0xFF0F766E), ); } return ColorScheme.fromSeed( seedColor: accent, brightness: Brightness.light, primary: const Color(0xFF1F6B5C), error: const Color(0xFFC45C5C), ).copyWith( surface: const Color(0xFFF4F7F5), surfaceContainerLowest: const Color(0xFFFFFFFF), surfaceContainerLow: const Color(0xFFEEF3F0), surfaceContainer: const Color(0xFFE6EDE9), surfaceContainerHigh: const Color(0xFFDCE6E1), surfaceContainerHighest: const Color(0xFFD0DCD6), onSurface: const Color(0xFF15201C), onSurfaceVariant: const Color(0xFF4A5A54), outline: const Color(0xFF7A8A83), outlineVariant: const Color(0xFFC5D0CB), tertiary: expense, error: expense, ); } static ThemeData _build(Brightness brightness) { final colorScheme = _scheme(brightness); final isDark = brightness == Brightness.dark; final textTheme = _textTheme(colorScheme); const radius = 14.0; const radiusLg = 18.0; return ThemeData( useMaterial3: true, brightness: brightness, colorScheme: colorScheme, textTheme: textTheme, scaffoldBackgroundColor: isDark ? darkCanvas : colorScheme.surfaceContainerLowest, canvasColor: isDark ? darkCanvas : colorScheme.surface, appBarTheme: AppBarTheme( centerTitle: false, elevation: 0, scrolledUnderElevation: 0, backgroundColor: isDark ? darkCanvas : colorScheme.surfaceContainerLowest, foregroundColor: colorScheme.onSurface, titleTextStyle: textTheme.titleLarge, systemOverlayStyle: isDark ? SystemUiOverlayStyle.light.copyWith( statusBarColor: Colors.transparent, systemNavigationBarColor: darkCanvas, ) : SystemUiOverlayStyle.dark.copyWith( statusBarColor: Colors.transparent, ), ), cardTheme: CardThemeData( elevation: 0, color: colorScheme.surfaceContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(radiusLg), ), clipBehavior: Clip.antiAlias, margin: EdgeInsets.zero, ), navigationBarTheme: NavigationBarThemeData( elevation: 0, height: 72, backgroundColor: isDark ? darkSurface : colorScheme.surfaceContainer, indicatorColor: colorScheme.primary.withValues(alpha: 0.22), labelTextStyle: WidgetStateProperty.resolveWith((states) { final selected = states.contains(WidgetState.selected); return textTheme.labelMedium?.copyWith( fontWeight: selected ? FontWeight.w600 : FontWeight.w500, color: selected ? colorScheme.primary : colorScheme.onSurfaceVariant, ); }), iconTheme: WidgetStateProperty.resolveWith((states) { final selected = states.contains(WidgetState.selected); return IconThemeData( color: selected ? colorScheme.primary : colorScheme.onSurfaceVariant, ); }), ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: colorScheme.surfaceContainerHigh, border: OutlineInputBorder( borderRadius: BorderRadius.circular(radius), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(radius), borderSide: BorderSide( color: colorScheme.outlineVariant.withValues(alpha: 0.6), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(radius), borderSide: BorderSide(color: colorScheme.primary, width: 1.5), ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), hintStyle: textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant.withValues(alpha: 0.75), ), ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(48), backgroundColor: colorScheme.primary, foregroundColor: colorScheme.onPrimary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(radius), ), textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700), ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( foregroundColor: colorScheme.primary, textStyle: textTheme.labelLarge, ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( foregroundColor: colorScheme.onSurface, side: BorderSide(color: colorScheme.outlineVariant), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(radius), ), minimumSize: const Size(48, 44), ), ), iconButtonTheme: IconButtonThemeData( style: IconButton.styleFrom( foregroundColor: colorScheme.onSurfaceVariant, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), floatingActionButtonTheme: FloatingActionButtonThemeData( backgroundColor: colorScheme.primary, foregroundColor: colorScheme.onPrimary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(radiusLg), ), ), dividerTheme: DividerThemeData( color: colorScheme.outlineVariant.withValues(alpha: 0.55), space: 1, thickness: 1, ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, backgroundColor: colorScheme.inverseSurface, contentTextStyle: textTheme.bodyMedium?.copyWith( color: colorScheme.onInverseSurface, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(radius), ), ), listTileTheme: ListTileThemeData( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(radius), ), iconColor: colorScheme.primary, textColor: colorScheme.onSurface, ), chipTheme: ChipThemeData( backgroundColor: colorScheme.surfaceContainerHigh, selectedColor: colorScheme.primary.withValues(alpha: 0.28), deleteIconColor: colorScheme.onSurfaceVariant, labelStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600), secondaryLabelStyle: textTheme.labelMedium, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: BorderSide( color: colorScheme.outlineVariant.withValues(alpha: 0.5), ), ), side: BorderSide( color: colorScheme.outlineVariant.withValues(alpha: 0.5), ), ), dialogTheme: DialogThemeData( backgroundColor: colorScheme.surfaceContainer, surfaceTintColor: Colors.transparent, elevation: 8, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), titleTextStyle: textTheme.titleLarge, contentTextStyle: textTheme.bodyMedium, ), bottomSheetTheme: BottomSheetThemeData( backgroundColor: colorScheme.surfaceContainer, surfaceTintColor: Colors.transparent, elevation: 8, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), dragHandleColor: colorScheme.outlineVariant, showDragHandle: true, ), // Avoid textStyle here — SegmentedButton lerps selected/unselected styles // and crashes if inherit flags disagree. segmentedButtonTheme: SegmentedButtonThemeData( style: ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return colorScheme.primary; } return colorScheme.surfaceContainerHigh; }), foregroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return colorScheme.onPrimary; } return colorScheme.onSurfaceVariant; }), side: WidgetStatePropertyAll( BorderSide(color: colorScheme.outlineVariant.withValues(alpha: 0.7)), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), visualDensity: VisualDensity.compact, ), ), sliderTheme: SliderThemeData( activeTrackColor: colorScheme.primary, inactiveTrackColor: colorScheme.outlineVariant.withValues(alpha: 0.5), thumbColor: colorScheme.primary, overlayColor: colorScheme.primary.withValues(alpha: 0.16), rangeThumbShape: const RoundRangeSliderThumbShape(enabledThumbRadius: 8), trackHeight: 4, ), checkboxTheme: CheckboxThemeData( fillColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return colorScheme.primary; } return Colors.transparent; }), checkColor: WidgetStatePropertyAll(colorScheme.onPrimary), side: BorderSide(color: colorScheme.outline, width: 1.5), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), ), progressIndicatorTheme: ProgressIndicatorThemeData( color: colorScheme.primary, linearTrackColor: colorScheme.outlineVariant.withValues(alpha: 0.35), ), shadowColor: isDark ? Colors.black : const Color(0x1A0F172A), ); } /// Built from Material 2021 typography so every style shares `inherit: false` /// and theme animations / SegmentedButton can lerp safely. static TextTheme _textTheme(ColorScheme colors) { final base = Typography.material2021().englishLike.apply( bodyColor: colors.onSurface, displayColor: colors.onSurface, ); return base.copyWith( displaySmall: base.displaySmall?.copyWith( fontSize: 36, fontWeight: FontWeight.w700, letterSpacing: -0.6, ), headlineMedium: base.headlineMedium?.copyWith( fontSize: 28, fontWeight: FontWeight.w600, letterSpacing: -0.4, ), titleLarge: base.titleLarge?.copyWith( fontSize: 22, fontWeight: FontWeight.w600, letterSpacing: -0.2, ), titleMedium: base.titleMedium?.copyWith( fontSize: 16, fontWeight: FontWeight.w600, ), titleSmall: base.titleSmall?.copyWith( fontSize: 14, fontWeight: FontWeight.w600, ), bodyLarge: base.bodyLarge?.copyWith( fontSize: 16, height: 1.4, fontWeight: FontWeight.w500, ), bodyMedium: base.bodyMedium?.copyWith( fontSize: 14, height: 1.4, color: colors.onSurfaceVariant, ), bodySmall: base.bodySmall?.copyWith( fontSize: 12, height: 1.35, color: colors.onSurfaceVariant, ), labelLarge: base.labelLarge?.copyWith( fontSize: 14, fontWeight: FontWeight.w600, letterSpacing: 0.1, ), labelMedium: base.labelMedium?.copyWith( fontSize: 12, fontWeight: FontWeight.w500, letterSpacing: 0.2, color: colors.onSurfaceVariant, ), labelSmall: base.labelSmall?.copyWith( fontSize: 11, fontWeight: FontWeight.w500, letterSpacing: 0.3, color: colors.onSurfaceVariant, ), ); } }