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

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