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,