Shuffle/app/lib/core/widgets/app_banner.dart
cschlaefke ec8b2ab4c1 feat(ui): add auth form components and screen layouts
Refs #1

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 00:03:11 +01:00

94 lines
2.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:shuffle/core/theme/app_spacing.dart';
enum AppBannerType { info, error }
/// Inline message banner (info / error) above forms.
///
/// Type is conveyed by icon + [typeLabel] (spoken prefix, e.g. "Error"),
/// never by color alone. The message is a live region, so screen readers
/// announce it when it appears. The optional action is a separate 48 dp text
/// button. All texts come from the caller (l10n).
class AppBanner extends StatelessWidget {
const AppBanner({
required this.type,
required this.message,
required this.typeLabel,
super.key,
this.actionLabel,
this.onAction,
}) : assert(
(actionLabel == null) == (onAction == null),
'actionLabel and onAction belong together',
);
final AppBannerType type;
final String message;
final String typeLabel;
final String? actionLabel;
final VoidCallback? onAction;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scheme = theme.colorScheme;
final isError = type == AppBannerType.error;
final background = isError
? scheme.errorContainer
: scheme.secondaryContainer;
final foreground = isError
? scheme.onErrorContainer
: scheme.onSecondaryContainer;
return DecoratedBox(
decoration: BoxDecoration(
color: background,
borderRadius: AppRadius.mdAll,
border: Border.all(color: foreground.withValues(alpha: 0.5)),
),
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Semantics(
container: true,
liveRegion: true,
label: '$typeLabel: $message',
child: ExcludeSemantics(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
isError ? Icons.error_outline : Icons.info_outline,
color: foreground,
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Text(
message,
style: theme.textTheme.bodyMedium?.copyWith(
color: foreground,
),
),
),
],
),
),
),
if (actionLabel != null)
Align(
alignment: AlignmentDirectional.centerEnd,
child: TextButton(
onPressed: onAction,
style: TextButton.styleFrom(foregroundColor: foreground),
child: Text(actionLabel!),
),
),
],
),
),
);
}
}