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
170 lines
5.2 KiB
Dart
170 lines
5.2 KiB
Dart
import "dart:ui" show Color, ImageFilter;
|
|
|
|
import "package:flutter/painting.dart" show HSLColor;
|
|
import "package:flutter/widgets.dart";
|
|
|
|
import "package:garage_ui/theme/garage_theme.dart";
|
|
|
|
// small pile of helpers the GarageUI widgets used to pull off the shadcn barrel.
|
|
// theyre generic, nothing shadcn specific, so we just keep our own copies.
|
|
|
|
// shadcns Color helpers that the widgets lean on.
|
|
extension ColorExtension on Color {
|
|
// scale the alpha channel by [factor] (0..1). shadcns scaleAlpha.
|
|
Color scaleAlpha(double factor) {
|
|
return withValues(alpha: (a * factor).clamp(0.0, 1.0));
|
|
}
|
|
|
|
// a readable foreground for this colour - flips lightness. shadcns getContrastColor.
|
|
Color getContrastColor([double luminanceContrast = 1]) {
|
|
final hsl = HSLColor.fromColor(this);
|
|
final l = hsl.lightness;
|
|
final target = l >= 0.5
|
|
? l - (l * luminanceContrast)
|
|
: l + ((1 - l) * luminanceContrast);
|
|
return hsl.withLightness(target.clamp(0.0, 1.0)).toColor();
|
|
}
|
|
}
|
|
|
|
// default anim duration shadcn used all over (hover/colour transitions etc).
|
|
const Duration kDefaultDuration = Duration(milliseconds: 150);
|
|
|
|
// widget value wins, then theme value, then the hard default. shadcns styleValue.
|
|
T styleValue<T>({T? widgetValue, T? themeValue, required T defaultValue}) {
|
|
return widgetValue ?? themeValue ?? defaultValue;
|
|
}
|
|
|
|
// we dropped shadcns density-insets system, so this is now just a passthrough -
|
|
// the GarageUI widgets only ever hand it plain EdgeInsets anyway.
|
|
EdgeInsetsGeometry resolveEdgeInsets(
|
|
EdgeInsetsGeometry padding,
|
|
double basePadding,
|
|
) {
|
|
return padding;
|
|
}
|
|
|
|
// shrinks a border radius by the border width so an inner surface tucks neatly
|
|
// inside its border. shadcns subtractByBorder.
|
|
BorderRadius subtractByBorder(BorderRadius radius, double borderWidth) {
|
|
Radius sub(Radius r) => Radius.elliptical(
|
|
(r.x - borderWidth).clamp(0.0, double.infinity),
|
|
(r.y - borderWidth).clamp(0.0, double.infinity),
|
|
);
|
|
return BorderRadius.only(
|
|
topLeft: sub(radius.topLeft),
|
|
topRight: sub(radius.topRight),
|
|
bottomLeft: sub(radius.bottomLeft),
|
|
bottomRight: sub(radius.bottomRight),
|
|
);
|
|
}
|
|
|
|
// used to be a density-aware Padding in shadcn. our padding is already resolved
|
|
// by the time it gets here, so this is just a Padding.
|
|
class DensityContainerPadding extends StatelessWidget {
|
|
const DensityContainerPadding({
|
|
super.key,
|
|
required this.padding,
|
|
required this.child,
|
|
});
|
|
|
|
final EdgeInsetsGeometry padding;
|
|
final Widget child;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Padding(padding: padding, child: child);
|
|
}
|
|
|
|
// shadcn used this to tell whether a surface was being rendered inside a sheet
|
|
// so it could drop its own rounding/border. the app doesnt use sheets, so its
|
|
// always false.
|
|
class SheetOverlayHandler {
|
|
const SheetOverlayHandler._();
|
|
static bool isSheetOverlay(BuildContext context) => false;
|
|
}
|
|
|
|
// draws a focus ring around a child when [focused]. shadcns FocusOutline, cut
|
|
// down to what the text field needs.
|
|
class FocusOutline extends StatelessWidget {
|
|
const FocusOutline({
|
|
super.key,
|
|
required this.child,
|
|
required this.focused,
|
|
this.borderRadius,
|
|
this.color,
|
|
this.width = 1.0,
|
|
});
|
|
|
|
final Widget child;
|
|
final bool focused;
|
|
final BorderRadiusGeometry? borderRadius;
|
|
final Color? color;
|
|
final double width;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!focused) return child;
|
|
final ring = color ?? GarageTheme.of(context).colorScheme.ring;
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: borderRadius,
|
|
// draw the ring OUTSIDE the box so it doesnt inset the child and
|
|
// change the field size when focus comes and goes.
|
|
border: Border.all(
|
|
color: ring,
|
|
width: width,
|
|
strokeAlign: BorderSide.strokeAlignOutside,
|
|
),
|
|
),
|
|
child: child,
|
|
);
|
|
}
|
|
}
|
|
|
|
// blurs whatever is behind the child (glassmorphism on popovers/cards). ported
|
|
// straight from shadcns outlined_container.dart.
|
|
class SurfaceBlur extends StatefulWidget {
|
|
const SurfaceBlur({
|
|
super.key,
|
|
required this.child,
|
|
this.surfaceBlur,
|
|
this.borderRadius,
|
|
});
|
|
|
|
final Widget child;
|
|
final double? surfaceBlur;
|
|
final BorderRadiusGeometry? borderRadius;
|
|
|
|
@override
|
|
State<SurfaceBlur> createState() => _SurfaceBlurState();
|
|
}
|
|
|
|
class _SurfaceBlurState extends State<SurfaceBlur> {
|
|
final GlobalKey _mainContainerKey = GlobalKey();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (widget.surfaceBlur == null || widget.surfaceBlur! <= 0) {
|
|
return KeyedSubtree(key: _mainContainerKey, child: widget.child);
|
|
}
|
|
return Stack(
|
|
fit: StackFit.passthrough,
|
|
children: [
|
|
Positioned.fill(
|
|
child: ClipRRect(
|
|
borderRadius: widget.borderRadius ?? BorderRadius.zero,
|
|
child: BackdropFilter(
|
|
filter: ImageFilter.blur(
|
|
sigmaX: widget.surfaceBlur!,
|
|
sigmaY: widget.surfaceBlur!,
|
|
),
|
|
// needs a child or it wont actually blur anything
|
|
child: const SizedBox(),
|
|
),
|
|
),
|
|
),
|
|
KeyedSubtree(key: _mainContainerKey, child: widget.child),
|
|
],
|
|
);
|
|
}
|
|
}
|