254 lines
9.1 KiB
TypeScript
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>
|
|
);
|
|
}
|