From 1fa8412e0a89e00cad018bc366cebf40d3e8d8f7 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 2 Oct 2025 01:58:12 +0530 Subject: [PATCH] feat: implement dynamic theme palette selection - Introduced a new feature allowing users to select from multiple accent color palettes for buttons, cards, and chat bubbles. - Added `AppThemePalette` provider to manage the current theme palette and persist user selections. - Updated the `AppTheme` class to utilize the selected palette for light and dark themes, enhancing visual customization. - Enhanced the `AppCustomizationPage` to include a palette selector, improving user experience and personalization options. - Updated localization files to support new palette selection UI elements in multiple languages. --- lib/core/persistence/persistence_keys.dart | 1 + .../persistence/persistence_migrator.dart | 2 + lib/core/providers/app_providers.dart | 38 ++ .../enhanced_accessibility_service.dart | 6 +- .../services/optimized_storage_service.dart | 9 + lib/features/chat/views/chat_page.dart | 3 +- .../widgets/assistant_message_widget.dart | 21 +- .../profile/views/app_customization_page.dart | 175 ++++++ lib/l10n/app_de.arb | 4 + lib/l10n/app_en.arb | 4 + lib/l10n/app_fr.arb | 4 + lib/l10n/app_it.arb | 4 + lib/l10n/app_localizations.dart | 12 + lib/l10n/app_localizations_de.dart | 7 + lib/l10n/app_localizations_en.dart | 7 + lib/l10n/app_localizations_fr.dart | 7 + lib/l10n/app_localizations_it.dart | 7 + lib/main.dart | 7 +- lib/shared/services/brand_service.dart | 102 +++- lib/shared/theme/app_theme.dart | 487 +++++++---------- lib/shared/theme/color_palettes.dart | 159 ++++++ lib/shared/theme/theme_extensions.dart | 505 +++++++++--------- lib/shared/widgets/loading_states.dart | 17 +- 23 files changed, 1011 insertions(+), 577 deletions(-) create mode 100644 lib/shared/theme/color_palettes.dart diff --git a/lib/core/persistence/persistence_keys.dart b/lib/core/persistence/persistence_keys.dart index 2e4b9af..2a72881 100644 --- a/lib/core/persistence/persistence_keys.dart +++ b/lib/core/persistence/persistence_keys.dart @@ -18,6 +18,7 @@ final class PreferenceKeys { static const String rememberCredentials = 'remember_credentials'; static const String activeServerId = 'active_server_id'; static const String themeMode = 'theme_mode'; + static const String themePalette = 'theme_palette_v1'; static const String localeCode = 'locale_code_v1'; static const String onboardingSeen = 'onboarding_seen_v1'; static const String reviewerMode = 'reviewer_mode_v1'; diff --git a/lib/core/persistence/persistence_migrator.dart b/lib/core/persistence/persistence_migrator.dart index cf0b5b4..dbf93b0 100644 --- a/lib/core/persistence/persistence_migrator.dart +++ b/lib/core/persistence/persistence_migrator.dart @@ -89,6 +89,7 @@ class PersistenceMigrator { copyBool(PreferenceKeys.rememberCredentials); copyString(PreferenceKeys.activeServerId); copyString(PreferenceKeys.themeMode); + copyString(PreferenceKeys.themePalette); copyString(PreferenceKeys.localeCode); copyBool(PreferenceKeys.onboardingSeen); copyBool(PreferenceKeys.reviewerMode); @@ -194,6 +195,7 @@ class PersistenceMigrator { PreferenceKeys.rememberCredentials, PreferenceKeys.activeServerId, PreferenceKeys.themeMode, + PreferenceKeys.themePalette, PreferenceKeys.localeCode, PreferenceKeys.onboardingSeen, PreferenceKeys.reviewerMode, diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index c8059f3..1976cee 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -23,6 +23,8 @@ import '../services/optimized_storage_service.dart'; import '../services/socket_service.dart'; import '../utils/debug_logger.dart'; import '../models/socket_event.dart'; +import '../../shared/theme/color_palettes.dart'; +import '../../shared/theme/app_theme.dart'; part 'app_providers.g.dart'; @@ -78,6 +80,42 @@ class AppThemeMode extends _$AppThemeMode { } } +@Riverpod(keepAlive: true) +class AppThemePalette extends _$AppThemePalette { + late final OptimizedStorageService _storage; + + @override + AppColorPalette build() { + _storage = ref.watch(optimizedStorageServiceProvider); + final storedId = _storage.getThemePaletteId(); + return AppColorPalettes.byId(storedId); + } + + Future setPalette(String paletteId) async { + final palette = AppColorPalettes.byId(paletteId); + state = palette; + await _storage.setThemePaletteId(palette.id); + } +} + +@Riverpod(keepAlive: true) +class AppLightTheme extends _$AppLightTheme { + @override + ThemeData build() { + final palette = ref.watch(appThemePaletteProvider); + return AppTheme.light(palette); + } +} + +@Riverpod(keepAlive: true) +class AppDarkTheme extends _$AppDarkTheme { + @override + ThemeData build() { + final palette = ref.watch(appThemePaletteProvider); + return AppTheme.dark(palette); + } +} + // Locale provider @Riverpod(keepAlive: true) class AppLocale extends _$AppLocale { diff --git a/lib/core/services/enhanced_accessibility_service.dart b/lib/core/services/enhanced_accessibility_service.dart index 01bf7dd..2448198 100644 --- a/lib/core/services/enhanced_accessibility_service.dart +++ b/lib/core/services/enhanced_accessibility_service.dart @@ -2,7 +2,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/semantics.dart'; -import '../../shared/theme/app_theme.dart'; +import '../../shared/theme/color_palettes.dart'; import '../../shared/theme/theme_extensions.dart'; /// Enhanced accessibility service for WCAG 2.2 AA compliance @@ -349,9 +349,7 @@ class EnhancedAccessibilityService { return BoxDecoration( border: hasFocus ? Border.all( - color: - focusColor ?? - AppTheme.brandPrimary, // Brand primary as fallback + color: focusColor ?? AppColorPalettes.auroraViolet.light.primary, width: borderWidth, ) : null, diff --git a/lib/core/services/optimized_storage_service.dart b/lib/core/services/optimized_storage_service.dart index b66fded..777e449 100644 --- a/lib/core/services/optimized_storage_service.dart +++ b/lib/core/services/optimized_storage_service.dart @@ -35,6 +35,7 @@ class OptimizedStorageService { static const String _rememberCredentialsKey = PreferenceKeys.rememberCredentials; static const String _themeModeKey = PreferenceKeys.themeMode; + static const String _themePaletteKey = PreferenceKeys.themePalette; static const String _localeCodeKey = PreferenceKeys.localeCode; static const String _localConversationsKey = HiveStoreKeys.localConversations; static const String _onboardingSeenKey = PreferenceKeys.onboardingSeen; @@ -261,6 +262,14 @@ class OptimizedStorageService { await _preferencesBox.put(_themeModeKey, mode); } + String? getThemePaletteId() { + return _preferencesBox.get(_themePaletteKey) as String?; + } + + Future setThemePaletteId(String paletteId) async { + await _preferencesBox.put(_themePaletteKey, paletteId); + } + String? getLocaleCode() { return _preferencesBox.get(_localeCodeKey) as String?; } diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index efb90ad..fe31ae1 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -946,8 +946,7 @@ class _ChatPageState extends ConsumerState { final greetingName = deriveUserDisplayName(user); return LayoutBuilder( builder: (context, constraints) { - return SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), + return Padding( padding: const EdgeInsets.all(Spacing.lg), child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), diff --git a/lib/features/chat/widgets/assistant_message_widget.dart b/lib/features/chat/widgets/assistant_message_widget.dart index bdf18e8..8f65dc0 100644 --- a/lib/features/chat/widgets/assistant_message_widget.dart +++ b/lib/features/chat/widgets/assistant_message_widget.dart @@ -115,6 +115,15 @@ class _AssistantMessageWidgetState extends ConsumerState _updateTypingIndicatorGate(); } + // Update typing indicator gate when message properties that affect emptiness change + if (oldWidget.message.statusHistory != widget.message.statusHistory || + oldWidget.message.files != widget.message.files || + oldWidget.message.attachmentIds != widget.message.attachmentIds || + oldWidget.message.followUps != widget.message.followUps || + oldWidget.message.codeExecutions != widget.message.codeExecutions) { + _updateTypingIndicatorGate(); + } + // Rebuild cached avatar if model name or icon changes if (oldWidget.modelName != widget.modelName || oldWidget.modelIconUrl != widget.modelIconUrl) { @@ -505,7 +514,17 @@ class _AssistantMessageWidgetState extends ConsumerState } final hasCodeExecutions = widget.message.codeExecutions.isNotEmpty; - return !hasCodeExecutions; + if (hasCodeExecutions) { + return false; + } + + // Check for tool calls in the content using ToolCallsParser + final hasToolCalls = + ToolCallsParser.segments( + content, + )?.any((segment) => segment.isToolCall) ?? + false; + return !hasToolCalls; } void _buildCachedAvatar() { diff --git a/lib/features/profile/views/app_customization_page.dart b/lib/features/profile/views/app_customization_page.dart index 26f7278..a80d17e 100644 --- a/lib/features/profile/views/app_customization_page.dart +++ b/lib/features/profile/views/app_customization_page.dart @@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/services/settings_service.dart'; import '../../../shared/theme/theme_extensions.dart'; +import '../../../shared/theme/color_palettes.dart'; import '../../tools/providers/tools_providers.dart'; import '../../../core/models/tool.dart'; import '../../../shared/widgets/conduit_components.dart'; @@ -36,6 +37,7 @@ class AppCustomizationPage extends ConsumerWidget { final locale = ref.watch(appLocaleProvider); final currentLanguageCode = locale?.languageCode ?? 'system'; final languageLabel = _resolveLanguageLabel(context, currentLanguageCode); + final activePalette = ref.watch(appThemePaletteProvider); return Scaffold( backgroundColor: context.conduitTheme.surfaceBackground, @@ -58,6 +60,7 @@ class AppCustomizationPage extends ConsumerWidget { currentLanguageCode, languageLabel, settings, + activePalette, ), const SizedBox(height: Spacing.sectionGap), _buildQuickPillsSection(context, ref, settings), @@ -110,6 +113,7 @@ class AppCustomizationPage extends ConsumerWidget { String currentLanguageCode, String languageLabel, AppSettings settings, + AppColorPalette palette, ) { final theme = context.conduitTheme; @@ -125,6 +129,8 @@ class AppCustomizationPage extends ConsumerWidget { const SizedBox(height: Spacing.sm), _buildThemeSelector(context, ref, themeMode, themeDescription), const SizedBox(height: Spacing.md), + _buildPaletteSelector(context, ref, palette), + const SizedBox(height: Spacing.md), _CustomizationTile( leading: _buildIconBadge( context, @@ -277,6 +283,53 @@ class AppCustomizationPage extends ConsumerWidget { ); } + Widget _buildPaletteSelector( + BuildContext context, + WidgetRef ref, + AppColorPalette activePalette, + ) { + final theme = context.conduitTheme; + final palettes = AppColorPalettes.all; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.themePalette, + style: + theme.bodyLarge?.copyWith( + color: theme.textPrimary, + fontWeight: FontWeight.w600, + ) ?? + TextStyle(color: theme.textPrimary, fontWeight: FontWeight.w600), + ), + const SizedBox(height: Spacing.xs), + Text( + AppLocalizations.of(context)!.themePaletteDescription, + style: + theme.bodySmall?.copyWith(color: theme.textSecondary) ?? + TextStyle(color: theme.textSecondary), + ), + const SizedBox(height: Spacing.sm), + ConduitCard( + padding: const EdgeInsets.all(Spacing.cardPadding), + child: Column( + children: [ + for (final palette in palettes) + _PaletteOption( + palette: palette, + activeId: activePalette.id, + onSelect: () => ref + .read(appThemePaletteProvider.notifier) + .setPalette(palette.id), + ), + ], + ), + ), + ], + ); + } + Widget _buildThemeChip( BuildContext context, WidgetRef ref, { @@ -551,6 +604,128 @@ class AppCustomizationPage extends ConsumerWidget { } } +class _PaletteOption extends StatelessWidget { + const _PaletteOption({ + required this.palette, + required this.activeId, + required this.onSelect, + }); + + final AppColorPalette palette; + final String activeId; + final VoidCallback onSelect; + + @override + Widget build(BuildContext context) { + final theme = context.conduitTheme; + final isSelected = palette.id == activeId; + final previewColors = + palette.preview ?? + [ + palette.light.primary, + palette.light.secondary, + palette.dark.primary, + ]; + + return InkWell( + onTap: onSelect, + borderRadius: BorderRadius.circular(AppBorderRadius.md), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: Spacing.sm), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + isSelected ? Icons.radio_button_checked : Icons.radio_button_off, + color: isSelected ? theme.buttonPrimary : theme.iconSecondary, + size: IconSize.md, + ), + const SizedBox(width: Spacing.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + palette.label, + style: + theme.bodyLarge?.copyWith( + color: theme.textPrimary, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.w500, + ) ?? + TextStyle( + color: theme.textPrimary, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + if (isSelected) + Padding( + padding: const EdgeInsets.only(left: Spacing.xs), + child: Icon( + Icons.check_circle, + color: theme.buttonPrimary, + size: IconSize.sm, + ), + ), + ], + ), + const SizedBox(height: Spacing.xxs), + Text( + palette.description, + style: + theme.bodySmall?.copyWith(color: theme.textSecondary) ?? + TextStyle(color: theme.textSecondary), + ), + const SizedBox(height: Spacing.xs), + Row( + children: [ + for (final color in previewColors) + _PaletteColorDot(color: color), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _PaletteColorDot extends StatelessWidget { + const _PaletteColorDot({required this.color}); + + final Color color; + + @override + Widget build(BuildContext context) { + final theme = context.conduitTheme; + return Container( + margin: const EdgeInsets.only(right: Spacing.xs), + width: 20, + height: 20, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all( + color: theme.dividerColor.withValues(alpha: 0.4), + width: 1.2, + ), + ), + ); + } +} + class _CustomizationTile extends StatelessWidget { const _CustomizationTile({ required this.leading, diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index d005786..adf0c17 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -260,6 +260,10 @@ "followingSystem": "Dem System folgen: {theme}", "@followingSystem": {"placeholders": {"theme": {"type": "String"}}}, "themeDark": "Dunkel", + "themePalette": "Farbpalette", + "@themePalette": {"description": "Titel für die Auswahl der App-Farbpalette."}, + "themePaletteDescription": "Wählen Sie die Akzentfarben für Schaltflächen, Karten und Chatblasen.", + "@themePaletteDescription": {"description": "Hilfetext zur Erklärung der Palettenauswahl."}, "themeLight": "Hell", "currentlyUsingDarkTheme": "Aktuell dunkles Thema", "currentlyUsingLightTheme": "Aktuell helles Thema", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 53ee8e4..dd116bf 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -519,6 +519,10 @@ }, "themeDark": "Dark", "@themeDark": {"description": "Theme label for dark appearance."}, + "themePalette": "Accent palette", + "@themePalette": {"description": "Title for selecting the app color palette."}, + "themePaletteDescription": "Choose the accent colors used for buttons, cards, and chat bubbles.", + "@themePaletteDescription": {"description": "Helper text explaining palette selection."}, "themeLight": "Light", "@themeLight": {"description": "Theme label for light appearance."}, "currentlyUsingDarkTheme": "Currently using Dark theme", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 760c18f..0d8b417 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -260,6 +260,10 @@ "followingSystem": "Selon le système : {theme}", "@followingSystem": {"placeholders": {"theme": {"type": "String"}}}, "themeDark": "Sombre", + "themePalette": "Palette de couleurs", + "@themePalette": {"description": "Titre pour choisir la palette de couleurs de l'application."}, + "themePaletteDescription": "Choisissez les couleurs d'accent utilisées pour les boutons, les cartes et les bulles de discussion.", + "@themePaletteDescription": {"description": "Texte d'aide expliquant la sélection de la palette."}, "themeLight": "Clair", "currentlyUsingDarkTheme": "Thème sombre actuellement utilisé", "currentlyUsingLightTheme": "Thème clair actuellement utilisé", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b7c4167..2b4876d 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -260,6 +260,10 @@ "followingSystem": "Segue il sistema: {theme}", "@followingSystem": {"placeholders": {"theme": {"type": "String"}}}, "themeDark": "Scuro", + "themePalette": "Palette di colori", + "@themePalette": {"description": "Titolo per scegliere la palette di colori dell'app."}, + "themePaletteDescription": "Scegli i colori di accento usati per pulsanti, schede e bolle di chat.", + "@themePaletteDescription": {"description": "Testo di supporto che spiega la scelta della palette."}, "themeLight": "Chiaro", "currentlyUsingDarkTheme": "Attualmente tema scuro", "currentlyUsingLightTheme": "Attualmente tema chiaro", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 1d682a6..d8acd28 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1458,6 +1458,18 @@ abstract class AppLocalizations { /// **'Dark'** String get themeDark; + /// Title for selecting the app color palette. + /// + /// In en, this message translates to: + /// **'Accent palette'** + String get themePalette; + + /// Helper text explaining palette selection. + /// + /// In en, this message translates to: + /// **'Choose the accent colors used for buttons, cards, and chat bubbles.'** + String get themePaletteDescription; + /// Theme label for light appearance. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index a33df3e..776c2bd 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -755,6 +755,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get themeDark => 'Dunkel'; + @override + String get themePalette => 'Farbpalette'; + + @override + String get themePaletteDescription => + 'Wählen Sie die Akzentfarben für Schaltflächen, Karten und Chatblasen.'; + @override String get themeLight => 'Hell'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0581993..644ec66 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -751,6 +751,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get themeDark => 'Dark'; + @override + String get themePalette => 'Accent palette'; + + @override + String get themePaletteDescription => + 'Choose the accent colors used for buttons, cards, and chat bubbles.'; + @override String get themeLight => 'Light'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index f4429fc..172552b 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -763,6 +763,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get themeDark => 'Sombre'; + @override + String get themePalette => 'Palette de couleurs'; + + @override + String get themePaletteDescription => + 'Choisissez les couleurs d\'accent utilisées pour les boutons, les cartes et les bulles de discussion.'; + @override String get themeLight => 'Clair'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 6f86b07..5dfc1f2 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -752,6 +752,13 @@ class AppLocalizationsIt extends AppLocalizations { @override String get themeDark => 'Scuro'; + @override + String get themePalette => 'Palette di colori'; + + @override + String get themePaletteDescription => + 'Scegli i colori di accento usati per pulsanti, schede e bolle di chat.'; + @override String get themeLight => 'Chiaro'; diff --git a/lib/main.dart b/lib/main.dart index 6d4b79d..a5c43c1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,7 +11,6 @@ import 'core/persistence/hive_bootstrap.dart'; import 'core/persistence/persistence_migrator.dart'; import 'core/persistence/persistence_providers.dart'; import 'core/router/app_router.dart'; -import 'shared/theme/app_theme.dart'; import 'shared/widgets/offline_indicator.dart'; import 'features/auth/providers/unified_auth_providers.dart'; import 'core/auth/auth_state_manager.dart'; @@ -151,13 +150,15 @@ class _ConduitAppState extends ConsumerState { final themeMode = ref.watch(appThemeModeProvider.select((mode) => mode)); final router = ref.watch(goRouterProvider); final locale = ref.watch(appLocaleProvider); + final lightTheme = ref.watch(appLightThemeProvider); + final darkTheme = ref.watch(appDarkThemeProvider); return ErrorBoundary( child: MaterialApp.router( routerConfig: router, onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, - theme: AppTheme.conduitLightTheme, - darkTheme: AppTheme.conduitDarkTheme, + theme: lightTheme, + darkTheme: darkTheme, themeMode: themeMode, debugShowCheckedModeBanner: false, locale: locale, diff --git a/lib/shared/services/brand_service.dart b/lib/shared/services/brand_service.dart index e440161..841d09b 100644 --- a/lib/shared/services/brand_service.dart +++ b/lib/shared/services/brand_service.dart @@ -3,6 +3,7 @@ import '../theme/theme_extensions.dart'; import 'package:flutter/cupertino.dart'; import 'dart:io' show Platform; import '../theme/app_theme.dart'; +import '../theme/color_palettes.dart'; /// Centralized service for consistent brand identity throughout the app /// Uses the hub icon as the primary brand element @@ -20,9 +21,32 @@ class BrandService { Platform.isIOS ? CupertinoIcons.globe : Icons.public; /// Brand colors - these should be accessed through context.conduitTheme in UI components - static Color get primaryBrandColor => AppTheme.brandPrimary; - static Color get secondaryBrandColor => AppTheme.brandPrimaryLight; - static Color get accentBrandColor => AppTheme.brandPrimaryDark; + static Color primaryBrandColor({ + BuildContext? context, + Brightness? brightness, + }) { + final palette = _resolvePalette(context); + final resolvedBrightness = brightness ?? _resolveBrightness(context); + return palette.primaryFor(resolvedBrightness); + } + + static Color secondaryBrandColor({ + BuildContext? context, + Brightness? brightness, + }) { + final palette = _resolvePalette(context); + final resolvedBrightness = brightness ?? _resolveBrightness(context); + return palette.secondaryFor(resolvedBrightness); + } + + static Color accentBrandColor({ + BuildContext? context, + Brightness? brightness, + }) { + final palette = _resolvePalette(context); + final resolvedBrightness = brightness ?? _resolveBrightness(context); + return palette.accentFor(resolvedBrightness); + } /// Creates a branded icon with consistent styling static Widget createBrandIcon({ @@ -31,21 +55,25 @@ class BrandService { IconData? icon, bool useGradient = false, bool addShadow = false, + BuildContext? context, }) { final iconData = icon ?? primaryIcon; - final iconColor = color ?? primaryBrandColor; + final resolvedColor = color ?? primaryBrandColor(context: context); Widget iconWidget = Icon( iconData, size: size, - color: useGradient ? null : iconColor, + color: useGradient ? null : resolvedColor, ); if (useGradient) { iconWidget = ShaderMask( blendMode: BlendMode.srcIn, shaderCallback: (bounds) => LinearGradient( - colors: [primaryBrandColor, secondaryBrandColor], + colors: [ + primaryBrandColor(context: context), + secondaryBrandColor(context: context), + ], ).createShader(bounds), child: Icon(iconData, size: size), ); @@ -56,7 +84,7 @@ class BrandService { decoration: BoxDecoration( boxShadow: [ BoxShadow( - color: primaryBrandColor.withValues(alpha: 0.3), + color: primaryBrandColor(context: context).withValues(alpha: 0.3), blurRadius: size * 0.3, offset: Offset(0, size * 0.1), ), @@ -78,7 +106,7 @@ class BrandService { String? fallbackText, BuildContext? context, }) { - final bgColor = backgroundColor ?? primaryBrandColor; + final bgColor = backgroundColor ?? primaryBrandColor(context: context); final iColor = iconColor ?? (context?.conduitTheme.textInverse ?? AppTheme.neutral50); @@ -90,14 +118,17 @@ class BrandService { ? LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [primaryBrandColor, secondaryBrandColor], + colors: [ + primaryBrandColor(context: context), + secondaryBrandColor(context: context), + ], ) : null, color: useGradient ? null : bgColor, borderRadius: BorderRadius.circular(size / 2), boxShadow: [ BoxShadow( - color: primaryBrandColor.withValues(alpha: 0.3), + color: primaryBrandColor(context: context).withValues(alpha: 0.3), blurRadius: size * 0.2, offset: Offset(0, size * 0.1), ), @@ -123,13 +154,16 @@ class BrandService { double size = 24, double strokeWidth = 2, Color? color, + BuildContext? context, }) { return SizedBox( width: size, height: size, child: CircularProgressIndicator( strokeWidth: strokeWidth, - valueColor: AlwaysStoppedAnimation(color ?? primaryBrandColor), + valueColor: AlwaysStoppedAnimation( + color ?? primaryBrandColor(context: context), + ), ), ); } @@ -149,6 +183,7 @@ class BrandService { size: size, color: iconColor, icon: primaryIconOutlined, + context: context, ); } @@ -167,6 +202,7 @@ class BrandService { size: size * 0.5, color: iconColor, icon: primaryIconOutlined, + context: context, ), ); } @@ -181,27 +217,27 @@ class BrandService { bool isSecondary = false, BuildContext? context, }) { + final theme = context?.conduitTheme; return SizedBox( width: width, height: 48, child: ElevatedButton.icon( onPressed: isLoading ? null : onPressed, icon: isLoading - ? createBrandLoadingIndicator(size: IconSize.sm) + ? createBrandLoadingIndicator(size: IconSize.sm, context: context) : createBrandIcon( size: IconSize.md, icon: icon ?? primaryIcon, - color: context?.conduitTheme.textInverse ?? AppTheme.neutral50, + color: theme?.textInverse ?? AppTheme.neutral50, + context: context, ), label: Text(text), style: ElevatedButton.styleFrom( backgroundColor: isSecondary - ? (context?.conduitTheme.buttonSecondary ?? AppTheme.neutral700) - : (context?.conduitTheme.buttonPrimary ?? primaryBrandColor), - foregroundColor: - context?.conduitTheme.buttonPrimaryText ?? AppTheme.neutral50, - disabledBackgroundColor: - context?.conduitTheme.buttonDisabled ?? AppTheme.neutral500, + ? (theme?.buttonSecondary ?? AppTheme.neutral700) + : (theme?.buttonPrimary ?? primaryBrandColor(context: context)), + foregroundColor: theme?.buttonPrimaryText ?? AppTheme.neutral50, + disabledBackgroundColor: theme?.buttonDisabled ?? AppTheme.neutral500, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), ), @@ -255,6 +291,14 @@ class BrandService { bool animate = true, BuildContext? context, }) { + final theme = context?.conduitTheme; + final baseColor = + theme?.buttonPrimary ?? + primaryBrandColor(context: context, brightness: Brightness.dark); + final accentColor = + theme?.buttonPrimary.withValues(alpha: 0.8) ?? + secondaryBrandColor(context: context, brightness: Brightness.dark); + return Container( width: size, height: size, @@ -262,11 +306,7 @@ class BrandService { gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ - context?.conduitTheme.buttonPrimary ?? primaryBrandColor, - context?.conduitTheme.buttonPrimary.withValues(alpha: 0.8) ?? - secondaryBrandColor, - ], + colors: [baseColor, accentColor], ), borderRadius: BorderRadius.circular(size / 2), boxShadow: ConduitShadows.glow, @@ -274,8 +314,20 @@ class BrandService { child: Icon( primaryIcon, size: size * 0.5, - color: context?.conduitTheme.textInverse ?? AppTheme.neutral50, + color: theme?.textInverse ?? AppTheme.neutral50, ), ); } + + static AppColorPalette _resolvePalette(BuildContext? context) { + if (context == null) { + return AppColorPalettes.auroraViolet; + } + final extension = Theme.of(context).extension(); + return extension?.palette ?? AppColorPalettes.auroraViolet; + } + + static Brightness _resolveBrightness(BuildContext? context) { + return context != null ? Theme.of(context).brightness : Brightness.light; + } } diff --git a/lib/shared/theme/app_theme.dart b/lib/shared/theme/app_theme.dart index 6ea2e7d..1803ec7 100644 --- a/lib/shared/theme/app_theme.dart +++ b/lib/shared/theme/app_theme.dart @@ -2,13 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'theme_extensions.dart'; +import 'color_palettes.dart'; class AppTheme { - // Brand accents tuned for WCAG contrast in light/dark modes - static const Color brandPrimary = Color(0xFFA420FF); // Light mode primary - static const Color brandPrimaryLight = Color(0xFFC773FF); // Light accents - static const Color brandPrimaryDark = Color(0xFF9500FF); // Dark mode primary - // Enhanced neutral palette for better contrast (WCAG AA compliant) static const Color neutral900 = Color(0xFF000000); // Pure black static const Color neutral800 = Color( @@ -35,298 +31,219 @@ class AppTheme { static const Color info = Color(0xFF0284C7); // Better blue contrast static const Color infoDark = Color(0xFF0369A1); // Dark theme blue - // Brand aliases - static const Color primaryColor = brandPrimary; - static const Color secondaryColor = brandPrimaryLight; - static const Color surfaceColor = neutral50; - static const Color errorColor = error; - static const Color successColor = success; + static ThemeData light(AppColorPalette palette) { + final lightTone = palette.light; - // Base Light Theme - static ThemeData lightTheme = ThemeData( - useMaterial3: true, - brightness: Brightness.light, - colorScheme: const ColorScheme.light( - primary: brandPrimary, - secondary: brandPrimaryLight, - surface: surfaceColor, - error: errorColor, - ), - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder(), - TargetPlatform.iOS: ZoomPageTransitionsBuilder(), - TargetPlatform.linux: ZoomPageTransitionsBuilder(), - TargetPlatform.macOS: ZoomPageTransitionsBuilder(), - TargetPlatform.windows: ZoomPageTransitionsBuilder(), - }, - ), - splashFactory: NoSplash.splashFactory, - appBarTheme: const AppBarTheme( - centerTitle: true, - elevation: Elevation.none, - backgroundColor: Colors.transparent, - foregroundColor: neutral900, - ), - bottomSheetTheme: BottomSheetThemeData( - backgroundColor: neutral50, - modalBackgroundColor: neutral50, - surfaceTintColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.modal), + return ThemeData( + useMaterial3: true, + brightness: Brightness.light, + colorScheme: ColorScheme.light( + primary: lightTone.primary, + secondary: lightTone.secondary, + surface: neutral50, + error: error, + ).copyWith(surfaceContainerHighest: const Color(0xFFF0F1F1)), + pageTransitionsTheme: _pageTransitionsTheme, + splashFactory: NoSplash.splashFactory, + appBarTheme: const AppBarTheme( + centerTitle: true, + elevation: Elevation.none, + backgroundColor: Colors.transparent, + foregroundColor: neutral800, ), - showDragHandle: false, - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.lg, - vertical: Spacing.xs, - ), + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: neutral50, + modalBackgroundColor: neutral50, + surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderRadius: BorderRadius.circular(AppBorderRadius.modal), + ), + showDragHandle: false, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.lg, + vertical: Spacing.xs, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + ), ), ), - ), - cardTheme: CardThemeData( - elevation: Elevation.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.lg), - side: BorderSide(color: neutral200), - ), - ), - snackBarTheme: SnackBarThemeData( - behavior: SnackBarBehavior.floating, - backgroundColor: neutral900.withValues(alpha: 0.92), - contentTextStyle: const TextStyle( - color: neutral50, - ).copyWith(fontSize: AppTypography.bodyMedium), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.snackbar), - ), - elevation: Elevation.high, - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: neutral50, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: primaryColor, width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: errorColor, width: 1), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - ), - // Use platform default system font text theme - textTheme: ThemeData.light().textTheme, - extensions: const [ConduitThemeExtension.light], - ); - - // Base Dark Theme - static ThemeData darkTheme = ThemeData( - useMaterial3: true, - brightness: Brightness.dark, - scaffoldBackgroundColor: Color(0xFF0A0D0C), - colorScheme: const ColorScheme.dark( - primary: brandPrimaryDark, - secondary: brandPrimary, - surface: Color(0xFF0A0D0C), - surfaceContainerHighest: neutral700, - onSurface: neutral50, - onSurfaceVariant: neutral300, - outline: neutral600, - error: error, - ), - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder(), - TargetPlatform.iOS: ZoomPageTransitionsBuilder(), - TargetPlatform.linux: ZoomPageTransitionsBuilder(), - TargetPlatform.macOS: ZoomPageTransitionsBuilder(), - TargetPlatform.windows: ZoomPageTransitionsBuilder(), - }, - ), - splashFactory: NoSplash.splashFactory, - appBarTheme: const AppBarTheme( - centerTitle: true, - elevation: Elevation.none, - backgroundColor: Colors.transparent, - foregroundColor: neutral50, - ), - bottomSheetTheme: BottomSheetThemeData( - backgroundColor: neutral900, - modalBackgroundColor: neutral900, - surfaceTintColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.modal), - ), - showDragHandle: false, - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.lg, - vertical: Spacing.xs, - ), + cardTheme: CardThemeData( + elevation: Elevation.none, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderRadius: BorderRadius.circular(AppBorderRadius.lg), + side: BorderSide(color: neutral200), ), ), - ), - cardTheme: CardThemeData( - elevation: Elevation.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.lg), - side: BorderSide(color: neutral800), + snackBarTheme: SnackBarThemeData( + behavior: SnackBarBehavior.floating, + backgroundColor: neutral900.withValues(alpha: 0.92), + contentTextStyle: const TextStyle( + color: neutral50, + ).copyWith(fontSize: AppTypography.bodyMedium), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.snackbar), + ), + elevation: Elevation.high, ), - ), - snackBarTheme: SnackBarThemeData( - behavior: SnackBarBehavior.floating, - backgroundColor: neutral800.withValues(alpha: 0.92), - contentTextStyle: const TextStyle( - color: neutral50, - ).copyWith(fontSize: AppTypography.bodyMedium), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.snackbar), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: neutral50, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: BorderSide(color: lightTone.primary, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: const BorderSide(color: error, width: 1), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), ), - elevation: Elevation.high, - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: neutral700, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: neutral600, width: 1), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: neutral600, width: 1), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: brandPrimaryDark, width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: error, width: 1), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - ), - // Use platform default system font text theme - textTheme: ThemeData.dark().textTheme, - extensions: const [ConduitThemeExtension.dark], - ); - - // Conduit variants using brand colors - static ThemeData conduitLightTheme = lightTheme.copyWith( - colorScheme: lightTheme.colorScheme.copyWith( - primary: brandPrimary, - secondary: brandPrimaryLight, - surface: neutral50, - ), - extensions: const [ConduitThemeExtension.light], - appBarTheme: const AppBarTheme( - centerTitle: true, - elevation: Elevation.none, - backgroundColor: Colors.transparent, - foregroundColor: neutral800, - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: neutral50, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: brandPrimary, width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: error, width: 1), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - ), - ); - - static ThemeData conduitDarkTheme = darkTheme.copyWith( - scaffoldBackgroundColor: const Color(0xFF0A0D0C), - colorScheme: darkTheme.colorScheme.copyWith( - primary: brandPrimaryDark, - secondary: brandPrimary, - surface: const Color(0xFF0A0D0C), - surfaceContainerHighest: neutral700, - ), - extensions: const [ConduitThemeExtension.dark], - appBarTheme: const AppBarTheme( - centerTitle: true, - elevation: Elevation.none, - backgroundColor: Colors.transparent, - foregroundColor: neutral50, - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: neutral700, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: neutral600, width: 1), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: neutral600, width: 1), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: brandPrimaryDark, width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: error, width: 1), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - ), - ); - - // Classic Conduit variants for runtime switching - // Removed classic Conduit variants from public API to keep Aurora only - - // Platform-specific theming helpers - static CupertinoThemeData cupertinoTheme(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return CupertinoThemeData( - brightness: isDark ? Brightness.dark : Brightness.light, - primaryColor: isDark ? brandPrimaryDark : brandPrimary, - scaffoldBackgroundColor: isDark ? neutral900 : neutral50, - barBackgroundColor: isDark ? neutral900 : neutral50, + textTheme: ThemeData.light().textTheme, + extensions: >[ + ConduitThemeExtension.lightPalette(palette), + AppPaletteThemeExtension(palette: palette), + ], ); } + + static ThemeData dark(AppColorPalette palette) { + final darkTone = palette.dark; + + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + scaffoldBackgroundColor: const Color(0xFF0A0D0C), + colorScheme: ColorScheme.dark( + primary: darkTone.primary, + secondary: darkTone.secondary, + surface: const Color(0xFF0A0D0C), + surfaceContainerHighest: neutral700, + onSurface: neutral50, + onSurfaceVariant: neutral300, + outline: neutral600, + error: error, + ), + pageTransitionsTheme: _pageTransitionsTheme, + splashFactory: NoSplash.splashFactory, + appBarTheme: const AppBarTheme( + centerTitle: true, + elevation: Elevation.none, + backgroundColor: Colors.transparent, + foregroundColor: neutral50, + ), + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: neutral900, + modalBackgroundColor: neutral900, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.modal), + ), + showDragHandle: false, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.lg, + vertical: Spacing.xs, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + ), + ), + ), + cardTheme: CardThemeData( + elevation: Elevation.none, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.lg), + side: BorderSide(color: neutral800), + ), + ), + snackBarTheme: SnackBarThemeData( + behavior: SnackBarBehavior.floating, + backgroundColor: neutral800.withValues(alpha: 0.92), + contentTextStyle: const TextStyle( + color: neutral50, + ).copyWith(fontSize: AppTypography.bodyMedium), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.snackbar), + ), + elevation: Elevation.high, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: neutral700, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: const BorderSide(color: neutral600, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: const BorderSide(color: neutral600, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: BorderSide(color: darkTone.primary, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: const BorderSide(color: error, width: 1), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), + ), + textTheme: ThemeData.dark().textTheme, + extensions: >[ + ConduitThemeExtension.darkPalette(palette), + AppPaletteThemeExtension(palette: palette), + ], + ); + } + + static CupertinoThemeData cupertinoTheme( + BuildContext context, + AppColorPalette palette, + ) { + final brightness = Theme.of(context).brightness; + final tone = palette.toneFor(brightness); + return CupertinoThemeData( + brightness: brightness, + primaryColor: tone.primary, + scaffoldBackgroundColor: brightness == Brightness.dark + ? neutral900 + : neutral50, + barBackgroundColor: brightness == Brightness.dark + ? neutral900 + : neutral50, + ); + } + + static const PageTransitionsTheme _pageTransitionsTheme = + PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + TargetPlatform.iOS: ZoomPageTransitionsBuilder(), + TargetPlatform.linux: ZoomPageTransitionsBuilder(), + TargetPlatform.macOS: ZoomPageTransitionsBuilder(), + TargetPlatform.windows: ZoomPageTransitionsBuilder(), + }, + ); } /// Animated theme wrapper for smooth theme transitions diff --git a/lib/shared/theme/color_palettes.dart b/lib/shared/theme/color_palettes.dart new file mode 100644 index 0000000..934b660 --- /dev/null +++ b/lib/shared/theme/color_palettes.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; + +@immutable +class AppPaletteTone { + const AppPaletteTone({ + required this.primary, + required this.secondary, + required this.accent, + }); + + final Color primary; + final Color secondary; + final Color accent; +} + +@immutable +class AppColorPalette { + const AppColorPalette({ + required this.id, + required this.label, + required this.description, + required this.light, + required this.dark, + this.preview, + }); + + final String id; + final String label; + final String description; + final AppPaletteTone light; + final AppPaletteTone dark; + final List? preview; +} + +@immutable +class AppPaletteThemeExtension + extends ThemeExtension { + const AppPaletteThemeExtension({required this.palette}); + + final AppColorPalette palette; + + @override + AppPaletteThemeExtension copyWith({AppColorPalette? palette}) { + return AppPaletteThemeExtension(palette: palette ?? this.palette); + } + + @override + AppPaletteThemeExtension lerp( + covariant ThemeExtension? other, + double t, + ) { + if (other is! AppPaletteThemeExtension) return this; + return t < 0.5 ? this : other; + } +} + +class AppColorPalettes { + static const String defaultPaletteId = 'aurora_violet'; + + static const AppColorPalette auroraViolet = AppColorPalette( + id: defaultPaletteId, + label: 'Aurora Violet', + description: 'Bold purples inspired by aurora skies.', + light: AppPaletteTone( + primary: Color(0xFFA420FF), + secondary: Color(0xFFB058FF), + accent: Color(0xFFD9A5FF), + ), + dark: AppPaletteTone( + primary: Color(0xFF9500FF), + secondary: Color(0xFFC773FF), + accent: Color(0xFFE3BDFF), + ), + preview: [Color(0xFF9500FF), Color(0xFFA420FF), Color(0xFFB058FF)], + ); + + static const AppColorPalette emeraldRush = AppColorPalette( + id: 'emerald_rush', + label: 'Emerald Rush', + description: 'High-contrast greens with calm highlights.', + light: AppPaletteTone( + primary: Color(0xFF0C7F48), + secondary: Color(0xFF26A164), + accent: Color(0xFF6DE0A4), + ), + dark: AppPaletteTone( + primary: Color(0xFF40DD7F), + secondary: Color(0xFF26A164), + accent: Color(0xFF6DE0A4), + ), + preview: [Color(0xFF0C7F48), Color(0xFF26A164), Color(0xFF40DD7F)], + ); + + static const AppColorPalette azurePulse = AppColorPalette( + id: 'azure_pulse', + label: 'Azure Pulse', + description: 'Electric blues with crisp highlights.', + light: AppPaletteTone( + primary: Color(0xFF1B64DA), + secondary: Color(0xFF2E7AF0), + accent: Color(0xFF6DA6FF), + ), + dark: AppPaletteTone( + primary: Color(0xFF37C7FF), + secondary: Color(0xFF2E7AF0), + accent: Color(0xFF6DA6FF), + ), + preview: [Color(0xFF1B64DA), Color(0xFF2E7AF0), Color(0xFF37C7FF)], + ); + + static const AppColorPalette sunsetGlow = AppColorPalette( + id: 'sunset_glow', + label: 'Sunset Glow', + description: 'Warm oranges for energetic interfaces.', + light: AppPaletteTone( + primary: Color(0xFFB83200), + secondary: Color(0xFFE65100), + accent: Color(0xFFFFA05B), + ), + dark: AppPaletteTone( + primary: Color(0xFFFF8A00), + secondary: Color(0xFFE65100), + accent: Color(0xFFFFA05B), + ), + preview: [Color(0xFFB83200), Color(0xFFE65100), Color(0xFFFF8A00)], + ); + + static const List all = [ + auroraViolet, + emeraldRush, + azurePulse, + sunsetGlow, + ]; + + static AppColorPalette byId(String? id) { + return all.firstWhere( + (palette) => palette.id == id, + orElse: () => auroraViolet, + ); + } +} + +extension AppColorPaletteX on AppColorPalette { + AppPaletteTone toneFor(Brightness brightness) { + return brightness == Brightness.dark ? dark : light; + } + + Color primaryFor(Brightness brightness) { + return toneFor(brightness).primary; + } + + Color secondaryFor(Brightness brightness) { + return toneFor(brightness).secondary; + } + + Color accentFor(Brightness brightness) { + return toneFor(brightness).accent; + } +} diff --git a/lib/shared/theme/theme_extensions.dart b/lib/shared/theme/theme_extensions.dart index d053f98..0562f75 100644 --- a/lib/shared/theme/theme_extensions.dart +++ b/lib/shared/theme/theme_extensions.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; // Using system fonts; no GoogleFonts dependency required import 'app_theme.dart'; +import 'color_palettes.dart'; /// Extended theme data for consistent styling across the app @immutable @@ -501,263 +502,263 @@ class ConduitThemeExtension extends ThemeExtension { ); } - /// Dark theme extension - static const ConduitThemeExtension dark = ConduitThemeExtension( - // Chat-specific colors - Enhanced for production-grade look - chatBubbleUser: AppTheme.brandPrimaryDark, - chatBubbleAssistant: Color(0xFF0E1010), - chatBubbleUserText: AppTheme.neutral50, - chatBubbleAssistantText: AppTheme.neutral50, - chatBubbleUserBorder: AppTheme.brandPrimaryDark, - chatBubbleAssistantBorder: Color(0xFF1A1D1C), - // Input and form colors - inputBackground: Color(0xFF141615), - inputBorder: AppTheme.neutral600, - inputBorderFocused: AppTheme.brandPrimaryDark, - inputText: AppTheme.neutral50, - inputPlaceholder: AppTheme.neutral300, - inputError: AppTheme.error, + /// Dark theme extension derived from the active color palette. + static ConduitThemeExtension darkPalette(AppColorPalette palette) { + final darkTone = palette.dark; + return ConduitThemeExtension( + chatBubbleUser: darkTone.primary, + chatBubbleAssistant: const Color(0xFF0E1010), + chatBubbleUserText: AppTheme.neutral50, + chatBubbleAssistantText: AppTheme.neutral50, + chatBubbleUserBorder: darkTone.secondary, + chatBubbleAssistantBorder: const Color(0xFF1A1D1C), + inputBackground: const Color(0xFF141615), + inputBorder: AppTheme.neutral600, + inputBorderFocused: darkTone.primary, + inputText: AppTheme.neutral50, + inputPlaceholder: AppTheme.neutral300, + inputError: AppTheme.error, + cardBackground: const Color(0xFF0C0F0E), + cardBorder: const Color(0xFF151918), + cardShadow: AppTheme.neutral900, + surfaceBackground: const Color(0xFF0A0D0C), + surfaceContainer: const Color(0xFF0C0F0E), + surfaceContainerHighest: const Color(0xFF121514), + buttonPrimary: darkTone.primary, + buttonPrimaryText: AppTheme.neutral50, + buttonSecondary: const Color(0xFF151918), + buttonSecondaryText: AppTheme.neutral50, + buttonDisabled: AppTheme.neutral600, + buttonDisabledText: AppTheme.neutral400, + success: const Color(0xFF34D399), + successBackground: const Color(0xFF14532D), + error: const Color(0xFFFCA5A5), + errorBackground: const Color(0xFF7F1D1D), + warning: const Color(0xFFFBBF24), + warningBackground: const Color(0xFF451A03), + info: const Color(0xFF93C5FD), + infoBackground: const Color(0xFF0C4A6E), + dividerColor: AppTheme.neutral600, + navigationBackground: const Color(0xFF0A0D0C), + navigationSelected: darkTone.primary, + navigationUnselected: AppTheme.neutral300, + navigationSelectedBackground: _surfaceTint( + darkTone.primary, + const Color(0xFF0A0D0C), + 0.24, + ), + shimmerBase: const Color(0xFF121514), + shimmerHighlight: const Color(0xFF1A1D1C), + loadingIndicator: darkTone.primary, + textPrimary: AppTheme.neutral50, + textSecondary: const Color(0xFFBAC2C0), + textTertiary: AppTheme.neutral400, + textInverse: AppTheme.neutral900, + textDisabled: AppTheme.neutral600, + iconPrimary: AppTheme.neutral50, + iconSecondary: const Color(0xFFA0A8A5), + iconDisabled: AppTheme.neutral600, + iconInverse: AppTheme.neutral900, + headingLarge: TextStyle( + fontSize: AppTypography.displaySmall, + fontWeight: FontWeight.w700, + color: AppTheme.neutral50, + height: 1.2, + ), + headingMedium: TextStyle( + fontSize: AppTypography.headlineLarge, + fontWeight: FontWeight.w600, + color: AppTheme.neutral50, + height: 1.3, + ), + headingSmall: TextStyle( + fontSize: AppTypography.headlineSmall, + fontWeight: FontWeight.w600, + color: AppTheme.neutral50, + height: 1.4, + ), + bodyLarge: TextStyle( + fontSize: AppTypography.bodyLarge, + fontWeight: FontWeight.w400, + color: AppTheme.neutral50, + height: 1.5, + ), + bodyMedium: TextStyle( + fontSize: AppTypography.bodyMedium, + fontWeight: FontWeight.w400, + color: AppTheme.neutral50, + height: 1.5, + ), + bodySmall: TextStyle( + fontSize: AppTypography.bodySmall, + fontWeight: FontWeight.w400, + color: const Color(0xFFD1D5DB), + height: 1.4, + ), + caption: TextStyle( + fontSize: AppTypography.labelMedium, + fontWeight: FontWeight.w500, + color: AppTheme.neutral300, + height: 1.3, + letterSpacing: 0.5, + ), + label: TextStyle( + fontSize: AppTypography.labelLarge, + fontWeight: FontWeight.w500, + color: const Color(0xFFD1D5DB), + height: 1.3, + ), + code: TextStyle( + fontSize: AppTypography.bodySmall, + fontWeight: FontWeight.w400, + color: const Color(0xFFD1D5DB), + height: 1.4, + fontFamily: AppTypography.monospaceFontFamily, + ), + ); + } - // Card and surface colors - Enhanced depth and hierarchy - cardBackground: Color(0xFF0C0F0E), - cardBorder: Color(0xFF151918), - cardShadow: AppTheme.neutral900, - surfaceBackground: Color(0xFF0A0D0C), - surfaceContainer: Color(0xFF0C0F0E), - surfaceContainerHighest: Color(0xFF121514), + /// Light theme extension derived from the active color palette. + static ConduitThemeExtension lightPalette(AppColorPalette palette) { + final lightTone = palette.light; + final darkTone = palette.dark; + return ConduitThemeExtension( + chatBubbleUser: lightTone.primary, + chatBubbleAssistant: const Color(0xFFF7F7F7), + chatBubbleUserText: AppTheme.neutral50, + chatBubbleAssistantText: const Color(0xFF1C1C1C), + chatBubbleUserBorder: darkTone.primary, + chatBubbleAssistantBorder: const Color(0xFFE7E7E7), + inputBackground: AppTheme.neutral50, + inputBorder: AppTheme.neutral200, + inputBorderFocused: lightTone.primary, + inputText: AppTheme.neutral900, + inputPlaceholder: AppTheme.neutral500, + inputError: AppTheme.error, + cardBackground: AppTheme.neutral50, + cardBorder: const Color(0xFFE7E7E7), + cardShadow: const Color(0xFFF3F4F6), + surfaceBackground: AppTheme.neutral50, + surfaceContainer: const Color(0xFFF7F7F7), + surfaceContainerHighest: const Color(0xFFF0F1F1), + buttonPrimary: lightTone.primary, + buttonPrimaryText: AppTheme.neutral50, + buttonSecondary: const Color(0xFFF0F1F1), + buttonSecondaryText: const Color(0xFF1C1C1C), + buttonDisabled: AppTheme.neutral300, + buttonDisabledText: AppTheme.neutral500, + success: const Color(0xFF166534), + successBackground: const Color(0xFFECFDF3), + error: const Color(0xFFB91C1C), + errorBackground: const Color(0xFFFEE2E2), + warning: const Color(0xFF92400E), + warningBackground: const Color(0xFFFEF3C7), + info: const Color(0xFF1D4ED8), + infoBackground: const Color(0xFFDBEAFE), + dividerColor: AppTheme.neutral100, + navigationBackground: AppTheme.neutral50, + navigationSelected: lightTone.primary, + navigationUnselected: AppTheme.neutral600, + navigationSelectedBackground: _surfaceTint( + lightTone.primary, + AppTheme.neutral50, + 0.16, + ), + shimmerBase: const Color(0xFFF3F4F6), + shimmerHighlight: AppTheme.neutral50, + loadingIndicator: lightTone.primary, + textPrimary: const Color(0xFF1C1C1C), + textSecondary: const Color(0xFF3A3F3E), + textTertiary: AppTheme.neutral500, + textInverse: AppTheme.neutral50, + textDisabled: AppTheme.neutral400, + iconPrimary: const Color(0xFF1C1C1C), + iconSecondary: const Color(0xFF666C6A), + iconDisabled: AppTheme.neutral400, + iconInverse: AppTheme.neutral50, + headingLarge: TextStyle( + fontSize: AppTypography.displaySmall, + fontWeight: FontWeight.w700, + color: const Color(0xFF111827), + height: 1.2, + ), + headingMedium: TextStyle( + fontSize: AppTypography.headlineLarge, + fontWeight: FontWeight.w600, + color: const Color(0xFF111827), + height: 1.3, + ), + headingSmall: TextStyle( + fontSize: AppTypography.headlineSmall, + fontWeight: FontWeight.w600, + color: const Color(0xFF111827), + height: 1.4, + ), + bodyLarge: TextStyle( + fontSize: AppTypography.bodyLarge, + fontWeight: FontWeight.w400, + color: const Color(0xFF111827), + height: 1.5, + ), + bodyMedium: TextStyle( + fontSize: AppTypography.bodyMedium, + fontWeight: FontWeight.w400, + color: const Color(0xFF111827), + height: 1.5, + ), + bodySmall: TextStyle( + fontSize: AppTypography.bodySmall, + fontWeight: FontWeight.w400, + color: AppTheme.neutral500, + height: 1.4, + ), + caption: TextStyle( + fontSize: AppTypography.labelMedium, + fontWeight: FontWeight.w500, + color: AppTheme.neutral400, + height: 1.3, + letterSpacing: 0.5, + ), + label: TextStyle( + fontSize: AppTypography.labelLarge, + fontWeight: FontWeight.w500, + color: const Color(0xFF444948), + height: 1.3, + ), + code: TextStyle( + fontSize: AppTypography.bodySmall, + fontWeight: FontWeight.w400, + color: const Color(0xFF1C1C1C), + height: 1.4, + fontFamily: AppTypography.monospaceFontFamily, + ), + ); + } - // Interactive element colors - More vibrant and accessible - buttonPrimary: AppTheme.brandPrimaryDark, - buttonPrimaryText: AppTheme.neutral50, - buttonSecondary: Color(0xFF151918), - buttonSecondaryText: AppTheme.neutral50, - buttonDisabled: AppTheme.neutral600, - buttonDisabledText: AppTheme.neutral400, - - // Status and feedback colors - Enhanced visibility - success: Color(0xFF34D399), - successBackground: Color(0xFF14532D), - error: Color(0xFFFCA5A5), - errorBackground: Color(0xFF7F1D1D), - warning: Color(0xFFFBBF24), - warningBackground: Color(0xFF451A03), - info: Color(0xFF93C5FD), - infoBackground: Color(0xFF0C4A6E), - - // Navigation and UI element colors - Enhanced contrast - dividerColor: AppTheme.neutral600, - navigationBackground: Color(0xFF0A0D0C), - navigationSelected: AppTheme.brandPrimaryDark, - navigationUnselected: AppTheme.neutral300, - navigationSelectedBackground: Color(0xFF312E81), - - // Loading and animation colors - Enhanced visibility - shimmerBase: Color(0xFF121514), - shimmerHighlight: Color(0xFF1A1D1C), - loadingIndicator: AppTheme.brandPrimaryDark, - // Text colors - Enhanced hierarchy - textPrimary: AppTheme.neutral50, - textSecondary: Color(0xFFBAC2C0), - textTertiary: AppTheme.neutral400, - textInverse: AppTheme.neutral900, - textDisabled: AppTheme.neutral600, - - // Icon colors - Enhanced visibility - iconPrimary: AppTheme.neutral50, - iconSecondary: Color(0xFFA0A8A5), - iconDisabled: AppTheme.neutral600, - iconInverse: AppTheme.neutral900, - - // Typography styles - headingLarge: TextStyle( - fontSize: AppTypography.displaySmall, - fontWeight: FontWeight.w700, - color: AppTheme.neutral50, - height: 1.2, - ), - headingMedium: TextStyle( - fontSize: AppTypography.headlineLarge, - fontWeight: FontWeight.w600, - color: AppTheme.neutral50, - height: 1.3, - ), - headingSmall: TextStyle( - fontSize: AppTypography.headlineSmall, - fontWeight: FontWeight.w600, - color: AppTheme.neutral50, - height: 1.4, - ), - bodyLarge: TextStyle( - fontSize: AppTypography.bodyLarge, - fontWeight: FontWeight.w400, - color: AppTheme.neutral50, - height: 1.5, - ), - bodyMedium: TextStyle( - fontSize: AppTypography.bodyMedium, - fontWeight: FontWeight.w400, - color: AppTheme.neutral50, - height: 1.5, - ), - bodySmall: TextStyle( - fontSize: AppTypography.bodySmall, - fontWeight: FontWeight.w400, - color: Color(0xFFD1D5DB), // Enhanced contrast - height: 1.4, - ), - caption: TextStyle( - fontSize: AppTypography.labelMedium, - fontWeight: FontWeight.w500, - color: AppTheme.neutral300, - height: 1.3, - letterSpacing: 0.5, - ), - label: TextStyle( - fontSize: AppTypography.labelLarge, - fontWeight: FontWeight.w500, - color: Color(0xFFD1D5DB), // Enhanced contrast - height: 1.3, - ), - code: TextStyle( - fontSize: AppTypography.bodySmall, - fontWeight: FontWeight.w400, - color: Color(0xFFD1D5DB), // Enhanced contrast - height: 1.4, - fontFamily: AppTypography.monospaceFontFamily, - ), - ); - - /// Light theme extension - static const ConduitThemeExtension light = ConduitThemeExtension( - // Chat-specific colors - Enhanced for production-grade look - chatBubbleUser: AppTheme.brandPrimary, - chatBubbleAssistant: Color(0xFFF7F7F7), - chatBubbleUserText: AppTheme.neutral50, - chatBubbleAssistantText: Color(0xFF1C1C1C), - chatBubbleUserBorder: AppTheme.brandPrimaryDark, - chatBubbleAssistantBorder: Color(0xFFE7E7E7), - // Input and form colors - inputBackground: AppTheme.neutral50, - inputBorder: AppTheme.neutral200, - inputBorderFocused: AppTheme.brandPrimary, - inputText: AppTheme.neutral900, - inputPlaceholder: AppTheme.neutral500, - inputError: AppTheme.error, - - // Card and surface colors - Enhanced depth and hierarchy - cardBackground: AppTheme.neutral50, - cardBorder: Color(0xFFE7E7E7), - cardShadow: Color(0xFFF3F4F6), - surfaceBackground: AppTheme.neutral50, - surfaceContainer: Color(0xFFF7F7F7), - surfaceContainerHighest: Color(0xFFF0F1F1), - // Interactive element colors - More vibrant and accessible - buttonPrimary: AppTheme.brandPrimary, - buttonPrimaryText: AppTheme.neutral50, - buttonSecondary: Color(0xFFF0F1F1), - buttonSecondaryText: Color(0xFF1C1C1C), - buttonDisabled: AppTheme.neutral300, - buttonDisabledText: AppTheme.neutral500, - - // Status and feedback colors - Enhanced visibility - success: Color(0xFF166534), - successBackground: Color(0xFFECFDF3), - error: Color(0xFFB91C1C), - errorBackground: Color(0xFFFEE2E2), - warning: Color(0xFF92400E), - warningBackground: Color(0xFFFEF3C7), - info: Color(0xFF1D4ED8), - infoBackground: Color(0xFFDBEAFE), - - // Navigation and UI element colors - Enhanced contrast - dividerColor: AppTheme.neutral100, - navigationBackground: AppTheme.neutral50, - navigationSelected: AppTheme.brandPrimary, - navigationUnselected: AppTheme.neutral600, - navigationSelectedBackground: Color(0xFFE0E7FF), - - // Loading and animation colors - Enhanced visibility - shimmerBase: Color(0xFFF3F4F6), - shimmerHighlight: AppTheme.neutral50, - loadingIndicator: AppTheme.brandPrimary, - // Text colors - Enhanced hierarchy - textPrimary: Color(0xFF1C1C1C), - textSecondary: Color(0xFF3A3F3E), - textTertiary: AppTheme.neutral500, - textInverse: AppTheme.neutral50, - textDisabled: AppTheme.neutral400, - - // Icon colors - Enhanced visibility - iconPrimary: Color(0xFF1C1C1C), - iconSecondary: Color(0xFF666C6A), - iconDisabled: AppTheme.neutral400, - iconInverse: AppTheme.neutral50, - - // Typography styles - headingLarge: TextStyle( - fontSize: AppTypography.displaySmall, - fontWeight: FontWeight.w700, - color: Color(0xFF111827), // Better contrast - height: 1.2, - ), - headingMedium: TextStyle( - fontSize: AppTypography.headlineLarge, - fontWeight: FontWeight.w600, - color: Color(0xFF111827), // Better contrast - height: 1.3, - ), - headingSmall: TextStyle( - fontSize: AppTypography.headlineSmall, - fontWeight: FontWeight.w600, - color: Color(0xFF111827), // Better contrast - height: 1.4, - ), - bodyLarge: TextStyle( - fontSize: AppTypography.bodyLarge, - fontWeight: FontWeight.w400, - color: Color(0xFF111827), // Better contrast - height: 1.5, - ), - bodyMedium: TextStyle( - fontSize: AppTypography.bodyMedium, - fontWeight: FontWeight.w400, - color: Color(0xFF374151), // Better contrast - height: 1.5, - ), - bodySmall: TextStyle( - fontSize: AppTypography.bodySmall, - fontWeight: FontWeight.w400, - color: Color(0xFF6B7280), // Better contrast - height: 1.4, - ), - caption: TextStyle( - fontSize: AppTypography.labelMedium, - fontWeight: FontWeight.w500, - color: AppTheme.neutral500, - height: 1.3, - letterSpacing: 0.5, - ), - label: TextStyle( - fontSize: AppTypography.labelLarge, - fontWeight: FontWeight.w500, - color: Color(0xFF374151), // Better contrast - height: 1.3, - ), - code: TextStyle( - fontSize: AppTypography.bodySmall, - fontWeight: FontWeight.w400, - color: Color(0xFF374151), // Better contrast - height: 1.4, - fontFamily: AppTypography.monospaceFontFamily, - ), - ); + static Color _surfaceTint(Color tone, Color surface, double opacity) { + return Color.alphaBlend(tone.withValues(alpha: opacity), surface); + } } /// Extension method to easily access Conduit theme from BuildContext extension ConduitThemeContext on BuildContext { ConduitThemeExtension get conduitTheme { - return Theme.of(this).extension() ?? - ConduitThemeExtension.dark; + final theme = Theme.of(this); + final extension = theme.extension(); + if (extension != null) return extension; + final palette = + theme.extension()?.palette ?? + AppColorPalettes.auroraViolet; + return theme.brightness == Brightness.dark + ? ConduitThemeExtension.darkPalette(palette) + : ConduitThemeExtension.lightPalette(palette); + } +} + +extension ConduitPaletteContext on BuildContext { + AppColorPalette get conduitPalette { + return Theme.of(this).extension()?.palette ?? + AppColorPalettes.auroraViolet; } } @@ -931,7 +932,9 @@ class ConduitShadows { static List get glow => [ BoxShadow( - color: AppTheme.brandPrimary.withValues(alpha: 0.25), + color: AppColorPalettes.auroraViolet.light.primary.withValues( + alpha: 0.25, + ), blurRadius: 20, offset: const Offset(0, 0), spreadRadius: 0, diff --git a/lib/shared/widgets/loading_states.dart b/lib/shared/widgets/loading_states.dart index 7ef6d7c..f7270fb 100644 --- a/lib/shared/widgets/loading_states.dart +++ b/lib/shared/widgets/loading_states.dart @@ -21,7 +21,7 @@ class ConduitLoading { }) { return _LoadingIndicator( size: size, - color: color ?? BrandService.primaryBrandColor, + color: color, message: message, type: _LoadingType.primary, ); @@ -40,7 +40,7 @@ class ConduitLoading { color ?? (context?.conduitTheme.loadingIndicator ?? context?.conduitTheme.buttonPrimary ?? - AppTheme.brandPrimary), + BrandService.primaryBrandColor(context: context)), message: message, type: _LoadingType.inline, ); @@ -91,30 +91,35 @@ enum _LoadingType { primary, inline, button } class _LoadingIndicator extends StatelessWidget { final double size; - final Color color; + final Color? color; final String? message; final _LoadingType type; const _LoadingIndicator({ required this.size, - required this.color, + this.color, this.message, required this.type, }); @override Widget build(BuildContext context) { + final resolvedColor = color ?? context.conduitTheme.loadingIndicator; + Widget indicator; if (Platform.isIOS) { - indicator = CupertinoActivityIndicator(color: color, radius: size / 2); + indicator = CupertinoActivityIndicator( + color: resolvedColor, + radius: size / 2, + ); } else { indicator = SizedBox( width: size, height: size, child: CircularProgressIndicator( strokeWidth: size / 8, - valueColor: AlwaysStoppedAnimation(color), + valueColor: AlwaysStoppedAnimation(resolvedColor), ), ); }