/opt/canhelp/apps/mobile/lib/theme
Edit: /opt/canhelp/apps/mobile/lib/theme/app_theme.dart (17852B)
import 'package:flutter/material.dart';
class AppTheme {
AppTheme._();
// ─── Brand palette (Green) ───
static const primary = Color(0xFF22C55E);
static const primaryDark = Color(0xFF16A34A);
static const primaryLight = Color(0xFFF0FDF4);
static const primaryContainer = Color(0xFFDCFCE7);
// ─── Slate surface palette ───
static const background = Color(0xFFF1F5F9); // slate-100
static const surface = Color(0xFFFFFFFF);
static const surfaceVariant = Color(0xFFF8FAFC); // slate-50 (input fill)
static const border = Color(0xFFE2E8F0); // slate-200
// ─── Text (Slate) ───
static const textPrimary = Color(0xFF0F172A); // slate-900
static const textSecondary = Color(0xFF64748B); // slate-500
static const textHint = Color(0xFF94A3B8); // slate-400
// ─── Semantic status ───
static const success = Color(0xFF16A34A);
static const successLight = Color(0xFFDCFCE7);
static const warning = Color(0xFFD97706);
static const warningLight = Color(0xFFFEF3C7);
static const danger = Color(0xFFDC2626);
static const dangerLight = Color(0xFFFEE2E2);
// ─── Extra accents ───
static const emerald = Color(0xFF10B981); // online / verified
static const amber = Color(0xFFF59E0B); // star rating
static const ratingText = Color(0xFFCA8A04); // Yellow-600 – star-rating text
// ─── Amber shades (for confidential/warning blocks) ───
static const amber50 = Color(0xFFFFFBEB);
static const amber200 = Color(0xFFFDE68A);
static const amber700 = Color(0xFFB45309);
static const amber900 = Color(0xFF92400E);
static const dangerBorder = Color(0xFFFCA5A5);
// ─── Category colors ───
static const Map
_categoryColors = {
'cleaning': Color(0xFF22C55E),
'home-cleaning': Color(0xFF22C55E),
'repairs': Color(0xFFEF4444),
'plumbing': Color(0xFF3B82F6),
'electrical': Color(0xFFF59E0B),
'moving': Color(0xFF8B5CF6),
'tutoring': Color(0xFF06B6D4),
'english': Color(0xFF06B6D4),
'math': Color(0xFF06B6D4),
'beauty': Color(0xFFEC4899),
'it': Color(0xFF6366F1),
'it_support': Color(0xFF6366F1),
'design': Color(0xFF8B5CF6),
'events': Color(0xFFEC4899),
'pets': Color(0xFF10B981),
'auto': Color(0xFF6366F1),
'legal': Color(0xFF0EA5E9),
'accounting': Color(0xFF14B8A6),
'tech-repair': Color(0xFFF59E0B),
'photo-video': Color(0xFFEC4899),
'other': Color(0xFF64748B),
};
static Color categoryColor(String? slug) =>
slug != null ? (_categoryColors[slug] ?? textSecondary) : textSecondary;
static IconData categoryIcon(String? slug) {
switch (slug) {
case 'cleaning':
case 'home-cleaning':
return Icons.cleaning_services_rounded;
case 'plumbing':
return Icons.plumbing_rounded;
case 'electrical':
return Icons.electrical_services_rounded;
case 'moving':
return Icons.local_shipping_rounded;
case 'tutoring':
case 'english':
case 'math':
return Icons.school_rounded;
case 'beauty':
return Icons.face_retouching_natural_rounded;
case 'it':
case 'it_support':
return Icons.computer_rounded;
case 'design':
return Icons.palette_rounded;
case 'events':
return Icons.celebration_rounded;
case 'pets':
return Icons.pets_rounded;
case 'auto':
return Icons.directions_car_rounded;
case 'legal':
return Icons.gavel_rounded;
case 'accounting':
return Icons.calculate_rounded;
case 'tech-repair':
return Icons.build_circle_rounded;
case 'photo-video':
return Icons.camera_alt_rounded;
case 'repairs':
return Icons.build_rounded;
default:
return Icons.category_rounded;
}
}
// ─── Shadows ───
static const shadowColor = Color(0x0F000000);
static const shadowColorLight = Color(0x08000000);
// ─── Radii ───
static const double cardRadius = 16.0;
static const double buttonRadius = 12.0;
static const double inputRadius = 12.0;
static const double chipRadius = 20.0;
static const double smallRadius = 8.0;
static const _bodyFont = 'DM Sans';
static const _displayFont = 'Outfit';
static const _fallbackFonts = ['.AppleSystemUIFont', 'Roboto', 'sans-serif'];
static ThemeData get light {
final colorScheme = ColorScheme(
brightness: Brightness.light,
primary: primary,
onPrimary: Colors.white,
primaryContainer: primaryContainer,
onPrimaryContainer: primaryDark,
secondary: const Color(0xFF7C3AED),
onSecondary: Colors.white,
secondaryContainer: const Color(0xFFEDE9FE),
onSecondaryContainer: const Color(0xFF5B21B6),
tertiary: const Color(0xFF0891B2),
onTertiary: Colors.white,
surface: surface,
onSurface: textPrimary,
onSurfaceVariant: textSecondary,
error: danger,
onError: Colors.white,
outline: const Color(0xFFCBD5E1),
outlineVariant: border,
shadow: shadowColorLight,
inverseSurface: const Color(0xFF1E293B),
onInverseSurface: const Color(0xFFF8FAFC),
inversePrimary: const Color(0xFF86EFAC),
surfaceContainerHighest: background,
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
scaffoldBackgroundColor: background,
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
textTheme: _buildTextTheme(colorScheme),
appBarTheme: const AppBarTheme(
centerTitle: false,
elevation: 0,
scrolledUnderElevation: 1,
backgroundColor: primary,
foregroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
iconTheme: IconThemeData(color: Colors.white),
actionsIconTheme: IconThemeData(color: Colors.white),
titleTextStyle: TextStyle(
fontFamily: _displayFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
cardTheme: CardThemeData(
elevation: 0,
shadowColor: Colors.black.withValues(alpha: 0.06),
surfaceTintColor: Colors.transparent,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(cardRadius),
side: const BorderSide(color: border),
),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(buttonRadius),
),
textStyle: const TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(buttonRadius),
),
textStyle: const TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: primary,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(buttonRadius),
),
side: const BorderSide(color: primary, width: 1.5),
textStyle: const TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: primary,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(buttonRadius),
),
textStyle: const TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: primary,
foregroundColor: Colors.white,
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(cardRadius),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(inputRadius),
borderSide: const BorderSide(color: border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(inputRadius),
borderSide: const BorderSide(color: border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(inputRadius),
borderSide: const BorderSide(color: primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(inputRadius),
borderSide: const BorderSide(color: danger),
),
hintStyle: const TextStyle(
color: textHint,
fontWeight: FontWeight.w400,
),
),
chipTheme: ChipThemeData(
backgroundColor: background,
selectedColor: primaryLight,
labelStyle: const TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 13,
fontWeight: FontWeight.w500,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(chipRadius),
),
side: BorderSide.none,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: Colors.white,
indicatorColor: primaryLight,
elevation: 2,
surfaceTintColor: Colors.transparent,
labelTextStyle: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 11,
fontWeight: FontWeight.w600,
color: primary,
);
}
return const TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 11,
fontWeight: FontWeight.w400,
color: textSecondary,
);
}),
iconTheme: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const IconThemeData(color: primary);
}
return const IconThemeData(color: textSecondary);
}),
),
dividerTheme: const DividerThemeData(
color: border,
thickness: 1,
space: 1,
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(buttonRadius),
),
backgroundColor: const Color(0xFF1E293B),
),
dialogTheme: DialogThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(cardRadius),
),
surfaceTintColor: Colors.transparent,
backgroundColor: Colors.white,
),
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
showDragHandle: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
),
);
}
static ThemeData get dark {
final colorScheme = ColorScheme(
brightness: Brightness.dark,
primary: const Color(0xFF60A5FA),
onPrimary: const Color(0xFF1E3A5F),
primaryContainer: const Color(0xFF1D4ED8),
onPrimaryContainer: const Color(0xFFBFDBFE),
secondary: const Color(0xFFA78BFA),
onSecondary: const Color(0xFF2E1065),
secondaryContainer: const Color(0xFF5B21B6),
onSecondaryContainer: const Color(0xFFEDE9FE),
tertiary: const Color(0xFF22D3EE),
onTertiary: const Color(0xFF164E63),
surface: const Color(0xFF1C1B1F),
onSurface: const Color(0xFFE6E1E5),
onSurfaceVariant: const Color(0xFF9CA3AF),
error: const Color(0xFFFCA5A5),
onError: const Color(0xFF7F1D1D),
outline: const Color(0xFF4B5563),
outlineVariant: const Color(0xFF374151),
shadow: Colors.black.withValues(alpha: 0.3),
inverseSurface: const Color(0xFFE6E1E5),
onInverseSurface: const Color(0xFF313033),
inversePrimary: primary,
surfaceContainerHighest: const Color(0xFF111827),
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
scaffoldBackgroundColor: const Color(0xFF111827),
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
textTheme: _buildTextTheme(colorScheme),
appBarTheme: AppBarTheme(
centerTitle: false,
elevation: 0,
backgroundColor: const Color(0xFF1F2937),
foregroundColor: colorScheme.onSurface,
surfaceTintColor: Colors.transparent,
),
cardTheme: CardThemeData(
elevation: 0,
color: const Color(0xFF1F2937),
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(cardRadius),
side: const BorderSide(color: Color(0xFF374151)),
),
),
);
}
static TextTheme _buildTextTheme(ColorScheme scheme) {
final onSurface = scheme.onSurface;
final muted = scheme.onSurfaceVariant;
return TextTheme(
displayLarge: TextStyle(
fontFamily: _displayFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 57,
fontWeight: FontWeight.w700,
letterSpacing: -0.25,
color: onSurface,
),
displayMedium: TextStyle(
fontFamily: _displayFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 45,
fontWeight: FontWeight.w700,
color: onSurface,
),
displaySmall: TextStyle(
fontFamily: _displayFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 36,
fontWeight: FontWeight.w600,
color: onSurface,
),
headlineLarge: TextStyle(
fontFamily: _displayFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 32,
fontWeight: FontWeight.w600,
color: onSurface,
),
headlineMedium: TextStyle(
fontFamily: _displayFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 28,
fontWeight: FontWeight.w600,
color: onSurface,
),
headlineSmall: TextStyle(
fontFamily: _displayFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 24,
fontWeight: FontWeight.w600,
color: onSurface,
),
titleLarge: TextStyle(
fontFamily: _displayFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 22,
fontWeight: FontWeight.w600,
color: onSurface,
),
titleMedium: TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 16,
fontWeight: FontWeight.w600,
color: onSurface,
),
titleSmall: TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 14,
fontWeight: FontWeight.w600,
color: onSurface,
),
bodyLarge: TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 16,
fontWeight: FontWeight.w400,
color: onSurface,
),
bodyMedium: TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 14,
fontWeight: FontWeight.w400,
color: muted,
),
bodySmall: TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 12,
fontWeight: FontWeight.w400,
color: muted,
),
labelLarge: TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 14,
fontWeight: FontWeight.w600,
color: onSurface,
),
labelMedium: TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 12,
fontWeight: FontWeight.w500,
color: muted,
),
labelSmall: TextStyle(
fontFamily: _bodyFont,
fontFamilyFallback: _fallbackFonts,
fontSize: 11,
fontWeight: FontWeight.w500,
color: muted,
),
);
}
}