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.
This commit is contained in:
cogwheel0
2025-10-03 00:12:25 +05:30
parent ad3834b43e
commit 1ea85d5ed1
21 changed files with 1009 additions and 454 deletions

View File

@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'api_error.dart'; import 'api_error.dart';
import 'api_error_handler.dart'; import 'api_error_handler.dart';
import 'api_error_interceptor.dart'; import 'api_error_interceptor.dart';
import '../../shared/theme/app_theme.dart';
import '../../shared/theme/theme_extensions.dart'; import '../../shared/theme/theme_extensions.dart';
import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/l10n/app_localizations.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
@@ -132,7 +131,7 @@ class EnhancedErrorService {
], ],
], ],
), ),
backgroundColor: _getErrorColor(error), backgroundColor: _getErrorColor(context, error),
duration: duration ?? _getSnackbarDuration(error), duration: duration ?? _getSnackbarDuration(error),
action: isRetryableError && onRetry != null action: isRetryableError && onRetry != null
? SnackBarAction( ? SnackBarAction(
@@ -169,7 +168,7 @@ class EnhancedErrorService {
return AlertDialog( return AlertDialog(
title: Row( title: Row(
children: [ children: [
Icon(_getErrorIcon(error), color: _getErrorColor(error)), Icon(_getErrorIcon(error), color: _getErrorColor(context, error)),
const SizedBox(width: Spacing.sm), const SizedBox(width: Spacing.sm),
Expanded(child: Text(title ?? _getErrorTitle(error))), Expanded(child: Text(title ?? _getErrorTitle(error))),
], ],
@@ -250,7 +249,7 @@ class EnhancedErrorService {
Icon( Icon(
_getErrorIcon(error), _getErrorIcon(error),
size: IconSize.xxl, size: IconSize.xxl,
color: _getErrorColor(error), color: _getErrorColor(context, error),
), ),
const SizedBox(height: Spacing.md), const SizedBox(height: Spacing.md),
Text( Text(
@@ -416,27 +415,28 @@ class EnhancedErrorService {
return Icons.error_outline; return Icons.error_outline;
} }
Color _getErrorColor(dynamic error) { Color _getErrorColor(BuildContext context, dynamic error) {
final tokens = context.colorTokens;
if (error is ApiError) { if (error is ApiError) {
switch (error.type) { switch (error.type) {
case ApiErrorType.network: case ApiErrorType.network:
case ApiErrorType.timeout: case ApiErrorType.timeout:
return AppTheme.warning; return tokens.statusWarning60;
case ApiErrorType.authentication: case ApiErrorType.authentication:
case ApiErrorType.authorization: case ApiErrorType.authorization:
return AppTheme.error; return tokens.statusError60;
case ApiErrorType.validation: case ApiErrorType.validation:
case ApiErrorType.badRequest: case ApiErrorType.badRequest:
return AppTheme.warning; return tokens.statusWarning60;
case ApiErrorType.server: case ApiErrorType.server:
return AppTheme.error; return tokens.statusError60;
case ApiErrorType.rateLimit: case ApiErrorType.rateLimit:
return AppTheme.info; return tokens.statusInfo60;
default: default:
return AppTheme.error; return tokens.statusError60;
} }
} }
return AppTheme.error; return tokens.statusError60;
} }
String _getErrorTitle(dynamic error) { String _getErrorTitle(dynamic error) {

View File

@@ -535,7 +535,7 @@ Future<void> _maybeShowOnboarding(Ref ref) async {
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.modal), top: Radius.circular(AppBorderRadius.modal),
), ),
boxShadow: ConduitShadows.modal, boxShadow: ConduitShadows.modal(context),
), ),
child: const OnboardingSheet(), child: const OnboardingSheet(),
), ),

View File

@@ -116,13 +116,7 @@ class _ConnectionIssuePageState extends ConsumerState<ConnectionIssuePage> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.conduitTheme.surfaceContainerHighest, color: context.conduitTheme.surfaceContainerHighest,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: ConduitShadows.high(context),
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 18,
offset: const Offset(0, 12),
),
],
), ),
child: Icon( child: Icon(
Platform.isIOS Platform.isIOS

View File

@@ -214,7 +214,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.modal), top: Radius.circular(AppBorderRadius.modal),
), ),
boxShadow: ConduitShadows.modal, boxShadow: ConduitShadows.modal(context),
), ),
child: const OnboardingSheet(), child: const OnboardingSheet(),
), ),
@@ -735,7 +735,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
color: context.conduitTheme.cardBorder, color: context.conduitTheme.cardBorder,
width: BorderWidth.regular, width: BorderWidth.regular,
), ),
boxShadow: ConduitShadows.messageBubble, boxShadow: ConduitShadows.messageBubble(context),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -1202,7 +1202,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
drawerEnableOpenDragGesture: true, drawerEnableOpenDragGesture: true,
drawerDragStartBehavior: DragStartBehavior.start, drawerDragStartBehavior: DragStartBehavior.start,
drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.75, drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.75,
drawerScrimColor: Colors.black.withValues(alpha: 0.32), drawerScrimColor: context.colorTokens.overlayStrong,
drawer: Drawer( drawer: Drawer(
width: (MediaQuery.of(context).size.width * 0.80).clamp( width: (MediaQuery.of(context).size.width * 0.80).clamp(
280.0, 280.0,
@@ -1638,7 +1638,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
AppBorderRadius.floatingButton, AppBorderRadius.floatingButton,
), ),
boxShadow: ConduitShadows.button, boxShadow: ConduitShadows.button(context),
), ),
child: SizedBox( child: SizedBox(
width: TouchTarget.button, width: TouchTarget.button,
@@ -1838,7 +1838,7 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
color: context.conduitTheme.dividerColor, color: context.conduitTheme.dividerColor,
width: BorderWidth.regular, width: BorderWidth.regular,
), ),
boxShadow: ConduitShadows.modal, boxShadow: ConduitShadows.modal(context),
), ),
child: ModalSheetSafeArea( child: ModalSheetSafeArea(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -2043,7 +2043,7 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
: context.conduitTheme.dividerColor, : context.conduitTheme.dividerColor,
width: BorderWidth.regular, width: BorderWidth.regular,
), ),
boxShadow: isSelected ? ConduitShadows.card : null, boxShadow: isSelected ? ConduitShadows.card(context) : null,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -2327,7 +2327,7 @@ class _VoiceInputSheetState extends ConsumerState<_VoiceInputSheet> {
color: context.conduitTheme.dividerColor, color: context.conduitTheme.dividerColor,
width: BorderWidth.regular, width: BorderWidth.regular,
), ),
boxShadow: ConduitShadows.modal, boxShadow: ConduitShadows.modal(context),
), ),
padding: const EdgeInsets.all(Spacing.bottomSheetPadding), padding: const EdgeInsets.all(Spacing.bottomSheetPadding),
child: SafeArea( child: SafeArea(
@@ -2435,7 +2435,7 @@ class _VoiceInputSheetState extends ConsumerState<_VoiceInputSheet> {
top: Radius.circular(AppBorderRadius.bottomSheet), top: Radius.circular(AppBorderRadius.bottomSheet),
), ),
border: Border.all(color: context.conduitTheme.dividerColor, width: 1), border: Border.all(color: context.conduitTheme.dividerColor, width: 1),
boxShadow: ConduitShadows.modal, boxShadow: ConduitShadows.modal(context),
), ),
child: SafeArea( child: SafeArea(
top: false, top: false,
@@ -2940,7 +2940,7 @@ class _SelectableMessageWrapper extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary, color: context.conduitTheme.buttonPrimary,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: ConduitShadows.medium, boxShadow: ConduitShadows.medium(context),
), ),
child: Icon( child: Icon(
Icons.check, Icons.check,

View File

@@ -572,8 +572,12 @@ class FullScreenImageViewer extends ConsumerWidget {
} }
} }
final tokens = context.colorTokens;
final background = tokens.neutralTone10;
final iconColor = tokens.neutralOnSurface;
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: background,
body: Stack( body: Stack(
children: [ children: [
Center( Center(
@@ -595,14 +599,14 @@ class FullScreenImageViewer extends ConsumerWidget {
IconButton( IconButton(
icon: Icon( icon: Icon(
Platform.isIOS ? Icons.ios_share : Icons.share_outlined, Platform.isIOS ? Icons.ios_share : Icons.share_outlined,
color: Colors.white, color: iconColor,
size: 26, size: 26,
), ),
onPressed: () => _shareImage(context, ref), onPressed: () => _shareImage(context, ref),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
IconButton( IconButton(
icon: const Icon(Icons.close, color: Colors.white, size: 28), icon: Icon(Icons.close, color: iconColor, size: 28),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
), ),
], ],

View File

@@ -1281,7 +1281,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
alpha: Alpha.buttonPressed, alpha: Alpha.buttonPressed,
), ),
borderRadius: BorderRadius.circular(radius), borderRadius: BorderRadius.circular(radius),
boxShadow: ConduitShadows.button, boxShadow: ConduitShadows.button(context),
), ),
child: Center( child: Center(
child: Icon( child: Icon(
@@ -1696,7 +1696,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
color: theme.dividerColor, color: theme.dividerColor,
width: BorderWidth.thin, width: BorderWidth.thin,
), ),
boxShadow: ConduitShadows.modal, boxShadow: ConduitShadows.modal(context),
), ),
child: ModalSheetSafeArea( child: ModalSheetSafeArea(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
@@ -1810,7 +1810,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
color: background, color: background,
borderRadius: BorderRadius.circular(AppBorderRadius.input), borderRadius: BorderRadius.circular(AppBorderRadius.input),
border: Border.all(color: borderColor, width: BorderWidth.thin), border: Border.all(color: borderColor, width: BorderWidth.thin),
boxShadow: value ? ConduitShadows.low : const [], boxShadow: value ? ConduitShadows.low(context) : const [],
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -1897,7 +1897,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
color: background, color: background,
borderRadius: BorderRadius.circular(AppBorderRadius.input), borderRadius: BorderRadius.circular(AppBorderRadius.input),
border: Border.all(color: borderColor, width: BorderWidth.thin), border: Border.all(color: borderColor, width: BorderWidth.thin),
boxShadow: selected ? ConduitShadows.low : const [], boxShadow: selected ? ConduitShadows.low(context) : const [],
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,

View File

@@ -563,13 +563,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
context.conduitTheme.chatBubbleUserBorder, context.conduitTheme.chatBubbleUserBorder,
width: BorderWidth.regular, width: BorderWidth.regular,
), ),
boxShadow: [ boxShadow: ConduitShadows.small(context),
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
), ),
child: _isEditing child: _isEditing
? Focus( ? Focus(

View File

@@ -1362,7 +1362,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
color: theme.dividerColor, color: theme.dividerColor,
width: BorderWidth.regular, width: BorderWidth.regular,
), ),
boxShadow: ConduitShadows.card, boxShadow: ConduitShadows.card(context),
), ),
child: Row( child: Row(
children: [ children: [
@@ -1631,7 +1631,9 @@ class _ConversationTile extends StatelessWidget {
final Color borderColor = selected final Color borderColor = selected
? theme.buttonPrimary.withValues(alpha: 0.7) ? theme.buttonPrimary.withValues(alpha: 0.7)
: theme.surfaceContainerHighest.withValues(alpha: 0.40); : theme.surfaceContainerHighest.withValues(alpha: 0.40);
final List<BoxShadow> shadow = selected ? ConduitShadows.low : const []; final List<BoxShadow> shadow = selected
? ConduitShadows.low(context)
: const [];
Color? overlayForStates(Set<WidgetState> states) { Color? overlayForStates(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed)) { if (states.contains(WidgetState.pressed)) {

View File

@@ -85,7 +85,7 @@ class _OnboardingSheetState extends ConsumerState<OnboardingSheet> {
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.modal), top: Radius.circular(AppBorderRadius.modal),
), ),
boxShadow: ConduitShadows.modal, boxShadow: ConduitShadows.modal(context),
), ),
child: SafeArea( child: SafeArea(
child: Padding( child: Padding(
@@ -228,7 +228,7 @@ class _IllustratedPage extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary, color: context.conduitTheme.buttonPrimary,
borderRadius: BorderRadius.circular(AppBorderRadius.avatar), borderRadius: BorderRadius.circular(AppBorderRadius.avatar),
boxShadow: ConduitShadows.glow, boxShadow: ConduitShadows.glow(context),
), ),
child: Icon(page.icon, color: context.conduitTheme.textInverse), child: Icon(page.icon, color: context.conduitTheme.textInverse),
).animate().scale(duration: AnimationDuration.fast), ).animate().scale(duration: AnimationDuration.fast),
@@ -304,7 +304,7 @@ class _IllustratedPage extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: context.conduitTheme.buttonPrimary.withValues(alpha: alpha), color: context.conduitTheme.buttonPrimary.withValues(alpha: alpha),
boxShadow: ConduitShadows.glow, boxShadow: ConduitShadows.glow(context),
), ),
); );
} }

