diff --git a/catalyst_voices/lib/widgets/indicators/voices_circular_progress_indicator.dart b/catalyst_voices/lib/widgets/indicators/voices_circular_progress_indicator.dart new file mode 100644 index 0000000000..c3c67e746a --- /dev/null +++ b/catalyst_voices/lib/widgets/indicators/voices_circular_progress_indicator.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +/// A Voices circular progress indicator with optional track visibility. +/// +/// This widget provides a circular progress indicator with customization +/// options for the progress value and track visibility. +class VoicesCircularProgressIndicator extends StatelessWidget { + /// The current progress value, from 0.0 to 1.0. If null, + /// the indicator will be indeterminate. + final double? value; + + /// Whether to show the progress indicator's track. + final bool showTrack; + + /// Creates a [VoicesCircularProgressIndicator] widget. + const VoicesCircularProgressIndicator({ + super.key, + this.value, + this.showTrack = true, + }); + + @override + Widget build(BuildContext context) { + return CircularProgressIndicator( + value: value, + strokeCap: StrokeCap.round, + backgroundColor: showTrack ? null : Colors.transparent, + ); + } +} diff --git a/catalyst_voices/lib/widgets/indicators/voices_linear_progress_indicator.dart b/catalyst_voices/lib/widgets/indicators/voices_linear_progress_indicator.dart new file mode 100644 index 0000000000..5c16f5cd00 --- /dev/null +++ b/catalyst_voices/lib/widgets/indicators/voices_linear_progress_indicator.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +/// A custom linear progress indicator with optional track visibility and +/// rounded corners. +/// +/// This widget provides a linear progress indicator with customization +/// options for the progress value, track visibility, and rounded corners. +class VoicesLinearProgressIndicator extends StatelessWidget { + /// The current progress value, from 0.0 to 1.0. If null, the indicator will + /// be indeterminate. + final double? value; + + /// Whether to show the progress indicator's track. + final bool showTrack; + + /// Creates a [VoicesLinearProgressIndicator] widget. + const VoicesLinearProgressIndicator({ + super.key, + this.value, + this.showTrack = true, + }); + + @override + Widget build(BuildContext context) { + return LinearProgressIndicator( + value: value, + borderRadius: BorderRadius.circular(4), + backgroundColor: showTrack ? null : Colors.transparent, + ); + } +} diff --git a/catalyst_voices/lib/widgets/separators/voices_divider.dart b/catalyst_voices/lib/widgets/separators/voices_divider.dart new file mode 100644 index 0000000000..040e0ef81b --- /dev/null +++ b/catalyst_voices/lib/widgets/separators/voices_divider.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +const _kDefaultIntent = 24.0; + +/// The [VoicesDivider] widget is a simple wrapper around Material's [Divider] +/// widget that provides additional customization options for indentation. +/// +/// It's primarily used to create horizontal dividers within the context +/// of a list or other layout with specific indentation requirements. +class VoicesDivider extends StatelessWidget { + /// A double value representing the indentation of the divider from the + /// start of the parent container. + /// + /// Defaults to [_kDefaultIntent] (24.0). + final double indent; + + /// A double value representing the indentation of the divider from the + /// end of the parent container. + /// + /// Defaults to [_kDefaultIntent] (24.0). + final double endIntent; + + const VoicesDivider({ + super.key, + this.indent = _kDefaultIntent, + this.endIntent = _kDefaultIntent, + }); + + @override + Widget build(BuildContext context) { + return Divider( + indent: indent, + endIndent: endIntent, + ); + } +} diff --git a/catalyst_voices/lib/widgets/separators/voices_text_divider.dart b/catalyst_voices/lib/widgets/separators/voices_text_divider.dart new file mode 100644 index 0000000000..c9e90b0d1c --- /dev/null +++ b/catalyst_voices/lib/widgets/separators/voices_text_divider.dart @@ -0,0 +1,60 @@ +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; + +/// A divider with text placed in the middle. +/// +/// This widget is a variation of [Divider] that displays a text child centered +/// between two divider lines. It provides customization options for indent, +/// gap, and overall size. +/// +/// Example usage: +/// +/// ```dart +/// VoicesTextDivider( +/// child: Text('My Name'), +/// ), +/// ``` +class VoicesTextDivider extends StatelessWidget { + /// The indentation of the divider lines from the start and end of the row. + final double indent; + + /// The gap between the divider lines and the text child. + final double nameGap; + + /// The size of the row containing the divider lines and text child. + final MainAxisSize mainAxisSize; + + /// The text to display in the middle of the divider. + final Widget child; + + const VoicesTextDivider({ + super.key, + this.indent = 24, + this.nameGap = 8, + this.mainAxisSize = MainAxisSize.min, + required this.child, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return DividerTheme( + data: const DividerThemeData(space: 40), + child: Row( + mainAxisSize: mainAxisSize, + children: [ + Expanded(child: Divider(indent: indent)), + SizedBox(width: nameGap), + DefaultTextStyle( + style: (theme.textTheme.bodyLarge ?? const TextStyle()) + .copyWith(color: theme.colors.textOnPrimary), + child: child, + ), + SizedBox(width: nameGap), + Expanded(child: Divider(endIndent: indent)), + ], + ), + ); + } +} diff --git a/catalyst_voices/lib/widgets/separators/voices_vertical_divider.dart b/catalyst_voices/lib/widgets/separators/voices_vertical_divider.dart new file mode 100644 index 0000000000..bf5a9dd7ea --- /dev/null +++ b/catalyst_voices/lib/widgets/separators/voices_vertical_divider.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +/// A vertical divider that ensures consistent color based on [DividerTheme]. +/// +/// This widget wraps [VerticalDivider] and explicitly sets its `color` +/// property to match the current [DividerTheme]. This is necessary because M3 +/// overrides the default color behavior. +/// +/// You can customize the divider's appearance using the [indent] and +/// [endIndent] properties. +class VoicesVerticalDivider extends StatelessWidget { + /// The indentation of the divider from the start of the column. + /// + /// See [VerticalDivider.indent] for more details. + final double? indent; + + /// The indentation of the divider from the end of the column. + /// + /// See [VerticalDivider.endIndent] for more details. + final double? endIndent; + + const VoicesVerticalDivider({ + super.key, + this.indent, + this.endIndent, + }); + + @override + Widget build(BuildContext context) { + return VerticalDivider( + indent: indent, + endIndent: endIndent, + // M3 will override it and use outline color that's why setting + // it explicitly. + color: DividerTheme.of(context).color, + ); + } +} diff --git a/catalyst_voices/lib/widgets/toggles/voices_switch.dart b/catalyst_voices/lib/widgets/toggles/voices_switch.dart new file mode 100644 index 0000000000..0780d3cec9 --- /dev/null +++ b/catalyst_voices/lib/widgets/toggles/voices_switch.dart @@ -0,0 +1,47 @@ +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; + +/// A Voices version of a [Switch] widget with optional thumb icon +/// customization. +/// +/// This widget provides a basic switch functionality with the ability to +/// display a custom icon on the thumb. +class VoicesSwitch extends StatelessWidget { + /// The current state of the switch. + final bool value; + + /// An optional icon to display on the switch thumb. + final IconData? thumbIcon; + + /// A callback function triggered when the switch value changes. + final ValueChanged? onChanged; + + /// Creates a [VoicesSwitch] widget. + /// + /// The [value] argument is required and specifies the initial state + /// of the switch. + const VoicesSwitch({ + super.key, + required this.value, + this.thumbIcon, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final thumbIcon = this.thumbIcon; + + return Switch( + value: value, + onChanged: onChanged, + thumbIcon: thumbIcon != null + ? WidgetStatePropertyAll( + Icon( + thumbIcon, + color: Theme.of(context).colors.iconsForeground, + ), + ) + : null, + ); + } +} diff --git a/catalyst_voices/lib/widgets/widgets.dart b/catalyst_voices/lib/widgets/widgets.dart index 30c4c8cc5e..d508e93684 100644 --- a/catalyst_voices/lib/widgets/widgets.dart +++ b/catalyst_voices/lib/widgets/widgets.dart @@ -3,9 +3,15 @@ export 'buttons/voices_icon_button.dart'; export 'buttons/voices_outlined_button.dart'; export 'buttons/voices_segmented_button.dart'; export 'buttons/voices_text_button.dart'; +export 'indicators/voices_circular_progress_indicator.dart'; +export 'indicators/voices_linear_progress_indicator.dart'; +export 'separators/voices_divider.dart'; +export 'separators/voices_text_divider.dart'; +export 'separators/voices_vertical_divider.dart'; export 'text_field/voices_email_text_field.dart'; export 'text_field/voices_password_text_field.dart'; export 'text_field/voices_text_field.dart'; export 'toggles/voices_checkbox.dart'; export 'toggles/voices_checkbox_group.dart'; export 'toggles/voices_radio.dart'; +export 'toggles/voices_switch.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/catalyst.dart b/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/catalyst.dart index 6c98debc64..af148ce555 100644 --- a/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/catalyst.dart +++ b/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/catalyst.dart @@ -296,6 +296,14 @@ ThemeData _buildThemeData( ), dividerTheme: DividerThemeData( color: colorScheme.outlineVariant, + space: 16, + thickness: 1, + ), + progressIndicatorTheme: ProgressIndicatorThemeData( + color: colorScheme.primary, + linearTrackColor: colorScheme.secondaryContainer, + circularTrackColor: colorScheme.secondaryContainer, + refreshBackgroundColor: colorScheme.secondaryContainer, ), textTheme: textTheme, colorScheme: colorScheme, diff --git a/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/widgets/toggles_theme.dart b/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/widgets/toggles_theme.dart index 640f3315ef..4771f6710b 100644 --- a/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/widgets/toggles_theme.dart +++ b/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/widgets/toggles_theme.dart @@ -106,6 +106,63 @@ extension TogglesTheme on ThemeData { materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: const VisualDensity(horizontal: -4, vertical: -4), ), + // Out of the box configuration is correct + switchTheme: SwitchThemeData( + trackColor: WidgetStateProperty.resolveWith( + (states) { + if (states.contains(WidgetState.selected) && + !states.contains(WidgetState.disabled)) { + return colorScheme.primary; + } + + return colors.outlineBorderVariant?.withOpacity(0.38); + }, + ), + trackOutlineColor: WidgetStateProperty.resolveWith( + (states) { + if (states.contains(WidgetState.selected) && + !states.contains(WidgetState.disabled)) { + return colorScheme.primary; + } + + return colors.outlineBorder; + }, + ), + trackOutlineWidth: WidgetStateProperty.resolveWith( + (states) { + if (states.contains(WidgetState.selected)) { + return states.contains(WidgetState.disabled) ? 1.0 : null; + } + + return 2.0; + }, + ), + thumbColor: WidgetStateProperty.resolveWith( + (states) { + // Selected states + if (states.contains(WidgetState.selected)) { + if (states.contains(WidgetState.disabled)) { + return colorScheme.surface; + } + if (states.contains(WidgetState.pressed) || + states.contains(WidgetState.hovered)) { + return colorScheme.primaryContainer; + } + + return colorScheme.onPrimary; + } + + // Not disabled and pressed or hovered + if (!states.contains(WidgetState.disabled) && + (states.contains(WidgetState.pressed) || + states.contains(WidgetState.hovered))) { + return colors.iconsForeground; + } + + return colors.outlineBorder; + }, + ), + ), ); } } diff --git a/catalyst_voices/uikit_example/lib/examples/voices_indicators_example.dart b/catalyst_voices/uikit_example/lib/examples/voices_indicators_example.dart new file mode 100644 index 0000000000..c67231b124 --- /dev/null +++ b/catalyst_voices/uikit_example/lib/examples/voices_indicators_example.dart @@ -0,0 +1,57 @@ +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:flutter/material.dart'; + +class VoicesIndicatorsExample extends StatelessWidget { + static const String route = '/indicators-example'; + + const VoicesIndicatorsExample({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Voices Indicators')), + body: const Padding( + padding: EdgeInsets.symmetric(horizontal: 42, vertical: 24), + child: Column( + children: [ + Text('Linear - Indeterminate'), + SizedBox(height: 8), + VoicesLinearProgressIndicator(), + SizedBox(height: 16), + VoicesLinearProgressIndicator(showTrack: false), + SizedBox(height: 22), + Text('Linear - Fixed'), + SizedBox(height: 8), + VoicesLinearProgressIndicator(value: 0.25), + SizedBox(height: 16), + VoicesLinearProgressIndicator(value: 0.25, showTrack: false), + SizedBox(height: 22), + Text('Circular - Indeterminate'), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + VoicesCircularProgressIndicator(), + SizedBox(width: 16), + VoicesCircularProgressIndicator(showTrack: false), + ], + ), + SizedBox(height: 22), + Text('Circular - Fixed'), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + VoicesCircularProgressIndicator(value: 0.75), + SizedBox(width: 16), + VoicesCircularProgressIndicator(value: 0.75, showTrack: false), + ], + ), + ], + ), + ), + ); + } +} diff --git a/catalyst_voices/uikit_example/lib/examples/voices_separators_example.dart b/catalyst_voices/uikit_example/lib/examples/voices_separators_example.dart new file mode 100644 index 0000000000..68bb94d7c2 --- /dev/null +++ b/catalyst_voices/uikit_example/lib/examples/voices_separators_example.dart @@ -0,0 +1,69 @@ +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:flutter/material.dart'; + +class VoicesSeparatorsExample extends StatelessWidget { + static const String route = '/separators-example'; + + const VoicesSeparatorsExample({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Voices Switch')), + body: Column( + children: [ + ColoredBox( + color: Theme.of(context).colorScheme.primaryContainer, + child: const Padding( + padding: EdgeInsets.all(32), + child: Text('Paragraph'), + ), + ), + const VoicesDivider(), + ColoredBox( + color: Theme.of(context).colorScheme.primaryContainer, + child: const Padding( + padding: EdgeInsets.all(32), + child: Text('Paragraph'), + ), + ), + const VoicesTextDivider( + child: Text('Your account creation progress'), + ), + ColoredBox( + color: Theme.of(context).colorScheme.primaryContainer, + child: const Padding( + padding: EdgeInsets.all(32), + child: Text('Paragraph'), + ), + ), + const SizedBox(height: 28), + SizedBox( + height: 48, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + constraints: BoxConstraints.tight(const Size.square(48)), + color: Theme.of(context).colorScheme.primaryContainer, + ), + const VoicesVerticalDivider(), + Container( + constraints: BoxConstraints.tight(const Size.square(48)), + color: Theme.of(context).colorScheme.primaryContainer, + ), + const VoicesVerticalDivider(), + Container( + constraints: BoxConstraints.tight(const Size.square(48)), + color: Theme.of(context).colorScheme.primaryContainer, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/catalyst_voices/uikit_example/lib/examples/voices_switch_example.dart b/catalyst_voices/uikit_example/lib/examples/voices_switch_example.dart new file mode 100644 index 0000000000..b1405b6ed6 --- /dev/null +++ b/catalyst_voices/uikit_example/lib/examples/voices_switch_example.dart @@ -0,0 +1,56 @@ +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:flutter/material.dart'; + +class VoicesSwitchExample extends StatefulWidget { + static const String route = '/switch-example'; + + const VoicesSwitchExample({ + super.key, + }); + + @override + State createState() => _VoicesSwitchExampleState(); +} + +class _VoicesSwitchExampleState extends State { + final _switchStateMap = {}; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Voices Switch')), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + children: [ + VoicesSwitch( + value: _switchStateMap[0] ?? true, + onChanged: (value) { + setState(() { + _switchStateMap[0] = value; + }); + }, + ), + const SizedBox(height: 8), + VoicesSwitch( + value: _switchStateMap[1] ?? true, + thumbIcon: Icons.check, + onChanged: (value) { + setState(() { + _switchStateMap[1] = value; + }); + }, + ), + const SizedBox(height: 8), + const VoicesSwitch(value: false), + const SizedBox(height: 8), + const VoicesSwitch( + value: false, + thumbIcon: Icons.close, + ), + ], + ), + ), + ); + } +} diff --git a/catalyst_voices/uikit_example/lib/examples_list.dart b/catalyst_voices/uikit_example/lib/examples_list.dart index 8a4d33c49c..b178c1d700 100644 --- a/catalyst_voices/uikit_example/lib/examples_list.dart +++ b/catalyst_voices/uikit_example/lib/examples_list.dart @@ -5,10 +5,13 @@ import 'package:flutter/material.dart'; import 'package:uikit_example/examples/voices_buttons_example.dart'; import 'package:uikit_example/examples/voices_checkbox_example.dart'; import 'package:uikit_example/examples/voices_chip_example.dart'; +import 'package:uikit_example/examples/voices_indicators_example.dart'; import 'package:uikit_example/examples/voices_navigation_example.dart'; import 'package:uikit_example/examples/voices_radio_example.dart'; import 'package:uikit_example/examples/voices_segmented_button_example.dart'; +import 'package:uikit_example/examples/voices_separators_example.dart'; import 'package:uikit_example/examples/voices_snackbar_example.dart'; +import 'package:uikit_example/examples/voices_switch_example.dart'; class ExamplesListPage extends StatelessWidget { static List get examples { @@ -48,6 +51,21 @@ class ExamplesListPage extends StatelessWidget { route: VoicesCheckboxExample.route, page: VoicesCheckboxExample(), ), + ExampleTile( + title: 'Voices Switch', + route: VoicesSwitchExample.route, + page: VoicesSwitchExample(), + ), + ExampleTile( + title: 'Voices Separators', + route: VoicesSeparatorsExample.route, + page: VoicesSeparatorsExample(), + ), + ExampleTile( + title: 'Voices Indicators', + route: VoicesIndicatorsExample.route, + page: VoicesIndicatorsExample(), + ), ]; }