xworkmate-app/lib/theme/app_theme.dart

443 lines
14 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'app_palette.dart';
class AppSpacing {
AppSpacing._();
static const double xxs = 4.0;
static const double xs = 8.0;
static const double sm = 12.0;
static const double md = 16.0;
static const double lg = 24.0;
static const double xl = 32.0;
}
class AppRadius {
AppRadius._();
static const double card = 16.0;
static const double button = 12.0;
static const double input = 16.0;
static const double chip = 999.0;
static const double badge = 999.0;
static const double dialog = 16.0;
static const double sidebar = 20.0;
static const double icon = 12.0;
}
class AppTypography {
AppTypography._();
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 = 16.0;
static const FontWeight sectionWeight = FontWeight.w500;
static const double sectionHeight = 20 / 16;
static const double bodySize = 14.0;
static const FontWeight bodyWeight = FontWeight.w400;
static const double bodyHeight = 18 / 14;
static const double captionSize = 12.0;
static const FontWeight captionWeight = FontWeight.w400;
static const double captionHeight = 16 / 12;
}
class AppSizes {
AppSizes._();
static const double sidebarItemHeight = 40.0;
static const double sidebarIconSize = 20.0;
static const double sidebarTextSize = 14.0;
static const double sidebarExpandedWidth = 212.0;
static const double sidebarCollapsedWidth = 72.0;
static const double textareaHeight = 48.0;
static const double toolbarHeight = 40.0;
static const double buttonHeightDesktop = 40.0;
static const double buttonHeightMobile = 40.0;
}
class AppTheme {
static ThemeData light() =>
_theme(brightness: Brightness.light, palette: AppPalette.light);
static ThemeData dark() =>
_theme(brightness: Brightness.dark, palette: AppPalette.dark);
static ThemeData _theme({
required Brightness brightness,
required AppPalette palette,
}) {
final platform = defaultTargetPlatform;
final isDesktop =
platform == TargetPlatform.macOS ||
platform == TargetPlatform.windows ||
platform == TargetPlatform.linux;
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: platform),
colorScheme: colorScheme,
scaffoldBackgroundColor: palette.canvas,
extensions: [palette],
);
final tunedTextTheme = _textTheme(base.textTheme, palette: palette);
return base.copyWith(
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),
),
),
chipTheme: base.chipTheme.copyWith(
backgroundColor: palette.surfaceSecondary,
selectedColor: palette.surfacePrimary,
secondarySelectedColor: palette.surfacePrimary,
disabledColor: palette.surfaceSecondary,
side: BorderSide.none,
checkmarkColor: Colors.transparent,
labelStyle: tunedTextTheme.labelMedium,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.chip),
),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: palette.accent,
foregroundColor: Colors.white,
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: AppSpacing.md,
vertical: AppSpacing.sm,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
),
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: AppSpacing.md,
vertical: AppSpacing.sm,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
side: BorderSide.none,
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: palette.textPrimary,
textStyle: tunedTextTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w500,
),
minimumSize: Size(0, isDesktop ? 32 : 34),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
),
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
foregroundColor: palette.textSecondary,
backgroundColor: palette.surfaceSecondary,
surfaceTintColor: Colors.transparent,
minimumSize: const Size(40, 40),
padding: const EdgeInsets.all(10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.icon),
),
),
),
inputDecorationTheme: InputDecorationTheme(
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.md,
vertical: AppSpacing.sm,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Colors.transparent),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Colors.transparent),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: BorderSide(
color: palette.accent.withValues(alpha: 0.18),
),
),
),
segmentedButtonTheme: SegmentedButtonThemeData(
style: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return palette.surfacePrimary;
}
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.xs,
),
),
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.chip),
),
),
textStyle: WidgetStatePropertyAll(
tunedTextTheme.labelLarge?.copyWith(fontWeight: FontWeight.w500),
),
),
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
backgroundColor: palette.surfacePrimary,
contentTextStyle: TextStyle(color: palette.textPrimary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.dialog),
),
),
popupMenuTheme: PopupMenuThemeData(
color: palette.surfacePrimary,
surfaceTintColor: Colors.transparent,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.dialog),
),
),
);
}
static TextTheme _textTheme(
TextTheme base, {
required AppPalette palette,
}) {
final fallbackFonts = switch (defaultTargetPlatform) {
TargetPlatform.macOS || TargetPlatform.iOS => const <String>[
'.SF Pro Text',
'.SF NS Text',
'PingFang SC',
],
_ => const <String>[
'Inter',
'Segoe UI',
'Noto Sans CJK SC',
'PingFang SC',
],
};
TextStyle withUiFont(TextStyle? style) {
return (style ?? const TextStyle()).copyWith(
fontFamilyFallback: fallbackFonts,
package: null,
);
}
return base.copyWith(
displaySmall: withUiFont(
base.displaySmall?.copyWith(
fontSize: AppTypography.displaySize,
fontWeight: AppTypography.displayWeight,
letterSpacing: -0.32,
height: 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.08,
height: AppTypography.sectionHeight,
color: palette.textPrimary,
),
),
titleMedium: withUiFont(
base.titleMedium?.copyWith(
fontSize: AppTypography.sectionSize,
fontWeight: AppTypography.sectionWeight,
letterSpacing: -0.08,
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.bodySize,
fontWeight: AppTypography.bodyWeight,
height: AppTypography.bodyHeight,
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.bodySize,
fontWeight: FontWeight.w600,
height: AppTypography.bodyHeight,
color: palette.textPrimary,
),
),
labelMedium: withUiFont(
base.labelMedium?.copyWith(
fontSize: AppTypography.captionSize,
fontWeight: FontWeight.w500,
height: AppTypography.captionHeight,
color: palette.textSecondary,
),
),
labelSmall: withUiFont(
base.labelSmall?.copyWith(
fontSize: AppTypography.captionSize,
fontWeight: FontWeight.w400,
height: AppTypography.captionHeight,
color: palette.textMuted,
),
),
);
}
}