View File

@@ -562,7 +562,7 @@ class AppCustomizationPage extends ConsumerWidget {
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.modal), top: Radius.circular(AppBorderRadius.modal),
), ),
boxShadow: ConduitShadows.modal, boxShadow: ConduitShadows.modal(context),
), ),
child: SafeArea( child: SafeArea(
top: false, top: false,

View File

@@ -336,7 +336,7 @@ class ProfilePage extends ConsumerWidget {
color: accent.withValues(alpha: 0.18), color: accent.withValues(alpha: 0.18),
width: BorderWidth.thin, width: BorderWidth.thin,
), ),
boxShadow: ConduitShadows.medium, boxShadow: ConduitShadows.medium(context),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -347,7 +347,7 @@ class ProfilePage extends ConsumerWidget {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppBorderRadius.avatar), borderRadius: BorderRadius.circular(AppBorderRadius.avatar),
boxShadow: ConduitShadows.high, boxShadow: ConduitShadows.high(context),
), ),
child: UserAvatar( child: UserAvatar(
size: IconSize.avatar, size: IconSize.avatar,
@@ -973,7 +973,7 @@ class _DefaultModelBottomSheetState
color: context.conduitTheme.dividerColor, color: context.conduitTheme.dividerColor,
width: BorderWidth.regular, width: BorderWidth.regular,
), ),
boxShadow: ConduitShadows.modal, boxShadow: ConduitShadows.modal(context),
), ),
child: ModalSheetSafeArea( child: ModalSheetSafeArea(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -1234,7 +1234,7 @@ class _DefaultModelBottomSheetState
: context.conduitTheme.dividerColor, : context.conduitTheme.dividerColor,
width: BorderWidth.regular, width: BorderWidth.regular,
), ),
boxShadow: isSelected ? ConduitShadows.card : null, boxShadow: isSelected ? ConduitShadows.card(context) : null,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import '../theme/theme_extensions.dart'; import '../theme/theme_extensions.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import '../theme/app_theme.dart'; import '../theme/color_tokens.dart';
import '../theme/color_palettes.dart'; import '../theme/color_palettes.dart';
/// Centralized service for consistent brand identity throughout the app /// Centralized service for consistent brand identity throughout the app
@@ -107,8 +107,10 @@ class BrandService {
BuildContext? context, BuildContext? context,
}) { }) {
final bgColor = backgroundColor ?? primaryBrandColor(context: context); final bgColor = backgroundColor ?? primaryBrandColor(context: context);
final tokens = _resolveTokens(context);
final iColor = final iColor =
iconColor ?? (context?.conduitTheme.textInverse ?? AppTheme.neutral50); iconColor ??
(context?.conduitTheme.textInverse ?? tokens.neutralTone00);
return Container( return Container(
width: size, width: size,
@@ -175,8 +177,9 @@ class BrandService {
bool showBackground = true, bool showBackground = true,
BuildContext? context, BuildContext? context,
}) { }) {
final tokens = _resolveTokens(context);
final iconColor = final iconColor =
color ?? (context?.conduitTheme.iconSecondary ?? AppTheme.neutral400); color ?? (context?.conduitTheme.iconSecondary ?? tokens.neutralTone80);
if (!showBackground) { if (!showBackground) {
return createBrandIcon( return createBrandIcon(
@@ -191,10 +194,10 @@ class BrandService {
width: size, width: size,
height: size, height: size,
decoration: BoxDecoration( decoration: BoxDecoration(
color: context?.conduitTheme.surfaceBackground ?? AppTheme.neutral700, color: context?.conduitTheme.surfaceBackground ?? tokens.neutralTone10,
borderRadius: BorderRadius.circular(size / 2), borderRadius: BorderRadius.circular(size / 2),
border: Border.all( border: Border.all(
color: context?.conduitTheme.dividerColor ?? AppTheme.neutral600, color: context?.conduitTheme.dividerColor ?? tokens.neutralTone40,
width: 2, width: 2,
), ),
), ),
@@ -218,6 +221,7 @@ class BrandService {
BuildContext? context, BuildContext? context,
}) { }) {
final theme = context?.conduitTheme; final theme = context?.conduitTheme;
final tokens = _resolveTokens(context);
return SizedBox( return SizedBox(
width: width, width: width,
height: 48, height: 48,
@@ -228,16 +232,17 @@ class BrandService {
: createBrandIcon( : createBrandIcon(
size: IconSize.md, size: IconSize.md,
icon: icon ?? primaryIcon, icon: icon ?? primaryIcon,
color: theme?.textInverse ?? AppTheme.neutral50, color: theme?.textInverse ?? tokens.neutralTone00,
context: context, context: context,
), ),
label: Text(text), label: Text(text),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: isSecondary backgroundColor: isSecondary
? (theme?.buttonSecondary ?? AppTheme.neutral700) ? (theme?.buttonSecondary ?? tokens.neutralTone20)
: (theme?.buttonPrimary ?? primaryBrandColor(context: context)), : (theme?.buttonPrimary ?? primaryBrandColor(context: context)),
foregroundColor: theme?.buttonPrimaryText ?? AppTheme.neutral50, foregroundColor: theme?.buttonPrimaryText ?? tokens.brandOn60,
disabledBackgroundColor: theme?.buttonDisabled ?? AppTheme.neutral500, disabledBackgroundColor:
theme?.buttonDisabled ?? tokens.neutralTone40,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md), borderRadius: BorderRadius.circular(AppBorderRadius.md),
), ),
@@ -292,6 +297,7 @@ class BrandService {
BuildContext? context, BuildContext? context,
}) { }) {
final theme = context?.conduitTheme; final theme = context?.conduitTheme;
final tokens = _resolveTokens(context);
final baseColor = final baseColor =
theme?.buttonPrimary ?? theme?.buttonPrimary ??
primaryBrandColor(context: context, brightness: Brightness.dark); primaryBrandColor(context: context, brightness: Brightness.dark);
@@ -309,12 +315,14 @@ class BrandService {
colors: [baseColor, accentColor], colors: [baseColor, accentColor],
), ),
borderRadius: BorderRadius.circular(size / 2), borderRadius: BorderRadius.circular(size / 2),
boxShadow: ConduitShadows.glow, boxShadow: context != null
? ConduitShadows.glow(context)
: ConduitShadows.glowWithTokens(tokens),
), ),
child: Icon( child: Icon(
primaryIcon, primaryIcon,
size: size * 0.5, 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) { static Brightness _resolveBrightness(BuildContext? context) {
return context != null ? Theme.of(context).brightness : Brightness.light; 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);
}
} }

View File

@@ -1,59 +1,64 @@
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'theme_extensions.dart'; import 'theme_extensions.dart';
import 'color_palettes.dart'; import 'color_palettes.dart';
import 'color_tokens.dart';
class AppTheme { class AppTheme {
// Enhanced neutral palette for better contrast (WCAG AA compliant) // Enhanced neutral palette for better contrast (WCAG AA compliant)
static const Color neutral900 = Color(0xFF000000); // Pure black static const Color neutral900 = Color(0xFF0B0E14);
static const Color neutral800 = Color( static const Color neutral800 = Color(0xFF161B24);
0xFF0D0D0D, static const Color neutral700 = Color(0xFF1F2531);
); // Darker for better contrast static const Color neutral600 = Color(0xFF343C4D);
static const Color neutral700 = Color(0xFF1A1A1A); static const Color neutral500 = Color(0xFF4A5161);
static const Color neutral600 = Color(0xFF2D2D2D); // Improved contrast static const Color neutral400 = Color(0xFF9099AC);
static const Color neutral500 = Color(0xFF404040); // Better middle gray static const Color neutral300 = Color(0xFFC5CCD9);
static const Color neutral400 = Color(0xFF525252); static const Color neutral200 = Color(0xFFE6EAF1);
static const Color neutral300 = Color(0xFF6B6B6B); // Improved contrast ratio static const Color neutral100 = Color(0xFFF5F7FA);
static const Color neutral200 = Color(0xFF9E9E9E); // Better readability static const Color neutral50 = Color(0xFFFFFFFF);
static const Color neutral100 = Color(0xFFD1D1D1); // Enhanced contrast
static const Color neutral50 = Color(
0xFFF8F8F8,
); // Softer white for reduced eye strain
// Enhanced semantic colors for WCAG AA compliance // Semantic colors derived from the token specification
static const Color error = Color(0xFFDC2626); // Improved red contrast static const Color error = Color(0xFFCE2C31);
static const Color errorDark = Color(0xFFB91C1C); // Darker red for dark theme static const Color errorDark = Color(0xFFFF5F67);
static const Color success = Color(0xFF059669); // Better green contrast static const Color success = Color(0xFF0E9D58);
static const Color successDark = Color(0xFF047857); // Dark theme green static const Color successDark = Color(0xFF23C179);
static const Color warning = Color(0xFFD97706); // Improved orange contrast static const Color warning = Color(0xFFDB7900);
static const Color warningDark = Color(0xFFB45309); // Dark theme orange static const Color warningDark = Color(0xFFFF9800);
static const Color info = Color(0xFF0284C7); // Better blue contrast static const Color info = Color(0xFF0174D3);
static const Color infoDark = Color(0xFF0369A1); // Dark theme blue static const Color infoDark = Color(0xFF4CA8FF);
static ThemeData light(AppColorPalette palette) { static ThemeData light(AppColorPalette palette) {
final lightTone = palette.light; 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( return ThemeData(
useMaterial3: true, useMaterial3: true,
brightness: Brightness.light, brightness: Brightness.light,
colorScheme: ColorScheme.light( colorScheme: colorScheme,
primary: lightTone.primary,
secondary: lightTone.secondary,
surface: neutral50,
error: error,
).copyWith(surfaceContainerHighest: const Color(0xFFF0F1F1)),
pageTransitionsTheme: _pageTransitionsTheme, pageTransitionsTheme: _pageTransitionsTheme,
splashFactory: NoSplash.splashFactory, splashFactory: NoSplash.splashFactory,
appBarTheme: const AppBarTheme( scaffoldBackgroundColor: tokens.neutralTone10,
appBarTheme: AppBarTheme(
centerTitle: true, centerTitle: true,
elevation: Elevation.none, elevation: Elevation.none,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
foregroundColor: neutral800, foregroundColor: tokens.neutralOnSurface,
), ),
bottomSheetTheme: BottomSheetThemeData( bottomSheetTheme: BottomSheetThemeData(
backgroundColor: neutral50, backgroundColor: tokens.neutralTone00,
modalBackgroundColor: neutral50, modalBackgroundColor: tokens.neutralTone00,
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.modal), borderRadius: BorderRadius.circular(AppBorderRadius.modal),
@@ -66,6 +71,8 @@ class AppTheme {
horizontal: Spacing.lg, horizontal: Spacing.lg,
vertical: Spacing.xs, vertical: Spacing.xs,
), ),
backgroundColor: lightTone.primary,
foregroundColor: _pickOnColor(lightTone.primary, tokens),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md), borderRadius: BorderRadius.circular(AppBorderRadius.md),
), ),
@@ -75,15 +82,19 @@ class AppTheme {
elevation: Elevation.none, elevation: Elevation.none,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.lg), borderRadius: BorderRadius.circular(AppBorderRadius.lg),
side: BorderSide(color: neutral200), side: BorderSide(color: tokens.neutralTone20),
), ),
), ),
snackBarTheme: SnackBarThemeData( snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
backgroundColor: neutral900.withValues(alpha: 0.92), backgroundColor: Color.alphaBlend(
contentTextStyle: const TextStyle( tokens.overlayStrong,
color: neutral50, tokens.neutralOnSurface,
).copyWith(fontSize: AppTypography.bodyMedium), ),
contentTextStyle: TextStyle(
color: tokens.neutralTone00,
fontSize: AppTypography.bodyMedium,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.snackbar), borderRadius: BorderRadius.circular(AppBorderRadius.snackbar),
), ),
@@ -91,7 +102,7 @@ class AppTheme {
), ),
inputDecorationTheme: InputDecorationTheme( inputDecorationTheme: InputDecorationTheme(
filled: true, filled: true,
fillColor: neutral50, fillColor: tokens.neutralTone00,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md), borderRadius: BorderRadius.circular(AppBorderRadius.md),
borderSide: BorderSide.none, borderSide: BorderSide.none,
@@ -106,7 +117,7 @@ class AppTheme {
), ),
errorBorder: OutlineInputBorder( errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md), borderRadius: BorderRadius.circular(AppBorderRadius.md),
borderSide: const BorderSide(color: error, width: 1), borderSide: BorderSide(color: tokens.statusError60, width: 1),
), ),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.md, horizontal: Spacing.md,
@@ -115,7 +126,8 @@ class AppTheme {
), ),
textTheme: ThemeData.light().textTheme, textTheme: ThemeData.light().textTheme,
extensions: <ThemeExtension<dynamic>>[ extensions: <ThemeExtension<dynamic>>[
ConduitThemeExtension.lightPalette(palette), tokens,
ConduitThemeExtension.lightPalette(palette: palette, tokens: tokens),
AppPaletteThemeExtension(palette: palette), AppPaletteThemeExtension(palette: palette),
], ],
); );
@@ -123,32 +135,33 @@ class AppTheme {
static ThemeData dark(AppColorPalette palette) { static ThemeData dark(AppColorPalette palette) {
final darkTone = palette.dark; 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( return ThemeData(
useMaterial3: true, useMaterial3: true,
brightness: Brightness.dark, brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF0A0D0C), colorScheme: colorScheme,
colorScheme: ColorScheme.dark( scaffoldBackgroundColor: tokens.neutralTone10,
primary: darkTone.primary,
secondary: darkTone.secondary,
surface: const Color(0xFF0A0D0C),
surfaceContainerHighest: neutral700,
onSurface: neutral50,
onSurfaceVariant: neutral300,
outline: neutral600,
error: error,
),
pageTransitionsTheme: _pageTransitionsTheme, pageTransitionsTheme: _pageTransitionsTheme,
splashFactory: NoSplash.splashFactory, splashFactory: NoSplash.splashFactory,
appBarTheme: const AppBarTheme( appBarTheme: AppBarTheme(
centerTitle: true, centerTitle: true,
elevation: Elevation.none, elevation: Elevation.none,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
foregroundColor: neutral50, foregroundColor: tokens.neutralOnSurface,
), ),
bottomSheetTheme: BottomSheetThemeData( bottomSheetTheme: BottomSheetThemeData(
backgroundColor: neutral900, backgroundColor: tokens.neutralTone00,
modalBackgroundColor: neutral900, modalBackgroundColor: tokens.neutralTone00,
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.modal), borderRadius: BorderRadius.circular(AppBorderRadius.modal),
@@ -161,6 +174,8 @@ class AppTheme {
horizontal: Spacing.lg, horizontal: Spacing.lg,
vertical: Spacing.xs, vertical: Spacing.xs,
), ),
backgroundColor: darkTone.primary,
foregroundColor: _pickOnColor(darkTone.primary, tokens),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md), borderRadius: BorderRadius.circular(AppBorderRadius.md),
), ),
@@ -170,15 +185,19 @@ class AppTheme {
elevation: Elevation.none, elevation: Elevation.none,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.lg), borderRadius: BorderRadius.circular(AppBorderRadius.lg),
side: BorderSide(color: neutral800), side: BorderSide(color: tokens.neutralTone40),
), ),
), ),
snackBarTheme: SnackBarThemeData( snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
backgroundColor: neutral800.withValues(alpha: 0.92), backgroundColor: Color.alphaBlend(
contentTextStyle: const TextStyle( tokens.overlayStrong,
color: neutral50, tokens.neutralTone20,
).copyWith(fontSize: AppTypography.bodyMedium), ),
contentTextStyle: TextStyle(
color: tokens.neutralOnSurface,
fontSize: AppTypography.bodyMedium,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.snackbar), borderRadius: BorderRadius.circular(AppBorderRadius.snackbar),
), ),
@@ -186,14 +205,14 @@ class AppTheme {
), ),
inputDecorationTheme: InputDecorationTheme( inputDecorationTheme: InputDecorationTheme(
filled: true, filled: true,
fillColor: neutral700, fillColor: tokens.neutralTone20,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md), borderRadius: BorderRadius.circular(AppBorderRadius.md),
borderSide: const BorderSide(color: neutral600, width: 1), borderSide: BorderSide(color: tokens.neutralTone40, width: 1),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md), borderRadius: BorderRadius.circular(AppBorderRadius.md),
borderSide: const BorderSide(color: neutral600, width: 1), borderSide: BorderSide(color: tokens.neutralTone40, width: 1),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md), borderRadius: BorderRadius.circular(AppBorderRadius.md),
@@ -201,7 +220,7 @@ class AppTheme {
), ),
errorBorder: OutlineInputBorder( errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md), borderRadius: BorderRadius.circular(AppBorderRadius.md),
borderSide: const BorderSide(color: error, width: 1), borderSide: BorderSide(color: tokens.statusError60, width: 1),
), ),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.md, horizontal: Spacing.md,
@@ -210,7 +229,8 @@ class AppTheme {
), ),
textTheme: ThemeData.dark().textTheme, textTheme: ThemeData.dark().textTheme,
extensions: <ThemeExtension<dynamic>>[ extensions: <ThemeExtension<dynamic>>[
ConduitThemeExtension.darkPalette(palette), tokens,
ConduitThemeExtension.darkPalette(palette: palette, tokens: tokens),
AppPaletteThemeExtension(palette: palette), AppPaletteThemeExtension(palette: palette),
], ],
); );
@@ -222,18 +242,33 @@ class AppTheme {
) { ) {
final brightness = Theme.of(context).brightness; final brightness = Theme.of(context).brightness;
final tone = palette.toneFor(brightness); final tone = palette.toneFor(brightness);
final tokens = brightness == Brightness.dark
? AppColorTokens.dark(palette: palette)
: AppColorTokens.light(palette: palette);
return CupertinoThemeData( return CupertinoThemeData(
brightness: brightness, brightness: brightness,
primaryColor: tone.primary, primaryColor: tone.primary,
scaffoldBackgroundColor: brightness == Brightness.dark scaffoldBackgroundColor: tokens.neutralTone10,
? neutral900 barBackgroundColor: tokens.neutralTone10,
: neutral50,
barBackgroundColor: brightness == Brightness.dark
? neutral900
: neutral50,
); );
} }
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 = static const PageTransitionsTheme _pageTransitionsTheme =
PageTransitionsTheme( PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{ builders: <TargetPlatform, PageTransitionsBuilder>{

View File

@@ -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<AppColorTokens> {
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<AppColorTokens>? 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);
}
}

View File

@@ -2,8 +2,8 @@ import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// Using system fonts; no GoogleFonts dependency required // Using system fonts; no GoogleFonts dependency required
import 'app_theme.dart';
import 'color_palettes.dart'; import 'color_palettes.dart';
import 'color_tokens.dart';
/// Extended theme data for consistent styling across the app /// Extended theme data for consistent styling across the app
@immutable @immutable
@@ -62,6 +62,12 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
final Color shimmerHighlight; final Color shimmerHighlight;
final Color loadingIndicator; final Color loadingIndicator;
// Markdown/code colors
final Color codeBackground;
final Color codeBorder;
final Color codeText;
final Color codeAccent;
// Text colors // Text colors
final Color textPrimary; final Color textPrimary;
final Color textSecondary; final Color textSecondary;
@@ -141,6 +147,12 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
required this.shimmerHighlight, required this.shimmerHighlight,
required this.loadingIndicator, required this.loadingIndicator,
// Markdown/code colors
required this.codeBackground,
required this.codeBorder,
required this.codeText,
required this.codeAccent,
// Text colors // Text colors
required this.textPrimary, required this.textPrimary,
required this.textSecondary, required this.textSecondary,
@@ -222,6 +234,12 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
Color? shimmerHighlight, Color? shimmerHighlight,
Color? loadingIndicator, Color? loadingIndicator,
// Markdown/code colors
Color? codeBackground,
Color? codeBorder,
Color? codeText,
Color? codeAccent,
// Text colors // Text colors
Color? textPrimary, Color? textPrimary,
Color? textSecondary, Color? textSecondary,
@@ -305,6 +323,12 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
shimmerHighlight: shimmerHighlight ?? this.shimmerHighlight, shimmerHighlight: shimmerHighlight ?? this.shimmerHighlight,
loadingIndicator: loadingIndicator ?? this.loadingIndicator, 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 // Text colors
textPrimary: textPrimary ?? this.textPrimary, textPrimary: textPrimary ?? this.textPrimary,
textSecondary: textSecondary ?? this.textSecondary, textSecondary: textSecondary ?? this.textSecondary,
@@ -477,6 +501,10 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
other.loadingIndicator, other.loadingIndicator,
t, 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 // Text colors
textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!, textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!,
@@ -505,116 +533,136 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
} }
/// Dark theme extension derived from the active color palette. /// 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 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( return ConduitThemeExtension(
chatBubbleUser: darkTone.primary, chatBubbleUser: darkTone.primary,
chatBubbleAssistant: const Color(0xFF0E1010), chatBubbleAssistant: tokens.neutralTone20,
chatBubbleUserText: onDarkPrimary, chatBubbleUserText: onPrimary,
chatBubbleAssistantText: AppTheme.neutral50, chatBubbleAssistantText: tokens.neutralOnSurface,
chatBubbleUserBorder: darkTone.secondary, chatBubbleUserBorder: darkTone.secondary,
chatBubbleAssistantBorder: const Color(0xFF1A1D1C), chatBubbleAssistantBorder: tokens.neutralTone40,
inputBackground: const Color(0xFF141615), inputBackground: tokens.neutralTone20,
inputBorder: AppTheme.neutral600, inputBorder: tokens.neutralTone40,
inputBorderFocused: darkTone.primary, inputBorderFocused: darkTone.primary,
inputText: AppTheme.neutral50, inputText: tokens.neutralOnSurface,
inputPlaceholder: AppTheme.neutral300, inputPlaceholder: tokens.neutralTone80,
inputError: AppTheme.error, inputError: tokens.statusError60,
cardBackground: const Color(0xFF0C0F0E), cardBackground: tokens.neutralTone00,
cardBorder: const Color(0xFF151918), cardBorder: tokens.neutralTone40,
cardShadow: AppTheme.neutral900, cardShadow: blend(tokens.overlayWeak, surface: tokens.neutralTone00),
surfaceBackground: const Color(0xFF0A0D0C), surfaceBackground: tokens.neutralTone10,
surfaceContainer: const Color(0xFF0C0F0E), surfaceContainer: tokens.neutralTone00,
surfaceContainerHighest: const Color(0xFF121514), surfaceContainerHighest: tokens.neutralTone20,
buttonPrimary: darkTone.primary, buttonPrimary: darkTone.primary,
buttonPrimaryText: onDarkPrimary, buttonPrimaryText: onPrimary,
buttonSecondary: const Color(0xFF151918), buttonSecondary: tokens.neutralTone20,
buttonSecondaryText: AppTheme.neutral50, buttonSecondaryText: tokens.neutralOnSurface,
buttonDisabled: AppTheme.neutral600, buttonDisabled: tokens.neutralTone40,
buttonDisabledText: AppTheme.neutral400, buttonDisabledText: tokens.neutralTone80,
success: const Color(0xFF34D399), success: tokens.statusSuccess60,
successBackground: const Color(0xFF14532D), successBackground: toneBackground(tokens.statusSuccess60),
error: const Color(0xFFFCA5A5), error: tokens.statusError60,
errorBackground: const Color(0xFF7F1D1D), errorBackground: toneBackground(tokens.statusError60),
warning: const Color(0xFFFBBF24), warning: tokens.statusWarning60,
warningBackground: const Color(0xFF451A03), warningBackground: toneBackground(tokens.statusWarning60),
info: const Color(0xFF93C5FD), info: tokens.statusInfo60,
infoBackground: const Color(0xFF0C4A6E), infoBackground: toneBackground(tokens.statusInfo60),
dividerColor: AppTheme.neutral600, dividerColor: tokens.neutralTone40,
navigationBackground: const Color(0xFF0A0D0C), navigationBackground: tokens.neutralTone10,
navigationSelected: darkTone.primary, navigationSelected: darkTone.primary,
navigationUnselected: AppTheme.neutral300, navigationUnselected: tokens.neutralTone80,
navigationSelectedBackground: _surfaceTint( navigationSelectedBackground: blend(
darkTone.primary, tokens.overlayMedium,
const Color(0xFF0A0D0C), surface: tokens.neutralTone10,
0.24, ),
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, loadingIndicator: darkTone.primary,
textPrimary: AppTheme.neutral50, codeBackground: tokens.codeBackground,
textSecondary: const Color(0xFFBAC2C0), codeBorder: tokens.codeBorder,
textTertiary: AppTheme.neutral400, codeText: tokens.codeText,
textInverse: AppTheme.neutral900, codeAccent: tokens.codeAccent,
textDisabled: AppTheme.neutral600, textPrimary: tokens.neutralOnSurface,
iconPrimary: AppTheme.neutral50, textSecondary: tokens.neutralTone80,
iconSecondary: const Color(0xFFA0A8A5), textTertiary: tokens.neutralTone60,
iconDisabled: AppTheme.neutral600, textInverse: tokens.neutralTone00,
iconInverse: AppTheme.neutral900, textDisabled: tokens.neutralTone40,
iconPrimary: tokens.neutralOnSurface,
iconSecondary: tokens.neutralTone80,
iconDisabled: tokens.neutralTone40,
iconInverse: tokens.neutralTone00,
headingLarge: TextStyle( headingLarge: TextStyle(
fontSize: AppTypography.displaySmall, fontSize: AppTypography.displaySmall,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppTheme.neutral50, color: tokens.neutralOnSurface,
height: 1.2, height: 1.2,
), ),
headingMedium: TextStyle( headingMedium: TextStyle(
fontSize: AppTypography.headlineLarge, fontSize: AppTypography.headlineLarge,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppTheme.neutral50, color: tokens.neutralOnSurface,
height: 1.3, height: 1.3,
), ),
headingSmall: TextStyle( headingSmall: TextStyle(
fontSize: AppTypography.headlineSmall, fontSize: AppTypography.headlineSmall,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppTheme.neutral50, color: tokens.neutralOnSurface,
height: 1.4, height: 1.4,
), ),
bodyLarge: TextStyle( bodyLarge: TextStyle(
fontSize: AppTypography.bodyLarge, fontSize: AppTypography.bodyLarge,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: AppTheme.neutral50, color: tokens.neutralOnSurface,
height: 1.5, height: 1.5,
), ),
bodyMedium: TextStyle( bodyMedium: TextStyle(
fontSize: AppTypography.bodyMedium, fontSize: AppTypography.bodyMedium,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: AppTheme.neutral50, color: tokens.neutralOnSurface,
height: 1.5, height: 1.5,
), ),
bodySmall: TextStyle( bodySmall: TextStyle(
fontSize: AppTypography.bodySmall, fontSize: AppTypography.bodySmall,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: const Color(0xFFD1D5DB), color: tokens.neutralTone80,
height: 1.4, height: 1.4,
), ),
caption: TextStyle( caption: TextStyle(
fontSize: AppTypography.labelMedium, fontSize: AppTypography.labelMedium,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: AppTheme.neutral300, color: tokens.neutralTone80,
height: 1.3, height: 1.3,
letterSpacing: 0.5, letterSpacing: 0.5,
), ),
label: TextStyle( label: TextStyle(
fontSize: AppTypography.labelLarge, fontSize: AppTypography.labelLarge,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: const Color(0xFFD1D5DB), color: tokens.neutralOnSurface,
height: 1.3, height: 1.3,
), ),
code: TextStyle( code: TextStyle(
fontSize: AppTypography.bodySmall, fontSize: AppTypography.bodySmall,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: const Color(0xFFD1D5DB), color: tokens.neutralOnSurface,
height: 1.4, height: 1.4,
fontFamily: AppTypography.monospaceFontFamily, fontFamily: AppTypography.monospaceFontFamily,
), ),
@@ -622,133 +670,143 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
} }
/// Light theme extension derived from the active color palette. /// 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 lightTone = palette.light;
final darkTone = palette.dark; 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( return ConduitThemeExtension(
chatBubbleUser: lightTone.primary, chatBubbleUser: lightTone.primary,
chatBubbleAssistant: const Color(0xFFF7F7F7), chatBubbleAssistant: tokens.neutralTone00,
chatBubbleUserText: onLightPrimary, chatBubbleUserText: onPrimary,
chatBubbleAssistantText: const Color(0xFF1C1C1C), chatBubbleAssistantText: tokens.neutralOnSurface,
chatBubbleUserBorder: darkTone.primary, chatBubbleUserBorder: darkTone.primary,
chatBubbleAssistantBorder: const Color(0xFFE7E7E7), chatBubbleAssistantBorder: tokens.neutralTone20,
inputBackground: AppTheme.neutral50, inputBackground: tokens.neutralTone00,
inputBorder: AppTheme.neutral200, inputBorder: tokens.neutralTone20,
inputBorderFocused: lightTone.primary, inputBorderFocused: lightTone.primary,
inputText: AppTheme.neutral900, inputText: tokens.neutralOnSurface,
inputPlaceholder: AppTheme.neutral500, inputPlaceholder: tokens.neutralTone60,
inputError: AppTheme.error, inputError: tokens.statusError60,
cardBackground: AppTheme.neutral50, cardBackground: tokens.neutralTone00,
cardBorder: const Color(0xFFE7E7E7), cardBorder: tokens.neutralTone20,
cardShadow: const Color(0xFFF3F4F6), cardShadow: blend(tokens.overlayWeak),
surfaceBackground: AppTheme.neutral50, surfaceBackground: tokens.neutralTone10,
surfaceContainer: const Color(0xFFF7F7F7), surfaceContainer: tokens.neutralTone00,
surfaceContainerHighest: const Color(0xFFF0F1F1), surfaceContainerHighest: tokens.neutralTone20,
buttonPrimary: lightTone.primary, buttonPrimary: lightTone.primary,
buttonPrimaryText: onLightPrimary, buttonPrimaryText: onPrimary,
buttonSecondary: const Color(0xFFF0F1F1), buttonSecondary: tokens.neutralTone20,
buttonSecondaryText: const Color(0xFF1C1C1C), buttonSecondaryText: tokens.neutralOnSurface,
buttonDisabled: AppTheme.neutral300, buttonDisabled: tokens.neutralTone40,
buttonDisabledText: AppTheme.neutral500, buttonDisabledText: tokens.neutralTone60,
success: const Color(0xFF166534), success: tokens.statusSuccess60,
successBackground: const Color(0xFFECFDF3), successBackground: toneBackground(tokens.statusSuccess60),
error: const Color(0xFFB91C1C), error: tokens.statusError60,
errorBackground: const Color(0xFFFEE2E2), errorBackground: toneBackground(tokens.statusError60),
warning: const Color(0xFF92400E), warning: tokens.statusWarning60,
warningBackground: const Color(0xFFFEF3C7), warningBackground: toneBackground(tokens.statusWarning60),
info: const Color(0xFF1D4ED8), info: tokens.statusInfo60,
infoBackground: const Color(0xFFDBEAFE), infoBackground: toneBackground(tokens.statusInfo60),
dividerColor: AppTheme.neutral100, dividerColor: tokens.neutralTone20,
navigationBackground: AppTheme.neutral50, navigationBackground: tokens.neutralTone00,
navigationSelected: lightTone.primary, navigationSelected: lightTone.primary,
navigationUnselected: AppTheme.neutral600, navigationUnselected: tokens.neutralTone60,
navigationSelectedBackground: _surfaceTint( navigationSelectedBackground: blend(tokens.overlayMedium),
lightTone.primary, shimmerBase: blend(tokens.overlayWeak, surface: tokens.neutralTone10),
AppTheme.neutral50, shimmerHighlight: tokens.neutralTone00,
0.16,
),
shimmerBase: const Color(0xFFF3F4F6),
shimmerHighlight: AppTheme.neutral50,
loadingIndicator: lightTone.primary, loadingIndicator: lightTone.primary,
textPrimary: const Color(0xFF1C1C1C), codeBackground: tokens.codeBackground,
textSecondary: const Color(0xFF3A3F3E), codeBorder: tokens.codeBorder,
textTertiary: AppTheme.neutral500, codeText: tokens.codeText,
textInverse: AppTheme.neutral50, codeAccent: tokens.codeAccent,
textDisabled: AppTheme.neutral400, textPrimary: tokens.neutralOnSurface,
iconPrimary: const Color(0xFF1C1C1C), textSecondary: tokens.neutralTone80,
iconSecondary: const Color(0xFF666C6A), textTertiary: tokens.neutralTone60,
iconDisabled: AppTheme.neutral400, textInverse: tokens.neutralTone00,
iconInverse: AppTheme.neutral50, textDisabled: tokens.neutralTone60,
iconPrimary: tokens.neutralOnSurface,
iconSecondary: tokens.neutralTone80,
iconDisabled: tokens.neutralTone60,
iconInverse: tokens.neutralTone00,
headingLarge: TextStyle( headingLarge: TextStyle(
fontSize: AppTypography.displaySmall, fontSize: AppTypography.displaySmall,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: const Color(0xFF111827), color: tokens.neutralOnSurface,
height: 1.2, height: 1.2,
), ),
headingMedium: TextStyle( headingMedium: TextStyle(
fontSize: AppTypography.headlineLarge, fontSize: AppTypography.headlineLarge,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: const Color(0xFF111827), color: tokens.neutralOnSurface,
height: 1.3, height: 1.3,
), ),
headingSmall: TextStyle( headingSmall: TextStyle(
fontSize: AppTypography.headlineSmall, fontSize: AppTypography.headlineSmall,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: const Color(0xFF111827), color: tokens.neutralOnSurface,
height: 1.4, height: 1.4,
), ),
bodyLarge: TextStyle( bodyLarge: TextStyle(
fontSize: AppTypography.bodyLarge, fontSize: AppTypography.bodyLarge,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: const Color(0xFF111827), color: tokens.neutralOnSurface,
height: 1.5, height: 1.5,
), ),
bodyMedium: TextStyle( bodyMedium: TextStyle(
fontSize: AppTypography.bodyMedium, fontSize: AppTypography.bodyMedium,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: const Color(0xFF111827), color: tokens.neutralOnSurface,
height: 1.5, height: 1.5,
), ),
bodySmall: TextStyle( bodySmall: TextStyle(
fontSize: AppTypography.bodySmall, fontSize: AppTypography.bodySmall,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: AppTheme.neutral500, color: tokens.neutralTone60,
height: 1.4, height: 1.4,
), ),
caption: TextStyle( caption: TextStyle(
fontSize: AppTypography.labelMedium, fontSize: AppTypography.labelMedium,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: AppTheme.neutral400, color: tokens.neutralTone60,
height: 1.3, height: 1.3,
letterSpacing: 0.5, letterSpacing: 0.5,
), ),
label: TextStyle( label: TextStyle(
fontSize: AppTypography.labelLarge, fontSize: AppTypography.labelLarge,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: const Color(0xFF444948), color: tokens.neutralTone80,
height: 1.3, height: 1.3,
), ),
code: TextStyle( code: TextStyle(
fontSize: AppTypography.bodySmall, fontSize: AppTypography.bodySmall,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: const Color(0xFF1C1C1C), color: tokens.neutralOnSurface,
height: 1.4, height: 1.4,
fontFamily: AppTypography.monospaceFontFamily, fontFamily: AppTypography.monospaceFontFamily,
), ),
); );
} }
static Color _surfaceTint(Color tone, Color surface, double opacity) { static Color _onSurfaceColor(Color background, AppColorTokens tokens) {
return Color.alphaBlend(tone.withValues(alpha: opacity), surface); final contrastOnLight = _contrastRatio(background, tokens.neutralTone00);
} final contrastOnDark = _contrastRatio(background, tokens.neutralOnSurface);
static Color _onSurfaceColor(Color background) {
final contrastOnLight = _contrastRatio(background, AppTheme.neutral50);
final contrastOnDark = _contrastRatio(background, AppTheme.neutral900);
return contrastOnLight >= contrastOnDark return contrastOnLight >= contrastOnDark
? AppTheme.neutral50 ? tokens.neutralTone00
: AppTheme.neutral900; : tokens.neutralOnSurface;
} }
static double _contrastRatio(Color a, Color b) { static double _contrastRatio(Color a, Color b) {
@@ -769,9 +827,26 @@ extension ConduitThemeContext on BuildContext {
final palette = final palette =
theme.extension<AppPaletteThemeExtension>()?.palette ?? theme.extension<AppPaletteThemeExtension>()?.palette ??
AppColorPalettes.auroraViolet; AppColorPalettes.auroraViolet;
final tokens = theme.brightness == Brightness.dark
? AppColorTokens.dark(palette: palette)
: AppColorTokens.light(palette: palette);
return theme.brightness == Brightness.dark return theme.brightness == Brightness.dark
? ConduitThemeExtension.darkPalette(palette) ? ConduitThemeExtension.darkPalette(palette: palette, tokens: tokens)
: ConduitThemeExtension.lightPalette(palette); : ConduitThemeExtension.lightPalette(palette: palette, tokens: tokens);
}
}
extension ConduitColorTokensContext on BuildContext {
AppColorTokens get colorTokens {
final theme = Theme.of(this);
final tokens = theme.extension<AppColorTokens>();
if (tokens != null) return tokens;
final palette =
theme.extension<AppPaletteThemeExtension>()?.palette ??
AppColorPalettes.auroraViolet;
return theme.brightness == Brightness.dark
? AppColorTokens.dark(palette: palette)
: AppColorTokens.light(palette: palette);
} }
} }
@@ -923,184 +998,155 @@ class Elevation {
/// Helper class for consistent shadows - Enhanced for production with better hierarchy /// Helper class for consistent shadows - Enhanced for production with better hierarchy
class ConduitShadows { class ConduitShadows {
static List<BoxShadow> get low => [ static List<BoxShadow> low(BuildContext context) => _shadow(
BoxShadow( context.colorTokens,
color: AppTheme.neutral900.withValues(alpha: 0.08), opacity: 0.08,
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
spreadRadius: 0, );
),
];
static List<BoxShadow> get medium => [ static List<BoxShadow> medium(BuildContext context) => _shadow(
BoxShadow( context.colorTokens,
color: AppTheme.neutral900.withValues(alpha: 0.12), opacity: 0.12,
blurRadius: 16, blurRadius: 16,
offset: const Offset(0, 4), offset: const Offset(0, 4),
spreadRadius: 0, );
),
];
static List<BoxShadow> get high => [ static List<BoxShadow> high(BuildContext context) => _shadow(
BoxShadow( context.colorTokens,
color: AppTheme.neutral900.withValues(alpha: 0.16), opacity: 0.16,
blurRadius: 24, blurRadius: 24,
offset: const Offset(0, 8), offset: const Offset(0, 8),
spreadRadius: 0, );
),
];
static List<BoxShadow> get glow => [ static List<BoxShadow> glow(BuildContext context) =>
glowWithTokens(context.colorTokens);
static List<BoxShadow> glowWithTokens(AppColorTokens tokens) {
final double alpha = tokens.brightness == Brightness.light ? 0.25 : 0.35;
return [
BoxShadow( BoxShadow(
color: AppColorPalettes.auroraViolet.light.primary.withValues( color: tokens.brandTone60.withValues(alpha: alpha),
alpha: 0.25,
),
blurRadius: 20, blurRadius: 20,
offset: const Offset(0, 0), offset: const Offset(0, 0),
spreadRadius: 0, spreadRadius: 0,
), ),
]; ];
}
// Enhanced shadows for specific components with better hierarchy static List<BoxShadow> card(BuildContext context) => _shadow(
static List<BoxShadow> get card => [ context.colorTokens,
BoxShadow( opacity: 0.06,
color: AppTheme.neutral900.withValues(alpha: 0.06),
blurRadius: 12, blurRadius: 12,
offset: const Offset(0, 3), offset: const Offset(0, 3),
spreadRadius: 0, );
),
];
static List<BoxShadow> get button => [ static List<BoxShadow> button(BuildContext context) => _shadow(
BoxShadow( context.colorTokens,
color: AppTheme.neutral900.withValues(alpha: 0.1), opacity: 0.1,
blurRadius: 6, blurRadius: 6,
offset: const Offset(0, 2), offset: const Offset(0, 2),
spreadRadius: 0, );
),
];
static List<BoxShadow> get modal => [ static List<BoxShadow> modal(BuildContext context) => _shadow(
BoxShadow( context.colorTokens,
color: AppTheme.neutral900.withValues(alpha: 0.2), opacity: 0.2,
blurRadius: 32, blurRadius: 32,
offset: const Offset(0, 12), offset: const Offset(0, 12),
spreadRadius: 0, );
),
];
static List<BoxShadow> get navigation => [ static List<BoxShadow> navigation(BuildContext context) => _shadow(
BoxShadow( context.colorTokens,
color: AppTheme.neutral900.withValues(alpha: 0.08), opacity: 0.08,
blurRadius: 16, blurRadius: 16,
offset: const Offset(0, -2), offset: const Offset(0, -2),
spreadRadius: 0, );
),
];
static List<BoxShadow> get messageBubble => [ static List<BoxShadow> messageBubble(BuildContext context) => _shadow(
BoxShadow( context.colorTokens,
color: AppTheme.neutral900.withValues(alpha: 0.04), opacity: 0.04,
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 1), offset: const Offset(0, 1),
spreadRadius: 0, );
),
];
static List<BoxShadow> get input => [ static List<BoxShadow> input(BuildContext context) => _shadow(
BoxShadow( context.colorTokens,
color: AppTheme.neutral900.withValues(alpha: 0.05), opacity: 0.05,
blurRadius: 4, blurRadius: 4,
offset: const Offset(0, 1), offset: const Offset(0, 1),
spreadRadius: 0, );
),
];
// Dark theme specific shadows with better contrast static List<BoxShadow> pressed(BuildContext context) => _shadow(
static List<BoxShadow> get darkCard => [ context.colorTokens,
BoxShadow( opacity: 0.15,
color: AppTheme.neutral900.withValues(alpha: 0.3),
blurRadius: 16,
offset: const Offset(0, 4),
spreadRadius: 0,
),
];
static List<BoxShadow> get darkModal => [
BoxShadow(
color: AppTheme.neutral900.withValues(alpha: 0.4),
blurRadius: 40,
offset: const Offset(0, 16),
spreadRadius: 0,
),
];
// Interactive shadows with better feedback
static List<BoxShadow> get pressed => [
BoxShadow(
color: AppTheme.neutral900.withValues(alpha: 0.15),
blurRadius: 4, blurRadius: 4,
offset: const Offset(0, 1), offset: const Offset(0, 1),
spreadRadius: 0, );
),
];
static List<BoxShadow> get hover => [ static List<BoxShadow> hover(BuildContext context) => _shadow(
BoxShadow( context.colorTokens,
color: AppTheme.neutral900.withValues(alpha: 0.12), opacity: 0.12,
blurRadius: 12, blurRadius: 12,
offset: const Offset(0, 4), offset: const Offset(0, 4),
spreadRadius: 0, );
),
];
// Enhanced shadows for better visual hierarchy static List<BoxShadow> micro(BuildContext context) => _shadow(
static List<BoxShadow> get micro => [ context.colorTokens,
BoxShadow( opacity: 0.04,
color: AppTheme.neutral900.withValues(alpha: 0.04),
blurRadius: 4, blurRadius: 4,
offset: const Offset(0, 1), offset: const Offset(0, 1),
spreadRadius: 0, );
),
];
static List<BoxShadow> get small => [ static List<BoxShadow> small(BuildContext context) => _shadow(
BoxShadow( context.colorTokens,
color: AppTheme.neutral900.withValues(alpha: 0.06), opacity: 0.06,
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
spreadRadius: 0, );
),
];
static List<BoxShadow> get standard => [ static List<BoxShadow> standard(BuildContext context) => _shadow(
BoxShadow( context.colorTokens,
color: AppTheme.neutral900.withValues(alpha: 0.08), opacity: 0.08,
blurRadius: 12, blurRadius: 12,
offset: const Offset(0, 3), offset: const Offset(0, 3),
spreadRadius: 0, );
),
];
static List<BoxShadow> get large => [ static List<BoxShadow> large(BuildContext context) => _shadow(
BoxShadow( context.colorTokens,
color: AppTheme.neutral900.withValues(alpha: 0.12), opacity: 0.12,
blurRadius: 16, blurRadius: 16,
offset: const Offset(0, 4), offset: const Offset(0, 4),
spreadRadius: 0, );
),
];
static List<BoxShadow> get extraLarge => [ static List<BoxShadow> extraLarge(BuildContext context) => _shadow(
BoxShadow( context.colorTokens,
color: AppTheme.neutral900.withValues(alpha: 0.16), opacity: 0.16,
blurRadius: 24, blurRadius: 24,
offset: const Offset(0, 8), offset: const Offset(0, 8),
);
static List<BoxShadow> _shadow(
AppColorTokens tokens, {
required double opacity,
required double blurRadius,
required Offset offset,
}) {
return [
BoxShadow(
color: _overlayColor(tokens, opacity),
blurRadius: blurRadius,
offset: offset,
spreadRadius: 0, 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 /// Typography scale following Conduit design tokens - Enhanced for production
class AppTypography { class AppTypography {
// Primary UI font now uses the platform default system font // Primary UI font now uses the platform default system font

View File

@@ -84,7 +84,7 @@ Future<void> showConduitContextMenu({
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.surfaceBackground, color: theme.surfaceBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.lg), borderRadius: BorderRadius.circular(AppBorderRadius.lg),
boxShadow: ConduitShadows.modal, boxShadow: ConduitShadows.modal(context),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@@ -326,7 +326,7 @@ class ConduitCard extends StatelessWidget {
: context.conduitTheme.cardBorder, : context.conduitTheme.cardBorder,
width: BorderWidth.standard, width: BorderWidth.standard,
), ),
boxShadow: isElevated ? ConduitShadows.card : null, boxShadow: isElevated ? ConduitShadows.card(context) : null,
), ),
child: child, child: child,
), ),

View File

@@ -297,7 +297,7 @@ class LoadingOverlay extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.conduitTheme.cardBackground, color: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.card), borderRadius: BorderRadius.circular(AppBorderRadius.card),
boxShadow: ConduitShadows.card, boxShadow: ConduitShadows.card(context),
), ),
child: ImprovedLoadingState( child: ImprovedLoadingState(
message: message, message: message,

View File

@@ -5,7 +5,7 @@ import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import '../services/brand_service.dart'; import '../services/brand_service.dart';
import '../theme/app_theme.dart'; import '../theme/color_tokens.dart';
import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/l10n/app_localizations.dart';
/// Standard loading indicators following Conduit design patterns /// Standard loading indicators following Conduit design patterns
@@ -52,13 +52,14 @@ class ConduitLoading {
Color? color, Color? color,
BuildContext? context, BuildContext? context,
}) { }) {
final tokens = context?.colorTokens ?? AppColorTokens.fallback();
return _LoadingIndicator( return _LoadingIndicator(
size: size, size: size,
color: color:
color ?? color ??
(context?.conduitTheme.buttonPrimaryText ?? (context?.conduitTheme.buttonPrimaryText ??
context?.conduitTheme.textPrimary ?? context?.conduitTheme.textPrimary ??
AppTheme.neutral50), tokens.neutralTone00),
type: _LoadingType.button, type: _LoadingType.button,
); );
} }
@@ -175,7 +176,7 @@ class _LoadingOverlay extends StatelessWidget {
? context.conduitTheme.surfaceBackground ? context.conduitTheme.surfaceBackground
: context.conduitTheme.surfaceBackground, : context.conduitTheme.surfaceBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.lg), borderRadius: BorderRadius.circular(AppBorderRadius.lg),
boxShadow: ConduitShadows.high, boxShadow: ConduitShadows.high(context),
), ),
child: ConduitLoading.primary( child: ConduitLoading.primary(
size: IconSize.xl, size: IconSize.xl,

View File

@@ -16,6 +16,7 @@ import 'package:webview_flutter/webview_flutter.dart';
import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/l10n/app_localizations.dart';
import '../../theme/theme_extensions.dart'; import '../../theme/theme_extensions.dart';
import '../../theme/color_tokens.dart';
class MarkdownFeatureFlags { class MarkdownFeatureFlags {
const MarkdownFeatureFlags({ const MarkdownFeatureFlags({
@@ -172,7 +173,7 @@ class ConduitMarkdownConfig {
required bool enableHighlight, required bool enableHighlight,
}) { }) {
final textStyle = AppTypography.codeStyle.copyWith( final textStyle = AppTypography.codeStyle.copyWith(
color: const Color(0xFFE2E8F0), color: conduitTheme.codeText,
height: 1.55, height: 1.55,
fontSize: 13, fontSize: 13,
); );
@@ -212,6 +213,7 @@ class ConduitMarkdownConfig {
required ThemeData materialTheme, required ThemeData materialTheme,
required String code, required String code,
}) { }) {
final tokens = context.colorTokens;
return SizedBox( return SizedBox(
height: 360, height: 360,
width: double.infinity, width: double.infinity,
@@ -221,6 +223,7 @@ class ConduitMarkdownConfig {
code: code, code: code,
brightness: materialTheme.brightness, brightness: materialTheme.brightness,
colorScheme: materialTheme.colorScheme, colorScheme: materialTheme.colorScheme,
tokens: tokens,
), ),
), ),
); );
@@ -232,7 +235,7 @@ class ConduitMarkdownConfig {
required String code, required String code,
}) { }) {
final textStyle = AppTypography.bodySmallStyle.copyWith( final textStyle = AppTypography.bodySmallStyle.copyWith(
color: Colors.white.withValues(alpha: 0.7), color: conduitTheme.codeText.withValues(alpha: 0.7),
); );
return Column( return Column(
@@ -468,6 +471,7 @@ class _CodeBlockWrapperState extends State<CodeBlockWrapper> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final conduitTheme = widget.theme;
final canCopy = widget.closed && widget.code.trim().isNotEmpty; final canCopy = widget.closed && widget.code.trim().isNotEmpty;
final icon = _copied final icon = _copied
? Icons.check ? Icons.check
@@ -475,9 +479,9 @@ class _CodeBlockWrapperState extends State<CodeBlockWrapper> {
? Icons.copy ? Icons.copy
: Icons.hourglass_empty; : Icons.hourglass_empty;
const background = Color(0xFF0F172A); final background = conduitTheme.codeBackground;
final borderColor = const Color(0xFF1E293B).withValues(alpha: 0.6); final borderColor = conduitTheme.codeBorder.withValues(alpha: 0.6);
final headerColor = const Color(0xFF1E293B).withValues(alpha: 0.85); final headerColor = conduitTheme.codeAccent.withValues(alpha: 0.85);
final languageLabel = (widget.language?.isNotEmpty ?? false) final languageLabel = (widget.language?.isNotEmpty ?? false)
? widget.language! ? widget.language!
@@ -488,13 +492,7 @@ class _CodeBlockWrapperState extends State<CodeBlockWrapper> {
margin: const EdgeInsets.symmetric(vertical: Spacing.xs), margin: const EdgeInsets.symmetric(vertical: Spacing.xs),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppBorderRadius.md), borderRadius: BorderRadius.circular(AppBorderRadius.md),
boxShadow: const [ boxShadow: ConduitShadows.medium(context),
BoxShadow(
color: Color(0x33000000),
blurRadius: 14,
offset: Offset(0, 10),
),
],
border: Border.all(color: borderColor, width: BorderWidth.micro), border: Border.all(color: borderColor, width: BorderWidth.micro),
), ),
child: ClipRRect( child: ClipRRect(
@@ -514,7 +512,7 @@ class _CodeBlockWrapperState extends State<CodeBlockWrapper> {
Text( Text(
languageLabel, languageLabel,
style: AppTypography.bodySmallStyle.copyWith( style: AppTypography.bodySmallStyle.copyWith(
color: Colors.white.withValues(alpha: 0.85), color: conduitTheme.codeText.withValues(alpha: 0.85),
fontFamily: AppTypography.monospaceFontFamily, fontFamily: AppTypography.monospaceFontFamily,
), ),
), ),
@@ -531,17 +529,16 @@ class _CodeBlockWrapperState extends State<CodeBlockWrapper> {
onPressed: canCopy ? _handleCopy : null, onPressed: canCopy ? _handleCopy : null,
icon: Icon(icon, size: IconSize.sm), icon: Icon(icon, size: IconSize.sm),
color: canCopy color: canCopy
? Colors.white ? conduitTheme.codeText
: Colors.white.withValues(alpha: 0.5), : conduitTheme.codeText.withValues(alpha: 0.5),
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(Spacing.xs), padding: const EdgeInsets.all(Spacing.xs),
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: Colors.white.withValues( backgroundColor: conduitTheme.codeText.withValues(
alpha: canCopy ? 0.08 : 0.04, alpha: canCopy ? 0.08 : 0.04,
), ),
disabledBackgroundColor: Colors.white.withValues( disabledBackgroundColor: conduitTheme.codeText
alpha: 0.03, .withValues(alpha: 0.03),
),
), ),
), ),
), ),
@@ -553,7 +550,7 @@ class _CodeBlockWrapperState extends State<CodeBlockWrapper> {
padding: const EdgeInsets.all(Spacing.sm), padding: const EdgeInsets.all(Spacing.sm),
child: DefaultTextStyle.merge( child: DefaultTextStyle.merge(
style: AppTypography.codeStyle.copyWith( style: AppTypography.codeStyle.copyWith(
color: const Color(0xFFE2E8F0), color: conduitTheme.codeText,
), ),
child: widget.child, child: widget.child,
), ),
@@ -571,11 +568,13 @@ class MermaidDiagram extends StatefulWidget {
required this.code, required this.code,
required this.brightness, required this.brightness,
required this.colorScheme, required this.colorScheme,
required this.tokens,
}); });
final String code; final String code;
final Brightness brightness; final Brightness brightness;
final ColorScheme colorScheme; final ColorScheme colorScheme;
final AppColorTokens tokens;
static bool get isSupported => !kIsWeb; static bool get isSupported => !kIsWeb;
@@ -625,7 +624,8 @@ class _MermaidDiagramState extends State<MermaidDiagram> {
final codeChanged = oldWidget.code != widget.code; final codeChanged = oldWidget.code != widget.code;
final themeChanged = final themeChanged =
oldWidget.brightness != widget.brightness || oldWidget.brightness != widget.brightness ||
oldWidget.colorScheme != widget.colorScheme; oldWidget.colorScheme != widget.colorScheme ||
oldWidget.tokens != widget.tokens;
if (codeChanged || themeChanged) { if (codeChanged || themeChanged) {
_loadHtml(); _loadHtml();
} }
@@ -654,10 +654,12 @@ class _MermaidDiagramState extends State<MermaidDiagram> {
String _buildHtml(String code, String script) { String _buildHtml(String code, String script) {
final theme = widget.brightness == Brightness.dark ? 'dark' : 'default'; final theme = widget.brightness == Brightness.dark ? 'dark' : 'default';
final encoded = jsonEncode(code); final encoded = jsonEncode(code);
final primary = _toHex(widget.colorScheme.primary); final primary = _toHex(widget.tokens.brandTone60);
final secondary = _toHex(widget.colorScheme.secondary); final secondary = _toHex(widget.tokens.accentTeal60);
final background = _toHex(widget.colorScheme.surface); final background = _toHex(widget.tokens.codeBackground);
final onBackground = _toHex(widget.colorScheme.onSurface); final onBackground = _toHex(widget.tokens.codeText);
final lineColor = _toHex(widget.tokens.codeAccent);
final errorColor = _toHex(widget.tokens.statusError60);
return ''' return '''
<!DOCTYPE html> <!DOCTYPE html>
@@ -688,7 +690,7 @@ $script
secondaryColor: '$secondary', secondaryColor: '$secondary',
background: '$background', background: '$background',
textColor: '$onBackground', textColor: '$onBackground',
lineColor: '$onBackground' lineColor: '$lineColor'
} }
}; };
@@ -702,7 +704,7 @@ $script
bindFunctions(target); bindFunctions(target);
} }
} catch (error) { } catch (error) {
target.innerHTML = '<pre style="color:#ef4444">' + String(error) + '</pre>'; target.innerHTML = '<pre style="color:$errorColor">' + String(error) + '</pre>';
console.error('Mermaid render failed', error); console.error('Mermaid render failed', error);
} }
})(); })();

View File

@@ -105,7 +105,7 @@ class _BackOnlineToast extends StatelessWidget {
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
AppBorderRadius.round, AppBorderRadius.round,
), ),
boxShadow: ConduitShadows.low, boxShadow: ConduitShadows.low(context),
), ),
child: Text( child: Text(
// Reuse existing l10n; otherwise add a dedicated "Back online" key later // Reuse existing l10n; otherwise add a dedicated "Back online" key later