diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/FeaturedCreators/FeaturedCreators.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/FeaturedCreators/FeaturedCreators.tsx
index ddb3e1a50d..22ddd64271 100644
--- a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/FeaturedCreators/FeaturedCreators.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/FeaturedCreators/FeaturedCreators.tsx
@@ -1,5 +1,7 @@
"use client";
+import { FadeIn } from "@/components/molecules/FadeIn/FadeIn";
+import { StaggeredList } from "@/components/molecules/StaggeredList/StaggeredList";
import { CreatorCard } from "../CreatorCard/CreatorCard";
import { useFeaturedCreators } from "./useFeaturedCreators";
import { Creator } from "@/app/api/__generated__/models/creator";
@@ -19,11 +21,17 @@ export const FeaturedCreators = ({
return (
-
- {title}
-
+
+
+ {title}
+
+
-
+
{displayedCreators.map((creator, index) => (
))}
-
+
);
diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/FeaturedSection/FeaturedSection.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/FeaturedSection/FeaturedSection.tsx
index 309148c02b..358c98dc40 100644
--- a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/FeaturedSection/FeaturedSection.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/FeaturedSection/FeaturedSection.tsx
@@ -8,6 +8,7 @@ import {
CarouselNext,
CarouselIndicator,
} from "@/components/__legacy__/ui/carousel";
+import { FadeIn } from "@/components/molecules/FadeIn/FadeIn";
import Link from "next/link";
import { useFeaturedSection } from "./useFeaturedSection";
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
@@ -25,40 +26,44 @@ export const FeaturedSection = ({ featuredAgents }: FeaturedSectionProps) => {
return (
-
- Featured agents
-
+
+
+ Featured agents
+
+
-
-
- {featuredAgents.map((agent, index) => (
-
-
+
+
+ {featuredAgents.map((agent, index) => (
+
-
-
-
- ))}
-
-
-
-
-
-
-
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
);
};
diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/FilterChips/FilterChips.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/FilterChips/FilterChips.tsx
index 5266c7383a..c46c7de7a9 100644
--- a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/FilterChips/FilterChips.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/FilterChips/FilterChips.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Badge } from "@/components/__legacy__/ui/badge";
+import { FilterChip } from "@/components/atoms/FilterChip/FilterChip";
import { useFilterChips } from "./useFilterChips";
interface FilterChipsProps {
@@ -9,8 +9,6 @@ interface FilterChipsProps {
multiSelect?: boolean;
}
-// Some flaws in its logic
-// FRONTEND-TODO : This needs to be fixed
export const FilterChips = ({
badges,
onFilterChange,
@@ -22,18 +20,20 @@ export const FilterChips = ({
});
return (
-
+
{badges.map((badge) => (
-
handleBadgeClick(badge)}
- >
-
- {badge}
-
-
+ size="lg"
+ className="mb-2 lg:mb-3"
+ />
))}
);
diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/HeroSection/HeroSection.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/HeroSection/HeroSection.tsx
index 363c106209..9441d383a2 100644
--- a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/HeroSection/HeroSection.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/HeroSection/HeroSection.tsx
@@ -1,5 +1,6 @@
"use client";
+import { FadeIn } from "@/components/molecules/FadeIn/FadeIn";
import { FilterChips } from "../FilterChips/FilterChips";
import { SearchBar } from "../SearchBar/SearchBar";
import { useHeroSection } from "./useHeroSection";
@@ -9,30 +10,36 @@ export const HeroSection = () => {
return (
-
-
-
- Explore AI agents built for{" "}
-
-
- you
-
-
-
- by the{" "}
-
-
- community
-
-
-
-
- Bringing you AI agents designed by thinkers from around the world
-
-
-
-
-
+
+
+
+
+ Explore AI agents built for{" "}
+
+
+ you
+
+
+
+ by the{" "}
+
+
+ community
+
+
+
+
+
+
+ Bringing you AI agents designed by thinkers from around the world
+
+
+
+
+
+
+
+
{
multiSelect={false}
/>
-
+
);
diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainMarketplacePage/MainMarketplacePage.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainMarketplacePage/MainMarketplacePage.tsx
index e84e4580dd..ecfe830577 100644
--- a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainMarketplacePage/MainMarketplacePage.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainMarketplacePage/MainMarketplacePage.tsx
@@ -1,5 +1,6 @@
"use client";
-import { Separator } from "@/components/__legacy__/ui/separator";
+import { Separator } from "@/components/atoms/Separator/Separator";
+import { FadeIn } from "@/components/molecules/FadeIn/FadeIn";
import { FeaturedSection } from "../FeaturedSection/FeaturedSection";
import { BecomeACreator } from "../BecomeACreator/BecomeACreator";
import { HeroSection } from "../HeroSection/HeroSection";
@@ -54,11 +55,13 @@ export const MainMarkeplacePage = () => {
)}
-
+
+
+
);
diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/SearchBar/SearchBar.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/SearchBar/SearchBar.tsx
index 3395fe39db..f8f37a73a6 100644
--- a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/SearchBar/SearchBar.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/SearchBar/SearchBar.tsx
@@ -16,9 +16,9 @@ interface SearchBarProps {
export const SearchBar = ({
placeholder = 'Search for tasks like "optimise SEO"',
backgroundColor = "bg-neutral-100 dark:bg-neutral-800",
- iconColor = "text-[#646464] dark:text-neutral-400",
- textColor = "text-[#707070] dark:text-neutral-200",
- placeholderColor = "text-[#707070] dark:text-neutral-400",
+ iconColor = "text-neutral-500 dark:text-neutral-400",
+ textColor = "text-neutral-500 dark:text-neutral-200",
+ placeholderColor = "text-neutral-500 dark:text-neutral-400",
width = "w-9/10 lg:w-[56.25rem]",
height = "h-[60px]",
}: SearchBarProps) => {
@@ -32,10 +32,13 @@ export const SearchBar = ({
>
setSearchQuery(e.target.value)}
placeholder={placeholder}
+ aria-label="Search for AI agents"
className={`flex-grow border-none bg-transparent ${textColor} font-sans text-lg font-normal leading-[2.25rem] tracking-tight md:text-xl placeholder:${placeholderColor} focus:outline-none`}
data-testid="store-search-input"
/>
diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/StoreCard/StoreCard.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/StoreCard/StoreCard.tsx
index b9e6ab5f95..c7898897dd 100644
--- a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/StoreCard/StoreCard.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/StoreCard/StoreCard.tsx
@@ -1,10 +1,25 @@
import Image from "next/image";
-import { StarRatingIcons } from "@/components/__legacy__/ui/icons";
+import { Star } from "@phosphor-icons/react";
import Avatar, {
AvatarFallback,
AvatarImage,
} from "@/components/atoms/Avatar/Avatar";
+function StarRating({ rating }: { rating: number }) {
+ const stars = [];
+ const clampedRating = Math.max(0, Math.min(5, rating));
+ for (let i = 1; i <= 5; i++) {
+ stars.push(
+
,
+ );
+ }
+ return <>{stars}>;
+}
+
interface StoreCardProps {
agentName: string;
agentImage: string;
@@ -34,7 +49,7 @@ export const StoreCard: React.FC
= ({
return (
= ({
{/* Second Section: Agent Name and Creator Name */}
-
+
{agentName}
{!hideAvatar && creatorName && (
@@ -107,11 +122,11 @@ export const StoreCard: React.FC = ({
{rating.toFixed(1)}
- {StarRatingIcons(rating)}
+
diff --git a/autogpt_platform/frontend/src/components/atoms/FilterChip/FilterChip.stories.tsx b/autogpt_platform/frontend/src/components/atoms/FilterChip/FilterChip.stories.tsx
new file mode 100644
index 0000000000..00ae8d8cd7
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/atoms/FilterChip/FilterChip.stories.tsx
@@ -0,0 +1,151 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { useState } from "react";
+import { FilterChip } from "./FilterChip";
+
+const meta: Meta
= {
+ title: "Atoms/FilterChip",
+ component: FilterChip,
+ tags: ["autodocs"],
+ parameters: {
+ layout: "centered",
+ },
+ argTypes: {
+ size: {
+ control: "select",
+ options: ["sm", "md", "lg"],
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ label: "Marketing",
+ },
+};
+
+export const Selected: Story = {
+ args: {
+ label: "Marketing",
+ selected: true,
+ },
+};
+
+export const Dismissible: Story = {
+ args: {
+ label: "Marketing",
+ selected: true,
+ dismissible: true,
+ },
+};
+
+export const Sizes: Story = {
+ render: () => (
+
+
+
+
+
+ ),
+};
+
+export const Disabled: Story = {
+ args: {
+ label: "Disabled",
+ disabled: true,
+ },
+};
+
+function FilterChipGroupDemo() {
+ const filters = [
+ "Marketing",
+ "Sales",
+ "Development",
+ "Design",
+ "Research",
+ "Analytics",
+ ];
+ const [selected, setSelected] = useState(["Marketing"]);
+
+ function handleToggle(filter: string) {
+ setSelected((prev) =>
+ prev.includes(filter)
+ ? prev.filter((f) => f !== filter)
+ : [...prev, filter],
+ );
+ }
+
+ return (
+
+ {filters.map((filter) => (
+ handleToggle(filter)}
+ />
+ ))}
+
+ );
+}
+
+export const FilterGroup: Story = {
+ render: () => ,
+};
+
+function SingleSelectDemo() {
+ const filters = ["All", "Featured", "Popular", "New"];
+ const [selected, setSelected] = useState("All");
+
+ return (
+
+ {filters.map((filter) => (
+ setSelected(filter)}
+ />
+ ))}
+
+ );
+}
+
+export const SingleSelect: Story = {
+ render: () => ,
+};
+
+function DismissibleDemo() {
+ const [filters, setFilters] = useState([
+ "Marketing",
+ "Sales",
+ "Development",
+ ]);
+
+ function handleDismiss(filter: string) {
+ setFilters((prev) => prev.filter((f) => f !== filter));
+ }
+
+ return (
+
+ {filters.map((filter) => (
+ handleDismiss(filter)}
+ />
+ ))}
+ {filters.length === 0 && (
+ No filters selected
+ )}
+
+ );
+}
+
+export const DismissibleGroup: Story = {
+ render: () => ,
+};
diff --git a/autogpt_platform/frontend/src/components/atoms/FilterChip/FilterChip.tsx b/autogpt_platform/frontend/src/components/atoms/FilterChip/FilterChip.tsx
new file mode 100644
index 0000000000..3444fb79f2
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/atoms/FilterChip/FilterChip.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { X } from "@phosphor-icons/react";
+
+type FilterChipSize = "sm" | "md" | "lg";
+
+interface FilterChipProps {
+ /** The label text displayed in the chip */
+ label: string;
+ /** Whether the chip is currently selected */
+ selected?: boolean;
+ /** Callback when the chip is clicked */
+ onClick?: () => void;
+ /** Whether to show a dismiss/remove button */
+ dismissible?: boolean;
+ /** Callback when the dismiss button is clicked */
+ onDismiss?: () => void;
+ /** Size variant of the chip */
+ size?: FilterChipSize;
+ /** Whether the chip is disabled */
+ disabled?: boolean;
+ /** Additional CSS classes */
+ className?: string;
+}
+
+const sizeStyles: Record = {
+ sm: "px-3 py-1 text-sm gap-1.5",
+ md: "px-4 py-1.5 text-base gap-2",
+ lg: "px-6 py-2 text-lg gap-2.5 lg:text-xl lg:leading-9",
+};
+
+const iconSizes: Record = {
+ sm: "h-3 w-3",
+ md: "h-4 w-4",
+ lg: "h-5 w-5",
+};
+
+/**
+ * A filter chip component for selecting/deselecting filter options.
+ * Supports single and multi-select patterns with proper accessibility.
+ */
+export function FilterChip({
+ label,
+ selected = false,
+ onClick,
+ dismissible = false,
+ onDismiss,
+ size = "md",
+ disabled = false,
+ className,
+}: FilterChipProps) {
+ function handleDismiss(e: React.MouseEvent) {
+ e.stopPropagation();
+ onDismiss?.();
+ }
+
+ return (
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/components/atoms/Separator/Separator.stories.tsx b/autogpt_platform/frontend/src/components/atoms/Separator/Separator.stories.tsx
new file mode 100644
index 0000000000..96fa24a281
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/atoms/Separator/Separator.stories.tsx
@@ -0,0 +1,72 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { Separator } from "./Separator";
+
+const meta: Meta = {
+ title: "Atoms/Separator",
+ component: Separator,
+ tags: ["autodocs"],
+ parameters: {
+ layout: "padded",
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Horizontal: Story = {
+ render: () => (
+
+
+ Content above the separator
+
+
+
+ Content below the separator
+
+
+ ),
+};
+
+export const Vertical: Story = {
+ render: () => (
+
+ Left
+
+ Right
+
+ ),
+};
+
+export const WithCustomStyles: Story = {
+ render: () => (
+
+
+
+
+
+ ),
+};
+
+export const InSection: Story = {
+ render: () => (
+
+
+
+ Featured Agents
+
+
+ Browse our collection of featured AI agents.
+
+
+
+
+
+ Top Creators
+
+
+ Meet the creators behind the most popular agents.
+
+
+
+ ),
+};
diff --git a/autogpt_platform/frontend/src/components/atoms/Separator/Separator.tsx b/autogpt_platform/frontend/src/components/atoms/Separator/Separator.tsx
new file mode 100644
index 0000000000..d005549095
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/atoms/Separator/Separator.tsx
@@ -0,0 +1,43 @@
+import { cn } from "@/lib/utils";
+
+type SeparatorOrientation = "horizontal" | "vertical";
+
+interface SeparatorProps {
+ /** The orientation of the separator */
+ orientation?: SeparatorOrientation;
+ /** Whether the separator is purely decorative (true) or represents a semantic boundary (false) */
+ decorative?: boolean;
+ /** Additional CSS classes */
+ className?: string;
+}
+
+/**
+ * A visual separator that divides content.
+ * Uses semantic `
` for horizontal separators and a styled `` for vertical.
+ */
+export function Separator({
+ orientation = "horizontal",
+ decorative = true,
+ className,
+}: SeparatorProps) {
+ const baseStyles = "shrink-0 bg-neutral-200 dark:bg-neutral-800";
+
+ if (orientation === "horizontal") {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/components/molecules/FadeIn/FadeIn.stories.tsx b/autogpt_platform/frontend/src/components/molecules/FadeIn/FadeIn.stories.tsx
new file mode 100644
index 0000000000..f2f92db82c
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/molecules/FadeIn/FadeIn.stories.tsx
@@ -0,0 +1,128 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { FadeIn } from "./FadeIn";
+
+const meta: Meta
= {
+ title: "Molecules/FadeIn",
+ component: FadeIn,
+ tags: ["autodocs"],
+ parameters: {
+ layout: "padded",
+ },
+ argTypes: {
+ direction: {
+ control: "select",
+ options: ["up", "down", "left", "right", "none"],
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+const DemoCard = ({ title }: { title: string }) => (
+
+
+ {title}
+
+
+ This card fades in with a smooth animation.
+
+
+);
+
+export const Default: Story = {
+ args: {
+ direction: "up",
+ children: ,
+ },
+};
+
+export const FadeDown: Story = {
+ args: {
+ direction: "down",
+ children: ,
+ },
+};
+
+export const FadeLeft: Story = {
+ args: {
+ direction: "left",
+ children: ,
+ },
+};
+
+export const FadeRight: Story = {
+ args: {
+ direction: "right",
+ children: ,
+ },
+};
+
+export const FadeOnly: Story = {
+ args: {
+ direction: "none",
+ children: ,
+ },
+};
+
+export const WithDelay: Story = {
+ args: {
+ direction: "up",
+ delay: 0.5,
+ children: ,
+ },
+};
+
+export const SlowAnimation: Story = {
+ args: {
+ direction: "up",
+ duration: 1.5,
+ children: ,
+ },
+};
+
+export const LargeDistance: Story = {
+ args: {
+ direction: "up",
+ distance: 60,
+ children: ,
+ },
+};
+
+export const MultipleElements: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+
+ ),
+};
+
+export const HeroExample: Story = {
+ render: () => (
+
+
+
+ Welcome to the Marketplace
+
+
+
+
+ Discover AI agents built by the community
+
+
+
+
+
+
+ ),
+};
diff --git a/autogpt_platform/frontend/src/components/molecules/FadeIn/FadeIn.tsx b/autogpt_platform/frontend/src/components/molecules/FadeIn/FadeIn.tsx
new file mode 100644
index 0000000000..888bd9e7b3
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/molecules/FadeIn/FadeIn.tsx
@@ -0,0 +1,109 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { motion, useReducedMotion, type Variants } from "framer-motion";
+import { ReactNode } from "react";
+
+type FadeDirection = "up" | "down" | "left" | "right" | "none";
+
+interface FadeInProps {
+ /** Content to animate */
+ children: ReactNode;
+ /** Direction the content fades in from */
+ direction?: FadeDirection;
+ /** Distance to travel in pixels (only applies when direction is not "none") */
+ distance?: number;
+ /** Animation duration in seconds */
+ duration?: number;
+ /** Delay before animation starts in seconds */
+ delay?: number;
+ /** Whether to trigger animation when element enters viewport */
+ viewport?: boolean;
+ /** How much of element must be visible to trigger (0-1) */
+ viewportAmount?: number;
+ /** Whether animation should only trigger once */
+ once?: boolean;
+ /** Additional CSS classes */
+ className?: string;
+ /** HTML element to render as */
+ as?: keyof JSX.IntrinsicElements;
+}
+
+function getDirectionOffset(
+ direction: FadeDirection,
+ distance: number,
+): { x: number; y: number } {
+ switch (direction) {
+ case "up":
+ return { x: 0, y: distance };
+ case "down":
+ return { x: 0, y: -distance };
+ case "left":
+ return { x: distance, y: 0 };
+ case "right":
+ return { x: -distance, y: 0 };
+ case "none":
+ default:
+ return { x: 0, y: 0 };
+ }
+}
+
+/**
+ * A fade-in animation wrapper component.
+ * Animates children with a fade effect and optional directional slide.
+ * Respects user's reduced motion preferences.
+ */
+export function FadeIn({
+ children,
+ direction = "up",
+ distance = 24,
+ duration = 0.5,
+ delay = 0,
+ viewport = true,
+ viewportAmount = 0.2,
+ once = true,
+ className,
+ as = "div",
+}: FadeInProps) {
+ const shouldReduceMotion = useReducedMotion();
+ const offset = getDirectionOffset(direction, distance);
+
+ // If user prefers reduced motion, render without animation
+ if (shouldReduceMotion) {
+ const Component = as as keyof JSX.IntrinsicElements;
+ return {children};
+ }
+
+ const variants: Variants = {
+ hidden: {
+ opacity: 0,
+ x: offset.x,
+ y: offset.y,
+ },
+ visible: {
+ opacity: 1,
+ x: 0,
+ y: 0,
+ transition: {
+ duration,
+ delay,
+ ease: [0.25, 0.1, 0.25, 1], // Custom easing for smooth feel
+ },
+ },
+ };
+
+ const MotionComponent = motion[as as keyof typeof motion] as typeof motion.div;
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/components/molecules/StaggeredList/StaggeredList.stories.tsx b/autogpt_platform/frontend/src/components/molecules/StaggeredList/StaggeredList.stories.tsx
new file mode 100644
index 0000000000..ca6320013a
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/molecules/StaggeredList/StaggeredList.stories.tsx
@@ -0,0 +1,180 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { StaggeredList } from "./StaggeredList";
+
+const meta: Meta = {
+ title: "Molecules/StaggeredList",
+ component: StaggeredList,
+ tags: ["autodocs"],
+ parameters: {
+ layout: "padded",
+ },
+ argTypes: {
+ direction: {
+ control: "select",
+ options: ["up", "down", "left", "right", "none"],
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+const DemoCard = ({ title, index }: { title: string; index: number }) => (
+
+
+ {title}
+
+
+ Card #{index + 1} with staggered animation
+
+
+);
+
+const items = ["First Item", "Second Item", "Third Item", "Fourth Item"];
+
+export const Default: Story = {
+ args: {
+ direction: "up",
+ className: "space-y-4",
+ children: items.map((item, i) => ),
+ },
+};
+
+export const FadeDown: Story = {
+ args: {
+ direction: "down",
+ className: "space-y-4",
+ children: items.map((item, i) => ),
+ },
+};
+
+export const FadeLeft: Story = {
+ args: {
+ direction: "left",
+ className: "flex gap-4",
+ children: items.map((item, i) => ),
+ },
+};
+
+export const FadeRight: Story = {
+ args: {
+ direction: "right",
+ className: "flex gap-4",
+ children: items.map((item, i) => ),
+ },
+};
+
+export const FastStagger: Story = {
+ args: {
+ direction: "up",
+ staggerDelay: 0.05,
+ className: "space-y-4",
+ children: items.map((item, i) => ),
+ },
+};
+
+export const SlowStagger: Story = {
+ args: {
+ direction: "up",
+ staggerDelay: 0.3,
+ className: "space-y-4",
+ children: items.map((item, i) => ),
+ },
+};
+
+export const WithInitialDelay: Story = {
+ args: {
+ direction: "up",
+ initialDelay: 0.5,
+ className: "space-y-4",
+ children: items.map((item, i) => ),
+ },
+};
+
+export const GridLayout: Story = {
+ args: {
+ direction: "up",
+ staggerDelay: 0.08,
+ className: "grid grid-cols-2 gap-4 md:grid-cols-4",
+ children: [
+ ...items,
+ "Fifth Item",
+ "Sixth Item",
+ "Seventh Item",
+ "Eighth Item",
+ ].map((item, i) => ),
+ },
+};
+
+export const AgentCardsExample: Story = {
+ render: () => {
+ const agents = [
+ { name: "SEO Optimizer", runs: 1234 },
+ { name: "Content Writer", runs: 987 },
+ { name: "Data Analyzer", runs: 756 },
+ { name: "Code Reviewer", runs: 543 },
+ ];
+
+ return (
+
+ {agents.map((agent, i) => (
+
+
+
+ {agent.name}
+
+
{agent.runs} runs
+
+ ))}
+
+ );
+ },
+};
+
+export const CreatorCardsExample: Story = {
+ render: () => {
+ const creators = [
+ { name: "Alice", agents: 12 },
+ { name: "Bob", agents: 8 },
+ { name: "Charlie", agents: 15 },
+ { name: "Diana", agents: 6 },
+ ];
+
+ const colors = [
+ "bg-violet-100 dark:bg-violet-900/30",
+ "bg-blue-100 dark:bg-blue-900/30",
+ "bg-green-100 dark:bg-green-900/30",
+ "bg-orange-100 dark:bg-orange-900/30",
+ ];
+
+ return (
+
+ {creators.map((creator, i) => (
+
+
+
+ {creator.name}
+
+
+ {creator.agents} agents
+
+
+ ))}
+
+ );
+ },
+};
diff --git a/autogpt_platform/frontend/src/components/molecules/StaggeredList/StaggeredList.tsx b/autogpt_platform/frontend/src/components/molecules/StaggeredList/StaggeredList.tsx
new file mode 100644
index 0000000000..8654920d5a
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/molecules/StaggeredList/StaggeredList.tsx
@@ -0,0 +1,130 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { motion, useReducedMotion, type Variants } from "framer-motion";
+import { ReactNode } from "react";
+
+type StaggerDirection = "up" | "down" | "left" | "right" | "none";
+
+interface StaggeredListProps {
+ /** Array of items to render with staggered animation */
+ children: ReactNode[];
+ /** Direction items animate from */
+ direction?: StaggerDirection;
+ /** Distance to travel in pixels */
+ distance?: number;
+ /** Base duration for each item's animation */
+ duration?: number;
+ /** Delay between each item's animation start */
+ staggerDelay?: number;
+ /** Initial delay before first item animates */
+ initialDelay?: number;
+ /** Whether to trigger animation when element enters viewport */
+ viewport?: boolean;
+ /** How much of container must be visible to trigger */
+ viewportAmount?: number;
+ /** Whether animation should only trigger once */
+ once?: boolean;
+ /** Additional CSS classes for the container */
+ className?: string;
+ /** Additional CSS classes for each item wrapper */
+ itemClassName?: string;
+}
+
+function getDirectionOffset(
+ direction: StaggerDirection,
+ distance: number,
+): { x: number; y: number } {
+ switch (direction) {
+ case "up":
+ return { x: 0, y: distance };
+ case "down":
+ return { x: 0, y: -distance };
+ case "left":
+ return { x: distance, y: 0 };
+ case "right":
+ return { x: -distance, y: 0 };
+ case "none":
+ default:
+ return { x: 0, y: 0 };
+ }
+}
+
+/**
+ * Animates a list of children with staggered fade-in effects.
+ * Each child appears sequentially with a configurable delay.
+ * Respects user's reduced motion preferences.
+ */
+export function StaggeredList({
+ children,
+ direction = "up",
+ distance = 20,
+ duration = 0.4,
+ staggerDelay = 0.1,
+ initialDelay = 0,
+ viewport = true,
+ viewportAmount = 0.1,
+ once = true,
+ className,
+ itemClassName,
+}: StaggeredListProps) {
+ const shouldReduceMotion = useReducedMotion();
+ const offset = getDirectionOffset(direction, distance);
+
+ // If user prefers reduced motion, render without animation
+ if (shouldReduceMotion) {
+ return (
+
+ {children.map((child, index) => (
+
+ {child}
+
+ ))}
+
+ );
+ }
+
+ const containerVariants: Variants = {
+ hidden: {},
+ visible: {
+ transition: {
+ staggerChildren: staggerDelay,
+ delayChildren: initialDelay,
+ },
+ },
+ };
+
+ const itemVariants: Variants = {
+ hidden: {
+ opacity: 0,
+ x: offset.x,
+ y: offset.y,
+ },
+ visible: {
+ opacity: 1,
+ x: 0,
+ y: 0,
+ transition: {
+ duration,
+ ease: [0.25, 0.1, 0.25, 1],
+ },
+ },
+ };
+
+ return (
+
+ {children.map((child, index) => (
+
+ {child}
+
+ ))}
+
+ );
+}