Files
valuecards/lib/utils/app_theme.dart
T
2026-09-18 00:01:42 +02:00

381 lines
13 KiB
Dart

/// Mindful Values Architecture theme — warm surfaces with high-contrast type.
library;
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
/// Builds [ThemeData] tuned for readable contrast on warm paper surfaces.
abstract final class AppTheme {
/// Page canvas — light paper, clearly lighter than elevated cards.
static const Color surface = Color(0xFFFFFBF5);
/// Elevated cards / sheets — distinct from the canvas.
static const Color surfaceContainerLow = Color(0xFFFFFFFF);
static const Color surfaceContainer = Color(0xFFF3EDE4);
static const Color surfaceContainerHigh = Color(0xFFE9E2D7);
static const Color surfaceContainerHighest = Color(0xFFDED6C9);
/// Near-black espresso for primary copy (≈16:1 on surface).
static const Color onSurface = Color(0xFF1A1612);
/// Secondary copy — still strong on cream (≈9:1).
static const Color onSurfaceVariant = Color(0xFF3F3832);
/// Icons / meta — darker than the old stone grey.
static const Color outline = Color(0xFF5C534B);
static const Color outlineVariant = Color(0xFFC4B8AA);
static const Color primary = Color(0xFF4F4032);
static const Color primaryContainer = Color(0xFF6B5848);
static const Color onPrimary = Color(0xFFFFFBF5);
static const Color secondary = Color(0xFF8A3F1C);
static const Color secondaryContainer = Color(0xFFFFD4BC);
static const Color tertiary = Color(0xFF3A5238);
static const Color inverseSurface = Color(0xFF25211D);
static ThemeData light() {
const colorScheme = ColorScheme(
brightness: Brightness.light,
primary: primary,
onPrimary: onPrimary,
primaryContainer: primaryContainer,
onPrimaryContainer: Color(0xFFFFF3E8),
secondary: secondary,
onSecondary: onPrimary,
secondaryContainer: secondaryContainer,
onSecondaryContainer: Color(0xFF4A1E08),
tertiary: tertiary,
onTertiary: onPrimary,
tertiaryContainer: Color(0xFF5A7358),
onTertiaryContainer: Color(0xFFEAF6E7),
error: Color(0xFFB3261E),
onError: onPrimary,
errorContainer: Color(0xFFFFDAD6),
onErrorContainer: Color(0xFF410002),
surface: surface,
onSurface: onSurface,
surfaceContainerHighest: surfaceContainerHighest,
surfaceContainerHigh: surfaceContainerHigh,
surfaceContainer: surfaceContainer,
surfaceContainerLow: surfaceContainerLow,
surfaceContainerLowest: Color(0xFFFFFFFF),
onSurfaceVariant: onSurfaceVariant,
outline: outline,
outlineVariant: outlineVariant,
inverseSurface: inverseSurface,
onInverseSurface: Color(0xFFF7F1E8),
inversePrimary: Color(0xFFDCC4A8),
shadow: Color(0xFF1A1612),
scrim: Color(0xFF1A1612),
);
final textTheme = _textTheme(colorScheme);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
textTheme: textTheme,
scaffoldBackgroundColor: surface,
appBarTheme: AppBarTheme(
centerTitle: false,
elevation: 0,
scrolledUnderElevation: 0,
backgroundColor: surface,
foregroundColor: onSurface,
titleTextStyle: textTheme.titleLarge,
),
cardTheme: CardThemeData(
elevation: 0,
color: surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
side: const BorderSide(color: outlineVariant, width: 1.2),
),
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.zero,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
hintStyle: textTheme.bodyMedium?.copyWith(
color: outline.withValues(alpha: 0.85),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: outlineVariant, width: 1.2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: outlineVariant, width: 1.2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: primary, width: 1.8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
backgroundColor: primary,
foregroundColor: onPrimary,
shape: const StadiumBorder(),
textStyle: textTheme.labelLarge?.copyWith(color: onPrimary),
elevation: 0,
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: onSurface,
textStyle: textTheme.labelMedium?.copyWith(
color: onSurface,
fontWeight: FontWeight.w600,
),
shape: const StadiumBorder(),
),
),
chipTheme: ChipThemeData(
backgroundColor: Colors.white,
selectedColor: surfaceContainerHigh,
side: const BorderSide(color: outlineVariant, width: 1.2),
shape: const StadiumBorder(),
labelStyle: textTheme.labelMedium?.copyWith(color: onSurface),
secondaryLabelStyle: textTheme.labelMedium?.copyWith(color: onSurface),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
),
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: surfaceContainerLow,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(32)),
),
showDragHandle: true,
),
dividerTheme: const DividerThemeData(
color: outlineVariant,
space: 1,
thickness: 1,
),
listTileTheme: const ListTileThemeData(
textColor: onSurface,
iconColor: onSurfaceVariant,
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
backgroundColor: inverseSurface,
contentTextStyle: textTheme.bodyMedium?.copyWith(
color: const Color(0xFFF7F1E8),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
);
}
/// Soft dark mirror — elevated cards stay darker than text for contrast.
static ThemeData dark() {
const darkScheme = ColorScheme(
brightness: Brightness.dark,
primary: Color(0xFFE2CDB6),
onPrimary: Color(0xFF2A2118),
primaryContainer: Color(0xFF6B5848),
onPrimaryContainer: Color(0xFFFFF3E8),
secondary: Color(0xFFFFB691),
onSecondary: Color(0xFF4A1E08),
secondaryContainer: Color(0xFF6B3014),
onSecondaryContainer: Color(0xFFFFD4BC),
tertiary: Color(0xFFB5CDB0),
onTertiary: Color(0xFF1A2A1A),
tertiaryContainer: Color(0xFF3A5238),
onTertiaryContainer: Color(0xFFEAF6E7),
error: Color(0xFFFFB4AB),
onError: Color(0xFF690005),
errorContainer: Color(0xFF93000A),
onErrorContainer: Color(0xFFFFDAD6),
surface: Color(0xFF161310),
onSurface: Color(0xFFF5EFE6),
surfaceContainerHighest: Color(0xFF3A342E),
surfaceContainerHigh: Color(0xFF302B26),
surfaceContainer: Color(0xFF26221E),
surfaceContainerLow: Color(0xFF221E1A),
surfaceContainerLowest: Color(0xFF0E0C0A),
onSurfaceVariant: Color(0xFFD9CEC2),
outline: Color(0xFFB0A397),
outlineVariant: Color(0xFF4A433C),
inverseSurface: Color(0xFFF5EFE6),
onInverseSurface: Color(0xFF1C1916),
inversePrimary: Color(0xFF4F4032),
shadow: Color(0xFF000000),
scrim: Color(0xFF000000),
);
final textTheme = _textTheme(darkScheme);
return ThemeData(
useMaterial3: true,
colorScheme: darkScheme,
textTheme: textTheme,
scaffoldBackgroundColor: darkScheme.surface,
appBarTheme: AppBarTheme(
centerTitle: false,
elevation: 0,
scrolledUnderElevation: 0,
backgroundColor: darkScheme.surface,
foregroundColor: darkScheme.onSurface,
titleTextStyle: textTheme.titleLarge,
),
cardTheme: CardThemeData(
elevation: 0,
color: darkScheme.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
side: BorderSide(color: darkScheme.outlineVariant, width: 1.2),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: darkScheme.surfaceContainer,
hintStyle: textTheme.bodyMedium?.copyWith(
color: darkScheme.outline.withValues(alpha: 0.9),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: darkScheme.outlineVariant, width: 1.2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: darkScheme.outlineVariant, width: 1.2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: darkScheme.primary, width: 1.8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
backgroundColor: darkScheme.primary,
foregroundColor: darkScheme.onPrimary,
shape: const StadiumBorder(),
textStyle: textTheme.labelLarge?.copyWith(color: darkScheme.onPrimary),
elevation: 0,
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: darkScheme.onSurface,
textStyle: textTheme.labelMedium?.copyWith(
color: darkScheme.onSurface,
fontWeight: FontWeight.w600,
),
shape: const StadiumBorder(),
),
),
chipTheme: ChipThemeData(
backgroundColor: darkScheme.surfaceContainer,
selectedColor: darkScheme.surfaceContainerHigh,
side: BorderSide(color: darkScheme.outlineVariant, width: 1.2),
shape: const StadiumBorder(),
labelStyle: textTheme.labelMedium?.copyWith(color: darkScheme.onSurface),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: darkScheme.surfaceContainerLow,
surfaceTintColor: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(32)),
),
showDragHandle: true,
),
listTileTheme: ListTileThemeData(
textColor: darkScheme.onSurface,
iconColor: darkScheme.onSurfaceVariant,
),
);
}
static TextTheme _textTheme(ColorScheme colors) {
final newsreader = GoogleFonts.newsreaderTextTheme();
final jakarta = GoogleFonts.plusJakartaSansTextTheme();
return TextTheme(
displaySmall: newsreader.displaySmall?.copyWith(
fontSize: 32,
height: 40 / 32,
fontWeight: FontWeight.w500,
letterSpacing: -0.32,
color: colors.onSurface,
),
headlineMedium: newsreader.headlineMedium?.copyWith(
fontSize: 26,
height: 34 / 26,
fontWeight: FontWeight.w500,
color: colors.onSurface,
),
headlineSmall: newsreader.headlineSmall?.copyWith(
fontSize: 22,
height: 30 / 22,
fontWeight: FontWeight.w500,
color: colors.onSurface,
),
titleLarge: newsreader.titleLarge?.copyWith(
fontSize: 22,
height: 30 / 22,
fontWeight: FontWeight.w500,
color: colors.onSurface,
),
titleMedium: jakarta.titleMedium?.copyWith(
fontSize: 16,
fontWeight: FontWeight.w700,
color: colors.onSurface,
),
bodyLarge: jakarta.bodyLarge?.copyWith(
fontSize: 16,
height: 24 / 16,
fontWeight: FontWeight.w500,
color: colors.onSurface,
),
bodyMedium: jakarta.bodyMedium?.copyWith(
fontSize: 14,
height: 20 / 14,
fontWeight: FontWeight.w500,
color: colors.onSurfaceVariant,
),
bodySmall: jakarta.bodySmall?.copyWith(
fontSize: 12,
height: 16 / 12,
fontWeight: FontWeight.w500,
color: colors.onSurfaceVariant,
),
labelLarge: jakarta.labelLarge?.copyWith(
fontSize: 15,
height: 20 / 15,
fontWeight: FontWeight.w700,
letterSpacing: 0.15,
color: colors.onPrimary,
),
labelMedium: jakarta.labelMedium?.copyWith(
fontSize: 13,
height: 18 / 13,
fontWeight: FontWeight.w600,
letterSpacing: 0.2,
color: colors.onSurfaceVariant,
),
labelSmall: jakarta.labelSmall?.copyWith(
fontSize: 11,
height: 16 / 11,
fontWeight: FontWeight.w700,
letterSpacing: 0.4,
color: colors.onSurfaceVariant,
),
);
}
}