Files
buzz/mobile/lib/shared/widgets/modal_presentation.dart
klopez4212 f7708d0694 Polish mobile onboarding and profile setup (#8438)
Originally stacked on #8375, now merged. Rebased onto main.

Polishes the onboarding landing page, community-link entry, and recovery
sheets. Profile setup uses a consistent sheet with a larger avatar,
inline display name, and draft photo/emoji/animated editing; iOS uses
native controls and coordinated avatar/sheet transitions.

Validation: mobile analysis, full tests, and normal pre-push checks
passed after rebase. Prior iOS build and device walkthrough passed;
native validation was not repeated after this rebase. Snapshots omitted
as requested.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
2026-10-22 17:27:38 +00:00

225 lines
7.2 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../theme/theme.dart';
import 'buzz_sheet_header.dart';
import 'buzz_titled_sheet_layout.dart';
import 'concentric_sheet_surface.dart';
/// Shared motion for occasional modal UI.
///
/// The strong ease-out makes entrances respond immediately, while the shorter
/// exit keeps dismissals from feeling sluggish.
const buzzModalAnimationStyle = AnimationStyle(
curve: Cubic(0.23, 1, 0.32, 1),
duration: Duration(milliseconds: 280),
reverseCurve: Cubic(0.77, 0, 0.175, 1),
reverseDuration: Duration(milliseconds: 220),
);
/// Shows a bottom sheet with Buzz's shared motion and sheet chrome.
///
/// Sheets include the shared close control by default. On iOS, the surface
/// uses native concentric corners when available and paints a requested drag
/// handle inside the shared header so its spacing is consistent on every
/// platform.
/// Set [padBottom] to false when the content owns its bottom edge spacing.
Future<T?> showBuzzModalBottomSheet<T>({
required BuildContext context,
required WidgetBuilder builder,
String? title,
Color? backgroundColor,
String? barrierLabel,
double? elevation,
ShapeBorder? shape,
Clip? clipBehavior,
BoxConstraints? constraints,
Color? barrierColor,
bool isScrollControlled = false,
double scrollControlDisabledMaxHeightRatio = 9.0 / 16.0,
bool useRootNavigator = false,
bool isDismissible = true,
bool enableDrag = true,
bool? showDragHandle,
bool showCloseButton = true,
bool useSafeArea = false,
bool padBottom = true,
RouteSettings? routeSettings,
AnimationController? transitionAnimationController,
Offset? anchorPoint,
AnimationStyle? sheetAnimationStyle,
bool? requestFocus,
}) {
final isIos = defaultTargetPlatform == TargetPlatform.iOS;
final theme = Theme.of(context);
// Explicitly colored sheets (for example the dark Huddle drawer) own their
// complete surface treatment. Standard utility sheets use the quieter page
// canvas and promote their controls onto the base app surface.
final sheetTheme = backgroundColor == null
? utilitySurfaceThemeData(theme)
: theme;
final surfaceColor =
backgroundColor ??
sheetTheme.bottomSheetTheme.modalBackgroundColor ??
sheetTheme.bottomSheetTheme.backgroundColor ??
sheetTheme.colorScheme.surface;
final reduceMotion = MediaQuery.disableAnimationsOf(context);
return showModalBottomSheet<T>(
context: context,
builder: (sheetContext) => Theme(
data: sheetTheme,
child: Builder(
builder: (themedContext) => ConcentricSheetSurface(
enabled: isIos,
color: surfaceColor,
child: _SheetContent(
title: title,
showCloseButton: showCloseButton,
showDragHandle: showDragHandle == true,
surfaceColor: surfaceColor,
// Keep the entire scroll viewport above gesture navigation. The
// inner builder sees a consumed bottom inset, so sheet-local safe
// areas and scroll padding do not reserve that system inset twice.
child: SafeArea(
top: false,
left: false,
right: false,
bottom: padBottom,
child: Padding(
padding: EdgeInsets.only(bottom: padBottom ? Grid.half : 0),
child: Builder(builder: builder),
),
),
),
),
),
),
backgroundColor: isIos || title != null ? Colors.transparent : surfaceColor,
barrierLabel: barrierLabel,
elevation: elevation,
shape: shape,
clipBehavior: clipBehavior,
constraints: constraints,
barrierColor: barrierColor,
isScrollControlled: isScrollControlled,
scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
useRootNavigator: useRootNavigator,
isDismissible: isDismissible,
enableDrag: enableDrag,
// The shared header owns the handle so Android does not reserve a second
// handle band above the title row and iOS keeps it inside its native inset.
showDragHandle: false,
useSafeArea: useSafeArea,
routeSettings: routeSettings,
transitionAnimationController: transitionAnimationController,
anchorPoint: anchorPoint,
sheetAnimationStyle: reduceMotion
? AnimationStyle.noAnimation
: (sheetAnimationStyle ?? buzzModalAnimationStyle),
requestFocus: requestFocus,
);
}
class _SheetContent extends StatelessWidget {
const _SheetContent({
required this.child,
required this.title,
required this.showCloseButton,
required this.showDragHandle,
required this.surfaceColor,
});
final Widget child;
final String? title;
final bool showCloseButton;
final bool showDragHandle;
final Color surfaceColor;
@override
Widget build(BuildContext context) {
if (title == null || !showCloseButton) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (showCloseButton)
BuzzSheetHeader(title: title, showDragHandle: showDragHandle)
else if (showDragHandle)
const Padding(
padding: EdgeInsets.only(top: Grid.xxs, bottom: Grid.xs),
child: _StandaloneSheetDragHandle(),
),
Flexible(child: child),
],
);
}
return BuzzTitledSheetLayout(
title: title!,
showDragHandle: showDragHandle,
surfaceColor: surfaceColor,
child: child,
);
}
}
class _StandaloneSheetDragHandle extends StatelessWidget {
const _StandaloneSheetDragHandle();
@override
Widget build(BuildContext context) {
return Semantics(
label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
container: true,
button: true,
onTap: () => Navigator.of(context).pop(),
child: Container(
key: const ValueKey('buzz-sheet-drag-handle'),
width: 32,
height: 4,
decoration: BoxDecoration(
color: context.colors.onSurfaceVariant.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(Radii.full),
),
),
);
}
}
/// Shows a dialog with Buzz's shared motion, respecting reduced-motion settings.
Future<T?> showBuzzDialog<T>({
required BuildContext context,
required WidgetBuilder builder,
bool barrierDismissible = true,
Color? barrierColor,
String? barrierLabel,
bool useSafeArea = true,
bool useRootNavigator = true,
RouteSettings? routeSettings,
Offset? anchorPoint,
TraversalEdgeBehavior? traversalEdgeBehavior,
bool fullscreenDialog = false,
bool? requestFocus,
AnimationStyle? animationStyle,
}) {
final reduceMotion = MediaQuery.disableAnimationsOf(context);
return showDialog<T>(
context: context,
builder: builder,
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
barrierLabel: barrierLabel,
useSafeArea: useSafeArea,
useRootNavigator: useRootNavigator,
routeSettings: routeSettings,
anchorPoint: anchorPoint,
traversalEdgeBehavior: traversalEdgeBehavior,
fullscreenDialog: fullscreenDialog,
requestFocus: requestFocus,
animationStyle: reduceMotion
? AnimationStyle.noAnimation
: (animationStyle ?? buzzModalAnimationStyle),
);
}