329 lines
14 KiB
Dart
329 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
|
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: _seed,
|
|
brightness: Brightness.light,
|
|
),
|
|
useMaterial3: true,
|
|
);
|
|
|
|
return _apply(base);
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────
|
|
// DARK
|
|
// ────────────────────────────────────────────────────────────
|
|
static ThemeData dark() {
|
|
final base = ThemeData(
|
|
colorScheme: ColorScheme.fromSeed(
|
|
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(
|
|
label:
|
|
monoTheme.labelMedium?.copyWith(letterSpacing: 0.3) ??
|
|
const TextStyle(letterSpacing: 0.3),
|
|
body:
|
|
monoTheme.bodyMedium?.copyWith(letterSpacing: 0.2) ??
|
|
const TextStyle(letterSpacing: 0.2),
|
|
);
|
|
|
|
const surfaces = AppSurfaces(
|
|
cardRadius: 16,
|
|
compactCardRadius: 12,
|
|
containerRadius: 28,
|
|
dialogRadius: 28,
|
|
chipRadius: 12,
|
|
);
|
|
|
|
return base.copyWith(
|
|
textTheme: textTheme,
|
|
scaffoldBackgroundColor: isDark ? cs.surface : cs.surfaceContainerLowest,
|
|
extensions: [mono, surfaces],
|
|
|
|
// ── AppBar ──────────────────────────────────────────────
|
|
appBarTheme: AppBarTheme(
|
|
backgroundColor: cs.surface,
|
|
foregroundColor: cs.onSurface,
|
|
elevation: 0,
|
|
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: isDark ? cs.surfaceContainer : cs.surfaceContainerLow,
|
|
elevation: 1,
|
|
margin: EdgeInsets.zero,
|
|
shadowColor: Colors.transparent,
|
|
surfaceTintColor: cs.surfaceTint,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
),
|
|
|
|
// ── Chips ───────────────────────────────────────────────
|
|
chipTheme: ChipThemeData(
|
|
backgroundColor: cs.surfaceContainerHighest,
|
|
side: BorderSide.none,
|
|
labelStyle: textTheme.labelSmall,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
),
|
|
|
|
// ── Dividers ────────────────────────────────────────────
|
|
dividerTheme: DividerThemeData(color: cs.outlineVariant, thickness: 1),
|
|
|
|
// ── Input Fields ────────────────────────────────────────
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: cs.surfaceContainerLow,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
borderSide: BorderSide(color: cs.outlineVariant),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
borderSide: BorderSide(color: cs.outlineVariant),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
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(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,
|
|
),
|
|
),
|
|
),
|
|
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),
|
|
),
|
|
),
|
|
),
|
|
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: cs.surface,
|
|
indicatorColor: cs.secondaryContainer,
|
|
tileHeight: 56,
|
|
indicatorShape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(28),
|
|
),
|
|
),
|
|
navigationRailTheme: NavigationRailThemeData(
|
|
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),
|
|
),
|
|
),
|
|
navigationBarTheme: NavigationBarThemeData(
|
|
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: 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,
|
|
),
|
|
);
|
|
}
|
|
}
|