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.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) {
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>{
|
||||||
|
|||||||
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';
|
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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user