147 lines
6.9 KiB
Dart
147 lines
6.9 KiB
Dart
import 'package:flutter/material.dart';
|
|||
|
|
|
||
|
|
import 'app_colors.dart';
|
||
|
|
import 'app_data_styles.dart';
|
||
|
|
import 'status_colors.dart';
|
||
|
|
|
||
|
|
/// Builds the app's light/dark [ThemeData]. Colors are constructed
|
||
|
|
/// explicitly from [AppColors] rather than via [ColorScheme.fromSeed] so the
|
||
|
|
/// approved mockup's exact hex values can't drift through algorithmic tone
|
||
|
|
/// generation.
|
||
|
|
abstract final class AppTheme {
|
||
|
|
static ThemeData light() => _build(brightness: Brightness.light);
|
||
|
|
static ThemeData dark() => _build(brightness: Brightness.dark);
|
||
|
|
|
||
|
|
static ThemeData _build({required Brightness brightness}) {
|
||
|
|
final isDark = brightness == Brightness.dark;
|
||
|
|
|
||
|
|
final colorScheme = isDark
|
||
|
|
? const ColorScheme.dark(
|
||
|
|
surface: AppColors.darkBg,
|
||
|
|
onSurface: AppColors.darkText,
|
||
|
|
surfaceContainerHighest: AppColors.darkSurfaceAlt,
|
||
|
|
primary: AppColors.darkAccent,
|
||
|
|
onPrimary: AppColors.darkAccentInk,
|
||
|
|
secondary: AppColors.darkAccentStrong,
|
||
|
|
onSecondary: AppColors.darkAccentInk,
|
||
|
|
error: AppColors.critical,
|
||
|
|
onError: Colors.white,
|
||
|
|
outline: AppColors.darkBorder,
|
||
|
|
outlineVariant: AppColors.darkBorder,
|
||
|
|
)
|
||
|
|
: const ColorScheme.light(
|
||
|
|
surface: AppColors.lightBg,
|
||
|
|
onSurface: AppColors.lightText,
|
||
|
|
surfaceContainerHighest: AppColors.lightSurfaceAlt,
|
||
|
|
primary: AppColors.lightAccent,
|
||
|
|
onPrimary: AppColors.lightAccentInk,
|
||
|
|
secondary: AppColors.lightAccentStrong,
|
||
|
|
onSecondary: AppColors.lightAccentInk,
|
||
|
|
error: AppColors.critical,
|
||
|
|
onError: Colors.white,
|
||
|
|
outline: AppColors.lightBorder,
|
||
|
|
outlineVariant: AppColors.lightBorder,
|
||
|
|
);
|
||
|
|
|
||
|
|
final surfaceCard = isDark ? AppColors.darkSurface : AppColors.lightSurface;
|
||
|
|
final textColor = isDark ? AppColors.darkText : AppColors.lightText;
|
||
|
|
final textMuted = isDark ? AppColors.darkTextMuted : AppColors.lightTextMuted;
|
||
|
|
final textFaint = isDark ? AppColors.darkTextFaint : AppColors.lightTextFaint;
|
||
|
|
final border = isDark ? AppColors.darkBorder : AppColors.lightBorder;
|
||
|
|
|
||
|
|
final textTheme = TextTheme(
|
||
|
|
displaySmall: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w700, fontSize: 30, color: textColor, letterSpacing: -0.2),
|
||
|
|
headlineSmall: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w700, fontSize: 22, color: textColor, letterSpacing: -0.1),
|
||
|
|
titleLarge: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w700, fontSize: 17, color: textColor, letterSpacing: -0.1),
|
||
|
|
titleMedium: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w600, fontSize: 14, color: textColor),
|
||
|
|
titleSmall: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w700, fontSize: 11, color: textFaint, letterSpacing: 0.6),
|
||
|
|
bodyLarge: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w400, fontSize: 15, color: textColor, height: 1.5),
|
||
|
|
bodyMedium: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w400, fontSize: 13.5, color: textColor, height: 1.45),
|
||
|
|
bodySmall: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w500, fontSize: 12, color: textMuted),
|
||
|
|
labelLarge: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w600, fontSize: 13, color: textColor),
|
||
|
|
labelMedium: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w600, fontSize: 12, color: textMuted),
|
||
|
|
labelSmall: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w700, fontSize: 11, color: textFaint, letterSpacing: 0.5),
|
||
|
|
);
|
||
|
|
|
||
|
|
return ThemeData(
|
||
|
|
useMaterial3: true,
|
||
|
|
brightness: brightness,
|
||
|
|
colorScheme: colorScheme,
|
||
|
|
scaffoldBackgroundColor: colorScheme.surface,
|
||
|
|
canvasColor: colorScheme.surface,
|
||
|
|
dividerColor: border,
|
||
|
|
fontFamily: 'Inter',
|
||
|
|
textTheme: textTheme,
|
||
|
|
cardTheme: CardThemeData(
|
||
|
|
color: surfaceCard,
|
||
|
|
elevation: 0,
|
||
|
|
margin: EdgeInsets.zero,
|
||
|
|
shape: RoundedRectangleBorder(
|
||
|
|
borderRadius: BorderRadius.circular(10),
|
||
|
|
side: BorderSide(color: border),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
appBarTheme: AppBarTheme(
|
||
|
|
backgroundColor: colorScheme.surface,
|
||
|
|
foregroundColor: textColor,
|
||
|
|
elevation: 0,
|
||
|
|
surfaceTintColor: Colors.transparent,
|
||
|
|
),
|
||
|
|
navigationRailTheme: NavigationRailThemeData(
|
||
|
|
backgroundColor: isDark ? AppColors.darkSurfaceAlt : AppColors.lightSurfaceAlt,
|
||
|
|
indicatorColor: colorScheme.primary.withValues(alpha: 0.16),
|
||
|
|
selectedIconTheme: IconThemeData(color: colorScheme.secondary),
|
||
|
|
unselectedIconTheme: IconThemeData(color: textMuted),
|
||
|
|
selectedLabelTextStyle: TextStyle(color: colorScheme.secondary, fontWeight: FontWeight.w700, fontSize: 12.5),
|
||
|
|
unselectedLabelTextStyle: TextStyle(color: textMuted, fontWeight: FontWeight.w500, fontSize: 12.5),
|
||
|
|
),
|
||
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||
|
|
style: ElevatedButton.styleFrom(
|
||
|
|
backgroundColor: colorScheme.primary,
|
||
|
|
foregroundColor: colorScheme.onPrimary,
|
||
|
|
elevation: 0,
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)),
|
||
|
|
textStyle: const TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w600, fontSize: 13),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||
|
|
style: OutlinedButton.styleFrom(
|
||
|
|
foregroundColor: textColor,
|
||
|
|
side: BorderSide(color: border),
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)),
|
||
|
|
textStyle: const TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w600, fontSize: 13),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
inputDecorationTheme: InputDecorationTheme(
|
||
|
|
filled: true,
|
||
|
|
fillColor: colorScheme.surface,
|
||
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||
|
|
border: OutlineInputBorder(
|
||
|
|
borderRadius: BorderRadius.circular(7),
|
||
|
|
borderSide: BorderSide(color: border),
|
||
|
|
),
|
||
|
|
enabledBorder: OutlineInputBorder(
|
||
|
|
borderRadius: BorderRadius.circular(7),
|
||
|
|
borderSide: BorderSide(color: border),
|
||
|
|
),
|
||
|
|
focusedBorder: OutlineInputBorder(
|
||
|
|
borderRadius: BorderRadius.circular(7),
|
||
|
|
borderSide: BorderSide(color: colorScheme.primary, width: 1.5),
|
||
|
|
),
|
||
|
|
labelStyle: TextStyle(color: textMuted, fontSize: 12.5, fontWeight: FontWeight.w600),
|
||
|
|
),
|
||
|
|
dataTableTheme: DataTableThemeData(
|
||
|
|
headingTextStyle: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w700, fontSize: 11, color: textFaint, letterSpacing: 0.5),
|
||
|
|
dataTextStyle: TextStyle(fontFamily: 'Inter', fontSize: 13, color: textColor),
|
||
|
|
dividerThickness: 1,
|
||
|
|
),
|
||
|
|
extensions: [
|
||
|
|
isDark ? StatusColors.dark : StatusColors.light,
|
||
|
|
AppDataStyles.resolve(textColor: textColor, mutedColor: textMuted),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|