Initial commit
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user