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 '../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<void> setPalette(String paletteId) async {
final palette = AppColorPalettes.byId(paletteId);
final palette = TweakcnThemes.byId(paletteId);
state = palette;
await _storage.setThemePaletteId(palette.id);
}

View File

@@ -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,

View File

@@ -1217,12 +1217,15 @@ class _ChatPageState extends ConsumerState<ChatPage> {
.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,

View File

@@ -152,10 +152,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
@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<ChatsDrawer> {
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<ChatsDrawer> {
}
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<ChatsDrawer> {
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<ChatsDrawer> {
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<ChatsDrawer> {
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<ChatsDrawer> {
}
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<ChatsDrawer> {
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<ChatsDrawer> {
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<ChatsDrawer> {
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<ChatsDrawer> {
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<ChatsDrawer> {
Platform.isIOS
? CupertinoIcons.settings
: Icons.settings_rounded,
color: theme.iconSecondary,
color: sidebarTheme.foreground.withValues(alpha: 0.8),
size: IconSize.medium,
),
),

View File

@@ -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<void>(
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 ??
<Color>[
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),
),
),
],
),

View File

@@ -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

View File

@@ -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<AppPaletteThemeExtension>();
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);
}
}

View File

@@ -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: <ThemeExtension<dynamic>>[
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: <ThemeExtension<dynamic>>[
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: <ThemeExtension<dynamic>>[
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);

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 'color_palettes.dart';
import 'tweakcn_themes.dart';
/// Immutable set of semantic color tokens exposed through [ThemeExtension].
///
@@ -92,136 +92,118 @@ class AppColorTokens extends ThemeExtension<AppColorTokens> {
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<AppColorTokens> {
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> {
: 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) {

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,
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,
),
),

View File

@@ -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,