# Theming Guide This document describes the Material 3 theming system used in Tatlock UI. ## Overview Tatlock UI uses Material 3 (Material You) with: - Dynamic color schemes from a seed color - System preference detection (light/dark) - Manual override with persistence - Consistent use of `colorScheme` throughout ## Theme Modes ### System Preference (Default) By default, the app follows the system's light/dark mode preference: ```dart MaterialApp( themeMode: ThemeMode.system, theme: AppTheme.light, darkTheme: AppTheme.dark, ) ``` ### Manual Override Users can override system preference. The choice persists via SharedPreferences: ```dart enum ThemeSetting { system, // Follow OS light, // Always light dark, // Always dark } ``` ## Color Scheme ### Seed Color All colors derive from a single seed color for consistency: ```dart const seedColor = Color(0xFF009688); // Teal final lightScheme = ColorScheme.fromSeed( seedColor: seedColor, brightness: Brightness.light, ); final darkScheme = ColorScheme.fromSeed( seedColor: seedColor, brightness: Brightness.dark, ); ``` ### Using Colors **Always use `colorScheme`** - never use raw colors in widgets: ```dart // Good Container( color: Theme.of(context).colorScheme.surface, child: Text( 'Hello', style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), ) // Bad - don't do this Container( color: Colors.white, // Won't adapt to dark mode child: Text('Hello', style: TextStyle(color: Colors.black)), ) ``` ### ColorScheme Roles | Role | Usage | |------|-------| | `primary` | Key actions, selected states, important text | | `onPrimary` | Text/icons on primary backgrounds | | `primaryContainer` | Less prominent primary surfaces | | `secondary` | Secondary actions, accents | | `surface` | Background of cards, sheets, dialogs | | `onSurface` | Text on surface backgrounds | | `surfaceContainerHighest` | Elevated surfaces (cards on surface) | | `error` | Error states, destructive actions | | `outline` | Borders, dividers | | `outlineVariant` | Subtle borders | ### Semantic Colors For domain-specific colors (status badges, charts), define semantic extensions: ```dart extension SemanticColors on ColorScheme { Color get success => brightness == Brightness.light ? const Color(0xFF2E7D32) : const Color(0xFF81C784); Color get warning => brightness == Brightness.light ? const Color(0xFFF57C00) : const Color(0xFFFFB74D); Color get info => brightness == Brightness.light ? const Color(0xFF1976D2) : const Color(0xFF64B5F6); } // Usage Container(color: Theme.of(context).colorScheme.success) ``` ## Theme Data ### AppTheme Class ```dart class AppTheme { AppTheme._(); static const _seedColor = Color(0xFF009688); // Teal static final light = ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( seedColor: _seedColor, brightness: Brightness.light, ), // Component themes below ); static final dark = ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( seedColor: _seedColor, brightness: Brightness.dark, ), // Component themes below ); } ``` ### Component Themes Customize individual components while maintaining consistency: ```dart ThemeData( // ...colorScheme... appBarTheme: AppBarTheme( centerTitle: false, elevation: 0, scrolledUnderElevation: 1, ), cardTheme: CardTheme( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: colorScheme.outlineVariant), ), ), inputDecorationTheme: InputDecorationTheme( filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( minimumSize: const Size(88, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ), ) ``` ## State Management ### ThemeNotifier ```dart @riverpod class ThemeNotifier extends _$ThemeNotifier { static const _key = 'theme_setting'; @override ThemeSetting build() { _loadSavedSetting(); return ThemeSetting.system; } Future _loadSavedSetting() async { final prefs = await SharedPreferences.getInstance(); final value = prefs.getString(_key); if (value != null) { state = ThemeSetting.values.byName(value); } } Future setSetting(ThemeSetting setting) async { state = setting; final prefs = await SharedPreferences.getInstance(); await prefs.setString(_key, setting.name); } ThemeMode get themeMode => switch (state) { ThemeSetting.system => ThemeMode.system, ThemeSetting.light => ThemeMode.light, ThemeSetting.dark => ThemeMode.dark, }; } ``` ### Using in App ```dart class TatlockApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final themeNotifier = ref.watch(themeNotifierProvider.notifier); return MaterialApp.router( theme: AppTheme.light, darkTheme: AppTheme.dark, themeMode: themeNotifier.themeMode, routerConfig: router, ); } } ``` ## Typography ### Text Styles Use `textTheme` for consistent typography: ```dart Text( 'Heading', style: Theme.of(context).textTheme.headlineMedium, ) Text( 'Body text', style: Theme.of(context).textTheme.bodyLarge, ) Text( 'Label', style: Theme.of(context).textTheme.labelMedium, ) ``` ### Text Theme Scale | Style | Size | Weight | Usage | |-------|------|--------|-------| | `displayLarge` | 57 | 400 | Hero text | | `displayMedium` | 45 | 400 | Large headers | | `displaySmall` | 36 | 400 | Section headers | | `headlineLarge` | 32 | 400 | Page titles | | `headlineMedium` | 28 | 400 | Card titles | | `headlineSmall` | 24 | 400 | Subheadings | | `titleLarge` | 22 | 400 | App bar titles | | `titleMedium` | 16 | 500 | List item titles | | `titleSmall` | 14 | 500 | Tabs, chips | | `bodyLarge` | 16 | 400 | Primary body text | | `bodyMedium` | 14 | 400 | Secondary body text | | `bodySmall` | 12 | 400 | Captions | | `labelLarge` | 14 | 500 | Button text | | `labelMedium` | 12 | 500 | Navigation labels | | `labelSmall` | 11 | 500 | Badges, tags | ## Spacing Use consistent spacing with a base unit: ```dart class Spacing { Spacing._(); static const double xs = 4; static const double sm = 8; static const double md = 16; static const double lg = 24; static const double xl = 32; static const double xxl = 48; } // Usage Padding( padding: const EdgeInsets.all(Spacing.md), child: ... ) SizedBox(height: Spacing.sm) ``` ## Border Radius Consistent corner radii: ```dart class Radii { Radii._(); static const double sm = 4; static const double md = 8; static const double lg = 12; static const double xl = 16; static const double full = 9999; } // Usage Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(Radii.md), ), ) ``` ## Elevation Material 3 uses tonal elevation (surface tint) rather than shadows: ```dart // Elevation levels // 0 - Surface (no elevation) // 1 - Slight elevation (cards, app bar scrolled) // 2 - Moderate elevation (dialogs, menus) // 3 - High elevation (navigation drawers) Card( elevation: 0, // Use tonal surface instead of shadow color: Theme.of(context).colorScheme.surfaceContainerHighest, ) ``` ## Dark Mode Considerations ### Contrast Ensure sufficient contrast in dark mode: - Primary text: `onSurface` (high contrast) - Secondary text: `onSurfaceVariant` (medium contrast) - Disabled text: `onSurface` with opacity ### Elevation in Dark Mode Dark surfaces get lighter with elevation, not darker: ```dart // M3 handles this automatically with surfaceContainerLow/High ``` ### Images Consider providing dark mode variants for images/icons: ```dart Image.asset( Theme.of(context).brightness == Brightness.dark ? 'assets/logo_dark.png' : 'assets/logo_light.png', ) ``` ## Testing Themes ### Widget Tests ```dart testWidgets('renders correctly in dark mode', (tester) async { await tester.pumpWidget( MaterialApp( theme: AppTheme.dark, home: const MyWidget(), ), ); // Verify dark mode appearance }); ``` ### Golden Tests Use golden tests to catch visual regressions: ```dart testWidgets('matches golden in light mode', (tester) async { await tester.pumpWidget( MaterialApp( theme: AppTheme.light, home: const MyWidget(), ), ); await expectLater( find.byType(MyWidget), matchesGoldenFile('goldens/my_widget_light.png'), ); }); ``` ## flex_color_scheme (Optional) For more sophisticated theming, consider `flex_color_scheme`: ```dart import 'package:flex_color_scheme/flex_color_scheme.dart'; final lightTheme = FlexThemeData.light( scheme: FlexScheme.teal, surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold, blendLevel: 9, subThemesData: const FlexSubThemesData( blendOnLevel: 10, blendOnColors: false, ), useMaterial3: true, ); final darkTheme = FlexThemeData.dark( scheme: FlexScheme.teal, surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold, blendLevel: 15, subThemesData: const FlexSubThemesData( blendOnLevel: 20, ), useMaterial3: true, ); ```