Merge pull request #807 from AnishSarkar22/fix/homepage

feat: improve homepage integrations section
This commit is contained in:
Rohan Verma 2026-02-10 14:08:43 -08:00 committed by GitHub
commit 9f335f5b3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 280 additions and 168 deletions

View file

@ -187,5 +187,24 @@ button {
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/streamdown/dist/*.js';

View file

@ -1,5 +1,7 @@
"use client";
import React, { useEffect, useState } from "react";
import type React from "react";
import Image from "next/image";
interface Integration {
name: string;
@ -8,181 +10,210 @@ interface Integration {
const INTEGRATIONS: Integration[] = [
// Search
{ name: "Tavily", icon: "https://www.tavily.com/images/logo.svg" },
{
name: "LinkUp",
icon: "https://framerusercontent.com/images/7zeIm6t3f1HaSltkw8upEvsD80.png?scale-down-to=512",
},
{ name: "Elasticsearch", icon: "https://cdn.simpleicons.org/elastic/00A9E5" },
{ name: "Tavily", icon: "/connectors/tavily.svg" },
{ name: "Elasticsearch", icon: "/connectors/elasticsearch.svg" },
{ name: "Baidu Search", icon: "/connectors/baidu-search.svg" },
{ name: "SearXNG", icon: "/connectors/searxng.svg" },
// Communication
{
name: "Slack",
icon: "https://upload.wikimedia.org/wikipedia/commons/d/d5/Slack_icon_2019.svg",
},
{ name: "Discord", icon: "https://cdn.simpleicons.org/discord/5865F2" },
{ name: "Gmail", icon: "https://cdn.simpleicons.org/gmail/EA4335" },
{ name: "Slack", icon: "/connectors/slack.svg" },
{ name: "Discord", icon: "/connectors/discord.svg" },
{ name: "Gmail", icon: "/connectors/google-gmail.svg" },
{ name: "Microsoft Teams", icon: "/connectors/microsoft-teams.svg" },
// Project Management
{ name: "Linear", icon: "https://cdn.simpleicons.org/linear/5E6AD2" },
{ name: "Jira", icon: "https://cdn.simpleicons.org/jira/0052CC" },
{ name: "ClickUp", icon: "https://cdn.simpleicons.org/clickup/7B68EE" },
{ name: "Airtable", icon: "https://cdn.simpleicons.org/airtable/18BFFF" },
{ name: "Linear", icon: "/connectors/linear.svg" },
{ name: "Jira", icon: "/connectors/jira.svg" },
{ name: "ClickUp", icon: "/connectors/clickup.svg" },
{ name: "Airtable", icon: "/connectors/airtable.svg" },
// Documentation & Knowledge
{ name: "Confluence", icon: "https://cdn.simpleicons.org/confluence/172B4D" },
{ name: "Notion", icon: "https://cdn.simpleicons.org/notion/000000/ffffff" },
{ name: "Web Pages", icon: "https://cdn.jsdelivr.net/npm/lucide-static@0.294.0/icons/globe.svg" },
{ name: "Confluence", icon: "/connectors/confluence.svg" },
{ name: "Notion", icon: "/connectors/notion.svg" },
{ name: "BookStack", icon: "/connectors/bookstack.svg" },
{ name: "Obsidian", icon: "/connectors/obsidian.svg" },
// Cloud Storage
{ name: "Google Drive", icon: "https://cdn.simpleicons.org/googledrive/4285F4" },
{ 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",
},
{ name: "Google Drive", icon: "/connectors/google-drive.svg" },
// Development
{ name: "GitHub", icon: "https://cdn.simpleicons.org/github/181717/ffffff" },
{ name: "GitHub", icon: "/connectors/github.svg" },
// Productivity
{ name: "Google Calendar", icon: "https://cdn.simpleicons.org/googlecalendar/4285F4" },
{ name: "Luma", icon: "https://images.lumacdn.com/social-images/default-social-202407.png" },
{ name: "Google Calendar", icon: "/connectors/google-calendar.svg" },
{ name: "Luma", icon: "/connectors/luma.svg" },
// Media
{ name: "YouTube", icon: "https://cdn.simpleicons.org/youtube/FF0000" },
{ name: "YouTube", icon: "/connectors/youtube.svg" },
// Search
{ name: "Linkup", icon: "/connectors/linkup.svg" },
// Meetings
{ name: "Circleback", icon: "/connectors/circleback.svg" },
// AI
{ name: "MCP", icon: "/connectors/modelcontextprotocol.svg" },
];
function SemiCircleOrbit({ radius, centerX, centerY, count, iconSize, startIndex }: any) {
// 5 vertical columns — 23 icons spread across categories
const COLUMNS: number[][] = [
[2, 5, 10, 0, 21, 11],
[1, 7, 20, 17],
[13, 6, 23, 4, 16],
[12, 8, 15, 18],
[3, 9, 14, 22, 19],
];
// Different scroll speeds per column for organic feel (seconds)
const SCROLL_DURATIONS = [26, 32, 22, 30, 28];
function IntegrationCard({ integration }: { integration: Integration }) {
return (
<>
{/* Semi-circle glow background */}
<div className="absolute inset-0 flex justify-center items-start overflow-visible">
<div
className="
w-[800px] h-[800px] rounded-full
bg-[radial-gradient(circle_at_center,rgba(0,0,0,0.15),transparent_70%)]
dark:bg-[radial-gradient(circle_at_center,rgba(255,255,255,0.15),transparent_70%)]
blur-3xl
pointer-events-none
"
style={{
zIndex: 0,
transform: "translateY(-20%)",
}}
/>
<div
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 select-none"
style={{
background: "linear-gradient(145deg, var(--card-from), var(--card-to))",
boxShadow: "inset 0 1px 0 0 var(--card-highlight), 0 4px 24px var(--card-shadow)",
}}
>
<Image
src={integration.icon}
alt={integration.name}
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 select-none pointer-events-none"
loading="lazy"
draggable={false}
width={48}
height={48}
/>
</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>
{/* 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>
);
})}
</>
</div>
);
}
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 (
<section className="py-12 relative min-h-screen w-full overflow-visible">
<div className="relative flex flex-col items-center text-center z-10">
<h1 className="my-6 text-4xl font-bold lg:text-7xl">Integrations</h1>
<p className="mb-12 max-w-2xl text-gray-600 dark:text-gray-400 lg:text-xl">
Integrate with your team's most important tools
</p>
<section
className={[
"relative py-20 md:py-28 overflow-hidden",
// No explicit background — inherits the page gradient for seamless blending
// CSS custom properties — light mode (card styling)
"[--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
className="relative overflow-visible"
style={{ width: baseWidth, height: baseWidth * 0.7, paddingBottom: "100px" }}
>
<SemiCircleOrbit
radius={baseWidth * 0.22}
centerX={centerX}
centerY={centerY}
count={5}
iconSize={iconSize}
startIndex={0}
/>
<SemiCircleOrbit
radius={baseWidth * 0.36}
centerX={centerX}
centerY={centerY}
count={6}
iconSize={iconSize}
startIndex={5}
/>
<SemiCircleOrbit
radius={baseWidth * 0.5}
centerX={centerX}
centerY={centerY}
count={8}
iconSize={iconSize}
startIndex={11}
/>
{/* Scrolling columns container — masked at edges so the page background shows through seamlessly */}
<div
className="relative"
style={
{
maskImage:
"linear-gradient(to bottom, transparent 0%, black 25%, black 70%, transparent 100%), " +
"linear-gradient(to right, transparent 0%, black 12%, black 88%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, transparent 0%, black 25%, black 75%, transparent 100%), " +
"linear-gradient(to right, transparent 0%, black 12%, black 88%, transparent 100%)",
maskComposite: "intersect",
WebkitMaskComposite: "source-in",
} as React.CSSProperties
}
>
{/* 5 scrolling columns */}
<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">
{COLUMNS.map((column, colIndex) => (
<ScrollingColumn
key={`col-${SCROLL_DURATIONS[colIndex]}-${colIndex}`}
cards={column}
scrollUp={colIndex % 2 === 0}
duration={SCROLL_DURATIONS[colIndex]}
colIndex={colIndex}
isEdge={colIndex === 0 || colIndex === COLUMNS.length - 1}
isEdgeAdjacent={colIndex === 1 || colIndex === COLUMNS.length - 2}
/>
))}
</div>
</div>
</section>

