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>
376 lines
13 KiB
Dart
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);
|
|
}
|