This commit is contained in:
2025-09-26 18:48:14 +07:00
parent 382a0e7909
commit 30ed6b39b5
85 changed files with 20722 additions and 112 deletions

288
lib/core/theme/README.md Normal file
View File

@@ -0,0 +1,288 @@
# Material 3 Theme System
A comprehensive Material 3 (Material You) design system implementation for Flutter applications.
## Overview
This theme system provides:
- **Complete Material 3 design system** with proper color schemes, typography, and spacing
- **Dynamic color support** for Material You theming
- **Light and dark theme configurations** with accessibility-compliant colors
- **Responsive typography** that scales based on screen size
- **Consistent spacing system** following Material Design guidelines
- **Custom component themes** for buttons, cards, inputs, and more
- **Theme switching widgets** with smooth animations
## Quick Start
### 1. Import the theme system
```dart
import 'package:base_flutter/core/theme/theme.dart';
```
### 2. Apply themes to your app
```dart
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeControllerProvider);
return MaterialApp(
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeMode,
// ... rest of your app
);
}
}
```
### 3. Add theme switching capability
```dart
// Simple toggle button
ThemeToggleIconButton()
// Segmented control
ThemeModeSwitch(
style: ThemeSwitchStyle.segmented,
showLabel: true,
)
// Animated switch
AnimatedThemeModeSwitch()
```
## Components
### AppTheme
Main theme configuration class containing Material 3 light and dark themes.
```dart
// Get themes
ThemeData lightTheme = AppTheme.lightTheme;
ThemeData darkTheme = AppTheme.darkTheme;
// Responsive theme
ThemeData responsiveTheme = AppTheme.responsiveTheme(context, isDark: false);
// Dynamic colors (Material You)
ColorScheme? dynamicLight = AppTheme.dynamicLightColorScheme(context);
ThemeData dynamicTheme = AppTheme.createDynamicTheme(
colorScheme: dynamicLight!,
isDark: false,
);
```
### AppColors
Material 3 color system with semantic colors.
```dart
// Color schemes
ColorScheme lightScheme = AppColors.lightScheme;
ColorScheme darkScheme = AppColors.darkScheme;
// Semantic colors
Color success = AppColors.success;
Color warning = AppColors.warning;
Color info = AppColors.info;
// Surface colors with elevation
Color elevatedSurface = AppColors.getSurfaceColor(2, false);
// Accessibility check
bool isAccessible = AppColors.isAccessible(backgroundColor, textColor);
```
### AppTypography
Material 3 typography system with responsive scaling.
```dart
// Typography styles
TextStyle displayLarge = AppTypography.displayLarge;
TextStyle headlineMedium = AppTypography.headlineMedium;
TextStyle bodyLarge = AppTypography.bodyLarge;
// Responsive typography
TextTheme responsiveTheme = AppTypography.responsiveTextTheme(context);
// Semantic text styles
TextStyle errorStyle = AppTypography.error(context);
TextStyle successStyle = AppTypography.success(context);
```
### AppSpacing
Consistent spacing system based on Material Design grid.
```dart
// Spacing values
double small = AppSpacing.sm; // 8dp
double medium = AppSpacing.md; // 12dp
double large = AppSpacing.lg; // 16dp
// EdgeInsets presets
EdgeInsets padding = AppSpacing.paddingLG;
EdgeInsets screenPadding = AppSpacing.screenPaddingAll;
// SizedBox presets
SizedBox verticalSpace = AppSpacing.verticalSpaceMD;
SizedBox horizontalSpace = AppSpacing.horizontalSpaceLG;
// Responsive padding
EdgeInsets responsivePadding = AppSpacing.responsivePadding(context);
// Border radius
BorderRadius cardRadius = AppSpacing.cardRadius;
BorderRadius buttonRadius = AppSpacing.buttonRadius;
// Screen size checks
bool isMobile = AppSpacing.isMobile(context);
bool isTablet = AppSpacing.isTablet(context);
```
## Theme Extensions
### AppColorsExtension
Additional semantic colors not covered by Material 3 ColorScheme.
```dart
// Access theme extension
final colors = Theme.of(context).extension<AppColorsExtension>();
// Use semantic colors
Color successColor = colors?.success ?? Colors.green;
Color warningColor = colors?.warning ?? Colors.orange;
Color infoColor = colors?.info ?? Colors.blue;
```
## Usage Examples
### Using Colors
```dart
Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: Text(
'Hello World',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
)
```
### Using Typography
```dart
Column(
children: [
Text('Headline', style: Theme.of(context).textTheme.headlineMedium),
AppSpacing.verticalSpaceSM,
Text('Body text', style: Theme.of(context).textTheme.bodyLarge),
],
)
```
### Using Spacing
```dart
Padding(
padding: AppSpacing.responsivePadding(context),
child: Column(
children: [
Card(),
AppSpacing.verticalSpaceLG,
ElevatedButton(),
],
),
)
```
### Theme-aware Widgets
```dart
class ThemedCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
child: Padding(
padding: AppSpacing.paddingLG,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Card Title',
style: theme.textTheme.titleMedium,
),
AppSpacing.verticalSpaceSM,
Text(
'Card content that adapts to the current theme.',
style: theme.textTheme.bodyMedium,
),
],
),
),
);
}
}
```
## Features
### ✅ Material 3 Design System
- Complete Material 3 color roles and palettes
- Proper elevation handling with surface tints
- Accessibility-compliant color combinations
- Support for dynamic colors (Material You)
### ✅ Responsive Design
- Typography that scales based on screen size
- Responsive padding and margins
- Adaptive layouts for mobile, tablet, and desktop
- Screen size utilities and breakpoints
### ✅ Theme Management
- State management integration with Riverpod
- Theme mode persistence support
- Smooth theme transitions
- Multiple theme switching UI options
### ✅ Developer Experience
- Type-safe theme access
- Consistent spacing system
- Semantic color names
- Comprehensive documentation
## Customization
### Custom Colors
```dart
// Extend AppColors for your brand colors
class BrandColors extends AppColors {
static const Color brandPrimary = Color(0xFF1976D2);
static const Color brandSecondary = Color(0xFF0288D1);
}
```
### Custom Typography
```dart
// Override typography for custom fonts
class CustomTypography extends AppTypography {
static const String fontFamily = 'CustomFont';
static TextStyle get customStyle => const TextStyle(
fontFamily: fontFamily,
fontSize: 16,
fontWeight: FontWeight.w500,
);
}
```
### Theme Showcase
See `theme_showcase.dart` for a comprehensive demo of all theme components and how they work together.
---
This theme system provides a solid foundation for building beautiful, consistent Flutter applications that follow Material 3 design principles while remaining flexible and customizable for your specific needs.

View File

