From c2c24957aabf6d01ddeb7a47391e4f46817adda2 Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Thu, 8 Aug 2024 13:03:07 +0200 Subject: [PATCH 01/16] feat: [DX-1203] Add OptimusSelectionCard component --- optimus/lib/optimus.dart | 1 + optimus/lib/src/selection_card.dart | 25 +++++++++++++++++++++++ storybook/lib/stories/selection_card.dart | 7 +++++++ 3 files changed, 33 insertions(+) create mode 100644 optimus/lib/src/selection_card.dart create mode 100644 storybook/lib/stories/selection_card.dart diff --git a/optimus/lib/optimus.dart b/optimus/lib/optimus.dart index 33e1d4bb..bc48f872 100644 --- a/optimus/lib/optimus.dart +++ b/optimus/lib/optimus.dart @@ -80,6 +80,7 @@ export 'src/search/search_field.dart'; export 'src/segmented_control/segmented_control.dart'; export 'src/select.dart'; export 'src/select_input.dart'; +export 'src/selection_card.dart'; export 'src/slidable/slidable.dart'; export 'src/slidable/slide_action.dart'; export 'src/stack.dart'; diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart new file mode 100644 index 00000000..a25f9c5f --- /dev/null +++ b/optimus/lib/src/selection_card.dart @@ -0,0 +1,25 @@ +import 'package:flutter/widgets.dart'; + +enum OptimusSelectionCardVariant { vertical, horizontal } + +enum OptimusSelectionCardBorderRadius { small, medium } + +class OptimusSelectionCard extends StatelessWidget { + const OptimusSelectionCard({ + super.key, + required this.title, + this.description, + this.trailing, + this.isSelected = false, + this.isEnabled = true, + }); + + final String title; + final String? description; + final Widget? trailing; + final bool isSelected; + final bool isEnabled; + + @override + Widget build(BuildContext context) => Container(); +} diff --git a/storybook/lib/stories/selection_card.dart b/storybook/lib/stories/selection_card.dart new file mode 100644 index 00000000..89524388 --- /dev/null +++ b/storybook/lib/stories/selection_card.dart @@ -0,0 +1,7 @@ +import 'package:flutter/widgets.dart'; +import 'package:storybook_flutter/storybook_flutter.dart'; + +final selectionCardStory = Story( + name: 'Forms/SelectionCard', + builder: (context) => const Center(), +); From f5919794ea34e1547ad8e9cebdc854949bba37e6 Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Fri, 9 Aug 2024 13:15:53 +0200 Subject: [PATCH 02/16] upd --- optimus/lib/src/radio/circle.dart | 43 ++++++++++++++++ optimus/lib/src/radio/radio.dart | 80 +++-------------------------- optimus/lib/src/radio/state.dart | 30 +++++++++++ optimus/lib/src/selection_card.dart | 34 +++++++++++- 4 files changed, 113 insertions(+), 74 deletions(-) create mode 100644 optimus/lib/src/radio/circle.dart create mode 100644 optimus/lib/src/radio/state.dart diff --git a/optimus/lib/src/radio/circle.dart b/optimus/lib/src/radio/circle.dart new file mode 100644 index 00000000..f6ee8757 --- /dev/null +++ b/optimus/lib/src/radio/circle.dart @@ -0,0 +1,43 @@ +import 'package:flutter/widgets.dart'; +import 'package:optimus/src/radio/state.dart'; +import 'package:optimus/src/theme/theme.dart'; + +class RadioCircle extends StatelessWidget { + const RadioCircle({ + super.key, + required this.state, + required this.isSelected, + }); + + final RadioState state; + final bool isSelected; + + @override + Widget build(BuildContext context) { + final tokens = context.tokens; + final size = tokens.sizing200; + + return Padding( + padding: EdgeInsets.only( + top: tokens.spacing100, + bottom: tokens.spacing100, + right: tokens.spacing200, + ), + child: AnimatedContainer( + duration: const Duration(milliseconds: 100), + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: isSelected ? _selectedBorder : tokens.borderWidth150, + color: state.borderColor(tokens, isSelected: isSelected), + ), + color: state.circleFillColor(tokens), + ), + ), + ); + } +} + +const double _selectedBorder = 6.0; diff --git a/optimus/lib/src/radio/radio.dart b/optimus/lib/src/radio/radio.dart index 7436d2e2..f801d76a 100644 --- a/optimus/lib/src/radio/radio.dart +++ b/optimus/lib/src/radio/radio.dart @@ -3,6 +3,8 @@ import 'package:flutter/widgets.dart'; import 'package:optimus/optimus.dart'; import 'package:optimus/src/common/gesture_wrapper.dart'; import 'package:optimus/src/common/group_wrapper.dart'; +import 'package:optimus/src/radio/circle.dart'; +import 'package:optimus/src/radio/state.dart'; /// The radio component is available in two size variants to accommodate /// different environments with different requirements. @@ -109,12 +111,12 @@ class _OptimusRadioState extends State> with ThemeGetter { } } - _RadioState get _state { - if (!widget.isEnabled) return _RadioState.disabled; - if (_isPressed) return _RadioState.active; - if (_isHovering) return _RadioState.hover; + RadioState get _state { + if (!widget.isEnabled) return RadioState.disabled; + if (_isPressed) return RadioState.active; + if (_isHovering) return RadioState.hover; - return _RadioState.basic; + return RadioState.basic; } @override @@ -139,7 +141,7 @@ class _OptimusRadioState extends State> with ThemeGetter { width: leadingSize, child: Align( alignment: Alignment.topLeft, - child: _RadioCircle( + child: RadioCircle( state: _state, isSelected: _isSelected, ), @@ -172,69 +174,3 @@ class _OptimusRadioState extends State> with ThemeGetter { ); } } - -class _RadioCircle extends StatelessWidget { - const _RadioCircle({ - required this.state, - required this.isSelected, - }); - - final _RadioState state; - final bool isSelected; - - @override - Widget build(BuildContext context) { - final tokens = context.tokens; - final size = tokens.sizing200; - - return Padding( - padding: EdgeInsets.only( - top: tokens.spacing100, - bottom: tokens.spacing100, - right: tokens.spacing200, - ), - child: AnimatedContainer( - duration: const Duration(milliseconds: 100), - width: size, - height: size, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - width: isSelected ? _selectedBorder : tokens.borderWidth150, - color: state.borderColor(tokens, isSelected: isSelected), - ), - color: state.circleFillColor(tokens), - ), - ), - ); - } -} - -enum _RadioState { basic, hover, active, disabled } - -extension on _RadioState { - Color borderColor(OptimusTokens tokens, {required bool isSelected}) => - switch (this) { - _RadioState.basic => isSelected - ? tokens.backgroundInteractivePrimaryDefault - : tokens.borderInteractiveSecondaryDefault, - _RadioState.hover => isSelected - ? tokens.backgroundInteractivePrimaryHover - : tokens.borderInteractiveSecondaryHover, - _RadioState.active => isSelected - ? tokens.backgroundInteractivePrimaryActive - : tokens.borderInteractiveSecondaryActive, - _RadioState.disabled => - isSelected ? tokens.backgroundDisabled : tokens.borderDisabled, - }; - - Color circleFillColor(OptimusTokens tokens) => switch (this) { - _RadioState.basic || - _RadioState.disabled => - tokens.backgroundInteractiveNeutralSubtleDefault, - _RadioState.hover => tokens.backgroundInteractiveNeutralSubtleHover, - _RadioState.active => tokens.backgroundInteractiveNeutralSubtleActive, - }; -} - -const double _selectedBorder = 6.0; diff --git a/optimus/lib/src/radio/state.dart b/optimus/lib/src/radio/state.dart new file mode 100644 index 00000000..567ae33d --- /dev/null +++ b/optimus/lib/src/radio/state.dart @@ -0,0 +1,30 @@ +import 'dart:ui'; + +import 'package:optimus/src/theme/optimus_tokens.dart'; + +enum RadioState { basic, hover, active, disabled } + +extension TokensTheme on RadioState { + Color borderColor(OptimusTokens tokens, {required bool isSelected}) => + switch (this) { + RadioState.basic => isSelected + ? tokens.backgroundInteractivePrimaryDefault + : tokens.borderInteractiveSecondaryDefault, + RadioState.hover => isSelected + ? tokens.backgroundInteractivePrimaryHover + : tokens.borderInteractiveSecondaryHover, + RadioState.active => isSelected + ? tokens.backgroundInteractivePrimaryActive + : tokens.borderInteractiveSecondaryActive, + RadioState.disabled => + isSelected ? tokens.backgroundDisabled : tokens.borderDisabled, + }; + + Color circleFillColor(OptimusTokens tokens) => switch (this) { + RadioState.basic || + RadioState.disabled => + tokens.backgroundInteractiveNeutralSubtleDefault, + RadioState.hover => tokens.backgroundInteractiveNeutralSubtleHover, + RadioState.active => tokens.backgroundInteractiveNeutralSubtleActive, + }; +} diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index a25f9c5f..d1854137 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -1,10 +1,12 @@ import 'package:flutter/widgets.dart'; +import 'package:optimus/optimus.dart'; +import 'package:optimus/src/common/gesture_wrapper.dart'; enum OptimusSelectionCardVariant { vertical, horizontal } enum OptimusSelectionCardBorderRadius { small, medium } -class OptimusSelectionCard extends StatelessWidget { +class OptimusSelectionCard extends StatefulWidget { const OptimusSelectionCard({ super.key, required this.title, @@ -21,5 +23,33 @@ class OptimusSelectionCard extends StatelessWidget { final bool isEnabled; @override - Widget build(BuildContext context) => Container(); + State createState() => _OptimusSelectionCardState(); +} + +class _OptimusSelectionCardState extends State { + bool _isHovered = false; + bool _isPressed = false; + + @override + Widget build(BuildContext context) => GestureWrapper( + onHoverChanged: (isHovered) { + setState(() { + _isHovered = isHovered; + }); + }, + onPressedChanged: (isPressed) { + setState(() { + _isPressed = isPressed; + }); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: context.tokens + .borderInteractiveSecondaryDefault, // TODO(witwash): replace + width: context.tokens.borderWidth150, + ), + ), + ), + ); } From a6c25e1812f4815574186a3e1769d5ef68170c9e Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Fri, 9 Aug 2024 13:32:27 +0200 Subject: [PATCH 03/16] upd --- optimus/lib/src/selection_card.dart | 17 ++++++++++++++--- storybook/lib/main.dart | 2 +- storybook/lib/stories/selection_card.dart | 23 ++++++++++++++++++++++- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index d1854137..f2e7dba0 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -1,6 +1,8 @@ import 'package:flutter/widgets.dart'; import 'package:optimus/optimus.dart'; import 'package:optimus/src/common/gesture_wrapper.dart'; +import 'package:optimus/src/radio/circle.dart'; +import 'package:optimus/src/radio/state.dart'; enum OptimusSelectionCardVariant { vertical, horizontal } @@ -16,8 +18,8 @@ class OptimusSelectionCard extends StatefulWidget { this.isEnabled = true, }); - final String title; - final String? description; + final Widget title; + final Widget? description; final Widget? trailing; final bool isSelected; final bool isEnabled; @@ -42,7 +44,7 @@ class _OptimusSelectionCardState extends State { _isPressed = isPressed; }); }, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( border: Border.all( color: context.tokens @@ -50,6 +52,15 @@ class _OptimusSelectionCardState extends State { width: context.tokens.borderWidth150, ), ), + child: Row( + children: [ + RadioCircle( + state: RadioState.basic, + isSelected: widget.isSelected, + ), + OptimusTitleMedium(child: widget.title), + ], + ), ), ); } diff --git a/storybook/lib/main.dart b/storybook/lib/main.dart index c6574b13..652d8073 100644 --- a/storybook/lib/main.dart +++ b/storybook/lib/main.dart @@ -105,7 +105,7 @@ class _MyAppState extends State { debugShowCheckedModeBanner: false, home: Scaffold(body: Center(child: child)), ), - initialStory: 'Welcome', + initialStory: 'Forms/SelectionCard', stories: [ welcomeStory, formStory, diff --git a/storybook/lib/stories/selection_card.dart b/storybook/lib/stories/selection_card.dart index 89524388..d01b7f76 100644 --- a/storybook/lib/stories/selection_card.dart +++ b/storybook/lib/stories/selection_card.dart @@ -1,7 +1,28 @@ import 'package:flutter/widgets.dart'; +import 'package:optimus/optimus.dart'; +import 'package:storybook/utils.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; final selectionCardStory = Story( name: 'Forms/SelectionCard', - builder: (context) => const Center(), + builder: (context) { + final k = context.knobs; + final title = k.text(label: 'Title', initial: 'Title'); + final description = k.text(label: 'Description', initial: 'Description'); + final trailing = k.options( + label: 'Trailing', + options: exampleIcons, + initial: exampleIcons.first, + ); + final isEnabled = k.boolean(label: 'Enabled', initial: true); + final isSelected = k.boolean(label: 'Selected', initial: false); + + return OptimusSelectionCard( + title: Text(title), + description: description.isNotEmpty ? Text(description) : null, + trailing: trailing != null ? const Icon(OptimusIcons.add) : null, + isSelected: isSelected, + isEnabled: isEnabled, + ); + }, ); From aed07ca5b9f9e6d96d4423aab93d998965ccbeae Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Fri, 9 Aug 2024 17:15:18 +0200 Subject: [PATCH 04/16] upd --- optimus/lib/src/selection_card.dart | 33 +++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index f2e7dba0..2df80ef7 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -14,6 +14,8 @@ class OptimusSelectionCard extends StatefulWidget { required this.title, this.description, this.trailing, + this.variant = OptimusSelectionCardVariant.horizontal, + this.borderRadius = OptimusSelectionCardBorderRadius.medium, this.isSelected = false, this.isEnabled = true, }); @@ -21,6 +23,8 @@ class OptimusSelectionCard extends StatefulWidget { final Widget title; final Widget? description; final Widget? trailing; + final OptimusSelectionCardVariant variant; + final OptimusSelectionCardBorderRadius borderRadius; final bool isSelected; final bool isEnabled; @@ -28,7 +32,8 @@ class OptimusSelectionCard extends StatefulWidget { State createState() => _OptimusSelectionCardState(); } -class _OptimusSelectionCardState extends State { +class _OptimusSelectionCardState extends State + with ThemeGetter { bool _isHovered = false; bool _isPressed = false; @@ -46,6 +51,8 @@ class _OptimusSelectionCardState extends State { }, child: DecoratedBox( decoration: BoxDecoration( + borderRadius: + BorderRadius.all(widget.borderRadius.getBorderRadius(tokens)), border: Border.all( color: context.tokens .borderInteractiveSecondaryDefault, // TODO(witwash): replace @@ -53,14 +60,36 @@ class _OptimusSelectionCardState extends State { ), ), child: Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ RadioCircle( state: RadioState.basic, isSelected: widget.isSelected, ), - OptimusTitleMedium(child: widget.title), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OptimusTitleMedium(child: widget.title), + if (widget.description case final description?) + OptimusSubtitle(child: description), + ], + ), + const Spacer(), + if (widget.trailing case final trailing?) trailing, ], ), ), ); } + +extension on OptimusSelectionCardBorderRadius { + Radius getBorderRadius(OptimusTokens tokens) { + switch (this) { + case OptimusSelectionCardBorderRadius.small: + return tokens.borderRadius100; + case OptimusSelectionCardBorderRadius.medium: + return tokens.borderRadius200; + } + } +} From 42a94f42f07fade87f12aff0fcbbb53ce289012c Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Mon, 12 Aug 2024 11:17:12 +0200 Subject: [PATCH 05/16] upd style --- optimus/lib/src/selection_card.dart | 157 +++++++++++++++++++++------- 1 file changed, 119 insertions(+), 38 deletions(-) diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index 2df80ef7..2c945f3a 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -36,53 +36,134 @@ class _OptimusSelectionCardState extends State with ThemeGetter { bool _isHovered = false; bool _isPressed = false; + final _controller = WidgetStatesController(); @override - Widget build(BuildContext context) => GestureWrapper( - onHoverChanged: (isHovered) { - setState(() { - _isHovered = isHovered; - }); - }, - onPressedChanged: (isPressed) { - setState(() { - _isPressed = isPressed; - }); - }, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: - BorderRadius.all(widget.borderRadius.getBorderRadius(tokens)), - border: Border.all( - color: context.tokens - .borderInteractiveSecondaryDefault, // TODO(witwash): replace - width: context.tokens.borderWidth150, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - RadioCircle( - state: RadioState.basic, - isSelected: widget.isSelected, + void dispose() { + _controller.dispose(); + super.dispose(); + } + + _InteractiveStateColor get _backgroundColor => _InteractiveStateColor( + context.tokens.backgroundStaticFlat.value, + disabled: context.tokens.backgroundStaticFlat, + pressed: context.tokens.backgroundStaticFlat, + hovered: context.tokens.backgroundStaticFlat, + ); + + _InteractiveStateColor get _borderColor => _InteractiveStateColor( + context.tokens.borderInteractiveSecondaryDefault.value, + disabled: context.tokens.borderDisabled, + pressed: context.tokens.borderInteractiveSecondaryActive, + hovered: context.tokens.borderInteractiveSecondaryHover, + ); + + _InteractiveStateColor get _titleColor => _InteractiveStateColor( + context.tokens.textStaticPrimary.value, + disabled: context.tokens.textDisabled, + pressed: context.tokens.textStaticPrimary, + hovered: context.tokens.textStaticPrimary, + ); + + _InteractiveStateColor get _descriptionColor => _InteractiveStateColor( + context.tokens.textStaticTertiary.value, + disabled: context.tokens.textDisabled, + pressed: context.tokens.textStaticTertiary, + hovered: context.tokens.textStaticTertiary, + ); + + @override + void didUpdateWidget(covariant OptimusSelectionCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isEnabled != oldWidget.isEnabled) { + _controller.update(WidgetState.disabled, !widget.isEnabled); + } + } + + @override + Widget build(BuildContext context) => ListenableBuilder( + listenable: _controller, + builder: (context, _) { + final backgroundColor = _backgroundColor.resolve(_controller.value); + final borderColor = _borderColor.resolve(_controller.value); + final titleColor = _titleColor.resolve(_controller.value); + final descriptionColor = _descriptionColor.resolve(_controller.value); + + return GestureWrapper( + onHoverChanged: (isHovered) => + _controller.update(WidgetState.hovered, isHovered), + onPressedChanged: (isPressed) => + _controller.update(WidgetState.pressed, isPressed), + child: DecoratedBox( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.all( + widget.borderRadius.getBorderRadius(tokens), + ), + border: Border.all( + color: borderColor, // TODO(witwash): replace + width: context.tokens.borderWidth150, + ), ), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ - OptimusTitleMedium(child: widget.title), - if (widget.description case final description?) - OptimusSubtitle(child: description), + RadioCircle( + state: RadioState.basic, + isSelected: widget.isSelected, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle.merge( + child: widget.title, + style: + tokens.bodyLargeStrong.copyWith(color: titleColor), + ), + if (widget.description case final description?) + DefaultTextStyle.merge( + child: description, + style: tokens.bodyMedium + .copyWith(color: descriptionColor)), + ], + ), + const Spacer(), + if (widget.trailing case final trailing?) trailing, ], ), - const Spacer(), - if (widget.trailing case final trailing?) trailing, - ], - ), - ), + ), + ); + }, ); } +class _InteractiveStateColor extends WidgetStateColor { + const _InteractiveStateColor(super.defaultValue, + {required this.disabled, required this.pressed, required this.hovered}) + : _defaultColor = defaultValue; + + final Color disabled; + final Color pressed; + final Color hovered; + final int _defaultColor; + + @override + Color resolve(Set states) { + if (states.isDisabled) return disabled; + if (states.isPressed) return pressed; + if (states.isHovered) return hovered; + + return Color(_defaultColor); + } +} + +extension on Set { + bool get isDisabled => contains(WidgetState.disabled); + bool get isPressed => contains(WidgetState.pressed); + bool get isHovered => contains(WidgetState.hovered); +} + extension on OptimusSelectionCardBorderRadius { Radius getBorderRadius(OptimusTokens tokens) { switch (this) { From a60bdf9c1bff113471f5f69ea795e9949ae78d55 Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Mon, 12 Aug 2024 11:25:39 +0200 Subject: [PATCH 06/16] upd --- optimus/lib/src/radio/circle.dart | 1 + optimus/lib/src/selection_card.dart | 97 +++++++++++++---------- storybook/lib/stories/selection_card.dart | 24 ++++-- 3 files changed, 74 insertions(+), 48 deletions(-) diff --git a/optimus/lib/src/radio/circle.dart b/optimus/lib/src/radio/circle.dart index f6ee8757..7a4e471d 100644 --- a/optimus/lib/src/radio/circle.dart +++ b/optimus/lib/src/radio/circle.dart @@ -18,6 +18,7 @@ class RadioCircle extends StatelessWidget { final size = tokens.sizing200; return Padding( + // TODO(witwash): remove padding and move it up padding: EdgeInsets.only( top: tokens.spacing100, bottom: tokens.spacing100, diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index 2c945f3a..84ada0ff 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -18,6 +18,7 @@ class OptimusSelectionCard extends StatefulWidget { this.borderRadius = OptimusSelectionCardBorderRadius.medium, this.isSelected = false, this.isEnabled = true, + this.onPressed, }); final Widget title; @@ -27,6 +28,7 @@ class OptimusSelectionCard extends StatefulWidget { final OptimusSelectionCardBorderRadius borderRadius; final bool isSelected; final bool isEnabled; + final VoidCallback? onPressed; @override State createState() => _OptimusSelectionCardState(); @@ -45,31 +47,31 @@ class _OptimusSelectionCardState extends State } _InteractiveStateColor get _backgroundColor => _InteractiveStateColor( - context.tokens.backgroundStaticFlat.value, - disabled: context.tokens.backgroundStaticFlat, - pressed: context.tokens.backgroundStaticFlat, - hovered: context.tokens.backgroundStaticFlat, + tokens.backgroundStaticFlat.value, + disabled: tokens.backgroundStaticFlat, + pressed: tokens.backgroundStaticFlat, + hovered: tokens.backgroundStaticFlat, ); _InteractiveStateColor get _borderColor => _InteractiveStateColor( - context.tokens.borderInteractiveSecondaryDefault.value, - disabled: context.tokens.borderDisabled, - pressed: context.tokens.borderInteractiveSecondaryActive, - hovered: context.tokens.borderInteractiveSecondaryHover, + tokens.borderInteractiveSecondaryDefault.value, + disabled: tokens.borderDisabled, + pressed: tokens.borderInteractiveSecondaryActive, + hovered: tokens.borderInteractiveSecondaryHover, ); _InteractiveStateColor get _titleColor => _InteractiveStateColor( - context.tokens.textStaticPrimary.value, - disabled: context.tokens.textDisabled, - pressed: context.tokens.textStaticPrimary, - hovered: context.tokens.textStaticPrimary, + tokens.textStaticPrimary.value, + disabled: tokens.textDisabled, + pressed: tokens.textStaticPrimary, + hovered: tokens.textStaticPrimary, ); _InteractiveStateColor get _descriptionColor => _InteractiveStateColor( - context.tokens.textStaticTertiary.value, - disabled: context.tokens.textDisabled, - pressed: context.tokens.textStaticTertiary, - hovered: context.tokens.textStaticTertiary, + tokens.textStaticTertiary.value, + disabled: tokens.textDisabled, + pressed: tokens.textStaticTertiary, + hovered: tokens.textStaticTertiary, ); @override @@ -94,6 +96,7 @@ class _OptimusSelectionCardState extends State _controller.update(WidgetState.hovered, isHovered), onPressedChanged: (isPressed) => _controller.update(WidgetState.pressed, isPressed), + onTap: widget.onPressed, child: DecoratedBox( decoration: BoxDecoration( color: backgroundColor, @@ -101,36 +104,44 @@ class _OptimusSelectionCardState extends State widget.borderRadius.getBorderRadius(tokens), ), border: Border.all( - color: borderColor, // TODO(witwash): replace - width: context.tokens.borderWidth150, + color: borderColor, + width: tokens.borderWidth150, ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - RadioCircle( - state: RadioState.basic, - isSelected: widget.isSelected, - ), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DefaultTextStyle.merge( - child: widget.title, - style: - tokens.bodyLargeStrong.copyWith(color: titleColor), + child: Padding( + padding: EdgeInsets.all(tokens.spacing200), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + RadioCircle( + state: RadioState.basic, + isSelected: widget.isSelected, + ), + Padding( + padding: + EdgeInsets.symmetric(horizontal: tokens.spacing200), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle.merge( + child: widget.title, + style: tokens.bodyLargeStrong + .copyWith(color: titleColor), + ), + if (widget.description case final description?) + DefaultTextStyle.merge( + child: description, + style: tokens.bodyMedium + .copyWith(color: descriptionColor), + ), + ], ), - if (widget.description case final description?) - DefaultTextStyle.merge( - child: description, - style: tokens.bodyMedium - .copyWith(color: descriptionColor)), - ], - ), - const Spacer(), - if (widget.trailing case final trailing?) trailing, - ], + ), + const Spacer(), + if (widget.trailing case final trailing?) trailing, + ], + ), ), ), ); diff --git a/storybook/lib/stories/selection_card.dart b/storybook/lib/stories/selection_card.dart index d01b7f76..461d2200 100644 --- a/storybook/lib/stories/selection_card.dart +++ b/storybook/lib/stories/selection_card.dart @@ -5,7 +5,21 @@ import 'package:storybook_flutter/storybook_flutter.dart'; final selectionCardStory = Story( name: 'Forms/SelectionCard', - builder: (context) { + builder: (context) => const _SelectionCardExample(), +); + +class _SelectionCardExample extends StatefulWidget { + const _SelectionCardExample(); + + @override + State<_SelectionCardExample> createState() => _SelectionCardExampleState(); +} + +class _SelectionCardExampleState extends State<_SelectionCardExample> { + bool _isSelected = false; + + @override + Widget build(BuildContext context) { final k = context.knobs; final title = k.text(label: 'Title', initial: 'Title'); final description = k.text(label: 'Description', initial: 'Description'); @@ -15,14 +29,14 @@ final selectionCardStory = Story( initial: exampleIcons.first, ); final isEnabled = k.boolean(label: 'Enabled', initial: true); - final isSelected = k.boolean(label: 'Selected', initial: false); return OptimusSelectionCard( title: Text(title), description: description.isNotEmpty ? Text(description) : null, trailing: trailing != null ? const Icon(OptimusIcons.add) : null, - isSelected: isSelected, + isSelected: _isSelected, isEnabled: isEnabled, + onPressed: () => setState(() => _isSelected = !_isSelected), ); - }, -); + } +} From 6fa56b4b52a1df921a50be58c76af8b41287764e Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Mon, 12 Aug 2024 11:59:45 +0200 Subject: [PATCH 07/16] upd --- optimus/lib/src/selection_card.dart | 77 ++++++++++++++++------- storybook/lib/stories/selection_card.dart | 8 +++ 2 files changed, 64 insertions(+), 21 deletions(-) diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index 84ada0ff..af3d2c4a 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:optimus/optimus.dart'; +import 'package:optimus/src/checkbox/checkbox_tick.dart'; import 'package:optimus/src/common/gesture_wrapper.dart'; import 'package:optimus/src/radio/circle.dart'; import 'package:optimus/src/radio/state.dart'; @@ -8,6 +9,8 @@ enum OptimusSelectionCardVariant { vertical, horizontal } enum OptimusSelectionCardBorderRadius { small, medium } +enum OptimusSelectionCardSelectionVariant { radio, checkbox } + class OptimusSelectionCard extends StatefulWidget { const OptimusSelectionCard({ super.key, @@ -16,7 +19,9 @@ class OptimusSelectionCard extends StatefulWidget { this.trailing, this.variant = OptimusSelectionCardVariant.horizontal, this.borderRadius = OptimusSelectionCardBorderRadius.medium, + this.selectionVariant = OptimusSelectionCardSelectionVariant.radio, this.isSelected = false, + this.showSelector = true, this.isEnabled = true, this.onPressed, }); @@ -26,7 +31,9 @@ class OptimusSelectionCard extends StatefulWidget { final Widget? trailing; final OptimusSelectionCardVariant variant; final OptimusSelectionCardBorderRadius borderRadius; + final OptimusSelectionCardSelectionVariant selectionVariant; final bool isSelected; + final bool showSelector; final bool isEnabled; final VoidCallback? onPressed; @@ -36,8 +43,6 @@ class OptimusSelectionCard extends StatefulWidget { class _OptimusSelectionCardState extends State with ThemeGetter { - bool _isHovered = false; - bool _isPressed = false; final _controller = WidgetStatesController(); @override @@ -47,28 +52,40 @@ class _OptimusSelectionCardState extends State } _InteractiveStateColor get _backgroundColor => _InteractiveStateColor( - tokens.backgroundStaticFlat.value, + defaultColor: widget.isSelected + ? tokens.backgroundInteractiveSecondaryDefault + : tokens.backgroundStaticFlat, disabled: tokens.backgroundStaticFlat, - pressed: tokens.backgroundStaticFlat, - hovered: tokens.backgroundStaticFlat, + pressed: widget.isSelected + ? tokens.backgroundInteractiveSecondaryActive + : tokens.backgroundStaticFlat, + hovered: widget.isSelected + ? tokens.backgroundInteractiveSecondaryHover + : tokens.backgroundStaticFlat, ); _InteractiveStateColor get _borderColor => _InteractiveStateColor( - tokens.borderInteractiveSecondaryDefault.value, + defaultColor: widget.isSelected + ? tokens.borderInteractivePrimaryDefault + : tokens.borderInteractiveSecondaryDefault, disabled: tokens.borderDisabled, - pressed: tokens.borderInteractiveSecondaryActive, - hovered: tokens.borderInteractiveSecondaryHover, + pressed: widget.isSelected + ? tokens.borderInteractivePrimaryActive + : tokens.borderInteractiveSecondaryActive, + hovered: widget.isSelected + ? tokens.borderInteractivePrimaryHover + : tokens.borderInteractiveSecondaryHover, ); _InteractiveStateColor get _titleColor => _InteractiveStateColor( - tokens.textStaticPrimary.value, + defaultColor: tokens.textStaticPrimary, disabled: tokens.textDisabled, pressed: tokens.textStaticPrimary, hovered: tokens.textStaticPrimary, ); _InteractiveStateColor get _descriptionColor => _InteractiveStateColor( - tokens.textStaticTertiary.value, + defaultColor: tokens.textStaticTertiary, disabled: tokens.textDisabled, pressed: tokens.textStaticTertiary, hovered: tokens.textStaticTertiary, @@ -91,6 +108,19 @@ class _OptimusSelectionCardState extends State final titleColor = _titleColor.resolve(_controller.value); final descriptionColor = _descriptionColor.resolve(_controller.value); + final selector = widget.selectionVariant == + OptimusSelectionCardSelectionVariant.radio + ? RadioCircle( + state: RadioState.basic, + isSelected: widget.isSelected, + ) + : CheckboxTick( + isEnabled: widget.isEnabled, + isChecked: widget.isSelected, + onChanged: (_) {}, + onTap: () {}, + ); + return GestureWrapper( onHoverChanged: (isHovered) => _controller.update(WidgetState.hovered, isHovered), @@ -113,10 +143,7 @@ class _OptimusSelectionCardState extends State child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - RadioCircle( - state: RadioState.basic, - isSelected: widget.isSelected, - ), + if (widget.showSelector) selector, Padding( padding: EdgeInsets.symmetric(horizontal: tokens.spacing200), @@ -129,6 +156,7 @@ class _OptimusSelectionCardState extends State style: tokens.bodyLargeStrong .copyWith(color: titleColor), ), + SizedBox(height: tokens.spacing25), if (widget.description case final description?) DefaultTextStyle.merge( child: description, @@ -139,7 +167,11 @@ class _OptimusSelectionCardState extends State ), ), const Spacer(), - if (widget.trailing case final trailing?) trailing, + if (widget.trailing case final trailing?) + IconTheme.merge( + child: trailing, + data: IconThemeData(color: titleColor), + ), ], ), ), @@ -149,15 +181,18 @@ class _OptimusSelectionCardState extends State ); } -class _InteractiveStateColor extends WidgetStateColor { - const _InteractiveStateColor(super.defaultValue, - {required this.disabled, required this.pressed, required this.hovered}) - : _defaultColor = defaultValue; +class _InteractiveStateColor extends WidgetStateProperty { + _InteractiveStateColor({ + required this.defaultColor, + required this.disabled, + required this.pressed, + required this.hovered, + }); final Color disabled; final Color pressed; final Color hovered; - final int _defaultColor; + final Color defaultColor; @override Color resolve(Set states) { @@ -165,7 +200,7 @@ class _InteractiveStateColor extends WidgetStateColor { if (states.isPressed) return pressed; if (states.isHovered) return hovered; - return Color(_defaultColor); + return defaultColor; } } diff --git a/storybook/lib/stories/selection_card.dart b/storybook/lib/stories/selection_card.dart index 461d2200..e18534ac 100644 --- a/storybook/lib/stories/selection_card.dart +++ b/storybook/lib/stories/selection_card.dart @@ -28,6 +28,12 @@ class _SelectionCardExampleState extends State<_SelectionCardExample> { options: exampleIcons, initial: exampleIcons.first, ); + final selectorVariant = k.options( + label: 'Selector variant', + initial: OptimusSelectionCardSelectionVariant.radio, + options: OptimusSelectionCardSelectionVariant.values.toOptions(), + ); + final showSelector = k.boolean(label: 'Show selector', initial: true); final isEnabled = k.boolean(label: 'Enabled', initial: true); return OptimusSelectionCard( @@ -35,6 +41,8 @@ class _SelectionCardExampleState extends State<_SelectionCardExample> { description: description.isNotEmpty ? Text(description) : null, trailing: trailing != null ? const Icon(OptimusIcons.add) : null, isSelected: _isSelected, + showSelector: showSelector, + selectionVariant: selectorVariant, isEnabled: isEnabled, onPressed: () => setState(() => _isSelected = !_isSelected), ); From d431747c2221929e437c340da66885a28907364d Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Mon, 12 Aug 2024 12:02:55 +0200 Subject: [PATCH 08/16] upd --- optimus/lib/src/selection_card.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index af3d2c4a..ee06fa2f 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -26,15 +26,34 @@ class OptimusSelectionCard extends StatefulWidget { this.onPressed, }); + /// The title of the card. final Widget title; + + /// The description of the card. final Widget? description; + + /// The trailing widget of the card. final Widget? trailing; + + /// The variant of the card. Default is [OptimusSelectionCardVariant.horizontal]. final OptimusSelectionCardVariant variant; + + /// The border radius of the card. Default is [OptimusSelectionCardBorderRadius.medium]. final OptimusSelectionCardBorderRadius borderRadius; + + /// The selection variant of the card. Default is [OptimusSelectionCardSelectionVariant.radio]. final OptimusSelectionCardSelectionVariant selectionVariant; + + /// Whether the card is selected. Default is false. final bool isSelected; + + /// Whether the selector is shown. Default is true. final bool showSelector; + + /// Whether the card is enabled. Default is true. final bool isEnabled; + + /// The callback that is called when the card is pressed. final VoidCallback? onPressed; @override From 0491fb677ffa4f17b11d8a4a50f13c8e9872c072 Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Mon, 12 Aug 2024 12:18:54 +0200 Subject: [PATCH 09/16] upd --- optimus/lib/src/common/state_property.dart | 30 +++++++++++++++++ optimus/lib/src/selection_card.dart | 38 +++------------------- storybook/lib/stories/selection_card.dart | 16 +++++++-- 3 files changed, 49 insertions(+), 35 deletions(-) create mode 100644 optimus/lib/src/common/state_property.dart diff --git a/optimus/lib/src/common/state_property.dart b/optimus/lib/src/common/state_property.dart new file mode 100644 index 00000000..3ea11d5a --- /dev/null +++ b/optimus/lib/src/common/state_property.dart @@ -0,0 +1,30 @@ +import 'package:flutter/widgets.dart'; + +class InteractiveStateColor extends WidgetStateProperty { + InteractiveStateColor({ + required this.defaultColor, + required this.disabled, + required this.pressed, + required this.hovered, + }); + + final Color disabled; + final Color pressed; + final Color hovered; + final Color defaultColor; + + @override + Color resolve(Set states) { + if (states.isDisabled) return disabled; + if (states.isPressed) return pressed; + if (states.isHovered) return hovered; + + return defaultColor; + } +} + +extension States on Set { + bool get isDisabled => contains(WidgetState.disabled); + bool get isPressed => contains(WidgetState.pressed); + bool get isHovered => contains(WidgetState.hovered); +} diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index ee06fa2f..330753ef 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:optimus/optimus.dart'; import 'package:optimus/src/checkbox/checkbox_tick.dart'; import 'package:optimus/src/common/gesture_wrapper.dart'; +import 'package:optimus/src/common/state_property.dart'; import 'package:optimus/src/radio/circle.dart'; import 'package:optimus/src/radio/state.dart'; @@ -70,7 +71,7 @@ class _OptimusSelectionCardState extends State super.dispose(); } - _InteractiveStateColor get _backgroundColor => _InteractiveStateColor( + InteractiveStateColor get _backgroundColor => InteractiveStateColor( defaultColor: widget.isSelected ? tokens.backgroundInteractiveSecondaryDefault : tokens.backgroundStaticFlat, @@ -83,7 +84,7 @@ class _OptimusSelectionCardState extends State : tokens.backgroundStaticFlat, ); - _InteractiveStateColor get _borderColor => _InteractiveStateColor( + InteractiveStateColor get _borderColor => InteractiveStateColor( defaultColor: widget.isSelected ? tokens.borderInteractivePrimaryDefault : tokens.borderInteractiveSecondaryDefault, @@ -96,14 +97,14 @@ class _OptimusSelectionCardState extends State : tokens.borderInteractiveSecondaryHover, ); - _InteractiveStateColor get _titleColor => _InteractiveStateColor( + InteractiveStateColor get _titleColor => InteractiveStateColor( defaultColor: tokens.textStaticPrimary, disabled: tokens.textDisabled, pressed: tokens.textStaticPrimary, hovered: tokens.textStaticPrimary, ); - _InteractiveStateColor get _descriptionColor => _InteractiveStateColor( + InteractiveStateColor get _descriptionColor => InteractiveStateColor( defaultColor: tokens.textStaticTertiary, disabled: tokens.textDisabled, pressed: tokens.textStaticTertiary, @@ -200,35 +201,6 @@ class _OptimusSelectionCardState extends State ); } -class _InteractiveStateColor extends WidgetStateProperty { - _InteractiveStateColor({ - required this.defaultColor, - required this.disabled, - required this.pressed, - required this.hovered, - }); - - final Color disabled; - final Color pressed; - final Color hovered; - final Color defaultColor; - - @override - Color resolve(Set states) { - if (states.isDisabled) return disabled; - if (states.isPressed) return pressed; - if (states.isHovered) return hovered; - - return defaultColor; - } -} - -extension on Set { - bool get isDisabled => contains(WidgetState.disabled); - bool get isPressed => contains(WidgetState.pressed); - bool get isHovered => contains(WidgetState.hovered); -} - extension on OptimusSelectionCardBorderRadius { Radius getBorderRadius(OptimusTokens tokens) { switch (this) { diff --git a/storybook/lib/stories/selection_card.dart b/storybook/lib/stories/selection_card.dart index e18534ac..2d11054f 100644 --- a/storybook/lib/stories/selection_card.dart +++ b/storybook/lib/stories/selection_card.dart @@ -26,7 +26,17 @@ class _SelectionCardExampleState extends State<_SelectionCardExample> { final trailing = k.options( label: 'Trailing', options: exampleIcons, - initial: exampleIcons.first, + initial: OptimusIcons.chevron_right, + ); + final variant = k.options( + label: 'Variant', + initial: OptimusSelectionCardVariant.horizontal, + options: OptimusSelectionCardVariant.values.toOptions(), + ); + final borderRadius = k.options( + label: 'Border radius', + initial: OptimusSelectionCardBorderRadius.medium, + options: OptimusSelectionCardBorderRadius.values.toOptions(), ); final selectorVariant = k.options( label: 'Selector variant', @@ -39,10 +49,12 @@ class _SelectionCardExampleState extends State<_SelectionCardExample> { return OptimusSelectionCard( title: Text(title), description: description.isNotEmpty ? Text(description) : null, - trailing: trailing != null ? const Icon(OptimusIcons.add) : null, + trailing: trailing != null ? Icon(trailing) : null, + variant: variant, isSelected: _isSelected, showSelector: showSelector, selectionVariant: selectorVariant, + borderRadius: borderRadius, isEnabled: isEnabled, onPressed: () => setState(() => _isSelected = !_isSelected), ); From 4ef7eca7ca2238fae497be9a0f77e1469c94316e Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Mon, 12 Aug 2024 15:53:18 +0200 Subject: [PATCH 10/16] upd documentation --- optimus/lib/src/selection_card.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index 330753ef..49d36045 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -12,6 +12,10 @@ enum OptimusSelectionCardBorderRadius { small, medium } enum OptimusSelectionCardSelectionVariant { radio, checkbox } +/// A card that represents a choice. Depending on the [selectionVariant], it can +/// be a radio button or a checkbox, hence single or multiple selection. +/// The card consists of a title, an optional description, and an optional +/// trailing widget. class OptimusSelectionCard extends StatefulWidget { const OptimusSelectionCard({ super.key, From 002ca3de68d734169dd75201f350710b7bfbc159 Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Mon, 12 Aug 2024 16:15:30 +0200 Subject: [PATCH 11/16] upd --- optimus/lib/src/selection_card.dart | 145 ++++++++++++++++------ storybook/lib/stories/selection_card.dart | 5 +- 2 files changed, 109 insertions(+), 41 deletions(-) diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index 49d36045..276c7411 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -26,7 +26,7 @@ class OptimusSelectionCard extends StatefulWidget { this.borderRadius = OptimusSelectionCardBorderRadius.medium, this.selectionVariant = OptimusSelectionCardSelectionVariant.radio, this.isSelected = false, - this.showSelector = true, + this.isSelectorVisible = true, this.isEnabled = true, this.onPressed, }); @@ -53,7 +53,7 @@ class OptimusSelectionCard extends StatefulWidget { final bool isSelected; /// Whether the selector is shown. Default is true. - final bool showSelector; + final bool isSelectorVisible; /// Whether the card is enabled. Default is true. final bool isEnabled; @@ -145,6 +145,18 @@ class _OptimusSelectionCardState extends State onTap: () {}, ); + final title = DefaultTextStyle.merge( + child: widget.title, + style: tokens.bodyLargeStrong.copyWith(color: titleColor), + ); + + final description = widget.description != null + ? DefaultTextStyle.merge( + child: widget.description!, + style: tokens.bodyMedium.copyWith(color: descriptionColor), + ) + : null; + return GestureWrapper( onHoverChanged: (isHovered) => _controller.update(WidgetState.hovered, isHovered), @@ -162,49 +174,104 @@ class _OptimusSelectionCardState extends State width: tokens.borderWidth150, ), ), - child: Padding( - padding: EdgeInsets.all(tokens.spacing200), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (widget.showSelector) selector, - Padding( - padding: - EdgeInsets.symmetric(horizontal: tokens.spacing200), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DefaultTextStyle.merge( - child: widget.title, - style: tokens.bodyLargeStrong - .copyWith(color: titleColor), - ), - SizedBox(height: tokens.spacing25), - if (widget.description case final description?) - DefaultTextStyle.merge( - child: description, - style: tokens.bodyMedium - .copyWith(color: descriptionColor), - ), - ], - ), - ), - const Spacer(), - if (widget.trailing case final trailing?) - IconTheme.merge( - child: trailing, - data: IconThemeData(color: titleColor), - ), - ], - ), - ), + child: switch (widget.variant) { + OptimusSelectionCardVariant.horizontal => _HorizontalCard( + title: title, + description: description, + trailing: widget.trailing, + isSelected: widget.isSelected, + selector: widget.isSelectorVisible ? selector : null, + ), + OptimusSelectionCardVariant.vertical => _VerticalCard( + title: widget.title, + description: widget.description, + trailing: widget.trailing, + isSelected: widget.isSelected, + selector: widget.isSelectorVisible ? selector : null, + ) + }, ), ); }, ); } +class _HorizontalCard extends StatelessWidget { + const _HorizontalCard({ + required this.title, + this.description, + this.trailing, + required this.isSelected, + this.selector, + }); + + final Widget title; + final Widget? description; + final Widget? trailing; + final Widget? selector; + final bool isSelected; + + @override + Widget build(BuildContext context) { + final tokens = context.tokens; + + return Padding( + padding: EdgeInsets.all(tokens.spacing200), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (selector case final selector?) selector, + Padding( + padding: EdgeInsets.symmetric(horizontal: tokens.spacing200), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title, + SizedBox(height: tokens.spacing25), + if (description case final description?) description, + ], + ), + ), + const Spacer(), + if (trailing case final trailing?) trailing, + ], + ), + ); + } +} + +class _VerticalCard extends StatelessWidget { + const _VerticalCard({ + required this.title, + this.description, + this.trailing, + required this.isSelected, + this.selector, + }); + + final Widget title; + final Widget? description; + final Widget? trailing; + final bool isSelected; + final Widget? selector; + + @override + Widget build(BuildContext context) => Stack( + children: [ + if (selector case final selector?) + Align(alignment: Alignment.topRight, child: selector), + Row( + children: [ + if (trailing case final trailing?) trailing, + title, + if (description case final description?) description, + ], + ), + ], + ); +} + extension on OptimusSelectionCardBorderRadius { Radius getBorderRadius(OptimusTokens tokens) { switch (this) { diff --git a/storybook/lib/stories/selection_card.dart b/storybook/lib/stories/selection_card.dart index 2d11054f..b56260d3 100644 --- a/storybook/lib/stories/selection_card.dart +++ b/storybook/lib/stories/selection_card.dart @@ -43,7 +43,8 @@ class _SelectionCardExampleState extends State<_SelectionCardExample> { initial: OptimusSelectionCardSelectionVariant.radio, options: OptimusSelectionCardSelectionVariant.values.toOptions(), ); - final showSelector = k.boolean(label: 'Show selector', initial: true); + final isSelectorVisible = + k.boolean(label: 'Selector visible', initial: true); final isEnabled = k.boolean(label: 'Enabled', initial: true); return OptimusSelectionCard( @@ -52,7 +53,7 @@ class _SelectionCardExampleState extends State<_SelectionCardExample> { trailing: trailing != null ? Icon(trailing) : null, variant: variant, isSelected: _isSelected, - showSelector: showSelector, + isSelectorVisible: isSelectorVisible, selectionVariant: selectorVariant, borderRadius: borderRadius, isEnabled: isEnabled, From cc6f0abcea80cf694b6e4e1ff6b2c19e0cdbd767 Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Mon, 12 Aug 2024 17:15:22 +0200 Subject: [PATCH 12/16] upd --- optimus/lib/src/selection_card.dart | 116 ++++++++++++++-------- storybook/lib/main.dart | 4 +- storybook/lib/stories/selection_card.dart | 29 +++--- 3 files changed, 93 insertions(+), 56 deletions(-) diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index 276c7411..fa85b9f9 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -1,3 +1,4 @@ +import 'package:dfunc/dfunc.dart'; import 'package:flutter/widgets.dart'; import 'package:optimus/optimus.dart'; import 'package:optimus/src/checkbox/checkbox_tick.dart'; @@ -132,30 +133,39 @@ class _OptimusSelectionCardState extends State final titleColor = _titleColor.resolve(_controller.value); final descriptionColor = _descriptionColor.resolve(_controller.value); - final selector = widget.selectionVariant == - OptimusSelectionCardSelectionVariant.radio - ? RadioCircle( - state: RadioState.basic, - isSelected: widget.isSelected, - ) - : CheckboxTick( - isEnabled: widget.isEnabled, - isChecked: widget.isSelected, - onChanged: (_) {}, - onTap: () {}, - ); + final selector = widget.isSelectorVisible + ? switch (widget.selectionVariant) { + OptimusSelectionCardSelectionVariant.radio => RadioCircle( + state: RadioState.basic, + isSelected: widget.isSelected, + ), + OptimusSelectionCardSelectionVariant.checkbox => CheckboxTick( + isEnabled: widget.isEnabled, + isChecked: widget.isSelected, + onChanged: (_) {}, + onTap: () {}, + ) + } + : null; final title = DefaultTextStyle.merge( child: widget.title, style: tokens.bodyLargeStrong.copyWith(color: titleColor), ); - final description = widget.description != null - ? DefaultTextStyle.merge( - child: widget.description!, - style: tokens.bodyMedium.copyWith(color: descriptionColor), - ) - : null; + final Widget? description = widget.description?.let( + (it) => DefaultTextStyle.merge( + child: it, + style: tokens.bodyMedium.copyWith(color: descriptionColor), + ), + ); + + final Widget? trailing = widget.trailing?.let( + (it) => IconTheme.merge( + child: it, + data: IconThemeData(color: titleColor), + ), + ); return GestureWrapper( onHoverChanged: (isHovered) => @@ -178,16 +188,16 @@ class _OptimusSelectionCardState extends State OptimusSelectionCardVariant.horizontal => _HorizontalCard( title: title, description: description, - trailing: widget.trailing, + trailing: trailing, isSelected: widget.isSelected, - selector: widget.isSelectorVisible ? selector : null, + selector: selector, ), OptimusSelectionCardVariant.vertical => _VerticalCard( - title: widget.title, - description: widget.description, - trailing: widget.trailing, + title: title, + description: description, + trailing: trailing, isSelected: widget.isSelected, - selector: widget.isSelectorVisible ? selector : null, + selector: selector, ) }, ), @@ -219,21 +229,24 @@ class _HorizontalCard extends StatelessWidget { padding: EdgeInsets.all(tokens.spacing200), child: Row( mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ if (selector case final selector?) selector, - Padding( - padding: EdgeInsets.symmetric(horizontal: tokens.spacing200), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title, - SizedBox(height: tokens.spacing25), - if (description case final description?) description, - ], + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: tokens.spacing200), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible(child: title), + SizedBox(height: tokens.spacing25), + if (description case final description?) + Flexible(child: description), + ], + ), ), ), - const Spacer(), if (trailing case final trailing?) trailing, ], ), @@ -258,15 +271,34 @@ class _VerticalCard extends StatelessWidget { @override Widget build(BuildContext context) => Stack( + alignment: Alignment.center, children: [ if (selector case final selector?) - Align(alignment: Alignment.topRight, child: selector), - Row( - children: [ - if (trailing case final trailing?) trailing, - title, - if (description case final description?) description, - ], + Positioned( + right: context + .tokens.spacing0, // TODO(witwash): fix after radio is fixed + top: context.tokens.spacing100, + child: selector, + ), + Container( + padding: EdgeInsets.symmetric( + horizontal: context.tokens.spacing200, + vertical: context.tokens.spacing400, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (trailing case final trailing?) trailing, + SizedBox( + height: context.tokens.spacing200, + width: context.tokens.sizing1300, + ), + Flexible(child: title), + SizedBox(height: context.tokens.spacing50), + if (description case final description?) + Flexible(child: description), + ], + ), ), ], ); diff --git a/storybook/lib/main.dart b/storybook/lib/main.dart index 652d8073..559f02fc 100644 --- a/storybook/lib/main.dart +++ b/storybook/lib/main.dart @@ -45,6 +45,7 @@ import 'package:storybook/stories/radio.dart'; import 'package:storybook/stories/search_field.dart'; import 'package:storybook/stories/segmented_control.dart'; import 'package:storybook/stories/select_input.dart'; +import 'package:storybook/stories/selection_card.dart'; import 'package:storybook/stories/slidable.dart'; import 'package:storybook/stories/spacing.dart'; import 'package:storybook/stories/stack.dart'; @@ -105,7 +106,7 @@ class _MyAppState extends State { debugShowCheckedModeBanner: false, home: Scaffold(body: Center(child: child)), ), - initialStory: 'Forms/SelectionCard', + initialStory: 'Forms/Selection Card', stories: [ welcomeStory, formStory, @@ -171,6 +172,7 @@ class _MyAppState extends State { passwordStory, toggleButtonStory, textAreaStory, + selectionCardStory, ], ), }, diff --git a/storybook/lib/stories/selection_card.dart b/storybook/lib/stories/selection_card.dart index b56260d3..686153d3 100644 --- a/storybook/lib/stories/selection_card.dart +++ b/storybook/lib/stories/selection_card.dart @@ -4,7 +4,7 @@ import 'package:storybook/utils.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; final selectionCardStory = Story( - name: 'Forms/SelectionCard', + name: 'Forms/Selection Card', builder: (context) => const _SelectionCardExample(), ); @@ -30,7 +30,7 @@ class _SelectionCardExampleState extends State<_SelectionCardExample> { ); final variant = k.options( label: 'Variant', - initial: OptimusSelectionCardVariant.horizontal, + initial: OptimusSelectionCardVariant.vertical, options: OptimusSelectionCardVariant.values.toOptions(), ); final borderRadius = k.options( @@ -47,17 +47,20 @@ class _SelectionCardExampleState extends State<_SelectionCardExample> { k.boolean(label: 'Selector visible', initial: true); final isEnabled = k.boolean(label: 'Enabled', initial: true); - return OptimusSelectionCard( - title: Text(title), - description: description.isNotEmpty ? Text(description) : null, - trailing: trailing != null ? Icon(trailing) : null, - variant: variant, - isSelected: _isSelected, - isSelectorVisible: isSelectorVisible, - selectionVariant: selectorVariant, - borderRadius: borderRadius, - isEnabled: isEnabled, - onPressed: () => setState(() => _isSelected = !_isSelected), + return SizedBox( + width: 500, + child: OptimusSelectionCard( + title: Text(title), + description: description.isNotEmpty ? Text(description) : null, + trailing: trailing != null ? Icon(trailing) : null, + variant: variant, + isSelected: _isSelected, + isSelectorVisible: isSelectorVisible, + selectionVariant: selectorVariant, + borderRadius: borderRadius, + isEnabled: isEnabled, + onPressed: () => setState(() => _isSelected = !_isSelected), + ), ); } } From 281ea43db2419903807fc8adf2bdfe71c3c65628 Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Wed, 14 Aug 2024 12:35:25 +0200 Subject: [PATCH 13/16] fix after merge --- optimus/lib/src/radio/circle.dart | 44 ----------------------------- optimus/lib/src/radio/state.dart | 30 -------------------- optimus/lib/src/selection_card.dart | 8 ++---- 3 files changed, 3 insertions(+), 79 deletions(-) delete mode 100644 optimus/lib/src/radio/circle.dart delete mode 100644 optimus/lib/src/radio/state.dart diff --git a/optimus/lib/src/radio/circle.dart b/optimus/lib/src/radio/circle.dart deleted file mode 100644 index 7a4e471d..00000000 --- a/optimus/lib/src/radio/circle.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:optimus/src/radio/state.dart'; -import 'package:optimus/src/theme/theme.dart'; - -class RadioCircle extends StatelessWidget { - const RadioCircle({ - super.key, - required this.state, - required this.isSelected, - }); - - final RadioState state; - final bool isSelected; - - @override - Widget build(BuildContext context) { - final tokens = context.tokens; - final size = tokens.sizing200; - - return Padding( - // TODO(witwash): remove padding and move it up - padding: EdgeInsets.only( - top: tokens.spacing100, - bottom: tokens.spacing100, - right: tokens.spacing200, - ), - child: AnimatedContainer( - duration: const Duration(milliseconds: 100), - width: size, - height: size, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - width: isSelected ? _selectedBorder : tokens.borderWidth150, - color: state.borderColor(tokens, isSelected: isSelected), - ), - color: state.circleFillColor(tokens), - ), - ), - ); - } -} - -const double _selectedBorder = 6.0; diff --git a/optimus/lib/src/radio/state.dart b/optimus/lib/src/radio/state.dart deleted file mode 100644 index 567ae33d..00000000 --- a/optimus/lib/src/radio/state.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:ui'; - -import 'package:optimus/src/theme/optimus_tokens.dart'; - -enum RadioState { basic, hover, active, disabled } - -extension TokensTheme on RadioState { - Color borderColor(OptimusTokens tokens, {required bool isSelected}) => - switch (this) { - RadioState.basic => isSelected - ? tokens.backgroundInteractivePrimaryDefault - : tokens.borderInteractiveSecondaryDefault, - RadioState.hover => isSelected - ? tokens.backgroundInteractivePrimaryHover - : tokens.borderInteractiveSecondaryHover, - RadioState.active => isSelected - ? tokens.backgroundInteractivePrimaryActive - : tokens.borderInteractiveSecondaryActive, - RadioState.disabled => - isSelected ? tokens.backgroundDisabled : tokens.borderDisabled, - }; - - Color circleFillColor(OptimusTokens tokens) => switch (this) { - RadioState.basic || - RadioState.disabled => - tokens.backgroundInteractiveNeutralSubtleDefault, - RadioState.hover => tokens.backgroundInteractiveNeutralSubtleHover, - RadioState.active => tokens.backgroundInteractiveNeutralSubtleActive, - }; -} diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index fa85b9f9..111d5695 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -4,8 +4,7 @@ import 'package:optimus/optimus.dart'; import 'package:optimus/src/checkbox/checkbox_tick.dart'; import 'package:optimus/src/common/gesture_wrapper.dart'; import 'package:optimus/src/common/state_property.dart'; -import 'package:optimus/src/radio/circle.dart'; -import 'package:optimus/src/radio/state.dart'; +import 'package:optimus/src/radio/radio_circle.dart'; enum OptimusSelectionCardVariant { vertical, horizontal } @@ -136,7 +135,7 @@ class _OptimusSelectionCardState extends State final selector = widget.isSelectorVisible ? switch (widget.selectionVariant) { OptimusSelectionCardSelectionVariant.radio => RadioCircle( - state: RadioState.basic, + controller: _controller, isSelected: widget.isSelected, ), OptimusSelectionCardSelectionVariant.checkbox => CheckboxTick( @@ -275,8 +274,7 @@ class _VerticalCard extends StatelessWidget { children: [ if (selector case final selector?) Positioned( - right: context - .tokens.spacing0, // TODO(witwash): fix after radio is fixed + right: context.tokens.spacing100, top: context.tokens.spacing100, child: selector, ), From 3d7da46982e4cdf078fbe3fdf252f2705a0e5aba Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Wed, 14 Aug 2024 12:38:23 +0200 Subject: [PATCH 14/16] rollback initialStory change --- storybook/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storybook/lib/main.dart b/storybook/lib/main.dart index 559f02fc..1ae60afb 100644 --- a/storybook/lib/main.dart +++ b/storybook/lib/main.dart @@ -106,7 +106,7 @@ class _MyAppState extends State { debugShowCheckedModeBanner: false, home: Scaffold(body: Center(child: child)), ), - initialStory: 'Forms/Selection Card', + initialStory: 'Welcome', stories: [ welcomeStory, formStory, From f4cea96fe82e8c9ac600196ba364b68e731ee0e0 Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Wed, 14 Aug 2024 12:47:11 +0200 Subject: [PATCH 15/16] fix disabled interaction --- optimus/lib/src/selection_card.dart | 153 ++++++++++++++-------------- 1 file changed, 79 insertions(+), 74 deletions(-) diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index 111d5695..c0323fc5 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -124,84 +124,89 @@ class _OptimusSelectionCardState extends State } @override - Widget build(BuildContext context) => ListenableBuilder( - listenable: _controller, - builder: (context, _) { - final backgroundColor = _backgroundColor.resolve(_controller.value); - final borderColor = _borderColor.resolve(_controller.value); - final titleColor = _titleColor.resolve(_controller.value); - final descriptionColor = _descriptionColor.resolve(_controller.value); - - final selector = widget.isSelectorVisible - ? switch (widget.selectionVariant) { - OptimusSelectionCardSelectionVariant.radio => RadioCircle( - controller: _controller, + Widget build(BuildContext context) => IgnorePointer( + ignoring: !widget.isEnabled, + child: ListenableBuilder( + listenable: _controller, + builder: (context, _) { + final backgroundColor = _backgroundColor.resolve(_controller.value); + final borderColor = _borderColor.resolve(_controller.value); + final titleColor = _titleColor.resolve(_controller.value); + final descriptionColor = + _descriptionColor.resolve(_controller.value); + + final selector = widget.isSelectorVisible + ? switch (widget.selectionVariant) { + OptimusSelectionCardSelectionVariant.radio => RadioCircle( + controller: _controller, + isSelected: widget.isSelected, + ), + OptimusSelectionCardSelectionVariant.checkbox => + CheckboxTick( + isEnabled: widget.isEnabled, + isChecked: widget.isSelected, + onChanged: (_) {}, + onTap: () {}, + ) + } + : null; + + final title = DefaultTextStyle.merge( + child: widget.title, + style: tokens.bodyLargeStrong.copyWith(color: titleColor), + ); + + final Widget? description = widget.description?.let( + (it) => DefaultTextStyle.merge( + child: it, + style: tokens.bodyMedium.copyWith(color: descriptionColor), + ), + ); + + final Widget? trailing = widget.trailing?.let( + (it) => IconTheme.merge( + child: it, + data: IconThemeData(color: titleColor), + ), + ); + + return GestureWrapper( + onHoverChanged: (isHovered) => + _controller.update(WidgetState.hovered, isHovered), + onPressedChanged: (isPressed) => + _controller.update(WidgetState.pressed, isPressed), + onTap: widget.onPressed, + child: DecoratedBox( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.all( + widget.borderRadius.getBorderRadius(tokens), + ), + border: Border.all( + color: borderColor, + width: tokens.borderWidth150, + ), + ), + child: switch (widget.variant) { + OptimusSelectionCardVariant.horizontal => _HorizontalCard( + title: title, + description: description, + trailing: trailing, isSelected: widget.isSelected, + selector: selector, ), - OptimusSelectionCardSelectionVariant.checkbox => CheckboxTick( - isEnabled: widget.isEnabled, - isChecked: widget.isSelected, - onChanged: (_) {}, - onTap: () {}, + OptimusSelectionCardVariant.vertical => _VerticalCard( + title: title, + description: description, + trailing: trailing, + isSelected: widget.isSelected, + selector: selector, ) - } - : null; - - final title = DefaultTextStyle.merge( - child: widget.title, - style: tokens.bodyLargeStrong.copyWith(color: titleColor), - ); - - final Widget? description = widget.description?.let( - (it) => DefaultTextStyle.merge( - child: it, - style: tokens.bodyMedium.copyWith(color: descriptionColor), - ), - ); - - final Widget? trailing = widget.trailing?.let( - (it) => IconTheme.merge( - child: it, - data: IconThemeData(color: titleColor), - ), - ); - - return GestureWrapper( - onHoverChanged: (isHovered) => - _controller.update(WidgetState.hovered, isHovered), - onPressedChanged: (isPressed) => - _controller.update(WidgetState.pressed, isPressed), - onTap: widget.onPressed, - child: DecoratedBox( - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.all( - widget.borderRadius.getBorderRadius(tokens), - ), - border: Border.all( - color: borderColor, - width: tokens.borderWidth150, - ), + }, ), - child: switch (widget.variant) { - OptimusSelectionCardVariant.horizontal => _HorizontalCard( - title: title, - description: description, - trailing: trailing, - isSelected: widget.isSelected, - selector: selector, - ), - OptimusSelectionCardVariant.vertical => _VerticalCard( - title: title, - description: description, - trailing: trailing, - isSelected: widget.isSelected, - selector: selector, - ) - }, - ), - ); - }, + ); + }, + ), ); } From 4b712ffbb69cfd9f3fe5f05b2492c46d47bdbdd3 Mon Sep 17 00:00:00 2001 From: Vitalij Vascenko Date: Thu, 15 Aug 2024 18:02:20 +0200 Subject: [PATCH 16/16] fix linter bug --- optimus/lib/src/selection_card.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/optimus/lib/src/selection_card.dart b/optimus/lib/src/selection_card.dart index c0323fc5..d632f2cf 100644 --- a/optimus/lib/src/selection_card.dart +++ b/optimus/lib/src/selection_card.dart @@ -145,6 +145,7 @@ class _OptimusSelectionCardState extends State CheckboxTick( isEnabled: widget.isEnabled, isChecked: widget.isSelected, + // ignore: prefer-boolean-prefixes, DCM bug onChanged: (_) {}, onTap: () {}, )