diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 78272a5..9257d86 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -25,7 +25,7 @@ import '../services/optimized_storage_service.dart'; import '../services/socket_service.dart'; import '../utils/debug_logger.dart'; import '../models/socket_event.dart'; -import '../../shared/theme/color_palettes.dart'; +import '../../shared/theme/tweakcn_themes.dart'; import '../../shared/theme/app_theme.dart'; import '../../features/tools/providers/tools_providers.dart'; @@ -88,14 +88,14 @@ class AppThemePalette extends _$AppThemePalette { late final OptimizedStorageService _storage; @override - AppColorPalette build() { + TweakcnThemeDefinition build() { _storage = ref.watch(optimizedStorageServiceProvider); final storedId = _storage.getThemePaletteId(); - return AppColorPalettes.byId(storedId); + return TweakcnThemes.byId(storedId); } Future setPalette(String paletteId) async { - final palette = AppColorPalettes.byId(paletteId); + final palette = TweakcnThemes.byId(paletteId); state = palette; await _storage.setThemePaletteId(palette.id); } diff --git a/lib/core/services/enhanced_accessibility_service.dart b/lib/core/services/enhanced_accessibility_service.dart index 2448198..ce7d3f0 100644 --- a/lib/core/services/enhanced_accessibility_service.dart +++ b/lib/core/services/enhanced_accessibility_service.dart @@ -2,7 +2,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/semantics.dart'; -import '../../shared/theme/color_palettes.dart'; +import '../../shared/theme/tweakcn_themes.dart'; import '../../shared/theme/theme_extensions.dart'; /// Enhanced accessibility service for WCAG 2.2 AA compliance @@ -349,7 +349,7 @@ class EnhancedAccessibilityService { return BoxDecoration( border: hasFocus ? Border.all( - color: focusColor ?? AppColorPalettes.auroraViolet.light.primary, + color: focusColor ?? TweakcnThemes.t3Chat.light.primary, width: borderWidth, ) : null, diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 7fe556b..3cd9997 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -1217,12 +1217,15 @@ class _ChatPageState extends ConsumerState { .set(false); } catch (_) {} }, - drawer: SafeArea( - top: true, - bottom: true, - left: false, - right: false, - child: const ChatsDrawer(), + drawer: Container( + color: context.sidebarTheme.background, + child: SafeArea( + top: true, + bottom: true, + left: false, + right: false, + child: const ChatsDrawer(), + ), ), child: Scaffold( backgroundColor: context.conduitTheme.surfaceBackground, diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index dacd56c..ce61b0c 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -152,10 +152,13 @@ class _ChatsDrawerState extends ConsumerState { @override Widget build(BuildContext context) { // Bottom section now only shows navigation actions - final theme = context.conduitTheme; + final sidebarTheme = context.sidebarTheme; return Container( - color: theme.surfaceBackground, + decoration: BoxDecoration( + color: sidebarTheme.background, + border: Border(right: BorderSide(color: sidebarTheme.border)), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -169,7 +172,7 @@ class _ChatsDrawerState extends ConsumerState { child: Row(children: [Expanded(child: _buildSearchField(context))]), ), Expanded(child: _buildConversationList(context)), - Divider(height: 1, color: theme.dividerColor), + Divider(height: 1, color: sidebarTheme.border), _buildBottomSection(context), ], ), @@ -177,23 +180,23 @@ class _ChatsDrawerState extends ConsumerState { } Widget _buildSearchField(BuildContext context) { - final theme = context.conduitTheme; + final sidebarTheme = context.sidebarTheme; return Material( color: Colors.transparent, child: TextField( controller: _searchController, focusNode: _searchFocusNode, onChanged: (_) => _onSearchChanged(), - style: AppTypography.standard.copyWith(color: theme.inputText), + style: AppTypography.standard.copyWith(color: sidebarTheme.foreground), decoration: InputDecoration( isDense: true, hintText: AppLocalizations.of(context)!.searchConversations, hintStyle: AppTypography.standard.copyWith( - color: theme.inputPlaceholder, + color: sidebarTheme.foreground.withValues(alpha: 0.6), ), prefixIcon: Icon( Platform.isIOS ? CupertinoIcons.search : Icons.search, - color: theme.iconSecondary, + color: sidebarTheme.foreground.withValues(alpha: 0.7), size: IconSize.input, ), prefixIconConstraints: const BoxConstraints( @@ -211,7 +214,7 @@ class _ChatsDrawerState extends ConsumerState { Platform.isIOS ? CupertinoIcons.clear_circled_solid : Icons.clear, - color: theme.iconSecondary, + color: sidebarTheme.foreground.withValues(alpha: 0.7), size: IconSize.input, ), ) @@ -221,18 +224,18 @@ class _ChatsDrawerState extends ConsumerState { minHeight: TouchTarget.minimum, ), filled: true, - fillColor: theme.inputBackground, + fillColor: sidebarTheme.accent.withValues(alpha: 0.9), border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide(color: theme.inputBorder, width: 1), + borderSide: BorderSide(color: sidebarTheme.border, width: 1), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide(color: theme.buttonPrimary, width: 1), + borderSide: BorderSide(color: sidebarTheme.ring, width: 1.2), ), contentPadding: const EdgeInsets.symmetric( horizontal: Spacing.md, @@ -673,7 +676,7 @@ class _ChatsDrawerState extends ConsumerState { child: Text( 'Search failed', style: AppTypography.bodyMediumStyle.copyWith( - color: theme.textSecondary, + color: context.sidebarTheme.foreground.withValues(alpha: 0.7), ), ), ), @@ -682,13 +685,13 @@ class _ChatsDrawerState extends ConsumerState { } Widget _buildSectionHeader(String title, int count) { - final theme = context.conduitTheme; + final sidebarTheme = context.sidebarTheme; return Row( children: [ Text( title, style: AppTypography.labelStyle.copyWith( - color: theme.textSecondary, + color: sidebarTheme.foreground.withValues(alpha: 0.9), fontWeight: FontWeight.w600, decoration: TextDecoration.none, ), @@ -697,17 +700,17 @@ class _ChatsDrawerState extends ConsumerState { Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: theme.surfaceContainer.withValues(alpha: 0.4), + color: sidebarTheme.accent.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(AppBorderRadius.xs), border: Border.all( - color: theme.dividerColor.withValues(alpha: 0.5), + color: sidebarTheme.border.withValues(alpha: 0.6), width: BorderWidth.thin, ), ), child: Text( '$count', style: AppTypography.tiny.copyWith( - color: theme.textSecondary, + color: sidebarTheme.foreground.withValues(alpha: 0.8), decoration: TextDecoration.none, ), ), @@ -1432,6 +1435,7 @@ class _ChatsDrawerState extends ConsumerState { Widget _buildBottomSection(BuildContext context) { final theme = context.conduitTheme; + final sidebarTheme = context.sidebarTheme; final currentUserAsync = ref.watch(currentUserProvider); final userFromProfile = currentUserAsync.maybeWhen( data: (u) => u, @@ -1460,10 +1464,10 @@ class _ChatsDrawerState extends ConsumerState { Container( padding: const EdgeInsets.all(Spacing.sm), decoration: BoxDecoration( - color: theme.surfaceContainer.withValues(alpha: 0.3), + color: sidebarTheme.accent.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(AppBorderRadius.small), border: Border.all( - color: theme.dividerColor.withValues(alpha: 0.5), + color: sidebarTheme.border.withValues(alpha: 0.6), width: BorderWidth.standard, ), ), @@ -1497,7 +1501,7 @@ class _ChatsDrawerState extends ConsumerState { maxLines: 1, overflow: TextOverflow.ellipsis, style: AppTypography.bodySmallStyle.copyWith( - color: theme.textPrimary, + color: sidebarTheme.foreground, fontWeight: FontWeight.w600, decoration: TextDecoration.none, ), @@ -1514,7 +1518,7 @@ class _ChatsDrawerState extends ConsumerState { Platform.isIOS ? CupertinoIcons.settings : Icons.settings_rounded, - color: theme.iconSecondary, + color: sidebarTheme.foreground.withValues(alpha: 0.8), size: IconSize.medium, ), ), diff --git a/lib/features/profile/views/app_customization_page.dart b/lib/features/profile/views/app_customization_page.dart index 9ab4ea1..731aa71 100644 --- a/lib/features/profile/views/app_customization_page.dart +++ b/lib/features/profile/views/app_customization_page.dart @@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/services/settings_service.dart'; import '../../../shared/theme/theme_extensions.dart'; -import '../../../shared/theme/color_palettes.dart'; +import '../../../shared/theme/tweakcn_themes.dart'; import '../../tools/providers/tools_providers.dart'; import '../../../core/models/tool.dart'; import '../../../shared/widgets/conduit_components.dart'; @@ -38,10 +38,10 @@ class AppCustomizationPage extends ConsumerWidget { final locale = ref.watch(appLocaleProvider); final currentLanguageCode = locale?.languageCode ?? 'system'; final languageLabel = _resolveLanguageLabel(context, currentLanguageCode); - final activePalette = ref.watch(appThemePaletteProvider); + final activeTheme = ref.watch(appThemePaletteProvider); return Scaffold( - backgroundColor: context.conduitTheme.surfaceBackground, + backgroundColor: context.sidebarTheme.background, appBar: _buildAppBar(context), body: SafeArea( child: ListView( @@ -61,7 +61,7 @@ class AppCustomizationPage extends ConsumerWidget { currentLanguageCode, languageLabel, settings, - activePalette, + activeTheme, ), const SizedBox(height: Spacing.xl), _buildQuickPillsSection(context, ref, settings), @@ -78,7 +78,7 @@ class AppCustomizationPage extends ConsumerWidget { PreferredSizeWidget _buildAppBar(BuildContext context) { final canPop = ModalRoute.of(context)?.canPop ?? false; return AppBar( - backgroundColor: context.conduitTheme.surfaceBackground, + backgroundColor: context.sidebarTheme.background, surfaceTintColor: Colors.transparent, elevation: Elevation.none, toolbarHeight: kToolbarHeight, @@ -116,7 +116,7 @@ class AppCustomizationPage extends ConsumerWidget { String currentLanguageCode, String languageLabel, AppSettings settings, - AppColorPalette palette, + TweakcnThemeDefinition activeTheme, ) { final theme = context.conduitTheme; @@ -126,13 +126,13 @@ class AppCustomizationPage extends ConsumerWidget { Text( AppLocalizations.of(context)!.display, style: - theme.headingSmall?.copyWith(color: theme.textPrimary) ?? - TextStyle(color: theme.textPrimary, fontSize: 18), + theme.headingSmall?.copyWith(color: theme.sidebarForeground) ?? + TextStyle(color: theme.sidebarForeground, fontSize: 18), ), const SizedBox(height: Spacing.sm), _buildThemeSelector(context, ref, themeMode, themeDescription), const SizedBox(height: Spacing.md), - _buildPaletteSelector(context, ref, palette), + _buildPaletteSelector(context, ref, activeTheme), const SizedBox(height: Spacing.md), _CustomizationTile( leading: _buildIconBadge( @@ -196,7 +196,7 @@ class AppCustomizationPage extends ConsumerWidget { Text( AppLocalizations.of(context)!.darkMode, style: theme.bodyMedium?.copyWith( - color: theme.textPrimary, + color: theme.sidebarForeground, fontWeight: FontWeight.w600, ), ), @@ -204,7 +204,7 @@ class AppCustomizationPage extends ConsumerWidget { Text( themeDescription, style: theme.bodySmall?.copyWith( - color: theme.textSecondary, + color: theme.sidebarForeground.withValues(alpha: 0.75), ), ), ], @@ -260,10 +260,10 @@ class AppCustomizationPage extends ConsumerWidget { Widget _buildPaletteSelector( BuildContext context, WidgetRef ref, - AppColorPalette activePalette, + TweakcnThemeDefinition activeTheme, ) { final theme = context.conduitTheme; - final palettes = AppColorPalettes.all; + final palettes = TweakcnThemes.all; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -272,17 +272,22 @@ class AppCustomizationPage extends ConsumerWidget { AppLocalizations.of(context)!.themePalette, style: theme.bodyLarge?.copyWith( - color: theme.textPrimary, + color: theme.sidebarForeground, fontWeight: FontWeight.w600, ) ?? - TextStyle(color: theme.textPrimary, fontWeight: FontWeight.w600), + TextStyle( + color: theme.sidebarForeground, + fontWeight: FontWeight.w600, + ), ), const SizedBox(height: Spacing.xs), Text( AppLocalizations.of(context)!.themePaletteDescription, style: - theme.bodySmall?.copyWith(color: theme.textSecondary) ?? - TextStyle(color: theme.textSecondary), + theme.bodySmall?.copyWith( + color: theme.sidebarForeground.withValues(alpha: 0.75), + ) ?? + TextStyle(color: theme.sidebarForeground.withValues(alpha: 0.75)), ), const SizedBox(height: Spacing.sm), ConduitCard( @@ -291,8 +296,8 @@ class AppCustomizationPage extends ConsumerWidget { children: [ for (final palette in palettes) _PaletteOption( - palette: palette, - activeId: activePalette.id, + themeDefinition: palette, + activeId: activeTheme.id, onSelect: () => ref .read(appThemePaletteProvider.notifier) .setPalette(palette.id), @@ -378,8 +383,8 @@ class AppCustomizationPage extends ConsumerWidget { Text( AppLocalizations.of(context)!.onboardQuickTitle, style: - theme.headingSmall?.copyWith(color: theme.textPrimary) ?? - TextStyle(color: theme.textPrimary, fontSize: 18), + theme.headingSmall?.copyWith(color: theme.sidebarForeground) ?? + TextStyle(color: theme.sidebarForeground, fontSize: 18), ), const SizedBox(height: Spacing.sm), ConduitCard( @@ -403,7 +408,7 @@ class AppCustomizationPage extends ConsumerWidget { child: Text( AppLocalizations.of(context)!.quickActionsDescription, style: theme.bodySmall?.copyWith( - color: theme.textSecondary, + color: theme.sidebarForeground.withValues(alpha: 0.75), ), ), ), @@ -461,8 +466,8 @@ class AppCustomizationPage extends ConsumerWidget { Text( l10n.chatSettings, style: - theme.headingSmall?.copyWith(color: theme.textPrimary) ?? - TextStyle(color: theme.textPrimary, fontSize: 18), + theme.headingSmall?.copyWith(color: theme.sidebarForeground) ?? + TextStyle(color: theme.sidebarForeground, fontSize: 18), ), const SizedBox(height: Spacing.sm), _CustomizationTile( @@ -500,8 +505,8 @@ class AppCustomizationPage extends ConsumerWidget { Text( l10n.ttsSettings, style: - theme.headingSmall?.copyWith(color: theme.textPrimary) ?? - TextStyle(color: theme.textPrimary, fontSize: 18), + theme.headingSmall?.copyWith(color: theme.sidebarForeground) ?? + TextStyle(color: theme.sidebarForeground, fontSize: 18), ), const SizedBox(height: Spacing.sm), // Voice Selection @@ -621,20 +626,23 @@ class AppCustomizationPage extends ConsumerWidget { title, style: theme.bodyMedium?.copyWith( - color: theme.textPrimary, + color: theme.sidebarForeground, fontWeight: FontWeight.w500, ) ?? - TextStyle(color: theme.textPrimary, fontSize: 14), + TextStyle(color: theme.sidebarForeground, fontSize: 14), ), ), Text( label, style: theme.bodyMedium?.copyWith( - color: theme.textSecondary, + color: theme.sidebarForeground.withValues(alpha: 0.75), fontWeight: FontWeight.w500, ) ?? - TextStyle(color: theme.textSecondary, fontSize: 14), + TextStyle( + color: theme.sidebarForeground.withValues(alpha: 0.75), + fontSize: 14, + ), ), ], ), @@ -718,7 +726,7 @@ class AppCustomizationPage extends ConsumerWidget { showModalBottomSheet( context: context, - backgroundColor: theme.surfaceBackground, + backgroundColor: theme.sidebarBackground, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), @@ -741,10 +749,10 @@ class AppCustomizationPage extends ConsumerWidget { l10n.ttsSelectVoice, style: theme.headingSmall?.copyWith( - color: theme.textPrimary, + color: theme.sidebarForeground, ) ?? TextStyle( - color: theme.textPrimary, + color: theme.sidebarForeground, fontSize: 18, fontWeight: FontWeight.bold, ), @@ -765,18 +773,18 @@ class AppCustomizationPage extends ConsumerWidget { ios: CupertinoIcons.speaker_3, android: Icons.record_voice_over, ), - color: theme.textPrimary, + color: theme.sidebarForeground, ), title: Text( l10n.ttsSystemDefault, style: theme.bodyMedium?.copyWith( - color: theme.textPrimary, + color: theme.sidebarForeground, fontWeight: settings.ttsVoice == null ? FontWeight.bold : FontWeight.normal, ) ?? - TextStyle(color: theme.textPrimary), + TextStyle(color: theme.sidebarForeground), ), trailing: settings.ttsVoice == null ? Icon(Icons.check, color: theme.buttonPrimary) @@ -809,11 +817,15 @@ class AppCustomizationPage extends ConsumerWidget { ), style: theme.bodySmall?.copyWith( - color: theme.textSecondary, + color: theme.sidebarForeground.withValues( + alpha: 0.75, + ), fontWeight: FontWeight.bold, ) ?? TextStyle( - color: theme.textSecondary, + color: theme.sidebarForeground.withValues( + alpha: 0.75, + ), fontSize: 12, fontWeight: FontWeight.bold, ), @@ -831,11 +843,15 @@ class AppCustomizationPage extends ConsumerWidget { l10n.ttsOtherVoices, style: theme.bodySmall?.copyWith( - color: theme.textSecondary, + color: theme.sidebarForeground.withValues( + alpha: 0.75, + ), fontWeight: FontWeight.bold, ) ?? TextStyle( - color: theme.textSecondary, + color: theme.sidebarForeground.withValues( + alpha: 0.75, + ), fontSize: 12, fontWeight: FontWeight.bold, ), @@ -866,28 +882,32 @@ class AppCustomizationPage extends ConsumerWidget { ios: CupertinoIcons.person_fill, android: Icons.person, ), - color: theme.textPrimary, + color: theme.sidebarForeground, ), title: Text( displayName, style: theme.bodyMedium?.copyWith( - color: theme.textPrimary, + color: theme.sidebarForeground, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ) ?? - TextStyle(color: theme.textPrimary), + TextStyle(color: theme.sidebarForeground), ), subtitle: subtitle.isNotEmpty ? Text( subtitle, style: theme.bodySmall?.copyWith( - color: theme.textSecondary, + color: theme.sidebarForeground.withValues( + alpha: 0.75, + ), ) ?? TextStyle( - color: theme.textSecondary, + color: theme.sidebarForeground.withValues( + alpha: 0.75, + ), fontSize: 12, ), ) @@ -1162,7 +1182,7 @@ class AppCustomizationPage extends ConsumerWidget { isScrollControlled: true, builder: (context) => Container( decoration: BoxDecoration( - color: context.conduitTheme.surfaceBackground, + color: context.sidebarTheme.background, borderRadius: const BorderRadius.vertical( top: Radius.circular(AppBorderRadius.modal), ), @@ -1230,26 +1250,20 @@ class AppCustomizationPage extends ConsumerWidget { class _PaletteOption extends StatelessWidget { const _PaletteOption({ - required this.palette, + required this.themeDefinition, required this.activeId, required this.onSelect, }); - final AppColorPalette palette; + final TweakcnThemeDefinition themeDefinition; final String activeId; final VoidCallback onSelect; @override Widget build(BuildContext context) { final theme = context.conduitTheme; - final isSelected = palette.id == activeId; - final previewColors = - palette.preview ?? - [ - palette.light.primary, - palette.light.secondary, - palette.dark.primary, - ]; + final isSelected = themeDefinition.id == activeId; + final previewColors = themeDefinition.preview; return InkWell( onTap: onSelect, @@ -1274,9 +1288,9 @@ class _PaletteOption extends StatelessWidget { children: [ Expanded( child: Text( - palette.label, + themeDefinition.label, style: theme.bodyMedium?.copyWith( - color: theme.textPrimary, + color: theme.sidebarForeground, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, @@ -1297,10 +1311,18 @@ class _PaletteOption extends StatelessWidget { ), const SizedBox(height: Spacing.xxs), Text( - palette.description, + themeDefinition.description, style: - theme.bodySmall?.copyWith(color: theme.textSecondary) ?? - TextStyle(color: theme.textSecondary), + theme.bodySmall?.copyWith( + color: theme.sidebarForeground.withValues( + alpha: 0.75, + ), + ) ?? + TextStyle( + color: theme.sidebarForeground.withValues( + alpha: 0.75, + ), + ), ), const SizedBox(height: Spacing.xs), Row( @@ -1378,14 +1400,16 @@ class _CustomizationTile extends StatelessWidget { Text( title, style: theme.bodyMedium?.copyWith( - color: theme.textPrimary, + color: theme.sidebarForeground, fontWeight: FontWeight.w600, ), ), const SizedBox(height: Spacing.xs), Text( subtitle, - style: theme.bodySmall?.copyWith(color: theme.textSecondary), + style: theme.bodySmall?.copyWith( + color: theme.sidebarForeground.withValues(alpha: 0.75), + ), ), ], ), diff --git a/lib/features/profile/views/profile_page.dart b/lib/features/profile/views/profile_page.dart index 4dc0f3a..d244fb7 100644 --- a/lib/features/profile/views/profile_page.dart +++ b/lib/features/profile/views/profile_page.dart @@ -79,7 +79,7 @@ class ProfilePage extends ConsumerWidget { Scaffold _buildScaffold(BuildContext context, {required Widget body}) { return Scaffold( - backgroundColor: context.conduitTheme.surfaceBackground, + backgroundColor: context.sidebarTheme.background, appBar: _buildAppBar(context), body: body, ); @@ -88,7 +88,7 @@ class ProfilePage extends ConsumerWidget { PreferredSizeWidget _buildAppBar(BuildContext context) { final canPop = ModalRoute.of(context)?.canPop ?? false; return AppBar( - backgroundColor: context.conduitTheme.surfaceBackground, + backgroundColor: context.sidebarTheme.background, surfaceTintColor: Colors.transparent, elevation: Elevation.none, toolbarHeight: kToolbarHeight, @@ -156,8 +156,10 @@ class ProfilePage extends ConsumerWidget { Widget _buildSupportSection(BuildContext context) { final theme = context.conduitTheme; final textTheme = - theme.bodySmall?.copyWith(color: theme.textSecondary) ?? - TextStyle(color: theme.textSecondary); + theme.bodySmall?.copyWith( + color: theme.sidebarForeground.withValues(alpha: 0.75), + ) ?? + TextStyle(color: theme.sidebarForeground.withValues(alpha: 0.75)); final supportTiles = [ _buildSupportOption( @@ -189,7 +191,7 @@ class ProfilePage extends ConsumerWidget { children: [ Text( AppLocalizations.of(context)!.supportConduit, - style: theme.headingSmall?.copyWith(color: theme.textPrimary), + style: theme.headingSmall?.copyWith(color: theme.sidebarForeground), ), const SizedBox(height: Spacing.xs), Text( @@ -216,7 +218,6 @@ class ProfilePage extends ConsumerWidget { final theme = context.conduitTheme; return _ProfileSettingTile( onTap: () => _openExternalLink(context, url), - isDestructive: false, leading: _buildIconBadge(context, icon, color: color), title: title, subtitle: subtitle, @@ -287,15 +288,14 @@ class ProfilePage extends ConsumerWidget { final email = extractEmail(user) ?? 'No email'; final theme = context.conduitTheme; - final accent = theme.buttonPrimary; return Container( padding: const EdgeInsets.all(Spacing.md), decoration: BoxDecoration( - color: accent.withValues(alpha: 0.08), + color: theme.sidebarAccent.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(AppBorderRadius.large), border: Border.all( - color: accent.withValues(alpha: 0.15), + color: theme.sidebarBorder.withValues(alpha: 0.6), width: BorderWidth.thin, ), ), @@ -314,7 +314,7 @@ class ProfilePage extends ConsumerWidget { Text( displayName, style: theme.headingMedium?.copyWith( - color: theme.textPrimary, + color: theme.sidebarForeground, fontWeight: FontWeight.w600, ), ), @@ -327,14 +327,18 @@ class ProfilePage extends ConsumerWidget { android: Icons.mail_outline, ), size: IconSize.small, - color: theme.textSecondary, + color: theme.sidebarForeground.withValues( + alpha: 0.75, + ), ), const SizedBox(width: Spacing.xs), Flexible( child: Text( email, style: theme.bodySmall?.copyWith( - color: theme.textSecondary, + color: theme.sidebarForeground.withValues( + alpha: 0.75, + ), ), overflow: TextOverflow.ellipsis, maxLines: 1, @@ -377,7 +381,6 @@ class ProfilePage extends ConsumerWidget { title: AppLocalizations.of(context)!.signOut, subtitle: AppLocalizations.of(context)!.endYourSession, onTap: () => _signOut(context, ref), - isDestructive: true, showChevron: false, ), ]; @@ -399,14 +402,12 @@ class ProfilePage extends ConsumerWidget { required String title, required String subtitle, required VoidCallback onTap, - bool isDestructive = false, bool showChevron = true, }) { final theme = context.conduitTheme; - final color = isDestructive ? theme.error : theme.buttonPrimary; + final color = theme.buttonPrimary; return _ProfileSettingTile( onTap: onTap, - isDestructive: isDestructive, leading: _buildIconBadge(context, icon, color: color), title: title, subtitle: subtitle, @@ -456,7 +457,7 @@ class ProfilePage extends ConsumerWidget { width: 40, height: 40, decoration: BoxDecoration( - color: theme.surfaceBackground.withValues(alpha: 0.85), + color: theme.sidebarAccent.withValues(alpha: 0.8), borderRadius: BorderRadius.circular(AppBorderRadius.small), border: Border.all( color: theme.cardBorder, @@ -518,11 +519,10 @@ class ProfilePage extends ConsumerWidget { ios: CupertinoIcons.exclamationmark_triangle, android: Icons.error_outline, ), - color: context.conduitTheme.error, + color: Colors.red, ), title: AppLocalizations.of(context)!.defaultModel, subtitle: AppLocalizations.of(context)!.failedToLoadModels, - isDestructive: true, showChevron: false, onTap: () => ref.invalidate(modelsProvider), trailing: IconButton( @@ -533,7 +533,7 @@ class ProfilePage extends ConsumerWidget { ios: CupertinoIcons.refresh, android: Icons.refresh, ), - color: context.conduitTheme.error, + color: Colors.red, size: IconSize.small, ), ), @@ -589,11 +589,11 @@ class ProfilePage extends ConsumerWidget { context: context, builder: (ctx) { return AlertDialog( - backgroundColor: ctx.conduitTheme.surfaceBackground, + backgroundColor: ctx.sidebarTheme.background, title: Text( AppLocalizations.of(ctx)!.aboutConduit, style: ctx.conduitTheme.headingSmall?.copyWith( - color: ctx.conduitTheme.textPrimary, + color: ctx.sidebarTheme.foreground, ), ), content: Column( @@ -605,7 +605,7 @@ class ProfilePage extends ConsumerWidget { ctx, )!.versionLabel(info.version, info.buildNumber), style: ctx.conduitTheme.bodyMedium?.copyWith( - color: ctx.conduitTheme.textSecondary, + color: ctx.sidebarTheme.foreground.withValues(alpha: 0.75), ), ), const SizedBox(height: Spacing.md), @@ -704,7 +704,6 @@ class _ProfileSettingTile extends StatelessWidget { required this.subtitle, this.onTap, this.trailing, - this.isDestructive = false, this.showChevron = true, }); @@ -713,16 +712,13 @@ class _ProfileSettingTile extends StatelessWidget { final String subtitle; final VoidCallback? onTap; final Widget? trailing; - final bool isDestructive; final bool showChevron; @override Widget build(BuildContext context) { final theme = context.conduitTheme; - final textColor = isDestructive ? theme.error : theme.textPrimary; - final subtitleColor = isDestructive - ? theme.error.withValues(alpha: 0.85) - : theme.textSecondary; + final textColor = theme.sidebarForeground; + final subtitleColor = theme.sidebarForeground.withValues(alpha: 0.75); return ConduitCard( padding: const EdgeInsets.all(Spacing.md), @@ -888,7 +884,7 @@ class _DefaultModelBottomSheetState builder: (context, scrollController) { return Container( decoration: BoxDecoration( - color: context.conduitTheme.surfaceBackground, + color: context.sidebarTheme.background, borderRadius: const BorderRadius.vertical( top: Radius.circular(AppBorderRadius.bottomSheet), ), @@ -1010,8 +1006,9 @@ class _DefaultModelBottomSheetState vertical: 2, ), decoration: BoxDecoration( - color: context.conduitTheme.surfaceBackground - .withValues(alpha: 0.6), + color: context.sidebarTheme.background.withValues( + alpha: 0.6, + ), borderRadius: BorderRadius.circular( AppBorderRadius.xs, ), @@ -1141,7 +1138,7 @@ class _DefaultModelBottomSheetState decoration: BoxDecoration( color: isSelected ? context.conduitTheme.buttonPrimary.withValues(alpha: 0.1) - : context.conduitTheme.surfaceBackground.withValues(alpha: 0.05), + : context.sidebarTheme.background.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(AppBorderRadius.small), border: Border.all( color: isSelected diff --git a/lib/shared/services/brand_service.dart b/lib/shared/services/brand_service.dart index 55d7889..0a17431 100644 --- a/lib/shared/services/brand_service.dart +++ b/lib/shared/services/brand_service.dart @@ -3,7 +3,7 @@ import '../theme/theme_extensions.dart'; import 'package:flutter/cupertino.dart'; import 'dart:io' show Platform; import '../theme/color_tokens.dart'; -import '../theme/color_palettes.dart'; +import '../theme/tweakcn_themes.dart'; /// Centralized service for consistent brand identity throughout the app /// Uses the hub icon as the primary brand element @@ -27,7 +27,7 @@ class BrandService { }) { final palette = _resolvePalette(context); final resolvedBrightness = brightness ?? _resolveBrightness(context); - return palette.primaryFor(resolvedBrightness); + return palette.variantFor(resolvedBrightness).primary; } static Color secondaryBrandColor({ @@ -36,7 +36,7 @@ class BrandService { }) { final palette = _resolvePalette(context); final resolvedBrightness = brightness ?? _resolveBrightness(context); - return palette.secondaryFor(resolvedBrightness); + return palette.variantFor(resolvedBrightness).secondary; } static Color accentBrandColor({ @@ -45,7 +45,7 @@ class BrandService { }) { final palette = _resolvePalette(context); final resolvedBrightness = brightness ?? _resolveBrightness(context); - return palette.accentFor(resolvedBrightness); + return palette.variantFor(resolvedBrightness).accent; } /// Creates a branded icon with consistent styling @@ -327,12 +327,12 @@ class BrandService { ); } - static AppColorPalette _resolvePalette(BuildContext? context) { + static TweakcnThemeDefinition _resolvePalette(BuildContext? context) { if (context == null) { - return AppColorPalettes.auroraViolet; + return TweakcnThemes.t3Chat; } final extension = Theme.of(context).extension(); - return extension?.palette ?? AppColorPalettes.auroraViolet; + return extension?.palette ?? TweakcnThemes.t3Chat; } static Brightness _resolveBrightness(BuildContext? context) { @@ -343,7 +343,7 @@ class BrandService { final palette = _resolvePalette(context); final brightness = _resolveBrightness(context); return brightness == Brightness.dark - ? AppColorTokens.dark(palette: palette) - : AppColorTokens.light(palette: palette); + ? AppColorTokens.dark(theme: palette) + : AppColorTokens.light(theme: palette); } } diff --git a/lib/shared/theme/app_theme.dart b/lib/shared/theme/app_theme.dart index 9937ecb..d7f9684 100644 --- a/lib/shared/theme/app_theme.dart +++ b/lib/shared/theme/app_theme.dart @@ -4,255 +4,251 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'theme_extensions.dart'; -import 'color_palettes.dart'; +import 'tweakcn_themes.dart'; import 'color_tokens.dart'; class AppTheme { - // Enhanced neutral palette for better contrast (WCAG AA compliant) - static const Color neutral900 = Color(0xFF0B0E14); - static const Color neutral800 = Color(0xFF161B24); - static const Color neutral700 = Color(0xFF1F2531); - static const Color neutral600 = Color(0xFF343C4D); - static const Color neutral500 = Color(0xFF4A5161); - static const Color neutral400 = Color(0xFF9099AC); - static const Color neutral300 = Color(0xFFC5CCD9); - static const Color neutral200 = Color(0xFFE6EAF1); - static const Color neutral100 = Color(0xFFF5F7FA); - static const Color neutral50 = Color(0xFFFFFFFF); - - // Semantic colors derived from the token specification - static const Color error = Color(0xFFCE2C31); - static const Color errorDark = Color(0xFFFF5F67); - static const Color success = Color(0xFF0E9D58); - static const Color successDark = Color(0xFF23C179); - static const Color warning = Color(0xFFDB7900); - static const Color warningDark = Color(0xFFFF9800); - static const Color info = Color(0xFF0174D3); - static const Color infoDark = Color(0xFF4CA8FF); - - static ThemeData light(AppColorPalette palette) { - final lightTone = palette.light; - final tokens = AppColorTokens.light(palette: palette); - final colorScheme = tokens.toColorScheme().copyWith( - primary: lightTone.primary, - onPrimary: _pickOnColor(lightTone.primary, tokens), - secondary: lightTone.secondary, - onSecondary: _pickOnColor(lightTone.secondary, tokens), - tertiary: lightTone.accent, - onTertiary: _pickOnColor(lightTone.accent, tokens), - surfaceTint: lightTone.primary, - ); - - return ThemeData( - useMaterial3: true, + static ThemeData light(TweakcnThemeDefinition theme) { + final tokens = AppColorTokens.light(theme: theme); + return _buildTheme( + theme: theme, + tokens: tokens, brightness: Brightness.light, - colorScheme: colorScheme, - pageTransitionsTheme: _pageTransitionsTheme, - splashFactory: NoSplash.splashFactory, - scaffoldBackgroundColor: tokens.neutralTone10, - appBarTheme: AppBarTheme( - centerTitle: true, - elevation: Elevation.none, - backgroundColor: Colors.transparent, - foregroundColor: tokens.neutralOnSurface, - ), - bottomSheetTheme: BottomSheetThemeData( - backgroundColor: tokens.neutralTone00, - modalBackgroundColor: tokens.neutralTone00, - 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, - ), - backgroundColor: lightTone.primary, - foregroundColor: _pickOnColor(lightTone.primary, tokens), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - ), - ), - ), - cardTheme: CardThemeData( - elevation: Elevation.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.lg), - side: BorderSide(color: tokens.neutralTone20), - ), - ), - snackBarTheme: SnackBarThemeData( - behavior: SnackBarBehavior.floating, - backgroundColor: Color.alphaBlend( - tokens.overlayStrong, - tokens.neutralOnSurface, - ), - contentTextStyle: TextStyle( - color: tokens.neutralTone00, - fontSize: AppTypography.bodyMedium, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.snackbar), - ), - elevation: Elevation.high, - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: tokens.neutralTone00, - 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: BorderSide(color: lightTone.primary, width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide(color: tokens.statusError60, width: 1), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - ), - textTheme: ThemeData.light().textTheme, - extensions: >[ - tokens, - ConduitThemeExtension.lightPalette(palette: palette, tokens: tokens), - AppPaletteThemeExtension(palette: palette), - ], ); } - static ThemeData dark(AppColorPalette palette) { - final darkTone = palette.dark; - final tokens = AppColorTokens.dark(palette: palette); - final colorScheme = tokens.toColorScheme().copyWith( - primary: darkTone.primary, - onPrimary: _pickOnColor(darkTone.primary, tokens), - secondary: darkTone.secondary, - onSecondary: _pickOnColor(darkTone.secondary, tokens), - tertiary: darkTone.accent, - onTertiary: _pickOnColor(darkTone.accent, tokens), - surfaceTint: darkTone.primary, - ); - - return ThemeData( - useMaterial3: true, + static ThemeData dark(TweakcnThemeDefinition theme) { + final tokens = AppColorTokens.dark(theme: theme); + return _buildTheme( + theme: theme, + tokens: tokens, brightness: Brightness.dark, - colorScheme: colorScheme, - scaffoldBackgroundColor: tokens.neutralTone10, - pageTransitionsTheme: _pageTransitionsTheme, - splashFactory: NoSplash.splashFactory, - appBarTheme: AppBarTheme( - centerTitle: true, - elevation: Elevation.none, - backgroundColor: Colors.transparent, - foregroundColor: tokens.neutralOnSurface, - ), - bottomSheetTheme: BottomSheetThemeData( - backgroundColor: tokens.neutralTone00, - modalBackgroundColor: tokens.neutralTone00, - 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, - ), - backgroundColor: darkTone.primary, - foregroundColor: _pickOnColor(darkTone.primary, tokens), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - ), - ), - ), - cardTheme: CardThemeData( - elevation: Elevation.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.lg), - side: BorderSide(color: tokens.neutralTone40), - ), - ), - snackBarTheme: SnackBarThemeData( - behavior: SnackBarBehavior.floating, - backgroundColor: Color.alphaBlend( - tokens.overlayStrong, - tokens.neutralTone20, - ), - contentTextStyle: TextStyle( - color: tokens.neutralOnSurface, - fontSize: AppTypography.bodyMedium, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.snackbar), - ), - elevation: Elevation.high, - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: tokens.neutralTone20, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide(color: tokens.neutralTone40, width: 1), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide(color: tokens.neutralTone40, width: 1), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide(color: darkTone.primary, width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide(color: tokens.statusError60, width: 1), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - ), - textTheme: ThemeData.dark().textTheme, - extensions: >[ - tokens, - ConduitThemeExtension.darkPalette(palette: palette, tokens: tokens), - AppPaletteThemeExtension(palette: palette), - ], ); } static CupertinoThemeData cupertinoTheme( BuildContext context, - AppColorPalette palette, + TweakcnThemeDefinition theme, ) { final brightness = Theme.of(context).brightness; - final tone = palette.toneFor(brightness); + final variant = theme.variantFor(brightness); final tokens = brightness == Brightness.dark - ? AppColorTokens.dark(palette: palette) - : AppColorTokens.light(palette: palette); + ? AppColorTokens.dark(theme: theme) + : AppColorTokens.light(theme: theme); return CupertinoThemeData( brightness: brightness, - primaryColor: tone.primary, + primaryColor: variant.primary, scaffoldBackgroundColor: tokens.neutralTone10, barBackgroundColor: tokens.neutralTone10, ); } + static ThemeData _buildTheme({ + required TweakcnThemeDefinition theme, + required AppColorTokens tokens, + required Brightness brightness, + }) { + final variant = theme.variantFor(brightness); + final isDark = brightness == Brightness.dark; + final typography = TypographyThemeExtension.fromVariant(variant); + final surfaces = SurfaceThemeExtension.fromVariant(variant); + final shadows = ShadowThemeExtension.standard(); + final shapes = ShapeThemeExtension.fromVariant(variant); + final sidebar = SidebarThemeExtension.fromVariant(variant); + final conduitExtension = ConduitThemeExtension.create( + theme: theme, + tokens: tokens, + brightness: brightness, + typography: typography, + surfaces: surfaces, + shadows: shadows, + shapes: shapes, + ); + final colorScheme = tokens.toColorScheme().copyWith( + primary: variant.primary, + onPrimary: _pickOnColor(variant.primary, tokens), + secondary: variant.secondary, + onSecondary: _pickOnColor(variant.secondary, tokens), + tertiary: variant.accent, + onTertiary: _pickOnColor(variant.accent, tokens), + surfaceTint: variant.primary, + ); + + final OutlineInputBorder baseInputBorder = OutlineInputBorder( + borderRadius: shapes.medium, + borderSide: BorderSide( + color: isDark + ? Color.lerp(surfaces.border, surfaces.input, 0.6)! + : Color.lerp(surfaces.border, surfaces.input, 0.4)!, + width: 1, + ), + ); + + final TextTheme baseTextTheme = brightness == Brightness.dark + ? ThemeData.dark().textTheme + : ThemeData.light().textTheme; + final TextTheme textTheme = baseTextTheme.apply( + fontFamily: typography.primaryFont.isEmpty + ? null + : typography.primaryFont, + fontFamilyFallback: typography.primaryFallback.isEmpty + ? null + : typography.primaryFallback, + ); + + return ThemeData( + useMaterial3: true, + brightness: brightness, + fontFamily: typography.primaryFont.isEmpty + ? null + : typography.primaryFont, + fontFamilyFallback: typography.primaryFallback.isEmpty + ? null + : typography.primaryFallback, + colorScheme: colorScheme, + scaffoldBackgroundColor: surfaces.background, + canvasColor: surfaces.background, + pageTransitionsTheme: _pageTransitionsTheme, + splashFactory: NoSplash.splashFactory, + appBarTheme: AppBarTheme( + centerTitle: true, + elevation: Elevation.none, + backgroundColor: surfaces.background, + foregroundColor: tokens.neutralOnSurface, + ), + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: surfaces.card, + modalBackgroundColor: surfaces.card, + surfaceTintColor: surfaces.card, + shape: RoundedRectangleBorder(borderRadius: shapes.extraLarge), + showDragHandle: false, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.lg, + vertical: Spacing.xs, + ), + backgroundColor: variant.primary, + foregroundColor: _pickOnColor(variant.primary, tokens), + shape: RoundedRectangleBorder(borderRadius: shapes.medium), + elevation: Elevation.low, + shadowColor: shadows.shadowSm.first.color, + ), + ), + cardTheme: CardThemeData( + color: surfaces.card, + elevation: Elevation.low, + shape: RoundedRectangleBorder( + borderRadius: shapes.large, + side: BorderSide(color: surfaces.border), + ), + shadowColor: shadows.shadowSm.first.color, + surfaceTintColor: Colors.transparent, + ), + snackBarTheme: SnackBarThemeData( + behavior: SnackBarBehavior.floating, + backgroundColor: conduitExtension.statusPalette.info.base, + contentTextStyle: textTheme.bodyMedium?.copyWith( + color: conduitExtension.statusPalette.info.onBase, + ), + actionTextColor: conduitExtension.statusPalette.info.onBase, + shape: RoundedRectangleBorder(borderRadius: shapes.medium), + elevation: Elevation.low, + insetPadding: const EdgeInsets.all(Spacing.md), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: conduitExtension.inputBackground, + focusColor: surfaces.ring, + hoverColor: Color.alphaBlend( + shadows.shadowXs.first.color, + conduitExtension.inputBackground, + ), + border: baseInputBorder, + enabledBorder: baseInputBorder, + focusedBorder: baseInputBorder.copyWith( + borderSide: BorderSide(color: surfaces.ring, width: 2), + ), + errorBorder: baseInputBorder.copyWith( + borderSide: BorderSide(color: tokens.statusError60, width: 1), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), + ), + chipTheme: ChipThemeData( + shape: RoundedRectangleBorder(borderRadius: shapes.medium), + padding: const EdgeInsets.symmetric( + horizontal: Spacing.sm, + vertical: Spacing.xs, + ), + backgroundColor: Color.lerp(surfaces.card, surfaces.muted, 0.4)!, + disabledColor: Color.alphaBlend( + shadows.shadowXs.first.color, + surfaces.card, + ), + selectedColor: conduitExtension.statusPalette.success.background, + secondarySelectedColor: conduitExtension.statusPalette.info.background, + shadowColor: shadows.shadowSm.first.color, + selectedShadowColor: shadows.shadowSm.first.color, + brightness: brightness, + labelStyle: textTheme.bodySmall?.copyWith( + color: tokens.neutralOnSurface, + ), + secondaryLabelStyle: textTheme.bodySmall?.copyWith( + color: conduitExtension.statusPalette.info.onBase, + ), + side: BorderSide(color: surfaces.border), + ), + badgeTheme: BadgeThemeData( + backgroundColor: conduitExtension.statusPalette.info.base, + textColor: conduitExtension.statusPalette.info.onBase, + padding: const EdgeInsets.symmetric( + horizontal: Spacing.xs, + vertical: Spacing.xxs, + ), + largeSize: 24, + smallSize: 18, + ), + dialogTheme: DialogThemeData( + backgroundColor: surfaces.popover, + surfaceTintColor: Colors.transparent, + elevation: Elevation.medium, + shadowColor: shadows.shadowLg.first.color, + shape: RoundedRectangleBorder(borderRadius: shapes.large), + titleTextStyle: textTheme.titleLarge?.copyWith( + color: surfaces.popoverForeground, + ), + contentTextStyle: textTheme.bodyMedium?.copyWith( + color: tokens.neutralOnSurface, + ), + ), + listTileTheme: ListTileThemeData( + shape: RoundedRectangleBorder(borderRadius: shapes.medium), + tileColor: Color.lerp(surfaces.card, surfaces.muted, 0.25), + selectedTileColor: Color.alphaBlend( + conduitExtension.statusPalette.info.background, + surfaces.card, + ), + iconColor: tokens.neutralTone80, + textColor: tokens.neutralOnSurface, + ), + textTheme: textTheme, + extensions: >[ + tokens, + typography, + surfaces, + shadows, + shapes, + sidebar, + conduitExtension, + AppPaletteThemeExtension(palette: theme), + ], + ); + } + static Color _pickOnColor(Color background, AppColorTokens tokens) { final contrastOnLight = _contrastRatio(background, tokens.neutralTone00); final contrastOnDark = _contrastRatio(background, tokens.neutralOnSurface); diff --git a/lib/shared/theme/color_palettes.dart b/lib/shared/theme/color_palettes.dart deleted file mode 100644 index 934b660..0000000 --- a/lib/shared/theme/color_palettes.dart +++ /dev/null @@ -1,159 +0,0 @@ -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? preview; -} - -@immutable -class AppPaletteThemeExtension - extends ThemeExtension { - const AppPaletteThemeExtension({required this.palette}); - - final AppColorPalette palette; - - @override - AppPaletteThemeExtension copyWith({AppColorPalette? palette}) { - return AppPaletteThemeExtension(palette: palette ?? this.palette); - } - - @override - AppPaletteThemeExtension lerp( - covariant ThemeExtension? 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 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; - } -} diff --git a/lib/shared/theme/color_tokens.dart b/lib/shared/theme/color_tokens.dart index df5d96b..4a3a06f 100644 --- a/lib/shared/theme/color_tokens.dart +++ b/lib/shared/theme/color_tokens.dart @@ -2,7 +2,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; -import 'color_palettes.dart'; +import 'tweakcn_themes.dart'; /// Immutable set of semantic color tokens exposed through [ThemeExtension]. /// @@ -92,136 +92,118 @@ class AppColorTokens extends ThemeExtension { final Color codeText; final Color codeAccent; - factory AppColorTokens.light({AppColorPalette? palette}) { - return AppColorTokens._fromPalette( - palette ?? AppColorPalettes.auroraViolet, + factory AppColorTokens.light({TweakcnThemeDefinition? theme}) { + return AppColorTokens._fromTheme( + theme ?? TweakcnThemes.conduit, Brightness.light, ); } - factory AppColorTokens.dark({AppColorPalette? palette}) { - return AppColorTokens._fromPalette( - palette ?? AppColorPalettes.auroraViolet, + factory AppColorTokens.dark({TweakcnThemeDefinition? theme}) { + return AppColorTokens._fromTheme( + theme ?? TweakcnThemes.conduit, Brightness.dark, ); } - factory AppColorTokens._fromPalette( - AppColorPalette palette, + factory AppColorTokens._fromTheme( + TweakcnThemeDefinition theme, Brightness brightness, ) { - final AppPaletteTone tone = palette.toneFor(brightness); - + final TweakcnThemeVariant variant = theme.variantFor(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 ColorScheme seedScheme = ColorScheme.fromSeed( - seedColor: tone.primary, - brightness: brightness, + final Color neutralTone00 = variant.background; + final Color neutralTone20 = variant.card; + final Color neutralTone10 = mix(neutralTone00, neutralTone20, 0.5); + final Color neutralTone40 = variant.muted; + final Color neutralTone60 = mix( + variant.mutedForeground, + variant.foreground, + isLight ? 0.25 : 0.4, + ); + final Color neutralTone80 = mix( + variant.foreground, + isLight ? Colors.black : Colors.white, + isLight ? 0.06 : 0.3, + ); + final Color neutralOnSurface = _ensureContrast( + surface: neutralTone00, + foreground: variant.foreground, + minContrast: 4.5, ); - final Color brandTone60 = seedScheme.primary; - final Color brandOn60 = _preferredOnColor( - background: brandTone60, - light: neutralTone00, - dark: neutralOnSurface, + final Color brandTone60 = variant.primary; + final Color brandOn60 = _ensureContrast( + surface: brandTone60, + foreground: variant.primaryForeground, + ); + final Color brandTone90 = mix( + variant.primary, + neutralTone00, + isLight ? 0.7 : 0.3, + ); + final Color brandOn90 = _ensureContrast( + surface: brandTone90, + foreground: brandOn60, + ); + final Color brandTone40 = mix( + variant.primary, + neutralOnSurface, + isLight ? 0.35 : 0.55, ); - final Color brandTone90 = seedScheme.primaryContainer; - final Color brandOn90 = _preferredOnColor( - background: brandTone90, - light: neutralTone00, - dark: neutralOnSurface, + final Color accentIndigo60 = variant.secondary; + final Color accentOnIndigo60 = _ensureContrast( + surface: accentIndigo60, + foreground: variant.secondaryForeground, + ); + final Color accentTeal60 = variant.accent; + final Color accentGold60 = mix( + variant.accent, + isLight ? Colors.white : Colors.black, + isLight ? 0.18 : 0.24, ); - 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 statusError60 = variant.destructive; + final Color statusOnError60 = _ensureContrast( + surface: statusError60, + foreground: variant.destructiveForeground, + ); + final Color statusSuccess60 = variant.success; + final Color statusOnSuccess60 = _ensureContrast( + surface: statusSuccess60, + foreground: variant.successForeground, + ); + final Color statusWarning60 = variant.warning; + final Color statusOnWarning60 = _ensureContrast( + surface: statusWarning60, + foreground: variant.warningForeground, + ); + final Color statusInfo60 = variant.info; + final Color statusOnInfo60 = _ensureContrast( + surface: statusInfo60, + foreground: variant.infoForeground, ); - 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 overlayWeak = neutralOnSurface.withValues( + alpha: isLight ? 0.08 : 0.12, + ); + final Color overlayMedium = neutralOnSurface.withValues( + alpha: isLight ? 0.16 : 0.2, + ); + final Color overlayStrong = neutralOnSurface.withValues( + alpha: isLight ? 0.32 : 0.36, ); - final Color statusWarning60 = isLight - ? const Color(0xFFDB7900) - : const Color(0xFFFF9800); - final Color statusOnWarning60 = _preferredOnColor( - background: statusWarning60, - light: neutralTone00, - dark: neutralOnSurface, + final Color codeBackground = mix(variant.muted, neutralTone00, 0.5); + final Color codeBorder = mix(variant.border, neutralTone40, 0.6); + final Color codeText = _ensureContrast( + surface: codeBackground, + foreground: neutralOnSurface, + minContrast: 4.5, ); - - 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, - ); - - final Color codeBackground = isLight ? neutralTone10 : neutralTone00; - final Color codeBorder = isLight ? neutralTone20 : neutralTone40; - final Color codeText = neutralOnSurface; - final Color codeAccent = isLight - ? Color.alphaBlend(brandTone60.withValues(alpha: 0.14), codeBackground) - : Color.alphaBlend(brandTone40.withValues(alpha: 0.24), codeBackground); + final Color codeAccent = mix(variant.accent, variant.primary, 0.4); return AppColorTokens( brightness: brightness, @@ -406,7 +388,10 @@ class AppColorTokens extends ThemeExtension { secondary: accentIndigo60, onSecondary: accentOnIndigo60, tertiary: accentTeal60, - onTertiary: neutralTone00, + onTertiary: _ensureContrast( + surface: accentTeal60, + foreground: neutralTone00, + ), surface: neutralTone00, surfaceContainerLow: neutralTone10, surfaceContainerHighest: neutralTone20, @@ -433,20 +418,24 @@ class AppColorTokens extends ThemeExtension { : 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, + static Color _ensureContrast({ + required Color surface, + required Color foreground, + double minContrast = 4.5, }) { - final double lightContrast = _contrastRatio(background, light); - final double darkContrast = _contrastRatio(background, dark); - return lightContrast >= darkContrast ? light : dark; + if (_contrastRatio(surface, foreground) >= minContrast) { + return foreground; + } + final bool surfaceIsDark = surface.computeLuminance() < 0.5; + final Color target = surfaceIsDark ? Colors.white : Colors.black; + Color candidate = foreground; + for (var i = 0; i < 6; i++) { + candidate = Color.lerp(candidate, target, 0.3)!; + if (_contrastRatio(surface, candidate) >= minContrast) { + return candidate; + } + } + return target; } static double _contrastRatio(Color a, Color b) { diff --git a/lib/shared/theme/theme_extensions.dart b/lib/shared/theme/theme_extensions.dart index 763e8f7..c205a0f 100644 --- a/lib/shared/theme/theme_extensions.dart +++ b/lib/shared/theme/theme_extensions.dart @@ -2,806 +2,296 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; // Using system fonts; no GoogleFonts dependency required -import 'color_palettes.dart'; +import 'tweakcn_themes.dart'; import 'color_tokens.dart'; /// Extended theme data for consistent styling across the app @immutable class ConduitThemeExtension extends ThemeExtension { - // Chat-specific colors - final Color chatBubbleUser; - final Color chatBubbleAssistant; - final Color chatBubbleUserText; - final Color chatBubbleAssistantText; - final Color chatBubbleUserBorder; - final Color chatBubbleAssistantBorder; - - // Input and form colors - final Color inputBackground; - final Color inputBorder; - final Color inputBorderFocused; - final Color inputText; - final Color inputPlaceholder; - final Color inputError; - - // Card and surface colors - final Color cardBackground; - final Color cardBorder; - final Color cardShadow; - final Color surfaceBackground; - final Color surfaceContainer; - final Color surfaceContainerHighest; - - // Interactive element colors - final Color buttonPrimary; - final Color buttonPrimaryText; - final Color buttonSecondary; - final Color buttonSecondaryText; - final Color buttonDisabled; - final Color buttonDisabledText; - - // Status and feedback colors - final Color success; - final Color successBackground; - final Color error; - final Color errorBackground; - final Color warning; - final Color warningBackground; - final Color info; - final Color infoBackground; - - // Navigation and UI element colors - final Color dividerColor; - final Color navigationBackground; - final Color navigationSelected; - final Color navigationUnselected; - final Color navigationSelectedBackground; - - // Loading and animation colors - final Color shimmerBase; - final Color shimmerHighlight; - final Color loadingIndicator; - - // Markdown/code colors - final Color codeBackground; - final Color codeBorder; - final Color codeText; - final Color codeAccent; - - // Text colors - final Color textPrimary; - final Color textSecondary; - final Color textTertiary; - final Color textInverse; - final Color textDisabled; - - // Icon colors - final Color iconPrimary; - final Color iconSecondary; - final Color iconDisabled; - final Color iconInverse; - - // Typography styles - final TextStyle? headingLarge; - final TextStyle? headingMedium; - final TextStyle? headingSmall; - final TextStyle? bodyLarge; - final TextStyle? bodyMedium; - final TextStyle? bodySmall; - final TextStyle? caption; - final TextStyle? label; - final TextStyle? code; - - const ConduitThemeExtension({ - // Chat-specific colors - required this.chatBubbleUser, - required this.chatBubbleAssistant, - required this.chatBubbleUserText, - required this.chatBubbleAssistantText, - required this.chatBubbleUserBorder, - required this.chatBubbleAssistantBorder, - - // Input and form colors - required this.inputBackground, - required this.inputBorder, - required this.inputBorderFocused, - required this.inputText, - required this.inputPlaceholder, - required this.inputError, - - // Card and surface colors - required this.cardBackground, - required this.cardBorder, - required this.cardShadow, - required this.surfaceBackground, - required this.surfaceContainer, - required this.surfaceContainerHighest, - - // Interactive element colors - required this.buttonPrimary, - required this.buttonPrimaryText, - required this.buttonSecondary, - required this.buttonSecondaryText, - required this.buttonDisabled, - required this.buttonDisabledText, - - // Status and feedback colors - required this.success, - required this.successBackground, - required this.error, - required this.errorBackground, - required this.warning, - required this.warningBackground, - required this.info, - required this.infoBackground, - - // Navigation and UI element colors - required this.dividerColor, - required this.navigationBackground, - required this.navigationSelected, - required this.navigationUnselected, - required this.navigationSelectedBackground, - - // Loading and animation colors - required this.shimmerBase, - required this.shimmerHighlight, - required this.loadingIndicator, - - // Markdown/code colors - required this.codeBackground, - required this.codeBorder, - required this.codeText, - required this.codeAccent, - - // Text colors - required this.textPrimary, - required this.textSecondary, - required this.textTertiary, - required this.textInverse, - required this.textDisabled, - - // Icon colors - required this.iconPrimary, - required this.iconSecondary, - required this.iconDisabled, - required this.iconInverse, - - // Typography styles - this.headingLarge, - this.headingMedium, - this.headingSmall, - this.bodyLarge, - this.bodyMedium, - this.bodySmall, - this.caption, - this.label, - this.code, + const ConduitThemeExtension._({ + required this.tokens, + required this.variant, + required this.isDark, + required this.typography, + required this.surfaces, + required this.shadows, + required this.shapes, }); + factory ConduitThemeExtension.create({ + required TweakcnThemeDefinition theme, + required AppColorTokens tokens, + required Brightness brightness, + required TypographyThemeExtension typography, + required SurfaceThemeExtension surfaces, + required ShadowThemeExtension shadows, + required ShapeThemeExtension shapes, + }) { + return ConduitThemeExtension._( + tokens: tokens, + variant: theme.variantFor(brightness), + isDark: brightness == Brightness.dark, + typography: typography, + surfaces: surfaces, + shadows: shadows, + shapes: shapes, + ); + } + + final AppColorTokens tokens; + final TweakcnThemeVariant variant; + final bool isDark; + final TypographyThemeExtension typography; + final SurfaceThemeExtension surfaces; + final ShadowThemeExtension shadows; + final ShapeThemeExtension shapes; + + Color get chatBubbleUser => variant.primary; + Color get chatBubbleAssistant => + isDark ? tokens.neutralTone20 : tokens.neutralTone00; + Color get chatBubbleUserText => _onSurfaceColor(variant.primary); + Color get chatBubbleAssistantText => tokens.neutralOnSurface; + Color get chatBubbleUserBorder => variant.secondary; + Color get chatBubbleAssistantBorder => + isDark ? tokens.neutralTone40 : tokens.neutralTone20; + + Color get inputBackground => + Color.lerp(surfaces.background, surfaces.input, isDark ? 0.35 : 0.75)!; + Color get inputBorder => + Color.lerp(surfaces.border, surfaces.ring, isDark ? 0.4 : 0.2)!; + Color get inputBorderFocused => surfaces.ring; + Color get inputText => tokens.neutralOnSurface; + Color get inputPlaceholder => + isDark ? tokens.neutralTone80 : tokens.neutralTone60; + Color get inputError => tokens.statusError60; + + Color get cardBackground => surfaces.card; + Color get cardBorder => surfaces.border; + Color get cardShadow => shadows.shadowSm.first.color; + List get cardShadows => shadows.shadowSm; + List get popoverShadows => shadows.shadowLg; + List get overlayShadows => shadows.shadowXs; + + Color get surfaceBackground => surfaces.background; + Color get surfaceContainer => surfaces.container; + Color get surfaceContainerHighest => surfaces.containerHighest; + + Color get buttonPrimary => variant.primary; + Color get buttonPrimaryText => _onSurfaceColor(variant.primary); + Color get buttonSecondary => tokens.neutralTone20; + Color get buttonSecondaryText => tokens.neutralOnSurface; + Color get buttonDisabled => tokens.neutralTone40; + Color get buttonDisabledText => + isDark ? tokens.neutralTone80 : tokens.neutralTone60; + + StatusPalette get statusPalette => StatusPalette( + success: StatusColors( + base: tokens.statusSuccess60, + onBase: tokens.statusOnSuccess60, + background: _toneOverlay(tokens.statusSuccess60), + light: tokens.statusSuccess60, + dark: Color.alphaBlend( + tokens.statusSuccess60.withValues(alpha: 0.8), + tokens.neutralTone20, + ), + ), + warning: StatusColors( + base: tokens.statusWarning60, + onBase: tokens.statusOnWarning60, + background: _toneOverlay(tokens.statusWarning60), + light: tokens.statusWarning60, + dark: Color.alphaBlend( + tokens.statusWarning60.withValues(alpha: 0.8), + tokens.neutralTone20, + ), + ), + info: StatusColors( + base: tokens.statusInfo60, + onBase: tokens.statusOnInfo60, + background: _toneOverlay(tokens.statusInfo60), + light: tokens.statusInfo60, + dark: Color.alphaBlend( + tokens.statusInfo60.withValues(alpha: 0.8), + tokens.neutralTone20, + ), + ), + destructive: StatusColors( + base: tokens.statusError60, + onBase: tokens.statusOnError60, + background: _toneOverlay(tokens.statusError60), + light: tokens.statusError60, + dark: Color.alphaBlend( + tokens.statusError60.withValues(alpha: 0.8), + tokens.neutralTone20, + ), + ), + ); + + Color get success => statusPalette.success.base; + Color get successBackground => statusPalette.success.background; + Color get error => statusPalette.destructive.base; + Color get errorBackground => statusPalette.destructive.background; + Color get warning => statusPalette.warning.base; + Color get warningBackground => statusPalette.warning.background; + Color get info => statusPalette.info.base; + Color get infoBackground => statusPalette.info.background; + + Color get dividerColor => + isDark ? tokens.neutralTone40 : tokens.neutralTone20; + Color get navigationBackground => + isDark ? tokens.neutralTone10 : tokens.neutralTone00; + Color get navigationSelected => variant.primary; + Color get navigationUnselected => + isDark ? tokens.neutralTone80 : tokens.neutralTone60; + Color get navigationSelectedBackground => + _overlay(tokens.overlayMedium, surface: navigationBackground); + + Color get shimmerBase => + _overlay(tokens.overlayWeak, surface: tokens.neutralTone10); + Color get shimmerHighlight => isDark + ? _overlay(tokens.overlayMedium, surface: tokens.neutralTone20) + : tokens.neutralTone00; + Color get loadingIndicator => variant.primary; + + Color get codeBackground => tokens.codeBackground; + Color get codeBorder => tokens.codeBorder; + Color get codeText => tokens.codeText; + Color get codeAccent => tokens.codeAccent; + + Color get textPrimary => tokens.neutralOnSurface; + Color get textSecondary => tokens.neutralTone80; + Color get textTertiary => tokens.neutralTone60; + Color get textInverse => tokens.neutralTone00; + Color get textDisabled => + isDark ? tokens.neutralTone40 : tokens.neutralTone60; + + Color get iconPrimary => tokens.neutralOnSurface; + Color get iconSecondary => tokens.neutralTone80; + Color get iconDisabled => + isDark ? tokens.neutralTone40 : tokens.neutralTone60; + Color get iconInverse => tokens.neutralTone00; + + double get radiusSm => shapes.radiusSm; + double get radiusMd => shapes.radiusMd; + double get radiusLg => shapes.radiusLg; + double get radiusXl => shapes.radiusXl; + + Color get sidebarBackground => variant.sidebarBackground; + Color get sidebarForeground => variant.sidebarForeground; + Color get sidebarPrimary => variant.sidebarPrimary; + Color get sidebarPrimaryText => variant.sidebarPrimaryForeground; + Color get sidebarAccent => variant.sidebarAccent; + Color get sidebarAccentText => variant.sidebarAccentForeground; + Color get sidebarBorder => variant.sidebarBorder; + Color get sidebarRing => variant.sidebarRing; + + TextStyle? get headingLarge => TextStyle( + fontSize: AppTypography.displaySmall, + fontWeight: FontWeight.w700, + color: tokens.neutralOnSurface, + height: 1.2, + ); + + TextStyle? get headingMedium => TextStyle( + fontSize: AppTypography.headlineLarge, + fontWeight: FontWeight.w600, + color: tokens.neutralOnSurface, + height: 1.3, + ); + + TextStyle? get headingSmall => TextStyle( + fontSize: AppTypography.headlineSmall, + fontWeight: FontWeight.w600, + color: tokens.neutralOnSurface, + height: 1.4, + ); + + TextStyle? get bodyLarge => TextStyle( + fontSize: AppTypography.bodyLarge, + fontWeight: FontWeight.w400, + color: tokens.neutralOnSurface, + height: 1.5, + ); + + TextStyle? get bodyMedium => TextStyle( + fontSize: AppTypography.bodyMedium, + fontWeight: FontWeight.w400, + color: tokens.neutralOnSurface, + height: 1.5, + ); + + TextStyle? get bodySmall => TextStyle( + fontSize: AppTypography.bodySmall, + fontWeight: FontWeight.w400, + color: isDark ? tokens.neutralTone80 : tokens.neutralTone60, + height: 1.4, + ); + + TextStyle? get caption => TextStyle( + fontSize: AppTypography.labelMedium, + fontWeight: FontWeight.w500, + color: isDark ? tokens.neutralTone80 : tokens.neutralTone60, + height: 1.3, + letterSpacing: 0.5, + ); + + TextStyle? get label => TextStyle( + fontSize: AppTypography.labelLarge, + fontWeight: FontWeight.w500, + color: tokens.neutralTone80, + height: 1.3, + ); + + TextStyle? get code => TextStyle( + fontSize: AppTypography.bodySmall, + fontWeight: FontWeight.w400, + color: tokens.neutralOnSurface, + height: 1.4, + fontFamily: typography.monospaceFont, + fontFamilyFallback: typography.monospaceFallback, + ); + @override ConduitThemeExtension copyWith({ - // Chat-specific colors - Color? chatBubbleUser, - Color? chatBubbleAssistant, - Color? chatBubbleUserText, - Color? chatBubbleAssistantText, - Color? chatBubbleUserBorder, - Color? chatBubbleAssistantBorder, - - // Input and form colors - Color? inputBackground, - Color? inputBorder, - Color? inputBorderFocused, - Color? inputText, - Color? inputPlaceholder, - Color? inputError, - - // Card and surface colors - Color? cardBackground, - Color? cardBorder, - Color? cardShadow, - Color? surfaceBackground, - Color? surfaceContainer, - Color? surfaceContainerHighest, - - // Interactive element colors - Color? buttonPrimary, - Color? buttonPrimaryText, - Color? buttonSecondary, - Color? buttonSecondaryText, - Color? buttonDisabled, - Color? buttonDisabledText, - - // Status and feedback colors - Color? success, - Color? successBackground, - Color? error, - Color? errorBackground, - Color? warning, - Color? warningBackground, - Color? info, - Color? infoBackground, - - // Navigation and UI element colors - Color? dividerColor, - Color? navigationBackground, - Color? navigationSelected, - Color? navigationUnselected, - Color? navigationSelectedBackground, - - // Loading and animation colors - Color? shimmerBase, - Color? shimmerHighlight, - Color? loadingIndicator, - - // Markdown/code colors - Color? codeBackground, - Color? codeBorder, - Color? codeText, - Color? codeAccent, - - // Text colors - Color? textPrimary, - Color? textSecondary, - Color? textTertiary, - Color? textInverse, - Color? textDisabled, - - // Icon colors - Color? iconPrimary, - Color? iconSecondary, - Color? iconDisabled, - Color? iconInverse, - - // Typography styles - TextStyle? headingLarge, - TextStyle? headingMedium, - TextStyle? headingSmall, - TextStyle? bodyLarge, - TextStyle? bodyMedium, - TextStyle? bodySmall, - TextStyle? caption, - TextStyle? label, - TextStyle? code, + AppColorTokens? tokens, + TweakcnThemeVariant? variant, + bool? isDark, + TypographyThemeExtension? typography, + SurfaceThemeExtension? surfaces, + ShadowThemeExtension? shadows, + ShapeThemeExtension? shapes, }) { - return ConduitThemeExtension( - // Chat-specific colors - chatBubbleUser: chatBubbleUser ?? this.chatBubbleUser, - chatBubbleAssistant: chatBubbleAssistant ?? this.chatBubbleAssistant, - chatBubbleUserText: chatBubbleUserText ?? this.chatBubbleUserText, - chatBubbleAssistantText: - chatBubbleAssistantText ?? this.chatBubbleAssistantText, - chatBubbleUserBorder: chatBubbleUserBorder ?? this.chatBubbleUserBorder, - chatBubbleAssistantBorder: - chatBubbleAssistantBorder ?? this.chatBubbleAssistantBorder, - - // Input and form colors - inputBackground: inputBackground ?? this.inputBackground, - inputBorder: inputBorder ?? this.inputBorder, - inputBorderFocused: inputBorderFocused ?? this.inputBorderFocused, - inputText: inputText ?? this.inputText, - inputPlaceholder: inputPlaceholder ?? this.inputPlaceholder, - inputError: inputError ?? this.inputError, - - // Card and surface colors - cardBackground: cardBackground ?? this.cardBackground, - cardBorder: cardBorder ?? this.cardBorder, - cardShadow: cardShadow ?? this.cardShadow, - surfaceBackground: surfaceBackground ?? this.surfaceBackground, - surfaceContainer: surfaceContainer ?? this.surfaceContainer, - surfaceContainerHighest: - surfaceContainerHighest ?? this.surfaceContainerHighest, - - // Interactive element colors - buttonPrimary: buttonPrimary ?? this.buttonPrimary, - buttonPrimaryText: buttonPrimaryText ?? this.buttonPrimaryText, - buttonSecondary: buttonSecondary ?? this.buttonSecondary, - buttonSecondaryText: buttonSecondaryText ?? this.buttonSecondaryText, - buttonDisabled: buttonDisabled ?? this.buttonDisabled, - buttonDisabledText: buttonDisabledText ?? this.buttonDisabledText, - - // Status and feedback colors - success: success ?? this.success, - successBackground: successBackground ?? this.successBackground, - error: error ?? this.error, - errorBackground: errorBackground ?? this.errorBackground, - warning: warning ?? this.warning, - warningBackground: warningBackground ?? this.warningBackground, - info: info ?? this.info, - infoBackground: infoBackground ?? this.infoBackground, - - // Navigation and UI element colors - dividerColor: dividerColor ?? this.dividerColor, - navigationBackground: navigationBackground ?? this.navigationBackground, - navigationSelected: navigationSelected ?? this.navigationSelected, - navigationUnselected: navigationUnselected ?? this.navigationUnselected, - navigationSelectedBackground: - navigationSelectedBackground ?? this.navigationSelectedBackground, - - // Loading and animation colors - shimmerBase: shimmerBase ?? this.shimmerBase, - shimmerHighlight: shimmerHighlight ?? this.shimmerHighlight, - loadingIndicator: loadingIndicator ?? this.loadingIndicator, - - // Markdown/code colors - codeBackground: codeBackground ?? this.codeBackground, - codeBorder: codeBorder ?? this.codeBorder, - codeText: codeText ?? this.codeText, - codeAccent: codeAccent ?? this.codeAccent, - - // Text colors - textPrimary: textPrimary ?? this.textPrimary, - textSecondary: textSecondary ?? this.textSecondary, - textTertiary: textTertiary ?? this.textTertiary, - textInverse: textInverse ?? this.textInverse, - textDisabled: textDisabled ?? this.textDisabled, - - // Icon colors - iconPrimary: iconPrimary ?? this.iconPrimary, - iconSecondary: iconSecondary ?? this.iconSecondary, - iconDisabled: iconDisabled ?? this.iconDisabled, - iconInverse: iconInverse ?? this.iconInverse, - - // Typography styles - headingLarge: headingLarge ?? this.headingLarge, - headingMedium: headingMedium ?? this.headingMedium, - headingSmall: headingSmall ?? this.headingSmall, - bodyLarge: bodyLarge ?? this.bodyLarge, - bodyMedium: bodyMedium ?? this.bodyMedium, - bodySmall: bodySmall ?? this.bodySmall, - caption: caption ?? this.caption, - label: label ?? this.label, - code: code ?? this.code, + return ConduitThemeExtension._( + tokens: tokens ?? this.tokens, + variant: variant ?? this.variant, + isDark: isDark ?? this.isDark, + typography: typography ?? this.typography, + surfaces: surfaces ?? this.surfaces, + shadows: shadows ?? this.shadows, + shapes: shapes ?? this.shapes, ); } @override ConduitThemeExtension lerp( - ThemeExtension? other, + covariant ThemeExtension? other, double t, ) { - if (other is! ConduitThemeExtension) { - return this; - } - return ConduitThemeExtension( - // Chat-specific colors - chatBubbleUser: Color.lerp(chatBubbleUser, other.chatBubbleUser, t)!, - chatBubbleAssistant: Color.lerp( - chatBubbleAssistant, - other.chatBubbleAssistant, - t, - )!, - chatBubbleUserText: Color.lerp( - chatBubbleUserText, - other.chatBubbleUserText, - t, - )!, - chatBubbleAssistantText: Color.lerp( - chatBubbleAssistantText, - other.chatBubbleAssistantText, - t, - )!, - chatBubbleUserBorder: Color.lerp( - chatBubbleUserBorder, - other.chatBubbleUserBorder, - t, - )!, - chatBubbleAssistantBorder: Color.lerp( - chatBubbleAssistantBorder, - other.chatBubbleAssistantBorder, - t, - )!, - - // Input and form colors - inputBackground: Color.lerp(inputBackground, other.inputBackground, t)!, - inputBorder: Color.lerp(inputBorder, other.inputBorder, t)!, - inputBorderFocused: Color.lerp( - inputBorderFocused, - other.inputBorderFocused, - t, - )!, - inputText: Color.lerp(inputText, other.inputText, t)!, - inputPlaceholder: Color.lerp( - inputPlaceholder, - other.inputPlaceholder, - t, - )!, - inputError: Color.lerp(inputError, other.inputError, t)!, - - // Card and surface colors - cardBackground: Color.lerp(cardBackground, other.cardBackground, t)!, - cardBorder: Color.lerp(cardBorder, other.cardBorder, t)!, - cardShadow: Color.lerp(cardShadow, other.cardShadow, t)!, - surfaceBackground: Color.lerp( - surfaceBackground, - other.surfaceBackground, - t, - )!, - surfaceContainer: Color.lerp( - surfaceContainer, - other.surfaceContainer, - t, - )!, - surfaceContainerHighest: Color.lerp( - surfaceContainerHighest, - other.surfaceContainerHighest, - t, - )!, - - // Interactive element colors - buttonPrimary: Color.lerp(buttonPrimary, other.buttonPrimary, t)!, - buttonPrimaryText: Color.lerp( - buttonPrimaryText, - other.buttonPrimaryText, - t, - )!, - buttonSecondary: Color.lerp(buttonSecondary, other.buttonSecondary, t)!, - buttonSecondaryText: Color.lerp( - buttonSecondaryText, - other.buttonSecondaryText, - t, - )!, - buttonDisabled: Color.lerp(buttonDisabled, other.buttonDisabled, t)!, - buttonDisabledText: Color.lerp( - buttonDisabledText, - other.buttonDisabledText, - t, - )!, - - // Status and feedback colors - success: Color.lerp(success, other.success, t)!, - successBackground: Color.lerp( - successBackground, - other.successBackground, - t, - )!, - error: Color.lerp(error, other.error, t)!, - errorBackground: Color.lerp(errorBackground, other.errorBackground, t)!, - warning: Color.lerp(warning, other.warning, t)!, - warningBackground: Color.lerp( - warningBackground, - other.warningBackground, - t, - )!, - info: Color.lerp(info, other.info, t)!, - infoBackground: Color.lerp(infoBackground, other.infoBackground, t)!, - - // Navigation and UI element colors - dividerColor: Color.lerp(dividerColor, other.dividerColor, t)!, - navigationBackground: Color.lerp( - navigationBackground, - other.navigationBackground, - t, - )!, - navigationSelected: Color.lerp( - navigationSelected, - other.navigationSelected, - t, - )!, - navigationUnselected: Color.lerp( - navigationUnselected, - other.navigationUnselected, - t, - )!, - navigationSelectedBackground: Color.lerp( - navigationSelectedBackground, - other.navigationSelectedBackground, - t, - )!, - - // Loading and animation colors - shimmerBase: Color.lerp(shimmerBase, other.shimmerBase, t)!, - shimmerHighlight: Color.lerp( - shimmerHighlight, - other.shimmerHighlight, - t, - )!, - loadingIndicator: Color.lerp( - loadingIndicator, - other.loadingIndicator, - t, - )!, - codeBackground: Color.lerp(codeBackground, other.codeBackground, t)!, - codeBorder: Color.lerp(codeBorder, other.codeBorder, t)!, - codeText: Color.lerp(codeText, other.codeText, t)!, - codeAccent: Color.lerp(codeAccent, other.codeAccent, t)!, - - // Text colors - textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!, - textSecondary: Color.lerp(textSecondary, other.textSecondary, t)!, - textTertiary: Color.lerp(textTertiary, other.textTertiary, t)!, - textInverse: Color.lerp(textInverse, other.textInverse, t)!, - textDisabled: Color.lerp(textDisabled, other.textDisabled, t)!, - - // Icon colors - iconPrimary: Color.lerp(iconPrimary, other.iconPrimary, t)!, - iconSecondary: Color.lerp(iconSecondary, other.iconSecondary, t)!, - iconDisabled: Color.lerp(iconDisabled, other.iconDisabled, t)!, - iconInverse: Color.lerp(iconInverse, other.iconInverse, t)!, - - // Typography styles - headingLarge: TextStyle.lerp(headingLarge, other.headingLarge, t), - headingMedium: TextStyle.lerp(headingMedium, other.headingMedium, t), - headingSmall: TextStyle.lerp(headingSmall, other.headingSmall, t), - bodyLarge: TextStyle.lerp(bodyLarge, other.bodyLarge, t), - bodyMedium: TextStyle.lerp(bodyMedium, other.bodyMedium, t), - bodySmall: TextStyle.lerp(bodySmall, other.bodySmall, t), - caption: TextStyle.lerp(caption, other.caption, t), - label: TextStyle.lerp(label, other.label, t), - code: TextStyle.lerp(code, other.code, t), - ); + if (other is! ConduitThemeExtension) return this; + return t < 0.5 ? this : other; } - /// Dark theme extension derived from the active color palette. - static ConduitThemeExtension darkPalette({ - required AppColorPalette palette, - required AppColorTokens tokens, - }) { - final darkTone = palette.dark; - final onPrimary = _onSurfaceColor(darkTone.primary, tokens); - Color blend(Color overlay, {Color? surface}) { - return Color.alphaBlend(overlay, surface ?? tokens.neutralTone10); - } - - Color toneBackground(Color tone, {double opacity = 0.24}) { - return Color.alphaBlend( - tone.withValues(alpha: opacity), - tokens.neutralTone10, - ); - } - - return ConduitThemeExtension( - chatBubbleUser: darkTone.primary, - chatBubbleAssistant: tokens.neutralTone20, - chatBubbleUserText: onPrimary, - chatBubbleAssistantText: tokens.neutralOnSurface, - chatBubbleUserBorder: darkTone.secondary, - chatBubbleAssistantBorder: tokens.neutralTone40, - inputBackground: tokens.neutralTone20, - inputBorder: tokens.neutralTone40, - inputBorderFocused: darkTone.primary, - inputText: tokens.neutralOnSurface, - inputPlaceholder: tokens.neutralTone80, - inputError: tokens.statusError60, - cardBackground: tokens.neutralTone00, - cardBorder: tokens.neutralTone40, - cardShadow: blend(tokens.overlayWeak, surface: tokens.neutralTone00), - surfaceBackground: tokens.neutralTone10, - surfaceContainer: tokens.neutralTone00, - surfaceContainerHighest: tokens.neutralTone20, - buttonPrimary: darkTone.primary, - buttonPrimaryText: onPrimary, - buttonSecondary: tokens.neutralTone20, - buttonSecondaryText: tokens.neutralOnSurface, - buttonDisabled: tokens.neutralTone40, - buttonDisabledText: tokens.neutralTone80, - success: tokens.statusSuccess60, - successBackground: toneBackground(tokens.statusSuccess60), - error: tokens.statusError60, - errorBackground: toneBackground(tokens.statusError60), - warning: tokens.statusWarning60, - warningBackground: toneBackground(tokens.statusWarning60), - info: tokens.statusInfo60, - infoBackground: toneBackground(tokens.statusInfo60), - dividerColor: tokens.neutralTone40, - navigationBackground: tokens.neutralTone10, - navigationSelected: darkTone.primary, - navigationUnselected: tokens.neutralTone80, - navigationSelectedBackground: blend( - tokens.overlayMedium, - surface: tokens.neutralTone10, - ), - shimmerBase: blend(tokens.overlayWeak, surface: tokens.neutralTone10), - shimmerHighlight: blend( - tokens.overlayMedium, - surface: tokens.neutralTone20, - ), - loadingIndicator: darkTone.primary, - codeBackground: tokens.codeBackground, - codeBorder: tokens.codeBorder, - codeText: tokens.codeText, - codeAccent: tokens.codeAccent, - textPrimary: tokens.neutralOnSurface, - textSecondary: tokens.neutralTone80, - textTertiary: tokens.neutralTone60, - textInverse: tokens.neutralTone00, - textDisabled: tokens.neutralTone40, - iconPrimary: tokens.neutralOnSurface, - iconSecondary: tokens.neutralTone80, - iconDisabled: tokens.neutralTone40, - iconInverse: tokens.neutralTone00, - headingLarge: TextStyle( - fontSize: AppTypography.displaySmall, - fontWeight: FontWeight.w700, - color: tokens.neutralOnSurface, - height: 1.2, - ), - headingMedium: TextStyle( - fontSize: AppTypography.headlineLarge, - fontWeight: FontWeight.w600, - color: tokens.neutralOnSurface, - height: 1.3, - ), - headingSmall: TextStyle( - fontSize: AppTypography.headlineSmall, - fontWeight: FontWeight.w600, - color: tokens.neutralOnSurface, - height: 1.4, - ), - bodyLarge: TextStyle( - fontSize: AppTypography.bodyLarge, - fontWeight: FontWeight.w400, - color: tokens.neutralOnSurface, - height: 1.5, - ), - bodyMedium: TextStyle( - fontSize: AppTypography.bodyMedium, - fontWeight: FontWeight.w400, - color: tokens.neutralOnSurface, - height: 1.5, - ), - bodySmall: TextStyle( - fontSize: AppTypography.bodySmall, - fontWeight: FontWeight.w400, - color: tokens.neutralTone80, - height: 1.4, - ), - caption: TextStyle( - fontSize: AppTypography.labelMedium, - fontWeight: FontWeight.w500, - color: tokens.neutralTone80, - height: 1.3, - letterSpacing: 0.5, - ), - label: TextStyle( - fontSize: AppTypography.labelLarge, - fontWeight: FontWeight.w500, - color: tokens.neutralOnSurface, - height: 1.3, - ), - code: TextStyle( - fontSize: AppTypography.bodySmall, - fontWeight: FontWeight.w400, - color: tokens.neutralOnSurface, - height: 1.4, - fontFamily: AppTypography.monospaceFontFamily, - ), - ); + Color _overlay(Color overlay, {Color? surface}) { + return Color.alphaBlend(overlay, surface ?? surfaceBackground); } - /// Light theme extension derived from the active color palette. - static ConduitThemeExtension lightPalette({ - required AppColorPalette palette, - required AppColorTokens tokens, - }) { - final lightTone = palette.light; - final darkTone = palette.dark; - final onPrimary = _onSurfaceColor(lightTone.primary, tokens); - Color blend(Color overlay, {Color? surface}) { - return Color.alphaBlend(overlay, surface ?? tokens.neutralTone00); - } - - Color toneBackground(Color tone, {double opacity = 0.12}) { - return Color.alphaBlend( - tone.withValues(alpha: opacity), - tokens.neutralTone00, - ); - } - - return ConduitThemeExtension( - chatBubbleUser: lightTone.primary, - chatBubbleAssistant: tokens.neutralTone00, - chatBubbleUserText: onPrimary, - chatBubbleAssistantText: tokens.neutralOnSurface, - chatBubbleUserBorder: darkTone.primary, - chatBubbleAssistantBorder: tokens.neutralTone20, - inputBackground: tokens.neutralTone00, - inputBorder: tokens.neutralTone20, - inputBorderFocused: lightTone.primary, - inputText: tokens.neutralOnSurface, - inputPlaceholder: tokens.neutralTone60, - inputError: tokens.statusError60, - cardBackground: tokens.neutralTone00, - cardBorder: tokens.neutralTone20, - cardShadow: blend(tokens.overlayWeak), - surfaceBackground: tokens.neutralTone10, - surfaceContainer: tokens.neutralTone00, - surfaceContainerHighest: tokens.neutralTone20, - buttonPrimary: lightTone.primary, - buttonPrimaryText: onPrimary, - buttonSecondary: tokens.neutralTone20, - buttonSecondaryText: tokens.neutralOnSurface, - buttonDisabled: tokens.neutralTone40, - buttonDisabledText: tokens.neutralTone60, - success: tokens.statusSuccess60, - successBackground: toneBackground(tokens.statusSuccess60), - error: tokens.statusError60, - errorBackground: toneBackground(tokens.statusError60), - warning: tokens.statusWarning60, - warningBackground: toneBackground(tokens.statusWarning60), - info: tokens.statusInfo60, - infoBackground: toneBackground(tokens.statusInfo60), - dividerColor: tokens.neutralTone20, - navigationBackground: tokens.neutralTone00, - navigationSelected: lightTone.primary, - navigationUnselected: tokens.neutralTone60, - navigationSelectedBackground: blend(tokens.overlayMedium), - shimmerBase: blend(tokens.overlayWeak, surface: tokens.neutralTone10), - shimmerHighlight: tokens.neutralTone00, - loadingIndicator: lightTone.primary, - codeBackground: tokens.codeBackground, - codeBorder: tokens.codeBorder, - codeText: tokens.codeText, - codeAccent: tokens.codeAccent, - textPrimary: tokens.neutralOnSurface, - textSecondary: tokens.neutralTone80, - textTertiary: tokens.neutralTone60, - textInverse: tokens.neutralTone00, - textDisabled: tokens.neutralTone60, - iconPrimary: tokens.neutralOnSurface, - iconSecondary: tokens.neutralTone80, - iconDisabled: tokens.neutralTone60, - iconInverse: tokens.neutralTone00, - headingLarge: TextStyle( - fontSize: AppTypography.displaySmall, - fontWeight: FontWeight.w700, - color: tokens.neutralOnSurface, - height: 1.2, - ), - headingMedium: TextStyle( - fontSize: AppTypography.headlineLarge, - fontWeight: FontWeight.w600, - color: tokens.neutralOnSurface, - height: 1.3, - ), - headingSmall: TextStyle( - fontSize: AppTypography.headlineSmall, - fontWeight: FontWeight.w600, - color: tokens.neutralOnSurface, - height: 1.4, - ), - bodyLarge: TextStyle( - fontSize: AppTypography.bodyLarge, - fontWeight: FontWeight.w400, - color: tokens.neutralOnSurface, - height: 1.5, - ), - bodyMedium: TextStyle( - fontSize: AppTypography.bodyMedium, - fontWeight: FontWeight.w400, - color: tokens.neutralOnSurface, - height: 1.5, - ), - bodySmall: TextStyle( - fontSize: AppTypography.bodySmall, - fontWeight: FontWeight.w400, - color: tokens.neutralTone60, - height: 1.4, - ), - caption: TextStyle( - fontSize: AppTypography.labelMedium, - fontWeight: FontWeight.w500, - color: tokens.neutralTone60, - height: 1.3, - letterSpacing: 0.5, - ), - label: TextStyle( - fontSize: AppTypography.labelLarge, - fontWeight: FontWeight.w500, - color: tokens.neutralTone80, - height: 1.3, - ), - code: TextStyle( - fontSize: AppTypography.bodySmall, - fontWeight: FontWeight.w400, - color: tokens.neutralOnSurface, - height: 1.4, - fontFamily: AppTypography.monospaceFontFamily, - ), - ); + Color _toneOverlay(Color tone) { + final double alpha = isDark ? 0.24 : 0.12; + final Color base = isDark ? tokens.neutralTone10 : tokens.neutralTone00; + return Color.alphaBlend(tone.withValues(alpha: alpha), base); } - static Color _onSurfaceColor(Color background, AppColorTokens tokens) { + Color _onSurfaceColor(Color background) { final contrastOnLight = _contrastRatio(background, tokens.neutralTone00); final contrastOnDark = _contrastRatio(background, tokens.neutralOnSurface); return contrastOnLight >= contrastOnDark @@ -826,13 +316,32 @@ extension ConduitThemeContext on BuildContext { if (extension != null) return extension; final palette = theme.extension()?.palette ?? - AppColorPalettes.auroraViolet; + TweakcnThemes.t3Chat; + final TweakcnThemeVariant variant = palette.variantFor(theme.brightness); final tokens = theme.brightness == Brightness.dark - ? AppColorTokens.dark(palette: palette) - : AppColorTokens.light(palette: palette); - return theme.brightness == Brightness.dark - ? ConduitThemeExtension.darkPalette(palette: palette, tokens: tokens) - : ConduitThemeExtension.lightPalette(palette: palette, tokens: tokens); + ? AppColorTokens.dark(theme: palette) + : AppColorTokens.light(theme: palette); + final TypographyThemeExtension typography = + theme.extension() ?? + TypographyThemeExtension.fromVariant(variant); + final SurfaceThemeExtension surfaces = + theme.extension() ?? + SurfaceThemeExtension.fromVariant(variant); + final ShadowThemeExtension shadows = + theme.extension() ?? + ShadowThemeExtension.standard(); + final ShapeThemeExtension shapes = + theme.extension() ?? + ShapeThemeExtension.fromVariant(variant); + return ConduitThemeExtension.create( + theme: palette, + tokens: tokens, + brightness: theme.brightness, + typography: typography, + surfaces: surfaces, + shadows: shadows, + shapes: shapes, + ); } } @@ -843,20 +352,528 @@ extension ConduitColorTokensContext on BuildContext { if (tokens != null) return tokens; final palette = theme.extension()?.palette ?? - AppColorPalettes.auroraViolet; + TweakcnThemes.t3Chat; return theme.brightness == Brightness.dark - ? AppColorTokens.dark(palette: palette) - : AppColorTokens.light(palette: palette); + ? AppColorTokens.dark(theme: palette) + : AppColorTokens.light(theme: palette); } } extension ConduitPaletteContext on BuildContext { - AppColorPalette get conduitPalette { + TweakcnThemeDefinition get conduitPalette { return Theme.of(this).extension()?.palette ?? - AppColorPalettes.auroraViolet; + TweakcnThemes.t3Chat; } } +extension SidebarThemeContext on BuildContext { + SidebarThemeExtension get sidebarTheme { + final theme = Theme.of(this); + final extension = theme.extension(); + if (extension != null) return extension; + final palette = + theme.extension()?.palette ?? + TweakcnThemes.t3Chat; + final TweakcnThemeVariant variant = palette.variantFor(theme.brightness); + return SidebarThemeExtension.fromVariant(variant); + } +} + +@immutable +class StatusColors { + const StatusColors({ + required this.base, + required this.onBase, + required this.background, + required this.light, + required this.dark, + }); + + final Color base; + final Color onBase; + final Color background; + final Color light; + final Color dark; +} + +@immutable +class StatusPalette { + const StatusPalette({ + required this.success, + required this.warning, + required this.info, + required this.destructive, + }); + + final StatusColors success; + final StatusColors warning; + final StatusColors info; + final StatusColors destructive; +} + +@immutable +class TypographyThemeExtension + extends ThemeExtension { + const TypographyThemeExtension({ + required this.primaryFont, + required this.primaryFallback, + required this.serifFont, + required this.serifFallback, + required this.monospaceFont, + required this.monospaceFallback, + }); + + factory TypographyThemeExtension.fromVariant(TweakcnThemeVariant variant) { + final List sansStack = _sanitizeFontStack(variant.fontSans); + final List serifStack = _sanitizeFontStack(variant.fontSerif); + final List monoStack = _sanitizeFontStack(variant.fontMono); + + return TypographyThemeExtension( + primaryFont: _preferredFont(sansStack), + primaryFallback: _fallbackForStack(sansStack), + serifFont: _preferredFont(serifStack), + serifFallback: _fallbackForStack(serifStack), + monospaceFont: _preferredFont(monoStack), + monospaceFallback: _fallbackForStack(monoStack), + ); + } + + final String primaryFont; + final List primaryFallback; + final String serifFont; + final List serifFallback; + final String monospaceFont; + final List monospaceFallback; + + @override + TypographyThemeExtension copyWith({ + String? primaryFont, + List? primaryFallback, + String? serifFont, + List? serifFallback, + String? monospaceFont, + List? monospaceFallback, + }) { + return TypographyThemeExtension( + primaryFont: primaryFont ?? this.primaryFont, + primaryFallback: primaryFallback ?? this.primaryFallback, + serifFont: serifFont ?? this.serifFont, + serifFallback: serifFallback ?? this.serifFallback, + monospaceFont: monospaceFont ?? this.monospaceFont, + monospaceFallback: monospaceFallback ?? this.monospaceFallback, + ); + } + + @override + TypographyThemeExtension lerp( + covariant ThemeExtension? other, + double t, + ) { + if (other is! TypographyThemeExtension) return this; + return t < 0.5 ? this : other; + } +} + +@immutable +class SurfaceThemeExtension extends ThemeExtension { + const SurfaceThemeExtension({ + required this.background, + required this.container, + required this.containerHighest, + required this.card, + required this.cardForeground, + required this.popover, + required this.popoverForeground, + required this.muted, + required this.mutedForeground, + required this.border, + required this.ring, + required this.input, + }); + + factory SurfaceThemeExtension.fromVariant(TweakcnThemeVariant variant) { + return SurfaceThemeExtension( + background: variant.background, + container: Color.lerp(variant.background, variant.card, 0.5)!, + containerHighest: variant.card, + card: variant.card, + cardForeground: variant.cardForeground, + popover: variant.popover, + popoverForeground: variant.popoverForeground, + muted: variant.muted, + mutedForeground: variant.mutedForeground, + border: variant.border, + ring: variant.ring, + input: variant.input, + ); + } + + final Color background; + final Color container; + final Color containerHighest; + final Color card; + final Color cardForeground; + final Color popover; + final Color popoverForeground; + final Color muted; + final Color mutedForeground; + final Color border; + final Color ring; + final Color input; + + @override + SurfaceThemeExtension copyWith({ + Color? background, + Color? container, + Color? containerHighest, + Color? card, + Color? cardForeground, + Color? popover, + Color? popoverForeground, + Color? muted, + Color? mutedForeground, + Color? border, + Color? ring, + Color? input, + }) { + return SurfaceThemeExtension( + background: background ?? this.background, + container: container ?? this.container, + containerHighest: containerHighest ?? this.containerHighest, + card: card ?? this.card, + cardForeground: cardForeground ?? this.cardForeground, + popover: popover ?? this.popover, + popoverForeground: popoverForeground ?? this.popoverForeground, + muted: muted ?? this.muted, + mutedForeground: mutedForeground ?? this.mutedForeground, + border: border ?? this.border, + ring: ring ?? this.ring, + input: input ?? this.input, + ); + } + + @override + SurfaceThemeExtension lerp( + covariant ThemeExtension? other, + double t, + ) { + if (other is! SurfaceThemeExtension) return this; + return t < 0.5 ? this : other; + } +} + +@immutable +class ShadowThemeExtension extends ThemeExtension { + const ShadowThemeExtension({ + required this.shadow2Xs, + required this.shadowXs, + required this.shadowSm, + required this.shadow, + required this.shadowMd, + required this.shadowLg, + required this.shadowXl, + required this.shadow2Xl, + }); + + factory ShadowThemeExtension.standard() { + return ShadowThemeExtension( + shadow2Xs: _buildShadow(const <_ShadowSpec>[ + _ShadowSpec(dx: 0, dy: 1, blur: 3, spread: 0, opacity: 0.05), + ]), + shadowXs: _buildShadow(const <_ShadowSpec>[ + _ShadowSpec(dx: 0, dy: 1, blur: 3, spread: 0, opacity: 0.05), + ]), + shadowSm: _buildShadow(const <_ShadowSpec>[ + _ShadowSpec(dx: 0, dy: 1, blur: 3, spread: 0, opacity: 0.10), + _ShadowSpec(dx: 0, dy: 1, blur: 2, spread: -1, opacity: 0.10), + ]), + shadow: _buildShadow(const <_ShadowSpec>[ + _ShadowSpec(dx: 0, dy: 1, blur: 3, spread: 0, opacity: 0.10), + _ShadowSpec(dx: 0, dy: 1, blur: 2, spread: -1, opacity: 0.10), + ]), + shadowMd: _buildShadow(const <_ShadowSpec>[ + _ShadowSpec(dx: 0, dy: 1, blur: 3, spread: 0, opacity: 0.10), + _ShadowSpec(dx: 0, dy: 2, blur: 4, spread: -1, opacity: 0.10), + ]), + shadowLg: _buildShadow(const <_ShadowSpec>[ + _ShadowSpec(dx: 0, dy: 1, blur: 3, spread: 0, opacity: 0.10), + _ShadowSpec(dx: 0, dy: 4, blur: 6, spread: -1, opacity: 0.10), + ]), + shadowXl: _buildShadow(const <_ShadowSpec>[ + _ShadowSpec(dx: 0, dy: 1, blur: 3, spread: 0, opacity: 0.10), + _ShadowSpec(dx: 0, dy: 8, blur: 10, spread: -1, opacity: 0.10), + ]), + shadow2Xl: _buildShadow(const <_ShadowSpec>[ + _ShadowSpec(dx: 0, dy: 1, blur: 3, spread: 0, opacity: 0.25), + ]), + ); + } + + final List shadow2Xs; + final List shadowXs; + final List shadowSm; + final List shadow; + final List shadowMd; + final List shadowLg; + final List shadowXl; + final List shadow2Xl; + + @override + ShadowThemeExtension copyWith({ + List? shadow2Xs, + List? shadowXs, + List? shadowSm, + List? shadow, + List? shadowMd, + List? shadowLg, + List? shadowXl, + List? shadow2Xl, + }) { + return ShadowThemeExtension( + shadow2Xs: shadow2Xs ?? this.shadow2Xs, + shadowXs: shadowXs ?? this.shadowXs, + shadowSm: shadowSm ?? this.shadowSm, + shadow: shadow ?? this.shadow, + shadowMd: shadowMd ?? this.shadowMd, + shadowLg: shadowLg ?? this.shadowLg, + shadowXl: shadowXl ?? this.shadowXl, + shadow2Xl: shadow2Xl ?? this.shadow2Xl, + ); + } + + @override + ShadowThemeExtension lerp( + covariant ThemeExtension? other, + double t, + ) { + if (other is! ShadowThemeExtension) return this; + return t < 0.5 ? this : other; + } +} + +@immutable +class ShapeThemeExtension extends ThemeExtension { + const ShapeThemeExtension({ + required this.radiusBase, + required this.radiusSm, + required this.radiusMd, + required this.radiusLg, + required this.radiusXl, + }); + + factory ShapeThemeExtension.fromVariant(TweakcnThemeVariant variant) { + final double base = variant.radius; + return ShapeThemeExtension( + radiusBase: base, + radiusSm: math.max(0, base - 4), + radiusMd: math.max(0, base - 2), + radiusLg: base, + radiusXl: base + 4, + ); + } + + final double radiusBase; + final double radiusSm; + final double radiusMd; + final double radiusLg; + final double radiusXl; + + BorderRadius get small => BorderRadius.circular(radiusSm); + BorderRadius get medium => BorderRadius.circular(radiusMd); + BorderRadius get large => BorderRadius.circular(radiusLg); + BorderRadius get extraLarge => BorderRadius.circular(radiusXl); + + @override + ShapeThemeExtension copyWith({ + double? radiusBase, + double? radiusSm, + double? radiusMd, + double? radiusLg, + double? radiusXl, + }) { + return ShapeThemeExtension( + radiusBase: radiusBase ?? this.radiusBase, + radiusSm: radiusSm ?? this.radiusSm, + radiusMd: radiusMd ?? this.radiusMd, + radiusLg: radiusLg ?? this.radiusLg, + radiusXl: radiusXl ?? this.radiusXl, + ); + } + + @override + ShapeThemeExtension lerp( + covariant ThemeExtension? other, + double t, + ) { + if (other is! ShapeThemeExtension) return this; + return ShapeThemeExtension( + radiusBase: lerpDouble(radiusBase, other.radiusBase, t), + radiusSm: lerpDouble(radiusSm, other.radiusSm, t), + radiusMd: lerpDouble(radiusMd, other.radiusMd, t), + radiusLg: lerpDouble(radiusLg, other.radiusLg, t), + radiusXl: lerpDouble(radiusXl, other.radiusXl, t), + ); + } + + static double lerpDouble(double a, double b, double t) { + return a + (b - a) * t; + } +} + +@immutable +class SidebarThemeExtension extends ThemeExtension { + const SidebarThemeExtension({ + required this.background, + required this.foreground, + required this.primary, + required this.primaryForeground, + required this.accent, + required this.accentForeground, + required this.border, + required this.ring, + }); + + factory SidebarThemeExtension.fromVariant(TweakcnThemeVariant variant) { + return SidebarThemeExtension( + background: variant.sidebarBackground, + foreground: variant.sidebarForeground, + primary: variant.sidebarPrimary, + primaryForeground: variant.sidebarPrimaryForeground, + accent: variant.sidebarAccent, + accentForeground: variant.sidebarAccentForeground, + border: variant.sidebarBorder, + ring: variant.sidebarRing, + ); + } + + final Color background; + final Color foreground; + final Color primary; + final Color primaryForeground; + final Color accent; + final Color accentForeground; + final Color border; + final Color ring; + + @override + SidebarThemeExtension copyWith({ + Color? background, + Color? foreground, + Color? primary, + Color? primaryForeground, + Color? accent, + Color? accentForeground, + Color? border, + Color? ring, + }) { + return SidebarThemeExtension( + background: background ?? this.background, + foreground: foreground ?? this.foreground, + primary: primary ?? this.primary, + primaryForeground: primaryForeground ?? this.primaryForeground, + accent: accent ?? this.accent, + accentForeground: accentForeground ?? this.accentForeground, + border: border ?? this.border, + ring: ring ?? this.ring, + ); + } + + @override + SidebarThemeExtension lerp( + covariant ThemeExtension? other, + double t, + ) { + if (other is! SidebarThemeExtension) return this; + return SidebarThemeExtension( + background: Color.lerp(background, other.background, t)!, + foreground: Color.lerp(foreground, other.foreground, t)!, + primary: Color.lerp(primary, other.primary, t)!, + primaryForeground: Color.lerp( + primaryForeground, + other.primaryForeground, + t, + )!, + accent: Color.lerp(accent, other.accent, t)!, + accentForeground: Color.lerp( + accentForeground, + other.accentForeground, + t, + )!, + border: Color.lerp(border, other.border, t)!, + ring: Color.lerp(ring, other.ring, t)!, + ); + } +} + +class _ShadowSpec { + const _ShadowSpec({ + required this.dx, + required this.dy, + required this.blur, + required this.spread, + required this.opacity, + }); + + final double dx; + final double dy; + final double blur; + final double spread; + final double opacity; +} + +List _buildShadow(List<_ShadowSpec> specs) { + return specs + .map( + (spec) => BoxShadow( + color: Colors.black.withValues(alpha: spec.opacity), + offset: Offset(spec.dx, spec.dy), + blurRadius: spec.blur, + spreadRadius: spec.spread, + ), + ) + .toList(growable: false); +} + +final Set _genericFontFamilies = { + 'ui-sans-serif', + 'system-ui', + '-apple-system', + 'BlinkMacSystemFont', + 'sans-serif', + 'ui-serif', + 'serif', + 'ui-monospace', + 'monospace', +}; + +List _sanitizeFontStack(List stack) { + final List cleaned = []; + for (final String raw in stack) { + final String trimmed = raw + .trim() + .replaceAll(RegExp("^[\"']"), '') + .replaceAll(RegExp("[\"']\$"), ''); + if (trimmed.isEmpty) continue; + if (!cleaned.contains(trimmed)) cleaned.add(trimmed); + } + return cleaned; +} + +String _preferredFont(List stack) { + for (final String font in stack) { + if (!_genericFontFamilies.contains(font)) { + return font; + } + } + return stack.isNotEmpty ? stack.first : ''; +} + +List _fallbackForStack(List stack) { + if (stack.isEmpty) return const []; + final String primary = _preferredFont(stack); + return stack.where((font) => font != primary).toList(growable: false); +} + /// Consistent spacing values - Enhanced for production with better hierarchy class Spacing { // Base spacing scale (8pt grid system) diff --git a/lib/shared/theme/tweakcn_themes.dart b/lib/shared/theme/tweakcn_themes.dart new file mode 100644 index 0000000..2b5f4bc --- /dev/null +++ b/lib/shared/theme/tweakcn_themes.dart @@ -0,0 +1,386 @@ +import 'package:flutter/material.dart'; + +/// Represents a single tweakcn theme variant (light or dark) and exposes the +/// standard set of color tokens defined by the registry. +@immutable +class TweakcnThemeVariant { + const TweakcnThemeVariant({ + required this.background, + required this.foreground, + required this.card, + required this.cardForeground, + required this.popover, + required this.popoverForeground, + required this.primary, + required this.primaryForeground, + required this.secondary, + required this.secondaryForeground, + required this.muted, + required this.mutedForeground, + required this.accent, + required this.accentForeground, + required this.destructive, + required this.destructiveForeground, + required this.border, + required this.input, + required this.ring, + required this.sidebarBackground, + required this.sidebarForeground, + required this.sidebarPrimary, + required this.sidebarPrimaryForeground, + required this.sidebarAccent, + required this.sidebarAccentForeground, + required this.sidebarBorder, + required this.sidebarRing, + required this.success, + required this.successForeground, + required this.warning, + required this.warningForeground, + required this.info, + required this.infoForeground, + this.radius = 16, + this.fontSans = const [], + this.fontSerif = const [], + this.fontMono = const [], + }); + + final Color background; + final Color foreground; + final Color card; + final Color cardForeground; + final Color popover; + final Color popoverForeground; + final Color primary; + final Color primaryForeground; + final Color secondary; + final Color secondaryForeground; + final Color muted; + final Color mutedForeground; + final Color accent; + final Color accentForeground; + final Color destructive; + final Color destructiveForeground; + final Color border; + final Color input; + final Color ring; + final Color sidebarBackground; + final Color sidebarForeground; + final Color sidebarPrimary; + final Color sidebarPrimaryForeground; + final Color sidebarAccent; + final Color sidebarAccentForeground; + final Color sidebarBorder; + final Color sidebarRing; + final Color success; + final Color successForeground; + final Color warning; + final Color warningForeground; + final Color info; + final Color infoForeground; + final double radius; + final List fontSans; + final List fontSerif; + final List fontMono; +} + +/// Definition of a tweakcn theme that provides both light and dark variants. +@immutable +class TweakcnThemeDefinition { + const TweakcnThemeDefinition({ + required this.id, + required this.label, + required this.description, + required this.light, + required this.dark, + required this.preview, + }); + + final String id; + final String label; + final String description; + final TweakcnThemeVariant light; + final TweakcnThemeVariant dark; + final List preview; + + TweakcnThemeVariant variantFor(Brightness brightness) { + return brightness == Brightness.dark ? dark : light; + } +} + +Color mix(Color a, Color b, double amount) { + return Color.lerp(a, b, amount.clamp(0.0, 1.0)) ?? a; +} + +class TweakcnThemes { + static final TweakcnThemeVariant _conduitLight = TweakcnThemeVariant( + background: const Color(0xFFFFFFFF), + foreground: const Color(0xFF0A0A0A), + card: const Color(0xFFFFFFFF), + cardForeground: const Color(0xFF0A0A0A), + popover: const Color(0xFFFFFFFF), + popoverForeground: const Color(0xFF0A0A0A), + primary: const Color(0xFF171717), + primaryForeground: const Color(0xFFFAFAFA), + secondary: const Color(0xFFF5F5F5), + secondaryForeground: const Color(0xFF171717), + muted: const Color(0xFFF5F5F5), + mutedForeground: const Color(0xFF737373), + accent: const Color(0xFFF5F5F5), + accentForeground: const Color(0xFF171717), + destructive: const Color(0xFFE7000B), + destructiveForeground: const Color(0xFFFAFAFA), + border: const Color(0xFFE5E5E5), + input: const Color(0xFFE5E5E5), + ring: const Color(0xFFA1A1A1), + sidebarBackground: const Color(0xFFFAFAFA), + sidebarForeground: const Color(0xFF0A0A0A), + sidebarPrimary: const Color(0xFF171717), + sidebarPrimaryForeground: const Color(0xFFFAFAFA), + sidebarAccent: const Color(0xFFF5F5F5), + sidebarAccentForeground: const Color(0xFF171717), + sidebarBorder: const Color(0xFFE5E5E5), + sidebarRing: const Color(0xFFA1A1A1), + success: const Color(0xFF00E6C7), + successForeground: const Color(0xFF09090B), + warning: const Color(0xFFF97316), + warningForeground: const Color(0xFF09090B), + info: const Color(0xFF2563EB), + infoForeground: const Color(0xFFFAFAFA), + radius: 10, + fontSans: const [ + 'ui-sans-serif', + 'system-ui', + '-apple-system', + 'BlinkMacSystemFont', + 'Segoe UI', + 'Roboto', + 'Helvetica Neue', + 'Arial', + 'Noto Sans', + 'sans-serif', + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji', + ], + fontSerif: const [ + 'ui-serif', + 'Georgia', + 'Cambria', + 'Times New Roman', + 'Times', + 'serif', + ], + fontMono: const [ + 'ui-monospace', + 'SFMono-Regular', + 'SF Mono', + 'Menlo', + 'Monaco', + 'Consolas', + 'Liberation Mono', + 'Courier New', + 'monospace', + ], + ); + + static final TweakcnThemeVariant _conduitDark = TweakcnThemeVariant( + background: const Color(0xFF0A0A0A), + foreground: const Color(0xFFFAFAFA), + card: const Color(0xFF171717), + cardForeground: const Color(0xFFFAFAFA), + popover: const Color(0xFF262626), + popoverForeground: const Color(0xFFFAFAFA), + primary: const Color(0xFFE5E5E5), + primaryForeground: const Color(0xFF171717), + secondary: const Color(0xFF262626), + secondaryForeground: const Color(0xFFFAFAFA), + muted: const Color(0xFF262626), + mutedForeground: const Color(0xFFA1A1AA), + accent: const Color(0xFF404040), + accentForeground: const Color(0xFFFAFAFA), + destructive: const Color(0xFFFF6467), + destructiveForeground: const Color(0xFFFAFAFA), + border: const Color(0xFF282828), + input: const Color(0xFF343434), + ring: const Color(0xFF737373), + sidebarBackground: const Color(0xFF171717), + sidebarForeground: const Color(0xFFFAFAFA), + sidebarPrimary: const Color(0xFF1447E6), + sidebarPrimaryForeground: const Color(0xFFFAFAFA), + sidebarAccent: const Color(0xFF262626), + sidebarAccentForeground: const Color(0xFFFAFAFA), + sidebarBorder: const Color(0xFF282828), + sidebarRing: const Color(0xFF525252), + success: const Color(0xFF00E6C7), + successForeground: const Color(0xFF09090B), + warning: const Color(0xFFF97316), + warningForeground: const Color(0xFF09090B), + info: const Color(0xFF2563EB), + infoForeground: const Color(0xFFFAFAFA), + radius: 10, + fontSans: const [ + 'ui-sans-serif', + 'system-ui', + '-apple-system', + 'BlinkMacSystemFont', + 'Segoe UI', + 'Roboto', + 'Helvetica Neue', + 'Arial', + 'Noto Sans', + 'sans-serif', + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji', + ], + fontSerif: const [ + 'ui-serif', + 'Georgia', + 'Cambria', + 'Times New Roman', + 'Times', + 'serif', + ], + fontMono: const [ + 'ui-monospace', + 'SFMono-Regular', + 'SF Mono', + 'Menlo', + 'Monaco', + 'Consolas', + 'Liberation Mono', + 'Courier New', + 'monospace', + ], + ); + + static final TweakcnThemeVariant _t3ChatLight = TweakcnThemeVariant( + background: const Color(0xFFFAF5FA), + foreground: const Color(0xFF501854), + card: const Color(0xFFFAF5FA), + cardForeground: const Color(0xFF501854), + popover: const Color(0xFFFFFFFF), + popoverForeground: const Color(0xFF501854), + primary: const Color(0xFFA84370), + primaryForeground: const Color(0xFFFFFFFF), + secondary: const Color(0xFFF1C4E6), + secondaryForeground: const Color(0xFF77347C), + muted: const Color(0xFFF6E5F3), + mutedForeground: const Color(0xFF834588), + accent: const Color(0xFFF1C4E6), + accentForeground: const Color(0xFF77347C), + destructive: const Color(0xFFAB4347), + destructiveForeground: const Color(0xFFFFFFFF), + border: const Color(0xFFEFBDEB), + input: const Color(0xFFE7C1DC), + ring: const Color(0xFFDB2777), + sidebarBackground: const Color(0xFFF3E4F6), + sidebarForeground: const Color(0xFFAC1668), + sidebarPrimary: const Color(0xFF454554), + sidebarPrimaryForeground: const Color(0xFFFAF1F7), + sidebarAccent: const Color(0xFFF8F8F7), + sidebarAccentForeground: const Color(0xFF454554), + sidebarBorder: const Color(0xFFECEAE9), + sidebarRing: const Color(0xFFDB2777), + success: const Color(0xFFF4A462), + successForeground: const Color(0xFF501854), + warning: const Color(0xFFE8C468), + warningForeground: const Color(0xFF501854), + info: const Color(0xFF6C12B9), + infoForeground: const Color(0xFFF8F1F5), + radius: 8, + ); + + static final TweakcnThemeVariant _t3ChatDark = TweakcnThemeVariant( + background: const Color(0xFF221D27), + foreground: const Color(0xFFD2C4DE), + card: const Color(0xFF2C2632), + cardForeground: const Color(0xFFDBC5D2), + popover: const Color(0xFF100A0E), + popoverForeground: const Color(0xFFF8F1F5), + primary: const Color(0xFFA3004C), + primaryForeground: const Color(0xFFEFC0D8), + secondary: const Color(0xFF362D3D), + secondaryForeground: const Color(0xFFD4C7E1), + muted: const Color(0xFF28222D), + mutedForeground: const Color(0xFFC2B6CF), + accent: const Color(0xFF463753), + accentForeground: const Color(0xFFF8F1F5), + destructive: const Color(0xFF301015), + destructiveForeground: const Color(0xFFFFFFFF), + border: const Color(0xFF3B3237), + input: const Color(0xFF3E343C), + ring: const Color(0xFFDB2777), + sidebarBackground: const Color(0xFF181117), + sidebarForeground: const Color(0xFFE0CAD6), + sidebarPrimary: const Color(0xFF1D4ED8), + sidebarPrimaryForeground: const Color(0xFFFFFFFF), + sidebarAccent: const Color(0xFF261922), + sidebarAccentForeground: const Color(0xFFF4F4F5), + sidebarBorder: const Color(0xFF000000), + sidebarRing: const Color(0xFFDB2777), + success: const Color(0xFFE88C30), + successForeground: const Color(0xFF181117), + warning: const Color(0xFFAF57DB), + warningForeground: const Color(0xFF181117), + info: const Color(0xFF934DCB), + infoForeground: const Color(0xFFF8F1F5), + radius: 8, + ); + + static final TweakcnThemeDefinition t3Chat = TweakcnThemeDefinition( + id: 't3_chat', + label: 'T3 Chat', + description: 'Playful gradients inspired by the T3 Stack brand.', + light: _t3ChatLight, + dark: _t3ChatDark, + preview: const [ + Color(0xFFA84370), + Color(0xFFF1C4E6), + Color(0xFFDB2777), + ], + ); + + static final TweakcnThemeDefinition conduit = TweakcnThemeDefinition( + id: 'conduit', + label: 'Conduit', + description: 'Clean neutral theme designed for Conduit.', + light: _conduitLight, + dark: _conduitDark, + preview: const [ + Color(0xFFA1A1AA), + Color(0xFFF4F4F5), + Color(0xFF2563EB), + ], + ); + + static List all = [conduit, t3Chat]; + + static TweakcnThemeDefinition byId(String? id) { + return all.firstWhere((theme) => theme.id == id, orElse: () => conduit); + } +} + +@immutable +class AppPaletteThemeExtension + extends ThemeExtension { + const AppPaletteThemeExtension({required this.palette}); + + final TweakcnThemeDefinition palette; + + @override + AppPaletteThemeExtension copyWith({TweakcnThemeDefinition? palette}) { + return AppPaletteThemeExtension(palette: palette ?? this.palette); + } + + @override + AppPaletteThemeExtension lerp( + covariant ThemeExtension? other, + double t, + ) { + if (other is! AppPaletteThemeExtension) return this; + return t < 0.5 ? this : other; + } +} diff --git a/lib/shared/utils/conversation_context_menu.dart b/lib/shared/utils/conversation_context_menu.dart index 8bdd5c1..7dfb674 100644 --- a/lib/shared/utils/conversation_context_menu.dart +++ b/lib/shared/utils/conversation_context_menu.dart @@ -57,13 +57,15 @@ Future showConduitContextMenu({ isCompact: true, leading: Icon( Platform.isIOS ? action.cupertinoIcon : action.materialIcon, - color: action.destructive ? theme.error : theme.iconPrimary, + color: action.destructive ? Colors.red : theme.iconPrimary, size: IconSize.modal, ), title: Text( action.label, style: AppTypography.standard.copyWith( - color: action.destructive ? theme.error : theme.textPrimary, + color: action.destructive + ? Colors.red + : theme.textPrimary, fontWeight: FontWeight.w500, ), ), diff --git a/lib/shared/widgets/conduit_components.dart b/lib/shared/widgets/conduit_components.dart index adfa639..0fe9c26 100644 --- a/lib/shared/widgets/conduit_components.dart +++ b/lib/shared/widgets/conduit_components.dart @@ -293,6 +293,8 @@ class ConduitCard extends StatelessWidget { final bool isSelected; final bool isElevated; final bool isCompact; + final Color? backgroundColor; + final Color? borderColor; const ConduitCard({ super.key, @@ -302,6 +304,8 @@ class ConduitCard extends StatelessWidget { this.isSelected = false, this.isElevated = false, this.isCompact = false, + this.backgroundColor, + this.borderColor, }); @override @@ -317,14 +321,14 @@ class ConduitCard extends StatelessWidget { ? context.conduitTheme.buttonPrimary.withValues( alpha: Alpha.highlight, ) - : context.conduitTheme.cardBackground, + : backgroundColor ?? context.conduitTheme.cardBackground, borderRadius: BorderRadius.circular(AppBorderRadius.card), border: Border.all( color: isSelected ? context.conduitTheme.buttonPrimary.withValues( alpha: Alpha.standard, ) - : context.conduitTheme.cardBorder, + : borderColor ?? context.conduitTheme.cardBorder, width: BorderWidth.standard, ), boxShadow: isElevated ? ConduitShadows.card(context) : null,