Files
DriverVault/Phone App/lib/theme.dart
T
2026-07-06 08:50:52 +02:00

284 lines
11 KiB
Dart

import "package:flutter/material.dart";
import "package:google_fonts/google_fonts.dart";
/// DriverVault design tokens + Material theme, mirroring the web app's design
/// system (blue-led cool palette, Archivo + DM Mono type, 22px cards, soft
/// cool shadows). Every screen styles through this so light/dark theme for free.
class DriverVault {
DriverVault._();
// ---- Brand blue ramp (from the DriverVault mark) ----
static const brand900 = Color(0xFF0B1730);
static const brand800 = Color(0xFF0F1E3D);
static const brand700 = Color(0xFF1E40AF);
static const brand600 = Color(0xFF2563EB);
static const brand500 = Color(0xFF3B82F6);
static const brand400 = Color(0xFF60A5FA);
static const brand300 = Color(0xFF93C5FD);
static const brand100 = Color(0xFFE8F0FD);
// ---- Cool neutrals ----
static const ink900 = Color(0xFF0F1E3D);
static const ink600 = Color(0xFF3E4E68);
static const ink500 = Color(0xFF5C6B85);
static const ink400 = Color(0xFF7A8AA6);
static const ink200 = Color(0xFFD6DEEA);
static const ink100 = Color(0xFFE4E9F2);
static const ink50 = Color(0xFFEEF2F8);
static const ink25 = Color(0xFFF7F9FC);
// ---- Semantic status (car: OK / due / fault) ----
static const success = Color(0xFF1F8A5B);
static const warning = Color(0xFFD9822B);
static const danger = Color(0xFFDC2A45);
static const info = Color(0xFF2563EB);
// Light status tints
static const successSoft = Color(0xFFE1F3EA);
static const warningSoft = Color(0xFFFBEDDD);
static const dangerSoft = Color(0xFFFBE3E7);
static const infoSoft = Color(0xFFE8F0FD);
// Dark status tints (re-cut for dark surfaces)
static const successSoftDark = Color(0xFF12352A);
static const warningSoftDark = Color(0xFF3A2A16);
static const dangerSoftDark = Color(0xFF3A1620);
static const infoSoftDark = Color(0xFF122A4D);
// ---- Dark surfaces / text / borders ----
static const darkPage = Color(0xFF0B1730);
static const darkCard = Color(0xFF13233F);
static const darkSunken = Color(0xFF0F1E38);
static const darkBorder = Color(0xFF21324F);
static const darkBorderStrong = Color(0xFF2C3F5E);
static const darkTextStrong = Color(0xFFF2F6FC);
static const darkTextBody = Color(0xFFB7C4D9);
static const darkTextMuted = Color(0xFF7C8CA8);
static const darkAccent = Color(0xFF3B82F6);
static const radiusControl = 12.0;
static const radiusCard = 22.0;
static bool isDark(BuildContext c) => Theme.of(c).brightness == Brightness.dark;
// Brand tint used for avatars / info chips (adapts to theme).
static Color brandTint(BuildContext c) => isDark(c) ? const Color(0xFF17294A) : brand100;
static Color brandOnTint(BuildContext c) => isDark(c) ? brand300 : brand700;
/// Muted secondary text colour (replaces ad-hoc Colors.grey).
static Color muted(BuildContext c) => isDark(c) ? darkTextMuted : ink400;
/// DM Mono style for data / units / labels.
static TextStyle mono(BuildContext c, {double size = 13, FontWeight weight = FontWeight.w500, Color? color}) =>
GoogleFonts.dmMono(
fontSize: size,
fontWeight: weight,
letterSpacing: 0.2,
color: color ?? Theme.of(c).textTheme.bodyMedium?.color,
);
static ThemeData theme(Brightness brightness) {
final dark = brightness == Brightness.dark;
final scheme = ColorScheme(
brightness: brightness,
primary: dark ? darkAccent : brand600,
onPrimary: Colors.white,
primaryContainer: dark ? const Color(0xFF17294A) : brand100,
onPrimaryContainer: dark ? brand300 : brand700,
secondary: dark ? brand400 : brand700,
onSecondary: Colors.white,
surface: dark ? darkCard : Colors.white,
onSurface: dark ? darkTextStrong : ink900,
surfaceContainerHighest: dark ? darkSunken : ink50,
onSurfaceVariant: dark ? darkTextBody : ink600,
outline: dark ? darkBorderStrong : ink200,
outlineVariant: dark ? darkBorder : ink100,
error: danger,
onError: Colors.white,
errorContainer: dark ? dangerSoftDark : dangerSoft,
onErrorContainer: danger,
);
final baseText = dark ? ThemeData.dark().textTheme : ThemeData.light().textTheme;
return ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: scheme,
scaffoldBackgroundColor: dark ? darkPage : ink25,
textTheme: GoogleFonts.archivoTextTheme(baseText).apply(
bodyColor: dark ? darkTextBody : ink600,
displayColor: dark ? darkTextStrong : ink900,
),
appBarTheme: AppBarTheme(
backgroundColor: dark ? darkCard : Colors.white,
foregroundColor: dark ? darkTextStrong : ink900,
elevation: 0,
scrolledUnderElevation: 0.5,
centerTitle: false,
titleTextStyle: GoogleFonts.archivo(
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: -0.4,
color: dark ? darkTextStrong : ink900,
),
),
cardTheme: CardThemeData(
color: dark ? darkCard : Colors.white,
elevation: 0,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusCard),
side: BorderSide(color: dark ? darkBorder : ink100),
),
),
dividerColor: dark ? darkBorder : ink100,
dividerTheme: DividerThemeData(color: dark ? darkBorder : ink100, thickness: 1),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: dark ? darkSunken : Colors.white,
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusControl),
borderSide: BorderSide(color: dark ? darkBorderStrong : ink200),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusControl),
borderSide: BorderSide(color: dark ? darkBorderStrong : ink200),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusControl),
borderSide: BorderSide(color: dark ? darkAccent : brand600, width: 2),
),
labelStyle: TextStyle(color: dark ? darkTextMuted : ink500),
floatingLabelStyle: TextStyle(color: dark ? darkAccent : brand600),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: dark ? darkAccent : brand600,
foregroundColor: Colors.white,
textStyle: GoogleFonts.archivo(fontWeight: FontWeight.w600, fontSize: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusControl)),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: dark ? darkTextBody : ink600,
side: BorderSide(color: dark ? darkBorderStrong : ink200),
textStyle: GoogleFonts.archivo(fontWeight: FontWeight.w600, fontSize: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusControl)),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: dark ? brand300 : brand700,
textStyle: GoogleFonts.archivo(fontWeight: FontWeight.w600, fontSize: 14),
),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: dark ? darkAccent : brand600,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusControl)),
),
chipTheme: ChipThemeData(
backgroundColor: dark ? darkSunken : ink50,
side: BorderSide(color: dark ? darkBorder : ink100),
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: dark ? darkCard : Colors.white,
surfaceTintColor: Colors.transparent,
indicatorColor: dark ? const Color(0xFF17294A) : brand100,
elevation: 0,
height: 66,
labelTextStyle: WidgetStateProperty.resolveWith((states) {
final on = states.contains(WidgetState.selected);
return GoogleFonts.archivo(
fontSize: 11,
fontWeight: on ? FontWeight.w600 : FontWeight.w500,
color: on ? (dark ? brand300 : brand700) : (dark ? darkTextMuted : ink400),
);
}),
iconTheme: WidgetStateProperty.resolveWith((states) {
final on = states.contains(WidgetState.selected);
return IconThemeData(
size: 24,
color: on ? (dark ? brand300 : brand700) : (dark ? darkTextMuted : ink400),
);
}),
),
snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating),
);
}
}
/// The DriverVault "Fast Forward" mark — three forward-leaning rounded bars of
/// increasing height (skewX(-13°)), reading as acceleration / momentum.
class DriverVaultMark extends StatelessWidget {
final double size;
const DriverVaultMark({super.key, this.size = 32});
@override
Widget build(BuildContext context) {
final u = size / 40; // scale factor
Widget bar(double h, Color c) => Container(
width: 5 * u,
height: h * u,
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(3 * u)),
);
return SizedBox(
width: size,
height: size,
child: Transform(
alignment: Alignment.center,
transform: Matrix4.skewX(-0.23),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
bar(18, DriverVault.brand700),
SizedBox(width: 3.5 * u),
bar(28, DriverVault.brand500),
SizedBox(width: 3.5 * u),
bar(36, DriverVault.brand400),
],
),
),
);
}
}
/// Full DriverVault lockup: the mark + the Archivo italic-800 wordmark
/// ("Driver" in strong ink, "Vault" in brand blue).
class DriverVaultLogo extends StatelessWidget {
final double markSize;
final double fontSize;
const DriverVaultLogo({super.key, this.markSize = 32, this.fontSize = 24});
@override
Widget build(BuildContext context) {
final strong = DriverVault.isDark(context) ? DriverVault.darkTextStrong : DriverVault.ink900;
final brand = DriverVault.isDark(context) ? DriverVault.brand400 : DriverVault.brand700;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
DriverVaultMark(size: markSize),
SizedBox(width: markSize * 0.32),
Text.rich(
TextSpan(children: [
TextSpan(text: "Driver", style: TextStyle(color: strong)),
TextSpan(text: "Vault", style: TextStyle(color: brand)),
]),
style: GoogleFonts.archivo(
fontSize: fontSize,
fontWeight: FontWeight.w800,
fontStyle: FontStyle.italic,
letterSpacing: -0.5,
),
),
],
);
}
}