xworkmate-app/lib/theme/app_theme.dart

556 lines
19 KiB
Dart

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 = 12.0;
static const double button = 12.0;
static const double input = 12.0;
static const double chip = 12.0;
static const double badge = 12.0;
static const double dialog = 12.0;
static const double sidebar = 12.0;
static const double icon = 12.0;
}
class SimpleTypography {
SimpleTypography._();
static const double displaySize = 28.0;
static const FontWeight displayWeight = FontWeight.w700;
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 = 32.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.surfacePrimary.withValues(alpha: 0.94),
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,
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 <String>[],
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.compactBodySize,
fontWeight: FontWeight.w600,
height: AppTypography.compactBodyHeight,
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.emphasizedBodySize,
fontWeight: AppTypography.emphasizedBodyWeight,
height: AppTypography.emphasizedBodyHeight,
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,
),
),
);
}
}