From 405a0680edfec5e8242763ff9f82c747c4ecd3a7 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62367544+tilucasoli@users.noreply.github.com> Date: Fri, 18 Oct 2024 14:07:58 -0300 Subject: [PATCH] feat: Menu Item Component (#508) * create menu item * menu item * review names --- .../lib/components/menu_item_use_case.dart | 54 +++ .../remix/demo/lib/main.directories.g.dart | 41 ++- packages/remix/lib/remix.dart | 1 + .../src/components/menu_item/menu_item.dart | 51 +++ .../src/components/menu_item/menu_item.g.dart | 335 ++++++++++++++++++ .../components/menu_item/menu_item_style.dart | 70 ++++ .../components/menu_item/menu_item_theme.dart | 53 +++ .../menu_item/menu_item_widget.dart | 60 ++++ packages/remix/lib/src/theme/remix_theme.dart | 8 + 9 files changed, 659 insertions(+), 14 deletions(-) create mode 100644 packages/remix/demo/lib/components/menu_item_use_case.dart create mode 100644 packages/remix/lib/src/components/menu_item/menu_item.dart create mode 100644 packages/remix/lib/src/components/menu_item/menu_item.g.dart create mode 100644 packages/remix/lib/src/components/menu_item/menu_item_style.dart create mode 100644 packages/remix/lib/src/components/menu_item/menu_item_theme.dart create mode 100644 packages/remix/lib/src/components/menu_item/menu_item_widget.dart diff --git a/packages/remix/demo/lib/components/menu_item_use_case.dart b/packages/remix/demo/lib/components/menu_item_use_case.dart new file mode 100644 index 000000000..5c8d4eb52 --- /dev/null +++ b/packages/remix/demo/lib/components/menu_item_use_case.dart @@ -0,0 +1,54 @@ +import 'package:demo/helpers/knob_builder.dart'; +import 'package:flutter/material.dart' hide Scaffold; +import 'package:remix/remix.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +final _key = GlobalKey(); + +@widgetbook.UseCase( + name: 'Menu Item Component', + type: MenuItem, +) +Widget buildButtonUseCase(BuildContext context) { + return KeyedSubtree( + key: _key, + child: Scaffold( + body: Center( + child: SizedBox( + width: 350, + child: MenuItem( + variants: [ + context.knobs.variant(FortalezaButtonStyle.variants), + ], + title: context.knobs.string( + label: 'Title', + initialValue: 'Menu Item', + ), + subtitle: context.knobs.stringOrNull( + label: 'Subtitle', + initialValue: 'Subtitle', + ), + onPress: () {}, + disabled: context.knobs.boolean( + label: 'Disabled', + initialValue: false, + ), + leadingWidgetBuilder: (icon) => context.knobs.boolean( + label: 'Show leading widget', + initialValue: false, + ) + ? Avatar(fallbackBuilder: (spec) => spec('LF')) + : const SizedBox.shrink(), + trailingWidgetBuilder: (icon) => context.knobs.boolean( + label: 'Show trailing widget', + initialValue: false, + ) + ? icon(Icons.chevron_right) + : const SizedBox.shrink(), + ), + ), + ), + ), + ); +} diff --git a/packages/remix/demo/lib/main.directories.g.dart b/packages/remix/demo/lib/main.directories.g.dart index 1db34b2b3..ef4171f99 100644 --- a/packages/remix/demo/lib/main.directories.g.dart +++ b/packages/remix/demo/lib/main.directories.g.dart @@ -20,13 +20,14 @@ import 'package:demo/components/chip_use_case.dart' as _i9; import 'package:demo/components/dialog_use_case.dart' as _i10; import 'package:demo/components/divider_use_case.dart' as _i11; import 'package:demo/components/icon_button_use_case.dart' as _i12; -import 'package:demo/components/progress_use_case.dart' as _i13; -import 'package:demo/components/radio_use_case.dart' as _i14; -import 'package:demo/components/segmented_control_use_case.dart' as _i15; -import 'package:demo/components/select_use_case.dart' as _i16; -import 'package:demo/components/spinner_use_case.dart' as _i17; -import 'package:demo/components/switch_use_case.dart' as _i18; -import 'package:demo/components/toast_use_case.dart' as _i19; +import 'package:demo/components/menu_item_use_case.dart' as _i13; +import 'package:demo/components/progress_use_case.dart' as _i14; +import 'package:demo/components/radio_use_case.dart' as _i15; +import 'package:demo/components/segmented_control_use_case.dart' as _i16; +import 'package:demo/components/select_use_case.dart' as _i17; +import 'package:demo/components/spinner_use_case.dart' as _i18; +import 'package:demo/components/switch_use_case.dart' as _i19; +import 'package:demo/components/toast_use_case.dart' as _i20; import 'package:widgetbook/widgetbook.dart' as _i1; final directories = <_i1.WidgetbookNode>[ @@ -165,6 +166,18 @@ final directories = <_i1.WidgetbookNode>[ ) ], ), + _i1.WidgetbookFolder( + name: 'menu_item', + children: [ + _i1.WidgetbookLeafComponent( + name: 'MenuItem', + useCase: _i1.WidgetbookUseCase( + name: 'Menu Item Component', + builder: _i13.buildButtonUseCase, + ), + ) + ], + ), _i1.WidgetbookFolder( name: 'progress', children: [ @@ -172,7 +185,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'Progress', useCase: _i1.WidgetbookUseCase( name: 'Progress Component', - builder: _i13.buildProgressUseCase, + builder: _i14.buildProgressUseCase, ), ) ], @@ -184,7 +197,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'Radio', useCase: _i1.WidgetbookUseCase( name: 'Radio Component', - builder: _i14.buildRadioUseCase, + builder: _i15.buildRadioUseCase, ), ) ], @@ -196,7 +209,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'SegmentedControl', useCase: _i1.WidgetbookUseCase( name: 'SegmentedControl Component', - builder: _i15.buildAccordionUseCase, + builder: _i16.buildAccordionUseCase, ), ) ], @@ -208,7 +221,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'Select', useCase: _i1.WidgetbookUseCase( name: 'Select Component', - builder: _i16.buildSelect, + builder: _i17.buildSelect, ), ) ], @@ -220,7 +233,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'Spinner', useCase: _i1.WidgetbookUseCase( name: 'Spinner Component', - builder: _i17.buildSpinnerUseCase, + builder: _i18.buildSpinnerUseCase, ), ) ], @@ -232,7 +245,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'Switch', useCase: _i1.WidgetbookUseCase( name: 'Switch Component', - builder: _i18.buildSwitchUseCase, + builder: _i19.buildSwitchUseCase, ), ) ], @@ -244,7 +257,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'Toast', useCase: _i1.WidgetbookUseCase( name: 'Toast Component', - builder: _i19.buildButtonUseCase, + builder: _i20.buildButtonUseCase, ), ) ], diff --git a/packages/remix/lib/remix.dart b/packages/remix/lib/remix.dart index 1b772a6b9..148dee049 100644 --- a/packages/remix/lib/remix.dart +++ b/packages/remix/lib/remix.dart @@ -33,6 +33,7 @@ export 'src/components/chip/chip.dart'; export 'src/components/dialog/dialog.dart'; export 'src/components/divider/divider.dart'; export 'src/components/icon_button/icon_button.dart'; +export 'src/components/menu_item/menu_item.dart'; export 'src/components/progress/progress.dart'; export 'src/components/radio/radio.dart'; export 'src/components/scaffold/scaffold.dart'; diff --git a/packages/remix/lib/src/components/menu_item/menu_item.dart b/packages/remix/lib/src/components/menu_item/menu_item.dart new file mode 100644 index 000000000..358dee982 --- /dev/null +++ b/packages/remix/lib/src/components/menu_item/menu_item.dart @@ -0,0 +1,51 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:mix/mix.dart'; +import 'package:mix_annotations/mix_annotations.dart'; + +import '../../helpers/component_builder.dart'; +import '../../theme/remix_theme.dart'; +import '../../theme/remix_tokens.dart'; + +part 'menu_item.g.dart'; +part 'menu_item_style.dart'; +part 'menu_item_theme.dart'; +part 'menu_item_widget.dart'; + +@MixableSpec() +class MenuItemSpec extends Spec + with _$MenuItemSpec, Diagnosticable { + final BoxSpec outerContainer; + final FlexSpec contentLayout; + final FlexSpec titleSubtitleLayout; + final IconSpec icon; + final TextSpec title; + final TextSpec subtitle; + + /// {@macro button_spec_of} + static const of = _$MenuItemSpec.of; + + static const from = _$MenuItemSpec.from; + + const MenuItemSpec({ + BoxSpec? outerContainer, + FlexSpec? contentLayout, + FlexSpec? titleSubtitleLayout, + IconSpec? icon, + TextSpec? title, + TextSpec? subtitle, + super.modifiers, + super.animated, + }) : outerContainer = outerContainer ?? const BoxSpec(), + contentLayout = contentLayout ?? const FlexSpec(), + titleSubtitleLayout = titleSubtitleLayout ?? const FlexSpec(), + icon = icon ?? const IconSpec(), + title = title ?? const TextSpec(), + subtitle = subtitle ?? const TextSpec(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + _debugFillProperties(properties); + } +} diff --git a/packages/remix/lib/src/components/menu_item/menu_item.g.dart b/packages/remix/lib/src/components/menu_item/menu_item.g.dart new file mode 100644 index 000000000..02b7bb9b8 --- /dev/null +++ b/packages/remix/lib/src/components/menu_item/menu_item.g.dart @@ -0,0 +1,335 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'menu_item.dart'; + +// ************************************************************************** +// MixableSpecGenerator +// ************************************************************************** + +mixin _$MenuItemSpec on Spec { + static MenuItemSpec from(MixData mix) { + return mix.attributeOf()?.resolve(mix) ?? + const MenuItemSpec(); + } + + /// {@template menu_item_spec_of} + /// Retrieves the [MenuItemSpec] from the nearest [Mix] ancestor in the widget tree. + /// + /// This method uses [Mix.of] to obtain the [Mix] instance associated with the + /// given [BuildContext], and then retrieves the [MenuItemSpec] from that [Mix]. + /// If no ancestor [Mix] is found, this method returns an empty [MenuItemSpec]. + /// + /// Example: + /// + /// ```dart + /// final menuItemSpec = MenuItemSpec.of(context); + /// ``` + /// {@endtemplate} + static MenuItemSpec of(BuildContext context) { + return _$MenuItemSpec.from(Mix.of(context)); + } + + /// Creates a copy of this [MenuItemSpec] but with the given fields + /// replaced with the new values. + @override + MenuItemSpec copyWith({ + BoxSpec? outerContainer, + FlexSpec? contentLayout, + FlexSpec? titleSubtitleLayout, + IconSpec? icon, + TextSpec? title, + TextSpec? subtitle, + WidgetModifiersData? modifiers, + AnimatedData? animated, + }) { + return MenuItemSpec( + outerContainer: outerContainer ?? _$this.outerContainer, + contentLayout: contentLayout ?? _$this.contentLayout, + titleSubtitleLayout: titleSubtitleLayout ?? _$this.titleSubtitleLayout, + icon: icon ?? _$this.icon, + title: title ?? _$this.title, + subtitle: subtitle ?? _$this.subtitle, + modifiers: modifiers ?? _$this.modifiers, + animated: animated ?? _$this.animated, + ); + } + + /// Linearly interpolates between this [MenuItemSpec] and another [MenuItemSpec] based on the given parameter [t]. + /// + /// The parameter [t] represents the interpolation factor, typically ranging from 0.0 to 1.0. + /// When [t] is 0.0, the current [MenuItemSpec] is returned. When [t] is 1.0, the [other] [MenuItemSpec] is returned. + /// For values of [t] between 0.0 and 1.0, an interpolated [MenuItemSpec] is returned. + /// + /// If [other] is null, this method returns the current [MenuItemSpec] instance. + /// + /// The interpolation is performed on each property of the [MenuItemSpec] using the appropriate + /// interpolation method: + /// + /// - [BoxSpec.lerp] for [outerContainer]. + /// - [FlexSpec.lerp] for [contentLayout] and [titleSubtitleLayout]. + /// - [IconSpec.lerp] for [icon]. + /// - [TextSpec.lerp] for [title] and [subtitle]. + + /// For [modifiers] and [animated], the interpolation is performed using a step function. + /// If [t] is less than 0.5, the value from the current [MenuItemSpec] is used. Otherwise, the value + /// from the [other] [MenuItemSpec] is used. + /// + /// This method is typically used in animations to smoothly transition between + /// different [MenuItemSpec] configurations. + @override + MenuItemSpec lerp(MenuItemSpec? other, double t) { + if (other == null) return _$this; + + return MenuItemSpec( + outerContainer: _$this.outerContainer.lerp(other.outerContainer, t), + contentLayout: _$this.contentLayout.lerp(other.contentLayout, t), + titleSubtitleLayout: + _$this.titleSubtitleLayout.lerp(other.titleSubtitleLayout, t), + icon: _$this.icon.lerp(other.icon, t), + title: _$this.title.lerp(other.title, t), + subtitle: _$this.subtitle.lerp(other.subtitle, t), + modifiers: other.modifiers, + animated: t < 0.5 ? _$this.animated : other.animated, + ); + } + + /// The list of properties that constitute the state of this [MenuItemSpec]. + /// + /// This property is used by the [==] operator and the [hashCode] getter to + /// compare two [MenuItemSpec] instances for equality. + @override + List get props => [ + _$this.outerContainer, + _$this.contentLayout, + _$this.titleSubtitleLayout, + _$this.icon, + _$this.title, + _$this.subtitle, + _$this.modifiers, + _$this.animated, + ]; + + MenuItemSpec get _$this => this as MenuItemSpec; + + void _debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties.add(DiagnosticsProperty('outerContainer', _$this.outerContainer, + defaultValue: null)); + properties.add(DiagnosticsProperty('contentLayout', _$this.contentLayout, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'titleSubtitleLayout', _$this.titleSubtitleLayout, + defaultValue: null)); + properties + .add(DiagnosticsProperty('icon', _$this.icon, defaultValue: null)); + properties + .add(DiagnosticsProperty('title', _$this.title, defaultValue: null)); + properties.add( + DiagnosticsProperty('subtitle', _$this.subtitle, defaultValue: null)); + properties.add( + DiagnosticsProperty('modifiers', _$this.modifiers, defaultValue: null)); + properties.add( + DiagnosticsProperty('animated', _$this.animated, defaultValue: null)); + } +} + +/// Represents the attributes of a [MenuItemSpec]. +/// +/// This class encapsulates properties defining the layout and +/// appearance of a [MenuItemSpec]. +/// +/// Use this class to configure the attributes of a [MenuItemSpec] and pass it to +/// the [MenuItemSpec] constructor. +class MenuItemSpecAttribute extends SpecAttribute + with Diagnosticable { + final BoxSpecAttribute? outerContainer; + final FlexSpecAttribute? contentLayout; + final FlexSpecAttribute? titleSubtitleLayout; + final IconSpecAttribute? icon; + final TextSpecAttribute? title; + final TextSpecAttribute? subtitle; + + const MenuItemSpecAttribute({ + this.outerContainer, + this.contentLayout, + this.titleSubtitleLayout, + this.icon, + this.title, + this.subtitle, + super.modifiers, + super.animated, + }); + + /// Resolves to [MenuItemSpec] using the provided [MixData]. + /// + /// If a property is null in the [MixData], it falls back to the + /// default value defined in the `defaultValue` for that property. + /// + /// ```dart + /// final menuItemSpec = MenuItemSpecAttribute(...).resolve(mix); + /// ``` + @override + MenuItemSpec resolve(MixData mix) { + return MenuItemSpec( + outerContainer: outerContainer?.resolve(mix), + contentLayout: contentLayout?.resolve(mix), + titleSubtitleLayout: titleSubtitleLayout?.resolve(mix), + icon: icon?.resolve(mix), + title: title?.resolve(mix), + subtitle: subtitle?.resolve(mix), + modifiers: modifiers?.resolve(mix), + animated: animated?.resolve(mix) ?? mix.animation, + ); + } + + /// Merges the properties of this [MenuItemSpecAttribute] with the properties of [other]. + /// + /// If [other] is null, returns this instance unchanged. Otherwise, returns a new + /// [MenuItemSpecAttribute] with the properties of [other] taking precedence over + /// the corresponding properties of this instance. + /// + /// Properties from [other] that are null will fall back + /// to the values from this instance. + @override + MenuItemSpecAttribute merge(covariant MenuItemSpecAttribute? other) { + if (other == null) return this; + + return MenuItemSpecAttribute( + outerContainer: + outerContainer?.merge(other.outerContainer) ?? other.outerContainer, + contentLayout: + contentLayout?.merge(other.contentLayout) ?? other.contentLayout, + titleSubtitleLayout: + titleSubtitleLayout?.merge(other.titleSubtitleLayout) ?? + other.titleSubtitleLayout, + icon: icon?.merge(other.icon) ?? other.icon, + title: title?.merge(other.title) ?? other.title, + subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle, + modifiers: modifiers?.merge(other.modifiers) ?? other.modifiers, + animated: animated?.merge(other.animated) ?? other.animated, + ); + } + + /// The list of properties that constitute the state of this [MenuItemSpecAttribute]. + /// + /// This property is used by the [==] operator and the [hashCode] getter to + /// compare two [MenuItemSpecAttribute] instances for equality. + @override + List get props => [ + outerContainer, + contentLayout, + titleSubtitleLayout, + icon, + title, + subtitle, + modifiers, + animated, + ]; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('outerContainer', outerContainer, + defaultValue: null)); + properties.add(DiagnosticsProperty('contentLayout', contentLayout, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'titleSubtitleLayout', titleSubtitleLayout, + defaultValue: null)); + properties.add(DiagnosticsProperty('icon', icon, defaultValue: null)); + properties.add(DiagnosticsProperty('title', title, defaultValue: null)); + properties + .add(DiagnosticsProperty('subtitle', subtitle, defaultValue: null)); + properties + .add(DiagnosticsProperty('modifiers', modifiers, defaultValue: null)); + properties + .add(DiagnosticsProperty('animated', animated, defaultValue: null)); + } +} + +/// Utility class for configuring [MenuItemSpec] properties. +/// +/// This class provides methods to set individual properties of a [MenuItemSpec]. +/// Use the methods of this class to configure specific properties of a [MenuItemSpec]. +class MenuItemSpecUtility + extends SpecUtility { + /// Utility for defining [MenuItemSpecAttribute.outerContainer] + late final outerContainer = BoxSpecUtility((v) => only(outerContainer: v)); + + /// Utility for defining [MenuItemSpecAttribute.contentLayout] + late final contentLayout = FlexSpecUtility((v) => only(contentLayout: v)); + + /// Utility for defining [MenuItemSpecAttribute.titleSubtitleLayout] + late final titleSubtitleLayout = + FlexSpecUtility((v) => only(titleSubtitleLayout: v)); + + /// Utility for defining [MenuItemSpecAttribute.icon] + late final icon = IconSpecUtility((v) => only(icon: v)); + + /// Utility for defining [MenuItemSpecAttribute.title] + late final title = TextSpecUtility((v) => only(title: v)); + + /// Utility for defining [MenuItemSpecAttribute.subtitle] + late final subtitle = TextSpecUtility((v) => only(subtitle: v)); + + /// Utility for defining [MenuItemSpecAttribute.modifiers] + late final wrap = SpecModifierUtility((v) => only(modifiers: v)); + + /// Utility for defining [MenuItemSpecAttribute.animated] + late final animated = AnimatedUtility((v) => only(animated: v)); + + MenuItemSpecUtility(super.builder, {super.mutable}); + + MenuItemSpecUtility get chain => + MenuItemSpecUtility(attributeBuilder, mutable: true); + + static MenuItemSpecUtility get self => + MenuItemSpecUtility((v) => v); + + /// Returns a new [MenuItemSpecAttribute] with the specified properties. + @override + T only({ + BoxSpecAttribute? outerContainer, + FlexSpecAttribute? contentLayout, + FlexSpecAttribute? titleSubtitleLayout, + IconSpecAttribute? icon, + TextSpecAttribute? title, + TextSpecAttribute? subtitle, + WidgetModifiersDataDto? modifiers, + AnimatedDataDto? animated, + }) { + return builder(MenuItemSpecAttribute( + outerContainer: outerContainer, + contentLayout: contentLayout, + titleSubtitleLayout: titleSubtitleLayout, + icon: icon, + title: title, + subtitle: subtitle, + modifiers: modifiers, + animated: animated, + )); + } +} + +/// A tween that interpolates between two [MenuItemSpec] instances. +/// +/// This class can be used in animations to smoothly transition between +/// different [MenuItemSpec] specifications. +class MenuItemSpecTween extends Tween { + MenuItemSpecTween({ + super.begin, + super.end, + }); + + @override + MenuItemSpec lerp(double t) { + if (begin == null && end == null) { + return const MenuItemSpec(); + } + + if (begin == null) { + return end!; + } + + return begin!.lerp(end!, t); + } +} diff --git a/packages/remix/lib/src/components/menu_item/menu_item_style.dart b/packages/remix/lib/src/components/menu_item/menu_item_style.dart new file mode 100644 index 000000000..b30b58856 --- /dev/null +++ b/packages/remix/lib/src/components/menu_item/menu_item_style.dart @@ -0,0 +1,70 @@ +part of 'menu_item.dart'; + +class MenuItemStyle extends SpecStyle { + const MenuItemStyle(); + + @override + Style makeStyle(SpecConfiguration spec) { + final $ = spec.utilities; + + final titleSubtitleLayout = $.titleSubtitleLayout.chain + ..mainAxisAlignment.start() + ..crossAxisAlignment.start() + ..mainAxisSize.min() + ..wrap.expanded() + ..gap(4.0); + + final contentLayout = $.contentLayout.gap(12.0); + + final title = $.title.style.fontSize(14.0); + + final subtitle = $.subtitle.chain + ..style.fontSize(12.0) + ..style.color.grey.shade600() + ..maxLines(2); + + final outerContainer = $.outerContainer.chain + ..padding(12) + ..borderRadius(12); + + final icon = $.icon.chain + ..size(20) + ..color.black87(); + + final disabled = spec.on.disabled( + $.title.style.color.grey.shade600(), + $.subtitle.style.color.grey.shade400(), + ); + + return Style.create([ + titleSubtitleLayout, + contentLayout, + title, + subtitle, + outerContainer, + icon, + spec.on.disabled(disabled), + ]); + } +} + +class MenuItemDarkStyle extends MenuItemStyle { + const MenuItemDarkStyle(); + + @override + Style makeStyle(SpecConfiguration spec) { + final $ = spec.utilities; + + final disabled = spec.on.disabled( + $.title.style.color.grey.shade400(), + $.subtitle.style.color.grey.shade700(), + ); + + return Style.create([ + super.makeStyle(spec).call(), + $.title.style.color.white(), + $.icon.color.white(), + spec.on.disabled(disabled), + ]); + } +} diff --git a/packages/remix/lib/src/components/menu_item/menu_item_theme.dart b/packages/remix/lib/src/components/menu_item/menu_item_theme.dart new file mode 100644 index 000000000..ba541402e --- /dev/null +++ b/packages/remix/lib/src/components/menu_item/menu_item_theme.dart @@ -0,0 +1,53 @@ +part of 'menu_item.dart'; + +class FortalezaMenuItemStyle extends MenuItemStyle { + const FortalezaMenuItemStyle(); + + @override + Style makeStyle(SpecConfiguration spec) { + final $ = spec.utilities; + + final baseStyle = super.makeStyle(spec); + final titleSubtitleLayout = $.titleSubtitleLayout.chain..gap.$space(1); + + final contentLayout = $.contentLayout.chain..gap.$space(3); + final title = $.title.chain + ..style.$text(2) + ..style.color.resetDirectives() + ..style.color.$neutral(12); + + final subtitle = $.subtitle.chain + ..style.$text(1) + ..style.color.resetDirectives() + ..style.color.$neutral(9); + + final outerContainer = $.outerContainer.chain + ..padding.all.$space(3) + ..padding.right.$space(4) + ..borderRadius.all.$radius(2); + + final icon = $.icon.color.$neutral(11); + + final hovered = $.outerContainer.color.$accent(2); + + final disabled = $.chain + ..title.style.color.$neutral(9) + ..subtitle.style.color.$neutral(8) + ..icon.color.$neutral(8); + + return Style.create([ + baseStyle(), + titleSubtitleLayout, + contentLayout, + title, + subtitle, + outerContainer, + icon, + spec.on.hover(hovered), + spec.on.disabled(disabled), + ]).animate( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + ); + } +} diff --git a/packages/remix/lib/src/components/menu_item/menu_item_widget.dart b/packages/remix/lib/src/components/menu_item/menu_item_widget.dart new file mode 100644 index 000000000..7f83ed2e1 --- /dev/null +++ b/packages/remix/lib/src/components/menu_item/menu_item_widget.dart @@ -0,0 +1,60 @@ +part of 'menu_item.dart'; + +class MenuItem extends StatelessWidget { + const MenuItem({ + super.key, + required this.title, + this.subtitle, + this.leadingWidgetBuilder, + this.trailingWidgetBuilder, + this.onPress, + this.variants = const [], + this.style, + this.disabled = false, + }); + + final String title; + final String? subtitle; + final VoidCallback? onPress; + final WidgetSpecBuilder? leadingWidgetBuilder; + final WidgetSpecBuilder? trailingWidgetBuilder; + final bool disabled; + final List variants; + final MenuItemStyle? style; + + @override + Widget build(BuildContext context) { + final style = this.style ?? context.remix.components.menuItem; + final configuration = SpecConfiguration(context, MenuItemSpecUtility.self); + + return Pressable( + enabled: !disabled, + onPress: disabled ? null : onPress, + child: SpecBuilder( + style: style.makeStyle(configuration).applyVariants([...variants]), + builder: (context) { + final spec = MenuItemSpec.of(context); + + return spec.outerContainer( + child: spec.contentLayout( + direction: Axis.horizontal, + children: [ + if (leadingWidgetBuilder != null) + leadingWidgetBuilder!(spec.icon), + spec.titleSubtitleLayout( + direction: Axis.vertical, + children: [ + spec.title(title), + if (subtitle != null) spec.subtitle(subtitle!), + ], + ), + if (trailingWidgetBuilder != null) + trailingWidgetBuilder!(spec.icon), + ], + ), + ); + }, + ), + ); + } +} diff --git a/packages/remix/lib/src/theme/remix_theme.dart b/packages/remix/lib/src/theme/remix_theme.dart index 4d32663a8..dc0629e76 100644 --- a/packages/remix/lib/src/theme/remix_theme.dart +++ b/packages/remix/lib/src/theme/remix_theme.dart @@ -13,6 +13,7 @@ import '../components/chip/chip.dart'; import '../components/dialog/dialog.dart'; import '../components/divider/divider.dart'; import '../components/icon_button/icon_button.dart'; +import '../components/menu_item/menu_item.dart'; import '../components/progress/progress.dart'; import '../components/radio/radio.dart'; import '../components/scaffold/scaffold.dart'; @@ -35,6 +36,7 @@ class RemixComponentTheme { final ChipStyle chip; final DividerStyle divider; final IconButtonStyle iconButton; + final MenuItemStyle menuItem; final ProgressStyle progress; final RadioStyle radio; final ScaffoldStyle scaffold; @@ -56,6 +58,7 @@ class RemixComponentTheme { required this.chip, required this.divider, required this.iconButton, + required this.menuItem, required this.progress, required this.radio, required this.scaffold, @@ -79,6 +82,7 @@ class RemixComponentTheme { chip: ChipStyle(), divider: DividerStyle(), iconButton: IconButtonStyle(), + menuItem: MenuItemStyle(), progress: ProgressStyle(), radio: RadioStyle(), scaffold: ScaffoldStyle(), @@ -103,6 +107,7 @@ class RemixComponentTheme { chip: const ChipDarkStyle(), divider: const DividerDarkStyle(), iconButton: const IconButtonDarkStyle(), + menuItem: const MenuItemDarkStyle(), progress: const ProgressDarkStyle(), radio: const RadioDarkStyle(), scaffold: const ScaffoldDarkStyle(), @@ -126,6 +131,7 @@ class RemixComponentTheme { chip: FortalezaChipStyle(), divider: FortalezaDividerStyle(), iconButton: FortalezaIconButtonStyle(), + menuItem: FortalezaMenuItemStyle(), progress: FortalezaProgressStyle(), radio: FortalezaRadioStyle(), scaffold: FortalezaScaffoldStyle(), @@ -159,6 +165,7 @@ class RemixComponentTheme { ChipStyle? chip, DividerStyle? divider, IconButtonStyle? iconButton, + MenuItemStyle? menuItem, ProgressStyle? progress, RadioStyle? radio, ScaffoldStyle? scaffold, @@ -180,6 +187,7 @@ class RemixComponentTheme { chip: chip ?? this.chip, divider: divider ?? this.divider, iconButton: iconButton ?? this.iconButton, + menuItem: menuItem ?? this.menuItem, progress: progress ?? this.progress, radio: radio ?? this.radio, scaffold: scaffold ?? this.scaffold,