feat(ground-suite): redesign observability console
This commit is contained in:
1 parent
c67d553076
commit
153b0bf262
12 files changed
+1145
-507
No files matched your search
@@ -1,121 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/theme.dart';
|
||||
|
||||
/// A bounded, translucent surface used for Ground Suite's data panels.
|
||||
///
|
||||
/// The blur is applied only inside the clipped panel, keeping the effect local
|
||||
/// to the UI rather than turning the whole desktop window into a filter.
|
||||
class GlassPanel extends StatelessWidget {
|
||||
const GlassPanel({
|
||||
required this.child,
|
||||
super.key,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(16)),
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
|
||||
child: _GlassSurface(borderRadius: borderRadius, child: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A scroll-friendly translucent panel without a backdrop filter.
|
||||
///
|
||||
/// Timeline rows use this surface so diagnostic volume does not turn into a
|
||||
/// growing set of expensive compositing layers.
|
||||
class GlassListItem extends StatelessWidget {
|
||||
const GlassListItem({
|
||||
required this.child,
|
||||
super.key,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(16)),
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
_GlassSurface(borderRadius: borderRadius, child: child);
|
||||
}
|
||||
|
||||
class _GlassSurface extends StatelessWidget {
|
||||
const _GlassSurface({required this.child, required this.borderRadius});
|
||||
|
||||
final Widget child;
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final isDark = scheme.brightness == Brightness.dark;
|
||||
final fill = isDark ? const Color(0xb30a0a0a) : const Color(0xc9ffffff);
|
||||
final border = isDark
|
||||
? scheme.primary.withValues(alpha: 0.24)
|
||||
: scheme.outline.withValues(alpha: 0.7);
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: fill,
|
||||
border: Border.all(color: border),
|
||||
borderRadius: borderRadius,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: isDark ? 0.24 : 0.08),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GroundSuiteBackdrop extends StatelessWidget {
|
||||
const GroundSuiteBackdrop({required this.child, super.key});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final base = isDark ? const Color(0xff050505) : const Color(0xffeef3ee);
|
||||
final haze = isDark
|
||||
? accentGreen.withValues(alpha: 0.1)
|
||||
: const Color(0xff007a2e).withValues(alpha: 0.07);
|
||||
|
||||
return ColoredBox(
|
||||
color: base,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: -260,
|
||||
right: -220,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
width: 620,
|
||||
height: 620,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(colors: [haze, Colors.transparent]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,28 @@ class SeverityBadge extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dark = Theme.of(context).brightness == Brightness.dark;
|
||||
final (icon, color) = switch (level.wireValue) {
|
||||
0 || 1 || 2 || 3 => (Icons.error_outline, Colors.red.shade700),
|
||||
4 => (Icons.warning_amber_outlined, Colors.orange.shade800),
|
||||
5 || 6 => (Icons.info_outline, Colors.blue.shade700),
|
||||
7 => (Icons.bug_report_outlined, Colors.grey.shade700),
|
||||
_ => (Icons.help_outline, Colors.grey.shade700),
|
||||
0 || 1 || 2 || 3 => (
|
||||
Icons.error_outline,
|
||||
dark ? const Color(0xffff6b63) : const Color(0xffd92d20),
|
||||
),
|
||||
4 => (
|
||||
Icons.warning_amber_outlined,
|
||||
dark ? const Color(0xfff7b955) : const Color(0xffb54708),
|
||||
),
|
||||
5 || 6 => (
|
||||
Icons.info_outline,
|
||||
dark ? const Color(0xff70b8ff) : const Color(0xff175cd3),
|
||||
),
|
||||
7 => (
|
||||
Icons.bug_report_outlined,
|
||||
dark ? const Color(0xffa8b5ac) : const Color(0xff546258),
|
||||
),
|
||||
_ => (
|
||||
Icons.help_outline,
|
||||
dark ? const Color(0xffa8b5ac) : const Color(0xff546258),
|
||||
),
|
||||
};
|
||||
|
||||
return Semantics(
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/theme.dart';
|
||||
|
||||
/// A blurred surface for stable application chrome only.
|
||||
///
|
||||
/// Keep this out of scrollable content. Diagnostic rows and metric panels use
|
||||
/// [InstrumentPanel], which has the same visual language without creating a
|
||||
/// compositing layer per item.
|
||||
class FrostedChrome extends StatelessWidget {
|
||||
const FrostedChrome({
|
||||
required this.child,
|
||||
super.key,
|
||||
this.borderRadius = BorderRadius.zero,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final highContrast = MediaQuery.maybeOf(context)?.highContrast ?? false;
|
||||
final surface = _SurfaceFrame(
|
||||
borderRadius: borderRadius,
|
||||
chrome: true,
|
||||
opaque: highContrast,
|
||||
child: child,
|
||||
);
|
||||
if (highContrast) {
|
||||
return surface;
|
||||
}
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: surface,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An opaque, high-legibility panel for operational data and event rows.
|
||||
class InstrumentPanel extends StatelessWidget {
|
||||
const InstrumentPanel({
|
||||
required this.child,
|
||||
super.key,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(12)),
|
||||
this.accent,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final BorderRadius borderRadius;
|
||||
final Color? accent;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _SurfaceFrame(
|
||||
borderRadius: borderRadius,
|
||||
accent: accent,
|
||||
opaque: true,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
class _SurfaceFrame extends StatelessWidget {
|
||||
const _SurfaceFrame({
|
||||
required this.child,
|
||||
required this.borderRadius,
|
||||
this.chrome = false,
|
||||
this.opaque = false,
|
||||
this.accent,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final BorderRadius borderRadius;
|
||||
final bool chrome;
|
||||
final bool opaque;
|
||||
final Color? accent;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final dark = scheme.brightness == Brightness.dark;
|
||||
final opacity = opaque ? 0xf6 : (dark ? 0xbf : 0xd9);
|
||||
final top = dark
|
||||
? Color.fromARGB(
|
||||
opacity,
|
||||
chrome ? 12 : 16,
|
||||
chrome ? 17 : 23,
|
||||
chrome ? 14 : 18,
|
||||
)
|
||||
: Color.fromARGB(opacity, 255, 255, 255);
|
||||
final bottom = dark
|
||||
? Color.fromARGB(opacity, 8, 11, 9)
|
||||
: Color.fromARGB(opacity, 244, 248, 244);
|
||||
final border =
|
||||
accent?.withValues(alpha: 0.74) ??
|
||||
scheme.outline.withValues(alpha: dark ? 0.92 : 0.82);
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [top, bottom],
|
||||
),
|
||||
border: Border.all(color: border),
|
||||
borderRadius: borderRadius,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: dark ? 0.18 : 0.06),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The static canvas behind Ground Suite's chrome and instrument surfaces.
|
||||
class AppCanvas extends StatelessWidget {
|
||||
const AppCanvas({required this.child, super.key});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dark = Theme.of(context).brightness == Brightness.dark;
|
||||
final base = dark ? const Color(0xff080b09) : const Color(0xfff3f6f3);
|
||||
final primaryHaze = (dark ? accentGreen : const Color(0xff087a3b))
|
||||
.withValues(alpha: dark ? 0.075 : 0.05);
|
||||
|
||||
return ColoredBox(
|
||||
color: base,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: -250,
|
||||
right: -200,
|
||||
child: IgnorePointer(child: _Haze(color: primaryHaze)),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -330,
|
||||
left: -260,
|
||||
child: IgnorePointer(
|
||||
child: _Haze(color: primaryHaze.withValues(alpha: 0.55)),
|
||||
),
|
||||
),
|
||||
Positioned.fill(child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Haze extends StatelessWidget {
|
||||
const _Haze({required this.color});
|
||||
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: 600,
|
||||
height: 600,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(colors: [color, Colors.transparent]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ConsoleLabel extends StatelessWidget {
|
||||
const ConsoleLabel(this.text, {super.key, this.color});
|
||||
|
||||
final String text;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Text(
|
||||
text.toUpperCase(),
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: color ?? Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.25,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class StatusBadge extends StatelessWidget {
|
||||
const StatusBadge({required this.label, super.key, this.color});
|
||||
|
||||
final String label;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final tone = color ?? scheme.onSurfaceVariant;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: tone.withValues(alpha: 0.11),
|
||||
border: Border.all(color: tone.withValues(alpha: 0.45)),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
child: ConsoleLabel(label, color: tone),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user