feat: improve integration UI

This commit is contained in:
Anish Sarkar 2026-02-09 02:49:08 +05:30
parent 20a13df7e7
commit cf76f6f575
3 changed files with 187 additions and 163 deletions

View file

@ -187,5 +187,24 @@ button {
background-color: hsl(var(--muted-foreground) / 0.4); background-color: hsl(var(--muted-foreground) / 0.4);
} }
/* Integrations section — vertical column auto-scroll */
@keyframes integrations-scroll-up {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%);
}
}
@keyframes integrations-scroll-down {
0% {
transform: translateY(-50%);
}
100% {
transform: translateY(0);
}
}
@source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}'; @source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}';
@source '../node_modules/streamdown/dist/*.js'; @source '../node_modules/streamdown/dist/*.js';

View file

@ -1,5 +1,6 @@
"use client"; "use client";
import React, { useEffect, useState } from "react";
import type React from "react";
interface Integration { interface Integration {
name: string; name: string;
@ -8,181 +9,197 @@ interface Integration {
const INTEGRATIONS: Integration[] = [ const INTEGRATIONS: Integration[] = [
// Search // Search
{ name: "Tavily", icon: "https://www.tavily.com/images/logo.svg" }, { name: "Tavily", icon: "/connectors/tavily.svg" },
{ { name: "Elasticsearch", icon: "/connectors/elasticsearch.svg" },
name: "LinkUp", { name: "Baidu Search", icon: "/connectors/baidu-search.svg" },
icon: "https://framerusercontent.com/images/7zeIm6t3f1HaSltkw8upEvsD80.png?scale-down-to=512", { name: "SearXNG", icon: "/connectors/searxng.svg" },
},
{ name: "Elasticsearch", icon: "https://cdn.simpleicons.org/elastic/00A9E5" },
// Communication // Communication
{ { name: "Slack", icon: "/connectors/slack.svg" },
name: "Slack", { name: "Discord", icon: "/connectors/discord.svg" },
icon: "https://upload.wikimedia.org/wikipedia/commons/d/d5/Slack_icon_2019.svg", { name: "Gmail", icon: "/connectors/google-gmail.svg" },
}, { name: "Microsoft Teams", icon: "/connectors/microsoft-teams.svg" },
{ name: "Discord", icon: "https://cdn.simpleicons.org/discord/5865F2" },
{ name: "Gmail", icon: "https://cdn.simpleicons.org/gmail/EA4335" },
// Project Management // Project Management
{ name: "Linear", icon: "https://cdn.simpleicons.org/linear/5E6AD2" }, { name: "Linear", icon: "/connectors/linear.svg" },
{ name: "Jira", icon: "https://cdn.simpleicons.org/jira/0052CC" }, { name: "Jira", icon: "/connectors/jira.svg" },
{ name: "ClickUp", icon: "https://cdn.simpleicons.org/clickup/7B68EE" }, { name: "ClickUp", icon: "/connectors/clickup.svg" },
{ name: "Airtable", icon: "https://cdn.simpleicons.org/airtable/18BFFF" }, { name: "Airtable", icon: "/connectors/airtable.svg" },
// Documentation & Knowledge // Documentation & Knowledge
{ name: "Confluence", icon: "https://cdn.simpleicons.org/confluence/172B4D" }, { name: "Confluence", icon: "/connectors/confluence.svg" },
{ name: "Notion", icon: "https://cdn.simpleicons.org/notion/000000/ffffff" }, { name: "Notion", icon: "/connectors/notion.svg" },
{ name: "Web Pages", icon: "https://cdn.jsdelivr.net/npm/lucide-static@0.294.0/icons/globe.svg" }, { name: "BookStack", icon: "/connectors/bookstack.svg" },
{ name: "Obsidian", icon: "/connectors/obsidian.svg" },
// Cloud Storage // Cloud Storage
{ name: "Google Drive", icon: "https://cdn.simpleicons.org/googledrive/4285F4" }, { name: "Google Drive", icon: "/connectors/google-drive.svg" },
{ name: "Dropbox", icon: "https://cdn.simpleicons.org/dropbox/0061FF" },
{
name: "Amazon S3",
icon: "https://upload.wikimedia.org/wikipedia/commons/b/bc/Amazon-S3-Logo.svg",
},
// Development // Development
{ name: "GitHub", icon: "https://cdn.simpleicons.org/github/181717/ffffff" }, { name: "GitHub", icon: "/connectors/github.svg" },
// Productivity // Productivity
{ name: "Google Calendar", icon: "https://cdn.simpleicons.org/googlecalendar/4285F4" }, { name: "Google Calendar", icon: "/connectors/google-calendar.svg" },
{ name: "Luma", icon: "https://images.lumacdn.com/social-images/default-social-202407.png" }, { name: "Luma", icon: "/connectors/luma.svg" },
// Media // Media
{ name: "YouTube", icon: "https://cdn.simpleicons.org/youtube/FF0000" }, { name: "YouTube", icon: "/connectors/youtube.svg" },
]; ];
function SemiCircleOrbit({ radius, centerX, centerY, count, iconSize, startIndex }: any) { // 5 vertical columns — 21 icons spread across categories
const COLUMNS: number[][] = [
[2, 5, 10, 0, 11],
[1, 7, 20, 17],
[13, 6, 4, 16],
[12, 8, 15, 18],
[3, 9, 14, 19],
];
// Different scroll speeds per column for organic feel (seconds)
const SCROLL_DURATIONS = [26, 32, 22, 30, 28];
function IntegrationCard({ integration }: { integration: Integration }) {
return ( return (
<> <div
{/* Semi-circle glow background */} className="w-[60px] h-[60px] sm:w-[80px] sm:h-[80px] md:w-[120px] md:h-[120px] lg:w-[140px] lg:h-[140px] rounded-[16px] sm:rounded-[20px] md:rounded-[24px] flex items-center justify-center shrink-0"
<div className="absolute inset-0 flex justify-center items-start overflow-visible"> style={{
<div background:
className=" "linear-gradient(145deg, var(--card-from), var(--card-to))",
w-[800px] h-[800px] rounded-full boxShadow:
bg-[radial-gradient(circle_at_center,rgba(0,0,0,0.15),transparent_70%)] "inset 0 1px 0 0 var(--card-highlight), 0 4px 24px var(--card-shadow)",
dark:bg-[radial-gradient(circle_at_center,rgba(255,255,255,0.15),transparent_70%)] }}
blur-3xl >
pointer-events-none <img
" src={integration.icon}
style={{ alt={integration.name}
zIndex: 0, className="w-6 h-6 sm:w-7 sm:h-7 md:w-10 md:h-10 lg:w-12 lg:h-12 object-contain"
transform: "translateY(-20%)", loading="lazy"
}} />
/> </div>
);
}
function ScrollingColumn({
cards,
scrollUp,
duration,
colIndex,
isEdge,
isEdgeAdjacent,
}: {
cards: number[];
scrollUp: boolean;
duration: number;
colIndex: number;
isEdge: boolean;
isEdgeAdjacent: boolean;
}) {
// Edge columns get a heavy vertical mask; edge-adjacent columns get a lighter one to smooth the transition
const columnMask = isEdge
? {
maskImage:
"linear-gradient(to bottom, transparent 0%, transparent 20%, black 40%, black 60%, transparent 80%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, transparent 0%, transparent 20%, black 40%, black 60%, transparent 80%, transparent 100%)",
}
: isEdgeAdjacent
? {
maskImage:
"linear-gradient(to bottom, transparent 0%, transparent 10%, black 30%, black 70%, transparent 90%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, transparent 0%, transparent 10%, black 30%, black 70%, transparent 90%, transparent 100%)",
}
: {};
const cardSet = cards.map((integrationIndex, i) => (
<IntegrationCard
key={`${INTEGRATIONS[integrationIndex].name}-c${colIndex}-${i}`}
integration={INTEGRATIONS[integrationIndex]}
/>
));
return (
<div className="flex-shrink-0 overflow-hidden" style={{ ...columnMask, contain: "layout style paint" }}>
{/* Outer div has NO gap — each inner copy uses pb matching the gap so both halves are identical in height → seamless -50% loop */}
<div
className="flex flex-col"
style={{
animation: `${scrollUp ? "integrations-scroll-up" : "integrations-scroll-down"} ${duration}s linear infinite`,
willChange: "transform",
transform: "translateZ(0)",
}}
>
<div className="flex flex-col gap-2 sm:gap-3 md:gap-5 lg:gap-6 pb-2 sm:pb-3 md:pb-5 lg:pb-6">
{cardSet}
</div>
<div className="flex flex-col gap-2 sm:gap-3 md:gap-5 lg:gap-6 pb-2 sm:pb-3 md:pb-5 lg:pb-6">
{cardSet}
</div>
</div> </div>
</div>
{/* Orbit icons */}
{Array.from({ length: count }).map((_, index) => {
const actualIndex = startIndex + index;
// Skip if we've run out of integrations
if (actualIndex >= INTEGRATIONS.length) return null;
const angle = (index / (count - 1)) * 180;
const x = radius * Math.cos((angle * Math.PI) / 180);
const y = radius * Math.sin((angle * Math.PI) / 180);
const integration = INTEGRATIONS[actualIndex];
// Tooltip positioning — above or below based on angle
const tooltipAbove = angle > 90;
return (
<div
key={index}
className="absolute flex flex-col items-center group"
style={{
left: `${centerX + x - iconSize / 2}px`,
top: `${centerY - y - iconSize / 2}px`,
zIndex: 5,
}}
>
<img
src={integration.icon}
alt={integration.name}
width={iconSize}
height={iconSize}
className="object-contain cursor-pointer transition-transform hover:scale-110"
style={{ minWidth: iconSize, minHeight: iconSize }} // fix accidental shrink
/>
{/* Tooltip */}
<div
className={`absolute ${
tooltipAbove ? "bottom-[calc(100%+8px)]" : "top-[calc(100%+8px)]"
} hidden group-hover:block w-auto min-w-max rounded-lg bg-black px-3 py-1.5 text-xs text-white shadow-lg text-center whitespace-nowrap`}
>
{integration.name}
<div
className={`absolute left-1/2 -translate-x-1/2 w-3 h-3 rotate-45 bg-black ${
tooltipAbove ? "top-full" : "bottom-full"
}`}
></div>
</div>
</div>
);
})}
</>
); );
} }
export default function ExternalIntegrations() { export default function ExternalIntegrations() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const updateSize = () => setSize({ width: window.innerWidth, height: window.innerHeight });
updateSize();
window.addEventListener("resize", updateSize);
return () => window.removeEventListener("resize", updateSize);
}, []);
const baseWidth = Math.min(size.width * 0.8, 700);
const centerX = baseWidth / 2;
const centerY = baseWidth * 0.5;
const iconSize =
size.width < 480
? Math.max(24, baseWidth * 0.05)
: size.width < 768
? Math.max(28, baseWidth * 0.06)
: Math.max(32, baseWidth * 0.07);
return ( return (
<section className="py-12 relative min-h-screen w-full overflow-visible"> <section
<div className="relative flex flex-col items-center text-center z-10"> className={[
<h1 className="my-6 text-4xl font-bold lg:text-7xl">Integrations</h1> "relative py-20 md:py-28 overflow-hidden",
<p className="mb-12 max-w-2xl text-gray-600 dark:text-gray-400 lg:text-xl"> // No explicit background — inherits the page gradient for seamless blending
Integrate with your team's most important tools // CSS custom properties — light mode (card styling)
</p> "[--card-from:rgba(255,255,255,0.9)]",
"[--card-to:rgba(245,245,248,0.92)]",
"[--card-highlight:rgba(255,255,255,0.5)]",
"[--card-lowlight:transparent]",
"[--card-shadow:transparent]",
"[--card-border:transparent]",
// CSS custom properties — dark mode (card styling)
"dark:[--card-from:rgb(28,28,32)]",
"dark:[--card-to:rgb(28,28,32)]",
"dark:[--card-highlight:rgba(255,255,255,0.03)]",
"dark:[--card-lowlight:rgba(0,0,0,0.1)]",
"dark:[--card-shadow:rgba(0,0,0,0.15)]",
"dark:[--card-border:rgba(255,255,255,0.03)]",
].join(" ")}
>
{/* Heading */}
<div className="text-center mb-12 md:mb-16 relative z-20 px-4">
<h3 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-white leading-[1.1] tracking-tight">
Integrate with your
<br />
team&apos;s most important tools
</h3>
</div>
<div {/* Scrolling columns container — masked at edges so the page background shows through seamlessly */}
className="relative overflow-visible" <div
style={{ width: baseWidth, height: baseWidth * 0.7, paddingBottom: "100px" }} className="relative"
> style={
<SemiCircleOrbit {
radius={baseWidth * 0.22} maskImage:
centerX={centerX} "linear-gradient(to bottom, transparent 0%, black 25%, black 70%, transparent 100%), " +
centerY={centerY} "linear-gradient(to right, transparent 0%, black 12%, black 88%, transparent 100%)",
count={5} WebkitMaskImage:
iconSize={iconSize} "linear-gradient(to bottom, transparent 0%, black 25%, black 75%, transparent 100%), " +
startIndex={0} "linear-gradient(to right, transparent 0%, black 12%, black 88%, transparent 100%)",
/> maskComposite: "intersect",
<SemiCircleOrbit WebkitMaskComposite: "source-in",
radius={baseWidth * 0.36} } as React.CSSProperties
centerX={centerX} }
centerY={centerY} >
count={6} {/* 5 scrolling columns */}
iconSize={iconSize} <div className="flex justify-center gap-2 sm:gap-3 md:gap-5 lg:gap-6 h-[340px] sm:h-[420px] md:h-[560px] lg:h-[640px] overflow-hidden">
startIndex={5} {COLUMNS.map((column, colIndex) => (
/> <ScrollingColumn
<SemiCircleOrbit key={`col-${SCROLL_DURATIONS[colIndex]}-${colIndex}`}
radius={baseWidth * 0.5} cards={column}
centerX={centerX} scrollUp={colIndex % 2 === 0}
centerY={centerY} duration={SCROLL_DURATIONS[colIndex]}
count={8} colIndex={colIndex}
iconSize={iconSize} isEdge={colIndex === 0 || colIndex === COLUMNS.length - 1}
startIndex={11} isEdgeAdjacent={colIndex === 1 || colIndex === COLUMNS.length - 2}
/> />
))}
</div> </div>
</div> </div>
</section> </section>

View file

@ -1,12 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="6" fill="url(#composio-gradient)"/>
<path d="M12 6L17 9V15L12 18L7 15V9L12 6Z" stroke="white" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M12 6V12M12 12L17 9M12 12L7 9M12 12V18" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="12" cy="12" r="2" fill="white"/>
<defs>
<linearGradient id="composio-gradient" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
<stop stop-color="#8B5CF6"/>
<stop offset="1" stop-color="#A855F7"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 640 B