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
This commit is contained in:
ImBenji
2026-09-23 18:49:21 +01:00
co-authored by Claude Opus 5.5
commit b269201919
117 changed files with 26944 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
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,
);
}
}