diff --git a/example/lib/home.dart b/example/lib/home.dart index 2c96e4fe..a0b93d43 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:zeta_example/pages/components/accordion_example.dart'; +import 'package:zeta_example/pages/components/app_bar_example.dart'; import 'package:zeta_example/pages/components/avatar_example.dart'; import 'package:zeta_example/pages/components/badges_example.dart'; import 'package:zeta_example/pages/components/banner_example.dart'; @@ -10,16 +11,22 @@ import 'package:zeta_example/pages/components/button_example.dart'; import 'package:zeta_example/pages/components/checkbox_example.dart'; import 'package:zeta_example/pages/components/chip_example.dart'; 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/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/phone_input_example.dart'; import 'package:zeta_example/pages/components/radio_example.dart'; +import 'package:zeta_example/pages/components/search_bar_example.dart'; +import 'package:zeta_example/pages/components/segmented_control_example.dart'; import 'package:zeta_example/pages/components/stepper_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/components/tabs_example.dart'; import 'package:zeta_example/pages/components/pagination_example.dart'; +import 'package:zeta_example/pages/components/tooltip_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'; @@ -39,6 +46,7 @@ class Component { final List components = [ Component(AccordionExample.name, (context) => const AccordionExample()), + Component(AppBarExample.name, (context) => const AppBarExample()), Component(AvatarExample.name, (context) => const AvatarExample()), Component(BannerExample.name, (context) => const BannerExample()), Component(BadgesExample.name, (context) => const BadgesExample()), @@ -53,6 +61,7 @@ final List components = [ Component(PasswordInputExample.name, (context) => const PasswordInputExample()), Component(DropdownExample.name, (context) => const DropdownExample()), Component(ProgressExample.name, (context) => const ProgressExample()), + Component(SegmentedControlExample.name, (context) => const SegmentedControlExample()), Component(SnackBarExample.name, (context) => const SnackBarExample()), Component(StepperExample.name, (context) => const StepperExample()), Component(TabsExample.name, (context) => const TabsExample()), @@ -60,6 +69,11 @@ final List components = [ Component(RadioButtonExample.name, (context) => const RadioButtonExample()), Component(SwitchExample.name, (context) => const SwitchExample()), Component(DateInputExample.name, (context) => const DateInputExample()), + Component(PhoneInputExample.name, (context) => const PhoneInputExample()), + Component(DialogExample.name, (context) => const DialogExample()), + Component(SearchBarExample.name, (context) => const SearchBarExample()), + Component(TooltipExample.name, (context) => const TooltipExample()), + Component(NavigationRailExample.name, (context) => const NavigationRailExample()), ]; final List theme = [ diff --git a/example/lib/pages/components/app_bar_example.dart b/example/lib/pages/components/app_bar_example.dart new file mode 100644 index 00000000..250bc8a7 --- /dev/null +++ b/example/lib/pages/components/app_bar_example.dart @@ -0,0 +1,182 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class AppBarExample extends StatefulWidget { + const AppBarExample({super.key}); + + static const String name = 'AppBar'; + + @override + State createState() => _AppBarExampleState(); +} + +class _AppBarExampleState extends State { + late final _searchController = AppBarSearchController(); + + void _showHideSearch() { + _searchController.isEnabled ? _searchController.closeSearch() : _searchController.startSearch(); + } + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: AppBarExample.name, + child: SingleChildScrollView( + child: Column( + children: [ + // Default + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaAppBar( + leading: IconButton( + onPressed: () {}, + icon: Icon(Icons.menu_rounded), + ), + title: Row( + children: [ + ZetaAvatar(size: ZetaAvatarSize.xs), + Padding( + padding: const EdgeInsets.only(left: ZetaSpacing.s), + child: Text("Title"), + ), + ], + ), + actions: [ + IconButton( + onPressed: () {}, + icon: Icon(Icons.language), + ), + IconButton( + onPressed: () {}, + icon: Icon(Icons.favorite), + ), + IconButton( + onPressed: () {}, + icon: Icon(ZetaIcons.more_vertical_round), + ) + ], + ), + ), + + // Centered + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaAppBar( + type: ZetaAppBarType.centeredTitle, + leading: IconButton( + onPressed: () {}, + icon: Icon(Icons.menu_rounded), + ), + title: Text("Title"), + actions: [ + IconButton( + onPressed: () {}, + icon: Icon(Icons.account_circle), + ), + ], + ), + ), + + // Contextual + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaAppBar( + leading: IconButton( + onPressed: () {}, + icon: Icon(ZetaIcons.close_round), + ), + title: Text("2 items"), + actions: [ + IconButton( + onPressed: () {}, + icon: Icon(ZetaIcons.edit_round), + ), + IconButton( + onPressed: () {}, + icon: Icon(ZetaIcons.share_round), + ), + IconButton( + onPressed: () {}, + icon: Icon(ZetaIcons.delete_round), + ), + IconButton( + onPressed: () {}, + icon: Icon(ZetaIcons.more_vertical_round), + ), + ], + ), + ), + + // Search + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: Column( + children: [ + ZetaAppBar( + type: ZetaAppBarType.centeredTitle, + leading: BackButton(), + title: Text("Title"), + actions: [ + IconButton( + onPressed: _showHideSearch, + icon: Icon(ZetaIcons.search_round), + ) + ], + searchController: _searchController, + onSearch: (text) => debugPrint('search text: $text'), + onSearchMicrophoneIconPressed: () async { + var sampleTexts = [ + 'This is a sample text', + 'Another sample', + 'Speech recognition text', + 'Example' + ]; + + var generatedText = sampleTexts[Random().nextInt(sampleTexts.length)]; + + _searchController.text = generatedText; + }, + ), + ZetaButton.primary( + label: "Show/Hide Search", + onPressed: _showHideSearch, + ) + ], + ), + ), + + // Extended + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaAppBar( + type: ZetaAppBarType.extendedTitle, + leading: IconButton( + onPressed: () {}, + icon: Icon(Icons.menu), + ), + title: Text("Large title"), + actions: [ + IconButton( + onPressed: () {}, + icon: Icon(Icons.language), + ), + IconButton( + onPressed: () {}, + icon: Icon(Icons.favorite), + ), + IconButton( + onPressed: () {}, + icon: Icon(ZetaIcons.more_vertical_round), + ) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/date_input_example.dart b/example/lib/pages/components/date_input_example.dart index 83ac0812..d02d5d9e 100644 --- a/example/lib/pages/components/date_input_example.dart +++ b/example/lib/pages/components/date_input_example.dart @@ -82,7 +82,7 @@ class _DateInputExampleState extends State { label: 'Label', hint: 'Default hint text', errorText: 'Oops! Error hint text', - size: ZetaDateInputSize.medium, + size: ZetaWidgetSize.medium, ), ), Divider(color: Colors.grey[200]), @@ -96,7 +96,7 @@ class _DateInputExampleState extends State { label: 'Label', hint: 'Default hint text', errorText: 'Oops! Error hint text', - size: ZetaDateInputSize.small, + size: ZetaWidgetSize.small, ), ), ], diff --git a/example/lib/pages/components/dialog_example.dart b/example/lib/pages/components/dialog_example.dart new file mode 100644 index 00000000..66d52334 --- /dev/null +++ b/example/lib/pages/components/dialog_example.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class DialogExample extends StatelessWidget { + static const String name = 'Dialog'; + + const DialogExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final zeta = Zeta.of(context); + return ExampleScaffold( + name: 'Dialog', + child: Center( + child: Column( + children: [ + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + title: 'Dialog Title', + icon: Icon( + ZetaIcons.warning_round, + color: zeta.colors.warning, + ), + message: + 'Lorem ipsum dolor sit amet, conse ctetur adipiscing elit, sed do eiusm od tempor incididunt ut labore et do lore magna aliqua.', + primaryButtonLabel: 'Confirm', + ), + child: Text('Show dialog with one button'), + ), + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + title: 'Dialog Title', + icon: Icon( + ZetaIcons.warning_round, + color: zeta.colors.warning, + ), + message: + 'Lorem ipsum dolor sit amet, conse ctetur adipiscing elit, sed do eiusm od tempor incididunt ut labore et do lore magna aliqua.', + primaryButtonLabel: 'Confirm', + secondaryButtonLabel: 'Cancel', + ), + child: Text('Show dialog with two buttons'), + ), + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + title: 'Dialog Title', + icon: Icon( + ZetaIcons.warning_round, + color: zeta.colors.warning, + ), + message: + 'Lorem ipsum dolor sit amet, conse ctetur adipiscing elit, sed do eiusm od tempor incididunt ut labore et do lore magna aliqua.', + primaryButtonLabel: 'Confirm', + secondaryButtonLabel: 'Cancel', + tertiaryButtonLabel: 'Learn more', + onTertiaryButtonPressed: () {}, + ), + child: Text('Show dialog with three buttons'), + ), + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + title: 'Dialog Title', + icon: Icon( + ZetaIcons.warning_round, + color: zeta.colors.warning, + ), + message: + 'Lorem ipsum dolor sit amet, conse ctetur adipiscing elit, sed do eiusm od tempor incididunt ut labore et do lore magna aliqua.', + headerAlignment: ZetaDialogHeaderAlignment.left, + primaryButtonLabel: 'Confirm', + secondaryButtonLabel: 'Cancel', + rounded: false, + ), + child: Text( + 'Show dialog with header to the left\nand sharp buttons', + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/navigation_rail_example.dart b/example/lib/pages/components/navigation_rail_example.dart new file mode 100644 index 00000000..5b930512 --- /dev/null +++ b/example/lib/pages/components/navigation_rail_example.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class NavigationRailExample extends StatefulWidget { + static const String name = 'NavigationRail'; + + const NavigationRailExample({Key? key}) : super(key: key); + + @override + State createState() => _NavigationRailExampleState(); +} + +class _NavigationRailExampleState extends State { + List _titles = [ + 'Label', + 'User Preferences', + 'Account Settings', + 'Label', + ]; + int? _selectedIndex; + + @override + Widget build(BuildContext context) { + final zeta = Zeta.of(context); + + return SafeArea( + child: ExampleScaffold( + name: 'Navigation Rail', + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ZetaNavigationRail( + selectedIndex: _selectedIndex, + onSelect: (index) => setState(() => _selectedIndex = index), + wordWrap: false, + items: [ + ZetaNavigationRailItem( + label: 'Label', + icon: Icon(ZetaIcons.star_round), + ), + ZetaNavigationRailItem( + label: 'User\nPreferences', + icon: Icon(ZetaIcons.star_round), + ), + ZetaNavigationRailItem( + label: 'Account Settings', + icon: Icon(ZetaIcons.star_round), + ), + ZetaNavigationRailItem( + label: 'Label', + icon: Icon(ZetaIcons.star_round), + disabled: true, + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(ZetaSpacing.x5), + child: _selectedIndex == null + ? const SizedBox() + : Text( + _titles[_selectedIndex!], + textAlign: TextAlign.center, + style: ZetaTextStyles.titleMedium.copyWith( + color: zeta.colors.textDefault, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/phone_input_example.dart b/example/lib/pages/components/phone_input_example.dart new file mode 100644 index 00000000..49ccf0c0 --- /dev/null +++ b/example/lib/pages/components/phone_input_example.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class PhoneInputExample extends StatefulWidget { + static const String name = 'PhoneInput'; + + const PhoneInputExample({Key? key}) : super(key: key); + + @override + State createState() => _PhoneInputExampleState(); +} + +class _PhoneInputExampleState extends State { + String? _errorText; + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: 'Phone Input', + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Rounded', style: ZetaTextStyles.titleMedium), + ), + Padding( + padding: const EdgeInsets.all(20), + child: ZetaPhoneInput( + label: 'Phone number', + hint: 'Enter your phone number', + hasError: _errorText != null, + errorText: _errorText, + onChanged: (value) { + if (value?.isEmpty ?? true) setState(() => _errorText = null); + print(value); + }, + countries: ['US', 'GB', 'DE', 'AT', 'FR', 'IT', 'BG'], + ), + ), + Divider(color: Colors.grey[200]), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Sharp', style: ZetaTextStyles.titleMedium), + ), + Padding( + padding: const EdgeInsets.all(20), + child: ZetaPhoneInput( + label: 'Phone number', + hint: 'Enter your phone number', + countryDialCode: '+44', + phoneNumber: '987654321', + hasError: _errorText != null, + errorText: _errorText, + onChanged: (value) { + if (value?.isEmpty ?? true) return setState(() => _errorText = null); + }, + rounded: false, + ), + ), + Divider(color: Colors.grey[200]), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Disabled', style: ZetaTextStyles.titleMedium), + ), + Padding( + padding: const EdgeInsets.all(20), + child: ZetaPhoneInput( + label: 'Phone number', + hint: 'Enter your phone number', + enabled: false, + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/search_bar_example.dart b/example/lib/pages/components/search_bar_example.dart new file mode 100644 index 00000000..693deea2 --- /dev/null +++ b/example/lib/pages/components/search_bar_example.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class SearchBarExample extends StatefulWidget { + static const String name = 'SearchBar'; + + const SearchBarExample({Key? key}) : super(key: key); + + @override + State createState() => _SearchBarExampleState(); +} + +class _SearchBarExampleState extends State { + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: 'Search Bar', + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Rounded', style: ZetaTextStyles.titleMedium), + ), + Padding( + padding: const EdgeInsets.all(20), + child: ZetaSearchBar( + onChanged: (value) {}, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Full', style: ZetaTextStyles.titleMedium), + ), + Padding( + padding: const EdgeInsets.all(20), + child: ZetaSearchBar( + shape: ZetaWidgetBorder.full, + onSpeechToText: () async => 'I wanted to say...', + ), + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Sharp', style: ZetaTextStyles.titleMedium), + ), + Padding( + padding: const EdgeInsets.all(20), + child: ZetaSearchBar( + initialValue: 'Initial value', + shape: ZetaWidgetBorder.sharp, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Disabled', style: ZetaTextStyles.titleMedium), + ), + Padding( + padding: const EdgeInsets.all(20), + child: ZetaSearchBar( + enabled: false, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Medium', style: ZetaTextStyles.titleMedium), + ), + Padding( + padding: const EdgeInsets.all(20), + child: ZetaSearchBar( + size: ZetaWidgetSize.medium, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Small', style: ZetaTextStyles.titleMedium), + ), + Padding( + padding: const EdgeInsets.all(20), + child: ZetaSearchBar( + size: ZetaWidgetSize.small, + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/segmented_control_example.dart b/example/lib/pages/components/segmented_control_example.dart new file mode 100644 index 00000000..783e966c --- /dev/null +++ b/example/lib/pages/components/segmented_control_example.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class SegmentedControlExample extends StatefulWidget { + const SegmentedControlExample({super.key}); + + static const String name = 'SegmentedControl'; + + @override + State createState() => _SegmentedControlExampleState(); +} + +class _SegmentedControlExampleState extends State { + final _iconsSegments = [1, 2, 3, 4, 5]; + final _numberSegments = [1, 2, 3, 4, 5]; + late int _selectedIconSegment = _iconsSegments.first; + late int _selectedNumberSegment = _numberSegments.first; + late String _selectedTextSegment = _textSegments.first; + final _textSegments = ["Item 1", "Item 2", "Item 3"]; + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: SegmentedControlExample.name, + child: SingleChildScrollView( + child: Column( + children: [ + // Text + Padding( + padding: const EdgeInsets.all(ZetaSpacing.l), + child: ZetaSegmentedControl( + segments: [ + for (final value in _textSegments) + ZetaButtonSegment( + value: value, + child: Text(value), + ), + ], + onChanged: (value) => setState( + () => _selectedTextSegment = value, + ), + selected: _selectedTextSegment, + ), + ), + + // Numbers + Padding( + padding: const EdgeInsets.all(ZetaSpacing.l), + child: ZetaSegmentedControl( + segments: [ + for (final value in _numberSegments) + ZetaButtonSegment( + value: value, + child: Text(value.toString()), + ), + ], + onChanged: (value) => setState( + () => _selectedNumberSegment = value, + ), + selected: _selectedNumberSegment, + ), + ), + + // Icons + Padding( + padding: const EdgeInsets.all(ZetaSpacing.l), + child: ZetaSegmentedControl( + segments: [ + for (final value in _iconsSegments) + ZetaButtonSegment( + value: value, + child: Icon(ZetaIcons.star_round), + ), + ], + onChanged: (value) => setState( + () => _selectedIconSegment = value, + ), + selected: _selectedIconSegment, + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/tooltip_example.dart b/example/lib/pages/components/tooltip_example.dart new file mode 100644 index 00000000..01912cfc --- /dev/null +++ b/example/lib/pages/components/tooltip_example.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class TooltipExample extends StatelessWidget { + static const String name = 'Tooltip'; + + const TooltipExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: 'Tooltip', + child: SizedBox( + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('Rounded'), + const SizedBox(height: ZetaSpacing.l), + ZetaTooltip( + child: Text('Label'), + ), + const SizedBox(height: ZetaSpacing.l), + ZetaTooltip( + child: Text('Label'), + arrowDirection: ZetaTooltipArrowDirection.right, + ), + const SizedBox(height: ZetaSpacing.l), + ZetaTooltip( + child: Text('Label'), + arrowDirection: ZetaTooltipArrowDirection.up, + ), + const SizedBox(height: ZetaSpacing.l), + ZetaTooltip( + child: Text('Label'), + arrowDirection: ZetaTooltipArrowDirection.left, + ), + Divider(height: ZetaSpacing.xxl), + Text('Sharp'), + const SizedBox(height: ZetaSpacing.l), + ZetaTooltip( + child: Text('Label'), + rounded: false, + ), + const SizedBox(height: ZetaSpacing.l), + ZetaTooltip( + child: Text('Label'), + arrowDirection: ZetaTooltipArrowDirection.right, + rounded: false, + ), + const SizedBox(height: ZetaSpacing.l), + ZetaTooltip( + child: Text('Label'), + arrowDirection: ZetaTooltipArrowDirection.up, + rounded: false, + ), + const SizedBox(height: ZetaSpacing.l), + ZetaTooltip( + child: Text('Label'), + arrowDirection: ZetaTooltipArrowDirection.left, + rounded: false, + ), + ], + ), + ), + ); + } +} diff --git a/example/widgetbook/main.dart b/example/widgetbook/main.dart index 96a46c7f..0a7ffcce 100644 --- a/example/widgetbook/main.dart +++ b/example/widgetbook/main.dart @@ -4,6 +4,7 @@ import 'package:zeta_flutter/zeta_flutter.dart'; import 'pages/assets/icon_widgetbook.dart'; import 'pages/components/accordion_widgetbook.dart'; +import 'pages/components/app_bar_widgetbook.dart'; import 'pages/components/avatar_widgetbook.dart'; import 'pages/components/badges_widgetbook.dart'; import 'pages/components/banner_widgetbook.dart'; @@ -14,18 +15,24 @@ import 'pages/components/checkbox_widgetbook.dart'; import 'pages/components/chip_widgetbook.dart'; 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/in_page_banner_widgetbook.dart'; import 'pages/components/list_item_widgetbook.dart'; import 'pages/components/navigation_bar_widgetbook.dart'; +import 'pages/components/navigation_rail_widgetbook.dart'; import 'pages/components/pagination_widgetbook.dart'; import 'pages/components/password_input_widgetbook.dart'; +import 'pages/components/phone_input_widgetbook.dart'; import 'pages/components/progress_widgetbook.dart'; import 'pages/components/radio_widgetbook.dart'; +import 'pages/components/search_bar_widgetbook.dart'; +import 'pages/components/segmented_control_widgetbook.dart'; import 'pages/components/stepper_widgetbook.dart'; import 'pages/components/switch_widgetbook.dart'; import 'pages/components/snack_bar_widgetbook.dart'; import 'pages/components/tabs_widgetbook.dart'; +import 'pages/components/tooltip_widgetbook.dart'; import 'pages/theme/color_widgetbook.dart'; import 'pages/theme/radius_widgetbook.dart'; import 'pages/theme/spacing_widgetbook.dart'; @@ -46,6 +53,19 @@ class HotReload extends StatelessWidget { name: 'Components', isInitiallyExpanded: false, children: [ + WidgetbookComponent( + name: 'App Bar', + useCases: [ + WidgetbookUseCase( + name: 'Default', + builder: (context) => defaultAppBarUseCase(context), + ), + WidgetbookUseCase( + name: 'Search', + builder: (context) => searchAppBarUseCase(context), + ), + ], + ), WidgetbookComponent( name: 'Badge', useCases: [ @@ -97,17 +117,26 @@ class HotReload extends StatelessWidget { ], ), WidgetbookUseCase(name: 'Radio Button', builder: (context) => radioButtonUseCase(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: '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: 'Tabs', builder: (context) => tabsUseCase(context)), + WidgetbookUseCase(name: 'Dialog', builder: (context) => dialogUseCase(context)), + WidgetbookUseCase(name: 'Search Bar', builder: (context) => searchBarUseCase(context)), + WidgetbookUseCase(name: 'Navigation Rail', builder: (context) => navigationRailUseCase(context)), + WidgetbookUseCase(name: 'Tooltip', builder: (context) => tooltipUseCase(context)), ]..sort((a, b) => a.name.compareTo(b.name)), ), WidgetbookCategory( diff --git a/example/widgetbook/pages/components/app_bar_widgetbook.dart b/example/widgetbook/pages/components/app_bar_widgetbook.dart new file mode 100644 index 00000000..097cdaf1 --- /dev/null +++ b/example/widgetbook/pages/components/app_bar_widgetbook.dart @@ -0,0 +1,164 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget defaultAppBarUseCase(BuildContext context) { + return WidgetbookTestWidget( + widget: StatefulBuilder( + builder: (context, setState) { + final title = context.knobs.string(label: "Title", initialValue: "Title"); + + final type = context.knobs.list( + label: "Type", + options: [ + ZetaAppBarType.defaultAppBar, + ZetaAppBarType.centeredTitle, + ZetaAppBarType.extendedTitle, + ], + initialOption: ZetaAppBarType.defaultAppBar, + labelBuilder: (type) => type.name, + ); + + final enabledActions = context.knobs.boolean( + label: "Enabled actions", + initialValue: true, + ); + + final leadingIcon = context.knobs.list( + label: "Leading Icon", + options: [ + Icon( + key: Key("Menu"), + Icons.menu_rounded, + ), + Icon( + key: Key("Close"), + ZetaIcons.close_round, + ), + Icon( + key: Key("Arrow back"), + ZetaIcons.arrow_back_round, + ), + ], + initialOption: Icon(Icons.menu_rounded), + labelBuilder: (icon) => icon.key.toString(), + ); + + return ZetaAppBar( + leading: IconButton( + onPressed: () {}, + icon: leadingIcon, + ), + type: type, + title: Text(title), + actions: enabledActions + ? [ + IconButton( + onPressed: () {}, + icon: Icon(Icons.language), + ), + IconButton( + onPressed: () {}, + icon: Icon(Icons.favorite), + ), + IconButton( + onPressed: () {}, + icon: Icon(ZetaIcons.more_vertical_round), + ) + ] + : null, + ); + }, + ), + ); +} + +Widget searchAppBarUseCase(BuildContext context) { + return WidgetbookTestWidget( + widget: _SearchUseCase(), + ); +} + +class _SearchUseCase extends StatefulWidget { + const _SearchUseCase(); + + @override + State<_SearchUseCase> createState() => _SearchUseCaseState(); +} + +class _SearchUseCaseState extends State<_SearchUseCase> { + late final searchController = AppBarSearchController(); + + @override + Widget build(BuildContext context) { + final title = context.knobs.string(label: "Title", initialValue: "Title"); + + final type = context.knobs.list( + label: "Type", + options: [ + ZetaAppBarType.defaultAppBar, + ZetaAppBarType.centeredTitle, + ZetaAppBarType.extendedTitle, + ], + initialOption: ZetaAppBarType.defaultAppBar, + labelBuilder: (type) => type.name, + ); + + final leadingIcon = context.knobs.list( + label: "Leading Icon", + options: [ + Icon( + key: Key("Menu"), + Icons.menu_rounded, + ), + Icon( + key: Key("Close"), + ZetaIcons.close_round, + ), + Icon( + key: Key("Arrow back"), + ZetaIcons.arrow_back_round, + ), + ], + initialOption: Icon(Icons.menu_rounded), + labelBuilder: (icon) => icon.key.toString(), + ); + + final enabledSpeechRecognition = context.knobs.boolean( + label: "Enabled speech recognition", + description: + "Randomly generated text. There is no real speech recognition. That is just for testing the functionality", + initialValue: false, + ); + + return ZetaAppBar( + leading: IconButton( + onPressed: () {}, + icon: leadingIcon, + ), + type: type, + title: Text(title), + searchController: searchController, + onSearchMicrophoneIconPressed: enabledSpeechRecognition + ? () { + var sampleTexts = ['This is a sample text', 'Another sample', 'Speech recognition text', 'Example']; + + var generatedText = sampleTexts[Random().nextInt(sampleTexts.length)]; + + searchController.text = generatedText; + } + : null, + actions: [ + IconButton( + onPressed: () { + searchController.isEnabled ? searchController.closeSearch() : searchController.startSearch(); + }, + icon: Icon(ZetaIcons.search_round)), + ], + ); + } +} diff --git a/example/widgetbook/pages/components/date_input_widgetbook.dart b/example/widgetbook/pages/components/date_input_widgetbook.dart index eb3d7cf1..5e85be3a 100644 --- a/example/widgetbook/pages/components/date_input_widgetbook.dart +++ b/example/widgetbook/pages/components/date_input_widgetbook.dart @@ -16,9 +16,9 @@ Widget dateInputUseCase(BuildContext context) { ); final rounded = context.knobs.boolean(label: 'Rounded', initialValue: true); final enabled = context.knobs.boolean(label: 'Enabled', initialValue: true); - final size = context.knobs.list( + final size = context.knobs.list( label: 'Size', - options: ZetaDateInputSize.values, + options: ZetaWidgetSize.values, labelBuilder: (size) => size.name, ); final datePattern = context.knobs.list( diff --git a/example/widgetbook/pages/components/dialog_widgetbook.dart b/example/widgetbook/pages/components/dialog_widgetbook.dart new file mode 100644 index 00000000..d00f4381 --- /dev/null +++ b/example/widgetbook/pages/components/dialog_widgetbook.dart @@ -0,0 +1,101 @@ +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 dialogUseCase(BuildContext context) { + final zeta = Zeta.of(context); + final title = context.knobs.string( + label: 'Dialog title', + initialValue: 'Dialog Title', + ); + final message = context.knobs.string( + label: 'Dialog message', + initialValue: + 'Lorem ipsum dolor sit amet, conse ctetur adipiscing elit, sed do eiusm od tempor incididunt ut labore et do lore magna aliqua.', + ); + final rounded = context.knobs.boolean(label: 'Rounded', initialValue: true); + final iconData = iconKnob( + context, + name: "Icon", + rounded: rounded, + initial: rounded ? Icons.warning_rounded : Icons.warning_sharp, + ); + final barrierDismissible = context.knobs.boolean(label: 'Barrier dismissible', initialValue: true); + final headerAlignment = context.knobs.list( + label: 'Header alignment', + options: ZetaDialogHeaderAlignment.values, + labelBuilder: (value) => value.name, + ); + return WidgetbookTestWidget( + widget: Padding( + padding: const EdgeInsets.all(ZetaSpacing.x5), + child: Center( + child: Column( + children: [ + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + zeta: zeta, + rounded: rounded, + barrierDismissible: barrierDismissible, + headerAlignment: headerAlignment, + title: title, + icon: Icon( + iconData, + color: zeta.colors.warning, + ), + message: message, + primaryButtonLabel: 'Confirm', + ), + child: Text('Show dialog with one button'), + ), + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + zeta: zeta, + rounded: rounded, + barrierDismissible: barrierDismissible, + headerAlignment: headerAlignment, + title: title, + icon: Icon( + iconData, + color: zeta.colors.warning, + ), + message: message, + primaryButtonLabel: 'Confirm', + secondaryButtonLabel: 'Cancel', + ), + child: Text('Show dialog with two buttons'), + ), + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + zeta: zeta, + rounded: rounded, + barrierDismissible: barrierDismissible, + headerAlignment: headerAlignment, + title: title, + icon: Icon( + iconData, + color: zeta.colors.warning, + ), + message: message, + primaryButtonLabel: 'Confirm', + secondaryButtonLabel: 'Cancel', + tertiaryButtonLabel: 'Learn more', + onTertiaryButtonPressed: () {}, + ), + child: Text('Show dialog with three buttons'), + ), + ], + ), + ), + ), + ); +} diff --git a/example/widgetbook/pages/components/navigation_rail_widgetbook.dart b/example/widgetbook/pages/components/navigation_rail_widgetbook.dart new file mode 100644 index 00000000..f981bfd7 --- /dev/null +++ b/example/widgetbook/pages/components/navigation_rail_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'; +import '../../utils/utils.dart'; + +Widget navigationRailUseCase(BuildContext context) { + final zeta = Zeta.of(context); + int? selectedIndex; + final items = context.knobs.string( + label: 'Items separated with comma', + initialValue: 'Label,User Preferences,Account Settings,Label', + ); + final rounded = context.knobs.boolean(label: 'Rounded', initialValue: true); + final iconData = iconKnob( + context, + name: "Icon", + rounded: rounded, + initial: rounded ? ZetaIcons.star_round : ZetaIcons.star_sharp, + ); + final wordWrap = context.knobs.boolean(label: 'Word wrap', initialValue: true); + final disabled = context.knobs.boolean(label: 'Disabled', initialValue: false); + final itemsList = items.split(',').where((element) => element.trim().isNotEmpty).toList(); + return SafeArea( + child: WidgetbookTestWidget( + widget: StatefulBuilder( + builder: (context, setState) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ZetaNavigationRail( + selectedIndex: selectedIndex, + onSelect: (index) => setState(() => selectedIndex = index), + wordWrap: wordWrap, + items: itemsList + .map((item) => ZetaNavigationRailItem( + label: item, + icon: Icon(iconData), + disabled: disabled, + )) + .toList(), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(ZetaSpacing.x5), + child: selectedIndex == null + ? const SizedBox() + : Text( + itemsList[selectedIndex!], + textAlign: TextAlign.center, + style: ZetaTextStyles.titleMedium.copyWith( + color: zeta.colors.textDefault, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ); + }, + ), + ), + ); +} diff --git a/example/widgetbook/pages/components/phone_input_widgetbook.dart b/example/widgetbook/pages/components/phone_input_widgetbook.dart new file mode 100644 index 00000000..1b36f6c0 --- /dev/null +++ b/example/widgetbook/pages/components/phone_input_widgetbook.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget phoneInputUseCase(BuildContext context) { + final countries = context.knobs.string( + label: 'ISO 3166-1 alpha-2 county codes', + initialValue: '', + ); + final rounded = context.knobs.boolean(label: 'Rounded', initialValue: true); + final enabled = context.knobs.boolean(label: 'Enabled', initialValue: true); + return WidgetbookTestWidget( + widget: StatefulBuilder( + builder: (context, setState) { + return Padding( + padding: const EdgeInsets.all(ZetaSpacing.x5), + child: ZetaPhoneInput( + rounded: rounded, + enabled: enabled, + label: 'Phone number', + hint: 'Enter your phone number', + countries: countries.isEmpty ? null : countries.toUpperCase().split(','), + useRootNavigator: false, + ), + ); + }, + ), + ); +} diff --git a/example/widgetbook/pages/components/search_bar_widgetbook.dart b/example/widgetbook/pages/components/search_bar_widgetbook.dart new file mode 100644 index 00000000..fb4b3b1c --- /dev/null +++ b/example/widgetbook/pages/components/search_bar_widgetbook.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +const List _items = [ + 'The quick...', + 'The quick brown...', + 'The quick brown fox...', + 'The quick brown fox jumped...', + 'The quick brown fox jumped into...', + 'The quick brown fox jumped into the hole...', +]; + +Widget searchBarUseCase(BuildContext context) { + List items = List.from(_items); + return WidgetbookTestWidget( + widget: StatefulBuilder( + builder: (context, setState) { + final hint = context.knobs.string( + label: 'Hint', + initialValue: 'Search', + ); + final enabled = context.knobs.boolean( + label: 'Enabled', + initialValue: true, + ); + final size = context.knobs.list( + label: 'Size', + options: ZetaWidgetSize.values, + labelBuilder: (size) => size.name, + ); + final shape = context.knobs.list( + label: 'Shape', + options: ZetaWidgetBorder.values, + labelBuilder: (shape) => shape.name, + ); + final showLeadingIcon = context.knobs.boolean( + label: 'Show leading icon', + initialValue: true, + ); + final showSpeechToText = context.knobs.boolean( + label: 'Show Speech-To-Text button', + initialValue: true, + ); + + return Padding( + padding: const EdgeInsets.all(ZetaSpacing.x5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ZetaSearchBar( + size: size, + shape: shape, + enabled: enabled, + hint: hint, + showLeadingIcon: showLeadingIcon, + showSpeechToText: showSpeechToText, + onChanged: (value) { + if (value == null) return; + setState( + () => items = _items + .where((item) => item.toLowerCase().contains( + value.toLowerCase(), + )) + .toList(), + ); + }, + onSpeechToText: () async => 'I wanted to say...', + ), + const SizedBox(height: ZetaSpacing.x5), + ...items.map((item) => Text(item)).toList(), + ], + ), + ); + }, + ), + ); +} diff --git a/example/widgetbook/pages/components/segmented_control_widgetbook.dart b/example/widgetbook/pages/components/segmented_control_widgetbook.dart new file mode 100644 index 00000000..e3544fa7 --- /dev/null +++ b/example/widgetbook/pages/components/segmented_control_widgetbook.dart @@ -0,0 +1,64 @@ +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 segmentedControlUseCase(BuildContext context) { + final iconsSegments = List.generate(5, (index) => index); + int selectedIconSegment = iconsSegments.first; + + final rounded = context.knobs.boolean(label: "Rounded", initialValue: true); + final icon = iconKnob(context, rounded: rounded, initial: ZetaIcons.star_round); + + final text = context.knobs.string(label: 'Text', initialValue: "Item"); + + final textSegments = List.generate(3, (index) => "$text ${index + 1}"); + String selectedTextSegment = textSegments.first; + + return WidgetbookTestWidget( + widget: StatefulBuilder(builder: (context, setState) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(ZetaSpacing.l), + child: ZetaSegmentedControl( + rounded: rounded, + segments: [ + for (final value in iconsSegments) + ZetaButtonSegment( + value: value, + child: Icon(icon), + ), + ], + onChanged: (value) => setState( + () => selectedIconSegment = value, + ), + selected: selectedIconSegment, + ), + ), + Padding( + padding: const EdgeInsets.all(ZetaSpacing.l), + child: ZetaSegmentedControl( + rounded: rounded, + segments: [ + for (final value in textSegments) + ZetaButtonSegment( + value: value, + child: Text( + value, + ), + ), + ], + onChanged: (value) => setState( + () => selectedTextSegment = value, + ), + selected: selectedTextSegment, + ), + ), + ], + ); + }), + ); +} diff --git a/example/widgetbook/pages/components/tooltip_widgetbook.dart b/example/widgetbook/pages/components/tooltip_widgetbook.dart new file mode 100644 index 00000000..8212303e --- /dev/null +++ b/example/widgetbook/pages/components/tooltip_widgetbook.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget tooltipUseCase(BuildContext context) { + final text = context.knobs.string( + label: 'Tooltip text', + initialValue: 'Label', + ); + final direction = context.knobs.list( + label: 'Arrow direction', + options: ZetaTooltipArrowDirection.values, + labelBuilder: (direction) => direction.name, + ); + final rounded = context.knobs.boolean(label: 'Rounded', initialValue: true); + return WidgetbookTestWidget( + widget: StatefulBuilder( + builder: (context, setState) { + return Padding( + padding: const EdgeInsets.all(ZetaSpacing.l), + child: ZetaTooltip( + child: Text(text), + rounded: rounded, + arrowDirection: direction, + ), + ); + }, + ), + ); +} diff --git a/lib/src/assets/flags/ad.png b/lib/src/assets/flags/ad.png new file mode 100644 index 00000000..fdc41f6a Binary files /dev/null and b/lib/src/assets/flags/ad.png differ diff --git a/lib/src/assets/flags/ae.png b/lib/src/assets/flags/ae.png new file mode 100644 index 00000000..8e0ae665 Binary files /dev/null and b/lib/src/assets/flags/ae.png differ diff --git a/lib/src/assets/flags/af.png b/lib/src/assets/flags/af.png new file mode 100644 index 00000000..bb620b1b Binary files /dev/null and b/lib/src/assets/flags/af.png differ diff --git a/lib/src/assets/flags/ag.png b/lib/src/assets/flags/ag.png new file mode 100644 index 00000000..3579ff16 Binary files /dev/null and b/lib/src/assets/flags/ag.png differ diff --git a/lib/src/assets/flags/ai.png b/lib/src/assets/flags/ai.png new file mode 100644 index 00000000..1aca2ec9 Binary files /dev/null and b/lib/src/assets/flags/ai.png differ diff --git a/lib/src/assets/flags/al.png b/lib/src/assets/flags/al.png new file mode 100644 index 00000000..f0708963 Binary files /dev/null and b/lib/src/assets/flags/al.png differ diff --git a/lib/src/assets/flags/am.png b/lib/src/assets/flags/am.png new file mode 100644 index 00000000..9dd35f0d Binary files /dev/null and b/lib/src/assets/flags/am.png differ diff --git a/lib/src/assets/flags/an.png b/lib/src/assets/flags/an.png new file mode 100644 index 00000000..cd6b451f Binary files /dev/null and b/lib/src/assets/flags/an.png differ diff --git a/lib/src/assets/flags/ao.png b/lib/src/assets/flags/ao.png new file mode 100644 index 00000000..197cdc6e Binary files /dev/null and b/lib/src/assets/flags/ao.png differ diff --git a/lib/src/assets/flags/aq.png b/lib/src/assets/flags/aq.png new file mode 100644 index 00000000..827282f7 Binary files /dev/null and b/lib/src/assets/flags/aq.png differ diff --git a/lib/src/assets/flags/ar.png b/lib/src/assets/flags/ar.png new file mode 100644 index 00000000..846a2bc1 Binary files /dev/null and b/lib/src/assets/flags/ar.png differ diff --git a/lib/src/assets/flags/as.png b/lib/src/assets/flags/as.png new file mode 100644 index 00000000..c2af5ea9 Binary files /dev/null and b/lib/src/assets/flags/as.png differ diff --git a/lib/src/assets/flags/at.png b/lib/src/assets/flags/at.png new file mode 100644 index 00000000..05dbd1d6 Binary files /dev/null and b/lib/src/assets/flags/at.png differ diff --git a/lib/src/assets/flags/au.png b/lib/src/assets/flags/au.png new file mode 100644 index 00000000..7181dc56 Binary files /dev/null and b/lib/src/assets/flags/au.png differ diff --git a/lib/src/assets/flags/aw.png b/lib/src/assets/flags/aw.png new file mode 100644 index 00000000..efe03a96 Binary files /dev/null and b/lib/src/assets/flags/aw.png differ diff --git a/lib/src/assets/flags/ax.png b/lib/src/assets/flags/ax.png new file mode 100644 index 00000000..c3df4adf Binary files /dev/null and b/lib/src/assets/flags/ax.png differ diff --git a/lib/src/assets/flags/az.png b/lib/src/assets/flags/az.png new file mode 100644 index 00000000..5bc4a0f6 Binary files /dev/null and b/lib/src/assets/flags/az.png differ diff --git a/lib/src/assets/flags/ba.png b/lib/src/assets/flags/ba.png new file mode 100644 index 00000000..09cec0d5 Binary files /dev/null and b/lib/src/assets/flags/ba.png differ diff --git a/lib/src/assets/flags/bb.png b/lib/src/assets/flags/bb.png new file mode 100644 index 00000000..ed7731e6 Binary files /dev/null and b/lib/src/assets/flags/bb.png differ diff --git a/lib/src/assets/flags/bd.png b/lib/src/assets/flags/bd.png new file mode 100644 index 00000000..c24170cc Binary files /dev/null and b/lib/src/assets/flags/bd.png differ diff --git a/lib/src/assets/flags/be.png b/lib/src/assets/flags/be.png new file mode 100644 index 00000000..202c4899 Binary files /dev/null and b/lib/src/assets/flags/be.png differ diff --git a/lib/src/assets/flags/bf.png b/lib/src/assets/flags/bf.png new file mode 100644 index 00000000..9a3d8d1c Binary files /dev/null and b/lib/src/assets/flags/bf.png differ diff --git a/lib/src/assets/flags/bg.png b/lib/src/assets/flags/bg.png new file mode 100644 index 00000000..ab3eb4f1 Binary files /dev/null and b/lib/src/assets/flags/bg.png differ diff --git a/lib/src/assets/flags/bh.png b/lib/src/assets/flags/bh.png new file mode 100644 index 00000000..62011b81 Binary files /dev/null and b/lib/src/assets/flags/bh.png differ diff --git a/lib/src/assets/flags/bi.png b/lib/src/assets/flags/bi.png new file mode 100644 index 00000000..f4de108e Binary files /dev/null and b/lib/src/assets/flags/bi.png differ diff --git a/lib/src/assets/flags/bj.png b/lib/src/assets/flags/bj.png new file mode 100644 index 00000000..83c9474f Binary files /dev/null and b/lib/src/assets/flags/bj.png differ diff --git a/lib/src/assets/flags/bl.png b/lib/src/assets/flags/bl.png new file mode 100644 index 00000000..63209251 Binary files /dev/null and b/lib/src/assets/flags/bl.png differ diff --git a/lib/src/assets/flags/bm.png b/lib/src/assets/flags/bm.png new file mode 100644 index 00000000..5e87c6af Binary files /dev/null and b/lib/src/assets/flags/bm.png differ diff --git a/lib/src/assets/flags/bn.png b/lib/src/assets/flags/bn.png new file mode 100644 index 00000000..1006d688 Binary files /dev/null and b/lib/src/assets/flags/bn.png differ diff --git a/lib/src/assets/flags/bo.png b/lib/src/assets/flags/bo.png new file mode 100644 index 00000000..17d56dc6 Binary files /dev/null and b/lib/src/assets/flags/bo.png differ diff --git a/lib/src/assets/flags/bq.png b/lib/src/assets/flags/bq.png new file mode 100644 index 00000000..f545dc87 Binary files /dev/null and b/lib/src/assets/flags/bq.png differ diff --git a/lib/src/assets/flags/br.png b/lib/src/assets/flags/br.png new file mode 100644 index 00000000..8b18eb2c Binary files /dev/null and b/lib/src/assets/flags/br.png differ diff --git a/lib/src/assets/flags/bs.png b/lib/src/assets/flags/bs.png new file mode 100644 index 00000000..6bdc618b Binary files /dev/null and b/lib/src/assets/flags/bs.png differ diff --git a/lib/src/assets/flags/bt.png b/lib/src/assets/flags/bt.png new file mode 100644 index 00000000..013a6a35 Binary files /dev/null and b/lib/src/assets/flags/bt.png differ diff --git a/lib/src/assets/flags/bv.png b/lib/src/assets/flags/bv.png new file mode 100644 index 00000000..b7a42f40 Binary files /dev/null and b/lib/src/assets/flags/bv.png differ diff --git a/lib/src/assets/flags/bw.png b/lib/src/assets/flags/bw.png new file mode 100644 index 00000000..04446c45 Binary files /dev/null and b/lib/src/assets/flags/bw.png differ diff --git a/lib/src/assets/flags/by.png b/lib/src/assets/flags/by.png new file mode 100644 index 00000000..5633bdd4 Binary files /dev/null and b/lib/src/assets/flags/by.png differ diff --git a/lib/src/assets/flags/bz.png b/lib/src/assets/flags/bz.png new file mode 100644 index 00000000..0ea4c76e Binary files /dev/null and b/lib/src/assets/flags/bz.png differ diff --git a/lib/src/assets/flags/ca.png b/lib/src/assets/flags/ca.png new file mode 100644 index 00000000..5c7340a6 Binary files /dev/null and b/lib/src/assets/flags/ca.png differ diff --git a/lib/src/assets/flags/cc.png b/lib/src/assets/flags/cc.png new file mode 100644 index 00000000..91914305 Binary files /dev/null and b/lib/src/assets/flags/cc.png differ diff --git a/lib/src/assets/flags/cd.png b/lib/src/assets/flags/cd.png new file mode 100644 index 00000000..7e85a3dd Binary files /dev/null and b/lib/src/assets/flags/cd.png differ diff --git a/lib/src/assets/flags/cf.png b/lib/src/assets/flags/cf.png new file mode 100644 index 00000000..6fdd1dc8 Binary files /dev/null and b/lib/src/assets/flags/cf.png differ diff --git a/lib/src/assets/flags/cg.png b/lib/src/assets/flags/cg.png new file mode 100644 index 00000000..79d62cf3 Binary files /dev/null and b/lib/src/assets/flags/cg.png differ diff --git a/lib/src/assets/flags/ch.png b/lib/src/assets/flags/ch.png new file mode 100644 index 00000000..031d1f7a Binary files /dev/null and b/lib/src/assets/flags/ch.png differ diff --git a/lib/src/assets/flags/ci.png b/lib/src/assets/flags/ci.png new file mode 100644 index 00000000..97a79e2b Binary files /dev/null and b/lib/src/assets/flags/ci.png differ diff --git a/lib/src/assets/flags/ck.png b/lib/src/assets/flags/ck.png new file mode 100644 index 00000000..8c1d086d Binary files /dev/null and b/lib/src/assets/flags/ck.png differ diff --git a/lib/src/assets/flags/cl.png b/lib/src/assets/flags/cl.png new file mode 100644 index 00000000..2bd971db Binary files /dev/null and b/lib/src/assets/flags/cl.png differ diff --git a/lib/src/assets/flags/cm.png b/lib/src/assets/flags/cm.png new file mode 100644 index 00000000..fa35a326 Binary files /dev/null and b/lib/src/assets/flags/cm.png differ diff --git a/lib/src/assets/flags/cn.png b/lib/src/assets/flags/cn.png new file mode 100644 index 00000000..ca5a8b39 Binary files /dev/null and b/lib/src/assets/flags/cn.png differ diff --git a/lib/src/assets/flags/co.png b/lib/src/assets/flags/co.png new file mode 100644 index 00000000..6fcfd962 Binary files /dev/null and b/lib/src/assets/flags/co.png differ diff --git a/lib/src/assets/flags/cr.png b/lib/src/assets/flags/cr.png new file mode 100644 index 00000000..d4a25c27 Binary files /dev/null and b/lib/src/assets/flags/cr.png differ diff --git a/lib/src/assets/flags/cu.png b/lib/src/assets/flags/cu.png new file mode 100644 index 00000000..000b7736 Binary files /dev/null and b/lib/src/assets/flags/cu.png differ diff --git a/lib/src/assets/flags/cv.png b/lib/src/assets/flags/cv.png new file mode 100644 index 00000000..1684576e Binary files /dev/null and b/lib/src/assets/flags/cv.png differ diff --git a/lib/src/assets/flags/cw.png b/lib/src/assets/flags/cw.png new file mode 100644 index 00000000..aa7b8c85 Binary files /dev/null and b/lib/src/assets/flags/cw.png differ diff --git a/lib/src/assets/flags/cx.png b/lib/src/assets/flags/cx.png new file mode 100644 index 00000000..826b2309 Binary files /dev/null and b/lib/src/assets/flags/cx.png differ diff --git a/lib/src/assets/flags/cy.png b/lib/src/assets/flags/cy.png new file mode 100644 index 00000000..39055d7b Binary files /dev/null and b/lib/src/assets/flags/cy.png differ diff --git a/lib/src/assets/flags/cz.png b/lib/src/assets/flags/cz.png new file mode 100644 index 00000000..8c679f21 Binary files /dev/null and b/lib/src/assets/flags/cz.png differ diff --git a/lib/src/assets/flags/de.png b/lib/src/assets/flags/de.png new file mode 100644 index 00000000..f078fdfe Binary files /dev/null and b/lib/src/assets/flags/de.png differ diff --git a/lib/src/assets/flags/dj.png b/lib/src/assets/flags/dj.png new file mode 100644 index 00000000..64ab04e2 Binary files /dev/null and b/lib/src/assets/flags/dj.png differ diff --git a/lib/src/assets/flags/dk.png b/lib/src/assets/flags/dk.png new file mode 100644 index 00000000..5f907cee Binary files /dev/null and b/lib/src/assets/flags/dk.png differ diff --git a/lib/src/assets/flags/dm.png b/lib/src/assets/flags/dm.png new file mode 100644 index 00000000..307e3742 Binary files /dev/null and b/lib/src/assets/flags/dm.png differ diff --git a/lib/src/assets/flags/do.png b/lib/src/assets/flags/do.png new file mode 100644 index 00000000..16160a37 Binary files /dev/null and b/lib/src/assets/flags/do.png differ diff --git a/lib/src/assets/flags/dz.png b/lib/src/assets/flags/dz.png new file mode 100644 index 00000000..0de12565 Binary files /dev/null and b/lib/src/assets/flags/dz.png differ diff --git a/lib/src/assets/flags/ec.png b/lib/src/assets/flags/ec.png new file mode 100644 index 00000000..96d84f79 Binary files /dev/null and b/lib/src/assets/flags/ec.png differ diff --git a/lib/src/assets/flags/ee.png b/lib/src/assets/flags/ee.png new file mode 100644 index 00000000..56aed18a Binary files /dev/null and b/lib/src/assets/flags/ee.png differ diff --git a/lib/src/assets/flags/eg.png b/lib/src/assets/flags/eg.png new file mode 100644 index 00000000..a5320a6b Binary files /dev/null and b/lib/src/assets/flags/eg.png differ diff --git a/lib/src/assets/flags/eh.png b/lib/src/assets/flags/eh.png new file mode 100644 index 00000000..24bdb355 Binary files /dev/null and b/lib/src/assets/flags/eh.png differ diff --git a/lib/src/assets/flags/er.png b/lib/src/assets/flags/er.png new file mode 100644 index 00000000..bd47590b Binary files /dev/null and b/lib/src/assets/flags/er.png differ diff --git a/lib/src/assets/flags/es.png b/lib/src/assets/flags/es.png new file mode 100644 index 00000000..23e71412 Binary files /dev/null and b/lib/src/assets/flags/es.png differ diff --git a/lib/src/assets/flags/et.png b/lib/src/assets/flags/et.png new file mode 100644 index 00000000..258e32d2 Binary files /dev/null and b/lib/src/assets/flags/et.png differ diff --git a/lib/src/assets/flags/eu.png b/lib/src/assets/flags/eu.png new file mode 100644 index 00000000..0018b6cf Binary files /dev/null and b/lib/src/assets/flags/eu.png differ diff --git a/lib/src/assets/flags/fi.png b/lib/src/assets/flags/fi.png new file mode 100644 index 00000000..7c1f9087 Binary files /dev/null and b/lib/src/assets/flags/fi.png differ diff --git a/lib/src/assets/flags/fj.png b/lib/src/assets/flags/fj.png new file mode 100644 index 00000000..6d9a4e18 Binary files /dev/null and b/lib/src/assets/flags/fj.png differ diff --git a/lib/src/assets/flags/fk.png b/lib/src/assets/flags/fk.png new file mode 100644 index 00000000..fc9b5db4 Binary files /dev/null and b/lib/src/assets/flags/fk.png differ diff --git a/lib/src/assets/flags/fm.png b/lib/src/assets/flags/fm.png new file mode 100644 index 00000000..8f5b1503 Binary files /dev/null and b/lib/src/assets/flags/fm.png differ diff --git a/lib/src/assets/flags/fo.png b/lib/src/assets/flags/fo.png new file mode 100644 index 00000000..6a1e0480 Binary files /dev/null and b/lib/src/assets/flags/fo.png differ diff --git a/lib/src/assets/flags/fr.png b/lib/src/assets/flags/fr.png new file mode 100644 index 00000000..6a196cb7 Binary files /dev/null and b/lib/src/assets/flags/fr.png differ diff --git a/lib/src/assets/flags/ga.png b/lib/src/assets/flags/ga.png new file mode 100644 index 00000000..dcade005 Binary files /dev/null and b/lib/src/assets/flags/ga.png differ diff --git a/lib/src/assets/flags/gb-eng.png b/lib/src/assets/flags/gb-eng.png new file mode 100644 index 00000000..b534e628 Binary files /dev/null and b/lib/src/assets/flags/gb-eng.png differ diff --git a/lib/src/assets/flags/gb-nir.png b/lib/src/assets/flags/gb-nir.png new file mode 100644 index 00000000..8a020a8c Binary files /dev/null and b/lib/src/assets/flags/gb-nir.png differ diff --git a/lib/src/assets/flags/gb-sct.png b/lib/src/assets/flags/gb-sct.png new file mode 100644 index 00000000..7f5f28f4 Binary files /dev/null and b/lib/src/assets/flags/gb-sct.png differ diff --git a/lib/src/assets/flags/gb-wls.png b/lib/src/assets/flags/gb-wls.png new file mode 100644 index 00000000..9571ac5e Binary files /dev/null and b/lib/src/assets/flags/gb-wls.png differ diff --git a/lib/src/assets/flags/gb.png b/lib/src/assets/flags/gb.png new file mode 100644 index 00000000..23e15749 Binary files /dev/null and b/lib/src/assets/flags/gb.png differ diff --git a/lib/src/assets/flags/gd.png b/lib/src/assets/flags/gd.png new file mode 100644 index 00000000..2c070dd7 Binary files /dev/null and b/lib/src/assets/flags/gd.png differ diff --git a/lib/src/assets/flags/ge.png b/lib/src/assets/flags/ge.png new file mode 100644 index 00000000..1cc52042 Binary files /dev/null and b/lib/src/assets/flags/ge.png differ diff --git a/lib/src/assets/flags/gf.png b/lib/src/assets/flags/gf.png new file mode 100644 index 00000000..54c2d5e9 Binary files /dev/null and b/lib/src/assets/flags/gf.png differ diff --git a/lib/src/assets/flags/gg.png b/lib/src/assets/flags/gg.png new file mode 100644 index 00000000..e7343041 Binary files /dev/null and b/lib/src/assets/flags/gg.png differ diff --git a/lib/src/assets/flags/gh.png b/lib/src/assets/flags/gh.png new file mode 100644 index 00000000..a8ef8c45 Binary files /dev/null and b/lib/src/assets/flags/gh.png differ diff --git a/lib/src/assets/flags/gi.png b/lib/src/assets/flags/gi.png new file mode 100644 index 00000000..ca2b8daf Binary files /dev/null and b/lib/src/assets/flags/gi.png differ diff --git a/lib/src/assets/flags/gl.png b/lib/src/assets/flags/gl.png new file mode 100644 index 00000000..3a8ce02a Binary files /dev/null and b/lib/src/assets/flags/gl.png differ diff --git a/lib/src/assets/flags/gm.png b/lib/src/assets/flags/gm.png new file mode 100644 index 00000000..2a70b15d Binary files /dev/null and b/lib/src/assets/flags/gm.png differ diff --git a/lib/src/assets/flags/gn.png b/lib/src/assets/flags/gn.png new file mode 100644 index 00000000..609fd664 Binary files /dev/null and b/lib/src/assets/flags/gn.png differ diff --git a/lib/src/assets/flags/gp.png b/lib/src/assets/flags/gp.png new file mode 100644 index 00000000..6a196cb7 Binary files /dev/null and b/lib/src/assets/flags/gp.png differ diff --git a/lib/src/assets/flags/gq.png b/lib/src/assets/flags/gq.png new file mode 100644 index 00000000..60a82d9c Binary files /dev/null and b/lib/src/assets/flags/gq.png differ diff --git a/lib/src/assets/flags/gr.png b/lib/src/assets/flags/gr.png new file mode 100644 index 00000000..ac2f75c5 Binary files /dev/null and b/lib/src/assets/flags/gr.png differ diff --git a/lib/src/assets/flags/gs.png b/lib/src/assets/flags/gs.png new file mode 100644 index 00000000..60d421bd Binary files /dev/null and b/lib/src/assets/flags/gs.png differ diff --git a/lib/src/assets/flags/gt.png b/lib/src/assets/flags/gt.png new file mode 100644 index 00000000..4971a581 Binary files /dev/null and b/lib/src/assets/flags/gt.png differ diff --git a/lib/src/assets/flags/gu.png b/lib/src/assets/flags/gu.png new file mode 100644 index 00000000..cdca8e99 Binary files /dev/null and b/lib/src/assets/flags/gu.png differ diff --git a/lib/src/assets/flags/gw.png b/lib/src/assets/flags/gw.png new file mode 100644 index 00000000..1472f237 Binary files /dev/null and b/lib/src/assets/flags/gw.png differ diff --git a/lib/src/assets/flags/gy.png b/lib/src/assets/flags/gy.png new file mode 100644 index 00000000..36279ec5 Binary files /dev/null and b/lib/src/assets/flags/gy.png differ diff --git a/lib/src/assets/flags/hk.png b/lib/src/assets/flags/hk.png new file mode 100644 index 00000000..92123c54 Binary files /dev/null and b/lib/src/assets/flags/hk.png differ diff --git a/lib/src/assets/flags/hm.png b/lib/src/assets/flags/hm.png new file mode 100644 index 00000000..fff75ea6 Binary files /dev/null and b/lib/src/assets/flags/hm.png differ diff --git a/lib/src/assets/flags/hn.png b/lib/src/assets/flags/hn.png new file mode 100644 index 00000000..99359644 Binary files /dev/null and b/lib/src/assets/flags/hn.png differ diff --git a/lib/src/assets/flags/hr.png b/lib/src/assets/flags/hr.png new file mode 100644 index 00000000..8396e00d Binary files /dev/null and b/lib/src/assets/flags/hr.png differ diff --git a/lib/src/assets/flags/ht.png b/lib/src/assets/flags/ht.png new file mode 100644 index 00000000..43e208b1 Binary files /dev/null and b/lib/src/assets/flags/ht.png differ diff --git a/lib/src/assets/flags/hu.png b/lib/src/assets/flags/hu.png new file mode 100644 index 00000000..d181d5d7 Binary files /dev/null and b/lib/src/assets/flags/hu.png differ diff --git a/lib/src/assets/flags/id.png b/lib/src/assets/flags/id.png new file mode 100644 index 00000000..072bd8c6 Binary files /dev/null and b/lib/src/assets/flags/id.png differ diff --git a/lib/src/assets/flags/ie.png b/lib/src/assets/flags/ie.png new file mode 100644 index 00000000..10fbab9c Binary files /dev/null and b/lib/src/assets/flags/ie.png differ diff --git a/lib/src/assets/flags/il.png b/lib/src/assets/flags/il.png new file mode 100644 index 00000000..32aba13b Binary files /dev/null and b/lib/src/assets/flags/il.png differ diff --git a/lib/src/assets/flags/im.png b/lib/src/assets/flags/im.png new file mode 100644 index 00000000..f54a4640 Binary files /dev/null and b/lib/src/assets/flags/im.png differ diff --git a/lib/src/assets/flags/in.png b/lib/src/assets/flags/in.png new file mode 100644 index 00000000..be3710d7 Binary files /dev/null and b/lib/src/assets/flags/in.png differ diff --git a/lib/src/assets/flags/io.png b/lib/src/assets/flags/io.png new file mode 100644 index 00000000..c7eddc78 Binary files /dev/null and b/lib/src/assets/flags/io.png differ diff --git a/lib/src/assets/flags/iq.png b/lib/src/assets/flags/iq.png new file mode 100644 index 00000000..273672ad Binary files /dev/null and b/lib/src/assets/flags/iq.png differ diff --git a/lib/src/assets/flags/ir.png b/lib/src/assets/flags/ir.png new file mode 100644 index 00000000..e218c838 Binary files /dev/null and b/lib/src/assets/flags/ir.png differ diff --git a/lib/src/assets/flags/is.png b/lib/src/assets/flags/is.png new file mode 100644 index 00000000..ff6cb997 Binary files /dev/null and b/lib/src/assets/flags/is.png differ diff --git a/lib/src/assets/flags/it.png b/lib/src/assets/flags/it.png new file mode 100644 index 00000000..3f5e25b1 Binary files /dev/null and b/lib/src/assets/flags/it.png differ diff --git a/lib/src/assets/flags/je.png b/lib/src/assets/flags/je.png new file mode 100644 index 00000000..0e26cb98 Binary files /dev/null and b/lib/src/assets/flags/je.png differ diff --git a/lib/src/assets/flags/jm.png b/lib/src/assets/flags/jm.png new file mode 100644 index 00000000..4768ee5c Binary files /dev/null and b/lib/src/assets/flags/jm.png differ diff --git a/lib/src/assets/flags/jo.png b/lib/src/assets/flags/jo.png new file mode 100644 index 00000000..d5b39daf Binary files /dev/null and b/lib/src/assets/flags/jo.png differ diff --git a/lib/src/assets/flags/jp.png b/lib/src/assets/flags/jp.png new file mode 100644 index 00000000..253e485e Binary files /dev/null and b/lib/src/assets/flags/jp.png differ diff --git a/lib/src/assets/flags/ke.png b/lib/src/assets/flags/ke.png new file mode 100644 index 00000000..3017b3b7 Binary files /dev/null and b/lib/src/assets/flags/ke.png differ diff --git a/lib/src/assets/flags/kg.png b/lib/src/assets/flags/kg.png new file mode 100644 index 00000000..65111f5a Binary files /dev/null and b/lib/src/assets/flags/kg.png differ diff --git a/lib/src/assets/flags/kh.png b/lib/src/assets/flags/kh.png new file mode 100644 index 00000000..74106c59 Binary files /dev/null and b/lib/src/assets/flags/kh.png differ diff --git a/lib/src/assets/flags/ki.png b/lib/src/assets/flags/ki.png new file mode 100644 index 00000000..cdbac2fd Binary files /dev/null and b/lib/src/assets/flags/ki.png differ diff --git a/lib/src/assets/flags/km.png b/lib/src/assets/flags/km.png new file mode 100644 index 00000000..22d97609 Binary files /dev/null and b/lib/src/assets/flags/km.png differ diff --git a/lib/src/assets/flags/kn.png b/lib/src/assets/flags/kn.png new file mode 100644 index 00000000..be0a5865 Binary files /dev/null and b/lib/src/assets/flags/kn.png differ diff --git a/lib/src/assets/flags/kp.png b/lib/src/assets/flags/kp.png new file mode 100644 index 00000000..1d9ca5e8 Binary files /dev/null and b/lib/src/assets/flags/kp.png differ diff --git a/lib/src/assets/flags/kr.png b/lib/src/assets/flags/kr.png new file mode 100644 index 00000000..fd9b58db Binary files /dev/null and b/lib/src/assets/flags/kr.png differ diff --git a/lib/src/assets/flags/kw.png b/lib/src/assets/flags/kw.png new file mode 100644 index 00000000..a9390619 Binary files /dev/null and b/lib/src/assets/flags/kw.png differ diff --git a/lib/src/assets/flags/ky.png b/lib/src/assets/flags/ky.png new file mode 100644 index 00000000..47c5580f Binary files /dev/null and b/lib/src/assets/flags/ky.png differ diff --git a/lib/src/assets/flags/kz.png b/lib/src/assets/flags/kz.png new file mode 100644 index 00000000..b3bb7f0c Binary files /dev/null and b/lib/src/assets/flags/kz.png differ diff --git a/lib/src/assets/flags/la.png b/lib/src/assets/flags/la.png new file mode 100644 index 00000000..006e38ff Binary files /dev/null and b/lib/src/assets/flags/la.png differ diff --git a/lib/src/assets/flags/lb.png b/lib/src/assets/flags/lb.png new file mode 100644 index 00000000..a2d62a8e Binary files /dev/null and b/lib/src/assets/flags/lb.png differ diff --git a/lib/src/assets/flags/lc.png b/lib/src/assets/flags/lc.png new file mode 100644 index 00000000..40948b60 Binary files /dev/null and b/lib/src/assets/flags/lc.png differ diff --git a/lib/src/assets/flags/li.png b/lib/src/assets/flags/li.png new file mode 100644 index 00000000..8d73ed84 Binary files /dev/null and b/lib/src/assets/flags/li.png differ diff --git a/lib/src/assets/flags/lk.png b/lib/src/assets/flags/lk.png new file mode 100644 index 00000000..fb627380 Binary files /dev/null and b/lib/src/assets/flags/lk.png differ diff --git a/lib/src/assets/flags/lr.png b/lib/src/assets/flags/lr.png new file mode 100644 index 00000000..b20d7f0e Binary files /dev/null and b/lib/src/assets/flags/lr.png differ diff --git a/lib/src/assets/flags/ls.png b/lib/src/assets/flags/ls.png new file mode 100644 index 00000000..75d9ce35 Binary files /dev/null and b/lib/src/assets/flags/ls.png differ diff --git a/lib/src/assets/flags/lt.png b/lib/src/assets/flags/lt.png new file mode 100644 index 00000000..c8bed373 Binary files /dev/null and b/lib/src/assets/flags/lt.png differ diff --git a/lib/src/assets/flags/lu.png b/lib/src/assets/flags/lu.png new file mode 100644 index 00000000..0937120d Binary files /dev/null and b/lib/src/assets/flags/lu.png differ diff --git a/lib/src/assets/flags/lv.png b/lib/src/assets/flags/lv.png new file mode 100644 index 00000000..5447b3f4 Binary files /dev/null and b/lib/src/assets/flags/lv.png differ diff --git a/lib/src/assets/flags/ly.png b/lib/src/assets/flags/ly.png new file mode 100644 index 00000000..d898e496 Binary files /dev/null and b/lib/src/assets/flags/ly.png differ diff --git a/lib/src/assets/flags/ma.png b/lib/src/assets/flags/ma.png new file mode 100644 index 00000000..a0e02907 Binary files /dev/null and b/lib/src/assets/flags/ma.png differ diff --git a/lib/src/assets/flags/mc.png b/lib/src/assets/flags/mc.png new file mode 100644 index 00000000..f40ca3ef Binary files /dev/null and b/lib/src/assets/flags/mc.png differ diff --git a/lib/src/assets/flags/md.png b/lib/src/assets/flags/md.png new file mode 100644 index 00000000..f8408e24 Binary files /dev/null and b/lib/src/assets/flags/md.png differ diff --git a/lib/src/assets/flags/me.png b/lib/src/assets/flags/me.png new file mode 100644 index 00000000..1d568555 Binary files /dev/null and b/lib/src/assets/flags/me.png differ diff --git a/lib/src/assets/flags/mf.png b/lib/src/assets/flags/mf.png new file mode 100644 index 00000000..6a196cb7 Binary files /dev/null and b/lib/src/assets/flags/mf.png differ diff --git a/lib/src/assets/flags/mg.png b/lib/src/assets/flags/mg.png new file mode 100644 index 00000000..21ccba36 Binary files /dev/null and b/lib/src/assets/flags/mg.png differ diff --git a/lib/src/assets/flags/mh.png b/lib/src/assets/flags/mh.png new file mode 100644 index 00000000..e7f88ed2 Binary files /dev/null and b/lib/src/assets/flags/mh.png differ diff --git a/lib/src/assets/flags/mk.png b/lib/src/assets/flags/mk.png new file mode 100644 index 00000000..e670919c Binary files /dev/null and b/lib/src/assets/flags/mk.png differ diff --git a/lib/src/assets/flags/ml.png b/lib/src/assets/flags/ml.png new file mode 100644 index 00000000..f0e9990f Binary files /dev/null and b/lib/src/assets/flags/ml.png differ diff --git a/lib/src/assets/flags/mm.png b/lib/src/assets/flags/mm.png new file mode 100644 index 00000000..09363b3f Binary files /dev/null and b/lib/src/assets/flags/mm.png differ diff --git a/lib/src/assets/flags/mn.png b/lib/src/assets/flags/mn.png new file mode 100644 index 00000000..0d30b3e6 Binary files /dev/null and b/lib/src/assets/flags/mn.png differ diff --git a/lib/src/assets/flags/mo.png b/lib/src/assets/flags/mo.png new file mode 100644 index 00000000..4a731ce4 Binary files /dev/null and b/lib/src/assets/flags/mo.png differ diff --git a/lib/src/assets/flags/mp.png b/lib/src/assets/flags/mp.png new file mode 100644 index 00000000..a7aa3cd2 Binary files /dev/null and b/lib/src/assets/flags/mp.png differ diff --git a/lib/src/assets/flags/mq.png b/lib/src/assets/flags/mq.png new file mode 100644 index 00000000..2d396eac Binary files /dev/null and b/lib/src/assets/flags/mq.png differ diff --git a/lib/src/assets/flags/mr.png b/lib/src/assets/flags/mr.png new file mode 100644 index 00000000..cc1960fa Binary files /dev/null and b/lib/src/assets/flags/mr.png differ diff --git a/lib/src/assets/flags/ms.png b/lib/src/assets/flags/ms.png new file mode 100644 index 00000000..e51480c7 Binary files /dev/null and b/lib/src/assets/flags/ms.png differ diff --git a/lib/src/assets/flags/mt.png b/lib/src/assets/flags/mt.png new file mode 100644 index 00000000..18ec0710 Binary files /dev/null and b/lib/src/assets/flags/mt.png differ diff --git a/lib/src/assets/flags/mu.png b/lib/src/assets/flags/mu.png new file mode 100644 index 00000000..b4165462 Binary files /dev/null and b/lib/src/assets/flags/mu.png differ diff --git a/lib/src/assets/flags/mv.png b/lib/src/assets/flags/mv.png new file mode 100644 index 00000000..a011b782 Binary files /dev/null and b/lib/src/assets/flags/mv.png differ diff --git a/lib/src/assets/flags/mw.png b/lib/src/assets/flags/mw.png new file mode 100644 index 00000000..58219771 Binary files /dev/null and b/lib/src/assets/flags/mw.png differ diff --git a/lib/src/assets/flags/mx.png b/lib/src/assets/flags/mx.png new file mode 100644 index 00000000..db28a24d Binary files /dev/null and b/lib/src/assets/flags/mx.png differ diff --git a/lib/src/assets/flags/my.png b/lib/src/assets/flags/my.png new file mode 100644 index 00000000..6fe220e9 Binary files /dev/null and b/lib/src/assets/flags/my.png differ diff --git a/lib/src/assets/flags/mz.png b/lib/src/assets/flags/mz.png new file mode 100644 index 00000000..81c587d0 Binary files /dev/null and b/lib/src/assets/flags/mz.png differ diff --git a/lib/src/assets/flags/na.png b/lib/src/assets/flags/na.png new file mode 100644 index 00000000..1a4f7096 Binary files /dev/null and b/lib/src/assets/flags/na.png differ diff --git a/lib/src/assets/flags/nc.png b/lib/src/assets/flags/nc.png new file mode 100644 index 00000000..a0ecb850 Binary files /dev/null and b/lib/src/assets/flags/nc.png differ diff --git a/lib/src/assets/flags/ne.png b/lib/src/assets/flags/ne.png new file mode 100644 index 00000000..fa8859a5 Binary files /dev/null and b/lib/src/assets/flags/ne.png differ diff --git a/lib/src/assets/flags/nf.png b/lib/src/assets/flags/nf.png new file mode 100644 index 00000000..3c94505b Binary files /dev/null and b/lib/src/assets/flags/nf.png differ diff --git a/lib/src/assets/flags/ng.png b/lib/src/assets/flags/ng.png new file mode 100644 index 00000000..fd111eb0 Binary files /dev/null and b/lib/src/assets/flags/ng.png differ diff --git a/lib/src/assets/flags/ni.png b/lib/src/assets/flags/ni.png new file mode 100644 index 00000000..fa964780 Binary files /dev/null and b/lib/src/assets/flags/ni.png differ diff --git a/lib/src/assets/flags/nl.png b/lib/src/assets/flags/nl.png new file mode 100644 index 00000000..f545dc87 Binary files /dev/null and b/lib/src/assets/flags/nl.png differ diff --git a/lib/src/assets/flags/no.png b/lib/src/assets/flags/no.png new file mode 100644 index 00000000..b7a42f40 Binary files /dev/null and b/lib/src/assets/flags/no.png differ diff --git a/lib/src/assets/flags/np.png b/lib/src/assets/flags/np.png new file mode 100644 index 00000000..7f2ffbed Binary files /dev/null and b/lib/src/assets/flags/np.png differ diff --git a/lib/src/assets/flags/nr.png b/lib/src/assets/flags/nr.png new file mode 100644 index 00000000..abaae232 Binary files /dev/null and b/lib/src/assets/flags/nr.png differ diff --git a/lib/src/assets/flags/nu.png b/lib/src/assets/flags/nu.png new file mode 100644 index 00000000..2aa41ac5 Binary files /dev/null and b/lib/src/assets/flags/nu.png differ diff --git a/lib/src/assets/flags/nz.png b/lib/src/assets/flags/nz.png new file mode 100644 index 00000000..c8263c30 Binary files /dev/null and b/lib/src/assets/flags/nz.png differ diff --git a/lib/src/assets/flags/om.png b/lib/src/assets/flags/om.png new file mode 100644 index 00000000..4945850b Binary files /dev/null and b/lib/src/assets/flags/om.png differ diff --git a/lib/src/assets/flags/pa.png b/lib/src/assets/flags/pa.png new file mode 100644 index 00000000..a3e57a55 Binary files /dev/null and b/lib/src/assets/flags/pa.png differ diff --git a/lib/src/assets/flags/pe.png b/lib/src/assets/flags/pe.png new file mode 100644 index 00000000..2bed96a3 Binary files /dev/null and b/lib/src/assets/flags/pe.png differ diff --git a/lib/src/assets/flags/pf.png b/lib/src/assets/flags/pf.png new file mode 100644 index 00000000..9bf2ac49 Binary files /dev/null and b/lib/src/assets/flags/pf.png differ diff --git a/lib/src/assets/flags/pg.png b/lib/src/assets/flags/pg.png new file mode 100644 index 00000000..c18cc5cb Binary files /dev/null and b/lib/src/assets/flags/pg.png differ diff --git a/lib/src/assets/flags/ph.png b/lib/src/assets/flags/ph.png new file mode 100644 index 00000000..6f73a4f3 Binary files /dev/null and b/lib/src/assets/flags/ph.png differ diff --git a/lib/src/assets/flags/pk.png b/lib/src/assets/flags/pk.png new file mode 100644 index 00000000..b2355db5 Binary files /dev/null and b/lib/src/assets/flags/pk.png differ diff --git a/lib/src/assets/flags/pl.png b/lib/src/assets/flags/pl.png new file mode 100644 index 00000000..e335edb7 Binary files /dev/null and b/lib/src/assets/flags/pl.png differ diff --git a/lib/src/assets/flags/pm.png b/lib/src/assets/flags/pm.png new file mode 100644 index 00000000..6a196cb7 Binary files /dev/null and b/lib/src/assets/flags/pm.png differ diff --git a/lib/src/assets/flags/pn.png b/lib/src/assets/flags/pn.png new file mode 100644 index 00000000..65acfc19 Binary files /dev/null and b/lib/src/assets/flags/pn.png differ diff --git a/lib/src/assets/flags/pr.png b/lib/src/assets/flags/pr.png new file mode 100644 index 00000000..37f3b647 Binary files /dev/null and b/lib/src/assets/flags/pr.png differ diff --git a/lib/src/assets/flags/ps.png b/lib/src/assets/flags/ps.png new file mode 100644 index 00000000..cac9ec76 Binary files /dev/null and b/lib/src/assets/flags/ps.png differ diff --git a/lib/src/assets/flags/pt.png b/lib/src/assets/flags/pt.png new file mode 100644 index 00000000..56379a61 Binary files /dev/null and b/lib/src/assets/flags/pt.png differ diff --git a/lib/src/assets/flags/pw.png b/lib/src/assets/flags/pw.png new file mode 100644 index 00000000..41b95b88 Binary files /dev/null and b/lib/src/assets/flags/pw.png differ diff --git a/lib/src/assets/flags/py.png b/lib/src/assets/flags/py.png new file mode 100644 index 00000000..f9d7cc94 Binary files /dev/null and b/lib/src/assets/flags/py.png differ diff --git a/lib/src/assets/flags/qa.png b/lib/src/assets/flags/qa.png new file mode 100644 index 00000000..93f3eda7 Binary files /dev/null and b/lib/src/assets/flags/qa.png differ diff --git a/lib/src/assets/flags/re.png b/lib/src/assets/flags/re.png new file mode 100644 index 00000000..6a196cb7 Binary files /dev/null and b/lib/src/assets/flags/re.png differ diff --git a/lib/src/assets/flags/ro.png b/lib/src/assets/flags/ro.png new file mode 100644 index 00000000..b8c25cec Binary files /dev/null and b/lib/src/assets/flags/ro.png differ diff --git a/lib/src/assets/flags/rs.png b/lib/src/assets/flags/rs.png new file mode 100644 index 00000000..1c93130f Binary files /dev/null and b/lib/src/assets/flags/rs.png differ diff --git a/lib/src/assets/flags/ru.png b/lib/src/assets/flags/ru.png new file mode 100644 index 00000000..9739ab6e Binary files /dev/null and b/lib/src/assets/flags/ru.png differ diff --git a/lib/src/assets/flags/rw.png b/lib/src/assets/flags/rw.png new file mode 100644 index 00000000..c01b9a65 Binary files /dev/null and b/lib/src/assets/flags/rw.png differ diff --git a/lib/src/assets/flags/sa.png b/lib/src/assets/flags/sa.png new file mode 100644 index 00000000..c1b1f70d Binary files /dev/null and b/lib/src/assets/flags/sa.png differ diff --git a/lib/src/assets/flags/sb.png b/lib/src/assets/flags/sb.png new file mode 100644 index 00000000..ef529e13 Binary files /dev/null and b/lib/src/assets/flags/sb.png differ diff --git a/lib/src/assets/flags/sc.png b/lib/src/assets/flags/sc.png new file mode 100644 index 00000000..a743a489 Binary files /dev/null and b/lib/src/assets/flags/sc.png differ diff --git a/lib/src/assets/flags/sd.png b/lib/src/assets/flags/sd.png new file mode 100644 index 00000000..117ad21f Binary files /dev/null and b/lib/src/assets/flags/sd.png differ diff --git a/lib/src/assets/flags/se.png b/lib/src/assets/flags/se.png new file mode 100644 index 00000000..b6fcc628 Binary files /dev/null and b/lib/src/assets/flags/se.png differ diff --git a/lib/src/assets/flags/sg.png b/lib/src/assets/flags/sg.png new file mode 100644 index 00000000..65a0422f Binary files /dev/null and b/lib/src/assets/flags/sg.png differ diff --git a/lib/src/assets/flags/sh.png b/lib/src/assets/flags/sh.png new file mode 100644 index 00000000..f77da1f2 Binary files /dev/null and b/lib/src/assets/flags/sh.png differ diff --git a/lib/src/assets/flags/si.png b/lib/src/assets/flags/si.png new file mode 100644 index 00000000..a837d2c4 Binary files /dev/null and b/lib/src/assets/flags/si.png differ diff --git a/lib/src/assets/flags/sj.png b/lib/src/assets/flags/sj.png new file mode 100644 index 00000000..b7a42f40 Binary files /dev/null and b/lib/src/assets/flags/sj.png differ diff --git a/lib/src/assets/flags/sk.png b/lib/src/assets/flags/sk.png new file mode 100644 index 00000000..5ed13487 Binary files /dev/null and b/lib/src/assets/flags/sk.png differ diff --git a/lib/src/assets/flags/sl.png b/lib/src/assets/flags/sl.png new file mode 100644 index 00000000..4f8d54ee Binary files /dev/null and b/lib/src/assets/flags/sl.png differ diff --git a/lib/src/assets/flags/sm.png b/lib/src/assets/flags/sm.png new file mode 100644 index 00000000..7ce47152 Binary files /dev/null and b/lib/src/assets/flags/sm.png differ diff --git a/lib/src/assets/flags/sn.png b/lib/src/assets/flags/sn.png new file mode 100644 index 00000000..719c1ef3 Binary files /dev/null and b/lib/src/assets/flags/sn.png differ diff --git a/lib/src/assets/flags/so.png b/lib/src/assets/flags/so.png new file mode 100644 index 00000000..6e784b1a Binary files /dev/null and b/lib/src/assets/flags/so.png differ diff --git a/lib/src/assets/flags/sr.png b/lib/src/assets/flags/sr.png new file mode 100644 index 00000000..9a054f01 Binary files /dev/null and b/lib/src/assets/flags/sr.png differ diff --git a/lib/src/assets/flags/ss.png b/lib/src/assets/flags/ss.png new file mode 100644 index 00000000..58338832 Binary files /dev/null and b/lib/src/assets/flags/ss.png differ diff --git a/lib/src/assets/flags/st.png b/lib/src/assets/flags/st.png new file mode 100644 index 00000000..133edbc0 Binary files /dev/null and b/lib/src/assets/flags/st.png differ diff --git a/lib/src/assets/flags/sv.png b/lib/src/assets/flags/sv.png new file mode 100644 index 00000000..acd4a36d Binary files /dev/null and b/lib/src/assets/flags/sv.png differ diff --git a/lib/src/assets/flags/sx.png b/lib/src/assets/flags/sx.png new file mode 100644 index 00000000..3e347b01 Binary files /dev/null and b/lib/src/assets/flags/sx.png differ diff --git a/lib/src/assets/flags/sy.png b/lib/src/assets/flags/sy.png new file mode 100644 index 00000000..45655a59 Binary files /dev/null and b/lib/src/assets/flags/sy.png differ diff --git a/lib/src/assets/flags/sz.png b/lib/src/assets/flags/sz.png new file mode 100644 index 00000000..5b413eb2 Binary files /dev/null and b/lib/src/assets/flags/sz.png differ diff --git a/lib/src/assets/flags/tc.png b/lib/src/assets/flags/tc.png new file mode 100644 index 00000000..698a4cd0 Binary files /dev/null and b/lib/src/assets/flags/tc.png differ diff --git a/lib/src/assets/flags/td.png b/lib/src/assets/flags/td.png new file mode 100644 index 00000000..b835dc2c Binary files /dev/null and b/lib/src/assets/flags/td.png differ diff --git a/lib/src/assets/flags/tf.png b/lib/src/assets/flags/tf.png new file mode 100644 index 00000000..5189a18f Binary files /dev/null and b/lib/src/assets/flags/tf.png differ diff --git a/lib/src/assets/flags/tg.png b/lib/src/assets/flags/tg.png new file mode 100644 index 00000000..028167aa Binary files /dev/null and b/lib/src/assets/flags/tg.png differ diff --git a/lib/src/assets/flags/th.png b/lib/src/assets/flags/th.png new file mode 100644 index 00000000..cd0af74d Binary files /dev/null and b/lib/src/assets/flags/th.png differ diff --git a/lib/src/assets/flags/tj.png b/lib/src/assets/flags/tj.png new file mode 100644 index 00000000..6cb33397 Binary files /dev/null and b/lib/src/assets/flags/tj.png differ diff --git a/lib/src/assets/flags/tk.png b/lib/src/assets/flags/tk.png new file mode 100644 index 00000000..a960307b Binary files /dev/null and b/lib/src/assets/flags/tk.png differ diff --git a/lib/src/assets/flags/tl.png b/lib/src/assets/flags/tl.png new file mode 100644 index 00000000..ba934170 Binary files /dev/null and b/lib/src/assets/flags/tl.png differ diff --git a/lib/src/assets/flags/tm.png b/lib/src/assets/flags/tm.png new file mode 100644 index 00000000..b46f2a23 Binary files /dev/null and b/lib/src/assets/flags/tm.png differ diff --git a/lib/src/assets/flags/tn.png b/lib/src/assets/flags/tn.png new file mode 100644 index 00000000..0d6a976c Binary files /dev/null and b/lib/src/assets/flags/tn.png differ diff --git a/lib/src/assets/flags/to.png b/lib/src/assets/flags/to.png new file mode 100644 index 00000000..ab11e514 Binary files /dev/null and b/lib/src/assets/flags/to.png differ diff --git a/lib/src/assets/flags/tr.png b/lib/src/assets/flags/tr.png new file mode 100644 index 00000000..2b0614ca Binary files /dev/null and b/lib/src/assets/flags/tr.png differ diff --git a/lib/src/assets/flags/tt.png b/lib/src/assets/flags/tt.png new file mode 100644 index 00000000..9b4575fb Binary files /dev/null and b/lib/src/assets/flags/tt.png differ diff --git a/lib/src/assets/flags/tv.png b/lib/src/assets/flags/tv.png new file mode 100644 index 00000000..8d68ead6 Binary files /dev/null and b/lib/src/assets/flags/tv.png differ diff --git a/lib/src/assets/flags/tw.png b/lib/src/assets/flags/tw.png new file mode 100644 index 00000000..ec71c10b Binary files /dev/null and b/lib/src/assets/flags/tw.png differ diff --git a/lib/src/assets/flags/tz.png b/lib/src/assets/flags/tz.png new file mode 100644 index 00000000..0c67a2a0 Binary files /dev/null and b/lib/src/assets/flags/tz.png differ diff --git a/lib/src/assets/flags/ua.png b/lib/src/assets/flags/ua.png new file mode 100644 index 00000000..42b2cde9 Binary files /dev/null and b/lib/src/assets/flags/ua.png differ diff --git a/lib/src/assets/flags/ug.png b/lib/src/assets/flags/ug.png new file mode 100644 index 00000000..4d040f83 Binary files /dev/null and b/lib/src/assets/flags/ug.png differ diff --git a/lib/src/assets/flags/um.png b/lib/src/assets/flags/um.png new file mode 100644 index 00000000..ed2d8d0e Binary files /dev/null and b/lib/src/assets/flags/um.png differ diff --git a/lib/src/assets/flags/us.png b/lib/src/assets/flags/us.png new file mode 100644 index 00000000..4e47eaaf Binary files /dev/null and b/lib/src/assets/flags/us.png differ diff --git a/lib/src/assets/flags/uy.png b/lib/src/assets/flags/uy.png new file mode 100644 index 00000000..7b5670b4 Binary files /dev/null and b/lib/src/assets/flags/uy.png differ diff --git a/lib/src/assets/flags/uz.png b/lib/src/assets/flags/uz.png new file mode 100644 index 00000000..97b1a533 Binary files /dev/null and b/lib/src/assets/flags/uz.png differ diff --git a/lib/src/assets/flags/va.png b/lib/src/assets/flags/va.png new file mode 100644 index 00000000..dcdc3a72 Binary files /dev/null and b/lib/src/assets/flags/va.png differ diff --git a/lib/src/assets/flags/vc.png b/lib/src/assets/flags/vc.png new file mode 100644 index 00000000..4c13bc2c Binary files /dev/null and b/lib/src/assets/flags/vc.png differ diff --git a/lib/src/assets/flags/ve.png b/lib/src/assets/flags/ve.png new file mode 100644 index 00000000..cc80484c Binary files /dev/null and b/lib/src/assets/flags/ve.png differ diff --git a/lib/src/assets/flags/vg.png b/lib/src/assets/flags/vg.png new file mode 100644 index 00000000..9cd84e23 Binary files /dev/null and b/lib/src/assets/flags/vg.png differ diff --git a/lib/src/assets/flags/vi.png b/lib/src/assets/flags/vi.png new file mode 100644 index 00000000..e9127a84 Binary files /dev/null and b/lib/src/assets/flags/vi.png differ diff --git a/lib/src/assets/flags/vn.png b/lib/src/assets/flags/vn.png new file mode 100644 index 00000000..cbf65d41 Binary files /dev/null and b/lib/src/assets/flags/vn.png differ diff --git a/lib/src/assets/flags/vu.png b/lib/src/assets/flags/vu.png new file mode 100644 index 00000000..58764567 Binary files /dev/null and b/lib/src/assets/flags/vu.png differ diff --git a/lib/src/assets/flags/wf.png b/lib/src/assets/flags/wf.png new file mode 100644 index 00000000..ffd56d47 Binary files /dev/null and b/lib/src/assets/flags/wf.png differ diff --git a/lib/src/assets/flags/ws.png b/lib/src/assets/flags/ws.png new file mode 100644 index 00000000..18c5b866 Binary files /dev/null and b/lib/src/assets/flags/ws.png differ diff --git a/lib/src/assets/flags/xk.png b/lib/src/assets/flags/xk.png new file mode 100644 index 00000000..883b9ea1 Binary files /dev/null and b/lib/src/assets/flags/xk.png differ diff --git a/lib/src/assets/flags/ye.png b/lib/src/assets/flags/ye.png new file mode 100644 index 00000000..c094f80e Binary files /dev/null and b/lib/src/assets/flags/ye.png differ diff --git a/lib/src/assets/flags/yt.png b/lib/src/assets/flags/yt.png new file mode 100644 index 00000000..6a196cb7 Binary files /dev/null and b/lib/src/assets/flags/yt.png differ diff --git a/lib/src/assets/flags/za.png b/lib/src/assets/flags/za.png new file mode 100644 index 00000000..110c48de Binary files /dev/null and b/lib/src/assets/flags/za.png differ diff --git a/lib/src/assets/flags/zm.png b/lib/src/assets/flags/zm.png new file mode 100644 index 00000000..b6470586 Binary files /dev/null and b/lib/src/assets/flags/zm.png differ diff --git a/lib/src/assets/flags/zw.png b/lib/src/assets/flags/zw.png new file mode 100644 index 00000000..f5020279 Binary files /dev/null and b/lib/src/assets/flags/zw.png differ diff --git a/lib/src/components/app_bar/app_bar.dart b/lib/src/components/app_bar/app_bar.dart new file mode 100644 index 00000000..a0debf79 --- /dev/null +++ b/lib/src/components/app_bar/app_bar.dart @@ -0,0 +1,421 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zeta_flutter.dart'; + +/// Zeta app bar. +class ZetaAppBar extends StatefulWidget implements PreferredSizeWidget { + /// Creates a Zeta app bar. + const ZetaAppBar({ + this.actions, + this.automaticallyImplyLeading = true, + this.searchController, + this.leading, + this.title, + this.type = ZetaAppBarType.defaultAppBar, + this.onSearch, + this.searchHintText = 'Search', + this.onSearchMicrophoneIconPressed, + super.key, + }); + + /// Called when text in the search field is submited. + final void Function(String)? onSearch; + + /// A list of Widgets to display in a row after the [title] widget. + final List? actions; + + /// Configures whether the back button to be displayed. + final bool automaticallyImplyLeading; + + /// Widget displayed first in the app bar row. + final Widget? leading; + + /// If omitted the microphone icon won't show up. Called when the icon button is pressed. Normally used for speech recognition/speech to text. + final VoidCallback? onSearchMicrophoneIconPressed; + + /// Used to controll the search textfield and states. + final AppBarSearchController? searchController; + + /// Label used as hint text. + final String searchHintText; + + /// Title of the app bar. Normally a [Text] widget. + final Widget? title; + + /// Defines the styles of the app bar. + final ZetaAppBarType type; + + @override + State createState() => _ZetaAppBarState(); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add( + ObjectFlagProperty.has('onSearch', onSearch), + ) + ..add( + DiagnosticsProperty( + 'automaticallyImplyLeading', + automaticallyImplyLeading, + ), + ) + ..add( + DiagnosticsProperty( + 'searchController', + searchController, + ), + ) + ..add( + ObjectFlagProperty.has( + 'onSearchMicrophoneIconPressed', + onSearchMicrophoneIconPressed, + ), + ) + ..add(StringProperty('searchHintText', searchHintText)) + ..add(EnumProperty('type', type)); + } +} + +class _ZetaAppBarState extends State { + bool _isSearchEnabled = false; + + @override + void initState() { + widget.searchController?.addListener(_onSearchControllerChanged); + super.initState(); + } + + void _onSearchControllerChanged() { + final controller = widget.searchController; + if (controller == null) return; + + setState(() => _isSearchEnabled = controller.isEnabled); + } + + @override + void dispose() { + widget.searchController?.removeListener(_onSearchControllerChanged); + super.dispose(); + } + + Widget? _getTitle() { + return widget.type != ZetaAppBarType.extendedTitle + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: ZetaSpacing.b), + child: widget.title, + ) + : null; + } + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return IconButtonTheme( + data: IconButtonThemeData( + style: IconButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: ZetaSpacing.b), + child: AppBar( + elevation: 0, + iconTheme: IconThemeData(color: colors.cool.shade90), + leadingWidth: ZetaSpacing.x10, + leading: widget.leading, + automaticallyImplyLeading: widget.automaticallyImplyLeading, + centerTitle: widget.type == ZetaAppBarType.centeredTitle, + titleSpacing: 0, + titleTextStyle: ZetaTextStyles.bodyLarge.copyWith( + color: colors.textDefault, + ), + title: widget.searchController != null + ? _SearchField( + searchController: widget.searchController, + hintText: widget.searchHintText, + onSearch: widget.onSearch, + type: widget.type, + child: _getTitle(), + ) + : _getTitle(), + actions: _isSearchEnabled + ? [ + IconButtonTheme( + data: IconButtonThemeData( + style: IconButton.styleFrom( + iconSize: ZetaSpacing.x5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + color: colors.cool.shade50, + onPressed: () => widget.searchController?.clearText(), + icon: const Icon(ZetaIcons.cancel_round), + ), + if (widget.onSearchMicrophoneIconPressed != null) ...[ + SizedBox( + height: ZetaSpacing.m, + child: VerticalDivider( + width: ZetaSpacing.x0_5, + color: colors.cool.shade70, + ), + ), + IconButton( + onPressed: widget.onSearchMicrophoneIconPressed, + icon: const Icon(ZetaIcons.microphone_round), + ), + ], + ], + ), + ), + ] + : widget.actions, + flexibleSpace: widget.type == ZetaAppBarType.extendedTitle + ? Padding( + padding: EdgeInsets.only( + top: widget.preferredSize.height, + left: ZetaSpacing.s, + right: ZetaSpacing.s, + ), + child: DefaultTextStyle( + style: ZetaTextStyles.bodyLarge.copyWith( + color: colors.textDefault, + ), + child: widget.title ?? const SizedBox(), + ), + ) + : null, + ), + ), + ); + } +} + +/// Defines the style of the app bar. +enum ZetaAppBarType { + /// Title positioned on the left side. + defaultAppBar, + + /// Title in the center. + centeredTitle, + + /// Title below the app bar. + extendedTitle, +} + +class _SearchField extends StatefulWidget { + const _SearchField({ + required this.child, + required this.onSearch, + required this.searchController, + required this.hintText, + required this.type, + }); + + final void Function(String value)? onSearch; + final Widget? child; + final String hintText; + final AppBarSearchController? searchController; + final ZetaAppBarType type; + + @override + State<_SearchField> createState() => _SearchFieldState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add( + ObjectFlagProperty.has( + 'onSearch', + onSearch, + ), + ) + ..add(StringProperty('hintText', hintText)) + ..add( + DiagnosticsProperty( + 'searchController', + searchController, + ), + ) + ..add(EnumProperty('type', type)); + } +} + +class _SearchFieldState extends State<_SearchField> with SingleTickerProviderStateMixin { + late final _animationController = AnimationController( + vsync: this, + duration: kThemeAnimationDuration, + ); + + late bool _isSearching = widget.searchController?.isEnabled ?? false; + late final _textFocusNode = FocusNode(); + + @override + void initState() { + _textFocusNode.addListener(_onFocusChanged); + widget.searchController?.addListener(_onSearchControllerChanged); + widget.searchController?.textEditingController ??= TextEditingController(); + + super.initState(); + } + + void _onFocusChanged() { + final text = widget.searchController?.text ?? ''; + final shouldCloseSearch = _isSearching && text.isEmpty && !_textFocusNode.hasFocus; + + if (shouldCloseSearch) _closeSearch(); + } + + void _onSearchControllerChanged() { + final controller = widget.searchController; + if (controller == null) return; + + controller.isEnabled ? _startSearch() : _closeSearch(); + } + + void _setNextSearchState() { + if (!_isSearching) return _startSearch(); + + _closeSearch(); + } + + void _startSearch() { + widget.searchController?.startSearch(); + setState(() => _isSearching = true); + + _animationController.forward(); + FocusScope.of(context).requestFocus(_textFocusNode); + } + + void _closeSearch() { + widget.searchController?.closeSearch(); + setState(() => _isSearching = false); + _animationController.reverse(); + _removeFocus(context); + } + + void _submitSearch() { + widget.onSearch?.call(widget.searchController?.text ?? ''); + widget.searchController?.text = ''; + _closeSearch(); + } + + void _removeFocus(BuildContext context) { + final currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) { + FocusManager.instance.primaryFocus?.unfocus(); + } + } + + @override + void didUpdateWidget(covariant _SearchField oldWidget) { + if (oldWidget.searchController != widget.searchController) { + _setNextSearchState(); + } + + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + _animationController.dispose(); + _textFocusNode.dispose(); + widget.searchController?.removeListener(_onSearchControllerChanged); + widget.searchController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + Row( + mainAxisAlignment: + widget.type == ZetaAppBarType.centeredTitle ? MainAxisAlignment.center : MainAxisAlignment.start, + children: [ + widget.child ?? const SizedBox(), + ], + ), + AnimatedBuilder( + animation: _animationController, + builder: (context, child) => Transform.scale( + scaleX: _animationController.value * 1, + alignment: Alignment.centerRight, + origin: Offset.zero, + child: TextField( + controller: widget.searchController?.textEditingController, + focusNode: _textFocusNode, + style: ZetaTextStyles.bodyMedium, + cursorColor: colors.cool.shade90, + decoration: InputDecoration( + iconColor: colors.cool.shade90, + filled: true, + border: InputBorder.none, + hintStyle: ZetaTextStyles.bodyMedium.copyWith( + color: colors.textDisabled, + ), + hintText: widget.hintText, + ), + onEditingComplete: _submitSearch, + textInputAction: TextInputAction.search, + ), + ), + ), + ], + ); + } +} + +/// Controlls the search. +class AppBarSearchController extends ChangeNotifier { + bool _enabled = false; + + /// Controller used for the search field. + TextEditingController? textEditingController; + + /// Whether the search is currently vissible. + bool get isEnabled => _enabled; + + /// The current text in the search field. + String get text => textEditingController?.text ?? ''; + + /// Displayes text in the search field and overrides the existing. + set text(String text) => textEditingController?.text = text; + + /// Displays the search field over the title in the app bar. + void startSearch() { + if (_enabled) return; + + _enabled = true; + notifyListeners(); + } + + /// Hides the search field from the app bar. + void closeSearch() { + if (!_enabled) return; + + _enabled = false; + notifyListeners(); + } + + /// Removes the text from search field. + void clearText() => textEditingController?.clear(); + + @override + void dispose() { + textEditingController?.dispose(); + super.dispose(); + } +} diff --git a/lib/src/components/buttons/button.dart b/lib/src/components/buttons/button.dart index c6c61700..be2087cb 100644 --- a/lib/src/components/buttons/button.dart +++ b/lib/src/components/buttons/button.dart @@ -12,6 +12,7 @@ class ZetaButton extends StatelessWidget { this.type = ZetaButtonType.primary, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }); @@ -21,6 +22,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.primary; @@ -30,6 +32,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.secondary; @@ -39,6 +42,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.positive; @@ -48,6 +52,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.negative; @@ -57,6 +62,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.outline; @@ -66,6 +72,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.outlineSubtle; @@ -75,6 +82,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.text; @@ -94,6 +102,10 @@ class ZetaButton extends StatelessWidget { /// Size of the button. Defaults to large. final ZetaWidgetSize size; + /// Sometimes we need to pass Zeta from outside, + /// like for example from [showZetaDialog] + final Zeta? zeta; + /// Creates a clone. ZetaButton copyWith({ String? label, @@ -109,13 +121,15 @@ class ZetaButton extends StatelessWidget { type: type ?? this.type, size: size ?? this.size, borderType: borderType ?? this.borderType, + zeta: zeta, key: key ?? this.key, ); } @override Widget build(BuildContext context) { - final colors = Zeta.of(context).colors; + final zeta = this.zeta ?? Zeta.of(context); + final colors = zeta.colors; return ConstrainedBox( constraints: BoxConstraints(minHeight: _minConstraints, minWidth: _minConstraints), child: FilledButton( diff --git a/lib/src/components/date_input/date_input.dart b/lib/src/components/date_input/date_input.dart index 83c07811..aa7a980f 100644 --- a/lib/src/components/date_input/date_input.dart +++ b/lib/src/components/date_input/date_input.dart @@ -4,18 +4,6 @@ import 'package:intl/intl.dart'; import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; import '../../../zeta_flutter.dart'; -/// [ZetaDateInput] size -enum ZetaDateInputSize { - /// [large] 48 pixels height of the input field. - large, - - /// [medium] 40 pixels height of the input field. - medium, - - /// [small] 32 pixels height of the input field. - small, -} - /// ZetaDateInput allows entering date in a pre-defined format. /// Validation is performed to make sure the date is valid /// and is in the proper format. @@ -57,7 +45,7 @@ class ZetaDateInput extends StatefulWidget { /// Determines the size of the input field. /// Default is `ZetaDateInputSize.large` - final ZetaDateInputSize? size; + final ZetaWidgetSize? size; /// If provided, displays a label above the input field. final String? label; @@ -102,7 +90,7 @@ class ZetaDateInput extends StatefulWidget { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(EnumProperty('size', size)) + ..add(EnumProperty('size', size)) ..add(StringProperty('label', label)) ..add(StringProperty('hint', hint)) ..add(DiagnosticsProperty('enabled', enabled)) @@ -116,7 +104,7 @@ class ZetaDateInput extends StatefulWidget { class _ZetaDateInputState extends State { final _controller = TextEditingController(); - late ZetaDateInputSize _size; + late ZetaWidgetSize _size; late final String _hintText; late final MaskTextInputFormatter _dateFormatter; bool _invalidDate = false; @@ -141,7 +129,7 @@ class _ZetaDateInputState extends State { } void _setParams() { - _size = widget.size ?? ZetaDateInputSize.large; + _size = widget.size ?? ZetaWidgetSize.large; _hasError = widget.hasError; } @@ -197,7 +185,7 @@ class _ZetaDateInputState extends State { inputFormatters: [_dateFormatter], keyboardType: TextInputType.number, onChanged: (_) => _onChanged(), - style: _size == ZetaDateInputSize.small ? ZetaTextStyles.bodyXSmall : ZetaTextStyles.bodyMedium, + style: _size == ZetaWidgetSize.small ? ZetaTextStyles.bodyXSmall : ZetaTextStyles.bodyMedium, decoration: InputDecoration( isDense: true, contentPadding: EdgeInsets.symmetric( @@ -235,7 +223,7 @@ class _ZetaDateInputState extends State { minHeight: ZetaSpacing.m, minWidth: ZetaSpacing.m, ), - hintStyle: _size == ZetaDateInputSize.small + hintStyle: _size == ZetaWidgetSize.small ? ZetaTextStyles.bodyXSmall.copyWith( color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, ) @@ -289,16 +277,16 @@ class _ZetaDateInputState extends State { ); } - double _inputVerticalPadding(ZetaDateInputSize size) => switch (size) { - ZetaDateInputSize.large => ZetaSpacing.x3, - ZetaDateInputSize.medium => ZetaSpacing.x2, - ZetaDateInputSize.small => ZetaSpacing.x2, + double _inputVerticalPadding(ZetaWidgetSize size) => switch (size) { + ZetaWidgetSize.large => ZetaSpacing.x3, + ZetaWidgetSize.medium => ZetaSpacing.x2, + ZetaWidgetSize.small => ZetaSpacing.x2, }; - double _iconSize(ZetaDateInputSize size) => switch (size) { - ZetaDateInputSize.large => ZetaSpacing.x6, - ZetaDateInputSize.medium => ZetaSpacing.x5, - ZetaDateInputSize.small => ZetaSpacing.x4, + double _iconSize(ZetaWidgetSize size) => switch (size) { + ZetaWidgetSize.large => ZetaSpacing.x6, + ZetaWidgetSize.medium => ZetaSpacing.x5, + ZetaWidgetSize.small => ZetaSpacing.x4, }; OutlineInputBorder _defaultInputBorder( diff --git a/lib/src/components/dialog/dialog.dart b/lib/src/components/dialog/dialog.dart new file mode 100644 index 00000000..22068383 --- /dev/null +++ b/lib/src/components/dialog/dialog.dart @@ -0,0 +1,227 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zeta_flutter.dart'; + +/// [ZetaDialogHeaderAlignment] +enum ZetaDialogHeaderAlignment { + /// [left] + left, + + /// [center] + center, +} + +/// [showZetaDialog] +Future showZetaDialog( + BuildContext context, { + Zeta? zeta, + ZetaDialogHeaderAlignment headerAlignment = ZetaDialogHeaderAlignment.center, + Widget? icon, + String? title, + required String message, + String? primaryButtonLabel, + VoidCallback? onPrimaryButtonPressed, + String? secondaryButtonLabel, + VoidCallback? onSecondaryButtonPressed, + String? tertiaryButtonLabel, + VoidCallback? onTertiaryButtonPressed, + bool rounded = true, + bool barrierDismissible = true, + bool useRootNavigator = true, +}) => + showDialog( + context: context, + barrierDismissible: barrierDismissible, + useRootNavigator: useRootNavigator, + builder: (_) => _ZetaDialog( + zeta: zeta, + headerAlignment: headerAlignment, + icon: icon, + title: title, + message: message, + primaryButtonLabel: primaryButtonLabel, + onPrimaryButtonPressed: onPrimaryButtonPressed, + secondaryButtonLabel: secondaryButtonLabel, + onSecondaryButtonPressed: onSecondaryButtonPressed, + tertiaryButtonLabel: tertiaryButtonLabel, + onTertiaryButtonPressed: onTertiaryButtonPressed, + rounded: rounded, + ), + ); + +class _ZetaDialog extends StatelessWidget { + const _ZetaDialog({ + this.headerAlignment = ZetaDialogHeaderAlignment.center, + this.icon, + this.title, + required this.message, + this.primaryButtonLabel, + this.onPrimaryButtonPressed, + this.secondaryButtonLabel, + this.onSecondaryButtonPressed, + this.tertiaryButtonLabel, + this.onTertiaryButtonPressed, + this.rounded = true, + this.zeta, + }); + + final ZetaDialogHeaderAlignment headerAlignment; + final Widget? icon; + final String? title; + final String message; + final String? primaryButtonLabel; + final VoidCallback? onPrimaryButtonPressed; + final String? secondaryButtonLabel; + final VoidCallback? onSecondaryButtonPressed; + final String? tertiaryButtonLabel; + final VoidCallback? onTertiaryButtonPressed; + final bool rounded; + final Zeta? zeta; + + @override + Widget build(BuildContext context) { + final zeta = this.zeta ?? Zeta.of(context); + final primaryButton = primaryButtonLabel == null + ? null + : ZetaButton( + zeta: zeta, + label: primaryButtonLabel!, + onPressed: onPrimaryButtonPressed ?? () => Navigator.of(context).pop(true), + borderType: rounded ? ZetaWidgetBorder.rounded : ZetaWidgetBorder.sharp, + ); + final secondaryButton = secondaryButtonLabel == null + ? null + : ZetaButton.outlineSubtle( + zeta: zeta, + label: secondaryButtonLabel!, + onPressed: onSecondaryButtonPressed ?? () => Navigator.of(context).pop(false), + borderType: rounded ? ZetaWidgetBorder.rounded : ZetaWidgetBorder.sharp, + ); + final tertiaryButton = tertiaryButtonLabel == null + ? null + : TextButton( + onPressed: onTertiaryButtonPressed, + child: Text(tertiaryButtonLabel!), + ); + final hasButton = primaryButton != null || secondaryButton != null || tertiaryButton != null; + + return AlertDialog( + surfaceTintColor: zeta.colors.surfacePrimary, + shape: const RoundedRectangleBorder(borderRadius: ZetaRadius.large), + title: icon != null || title != null + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: switch (headerAlignment) { + ZetaDialogHeaderAlignment.left => CrossAxisAlignment.start, + ZetaDialogHeaderAlignment.center => CrossAxisAlignment.center, + }, + children: [ + if (icon != null) + Padding( + padding: const EdgeInsets.only(bottom: ZetaSpacing.s), + child: icon, + ), + if (title != null) + Text( + title!, + textAlign: switch (headerAlignment) { + ZetaDialogHeaderAlignment.left => TextAlign.left, + ZetaDialogHeaderAlignment.center => TextAlign.center, + }, + ), + ], + ) + : null, + titlePadding: context.deviceType == DeviceType.mobilePortrait + ? null + : const EdgeInsets.only( + left: ZetaSpacing.x10, + right: ZetaSpacing.x10, + top: ZetaSpacing.m, + ), + titleTextStyle: zetaTextTheme.headlineSmall?.copyWith( + color: zeta.colors.textDefault, + ), + content: Text(message), + contentPadding: context.deviceType == DeviceType.mobilePortrait + ? null + : const EdgeInsets.only( + left: ZetaSpacing.x10, + right: ZetaSpacing.x10, + top: ZetaSpacing.s, + bottom: ZetaSpacing.m, + ), + contentTextStyle: context.deviceType == DeviceType.mobilePortrait + ? zetaTextTheme.bodySmall?.copyWith(color: zeta.colors.textDefault) + : zetaTextTheme.bodyMedium?.copyWith(color: zeta.colors.textDefault), + actions: [ + if (context.deviceType == DeviceType.mobilePortrait) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (hasButton) const SizedBox(height: ZetaSpacing.m), + if (tertiaryButton == null) + Row( + children: [ + if (secondaryButton != null) Expanded(child: secondaryButton), + if (primaryButton != null && secondaryButton != null) const SizedBox(width: ZetaSpacing.b), + if (primaryButton != null) Expanded(child: primaryButton), + ], + ) + else ...[ + if (primaryButton != null) primaryButton, + if (primaryButton != null && secondaryButton != null) const SizedBox(height: ZetaSpacing.s), + if (secondaryButton != null) secondaryButton, + if (primaryButton != null || secondaryButton != null) const SizedBox(height: ZetaSpacing.xs), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [tertiaryButton], + ), + ], + ], + ) + else + Row( + children: [ + if (tertiaryButton != null) tertiaryButton, + if (primaryButton != null || secondaryButton != null) ...[ + const SizedBox(width: ZetaSpacing.m), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (secondaryButton != null) secondaryButton, + if (primaryButton != null && secondaryButton != null) const SizedBox(width: ZetaSpacing.b), + if (primaryButton != null) primaryButton, + ], + ), + ), + ], + ], + ), + ], + actionsPadding: context.deviceType == DeviceType.mobilePortrait + ? null + : const EdgeInsets.symmetric( + horizontal: ZetaSpacing.x10, + vertical: ZetaSpacing.m, + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('headerAlignment', headerAlignment)) + ..add(StringProperty('title', title)) + ..add(StringProperty('message', message)) + ..add(StringProperty('primaryButtonLabel', primaryButtonLabel)) + ..add(ObjectFlagProperty.has('onPrimaryButtonPressed', onPrimaryButtonPressed)) + ..add(StringProperty('secondaryButtonLabel', secondaryButtonLabel)) + ..add(ObjectFlagProperty.has('onSecondaryButtonPressed', onSecondaryButtonPressed)) + ..add(StringProperty('tertiaryButtonLabel', tertiaryButtonLabel)) + ..add(ObjectFlagProperty.has('onTertiaryButtonPressed', onTertiaryButtonPressed)) + ..add(DiagnosticsProperty('rounded', rounded)); + } +} diff --git a/lib/src/components/navigation_rail/navigation_rail.dart b/lib/src/components/navigation_rail/navigation_rail.dart new file mode 100644 index 00000000..1fbfc4dc --- /dev/null +++ b/lib/src/components/navigation_rail/navigation_rail.dart @@ -0,0 +1,229 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; + +/// [ZetaNavigationRail] +class ZetaNavigationRail extends StatefulWidget { + /// Constructor for [ZetaNavigationRail]. + const ZetaNavigationRail({ + super.key, + required this.items, + this.selectedIndex, + this.onSelect, + this.rounded = true, + this.margin = const EdgeInsets.all(ZetaSpacing.x5), + this.itemSpacing = const EdgeInsets.only(bottom: ZetaSpacing.xxs), + this.itemPadding, + this.wordWrap, + }); + + /// Required list of navigation items. + final List items; + + /// Initially selected item form the list of `items`. + final int? selectedIndex; + + /// Called when an item is selected. + final void Function(int)? onSelect; + + /// Determines if the items are rounded (default) or sharp. + final bool rounded; + + /// The margin around the [ZetaNavigationRail]. + /// Default is: + /// ``` + /// const EdgeInsets.all(ZetaSpacing.x5) + /// ``` + final EdgeInsets margin; + + /// The spacing between items in [ZetaNavigationRail]. + /// Default is: + /// ``` + /// const EdgeInsets.only(bottom: ZetaSpacing.xxs) + /// ``` + final EdgeInsets itemSpacing; + + /// The padding within an item in [ZetaNavigationRail]. + /// Default is: + /// ``` + /// const EdgeInsets.symmetric( + /// horizontal: ZetaSpacing.xs, + /// vertical: ZetaSpacing.s, + /// ), + /// ``` + final EdgeInsets? itemPadding; + + /// Determines if words in items' labels should be wrapped on separate lines. + /// Default is `true`. + final bool? wordWrap; + + @override + State createState() => _ZetaNavigationRailState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(IntProperty('selectedIndex', selectedIndex)) + ..add(ObjectFlagProperty.has('onSelect', onSelect)) + ..add(IterableProperty('items', items)) + ..add(DiagnosticsProperty('margin', margin)) + ..add(DiagnosticsProperty('itemSpacing', itemSpacing)) + ..add(DiagnosticsProperty('itemPadding', itemPadding)) + ..add(DiagnosticsProperty('wordWrap', wordWrap)); + } +} + +class _ZetaNavigationRailState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: widget.margin, + child: IntrinsicWidth( + child: Column( + children: [ + for (int i = 0; i < widget.items.length; i++) + Row( + children: [ + Expanded( + child: Padding( + padding: widget.itemSpacing, + child: _ZetaNavigationRailItemContent( + label: widget.items[i].label, + icon: widget.items[i].icon, + selected: widget.selectedIndex == i, + disabled: widget.items[i].disabled, + onTap: () => widget.onSelect?.call(i), + rounded: widget.rounded, + padding: widget.itemPadding, + wordWrap: widget.wordWrap, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +class _ZetaNavigationRailItemContent extends StatelessWidget { + const _ZetaNavigationRailItemContent({ + required this.label, + this.icon, + this.selected = false, + this.disabled = false, + this.onTap, + this.rounded = true, + this.padding, + this.wordWrap, + }); + + final String label; + final Widget? icon; + final bool selected; + final bool disabled; + final VoidCallback? onTap; + final bool rounded; + final EdgeInsets? padding; + final bool? wordWrap; + + @override + Widget build(BuildContext context) { + final zeta = Zeta.of(context); + return MouseRegion( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: disabled ? null : onTap, + child: DecoratedBox( + decoration: BoxDecoration( + color: disabled + ? null + : selected + ? zeta.colors.blue.shade10 + : null, + borderRadius: rounded ? ZetaRadius.rounded : null, + ), + child: ConstrainedBox( + constraints: const BoxConstraints( + minWidth: ZetaSpacing.x16, + minHeight: ZetaSpacing.x16, + ), + child: SelectionContainer.disabled( + child: Padding( + padding: padding ?? + const EdgeInsets.symmetric( + horizontal: ZetaSpacing.xs, + vertical: ZetaSpacing.s, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) + IconTheme( + data: IconThemeData( + color: disabled + ? zeta.colors.cool.shade50 + : selected + ? zeta.colors.textDefault + : zeta.colors.cool.shade70, + size: 24, + ), + child: icon!, + ), + Text( + (wordWrap ?? true) ? label.replaceAll(' ', '\n') : label, + textAlign: TextAlign.center, + style: ZetaTextStyles.titleSmall.copyWith( + color: disabled + ? zeta.colors.cool.shade50 + : selected + ? zeta.colors.textDefault + : zeta.colors.cool.shade70, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty.has('onTap', onTap)) + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(DiagnosticsProperty('disabled', disabled)) + ..add(DiagnosticsProperty('selected', selected)) + ..add(DiagnosticsProperty('padding', padding)) + ..add(StringProperty('label', label)) + ..add(DiagnosticsProperty('wordWrap', wordWrap)); + } +} + +/// [ZetaNavigationRailItem] +class ZetaNavigationRailItem { + /// Constructor for [ZetaNavigationRailItem]. + const ZetaNavigationRailItem({ + required this.label, + this.icon, + this.disabled = false, + }); + + /// Item's label. + final String label; + + /// Optional item's icon. + final Widget? icon; + + /// Indicates that this navigation item is inaccessible. + final bool disabled; +} diff --git a/lib/src/components/phone_input/countries.dart b/lib/src/components/phone_input/countries.dart new file mode 100644 index 00000000..0e600934 --- /dev/null +++ b/lib/src/components/phone_input/countries.dart @@ -0,0 +1,1261 @@ +/// [Country] +class Country { + /// Constructor for [Country] + const Country({ + required this.name, + required this.dialCode, + required this.isoCode, + }); + + /// Country's name + final String name; + + /// Country's dial code + final String dialCode; + + /// Country's ISO code + final String isoCode; + + /// Country's flag URI + String get flagUri => 'lib/src/assets/flags/${isoCode.toLowerCase()}.png'; + + @override + String toString() => { + 'dialCode': dialCode, + 'name': name, + 'isoCode': isoCode, + }.toString(); +} + +/// [Countries] +class Countries { + /// List of [Country] for most countries around the world. + static List get list => _countriesList; + static final List _countriesList = [ + const Country( + isoCode: 'AF', + name: 'Afghanistan', + dialCode: '+93', + ), + const Country( + isoCode: 'AX', + name: 'Åland Islands', + dialCode: '+358', + ), + const Country( + isoCode: 'AL', + name: 'Albania', + dialCode: '+355', + ), + const Country( + isoCode: 'DZ', + name: 'Algeria', + dialCode: '+213', + ), + const Country( + isoCode: 'AS', + name: 'American Samoa', + dialCode: '+1684', + ), + const Country( + isoCode: 'AD', + name: 'Andorra', + dialCode: '+376', + ), + const Country( + isoCode: 'AO', + name: 'Angola', + dialCode: '+244', + ), + const Country( + isoCode: 'AI', + name: 'Anguilla', + dialCode: '+1264', + ), + const Country( + isoCode: 'AQ', + name: 'Antarctica', + dialCode: '+672', + ), + const Country( + isoCode: 'AG', + name: 'Antigua and Barbuda', + dialCode: '+1268', + ), + const Country( + isoCode: 'AR', + name: 'Argentina', + dialCode: '+54', + ), + const Country( + isoCode: 'AM', + name: 'Armenia', + dialCode: '+374', + ), + const Country( + isoCode: 'AW', + name: 'Aruba', + dialCode: '+297', + ), + const Country( + isoCode: 'AU', + name: 'Australia', + dialCode: '+61', + ), + const Country( + isoCode: 'AT', + name: 'Austria', + dialCode: '+43', + ), + const Country( + isoCode: 'AZ', + name: 'Azerbaijan', + dialCode: '+994', + ), + const Country( + isoCode: 'BS', + name: 'Bahamas', + dialCode: '+1242', + ), + const Country( + isoCode: 'BH', + name: 'Bahrain', + dialCode: '+973', + ), + const Country( + isoCode: 'BD', + name: 'Bangladesh', + dialCode: '+880', + ), + const Country( + isoCode: 'BB', + name: 'Barbados', + dialCode: '+1246', + ), + const Country( + isoCode: 'BY', + name: 'Belarus', + dialCode: '+375', + ), + const Country( + isoCode: 'BE', + name: 'Belgium', + dialCode: '+32', + ), + const Country( + isoCode: 'BZ', + name: 'Belize', + dialCode: '+501', + ), + const Country( + isoCode: 'BJ', + name: 'Benin', + dialCode: '+229', + ), + const Country( + isoCode: 'BM', + name: 'Bermuda', + dialCode: '+1441', + ), + const Country( + isoCode: 'BT', + name: 'Bhutan', + dialCode: '+975', + ), + const Country( + isoCode: 'BO', + name: 'Bolivia (Plurinational State of)', + dialCode: '+591', + ), + const Country( + isoCode: 'BA', + name: 'Bosnia and Herzegovina', + dialCode: '+387', + ), + const Country( + isoCode: 'BW', + name: 'Botswana', + dialCode: '+267', + ), + const Country( + isoCode: 'BV', + name: 'Bouvet Island', + dialCode: '+47', + ), + const Country( + isoCode: 'BR', + name: 'Brazil', + dialCode: '+55', + ), + const Country( + isoCode: 'IO', + name: 'British Indian Ocean Territory', + dialCode: '+246', + ), + const Country( + isoCode: 'BN', + name: 'Brunei Darussalam', + dialCode: '+673', + ), + const Country( + isoCode: 'BG', + name: 'Bulgaria', + dialCode: '+359', + ), + const Country( + isoCode: 'BF', + name: 'Burkina Faso', + dialCode: '+226', + ), + const Country( + isoCode: 'BI', + name: 'Burundi', + dialCode: '+257', + ), + const Country( + isoCode: 'CV', + name: 'Cabo Verde', + dialCode: '+238', + ), + const Country( + isoCode: 'KH', + name: 'Cambodia', + dialCode: '+855', + ), + const Country( + isoCode: 'CM', + name: 'Cameroon', + dialCode: '+237', + ), + const Country( + isoCode: 'CA', + name: 'Canada', + dialCode: '+1', + ), + const Country( + isoCode: 'KY', + name: 'Cayman Islands', + dialCode: '+1345', + ), + const Country( + isoCode: 'CF', + name: 'Central African Republic', + dialCode: '+236', + ), + const Country( + isoCode: 'TD', + name: 'Chad', + dialCode: '+235', + ), + const Country( + isoCode: 'CL', + name: 'Chile', + dialCode: '+56', + ), + const Country( + isoCode: 'CN', + name: 'China', + dialCode: '+86', + ), + const Country( + isoCode: 'CX', + name: 'Christmas Island', + dialCode: '+61', + ), + const Country( + isoCode: 'CC', + name: 'Cocos (Keeling) Islands', + dialCode: '+61', + ), + const Country( + isoCode: 'CO', + name: 'Colombia', + dialCode: '+57', + ), + const Country( + isoCode: 'KM', + name: 'Comoros', + dialCode: '+269', + ), + const Country( + isoCode: 'CG', + name: 'Congo (Republic of the)', + dialCode: '+242', + ), + const Country( + isoCode: 'CD', + name: 'Congo (Democratic Republic of the)', + dialCode: '+243', + ), + const Country( + isoCode: 'CK', + name: 'Cook Islands', + dialCode: '+682', + ), + const Country( + isoCode: 'CR', + name: 'Costa Rica', + dialCode: '+506', + ), + const Country( + isoCode: 'CI', + name: 'Côte d`Ivoire', + dialCode: '+225', + ), + const Country( + isoCode: 'HR', + name: 'Croatia', + dialCode: '+385', + ), + const Country( + isoCode: 'CU', + name: 'Cuba', + dialCode: '+53', + ), + const Country( + isoCode: 'CY', + name: 'Cyprus', + dialCode: '+357', + ), + const Country( + isoCode: 'CZ', + name: 'Czech Republic', + dialCode: '+420', + ), + const Country( + isoCode: 'DK', + name: 'Denmark', + dialCode: '+45', + ), + const Country( + isoCode: 'DJ', + name: 'Djibouti', + dialCode: '+253', + ), + const Country( + isoCode: 'DM', + name: 'Dominica', + dialCode: '+1767', + ), + const Country( + isoCode: 'DO', + name: 'Dominican Republic', + dialCode: '+1', + ), + const Country( + isoCode: 'EC', + name: 'Ecuador', + dialCode: '+593', + ), + const Country( + isoCode: 'EG', + name: 'Egypt', + dialCode: '+20', + ), + const Country( + isoCode: 'SV', + name: 'El Salvador', + dialCode: '+503', + ), + const Country( + isoCode: 'GQ', + name: 'Equatorial Guinea', + dialCode: '+240', + ), + const Country( + isoCode: 'ER', + name: 'Eritrea', + dialCode: '+291', + ), + const Country( + isoCode: 'EE', + name: 'Estonia', + dialCode: '+372', + ), + const Country( + isoCode: 'ET', + name: 'Ethiopia', + dialCode: '+251', + ), + const Country( + isoCode: 'FK', + name: 'Falkland Islands (Malvinas)', + dialCode: '+500', + ), + const Country( + isoCode: 'FO', + name: 'Faroe Islands', + dialCode: '+298', + ), + const Country( + isoCode: 'FJ', + name: 'Fiji', + dialCode: '+679', + ), + const Country( + isoCode: 'FI', + name: 'Finland', + dialCode: '+358', + ), + const Country( + isoCode: 'FR', + name: 'France', + dialCode: '+33', + ), + const Country( + isoCode: 'GF', + name: 'French Guiana', + dialCode: '+594', + ), + const Country( + isoCode: 'PF', + name: 'French Polynesia', + dialCode: '+689', + ), + const Country( + isoCode: 'TF', + name: 'French Southern Territories', + dialCode: '+262', + ), + const Country( + isoCode: 'GA', + name: 'Gabon', + dialCode: '+241', + ), + const Country( + isoCode: 'GM', + name: 'Gambia', + dialCode: '+220', + ), + const Country( + isoCode: 'GE', + name: 'Georgia', + dialCode: '+995', + ), + const Country( + isoCode: 'DE', + name: 'Germany', + dialCode: '+49', + ), + const Country( + isoCode: 'GH', + name: 'Ghana', + dialCode: '+233', + ), + const Country( + isoCode: 'GI', + name: 'Gibraltar', + dialCode: '+350', + ), + const Country( + isoCode: 'GR', + name: 'Greece', + dialCode: '+30', + ), + const Country( + isoCode: 'GL', + name: 'Greenland', + dialCode: '+299', + ), + const Country( + isoCode: 'GD', + name: 'Grenada', + dialCode: '+1473', + ), + const Country( + isoCode: 'GP', + name: 'Guadeloupe', + dialCode: '+590', + ), + const Country( + isoCode: 'GU', + name: 'Guam', + dialCode: '+1671', + ), + const Country( + isoCode: 'GT', + name: 'Guatemala', + dialCode: '+502', + ), + const Country( + isoCode: 'GG', + name: 'Guernsey', + dialCode: '+44', + ), + const Country( + isoCode: 'GN', + name: 'Guinea', + dialCode: '+224', + ), + const Country( + isoCode: 'GW', + name: 'Guinea-Bissau', + dialCode: '+245', + ), + const Country( + isoCode: 'GY', + name: 'Guyana', + dialCode: '+592', + ), + const Country( + isoCode: 'HT', + name: 'Haiti', + dialCode: '+509', + ), + const Country( + isoCode: 'HM', + name: 'Heard Island and McDonald Islands', + dialCode: '+0', + ), + const Country( + isoCode: 'VA', + name: 'Vatican City State', + dialCode: '+379', + ), + const Country( + isoCode: 'HN', + name: 'Honduras', + dialCode: '+504', + ), + const Country( + isoCode: 'HK', + name: 'Hong Kong', + dialCode: '+852', + ), + const Country( + isoCode: 'HU', + name: 'Hungary', + dialCode: '+36', + ), + const Country( + isoCode: 'IS', + name: 'Iceland', + dialCode: '+354', + ), + const Country( + isoCode: 'IN', + name: 'India', + dialCode: '+91', + ), + const Country( + isoCode: 'ID', + name: 'Indonesia', + dialCode: '+62', + ), + const Country( + isoCode: 'IR', + name: 'Iran', + dialCode: '+98', + ), + const Country( + isoCode: 'IQ', + name: 'Iraq', + dialCode: '+964', + ), + const Country( + isoCode: 'IE', + name: 'Ireland', + dialCode: '+353', + ), + const Country( + isoCode: 'IM', + name: 'Isle of Man', + dialCode: '+44', + ), + const Country( + isoCode: 'IL', + name: 'Israel', + dialCode: '+972', + ), + const Country( + isoCode: 'IT', + name: 'Italy', + dialCode: '+39', + ), + const Country( + isoCode: 'JM', + name: 'Jamaica', + dialCode: '+1876', + ), + const Country( + isoCode: 'JP', + name: 'Japan', + dialCode: '+81', + ), + const Country( + isoCode: 'JE', + name: 'Jersey', + dialCode: '+44', + ), + const Country( + isoCode: 'JO', + name: 'Jordan', + dialCode: '+962', + ), + const Country( + isoCode: 'KZ', + name: 'Kazakhstan', + dialCode: '+7', + ), + const Country( + isoCode: 'KE', + name: 'Kenya', + dialCode: '+254', + ), + const Country( + isoCode: 'KI', + name: 'Kiribati', + dialCode: '+686', + ), + const Country( + isoCode: 'XK', + name: 'Kosovo (Republic of)', + dialCode: '+383', + ), + const Country( + isoCode: 'KP', + name: 'Korea (Democratic People`s Republic of)', + dialCode: '+850', + ), + const Country( + isoCode: 'KR', + name: 'Korea (Republic of)', + dialCode: '+82', + ), + const Country( + isoCode: 'KW', + name: 'Kuwait', + dialCode: '+965', + ), + const Country( + isoCode: 'KG', + name: 'Kyrgyzstan', + dialCode: '+996', + ), + const Country( + isoCode: 'LA', + name: 'Lao People`s Democratic Republic', + dialCode: '+856', + ), + const Country( + isoCode: 'LV', + name: 'Latvia', + dialCode: '+371', + ), + const Country( + isoCode: 'LB', + name: 'Lebanon', + dialCode: '+961', + ), + const Country( + isoCode: 'LS', + name: 'Lesotho', + dialCode: '+266', + ), + const Country( + isoCode: 'LR', + name: 'Liberia', + dialCode: '+231', + ), + const Country( + isoCode: 'LY', + name: 'Libya', + dialCode: '+218', + ), + const Country( + isoCode: 'LI', + name: 'Liechtenstein', + dialCode: '+423', + ), + const Country( + isoCode: 'LT', + name: 'Lithuania', + dialCode: '+370', + ), + const Country( + isoCode: 'LU', + name: 'Luxembourg', + dialCode: '+352', + ), + const Country( + isoCode: 'MO', + name: 'Macao', + dialCode: '+853', + ), + const Country( + isoCode: 'MK', + name: 'Macedonia (the former Yugoslav Republic of)', + dialCode: '+389', + ), + const Country( + isoCode: 'MG', + name: 'Madagascar', + dialCode: '+261', + ), + const Country( + isoCode: 'MW', + name: 'Malawi', + dialCode: '+265', + ), + const Country( + isoCode: 'MY', + name: 'Malaysia', + dialCode: '+60', + ), + const Country( + isoCode: 'MV', + name: 'Maldives', + dialCode: '+960', + ), + const Country( + isoCode: 'ML', + name: 'Mali', + dialCode: '+223', + ), + const Country( + isoCode: 'MT', + name: 'Malta', + dialCode: '+356', + ), + const Country( + isoCode: 'MH', + name: 'Marshall Islands', + dialCode: '+692', + ), + const Country( + isoCode: 'MQ', + name: 'Martinique', + dialCode: '+596', + ), + const Country( + isoCode: 'MR', + name: 'Mauritania', + dialCode: '+222', + ), + const Country( + isoCode: 'MU', + name: 'Mauritius', + dialCode: '+230', + ), + const Country( + isoCode: 'YT', + name: 'Mayotte', + dialCode: '+262', + ), + const Country( + isoCode: 'MX', + name: 'Mexico', + dialCode: '+52', + ), + const Country( + isoCode: 'FM', + name: 'Micronesia (Federated States of)', + dialCode: '+691', + ), + const Country( + isoCode: 'MD', + name: 'Moldova (Republic of)', + dialCode: '+373', + ), + const Country( + isoCode: 'MC', + name: 'Monaco', + dialCode: '+377', + ), + const Country( + isoCode: 'MN', + name: 'Mongolia', + dialCode: '+976', + ), + const Country( + isoCode: 'ME', + name: 'Montenegro', + dialCode: '+382', + ), + const Country( + isoCode: 'MS', + name: 'Montserrat', + dialCode: '+1664', + ), + const Country( + isoCode: 'MA', + name: 'Morocco', + dialCode: '+212', + ), + const Country( + isoCode: 'MZ', + name: 'Mozambique', + dialCode: '+258', + ), + const Country( + isoCode: 'MM', + name: 'Myanmar', + dialCode: '+95', + ), + const Country( + isoCode: 'NA', + name: 'Namibia', + dialCode: '+264', + ), + const Country( + isoCode: 'NR', + name: 'Nauru', + dialCode: '+674', + ), + const Country( + isoCode: 'NP', + name: 'Nepal', + dialCode: '+977', + ), + const Country( + isoCode: 'NL', + name: 'Netherlands', + dialCode: '+31', + ), + const Country( + isoCode: 'NC', + name: 'New Caledonia', + dialCode: '+687', + ), + const Country( + isoCode: 'NZ', + name: 'New Zealand', + dialCode: '+64', + ), + const Country( + isoCode: 'NI', + name: 'Nicaragua', + dialCode: '+505', + ), + const Country( + isoCode: 'NE', + name: 'Niger', + dialCode: '+227', + ), + const Country( + isoCode: 'NG', + name: 'Nigeria', + dialCode: '+234', + ), + const Country( + isoCode: 'NU', + name: 'Niue', + dialCode: '+683', + ), + const Country( + isoCode: 'NF', + name: 'Norfolk Island', + dialCode: '+672', + ), + const Country( + isoCode: 'MP', + name: 'Northern Mariana Islands', + dialCode: '+1670', + ), + const Country( + isoCode: 'NO', + name: 'Norway', + dialCode: '+47', + ), + const Country( + isoCode: 'OM', + name: 'Oman', + dialCode: '+968', + ), + const Country( + isoCode: 'PK', + name: 'Pakistan', + dialCode: '+92', + ), + const Country( + isoCode: 'PW', + name: 'Palau', + dialCode: '+680', + ), + const Country( + isoCode: 'PS', + name: 'Palestine, State of', + dialCode: '+970', + ), + const Country( + isoCode: 'PA', + name: 'Panama', + dialCode: '+507', + ), + const Country( + isoCode: 'PG', + name: 'Papua New Guinea', + dialCode: '+675', + ), + const Country( + isoCode: 'PY', + name: 'Paraguay', + dialCode: '+595', + ), + const Country( + isoCode: 'PE', + name: 'Peru', + dialCode: '+51', + ), + const Country( + isoCode: 'PH', + name: 'Philippines', + dialCode: '+63', + ), + const Country( + isoCode: 'PN', + name: 'Pitcairn', + dialCode: '+64', + ), + const Country( + isoCode: 'PL', + name: 'Poland', + dialCode: '+48', + ), + const Country( + isoCode: 'PT', + name: 'Portugal', + dialCode: '+351', + ), + const Country( + isoCode: 'PR', + name: 'Puerto Rico', + dialCode: '+1939', + ), + const Country( + isoCode: 'QA', + name: 'Qatar', + dialCode: '+974', + ), + const Country( + isoCode: 'RE', + name: 'Réunion', + dialCode: '+262', + ), + const Country( + isoCode: 'RO', + name: 'Romania', + dialCode: '+40', + ), + const Country( + isoCode: 'RU', + name: 'Russian Federation', + dialCode: '+7', + ), + const Country( + isoCode: 'RW', + name: 'Rwanda', + dialCode: '+250', + ), + const Country( + isoCode: 'BL', + name: 'Saint Barthélemy', + dialCode: '+590', + ), + const Country( + isoCode: 'SH', + name: 'Saint Helena, Ascension and Tristan da Cunha', + dialCode: '+290', + ), + const Country( + isoCode: 'KN', + name: 'Saint Kitts and Nevis', + dialCode: '+1869', + ), + const Country( + isoCode: 'LC', + name: 'Saint Lucia', + dialCode: '+1758', + ), + const Country( + isoCode: 'MF', + name: 'Saint Martin (French part)', + dialCode: '+590', + ), + const Country( + isoCode: 'PM', + name: 'Saint Pierre and Miquelon', + dialCode: '+508', + ), + const Country( + isoCode: 'VC', + name: 'Saint Vincent and the Grenadines', + dialCode: '+1784', + ), + const Country( + isoCode: 'WS', + name: 'Samoa', + dialCode: '+685', + ), + const Country( + isoCode: 'SM', + name: 'San Marino', + dialCode: '+378', + ), + const Country( + isoCode: 'ST', + name: 'Sao Tome and Principe', + dialCode: '+239', + ), + const Country( + isoCode: 'SA', + name: 'Saudi Arabia', + dialCode: '+966', + ), + const Country( + isoCode: 'SN', + name: 'Senegal', + dialCode: '+221', + ), + const Country( + isoCode: 'RS', + name: 'Serbia', + dialCode: '+381', + ), + const Country( + isoCode: 'SC', + name: 'Seychelles', + dialCode: '+248', + ), + const Country( + isoCode: 'SL', + name: 'Sierra Leone', + dialCode: '+232', + ), + const Country( + isoCode: 'SG', + name: 'Singapore', + dialCode: '+65', + ), + const Country( + isoCode: 'SK', + name: 'Slovakia', + dialCode: '+421', + ), + const Country( + isoCode: 'SI', + name: 'Slovenia', + dialCode: '+386', + ), + const Country( + isoCode: 'SB', + name: 'Solomon Islands', + dialCode: '+677', + ), + const Country( + isoCode: 'SO', + name: 'Somalia', + dialCode: '+252', + ), + const Country( + isoCode: 'ZA', + name: 'South Africa', + dialCode: '+27', + ), + const Country( + isoCode: 'GS', + name: 'South Georgia and the South Sandwich Islands', + dialCode: '+500', + ), + const Country( + isoCode: 'SS', + name: 'South Sudan', + dialCode: '+211', + ), + const Country( + isoCode: 'ES', + name: 'Spain', + dialCode: '+34', + ), + const Country( + isoCode: 'LK', + name: 'Sri Lanka', + dialCode: '+94', + ), + const Country( + isoCode: 'SD', + name: 'Sudan', + dialCode: '+249', + ), + const Country( + isoCode: 'SR', + name: 'Suriname', + dialCode: '+597', + ), + const Country( + isoCode: 'SJ', + name: 'Svalbard and Jan Mayen', + dialCode: '+47', + ), + const Country( + isoCode: 'SZ', + name: 'Swaziland', + dialCode: '+268', + ), + const Country( + isoCode: 'SE', + name: 'Sweden', + dialCode: '+46', + ), + const Country( + isoCode: 'CH', + name: 'Switzerland', + dialCode: '+41', + ), + const Country( + isoCode: 'SY', + name: 'Syrian Arab Republic', + dialCode: '+963', + ), + const Country( + isoCode: 'TW', + name: 'Taiwan', + dialCode: '+886', + ), + const Country( + isoCode: 'TJ', + name: 'Tajikistan', + dialCode: '+992', + ), + const Country( + isoCode: 'TZ', + name: 'Tanzania, United Republic of', + dialCode: '+255', + ), + const Country( + isoCode: 'TH', + name: 'Thailand', + dialCode: '+66', + ), + const Country( + isoCode: 'TL', + name: 'Timor-Leste', + dialCode: '+670', + ), + const Country( + isoCode: 'TG', + name: 'Togo', + dialCode: '+228', + ), + const Country( + isoCode: 'TK', + name: 'Tokelau', + dialCode: '+690', + ), + const Country( + isoCode: 'TO', + name: 'Tonga', + dialCode: '+676', + ), + const Country( + isoCode: 'TT', + name: 'Trinidad and Tobago', + dialCode: '+1868', + ), + const Country( + isoCode: 'TN', + name: 'Tunisia', + dialCode: '+216', + ), + const Country( + isoCode: 'TR', + name: 'Turkey', + dialCode: '+90', + ), + const Country( + isoCode: 'TM', + name: 'Turkmenistan', + dialCode: '+993', + ), + const Country( + isoCode: 'TC', + name: 'Turks and Caicos Islands', + dialCode: '+1649', + ), + const Country( + isoCode: 'TV', + name: 'Tuvalu', + dialCode: '+688', + ), + const Country( + isoCode: 'UG', + name: 'Uganda', + dialCode: '+256', + ), + const Country( + isoCode: 'UA', + name: 'Ukraine', + dialCode: '+380', + ), + const Country( + isoCode: 'AE', + name: 'United Arab Emirates', + dialCode: '+971', + ), + const Country( + isoCode: 'GB', + name: 'United Kingdom of Great Britain and Northern Ireland', + dialCode: '+44', + ), + const Country( + isoCode: 'US', + name: 'United States of America', + dialCode: '+1', + ), + const Country( + isoCode: 'UY', + name: 'Uruguay', + dialCode: '+598', + ), + const Country( + isoCode: 'UZ', + name: 'Uzbekistan', + dialCode: '+998', + ), + const Country( + isoCode: 'VU', + name: 'Vanuatu', + dialCode: '+678', + ), + const Country( + isoCode: 'VE', + name: 'Venezuela (Bolivarian Republic of)', + dialCode: '+58', + ), + const Country( + isoCode: 'VN', + name: 'Vietnam', + dialCode: '+84', + ), + const Country( + isoCode: 'VG', + name: 'Virgin Islands (British)', + dialCode: '+1284', + ), + const Country( + isoCode: 'VI', + name: 'Virgin Islands (U.S.)', + dialCode: '+1340', + ), + const Country( + isoCode: 'WF', + name: 'Wallis and Futuna', + dialCode: '+681', + ), + const Country( + isoCode: 'YE', + name: 'Yemen', + dialCode: '+967', + ), + const Country( + isoCode: 'ZM', + name: 'Zambia', + dialCode: '+260', + ), + const Country( + isoCode: 'ZW', + name: 'Zimbabwe', + dialCode: '+263', + ), + ]; +} diff --git a/lib/src/components/phone_input/countries_dialog.dart b/lib/src/components/phone_input/countries_dialog.dart new file mode 100644 index 00000000..6539f878 --- /dev/null +++ b/lib/src/components/phone_input/countries_dialog.dart @@ -0,0 +1,210 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zeta_flutter.dart'; +import 'countries.dart'; + +/// Class for [CountriesDialog] +class CountriesDialog extends StatefulWidget { + ///Constructor of [CountriesDialog] + const CountriesDialog({ + super.key, + this.zeta, + required this.button, + required this.items, + required this.onChanged, + this.enabled = true, + this.searchHint, + this.useRootNavigator = true, + }); + + /// Sometimes it is needed to pass an instance of [Zeta] from outside. + final Zeta? zeta; + + /// The button, which opens the dialog. + final Widget button; + + /// List of [CountriesMenuItem] + final List items; + + /// Called when an item is selected. + final ValueSetter onChanged; + + /// Determines if the button should be enabled (default) or disabled. + final bool enabled; + + /// The hint to be shown inside the country search input field. + /// Default is `Search by name or dial code`. + final String? searchHint; + + /// Determines if the root navigator should be used. + final bool useRootNavigator; + + @override + State createState() => _CountriesDialogState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IterableProperty('items', items)) + ..add(ObjectFlagProperty>.has('onChanged', onChanged)) + ..add(DiagnosticsProperty('enabled', enabled)) + ..add(DiagnosticsProperty('useRootNavigator', useRootNavigator)) + ..add(StringProperty('searchHint', searchHint)); + } +} + +class _CountriesDialogState extends State { + Future _showCountriesDialog( + BuildContext context, { + Zeta? zeta, + required List items, + bool barrierDismissible = true, + bool useRootNavigator = true, + }) => + showDialog( + context: context, + barrierDismissible: barrierDismissible, + useRootNavigator: useRootNavigator, + builder: (_) => _CountriesList( + items: items, + searchHint: widget.searchHint, + zeta: zeta, + ), + ); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: widget.items.isEmpty || !widget.enabled + ? null + : () async { + final item = await _showCountriesDialog( + context, + zeta: widget.zeta, + items: widget.items, + useRootNavigator: widget.useRootNavigator, + ); + widget.onChanged(item); + }, + child: widget.button, + ); + } +} + +class _CountriesList extends StatefulWidget { + const _CountriesList({ + required this.items, + this.searchHint, + this.zeta, + }); + + final Zeta? zeta; + final List items; + final String? searchHint; + + @override + State<_CountriesList> createState() => _CountriesListState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IterableProperty('items', items)) + ..add(StringProperty('searchHint', searchHint)); + } +} + +class _CountriesListState extends State<_CountriesList> { + late final bool _enableSearch = widget.items.length > 20; + List _items = []; + + @override + void initState() { + super.initState(); + _items = List.from(widget.items); + } + + void _search(String? text) { + final value = text ?? ''; + setState(() { + _items = widget.items.where((item) { + return item.value.name.toLowerCase().contains(value.toLowerCase()) || + (RegExp(r'^\d+$').hasMatch(value) && item.value.dialCode.indexOf('+$value') == 0); + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { + final zeta = widget.zeta ?? Zeta.of(context); + + return AlertDialog( + surfaceTintColor: zeta.colors.surfacePrimary, + shape: const RoundedRectangleBorder(borderRadius: ZetaRadius.large), + content: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_enableSearch) + Padding( + padding: const EdgeInsets.only(bottom: ZetaSpacing.b), + child: ZetaSearchBar( + onChanged: _search, + hint: widget.searchHint ?? 'Country or dial code', + shape: ZetaWidgetBorder.full, + showSpeechToText: false, + ), + ), + if (_enableSearch) + Expanded( + child: _listView(context), + ) + else + _listView(context), + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.b), + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ), + ], + ), + ), + ); + } + + Widget _listView(BuildContext context) => ListView.builder( + shrinkWrap: true, + itemCount: _items.length, + itemBuilder: (_, index) => InkWell( + onTap: () { + Navigator.of(context).pop(_items[index].value); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: ZetaSpacing.xs, + ), + child: _items[index].child, + ), + ), + ); +} + +/// [CountriesMenuItem] +/// Item for the country selection dialog. +class CountriesMenuItem { + /// Constructor for [CountriesMenuItem]. + const CountriesMenuItem({ + required this.value, + required this.child, + }); + + /// The selected value from the list. + final Country value; + + /// The widget which will represent each item in the list. + final Widget child; +} diff --git a/lib/src/components/phone_input/phone_input.dart b/lib/src/components/phone_input/phone_input.dart new file mode 100644 index 00000000..ac347360 --- /dev/null +++ b/lib/src/components/phone_input/phone_input.dart @@ -0,0 +1,362 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../../../zeta_flutter.dart'; +import 'countries.dart'; +import 'countries_dialog.dart'; + +/// ZetaPhoneInput allows entering phone numbers. +class ZetaPhoneInput extends StatefulWidget { + /// Constructor for [ZetaPhoneInput]. + const ZetaPhoneInput({ + super.key, + this.label, + this.hint, + this.enabled = true, + this.rounded = true, + this.hasError = false, + this.errorText, + this.onChanged, + this.countryDialCode, + this.phoneNumber, + this.countries, + this.countrySearchHint, + this.useRootNavigator = true, + }); + + /// If provided, displays a label above the input field. + final String? label; + + /// If provided, displays a hint below the input field. + final String? hint; + + /// Determines if the inputs should be enabled (default) or disabled. + final bool enabled; + + /// Determines if the input field corners are rounded (default) or sharp. + final bool rounded; + + /// Determines if the input field should be displayed in error style. + /// Default is `false`. + /// If `enabled` is `false`, this has no effect. + final bool hasError; + + /// In combination with `hasError: true`, provides the error message + /// to be displayed below the input field. + final String? errorText; + + /// A callback, which provides the entered phone number. + final void Function(Map?)? onChanged; + + /// The initial value for the country dial code including leading + + final String? countryDialCode; + + /// The initial value for the phone number + final String? phoneNumber; + + /// List of countries ISO 3166-1 alpha-2 codes + final List? countries; + + /// The hint to be shown inside the country search input field. + /// Default is `Search by name or dial code`. + final String? countrySearchHint; + + /// Determines if the root navigator should be used in the [CountriesDialog]. + final bool useRootNavigator; + + @override + State createState() => _ZetaPhoneInputState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('label', label)) + ..add(StringProperty('hint', hint)) + ..add(DiagnosticsProperty('enabled', enabled)) + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(DiagnosticsProperty('hasError', hasError)) + ..add(StringProperty('errorText', errorText)) + ..add(ObjectFlagProperty? p1)?>.has('onChanged', onChanged)) + ..add(StringProperty('countryDialCode', countryDialCode)) + ..add(StringProperty('phoneNumber', phoneNumber)) + ..add(IterableProperty('countries', countries)) + ..add(DiagnosticsProperty('useRootNavigator', useRootNavigator)) + ..add(StringProperty('countrySearchHint', countrySearchHint)); + } +} + +class _ZetaPhoneInputState extends State { + bool _hasError = false; + late List _countries; + late Country _selectedCountry; + late String _phoneNumber; + + @override + void initState() { + super.initState(); + _countries = widget.countries?.isEmpty ?? true + ? Countries.list + : Countries.list.where((country) => widget.countries!.contains(country.isoCode)).toList(); + if (_countries.isNotEmpty && (widget.countries?.isNotEmpty ?? false)) { + _countries.sort( + (a, b) => widget.countries!.indexOf(a.isoCode).compareTo( + widget.countries!.indexOf(b.isoCode), + ), + ); + } + if (_countries.isEmpty) _countries = Countries.list; + _selectedCountry = _countries.firstWhereOrNull( + (country) => country.dialCode == widget.countryDialCode, + ) ?? + _countries.first; + _phoneNumber = widget.phoneNumber ?? ''; + _hasError = widget.hasError; + } + + @override + void didUpdateWidget(ZetaPhoneInput oldWidget) { + super.didUpdateWidget(oldWidget); + _hasError = widget.hasError; + } + + void _onChanged({Country? selectedCountry, String? phoneNumber}) { + setState(() { + if (selectedCountry != null) _selectedCountry = selectedCountry; + if (phoneNumber != null) _phoneNumber = phoneNumber; + }); + widget.onChanged?.call( + _phoneNumber.isEmpty + ? {} + : { + 'countryDialCode': _selectedCountry.dialCode, + 'phoneNumber': _phoneNumber, + }, + ); + } + + @override + Widget build(BuildContext context) { + final zeta = Zeta.of(context); + final showError = _hasError && widget.errorText != null; + final hintErrorColor = widget.enabled + ? showError + ? zeta.colors.red + : zeta.colors.cool.shade70 + : zeta.colors.cool.shade50; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.label != null) + Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Text( + widget.label!, + style: ZetaTextStyles.bodyMedium.copyWith( + color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, + ), + ), + ), + SizedBox( + width: double.infinity, + child: Row( + children: [ + SizedBox( + width: 64, + height: 48, + child: DecoratedBox( + decoration: BoxDecoration( + color: widget.enabled ? zeta.colors.surfacePrimary : zeta.colors.cool.shade30, + borderRadius: widget.rounded + ? const BorderRadius.only( + topLeft: Radius.circular(ZetaSpacing.xxs), + bottomLeft: Radius.circular(ZetaSpacing.xxs), + ) + : ZetaRadius.none, + border: Border( + top: BorderSide(color: zeta.colors.cool.shade40), + bottom: BorderSide(color: zeta.colors.cool.shade40), + left: BorderSide(color: zeta.colors.cool.shade40), + ), + ), + child: CountriesDialog( + zeta: zeta, + useRootNavigator: widget.useRootNavigator, + enabled: widget.enabled, + searchHint: widget.countrySearchHint, + button: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: ZetaSpacing.x2_5, + ), + child: Image.asset( + _selectedCountry.flagUri, + package: 'zeta_flutter', + width: 26, + height: 18, + fit: BoxFit.fitHeight, + ), + ), + Icon( + widget.rounded ? ZetaIcons.expand_more_round : ZetaIcons.expand_more_sharp, + color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, + size: ZetaSpacing.x5, + ), + ], + ), + items: _countries + .map( + (country) => CountriesMenuItem( + value: country, + child: Row( + children: [ + SizedBox( + width: 60, + child: Text(country.dialCode), + ), + Expanded( + child: Text(country.name), + ), + ], + ), + ), + ) + .toList(), + onChanged: (value) => _onChanged(selectedCountry: value), + ), + ), + ), + Expanded( + child: TextFormField( + maxLength: 20, + initialValue: widget.phoneNumber, + enabled: widget.enabled, + inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d\s\-]'))], + keyboardType: TextInputType.phone, + onChanged: (value) => _onChanged(phoneNumber: value), + style: ZetaTextStyles.bodyMedium.copyWith( + color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, + ), + decoration: InputDecoration( + counterText: '', + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, + ), + hintStyle: ZetaTextStyles.bodyMedium.copyWith( + color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, + ), + prefixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: ZetaSpacing.xs), + child: Text( + _selectedCountry.dialCode, + style: ZetaTextStyles.bodyMedium.copyWith( + color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, + ), + ), + ), + ], + ), + prefixIconConstraints: const BoxConstraints( + minHeight: ZetaSpacing.x12, + minWidth: ZetaSpacing.x10, + ), + filled: true, + fillColor: widget.enabled + ? _hasError + ? zeta.colors.red.shade10 + : zeta.colors.surfacePrimary + : zeta.colors.cool.shade30, + enabledBorder: _hasError + ? _errorInputBorder(zeta, rounded: widget.rounded) + : _defaultInputBorder(zeta, rounded: widget.rounded), + focusedBorder: _hasError + ? _errorInputBorder(zeta, rounded: widget.rounded) + : _focusedInputBorder(zeta, rounded: widget.rounded), + disabledBorder: _defaultInputBorder(zeta, rounded: widget.rounded), + errorBorder: _errorInputBorder(zeta, rounded: widget.rounded), + focusedErrorBorder: _errorInputBorder(zeta, rounded: widget.rounded), + ), + ), + ), + ], + ), + ), + if (widget.hint != null || showError) + Padding( + padding: const EdgeInsets.only(top: 5), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: Icon( + showError && widget.enabled + ? (widget.rounded ? ZetaIcons.error_round : ZetaIcons.error_sharp) + : (widget.rounded ? ZetaIcons.info_round : ZetaIcons.info_sharp), + size: ZetaSpacing.b, + color: hintErrorColor, + ), + ), + Expanded( + child: Text( + showError && widget.enabled ? widget.errorText! : widget.hint!, + style: ZetaTextStyles.bodyXSmall.copyWith( + color: hintErrorColor, + ), + ), + ), + ], + ), + ), + ], + ); + } + + OutlineInputBorder _defaultInputBorder( + Zeta zeta, { + required bool rounded, + }) => + OutlineInputBorder( + borderRadius: rounded + ? const BorderRadius.only( + topRight: Radius.circular(ZetaSpacing.xxs), + bottomRight: Radius.circular(ZetaSpacing.xxs), + ) + : ZetaRadius.none, + borderSide: BorderSide(color: zeta.colors.cool.shade40), + ); + + OutlineInputBorder _focusedInputBorder( + Zeta zeta, { + required bool rounded, + }) => + OutlineInputBorder( + borderRadius: rounded + ? const BorderRadius.only( + topRight: Radius.circular(ZetaSpacing.xxs), + bottomRight: Radius.circular(ZetaSpacing.xxs), + ) + : ZetaRadius.none, + borderSide: BorderSide(color: zeta.colors.blue.shade50), + ); + + OutlineInputBorder _errorInputBorder( + Zeta zeta, { + required bool rounded, + }) => + OutlineInputBorder( + borderRadius: rounded + ? const BorderRadius.only( + topRight: Radius.circular(ZetaSpacing.xxs), + bottomRight: Radius.circular(ZetaSpacing.xxs), + ) + : ZetaRadius.none, + borderSide: BorderSide(color: zeta.colors.red.shade50), + ); +} diff --git a/lib/src/components/search_bar/search_bar.dart b/lib/src/components/search_bar/search_bar.dart new file mode 100644 index 00000000..514ae3f8 --- /dev/null +++ b/lib/src/components/search_bar/search_bar.dart @@ -0,0 +1,238 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zeta_flutter.dart'; + +/// ZetaSearchBar provides input field for searching. +class ZetaSearchBar extends StatefulWidget { + /// Constructor for [ZetaSearchBar]. + const ZetaSearchBar({ + super.key, + this.size, + this.shape, + this.hint, + this.initialValue, + this.onChanged, + this.onSpeechToText, + this.enabled = true, + this.showLeadingIcon = true, + this.showSpeechToText = true, + }); + + /// Determines the size of the input field. + /// Default is `ZetaSearchBarSize.large` + final ZetaWidgetSize? size; + + /// Determines the shape of the input field. + /// Default is `ZetaSearchBarShape.rounded` + final ZetaWidgetBorder? shape; + + /// If provided, displays a hint inside the input field. + /// Default is `Search`. + final String? hint; + + /// The initial value. + final String? initialValue; + + /// A callback, which provides the entered text. + final void Function(String?)? onChanged; + + /// A callback, which is invoked when the microphone button is pressed. + final Future Function()? onSpeechToText; + + /// Determines if the input field should be enabled (default) or disabled. + final bool enabled; + + /// Determines if there should be a leading icon. + /// Default is `true`. + final bool showLeadingIcon; + + /// Determines if there should be a Speech-To-Text button. + /// Default is `true`. + final bool showSpeechToText; + + @override + State createState() => _ZetaSearchBarState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('size', size)) + ..add(EnumProperty('shape', shape)) + ..add(StringProperty('hint', hint)) + ..add(DiagnosticsProperty('enabled', enabled)) + ..add(ObjectFlagProperty.has('onChanged', onChanged)) + ..add(StringProperty('initialValue', initialValue)) + ..add(ObjectFlagProperty.has('onSpeechToText', onSpeechToText)) + ..add(DiagnosticsProperty('showLeadingIcon', showLeadingIcon)) + ..add(DiagnosticsProperty('showSpeechToText', showSpeechToText)); + } +} + +class _ZetaSearchBarState extends State { + late final TextEditingController _controller; + late ZetaWidgetSize _size; + late ZetaWidgetBorder _shape; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.initialValue ?? ''); + _size = widget.size ?? ZetaWidgetSize.large; + _shape = widget.shape ?? ZetaWidgetBorder.rounded; + } + + @override + void didUpdateWidget(ZetaSearchBar oldWidget) { + super.didUpdateWidget(oldWidget); + _size = widget.size ?? ZetaWidgetSize.large; + _shape = widget.shape ?? ZetaWidgetBorder.rounded; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final zeta = Zeta.of(context); + final sharp = widget.shape == ZetaWidgetBorder.sharp; + final iconSize = _iconSize(_size); + + return TextFormField( + enabled: widget.enabled, + controller: _controller, + keyboardType: TextInputType.text, + onChanged: (value) => setState(() => widget.onChanged?.call(value)), + style: ZetaTextStyles.bodyMedium, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 10, + vertical: _inputVerticalPadding(_size), + ), + hintText: widget.hint ?? 'Search', + hintStyle: ZetaTextStyles.bodyMedium.copyWith( + color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, + ), + prefixIcon: widget.showLeadingIcon + ? Padding( + padding: const EdgeInsets.only(left: ZetaSpacing.x2_5, right: ZetaSpacing.xs), + child: Icon( + sharp ? ZetaIcons.search_sharp : ZetaIcons.search_round, + color: widget.enabled ? zeta.colors.cool.shade70 : zeta.colors.cool.shade50, + size: iconSize, + ), + ) + : null, + prefixIconConstraints: const BoxConstraints( + minHeight: ZetaSpacing.m, + minWidth: ZetaSpacing.m, + ), + suffixIcon: IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_controller.text.isNotEmpty && widget.enabled) ...[ + IconButton( + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + onPressed: () { + setState(_controller.clear); + widget.onChanged?.call(''); + }, + icon: Icon( + sharp ? ZetaIcons.cancel_sharp : ZetaIcons.cancel_round, + color: zeta.colors.cool.shade70, + size: iconSize, + ), + ), + if (widget.showSpeechToText) + SizedBox( + height: iconSize, + child: VerticalDivider( + color: zeta.colors.cool.shade40, + width: 5, + thickness: 1, + ), + ), + ], + Padding( + padding: const EdgeInsets.only(right: ZetaSpacing.xxs), + child: widget.showSpeechToText + ? IconButton( + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + onPressed: widget.onSpeechToText == null + ? null + : () async { + final text = await widget.onSpeechToText!.call(); + if (text != null) { + setState(() => _controller.text = text); + widget.onChanged?.call(text); + } + }, + icon: Icon( + sharp ? ZetaIcons.microphone_sharp : ZetaIcons.microphone_round, + size: iconSize, + ), + ) + : const SizedBox(), + ), + ], + ), + ), + suffixIconConstraints: const BoxConstraints( + minHeight: ZetaSpacing.m, + minWidth: ZetaSpacing.m, + ), + filled: widget.enabled ? null : true, + fillColor: widget.enabled ? null : zeta.colors.cool.shade30, + enabledBorder: _defaultInputBorder(zeta, shape: _shape), + focusedBorder: _focusedInputBorder(zeta, shape: _shape), + disabledBorder: _defaultInputBorder(zeta, shape: _shape), + ), + ); + } + + double _inputVerticalPadding(ZetaWidgetSize size) => switch (size) { + ZetaWidgetSize.large => ZetaSpacing.x3, + ZetaWidgetSize.medium => ZetaSpacing.x2, + ZetaWidgetSize.small => ZetaSpacing.x1, + }; + + double _iconSize(ZetaWidgetSize size) => switch (size) { + ZetaWidgetSize.large => ZetaSpacing.x6, + ZetaWidgetSize.medium => ZetaSpacing.x5, + ZetaWidgetSize.small => ZetaSpacing.x4, + }; + + OutlineInputBorder _defaultInputBorder( + Zeta zeta, { + required ZetaWidgetBorder shape, + }) => + OutlineInputBorder( + borderRadius: _borderRadius(shape), + borderSide: BorderSide(color: zeta.colors.cool.shade40), + ); + + OutlineInputBorder _focusedInputBorder( + Zeta zeta, { + required ZetaWidgetBorder shape, + }) => + OutlineInputBorder( + borderRadius: _borderRadius(shape), + borderSide: BorderSide(color: zeta.colors.blue.shade50), + ); + + BorderRadius _borderRadius(ZetaWidgetBorder shape) => switch (shape) { + ZetaWidgetBorder.rounded => ZetaRadius.minimal, + ZetaWidgetBorder.full => ZetaRadius.full, + _ => ZetaRadius.none, + }; +} diff --git a/lib/src/components/segmented_control/segmented_control.dart b/lib/src/components/segmented_control/segmented_control.dart new file mode 100644 index 00000000..43868eec --- /dev/null +++ b/lib/src/components/segmented_control/segmented_control.dart @@ -0,0 +1,621 @@ +import 'dart:math' as math; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/physics.dart'; +import 'package:flutter/rendering.dart'; +import '../../../zeta_flutter.dart'; + +/// Creates an segmented control bar. +class ZetaSegmentedControl extends StatefulWidget { + /// Constructs an segmented control bar. + const ZetaSegmentedControl({ + required this.segments, + required this.onChanged, + required this.selected, + this.rounded = true, + super.key, + }); + + /// The callback that is called when a new option is tapped. + final void Function(T)? onChanged; + + /// Whether the corners to be rounded. + final bool rounded; + + /// Descriptions of the segments in the button. + final List> segments; + + /// Currently selected segment. + final T selected; + + @override + State> createState() => _ZetaSegmentedControlState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add( + ObjectFlagProperty.has('onChanged', onChanged), + ) + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(IterableProperty>('segments', segments)) + ..add(DiagnosticsProperty('selected', selected)); + } +} + +class _ZetaSegmentedControlState extends State> + with TickerProviderStateMixin> { + T? _highlighted; + Animatable? _thumbAnimatable; + late final AnimationController _thumbController = AnimationController( + duration: kThemeAnimationDuration, + value: 0, + vsync: this, + ); + + late final _thumbScaleAnimation = _thumbScaleController.drive(Tween(begin: 1)); + + late final _thumbScaleController = AnimationController( + duration: kThemeAnimationDuration, + value: 0, + vsync: this, + ); + + @override + void initState() { + super.initState(); + + _highlighted = widget.selected; + } + + @override + void didUpdateWidget(ZetaSegmentedControl oldWidget) { + super.didUpdateWidget(oldWidget); + + if (_highlighted != widget.selected) { + _thumbController.animateWith( + SpringSimulation( + const SpringDescription(mass: 1, stiffness: 500, damping: 44), + 0, + 1, + 0, // Every time a new spring animation starts the previous animation stops. + ), + ); + _thumbAnimatable = null; + _highlighted = widget.selected; + } + } + + @override + void dispose() { + _thumbScaleController.dispose(); + _thumbController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final List children = []; + int index = 0; + int? highlightedIndex; + for (final segment in widget.segments) { + final isHighlighted = _highlighted == segment.value; + if (isHighlighted) highlightedIndex = index; + + if (index != 0) { + children.add(SizedBox(key: Key(index.toString()))); + } + + children.add( + _Segment( + key: ValueKey(segment.value), + rounded: widget.rounded, + child: segment.child, + onTap: () => widget.onChanged?.call(segment.value), + ), + ); + + index += 1; + } + + final colors = Zeta.of(context).colors; + + return MouseRegion( + cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer, + child: SelectionContainer.disabled( + child: Container( + padding: const EdgeInsets.all(ZetaSpacing.xxs), + decoration: BoxDecoration( + color: colors.surfaceDisabled, + borderRadius: widget.rounded ? ZetaRadius.minimal : ZetaRadius.none, + ), + child: AnimatedBuilder( + animation: _thumbScaleAnimation, + builder: (BuildContext context, Widget? child) { + return _SegmentedControlRenderWidget( + highlightedIndex: highlightedIndex, + thumbColor: colors.surfacePrimary, + thumbScale: _thumbScaleAnimation.value, + rounded: widget.rounded, + state: this, + children: children, + ); + }, + ), + ), + ), + ); + } +} + +/// Data describing a segment of a [ZetaSegmentedControl]. +class ZetaButtonSegment { + /// Construct a [ZetaButtonSegment]. + const ZetaButtonSegment({ + required this.value, + required this.child, + }); + + /// The child to be displayed + final Widget child; + + /// Value used to identify the segment. + /// + /// This value must be unique across all segments. + final T value; +} + +class _Segment extends StatefulWidget { + const _Segment({ + required ValueKey key, + required this.child, + required this.rounded, + required this.onTap, + }) : super(key: key); + + final Widget child; + final bool rounded; + final VoidCallback onTap; + + @override + _SegmentState createState() => _SegmentState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(ObjectFlagProperty.has('onTap', onTap)); + } +} + +class _SegmentState extends State<_Segment> with TickerProviderStateMixin<_Segment> { + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return Material( + color: Colors.transparent, + child: InkWell( + splashFactory: NoSplash.splashFactory, + borderRadius: widget.rounded ? ZetaRadius.minimal : ZetaRadius.none, + onTap: widget.onTap, + child: IndexedStack( + alignment: Alignment.center, + children: [ + widget.child, + IconTheme( + data: const IconThemeData(size: ZetaSpacing.x5), + child: DefaultTextStyle( + style: ZetaTextStyles.labelMedium.copyWith( + color: colors.textDefault, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: ZetaSpacing.l, + vertical: ZetaSpacing.xxs, + ), + child: widget.child, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _SegmentedControlRenderWidget extends MultiChildRenderObjectWidget { + const _SegmentedControlRenderWidget({ + super.key, + super.children, + required this.highlightedIndex, + required this.thumbColor, + required this.thumbScale, + required this.state, + required this.rounded, + }); + + final int? highlightedIndex; + final bool rounded; + final _ZetaSegmentedControlState state; + final Color thumbColor; + final double thumbScale; + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderSegmentedControl( + highlightedIndex: highlightedIndex, + thumbColor: thumbColor, + rounded: rounded, + state: state, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IntProperty('highlightedIndex', highlightedIndex)) + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(DiagnosticsProperty<_ZetaSegmentedControlState>('state', state)) + ..add(ColorProperty('thumbColor', thumbColor)) + ..add(DoubleProperty('thumbScale', thumbScale)); + } + + @override + void updateRenderObject( + BuildContext context, + _RenderSegmentedControl renderObject, + ) { + renderObject + ..thumbColor = thumbColor + ..highlightedIndex = highlightedIndex; + } +} + +class _SegmentedControlContainerBoxParentData extends ContainerBoxParentData {} + +class _RenderSegmentedControl extends RenderBox + with + ContainerRenderObjectMixin>, + RenderBoxContainerDefaultsMixin> { + _RenderSegmentedControl({ + required int? highlightedIndex, + required Color thumbColor, + required this.rounded, + required this.state, + }) : _highlightedIndex = highlightedIndex, + _thumbColor = thumbColor; + + // The current **Unscaled** Thumb Rect in this RenderBox's coordinate space. + Rect? currentThumbRect; + + /// Wether the corners to be rounded. + final bool rounded; + + // Paint the separator to the right of the given child. + final Paint separatorPaint = Paint(); + + final _ZetaSegmentedControlState state; + + int? _highlightedIndex; + Color _thumbColor; + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + state._thumbController.addListener(markNeedsPaint); + } + + @override + double? computeDistanceToActualBaseline(TextBaseline baseline) { + return defaultComputeDistanceToHighestActualBaseline(baseline); + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + final Size childSize = _calculateChildSize(constraints); + return _computeOverallSizeFromChildSize(childSize, constraints); + } + + @override + double computeMaxIntrinsicHeight(double width) { + RenderBox? child = firstChild; + double maxMaxChildHeight = ZetaSpacing.x7; + while (child != null) { + final double childHeight = child.getMaxIntrinsicHeight(width); + maxMaxChildHeight = math.max(maxMaxChildHeight, childHeight); + child = nonSeparatorChildAfter(child); + } + return maxMaxChildHeight; + } + + @override + double computeMaxIntrinsicWidth(double height) { + final int childCount = this.childCount ~/ 2 + 1; + RenderBox? child = firstChild; + double maxMaxChildWidth = 0; + while (child != null) { + final double childWidth = child.getMaxIntrinsicWidth(height); + maxMaxChildWidth = math.max(maxMaxChildWidth, childWidth); + child = nonSeparatorChildAfter(child); + } + return maxMaxChildWidth * childCount + totalSeparatorWidth; + } + + @override + double computeMinIntrinsicHeight(double width) { + RenderBox? child = firstChild; + double maxMinChildHeight = ZetaSpacing.x7; + while (child != null) { + final double childHeight = child.getMinIntrinsicHeight(width); + maxMinChildHeight = math.max(maxMinChildHeight, childHeight); + child = nonSeparatorChildAfter(child); + } + return maxMinChildHeight; + } + + @override + double computeMinIntrinsicWidth(double height) { + final int childCount = this.childCount ~/ 2 + 1; + RenderBox? child = firstChild; + double maxMinChildWidth = 0; + while (child != null) { + final double childWidth = child.getMinIntrinsicWidth(height); + maxMinChildWidth = math.max(maxMinChildWidth, childWidth); + child = nonSeparatorChildAfter(child); + } + return (maxMinChildWidth + 2 * ZetaSpacing.l) * childCount + totalSeparatorWidth; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('currentThumbRect', currentThumbRect)) + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(DiagnosticsProperty('separatorPaint', separatorPaint)) + ..add(DiagnosticsProperty<_ZetaSegmentedControlState>('state', state)) + ..add(IntProperty('highlightedIndex', highlightedIndex)) + ..add(ColorProperty('thumbColor', thumbColor)) + ..add(DoubleProperty('totalSeparatorWidth', totalSeparatorWidth)); + } + + @override + void detach() { + state._thumbController.removeListener(markNeedsPaint); + super.detach(); + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + RenderBox? child = lastChild; + while (child != null) { + final _SegmentedControlContainerBoxParentData childParentData = + child.parentData! as _SegmentedControlContainerBoxParentData; + if ((childParentData.offset & child.size).contains(position)) { + return result.addWithPaintOffset( + offset: childParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset localOffset) { + return child!.hitTest(result, position: localOffset); + }, + ); + } + child = childParentData.previousSibling; + } + return false; + } + + @override + void paint(PaintingContext context, Offset offset) { + final List children = getChildrenAsList(); + + for (int index = 1; index < childCount; index += 2) { + _paintSeparator(context, offset, children[index]); + } + + final int? highlightedChildIndex = highlightedIndex; + // Paint thumb if there's a highlighted segment. + if (highlightedChildIndex != null) { + final RenderBox selectedChild = children[highlightedChildIndex * 2]; + + final _SegmentedControlContainerBoxParentData childParentData = + selectedChild.parentData! as _SegmentedControlContainerBoxParentData; + final newThumbRect = childParentData.offset & selectedChild.size; + + // Update thumb animation's tween, in case the end rect changed (e.g., a + // new segment is added during the animation). + if (state._thumbController.isAnimating) { + final Animatable? thumbTween = state._thumbAnimatable; + if (thumbTween == null) { + // This is the first frame of the animation. + final Rect startingRect = moveThumbRectInBound(currentThumbRect, children) ?? newThumbRect; + state._thumbAnimatable = RectTween(begin: startingRect, end: newThumbRect); + } else if (newThumbRect != thumbTween.transform(1)) { + // The thumbTween of the running sliding animation needs updating, + // without restarting the animation. + final Rect startingRect = moveThumbRectInBound(currentThumbRect, children) ?? newThumbRect; + state._thumbAnimatable = RectTween(begin: startingRect, end: newThumbRect).chain( + CurveTween(curve: Interval(state._thumbController.value, 1)), + ); + } + } else { + state._thumbAnimatable = null; + } + + final Rect unscaledThumbRect = state._thumbAnimatable?.evaluate(state._thumbController) ?? newThumbRect; + currentThumbRect = unscaledThumbRect; + final Rect thumbRect = Rect.fromCenter( + center: unscaledThumbRect.center, + width: unscaledThumbRect.width, + height: unscaledThumbRect.height, + ); + + _paintThumb(context, offset, thumbRect); + } else { + currentThumbRect = null; + } + + for (int index = 0; index < children.length; index += 2) { + _paintChild(context, offset, children[index]); + } + } + + @override + void performLayout() { + final BoxConstraints constraints = this.constraints; + final Size childSize = _calculateChildSize(constraints); + final BoxConstraints childConstraints = BoxConstraints.tight(childSize); + final BoxConstraints separatorConstraints = childConstraints.heightConstraints(); + + RenderBox? child = firstChild; + int index = 0; + double start = 0; + while (child != null) { + child.layout( + index.isEven ? childConstraints : separatorConstraints, + parentUsesSize: true, + ); + final _SegmentedControlContainerBoxParentData childParentData = + child.parentData! as _SegmentedControlContainerBoxParentData; + final Offset childOffset = Offset(start, 0); + childParentData.offset = childOffset; + start += child.size.width; + + child = childAfter(child); + index += 1; + } + + size = _computeOverallSizeFromChildSize(childSize, constraints); + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! _SegmentedControlContainerBoxParentData) { + child.parentData = _SegmentedControlContainerBoxParentData(); + } + } + + int? get highlightedIndex => _highlightedIndex; + + set highlightedIndex(int? value) { + if (_highlightedIndex == value) { + return; + } + + _highlightedIndex = value; + markNeedsPaint(); + } + + Color get thumbColor => _thumbColor; + + set thumbColor(Color value) { + if (_thumbColor == value) { + return; + } + _thumbColor = value; + markNeedsPaint(); + } + + double get totalSeparatorWidth => 0.0 * (childCount ~/ 2); + + RenderBox? nonSeparatorChildAfter(RenderBox child) { + final RenderBox? nextChild = childAfter(child); + return nextChild == null ? null : childAfter(nextChild); + } + + // This method is used to convert the original unscaled thumb rect painted in + // the previous frame, to a Rect that is within the valid boundary defined by + // the child segments. + // + // The overall size does not include that of the thumb. That is, if the thumb + // is located at the first or the last segment, the thumb can get cut off if + // one of the values in _kThumbInsets is positive. + Rect? moveThumbRectInBound(Rect? thumbRect, List children) { + if (thumbRect == null) { + return null; + } + + final Offset firstChildOffset = (children.first.parentData! as _SegmentedControlContainerBoxParentData).offset; + final double leftMost = firstChildOffset.dx; + final double rightMost = + (children.last.parentData! as _SegmentedControlContainerBoxParentData).offset.dx + children.last.size.width; + + // Ignore the horizontal position and the height of `thumbRect`, and + // calculates them from `children`. + return Rect.fromLTRB( + math.max(thumbRect.left, leftMost), + firstChildOffset.dy, + math.min(thumbRect.right, rightMost), + firstChildOffset.dy + children.first.size.height, + ); + } + + Size _calculateChildSize(BoxConstraints constraints) { + final int childCount = this.childCount ~/ 2 + 1; + double childWidth = (constraints.minWidth - totalSeparatorWidth) / childCount; + double maxHeight = ZetaSpacing.x7; + RenderBox? child = firstChild; + while (child != null) { + childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity) + 2); + child = nonSeparatorChildAfter(child); + } + childWidth = math.min( + childWidth, + (constraints.maxWidth - totalSeparatorWidth) / childCount, + ); + child = firstChild; + while (child != null) { + final double boxHeight = child.getMaxIntrinsicHeight(childWidth); + maxHeight = math.max(maxHeight, boxHeight); + child = nonSeparatorChildAfter(child); + } + return Size(childWidth, maxHeight); + } + + Size _computeOverallSizeFromChildSize( + Size childSize, + BoxConstraints constraints, + ) { + final int childCount = this.childCount ~/ 2 + 1; + return constraints.constrain( + Size( + childSize.width * childCount + totalSeparatorWidth, + childSize.height, + ), + ); + } + + void _paintSeparator( + PaintingContext context, + Offset offset, + RenderBox child, + ) { + final _SegmentedControlContainerBoxParentData childParentData = + child.parentData! as _SegmentedControlContainerBoxParentData; + context.paintChild(child, offset + childParentData.offset); + } + + void _paintChild(PaintingContext context, Offset offset, RenderBox child) { + final _SegmentedControlContainerBoxParentData childParentData = + child.parentData! as _SegmentedControlContainerBoxParentData; + context.paintChild(child, childParentData.offset + offset); + } + + void _paintThumb(PaintingContext context, Offset offset, Rect thumbRect) { + final RRect thumbRRect = RRect.fromRectAndRadius( + thumbRect.shift(offset), + rounded ? ZetaRadius.minimal.topLeft : ZetaRadius.none.topLeft, + ); + + context.canvas.drawRRect( + thumbRRect, + Paint()..color = thumbColor, + ); + } +} diff --git a/lib/src/components/tooltip/tooltip.dart b/lib/src/components/tooltip/tooltip.dart new file mode 100644 index 00000000..8f2fcc41 --- /dev/null +++ b/lib/src/components/tooltip/tooltip.dart @@ -0,0 +1,219 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../../zeta_flutter.dart'; + +const _horizontalArrowSize = Size(4, 8); +const _verticalArrowSize = Size(8, 4); + +/// The direction of [ZetaTooltip]'s arrow +enum ZetaTooltipArrowDirection { + /// [up] + up, + + /// [down] + down, + + /// [left] + left, + + /// [right] + right, +} + +/// [ZetaTooltip] +class ZetaTooltip extends StatelessWidget { + /// Constructor for [ZetaTooltip]. + const ZetaTooltip({ + super.key, + required this.child, + this.rounded = true, + this.padding, + this.color, + this.textStyle, + this.arrowDirection = ZetaTooltipArrowDirection.down, + this.maxWidth, + }); + + /// The content of the tooltip. + final Widget child; + + /// Determines if the tooltip should have rounded or sharp corners. + /// Default is `true` (rounded). + final bool rounded; + + /// The padding inside the [ZetaTooltip]. + /// Default is: + /// ``` + /// const EdgeInsets.symmetric( + /// horizontal: ZetaSpacing.xs, + /// vertical: ZetaSpacing.xxs, + /// ) + /// ``` + final EdgeInsets? padding; + + /// The color of the tooltip. + /// Default is `zeta.colors.textDefault`. + final Color? color; + + /// The text style of the tooltip. + /// Default is: + /// ``` + /// ZetaTextStyles.bodyXSmall.copyWith( + /// color: zeta.colors.textInverse, + /// fontWeight: FontWeight.w500, + /// ), + /// ``` + final TextStyle? textStyle; + + /// The direction of the tooltip's arrow. + /// Default is `ZetaTooltipArrowDirection.down`. + final ZetaTooltipArrowDirection arrowDirection; + + /// The maximum width of the tooltip. + final double? maxWidth; + + @override + Widget build(BuildContext context) { + final zeta = Zeta.of(context); + final color = this.color ?? zeta.colors.textDefault; + final horizontalArrowWidth = + [ZetaTooltipArrowDirection.left, ZetaTooltipArrowDirection.right].contains(arrowDirection) + ? _horizontalArrowSize.width + : 0; + + return LayoutBuilder( + builder: (context, constraints) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (arrowDirection == ZetaTooltipArrowDirection.up) + Center( + child: CustomPaint( + painter: _FilledArrow( + color: color, + direction: arrowDirection, + ), + size: _verticalArrowSize, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (arrowDirection == ZetaTooltipArrowDirection.left) + Center( + child: CustomPaint( + painter: _FilledArrow( + color: color, + direction: arrowDirection, + ), + size: _horizontalArrowSize, + ), + ), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: maxWidth ?? (constraints.maxWidth - horizontalArrowWidth), + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: color, + borderRadius: rounded ? ZetaRadius.minimal : null, + ), + child: Padding( + padding: padding ?? + const EdgeInsets.symmetric( + horizontal: ZetaSpacing.xs, + vertical: ZetaSpacing.xxs, + ), + child: DefaultTextStyle( + style: textStyle ?? + ZetaTextStyles.bodyXSmall.copyWith( + color: zeta.colors.textInverse, + fontWeight: FontWeight.w500, + ), + child: child, + ), + ), + ), + ), + if (arrowDirection == ZetaTooltipArrowDirection.right) + Center( + child: CustomPaint( + painter: _FilledArrow( + color: color, + direction: arrowDirection, + ), + size: _horizontalArrowSize, + ), + ), + ], + ), + if (arrowDirection == ZetaTooltipArrowDirection.down) + Center( + child: CustomPaint( + painter: _FilledArrow( + color: color, + direction: arrowDirection, + ), + size: _verticalArrowSize, + ), + ), + ], + ); + }, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(DiagnosticsProperty('padding', padding)) + ..add(ColorProperty('color', color)) + ..add(DiagnosticsProperty('textStyle', textStyle)) + ..add(EnumProperty('arrowDirection', arrowDirection)) + ..add(DoubleProperty('maxWidth', maxWidth)); + } +} + +class _FilledArrow extends CustomPainter { + _FilledArrow({ + required this.direction, + this.color, + }); + + final Color? color; + final ZetaTooltipArrowDirection direction; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..style = PaintingStyle.fill + ..color = color ?? Colors.transparent; + final path = switch (direction) { + ZetaTooltipArrowDirection.up => Path() + ..moveTo(size.width / 2, 0) + ..lineTo(0, size.height) + ..lineTo(size.width, size.height) + ..lineTo(size.width / 2, 0), + ZetaTooltipArrowDirection.left => Path() + ..moveTo(size.width, 0) + ..lineTo(0, size.height / 2) + ..lineTo(size.width, size.height) + ..lineTo(size.width, 0), + ZetaTooltipArrowDirection.down => Path() + ..lineTo(size.width, 0) + ..lineTo(size.width / 2, size.height) + ..lineTo(0, 0), + ZetaTooltipArrowDirection.right => Path() + ..lineTo(size.width, size.height / 2) + ..lineTo(0, size.height) + ..lineTo(0, 0), + }; + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/src/theme/tokens.dart b/lib/src/theme/tokens.dart index 5a31e659..caecce51 100644 --- a/lib/src/theme/tokens.dart +++ b/lib/src/theme/tokens.dart @@ -111,6 +111,9 @@ class ZetaRadius { /// Border radius used when rounded parameter is true; 8px radius. static const BorderRadius rounded = BorderRadius.all(Radius.circular(ZetaSpacing.xs)); + /// Large border radius; 16px radius. + static const BorderRadius large = BorderRadius.all(Radius.circular(ZetaSpacing.b)); + /// Wide border radius; 24px radius. static const BorderRadius wide = BorderRadius.all(Radius.circular(ZetaSpacing.m)); diff --git a/lib/src/utils/extensions.dart b/lib/src/utils/extensions.dart index 77dc6bcc..3b4d5a45 100644 --- a/lib/src/utils/extensions.dart +++ b/lib/src/utils/extensions.dart @@ -101,3 +101,14 @@ extension StringExtensions on String? { return '${this![0].toUpperCase()}${this!.substring(1).toLowerCase()}'; } } + +/// Extension [FirstWhereOrNull] on [Iterable]. +extension FirstWhereOrNull on Iterable { + /// Returns the first element satisfying test, or null if there are none. + T? firstWhereOrNull(bool Function(T element) test) { + for (final element in this) { + if (test(element)) return element; + } + return null; + } +} diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index bca4aef8..f56ab17e 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -3,6 +3,7 @@ library zeta_flutter; export 'src/assets/icons.dart'; export 'src/components/accordion/accordion.dart'; +export 'src/components/app_bar/app_bar.dart'; export 'src/components/avatars/avatar.dart'; export 'src/components/badges/badge.dart'; export 'src/components/badges/indicator.dart'; @@ -24,19 +25,25 @@ export 'src/components/checkbox/checkbox.dart'; export 'src/components/chips/chip.dart'; export 'src/components/date_input/date_input.dart'; export 'src/components/dial_pad/dial_pad.dart'; +export 'src/components/dialog/dialog.dart'; export 'src/components/dropdown/dropdown.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'; export 'src/components/pagination/pagination.dart'; export 'src/components/password/password_input.dart'; +export 'src/components/phone_input/phone_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/search_bar/search_bar.dart'; +export 'src/components/segmented_control/segmented_control.dart'; export 'src/components/snack_bar/snack_bar.dart'; export 'src/components/stepper/stepper.dart'; export 'src/components/switch/zeta_switch.dart'; export 'src/components/tabs/tab.dart'; export 'src/components/tabs/tab_bar.dart'; +export 'src/components/tooltip/tooltip.dart'; export 'src/theme/color_extensions.dart'; export 'src/theme/color_scheme.dart'; export 'src/theme/color_swatch.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 4c1527ab..9c230be8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,4 +45,6 @@ flutter: - family: zeta-icons-sharp fonts: - asset: lib/src/assets/fonts/zeta-icons-sharp.ttf + assets: + - lib/src/assets/flags/ uses-material-design: true