// Theme assembly. ColorScheme + typography + component themes. import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'tokens.dart'; class FaiTheme { FaiTheme._(); /// Inter for UI, JetBrains Mono for technical strings (IDs, /// paths, capability refs). Both via google_fonts so they /// bundle on first run; for production we'd ship them as /// app assets. static TextTheme _textTheme(Brightness b) { final base = b == Brightness.dark ? Typography.whiteCupertino : Typography.blackCupertino; return GoogleFonts.interTextTheme(base).copyWith( displaySmall: GoogleFonts.inter( fontSize: 28, fontWeight: FontWeight.w600, letterSpacing: -0.4, ), headlineSmall: GoogleFonts.inter( fontSize: 20, fontWeight: FontWeight.w600, letterSpacing: -0.2, ), titleMedium: GoogleFonts.inter( fontSize: 15, fontWeight: FontWeight.w600, letterSpacing: 0, ), titleSmall: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w500, ), bodyLarge: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w400, height: 1.45, ), bodyMedium: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w400, height: 1.4, ), bodySmall: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w400, height: 1.4, ), labelMedium: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w500, ), labelSmall: GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w500, letterSpacing: 0.2, ), ); } /// JetBrains Mono for monospaced technical content. Used by /// FaiMono helper. static TextStyle mono({ double size = 12, FontWeight weight = FontWeight.w400, Color? color, }) => GoogleFonts.jetBrainsMono( fontSize: size, fontWeight: weight, color: color, height: 1.4, ); static ThemeData dark() { final scheme = ColorScheme( brightness: Brightness.dark, primary: FaiColors.accent, onPrimary: FaiColors.black, primaryContainer: FaiColors.accentMuted, onPrimaryContainer: FaiColors.textStrong, secondary: FaiColors.accentDeep, onSecondary: FaiColors.black, secondaryContainer: FaiColors.surfaceHigh, onSecondaryContainer: FaiColors.text, tertiary: FaiColors.accent, onTertiary: FaiColors.black, tertiaryContainer: FaiColors.surfaceHigh, onTertiaryContainer: FaiColors.text, error: FaiColors.danger, onError: FaiColors.textStrong, errorContainer: const Color(0xFF3F1518), onErrorContainer: const Color(0xFFFCA5A5), surface: FaiColors.black, onSurface: FaiColors.text, onSurfaceVariant: FaiColors.muted, outline: FaiColors.border, outlineVariant: FaiColors.border, surfaceContainerLowest: FaiColors.black, surfaceContainerLow: FaiColors.surface, surfaceContainer: FaiColors.surface, surfaceContainerHigh: FaiColors.surfaceHigh, surfaceContainerHighest: FaiColors.surfaceHigh, ); return _build(scheme); } static ThemeData light() { final scheme = ColorScheme( brightness: Brightness.light, primary: FaiColors.accentDeep, onPrimary: FaiColors.lightSurface, primaryContainer: const Color(0xFFE0F2FE), onPrimaryContainer: FaiColors.accentMuted, secondary: FaiColors.accentMuted, onSecondary: FaiColors.lightSurface, secondaryContainer: FaiColors.lightSurfaceHigh, onSecondaryContainer: FaiColors.lightText, tertiary: FaiColors.accentDeep, onTertiary: FaiColors.lightSurface, tertiaryContainer: const Color(0xFFE0F2FE), onTertiaryContainer: FaiColors.accentMuted, error: FaiColors.danger, onError: FaiColors.lightSurface, errorContainer: const Color(0xFFFEE2E2), onErrorContainer: const Color(0xFF7F1D1D), surface: FaiColors.lightCanvas, onSurface: FaiColors.lightText, onSurfaceVariant: FaiColors.lightMuted, outline: FaiColors.lightBorder, outlineVariant: FaiColors.lightBorder, surfaceContainerLowest: FaiColors.lightCanvas, surfaceContainerLow: FaiColors.lightSurface, surfaceContainer: FaiColors.lightSurface, surfaceContainerHigh: FaiColors.lightSurfaceHigh, surfaceContainerHighest: FaiColors.lightSurfaceHigh, ); return _build(scheme); } static ThemeData _build(ColorScheme scheme) { final textTheme = _textTheme(scheme.brightness); return ThemeData( useMaterial3: true, colorScheme: scheme, scaffoldBackgroundColor: scheme.surface, textTheme: textTheme, primaryTextTheme: textTheme, appBarTheme: AppBarTheme( backgroundColor: scheme.surface, foregroundColor: scheme.onSurface, elevation: 0, scrolledUnderElevation: 0, centerTitle: false, titleTextStyle: textTheme.headlineSmall, toolbarHeight: 64, ), cardTheme: CardThemeData( color: scheme.surfaceContainer, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(FaiRadius.md), side: BorderSide(color: scheme.outlineVariant), ), margin: EdgeInsets.zero, ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( backgroundColor: scheme.primary, foregroundColor: scheme.onPrimary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(FaiRadius.sm), ), padding: const EdgeInsets.symmetric( horizontal: FaiSpace.lg, vertical: FaiSpace.md, ), textStyle: textTheme.labelMedium, animationDuration: FaiMotion.fast, ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( foregroundColor: scheme.onSurface, side: BorderSide(color: scheme.outline), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(FaiRadius.sm), ), padding: const EdgeInsets.symmetric( horizontal: FaiSpace.lg, vertical: FaiSpace.md, ), textStyle: textTheme.labelMedium, animationDuration: FaiMotion.fast, ), ), iconButtonTheme: IconButtonThemeData( style: IconButton.styleFrom( foregroundColor: scheme.onSurfaceVariant, ), ), navigationRailTheme: NavigationRailThemeData( backgroundColor: scheme.surfaceContainerLow, indicatorColor: scheme.primaryContainer, selectedIconTheme: IconThemeData(color: scheme.primary, size: 22), unselectedIconTheme: IconThemeData(color: scheme.onSurfaceVariant, size: 22), selectedLabelTextStyle: textTheme.labelMedium?.copyWith(color: scheme.primary), unselectedLabelTextStyle: textTheme.labelMedium ?.copyWith(color: scheme.onSurfaceVariant), useIndicator: true, ), dividerColor: scheme.outlineVariant, dividerTheme: DividerThemeData( color: scheme.outlineVariant, thickness: 1, space: 1, ), snackBarTheme: SnackBarThemeData( backgroundColor: scheme.surfaceContainerHigh, contentTextStyle: textTheme.bodyMedium?.copyWith(color: scheme.onSurface), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(FaiRadius.sm), ), ), ); } }