diff --git a/example/lib/home.dart b/example/lib/home.dart index d09cdcca..d1293377 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -15,6 +15,7 @@ import 'package:zeta_example/pages/components/list_item_example.dart'; import 'package:zeta_example/pages/components/navigation_bar_example.dart'; import 'package:zeta_example/pages/components/radio_example.dart'; import 'package:zeta_example/pages/components/switch_example.dart'; +import 'package:zeta_example/pages/components/snackbar_example.dart'; import 'package:zeta_example/pages/theme/color_example.dart'; import 'package:zeta_example/pages/components/password_input_example.dart'; import 'package:zeta_example/pages/components/progress_example.dart'; @@ -47,6 +48,7 @@ final List components = [ Component(PasswordInputExample.name, (context) => const PasswordInputExample()), Component(DropdownExample.name, (context) => const DropdownExample()), Component(ProgressExample.name, (context) => const ProgressExample()), + Component(SnackBarExample.name, (context) => const SnackBarExample()), Component(DialPadExample.name, (context) => const DialPadExample()), Component(RadioButtonExample.name, (context) => const RadioButtonExample()), Component(SwitchExample.name, (context) => const SwitchExample()), diff --git a/example/lib/pages/components/snackbar_example.dart b/example/lib/pages/components/snackbar_example.dart new file mode 100644 index 00000000..b07ab311 --- /dev/null +++ b/example/lib/pages/components/snackbar_example.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class SnackBarExample extends StatelessWidget { + static const String name = 'SnackBar'; + + const SnackBarExample({super.key}); + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: SnackBarExample.name, + child: SingleChildScrollView( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + // Standard Rounded + Row( + children: [ + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Standard Rounded SnackBar", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + actionLabel: "Action", + content: Text('This is a snackbar'), + ), + ); + }, + ), + ), + ], + ), + + // Standard Sharp + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Standard Sharp SnackBar", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + actionLabel: "Action", + rounded: false, + content: Text('This is a snackbar'), + ), + ); + }, + ), + ), + + // Default + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Contectual Default", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.defaultType, + leadingIcon: Icon(Icons.mood_rounded), + content: Text('Message with icon'), + ), + ); + }, + ), + ), + + // Action + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Action", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.action, + onPressed: () {}, + actionLabel: "Action", + content: Text('Actionable message with icon'), + ), + ); + }, + ), + ), + + // Positive + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Positive", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.positive, + content: Text('Request sent successfully'), + ), + ); + }, + ), + ), + + // Info + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Info", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.info, + content: Text('Information is being displayed'), + ), + ); + }, + ), + ), + + // Info + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Info", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.info, + content: Text('Information is being displayed'), + ), + ); + }, + ), + ), + + // Warning + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Warning", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.warning, + content: Text('Warning has been issued'), + ), + ); + }, + ), + ), + + // Error + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Error", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.error, + content: Text('Error has been detected'), + ), + ); + }, + ), + ), + + // Deletion + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Deletion", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.deletion, + onPressed: () {}, + content: Text('Item was deleted'), + ), + ); + }, + ), + ), + + // View + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "View", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.view, + onPressed: () {}, + content: Text('Something neeeds your attention'), + ), + ); + }, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/example/widgetbook/main.dart b/example/widgetbook/main.dart index d8f71422..f706613f 100644 --- a/example/widgetbook/main.dart +++ b/example/widgetbook/main.dart @@ -21,6 +21,7 @@ import 'pages/components/password_input_widgetbook.dart'; import 'pages/components/progress_widgetbook.dart'; import 'pages/components/radio_widgetbook.dart'; import 'pages/components/switch_widgetbook.dart'; +import 'pages/components/snack_bar_widgetbook.dart'; import 'pages/theme/color_widgetbook.dart'; import 'pages/theme/radius_widgetbook.dart'; import 'pages/theme/spacing_widgetbook.dart'; @@ -92,6 +93,10 @@ class HotReload extends StatelessWidget { ), WidgetbookUseCase(name: 'Radio Button', builder: (context) => radioButtonUseCase(context)), WidgetbookUseCase(name: 'Switch', builder: (context) => switchUseCase(context)), + WidgetbookUseCase( + name: 'Snack Bar', + builder: (context) => snackBarUseCase(context), + ), ]..sort((a, b) => a.name.compareTo(b.name)), ), WidgetbookCategory( diff --git a/example/widgetbook/pages/components/snack_bar_widgetbook.dart b/example/widgetbook/pages/components/snack_bar_widgetbook.dart new file mode 100644 index 00000000..7f3deeba --- /dev/null +++ b/example/widgetbook/pages/components/snack_bar_widgetbook.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; +import '../../utils/utils.dart'; + +Widget snackBarUseCase(BuildContext context) { + return WidgetbookTestWidget( + widget: Builder( + builder: (context) { + final text = context.knobs.string( + label: 'Content Text', + initialValue: 'This is a snackbar', + ); + + final actionLabel = context.knobs.stringOrNull( + label: 'Action Label', + initialValue: null, + ); + + final type = context.knobs.listOrNull( + label: 'Type', + options: [null, ...ZetaSnackBarType.values], + labelBuilder: (type) => type?.name ?? '', + ); + + final leadingIcon = iconKnob( + context, + name: "Leading Icon", + initial: Icons.mood_rounded, + nullable: true, + ); + + final rounded = context.knobs.boolean(label: 'Rounded', initialValue: true); + + return ZetaButton.primary( + label: "Show Snackbar", + onPressed: () { + print(actionLabel); + final snackBar = ZetaSnackBar( + context: context, + onPressed: () {}, + actionLabel: actionLabel, + type: type, + leadingIcon: leadingIcon != null ? Icon(leadingIcon) : null, + rounded: rounded, + content: Text(text), + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }); + }, + ), + ); +} diff --git a/lib/src/components/snack_bar/snack_bar.dart b/lib/src/components/snack_bar/snack_bar.dart new file mode 100644 index 00000000..e5250583 --- /dev/null +++ b/lib/src/components/snack_bar/snack_bar.dart @@ -0,0 +1,350 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zeta_flutter.dart'; + +/// Type used to define contextual [SnackBar]. The type defines the styles, icons and behavior. +enum ZetaSnackBarType { + /// Default colors with leading icon with close action. + defaultType, + + /// Default colors with leading icon and custom action. + action, + + /// Success styles with close action. + positive, + + /// Info styles with close action. + info, + + /// Warning styles with close action. + warning, + + /// Error styles with close action. + error, + + /// Deletion styles with custom undo action. + deletion, + + /// View styles with custom view action. + view, +} + +/// A lightweight message with an optional action which briefly displays at the +/// bottom of the screen. +/// +/// Different styles can be applied to [ZetaSnackBar] with [ZetaSnackBarType]. +class ZetaSnackBar extends SnackBar { + /// Sets basic styles for the [SnackBar]. + ZetaSnackBar({ + required BuildContext context, + required Widget content, + VoidCallback? onPressed, + ZetaSnackBarType? type, + Icon? leadingIcon, + bool rounded = true, + String? actionLabel, + String deleteActionLabel = 'Undo', + String viewActionLabel = 'View', + super.margin, + super.behavior = SnackBarBehavior.floating, + super.key, + }) : super( + elevation: 0, + padding: EdgeInsets.zero, + backgroundColor: _getBackgroundColorForType(context, type), + shape: RoundedRectangleBorder( + borderRadius: type != null + ? ZetaRadius.full + : rounded + ? ZetaRadius.minimal + : ZetaRadius.none, + ), + content: Padding( + padding: const EdgeInsets.symmetric(vertical: ZetaSpacing.xs), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + _LeadingIcon(type, leadingIcon), + Flexible( + child: Padding( + padding: const EdgeInsets.only(left: ZetaSpacing.s), + child: _Content(type: type, child: content), + ), + ), + ], + ), + ), + _Action( + type: type, + actionLabel: actionLabel, + onPressed: onPressed, + deleteActionLabel: deleteActionLabel, + viewActionLabel: viewActionLabel, + ), + ], + ), + ), + ); + + static Color _getBackgroundColorForType( + BuildContext context, + ZetaSnackBarType? type, + ) { + final colors = Zeta.of(context).colors; + + return switch (type) { + ZetaSnackBarType.positive => colors.green.shade10, + ZetaSnackBarType.info => colors.purple.shade10, + ZetaSnackBarType.warning => colors.orange.shade10, + ZetaSnackBarType.deletion || ZetaSnackBarType.error => colors.red.shade10, + ZetaSnackBarType.view => colors.blue.shade10, + _ => colors.warm.shade100, + }; + } +} + +class _Content extends StatelessWidget { + const _Content({required this.child, required this.type}); + + final Widget child; + final ZetaSnackBarType? type; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('type', type)) + ..add(DiagnosticsProperty('child', child)); + } + + Color _getColorForType( + ZetaColors colors, + ZetaSnackBarType? type, + ) { + return switch (type) { + ZetaSnackBarType.positive || + ZetaSnackBarType.info || + ZetaSnackBarType.warning || + ZetaSnackBarType.deletion || + ZetaSnackBarType.error || + ZetaSnackBarType.view => + colors.textDefault, + _ => colors.textInverse, + }; + } + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return DefaultTextStyle( + style: ZetaTextStyles.bodyMedium.copyWith( + color: _getColorForType(colors, type), + ), + child: child, + ); + } +} + +class _Action extends StatelessWidget { + const _Action({ + required this.type, + required this.actionLabel, + required this.onPressed, + required this.deleteActionLabel, + required this.viewActionLabel, + }); + + final String? actionLabel; + final String deleteActionLabel; + final VoidCallback? onPressed; + final ZetaSnackBarType? type; + final String viewActionLabel; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('type', type)) + ..add(StringProperty('actionLabel', actionLabel)) + ..add(StringProperty('deleteActionLabel', deleteActionLabel)) + ..add(ObjectFlagProperty.has('onPressed', onPressed)) + ..add(StringProperty('viewActionLabel', viewActionLabel)); + } + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return switch (type) { + ZetaSnackBarType.defaultType => _IconButton( + onPressed: () => ScaffoldMessenger.of(context).removeCurrentSnackBar(), + color: colors.iconInverse, + ), + ZetaSnackBarType.action => _ActionButton( + onPressed: onPressed, + label: actionLabel, + color: colors.blue.shade50, + ), + ZetaSnackBarType.positive || + ZetaSnackBarType.info || + ZetaSnackBarType.warning || + ZetaSnackBarType.error => + _IconButton( + onPressed: () => ScaffoldMessenger.of(context).removeCurrentSnackBar(), + color: colors.cool.shade90, + ), + ZetaSnackBarType.deletion => _ActionButton( + onPressed: onPressed, + label: deleteActionLabel, + color: colors.cool.shade90, + ), + ZetaSnackBarType.view => _ActionButton( + onPressed: onPressed, + label: viewActionLabel, + color: colors.cool.shade90, + ), + _ => _ActionButton( + onPressed: onPressed, + label: actionLabel, + color: colors.blue.shade50, + ), + }; + } +} + +class _IconButton extends StatelessWidget { + const _IconButton({ + required this.onPressed, + required this.color, + }); + + final Color color; + final VoidCallback? onPressed; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty.has('onPressed', onPressed)) + ..add(ColorProperty('color', color)); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: ZetaSpacing.xxs), + child: IconButton( + style: IconButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: ZetaSpacing.s), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size(20, 20), + ), + onPressed: onPressed, + icon: Icon( + ZetaIcons.close_round, + color: color, + size: 20, + ), + ), + ); + } +} + +class _ActionButton extends StatelessWidget { + const _ActionButton({ + required this.onPressed, + required this.label, + required this.color, + }); + + final Color color; + final String? label; + final VoidCallback? onPressed; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty.has('onPressed', onPressed)) + ..add(StringProperty('label', label)) + ..add(ColorProperty('color', color)); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: ZetaSpacing.s), + child: TextButton( + style: TextButton.styleFrom( + textStyle: ZetaTextStyles.labelLarge, + padding: const EdgeInsets.symmetric( + horizontal: ZetaSpacing.s, + vertical: ZetaSpacing.xxs, + ), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: Size.zero, + ), + onPressed: onPressed, + child: Text( + label ?? '', + style: ZetaTextStyles.labelLarge.copyWith(color: color), + ), + ), + ); + } +} + +class _LeadingIcon extends StatelessWidget { + const _LeadingIcon(this.type, this.icon); + + final Icon? icon; + final ZetaSnackBarType? type; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(EnumProperty('type', type)); + } + + Color _getIconColor(ZetaColors colors, ZetaSnackBarType? type) { + return switch (type) { + ZetaSnackBarType.positive => colors.positive, + ZetaSnackBarType.info => colors.info, + ZetaSnackBarType.warning => colors.warning, + ZetaSnackBarType.error || ZetaSnackBarType.deletion => colors.negative, + ZetaSnackBarType.view => colors.primary, + _ => colors.iconInverse, + }; + } + + Widget _getIcon(ZetaSnackBarType? type) { + return switch (type) { + ZetaSnackBarType.positive => const Icon(ZetaIcons.check_circle_round), + ZetaSnackBarType.info => const Icon(ZetaIcons.info_round), + ZetaSnackBarType.warning => const Icon(ZetaIcons.warning_round), + ZetaSnackBarType.error => const Icon(ZetaIcons.error_round), + ZetaSnackBarType.deletion => const Icon(ZetaIcons.delete_round), + ZetaSnackBarType.view => const Icon(ZetaIcons.open_in_new_window_round), + _ => const SizedBox(), + }; + } + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return Padding( + padding: type != null || icon != null ? const EdgeInsets.only(left: ZetaSpacing.s) : EdgeInsets.zero, + child: IconTheme( + data: IconThemeData( + color: _getIconColor(colors, type), + ), + child: icon ?? _getIcon(type), + ), + ); + } +} diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index 8511cd6a..1a41efe5 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -30,6 +30,7 @@ export 'src/components/password/password_input.dart'; export 'src/components/progress/progress_bar.dart'; export 'src/components/progress/progress_circle.dart'; export 'src/components/radio/radio.dart'; +export 'src/components/snack_bar/snack_bar.dart'; export 'src/components/switch/zeta_switch.dart'; export 'src/theme/color_extensions.dart'; export 'src/theme/color_scheme.dart';