Compare commits

..

2 Commits

Author SHA1 Message Date
Abhimanyu Yadav
8fa75c8da4 Merge branch 'dev' into testing-claude-code 2026-01-22 16:39:44 +05:30
abhi1992002
919cc877ad feat(frontend): enhance UI components with animations and accessibility improvements
### Changes 🏗️
- Integrated `FadeIn` animations in `AgentsSection`, `FeaturedCreators`, `FeaturedSection`, `HeroSection`, and `BecomeACreator` components for improved visual appeal.
- Replaced static elements with `StaggeredList` in `FeaturedCreators` and `AgentsSection` for a more dynamic layout.
- Updated `SearchBar` to use `type="search"` and added `aria-label` for better accessibility.
- Enhanced `StoreCard` with focus-visible styles and keyboard navigation support.
- Refactored `FilterChips` to utilize `FilterChip` component for a more consistent design.

### Checklist 📋
- [x] Verified animations function correctly across components.
- [x] Ensured accessibility improvements are in place and tested.
- [x] Confirmed UI consistency with design specifications.
2026-01-20 20:12:33 +05:30
197 changed files with 1666 additions and 1851 deletions

View File

@@ -128,7 +128,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
exitOnceUploaded: true exitOnceUploaded: true
e2e_test: test:
runs-on: big-boi runs-on: big-boi
needs: setup needs: setup
strategy: strategy:
@@ -258,39 +258,3 @@ jobs:
- name: Print Final Docker Compose logs - name: Print Final Docker Compose logs
if: always() if: always()
run: docker compose -f ../docker-compose.yml logs run: docker compose -f ../docker-compose.yml logs
integration_test:
runs-on: ubuntu-latest
needs: setup
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22.18.0"
- name: Enable corepack
run: corepack enable
- name: Restore dependencies cache
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ needs.setup.outputs.cache-key }}
restore-keys: |
${{ runner.os }}-pnpm-${{ hashFiles('autogpt_platform/frontend/pnpm-lock.yaml') }}
${{ runner.os }}-pnpm-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate API client
run: pnpm generate:api
- name: Run Integration Tests
run: pnpm test:unit

View File

@@ -1,37 +1,12 @@
-- CreateExtension -- CreateExtension
-- Supabase: pgvector must be enabled via Dashboard → Database → Extensions first -- Supabase: pgvector must be enabled via Dashboard → Database → Extensions first
-- Ensures vector extension is in the current schema (from DATABASE_URL ?schema= param) -- Creates extension in current schema (determined by search_path from DATABASE_URL ?schema= param)
-- If it exists in a different schema (e.g., public), we drop and recreate it in the current schema
-- This ensures vector type is in the same schema as tables, making ::vector work without explicit qualification -- This ensures vector type is in the same schema as tables, making ::vector work without explicit qualification
DO $$ DO $$
DECLARE
current_schema_name text;
vector_schema text;
BEGIN BEGIN
-- Get the current schema from search_path CREATE EXTENSION IF NOT EXISTS "vector";
SELECT current_schema() INTO current_schema_name;
-- Check if vector extension exists and which schema it's in
SELECT n.nspname INTO vector_schema
FROM pg_extension e
JOIN pg_namespace n ON e.extnamespace = n.oid
WHERE e.extname = 'vector';
-- Handle removal if in wrong schema
IF vector_schema IS NOT NULL AND vector_schema != current_schema_name THEN
BEGIN
-- Vector exists in a different schema, drop it first
RAISE WARNING 'pgvector found in schema "%" but need it in "%". Dropping and reinstalling...',
vector_schema, current_schema_name;
EXECUTE 'DROP EXTENSION IF EXISTS vector CASCADE';
EXCEPTION WHEN OTHERS THEN EXCEPTION WHEN OTHERS THEN
RAISE EXCEPTION 'Failed to drop pgvector from schema "%": %. You may need to drop it manually.', RAISE NOTICE 'vector extension not available or already exists, skipping';
vector_schema, SQLERRM;
END;
END IF;
-- Create extension in current schema (let it fail naturally if not available)
EXECUTE format('CREATE EXTENSION IF NOT EXISTS vector SCHEMA %I', current_schema_name);
END $$; END $$;
-- CreateEnum -- CreateEnum

View File

@@ -0,0 +1,71 @@
-- Acknowledge Supabase-managed extensions to prevent drift warnings
-- These extensions are pre-installed by Supabase in specific schemas
-- This migration ensures they exist where available (Supabase) or skips gracefully (CI)
-- Create schemas (safe in both CI and Supabase)
CREATE SCHEMA IF NOT EXISTS "extensions";
-- Extensions that exist in both CI and Supabase
DO $$
BEGIN
CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions";
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'pgcrypto extension not available, skipping';
END $$;
DO $$
BEGIN
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions";
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'uuid-ossp extension not available, skipping';
END $$;
-- Supabase-specific extensions (skip gracefully in CI)
DO $$
BEGIN
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions";
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'pg_stat_statements extension not available, skipping';
END $$;
DO $$
BEGIN
CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "extensions";
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'pg_net extension not available, skipping';
END $$;
DO $$
BEGIN
CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions";
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'pgjwt extension not available, skipping';
END $$;
DO $$
BEGIN
CREATE SCHEMA IF NOT EXISTS "graphql";
CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql";
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'pg_graphql extension not available, skipping';
END $$;
DO $$
BEGIN
CREATE SCHEMA IF NOT EXISTS "pgsodium";
CREATE EXTENSION IF NOT EXISTS "pgsodium" WITH SCHEMA "pgsodium";
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'pgsodium extension not available, skipping';
END $$;
DO $$
BEGIN
CREATE SCHEMA IF NOT EXISTS "vault";
CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault";
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'supabase_vault extension not available, skipping';
END $$;
-- Return to platform
CREATE SCHEMA IF NOT EXISTS "platform";

View File

