agent-orchestrator/frontend/src/landing/components/LandingNav.tsx

254 lines
9.1 KiB
TypeScript

"use client";
import { useEffect, useState, useRef } from "react";
import gsap from "gsap";
import ScrollTrigger from "gsap/ScrollTrigger";
import { useGSAP } from "@gsap/react";
if (typeof window !== "undefined") {
gsap.registerPlugin(ScrollTrigger, useGSAP);
}
function DownloadIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M12 3v12" />
<path d="m7 10 5 5 5-5" />
<path d="M5 21h14" />
</svg>
);
}
function MenuIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M4 6h16" />
<path d="M4 12h16" />
<path d="M4 18h16" />
</svg>
);
}
function CloseIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
function XSocialIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.9 2.25h3.24l-7.08 8.09 8.33 11.41h-6.52l-5.11-6.91-5.84 6.91H2.66l7.57-8.67L2.25 2.25h6.69l4.62 6.3 5.34-6.3Zm-1.14 17.5h1.8L7.96 4.14H6.03l11.73 15.61Z" />
</svg>
);
}
function DiscordIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M20.32 4.37A19.8 19.8 0 0 0 15.36 2.8a13.7 13.7 0 0 0-.64 1.32 18.4 18.4 0 0 0-5.44 0 13.7 13.7 0 0 0-.64-1.32 19.7 19.7 0 0 0-4.96 1.57C.54 9.04-.32 13.6.1 18.1a19.9 19.9 0 0 0 6.08 3.08c.49-.67.93-1.38 1.3-2.12-.72-.27-1.4-.6-2.05-.98.17-.12.34-.25.5-.38a14.2 14.2 0 0 0 12.14 0c.16.13.33.26.5.38-.65.39-1.34.72-2.06.99.38.74.81 1.45 1.31 2.12a19.9 19.9 0 0 0 6.08-3.08c.5-5.22-.86-9.74-3.58-13.73ZM8.02 15.33c-1.18 0-2.15-1.08-2.15-2.41 0-1.34.95-2.42 2.15-2.42 1.2 0 2.17 1.09 2.15 2.42 0 1.33-.96 2.41-2.15 2.41Zm7.96 0c-1.18 0-2.15-1.08-2.15-2.41 0-1.34.95-2.42 2.15-2.42 1.2 0 2.17 1.09 2.15 2.42 0 1.33-.95 2.41-2.15 2.41Z" />
</svg>
);
}
const socials = [
{
label: "Discord",
href: "https://discord.com/invite/UZv7JjxbwG",
icon: DiscordIcon,
},
{
label: "X",
href: "https://twitter.com/aoagents",
icon: XSocialIcon,
},
];
const navLinks = [
{ label: "Demo", href: "#see-it" },
{ label: "Features", href: "#features" },
{ label: "Docs", href: "/docs" },
];
function getPlatformLabel() {
if (typeof navigator === "undefined") return "Install AO";
const platform = `${navigator.platform} ${navigator.userAgent}`.toLowerCase();
if (platform.includes("mac")) return "Install for macOS";
if (platform.includes("win")) return "Install for Windows";
if (platform.includes("linux") || platform.includes("x11")) return "Install for Linux";
return "Install AO";
}
export function LandingNav() {
const [open, setOpen] = useState(false);
const [installLabel, setInstallLabel] = useState(getPlatformLabel);
const navRef = useRef<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setInstallLabel(getPlatformLabel());
}, []);
useGSAP(() => {
// Shrink + hide-on-scroll only on desktop (>=768px). On mobile/tablet the
// nav stays in its normal, full-size state.
const mm = gsap.matchMedia();
mm.add("(min-width: 768px)", () => {
const trigger = ScrollTrigger.create({
start: "top -50",
end: 99999,
toggleClass: { className: "nav-scrolled", targets: navRef.current },
onUpdate: (self) => {
if (self.direction === 1) {
gsap.to(innerRef.current, {
yPercent: -100,
opacity: 0,
duration: 0.4,
ease: "power3.inOut",
});
} else {
gsap.to(innerRef.current, {
yPercent: 0,
opacity: 1,
duration: 0.4,
ease: "power3.out",
});
}
},
});
return () => {
trigger.kill();
// Clear any inline transform/class left from desktop state.
navRef.current?.classList.remove("nav-scrolled");
if (innerRef.current) gsap.set(innerRef.current, { clearProps: "transform,opacity" });
};
});
return () => mm.revert();
}, []);
return (
<header data-testid="site-nav" ref={navRef} className="pointer-events-auto fixed inset-x-0 top-0 z-40 pt-4 px-4 transition-all duration-500 ease-out flex justify-center [&.nav-scrolled]:pt-2">
<div
ref={innerRef}
className="w-full max-w-6xl mx-auto flex h-14 items-center justify-between gap-6 rounded-full border border-[color:var(--border)] bg-[color:var(--bg)]/70 px-6 backdrop-blur-xl shadow-lg transition-all duration-500 ease-out [.nav-scrolled_&]:h-12 [.nav-scrolled_&]:max-w-4xl [.nav-scrolled_&]:bg-[color:var(--bg)]/90"
>
<a
href="/"
data-testid="nav-logo"
className="group inline-flex h-10 shrink-0 items-center gap-3"
>
<img
src="/ao-logo.svg"
alt="Agent Orchestrator"
className="block h-7 w-7 shrink-0 object-contain transition-transform duration-300 group-hover:scale-105"
/>
<span className="hidden text-[15px] font-semibold leading-[1.1] tracking-tight text-[color:var(--fg)] sm:block">
Agent Orchestrator
</span>
</a>
<nav
className="hidden items-center gap-8 md:flex"
aria-label="Primary"
>
{navLinks.map((item) => (
<a
key={item.label}
href={item.href}
className="group/navlink relative text-[13px] font-medium tracking-wide text-[color:var(--fg-muted)] transition-colors duration-200 hover:text-[color:var(--fg)]"
>
{item.label}
<span className="absolute -bottom-1.5 left-0 h-px w-full origin-left scale-x-0 bg-[color:var(--accent)] transition-transform duration-300 ease-out group-hover/navlink:scale-x-100" />
</a>
))}
</nav>
<div className="flex items-center gap-3">
<div className="hidden items-center gap-3 lg:flex">
{socials.map((item) => {
const Icon = item.icon;
return (
<a
key={item.label}
href={item.href}
target="_blank"
rel="noreferrer"
aria-label={item.label}
title={item.label}
className="group/social inline-flex h-8 w-8 items-center justify-center rounded-full text-[color:var(--fg-dim)] transition-[color,background-color] duration-300 ease-out hover:bg-[color:var(--bg-elevated)] hover:text-[color:var(--fg)]"
>
<Icon className="h-4 w-4 transition-transform duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] group-hover/social:scale-110 group-active/social:scale-90" />
</a>
);
})}
</div>
<div className="mx-1 hidden h-4 w-px bg-[color:var(--border)] lg:block" />
<a
href="/docs/installation"
data-testid="nav-cta-btn"
style={{ color: "#000000" }}
className="fluid-press group/cta inline-flex h-9 items-center gap-2 rounded-full bg-[color:var(--accent)] px-5 text-[13px] font-semibold shadow-[0_8px_24px_-14px_var(--accent-glow)] hover:shadow-[0_14px_34px_-12px_var(--accent-glow)] hover:brightness-[1.07] max-[365px]:hidden [.nav-scrolled_&]:h-8 [.nav-scrolled_&]:px-4 [.nav-scrolled_&]:text-[12px]"
>
<DownloadIcon className="h-3.5 w-3.5 transition-transform duration-[450ms] ease-[cubic-bezier(0.16,1,0.3,1)] group-hover/cta:translate-y-0.5" />
<span>{installLabel}</span>
</a>
<button
type="button"
className="inline-flex h-9 w-9 items-center justify-center rounded-full text-[color:var(--fg)] transition-colors hover:bg-[color:var(--bg-elevated)] md:hidden"
onClick={() => setOpen(!open)}
>
{open ? <CloseIcon className="h-5 w-5" /> : <MenuIcon className="h-5 w-5" />}
</button>
</div>
</div>
{open && (
<div className="absolute inset-x-0 top-full mt-4 flex flex-col gap-1 rounded-2xl border border-[color:var(--border)] bg-[color:var(--bg)]/95 p-4 mx-4 backdrop-blur-xl shadow-2xl md:hidden">
<a
href="/docs/installation"
onClick={() => setOpen(false)}
style={{ color: "#000000" }}
className="mb-1 hidden items-center justify-center gap-2 rounded-lg bg-[color:var(--accent)] px-4 py-3 text-[15px] font-semibold max-[365px]:flex"
>
<DownloadIcon className="h-4 w-4" />
<span>{installLabel}</span>
</a>
{navLinks.map((item) => (
<a
key={item.label}
href={item.href}
onClick={() => setOpen(false)}
className="flex items-center rounded-lg px-4 py-3 text-[15px] font-medium text-[color:var(--fg)] transition-colors hover:bg-[color:var(--bg-elevated)]"
>
{item.label}
</a>
))}
<div className="my-2 h-px bg-[color:var(--border)]" />
<div className="flex justify-center gap-6 py-2">
{socials.map((item) => {
const Icon = item.icon;
return (
<a
key={item.label}
href={item.href}
target="_blank"
rel="noreferrer"
className="text-[color:var(--fg-muted)] hover:text-[color:var(--fg)]"
>
<Icon className="h-5 w-5" />
</a>
);
})}
</div>
</div>
)}
</header>
);
}