Files
buzz/mobile/lib/shared/widgets/toast_capsule_content.dart
klopez4212 99873459e9 Keep sign-in toasts usable at large text sizes (#9057)
At large text sizes, organization sign-in prompts could clip their
message and push recovery actions off-screen. Stack actions below the
message above 1.3× text, use fixed card corners, and skip the Dynamic
Island morph for that layout.

Follow-up to #9050, addressing [Wes’s large-text
blocker](https://github.com/block/buzz/pull/9050#discussion_r4469530660).
Normal-size styling and recovery callbacks are unchanged.

Validation: extended existing sign-in/cancel/error journeys at 1×, 2×
and 3× using Inter on a 320×568 viewport; assert text lies inside the
painted clip and the card stays on-screen. Removing the fix fails all
six enlarged-text cases. Mobile checks and full tests run through the
push hook. Human device testing of this follow-up is pending.

Local Flutter previews at 3× text (320×568):

| Before | After |
| --- | --- |
| ![Before: clipped text and action extending off
screen](https://github.com/user-attachments/assets/89b62c46-173c-4a1e-960a-f402f6d0346a)
| ![After: readable message above reachable Sign in
action](https://github.com/user-attachments/assets/ae3de975-3505-48ee-b9f3-b60aa710600f)
|

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-11-04 20:33:08 +00:00

120 lines
3.4 KiB
Dart

import 'package:flutter/material.dart';
import '../theme/theme.dart';
/// Shared text and inset action pill for temporary and persistent feedback.
class ToastCapsuleContent extends StatelessWidget {
const ToastCapsuleContent({
super.key,
required this.message,
required this.foreground,
this.actionLabel = 'Retry',
this.destructiveAction = false,
this.secondaryAction,
this.onAction,
this.stackActions = false,
});
/// Places actions below the message when enlarged text needs more width.
final bool stackActions;
final String message;
final Color foreground;
final String actionLabel;
/// Uses the theme error colors for the primary action.
final bool destructiveAction;
/// Adds a second recovery action and stacks both buttons below the message.
final ({String label, VoidCallback onPressed})? secondaryAction;
final VoidCallback? onAction;
@override
Widget build(BuildContext context) {
final text = Padding(
padding: const EdgeInsets.symmetric(
horizontal: Grid.gutter,
vertical: Grid.twelve,
),
child: Text(
message,
textAlign: TextAlign.center,
style: context.textTheme.bodyMedium?.copyWith(
color: foreground,
fontWeight: FontWeight.w600,
),
),
);
if (onAction == null) return text;
Widget action(
String label,
VoidCallback onPressed, {
bool destructive = false,
bool secondary = false,
}) => FilledButton(
onPressed: onPressed,
style: FilledButton.styleFrom(
backgroundColor: destructive
? lightColorScheme.error
: secondary
? darkColorScheme.secondaryContainer
: Colors.white,
foregroundColor: destructive
? lightColorScheme.onError
: secondary
? darkColorScheme.secondary
: Colors.black,
shape: const RoundedSuperellipseBorder(
borderRadius: BorderRadius.all(Radius.circular(999)),
),
minimumSize: const Size(48, 36),
padding: const EdgeInsets.symmetric(horizontal: Grid.gutter),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(label),
);
final secondary = secondaryAction;
if (secondary != null || stackActions) {
return Padding(
padding: const EdgeInsets.fromLTRB(
Grid.twelve,
6,
Grid.twelve,
Grid.twelve,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
text,
action(actionLabel, onAction!, destructive: destructiveAction),
if (secondary != null) ...[
const SizedBox(height: 6),
action(secondary.label, secondary.onPressed, secondary: true),
],
],
),
);
}
return IntrinsicWidth(
child: IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: Center(widthFactor: 1, child: text)),
Padding(
padding: const EdgeInsets.only(top: 6, right: 6, bottom: 6),
child: action(
actionLabel,
onAction!,
destructive: destructiveAction,
),
),
],
),
),
);
}
}