@@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
/// App color schemes for light and dark modes following Material 3 guidelines
class AppColors {
// Prevent instantiation
AppColors._();
/// Light theme color scheme
static const ColorScheme lightScheme = ColorScheme(
brightness: Brightness.light,
// Primary colors
primary: Color(0xFF1976D2),
onPrimary: Color(0xFFFFFFFF),
primaryContainer: Color(0xFFBBDEFB),
onPrimaryContainer: Color(0xFF0D47A1),
// Secondary colors
secondary: Color(0xFF0288D1),
onSecondary: Color(0xFFFFFFFF),
secondaryContainer: Color(0xFFB3E5FC),
onSecondaryContainer: Color(0xFF006064),
// Tertiary colors
tertiary: Color(0xFF7B1FA2),
onTertiary: Color(0xFFFFFFFF),
tertiaryContainer: Color(0xFFE1BEE7),
onTertiaryContainer: Color(0xFF4A148C),
// Error colors
error: Color(0xFFD32F2F),
onError: Color(0xFFFFFFFF),
errorContainer: Color(0xFFFFCDD2),
onErrorContainer: Color(0xFFB71C1C),
// Background colors
surface: Color(0xFFFFFFFF),
onSurface: Color(0xFF212121),
surfaceContainerHighest: Color(0xFFF5F5F5),
onSurfaceVariant: Color(0xFF757575),
// Outline colors
outline: Color(0xFFBDBDBD),
outlineVariant: Color(0xFFE0E0E0),
// Shadow and scrim
shadow: Color(0xFF000000),
scrim: Color(0xFF000000),
// Inverse colors
inverseSurface: Color(0xFF303030),
onInverseSurface: Color(0xFFFFFFFF),
inversePrimary: Color(0xFF90CAF9),
);
/// Dark theme color scheme
static const ColorScheme darkScheme = ColorScheme(
brightness: Brightness.dark,
// Primary colors
primary: Color(0xFF90CAF9),
onPrimary: Color(0xFF0D47A1),
primaryContainer: Color(0xFF1565C0),
onPrimaryContainer: Color(0xFFE3F2FD),
// Secondary colors
secondary: Color(0xFF81D4FA),
onSecondary: Color(0xFF006064),
secondaryContainer: Color(0xFF0277BD),
onSecondaryContainer: Color(0xFFE0F7FA),
// Tertiary colors
tertiary: Color(0xFFCE93D8),
onTertiary: Color(0xFF4A148C),
tertiaryContainer: Color(0xFF8E24AA),
onTertiaryContainer: Color(0xFFF3E5F5),
// Error colors
error: Color(0xFFEF5350),
onError: Color(0xFFB71C1C),
errorContainer: Color(0xFFC62828),
onErrorContainer: Color(0xFFFFEBEE),
// Background colors
surface: Color(0xFF121212),
onSurface: Color(0xFFFFFFFF),
surfaceContainerHighest: Color(0xFF1E1E1E),
onSurfaceVariant: Color(0xFFBDBDBD),
// Outline colors
outline: Color(0xFF616161),
outlineVariant: Color(0xFF424242),
// Shadow and scrim
shadow: Color(0xFF000000),
scrim: Color(0xFF000000),
// Inverse colors
inverseSurface: Color(0xFFE0E0E0),
onInverseSurface: Color(0xFF303030),
inversePrimary: Color(0xFF1976D2),
);
/// Semantic colors for common use cases
static const Color success = Color(0xFF4CAF50);
static const Color onSuccess = Color(0xFFFFFFFF);
static const Color successContainer = Color(0xFFC8E6C9);
static const Color onSuccessContainer = Color(0xFF1B5E20);
static const Color warning = Color(0xFFFF9800);
static const Color onWarning = Color(0xFF000000);
static const Color warningContainer = Color(0xFFFFE0B2);
static const Color onWarningContainer = Color(0xFFE65100);
static const Color info = Color(0xFF2196F3);
static const Color onInfo = Color(0xFFFFFFFF);
static const Color infoContainer = Color(0xFFBBDEFB);
static const Color onInfoContainer = Color(0xFF0D47A1);
/// Surface elevation tints for Material 3
static const List<Color> surfaceTintLight = [
Color(0xFFFFFFFF), // elevation 0
Color(0xFFFCFCFC), // elevation 1
Color(0xFFF8F8F8), // elevation 2
Color(0xFFF5F5F5), // elevation 3
Color(0xFFF2F2F2), // elevation 4
Color(0xFFEFEFEF), // elevation 5
];
static const List<Color> surfaceTintDark = [
Color(0xFF121212), // elevation 0
Color(0xFF1E1E1E), // elevation 1
Color(0xFF232323), // elevation 2
Color(0xFF252525), // elevation 3
Color(0xFF272727), // elevation 4
Color(0xFF2C2C2C), // elevation 5
];
/// Get surface color with elevation tint
static Color getSurfaceColor(int elevation, bool isDark) {
final tints = isDark ? surfaceTintDark : surfaceTintLight;
final index = elevation.clamp(0, tints.length - 1);
return tints[index];
}
/// Accessibility compliant color pairs
static final Map<Color, Color> accessiblePairs = {
// High contrast pairs for better accessibility
const Color(0xFF000000): const Color(0xFFFFFFFF),
const Color(0xFFFFFFFF): const Color(0xFF000000),
const Color(0xFF1976D2): const Color(0xFFFFFFFF),
const Color(0xFFD32F2F): const Color(0xFFFFFFFF),
const Color(0xFF4CAF50): const Color(0xFFFFFFFF),
const Color(0xFFFF9800): const Color(0xFF000000),
};
/// Check if color combination meets WCAG AA standards
static bool isAccessible(Color background, Color foreground) {
final bgLuminance = background.computeLuminance();
final fgLuminance = foreground.computeLuminance();
final ratio = (bgLuminance + 0.05) / (fgLuminance + 0.05);
return ratio >= 4.5 || (1 / ratio) >= 4.5;
}
}

View File

