chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { Tabs, Tab } from "../docs-tabs";
|
||||
|
||||
interface IframeSwitcherProps {
|
||||
id?: string;
|
||||
exampleUrl: string;
|
||||
codeUrl: string;
|
||||
exampleLabel?: string;
|
||||
codeLabel?: string;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
export function IframeSwitcher({
|
||||
id,
|
||||
exampleUrl,
|
||||
codeUrl,
|
||||
exampleLabel = "Demo",
|
||||
codeLabel = "Code",
|
||||
height = "600px",
|
||||
}: IframeSwitcherProps) {
|
||||
return (
|
||||
<div id={id}>
|
||||
<Tabs items={[exampleLabel, codeLabel]}>
|
||||
<Tab value={exampleLabel}>
|
||||
<iframe
|
||||
src={exampleUrl}
|
||||
className="shell-docs-radius-surface w-full border-0 bg-[var(--bg-surface)]"
|
||||
style={{ height }}
|
||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab value={codeLabel}>
|
||||
<iframe
|
||||
src={codeUrl}
|
||||
className="shell-docs-radius-surface w-full border-0 bg-[var(--bg-surface)]"
|
||||
style={{ height }}
|
||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { IframeSwitcher } from "./iframe-switcher";
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { FrameworkOverview } from "../framework-overview";
|
||||
import type { FrameworkOverviewData } from "@/data/frameworks/types";
|
||||
|
||||
const overviewData: FrameworkOverviewData = {
|
||||
slug: "langgraph-python",
|
||||
frameworkName: "LangChain",
|
||||
iconKey: "langgraph",
|
||||
header: "Bring your LangChain agents to your users",
|
||||
subheader: "Build rich, interactive, agent-powered applications.",
|
||||
guideLink: "/langgraph-python/quickstart",
|
||||
initCommand: "npx copilotkit@latest init",
|
||||
featuresLink: "/langgraph-python",
|
||||
supportedFeatures: [],
|
||||
liveDemos: [],
|
||||
};
|
||||
|
||||
describe("FrameworkOverview", () => {
|
||||
it("renders the primary quickstart CTA and optional agent CLI setup action", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<FrameworkOverview
|
||||
data={overviewData}
|
||||
currentFramework="langgraph-python"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain("Quickstart");
|
||||
expect(markup).toContain("Start using agents");
|
||||
expect(markup).toContain('aria-controls="hero-cli-commands"');
|
||||
expect(markup).toContain("border-[var(--accent)]");
|
||||
expect(markup).toContain("bg-[var(--accent)]");
|
||||
expect(markup).toContain("shell-docs-primary-cta");
|
||||
expect(markup).toContain("text-[var(--primary-foreground)]");
|
||||
});
|
||||
|
||||
it("renders the framework identity icon in accent purple", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<FrameworkOverview
|
||||
data={overviewData}
|
||||
currentFramework="langgraph-python"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain(
|
||||
"shell-docs-radius-icon flex h-10 w-10 items-center justify-center border border-[var(--accent)] bg-[var(--accent-dim)] text-[var(--accent)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not add top padding before the framework hero", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<FrameworkOverview
|
||||
data={overviewData}
|
||||
currentFramework="langgraph-python"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain('class="pb-8 sm:pb-12"');
|
||||
expect(markup).not.toContain("pt-2 sm:pt-4");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,463 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowRight, Copy, Check, ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { customIcons } from "@/components/icons";
|
||||
import type { IconKey } from "@/components/icons";
|
||||
import {
|
||||
HeroStartActions,
|
||||
QuickstartLinkButton,
|
||||
} from "@/components/hero-start-commands";
|
||||
import { OpsPlatformCTA } from "@/components/react/ops-platform-cta";
|
||||
import type {
|
||||
FrameworkOverviewData,
|
||||
OpsPlatformCTAData,
|
||||
} from "@/data/frameworks/types";
|
||||
|
||||
export interface FrameworkOverviewProps {
|
||||
data: FrameworkOverviewData;
|
||||
/**
|
||||
* The framework slug from the current URL (e.g. "langgraph-typescript").
|
||||
* Used to rewrite the data record's links so they stay within the user's
|
||||
* selected variant — without this, langgraph-typescript users clicking
|
||||
* "Quickstart" land on langgraph-python's quickstart via SLUG_RENAMES,
|
||||
* because the data record's `guideLink` embeds the primary variant's slug.
|
||||
*/
|
||||
currentFramework: string;
|
||||
/**
|
||||
* Optional slot rendered between the supported-features section and the
|
||||
* architecture section. When supplied, this takes precedence over `data.cta`
|
||||
* (which is the structured fallback). Routes that pre-render
|
||||
* `after-features.mdx` should pass the compiled MDX here.
|
||||
*/
|
||||
afterFeatures?: ReactNode;
|
||||
/**
|
||||
* Optional override for the framework icon. Takes precedence over the
|
||||
* `iconKey` lookup in `data.iconKey`. Used by the MDX adapter
|
||||
* (`MdxFrameworkOverview`) so authored `index.mdx` files can pass a
|
||||
* concrete `<XIcon />` JSX node instead of having to use a registered
|
||||
* iconKey. When supplied, `data.iconKey` is ignored.
|
||||
*/
|
||||
iconOverride?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap the framework slug embedded in a URL for the user's currently selected
|
||||
* variant. Applied to both in-app internal paths and feature-viewer external
|
||||
* URLs (which encode the framework slug as a path segment).
|
||||
*/
|
||||
function rewriteHref(href: string, fromSlug: string, toSlug: string): string {
|
||||
if (!fromSlug || fromSlug === toSlug) return href;
|
||||
if (href === `/${fromSlug}`) return `/${toSlug}`;
|
||||
if (href.startsWith(`/${fromSlug}/`)) {
|
||||
return `/${toSlug}${href.slice(fromSlug.length + 1)}`;
|
||||
}
|
||||
const featureViewerNeedle = `feature-viewer.copilotkit.ai/${fromSlug}/`;
|
||||
if (href.includes(featureViewerNeedle)) {
|
||||
return href.replace(
|
||||
featureViewerNeedle,
|
||||
`feature-viewer.copilotkit.ai/${toSlug}/`,
|
||||
);
|
||||
}
|
||||
return href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Track A's `OpsPlatformCTAData.variant` ("card" | "banner") onto the
|
||||
* variants supported by shell-docs's `OpsPlatformCTA` ("tile" | "inline" |
|
||||
* "card" | "info"). "banner" => "inline" preserves the full-width prominent
|
||||
* CTA intent without introducing a new variant.
|
||||
*/
|
||||
function ctaVariantFor(data: OpsPlatformCTAData): "card" | "inline" {
|
||||
return data.variant === "banner" ? "inline" : "card";
|
||||
}
|
||||
|
||||
/**
|
||||
* Section eyebrow — small sans-serif label with a hairline rule. Dropped
|
||||
* the prior monospace + wide-tracking treatment because it read as
|
||||
* editorial pastiche on a developer-docs surface.
|
||||
*/
|
||||
function SectionEyebrow({ label }: { label: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)] whitespace-nowrap">
|
||||
{label}
|
||||
</span>
|
||||
<div className="flex-1 h-px bg-[var(--border)]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Docs framework slug -> the `copilotkit` CLI's own `--framework` value. The
|
||||
// CLI uses different identifiers than our docs slugs, so the create command on
|
||||
// a framework page only pre-selects a framework when there's a verified 1:1
|
||||
// template match (values confirmed against the CLI's AGENT_FRAMEWORKS enum).
|
||||
// Slugs intentionally omitted fall back to a bare `npx copilotkit create`:
|
||||
// - crewai-crews: the CLI ships "CrewAI Flows" (`flows`), not Crews — different
|
||||
// - langgraph-fastapi, claude-sdk-*, langroid, ms-agent-harness-dotnet,
|
||||
// spring-ai, agent-spec, deepagents: no matching CLI template
|
||||
const DOCS_SLUG_TO_CLI_FRAMEWORK: Record<string, string> = {
|
||||
"langgraph-python": "langgraph-py",
|
||||
"langgraph-typescript": "langgraph-js",
|
||||
"google-adk": "adk",
|
||||
strands: "aws-strands-py",
|
||||
"ms-agent-dotnet": "microsoft-agent-framework-dotnet",
|
||||
"ms-agent-python": "microsoft-agent-framework-py",
|
||||
mastra: "mastra",
|
||||
"pydantic-ai": "pydantic-ai",
|
||||
llamaindex: "llamaindex",
|
||||
agno: "agno",
|
||||
ag2: "ag2",
|
||||
};
|
||||
|
||||
export function FrameworkOverview({
|
||||
data,
|
||||
currentFramework,
|
||||
afterFeatures,
|
||||
iconOverride,
|
||||
}: FrameworkOverviewProps) {
|
||||
const {
|
||||
frameworkName,
|
||||
iconKey,
|
||||
header,
|
||||
subheader,
|
||||
guideLink: rawGuideLink,
|
||||
initCommand,
|
||||
supportedFeatures = [],
|
||||
architectureImage,
|
||||
architectureVideo,
|
||||
liveDemos = [],
|
||||
cta,
|
||||
} = data;
|
||||
|
||||
// Derive the primary variant's slug from the data record's own links —
|
||||
// typically the path segment after the leading `/` of `guideLink`
|
||||
// (e.g. "/langgraph/quickstart" → "langgraph"). This is the slug we
|
||||
// rewrite *away from* so that variant users land on their own variant's
|
||||
// sub-pages.
|
||||
const fromSlug = rawGuideLink.split("/")[1] ?? "";
|
||||
const link = (href: string) => rewriteHref(href, fromSlug, currentFramework);
|
||||
|
||||
// Frameworks whose init is the generic top-level command get the unified
|
||||
// two-command recommendation (matching the home hero). Frameworks with
|
||||
// bespoke setup (e.g. a2a's `git clone`, ms-agent-dotnet) keep their own
|
||||
// single command chip — those commands aren't interchangeable with the CLI.
|
||||
const isGenericInit = initCommand.trim() === "npx copilotkit@latest init";
|
||||
const createFramework = DOCS_SLUG_TO_CLI_FRAMEWORK[currentFramework];
|
||||
|
||||
const [activeDemo, setActiveDemo] = useState<string>(
|
||||
liveDemos[0]?.type || "saas",
|
||||
);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Look up the icon by key. If the key isn't registered (forward-compat with
|
||||
// string IconKey from Track A), fall back to rendering nothing rather than
|
||||
// crashing — the framework name still appears next to it.
|
||||
const IconComponent = customIcons[iconKey as IconKey];
|
||||
const hasIcon = Boolean(iconOverride || IconComponent);
|
||||
|
||||
const handleCopyCommand = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(initCommand);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
// clipboard.writeText rejects in non-secure contexts (http://),
|
||||
// when the document isn't focused, or when the user has denied
|
||||
// permission. Don't flip the "copied" indicator on failure — the
|
||||
// user would see a checkmark and paste an empty/stale buffer.
|
||||
console.error(
|
||||
"[framework-overview] clipboard write failed; copy button no-op",
|
||||
err,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// If no explicit afterFeatures slot is supplied, render the structured cta
|
||||
// (if any) so data-driven intros still get a CTA without needing MDX.
|
||||
const resolvedAfterFeatures: ReactNode =
|
||||
afterFeatures ??
|
||||
(cta ? (
|
||||
<OpsPlatformCTA
|
||||
variant={ctaVariantFor(cta)}
|
||||
title={cta.title}
|
||||
body={cta.body}
|
||||
ctaLabel={cta.ctaLabel}
|
||||
surface={cta.surface}
|
||||
/>
|
||||
) : null);
|
||||
|
||||
const activeDemoData = liveDemos.find((demo) => demo.type === activeDemo);
|
||||
|
||||
return (
|
||||
<div className="relative pb-24">
|
||||
<div className="relative z-10">
|
||||
{/* =========================================================
|
||||
HERO
|
||||
========================================================= */}
|
||||
<header className="pb-8 sm:pb-12">
|
||||
{/* Framework identity: icon + name in a horizontal lockup. */}
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
{hasIcon && (
|
||||
<div className="shell-docs-radius-icon flex h-10 w-10 items-center justify-center border border-[var(--accent)] bg-[var(--accent-dim)] text-[var(--accent)]">
|
||||
{iconOverride ??
|
||||
(IconComponent ? (
|
||||
<IconComponent className="h-6 w-6" />
|
||||
) : null)}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-base font-semibold tracking-tight text-[var(--text)]">
|
||||
{frameworkName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Headline + supporting copy — tightened from the prior
|
||||
display-scale type. Still left-aligned with balanced wrap. */}
|
||||
<h1 className="text-[1.75rem] sm:text-[2.25rem] md:text-[2.5rem] font-semibold leading-[1.1] tracking-[-0.02em] text-[var(--text)] text-balance max-w-[24ch]">
|
||||
{header}
|
||||
</h1>
|
||||
<p className="mt-4 max-w-[58ch] text-base sm:text-lg text-[var(--text-muted)] leading-[1.55] text-pretty">
|
||||
{subheader}
|
||||
</p>
|
||||
|
||||
{/* Action cluster — the same <HeroStartActions> block as the home
|
||||
hero, with Quickstart primary and the agent CLI setup menu
|
||||
secondary. The quickstart slot is a direct link here because a
|
||||
framework is already selected. Frameworks with bespoke setup
|
||||
(e.g. a2a's `git clone`, ms-agent-dotnet) keep their own
|
||||
copy-command chip because those commands aren't interchangeable
|
||||
with the CLI. */}
|
||||
<div className="mt-7">
|
||||
{isGenericInit ? (
|
||||
<HeroStartActions
|
||||
createFramework={createFramework}
|
||||
quickstart={<QuickstartLinkButton href={link(rawGuideLink)} />}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<QuickstartLinkButton href={link(rawGuideLink)} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyCommand}
|
||||
className="shell-docs-radius-control group inline-flex h-11 w-full cursor-pointer items-center justify-between gap-3 border border-[var(--border)] bg-[var(--bg-surface)] px-4 text-[var(--text)] shadow-[var(--shadow-control)] transition-colors hover:bg-[var(--bg-elevated)] sm:w-auto sm:justify-start"
|
||||
aria-label="Copy install command"
|
||||
>
|
||||
<span className="flex items-center gap-2 text-[13.5px]">
|
||||
<span className="text-[var(--accent)] opacity-70 font-mono">
|
||||
$
|
||||
</span>
|
||||
<span className="font-mono text-[13px] text-[var(--text-secondary)] group-hover:text-[var(--text)]">
|
||||
{initCommand}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-[var(--text-muted)] group-hover:text-[var(--text)]">
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4 text-[var(--accent)]" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* =========================================================
|
||||
SUPPORTED FEATURES — numbered milestone list
|
||||
========================================================= */}
|
||||
{supportedFeatures.length > 0 && (
|
||||
<section className="mb-20 sm:mb-28">
|
||||
<SectionEyebrow label="What you can build" />
|
||||
<div className="mb-12 max-w-[58ch]">
|
||||
<h2 className="text-[2rem] sm:text-[2.5rem] font-semibold tracking-[-0.02em] leading-[1.1] text-[var(--text)]">
|
||||
Build with {frameworkName}
|
||||
</h2>
|
||||
<p className="mt-3 text-[15px] sm:text-base text-[var(--text-muted)] leading-relaxed">
|
||||
The user-facing primitives every {frameworkName} integration
|
||||
ships with — pick the one that fits your product and drop the
|
||||
code in.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-16 sm:gap-24">
|
||||
{supportedFeatures.map((feature) => {
|
||||
const hasMedia = Boolean(feature.videoUrl);
|
||||
return (
|
||||
<article
|
||||
key={feature.title}
|
||||
className="grid lg:grid-cols-12 gap-8 lg:gap-12 items-start"
|
||||
>
|
||||
{/* Left column: title + description + links */}
|
||||
<div className="lg:col-span-5">
|
||||
<h3 className="text-[1.5rem] sm:text-[1.75rem] font-semibold tracking-[-0.015em] leading-[1.15] text-[var(--text)]">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="mt-3 text-[15px] text-[var(--text-muted)] leading-[1.6]">
|
||||
{feature.description}
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-wrap items-center gap-x-5 gap-y-2">
|
||||
<Link
|
||||
href={link(feature.documentationLink)}
|
||||
className="inline-flex items-center gap-1.5 text-[14px] font-medium text-[var(--accent)] hover:text-[var(--accent)] hover:brightness-110 no-underline group"
|
||||
>
|
||||
Read the docs
|
||||
<ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" />
|
||||
</Link>
|
||||
{feature.demoLink && (
|
||||
<Link
|
||||
href={link(feature.demoLink)}
|
||||
className="inline-flex items-center gap-1.5 text-[14px] text-[var(--text-muted)] hover:text-[var(--text)] no-underline transition-colors"
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
Live demo
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right column: video. If no media, the left column
|
||||
spans wider and we leave the right empty (graceful
|
||||
fallback for sparse data records). */}
|
||||
{hasMedia && (
|
||||
<div className="lg:col-span-7">
|
||||
<div className="shell-docs-radius-surface relative overflow-hidden border border-[var(--border)] bg-[var(--bg-surface)] shadow-[var(--shadow-panel)]">
|
||||
<video
|
||||
src={feature.videoUrl}
|
||||
className="w-full block"
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="shell-docs-radius-surface pointer-events-none absolute inset-0 ring-1 ring-inset ring-white/5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* =========================================================
|
||||
AFTER FEATURES (CTA or MDX escape hatch)
|
||||
========================================================= */}
|
||||
{resolvedAfterFeatures && (
|
||||
<section className="mb-20 sm:mb-28">{resolvedAfterFeatures}</section>
|
||||
)}
|
||||
|
||||
{/* =========================================================
|
||||
ARCHITECTURE
|
||||
========================================================= */}
|
||||
{(architectureImage || architectureVideo) && (
|
||||
<section className="mb-20 sm:mb-28">
|
||||
<SectionEyebrow label="How it fits together" />
|
||||
<div className="mb-10 max-w-[58ch]">
|
||||
<h2 className="text-[2rem] sm:text-[2.5rem] font-semibold tracking-[-0.02em] leading-[1.1] text-[var(--text)]">
|
||||
Architecture
|
||||
</h2>
|
||||
<p className="mt-3 text-[15px] sm:text-base text-[var(--text-muted)] leading-relaxed">
|
||||
The shape of a CopilotKit + {frameworkName} application — from
|
||||
your UI down to the agent runtime.
|
||||
</p>
|
||||
</div>
|
||||
<div className="shell-docs-radius-surface overflow-hidden border border-[var(--border)] bg-[var(--bg-surface)] shadow-[var(--shadow-panel)]">
|
||||
{architectureImage && (
|
||||
<Image
|
||||
src={architectureImage}
|
||||
alt={`CopilotKit ${frameworkName} architecture diagram`}
|
||||
height={800}
|
||||
width={1600}
|
||||
className="w-full h-auto block"
|
||||
/>
|
||||
)}
|
||||
{architectureVideo && (
|
||||
<video
|
||||
src={architectureVideo}
|
||||
className="w-full block"
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* =========================================================
|
||||
LIVE DEMOS
|
||||
========================================================= */}
|
||||
{liveDemos.length > 0 && (
|
||||
<section className="mb-20 sm:mb-28">
|
||||
<SectionEyebrow label="Live example" />
|
||||
<div className="mb-8 max-w-[58ch]">
|
||||
<h2 className="text-[2rem] sm:text-[2.5rem] font-semibold tracking-[-0.02em] leading-[1.1] text-[var(--text)]">
|
||||
Run {frameworkName} in your browser
|
||||
</h2>
|
||||
<p className="mt-3 text-[15px] sm:text-base text-[var(--text-muted)] leading-relaxed">
|
||||
Two patterns we see most often — drive a SaaS workflow, or
|
||||
collaborate on a canvas with your agent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Segmented control — flat, single-row, with a moving accent
|
||||
underline. Mirrors the dojo's "view toggle" treatment but
|
||||
in a flatter style that suits a landing page. */}
|
||||
{liveDemos.length > 1 && (
|
||||
<div className="shell-docs-radius-control mb-6 inline-flex items-center gap-1 border border-[var(--border)] bg-[var(--bg-surface)] p-1 shadow-[var(--shadow-control)]">
|
||||
{liveDemos.map((demo) => {
|
||||
const active = activeDemo === demo.type;
|
||||
return (
|
||||
<button
|
||||
key={demo.type}
|
||||
type="button"
|
||||
onClick={() => setActiveDemo(demo.type)}
|
||||
className={`shell-docs-radius-control h-8 px-4 text-[13px] font-medium transition-colors ${
|
||||
active
|
||||
? "bg-[var(--bg-elevated)] text-[var(--text)] shadow-[var(--shadow-control)]"
|
||||
: "bg-transparent text-[var(--text-muted)] hover:text-[var(--text)]"
|
||||
}`}
|
||||
>
|
||||
{demo.title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeDemoData && (
|
||||
<p className="mb-5 text-[14.5px] text-[var(--text-muted)] leading-[1.6] max-w-[68ch]">
|
||||
{activeDemoData.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="shell-docs-radius-surface relative overflow-hidden border border-[var(--border)] bg-[var(--bg-surface)] shadow-[var(--shadow-panel)]">
|
||||
{activeDemoData && (
|
||||
<iframe
|
||||
src={activeDemoData.iframeUrl}
|
||||
className="w-full h-[480px] sm:h-[600px] block"
|
||||
title={`${activeDemoData.title} Demo`}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
aria-hidden
|
||||
className="shell-docs-radius-surface pointer-events-none absolute inset-0 ring-1 ring-inset ring-white/5"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { FrameworkOverview } from "./framework-overview";
|
||||
import type {
|
||||
FrameworkOverviewData,
|
||||
LiveDemo,
|
||||
SupportedFeature,
|
||||
} from "@/data/frameworks/types";
|
||||
|
||||
/**
|
||||
* MDX-friendly adapter for `<FrameworkOverview>`. Accepts the flat props
|
||||
* the v1 docs MDX uses (e.g. `frameworkName`, `frameworkIcon`, `header`,
|
||||
* `supportedFeatures`, ...) and renders the existing data-driven
|
||||
* `FrameworkOverview` component with a synthesized `FrameworkOverviewData`.
|
||||
*
|
||||
* Why this exists: `docs_mode: authored` frameworks ship their own
|
||||
* `integrations/<folder>/index.mdx` ported from v1, which uses flat
|
||||
* props that don't match the typed `data: FrameworkOverviewData` shape
|
||||
* the data-driven path uses. Without this adapter, MDX `<FrameworkOverview>`
|
||||
* would render as the registry shim (empty div + children) and the
|
||||
* ported props would be dropped on the floor.
|
||||
*
|
||||
* The icon is passed through as a JSX node via `iconOverride`, so MDX
|
||||
* files keep using `frameworkIcon={<MastraIcon className="..." />}`
|
||||
* without needing to know about the iconKey registry.
|
||||
*
|
||||
* `currentFramework` is the URL framework slug (e.g. `langgraph-fastapi`)
|
||||
* supplied by the page render site via a per-render override of this
|
||||
* component in the MDX components map. It feeds into
|
||||
* `FrameworkOverview`'s `rewriteHref` link rewriter so internal links
|
||||
* and feature-viewer URLs land on the URL-active variant. Two cases:
|
||||
*
|
||||
* 1. Same-slug case (e.g. `/mastra` rendering `integrations/mastra/index.mdx`,
|
||||
* where the MDX's `guideLink="/mastra/quickstart"` already embeds the
|
||||
* URL slug). `fromSlug === toSlug` → `rewriteHref` short-circuits and
|
||||
* every link passes through unchanged.
|
||||
* 2. Shared-folder case (e.g. `/langgraph-fastapi` rendering
|
||||
* `integrations/langgraph/index.mdx`, where the MDX embeds
|
||||
* `/langgraph/...` and we need to rewrite to `/langgraph-fastapi/...`).
|
||||
* Without this prop the adapter passed an empty `currentFramework`,
|
||||
* so the rewriter stripped `/langgraph/` entirely (toSlug was empty),
|
||||
* producing broken `//` URLs.
|
||||
*
|
||||
* Fallback: when the page render site doesn't override this component
|
||||
* (e.g. an MDX rendered outside the framework-scoped router), we derive
|
||||
* the slug from `guideLink` itself — that makes `fromSlug === toSlug`
|
||||
* so the rewriter no-ops, preserving the historical empty-slug behavior
|
||||
* for the same-slug case.
|
||||
*/
|
||||
export interface MdxFrameworkOverviewProps {
|
||||
frameworkName: string;
|
||||
frameworkIcon?: ReactNode;
|
||||
header: string;
|
||||
subheader?: string;
|
||||
bannerVideo?: string;
|
||||
guideLink?: string;
|
||||
initCommand?: string;
|
||||
featuresLink?: string;
|
||||
supportedFeatures?: SupportedFeature[];
|
||||
architectureImage?: string;
|
||||
architectureVideo?: string;
|
||||
liveDemos?: LiveDemo[];
|
||||
tutorialLink?: string;
|
||||
/**
|
||||
* URL framework slug bound by the per-render override in
|
||||
* `app/[framework]/[[...slug]]/page.tsx` (see header comment). Authored
|
||||
* MDX never sets this directly — the render site injects it via the
|
||||
* components map so the rewriter has the URL-active variant to rewrite
|
||||
* toward.
|
||||
*/
|
||||
currentFramework?: string;
|
||||
}
|
||||
|
||||
export function MdxFrameworkOverview(props: MdxFrameworkOverviewProps) {
|
||||
// Derive the "from" slug embedded in guideLink so the fallback path
|
||||
// (no `currentFramework` from the render site) makes rewriteHref a
|
||||
// no-op. Mirrors `FrameworkOverview`'s own `fromSlug` derivation.
|
||||
const guideLinkSlug = (props.guideLink ?? "").split("/")[1] ?? "";
|
||||
const currentFramework = props.currentFramework ?? guideLinkSlug;
|
||||
const synthData: FrameworkOverviewData = {
|
||||
// slug is only used by FrameworkOverview's link-rewriting logic to
|
||||
// strip a "from-slug" prefix off internal links. We pass an empty
|
||||
// string here because the `data.slug` field is no longer the source
|
||||
// of truth for the rewriter — `FrameworkOverview` derives `fromSlug`
|
||||
// from `guideLink` directly. Leaving this empty avoids a stale
|
||||
// duplicate of the prefix.
|
||||
slug: "",
|
||||
frameworkName: props.frameworkName,
|
||||
// iconKey is ignored because we always pass `iconOverride` below.
|
||||
iconKey: "",
|
||||
header: props.header,
|
||||
subheader: props.subheader ?? "",
|
||||
bannerVideo: props.bannerVideo,
|
||||
guideLink: props.guideLink ?? "",
|
||||
initCommand: props.initCommand ?? "npx copilotkit@latest init",
|
||||
featuresLink: props.featuresLink ?? "",
|
||||
supportedFeatures: props.supportedFeatures ?? [],
|
||||
architectureImage: props.architectureImage,
|
||||
architectureVideo: props.architectureVideo,
|
||||
liveDemos: props.liveDemos ?? [],
|
||||
tutorialLink: props.tutorialLink,
|
||||
};
|
||||
return (
|
||||
<FrameworkOverview
|
||||
data={synthData}
|
||||
currentFramework={currentFramework}
|
||||
iconOverride={props.frameworkIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user