Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/blog/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/global.css",
"css": "../../packages/ui/src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/global.css",
"css": "../../packages/ui/src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
Expand Down
4 changes: 2 additions & 2 deletions apps/eclipse/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
"config": "",
"css": "src/app/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
Expand Down
2 changes: 1 addition & 1 deletion apps/site/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/global.css",
"css": "../../packages/ui/src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
Expand Down
42 changes: 42 additions & 0 deletions apps/site/src/app/mcp/_components/agent-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Image from "next/image";
import type { LucideIcon } from "lucide-react";

export function AgentCard({
logo,
alt,
icon: Icon,
href,
}: {
logo: string | null;
alt: string;
icon: LucideIcon;
href: string;
}) {
return (
<a
href={href}
title={alt}
aria-label={alt}
className="group relative flex h-[120px] w-full items-center justify-center rounded-[12px] border border-stroke-neutral bg-background-neutral-weaker no-underline outline-offset-4 transition-[border-color,background-color] hover:border-stroke-ppg/60 hover:bg-background-neutral focus-visible:ring-2 focus-visible:ring-stroke-ppg"
>
{logo ? (
<Image
src={logo}
alt=""
width={48}
height={48}
className="size-12 object-contain opacity-55 grayscale transition-opacity group-hover:opacity-80 dark:brightness-0 dark:invert"
unoptimized
/>
) : (
<span className="font-mono text-lg text-foreground-neutral-weak">Any AI agent</span>
)}
<span
className="absolute right-[7px] top-[7px] text-foreground-neutral-weaker opacity-60 transition-opacity group-hover:opacity-100"
aria-hidden
>
<Icon className="size-3.5" strokeWidth={1.75} />
</span>
</a>
);
}
96 changes: 96 additions & 0 deletions apps/site/src/app/mcp/_components/capability-cards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { LucideIcon } from "lucide-react";

import { McpPromptBubble } from "./mcp-bubble";

const capabilityIconClass = "size-6 shrink-0 text-foreground-ppg";
const capabilityCardClass =
"relative flex w-full flex-col overflow-hidden rounded-[12px] border border-stroke-neutral bg-[linear-gradient(180deg,var(--color-background-default)_0%,var(--color-background-ppg)_262.5%)] shadow-[0_1px_2px_rgba(0,0,0,0.04)]";
const capabilityHeaderClass = "flex items-center gap-4";
const capabilityDescriptionClass = "max-w-full text-[16px] leading-6 text-foreground-neutral-weak";

function CapabilityCardContent({
icon: Icon,
title,
description,
}: {
icon: LucideIcon;
title: string;
description: string;
}) {
return (
<div className="flex flex-col gap-4 p-4">
<div className={capabilityHeaderClass}>
<div className="flex size-12 shrink-0 items-center justify-center rounded-[6px] bg-background-ppg">
<Icon className={capabilityIconClass} strokeWidth={1.75} aria-hidden />
</div>
<h4 className="font-sans-display text-[20px] leading-7 font-extrabold text-foreground-neutral">
{title}
</h4>
</div>
<p className={capabilityDescriptionClass}>{description}</p>
</div>
);
}

export function MobileCapabilityCard({
icon: Icon,
title,
description,
prompt,
mobileTall,
}: {
icon: LucideIcon;
title: string;
description: string;
prompt: string;
mobileTall: boolean;
}) {
return <CapabilityCard icon={Icon} title={title} description={description} prompt={prompt} mobileTall={mobileTall} size="compact" />;
}

