From a491dc735b1717b2e4099c0a53f8b7c9c81a33b7 Mon Sep 17 00:00:00 2001 From: Harry Sild <46851868+Kypsis@users.noreply.github.com> Date: Mon, 3 Apr 2023 18:22:05 +0300 Subject: [PATCH] feat: [MDS-463] Create Alert widget (#97) Co-authored-by: BirgittMajas <79840500+BirgittMajas@users.noreply.github.com> --- example/lib/src/storybook/stories/alert.dart | 253 ++++++++++++++ example/lib/src/storybook/storybook.dart | 2 + lib/moon_design.dart | 4 + lib/src/theme/alert/alert_colors.dart | 68 ++++ lib/src/theme/alert/alert_properties.dart | 119 +++++++ lib/src/theme/alert/alert_theme.dart | 59 ++++ lib/src/theme/theme.dart | 11 + lib/src/widgets/alert/alert.dart | 327 +++++++++++++++++++ lib/src/widgets/alert/filled_alert.dart | 80 +++++ lib/src/widgets/alert/outlined_alert.dart | 90 +++++ lib/src/widgets/buttons/text_button.dart | 1 - 11 files changed, 1013 insertions(+), 1 deletion(-) create mode 100644 example/lib/src/storybook/stories/alert.dart create mode 100644 lib/src/theme/alert/alert_colors.dart create mode 100644 lib/src/theme/alert/alert_properties.dart create mode 100644 lib/src/theme/alert/alert_theme.dart create mode 100644 lib/src/widgets/alert/alert.dart create mode 100644 lib/src/widgets/alert/filled_alert.dart create mode 100644 lib/src/widgets/alert/outlined_alert.dart diff --git a/example/lib/src/storybook/stories/alert.dart b/example/lib/src/storybook/stories/alert.dart new file mode 100644 index 00000000..b8b999ce --- /dev/null +++ b/example/lib/src/storybook/stories/alert.dart @@ -0,0 +1,253 @@ +import 'package:example/src/storybook/common/color_options.dart'; +import 'package:example/src/storybook/common/widgets/text_divider.dart'; +import 'package:flutter/material.dart'; +import 'package:moon_design/moon_design.dart'; +import 'package:storybook_flutter/storybook_flutter.dart'; + +bool showAlert = true; + +class AlertStory extends Story { + AlertStory() + : super( + name: "Alert", + builder: (context) { + final backgroundColorsKnob = context.knobs.options( + label: "backgroundColor", + description: "MoonColors variants for background.", + initial: 4, // Gohan + options: colorOptions, + ); + + final backgroundColor = colorTable(context)[backgroundColorsKnob]; + + final textColorsKnob = context.knobs.options( + label: "textColor", + description: "MoonColors variants for text.", + initial: 5, // Bulma + options: colorOptions, + ); + + final textColor = colorTable(context)[textColorsKnob]; + + final leadingColorsKnob = context.knobs.options( + label: "leadingColor", + description: "MoonColors variants for leading.", + initial: 5, // Bulma + options: colorOptions, + ); + + final leadingColor = colorTable(context)[leadingColorsKnob]; + + final trailingColorsKnob = context.knobs.options( + label: "trailingColor", + description: "MoonColors variants for trailing.", + initial: 5, // Bulma + options: colorOptions, + ); + + final trailingColor = colorTable(context)[trailingColorsKnob]; + + final borderColorsKnob = context.knobs.options( + label: "borderColor", + description: "MoonColors variants for border.", + initial: 5, // Bulma + options: colorOptions, + ); + + final borderColor = colorTable(context)[borderColorsKnob]; + + final borderRadiusKnob = context.knobs.sliderInt( + max: 12, + initial: 8, + label: "borderRadius", + description: "Border radius for Alert.", + ); + + final showBorderKnob = context.knobs.boolean( + label: "showBorder", + description: "Show border for Alert.", + ); + + final showLeadingKnob = context.knobs.boolean( + label: "leading", + description: "Show widget in the leading slot.", + initial: true, + ); + + final showBodyKnob = context.knobs.boolean( + label: "body", + description: "Show widget in the body slot.", + ); + + final showTrailingKnob = context.knobs.boolean( + label: "trailing", + description: "Show widget in the trailing slot.", + initial: true, + ); + + final showDisabledKnob = context.knobs.boolean( + label: "Disabled", + description: "onTrailingTap() is null.", + ); + + final setRtlModeKnob = context.knobs.boolean( + label: "RTL mode", + description: "Switch between LTR and RTL modes.", + ); + + return Directionality( + textDirection: setRtlModeKnob ? TextDirection.rtl : TextDirection.ltr, + child: Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 64), + const TextDivider(text: "Base Alert"), + const SizedBox(height: 32), + StatefulBuilder( + builder: (context, setState) { + return Column( + children: [ + MoonAlert( + show: showAlert, + title: const SizedBox( + height: 24, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text("Base Alert"), + ), + ), + borderRadius: BorderRadius.circular(borderRadiusKnob.toDouble()), + leading: showLeadingKnob ? const Icon(MoonIconsOther.frame24) : null, + trailing: showTrailingKnob + ? MoonButton.icon( + buttonSize: MoonButtonSize.xs, + borderRadius: BorderRadius.circular(borderRadiusKnob.toDouble()), + disabledOpacityValue: 1, + icon: Icon( + MoonIconsControls.close_small24, + color: trailingColor, + size: 24, + ), + gap: 0, + onTap: showDisabledKnob + ? null + : () { + setState(() => showAlert = !showAlert); + }, + ) + : null, + backgroundColor: backgroundColor, + showBorder: showBorderKnob, + leadingColor: leadingColor, + trailingColor: trailingColor, + textColor: textColor, + borderColor: borderColor, + body: showBodyKnob + ? const SizedBox( + height: 24, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text("Here goes Alert body"), + ), + ) + : null, + ), + const SizedBox(height: 16), + MoonFilledButton( + label: const Text("Show/Hide Alert"), + backgroundColor: context.moonColors!.piccolo, + onTap: () { + setState(() => showAlert = !showAlert); + }, + ), + ], + ); + }, + ), + const SizedBox(height: 40), + const TextDivider(text: "Filled Alert variant"), + const SizedBox(height: 32), + MoonFilledAlert( + show: true, + title: const Text("Filled error Alert"), + borderRadius: BorderRadius.circular(borderRadiusKnob.toDouble()), + leading: showLeadingKnob ? const Icon(MoonIconsNotifications.alert24) : null, + color: context.moonColors!.chiChi100, + body: showBodyKnob + ? const SizedBox( + height: 24, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text("Here goes Alert body"), + ), + ) + : null, + onTrailingTap: () {}, + ), + const SizedBox(height: 16), + MoonFilledAlert( + show: true, + title: const Text("Filled warning Alert"), + borderRadius: BorderRadius.circular(borderRadiusKnob.toDouble()), + leading: showLeadingKnob ? const Icon(MoonIconsGeneric.alarm24) : null, + color: context.moonColors!.krillin100, + body: showBodyKnob + ? const SizedBox( + height: 24, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text("Here goes Alert body"), + ), + ) + : null, + onTrailingTap: () {}, + ), + const SizedBox(height: 40), + const TextDivider(text: "Outlined Alert variant"), + const SizedBox(height: 32), + MoonOutlinedAlert( + show: true, + title: const Text("Outlined success Alert"), + borderRadius: BorderRadius.circular(borderRadiusKnob.toDouble()), + leading: showLeadingKnob ? const Icon(MoonIconsGeneric.check_rounded24) : null, + color: context.moonColors!.roshi100, + body: showBodyKnob + ? const SizedBox( + height: 24, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text("Here goes Alert body"), + ), + ) + : null, + onTrailingTap: () {}, + ), + const SizedBox(height: 16), + MoonOutlinedAlert( + show: true, + title: const Text('Outlined info Alert'), + borderRadius: BorderRadius.circular(borderRadiusKnob.toDouble()), + leading: showLeadingKnob ? const Icon(MoonIconsNotifications.alert24) : null, + color: context.moonColors!.whis100, + body: showBodyKnob + ? const SizedBox( + height: 24, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text("Here goes Alert body"), + ), + ) + : null, + onTrailingTap: () {}, + ), + const SizedBox(height: 64), + ], + ), + ), + ), + ); + }, + ); +} diff --git a/example/lib/src/storybook/storybook.dart b/example/lib/src/storybook/storybook.dart index f16f6595..b8c8bc61 100644 --- a/example/lib/src/storybook/storybook.dart +++ b/example/lib/src/storybook/storybook.dart @@ -1,5 +1,6 @@ import 'package:example/src/storybook/common/widgets/version.dart'; import 'package:example/src/storybook/stories/accordion.dart'; +import 'package:example/src/storybook/stories/alert.dart'; import 'package:example/src/storybook/stories/avatar.dart'; import 'package:example/src/storybook/stories/button.dart'; import 'package:example/src/storybook/stories/checkbox.dart'; @@ -70,6 +71,7 @@ class StorybookPage extends StatelessWidget { ), stories: [ AccordionStory(), + AlertStory(), AvatarStory(), ButtonStory(), CheckboxStory(), diff --git a/lib/moon_design.dart b/lib/moon_design.dart index d92bdfa3..0b20cc8d 100644 --- a/lib/moon_design.dart +++ b/lib/moon_design.dart @@ -1,6 +1,7 @@ library moon_design; export 'package:moon_design/src/theme/accordion/accordion_theme.dart'; +export 'package:moon_design/src/theme/alert/alert_theme.dart'; export 'package:moon_design/src/theme/avatar/avatar_theme.dart'; export 'package:moon_design/src/theme/borders.dart'; export 'package:moon_design/src/theme/button/button_theme.dart'; @@ -30,6 +31,9 @@ export 'package:moon_design/src/utils/measure_size.dart'; export 'package:moon_design/src/utils/widget_surveyor.dart'; export 'package:moon_design/src/widgets/accordion/accordion_item.dart'; +export 'package:moon_design/src/widgets/alert/alert.dart'; +export 'package:moon_design/src/widgets/alert/filled_alert.dart'; +export 'package:moon_design/src/widgets/alert/outlined_alert.dart'; export 'package:moon_design/src/widgets/avatar/avatar.dart'; export 'package:moon_design/src/widgets/buttons/button.dart'; export 'package:moon_design/src/widgets/buttons/filled_button.dart'; diff --git a/lib/src/theme/alert/alert_colors.dart b/lib/src/theme/alert/alert_colors.dart new file mode 100644 index 00000000..47981f8b --- /dev/null +++ b/lib/src/theme/alert/alert_colors.dart @@ -0,0 +1,68 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/colors.dart'; + +@immutable +class MoonAlertColors extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonAlertColors( + backgroundColor: MoonColors.light.gohan, + borderColor: MoonColors.light.bulma, + outlinedVariantColor: MoonColors.light.bulma, + ); + + static final dark = MoonAlertColors( + backgroundColor: MoonColors.dark.gohan, + borderColor: MoonColors.dark.trunks, + outlinedVariantColor: MoonColors.dark.bulma, + ); + + /// Alert background color. + final Color backgroundColor; + + /// Alert border color. + final Color borderColor; + + /// Alert text color. + final Color outlinedVariantColor; + + const MoonAlertColors({ + required this.backgroundColor, + required this.borderColor, + required this.outlinedVariantColor, + }); + + @override + MoonAlertColors copyWith({ + Color? backgroundColor, + Color? borderColor, + Color? outlinedVariantColor, + }) { + return MoonAlertColors( + backgroundColor: backgroundColor ?? this.backgroundColor, + borderColor: borderColor ?? this.borderColor, + outlinedVariantColor: outlinedVariantColor ?? this.outlinedVariantColor, + ); + } + + @override + MoonAlertColors lerp(ThemeExtension? other, double t) { + if (other is! MoonAlertColors) return this; + + return MoonAlertColors( + backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t)!, + borderColor: Color.lerp(borderColor, other.borderColor, t)!, + outlinedVariantColor: Color.lerp(outlinedVariantColor, other.outlinedVariantColor, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonAlertColors")) + ..add(ColorProperty("backgroundColor", backgroundColor)) + ..add(ColorProperty("borderColor", borderColor)) + ..add(ColorProperty("outlinedVariantColor", outlinedVariantColor)); + } +} diff --git a/lib/src/theme/alert/alert_properties.dart b/lib/src/theme/alert/alert_properties.dart new file mode 100644 index 00000000..6011c7a9 --- /dev/null +++ b/lib/src/theme/alert/alert_properties.dart @@ -0,0 +1,119 @@ +import 'dart:ui'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/borders.dart'; +import 'package:moon_design/src/theme/sizes.dart'; +import 'package:moon_design/src/theme/typography/text_styles.dart'; + +@immutable +class MoonAlertProperties extends ThemeExtension with DiagnosticableTreeMixin { + static final properties = MoonAlertProperties( + borderRadius: MoonBorders.borders.interactiveSm, + horizontalGap: MoonSizes.sizes.x3s, + minimumHeight: MoonSizes.sizes.xl, + verticalGap: MoonSizes.sizes.x4s, + transitionDuration: const Duration(milliseconds: 200), + transitionCurve: Curves.easeInOutCubic, + padding: EdgeInsets.all(MoonSizes.sizes.x2s), + bodyTextStyle: MoonTextStyles.body.text14, + titleTextStyle: MoonTextStyles.heading.text14, + ); + + /// Border radius for alert. + final BorderRadius borderRadius; + + /// Horizontal space between alert leading, trailing and title. + final double horizontalGap; + + /// Alert Widget minimum height. + final double minimumHeight; + + /// Vertical space between alert header and body. + final double verticalGap; + + /// Alert transition duration (show and hide animation). + final Duration transitionDuration; + + /// Alert transition curve (show and hide animation). + final Curve transitionCurve; + + /// Alert padding. + final EdgeInsets padding; + + /// Alert body text style. + final TextStyle bodyTextStyle; + + /// Alert title text style. + final TextStyle titleTextStyle; + + const MoonAlertProperties({ + required this.borderRadius, + required this.horizontalGap, + required this.minimumHeight, + required this.verticalGap, + required this.transitionDuration, + required this.transitionCurve, + required this.padding, + required this.bodyTextStyle, + required this.titleTextStyle, + }); + + @override + MoonAlertProperties copyWith({ + BorderRadius? borderRadius, + double? horizontalGap, + double? minimumHeight, + double? verticalGap, + Duration? transitionDuration, + Curve? transitionCurve, + EdgeInsets? padding, + TextStyle? bodyTextStyle, + TextStyle? titleTextStyle, + }) { + return MoonAlertProperties( + borderRadius: borderRadius ?? this.borderRadius, + horizontalGap: horizontalGap ?? this.horizontalGap, + minimumHeight: minimumHeight ?? this.minimumHeight, + verticalGap: verticalGap ?? this.verticalGap, + transitionDuration: transitionDuration ?? this.transitionDuration, + transitionCurve: transitionCurve ?? this.transitionCurve, + padding: padding ?? this.padding, + bodyTextStyle: bodyTextStyle ?? this.bodyTextStyle, + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + ); + } + + @override + MoonAlertProperties lerp(ThemeExtension? other, double t) { + if (other is! MoonAlertProperties) return this; + + return MoonAlertProperties( + borderRadius: BorderRadius.lerp(borderRadius, other.borderRadius, t)!, + horizontalGap: lerpDouble(horizontalGap, other.horizontalGap, t)!, + minimumHeight: lerpDouble(minimumHeight, other.minimumHeight, t)!, + verticalGap: lerpDouble(verticalGap, other.verticalGap, t)!, + transitionDuration: lerpDuration(transitionDuration, other.transitionDuration, t), + transitionCurve: other.transitionCurve, + padding: EdgeInsets.lerp(padding, other.padding, t)!, + bodyTextStyle: TextStyle.lerp(bodyTextStyle, other.bodyTextStyle, t)!, + titleTextStyle: TextStyle.lerp(titleTextStyle, other.titleTextStyle, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonAlertProperties")) + ..add(DiagnosticsProperty("borderRadius", borderRadius)) + ..add(DiagnosticsProperty("horizontalGap", horizontalGap)) + ..add(DiagnosticsProperty("minimumHeight", minimumHeight)) + ..add(DiagnosticsProperty("verticalGap", verticalGap)) + ..add(DiagnosticsProperty("transitionDuration", transitionDuration)) + ..add(DiagnosticsProperty("transitionCurve", transitionCurve)) + ..add(DiagnosticsProperty("padding", padding)) + ..add(DiagnosticsProperty("contentTextStyle", bodyTextStyle)) + ..add(DiagnosticsProperty("titleTextStyle", titleTextStyle)); + } +} diff --git a/lib/src/theme/alert/alert_theme.dart b/lib/src/theme/alert/alert_theme.dart new file mode 100644 index 00000000..f45873ba --- /dev/null +++ b/lib/src/theme/alert/alert_theme.dart @@ -0,0 +1,59 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/alert/alert_colors.dart'; +import 'package:moon_design/src/theme/alert/alert_properties.dart'; + +@immutable +class MoonAlertTheme extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonAlertTheme( + colors: MoonAlertColors.light, + properties: MoonAlertProperties.properties, + ); + + static final dark = MoonAlertTheme( + colors: MoonAlertColors.dark, + properties: MoonAlertProperties.properties, + ); + + /// Alert colors. + final MoonAlertColors colors; + + /// Alert properties. + final MoonAlertProperties properties; + + const MoonAlertTheme({ + required this.colors, + required this.properties, + }); + + @override + MoonAlertTheme copyWith({ + MoonAlertColors? colors, + MoonAlertProperties? properties, + }) { + return MoonAlertTheme( + colors: colors ?? this.colors, + properties: properties ?? this.properties, + ); + } + + @override + MoonAlertTheme lerp(ThemeExtension? other, double t) { + if (other is! MoonAlertTheme) return this; + + return MoonAlertTheme( + colors: colors.lerp(other.colors, t), + properties: properties.lerp(other.properties, t), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder diagnosticProperties) { + super.debugFillProperties(diagnosticProperties); + diagnosticProperties + ..add(DiagnosticsProperty("type", "MoonAlertTheme")) + ..add(DiagnosticsProperty("colors", colors)) + ..add(DiagnosticsProperty("properties", properties)); + } +} diff --git a/lib/src/theme/theme.dart b/lib/src/theme/theme.dart index 5dff8fa2..3c7fe63c 100644 --- a/lib/src/theme/theme.dart +++ b/lib/src/theme/theme.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:moon_design/src/theme/accordion/accordion_theme.dart'; +import 'package:moon_design/src/theme/alert/alert_theme.dart'; import 'package:moon_design/src/theme/avatar/avatar_theme.dart'; import 'package:moon_design/src/theme/borders.dart'; import 'package:moon_design/src/theme/button/button_theme.dart'; @@ -28,6 +29,7 @@ import 'package:moon_design/src/theme/typography/typography.dart'; class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { static final light = MoonTheme( accordionTheme: MoonAccordionTheme.light, + alertTheme: MoonAlertTheme.light, avatarTheme: MoonAvatarTheme.light, borders: MoonBorders.borders, buttonTheme: MoonButtonTheme.light, @@ -53,6 +55,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { static final dark = MoonTheme( accordionTheme: MoonAccordionTheme.dark, + alertTheme: MoonAlertTheme.dark, avatarTheme: MoonAvatarTheme.dark, borders: MoonBorders.borders, buttonTheme: MoonButtonTheme.dark, @@ -79,6 +82,9 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { /// Moon Design System MoonAccordion widget theming. final MoonAccordionTheme accordionTheme; + /// Moon Design System MoonAlert widget theming. + final MoonAlertTheme alertTheme; + /// Moon Design System MoonAvatar widget theming. final MoonAvatarTheme avatarTheme; @@ -144,6 +150,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { const MoonTheme({ required this.accordionTheme, + required this.alertTheme, required this.avatarTheme, required this.borders, required this.buttonTheme, @@ -170,6 +177,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { @override MoonTheme copyWith({ MoonAccordionTheme? accordionTheme, + MoonAlertTheme? alertTheme, MoonAvatarTheme? avatarTheme, MoonBorders? borders, MoonButtonTheme? buttonTheme, @@ -194,6 +202,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { }) { return MoonTheme( accordionTheme: accordionTheme ?? this.accordionTheme, + alertTheme: alertTheme ?? this.alertTheme, avatarTheme: avatarTheme ?? this.avatarTheme, borders: borders ?? this.borders, buttonTheme: buttonTheme ?? this.buttonTheme, @@ -224,6 +233,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { return MoonTheme( accordionTheme: accordionTheme.lerp(other.accordionTheme, t), + alertTheme: alertTheme.lerp(other.alertTheme, t), avatarTheme: avatarTheme.lerp(other.avatarTheme, t), borders: borders.lerp(other.borders, t), buttonTheme: buttonTheme.lerp(other.buttonTheme, t), @@ -254,6 +264,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { properties ..add(DiagnosticsProperty("type", "MoonTheme")) ..add(DiagnosticsProperty("MoonAccordionTheme", accordionTheme)) + ..add(DiagnosticsProperty("MoonAlertTheme", alertTheme)) ..add(DiagnosticsProperty("MoonAvatarTheme", avatarTheme)) ..add(DiagnosticsProperty("MoonBorders", borders)) ..add(DiagnosticsProperty("MoonButtonTheme", buttonTheme)) diff --git a/lib/src/widgets/alert/alert.dart b/lib/src/widgets/alert/alert.dart new file mode 100644 index 00000000..c4435dcd --- /dev/null +++ b/lib/src/widgets/alert/alert.dart @@ -0,0 +1,327 @@ +import 'package:figma_squircle/figma_squircle.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/borders.dart'; +import 'package:moon_design/src/theme/colors.dart'; +import 'package:moon_design/src/theme/sizes.dart'; +import 'package:moon_design/src/theme/theme.dart'; +import 'package:moon_design/src/theme/typography/text_styles.dart'; +import 'package:moon_design/src/utils/extensions.dart'; +import 'package:moon_design/src/widgets/common/animated_icon_theme.dart'; + +class MoonAlert extends StatefulWidget { + /// Controls whether the alert is shown. + final bool show; + + /// Whether the alert should show a border. + final bool showBorder; + + /// The border radius of the alert. + final BorderRadius? borderRadius; + + /// The background color of the alert. + final Color? backgroundColor; + + /// The border color of the alert. + final Color? borderColor; + + /// The color of the widget in leading slot of the alert. + final Color? leadingColor; + + /// The text color of the alert. + final Color? textColor; + + /// The color of the widget in trailing slot of the alert. + final Color? trailingColor; + + /// The border width of the alert. + final double? borderWidth; + + /// The horizontal space between alert leading, trailing and title. + final double? horizontalGap; + + /// The minimum height of Alert. + final double? minimumHeight; + + /// The vertical space between alert header and body. + final double? verticalGap; + + /// Alert transition duration (show and hide animation). + final Duration? transitionDuration; + + /// Alert transition curve (show and hide animation). + final Curve? transitionCurve; + + /// The padding of the alert. + final EdgeInsets? padding; + + /// The semantic label for the alert. + final String? semanticLabel; + + /// The text style for body + final TextStyle? bodyTextStyle; + + /// The text style for title + final TextStyle? titleTextStyle; + + /// The widget in the body slot of the alert. + final Widget? body; + + /// The widget in the leading slot of the alert. + final Widget? leading; + + /// The widget in the title slot of the alert. + final Widget title; + + /// The widget in the trailing slot of the alert. + final Widget? trailing; + + /// MDS base alert. + /// + /// See also: + /// + /// * [MoonFilledAlert], MDS filled alert. + /// * [MoonOutlinedAlert], MDS outlined alert. + const MoonAlert({ + super.key, + this.show = false, + this.showBorder = false, + this.borderRadius, + this.backgroundColor, + this.borderColor, + this.leadingColor, + this.textColor, + this.trailingColor, + this.borderWidth, + this.horizontalGap, + this.minimumHeight, + this.verticalGap, + this.transitionDuration, + this.transitionCurve, + this.padding, + this.semanticLabel, + this.bodyTextStyle, + this.titleTextStyle, + this.body, + this.leading, + required this.title, + this.trailing, + }); + + @override + State createState() => _MoonAlertState(); +} + +class _MoonAlertState extends State with SingleTickerProviderStateMixin { + bool _isVisible = true; + + AnimationController? _animationController; + Animation? _curvedAnimation; + + Color _getElementColor({required Color effectiveBackgroundColor, required Color? elementColor}) { + if (elementColor != null) return elementColor; + + final backgroundLuminance = effectiveBackgroundColor.computeLuminance(); + if (backgroundLuminance > 0.5) { + return MoonColors.light.bulma; + } else { + return MoonColors.dark.bulma; + } + } + + TextStyle _getTextStyle({required BuildContext context}) { + if (widget.titleTextStyle != null) return widget.titleTextStyle!; + + if (widget.body != null) { + return context.moonTheme?.alertTheme.properties.titleTextStyle ?? MoonTextStyles.heading.text14; + } else { + return context.moonTheme?.alertTheme.properties.bodyTextStyle ?? MoonTextStyles.body.text14; + } + } + + void _showAlert() { + if (!mounted) return; + _animationController!.forward(); + + setState(() => _isVisible = true); + } + + void _hideAlert() { + if (!mounted) return; + + _animationController!.reverse().then((void value) { + if (!mounted) return; + + setState(() => _isVisible = false); + }); + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + + if (_isVisible) { + _animationController!.value = 1.0; + } + }); + } + + @override + void didUpdateWidget(MoonAlert oldWidget) { + super.didUpdateWidget(oldWidget); + if (mounted) { + if (oldWidget.show != widget.show) { + if (widget.show) { + _showAlert(); + } else { + _hideAlert(); + } + } + } + } + + @override + void dispose() { + _animationController!.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final EdgeInsets effectivePadding = + widget.padding ?? context.moonTheme?.alertTheme.properties.padding ?? EdgeInsets.all(MoonSizes.sizes.x2s); + + final double effectiveHorizontalGap = + widget.horizontalGap ?? context.moonTheme?.alertTheme.properties.horizontalGap ?? MoonSizes.sizes.x3s; + + final double effectiveVerticalGap = + widget.verticalGap ?? context.moonTheme?.alertTheme.properties.verticalGap ?? MoonSizes.sizes.x4s; + + final double effectiveMinimumHeight = + widget.minimumHeight ?? context.moonTheme?.alertTheme.properties.minimumHeight ?? MoonSizes.sizes.xl; + + final BorderRadius effectiveBorderRadius = widget.borderRadius ?? + context.moonTheme?.alertTheme.properties.borderRadius ?? + MoonBorders.borders.interactiveSm; + + final double effectiveBorderWidth = + widget.borderWidth ?? context.moonBorders?.borderWidth ?? MoonBorders.borders.borderWidth; + + final Color effectiveBorderColor = + widget.borderColor ?? context.moonTheme?.alertTheme.colors.borderColor ?? MoonColors.light.bulma; + + final Color effectiveBackgroundColor = + widget.backgroundColor ?? context.moonTheme?.alertTheme.colors.backgroundColor ?? MoonColors.light.gohan; + + final Color effectiveLeadingColor = + _getElementColor(effectiveBackgroundColor: effectiveBackgroundColor, elementColor: widget.leadingColor); + + final Color effectiveTrailingColor = + _getElementColor(effectiveBackgroundColor: effectiveBackgroundColor, elementColor: widget.trailingColor); + + final Color effectiveTextColor = + _getElementColor(effectiveBackgroundColor: effectiveBackgroundColor, elementColor: widget.textColor); + + final TextStyle effectiveTitleTextStyle = _getTextStyle(context: context); + + final TextStyle effectiveBodyTextStyle = + widget.bodyTextStyle ?? context.moonTheme?.alertTheme.properties.bodyTextStyle ?? MoonTextStyles.body.text14; + + final Duration effectiveTransitionDuration = widget.transitionDuration ?? + context.moonTheme?.alertTheme.properties.transitionDuration ?? + const Duration(milliseconds: 200); + + final Curve effectiveTransitionCurve = + widget.transitionCurve ?? context.moonTheme?.alertTheme.properties.transitionCurve ?? Curves.easeInOutCubic; + + _animationController ??= AnimationController( + duration: effectiveTransitionDuration, + vsync: this, + ); + + _curvedAnimation ??= CurvedAnimation( + parent: _animationController!, + curve: effectiveTransitionCurve, + ); + + return Visibility( + visible: _isVisible, + child: Semantics( + label: widget.semanticLabel, + child: RepaintBoundary( + child: FadeTransition( + opacity: _curvedAnimation!, + child: Container( + padding: effectivePadding, + constraints: BoxConstraints(minHeight: effectiveMinimumHeight), + decoration: ShapeDecoration( + color: effectiveBackgroundColor, + shape: SmoothRectangleBorder( + side: BorderSide( + color: effectiveBorderColor, + width: widget.showBorder ? effectiveBorderWidth : 0, + style: widget.showBorder ? BorderStyle.solid : BorderStyle.none, + ), + borderRadius: effectiveBorderRadius.smoothBorderRadius, + ), + ), + child: AnimatedDefaultTextStyle( + duration: effectiveTransitionDuration, + style: TextStyle(color: effectiveTextColor), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + if (widget.leading != null) + AnimatedIconTheme( + duration: effectiveTransitionDuration, + color: effectiveLeadingColor, + child: Padding( + padding: EdgeInsetsDirectional.only(end: effectiveHorizontalGap), + child: widget.leading, + ), + ), + DefaultTextStyle.merge( + style: effectiveTitleTextStyle, + child: Expanded( + child: widget.title, + ), + ), + if (widget.trailing != null) + AnimatedIconTheme( + duration: effectiveTransitionDuration, + color: effectiveTrailingColor, + child: Padding( + padding: EdgeInsetsDirectional.only(start: effectiveHorizontalGap), + child: widget.trailing, + ), + ), + ], + ), + if (widget.body != null) + Row( + children: [ + DefaultTextStyle.merge( + style: effectiveBodyTextStyle, + child: Expanded( + child: Padding( + padding: EdgeInsetsDirectional.only(top: effectiveVerticalGap), + child: widget.body, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/alert/filled_alert.dart b/lib/src/widgets/alert/filled_alert.dart new file mode 100644 index 00000000..f08f09f3 --- /dev/null +++ b/lib/src/widgets/alert/filled_alert.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/widgets/alert/alert.dart'; +import 'package:moon_design/src/widgets/buttons/button.dart'; +import 'package:moon_design/src/widgets/common/icons/icons.dart'; + +class MoonFilledAlert extends StatelessWidget { + /// Controls whether the alert is shown. + final bool show; + + /// The border radius of the alert. + final BorderRadius? borderRadius; + + /// The color of the alert. + final Color? color; + + /// The semantic label for the alert. + final String? semanticLabel; + + /// The callback that is called when the trailing slot widget is tapped. + final VoidCallback? onTrailingTap; + + /// The widget in the leading slot of the alert. + final Widget? leading; + + /// The widget in the title slot of the alert. + final Widget title; + + /// The widget in the body slot of the alert. + final Widget? body; + + /// MDS filled alert variant. + /// + /// See also: + /// + /// * [MoonOutlinedAlert], MDS outlined button. + const MoonFilledAlert({ + super.key, + this.show = false, + this.borderRadius, + this.color, + this.semanticLabel, + this.onTrailingTap, + this.leading, + required this.title, + this.body, + }); + + @override + Widget build(BuildContext context) { + final resolvedBackgroundColor = color != null ? color!.withOpacity(0.1) : null; + + final effectiveTrailing = MoonButton.icon( + onTap: onTrailingTap, + semanticLabel: semanticLabel, + buttonSize: MoonButtonSize.xs, + borderRadius: borderRadius, + disabledOpacityValue: 1, + icon: Icon( + MoonIconsControls.close_small24, + color: color, + size: 24, + ), + gap: 0, + ); + + return MoonAlert( + show: show, + borderRadius: borderRadius, + semanticLabel: semanticLabel, + leading: leading, + title: title, + trailing: effectiveTrailing, + body: body, + backgroundColor: resolvedBackgroundColor, + leadingColor: color, + textColor: color, + ); + } +} diff --git a/lib/src/widgets/alert/outlined_alert.dart b/lib/src/widgets/alert/outlined_alert.dart new file mode 100644 index 00000000..2e806b42 --- /dev/null +++ b/lib/src/widgets/alert/outlined_alert.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/colors.dart'; +import 'package:moon_design/src/theme/theme.dart'; +import 'package:moon_design/src/widgets/alert/alert.dart'; +import 'package:moon_design/src/widgets/buttons/button.dart'; +import 'package:moon_design/src/widgets/common/icons/icons.dart'; + +class MoonOutlinedAlert extends StatelessWidget { + /// Controls whether the alert is shown. + final bool show; + + /// The border radius of the alert. + final BorderRadius? borderRadius; + + /// The border color of the alert. + final Color? color; + + /// The border width of the alert. + final double? borderWidth; + + /// The semantic label for the alert. + final String? semanticLabel; + + /// The callback that is called when the trailing slot widget is tapped. + final VoidCallback? onTrailingTap; + + /// The widget in the leading slot of the alert. + final Widget? leading; + + /// The widget in the title slot of the alert. + final Widget title; + + /// The widget in the body slot of the alert. + final Widget? body; + + /// MDS outlined alert variant. + /// + /// See also: + /// + /// * [MoonFilledAlert], MDS filled button. + const MoonOutlinedAlert({ + super.key, + this.show = false, + this.borderRadius, + this.color, + this.borderWidth, + this.semanticLabel, + this.onTrailingTap, + this.leading, + required this.title, + this.body, + }); + + @override + Widget build(BuildContext context) { + final effectiveElementColor = + color ?? context.moonTheme?.alertTheme.colors.outlinedVariantColor ?? MoonColors.light.bulma; + + final effectiveTrailing = MoonButton.icon( + onTap: onTrailingTap, + semanticLabel: semanticLabel, + buttonSize: MoonButtonSize.xs, + borderRadius: borderRadius, + disabledOpacityValue: 1, + icon: Icon( + MoonIconsControls.close_small24, + color: effectiveElementColor, + size: 24, + ), + gap: 0, + ); + + return MoonAlert( + show: show, + showBorder: true, + borderRadius: borderRadius, + borderWidth: borderWidth, + semanticLabel: semanticLabel, + leading: leading, + title: title, + trailing: effectiveTrailing, + body: body, + backgroundColor: Colors.transparent, + borderColor: color, + leadingColor: effectiveElementColor, + textColor: effectiveElementColor, + ); + } +} diff --git a/lib/src/widgets/buttons/text_button.dart b/lib/src/widgets/buttons/text_button.dart index ebfb72ad..354f25a1 100644 --- a/lib/src/widgets/buttons/text_button.dart +++ b/lib/src/widgets/buttons/text_button.dart @@ -66,7 +66,6 @@ class MoonTextButton extends StatelessWidget { /// /// * [MoonFilledButton], MDS filled button. /// * [MoonOutlinedButton], MDS outlined button. - const MoonTextButton({ super.key, this.onTap,