diff --git a/example/lib/home.dart b/example/lib/home.dart index 2edc5ff4..6dfa31e8 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -20,6 +20,7 @@ import 'package:zeta_example/pages/components/filter_selection_example.dart'; 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/navigation_rail_example.dart'; +import 'package:zeta_example/pages/components/notification_list_example.dart'; import 'package:zeta_example/pages/components/phone_input_example.dart'; import 'package:zeta_example/pages/components/radio_example.dart'; import 'package:zeta_example/pages/components/screen_header_bar_example.dart'; @@ -68,6 +69,7 @@ final List components = [ Component(ContactItemExample.name, (context) => const ContactItemExample()), Component(ListItemExample.name, (context) => const ListItemExample()), Component(NavigationBarExample.name, (context) => const NavigationBarExample()), + Component(NotificationListItemExample.name, (context) => const NotificationListItemExample()), Component(PaginationExample.name, (context) => const PaginationExample()), Component(PasswordInputExample.name, (context) => const PasswordInputExample()), Component(GroupHeaderExample.name, (context) => const GroupHeaderExample()), diff --git a/example/lib/pages/components/notification_list_example.dart b/example/lib/pages/components/notification_list_example.dart new file mode 100644 index 00000000..a6ad01bb --- /dev/null +++ b/example/lib/pages/components/notification_list_example.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class NotificationListItemExample extends StatefulWidget { + static const String name = 'NotificationListItem'; + + const NotificationListItemExample({super.key}); + + @override + State createState() => _NotificationListItemExampleState(); +} + +class _NotificationListItemExampleState extends State { + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: "NotificationListItem", + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 400), + child: ZetaNotificationListItem( + body: Text( + "New urgent" * 300, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + title: 'Urgent Message', + leading: ZetaNotificationBadge.icon(icon: ZetaIcons.check_circle_round), + notificationTime: "Just now", + action: ZetaButton.negative( + label: "Remove", + onPressed: () {}, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 200), + child: ZetaNotificationListItem( + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "New urgent" * 300, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ZetaButton.text(label: "label") + ], + ), + title: 'Urgent Message', + leading: ZetaNotificationBadge.icon(icon: ZetaIcons.check_circle_round), + notificationTime: "Just now", + action: ZetaButton.negative( + label: "Remove", + onPressed: () {}, + ), + ), + ), + ].gap(ZetaSpacing.l))), + ); + } +} diff --git a/example/widgetbook/main.dart b/example/widgetbook/main.dart index 16547e33..3b903802 100644 --- a/example/widgetbook/main.dart +++ b/example/widgetbook/main.dart @@ -5,6 +5,7 @@ import 'package:zeta_flutter/zeta_flutter.dart'; import 'pages/assets/icon_widgetbook.dart'; import 'pages/components/accordion_widgetbook.dart'; +import 'pages/components/notification_list_item_widgetbook.dart'; import 'pages/components/text_input_widgetbook.dart'; import 'pages/components/top_app_bar_widgetbook.dart'; import 'pages/components/avatar_widgetbook.dart'; @@ -140,6 +141,8 @@ class _HotReloadState extends State { WidgetbookUseCase(name: 'Dial Pad', builder: (context) => dialPadUseCase(context)), WidgetbookUseCase(name: 'Global Header', builder: (context) => globalHeaderUseCase(context)), WidgetbookUseCase(name: 'List Item', builder: (context) => listItemUseCase(context)), + WidgetbookUseCase( + name: 'Notification List Item', builder: (context) => notificationListItemUseCase(context)), WidgetbookUseCase(name: 'Navigation Bar', builder: (context) => navigationBarUseCase(context)), WidgetbookUseCase(name: 'Pagination', builder: (context) => paginationUseCase(context)), WidgetbookUseCase(name: 'Radio Button', builder: (context) => radioButtonUseCase(context)), diff --git a/example/widgetbook/pages/components/avatar_widgetbook.dart b/example/widgetbook/pages/components/avatar_widgetbook.dart index 488cd5a0..38cf250a 100644 --- a/example/widgetbook/pages/components/avatar_widgetbook.dart +++ b/example/widgetbook/pages/components/avatar_widgetbook.dart @@ -12,10 +12,10 @@ Widget avatarUseCase(BuildContext context) { widget: ZetaAvatar( image: context.knobs.boolean(label: 'Image') ? image : null, size: context.knobs.list( - label: 'Size', - options: ZetaAvatarSize.values, - labelBuilder: (value) => value.name.split('.').last.toUpperCase(), - ), + label: 'Size', + options: ZetaAvatarSize.values, + labelBuilder: (value) => value.name.split('.').last.toUpperCase(), + initialOption: ZetaAvatarSize.m), upperBadge: context.knobs.boolean(label: 'Status Badge', initialValue: false) ? ZetaAvatarBadge.icon( icon: ZetaIcons.close_round, @@ -33,9 +33,7 @@ Widget avatarUseCase(BuildContext context) { ) : null, initials: context.knobs.stringOrNull(label: 'Initials', initialValue: null), - backgroundColor: context.knobs.colorOrNull( - label: 'Background color', - ), + backgroundColor: context.knobs.colorOrNull(label: 'Background color', initialValue: colors.purple.shade80), ), ); } diff --git a/example/widgetbook/pages/components/notification_list_item_widgetbook.dart b/example/widgetbook/pages/components/notification_list_item_widgetbook.dart new file mode 100644 index 00000000..bd72b586 --- /dev/null +++ b/example/widgetbook/pages/components/notification_list_item_widgetbook.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget notificationListItemUseCase(BuildContext context) => WidgetbookTestWidget( + widget: Padding( + padding: EdgeInsets.symmetric(horizontal: context.knobs.list(label: "Size", options: [100, 200, 400])), + child: ZetaNotificationListItem( + body: context.knobs.boolean(label: "Include Link") + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "New urgent" * 300, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ZetaButton.text(label: "label") + ], + ) + : Text( + "New urgent" * 300, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + title: context.knobs.string(label: "Title", initialValue: "Urgent Notification"), + notificationTime: context.knobs.stringOrNull(label: "Notification Time", initialValue: "Just Now"), + notificationRead: context.knobs.boolean(label: "Notification Read", initialValue: false), + leading: context.knobs.list( + label: 'Badge', + options: [ + ZetaNotificationBadge.avatar(avatar: ZetaAvatar.initials(initials: "AO")), + ZetaNotificationBadge.icon(icon: ZetaIcons.check_circle_round), + ZetaNotificationBadge.image( + image: Image.network( + "https://www.google.com/url?sa=i&url=https%3A%2F%2Fgithub.com%2Fzebratechnologies&psig=AOvVaw0fBPVE5gUkkpFw8mVf6B8G&ust=1717073069230000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCPCwn-XxsoYDFQAAAAAdAAAAABAE")) + ], + labelBuilder: (value) => value.avatar != null + ? "Avatar" + : value.icon != null + ? "Icon" + : "Image", + ), + action: context.knobs.list( + label: "Action Buttons", + options: [ + ZetaButton.negative(label: "Remove"), + ZetaButton.positive(label: "Add"), + ZetaButton.outline(label: "Action"), + ], + labelBuilder: (value) { + final button = (value as ZetaButton); + return button.label == "Remove" + ? "Negative" + : button.label == "Add" + ? "Positive" + : "Netutral"; + }, + ), + showDivider: context.knobs.booleanOrNull(label: "Has More"), + ), + ), + ); diff --git a/lib/src/components/avatars/avatar.dart b/lib/src/components/avatars/avatar.dart index 35df0e13..3dd62b52 100644 --- a/lib/src/components/avatars/avatar.dart +++ b/lib/src/components/avatars/avatar.dart @@ -308,8 +308,8 @@ class ZetaAvatarBadge extends StatelessWidget { this.color, this.icon = ZetaIcons.star_round, this.iconColor, + this.size = ZetaAvatarSize.xxxl, }) : value = null, - size = ZetaAvatarSize.xxxl, type = ZetaAvatarBadgeType.icon; /// Constructs [ZetaAvatarBadge] with notifications @@ -407,7 +407,7 @@ class ZetaAvatarBadge extends StatelessWidget { ) : null, ), - child: Center(child: innerContent), + child: innerContent, ); } diff --git a/lib/src/components/badges/indicator.dart b/lib/src/components/badges/indicator.dart index f70163ed..5321e867 100644 --- a/lib/src/components/badges/indicator.dart +++ b/lib/src/components/badges/indicator.dart @@ -23,6 +23,7 @@ class ZetaIndicator extends StatelessWidget { this.icon, this.value, this.inverse = false, + this.color, }); /// Constructor for [ZetaIndicator] of type [ZetaIndicatorType.icon]. @@ -31,6 +32,7 @@ class ZetaIndicator extends StatelessWidget { this.size = ZetaWidgetSize.large, this.inverse = false, this.icon, + this.color, }) : type = ZetaIndicatorType.icon, value = null; @@ -41,6 +43,7 @@ class ZetaIndicator extends StatelessWidget { this.inverse = false, this.icon, this.value, + this.color, }) : type = ZetaIndicatorType.notification; /// The type of the [ZetaIndicator] - icon or notification. @@ -62,6 +65,9 @@ class ZetaIndicator extends StatelessWidget { /// Value for the type `notification`. final int? value; + /// Color for zeta indicator + final Color? color; + /// Creates a clone. ZetaIndicator copyWith({ ZetaIndicatorType? type, @@ -82,7 +88,8 @@ class ZetaIndicator extends StatelessWidget { @override Widget build(BuildContext context) { final zetaColors = Zeta.of(context).colors; - final Color backgroundColor = (type == ZetaIndicatorType.icon ? zetaColors.blue : zetaColors.surfaceNegative); + final Color backgroundColor = + color ?? (type == ZetaIndicatorType.icon ? zetaColors.blue : zetaColors.surfaceNegative); final Color foregroundColor = backgroundColor.onColor; final sizePixels = _getSizePixels(size, type); @@ -163,6 +170,7 @@ class ZetaIndicator extends StatelessWidget { ..add(DiagnosticsProperty('size', size)) ..add(DiagnosticsProperty('value', value)) ..add(DiagnosticsProperty('icon', icon)) - ..add(DiagnosticsProperty('inverseBorder', inverse)); + ..add(DiagnosticsProperty('inverseBorder', inverse)) + ..add(ColorProperty('color', color)); } } diff --git a/lib/src/components/buttons/button_style.dart b/lib/src/components/buttons/button_style.dart index 52c16ef8..f7b836b4 100644 --- a/lib/src/components/buttons/button_style.dart +++ b/lib/src/components/buttons/button_style.dart @@ -107,7 +107,7 @@ ButtonStyle buttonStyle( return isSolid ? color.shade50 : colors.cool.shade20; } if (backgroundColor != null) return backgroundColor; - return isSolid ? color : Colors.transparent; + return isSolid ? color : colors.surfacePrimary; }, ), foregroundColor: WidgetStateProperty.resolveWith( diff --git a/lib/src/components/list_item/notification_list_item.dart b/lib/src/components/list_item/notification_list_item.dart new file mode 100644 index 00000000..460420e1 --- /dev/null +++ b/lib/src/components/list_item/notification_list_item.dart @@ -0,0 +1,234 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; + +/// List Item for notifications +class ZetaNotificationListItem extends StatefulWidget { + /// Constructor for [ZetaNotificationListItem] + const ZetaNotificationListItem({ + super.key, + required this.leading, + required this.body, + required this.title, + this.notificationRead = false, + this.notificationTime, + required this.action, + this.showDivider = false, + }); + + /// Notification Badge to indicate type of notification or who it's coming from + final ZetaNotificationBadge leading; + + /// Body of notification item + final Widget body; + + /// Notification title + final String title; + + /// If notification has been read + final bool notificationRead; + + /// Time of notificaiton + final String? notificationTime; + + /// If notification is a grouped and there are more notifications show divider. + final bool? showDivider; + + /// Pass in a action widget to handle action functionality. + final Widget action; + + @override + State createState() => _ZetaNotificationListItemState(); + + /// Function that returns copy of a notification item with altered fields + ZetaNotificationListItem copyWith({ + ZetaNotificationBadge? leading, + Widget? body, + String? title, + String? notificationTime, + String? linkText, + VoidCallback? linkOnClick, + Widget? action, + bool? showDivider, + }) { + return ZetaNotificationListItem( + leading: leading ?? this.leading, + body: body ?? this.body, + title: title ?? this.title, + notificationTime: notificationTime ?? this.notificationTime, + action: action ?? this.action, + showDivider: showDivider ?? this.showDivider, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('notificationTime', notificationTime)) + ..add(StringProperty('title', title)) + ..add(DiagnosticsProperty('notificationRead', notificationRead)) + ..add(DiagnosticsProperty('showDivider', showDivider)); + } +} + +class _ZetaNotificationListItemState extends State { + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + return DecoratedBox( + decoration: _getStyle(colors), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + widget.leading, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (!widget.notificationRead) + ZetaIndicator( + color: colors.blue, + size: ZetaWidgetSize.small, + ), + Text( + widget.title, + style: ZetaTextStyles.labelLarge, + ), + ], + ), + Row( + children: [ + if (widget.notificationTime != null) + Text( + widget.notificationTime!, + style: ZetaTextStyles.bodySmall.apply(color: colors.textDisabled), + ), + Container( + padding: const EdgeInsets.all(ZetaSpacing.x0_5), + decoration: BoxDecoration(color: colors.surfaceNegative, borderRadius: ZetaRadius.full), + child: Icon( + ZetaIcons.important_notification_round, + color: colors.white, + size: ZetaSpacing.x3, + ), + ), + ].gap(ZetaSpacing.x1), + ), + ], + ), + widget.body, + ].gap(ZetaSpacing.x1), + ), + ), + ].gap(ZetaSpacing.x2), + ), + Container(alignment: Alignment.centerRight, child: widget.action), + ], + ).paddingAll(ZetaSpacing.x2), + ); + } + + BoxDecoration _getStyle(ZetaColors colors) { + return BoxDecoration( + color: widget.notificationRead ? colors.surfacePrimary : colors.surfaceSelected, + borderRadius: ZetaRadius.rounded, + border: + (widget.showDivider ?? false) ? Border(bottom: BorderSide(width: ZetaSpacing.x1, color: colors.blue)) : null, + ); + } +} + +extension on Image { + /// Return copy of image with altered height and width + Image copyWith({double? height, double? width, BoxFit? fit}) { + return Image( + height: height ?? this.height, + width: width ?? this.width, + fit: fit ?? this.fit, + image: image, + ); + } +} + +/// Badge item for notification list items. Can be an avatar, icon or image +class ZetaNotificationBadge extends StatelessWidget { + /// Constructs a notification badge with an avatar. + const ZetaNotificationBadge.avatar({ + super.key, + required this.avatar, + }) : icon = null, + iconColor = null, + image = null; + + /// Constructs a notification badge with an icon. + const ZetaNotificationBadge.icon({ + super.key, + required this.icon, + this.iconColor, + }) : avatar = null, + image = null; + + /// Constructs a notification badge with an image. + const ZetaNotificationBadge.image({ + super.key, + required this.image, + }) : icon = null, + iconColor = null, + avatar = null; + + /// Avatar to display as notification badge. + final ZetaAvatar? avatar; + + /// Image to display as notification badge. + final Image? image; + + /// Icon to display as notification badge. + final IconData? icon; + + /// Icon color for notification badge. + final Color? iconColor; + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + return avatar != null + ? avatar!.copyWith( + size: ZetaAvatarSize.m, + lowerBadge: ZetaAvatarBadge.icon(icon: ZetaIcons.check_mark_round, color: colors.green), + backgroundColor: colors.purple.shade80, + ) + : icon != null + ? Icon( + icon, + size: ZetaSpacing.x12, + color: iconColor, + ) + : ClipRRect( + borderRadius: ZetaRadius.rounded, + child: SizedBox.fromSize( + size: const Size.square(ZetaSpacing.x12), // Image radius + child: image!.copyWith(fit: BoxFit.cover), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('icon', icon)) + ..add(ColorProperty('iconColor', iconColor)); + } +} diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index c77ecd2c..63c76cae 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -31,6 +31,7 @@ export 'src/components/filter_selection/filter_selection.dart'; export 'src/components/global_header/global_header.dart'; export 'src/components/global_header/header_tab_item.dart'; export 'src/components/list_item/list_item.dart'; +export 'src/components/list_item/notification_list_item.dart'; export 'src/components/navigation bar/navigation_bar.dart'; export 'src/components/navigation_rail/navigation_rail.dart'; export 'src/components/pagination/pagination.dart';