export function CapabilityCard({
icon: Icon,
title,
description,
prompt,
mobileTall = false,
size,
}: {
icon: LucideIcon;
title: string;
description: string;
prompt: string;
mobileTall?: boolean;
size: "wide" | "compact";
}) {
const isWide = size === "wide";
const cardHeightClass = mobileTall
? "h-[227px] xl:h-[179px]"
: isWide
? "h-[203px] xl:h-[179px]"
: "h-[203px]";
const promptInsetClass = isWide
? "bottom-[14px] left-[14px] right-[14px] xl:bottom-[15px] xl:left-[16px] xl:right-[25px]"
: "bottom-[14px] left-[14px] right-[14px] xl:left-[16px] xl:right-[27px]";
const contentPadClass = mobileTall
? "pb-[104px] xl:pb-[60px]"
: isWide
? "pb-[80px] xl:pb-[60px]"
: "pb-[80px] xl:pb-[66px]";
const promptVariant = mobileTall ? "mobile-tall" : isWide ? "wide" : "compact";

return (
<div
className={`relative w-full ${cardHeightClass} ${isWide ? "xl:col-span-3" : "sm:col-span-1 xl:col-span-2"}`}
>
<div className={`${capabilityCardClass} h-full`}>
<div className={contentPadClass}>
<CapabilityCardContent icon={Icon} title={title} description={description} />
</div>
</div>
<div className={`absolute ${promptInsetClass}`}>
<McpPromptBubble variant={promptVariant}>{prompt}</McpPromptBubble>
</div>
</div>
);
}
49 changes: 49 additions & 0 deletions apps/site/src/app/mcp/_components/mcp-agents-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Button } from "@prisma/eclipse";
import type { LucideIcon } from "lucide-react";

import { AgentCard } from "./agent-card";

export type McpAgent = {
logo: string | null;
alt: string;
icon: LucideIcon;
href: string;
};

export function McpAgentsSection({
docsHref,
agents,
}: {
docsHref: string;
agents: readonly McpAgent[];
}) {
return (
<section className="px-4 py-12 md:px-0">
<div className="mx-auto flex max-w-[790px] flex-col items-center gap-12 text-center">
<div className="flex max-w-[768px] flex-col items-center gap-4">
<h2 className="font-sans-display stretch-display font-black text-foreground-neutral text-3xl">
Works with your AI agent
</h2>
<p className="text-base leading-6 text-foreground-neutral-weak">
Works with any AI agent, whether you prefer to use a remote or a local server,
we&apos;ve got you.
</p>
</div>

<div className="grid w-full max-w-[368px] grid-cols-2 gap-4 md:max-w-[790px] md:grid-cols-4 md:gap-8">
{agents.map(({ logo, alt, icon, href }) => (
<AgentCard key={alt} logo={logo} alt={alt} icon={icon} href={href} />
))}
</div>

<Button
href={docsHref}
variant={"link"}
className="text-sm font-semibold text-foreground-ppg underline"
>
Want to see your tool listed?
</Button>
</div>
</section>
);
}
150 changes: 150 additions & 0 deletions apps/site/src/app/mcp/_components/mcp-bubble.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { useId, type ReactNode } from "react";

const fillTitle = "#030712";
const fillTeal = "#042F2E";
const strokeTeal = "#2DD4BF";
const promptFill = "#0D3A38";
const promptStroke = "#16A394";
const bubbleShadow = "shadow-[0_12px_36px_rgba(0,0,0,0.2)]";

export type McpBubbleVariant =
| "hero-desktop-title"
| "hero-desktop-description"
| "hero-mobile-title"
| "hero-mobile-description";

export type McpPromptBubbleVariant = "mobile" | "mobile-tall" | "wide" | "compact";

type BubbleConfig = {
shell: string;
fill: string;
stroke: string;
monoPrompt: boolean;
tailSide: "left" | "right";
};

