refactor: Migrate to Tweakcn themes and enhance UI consistency

- Replaced references to AppColorPalettes with TweakcnThemes across various files to standardize theme usage.
- Updated the AppTheme and AppColorTokens to utilize TweakcnThemeDefinition for improved theme management.
- Adjusted UI components in ChatPage, ChatsDrawer, AppCustomizationPage, and ProfilePage to align with the new theme structure, ensuring consistent styling and color application.
- Removed the deprecated color_palettes.dart file to streamline the theme architecture.
This commit is contained in:
cogwheel0
2025-10-18 13:58:15 +05:30
parent 23071bb7b1
commit 60883315a2
14 changed files with 1700 additions and 1437 deletions

View File

@@ -25,7 +25,7 @@ import '../services/optimized_storage_service.dart';
import '../services/socket_service.dart'; import '../services/socket_service.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
import '../models/socket_event.dart'; import '../models/socket_event.dart';
import '../../shared/theme/color_palettes.dart'; import '../../shared/theme/tweakcn_themes.dart';
import '../../shared/theme/app_theme.dart'; import '../../shared/theme/app_theme.dart';
import '../../features/tools/providers/tools_providers.dart'; import '../../features/tools/providers/tools_providers.dart';
@@ -88,14 +88,14 @@ class AppThemePalette extends _$AppThemePalette {
late final OptimizedStorageService _storage; late final OptimizedStorageService _storage;
@override @override
AppColorPalette build() { TweakcnThemeDefinition build() {
_storage = ref.watch(optimizedStorageServiceProvider); _storage = ref.watch(optimizedStorageServiceProvider);
final storedId = _storage.getThemePaletteId(); final storedId = _storage.getThemePaletteId();
return AppColorPalettes.byId(storedId); return TweakcnThemes.byId(storedId);
} }
Future<void> setPalette(String paletteId) async { Future<void> setPalette(String paletteId) async {
final palette = AppColorPalettes.byId(paletteId); final palette = TweakcnThemes.byId(paletteId);
state = palette; state = palette;
await _storage.setThemePaletteId(palette.id); await _storage.setThemePaletteId(palette.id);
} }

View File

@@ -2,7 +2,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/semantics.dart'; import 'package:flutter/semantics.dart';
import '../../shared/theme/color_palettes.dart'; import '../../shared/theme/tweakcn_themes.dart';
import '../../shared/theme/theme_extensions.dart'; import '../../shared/theme/theme_extensions.dart';
/// Enhanced accessibility service for WCAG 2.2 AA compliance /// Enhanced accessibility service for WCAG 2.2 AA compliance
@@ -349,7 +349,7 @@ class EnhancedAccessibilityService {
return BoxDecoration( return BoxDecoration(
border: hasFocus border: hasFocus
? Border.all( ? Border.all(
color: focusColor ?? AppColorPalettes.auroraViolet.light.primary, color: focusColor ?? TweakcnThemes.t3Chat.light.primary,
width: borderWidth, width: borderWidth,
) )
: null, : null,

View File

@@ -1217,13 +1217,16 @@ class _ChatPageState extends ConsumerState<ChatPage> {
.set(false); .set(false);
} catch (_) {} } catch (_) {}
}, },
drawer: SafeArea( drawer: Container(
color: context.sidebarTheme.background,
child: SafeArea(
top: true, top: true,
bottom: true, bottom: true,
left: false, left: false,
right: false, right: false,
child: const ChatsDrawer(), child: const ChatsDrawer(),
), ),
),
child: Scaffold( child: Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground, backgroundColor: context.conduitTheme.surfaceBackground,
// Replace Scaffold drawer with a tunable slide drawer for gentler snap behavior. // Replace Scaffold drawer with a tunable slide drawer for gentler snap behavior.

View File

@@ -152,10 +152,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Bottom section now only shows navigation actions // Bottom section now only shows navigation actions
final theme = context.conduitTheme; final sidebarTheme = context.sidebarTheme;
return Container( return Container(
color: theme.surfaceBackground, decoration: BoxDecoration(
color: sidebarTheme.background,
border: Border(right: BorderSide(color: sidebarTheme.border)),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@@ -169,7 +172,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
child: Row(children: [Expanded(child: _buildSearchField(context))]), child: Row(children: [Expanded(child: _buildSearchField(context))]),
), ),
Expanded(child: _buildConversationList(context)), Expanded(child: _buildConversationList(context)),
Divider(height: 1, color: theme.dividerColor), Divider(height: 1, color: sidebarTheme.border),
_buildBottomSection(context), _buildBottomSection(context),
], ],
), ),
@@ -177,23 +180,23 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
} }
Widget _buildSearchField(BuildContext context) { Widget _buildSearchField(BuildContext context) {
final theme = context.conduitTheme; final sidebarTheme = context.sidebarTheme;
return Material( return Material(
color: Colors.transparent, color: Colors.transparent,
child: TextField( child: TextField(
controller: _searchController, controller: _searchController,
focusNode: _searchFocusNode, focusNode: _searchFocusNode,
onChanged: (_) => _onSearchChanged(), onChanged: (_) => _onSearchChanged(),
style: AppTypography.standard.copyWith(color: theme.inputText), style: AppTypography.standard.copyWith(color: sidebarTheme.foreground),
decoration: InputDecoration( decoration: InputDecoration(
isDense: true, isDense: true,
hintText: AppLocalizations.of(context)!.searchConversations, hintText: AppLocalizations.of(context)!.searchConversations,
hintStyle: AppTypography.standard.copyWith( hintStyle: AppTypography.standard.copyWith(
color: theme.inputPlaceholder, color: sidebarTheme.foreground.withValues(alpha: 0.6),
), ),
prefixIcon: Icon( prefixIcon: Icon(
Platform.isIOS ? CupertinoIcons.search : Icons.search, Platform.isIOS ? CupertinoIcons.search : Icons.search,
color: theme.iconSecondary, color: sidebarTheme.foreground.withValues(alpha: 0.7),
size: IconSize.input, size: IconSize.input,
), ),
prefixIconConstraints: const BoxConstraints( prefixIconConstraints: const BoxConstraints(
@@ -211,7 +214,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
Platform.isIOS Platform.isIOS
? CupertinoIcons.clear_circled_solid ? CupertinoIcons.clear_circled_solid
: Icons.clear, : Icons.clear,
color: theme.iconSecondary, color: sidebarTheme.foreground.withValues(alpha: 0.7),
size: IconSize.input, size: IconSize.input,
), ),
) )
@@ -221,18 +224,18 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
minHeight: TouchTarget.minimum, minHeight: TouchTarget.minimum,
), ),
filled: true, filled: true,
fillColor: theme.inputBackground, fillColor: sidebarTheme.accent.withValues(alpha: 0.9),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md), borderRadius: BorderRadius.circular(AppBorderRadius.md),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md), borderRadius: BorderRadius.circular(AppBorderRadius.md),
borderSide: BorderSide(color: theme.inputBorder, width: 1), borderSide: BorderSide(color: sidebarTheme.border, width: 1),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md), borderRadius: BorderRadius.circular(AppBorderRadius.md),
borderSide: BorderSide(color: theme.buttonPrimary, width: 1), borderSide: BorderSide(color: sidebarTheme.ring, width: 1.2),
), ),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.md, horizontal: Spacing.md,
@@ -673,7 +676,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
child: Text( child: Text(
'Search failed', 'Search failed',
style: AppTypography.bodyMediumStyle.copyWith( style: AppTypography.bodyMediumStyle.copyWith(
color: theme.textSecondary, color: context.sidebarTheme.foreground.withValues(alpha: 0.7),
), ),
), ),
), ),
@@ -682,13 +685,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
} }
Widget _buildSectionHeader(String title, int count) { Widget _buildSectionHeader(String title, int count) {
final theme = context.conduitTheme; final sidebarTheme = context.sidebarTheme;
return Row( return Row(
children: [ children: [
Text( Text(
title, title,
style: AppTypography.labelStyle.copyWith( style: AppTypography.labelStyle.copyWith(
color: theme.textSecondary, color: sidebarTheme.foreground.withValues(alpha: 0.9),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
decoration: TextDecoration.none, decoration: TextDecoration.none,
), ),
@@ -697,17 +700,17 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.surfaceContainer.withValues(alpha: 0.4), color: sidebarTheme.accent.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(AppBorderRadius.xs), borderRadius: BorderRadius.circular(AppBorderRadius.xs),
border: Border.all( border: Border.all(
color: theme.dividerColor.withValues(alpha: 0.5), color: sidebarTheme.border.withValues(alpha: 0.6),
width: BorderWidth.thin, width: BorderWidth.thin,
), ),
), ),
child: Text( child: Text(
'$count', '$count',
style: AppTypography.tiny.copyWith( style: AppTypography.tiny.copyWith(
color: theme.textSecondary, color: sidebarTheme.foreground.withValues(alpha: 0.8),
decoration: TextDecoration.none, decoration: TextDecoration.none,
), ),
), ),
@@ -1432,6 +1435,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
Widget _buildBottomSection(BuildContext context) { Widget _buildBottomSection(BuildContext context) {
final theme = context.conduitTheme; final theme = context.conduitTheme;
final sidebarTheme = context.sidebarTheme;
final currentUserAsync = ref.watch(currentUserProvider); final currentUserAsync = ref.watch(currentUserProvider);
final userFromProfile = currentUserAsync.maybeWhen( final userFromProfile = currentUserAsync.maybeWhen(
data: (u) => u, data: (u) => u,
@@ -1460,10 +1464,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
Container( Container(
padding: const EdgeInsets.all(Spacing.sm), padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.surfaceContainer.withValues(alpha: 0.3), color: sidebarTheme.accent.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(AppBorderRadius.small), borderRadius: BorderRadius.circular(AppBorderRadius.small),
border: Border.all( border: Border.all(
color: theme.dividerColor.withValues(alpha: 0.5), color: sidebarTheme.border.withValues(alpha: 0.6),
width: BorderWidth.standard, width: BorderWidth.standard,
), ),
), ),
@@ -1497,7 +1501,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: AppTypography.bodySmallStyle.copyWith( style: AppTypography.bodySmallStyle.copyWith(
color: theme.textPrimary, color: sidebarTheme.foreground,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
decoration: TextDecoration.none, decoration: TextDecoration.none,
), ),
@@ -1514,7 +1518,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
Platform.isIOS Platform.isIOS
? CupertinoIcons.settings ? CupertinoIcons.settings
: Icons.settings_rounded, : Icons.settings_rounded,
color: theme.iconSecondary, color: sidebarTheme.foreground.withValues(alpha: 0.8),
size: IconSize.medium, size: IconSize.medium,
), ),
), ),

View File

@@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/services/settings_service.dart'; import '../../../core/services/settings_service.dart';
import '../../../shared/theme/theme_extensions.dart'; import '../../../shared/theme/theme_extensions.dart';
import '../../../shared/theme/color_palettes.dart'; import '../../../shared/theme/tweakcn_themes.dart';
import '../../tools/providers/tools_providers.dart'; import '../../tools/providers/tools_providers.dart';
import '../../../core/models/tool.dart'; import '../../../core/models/tool.dart';
import '../../../shared/widgets/conduit_components.dart'; import '../../../shared/widgets/conduit_components.dart';
@@ -38,10 +38,10 @@ class AppCustomizationPage extends ConsumerWidget {
final locale = ref.watch(appLocaleProvider); final locale = ref.watch(appLocaleProvider);
final currentLanguageCode = locale?.languageCode ?? 'system'; final currentLanguageCode = locale?.languageCode ?? 'system';
final languageLabel = _resolveLanguageLabel(context, currentLanguageCode); final languageLabel = _resolveLanguageLabel(context, currentLanguageCode);
final activePalette = ref.watch(appThemePaletteProvider); final activeTheme = ref.watch(appThemePaletteProvider);
return Scaffold( return Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground, backgroundColor: context.sidebarTheme.background,
appBar: _buildAppBar(context), appBar: _buildAppBar(context),
body: SafeArea( body: SafeArea(
child: ListView( child: ListView(
@@ -61,7 +61,7 @@ class AppCustomizationPage extends ConsumerWidget {
currentLanguageCode, currentLanguageCode,
languageLabel, languageLabel,
settings, settings,
activePalette, activeTheme,
), ),
const SizedBox(height: Spacing.xl), const SizedBox(height: Spacing.xl),
_buildQuickPillsSection(context, ref, settings), _buildQuickPillsSection(context, ref, settings),
@@ -78,7 +78,7 @@ class AppCustomizationPage extends ConsumerWidget {
PreferredSizeWidget _buildAppBar(BuildContext context) { PreferredSizeWidget _buildAppBar(BuildContext context) {
final canPop = ModalRoute.of(context)?.canPop ?? false; final canPop = ModalRoute.of(context)?.canPop ?? false;
return AppBar( return AppBar(
backgroundColor: context.conduitTheme.surfaceBackground, backgroundColor: context.sidebarTheme.background,
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
elevation: Elevation.none, elevation: Elevation.none,
toolbarHeight: kToolbarHeight, toolbarHeight: kToolbarHeight,
@@ -116,7 +116,7 @@ class AppCustomizationPage extends ConsumerWidget {
String currentLanguageCode, String currentLanguageCode,
String languageLabel, String languageLabel,
AppSettings settings, AppSettings settings,
AppColorPalette palette, TweakcnThemeDefinition activeTheme,
) { ) {
final theme = context.conduitTheme; final theme = context.conduitTheme;
@@ -126,13 +126,13 @@ class AppCustomizationPage extends ConsumerWidget {
Text( Text(
AppLocalizations.of(context)!.display, AppLocalizations.of(context)!.display,
style: style:
theme.headingSmall?.copyWith(color: theme.textPrimary) ?? theme.headingSmall?.copyWith(color: theme.sidebarForeground) ??
TextStyle(color: theme.textPrimary, fontSize: 18), TextStyle(color: theme.sidebarForeground, fontSize: 18),
), ),
const SizedBox(height: Spacing.sm), const SizedBox(height: Spacing.sm),
_buildThemeSelector(context, ref, themeMode, themeDescription), _buildThemeSelector(context, ref, themeMode, themeDescription),
const SizedBox(height: Spacing.md), const SizedBox(height: Spacing.md),
_buildPaletteSelector(context, ref, palette), _buildPaletteSelector(context, ref, activeTheme),
const SizedBox(height: Spacing.md), const SizedBox(height: Spacing.md),
_CustomizationTile( _CustomizationTile(
leading: _buildIconBadge( leading: _buildIconBadge(
@@ -196,7 +196,7 @@ class AppCustomizationPage extends ConsumerWidget {
Text( Text(
AppLocalizations.of(context)!.darkMode, AppLocalizations.of(context)!.darkMode,
style: theme.bodyMedium?.copyWith( style: theme.bodyMedium?.copyWith(
color: theme.textPrimary, color: theme.sidebarForeground,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
@@ -204,7 +204,7 @@ class AppCustomizationPage extends ConsumerWidget {
Text( Text(
themeDescription, themeDescription,
style: theme.bodySmall?.copyWith( style: theme.bodySmall?.copyWith(
color: theme.textSecondary, color: theme.sidebarForeground.withValues(alpha: 0.75),
), ),
), ),
], ],
@@ -260,10 +260,10 @@ class AppCustomizationPage extends ConsumerWidget {
Widget _buildPaletteSelector( Widget _buildPaletteSelector(
BuildContext context, BuildContext context,
WidgetRef ref, WidgetRef ref,
AppColorPalette activePalette, TweakcnThemeDefinition activeTheme,
) { ) {
final theme = context.conduitTheme; final theme = context.conduitTheme;
final palettes = AppColorPalettes.all; final palettes = TweakcnThemes.all;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -272,17 +272,22 @@ class AppCustomizationPage extends ConsumerWidget {
AppLocalizations.of(context)!.themePalette, AppLocalizations.of(context)!.themePalette,
style: style:
theme.bodyLarge?.copyWith( theme.bodyLarge?.copyWith(
color: theme.textPrimary, color: theme.sidebarForeground,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
) ?? ) ??
TextStyle(color: theme.textPrimary, fontWeight: FontWeight.w600), TextStyle(
color: theme.sidebarForeground,
fontWeight: FontWeight.w600,
),
), ),
const SizedBox(height: Spacing.xs), const SizedBox(height: Spacing.xs),
Text( Text(
AppLocalizations.of(context)!.themePaletteDescription, AppLocalizations.of(context)!.themePaletteDescription,
style: style:
theme.bodySmall?.copyWith(color: theme.textSecondary) ?? theme.bodySmall?.copyWith(
TextStyle(color: theme.textSecondary), color: theme.sidebarForeground.withValues(alpha: 0.75),
) ??
TextStyle(color: theme.sidebarForeground.withValues(alpha: 0.75)),
), ),
const SizedBox(height: Spacing.sm), const SizedBox(height: Spacing.sm),
ConduitCard( ConduitCard(
@@ -291,8 +296,8 @@ class AppCustomizationPage extends ConsumerWidget {
children: [ children: [
for (final palette in palettes) for (final palette in palettes)
_PaletteOption( _PaletteOption(
palette: palette, themeDefinition: palette,
activeId: activePalette.id, activeId: activeTheme.id,
onSelect: () => ref onSelect: () => ref
.read(appThemePaletteProvider.notifier) .read(appThemePaletteProvider.notifier)
.setPalette(palette.id), .setPalette(palette.id),
@@ -378,8 +383,8 @@ class AppCustomizationPage extends ConsumerWidget {
Text( Text(
AppLocalizations.of(context)!.onboardQuickTitle, AppLocalizations.of(context)!.onboardQuickTitle,
style: style:
theme.headingSmall?.copyWith(color: theme.textPrimary) ?? theme.headingSmall?.copyWith(color: theme.sidebarForeground) ??
TextStyle(color: theme.textPrimary, fontSize: 18), TextStyle(color: theme.sidebarForeground, fontSize: 18),
), ),
const SizedBox(height: Spacing.sm), const SizedBox(height: Spacing.sm),
ConduitCard( ConduitCard(
@@ -403,7 +408,7 @@ class AppCustomizationPage extends ConsumerWidget {
child: Text( child: Text(
AppLocalizations.of(context)!.quickActionsDescription, AppLocalizations.of(context)!.quickActionsDescription,
style: theme.bodySmall?.copyWith( style: theme.bodySmall?.copyWith(
color: theme.textSecondary, color: theme.sidebarForeground.withValues(alpha: 0.75),
), ),
), ),
), ),
@@ -461,8 +466,8 @@ class AppCustomizationPage extends ConsumerWidget {
Text( Text(
l10n.chatSettings, l10n.chatSettings,
style: style:
theme.headingSmall?.copyWith(color: theme.textPrimary) ?? theme.headingSmall?.copyWith(color: theme.sidebarForeground) ??
TextStyle(color: theme.textPrimary, fontSize: 18), TextStyle(color: theme.sidebarForeground, fontSize: 18),
), ),
const SizedBox(height: Spacing.sm), const SizedBox(height: Spacing.sm),
_CustomizationTile( _CustomizationTile(
@@ -500,8 +505,8 @@ class AppCustomizationPage extends ConsumerWidget {
Text( Text(
l10n.ttsSettings, l10n.ttsSettings,
style: style:
theme.headingSmall?.copyWith(color: theme.textPrimary) ?? theme.headingSmall?.copyWith(color: theme.sidebarForeground) ??
TextStyle(color: theme.textPrimary, fontSize: 18), TextStyle(color: theme.sidebarForeground, fontSize: 18),
), ),
const SizedBox(height: Spacing.sm), const SizedBox(height: Spacing.sm),
// Voice Selection // Voice Selection
@@ -621,20 +626,23 @@ class AppCustomizationPage extends ConsumerWidget {
title, title,
style: style:
theme.bodyMedium?.copyWith( theme.bodyMedium?.copyWith(
color: theme.textPrimary, color: theme.sidebarForeground,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
) ?? ) ??
TextStyle(color: theme.textPrimary, fontSize: 14), TextStyle(color: theme.sidebarForeground, fontSize: 14),
), ),
), ),
Text( Text(
label, label,
style: style:
theme.bodyMedium?.copyWith( theme.bodyMedium?.copyWith(
color: theme.textSecondary, color: theme.sidebarForeground.withValues(alpha: 0.75),
fontWeight: FontWeight.w500, 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<void>( showModalBottomSheet<void>(
context: context, context: context,
backgroundColor: theme.surfaceBackground, backgroundColor: theme.sidebarBackground,
isScrollControlled: true, isScrollControlled: true,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)), borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
@@ -741,10 +749,10 @@ class AppCustomizationPage extends ConsumerWidget {
l10n.ttsSelectVoice, l10n.ttsSelectVoice,
style: style:
theme.headingSmall?.copyWith( theme.headingSmall?.copyWith(
color: theme.textPrimary, color: theme.sidebarForeground,
) ?? ) ??
TextStyle( TextStyle(
color: theme.textPrimary, color: theme.sidebarForeground,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -765,18 +773,18 @@ class AppCustomizationPage extends ConsumerWidget {
ios: CupertinoIcons.speaker_3, ios: CupertinoIcons.speaker_3,
android: Icons.record_voice_over, android: Icons.record_voice_over,
), ),
color: theme.textPrimary, color: theme.sidebarForeground,
), ),
title: Text( title: Text(
l10n.ttsSystemDefault, l10n.ttsSystemDefault,
style: style:
theme.bodyMedium?.copyWith( theme.bodyMedium?.copyWith(
color: theme.textPrimary, color: theme.sidebarForeground,
fontWeight: settings.ttsVoice == null fontWeight: settings.ttsVoice == null
? FontWeight.bold ? FontWeight.bold
: FontWeight.normal, : FontWeight.normal,
) ?? ) ??
TextStyle(color: theme.textPrimary), TextStyle(color: theme.sidebarForeground),
), ),
trailing: settings.ttsVoice == null trailing: settings.ttsVoice == null
? Icon(Icons.check, color: theme.buttonPrimary) ? Icon(Icons.check, color: theme.buttonPrimary)
@@ -809,11 +817,15 @@ class AppCustomizationPage extends ConsumerWidget {
), ),
style: style:
theme.bodySmall?.copyWith( theme.bodySmall?.copyWith(
color: theme.textSecondary, color: theme.sidebarForeground.withValues(
alpha: 0.75,
),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
) ?? ) ??
TextStyle( TextStyle(
color: theme.textSecondary, color: theme.sidebarForeground.withValues(
alpha: 0.75,
),
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -831,11 +843,15 @@ class AppCustomizationPage extends ConsumerWidget {
l10n.ttsOtherVoices, l10n.ttsOtherVoices,
style: style:
theme.bodySmall?.copyWith( theme.bodySmall?.copyWith(
color: theme.textSecondary, color: theme.sidebarForeground.withValues(
alpha: 0.75,
),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
) ?? ) ??
TextStyle( TextStyle(
color: theme.textSecondary, color: theme.sidebarForeground.withValues(
alpha: 0.75,
),
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -866,28 +882,32 @@ class AppCustomizationPage extends ConsumerWidget {
ios: CupertinoIcons.person_fill, ios: CupertinoIcons.person_fill,
android: Icons.person, android: Icons.person,
), ),
color: theme.textPrimary, color: theme.sidebarForeground,
), ),
title: Text( title: Text(
displayName, displayName,
style: style:
theme.bodyMedium?.copyWith( theme.bodyMedium?.copyWith(
color: theme.textPrimary, color: theme.sidebarForeground,
fontWeight: isSelected fontWeight: isSelected
? FontWeight.bold ? FontWeight.bold
: FontWeight.normal, : FontWeight.normal,
) ?? ) ??
TextStyle(color: theme.textPrimary), TextStyle(color: theme.sidebarForeground),
), ),
subtitle: subtitle.isNotEmpty subtitle: subtitle.isNotEmpty
? Text( ? Text(
subtitle, subtitle,
style: style:
theme.bodySmall?.copyWith( theme.bodySmall?.copyWith(
color: theme.textSecondary, color: theme.sidebarForeground.withValues(
alpha: 0.75,
),
) ?? ) ??
TextStyle( TextStyle(
color: theme.textSecondary, color: theme.sidebarForeground.withValues(
alpha: 0.75,
),
fontSize: 12, fontSize: 12,
), ),
) )
@@ -1162,7 +1182,7 @@ class AppCustomizationPage extends ConsumerWidget {
isScrollControlled: true, isScrollControlled: true,
builder: (context) => Container( builder: (context) => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.conduitTheme.surfaceBackground, color: context.sidebarTheme.background,
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.modal), top: Radius.circular(AppBorderRadius.modal),
), ),
@@ -1230,26 +1250,20 @@ class AppCustomizationPage extends ConsumerWidget {
class _PaletteOption extends StatelessWidget { class _PaletteOption extends StatelessWidget {
const _PaletteOption({ const _PaletteOption({
required this.palette, required this.themeDefinition,
required this.activeId, required this.activeId,
required this.onSelect, required this.onSelect,
}); });
final AppColorPalette palette; final TweakcnThemeDefinition themeDefinition;
final String activeId; final String activeId;
final VoidCallback onSelect; final VoidCallback onSelect;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.conduitTheme; final theme = context.conduitTheme;
final isSelected = palette.id == activeId; final isSelected = themeDefinition.id == activeId;
final previewColors = final previewColors = themeDefinition.preview;
palette.preview ??
<Color>[
palette.light.primary,
palette.light.secondary,
palette.dark.primary,
];
return InkWell( return InkWell(
onTap: onSelect, onTap: onSelect,
@@ -1274,9 +1288,9 @@ class _PaletteOption extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
palette.label, themeDefinition.label,
style: theme.bodyMedium?.copyWith( style: theme.bodyMedium?.copyWith(
color: theme.textPrimary, color: theme.sidebarForeground,
fontWeight: isSelected fontWeight: isSelected
? FontWeight.w600 ? FontWeight.w600
: FontWeight.w500, : FontWeight.w500,
@@ -1297,10 +1311,18 @@ class _PaletteOption extends StatelessWidget {
), ),
const SizedBox(height: Spacing.xxs), const SizedBox(height: Spacing.xxs),
Text( Text(
palette.description, themeDefinition.description,
style: style:
theme.bodySmall?.copyWith(color: theme.textSecondary) ?? theme.bodySmall?.copyWith(
TextStyle(color: theme.textSecondary), color: theme.sidebarForeground.withValues(
alpha: 0.75,
),
) ??
TextStyle(
color: theme.sidebarForeground.withValues(
alpha: 0.75,
),
),
), ),
const SizedBox(height: Spacing.xs), const SizedBox(height: Spacing.xs),
Row( Row(
@@ -1378,14 +1400,16 @@ class _CustomizationTile extends StatelessWidget {
Text( Text(
title, title,
style: theme.bodyMedium?.copyWith( style: theme.bodyMedium?.copyWith(
color: theme.textPrimary, color: theme.sidebarForeground,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(height: Spacing.xs), const SizedBox(height: Spacing.xs),
Text( Text(
subtitle, subtitle,
style: theme.bodySmall?.copyWith(color: theme.textSecondary), style: theme.bodySmall?.copyWith(
color: theme.sidebarForeground.withValues(alpha: 0.75),
),
), ),
], ],
), ),

View File

@@ -79,7 +79,7 @@ class ProfilePage extends ConsumerWidget {
Scaffold _buildScaffold(BuildContext context, {required Widget body}) { Scaffold _buildScaffold(BuildContext context, {required Widget body}) {
return Scaffold( return Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground, backgroundColor: context.sidebarTheme.background,
appBar: _buildAppBar(context), appBar: _buildAppBar(context),
body: body, body: body,
); );
@@ -88,7 +88,7 @@ class ProfilePage extends ConsumerWidget {
PreferredSizeWidget _buildAppBar(BuildContext context) { PreferredSizeWidget _buildAppBar(BuildContext context) {
final canPop = ModalRoute.of(context)?.canPop ?? false; final canPop = ModalRoute.of(context)?.canPop ?? false;
return AppBar( return AppBar(
backgroundColor: context.conduitTheme.surfaceBackground, backgroundColor: context.sidebarTheme.background,
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
elevation: Elevation.none, elevation: Elevation.none,
toolbarHeight: kToolbarHeight, toolbarHeight: kToolbarHeight,
@@ -156,8 +156,10 @@ class ProfilePage extends ConsumerWidget {
Widget _buildSupportSection(BuildContext context) { Widget _buildSupportSection(BuildContext context) {
final theme = context.conduitTheme; final theme = context.conduitTheme;
final textTheme = final textTheme =
theme.bodySmall?.copyWith(color: theme.textSecondary) ?? theme.bodySmall?.copyWith(
TextStyle(color: theme.textSecondary); color: theme.sidebarForeground.withValues(alpha: 0.75),
) ??
TextStyle(color: theme.sidebarForeground.withValues(alpha: 0.75));
final supportTiles = [ final supportTiles = [
_buildSupportOption( _buildSupportOption(
@@ -189,7 +191,7 @@ class ProfilePage extends ConsumerWidget {
children: [ children: [
Text( Text(
AppLocalizations.of(context)!.supportConduit, AppLocalizations.of(context)!.supportConduit,
style: theme.headingSmall?.copyWith(color: theme.textPrimary), style: theme.headingSmall?.copyWith(color: theme.sidebarForeground),
), ),
const SizedBox(height: Spacing.xs), const SizedBox(height: Spacing.xs),
Text( Text(
@@ -216,7 +218,6 @@ class ProfilePage extends ConsumerWidget {
final theme = context.conduitTheme; final theme = context.conduitTheme;
return _ProfileSettingTile( return _ProfileSettingTile(
onTap: () => _openExternalLink(context, url), onTap: () => _openExternalLink(context, url),
isDestructive: false,
leading: _buildIconBadge(context, icon, color: color), leading: _buildIconBadge(context, icon, color: color),
title: title, title: title,
subtitle: subtitle, subtitle: subtitle,
@@ -287,15 +288,14 @@ class ProfilePage extends ConsumerWidget {
final email = extractEmail(user) ?? 'No email'; final email = extractEmail(user) ?? 'No email';
final theme = context.conduitTheme; final theme = context.conduitTheme;
final accent = theme.buttonPrimary;
return Container( return Container(
padding: const EdgeInsets.all(Spacing.md), padding: const EdgeInsets.all(Spacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: accent.withValues(alpha: 0.08), color: theme.sidebarAccent.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(AppBorderRadius.large), borderRadius: BorderRadius.circular(AppBorderRadius.large),
border: Border.all( border: Border.all(
color: accent.withValues(alpha: 0.15), color: theme.sidebarBorder.withValues(alpha: 0.6),
width: BorderWidth.thin, width: BorderWidth.thin,
), ),
), ),
@@ -314,7 +314,7 @@ class ProfilePage extends ConsumerWidget {
Text( Text(
displayName, displayName,
style: theme.headingMedium?.copyWith( style: theme.headingMedium?.copyWith(
color: theme.textPrimary, color: theme.sidebarForeground,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
@@ -327,14 +327,18 @@ class ProfilePage extends ConsumerWidget {
android: Icons.mail_outline, android: Icons.mail_outline,
), ),
size: IconSize.small, size: IconSize.small,
color: theme.textSecondary, color: theme.sidebarForeground.withValues(
alpha: 0.75,
),
), ),
const SizedBox(width: Spacing.xs), const SizedBox(width: Spacing.xs),
Flexible( Flexible(
child: Text( child: Text(
email, email,
style: theme.bodySmall?.copyWith( style: theme.bodySmall?.copyWith(
color: theme.textSecondary, color: theme.sidebarForeground.withValues(
alpha: 0.75,
),
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1, maxLines: 1,
@@ -377,7 +381,6 @@ class ProfilePage extends ConsumerWidget {
title: AppLocalizations.of(context)!.signOut, title: AppLocalizations.of(context)!.signOut,
subtitle: AppLocalizations.of(context)!.endYourSession, subtitle: AppLocalizations.of(context)!.endYourSession,
onTap: () => _signOut(context, ref), onTap: () => _signOut(context, ref),
isDestructive: true,
showChevron: false, showChevron: false,
), ),
]; ];
@@ -399,14 +402,12 @@ class ProfilePage extends ConsumerWidget {
required String title, required String title,
required String subtitle, required String subtitle,
required VoidCallback onTap, required VoidCallback onTap,
bool isDestructive = false,
bool showChevron = true, bool showChevron = true,
}) { }) {
final theme = context.conduitTheme; final theme = context.conduitTheme;
final color = isDestructive ? theme.error : theme.buttonPrimary; final color = theme.buttonPrimary;
return _ProfileSettingTile( return _ProfileSettingTile(
onTap: onTap, onTap: onTap,
isDestructive: isDestructive,
leading: _buildIconBadge(context, icon, color: color), leading: _buildIconBadge(context, icon, color: color),
title: title, title: title,
subtitle: subtitle, subtitle: subtitle,
@@ -456,7 +457,7 @@ class ProfilePage extends ConsumerWidget {
width: 40, width: 40,
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.surfaceBackground.withValues(alpha: 0.85), color: theme.sidebarAccent.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(AppBorderRadius.small), borderRadius: BorderRadius.circular(AppBorderRadius.small),
border: Border.all( border: Border.all(
color: theme.cardBorder, color: theme.cardBorder,
@@ -518,11 +519,10 @@ class ProfilePage extends ConsumerWidget {
ios: CupertinoIcons.exclamationmark_triangle, ios: CupertinoIcons.exclamationmark_triangle,
android: Icons.error_outline, android: Icons.error_outline,
), ),
color: context.conduitTheme.error, color: Colors.red,
), ),
title: AppLocalizations.of(context)!.defaultModel, title: AppLocalizations.of(context)!.defaultModel,
subtitle: AppLocalizations.of(context)!.failedToLoadModels, subtitle: AppLocalizations.of(context)!.failedToLoadModels,
isDestructive: true,
showChevron: false, showChevron: false,
onTap: () => ref.invalidate(modelsProvider), onTap: () => ref.invalidate(modelsProvider),
trailing: IconButton( trailing: IconButton(
@@ -533,7 +533,7 @@ class ProfilePage extends ConsumerWidget {
ios: CupertinoIcons.refresh, ios: CupertinoIcons.refresh,
android: Icons.refresh, android: Icons.refresh,
), ),
color: context.conduitTheme.error, color: Colors.red,
size: IconSize.small, size: IconSize.small,
), ),
), ),
@@ -589,11 +589,11 @@ class ProfilePage extends ConsumerWidget {
context: context, context: context,
builder: (ctx) { builder: (ctx) {
return AlertDialog( return AlertDialog(
backgroundColor: ctx.conduitTheme.surfaceBackground, backgroundColor: ctx.sidebarTheme.background,
title: Text( title: Text(
AppLocalizations.of(ctx)!.aboutConduit, AppLocalizations.of(ctx)!.aboutConduit,
style: ctx.conduitTheme.headingSmall?.copyWith( style: ctx.conduitTheme.headingSmall?.copyWith(
color: ctx.conduitTheme.textPrimary, color: ctx.sidebarTheme.foreground,
), ),
), ),
content: Column( content: Column(
@@ -605,7 +605,7 @@ class ProfilePage extends ConsumerWidget {
ctx, ctx,
)!.versionLabel(info.version, info.buildNumber), )!.versionLabel(info.version, info.buildNumber),
style: ctx.conduitTheme.bodyMedium?.copyWith( style: ctx.conduitTheme.bodyMedium?.copyWith(
color: ctx.conduitTheme.textSecondary, color: ctx.sidebarTheme.foreground.withValues(alpha: 0.75),
), ),
), ),
const SizedBox(height: Spacing.md), const SizedBox(height: Spacing.md),
@@ -704,7 +704,6 @@ class _ProfileSettingTile extends StatelessWidget {
required this.subtitle, required this.subtitle,
this.onTap, this.onTap,
this.trailing, this.trailing,
this.isDestructive = false,
this.showChevron = true, this.showChevron = true,
}); });
@@ -713,16 +712,13 @@ class _ProfileSettingTile extends StatelessWidget {
final String subtitle; final String subtitle;
final VoidCallback? onTap; final VoidCallback? onTap;
final Widget? trailing; final Widget? trailing;
final bool isDestructive;
final bool showChevron; final bool showChevron;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.conduitTheme; final theme = context.conduitTheme;
final textColor = isDestructive ? theme.error : theme.textPrimary; final textColor = theme.sidebarForeground;
final subtitleColor = isDestructive final subtitleColor = theme.sidebarForeground.withValues(alpha: 0.75);
? theme.error.withValues(alpha: 0.85)
: theme.textSecondary;
return ConduitCard( return ConduitCard(
padding: const EdgeInsets.all(Spacing.md), padding: const EdgeInsets.all(Spacing.md),
@@ -888,7 +884,7 @@ class _DefaultModelBottomSheetState
builder: (context, scrollController) { builder: (context, scrollController) {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.conduitTheme.surfaceBackground, color: context.sidebarTheme.background,
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.bottomSheet), top: Radius.circular(AppBorderRadius.bottomSheet),
), ),
@@ -1010,8 +1006,9 @@ class _DefaultModelBottomSheetState
vertical: 2, vertical: 2,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.conduitTheme.surfaceBackground color: context.sidebarTheme.background.withValues(
.withValues(alpha: 0.6), alpha: 0.6,
),
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
AppBorderRadius.xs, AppBorderRadius.xs,
), ),
@@ -1141,7 +1138,7 @@ class _DefaultModelBottomSheetState
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? context.conduitTheme.buttonPrimary.withValues(alpha: 0.1) ? 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), borderRadius: BorderRadius.circular(AppBorderRadius.small),
border: Border.all( border: Border.all(
color: isSelected color: isSelected

View File

@@ -3,7 +3,7 @@ import '../theme/theme_extensions.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import '../theme/color_tokens.dart'; import '../theme/color_tokens.dart';
import '../theme/color_palettes.dart'; import '../theme/tweakcn_themes.dart';
/// Centralized service for consistent brand identity throughout the app /// Centralized service for consistent brand identity throughout the app
/// Uses the hub icon as the primary brand element /// Uses the hub icon as the primary brand element
@@ -27,7 +27,7 @@ class BrandService {
}) { }) {
final palette = _resolvePalette(context); final palette = _resolvePalette(context);
final resolvedBrightness = brightness ?? _resolveBrightness(context); final resolvedBrightness = brightness ?? _resolveBrightness(context);
return palette.primaryFor(resolvedBrightness); return palette.variantFor(resolvedBrightness).primary;
} }
static Color secondaryBrandColor({ static Color secondaryBrandColor({
@@ -36,7 +36,7 @@ class BrandService {
}) { }) {
final palette = _resolvePalette(context); final palette = _resolvePalette(context);
final resolvedBrightness = brightness ?? _resolveBrightness(context); final resolvedBrightness = brightness ?? _resolveBrightness(context);
return palette.secondaryFor(resolvedBrightness); return palette.variantFor(resolvedBrightness).secondary;
} }
static Color accentBrandColor({ static Color accentBrandColor({
@@ -45,7 +45,7 @@ class BrandService {
}) { }) {
final palette = _resolvePalette(context); final palette = _resolvePalette(context);
final resolvedBrightness = brightness ?? _resolveBrightness(context); final resolvedBrightness = brightness ?? _resolveBrightness(context);
return palette.accentFor(resolvedBrightness); return palette.variantFor(resolvedBrightness).accent;
} }
/// Creates a branded icon with consistent styling /// 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) { if (context == null) {
return AppColorPalettes.auroraViolet; return TweakcnThemes.t3Chat;
} }
final extension = Theme.of(context).extension<AppPaletteThemeExtension>(); final extension = Theme.of(context).extension<AppPaletteThemeExtension>();
return extension?.palette ?? AppColorPalettes.auroraViolet; return extension?.palette ?? TweakcnThemes.t3Chat;
} }
static Brightness _resolveBrightness(BuildContext? context) { static Brightness _resolveBrightness(BuildContext? context) {
@@ -343,7 +343,7 @@ class BrandService {
final palette = _resolvePalette(context); final palette = _resolvePalette(context);
final brightness = _resolveBrightness(context); final brightness = _resolveBrightness(context);
return brightness == Brightness.dark return brightness == Brightness.dark
? AppColorTokens.dark(palette: palette) ? AppColorTokens.dark(theme: palette)
: AppColorTokens.light(palette: palette); : AppColorTokens.light(theme: palette);
} }
} }

View File

@@ -4,255 +4,251 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'theme_extensions.dart'; import 'theme_extensions.dart';
import 'color_palettes.dart'; import 'tweakcn_themes.dart';
import 'color_tokens.dart'; import 'color_tokens.dart';
class AppTheme { class AppTheme {
// Enhanced neutral palette for better contrast (WCAG AA compliant) static ThemeData light(TweakcnThemeDefinition theme) {
static const Color neutral900 = Color(0xFF0B0E14); final tokens = AppColorTokens.light(theme: theme);
static const Color neutral800 = Color(0xFF161B24); return _buildTheme(
static const Color neutral700 = Color(0xFF1F2531); theme: theme,
static const Color neutral600 = Color(0xFF343C4D); tokens: tokens,
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,
brightness: Brightness.light, 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: <ThemeExtension<dynamic>>[
tokens,
ConduitThemeExtension.lightPalette(palette: palette, tokens: tokens),
AppPaletteThemeExtension(palette: palette),
],
); );
} }
static ThemeData dark(AppColorPalette palette) { static ThemeData dark(TweakcnThemeDefinition theme) {
final darkTone = palette.dark; final tokens = AppColorTokens.dark(theme: theme);
final tokens = AppColorTokens.dark(palette: palette); return _buildTheme(
final colorScheme = tokens.toColorScheme().copyWith( theme: theme,
primary: darkTone.primary, tokens: tokens,
onPrimary: _pickOnColor(darkTone.primary, tokens),
secondary: darkTone.secondary,
onSecondary: _pickOnColor(darkTone.secondary, tokens),
tertiary: darkTone.accent,
onTertiary: _pickOnColor(darkTone.accent, tokens),
surfaceTint: darkTone.primary,
);
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark, 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: <ThemeExtension<dynamic>>[
tokens,
ConduitThemeExtension.darkPalette(palette: palette, tokens: tokens),
AppPaletteThemeExtension(palette: palette),
],
); );
} }
static CupertinoThemeData cupertinoTheme( static CupertinoThemeData cupertinoTheme(
BuildContext context, BuildContext context,
AppColorPalette palette, TweakcnThemeDefinition theme,
) { ) {
final brightness = Theme.of(context).brightness; final brightness = Theme.of(context).brightness;
final tone = palette.toneFor(brightness); final variant = theme.variantFor(brightness);
final tokens = brightness == Brightness.dark final tokens = brightness == Brightness.dark
? AppColorTokens.dark(palette: palette) ? AppColorTokens.dark(theme: theme)
: AppColorTokens.light(palette: palette); : AppColorTokens.light(theme: theme);
return CupertinoThemeData( return CupertinoThemeData(
brightness: brightness, brightness: brightness,
primaryColor: tone.primary, primaryColor: variant.primary,
scaffoldBackgroundColor: tokens.neutralTone10, scaffoldBackgroundColor: tokens.neutralTone10,
barBackgroundColor: 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: <ThemeExtension<dynamic>>[
tokens,
typography,
surfaces,
shadows,
shapes,
sidebar,
conduitExtension,
AppPaletteThemeExtension(palette: theme),
],
);
}
static Color _pickOnColor(Color background, AppColorTokens tokens) { static Color _pickOnColor(Color background, AppColorTokens tokens) {
final contrastOnLight = _contrastRatio(background, tokens.neutralTone00); final contrastOnLight = _contrastRatio(background, tokens.neutralTone00);
final contrastOnDark = _contrastRatio(background, tokens.neutralOnSurface); final contrastOnDark = _contrastRatio(background, tokens.neutralOnSurface);

View File

@@ -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<Color>? preview;
}
@immutable
class AppPaletteThemeExtension
extends ThemeExtension<AppPaletteThemeExtension> {
const AppPaletteThemeExtension({required this.palette});
final AppColorPalette palette;
@override
AppPaletteThemeExtension copyWith({AppColorPalette? palette}) {
return AppPaletteThemeExtension(palette: palette ?? this.palette);
}
@override
AppPaletteThemeExtension lerp(
covariant ThemeExtension<AppPaletteThemeExtension>? other,
double t,
) {
if (other is! AppPaletteThemeExtension) return this;
return t < 0.5 ? this : other;
}
}
class AppColorPalettes {
static const String defaultPaletteId = 'aurora_violet';
static const AppColorPalette auroraViolet = AppColorPalette(
id: defaultPaletteId,
label: 'Aurora Violet',
description: 'Bold purples inspired by aurora skies.',
light: AppPaletteTone(
primary: Color(0xFFA420FF),
secondary: Color(0xFFB058FF),
accent: Color(0xFFD9A5FF),
),
dark: AppPaletteTone(
primary: Color(0xFF9500FF),
secondary: Color(0xFFC773FF),
accent: Color(0xFFE3BDFF),
),
preview: [Color(0xFF9500FF), Color(0xFFA420FF), Color(0xFFB058FF)],
);
static const AppColorPalette emeraldRush = AppColorPalette(
id: 'emerald_rush',
label: 'Emerald Rush',
description: 'High-contrast greens with calm highlights.',
light: AppPaletteTone(
primary: Color(0xFF0C7F48),
secondary: Color(0xFF26A164),
accent: Color(0xFF6DE0A4),
),
dark: AppPaletteTone(
primary: Color(0xFF40DD7F),
secondary: Color(0xFF26A164),
accent: Color(0xFF6DE0A4),
),
preview: [Color(0xFF0C7F48), Color(0xFF26A164), Color(0xFF40DD7F)],
);
static const AppColorPalette azurePulse = AppColorPalette(
id: 'azure_pulse',
label: 'Azure Pulse',
description: 'Electric blues with crisp highlights.',
light: AppPaletteTone(
primary: Color(0xFF1B64DA),
secondary: Color(0xFF2E7AF0),
accent: Color(0xFF6DA6FF),
),
dark: AppPaletteTone(
primary: Color(0xFF37C7FF),
secondary: Color(0xFF2E7AF0),
accent: Color(0xFF6DA6FF),
),
preview: [Color(0xFF1B64DA), Color(0xFF2E7AF0), Color(0xFF37C7FF)],
);
static const AppColorPalette sunsetGlow = AppColorPalette(
id: 'sunset_glow',
label: 'Sunset Glow',
description: 'Warm oranges for energetic interfaces.',
light: AppPaletteTone(
primary: Color(0xFFB83200),
secondary: Color(0xFFE65100),
accent: Color(0xFFFFA05B),
),
dark: AppPaletteTone(
primary: Color(0xFFFF8A00),
secondary: Color(0xFFE65100),
accent: Color(0xFFFFA05B),
),
preview: [Color(0xFFB83200), Color(0xFFE65100), Color(0xFFFF8A00)],
);
static const List<AppColorPalette> all = [
auroraViolet,
emeraldRush,
azurePulse,
sunsetGlow,
];
static AppColorPalette byId(String? id) {
return all.firstWhere(
(palette) => palette.id == id,
orElse: () => auroraViolet,
);
}
}
extension AppColorPaletteX on AppColorPalette {
AppPaletteTone toneFor(Brightness brightness) {
return brightness == Brightness.dark ? dark : light;
}
Color primaryFor(Brightness brightness) {
return toneFor(brightness).primary;
}
Color secondaryFor(Brightness brightness) {
return toneFor(brightness).secondary;
}
Color accentFor(Brightness brightness) {
return toneFor(brightness).accent;
}
}

View File

@@ -2,7 +2,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'color_palettes.dart'; import 'tweakcn_themes.dart';
/// Immutable set of semantic color tokens exposed through [ThemeExtension]. /// Immutable set of semantic color tokens exposed through [ThemeExtension].
/// ///
@@ -92,136 +92,118 @@ class AppColorTokens extends ThemeExtension<AppColorTokens> {
final Color codeText; final Color codeText;
final Color codeAccent; final Color codeAccent;
factory AppColorTokens.light({AppColorPalette? palette}) { factory AppColorTokens.light({TweakcnThemeDefinition? theme}) {
return AppColorTokens._fromPalette( return AppColorTokens._fromTheme(
palette ?? AppColorPalettes.auroraViolet, theme ?? TweakcnThemes.conduit,
Brightness.light, Brightness.light,
); );
} }
factory AppColorTokens.dark({AppColorPalette? palette}) { factory AppColorTokens.dark({TweakcnThemeDefinition? theme}) {
return AppColorTokens._fromPalette( return AppColorTokens._fromTheme(
palette ?? AppColorPalettes.auroraViolet, theme ?? TweakcnThemes.conduit,
Brightness.dark, Brightness.dark,
); );
} }
factory AppColorTokens._fromPalette( factory AppColorTokens._fromTheme(
AppColorPalette palette, TweakcnThemeDefinition theme,
Brightness brightness, Brightness brightness,
) { ) {
final AppPaletteTone tone = palette.toneFor(brightness); final TweakcnThemeVariant variant = theme.variantFor(brightness);
final bool isLight = brightness == Brightness.light; final bool isLight = brightness == Brightness.light;
final Color neutralTone00 = isLight final Color neutralTone00 = variant.background;
? const Color(0xFFFFFFFF) final Color neutralTone20 = variant.card;
: const Color(0xFF0B0E14); final Color neutralTone10 = mix(neutralTone00, neutralTone20, 0.5);
final Color neutralTone10 = isLight final Color neutralTone40 = variant.muted;
? const Color(0xFFF5F7FA) final Color neutralTone60 = mix(
: const Color(0xFF161B24); variant.mutedForeground,
final Color neutralTone20 = isLight variant.foreground,
? const Color(0xFFE6EAF1) isLight ? 0.25 : 0.4,
: const Color(0xFF1F2531); );
final Color neutralTone40 = isLight final Color neutralTone80 = mix(
? const Color(0xFFC5CCD9) variant.foreground,
: const Color(0xFF343C4D); isLight ? Colors.black : Colors.white,
final Color neutralTone60 = isLight isLight ? 0.06 : 0.3,
? const Color(0xFF9099AC) );
: const Color(0xFF4C566A); final Color neutralOnSurface = _ensureContrast(
final Color neutralTone80 = isLight surface: neutralTone00,
? const Color(0xFF4A5161) foreground: variant.foreground,
: const Color(0xFF8B95AA); minContrast: 4.5,
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 brandTone60 = seedScheme.primary; final Color brandTone60 = variant.primary;
final Color brandOn60 = _preferredOnColor( final Color brandOn60 = _ensureContrast(
background: brandTone60, surface: brandTone60,
light: neutralTone00, foreground: variant.primaryForeground,
dark: neutralOnSurface, );
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 accentIndigo60 = variant.secondary;
final Color brandOn90 = _preferredOnColor( final Color accentOnIndigo60 = _ensureContrast(
background: brandTone90, surface: accentIndigo60,
light: neutralTone00, foreground: variant.secondaryForeground,
dark: neutralOnSurface, );
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 statusError60 = variant.destructive;
final Color brandTone40 = _shiftLightness(brandTone60, brandShift); final Color statusOnError60 = _ensureContrast(
surface: statusError60,
final Color accentIndigo60 = tone.secondary; foreground: variant.destructiveForeground,
final Color accentOnIndigo60 = _preferredOnColor( );
background: accentIndigo60, final Color statusSuccess60 = variant.success;
light: neutralTone00, final Color statusOnSuccess60 = _ensureContrast(
dark: neutralOnSurface, 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 overlayWeak = neutralOnSurface.withValues(
final Color accentGold60 = isLight alpha: isLight ? 0.08 : 0.12,
? const Color(0xFFFFB54A) );
: const Color(0xFFFFC266); final Color overlayMedium = neutralOnSurface.withValues(
alpha: isLight ? 0.16 : 0.2,
final Color statusSuccess60 = isLight );
? const Color(0xFF0E9D58) final Color overlayStrong = neutralOnSurface.withValues(
: const Color(0xFF23C179); alpha: isLight ? 0.32 : 0.36,
final Color statusOnSuccess60 = _preferredOnColor(
background: statusSuccess60,
light: neutralTone00,
dark: neutralOnSurface,
); );
final Color statusWarning60 = isLight final Color codeBackground = mix(variant.muted, neutralTone00, 0.5);
? const Color(0xFFDB7900) final Color codeBorder = mix(variant.border, neutralTone40, 0.6);
: const Color(0xFFFF9800); final Color codeText = _ensureContrast(
final Color statusOnWarning60 = _preferredOnColor( surface: codeBackground,
background: statusWarning60, foreground: neutralOnSurface,
light: neutralTone00, minContrast: 4.5,
dark: neutralOnSurface,
); );
final Color codeAccent = mix(variant.accent, variant.primary, 0.4);
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);
return AppColorTokens( return AppColorTokens(
brightness: brightness, brightness: brightness,
@@ -406,7 +388,10 @@ class AppColorTokens extends ThemeExtension<AppColorTokens> {
secondary: accentIndigo60, secondary: accentIndigo60,
onSecondary: accentOnIndigo60, onSecondary: accentOnIndigo60,
tertiary: accentTeal60, tertiary: accentTeal60,
onTertiary: neutralTone00, onTertiary: _ensureContrast(
surface: accentTeal60,
foreground: neutralTone00,
),
surface: neutralTone00, surface: neutralTone00,
surfaceContainerLow: neutralTone10, surfaceContainerLow: neutralTone10,
surfaceContainerHighest: neutralTone20, surfaceContainerHighest: neutralTone20,
@@ -433,20 +418,24 @@ class AppColorTokens extends ThemeExtension<AppColorTokens> {
: AppColorTokens.light(); : AppColorTokens.light();
} }
static Color _shiftLightness(Color color, double amount) { static Color _ensureContrast({
final HSLColor hsl = HSLColor.fromColor(color); required Color surface,
final double lightness = (hsl.lightness + amount).clamp(0.0, 1.0); required Color foreground,
return hsl.withLightness(lightness).toColor(); double minContrast = 4.5,
}
static Color _preferredOnColor({
required Color background,
required Color light,
required Color dark,
}) { }) {
final double lightContrast = _contrastRatio(background, light); if (_contrastRatio(surface, foreground) >= minContrast) {
final double darkContrast = _contrastRatio(background, dark); return foreground;
return lightContrast >= darkContrast ? light : dark; }
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) { static double _contrastRatio(Color a, Color b) {

File diff suppressed because it is too large Load Diff

View File

@@ -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 <String>[],
this.fontSerif = const <String>[],
this.fontMono = const <String>[],
});
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<String> fontSans;
final List<String> fontSerif;
final List<String> 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<Color> 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 <String>[
'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 <String>[
'ui-serif',
'Georgia',
'Cambria',
'Times New Roman',
'Times',
'serif',
],
fontMono: const <String>[
'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 <String>[
'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 <String>[
'ui-serif',
'Georgia',
'Cambria',
'Times New Roman',
'Times',
'serif',
],
fontMono: const <String>[
'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>[
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>[
Color(0xFFA1A1AA),
Color(0xFFF4F4F5),
Color(0xFF2563EB),
],
);
static List<TweakcnThemeDefinition> all = [conduit, t3Chat];
static TweakcnThemeDefinition byId(String? id) {
return all.firstWhere((theme) => theme.id == id, orElse: () => conduit);
}
}
@immutable
class AppPaletteThemeExtension
extends ThemeExtension<AppPaletteThemeExtension> {
const AppPaletteThemeExtension({required this.palette});
final TweakcnThemeDefinition palette;
@override
AppPaletteThemeExtension copyWith({TweakcnThemeDefinition? palette}) {
return AppPaletteThemeExtension(palette: palette ?? this.palette);
}
@override
AppPaletteThemeExtension lerp(
covariant ThemeExtension<AppPaletteThemeExtension>? other,
double t,
) {
if (other is! AppPaletteThemeExtension) return this;
return t < 0.5 ? this : other;
}
}

View File

@@ -57,13 +57,15 @@ Future<void> showConduitContextMenu({
isCompact: true, isCompact: true,
leading: Icon( leading: Icon(
Platform.isIOS ? action.cupertinoIcon : action.materialIcon, Platform.isIOS ? action.cupertinoIcon : action.materialIcon,
color: action.destructive ? theme.error : theme.iconPrimary, color: action.destructive ? Colors.red : theme.iconPrimary,
size: IconSize.modal, size: IconSize.modal,
), ),
title: Text( title: Text(
action.label, action.label,
style: AppTypography.standard.copyWith( style: AppTypography.standard.copyWith(
color: action.destructive ? theme.error : theme.textPrimary, color: action.destructive
? Colors.red
: theme.textPrimary,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),

View File

@@ -293,6 +293,8 @@ class ConduitCard extends StatelessWidget {
final bool isSelected; final bool isSelected;
final bool isElevated; final bool isElevated;
final bool isCompact; final bool isCompact;
final Color? backgroundColor;
final Color? borderColor;
const ConduitCard({ const ConduitCard({
super.key, super.key,
@@ -302,6 +304,8 @@ class ConduitCard extends StatelessWidget {
this.isSelected = false, this.isSelected = false,
this.isElevated = false, this.isElevated = false,
this.isCompact = false, this.isCompact = false,
this.backgroundColor,
this.borderColor,
}); });
@override @override
@@ -317,14 +321,14 @@ class ConduitCard extends StatelessWidget {
? context.conduitTheme.buttonPrimary.withValues( ? context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.highlight, alpha: Alpha.highlight,
) )
: context.conduitTheme.cardBackground, : backgroundColor ?? context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.card), borderRadius: BorderRadius.circular(AppBorderRadius.card),
border: Border.all( border: Border.all(
color: isSelected color: isSelected
? context.conduitTheme.buttonPrimary.withValues( ? context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.standard, alpha: Alpha.standard,
) )
: context.conduitTheme.cardBorder, : borderColor ?? context.conduitTheme.cardBorder,
width: BorderWidth.standard, width: BorderWidth.standard,
), ),
boxShadow: isElevated ? ConduitShadows.card(context) : null, boxShadow: isElevated ? ConduitShadows.card(context) : null,