Files
PilotVault/Fly App/lib/theme.dart
T
tajniak81andClaude Opus 4.8 afc6952eda Initial commit: PilotVault multi-service project
Add API Server (Go/PocketBase), Web App (Go BFF + Vue), Fly App
(Flutter/DJI MSDK), Adobe Plugin, and Docker/Docker AIO deployment
configs. Design assets and build artifacts are gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 11:43:33 +02:00

376 lines
13 KiB
Dart

import 'package:flutter/material.dart';
/// PilotVault design system (mobile) — light-default brand theme.
/// Signal Blue accent over Vault Navy and cool slate neutrals; Space Grotesk
/// for structure, Space Mono for data/telemetry and uppercase eyebrow labels.
class PV {
PV._();
static const String fontSans = 'Space Grotesk';
static const String fontMono = 'Space Mono';
// Brand
static const Color navy = Color(0xFF0F1E3D); // Vault Navy
static const Color accent = Color(0xFF3D7BF0); // Signal Blue
static const Color accentHover = Color(0xFF2B62CC);
// Status (names kept: ready=green, caution=amber, warning=red fault)
static const Color ready = Color(0xFF1F8A5B);
static const Color readyFg = Color(0xFF177049);
static const Color caution = Color(0xFFD9852B);
static const Color cautionFg = Color(0xFFB86C1B);
static const Color warning = Color(0xFFD64545);
static const Color warningFg = Color(0xFFB83232);
// Surfaces (light)
static const Color surface0 = Color(0xFFEEF0F3); // Cloud app ground
static const Color surface1 = Color(0xFFFFFFFF); // card / chrome
static const Color surface2 = Color(0xFFF6F7F9); // inset tiles / inputs
// Text
static const Color ink = navy; // primary
static const Color inkSecondary = Color(0xFF5A6B85); // steel
static const Color inkMuted = Color(0xFF97A1B0); // slate-400
// Hairlines
static const Color line = Color(0xFFDCE0E7);
static const Color lineStrong = Color(0xFFC5CCD7);
static const double radius = 14; // cards
static const double radiusCtl = 10; // controls
static const List<BoxShadow> shadowXs = <BoxShadow>[
BoxShadow(color: Color(0x0F0F1E3D), blurRadius: 2, offset: Offset(0, 1)),
];
// Type scale — mono for data, sans for chrome
static const TextStyle telemetry = TextStyle(
fontFamily: fontMono,
fontSize: 30,
fontWeight: FontWeight.w500,
height: 1.0,
color: ink,
fontFeatures: <FontFeature>[FontFeature.tabularFigures()],
);
static const TextStyle mode = TextStyle(
fontFamily: fontSans,
fontSize: 18,
fontWeight: FontWeight.w600,
letterSpacing: -0.4,
color: ink,
);
static const TextStyle body = TextStyle(fontFamily: fontSans, fontSize: 14, color: ink);
static const TextStyle caption = TextStyle(fontFamily: fontSans, fontSize: 12, color: inkSecondary);
// Mono ALL-CAPS eyebrow (telemetry field names / section labels)
static const TextStyle label = TextStyle(
fontFamily: fontMono,
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1.5,
color: inkMuted,
);
// Mono value (serials, coordinates, counts)
static const TextStyle mono = TextStyle(
fontFamily: fontMono,
fontSize: 14,
fontWeight: FontWeight.w700,
color: ink,
fontFeatures: <FontFeature>[FontFeature.tabularFigures()],
);
static ThemeData theme() {
final ThemeData base = ThemeData.light(useMaterial3: true);
final ColorScheme scheme = ColorScheme.fromSeed(
seedColor: accent,
brightness: Brightness.light,
).copyWith(primary: accent, surface: surface1, onSurface: ink);
OutlineInputBorder borderOf(Color c, [double w = 1]) => OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusCtl),
borderSide: BorderSide(color: c, width: w),
);
return base.copyWith(
colorScheme: scheme,
scaffoldBackgroundColor: surface0,
dividerColor: line,
textTheme: base.textTheme.apply(
fontFamily: fontSans,
bodyColor: ink,
displayColor: ink,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: surface2,
isDense: true,
hintStyle: const TextStyle(color: inkMuted),
labelStyle: const TextStyle(color: inkSecondary),
prefixIconColor: inkMuted,
enabledBorder: borderOf(line),
focusedBorder: borderOf(accent, 2),
border: borderOf(line),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: accent,
foregroundColor: Colors.white,
disabledBackgroundColor: surface2,
disabledForegroundColor: inkMuted,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusCtl)),
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18),
textStyle: const TextStyle(fontFamily: fontSans, fontWeight: FontWeight.w600, fontSize: 14),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: ink,
backgroundColor: surface1,
side: const BorderSide(color: lineStrong),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusCtl)),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
textStyle: const TextStyle(fontFamily: fontSans, fontWeight: FontWeight.w600, fontSize: 14),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(foregroundColor: inkSecondary),
),
snackBarTheme: SnackBarThemeData(
backgroundColor: navy,
contentTextStyle: const TextStyle(fontFamily: fontSans, color: Colors.white),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusCtl)),
behavior: SnackBarBehavior.floating,
),
);
}
}
/// PilotVault Vector mark — two offset chevrons. Back wing Signal Blue,
/// front wing the foreground color (navy on light).
class PvBrandMark extends StatelessWidget {
const PvBrandMark({super.key, this.size = 28, this.frontColor});
final double size;
final Color? frontColor;
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(size, size),
painter: _ChevronPainter(frontColor ?? PV.ink),
);
}
}
class _ChevronPainter extends CustomPainter {
_ChevronPainter(this.front);
final Color front;
@override
void paint(Canvas canvas, Size size) {
final double s = size.width / 48.0;
Paint stroke(Color c) => Paint()
..color = c
..style = PaintingStyle.stroke
..strokeWidth = 4 * s
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
final Path back = Path()
..moveTo(8 * s, 30 * s)
..lineTo(19 * s, 17 * s)
..lineTo(30 * s, 30 * s);
final Path frontWing = Path()
..moveTo(18 * s, 33 * s)
..lineTo(29 * s, 20 * s)
..lineTo(40 * s, 33 * s);
canvas.drawPath(back, stroke(PV.accent));
canvas.drawPath(frontWing, stroke(front));
}
@override
bool shouldRepaint(covariant _ChevronPainter old) => old.front != front;
}
/// A card surface: white face, hairline border, soft cool shadow.
class PvPanel extends StatelessWidget {
const PvPanel({super.key, required this.child, this.padding = const EdgeInsets.all(16)});
final Widget child;
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) {
return Container(
padding: padding,
decoration: BoxDecoration(
color: PV.surface1,
borderRadius: BorderRadius.circular(PV.radius),
border: Border.all(color: PV.line),
boxShadow: PV.shadowXs,
),
child: child,
);
}
}
/// Mono uppercase eyebrow label (telemetry field / section label).
class SectionLabel extends StatelessWidget {
const SectionLabel(this.text, {super.key});
final String text;
@override
Widget build(BuildContext context) => Text(text.toUpperCase(), style: PV.label);
}
/// A status dot. Solid by default; brand carries status by color, not glow.
class StatusDot extends StatelessWidget {
const StatusDot(this.color, {super.key, this.glow = false, this.size = 9});
final Color color;
final bool glow;
final double size;
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: glow ? <BoxShadow>[BoxShadow(color: color.withValues(alpha: 0.35), blurRadius: 6)] : null,
),
);
}
}
/// ─────────────────────────────────────────────────────────────────────────────
/// Full semantic token set, theme-flipping (mirrors tokens/colors.css). The Go
/// Fly and Album screens follow the device brightness; Flight Control overlays a
/// live camera feed and is always dark, so it uses fixed glass colors (below).
/// ─────────────────────────────────────────────────────────────────────────────
class PVScheme {
const PVScheme({
required this.brightness,
required this.bgApp,
required this.surface,
required this.surface2,
required this.surfaceInset,
required this.surfaceRaised,
required this.border,
required this.borderStrong,
required this.borderSubtle,
required this.textPrimary,
required this.textSecondary,
required this.textTertiary,
required this.textInverse,
required this.accent,
required this.accentHover,
required this.accentSoft,
required this.accentSoftFg,
required this.success,
required this.successSoft,
required this.successFg,
required this.warning,
required this.warningSoft,
required this.warningFg,
required this.danger,
required this.dangerSoft,
required this.dangerFg,
required this.shadowXs,
required this.shadowSm,
});
final Brightness brightness;
final Color bgApp, surface, surface2, surfaceInset, surfaceRaised;
final Color border, borderStrong, borderSubtle;
final Color textPrimary, textSecondary, textTertiary, textInverse;
final Color accent, accentHover, accentSoft, accentSoftFg;
final Color success, successSoft, successFg;
final Color warning, warningSoft, warningFg;
final Color danger, dangerSoft, dangerFg;
final List<BoxShadow> shadowXs, shadowSm;
bool get isDark => brightness == Brightness.dark;
static const PVScheme light = PVScheme(
brightness: Brightness.light,
bgApp: Color(0xFFEEF0F3),
surface: Color(0xFFFFFFFF),
surface2: Color(0xFFF6F7F9),
surfaceInset: Color(0xFFEEF0F3),
surfaceRaised: Color(0xFFFFFFFF),
border: Color(0xFFDCE0E7),
borderStrong: Color(0xFFC5CCD7),
borderSubtle: Color(0xFFE6E9EE),
textPrimary: Color(0xFF0F1E3D),
textSecondary: Color(0xFF5A6B85),
textTertiary: Color(0xFF97A1B0),
textInverse: Color(0xFFFFFFFF),
accent: Color(0xFF3D7BF0),
accentHover: Color(0xFF2B62CC),
accentSoft: Color(0xFFEAF1FE),
accentSoftFg: Color(0xFF1F4CA0),
success: Color(0xFF1F8A5B),
successSoft: Color(0xFFDCF1E7),
successFg: Color(0xFF177049),
warning: Color(0xFFD9852B),
warningSoft: Color(0xFFFBEBD5),
warningFg: Color(0xFFB86C1B),
danger: Color(0xFFD64545),
dangerSoft: Color(0xFFFBE0E0),
dangerFg: Color(0xFFB83232),
shadowXs: <BoxShadow>[BoxShadow(color: Color(0x0F0F1E3D), blurRadius: 2, offset: Offset(0, 1))],
shadowSm: <BoxShadow>[
BoxShadow(color: Color(0x0F0F1E3D), blurRadius: 3, offset: Offset(0, 1)),
BoxShadow(color: Color(0x0A0F1E3D), blurRadius: 2, offset: Offset(0, 1)),
],
);
static const PVScheme dark = PVScheme(
brightness: Brightness.dark,
bgApp: Color(0xFF0B1730),
surface: Color(0xFF10203F),
surface2: Color(0xFF142748),
surfaceInset: Color(0xFF0B1730),
surfaceRaised: Color(0xFF16294B),
border: Color(0x1AFFFFFF),
borderStrong: Color(0x2EFFFFFF),
borderSubtle: Color(0x0FFFFFFF),
textPrimary: Color(0xFFF4F7FC),
textSecondary: Color(0xFF8FA0BE),
textTertiary: Color(0xFF5E6E8C),
textInverse: Color(0xFF0F1E3D),
accent: Color(0xFF5B93F5),
accentHover: Color(0xFF8FB4F6),
accentSoft: Color(0x2E3D7BF0),
accentSoftFg: Color(0xFF8FB4F6),
success: Color(0xFF1F8A5B),
successSoft: Color(0x381F8A5B),
successFg: Color(0xFF5FD3A0),
warning: Color(0xFFD9852B),
warningSoft: Color(0x38D9852B),
warningFg: Color(0xFFF0B26A),
danger: Color(0xFFD64545),
dangerSoft: Color(0x38D64545),
dangerFg: Color(0xFFF08A8A),
shadowXs: <BoxShadow>[BoxShadow(color: Color(0x59000000), blurRadius: 2, offset: Offset(0, 1))],
shadowSm: <BoxShadow>[BoxShadow(color: Color(0x66000000), blurRadius: 3, offset: Offset(0, 1))],
);
/// Resolves the scheme from the device brightness.
static PVScheme of(BuildContext context) =>
MediaQuery.platformBrightnessOf(context) == Brightness.dark ? dark : light;
}
/// Fixed "glass" palette for the Flight Control overlay (always over a dark feed).
class Glass {
Glass._();
static const Color ink = Color(0xFFEAF0FA);
static const Color pill = Color(0x80081020); // rgba(8,16,32,0.5)
static const Color pillStrong = Color(0x8C081020); // rgba(8,16,32,0.55)
static const Color accent = Color(0xEB3D7BF0); // rgba(61,123,240,0.92)
static const Color hairline = Color(0x1FFFFFFF); // rgba(255,255,255,0.12)
static const Color sat = Color(0xFF7FE0B0);
static const Color rec = Color(0xFFD64545);
static const Color subject = Color(0xFFF4C542);
}