From 1ea85d5ed1cf3c5c3106b5e77eea1ccc01086a8d Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Fri, 3 Oct 2025 00:12:25 +0530 Subject: [PATCH] refactor: enhance theme and error handling across the application - Updated error handling in EnhancedErrorService to utilize context for color tokens, improving theme consistency. - Refactored various components to use context-aware shadow and color properties, enhancing visual coherence. - Replaced hardcoded color values with dynamic tokens in multiple widgets, ensuring better adaptability to theme changes. - Improved overall code maintainability by centralizing theme-related logic and reducing direct dependencies on static theme values. --- lib/core/error/enhanced_error_service.dart | 24 +- lib/core/providers/app_startup_providers.dart | 2 +- .../auth/views/connection_issue_page.dart | 8 +- lib/features/chat/views/chat_page.dart | 18 +- .../widgets/enhanced_image_attachment.dart | 10 +- .../chat/widgets/modern_chat_input.dart | 8 +- .../chat/widgets/user_message_bubble.dart | 8 +- .../navigation/widgets/chats_drawer.dart | 6 +- .../onboarding/views/onboarding_sheet.dart | 6 +- .../profile/views/app_customization_page.dart | 2 +- lib/features/profile/views/profile_page.dart | 8 +- lib/shared/services/brand_service.dart | 38 +- lib/shared/theme/app_theme.dart | 179 +++-- lib/shared/theme/color_tokens.dart | 461 +++++++++++++ lib/shared/theme/theme_extensions.dart | 614 ++++++++++-------- .../utils/conversation_context_menu.dart | 2 +- lib/shared/widgets/conduit_components.dart | 2 +- .../widgets/improved_loading_states.dart | 2 +- lib/shared/widgets/loading_states.dart | 7 +- .../widgets/markdown/markdown_config.dart | 56 +- lib/shared/widgets/offline_indicator.dart | 2 +- 21 files changed, 1009 insertions(+), 454 deletions(-) create mode 100644 lib/shared/theme/color_tokens.dart diff --git a/lib/core/error/enhanced_error_service.dart b/lib/core/error/enhanced_error_service.dart index 3d9a8ec..6630c7a 100644 --- a/lib/core/error/enhanced_error_service.dart +++ b/lib/core/error/enhanced_error_service.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'api_error.dart'; import 'api_error_handler.dart'; import 'api_error_interceptor.dart'; -import '../../shared/theme/app_theme.dart'; import '../../shared/theme/theme_extensions.dart'; import 'package:conduit/l10n/app_localizations.dart'; import '../utils/debug_logger.dart'; @@ -132,7 +131,7 @@ class EnhancedErrorService { ], ], ), - backgroundColor: _getErrorColor(error), + backgroundColor: _getErrorColor(context, error), duration: duration ?? _getSnackbarDuration(error), action: isRetryableError && onRetry != null ? SnackBarAction( @@ -169,7 +168,7 @@ class EnhancedErrorService { return AlertDialog( title: Row( children: [ - Icon(_getErrorIcon(error), color: _getErrorColor(error)), + Icon(_getErrorIcon(error), color: _getErrorColor(context, error)), const SizedBox(width: Spacing.sm), Expanded(child: Text(title ?? _getErrorTitle(error))), ], @@ -250,7 +249,7 @@ class EnhancedErrorService { Icon( _getErrorIcon(error), size: IconSize.xxl, - color: _getErrorColor(error), + color: _getErrorColor(context, error), ), const SizedBox(height: Spacing.md), Text( @@ -416,27 +415,28 @@ class EnhancedErrorService { return Icons.error_outline; } - Color _getErrorColor(dynamic error) { + Color _getErrorColor(BuildContext context, dynamic error) { + final tokens = context.colorTokens; if (error is ApiError) { switch (error.type) { case ApiErrorType.network: case ApiErrorType.timeout: - return AppTheme.warning; + return tokens.statusWarning60; case ApiErrorType.authentication: case ApiErrorType.authorization: - return AppTheme.error; + return tokens.statusError60; case ApiErrorType.validation: case ApiErrorType.badRequest: - return AppTheme.warning; + return tokens.statusWarning60; case ApiErrorType.server: - return AppTheme.error; + return tokens.statusError60; case ApiErrorType.rateLimit: - return AppTheme.info; + return tokens.statusInfo60; default: - return AppTheme.error; + return tokens.statusError60; } } - return AppTheme.error; + return tokens.statusError60; } String _getErrorTitle(dynamic error) { diff --git a/lib/core/providers/app_startup_providers.dart b/lib/core/providers/app_startup_providers.dart index f12a39c..b88ebae 100644 --- a/lib/core/providers/app_startup_providers.dart +++ b/lib/core/providers/app_startup_providers.dart @@ -535,7 +535,7 @@ Future _maybeShowOnboarding(Ref ref) async { borderRadius: const BorderRadius.vertical( top: Radius.circular(AppBorderRadius.modal), ), - boxShadow: ConduitShadows.modal, + boxShadow: ConduitShadows.modal(context), ), child: const OnboardingSheet(), ), diff --git a/lib/features/auth/views/connection_issue_page.dart b/lib/features/auth/views/connection_issue_page.dart index 6a20c09..ca2b2d2 100644 --- a/lib/features/auth/views/connection_issue_page.dart +++ b/lib/features/auth/views/connection_issue_page.dart @@ -116,13 +116,7 @@ class _ConnectionIssuePageState extends ConsumerState { decoration: BoxDecoration( color: context.conduitTheme.surfaceContainerHighest, shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.08), - blurRadius: 18, - offset: const Offset(0, 12), - ), - ], + boxShadow: ConduitShadows.high(context), ), child: Icon( Platform.isIOS diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index a73cb95..2c3c1ac 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -214,7 +214,7 @@ class _ChatPageState extends ConsumerState { borderRadius: const BorderRadius.vertical( top: Radius.circular(AppBorderRadius.modal), ), - boxShadow: ConduitShadows.modal, + boxShadow: ConduitShadows.modal(context), ), child: const OnboardingSheet(), ), @@ -735,7 +735,7 @@ class _ChatPageState extends ConsumerState { color: context.conduitTheme.cardBorder, width: BorderWidth.regular, ), - boxShadow: ConduitShadows.messageBubble, + boxShadow: ConduitShadows.messageBubble(context), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1202,7 +1202,7 @@ class _ChatPageState extends ConsumerState { drawerEnableOpenDragGesture: true, drawerDragStartBehavior: DragStartBehavior.start, drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.75, - drawerScrimColor: Colors.black.withValues(alpha: 0.32), + drawerScrimColor: context.colorTokens.overlayStrong, drawer: Drawer( width: (MediaQuery.of(context).size.width * 0.80).clamp( 280.0, @@ -1638,7 +1638,7 @@ class _ChatPageState extends ConsumerState { borderRadius: BorderRadius.circular( AppBorderRadius.floatingButton, ), - boxShadow: ConduitShadows.button, + boxShadow: ConduitShadows.button(context), ), child: SizedBox( width: TouchTarget.button, @@ -1838,7 +1838,7 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> { color: context.conduitTheme.dividerColor, width: BorderWidth.regular, ), - boxShadow: ConduitShadows.modal, + boxShadow: ConduitShadows.modal(context), ), child: ModalSheetSafeArea( padding: const EdgeInsets.symmetric( @@ -2043,7 +2043,7 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> { : context.conduitTheme.dividerColor, width: BorderWidth.regular, ), - boxShadow: isSelected ? ConduitShadows.card : null, + boxShadow: isSelected ? ConduitShadows.card(context) : null, ), child: Padding( padding: const EdgeInsets.symmetric( @@ -2327,7 +2327,7 @@ class _VoiceInputSheetState extends ConsumerState<_VoiceInputSheet> { color: context.conduitTheme.dividerColor, width: BorderWidth.regular, ), - boxShadow: ConduitShadows.modal, + boxShadow: ConduitShadows.modal(context), ), padding: const EdgeInsets.all(Spacing.bottomSheetPadding), child: SafeArea( @@ -2435,7 +2435,7 @@ class _VoiceInputSheetState extends ConsumerState<_VoiceInputSheet> { top: Radius.circular(AppBorderRadius.bottomSheet), ), border: Border.all(color: context.conduitTheme.dividerColor, width: 1), - boxShadow: ConduitShadows.modal, + boxShadow: ConduitShadows.modal(context), ), child: SafeArea( top: false, @@ -2940,7 +2940,7 @@ class _SelectableMessageWrapper extends StatelessWidget { decoration: BoxDecoration( color: context.conduitTheme.buttonPrimary, shape: BoxShape.circle, - boxShadow: ConduitShadows.medium, + boxShadow: ConduitShadows.medium(context), ), child: Icon( Icons.check, diff --git a/lib/features/chat/widgets/enhanced_image_attachment.dart b/lib/features/chat/widgets/enhanced_image_attachment.dart index 1fa3f0b..0a2a989 100644 --- a/lib/features/chat/widgets/enhanced_image_attachment.dart +++ b/lib/features/chat/widgets/enhanced_image_attachment.dart @@ -572,8 +572,12 @@ class FullScreenImageViewer extends ConsumerWidget { } } + final tokens = context.colorTokens; + final background = tokens.neutralTone10; + final iconColor = tokens.neutralOnSurface; + return Scaffold( - backgroundColor: Colors.black, + backgroundColor: background, body: Stack( children: [ Center( @@ -595,14 +599,14 @@ class FullScreenImageViewer extends ConsumerWidget { IconButton( icon: Icon( Platform.isIOS ? Icons.ios_share : Icons.share_outlined, - color: Colors.white, + color: iconColor, size: 26, ), onPressed: () => _shareImage(context, ref), ), const SizedBox(width: 8), IconButton( - icon: const Icon(Icons.close, color: Colors.white, size: 28), + icon: Icon(Icons.close, color: iconColor, size: 28), onPressed: () => Navigator.of(context).pop(), ), ], diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index f2b03f1..db2a8c0 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -1281,7 +1281,7 @@ class _ModernChatInputState extends ConsumerState alpha: Alpha.buttonPressed, ), borderRadius: BorderRadius.circular(radius), - boxShadow: ConduitShadows.button, + boxShadow: ConduitShadows.button(context), ), child: Center( child: Icon( @@ -1696,7 +1696,7 @@ class _ModernChatInputState extends ConsumerState color: theme.dividerColor, width: BorderWidth.thin, ), - boxShadow: ConduitShadows.modal, + boxShadow: ConduitShadows.modal(context), ), child: ModalSheetSafeArea( padding: const EdgeInsets.fromLTRB( @@ -1810,7 +1810,7 @@ class _ModernChatInputState extends ConsumerState color: background, borderRadius: BorderRadius.circular(AppBorderRadius.input), border: Border.all(color: borderColor, width: BorderWidth.thin), - boxShadow: value ? ConduitShadows.low : const [], + boxShadow: value ? ConduitShadows.low(context) : const [], ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -1897,7 +1897,7 @@ class _ModernChatInputState extends ConsumerState color: background, borderRadius: BorderRadius.circular(AppBorderRadius.input), border: Border.all(color: borderColor, width: BorderWidth.thin), - boxShadow: selected ? ConduitShadows.low : const [], + boxShadow: selected ? ConduitShadows.low(context) : const [], ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/features/chat/widgets/user_message_bubble.dart b/lib/features/chat/widgets/user_message_bubble.dart index 8e3b16e..20343ef 100644 --- a/lib/features/chat/widgets/user_message_bubble.dart +++ b/lib/features/chat/widgets/user_message_bubble.dart @@ -563,13 +563,7 @@ class _UserMessageBubbleState extends ConsumerState context.conduitTheme.chatBubbleUserBorder, width: BorderWidth.regular, ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.08), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], + boxShadow: ConduitShadows.small(context), ), child: _isEditing ? Focus( diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index c297dfd..8b32d2a 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -1362,7 +1362,7 @@ class _ChatsDrawerState extends ConsumerState { color: theme.dividerColor, width: BorderWidth.regular, ), - boxShadow: ConduitShadows.card, + boxShadow: ConduitShadows.card(context), ), child: Row( children: [ @@ -1631,7 +1631,9 @@ class _ConversationTile extends StatelessWidget { final Color borderColor = selected ? theme.buttonPrimary.withValues(alpha: 0.7) : theme.surfaceContainerHighest.withValues(alpha: 0.40); - final List shadow = selected ? ConduitShadows.low : const []; + final List shadow = selected + ? ConduitShadows.low(context) + : const []; Color? overlayForStates(Set states) { if (states.contains(WidgetState.pressed)) { diff --git a/lib/features/onboarding/views/onboarding_sheet.dart b/lib/features/onboarding/views/onboarding_sheet.dart index b25d091..433280f 100644 --- a/lib/features/onboarding/views/onboarding_sheet.dart +++ b/lib/features/onboarding/views/onboarding_sheet.dart @@ -85,7 +85,7 @@ class _OnboardingSheetState extends ConsumerState { borderRadius: const BorderRadius.vertical( top: Radius.circular(AppBorderRadius.modal), ), - boxShadow: ConduitShadows.modal, + boxShadow: ConduitShadows.modal(context), ), child: SafeArea( child: Padding( @@ -228,7 +228,7 @@ class _IllustratedPage extends StatelessWidget { decoration: BoxDecoration( color: context.conduitTheme.buttonPrimary, borderRadius: BorderRadius.circular(AppBorderRadius.avatar), - boxShadow: ConduitShadows.glow, + boxShadow: ConduitShadows.glow(context), ), child: Icon(page.icon, color: context.conduitTheme.textInverse), ).animate().scale(duration: AnimationDuration.fast), @@ -304,7 +304,7 @@ class _IllustratedPage extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, color: context.conduitTheme.buttonPrimary.withValues(alpha: alpha), - boxShadow: ConduitShadows.glow, + boxShadow: ConduitShadows.glow(context), ), ); } diff --git a/lib/features/profile/views/app_customization_page.dart b/lib/features/profile/views/app_customization_page.dart index a80d17e..7bd1c92 100644 --- a/lib/features/profile/views/app_customization_page.dart +++ b/lib/features/profile/views/app_customization_page.dart @@ -562,7 +562,7 @@ class AppCustomizationPage extends ConsumerWidget { borderRadius: const BorderRadius.vertical( top: Radius.circular(AppBorderRadius.modal), ), - boxShadow: ConduitShadows.modal, + boxShadow: ConduitShadows.modal(context), ), child: SafeArea( top: false, diff --git a/lib/features/profile/views/profile_page.dart b/lib/features/profile/views/profile_page.dart index 19825ef..741f855 100644 --- a/lib/features/profile/views/profile_page.dart +++ b/lib/features/profile/views/profile_page.dart @@ -336,7 +336,7 @@ class ProfilePage extends ConsumerWidget { color: accent.withValues(alpha: 0.18), width: BorderWidth.thin, ), - boxShadow: ConduitShadows.medium, + boxShadow: ConduitShadows.medium(context), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -347,7 +347,7 @@ class ProfilePage extends ConsumerWidget { Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppBorderRadius.avatar), - boxShadow: ConduitShadows.high, + boxShadow: ConduitShadows.high(context), ), child: UserAvatar( size: IconSize.avatar, @@ -973,7 +973,7 @@ class _DefaultModelBottomSheetState color: context.conduitTheme.dividerColor, width: BorderWidth.regular, ), - boxShadow: ConduitShadows.modal, + boxShadow: ConduitShadows.modal(context), ), child: ModalSheetSafeArea( padding: const EdgeInsets.symmetric( @@ -1234,7 +1234,7 @@ class _DefaultModelBottomSheetState : context.conduitTheme.dividerColor, width: BorderWidth.regular, ), - boxShadow: isSelected ? ConduitShadows.card : null, + boxShadow: isSelected ? ConduitShadows.card(context) : null, ), child: Padding( padding: const EdgeInsets.symmetric( diff --git a/lib/shared/services/brand_service.dart b/lib/shared/services/brand_service.dart index 841d09b..55d7889 100644 --- a/lib/shared/services/brand_service.dart +++ b/lib/shared/services/brand_service.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import '../theme/theme_extensions.dart'; import 'package:flutter/cupertino.dart'; import 'dart:io' show Platform; -import '../theme/app_theme.dart'; +import '../theme/color_tokens.dart'; import '../theme/color_palettes.dart'; /// Centralized service for consistent brand identity throughout the app @@ -107,8 +107,10 @@ class BrandService { BuildContext? context, }) { final bgColor = backgroundColor ?? primaryBrandColor(context: context); + final tokens = _resolveTokens(context); final iColor = - iconColor ?? (context?.conduitTheme.textInverse ?? AppTheme.neutral50); + iconColor ?? + (context?.conduitTheme.textInverse ?? tokens.neutralTone00); return Container( width: size, @@ -175,8 +177,9 @@ class BrandService { bool showBackground = true, BuildContext? context, }) { + final tokens = _resolveTokens(context); final iconColor = - color ?? (context?.conduitTheme.iconSecondary ?? AppTheme.neutral400); + color ?? (context?.conduitTheme.iconSecondary ?? tokens.neutralTone80); if (!showBackground) { return createBrandIcon( @@ -191,10 +194,10 @@ class BrandService { width: size, height: size, decoration: BoxDecoration( - color: context?.conduitTheme.surfaceBackground ?? AppTheme.neutral700, + color: context?.conduitTheme.surfaceBackground ?? tokens.neutralTone10, borderRadius: BorderRadius.circular(size / 2), border: Border.all( - color: context?.conduitTheme.dividerColor ?? AppTheme.neutral600, + color: context?.conduitTheme.dividerColor ?? tokens.neutralTone40, width: 2, ), ), @@ -218,6 +221,7 @@ class BrandService { BuildContext? context, }) { final theme = context?.conduitTheme; + final tokens = _resolveTokens(context); return SizedBox( width: width, height: 48, @@ -228,16 +232,17 @@ class BrandService { : createBrandIcon( size: IconSize.md, icon: icon ?? primaryIcon, - color: theme?.textInverse ?? AppTheme.neutral50, + color: theme?.textInverse ?? tokens.neutralTone00, context: context, ), label: Text(text), style: ElevatedButton.styleFrom( backgroundColor: isSecondary - ? (theme?.buttonSecondary ?? AppTheme.neutral700) + ? (theme?.buttonSecondary ?? tokens.neutralTone20) : (theme?.buttonPrimary ?? primaryBrandColor(context: context)), - foregroundColor: theme?.buttonPrimaryText ?? AppTheme.neutral50, - disabledBackgroundColor: theme?.buttonDisabled ?? AppTheme.neutral500, + foregroundColor: theme?.buttonPrimaryText ?? tokens.brandOn60, + disabledBackgroundColor: + theme?.buttonDisabled ?? tokens.neutralTone40, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), ), @@ -292,6 +297,7 @@ class BrandService { BuildContext? context, }) { final theme = context?.conduitTheme; + final tokens = _resolveTokens(context); final baseColor = theme?.buttonPrimary ?? primaryBrandColor(context: context, brightness: Brightness.dark); @@ -309,12 +315,14 @@ class BrandService { colors: [baseColor, accentColor], ), borderRadius: BorderRadius.circular(size / 2), - boxShadow: ConduitShadows.glow, + boxShadow: context != null + ? ConduitShadows.glow(context) + : ConduitShadows.glowWithTokens(tokens), ), child: Icon( primaryIcon, size: size * 0.5, - color: theme?.textInverse ?? AppTheme.neutral50, + color: theme?.textInverse ?? tokens.neutralTone00, ), ); } @@ -330,4 +338,12 @@ class BrandService { static Brightness _resolveBrightness(BuildContext? context) { return context != null ? Theme.of(context).brightness : Brightness.light; } + + static AppColorTokens _resolveTokens(BuildContext? context) { + final palette = _resolvePalette(context); + final brightness = _resolveBrightness(context); + return brightness == Brightness.dark + ? AppColorTokens.dark(palette: palette) + : AppColorTokens.light(palette: palette); + } } diff --git a/lib/shared/theme/app_theme.dart b/lib/shared/theme/app_theme.dart index 1803ec7..9937ecb 100644 --- a/lib/shared/theme/app_theme.dart +++ b/lib/shared/theme/app_theme.dart @@ -1,59 +1,64 @@ +import 'dart:math' as math; + 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'; +import 'color_tokens.dart'; class AppTheme { // Enhanced neutral palette for better contrast (WCAG AA compliant) - static const Color neutral900 = Color(0xFF000000); // Pure black - static const Color neutral800 = Color( - 0xFF0D0D0D, - ); // Darker for better contrast - static const Color neutral700 = Color(0xFF1A1A1A); - static const Color neutral600 = Color(0xFF2D2D2D); // Improved contrast - static const Color neutral500 = Color(0xFF404040); // Better middle gray - static const Color neutral400 = Color(0xFF525252); - static const Color neutral300 = Color(0xFF6B6B6B); // Improved contrast ratio - static const Color neutral200 = Color(0xFF9E9E9E); // Better readability - static const Color neutral100 = Color(0xFFD1D1D1); // Enhanced contrast - static const Color neutral50 = Color( - 0xFFF8F8F8, - ); // Softer white for reduced eye strain + static const Color neutral900 = Color(0xFF0B0E14); + static const Color neutral800 = Color(0xFF161B24); + static const Color neutral700 = Color(0xFF1F2531); + static const Color neutral600 = Color(0xFF343C4D); + static const Color neutral500 = Color(0xFF4A5161); + static const Color neutral400 = Color(0xFF9099AC); + static const Color neutral300 = Color(0xFFC5CCD9); + static const Color neutral200 = Color(0xFFE6EAF1); + static const Color neutral100 = Color(0xFFF5F7FA); + static const Color neutral50 = Color(0xFFFFFFFF); - // Enhanced semantic colors for WCAG AA compliance - static const Color error = Color(0xFFDC2626); // Improved red contrast - static const Color errorDark = Color(0xFFB91C1C); // Darker red for dark theme - static const Color success = Color(0xFF059669); // Better green contrast - static const Color successDark = Color(0xFF047857); // Dark theme green - static const Color warning = Color(0xFFD97706); // Improved orange contrast - static const Color warningDark = Color(0xFFB45309); // Dark theme orange - static const Color info = Color(0xFF0284C7); // Better blue contrast - static const Color infoDark = Color(0xFF0369A1); // Dark theme blue + // Semantic colors derived from the token specification + static const Color error = Color(0xFFCE2C31); + static const Color errorDark = Color(0xFFFF5F67); + static const Color success = Color(0xFF0E9D58); + static const Color successDark = Color(0xFF23C179); + static const Color warning = Color(0xFFDB7900); + static const Color warningDark = Color(0xFFFF9800); + static const Color info = Color(0xFF0174D3); + static const Color infoDark = Color(0xFF4CA8FF); static ThemeData light(AppColorPalette palette) { final lightTone = palette.light; + final tokens = AppColorTokens.light(palette: palette); + final colorScheme = tokens.toColorScheme().copyWith( + primary: lightTone.primary, + onPrimary: _pickOnColor(lightTone.primary, tokens), + secondary: lightTone.secondary, + onSecondary: _pickOnColor(lightTone.secondary, tokens), + tertiary: lightTone.accent, + onTertiary: _pickOnColor(lightTone.accent, tokens), + surfaceTint: lightTone.primary, + ); 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)), + colorScheme: colorScheme, pageTransitionsTheme: _pageTransitionsTheme, splashFactory: NoSplash.splashFactory, - appBarTheme: const AppBarTheme( + scaffoldBackgroundColor: tokens.neutralTone10, + appBarTheme: AppBarTheme( centerTitle: true, elevation: Elevation.none, backgroundColor: Colors.transparent, - foregroundColor: neutral800, + foregroundColor: tokens.neutralOnSurface, ), bottomSheetTheme: BottomSheetThemeData( - backgroundColor: neutral50, - modalBackgroundColor: neutral50, + backgroundColor: tokens.neutralTone00, + modalBackgroundColor: tokens.neutralTone00, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.modal), @@ -66,6 +71,8 @@ class AppTheme { horizontal: Spacing.lg, vertical: Spacing.xs, ), + backgroundColor: lightTone.primary, + foregroundColor: _pickOnColor(lightTone.primary, tokens), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), ), @@ -75,15 +82,19 @@ class AppTheme { elevation: Elevation.none, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.lg), - side: BorderSide(color: neutral200), + side: BorderSide(color: tokens.neutralTone20), ), ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, - backgroundColor: neutral900.withValues(alpha: 0.92), - contentTextStyle: const TextStyle( - color: neutral50, - ).copyWith(fontSize: AppTypography.bodyMedium), + backgroundColor: Color.alphaBlend( + tokens.overlayStrong, + tokens.neutralOnSurface, + ), + contentTextStyle: TextStyle( + color: tokens.neutralTone00, + fontSize: AppTypography.bodyMedium, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.snackbar), ), @@ -91,7 +102,7 @@ class AppTheme { ), inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: neutral50, + fillColor: tokens.neutralTone00, border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), borderSide: BorderSide.none, @@ -106,7 +117,7 @@ class AppTheme { ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: error, width: 1), + borderSide: BorderSide(color: tokens.statusError60, width: 1), ), contentPadding: const EdgeInsets.symmetric( horizontal: Spacing.md, @@ -115,7 +126,8 @@ class AppTheme { ), textTheme: ThemeData.light().textTheme, extensions: >[ - ConduitThemeExtension.lightPalette(palette), + tokens, + ConduitThemeExtension.lightPalette(palette: palette, tokens: tokens), AppPaletteThemeExtension(palette: palette), ], ); @@ -123,32 +135,33 @@ class AppTheme { static ThemeData dark(AppColorPalette palette) { final darkTone = palette.dark; + final tokens = AppColorTokens.dark(palette: palette); + final colorScheme = tokens.toColorScheme().copyWith( + primary: darkTone.primary, + onPrimary: _pickOnColor(darkTone.primary, tokens), + secondary: darkTone.secondary, + onSecondary: _pickOnColor(darkTone.secondary, tokens), + tertiary: darkTone.accent, + onTertiary: _pickOnColor(darkTone.accent, tokens), + surfaceTint: darkTone.primary, + ); 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, - ), + colorScheme: colorScheme, + scaffoldBackgroundColor: tokens.neutralTone10, pageTransitionsTheme: _pageTransitionsTheme, splashFactory: NoSplash.splashFactory, - appBarTheme: const AppBarTheme( + appBarTheme: AppBarTheme( centerTitle: true, elevation: Elevation.none, backgroundColor: Colors.transparent, - foregroundColor: neutral50, + foregroundColor: tokens.neutralOnSurface, ), bottomSheetTheme: BottomSheetThemeData( - backgroundColor: neutral900, - modalBackgroundColor: neutral900, + backgroundColor: tokens.neutralTone00, + modalBackgroundColor: tokens.neutralTone00, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.modal), @@ -161,6 +174,8 @@ class AppTheme { horizontal: Spacing.lg, vertical: Spacing.xs, ), + backgroundColor: darkTone.primary, + foregroundColor: _pickOnColor(darkTone.primary, tokens), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), ), @@ -170,15 +185,19 @@ class AppTheme { elevation: Elevation.none, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.lg), - side: BorderSide(color: neutral800), + side: BorderSide(color: tokens.neutralTone40), ), ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, - backgroundColor: neutral800.withValues(alpha: 0.92), - contentTextStyle: const TextStyle( - color: neutral50, - ).copyWith(fontSize: AppTypography.bodyMedium), + backgroundColor: Color.alphaBlend( + tokens.overlayStrong, + tokens.neutralTone20, + ), + contentTextStyle: TextStyle( + color: tokens.neutralOnSurface, + fontSize: AppTypography.bodyMedium, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.snackbar), ), @@ -186,14 +205,14 @@ class AppTheme { ), inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: neutral700, + fillColor: tokens.neutralTone20, border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: neutral600, width: 1), + borderSide: BorderSide(color: tokens.neutralTone40, width: 1), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: neutral600, width: 1), + borderSide: BorderSide(color: tokens.neutralTone40, width: 1), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), @@ -201,7 +220,7 @@ class AppTheme { ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: const BorderSide(color: error, width: 1), + borderSide: BorderSide(color: tokens.statusError60, width: 1), ), contentPadding: const EdgeInsets.symmetric( horizontal: Spacing.md, @@ -210,7 +229,8 @@ class AppTheme { ), textTheme: ThemeData.dark().textTheme, extensions: >[ - ConduitThemeExtension.darkPalette(palette), + tokens, + ConduitThemeExtension.darkPalette(palette: palette, tokens: tokens), AppPaletteThemeExtension(palette: palette), ], ); @@ -222,18 +242,33 @@ class AppTheme { ) { final brightness = Theme.of(context).brightness; final tone = palette.toneFor(brightness); + final tokens = brightness == Brightness.dark + ? AppColorTokens.dark(palette: palette) + : AppColorTokens.light(palette: palette); return CupertinoThemeData( brightness: brightness, primaryColor: tone.primary, - scaffoldBackgroundColor: brightness == Brightness.dark - ? neutral900 - : neutral50, - barBackgroundColor: brightness == Brightness.dark - ? neutral900 - : neutral50, + scaffoldBackgroundColor: tokens.neutralTone10, + barBackgroundColor: tokens.neutralTone10, ); } + static Color _pickOnColor(Color background, AppColorTokens tokens) { + final contrastOnLight = _contrastRatio(background, tokens.neutralTone00); + final contrastOnDark = _contrastRatio(background, tokens.neutralOnSurface); + return contrastOnLight >= contrastOnDark + ? tokens.neutralTone00 + : tokens.neutralOnSurface; + } + + static double _contrastRatio(Color a, Color b) { + final luminanceA = a.computeLuminance(); + final luminanceB = b.computeLuminance(); + final lighter = math.max(luminanceA, luminanceB); + final darker = math.min(luminanceA, luminanceB); + return (lighter + 0.05) / (darker + 0.05); + } + static const PageTransitionsTheme _pageTransitionsTheme = PageTransitionsTheme( builders: { diff --git a/lib/shared/theme/color_tokens.dart b/lib/shared/theme/color_tokens.dart new file mode 100644 index 0000000..f95056b --- /dev/null +++ b/lib/shared/theme/color_tokens.dart @@ -0,0 +1,461 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +import 'color_palettes.dart'; + +/// Immutable set of semantic color tokens exposed through [ThemeExtension]. +/// +/// The tokens are derived from the Conduit color specification and provide +/// consistent mappings for light and dark modes. Widgets should prefer using +/// these tokens instead of hard-coded color values to ensure theme parity and +/// accessible contrast levels. +@immutable +class AppColorTokens extends ThemeExtension { + const AppColorTokens({ + required this.brightness, + required this.neutralTone00, + required this.neutralTone10, + required this.neutralTone20, + required this.neutralTone40, + required this.neutralTone60, + required this.neutralTone80, + required this.neutralOnSurface, + required this.brandTone40, + required this.brandTone60, + required this.brandOn60, + required this.brandTone90, + required this.brandOn90, + required this.accentIndigo60, + required this.accentOnIndigo60, + required this.accentTeal60, + required this.accentGold60, + required this.statusSuccess60, + required this.statusOnSuccess60, + required this.statusWarning60, + required this.statusOnWarning60, + required this.statusError60, + required this.statusOnError60, + required this.statusInfo60, + required this.statusOnInfo60, + required this.overlayWeak, + required this.overlayMedium, + required this.overlayStrong, + required this.codeBackground, + required this.codeBorder, + required this.codeText, + required this.codeAccent, + }); + + final Brightness brightness; + + // Neutral tokens + final Color neutralTone00; + final Color neutralTone10; + final Color neutralTone20; + final Color neutralTone40; + final Color neutralTone60; + final Color neutralTone80; + final Color neutralOnSurface; + + // Brand tokens + final Color brandTone40; + final Color brandTone60; + final Color brandOn60; + final Color brandTone90; + final Color brandOn90; + + // Accent tokens + final Color accentIndigo60; + final Color accentOnIndigo60; + final Color accentTeal60; + final Color accentGold60; + + // Status tokens + final Color statusSuccess60; + final Color statusOnSuccess60; + final Color statusWarning60; + final Color statusOnWarning60; + final Color statusError60; + final Color statusOnError60; + final Color statusInfo60; + final Color statusOnInfo60; + + // Overlay tokens + final Color overlayWeak; + final Color overlayMedium; + final Color overlayStrong; + + // Markdown/code tokens + final Color codeBackground; + final Color codeBorder; + final Color codeText; + final Color codeAccent; + + factory AppColorTokens.light({AppColorPalette? palette}) { + return AppColorTokens._fromPalette( + palette ?? AppColorPalettes.auroraViolet, + Brightness.light, + ); + } + + factory AppColorTokens.dark({AppColorPalette? palette}) { + return AppColorTokens._fromPalette( + palette ?? AppColorPalettes.auroraViolet, + Brightness.dark, + ); + } + + factory AppColorTokens._fromPalette( + AppColorPalette palette, + Brightness brightness, + ) { + final AppPaletteTone tone = palette.toneFor(brightness); + + final bool isLight = brightness == Brightness.light; + + final Color neutralTone00 = isLight + ? const Color(0xFFFFFFFF) + : const Color(0xFF0B0E14); + final Color neutralTone10 = isLight + ? const Color(0xFFF5F7FA) + : const Color(0xFF161B24); + final Color neutralTone20 = isLight + ? const Color(0xFFE6EAF1) + : const Color(0xFF1F2531); + final Color neutralTone40 = isLight + ? const Color(0xFFC5CCD9) + : const Color(0xFF343C4D); + final Color neutralTone60 = isLight + ? const Color(0xFF9099AC) + : const Color(0xFF4C566A); + final Color neutralTone80 = isLight + ? const Color(0xFF4A5161) + : const Color(0xFF8B95AA); + final Color neutralOnSurface = isLight + ? const Color(0xFF151920) + : const Color(0xFFE8ECF5); + + final Color overlayWeak = isLight + ? const Color.fromRGBO(21, 25, 32, 0.08) + : const Color.fromRGBO(232, 236, 245, 0.08); + final Color overlayMedium = isLight + ? const Color.fromRGBO(21, 25, 32, 0.16) + : const Color.fromRGBO(232, 236, 245, 0.16); + final Color overlayStrong = isLight + ? const Color.fromRGBO(21, 25, 32, 0.32) + : const Color.fromRGBO(232, 236, 245, 0.48); + + final Color codeBackground = isLight + ? const Color(0xFF0F172A) + : const Color(0xFF111828); + final Color codeBorder = isLight + ? const Color(0xFF1E293B) + : const Color(0xFF1F2937); + final Color codeText = const Color(0xFFE2E8F0); + final Color codeAccent = codeBorder; + + final ColorScheme seedScheme = ColorScheme.fromSeed( + seedColor: tone.primary, + brightness: brightness, + ); + + final Color brandTone60 = seedScheme.primary; + final Color brandOn60 = _preferredOnColor( + background: brandTone60, + light: neutralTone00, + dark: neutralOnSurface, + ); + + final Color brandTone90 = seedScheme.primaryContainer; + final Color brandOn90 = _preferredOnColor( + background: brandTone90, + light: neutralTone00, + dark: neutralOnSurface, + ); + + final double brandShift = isLight ? 0.18 : -0.14; + final Color brandTone40 = _shiftLightness(brandTone60, brandShift); + + final Color accentIndigo60 = tone.secondary; + final Color accentOnIndigo60 = _preferredOnColor( + background: accentIndigo60, + light: neutralTone00, + dark: neutralOnSurface, + ); + + final Color accentTeal60 = tone.accent; + final Color accentGold60 = isLight + ? const Color(0xFFFFB54A) + : const Color(0xFFFFC266); + + final Color statusSuccess60 = isLight + ? const Color(0xFF0E9D58) + : const Color(0xFF23C179); + final Color statusOnSuccess60 = _preferredOnColor( + background: statusSuccess60, + light: neutralTone00, + dark: neutralOnSurface, + ); + + final Color statusWarning60 = isLight + ? const Color(0xFFDB7900) + : const Color(0xFFFF9800); + final Color statusOnWarning60 = _preferredOnColor( + background: statusWarning60, + light: neutralTone00, + dark: neutralOnSurface, + ); + + final Color statusError60 = isLight + ? const Color(0xFFCE2C31) + : const Color(0xFFFF5F67); + final Color statusOnError60 = _preferredOnColor( + background: statusError60, + light: neutralTone00, + dark: neutralOnSurface, + ); + + final Color statusInfo60 = isLight + ? const Color(0xFF0174D3) + : const Color(0xFF4CA8FF); + final Color statusOnInfo60 = _preferredOnColor( + background: statusInfo60, + light: neutralTone00, + dark: neutralOnSurface, + ); + + return AppColorTokens( + brightness: brightness, + neutralTone00: neutralTone00, + neutralTone10: neutralTone10, + neutralTone20: neutralTone20, + neutralTone40: neutralTone40, + neutralTone60: neutralTone60, + neutralTone80: neutralTone80, + neutralOnSurface: neutralOnSurface, + brandTone40: brandTone40, + brandTone60: brandTone60, + brandOn60: brandOn60, + brandTone90: brandTone90, + brandOn90: brandOn90, + accentIndigo60: accentIndigo60, + accentOnIndigo60: accentOnIndigo60, + accentTeal60: accentTeal60, + accentGold60: accentGold60, + statusSuccess60: statusSuccess60, + statusOnSuccess60: statusOnSuccess60, + statusWarning60: statusWarning60, + statusOnWarning60: statusOnWarning60, + statusError60: statusError60, + statusOnError60: statusOnError60, + statusInfo60: statusInfo60, + statusOnInfo60: statusOnInfo60, + overlayWeak: overlayWeak, + overlayMedium: overlayMedium, + overlayStrong: overlayStrong, + codeBackground: codeBackground, + codeBorder: codeBorder, + codeText: codeText, + codeAccent: codeAccent, + ); + } + + @override + AppColorTokens copyWith({ + Brightness? brightness, + Color? neutralTone00, + Color? neutralTone10, + Color? neutralTone20, + Color? neutralTone40, + Color? neutralTone60, + Color? neutralTone80, + Color? neutralOnSurface, + Color? brandTone40, + Color? brandTone60, + Color? brandOn60, + Color? brandTone90, + Color? brandOn90, + Color? accentIndigo60, + Color? accentOnIndigo60, + Color? accentTeal60, + Color? accentGold60, + Color? statusSuccess60, + Color? statusOnSuccess60, + Color? statusWarning60, + Color? statusOnWarning60, + Color? statusError60, + Color? statusOnError60, + Color? statusInfo60, + Color? statusOnInfo60, + Color? overlayWeak, + Color? overlayMedium, + Color? overlayStrong, + Color? codeBackground, + Color? codeBorder, + Color? codeText, + Color? codeAccent, + }) { + return AppColorTokens( + brightness: brightness ?? this.brightness, + neutralTone00: neutralTone00 ?? this.neutralTone00, + neutralTone10: neutralTone10 ?? this.neutralTone10, + neutralTone20: neutralTone20 ?? this.neutralTone20, + neutralTone40: neutralTone40 ?? this.neutralTone40, + neutralTone60: neutralTone60 ?? this.neutralTone60, + neutralTone80: neutralTone80 ?? this.neutralTone80, + neutralOnSurface: neutralOnSurface ?? this.neutralOnSurface, + brandTone40: brandTone40 ?? this.brandTone40, + brandTone60: brandTone60 ?? this.brandTone60, + brandOn60: brandOn60 ?? this.brandOn60, + brandTone90: brandTone90 ?? this.brandTone90, + brandOn90: brandOn90 ?? this.brandOn90, + accentIndigo60: accentIndigo60 ?? this.accentIndigo60, + accentOnIndigo60: accentOnIndigo60 ?? this.accentOnIndigo60, + accentTeal60: accentTeal60 ?? this.accentTeal60, + accentGold60: accentGold60 ?? this.accentGold60, + statusSuccess60: statusSuccess60 ?? this.statusSuccess60, + statusOnSuccess60: statusOnSuccess60 ?? this.statusOnSuccess60, + statusWarning60: statusWarning60 ?? this.statusWarning60, + statusOnWarning60: statusOnWarning60 ?? this.statusOnWarning60, + statusError60: statusError60 ?? this.statusError60, + statusOnError60: statusOnError60 ?? this.statusOnError60, + statusInfo60: statusInfo60 ?? this.statusInfo60, + statusOnInfo60: statusOnInfo60 ?? this.statusOnInfo60, + overlayWeak: overlayWeak ?? this.overlayWeak, + overlayMedium: overlayMedium ?? this.overlayMedium, + overlayStrong: overlayStrong ?? this.overlayStrong, + codeBackground: codeBackground ?? this.codeBackground, + codeBorder: codeBorder ?? this.codeBorder, + codeText: codeText ?? this.codeText, + codeAccent: codeAccent ?? this.codeAccent, + ); + } + + @override + AppColorTokens lerp( + covariant ThemeExtension? other, + double t, + ) { + if (other is! AppColorTokens) { + return this; + } + + return AppColorTokens( + brightness: t < 0.5 ? brightness : other.brightness, + neutralTone00: Color.lerp(neutralTone00, other.neutralTone00, t)!, + neutralTone10: Color.lerp(neutralTone10, other.neutralTone10, t)!, + neutralTone20: Color.lerp(neutralTone20, other.neutralTone20, t)!, + neutralTone40: Color.lerp(neutralTone40, other.neutralTone40, t)!, + neutralTone60: Color.lerp(neutralTone60, other.neutralTone60, t)!, + neutralTone80: Color.lerp(neutralTone80, other.neutralTone80, t)!, + neutralOnSurface: Color.lerp( + neutralOnSurface, + other.neutralOnSurface, + t, + )!, + brandTone40: Color.lerp(brandTone40, other.brandTone40, t)!, + brandTone60: Color.lerp(brandTone60, other.brandTone60, t)!, + brandOn60: Color.lerp(brandOn60, other.brandOn60, t)!, + brandTone90: Color.lerp(brandTone90, other.brandTone90, t)!, + brandOn90: Color.lerp(brandOn90, other.brandOn90, t)!, + accentIndigo60: Color.lerp(accentIndigo60, other.accentIndigo60, t)!, + accentOnIndigo60: Color.lerp( + accentOnIndigo60, + other.accentOnIndigo60, + t, + )!, + accentTeal60: Color.lerp(accentTeal60, other.accentTeal60, t)!, + accentGold60: Color.lerp(accentGold60, other.accentGold60, t)!, + statusSuccess60: Color.lerp(statusSuccess60, other.statusSuccess60, t)!, + statusOnSuccess60: Color.lerp( + statusOnSuccess60, + other.statusOnSuccess60, + t, + )!, + statusWarning60: Color.lerp(statusWarning60, other.statusWarning60, t)!, + statusOnWarning60: Color.lerp( + statusOnWarning60, + other.statusOnWarning60, + t, + )!, + statusError60: Color.lerp(statusError60, other.statusError60, t)!, + statusOnError60: Color.lerp(statusOnError60, other.statusOnError60, t)!, + statusInfo60: Color.lerp(statusInfo60, other.statusInfo60, t)!, + statusOnInfo60: Color.lerp(statusOnInfo60, other.statusOnInfo60, t)!, + overlayWeak: Color.lerp(overlayWeak, other.overlayWeak, t)!, + overlayMedium: Color.lerp(overlayMedium, other.overlayMedium, t)!, + overlayStrong: Color.lerp(overlayStrong, other.overlayStrong, t)!, + codeBackground: Color.lerp(codeBackground, other.codeBackground, t)!, + codeBorder: Color.lerp(codeBorder, other.codeBorder, t)!, + codeText: Color.lerp(codeText, other.codeText, t)!, + codeAccent: Color.lerp(codeAccent, other.codeAccent, t)!, + ); + } + + /// Generates a Material [ColorScheme] that aligns with the defined tokens. + ColorScheme toColorScheme() { + final base = ColorScheme.fromSeed( + seedColor: brandTone60, + brightness: brightness, + ); + + return base.copyWith( + primary: brandTone60, + onPrimary: brandOn60, + primaryContainer: brandTone90, + onPrimaryContainer: brandOn90, + secondary: accentIndigo60, + onSecondary: accentOnIndigo60, + tertiary: accentTeal60, + onTertiary: neutralTone00, + surface: neutralTone00, + surfaceContainerLow: neutralTone10, + surfaceContainerHighest: neutralTone20, + onSurface: neutralOnSurface, + onSurfaceVariant: neutralTone80, + outline: neutralTone60, + outlineVariant: neutralTone40, + error: statusError60, + onError: statusOnError60, + surfaceTint: brandTone40, + scrim: overlayStrong, + ); + } + + /// Convenience helper to composite an overlay on top of the correct surface. + Color overlayOnSurface(Color overlay, {Color? surface}) { + final baseSurface = surface ?? neutralTone00; + return Color.alphaBlend(overlay, baseSurface); + } + + static AppColorTokens fallback({Brightness brightness = Brightness.light}) { + return brightness == Brightness.dark + ? AppColorTokens.dark() + : AppColorTokens.light(); + } + + static Color _shiftLightness(Color color, double amount) { + final HSLColor hsl = HSLColor.fromColor(color); + final double lightness = (hsl.lightness + amount).clamp(0.0, 1.0); + return hsl.withLightness(lightness).toColor(); + } + + static Color _preferredOnColor({ + required Color background, + required Color light, + required Color dark, + }) { + final double lightContrast = _contrastRatio(background, light); + final double darkContrast = _contrastRatio(background, dark); + return lightContrast >= darkContrast ? light : dark; + } + + static double _contrastRatio(Color a, Color b) { + final double luminanceA = a.computeLuminance(); + final double luminanceB = b.computeLuminance(); + final double lighter = math.max(luminanceA, luminanceB); + final double darker = math.min(luminanceA, luminanceB); + return (lighter + 0.05) / (darker + 0.05); + } +} diff --git a/lib/shared/theme/theme_extensions.dart b/lib/shared/theme/theme_extensions.dart index 5582bcf..763e8f7 100644 --- a/lib/shared/theme/theme_extensions.dart +++ b/lib/shared/theme/theme_extensions.dart @@ -2,8 +2,8 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; // Using system fonts; no GoogleFonts dependency required -import 'app_theme.dart'; import 'color_palettes.dart'; +import 'color_tokens.dart'; /// Extended theme data for consistent styling across the app @immutable @@ -62,6 +62,12 @@ class ConduitThemeExtension extends ThemeExtension { final Color shimmerHighlight; final Color loadingIndicator; + // Markdown/code colors + final Color codeBackground; + final Color codeBorder; + final Color codeText; + final Color codeAccent; + // Text colors final Color textPrimary; final Color textSecondary; @@ -141,6 +147,12 @@ class ConduitThemeExtension extends ThemeExtension { required this.shimmerHighlight, required this.loadingIndicator, + // Markdown/code colors + required this.codeBackground, + required this.codeBorder, + required this.codeText, + required this.codeAccent, + // Text colors required this.textPrimary, required this.textSecondary, @@ -222,6 +234,12 @@ class ConduitThemeExtension extends ThemeExtension { Color? shimmerHighlight, Color? loadingIndicator, + // Markdown/code colors + Color? codeBackground, + Color? codeBorder, + Color? codeText, + Color? codeAccent, + // Text colors Color? textPrimary, Color? textSecondary, @@ -305,6 +323,12 @@ class ConduitThemeExtension extends ThemeExtension { shimmerHighlight: shimmerHighlight ?? this.shimmerHighlight, loadingIndicator: loadingIndicator ?? this.loadingIndicator, + // Markdown/code colors + codeBackground: codeBackground ?? this.codeBackground, + codeBorder: codeBorder ?? this.codeBorder, + codeText: codeText ?? this.codeText, + codeAccent: codeAccent ?? this.codeAccent, + // Text colors textPrimary: textPrimary ?? this.textPrimary, textSecondary: textSecondary ?? this.textSecondary, @@ -477,6 +501,10 @@ class ConduitThemeExtension extends ThemeExtension { other.loadingIndicator, t, )!, + codeBackground: Color.lerp(codeBackground, other.codeBackground, t)!, + codeBorder: Color.lerp(codeBorder, other.codeBorder, t)!, + codeText: Color.lerp(codeText, other.codeText, t)!, + codeAccent: Color.lerp(codeAccent, other.codeAccent, t)!, // Text colors textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!, @@ -505,116 +533,136 @@ class ConduitThemeExtension extends ThemeExtension { } /// Dark theme extension derived from the active color palette. - static ConduitThemeExtension darkPalette(AppColorPalette palette) { + static ConduitThemeExtension darkPalette({ + required AppColorPalette palette, + required AppColorTokens tokens, + }) { final darkTone = palette.dark; - final onDarkPrimary = _onSurfaceColor(darkTone.primary); + final onPrimary = _onSurfaceColor(darkTone.primary, tokens); + Color blend(Color overlay, {Color? surface}) { + return Color.alphaBlend(overlay, surface ?? tokens.neutralTone10); + } + + Color toneBackground(Color tone, {double opacity = 0.24}) { + return Color.alphaBlend( + tone.withValues(alpha: opacity), + tokens.neutralTone10, + ); + } + return ConduitThemeExtension( chatBubbleUser: darkTone.primary, - chatBubbleAssistant: const Color(0xFF0E1010), - chatBubbleUserText: onDarkPrimary, - chatBubbleAssistantText: AppTheme.neutral50, + chatBubbleAssistant: tokens.neutralTone20, + chatBubbleUserText: onPrimary, + chatBubbleAssistantText: tokens.neutralOnSurface, chatBubbleUserBorder: darkTone.secondary, - chatBubbleAssistantBorder: const Color(0xFF1A1D1C), - inputBackground: const Color(0xFF141615), - inputBorder: AppTheme.neutral600, + chatBubbleAssistantBorder: tokens.neutralTone40, + inputBackground: tokens.neutralTone20, + inputBorder: tokens.neutralTone40, 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), + inputText: tokens.neutralOnSurface, + inputPlaceholder: tokens.neutralTone80, + inputError: tokens.statusError60, + cardBackground: tokens.neutralTone00, + cardBorder: tokens.neutralTone40, + cardShadow: blend(tokens.overlayWeak, surface: tokens.neutralTone00), + surfaceBackground: tokens.neutralTone10, + surfaceContainer: tokens.neutralTone00, + surfaceContainerHighest: tokens.neutralTone20, buttonPrimary: darkTone.primary, - buttonPrimaryText: onDarkPrimary, - 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), + buttonPrimaryText: onPrimary, + buttonSecondary: tokens.neutralTone20, + buttonSecondaryText: tokens.neutralOnSurface, + buttonDisabled: tokens.neutralTone40, + buttonDisabledText: tokens.neutralTone80, + success: tokens.statusSuccess60, + successBackground: toneBackground(tokens.statusSuccess60), + error: tokens.statusError60, + errorBackground: toneBackground(tokens.statusError60), + warning: tokens.statusWarning60, + warningBackground: toneBackground(tokens.statusWarning60), + info: tokens.statusInfo60, + infoBackground: toneBackground(tokens.statusInfo60), + dividerColor: tokens.neutralTone40, + navigationBackground: tokens.neutralTone10, navigationSelected: darkTone.primary, - navigationUnselected: AppTheme.neutral300, - navigationSelectedBackground: _surfaceTint( - darkTone.primary, - const Color(0xFF0A0D0C), - 0.24, + navigationUnselected: tokens.neutralTone80, + navigationSelectedBackground: blend( + tokens.overlayMedium, + surface: tokens.neutralTone10, + ), + shimmerBase: blend(tokens.overlayWeak, surface: tokens.neutralTone10), + shimmerHighlight: blend( + tokens.overlayMedium, + surface: tokens.neutralTone20, ), - 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, + codeBackground: tokens.codeBackground, + codeBorder: tokens.codeBorder, + codeText: tokens.codeText, + codeAccent: tokens.codeAccent, + textPrimary: tokens.neutralOnSurface, + textSecondary: tokens.neutralTone80, + textTertiary: tokens.neutralTone60, + textInverse: tokens.neutralTone00, + textDisabled: tokens.neutralTone40, + iconPrimary: tokens.neutralOnSurface, + iconSecondary: tokens.neutralTone80, + iconDisabled: tokens.neutralTone40, + iconInverse: tokens.neutralTone00, headingLarge: TextStyle( fontSize: AppTypography.displaySmall, fontWeight: FontWeight.w700, - color: AppTheme.neutral50, + color: tokens.neutralOnSurface, height: 1.2, ), headingMedium: TextStyle( fontSize: AppTypography.headlineLarge, fontWeight: FontWeight.w600, - color: AppTheme.neutral50, + color: tokens.neutralOnSurface, height: 1.3, ), headingSmall: TextStyle( fontSize: AppTypography.headlineSmall, fontWeight: FontWeight.w600, - color: AppTheme.neutral50, + color: tokens.neutralOnSurface, height: 1.4, ), bodyLarge: TextStyle( fontSize: AppTypography.bodyLarge, fontWeight: FontWeight.w400, - color: AppTheme.neutral50, + color: tokens.neutralOnSurface, height: 1.5, ), bodyMedium: TextStyle( fontSize: AppTypography.bodyMedium, fontWeight: FontWeight.w400, - color: AppTheme.neutral50, + color: tokens.neutralOnSurface, height: 1.5, ), bodySmall: TextStyle( fontSize: AppTypography.bodySmall, fontWeight: FontWeight.w400, - color: const Color(0xFFD1D5DB), + color: tokens.neutralTone80, height: 1.4, ), caption: TextStyle( fontSize: AppTypography.labelMedium, fontWeight: FontWeight.w500, - color: AppTheme.neutral300, + color: tokens.neutralTone80, height: 1.3, letterSpacing: 0.5, ), label: TextStyle( fontSize: AppTypography.labelLarge, fontWeight: FontWeight.w500, - color: const Color(0xFFD1D5DB), + color: tokens.neutralOnSurface, height: 1.3, ), code: TextStyle( fontSize: AppTypography.bodySmall, fontWeight: FontWeight.w400, - color: const Color(0xFFD1D5DB), + color: tokens.neutralOnSurface, height: 1.4, fontFamily: AppTypography.monospaceFontFamily, ), @@ -622,133 +670,143 @@ class ConduitThemeExtension extends ThemeExtension { } /// Light theme extension derived from the active color palette. - static ConduitThemeExtension lightPalette(AppColorPalette palette) { + static ConduitThemeExtension lightPalette({ + required AppColorPalette palette, + required AppColorTokens tokens, + }) { final lightTone = palette.light; final darkTone = palette.dark; - final onLightPrimary = _onSurfaceColor(lightTone.primary); + final onPrimary = _onSurfaceColor(lightTone.primary, tokens); + Color blend(Color overlay, {Color? surface}) { + return Color.alphaBlend(overlay, surface ?? tokens.neutralTone00); + } + + Color toneBackground(Color tone, {double opacity = 0.12}) { + return Color.alphaBlend( + tone.withValues(alpha: opacity), + tokens.neutralTone00, + ); + } + return ConduitThemeExtension( chatBubbleUser: lightTone.primary, - chatBubbleAssistant: const Color(0xFFF7F7F7), - chatBubbleUserText: onLightPrimary, - chatBubbleAssistantText: const Color(0xFF1C1C1C), + chatBubbleAssistant: tokens.neutralTone00, + chatBubbleUserText: onPrimary, + chatBubbleAssistantText: tokens.neutralOnSurface, chatBubbleUserBorder: darkTone.primary, - chatBubbleAssistantBorder: const Color(0xFFE7E7E7), - inputBackground: AppTheme.neutral50, - inputBorder: AppTheme.neutral200, + chatBubbleAssistantBorder: tokens.neutralTone20, + inputBackground: tokens.neutralTone00, + inputBorder: tokens.neutralTone20, 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), + inputText: tokens.neutralOnSurface, + inputPlaceholder: tokens.neutralTone60, + inputError: tokens.statusError60, + cardBackground: tokens.neutralTone00, + cardBorder: tokens.neutralTone20, + cardShadow: blend(tokens.overlayWeak), + surfaceBackground: tokens.neutralTone10, + surfaceContainer: tokens.neutralTone00, + surfaceContainerHighest: tokens.neutralTone20, buttonPrimary: lightTone.primary, - buttonPrimaryText: onLightPrimary, - 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, + buttonPrimaryText: onPrimary, + buttonSecondary: tokens.neutralTone20, + buttonSecondaryText: tokens.neutralOnSurface, + buttonDisabled: tokens.neutralTone40, + buttonDisabledText: tokens.neutralTone60, + success: tokens.statusSuccess60, + successBackground: toneBackground(tokens.statusSuccess60), + error: tokens.statusError60, + errorBackground: toneBackground(tokens.statusError60), + warning: tokens.statusWarning60, + warningBackground: toneBackground(tokens.statusWarning60), + info: tokens.statusInfo60, + infoBackground: toneBackground(tokens.statusInfo60), + dividerColor: tokens.neutralTone20, + navigationBackground: tokens.neutralTone00, navigationSelected: lightTone.primary, - navigationUnselected: AppTheme.neutral600, - navigationSelectedBackground: _surfaceTint( - lightTone.primary, - AppTheme.neutral50, - 0.16, - ), - shimmerBase: const Color(0xFFF3F4F6), - shimmerHighlight: AppTheme.neutral50, + navigationUnselected: tokens.neutralTone60, + navigationSelectedBackground: blend(tokens.overlayMedium), + shimmerBase: blend(tokens.overlayWeak, surface: tokens.neutralTone10), + shimmerHighlight: tokens.neutralTone00, 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, + codeBackground: tokens.codeBackground, + codeBorder: tokens.codeBorder, + codeText: tokens.codeText, + codeAccent: tokens.codeAccent, + textPrimary: tokens.neutralOnSurface, + textSecondary: tokens.neutralTone80, + textTertiary: tokens.neutralTone60, + textInverse: tokens.neutralTone00, + textDisabled: tokens.neutralTone60, + iconPrimary: tokens.neutralOnSurface, + iconSecondary: tokens.neutralTone80, + iconDisabled: tokens.neutralTone60, + iconInverse: tokens.neutralTone00, headingLarge: TextStyle( fontSize: AppTypography.displaySmall, fontWeight: FontWeight.w700, - color: const Color(0xFF111827), + color: tokens.neutralOnSurface, height: 1.2, ), headingMedium: TextStyle( fontSize: AppTypography.headlineLarge, fontWeight: FontWeight.w600, - color: const Color(0xFF111827), + color: tokens.neutralOnSurface, height: 1.3, ), headingSmall: TextStyle( fontSize: AppTypography.headlineSmall, fontWeight: FontWeight.w600, - color: const Color(0xFF111827), + color: tokens.neutralOnSurface, height: 1.4, ), bodyLarge: TextStyle( fontSize: AppTypography.bodyLarge, fontWeight: FontWeight.w400, - color: const Color(0xFF111827), + color: tokens.neutralOnSurface, height: 1.5, ), bodyMedium: TextStyle( fontSize: AppTypography.bodyMedium, fontWeight: FontWeight.w400, - color: const Color(0xFF111827), + color: tokens.neutralOnSurface, height: 1.5, ), bodySmall: TextStyle( fontSize: AppTypography.bodySmall, fontWeight: FontWeight.w400, - color: AppTheme.neutral500, + color: tokens.neutralTone60, height: 1.4, ), caption: TextStyle( fontSize: AppTypography.labelMedium, fontWeight: FontWeight.w500, - color: AppTheme.neutral400, + color: tokens.neutralTone60, height: 1.3, letterSpacing: 0.5, ), label: TextStyle( fontSize: AppTypography.labelLarge, fontWeight: FontWeight.w500, - color: const Color(0xFF444948), + color: tokens.neutralTone80, height: 1.3, ), code: TextStyle( fontSize: AppTypography.bodySmall, fontWeight: FontWeight.w400, - color: const Color(0xFF1C1C1C), + color: tokens.neutralOnSurface, height: 1.4, fontFamily: AppTypography.monospaceFontFamily, ), ); } - static Color _surfaceTint(Color tone, Color surface, double opacity) { - return Color.alphaBlend(tone.withValues(alpha: opacity), surface); - } - - static Color _onSurfaceColor(Color background) { - final contrastOnLight = _contrastRatio(background, AppTheme.neutral50); - final contrastOnDark = _contrastRatio(background, AppTheme.neutral900); + static Color _onSurfaceColor(Color background, AppColorTokens tokens) { + final contrastOnLight = _contrastRatio(background, tokens.neutralTone00); + final contrastOnDark = _contrastRatio(background, tokens.neutralOnSurface); return contrastOnLight >= contrastOnDark - ? AppTheme.neutral50 - : AppTheme.neutral900; + ? tokens.neutralTone00 + : tokens.neutralOnSurface; } static double _contrastRatio(Color a, Color b) { @@ -769,9 +827,26 @@ extension ConduitThemeContext on BuildContext { final palette = theme.extension()?.palette ?? AppColorPalettes.auroraViolet; + final tokens = theme.brightness == Brightness.dark + ? AppColorTokens.dark(palette: palette) + : AppColorTokens.light(palette: palette); return theme.brightness == Brightness.dark - ? ConduitThemeExtension.darkPalette(palette) - : ConduitThemeExtension.lightPalette(palette); + ? ConduitThemeExtension.darkPalette(palette: palette, tokens: tokens) + : ConduitThemeExtension.lightPalette(palette: palette, tokens: tokens); + } +} + +extension ConduitColorTokensContext on BuildContext { + AppColorTokens get colorTokens { + final theme = Theme.of(this); + final tokens = theme.extension(); + if (tokens != null) return tokens; + final palette = + theme.extension()?.palette ?? + AppColorPalettes.auroraViolet; + return theme.brightness == Brightness.dark + ? AppColorTokens.dark(palette: palette) + : AppColorTokens.light(palette: palette); } } @@ -923,182 +998,153 @@ class Elevation { /// Helper class for consistent shadows - Enhanced for production with better hierarchy class ConduitShadows { - static List get low => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.08), - blurRadius: 8, - offset: const Offset(0, 2), - spreadRadius: 0, - ), - ]; + static List low(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.08, + blurRadius: 8, + offset: const Offset(0, 2), + ); - static List get medium => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.12), - blurRadius: 16, - offset: const Offset(0, 4), - spreadRadius: 0, - ), - ]; + static List medium(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.12, + blurRadius: 16, + offset: const Offset(0, 4), + ); - static List get high => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.16), - blurRadius: 24, - offset: const Offset(0, 8), - spreadRadius: 0, - ), - ]; + static List high(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.16, + blurRadius: 24, + offset: const Offset(0, 8), + ); - static List get glow => [ - BoxShadow( - color: AppColorPalettes.auroraViolet.light.primary.withValues( - alpha: 0.25, + static List glow(BuildContext context) => + glowWithTokens(context.colorTokens); + + static List glowWithTokens(AppColorTokens tokens) { + final double alpha = tokens.brightness == Brightness.light ? 0.25 : 0.35; + return [ + BoxShadow( + color: tokens.brandTone60.withValues(alpha: alpha), + blurRadius: 20, + offset: const Offset(0, 0), + spreadRadius: 0, ), - blurRadius: 20, - offset: const Offset(0, 0), - spreadRadius: 0, - ), - ]; + ]; + } - // Enhanced shadows for specific components with better hierarchy - static List get card => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.06), - blurRadius: 12, - offset: const Offset(0, 3), - spreadRadius: 0, - ), - ]; + static List card(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.06, + blurRadius: 12, + offset: const Offset(0, 3), + ); - static List get button => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.1), - blurRadius: 6, - offset: const Offset(0, 2), - spreadRadius: 0, - ), - ]; + static List button(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.1, + blurRadius: 6, + offset: const Offset(0, 2), + ); - static List get modal => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.2), - blurRadius: 32, - offset: const Offset(0, 12), - spreadRadius: 0, - ), - ]; + static List modal(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.2, + blurRadius: 32, + offset: const Offset(0, 12), + ); - static List get navigation => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.08), - blurRadius: 16, - offset: const Offset(0, -2), - spreadRadius: 0, - ), - ]; + static List navigation(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.08, + blurRadius: 16, + offset: const Offset(0, -2), + ); - static List get messageBubble => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.04), - blurRadius: 8, - offset: const Offset(0, 1), - spreadRadius: 0, - ), - ]; + static List messageBubble(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.04, + blurRadius: 8, + offset: const Offset(0, 1), + ); - static List get input => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.05), - blurRadius: 4, - offset: const Offset(0, 1), - spreadRadius: 0, - ), - ]; + static List input(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.05, + blurRadius: 4, + offset: const Offset(0, 1), + ); - // Dark theme specific shadows with better contrast - static List get darkCard => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.3), - blurRadius: 16, - offset: const Offset(0, 4), - spreadRadius: 0, - ), - ]; + static List pressed(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.15, + blurRadius: 4, + offset: const Offset(0, 1), + ); - static List get darkModal => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.4), - blurRadius: 40, - offset: const Offset(0, 16), - spreadRadius: 0, - ), - ]; + static List hover(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.12, + blurRadius: 12, + offset: const Offset(0, 4), + ); - // Interactive shadows with better feedback - static List get pressed => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.15), - blurRadius: 4, - offset: const Offset(0, 1), - spreadRadius: 0, - ), - ]; + static List micro(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.04, + blurRadius: 4, + offset: const Offset(0, 1), + ); - static List get hover => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.12), - blurRadius: 12, - offset: const Offset(0, 4), - spreadRadius: 0, - ), - ]; + static List small(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.06, + blurRadius: 8, + offset: const Offset(0, 2), + ); - // Enhanced shadows for better visual hierarchy - static List get micro => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.04), - blurRadius: 4, - offset: const Offset(0, 1), - spreadRadius: 0, - ), - ]; + static List standard(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.08, + blurRadius: 12, + offset: const Offset(0, 3), + ); - static List get small => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.06), - blurRadius: 8, - offset: const Offset(0, 2), - spreadRadius: 0, - ), - ]; + static List large(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.12, + blurRadius: 16, + offset: const Offset(0, 4), + ); - static List get standard => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.08), - blurRadius: 12, - offset: const Offset(0, 3), - spreadRadius: 0, - ), - ]; + static List extraLarge(BuildContext context) => _shadow( + context.colorTokens, + opacity: 0.16, + blurRadius: 24, + offset: const Offset(0, 8), + ); - static List get large => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.12), - blurRadius: 16, - offset: const Offset(0, 4), - spreadRadius: 0, - ), - ]; + static List _shadow( + AppColorTokens tokens, { + required double opacity, + required double blurRadius, + required Offset offset, + }) { + return [ + BoxShadow( + color: _overlayColor(tokens, opacity), + blurRadius: blurRadius, + offset: offset, + spreadRadius: 0, + ), + ]; + } - static List get extraLarge => [ - BoxShadow( - color: AppTheme.neutral900.withValues(alpha: 0.16), - blurRadius: 24, - offset: const Offset(0, 8), - spreadRadius: 0, - ), - ]; + static Color _overlayColor(AppColorTokens tokens, double alpha) { + final Color base = tokens.overlayStrong.withValues(alpha: 1.0); + return base.withValues(alpha: alpha.clamp(0.0, 1.0)); + } } /// Typography scale following Conduit design tokens - Enhanced for production diff --git a/lib/shared/utils/conversation_context_menu.dart b/lib/shared/utils/conversation_context_menu.dart index 9fdb2fb..8bdd5c1 100644 --- a/lib/shared/utils/conversation_context_menu.dart +++ b/lib/shared/utils/conversation_context_menu.dart @@ -84,7 +84,7 @@ Future showConduitContextMenu({ decoration: BoxDecoration( color: theme.surfaceBackground, borderRadius: BorderRadius.circular(AppBorderRadius.lg), - boxShadow: ConduitShadows.modal, + boxShadow: ConduitShadows.modal(context), ), child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/shared/widgets/conduit_components.dart b/lib/shared/widgets/conduit_components.dart index 02cab0d..0032a37 100644 --- a/lib/shared/widgets/conduit_components.dart +++ b/lib/shared/widgets/conduit_components.dart @@ -326,7 +326,7 @@ class ConduitCard extends StatelessWidget { : context.conduitTheme.cardBorder, width: BorderWidth.standard, ), - boxShadow: isElevated ? ConduitShadows.card : null, + boxShadow: isElevated ? ConduitShadows.card(context) : null, ), child: child, ), diff --git a/lib/shared/widgets/improved_loading_states.dart b/lib/shared/widgets/improved_loading_states.dart index 734ec67..c0c52df 100644 --- a/lib/shared/widgets/improved_loading_states.dart +++ b/lib/shared/widgets/improved_loading_states.dart @@ -297,7 +297,7 @@ class LoadingOverlay extends StatelessWidget { decoration: BoxDecoration( color: context.conduitTheme.cardBackground, borderRadius: BorderRadius.circular(AppBorderRadius.card), - boxShadow: ConduitShadows.card, + boxShadow: ConduitShadows.card(context), ), child: ImprovedLoadingState( message: message, diff --git a/lib/shared/widgets/loading_states.dart b/lib/shared/widgets/loading_states.dart index f7270fb..4005083 100644 --- a/lib/shared/widgets/loading_states.dart +++ b/lib/shared/widgets/loading_states.dart @@ -5,7 +5,7 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:io' show Platform; import '../services/brand_service.dart'; -import '../theme/app_theme.dart'; +import '../theme/color_tokens.dart'; import 'package:conduit/l10n/app_localizations.dart'; /// Standard loading indicators following Conduit design patterns @@ -52,13 +52,14 @@ class ConduitLoading { Color? color, BuildContext? context, }) { + final tokens = context?.colorTokens ?? AppColorTokens.fallback(); return _LoadingIndicator( size: size, color: color ?? (context?.conduitTheme.buttonPrimaryText ?? context?.conduitTheme.textPrimary ?? - AppTheme.neutral50), + tokens.neutralTone00), type: _LoadingType.button, ); } @@ -175,7 +176,7 @@ class _LoadingOverlay extends StatelessWidget { ? context.conduitTheme.surfaceBackground : context.conduitTheme.surfaceBackground, borderRadius: BorderRadius.circular(AppBorderRadius.lg), - boxShadow: ConduitShadows.high, + boxShadow: ConduitShadows.high(context), ), child: ConduitLoading.primary( size: IconSize.xl, diff --git a/lib/shared/widgets/markdown/markdown_config.dart b/lib/shared/widgets/markdown/markdown_config.dart index b8a23d7..3c77f92 100644 --- a/lib/shared/widgets/markdown/markdown_config.dart +++ b/lib/shared/widgets/markdown/markdown_config.dart @@ -16,6 +16,7 @@ import 'package:webview_flutter/webview_flutter.dart'; import 'package:conduit/l10n/app_localizations.dart'; import '../../theme/theme_extensions.dart'; +import '../../theme/color_tokens.dart'; class MarkdownFeatureFlags { const MarkdownFeatureFlags({ @@ -172,7 +173,7 @@ class ConduitMarkdownConfig { required bool enableHighlight, }) { final textStyle = AppTypography.codeStyle.copyWith( - color: const Color(0xFFE2E8F0), + color: conduitTheme.codeText, height: 1.55, fontSize: 13, ); @@ -212,6 +213,7 @@ class ConduitMarkdownConfig { required ThemeData materialTheme, required String code, }) { + final tokens = context.colorTokens; return SizedBox( height: 360, width: double.infinity, @@ -221,6 +223,7 @@ class ConduitMarkdownConfig { code: code, brightness: materialTheme.brightness, colorScheme: materialTheme.colorScheme, + tokens: tokens, ), ), ); @@ -232,7 +235,7 @@ class ConduitMarkdownConfig { required String code, }) { final textStyle = AppTypography.bodySmallStyle.copyWith( - color: Colors.white.withValues(alpha: 0.7), + color: conduitTheme.codeText.withValues(alpha: 0.7), ); return Column( @@ -468,6 +471,7 @@ class _CodeBlockWrapperState extends State { @override Widget build(BuildContext context) { + final conduitTheme = widget.theme; final canCopy = widget.closed && widget.code.trim().isNotEmpty; final icon = _copied ? Icons.check @@ -475,9 +479,9 @@ class _CodeBlockWrapperState extends State { ? Icons.copy : Icons.hourglass_empty; - const background = Color(0xFF0F172A); - final borderColor = const Color(0xFF1E293B).withValues(alpha: 0.6); - final headerColor = const Color(0xFF1E293B).withValues(alpha: 0.85); + final background = conduitTheme.codeBackground; + final borderColor = conduitTheme.codeBorder.withValues(alpha: 0.6); + final headerColor = conduitTheme.codeAccent.withValues(alpha: 0.85); final languageLabel = (widget.language?.isNotEmpty ?? false) ? widget.language! @@ -488,13 +492,7 @@ class _CodeBlockWrapperState extends State { margin: const EdgeInsets.symmetric(vertical: Spacing.xs), decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppBorderRadius.md), - boxShadow: const [ - BoxShadow( - color: Color(0x33000000), - blurRadius: 14, - offset: Offset(0, 10), - ), - ], + boxShadow: ConduitShadows.medium(context), border: Border.all(color: borderColor, width: BorderWidth.micro), ), child: ClipRRect( @@ -514,7 +512,7 @@ class _CodeBlockWrapperState extends State { Text( languageLabel, style: AppTypography.bodySmallStyle.copyWith( - color: Colors.white.withValues(alpha: 0.85), + color: conduitTheme.codeText.withValues(alpha: 0.85), fontFamily: AppTypography.monospaceFontFamily, ), ), @@ -531,17 +529,16 @@ class _CodeBlockWrapperState extends State { onPressed: canCopy ? _handleCopy : null, icon: Icon(icon, size: IconSize.sm), color: canCopy - ? Colors.white - : Colors.white.withValues(alpha: 0.5), + ? conduitTheme.codeText + : conduitTheme.codeText.withValues(alpha: 0.5), visualDensity: VisualDensity.compact, padding: const EdgeInsets.all(Spacing.xs), style: IconButton.styleFrom( - backgroundColor: Colors.white.withValues( + backgroundColor: conduitTheme.codeText.withValues( alpha: canCopy ? 0.08 : 0.04, ), - disabledBackgroundColor: Colors.white.withValues( - alpha: 0.03, - ), + disabledBackgroundColor: conduitTheme.codeText + .withValues(alpha: 0.03), ), ), ), @@ -553,7 +550,7 @@ class _CodeBlockWrapperState extends State { padding: const EdgeInsets.all(Spacing.sm), child: DefaultTextStyle.merge( style: AppTypography.codeStyle.copyWith( - color: const Color(0xFFE2E8F0), + color: conduitTheme.codeText, ), child: widget.child, ), @@ -571,11 +568,13 @@ class MermaidDiagram extends StatefulWidget { required this.code, required this.brightness, required this.colorScheme, + required this.tokens, }); final String code; final Brightness brightness; final ColorScheme colorScheme; + final AppColorTokens tokens; static bool get isSupported => !kIsWeb; @@ -625,7 +624,8 @@ class _MermaidDiagramState extends State { final codeChanged = oldWidget.code != widget.code; final themeChanged = oldWidget.brightness != widget.brightness || - oldWidget.colorScheme != widget.colorScheme; + oldWidget.colorScheme != widget.colorScheme || + oldWidget.tokens != widget.tokens; if (codeChanged || themeChanged) { _loadHtml(); } @@ -654,10 +654,12 @@ class _MermaidDiagramState extends State { String _buildHtml(String code, String script) { final theme = widget.brightness == Brightness.dark ? 'dark' : 'default'; final encoded = jsonEncode(code); - final primary = _toHex(widget.colorScheme.primary); - final secondary = _toHex(widget.colorScheme.secondary); - final background = _toHex(widget.colorScheme.surface); - final onBackground = _toHex(widget.colorScheme.onSurface); + final primary = _toHex(widget.tokens.brandTone60); + final secondary = _toHex(widget.tokens.accentTeal60); + final background = _toHex(widget.tokens.codeBackground); + final onBackground = _toHex(widget.tokens.codeText); + final lineColor = _toHex(widget.tokens.codeAccent); + final errorColor = _toHex(widget.tokens.statusError60); return ''' @@ -688,7 +690,7 @@ $script secondaryColor: '$secondary', background: '$background', textColor: '$onBackground', - lineColor: '$onBackground' + lineColor: '$lineColor' } }; @@ -702,7 +704,7 @@ $script bindFunctions(target); } } catch (error) { - target.innerHTML = '
' + String(error) + '
'; + target.innerHTML = '
' + String(error) + '
'; console.error('Mermaid render failed', error); } })(); diff --git a/lib/shared/widgets/offline_indicator.dart b/lib/shared/widgets/offline_indicator.dart index b2a716b..48b68ba 100644 --- a/lib/shared/widgets/offline_indicator.dart +++ b/lib/shared/widgets/offline_indicator.dart @@ -105,7 +105,7 @@ class _BackOnlineToast extends StatelessWidget { borderRadius: BorderRadius.circular( AppBorderRadius.round, ), - boxShadow: ConduitShadows.low, + boxShadow: ConduitShadows.low(context), ), child: Text( // Reuse existing l10n; otherwise add a dedicated "Back online" key later