import 'package:flutter/material.dart'; import 'package:shuffle/core/theme/app_spacing.dart'; /// Material 3 theme base of the design system (see docs/design-system.md). /// /// Colors come from [ColorScheme.fromSeed]; the contrast of all core pairs is /// verified in `test/core/theme/app_theme_test.dart` (WCAG AA, >= 4.5:1 for /// text, >= 3:1 for the focus indicator and input borders). abstract final class AppTheme { static const Color seed = Color(0xFF4F46E5); static ThemeData light() => _build(Brightness.light); static ThemeData dark() => _build(Brightness.dark); static ColorScheme _scheme(Brightness brightness) { final base = ColorScheme.fromSeed(seedColor: seed, brightness: brightness); // Tone 50/60 outlines keep >= 3:1 against the surface (WCAG 1.4.11); // the M3 default outline is slightly weaker in light mode. return base.copyWith( outline: brightness == Brightness.light ? const Color(0xFF6B6A7A) : const Color(0xFF9A99AB), ); } static ThemeData _build(Brightness brightness) { final scheme = _scheme(brightness); final textTheme = Typography.material2021(colorScheme: scheme).black .apply(bodyColor: scheme.onSurface, displayColor: scheme.onSurface); final themedText = brightness == Brightness.dark ? Typography.material2021(colorScheme: scheme).white.apply( bodyColor: scheme.onSurface, displayColor: scheme.onSurface, ) : textTheme; const minSize = Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget); final shape = RoundedRectangleBorder(borderRadius: AppRadius.mdAll); // Visible keyboard focus ring (web/desktop), contrast >= 3:1. final focusSide = WidgetStateProperty.resolveWith( (states) => states.contains(WidgetState.focused) ? BorderSide(color: scheme.primary, width: 3) : null, ); OutlineInputBorder border(Color color, double width) => OutlineInputBorder( borderRadius: AppRadius.mdAll, borderSide: BorderSide(color: color, width: width), ); return ThemeData( useMaterial3: true, colorScheme: scheme, textTheme: themedText, visualDensity: VisualDensity.standard, materialTapTargetSize: MaterialTapTargetSize.padded, scaffoldBackgroundColor: scheme.surface, focusColor: scheme.primary.withValues(alpha: 0.16), dividerTheme: DividerThemeData(color: scheme.outlineVariant), appBarTheme: AppBarTheme( backgroundColor: scheme.surface, foregroundColor: scheme.onSurface, centerTitle: false, titleTextStyle: themedText.titleLarge, ), cardTheme: CardThemeData( elevation: 0, color: scheme.surfaceContainerLow, shape: RoundedRectangleBorder( borderRadius: AppRadius.lgAll, side: BorderSide(color: scheme.outlineVariant), ), ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( minimumSize: minSize, shape: shape, padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xl), ).copyWith(side: focusSide), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( minimumSize: minSize, shape: shape, ).copyWith(side: focusSide), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( minimumSize: minSize, shape: shape, side: BorderSide(color: scheme.outline), padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xl), ).copyWith( side: WidgetStateProperty.resolveWith( (states) => states.contains(WidgetState.focused) ? BorderSide(color: scheme.primary, width: 3) : BorderSide(color: scheme.outline), ), ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( minimumSize: minSize, shape: shape, padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), ).copyWith(side: focusSide), ), iconButtonTheme: IconButtonThemeData( style: IconButton.styleFrom(minimumSize: minSize), ), checkboxTheme: CheckboxThemeData( materialTapTargetSize: MaterialTapTargetSize.padded, side: BorderSide(color: scheme.outline, width: 2), ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: scheme.surfaceContainerHighest, contentPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.lg, vertical: AppSpacing.md, ), border: border(scheme.outline, 1), enabledBorder: border(scheme.outline, 1), focusedBorder: border(scheme.primary, 3), errorBorder: border(scheme.error, 2), focusedErrorBorder: border(scheme.error, 3), errorStyle: themedText.bodySmall?.copyWith(color: scheme.error), helperStyle: themedText.bodySmall?.copyWith( color: scheme.onSurfaceVariant, ), ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, backgroundColor: scheme.inverseSurface, contentTextStyle: themedText.bodyMedium?.copyWith( color: scheme.onInverseSurface, ), actionTextColor: scheme.inversePrimary, shape: RoundedRectangleBorder(borderRadius: AppRadius.smAll), showCloseIcon: true, closeIconColor: scheme.onInverseSurface, // Wide screens: do not stretch across the whole window. width: null, ), ); } }