@@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
/// Consistent spacing system following Material 3 guidelines
class AppSpacing {
// Prevent instantiation
AppSpacing._();
/// Base spacing unit (4dp in Material Design)
static const double base = 4.0;
/// Spacing scale based on Material 3 8dp grid system
static const double xs = base; // 4dp
static const double sm = base * 2; // 8dp
static const double md = base * 3; // 12dp
static const double lg = base * 4; // 16dp
static const double xl = base * 5; // 20dp
static const double xxl = base * 6; // 24dp
static const double xxxl = base * 8; // 32dp
/// Semantic spacing values
static const double tiny = xs; // 4dp
static const double small = sm; // 8dp
static const double medium = lg; // 16dp
static const double large = xxl; // 24dp
static const double huge = xxxl; // 32dp
/// Screen margins and padding
static const double screenPadding = lg; // 16dp
static const double screenPaddingLarge = xxl; // 24dp
static const double screenMargin = lg; // 16dp
static const double screenMarginLarge = xxl; // 24dp
/// Card and container spacing
static const double cardPadding = lg; // 16dp
static const double cardPaddingLarge = xxl; // 24dp
static const double cardMargin = sm; // 8dp
static const double cardBorderRadius = md; // 12dp
static const double cardBorderRadiusLarge = lg; // 16dp
/// Button spacing
static const double buttonPadding = lg; // 16dp
static const double buttonPaddingVertical = md; // 12dp
static const double buttonPaddingHorizontal = xxl; // 24dp
static const double buttonSpacing = sm; // 8dp
static const double buttonBorderRadius = md; // 12dp
/// Icon spacing
static const double iconSpacing = sm; // 8dp
static const double iconMargin = xs; // 4dp
/// List item spacing
static const double listItemPadding = lg; // 16dp
static const double listItemSpacing = sm; // 8dp
static const double listItemMargin = xs; // 4dp
/// Form field spacing
static const double fieldSpacing = lg; // 16dp
static const double fieldPadding = lg; // 16dp
static const double fieldBorderRadius = sm; // 8dp
/// Component spacing
static const double componentSpacing = sm; // 8dp
static const double componentMargin = xs; // 4dp
static const double componentPadding = md; // 12dp
/// Divider and border spacing
static const double dividerSpacing = lg; // 16dp
static const double borderWidth = 1.0;
static const double borderWidthThin = 0.5;
static const double borderWidthThick = 2.0;
/// Common EdgeInsets presets
static const EdgeInsets paddingXS = EdgeInsets.all(xs);
static const EdgeInsets paddingSM = EdgeInsets.all(sm);
static const EdgeInsets paddingMD = EdgeInsets.all(md);
static const EdgeInsets paddingLG = EdgeInsets.all(lg);
static const EdgeInsets paddingXL = EdgeInsets.all(xl);
static const EdgeInsets paddingXXL = EdgeInsets.all(xxl);
static const EdgeInsets paddingXXXL = EdgeInsets.all(xxxl);
/// Horizontal padding presets
static const EdgeInsets horizontalXS = EdgeInsets.symmetric(horizontal: xs);
static const EdgeInsets horizontalSM = EdgeInsets.symmetric(horizontal: sm);
static const EdgeInsets horizontalMD = EdgeInsets.symmetric(horizontal: md);
static const EdgeInsets horizontalLG = EdgeInsets.symmetric(horizontal: lg);
static const EdgeInsets horizontalXL = EdgeInsets.symmetric(horizontal: xl);
static const EdgeInsets horizontalXXL = EdgeInsets.symmetric(horizontal: xxl);
/// Vertical padding presets
static const EdgeInsets verticalXS = EdgeInsets.symmetric(vertical: xs);
static const EdgeInsets verticalSM = EdgeInsets.symmetric(vertical: sm);
static const EdgeInsets verticalMD = EdgeInsets.symmetric(vertical: md);
static const EdgeInsets verticalLG = EdgeInsets.symmetric(vertical: lg);
static const EdgeInsets verticalXL = EdgeInsets.symmetric(vertical: xl);
static const EdgeInsets verticalXXL = EdgeInsets.symmetric(vertical: xxl);
/// Screen padding presets
static const EdgeInsets screenPaddingAll = EdgeInsets.all(screenPadding);
static const EdgeInsets screenPaddingHorizontal = EdgeInsets.symmetric(horizontal: screenPadding);
static const EdgeInsets screenPaddingVertical = EdgeInsets.symmetric(vertical: screenPadding);
/// Safe area padding that respects system UI
static EdgeInsets safeAreaPadding(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
return EdgeInsets.only(
top: mediaQuery.padding.top + screenPadding,
bottom: mediaQuery.padding.bottom + screenPadding,
left: mediaQuery.padding.left + screenPadding,
right: mediaQuery.padding.right + screenPadding,
);
}
/// Responsive padding based on screen size
static EdgeInsets responsivePadding(BuildContext context) {
final size = MediaQuery.of(context).size;
if (size.width < 600) {
return screenPaddingAll; // Phone
} else if (size.width < 840) {
return const EdgeInsets.all(screenPaddingLarge); // Large phone / Small tablet
} else {
return const EdgeInsets.all(xxxl); // Tablet and larger
}
}
/// Responsive horizontal padding
static EdgeInsets responsiveHorizontalPadding(BuildContext context) {
final size = MediaQuery.of(context).size;
if (size.width < 600) {
return screenPaddingHorizontal; // Phone
} else if (size.width < 840) {
return const EdgeInsets.symmetric(horizontal: screenPaddingLarge); // Large phone / Small tablet
} else {
return const EdgeInsets.symmetric(horizontal: xxxl); // Tablet and larger
}
}
/// Common SizedBox presets for spacing
static const SizedBox spaceXS = SizedBox(height: xs, width: xs);
static const SizedBox spaceSM = SizedBox(height: sm, width: sm);
static const SizedBox spaceMD = SizedBox(height: md, width: md);
static const SizedBox spaceLG = SizedBox(height: lg, width: lg);
static const SizedBox spaceXL = SizedBox(height: xl, width: xl);
static const SizedBox spaceXXL = SizedBox(height: xxl, width: xxl);
static const SizedBox spaceXXXL = SizedBox(height: xxxl, width: xxxl);
/// Vertical spacing
static const SizedBox verticalSpaceXS = SizedBox(height: xs);
static const SizedBox verticalSpaceSM = SizedBox(height: sm);
static const SizedBox verticalSpaceMD = SizedBox(height: md);
static const SizedBox verticalSpaceLG = SizedBox(height: lg);
static const SizedBox verticalSpaceXL = SizedBox(height: xl);
static const SizedBox verticalSpaceXXL = SizedBox(height: xxl);
static const SizedBox verticalSpaceXXXL = SizedBox(height: xxxl);
/// Horizontal spacing
static const SizedBox horizontalSpaceXS = SizedBox(width: xs);
static const SizedBox horizontalSpaceSM = SizedBox(width: sm);
static const SizedBox horizontalSpaceMD = SizedBox(width: md);
static const SizedBox horizontalSpaceLG = SizedBox(width: lg);
static const SizedBox horizontalSpaceXL = SizedBox(width: xl);
static const SizedBox horizontalSpaceXXL = SizedBox(width: xxl);
static const SizedBox horizontalSpaceXXXL = SizedBox(width: xxxl);
/// Border radius presets
static BorderRadius radiusXS = BorderRadius.circular(xs);
static BorderRadius radiusSM = BorderRadius.circular(sm);
static BorderRadius radiusMD = BorderRadius.circular(md);
static BorderRadius radiusLG = BorderRadius.circular(lg);
static BorderRadius radiusXL = BorderRadius.circular(xl);
static BorderRadius radiusXXL = BorderRadius.circular(xxl);
/// Component-specific border radius
static BorderRadius get cardRadius => radiusMD;
static BorderRadius get buttonRadius => radiusMD;
static BorderRadius get fieldRadius => radiusSM;
static BorderRadius get dialogRadius => radiusLG;
static BorderRadius get sheetRadius => radiusXL;
/// Animation durations following Material 3
static const Duration animationFast = Duration(milliseconds: 100);
static const Duration animationNormal = Duration(milliseconds: 200);
static const Duration animationSlow = Duration(milliseconds: 300);
static const Duration animationSlower = Duration(milliseconds: 500);
/// Elevation values for Material 3
static const double elevationNone = 0;
static const double elevationLow = 1;
static const double elevationMedium = 3;
static const double elevationHigh = 6;
static const double elevationHigher = 12;
static const double elevationHighest = 24;
/// Icon sizes
static const double iconXS = 16;
static const double iconSM = 20;
static const double iconMD = 24;
static const double iconLG = 32;
static const double iconXL = 40;
static const double iconXXL = 48;
/// Button heights following Material 3
static const double buttonHeightSmall = 32;
static const double buttonHeight = 40;
static const double buttonHeightLarge = 56;
/// Minimum touch target size (accessibility)
static const double minTouchTarget = 48;
/// Breakpoints for responsive design
static const double mobileBreakpoint = 600;
static const double tabletBreakpoint = 840;
static const double desktopBreakpoint = 1200;
/// Check if screen is mobile
static bool isMobile(BuildContext context) {
return MediaQuery.of(context).size.width < mobileBreakpoint;
}
/// Check if screen is tablet
static bool isTablet(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return width >= mobileBreakpoint && width < desktopBreakpoint;
}
/// Check if screen is desktop
static bool isDesktop(BuildContext context) {
return MediaQuery.of(context).size.width >= desktopBreakpoint;
}
}

View File

