Files
app-flutter/lib/widgets/stat_box.dart
T
mikkeli 3aeb09d61f
/ crosscheck (push) Successful in 0s
CI / Analyze & Test (push) Successful in 13s
CI / Debug APK (push) Successful in 22s
Palette: seven roles (accent, good, caution added; highlight renamed alarm); no hard-coded colours left
background, foreground, subdued, accent (emphasis without danger), and the state
gradient good → caution → alarm; surface and onFill derived. smart-serow's
four-key themes still load (highlight = alarm; the new roles fall back).
Material's scheme follows: buttons in accent instead of the alarm colour.
Link state, mock banner, signal tiles, raw log, checklist button and marks,
camera page all take palette roles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-14 13:55:52 +09:00

212 lines
6.8 KiB
Dart

// The boxed-widget pattern from smart-serow's HUD (pi/ui/lib/widgets/stat_box*.dart):
// every readout is a box that takes its share of a row (`flex`) and sizes its type
// from its own height, so a layout is a grid of boxes, and a value changing digits
// never moves anything. Value in foreground with tabular figures, unit and label in
// subdued; a warning turns the value to alarm.
//
// Added for Yukart: [dim]. A missing or stale value is a normal state in a car
// (app guideline 2), shown in subdued rather than hidden.
import 'package:flutter/material.dart';
import '../theme/palette.dart';
/// A labeled stat box: the small one, for rows of readouts.
class StatBox extends StatelessWidget {
const StatBox({
super.key,
required this.value,
this.unit,
required this.label,
this.flex = 1,
this.isWarning,
this.dim = false,
});
final String value;
final String? unit;
final String label;
final int flex;
/// If it returns true, the value shows in alarm.
final bool Function()? isWarning;
/// Missing or stale: the value shows in subdued.
final bool dim;
@override
Widget build(BuildContext context) {
final theme = AppTheme.of(context);
final warning = isWarning?.call() ?? false;
final valueColor = dim
? theme.subdued
: warning
? theme.alarm
: theme.foreground;
return Expanded(
flex: flex,
child: LayoutBuilder(
builder: (context, constraints) {
final baseSize = constraints.maxHeight * 0.4;
// Shrinks (never wraps) when a label is wider than its box.
return Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Value + optional unit
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: TextStyle(
fontFamily: hudFont,
fontSize: baseSize,
fontWeight: FontWeight.w400,
fontFeatures: const [FontFeature.tabularFigures()],
color: valueColor,
height: 1,
),
),
SizedBox(width: baseSize * 0.1),
if (unit != null) ...[
const SizedBox(width: 4),
Text(
unit!,
style: TextStyle(
fontFamily: hudFont,
fontSize: baseSize * 0.5,
fontWeight: FontWeight.w400,
color: theme.subdued,
height: 1,
),
),
],
],
),
SizedBox(height: baseSize * 0.1),
// Label
Text(
label,
style: TextStyle(
fontFamily: hudFont,
fontSize: baseSize * 0.6,
fontWeight: FontWeight.w400,
color: theme.subdued,
letterSpacing: 1,
),
),
],
),
),
);
},
),
);
}
}
/// The large stat box, for the main area. Fixed-size: content changes don't move the layout.
class StatBoxMain extends StatelessWidget {
const StatBoxMain({
super.key,
required this.value,
this.unit,
required this.label,
this.flex = 1,
this.isWarning,
this.dim = false,
});
final String value;
final String? unit;
final String label;
final int flex;
final bool Function()? isWarning;
final bool dim;
@override
Widget build(BuildContext context) {
final theme = AppTheme.of(context);
final warning = isWarning?.call() ?? false;
return Expanded(
flex: flex,
child: LayoutBuilder(
builder: (context, constraints) {
// Scale fonts relative to box height for consistent proportions
final baseSize = constraints.maxHeight * 0.4;
// Shrinks (never wraps) when a label is wider than its box.
return Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Value + optional unit row
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: TextStyle(
fontFamily: hudFont,
fontSize: baseSize,
fontWeight: FontWeight.w300,
fontFeatures: const [FontFeature.tabularFigures()],
color: dim
? theme.subdued
: warning
? theme.alarm
: theme.foreground,
height: 1,
),
),
if (unit != null) ...[
const SizedBox(width: 8),
Text(
unit!,
style: TextStyle(
fontFamily: hudFont,
fontSize: baseSize * 0.4,
fontWeight: FontWeight.w400,
color: theme.subdued,
height: 1,
),
),
],
],
),
SizedBox(height: baseSize * 0.1),
// Label
Text(
label,
style: TextStyle(
fontFamily: hudFont,
fontSize: baseSize * 0.35,
fontWeight: FontWeight.w400,
color: theme.subdued,
letterSpacing: 2,
),
),
],
),
),
);
},
),
);
}
}