@@ -34,10 +34,7 @@ logger = logging.getLogger(__name__)
# Default output directory relative to repo root # Default output directory relative to repo root
DEFAULT_OUTPUT_DIR = ( DEFAULT_OUTPUT_DIR = (
Path(__file__).parent.parent.parent.parent Path(__file__).parent.parent.parent.parent / "docs" / "integrations"
/ "docs"
/ "integrations"
/ "block-integrations"
) )
@@ -424,14 +421,6 @@ def generate_block_markdown(
lines.append("<!-- END MANUAL -->") lines.append("<!-- END MANUAL -->")
lines.append("") lines.append("")
# Optional per-block extras (only include if has content)
extras = manual_content.get("extras", "")
if extras:
lines.append("<!-- MANUAL: extras -->")
lines.append(extras)
lines.append("<!-- END MANUAL -->")
lines.append("")
lines.append("---") lines.append("---")
lines.append("") lines.append("")
@@ -467,52 +456,25 @@ def get_block_file_mapping(blocks: list[BlockDoc]) -> dict[str, list[BlockDoc]]:
return dict(file_mapping) return dict(file_mapping)
def generate_overview_table(blocks: list[BlockDoc], block_dir_prefix: str = "") -> str: def generate_overview_table(blocks: list[BlockDoc]) -> str:
"""Generate the overview table markdown (blocks.md). """Generate the overview table markdown (blocks.md)."""
Args:
blocks: List of block documentation objects
block_dir_prefix: Prefix for block file links (e.g., "block-integrations/")
"""
lines = [] lines = []
# GitBook YAML frontmatter
lines.append("---")
lines.append("layout:")
lines.append(" width: default")
lines.append(" title:")
lines.append(" visible: true")
lines.append(" description:")
lines.append(" visible: true")
lines.append(" tableOfContents:")
lines.append(" visible: false")
lines.append(" outline:")
lines.append(" visible: true")
lines.append(" pagination:")
lines.append(" visible: true")
lines.append(" metadata:")
lines.append(" visible: true")
lines.append("---")
lines.append("")
lines.append("# AutoGPT Blocks Overview") lines.append("# AutoGPT Blocks Overview")
lines.append("") lines.append("")
lines.append( lines.append(
'AutoGPT uses a modular approach with various "blocks" to handle different tasks. These blocks are the building blocks of AutoGPT workflows, allowing users to create complex automations by combining simple, specialized components.' 'AutoGPT uses a modular approach with various "blocks" to handle different tasks. These blocks are the building blocks of AutoGPT workflows, allowing users to create complex automations by combining simple, specialized components.'
) )
lines.append("") lines.append("")
lines.append('{% hint style="info" %}') lines.append('!!! info "Creating Your Own Blocks"')
lines.append("**Creating Your Own Blocks**")
lines.append("")
lines.append(" Want to create your own custom blocks? Check out our guides:") lines.append(" Want to create your own custom blocks? Check out our guides:")
lines.append(" ") lines.append(" ")
lines.append( lines.append(
"* [Build your own Blocks](https://docs.agpt.co/platform/new_blocks/) - Step-by-step tutorial with examples" " - [Build your own Blocks](https://docs.agpt.co/platform/new_blocks/) - Step-by-step tutorial with examples"
) )
lines.append( lines.append(
"* [Block SDK Guide](https://docs.agpt.co/platform/block-sdk-guide/) - Advanced SDK patterns with OAuth, webhooks, and provider configuration" " - [Block SDK Guide](https://docs.agpt.co/platform/block-sdk-guide/) - Advanced SDK patterns with OAuth, webhooks, and provider configuration"
) )
lines.append("{% endhint %}")
lines.append("") lines.append("")
lines.append( lines.append(
"Below is a comprehensive list of all available blocks, categorized by their primary function. Click on any block name to view its detailed documentation." "Below is a comprehensive list of all available blocks, categorized by their primary function. Click on any block name to view its detailed documentation."
@@ -575,8 +537,7 @@ def generate_overview_table(blocks: list[BlockDoc], block_dir_prefix: str = "")
else "No description" else "No description"
) )
short_desc = short_desc.replace("\n", " ").replace("|", "\\|") short_desc = short_desc.replace("\n", " ").replace("|", "\\|")
link_path = f"{block_dir_prefix}{file_path}" lines.append(f"| [{block.name}]({file_path}#{anchor}) | {short_desc} |")
lines.append(f"| [{block.name}]({link_path}#{anchor}) | {short_desc} |")
lines.append("") lines.append("")
continue continue
@@ -602,49 +563,7 @@ def generate_overview_table(blocks: list[BlockDoc], block_dir_prefix: str = "")
) )
short_desc = short_desc.replace("\n", " ").replace("|", "\\|") short_desc = short_desc.replace("\n", " ").replace("|", "\\|")
link_path = f"{block_dir_prefix}{file_path}" lines.append(f"| [{block.name}]({file_path}#{anchor}) | {short_desc} |")
lines.append(f"| [{block.name}]({link_path}#{anchor}) | {short_desc} |")
lines.append("")
return "\n".join(lines)
def generate_summary_md(
blocks: list[BlockDoc], root_dir: Path, block_dir_prefix: str = ""
) -> str:
"""Generate SUMMARY.md for GitBook navigation.
Args:
blocks: List of block documentation objects
root_dir: The root docs directory (e.g., docs/integrations/)
block_dir_prefix: Prefix for block file links (e.g., "block-integrations/")
"""
lines = []
lines.append("# Table of contents")
lines.append("")
lines.append("* [AutoGPT Blocks Overview](README.md)")
lines.append("")
# Check for guides/ directory at the root level (docs/integrations/guides/)
guides_dir = root_dir / "guides"
if guides_dir.exists():
lines.append("## Guides")
lines.append("")
for guide_file in sorted(guides_dir.glob("*.md")):
# Use just the file name for title (replace hyphens/underscores with spaces)
title = file_path_to_title(guide_file.stem.replace("-", "_") + ".md")
lines.append(f"* [{title}](guides/{guide_file.name})")
lines.append("")
lines.append("## Block Integrations")
lines.append("")
file_mapping = get_block_file_mapping(blocks)
for file_path in sorted(file_mapping.keys()):
title = file_path_to_title(file_path)
link_path = f"{block_dir_prefix}{file_path}"
lines.append(f"* [{title}]({link_path})")
lines.append("") lines.append("")
@@ -734,16 +653,6 @@ def write_block_docs(
) )
) )
# Add file-level additional_content section if present
file_additional = extract_manual_content(existing_content).get(
"additional_content", ""
)
if file_additional:
content_parts.append("<!-- MANUAL: additional_content -->")
content_parts.append(file_additional)
content_parts.append("<!-- END MANUAL -->")
content_parts.append("")
full_content = file_header + "\n" + "\n".join(content_parts) full_content = file_header + "\n" + "\n".join(content_parts)
generated_files[str(file_path)] = full_content generated_files[str(file_path)] = full_content
@@ -752,28 +661,14 @@ def write_block_docs(
full_path.write_text(full_content) full_path.write_text(full_content)
# Generate overview file at the parent directory (docs/integrations/) # Generate overview file
# with links prefixed to point into block-integrations/ overview_content = generate_overview_table(blocks)
root_dir = output_dir.parent overview_path = output_dir / "README.md"
block_dir_name = output_dir.name # "block-integrations"
block_dir_prefix = f"{block_dir_name}/"
overview_content = generate_overview_table(blocks, block_dir_prefix)
overview_path = root_dir / "README.md"
generated_files["README.md"] = overview_content generated_files["README.md"] = overview_content
overview_path.write_text(overview_content) overview_path.write_text(overview_content)
if verbose: if verbose:
print(" Writing README.md (overview) to parent directory") print(" Writing README.md (overview)")
# Generate SUMMARY.md for GitBook navigation at the parent directory
summary_content = generate_summary_md(blocks, root_dir, block_dir_prefix)
summary_path = root_dir / "SUMMARY.md"
generated_files["SUMMARY.md"] = summary_content
summary_path.write_text(summary_content)
if verbose:
print(" Writing SUMMARY.md (navigation) to parent directory")
return generated_files return generated_files
@@ -853,16 +748,6 @@ def check_docs_in_sync(output_dir: Path, blocks: list[BlockDoc]) -> bool:
elif block_match.group(1).strip() != expected_block_content.strip(): elif block_match.group(1).strip() != expected_block_content.strip():
mismatched_blocks.append(block.name) mismatched_blocks.append(block.name)
# Add file-level additional_content to expected content (matches write_block_docs)
file_additional = extract_manual_content(existing_content).get(
"additional_content", ""
)
if file_additional:
content_parts.append("<!-- MANUAL: additional_content -->")
content_parts.append(file_additional)
content_parts.append("<!-- END MANUAL -->")
content_parts.append("")
expected_content = file_header + "\n" + "\n".join(content_parts) expected_content = file_header + "\n" + "\n".join(content_parts)
if existing_content.strip() != expected_content.strip(): if existing_content.strip() != expected_content.strip():
@@ -872,15 +757,11 @@ def check_docs_in_sync(output_dir: Path, blocks: list[BlockDoc]) -> bool:
out_of_sync_details.append((file_path, mismatched_blocks)) out_of_sync_details.append((file_path, mismatched_blocks))
all_match = False all_match = False
# Check overview at the parent directory (docs/integrations/) # Check overview
root_dir = output_dir.parent overview_path = output_dir / "README.md"
block_dir_name = output_dir.name # "block-integrations"
block_dir_prefix = f"{block_dir_name}/"
overview_path = root_dir / "README.md"
if overview_path.exists(): if overview_path.exists():
existing_overview = overview_path.read_text() existing_overview = overview_path.read_text()
expected_overview = generate_overview_table(blocks, block_dir_prefix) expected_overview = generate_overview_table(blocks)
if existing_overview.strip() != expected_overview.strip(): if existing_overview.strip() != expected_overview.strip():
print("OUT OF SYNC: README.md (overview)") print("OUT OF SYNC: README.md (overview)")
print(" The blocks overview table needs regeneration") print(" The blocks overview table needs regeneration")
@@ -891,21 +772,6 @@ def check_docs_in_sync(output_dir: Path, blocks: list[BlockDoc]) -> bool:
out_of_sync_details.append(("README.md", ["overview table"])) out_of_sync_details.append(("README.md", ["overview table"]))
all_match = False all_match = False
# Check SUMMARY.md at the parent directory
summary_path = root_dir / "SUMMARY.md"
if summary_path.exists():
existing_summary = summary_path.read_text()
expected_summary = generate_summary_md(blocks, root_dir, block_dir_prefix)
if existing_summary.strip() != expected_summary.strip():
print("OUT OF SYNC: SUMMARY.md (navigation)")
print(" The GitBook navigation needs regeneration")
out_of_sync_details.append(("SUMMARY.md", ["navigation"]))
all_match = False
else:
print("MISSING: SUMMARY.md (navigation)")
out_of_sync_details.append(("SUMMARY.md", ["navigation"]))
all_match = False
# Check for unfilled manual sections # Check for unfilled manual sections
unfilled_patterns = [ unfilled_patterns = [
"_Add a description of this category of blocks._", "_Add a description of this category of blocks._",

View File

@@ -5,6 +5,8 @@ import {
CarouselContent, CarouselContent,
CarouselItem, CarouselItem,
} from "@/components/__legacy__/ui/carousel"; } from "@/components/__legacy__/ui/carousel";
import { FadeIn } from "@/components/molecules/FadeIn/FadeIn";
import { StaggeredList } from "@/components/molecules/StaggeredList/StaggeredList";
import { useAgentsSection } from "./useAgentsSection"; import { useAgentsSection } from "./useAgentsSection";
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent"; import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
import { StoreCard } from "../StoreCard/StoreCard"; import { StoreCard } from "../StoreCard/StoreCard";
@@ -41,12 +43,14 @@ export const AgentsSection = ({
return ( return (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<div className="w-full max-w-[1360px]"> <div className="w-full max-w-[1360px]">
<FadeIn direction="left" duration={0.5}>
<h2 <h2
style={{ marginBottom: margin }} style={{ marginBottom: margin }}
className="font-poppins text-lg font-semibold text-[#282828] dark:text-neutral-200" className="font-poppins text-lg font-semibold text-neutral-800 dark:text-neutral-200"
> >
{sectionTitle} {sectionTitle}
</h2> </h2>
</FadeIn>
{!displayedAgents || displayedAgents.length === 0 ? ( {!displayedAgents || displayedAgents.length === 0 ? (
<div className="text-center text-gray-500 dark:text-gray-400"> <div className="text-center text-gray-500 dark:text-gray-400">
No agents found No agents found
@@ -54,8 +58,8 @@ export const AgentsSection = ({
) : ( ) : (
<> <>
{/* Mobile Carousel View */} {/* Mobile Carousel View */}
<FadeIn direction="up" className="md:hidden">
<Carousel <Carousel
className="md:hidden"
opts={{ opts={{
loop: true, loop: true,
}} }}
@@ -78,8 +82,14 @@ export const AgentsSection = ({
))} ))}
</CarouselContent> </CarouselContent>
</Carousel> </Carousel>
</FadeIn>
<div className="hidden grid-cols-1 place-items-center gap-6 md:grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4"> {/* Desktop Grid View with Staggered Animation */}
<StaggeredList
direction="up"
staggerDelay={0.08}
className="hidden grid-cols-1 place-items-center gap-6 md:grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4"
>
{displayedAgents.map((agent, index) => ( {displayedAgents.map((agent, index) => (
<StoreCard <StoreCard
key={index} key={index}
@@ -94,7 +104,7 @@ export const AgentsSection = ({
onClick={() => handleCardClick(agent.creator, agent.slug)} onClick={() => handleCardClick(agent.creator, agent.slug)}
/> />
))} ))}
</div> </StaggeredList>
</> </>
)} )}
</div> </div>

View File

@@ -38,7 +38,7 @@ export function BecomeACreator({
<PublishAgentModal <PublishAgentModal
trigger={ trigger={
<button className="inline-flex h-[48px] cursor-pointer items-center justify-center rounded-[38px] bg-neutral-800 px-8 py-3 transition-colors hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600 md:h-[56px] md:px-10 md:py-4 lg:h-[68px] lg:px-12 lg:py-5"> <button className="inline-flex h-[48px] cursor-pointer items-center justify-center rounded-[38px] bg-neutral-800 px-8 py-3 transition-colors hover:bg-neutral-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:focus-visible:ring-neutral-50 md:h-[56px] md:px-10 md:py-4 lg:h-[68px] lg:px-12 lg:py-5">
<span className="whitespace-nowrap font-poppins text-base font-medium leading-normal text-neutral-50 md:text-lg md:leading-relaxed lg:text-xl lg:leading-7"> <span className="whitespace-nowrap font-poppins text-base font-medium leading-normal text-neutral-50 md:text-lg md:leading-relaxed lg:text-xl lg:leading-7">
{buttonText} {buttonText}
</span> </span>

View File

@@ -20,9 +20,18 @@ export const CreatorCard = ({
}: CreatorCardProps) => { }: CreatorCardProps) => {
return ( return (
<div <div
className={`h-[264px] w-full px-[18px] pb-5 pt-6 ${backgroundColor(index)} inline-flex cursor-pointer flex-col items-start justify-start gap-3.5 rounded-[26px] transition-all duration-200 hover:brightness-95`} className={`h-[264px] w-full px-[18px] pb-5 pt-6 ${backgroundColor(index)} inline-flex cursor-pointer flex-col items-start justify-start gap-3.5 rounded-[26px] transition-[filter] duration-200 hover:brightness-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 dark:focus-visible:ring-neutral-50`}
onClick={onClick} onClick={onClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onClick();
}
}}
data-testid="creator-card" data-testid="creator-card"
role="button"
tabIndex={0}
aria-label={`View ${creatorName}'s profile - ${agentsUploaded} agents`}
> >
<div className="relative h-[64px] w-[64px]"> <div className="relative h-[64px] w-[64px]">
<div className="absolute inset-0 overflow-hidden rounded-full"> <div className="absolute inset-0 overflow-hidden rounded-full">

View File

@@ -1,5 +1,7 @@
"use client"; "use client";
import { FadeIn } from "@/components/molecules/FadeIn/FadeIn";
import { StaggeredList } from "@/components/molecules/StaggeredList/StaggeredList";
import { CreatorCard } from "../CreatorCard/CreatorCard"; import { CreatorCard } from "../CreatorCard/CreatorCard";
import { useFeaturedCreators } from "./useFeaturedCreators"; import { useFeaturedCreators } from "./useFeaturedCreators";
import { Creator } from "@/app/api/__generated__/models/creator"; import { Creator } from "@/app/api/__generated__/models/creator";
@@ -19,11 +21,17 @@ export const FeaturedCreators = ({
return ( return (
<div className="flex w-full flex-col items-center justify-center"> <div className="flex w-full flex-col items-center justify-center">
<div className="w-full max-w-[1360px]"> <div className="w-full max-w-[1360px]">
<FadeIn direction="left" duration={0.5}>
<h2 className="mb-9 font-poppins text-lg font-semibold text-neutral-800 dark:text-neutral-200"> <h2 className="mb-9 font-poppins text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{title} {title}
</h2> </h2>
</FadeIn>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4"> <StaggeredList
direction="up"
staggerDelay={0.1}
className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4"
>
{displayedCreators.map((creator, index) => ( {displayedCreators.map((creator, index) => (
<CreatorCard <CreatorCard
key={index} key={index}
@@ -35,7 +43,7 @@ export const FeaturedCreators = ({
index={index} index={index}
/> />
))} ))}
</div> </StaggeredList>
</div> </div>
</div> </div>
); );

View File

@@ -8,6 +8,7 @@ import {
CarouselNext, CarouselNext,
CarouselIndicator, CarouselIndicator,
} from "@/components/__legacy__/ui/carousel"; } from "@/components/__legacy__/ui/carousel";
import { FadeIn } from "@/components/molecules/FadeIn/FadeIn";
import Link from "next/link"; import Link from "next/link";
import { useFeaturedSection } from "./useFeaturedSection"; import { useFeaturedSection } from "./useFeaturedSection";
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent"; import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
@@ -25,10 +26,13 @@ export const FeaturedSection = ({ featuredAgents }: FeaturedSectionProps) => {
return ( return (
<section className="w-full"> <section className="w-full">
<FadeIn direction="left" duration={0.5}>
<h2 className="mb-8 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200"> <h2 className="mb-8 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
Featured agents Featured agents
</h2> </h2>
</FadeIn>
<FadeIn direction="up" duration={0.6} delay={0.1}>
<Carousel <Carousel
opts={{ opts={{
align: "center", align: "center",
@@ -59,6 +63,7 @@ export const FeaturedSection = ({ featuredAgents }: FeaturedSectionProps) => {
<CarouselNext afterClick={handleNextSlide} /> <CarouselNext afterClick={handleNextSlide} />
</div> </div>
</Carousel> </Carousel>
</FadeIn>
</section> </section>
); );
}; };

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Badge } from "@/components/__legacy__/ui/badge"; import { FilterChip } from "@/components/atoms/FilterChip/FilterChip";
import { useFilterChips } from "./useFilterChips"; import { useFilterChips } from "./useFilterChips";
interface FilterChipsProps { interface FilterChipsProps {
@@ -9,8 +9,6 @@ interface FilterChipsProps {
multiSelect?: boolean; multiSelect?: boolean;
} }
// Some flaws in its logic
// FRONTEND-TODO : This needs to be fixed
export const FilterChips = ({ export const FilterChips = ({
badges, badges,
onFilterChange, onFilterChange,
@@ -22,18 +20,20 @@ export const FilterChips = ({
}); });
return ( return (
<div className="flex h-auto min-h-8 flex-wrap items-center justify-center gap-3 lg:min-h-14 lg:justify-start lg:gap-5"> <div
{badges.map((badge) => ( className="flex h-auto min-h-8 flex-wrap items-center justify-center gap-3 lg:min-h-14 lg:justify-start lg:gap-5"
<Badge role="group"
key={badge} aria-label="Filter options"
variant={selectedFilters.includes(badge) ? "secondary" : "outline"}
className="mb-2 flex cursor-pointer items-center justify-center gap-2 rounded-full border border-black/50 px-3 py-1 dark:border-white/50 lg:mb-3 lg:gap-2.5 lg:px-6 lg:py-2"
onClick={() => handleBadgeClick(badge)}
> >
<div className="text-sm font-light tracking-tight text-[#474747] dark:text-[#e0e0e0] lg:text-xl lg:font-medium lg:leading-9"> {badges.map((badge) => (
{badge} <FilterChip
</div> key={badge}
</Badge> label={badge}
selected={selectedFilters.includes(badge)}
onClick={() => handleBadgeClick(badge)}
size="lg"
className="mb-2 lg:mb-3"
/>
))} ))}
</div> </div>
); );

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { FadeIn } from "@/components/molecules/FadeIn/FadeIn";
import { FilterChips } from "../FilterChips/FilterChips"; import { FilterChips } from "../FilterChips/FilterChips";
import { SearchBar } from "../SearchBar/SearchBar"; import { SearchBar } from "../SearchBar/SearchBar";
import { useHeroSection } from "./useHeroSection"; import { useHeroSection } from "./useHeroSection";
@@ -9,6 +10,7 @@ export const HeroSection = () => {
return ( return (
<div className="mb-2 mt-8 flex flex-col items-center justify-center px-4 sm:mb-4 sm:mt-12 sm:px-6 md:mb-6 md:mt-16 lg:my-24 lg:px-8 xl:my-16"> <div className="mb-2 mt-8 flex flex-col items-center justify-center px-4 sm:mb-4 sm:mt-12 sm:px-6 md:mb-6 md:mt-16 lg:my-24 lg:px-8 xl:my-16">
<div className="w-full max-w-3xl lg:max-w-4xl xl:max-w-5xl"> <div className="w-full max-w-3xl lg:max-w-4xl xl:max-w-5xl">
<FadeIn direction="down" duration={0.6} delay={0}>
<div className="mb-4 text-center md:mb-8"> <div className="mb-4 text-center md:mb-8">
<h1 className="text-center"> <h1 className="text-center">
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50"> <span className="font-poppins text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50">
@@ -26,13 +28,18 @@ export const HeroSection = () => {
</span> </span>
</h1> </h1>
</div> </div>
</FadeIn>
<FadeIn direction="up" duration={0.6} delay={0.15}>
<h3 className="mb:text-2xl mb-6 text-center font-sans text-xl font-normal leading-loose text-neutral-700 dark:text-neutral-300 md:mb-12"> <h3 className="mb:text-2xl mb-6 text-center font-sans text-xl font-normal leading-loose text-neutral-700 dark:text-neutral-300 md:mb-12">
Bringing you AI agents designed by thinkers from around the world Bringing you AI agents designed by thinkers from around the world
</h3> </h3>
</FadeIn>
<FadeIn direction="up" duration={0.5} delay={0.3}>
<div className="mb-4 flex justify-center sm:mb-5"> <div className="mb-4 flex justify-center sm:mb-5">
<SearchBar height="h-[74px]" /> <SearchBar height="h-[74px]" />
</div> </div>
<div> </FadeIn>
<FadeIn direction="up" duration={0.5} delay={0.4}>
<div className="flex justify-center"> <div className="flex justify-center">
<FilterChips <FilterChips
badges={searchTerms} badges={searchTerms}
@@ -40,7 +47,7 @@ export const HeroSection = () => {
multiSelect={false} multiSelect={false}
/> />
</div> </div>
</div> </FadeIn>
</div> </div>
</div> </div>
); );

View File

@@ -1,5 +1,6 @@
"use client"; "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 { FeaturedSection } from "../FeaturedSection/FeaturedSection";
import { BecomeACreator } from "../BecomeACreator/BecomeACreator"; import { BecomeACreator } from "../BecomeACreator/BecomeACreator";
import { HeroSection } from "../HeroSection/HeroSection"; import { HeroSection } from "../HeroSection/HeroSection";
@@ -54,11 +55,13 @@ export const MainMarkeplacePage = () => {
<FeaturedCreators featuredCreators={featuredCreators.creators} /> <FeaturedCreators featuredCreators={featuredCreators.creators} />
)} )}
<Separator className="mb-[25px] mt-[60px]" /> <Separator className="mb-[25px] mt-[60px]" />
<FadeIn direction="up" duration={0.6}>
<BecomeACreator <BecomeACreator
title="Become a Creator" title="Become a Creator"
description="Join our ever-growing community of hackers and tinkerers" description="Join our ever-growing community of hackers and tinkerers"
buttonText="Become a Creator" buttonText="Become a Creator"
/> />
</FadeIn>
</main> </main>
</div> </div>
); );

View File

@@ -1,15 +0,0 @@
import { expect, test } from "vitest";
import { render, screen } from "@/tests/integrations/test-utils";
import { MainMarkeplacePage } from "../MainMarketplacePage";
import { server } from "@/mocks/mock-server";
import { getDeleteV2DeleteStoreSubmissionMockHandler422 } from "@/app/api/__generated__/endpoints/store/store.msw";
// Only for CI testing purpose, will remove it in future PR
test("MainMarketplacePage", async () => {
server.use(getDeleteV2DeleteStoreSubmissionMockHandler422());
render(<MainMarkeplacePage />);
expect(
await screen.findByText("Featured agents", { exact: false }),
).toBeDefined();
});

View File

@@ -16,9 +16,9 @@ interface SearchBarProps {
export const SearchBar = ({ export const SearchBar = ({
placeholder = 'Search for tasks like "optimise SEO"', placeholder = 'Search for tasks like "optimise SEO"',
backgroundColor = "bg-neutral-100 dark:bg-neutral-800", backgroundColor = "bg-neutral-100 dark:bg-neutral-800",
iconColor = "text-[#646464] dark:text-neutral-400", iconColor = "text-neutral-500 dark:text-neutral-400",
textColor = "text-[#707070] dark:text-neutral-200", textColor = "text-neutral-500 dark:text-neutral-200",
placeholderColor = "text-[#707070] dark:text-neutral-400", placeholderColor = "text-neutral-500 dark:text-neutral-400",
width = "w-9/10 lg:w-[56.25rem]", width = "w-9/10 lg:w-[56.25rem]",
height = "h-[60px]", height = "h-[60px]",
}: SearchBarProps) => { }: SearchBarProps) => {
@@ -32,10 +32,13 @@ export const SearchBar = ({
> >
<MagnifyingGlassIcon className={`h-5 w-5 md:h-7 md:w-7 ${iconColor}`} /> <MagnifyingGlassIcon className={`h-5 w-5 md:h-7 md:w-7 ${iconColor}`} />
<input <input
type="text" type="search"
name="search"
autoComplete="off"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder={placeholder} 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`} 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" data-testid="store-search-input"
/> />

View File

@@ -1,10 +1,25 @@
import Image from "next/image"; import Image from "next/image";
import { StarRatingIcons } from "@/components/__legacy__/ui/icons"; import { Star } from "@phosphor-icons/react";
import Avatar, { import Avatar, {
AvatarFallback, AvatarFallback,
AvatarImage, AvatarImage,
} from "@/components/atoms/Avatar/Avatar"; } 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(
<Star
key={i}
weight={i <= clampedRating ? "fill" : "regular"}
className="h-4 w-4 text-neutral-900 dark:text-yellow-500"
/>,
);
}
return <>{stars}</>;
}
interface StoreCardProps { interface StoreCardProps {
agentName: string; agentName: string;
agentImage: string; agentImage: string;
@@ -34,7 +49,7 @@ export const StoreCard: React.FC<StoreCardProps> = ({
return ( return (
<div <div
className="flex h-[27rem] w-full max-w-md cursor-pointer flex-col items-start rounded-3xl bg-background transition-all duration-300 hover:shadow-lg dark:hover:shadow-gray-700" className="flex h-[27rem] w-full max-w-md cursor-pointer flex-col items-start rounded-3xl bg-background transition-shadow duration-300 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 dark:hover:shadow-gray-700 dark:focus-visible:ring-neutral-50"
onClick={handleClick} onClick={handleClick}
data-testid="store-card" data-testid="store-card"
role="button" role="button"
@@ -76,7 +91,7 @@ export const StoreCard: React.FC<StoreCardProps> = ({
<div className="mt-3 flex w-full flex-1 flex-col px-4"> <div className="mt-3 flex w-full flex-1 flex-col px-4">
{/* Second Section: Agent Name and Creator Name */} {/* Second Section: Agent Name and Creator Name */}
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<h3 className="line-clamp-2 font-poppins text-2xl font-semibold text-[#272727] dark:text-neutral-100"> <h3 className="line-clamp-2 font-poppins text-2xl font-semibold text-neutral-800 dark:text-neutral-100">
{agentName} {agentName}
</h3> </h3>
{!hideAvatar && creatorName && ( {!hideAvatar && creatorName && (
@@ -107,11 +122,11 @@ export const StoreCard: React.FC<StoreCardProps> = ({
{rating.toFixed(1)} {rating.toFixed(1)}
</span> </span>
<div <div
className="inline-flex items-center" className="inline-flex items-center gap-0.5"
role="img" role="img"
aria-label={`Rating: ${rating.toFixed(1)} out of 5 stars`} aria-label={`Rating: ${rating.toFixed(1)} out of 5 stars`}
> >
{StarRatingIcons(rating)} <StarRating rating={rating} />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,151 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { FilterChip } from "./FilterChip";
const meta: Meta<typeof FilterChip> = {
title: "Atoms/FilterChip",
component: FilterChip,
tags: ["autodocs"],
parameters: {
layout: "centered",
},
argTypes: {
size: {
control: "select",
options: ["sm", "md", "lg"],
},
},
};
export default meta;
type Story = StoryObj<typeof FilterChip>;
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: () => (
<div className="flex items-center gap-4">
<FilterChip label="Small" size="sm" />
<FilterChip label="Medium" size="md" />
<FilterChip label="Large" size="lg" />
</div>
),
};
export const Disabled: Story = {
args: {
label: "Disabled",
disabled: true,
},
};
function FilterChipGroupDemo() {
const filters = [
"Marketing",
"Sales",
"Development",
"Design",
"Research",
"Analytics",
];
const [selected, setSelected] = useState<string[]>(["Marketing"]);
function handleToggle(filter: string) {
setSelected((prev) =>
prev.includes(filter)
? prev.filter((f) => f !== filter)
: [...prev, filter],
);
}
return (
<div className="flex flex-wrap gap-3">
{filters.map((filter) => (
<FilterChip
key={filter}
label={filter}
selected={selected.includes(filter)}
onClick={() => handleToggle(filter)}
/>
))}
</div>
);
}
export const FilterGroup: Story = {
render: () => <FilterChipGroupDemo />,
};
function SingleSelectDemo() {
const filters = ["All", "Featured", "Popular", "New"];
const [selected, setSelected] = useState("All");
return (
<div className="flex flex-wrap gap-3">
{filters.map((filter) => (
<FilterChip
key={filter}
label={filter}
selected={selected === filter}
onClick={() => setSelected(filter)}
/>
))}
</div>
);
}
export const SingleSelect: Story = {
render: () => <SingleSelectDemo />,
};
function DismissibleDemo() {
const [filters, setFilters] = useState([
"Marketing",
"Sales",
"Development",
]);
function handleDismiss(filter: string) {
setFilters((prev) => prev.filter((f) => f !== filter));
}
return (
<div className="flex flex-wrap gap-3">
{filters.map((filter) => (
<FilterChip
key={filter}
label={filter}
selected
dismissible
onDismiss={() => handleDismiss(filter)}
/>
))}
{filters.length === 0 && (
<span className="text-neutral-500">No filters selected</span>
)}
</div>
);
}
export const DismissibleGroup: Story = {
render: () => <DismissibleDemo />,
};

View File

@@ -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<FilterChipSize, string> = {
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<FilterChipSize, string> = {
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 (
<button
type="button"
onClick={onClick}
disabled={disabled}
aria-pressed={selected}
className={cn(
// Base styles
"inline-flex items-center justify-center rounded-full border font-medium transition-colors",
// Focus styles
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 dark:focus-visible:ring-neutral-50",
// Size styles
sizeStyles[size],
// State styles
selected
? "border-neutral-900 bg-neutral-100 text-neutral-800 dark:border-neutral-100 dark:bg-neutral-800 dark:text-neutral-200"
: "border-neutral-400 bg-transparent text-neutral-600 hover:bg-neutral-50 dark:border-neutral-500 dark:text-neutral-300 dark:hover:bg-neutral-800",
// Disabled styles
disabled && "pointer-events-none opacity-50",
className,
)}
>
<span>{label}</span>
{dismissible && selected && (
<span
role="button"
tabIndex={0}
onClick={handleDismiss}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleDismiss(e as unknown as React.MouseEvent);
}
}}
className="rounded-full p-0.5 hover:bg-neutral-200 dark:hover:bg-neutral-700"
aria-label={`Remove ${label} filter`}
>
<X className={iconSizes[size]} weight="bold" />
</span>
)}
</button>
);
}

View File

@@ -0,0 +1,72 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Separator } from "./Separator";
const meta: Meta<typeof Separator> = {
title: "Atoms/Separator",
component: Separator,
tags: ["autodocs"],
parameters: {
layout: "padded",
},
};
export default meta;
type Story = StoryObj<typeof Separator>;
export const Horizontal: Story = {
render: () => (
<div className="w-full max-w-md">
<p className="mb-4 text-neutral-700 dark:text-neutral-300">
Content above the separator
</p>
<Separator />
<p className="mt-4 text-neutral-700 dark:text-neutral-300">
Content below the separator
</p>
</div>
),
};
export const Vertical: Story = {
render: () => (
<div className="flex h-16 items-center gap-4">
<span className="text-neutral-700 dark:text-neutral-300">Left</span>
<Separator orientation="vertical" />
<span className="text-neutral-700 dark:text-neutral-300">Right</span>
</div>
),
};
export const WithCustomStyles: Story = {
render: () => (
<div className="w-full max-w-md space-y-4">
<Separator className="bg-violet-500" />
<Separator className="h-0.5 bg-gradient-to-r from-violet-500 to-blue-500" />
<Separator className="bg-neutral-400 dark:bg-neutral-600" />
</div>
),
};
export const InSection: Story = {
render: () => (
<div className="w-full max-w-md space-y-6">
<section>
<h2 className="mb-2 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
Featured Agents
</h2>
<p className="text-neutral-600 dark:text-neutral-400">
Browse our collection of featured AI agents.
</p>
</section>
<Separator className="my-6" />
<section>
<h2 className="mb-2 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
Top Creators
</h2>
<p className="text-neutral-600 dark:text-neutral-400">
Meet the creators behind the most popular agents.
</p>
</section>
</div>
),
};

View File

@@ -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 `<hr>` for horizontal separators and a styled `<div>` 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 (
<hr
className={cn(baseStyles, "h-px w-full border-0", className)}
aria-hidden={decorative}
role={decorative ? "none" : "separator"}
/>
);
}
return (
<div
className={cn(baseStyles, "h-full w-px", className)}
aria-hidden={decorative}
role={decorative ? "none" : "separator"}
aria-orientation="vertical"
/>
);
}

View File

@@ -0,0 +1,128 @@
import type { Meta, StoryObj } from "@storybook/react";
import { FadeIn } from "./FadeIn";
const meta: Meta<typeof FadeIn> = {
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<typeof FadeIn>;
const DemoCard = ({ title }: { title: string }) => (
<div className="rounded-xl bg-neutral-100 p-6 dark:bg-neutral-800">
<h3 className="mb-2 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{title}
</h3>
<p className="text-neutral-600 dark:text-neutral-400">
This card fades in with a smooth animation.
</p>
</div>
);
export const Default: Story = {
args: {
direction: "up",
children: <DemoCard title="Fade Up" />,
},
};
export const FadeDown: Story = {
args: {
direction: "down",
children: <DemoCard title="Fade Down" />,
},
};
export const FadeLeft: Story = {
args: {
direction: "left",
children: <DemoCard title="Fade Left" />,
},
};
export const FadeRight: Story = {
args: {
direction: "right",
children: <DemoCard title="Fade Right" />,
},
};
export const FadeOnly: Story = {
args: {
direction: "none",
children: <DemoCard title="Fade Only (No Direction)" />,
},
};
export const WithDelay: Story = {
args: {
direction: "up",
delay: 0.5,
children: <DemoCard title="Delayed Fade (0.5s)" />,
},
};
export const SlowAnimation: Story = {
args: {
direction: "up",
duration: 1.5,
children: <DemoCard title="Slow Animation (1.5s)" />,
},
};
export const LargeDistance: Story = {
args: {
direction: "up",
distance: 60,
children: <DemoCard title="Large Distance (60px)" />,
},
};
export const MultipleElements: Story = {
render: () => (
<div className="space-y-4">
<FadeIn direction="up" delay={0}>
<DemoCard title="First Card" />
</FadeIn>
<FadeIn direction="up" delay={0.1}>
<DemoCard title="Second Card" />
</FadeIn>
<FadeIn direction="up" delay={0.2}>
<DemoCard title="Third Card" />
</FadeIn>
</div>
),
};
export const HeroExample: Story = {
render: () => (
<div className="text-center">
<FadeIn direction="down" delay={0}>
<h1 className="mb-4 text-4xl font-bold text-neutral-900 dark:text-neutral-100">
Welcome to the Marketplace
</h1>
</FadeIn>
<FadeIn direction="up" delay={0.2}>
<p className="mb-8 text-xl text-neutral-600 dark:text-neutral-400">
Discover AI agents built by the community
</p>
</FadeIn>
<FadeIn direction="up" delay={0.4}>
<button className="rounded-full bg-violet-600 px-8 py-3 text-white hover:bg-violet-700">
Get Started
</button>
</FadeIn>
</div>
),
};

View File

@@ -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 <Component className={className}>{children}</Component>;
}
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 (
<MotionComponent
className={cn(className)}
initial="hidden"
animate={viewport ? undefined : "visible"}
whileInView={viewport ? "visible" : undefined}
viewport={viewport ? { once, amount: viewportAmount } : undefined}
variants={variants}
>
{children}
</MotionComponent>
);
}

View File

@@ -0,0 +1,180 @@
import type { Meta, StoryObj } from "@storybook/react";
import { StaggeredList } from "./StaggeredList";
const meta: Meta<typeof StaggeredList> = {
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<typeof StaggeredList>;
const DemoCard = ({ title, index }: { title: string; index: number }) => (
<div className="rounded-xl bg-neutral-100 p-4 dark:bg-neutral-800">
<h3 className="mb-1 font-semibold text-neutral-900 dark:text-neutral-100">
{title}
</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Card #{index + 1} with staggered animation
</p>
</div>
);
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) => <DemoCard key={i} title={item} index={i} />),
},
};
export const FadeDown: Story = {
args: {
direction: "down",
className: "space-y-4",
children: items.map((item, i) => <DemoCard key={i} title={item} index={i} />),
},
};
export const FadeLeft: Story = {
args: {
direction: "left",
className: "flex gap-4",
children: items.map((item, i) => <DemoCard key={i} title={item} index={i} />),
},
};
export const FadeRight: Story = {
args: {
direction: "right",
className: "flex gap-4",
children: items.map((item, i) => <DemoCard key={i} title={item} index={i} />),
},
};
export const FastStagger: Story = {
args: {
direction: "up",
staggerDelay: 0.05,
className: "space-y-4",
children: items.map((item, i) => <DemoCard key={i} title={item} index={i} />),
},
};
export const SlowStagger: Story = {
args: {
direction: "up",
staggerDelay: 0.3,
className: "space-y-4",
children: items.map((item, i) => <DemoCard key={i} title={item} index={i} />),
},
};
export const WithInitialDelay: Story = {
args: {
direction: "up",
initialDelay: 0.5,
className: "space-y-4",
children: items.map((item, i) => <DemoCard key={i} title={item} index={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) => <DemoCard key={i} title={item} index={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 (
<StaggeredList
direction="up"
staggerDelay={0.1}
className="grid grid-cols-2 gap-6 md:grid-cols-4"
>
{agents.map((agent, i) => (
<div
key={i}
className="rounded-2xl bg-white p-4 shadow-md dark:bg-neutral-900"
>
<div className="mb-3 aspect-video rounded-xl bg-gradient-to-br from-violet-500 to-blue-500" />
<h3 className="mb-1 font-semibold text-neutral-900 dark:text-neutral-100">
{agent.name}
</h3>
<p className="text-sm text-neutral-500">{agent.runs} runs</p>
</div>
))}
</StaggeredList>
);
},
};
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 (
<StaggeredList
direction="up"
staggerDelay={0.12}
className="grid grid-cols-2 gap-6 md:grid-cols-4"
>
{creators.map((creator, i) => (
<div
key={i}
className={`rounded-2xl p-5 ${colors[i % colors.length]}`}
>
<div className="mb-3 h-12 w-12 rounded-full bg-neutral-300 dark:bg-neutral-700" />
<h3 className="mb-1 font-semibold text-neutral-900 dark:text-neutral-100">
{creator.name}
</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{creator.agents} agents
</p>
</div>
))}
</StaggeredList>
);
},
};

View File

@@ -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 (
<div className={className}>
{children.map((child, index) => (
<div key={index} className={itemClassName}>
{child}
</div>
))}
</div>
);
}
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 (
<motion.div
className={cn(className)}
initial="hidden"
animate={viewport ? undefined : "visible"}
whileInView={viewport ? "visible" : undefined}
viewport={viewport ? { once, amount: viewportAmount } : undefined}
variants={containerVariants}
>
{children.map((child, index) => (
<motion.div key={index} className={itemClassName} variants={itemVariants}>
{child}
</motion.div>
))}
</motion.div>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,133 +0,0 @@
# Table of contents
* [AutoGPT Blocks Overview](README.md)
## Guides
* [LLM Providers](guides/llm-providers.md)
* [Voice Providers](guides/voice-providers.md)
## Block Integrations
* [Airtable Bases](block-integrations/airtable/bases.md)
* [Airtable Records](block-integrations/airtable/records.md)
* [Airtable Schema](block-integrations/airtable/schema.md)
* [Airtable Triggers](block-integrations/airtable/triggers.md)
* [Apollo Organization](block-integrations/apollo/organization.md)
* [Apollo People](block-integrations/apollo/people.md)
* [Apollo Person](block-integrations/apollo/person.md)
* [Ayrshare Post To Bluesky](block-integrations/ayrshare/post_to_bluesky.md)
* [Ayrshare Post To Facebook](block-integrations/ayrshare/post_to_facebook.md)
* [Ayrshare Post To GMB](block-integrations/ayrshare/post_to_gmb.md)
* [Ayrshare Post To Instagram](block-integrations/ayrshare/post_to_instagram.md)
* [Ayrshare Post To LinkedIn](block-integrations/ayrshare/post_to_linkedin.md)
* [Ayrshare Post To Pinterest](block-integrations/ayrshare/post_to_pinterest.md)
* [Ayrshare Post To Reddit](block-integrations/ayrshare/post_to_reddit.md)
* [Ayrshare Post To Snapchat](block-integrations/ayrshare/post_to_snapchat.md)
* [Ayrshare Post To Telegram](block-integrations/ayrshare/post_to_telegram.md)
* [Ayrshare Post To Threads](block-integrations/ayrshare/post_to_threads.md)
* [Ayrshare Post To TikTok](block-integrations/ayrshare/post_to_tiktok.md)
* [Ayrshare Post To X](block-integrations/ayrshare/post_to_x.md)
* [Ayrshare Post To YouTube](block-integrations/ayrshare/post_to_youtube.md)
* [Baas Bots](block-integrations/baas/bots.md)
* [Bannerbear Text Overlay](block-integrations/bannerbear/text_overlay.md)
* [Basic](block-integrations/basic.md)
* [Compass Triggers](block-integrations/compass/triggers.md)
* [Data](block-integrations/data.md)
* [Dataforseo Keyword Suggestions](block-integrations/dataforseo/keyword_suggestions.md)
* [Dataforseo Related Keywords](block-integrations/dataforseo/related_keywords.md)
* [Discord Bot Blocks](block-integrations/discord/bot_blocks.md)
* [Discord OAuth Blocks](block-integrations/discord/oauth_blocks.md)
* [Enrichlayer LinkedIn](block-integrations/enrichlayer/linkedin.md)
* [Exa Answers](block-integrations/exa/answers.md)
* [Exa Code Context](block-integrations/exa/code_context.md)
* [Exa Contents](block-integrations/exa/contents.md)
* [Exa Research](block-integrations/exa/research.md)
* [Exa Search](block-integrations/exa/search.md)
* [Exa Similar](block-integrations/exa/similar.md)
* [Exa Webhook Blocks](block-integrations/exa/webhook_blocks.md)
* [Exa Websets](block-integrations/exa/websets.md)
* [Exa Websets Enrichment](block-integrations/exa/websets_enrichment.md)
* [Exa Websets Import Export](block-integrations/exa/websets_import_export.md)
* [Exa Websets Items](block-integrations/exa/websets_items.md)
* [Exa Websets Monitor](block-integrations/exa/websets_monitor.md)
* [Exa Websets Polling](block-integrations/exa/websets_polling.md)
* [Exa Websets Search](block-integrations/exa/websets_search.md)
* [Fal AI Video Generator](block-integrations/fal/ai_video_generator.md)
* [Firecrawl Crawl](block-integrations/firecrawl/crawl.md)
* [Firecrawl Extract](block-integrations/firecrawl/extract.md)
* [Firecrawl Map](block-integrations/firecrawl/map.md)
* [Firecrawl Scrape](block-integrations/firecrawl/scrape.md)
* [Firecrawl Search](block-integrations/firecrawl/search.md)
* [Generic Webhook Triggers](block-integrations/generic_webhook/triggers.md)
* [GitHub Checks](block-integrations/github/checks.md)
* [GitHub CI](block-integrations/github/ci.md)
* [GitHub Issues](block-integrations/github/issues.md)
* [GitHub Pull Requests](block-integrations/github/pull_requests.md)
* [GitHub Repo](block-integrations/github/repo.md)
* [GitHub Reviews](block-integrations/github/reviews.md)
* [GitHub Statuses](block-integrations/github/statuses.md)
* [GitHub Triggers](block-integrations/github/triggers.md)
* [Google Calendar](block-integrations/google/calendar.md)
* [Google Docs](block-integrations/google/docs.md)
* [Google Gmail](block-integrations/google/gmail.md)
* [Google Sheets](block-integrations/google/sheets.md)
* [HubSpot Company](block-integrations/hubspot/company.md)
* [HubSpot Contact](block-integrations/hubspot/contact.md)
* [HubSpot Engagement](block-integrations/hubspot/engagement.md)
* [Jina Chunking](block-integrations/jina/chunking.md)
* [Jina Embeddings](block-integrations/jina/embeddings.md)
* [Jina Fact Checker](block-integrations/jina/fact_checker.md)
* [Jina Search](block-integrations/jina/search.md)
* [Linear Comment](block-integrations/linear/comment.md)
* [Linear Issues](block-integrations/linear/issues.md)
* [Linear Projects](block-integrations/linear/projects.md)
* [LLM](block-integrations/llm.md)
* [Logic](block-integrations/logic.md)
* [Misc](block-integrations/misc.md)
* [Multimedia](block-integrations/multimedia.md)
* [Notion Create Page](block-integrations/notion/create_page.md)
* [Notion Read Database](block-integrations/notion/read_database.md)
* [Notion Read Page](block-integrations/notion/read_page.md)
* [Notion Read Page Markdown](block-integrations/notion/read_page_markdown.md)
* [Notion Search](block-integrations/notion/search.md)
* [Nvidia Deepfake](block-integrations/nvidia/deepfake.md)
* [Replicate Flux Advanced](block-integrations/replicate/flux_advanced.md)
* [Replicate Replicate Block](block-integrations/replicate/replicate_block.md)
* [Search](block-integrations/search.md)
* [Slant3D Filament](block-integrations/slant3d/filament.md)
* [Slant3D Order](block-integrations/slant3d/order.md)
* [Slant3D Slicing](block-integrations/slant3d/slicing.md)
* [Slant3D Webhook](block-integrations/slant3d/webhook.md)
* [Smartlead Campaign](block-integrations/smartlead/campaign.md)
* [Stagehand Blocks](block-integrations/stagehand/blocks.md)
* [System Library Operations](block-integrations/system/library_operations.md)
* [System Store Operations](block-integrations/system/store_operations.md)
* [Text](block-integrations/text.md)
* [Todoist Comments](block-integrations/todoist/comments.md)
* [Todoist Labels](block-integrations/todoist/labels.md)
* [Todoist Projects](block-integrations/todoist/projects.md)
* [Todoist Sections](block-integrations/todoist/sections.md)
* [Todoist Tasks](block-integrations/todoist/tasks.md)
* [Twitter Blocks](block-integrations/twitter/blocks.md)
* [Twitter Bookmark](block-integrations/twitter/bookmark.md)
* [Twitter Follows](block-integrations/twitter/follows.md)
* [Twitter Hide](block-integrations/twitter/hide.md)
* [Twitter Like](block-integrations/twitter/like.md)
* [Twitter List Follows](block-integrations/twitter/list_follows.md)
* [Twitter List Lookup](block-integrations/twitter/list_lookup.md)
* [Twitter List Members](block-integrations/twitter/list_members.md)
* [Twitter List Tweets Lookup](block-integrations/twitter/list_tweets_lookup.md)
* [Twitter Manage](block-integrations/twitter/manage.md)
* [Twitter Manage Lists](block-integrations/twitter/manage_lists.md)
* [Twitter Mutes](block-integrations/twitter/mutes.md)
* [Twitter Pinned Lists](block-integrations/twitter/pinned_lists.md)
* [Twitter Quote](block-integrations/twitter/quote.md)
* [Twitter Retweet](block-integrations/twitter/retweet.md)
* [Twitter Search Spaces](block-integrations/twitter/search_spaces.md)
* [Twitter Spaces Lookup](block-integrations/twitter/spaces_lookup.md)
* [Twitter Timeline](block-integrations/twitter/timeline.md)
* [Twitter Tweet Lookup](block-integrations/twitter/tweet_lookup.md)
* [Twitter User Lookup](block-integrations/twitter/user_lookup.md)
* [Wolfram LLM API](block-integrations/wolfram/llm_api.md)
* [Zerobounce Validate Emails](block-integrations/zerobounce/validate_emails.md)

Some files were not shown because too many files have changed in this diff Show More