@@ -0,0 +1,509 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'app_colors.dart';
import 'app_typography.dart';
import 'app_spacing.dart';
/// Main theme configuration with Material 3 design system
class AppTheme {
// Prevent instantiation
AppTheme._();
/// Light theme configuration
static ThemeData get lightTheme {
return ThemeData(
// Material 3 configuration
useMaterial3: true,
colorScheme: AppColors.lightScheme,
// Typography
textTheme: AppTypography.textTheme,
// App bar theme
appBarTheme: _lightAppBarTheme,
// Card theme
cardTheme: CardThemeData(
elevation: AppSpacing.elevationLow,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.cardRadius,
),
margin: const EdgeInsets.all(AppSpacing.cardMargin),
),
// Button themes
elevatedButtonTheme: _elevatedButtonTheme,
filledButtonTheme: _filledButtonTheme,
outlinedButtonTheme: _outlinedButtonTheme,
textButtonTheme: _textButtonTheme,
iconButtonTheme: _iconButtonTheme,
floatingActionButtonTheme: _lightFabTheme,
// Input field themes
inputDecorationTheme: _inputDecorationTheme,
// Other component themes
bottomNavigationBarTheme: _lightBottomNavTheme,
navigationBarTheme: _lightNavigationBarTheme,
navigationRailTheme: _lightNavigationRailTheme,
drawerTheme: _lightDrawerTheme,
dialogTheme: DialogThemeData(
elevation: AppSpacing.elevationHigher,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.dialogRadius,
),
titleTextStyle: AppTypography.headlineSmall,
contentTextStyle: AppTypography.bodyMedium,
),
bottomSheetTheme: _bottomSheetTheme,
snackBarTheme: _snackBarTheme,
chipTheme: _lightChipTheme,
dividerTheme: _dividerTheme,
listTileTheme: _listTileTheme,
switchTheme: _lightSwitchTheme,
checkboxTheme: _lightCheckboxTheme,
radioTheme: _lightRadioTheme,
sliderTheme: _lightSliderTheme,
progressIndicatorTheme: _progressIndicatorTheme,
// Extensions
extensions: const [
AppColorsExtension.light,
],
// Visual density
visualDensity: VisualDensity.adaptivePlatformDensity,
// Material tap target size
materialTapTargetSize: MaterialTapTargetSize.padded,
// Page transitions
pageTransitionsTheme: _pageTransitionsTheme,
// Splash factory
splashFactory: InkRipple.splashFactory,
);
}
/// Dark theme configuration
static ThemeData get darkTheme {
return ThemeData(
// Material 3 configuration
useMaterial3: true,
colorScheme: AppColors.darkScheme,
// Typography
textTheme: AppTypography.textTheme,
// App bar theme
appBarTheme: _darkAppBarTheme,
// Card theme
cardTheme: CardThemeData(
elevation: AppSpacing.elevationLow,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.cardRadius,
),
margin: const EdgeInsets.all(AppSpacing.cardMargin),
),
// Button themes
elevatedButtonTheme: _elevatedButtonTheme,
filledButtonTheme: _filledButtonTheme,
outlinedButtonTheme: _outlinedButtonTheme,
textButtonTheme: _textButtonTheme,
iconButtonTheme: _iconButtonTheme,
floatingActionButtonTheme: _darkFabTheme,
// Input field themes
inputDecorationTheme: _inputDecorationTheme,
// Other component themes
bottomNavigationBarTheme: _darkBottomNavTheme,
navigationBarTheme: _darkNavigationBarTheme,
navigationRailTheme: _darkNavigationRailTheme,
drawerTheme: _darkDrawerTheme,
dialogTheme: DialogThemeData(
elevation: AppSpacing.elevationHigher,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.dialogRadius,
),
titleTextStyle: AppTypography.headlineSmall,
contentTextStyle: AppTypography.bodyMedium,
),
bottomSheetTheme: _bottomSheetTheme,
snackBarTheme: _snackBarTheme,
chipTheme: _darkChipTheme,
dividerTheme: _dividerTheme,
listTileTheme: _listTileTheme,
switchTheme: _darkSwitchTheme,
checkboxTheme: _darkCheckboxTheme,
radioTheme: _darkRadioTheme,
sliderTheme: _darkSliderTheme,
progressIndicatorTheme: _progressIndicatorTheme,
// Extensions
extensions: const [
AppColorsExtension.dark,
],
// Visual density
visualDensity: VisualDensity.adaptivePlatformDensity,
// Material tap target size
materialTapTargetSize: MaterialTapTargetSize.padded,
// Page transitions
pageTransitionsTheme: _pageTransitionsTheme,
// Splash factory
splashFactory: InkRipple.splashFactory,
);
}
/// System UI overlay styles
static const SystemUiOverlayStyle lightSystemUiOverlay = SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
systemNavigationBarColor: Colors.white,
systemNavigationBarIconBrightness: Brightness.dark,
);
static const SystemUiOverlayStyle darkSystemUiOverlay = SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
statusBarBrightness: Brightness.dark,
systemNavigationBarColor: Color(0xFF121212),
systemNavigationBarIconBrightness: Brightness.light,
);
// Private theme components
static AppBarTheme get _lightAppBarTheme => const AppBarTheme(
elevation: 0,
scrolledUnderElevation: 1,
backgroundColor: Colors.transparent,
foregroundColor: Colors.black87,
centerTitle: false,
systemOverlayStyle: lightSystemUiOverlay,
titleTextStyle: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
);
static AppBarTheme get _darkAppBarTheme => const AppBarTheme(
elevation: 0,
scrolledUnderElevation: 1,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
centerTitle: false,
systemOverlayStyle: darkSystemUiOverlay,
titleTextStyle: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w500,
color: Colors.white,
),
);
static ElevatedButtonThemeData get _elevatedButtonTheme => ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, AppSpacing.buttonHeight),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.buttonPaddingHorizontal,
),
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.buttonRadius,
),
textStyle: AppTypography.buttonText,
elevation: AppSpacing.elevationLow,
),
);
static FilledButtonThemeData get _filledButtonTheme => FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size(0, AppSpacing.buttonHeight),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.buttonPaddingHorizontal,
),
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.buttonRadius,
),
textStyle: AppTypography.buttonText,
),
);
static OutlinedButtonThemeData get _outlinedButtonTheme => OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size(0, AppSpacing.buttonHeight),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.buttonPaddingHorizontal,
),
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.buttonRadius,
),
textStyle: AppTypography.buttonText,
side: const BorderSide(width: AppSpacing.borderWidth),
),
);
static TextButtonThemeData get _textButtonTheme => TextButtonThemeData(
style: TextButton.styleFrom(
minimumSize: const Size(0, AppSpacing.buttonHeight),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.buttonPaddingHorizontal,
),
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.buttonRadius,
),
textStyle: AppTypography.buttonText,
),
);
static IconButtonThemeData get _iconButtonTheme => IconButtonThemeData(
style: IconButton.styleFrom(
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
iconSize: AppSpacing.iconMD,
),
);
static FloatingActionButtonThemeData get _lightFabTheme => FloatingActionButtonThemeData(
elevation: AppSpacing.elevationMedium,
highlightElevation: AppSpacing.elevationHigh,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusLG,
),
);
static FloatingActionButtonThemeData get _darkFabTheme => FloatingActionButtonThemeData(
elevation: AppSpacing.elevationMedium,
highlightElevation: AppSpacing.elevationHigh,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusLG,
),
);
static InputDecorationTheme get _inputDecorationTheme => InputDecorationTheme(
filled: true,
contentPadding: const EdgeInsets.all(AppSpacing.fieldPadding),
border: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: const BorderSide(width: AppSpacing.borderWidth),
),
enabledBorder: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: const BorderSide(width: AppSpacing.borderWidth),
),
focusedBorder: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: const BorderSide(width: AppSpacing.borderWidthThick),
),
errorBorder: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: const BorderSide(width: AppSpacing.borderWidth),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: const BorderSide(width: AppSpacing.borderWidthThick),
),
errorStyle: AppTypography.errorText,
hintStyle: AppTypography.hintText,
labelStyle: AppTypography.bodyMedium,
);
static BottomNavigationBarThemeData get _lightBottomNavTheme => const BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
elevation: AppSpacing.elevationMedium,
selectedLabelStyle: AppTypography.labelSmall,
unselectedLabelStyle: AppTypography.labelSmall,
);
static BottomNavigationBarThemeData get _darkBottomNavTheme => const BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
elevation: AppSpacing.elevationMedium,
selectedLabelStyle: AppTypography.labelSmall,
unselectedLabelStyle: AppTypography.labelSmall,
);
static NavigationBarThemeData get _lightNavigationBarTheme => NavigationBarThemeData(
height: 80,
elevation: AppSpacing.elevationMedium,
labelTextStyle: WidgetStateProperty.all(AppTypography.labelSmall),
);
static NavigationBarThemeData get _darkNavigationBarTheme => NavigationBarThemeData(
height: 80,
elevation: AppSpacing.elevationMedium,
labelTextStyle: WidgetStateProperty.all(AppTypography.labelSmall),
);
static NavigationRailThemeData get _lightNavigationRailTheme => const NavigationRailThemeData(
elevation: AppSpacing.elevationMedium,
labelType: NavigationRailLabelType.selected,
);
static NavigationRailThemeData get _darkNavigationRailTheme => const NavigationRailThemeData(
elevation: AppSpacing.elevationMedium,
labelType: NavigationRailLabelType.selected,
);
static DrawerThemeData get _lightDrawerTheme => DrawerThemeData(
elevation: AppSpacing.elevationHigh,
shape: const RoundedRectangleBorder(),
);
static DrawerThemeData get _darkDrawerTheme => DrawerThemeData(
elevation: AppSpacing.elevationHigh,
shape: const RoundedRectangleBorder(),
);
static BottomSheetThemeData get _bottomSheetTheme => BottomSheetThemeData(
elevation: AppSpacing.elevationHigh,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(AppSpacing.sheetRadius.topLeft.x),
topRight: Radius.circular(AppSpacing.sheetRadius.topRight.x),
),
),
);
static SnackBarThemeData get _snackBarTheme => SnackBarThemeData(
elevation: AppSpacing.elevationMedium,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusSM,
),
behavior: SnackBarBehavior.floating,
contentTextStyle: AppTypography.bodyMedium,
);
static ChipThemeData get _lightChipTheme => ChipThemeData(
elevation: AppSpacing.elevationLow,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusSM,
),
labelStyle: AppTypography.labelMedium,
);
static ChipThemeData get _darkChipTheme => ChipThemeData(
elevation: AppSpacing.elevationLow,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusSM,
),
labelStyle: AppTypography.labelMedium,
);
static const DividerThemeData _dividerTheme = DividerThemeData(
thickness: AppSpacing.borderWidth,
space: AppSpacing.dividerSpacing,
);
static ListTileThemeData get _listTileTheme => const ListTileThemeData(
contentPadding: EdgeInsets.symmetric(
horizontal: AppSpacing.listItemPadding,
vertical: AppSpacing.listItemMargin,
),
titleTextStyle: AppTypography.titleMedium,
subtitleTextStyle: AppTypography.bodyMedium,
);
static SwitchThemeData get _lightSwitchTheme => SwitchThemeData(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) => null,
),
);
static SwitchThemeData get _darkSwitchTheme => SwitchThemeData(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) => null,
),
);
static CheckboxThemeData get _lightCheckboxTheme => CheckboxThemeData(
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusXS,
),
);
static CheckboxThemeData get _darkCheckboxTheme => CheckboxThemeData(
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.radiusXS,
),
);
static RadioThemeData get _lightRadioTheme => const RadioThemeData();
static RadioThemeData get _darkRadioTheme => const RadioThemeData();
static SliderThemeData get _lightSliderTheme => const SliderThemeData(
trackHeight: 4,
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10),
);
static SliderThemeData get _darkSliderTheme => const SliderThemeData(
trackHeight: 4,
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10),
);
static const ProgressIndicatorThemeData _progressIndicatorTheme = ProgressIndicatorThemeData(
linearTrackColor: Colors.transparent,
);
static const PageTransitionsTheme _pageTransitionsTheme = PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(),
},
);
/// Create responsive theme based on screen size
static ThemeData responsiveTheme(BuildContext context, {required bool isDark}) {
final baseTheme = isDark ? darkTheme : lightTheme;
final responsiveTextTheme = AppTypography.responsiveTextTheme(context);
return baseTheme.copyWith(
textTheme: responsiveTextTheme,
);
}
/// Get appropriate system UI overlay style based on theme
static SystemUiOverlayStyle getSystemUiOverlayStyle(bool isDark) {
return isDark ? darkSystemUiOverlay : lightSystemUiOverlay;
}
/// Dynamic color scheme support
static ColorScheme? dynamicLightColorScheme(BuildContext context) {
try {
return ColorScheme.fromSeed(
seedColor: AppColors.lightScheme.primary,
brightness: Brightness.light,
);
} catch (e) {
return null;
}
}
static ColorScheme? dynamicDarkColorScheme(BuildContext context) {
try {
return ColorScheme.fromSeed(
seedColor: AppColors.darkScheme.primary,
brightness: Brightness.dark,
);
} catch (e) {
return null;
}
}
/// Create theme with dynamic colors
static ThemeData createDynamicTheme({
required ColorScheme colorScheme,
required bool isDark,
}) {
final baseTheme = isDark ? darkTheme : lightTheme;
return baseTheme.copyWith(colorScheme: colorScheme);
}
}

