refactor: enhance theme and error handling across the application
- Updated error handling in EnhancedErrorService to utilize context for color tokens, improving theme consistency. - Refactored various components to use context-aware shadow and color properties, enhancing visual coherence. - Replaced hardcoded color values with dynamic tokens in multiple widgets, ensuring better adaptability to theme changes. - Improved overall code maintainability by centralizing theme-related logic and reducing direct dependencies on static theme values.
This commit is contained in:
@@ -1,59 +1,64 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
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 'color_tokens.dart';
|
||||
|
||||
class AppTheme {
|
||||
// Enhanced neutral palette for better contrast (WCAG AA compliant)
|
||||
static const Color neutral900 = Color(0xFF000000); // Pure black
|
||||
static const Color neutral800 = Color(
|
||||
0xFF0D0D0D,
|
||||
); // Darker for better contrast
|
||||
static const Color neutral700 = Color(0xFF1A1A1A);
|
||||
static const Color neutral600 = Color(0xFF2D2D2D); // Improved contrast
|
||||
static const Color neutral500 = Color(0xFF404040); // Better middle gray
|
||||
static const Color neutral400 = Color(0xFF525252);
|
||||
static const Color neutral300 = Color(0xFF6B6B6B); // Improved contrast ratio
|
||||
static const Color neutral200 = Color(0xFF9E9E9E); // Better readability
|
||||
static const Color neutral100 = Color(0xFFD1D1D1); // Enhanced contrast
|
||||
static const Color neutral50 = Color(
|
||||
0xFFF8F8F8,
|
||||
); // Softer white for reduced eye strain
|
||||
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);
|
||||
|
||||
// Enhanced semantic colors for WCAG AA compliance
|
||||
static const Color error = Color(0xFFDC2626); // Improved red contrast
|
||||
static const Color errorDark = Color(0xFFB91C1C); // Darker red for dark theme
|
||||
static const Color success = Color(0xFF059669); // Better green contrast
|
||||
static const Color successDark = Color(0xFF047857); // Dark theme green
|
||||
static const Color warning = Color(0xFFD97706); // Improved orange contrast
|
||||
static const Color warningDark = Color(0xFFB45309); // Dark theme orange
|
||||
static const Color info = Color(0xFF0284C7); // Better blue contrast
|
||||
static const Color infoDark = Color(0xFF0369A1); // Dark theme blue
|
||||
// Semantic colors derived from the token specification
|
||||
static const Color error = Color(0xFFCE2C31);
|
||||
static const Color errorDark = Color(0xFFFF5F67);
|
||||
static const Color success = Color(0xFF0E9D58);
|
||||
static const Color successDark = Color(0xFF23C179);
|
||||
static const Color warning = Color(0xFFDB7900);
|
||||
static const Color warningDark = Color(0xFFFF9800);
|
||||
static const Color info = Color(0xFF0174D3);
|
||||
static const Color infoDark = Color(0xFF4CA8FF);
|
||||
|
||||
static ThemeData light(AppColorPalette palette) {
|
||||
final lightTone = palette.light;
|
||||
final tokens = AppColorTokens.light(palette: palette);
|
||||
final colorScheme = tokens.toColorScheme().copyWith(
|
||||
primary: lightTone.primary,
|
||||
onPrimary: _pickOnColor(lightTone.primary, tokens),
|
||||
secondary: lightTone.secondary,
|
||||
onSecondary: _pickOnColor(lightTone.secondary, tokens),
|
||||
tertiary: lightTone.accent,
|
||||
onTertiary: _pickOnColor(lightTone.accent, tokens),
|
||||
surfaceTint: lightTone.primary,
|
||||
);
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: lightTone.primary,
|
||||
secondary: lightTone.secondary,
|
||||
surface: neutral50,
|
||||
error: error,
|
||||
).copyWith(surfaceContainerHighest: const Color(0xFFF0F1F1)),
|
||||
colorScheme: colorScheme,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
appBarTheme: const AppBarTheme(
|
||||
scaffoldBackgroundColor: tokens.neutralTone10,
|
||||
appBarTheme: AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: Elevation.none,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: neutral800,
|
||||
foregroundColor: tokens.neutralOnSurface,
|
||||
),
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: neutral50,
|
||||
modalBackgroundColor: neutral50,
|
||||
backgroundColor: tokens.neutralTone00,
|
||||
modalBackgroundColor: tokens.neutralTone00,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.modal),
|
||||
@@ -66,6 +71,8 @@ class AppTheme {
|
||||
horizontal: Spacing.lg,
|
||||
vertical: Spacing.xs,
|
||||
),
|
||||
backgroundColor: lightTone.primary,
|
||||
foregroundColor: _pickOnColor(lightTone.primary, tokens),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
),
|
||||
@@ -75,15 +82,19 @@ class AppTheme {
|
||||
elevation: Elevation.none,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
|
||||
side: BorderSide(color: neutral200),
|
||||
side: BorderSide(color: tokens.neutralTone20),
|
||||
),
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: neutral900.withValues(alpha: 0.92),
|
||||
contentTextStyle: const TextStyle(
|
||||
color: neutral50,
|
||||
).copyWith(fontSize: AppTypography.bodyMedium),
|
||||
backgroundColor: Color.alphaBlend(
|
||||
tokens.overlayStrong,
|
||||
tokens.neutralOnSurface,
|
||||
),
|
||||
contentTextStyle: TextStyle(
|
||||
color: tokens.neutralTone00,
|
||||
fontSize: AppTypography.bodyMedium,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.snackbar),
|
||||
),
|
||||
@@ -91,7 +102,7 @@ class AppTheme {
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: neutral50,
|
||||
fillColor: tokens.neutralTone00,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
borderSide: BorderSide.none,
|
||||
@@ -106,7 +117,7 @@ class AppTheme {
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
borderSide: const BorderSide(color: error, width: 1),
|
||||
borderSide: BorderSide(color: tokens.statusError60, width: 1),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.md,
|
||||
@@ -115,7 +126,8 @@ class AppTheme {
|
||||
),
|
||||
textTheme: ThemeData.light().textTheme,
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
ConduitThemeExtension.lightPalette(palette),
|
||||
tokens,
|
||||
ConduitThemeExtension.lightPalette(palette: palette, tokens: tokens),
|
||||
AppPaletteThemeExtension(palette: palette),
|
||||
],
|
||||
);
|
||||
@@ -123,32 +135,33 @@ class AppTheme {
|
||||
|
||||
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,
|
||||
brightness: Brightness.dark,
|
||||
scaffoldBackgroundColor: const Color(0xFF0A0D0C),
|
||||
colorScheme: ColorScheme.dark(
|
||||
primary: darkTone.primary,
|
||||
secondary: darkTone.secondary,
|
||||
surface: const Color(0xFF0A0D0C),
|
||||
surfaceContainerHighest: neutral700,
|
||||
onSurface: neutral50,
|
||||
onSurfaceVariant: neutral300,
|
||||
outline: neutral600,
|
||||
error: error,
|
||||
),
|
||||
colorScheme: colorScheme,
|
||||
scaffoldBackgroundColor: tokens.neutralTone10,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
appBarTheme: const AppBarTheme(
|
||||
appBarTheme: AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: Elevation.none,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: neutral50,
|
||||
foregroundColor: tokens.neutralOnSurface,
|
||||
),
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: neutral900,
|
||||
modalBackgroundColor: neutral900,
|
||||
backgroundColor: tokens.neutralTone00,
|
||||
modalBackgroundColor: tokens.neutralTone00,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.modal),
|
||||
@@ -161,6 +174,8 @@ class AppTheme {
|
||||
horizontal: Spacing.lg,
|
||||
vertical: Spacing.xs,
|
||||
),
|
||||
backgroundColor: darkTone.primary,
|
||||
foregroundColor: _pickOnColor(darkTone.primary, tokens),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
),
|
||||
@@ -170,15 +185,19 @@ class AppTheme {
|
||||
elevation: Elevation.none,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
|
||||
side: BorderSide(color: neutral800),
|
||||
side: BorderSide(color: tokens.neutralTone40),
|
||||
),
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: neutral800.withValues(alpha: 0.92),
|
||||
contentTextStyle: const TextStyle(
|
||||
color: neutral50,
|
||||
).copyWith(fontSize: AppTypography.bodyMedium),
|
||||
backgroundColor: Color.alphaBlend(
|
||||
tokens.overlayStrong,
|
||||
tokens.neutralTone20,
|
||||
),
|
||||
contentTextStyle: TextStyle(
|
||||
color: tokens.neutralOnSurface,
|
||||
fontSize: AppTypography.bodyMedium,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.snackbar),
|
||||
),
|
||||
@@ -186,14 +205,14 @@ class AppTheme {
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: neutral700,
|
||||
fillColor: tokens.neutralTone20,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
borderSide: const BorderSide(color: neutral600, width: 1),
|
||||
borderSide: BorderSide(color: tokens.neutralTone40, width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
borderSide: const BorderSide(color: neutral600, width: 1),
|
||||
borderSide: BorderSide(color: tokens.neutralTone40, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
@@ -201,7 +220,7 @@ class AppTheme {
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
borderSide: const BorderSide(color: error, width: 1),
|
||||
borderSide: BorderSide(color: tokens.statusError60, width: 1),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.md,
|
||||
@@ -210,7 +229,8 @@ class AppTheme {
|
||||
),
|
||||
textTheme: ThemeData.dark().textTheme,
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
ConduitThemeExtension.darkPalette(palette),
|
||||
tokens,
|
||||
ConduitThemeExtension.darkPalette(palette: palette, tokens: tokens),
|
||||
AppPaletteThemeExtension(palette: palette),
|
||||
],
|
||||
);
|
||||
@@ -222,18 +242,33 @@ class AppTheme {
|
||||
) {
|
||||
final brightness = Theme.of(context).brightness;
|
||||
final tone = palette.toneFor(brightness);
|
||||
final tokens = brightness == Brightness.dark
|
||||
? AppColorTokens.dark(palette: palette)
|
||||
: AppColorTokens.light(palette: palette);
|
||||
return CupertinoThemeData(
|
||||
brightness: brightness,
|
||||
primaryColor: tone.primary,
|
||||
scaffoldBackgroundColor: brightness == Brightness.dark
|
||||
? neutral900
|
||||
: neutral50,
|
||||
barBackgroundColor: brightness == Brightness.dark
|
||||
? neutral900
|
||||
: neutral50,
|
||||
scaffoldBackgroundColor: tokens.neutralTone10,
|
||||
barBackgroundColor: tokens.neutralTone10,
|
||||
);
|
||||
}
|
||||
|
||||
static Color _pickOnColor(Color background, AppColorTokens tokens) {
|
||||
final contrastOnLight = _contrastRatio(background, tokens.neutralTone00);
|
||||
final contrastOnDark = _contrastRatio(background, tokens.neutralOnSurface);
|
||||
return contrastOnLight >= contrastOnDark
|
||||
? tokens.neutralTone00
|
||||
: tokens.neutralOnSurface;
|
||||
}
|
||||
|
||||
static double _contrastRatio(Color a, Color b) {
|
||||
final luminanceA = a.computeLuminance();
|
||||
final luminanceB = b.computeLuminance();
|
||||
final lighter = math.max(luminanceA, luminanceB);
|
||||
final darker = math.min(luminanceA, luminanceB);
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
static const PageTransitionsTheme _pageTransitionsTheme =
|
||||
PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
|
||||
461
lib/shared/theme/color_tokens.dart
Normal file
461
lib/shared/theme/color_tokens.dart
Normal file
@@ -0,0 +1,461 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'color_palettes.dart';
|
||||
|
||||
/// Immutable set of semantic color tokens exposed through [ThemeExtension].
|
||||
///
|
||||
/// The tokens are derived from the Conduit color specification and provide
|
||||
/// consistent mappings for light and dark modes. Widgets should prefer using
|
||||
/// these tokens instead of hard-coded color values to ensure theme parity and
|
||||
/// accessible contrast levels.
|
||||
@immutable
|
||||
class AppColorTokens extends ThemeExtension<AppColorTokens> {
|
||||
const AppColorTokens({
|
||||
required this.brightness,
|
||||
required this.neutralTone00,
|
||||
required this.neutralTone10,
|
||||
required this.neutralTone20,
|
||||
required this.neutralTone40,
|
||||
required this.neutralTone60,
|
||||
required this.neutralTone80,
|
||||
required this.neutralOnSurface,
|
||||
required this.brandTone40,
|
||||
required this.brandTone60,
|
||||
required this.brandOn60,
|
||||
required this.brandTone90,
|
||||
required this.brandOn90,
|
||||
required this.accentIndigo60,
|
||||
required this.accentOnIndigo60,
|
||||
required this.accentTeal60,
|
||||
required this.accentGold60,
|
||||
required this.statusSuccess60,
|
||||
required this.statusOnSuccess60,
|
||||
required this.statusWarning60,
|
||||
required this.statusOnWarning60,
|
||||
required this.statusError60,
|
||||
required this.statusOnError60,
|
||||
required this.statusInfo60,
|
||||
required this.statusOnInfo60,
|
||||
required this.overlayWeak,
|
||||
required this.overlayMedium,
|
||||
required this.overlayStrong,
|
||||
required this.codeBackground,
|
||||
required this.codeBorder,
|
||||
required this.codeText,
|
||||
required this.codeAccent,
|
||||
});
|
||||
|
||||
final Brightness brightness;
|
||||
|
||||
// Neutral tokens
|
||||
final Color neutralTone00;
|
||||
final Color neutralTone10;
|
||||
final Color neutralTone20;
|
||||
final Color neutralTone40;
|
||||
final Color neutralTone60;
|
||||
final Color neutralTone80;
|
||||
final Color neutralOnSurface;
|
||||
|
||||
// Brand tokens
|
||||
final Color brandTone40;
|
||||
final Color brandTone60;
|
||||
final Color brandOn60;
|
||||
final Color brandTone90;
|
||||
final Color brandOn90;
|
||||
|
||||
// Accent tokens
|
||||
final Color accentIndigo60;
|
||||
final Color accentOnIndigo60;
|
||||
final Color accentTeal60;
|
||||
final Color accentGold60;
|
||||
|
||||
// Status tokens
|
||||
final Color statusSuccess60;
|
||||
final Color statusOnSuccess60;
|
||||
final Color statusWarning60;
|
||||
final Color statusOnWarning60;
|
||||
final Color statusError60;
|
||||
final Color statusOnError60;
|
||||
final Color statusInfo60;
|
||||
final Color statusOnInfo60;
|
||||
|
||||
// Overlay tokens
|
||||
final Color overlayWeak;
|
||||
final Color overlayMedium;
|
||||
final Color overlayStrong;
|
||||
|
||||
// Markdown/code tokens
|
||||
final Color codeBackground;
|
||||
final Color codeBorder;
|
||||
final Color codeText;
|
||||
final Color codeAccent;
|
||||
|
||||
factory AppColorTokens.light({AppColorPalette? palette}) {
|
||||
return AppColorTokens._fromPalette(
|
||||
palette ?? AppColorPalettes.auroraViolet,
|
||||
Brightness.light,
|
||||
);
|
||||
}
|
||||
|
||||
factory AppColorTokens.dark({AppColorPalette? palette}) {
|
||||
return AppColorTokens._fromPalette(
|
||||
palette ?? AppColorPalettes.auroraViolet,
|
||||
Brightness.dark,
|
||||
);
|
||||
}
|
||||
|
||||
factory AppColorTokens._fromPalette(
|
||||
AppColorPalette palette,
|
||||
Brightness brightness,
|
||||
) {
|
||||
final AppPaletteTone tone = palette.toneFor(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 Color codeBackground = isLight
|
||||
? const Color(0xFF0F172A)
|
||||
: const Color(0xFF111828);
|
||||
final Color codeBorder = isLight
|
||||
? const Color(0xFF1E293B)
|
||||
: const Color(0xFF1F2937);
|
||||
final Color codeText = const Color(0xFFE2E8F0);
|
||||
final Color codeAccent = codeBorder;
|
||||
|
||||
final ColorScheme seedScheme = ColorScheme.fromSeed(
|
||||
seedColor: tone.primary,
|
||||
brightness: brightness,
|
||||
);
|
||||
|
||||
final Color brandTone60 = seedScheme.primary;
|
||||
final Color brandOn60 = _preferredOnColor(
|
||||
background: brandTone60,
|
||||
light: neutralTone00,
|
||||
dark: neutralOnSurface,
|
||||
);
|
||||
|
||||
final Color brandTone90 = seedScheme.primaryContainer;
|
||||
final Color brandOn90 = _preferredOnColor(
|
||||
background: brandTone90,
|
||||
light: neutralTone00,
|
||||
dark: neutralOnSurface,
|
||||
);
|
||||
|
||||
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 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 statusWarning60 = isLight
|
||||
? const Color(0xFFDB7900)
|
||||
: const Color(0xFFFF9800);
|
||||
final Color statusOnWarning60 = _preferredOnColor(
|
||||
background: statusWarning60,
|
||||
light: neutralTone00,
|
||||
dark: neutralOnSurface,
|
||||
);
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
return AppColorTokens(
|
||||
brightness: brightness,
|
||||
neutralTone00: neutralTone00,
|
||||
neutralTone10: neutralTone10,
|
||||
neutralTone20: neutralTone20,
|
||||
neutralTone40: neutralTone40,
|
||||
neutralTone60: neutralTone60,
|
||||
neutralTone80: neutralTone80,
|
||||
neutralOnSurface: neutralOnSurface,
|
||||
brandTone40: brandTone40,
|
||||
brandTone60: brandTone60,
|
||||
brandOn60: brandOn60,
|
||||
brandTone90: brandTone90,
|
||||
brandOn90: brandOn90,
|
||||
accentIndigo60: accentIndigo60,
|
||||
accentOnIndigo60: accentOnIndigo60,
|
||||
accentTeal60: accentTeal60,
|
||||
accentGold60: accentGold60,
|
||||
statusSuccess60: statusSuccess60,
|
||||
statusOnSuccess60: statusOnSuccess60,
|
||||
statusWarning60: statusWarning60,
|
||||
statusOnWarning60: statusOnWarning60,
|
||||
statusError60: statusError60,
|
||||
statusOnError60: statusOnError60,
|
||||
statusInfo60: statusInfo60,
|
||||
statusOnInfo60: statusOnInfo60,
|
||||
overlayWeak: overlayWeak,
|
||||
overlayMedium: overlayMedium,
|
||||
overlayStrong: overlayStrong,
|
||||
codeBackground: codeBackground,
|
||||
codeBorder: codeBorder,
|
||||
codeText: codeText,
|
||||
codeAccent: codeAccent,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AppColorTokens copyWith({
|
||||
Brightness? brightness,
|
||||
Color? neutralTone00,
|
||||
Color? neutralTone10,
|
||||
Color? neutralTone20,
|
||||
Color? neutralTone40,
|
||||
Color? neutralTone60,
|
||||
Color? neutralTone80,
|
||||
Color? neutralOnSurface,
|
||||
Color? brandTone40,
|
||||
Color? brandTone60,
|
||||
Color? brandOn60,
|
||||
Color? brandTone90,
|
||||
Color? brandOn90,
|
||||
Color? accentIndigo60,
|
||||
Color? accentOnIndigo60,
|
||||
Color? accentTeal60,
|
||||
Color? accentGold60,
|
||||
Color? statusSuccess60,
|
||||
Color? statusOnSuccess60,
|
||||
Color? statusWarning60,
|
||||
Color? statusOnWarning60,
|
||||
Color? statusError60,
|
||||
Color? statusOnError60,
|
||||
Color? statusInfo60,
|
||||
Color? statusOnInfo60,
|
||||
Color? overlayWeak,
|
||||
Color? overlayMedium,
|
||||
Color? overlayStrong,
|
||||
Color? codeBackground,
|
||||
Color? codeBorder,
|
||||
Color? codeText,
|
||||
Color? codeAccent,
|
||||
}) {
|
||||
return AppColorTokens(
|
||||
brightness: brightness ?? this.brightness,
|
||||
neutralTone00: neutralTone00 ?? this.neutralTone00,
|
||||
neutralTone10: neutralTone10 ?? this.neutralTone10,
|
||||
neutralTone20: neutralTone20 ?? this.neutralTone20,
|
||||
neutralTone40: neutralTone40 ?? this.neutralTone40,
|
||||
neutralTone60: neutralTone60 ?? this.neutralTone60,
|
||||
neutralTone80: neutralTone80 ?? this.neutralTone80,
|
||||
neutralOnSurface: neutralOnSurface ?? this.neutralOnSurface,
|
||||
brandTone40: brandTone40 ?? this.brandTone40,
|
||||
brandTone60: brandTone60 ?? this.brandTone60,
|
||||
brandOn60: brandOn60 ?? this.brandOn60,
|
||||
brandTone90: brandTone90 ?? this.brandTone90,
|
||||
brandOn90: brandOn90 ?? this.brandOn90,
|
||||
accentIndigo60: accentIndigo60 ?? this.accentIndigo60,
|
||||
accentOnIndigo60: accentOnIndigo60 ?? this.accentOnIndigo60,
|
||||
accentTeal60: accentTeal60 ?? this.accentTeal60,
|
||||
accentGold60: accentGold60 ?? this.accentGold60,
|
||||
statusSuccess60: statusSuccess60 ?? this.statusSuccess60,
|
||||
statusOnSuccess60: statusOnSuccess60 ?? this.statusOnSuccess60,
|
||||
statusWarning60: statusWarning60 ?? this.statusWarning60,
|
||||
statusOnWarning60: statusOnWarning60 ?? this.statusOnWarning60,
|
||||
statusError60: statusError60 ?? this.statusError60,
|
||||
statusOnError60: statusOnError60 ?? this.statusOnError60,
|
||||
statusInfo60: statusInfo60 ?? this.statusInfo60,
|
||||
statusOnInfo60: statusOnInfo60 ?? this.statusOnInfo60,
|
||||
overlayWeak: overlayWeak ?? this.overlayWeak,
|
||||
overlayMedium: overlayMedium ?? this.overlayMedium,
|
||||
overlayStrong: overlayStrong ?? this.overlayStrong,
|
||||
codeBackground: codeBackground ?? this.codeBackground,
|
||||
codeBorder: codeBorder ?? this.codeBorder,
|
||||
codeText: codeText ?? this.codeText,
|
||||
codeAccent: codeAccent ?? this.codeAccent,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AppColorTokens lerp(
|
||||
covariant ThemeExtension<AppColorTokens>? other,
|
||||
double t,
|
||||
) {
|
||||
if (other is! AppColorTokens) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return AppColorTokens(
|
||||
brightness: t < 0.5 ? brightness : other.brightness,
|
||||
neutralTone00: Color.lerp(neutralTone00, other.neutralTone00, t)!,
|
||||
neutralTone10: Color.lerp(neutralTone10, other.neutralTone10, t)!,
|
||||
neutralTone20: Color.lerp(neutralTone20, other.neutralTone20, t)!,
|
||||
neutralTone40: Color.lerp(neutralTone40, other.neutralTone40, t)!,
|
||||
neutralTone60: Color.lerp(neutralTone60, other.neutralTone60, t)!,
|
||||
neutralTone80: Color.lerp(neutralTone80, other.neutralTone80, t)!,
|
||||
neutralOnSurface: Color.lerp(
|
||||
neutralOnSurface,
|
||||
other.neutralOnSurface,
|
||||
t,
|
||||
)!,
|
||||
brandTone40: Color.lerp(brandTone40, other.brandTone40, t)!,
|
||||
brandTone60: Color.lerp(brandTone60, other.brandTone60, t)!,
|
||||
brandOn60: Color.lerp(brandOn60, other.brandOn60, t)!,
|
||||
brandTone90: Color.lerp(brandTone90, other.brandTone90, t)!,
|
||||
brandOn90: Color.lerp(brandOn90, other.brandOn90, t)!,
|
||||
accentIndigo60: Color.lerp(accentIndigo60, other.accentIndigo60, t)!,
|
||||
accentOnIndigo60: Color.lerp(
|
||||
accentOnIndigo60,
|
||||
other.accentOnIndigo60,
|
||||
t,
|
||||
)!,
|
||||
accentTeal60: Color.lerp(accentTeal60, other.accentTeal60, t)!,
|
||||
accentGold60: Color.lerp(accentGold60, other.accentGold60, t)!,
|
||||
statusSuccess60: Color.lerp(statusSuccess60, other.statusSuccess60, t)!,
|
||||
statusOnSuccess60: Color.lerp(
|
||||
statusOnSuccess60,
|
||||
other.statusOnSuccess60,
|
||||
t,
|
||||
)!,
|
||||
statusWarning60: Color.lerp(statusWarning60, other.statusWarning60, t)!,
|
||||
statusOnWarning60: Color.lerp(
|
||||
statusOnWarning60,
|
||||
other.statusOnWarning60,
|
||||
t,
|
||||
)!,
|
||||
statusError60: Color.lerp(statusError60, other.statusError60, t)!,
|
||||
statusOnError60: Color.lerp(statusOnError60, other.statusOnError60, t)!,
|
||||
statusInfo60: Color.lerp(statusInfo60, other.statusInfo60, t)!,
|
||||
statusOnInfo60: Color.lerp(statusOnInfo60, other.statusOnInfo60, t)!,
|
||||
overlayWeak: Color.lerp(overlayWeak, other.overlayWeak, t)!,
|
||||
overlayMedium: Color.lerp(overlayMedium, other.overlayMedium, t)!,
|
||||
overlayStrong: Color.lerp(overlayStrong, other.overlayStrong, t)!,
|
||||
codeBackground: Color.lerp(codeBackground, other.codeBackground, t)!,
|
||||
codeBorder: Color.lerp(codeBorder, other.codeBorder, t)!,
|
||||
codeText: Color.lerp(codeText, other.codeText, t)!,
|
||||
codeAccent: Color.lerp(codeAccent, other.codeAccent, t)!,
|
||||
);
|
||||
}
|
||||
|
||||
/// Generates a Material [ColorScheme] that aligns with the defined tokens.
|
||||
ColorScheme toColorScheme() {
|
||||
final base = ColorScheme.fromSeed(
|
||||
seedColor: brandTone60,
|
||||
brightness: brightness,
|
||||
);
|
||||
|
||||
return base.copyWith(
|
||||
primary: brandTone60,
|
||||
onPrimary: brandOn60,
|
||||
primaryContainer: brandTone90,
|
||||
onPrimaryContainer: brandOn90,
|
||||
secondary: accentIndigo60,
|
||||
onSecondary: accentOnIndigo60,
|
||||
tertiary: accentTeal60,
|
||||
onTertiary: neutralTone00,
|
||||
surface: neutralTone00,
|
||||
surfaceContainerLow: neutralTone10,
|
||||
surfaceContainerHighest: neutralTone20,
|
||||
onSurface: neutralOnSurface,
|
||||
onSurfaceVariant: neutralTone80,
|
||||
outline: neutralTone60,
|
||||
outlineVariant: neutralTone40,
|
||||
error: statusError60,
|
||||
onError: statusOnError60,
|
||||
surfaceTint: brandTone40,
|
||||
scrim: overlayStrong,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convenience helper to composite an overlay on top of the correct surface.
|
||||
Color overlayOnSurface(Color overlay, {Color? surface}) {
|
||||
final baseSurface = surface ?? neutralTone00;
|
||||
return Color.alphaBlend(overlay, baseSurface);
|
||||
}
|
||||
|
||||
static AppColorTokens fallback({Brightness brightness = Brightness.light}) {
|
||||
return brightness == Brightness.dark
|
||||
? AppColorTokens.dark()
|
||||
: 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,
|
||||
}) {
|
||||
final double lightContrast = _contrastRatio(background, light);
|
||||
final double darkContrast = _contrastRatio(background, dark);
|
||||
return lightContrast >= darkContrast ? light : dark;
|
||||
}
|
||||
|
||||
static double _contrastRatio(Color a, Color b) {
|
||||
final double luminanceA = a.computeLuminance();
|
||||
final double luminanceB = b.computeLuminance();
|
||||
final double lighter = math.max(luminanceA, luminanceB);
|
||||
final double darker = math.min(luminanceA, luminanceB);
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
// Using system fonts; no GoogleFonts dependency required
|
||||
import 'app_theme.dart';
|
||||
import 'color_palettes.dart';
|
||||
import 'color_tokens.dart';
|
||||
|
||||
/// Extended theme data for consistent styling across the app
|
||||
@immutable
|
||||
@@ -62,6 +62,12 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
|
||||
final Color shimmerHighlight;
|
||||
final Color loadingIndicator;
|
||||
|
||||
// Markdown/code colors
|
||||
final Color codeBackground;
|
||||
final Color codeBorder;
|
||||
final Color codeText;
|
||||
final Color codeAccent;
|
||||
|
||||
// Text colors
|
||||
final Color textPrimary;
|
||||
final Color textSecondary;
|
||||
@@ -141,6 +147,12 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
|
||||
required this.shimmerHighlight,
|
||||
required this.loadingIndicator,
|
||||
|
||||
// Markdown/code colors
|
||||
required this.codeBackground,
|
||||
required this.codeBorder,
|
||||
required this.codeText,
|
||||
required this.codeAccent,
|
||||
|
||||
// Text colors
|
||||
required this.textPrimary,
|
||||
required this.textSecondary,
|
||||
@@ -222,6 +234,12 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
|
||||
Color? shimmerHighlight,
|
||||
Color? loadingIndicator,
|
||||
|
||||
// Markdown/code colors
|
||||
Color? codeBackground,
|
||||
Color? codeBorder,
|
||||
Color? codeText,
|
||||
Color? codeAccent,
|
||||
|
||||
// Text colors
|
||||
Color? textPrimary,
|
||||
Color? textSecondary,
|
||||
@@ -305,6 +323,12 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
|
||||
shimmerHighlight: shimmerHighlight ?? this.shimmerHighlight,
|
||||
loadingIndicator: loadingIndicator ?? this.loadingIndicator,
|
||||
|
||||
// Markdown/code colors
|
||||
codeBackground: codeBackground ?? this.codeBackground,
|
||||
codeBorder: codeBorder ?? this.codeBorder,
|
||||
codeText: codeText ?? this.codeText,
|
||||
codeAccent: codeAccent ?? this.codeAccent,
|
||||
|
||||
// Text colors
|
||||
textPrimary: textPrimary ?? this.textPrimary,
|
||||
textSecondary: textSecondary ?? this.textSecondary,
|
||||
@@ -477,6 +501,10 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
|
||||
other.loadingIndicator,
|
||||
t,
|
||||
)!,
|
||||
codeBackground: Color.lerp(codeBackground, other.codeBackground, t)!,
|
||||
codeBorder: Color.lerp(codeBorder, other.codeBorder, t)!,
|
||||
codeText: Color.lerp(codeText, other.codeText, t)!,
|
||||
codeAccent: Color.lerp(codeAccent, other.codeAccent, t)!,
|
||||
|
||||
// Text colors
|
||||
textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!,
|
||||
@@ -505,116 +533,136 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
|
||||
}
|
||||
|
||||
/// Dark theme extension derived from the active color palette.
|
||||
static ConduitThemeExtension darkPalette(AppColorPalette palette) {
|
||||
static ConduitThemeExtension darkPalette({
|
||||
required AppColorPalette palette,
|
||||
required AppColorTokens tokens,
|
||||
}) {
|
||||
final darkTone = palette.dark;
|
||||
final onDarkPrimary = _onSurfaceColor(darkTone.primary);
|
||||
final onPrimary = _onSurfaceColor(darkTone.primary, tokens);
|
||||
Color blend(Color overlay, {Color? surface}) {
|
||||
return Color.alphaBlend(overlay, surface ?? tokens.neutralTone10);
|
||||
}
|
||||
|
||||
Color toneBackground(Color tone, {double opacity = 0.24}) {
|
||||
return Color.alphaBlend(
|
||||
tone.withValues(alpha: opacity),
|
||||
tokens.neutralTone10,
|
||||
);
|
||||
}
|
||||
|
||||
return ConduitThemeExtension(
|
||||
chatBubbleUser: darkTone.primary,
|
||||
chatBubbleAssistant: const Color(0xFF0E1010),
|
||||
chatBubbleUserText: onDarkPrimary,
|
||||
chatBubbleAssistantText: AppTheme.neutral50,
|
||||
chatBubbleAssistant: tokens.neutralTone20,
|
||||
chatBubbleUserText: onPrimary,
|
||||
chatBubbleAssistantText: tokens.neutralOnSurface,
|
||||
chatBubbleUserBorder: darkTone.secondary,
|
||||
chatBubbleAssistantBorder: const Color(0xFF1A1D1C),
|
||||
inputBackground: const Color(0xFF141615),
|
||||
inputBorder: AppTheme.neutral600,
|
||||
chatBubbleAssistantBorder: tokens.neutralTone40,
|
||||
inputBackground: tokens.neutralTone20,
|
||||
inputBorder: tokens.neutralTone40,
|
||||
inputBorderFocused: darkTone.primary,
|
||||
inputText: AppTheme.neutral50,
|
||||
inputPlaceholder: AppTheme.neutral300,
|
||||
inputError: AppTheme.error,
|
||||
cardBackground: const Color(0xFF0C0F0E),
|
||||
cardBorder: const Color(0xFF151918),
|
||||
cardShadow: AppTheme.neutral900,
|
||||
surfaceBackground: const Color(0xFF0A0D0C),
|
||||
surfaceContainer: const Color(0xFF0C0F0E),
|
||||
surfaceContainerHighest: const Color(0xFF121514),
|
||||
inputText: tokens.neutralOnSurface,
|
||||
inputPlaceholder: tokens.neutralTone80,
|
||||
inputError: tokens.statusError60,
|
||||
cardBackground: tokens.neutralTone00,
|
||||
cardBorder: tokens.neutralTone40,
|
||||
cardShadow: blend(tokens.overlayWeak, surface: tokens.neutralTone00),
|
||||
surfaceBackground: tokens.neutralTone10,
|
||||
surfaceContainer: tokens.neutralTone00,
|
||||
surfaceContainerHighest: tokens.neutralTone20,
|
||||
buttonPrimary: darkTone.primary,
|
||||
buttonPrimaryText: onDarkPrimary,
|
||||
buttonSecondary: const Color(0xFF151918),
|
||||
buttonSecondaryText: AppTheme.neutral50,
|
||||
buttonDisabled: AppTheme.neutral600,
|
||||
buttonDisabledText: AppTheme.neutral400,
|
||||
success: const Color(0xFF34D399),
|
||||
successBackground: const Color(0xFF14532D),
|
||||
error: const Color(0xFFFCA5A5),
|
||||
errorBackground: const Color(0xFF7F1D1D),
|
||||
warning: const Color(0xFFFBBF24),
|
||||
warningBackground: const Color(0xFF451A03),
|
||||
info: const Color(0xFF93C5FD),
|
||||
infoBackground: const Color(0xFF0C4A6E),
|
||||
dividerColor: AppTheme.neutral600,
|
||||
navigationBackground: const Color(0xFF0A0D0C),
|
||||
buttonPrimaryText: onPrimary,
|
||||
buttonSecondary: tokens.neutralTone20,
|
||||
buttonSecondaryText: tokens.neutralOnSurface,
|
||||
buttonDisabled: tokens.neutralTone40,
|
||||
buttonDisabledText: tokens.neutralTone80,
|
||||
success: tokens.statusSuccess60,
|
||||
successBackground: toneBackground(tokens.statusSuccess60),
|
||||
error: tokens.statusError60,
|
||||
errorBackground: toneBackground(tokens.statusError60),
|
||||
warning: tokens.statusWarning60,
|
||||
warningBackground: toneBackground(tokens.statusWarning60),
|
||||
info: tokens.statusInfo60,
|
||||
infoBackground: toneBackground(tokens.statusInfo60),
|
||||
dividerColor: tokens.neutralTone40,
|
||||
navigationBackground: tokens.neutralTone10,
|
||||
navigationSelected: darkTone.primary,
|
||||
navigationUnselected: AppTheme.neutral300,
|
||||
navigationSelectedBackground: _surfaceTint(
|
||||
darkTone.primary,
|
||||
const Color(0xFF0A0D0C),
|
||||
0.24,
|
||||
navigationUnselected: tokens.neutralTone80,
|
||||
navigationSelectedBackground: blend(
|
||||
tokens.overlayMedium,
|
||||
surface: tokens.neutralTone10,
|
||||
),
|
||||
shimmerBase: blend(tokens.overlayWeak, surface: tokens.neutralTone10),
|
||||
shimmerHighlight: blend(
|
||||
tokens.overlayMedium,
|
||||
surface: tokens.neutralTone20,
|
||||
),
|
||||
shimmerBase: const Color(0xFF121514),
|
||||
shimmerHighlight: const Color(0xFF1A1D1C),
|
||||
loadingIndicator: darkTone.primary,
|
||||
textPrimary: AppTheme.neutral50,
|
||||
textSecondary: const Color(0xFFBAC2C0),
|
||||
textTertiary: AppTheme.neutral400,
|
||||
textInverse: AppTheme.neutral900,
|
||||
textDisabled: AppTheme.neutral600,
|
||||
iconPrimary: AppTheme.neutral50,
|
||||
iconSecondary: const Color(0xFFA0A8A5),
|
||||
iconDisabled: AppTheme.neutral600,
|
||||
iconInverse: AppTheme.neutral900,
|
||||
codeBackground: tokens.codeBackground,
|
||||
codeBorder: tokens.codeBorder,
|
||||
codeText: tokens.codeText,
|
||||
codeAccent: tokens.codeAccent,
|
||||
textPrimary: tokens.neutralOnSurface,
|
||||
textSecondary: tokens.neutralTone80,
|
||||
textTertiary: tokens.neutralTone60,
|
||||
textInverse: tokens.neutralTone00,
|
||||
textDisabled: tokens.neutralTone40,
|
||||
iconPrimary: tokens.neutralOnSurface,
|
||||
iconSecondary: tokens.neutralTone80,
|
||||
iconDisabled: tokens.neutralTone40,
|
||||
iconInverse: tokens.neutralTone00,
|
||||
headingLarge: TextStyle(
|
||||
fontSize: AppTypography.displaySmall,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppTheme.neutral50,
|
||||
color: tokens.neutralOnSurface,
|
||||
height: 1.2,
|
||||
),
|
||||
headingMedium: TextStyle(
|
||||
fontSize: AppTypography.headlineLarge,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.neutral50,
|
||||
color: tokens.neutralOnSurface,
|
||||
height: 1.3,
|
||||
),
|
||||
headingSmall: TextStyle(
|
||||
fontSize: AppTypography.headlineSmall,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.neutral50,
|
||||
color: tokens.neutralOnSurface,
|
||||
height: 1.4,
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
fontSize: AppTypography.bodyLarge,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppTheme.neutral50,
|
||||
color: tokens.neutralOnSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontSize: AppTypography.bodyMedium,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppTheme.neutral50,
|
||||
color: tokens.neutralOnSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
bodySmall: TextStyle(
|
||||
fontSize: AppTypography.bodySmall,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xFFD1D5DB),
|
||||
color: tokens.neutralTone80,
|
||||
height: 1.4,
|
||||
),
|
||||
caption: TextStyle(
|
||||
fontSize: AppTypography.labelMedium,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.neutral300,
|
||||
color: tokens.neutralTone80,
|
||||
height: 1.3,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
label: TextStyle(
|
||||
fontSize: AppTypography.labelLarge,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xFFD1D5DB),
|
||||
color: tokens.neutralOnSurface,
|
||||
height: 1.3,
|
||||
),
|
||||
code: TextStyle(
|
||||
fontSize: AppTypography.bodySmall,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xFFD1D5DB),
|
||||
color: tokens.neutralOnSurface,
|
||||
height: 1.4,
|
||||
fontFamily: AppTypography.monospaceFontFamily,
|
||||
),
|
||||
@@ -622,133 +670,143 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
|
||||
}
|
||||
|
||||
/// Light theme extension derived from the active color palette.
|
||||
static ConduitThemeExtension lightPalette(AppColorPalette palette) {
|
||||
static ConduitThemeExtension lightPalette({
|
||||
required AppColorPalette palette,
|
||||
required AppColorTokens tokens,
|
||||
}) {
|
||||
final lightTone = palette.light;
|
||||
final darkTone = palette.dark;
|
||||
final onLightPrimary = _onSurfaceColor(lightTone.primary);
|
||||
final onPrimary = _onSurfaceColor(lightTone.primary, tokens);
|
||||
Color blend(Color overlay, {Color? surface}) {
|
||||
return Color.alphaBlend(overlay, surface ?? tokens.neutralTone00);
|
||||
}
|
||||
|
||||
Color toneBackground(Color tone, {double opacity = 0.12}) {
|
||||
return Color.alphaBlend(
|
||||
tone.withValues(alpha: opacity),
|
||||
tokens.neutralTone00,
|
||||
);
|
||||
}
|
||||
|
||||
return ConduitThemeExtension(
|
||||
chatBubbleUser: lightTone.primary,
|
||||
chatBubbleAssistant: const Color(0xFFF7F7F7),
|
||||
chatBubbleUserText: onLightPrimary,
|
||||
chatBubbleAssistantText: const Color(0xFF1C1C1C),
|
||||
chatBubbleAssistant: tokens.neutralTone00,
|
||||
chatBubbleUserText: onPrimary,
|
||||
chatBubbleAssistantText: tokens.neutralOnSurface,
|
||||
chatBubbleUserBorder: darkTone.primary,
|
||||
chatBubbleAssistantBorder: const Color(0xFFE7E7E7),
|
||||
inputBackground: AppTheme.neutral50,
|
||||
inputBorder: AppTheme.neutral200,
|
||||
chatBubbleAssistantBorder: tokens.neutralTone20,
|
||||
inputBackground: tokens.neutralTone00,
|
||||
inputBorder: tokens.neutralTone20,
|
||||
inputBorderFocused: lightTone.primary,
|
||||
inputText: AppTheme.neutral900,
|
||||
inputPlaceholder: AppTheme.neutral500,
|
||||
inputError: AppTheme.error,
|
||||
cardBackground: AppTheme.neutral50,
|
||||
cardBorder: const Color(0xFFE7E7E7),
|
||||
cardShadow: const Color(0xFFF3F4F6),
|
||||
surfaceBackground: AppTheme.neutral50,
|
||||
surfaceContainer: const Color(0xFFF7F7F7),
|
||||
surfaceContainerHighest: const Color(0xFFF0F1F1),
|
||||
inputText: tokens.neutralOnSurface,
|
||||
inputPlaceholder: tokens.neutralTone60,
|
||||
inputError: tokens.statusError60,
|
||||
cardBackground: tokens.neutralTone00,
|
||||
cardBorder: tokens.neutralTone20,
|
||||
cardShadow: blend(tokens.overlayWeak),
|
||||
surfaceBackground: tokens.neutralTone10,
|
||||
surfaceContainer: tokens.neutralTone00,
|
||||
surfaceContainerHighest: tokens.neutralTone20,
|
||||
buttonPrimary: lightTone.primary,
|
||||
buttonPrimaryText: onLightPrimary,
|
||||
buttonSecondary: const Color(0xFFF0F1F1),
|
||||
buttonSecondaryText: const Color(0xFF1C1C1C),
|
||||
buttonDisabled: AppTheme.neutral300,
|
||||
buttonDisabledText: AppTheme.neutral500,
|
||||
success: const Color(0xFF166534),
|
||||
successBackground: const Color(0xFFECFDF3),
|
||||
error: const Color(0xFFB91C1C),
|
||||
errorBackground: const Color(0xFFFEE2E2),
|
||||
warning: const Color(0xFF92400E),
|
||||
warningBackground: const Color(0xFFFEF3C7),
|
||||
info: const Color(0xFF1D4ED8),
|
||||
infoBackground: const Color(0xFFDBEAFE),
|
||||
dividerColor: AppTheme.neutral100,
|
||||
navigationBackground: AppTheme.neutral50,
|
||||
buttonPrimaryText: onPrimary,
|
||||
buttonSecondary: tokens.neutralTone20,
|
||||
buttonSecondaryText: tokens.neutralOnSurface,
|
||||
buttonDisabled: tokens.neutralTone40,
|
||||
buttonDisabledText: tokens.neutralTone60,
|
||||
success: tokens.statusSuccess60,
|
||||
successBackground: toneBackground(tokens.statusSuccess60),
|
||||
error: tokens.statusError60,
|
||||
errorBackground: toneBackground(tokens.statusError60),
|
||||
warning: tokens.statusWarning60,
|
||||
warningBackground: toneBackground(tokens.statusWarning60),
|
||||
info: tokens.statusInfo60,
|
||||
infoBackground: toneBackground(tokens.statusInfo60),
|
||||
dividerColor: tokens.neutralTone20,
|
||||
navigationBackground: tokens.neutralTone00,
|
||||
navigationSelected: lightTone.primary,
|
||||
navigationUnselected: AppTheme.neutral600,
|
||||
navigationSelectedBackground: _surfaceTint(
|
||||
lightTone.primary,
|
||||
AppTheme.neutral50,
|
||||
0.16,
|
||||
),
|
||||
shimmerBase: const Color(0xFFF3F4F6),
|
||||
shimmerHighlight: AppTheme.neutral50,
|
||||
navigationUnselected: tokens.neutralTone60,
|
||||
navigationSelectedBackground: blend(tokens.overlayMedium),
|
||||
shimmerBase: blend(tokens.overlayWeak, surface: tokens.neutralTone10),
|
||||
shimmerHighlight: tokens.neutralTone00,
|
||||
loadingIndicator: lightTone.primary,
|
||||
textPrimary: const Color(0xFF1C1C1C),
|
||||
textSecondary: const Color(0xFF3A3F3E),
|
||||
textTertiary: AppTheme.neutral500,
|
||||
textInverse: AppTheme.neutral50,
|
||||
textDisabled: AppTheme.neutral400,
|
||||
iconPrimary: const Color(0xFF1C1C1C),
|
||||
iconSecondary: const Color(0xFF666C6A),
|
||||
iconDisabled: AppTheme.neutral400,
|
||||
iconInverse: AppTheme.neutral50,
|
||||
codeBackground: tokens.codeBackground,
|
||||
codeBorder: tokens.codeBorder,
|
||||
codeText: tokens.codeText,
|
||||
codeAccent: tokens.codeAccent,
|
||||
textPrimary: tokens.neutralOnSurface,
|
||||
textSecondary: tokens.neutralTone80,
|
||||
textTertiary: tokens.neutralTone60,
|
||||
textInverse: tokens.neutralTone00,
|
||||
textDisabled: tokens.neutralTone60,
|
||||
iconPrimary: tokens.neutralOnSurface,
|
||||
iconSecondary: tokens.neutralTone80,
|
||||
iconDisabled: tokens.neutralTone60,
|
||||
iconInverse: tokens.neutralTone00,
|
||||
headingLarge: TextStyle(
|
||||
fontSize: AppTypography.displaySmall,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFF111827),
|
||||
color: tokens.neutralOnSurface,
|
||||
height: 1.2,
|
||||
),
|
||||
headingMedium: TextStyle(
|
||||
fontSize: AppTypography.headlineLarge,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF111827),
|
||||
color: tokens.neutralOnSurface,
|
||||
height: 1.3,
|
||||
),
|
||||
headingSmall: TextStyle(
|
||||
fontSize: AppTypography.headlineSmall,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF111827),
|
||||
color: tokens.neutralOnSurface,
|
||||
height: 1.4,
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
fontSize: AppTypography.bodyLarge,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xFF111827),
|
||||
color: tokens.neutralOnSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontSize: AppTypography.bodyMedium,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xFF111827),
|
||||
color: tokens.neutralOnSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
bodySmall: TextStyle(
|
||||
fontSize: AppTypography.bodySmall,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppTheme.neutral500,
|
||||
color: tokens.neutralTone60,
|
||||
height: 1.4,
|
||||
),
|
||||
caption: TextStyle(
|
||||
fontSize: AppTypography.labelMedium,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.neutral400,
|
||||
color: tokens.neutralTone60,
|
||||
height: 1.3,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
label: TextStyle(
|
||||
fontSize: AppTypography.labelLarge,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xFF444948),
|
||||
color: tokens.neutralTone80,
|
||||
height: 1.3,
|
||||
),
|
||||
code: TextStyle(
|
||||
fontSize: AppTypography.bodySmall,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xFF1C1C1C),
|
||||
color: tokens.neutralOnSurface,
|
||||
height: 1.4,
|
||||
fontFamily: AppTypography.monospaceFontFamily,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Color _surfaceTint(Color tone, Color surface, double opacity) {
|
||||
return Color.alphaBlend(tone.withValues(alpha: opacity), surface);
|
||||
}
|
||||
|
||||
static Color _onSurfaceColor(Color background) {
|
||||
final contrastOnLight = _contrastRatio(background, AppTheme.neutral50);
|
||||
final contrastOnDark = _contrastRatio(background, AppTheme.neutral900);
|
||||
static Color _onSurfaceColor(Color background, AppColorTokens tokens) {
|
||||
final contrastOnLight = _contrastRatio(background, tokens.neutralTone00);
|
||||
final contrastOnDark = _contrastRatio(background, tokens.neutralOnSurface);
|
||||
return contrastOnLight >= contrastOnDark
|
||||
? AppTheme.neutral50
|
||||
: AppTheme.neutral900;
|
||||
? tokens.neutralTone00
|
||||
: tokens.neutralOnSurface;
|
||||
}
|
||||
|
||||
static double _contrastRatio(Color a, Color b) {
|
||||
@@ -769,9 +827,26 @@ extension ConduitThemeContext on BuildContext {
|
||||
final palette =
|
||||
theme.extension<AppPaletteThemeExtension>()?.palette ??
|
||||
AppColorPalettes.auroraViolet;
|
||||
final tokens = theme.brightness == Brightness.dark
|
||||
? AppColorTokens.dark(palette: palette)
|
||||
: AppColorTokens.light(palette: palette);
|
||||
return theme.brightness == Brightness.dark
|
||||
? ConduitThemeExtension.darkPalette(palette)
|
||||
: ConduitThemeExtension.lightPalette(palette);
|
||||
? ConduitThemeExtension.darkPalette(palette: palette, tokens: tokens)
|
||||
: ConduitThemeExtension.lightPalette(palette: palette, tokens: tokens);
|
||||
}
|
||||
}
|
||||
|
||||
extension ConduitColorTokensContext on BuildContext {
|
||||
AppColorTokens get colorTokens {
|
||||
final theme = Theme.of(this);
|
||||
final tokens = theme.extension<AppColorTokens>();
|
||||
if (tokens != null) return tokens;
|
||||
final palette =
|
||||
theme.extension<AppPaletteThemeExtension>()?.palette ??
|
||||
AppColorPalettes.auroraViolet;
|
||||
return theme.brightness == Brightness.dark
|
||||
? AppColorTokens.dark(palette: palette)
|
||||
: AppColorTokens.light(palette: palette);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -923,182 +998,153 @@ class Elevation {
|
||||
|
||||
/// Helper class for consistent shadows - Enhanced for production with better hierarchy
|
||||
class ConduitShadows {
|
||||
static List<BoxShadow> get low => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.08),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> low(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.08,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get medium => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.12),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> medium(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.12,
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get high => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.16),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> high(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.16,
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 8),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get glow => [
|
||||
BoxShadow(
|
||||
color: AppColorPalettes.auroraViolet.light.primary.withValues(
|
||||
alpha: 0.25,
|
||||
static List<BoxShadow> glow(BuildContext context) =>
|
||||
glowWithTokens(context.colorTokens);
|
||||
|
||||
static List<BoxShadow> glowWithTokens(AppColorTokens tokens) {
|
||||
final double alpha = tokens.brightness == Brightness.light ? 0.25 : 0.35;
|
||||
return [
|
||||
BoxShadow(
|
||||
color: tokens.brandTone60.withValues(alpha: alpha),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 0),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 0),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
// Enhanced shadows for specific components with better hierarchy
|
||||
static List<BoxShadow> get card => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.06),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 3),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> card(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.06,
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 3),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get button => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.1),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> button(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.1,
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get modal => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.2),
|
||||
blurRadius: 32,
|
||||
offset: const Offset(0, 12),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> modal(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.2,
|
||||
blurRadius: 32,
|
||||
offset: const Offset(0, 12),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get navigation => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.08),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, -2),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> navigation(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.08,
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, -2),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get messageBubble => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 1),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> messageBubble(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.04,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 1),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get input => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> input(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.05,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
);
|
||||
|
||||
// Dark theme specific shadows with better contrast
|
||||
static List<BoxShadow> get darkCard => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.3),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> pressed(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.15,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get darkModal => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.4),
|
||||
blurRadius: 40,
|
||||
offset: const Offset(0, 16),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> hover(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.12,
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
);
|
||||
|
||||
// Interactive shadows with better feedback
|
||||
static List<BoxShadow> get pressed => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.15),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> micro(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.04,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get hover => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.12),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> small(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.06,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
);
|
||||
|
||||
// Enhanced shadows for better visual hierarchy
|
||||
static List<BoxShadow> get micro => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.04),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> standard(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.08,
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 3),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get small => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.06),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> large(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.12,
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get standard => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.08),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 3),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> extraLarge(BuildContext context) => _shadow(
|
||||
context.colorTokens,
|
||||
opacity: 0.16,
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 8),
|
||||
);
|
||||
|
||||
static List<BoxShadow> get large => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.12),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static List<BoxShadow> _shadow(
|
||||
AppColorTokens tokens, {
|
||||
required double opacity,
|
||||
required double blurRadius,
|
||||
required Offset offset,
|
||||
}) {
|
||||
return [
|
||||
BoxShadow(
|
||||
color: _overlayColor(tokens, opacity),
|
||||
blurRadius: blurRadius,
|
||||
offset: offset,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
static List<BoxShadow> get extraLarge => [
|
||||
BoxShadow(
|
||||
color: AppTheme.neutral900.withValues(alpha: 0.16),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
];
|
||||
static Color _overlayColor(AppColorTokens tokens, double alpha) {
|
||||
final Color base = tokens.overlayStrong.withValues(alpha: 1.0);
|
||||
return base.withValues(alpha: alpha.clamp(0.0, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
/// Typography scale following Conduit design tokens - Enhanced for production
|
||||
|
||||
Reference in New Issue
Block a user