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
146 lines
5.0 KiB
Dart
146 lines
5.0 KiB
Dart
import "package:flutter/widgets.dart";
|
|
|
|
// our own theme now - we only need it for the icon size theme (iconTheme.small etc).
|
|
import "package:garage_ui/theme/garage_theme.dart";
|
|
|
|
// Hand rolled replacements for the shadcn Widget/Icon extension methods.
|
|
// These get used 350+ times across the app so behaviour has to match the
|
|
// original exactly — same SizedBox/Padding/Center/Expanded wrappers, same
|
|
// icon sizes pulled from the theme.
|
|
|
|
/// Layout helpers that shadcn hangs off every Widget.
|
|
extension WidgetExtension on Widget {
|
|
/// Wraps this widget in a [SizedBox]. If it's already a SizedBox we merge
|
|
/// the dims instead of double wrapping (matches shadcn).
|
|
Widget sized({double? width, double? height, double? size}) {
|
|
if (this is SizedBox) {
|
|
return SizedBox(
|
|
width: width ?? size ?? (this as SizedBox).width,
|
|
height: height ?? size ?? (this as SizedBox).height,
|
|
child: (this as SizedBox).child,
|
|
);
|
|
}
|
|
return SizedBox(width: width ?? size, height: height ?? size, child: this);
|
|
}
|
|
|
|
/// Wraps this widget in [Padding]. You can pass individual edges, the
|
|
/// combined horizontal/vertical, a uniform `all`, or a raw EdgeInsets via
|
|
/// `padding` (which wins over everything else).
|
|
Widget withPadding({
|
|
double? top,
|
|
double? bottom,
|
|
double? left,
|
|
double? right,
|
|
double? horizontal,
|
|
double? vertical,
|
|
double? all,
|
|
EdgeInsetsGeometry? padding,
|
|
}) {
|
|
assert(() {
|
|
if (all != null) {
|
|
if (top != null ||
|
|
bottom != null ||
|
|
left != null ||
|
|
right != null ||
|
|
horizontal != null ||
|
|
vertical != null) {
|
|
throw FlutterError(
|
|
"All padding properties cannot be used with other padding properties.",
|
|
);
|
|
}
|
|
} else if (horizontal != null) {
|
|
if (left != null || right != null) {
|
|
throw FlutterError(
|
|
"Horizontal padding cannot be used with left or right padding.",
|
|
);
|
|
}
|
|
} else if (vertical != null) {
|
|
if (top != null || bottom != null) {
|
|
throw FlutterError(
|
|
"Vertical padding cannot be used with top or bottom padding.",
|
|
);
|
|
}
|
|
}
|
|
return true;
|
|
}());
|
|
|
|
var edgeInsets = EdgeInsets.only(
|
|
top: top ?? vertical ?? all ?? 0,
|
|
bottom: bottom ?? vertical ?? all ?? 0,
|
|
left: left ?? horizontal ?? all ?? 0,
|
|
right: right ?? horizontal ?? all ?? 0,
|
|
);
|
|
return Padding(padding: padding ?? edgeInsets, child: this);
|
|
}
|
|
|
|
/// Centers the widget in its parent.
|
|
Widget center({Key? key}) {
|
|
return Center(key: key, child: this);
|
|
}
|
|
|
|
/// Makes this widget [Expanded] inside a Row/Column.
|
|
Widget expanded({int flex = 1}) {
|
|
return Expanded(flex: flex, child: this);
|
|
}
|
|
}
|
|
|
|
/// Icon size helpers. shadcn returns a WrappedIcon that reads the size off the
|
|
/// theme (theme.iconTheme.small etc) so it respects AdaptiveScaling — we do the
|
|
/// exact same thing rather than hardcoding, otherwise scaled builds drift.
|
|
extension IconExtension on Widget {
|
|
/// small icon — 16px at scale 1.
|
|
Widget get iconSmall {
|
|
return _GarageWrappedIcon(pick: (t) => t.iconTheme.small, child: this);
|
|
}
|
|
|
|
/// medium icon — 20px at scale 1.
|
|
Widget get iconMedium {
|
|
return _GarageWrappedIcon(pick: (t) => t.iconTheme.medium, child: this);
|
|
}
|
|
|
|
/// large icon — 24px at scale 1.
|
|
Widget get iconLarge {
|
|
return _GarageWrappedIcon(pick: (t) => t.iconTheme.large, child: this);
|
|
}
|
|
}
|
|
|
|
typedef _IconThemePicker = IconThemeData Function(ThemeData theme);
|
|
|
|
// mirrors shadcn's WrappedIcon — but merges the SIZE ONLY.
|
|
//
|
|
// The tier's IconThemeData carries the global scheme foreground alongside its
|
|
// size, and merging that whole thing overrode whatever IconTheme already
|
|
// enclosed the icon. Inside a Button that meant the button's own variant
|
|
// colour lost to the global one: a primary button hands its leading icon
|
|
// primaryForeground, and `.iconSmall` put scheme.foreground back over the top.
|
|
// On a dark scheme both of those are near-white while the primary FILL is too,
|
|
// so the icon painted white on white and simply vanished.
|
|
//
|
|
// Size is the only thing these helpers are for. Colour is the enclosing
|
|
// theme's business - a button's, or the app's for an icon standing on its own,
|
|
// which is the same value the tier was supplying anyway.
|
|
class _GarageWrappedIcon extends StatelessWidget {
|
|
final _IconThemePicker pick;
|
|
final Widget child;
|
|
|
|
const _GarageWrappedIcon({required this.pick, required this.child});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
double? size;
|
|
try {
|
|
size = pick(GarageTheme.of(context)).size;
|
|
} catch (e, st) {
|
|
// shouldnt happen (there's always a Theme above us) but if it does we
|
|
// still want a visible icon rather than a crash. log it so it's debuggable.
|
|
debugPrint("ana iconTheme lookup failed: $e");
|
|
debugPrintStack(stackTrace: st);
|
|
size = 16;
|
|
}
|
|
return IconTheme.merge(
|
|
data: IconThemeData(size: size),
|
|
child: child,
|
|
);
|
|
}
|
|
}
|