View File

@@ -0,0 +1,381 @@
import 'package:flutter/material.dart';
/// Typography system following Material 3 guidelines
class AppTypography {
// Prevent instantiation
AppTypography._();
/// Base font family
static const String fontFamily = 'Roboto';
/// Display styles - Largest, reserved for short, important text
static const TextStyle displayLarge = TextStyle(
fontFamily: fontFamily,
fontSize: 57,
fontWeight: FontWeight.w400,
letterSpacing: -0.25,
height: 1.12,
);
static const TextStyle displayMedium = TextStyle(
fontFamily: fontFamily,
fontSize: 45,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.16,
);
static const TextStyle displaySmall = TextStyle(
fontFamily: fontFamily,
fontSize: 36,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.22,
);
/// Headline styles - High-emphasis text for headings
static const TextStyle headlineLarge = TextStyle(
fontFamily: fontFamily,
fontSize: 32,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.25,
);
static const TextStyle headlineMedium = TextStyle(
fontFamily: fontFamily,
fontSize: 28,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.29,
);
static const TextStyle headlineSmall = TextStyle(
fontFamily: fontFamily,
fontSize: 24,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.33,
);
/// Title styles - Medium-emphasis text for titles
static const TextStyle titleLarge = TextStyle(
fontFamily: fontFamily,
fontSize: 22,
fontWeight: FontWeight.w400,
letterSpacing: 0,
height: 1.27,
);
static const TextStyle titleMedium = TextStyle(
fontFamily: fontFamily,
fontSize: 16,
fontWeight: FontWeight.w500,
letterSpacing: 0.15,
height: 1.50,
);
static const TextStyle titleSmall = TextStyle(
fontFamily: fontFamily,
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 1.43,
);
/// Label styles - Small text for labels and captions
static const TextStyle labelLarge = TextStyle(
fontFamily: fontFamily,
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 1.43,
);
static const TextStyle labelMedium = TextStyle(
fontFamily: fontFamily,
fontSize: 12,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
height: 1.33,
);
static const TextStyle labelSmall = TextStyle(
fontFamily: fontFamily,
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
height: 1.45,
);
/// Body styles - Used for long-form writing
static const TextStyle bodyLarge = TextStyle(
fontFamily: fontFamily,
fontSize: 16,
fontWeight: FontWeight.w400,
letterSpacing: 0.15,
height: 1.50,
);
static const TextStyle bodyMedium = TextStyle(
fontFamily: fontFamily,
fontSize: 14,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
height: 1.43,
);
static const TextStyle bodySmall = TextStyle(
fontFamily: fontFamily,
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
height: 1.33,
);
/// Create a complete TextTheme for the app
static TextTheme textTheme = const TextTheme(
// Display styles
displayLarge: displayLarge,
displayMedium: displayMedium,
displaySmall: displaySmall,
// Headline styles
headlineLarge: headlineLarge,
headlineMedium: headlineMedium,
headlineSmall: headlineSmall,
// Title styles
titleLarge: titleLarge,
titleMedium: titleMedium,
titleSmall: titleSmall,
// Label styles
labelLarge: labelLarge,
labelMedium: labelMedium,
labelSmall: labelSmall,
// Body styles
bodyLarge: bodyLarge,
bodyMedium: bodyMedium,
bodySmall: bodySmall,
);
/// Responsive typography that scales based on screen size
static TextTheme responsiveTextTheme(BuildContext context) {
final size = MediaQuery.of(context).size;
final scaleFactor = _getScaleFactor(size.width);
return TextTheme(
displayLarge: displayLarge.copyWith(fontSize: displayLarge.fontSize! * scaleFactor),
displayMedium: displayMedium.copyWith(fontSize: displayMedium.fontSize! * scaleFactor),
displaySmall: displaySmall.copyWith(fontSize: displaySmall.fontSize! * scaleFactor),
headlineLarge: headlineLarge.copyWith(fontSize: headlineLarge.fontSize! * scaleFactor),
headlineMedium: headlineMedium.copyWith(fontSize: headlineMedium.fontSize! * scaleFactor),
headlineSmall: headlineSmall.copyWith(fontSize: headlineSmall.fontSize! * scaleFactor),
titleLarge: titleLarge.copyWith(fontSize: titleLarge.fontSize! * scaleFactor),
titleMedium: titleMedium.copyWith(fontSize: titleMedium.fontSize! * scaleFactor),
titleSmall: titleSmall.copyWith(fontSize: titleSmall.fontSize! * scaleFactor),
labelLarge: labelLarge.copyWith(fontSize: labelLarge.fontSize! * scaleFactor),
labelMedium: labelMedium.copyWith(fontSize: labelMedium.fontSize! * scaleFactor),
labelSmall: labelSmall.copyWith(fontSize: labelSmall.fontSize! * scaleFactor),
bodyLarge: bodyLarge.copyWith(fontSize: bodyLarge.fontSize! * scaleFactor),
bodyMedium: bodyMedium.copyWith(fontSize: bodyMedium.fontSize! * scaleFactor),
bodySmall: bodySmall.copyWith(fontSize: bodySmall.fontSize! * scaleFactor),
);
}
/// Calculate scale factor based on screen width
static double _getScaleFactor(double width) {
if (width < 360) {
return 0.9; // Small phones
} else if (width < 600) {
return 1.0; // Normal phones
} else if (width < 840) {
return 1.1; // Large phones / small tablets
} else {
return 1.2; // Tablets and larger
}
}
/// Font weight extensions for better readability
static const FontWeight thin = FontWeight.w100;
static const FontWeight extraLight = FontWeight.w200;
static const FontWeight light = FontWeight.w300;
static const FontWeight regular = FontWeight.w400;
static const FontWeight medium = FontWeight.w500;
static const FontWeight semiBold = FontWeight.w600;
static const FontWeight bold = FontWeight.w700;
static const FontWeight extraBold = FontWeight.w800;
static const FontWeight black = FontWeight.w900;
/// Common text styles for specific use cases
static const TextStyle buttonText = TextStyle(
fontFamily: fontFamily,
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 1.43,
);
static const TextStyle captionText = TextStyle(
fontFamily: fontFamily,
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
height: 1.33,
);
static const TextStyle overlineText = TextStyle(
fontFamily: fontFamily,
fontSize: 10,
fontWeight: FontWeight.w500,
letterSpacing: 1.5,
height: 1.6,
);
static const TextStyle errorText = TextStyle(
fontFamily: fontFamily,
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
height: 1.33,
);
static const TextStyle hintText = TextStyle(
fontFamily: fontFamily,
fontSize: 16,
fontWeight: FontWeight.w400,
letterSpacing: 0.15,
height: 1.50,
);
/// Text styles with semantic colors
static TextStyle success(BuildContext context) => bodyMedium.copyWith(
color: Theme.of(context).extension<AppColorsExtension>()?.success,
);
static TextStyle warning(BuildContext context) => bodyMedium.copyWith(
color: Theme.of(context).extension<AppColorsExtension>()?.warning,
);
static TextStyle error(BuildContext context) => bodyMedium.copyWith(
color: Theme.of(context).colorScheme.error,
);
static TextStyle info(BuildContext context) => bodyMedium.copyWith(
color: Theme.of(context).extension<AppColorsExtension>()?.info,
);
}
/// Theme extension for custom semantic colors
@immutable
class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
final Color? success;
final Color? onSuccess;
final Color? successContainer;
final Color? onSuccessContainer;
final Color? warning;
final Color? onWarning;
final Color? warningContainer;
final Color? onWarningContainer;
final Color? info;
final Color? onInfo;
final Color? infoContainer;
final Color? onInfoContainer;
const AppColorsExtension({
this.success,
this.onSuccess,
this.successContainer,
this.onSuccessContainer,
this.warning,
this.onWarning,
this.warningContainer,
this.onWarningContainer,
this.info,
this.onInfo,
this.infoContainer,
this.onInfoContainer,
});
@override
AppColorsExtension copyWith({
Color? success,
Color? onSuccess,
Color? successContainer,
Color? onSuccessContainer,
Color? warning,
Color? onWarning,
Color? warningContainer,
Color? onWarningContainer,
Color? info,
Color? onInfo,
Color? infoContainer,
Color? onInfoContainer,
}) {
return AppColorsExtension(
success: success ?? this.success,
onSuccess: onSuccess ?? this.onSuccess,
successContainer: successContainer ?? this.successContainer,
onSuccessContainer: onSuccessContainer ?? this.onSuccessContainer,
warning: warning ?? this.warning,
onWarning: onWarning ?? this.onWarning,
warningContainer: warningContainer ?? this.warningContainer,
onWarningContainer: onWarningContainer ?? this.onWarningContainer,
info: info ?? this.info,
onInfo: onInfo ?? this.onInfo,
infoContainer: infoContainer ?? this.infoContainer,
onInfoContainer: onInfoContainer ?? this.onInfoContainer,
);
}
@override
AppColorsExtension lerp(covariant ThemeExtension<AppColorsExtension>? other, double t) {
if (other is! AppColorsExtension) {
return this;
}
return AppColorsExtension(
success: Color.lerp(success, other.success, t),
onSuccess: Color.lerp(onSuccess, other.onSuccess, t),
successContainer: Color.lerp(successContainer, other.successContainer, t),
onSuccessContainer: Color.lerp(onSuccessContainer, other.onSuccessContainer, t),
warning: Color.lerp(warning, other.warning, t),
onWarning: Color.lerp(onWarning, other.onWarning, t),
warningContainer: Color.lerp(warningContainer, other.warningContainer, t),
onWarningContainer: Color.lerp(onWarningContainer, other.onWarningContainer, t),
info: Color.lerp(info, other.info, t),
onInfo: Color.lerp(onInfo, other.onInfo, t),
infoContainer: Color.lerp(infoContainer, other.infoContainer, t),
onInfoContainer: Color.lerp(onInfoContainer, other.onInfoContainer, t),
);
}
/// Light theme extension
static const AppColorsExtension light = AppColorsExtension(
success: Color(0xFF4CAF50),
onSuccess: Color(0xFFFFFFFF),
successContainer: Color(0xFFC8E6C9),
onSuccessContainer: Color(0xFF1B5E20),
warning: Color(0xFFFF9800),
onWarning: Color(0xFF000000),
warningContainer: Color(0xFFFFE0B2),
onWarningContainer: Color(0xFFE65100),
info: Color(0xFF2196F3),
onInfo: Color(0xFFFFFFFF),
infoContainer: Color(0xFFBBDEFB),
onInfoContainer: Color(0xFF0D47A1),
);
/// Dark theme extension
static const AppColorsExtension dark = AppColorsExtension(
success: Color(0xFF66BB6A),
onSuccess: Color(0xFF1B5E20),
successContainer: Color(0xFF2E7D32),
onSuccessContainer: Color(0xFFC8E6C9),
warning: Color(0xFFFFB74D),
onWarning: Color(0xFFE65100),
warningContainer: Color(0xFFF57C00),
onWarningContainer: Color(0xFFFFE0B2),
info: Color(0xFF42A5F5),
onInfo: Color(0xFF0D47A1),
infoContainer: Color(0xFF1976D2),
onInfoContainer: Color(0xFFBBDEFB),
);
}

