feat(ui): add shared app snackbar styling
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Semantic tones for lightweight, Material 3 SnackBars.
|
||||
enum AppSnackBarVariant { neutral, success, warning, error }
|
||||
|
||||
/// Shared SnackBar styling used across the production app.
|
||||
class AppSnackBar {
|
||||
const AppSnackBar._();
|
||||
|
||||
static const double _desktopMaxWidth = 560;
|
||||
static const double _radius = 16;
|
||||
static const double _elevation = 3;
|
||||
|
||||
/// Theme-level defaults for uncustomized SnackBars.
|
||||
static SnackBarThemeData theme(ColorScheme scheme) {
|
||||
return SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
elevation: _elevation,
|
||||
backgroundColor: scheme.surfaceContainerHigh,
|
||||
contentTextStyle: TextStyle(color: scheme.onSurface),
|
||||
actionTextColor: scheme.primary,
|
||||
disabledActionTextColor: scheme.onSurfaceVariant,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(_radius)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build a consistently light, floating SnackBar while preserving caller
|
||||
/// controlled behavior such as duration, margin, action, and dismissal.
|
||||
static SnackBar build({
|
||||
required BuildContext context,
|
||||
required Widget content,
|
||||
AppSnackBarVariant variant = AppSnackBarVariant.neutral,
|
||||
SnackBarAction? action,
|
||||
Duration? duration,
|
||||
EdgeInsetsGeometry? margin,
|
||||
DismissDirection dismissDirection = DismissDirection.down,
|
||||
}) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final viewWidth = MediaQuery.sizeOf(context).width;
|
||||
final useDesktopCap = viewWidth >= 600;
|
||||
final snackBarWidth = useDesktopCap && margin == null
|
||||
? _desktopMaxWidth
|
||||
: null;
|
||||
final effectiveMargin = useDesktopCap && margin != null
|
||||
? _desktopCappedMargin(context, margin)
|
||||
: margin;
|
||||
final foreground = _foregroundColor(scheme, variant);
|
||||
return SnackBar(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
width: snackBarWidth,
|
||||
margin: effectiveMargin,
|
||||
elevation: _elevation,
|
||||
backgroundColor: _backgroundColor(scheme, variant),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(_radius)),
|
||||
),
|
||||
dismissDirection: dismissDirection,
|
||||
duration: duration ?? const Duration(milliseconds: 4000),
|
||||
content: DefaultTextStyle.merge(
|
||||
style: TextStyle(color: foreground),
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(color: foreground),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
action: action,
|
||||
);
|
||||
}
|
||||
|
||||
static EdgeInsetsGeometry _desktopCappedMargin(
|
||||
BuildContext context,
|
||||
EdgeInsetsGeometry margin,
|
||||
) {
|
||||
final viewWidth = MediaQuery.sizeOf(context).width;
|
||||
final resolved = margin.resolve(Directionality.of(context));
|
||||
final extraHorizontal =
|
||||
(viewWidth - _desktopMaxWidth).clamp(0.0, viewWidth) / 2;
|
||||
return EdgeInsets.fromLTRB(
|
||||
resolved.left + extraHorizontal,
|
||||
resolved.top,
|
||||
resolved.right + extraHorizontal,
|
||||
resolved.bottom,
|
||||
);
|
||||
}
|
||||
|
||||
static SnackBar text({
|
||||
required BuildContext context,
|
||||
required String message,
|
||||
AppSnackBarVariant variant = AppSnackBarVariant.neutral,
|
||||
SnackBarAction? action,
|
||||
Duration? duration,
|
||||
EdgeInsetsGeometry? margin,
|
||||
DismissDirection dismissDirection = DismissDirection.down,
|
||||
int? maxLines,
|
||||
TextOverflow? overflow,
|
||||
}) {
|
||||
return build(
|
||||
context: context,
|
||||
variant: variant,
|
||||
action: action,
|
||||
duration: duration,
|
||||
margin: margin,
|
||||
dismissDirection: dismissDirection,
|
||||
content: Text(message, maxLines: maxLines, overflow: overflow),
|
||||
);
|
||||
}
|
||||
|
||||
static Color _backgroundColor(
|
||||
ColorScheme scheme,
|
||||
AppSnackBarVariant variant,
|
||||
) {
|
||||
return switch (variant) {
|
||||
AppSnackBarVariant.neutral => scheme.surfaceContainerHigh,
|
||||
AppSnackBarVariant.success => scheme.tertiaryContainer,
|
||||
AppSnackBarVariant.warning => scheme.secondaryContainer,
|
||||
AppSnackBarVariant.error => scheme.errorContainer,
|
||||
};
|
||||
}
|
||||
|
||||
static Color _foregroundColor(
|
||||
ColorScheme scheme,
|
||||
AppSnackBarVariant variant,
|
||||
) {
|
||||
return switch (variant) {
|
||||
AppSnackBarVariant.neutral => scheme.onSurface,
|
||||
AppSnackBarVariant.success => scheme.onTertiaryContainer,
|
||||
AppSnackBarVariant.warning => scheme.onSecondaryContainer,
|
||||
AppSnackBarVariant.error => scheme.onErrorContainer,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user