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