From 0822d4f070c6997a6599442e5ce3391a8f26e002 Mon Sep 17 00:00:00 2001 From: Luke Walton Date: Thu, 9 May 2024 12:23:52 +0100 Subject: [PATCH] chore: update from internal (#70) Update to `b25a469dcd4025fb3f4bb564086eb61ac2edc8dd` feat: Global Header (zebrafed#38) fix: Button update (zebrafed#42) Authored-by: Osman AO3856@zebra.com --- example/lib/home.dart | 2 + .../components/global_header_example.dart | 57 ++++++ example/macos/Podfile.lock | 2 +- example/widgetbook/main.dart | 57 +++--- .../pages/components/button_widgetbook.dart | 47 +++-- .../components/global_header_widgetbook.dart | 38 ++++ lib/src/components/app_bar/app_bar.dart | 1 + lib/src/components/avatars/avatar.dart | 120 +++++++----- lib/src/components/buttons/button.dart | 74 +++++++- .../global_header/global_header.dart | 173 ++++++++++++++++++ .../global_header/header_tab_item.dart | 72 ++++++++ lib/src/theme/colors_base.dart | 4 +- lib/src/utils/extensions.dart | 9 + lib/zeta_flutter.dart | 2 + pubspec.yaml | 1 + 15 files changed, 543 insertions(+), 116 deletions(-) create mode 100644 example/lib/pages/components/global_header_example.dart create mode 100644 example/widgetbook/pages/components/global_header_widgetbook.dart create mode 100644 lib/src/components/global_header/global_header.dart create mode 100644 lib/src/components/global_header/header_tab_item.dart diff --git a/example/lib/home.dart b/example/lib/home.dart index 63c48d63..0ff969ac 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -16,6 +16,7 @@ import 'package:zeta_example/pages/components/date_input_example.dart'; import 'package:zeta_example/pages/components/dialog_example.dart'; import 'package:zeta_example/pages/components/dialpad_example.dart'; import 'package:zeta_example/pages/components/dropdown_example.dart'; +import 'package:zeta_example/pages/components/global_header_example.dart'; 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'; @@ -67,6 +68,7 @@ final List components = [ Component(NavigationBarExample.name, (context) => const NavigationBarExample()), Component(PaginationExample.name, (context) => const PaginationExample()), Component(PasswordInputExample.name, (context) => const PasswordInputExample()), + Component(GroupHeaderExample.name, (context) => const GroupHeaderExample()), Component(DropdownExample.name, (context) => const DropdownExample()), Component(ProgressExample.name, (context) => const ProgressExample()), Component(SegmentedControlExample.name, (context) => const SegmentedControlExample()), diff --git a/example/lib/pages/components/global_header_example.dart b/example/lib/pages/components/global_header_example.dart new file mode 100644 index 00000000..7a36c4a9 --- /dev/null +++ b/example/lib/pages/components/global_header_example.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class GroupHeaderExample extends StatefulWidget { + static final name = "GlobalHeader"; + const GroupHeaderExample({super.key}); + + @override + State createState() => _GroupHeaderExampleState(); +} + +class _GroupHeaderExampleState extends State { + final childrenOne = List.filled(5, ZetaGlobalHeaderItem(label: 'Button')); + final childrenTwo = List.filled(10, ZetaGlobalHeaderItem(label: 'Button')); + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: "Global Header", + child: LayoutBuilder(builder: (context, constraints) { + return Center( + child: SingleChildScrollView( + child: Column(children: [ + Text(constraints.maxWidth.toString()), + ZetaGlobalHeader( + title: "Title", + tabItems: childrenOne, + searchBar: ZetaSearchBar(shape: ZetaWidgetBorder.full, size: ZetaWidgetSize.large), + onAppsButton: () {}, + actionButtons: [ + IconButton( + onPressed: () {}, + icon: const Icon( + ZetaIcons.alert_round, + ), + ), + IconButton( + onPressed: () {}, + icon: const Icon( + ZetaIcons.help_round, + ), + ), + ], + avatar: const ZetaAvatar(initials: 'PS'), + ), + const SizedBox( + height: ZetaSpacing.x5, + ), + ZetaGlobalHeader(title: "Title", tabItems: childrenTwo), + ]), + ), + ); + }), + ); + } +} diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index 5773bbb3..648712f2 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -29,7 +29,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 diff --git a/example/widgetbook/main.dart b/example/widgetbook/main.dart index 477af5ce..4f0e2166 100644 --- a/example/widgetbook/main.dart +++ b/example/widgetbook/main.dart @@ -19,6 +19,7 @@ import 'pages/components/date_input_widgetbook.dart'; import 'pages/components/dial_pad_widgetbook.dart'; import 'pages/components/dialog_widgetbook.dart'; import 'pages/components/dropdown_widgetbook.dart'; +import 'pages/components/global_header_widgetbook.dart'; import 'pages/components/filter_selection_widgetbook.dart'; import 'pages/components/in_page_banner_widgetbook.dart'; import 'pages/components/list_item_widgetbook.dart'; @@ -62,14 +63,8 @@ class HotReload extends StatelessWidget { WidgetbookComponent( name: 'App Bar', useCases: [ - WidgetbookUseCase( - name: 'Default', - builder: (context) => defaultAppBarUseCase(context), - ), - WidgetbookUseCase( - name: 'Search', - builder: (context) => searchAppBarUseCase(context), - ), + WidgetbookUseCase(name: 'Default', builder: (context) => defaultAppBarUseCase(context)), + WidgetbookUseCase(name: 'Search', builder: (context) => searchAppBarUseCase(context)), ], ), WidgetbookComponent( @@ -99,11 +94,6 @@ class HotReload extends StatelessWidget { WidgetbookUseCase(name: 'Group Button', builder: (context) => buttonGroupUseCase(context)), ], ), - WidgetbookUseCase(name: 'BreadCrumbs', builder: (context) => breadCrumbsUseCase(context)), - WidgetbookUseCase(name: 'Banners', builder: (context) => bannerUseCase(context)), - WidgetbookUseCase(name: "Dropdown", builder: (context) => dropdownUseCase(context)), - WidgetbookUseCase(name: 'In Page Banners', builder: (context) => inPageBannerUseCase(context)), - WidgetbookUseCase(name: 'Accordion', builder: (context) => accordionUseCase(context)), WidgetbookComponent( name: 'Chips', useCases: [ @@ -112,12 +102,6 @@ class HotReload extends StatelessWidget { WidgetbookUseCase(name: 'Assist Chip', builder: (context) => assistChipUseCase(context)), ], ), - WidgetbookUseCase(name: 'Password Input', builder: (context) => passwordInputUseCase(context)), - WidgetbookUseCase(name: 'Content', builder: (context) => bottomSheetContentUseCase(context)), - WidgetbookUseCase(name: 'Dial Pad', builder: (context) => dialPadUseCase(context)), - WidgetbookUseCase(name: 'List Item', builder: (context) => listItemUseCase(context)), - WidgetbookUseCase(name: 'Navigation Bar', builder: (context) => navigationBarUseCase(context)), - WidgetbookUseCase(name: 'Pagination', builder: (context) => paginationUseCase(context)), WidgetbookComponent( name: 'Progress', useCases: [ @@ -125,27 +109,29 @@ class HotReload extends StatelessWidget { WidgetbookUseCase(name: 'Circle', builder: (context) => progressCircleUseCase(context)) ], ), + WidgetbookUseCase(name: 'Accordion', builder: (context) => accordionUseCase(context)), + WidgetbookUseCase(name: 'Avatar', builder: (context) => avatarUseCase(context)), + WidgetbookUseCase(name: 'BreadCrumbs', builder: (context) => breadCrumbsUseCase(context)), + WidgetbookUseCase(name: 'Banners', builder: (context) => bannerUseCase(context)), + WidgetbookUseCase(name: 'Checkbox', builder: (context) => checkboxUseCase(context)), + WidgetbookUseCase(name: "Dropdown", builder: (context) => dropdownUseCase(context)), + WidgetbookUseCase(name: 'In Page Banners', builder: (context) => inPageBannerUseCase(context)), + WidgetbookUseCase(name: 'Password Input', builder: (context) => passwordInputUseCase(context)), + WidgetbookUseCase(name: 'Content', builder: (context) => bottomSheetContentUseCase(context)), + 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: 'Navigation Bar', builder: (context) => navigationBarUseCase(context)), + WidgetbookUseCase(name: 'Pagination', builder: (context) => paginationUseCase(context)), WidgetbookUseCase(name: 'Radio Button', builder: (context) => radioButtonUseCase(context)), - WidgetbookUseCase( - name: 'Segmented Control', - builder: (context) => segmentedControlUseCase(context), - ), + WidgetbookUseCase(name: 'Segmented Control', builder: (context) => segmentedControlUseCase(context)), WidgetbookUseCase(name: 'Switch', builder: (context) => switchUseCase(context)), - WidgetbookUseCase( - name: 'Snack Bar', - builder: (context) => snackBarUseCase(context), - ), + WidgetbookUseCase(name: 'Snack Bar', builder: (context) => snackBarUseCase(context)), WidgetbookUseCase(name: 'Date Input', builder: (context) => dateInputUseCase(context)), WidgetbookUseCase(name: 'Tabs', builder: (context) => tabsUseCase(context)), WidgetbookUseCase(name: 'Phone Input', builder: (context) => phoneInputUseCase(context)), - WidgetbookUseCase( - name: 'Stepper', - builder: (context) => stepperUseCase(context), - ), - WidgetbookUseCase( - name: 'Stepper Input', - builder: (context) => stepperInputUseCase(context), - ), + WidgetbookUseCase(name: 'Stepper', builder: (context) => stepperUseCase(context)), + WidgetbookUseCase(name: 'Stepper Input', builder: (context) => stepperInputUseCase(context)), WidgetbookUseCase(name: 'Dialog', builder: (context) => dialogUseCase(context)), WidgetbookUseCase(name: 'Search Bar', builder: (context) => searchBarUseCase(context)), WidgetbookUseCase(name: 'Navigation Rail', builder: (context) => navigationRailUseCase(context)), @@ -177,7 +163,6 @@ class HotReload extends StatelessWidget { DeviceFrameAddon( devices: [ Devices.windows.wideMonitor, - Devices.macOS.wideMonitor, Devices.ios.iPad, Devices.ios.iPhone13, Zebra.ec30, diff --git a/example/widgetbook/pages/components/button_widgetbook.dart b/example/widgetbook/pages/components/button_widgetbook.dart index 45ce0033..81e9f12a 100644 --- a/example/widgetbook/pages/components/button_widgetbook.dart +++ b/example/widgetbook/pages/components/button_widgetbook.dart @@ -5,27 +5,34 @@ import 'package:zeta_flutter/zeta_flutter.dart'; import '../../test/test_components.dart'; import '../../utils/utils.dart'; -Widget buttonUseCase(BuildContext context) => WidgetbookTestWidget( - widget: ZetaButton( - label: context.knobs.string(label: 'Text', initialValue: 'Button'), - onPressed: context.knobs.boolean(label: 'Disabled') ? null : () {}, - borderType: context.knobs.list( - label: 'Border type', - labelBuilder: enumLabelBuilder, - options: ZetaWidgetBorder.values, - ), - size: context.knobs.list( - label: 'Size', - options: ZetaWidgetSize.values, - labelBuilder: enumLabelBuilder, - ), - type: context.knobs.list( - label: 'Type', - options: ZetaButtonType.values, - labelBuilder: enumLabelBuilder, - ), +Widget buttonUseCase(BuildContext context) { + final borderType = context.knobs.list( + label: 'Border type', + labelBuilder: enumLabelBuilder, + options: ZetaWidgetBorder.values, + ); + return WidgetbookTestWidget( + widget: ZetaButton( + label: context.knobs.string(label: 'Text', initialValue: 'Button'), + onPressed: context.knobs.boolean(label: 'Disabled') ? null : () {}, + borderType: borderType, + size: context.knobs.list( + label: 'Size', + options: ZetaWidgetSize.values, + labelBuilder: enumLabelBuilder, ), - ); + type: context.knobs.list( + label: 'Type', + options: ZetaButtonType.values, + labelBuilder: enumLabelBuilder, + ), + leadingIcon: + iconKnob(context, rounded: borderType != ZetaWidgetBorder.sharp, nullable: true, name: "Leading Icon"), + trailingIcon: + iconKnob(context, rounded: borderType != ZetaWidgetBorder.sharp, nullable: true, name: "Trailing Icon"), + ), + ); +} Widget iconButtonUseCase(BuildContext context) { final borderType = context.knobs.list( diff --git a/example/widgetbook/pages/components/global_header_widgetbook.dart b/example/widgetbook/pages/components/global_header_widgetbook.dart new file mode 100644 index 00000000..b0e59e76 --- /dev/null +++ b/example/widgetbook/pages/components/global_header_widgetbook.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget globalHeaderUseCase(BuildContext context) { + final actionButtons = [ + IconButton( + onPressed: () {}, + icon: const Icon( + ZetaIcons.alert_round, + ), + ), + IconButton( + onPressed: () {}, + icon: const Icon( + ZetaIcons.help_round, + ), + ), + ]; + + return WidgetbookTestWidget( + widget: ZetaGlobalHeader( + title: context.knobs.string(label: "Title", initialValue: "Title"), + tabItems: List.generate( + context.knobs.int.slider(label: "Tabs"), (index) => ZetaGlobalHeaderItem(label: 'Button ${index + 1}')), + searchBar: context.knobs.boolean(label: 'Search bar', initialValue: true) + ? ZetaSearchBar(shape: ZetaWidgetBorder.full, size: ZetaWidgetSize.large) + : null, + actionButtons: context.knobs.boolean(label: "Menu buttons", initialValue: true) ? actionButtons : [], + avatar: context.knobs.boolean(label: "Show Avatar", initialValue: true) + ? const ZetaAvatar(initials: 'PS', size: ZetaAvatarSize.s) + : null, + onAppsButton: context.knobs.boolean(label: "Apps menu", initialValue: true) ? () => {} : null, + ), + ); +} diff --git a/lib/src/components/app_bar/app_bar.dart b/lib/src/components/app_bar/app_bar.dart index 5c54548f..158a0a3c 100644 --- a/lib/src/components/app_bar/app_bar.dart +++ b/lib/src/components/app_bar/app_bar.dart @@ -85,6 +85,7 @@ class ZetaAppBar extends StatefulWidget implements PreferredSizeWidget { ) ..add(StringProperty('searchHintText', searchHintText)) ..add(EnumProperty('type', type)) + ..add(EnumProperty('type', type)) ..add(DoubleProperty('titleSpacing', titleSpacing)) ..add(DiagnosticsProperty('titleTextStyle', titleTextStyle)); } diff --git a/lib/src/components/avatars/avatar.dart b/lib/src/components/avatars/avatar.dart index 69b3cf72..40c84b60 100644 --- a/lib/src/components/avatars/avatar.dart +++ b/lib/src/components/avatars/avatar.dart @@ -103,6 +103,27 @@ class ZetaAvatar extends StatelessWidget { /// Notification Badge shown at top right corner of avatar. final ZetaAvatarBadge? upperBadge; + /// Return copy of avatar with certain changed fields + ZetaAvatar copyWith({ + ZetaAvatarSize? size, + Widget? image, + String? initials, + Color? backgroundColor, + Color? borderColor, + ZetaAvatarBadge? lowerBadge, + ZetaAvatarBadge? upperBadge, + }) { + return ZetaAvatar( + size: size ?? this.size, + image: image ?? this.image, + initials: initials ?? this.initials, + backgroundColor: backgroundColor ?? this.backgroundColor, + borderColor: borderColor ?? this.borderColor, + lowerBadge: lowerBadge ?? this.lowerBadge, + upperBadge: upperBadge ?? this.upperBadge, + ); + } + bool get _showPlaceholder => image == null && (initials == null || initials!.isEmpty); @override @@ -122,6 +143,7 @@ class ZetaAvatar extends StatelessWidget { fontSize: size.fontSize, letterSpacing: 0, color: backgroundColor?.onColor, + fontWeight: FontWeight.w500, ), ), ) @@ -132,57 +154,59 @@ class ZetaAvatar extends StatelessWidget { child: innerChild, ); - return Stack( - children: [ - Container( - width: sizePixels, - height: sizePixels, - decoration: BoxDecoration( - border: borderColor != null ? Border.all(color: borderColor!, width: 0) : null, - borderRadius: ZetaRadius.full, - color: backgroundColor ?? (_showPlaceholder ? zetaColors.surfacePrimary : zetaColors.cool.shade20), - ), - child: borderColor != null - ? Container( - width: contentSizePixels, - height: contentSizePixels, - decoration: BoxDecoration( - color: backgroundColor ?? zetaColors.surfaceHovered, - border: Border.all(color: borderColor!, width: borderSize), - borderRadius: ZetaRadius.full, - ), - child: ClipRRect( - borderRadius: ZetaRadius.full, - child: innerContent, - ), - ) - : DecoratedBox( - decoration: BoxDecoration( - borderRadius: ZetaRadius.full, - color: backgroundColor ?? zetaColors.surfaceHovered, - ), - child: ClipRRect( - borderRadius: ZetaRadius.full, - child: innerContent, - ), - ), - ), - if (upperBadge != null) - Positioned( - right: 0, - child: upperBadge!.copyWith( - size: size, + return SelectionContainer.disabled( + child: Stack( + children: [ + Container( + width: sizePixels, + height: sizePixels, + decoration: BoxDecoration( + border: borderColor != null ? Border.all(color: borderColor!, width: 0) : null, + borderRadius: ZetaRadius.full, + color: backgroundColor ?? (_showPlaceholder ? zetaColors.surfacePrimary : zetaColors.cool.shade20), ), + child: borderColor != null + ? Container( + width: contentSizePixels, + height: contentSizePixels, + decoration: BoxDecoration( + color: backgroundColor ?? zetaColors.surfaceHovered, + border: Border.all(color: borderColor!, width: borderSize), + borderRadius: ZetaRadius.full, + ), + child: ClipRRect( + borderRadius: ZetaRadius.full, + child: innerContent, + ), + ) + : DecoratedBox( + decoration: BoxDecoration( + borderRadius: ZetaRadius.full, + color: backgroundColor ?? zetaColors.surfaceHovered, + ), + child: ClipRRect( + borderRadius: ZetaRadius.full, + child: innerContent, + ), + ), ), - if (lowerBadge != null) - Positioned( - right: 0, - bottom: 0, - child: lowerBadge!.copyWith( - size: size, + if (upperBadge != null) + Positioned( + right: 0, + child: upperBadge!.copyWith( + size: size, + ), ), - ), - ], + if (lowerBadge != null) + Positioned( + right: 0, + bottom: 0, + child: lowerBadge!.copyWith( + size: size, + ), + ), + ], + ), ); } diff --git a/lib/src/components/buttons/button.dart b/lib/src/components/buttons/button.dart index be2087cb..18619b52 100644 --- a/lib/src/components/buttons/button.dart +++ b/lib/src/components/buttons/button.dart @@ -13,6 +13,8 @@ class ZetaButton extends StatelessWidget { this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, this.zeta, + this.leadingIcon, + this.trailingIcon, super.key, }); @@ -23,6 +25,8 @@ class ZetaButton extends StatelessWidget { this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, this.zeta, + this.leadingIcon, + this.trailingIcon, super.key, }) : type = ZetaButtonType.primary; @@ -33,6 +37,8 @@ class ZetaButton extends StatelessWidget { this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, this.zeta, + this.leadingIcon, + this.trailingIcon, super.key, }) : type = ZetaButtonType.secondary; @@ -43,6 +49,8 @@ class ZetaButton extends StatelessWidget { this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, this.zeta, + this.leadingIcon, + this.trailingIcon, super.key, }) : type = ZetaButtonType.positive; @@ -53,6 +61,8 @@ class ZetaButton extends StatelessWidget { this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, this.zeta, + this.leadingIcon, + this.trailingIcon, super.key, }) : type = ZetaButtonType.negative; @@ -63,6 +73,8 @@ class ZetaButton extends StatelessWidget { this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, this.zeta, + this.leadingIcon, + this.trailingIcon, super.key, }) : type = ZetaButtonType.outline; @@ -73,6 +85,8 @@ class ZetaButton extends StatelessWidget { this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, this.zeta, + this.leadingIcon, + this.trailingIcon, super.key, }) : type = ZetaButtonType.outlineSubtle; @@ -83,6 +97,8 @@ class ZetaButton extends StatelessWidget { this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, this.zeta, + this.leadingIcon, + this.trailingIcon, super.key, }) : type = ZetaButtonType.text; @@ -106,6 +122,12 @@ class ZetaButton extends StatelessWidget { /// like for example from [showZetaDialog] final Zeta? zeta; + /// Leading icon of button. Goes infront of button. + final IconData? leadingIcon; + + /// Trailing icon of button. Goes behind button. + final IconData? trailingIcon; + /// Creates a clone. ZetaButton copyWith({ String? label, @@ -113,6 +135,8 @@ class ZetaButton extends StatelessWidget { ZetaButtonType? type, ZetaWidgetSize? size, ZetaWidgetBorder? borderType, + IconData? leadingIcon, + IconData? trailingIcon, Key? key, }) { return ZetaButton( @@ -122,6 +146,8 @@ class ZetaButton extends StatelessWidget { size: size ?? this.size, borderType: borderType ?? this.borderType, zeta: zeta, + leadingIcon: leadingIcon ?? this.leadingIcon, + trailingIcon: trailingIcon ?? this.trailingIcon, key: key ?? this.key, ); } @@ -136,12 +162,32 @@ class ZetaButton extends StatelessWidget { onPressed: onPressed, style: buttonStyle(colors, borderType, type, null), child: SelectionContainer.disabled( - child: label.isEmpty - ? const SizedBox() - : Text( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (leadingIcon != null) + Icon( + leadingIcon, + size: _iconSize, + ), + if (label.isNotEmpty) + Text( label, style: _textStyle, - ).paddingHorizontal(_textPadding), + ), + if (trailingIcon != null) + Icon( + trailingIcon, + size: _iconSize, + ), + ] + .divide( + const SizedBox( + width: ZetaSpacing.x2, + ), + ) + .toList(), + ).paddingHorizontal(_textPadding), ), ), ); @@ -165,13 +211,23 @@ class ZetaButton extends StatelessWidget { double get _textPadding { switch (size) { case ZetaWidgetSize.large: - return ZetaSpacing.m; + return ZetaSpacing.x4; case ZetaWidgetSize.medium: - return ZetaSpacing.x3_5; + return ZetaSpacing.x3; case ZetaWidgetSize.small: - return ZetaSpacing.x2_5; + return ZetaSpacing.x1; + } + } + + double get _iconSize { + switch (size) { + case ZetaWidgetSize.large: + case ZetaWidgetSize.medium: + return ZetaSpacing.x5; + case ZetaWidgetSize.small: + return ZetaSpacing.x4; } } @@ -183,6 +239,8 @@ class ZetaButton extends StatelessWidget { ..add(ObjectFlagProperty.has('onPressed', onPressed)) ..add(EnumProperty('type', type)) ..add(EnumProperty('borderType', borderType)) - ..add(EnumProperty('size', size)); + ..add(EnumProperty('size', size)) + ..add(DiagnosticsProperty('leadingIcon', leadingIcon)) + ..add(DiagnosticsProperty('trailingIcon', trailingIcon)); } } diff --git a/lib/src/components/global_header/global_header.dart b/lib/src/components/global_header/global_header.dart new file mode 100644 index 00000000..723d81c0 --- /dev/null +++ b/lib/src/components/global_header/global_header.dart @@ -0,0 +1,173 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../../zeta_flutter.dart'; + +/// Global header component +class ZetaGlobalHeader extends StatefulWidget { + /// Constructor for [ZetaGlobalHeader] + const ZetaGlobalHeader({ + super.key, + required this.title, + this.tabItems = const [], + this.actionButtons = const [], + this.avatar, + this.searchBar, + this.onAppsButton, + }); + + /// Header title in top left of header + final String title; + + /// Tab item buttons + final List tabItems; + + /// Action buttons. + final List actionButtons; + + /// Avatar component. + final ZetaAvatar? avatar; + + /// Search bar component. + final ZetaSearchBar? searchBar; + + /// Call back for apps icon button shown before avatar on bar. + /// + /// If null, apps button and preceding divider are not rendered. + final VoidCallback? onAppsButton; + + @override + State createState() => _GlobalHeaderState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('title', title)) + ..add(ObjectFlagProperty.has('onAppsButton', onAppsButton)); + } +} + +extension on DeviceType { + /// Render buttons along the top menu half + bool get isLarge { + return this == DeviceType.desktopL || this == DeviceType.desktopXL; + } + + /// Render search bar on bottom half of menu + bool get isSmall { + return this == DeviceType.mobilePortrait || this == DeviceType.mobileLandscape; + } +} + +class _GlobalHeaderState extends State { + int _selectedIndex = -1; + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + return LayoutBuilder( + builder: (context, constraints) { + final deviceType = constraints.deviceType; + + return Container( + padding: const EdgeInsets.symmetric(vertical: ZetaSpacing.s, horizontal: ZetaSpacing.b), + decoration: BoxDecoration(color: colors.surfacePrimary), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 48, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + // Top Section + children: [ + Row( + children: [ + Text(widget.title, style: ZetaTextStyles.h4), + const SizedBox.square(dimension: ZetaSpacing.s), + if (deviceType.isLarge) + // If using large screen, render some tabItems in to section + ...renderedChildren(widget.tabItems) + .sublist(0, widget.tabItems.length > 4 ? 4 : widget.tabItems.length), + ], + ), + // If screen is not small, render search bar on the top + if (!deviceType.isSmall && widget.searchBar != null) Expanded(child: widget.searchBar!), + Row( + children: [ + ...widget.actionButtons.map( + (e) => IconButton(onPressed: e.onPressed, icon: e.icon, iconSize: 24), + ), + if (widget.onAppsButton != null) ...[ + Container( + color: colors.borderDefault, + width: 1, + height: ZetaSpacing.x6, + margin: const EdgeInsets.symmetric(horizontal: ZetaSpacing.xxs), + ), + IconButton(icon: const Icon(ZetaIcons.apps_round), onPressed: widget.onAppsButton), + ], + const SizedBox(width: ZetaSpacing.xs), + if (widget.avatar != null) widget.avatar!.copyWith(size: ZetaAvatarSize.m), + ], + ), + ].gap(ZetaSpacing.s), + ), + ), + const SizedBox(height: ZetaSpacing.x2), + Row( + children: [ + if (deviceType.isSmall && widget.searchBar != null) Expanded(child: widget.searchBar!), + if (widget.tabItems.isNotEmpty && !deviceType.isSmall) + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + /// Large screen filters some tab items to render on top + children: deviceType.isLarge && widget.tabItems.length >= 5 + ? renderedChildren(widget.tabItems).sublist(5, widget.tabItems.length) + : renderedChildren(widget.tabItems), + ), + ), + ), + ].gap(ZetaSpacing.s), + ), + if (widget.tabItems.isNotEmpty && deviceType.isSmall) + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + // Large screen filters some tab items to render on top + children: deviceType.isLarge && widget.tabItems.length > 5 + ? renderedChildren(widget.tabItems).sublist(5, widget.tabItems.length - 1) + : renderedChildren(widget.tabItems), + ), + ), + ], + ), + ); + }, + ); + } + + /// Extend tab items to register their active states + List renderedChildren(List children) { + final List modifiedChildren = []; + for (final (index, child) in children.indexed) { + modifiedChildren.add( + child.copyWith( + active: _selectedIndex == index, + dropdown: child.dropdown, + handlePress: () { + setState(() { + _selectedIndex = index; + }); + child.handlePress!.call(); + }, + ), + ); + } + return modifiedChildren; + } +} diff --git a/lib/src/components/global_header/header_tab_item.dart b/lib/src/components/global_header/header_tab_item.dart new file mode 100644 index 00000000..4e2ba14c --- /dev/null +++ b/lib/src/components/global_header/header_tab_item.dart @@ -0,0 +1,72 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; + +/// Tab item to be used in [ZetaGlobalHeader] +class ZetaGlobalHeaderItem extends StatefulWidget { + ///Constructor for tab item + const ZetaGlobalHeaderItem({super.key, this.dropdown, this.active, this.handlePress, required this.label}); + + /// Dropdown widget for tab item + final Widget? dropdown; + + /// If the tab item is the active tab item + final bool? active; + + /// Handle press of tab item + final VoidCallback? handlePress; + + /// Content displayed on tab. + final String label; + + @override + State createState() => _ZetaGlobalHeaderItemState(); + + /// Return copy + ZetaGlobalHeaderItem copyWith({ + Widget? dropdown, + bool? active, + VoidCallback? handlePress, + String? label, + }) { + return ZetaGlobalHeaderItem( + dropdown: dropdown ?? this.dropdown, + active: active ?? this.active, + handlePress: handlePress ?? this.handlePress, + label: label ?? this.label, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('active', active)) + ..add(ObjectFlagProperty.has('handlePress', handlePress)) + ..add(StringProperty('label', label)); + } +} + +class _ZetaGlobalHeaderItemState extends State { + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + final foregroundColor = widget.active! ? colors.primary : colors.textSubtle; + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: widget.handlePress, + child: Row( + children: [ + Text(widget.label, style: TextStyle(color: foregroundColor)), + const SizedBox(width: ZetaSpacing.x2), + if (widget.dropdown != null) Icon(ZetaIcons.expand_more_round, color: foregroundColor), + ], + ).paddingHorizontal(ZetaSpacing.m).paddingVertical(ZetaSpacing.s), + ), + ); + } +} diff --git a/lib/src/theme/colors_base.dart b/lib/src/theme/colors_base.dart index dbd25a42..7fb92178 100644 --- a/lib/src/theme/colors_base.dart +++ b/lib/src/theme/colors_base.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; - -import 'color_swatch.dart'; -import 'colors.dart'; +import '../../zeta_flutter.dart'; /// Default set of Zeta Colors that can be used to make a [ZetaColors] instance. /// diff --git a/lib/src/utils/extensions.dart b/lib/src/utils/extensions.dart index 77dc6bcc..643ad0e7 100644 --- a/lib/src/utils/extensions.dart +++ b/lib/src/utils/extensions.dart @@ -17,6 +17,15 @@ extension ListDivider on Iterable { yield iterator.current; } } + + /// Space out a list of wigets with gap of fixed width + List gap(double gap) { + return divide( + SizedBox.square( + dimension: gap, + ), + ).toList(); + } } /// Extension to add spacing to any [Widget]. diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index 93af5218..ce453296 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -29,6 +29,8 @@ export 'src/components/dial_pad/dial_pad.dart'; export 'src/components/dialog/dialog.dart'; export 'src/components/dropdown/dropdown.dart'; 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/navigation bar/navigation_bar.dart'; export 'src/components/navigation_rail/navigation_rail.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 7aa891f0..1062cf76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: flutter_slidable: ^3.1.0 intl: ^0.18.1 mask_text_input_formatter: ^2.9.0 + web: ^0.5.0 dev_dependencies: zds_analysis: ^1.0.0