From cbcbeeadddf6f57d191b3d5771c12f6829534c4b Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Wed, 20 May 2026 09:21:42 -0400 Subject: [PATCH] refactor: full design system pass --- .gitignore | 1 + assets/icons/add_post_filled.svg | 2 +- assets/icons/folder_mini.svg | 2 +- assets/icons/grid.svg | 2 +- assets/icons/home_filled.svg | 2 +- assets/icons/less.svg | 2 +- assets/icons/match.svg | 2 +- assets/icons/navbar_post.svg | 2 +- assets/icons/navbar_post_filled.svg | 2 +- assets/icons/profile_curate.svg | 1 - assets/icons/side_curate.svg | 1 - assets/icons/verified.svg | 2 +- .../components/atoms/avatar_stack.dart | 2 +- .../atoms/buttons/accent_button.dart | 44 --- .../atoms/buttons/action_button.dart | 77 ---- .../components/atoms/buttons/app_button.dart | 40 +- .../atoms/buttons/app_leading_button.dart | 109 +++-- .../buttons/app_overlay_back_button.dart | 13 +- .../atoms/buttons/follow_pill_button.dart | 51 --- .../components/atoms/buttons/live_button.dart | 42 -- .../atoms/buttons/tappable_text.dart | 24 -- .../components/atoms/buttons/text_button.dart | 39 -- .../atoms/default_profile_avatar.dart | 24 ++ .../design_system/components/atoms/icons.dart | 20 - .../components/atoms/tags/feed_tag.dart | 16 +- .../components/atoms/tags/hashtag.dart | 59 --- .../components/atoms/user_avatar.dart | 72 ++++ .../components/molecules/feed_tag_list.dart | 35 +- .../molecules/known_interactions_bar.dart | 10 +- .../components/molecules/profile_avatar.dart | 7 +- .../components/molecules/profile_info.dart | 2 +- .../components/molecules/story_circle.dart | 9 +- .../components/organisms/bottom_nav_bar.dart | 38 +- .../components/organisms/side_action_bar.dart | 271 +------------ .../templates/chat_list_page_template.dart | 2 +- .../templates/explore_page_template.dart | 41 +- .../templates/feeds_bar_template.dart | 91 ++--- .../templates/image_review_page_template.dart | 2 + .../templates/info_bar_template.dart | 14 +- .../templates/recording_page_template.dart | 23 +- .../templates/video_review_page_template.dart | 1 + lib/src/core/design_system/tokens/colors.dart | 56 +++ .../core/design_system/tokens/constants.dart | 1 - .../core/design_system/tokens/gradients.dart | 170 -------- .../widgets/story_mention_picker_sheet.dart | 2 +- lib/src/core/ui/foundation/colors.dart | 78 +--- .../core/ui/theme/data/models/app_theme.dart | 2 +- .../ui/widgets/alt_text_editor_dialog.dart | 2 +- .../ui/widgets/content_warning_overlay.dart | 2 +- .../ui/widgets/default_profile_avatar.dart | 25 +- lib/src/core/ui/widgets/image_content.dart | 2 +- lib/src/core/ui/widgets/mentioned_text.dart | 2 +- .../core/ui/widgets/menu_action_button.dart | 2 +- lib/src/core/ui/widgets/user_avatar.dart | 77 +--- lib/src/core/ui/widgets/video_content.dart | 2 +- lib/src/core/utils/text_formatter.dart | 2 +- .../steps/onboarding_avatar_step.dart | 2 +- .../auth/ui/pages/auth_prompt_page.dart | 2 +- .../features/auth/ui/pages/login_page.dart | 2 +- .../ui/pages/crosspost_comments_page.dart | 2 +- .../comments/ui/widgets/comment_input.dart | 2 +- .../comments/ui/widgets/comment_item.dart | 4 +- .../comments/ui/widgets/emoji_picker.dart | 2 +- lib/src/features/feed/ui/pages/feed_page.dart | 2 +- .../features/feed/ui/pages/feeds_page.dart | 2 +- .../feed/ui/pages/standalone_post_page.dart | 2 +- .../widgets/action_buttons/share_panel.dart | 3 + .../action_buttons/side_action_bar.dart | 17 - .../ui/widgets/images/image_carousel.dart | 2 +- .../ui/widgets/post/feed_post_skeleton.dart | 2 +- .../ui/widgets/post/feed_post_widget.dart | 2 +- .../feed/ui/widgets/post/no_more_posts.dart | 2 +- .../feed/ui/widgets/videos/video_player.dart | 2 +- .../features/messages/ui/pages/chat_page.dart | 2 +- .../messages/ui/widgets/message_bubble.dart | 2 +- .../messages/ui/widgets/message_input.dart | 4 +- .../messages/ui/widgets/sender_avatar.dart | 4 +- .../ui/widgets/notification_item.dart | 4 +- .../mention_text_editing_controller.dart | 2 +- .../ui/widgets/mention_input_field.dart | 2 +- .../profile/ui/pages/edit_profile_page.dart | 2 +- .../ui/pages/standalone_likes_feed_page.dart | 2 +- .../pages/standalone_profile_feed_page.dart | 2 +- .../pages/standalone_reposts_feed_page.dart | 2 +- .../ui/widgets/early_supporter_sheet.dart | 2 +- .../ui/widgets/profile_feed_post_widget.dart | 2 +- .../features/search/ui/pages/search_page.dart | 24 +- .../ui/pages/labeler_label_settings_page.dart | 2 +- .../ui/pages/labeler_management_page.dart | 2 +- .../stories/ui/pages/author_stories_page.dart | 2 +- .../components/app_button_test.dart | 42 ++ widgetbook/lib/atoms/accent_button.dart | 48 --- widgetbook/lib/atoms/action_button.dart | 49 --- .../atoms/interactive_pressable_and_tags.dart | 26 -- widgetbook/lib/atoms/live_button.dart | 95 ----- widgetbook/lib/atoms/text_button.dart | 53 --- widgetbook/lib/atoms/user_avatar.dart | 46 +++ widgetbook/lib/organisms/bottom_nav_bar.dart | 10 +- widgetbook/lib/organisms/side_action_bar.dart | 184 +-------- widgetbook/lib/templates/page_templates.dart | 372 ++++++++++++++++++ .../lib/templates/profile_page_template.dart | 4 +- .../lib/widgetbook_usecases_templates.dart | 20 - 102 files changed, 934 insertions(+), 1753 deletions(-) delete mode 100644 assets/icons/profile_curate.svg delete mode 100644 assets/icons/side_curate.svg delete mode 100644 lib/src/core/design_system/components/atoms/buttons/accent_button.dart delete mode 100644 lib/src/core/design_system/components/atoms/buttons/action_button.dart delete mode 100644 lib/src/core/design_system/components/atoms/buttons/follow_pill_button.dart delete mode 100644 lib/src/core/design_system/components/atoms/buttons/live_button.dart delete mode 100644 lib/src/core/design_system/components/atoms/buttons/tappable_text.dart delete mode 100644 lib/src/core/design_system/components/atoms/buttons/text_button.dart create mode 100644 lib/src/core/design_system/components/atoms/default_profile_avatar.dart delete mode 100644 lib/src/core/design_system/components/atoms/tags/hashtag.dart create mode 100644 lib/src/core/design_system/components/atoms/user_avatar.dart delete mode 100644 lib/src/core/design_system/tokens/gradients.dart create mode 100644 test/src/core/design_system/components/app_button_test.dart delete mode 100644 widgetbook/lib/atoms/accent_button.dart delete mode 100644 widgetbook/lib/atoms/action_button.dart delete mode 100644 widgetbook/lib/atoms/live_button.dart delete mode 100644 widgetbook/lib/atoms/text_button.dart create mode 100644 widgetbook/lib/atoms/user_avatar.dart create mode 100644 widgetbook/lib/templates/page_templates.dart diff --git a/.gitignore b/.gitignore index cb36bbc..5cec25e 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,4 @@ ios/Runner/GoogleService-Info.plist *.g.dart *.gr.dart assets/build +test/src/core/design_system/templates/explore_page_template_test.dart diff --git a/assets/icons/add_post_filled.svg b/assets/icons/add_post_filled.svg index 7fb3b1f..a18569c 100644 --- a/assets/icons/add_post_filled.svg +++ b/assets/icons/add_post_filled.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/folder_mini.svg b/assets/icons/folder_mini.svg index a2bee25..67b6e53 100644 --- a/assets/icons/folder_mini.svg +++ b/assets/icons/folder_mini.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/grid.svg b/assets/icons/grid.svg index d1cd017..686ca54 100644 --- a/assets/icons/grid.svg +++ b/assets/icons/grid.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/home_filled.svg b/assets/icons/home_filled.svg index 579cb6b..2852216 100644 --- a/assets/icons/home_filled.svg +++ b/assets/icons/home_filled.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/less.svg b/assets/icons/less.svg index a3938b9..ff4215c 100644 --- a/assets/icons/less.svg +++ b/assets/icons/less.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/match.svg b/assets/icons/match.svg index 9c93fe5..bbd4402 100644 --- a/assets/icons/match.svg +++ b/assets/icons/match.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/navbar_post.svg b/assets/icons/navbar_post.svg index 13de1b5..53bb69f 100644 --- a/assets/icons/navbar_post.svg +++ b/assets/icons/navbar_post.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/navbar_post_filled.svg b/assets/icons/navbar_post_filled.svg index 5af7217..039422e 100644 --- a/assets/icons/navbar_post_filled.svg +++ b/assets/icons/navbar_post_filled.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/profile_curate.svg b/assets/icons/profile_curate.svg deleted file mode 100644 index b4c6720..0000000 --- a/assets/icons/profile_curate.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/icons/side_curate.svg b/assets/icons/side_curate.svg deleted file mode 100644 index 10852d3..0000000 --- a/assets/icons/side_curate.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/icons/verified.svg b/assets/icons/verified.svg index 5b2eef7..bb18a5d 100644 --- a/assets/icons/verified.svg +++ b/assets/icons/verified.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/lib/src/core/design_system/components/atoms/avatar_stack.dart b/lib/src/core/design_system/components/atoms/avatar_stack.dart index cd46859..e731a8c 100644 --- a/lib/src/core/design_system/components/atoms/avatar_stack.dart +++ b/lib/src/core/design_system/components/atoms/avatar_stack.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; /// Data class representing an avatar in the stack. class AvatarData { diff --git a/lib/src/core/design_system/components/atoms/buttons/accent_button.dart b/lib/src/core/design_system/components/atoms/buttons/accent_button.dart deleted file mode 100644 index 22c79a2..0000000 --- a/lib/src/core/design_system/components/atoms/buttons/accent_button.dart +++ /dev/null @@ -1,44 +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/gradients.dart'; -import 'package:spark/src/core/design_system/tokens/typography.dart'; - -class AccentButton extends StatelessWidget { - final String label; - final VoidCallback? onPressed; - - const AccentButton({required this.label, super.key, this.onPressed}); - - @override - Widget build(BuildContext context) { - return InteractivePressable( - onTap: onPressed, - borderRadius: BorderRadius.circular(500), - child: Container( - constraints: BoxConstraints.tightFor( - height: 30, - width: label.length * 10 + 20, - ), - height: 30, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - gradient: AppGradients.accent, - borderRadius: BorderRadius.circular(500), // pill shape - border: const Border.fromBorderSide( - BorderSide(color: Color(0x25FFFFFF), width: 2), - ), - ), - child: Align( - child: Text( - label, - textAlign: TextAlign.center, - style: AppTypography.textSmallBold.copyWith( - color: AppColors.greyWhite, - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/core/design_system/components/atoms/buttons/action_button.dart b/lib/src/core/design_system/components/atoms/buttons/action_button.dart deleted file mode 100644 index dfe4b0b..0000000 --- a/lib/src/core/design_system/components/atoms/buttons/action_button.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'dart:ui'; - -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 ActionButton extends StatelessWidget { - final Widget icon; - final String label; - final VoidCallback? onPressed; - final bool isActive; - - const ActionButton({ - required this.icon, - required this.label, - super.key, - this.onPressed, - this.isActive = false, - }); - - @override - Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - const baseSize = 45.0; - const pressedScale = 0.9; - const iconSize = 34.0; - - final sizedIcon = SizedBox(width: iconSize, height: iconSize, child: icon); - - return SizedBox( - width: baseSize, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - InteractivePressable( - onTap: onPressed, - pressedScale: pressedScale, - borderRadius: BorderRadius.circular(baseSize), - child: ClipOval( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 2.5, sigmaY: 2.5), - child: Container( - width: baseSize, - height: baseSize, - decoration: isActive - ? const BoxDecoration( - color: AppColors.primary600, - shape: BoxShape.circle, - ) - : BoxDecoration( - color: isDark - ? AppColors.darkGreyButton - : AppColors.lightGreyButton, - shape: BoxShape.circle, - border: const Border.fromBorderSide( - BorderSide( - color: AppColors.greyBorder, - width: 1.14667, - ), - ), - ), - child: Center(child: sizedIcon), - ), - ), - ), - ), - Text( - label, - textAlign: TextAlign.center, - style: AppTypography.textSmallMedium.copyWith(color: Colors.white), - ), - ], - ), - ); - } -} 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 index faf42b6..ba253af 100644 --- a/lib/src/core/design_system/components/atoms/buttons/app_button.dart +++ b/lib/src/core/design_system/components/atoms/buttons/app_button.dart @@ -67,27 +67,31 @@ class AppButton extends StatelessWidget { ], ); + final button = 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, + ), + ); + 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, - ), - ), + child: fullWidth + ? button + : Align(widthFactor: 1, heightFactor: 1, child: button), ); } } diff --git a/lib/src/core/design_system/components/atoms/buttons/app_leading_button.dart b/lib/src/core/design_system/components/atoms/buttons/app_leading_button.dart index 371844f..4e8bb0e 100644 --- a/lib/src/core/design_system/components/atoms/buttons/app_leading_button.dart +++ b/lib/src/core/design_system/components/atoms/buttons/app_leading_button.dart @@ -2,10 +2,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:spark/src/core/design_system/components/atoms/icons.dart'; -/// Design System leading button that leverages AutoLeadingButton for -/// smart back/close/drawer behavior while keeping Spark's visual style. +/// Design System leading button for app bars and template surfaces. class AppLeadingButton extends StatelessWidget { - const AppLeadingButton({super.key, this.color, this.tooltip}); + const AppLeadingButton({super.key, this.color, this.tooltip, this.onPressed}); /// Optional override color for the icon. final Color? color; @@ -13,38 +12,82 @@ class AppLeadingButton extends StatelessWidget { /// Optional tooltip for accessibility. final String? tooltip; + /// Optional custom callback. + final VoidCallback? onPressed; + @override Widget build(BuildContext context) { - return AutoLeadingButton( - builder: - ( - BuildContext context, - LeadingType leadingType, - VoidCallback? action, - ) { - // If there's nothing to pop/open, render nothing. - if (action == null) return const SizedBox.shrink(); - - final theme = Theme.of(context); - final iconColor = color ?? theme.textTheme.titleLarge?.color; - - return Padding( - padding: const EdgeInsets.only(left: 4), - child: SizedBox( - width: 40, - height: 40, - child: Tooltip( - message: tooltip ?? 'Back', - child: GestureDetector( - onTap: action, - child: Center( - child: AppIcons.chevronleft(color: iconColor, size: 28), - ), - ), - ), - ), - ); - }, + if (onPressed == null && StackRouterScope.of(context) != null) { + return AutoLeadingButton( + builder: (context, leadingType, action) { + if (action == null) return const SizedBox.shrink(); + + return _SparkLeadingIconButton( + color: color, + tooltip: tooltip ?? (leadingType.isDrawer ? 'Menu' : 'Back'), + onPressed: action, + ); + }, + ); + } + + final action = onPressed ?? _resolvePreviewAction(context); + + if (action == null) return const SizedBox.shrink(); + + return _SparkLeadingIconButton( + color: color, + tooltip: tooltip ?? 'Back', + onPressed: action, + ); + } + + VoidCallback? _resolvePreviewAction(BuildContext context) { + final navigator = Navigator.maybeOf(context); + if (navigator != null && navigator.canPop()) { + return () => navigator.maybePop(); + } + + final scaffold = Scaffold.maybeOf(context); + if (scaffold != null && scaffold.hasDrawer) { + return scaffold.openDrawer; + } + + return null; + } +} + +class _SparkLeadingIconButton extends StatelessWidget { + const _SparkLeadingIconButton({ + required this.onPressed, + this.color, + this.tooltip = 'Back', + }); + + final VoidCallback onPressed; + final Color? color; + final String tooltip; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final iconColor = color ?? theme.textTheme.titleLarge?.color; + + return Padding( + padding: const EdgeInsets.only(left: 4), + child: SizedBox( + width: 40, + height: 40, + child: Tooltip( + message: tooltip, + child: GestureDetector( + onTap: onPressed, + child: Center( + child: AppIcons.chevronleft(color: iconColor, size: 28), + ), + ), + ), + ), ); } } diff --git a/lib/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart b/lib/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart index 45b8623..28b2d46 100644 --- a/lib/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart +++ b/lib/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:spark/src/core/design_system/components/atoms/icons.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; /// Design System overlay back button for full-screen/dark pages. /// @@ -18,11 +18,18 @@ class AppOverlayBackButton extends StatelessWidget { /// Color for the icon. Defaults to white for dark/overlay screens. final Color color; - /// Optional custom callback. If null, defaults to `context.router.maybePop()` + /// Optional custom callback. If null, falls back to Flutter navigation. final VoidCallback? onPressed; @override Widget build(BuildContext context) { + final autoRouter = StackRouterScope.of(context)?.controller; + final action = + onPressed ?? + (autoRouter != null + ? () => autoRouter.maybePopTop() + : () => Navigator.maybePop(context)); + return SafeArea( child: Padding( padding: const EdgeInsets.only(left: 4), @@ -32,7 +39,7 @@ class AppOverlayBackButton extends StatelessWidget { child: Tooltip( message: 'Back', child: GestureDetector( - onTap: onPressed ?? () => context.router.maybePop(), + onTap: action, child: Center( child: AppIcons.chevronleft(color: color, size: 28), ), diff --git a/lib/src/core/design_system/components/atoms/buttons/follow_pill_button.dart b/lib/src/core/design_system/components/atoms/buttons/follow_pill_button.dart deleted file mode 100644 index 604403b..0000000 --- a/lib/src/core/design_system/components/atoms/buttons/follow_pill_button.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:ui'; - -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 FollowPillButton extends StatelessWidget { - const FollowPillButton({ - required this.onPressed, - super.key, - this.label = 'Follow', - this.padding = const EdgeInsets.symmetric(horizontal: 14, vertical: 6), - }); - - final VoidCallback onPressed; - final String label; - final EdgeInsets padding; - - @override - Widget build(BuildContext context) { - return ClipRRect( - borderRadius: BorderRadius.circular(500), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), - child: InteractivePressable( - onTap: onPressed, - borderRadius: BorderRadius.circular(500), - overlayColor: Colors.white24, - child: Container( - padding: padding, - decoration: const BoxDecoration( - color: Color(0x33FFFFFF), - borderRadius: BorderRadius.all(Radius.circular(500)), - border: Border.fromBorderSide( - BorderSide(color: Color(0x25FFFFFF), width: 1), - ), - ), - child: Text( - label, - textAlign: TextAlign.center, - style: AppTypography.textSmallBold.copyWith( - color: AppColors.greyWhite, - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/core/design_system/components/atoms/buttons/live_button.dart b/lib/src/core/design_system/components/atoms/buttons/live_button.dart deleted file mode 100644 index 04f265d..0000000 --- a/lib/src/core/design_system/components/atoms/buttons/live_button.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'dart:ui'; // Required for ImageFilter - -import 'package:flutter/material.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/interactive_pressable.dart'; - -class LiveButton extends StatelessWidget { - final Widget child; - final VoidCallback? onTap; - - const LiveButton({required this.child, super.key, this.onTap}); - - @override - Widget build(BuildContext context) { - return Positioned( - left: 20, - top: 20, - child: InteractivePressable( - onTap: onTap, - borderRadius: BorderRadius.circular(50), - child: ClipRRect( - borderRadius: BorderRadius.circular(50), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: Container( - width: 38, - height: 38, - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: const Color.fromRGBO(255, 255, 255, 0.2), - border: Border.all( - color: const Color.fromRGBO(255, 255, 255, 0.5), - ), - borderRadius: BorderRadius.circular(50), - ), - child: Center(child: child), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/core/design_system/components/atoms/buttons/tappable_text.dart b/lib/src/core/design_system/components/atoms/buttons/tappable_text.dart deleted file mode 100644 index 7c82485..0000000 --- a/lib/src/core/design_system/components/atoms/buttons/tappable_text.dart +++ /dev/null @@ -1,24 +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/typography.dart'; - -class TappableText extends StatelessWidget { - final String text; - final VoidCallback onTap; - - const TappableText({required this.text, required this.onTap, super.key}); - - @override - Widget build(BuildContext context) { - return InteractivePressable( - onTap: onTap, - child: SizedBox( - width: 41, - height: 14, - child: Center( - child: Text(text, style: AppTypography.textExtraSmallThin), - ), - ), - ); - } -} diff --git a/lib/src/core/design_system/components/atoms/buttons/text_button.dart b/lib/src/core/design_system/components/atoms/buttons/text_button.dart deleted file mode 100644 index d2a717b..0000000 --- a/lib/src/core/design_system/components/atoms/buttons/text_button.dart +++ /dev/null @@ -1,39 +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'; - -class TextButton extends StatelessWidget { - final String label; - final VoidCallback? onTap; - - const TextButton({required this.label, super.key, this.onTap}); - - @override - Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return InteractivePressable( - onTap: onTap, - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: Container( - width: double.infinity, - constraints: const BoxConstraints(minHeight: 36), - padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 11), - decoration: BoxDecoration( - color: isDark ? AppColors.darkGreyButton : AppColors.lightGreyButton, - borderRadius: const BorderRadius.all(Radius.circular(8)), - border: const Border.fromBorderSide( - BorderSide(color: AppColors.greyBorder, width: 1.14667), - ), - ), - child: Align( - child: Text( - label, - textAlign: TextAlign.center, - style: AppTypography.textSmallMedium, - ), - ), - ), - ); - } -} diff --git a/lib/src/core/design_system/components/atoms/default_profile_avatar.dart b/lib/src/core/design_system/components/atoms/default_profile_avatar.dart new file mode 100644 index 0000000..d144fc1 --- /dev/null +++ b/lib/src/core/design_system/components/atoms/default_profile_avatar.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class DefaultProfileAvatar extends StatelessWidget { + const DefaultProfileAvatar({required this.size, super.key}); + + static const assetName = 'images/profile.svg'; + static const assetPackage = 'assets'; + + final double size; + + @override + Widget build(BuildContext context) { + return ClipOval( + child: SvgPicture.asset( + assetName, + package: assetPackage, + width: size, + height: size, + fit: BoxFit.cover, + ), + ); + } +} diff --git a/lib/src/core/design_system/components/atoms/icons.dart b/lib/src/core/design_system/components/atoms/icons.dart index 8e8b49c..7518076 100644 --- a/lib/src/core/design_system/components/atoms/icons.dart +++ b/lib/src/core/design_system/components/atoms/icons.dart @@ -417,16 +417,6 @@ class AppIcons { : null, package: 'assets', ); - static Widget profileCurate({double size = 24, Color? color}) => - SvgPicture.asset( - '$_path/profile_curate.svg', - width: size, - height: size, - colorFilter: color != null - ? ColorFilter.mode(color, BlendMode.srcIn) - : null, - package: 'assets', - ); static Widget profileLiked({double size = 24, Color? color}) => SvgPicture.asset( '$_path/profile_liked.svg', @@ -556,16 +546,6 @@ class AppIcons { : null, package: 'assets', ); - static Widget sideCurate({double size = 24, Color? color}) => - SvgPicture.asset( - '$_path/side_curate.svg', - width: size, - height: size, - colorFilter: color != null - ? ColorFilter.mode(color, BlendMode.srcIn) - : null, - package: 'assets', - ); static Widget gear({double size = 24, Color? color}) => SvgPicture.asset( '$_path/gear.svg', width: size, diff --git a/lib/src/core/design_system/components/atoms/tags/feed_tag.dart b/lib/src/core/design_system/components/atoms/tags/feed_tag.dart index f4bc0b3..6ecfce9 100644 --- a/lib/src/core/design_system/components/atoms/tags/feed_tag.dart +++ b/lib/src/core/design_system/components/atoms/tags/feed_tag.dart @@ -20,10 +20,12 @@ class FeedTag extends StatelessWidget { @override Widget build(BuildContext context) { + const radius = BorderRadius.all(Radius.circular(9)); + if (!selected) { return InteractivePressable( onTap: onTap, - borderRadius: BorderRadius.circular(9), + borderRadius: radius, child: Text( text, style: AppTypography.textMediumThin.copyWith(color: Colors.white70), @@ -32,18 +34,18 @@ class FeedTag extends StatelessWidget { } return InteractivePressable( onTap: onTap, - borderRadius: BorderRadius.circular(9), + borderRadius: radius, child: ClipRRect( - borderRadius: BorderRadius.circular(9), + borderRadius: radius, child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8), child: Container( height: 30, padding: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( - color: Colors.white.withAlpha(50), - borderRadius: BorderRadius.circular(9), - border: Border.all(color: Colors.white.withAlpha(37)), + color: Colors.white.withAlpha(70), + borderRadius: radius, + border: Border.all(color: Colors.white.withAlpha(55)), ), child: Center( child: Text( diff --git a/lib/src/core/design_system/components/atoms/tags/hashtag.dart b/lib/src/core/design_system/components/atoms/tags/hashtag.dart deleted file mode 100644 index 1703581..0000000 --- a/lib/src/core/design_system/components/atoms/tags/hashtag.dart +++ /dev/null @@ -1,59 +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/components/atoms/icons.dart'; -import 'package:spark/src/core/design_system/tokens/colors.dart'; -import 'package:spark/src/core/design_system/tokens/typography.dart'; - -class GlassmorphicTag extends StatelessWidget { - final String label; - final VoidCallback? onDeleted; - final VoidCallback? onTap; - - const GlassmorphicTag({ - required this.label, - super.key, - this.onDeleted, - this.onTap, - }); - - @override - Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return InteractivePressable( - onTap: onTap, - borderRadius: BorderRadius.circular(13), - child: Container( - // height: 38, - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), - decoration: BoxDecoration( - color: isDark ? AppColors.darkGreyButton : AppColors.lightGreyButton, - borderRadius: BorderRadius.circular(13), - border: Border.all(color: AppColors.greyBorder), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - AppIcons.hashtag(size: 14), - const SizedBox(width: 5), - Text(label, style: AppTypography.textMediumMedium), - ], - ), - const Spacer(), - if (onDeleted != null) - InteractivePressable( - onTap: onDeleted, - borderRadius: BorderRadius.circular(22), - child: Padding( - padding: EdgeInsets.all(3), - child: AppIcons.cancel(size: 16), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/core/design_system/components/atoms/user_avatar.dart b/lib/src/core/design_system/components/atoms/user_avatar.dart new file mode 100644 index 0000000..471e774 --- /dev/null +++ b/lib/src/core/design_system/components/atoms/user_avatar.dart @@ -0,0 +1,72 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:spark/src/core/design_system/components/atoms/default_profile_avatar.dart'; + +class UserAvatar extends StatelessWidget { + const UserAvatar({ + super.key, + this.imageUrl = '', + this.username = '', + this.size = 40, + this.borderColor, + this.borderWidth = 0, + this.backgroundColor, + this.fallbackTextColor, + }); + + final String imageUrl; + final String username; + final double size; + final Color? borderColor; + final double borderWidth; + final Color? backgroundColor; + final Color? fallbackTextColor; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final effectiveBorderColor = borderColor ?? colorScheme.outline; + final effectiveBackgroundColor = backgroundColor ?? colorScheme.primary; + + if (imageUrl.isEmpty) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: effectiveBackgroundColor, + shape: BoxShape.circle, + border: borderWidth > 0 + ? Border.all(color: effectiveBorderColor, width: borderWidth) + : null, + ), + child: DefaultProfileAvatar(size: size), + ); + } + + return Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: borderWidth > 0 + ? Border.all(color: effectiveBorderColor, width: borderWidth) + : null, + ), + clipBehavior: Clip.antiAlias, + child: CachedNetworkImage( + fadeInDuration: Duration.zero, + fadeOutDuration: Duration.zero, + imageUrl: imageUrl, + fit: BoxFit.cover, + placeholder: (context, url) => ColoredBox( + color: effectiveBackgroundColor, + child: DefaultProfileAvatar(size: size), + ), + errorWidget: (context, url, error) => ColoredBox( + color: effectiveBackgroundColor, + child: DefaultProfileAvatar(size: size), + ), + ), + ); + } +} diff --git a/lib/src/core/design_system/components/molecules/feed_tag_list.dart b/lib/src/core/design_system/components/molecules/feed_tag_list.dart index 0166548..ff2be91 100644 --- a/lib/src/core/design_system/components/molecules/feed_tag_list.dart +++ b/lib/src/core/design_system/components/molecules/feed_tag_list.dart @@ -184,39 +184,6 @@ class _FeedTagListState extends State { }, ); - final fadedList = widget.enableRightFade - ? LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth; - final fadeWidth = widget.rightFadeWidth.clamp(0, width); - return ShaderMask( - blendMode: BlendMode.dstIn, - shaderCallback: (bounds) { - if (fadeWidth == 0 || width == 0) { - return const LinearGradient( - colors: [Colors.white, Colors.white], - ).createShader(bounds); - } - - final fadeStart = ((width - fadeWidth) / width).clamp( - 0.0, - 1.0, - ); - return LinearGradient( - colors: const [ - Colors.white, - Colors.white, - Colors.transparent, - ], - stops: [0.0, fadeStart, 1.0], - ).createShader(bounds); - }, - child: listView, - ); - }, - ) - : listView; - - return SizedBox(height: 30, child: fadedList); + return SizedBox(height: 30, child: listView); } } diff --git a/lib/src/core/design_system/components/molecules/known_interactions_bar.dart b/lib/src/core/design_system/components/molecules/known_interactions_bar.dart index 2a45003..9b7cbac 100644 --- a/lib/src/core/design_system/components/molecules/known_interactions_bar.dart +++ b/lib/src/core/design_system/components/molecules/known_interactions_bar.dart @@ -7,7 +7,7 @@ import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/ui/theme/theme.dart'; /// A widget that displays known interactions (reposts and likes) as -/// overlapping avatar stacks with icons in frosted glass pill containers. +/// overlapping avatar stacks with icons in media-overlay pill containers. /// /// Shows reposts on the left with a green repost icon, and likes on the right /// with a pink heart icon. Only renders if there are actual interactions. @@ -82,17 +82,19 @@ class _InteractionPill extends StatelessWidget { @override Widget build(BuildContext context) { + const radius = BorderRadius.all(Radius.circular(500)); + return ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(500)), + borderRadius: radius, child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: const BoxDecoration( color: Color(0x33FFFFFF), - borderRadius: BorderRadius.all(Radius.circular(500)), + borderRadius: radius, border: Border.fromBorderSide( - BorderSide(color: Color(0x25FFFFFF), width: 1), + BorderSide(color: Color(0x30FFFFFF), width: 1), ), ), child: Row( diff --git a/lib/src/core/design_system/components/molecules/profile_avatar.dart b/lib/src/core/design_system/components/molecules/profile_avatar.dart index 8757e5e..615df35 100644 --- a/lib/src/core/design_system/components/molecules/profile_avatar.dart +++ b/lib/src/core/design_system/components/molecules/profile_avatar.dart @@ -1,9 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; -import 'package:spark/src/core/design_system/tokens/gradients.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; -import 'package:spark/src/core/ui/widgets/default_profile_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/default_profile_avatar.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; class ProfileAvatar extends StatelessWidget { const ProfileAvatar({ @@ -51,7 +50,7 @@ class ProfileAvatar extends StatelessWidget { decoration: hasStories ? const BoxDecoration( shape: BoxShape.circle, - gradient: AppGradients.accent, + color: AppColors.primary, ) : BoxDecoration( color: isDarkMode diff --git a/lib/src/core/design_system/components/molecules/profile_info.dart b/lib/src/core/design_system/components/molecules/profile_info.dart index d253686..522bf26 100644 --- a/lib/src/core/design_system/components/molecules/profile_info.dart +++ b/lib/src/core/design_system/components/molecules/profile_info.dart @@ -1,7 +1,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; class ProfileInfo extends StatelessWidget { const ProfileInfo({ diff --git a/lib/src/core/design_system/components/molecules/story_circle.dart b/lib/src/core/design_system/components/molecules/story_circle.dart index 91b3b9d..0344fec 100644 --- a/lib/src/core/design_system/components/molecules/story_circle.dart +++ b/lib/src/core/design_system/components/molecules/story_circle.dart @@ -1,10 +1,9 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; -import 'package:spark/src/core/design_system/tokens/gradients.dart'; +import 'package:spark/src/core/design_system/components/atoms/default_profile_avatar.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/ui/foundation/colors.dart'; -import 'package:spark/src/core/ui/widgets/default_profile_avatar.dart'; enum StoryType { story, live, cf, create } @@ -21,7 +20,7 @@ class StoryCircle extends StatelessWidget { required this.live, }); - /// Constructor variant for an unread story with a gradient border. + /// Constructor variant for an unread story with an accent border. factory StoryCircle.story({ required String userName, required String imageUrl, @@ -175,7 +174,7 @@ class _LiveBadge extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - gradient: AppGradients.accent, + color: AppColors.primary, borderRadius: BorderRadius.circular(8), border: Border.all(width: 2), ), diff --git a/lib/src/core/design_system/components/organisms/bottom_nav_bar.dart b/lib/src/core/design_system/components/organisms/bottom_nav_bar.dart index 019e38b..36c42bb 100644 --- a/lib/src/core/design_system/components/organisms/bottom_nav_bar.dart +++ b/lib/src/core/design_system/components/organisms/bottom_nav_bar.dart @@ -1,11 +1,9 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spark/src/core/design_system/components/atoms/default_profile_avatar.dart'; import 'package:spark/src/core/design_system/components/atoms/icons.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/design_system/tokens/constants.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; -import 'package:spark/src/core/ui/widgets/default_profile_avatar.dart'; import 'package:spark/src/features/notifications/providers/unread_count_provider.dart'; class SparkBottomNavBar extends ConsumerWidget { @@ -34,9 +32,6 @@ class SparkBottomNavBar extends ConsumerWidget { _BarBackground( child: Container( padding: EdgeInsets.only(top: 12, bottom: 12 + bottomPadding), - color: isDark - ? const Color.fromARGB(51, 0, 0, 0) - : const Color.fromARGB(178, 255, 255, 255), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, @@ -101,25 +96,22 @@ class _BarBackground extends StatelessWidget { final Widget child; @override Widget build(BuildContext context) { - return ClipRRect( - borderRadius: BorderRadius.circular(0), - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: AppConstants.blurBottomBar.toDouble(), - sigmaY: AppConstants.blurBottomBar.toDouble(), - ), - child: DecoratedBox( - decoration: BoxDecoration( - border: Border( - top: BorderSide( - color: Colors.white.withValues(alpha: 0.08), - width: 2, - ), - ), + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return DecoratedBox( + decoration: BoxDecoration( + color: isDark ? AppColors.grey900 : theme.colorScheme.surface, + border: Border( + top: BorderSide( + color: isDark + ? AppColors.grey700 + : theme.colorScheme.outline.withValues(alpha: 0.35), + width: 0.5, ), - child: child, ), ), + child: child, ); } } diff --git a/lib/src/core/design_system/components/organisms/side_action_bar.dart b/lib/src/core/design_system/components/organisms/side_action_bar.dart index 83518c4..29ecdf3 100644 --- a/lib/src/core/design_system/components/organisms/side_action_bar.dart +++ b/lib/src/core/design_system/components/organisms/side_action_bar.dart @@ -1,15 +1,6 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:spark/src/core/design_system/components/atoms/icons.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; - -/// Curate popover item data -class CurateDestination { - final String label; - final VoidCallback? onSelected; - const CurateDestination(this.label, {this.onSelected}); -} +import 'package:spark/src/core/design_system/tokens/colors.dart'; class SparkSideActionBar extends StatefulWidget { const SparkSideActionBar({ @@ -17,7 +8,6 @@ class SparkSideActionBar extends StatefulWidget { this.onLike, this.onComment, this.onRepost, - this.onCurate, this.onShare, this.onShareLongPress, this.onSoundTap, @@ -25,24 +15,15 @@ class SparkSideActionBar extends StatefulWidget { this.likeCount, this.commentCount, this.repostCount, - this.curateCount, this.shareCount, this.isLiked = false, this.isReposted = false, - this.isCurated = false, this.soundCover, - this.curateDestinations = const [ - CurateDestination('Feed 1'), - CurateDestination('Feed 2'), - CurateDestination('Feed 3'), - ], }); final VoidCallback? onLike; final VoidCallback? onComment; final VoidCallback? onRepost; - final VoidCallback? - onCurate; // called after a feed selection (or when opening?) final VoidCallback? onShare; final VoidCallback? onShareLongPress; final VoidCallback? onSoundTap; @@ -51,105 +32,17 @@ class SparkSideActionBar extends StatefulWidget { final String? likeCount; final String? commentCount; final String? repostCount; - final String? curateCount; final String? shareCount; final bool isLiked; final bool isReposted; - final bool isCurated; final String? soundCover; - final List curateDestinations; @override State createState() => _SparkSideActionBarState(); } class _SparkSideActionBarState extends State { - final GlobalKey _curateKey = GlobalKey(); - OverlayEntry? _overlay; - OverlayEntry? _overlayIcon; // icon overlay above popover - bool _showingPopover = false; - - @override - void dispose() { - _removePopover(); - super.dispose(); - } - - void _togglePopover() { - if (_showingPopover) { - _removePopover(); - } else { - _showPopover(); - } - } - - void _showPopover() { - if (!mounted) return; - final renderBox = - _curateKey.currentContext?.findRenderObject() as RenderBox?; - final overlayBox = - Overlay.of(context).context.findRenderObject() as RenderBox?; - if (renderBox == null || overlayBox == null) return; - - final target = renderBox.localToGlobal(Offset.zero, ancestor: overlayBox); - final size = renderBox.size; - final iconCenter = Offset( - target.dx + size.width / 2, - target.dy + size.height / 2, - ); - - _overlay = OverlayEntry( - builder: (ctx) { - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: _removePopover, - child: Stack( - children: [ - Positioned( - top: iconCenter.dy - 48, - right: MediaQuery.of(context).size.width - target.dx + 16 - 30, - child: _CuratePopover( - destinations: widget.curateDestinations, - onSelected: (d) { - d.onSelected?.call(); - widget.onCurate?.call(); - _removePopover(); - }, - ), - ), - ], - ), - ); - }, - ); - // Icon overlay above popover for z-order (bar bg -> popover -> actual icon) - _overlayIcon = OverlayEntry( - builder: (ctx) => Positioned( - left: target.dx, - top: target.dy, - width: size.width, - height: size.height, - child: IgnorePointer( - child: Center(child: AppIcons.sideCurate(size: 32)), - ), - ), - ); - - Overlay.of(context) - ..insert(_overlay!) // background popover - ..insert(_overlayIcon!); // icon on top - setState(() => _showingPopover = true); - } - - void _removePopover() { - _overlay?.remove(); - _overlayIcon?.remove(); - _overlay = null; - _overlayIcon = null; - _showingPopover = false; - } - @override Widget build(BuildContext context) { final children = [ @@ -182,19 +75,6 @@ class _SparkSideActionBarState extends State { ), ]; - if (widget.onCurate != null) { - children.addAll([ - const SizedBox(height: 13), - _ActionItem( - key: _curateKey, - isActive: widget.isCurated || _showingPopover, - icon: AppIcons.sideCurate(size: 32), - label: widget.curateCount, - onTap: _togglePopover, - ), - ]); - } - children.addAll([ const SizedBox(height: 13), _ActionItem( @@ -237,7 +117,6 @@ class _ActionItem extends StatefulWidget { this.onTap, this.onLongPress, this.isActive = false, - super.key, }); final Widget icon; @@ -358,151 +237,3 @@ class _SoundItem extends StatelessWidget { ); } } - -class _CuratePopover extends StatelessWidget { - const _CuratePopover({required this.destinations, required this.onSelected}); - final List destinations; - final ValueChanged onSelected; - - @override - Widget build(BuildContext context) { - return _ConnectorPopover( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < destinations.length; i++) ...[ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => onSelected(destinations[i]), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text( - destinations[i].label, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - height: 1.2, - shadows: [ - Shadow( - color: Colors.black54, - offset: Offset(0, 1), - blurRadius: 2, - ), - ], - ), - ), - ), - ), - if (i < destinations.length - 1) - Container(height: 1, color: Colors.white.withValues(alpha: 0.25)), - ], - ], - ), - ); - } -} - -/// Popover shape: rounded rectangle with a circular connector bubble -/// protruding from the top-right that visually aligns with the curate icon. -class _ConnectorPopover extends StatelessWidget { - const _ConnectorPopover({required this.child}); - final Widget child; - - @override - Widget build(BuildContext context) { - return Material( - type: MaterialType.transparency, - child: _ConnectorShape( - child: ClipPath( - clipper: _ConnectorPopoverClipper(), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), - child: Container( - padding: const EdgeInsets.fromLTRB(20, 18, 60, 18), - decoration: const BoxDecoration(), - child: child, - ), - ), - ), - ), - ); - } -} - -class _ConnectorShape extends StatelessWidget { - const _ConnectorShape({required this.child}); - final Widget child; - - @override - Widget build(BuildContext context) { - return CustomPaint( - foregroundPainter: _ConnectorPopoverBorderPainter(), - painter: _ConnectorPopoverBackgroundPainter(), - child: child, - ); - } -} - -class _ConnectorPopoverClipper extends CustomClipper { - static const double radius = 22; // slightly larger pill - static const double connectorRadius = 24; // bigger circle to wrap icon - static const double overlap = 10; // how much circle cuts into main rect - static const double connectorShiftX = 16; // pushes circle further right - static const double connectorShiftDown = 24; // pushes circle downward - - @override - Path getClip(Size size) { - // Main body excludes connector width minus overlap - final bodyWidth = - size.width - (connectorRadius - connectorShiftX) + overlap; - final bodyRect = RRect.fromLTRBR( - 0, - 0, - bodyWidth, - size.height, - const Radius.circular(radius), - ); - final rectPath = Path()..addRRect(bodyRect); - final circleCenter = Offset( - bodyWidth - overlap + connectorShiftX, - connectorRadius + connectorShiftDown, - ); // shifted circle - final circlePath = Path() - ..addOval(Rect.fromCircle(center: circleCenter, radius: connectorRadius)); - return Path.combine(PathOperation.union, rectPath, circlePath); - } - - @override - bool shouldReclip(covariant CustomClipper oldClipper) => false; -} - -class _ConnectorPopoverBackgroundPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final clipper = _ConnectorPopoverClipper(); - final path = clipper.getClip(size); - final paintFill = Paint()..color = Colors.white.withValues(alpha: 0.20); - canvas.drawPath(path, paintFill); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} - -class _ConnectorPopoverBorderPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final clipper = _ConnectorPopoverClipper(); - final path = clipper.getClip(size); - final border = Paint() - ..color = Colors.white.withValues(alpha: 0.30) - ..style = PaintingStyle.stroke - ..strokeWidth = 1.5; - canvas.drawPath(path, border); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} diff --git a/lib/src/core/design_system/templates/chat_list_page_template.dart b/lib/src/core/design_system/templates/chat_list_page_template.dart index 2b05c13..6dae19b 100644 --- a/lib/src/core/design_system/templates/chat_list_page_template.dart +++ b/lib/src/core/design_system/templates/chat_list_page_template.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:spark/src/core/design_system/components/atoms/icons.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; class ChatListItemData { const ChatListItemData({ diff --git a/lib/src/core/design_system/templates/explore_page_template.dart b/lib/src/core/design_system/templates/explore_page_template.dart index bcfbb55..583dd63 100644 --- a/lib/src/core/design_system/templates/explore_page_template.dart +++ b/lib/src/core/design_system/templates/explore_page_template.dart @@ -1,17 +1,31 @@ +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; +import 'package:spark/src/core/design_system/components/molecules/input_field.dart'; class ExplorePageTemplate extends StatelessWidget { const ExplorePageTemplate({ - required this.searchWidget, required this.emptyStateWidget, required this.tabsWidget, required this.contentWidget, super.key, + this.searchWidget, + this.searchController, + this.searchFocusNode, + this.searchHintText = '', + this.onSearchSubmitted, + this.onClearSearch, + this.showClearSearch = false, this.showTabs = false, this.backgroundColor, }); - final Widget searchWidget; + final Widget? searchWidget; + final TextEditingController? searchController; + final FocusNode? searchFocusNode; + final String searchHintText; + final ValueChanged? onSearchSubmitted; + final VoidCallback? onClearSearch; + final bool showClearSearch; final Widget emptyStateWidget; final Widget? tabsWidget; final Widget contentWidget; @@ -29,7 +43,7 @@ class ExplorePageTemplate extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding(padding: const EdgeInsets.all(16), child: searchWidget), + Padding(padding: const EdgeInsets.all(16), child: _buildSearch()), if (showTabs && tabsWidget != null) tabsWidget!, Expanded(child: showTabs ? contentWidget : emptyStateWidget), ], @@ -37,4 +51,25 @@ class ExplorePageTemplate extends StatelessWidget { ), ); } + + Widget _buildSearch() { + if (searchWidget != null) return searchWidget!; + + return InputField.search( + controller: searchController, + focusNode: searchFocusNode, + hintText: searchHintText, + onSubmitted: onSearchSubmitted, + textInputAction: TextInputAction.search, + leadingWidgets: const [Icon(FluentIcons.search_24_regular, size: 20)], + actionWidgets: showClearSearch + ? [ + GestureDetector( + onTap: onClearSearch, + child: const Icon(FluentIcons.dismiss_24_regular, size: 20), + ), + ] + : null, + ); + } } diff --git a/lib/src/core/design_system/templates/feeds_bar_template.dart b/lib/src/core/design_system/templates/feeds_bar_template.dart index 5f313f7..bb87fb7 100644 --- a/lib/src/core/design_system/templates/feeds_bar_template.dart +++ b/lib/src/core/design_system/templates/feeds_bar_template.dart @@ -12,9 +12,8 @@ const kFeedsBarLeadingWidth = 40.0; /// Design-only template for the Feeds Bar. /// -/// This template renders the tags row with a built-in leading button and -/// a subtle top-to-bottom gradient backdrop. Implements [PreferredSizeWidget] -/// so it can be used as an AppBar. +/// This template renders the tags row with a built-in leading button. +/// Implements [PreferredSizeWidget] so it can be used as an AppBar. /// /// The leading button is built-in (similar to how [AppBar] handles leading) /// and displays a create post icon. Use [onLeadingPressed] to handle taps. @@ -48,64 +47,42 @@ class FeedsBarTemplate extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { - // Use a Column to let the widget size naturally based on SafeArea + content - // This avoids the preferredSize mismatch issue - return Stack( - clipBehavior: Clip.none, - children: [ - // Backdrop gradient - positioned to fill available space - const Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color.fromARGB(110, 0, 0, 0), Colors.transparent], - ), - ), - ), - ), - // Content with SafeArea - this determines the widget's intrinsic height - SafeArea( - bottom: false, - left: false, - right: false, - child: SizedBox( - height: kFeedsBarHeight, - child: Padding( - padding: const EdgeInsets.only(left: 4), - child: Row( - children: [ - // Leading button - matches AppLeadingButton structure exactly - SizedBox( - width: kFeedsBarLeadingWidth, - height: kFeedsBarLeadingWidth, - child: Tooltip( - message: 'Create post', - child: GestureDetector( - onTap: onLeadingPressed, - child: Center(child: AppIcons.addPostFilled(size: 28)), - ), - ), + return SafeArea( + bottom: false, + left: false, + right: false, + child: SizedBox( + height: kFeedsBarHeight, + child: Padding( + padding: const EdgeInsets.only(left: 4), + child: Row( + children: [ + SizedBox( + width: kFeedsBarLeadingWidth, + height: kFeedsBarLeadingWidth, + child: Tooltip( + message: 'Create post', + child: GestureDetector( + onTap: onLeadingPressed, + child: Center(child: AppIcons.addPostFilled(size: 28)), ), - const SizedBox(width: 8), - Expanded( - child: FeedTagList( - tags: tags, - selectedTagId: selectedTagId, - onTagTap: onTagTap, - onReorder: onReorder, - onLongPress: onLongPress, - enableReordering: enableReordering, - enableRightFade: true, - ), - ), - ], + ), + ), + const SizedBox(width: 8), + Expanded( + child: FeedTagList( + tags: tags, + selectedTagId: selectedTagId, + onTagTap: onTagTap, + onReorder: onReorder, + onLongPress: onLongPress, + enableReordering: enableReordering, + ), ), - ), + ], ), ), - ], + ), ); } } 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 0cf0d6c..7c5ede2 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 @@ -120,6 +120,7 @@ class ImageReviewPageTemplate extends StatelessWidget { : 'Image Limit Reached', onPressed: canAddMore ? onAddMore : null, size: AppButtonSize.compact, + fullWidth: true, ), ), ], @@ -166,6 +167,7 @@ class ImageReviewPageTemplate extends StatelessWidget { label: postLabel, onPressed: isOverLimit ? null : onPost, size: AppButtonSize.compact, + fullWidth: true, ), ), ), diff --git a/lib/src/core/design_system/templates/info_bar_template.dart b/lib/src/core/design_system/templates/info_bar_template.dart index 437a341..fccbae7 100644 --- a/lib/src/core/design_system/templates/info_bar_template.dart +++ b/lib/src/core/design_system/templates/info_bar_template.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/follow_pill_button.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/app_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/typography.dart'; @@ -116,7 +116,17 @@ class _InfoBarTemplateState extends State if (widget.showFollowButton) Padding( padding: const EdgeInsets.only(left: 8, top: 2), - child: FollowPillButton(onPressed: widget.onFollow ?? () {}), + child: AppButton( + label: 'Follow', + onPressed: widget.onFollow ?? () {}, + size: AppButtonSize.compact, + minHeight: 34, + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 7, + ), + borderRadius: BorderRadius.circular(999), + ), ), ], ), diff --git a/lib/src/core/design_system/templates/recording_page_template.dart b/lib/src/core/design_system/templates/recording_page_template.dart index 1795e65..10c1c8c 100644 --- a/lib/src/core/design_system/templates/recording_page_template.dart +++ b/lib/src/core/design_system/templates/recording_page_template.dart @@ -160,12 +160,14 @@ class _ProcessingOverlay extends StatelessWidget { @override Widget build(BuildContext context) { + const radius = BorderRadius.all(Radius.circular(20)); + return Positioned.fill( child: ColoredBox( color: Colors.black.withAlpha(110), child: Center( child: ClipRRect( - borderRadius: BorderRadius.circular(20), + borderRadius: radius, child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), child: Container( @@ -175,8 +177,8 @@ class _ProcessingOverlay extends StatelessWidget { ), decoration: BoxDecoration( color: Colors.black.withAlpha(130), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: Colors.white.withAlpha(35)), + borderRadius: radius, + border: Border.all(color: Colors.white.withAlpha(28)), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -248,22 +250,24 @@ class _ActionButton extends StatelessWidget { @override Widget build(BuildContext context) { + const radius = BorderRadius.all(Radius.circular(999)); + return GestureDetector( onTap: () { HapticFeedback.lightImpact(); onPressed(); }, child: ClipRRect( - borderRadius: BorderRadius.circular(999), + borderRadius: radius, child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( constraints: const BoxConstraints(minHeight: 40, minWidth: 40), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(999), + borderRadius: radius, color: Colors.black.withAlpha(90), - border: Border.all(color: Colors.white.withAlpha(40)), + border: Border.all(color: Colors.white.withAlpha(32)), ), child: Text( label, @@ -399,18 +403,19 @@ class _SoundButton extends StatelessWidget { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final effectiveLabel = label ?? l10n.buttonAddSound; + const radius = BorderRadius.all(Radius.circular(999)); return Center( child: ClipRRect( - borderRadius: BorderRadius.circular(999), + borderRadius: radius, child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( constraints: const BoxConstraints(maxWidth: 260, minHeight: 40), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(999), + borderRadius: radius, color: Colors.black.withAlpha(110), - border: Border.all(color: Colors.white.withAlpha(45)), + border: Border.all(color: Colors.white.withAlpha(35)), ), child: Row( mainAxisSize: MainAxisSize.min, 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 3bc417a..0e527c3 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 @@ -153,6 +153,7 @@ class VideoReviewPageTemplate extends StatelessWidget { label: postLabel, onPressed: isOverLimit ? null : onPost, size: AppButtonSize.compact, + fullWidth: true, ), ), ), diff --git a/lib/src/core/design_system/tokens/colors.dart b/lib/src/core/design_system/tokens/colors.dart index 41e52c7..dc040de 100644 --- a/lib/src/core/design_system/tokens/colors.dart +++ b/lib/src/core/design_system/tokens/colors.dart @@ -1,6 +1,62 @@ import 'package:flutter/material.dart'; class AppColors { + // Semantic aliases for the current app color language. These replace the + // older core/ui/foundation color class while keeping call sites readable. + static const primary = primary500; + static const secondary = brightPurple; + static const accent = richPurple; + + static const lightLavender = Color(0xffdcd9e2); + static const darkPurple = Color(0xff403848); + static const deepPurple = Color(0xff28232d); + static const richPurple = Color(0xff330072); + static const brightPurple = Color(0xffb20aff); + + static const pink = primary500; + static const white = greyWhite; + static const black = greyBlack; + static const nearBlack = Color(0xff0d0a0f); + + static const blue = Color(0xff0073ff); + static const lightBlue = Color(0xff40a9ff); + static const teal = Color(0xff00c9b8); + static const green = green500; + static const red = Color(0xffff3a2c); + static const orange = Color(0xffff7b00); + static const hintColor = Color(0xff808080); + + static const background = white; + static const lightBackground = white; + static const cardBackground = white; + static const darkBackground = nearBlack; + static const modalBackground = nearBlack; + + static const textPrimary = deepPurple; + static const textSecondary = darkPurple; + static const textLight = lightLavender; + static const textOnDark = white; + static const hintText = hintColor; + + static const border = darkPurple; + static const divider = lightLavender; + + static const success = green; + static const error = red; + static const warning = orange; + static const info = blue; + + static const likeColor = pink; + static const commentColor = blue; + static const repostColor = green; + static const followColor = blue; + static const unreadIndicator = pink; + + static const selectedIconLight = pink; + static const unselectedIconLight = darkPurple; + static const selectedIconDark = white; + static const unselectedIconDark = darkPurple; + static const primary50 = Color(0xfffef1f8); static const primary100 = Color(0xfffee5f4); diff --git a/lib/src/core/design_system/tokens/constants.dart b/lib/src/core/design_system/tokens/constants.dart index d2eaa0b..4d8c5d7 100644 --- a/lib/src/core/design_system/tokens/constants.dart +++ b/lib/src/core/design_system/tokens/constants.dart @@ -39,7 +39,6 @@ class AppConstants { static const int blur4 = 42; static const int blur5 = 52; static const int blur6 = 62; - static const int blurGlass = 5; // Character limits static const int postDescriptionMaxChars = 300; diff --git a/lib/src/core/design_system/tokens/gradients.dart b/lib/src/core/design_system/tokens/gradients.dart deleted file mode 100644 index 7f59cac..0000000 --- a/lib/src/core/design_system/tokens/gradients.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'dart:math' as math; - -import 'package:flutter/material.dart'; - -class AppGradients { - static const accent = LinearGradient( - begin: Alignment(-1.3914, 1.0057999999999998), - end: Alignment(2.0484, -1.3258), - stops: [0, 1], - colors: [Color(0xffff97cd), Color(0xffff349d)], - ); - - static const linear2 = LinearGradient( - begin: Alignment(-1.3914, 1.0057999999999998), - end: Alignment(2.0484, -1.3258), - stops: [0, 1], - colors: [Color(0xffffcee2), Color(0xfff99bb1)], - ); - - static const gradientLinearGlass = LinearGradient( - begin: Alignment.topLeft, - end: Alignment(1, 1.0028000000000001), - stops: [0, 0.37, 0.72, 1], - colors: [ - Color(0x4dffffff), - Color(0x26ffffff), - Color(0x1affffff), - Color(0x4dffffff), - ], - ); - - static const gradientLinearSecondaryGradient = LinearGradient( - begin: Alignment(-0.6918, 1), - end: Alignment(0.679, -1), - stops: [0, 1], - colors: [Color(0xffffcee2), Color(0xfff99bb1)], - ); - - static const gradientLinearPrimaryGradient = LinearGradient( - begin: Alignment(-0.28200000000000003, 1), - end: Alignment(0.28200000000000003, -1), - stops: [0, 1], - colors: [Color(0xffff97cd), Color(0xffff349d)], - ); - - static const gradientLinearGr4 = LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - stops: [0, 0.2822, 1], - colors: [Color(0xffff7f65), Color(0xfff63d68), Color(0xffffd9d1)], - ); - - static const gradientLinearGr5 = LinearGradient( - begin: Alignment.topLeft, - end: Alignment(0.6112, 0.1976), - stops: [0, 0.5312, 1], - colors: [Color(0xfffbb1c3), Color(0xff2834e7), Color(0xff000000)], - ); - - static const gradientLinearGr6 = LinearGradient( - begin: Alignment.topLeft, - end: Alignment(0.6112, 0.1976), - stops: [0, 0.5312, 1], - colors: [Color(0xffffd9d1), Color(0xff7042d2), Color(0xff000000)], - ); - - static const gradientLinearGr7 = LinearGradient( - begin: Alignment.topLeft, - end: Alignment(0.6112, 0.1976), - stops: [0, 0.5312, 1], - colors: [Color(0xff91f3f3), Color(0xff4648ff), Color(0xff000000)], - ); - - static const gradientLinearGr8 = LinearGradient( - begin: Alignment.topLeft, - end: Alignment(0.5524, 0.4710000000000001), - stops: [0, 0.2822, 1], - colors: [Color(0xffffccc1), Color(0xfff63d68), Color(0xff000000)], - ); - - static const gradientGreyGrey1 = LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - stops: [0, 1], - colors: [Color(0xff16171f), Color(0xff000000)], - ); - - static const gradientGreyGrey2 = LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - stops: [0, 1], - colors: [Color(0xff20212b), Color(0xff16171f)], - ); - - static const gradientGreyGrey3 = LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - stops: [0, 1], - colors: [Color(0xff373946), Color(0xff16171f)], - ); - - static const gradientGreyGrey4 = LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - stops: [0, 1], - colors: [Color(0xff4d4f60), Color(0xff292a36)], - ); - - static const backgroundLinearGlass = LinearGradient( - begin: Alignment(-0.8200000000000001, -0.8), - end: Alignment(0.72, 0.8200000000000001), - stops: [0, 0.3702, 0.7212, 1], - colors: [ - Color(0x4dffffff), - Color(0x26ffffff), - Color(0x1affffff), - Color(0x4dffffff), - ], - ); - - static const darkStroke = LinearGradient( - begin: Alignment(-0.8200000000000001, -0.8), - end: Alignment(0.72, 0.8200000000000001), - transform: GradientRotation(45 * (math.pi / 180)), - stops: [0, 0.3702, 0.7212, 1], - colors: [ - Color(0xFF313131), - Color(0xFF232323), - Color(0xFF1E1E1E), - Color(0xFF313131), - ], - ); - - static const softDarkStroke = LinearGradient( - begin: Alignment(-0.8200000000000001, -0.8), - end: Alignment(0.72, 0.8200000000000001), - transform: GradientRotation(45 * (math.pi / 180)), - stops: [0, 0.7212, 1], - colors: [Color(0xFF6F6F6F), Color(0xFF3D3D3D), Color(0xFF6F6F6F)], - ); - - static const lightStroke = LinearGradient( - begin: Alignment(-0.8200000000000001, -0.8), - end: Alignment(0.72, 0.8200000000000001), - transform: GradientRotation(45 * (math.pi / 180)), - stops: [0, 0.3702, 0.7212, 1], - colors: [ - Color(0xFFFEFEFE), - Color(0xFFFCFCFC), - Color(0xFFF1F1F1), - Color(0xFFFEFEFE), - ], - ); - - static const softLightStroke = LinearGradient( - begin: Alignment(-0.8200000000000001, -0.8), - end: Alignment(0.72, 0.8200000000000001), - transform: GradientRotation(45 * (math.pi / 180)), - stops: [0, 0.7212, 1], - colors: [Color(0xFFB0B0B0), Color(0xFFD2D2D2), Color(0xFFB0B0B0)], - ); - - static const green = LinearGradient( - colors: [Color(0xFF97FFBF), Color(0xFF34FF34)], - begin: Alignment(-0.8, -1), - end: Alignment(0.8, 1), - ); - - AppGradients._(); -} diff --git a/lib/src/core/pro_image_editor/ui/widgets/story_mention_picker_sheet.dart b/lib/src/core/pro_image_editor/ui/widgets/story_mention_picker_sheet.dart index 79d2009..27b6831 100644 --- a/lib/src/core/pro_image_editor/ui/widgets/story_mention_picker_sheet.dart +++ b/lib/src/core/pro_image_editor/ui/widgets/story_mention_picker_sheet.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; import 'package:spark/src/features/search/providers/actor_typeahead_provider.dart'; import 'package:spark/src/features/search/providers/actor_typeahead_state.dart'; import 'package:sprk_poptart/so/sprk/actor/defs.dart'; diff --git a/lib/src/core/ui/foundation/colors.dart b/lib/src/core/ui/foundation/colors.dart index bc26cd8..4863b0e 100644 --- a/lib/src/core/ui/foundation/colors.dart +++ b/lib/src/core/ui/foundation/colors.dart @@ -1,77 +1 @@ -import 'package:flutter/material.dart'; -// DEPRECATED, USE DESIGN SYSTEM TOKENS - -/// Application color constants. -/// -/// These colors are used throughout the application to ensure consistency -/// and maintainability. -class AppColors { - // Private constructor to prevent instantiation - AppColors._(); - - // Primary brand colors - static const Color primary = pink; - static const Color secondary = brightPurple; - static const Color accent = richPurple; - - // Purple variants - static const Color lightLavender = Color(0xFFDCD9E2); - static const Color darkPurple = Color(0xFF403848); - static const Color deepPurple = Color(0xFF28232D); - static const Color richPurple = Color(0xFF330072); - static const Color brightPurple = Color(0xFFB20AFF); - - // Core colors - static const Color pink = Color( - 0xFFFF2696, - ); // Main app color for buttons and highlights - static const Color white = Color(0xFFFFFFFF); - static const Color black = Color(0xFF000000); - static const Color nearBlack = Color(0xFF0D0A0F); - - // Functional colors - static const Color blue = Color(0xFF0073FF); // For followers - static const Color lightBlue = Color(0xFF40A9FF); // Lighter blue variant - static const Color teal = Color(0xFF00C9B8); // For bookmarks - static const Color green = Color(0xFF12DB59); // For comments - static const Color red = Color(0xFFFF3A2C); // For likes - static const Color orange = Color(0xFFFF7B00); // For alerts and danger - static const Color hintColor = Color(0xFF808080); - - // Background colors - static const Color background = white; - static const Color lightBackground = white; - static const Color cardBackground = white; - static const Color darkBackground = nearBlack; - static const Color modalBackground = nearBlack; - - // Text colors - static const Color textPrimary = deepPurple; - static const Color textSecondary = darkPurple; - static const Color textLight = lightLavender; - static const Color textOnDark = white; - static const Color hintText = hintColor; - - // UI element colors - static const Color border = darkPurple; - static const Color divider = lightLavender; - - // Status colors - static const Color success = green; - static const Color error = red; - static const Color warning = orange; - static const Color info = blue; - - // Interaction colors - static const Color likeColor = pink; - static const Color commentColor = blue; - static const Color repostColor = green; - static const Color followColor = blue; - static const Color unreadIndicator = pink; - - // Navigation icon colors - static const Color selectedIconLight = pink; - static const Color unselectedIconLight = darkPurple; - static const Color selectedIconDark = white; - static const Color unselectedIconDark = darkPurple; -} +export 'package:spark/src/core/design_system/tokens/colors.dart'; diff --git a/lib/src/core/ui/theme/data/models/app_theme.dart b/lib/src/core/ui/theme/data/models/app_theme.dart index 190fbf4..995b5e8 100644 --- a/lib/src/core/ui/theme/data/models/app_theme.dart +++ b/lib/src/core/ui/theme/data/models/app_theme.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; /// Application theme management class that provides theme data /// for both light and dark themes. diff --git a/lib/src/core/ui/widgets/alt_text_editor_dialog.dart b/lib/src/core/ui/widgets/alt_text_editor_dialog.dart index 40395da..5fb60e3 100644 --- a/lib/src/core/ui/widgets/alt_text_editor_dialog.dart +++ b/lib/src/core/ui/widgets/alt_text_editor_dialog.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; class AltTextEditorDialog extends StatefulWidget { const AltTextEditorDialog({ diff --git a/lib/src/core/ui/widgets/content_warning_overlay.dart b/lib/src/core/ui/widgets/content_warning_overlay.dart index a5c5c52..d14c097 100644 --- a/lib/src/core/ui/widgets/content_warning_overlay.dart +++ b/lib/src/core/ui/widgets/content_warning_overlay.dart @@ -1,7 +1,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; class ContentWarningOverlay extends StatelessWidget { const ContentWarningOverlay({ diff --git a/lib/src/core/ui/widgets/default_profile_avatar.dart b/lib/src/core/ui/widgets/default_profile_avatar.dart index d144fc1..f11b4f5 100644 --- a/lib/src/core/ui/widgets/default_profile_avatar.dart +++ b/lib/src/core/ui/widgets/default_profile_avatar.dart @@ -1,24 +1 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -class DefaultProfileAvatar extends StatelessWidget { - const DefaultProfileAvatar({required this.size, super.key}); - - static const assetName = 'images/profile.svg'; - static const assetPackage = 'assets'; - - final double size; - - @override - Widget build(BuildContext context) { - return ClipOval( - child: SvgPicture.asset( - assetName, - package: assetPackage, - width: size, - height: size, - fit: BoxFit.cover, - ), - ); - } -} +export 'package:spark/src/core/design_system/components/atoms/default_profile_avatar.dart'; diff --git a/lib/src/core/ui/widgets/image_content.dart b/lib/src/core/ui/widgets/image_content.dart index 95756d8..01c2f11 100644 --- a/lib/src/core/ui/widgets/image_content.dart +++ b/lib/src/core/ui/widgets/image_content.dart @@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/features/feed/ui/widgets/images/image_carousel.dart'; class ImageContent extends StatelessWidget { diff --git a/lib/src/core/ui/widgets/mentioned_text.dart b/lib/src/core/ui/widgets/mentioned_text.dart index bcd54e8..248267b 100644 --- a/lib/src/core/ui/widgets/mentioned_text.dart +++ b/lib/src/core/ui/widgets/mentioned_text.dart @@ -1,6 +1,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/utils/text_formatter.dart'; /// A widget that displays text with clickable @mentions diff --git a/lib/src/core/ui/widgets/menu_action_button.dart b/lib/src/core/ui/widgets/menu_action_button.dart index c0964ae..c56c1e3 100644 --- a/lib/src/core/ui/widgets/menu_action_button.dart +++ b/lib/src/core/ui/widgets/menu_action_button.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:spark/src/core/auth/data/repositories/auth_repository.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/ui/widgets/options_panel.dart'; class MenuActionButton extends StatelessWidget { diff --git a/lib/src/core/ui/widgets/user_avatar.dart b/lib/src/core/ui/widgets/user_avatar.dart index 3ba784b..892b8af 100644 --- a/lib/src/core/ui/widgets/user_avatar.dart +++ b/lib/src/core/ui/widgets/user_avatar.dart @@ -1,76 +1 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:spark/src/core/ui/widgets/default_profile_avatar.dart'; - -/// A customizable user avatar with fallback options when no image is available -class UserAvatar extends StatelessWidget { - const UserAvatar({ - super.key, - this.imageUrl = '', - this.username = '', - this.size = 40, - this.borderColor, - this.borderWidth = 0, - this.backgroundColor, - this.fallbackTextColor, - }); - final String imageUrl; - final String username; - final double size; - final Color? borderColor; - final double borderWidth; - final Color? backgroundColor; - final Color? fallbackTextColor; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - final effectiveBorderColor = borderColor ?? colorScheme.outline; - final effectiveBackgroundColor = backgroundColor ?? colorScheme.primary; - - // If no image URL is provided, show fallback avatar - if (imageUrl.isEmpty) { - return Container( - width: size, - height: size, - decoration: BoxDecoration( - color: effectiveBackgroundColor, - shape: BoxShape.circle, - border: borderWidth > 0 - ? Border.all(color: effectiveBorderColor, width: borderWidth) - : null, - ), - child: DefaultProfileAvatar(size: size), - ); - } - - // Otherwise, show the image with a placeholder - return Container( - width: size, - height: size, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: borderWidth > 0 - ? Border.all(color: effectiveBorderColor, width: borderWidth) - : null, - ), - clipBehavior: Clip.antiAlias, - child: CachedNetworkImage( - fadeInDuration: Duration.zero, - fadeOutDuration: Duration.zero, - imageUrl: imageUrl, - fit: BoxFit.cover, - placeholder: (context, url) => ColoredBox( - color: effectiveBackgroundColor, - child: DefaultProfileAvatar(size: size), - ), - errorWidget: (context, url, error) => ColoredBox( - color: effectiveBackgroundColor, - child: DefaultProfileAvatar(size: size), - ), - ), - ); - } -} +export 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; diff --git a/lib/src/core/ui/widgets/video_content.dart b/lib/src/core/ui/widgets/video_content.dart index db90a6c..8622f62 100644 --- a/lib/src/core/ui/widgets/video_content.dart +++ b/lib/src/core/ui/widgets/video_content.dart @@ -1,6 +1,6 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:video_player/video_player.dart'; class VideoContent extends StatefulWidget { diff --git a/lib/src/core/utils/text_formatter.dart b/lib/src/core/utils/text_formatter.dart index bedac59..107681b 100644 --- a/lib/src/core/utils/text_formatter.dart +++ b/lib/src/core/utils/text_formatter.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/ui/widgets/mentioned_text.dart'; /// Utility class for text formatting and processing diff --git a/lib/src/features/auth/ui/onboarding/steps/onboarding_avatar_step.dart b/lib/src/features/auth/ui/onboarding/steps/onboarding_avatar_step.dart index 39a541d..55cd810 100644 --- a/lib/src/features/auth/ui/onboarding/steps/onboarding_avatar_step.dart +++ b/lib/src/features/auth/ui/onboarding/steps/onboarding_avatar_step.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.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/ui/widgets/default_profile_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/default_profile_avatar.dart'; class OnboardingAvatarStep extends StatefulWidget { const OnboardingAvatarStep({ 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 3842ce0..e7a8529 100644 --- a/lib/src/features/auth/ui/pages/auth_prompt_page.dart +++ b/lib/src/features/auth/ui/pages/auth_prompt_page.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.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'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; @RoutePage() class AuthPromptPage extends StatelessWidget { diff --git a/lib/src/features/auth/ui/pages/login_page.dart b/lib/src/features/auth/ui/pages/login_page.dart index 49448cd..241178f 100644 --- a/lib/src/features/auth/ui/pages/login_page.dart +++ b/lib/src/features/auth/ui/pages/login_page.dart @@ -11,7 +11,7 @@ import 'package:spark/src/core/design_system/components/atoms/buttons/app_overla 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'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; import 'package:spark/src/features/auth/providers/auth_providers.dart'; import 'package:spark/src/features/auth/providers/onboarding_providers.dart'; import 'package:spark/src/features/search/providers/actor_typeahead_provider.dart'; diff --git a/lib/src/features/comments/ui/pages/crosspost_comments_page.dart b/lib/src/features/comments/ui/pages/crosspost_comments_page.dart index dcb942b..fa494b9 100644 --- a/lib/src/features/comments/ui/pages/crosspost_comments_page.dart +++ b/lib/src/features/comments/ui/pages/crosspost_comments_page.dart @@ -8,7 +8,7 @@ import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; import 'package:spark/src/core/ui/widgets/image_content.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; final crosspostCommentsProvider = FutureProvider.family, AtUri>((ref, anchorUri) async { diff --git a/lib/src/features/comments/ui/widgets/comment_input.dart b/lib/src/features/comments/ui/widgets/comment_input.dart index af99e6a..a3ea902 100644 --- a/lib/src/features/comments/ui/widgets/comment_input.dart +++ b/lib/src/features/comments/ui/widgets/comment_input.dart @@ -10,7 +10,7 @@ import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/design_system/tokens/constants.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; import 'package:spark/src/core/ui/widgets/alt_text_editor_dialog.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; import 'package:spark/src/features/auth/providers/auth_providers.dart'; import 'package:spark/src/features/comments/providers/comment_input_provider.dart'; import 'package:spark/src/features/comments/providers/comment_input_state.dart'; diff --git a/lib/src/features/comments/ui/widgets/comment_item.dart b/lib/src/features/comments/ui/widgets/comment_item.dart index 093b98a..adfa931 100644 --- a/lib/src/features/comments/ui/widgets/comment_item.dart +++ b/lib/src/features/comments/ui/widgets/comment_item.dart @@ -10,11 +10,11 @@ import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; import 'package:spark/src/core/routing/app_router.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/ui/widgets/image_content.dart'; import 'package:spark/src/core/ui/widgets/options_panel.dart'; import 'package:spark/src/core/ui/widgets/report_dialog.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; import 'package:spark/src/core/utils/utils.dart'; import 'package:spark/src/features/comments/providers/comment_provider.dart'; import 'package:spark/src/features/comments/providers/comment_state.dart'; diff --git a/lib/src/features/comments/ui/widgets/emoji_picker.dart b/lib/src/features/comments/ui/widgets/emoji_picker.dart index 6c1bc8e..f34806b 100644 --- a/lib/src/features/comments/ui/widgets/emoji_picker.dart +++ b/lib/src/features/comments/ui/widgets/emoji_picker.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; class EmojiPicker extends StatelessWidget { const EmojiPicker({ diff --git a/lib/src/features/feed/ui/pages/feed_page.dart b/lib/src/features/feed/ui/pages/feed_page.dart index 743b832..3b94e38 100644 --- a/lib/src/features/feed/ui/pages/feed_page.dart +++ b/lib/src/features/feed/ui/pages/feed_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/features/feed/providers/feed_action_controller.dart'; import 'package:spark/src/features/feed/providers/feed_provider.dart'; import 'package:spark/src/features/feed/providers/feed_refresh_trigger_provider.dart'; diff --git a/lib/src/features/feed/ui/pages/feeds_page.dart b/lib/src/features/feed/ui/pages/feeds_page.dart index 8d8ec26..68c20ab 100644 --- a/lib/src/features/feed/ui/pages/feeds_page.dart +++ b/lib/src/features/feed/ui/pages/feeds_page.dart @@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/features/feed/providers/feed_provider.dart'; import 'package:spark/src/features/feed/providers/feed_state.dart'; import 'package:spark/src/features/feed/ui/pages/feed_page.dart'; diff --git a/lib/src/features/feed/ui/pages/standalone_post_page.dart b/lib/src/features/feed/ui/pages/standalone_post_page.dart index 4706445..3a133c8 100644 --- a/lib/src/features/feed/ui/pages/standalone_post_page.dart +++ b/lib/src/features/feed/ui/pages/standalone_post_page.dart @@ -11,7 +11,7 @@ import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; import 'package:spark/src/core/routing/app_router.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/ui/widgets/content_warning_overlay.dart'; import 'package:spark/src/core/utils/label_utils.dart'; import 'package:spark/src/features/feed/providers/post_updates.dart'; 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 f18e938..1a665e3 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 @@ -226,6 +226,7 @@ class _SharePanelState extends ConsumerState { ? null : _sendToSelectedConversation, size: AppButtonSize.compact, + fullWidth: true, ), ) : Row( @@ -239,6 +240,7 @@ class _SharePanelState extends ConsumerState { onPressed: _copyLink, variant: AppButtonVariant.neutral, size: AppButtonSize.compact, + fullWidth: true, ), ), const SizedBox(width: 12), @@ -248,6 +250,7 @@ class _SharePanelState extends ConsumerState { onPressed: _shareNatively, variant: AppButtonVariant.neutral, size: AppButtonSize.compact, + fullWidth: true, ), ), ], diff --git a/lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart b/lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart index bda04be..5bb403b 100644 --- a/lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart +++ b/lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart @@ -401,20 +401,8 @@ class SideActionBarState extends ConsumerState { } catch (_) {} } - // Future _handleCurate() async { - // // For now, this is a placeholder for curate functionality - // // In the future, this could add the post to a custom feed or collection - // if (mounted) { - // ScaffoldMessenger.of(context).showSnackBar( - // const SnackBar(content: Text('Post curated to feed!')), - // ); - // } - // } - @override Widget build(BuildContext context) { - // Curation disabled: do not build curate destinations from feeds - final currentPost = _currentPost ?? widget.post; final authRepository = GetIt.instance(); final userDid = authRepository.did; @@ -423,13 +411,11 @@ class SideActionBarState extends ConsumerState { final commentCount = currentPost.replyCount ?? int.tryParse(widget.commentCount) ?? 0; - // final isCurated = currentPost.viewer?.repost != null; // Curation disabled return SparkSideActionBar( onLike: _handleLike, onComment: _handleCommentPressed, onRepost: _handleRepost, - // onCurate: _handleCurate, // Curation disabled onShare: _handleShare, onShareLongPress: _handleShareLongPress, onSoundTap: currentPost.sound != null ? _handleSoundTap : null, @@ -445,13 +431,10 @@ class SideActionBarState extends ConsumerState { likeCount: _likeCount.toString(), commentCount: commentCount.toString(), repostCount: _repostCount.toString(), - // curateCount: repostCount.toString(), // Curation disabled shareCount: widget.shareCount, isLiked: _isLiked, isReposted: _isReposted, soundCover: currentPost.sound?.coverArt.toString(), - // isCurated: isCurated, // Curation disabled - // curateDestinations: curateDestinations, // Curation disabled ); } } diff --git a/lib/src/features/feed/ui/widgets/images/image_carousel.dart b/lib/src/features/feed/ui/widgets/images/image_carousel.dart index 9c65166..f92e428 100644 --- a/lib/src/features/feed/ui/widgets/images/image_carousel.dart +++ b/lib/src/features/feed/ui/widgets/images/image_carousel.dart @@ -2,7 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/features/feed/ui/widgets/images/moderate_page_scroll_physics.dart'; class ImageCarousel extends ConsumerStatefulWidget { diff --git a/lib/src/features/feed/ui/widgets/post/feed_post_skeleton.dart b/lib/src/features/feed/ui/widgets/post/feed_post_skeleton.dart index 456ef5b..bcf4955 100644 --- a/lib/src/features/feed/ui/widgets/post/feed_post_skeleton.dart +++ b/lib/src/features/feed/ui/widgets/post/feed_post_skeleton.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:skeletonizer/skeletonizer.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; /// A skeleton loading placeholder for feed posts. /// Displays a shimmer animation over placeholder content that mimics diff --git a/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart b/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart index 678174d..8086c4c 100644 --- a/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart +++ b/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart @@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/routing/app_router.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/ui/widgets/content_warning_overlay.dart'; import 'package:spark/src/core/ui/widgets/heart_animation.dart'; import 'package:spark/src/core/utils/label_utils.dart'; diff --git a/lib/src/features/feed/ui/widgets/post/no_more_posts.dart b/lib/src/features/feed/ui/widgets/post/no_more_posts.dart index 57d92c9..a296453 100644 --- a/lib/src/features/feed/ui/widgets/post/no_more_posts.dart +++ b/lib/src/features/feed/ui/widgets/post/no_more_posts.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; class NoMorePosts extends StatelessWidget { const NoMorePosts({super.key}); diff --git a/lib/src/features/feed/ui/widgets/videos/video_player.dart b/lib/src/features/feed/ui/widgets/videos/video_player.dart index 0a65efe..91a2ec8 100644 --- a/lib/src/features/feed/ui/widgets/videos/video_player.dart +++ b/lib/src/features/feed/ui/widgets/videos/video_player.dart @@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:spark/src/core/design_system/components/atoms/icons.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/utils/logging/logging.dart'; import 'package:spark/src/features/feed/providers/feed_provider.dart'; import 'package:spark/src/features/feed/providers/feed_state.dart'; diff --git a/lib/src/features/messages/ui/pages/chat_page.dart b/lib/src/features/messages/ui/pages/chat_page.dart index da2d660..1880621 100644 --- a/lib/src/features/messages/ui/pages/chat_page.dart +++ b/lib/src/features/messages/ui/pages/chat_page.dart @@ -5,7 +5,7 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/design_system/templates/chat_thread_page_template.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/features/auth/providers/auth_providers.dart'; import 'package:spark/src/features/messages/providers/conversation_provider.dart'; import 'package:spark/src/features/messages/providers/polling_timer.dart'; diff --git a/lib/src/features/messages/ui/widgets/message_bubble.dart b/lib/src/features/messages/ui/widgets/message_bubble.dart index 64bd620..7aaf95f 100644 --- a/lib/src/features/messages/ui/widgets/message_bubble.dart +++ b/lib/src/features/messages/ui/widgets/message_bubble.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:spark/src/core/network/messages/data/models/message_models.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/features/messages/ui/widgets/sender_avatar.dart'; class MessageBubble extends StatelessWidget { diff --git a/lib/src/features/messages/ui/widgets/message_input.dart b/lib/src/features/messages/ui/widgets/message_input.dart index d0f8965..e4d5360 100644 --- a/lib/src/features/messages/ui/widgets/message_input.dart +++ b/lib/src/features/messages/ui/widgets/message_input.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/features/auth/providers/auth_providers.dart'; import 'package:spark/src/features/profile/providers/profile_provider.dart'; diff --git a/lib/src/features/messages/ui/widgets/sender_avatar.dart b/lib/src/features/messages/ui/widgets/sender_avatar.dart index 3cef08b..7db3c47 100644 --- a/lib/src/features/messages/ui/widgets/sender_avatar.dart +++ b/lib/src/features/messages/ui/widgets/sender_avatar.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; import 'package:spark/src/features/messages/ui/pages/chat_page.dart'; class SenderAvatar extends StatelessWidget { diff --git a/lib/src/features/notifications/ui/widgets/notification_item.dart b/lib/src/features/notifications/ui/widgets/notification_item.dart index 304d5de..ea9eb43 100644 --- a/lib/src/features/notifications/ui/widgets/notification_item.dart +++ b/lib/src/features/notifications/ui/widgets/notification_item.dart @@ -7,8 +7,8 @@ import 'package:spark/src/core/design_system/components/atoms/icons.dart'; import 'package:spark/src/core/network/atproto/data/models/notification_models.dart' as models; import 'package:spark/src/core/routing/app_router.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; import 'package:spark/src/features/messages/ui/pages/chat_page.dart'; import 'package:spark/src/features/notifications/models/grouped_notification.dart'; diff --git a/lib/src/features/posting/models/mention_text_editing_controller.dart b/lib/src/features/posting/models/mention_text_editing_controller.dart index a033288..4d6bfec 100644 --- a/lib/src/features/posting/models/mention_text_editing_controller.dart +++ b/lib/src/features/posting/models/mention_text_editing_controller.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/utils/text_formatter.dart'; import 'package:spark/src/features/posting/models/mention.dart'; diff --git a/lib/src/features/posting/ui/widgets/mention_input_field.dart b/lib/src/features/posting/ui/widgets/mention_input_field.dart index aa22e3c..137214d 100644 --- a/lib/src/features/posting/ui/widgets/mention_input_field.dart +++ b/lib/src/features/posting/ui/widgets/mention_input_field.dart @@ -3,7 +3,7 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/design_system/components/molecules/input_field.dart'; import 'package:spark/src/core/design_system/tokens/constants.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; import 'package:spark/src/core/utils/text_formatter.dart'; import 'package:spark/src/features/posting/models/mention.dart'; import 'package:spark/src/features/posting/models/mention_controller.dart'; diff --git a/lib/src/features/profile/ui/pages/edit_profile_page.dart b/lib/src/features/profile/ui/pages/edit_profile_page.dart index 0d80bdd..9c2346f 100644 --- a/lib/src/features/profile/ui/pages/edit_profile_page.dart +++ b/lib/src/features/profile/ui/pages/edit_profile_page.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/ui/widgets/custom_text_field.dart'; -import 'package:spark/src/core/ui/widgets/default_profile_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/default_profile_avatar.dart'; import 'package:spark/src/features/profile/providers/edit_profile_provider.dart'; import 'package:sprk_poptart/so/sprk/actor/defs.dart'; diff --git a/lib/src/features/profile/ui/pages/standalone_likes_feed_page.dart b/lib/src/features/profile/ui/pages/standalone_likes_feed_page.dart index 63c3b58..6cbc2d2 100644 --- a/lib/src/features/profile/ui/pages/standalone_likes_feed_page.dart +++ b/lib/src/features/profile/ui/pages/standalone_likes_feed_page.dart @@ -9,7 +9,7 @@ import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/design_system/tokens/constants.dart'; import 'package:spark/src/core/routing/app_router.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/features/feed/ui/widgets/feed/cacheable_page_view.dart'; import 'package:spark/src/features/feed/ui/widgets/feed/snappy_page_scroll_physics.dart'; import 'package:spark/src/features/profile/providers/profile_feed_index_provider.dart'; diff --git a/lib/src/features/profile/ui/pages/standalone_profile_feed_page.dart b/lib/src/features/profile/ui/pages/standalone_profile_feed_page.dart index 87cae3d..868eb0a 100644 --- a/lib/src/features/profile/ui/pages/standalone_profile_feed_page.dart +++ b/lib/src/features/profile/ui/pages/standalone_profile_feed_page.dart @@ -9,7 +9,7 @@ import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/design_system/tokens/constants.dart'; import 'package:spark/src/core/routing/app_router.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/features/feed/ui/widgets/feed/cacheable_page_view.dart'; import 'package:spark/src/features/feed/ui/widgets/feed/snappy_page_scroll_physics.dart'; import 'package:spark/src/features/profile/providers/profile_feed_index_provider.dart'; diff --git a/lib/src/features/profile/ui/pages/standalone_reposts_feed_page.dart b/lib/src/features/profile/ui/pages/standalone_reposts_feed_page.dart index f4d3617..e671604 100644 --- a/lib/src/features/profile/ui/pages/standalone_reposts_feed_page.dart +++ b/lib/src/features/profile/ui/pages/standalone_reposts_feed_page.dart @@ -9,7 +9,7 @@ import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/design_system/tokens/constants.dart'; import 'package:spark/src/core/routing/app_router.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/features/feed/ui/widgets/feed/cacheable_page_view.dart'; import 'package:spark/src/features/feed/ui/widgets/feed/snappy_page_scroll_physics.dart'; import 'package:spark/src/features/profile/providers/profile_feed_index_provider.dart'; diff --git a/lib/src/features/profile/ui/widgets/early_supporter_sheet.dart b/lib/src/features/profile/ui/widgets/early_supporter_sheet.dart index f37e011..1f55769 100644 --- a/lib/src/features/profile/ui/widgets/early_supporter_sheet.dart +++ b/lib/src/features/profile/ui/widgets/early_supporter_sheet.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; class EarlySupporterSheet extends StatelessWidget { const EarlySupporterSheet({super.key}); diff --git a/lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart b/lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart index dd56ed4..91800cc 100644 --- a/lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart +++ b/lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart @@ -6,7 +6,7 @@ import 'package:get_it/get_it.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; import 'package:spark/src/core/routing/app_router.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; +import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/ui/widgets/content_warning_overlay.dart'; import 'package:spark/src/core/ui/widgets/heart_animation.dart'; import 'package:spark/src/core/utils/label_utils.dart'; diff --git a/lib/src/features/search/ui/pages/search_page.dart b/lib/src/features/search/ui/pages/search_page.dart index 0e189ef..9729bfc 100644 --- a/lib/src/features/search/ui/pages/search_page.dart +++ b/lib/src/features/search/ui/pages/search_page.dart @@ -1,14 +1,12 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spark/src/core/design_system/components/molecules/input_field.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; import 'package:spark/src/core/design_system/templates/explore_page_template.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/routing/app_router.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; import 'package:spark/src/features/search/providers/actor_typeahead_provider.dart'; import 'package:spark/src/features/search/providers/actor_typeahead_state.dart'; import 'package:spark/src/features/search/providers/post_search_provider.dart'; @@ -114,21 +112,11 @@ class _SearchPageState extends ConsumerState { return DefaultTabController( length: 2, child: ExplorePageTemplate( - searchWidget: InputField.search( - controller: _searchController, - hintText: l10n.hintSearchUsersPosts, - onSubmitted: _onSubmitted, - textInputAction: TextInputAction.search, - leadingWidgets: const [Icon(FluentIcons.search_24_regular, size: 20)], - actionWidgets: _searchController.text.isNotEmpty - ? [ - GestureDetector( - onTap: _searchController.clear, - child: const Icon(FluentIcons.dismiss_24_regular, size: 20), - ), - ] - : null, - ), + searchController: _searchController, + searchHintText: l10n.hintSearchUsersPosts, + onSearchSubmitted: _onSubmitted, + onClearSearch: _searchController.clear, + showClearSearch: _searchController.text.isNotEmpty, showTabs: showSubmittedResults, tabsWidget: TabBar( tabs: [ diff --git a/lib/src/features/settings/ui/pages/labeler_label_settings_page.dart b/lib/src/features/settings/ui/pages/labeler_label_settings_page.dart index 17fff41..0385b02 100644 --- a/lib/src/features/settings/ui/pages/labeler_label_settings_page.dart +++ b/lib/src/features/settings/ui/pages/labeler_label_settings_page.dart @@ -8,7 +8,7 @@ import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/labeler_models.dart'; import 'package:spark/src/core/network/atproto/data/repositories/actor_repository.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; import 'package:spark/src/core/utils/logging/logging.dart'; import 'package:spark/src/features/settings/providers/settings_provider.dart'; import 'package:spark/src/features/settings/ui/widgets/widgets.dart'; diff --git a/lib/src/features/settings/ui/pages/labeler_management_page.dart b/lib/src/features/settings/ui/pages/labeler_management_page.dart index 79f46a5..56ec953 100644 --- a/lib/src/features/settings/ui/pages/labeler_management_page.dart +++ b/lib/src/features/settings/ui/pages/labeler_management_page.dart @@ -8,7 +8,7 @@ import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/repositories/actor_repository.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; import 'package:spark/src/core/routing/app_router.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; import 'package:spark/src/core/utils/logging/logging.dart'; import 'package:spark/src/features/settings/providers/settings_provider.dart'; import 'package:sprk_poptart/so/sprk/actor/defs.dart'; diff --git a/lib/src/features/stories/ui/pages/author_stories_page.dart b/lib/src/features/stories/ui/pages/author_stories_page.dart index 63bdec2..2fe7f8f 100644 --- a/lib/src/features/stories/ui/pages/author_stories_page.dart +++ b/lib/src/features/stories/ui/pages/author_stories_page.dart @@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/routing/app_router.dart'; -import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; import 'package:spark/src/features/stories/ui/pages/story_page.dart'; import 'package:sprk_poptart/so/sprk/actor/defs.dart'; diff --git a/test/src/core/design_system/components/app_button_test.dart b/test/src/core/design_system/components/app_button_test.dart new file mode 100644 index 0000000..d3d19bb --- /dev/null +++ b/test/src/core/design_system/components/app_button_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/app_button.dart'; +import 'package:spark/src/core/design_system/components/atoms/buttons/interactive_pressable.dart'; + +void main() { + testWidgets('content-width button does not fill a wide parent', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Center( + child: SizedBox( + width: 300, + child: AppButton(label: 'Tap', onPressed: () {}), + ), + ), + ), + ); + + final pressableSize = tester.getSize(find.byType(InteractivePressable)); + + expect(pressableSize.width, lessThan(300)); + }); + + testWidgets('full-width button fills a wide parent', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Center( + child: SizedBox( + width: 300, + child: AppButton(label: 'Tap', onPressed: () {}, fullWidth: true), + ), + ), + ), + ); + + final pressableSize = tester.getSize(find.byType(InteractivePressable)); + + expect(pressableSize.width, 300); + }); +} diff --git a/widgetbook/lib/atoms/accent_button.dart b/widgetbook/lib/atoms/accent_button.dart deleted file mode 100644 index e88ff5c..0000000 --- a/widgetbook/lib/atoms/accent_button.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:widgetbook/widgetbook.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart'; - -import 'package:spark/src/core/design_system/components/atoms/buttons/accent_button.dart'; -import 'package:widgetbook_workspace/widgetbook_usecases_templates.dart'; - -@UseCase(name: 'on_image_background', type: AccentButton) -Widget buildAccentButtonOnImageBackgroundUseCase(BuildContext context) { - return imageBackground( - context: context, - child: AccentButton( - label: context.knobs.string(label: 'Label', initialValue: 'Continue'), - ), - ); -} - -@UseCase(name: 'on_gradient_background', type: AccentButton) -Widget buildAccentButtonOnGradientBackgroundUseCase(BuildContext context) { - return gradientBackground( - context: context, - child: AccentButton( - label: context.knobs.string(label: 'Label', initialValue: 'Continue'), - ), - startColor: context.knobs.color( - label: 'Start Color', - initialValue: Colors.deepPurple, - ), - endColor: context.knobs.color( - label: 'End Color', - initialValue: Colors.pinkAccent, - ), - ); -} - -@UseCase(name: 'on_solid_color_background', type: AccentButton) -Widget buildAccentButtonOnSolidColorBackgroundUseCase(BuildContext context) { - return solidColorBackground( - context: context, - child: AccentButton( - label: context.knobs.string(label: 'Label', initialValue: 'Continue'), - ), - color: context.knobs.color( - label: 'Background Color', - initialValue: const Color(0xFF1A1A2E), - ), - ); -} diff --git a/widgetbook/lib/atoms/action_button.dart b/widgetbook/lib/atoms/action_button.dart deleted file mode 100644 index e090348..0000000 --- a/widgetbook/lib/atoms/action_button.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:widgetbook/widgetbook.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/action_button.dart'; -import 'package:spark/src/core/design_system/components/atoms/icons.dart'; -import 'package:widgetbook_workspace/widgetbook_usecases_templates.dart'; - -final _iconOptions = { - 'add': AppIcons.add(), - 'camera': AppIcons.camera(), - 'comment': AppIcons.comment(), - 'like': AppIcons.like(), - 'share': AppIcons.share(), - 'bookmarkOutline': AppIcons.bookmarkOutline(), - 'more': AppIcons.more(), - 'send': AppIcons.send(), - 'search': AppIcons.search(), - 'homeFilled': AppIcons.homeFilled(), -}; - -@UseCase(name: 'gradient', type: ActionButton) -Widget buildActionButtonInteractiveUseCase(BuildContext context) { - return gradientBackground( - context: context, - startColor: context.knobs.color( - label: 'startColor', - initialValue: Colors.blue, - ), - endColor: context.knobs.color( - label: 'endColor', - initialValue: Colors.purple, - ), - child: ActionButton( - icon: context.knobs.object.dropdown( - label: 'Icon', - initialOption: _iconOptions['like']!, - options: _iconOptions.values.toList(), - labelBuilder: (value) { - return _iconOptions.entries - .firstWhere((entry) => entry.value == value) - .key; - }, - ), - label: context.knobs.string(label: 'label', initialValue: '800'), - isActive: context.knobs.boolean(label: 'isActive', initialValue: false), - onPressed: () => print('Bookmark pressed'), - ), - ); -} diff --git a/widgetbook/lib/atoms/interactive_pressable_and_tags.dart b/widgetbook/lib/atoms/interactive_pressable_and_tags.dart index 8a44930..ca4c6e9 100644 --- a/widgetbook/lib/atoms/interactive_pressable_and_tags.dart +++ b/widgetbook/lib/atoms/interactive_pressable_and_tags.dart @@ -1,7 +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/tags/feed_tag.dart'; -import 'package:spark/src/core/design_system/components/atoms/tags/hashtag.dart'; import 'package:spark/src/core/design_system/components/atoms/tags/tag.dart'; import 'package:widgetbook/widgetbook.dart'; import 'package:widgetbook_annotation/widgetbook_annotation.dart'; @@ -74,31 +73,6 @@ Widget buildTagBasicUseCase(BuildContext context) { ); } -@UseCase(name: 'glassmorphic_tag_with_delete', type: GlassmorphicTag) -Widget buildGlassmorphicTagWithDeleteUseCase(BuildContext context) { - return Center( - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: context.knobs.double.slider( - label: 'max_width', - initialValue: 200, - min: 100, - max: 400, - divisions: 30, - ), - ), - child: GlassmorphicTag( - label: context.knobs.string(label: 'label', initialValue: 'hashtag'), - onTap: () => print('GlassmorphicTag tapped'), - onDeleted: - context.knobs.boolean(label: 'show_delete', initialValue: true) - ? () => print('Delete pressed') - : null, - ), - ), - ); -} - @UseCase(name: 'feed_tag_selected_states', type: FeedTag) Widget buildFeedTagSelectedStatesUseCase(BuildContext context) { final selected = context.knobs.boolean(label: 'selected', initialValue: true); diff --git a/widgetbook/lib/atoms/live_button.dart b/widgetbook/lib/atoms/live_button.dart deleted file mode 100644 index e505945..0000000 --- a/widgetbook/lib/atoms/live_button.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:widgetbook/widgetbook.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart'; - -import 'package:spark/src/core/design_system/components/atoms/icons.dart'; - -import 'package:spark/src/core/design_system/components/atoms/buttons/live_button.dart'; - -import 'package:widgetbook_workspace/widgetbook_usecases_templates.dart'; - -// Helper map to provide a selection of icons for the knob. -final _iconOptions = { - 'add': AppIcons.add(), - 'camera': AppIcons.camera(), - 'comment': AppIcons.comment(), - 'like': AppIcons.like(), - 'share': AppIcons.share(), - 'bookmarkOutline': AppIcons.bookmarkOutline(), - 'more': AppIcons.more(), - 'send': AppIcons.send(), - 'search': AppIcons.search(), - 'homeFilled': AppIcons.homeFilled(), -}; - -@UseCase(name: 'on_image_background', type: LiveButton) -Widget buildLiveButtonOnImageBackgroundUseCase(BuildContext context) { - return imageBackground( - context: context, - child: LiveButton( - onTap: () => print('LiveButton tapped!'), - child: context.knobs.object.dropdown( - label: 'Icon', - initialOption: _iconOptions['camera']!, - options: _iconOptions.values.toList(), - labelBuilder: (value) { - return _iconOptions.entries - .firstWhere((entry) => entry.value == value) - .key; - }, - ), - ), - ); -} - -@UseCase(name: 'on_gradient_background', type: LiveButton) -Widget buildLiveButtonOnGradientBackgroundUseCase(BuildContext context) { - return gradientBackground( - context: context, - child: LiveButton( - onTap: () => print('LiveButton tapped!'), - child: context.knobs.object.dropdown( - label: 'Icon', - initialOption: _iconOptions['camera']!, - options: _iconOptions.values.toList(), - labelBuilder: (value) { - return _iconOptions.entries - .firstWhere((entry) => entry.value == value) - .key; - }, - ), - ), - startColor: context.knobs.color( - label: 'Start Color', - initialValue: Colors.deepPurple, - ), - endColor: context.knobs.color( - label: 'End Color', - initialValue: Colors.pinkAccent, - ), - ); -} - -@UseCase(name: 'on_solid_color_background', type: LiveButton) -Widget buildLiveButtonOnSolidColorBackgroundUseCase(BuildContext context) { - return solidColorBackground( - context: context, - child: LiveButton( - onTap: () => print('LiveButton tapped!'), - child: context.knobs.object.dropdown( - label: 'Icon', - initialOption: _iconOptions['camera']!, - options: _iconOptions.values.toList(), - labelBuilder: (value) { - return _iconOptions.entries - .firstWhere((entry) => entry.value == value) - .key; - }, - ), - ), - color: context.knobs.color( - label: 'Background Color', - initialValue: const Color(0xFF1A1A2E), - ), - ); -} diff --git a/widgetbook/lib/atoms/text_button.dart b/widgetbook/lib/atoms/text_button.dart deleted file mode 100644 index 387440d..0000000 --- a/widgetbook/lib/atoms/text_button.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart' hide TextButton; -import 'package:widgetbook/widgetbook.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart'; -import 'package:spark/src/core/design_system/components/atoms/buttons/text_button.dart'; -import 'package:widgetbook_workspace/widgetbook_usecases_templates.dart'; - -@UseCase(name: 'on_image_background', type: TextButton) -Widget buildTextButtonOnImageBackgroundUseCase(BuildContext context) { - return imageBackground( - context: context, - child: TextButton( - label: context.knobs.string(label: 'Label', initialValue: 'Continue'), - ), - ); -} - -@UseCase(name: 'on_gradient_background', type: TextButton) -Widget buildTextButtonOnGradientBackgroundUseCase(BuildContext context) { - return gradientBackground( - context: context, - child: TextButton( - label: context.knobs.string(label: 'Label', initialValue: 'Continue'), - ), - startColor: context.knobs.color( - label: 'Start Color', - initialValue: context.knobs.color( - label: 'Start Color', - initialValue: Colors.deepPurple, - ), - ), - endColor: context.knobs.color( - label: 'End Color', - initialValue: context.knobs.color( - label: 'End Color', - initialValue: Colors.pinkAccent, - ), - ), - ); -} - -@UseCase(name: 'on_solid_color_background', type: TextButton) -Widget buildTextButtonOnSolidColorBackgroundUseCase(BuildContext context) { - return solidColorBackground( - context: context, - child: TextButton( - label: context.knobs.string(label: 'Label', initialValue: 'Continue'), - ), - color: context.knobs.color( - label: 'Background Color', - initialValue: const Color(0xFF1A1A2E), - ), - ); -} diff --git a/widgetbook/lib/atoms/user_avatar.dart b/widgetbook/lib/atoms/user_avatar.dart new file mode 100644 index 0000000..8b11e41 --- /dev/null +++ b/widgetbook/lib/atoms/user_avatar.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:spark/src/core/design_system/components/atoms/default_profile_avatar.dart'; +import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart'; + +@UseCase(name: 'fallback', type: DefaultProfileAvatar) +Widget buildDefaultProfileAvatarUseCase(BuildContext context) { + final size = context.knobs.double.slider( + label: 'size', + initialValue: 48, + min: 24, + max: 120, + divisions: 24, + ); + + return Center(child: DefaultProfileAvatar(size: size)); +} + +@UseCase(name: 'states', type: UserAvatar) +Widget buildUserAvatarStatesUseCase(BuildContext context) { + final size = context.knobs.double.slider( + label: 'size', + initialValue: 48, + min: 24, + max: 120, + divisions: 24, + ); + + return Center( + child: Wrap( + spacing: 16, + runSpacing: 16, + alignment: WrapAlignment.center, + children: [ + UserAvatar(size: size, username: 'spark'), + UserAvatar( + size: size, + username: 'spark', + borderWidth: 2, + borderColor: Theme.of(context).colorScheme.primary, + ), + ], + ), + ); +} diff --git a/widgetbook/lib/organisms/bottom_nav_bar.dart b/widgetbook/lib/organisms/bottom_nav_bar.dart index 2b1a147..b1e0303 100644 --- a/widgetbook/lib/organisms/bottom_nav_bar.dart +++ b/widgetbook/lib/organisms/bottom_nav_bar.dart @@ -32,15 +32,7 @@ Widget buildSparkBottomNavBarUseCase(BuildContext context) { errorBuilder: (c, e, s) => Container(color: Colors.grey.shade800), ); } else { - background = Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Color(0xFF111214), Color(0xFF000000)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - ); + background = const ColoredBox(color: Color(0xFF050506)); } int selected = initialIndex; diff --git a/widgetbook/lib/organisms/side_action_bar.dart b/widgetbook/lib/organisms/side_action_bar.dart index 98c5b38..e12e2d9 100644 --- a/widgetbook/lib/organisms/side_action_bar.dart +++ b/widgetbook/lib/organisms/side_action_bar.dart @@ -17,10 +17,6 @@ Widget buildSparkSideActionBarUseCase(BuildContext context) { label: 'repostCount', initialValue: '1.2k', ); - final curateCount = context.knobs.string( - label: 'curateCount', - initialValue: '129', - ); final shareCount = context.knobs.string( label: 'shareCount', initialValue: '98', @@ -30,25 +26,18 @@ Widget buildSparkSideActionBarUseCase(BuildContext context) { label: 'isReposted', initialValue: false, ); - final isCurated = context.knobs.boolean( - label: 'isCurated', - initialValue: false, - ); return _ScaffoldedBackground( child: SparkSideActionBar( likeCount: likeCount, commentCount: commentCount, repostCount: repostCount, - curateCount: curateCount, shareCount: shareCount, isLiked: isLiked, isReposted: isReposted, - isCurated: isCurated, onLike: () => print('Like tapped (current: $isLiked)'), onComment: () => print('Comment tapped'), onRepost: () => print('Repost tapped (current: $isReposted)'), - onCurate: () => print('Curate tapped (current: $isCurated)'), onShare: () => print('Share tapped'), ), ); @@ -61,25 +50,18 @@ Widget buildSparkSideActionBarEmptyCountsUseCase(BuildContext context) { label: 'isReposted', initialValue: false, ); - final isCurated = context.knobs.boolean( - label: 'isCurated', - initialValue: false, - ); return _ScaffoldedBackground( child: SparkSideActionBar( likeCount: '', commentCount: '', repostCount: '', - curateCount: '', shareCount: '', isLiked: isLiked, isReposted: isReposted, - isCurated: isCurated, onLike: () => print('Like tapped'), onComment: () => print('Comment tapped'), onRepost: () => print('Repost tapped'), - onCurate: () => print('Curate tapped'), onShare: () => print('Share tapped'), ), ); @@ -95,10 +77,6 @@ Widget buildSparkSideActionBarInteractiveStatefulUseCase(BuildContext context) { label: 'startReposted', initialValue: false, ); - final startCurated = context.knobs.boolean( - label: 'startCurated', - initialValue: false, - ); final likeCountStart = context.knobs.int.slider( label: 'likeStart', initialValue: 120, @@ -120,13 +98,6 @@ Widget buildSparkSideActionBarInteractiveStatefulUseCase(BuildContext context) { max: 5000, divisions: 50, ); - final curateCountStart = context.knobs.int.slider( - label: 'curateStart', - initialValue: 12, - min: 0, - max: 5000, - divisions: 50, - ); final shareCountStart = context.knobs.int.slider( label: 'shareStart', initialValue: 8, @@ -139,21 +110,17 @@ Widget buildSparkSideActionBarInteractiveStatefulUseCase(BuildContext context) { builder: (ctx, setState) { var liked = startLiked; var reposted = startReposted; - var curated = startCurated; var likeCount = likeCountStart; var repostCount = repostCountStart; - var curateCount = curateCountStart; return _ScaffoldedBackground( child: SparkSideActionBar( likeCount: likeCount.toString(), commentCount: commentCountStart.toString(), repostCount: repostCount.toString(), - curateCount: curateCount.toString(), shareCount: shareCountStart.toString(), isLiked: liked, isReposted: reposted, - isCurated: curated, onLike: () { setState(() { liked = !liked; @@ -168,13 +135,6 @@ Widget buildSparkSideActionBarInteractiveStatefulUseCase(BuildContext context) { }); print('Repost toggled -> $reposted (count: $repostCount)'); }, - onCurate: () { - setState(() { - curated = !curated; - curateCount += curated ? 1 : -1; - }); - print('Curate toggled -> $curated (count: $curateCount)'); - }, onComment: () => print('Comment tapped'), onShare: () => print('Share tapped'), ), @@ -185,7 +145,6 @@ Widget buildSparkSideActionBarInteractiveStatefulUseCase(BuildContext context) { @UseCase(name: 'dense_spacing', type: SparkSideActionBar) Widget buildSparkSideActionBarDenseSpacingUseCase(BuildContext context) { - // This variant demonstrates how the bar might look when placed tighter to edges. final scale = context.knobs.double.slider( label: 'scale', initialValue: 0.85, @@ -201,98 +160,14 @@ Widget buildSparkSideActionBarDenseSpacingUseCase(BuildContext context) { likeCount: '12', commentCount: '3', repostCount: '5', - curateCount: '4', shareCount: '1', isLiked: true, isReposted: false, - isCurated: true, ), ), ); } -@UseCase(name: 'curate_popover', type: SparkSideActionBar) -Widget buildSparkSideActionBarCuratePopoverUseCase(BuildContext context) { - final likeCount = context.knobs.int.slider( - label: 'likes', - initialValue: 25, - min: 0, - max: 999, - divisions: 50, - ); - final commentCount = context.knobs.int.slider( - label: 'comments', - initialValue: 8, - min: 0, - max: 500, - divisions: 50, - ); - final repostCount = context.knobs.int.slider( - label: 'reposts', - initialValue: 12, - min: 0, - max: 500, - divisions: 50, - ); - final shareCount = context.knobs.int.slider( - label: 'shares', - initialValue: 2, - min: 0, - max: 500, - divisions: 50, - ); - - return StatefulBuilder( - builder: (ctx, setState) { - var curated = false; - var reposted = false; - var curateCount = 5; - return _ScaffoldedBackground( - child: SparkSideActionBar( - likeCount: likeCount.toString(), - commentCount: commentCount.toString(), - repostCount: repostCount.toString(), - curateCount: curateCount.toString(), - shareCount: shareCount.toString(), - isLiked: false, - isReposted: reposted, - isCurated: curated, - curateDestinations: [ - CurateDestination( - 'Feed 1', - onSelected: () => print('Feed 1 selected'), - ), - CurateDestination( - 'Feed 2', - onSelected: () => print('Feed 2 selected'), - ), - CurateDestination( - 'Ideas', - onSelected: () => print('Ideas selected'), - ), - ], - onRepost: () { - setState(() { - reposted = !reposted; - }); - print('Reposted! $reposted'); - }, - onCurate: () { - setState(() { - curated = true; - curateCount += 1; - }); - print('Curated! Total: $curateCount'); - }, - onComment: () => print('Comment tapped'), - onLike: () => print('Like tapped'), - onShare: () => print('Share tapped'), - ), - ); - }, - ); -} - @UseCase(name: 'with_image_background', type: SparkSideActionBar) Widget buildSparkSideActionBarWithImageBackgroundUseCase(BuildContext context) { final imageUrl = context.knobs.string( @@ -300,47 +175,32 @@ Widget buildSparkSideActionBarWithImageBackgroundUseCase(BuildContext context) { initialValue: 'https://picsum.photos/600/1200?random=${DateTime.now().millisecond % 20}', ); - final likeCount = context.knobs.string( label: 'likeCount', initialValue: '2.4k', ); - final commentCount = context.knobs.string( label: 'commentCount', initialValue: '156', ); - final repostCount = context.knobs.string( label: 'repostCount', initialValue: '89', ); - - final curateCount = context.knobs.string( - label: 'curateCount', - initialValue: '42', - ); - final shareCount = context.knobs.string( label: 'shareCount', initialValue: '89', ); - final isLiked = context.knobs.boolean(label: 'isLiked', initialValue: true); final isReposted = context.knobs.boolean( label: 'isReposted', initialValue: false, ); - final isCurated = context.knobs.boolean( - label: 'isCurated', - initialValue: false, - ); return StatefulBuilder( builder: (ctx, setState) { var liked = isLiked; var reposted = isReposted; - var curated = isCurated; return Scaffold( backgroundColor: Colors.black, @@ -362,20 +222,6 @@ Widget buildSparkSideActionBarWithImageBackgroundUseCase(BuildContext context) { ), ); }, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Container( - color: Colors.grey.shade900, - child: Center( - child: CircularProgressIndicator( - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - ), - ), - ); - }, ), ), Positioned( @@ -385,25 +231,9 @@ Widget buildSparkSideActionBarWithImageBackgroundUseCase(BuildContext context) { likeCount: likeCount, commentCount: commentCount, repostCount: repostCount, - curateCount: curateCount, shareCount: shareCount, isLiked: liked, isReposted: reposted, - isCurated: curated, - curateDestinations: [ - CurateDestination( - 'Feed 1', - onSelected: () => print('Feed 1 selected'), - ), - CurateDestination( - 'Feed 2', - onSelected: () => print('Feed 2 selected'), - ), - CurateDestination( - 'Feed 3', - onSelected: () => print('Feed 3 selected'), - ), - ], onLike: () { setState(() => liked = !liked); print('Like toggled: $liked'); @@ -413,10 +243,6 @@ Widget buildSparkSideActionBarWithImageBackgroundUseCase(BuildContext context) { setState(() => reposted = !reposted); print('Repost toggled: $reposted'); }, - onCurate: () { - setState(() => curated = true); - print('Curated!'); - }, onShare: () => print('Share tapped'), ), ), @@ -442,15 +268,9 @@ class _ScaffoldedBackground extends StatelessWidget { body: SafeArea( child: Stack( children: [ - Positioned.fill( + const Positioned.fill( child: DecoratedBox( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Color(0xFF0E0F11), Color(0xFF050506)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), + decoration: BoxDecoration(color: Color(0xFF050506)), ), ), Positioned( diff --git a/widgetbook/lib/templates/page_templates.dart b/widgetbook/lib/templates/page_templates.dart new file mode 100644 index 0000000..de96c0a --- /dev/null +++ b/widgetbook/lib/templates/page_templates.dart @@ -0,0 +1,372 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter/material.dart'; +import 'package:spark/src/core/design_system/components/molecules/feed_tag_list.dart'; +import 'package:spark/src/core/design_system/components/molecules/story_circle.dart'; +import 'package:spark/src/core/design_system/templates/chat_list_page_template.dart'; +import 'package:spark/src/core/design_system/templates/chat_thread_page_template.dart'; +import 'package:spark/src/core/design_system/templates/explore_page_template.dart'; +import 'package:spark/src/core/design_system/templates/feeds_bar_template.dart'; +import 'package:spark/src/core/design_system/templates/image_review_page_template.dart'; +import 'package:spark/src/core/design_system/templates/recording_page_template.dart'; +import 'package:spark/src/core/design_system/templates/video_review_page_template.dart'; +import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; +import 'package:spark/src/core/network/atproto/data/models/pref_models.dart'; +import 'package:spark/src/features/search/providers/suggested_feeds_provider.dart'; +import 'package:spark/src/features/search/ui/widgets/suggested_feeds_list.dart'; +import 'package:spark/src/features/settings/providers/settings_provider.dart'; +import 'package:spark/src/features/settings/providers/settings_state.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart'; + +@UseCase(name: 'default', type: ChatListPageTemplate) +Widget buildChatListPageTemplateUseCase(BuildContext context) { + return ChatListPageTemplate( + items: const [ + ChatListItemData( + displayName: 'Alex Morgan', + handle: 'alex.sprk.so', + timestamp: '2m', + preview: 'That edit is ready to review.', + unread: true, + ), + ChatListItemData( + displayName: 'Sam Rivera', + handle: 'sam.sprk.so', + timestamp: '1h', + preview: 'Sending the clip now.', + ), + ], + onItemTap: (_) {}, + ); +} + +@UseCase(name: 'default', type: ChatThreadPageTemplate) +Widget buildChatThreadPageTemplateUseCase(BuildContext context) { + return ChatThreadPageTemplate( + displayName: 'Alex Morgan', + handle: 'alex.sprk.so', + messagesWidget: const Center(child: Text('Messages')), + textController: TextEditingController(), + onSend: () {}, + ); +} + +@UseCase(name: 'default', type: ExplorePageTemplate) +Widget buildExplorePageTemplateUseCase(BuildContext context) { + return DefaultTabController( + length: 2, + child: ExplorePageTemplate( + searchHintText: 'Search users and posts', + showClearSearch: false, + emptyStateWidget: const _ExploreDiscoveryPreview(), + tabsWidget: const TabBar( + tabs: [ + Tab(text: 'Posts'), + Tab(text: 'Users'), + ], + ), + contentWidget: const TabBarView( + children: [_ExplorePostsPreview(), _ExploreUsersPreview()], + ), + showTabs: false, + ), + ); +} + +@UseCase(name: 'default', type: FeedsBarTemplate) +Widget buildFeedsBarTemplateUseCase(BuildContext context) { + return const Scaffold( + backgroundColor: Colors.black, + appBar: FeedsBarTemplate( + tags: [ + FeedTagData(id: 'following', text: 'Following'), + FeedTagData(id: 'art', text: 'Art'), + FeedTagData(id: 'music', text: 'Music'), + ], + selectedTagId: 'following', + ), + body: SizedBox.expand(), + ); +} + +@UseCase(name: 'empty', type: ImageReviewPageTemplate) +Widget buildImageReviewPageTemplateUseCase(BuildContext context) { + return ImageReviewPageTemplate( + title: 'New post', + onBack: () {}, + imagePaths: const [], + currentPage: 0, + onPageChanged: (_) {}, + onTapEditImage: (_) {}, + onAltEdit: (_) {}, + onRemoveImage: (_) {}, + showAddMore: true, + canAddMore: true, + imagesCount: 0, + maxImages: 4, + onAddMore: () {}, + descriptionMaxChars: 300, + postLabel: 'Post', + onPost: () {}, + isPosting: false, + crossPostValue: false, + onCrossPostChanged: (_) {}, + ); +} + +@UseCase(name: 'default', type: RecordingPageTemplate) +Widget buildRecordingPageTemplateUseCase(BuildContext context) { + return RecordingPageTemplate( + cameraPreview: const ColoredBox(color: Colors.black), + aspectRatio: 9 / 16, + isRecording: false, + elapsedDuration: Duration.zero, + maxDuration: const Duration(minutes: 1), + onBack: () {}, + onFlipCamera: () {}, + canFlipCamera: true, + captureMode: CaptureMode.videoOnly, + ); +} + +@UseCase(name: 'default', type: VideoReviewPageTemplate) +Widget buildVideoReviewPageTemplateUseCase(BuildContext context) { + return VideoReviewPageTemplate( + title: 'New video', + onBack: () {}, + videoPreview: const ColoredBox(color: Colors.black), + onAltEdit: () {}, + descriptionMaxChars: 300, + postLabel: 'Post', + onPost: () {}, + isPosting: false, + crossPostValue: false, + onCrossPostChanged: (_) {}, + ); +} + +class _ExploreDiscoveryPreview extends StatelessWidget { + const _ExploreDiscoveryPreview(); + + @override + Widget build(BuildContext context) { + return const CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: _StoriesPreview()), + SliverToBoxAdapter(child: _SuggestedFeedsPreview()), + SliverFillRemaining( + hasScrollBody: false, + child: Center(child: Text('Discover stories, feeds, and people')), + ), + ], + ); + } +} + +class _StoriesPreview extends StatelessWidget { + const _StoriesPreview(); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Text( + 'Stories', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.manage_history_outlined, size: 20), + onPressed: () {}, + ), + ], + ), + ), + SizedBox( + height: 102, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + Padding( + padding: const EdgeInsets.only(right: 12), + child: StoryCircle.create(userName: 'Create', imageUrl: ''), + ), + Padding( + padding: const EdgeInsets.only(right: 12), + child: StoryCircle.story(userName: 'Maya', imageUrl: ''), + ), + Padding( + padding: const EdgeInsets.only(right: 12), + child: StoryCircle.story(userName: 'Jon', imageUrl: ''), + ), + Padding( + padding: const EdgeInsets.only(right: 12), + child: StoryCircle.story(userName: 'Ari', imageUrl: ''), + ), + ], + ), + ), + ], + ); + } +} + +class _SuggestedFeedsPreview extends StatelessWidget { + const _SuggestedFeedsPreview(); + + @override + Widget build(BuildContext context) { + final suggestedFeeds = _sampleSuggestedFeeds(); + final savedFeeds = suggestedFeeds + .map( + (generatorView) => Feed( + type: 'feed', + config: makeSavedFeed( + type: 'feed', + value: generatorView.uri.toString(), + pinned: false, + ), + view: generatorView, + ), + ) + .toList(); + + return ProviderScope( + overrides: [ + suggestedFeedsProvider.overrideWith( + () => _WidgetbookSuggestedFeeds(suggestedFeeds), + ), + settingsProvider.overrideWithValue( + SettingsState(activeFeed: savedFeeds.first, feeds: const []), + ), + ], + child: const SuggestedFeedsList(), + ); + } +} + +class _WidgetbookSuggestedFeeds extends SuggestedFeeds { + _WidgetbookSuggestedFeeds(this.feeds); + + final List feeds; + + @override + Future> build() async => feeds; +} + +List _sampleSuggestedFeeds() { + final now = DateTime(2026, 5, 19); + + return [ + _sampleGeneratorView( + uri: 'at://did:plc:sparkcreators/so.sprk.feed.generator/creators', + cid: 'bafyreicreators', + did: 'did:plc:sparkcreators', + handle: 'spark.sprk.so', + displayName: 'Spark Creators', + description: 'Fresh clips, edits, and creator updates.', + likeCount: 1200, + indexedAt: now, + ), + _sampleGeneratorView( + uri: 'at://did:plc:sounds/so.sprk.feed.generator/music-finds', + cid: 'bafyreisounds', + did: 'did:plc:sounds', + handle: 'sounds.sprk.so', + displayName: 'Music Finds', + description: 'Tracks people are using in new posts.', + likeCount: 840, + indexedAt: now, + ), + ]; +} + +GeneratorView _sampleGeneratorView({ + required String uri, + required String cid, + required String did, + required String handle, + required String displayName, + required String description, + required int likeCount, + required DateTime indexedAt, +}) { + return GeneratorView.fromJson({ + r'$type': 'so.sprk.feed.defs#generatorView', + 'uri': uri, + 'cid': cid, + 'did': did, + 'creator': { + r'$type': 'so.sprk.actor.defs#profileView', + 'did': did, + 'handle': handle, + 'displayName': displayName, + }, + 'displayName': displayName, + 'description': description, + 'likeCount': likeCount, + 'indexedAt': indexedAt.toIso8601String(), + }); +} + +class _ExplorePostsPreview extends StatelessWidget { + const _ExplorePostsPreview(); + + @override + Widget build(BuildContext context) { + return GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.45, + ), + itemCount: 6, + itemBuilder: (context, index) { + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: ColoredBox( + color: Color.lerp( + Theme.of(context).colorScheme.surfaceContainerHighest, + Theme.of(context).colorScheme.primaryContainer, + index / 8, + )!, + ), + ); + }, + ); + } +} + +class _ExploreUsersPreview extends StatelessWidget { + const _ExploreUsersPreview(); + + @override + Widget build(BuildContext context) { + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + itemCount: 4, + separatorBuilder: (context, index) => const Divider(height: 1), + itemBuilder: (context, index) { + return ListTile( + contentPadding: const EdgeInsets.symmetric(vertical: 4), + leading: CircleAvatar(child: Text('U${index + 1}')), + title: Text( + ['Maya Chen', 'Jon Bell', 'Ari Stone', 'Lee Park'][index], + ), + subtitle: Text('@${['maya', 'jon', 'ari', 'lee'][index]}.sprk.so'), + ); + }, + ); + } +} diff --git a/widgetbook/lib/templates/profile_page_template.dart b/widgetbook/lib/templates/profile_page_template.dart index 2e23f2b..5982f20 100644 --- a/widgetbook/lib/templates/profile_page_template.dart +++ b/widgetbook/lib/templates/profile_page_template.dart @@ -49,7 +49,7 @@ class _DefaultTabsDemoState extends State<_DefaultTabsDemo> { selectedTabIndex: _selectedTabIndex, onTabChanged: (index) { setState(() => _selectedTabIndex = index); - final tabNames = ['Grid', 'Curate', 'Likes', 'Tagged']; + final tabNames = ['Grid', 'Reposts', 'Likes', 'Tagged']; print('Tab changed to: ${tabNames[index]}'); }, contentWidget: _MockContentWidget( @@ -358,7 +358,7 @@ class _MockContentWidget extends StatelessWidget { @override Widget build(BuildContext context) { if (type == 'empty') { - final tabNames = ['Grid', 'Curate', 'Likes', 'Tagged']; + final tabNames = ['Grid', 'Reposts', 'Likes', 'Tagged']; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/widgetbook/lib/widgetbook_usecases_templates.dart b/widgetbook/lib/widgetbook_usecases_templates.dart index f62ec9f..8fcf3c0 100644 --- a/widgetbook/lib/widgetbook_usecases_templates.dart +++ b/widgetbook/lib/widgetbook_usecases_templates.dart @@ -40,26 +40,6 @@ Widget imageBackground({required BuildContext context, required Widget child}) { ); } -Widget gradientBackground({ - required BuildContext context, - required Widget child, - required Color startColor, - required Color endColor, -}) { - return _BackgroundWrapper( - background: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [startColor, endColor], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - ), - child: Center(child: child), - ); -} - Widget solidColorBackground({ required BuildContext context, required Widget child, -- 2.51.2