diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/AgentsSection/AgentsSection.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/AgentsSection/AgentsSection.tsx index 2d1497be62..6e3ea17223 100644 --- a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/AgentsSection/AgentsSection.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/AgentsSection/AgentsSection.tsx @@ -5,6 +5,8 @@ import { CarouselContent, CarouselItem, } from "@/components/__legacy__/ui/carousel"; +import { FadeIn } from "@/components/molecules/FadeIn/FadeIn"; +import { StaggeredList } from "@/components/molecules/StaggeredList/StaggeredList"; import { useAgentsSection } from "./useAgentsSection"; import { StoreAgent } from "@/app/api/__generated__/models/storeAgent"; import { StoreCard } from "../StoreCard/StoreCard"; @@ -41,12 +43,14 @@ export const AgentsSection = ({ return (
-

- {sectionTitle} -

+ +

+ {sectionTitle} +

+
{!displayedAgents || displayedAgents.length === 0 ? (
No agents found @@ -54,32 +58,38 @@ export const AgentsSection = ({ ) : ( <> {/* Mobile Carousel View */} - - - {displayedAgents.map((agent, index) => ( - - handleCardClick(agent.creator, agent.slug)} - /> - - ))} - - + + + + {displayedAgents.map((agent, index) => ( + + handleCardClick(agent.creator, agent.slug)} + /> + + ))} + + + -
+ {/* Desktop Grid View with Staggered Animation */} + {displayedAgents.map((agent, index) => ( handleCardClick(agent.creator, agent.slug)} /> ))} -
+ )}
diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/BecomeACreator/BecomeACreator.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/BecomeACreator/BecomeACreator.tsx index a5b6fb9db1..0019097842 100644 --- a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/BecomeACreator/BecomeACreator.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/BecomeACreator/BecomeACreator.tsx @@ -38,7 +38,7 @@ export function BecomeACreator({ + + ); +} 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} + + ))} + + ); +}