19
lib/core/theme/theme.dart Normal file
View File

@@ -0,0 +1,19 @@
// Theme system barrel file for easy imports
//
// This file exports all theme-related components for the Material 3 design system.
// Import this file to get access to all theme utilities, colors, typography, and spacing.
// Core theme configuration
export 'app_theme.dart';
// Color system
export 'app_colors.dart';
// Typography system
export 'app_typography.dart';
// Spacing and layout system
export 'app_spacing.dart';
// Theme widgets
export 'widgets/theme_mode_switch.dart';

View File

@@ -0,0 +1,398 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'theme.dart';
/// Theme showcase page demonstrating Material 3 design system
class ThemeShowcasePage extends ConsumerWidget {
/// Creates a theme showcase page
const ThemeShowcasePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final textTheme = theme.textTheme;
return Scaffold(
appBar: AppBar(
title: const Text('Material 3 Theme Showcase'),
actions: const [
ThemeToggleIconButton(),
SizedBox(width: 16),
],
),
body: SingleChildScrollView(
padding: AppSpacing.responsivePadding(context),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Theme Mode Switch Section
_buildSection(
context,
'Theme Mode Switch',
Column(
children: [
const ThemeModeSwitch(
style: ThemeSwitchStyle.segmented,
showLabel: true,
),
AppSpacing.verticalSpaceLG,
const AnimatedThemeModeSwitch(),
AppSpacing.verticalSpaceLG,
const ThemeModeSwitch(
style: ThemeSwitchStyle.toggle,
showLabel: true,
labelText: 'Dark Mode',
),
],
),
),
// Typography Section
_buildSection(
context,
'Typography',
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Display Large', style: textTheme.displayLarge),
AppSpacing.verticalSpaceSM,
Text('Display Medium', style: textTheme.displayMedium),
AppSpacing.verticalSpaceSM,
Text('Display Small', style: textTheme.displaySmall),
AppSpacing.verticalSpaceMD,
Text('Headline Large', style: textTheme.headlineLarge),
AppSpacing.verticalSpaceXS,
Text('Headline Medium', style: textTheme.headlineMedium),
AppSpacing.verticalSpaceXS,
Text('Headline Small', style: textTheme.headlineSmall),
AppSpacing.verticalSpaceMD,
Text('Title Large', style: textTheme.titleLarge),
AppSpacing.verticalSpaceXS,
Text('Title Medium', style: textTheme.titleMedium),
AppSpacing.verticalSpaceXS,
Text('Title Small', style: textTheme.titleSmall),
AppSpacing.verticalSpaceMD,
Text('Body Large', style: textTheme.bodyLarge),
AppSpacing.verticalSpaceXS,
Text('Body Medium', style: textTheme.bodyMedium),
AppSpacing.verticalSpaceXS,
Text('Body Small', style: textTheme.bodySmall),
AppSpacing.verticalSpaceMD,
Text('Label Large', style: textTheme.labelLarge),
AppSpacing.verticalSpaceXS,
Text('Label Medium', style: textTheme.labelMedium),
AppSpacing.verticalSpaceXS,
Text('Label Small', style: textTheme.labelSmall),
],
),
),
// Color Palette Section
_buildSection(
context,
'Color Palette',
Column(
children: [
_buildColorRow(
context,
'Primary',
colorScheme.primary,
colorScheme.onPrimary,
),
_buildColorRow(
context,
'Primary Container',
colorScheme.primaryContainer,
colorScheme.onPrimaryContainer,
),
_buildColorRow(
context,
'Secondary',
colorScheme.secondary,
colorScheme.onSecondary,
),
_buildColorRow(
context,
'Secondary Container',
colorScheme.secondaryContainer,
colorScheme.onSecondaryContainer,
),
_buildColorRow(
context,
'Tertiary',
colorScheme.tertiary,
colorScheme.onTertiary,
),
_buildColorRow(
context,
'Error',
colorScheme.error,
colorScheme.onError,
),
_buildColorRow(
context,
'Surface',
colorScheme.surface,
colorScheme.onSurface,
),
],
),
),
// Buttons Section
_buildSection(
context,
'Buttons',
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FilledButton(
onPressed: () {},
child: const Text('Filled Button'),
),
AppSpacing.verticalSpaceSM,
ElevatedButton(
onPressed: () {},
child: const Text('Elevated Button'),
),
AppSpacing.verticalSpaceSM,
OutlinedButton(
onPressed: () {},
child: const Text('Outlined Button'),
),
AppSpacing.verticalSpaceSM,
TextButton(
onPressed: () {},
child: const Text('Text Button'),
),
AppSpacing.verticalSpaceSM,
Row(
children: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.favorite),
),
AppSpacing.horizontalSpaceSM,
IconButton.filled(
onPressed: () {},
icon: const Icon(Icons.favorite),
),
AppSpacing.horizontalSpaceSM,
IconButton.outlined(
onPressed: () {},
icon: const Icon(Icons.favorite),
),
],
),
],
),
),
// Form Components Section
_buildSection(
context,
'Form Components',
Column(
children: [
const TextField(
decoration: InputDecoration(
labelText: 'Label',
hintText: 'Hint text',
helperText: 'Helper text',
),
),
AppSpacing.verticalSpaceLG,
const TextField(
decoration: InputDecoration(
labelText: 'Error state',
errorText: 'Error message',
prefixIcon: Icon(Icons.error),
),
),
AppSpacing.verticalSpaceLG,
Row(
children: [
Checkbox(
value: true,
onChanged: (value) {},
),
const Text('Checkbox'),
AppSpacing.horizontalSpaceLG,
Radio<bool>(
value: true,
groupValue: true,
onChanged: null,
),
const Text('Radio'),
const Spacer(),
Switch(
value: true,
onChanged: (value) {},
),
],
),
],
),
),
// Cards Section
_buildSection(
context,
'Cards',
Column(
children: [
Card(
child: Padding(
padding: AppSpacing.paddingLG,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Card Title',
style: theme.textTheme.titleMedium,
),
AppSpacing.verticalSpaceSM,
Text(
'Card content with some descriptive text that demonstrates how cards look in the Material 3 design system.',
style: theme.textTheme.bodyMedium,
),
AppSpacing.verticalSpaceSM,
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {},
child: const Text('Action'),
),
],
),
],
),
),
),
AppSpacing.verticalSpaceSM,
Card.filled(
child: Padding(
padding: AppSpacing.paddingLG,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Filled Card',
style: theme.textTheme.titleMedium,
),
AppSpacing.verticalSpaceSM,
Text(
'This is a filled card variant.',
style: theme.textTheme.bodyMedium,
),
],
),
),
),
],
),
),
// Spacing Demonstration
_buildSection(
context,
'Spacing System',
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('XS (4dp)', style: theme.textTheme.bodyMedium),
Container(
height: 2,
width: AppSpacing.xs,
color: colorScheme.primary,
),
AppSpacing.verticalSpaceSM,
Text('SM (8dp)', style: theme.textTheme.bodyMedium),
Container(
height: 2,
width: AppSpacing.sm,
color: colorScheme.primary,
),
AppSpacing.verticalSpaceSM,
Text('MD (12dp)', style: theme.textTheme.bodyMedium),
Container(
height: 2,
width: AppSpacing.md,
color: colorScheme.primary,
),
AppSpacing.verticalSpaceSM,
Text('LG (16dp)', style: theme.textTheme.bodyMedium),
Container(
height: 2,
width: AppSpacing.lg,
color: colorScheme.primary,
),
AppSpacing.verticalSpaceSM,
Text('XL (20dp)', style: theme.textTheme.bodyMedium),
Container(
height: 2,
width: AppSpacing.xl,
color: colorScheme.primary,
),
AppSpacing.verticalSpaceSM,
Text('XXL (24dp)', style: theme.textTheme.bodyMedium),
Container(
height: 2,
width: AppSpacing.xxl,
color: colorScheme.primary,
),
],
),
),
],
),
),
);
}
Widget _buildSection(BuildContext context, String title, Widget content) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineSmall,
),
AppSpacing.verticalSpaceLG,
content,
AppSpacing.verticalSpaceXXL,
],
);
}
Widget _buildColorRow(
BuildContext context,
String label,
Color color,
Color onColor,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: AppSpacing.xs),
child: Container(
height: 48,
decoration: BoxDecoration(
color: color,
borderRadius: AppSpacing.radiusSM,
border: Border.all(
color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3),
),
),
child: Center(
child: Text(
label,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: onColor,
),
),
),
),
);
}
}

