From a0ebd6cc0b6f3678beb7653d555bd3aa3cde044f Mon Sep 17 00:00:00 2001 From: dbastrikin Date: Sun, 22 Feb 2026 15:54:54 +0200 Subject: [PATCH] feat: apply iOS-style theme and replace removed color constants Switch AppColors to iOS system palette (007AFF blue, F2F2F7 grouped background, separator, label hierarchy) and rewrite AppTheme with iOS-inspired Material 3 tokens (no elevation, negative letter-spacing, 50px buttons, 12px radii). Replace removed primaryLight/accent references in recipe screens with primary.withValues(alpha:0.15) and primary. Co-Authored-By: Claude Sonnet 4.6 --- client/lib/core/theme/app_colors.dart | 39 ++- client/lib/core/theme/app_theme.dart | 239 +++++++++++++++++- .../recipes/recipe_detail_screen.dart | 10 +- .../recipes/saved_recipes_screen.dart | 4 +- .../features/recipes/widgets/recipe_card.dart | 6 +- 5 files changed, 261 insertions(+), 37 deletions(-) diff --git a/client/lib/core/theme/app_colors.dart b/client/lib/core/theme/app_colors.dart index c13b0ce..ada2ed9 100644 --- a/client/lib/core/theme/app_colors.dart +++ b/client/lib/core/theme/app_colors.dart @@ -1,29 +1,28 @@ import 'package:flutter/material.dart'; abstract class AppColors { - // Primary - static const primary = Color(0xFF4CAF50); - static const primaryLight = Color(0xFF81C784); - static const primaryDark = Color(0xFF388E3C); + // iOS System Blue + static const primary = Color(0xFF007AFF); - // Accent - static const accent = Color(0xFFFF9800); + // Backgrounds — iOS grouped layout + static const background = Color(0xFFF2F2F7); // systemGroupedBackground + static const surface = Color(0xFFFFFFFF); // secondarySystemGroupedBackground + static const surfaceTertiary = Color(0xFFF2F2F7); - // Background - static const background = Color(0xFFF5F5F5); - static const surface = Color(0xFFFFFFFF); + // Text — iOS label hierarchy + static const textPrimary = Color(0xFF000000); // label + static const textSecondary = Color(0xFF8E8E93); // secondaryLabel - // Text - static const textPrimary = Color(0xFF212121); - static const textSecondary = Color(0xFF757575); + // Separator + static const separator = Color(0xFFC6C6C8); - // Status - static const error = Color(0xFFE53935); - static const warning = Color(0xFFFFA726); - static const success = Color(0xFF66BB6A); + // iOS System colors + static const error = Color(0xFFFF3B30); // systemRed + static const warning = Color(0xFFFF9500); // systemOrange + static const success = Color(0xFF34C759); // systemGreen - // Shelf life indicators - static const freshGreen = Color(0xFF4CAF50); - static const warningYellow = Color(0xFFFFC107); - static const expiredRed = Color(0xFFE53935); + // Shelf life indicators (same as system colors) + static const freshGreen = Color(0xFF34C759); + static const warningYellow = Color(0xFFFF9500); + static const expiredRed = Color(0xFFFF3B30); } diff --git a/client/lib/core/theme/app_theme.dart b/client/lib/core/theme/app_theme.dart index 2d37eaf..607f99a 100644 --- a/client/lib/core/theme/app_theme.dart +++ b/client/lib/core/theme/app_theme.dart @@ -1,30 +1,255 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'app_colors.dart'; ThemeData appTheme() { + final base = ColorScheme.fromSeed( + seedColor: AppColors.primary, + brightness: Brightness.light, + surface: AppColors.surface, + onSurface: AppColors.textPrimary, + ).copyWith( + primary: AppColors.primary, + onPrimary: Colors.white, + secondary: AppColors.primary, + onSecondary: Colors.white, + tertiary: AppColors.primary, + error: AppColors.error, + surfaceContainerHighest: AppColors.surfaceTertiary, + onSurfaceVariant: AppColors.textSecondary, + outline: AppColors.separator, + ); + return ThemeData( useMaterial3: true, - colorSchemeSeed: AppColors.primary, + colorScheme: base, scaffoldBackgroundColor: AppColors.background, + + // ── AppBar ──────────────────────────────────────── appBarTheme: const AppBarTheme( centerTitle: true, elevation: 0, - backgroundColor: AppColors.surface, + scrolledUnderElevation: 0, + backgroundColor: AppColors.background, foregroundColor: AppColors.textPrimary, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarBrightness: Brightness.light, + statusBarIconBrightness: Brightness.dark, + ), + titleTextStyle: TextStyle( + color: AppColors.textPrimary, + fontSize: 17, + fontWeight: FontWeight.w600, + letterSpacing: -0.4, + ), ), + + // ── Card ────────────────────────────────────────── + // iOS: white cards on grey background, no shadow + cardTheme: CardThemeData( + elevation: 0, + color: AppColors.surface, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + + // ── Bottom navigation ───────────────────────────── bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: AppColors.surface, selectedItemColor: AppColors.primary, unselectedItemColor: AppColors.textSecondary, type: BottomNavigationBarType.fixed, + elevation: 0, + selectedLabelStyle: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + letterSpacing: -0.2, + ), + unselectedLabelStyle: TextStyle(fontSize: 10), ), - inputDecorationTheme: InputDecorationTheme( - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + + // ── Buttons ─────────────────────────────────────── + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + minimumSize: const Size(double.infinity, 50), + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + textStyle: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + letterSpacing: -0.3, + ), + ), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 48), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + minimumSize: const Size(double.infinity, 50), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + minimumSize: const Size(double.infinity, 50), + side: const BorderSide(color: AppColors.primary), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + textStyle: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w400, + letterSpacing: -0.3, + ), + ), + ), + + // ── Inputs ──────────────────────────────────────── + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: AppColors.surface, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.separator), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.separator), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.primary, width: 1.5), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.error), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + hintStyle: const TextStyle(color: AppColors.textSecondary), + ), + + // ── ListTile ────────────────────────────────────── + listTileTheme: const ListTileThemeData( + contentPadding: EdgeInsets.symmetric(horizontal: 16), + minVerticalPadding: 12, + ), + + // ── Divider ─────────────────────────────────────── + dividerTheme: const DividerThemeData( + color: AppColors.separator, + space: 1, + thickness: 0.5, + indent: 16, + ), + + // ── Chip ────────────────────────────────────────── + chipTheme: ChipThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + side: const BorderSide(color: AppColors.separator), + ), + + // ── Dialog ─────────────────────────────────────── + dialogTheme: DialogThemeData( + backgroundColor: AppColors.surface, + surfaceTintColor: Colors.transparent, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + ), + + // ── Bottom sheet ───────────────────────────────── + bottomSheetTheme: const BottomSheetThemeData( + backgroundColor: AppColors.surface, + surfaceTintColor: Colors.transparent, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + ), + + // ── Text ───────────────────────────────────────── + textTheme: const TextTheme( + headlineLarge: TextStyle( + fontSize: 34, + fontWeight: FontWeight.w700, + letterSpacing: -0.5, + color: AppColors.textPrimary, + ), + headlineMedium: TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700, + letterSpacing: -0.4, + color: AppColors.textPrimary, + ), + headlineSmall: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + letterSpacing: -0.3, + color: AppColors.textPrimary, + ), + titleLarge: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + letterSpacing: -0.3, + color: AppColors.textPrimary, + ), + titleMedium: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + letterSpacing: -0.3, + color: AppColors.textPrimary, + ), + titleSmall: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + letterSpacing: -0.2, + color: AppColors.textPrimary, + ), + bodyLarge: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w400, + letterSpacing: -0.3, + color: AppColors.textPrimary, + ), + bodyMedium: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w400, + letterSpacing: -0.2, + color: AppColors.textPrimary, + ), + bodySmall: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w400, + letterSpacing: -0.1, + color: AppColors.textSecondary, + ), + labelLarge: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + letterSpacing: -0.2, + color: AppColors.textPrimary, + ), + labelMedium: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + letterSpacing: -0.1, + color: AppColors.textPrimary, + ), + labelSmall: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w400, + letterSpacing: 0, + color: AppColors.textSecondary, ), ), ); diff --git a/client/lib/features/recipes/recipe_detail_screen.dart b/client/lib/features/recipes/recipe_detail_screen.dart index c619e00..2dc9996 100644 --- a/client/lib/features/recipes/recipe_detail_screen.dart +++ b/client/lib/features/recipes/recipe_detail_screen.dart @@ -194,7 +194,7 @@ class _RecipeDetailScreenState extends ConsumerState { class _PlaceholderImage extends StatelessWidget { @override Widget build(BuildContext context) => Container( - color: AppColors.primaryLight.withValues(alpha: 0.3), + color: AppColors.primary.withValues(alpha: 0.15), child: const Center(child: Icon(Icons.restaurant, size: 64)), ); } @@ -293,7 +293,7 @@ class _NutritionCard extends StatelessWidget { child: Text( '≈', style: TextStyle( - color: AppColors.accent, + color: AppColors.primary, fontSize: 16, fontWeight: FontWeight.bold, ), @@ -360,7 +360,7 @@ class _TagsRow extends StatelessWidget { .map( (t) => Chip( label: Text(t, style: const TextStyle(fontSize: 11)), - backgroundColor: AppColors.primaryLight.withValues(alpha: 0.3), + backgroundColor: AppColors.primary.withValues(alpha: 0.15), padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, ), @@ -490,12 +490,12 @@ class _StepTile extends StatelessWidget { Row( children: [ const Icon(Icons.timer_outlined, - size: 14, color: AppColors.accent), + size: 14, color: AppColors.primary), const SizedBox(width: 4), Text( _formatTimer(step.timerSeconds!), style: const TextStyle( - color: AppColors.accent, fontSize: 12), + color: AppColors.primary, fontSize: 12), ), ], ), diff --git a/client/lib/features/recipes/saved_recipes_screen.dart b/client/lib/features/recipes/saved_recipes_screen.dart index f18a1d8..fa0ffef 100644 --- a/client/lib/features/recipes/saved_recipes_screen.dart +++ b/client/lib/features/recipes/saved_recipes_screen.dart @@ -197,7 +197,7 @@ class _Thumbnail extends StatelessWidget { return Container( width: 80, height: 80, - color: AppColors.primaryLight.withValues(alpha: 0.3), + color: AppColors.primary.withValues(alpha: 0.15), child: const Icon(Icons.restaurant), ); } @@ -211,7 +211,7 @@ class _Thumbnail extends StatelessWidget { errorWidget: (_, __, ___) => Container( width: 80, height: 80, - color: AppColors.primaryLight.withValues(alpha: 0.3), + color: AppColors.primary.withValues(alpha: 0.15), child: const Icon(Icons.restaurant), ), ); diff --git a/client/lib/features/recipes/widgets/recipe_card.dart b/client/lib/features/recipes/widgets/recipe_card.dart index 25d8e72..9cdf317 100644 --- a/client/lib/features/recipes/widgets/recipe_card.dart +++ b/client/lib/features/recipes/widgets/recipe_card.dart @@ -125,7 +125,7 @@ class _RecipeImage extends StatelessWidget { if (imageUrl.isEmpty) { return Container( height: 180, - color: AppColors.primaryLight.withValues(alpha: 0.3), + color: AppColors.primary.withValues(alpha: 0.15), child: const Center(child: Icon(Icons.restaurant, size: 48)), ); } @@ -140,7 +140,7 @@ class _RecipeImage extends StatelessWidget { ), errorWidget: (_, __, ___) => Container( height: 180, - color: AppColors.primaryLight.withValues(alpha: 0.3), + color: AppColors.primary.withValues(alpha: 0.15), child: const Center(child: Icon(Icons.restaurant, size: 48)), ), ); @@ -235,7 +235,7 @@ class _NutritionRow extends StatelessWidget { ); return Row( children: [ - Text('≈ ', style: style?.copyWith(color: AppColors.accent)), + Text('≈ ', style: style?.copyWith(color: AppColors.primary)), _NutItem(label: 'ккал', value: nutrition.calories.round(), style: style), const SizedBox(width: 8), _NutItem(label: 'б', value: nutrition.proteinG.round(), style: style),