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
93 lines
3.2 KiB
Dart
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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|