- Adjusted brand primary colors in the AppTheme to enhance contrast in light and dark modes, ensuring compliance with WCAG standards. - Updated various theme elements, including chat bubbles and input borders, to utilize the new brand colors for better visual consistency and accessibility. - This refactor improves the overall user experience by providing a more vibrant and accessible color palette.
430 lines
15 KiB
Dart
430 lines
15 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter_animate/flutter_animate.dart';
|
|
import 'theme_extensions.dart';
|
|
|
|
class AppTheme {
|
|
// Brand accents tuned for WCAG contrast in light/dark modes
|
|
static const Color brandPrimary = Color(0xFFA420FF); // Light mode primary
|
|
static const Color brandPrimaryLight = Color(0xFFC773FF); // Light accents
|
|
static const Color brandPrimaryDark = Color(0xFF9500FF); // Dark mode primary
|
|
|
|
// Enhanced neutral palette for better contrast (WCAG AA compliant)
|
|
static const Color neutral900 = Color(0xFF000000); // Pure black
|
|
static const Color neutral800 = Color(
|
|
0xFF0D0D0D,
|
|
); // Darker for better contrast
|
|
static const Color neutral700 = Color(0xFF1A1A1A);
|
|
static const Color neutral600 = Color(0xFF2D2D2D); // Improved contrast
|
|
static const Color neutral500 = Color(0xFF404040); // Better middle gray
|
|
static const Color neutral400 = Color(0xFF525252);
|
|
static const Color neutral300 = Color(0xFF6B6B6B); // Improved contrast ratio
|
|
static const Color neutral200 = Color(0xFF9E9E9E); // Better readability
|
|
static const Color neutral100 = Color(0xFFD1D1D1); // Enhanced contrast
|
|
static const Color neutral50 = Color(
|
|
0xFFF8F8F8,
|
|
); // Softer white for reduced eye strain
|
|
|
|
// Enhanced semantic colors for WCAG AA compliance
|
|
static const Color error = Color(0xFFDC2626); // Improved red contrast
|
|
static const Color errorDark = Color(0xFFB91C1C); // Darker red for dark theme
|
|
static const Color success = Color(0xFF059669); // Better green contrast
|
|
static const Color successDark = Color(0xFF047857); // Dark theme green
|
|
static const Color warning = Color(0xFFD97706); // Improved orange contrast
|
|
static const Color warningDark = Color(0xFFB45309); // Dark theme orange
|
|
static const Color info = Color(0xFF0284C7); // Better blue contrast
|
|
static const Color infoDark = Color(0xFF0369A1); // Dark theme blue
|
|
|
|
// Brand aliases
|
|
static const Color primaryColor = brandPrimary;
|
|
static const Color secondaryColor = brandPrimaryLight;
|
|
static const Color surfaceColor = neutral50;
|
|
static const Color errorColor = error;
|
|
static const Color successColor = success;
|
|
|
|
// Base Light Theme
|
|
static ThemeData lightTheme = ThemeData(
|
|
useMaterial3: true,
|
|
brightness: Brightness.light,
|
|
colorScheme: const ColorScheme.light(
|
|
primary: brandPrimary,
|
|
secondary: brandPrimaryLight,
|
|
surface: surfaceColor,
|
|
error: errorColor,
|
|
),
|
|
pageTransitionsTheme: const PageTransitionsTheme(
|
|
builders: <TargetPlatform, PageTransitionsBuilder>{
|
|
TargetPlatform.android: ZoomPageTransitionsBuilder(),
|
|
TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
|
|
TargetPlatform.linux: ZoomPageTransitionsBuilder(),
|
|
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
|
|
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
|
},
|
|
),
|
|
splashFactory: NoSplash.splashFactory,
|
|
appBarTheme: const AppBarTheme(
|
|
centerTitle: true,
|
|
elevation: Elevation.none,
|
|
backgroundColor: Colors.transparent,
|
|
foregroundColor: neutral900,
|
|
),
|
|
bottomSheetTheme: BottomSheetThemeData(
|
|
backgroundColor: neutral50,
|
|
modalBackgroundColor: neutral50,
|
|
surfaceTintColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.modal),
|
|
),
|
|
showDragHandle: false,
|
|
),
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: Spacing.lg,
|
|
vertical: Spacing.xs,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
),
|
|
),
|
|
),
|
|
cardTheme: CardThemeData(
|
|
elevation: Elevation.none,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
|
|
side: BorderSide(color: neutral200),
|
|
),
|
|
),
|
|
snackBarTheme: SnackBarThemeData(
|
|
behavior: SnackBarBehavior.floating,
|
|
backgroundColor: neutral900.withValues(alpha: 0.92),
|
|
contentTextStyle: const TextStyle(
|
|
color: neutral50,
|
|
).copyWith(fontSize: AppTypography.bodyMedium),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.snackbar),
|
|
),
|
|
elevation: Elevation.high,
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: neutral50,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: const BorderSide(color: primaryColor, width: 2),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: const BorderSide(color: errorColor, width: 1),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: Spacing.md,
|
|
vertical: Spacing.sm,
|
|
),
|
|
),
|
|
// Use platform default system font text theme
|
|
textTheme: ThemeData.light().textTheme,
|
|
extensions: const [ConduitThemeExtension.light],
|
|
);
|
|
|
|
// Base Dark Theme
|
|
static ThemeData darkTheme = ThemeData(
|
|
useMaterial3: true,
|
|
brightness: Brightness.dark,
|
|
scaffoldBackgroundColor: Color(0xFF0A0D0C),
|
|
colorScheme: const ColorScheme.dark(
|
|
primary: brandPrimaryDark,
|
|
secondary: brandPrimary,
|
|
surface: Color(0xFF0A0D0C),
|
|
surfaceContainerHighest: neutral700,
|
|
onSurface: neutral50,
|
|
onSurfaceVariant: neutral300,
|
|
outline: neutral600,
|
|
error: error,
|
|
),
|
|
pageTransitionsTheme: const PageTransitionsTheme(
|
|
builders: <TargetPlatform, PageTransitionsBuilder>{
|
|
TargetPlatform.android: ZoomPageTransitionsBuilder(),
|
|
TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
|
|
TargetPlatform.linux: ZoomPageTransitionsBuilder(),
|
|
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
|
|
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
|
},
|
|
),
|
|
splashFactory: NoSplash.splashFactory,
|
|
appBarTheme: const AppBarTheme(
|
|
centerTitle: true,
|
|
elevation: Elevation.none,
|
|
backgroundColor: Colors.transparent,
|
|
foregroundColor: neutral50,
|
|
),
|
|
bottomSheetTheme: BottomSheetThemeData(
|
|
backgroundColor: neutral900,
|
|
modalBackgroundColor: neutral900,
|
|
surfaceTintColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.modal),
|
|
),
|
|
showDragHandle: false,
|
|
),
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: Spacing.lg,
|
|
vertical: Spacing.xs,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
),
|
|
),
|
|
),
|
|
cardTheme: CardThemeData(
|
|
elevation: Elevation.none,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
|
|
side: BorderSide(color: neutral800),
|
|
),
|
|
),
|
|
snackBarTheme: SnackBarThemeData(
|
|
behavior: SnackBarBehavior.floating,
|
|
backgroundColor: neutral800.withValues(alpha: 0.92),
|
|
contentTextStyle: const TextStyle(
|
|
color: neutral50,
|
|
).copyWith(fontSize: AppTypography.bodyMedium),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.snackbar),
|
|
),
|
|
elevation: Elevation.high,
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: neutral700,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: const BorderSide(color: neutral600, width: 1),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: const BorderSide(color: neutral600, width: 1),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: const BorderSide(color: brandPrimaryDark, width: 2),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: const BorderSide(color: error, width: 1),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: Spacing.md,
|
|
vertical: Spacing.sm,
|
|
),
|
|
),
|
|
// Use platform default system font text theme
|
|
textTheme: ThemeData.dark().textTheme,
|
|
extensions: const [ConduitThemeExtension.dark],
|
|
);
|
|
|
|
// Conduit variants using brand colors
|
|
static ThemeData conduitLightTheme = lightTheme.copyWith(
|
|
colorScheme: lightTheme.colorScheme.copyWith(
|
|
primary: brandPrimary,
|
|
secondary: brandPrimaryLight,
|
|
surface: neutral50,
|
|
),
|
|
extensions: const [ConduitThemeExtension.light],
|
|
appBarTheme: const AppBarTheme(
|
|
centerTitle: true,
|
|
elevation: Elevation.none,
|
|
backgroundColor: Colors.transparent,
|
|
foregroundColor: neutral800,
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: neutral50,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: const BorderSide(color: brandPrimary, width: 2),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: const BorderSide(color: error, width: 1),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: Spacing.md,
|
|
vertical: Spacing.sm,
|
|
),
|
|
),
|
|
);
|
|
|
|
static ThemeData conduitDarkTheme = darkTheme.copyWith(
|
|
scaffoldBackgroundColor: const Color(0xFF0A0D0C),
|
|
colorScheme: darkTheme.colorScheme.copyWith(
|
|
primary: brandPrimaryDark,
|
|
secondary: brandPrimary,
|
|
surface: const Color(0xFF0A0D0C),
|
|
surfaceContainerHighest: neutral700,
|
|
),
|
|
extensions: const [ConduitThemeExtension.dark],
|
|
appBarTheme: const AppBarTheme(
|
|
centerTitle: true,
|
|
elevation: Elevation.none,
|
|
backgroundColor: Colors.transparent,
|
|
foregroundColor: neutral50,
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: neutral700,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: const BorderSide(color: neutral600, width: 1),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: const BorderSide(color: neutral600, width: 1),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: const BorderSide(color: brandPrimaryDark, width: 2),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
borderSide: const BorderSide(color: error, width: 1),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: Spacing.md,
|
|
vertical: Spacing.sm,
|
|
),
|
|
),
|
|
);
|
|
|
|
// Classic Conduit variants for runtime switching
|
|
// Removed classic Conduit variants from public API to keep Aurora only
|
|
|
|
// Platform-specific theming helpers
|
|
static CupertinoThemeData cupertinoTheme(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
return CupertinoThemeData(
|
|
brightness: isDark ? Brightness.dark : Brightness.light,
|
|
primaryColor: isDark ? brandPrimaryDark : brandPrimary,
|
|
scaffoldBackgroundColor: isDark ? neutral900 : neutral50,
|
|
barBackgroundColor: isDark ? neutral900 : neutral50,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Animated theme wrapper for smooth theme transitions
|
|
class AnimatedThemeWrapper extends StatefulWidget {
|
|
final Widget child;
|
|
final ThemeData theme;
|
|
final Duration duration;
|
|
|
|
const AnimatedThemeWrapper({
|
|
super.key,
|
|
required this.child,
|
|
required this.theme,
|
|
this.duration = const Duration(milliseconds: 250),
|
|
});
|
|
|
|
@override
|
|
State<AnimatedThemeWrapper> createState() => _AnimatedThemeWrapperState();
|
|
}
|
|
|
|
class _AnimatedThemeWrapperState extends State<AnimatedThemeWrapper>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _animation;
|
|
ThemeData? _previousTheme;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(duration: widget.duration, vsync: this);
|
|
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
|
|
_previousTheme = widget.theme;
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(AnimatedThemeWrapper oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.theme != widget.theme) {
|
|
_previousTheme = oldWidget.theme;
|
|
_controller.forward(from: 0);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void deactivate() {
|
|
// Pause animations during deactivation to avoid rebuilds in wrong build scope
|
|
_controller.stop();
|
|
super.deactivate();
|
|
}
|
|
|
|
@override
|
|
void activate() {
|
|
super.activate();
|
|
// If a theme transition was in progress, resume it
|
|
if (_controller.value < 1.0 && !_controller.isAnimating) {
|
|
_controller.forward();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
return Theme(
|
|
data: ThemeData.lerp(
|
|
_previousTheme ?? widget.theme,
|
|
widget.theme,
|
|
_animation.value,
|
|
),
|
|
child: widget.child,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Theme transition widget for individual components
|
|
class ThemeTransition extends StatelessWidget {
|
|
final Widget child;
|
|
final Duration duration;
|
|
|
|
const ThemeTransition({
|
|
super.key,
|
|
required this.child,
|
|
this.duration = const Duration(milliseconds: 200),
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return child.animate().fadeIn(duration: duration);
|
|
}
|
|
}
|
|
|
|
// Typography, spacing, and design token classes are now in theme_extensions.dart for consistency
|