View file

@ -1,4 +1,4 @@
import { IconLinkPlus, IconUsersGroup } from "@tabler/icons-react";
import { IconUsersGroup } from "@tabler/icons-react";
import {
BookOpen,
File,
@ -15,11 +15,16 @@ import { EnumConnectorName } from "./connector";
export const getConnectorIcon = (connectorType: EnumConnectorName | string, className?: string) => {
const iconProps = { className: className || "h-4 w-4" };
const imgProps = { className: className || "h-5 w-5", width: 20, height: 20 };
const imgProps = {
className: `${className || "h-5 w-5"} select-none pointer-events-none`,
width: 20,
height: 20,
draggable: false as const,
};
switch (connectorType) {
case EnumConnectorName.LINKUP_API:
return <IconLinkPlus {...iconProps} />;
return <Image src="/connectors/linkup.svg" alt="Linkup" {...imgProps} />;
case EnumConnectorName.LINEAR_CONNECTOR:
return <Image src="/connectors/linear.svg" alt="Linear" {...imgProps} />;
case EnumConnectorName.GITHUB_CONNECTOR:
@ -63,7 +68,7 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
case EnumConnectorName.YOUTUBE_CONNECTOR:
return <Image src="/connectors/youtube.svg" alt="YouTube" {...imgProps} />;
case EnumConnectorName.CIRCLEBACK_CONNECTOR:
return <IconUsersGroup {...iconProps} />;
return <Image src="/connectors/circleback.svg" alt="Circleback" {...imgProps} />;
case EnumConnectorName.MCP_CONNECTOR:
return <Image src="/connectors/modelcontextprotocol.svg" alt="MCP" {...imgProps} />;
case EnumConnectorName.OBSIDIAN_CONNECTOR:

View file

@ -0,0 +1,19 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="340.000000pt" height="340.000000pt" viewBox="0 0 340.000000 340.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,340.000000) scale(0.100000,-0.100000)" stroke="none">
<path fill="#F24D1D" d="M1385 2574 c-268 -38 -456 -142 -603 -335 -218 -286 -226 -755 -19
-1054 110 -159 268 -272 467 -336 67 -21 94 -24 240 -24 185 0 243 11 367 70
98 46 157 87 229 161 55 56 143 198 131 210 -8 8 -288 144 -296 144 -4 0 -16
-19 -28 -43 -34 -69 -117 -153 -186 -187 -214 -105 -484 -39 -620 152 -121
170 -141 455 -46 655 58 123 192 233 320 262 78 18 213 13 282 -10 101 -34
202 -120 248 -211 12 -24 23 -44 24 -46 2 -2 295 136 304 144 2 2 -12 33 -30
69 -82 165 -266 301 -477 356 -71 18 -247 31 -307 23z"/>
<path fill="#4162FE" d="M2575 1135 c-114 -40 -147 -185 -61 -271 65 -65 167 -65 232 0 83 83
59 213 -49 265 -50 24 -68 25 -122 6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

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

View file

@ -0,0 +1,50 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="130.000000pt" viewBox="0 0 512.000000 130.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,130.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M470 1287 c-73 -21 -118 -47 -176 -99 -168 -154 -270 -444 -290 -825
-9 -174 -12 -172 168 -104 215 79 557 174 779 214 l77 14 6 -36 c3 -20 6 -66
6 -102 0 -95 8 -128 34 -135 50 -12 368 132 447 204 56 51 68 81 45 117 -22
33 -72 45 -190 45 -89 0 -98 2 -91 17 34 89 46 269 23 347 -16 53 -81 118
-134 134 -52 15 -161 16 -221 1 -45 -12 -45 -12 -71 22 -114 150 -278 225
-412 186z m201 -74 c51 -23 105 -67 149 -124 l31 -38 -73 -37 c-121 -61 -245
-153 -371 -274 -107 -103 -297 -324 -327 -380 -6 -12 -14 -20 -16 -17 -9 8 17
220 37 316 68 312 196 513 362 567 55 18 151 12 208 -13z m509 -202 c21 -11
45 -35 60 -62 21 -38 25 -56 24 -129 0 -55 -9 -114 -23 -166 l-23 -81 -45 -7
c-93 -12 -88 -16 -107 82 -21 103 -65 242 -101 313 -15 28 -24 53 -22 55 9 9
124 21 162 17 22 -3 56 -13 75 -22z m-260 -98 c18 -42 38 -97 46 -122 17 -55
54 -215 54 -234 0 -9 -32 -19 -97 -31 -160 -30 -467 -110 -643 -169 -91 -30
-166 -54 -167 -53 -1 1 37 53 85 116 105 140 318 354 429 433 85 60 224 137
248 137 7 0 27 -34 45 -77z m584 -405 c17 -9 16 -12 -10 -39 -29 -30 -174
-109 -280 -152 l-62 -25 54 112 53 111 58 6 c69 6 161 0 187 -13z m-314 -3 c0
-10 -81 -167 -84 -164 -2 2 -6 39 -10 81 l-7 77 28 4 c45 7 73 7 73 2z"/>
<path d="M1880 645 l0 -415 45 0 45 0 0 415 0 415 -45 0 -45 0 0 -415z"/>
<path d="M3180 645 l0 -415 45 0 45 0 0 125 c0 82 4 125 11 125 6 0 72 -56
147 -125 l136 -125 59 0 c32 0 57 4 55 9 -1 5 -72 71 -155 146 -84 76 -153
141 -153 145 0 5 68 68 150 140 83 72 150 133 150 136 0 2 -28 4 -62 4 l-63 0
-125 -115 c-69 -63 -131 -114 -137 -114 -10 -1 -13 53 -13 239 l0 240 -45 0
-45 0 0 -415z"/>
<path d="M2184 1002 c-23 -15 -35 -51 -24 -72 31 -58 110 -39 110 26 0 35 -57
66 -86 46z"/>
<path d="M4774 820 c-43 -10 -102 -47 -121 -76 -23 -36 -33 -29 -33 21 l0 45
-45 0 -45 0 0 -405 0 -405 45 0 45 0 0 160 c0 88 3 160 6 160 4 0 26 -18 49
-39 53 -48 108 -65 197 -59 152 11 248 129 248 303 0 97 -23 158 -83 220 -72
73 -163 99 -263 75z m171 -114 c52 -40 75 -95 75 -184 0 -87 -15 -127 -63
-169 -74 -65 -176 -69 -260 -11 -59 42 -92 165 -68 256 14 49 63 109 106 129
19 8 57 12 101 11 60 -3 76 -8 109 -32z"/>
<path d="M2654 802 c-22 -11 -55 -37 -72 -58 l-32 -39 0 53 0 52 -50 0 -50 0
2 -287 3 -288 45 0 45 0 5 190 c5 199 11 226 57 267 29 26 88 48 130 48 49 -1
118 -32 135 -62 9 -16 14 -85 18 -233 l5 -210 45 0 45 0 3 193 c1 109 -2 210
-8 231 -14 54 -61 115 -106 136 -57 28 -168 31 -220 7z"/>
<path d="M2170 520 l0 -290 45 0 45 0 0 290 0 290 -45 0 -45 0 0 -290z"/>
<path d="M3800 619 c0 -212 8 -260 52 -313 46 -56 100 -79 184 -79 81 0 136
24 176 77 l23 31 5 -50 5 -50 45 0 45 0 3 288 2 287 -50 0 -50 0 0 -177 c0
-193 -9 -232 -61 -280 -52 -47 -158 -59 -216 -23 -58 35 -63 57 -63 280 l0
200 -50 0 -50 0 0 -191z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB