Files
copilotkit__copilotkit/docs/components/layout/navbar.tsx
Sam Julien 84925930bd feat(nav): redesign "Talk to an Engineer" CTA on both docs surfaces
## Label
"Talk to Our Engineers" → "Talk to an Engineer" everywhere it appears
(button text, aria-labels, mobile drawer entry, source comment).

## Desktop pill (≥1100px)
- Gradient fill (indigo-500/90 → purple-500/90 at rest, full at hover)
- Soft shadow lift on hover
- Shimmer animation: a translucent white stripe slides across via an
  ::after pseudo-element on hover (overflow-hidden + after:translate-x
  transition over 700ms). Replaces the earlier scale-on-hover.
- Breakpoint lowered from 1400px → 1100px so the pill is visible at
  most laptop widths where there's plenty of room

## Compact calendar icon (md → 1099px)
- New second button rendered alongside the pill, visible only when
  the rest of the right cluster is icon-only (768–1099px)
- Same gradient + shimmer treatment in a 36×36 rounded-full button
- Inline calendar SVG (matches the Lucide calendar shape)

## Free Developer Access — shell-docs parity with docs/
- Added as a text link in shell-docs' LEFT_LINKS (mirrors the existing
  docs/ pattern); cloud icon on the right cluster now hands off to it
  at ≥1100px
- Visibility transitions on both surfaces realigned to 1100px so the
  cloud↔text and calendar↔pill flips happen at the same boundary
- whitespace-nowrap on LEFT_LINKS label spans so long labels like
  "Free Developer Access" don't wrap when the nav gets tight

## Mobile drawer
- docs/: add a Talk-to-Engineer button at the top of MobileSidebar
  (was missing entirely). Tracks `talk_to_us_clicked` with
  location: docs_navbar_mobile.
- shell-docs: move the existing Talk-to-Engineer button to the top of
  the drawer column so it's the first thing readers see.
2026-05-14 20:36:44 -05:00

354 lines
13 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
import Link from "fumadocs-core/link";
import { usePathname } from "next/navigation";
import { usePostHog } from "posthog-js/react";
import { DocsLayoutProps } from "fumadocs-ui/layouts/docs";
// Components
import { Logo } from "@/app/logo";
import SearchDialogButton from "@/components/ui/search-button";
import MobileSidebar from "@/components/layout/mobile-sidebar";
// Icons
import RocketIcon from "@/components/ui/icons/rocket";
import ConsoleIcon from "@/components/ui/icons/console";
import CloudIcon from "@/components/ui/icons/cloud";
import GithubIcon from "@/components/ui/icons/github";
import DiscordIcon from "@/components/ui/icons/discord";
import ExternalLinkIcon from "@/components/ui/icons/external-link";
import BurgerMenuIcon from "@/components/ui/icons/burger-menu";
import { BookOpenIcon, ScrollTextIcon } from "lucide-react";
export interface NavbarLink {
href: string;
icon: React.ReactNode;
label?: string;
target?: "_blank" | "_self" | "_parent" | "_top";
showExternalLinkIcon?: boolean;
}
interface NavbarProps {
pageTree: DocsLayoutProps["tree"];
}
export const LEFT_LINKS: NavbarLink[] = [
{
icon: <RocketIcon />,
label: "Documentation",
href: "/",
},
{
icon: <ConsoleIcon />,
label: "API Reference",
href: "/reference",
},
{
icon: <BookOpenIcon size={16} />,
label: "Learn",
href: "/learn",
},
{
icon: <CloudIcon />,
label: "Free Developer Access",
href: "https://dashboard.operations.copilotkit.ai/?utm_source=docs&utm_medium=cta&utm_campaign=intelligence&utm_content=navbar",
target: "_blank",
showExternalLinkIcon: true,
},
];
const RIGHT_LINKS: NavbarLink[] = [
{
icon: <CloudIcon />,
href: "https://dashboard.operations.copilotkit.ai/?utm_source=docs&utm_medium=cta&utm_campaign=intelligence&utm_content=navbar",
target: "_blank",
label: "Free Developer Access",
},
{
icon: <GithubIcon />,
href: "https://github.com/copilotkit/copilotkit",
target: "_blank",
},
{
icon: <DiscordIcon />,
href: "https://discord.gg/6dffbvGU3D",
target: "_blank",
},
];
const Navbar = ({ pageTree }: NavbarProps) => {
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
const [lastDocsPath, setLastDocsPath] = useState<string | null>(null);
const pathname = usePathname();
const posthog = usePostHog();
const handleTalkToEngineersClick = () => {
posthog?.capture("talk_to_us_clicked", { location: "docs_nav" });
window.location.href = "https://copilotkit.ai/talk-to-an-engineer";
};
// Read sessionStorage on client only to avoid hydration mismatch (tab-specific)
useEffect(() => {
setLastDocsPath(sessionStorage.getItem("lastDocsPath"));
}, []);
// Determine active route based on current path
const firstSegment = pathname === "/" ? "/" : `/${pathname.split("/")[1]}`;
const isReferencePage = firstSegment === "/reference";
const isLearnPage = firstSegment === "/learn";
// Reference pages → /reference, Learn pages → /learn, Everything else (root + integrations) → /
const activeRoute = isReferencePage
? "/reference"
: isLearnPage
? "/learn"
: "/";
// Get the appropriate href for Documentation link
const getDocumentationHref = () => {
// If we're on a reference page, try to restore last docs path
if (isReferencePage && lastDocsPath) {
return lastDocsPath;
}
return "/";
};
// Close mobile sidebar when viewport expands beyond mobile breakpoint (md: 768px)
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 768 && isMobileSidebarOpen) {
setIsMobileSidebarOpen(false);
}
};
window.addEventListener("resize", handleResize);
// Check on mount in case viewport is already large
handleResize();
return () => {
window.removeEventListener("resize", handleResize);
};
}, [isMobileSidebarOpen]);
const handleTryForFreeClick = (location: string) => {
posthog?.capture("try_for_free_clicked", { location });
};
const handleToggleTheme = () => {
document.documentElement.classList.toggle("dark");
localStorage.theme = localStorage.theme === "dark" ? "light" : "dark";
};
return (
<nav className="h-[68px] xl:h-[88px] p-1 xl:p-2 relative">
{isMobileSidebarOpen && (
<MobileSidebar
pageTree={pageTree}
setIsOpen={setIsMobileSidebarOpen}
handleToggleTheme={handleToggleTheme}
/>
)}
<div className="flex justify-between items-center w-full h-full">
<div className="flex w-full h-full">
<div
className="flex gap-11 items-center w-full h-full rounded-l-2xl border border-r-0 backdrop-blur-lg border-border"
style={{ backgroundColor: "var(--sidebar)" }}
>
<Logo className="pl-6" />
<ul className="hidden gap-6 items-center h-full md:flex">
{LEFT_LINKS.map((link) => {
// Hide only Free Developer Access at narrow widths
const hideAtNarrow = link.label === "Free Developer Access";
// Hide icons for Documentation and API Reference at very narrow widths
const hideIconAtNarrow =
link.label === "Documentation" ||
link.label === "Learn" ||
link.label === "API Reference";
// Use dynamic href for Documentation link
const href =
link.label === "Documentation"
? getDocumentationHref()
: link.href;
return (
<li
key={link.href}
className={`relative h-full group ${hideAtNarrow ? "[@media(width<1100px)]:hidden" : ""}`}
>
<Link
href={href}
target={link.target}
onClick={
link.label === "Free Developer Access"
? () =>
handleTryForFreeClick("docs_navbar_left")
: undefined
}
className={`h-full ${
activeRoute === link.href ? "opacity-100" : "opacity-50"
} hover:opacity-100 transition-opacity duration-300`}
suppressHydrationWarning={link.target === "_blank"}
>
<span className="flex gap-2 items-center h-full">
<span
className={
hideIconAtNarrow
? "[@media(width<808px)]:hidden"
: ""
}
>
{link.icon}
</span>
<span className="text-sm font-medium whitespace-nowrap">
{link.label}
</span>
{link.showExternalLinkIcon && <ExternalLinkIcon />}
</span>
</Link>
<div
className={`absolute bottom-0 left-0 w-full h-[3px] bg-[#7076D5] transition-opacity duration-300 ${
activeRoute === link.href
? "opacity-100"
: "opacity-0 group-hover:opacity-100"
}`}
/>
</li>
);
})}
</ul>
</div>
<Image
src="/images/navbar/slanted-end-border-dark.svg"
alt="Slanted end border"
width={29}
height={72}
className="hidden -ml-px dark:inline-block shrink-0 h-full w-auto object-cover"
/>
<Image
src="/images/navbar/slanted-end-border-light.svg"
alt="Slanted end border"
width={29}
height={72}
className="-ml-px dark:hidden shrink-0 h-full w-auto object-cover"
/>
</div>
<div className="flex items-center w-max h-full shrink-0 -ml-[7px]">
<Image
src="/images/navbar/slanted-start-border-dark.svg"
alt="Slanted start border"
width={29}
height={72}
className="hidden -mr-px dark:inline-block shrink-0 h-full w-auto object-cover"
/>
<Image
src="/images/navbar/slanted-start-border-light.svg"
alt="Slanted start border"
width={29}
height={72}
className="-mr-px dark:hidden shrink-0 h-full w-auto object-cover"
/>
<div
className="flex gap-1 items-center pr-2 w-max h-full rounded-r-2xl border border-l-0 backdrop-blur-lg md:pr-4 shrink-0 border-border"
style={{ backgroundColor: "var(--sidebar)" }}
>
{/* Talk to an Engineer — text pill at ≥1100px, compact
* calendar icon at md-to-1099px (when the rest of the
* right cluster is icon-only). Both share gradient +
* shimmer treatment. */}
<button
type="button"
onClick={handleTalkToEngineersClick}
className="hidden [@media(width>=1100px)]:flex items-center h-9 px-4 mr-2 text-sm font-medium rounded-full bg-gradient-to-r from-indigo-500/90 to-purple-500/90 text-white shadow-sm hover:from-indigo-500 hover:to-purple-500 hover:shadow-md transition-all duration-200 cursor-pointer whitespace-nowrap relative overflow-hidden after:content-[''] after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:-translate-x-full hover:after:translate-x-[100%] after:transition-transform after:duration-700 after:pointer-events-none"
aria-label="Talk to an engineer"
>
Talk to an Engineer
</button>
<button
type="button"
onClick={handleTalkToEngineersClick}
className="hidden md:flex [@media(width>=1100px)]:hidden justify-center items-center w-9 h-9 mr-2 rounded-full bg-gradient-to-r from-indigo-500/90 to-purple-500/90 text-white shadow-sm hover:from-indigo-500 hover:to-purple-500 hover:shadow-md transition-all duration-200 cursor-pointer relative overflow-hidden after:content-[''] after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:-translate-x-full hover:after:translate-x-[100%] after:transition-transform after:duration-700 after:pointer-events-none"
aria-label="Talk to an engineer"
title="Talk to an Engineer"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
</button>
{RIGHT_LINKS.map((link) => {
// Only show Free Developer Access icon at narrow widths (between 768px and 1099px) — matches Talk-to-Engineer pill↔calendar-icon transition.
const isIconOnlyLink = link.label === "Free Developer Access";
return (
<Link
key={link.href}
href={link.href}
target={link.target}
onClick={
link.label === "Free Developer Access"
? () => handleTryForFreeClick("docs_navbar_right")
: undefined
}
className={`${isIconOnlyLink ? "[@media(width>=1100px)]:hidden [@media(width<768px)]:hidden" : "hidden"} justify-center items-center w-11 h-full md:flex`}
title={link.label}
suppressHydrationWarning={link.target === "_blank"}
>
<span className="flex items-center h-full">{link.icon}</span>
</Link>
);
})}
<button
className="hidden justify-center items-center w-11 h-full cursor-pointer md:flex"
onClick={handleToggleTheme}
>
<Image
src="/images/navbar/theme-moon.svg"
alt="Theme icon"
width={20}
height={20}
className="hidden dark:inline-block"
/>
<Image
src="/images/navbar/theme-sun.svg"
alt="Theme icon"
width={20}
height={20}
className="dark:hidden"
/>
</button>
<SearchDialogButton />
<button
className="flex justify-center items-center w-11 h-full cursor-pointer md:hidden"
onClick={() => setIsMobileSidebarOpen(true)}
>
<BurgerMenuIcon />
</button>
</div>
</div>
</div>
</nav>
);
};
export default Navbar;