diff --git a/example/lib/src/storybook/stories/authcode.dart b/example/lib/src/storybook/stories/authcode.dart new file mode 100644 index 00000000..424cfa14 --- /dev/null +++ b/example/lib/src/storybook/stories/authcode.dart @@ -0,0 +1,239 @@ +import 'dart:async'; + +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'; + +final StreamController errorController = StreamController(); + +class AuthCodeStory extends Story { + AuthCodeStory() + : super( + name: "AuthCode", + builder: (context) { + final mainAxisAlignmentKnob = context.knobs.options( + label: "mainAxisAlignment", + description: "Horizontal alignment of auth input fields.", + initial: MainAxisAlignment.center, + options: const [ + Option(label: "start", value: MainAxisAlignment.start), + Option(label: "center", value: MainAxisAlignment.center), + Option(label: "end", value: MainAxisAlignment.end), + Option(label: "spaceBetween", value: MainAxisAlignment.spaceBetween), + Option(label: "spaceAround", value: MainAxisAlignment.spaceAround), + Option(label: "spaceEvenly", value: MainAxisAlignment.spaceEvenly), + ], + ); + + final shapeKnob = context.knobs.options( + label: "shape", + description: "Shape of the auth input fields.", + initial: AuthFieldShape.box, + options: const [ + Option(label: "box", value: AuthFieldShape.box), + Option(label: "circle", value: AuthFieldShape.circle), + Option(label: "underline", value: AuthFieldShape.underline), + ], + ); + + final selectedFillColorsKnob = context.knobs.options( + label: "selectedFillColor", + description: "MoonColors variants for MoonAuthCode.", + initial: 40, // null + options: colorOptions, + ); + + final selectedFillColor = colorTable(context)[selectedFillColorsKnob]; + + final activeFillColorsKnob = context.knobs.options( + label: "activeFillColor", + description: "MoonColors variants for MoonAuthCode.", + initial: 40, // null + options: colorOptions, + ); + + final activeFillColor = colorTable(context)[activeFillColorsKnob]; + + final inactiveFillColorsKnob = context.knobs.options( + label: "inactiveFillColor", + description: "MoonColors variants for MoonAuthCode.", + initial: 40, // null + options: colorOptions, + ); + + final inactiveFillColor = colorTable(context)[inactiveFillColorsKnob]; + + final selectedBorderColorsKnob = context.knobs.options( + label: "selectedBorderColor", + description: "MoonColors variants for MoonAuthCode.", + initial: 40, // null + options: colorOptions, + ); + + final selectedBorderColor = colorTable(context)[selectedBorderColorsKnob]; + + final activeBorderColorsKnob = context.knobs.options( + label: "activeBorderColor", + description: "MoonColors variants for MoonAuthCode.", + initial: 40, // null + options: colorOptions, + ); + + final activeBorderColor = colorTable(context)[activeBorderColorsKnob]; + + final inactiveBorderColorsKnob = context.knobs.options( + label: "inactiveBorderColor", + description: "MoonColors variants for MoonAuthCode.", + initial: 40, // null + options: colorOptions, + ); + + final inactiveBorderColor = colorTable(context)[inactiveBorderColorsKnob]; + + final borderRadiusKnob = context.knobs.sliderInt( + max: 12, + initial: 8, + label: "borderRadius", + description: "Border radius for auth input fields.", + ); + + final gapKnob = context.knobs.sliderInt( + max: 12, + initial: 8, + label: "gap", + description: "Gap between auth input fields.", + ); + + final enableKnob = context.knobs.boolean( + label: "enabled", + description: "Enable AuthCode.", + ); + + final obscuringKnob = context.knobs.boolean( + label: "obscureText", + description: "Obscure auth input fields.", + ); + + final peekWhenObscuringKnob = context.knobs.boolean( + label: "peekWhenObscuring", + description: "Peek when obscuring.", + ); + + final errorAnimationKnob = context.knobs.boolean( + label: "Error shake animation", + description: "Show error with shake animation (ErrorAnimationType.shake).", + ); + + final setRtlModeKnob = context.knobs.boolean( + label: "RTL mode", + description: "Switch between LTR and RTL modes.", + ); + + return StatefulBuilder( + builder: (context, setState) { + return Directionality( + textDirection: setRtlModeKnob ? TextDirection.rtl : TextDirection.ltr, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 64), + const TextDivider(text: "Disabled MoonAuthCode"), + const SizedBox(height: 32), + MoonAuthCode( + enableInputFill: true, + authInputFieldCount: 4, + mainAxisAlignment: mainAxisAlignmentKnob, + borderRadius: BorderRadius.circular(borderRadiusKnob.toDouble()), + selectedFillColor: selectedFillColor, + activeFillColor: activeFillColor, + inactiveFillColor: inactiveFillColor, + selectedBorderColor: selectedBorderColor, + activeBorderColor: activeBorderColor, + inactiveBorderColor: inactiveBorderColor, + enabled: enableKnob, + gap: gapKnob.toDouble(), + authFieldShape: shapeKnob, + obscureText: obscuringKnob, + obscuringCharacter: '*', + peekWhenObscuring: peekWhenObscuringKnob, + validator: (String? value) => null, + errorBuilder: (BuildContext context, String? errorText) => const SizedBox(), + ), + const SizedBox(height: 32), + const TextDivider(text: "Active MoonAuthCode"), + const SizedBox(height: 32), + MoonAuthCode( + autoFocus: true, + enableInputFill: true, + mainAxisAlignment: mainAxisAlignmentKnob, + borderRadius: BorderRadius.circular(borderRadiusKnob.toDouble()), + selectedFillColor: selectedFillColor, + activeFillColor: activeFillColor, + inactiveFillColor: inactiveFillColor, + selectedBorderColor: selectedBorderColor, + activeBorderColor: activeBorderColor, + inactiveBorderColor: inactiveBorderColor, + gap: gapKnob.toDouble(), + authFieldShape: shapeKnob, + obscureText: obscuringKnob, + obscuringCharacter: '*', + peekWhenObscuring: peekWhenObscuringKnob, + validator: (String? value) => null, + errorBuilder: (BuildContext context, String? errorText) => const SizedBox(), + ), + const SizedBox(height: 32), + const TextDivider(text: "Error MoonAuthCode"), + const SizedBox(height: 32), + SizedBox( + height: 95, + child: MoonAuthCode( + enableInputFill: true, + authInputFieldCount: 4, + mainAxisAlignment: mainAxisAlignmentKnob, + borderRadius: BorderRadius.circular(borderRadiusKnob.toDouble()), + errorStreamController: errorController, + selectedFillColor: selectedFillColor, + activeFillColor: activeFillColor, + inactiveFillColor: inactiveFillColor, + selectedBorderColor: selectedBorderColor, + activeBorderColor: activeBorderColor, + inactiveBorderColor: inactiveBorderColor, + gap: gapKnob.toDouble(), + authFieldShape: shapeKnob, + obscureText: obscuringKnob, + obscuringCharacter: '*', + peekWhenObscuring: peekWhenObscuringKnob, + onCompleted: (pin) { + if (pin != '9921') { + errorController.add( + errorAnimationKnob ? ErrorAnimationType.shake : ErrorAnimationType.noAnimation, + ); + } + }, + validator: (pin) { + if (pin?.length == 4 && pin != '9921') { + return 'Invalid authentication code. Please try again.'; + } + return null; + }, + errorBuilder: (context, errorText) { + return Align( + child: Container( + padding: const EdgeInsets.only(top: 8), + child: Text(errorText ?? ''), + ), + ); + }, + ), + ), + const SizedBox(height: 64), + ], + ), + ); + }, + ); + }, + ); +} diff --git a/example/lib/src/storybook/storybook.dart b/example/lib/src/storybook/storybook.dart index f954b23d..2c6e7f1b 100644 --- a/example/lib/src/storybook/storybook.dart +++ b/example/lib/src/storybook/storybook.dart @@ -1,6 +1,7 @@ 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/authCode.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'; @@ -73,6 +74,7 @@ class StorybookPage extends StatelessWidget { stories: [ AccordionStory(), AlertStory(), + AuthCodeStory(), AvatarStory(), ButtonStory(), CheckboxStory(), diff --git a/lib/moon_design.dart b/lib/moon_design.dart index 667bedbd..50f918ff 100644 --- a/lib/moon_design.dart +++ b/lib/moon_design.dart @@ -2,6 +2,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/authcode/authcode_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'; @@ -26,15 +27,14 @@ export 'package:moon_design/src/theme/tooltip/tooltip_theme.dart'; export 'package:moon_design/src/theme/typography/text_colors.dart'; export 'package:moon_design/src/theme/typography/text_styles.dart'; export 'package:moon_design/src/theme/typography/typography.dart'; - export 'package:moon_design/src/utils/extensions.dart'; 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/authcode/authcode.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_properties.dart b/lib/src/theme/alert/alert_properties.dart index 6011c7a9..37f32019 100644 --- a/lib/src/theme/alert/alert_properties.dart +++ b/lib/src/theme/alert/alert_properties.dart @@ -113,7 +113,7 @@ class MoonAlertProperties extends ThemeExtension with Diagn ..add(DiagnosticsProperty("transitionDuration", transitionDuration)) ..add(DiagnosticsProperty("transitionCurve", transitionCurve)) ..add(DiagnosticsProperty("padding", padding)) - ..add(DiagnosticsProperty("contentTextStyle", bodyTextStyle)) + ..add(DiagnosticsProperty("bodyTextStyle", bodyTextStyle)) ..add(DiagnosticsProperty("titleTextStyle", titleTextStyle)); } } diff --git a/lib/src/theme/authcode/authcode_colors.dart b/lib/src/theme/authcode/authcode_colors.dart new file mode 100644 index 00000000..67c93629 --- /dev/null +++ b/lib/src/theme/authcode/authcode_colors.dart @@ -0,0 +1,128 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/colors.dart'; + +@immutable +class MoonAuthCodeColors extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonAuthCodeColors( + selectedBorderColor: MoonColors.light.piccolo, + activeBorderColor: MoonColors.light.beerus, + inactiveBorderColor: MoonColors.light.beerus, + errorBorderColor: MoonColors.light.chiChi100, + selectedFillColor: MoonColors.light.gohan, + activeFillColor: MoonColors.light.gohan, + inactiveFillColor: MoonColors.light.gohan, + disabledColor: MoonColors.light.beerus, + textColor: MoonColors.light.bulma, + ); + + static final dark = MoonAuthCodeColors( + selectedBorderColor: MoonColors.dark.piccolo, + activeBorderColor: MoonColors.dark.beerus, + inactiveBorderColor: MoonColors.dark.beerus, + errorBorderColor: MoonColors.dark.chiChi100, + selectedFillColor: MoonColors.dark.gohan, + activeFillColor: MoonColors.dark.gohan, + inactiveFillColor: MoonColors.dark.gohan, + disabledColor: MoonColors.dark.beerus, + textColor: MoonColors.dark.bulma, + ); + + /// Border color of the selected auth input field. + final Color selectedBorderColor; + + /// Border color of the active auth input field which has input, but is not selected. + final Color activeBorderColor; + + /// Border color of the inactive auth input field which has no input. + final Color inactiveBorderColor; + + /// Border color of the auth input field in error mode. + final Color errorBorderColor; + + /// Fill color of the selected auth input field. + final Color selectedFillColor; + + /// Fill color of the active auth input field which has input, but is not selected. + final Color activeFillColor; + + /// Fill color of the inactive auth input field which has no input. + final Color inactiveFillColor; + + /// Color of the auth input field when MoonAuthCode is disabled. + final Color disabledColor; + + /// AuthCode text color. + final Color textColor; + + const MoonAuthCodeColors({ + required this.selectedBorderColor, + required this.activeBorderColor, + required this.inactiveBorderColor, + required this.errorBorderColor, + required this.selectedFillColor, + required this.activeFillColor, + required this.inactiveFillColor, + required this.disabledColor, + required this.textColor, + }); + + @override + MoonAuthCodeColors copyWith({ + Color? selectedBorderColor, + Color? activeBorderColor, + Color? inactiveBorderColor, + Color? errorBorderColor, + Color? selectedFillColor, + Color? activeFillColor, + Color? inactiveFillColor, + Color? disabledColor, + Color? textColor, + }) { + return MoonAuthCodeColors( + selectedBorderColor: selectedBorderColor ?? this.selectedBorderColor, + activeBorderColor: activeBorderColor ?? this.activeBorderColor, + inactiveBorderColor: inactiveBorderColor ?? this.inactiveBorderColor, + errorBorderColor: errorBorderColor ?? this.errorBorderColor, + selectedFillColor: selectedFillColor ?? this.selectedFillColor, + activeFillColor: activeFillColor ?? this.activeFillColor, + inactiveFillColor: inactiveFillColor ?? this.inactiveFillColor, + disabledColor: disabledColor ?? this.disabledColor, + textColor: textColor ?? this.textColor, + ); + } + + @override + MoonAuthCodeColors lerp(ThemeExtension? other, double t) { + if (other is! MoonAuthCodeColors) return this; + + return MoonAuthCodeColors( + selectedBorderColor: Color.lerp(selectedBorderColor, other.selectedBorderColor, t)!, + activeBorderColor: Color.lerp(activeBorderColor, other.activeBorderColor, t)!, + inactiveBorderColor: Color.lerp(inactiveBorderColor, other.inactiveBorderColor, t)!, + errorBorderColor: Color.lerp(errorBorderColor, other.errorBorderColor, t)!, + selectedFillColor: Color.lerp(selectedFillColor, other.selectedFillColor, t)!, + activeFillColor: Color.lerp(activeFillColor, other.activeFillColor, t)!, + inactiveFillColor: Color.lerp(inactiveFillColor, other.inactiveFillColor, t)!, + disabledColor: Color.lerp(disabledColor, other.disabledColor, t)!, + textColor: Color.lerp(textColor, other.textColor, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonAuthCodeColors")) + ..add(ColorProperty("selectedBorderColor", selectedBorderColor)) + ..add(ColorProperty("activeBorderColor", activeBorderColor)) + ..add(ColorProperty("inactiveBorderColor", inactiveBorderColor)) + ..add(ColorProperty("errorBorderColor", errorBorderColor)) + ..add(ColorProperty("selectedFillColor", selectedFillColor)) + ..add(ColorProperty("activeFillColor", activeFillColor)) + ..add(ColorProperty("inactiveFillColor", inactiveFillColor)) + ..add(ColorProperty("disabledColor", disabledColor)) + ..add(ColorProperty("textColor", textColor)); + } +} diff --git a/lib/src/theme/authcode/authcode_properties.dart b/lib/src/theme/authcode/authcode_properties.dart new file mode 100644 index 00000000..71c44765 --- /dev/null +++ b/lib/src/theme/authcode/authcode_properties.dart @@ -0,0 +1,138 @@ +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 MoonAuthCodeProperties extends ThemeExtension with DiagnosticableTreeMixin { + static final properties = MoonAuthCodeProperties( + borderRadius: MoonBorders.borders.interactiveSm, + gap: MoonSizes.sizes.x4s, + height: MoonSizes.sizes.xl, + width: MoonSizes.sizes.lg, + animationDuration: const Duration(milliseconds: 200), + errorAnimationDuration: const Duration(milliseconds: 200), + peekDuration: const Duration(milliseconds: 200), + animationCurve: Curves.easeInOutCubic, + errorAnimationCurve: Curves.easeInOutCubic, + textStyle: MoonTextStyles.body.text24, + errorTextStyle: MoonTextStyles.body.text12, + ); + + /// Border radius of the auth input field. + final BorderRadius borderRadius; + + /// Horizontal space between auth input fields. + final double gap; + + /// Height of the auth input field. + final double height; + + /// Width of the auth input field. + final double width; + + /// AuthCode input field animation duration. + final Duration animationDuration; + + /// AuthCode error animation duration. + final Duration errorAnimationDuration; + + /// Peek duration before obscuring. + final Duration peekDuration; + + /// AuthCode input field animation curve. + final Curve animationCurve; + + /// AuthCode error animation curve. + final Curve errorAnimationCurve; + + /// AuthCode text style. + final TextStyle textStyle; + + /// AuthCode error text style. + final TextStyle errorTextStyle; + + @override + MoonAuthCodeProperties copyWith({ + BorderRadius? borderRadius, + double? gap, + double? height, + double? width, + Duration? animationDuration, + Duration? errorAnimationDuration, + Duration? peekDuration, + Curve? animationCurve, + Curve? errorAnimationCurve, + TextStyle? textStyle, + TextStyle? errorTextStyle, + }) { + return MoonAuthCodeProperties( + borderRadius: borderRadius ?? this.borderRadius, + gap: gap ?? this.gap, + height: height ?? this.height, + width: width ?? this.width, + animationDuration: animationDuration ?? this.animationDuration, + errorAnimationDuration: errorAnimationDuration ?? this.errorAnimationDuration, + peekDuration: peekDuration ?? this.peekDuration, + animationCurve: animationCurve ?? this.animationCurve, + errorAnimationCurve: errorAnimationCurve ?? this.errorAnimationCurve, + textStyle: textStyle ?? this.textStyle, + errorTextStyle: errorTextStyle ?? this.errorTextStyle, + ); + } + + @override + MoonAuthCodeProperties lerp(ThemeExtension? other, double t) { + if (other is! MoonAuthCodeProperties) return this; + + return MoonAuthCodeProperties( + borderRadius: BorderRadius.lerp(borderRadius, other.borderRadius, t)!, + gap: lerpDouble(gap, other.gap, t)!, + height: lerpDouble(height, other.height, t)!, + width: lerpDouble(width, other.width, t)!, + animationDuration: lerpDuration(animationDuration, other.animationDuration, t), + errorAnimationDuration: lerpDuration(errorAnimationDuration, other.errorAnimationDuration, t), + peekDuration: lerpDuration(peekDuration, other.peekDuration, t), + animationCurve: other.animationCurve, + errorAnimationCurve: other.errorAnimationCurve, + textStyle: TextStyle.lerp(textStyle, other.textStyle, t)!, + errorTextStyle: TextStyle.lerp(errorTextStyle, other.errorTextStyle, t)!, + ); + } + + const MoonAuthCodeProperties({ + required this.borderRadius, + required this.gap, + required this.height, + required this.width, + required this.animationDuration, + required this.errorAnimationDuration, + required this.peekDuration, + required this.animationCurve, + required this.errorAnimationCurve, + required this.textStyle, + required this.errorTextStyle, + }); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonAuthCodeProperties")) + ..add(DiagnosticsProperty("borderRadius", borderRadius)) + ..add(DiagnosticsProperty("gap", gap)) + ..add(DiagnosticsProperty("height", height)) + ..add(DiagnosticsProperty("width", width)) + ..add(DiagnosticsProperty("animationDuration", animationDuration)) + ..add(DiagnosticsProperty("errorAnimationDuration", errorAnimationDuration)) + ..add(DiagnosticsProperty("peekDuration", peekDuration)) + ..add(DiagnosticsProperty("animationCurve", animationCurve)) + ..add(DiagnosticsProperty("errorAnimationCurve", errorAnimationCurve)) + ..add(DiagnosticsProperty("textStyle", textStyle)) + ..add(DiagnosticsProperty("errorTextStyle", errorTextStyle)); + } +} diff --git a/lib/src/theme/authcode/authcode_theme.dart b/lib/src/theme/authcode/authcode_theme.dart new file mode 100644 index 00000000..a6b035cc --- /dev/null +++ b/lib/src/theme/authcode/authcode_theme.dart @@ -0,0 +1,59 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/authcode/authcode_colors.dart'; +import 'package:moon_design/src/theme/authcode/authcode_properties.dart'; + +@immutable +class MoonAuthCodeTheme extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonAuthCodeTheme( + colors: MoonAuthCodeColors.light, + properties: MoonAuthCodeProperties.properties, + ); + + static final dark = MoonAuthCodeTheme( + colors: MoonAuthCodeColors.dark, + properties: MoonAuthCodeProperties.properties, + ); + + /// AuthCode colors. + final MoonAuthCodeColors colors; + + /// AuthCode properties. + final MoonAuthCodeProperties properties; + + const MoonAuthCodeTheme({ + required this.colors, + required this.properties, + }); + + @override + MoonAuthCodeTheme copyWith({ + MoonAuthCodeColors? colors, + MoonAuthCodeProperties? properties, + }) { + return MoonAuthCodeTheme( + colors: colors ?? this.colors, + properties: properties ?? this.properties, + ); + } + + @override + MoonAuthCodeTheme lerp(ThemeExtension? other, double t) { + if (other is! MoonAuthCodeTheme) return this; + + return MoonAuthCodeTheme( + colors: colors.lerp(other.colors, t), + properties: properties.lerp(other.properties, t), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder diagnosticProperties) { + super.debugFillProperties(diagnosticProperties); + diagnosticProperties + ..add(DiagnosticsProperty("type", "MoonAuthCodeTheme")) + ..add(DiagnosticsProperty("colors", colors)) + ..add(DiagnosticsProperty("properties", properties)); + } +} diff --git a/lib/src/theme/theme.dart b/lib/src/theme/theme.dart index d992ba88..dbc0c706 100644 --- a/lib/src/theme/theme.dart +++ b/lib/src/theme/theme.dart @@ -1,8 +1,8 @@ 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/authcode/authcode_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'; @@ -31,6 +31,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { static final light = MoonTheme( accordionTheme: MoonAccordionTheme.light, alertTheme: MoonAlertTheme.light, + authCodeTheme: MoonAuthCodeTheme.light, avatarTheme: MoonAvatarTheme.light, borders: MoonBorders.borders, buttonTheme: MoonButtonTheme.light, @@ -58,6 +59,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { static final dark = MoonTheme( accordionTheme: MoonAccordionTheme.dark, alertTheme: MoonAlertTheme.dark, + authCodeTheme: MoonAuthCodeTheme.dark, avatarTheme: MoonAvatarTheme.dark, borders: MoonBorders.borders, buttonTheme: MoonButtonTheme.dark, @@ -88,6 +90,9 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { /// Moon Design System MoonAlert widget theming. final MoonAlertTheme alertTheme; + /// Moon Design System MoonAuthCode widget theming. + final MoonAuthCodeTheme authCodeTheme; + /// Moon Design System MoonAvatar widget theming. final MoonAvatarTheme avatarTheme; @@ -157,6 +162,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { const MoonTheme({ required this.accordionTheme, required this.alertTheme, + required this.authCodeTheme, required this.avatarTheme, required this.borders, required this.buttonTheme, @@ -185,6 +191,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { MoonTheme copyWith({ MoonAccordionTheme? accordionTheme, MoonAlertTheme? alertTheme, + MoonAuthCodeTheme? authCodeTheme, MoonAvatarTheme? avatarTheme, MoonBorders? borders, MoonButtonTheme? buttonTheme, @@ -211,6 +218,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { return MoonTheme( accordionTheme: accordionTheme ?? this.accordionTheme, alertTheme: alertTheme ?? this.alertTheme, + authCodeTheme: authCodeTheme ?? this.authCodeTheme, avatarTheme: avatarTheme ?? this.avatarTheme, borders: borders ?? this.borders, buttonTheme: buttonTheme ?? this.buttonTheme, @@ -243,6 +251,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { return MoonTheme( accordionTheme: accordionTheme.lerp(other.accordionTheme, t), alertTheme: alertTheme.lerp(other.alertTheme, t), + authCodeTheme: authCodeTheme.lerp(other.authCodeTheme, t), avatarTheme: avatarTheme.lerp(other.avatarTheme, t), borders: borders.lerp(other.borders, t), buttonTheme: buttonTheme.lerp(other.buttonTheme, t), @@ -275,6 +284,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { ..add(DiagnosticsProperty("type", "MoonTheme")) ..add(DiagnosticsProperty("MoonAccordionTheme", accordionTheme)) ..add(DiagnosticsProperty("MoonAlertTheme", alertTheme)) + ..add(DiagnosticsProperty("MoonAuthCodeTheme", authCodeTheme)) ..add(DiagnosticsProperty("MoonAvatarTheme", avatarTheme)) ..add(DiagnosticsProperty("MoonBorders", borders)) ..add(DiagnosticsProperty("MoonButtonTheme", buttonTheme)) @@ -302,11 +312,18 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { extension MoonThemeX on BuildContext { MoonTheme? get moonTheme => Theme.of(this).extension(); + MoonBorders? get moonBorders => moonTheme?.borders; + MoonColors? get moonColors => moonTheme?.colors; + MoonEffects? get moonEffects => moonTheme?.effects; + MoonOpacity? get moonOpacity => moonTheme?.opacity; + MoonShadows? get moonShadows => moonTheme?.shadows; + MoonSizes? get moonSizes => moonTheme?.sizes; + MoonTypography? get moonTypography => moonTheme?.typography; } diff --git a/lib/src/widgets/authcode/authcode.dart b/lib/src/widgets/authcode/authcode.dart new file mode 100644 index 00000000..6028e998 --- /dev/null +++ b/lib/src/widgets/authcode/authcode.dart @@ -0,0 +1,851 @@ +import 'dart:async'; + +import 'package:figma_squircle/figma_squircle.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:moon_design/src/theme/borders.dart'; +import 'package:moon_design/src/theme/colors.dart'; +import 'package:moon_design/src/theme/opacity.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'; + +enum AuthFieldShape { box, underline, circle } + +enum ErrorAnimationType { noAnimation, shake } + +typedef MoonAuthCodeBuilder = Widget Function( + BuildContext context, + String? errorText, +); + +class MoonAuthCode extends StatefulWidget { + /// Shape of auth input field. + final AuthFieldShape? authFieldShape; + + /// Controls the keyboard dismissal on last input enter. + final bool autoDismissKeyboard; + + /// {@macro flutter.widgets.Focus.autofocus}. + final bool autoFocus; + + /// Controls whether to auto unfocus. + final bool autoUnfocus; + + /// Controls whether MoonAuthCode widget is enabled. + final bool enabled; + + /// Controls whether to show fill color for inputs. + final bool enableInputFill; + + /// When set to true, all characters in the auth input field are replaced by [obscuringCharacter]. + final bool obscureText; + + /// Controls whether typed character should be briefly shown before being obscured. + final bool peekWhenObscuring; + + /// Controls if haptic feedback is used. + final bool useHapticFeedback; + + /// Controls if cursor in selected auth input field should be shown. + final bool showAuthFieldCursor; + + /// Border radius of auth input field. + final BorderRadius? borderRadius; + + /// Color of the auth input field cursor. + final Color? authFieldCursorColor; + + /// Border color of the selected auth input field. + final Color? selectedBorderColor; + + /// Border color of the active auth input field which has input. + final Color? activeBorderColor; + + /// Border color of the inactive auth input field, which does not have input. + final Color? inactiveBorderColor; + + /// Border color of the auth input field when in error mode. + final Color? errorBorderColor; + + /// Fill color of the selected auth input field. + /// + /// [enableInputFill] has to be set true. + final Color? selectedFillColor; + + /// Fill color of the active auth input field which has input. + /// + /// [enableInputFill] has to be set true. + final Color? activeFillColor; + + /// Fill color of the inactive auth input field which does not have input. + /// + /// [enableInputFill] has to be set true. + final Color? inactiveFillColor; + + /// Color of the disabled AuthCode. + /// + /// [enabled] has to be set to false. + final Color? disabledColor; + + /// AuthCode text color + final Color? textColor; + + /// Border width for the auth input field. + final double? borderWidth; + + /// Horizontal space between input fields. + final double? gap; + + /// Height of the auth input field. + final double? height; + + /// Width of the auth input field. + final double? width; + + /// AuthCode input field animation duration. + final Duration? animationDuration; + + /// AuthCode error animation duration. + final Duration? errorAnimationDuration; + + /// Peek duration if [peekWhenObscuring] is set to true. + final Duration? peekDuration; + + /// AuthCode input field animation curve. + final Curve? animationCurve; + + /// AuthCode error animation curve. + final Curve? errorAnimationCurve; + + /// {@macro flutter.widgets.Focus.focusNode}. + final FocusNode? focusNode; + + /// Validator for the [TextFormField]. + final FormFieldValidator validator; + + /// Count of auth input fields. + final int authInputFieldCount; + + /// Box shadow for auth input field. + final List? boxShadows; + + /// Box shadow for selected auth input field. + final List? activeBoxShadows; + + /// Box shadow for inactive auth input field. + final List? inActiveBoxShadows; + + /// Controls how auth input fields are aligned on the main axis. + final MainAxisAlignment mainAxisAlignment; + + /// Builder for the error widget. + final MoonAuthCodeBuilder errorBuilder; + + /// Displays a hint or a placeholder in the input field if it's value is empty. + final String? hintCharacter; + + /// Character used for obscuring text if [obscureText] is true. + /// + /// Default is ● - 'Black Circle' (U+25CF). + final String obscuringCharacter; + + /// Semantic label for MoonAuthCode. + final String? semanticLabel; + + /// MoonAuthCode error stream controller. + final StreamController? errorStreamController; + + /// [TextEditingController] for an editable text field. + final TextEditingController? textController; + + /// An action user has requested the text input control to perform. + final TextInputAction textInputAction; + + /// [TextInputType] for MoonAuthCode. + final TextInputType keyboardType; + + /// Text style of [hintCharacter]. + final TextStyle? hintStyle; + + /// MoonAuthCode text style. + final TextStyle? textStyle; + + /// MoonAuthCode error text style. + final TextStyle? errorTextStyle; + + /// Returns current auth input text. + final ValueChanged? onChanged; + + /// Returns auth input text when all auth input fields are filled. + final ValueChanged? onCompleted; + + /// Returns auth input text on done/next action on the keyboard. + final ValueChanged? onSubmitted; + + /// [onEditingComplete] callback runs when editing is finished. + /// It differs from [onSubmitted] by having a default value which + /// updates [textController] and yields keyboard focus. + /// + /// Set this to empty function if keyboard should not close automatically on done/next press. + final VoidCallback? onEditingComplete; + + /// Widget used to obscure text. + /// + /// Overrides the [obscuringCharacter]. + final Widget? obscuringWidget; + + const MoonAuthCode({ + super.key, + this.authFieldShape = AuthFieldShape.box, + this.autoDismissKeyboard = true, + this.autoFocus = false, + this.autoUnfocus = true, + this.enabled = true, + this.enableInputFill = false, + this.obscureText = false, + this.peekWhenObscuring = false, + this.showAuthFieldCursor = true, + this.useHapticFeedback = false, + this.borderRadius, + this.authFieldCursorColor, + this.selectedBorderColor, + this.activeBorderColor, + this.inactiveBorderColor, + this.errorBorderColor, + this.selectedFillColor, + this.activeFillColor, + this.inactiveFillColor, + this.disabledColor, + this.textColor, + this.height, + this.width, + this.borderWidth, + this.gap, + this.errorAnimationCurve, + this.animationCurve, + this.errorAnimationDuration, + this.animationDuration, + this.peekDuration, + this.focusNode, + required this.validator, + this.authInputFieldCount = 6, + this.boxShadows, + this.activeBoxShadows, + this.inActiveBoxShadows, + this.mainAxisAlignment = MainAxisAlignment.center, + required this.errorBuilder, + this.hintCharacter, + this.obscuringCharacter = '●', + this.semanticLabel, + this.errorStreamController, + this.textController, + this.textInputAction = TextInputAction.done, + this.keyboardType = TextInputType.visiblePassword, + this.hintStyle, + this.textStyle, + this.errorTextStyle, + this.onChanged, + this.onCompleted, + this.onEditingComplete, + this.onSubmitted, + this.obscuringWidget, + }) : assert(authInputFieldCount > 0), + assert(height == null || height > 0), + assert(width == null || width > 0); + + @override + _MoonAuthCodeState createState() => _MoonAuthCodeState(); +} + +class _MoonAuthCodeState extends State with TickerProviderStateMixin { + late FocusNode _focusNode; + late List _inputList; + + late BorderRadius _effectiveBorderRadius; + late Color _effectiveSelectedBorderColor; + late Color _effectiveActiveBorderColor; + late Color _effectiveInactiveBorderColor; + late Color _effectiveErrorBorderColor; + late Color _effectiveSelectedFillColor; + late Color _effectiveActiveFillColor; + late Color _effectiveInactiveFillColor; + late Color _effectiveDisabledColor; + late Color _effectiveTextColor; + late Color _effectiveCursorColor; + late double _effectiveBorderWidth; + late double _effectiveGap; + late double _effectiveHeight; + late double _effectiveWidth; + late TextStyle _effectiveTextStyle; + late TextStyle _effectiveErrorTextStyle; + + late StreamSubscription _errorAnimationSubscription; + late TextEditingController _textEditingController; + late AnimationController _cursorController; + late Animation _cursorAnimation; + + AnimationController? _errorAnimationController; + Animation? _errorOffsetAnimation; + Duration? _peekDuration; + Duration? _animationDuration; + Curve? _animationCurve; + + bool _isInErrorMode = false; + bool _hasPeeked = false; + int _selectedIndex = 0; + Timer? _peekDebounce; + + TextStyle get _resolvedTextStyle => _effectiveTextStyle + .merge(widget.textStyle) + .copyWith(color: _isInErrorMode ? _resolvedErrorTextStyle.color : widget.textStyle?.color ?? _effectiveTextColor); + + TextStyle get _hintStyle => _resolvedTextStyle.merge(widget.hintStyle); + + TextStyle get _resolvedErrorTextStyle => _effectiveErrorTextStyle + .merge(widget.errorTextStyle) + .copyWith(color: widget.errorTextStyle?.color ?? _effectiveErrorBorderColor); + + void _initializeFields() { + _initializeFocusNode(); + _initializeInputList(); + _initializeTextEditingController(); + _initializeAuthFieldCursor(); + _initializeErrorAnimationListener(); + } + + void _initializeFocusNode() { + _focusNode = widget.focusNode ?? FocusNode(); + _focusNode.addListener(() => _setState(() {})); + } + + void _initializeInputList() { + _inputList = List.filled(widget.authInputFieldCount, ''); + } + + void _initializeTextEditingController() { + _textEditingController = widget.textController ?? TextEditingController(); + + _textEditingController.addListener(() { + if (_isInErrorMode) { + _setState(() => _isInErrorMode = false); + } + + if (widget.useHapticFeedback) HapticFeedback.lightImpact(); + + _debounceBlink(); + + String currentText = _textEditingController.text; + if (widget.enabled && _inputList.join() != currentText) { + if (currentText.length >= widget.authInputFieldCount) { + if (widget.onCompleted != null) { + if (currentText.length > widget.authInputFieldCount) { + currentText = currentText.substring(0, widget.authInputFieldCount); + } + Future.delayed(const Duration(milliseconds: 200), () => widget.onCompleted!(currentText)); + } + if (widget.autoDismissKeyboard) _focusNode.unfocus(); + } + widget.onChanged?.call(currentText); + } + + _updateTextField(currentText); + }); + + // Update UI if a default value is set for TextEditingController + if (_textEditingController.text.isNotEmpty) _updateTextField(_textEditingController.text); + } + + void _initializeAuthFieldCursor() { + _cursorController = AnimationController(duration: const Duration(seconds: 1), vsync: this); + _cursorAnimation = Tween(begin: 1, end: 0).animate( + CurvedAnimation( + parent: _cursorController, + curve: Curves.easeInOut, + ), + ); + + if (widget.showAuthFieldCursor) _cursorController.repeat(); + } + + void _initializeErrorAnimationListener() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + + _errorAnimationController!.addStatusListener((status) { + if (status == AnimationStatus.completed) _errorAnimationController!.reverse(); + }); + + if (widget.errorStreamController != null) { + _errorAnimationSubscription = widget.errorStreamController!.stream.listen((errorAnimation) { + if (errorAnimation == ErrorAnimationType.shake) { + _errorAnimationController!.forward(); + } + _setState(() => _isInErrorMode = true); + + if(widget.useHapticFeedback) HapticFeedback.vibrate(); + }); + } + }); + } + + void _debounceBlink() { + _hasPeeked = true; + + if (widget.peekWhenObscuring && _textEditingController.text.length > _inputList.where((x) => x.isNotEmpty).length) { + _setState(() => _hasPeeked = false); + + if (_peekDebounce?.isActive ?? false) _peekDebounce!.cancel(); + + _peekDebounce = Timer(_peekDuration!, () { + _setState(() => _hasPeeked = true); + }); + } + } + + void _onFocus() { + if (widget.autoUnfocus) { + if (_focusNode.hasFocus && MediaQuery.of(context).viewInsets.bottom == 0) { + _focusNode.unfocus(); + Future.delayed(const Duration(microseconds: 1), () => _focusNode.requestFocus()); + } else { + _focusNode.requestFocus(); + } + } else { + _focusNode.requestFocus(); + } + } + + Color _getBorderColorFromIndex(int index) { + if (!widget.enabled) return _effectiveDisabledColor; + + if (((_selectedIndex == index) || (_selectedIndex == index + 1 && index + 1 == widget.authInputFieldCount)) && + _focusNode.hasFocus) { + return _isInErrorMode ? _effectiveErrorBorderColor : _effectiveSelectedBorderColor; + } else if (_selectedIndex > index) { + return _isInErrorMode ? _effectiveErrorBorderColor : _effectiveActiveBorderColor; + } + + return _isInErrorMode ? _effectiveErrorBorderColor : _effectiveInactiveBorderColor; + } + + Color _getFillColorFromIndex(int index) { + if (!widget.enabled) return _effectiveDisabledColor; + + if (((_selectedIndex == index) || (_selectedIndex == index + 1 && index + 1 == widget.authInputFieldCount)) && + _focusNode.hasFocus) { + return _effectiveSelectedFillColor; + } else if (_selectedIndex > index) { + return _effectiveActiveFillColor; + } + + return _effectiveInactiveFillColor; + } + + double _getBorderWidthFromIndex(int index) { + if (((_selectedIndex == index) || (_selectedIndex == index + 1 && index + 1 == widget.authInputFieldCount)) && + _focusNode.hasFocus) { + return _effectiveBorderWidth + 1; + } + + return _effectiveBorderWidth; + } + + List? _getBoxShadowFromIndex(int index) { + if (_selectedIndex == index) { + return widget.activeBoxShadows; + } else if (_selectedIndex > index) { + return widget.inActiveBoxShadows; + } + + return []; + } + + 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; + } + } + + ShapeBorder _getAuthInputFieldShape({required int elementIndex}) { + final borderSide = BorderSide( + color: _getBorderColorFromIndex(elementIndex), + width: _getBorderWidthFromIndex(elementIndex), + ); + + switch (widget.authFieldShape) { + case AuthFieldShape.circle: + return CircleBorder(side: borderSide); + case AuthFieldShape.underline: + return Border(bottom: borderSide); + default: + return SmoothRectangleBorder( + borderRadius: _effectiveBorderRadius.smoothBorderRadius, + side: borderSide, + ); + } + } + + Future _updateTextField(String text) async { + final updatedList = List.filled(widget.authInputFieldCount, ''); + + for (int i = 0; i < widget.authInputFieldCount; i++) { + updatedList[i] = text.length > i ? text[i] : ''; + } + + _setState(() { + _selectedIndex = text.length; + _inputList = updatedList; + }); + } + + String? _validateInput() { + return widget.validator.call(_textEditingController.text); + } + + void _setState(void Function() function) { + if (mounted) setState(function); + } + + @override + void initState() { + super.initState(); + _initializeFields(); + } + + @override + void dispose() { + _textEditingController.dispose(); + _errorAnimationController!.dispose(); + _cursorController.dispose(); + _focusNode.dispose(); + _errorAnimationSubscription.cancel(); + + super.dispose(); + } + + List _generateAuthInputFields() { + final authInputFields = []; + + for (int i = 0; i < widget.authInputFieldCount; i++) { + authInputFields.add( + Padding( + padding: EdgeInsetsDirectional.only(end: i == widget.authInputFieldCount - 1 ? 0 : _effectiveGap), + child: RepaintBoundary( + child: AnimatedContainer( + curve: _animationCurve!, + duration: _animationDuration!, + width: _effectiveWidth, + height: _effectiveHeight, + decoration: ShapeDecoration( + shape: _getAuthInputFieldShape(elementIndex: i), + color: widget.enableInputFill ? _getFillColorFromIndex(i) : Colors.transparent, + shadows: (widget.activeBoxShadows != null || widget.inActiveBoxShadows != null) + ? _getBoxShadowFromIndex(i) + : widget.boxShadows, + ), + child: Center( + child: _buildChild(i), + ), + ), + ), + ), + ); + } + return authInputFields; + } + + Widget _buildChild(int index) { + if (((_selectedIndex == index) || (_selectedIndex == index + 1 && index + 1 == widget.authInputFieldCount)) && + _focusNode.hasFocus && + widget.showAuthFieldCursor) { + final cursorHeight = _resolvedTextStyle.fontSize!; + + if (_selectedIndex == index + 1 && index + 1 == widget.authInputFieldCount) { + return Stack( + alignment: Alignment.center, + children: [ + Center( + child: Padding( + padding: EdgeInsets.only(left: _resolvedTextStyle.fontSize! / 1.5), + child: RepaintBoundary( + child: FadeTransition( + opacity: _cursorAnimation, + child: CustomPaint( + size: Size(0, cursorHeight), + painter: _CursorPainter( + cursorColor: _effectiveCursorColor, + ), + ), + ), + ), + ), + ), + _renderAuthInputFieldText(index: index), + ], + ); + } else { + return Center( + child: RepaintBoundary( + child: FadeTransition( + opacity: _cursorAnimation, + child: CustomPaint( + size: Size(0, cursorHeight), + painter: _CursorPainter( + cursorColor: _effectiveCursorColor, + ), + ), + ), + ), + ); + } + } + return _renderAuthInputFieldText(index: index); + } + + Widget _renderAuthInputFieldText({@required int? index}) { + assert(index != null); + + final showObscured = !widget.peekWhenObscuring || + (widget.peekWhenObscuring && _hasPeeked) || + index != _inputList.where((x) => x.isNotEmpty).length - 1; + + if (widget.obscuringWidget != null && showObscured && _inputList[index!].isNotEmpty) { + return widget.obscuringWidget!; + } + + if (_inputList[index!].isEmpty && widget.hintCharacter != null) { + return Text( + widget.hintCharacter!, + key: ValueKey(_inputList[index]), + style: _hintStyle, + ); + } + + final text = widget.obscureText && _inputList[index].isNotEmpty && showObscured + ? widget.obscuringCharacter + : _inputList[index]; + + return Text( + text, + key: ValueKey(_inputList[index]), + style: _resolvedTextStyle, + ); + } + + Widget _getTextFormField() { + return Directionality( + textDirection: Directionality.of(context), + child: SizedBox( + height: _effectiveHeight, + child: TextFormField( + textInputAction: widget.textInputAction, + controller: _textEditingController, + focusNode: _focusNode, + enabled: widget.enabled, + autofocus: widget.autoFocus, + keyboardType: widget.keyboardType, + autovalidateMode: AutovalidateMode.onUserInteraction, + inputFormatters: [LengthLimitingTextInputFormatter(widget.authInputFieldCount)], + onFieldSubmitted: widget.onSubmitted, + onEditingComplete: widget.onEditingComplete, + onChanged: widget.onChanged, + enableInteractiveSelection: false, + enableSuggestions: false, + autocorrect: false, + showCursor: true, + smartDashesType: SmartDashesType.disabled, + cursorWidth: 0.01, + decoration: const InputDecoration( + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + ), + style: const TextStyle( + color: Colors.transparent, + height: .01, + fontSize: kIsWeb ? 1 : 0.01, + ), + scrollPadding: const EdgeInsets.all(24.0), + obscureText: widget.obscureText, + obscuringCharacter: widget.obscuringCharacter, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + _effectiveBorderRadius = widget.borderRadius ?? + context.moonTheme?.authCodeTheme.properties.borderRadius ?? + MoonBorders.borders.interactiveSm; + + _effectiveBorderWidth = widget.borderWidth ?? context.moonBorders?.borderWidth ?? MoonBorders.borders.borderWidth; + + _effectiveHeight = widget.height ?? context.moonTheme?.authCodeTheme.properties.height ?? MoonSizes.sizes.xl; + + _effectiveWidth = widget.width ?? context.moonTheme?.authCodeTheme.properties.width ?? MoonSizes.sizes.lg; + + _effectiveGap = widget.gap ?? context.moonTheme?.authCodeTheme.properties.gap ?? MoonSizes.sizes.x4s; + + _effectiveSelectedBorderColor = widget.selectedBorderColor ?? + context.moonTheme?.authCodeTheme.colors.selectedBorderColor ?? + MoonColors.light.piccolo; + + _effectiveActiveBorderColor = widget.activeBorderColor ?? + context.moonTheme?.authCodeTheme.colors.activeBorderColor ?? + MoonColors.light.beerus; + + _effectiveInactiveBorderColor = widget.inactiveBorderColor ?? + context.moonTheme?.authCodeTheme.colors.inactiveBorderColor ?? + MoonColors.light.beerus; + + _effectiveDisabledColor = + widget.disabledColor ?? context.moonTheme?.authCodeTheme.colors.disabledColor ?? MoonColors.light.goku; + + _effectiveErrorBorderColor = widget.errorBorderColor ?? + context.moonTheme?.authCodeTheme.colors.errorBorderColor ?? + MoonColors.light.chiChi100; + + _effectiveSelectedFillColor = + widget.selectedFillColor ?? context.moonTheme?.authCodeTheme.colors.selectedFillColor ?? MoonColors.light.gohan; + + _effectiveActiveFillColor = + widget.activeFillColor ?? context.moonTheme?.authCodeTheme.colors.activeFillColor ?? MoonColors.light.gohan; + + _effectiveInactiveFillColor = + widget.inactiveFillColor ?? context.moonTheme?.authCodeTheme.colors.inactiveFillColor ?? MoonColors.light.gohan; + + _effectiveTextStyle = context.moonTheme?.authCodeTheme.properties.textStyle ?? MoonTextStyles.body.text24; + + _effectiveErrorTextStyle = context.moonTheme?.authCodeTheme.properties.errorTextStyle ?? MoonTextStyles.body.text12; + + _effectiveTextColor = _getElementColor( + effectiveBackgroundColor: _effectiveActiveFillColor, + elementColor: widget.textColor, + ); + + _effectiveCursorColor = _getElementColor( + effectiveBackgroundColor: _effectiveSelectedFillColor, + elementColor: widget.authFieldCursorColor, + ); + + _animationDuration ??= widget.animationDuration ?? + context.moonTheme?.authCodeTheme.properties.animationDuration ?? + const Duration(milliseconds: 200); + + _animationCurve ??= + widget.animationCurve ?? context.moonTheme?.authCodeTheme.properties.animationCurve ?? Curves.easeInOutCubic; + + _peekDuration ??= widget.peekDuration ?? + context.moonTheme?.authCodeTheme.properties.peekDuration ?? + const Duration(milliseconds: 200); + + final double effectiveDisabledOpacityValue = context.moonTheme?.opacity.disabled ?? MoonOpacity.opacities.disabled; + + final Duration effectiveErrorAnimationDuration = widget.errorAnimationDuration ?? + context.moonTheme?.authCodeTheme.properties.errorAnimationDuration ?? + const Duration(milliseconds: 200); + + final Curve effectiveErrorAnimationCurve = widget.errorAnimationCurve ?? + context.moonTheme?.authCodeTheme.properties.errorAnimationCurve ?? + Curves.easeInOutCubic; + + _errorAnimationController ??= AnimationController( + duration: effectiveErrorAnimationDuration, + vsync: this, + ); + + _errorOffsetAnimation ??= Tween( + begin: Offset.zero, + end: const Offset(.01, 0.0), + ).animate( + CurvedAnimation( + parent: _errorAnimationController!, + curve: effectiveErrorAnimationCurve, + ), + ); + + return Semantics( + label: widget.semanticLabel, + child: Column( + children: [ + RepaintBoundary( + child: AnimatedOpacity( + duration: _animationDuration!, + curve: _animationCurve!, + opacity: widget.enabled ? 1 : effectiveDisabledOpacityValue, + child: SlideTransition( + position: _errorOffsetAnimation!, + child: Stack( + children: [ + AbsorbPointer( + // child: AutofillGroup(child: textField), + child: AutofillGroup(child: _getTextFormField()), + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: GestureDetector( + onTap: () => _onFocus(), + child: Row( + mainAxisAlignment: widget.mainAxisAlignment, + children: _generateAuthInputFields(), + ), + ), + ), + ], + ), + ), + ), + ), + if (_isInErrorMode) + RepaintBoundary( + child: AnimatedDefaultTextStyle( + style: _resolvedErrorTextStyle, + duration: _animationDuration!, + child: AnimatedIconTheme( + duration: _animationDuration!, + curve: _animationCurve!, + child: widget.errorBuilder(context, _validateInput()), + ), + ), + ) + ], + ), + ); + } +} + +class _CursorPainter extends CustomPainter { + final Color cursorColor; + + _CursorPainter({required this.cursorColor}); + + @override + void paint(Canvas canvas, Size size) { + const p1 = Offset.zero; + final p2 = Offset(0, size.height); + final paint = Paint() + ..color = cursorColor + ..strokeWidth = 2; + canvas.drawLine(p1, p2, paint); + } + + @override + bool shouldRepaint(CustomPainter old) { + return false; + } +}