405 lines
13 KiB
Dart
405 lines
13 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'app_palette.dart';
|
|
|
|
/// Design tokens for the XWorkmate design system.
|
|
/// Follows a modern AI developer tool design language with:
|
|
/// - 8px grid spacing
|
|
/// - Compact, neutral, professional aesthetic
|
|
/// - Consistent border radii
|
|
class AppSpacing {
|
|
AppSpacing._();
|
|
|
|
// 8px grid system
|
|
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 = 12.0;
|
|
static const double button = 8.0;
|
|
static const double input = 10.0;
|
|
static const double chip = 12.0;
|
|
static const double badge = 10.0;
|
|
static const double dialog = 16.0;
|
|
static const double sidebar = 14.0;
|
|
static const double icon = 8.0;
|
|
}
|
|
|
|
class AppTypography {
|
|
AppTypography._();
|
|
|
|
// H1 - 22px weight 600
|
|
static const double h1Size = 22.0;
|
|
static const FontWeight h1Weight = FontWeight.w600;
|
|
static const double h1Height = 1.25;
|
|
|
|
// H2 - 18px weight 600
|
|
static const double h2Size = 18.0;
|
|
static const FontWeight h2Weight = FontWeight.w600;
|
|
static const double h2Height = 1.3;
|
|
|
|
// Body - 14px weight 400
|
|
static const double bodySize = 14.0;
|
|
static const FontWeight bodyWeight = FontWeight.w400;
|
|
static const double bodyHeight = 1.4;
|
|
|
|
// Meta - 12px weight 400
|
|
static const double metaSize = 12.0;
|
|
static const FontWeight metaWeight = FontWeight.w400;
|
|
static const double metaHeight = 1.45;
|
|
}
|
|
|
|
class AppSizes {
|
|
AppSizes._();
|
|
|
|
// Sidebar
|
|
static const double sidebarItemHeight = 36.0;
|
|
static const double sidebarIconSize = 18.0;
|
|
static const double sidebarTextSize = 14.0;
|
|
static const double sidebarExpandedWidth = 204.0;
|
|
static const double sidebarCollapsedWidth = 72.0;
|
|
|
|
// Input area
|
|
static const double textareaHeight = 48.0;
|
|
static const double toolbarHeight = 36.0;
|
|
|
|
// Buttons
|
|
static const double buttonHeightDesktop = 34.0;
|
|
static const double buttonHeightMobile = 36.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.14,
|
|
),
|
|
);
|
|
|
|
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,
|
|
isDesktop: isDesktop,
|
|
);
|
|
|
|
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),
|
|
side: BorderSide(color: palette.strokeSoft),
|
|
),
|
|
),
|
|
chipTheme: base.chipTheme.copyWith(
|
|
backgroundColor: palette.surfaceSecondary,
|
|
side: BorderSide(color: palette.strokeSoft),
|
|
labelStyle: tunedTextTheme.labelMedium,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.chip)),
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
),
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: FilledButton.styleFrom(
|
|
textStyle: tunedTextTheme.labelLarge?.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
minimumSize: Size(0, isDesktop ? AppSizes.buttonHeightDesktop : AppSizes.buttonHeightMobile),
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.sm,
|
|
vertical: AppSpacing.xs,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.button),
|
|
),
|
|
),
|
|
),
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: palette.textPrimary,
|
|
textStyle: tunedTextTheme.labelLarge?.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
minimumSize: Size(0, isDesktop ? AppSizes.buttonHeightDesktop : AppSizes.buttonHeightMobile),
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.sm,
|
|
vertical: AppSpacing.xs,
|
|
),
|
|
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 ? 32 : 34),
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.sm,
|
|
vertical: AppSpacing.xs,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.button),
|
|
),
|
|
),
|
|
),
|
|
iconButtonTheme: IconButtonThemeData(
|
|
style: IconButton.styleFrom(
|
|
foregroundColor: palette.textSecondary,
|
|
backgroundColor: palette.surfaceSecondary,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.icon),
|
|
side: BorderSide(color: palette.strokeSoft),
|
|
),
|
|
minimumSize: const Size(34, 34),
|
|
padding: const EdgeInsets.all(8),
|
|
),
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: palette.surfaceSecondary,
|
|
hintStyle: tunedTextTheme.bodyMedium?.copyWith(
|
|
color: palette.textMuted,
|
|
),
|
|
labelStyle: tunedTextTheme.bodyMedium?.copyWith(
|
|
color: palette.textMuted,
|
|
),
|
|
contentPadding: EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.md,
|
|
vertical: AppSpacing.sm,
|
|
),
|
|
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.42)),
|
|
),
|
|
),
|
|
segmentedButtonTheme: SegmentedButtonThemeData(
|
|
style: ButtonStyle(
|
|
side: WidgetStatePropertyAll(BorderSide(color: palette.strokeSoft)),
|
|
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.surfaceTertiary,
|
|
contentTextStyle: TextStyle(color: palette.textPrimary),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.dialog)),
|
|
),
|
|
);
|
|
}
|
|
|
|
static TextTheme _textTheme(
|
|
TextTheme base, {
|
|
required AppPalette palette,
|
|
required bool isDesktop,
|
|
}) {
|
|
final fallbackFonts = switch (defaultTargetPlatform) {
|
|
TargetPlatform.macOS || TargetPlatform.iOS => const <String>[
|
|
'.SF NS Text',
|
|
'.SF Pro Text',
|
|
'PingFang SC',
|
|
'Helvetica Neue',
|
|
],
|
|
_ => const <String>['Inter', 'Noto Sans CJK SC', 'PingFang SC'],
|
|
};
|
|
|
|
TextStyle withUiFont(TextStyle? style) {
|
|
return (style ?? const TextStyle()).copyWith(
|
|
fontFamilyFallback: fallbackFonts,
|
|
package: null,
|
|
);
|
|
}
|
|
|
|
return base.copyWith(
|
|
// H1: 22px weight 600
|
|
displaySmall: withUiFont(
|
|
base.displaySmall?.copyWith(
|
|
fontSize: AppTypography.h1Size,
|
|
fontWeight: AppTypography.h1Weight,
|
|
letterSpacing: -0.24,
|
|
height: AppTypography.h1Height,
|
|
),
|
|
),
|
|
headlineSmall: withUiFont(
|
|
base.headlineSmall?.copyWith(
|
|
fontSize: AppTypography.h1Size,
|
|
fontWeight: AppTypography.h1Weight,
|
|
letterSpacing: -0.24,
|
|
height: AppTypography.h1Height,
|
|
),
|
|
),
|
|
// H2: 18px weight 600
|
|
titleLarge: withUiFont(
|
|
base.titleLarge?.copyWith(
|
|
fontSize: AppTypography.h2Size,
|
|
fontWeight: AppTypography.h2Weight,
|
|
letterSpacing: -0.16,
|
|
height: AppTypography.h2Height,
|
|
),
|
|
),
|
|
titleMedium: withUiFont(
|
|
base.titleMedium?.copyWith(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.08,
|
|
height: 1.35,
|
|
),
|
|
),
|
|
titleSmall: withUiFont(
|
|
base.titleSmall?.copyWith(
|
|
fontSize: isDesktop ? 14 : 15,
|
|
fontWeight: FontWeight.w500,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
// Body: 14px weight 400
|
|
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,
|
|
),
|
|
),
|
|
// Meta: 12px weight 400
|
|
bodySmall: withUiFont(
|
|
base.bodySmall?.copyWith(
|
|
fontSize: AppTypography.metaSize,
|
|
fontWeight: AppTypography.metaWeight,
|
|
height: AppTypography.metaHeight,
|
|
color: palette.textMuted,
|
|
),
|
|
),
|
|
labelLarge: withUiFont(
|
|
base.labelLarge?.copyWith(
|
|
fontSize: isDesktop ? 13 : 14,
|
|
fontWeight: FontWeight.w500,
|
|
height: 1.2,
|
|
),
|
|
),
|
|
labelMedium: withUiFont(
|
|
base.labelMedium?.copyWith(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
height: 1.2,
|
|
),
|
|
),
|
|
labelSmall: withUiFont(
|
|
base.labelSmall?.copyWith(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
height: 1.2,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|