Relocate the existing Astro website and preserve its assets and hosting configuration. Add the consumer/backend scaffold and themed Flutter package with consumer tests. Related to #1.
204 lines
7.5 KiB
Dart
204 lines
7.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'brand.dart';
|
|
|
|
/// Install with `MaterialApp(theme: SxchngeTheme.light(),
|
|
/// darkTheme: SxchngeTheme.dark())`.
|
|
abstract final class SxchngeTheme {
|
|
static const packageName = 'sxchnge_theme';
|
|
|
|
static ThemeData light() => _build(Brightness.light);
|
|
static ThemeData dark() => _build(Brightness.dark);
|
|
|
|
static ThemeData _build(Brightness brightness) {
|
|
final dark = brightness == Brightness.dark;
|
|
// Seed supplies complete Material roles; explicit overrides preserve brand
|
|
// colors and readable foregrounds rather than approximating the palette.
|
|
final scheme =
|
|
ColorScheme.fromSeed(
|
|
seedColor: SxchngeBrand.forest,
|
|
brightness: brightness,
|
|
).copyWith(
|
|
primary: dark ? SxchngeBrand.mint : SxchngeBrand.ink,
|
|
onPrimary: dark ? SxchngeBrand.ink : SxchngeBrand.mint,
|
|
primaryContainer: SxchngeBrand.mint,
|
|
onPrimaryContainer: SxchngeBrand.ink,
|
|
secondary: dark ? SxchngeBrand.mint : SxchngeBrand.forest,
|
|
onSecondary: dark ? SxchngeBrand.ink : SxchngeBrand.paper,
|
|
secondaryContainer: dark
|
|
? SxchngeBrand.darkReceive
|
|
: SxchngeBrand.sageSurface,
|
|
onSecondaryContainer: dark ? SxchngeBrand.mint : SxchngeBrand.ink,
|
|
tertiary: dark ? SxchngeBrand.lime : SxchngeBrand.forest,
|
|
onTertiary: dark ? SxchngeBrand.ink : SxchngeBrand.paper,
|
|
tertiaryContainer: SxchngeBrand.lime,
|
|
onTertiaryContainer: SxchngeBrand.ink,
|
|
surface: dark ? SxchngeBrand.ink : SxchngeBrand.paper,
|
|
onSurface: dark ? SxchngeBrand.paper : SxchngeBrand.ink,
|
|
onSurfaceVariant: dark ? const Color(0xFFB6C6B9) : SxchngeBrand.muted,
|
|
surfaceContainerLowest: dark ? const Color(0xFF101B17) : Colors.white,
|
|
surfaceContainerLow: dark
|
|
? const Color(0xFF1B2B23)
|
|
: SxchngeBrand.paper,
|
|
surfaceContainer: dark
|
|
? SxchngeBrand.darkField
|
|
: SxchngeBrand.sageSurface,
|
|
surfaceContainerHigh: dark
|
|
? SxchngeBrand.darkReceive
|
|
: SxchngeBrand.line,
|
|
surfaceContainerHighest: dark
|
|
? const Color(0xFF324A3C)
|
|
: const Color(0xFFCFD9CE),
|
|
outline: dark ? const Color(0xFF8CA291) : SxchngeBrand.muted,
|
|
outlineVariant: dark ? const Color(0xFF405A48) : SxchngeBrand.line,
|
|
inverseSurface: dark ? SxchngeBrand.paper : SxchngeBrand.ink,
|
|
onInverseSurface: dark ? SxchngeBrand.ink : SxchngeBrand.paper,
|
|
inversePrimary: dark ? SxchngeBrand.forest : SxchngeBrand.mint,
|
|
surfaceTint: Colors.transparent,
|
|
);
|
|
final base = ThemeData(
|
|
useMaterial3: true,
|
|
colorScheme: scheme,
|
|
fontFamily: SxchngeBrand.fontFamily,
|
|
package: packageName,
|
|
scaffoldBackgroundColor: scheme.surface,
|
|
visualDensity: VisualDensity.standard,
|
|
materialTapTargetSize: MaterialTapTargetSize.padded,
|
|
);
|
|
final text = base.textTheme.copyWith(
|
|
displayLarge: base.textTheme.displayLarge!.copyWith(
|
|
fontSize: 48,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -2,
|
|
height: 1.1,
|
|
),
|
|
displayMedium: base.textTheme.displayMedium!.copyWith(
|
|
fontSize: 40,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -1.5,
|
|
height: 1.15,
|
|
),
|
|
displaySmall: base.textTheme.displaySmall!.copyWith(
|
|
fontSize: 36,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -1,
|
|
height: 1.2,
|
|
),
|
|
headlineLarge: base.textTheme.headlineLarge!.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -1,
|
|
),
|
|
headlineMedium: base.textTheme.headlineMedium!.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.5,
|
|
),
|
|
headlineSmall: base.textTheme.headlineSmall!.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
bodyLarge: base.textTheme.bodyLarge!.copyWith(fontSize: 16, height: 1.65),
|
|
bodyMedium: base.textTheme.bodyMedium!.copyWith(
|
|
fontSize: 16,
|
|
height: 1.5,
|
|
),
|
|
bodySmall: base.textTheme.bodySmall!.copyWith(fontSize: 14, height: 1.5),
|
|
labelLarge: base.textTheme.labelLarge!.copyWith(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
labelMedium: base.textTheme.labelMedium!.copyWith(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
labelSmall: base.textTheme.labelSmall!.copyWith(fontSize: 14),
|
|
);
|
|
final border = OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
borderSide: BorderSide(color: scheme.outline),
|
|
);
|
|
return base.copyWith(
|
|
textTheme: text,
|
|
appBarTheme: AppBarThemeData(
|
|
backgroundColor: scheme.surface,
|
|
foregroundColor: scheme.onSurface,
|
|
surfaceTintColor: Colors.transparent,
|
|
elevation: 0,
|
|
centerTitle: false,
|
|
titleTextStyle: text.titleLarge,
|
|
),
|
|
cardTheme: CardThemeData(
|
|
color: scheme.surfaceContainer,
|
|
elevation: 0,
|
|
margin: EdgeInsets.zero,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
|
),
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: FilledButton.styleFrom(
|
|
minimumSize: const Size(64, 48),
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
|
shape: const StadiumBorder(),
|
|
textStyle: text.labelLarge,
|
|
),
|
|
),
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
minimumSize: const Size(64, 48),
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
|
shape: const StadiumBorder(),
|
|
textStyle: text.labelLarge,
|
|
elevation: 0,
|
|
),
|
|
),
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(
|
|
minimumSize: const Size(64, 48),
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
|
shape: const StadiumBorder(),
|
|
textStyle: text.labelLarge,
|
|
),
|
|
),
|
|
textButtonTheme: TextButtonThemeData(
|
|
style: TextButton.styleFrom(
|
|
minimumSize: const Size(48, 48),
|
|
textStyle: text.labelLarge,
|
|
),
|
|
),
|
|
inputDecorationTheme: InputDecorationThemeData(
|
|
filled: true,
|
|
fillColor: scheme.surfaceContainerLowest,
|
|
contentPadding: const EdgeInsets.all(16),
|
|
border: border,
|
|
enabledBorder: border,
|
|
focusedBorder: border.copyWith(
|
|
borderSide: BorderSide(
|
|
color: dark ? SxchngeBrand.mint : SxchngeBrand.focus,
|
|
width: 2,
|
|
),
|
|
),
|
|
errorBorder: border.copyWith(
|
|
borderSide: BorderSide(color: scheme.error),
|
|
),
|
|
focusedErrorBorder: border.copyWith(
|
|
borderSide: BorderSide(color: scheme.error, width: 2),
|
|
),
|
|
),
|
|
dialogTheme: DialogThemeData(
|
|
backgroundColor: scheme.surface,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
|
),
|
|
dividerTheme: DividerThemeData(
|
|
color: scheme.outlineVariant,
|
|
thickness: 1,
|
|
),
|
|
navigationBarTheme: NavigationBarThemeData(
|
|
backgroundColor: scheme.surface,
|
|
indicatorColor: scheme.primaryContainer,
|
|
),
|
|
textSelectionTheme: TextSelectionThemeData(
|
|
cursorColor: scheme.primary,
|
|
selectionColor: SxchngeBrand.lime.withValues(alpha: dark ? 0.3 : 0.5),
|
|
selectionHandleColor: scheme.primary,
|
|
),
|
|
);
|
|
}
|
|
}
|