diff --git a/.firebaserc b/.firebaserc index 91581093..6ff5098d 100644 --- a/.firebaserc +++ b/.firebaserc @@ -1,5 +1,14 @@ { "projects": { "default": "zeta-ds" + }, + "targets": { + "zeta-ds": { + "hosting": { + "flutter": [ + "zeta-flutter-main" + ] + } + } } } \ No newline at end of file diff --git a/.github/workflows/on-main.yml b/.github/workflows/on-main.yml index 615b41fb..bb3b9aa4 100644 --- a/.github/workflows/on-main.yml +++ b/.github/workflows/on-main.yml @@ -45,3 +45,21 @@ jobs: git add -A git commit -m '[automated commit] lint format and import sort' git push -f + deploy-qa-demo: + name: Deploy preview version of the storybook on firebase + needs: code-quality + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + - name: Setup flutter + run: flutter pub get + - name: Build example app + run: | + cd example + flutter build web -t widgetbook/main.dart -o ../build --no-tree-shake-icons + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: "${{ secrets.GITHUB_TOKEN }}" + firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_ZETA_DS }}" + channelId: "live" diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 31513970..dab856c5 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -87,9 +87,6 @@ jobs: with: ref: ${{ github.head_ref }} - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.19.x" - channel: "stable" - name: Setup flutter run: flutter pub get - name: Build example app @@ -100,6 +97,4 @@ jobs: with: repoToken: "${{ secrets.GITHUB_TOKEN }}" firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_ZETA_DS }}" - expires: 7d - projectId: zeta-ds channelId: "pr-${{ github.event.number }}-${{ github.event.pull_request.head.ref }}" diff --git a/CONTRIBUTING b/CONTRIBUTING deleted file mode 100644 index 2ccbf54b..00000000 --- a/CONTRIBUTING +++ /dev/null @@ -1,27 +0,0 @@ -# Getting Involved - -Thank you for your interest in this project. We'd love to see your contributions. There are just few small guidelines you need to follow. -Please note we have a code of conduct, please follow it in all your interactions with the project. - -## Opening an issue - -If you've noticed a bug or you have a suggestion for a new feature, please go ahead and open an issue in this project. Please do include as much information as possible. - -Please file issues before doing substantial work; this will ensure that others don't duplicate the work and that there's a chance to discuss any design issues. - -## Making a code change - -We're always open to pull requests, but these should be small and clearly described so that we can understand what you're trying to do. - -When you're ready to start coding, fork the needed repository to your own GitHub account and make your changes in a new branch. Once you're happy, open a pull request and explain what the change is and why you think we should include it in our project. - -## Code reviews - -All submissions, including submissions by project members, require review. We use GitHub pull requests (PRs) for this purpose. Consult [GitHub Help](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) for more information on using pull requests. - -Before a PR can be reviewed, ensure you have done the following, and fixed any issues that may arise: - -- Ensure branch is up to date `git rebase main` -- Check formatting: `flutter format .` -- Run static analyses: `flutter analyze` -- Run unit-tests: `flutter test` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..4b53985d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# Getting Involved + +Thank you for your interest in this project. We'd love to see your contributions. There are just few small guidelines you need to follow. +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Opening an issue + +If you've noticed a bug or you have a suggestion for a new feature, please go ahead and open an issue in this project. Please do include as much information as possible. + +Please file issues before doing substantial work; this will ensure that others don't duplicate the work and that there's a chance to discuss any design issues. + +## Making a code change + +We're always open to pull requests, but these should be small and clearly described so that we can understand what you're trying to do. + +When you're ready to start coding, fork the needed repository to your own GitHub account and make your changes in a new branch. Once you're happy, open a pull request and explain what the change is and why you think we should include it in our project. + +If the change is a bug fix, try to create a test that aligns with the bug. + +### Creating a new component + +We want the designs to be the source of truth for this repository, so new components will only be accepted if they are with the design files. + +New components should use all tokens matching the design, and should not use hardcoded values for color, spacing, or radius. This ensures that changes made to these fundamental tokens are reflected throughout the library. + +All components should have inline [dartdoc](https://dart.dev/tools/dart-doc) documentation on public functions and variables. This is enforced by the lint rules. + +To demonstrate a component, we need to create 2 examples: firstly in the zeta_flutter example app and secondly in widgetbook. + +Example app should show basic examples to compare against the designs and is typically used by developers whilst building out components. + +The widgetbook is used by the wider team to review components. We should attempt to show the full functionality of a component in a single Widgetbook instance. We have some helper functions for building knobs for icons and rounded bool in [utils.dart](./example/widgetbook/utils/utils.dart). +For more information on widgetbook, [read the docs](https://docs.widgetbook.io/). + +We should also create a test for each widget created. + +## Code reviews + +All submissions, including submissions by project members, require review. We use GitHub pull requests (PRs) for this purpose. Consult [GitHub Help](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) for more information on using pull requests. + +Before a PR can be reviewed, ensure you have done the following, and fixed any issues that may arise: + +- Ensure branch is up to date `git rebase main` +- Check formatting: `flutter format .` +- Run static analyses: `flutter analyze` +- Run unit-tests: `flutter test` + +All PRs should be titled according to [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), as the branch will be squashed, so the PR title will become the commit message. +Examples: + +- `feat(X):` for new features +- `fix(x):` for bug fixes +- `chore(x):` for admin / chores. diff --git a/README.md b/README.md index 703ca8e7..1aa10b68 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,9 @@ With these configurations, Zeta makes it easy to achieve consistent theming thro ## Viewing the components -To view examples of all the components in the library, you can run the example app in this repo or go to [Zeta](https://zeta-ds.web.app/) +To view examples of all the components in the library, you can pull this repo and run either the example app or widgetbook instance. + +You can also view the latest release at [Zeta](https://zeta-ds.web.app/) or the latest commits to main [here](https://zeta-flutter-main.web.app/). ## Licensing diff --git a/example/lib/home.dart b/example/lib/home.dart index 02538b11..8c6465f4 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -9,8 +9,16 @@ import 'package:zeta_example/pages/components/breadcrumbs_example.dart'; 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/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/radio_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/theme/color_example.dart'; import 'package:zeta_example/pages/components/password_input_example.dart'; import 'package:zeta_example/pages/components/progress_example.dart'; @@ -38,10 +46,18 @@ final List components = [ Component(ButtonExample.name, (context) => const ButtonExample()), Component(CheckBoxExample.name, (context) => const CheckBoxExample()), Component(ChipExample.name, (context) => const ChipExample()), + Component(ListItemExample.name, (context) => const ListItemExample()), Component(NavigationBarExample.name, (context) => const NavigationBarExample()), Component(PasswordInputExample.name, (context) => const PasswordInputExample()), + Component(DropdownExample.name, (context) => const DropdownExample()), Component(ProgressExample.name, (context) => const ProgressExample()), + Component(SnackBarExample.name, (context) => const SnackBarExample()), + Component(StepperExample.name, (context) => const StepperExample()), + Component(TabsExample.name, (context) => const TabsExample()), Component(DialPadExample.name, (context) => const DialPadExample()), + Component(RadioButtonExample.name, (context) => const RadioButtonExample()), + Component(SwitchExample.name, (context) => const SwitchExample()), + Component(DateInputExample.name, (context) => const DateInputExample()), ]; final List theme = [ diff --git a/example/lib/pages/components/date_input_example.dart b/example/lib/pages/components/date_input_example.dart new file mode 100644 index 00000000..83ac0812 --- /dev/null +++ b/example/lib/pages/components/date_input_example.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class DateInputExample extends StatefulWidget { + static const String name = 'DateInput'; + + const DateInputExample({Key? key}) : super(key: key); + + @override + State createState() => _DateInputExampleState(); +} + +class _DateInputExampleState extends State { + String? _errorText; + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: 'Date 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: ZetaDateInput( + label: 'Birthdate', + hint: 'Enter birthdate', + hasError: _errorText != null, + errorText: _errorText ?? 'Invalid date', + onChanged: (value) { + if (value == null) return setState(() => _errorText = null); + final now = DateTime.now(); + setState( + () => _errorText = value.difference(DateTime(now.year, now.month, now.day)).inDays > 0 + ? 'Birthdate cannot be in the future' + : null, + ); + }, + ), + ), + 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: ZetaDateInput( + label: 'Label', + hint: 'Default hint text', + errorText: 'Oops! Error hint text', + rounded: false, + datePattern: 'yyyy-MM-dd', + ), + ), + 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: ZetaDateInput( + label: 'Label', + hint: 'Default hint text', + enabled: false, + ), + ), + Divider(color: Colors.grey[200]), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Medium', style: ZetaTextStyles.titleMedium), + ), + Padding( + padding: const EdgeInsets.all(20), + child: ZetaDateInput( + label: 'Label', + hint: 'Default hint text', + errorText: 'Oops! Error hint text', + size: ZetaDateInputSize.medium, + ), + ), + Divider(color: Colors.grey[200]), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Small', style: ZetaTextStyles.titleMedium), + ), + Padding( + padding: const EdgeInsets.all(20), + child: ZetaDateInput( + label: 'Label', + hint: 'Default hint text', + errorText: 'Oops! Error hint text', + size: ZetaDateInputSize.small, + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/dropdown_example.dart b/example/lib/pages/components/dropdown_example.dart new file mode 100644 index 00000000..0b37dcc2 --- /dev/null +++ b/example/lib/pages/components/dropdown_example.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class DropdownExample extends StatefulWidget { + static const String name = "Dropdown"; + const DropdownExample({super.key}); + + @override + State createState() => _DropdownExampleState(); +} + +class _DropdownExampleState extends State { + ZetaDropdownItem selectedItem = ZetaDropdownItem( + value: "Item 1", + leadingIcon: Icon(ZetaIcons.star_round), + ); + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: "Dropdown", + child: Center( + child: SingleChildScrollView( + child: SizedBox( + width: 320, + child: Column(children: [ + ZetaDropdown( + leadingType: LeadingStyle.checkbox, + onChange: (value) { + setState(() { + selectedItem = value; + }); + }, + selectedItem: selectedItem, + items: [ + ZetaDropdownItem( + value: "Item 1", + leadingIcon: Icon(ZetaIcons.star_round), + ), + ZetaDropdownItem( + value: "Item 2", + leadingIcon: Icon(ZetaIcons.star_half_round), + ), + ZetaDropdownItem( + value: "Item 3", + ) + ], + ), + Text('Selected item : ${selectedItem.value}') + ])), + ), + ), + ); + } +} diff --git a/example/lib/pages/components/list_item_example.dart b/example/lib/pages/components/list_item_example.dart new file mode 100644 index 00000000..9cc60696 --- /dev/null +++ b/example/lib/pages/components/list_item_example.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class ListItemExample extends StatefulWidget { + static const String name = 'ListItem'; + + const ListItemExample({super.key}); + + @override + State createState() => _ListItemExampleState(); +} + +class _ListItemExampleState extends State { + bool _isCheckBoxEnabled = false; + bool _isSelected = true; + + _onDefaultListItemTap() { + setState(() => _isCheckBoxEnabled = !_isCheckBoxEnabled); + } + + @override + Widget build(BuildContext context) { + final zetaColors = Zeta.of(context).colors; + + return ExampleScaffold( + name: ListItemExample.name, + child: Container( + color: zetaColors.surfaceSecondary, + child: SingleChildScrollView( + child: Column( + children: [ + // List Item with descriptor + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaListItem( + dense: true, + leading: Container( + width: 48, + height: 48, + decoration: BoxDecoration(borderRadius: ZetaRadius.rounded), + child: Placeholder(), + ), + subtitle: Text("Descriptor"), + title: Text("List Item"), + trailing: ZetaCheckbox( + value: _isCheckBoxEnabled, + onChanged: (_) => _onDefaultListItemTap(), + ), + onTap: _onDefaultListItemTap, + ), + ), + + // Enabled + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.l), + child: Text( + "Enabled", + style: ZetaTextStyles.titleLarge, + ), + ), + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.m), + child: ZetaListItem(title: Text("List Item")), + ), + + // Selected + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.l), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "Selected", + style: ZetaTextStyles.titleLarge, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.m), + child: ZetaListItem( + title: Text("List Item"), + selected: _isSelected, + trailing: _isSelected + ? Icon( + ZetaIcons.check_mark_sharp, + color: zetaColors.primary, + ) + : null, + onTap: () => setState(() => _isSelected = !_isSelected), + ), + ), + + // Disabled + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.l), + child: Text( + "Disabled", + style: ZetaTextStyles.titleLarge, + ), + ), + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.m), + child: ZetaListItem( + title: Text("List Item"), + enabled: false, + onTap: () {}, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/pages/components/radio_example.dart b/example/lib/pages/components/radio_example.dart new file mode 100644 index 00000000..bd7b5f26 --- /dev/null +++ b/example/lib/pages/components/radio_example.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class RadioButtonExample extends StatefulWidget { + static const String name = 'RadioButton'; + + const RadioButtonExample({Key? key}) : super(key: key); + + @override + State createState() => _RadioButtonExampleState(); +} + +class _RadioButtonExampleState extends State { + String option1 = 'Label 1'; + String option2 = 'Label 2'; + String? groupValue; + bool isEnabled = true; + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: 'Radio Button', + child: Center( + child: Column( + children: [ + ZetaRadio( + value: option1, + groupValue: groupValue, + onChanged: isEnabled ? (value) => setState(() => groupValue = value) : null, + label: Text(option1), + ), + ZetaRadio( + value: option2, + groupValue: groupValue, + onChanged: isEnabled ? (value) => setState(() => groupValue = value) : null, + label: Text(option2), + ), + ZetaButton( + label: isEnabled ? 'Disable' : 'Enable', + onPressed: () => setState(() => isEnabled = !isEnabled), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/snackbar_example.dart b/example/lib/pages/components/snackbar_example.dart new file mode 100644 index 00000000..b07ab311 --- /dev/null +++ b/example/lib/pages/components/snackbar_example.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class SnackBarExample extends StatelessWidget { + static const String name = 'SnackBar'; + + const SnackBarExample({super.key}); + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: SnackBarExample.name, + child: SingleChildScrollView( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + // Standard Rounded + Row( + children: [ + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Standard Rounded SnackBar", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + actionLabel: "Action", + content: Text('This is a snackbar'), + ), + ); + }, + ), + ), + ], + ), + + // Standard Sharp + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Standard Sharp SnackBar", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + actionLabel: "Action", + rounded: false, + content: Text('This is a snackbar'), + ), + ); + }, + ), + ), + + // Default + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Contectual Default", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.defaultType, + leadingIcon: Icon(Icons.mood_rounded), + content: Text('Message with icon'), + ), + ); + }, + ), + ), + + // Action + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Action", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.action, + onPressed: () {}, + actionLabel: "Action", + content: Text('Actionable message with icon'), + ), + ); + }, + ), + ), + + // Positive + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Positive", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.positive, + content: Text('Request sent successfully'), + ), + ); + }, + ), + ), + + // Info + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Info", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.info, + content: Text('Information is being displayed'), + ), + ); + }, + ), + ), + + // Info + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Info", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.info, + content: Text('Information is being displayed'), + ), + ); + }, + ), + ), + + // Warning + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Warning", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.warning, + content: Text('Warning has been issued'), + ), + ); + }, + ), + ), + + // Error + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Error", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.error, + content: Text('Error has been detected'), + ), + ); + }, + ), + ), + + // Deletion + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "Deletion", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.deletion, + onPressed: () {}, + content: Text('Item was deleted'), + ), + ); + }, + ), + ), + + // View + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.x4), + child: ZetaButton.primary( + label: "View", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + ZetaSnackBar( + context: context, + type: ZetaSnackBarType.view, + onPressed: () {}, + content: Text('Something neeeds your attention'), + ), + ); + }, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/stepper_example.dart b/example/lib/pages/components/stepper_example.dart new file mode 100644 index 00000000..594b43da --- /dev/null +++ b/example/lib/pages/components/stepper_example.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class StepperExample extends StatefulWidget { + const StepperExample({super.key}); + + static const String name = 'Stepper'; + + @override + State createState() => _StepperExampleState(); +} + +class _StepperExampleState extends State { + int _roundedHorizontalStep = 0; + int _sharpHorizontalStep = 0; + int _verticalStep = 0; + + ZetaStepType _getForStepIndex({ + required int currentStep, + required int stepIndex, + }) { + if (currentStep == stepIndex) return ZetaStepType.enabled; + if (currentStep > stepIndex) return ZetaStepType.complete; + + return ZetaStepType.disabled; + } + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: StepperExample.name, + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox( + height: 150, + child: ZetaStepper( + currentStep: _roundedHorizontalStep, + onStepTapped: (index) => setState(() => _roundedHorizontalStep = index), + steps: [ + ZetaStep( + type: _getForStepIndex( + currentStep: _roundedHorizontalStep, + stepIndex: 0, + ), + title: Text("Title"), + content: Text("Content"), + ), + ZetaStep( + type: _getForStepIndex( + currentStep: _roundedHorizontalStep, + stepIndex: 1, + ), + title: Text("Title 2"), + ), + ZetaStep( + type: _getForStepIndex( + currentStep: _roundedHorizontalStep, + stepIndex: 2, + ), + title: Text("Title 3"), + content: Text("Content 3"), + ), + ], + ), + ), + SizedBox( + height: 150, + child: ZetaStepper( + rounded: false, + currentStep: _sharpHorizontalStep, + onStepTapped: (index) => setState(() => _sharpHorizontalStep = index), + steps: [ + ZetaStep( + type: _getForStepIndex( + currentStep: _sharpHorizontalStep, + stepIndex: 0, + ), + title: Text("Title"), + content: Text("Content"), + ), + ZetaStep( + type: _getForStepIndex( + currentStep: _sharpHorizontalStep, + stepIndex: 1, + ), + title: Text("Title 2"), + content: Text("Content 2"), + ), + ZetaStep( + type: _getForStepIndex( + currentStep: _sharpHorizontalStep, + stepIndex: 2, + ), + title: Text("Title 3"), + content: Text("Content 3"), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: ZetaSpacing.l), + child: ZetaStepper( + type: ZetaStepperType.vertical, + currentStep: _verticalStep, + onStepTapped: (index) => setState(() => _verticalStep = index), + steps: [ + ZetaStep( + type: _getForStepIndex( + currentStep: _verticalStep, + stepIndex: 0, + ), + title: Text("Title"), + subtitle: Text("Step Number"), + content: Text("Content"), + ), + ZetaStep( + type: _getForStepIndex( + currentStep: _verticalStep, + stepIndex: 1, + ), + title: Text("Title 2"), + subtitle: Text("Step Number"), + content: Text("Content 2"), + ), + ZetaStep( + type: _getForStepIndex( + currentStep: _verticalStep, + stepIndex: 2, + ), + title: Text("Title 3"), + subtitle: Text("Step Number"), + content: Text("Content 3"), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/switch_example.dart b/example/lib/pages/components/switch_example.dart new file mode 100644 index 00000000..471e0961 --- /dev/null +++ b/example/lib/pages/components/switch_example.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class SwitchExample extends StatefulWidget { + static const String name = 'Switch'; + + const SwitchExample({Key? key}) : super(key: key); + + @override + State createState() => _SwitchExampleState(); +} + +class _SwitchExampleState extends State { + bool? isOn = false; + bool isEnabled = true; + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: 'Switch', + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ZetaSwitch( + value: isOn, + onChanged: isEnabled ? (value) => setState(() => isOn = value) : null, + ), + ZetaButton( + label: isEnabled ? 'Disable' : 'Enable', + onPressed: () => setState(() => isEnabled = !isEnabled), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/tabs_example.dart b/example/lib/pages/components/tabs_example.dart new file mode 100644 index 00000000..135d6b83 --- /dev/null +++ b/example/lib/pages/components/tabs_example.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class TabsExample extends StatefulWidget { + const TabsExample({super.key}); + + static const String name = 'Tabs'; + + @override + State createState() => _TabsExampleState(); +} + +class _TabsExampleState extends State { + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: TabsExample.name, + child: Column( + children: [ + DefaultTabController( + length: 2, + child: ZetaTabBar( + context: context, + tabs: [ + ZetaTab(icon: Icon(ZetaIcons.star_round), text: "Tab Item"), + ZetaTab(icon: Icon(ZetaIcons.star_round), text: "Tab Item"), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.l), + child: DefaultTabController( + length: 5, + child: ZetaTabBar( + context: context, + isScrollable: true, + tabs: [ + ZetaTab(text: "Tab Item"), + ZetaTab(text: "Tab Item"), + ZetaTab(text: "Tab Item"), + ZetaTab(text: "Tab Item"), + ZetaTab(text: "Tab Item"), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.l), + child: DefaultTabController( + length: 5, + child: ZetaTabBar( + context: context, + isScrollable: true, + enabled: false, + tabs: [ + ZetaTab(icon: Icon(ZetaIcons.star_sharp), text: "Tab Item"), + ZetaTab(icon: Icon(ZetaIcons.star_sharp), text: "Tab Item"), + ZetaTab(icon: Icon(ZetaIcons.star_sharp), text: "Tab Item"), + ZetaTab(icon: Icon(ZetaIcons.star_sharp), text: "Tab Item"), + ZetaTab(icon: Icon(ZetaIcons.star_sharp), text: "Tab Item"), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/theme/typography_example.dart b/example/lib/pages/theme/typography_example.dart index 7e9efcee..da763e40 100644 --- a/example/lib/pages/theme/typography_example.dart +++ b/example/lib/pages/theme/typography_example.dart @@ -24,11 +24,11 @@ class TypographyExample extends StatelessWidget { 'Body Large': ZetaTextStyles.bodyLarge, 'Body Medium': ZetaTextStyles.bodyMedium, 'Body Small': ZetaTextStyles.bodySmall, + 'Body X-Small': ZetaTextStyles.bodyXSmall, 'Label Large': ZetaTextStyles.labelLarge, 'Label Medium': ZetaTextStyles.labelMedium, 'Label Small': ZetaTextStyles.labelSmall, 'Label Indicator': ZetaTextStyles.labelIndicator, - 'Label Tiny': ZetaTextStyles.labelTiny, }; return ExampleScaffold( diff --git a/example/lib/widgets.dart b/example/lib/widgets.dart index 6523f821..816cd4ea 100644 --- a/example/lib/widgets.dart +++ b/example/lib/widgets.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:zeta_example/utils/theme_color_switch.dart'; import 'package:zeta_example/utils/theme_constrast_switch.dart'; import 'package:zeta_example/utils/theme_mode_switch.dart'; -import 'package:zeta_flutter/zeta_flutter.dart'; class ExampleScaffold extends StatelessWidget { final String name; @@ -29,10 +28,7 @@ class ExampleScaffold extends StatelessWidget { floatingActionButton: floatingActionButton, appBar: AppBar( centerTitle: false, - title: Text( - name, - style: ZetaTextStyles.titleMedium, - ), + title: Text(name), backgroundColor: colors.primary, foregroundColor: colors.onPrimary, actions: [ diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index ee0056c3..253d9342 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -202,7 +202,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5b055a3a..83d88728 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ breadCrumbsUseCase(context)), WidgetbookUseCase(name: 'Banners', builder: (context) => bannerUseCase(context)), + WidgetbookUseCase(name: "Dropdown", builder: (context) => dropdownUseCase(context)), WidgetbookUseCase(name: 'In Page Banners', builder: (context) => inPageBannerUseCase(context)), WidgetbookUseCase(name: 'Accordion', builder: (context) => accordionUseCase(context)), WidgetbookComponent( @@ -76,6 +85,7 @@ class HotReload extends StatelessWidget { WidgetbookUseCase(name: 'Password Input', builder: (context) => passwordInputUseCase(context)), WidgetbookUseCase(name: 'Content', builder: (context) => bottomSheetContentUseCase(context)), WidgetbookUseCase(name: 'Dial Pad', builder: (context) => dialPadUseCase(context)), + WidgetbookUseCase(name: 'List Item', builder: (context) => listItemUseCase(context)), WidgetbookUseCase(name: 'Navigation Bar', builder: (context) => navigationBarUseCase(context)), WidgetbookComponent( name: 'Progress', @@ -84,6 +94,18 @@ class HotReload extends StatelessWidget { WidgetbookUseCase(name: 'Circle', builder: (context) => progressCircleUseCase(context)) ], ), + WidgetbookUseCase(name: 'Radio Button', builder: (context) => radioButtonUseCase(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: 'Stepper', + builder: (context) => stepperUseCase(context), + ), + WidgetbookUseCase(name: 'Tabs', builder: (context) => tabsUseCase(context)), ]..sort((a, b) => a.name.compareTo(b.name)), ), WidgetbookCategory( diff --git a/example/widgetbook/pages/components/badges_widgetbook.dart b/example/widgetbook/pages/components/badges_widgetbook.dart index d10cdb0d..4b17f1bd 100644 --- a/example/widgetbook/pages/components/badges_widgetbook.dart +++ b/example/widgetbook/pages/components/badges_widgetbook.dart @@ -74,7 +74,7 @@ Widget tagsUseCase(BuildContext context) => WidgetbookTestWidget( direction: context.knobs.list( label: 'Direction', options: ZetaTagDirection.values, - labelBuilder: (value) => value.name.split('.').last.capitalize(), + labelBuilder: enumLabelBuilder, ), ), ], @@ -90,12 +90,12 @@ Widget workcloudIndicatorsUseCase(BuildContext context) { label: context.knobs.string(label: 'Label', initialValue: 'Label'), prioritySize: context.knobs.list( label: 'Size', - labelBuilder: (value) => value.name.split('.').last.capitalize(), + labelBuilder: enumLabelBuilder, options: ZetaWidgetSize.values, ), priorityType: context.knobs.list( label: 'Type', - labelBuilder: (value) => value.name.split('.').last.capitalize(), + labelBuilder: enumLabelBuilder, options: ZetaWorkcloudIndicatorType.values, ), icon: iconKnob(context, rounded: rounded, nullable: true), diff --git a/example/widgetbook/pages/components/banner_widgetbook.dart b/example/widgetbook/pages/components/banner_widgetbook.dart index 85792576..5c796843 100644 --- a/example/widgetbook/pages/components/banner_widgetbook.dart +++ b/example/widgetbook/pages/components/banner_widgetbook.dart @@ -14,7 +14,7 @@ Widget bannerUseCase(BuildContext context) { type: context.knobs.list( label: 'Type', options: ZetaSystemBannerStatus.values, - labelBuilder: (value) => value.name.split('.').last.capitalize(), + labelBuilder: enumLabelBuilder, ), leadingIcon: iconKnob(context, rounded: rounded, nullable: true), titleStart: context.knobs.boolean(label: 'Center title'), diff --git a/example/widgetbook/pages/components/date_input_widgetbook.dart b/example/widgetbook/pages/components/date_input_widgetbook.dart new file mode 100644 index 00000000..eb3d7cf1 --- /dev/null +++ b/example/widgetbook/pages/components/date_input_widgetbook.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget dateInputUseCase(BuildContext context) { + String? _errorText; + + return WidgetbookTestWidget( + widget: StatefulBuilder( + builder: (context, setState) { + final errorText = context.knobs.string( + label: 'Error message for invalid date', + initialValue: 'Invalid date', + ); + final rounded = context.knobs.boolean(label: 'Rounded', initialValue: true); + final enabled = context.knobs.boolean(label: 'Enabled', initialValue: true); + final size = context.knobs.list( + label: 'Size', + options: ZetaDateInputSize.values, + labelBuilder: (size) => size.name, + ); + final datePattern = context.knobs.list( + label: 'Date pattern', + options: ['MM/dd/yyyy', 'dd/MM/yyyy', 'dd.MM.yyyy', 'yyyy-MM-dd'], + labelBuilder: (pattern) => pattern, + ); + + return Padding( + padding: const EdgeInsets.all(ZetaSpacing.x5), + child: ZetaDateInput( + size: size, + rounded: rounded, + enabled: enabled, + label: 'Birthdate', + hint: 'Enter birthdate', + datePattern: datePattern, + hasError: _errorText != null, + errorText: _errorText ?? errorText, + onChanged: (value) { + if (value == null) return setState(() => _errorText = null); + final now = DateTime.now(); + setState( + () => _errorText = value.difference(DateTime(now.year, now.month, now.day)).inDays > 0 + ? 'Birthdate cannot be in the future' + : null, + ); + }, + ), + ); + }, + ), + ); +} diff --git a/example/widgetbook/pages/components/dropdown_widgetbook.dart b/example/widgetbook/pages/components/dropdown_widgetbook.dart new file mode 100644 index 00000000..8cdc9191 --- /dev/null +++ b/example/widgetbook/pages/components/dropdown_widgetbook.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget dropdownUseCase(BuildContext context) => WidgetbookTestWidget( + widget: Center( + child: DropdownExample(context), + ), + ); + +class DropdownExample extends StatefulWidget { + const DropdownExample(this.c); + final BuildContext c; + + @override + State createState() => _DropdownExampleState(); +} + +class _DropdownExampleState extends State { + List _children = [ + ZetaDropdownItem( + value: "Item 1", + leadingIcon: Icon(ZetaIcons.star_round), + ), + ZetaDropdownItem( + value: "Item 2", + leadingIcon: Icon(ZetaIcons.star_half_round), + ), + ZetaDropdownItem( + value: "Item 3", + ) + ]; + + late ZetaDropdownItem selectedItem = ZetaDropdownItem( + value: "Item 1", + leadingIcon: Icon(ZetaIcons.star_round), + ); + + @override + Widget build(BuildContext _) { + return SingleChildScrollView( + child: SizedBox( + width: double.infinity, + child: Column(children: [ + ZetaDropdown( + leadingType: widget.c.knobs.list( + label: "Checkbox type", + options: [ + LeadingStyle.none, + LeadingStyle.checkbox, + LeadingStyle.radio, + ], + ), + onChange: (value) { + setState(() { + selectedItem = value; + }); + }, + selectedItem: selectedItem, + items: _children, + rounded: widget.c.knobs.boolean(label: "Rounded"), + isMinimized: widget.c.knobs.boolean(label: "Minimized"), + ), + Text('Selected item : ${selectedItem.value}') + ])), + ); + } +} diff --git a/example/widgetbook/pages/components/list_item_widgetbook.dart b/example/widgetbook/pages/components/list_item_widgetbook.dart new file mode 100644 index 00000000..e545cf7c --- /dev/null +++ b/example/widgetbook/pages/components/list_item_widgetbook.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget listItemUseCase(BuildContext context) { + return WidgetbookTestWidget( + widget: StatefulBuilder( + builder: (context, setState) { + final subtitle = context.knobs.stringOrNull(label: 'Descriptor', initialValue: null); + + final trailing = context.knobs.boolean(label: 'Trailing', initialValue: false) + ? Checkbox(value: false, onChanged: (_) {}) + : null; + + final leading = context.knobs.boolean(label: 'Leading', initialValue: false) + ? Container( + width: 48, + height: 48, + decoration: BoxDecoration(borderRadius: ZetaRadius.rounded), + child: Placeholder(), + ) + : null; + + return ZetaListItem( + dense: context.knobs.boolean(label: 'Dense', initialValue: false), + enabled: context.knobs.boolean(label: 'Enabled', initialValue: true), + enabledDivider: context.knobs.boolean( + label: 'Enabled Divider', + initialValue: true, + ), + selected: context.knobs.boolean(label: 'Selected', initialValue: true), + leading: leading, + title: Text( + context.knobs.string(label: 'Title', initialValue: 'List Item'), + ), + subtitle: subtitle != null ? Text(subtitle) : null, + trailing: trailing, + onTap: () {}, + ); + }, + ), + ); +} diff --git a/example/widgetbook/pages/components/radio_widgetbook.dart b/example/widgetbook/pages/components/radio_widgetbook.dart new file mode 100644 index 00000000..e8aacdc4 --- /dev/null +++ b/example/widgetbook/pages/components/radio_widgetbook.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget radioButtonUseCase(BuildContext context) { + String option1 = 'Label 1'; + String option2 = 'Label 2'; + String? groupValue; + + return WidgetbookTestWidget( + widget: StatefulBuilder( + builder: (context, setState) { + ValueChanged? onChanged = context.knobs.boolean(label: 'Enabled', initialValue: true) + ? (value) => setState(() => groupValue = value) + : null; + return Padding( + padding: const EdgeInsets.all(ZetaSpacing.x5), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: ZetaSpacing.x5), + child: Text('Radio Button'), + ), + ZetaRadio( + value: option1, + groupValue: groupValue, + onChanged: onChanged, + label: Text(option1), + ), + ZetaRadio( + value: option2, + groupValue: groupValue, + onChanged: onChanged, + label: Text(option2), + ), + ], + ), + ); + }, + ), + ); +} diff --git a/example/widgetbook/pages/components/snack_bar_widgetbook.dart b/example/widgetbook/pages/components/snack_bar_widgetbook.dart new file mode 100644 index 00000000..92430c67 --- /dev/null +++ b/example/widgetbook/pages/components/snack_bar_widgetbook.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; +import '../../utils/utils.dart'; + +Widget snackBarUseCase(BuildContext context) { + return WidgetbookTestWidget( + widget: Builder( + builder: (context) { + final text = context.knobs.string( + label: 'Content Text', + initialValue: 'This is a snackbar', + ); + + final actionLabel = context.knobs.stringOrNull( + label: 'Action Label', + initialValue: null, + ); + + final type = context.knobs.listOrNull( + label: 'Type', + options: [null, ...ZetaSnackBarType.values], + labelBuilder: enumLabelBuilder, + ); + + final leadingIcon = iconKnob( + context, + name: "Leading Icon", + initial: Icons.mood_rounded, + nullable: true, + ); + + final rounded = context.knobs.boolean(label: 'Rounded', initialValue: true); + + return ZetaButton.primary( + label: "Show Snackbar", + onPressed: () { + print(actionLabel); + final snackBar = ZetaSnackBar( + context: context, + onPressed: () {}, + actionLabel: actionLabel, + type: type, + leadingIcon: leadingIcon != null ? Icon(leadingIcon) : null, + rounded: rounded, + content: Text(text), + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }); + }, + ), + ); +} diff --git a/example/widgetbook/pages/components/stepper_widgetbook.dart b/example/widgetbook/pages/components/stepper_widgetbook.dart new file mode 100644 index 00000000..4c06303f --- /dev/null +++ b/example/widgetbook/pages/components/stepper_widgetbook.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget stepperUseCase(BuildContext context) { + int currentStep = 0; + + ZetaStepType getForStepIndex(int stepIndex) { + if (currentStep == stepIndex) return ZetaStepType.enabled; + if (currentStep > stepIndex) return ZetaStepType.complete; + + return ZetaStepType.disabled; + } + + final type = context.knobs.list( + label: "Type", + options: [ + ZetaStepperType.horizontal, + ZetaStepperType.vertical, + ], + initialOption: ZetaStepperType.horizontal, + labelBuilder: (type) => type.name, + ); + + final rounded = context.knobs.boolean(label: 'Rounded', initialValue: true); + + final enabledContent = context.knobs.boolean(label: 'Enabled Content', initialValue: true); + + return WidgetbookTestWidget( + widget: StatefulBuilder( + builder: (context, setState) { + return Container( + height: type == ZetaStepperType.horizontal ? 300 : null, + padding: EdgeInsets.all( + type == ZetaStepperType.horizontal ? 0.0 : ZetaSpacing.l, + ), + child: ZetaStepper( + currentStep: currentStep, + onStepTapped: (index) => setState(() => currentStep = index), + rounded: type == ZetaStepperType.horizontal ? rounded : true, + type: type, + steps: [ + ZetaStep( + type: getForStepIndex(0), + title: Text("Title"), + content: enabledContent ? Text("Content") : null, + ), + ZetaStep( + type: getForStepIndex(1), + title: Text("Title 2"), + content: enabledContent ? Text("Content 2") : null, + ), + ZetaStep( + type: getForStepIndex(2), + title: Text("Title 3"), + content: enabledContent ? Text("Content 3") : null, + ), + ], + ), + ); + }, + ), + ); +} diff --git a/example/widgetbook/pages/components/switch_widgetbook.dart b/example/widgetbook/pages/components/switch_widgetbook.dart new file mode 100644 index 00000000..0ee2f9c3 --- /dev/null +++ b/example/widgetbook/pages/components/switch_widgetbook.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; +import '../../utils/utils.dart'; + +Widget switchUseCase(BuildContext context) { + bool? isOn = false; + + return WidgetbookTestWidget( + widget: StatefulBuilder( + builder: (context, setState) { + ValueChanged? onChanged = context.knobs.boolean(label: 'Enabled', initialValue: true) + ? (value) => setState(() => isOn = value) + : null; + return Padding( + padding: const EdgeInsets.all(ZetaSpacing.x5), + child: Column( + children: [ + Text('Switch'), + ZetaSwitch( + value: isOn, + onChanged: onChanged, + variant: context.knobs.listOrNull( + label: 'Variant', + options: ZetaSwitchType.values, + labelBuilder: enumLabelBuilder, + ), + ), + Text(isOn == true ? 'On' : 'Off'), + ], + ), + ); + }, + ), + ); +} diff --git a/example/widgetbook/pages/components/tabs_widgetbook.dart b/example/widgetbook/pages/components/tabs_widgetbook.dart new file mode 100644 index 00000000..ca0687d5 --- /dev/null +++ b/example/widgetbook/pages/components/tabs_widgetbook.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget tabsUseCase(BuildContext context) { + return WidgetbookTestWidget( + widget: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.l), + child: DefaultTabController( + length: 2, + child: ZetaTabBar( + context: context, + enabled: context.knobs.boolean( + label: "Enabled", + initialValue: true, + ), + tabs: [ + ZetaTab(icon: Icon(ZetaIcons.star_round), text: "Tab Item"), + ZetaTab(icon: Icon(ZetaIcons.star_round), text: "Tab Item"), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.l), + child: DefaultTabController( + length: 5, + child: ZetaTabBar( + context: context, + enabled: context.knobs.boolean(label: "Enabled"), + isScrollable: true, + tabs: [ + ZetaTab(text: "Tab Item"), + ZetaTab(text: "Tab Item"), + ZetaTab(text: "Tab Item"), + ZetaTab(text: "Tab Item"), + ZetaTab(text: "Tab Item"), + ], + ), + ), + ), + ], + ), + ), + ); +} diff --git a/example/widgetbook/pages/theme/typography_widgetbook.dart b/example/widgetbook/pages/theme/typography_widgetbook.dart index f768f354..45da8649 100644 --- a/example/widgetbook/pages/theme/typography_widgetbook.dart +++ b/example/widgetbook/pages/theme/typography_widgetbook.dart @@ -16,11 +16,11 @@ const Map allTypes = { 'Body large': ZetaTextStyles.bodyLarge, 'Body medium': ZetaTextStyles.bodyMedium, 'Body small': ZetaTextStyles.bodySmall, + 'Body X-small': ZetaTextStyles.bodyXSmall, 'Label large': ZetaTextStyles.labelLarge, 'Label medium': ZetaTextStyles.labelMedium, 'Label small': ZetaTextStyles.labelSmall, 'Label indicator': ZetaTextStyles.labelIndicator, - 'Label tiny': ZetaTextStyles.labelTiny, }; Widget typographyUseCase(BuildContext context) => Padding( diff --git a/example/widgetbook/utils/utils.dart b/example/widgetbook/utils/utils.dart index 4baec50c..207ec12c 100644 --- a/example/widgetbook/utils/utils.dart +++ b/example/widgetbook/utils/utils.dart @@ -16,7 +16,7 @@ String iconLabelBuilder(IconData? value, [bool rounded = true]) { List iconOptions(rounded) => rounded ? iconsRounded.values.toList() : iconsSharp.values.toList(); -String enumLabelBuilder(Enum value) => value.name.split('.').last.capitalize(); +String enumLabelBuilder(Enum? value) => value?.name.split('.').last.capitalize() ?? ''; IconData? iconKnob(BuildContext context, {bool rounded = true, bool nullable = false, String name = 'Icon', final IconData? initial}) { diff --git a/firebase.json b/firebase.json index 4a130f50..bf8f2069 100644 --- a/firebase.json +++ b/firebase.json @@ -1,6 +1,7 @@ { "hosting": { "public": "build", + "target": "flutter", "rewrites": [ { "source": "**", diff --git a/lib/src/components/accordion/accordion.dart b/lib/src/components/accordion/accordion.dart index 6b2966f8..5fc24290 100644 --- a/lib/src/components/accordion/accordion.dart +++ b/lib/src/components/accordion/accordion.dart @@ -174,7 +174,17 @@ class _ZetaAccordionState extends State with TickerProviderStateM axisAlignment: -1, child: Padding( padding: const EdgeInsets.fromLTRB(ZetaSpacing.x4, 0, ZetaSpacing.x4, ZetaSpacing.x4), - child: DefaultTextStyle(style: ZetaTextStyles.titleSmall, child: widget.child ?? const SizedBox()), + child: Theme( + data: Theme.of(context).copyWith( + listTileTheme: ListTileThemeData( + titleTextStyle: ZetaTextStyles.titleSmall.apply(color: zetaColors.textDefault), + ), + ), + child: DefaultTextStyle( + style: ZetaTextStyles.titleSmall, + child: widget.child ?? const SizedBox(), + ), + ), ), ), ], diff --git a/lib/src/components/badges/indicator.dart b/lib/src/components/badges/indicator.dart index 7b5a91d6..ff77a5ad 100644 --- a/lib/src/components/badges/indicator.dart +++ b/lib/src/components/badges/indicator.dart @@ -123,8 +123,10 @@ class ZetaIndicator extends StatelessWidget { return Center( child: Text( value.formatMaxChars(), - style: (size == ZetaWidgetSize.large ? ZetaTextStyles.labelIndicator : ZetaTextStyles.labelTiny) - .apply(color: foregroundColor), + style: ZetaTextStyles.labelIndicator.copyWith( + color: foregroundColor, + fontSize: size == ZetaWidgetSize.large ? null : 11, + ), // TODO(thelukwalton): Awaiting updated design. ), ); } diff --git a/lib/src/components/badges/priority_pill.dart b/lib/src/components/badges/priority_pill.dart index a95002c0..8e027667 100644 --- a/lib/src/components/badges/priority_pill.dart +++ b/lib/src/components/badges/priority_pill.dart @@ -46,13 +46,13 @@ class ZetaPriorityPill extends StatelessWidget { shape: rounded ? BoxShape.circle : BoxShape.rectangle, color: backgroundColor, ), - child: Text(index.formatMaxChars(), style: ZetaTextStyles.bodyMedium.apply(color: foregroundColor)), + child: Text(index.formatMaxChars(), style: ZetaTextStyles.bodySmall.apply(color: foregroundColor)), ), Padding( padding: const EdgeInsets.symmetric(horizontal: ZetaSpacing.x2, vertical: ZetaSpacing.x1), child: Text( priority, - style: ZetaTextStyles.bodyMedium, + style: ZetaTextStyles.bodySmall, overflow: TextOverflow.ellipsis, ), ), diff --git a/lib/src/components/badges/workcloud_indicator.dart b/lib/src/components/badges/workcloud_indicator.dart index 3a79c04a..c1efefde 100644 --- a/lib/src/components/badges/workcloud_indicator.dart +++ b/lib/src/components/badges/workcloud_indicator.dart @@ -103,7 +103,7 @@ class ZetaWorkcloudIndicator extends StatelessWidget { @override Widget build(BuildContext context) { final ZetaColorSwatch color = priorityType.color(context); - final textStyle = prioritySize == ZetaWidgetSize.large ? ZetaTextStyles.labelMedium : ZetaTextStyles.labelTiny; + final textStyle = prioritySize == ZetaWidgetSize.large ? ZetaTextStyles.bodySmall : ZetaTextStyles.bodyXSmall; return DecoratedBox( decoration: BoxDecoration( diff --git a/lib/src/components/banners/in_page_banner.dart b/lib/src/components/banners/in_page_banner.dart index 15d9e20b..15323eaa 100644 --- a/lib/src/components/banners/in_page_banner.dart +++ b/lib/src/components/banners/in_page_banner.dart @@ -71,14 +71,14 @@ class ZetaInPageBanner extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: ZetaSpacing.x2_5), + const SizedBox(height: ZetaSpacing.x2), if (hasTitle) Text( title!, - style: ZetaTextStyles.titleSmall, + style: ZetaTextStyles.labelLarge, ).paddingBottom(ZetaSpacing.xxs), DefaultTextStyle( - style: ZetaTextStyles.bodyMedium.apply(color: theme.colors.textDefault), + style: ZetaTextStyles.bodySmall.apply(color: theme.colors.textDefault), child: content, ), if (actions.isNotEmpty) diff --git a/lib/src/components/banners/system_banner.dart b/lib/src/components/banners/system_banner.dart index e6b40dff..1c9a29f6 100644 --- a/lib/src/components/banners/system_banner.dart +++ b/lib/src/components/banners/system_banner.dart @@ -54,10 +54,9 @@ class ZetaSystemBanner extends MaterialBanner { } return DefaultTextStyle( - style: ZetaTextStyles.titleSmall.copyWith( + style: ZetaTextStyles.labelLarge.copyWith( color: foregroundColor, overflow: TextOverflow.ellipsis, - height: 1, ), child: Row( mainAxisAlignment: titleStart ? MainAxisAlignment.center : MainAxisAlignment.start, diff --git a/lib/src/components/breadcrumbs/breadcrumbs.dart b/lib/src/components/breadcrumbs/breadcrumbs.dart index b1a79d43..f318946d 100644 --- a/lib/src/components/breadcrumbs/breadcrumbs.dart +++ b/lib/src/components/breadcrumbs/breadcrumbs.dart @@ -184,32 +184,29 @@ class _ZetaBreadCrumbState extends State { @override Widget build(BuildContext context) { final colors = Zeta.of(context).colors; - return Material( - color: Colors.transparent, - child: InkWell( - statesController: controller, - onTap: widget.onPressed, - enableFeedback: false, - splashColor: Colors.transparent, - overlayColor: MaterialStateProperty.resolveWith((states) { - return Colors.transparent; - }), - child: Row( - children: [ - if (widget.isSelected) - Icon( - widget.activeIcon ?? ZetaIcons.star_round, - color: getColor(controller.value, colors), - ), - const SizedBox( - width: ZetaSpacing.xs, - ), - Text( - widget.label, - style: TextStyle(color: getColor(controller.value, colors)), + return InkWell( + statesController: controller, + onTap: widget.onPressed, + enableFeedback: false, + splashColor: Colors.transparent, + overlayColor: MaterialStateProperty.resolveWith((states) { + return Colors.transparent; + }), + child: Row( + children: [ + if (widget.isSelected) + Icon( + widget.activeIcon ?? ZetaIcons.star_round, + color: getColor(controller.value, colors), ), - ], - ), + const SizedBox( + width: ZetaSpacing.xs, + ), + Text( + widget.label, + style: ZetaTextStyles.bodySmall.apply(color: getColor(controller.value, colors)), + ), + ], ), ); } diff --git a/lib/src/components/buttons/button.dart b/lib/src/components/buttons/button.dart index 252cdd1b..c6c61700 100644 --- a/lib/src/components/buttons/button.dart +++ b/lib/src/components/buttons/button.dart @@ -3,8 +3,6 @@ import 'package:flutter/material.dart'; import '../../../zeta_flutter.dart'; -///Button types - ///Zeta Button class ZetaButton extends StatelessWidget { ///Constructs [ZetaButton] @@ -135,7 +133,7 @@ class ZetaButton extends StatelessWidget { ); } - TextStyle get _textStyle => size == ZetaWidgetSize.small ? ZetaTextStyles.labelMedium : ZetaTextStyles.labelLarge; + TextStyle get _textStyle => size == ZetaWidgetSize.small ? ZetaTextStyles.labelSmall : ZetaTextStyles.labelLarge; double get _minConstraints { switch (size) { diff --git a/lib/src/components/buttons/button_group.dart b/lib/src/components/buttons/button_group.dart index 52b8ea20..bf903921 100644 --- a/lib/src/components/buttons/button_group.dart +++ b/lib/src/components/buttons/button_group.dart @@ -36,18 +36,21 @@ class ZetaButtonGroup extends StatelessWidget { /// Returns [ZetaGroupButton]s with there appropriate styling. List getButtons() { - for (final element in buttons) { - element - .._isInitial = element._isFinal = false - .._isLarge = isLarge - .._rounded = rounded - .._isInverse = isInverse; + final List mappedButtons = []; + + for (final (index, button) in buttons.indexed) { + mappedButtons.add( + button.copyWith( + large: isLarge, + inverse: isInverse, + round: rounded, + fin: index == buttons.length - 1, + initial: index == 0, + ), + ); } - buttons.first._isInitial = true; - buttons.last._isFinal = true; - - return buttons; + return mappedButtons; } @override @@ -63,34 +66,59 @@ class ZetaButtonGroup extends StatelessWidget { // TODO(UX-854): Create country variant. /// Group Button item -// ignore: must_be_immutable class ZetaGroupButton extends StatefulWidget { - /// Constructs [ZetaGroupButton] - ZetaGroupButton({ + /// Public Constructor for [ZetaGroupButton] + const ZetaGroupButton({ super.key, this.label, this.icon, this.onPressed, this.dropdown, + }) : isFinal = false, + isInitial = false, + isInverse = false, + isLarge = true, + rounded = true; + + /// Private constructor + const ZetaGroupButton._({ + super.key, + this.label, + this.icon, + this.onPressed, + this.dropdown, + required this.isFinal, + required this.isInitial, + required this.isInverse, + required this.isLarge, + required this.rounded, }); /// Constructs dropdown group button - ZetaGroupButton.dropdown({ + const ZetaGroupButton.dropdown({ super.key, required this.onPressed, required this.dropdown, this.icon, this.label, - }); + }) : isFinal = false, + isInitial = false, + isInverse = false, + isLarge = true, + rounded = true; ///Constructs group button with icon - ZetaGroupButton.icon({ + const ZetaGroupButton.icon({ super.key, required this.icon, this.dropdown, this.onPressed, this.label, - }); + }) : isFinal = false, + isInitial = false, + isInverse = false, + isLarge = true, + rounded = true; /// Label for [ZetaGroupButton]. final String? label; @@ -105,29 +133,43 @@ class ZetaGroupButton extends StatefulWidget { final Widget? dropdown; ///If [ZetaGroupButton] is large. - bool _isLarge = false; + final bool isLarge; ///If [ZetaGroupButton] is rounded. - bool _rounded = false; + final bool rounded; /// If [ZetaGroupButton] is the first button in its list. - bool _isInitial = false; + final bool isInitial; /// If [ZetaGroupButton] is the final button in its list. - bool _isFinal = false; + final bool isFinal; + + /// If [ZetaGroupButton] is inverse. - bool _isInverse = false; + final bool isInverse; @override State createState() => _ZetaGroupButtonState(); /// Returns copy of [ZetaGroupButton] with fields. - ZetaGroupButton copyWith({bool? isFinal, bool? isInitial}) { - return ZetaGroupButton( + ZetaGroupButton copyWith({ + bool? fin, + bool? initial, + bool? large, + bool? round, + bool? inverse, + }) { + return ZetaGroupButton._( key: key, label: label, icon: icon, onPressed: onPressed, + dropdown: dropdown, + isFinal: fin ?? isFinal, + isInitial: initial ?? isInitial, + isLarge: large ?? isLarge, + rounded: round ?? rounded, + isInverse: inverse ?? isInverse, ); } @@ -137,7 +179,12 @@ class ZetaGroupButton extends StatefulWidget { properties ..add(StringProperty('Label', label)) ..add(DiagnosticsProperty('icon', icon)) - ..add(ObjectFlagProperty.has('onPressed', onPressed)); + ..add(ObjectFlagProperty.has('onPressed', onPressed)) + ..add(DiagnosticsProperty('isInitial', isInitial)) + ..add(DiagnosticsProperty('isLarge', isLarge)) + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(DiagnosticsProperty('isFinal', isFinal)) + ..add(DiagnosticsProperty('isInverse', isInverse)); } } @@ -156,11 +203,19 @@ class _ZetaGroupButtonState extends State { }); } + @override + void didUpdateWidget(ZetaGroupButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.onPressed != widget.onPressed) { + setState(() {}); + } + } + @override Widget build(BuildContext context) { final colors = Zeta.of(context).colors; - final borderType = widget._rounded ? ZetaWidgetBorder.rounded : ZetaWidgetBorder.sharp; + final borderType = widget.rounded ? ZetaWidgetBorder.rounded : ZetaWidgetBorder.sharp; final BorderSide borderSide = _getBorderSide(controller.value, colors, false); @@ -172,7 +227,7 @@ class _ZetaGroupButtonState extends State { bottom: borderSide, right: controller.value.contains(MaterialState.focused) ? BorderSide(color: colors.blue.shade50, width: 2) - : (widget._isFinal) + : (widget.isFinal) ? borderSide : BorderSide.none, ), @@ -187,18 +242,18 @@ class _ZetaGroupButtonState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - if (widget.icon != null) Icon(widget.icon), - Text(widget.label!), + if (widget.icon != null) Icon(widget.icon, size: ZetaSpacing.x5), + Text(widget.label ?? '', style: ZetaTextStyles.labelMedium), if (widget.dropdown != null) // TODO(UX-1006): Dropdown - Icon(widget._rounded ? ZetaIcons.expand_more_round : ZetaIcons.expand_more_sharp, size: 20), - ], + Icon(widget.rounded ? ZetaIcons.expand_more_round : ZetaIcons.expand_more_sharp, size: ZetaSpacing.x5), + ].divide(const SizedBox(width: ZetaSpacing.x1)).toList(), ).paddingAll(_padding), ), ), ); } - double get _padding => widget._isLarge ? ZetaSpacing.x4 : ZetaSpacing.x3; + double get _padding => widget.isLarge ? ZetaSpacing.x4 : ZetaSpacing.x3; BorderSide _getBorderSide( Set states, @@ -208,7 +263,7 @@ class _ZetaGroupButtonState extends State { if (states.contains(MaterialState.focused)) { return BorderSide(color: colors.blue.shade50, width: ZetaSpacing.x0_5); } - if (widget._isInverse) return BorderSide(color: colors.black); + if (widget.isInverse) return BorderSide(color: colors.black); if (states.contains(MaterialState.disabled)) { return BorderSide(color: colors.cool.shade40); } @@ -218,13 +273,13 @@ class _ZetaGroupButtonState extends State { } BorderRadius _getRadius(ZetaWidgetBorder borderType) { - if (widget._isInitial) { + if (widget.isInitial) { return borderType.radius.copyWith( topRight: Radius.zero, bottomRight: Radius.zero, ); } - if (widget._isFinal) { + if (widget.isFinal) { return borderType.radius.copyWith( topLeft: Radius.zero, bottomLeft: Radius.zero, @@ -241,7 +296,7 @@ class _ZetaGroupButtonState extends State { ), ), backgroundColor: MaterialStateProperty.resolveWith((states) { - if (widget._isInverse) return colors.cool.shade100; + if (widget.isInverse) return colors.cool.shade100; if (states.contains(MaterialState.disabled)) { return colors.surfaceDisabled; @@ -258,7 +313,7 @@ class _ZetaGroupButtonState extends State { if (states.contains(MaterialState.disabled)) { return colors.textDisabled; } - if (widget._isInverse) return colors.cool.shade100.onColor; + if (widget.isInverse) return colors.cool.shade100.onColor; return colors.textDefault; }), elevation: const MaterialStatePropertyAll(0), @@ -269,6 +324,11 @@ class _ZetaGroupButtonState extends State { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('controller', controller)); + properties.add( + DiagnosticsProperty( + 'controller', + controller, + ), + ); } } diff --git a/lib/src/components/checkbox/checkbox.dart b/lib/src/components/checkbox/checkbox.dart index 350f49b0..36d40b8c 100644 --- a/lib/src/components/checkbox/checkbox.dart +++ b/lib/src/components/checkbox/checkbox.dart @@ -207,7 +207,7 @@ class _CheckboxState extends State<_Checkbox> { Flexible( child: Padding( padding: const EdgeInsets.only(left: ZetaSpacing.s), - child: Text(widget.label!, style: ZetaTextStyles.bodyLarge), + child: Text(widget.label!, style: ZetaTextStyles.bodyMedium), ), ), ], diff --git a/lib/src/components/chips/chip.dart b/lib/src/components/chips/chip.dart index 78d28324..6432b23d 100644 --- a/lib/src/components/chips/chip.dart +++ b/lib/src/components/chips/chip.dart @@ -97,7 +97,7 @@ class _ZetaChipState extends State { shape: MaterialStateProperty.all( RoundedRectangleBorder(borderRadius: widget.rounded ? ZetaRadius.full : ZetaRadius.none), ), - textStyle: MaterialStateProperty.all(ZetaTextStyles.bodyMedium), + textStyle: MaterialStateProperty.all(ZetaTextStyles.bodySmall), backgroundColor: MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.disabled)) { return colors.surfaceDisabled; diff --git a/lib/src/components/date_input/date_input.dart b/lib/src/components/date_input/date_input.dart new file mode 100644 index 00000000..83c07811 --- /dev/null +++ b/lib/src/components/date_input/date_input.dart @@ -0,0 +1,330 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +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. +class ZetaDateInput extends StatefulWidget { + /// Constructor for [ZetaDateInput]. + /// + /// Example usage how to provide custom validanions + /// via `onChanged`, `hasError` and `errorText`: + /// ```dart + /// ZetaDateInput( + /// label: 'Birthdate', + /// hint: 'Enter birthdate', + /// hasError: _errorText != null, + /// errorText: _errorText ?? 'Invalid date', + /// onChanged: (value) { + /// if (value == null) return setState(() => _errorText = null); + /// final now = DateTime.now(); + /// setState( + /// () => _errorText = value.difference( + /// DateTime(now.year, now.month, now.day)).inDays > 0 + /// ? 'Birthdate cannot be in the future' + /// : null, + /// ); + /// }, + /// ) + /// ``` + const ZetaDateInput({ + super.key, + this.size, + this.label, + this.hint, + this.enabled = true, + this.rounded = true, + this.hasError = false, + this.errorText, + this.onChanged, + this.datePattern = 'MM/dd/yyyy', + }); + + /// Determines the size of the input field. + /// Default is `ZetaDateInputSize.large` + final ZetaDateInputSize? size; + + /// 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 input field 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. + /// + /// If `hasError` is false, then `errorText` should provide + /// date validation error message. + /// + /// See the example in the [ZetaDateInput] documentation. + final String? errorText; + + /// A callback, which provides the entered date, or `null`, if invalid. + /// + /// See the example in the [ZetaDateInput] documentation + /// how to provide custom validations + /// in combination with `hasError` and `errorText`. + final void Function(DateTime?)? onChanged; + + /// `datePattern` is needed for the date format validation as described here: + /// https://pub.dev/documentation/intl/latest/intl/DateFormat-class.html + final String datePattern; + + @override + State createState() => _ZetaDateInputState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('size', size)) + ..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.has('onChanged', onChanged)) + ..add(StringProperty('datePattern', datePattern)); + } +} + +class _ZetaDateInputState extends State { + final _controller = TextEditingController(); + late ZetaDateInputSize _size; + late final String _hintText; + late final MaskTextInputFormatter _dateFormatter; + bool _invalidDate = false; + bool _hasError = false; + + @override + void initState() { + super.initState(); + _hintText = widget.datePattern.toLowerCase(); + _dateFormatter = MaskTextInputFormatter( + mask: _hintText.replaceAll(RegExp('[a-z]'), '#'), + filter: {'#': RegExp('[0-9]')}, + type: MaskAutoCompletionType.eager, + ); + _setParams(); + } + + @override + void didUpdateWidget(ZetaDateInput oldWidget) { + super.didUpdateWidget(oldWidget); + _setParams(); + } + + void _setParams() { + _size = widget.size ?? ZetaDateInputSize.large; + _hasError = widget.hasError; + } + + void _onChanged() { + final value = _dateFormatter.getMaskedText().trim(); + final date = DateFormat(widget.datePattern).tryParseStrict(value); + _invalidDate = value.isNotEmpty && date == null; + widget.onChanged?.call(date); + setState(() {}); + } + + void _clear() { + _controller.clear(); + setState(() { + _invalidDate = false; + _hasError = false; + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final zeta = Zeta.of(context); + final hasError = _invalidDate || _hasError; + 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, + ), + ), + ), + TextFormField( + enabled: widget.enabled, + controller: _controller, + inputFormatters: [_dateFormatter], + keyboardType: TextInputType.number, + onChanged: (_) => _onChanged(), + style: _size == ZetaDateInputSize.small ? ZetaTextStyles.bodyXSmall : ZetaTextStyles.bodyMedium, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 10, + vertical: _inputVerticalPadding(_size), + ), + hintText: _hintText, + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_controller.text.isNotEmpty) + IconButton( + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + onPressed: _clear, + icon: Icon( + widget.rounded ? ZetaIcons.cancel_round : ZetaIcons.cancel_sharp, + color: zeta.colors.cool.shade70, + size: _iconSize(_size), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 6, right: 10), + child: Icon( + widget.rounded ? ZetaIcons.calendar_3_day_round : ZetaIcons.calendar_3_day_sharp, + color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, + size: _iconSize(_size), + ), + ), + ], + ), + suffixIconConstraints: const BoxConstraints( + minHeight: ZetaSpacing.m, + minWidth: ZetaSpacing.m, + ), + hintStyle: _size == ZetaDateInputSize.small + ? ZetaTextStyles.bodyXSmall.copyWith( + color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, + ) + : ZetaTextStyles.bodyMedium.copyWith( + color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, + ), + filled: !widget.enabled || hasError ? true : null, + fillColor: widget.enabled + ? hasError + ? zeta.colors.red.shade10 + : null + : 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, + ), + ), + ), + ], + ), + ), + ], + ); + } + + double _inputVerticalPadding(ZetaDateInputSize size) => switch (size) { + ZetaDateInputSize.large => ZetaSpacing.x3, + ZetaDateInputSize.medium => ZetaSpacing.x2, + ZetaDateInputSize.small => ZetaSpacing.x2, + }; + + double _iconSize(ZetaDateInputSize size) => switch (size) { + ZetaDateInputSize.large => ZetaSpacing.x6, + ZetaDateInputSize.medium => ZetaSpacing.x5, + ZetaDateInputSize.small => ZetaSpacing.x4, + }; + + OutlineInputBorder _defaultInputBorder( + Zeta zeta, { + required bool rounded, + }) => + OutlineInputBorder( + borderRadius: rounded ? ZetaRadius.minimal : ZetaRadius.none, + borderSide: BorderSide(color: zeta.colors.cool.shade40), + ); + + OutlineInputBorder _focusedInputBorder( + Zeta zeta, { + required bool rounded, + }) => + OutlineInputBorder( + borderRadius: rounded ? ZetaRadius.minimal : ZetaRadius.none, + borderSide: BorderSide(color: zeta.colors.blue.shade50), + ); + + OutlineInputBorder _errorInputBorder( + Zeta zeta, { + required bool rounded, + }) => + OutlineInputBorder( + borderRadius: rounded ? ZetaRadius.minimal : ZetaRadius.none, + borderSide: BorderSide(color: zeta.colors.red.shade50), + ); +} diff --git a/lib/src/components/dropdown/dropdown.dart b/lib/src/components/dropdown/dropdown.dart new file mode 100644 index 00000000..0e761d81 --- /dev/null +++ b/lib/src/components/dropdown/dropdown.dart @@ -0,0 +1,444 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; + +/// Class for [ZetaDropdown] +class ZetaDropdown extends StatefulWidget { + ///Constructor of [ZetaDropdown] + const ZetaDropdown({ + super.key, + required this.items, + required this.onChange, + required this.selectedItem, + this.rounded = true, + this.leadingType = LeadingStyle.none, + this.isMinimized = false, + }); + + /// Input items as list of [ZetaDropdownItem] + final List items; + + /// Currently selected item + final ZetaDropdownItem selectedItem; + + /// Handles changes of dropdown menu + final ValueSetter onChange; + + /// {@macro zeta-component-rounded} + final bool rounded; + + /// The style for the leading widget. Can be a checkbox or radio button + final LeadingStyle leadingType; + + /// If menu is minimised. + final bool isMinimized; + + @override + State createState() => _ZetaDropDownState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('leadingType', leadingType)) + ..add(DiagnosticsProperty('rounded', rounded)) + ..add( + ObjectFlagProperty>.has( + 'onChange', + onChange, + ), + ) + ..add(DiagnosticsProperty('isMinimized', isMinimized)); + } +} + +class _ZetaDropDownState extends State { + final OverlayPortalController _tooltipController = OverlayPortalController(); + final _link = LayerLink(); + final _menuKey = GlobalKey(); // declare a global key + + /// Returns if click event position is within the header. + bool _isInHeader( + Offset headerPosition, + Size headerSize, + Offset clickPosition, + ) { + return clickPosition.dx >= headerPosition.dx && + clickPosition.dx <= headerPosition.dx + headerSize.width && + clickPosition.dy >= headerPosition.dy && + clickPosition.dy <= headerPosition.dy + headerSize.height; + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: _size, + child: CompositedTransformTarget( + link: _link, + child: OverlayPortal( + controller: _tooltipController, + overlayChildBuilder: (BuildContext context) { + return CompositedTransformFollower( + link: _link, + targetAnchor: Alignment.bottomLeft, // Align overlay dropdown in its correct position + child: Align( + alignment: AlignmentDirectional.topStart, + child: TapRegion( + onTapOutside: (event) { + final headerBox = _menuKey.currentContext!.findRenderObject()! as RenderBox; + + final headerPosition = headerBox.localToGlobal(Offset.zero); + + if (!_isInHeader( + headerPosition, + headerBox.size, + event.position, + )) _tooltipController.hide(); + }, + child: ZetaDropDownMenu( + items: widget.items, + selected: widget.selectedItem.value, + width: _size, + boxType: widget.leadingType, + onPress: (item) { + if (item != null) { + widget.onChange(item); + } + _tooltipController.hide(); + }, + ), + ), + ), + ); + }, + child: widget.selectedItem.copyWith( + round: widget.rounded, + focus: _tooltipController.isShowing, + press: onTap, + inputKey: _menuKey, + ), + ), + ), + ); + } + + double get _size => widget.isMinimized ? 120 : 320; + + void onTap() { + _tooltipController.toggle(); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DiagnosticsProperty>>( + 'menuKey', + _menuKey, + ), + ); + } +} + +/// Checkbox enum for different checkbox types +enum LeadingStyle { + /// No Leading + none, + + /// Circular checkbox + checkbox, + + /// Square checkbox + radio +} + +/// Class for [ZetaDropdownItem] +class ZetaDropdownItem extends StatefulWidget { + ///Public constructor for [ZetaDropdownItem] + const ZetaDropdownItem({ + super.key, + required this.value, + this.leadingIcon, + }) : rounded = true, + selected = false, + leadingType = LeadingStyle.none, + itemKey = null, + onPress = null; + + const ZetaDropdownItem._({ + super.key, + required this.rounded, + required this.selected, + required this.value, + this.leadingIcon, + this.onPress, + this.leadingType, + this.itemKey, + }); + + /// {@macro zeta-component-rounded} + final bool rounded; + + /// If [ZetaDropdownItem] is selected + final bool selected; + + /// Value of [ZetaDropdownItem] + final String value; + + /// Leading icon for [ZetaDropdownItem] + final Icon? leadingIcon; + + /// Handles clicking for [ZetaDropdownItem] + final VoidCallback? onPress; + + /// If checkbox is to be shown, the type of it. + final LeadingStyle? leadingType; + + /// Key for item + final GlobalKey? itemKey; + + /// Returns copy of [ZetaDropdownItem] with those private variables included + ZetaDropdownItem copyWith({ + bool? round, + bool? focus, + LeadingStyle? boxType, + VoidCallback? press, + GlobalKey? inputKey, + }) { + return ZetaDropdownItem._( + rounded: round ?? rounded, + selected: focus ?? selected, + onPress: press ?? onPress, + leadingType: boxType ?? leadingType, + itemKey: inputKey ?? itemKey, + value: value, + leadingIcon: leadingIcon, + key: key, + ); + } + + @override + State createState() => _ZetaDropdownMenuItemState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(DiagnosticsProperty('selected', selected)) + ..add(StringProperty('value', value)) + ..add(ObjectFlagProperty.has('onPress', onPress)) + ..add(EnumProperty('leadingType', leadingType)) + ..add( + DiagnosticsProperty>?>( + 'itemKey', + itemKey, + ), + ); + } +} + +class _ZetaDropdownMenuItemState extends State { + final controller = MaterialStatesController(); + + @override + void initState() { + super.initState(); + controller.addListener(() { + if (context.mounted && mounted && !controller.value.contains(MaterialState.disabled)) { + setState(() {}); + } + }); + } + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return DefaultTextStyle( + style: ZetaTextStyles.bodyMedium, + child: OutlinedButton( + key: widget.itemKey, + onPressed: widget.onPress, + style: _getStyle(colors), + child: Row( + children: [ + const SizedBox(width: ZetaSpacing.x3), + _getLeadingWidget(), + const SizedBox(width: ZetaSpacing.x3), + Text( + widget.value, + ), + ], + ).paddingVertical(ZetaSpacing.x2_5), + ), + ); + } + + Widget _getLeadingWidget() { + switch (widget.leadingType!) { + case LeadingStyle.checkbox: + return Checkbox( + value: widget.selected, + onChanged: (val) { + widget.onPress!.call(); + }, + ); + case LeadingStyle.radio: + return Radio( + value: widget.selected, + groupValue: true, + onChanged: (val) { + widget.onPress!.call(); + }, + ); + case LeadingStyle.none: + return widget.leadingIcon ?? + const SizedBox( + width: 24, + ); + } + } + + ButtonStyle _getStyle(ZetaColors colors) { + return ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.hovered)) { + return colors.surfaceHovered; + } + + if (states.contains(MaterialState.pressed)) { + return colors.surfaceSelected; + } + + if (states.contains(MaterialState.disabled) || widget.onPress == null) { + return colors.surfaceDisabled; + } + return colors.surfacePrimary; + }), + foregroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return colors.textDisabled; + } + return colors.textDefault; + }), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: widget.rounded ? ZetaRadius.minimal : ZetaRadius.none, + ), + ), + side: MaterialStatePropertyAll( + widget.selected ? BorderSide(color: colors.primary.shade60) : BorderSide.none, + ), + padding: const MaterialStatePropertyAll(EdgeInsets.zero), + elevation: const MaterialStatePropertyAll(0), + overlayColor: const MaterialStatePropertyAll(Colors.transparent), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DiagnosticsProperty( + 'controller', + controller, + ), + ); + } +} + +///Class for [ZetaDropDownMenu] +class ZetaDropDownMenu extends StatefulWidget { + ///Constructor for [ZetaDropDownMenu] + const ZetaDropDownMenu({ + super.key, + required this.items, + required this.onPress, + required this.selected, + this.rounded = false, + this.width, + this.boxType, + }); + + /// Input items for the menu + final List items; + + ///Handles clicking of item in menu + final ValueSetter onPress; + + /// If item in menu is the currently selected item + final String selected; + + /// {@macro zeta-component-rounded} + final bool rounded; + + /// Width for menu + final double? width; + + /// If items have checkboxes, the type of that checkbox. + final LeadingStyle? boxType; + + @override + State createState() => _ZetaDropDownMenuState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add( + ObjectFlagProperty>.has( + 'onPress', + onPress, + ), + ) + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(DoubleProperty('width', width)) + ..add(EnumProperty('boxType', boxType)) + ..add(StringProperty('selected', selected)); + } +} + +class _ZetaDropDownMenuState extends State { + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + return Container( + decoration: BoxDecoration( + color: colors.surfacePrimary, + borderRadius: widget.rounded ? ZetaRadius.minimal : ZetaRadius.none, + boxShadow: const [ + BoxShadow(blurRadius: 2, color: Color.fromRGBO(40, 51, 61, 0.04)), + BoxShadow( + blurRadius: 8, + color: Color.fromRGBO(96, 104, 112, 0.16), + blurStyle: BlurStyle.outer, + offset: Offset(0, 4), + ), + ], + ), + width: widget.width, + child: Builder( + builder: (BuildContext bcontext) { + return Column( + mainAxisSize: MainAxisSize.min, + children: widget.items.map((item) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + item.copyWith( + round: widget.rounded, + focus: widget.selected == item.value, + boxType: widget.boxType, + press: () { + widget.onPress(item); + }, + ), + const SizedBox(height: ZetaSpacing.x1), + ], + ); + }).toList(), + ); + }, + ), + ); + } +} diff --git a/lib/src/components/list_item/list_item.dart b/lib/src/components/list_item/list_item.dart new file mode 100644 index 00000000..f73ebac6 --- /dev/null +++ b/lib/src/components/list_item/list_item.dart @@ -0,0 +1,208 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zeta_flutter.dart'; + +/// A single row that typically contains some text as well as a leading or trailing widgets. +class ZetaListItem extends StatelessWidget { + /// Creates a [ZetaListItem]. + const ZetaListItem({ + required this.title, + this.dense = false, + this.enabled = true, + this.enabledDivider = true, + this.leading, + this.onTap, + this.selected = false, + this.subtitle, + this.trailing, + super.key, + }); + + /// Dense list items have less space between widgets and use smaller [TextStyle] + final bool dense; + + /// Whether this [ZetaListItem] is interactive. + /// If false the [onTap] callback is inoperative. + final bool enabled; + + /// Whether to apply divider. Normally at the bottom of the [ZetaListItem]. + final bool enabledDivider; + + /// A Widget to display before the title; + final Widget? leading; + + /// Called when user taps on the [ZetaListItem]. + final VoidCallback? onTap; + + /// Applies selected styles. If selected is true trailing mu + final bool selected; + + /// Additional content displayed over the title. + /// Typically a [Text] widget. + final Widget? subtitle; + + /// The primary content of the [ZetaListItem]. + final Widget title; + + /// A widget to display after the title. + final Widget? trailing; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('dense', dense)) + ..add(DiagnosticsProperty('enabled', enabled)) + ..add(DiagnosticsProperty('enabledDivider', enabledDivider)) + ..add(DiagnosticsProperty('leading', leading)) + ..add(ObjectFlagProperty.has('onTap', onTap)) + ..add(DiagnosticsProperty('selected', selected)) + ..add(DiagnosticsProperty('subtitle', subtitle)) + ..add(DiagnosticsProperty('title', title)) + ..add(DiagnosticsProperty('trailing', trailing)); + } + + TextStyle get _titleTextStyle => dense ? ZetaTextStyles.titleSmall : ZetaTextStyles.titleMedium; + + @override + Widget build(BuildContext context) { + final zetaColors = Zeta.of(context).colors; + + return _ListItemContainer( + enabled: enabled, + selected: selected, + onTap: onTap, + dense: dense, + enabledDivider: enabledDivider, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + if (leading != null) + Padding( + padding: EdgeInsets.only( + right: dense ? ZetaSpacing.x2 : ZetaSpacing.x4, + ), + child: leading, + ), + Flexible( + child: Padding( + padding: EdgeInsets.symmetric( + vertical: dense ? 0.0 : ZetaSpacing.x4, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (subtitle != null) + DefaultTextStyle( + style: ZetaTextStyles.titleSmall.copyWith( + color: enabled ? zetaColors.textSubtle : zetaColors.textDisabled, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: subtitle ?? const SizedBox(), + ), + DefaultTextStyle( + style: _titleTextStyle.copyWith( + color: enabled ? zetaColors.textDefault : zetaColors.textDisabled, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: title, + ), + ], + ), + ), + ), + ], + ), + ), + if (trailing != null) + Padding( + padding: EdgeInsets.only( + left: dense ? ZetaSpacing.x2 : ZetaSpacing.x4, + ), + child: trailing, + ), + if (trailing == null && selected && enabled) + Padding( + padding: EdgeInsets.only( + left: dense ? ZetaSpacing.x2 : ZetaSpacing.x4, + ), + child: Icon( + ZetaIcons.check_mark_round, + color: zetaColors.blue.shade60, + ), + ), + ], + ), + ); + } +} + +class _ListItemContainer extends StatelessWidget { + const _ListItemContainer({ + required this.child, + required this.dense, + required this.enabled, + required this.enabledDivider, + required this.onTap, + required this.selected, + }); + + final Widget child; + final bool dense; + final bool enabled; + final bool enabledDivider; + final VoidCallback? onTap; + final bool selected; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('child', child)) + ..add(DiagnosticsProperty('enabled', enabled)) + ..add(DiagnosticsProperty('selected', selected)) + ..add(ObjectFlagProperty.has('onTap', onTap)) + ..add(DiagnosticsProperty('dense', dense)) + ..add(DiagnosticsProperty('enabledDivider', enabledDivider)); + } + + @override + Widget build(BuildContext context) { + final zetaColors = Zeta.of(context).colors; + + return AbsorbPointer( + absorbing: !enabled, + child: Material( + color: enabled + ? selected + ? zetaColors.blue.shade10 + : zetaColors.white + : zetaColors.surfaceDisabled, + child: InkWell( + onTap: enabled ? onTap : null, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: dense ? ZetaSpacing.x4 : ZetaSpacing.x8, + vertical: dense ? ZetaSpacing.x2 : ZetaSpacing.x4, + ), + decoration: BoxDecoration( + border: enabled && enabledDivider + ? Border( + bottom: BorderSide( + color: selected ? zetaColors.blue.shade40 : zetaColors.borderDefault, + ), + ) + : null, + ), + child: child, + ), + ), + ), + ); + } +} diff --git a/lib/src/components/radio/radio.dart b/lib/src/components/radio/radio.dart new file mode 100644 index 00000000..7971e2ca --- /dev/null +++ b/lib/src/components/radio/radio.dart @@ -0,0 +1,157 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; + +/// Zeta Radio Button +/// +/// Radio Button can select one single option from a goup of different options. +class ZetaRadio extends StatefulWidget { + /// Constructor for [ZetaRadio]. + const ZetaRadio({ + super.key, + required this.value, + this.groupValue, + this.onChanged, + this.label, + }); + + /// The value of the option, which can be selected by this Radio Button. + final T value; + + /// The selected value among all possible options. + final T? groupValue; + + /// Callback function to call when the Radio Button is tapped. + final ValueChanged? onChanged; + + /// The label which appears next to the Radio Button, on the right side. + final Widget? label; + + bool get _selected => value == groupValue; + + @override + State> createState() => _ZetaRadioState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('value', value)) + ..add(DiagnosticsProperty('groupValue', groupValue)) + ..add(ObjectFlagProperty?>('onChanged', onChanged, ifNull: 'disabled')); + } +} + +class _ZetaRadioState extends State> with TickerProviderStateMixin, ToggleableStateMixin { + final ToggleablePainter _painter = _RadioPainter(); + @override + Widget build(BuildContext context) { + final zetaColors = Zeta.of(context).colors; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Semantics( + inMutuallyExclusiveGroup: true, + checked: widget._selected, + selected: value, + child: buildToggleable( + size: const Size(36, 36), + painter: _painter + ..position = position + ..reaction = reaction + ..reactionFocusFade = reactionFocusFade + ..reactionHoverFade = reactionHoverFade + ..inactiveReactionColor = Colors.transparent + ..reactionColor = Colors.transparent + ..hoverColor = Colors.transparent + ..focusColor = zetaColors.blue.shade50 + ..splashRadius = 12 + ..downPosition = downPosition + ..isFocused = states.contains(MaterialState.focused) + ..isHovered = states.contains(MaterialState.hovered) + ..activeColor = + states.contains(MaterialState.disabled) ? zetaColors.cool.shade30 : zetaColors.blue.shade60 + ..inactiveColor = + states.contains(MaterialState.disabled) ? zetaColors.cool.shade30 : zetaColors.cool.shade70, + mouseCursor: MaterialStateProperty.all( + MaterialStateProperty.resolveAs( + MaterialStateMouseCursor.clickable, + states, + ), + ), + ), + ), + if (widget.label != null) + GestureDetector( + onTap: () => onChanged?.call(true), + child: DefaultTextStyle( + style: ZetaTextStyles.bodyMedium.copyWith( + color: states.contains(MaterialState.disabled) ? zetaColors.textDisabled : zetaColors.textDefault, + height: 1.33, + ), + child: widget.label!, + ), + ), + ], + ); + } + + void _handleChanged(bool? selected) { + if (selected == null) { + widget.onChanged!(null); + return; + } + if (selected) { + widget.onChanged!(widget.value); + } + } + + @override + ValueChanged? get onChanged => widget.onChanged != null ? _handleChanged : null; + + @override + bool get tristate => false; + + @override + bool get value => widget._selected; + + @override + void didUpdateWidget(ZetaRadio oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget._selected != oldWidget._selected) { + animateToValue(); + } + } + + @override + void dispose() { + _painter.dispose(); + super.dispose(); + } +} + +const double _kOuterRadius = 10; +const double _kInnerRadius = 5; + +class _RadioPainter extends ToggleablePainter { + @override + void paint(Canvas canvas, Size size) { + paintRadialReaction(canvas: canvas, origin: size.center(Offset.zero)); + + final Offset center = (Offset.zero & size).center; + + // Outer circle + final Paint paint = Paint() + ..color = Color.lerp(inactiveColor, activeColor, position.value)! + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + canvas.drawCircle(center, _kOuterRadius, paint); + + // Inner circle + if (!position.isDismissed) { + paint.style = PaintingStyle.fill; + canvas.drawCircle(center, _kInnerRadius * position.value, paint); + } + } +} diff --git a/lib/src/components/snack_bar/snack_bar.dart b/lib/src/components/snack_bar/snack_bar.dart new file mode 100644 index 00000000..e5250583 --- /dev/null +++ b/lib/src/components/snack_bar/snack_bar.dart @@ -0,0 +1,350 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zeta_flutter.dart'; + +/// Type used to define contextual [SnackBar]. The type defines the styles, icons and behavior. +enum ZetaSnackBarType { + /// Default colors with leading icon with close action. + defaultType, + + /// Default colors with leading icon and custom action. + action, + + /// Success styles with close action. + positive, + + /// Info styles with close action. + info, + + /// Warning styles with close action. + warning, + + /// Error styles with close action. + error, + + /// Deletion styles with custom undo action. + deletion, + + /// View styles with custom view action. + view, +} + +/// A lightweight message with an optional action which briefly displays at the +/// bottom of the screen. +/// +/// Different styles can be applied to [ZetaSnackBar] with [ZetaSnackBarType]. +class ZetaSnackBar extends SnackBar { + /// Sets basic styles for the [SnackBar]. + ZetaSnackBar({ + required BuildContext context, + required Widget content, + VoidCallback? onPressed, + ZetaSnackBarType? type, + Icon? leadingIcon, + bool rounded = true, + String? actionLabel, + String deleteActionLabel = 'Undo', + String viewActionLabel = 'View', + super.margin, + super.behavior = SnackBarBehavior.floating, + super.key, + }) : super( + elevation: 0, + padding: EdgeInsets.zero, + backgroundColor: _getBackgroundColorForType(context, type), + shape: RoundedRectangleBorder( + borderRadius: type != null + ? ZetaRadius.full + : rounded + ? ZetaRadius.minimal + : ZetaRadius.none, + ), + content: Padding( + padding: const EdgeInsets.symmetric(vertical: ZetaSpacing.xs), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + _LeadingIcon(type, leadingIcon), + Flexible( + child: Padding( + padding: const EdgeInsets.only(left: ZetaSpacing.s), + child: _Content(type: type, child: content), + ), + ), + ], + ), + ), + _Action( + type: type, + actionLabel: actionLabel, + onPressed: onPressed, + deleteActionLabel: deleteActionLabel, + viewActionLabel: viewActionLabel, + ), + ], + ), + ), + ); + + static Color _getBackgroundColorForType( + BuildContext context, + ZetaSnackBarType? type, + ) { + final colors = Zeta.of(context).colors; + + return switch (type) { + ZetaSnackBarType.positive => colors.green.shade10, + ZetaSnackBarType.info => colors.purple.shade10, + ZetaSnackBarType.warning => colors.orange.shade10, + ZetaSnackBarType.deletion || ZetaSnackBarType.error => colors.red.shade10, + ZetaSnackBarType.view => colors.blue.shade10, + _ => colors.warm.shade100, + }; + } +} + +class _Content extends StatelessWidget { + const _Content({required this.child, required this.type}); + + final Widget child; + final ZetaSnackBarType? type; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('type', type)) + ..add(DiagnosticsProperty('child', child)); + } + + Color _getColorForType( + ZetaColors colors, + ZetaSnackBarType? type, + ) { + return switch (type) { + ZetaSnackBarType.positive || + ZetaSnackBarType.info || + ZetaSnackBarType.warning || + ZetaSnackBarType.deletion || + ZetaSnackBarType.error || + ZetaSnackBarType.view => + colors.textDefault, + _ => colors.textInverse, + }; + } + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return DefaultTextStyle( + style: ZetaTextStyles.bodyMedium.copyWith( + color: _getColorForType(colors, type), + ), + child: child, + ); + } +} + +class _Action extends StatelessWidget { + const _Action({ + required this.type, + required this.actionLabel, + required this.onPressed, + required this.deleteActionLabel, + required this.viewActionLabel, + }); + + final String? actionLabel; + final String deleteActionLabel; + final VoidCallback? onPressed; + final ZetaSnackBarType? type; + final String viewActionLabel; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('type', type)) + ..add(StringProperty('actionLabel', actionLabel)) + ..add(StringProperty('deleteActionLabel', deleteActionLabel)) + ..add(ObjectFlagProperty.has('onPressed', onPressed)) + ..add(StringProperty('viewActionLabel', viewActionLabel)); + } + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return switch (type) { + ZetaSnackBarType.defaultType => _IconButton( + onPressed: () => ScaffoldMessenger.of(context).removeCurrentSnackBar(), + color: colors.iconInverse, + ), + ZetaSnackBarType.action => _ActionButton( + onPressed: onPressed, + label: actionLabel, + color: colors.blue.shade50, + ), + ZetaSnackBarType.positive || + ZetaSnackBarType.info || + ZetaSnackBarType.warning || + ZetaSnackBarType.error => + _IconButton( + onPressed: () => ScaffoldMessenger.of(context).removeCurrentSnackBar(), + color: colors.cool.shade90, + ), + ZetaSnackBarType.deletion => _ActionButton( + onPressed: onPressed, + label: deleteActionLabel, + color: colors.cool.shade90, + ), + ZetaSnackBarType.view => _ActionButton( + onPressed: onPressed, + label: viewActionLabel, + color: colors.cool.shade90, + ), + _ => _ActionButton( + onPressed: onPressed, + label: actionLabel, + color: colors.blue.shade50, + ), + }; + } +} + +class _IconButton extends StatelessWidget { + const _IconButton({ + required this.onPressed, + required this.color, + }); + + final Color color; + final VoidCallback? onPressed; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty.has('onPressed', onPressed)) + ..add(ColorProperty('color', color)); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: ZetaSpacing.xxs), + child: IconButton( + style: IconButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: ZetaSpacing.s), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size(20, 20), + ), + onPressed: onPressed, + icon: Icon( + ZetaIcons.close_round, + color: color, + size: 20, + ), + ), + ); + } +} + +class _ActionButton extends StatelessWidget { + const _ActionButton({ + required this.onPressed, + required this.label, + required this.color, + }); + + final Color color; + final String? label; + final VoidCallback? onPressed; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty.has('onPressed', onPressed)) + ..add(StringProperty('label', label)) + ..add(ColorProperty('color', color)); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: ZetaSpacing.s), + child: TextButton( + style: TextButton.styleFrom( + textStyle: ZetaTextStyles.labelLarge, + padding: const EdgeInsets.symmetric( + horizontal: ZetaSpacing.s, + vertical: ZetaSpacing.xxs, + ), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: Size.zero, + ), + onPressed: onPressed, + child: Text( + label ?? '', + style: ZetaTextStyles.labelLarge.copyWith(color: color), + ), + ), + ); + } +} + +class _LeadingIcon extends StatelessWidget { + const _LeadingIcon(this.type, this.icon); + + final Icon? icon; + final ZetaSnackBarType? type; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(EnumProperty('type', type)); + } + + Color _getIconColor(ZetaColors colors, ZetaSnackBarType? type) { + return switch (type) { + ZetaSnackBarType.positive => colors.positive, + ZetaSnackBarType.info => colors.info, + ZetaSnackBarType.warning => colors.warning, + ZetaSnackBarType.error || ZetaSnackBarType.deletion => colors.negative, + ZetaSnackBarType.view => colors.primary, + _ => colors.iconInverse, + }; + } + + Widget _getIcon(ZetaSnackBarType? type) { + return switch (type) { + ZetaSnackBarType.positive => const Icon(ZetaIcons.check_circle_round), + ZetaSnackBarType.info => const Icon(ZetaIcons.info_round), + ZetaSnackBarType.warning => const Icon(ZetaIcons.warning_round), + ZetaSnackBarType.error => const Icon(ZetaIcons.error_round), + ZetaSnackBarType.deletion => const Icon(ZetaIcons.delete_round), + ZetaSnackBarType.view => const Icon(ZetaIcons.open_in_new_window_round), + _ => const SizedBox(), + }; + } + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return Padding( + padding: type != null || icon != null ? const EdgeInsets.only(left: ZetaSpacing.s) : EdgeInsets.zero, + child: IconTheme( + data: IconThemeData( + color: _getIconColor(colors, type), + ), + child: icon ?? _getIcon(type), + ), + ); + } +} diff --git a/lib/src/components/stepper/stepper.dart b/lib/src/components/stepper/stepper.dart new file mode 100644 index 00000000..25534fc9 --- /dev/null +++ b/lib/src/components/stepper/stepper.dart @@ -0,0 +1,414 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zeta_flutter.dart'; + +/// Zeta stepper widget that displays progress through a sequence of +/// steps. Steppers are particularly useful in the case of forms where one step +/// requires the completion of another one, or where multiple steps need to be +/// completed in order to submit the whole form. +class ZetaStepper extends StatefulWidget { + /// Creates a stepper from a list of steps. + /// + /// This widget is not meant to be rebuilt with a different list of steps + /// unless a key is provided in order to distinguish the old stepper from the + /// new one. + const ZetaStepper({ + required this.steps, + required this.currentStep, + this.type = ZetaStepperType.horizontal, + this.onStepTapped, + this.rounded = true, + super.key, + }); + + /// The index into [steps] of the current step whose content is displayed. + final int currentStep; + + /// The callback called when a step is tapped, with its index passed as + /// an argument. + final ValueChanged? onStepTapped; + + /// Whether the icons of the horizontal stepper to be rounded or square. + final bool rounded; + + /// The steps of the stepper whose titles, subtitles, icons always get shown. + /// + /// The length of [steps] must not change. + final List steps; + + /// The type of stepper that determines the layout. In the case of + /// [ZetaStepperType.horizontal], the content of the current step is displayed + /// underneath as opposed to the [ZetaStepperType.vertical] case where it is + /// displayed in-between. + final ZetaStepperType type; + + @override + State createState() => _ZetaStepperState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IterableProperty('steps', steps)) + ..properties.add(IntProperty('currentStep', currentStep)) + ..add(EnumProperty('type', type)) + ..add( + ObjectFlagProperty?>.has( + 'onStepTapped', + onStepTapped, + ), + ) + ..properties.add(DiagnosticsProperty('rounded', rounded)); + } +} + +class _ZetaStepperState extends State with TickerProviderStateMixin { + late List _keys; + + @override + void initState() { + super.initState(); + _keys = List.generate( + widget.steps.length, + (_) => GlobalKey(), + ); + } + + ZetaColors get _colors => Zeta.of(context).colors; + + bool _isFirst(int index) { + return index == 0; + } + + bool _isLast(int index) { + return widget.steps.length - 1 == index; + } + + bool _isCurrent(int index) { + return widget.currentStep == index; + } + + Widget _buildHorizotalIcon(int index) { + return SizedBox( + width: ZetaSpacing.l, + height: ZetaSpacing.l, + child: AnimatedContainer( + curve: Curves.fastOutSlowIn, + duration: kThemeAnimationDuration, + decoration: BoxDecoration( + color: _getColorForType(widget.steps[index].type), + shape: widget.rounded ? BoxShape.circle : BoxShape.rectangle, + ), + child: Center( + child: switch (widget.steps[index].type) { + ZetaStepType.complete => Icon( + widget.rounded ? ZetaIcons.check_mark_round : ZetaIcons.check_mark_sharp, + color: _colors.textInverse, + ), + ZetaStepType.enabled || ZetaStepType.disabled => Text( + (index + 1).toString(), + style: ZetaTextStyles.labelLarge.copyWith( + color: _colors.textInverse, + ), + ), + }, + ), + ), + ); + } + + Widget _getVerticalIcon(int index) { + return SizedBox( + width: ZetaSpacing.x12, + height: ZetaSpacing.x12, + child: AnimatedContainer( + curve: Curves.fastOutSlowIn, + duration: kThemeAnimationDuration, + decoration: BoxDecoration( + color: _getColorForType(widget.steps[index].type), + shape: widget.rounded ? BoxShape.circle : BoxShape.rectangle, + ), + child: Center( + child: switch (widget.steps[index].type) { + ZetaStepType.complete => Icon( + widget.rounded ? ZetaIcons.check_mark_round : ZetaIcons.check_mark_sharp, + color: _colors.textInverse, + ), + ZetaStepType.enabled || ZetaStepType.disabled => Text( + (index + 1).toString(), + style: ZetaTextStyles.titleLarge.copyWith( + color: _colors.textInverse, + ), + ), + }, + ), + ), + ); + } + + Widget _getHeaderText(int index) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedDefaultTextStyle( + style: switch (widget.steps[index].type) { + ZetaStepType.enabled || ZetaStepType.complete => ZetaTextStyles.bodySmall.copyWith( + color: _colors.textDefault, + ), + ZetaStepType.disabled => ZetaTextStyles.bodySmall.copyWith( + color: _colors.textDisabled, + ), + }, + maxLines: 1, + duration: kThemeAnimationDuration, + curve: Curves.fastOutSlowIn, + child: widget.steps[index].title, + ), + ], + ); + } + + Widget _getVerticalHeader(int index) { + final subtitle = widget.steps[index].subtitle; + + return Container( + margin: EdgeInsets.only(top: _isFirst(index) ? 0.0 : ZetaSpacing.m), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + _getVerticalIcon(index), + Container( + margin: const EdgeInsets.only(top: ZetaSpacing.x1), + width: ZetaSpacing.x1, + height: ZetaSpacing.x12, + decoration: BoxDecoration( + borderRadius: ZetaRadius.full, + color: switch (widget.steps[index].type) { + ZetaStepType.complete => _colors.green.shade50, + ZetaStepType.disabled => _colors.borderSubtle, + ZetaStepType.enabled => _colors.blue.shade50, + }, + ), + ), + ], + ), + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: ZetaSpacing.m), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (subtitle != null) + AnimatedDefaultTextStyle( + style: ZetaTextStyles.bodyMedium.copyWith( + color: _getColorForType(widget.steps[index].type), + ), + maxLines: 1, + duration: kThemeAnimationDuration, + curve: Curves.fastOutSlowIn, + child: subtitle, + ), + AnimatedDefaultTextStyle( + style: switch (widget.steps[index].type) { + ZetaStepType.enabled || ZetaStepType.complete => ZetaTextStyles.titleLarge.copyWith( + color: _colors.textDefault, + ), + ZetaStepType.disabled => ZetaTextStyles.titleLarge.copyWith( + color: _colors.textDisabled, + ), + }, + maxLines: 1, + duration: kThemeAnimationDuration, + curve: Curves.fastOutSlowIn, + child: widget.steps[index].title, + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _getVerticalBody(int index) { + return Stack( + children: [ + AnimatedCrossFade( + firstChild: Container(height: 0), + secondChild: widget.steps[index].content ?? const SizedBox(), + firstCurve: const Interval(0, 0.6, curve: Curves.fastOutSlowIn), + secondCurve: const Interval(0.4, 1, curve: Curves.fastOutSlowIn), + sizeCurve: Curves.fastOutSlowIn, + crossFadeState: _isCurrent(index) ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: kThemeAnimationDuration, + ), + ], + ); + } + + Color _getColorForType(ZetaStepType type) { + return switch (type) { + ZetaStepType.complete => _colors.positive, + ZetaStepType.disabled => _colors.cool.shade50, + ZetaStepType.enabled => _colors.primary, + }; + } + + @override + Widget build(BuildContext context) { + return switch (widget.type) { + ZetaStepperType.vertical => Column( + children: [ + for (int index = 0; index < widget.steps.length; index += 1) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + key: _keys[index], + children: [ + InkResponse( + containedInkWell: true, + borderRadius: ZetaRadius.minimal, + onTap: widget.onStepTapped != null ? () => widget.onStepTapped?.call(index) : null, + canRequestFocus: widget.steps[index].type != ZetaStepType.disabled, + child: _getVerticalHeader(index), + ), + _getVerticalBody(index), + ], + ), + ], + ), + ZetaStepperType.horizontal => Builder( + builder: (context) { + final children = [ + for (int index = 0; index < widget.steps.length; index += 1) ...[ + InkResponse( + onTap: widget.onStepTapped != null ? () => widget.onStepTapped?.call(index) : null, + canRequestFocus: widget.steps[index].type != ZetaStepType.disabled, + child: Column( + children: [ + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: ZetaSpacing.s, + ), + child: _buildHorizotalIcon(index), + ), + ), + _getHeaderText(index), + ], + ), + ), + if (!_isLast(index)) + Expanded( + child: Container( + key: Key('line$index'), + margin: const EdgeInsets.only( + top: ZetaSpacing.x7, + right: ZetaSpacing.b, + left: ZetaSpacing.b, + ), + height: ZetaSpacing.x0_5, + decoration: BoxDecoration( + borderRadius: ZetaRadius.full, + color: switch (widget.steps[index].type) { + ZetaStepType.complete => _colors.green.shade50, + ZetaStepType.disabled => _colors.borderSubtle, + ZetaStepType.enabled => _colors.blue.shade50, + }, + ), + ), + ), + ], + ]; + + final List stepPanels = []; + for (int i = 0; i < widget.steps.length; i += 1) { + stepPanels.add( + Visibility( + maintainState: true, + visible: i == widget.currentStep, + child: widget.steps[i].content ?? const SizedBox(), + ), + ); + } + + return Column( + children: [ + Material( + color: Colors.transparent, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: ZetaSpacing.m), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ), + ), + Expanded( + child: AnimatedSize( + curve: Curves.fastOutSlowIn, + duration: kThemeAnimationDuration, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: stepPanels, + ), + ), + ), + ], + ); + }, + ), + }; + } +} + +/// Zeta step used in [ZetaStepper]. The step can have a title and subtitle, +/// an icon within its circle, some content and a state that governs its +/// styling. +class ZetaStep { + /// Creates a step for a [ZetaStepper]. + const ZetaStep({ + required this.title, + this.content, + this.subtitle, + this.type = ZetaStepType.disabled, + }); + + /// The content of the step that appears below the [title] and [subtitle]. + final Widget? content; + + /// The subtitle of the step that appears above the title. + final Widget? subtitle; + + /// The title of the step that typically describes it. + final Widget title; + + /// The type of the step which determines the styling of its components + /// and whether steps are interactive. + final ZetaStepType type; +} + +/// The type of a [ZetaStep] which is used to control the style of the circle and text. +enum ZetaStepType { + /// A step that is currently selected with primary color icon + enabled, + + /// A step that displays a tick icon in its circle. + complete, + + /// A step that is disabled and does not to react to taps. + disabled, +} + +/// Defines the [ZetaStepper]'s main axis. +enum ZetaStepperType { + /// A vertical layout of the steps with their content in-between the titles. + vertical, + + /// A horizontal layout of the steps with their content below the titles. + horizontal, +} diff --git a/lib/src/components/switch/material_switch.dart b/lib/src/components/switch/material_switch.dart new file mode 100644 index 00000000..34cf6d62 --- /dev/null +++ b/lib/src/components/switch/material_switch.dart @@ -0,0 +1,1151 @@ +// ignore_for_file: prefer_asserts_with_message, diagnostic_describe_all_properties, public_member_api_docs + +// The content of this file is taken from +// package:flutter/src/material/switch.dart +// Changes are commented with "Zeta change:" + +import 'dart:ui'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +const double _kSwitchMinSize = kMinInteractiveDimension - 8.0; + +// Zeta change: +// Converted Flutter's private stateless [_MaterialSwitch] +// to public stateful [MaterialSwitch], so that it can be +// imported and used in [ZetaSwitch]. +class MaterialSwitch extends StatefulWidget { + const MaterialSwitch({ + super.key, + required this.value, + required this.onChanged, + // Zeta change: Require the switch `size` to be passed + // in the constructor of [MaterialSwitch]. + required this.size, + this.activeColor, + this.activeTrackColor, + this.inactiveThumbColor, + this.inactiveTrackColor, + this.activeThumbImage, + this.onActiveThumbImageError, + this.inactiveThumbImage, + this.onInactiveThumbImageError, + this.thumbColor, + this.trackColor, + this.trackOutlineColor, + this.trackOutlineWidth, + this.thumbIcon, + this.materialTapTargetSize, + this.dragStartBehavior = DragStartBehavior.start, + this.mouseCursor, + this.focusColor, + this.hoverColor, + this.overlayColor, + this.splashRadius, + this.focusNode, + this.onFocusChange, + this.autofocus = false, + // Zeta change: added optional parameter `showHover` and `thumbSize`. + this.showHover = false, + this.thumbSize, + }) : assert(activeThumbImage != null || onActiveThumbImageError == null), + assert(inactiveThumbImage != null || onInactiveThumbImageError == null); + + final bool value; + final ValueChanged? onChanged; + final Color? activeColor; + final Color? activeTrackColor; + final Color? inactiveThumbColor; + final Color? inactiveTrackColor; + final ImageProvider? activeThumbImage; + final ImageErrorListener? onActiveThumbImageError; + final ImageProvider? inactiveThumbImage; + final ImageErrorListener? onInactiveThumbImageError; + final MaterialStateProperty? thumbColor; + final MaterialStateProperty? trackColor; + final MaterialStateProperty? trackOutlineColor; + final MaterialStateProperty? trackOutlineWidth; + final MaterialStateProperty? thumbIcon; + final MaterialTapTargetSize? materialTapTargetSize; + final DragStartBehavior dragStartBehavior; + final MouseCursor? mouseCursor; + final Color? focusColor; + final Color? hoverColor; + final MaterialStateProperty? overlayColor; + final double? splashRadius; + final FocusNode? focusNode; + final ValueChanged? onFocusChange; + final bool autofocus; + // Zeta change: added optional parameters `showHover` and `thumbSize`. + final bool showHover; + final Size? thumbSize; + final Size size; + + @override + State createState() => _MaterialSwitchState(); +} + +class _MaterialSwitchState extends State with TickerProviderStateMixin, ToggleableStateMixin { + final _SwitchPainter _painter = _SwitchPainter(); + // Zeta change: added local `_size` and `_switchConfig`. + late final Size _size; + late final _SwitchConfig _switchConfig; + + // Zeta change: added initState(). + @override + void initState() { + super.initState(); + _switchConfig = _SwitchConfigM3(size: widget.size); + _size = Size(_switchConfig.switchWidth, _switchConfig.switchHeightCollapsed); + } + + @override + void didUpdateWidget(MaterialSwitch oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.value != widget.value) { + // During a drag we may have modified the curve, reset it if its possible + // to do without visual discontinuation. + if (position.value == 0.0 || position.value == 1.0) { + updateCurve(); + } + animateToValue(); + } + } + + @override + void dispose() { + _painter.dispose(); + super.dispose(); + } + + @override + ValueChanged? get onChanged => widget.onChanged != null ? _handleChanged : null; + + @override + bool get tristate => false; + + @override + bool? get value => widget.value; + + void updateCurve() { + if (Theme.of(context).useMaterial3) { + position + ..curve = Curves.easeOutBack + ..reverseCurve = Curves.easeOutBack.flipped; + } else { + position + ..curve = Curves.easeIn + ..reverseCurve = Curves.easeOut; + } + } + + MaterialStateProperty get _widgetThumbColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return widget.inactiveThumbColor; + } + if (states.contains(MaterialState.selected)) { + return widget.activeColor; + } + return widget.inactiveThumbColor; + }); + } + + MaterialStateProperty get _widgetTrackColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return widget.activeTrackColor; + } + return widget.inactiveTrackColor; + }); + } + + double get _trackInnerLength { + return _size.width - _kSwitchMinSize; + } + + void _handleDragStart(DragStartDetails details) { + if (isInteractive) { + reactionController.forward(); + } + } + + void _handleDragUpdate(DragUpdateDetails details) { + if (isInteractive) { + position + ..curve = Curves.linear + ..reverseCurve = null; + final double delta = details.primaryDelta! / _trackInnerLength; + switch (Directionality.of(context)) { + case TextDirection.rtl: + positionController.value -= delta; + case TextDirection.ltr: + positionController.value += delta; + } + } + } + + bool _needsPositionAnimation = false; + + void _handleDragEnd(DragEndDetails details) { + if (position.value >= 0.5 != widget.value) { + widget.onChanged?.call(!widget.value); + // Wait with finishing the animation until widget.value has changed to + // !widget.value as part of the widget.onChanged call above. + setState(() { + _needsPositionAnimation = true; + }); + } else { + animateToValue(); + } + reactionController.reverse(); + } + + void _handleChanged(bool? value) { + assert(value != null); + assert(widget.onChanged != null); + widget.onChanged?.call(value!); + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); + + if (_needsPositionAnimation) { + _needsPositionAnimation = false; + animateToValue(); + } + + final ThemeData theme = Theme.of(context); + final SwitchThemeData switchTheme = SwitchTheme.of(context); + final SwitchThemeData defaults = _SwitchDefaultsM3(context, size: widget.size); + + positionController.duration = Duration(milliseconds: _switchConfig.toggleDuration); + + // Colors need to be resolved in selected and non selected states separately + // so that they can be lerped between. + final Set activeStates = states..add(MaterialState.selected); + final Set inactiveStates = states..remove(MaterialState.selected); + + final Color? activeThumbColor = widget.thumbColor?.resolve(activeStates) ?? + _widgetThumbColor.resolve(activeStates) ?? + switchTheme.thumbColor?.resolve(activeStates); + final Color effectiveActiveThumbColor = activeThumbColor ?? defaults.thumbColor!.resolve(activeStates)!; + final Color? inactiveThumbColor = widget.thumbColor?.resolve(inactiveStates) ?? + _widgetThumbColor.resolve(inactiveStates) ?? + switchTheme.thumbColor?.resolve(inactiveStates); + final Color effectiveInactiveThumbColor = inactiveThumbColor ?? defaults.thumbColor!.resolve(inactiveStates)!; + final Color effectiveActiveTrackColor = widget.trackColor?.resolve(activeStates) ?? + _widgetTrackColor.resolve(activeStates) ?? + switchTheme.trackColor?.resolve(activeStates) ?? + _widgetThumbColor.resolve(activeStates)?.withAlpha(0x80) ?? + defaults.trackColor!.resolve(activeStates)!; + final Color? effectiveActiveTrackOutlineColor = widget.trackOutlineColor?.resolve(activeStates) ?? + switchTheme.trackOutlineColor?.resolve(activeStates) ?? + defaults.trackOutlineColor!.resolve(activeStates); + final double? effectiveActiveTrackOutlineWidth = widget.trackOutlineWidth?.resolve(activeStates) ?? + switchTheme.trackOutlineWidth?.resolve(activeStates) ?? + defaults.trackOutlineWidth?.resolve(activeStates); + + final Color effectiveInactiveTrackColor = widget.trackColor?.resolve(inactiveStates) ?? + _widgetTrackColor.resolve(inactiveStates) ?? + switchTheme.trackColor?.resolve(inactiveStates) ?? + defaults.trackColor!.resolve(inactiveStates)!; + final Color? effectiveInactiveTrackOutlineColor = widget.trackOutlineColor?.resolve(inactiveStates) ?? + switchTheme.trackOutlineColor?.resolve(inactiveStates) ?? + defaults.trackOutlineColor?.resolve(inactiveStates); + final double? effectiveInactiveTrackOutlineWidth = widget.trackOutlineWidth?.resolve(inactiveStates) ?? + switchTheme.trackOutlineWidth?.resolve(inactiveStates) ?? + defaults.trackOutlineWidth?.resolve(inactiveStates); + + final Icon? effectiveActiveIcon = + widget.thumbIcon?.resolve(activeStates) ?? switchTheme.thumbIcon?.resolve(activeStates); + final Icon? effectiveInactiveIcon = + widget.thumbIcon?.resolve(inactiveStates) ?? switchTheme.thumbIcon?.resolve(inactiveStates); + + final Color effectiveActiveIconColor = effectiveActiveIcon?.color ?? effectiveActiveThumbColor; + final Color effectiveInactiveIconColor = effectiveInactiveIcon?.color ?? effectiveInactiveThumbColor; + + final Set focusedStates = states..add(MaterialState.focused); + final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates) ?? + widget.focusColor ?? + switchTheme.overlayColor?.resolve(focusedStates) ?? + defaults.overlayColor!.resolve(focusedStates)!; + + final Set hoveredStates = states..add(MaterialState.hovered); + final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates) ?? + widget.hoverColor ?? + switchTheme.overlayColor?.resolve(hoveredStates) ?? + defaults.overlayColor!.resolve(hoveredStates)!; + + final Set activePressedStates = activeStates..add(MaterialState.pressed); + final Color effectiveActivePressedThumbColor = widget.thumbColor?.resolve(activePressedStates) ?? + _widgetThumbColor.resolve(activePressedStates) ?? + switchTheme.thumbColor?.resolve(activePressedStates) ?? + defaults.thumbColor!.resolve(activePressedStates)!; + final Color effectiveActivePressedOverlayColor = widget.overlayColor?.resolve(activePressedStates) ?? + switchTheme.overlayColor?.resolve(activePressedStates) ?? + activeThumbColor?.withAlpha(kRadialReactionAlpha) ?? + defaults.overlayColor!.resolve(activePressedStates)!; + + final Set inactivePressedStates = inactiveStates..add(MaterialState.pressed); + final Color effectiveInactivePressedThumbColor = widget.thumbColor?.resolve(inactivePressedStates) ?? + _widgetThumbColor.resolve(inactivePressedStates) ?? + switchTheme.thumbColor?.resolve(inactivePressedStates) ?? + defaults.thumbColor!.resolve(inactivePressedStates)!; + final Color effectiveInactivePressedOverlayColor = widget.overlayColor?.resolve(inactivePressedStates) ?? + switchTheme.overlayColor?.resolve(inactivePressedStates) ?? + inactiveThumbColor?.withAlpha(kRadialReactionAlpha) ?? + defaults.overlayColor!.resolve(inactivePressedStates)!; + + final MaterialStateProperty effectiveMouseCursor = + MaterialStateProperty.resolveWith((Set states) { + return MaterialStateProperty.resolveAs(widget.mouseCursor, states) ?? + switchTheme.mouseCursor?.resolve(states) ?? + defaults.mouseCursor!.resolve(states)!; + }); + + final double effectiveActiveThumbRadius = + effectiveActiveIcon == null ? _switchConfig.activeThumbRadius : _switchConfig.thumbRadiusWithIcon; + final double effectiveInactiveThumbRadius = effectiveInactiveIcon == null && widget.inactiveThumbImage == null + ? _switchConfig.inactiveThumbRadius + : _switchConfig.thumbRadiusWithIcon; + final double effectiveSplashRadius = widget.splashRadius ?? switchTheme.splashRadius ?? defaults.splashRadius!; + + return Semantics( + toggled: widget.value, + child: GestureDetector( + excludeFromSemantics: true, + onHorizontalDragStart: _handleDragStart, + onHorizontalDragUpdate: _handleDragUpdate, + onHorizontalDragEnd: _handleDragEnd, + dragStartBehavior: widget.dragStartBehavior, + child: buildToggleable( + mouseCursor: effectiveMouseCursor, + focusNode: widget.focusNode, + onFocusChange: widget.onFocusChange, + autofocus: widget.autofocus, + size: _size, + painter: _painter + ..position = position + ..reaction = reaction + ..reactionFocusFade = reactionFocusFade + ..reactionHoverFade = reactionHoverFade + ..inactiveReactionColor = effectiveInactivePressedOverlayColor + ..reactionColor = effectiveActivePressedOverlayColor + ..hoverColor = effectiveHoverOverlayColor + ..focusColor = effectiveFocusOverlayColor + ..splashRadius = effectiveSplashRadius + ..downPosition = downPosition + ..isFocused = states.contains(MaterialState.focused) + // Zeta change: added `widget.showHover` to the below condition. + ..isHovered = widget.showHover && states.contains(MaterialState.hovered) + ..activeColor = effectiveActiveThumbColor + ..inactiveColor = effectiveInactiveThumbColor + ..activePressedColor = effectiveActivePressedThumbColor + ..inactivePressedColor = effectiveInactivePressedThumbColor + ..activeThumbImage = widget.activeThumbImage + ..onActiveThumbImageError = widget.onActiveThumbImageError + ..inactiveThumbImage = widget.inactiveThumbImage + ..onInactiveThumbImageError = widget.onInactiveThumbImageError + ..activeTrackColor = effectiveActiveTrackColor + ..activeTrackOutlineColor = effectiveActiveTrackOutlineColor + ..activeTrackOutlineWidth = effectiveActiveTrackOutlineWidth + ..inactiveTrackColor = effectiveInactiveTrackColor + ..inactiveTrackOutlineColor = effectiveInactiveTrackOutlineColor + ..inactiveTrackOutlineWidth = effectiveInactiveTrackOutlineWidth + ..configuration = createLocalImageConfiguration(context) + ..isInteractive = isInteractive + ..trackInnerLength = _trackInnerLength + ..textDirection = Directionality.of(context) + ..surfaceColor = theme.colorScheme.surface + ..inactiveThumbRadius = effectiveInactiveThumbRadius + ..activeThumbRadius = effectiveActiveThumbRadius + ..pressedThumbRadius = _switchConfig.pressedThumbRadius + ..thumbOffset = _switchConfig.thumbOffset + ..trackHeight = _switchConfig.trackHeight + ..trackWidth = _switchConfig.trackWidth + ..activeIconColor = effectiveActiveIconColor + ..inactiveIconColor = effectiveInactiveIconColor + ..activeIcon = effectiveActiveIcon + ..inactiveIcon = effectiveInactiveIcon + ..iconTheme = IconTheme.of(context) + ..thumbShadow = _switchConfig.thumbShadow + ..positionController = positionController + // Zeta change: pass thumbsize + .._thumbSize = widget.thumbSize, + ), + ), + ); + } +} + +class _SwitchPainter extends ToggleablePainter { + AnimationController get positionController => _positionController!; + AnimationController? _positionController; + set positionController(AnimationController? value) { + assert(value != null); + if (value == _positionController) { + return; + } + _positionController = value; + notifyListeners(); + } + + Icon? get activeIcon => _activeIcon; + Icon? _activeIcon; + set activeIcon(Icon? value) { + if (value == _activeIcon) { + return; + } + _activeIcon = value; + notifyListeners(); + } + + Icon? get inactiveIcon => _inactiveIcon; + Icon? _inactiveIcon; + set inactiveIcon(Icon? value) { + if (value == _inactiveIcon) { + return; + } + _inactiveIcon = value; + notifyListeners(); + } + + IconThemeData? get iconTheme => _iconTheme; + IconThemeData? _iconTheme; + set iconTheme(IconThemeData? value) { + if (value == _iconTheme) { + return; + } + _iconTheme = value; + notifyListeners(); + } + + Color get activeIconColor => _activeIconColor!; + Color? _activeIconColor; + set activeIconColor(Color value) { + if (value == _activeIconColor) { + return; + } + _activeIconColor = value; + notifyListeners(); + } + + Color get inactiveIconColor => _inactiveIconColor!; + Color? _inactiveIconColor; + set inactiveIconColor(Color value) { + if (value == _inactiveIconColor) { + return; + } + _inactiveIconColor = value; + notifyListeners(); + } + + Color get activePressedColor => _activePressedColor!; + Color? _activePressedColor; + set activePressedColor(Color? value) { + assert(value != null); + if (value == _activePressedColor) { + return; + } + _activePressedColor = value; + notifyListeners(); + } + + Color get inactivePressedColor => _inactivePressedColor!; + Color? _inactivePressedColor; + set inactivePressedColor(Color? value) { + assert(value != null); + if (value == _inactivePressedColor) { + return; + } + _inactivePressedColor = value; + notifyListeners(); + } + + double get activeThumbRadius => _activeThumbRadius!; + double? _activeThumbRadius; + set activeThumbRadius(double value) { + if (value == _activeThumbRadius) { + return; + } + _activeThumbRadius = value; + notifyListeners(); + } + + double get inactiveThumbRadius => _inactiveThumbRadius!; + double? _inactiveThumbRadius; + set inactiveThumbRadius(double value) { + if (value == _inactiveThumbRadius) { + return; + } + _inactiveThumbRadius = value; + notifyListeners(); + } + + double get pressedThumbRadius => _pressedThumbRadius!; + double? _pressedThumbRadius; + set pressedThumbRadius(double value) { + if (value == _pressedThumbRadius) { + return; + } + _pressedThumbRadius = value; + notifyListeners(); + } + + double? get thumbOffset => _thumbOffset; + double? _thumbOffset; + set thumbOffset(double? value) { + if (value == _thumbOffset) { + return; + } + _thumbOffset = value; + notifyListeners(); + } + + double get trackHeight => _trackHeight!; + double? _trackHeight; + set trackHeight(double value) { + if (value == _trackHeight) { + return; + } + _trackHeight = value; + notifyListeners(); + } + + double get trackWidth => _trackWidth!; + double? _trackWidth; + set trackWidth(double value) { + if (value == _trackWidth) { + return; + } + _trackWidth = value; + notifyListeners(); + } + + ImageProvider? get activeThumbImage => _activeThumbImage; + ImageProvider? _activeThumbImage; + set activeThumbImage(ImageProvider? value) { + if (value == _activeThumbImage) { + return; + } + _activeThumbImage = value; + notifyListeners(); + } + + ImageErrorListener? get onActiveThumbImageError => _onActiveThumbImageError; + ImageErrorListener? _onActiveThumbImageError; + set onActiveThumbImageError(ImageErrorListener? value) { + if (value == _onActiveThumbImageError) { + return; + } + _onActiveThumbImageError = value; + notifyListeners(); + } + + ImageProvider? get inactiveThumbImage => _inactiveThumbImage; + ImageProvider? _inactiveThumbImage; + set inactiveThumbImage(ImageProvider? value) { + if (value == _inactiveThumbImage) { + return; + } + _inactiveThumbImage = value; + notifyListeners(); + } + + ImageErrorListener? get onInactiveThumbImageError => _onInactiveThumbImageError; + ImageErrorListener? _onInactiveThumbImageError; + set onInactiveThumbImageError(ImageErrorListener? value) { + if (value == _onInactiveThumbImageError) { + return; + } + _onInactiveThumbImageError = value; + notifyListeners(); + } + + Color get activeTrackColor => _activeTrackColor!; + Color? _activeTrackColor; + set activeTrackColor(Color value) { + if (value == _activeTrackColor) { + return; + } + _activeTrackColor = value; + notifyListeners(); + } + + Color? get activeTrackOutlineColor => _activeTrackOutlineColor; + Color? _activeTrackOutlineColor; + set activeTrackOutlineColor(Color? value) { + if (value == _activeTrackOutlineColor) { + return; + } + _activeTrackOutlineColor = value; + notifyListeners(); + } + + Color? get inactiveTrackOutlineColor => _inactiveTrackOutlineColor; + Color? _inactiveTrackOutlineColor; + set inactiveTrackOutlineColor(Color? value) { + if (value == _inactiveTrackOutlineColor) { + return; + } + _inactiveTrackOutlineColor = value; + notifyListeners(); + } + + double? get activeTrackOutlineWidth => _activeTrackOutlineWidth; + double? _activeTrackOutlineWidth; + set activeTrackOutlineWidth(double? value) { + if (value == _activeTrackOutlineWidth) { + return; + } + _activeTrackOutlineWidth = value; + notifyListeners(); + } + + double? get inactiveTrackOutlineWidth => _inactiveTrackOutlineWidth; + double? _inactiveTrackOutlineWidth; + set inactiveTrackOutlineWidth(double? value) { + if (value == _inactiveTrackOutlineWidth) { + return; + } + _inactiveTrackOutlineWidth = value; + notifyListeners(); + } + + Color get inactiveTrackColor => _inactiveTrackColor!; + Color? _inactiveTrackColor; + set inactiveTrackColor(Color value) { + if (value == _inactiveTrackColor) { + return; + } + _inactiveTrackColor = value; + notifyListeners(); + } + + ImageConfiguration get configuration => _configuration!; + ImageConfiguration? _configuration; + set configuration(ImageConfiguration value) { + if (value == _configuration) { + return; + } + _configuration = value; + notifyListeners(); + } + + TextDirection get textDirection => _textDirection!; + TextDirection? _textDirection; + set textDirection(TextDirection value) { + if (_textDirection == value) { + return; + } + _textDirection = value; + notifyListeners(); + } + + Color get surfaceColor => _surfaceColor!; + Color? _surfaceColor; + set surfaceColor(Color value) { + if (value == _surfaceColor) { + return; + } + _surfaceColor = value; + notifyListeners(); + } + + bool get isInteractive => _isInteractive!; + bool? _isInteractive; + set isInteractive(bool value) { + if (value == _isInteractive) { + return; + } + _isInteractive = value; + notifyListeners(); + } + + double get trackInnerLength => _trackInnerLength!; + double? _trackInnerLength; + set trackInnerLength(double value) { + if (value == _trackInnerLength) { + return; + } + _trackInnerLength = value; + notifyListeners(); + } + + List? get thumbShadow => _thumbShadow; + List? _thumbShadow; + set thumbShadow(List? value) { + if (value == _thumbShadow) { + return; + } + _thumbShadow = value; + notifyListeners(); + } + + final TextPainter _textPainter = TextPainter(); + Color? _cachedThumbColor; + ImageProvider? _cachedThumbImage; + ImageErrorListener? _cachedThumbErrorListener; + BoxPainter? _cachedThumbPainter; + // Zeta change: add `_thumbSize`. + Size? _thumbSize; + + ShapeDecoration _createDefaultThumbDecoration(Color color, ImageProvider? image, ImageErrorListener? errorListener) { + return ShapeDecoration( + color: color, + image: image == null ? null : DecorationImage(image: image, onError: errorListener), + shape: const StadiumBorder(), + ); + } + + bool _isPainting = false; + + void _handleDecorationChanged() { + // If the image decoration is available synchronously, we'll get called here + // during paint. There's no reason to mark ourselves as needing paint if we + // are already in the middle of painting. (In fact, doing so would trigger + // an assert). + if (!_isPainting) { + notifyListeners(); + } + } + + bool _stopPressAnimation = false; + late double? _pressedThumbExtension; + + @override + void paint(Canvas canvas, Size size) { + final double currentValue = position.value; + + final double visualPosition; + switch (textDirection) { + case TextDirection.rtl: + visualPosition = 1.0 - currentValue; + case TextDirection.ltr: + visualPosition = currentValue; + } + if (reaction.status == AnimationStatus.reverse && !_stopPressAnimation) { + _stopPressAnimation = true; + } else { + _stopPressAnimation = false; + } + + if (!_stopPressAnimation) { + _pressedThumbExtension = 0; + } + + // Zeta change: `_thumbSize` override. + Size? thumbSize = _thumbSize ?? Size.fromRadius(pressedThumbRadius); + + // The thumb contracts slightly during the animation in Material 2. + final double inset = thumbOffset == null ? 0 : 1.0 - (currentValue - thumbOffset!).abs() * 2.0; + thumbSize = Size(thumbSize.width - inset, thumbSize.height - inset); + + final double colorValue = + CurvedAnimation(parent: positionController, curve: Curves.easeOut, reverseCurve: Curves.easeIn).value; + final Color trackColor = Color.lerp(inactiveTrackColor, activeTrackColor, colorValue)!; + final Color? trackOutlineColor = inactiveTrackOutlineColor == null || activeTrackOutlineColor == null + ? null + : Color.lerp(inactiveTrackOutlineColor, activeTrackOutlineColor, colorValue); + final double? trackOutlineWidth = lerpDouble(inactiveTrackOutlineWidth, activeTrackOutlineWidth, colorValue); + Color lerpedThumbColor; + if (!reaction.isDismissed) { + lerpedThumbColor = Color.lerp(inactivePressedColor, activePressedColor, colorValue)!; + } else if (positionController.status == AnimationStatus.forward) { + lerpedThumbColor = Color.lerp(inactivePressedColor, activeColor, colorValue)!; + } else if (positionController.status == AnimationStatus.reverse) { + lerpedThumbColor = Color.lerp(inactiveColor, activePressedColor, colorValue)!; + } else { + lerpedThumbColor = Color.lerp(inactiveColor, activeColor, colorValue)!; + } + + // Blend the thumb color against a `surfaceColor` background in case the + // thumbColor is not opaque. This way we do not see through the thumb to the + // track underneath. + final Color thumbColor = Color.alphaBlend(lerpedThumbColor, surfaceColor); + + final Icon? thumbIcon = currentValue < 0.5 ? inactiveIcon : activeIcon; + + final ImageProvider? thumbImage = currentValue < 0.5 ? inactiveThumbImage : activeThumbImage; + + final ImageErrorListener? thumbErrorListener = + currentValue < 0.5 ? onInactiveThumbImageError : onActiveThumbImageError; + + final Paint paint = Paint()..color = trackColor; + + final Offset trackPaintOffset = _computeTrackPaintOffset(size, trackWidth, trackHeight); + final Offset thumbPaintOffset = _computeThumbPaintOffset(trackPaintOffset, thumbSize, visualPosition); + final Offset radialReactionOrigin = Offset(thumbPaintOffset.dx + thumbSize.height / 2, size.height / 2); + + _paintTrackWith(canvas, paint, trackPaintOffset, trackOutlineColor, trackOutlineWidth); + paintRadialReaction(canvas: canvas, origin: radialReactionOrigin); + _paintThumbWith( + thumbPaintOffset, + canvas, + colorValue, + thumbColor, + thumbImage, + thumbErrorListener, + thumbIcon, + thumbSize, + inset, + ); + } + + /// Computes canvas offset for track's upper left corner + Offset _computeTrackPaintOffset(Size canvasSize, double trackWidth, double trackHeight) { + final double horizontalOffset = (canvasSize.width - trackWidth) / 2.0; + final double verticalOffset = (canvasSize.height - trackHeight) / 2.0; + + return Offset(horizontalOffset, verticalOffset); + } + + /// Computes canvas offset for thumb's upper left corner as if it were a + /// square + Offset _computeThumbPaintOffset(Offset trackPaintOffset, Size thumbSize, double visualPosition) { + // How much thumb radius extends beyond the track + final double trackRadius = trackHeight / 2; + final double additionalThumbRadius = thumbSize.height / 2 - trackRadius; + + final double horizontalProgress = visualPosition * (trackInnerLength - _pressedThumbExtension!); + final double thumbHorizontalOffset = + trackPaintOffset.dx + trackRadius + (_pressedThumbExtension! / 2) - thumbSize.width / 2 + horizontalProgress; + final double thumbVerticalOffset = trackPaintOffset.dy - additionalThumbRadius; + return Offset(thumbHorizontalOffset, thumbVerticalOffset); + } + + void _paintTrackWith( + Canvas canvas, + Paint paint, + Offset trackPaintOffset, + Color? trackOutlineColor, + double? trackOutlineWidth, + ) { + final Rect trackRect = Rect.fromLTWH( + trackPaintOffset.dx, + trackPaintOffset.dy, + trackWidth, + trackHeight, + ); + final double trackRadius = trackHeight / 2; + final RRect trackRRect = RRect.fromRectAndRadius( + trackRect, + Radius.circular(trackRadius), + ); + + canvas.drawRRect(trackRRect, paint); + + // paint track outline + if (trackOutlineColor != null) { + final Rect outlineTrackRect = Rect.fromLTWH( + trackPaintOffset.dx + 1, + trackPaintOffset.dy + 1, + trackWidth - 2, + trackHeight - 2, + ); + final RRect outlineTrackRRect = RRect.fromRectAndRadius( + outlineTrackRect, + Radius.circular(trackRadius), + ); + + final Paint outlinePaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = trackOutlineWidth ?? 2.0 + ..color = trackOutlineColor; + + canvas.drawRRect(outlineTrackRRect, outlinePaint); + } + } + + void _paintThumbWith( + Offset thumbPaintOffset, + Canvas canvas, + double currentValue, + Color thumbColor, + ImageProvider? thumbImage, + ImageErrorListener? thumbErrorListener, + Icon? thumbIcon, + Size thumbSize, + double inset, + ) { + try { + _isPainting = true; + if (_cachedThumbPainter == null || + thumbColor != _cachedThumbColor || + thumbImage != _cachedThumbImage || + thumbErrorListener != _cachedThumbErrorListener) { + _cachedThumbColor = thumbColor; + _cachedThumbImage = thumbImage; + _cachedThumbErrorListener = thumbErrorListener; + _cachedThumbPainter?.dispose(); + _cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage, thumbErrorListener) + .createBoxPainter(_handleDecorationChanged); + } + _cachedThumbPainter!.paint( + canvas, + thumbPaintOffset, + configuration.copyWith(size: thumbSize), + ); + + if (thumbIcon != null && thumbIcon.icon != null) { + final Color iconColor = Color.lerp(inactiveIconColor, activeIconColor, currentValue)!; + final double iconSize = thumbIcon.size ?? thumbSize.height; + final IconData iconData = thumbIcon.icon!; + final double? iconWeight = thumbIcon.weight ?? iconTheme?.weight; + final double? iconFill = thumbIcon.fill ?? iconTheme?.fill; + final double? iconGrade = thumbIcon.grade ?? iconTheme?.grade; + final double? iconOpticalSize = thumbIcon.opticalSize ?? iconTheme?.opticalSize; + final List? iconShadows = thumbIcon.shadows ?? iconTheme?.shadows; + + final TextSpan textSpan = TextSpan( + text: String.fromCharCode(iconData.codePoint), + style: TextStyle( + fontVariations: [ + if (iconFill != null) FontVariation('FILL', iconFill), + if (iconWeight != null) FontVariation('wght', iconWeight), + if (iconGrade != null) FontVariation('GRAD', iconGrade), + if (iconOpticalSize != null) FontVariation('opsz', iconOpticalSize), + ], + color: iconColor, + fontSize: iconSize, + inherit: false, + fontFamily: iconData.fontFamily, + package: iconData.fontPackage, + shadows: iconShadows, + ), + ); + _textPainter + ..textDirection = textDirection + ..text = textSpan + ..layout(); + final double additionalHorizontalOffset = (thumbSize.width - iconSize) / 2; + final double additionalVerticalOffset = (thumbSize.height - iconSize) / 2; + final Offset offset = thumbPaintOffset + Offset(additionalHorizontalOffset, additionalVerticalOffset); + + _textPainter.paint(canvas, offset); + } + } finally { + _isPainting = false; + } + } + + @override + void dispose() { + _textPainter.dispose(); + _cachedThumbPainter?.dispose(); + _cachedThumbPainter = null; + _cachedThumbColor = null; + _cachedThumbImage = null; + _cachedThumbErrorListener = null; + super.dispose(); + } +} + +mixin _SwitchConfig { + double get trackHeight; + double get trackWidth; + double get switchWidth; + double get switchHeight; + double get switchHeightCollapsed; + double get activeThumbRadius; + double get inactiveThumbRadius; + double get pressedThumbRadius; + double get thumbRadiusWithIcon; + List? get thumbShadow; + double? get thumbOffset; + int get toggleDuration; +} + +class _SwitchDefaultsM3 extends SwitchThemeData { + // Zeta change: Require the switch `size` to be passed + // in the constructor of [_SwitchDefaultsM3]. + _SwitchDefaultsM3(this.context, {required this.size}); + + final BuildContext context; + // Zeta change: Added parameter for the switch `size`. + final Size size; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + MaterialStateProperty get thumbColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + if (states.contains(MaterialState.selected)) { + return _colors.surface.withOpacity(1); + } + return _colors.onSurface.withOpacity(0.38); + } + if (states.contains(MaterialState.selected)) { + if (states.contains(MaterialState.pressed)) { + return _colors.primaryContainer; + } + if (states.contains(MaterialState.hovered)) { + return _colors.primaryContainer; + } + if (states.contains(MaterialState.focused)) { + return _colors.primaryContainer; + } + return _colors.onPrimary; + } + if (states.contains(MaterialState.pressed)) { + return _colors.onSurfaceVariant; + } + if (states.contains(MaterialState.hovered)) { + return _colors.onSurfaceVariant; + } + if (states.contains(MaterialState.focused)) { + return _colors.onSurfaceVariant; + } + return _colors.outline; + }); + } + + @override + MaterialStateProperty get trackColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + if (states.contains(MaterialState.selected)) { + return _colors.onSurface.withOpacity(0.12); + } + return _colors.surfaceVariant.withOpacity(0.12); + } + if (states.contains(MaterialState.selected)) { + if (states.contains(MaterialState.pressed)) { + return _colors.primary; + } + if (states.contains(MaterialState.hovered)) { + return _colors.primary; + } + if (states.contains(MaterialState.focused)) { + return _colors.primary; + } + return _colors.primary; + } + if (states.contains(MaterialState.pressed)) { + return _colors.surfaceVariant; + } + if (states.contains(MaterialState.hovered)) { + return _colors.surfaceVariant; + } + if (states.contains(MaterialState.focused)) { + return _colors.surfaceVariant; + } + return _colors.surfaceVariant; + }); + } + + @override + MaterialStateProperty get trackOutlineColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return Colors.transparent; + } + if (states.contains(MaterialState.disabled)) { + return _colors.onSurface.withOpacity(0.12); + } + return _colors.outline; + }); + } + + @override + MaterialStateProperty get overlayColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + if (states.contains(MaterialState.pressed)) { + return _colors.primary.withOpacity(0.12); + } + if (states.contains(MaterialState.hovered)) { + return _colors.primary.withOpacity(0.08); + } + if (states.contains(MaterialState.focused)) { + return _colors.primary.withOpacity(0.12); + } + return null; + } + if (states.contains(MaterialState.pressed)) { + return _colors.onSurface.withOpacity(0.12); + } + if (states.contains(MaterialState.hovered)) { + return _colors.onSurface.withOpacity(0.08); + } + if (states.contains(MaterialState.focused)) { + return _colors.onSurface.withOpacity(0.12); + } + return null; + }); + } + + @override + MaterialStateProperty get mouseCursor { + return MaterialStateProperty.resolveWith( + (Set states) => MaterialStateMouseCursor.clickable.resolve(states), + ); + } + + @override + MaterialStatePropertyAll get trackOutlineWidth => const MaterialStatePropertyAll(2); + + // Zeta change: `splashRadius` was fixed value in Flutter's [Switch], + // but not we use `size.height` for this. + @override + double get splashRadius => size.height / 2 + 8; +} + +class _SwitchConfigM3 with _SwitchConfig { + // Zeta change: Require the switch `size` to be passed + // in the constructor of [_SwitchConfigM3]. + _SwitchConfigM3({required this.size}); + + // Zeta change: Added parameter for the switch `size`. + final Size size; + + // Zeta change: `activeThumbRadius` was fixed value in Flutter's [Switch], + // but not we use `size.height` for this. + @override + double get activeThumbRadius => (size.height - 4) / 2; + + // Zeta change: `inactiveThumbRadius` was fixed value in Flutter's [Switch], + // but not we use `size.height` for this. + @override + double get inactiveThumbRadius => (size.height - 4) / 2; + + // Zeta change: `pressedThumbRadius` was fixed value in Flutter's [Switch], + // but not we use `size.height` for this. + @override + double get pressedThumbRadius => (size.height - 4) / 2; + + @override + double get switchHeight => _kSwitchMinSize + 8; + + @override + double get switchHeightCollapsed => _kSwitchMinSize; + + @override + double get switchWidth => trackWidth - 2 * (trackHeight / 2) + _kSwitchMinSize; + + @override + double get thumbRadiusWithIcon => (size.height - 4) / 2; + + @override + List? get thumbShadow => kElevationToShadow[0]; + + // Zeta change: `trackHeight` was fixed value in Flutter's [Switch], + // but not we use `size.height` for this. + @override + double get trackHeight => size.height; + + // Zeta change: `trackWidth` was fixed value in Flutter's [Switch], + // but not we use `size.width` for this. + @override + double get trackWidth => size.width; + + // Hand coded default based on the animation specs. + @override + int get toggleDuration => 300; + + // Hand coded default based on the animation specs. + @override + double? get thumbOffset => null; +} diff --git a/lib/src/components/switch/zeta_switch.dart b/lib/src/components/switch/zeta_switch.dart new file mode 100644 index 00000000..0dd121ae --- /dev/null +++ b/lib/src/components/switch/zeta_switch.dart @@ -0,0 +1,101 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; +import 'material_switch.dart'; + +const _sizeAndroid = Size(48, 24); +const _sizeIOS = Size(56, 32); +const _sizeWeb = Size(64, 32); + +/// Variants of [ZetaSwitch]. +enum ZetaSwitchType { + /// 64 x 32 + web, + + /// 48 x 24 + android, + + /// 56 x 32 + ios, +} + +/// Zeta Switch. +/// +/// Switch can turn an option on or off. +/// +/// Switch has styles for Android, iOS and Web. +// TODO(switch): Add web icon support. +class ZetaSwitch extends StatelessWidget { + /// Constructor for [ZetaSwitch]. + const ZetaSwitch({ + super.key, + this.value = false, + this.onChanged, + this.variant, + }); + + /// Determines whether the switch is on or off. + final bool? value; + + /// Called when the value of the switch should change. + final ValueChanged? onChanged; + + /// Variant of switch for different platforms. + /// + /// Defaults to match the platform, or falls back to web. + final ZetaSwitchType? variant; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(FlagProperty('value', value: value, ifTrue: 'on', ifFalse: 'off', showName: true)) + ..add(ObjectFlagProperty>('onChanged', onChanged, ifNull: 'disabled')) + ..add(EnumProperty('variant', variant)); + } + + ZetaSwitchType get _variant { + if (variant != null) return variant!; + if (kIsWeb) return ZetaSwitchType.web; + return switch (Platform.operatingSystem) { + 'ios' => ZetaSwitchType.ios, + 'android' => ZetaSwitchType.android, + _ => ZetaSwitchType.web, + }; + } + + Size get _size { + return switch (_variant) { + ZetaSwitchType.ios => _sizeIOS, + ZetaSwitchType.android => _sizeAndroid, + _ => _sizeWeb, + }; + } + + @override + Widget build(BuildContext context) { + final zetaColors = Zeta.of(context).colors; + + return MaterialSwitch( + size: _size, + trackOutlineWidth: const MaterialStatePropertyAll(0), + trackOutlineColor: const MaterialStatePropertyAll(Colors.transparent), + trackColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return zetaColors.cool.shade30; + } else { + return states.contains(MaterialState.selected) ? zetaColors.primary : zetaColors.cool.shade50; + } + }), + thumbColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.disabled) ? zetaColors.cool.shade50 : zetaColors.cool.shade20, + ), + value: value ?? false, + onChanged: onChanged, + thumbSize: _variant == ZetaSwitchType.web ? const Size.square(ZetaSpacing.m) : null, + ); + } +} diff --git a/lib/src/components/tabs/tab.dart b/lib/src/components/tabs/tab.dart new file mode 100644 index 00000000..aaa77635 --- /dev/null +++ b/lib/src/components/tabs/tab.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import '../../../zeta_flutter.dart'; + +/// Defines how the bounds of the selected tab indicator are computed. Intended to be used with [ZetaTabBar]. +class ZetaTab extends Tab { + /// Creates a Zeta Design tab bar. + ZetaTab({ + Widget? icon, + String? text, + super.key, + }) : super( + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + const SizedBox(width: ZetaSpacing.s), + icon, + ], + if (text != null) + Padding( + padding: icon != null ? const EdgeInsets.only(left: ZetaSpacing.x2) : EdgeInsets.zero, + child: Text(text), + ), + if (icon != null) const SizedBox(width: ZetaSpacing.s), + ], + ), + ); +} diff --git a/lib/src/components/tabs/tab_bar.dart b/lib/src/components/tabs/tab_bar.dart new file mode 100644 index 00000000..bfbf2bf9 --- /dev/null +++ b/lib/src/components/tabs/tab_bar.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import '../../../zeta_flutter.dart'; + +/// A Zeta Design primary tab bar. +class ZetaTabBar extends TabBar { + /// Creates a Zeta Design primary tab bar. + ZetaTabBar({ + required BuildContext context, + required List super.tabs, + TabAlignment super.tabAlignment = TabAlignment.center, + bool enabled = true, + super.isScrollable, + super.enableFeedback, + super.dragStartBehavior, + super.padding, + super.onTap, + super.key, + }) : super( + indicatorSize: isScrollable ? TabBarIndicatorSize.label : TabBarIndicatorSize.tab, + labelPadding: isScrollable ? null : EdgeInsets.zero, + indicator: UnderlineTabIndicator( + borderSide: BorderSide( + color: Zeta.of(context).colors.primary, + width: enabled ? 4 : 0, + ), + borderRadius: ZetaRadius.none, + ), + labelStyle: ZetaTextStyles.labelLarge.copyWith( + color: enabled ? Zeta.of(context).colors.textDefault : Zeta.of(context).colors.textDisabled, + ), + unselectedLabelStyle: ZetaTextStyles.labelLarge.copyWith( + color: enabled ? Zeta.of(context).colors.textSubtle : Zeta.of(context).colors.textDisabled, + ), + ); +} diff --git a/lib/src/theme/typography.dart b/lib/src/theme/typography.dart index c5515f42..f9e21f8c 100644 --- a/lib/src/theme/typography.dart +++ b/lib/src/theme/typography.dart @@ -14,17 +14,32 @@ class ZetaTextStyles { /// As the largest text on the screen, display styles are reserved for short, /// important text or numerals. They work best on large screens. /// {@endtemplate} - static const TextStyle displayLarge = TextStyle(fontSize: 52, fontWeight: FontWeight.w300, height: 64 / 52); + static const TextStyle displayLarge = TextStyle( + fontSize: 52, + fontWeight: FontWeight.w300, + height: 60 / 52, + fontFamily: kZetaFontFamily, + ); /// Middle size of the display styles. /// /// {@macro zeta-text-display} - static const TextStyle displayMedium = TextStyle(fontSize: 42, fontWeight: FontWeight.w300, height: 56 / 42); + static const TextStyle displayMedium = TextStyle( + fontSize: 44, + fontWeight: FontWeight.w300, + height: 52 / 44, + fontFamily: kZetaFontFamily, + ); /// Smallest of the display styles. /// /// {@macro zeta-text-display} - static const TextStyle displaySmall = TextStyle(fontSize: 36, fontWeight: FontWeight.w300, height: 48 / 36); + static const TextStyle displaySmall = TextStyle( + fontSize: 36, + fontWeight: FontWeight.w300, + height: 40 / 36, + fontFamily: kZetaFontFamily, + ); /// Largest of the headline styles. /// @@ -32,17 +47,32 @@ class ZetaTextStyles { /// Headline styles are smaller than display styles. They're best-suited for /// short, high-emphasis text on smaller screens. /// {@endtemplate} - static const TextStyle heading1 = TextStyle(fontSize: 32, fontWeight: FontWeight.w500, height: 36 / 32); + static const TextStyle heading1 = TextStyle( + fontSize: 32, + fontWeight: FontWeight.w500, + height: 36 / 32, + fontFamily: kZetaFontFamily, + ); /// Middle size of the headline styles. /// /// {@macro zeta-text-headline} - static const TextStyle heading2 = TextStyle(fontSize: 28, fontWeight: FontWeight.w500, height: 32 / 28); + static const TextStyle heading2 = TextStyle( + fontSize: 28, + fontWeight: FontWeight.w500, + height: 32 / 28, + fontFamily: kZetaFontFamily, + ); /// Smallest of the headline styles. /// /// {@macro zeta-text-headline} - static const TextStyle heading3 = TextStyle(fontSize: 24, fontWeight: FontWeight.w500, height: 32 / 24); + static const TextStyle heading3 = TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + height: 28 / 24, + fontFamily: kZetaFontFamily, + ); /// Largest of the title styles. /// @@ -50,36 +80,76 @@ class ZetaTextStyles { /// Titles are smaller than headline styles and should be used for shorter, /// medium-emphasis text. /// {@endtemplate} - static const TextStyle titleLarge = TextStyle(fontSize: 32, fontWeight: FontWeight.w500, height: 20 / 28); + static const TextStyle titleLarge = TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + height: 24 / 20, + fontFamily: kZetaFontFamily, + ); /// Middle size of the title styles. /// /// {@macro zeta-text-title} - static const TextStyle titleMedium = TextStyle(fontSize: 18, fontWeight: FontWeight.w500, height: 24 / 18); + static const TextStyle titleMedium = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + height: 20 / 16, + fontFamily: kZetaFontFamily, + ); /// Smallest of the title styles. /// /// {@macro zeta-text-title} - static const TextStyle titleSmall = TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 24 / 16); + static const TextStyle titleSmall = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + height: 16 / 12, + fontFamily: kZetaFontFamily, + ); /// Largest of the body styles. /// /// {@template zeta-text-body} /// Body styles are used for longer passages of text. /// {@endtemplate} - static const TextStyle bodyLarge = TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 16 / 24); + static const TextStyle bodyLarge = TextStyle( + fontSize: 20, + fontWeight: FontWeight.w400, + height: 24 / 20, + fontFamily: kZetaFontFamily, + ); /// Middle size of the body styles. /// /// {@macro zeta-text-body} /// /// The default Text style for [Zeta]. - static const TextStyle bodyMedium = TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 20 / 14); + static const TextStyle bodyMedium = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + height: 24 / 16, + fontFamily: kZetaFontFamily, + ); + + /// Small size of the body styles. + /// + /// {@macro zeta-text-body} + static const TextStyle bodySmall = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + height: 18 / 14, + fontFamily: kZetaFontFamily, + ); /// Smallest of the body styles. /// /// {@macro zeta-text-body} - static const TextStyle bodySmall = TextStyle(fontSize: 12, fontWeight: FontWeight.w400, height: 16 / 12); + static const TextStyle bodyXSmall = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + height: 16 / 12, + fontFamily: kZetaFontFamily, + ); /// Largest of the label styles. /// @@ -90,26 +160,72 @@ class ZetaTextStyles { /// {@endtemplate} /// /// Used for text on [ZetaButton]. - static const TextStyle labelLarge = TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 24 / 16); + static const TextStyle labelLarge = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + height: 24 / 16, + fontFamily: kZetaFontFamily, + ); /// Middle size of the label styles. /// /// {@macro zeta-text-label} - static const TextStyle labelMedium = TextStyle(fontSize: 14, fontWeight: FontWeight.w500, height: 20 / 14); + static const TextStyle labelMedium = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + height: 20 / 14, + fontFamily: kZetaFontFamily, + ); /// Small size of the label styles. /// /// {@macro zeta-text-label} - static const TextStyle labelSmall = TextStyle(fontSize: 12, fontWeight: FontWeight.w500, height: 16 / 12); + static const TextStyle labelSmall = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + height: 16 / 12, + fontFamily: kZetaFontFamily, + ); /// Label text style used specifically for Indicator. + /// /// {@macro zeta-text-label} - static const TextStyle labelIndicator = TextStyle(fontSize: 12, fontWeight: FontWeight.w500, height: 14 / 12); + static const TextStyle labelIndicator = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + height: 14 / 12, + fontFamily: kZetaFontFamily, + ); - /// Smallest of the label styles. + /// Largest heading style. /// - /// {@macro zeta-text-label} - static const TextStyle labelTiny = TextStyle(fontSize: 11, fontWeight: FontWeight.w500, height: 14 / 11); + /// {@macro zeta-text-headline} + static const TextStyle h1 = heading1; + + /// Second largest heading style. + /// + /// {@macro zeta-text-headline} + static const TextStyle h2 = heading2; + + /// Third largest heading style. + /// + /// {@macro zeta-text-headline} + static const TextStyle h3 = heading3; + + /// Fourth largest heading style. + /// + /// {@macro zeta-text-headline} + static const TextStyle h4 = titleLarge; + + /// Fifth largest heading style. + /// + /// {@macro zeta-text-headline} + static const TextStyle h5 = titleMedium; + + /// Sixth largest heading style. + /// + /// {@macro zeta-text-headline} + static const TextStyle h6 = titleSmall; } /// [ZetaTextStyles] combined into a [TextTheme]. diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index 75d941d8..468ce203 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -22,11 +22,20 @@ export 'src/components/buttons/fab.dart'; export 'src/components/buttons/icon_button.dart'; 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/dropdown/dropdown.dart'; +export 'src/components/list_item/list_item.dart'; export 'src/components/navigation bar/navigation_bar.dart'; export 'src/components/password/password_input.dart'; export 'src/components/progress/progress_bar.dart'; export 'src/components/progress/progress_circle.dart'; +export 'src/components/radio/radio.dart'; +export 'src/components/snack_bar/snack_bar.dart'; +export 'src/components/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/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 7f88b8e6..302b2225 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,8 @@ environment: dependencies: flutter: sdk: flutter + intl: ^0.19.0 + mask_text_input_formatter: ^2.9.0 dev_dependencies: zds_analysis: ^1.0.0