import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'app_palette.dart'; // Default theme token set: simple class SimpleSpacing { SimpleSpacing._(); static const double page = 0.0; static const double compact = 6.0; static const double section = 8.0; static const double xxs = 4.0; static const double xs = compact; static const double sm = section; static const double md = section; static const double lg = section; static const double xl = 12.0; } class SimpleRadius { SimpleRadius._(); static const double card = 16.0; static const double button = 12.0; static const double input = 14.0; static const double chip = 12.0; static const double badge = 999.0; static const double dialog = 18.0; static const double sidebar = 20.0; static const double icon = 12.0; } class SimpleTypography { SimpleTypography._(); static const double displaySize = 28.0; static const FontWeight displayWeight = FontWeight.w600; static const double displayHeight = 32 / 28; static const double titleSize = 20.0; static const FontWeight titleWeight = FontWeight.w600; static const double titleHeight = 24 / 20; static const double sectionSize = 13.0; static const FontWeight sectionWeight = FontWeight.w600; static const double sectionHeight = 14 / 13; static const double bodySize = 13.0; static const FontWeight bodyWeight = FontWeight.w400; static const double bodyHeight = 15 / 13; static const double compactBodySize = 13.0; static const FontWeight compactBodyWeight = FontWeight.w400; static const double compactBodyHeight = 15 / 13; static const double emphasizedBodySize = 13.0; static const FontWeight emphasizedBodyWeight = FontWeight.w600; static const double emphasizedBodyHeight = 14 / 13; static const double captionSize = 12.0; static const FontWeight captionWeight = FontWeight.w400; static const double captionHeight = 16 / 12; } class SimpleSizes { SimpleSizes._(); static const double sidebarItemHeight = 34.0; static const double sidebarIconSize = 18.0; static const double sidebarTextSize = 13.0; static const double sidebarExpandedWidthZh = 152.0; static const double sidebarExpandedWidthEn = 188.0; static const double sidebarExpandedWidth = sidebarExpandedWidthZh; static const double sidebarCollapsedWidth = 56.0; static const double textareaHeight = 36.0; static const double toolbarHeight = 40.0; static const double inputHeight = 40.0; static const double buttonHeightDesktop = 30.0; static const double buttonHeightMobile = 36.0; } class AppSpacing { AppSpacing._(); static const double page = SimpleSpacing.page; static const double compact = SimpleSpacing.compact; static const double section = SimpleSpacing.section; static const double xxs = SimpleSpacing.xxs; static const double xs = SimpleSpacing.xs; static const double sm = SimpleSpacing.sm; static const double md = SimpleSpacing.md; static const double lg = SimpleSpacing.lg; static const double xl = SimpleSpacing.xl; } class AppRadius { AppRadius._(); static const double card = SimpleRadius.card; static const double button = SimpleRadius.button; static const double input = SimpleRadius.input; static const double chip = SimpleRadius.chip; static const double badge = SimpleRadius.badge; static const double dialog = SimpleRadius.dialog; static const double sidebar = SimpleRadius.sidebar; static const double icon = SimpleRadius.icon; } class AppTypography { AppTypography._(); static const double displaySize = SimpleTypography.displaySize; static const FontWeight displayWeight = SimpleTypography.displayWeight; static const double displayHeight = SimpleTypography.displayHeight; static const double titleSize = SimpleTypography.titleSize; static const FontWeight titleWeight = SimpleTypography.titleWeight; static const double titleHeight = SimpleTypography.titleHeight; static const double sectionSize = SimpleTypography.sectionSize; static const FontWeight sectionWeight = SimpleTypography.sectionWeight; static const double sectionHeight = SimpleTypography.sectionHeight; static const double bodySize = SimpleTypography.bodySize; static const FontWeight bodyWeight = SimpleTypography.bodyWeight; static const double bodyHeight = SimpleTypography.bodyHeight; static const double compactBodySize = SimpleTypography.compactBodySize; static const FontWeight compactBodyWeight = SimpleTypography.compactBodyWeight; static const double compactBodyHeight = SimpleTypography.compactBodyHeight; static const double emphasizedBodySize = SimpleTypography.emphasizedBodySize; static const FontWeight emphasizedBodyWeight = SimpleTypography.emphasizedBodyWeight; static const double emphasizedBodyHeight = SimpleTypography.emphasizedBodyHeight; static const double captionSize = SimpleTypography.captionSize; static const FontWeight captionWeight = SimpleTypography.captionWeight; static const double captionHeight = SimpleTypography.captionHeight; } class AppSizes { AppSizes._(); static const double sidebarItemHeight = SimpleSizes.sidebarItemHeight; static const double sidebarIconSize = SimpleSizes.sidebarIconSize; static const double sidebarTextSize = SimpleSizes.sidebarTextSize; static const double sidebarExpandedWidthZh = SimpleSizes.sidebarExpandedWidthZh; static const double sidebarExpandedWidthEn = SimpleSizes.sidebarExpandedWidthEn; static const double sidebarExpandedWidth = SimpleSizes.sidebarExpandedWidth; static const double sidebarCollapsedWidth = SimpleSizes.sidebarCollapsedWidth; static const double textareaHeight = SimpleSizes.textareaHeight; static const double toolbarHeight = SimpleSizes.toolbarHeight; static const double inputHeight = SimpleSizes.inputHeight; static const double buttonHeightDesktop = SimpleSizes.buttonHeightDesktop; static const double buttonHeightMobile = SimpleSizes.buttonHeightMobile; } class AppTheme { static ThemeData light({TargetPlatform? platform}) => _theme( brightness: Brightness.light, palette: AppPalette.light, platform: platform, ); static ThemeData dark({TargetPlatform? platform}) => _theme( brightness: Brightness.dark, palette: AppPalette.dark, platform: platform, ); static ThemeData _theme({ required Brightness brightness, required AppPalette palette, TargetPlatform? platform, }) { final resolvedPlatform = platform ?? defaultTargetPlatform; final isDesktop = resolvedPlatform == TargetPlatform.macOS || resolvedPlatform == TargetPlatform.windows || resolvedPlatform == TargetPlatform.linux; final isMobile = resolvedPlatform == TargetPlatform.iOS || resolvedPlatform == TargetPlatform.android; final colorScheme = ColorScheme.fromSeed( seedColor: palette.accent, brightness: brightness, surface: palette.surfacePrimary, ).copyWith( primary: palette.accent, onPrimary: Colors.white, secondary: palette.accent, onSecondary: Colors.white, tertiary: palette.success, onTertiary: Colors.white, error: palette.danger, onError: Colors.white, surface: palette.surfacePrimary, onSurface: palette.textPrimary, surfaceContainerHighest: palette.surfaceSecondary, outline: palette.stroke, outlineVariant: palette.strokeSoft, inverseSurface: palette.textPrimary, onInverseSurface: palette.surfacePrimary, shadow: palette.shadow, scrim: Colors.black.withValues( alpha: brightness == Brightness.dark ? 0.62 : 0.12, ), ); final base = ThemeData( useMaterial3: true, brightness: brightness, typography: Typography.material2021(platform: resolvedPlatform), colorScheme: colorScheme, scaffoldBackgroundColor: palette.canvas, extensions: [palette], ); final tunedTextTheme = _textTheme( base.textTheme, palette: palette, isMobile: isMobile, ); return base.copyWith( platform: resolvedPlatform, splashFactory: NoSplash.splashFactory, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: isDesktop ? const VisualDensity(horizontal: -1, vertical: -1) : VisualDensity.standard, dividerColor: palette.strokeSoft, hoverColor: palette.hover, textTheme: tunedTextTheme, primaryTextTheme: tunedTextTheme, appBarTheme: const AppBarTheme( backgroundColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, surfaceTintColor: Colors.transparent, ), cardTheme: CardThemeData( elevation: 0, color: palette.surfacePrimary, margin: EdgeInsets.zero, shadowColor: palette.shadow, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.card), side: BorderSide(color: palette.strokeSoft), ), ), chipTheme: base.chipTheme.copyWith( backgroundColor: palette.surfaceSecondary, selectedColor: palette.surfacePrimary, secondarySelectedColor: palette.surfacePrimary, disabledColor: palette.surfaceSecondary, side: BorderSide(color: palette.strokeSoft), checkmarkColor: Colors.transparent, labelStyle: tunedTextTheme.labelMedium, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.chip), ), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), ), filledButtonTheme: FilledButtonThemeData( style: ButtonStyle( foregroundColor: const WidgetStatePropertyAll(Colors.white), shadowColor: WidgetStatePropertyAll(palette.shadow), elevation: const WidgetStatePropertyAll(0), surfaceTintColor: const WidgetStatePropertyAll(Colors.transparent), textStyle: WidgetStatePropertyAll( tunedTextTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600), ), minimumSize: WidgetStatePropertyAll( Size( 0, isDesktop ? AppSizes.buttonHeightDesktop : AppSizes.buttonHeightMobile, ), ), padding: const WidgetStatePropertyAll( EdgeInsets.symmetric(horizontal: 12, vertical: 0), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.button), ), ), backgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.pressed)) { return palette.accentHover; } return palette.accent; }), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( backgroundColor: palette.surfaceSecondary, foregroundColor: palette.textPrimary, shadowColor: palette.shadow, elevation: 0, surfaceTintColor: Colors.transparent, textStyle: tunedTextTheme.labelLarge?.copyWith( fontWeight: FontWeight.w600, ), minimumSize: Size( 0, isDesktop ? AppSizes.buttonHeightDesktop : AppSizes.buttonHeightMobile, ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.button), ), side: BorderSide(color: palette.strokeSoft), ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( foregroundColor: palette.textPrimary, textStyle: tunedTextTheme.labelLarge?.copyWith( fontWeight: FontWeight.w500, ), minimumSize: Size( 0, isDesktop ? AppSizes.buttonHeightDesktop : AppSizes.buttonHeightMobile, ), padding: const EdgeInsets.symmetric( horizontal: AppSpacing.sm, vertical: 0, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.button), ), ), ), iconButtonTheme: IconButtonThemeData( style: IconButton.styleFrom( foregroundColor: palette.textSecondary, backgroundColor: palette.surfaceSecondary.withValues(alpha: 0.88), surfaceTintColor: Colors.transparent, minimumSize: const Size(32, 32), padding: const EdgeInsets.all(7), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.icon), ), ), ), inputDecorationTheme: InputDecorationTheme( isDense: true, filled: true, fillColor: palette.surfacePrimary.withValues(alpha: 0.92), hintStyle: tunedTextTheme.bodyMedium?.copyWith( color: palette.textMuted, ), labelStyle: tunedTextTheme.bodyMedium?.copyWith( color: palette.textMuted, ), floatingLabelStyle: tunedTextTheme.bodyMedium?.copyWith( color: palette.textSecondary, ), contentPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.sm, vertical: AppSpacing.compact, ), constraints: const BoxConstraints(minHeight: AppSizes.inputHeight), border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.input), borderSide: BorderSide(color: palette.strokeSoft), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.input), borderSide: BorderSide(color: palette.strokeSoft), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.input), borderSide: BorderSide(color: palette.accent.withValues(alpha: 0.32)), ), ), segmentedButtonTheme: SegmentedButtonThemeData( style: ButtonStyle( side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return palette.surfacePrimary.withValues(alpha: 0.96); } return palette.surfaceSecondary; }), foregroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return palette.textPrimary; } return palette.textSecondary; }), padding: const WidgetStatePropertyAll( EdgeInsets.symmetric( horizontal: AppSpacing.sm, vertical: AppSpacing.compact, ), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.chip), ), ), textStyle: WidgetStatePropertyAll( tunedTextTheme.labelLarge?.copyWith(fontWeight: FontWeight.w500), ), ), ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, backgroundColor: palette.surfacePrimary.withValues(alpha: 0.96), contentTextStyle: TextStyle(color: palette.textPrimary), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.dialog), ), ), popupMenuTheme: PopupMenuThemeData( color: palette.surfacePrimary.withValues(alpha: 0.98), surfaceTintColor: Colors.transparent, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.dialog), ), ), ); } static TextTheme _textTheme( TextTheme base, { required AppPalette palette, required bool isMobile, }) { TextStyle withUiFont(TextStyle? style) { return (style ?? const TextStyle()).copyWith( fontFamily: null, fontFamilyFallback: const [], package: null, ); } return base.copyWith( displaySmall: withUiFont( base.displaySmall?.copyWith( fontSize: isMobile ? 24 : AppTypography.displaySize, fontWeight: AppTypography.displayWeight, letterSpacing: isMobile ? -0.24 : -0.32, height: isMobile ? 28 / 24 : AppTypography.displayHeight, color: palette.textPrimary, ), ), headlineSmall: withUiFont( base.headlineSmall?.copyWith( fontSize: AppTypography.titleSize, fontWeight: AppTypography.titleWeight, letterSpacing: -0.18, height: AppTypography.titleHeight, color: palette.textPrimary, ), ), titleLarge: withUiFont( base.titleLarge?.copyWith( fontSize: AppTypography.sectionSize, fontWeight: FontWeight.w600, letterSpacing: 0.02, height: AppTypography.sectionHeight, color: palette.textPrimary, ), ), titleMedium: withUiFont( base.titleMedium?.copyWith( fontSize: AppTypography.sectionSize, fontWeight: AppTypography.sectionWeight, letterSpacing: 0, height: AppTypography.sectionHeight, color: palette.textPrimary, ), ), titleSmall: withUiFont( base.titleSmall?.copyWith( fontSize: AppTypography.bodySize, fontWeight: FontWeight.w600, height: AppTypography.bodyHeight, color: palette.textPrimary, ), ), bodyLarge: withUiFont( base.bodyLarge?.copyWith( fontSize: AppTypography.bodySize, fontWeight: AppTypography.bodyWeight, height: AppTypography.bodyHeight, color: palette.textPrimary, ), ), bodyMedium: withUiFont( base.bodyMedium?.copyWith( fontSize: AppTypography.compactBodySize, fontWeight: AppTypography.compactBodyWeight, height: AppTypography.compactBodyHeight, color: palette.textSecondary, ), ), bodySmall: withUiFont( base.bodySmall?.copyWith( fontSize: AppTypography.captionSize, fontWeight: AppTypography.captionWeight, height: AppTypography.captionHeight, color: palette.textMuted, ), ), labelLarge: withUiFont( base.labelLarge?.copyWith( fontSize: AppTypography.sectionSize, fontWeight: AppTypography.emphasizedBodyWeight, height: AppTypography.sectionHeight, color: palette.textPrimary, ), ), labelMedium: withUiFont( base.labelMedium?.copyWith( fontSize: AppTypography.captionSize, fontWeight: FontWeight.w600, letterSpacing: 0.2, height: AppTypography.captionHeight, color: palette.textSecondary, ), ), labelSmall: withUiFont( base.labelSmall?.copyWith( fontSize: AppTypography.captionSize, fontWeight: FontWeight.w400, height: AppTypography.captionHeight, color: palette.textMuted, ), ), ); } }