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:
@@ -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) {
|
||||
|
||||
@@ -535,7 +535,7 @@ Future<void> _maybeShowOnboarding(Ref ref) async {
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(AppBorderRadius.modal),
|
||||
),
|
||||
boxShadow: ConduitShadows.modal,
|
||||
boxShadow: ConduitShadows.modal(context),
|
||||
),
|
||||
child: const OnboardingSheet(),
|
||||
),
|
||||
|
||||
@@ -116,13 +116,7 @@ class _ConnectionIssuePageState extends ConsumerState<ConnectionIssuePage> {
|
||||
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
|
||||
|
||||
@@ -214,7 +214,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
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<ChatPage> {
|
||||
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<ChatPage> {
|
||||
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<ChatPage> {
|
||||
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,
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1281,7 +1281,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
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<ModernChatInput>
|
||||
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<ModernChatInput>
|
||||
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<ModernChatInput>
|
||||
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,
|
||||
|
||||
@@ -563,13 +563,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
|
||||
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(
|
||||
|
||||
@@ -1362,7 +1362,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
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<BoxShadow> shadow = selected ? ConduitShadows.low : const [];
|
||||
final List<BoxShadow> shadow = selected
|
||||
? ConduitShadows.low(context)
|
||||
: const [];
|
||||
|
||||
Color? overlayForStates(Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
|
||||
@@ -85,7 +85,7 @@ class _OnboardingSheetState extends ConsumerState<OnboardingSheet> {
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: <ThemeExtension<dynamic>>[
|
||||
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: <ThemeExtension<dynamic>>[
|
||||
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: <TargetPlatform, PageTransitionsBuilder>{
|
||||
|
||||
461
lib/shared/theme/color_tokens.dart
Normal file
461
lib/shared/theme/color_tokens.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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<ConduitThemeExtension> {
|
||||
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<ConduitThemeExtension> {
|
||||
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<ConduitThemeExtension> {
|
||||
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<ConduitThemeExtension> {
|
||||
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<ConduitThemeExtension> {
|
||||
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<ConduitThemeExtension> {
|
||||
}
|
||||
|
||||
/// 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<ConduitThemeExtension> {
|
||||
}
|
||||
|
||||
/// 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<AppPaletteThemeExtension>()?.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<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,182 +998,153 @@ class Elevation {
|
||||
|
||||
/// Helper class for consistent shadows - Enhanced for production with better hierarchy
|
||||
class ConduitShadows {
|
||||
static List<BoxShadow> get low => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.08),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> low(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.08,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get medium => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.12),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> medium(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.12,
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get high => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.16),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> high(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.16,
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 8),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get glow => [
|
||||
BoxShadow(
|
||||
color: AppColorPalettes.auroraViolet.light.primary.withValues(
|
||||
alpha: 0.25,
|
||||
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(
|
||||
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<BoxShadow> get card => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.06),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 3),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> card(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.06,
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 3),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get button => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.1),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> button(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.1,
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get modal => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.2),
|
||||
blurRadius: 32,
|
||||
offset: const Offset(0, 12),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> modal(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.2,
|
||||
blurRadius: 32,
|
||||
offset: const Offset(0, 12),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get navigation => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.08),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, -2),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> navigation(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.08,
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, -2),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get messageBubble => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 1),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> messageBubble(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.04,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 1),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get input => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> 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<BoxShadow> get darkCard => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.3),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> pressed(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.15,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get darkModal => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.4),
|
||||
blurRadius: 40,
|
||||
offset: const Offset(0, 16),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> hover(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.12,
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
);
|
||||
|
||||
// Interactive shadows with better feedback
|
||||
static List<BoxShadow> get pressed => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.15),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> micro(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.04,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get hover => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.12),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> small(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.06,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
);
|
||||
|
||||
// Enhanced shadows for better visual hierarchy
|
||||
static List<BoxShadow> get micro => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.04),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> standard(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.08,
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 3),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get small => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.06),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> large(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.12,
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get standard => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.08),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 3),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> extraLarge(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.16,
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 8),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get large => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.12),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
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,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
static List<BoxShadow> 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
|
||||
|
||||
@@ -84,7 +84,7 @@ Future<void> showConduitContextMenu({
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaceBackground,
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
|
||||
boxShadow: ConduitShadows.modal,
|
||||
boxShadow: ConduitShadows.modal(context),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<CodeBlockWrapper> {
|
||||
|
||||
@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<CodeBlockWrapper> {
|
||||
? 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<CodeBlockWrapper> {
|
||||
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<CodeBlockWrapper> {
|
||||
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<CodeBlockWrapper> {
|
||||
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<CodeBlockWrapper> {
|
||||
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<MermaidDiagram> {
|
||||
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<MermaidDiagram> {
|
||||
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 '''
|
||||
<!DOCTYPE html>
|
||||
@@ -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 = '<pre style="color:#ef4444">' + String(error) + '</pre>';
|
||||
target.innerHTML = '<pre style="color:$errorColor">' + String(error) + '</pre>';
|
||||
console.error('Mermaid render failed', error);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user