381 lines
13 KiB
Dart
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,
|
|
),
|
|
);
|
|
}
|
|
}
|