M3 Overhaul
This commit is contained in:
+233
-166
@@ -4,143 +4,59 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import 'app_typography.dart';
|
||||
import 'app_surfaces.dart';
|
||||
|
||||
/// M3 Expressive theme for TasQ.
|
||||
///
|
||||
/// Key differences from the previous Hybrid M2/M3 theme:
|
||||
/// * Cards use **tonal elevation** (surfaceTint color overlays) instead of
|
||||
/// drop-shadows, giving surfaces an organic, seed-tinted look.
|
||||
/// * Large containers use the M3 standard **28 dp** corner radius.
|
||||
/// * Buttons follow the M3 hierarchy: FilledButton (primary), Tonal, Elevated,
|
||||
/// Outlined, and Text.
|
||||
/// * NavigationBar / NavigationRail use pill-shaped indicators with the
|
||||
/// secondary-container tonal color.
|
||||
/// * Spring-physics inspired durations: transitions default to 400 ms with an
|
||||
/// emphasized easing curve.
|
||||
class AppTheme {
|
||||
/// The seed color drives M3's entire tonal palette generation.
|
||||
static const Color _seed = Color(0xFF4A6FA5);
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// LIGHT
|
||||
// ────────────────────────────────────────────────────────────
|
||||
static ThemeData light() {
|
||||
final base = ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF334155),
|
||||
seedColor: _seed,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
useMaterial3: true,
|
||||
);
|
||||
|
||||
final textTheme = GoogleFonts.spaceGroteskTextTheme(base.textTheme);
|
||||
final monoTheme = GoogleFonts.robotoMonoTextTheme(base.textTheme);
|
||||
final mono = AppMonoText(
|
||||
label:
|
||||
monoTheme.labelMedium?.copyWith(letterSpacing: 0.3) ??
|
||||
const TextStyle(letterSpacing: 0.3),
|
||||
body:
|
||||
monoTheme.bodyMedium?.copyWith(letterSpacing: 0.2) ??
|
||||
const TextStyle(letterSpacing: 0.2),
|
||||
);
|
||||
|
||||
final surfaces = AppSurfaces(
|
||||
cardRadius: 16,
|
||||
compactCardRadius: 12,
|
||||
dialogRadius: 20,
|
||||
cardElevation: 3,
|
||||
cardShadowColor: const Color.fromRGBO(0, 0, 0, 0.12),
|
||||
compactShadowColor: const Color.fromRGBO(0, 0, 0, 0.08),
|
||||
);
|
||||
|
||||
return base.copyWith(
|
||||
textTheme: textTheme,
|
||||
scaffoldBackgroundColor: base.colorScheme.surfaceContainerLowest,
|
||||
extensions: [mono, surfaces],
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: base.colorScheme.surface,
|
||||
foregroundColor: base.colorScheme.onSurface,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 1,
|
||||
surfaceTintColor: base.colorScheme.surfaceTint,
|
||||
centerTitle: false,
|
||||
titleTextStyle: textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: base.colorScheme.surface,
|
||||
elevation: 3, // M2-style elevation for visible separation (2-4 allowed)
|
||||
margin: EdgeInsets.zero,
|
||||
shadowColor: const Color.fromRGBO(0, 0, 0, 0.12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: base.colorScheme.outlineVariant, width: 1),
|
||||
),
|
||||
),
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: base.colorScheme.surfaceContainerHighest,
|
||||
side: BorderSide(color: base.colorScheme.outlineVariant),
|
||||
labelStyle: textTheme.labelSmall,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
dividerTheme: DividerThemeData(
|
||||
color: base.colorScheme.outlineVariant,
|
||||
thickness: 1,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: base.colorScheme.surfaceContainerLow,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: base.colorScheme.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: base.colorScheme.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: base.colorScheme.primary, width: 1.5),
|
||||
),
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
side: BorderSide(color: base.colorScheme.outline),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
),
|
||||
),
|
||||
navigationDrawerTheme: NavigationDrawerThemeData(
|
||||
backgroundColor: base.colorScheme.surface,
|
||||
indicatorColor: base.colorScheme.secondaryContainer,
|
||||
tileHeight: 52,
|
||||
),
|
||||
navigationRailTheme: NavigationRailThemeData(
|
||||
backgroundColor: base.colorScheme.surface,
|
||||
selectedIconTheme: IconThemeData(color: base.colorScheme.primary),
|
||||
selectedLabelTextStyle: textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
unselectedIconTheme: IconThemeData(color: base.colorScheme.onSurface),
|
||||
indicatorColor: base.colorScheme.secondaryContainer,
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: base.colorScheme.surface,
|
||||
indicatorColor: base.colorScheme.primaryContainer,
|
||||
labelTextStyle: WidgetStateProperty.all(
|
||||
textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
listTileTheme: ListTileThemeData(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
tileColor: base.colorScheme.surface,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
),
|
||||
);
|
||||
return _apply(base);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// DARK
|
||||
// ────────────────────────────────────────────────────────────
|
||||
static ThemeData dark() {
|
||||
final base = ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF334155),
|
||||
seedColor: _seed,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
useMaterial3: true,
|
||||
);
|
||||
|
||||
return _apply(base);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// SHARED BUILDER
|
||||
// ────────────────────────────────────────────────────────────
|
||||
static ThemeData _apply(ThemeData base) {
|
||||
final cs = base.colorScheme;
|
||||
final isDark = cs.brightness == Brightness.dark;
|
||||
|
||||
final textTheme = GoogleFonts.spaceGroteskTextTheme(base.textTheme);
|
||||
final monoTheme = GoogleFonts.robotoMonoTextTheme(base.textTheme);
|
||||
final mono = AppMonoText(
|
||||
@@ -152,110 +68,261 @@ class AppTheme {
|
||||
const TextStyle(letterSpacing: 0.2),
|
||||
);
|
||||
|
||||
final surfaces = AppSurfaces(
|
||||
const surfaces = AppSurfaces(
|
||||
cardRadius: 16,
|
||||
compactCardRadius: 12,
|
||||
dialogRadius: 20,
|
||||
cardElevation: 3,
|
||||
cardShadowColor: const Color.fromRGBO(0, 0, 0, 0.24),
|
||||
compactShadowColor: const Color.fromRGBO(0, 0, 0, 0.12),
|
||||
containerRadius: 28,
|
||||
dialogRadius: 28,
|
||||
chipRadius: 12,
|
||||
);
|
||||
|
||||
return base.copyWith(
|
||||
textTheme: textTheme,
|
||||
scaffoldBackgroundColor: base.colorScheme.surface,
|
||||
scaffoldBackgroundColor: isDark ? cs.surface : cs.surfaceContainerLowest,
|
||||
extensions: [mono, surfaces],
|
||||
|
||||
// ── AppBar ──────────────────────────────────────────────
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: base.colorScheme.surface,
|
||||
foregroundColor: base.colorScheme.onSurface,
|
||||
backgroundColor: cs.surface,
|
||||
foregroundColor: cs.onSurface,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 1,
|
||||
surfaceTintColor: base.colorScheme.surfaceTint,
|
||||
scrolledUnderElevation: 2,
|
||||
surfaceTintColor: cs.surfaceTint,
|
||||
centerTitle: false,
|
||||
titleTextStyle: textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
|
||||
// ── Cards — M3 Elevated (tonal surface tint, no hard shadow) ──
|
||||
cardTheme: CardThemeData(
|
||||
color: base.colorScheme.surfaceContainer,
|
||||
elevation: 3, // M2-style elevation for visible separation (2-4 allowed)
|
||||
color: isDark ? cs.surfaceContainer : cs.surfaceContainerLow,
|
||||
elevation: 1,
|
||||
margin: EdgeInsets.zero,
|
||||
shadowColor: const Color.fromRGBO(0, 0, 0, 0.24),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: base.colorScheme.outlineVariant, width: 1),
|
||||
),
|
||||
shadowColor: Colors.transparent,
|
||||
surfaceTintColor: cs.surfaceTint,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
|
||||
// ── Chips ───────────────────────────────────────────────
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: base.colorScheme.surfaceContainerHighest,
|
||||
side: BorderSide(color: base.colorScheme.outlineVariant),
|
||||
backgroundColor: cs.surfaceContainerHighest,
|
||||
side: BorderSide.none,
|
||||
labelStyle: textTheme.labelSmall,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
dividerTheme: DividerThemeData(
|
||||
color: base.colorScheme.outlineVariant,
|
||||
thickness: 1,
|
||||
),
|
||||
|
||||
// ── Dividers ────────────────────────────────────────────
|
||||
dividerTheme: DividerThemeData(color: cs.outlineVariant, thickness: 1),
|
||||
|
||||
// ── Input Fields ────────────────────────────────────────
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: base.colorScheme.surfaceContainerLow,
|
||||
fillColor: cs.surfaceContainerLow,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: base.colorScheme.outlineVariant),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: cs.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: base.colorScheme.outlineVariant),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: cs.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: base.colorScheme.primary, width: 1.5),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: cs.primary, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
|
||||
// ── Buttons — M3 Expressive hierarchy ───────────────────
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
textStyle: textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: cs.surfaceContainerLow,
|
||||
foregroundColor: cs.primary,
|
||||
elevation: 1,
|
||||
shadowColor: Colors.transparent,
|
||||
surfaceTintColor: cs.surfaceTint,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
textStyle: textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
side: BorderSide(color: cs.outline),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
textStyle: textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
textStyle: textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
iconButtonTheme: IconButtonThemeData(
|
||||
style: IconButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
side: BorderSide(color: base.colorScheme.outline),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
),
|
||||
),
|
||||
segmentedButtonTheme: SegmentedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── FAB — M3 Expressive ──────────────────────────────────
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: cs.primaryContainer,
|
||||
foregroundColor: cs.onPrimaryContainer,
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
|
||||
// ── Navigation — M3 Expressive pill indicators ──────────
|
||||
navigationDrawerTheme: NavigationDrawerThemeData(
|
||||
backgroundColor: base.colorScheme.surface,
|
||||
indicatorColor: base.colorScheme.secondaryContainer,
|
||||
tileHeight: 52,
|
||||
backgroundColor: cs.surface,
|
||||
indicatorColor: cs.secondaryContainer,
|
||||
tileHeight: 56,
|
||||
indicatorShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
),
|
||||
navigationRailTheme: NavigationRailThemeData(
|
||||
backgroundColor: base.colorScheme.surface,
|
||||
selectedIconTheme: IconThemeData(color: base.colorScheme.primary),
|
||||
selectedLabelTextStyle: textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
backgroundColor: cs.surface,
|
||||
selectedIconTheme: IconThemeData(color: cs.onSecondaryContainer),
|
||||
unselectedIconTheme: IconThemeData(color: cs.onSurfaceVariant),
|
||||
selectedLabelTextStyle: textTheme.labelMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
unselectedLabelTextStyle: textTheme.labelMedium?.copyWith(
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
indicatorColor: cs.secondaryContainer,
|
||||
indicatorShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
unselectedIconTheme: IconThemeData(color: base.colorScheme.onSurface),
|
||||
indicatorColor: base.colorScheme.secondaryContainer,
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: base.colorScheme.surface,
|
||||
indicatorColor: base.colorScheme.primaryContainer,
|
||||
labelTextStyle: WidgetStateProperty.all(
|
||||
textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
backgroundColor: cs.surfaceContainer,
|
||||
indicatorColor: cs.secondaryContainer,
|
||||
indicatorShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||
final selected = states.contains(WidgetState.selected);
|
||||
return textTheme.labelMedium?.copyWith(
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||||
color: selected ? cs.onSurface : cs.onSurfaceVariant,
|
||||
);
|
||||
}),
|
||||
elevation: 2,
|
||||
surfaceTintColor: cs.surfaceTint,
|
||||
),
|
||||
|
||||
// ── List Tiles ──────────────────────────────────────────
|
||||
listTileTheme: ListTileThemeData(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
tileColor: base.colorScheme.surfaceContainer,
|
||||
tileColor: Colors.transparent,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
),
|
||||
|
||||
// ── Dialogs ─────────────────────────────────────────────
|
||||
dialogTheme: DialogThemeData(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||
surfaceTintColor: cs.surfaceTint,
|
||||
backgroundColor: isDark
|
||||
? cs.surfaceContainerHigh
|
||||
: cs.surfaceContainerLowest,
|
||||
),
|
||||
|
||||
// ── Bottom Sheets ───────────────────────────────────────
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
),
|
||||
backgroundColor: isDark
|
||||
? cs.surfaceContainerHigh
|
||||
: cs.surfaceContainerLowest,
|
||||
surfaceTintColor: cs.surfaceTint,
|
||||
showDragHandle: true,
|
||||
),
|
||||
|
||||
// ── Snackbar ────────────────────────────────────────────
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
|
||||
// ── Search Bar ──────────────────────────────────────────
|
||||
searchBarTheme: SearchBarThemeData(
|
||||
elevation: WidgetStateProperty.all(0),
|
||||
backgroundColor: WidgetStateProperty.all(cs.surfaceContainerHigh),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Tooltips ────────────────────────────────────────────
|
||||
tooltipTheme: TooltipThemeData(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.inverseSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
textStyle: textTheme.bodySmall?.copyWith(color: cs.onInverseSurface),
|
||||
),
|
||||
|
||||
// ── Tab Bar ─────────────────────────────────────────────
|
||||
tabBarTheme: TabBarThemeData(
|
||||
labelStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700),
|
||||
unselectedLabelStyle: textTheme.labelLarge,
|
||||
indicatorColor: cs.primary,
|
||||
labelColor: cs.primary,
|
||||
unselectedLabelColor: cs.onSurfaceVariant,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
dividerColor: cs.outlineVariant,
|
||||
),
|
||||
|
||||
// ── PopupMenu ───────────────────────────────────────────
|
||||
popupMenuTheme: PopupMenuThemeData(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
surfaceTintColor: cs.surfaceTint,
|
||||
color: isDark ? cs.surfaceContainerHigh : cs.surfaceContainerLowest,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user