Files
Garage-SDKs/garage_ui/lib/field_error.dart
T
ImBenjiandClaude Opus 5.5 b269201919 The Garage SDKs, in the open
garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of
Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme,
docs under docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
2026-09-23 18:49:21 +01:00

93 lines
3.2 KiB
Dart

import "package:flutter/widgets.dart";
import "package:garage_ui/theme/garage_theme.dart";
/// How long a field takes to admit theres something wrong with it, and how
/// long the row takes to make room for saying so. One number for both: the
/// outline and the line under it are one event and shouldnt arrive at
/// different times.
const Duration kFieldErrorDuration = Duration(milliseconds: 180);
const Curve kFieldErrorCurve = Curves.easeInOutCubic;
/// "This one was rejected", published over a control rather than set on it.
///
/// A row knows the value is wrong. What the row is HOLDING could be anything -
/// a text box, a select, a group of them - so the row cant reach in and colour
/// it. It says so here instead, and every control that draws itself a border
/// reads this on the way past.
///
/// Only the border moves. A field whose fill goes red reads as a state the
/// thing is permanently in; a red outline reads as a correction, which is what
/// this is - and it goes back to normal the moment the value does.
class FieldErrorScope extends InheritedWidget {
const FieldErrorScope({
super.key,
required this.invalid,
required super.child,
});
final bool invalid;
static bool of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<FieldErrorScope>()?.invalid ??
false;
@override
bool updateShouldNotify(FieldErrorScope old) => old.invalid != invalid;
}
/// The destructive outline, faded in OVER whatever border is allready there.
///
/// Over rather than instead of, deliberately. A control's fill changes the
/// instant you hover it - button.dart says so in as many words, and a
/// TextField matches it - so rebuilding the decoration with an animated
/// colour in it would have dragged the fill into the transition too and made
/// every hover ease. This sits on top and is the only thing that moves.
///
/// It also means a control with no border at all - a ghost select, say - gets
/// one here for nothing, which is what "the border goes red" needs in order
/// to mean anything.
class FieldErrorOutline extends StatelessWidget {
const FieldErrorOutline({
super.key,
required this.invalid,
required this.borderRadius,
required this.child,
});
final bool invalid;
final BorderRadiusGeometry borderRadius;
final Widget child;
@override
Widget build(BuildContext context) {
final theme = GarageTheme.of(context);
return Stack(
children: [
child,
// Positioned.fill, so it takes the child's size rather than joining in
// deciding what that size is.
Positioned.fill(
child: IgnorePointer(
child: AnimatedOpacity(
opacity: invalid ? 1.0 : 0.0,
duration: kFieldErrorDuration,
curve: kFieldErrorCurve,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: borderRadius,
border: Border.all(
color: theme.colorScheme.destructive,
width: theme.density.controlBorderWidth,
),
),
),
),
),
),
],
);
}
}