feat(ui): redesign feedback dialogs with pill style for legacy mode

This commit is contained in:
2026-04-23 02:04:33 +02:00
parent f0666d9848
commit 92c922a130
3 changed files with 202 additions and 40 deletions

View File

@@ -135,7 +135,7 @@ class _RequestCancelSection extends ConsumerWidget {
await showInfoDialog(
context,
I18n.accountDeletionHasDevices,
autoDismiss: null,
autoDismiss: const Duration(seconds: 5),
);
return;
}

View File

@@ -20,6 +20,12 @@ class SfColors extends ThemeExtension<SfColors> {
required this.cardColorSet1,
required this.cardColorSet2,
required this.disabledCardColors,
required this.successBackground,
required this.successCircle,
required this.successText,
required this.errorBackground,
required this.errorCircle,
required this.errorText,
});
/// Brand secondary color. Also mirrored in [ColorScheme.tertiary].
@@ -34,18 +40,38 @@ class SfColors extends ThemeExtension<SfColors> {
/// Gradient applied to disabled cards.
final List<Color> disabledCardColors;
/// Feedback dialog colors.
final Color successBackground;
final Color successCircle;
final Color successText;
final Color errorBackground;
final Color errorCircle;
final Color errorText;
@override
SfColors copyWith({
Color? legacyPrimary,
List<Color>? cardColorSet1,
List<Color>? cardColorSet2,
List<Color>? disabledCardColors,
Color? successBackground,
Color? successCircle,
Color? successText,
Color? errorBackground,
Color? errorCircle,
Color? errorText,
}) {
return SfColors(
legacyPrimary: legacyPrimary ?? this.legacyPrimary,
cardColorSet1: cardColorSet1 ?? this.cardColorSet1,
cardColorSet2: cardColorSet2 ?? this.cardColorSet2,
disabledCardColors: disabledCardColors ?? this.disabledCardColors,
successBackground: successBackground ?? this.successBackground,
successCircle: successCircle ?? this.successCircle,
successText: successText ?? this.successText,
errorBackground: errorBackground ?? this.errorBackground,
errorCircle: errorCircle ?? this.errorCircle,
errorText: errorText ?? this.errorText,
);
}
@@ -57,6 +83,12 @@ class SfColors extends ThemeExtension<SfColors> {
cardColorSet1: _lerpList(cardColorSet1, other.cardColorSet1, t),
cardColorSet2: _lerpList(cardColorSet2, other.cardColorSet2, t),
disabledCardColors: _lerpList(disabledCardColors, other.disabledCardColors, t),
successBackground: Color.lerp(successBackground, other.successBackground, t)!,
successCircle: Color.lerp(successCircle, other.successCircle, t)!,
successText: Color.lerp(successText, other.successText, t)!,
errorBackground: Color.lerp(errorBackground, other.errorBackground, t)!,
errorCircle: Color.lerp(errorCircle, other.errorCircle, t)!,
errorText: Color.lerp(errorText, other.errorText, t)!,
);
}
@@ -78,6 +110,12 @@ class SfColors extends ThemeExtension<SfColors> {
Color(0xFF797676),
Color(0xFF5F5A5A),
],
successBackground: Color(0xFFE8F5EC),
successCircle: Color(0xFF66D693),
successText: Color(0xFF424242),
errorBackground: Color(0xFFFDE8E8),
errorCircle: Color(0xFFE57373),
errorText: Color(0xFF424242),
);
/// Dark variant — cards kept visually consistent; brand tone lightened.
@@ -90,6 +128,12 @@ class SfColors extends ThemeExtension<SfColors> {
Color(0xFF494949),
Color(0xFF363636),
],
successBackground: Color(0xFF1B3A2A),
successCircle: Color(0xFF66D693),
successText: Color(0xFFE0E0E0),
errorBackground: Color(0xFF3A1B1B),
errorCircle: Color(0xFFE57373),
errorText: Color(0xFFE0E0E0),
);
}

View File