View File

@@ -0,0 +1,593 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../app_spacing.dart';
import '../app_typography.dart';
/// Theme mode switch widget with Material 3 design
class ThemeModeSwitch extends ConsumerWidget {
/// Creates a theme mode switch
const ThemeModeSwitch({
super.key,
this.showLabel = true,
this.labelText,
this.onChanged,
this.size = ThemeSwitchSize.medium,
this.style = ThemeSwitchStyle.toggle,
});
/// Whether to show the label
final bool showLabel;
/// Custom label text
final String? labelText;
/// Callback when theme mode changes
final ValueChanged<ThemeMode>? onChanged;
/// Size of the switch
final ThemeSwitchSize size;
/// Style of the switch
final ThemeSwitchStyle style;
@override
Widget build(BuildContext context, WidgetRef ref) {
// For now, we'll use a simple state provider
// In a real app, this would be connected to your theme provider
final themeMode = ref.watch(_themeModeProvider);
final theme = Theme.of(context);
switch (style) {
case ThemeSwitchStyle.toggle:
return _buildToggleSwitch(context, theme, themeMode, ref);
case ThemeSwitchStyle.segmented:
return _buildSegmentedSwitch(context, theme, themeMode, ref);
case ThemeSwitchStyle.radio:
return _buildRadioSwitch(context, theme, themeMode, ref);
case ThemeSwitchStyle.dropdown:
return _buildDropdownSwitch(context, theme, themeMode, ref);
}
}
Widget _buildToggleSwitch(
BuildContext context,
ThemeData theme,
ThemeMode themeMode,
WidgetRef ref,
) {
final isDark = themeMode == ThemeMode.dark;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (showLabel) ...[
Icon(
Icons.light_mode,
size: _getIconSize(),
color: isDark ? theme.colorScheme.onSurface.withValues(alpha: 0.6) : theme.colorScheme.primary,
),
AppSpacing.horizontalSpaceSM,
],
Switch(
value: isDark,
onChanged: (value) {
final newMode = value ? ThemeMode.dark : ThemeMode.light;
ref.read(_themeModeProvider.notifier).state = newMode;
onChanged?.call(newMode);
},
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return Icon(
Icons.dark_mode,
size: _getIconSize() * 0.7,
);
}
return Icon(
Icons.light_mode,
size: _getIconSize() * 0.7,
);
},
),
),
if (showLabel) ...[
AppSpacing.horizontalSpaceSM,
Icon(
Icons.dark_mode,
size: _getIconSize(),
color: isDark ? theme.colorScheme.primary : theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
],
if (showLabel && labelText != null) ...[
AppSpacing.horizontalSpaceSM,
Text(
labelText!,
style: _getLabelStyle(),
),
],
],
);
}
Widget _buildSegmentedSwitch(
BuildContext context,
ThemeData theme,
ThemeMode themeMode,
WidgetRef ref,
) {
return SegmentedButton<ThemeMode>(
segments: [
ButtonSegment<ThemeMode>(
value: ThemeMode.light,
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.light_mode, size: _getIconSize()),
if (showLabel) ...[
AppSpacing.horizontalSpaceXS,
Text('Light', style: _getLabelStyle()),
],
],
),
),
ButtonSegment<ThemeMode>(
value: ThemeMode.system,
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.brightness_auto, size: _getIconSize()),
if (showLabel) ...[
AppSpacing.horizontalSpaceXS,
Text('Auto', style: _getLabelStyle()),
],
],
),
),
ButtonSegment<ThemeMode>(
value: ThemeMode.dark,
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.dark_mode, size: _getIconSize()),
if (showLabel) ...[
AppSpacing.horizontalSpaceXS,
Text('Dark', style: _getLabelStyle()),
],
],
),
),
],
selected: {themeMode},
onSelectionChanged: (Set<ThemeMode> selection) {
final newMode = selection.first;
ref.read(_themeModeProvider.notifier).state = newMode;
onChanged?.call(newMode);
},
);
}
Widget _buildRadioSwitch(
BuildContext context,
ThemeData theme,
ThemeMode themeMode,
WidgetRef ref,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (showLabel && labelText != null)
Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
child: Text(
labelText!,
style: theme.textTheme.titleMedium,
),
),
...ThemeMode.values.map((mode) {
return ListTile(
contentPadding: EdgeInsets.zero,
dense: size == ThemeSwitchSize.small,
leading: Radio<ThemeMode>(
value: mode,
groupValue: themeMode,
onChanged: (ThemeMode? value) {
if (value != null) {
ref.read(_themeModeProvider.notifier).state = value;
onChanged?.call(value);
}
},
),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_getThemeModeIcon(mode), size: _getIconSize()),
AppSpacing.horizontalSpaceSM,
Text(_getThemeModeLabel(mode), style: _getLabelStyle()),
],
),
onTap: () {
ref.read(_themeModeProvider.notifier).state = mode;
onChanged?.call(mode);
},
);
}),
],
);
}
Widget _buildDropdownSwitch(
BuildContext context,
ThemeData theme,
ThemeMode themeMode,
WidgetRef ref,
) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (showLabel && labelText != null) ...[
Text(
labelText!,
style: theme.textTheme.titleMedium,
),
AppSpacing.horizontalSpaceSM,
],
DropdownButton<ThemeMode>(
value: themeMode,
onChanged: (ThemeMode? value) {
if (value != null) {
ref.read(_themeModeProvider.notifier).state = value;
onChanged?.call(value);
}
},
items: ThemeMode.values.map((mode) {
return DropdownMenuItem<ThemeMode>(
value: mode,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_getThemeModeIcon(mode), size: _getIconSize()),
AppSpacing.horizontalSpaceSM,
Text(_getThemeModeLabel(mode), style: _getLabelStyle()),
],
),
);
}).toList(),
underline: const SizedBox(),
),
],
);
}
double _getIconSize() {
switch (size) {
case ThemeSwitchSize.small:
return AppSpacing.iconSM;
case ThemeSwitchSize.medium:
return AppSpacing.iconMD;
case ThemeSwitchSize.large:
return AppSpacing.iconLG;
}
}
TextStyle _getLabelStyle() {
switch (size) {
case ThemeSwitchSize.small:
return AppTypography.labelSmall;
case ThemeSwitchSize.medium:
return AppTypography.labelMedium;
case ThemeSwitchSize.large:
return AppTypography.labelLarge;
}
}
IconData _getThemeModeIcon(ThemeMode mode) {
switch (mode) {
case ThemeMode.light:
return Icons.light_mode;
case ThemeMode.dark:
return Icons.dark_mode;
case ThemeMode.system:
return Icons.brightness_auto;
}
}
String _getThemeModeLabel(ThemeMode mode) {
switch (mode) {
case ThemeMode.light:
return 'Light';
case ThemeMode.dark:
return 'Dark';
case ThemeMode.system:
return 'System';
}
}
}
/// Theme switch sizes
enum ThemeSwitchSize {
small,
medium,
large,
}
/// Theme switch styles
enum ThemeSwitchStyle {
toggle,
segmented,
radio,
dropdown,
}
/// Animated theme mode switch with smooth transitions
class AnimatedThemeModeSwitch extends ConsumerStatefulWidget {
/// Creates an animated theme mode switch
const AnimatedThemeModeSwitch({
super.key,
this.duration = const Duration(milliseconds: 300),
this.curve = Curves.easeInOut,
this.showIcon = true,
this.iconSize = 24.0,
});
/// Animation duration
final Duration duration;
/// Animation curve
final Curve curve;
/// Whether to show the theme icon
final bool showIcon;
/// Size of the theme icon
final double iconSize;
@override
ConsumerState<AnimatedThemeModeSwitch> createState() => _AnimatedThemeModeSwitchState();
}
class _AnimatedThemeModeSwitchState extends ConsumerState<AnimatedThemeModeSwitch>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: widget.curve,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final themeMode = ref.watch(_themeModeProvider);
final isDark = themeMode == ThemeMode.dark;
final theme = Theme.of(context);
// Update animation based on theme mode
if (isDark) {
_controller.forward();
} else {
_controller.reverse();
}
return GestureDetector(
onTap: () {
final newMode = isDark ? ThemeMode.light : ThemeMode.dark;
ref.read(_themeModeProvider.notifier).state = newMode;
},
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: widget.iconSize * 2.5,
height: widget.iconSize * 1.4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.iconSize * 0.7),
color: Color.lerp(
theme.colorScheme.surfaceContainerHighest,
theme.colorScheme.inverseSurface,
_animation.value,
),
border: Border.all(
color: theme.colorScheme.outline.withValues(alpha: 0.3),
width: 1,
),
),
child: Stack(
children: [
// Sun icon
AnimatedPositioned(
duration: widget.duration,
curve: widget.curve,
left: isDark ? -widget.iconSize : widget.iconSize * 0.2,
top: widget.iconSize * 0.2,
child: AnimatedOpacity(
duration: widget.duration,
opacity: isDark ? 0.0 : 1.0,
child: Icon(
Icons.light_mode,
size: widget.iconSize,
color: theme.colorScheme.primary,
),
),
),
// Moon icon
AnimatedPositioned(
duration: widget.duration,
curve: widget.curve,
left: isDark ? widget.iconSize * 1.3 : widget.iconSize * 2.5,
top: widget.iconSize * 0.2,
child: AnimatedOpacity(
duration: widget.duration,
opacity: isDark ? 1.0 : 0.0,
child: Icon(
Icons.dark_mode,
size: widget.iconSize,
color: theme.colorScheme.onInverseSurface,
),
),
),
// Sliding thumb
AnimatedPositioned(
duration: widget.duration,
curve: widget.curve,
left: isDark ? widget.iconSize * 1.1 : widget.iconSize * 0.1,
top: widget.iconSize * 0.1,
child: Container(
width: widget.iconSize * 1.2,
height: widget.iconSize * 1.2,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: theme.colorScheme.surface,
boxShadow: [
BoxShadow(
color: theme.colorScheme.shadow.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
),
),
],
),
);
},
),
);
}
}
/// Simple theme toggle icon button
class ThemeToggleIconButton extends ConsumerWidget {
/// Creates a theme toggle icon button
const ThemeToggleIconButton({
super.key,
this.tooltip,
this.iconSize,
this.onPressed,
});
/// Tooltip text
final String? tooltip;
/// Icon size
final double? iconSize;
/// Custom callback
final VoidCallback? onPressed;
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(_themeModeProvider);
final isDark = themeMode == ThemeMode.dark;
return IconButton(
onPressed: onPressed ??
() {
final newMode = isDark ? ThemeMode.light : ThemeMode.dark;
ref.read(_themeModeProvider.notifier).state = newMode;
},
icon: AnimatedSwitcher(
duration: AppSpacing.animationNormal,
transitionBuilder: (Widget child, Animation<double> animation) {
return RotationTransition(turns: animation, child: child);
},
child: Icon(
isDark ? Icons.dark_mode : Icons.light_mode,
key: ValueKey(isDark),
size: iconSize,
),
),
tooltip: tooltip ?? (isDark ? 'Switch to light mode' : 'Switch to dark mode'),
);
}
}
/// Theme mode provider - replace with your actual theme provider
final _themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
/// Theme mode controller for managing theme state
class ThemeModeController extends StateNotifier<ThemeMode> {
ThemeModeController() : super(ThemeMode.system);
/// Switch to light mode
void setLightMode() => state = ThemeMode.light;
/// Switch to dark mode
void setDarkMode() => state = ThemeMode.dark;
/// Switch to system mode
void setSystemMode() => state = ThemeMode.system;
/// Toggle between light and dark (ignoring system)
void toggle() {
state = state == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
}
/// Check if current mode is dark
bool get isDark => state == ThemeMode.dark;
/// Check if current mode is light
bool get isLight => state == ThemeMode.light;
/// Check if current mode is system
bool get isSystem => state == ThemeMode.system;
}
/// Provider for theme mode controller
final themeModeControllerProvider = StateNotifierProvider<ThemeModeController, ThemeMode>((ref) {
return ThemeModeController();
});
/// Utility methods for theme mode
extension ThemeModeExtensions on ThemeMode {
/// Get the display name for the theme mode
String get displayName {
switch (this) {
case ThemeMode.light:
return 'Light';
case ThemeMode.dark:
return 'Dark';
case ThemeMode.system:
return 'System';
}
}
/// Get the icon for the theme mode
IconData get icon {
switch (this) {
case ThemeMode.light:
return Icons.light_mode;
case ThemeMode.dark:
return Icons.dark_mode;
case ThemeMode.system:
return Icons.brightness_auto;
}
}
/// Get the description for the theme mode
String get description {
switch (this) {
case ThemeMode.light:
return 'Use light theme';
case ThemeMode.dark:
return 'Use dark theme';
case ThemeMode.system:
return 'Follow system setting';
}
}
}