/ app / lib / core / theme / app_theme.dart
app_theme.dart
  1  import 'package:flutter/material.dart';
  2  import 'package:flutter/services.dart';
  3  
  4  import 'app_colors.dart';
  5  import 'app_spacing.dart';
  6  import 'app_typography.dart';
  7  
  8  export 'app_colors.dart';
  9  export 'app_spacing.dart';
 10  export 'app_responsive.dart';
 11  export 'app_typography.dart';
 12  
 13  /// App theme configuration using Material You tonal system
 14  abstract final class AppTheme {
 15    /// Light theme
 16    static ThemeData light({double scaleFactor = 1.0}) {
 17      final colors = AppColors.light;
 18      final textTheme = AppTypography.textTheme(
 19        colors.onBackground,
 20        colors.onBackgroundSecondary,
 21      );
 22      final scaledButtonHeight = AppSizes.buttonHeightMedium * scaleFactor;
 23      final scaledFabSize = 56.0 * scaleFactor;
 24  
 25      return ThemeData(
 26        useMaterial3: true,
 27        brightness: Brightness.light,
 28        fontFamily: AppTypography.fontFamily,
 29  
 30        // Colors — use seed-derived ColorScheme directly
 31        colorScheme: colors.colorScheme,
 32  
 33        scaffoldBackgroundColor: colors.background,
 34        dividerColor: colors.divider,
 35  
 36        // Text theme
 37        textTheme: textTheme,
 38  
 39        // Icon theme — scaled for tablets
 40        iconTheme: IconThemeData(size: AppSizes.iconMd * scaleFactor),
 41  
 42        // AppBar
 43        appBarTheme: AppBarTheme(
 44          elevation: 0,
 45          scrolledUnderElevation: 0,
 46          centerTitle: false,
 47          backgroundColor: colors.background,
 48          foregroundColor: colors.onBackground,
 49          titleTextStyle: textTheme.titleLarge?.copyWith(
 50            fontSize: 18,
 51            fontWeight: AppTypography.semiBold,
 52          ),
 53          systemOverlayStyle: SystemUiOverlayStyle.dark,
 54        ),
 55  
 56        // Cards — flat with tonal surface
 57        cardTheme: CardThemeData(
 58          elevation: 0,
 59          shape: RoundedRectangleBorder(borderRadius: AppRadius.borderRadiusCard),
 60          color: colors.surfaceContainerLow,
 61          margin: EdgeInsets.zero,
 62        ),
 63  
 64        // Buttons
 65        elevatedButtonTheme: ElevatedButtonThemeData(
 66          style: ElevatedButton.styleFrom(
 67            elevation: 0,
 68            backgroundColor: colors.primary,
 69            foregroundColor: colors.onPrimary,
 70            minimumSize: Size.fromHeight(scaledButtonHeight),
 71            padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
 72            shape: RoundedRectangleBorder(
 73              borderRadius: AppRadius.borderRadiusButton,
 74            ),
 75            textStyle: textTheme.labelLarge?.copyWith(
 76              fontWeight: AppTypography.semiBold,
 77            ),
 78          ),
 79        ),
 80  
 81        outlinedButtonTheme: OutlinedButtonThemeData(
 82          style: OutlinedButton.styleFrom(
 83            foregroundColor: colors.primary,
 84            minimumSize: Size.fromHeight(scaledButtonHeight),
 85            padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
 86            shape: RoundedRectangleBorder(
 87              borderRadius: AppRadius.borderRadiusButton,
 88            ),
 89            side: BorderSide(color: colors.primary),
 90            textStyle: textTheme.labelLarge?.copyWith(
 91              fontWeight: AppTypography.semiBold,
 92            ),
 93          ),
 94        ),
 95  
 96        textButtonTheme: TextButtonThemeData(
 97          style: TextButton.styleFrom(
 98            foregroundColor: colors.primary,
 99            padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
100            textStyle: textTheme.labelLarge?.copyWith(
101              fontWeight: AppTypography.semiBold,
102            ),
103          ),
104        ),
105  
106        // FAB — tonal primary container
107        floatingActionButtonTheme: FloatingActionButtonThemeData(
108          backgroundColor: colors.primaryContainer,
109          foregroundColor: colors.onPrimaryContainer,
110          elevation: 3,
111          sizeConstraints: BoxConstraints.tightFor(
112            width: scaledFabSize,
113            height: scaledFabSize,
114          ),
115          shape: RoundedRectangleBorder(
116            borderRadius: BorderRadius.circular(AppRadius.xl),
117          ),
118        ),
119  
120        // Input fields
121        inputDecorationTheme: InputDecorationTheme(
122          filled: true,
123          fillColor: colors.surfaceContainerLow,
124          contentPadding: const EdgeInsets.symmetric(
125            horizontal: AppSpacing.md,
126            vertical: AppSpacing.sm,
127          ),
128          border: OutlineInputBorder(
129            borderRadius: AppRadius.borderRadiusInput,
130            borderSide: BorderSide(color: colors.outline),
131          ),
132          enabledBorder: OutlineInputBorder(
133            borderRadius: AppRadius.borderRadiusInput,
134            borderSide: BorderSide(color: colors.outline),
135          ),
136          focusedBorder: OutlineInputBorder(
137            borderRadius: AppRadius.borderRadiusInput,
138            borderSide: BorderSide(color: colors.primary, width: 2),
139          ),
140          errorBorder: OutlineInputBorder(
141            borderRadius: AppRadius.borderRadiusInput,
142            borderSide: BorderSide(color: colors.error),
143          ),
144          hintStyle: textTheme.bodyLarge?.copyWith(
145            color: colors.onBackgroundSecondary,
146          ),
147        ),
148  
149        // Chips
150        chipTheme: ChipThemeData(
151          backgroundColor: colors.surfaceContainerLow,
152          selectedColor: colors.primaryContainer,
153          labelStyle: textTheme.labelMedium,
154          shape: RoundedRectangleBorder(borderRadius: AppRadius.borderRadiusChip),
155          side: BorderSide(color: colors.outline),
156        ),
157  
158        // Bottom sheet
159        bottomSheetTheme: BottomSheetThemeData(
160          backgroundColor: colors.background,
161          shape: const RoundedRectangleBorder(
162            borderRadius: AppRadius.borderRadiusBottomSheet,
163          ),
164        ),
165  
166        // Dialog
167        dialogTheme: DialogThemeData(
168          backgroundColor: colors.background,
169          shape: RoundedRectangleBorder(
170            borderRadius: BorderRadius.circular(AppRadius.dialog),
171          ),
172        ),
173  
174        // Divider
175        dividerTheme: DividerThemeData(
176          color: colors.divider,
177          thickness: 1,
178          space: 0,
179        ),
180  
181        // List tile
182        listTileTheme: ListTileThemeData(
183          contentPadding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
184          minVerticalPadding: AppSpacing.sm,
185        ),
186  
187        // Bottom navigation
188        bottomNavigationBarTheme: BottomNavigationBarThemeData(
189          backgroundColor: colors.background,
190          selectedItemColor: colors.primary,
191          unselectedItemColor: colors.onBackgroundSecondary,
192          elevation: 8,
193          type: BottomNavigationBarType.fixed,
194        ),
195  
196        // Snackbar — M3 inverse surface
197        snackBarTheme: SnackBarThemeData(
198          backgroundColor: colors.colorScheme.inverseSurface,
199          contentTextStyle: textTheme.bodyMedium?.copyWith(
200            color: colors.colorScheme.onInverseSurface,
201          ),
202          shape: RoundedRectangleBorder(borderRadius: AppRadius.borderRadiusMd),
203          behavior: SnackBarBehavior.floating,
204        ),
205      );
206    }
207  
208    /// Dark theme
209    static ThemeData dark({double scaleFactor = 1.0}) {
210      final colors = AppColors.dark;
211      final textTheme = AppTypography.textTheme(
212        colors.onBackground,
213        colors.onBackgroundSecondary,
214      );
215      final scaledButtonHeight = AppSizes.buttonHeightMedium * scaleFactor;
216      final scaledFabSize = 56.0 * scaleFactor;
217  
218      return ThemeData(
219        useMaterial3: true,
220        brightness: Brightness.dark,
221        fontFamily: AppTypography.fontFamily,
222  
223        // Colors — use seed-derived ColorScheme directly
224        colorScheme: colors.colorScheme,
225  
226        scaffoldBackgroundColor: colors.background,
227        dividerColor: colors.divider,
228  
229        // Text theme
230        textTheme: textTheme,
231  
232        // Icon theme — scaled for tablets
233        iconTheme: IconThemeData(size: AppSizes.iconMd * scaleFactor),
234  
235        // AppBar
236        appBarTheme: AppBarTheme(
237          elevation: 0,
238          scrolledUnderElevation: 0,
239          centerTitle: false,
240          backgroundColor: colors.background,
241          foregroundColor: colors.onBackground,
242          titleTextStyle: textTheme.titleLarge?.copyWith(
243            fontSize: 18,
244            fontWeight: AppTypography.semiBold,
245          ),
246          systemOverlayStyle: SystemUiOverlayStyle.light,
247        ),
248  
249        // Cards — flat with tonal surface
250        cardTheme: CardThemeData(
251          elevation: 0,
252          shape: RoundedRectangleBorder(borderRadius: AppRadius.borderRadiusCard),
253          color: colors.surfaceContainerLow,
254          margin: EdgeInsets.zero,
255        ),
256  
257        // Buttons
258        elevatedButtonTheme: ElevatedButtonThemeData(
259          style: ElevatedButton.styleFrom(
260            elevation: 0,
261            backgroundColor: colors.primary,
262            foregroundColor: colors.onPrimary,
263            minimumSize: Size.fromHeight(scaledButtonHeight),
264            padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
265            shape: RoundedRectangleBorder(
266              borderRadius: AppRadius.borderRadiusButton,
267            ),
268            textStyle: textTheme.labelLarge?.copyWith(
269              fontWeight: AppTypography.semiBold,
270            ),
271          ),
272        ),
273  
274        outlinedButtonTheme: OutlinedButtonThemeData(
275          style: OutlinedButton.styleFrom(
276            foregroundColor: colors.primary,
277            minimumSize: Size.fromHeight(scaledButtonHeight),
278            padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
279            shape: RoundedRectangleBorder(
280              borderRadius: AppRadius.borderRadiusButton,
281            ),
282            side: BorderSide(color: colors.primary),
283            textStyle: textTheme.labelLarge?.copyWith(
284              fontWeight: AppTypography.semiBold,
285            ),
286          ),
287        ),
288  
289        textButtonTheme: TextButtonThemeData(
290          style: TextButton.styleFrom(
291            foregroundColor: colors.primary,
292            padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
293            textStyle: textTheme.labelLarge?.copyWith(
294              fontWeight: AppTypography.semiBold,
295            ),
296          ),
297        ),
298  
299        // FAB — tonal primary container
300        floatingActionButtonTheme: FloatingActionButtonThemeData(
301          backgroundColor: colors.primaryContainer,
302          foregroundColor: colors.onPrimaryContainer,
303          elevation: 3,
304          sizeConstraints: BoxConstraints.tightFor(
305            width: scaledFabSize,
306            height: scaledFabSize,
307          ),
308          shape: RoundedRectangleBorder(
309            borderRadius: BorderRadius.circular(AppRadius.xl),
310          ),
311        ),
312  
313        // Input fields
314        inputDecorationTheme: InputDecorationTheme(
315          filled: true,
316          fillColor: colors.surfaceContainerLow,
317          contentPadding: const EdgeInsets.symmetric(
318            horizontal: AppSpacing.md,
319            vertical: AppSpacing.sm,
320          ),
321          border: OutlineInputBorder(
322            borderRadius: AppRadius.borderRadiusInput,
323            borderSide: BorderSide(color: colors.outline),
324          ),
325          enabledBorder: OutlineInputBorder(
326            borderRadius: AppRadius.borderRadiusInput,
327            borderSide: BorderSide(color: colors.outline),
328          ),
329          focusedBorder: OutlineInputBorder(
330            borderRadius: AppRadius.borderRadiusInput,
331            borderSide: BorderSide(color: colors.primary, width: 2),
332          ),
333          errorBorder: OutlineInputBorder(
334            borderRadius: AppRadius.borderRadiusInput,
335            borderSide: BorderSide(color: colors.error),
336          ),
337          hintStyle: textTheme.bodyLarge?.copyWith(
338            color: colors.onBackgroundSecondary,
339          ),
340        ),
341  
342        // Chips
343        chipTheme: ChipThemeData(
344          backgroundColor: colors.surfaceContainerLow,
345          selectedColor: colors.primaryContainer,
346          labelStyle: textTheme.labelMedium,
347          shape: RoundedRectangleBorder(borderRadius: AppRadius.borderRadiusChip),
348          side: BorderSide(color: colors.outline),
349        ),
350  
351        // Bottom sheet
352        bottomSheetTheme: BottomSheetThemeData(
353          backgroundColor: colors.background,
354          shape: const RoundedRectangleBorder(
355            borderRadius: AppRadius.borderRadiusBottomSheet,
356          ),
357        ),
358  
359        // Dialog
360        dialogTheme: DialogThemeData(
361          backgroundColor: colors.surfaceContainerLow,
362          shape: RoundedRectangleBorder(
363            borderRadius: BorderRadius.circular(AppRadius.dialog),
364          ),
365        ),
366  
367        // Divider
368        dividerTheme: DividerThemeData(
369          color: colors.divider,
370          thickness: 1,
371          space: 0,
372        ),
373  
374        // List tile
375        listTileTheme: ListTileThemeData(
376          contentPadding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
377          minVerticalPadding: AppSpacing.sm,
378        ),
379  
380        // Bottom navigation
381        bottomNavigationBarTheme: BottomNavigationBarThemeData(
382          backgroundColor: colors.background,
383          selectedItemColor: colors.primary,
384          unselectedItemColor: colors.onBackgroundSecondary,
385          elevation: 0,
386          type: BottomNavigationBarType.fixed,
387        ),
388  
389        // Snackbar — M3 inverse surface
390        snackBarTheme: SnackBarThemeData(
391          backgroundColor: colors.colorScheme.inverseSurface,
392          contentTextStyle: textTheme.bodyMedium?.copyWith(
393            color: colors.colorScheme.onInverseSurface,
394          ),
395          shape: RoundedRectangleBorder(borderRadius: AppRadius.borderRadiusMd),
396          behavior: SnackBarBehavior.floating,
397        ),
398      );
399    }
400  }