diff --git a/assets/lang/en.json b/assets/lang/en.json index 2c6658f..6bb1e6a 100644 --- a/assets/lang/en.json +++ b/assets/lang/en.json @@ -78,5 +78,23 @@ "settings.theme.light": "Light", "settings.theme.dark": "Dark", "lang.en": "English", - "lang.ms": "Bahasa Malaysia" + "lang.ms": "Bahasa Malaysia", + "welcome.title": "Spot it. Snap it. Fix it.", + "welcome.subtitle": "Report city issues in seconds—help crews act faster.", + "cta.continueGuest": "Continue as guest", + "cta.signIn": "Sign in", + "cta.skip": "Skip", + "cta.next": "Next", + "cta.getStarted": "Get started", + "onboarding.header": "Welcome to FixMate", + "onboarding.title1": "Fast capture", + "onboarding.body1": "Take a photo and submit in under a minute.", + "onboarding.title2": "Map clarity", + "onboarding.body2": "See issues around you with smart clustering.", + "onboarding.title3": "Track progress", + "onboarding.body3": "Follow status updates as work gets done.", + "auth.title": "Sign in", + "auth.signInWithApple": "Sign in with Apple", + "auth.signInWithGoogle": "Sign in with Google", + "auth.comingSoon": "Coming soon" } \ No newline at end of file diff --git a/assets/lang/ms.json b/assets/lang/ms.json index 72c1bea..f7e63da 100644 --- a/assets/lang/ms.json +++ b/assets/lang/ms.json @@ -78,5 +78,23 @@ "settings.theme.light": "Terang", "settings.theme.dark": "Gelap", "lang.en": "English", - "lang.ms": "Bahasa Malaysia" + "lang.ms": "Bahasa Malaysia", + "welcome.title": "Nampak. Tangkap. Baiki.", + "welcome.subtitle": "Lapor isu bandar dalam beberapa saat — bantu pasukan bertindak lebih pantas.", + "cta.continueGuest": "Teruskan sebagai tetamu", + "cta.signIn": "Log masuk", + "cta.skip": "Langkau", + "cta.next": "Seterusnya", + "cta.getStarted": "Mula", + "onboarding.header": "Selamat datang ke FixMate", + "onboarding.title1": "Tangkap pantas", + "onboarding.body1": "Ambil gambar dan hantar dalam kurang satu minit.", + "onboarding.title2": "Peta yang jelas", + "onboarding.body2": "Lihat isu di sekitar anda dengan pengelompokan pintar.", + "onboarding.title3": "Jejak kemajuan", + "onboarding.body3": "Ikuti kemas kini status apabila kerja disiapkan.", + "auth.title": "Log masuk", + "auth.signInWithApple": "Log masuk dengan Apple", + "auth.signInWithGoogle": "Log masuk dengan Google", + "auth.comingSoon": "Akan datang" } \ No newline at end of file diff --git a/assets/tokens/design-tokens.json b/assets/tokens/design-tokens.json new file mode 100644 index 0000000..79acd18 --- /dev/null +++ b/assets/tokens/design-tokens.json @@ -0,0 +1,174 @@ +{ + "meta": { + "name": "FixMate Design Tokens", + "version": "1.0.0", + "brand": "Civic Premium – Citizen First" + }, + "color": { + "palette": { + "brandPrimary": "#1E5CE0", + "brandPrimaryDark": "#1748AC", + "brandPrimaryLight": "#8CB0FF", + "neutral0": "#FFFFFF", + "neutral10": "#F5F7FB", + "neutral20": "#E9EDF5", + "neutral30": "#D8DFEB", + "neutral40": "#B8C2D5", + "neutral50": "#99A3B8", + "neutral60": "#7A859E", + "neutral70": "#5C6782", + "neutral80": "#3D4765", + "neutral90": "#21283C", + "neutral100": "#0E1322", + "success": "#2E7D32", + "warning": "#ED6C02", + "error": "#D32F2F", + "info": "#0288D1" + }, + "semantic": { + "primary": "{color.palette.brandPrimary}", + "primaryDark": "{color.palette.brandPrimaryDark}", + "primaryLight": "{color.palette.brandPrimaryLight}", + "background": "{color.palette.neutral10}", + "surface": "{color.palette.neutral0}", + "surfaceVariant": "{color.palette.neutral20}", + "onPrimary": "#FFFFFF", + "onBackground": "{color.palette.neutral100}", + "onSurface": "{color.palette.neutral100}", + "outline": "{color.palette.neutral30}", + "success": "{color.palette.success}", + "onSuccess": "#FFFFFF", + "warning": "{color.palette.warning}", + "onWarning": "#FFFFFF", + "error": "{color.palette.error}", + "onError": "#FFFFFF", + "info": "{color.palette.info}", + "onInfo": "#FFFFFF" + } + }, + "typography": { + "families": { + "display": "SF Pro Display, Inter, Roboto, system-ui, -apple-system", + "body": "SF Pro Text, Inter, Roboto, system-ui, -apple-system", + "mono": "SF Mono, Menlo, Roboto Mono, ui-monospace" + }, + "weight": { + "regular": 400, + "medium": 500, + "semibold": 600, + "bold": 700 + }, + "scale": { + "xs": 12, + "sm": 14, + "md": 16, + "lg": 18, + "xl": 20, + "xxl": 24, + "displaySm": 28, + "displayMd": 32, + "displayLg": 40 + }, + "lineHeight": { + "tight": 1.15, + "standard": 1.35, + "loose": 1.5 + } + }, + "spacing": { + "x1": 4, + "x2": 8, + "x3": 12, + "x4": 16, + "x5": 20, + "x6": 24, + "x8": 32, + "x10": 40, + "x12": 48, + "x16": 64 + }, + "radii": { + "xs": 6, + "sm": 8, + "md": 12, + "lg": 16, + "xl": 24, + "pill": 100 + }, + "shadows": { + "sm": [ + {"x":0,"y":1,"blur":2,"spread":0,"opacity":0.10} + ], + "md": [ + {"x":0,"y":2,"blur":6,"spread":0,"opacity":0.12}, + {"x":0,"y":8,"blur":12,"spread":-6,"opacity":0.10} + ], + "lg": [ + {"x":0,"y":12,"blur":24,"spread":-6,"opacity":0.12}, + {"x":0,"y":24,"blur":36,"spread":-12,"opacity":0.10} + ] + }, + "motion": { + "durations": { + "fast": 120, + "medium": 200, + "slow": 300, + "slower": 450 + }, + "easing": { + "standard": [0.2, 0.0, 0.0, 1.0], + "accelerate": [0.4, 0.0, 1.0, 1.0], + "decelerate": [0.0, 0.0, 0.2, 1.0], + "emphasize": [0.2, 0.0, 0.0, 1.0] + }, + "recommendations": { + "screenTransitionMs": [200, 320], + "buttonTapMs": [80, 140], + "listReorderMs": [120, 200] + } + }, + "themes": { + "light": { + "primary": "{color.palette.brandPrimary}", + "onPrimary": "#FFFFFF", + "background": "{color.palette.neutral10}", + "onBackground": "{color.palette.neutral100}", + "surface": "{color.palette.neutral0}", + "onSurface": "{color.palette.neutral100}", + "surfaceVariant": "{color.palette.neutral20}", + "onSurfaceVariant": "{color.palette.neutral80}" + }, + "dark": { + "primary": "{color.palette.brandPrimaryLight}", + "onPrimary": "{color.palette.neutral100}", + "background": "{color.palette.neutral100}", + "onBackground": "{color.palette.neutral10}", + "surface": "{color.palette.neutral90}", + "onSurface": "{color.palette.neutral10}", + "surfaceVariant": "{color.palette.neutral80}", + "onSurfaceVariant": "{color.palette.neutral20}" + }, + "highContrast": { + "primary": "{color.palette.brandPrimaryDark}", + "onPrimary": "#FFFFFF", + "background": "{color.palette.neutral0}", + "onBackground": "{color.palette.neutral100}", + "surface": "{color.palette.neutral0}", + "onSurface": "{color.palette.neutral100}", + "surfaceVariant": "{color.palette.neutral20}", + "onSurfaceVariant": "{color.palette.neutral100}" + } + }, + "status": { + "severity": { + "high": "#D32F2F", + "medium": "#ED6C02", + "low": "#2E7D32" + }, + "report": { + "submitted": "#1E5CE0", + "in_progress": "#7A859E", + "fixed": "#2E7D32" + } + } +} \ No newline at end of file diff --git a/lib/app.dart b/lib/app.dart index 29b4fc0..b1cf56d 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,6 +6,8 @@ import 'screens/report_flow/capture_screen.dart'; import 'screens/map/map_screen.dart'; import 'screens/my_reports/my_reports_screen.dart'; import 'screens/settings/settings_screen.dart'; +import 'theme/themes.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class FixMateApp extends StatelessWidget { const FixMateApp({super.key}); @@ -16,17 +18,15 @@ class FixMateApp extends StatelessWidget { builder: (context, localeProvider, child) { return MaterialApp( title: I18n.t('app.name'), - theme: ThemeData( - primarySwatch: Colors.blue, - brightness: Brightness.light, - useMaterial3: true, - ), + theme: AppThemes.light(), + darkTheme: AppThemes.dark(), + themeMode: ThemeMode.system, locale: localeProvider.locale, supportedLocales: const [ Locale('en', 'US'), Locale('ms', 'MY'), ], - home: const MainScreen(), + home: const StartRouter(), ); }, ); @@ -108,4 +108,264 @@ class PlaceholderScreen extends StatelessWidget { ), ); } +} + +/// Router that decides whether to show onboarding or main app +class StartRouter extends StatefulWidget { + const StartRouter({super.key}); + + @override + State createState() => _StartRouterState(); +} + +class _StartRouterState extends State { + bool _loading = true; + bool _onboarded = false; + + static const String _kOnboardedKey = 'onboarded_v1'; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + final prefs = await SharedPreferences.getInstance(); + final flag = prefs.getBool(_kOnboardedKey) ?? false; + if (mounted) { + setState(() { + _onboarded = flag; + _loading = false; + }); + } + } + + Future _setOnboarded() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_kOnboardedKey, true); + if (mounted) { + setState(() { + _onboarded = true; + }); + } + } + + @override + Widget build(BuildContext context) { + if (_loading) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + if (_onboarded) return const MainScreen(); + return WelcomeScreen( + onContinue: () async { + final completed = await Navigator.push( + context, + MaterialPageRoute(builder: (_) => const OnboardingFlow()), + ); + if (completed == true) { + await _setOnboarded(); + } + }, + onSignIn: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const SignInScreen()), + ); + }, + onSkip: () async { + await _setOnboarded(); + }, + ); + } +} + +/// Branded welcome screen (in-app splash handoff) +class WelcomeScreen extends StatelessWidget { + final VoidCallback onContinue; + final VoidCallback onSignIn; + final VoidCallback onSkip; + + const WelcomeScreen({ + super.key, + required this.onContinue, + required this.onSignIn, + required this.onSkip, + }); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 12), + Row( + children: [ + CircleAvatar( + radius: 20, + backgroundColor: cs.primary, + child: const Icon(Icons.build, color: Colors.white), + ), + const SizedBox(width: 12), + Text(I18n.t('app.name'), style: Theme.of(context).textTheme.titleLarge), + ], + ), + const Spacer(), + Text( + I18n.t('welcome.title'), + style: Theme.of(context).textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 12), + Text( + I18n.t('welcome.subtitle'), + style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: cs.onSurface.withOpacity(0.75)), + ), + const Spacer(), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: onContinue, + icon: const Icon(Icons.arrow_forward), + label: Text(I18n.t('cta.continueGuest')), + ), + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: onSignIn, + icon: const Icon(Icons.login), + label: Text(I18n.t('cta.signIn')), + ), + ), + const SizedBox(height: 8), + TextButton( + onPressed: onSkip, + child: Text(I18n.t('cta.skip')), + ), + ], + ), + ), + ), + ); + } +} + +/// Three-step onboarding flow with concise benefits +class OnboardingFlow extends StatefulWidget { + const OnboardingFlow({super.key}); + + @override + State createState() => _OnboardingFlowState(); +} + +class _OnboardingFlowState extends State { + final PageController _pc = PageController(); + int _index = 0; + + void _next() { + if (_index < 2) { + _pc.nextPage(duration: const Duration(milliseconds: 220), curve: Curves.easeOutCubic); + } else { + Navigator.pop(context, true); + } + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + Widget _page(String titleKey, String bodyKey, IconData icon) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircleAvatar(radius: 48, backgroundColor: cs.primary, child: Icon(icon, color: Colors.white, size: 40)), + const SizedBox(height: 24), + Text(I18n.t(titleKey), style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w700), textAlign: TextAlign.center), + const SizedBox(height: 12), + Text(I18n.t(bodyKey), style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center), + ], + ), + ); + } + + return Scaffold( + appBar: AppBar( + title: Text(I18n.t('onboarding.header')), + ), + body: Column( + children: [ + Expanded( + child: PageView( + controller: _pc, + onPageChanged: (i) => setState(() => _index = i), + children: [ + _page('onboarding.title1', 'onboarding.body1', Icons.flash_on), + _page('onboarding.title2', 'onboarding.body2', Icons.map), + _page('onboarding.title3', 'onboarding.body3', Icons.check_circle), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Row( + children: [ + TextButton(onPressed: () => Navigator.pop(context, true), child: Text(I18n.t('cta.skip'))), + const Spacer(), + ElevatedButton( + onPressed: _next, + child: Text(_index < 2 ? I18n.t('cta.next') : I18n.t('cta.getStarted')), + ), + ], + ), + ), + ], + ), + ); + } +} + +/// Sign-in placeholder with SSO buttons; supports continue as guest +class SignInScreen extends StatelessWidget { + const SignInScreen({super.key}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Scaffold( + appBar: AppBar(title: Text(I18n.t('auth.title'))), + body: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(I18n.t('auth.comingSoon')))), + icon: const Icon(Icons.apple), + label: Text(I18n.t('auth.signInWithApple')), + style: ElevatedButton.styleFrom(backgroundColor: cs.onSurface, foregroundColor: cs.surface), + ), + const SizedBox(height: 12), + OutlinedButton.icon( + onPressed: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(I18n.t('auth.comingSoon')))), + icon: const Icon(Icons.g_mobiledata), + label: Text(I18n.t('auth.signInWithGoogle')), + ), + const Spacer(), + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(I18n.t('cta.continueGuest')), + ), + ], + ), + ), + ); + } } \ No newline at end of file diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart new file mode 100644 index 0000000..8d2b7bf --- /dev/null +++ b/lib/providers/theme_provider.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// ThemeProvider manages ThemeMode and High-Contrast flag with persistence. +class ThemeProvider extends ChangeNotifier { + static const _kThemeModeKey = 'ui.theme_mode'; // 'system' | 'light' | 'dark' + static const _kHighContrastKey = 'ui.high_contrast'; // bool + + ThemeMode _mode = ThemeMode.system; + bool _highContrast = false; + + ThemeMode get themeMode => _mode; + bool get highContrast => _highContrast; + + Future init() async { + final prefs = await SharedPreferences.getInstance(); + final modeStr = prefs.getString(_kThemeModeKey) ?? 'system'; + _mode = _decodeMode(modeStr); + _highContrast = prefs.getBool(_kHighContrastKey) ?? false; + notifyListeners(); + } + + Future setThemeMode(ThemeMode mode) async { + if (_mode == mode) return; + _mode = mode; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_kThemeModeKey, _encodeMode(mode)); + notifyListeners(); + } + + Future setHighContrast(bool value) async { + if (_highContrast == value) return; + _highContrast = value; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_kHighContrastKey, value); + notifyListeners(); + } + + String _encodeMode(ThemeMode m) { + switch (m) { + case ThemeMode.light: + return 'light'; + case ThemeMode.dark: + return 'dark'; + case ThemeMode.system: + default: + return 'system'; + } + } + + ThemeMode _decodeMode(String s) { + switch (s) { + case 'light': + return ThemeMode.light; + case 'dark': + return ThemeMode.dark; + case 'system': + default: + return ThemeMode.system; + } + } +} \ No newline at end of file diff --git a/lib/theme/themes.dart b/lib/theme/themes.dart new file mode 100644 index 0000000..4bb81a0 --- /dev/null +++ b/lib/theme/themes.dart @@ -0,0 +1,347 @@ +import 'package:flutter/material.dart'; + +/// Design tokens and themes for FixMate (Civic Pro Minimal) +class AppColors { + static const Color brandPrimary = Color(0xFF1E5CE0); + static const Color brandPrimaryDark = Color(0xFF1748AC); + static const Color brandPrimaryLight = Color(0xFF8CB0FF); + + static const Color neutral0 = Color(0xFFFFFFFF); + static const Color neutral10 = Color(0xFFF5F7FB); + static const Color neutral20 = Color(0xFFE9EDF5); + static const Color neutral30 = Color(0xFFD8DFEB); + static const Color neutral40 = Color(0xFFB8C2D5); + static const Color neutral50 = Color(0xFF99A3B8); + static const Color neutral60 = Color(0xFF7A859E); + static const Color neutral70 = Color(0xFF5C6782); + static const Color neutral80 = Color(0xFF3D4765); + static const Color neutral90 = Color(0xFF21283C); + static const Color neutral100 = Color(0xFF0E1322); + + static const Color success = Color(0xFF2E7D32); + static const Color warning = Color(0xFFED6C02); + static const Color error = Color(0xFFD32F2F); + static const Color info = Color(0xFF0288D1); +} + +class AppRadii { + static const double xs = 6; + static const double sm = 8; + static const double md = 12; + static const double lg = 16; + static const double xl = 24; + static const double pill = 100; +} + +class AppSpacing { + static const double x1 = 4; + static const double x2 = 8; + static const double x3 = 12; + static const double x4 = 16; + static const double x5 = 20; + static const double x6 = 24; + static const double x8 = 32; + static const double x10 = 40; + static const double x12 = 48; + static const double x16 = 64; +} + +class AppMotion { + static const Duration fast = Duration(milliseconds: 120); + static const Duration medium = Duration(milliseconds: 200); + static const Duration slow = Duration(milliseconds: 300); + static const Duration slower = Duration(milliseconds: 450); + + static const Curve standard = Curves.easeOutCubic; + static const Curve accelerate = Curves.easeInCubic; + static const Curve decelerate = Curves.easeOutCubic; + static const Curve emphasize = Cubic(0.2, 0.0, 0.0, 1.0); +} + +class AppThemes { + static const PageTransitionsTheme transitions = PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(), + TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(), + }, + ); + + static ThemeData light() { + final colorScheme = ColorScheme.fromSeed( + seedColor: AppColors.brandPrimary, + brightness: Brightness.light, + primary: AppColors.brandPrimary, + ); + + return ThemeData( + useMaterial3: true, + colorScheme: colorScheme, + scaffoldBackgroundColor: AppColors.neutral10, + canvasColor: AppColors.neutral10, + appBarTheme: AppBarTheme( + backgroundColor: colorScheme.surface, + foregroundColor: colorScheme.onSurface, + elevation: 0, + centerTitle: false, + titleTextStyle: TextStyle( + color: colorScheme.onSurface, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), + textStyle: const TextStyle(fontWeight: FontWeight.w600), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: colorScheme.primary, + side: BorderSide(color: colorScheme.primary, width: 1.2), + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: colorScheme.primary, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.md), + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: colorScheme.surfaceVariant.withOpacity(0.5), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + borderSide: BorderSide(color: AppColors.neutral30), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + borderSide: BorderSide(color: AppColors.neutral30), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + borderSide: BorderSide(color: colorScheme.primary, width: 1.6), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: AppSpacing.x4, + horizontal: AppSpacing.x4, + ), + ), + cardTheme: CardThemeData( + color: colorScheme.surface, + elevation: 1, + margin: const EdgeInsets.all(0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + ), + chipTheme: ChipThemeData( + backgroundColor: colorScheme.surfaceVariant, + selectedColor: colorScheme.primary.withOpacity(0.14), + secondarySelectedColor: colorScheme.primary.withOpacity(0.14), + labelStyle: TextStyle(color: colorScheme.onSurface), + secondaryLabelStyle: TextStyle(color: colorScheme.onSurface), + brightness: Brightness.light, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.md), + ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + ), + dialogTheme: DialogThemeData( + backgroundColor: colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.xl), + ), + ), + snackBarTheme: SnackBarThemeData( + backgroundColor: AppColors.neutral90, + contentTextStyle: const TextStyle(color: Colors.white), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: colorScheme.surface, + selectedItemColor: colorScheme.primary, + unselectedItemColor: AppColors.neutral60, + type: BottomNavigationBarType.fixed, + selectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600), + ), + pageTransitionsTheme: transitions, + visualDensity: VisualDensity.standard, + ); + } + + static ThemeData dark() { + final colorScheme = ColorScheme.fromSeed( + seedColor: AppColors.brandPrimary, + brightness: Brightness.dark, + primary: AppColors.brandPrimaryLight, + ); + + return ThemeData( + useMaterial3: true, + colorScheme: colorScheme, + scaffoldBackgroundColor: AppColors.neutral100, + canvasColor: AppColors.neutral100, + appBarTheme: AppBarTheme( + backgroundColor: colorScheme.surface, + foregroundColor: colorScheme.onSurface, + elevation: 0, + centerTitle: false, + titleTextStyle: TextStyle( + color: colorScheme.onSurface, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), + textStyle: const TextStyle(fontWeight: FontWeight.w600), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: colorScheme.primary, + side: BorderSide(color: colorScheme.primary, width: 1.2), + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: colorScheme.surfaceVariant.withOpacity(0.3), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + borderSide: BorderSide(color: AppColors.neutral80), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + borderSide: BorderSide(color: AppColors.neutral80), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + borderSide: BorderSide(color: colorScheme.primary, width: 1.6), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: AppSpacing.x4, + horizontal: AppSpacing.x4, + ), + ), + cardTheme: CardThemeData( + color: colorScheme.surface, + elevation: 1, + margin: const EdgeInsets.all(0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + ), + chipTheme: ChipThemeData( + backgroundColor: colorScheme.surfaceVariant.withOpacity(0.5), + selectedColor: colorScheme.primary.withOpacity(0.22), + secondarySelectedColor: colorScheme.primary.withOpacity(0.22), + labelStyle: TextStyle(color: colorScheme.onSurface), + secondaryLabelStyle: TextStyle(color: colorScheme.onSurface), + brightness: Brightness.dark, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.md), + ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + ), + dialogTheme: DialogThemeData( + backgroundColor: colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.xl), + ), + ), + snackBarTheme: SnackBarThemeData( + backgroundColor: AppColors.neutral90, + contentTextStyle: const TextStyle(color: Colors.white), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: colorScheme.surface, + selectedItemColor: colorScheme.primary, + unselectedItemColor: AppColors.neutral60, + type: BottomNavigationBarType.fixed, + selectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600), + ), + pageTransitionsTheme: transitions, + visualDensity: VisualDensity.standard, + ); + } + + static ThemeData highContrast() { + // High-contrast light theme with stronger borders and higher on-color contrast + final base = light(); + final cs = base.colorScheme; + final highCs = cs.copyWith( + primary: AppColors.brandPrimaryDark, + onPrimary: Colors.white, + surface: AppColors.neutral0, + onSurface: AppColors.neutral100, + surfaceTint: Colors.transparent, + ); + return base.copyWith( + colorScheme: highCs, + inputDecorationTheme: base.inputDecorationTheme.copyWith( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + borderSide: BorderSide(color: AppColors.neutral80, width: 2), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + borderSide: BorderSide(color: AppColors.neutral80, width: 2), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + borderSide: BorderSide(color: highCs.primary, width: 2.2), + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: highCs.primary, + foregroundColor: highCs.onPrimary, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 18), + textStyle: const TextStyle(fontWeight: FontWeight.w700), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + side: BorderSide(color: highCs.primary, width: 2), + ), + ), + snackBarTheme: base.snackBarTheme.copyWith( + backgroundColor: Colors.black, + contentTextStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700), + ), + ); + } +} \ No newline at end of file