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
90 lines
3.2 KiB
Dart
90 lines
3.2 KiB
Dart
import "dart:ui" show ImageFilter;
|
|
|
|
import "package:flutter/widgets.dart";
|
|
|
|
import "package:garage_ui/theme/garage_theme.dart";
|
|
|
|
/// The entrance a Garage pane dialog makes: blurred backdrop, dimmed scrim,
|
|
/// fade and scale in, width clamped to the viewport.
|
|
///
|
|
/// This lived twice in Arcs & Angles - once for the titled pane and once for
|
|
/// the Blender-language one - identical apart from the shell widget wrapped
|
|
/// around the content. The presentation is the generic half, so it lives here
|
|
/// and [builder] supplies the shell.
|
|
///
|
|
/// [builder] is handed an `onClose` that pops the route, or null when
|
|
/// [dismissible] is false, so the shell can wire its own close affordance.
|
|
/// Content pops with its own result the usual way.
|
|
Future<T?> showPaneOverlay<T>({
|
|
required BuildContext context,
|
|
required Widget Function(BuildContext context, VoidCallback? onClose) builder,
|
|
double maxWidth = 450,
|
|
bool dismissible = true,
|
|
String? barrierLabel,
|
|
Duration duration = const Duration(milliseconds: 180),
|
|
double blurSigma = 3,
|
|
double scrimOpacity = 0.35,
|
|
double enterScale = 0.93,
|
|
double viewportInset = 24,
|
|
}) {
|
|
// read before the route is pushed so it reflects the theme at the call site
|
|
// rather than whatever sits above the navigator.
|
|
final scrim = GarageTheme.of(context).colorScheme.background;
|
|
|
|
return showGeneralDialog<T>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
barrierColor: const Color(0x00000000),
|
|
barrierLabel: barrierLabel,
|
|
transitionDuration: duration,
|
|
pageBuilder: (ctx, anim, secondaryAnim) => const SizedBox.shrink(),
|
|
transitionBuilder: (ctx, anim, secondaryAnim, _) {
|
|
final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut);
|
|
final scale = Tween<double>(
|
|
begin: enterScale,
|
|
end: 1.0,
|
|
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
|
|
|
|
final screenW = MediaQuery.sizeOf(ctx).width;
|
|
final maxW = (screenW - viewportInset).clamp(0.0, maxWidth);
|
|
|
|
void close() => Navigator.of(ctx).pop();
|
|
|
|
return Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: FadeTransition(
|
|
opacity: fade,
|
|
child: BackdropFilter(
|
|
filter: ImageFilter.blur(sigmaX: blurSigma, sigmaY: blurSigma),
|
|
child: GestureDetector(
|
|
// a dismiss scrim, not a control - Escape and the dialog's
|
|
// own close button are the reachable ways out. Left in the
|
|
// tree it reads as a full-screen unnamed button.
|
|
excludeFromSemantics: true,
|
|
onTap: dismissible ? close : null,
|
|
child: ColoredBox(
|
|
color: scrim.withValues(alpha: scrimOpacity),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Center(
|
|
child: FadeTransition(
|
|
opacity: fade,
|
|
child: ScaleTransition(
|
|
scale: scale,
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(minWidth: maxW, maxWidth: maxW),
|
|
child: builder(ctx, dismissible ? close : null),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|