const config: Record<McpBubbleVariant, BubbleConfig> = {
"hero-desktop-title": {
fill: fillTitle,
stroke: strokeTeal,
monoPrompt: false,
tailSide: "right",
shell: `min-h-[120px] items-center justify-center rounded-xl px-5 py-5 sm:min-h-[96px] sm:px-6 sm:py-4 lg:min-h-[72px] lg:px-6 ${bubbleShadow}`,
},
"hero-desktop-description": {
fill: fillTeal,
stroke: strokeTeal,
monoPrompt: false,
tailSide: "left",
shell: `min-h-[108px] items-center rounded-xl px-4 py-4 sm:min-h-[92px] sm:px-5 sm:py-3.5 lg:min-h-[78px] lg:px-6 lg:py-3 ${bubbleShadow}`,
},
"hero-mobile-title": {
fill: fillTitle,
stroke: strokeTeal,
monoPrompt: false,
tailSide: "right",
shell: `min-h-[120px] items-center justify-center rounded-xl px-5 py-5 sm:min-h-[128px] sm:px-6 ${bubbleShadow}`,
},
"hero-mobile-description": {
fill: fillTeal,
stroke: strokeTeal,
monoPrompt: false,
tailSide: "left",
shell: `min-h-[108px] items-center rounded-xl px-4 py-4 sm:min-h-[112px] sm:px-5 ${bubbleShadow}`,
},
};

const promptConfig: Record<McpPromptBubbleVariant, string> = {
mobile: "min-h-[50px] px-4 py-[2px]",
"mobile-tall": "min-h-[74px] px-4 py-[9px] xl:min-h-[45px] xl:py-[2px]",
wide: "min-h-[50px] px-4 py-[2px] xl:min-h-[45px]",
compact: "min-h-[50px] px-4 py-[2px]",
};

const promptTextClass =
"inline-block w-full break-words text-pretty font-mono text-[14px] font-normal leading-5 text-[#99F6E4]";

function BubbleTail({
fill,
stroke,
side,
}: {
fill: string;
stroke: string;
side: "left" | "right";
}) {
const positionClass =
side === "left" ? "bottom-[-2px] left-[-10.5px]" : "bottom-[-2px] right-[-10.5px] scale-x-[-1]";
const clipPathId = useId();

return (
<svg
aria-hidden
className={`pointer-events-none absolute ${positionClass}`}
width="27"
height="19"
viewBox="0 0 27 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath={`url(#${clipPathId})`}>
<path
d="M26.6301 0H9.91007V0.07C9.61007 2.18 8.84007 6.95 7.40007 9.44C5.95007 11.95 2.86007 14.49 0.850074 15.98C0.220074 16.44 0.480074 17.47 1.26007 17.52C4.86007 17.75 8.44007 17.42 11.7901 16.52C12.9101 16.22 13.9901 15.84 15.0501 15.42C17.0001 16.78 19.3601 17.59 21.9201 17.59H26.6401V0H26.6301Z"
fill={fill}
/>
<path
d="M9.91007 0V0.07C9.61007 2.18 8.84007 6.95 7.40007 9.44C5.95007 11.95 2.86007 14.49 0.850074 15.98C0.220074 16.44 0.480074 17.47 1.26007 17.52C4.86007 17.75 8.44007 17.42 11.7901 16.52C12.9101 16.22 13.9901 15.84 15.0501 15.42C17.0001 16.78 19.3601 17.59 21.9201 17.59H26.6401"
stroke={stroke}
strokeMiterlimit="10"
/>
</g>
<defs>
<clipPath id={clipPathId}>
<rect width="26.63" height="18.08" fill="white" />
</clipPath>
</defs>
</svg>
);
}

export function McpPromptBubble({
variant,
children,
}: {
variant: McpPromptBubbleVariant;
children: ReactNode;
}) {
return (
<div className="relative w-full">
<div
className={`relative z-10 flex w-full items-center rounded-[12px] border bg-[#0D3A38] transition-colors duration-300 ${promptConfig[variant]}`}
style={{ borderColor: promptStroke }}
>
<code className={promptTextClass}>{children}</code>
<BubbleTail fill={promptFill} stroke={promptStroke} side="right" />
</div>
</div>
);
}

export function McpBubble({
variant,
children,
}: {
variant: McpBubbleVariant;
children: ReactNode;
}) {
const { shell, fill, stroke, tailSide } = config[variant];

return (
<div className="relative w-full">
<div
className={`relative z-10 flex w-full border border-stroke-ppg ${shell}`}
style={{ backgroundColor: fill }}
>
{children}
<BubbleTail fill={fill} stroke={stroke} side={tailSide} />
</div>
</div>
);
}
Loading
Loading