Files
lab-app/lib/core/theme/app_theme.dart
T
2026-06-10 23:22:15 +03:00

300 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
abstract final class AppColors {
// Primary — professional navy
static const primary = Color(0xFF1E3A5F);
static const onPrimary = Color(0xFFFFFFFF);
// Accent — sky blue CTA
static const accent = Color(0xFF0369A1);
static const onAccent = Color(0xFFFFFFFF);
// Status
static const pending = Color(0xFFF59E0B);
static const pendingBg = Color(0xFFFFFBEB);
static const inProgress = Color(0xFF0369A1);
static const inProgressBg = Color(0xFFEFF6FF);
static const success = Color(0xFF059669);
static const successBg = Color(0xFFECFDF5);
static const cancelled = Color(0xFFDC2626);
static const cancelledBg = Color(0xFFFEF2F2);
// Surfaces
static const background = Color(0xFFF1F5F9);
static const surface = Color(0xFFFFFFFF);
static const surfaceVariant = Color(0xFFF8FAFC);
static const muted = Color(0xFFE2E8F0);
static const border = Color(0xFFE2E8F0);
// Text
static const textPrimary = Color(0xFF0F172A);
static const textSecondary = Color(0xFF64748B);
static const textMuted = Color(0xFF94A3B8);
// Dark variants
static const darkBackground = Color(0xFF0F172A);
static const darkSurface = Color(0xFF1E293B);
static const darkSurfaceVariant = Color(0xFF273344);
static const darkBorder = Color(0xFF334155);
static const darkTextPrimary = Color(0xFFF1F5F9);
static const darkTextSecondary = Color(0xFF94A3B8);
}
abstract final class AppLayout {
/// Window width above which the sidebar navigation is shown instead of bottom nav.
static const double sidebarBreakpoint = 720.0;
/// Window width above which wide-desktop content layouts activate
/// (e.g., 3-column stat card row, 2-column forms).
static const double wideBreakpoint = 1100.0;
/// Maximum content width used for dashboard horizontal padding.
static const double contentMaxWidth = 1040.0;
}
abstract final class AppTheme {
static TextTheme _buildTextTheme(Color bodyColor, Color displayColor) {
final base = GoogleFonts.plusJakartaSansTextTheme();
return base.copyWith(
displayLarge: base.displayLarge?.copyWith(color: displayColor, fontWeight: FontWeight.w800),
displayMedium: base.displayMedium?.copyWith(color: displayColor, fontWeight: FontWeight.w700),
headlineLarge: base.headlineLarge?.copyWith(color: displayColor, fontWeight: FontWeight.w700),
headlineMedium: base.headlineMedium?.copyWith(color: displayColor, fontWeight: FontWeight.w700),
headlineSmall: base.headlineSmall?.copyWith(color: displayColor, fontWeight: FontWeight.w600),
titleLarge: base.titleLarge?.copyWith(color: displayColor, fontWeight: FontWeight.w600),
titleMedium: base.titleMedium?.copyWith(color: displayColor, fontWeight: FontWeight.w600),
titleSmall: base.titleSmall?.copyWith(color: displayColor, fontWeight: FontWeight.w500),
bodyLarge: base.bodyLarge?.copyWith(color: bodyColor),
bodyMedium: base.bodyMedium?.copyWith(color: bodyColor),
bodySmall: base.bodySmall?.copyWith(color: AppColors.textSecondary),
labelLarge: base.labelLarge?.copyWith(fontWeight: FontWeight.w600),
labelMedium: base.labelMedium?.copyWith(fontWeight: FontWeight.w500),
);
}
static final light = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme(
brightness: Brightness.light,
primary: AppColors.primary,
onPrimary: AppColors.onPrimary,
primaryContainer: const Color(0xFFDBEAFE),
onPrimaryContainer: AppColors.primary,
secondary: AppColors.accent,
onSecondary: AppColors.onAccent,
secondaryContainer: const Color(0xFFE0F2FE),
onSecondaryContainer: AppColors.accent,
tertiary: AppColors.success,
onTertiary: Colors.white,
tertiaryContainer: AppColors.successBg,
onTertiaryContainer: AppColors.success,
error: AppColors.cancelled,
onError: Colors.white,
errorContainer: AppColors.cancelledBg,
onErrorContainer: AppColors.cancelled,
surface: AppColors.surface,
onSurface: AppColors.textPrimary,
surfaceContainerHighest: AppColors.surfaceVariant,
onSurfaceVariant: AppColors.textSecondary,
outline: AppColors.border,
outlineVariant: AppColors.muted,
scrim: Colors.black54,
inverseSurface: AppColors.darkSurface,
onInverseSurface: AppColors.darkTextPrimary,
inversePrimary: const Color(0xFF93C5FD),
),
scaffoldBackgroundColor: AppColors.background,
textTheme: _buildTextTheme(AppColors.textPrimary, AppColors.textPrimary),
appBarTheme: AppBarTheme(
backgroundColor: AppColors.surface,
foregroundColor: AppColors.textPrimary,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
shadowColor: Colors.transparent,
centerTitle: false,
systemOverlayStyle: SystemUiOverlayStyle.dark,
titleTextStyle: GoogleFonts.plusJakartaSans(
fontSize: 17,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
iconTheme: const IconThemeData(color: AppColors.textPrimary, size: 22),
),
cardTheme: CardThemeData(
elevation: 0,
color: AppColors.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: AppColors.border, width: 1),
),
margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias,
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: AppColors.surface,
elevation: 0,
shadowColor: Colors.transparent,
indicatorColor: const Color(0xFFDBEAFE),
iconTheme: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const IconThemeData(color: AppColors.primary, size: 22);
}
return IconThemeData(color: AppColors.textSecondary, size: 22);
}),
labelTextStyle: WidgetStateProperty.resolveWith((states) {
final style = GoogleFonts.plusJakartaSans(fontSize: 11);
if (states.contains(WidgetState.selected)) {
return style.copyWith(fontWeight: FontWeight.w600, color: AppColors.primary);
}
return style.copyWith(fontWeight: FontWeight.w500, color: AppColors.textSecondary);
}),
surfaceTintColor: Colors.transparent,
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
textStyle: GoogleFonts.plusJakartaSans(fontSize: 15, fontWeight: FontWeight.w600),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
minimumSize: const Size(0, 48),
side: const BorderSide(color: AppColors.border, width: 1.5),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
textStyle: GoogleFonts.plusJakartaSans(fontSize: 15, fontWeight: FontWeight.w600),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.surfaceVariant,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.accent, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.cancelled, width: 1.5),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
labelStyle: GoogleFonts.plusJakartaSans(color: AppColors.textSecondary),
hintStyle: GoogleFonts.plusJakartaSans(color: AppColors.textMuted),
),
chipTheme: ChipThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
side: BorderSide.none,
),
dividerTheme: const DividerThemeData(
color: AppColors.border,
thickness: 1,
space: 1,
),
listTileTheme: const ListTileThemeData(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
),
);
static final dark = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme(
brightness: Brightness.dark,
primary: const Color(0xFF93C5FD),
onPrimary: const Color(0xFF1E3A5F),
primaryContainer: const Color(0xFF1E3A5F),
onPrimaryContainer: const Color(0xFFDBEAFE),
secondary: const Color(0xFF7DD3FC),
onSecondary: const Color(0xFF0C4A6E),
secondaryContainer: const Color(0xFF0C4A6E),
onSecondaryContainer: const Color(0xFFE0F2FE),
tertiary: const Color(0xFF6EE7B7),
onTertiary: const Color(0xFF064E3B),
tertiaryContainer: const Color(0xFF064E3B),
onTertiaryContainer: const Color(0xFFD1FAE5),
error: const Color(0xFFFCA5A5),
onError: const Color(0xFF7F1D1D),
errorContainer: const Color(0xFF7F1D1D),
onErrorContainer: const Color(0xFFFEE2E2),
surface: AppColors.darkSurface,
onSurface: AppColors.darkTextPrimary,
surfaceContainerHighest: AppColors.darkSurfaceVariant,
onSurfaceVariant: AppColors.darkTextSecondary,
outline: AppColors.darkBorder,
outlineVariant: const Color(0xFF1E293B),
scrim: Colors.black87,
inverseSurface: const Color(0xFFF1F5F9),
onInverseSurface: AppColors.textPrimary,
inversePrimary: AppColors.primary,
),
scaffoldBackgroundColor: AppColors.darkBackground,
textTheme: _buildTextTheme(AppColors.darkTextPrimary, AppColors.darkTextPrimary),
appBarTheme: AppBarTheme(
backgroundColor: AppColors.darkSurface,
foregroundColor: AppColors.darkTextPrimary,
elevation: 0,
scrolledUnderElevation: 1,
systemOverlayStyle: SystemUiOverlayStyle.light,
titleTextStyle: GoogleFonts.plusJakartaSans(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.darkTextPrimary,
),
),
cardTheme: CardThemeData(
elevation: 0,
color: AppColors.darkSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: AppColors.darkBorder, width: 1),
),
margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias,
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: AppColors.darkSurface,
elevation: 0,
indicatorColor: const Color(0xFF1E3A5F),
iconTheme: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const IconThemeData(color: Color(0xFF93C5FD), size: 22);
}
return IconThemeData(color: AppColors.darkTextSecondary, size: 22);
}),
labelTextStyle: WidgetStateProperty.resolveWith((states) {
final style = GoogleFonts.plusJakartaSans(fontSize: 11);
if (states.contains(WidgetState.selected)) {
return style.copyWith(fontWeight: FontWeight.w600, color: const Color(0xFF93C5FD));
}
return style.copyWith(fontWeight: FontWeight.w500, color: AppColors.darkTextSecondary);
}),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFF93C5FD),
foregroundColor: const Color(0xFF1E3A5F),
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
textStyle: GoogleFonts.plusJakartaSans(fontSize: 15, fontWeight: FontWeight.w600),
),
),
dividerTheme: const DividerThemeData(
color: AppColors.darkBorder,
thickness: 1,
space: 1,
),
);
}