Files
Garage-SDKs/garage_ui/lib/theme/support.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

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),
],
);
}
}