diff --git a/lib/src/core/design_system/components/atoms/buttons/app_button.dart b/lib/src/core/design_system/components/atoms/buttons/app_button.dart new file mode 100644 index 00000000..faf42b6c --- /dev/null +++ b/lib/src/core/design_system/components/atoms/buttons/app_button.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/interactive_pressable.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:spark/src/core/design_system/tokens/typography.dart'; + +class AppButton extends StatelessWidget { + final String label; + final VoidCallback? onPressed; + final AppButtonVariant variant; + final AppButtonSize size; + final Widget? leading; + final Widget? trailing; + final bool fullWidth; + final double? minWidth; + final double? minHeight; + final EdgeInsetsGeometry? padding; + final BorderRadius? borderRadius; + final TextStyle? textStyle; + + const AppButton({ + required this.label, + super.key, + this.onPressed, + this.variant = AppButtonVariant.primary, + this.size = AppButtonSize.medium, + this.leading, + this.trailing, + this.fullWidth = false, + this.minWidth, + this.minHeight, + this.padding, + this.borderRadius, + this.textStyle, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final colorScheme = theme.colorScheme; + final isEnabled = onPressed != null; + final resolvedSize = _AppButtonSizeStyle.from(size); + final resolvedVariant = _AppButtonVariantStyle.resolve( + variant: variant, + isDark: isDark, + colorScheme: colorScheme, + isEnabled: isEnabled, + ); + final resolvedBorderRadius = borderRadius ?? resolvedSize.borderRadius; + + final labelText = Text( + label, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: (textStyle ?? resolvedSize.textStyle).copyWith( + color: resolvedVariant.foregroundColor, + ), + ); + + final content = Row( + mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (leading != null) ...[leading!, SizedBox(width: resolvedSize.gap)], + if (fullWidth) Flexible(child: labelText) else labelText, + if (trailing != null) ...[SizedBox(width: resolvedSize.gap), trailing!], + ], + ); + + return Semantics( + button: true, + enabled: isEnabled, + child: InteractivePressable( + onTap: onPressed, + borderRadius: resolvedBorderRadius, + child: Container( + width: fullWidth ? double.infinity : null, + constraints: BoxConstraints( + minWidth: minWidth ?? resolvedSize.minWidth, + minHeight: minHeight ?? resolvedSize.minHeight, + ), + padding: padding ?? resolvedSize.padding, + decoration: BoxDecoration( + color: resolvedVariant.backgroundColor, + borderRadius: resolvedBorderRadius, + border: resolvedVariant.border, + ), + child: content, + ), + ), + ); + } +} + +enum AppButtonVariant { primary, secondary, neutral, destructive } + +enum AppButtonSize { compact, medium, large } + +class _AppButtonSizeStyle { + final double minWidth; + final double minHeight; + final double gap; + final EdgeInsetsGeometry padding; + final BorderRadius borderRadius; + final TextStyle textStyle; + + const _AppButtonSizeStyle({ + required this.minWidth, + required this.minHeight, + required this.gap, + required this.padding, + required this.borderRadius, + required this.textStyle, + }); + + factory _AppButtonSizeStyle.from(AppButtonSize size) { + switch (size) { + case AppButtonSize.compact: + return _AppButtonSizeStyle( + minWidth: 0, + minHeight: 40, + gap: 8, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 11), + borderRadius: BorderRadius.circular(8), + textStyle: AppTypography.textMediumBold, + ); + case AppButtonSize.medium: + return _AppButtonSizeStyle( + minWidth: 0, + minHeight: 48, + gap: 8, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 18), + borderRadius: BorderRadius.circular(12), + textStyle: AppTypography.textMediumBold, + ); + case AppButtonSize.large: + return _AppButtonSizeStyle( + minWidth: 0, + minHeight: 60, + gap: 8, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), + borderRadius: BorderRadius.circular(16), + textStyle: AppTypography.textLargeMedium, + ); + } + } +} + +class _AppButtonVariantStyle { + final Color backgroundColor; + final Color foregroundColor; + final Border? border; + + const _AppButtonVariantStyle({ + required this.backgroundColor, + required this.foregroundColor, + this.border, + }); + + factory _AppButtonVariantStyle.resolve({ + required AppButtonVariant variant, + required bool isDark, + required ColorScheme colorScheme, + required bool isEnabled, + }) { + if (!isEnabled) { + return _AppButtonVariantStyle( + backgroundColor: colorScheme.surfaceContainerHighest.withValues( + alpha: 0.55, + ), + foregroundColor: colorScheme.onSurface.withValues(alpha: 0.55), + border: Border.all(color: colorScheme.outline.withValues(alpha: 0.45)), + ); + } + + switch (variant) { + case AppButtonVariant.primary: + return const _AppButtonVariantStyle( + backgroundColor: AppColors.primary600, + foregroundColor: AppColors.greyWhite, + ); + case AppButtonVariant.secondary: + return _AppButtonVariantStyle( + backgroundColor: isDark ? AppColors.primary900 : AppColors.primary50, + foregroundColor: isDark ? AppColors.primary100 : AppColors.primary700, + border: Border.all( + color: isDark ? AppColors.primary800 : AppColors.primary200, + ), + ); + case AppButtonVariant.neutral: + return _AppButtonVariantStyle( + backgroundColor: isDark + ? AppColors.darkGreyButton + : AppColors.lightGreyButton, + foregroundColor: isDark ? AppColors.greyWhite : AppColors.grey900, + border: Border.all( + color: isDark ? AppColors.greyBorder : AppColors.grey200, + width: 1.14667, + ), + ); + case AppButtonVariant.destructive: + return _AppButtonVariantStyle( + backgroundColor: isDark ? AppColors.red900 : AppColors.red50, + foregroundColor: isDark ? AppColors.red100 : AppColors.red700, + border: Border.all( + color: isDark ? AppColors.red800 : AppColors.red200, + ), + ); + } + } +} diff --git a/lib/src/core/design_system/components/atoms/buttons/long_button.dart b/lib/src/core/design_system/components/atoms/buttons/long_button.dart deleted file mode 100644 index 93035713..00000000 --- a/lib/src/core/design_system/components/atoms/buttons/long_button.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/interactive_pressable.dart'; -import 'package:spark/src/core/design_system/tokens/colors.dart'; -import 'package:spark/src/core/design_system/tokens/typography.dart'; - -/// A general-purpose button with gradient background that can display any text. -/// -/// It has an accent gradient background with a glass stroke border. -/// It's commonly used in authentication screens and forms, but can be used -/// anywhere a prominent action button is needed. -class LongButton extends StatelessWidget { - /// The text to display on the button - final String label; - - /// Callback when the button is pressed - final VoidCallback? onPressed; - - /// Button variant style - /// - [primary]: Pink/primary gradient (default) - /// - [regular]: Grey solid color - final LongButtonVariant variant; - - const LongButton({ - required this.label, - super.key, - this.onPressed, - this.variant = LongButtonVariant.primary, - }); - - @override - Widget build(BuildContext context) { - final isPrimary = variant == LongButtonVariant.primary; - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - final colorScheme = theme.colorScheme; - final isEnabled = onPressed != null; - final backgroundColor = !isEnabled - ? colorScheme.surfaceContainerHighest.withValues(alpha: 0.55) - : isPrimary - ? AppColors.primary600 - : (isDark ? AppColors.darkGreyButton : AppColors.lightGreyButton); - final border = !isEnabled - ? Border.all(color: colorScheme.outline.withValues(alpha: 0.45)) - : isPrimary - ? null - : Border.all( - color: isDark ? AppColors.greyBorder : AppColors.grey200, - width: 1.14667, - ); - final textColor = !isEnabled - ? colorScheme.onSurface.withValues(alpha: 0.55) - : isPrimary - ? AppColors.greyWhite - : (isDark ? AppColors.greyWhite : AppColors.grey900); - - return Semantics( - button: true, - enabled: isEnabled, - child: InteractivePressable( - onTap: onPressed, - borderRadius: BorderRadius.circular(8), - child: Container( - // height: 40, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 11), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(8), - border: border, - ), - child: Center( - child: Text( - label, - style: AppTypography.textMediumBold.copyWith(color: textColor), - ), - ), - ), - ), - ); - } -} - -/// Button variant styles for LongButton -enum LongButtonVariant { - /// Primary variant with pink/primary gradient - primary, - - /// Regular variant with grey solid color - regular, -} diff --git a/lib/src/core/design_system/components/atoms/buttons/primary_button.dart b/lib/src/core/design_system/components/atoms/buttons/primary_button.dart deleted file mode 100644 index 7be24c8c..00000000 --- a/lib/src/core/design_system/components/atoms/buttons/primary_button.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/interactive_pressable.dart'; -import 'package:spark/src/core/design_system/tokens/colors.dart'; -import 'package:spark/src/core/design_system/tokens/typography.dart'; - -/// A general-purpose primary button that can display any text. -/// -/// It uses the primary brand color and is commonly used in authentication -/// screens, but can be used anywhere a prominent primary action is needed. -/// It supports an optional trailing widget (e.g., SVG icon) and customizable -/// minimum size. -class PrimaryButton extends StatelessWidget { - /// The text to display on the button - final String text; - - /// Optional widget to display after the text (e.g., SVG logo) - final Widget? trailing; - - /// Callback when the button is pressed - final VoidCallback? onPressed; - - /// Minimum width of the button (default: 320) - final double? minWidth; - - /// Minimum height of the button (default: 60) - final double? minHeight; - - const PrimaryButton({ - required this.text, - this.trailing, - this.onPressed, - this.minWidth, - this.minHeight, - super.key, - }); - - @override - Widget build(BuildContext context) { - return InteractivePressable( - onTap: onPressed, - borderRadius: BorderRadius.circular(16), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), - constraints: BoxConstraints( - minWidth: minWidth ?? 320, - minHeight: minHeight ?? 60, - ), - decoration: BoxDecoration( - color: AppColors.primary600, - borderRadius: BorderRadius.circular(16), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - text, - style: AppTypography.textLargeMedium.copyWith( - color: AppColors.greyWhite, - ), - ), - if (trailing != null) ...[const SizedBox(width: 8), trailing!], - ], - ), - ), - ); - } -} diff --git a/lib/src/core/design_system/components/atoms/toggles/follow_button.dart b/lib/src/core/design_system/components/atoms/toggles/follow_button.dart deleted file mode 100644 index fd10feed..00000000 --- a/lib/src/core/design_system/components/atoms/toggles/follow_button.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/interactive_pressable.dart'; -import 'package:spark/src/core/design_system/tokens/colors.dart'; -import 'package:spark/src/core/design_system/tokens/typography.dart'; - -class FollowButton extends StatelessWidget { - const FollowButton({ - required this.isFollowing, - required this.onFollow, - required this.onUnfollow, - super.key, - this.followText = 'Follow', - this.unfollowText = 'Unfollow', - this.isBlocking = false, - this.onUnblock, - this.unblockText = 'Unblock', - this.width, - }); - - final bool isFollowing; - final VoidCallback onFollow; - final VoidCallback onUnfollow; - final String followText; - final String unfollowText; - final bool isBlocking; - final VoidCallback? onUnblock; - final String unblockText; - final double? width; - - @override - Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - - if (isBlocking && onUnblock != null) { - return InteractivePressable( - onTap: () { - HapticFeedback.mediumImpact(); - onUnblock!(); - }, - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: Container( - width: width ?? 109.47, - height: 36, - decoration: BoxDecoration( - color: isDark ? AppColors.red900 : AppColors.red50, - borderRadius: const BorderRadius.all(Radius.circular(8)), - border: Border.fromBorderSide( - BorderSide(color: isDark ? AppColors.red800 : AppColors.red200), - ), - ), - child: Align( - child: Text( - unblockText, - textAlign: TextAlign.center, - style: AppTypography.textSmallMedium.copyWith( - color: isDark ? AppColors.red400 : AppColors.red700, - ), - ), - ), - ), - ); - } - - return InteractivePressable( - onTap: () { - HapticFeedback.mediumImpact(); - if (isFollowing) { - onUnfollow(); - } else { - onFollow(); - } - }, - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: Container( - width: width ?? 109.47, - height: 36, - decoration: isFollowing - ? BoxDecoration( - color: isDark - ? AppColors.darkGreyButton - : AppColors.lightGreyButton, - borderRadius: const BorderRadius.all(Radius.circular(8)), - border: Border.fromBorderSide( - BorderSide( - color: isDark - ? AppColors.grey700.withValues(alpha: 0.3) - : AppColors.grey100.withValues(alpha: 0.3), - width: 1.14667, - ), - ), - ) - : const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8)), - color: AppColors.primary600, - ), - child: Align( - child: Text( - isFollowing ? unfollowText : followText, - textAlign: TextAlign.center, - style: AppTypography.textSmallMedium, - ), - ), - ), - ); - } -} diff --git a/lib/src/core/design_system/components/atoms/toggles/toggle_button.dart b/lib/src/core/design_system/components/atoms/toggles/toggle_button.dart new file mode 100644 index 00000000..709e0c75 --- /dev/null +++ b/lib/src/core/design_system/components/atoms/toggles/toggle_button.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/interactive_pressable.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:spark/src/core/design_system/tokens/typography.dart'; + +enum ToggleButtonTone { primary, neutral, danger } + +class ToggleButton extends StatelessWidget { + const ToggleButton({ + required this.isSelected, + required this.selectedLabel, + required this.unselectedLabel, + required this.onChanged, + super.key, + this.selectedTone = ToggleButtonTone.neutral, + this.unselectedTone = ToggleButtonTone.primary, + this.width, + this.height = 36, + }); + + final bool isSelected; + final String selectedLabel; + final String unselectedLabel; + final ValueChanged onChanged; + final ToggleButtonTone selectedTone; + final ToggleButtonTone unselectedTone; + final double? width; + final double height; + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final tone = isSelected ? selectedTone : unselectedTone; + + return InteractivePressable( + onTap: () { + HapticFeedback.mediumImpact(); + onChanged(!isSelected); + }, + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: Container( + width: width ?? 109.47, + height: height, + decoration: _decorationFor(tone, isDark), + child: Align( + child: Text( + isSelected ? selectedLabel : unselectedLabel, + textAlign: TextAlign.center, + style: AppTypography.textSmallMedium.copyWith( + color: _textColorFor(tone, isDark), + ), + ), + ), + ), + ); + } + + BoxDecoration _decorationFor(ToggleButtonTone tone, bool isDark) { + switch (tone) { + case ToggleButtonTone.primary: + return const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8)), + color: AppColors.primary600, + ); + case ToggleButtonTone.neutral: + return BoxDecoration( + color: isDark ? AppColors.darkGreyButton : AppColors.lightGreyButton, + borderRadius: const BorderRadius.all(Radius.circular(8)), + border: Border.fromBorderSide( + BorderSide( + color: isDark + ? AppColors.grey700.withValues(alpha: 0.3) + : AppColors.grey100.withValues(alpha: 0.3), + width: 1.14667, + ), + ), + ); + case ToggleButtonTone.danger: + return BoxDecoration( + color: isDark ? AppColors.red900 : AppColors.red50, + borderRadius: const BorderRadius.all(Radius.circular(8)), + border: Border.fromBorderSide( + BorderSide(color: isDark ? AppColors.red800 : AppColors.red200), + ), + ); + } + } + + Color _textColorFor(ToggleButtonTone tone, bool isDark) { + switch (tone) { + case ToggleButtonTone.primary: + return AppColors.greyWhite; + case ToggleButtonTone.neutral: + return isDark ? AppColors.greyWhite : AppColors.greyBlack; + case ToggleButtonTone.danger: + return isDark ? AppColors.red400 : AppColors.red700; + } + } +} diff --git a/lib/src/core/design_system/components/molecules/profile_action_buttons.dart b/lib/src/core/design_system/components/molecules/profile_action_buttons.dart index cd137444..02b19402 100644 --- a/lib/src/core/design_system/components/molecules/profile_action_buttons.dart +++ b/lib/src/core/design_system/components/molecules/profile_action_buttons.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/interactive_pressable.dart'; -import 'package:spark/src/core/design_system/components/atoms/toggles/follow_button.dart'; +import 'package:spark/src/core/design_system/components/atoms/toggles/toggle_button.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; @@ -37,13 +37,24 @@ class ProfileActionButtons extends StatelessWidget { return Row( children: [ Expanded( - child: FollowButton( - isFollowing: isFollowing, - isBlocking: isBlocking, - onFollow: onFollowTap ?? () {}, - onUnfollow: onUnfollowTap ?? () {}, - onUnblock: onUnblockTap, - unfollowText: l10n.labelFollowing, + child: ToggleButton( + isSelected: isBlocking && onUnblockTap != null ? true : isFollowing, + selectedLabel: isBlocking && onUnblockTap != null + ? l10n.buttonUnblock + : l10n.labelFollowing, + unselectedLabel: l10n.labelFollow, + selectedTone: isBlocking && onUnblockTap != null + ? ToggleButtonTone.danger + : ToggleButtonTone.neutral, + onChanged: (isSelected) { + if (isBlocking && onUnblockTap != null) { + onUnblockTap!(); + } else if (isSelected) { + onFollowTap?.call(); + } else { + onUnfollowTap?.call(); + } + }, width: double.infinity, ), ), @@ -53,7 +64,7 @@ class ProfileActionButtons extends StatelessWidget { } } -/// Edit button that matches the FollowButton's following state style +/// Edit button that matches the ToggleButton's selected state style class _EditButton extends StatelessWidget { const _EditButton({this.onTap}); diff --git a/lib/src/core/design_system/components/molecules/profile_card.dart b/lib/src/core/design_system/components/molecules/profile_card.dart index 5142ce42..a5a38b4d 100644 --- a/lib/src/core/design_system/components/molecules/profile_card.dart +++ b/lib/src/core/design_system/components/molecules/profile_card.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:spark/src/core/design_system/components/atoms/toggles/follow_button.dart'; +import 'package:spark/src/core/design_system/components/atoms/toggles/toggle_button.dart'; import 'package:spark/src/core/design_system/components/molecules/profile_avatar.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/design_system/tokens/shapes.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; class ProfileCard extends StatelessWidget { const ProfileCard({ @@ -72,6 +73,7 @@ class ProfileCard extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final isDark = Theme.of(context).brightness == Brightness.dark; final radius = BorderRadius.circular(AppShapes.squircleRadius); final borderColor = isDark ? AppColors.grey800 : AppColors.grey200; @@ -134,12 +136,26 @@ class ProfileCard extends StatelessWidget { ), if (showFollowButton) ...[ const SizedBox(width: 8), - FollowButton( - isFollowing: isFollowing, - isBlocking: isBlocking, - onFollow: onFollow, - onUnfollow: onUnfollow, - onUnblock: onUnblock, + ToggleButton( + isSelected: isBlocking && onUnblock != null + ? true + : isFollowing, + selectedLabel: isBlocking && onUnblock != null + ? l10n.buttonUnblock + : l10n.labelUnfollow, + unselectedLabel: l10n.labelFollow, + selectedTone: isBlocking && onUnblock != null + ? ToggleButtonTone.danger + : ToggleButtonTone.neutral, + onChanged: (isSelected) { + if (isBlocking && onUnblock != null) { + onUnblock!(); + } else if (isSelected) { + onFollow(); + } else { + onUnfollow(); + } + }, ), ], ], diff --git a/lib/src/core/design_system/templates/image_review_page_template.dart b/lib/src/core/design_system/templates/image_review_page_template.dart index 4f98407e..0cf0d6c4 100644 --- a/lib/src/core/design_system/templates/image_review_page_template.dart +++ b/lib/src/core/design_system/templates/image_review_page_template.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/app_button.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; import 'package:spark/src/core/design_system/components/atoms/icons.dart'; import 'package:spark/src/core/design_system/components/molecules/input_field.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; @@ -114,11 +114,12 @@ class ImageReviewPageTemplate extends StatelessWidget { const SizedBox(height: 20), SizedBox( width: double.infinity, - child: LongButton( + child: AppButton( label: canAddMore ? 'Add More Images ($imagesCount/$maxImages)' : 'Image Limit Reached', onPressed: canAddMore ? onAddMore : null, + size: AppButtonSize.compact, ), ), ], @@ -161,9 +162,10 @@ class ImageReviewPageTemplate extends StatelessWidget { ), ), ) - : LongButton( + : AppButton( label: postLabel, onPressed: isOverLimit ? null : onPost, + size: AppButtonSize.compact, ), ), ), diff --git a/lib/src/core/design_system/templates/video_review_page_template.dart b/lib/src/core/design_system/templates/video_review_page_template.dart index efb230cb..3bc417a4 100644 --- a/lib/src/core/design_system/templates/video_review_page_template.dart +++ b/lib/src/core/design_system/templates/video_review_page_template.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/app_button.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; import 'package:spark/src/core/design_system/components/molecules/input_field.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/design_system/tokens/shapes.dart'; @@ -149,9 +149,10 @@ class VideoReviewPageTemplate extends StatelessWidget { ), ), ) - : LongButton( + : AppButton( label: postLabel, onPressed: isOverLimit ? null : onPost, + size: AppButtonSize.compact, ), ), ), diff --git a/lib/src/core/l10n/app_localizations.dart b/lib/src/core/l10n/app_localizations.dart index a498f0f1..58a20646 100644 --- a/lib/src/core/l10n/app_localizations.dart +++ b/lib/src/core/l10n/app_localizations.dart @@ -1114,12 +1114,24 @@ abstract class AppLocalizations { /// **'Share'** String get labelShare; + /// Follow action label + /// + /// In en, this message translates to: + /// **'Follow'** + String get labelFollow; + /// Following label /// /// In en, this message translates to: /// **'Following'** String get labelFollowing; + /// Unfollow action label + /// + /// In en, this message translates to: + /// **'Unfollow'** + String get labelUnfollow; + /// Posts label /// /// In en, this message translates to: diff --git a/lib/src/core/l10n/app_localizations_en.dart b/lib/src/core/l10n/app_localizations_en.dart index 90da8db8..ee0a188e 100644 --- a/lib/src/core/l10n/app_localizations_en.dart +++ b/lib/src/core/l10n/app_localizations_en.dart @@ -554,9 +554,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get labelShare => 'Share'; + @override + String get labelFollow => 'Follow'; + @override String get labelFollowing => 'Following'; + @override + String get labelUnfollow => 'Unfollow'; + @override String get labelPosts => 'Posts'; diff --git a/lib/src/core/l10n/intl_en.arb b/lib/src/core/l10n/intl_en.arb index 8c8ac652..7827ed32 100644 --- a/lib/src/core/l10n/intl_en.arb +++ b/lib/src/core/l10n/intl_en.arb @@ -900,11 +900,21 @@ "description": "Share label" }, + "labelFollow": "Follow", + "@labelFollow": { + "description": "Follow action label" + }, + "labelFollowing": "Following", "@labelFollowing": { "description": "Following label" }, + "labelUnfollow": "Unfollow", + "@labelUnfollow": { + "description": "Unfollow action label" + }, + "labelPosts": "Posts", "@labelPosts": { "description": "Posts label" diff --git a/lib/src/core/ui/widgets/report_dialog.dart b/lib/src/core/ui/widgets/report_dialog.dart index 8a912a6a..0c7b2d40 100644 --- a/lib/src/core/ui/widgets/report_dialog.dart +++ b/lib/src/core/ui/widgets/report_dialog.dart @@ -6,8 +6,8 @@ import 'package:poptart_lex/com/atproto/repo/strong_ref.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/app_button.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; import 'package:spark/src/core/utils/logging/log_service.dart'; import 'package:spark/src/core/utils/logging/logger.dart'; @@ -554,7 +554,11 @@ class _ReportDialogState extends ConsumerState { height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) - : LongButton(label: l10n.buttonSubmit, onPressed: _submitReport), + : AppButton( + label: l10n.buttonSubmit, + onPressed: _submitReport, + size: AppButtonSize.compact, + ), ], ); } diff --git a/lib/src/features/auth/ui/pages/auth_prompt_page.dart b/lib/src/features/auth/ui/pages/auth_prompt_page.dart index c08672fb..3842ce07 100644 --- a/lib/src/features/auth/ui/pages/auth_prompt_page.dart +++ b/lib/src/features/auth/ui/pages/auth_prompt_page.dart @@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/primary_button.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/app_button.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/ui/foundation/colors.dart'; @@ -80,25 +80,29 @@ class AuthPromptPage extends StatelessWidget { ), ), const SizedBox(height: 60), - PrimaryButton( - text: 'Login with ', + AppButton( + label: 'Login with ', trailing: SvgPicture.asset( 'assets/images/ataccount.svg', height: 22, width: 100, ), + size: AppButtonSize.large, + minWidth: 320, onPressed: () { context.router.push(const LoginRoute()); }, ), const SizedBox(height: 16), - PrimaryButton( - text: 'Create an ', + AppButton( + label: 'Create an ', trailing: SvgPicture.asset( 'assets/images/ataccount.svg', height: 22, width: 100, ), + size: AppButtonSize.large, + minWidth: 320, onPressed: () { context.router.push(const RegisterRoute()); }, diff --git a/lib/src/features/auth/ui/pages/auth_recovery_page.dart b/lib/src/features/auth/ui/pages/auth_recovery_page.dart index 6e47f406..e203b370 100644 --- a/lib/src/features/auth/ui/pages/auth_recovery_page.dart +++ b/lib/src/features/auth/ui/pages/auth_recovery_page.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/app_button.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/routing/app_router.dart'; @@ -246,18 +246,20 @@ class _AuthRecoveryPageState extends ConsumerState { const SizedBox(height: 24), Opacity( opacity: isLoading ? 0.5 : 1.0, - child: LongButton( + child: AppButton( label: l10n.buttonContinueAs( _handleController.text.trim(), ), onPressed: isLoading ? null : _initiateOAuth, + size: AppButtonSize.compact, ), ), const SizedBox(height: 12), - LongButton( + AppButton( label: l10n.buttonGoToGetStarted, - variant: LongButtonVariant.regular, + variant: AppButtonVariant.neutral, onPressed: isLoading ? null : _goToGetStarted, + size: AppButtonSize.compact, ), ], ], diff --git a/lib/src/features/auth/ui/pages/login_page.dart b/lib/src/features/auth/ui/pages/login_page.dart index 57e607f1..49448cd4 100644 --- a/lib/src/features/auth/ui/pages/login_page.dart +++ b/lib/src/features/auth/ui/pages/login_page.dart @@ -6,8 +6,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/app_button.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/routing/app_router.dart'; @@ -375,9 +375,10 @@ class _LoginPageState extends ConsumerState { ? Duration.zero : const Duration(milliseconds: 140), opacity: isLoading ? 0.5 : 1.0, - child: LongButton( + child: AppButton( label: l10n.buttonContinue, onPressed: isLoading ? null : _initiateOAuth, + size: AppButtonSize.compact, ), ), ), diff --git a/lib/src/features/auth/ui/pages/register_page.dart b/lib/src/features/auth/ui/pages/register_page.dart index 1c93c992..1098a0ce 100644 --- a/lib/src/features/auth/ui/pages/register_page.dart +++ b/lib/src/features/auth/ui/pages/register_page.dart @@ -4,7 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/app_button.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/routing/app_router.dart'; @@ -196,16 +196,18 @@ class _RegisterPageState extends ConsumerState { if (!_hasReceivedCallback) ...[ Opacity( opacity: isLoading ? 0.5 : 1.0, - child: LongButton( + child: AppButton( label: l10n.buttonGetStarted, onPressed: isLoading ? null : _initiateOAuth, + size: AppButtonSize.compact, ), ), const SizedBox(height: 12), - LongButton( + AppButton( label: l10n.buttonHaveAccount, - variant: LongButtonVariant.regular, + variant: AppButtonVariant.neutral, onPressed: () => context.router.push(const LoginRoute()), + size: AppButtonSize.compact, ), const SizedBox(height: 16), ], diff --git a/lib/src/features/feed/ui/widgets/action_buttons/share_panel.dart b/lib/src/features/feed/ui/widgets/action_buttons/share_panel.dart index 8c62395b..f18e9380 100644 --- a/lib/src/features/feed/ui/widgets/action_buttons/share_panel.dart +++ b/lib/src/features/feed/ui/widgets/action_buttons/share_panel.dart @@ -5,7 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:share_plus/share_plus.dart'; import 'package:skeletonizer/skeletonizer.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/app_button.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/utils/logging/log_service.dart'; import 'package:spark/src/features/messages/providers/conversation_provider.dart'; @@ -218,33 +218,36 @@ class _SharePanelState extends ConsumerState { ? SizedBox( key: const ValueKey('selected-actions'), width: double.infinity, - child: LongButton( + child: AppButton( label: _sending ? l10n.messageSending : l10n.buttonSend, onPressed: _sending ? null : _sendToSelectedConversation, + size: AppButtonSize.compact, ), ) : Row( key: const ValueKey('default-actions'), children: [ Expanded( - child: LongButton( + child: AppButton( label: _copiedLink ? l10n.buttonCopied : l10n.buttonCopyLink, onPressed: _copyLink, - variant: LongButtonVariant.regular, + variant: AppButtonVariant.neutral, + size: AppButtonSize.compact, ), ), const SizedBox(width: 12), Expanded( - child: LongButton( + child: AppButton( label: l10n.buttonShare, onPressed: _shareNatively, - variant: LongButtonVariant.regular, + variant: AppButtonVariant.neutral, + size: AppButtonSize.compact, ), ), ], diff --git a/widgetbook/lib/atoms/follow_buttons_and_toggle.dart b/widgetbook/lib/atoms/follow_buttons_and_toggle.dart index 37d9d2d5..70cff40c 100644 --- a/widgetbook/lib/atoms/follow_buttons_and_toggle.dart +++ b/widgetbook/lib/atoms/follow_buttons_and_toggle.dart @@ -1,25 +1,24 @@ import 'package:flutter/material.dart'; -import 'package:spark/src/core/design_system/components/atoms/toggles/follow_button.dart'; +import 'package:spark/src/core/design_system/components/atoms/toggles/toggle_button.dart'; import 'package:widgetbook/widgetbook.dart'; import 'package:widgetbook_annotation/widgetbook_annotation.dart'; -@UseCase(name: 'follow_states', type: FollowButton) -Widget buildFollowButtonFollowStatesUseCase(BuildContext context) { - final isFollowing = context.knobs.boolean( - label: 'is_following', +@UseCase(name: 'states', type: ToggleButton) +Widget buildToggleButtonStatesUseCase(BuildContext context) { + final isSelected = context.knobs.boolean( + label: 'is_selected', initialValue: false, ); return Center( - child: FollowButton( - isFollowing: isFollowing, - onFollow: () => print('Follow pressed'), - onUnfollow: () => print('Unfollow pressed'), - followText: context.knobs.string( - label: 'follow_text', + child: ToggleButton( + isSelected: isSelected, + onChanged: (value) => print('Toggle changed to $value'), + unselectedLabel: context.knobs.string( + label: 'unselected_label', initialValue: 'Follow', ), - unfollowText: context.knobs.string( - label: 'unfollow_text', + selectedLabel: context.knobs.string( + label: 'selected_label', initialValue: 'Unfollow', ), ), diff --git a/widgetbook/lib/atoms/long_button.dart b/widgetbook/lib/atoms/long_button.dart index 23abe3b2..fa68f376 100644 --- a/widgetbook/lib/atoms/long_button.dart +++ b/widgetbook/lib/atoms/long_button.dart @@ -1,20 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/app_button.dart'; import 'package:widgetbook/widgetbook.dart'; import 'package:widgetbook_annotation/widgetbook_annotation.dart'; -// Assuming the LongButton component is located at this path -import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; - -@UseCase(name: 'no_ui', type: LongButton) -Widget buildLongButtonNoUIUseCase(BuildContext context) { - return LongButton( +@UseCase(name: 'no_ui', type: AppButton) +Widget buildAppButtonCompactNoUIUseCase(BuildContext context) { + return AppButton( label: context.knobs.string(label: 'Label', initialValue: 'Continue'), - onPressed: () => print('LongButton was pressed.'), + onPressed: () => print('AppButton was pressed.'), + size: AppButtonSize.compact, ); } -@UseCase(name: 'resizable_parent', type: LongButton) -Widget buildLongButtonInAResizableContainerUseCase(BuildContext context) { +@UseCase(name: 'resizable_parent', type: AppButton) +Widget buildAppButtonCompactInAResizableContainerUseCase(BuildContext context) { return Center( child: Container( width: context.knobs.double.slider( @@ -36,12 +35,13 @@ Widget buildLongButtonInAResizableContainerUseCase(BuildContext context) { color: Colors.grey.shade200, ), child: Center( - child: LongButton( + child: AppButton( label: context.knobs.string( label: 'Label', initialValue: 'Resizable Button', ), onPressed: () => print('Button in resizable container pressed.'), + size: AppButtonSize.compact, ), ), ), diff --git a/widgetbook/lib/atoms/primary_button.dart b/widgetbook/lib/atoms/primary_button.dart new file mode 100644 index 00000000..7aead2be --- /dev/null +++ b/widgetbook/lib/atoms/primary_button.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/app_button.dart'; +import 'package:spark/src/core/design_system/components/atoms/icons.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart'; + +@UseCase(name: 'default', type: AppButton) +Widget buildAppButtonLargeDefaultUseCase(BuildContext context) { + final isEnabled = context.knobs.boolean(label: 'Enabled', initialValue: true); + final hasTrailingIcon = context.knobs.boolean( + label: 'Trailing icon', + initialValue: false, + ); + + return Center( + child: AppButton( + label: context.knobs.string(label: 'Text', initialValue: 'Continue'), + minWidth: context.knobs.double.slider( + label: 'Min width', + initialValue: 320, + min: 160, + max: 420, + divisions: 26, + ), + minHeight: context.knobs.double.slider( + label: 'Min height', + initialValue: 60, + min: 44, + max: 84, + divisions: 20, + ), + trailing: hasTrailingIcon + ? AppIcons.arrowRight(size: 20, color: AppColors.greyWhite) + : null, + size: AppButtonSize.large, + onPressed: isEnabled ? () => print('AppButton was pressed.') : null, + ), + ); +}