init cc
This commit is contained in:
288
lib/core/theme/README.md
Normal file
288
lib/core/theme/README.md
Normal 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.
|
||||
148
lib/core/theme/app_colors.dart
Normal file
148
lib/core/theme/app_colors.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
229
lib/core/theme/app_spacing.dart
Normal file
229
lib/core/theme/app_spacing.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
509
lib/core/theme/app_theme.dart
Normal file
509
lib/core/theme/app_theme.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
381
lib/core/theme/app_typography.dart
Normal file
381
lib/core/theme/app_typography.dart
Normal 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
19
lib/core/theme/theme.dart
Normal 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';
|
||||
398
lib/core/theme/theme_showcase.dart
Normal file
398
lib/core/theme/theme_showcase.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
593
lib/core/theme/widgets/theme_mode_switch.dart
Normal file
593
lib/core/theme/widgets/theme_mode_switch.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user