@@ -9,13 +9,14 @@ Future<void> showErrorDialog(
BuildContext context,
String messageKey, {
Map<String, dynamic>? args,
Duration autoDismiss = const Duration(seconds: 3),
}) {
return _showFeedbackDialog(
return _showPillFeedback(
context,
messageKey: messageKey,
args: args,
kind: _FeedbackKind.error,
autoDismiss: null,
autoDismiss: autoDismiss,
);
}
@@ -23,9 +24,9 @@ Future<void> showSuccessDialog(
BuildContext context,
String messageKey, {
Map<String, dynamic>? args,
Duration autoDismiss = const Duration(seconds: 2),
Duration autoDismiss = const Duration(seconds: 3),
}) {
return _showFeedbackDialog(
return _showPillFeedback(
context,
messageKey: messageKey,
args: args,
@@ -38,9 +39,9 @@ Future<void> showInfoDialog(
BuildContext context,
String messageKey, {
Map<String, dynamic>? args,
Duration? autoDismiss = const Duration(seconds: 2),
Duration autoDismiss = const Duration(seconds: 3),
}) {
return _showFeedbackDialog(
return _showPillFeedback(
context,
messageKey: messageKey,
args: args,
@@ -49,45 +50,162 @@ Future<void> showInfoDialog(
);
}
Future<void> _showFeedbackDialog(
Future<void> _showPillFeedback(
BuildContext context, {
required String messageKey,
required Map<String, dynamic>? args,
required _FeedbackKind kind,
required Duration? autoDismiss,
required Duration autoDismiss,
}) async {
Timer? timer;
await showDialog<void>(
context: context,
barrierDismissible: autoDismiss != null,
builder: (ctx) {
if (autoDismiss != null) {
timer = Timer(autoDismiss, () {
if (Navigator.of(ctx).canPop()) {
Navigator.of(ctx).pop();
}
});
}
final overlay = Overlay.of(context);
final resolved = context.translate(messageKey, args: args);
final resolved = ctx.translate(messageKey, args: args);
final theme = Theme.of(ctx);
final (icon, color) = switch (kind) {
_FeedbackKind.error => (Icons.error_outline, theme.colorScheme.error),
_FeedbackKind.success => (Icons.check_circle_outline, Colors.green),
_FeedbackKind.info => (Icons.info_outline, theme.colorScheme.primary),
};
final (backgroundColor, circleColor, iconData) = switch (kind) {
_FeedbackKind.success => (
const Color(0xFFE8F5EC),
const Color(0xFF66D693),
Icons.check,
),
_FeedbackKind.error => (
const Color(0xFFFDE8E8),
const Color(0xFFE57373),
Icons.close,
),
_FeedbackKind.info => (
const Color(0xFFE3F2FD),
const Color(0xFF64B5F6),
Icons.info_outline,
),
};
return AlertDialog(
icon: Icon(icon, color: color, size: 48),
content: Text(resolved, textAlign: TextAlign.center),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(ctx.translate(I18n.accept)),
),
],
);
},
late final OverlayEntry entry;
late final AnimationController controller;
controller = AnimationController(
duration: const Duration(milliseconds: 350),
vsync: overlay,
);
timer?.cancel();
final curvedAnimation = CurvedAnimation(
parent: controller,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic,
);
void dismiss() {
controller.reverse().then((_) {
entry.remove();
controller.dispose();
});
}
entry = OverlayEntry(
builder: (_) => Positioned.fill(
child: GestureDetector(
onTap: dismiss,
behavior: HitTestBehavior.translucent,
child: Center(
child: ScaleTransition(
scale: Tween<double>(begin: 0.8, end: 1.0).animate(curvedAnimation),
child: FadeTransition(
opacity: curvedAnimation,
child: Material(
color: Colors.transparent,
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.only(
left: 58,
top: 14,
bottom: 14,
right: 16,
),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(28),
boxShadow: const [
BoxShadow(
color: Colors.white,
blurRadius: 12.85,
spreadRadius: 8.91,
offset: Offset(0, 4.11),
),
],
),
child: Text(
resolved,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.grey.shade800,
),
),
),
Positioned(
left: 30,
top: 0,
bottom: 0,
child: Center(
child: Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: circleColor,
shape: BoxShape.circle,
),
child: Icon(
iconData,
color: Colors.white,
size: 38,
),
),
),
),
Positioned(
top: -8,
right: 18,
child: GestureDetector(
onTap: dismiss,
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
),
child: Icon(
Icons.close,
size: 14,
color: Colors.grey.shade500,
),
),
),
),
],
),
),
),
),
),
),
),
);
overlay.insert(entry);
controller.forward();
await Future.delayed(autoDismiss);
if (controller.isAnimating ||
controller.status == AnimationStatus.completed) {
dismiss();
}
}