diff --git a/apps/blog/components.json b/apps/blog/components.json index efa8bd9fad..87bf1a15c6 100644 --- a/apps/blog/components.json +++ b/apps/blog/components.json @@ -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": "" diff --git a/apps/docs/components.json b/apps/docs/components.json index efa8bd9fad..87bf1a15c6 100644 --- a/apps/docs/components.json +++ b/apps/docs/components.json @@ -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": "" diff --git a/apps/eclipse/components.json b/apps/eclipse/components.json index 27a7524b70..d8aed6a2e1 100644 --- a/apps/eclipse/components.json +++ b/apps/eclipse/components.json @@ -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": "" diff --git a/apps/site/components.json b/apps/site/components.json index efa8bd9fad..87bf1a15c6 100644 --- a/apps/site/components.json +++ b/apps/site/components.json @@ -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": "" diff --git a/apps/site/public/mcp/logos/chatgpt.svg b/apps/site/public/mcp/logos/chatgpt.svg new file mode 100644 index 0000000000..685806303d --- /dev/null +++ b/apps/site/public/mcp/logos/chatgpt.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/site/public/mcp/logos/claude-code.svg b/apps/site/public/mcp/logos/claude-code.svg new file mode 100644 index 0000000000..7ef4d2d53e --- /dev/null +++ b/apps/site/public/mcp/logos/claude-code.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/site/public/mcp/logos/cursor.svg b/apps/site/public/mcp/logos/cursor.svg new file mode 100644 index 0000000000..cf81f414a1 --- /dev/null +++ b/apps/site/public/mcp/logos/cursor.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/site/public/mcp/logos/gemini.svg b/apps/site/public/mcp/logos/gemini.svg new file mode 100644 index 0000000000..4d56d9f36e --- /dev/null +++ b/apps/site/public/mcp/logos/gemini.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/site/public/mcp/logos/vscode.svg b/apps/site/public/mcp/logos/vscode.svg new file mode 100644 index 0000000000..d57cde7346 --- /dev/null +++ b/apps/site/public/mcp/logos/vscode.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/site/public/mcp/logos/warp.svg b/apps/site/public/mcp/logos/warp.svg new file mode 100644 index 0000000000..64f4ae5969 --- /dev/null +++ b/apps/site/public/mcp/logos/warp.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/site/public/mcp/logos/windsurf.svg b/apps/site/public/mcp/logos/windsurf.svg new file mode 100644 index 0000000000..4ec64a8a5b --- /dev/null +++ b/apps/site/public/mcp/logos/windsurf.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/site/src/app/global.css b/apps/site/src/app/global.css index f0100f4a23..bb9ed50fc5 100644 --- a/apps/site/src/app/global.css +++ b/apps/site/src/app/global.css @@ -45,6 +45,21 @@ ) } +@keyframes mcp-cursor-blink { + 0%, + 48% { + opacity: 1; + } + 52%, + 100% { + opacity: 0; + } +} + +.mcp-type-cursor { + animation: mcp-cursor-blink 1s step-end infinite; +} + @keyframes glitch-1 { 0% { clip-path: inset(20% 0 60% 0); @@ -86,4 +101,3 @@ clip-path: inset(75% 0 15% 0); } } - diff --git a/apps/site/src/app/mcp/_components/agent-card.tsx b/apps/site/src/app/mcp/_components/agent-card.tsx new file mode 100644 index 0000000000..376a228fed --- /dev/null +++ b/apps/site/src/app/mcp/_components/agent-card.tsx @@ -0,0 +1,45 @@ +import Image from "next/image"; + +export function AgentCard({ + logo, + alt, + icon, + href, +}: { + logo: string | null; + alt: string; + icon: string | null; + href: string; +}) { + return ( + + {logo ? ( + + ) : ( + + Any AI agent + + )} + {icon ? ( + + + + ) : null} + + ); +} diff --git a/apps/site/src/app/mcp/_components/capability-cards.tsx b/apps/site/src/app/mcp/_components/capability-cards.tsx new file mode 100644 index 0000000000..74fd963190 --- /dev/null +++ b/apps/site/src/app/mcp/_components/capability-cards.tsx @@ -0,0 +1,112 @@ +import { McpPromptBubble } from "./mcp-bubble"; + +const capabilityIconClass = "shrink-0 text-[24px] 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-box-low"; +const capabilityHeaderClass = "flex items-center gap-4"; +const capabilityDescriptionClass = + "max-w-full text-[16px] leading-6 text-foreground-neutral-weak"; + +function CapabilityCardContent({ + icon, + title, + description, +}: { + icon: string; + title: string; + description: string; +}) { + return ( +
+
+
+ +
+

+ {title} +

+
+

{description}

+
+ ); +} + +export function MobileCapabilityCard({ + icon, + title, + description, + prompt, + mobileTall, +}: { + icon: string; + title: string; + description: string; + prompt: string; + mobileTall: boolean; +}) { + return ( + + ); +} + +export function CapabilityCard({ + icon, + title, + description, + prompt, + mobileTall = false, + size, +}: { + icon: string; + 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 ( +
+
+
+ +
+
+
+ {prompt} +
+
+ ); +} diff --git a/apps/site/src/app/mcp/_components/mcp-agents-section.tsx b/apps/site/src/app/mcp/_components/mcp-agents-section.tsx new file mode 100644 index 0000000000..9cff01c390 --- /dev/null +++ b/apps/site/src/app/mcp/_components/mcp-agents-section.tsx @@ -0,0 +1,48 @@ +import { Button } from "@prisma/eclipse"; + +import { AgentCard } from "./agent-card"; + +export type McpAgent = { + logo: string | null; + alt: string; + icon: string | null; + href: string; +}; + +export function McpAgentsSection({ + docsHref, + agents, +}: { + docsHref: string; + agents: readonly McpAgent[]; +}) { + return ( +
+
+
+

+ Works with your AI agent +

+

+ Works with any AI agent, whether you prefer to use a remote or a local server, + we've got you. +

+
+ +
+ {agents.map(({ logo, alt, icon, href }) => ( + + ))} +
+ + +
+
+ ); +} diff --git a/apps/site/src/app/mcp/_components/mcp-bubble.tsx b/apps/site/src/app/mcp/_components/mcp-bubble.tsx new file mode 100644 index 0000000000..b95d1770f9 --- /dev/null +++ b/apps/site/src/app/mcp/_components/mcp-bubble.tsx @@ -0,0 +1,124 @@ +import { useId, type ReactNode } from "react"; + +const bubbleShadow = "shadow-box-low dark:shadow-box-high"; + +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; + tailSide: "left" | "right"; +}; + +const config: Record = { + "hero-desktop-title": { + 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} [--mcp-bubble-fill:var(--color-background-default)] dark:[--mcp-bubble-fill:var(--color-background-neutral-weaker)] [--mcp-bubble-stroke:var(--color-stroke-ppg)] [background-color:var(--mcp-bubble-fill)] [border-color:var(--mcp-bubble-stroke)]`, + }, + "hero-desktop-description": { + 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} [--mcp-bubble-fill:var(--color-background-ppg)] [--mcp-bubble-stroke:var(--color-stroke-ppg)] [background-color:var(--mcp-bubble-fill)] [border-color:var(--mcp-bubble-stroke)]`, + }, + "hero-mobile-title": { + tailSide: "right", + shell: `min-h-[120px] items-center justify-center rounded-xl px-5 py-5 sm:min-h-[128px] sm:px-6 ${bubbleShadow} [--mcp-bubble-fill:var(--color-background-default)] dark:[--mcp-bubble-fill:var(--color-background-neutral-weaker)] [--mcp-bubble-stroke:var(--color-stroke-ppg)] [background-color:var(--mcp-bubble-fill)] [border-color:var(--mcp-bubble-stroke)]`, + }, + "hero-mobile-description": { + tailSide: "left", + shell: `min-h-[108px] items-center rounded-xl px-4 py-4 sm:min-h-[112px] sm:px-5 ${bubbleShadow} [--mcp-bubble-fill:var(--color-background-ppg)] [--mcp-bubble-stroke:var(--color-stroke-ppg)] [background-color:var(--mcp-bubble-fill)] [border-color:var(--mcp-bubble-stroke)]`, + }, +}; + +const promptConfig: Record = { + 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-background-ppg-reverse-strong dark:text-foreground-ppg-reverse-weak"; + +function BubbleTail({ side }: { side: "left" | "right" }) { + const positionClass = + side === "left" + ? "bottom-[-2px] left-[-10.5px]" + : "bottom-[-2px] right-[-10.5px] scale-x-[-1]"; + const clipPathId = useId(); + + return ( + + + + + + + + + + + + ); +} + +export function McpPromptBubble({ + variant, + children, +}: { + variant: McpPromptBubbleVariant; + children: ReactNode; +}) { + return ( +
+
+ {children} + +
+
+ ); +} + +export function McpBubble({ + variant, + children, +}: { + variant: McpBubbleVariant; + children: ReactNode; +}) { + const { shell, tailSide } = config[variant]; + + return ( +
+
+ {children} + +
+
+ ); +} diff --git a/apps/site/src/app/mcp/_components/mcp-capabilities-section.tsx b/apps/site/src/app/mcp/_components/mcp-capabilities-section.tsx new file mode 100644 index 0000000000..6caf028030 --- /dev/null +++ b/apps/site/src/app/mcp/_components/mcp-capabilities-section.tsx @@ -0,0 +1,32 @@ +import { CapabilityCard } from "./capability-cards"; + +export type McpCapability = { + icon: string; + title: string; + description: string; + prompt: string; + mobileTall: boolean; +}; + +export function McpCapabilitiesSection({ + capabilities, +}: { + capabilities: readonly McpCapability[]; +}) { + return ( +
+
+

+ What can I do with MCP? +

+ +
+ {capabilities.map((cap, index) => { + const size = index < 2 ? "wide" : "compact"; + return ; + })} +
+
+
+ ); +} diff --git a/apps/site/src/app/mcp/_components/mcp-cta-section.tsx b/apps/site/src/app/mcp/_components/mcp-cta-section.tsx new file mode 100644 index 0000000000..05a3ff011e --- /dev/null +++ b/apps/site/src/app/mcp/_components/mcp-cta-section.tsx @@ -0,0 +1,66 @@ +import { Button } from "@prisma/eclipse"; + +export function McpCtaSection({ docsHref }: { docsHref: string }) { + return ( +
+
+
+
+
+
+
+

+ Start Building with AI +

+

+ Join thousands of developers, and agents, already using Prisma MCP + for faster, more intuitive database workflows. +

+
+ +
+
+ + +
+ +

+ 2-minute setup • Works with all MCP tools +

+
+
+
+
+ ); +} diff --git a/apps/site/src/app/mcp/_components/mcp-hero-section.tsx b/apps/site/src/app/mcp/_components/mcp-hero-section.tsx new file mode 100644 index 0000000000..6b1d33721b --- /dev/null +++ b/apps/site/src/app/mcp/_components/mcp-hero-section.tsx @@ -0,0 +1,118 @@ +import type { ReactNode } from "react"; + +import { Button } from "@prisma/eclipse"; + +import { McpBubble } from "./mcp-bubble"; +import { McpTypeText } from "./mcp-type-text"; + +export type McpHeroFeature = { + icon: string; + line1: string; + line2: string; + mobileText?: ReactNode; +}; + +const heroFeatureIconClass = "text-[24px] text-foreground-ppg"; + +export function McpHeroSection({ + docsHref, + features, +}: { + docsHref: string; + features: readonly McpHeroFeature[]; +}) { + return ( +
+
+
+
+

+ + Prisma MCP Server +

+
+
+ +

+ + + + + + + + _ + +

+
+
+
+ +

+ + + + + + + + _ + +

+
+
+
+
+ + +
+ +
+ {features.map(({ icon, line1, line2, mobileText }) => ( +
+
+ +
+

+ + {mobileText ?? `${line1} ${line2}`} + + + {line1} +
+ {line2} +
+

+
+ ))} +
+
+
+ ); +} diff --git a/apps/site/src/app/mcp/_components/mcp-type-text.tsx b/apps/site/src/app/mcp/_components/mcp-type-text.tsx new file mode 100644 index 0000000000..5345aa6466 --- /dev/null +++ b/apps/site/src/app/mcp/_components/mcp-type-text.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useEffect, useState } from "react"; + +export function McpTypeText({ + text, + speed = 20, + delay = 0, + className, +}: { + text: string; + speed?: number; + delay?: number; + className?: string; +}) { + const [displayText, setDisplayText] = useState(""); + + useEffect(() => { + let index = 0; + let timer: ReturnType | null = null; + let startTimer: ReturnType | null = null; + + setDisplayText(""); + + const typeNext = () => { + index += 1; + setDisplayText(text.slice(0, index)); + + if (index < text.length) { + timer = setTimeout(typeNext, speed); + } + }; + + startTimer = setTimeout(typeNext, delay); + + return () => { + if (timer) clearTimeout(timer); + if (startTimer) clearTimeout(startTimer); + }; + }, [delay, speed, text]); + + return {displayText}; +} diff --git a/apps/site/src/app/mcp/_components/mcp-video-section.tsx b/apps/site/src/app/mcp/_components/mcp-video-section.tsx new file mode 100644 index 0000000000..66cac6368c --- /dev/null +++ b/apps/site/src/app/mcp/_components/mcp-video-section.tsx @@ -0,0 +1,18 @@ +export function McpVideoSection() { + return ( +
+
+
+