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 }