feat: redesign archgw -> plano + website in Next.js (#613)

* feat: redesign archgw -> plano + website

* feat(www): refactor landing page sections, add new diagrams and UI improvements

* feat(www): sections enhanced for clarify & diagrams added

* feat(www): improvements to mobile design, layout of diagrams

* feat(www): clean + typecheck

* feat(www): feedback loop changes

* feat(www): fix type error

* fix lib/utils error

* feat(www): ran biome formatting

* feat(www): graphic changes

* feat(www): web analytics

* fea(www): changes

* feat(www): introduce monorepo

This change brings Turborepo monorepo to independently handle the marketing website, the docs website and any other future use cases for mutli-platform support. They are using internal @katanemo package handlers for the design system and logic.

* fix(www): transpiler failure

* fix(www): tsconfig issue

* fix(www): next.config issue

* feat(docs): hold off on docs

* Delete next.config.ts

* feat(www): content fix

* feat(www): introduce blog

* feat(www): content changes

* Update package-lock.json

* feat: update text

* Update IntroSection.tsx

* feat: Turbopack issue

* fix

* Update IntroSection.tsx

* feat: updated Research page

* refactor(www): text clarity, padding adj.

* format(www)

* fix: add missing lib/ files to git - fixes Vercel GitHub deployment

- Updated .gitignore to properly exclude Python lib/ but include Next.js lib/ directories
- Added packages/ui/src/lib/utils.ts (cn utility function)
- Added apps/www/src/lib/sanity.ts (Sanity client configuration)
- Fixes module resolution errors in Vercel GitHub deployments (case-sensitive filesystem)

* Update .gitignore

* style(www): favicon + metadata

* fix(www): links

* fix(www): add analytics

* fix(www): add

* fix(www): fix links + image

* fix(www): fix links + image

* fix(www): fix links

* fix(www): remove from tools testing.md
This commit is contained in:
Musa 2025-12-18 15:55:15 -08:00 committed by GitHub
parent 48bbc7cce7
commit 0c3efdbef2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
119 changed files with 27142 additions and 266 deletions

View file

@ -0,0 +1,109 @@
import React from "react";
import Link from "next/link";
import Image from "next/image";
const footerLinks = {
product: [
{ label: "Research", href: "/research" },
{ label: "Blog", href: "/blog" },
{ label: "Documentation", href: "https://docs.planoai.dev", external: true },
{ label: "Hugging Face", href: "https://huggingface.co/katanemo", external: true },
],
resources: [
{ label: "GitHub", href: "https://github.com/katanemo/arch", external: true },
{ label: "Discord", href: "https://discord.gg/pGZf2gcwEc", external: true },
{ label: "Get Started", href: "https://docs.planoai.dev/get_started/installation", external: true },
],
};
export function Footer() {
return (
<footer
className="relative overflow-hidden pt-20 px-6 lg:px-[102px] pb-48"
style={{ background: "linear-gradient(to top right, #ffffff, #dcdfff)" }}
>
<div className="max-w-[81rem] mx-auto relative z-10">
{/* Main Grid Layout */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-20">
{/* Left Column - Tagline and Copyright */}
<div className="flex flex-col">
<p className="ext-base sm:text-lg md:text-xl lg:text-[22px] font-sans font-normal tracking-[-1.0px] sm:tracking-[-1.22px]! text-black mb-6 sm:mb-8">
Plano is a powerful agent delivery infrastructure platform that is framework-friendly,
and empowers developers and teams to seamlessly build, deliver, and scale agentic
applications.
</p>
{/* Copyright */}
<div className="mt-auto">
<p className="font-sans text-sm sm:text-base text-black/63 tracking-[-0.6px] sm:tracking-[-0.8px]!">
© Katanemo Labs, Inc. 2025 / Plano by Katanemo Labs, Inc.
</p>
</div>
</div>
{/* Right Column - Navigation Links */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8">
{/* Product Links */}
<div>
<h3 className="font-sans font-normal text-xl sm:text-2xl lg:text-3xl text-black tracking-[-1.2px] sm:tracking-[-1.4px] lg:tracking-[-1.6px]! mb-4 sm:mb-6">
Product
</h3>
<nav className="space-y-3 sm:space-y-4">
{footerLinks.product.map((link) => (
<Link
key={link.href}
href={link.href}
target={link.external ? "_blank" : undefined}
rel={link.external ? "noopener noreferrer" : undefined}
className="block font-sans font-normal text-sm sm:text-base lg:text-lg text-black tracking-[-0.8px] sm:tracking-[-0.9px] lg:tracking-[-1px]! hover:text-[var(--primary)] transition-colors"
>
{link.label}
</Link>
))}
</nav>
</div>
{/* Resources Links */}
<div>
<h3 className="font-sans font-normal text-xl sm:text-2xl lg:text-3xl text-black tracking-[-1.2px] sm:tracking-[-1.4px] lg:tracking-[-1.6px]! mb-4 sm:mb-6">
Resources
</h3>
<nav className="space-y-3 sm:space-y-4">
{footerLinks.resources.map((link) => (
<Link
key={link.href}
href={link.href}
target={link.external ? "_blank" : undefined}
rel={link.external ? "noopener noreferrer" : undefined}
className="block font-sans font-normal text-sm sm:text-base lg:text-lg text-black tracking-[-0.8px] sm:tracking-[-0.9px] lg:tracking-[-1px]! hover:text-[var(--primary)] transition-colors"
>
{link.label}
</Link>
))}
</nav>
</div>
</div>
</div>
</div>
{/* Half-Cut Plano Logo Background */}
<div className="absolute bottom-0 left-0 right-0 overflow-hidden pointer-events-none">
<div className="max-w-[81rem] mx-auto px-6 lg:px-[1px]">
<div className="relative w-full flex justify-start">
<Image
src="/LogoOutline.svg"
alt="Plano Logo"
width={1800}
height={200}
className="w-150 h-auto opacity-30 select-none"
style={{
transform: "translateY(0%)", // Push logo down more while showing top part
transformOrigin: "center bottom",
}}
/>
</div>
</div>
</div>
</footer>
);
}

View file

@ -0,0 +1,17 @@
import React from "react";
import Image from "next/image";
export function Logo() {
return (
<div className="flex items-center">
{/* LogoMarkSquare SVG */}
<Image
src="/Logomark.svg"
alt="Plano Logo"
width={90}
height={20}
className="flex-shrink-0"
/>
</div>
);
}

View file

@ -0,0 +1,289 @@
"use client";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { Logo } from "./Logo";
import { Button } from "./ui/button";
import { cn } from "../lib/utils";
import { motion, AnimatePresence } from "framer-motion";
import { X, Menu } from "lucide-react";
const navItems = [
{ href: "https://docs.planoai.dev/get_started/quickstart", label: "start locally" },
{ href: "https://docs.planoai.dev", label: "docs" },
{ href: "/research", label: "research" },
{ href: "/blog", label: "blog" },
];
export function Navbar() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isDarkBackground, setIsDarkBackground] = useState(false);
// Detect background color behind dropdown menu
useEffect(() => {
if (!isMenuOpen) {
setIsDarkBackground(false);
return;
}
const detectBackground = () => {
// Small delay to ensure DOM is ready
setTimeout(() => {
const nav = document.querySelector("nav");
if (!nav) return;
const navRect = nav.getBoundingClientRect();
const dropdownBottom = navRect.bottom;
const checkY = dropdownBottom + 20; // Just below the dropdown
// First, try to find section elements directly
const main = document.querySelector("main");
if (main) {
const sections = main.querySelectorAll("section");
let foundDarkSection = false;
sections.forEach((section) => {
const rect = section.getBoundingClientRect();
// Check if this section is visible below the navbar
if (rect.top <= checkY && rect.bottom > checkY) {
// Check for dark background classes
const classList = Array.from(section.classList);
const hasDarkBg = classList.some(
(cls) =>
cls.includes("bg-[#1a1a1a]") ||
cls.includes("bg-black") ||
cls.includes("bg-gray-900") ||
cls.includes("bg-neutral-900") ||
cls.includes("dark"),
);
if (hasDarkBg) {
foundDarkSection = true;
setIsDarkBackground(true);
return;
}
// Also check computed background
const computed = window.getComputedStyle(section);
const bg = computed.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
const rgbMatch = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (rgbMatch) {
const r = parseInt(rgbMatch[1]);
const g = parseInt(rgbMatch[2]);
const b = parseInt(rgbMatch[3]);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
setIsDarkBackground(luminance < 0.5);
foundDarkSection = true;
return;
}
}
}
});
if (foundDarkSection) return;
}
// Fallback: Check element at point
const centerX = window.innerWidth / 2;
const elementBelow = document.elementFromPoint(centerX, checkY);
if (elementBelow) {
let current: HTMLElement | null = elementBelow as HTMLElement;
let backgroundColor = "";
// Walk up the DOM tree
let levels = 0;
while (
current &&
!backgroundColor &&
current !== document.body &&
levels < 15
) {
const computed = window.getComputedStyle(current);
const bg = computed.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
const rgbaMatch = bg.match(
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/,
);
if (rgbaMatch) {
const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1;
if (alpha > 0.1) {
backgroundColor = bg;
break;
}
} else {
backgroundColor = bg;
break;
}
}
current = current.parentElement;
levels++;
}
if (!backgroundColor) {
const bodyBg = window.getComputedStyle(
document.body,
).backgroundColor;
backgroundColor = bodyBg;
}
if (backgroundColor) {
const rgbMatch = backgroundColor.match(
/rgba?\((\d+),\s*(\d+),\s*(\d+)/,
);
if (rgbMatch) {
const r = parseInt(rgbMatch[1]);
const g = parseInt(rgbMatch[2]);
const b = parseInt(rgbMatch[3]);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
setIsDarkBackground(luminance < 0.5);
} else {
const darkColors = [
"black",
"#000",
"#000000",
"rgb(0,0,0)",
"rgba(0,0,0",
"#1a1a1a",
];
const isDark = darkColors.some((color) =>
backgroundColor.toLowerCase().includes(color.toLowerCase()),
);
setIsDarkBackground(isDark);
}
}
}
}, 100);
};
// Detect on open and on scroll
detectBackground();
const scrollHandler = () => detectBackground();
const resizeHandler = () => detectBackground();
window.addEventListener("scroll", scrollHandler, { passive: true });
window.addEventListener("resize", resizeHandler);
return () => {
window.removeEventListener("scroll", scrollHandler);
window.removeEventListener("resize", resizeHandler);
};
}, [isMenuOpen]);
// Close menu when route changes
const handleLinkClick = () => {
setIsMenuOpen(false);
};
return (
<nav className="relative z-50 bg-gradient-to-b from-transparent to-white/5 backdrop-blur border-b border-neutral-200/5">
<div className="max-w-[85rem] mx-auto px-6 lg:px-8">
<div className="flex items-center justify-between h-20">
{/* Logo */}
<Link href="/" className="flex items-center">
<Logo />
</Link>
{/* Navigation Links and CTA - Far Right */}
<div className="hidden md:flex items-center justify-end gap-8">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
"text-lg font-medium text-[var(--muted)]",
"hover:text-[var(--primary)] transition-colors",
"font-mono tracking-tighter",
)}
>
{item.label}
</Link>
))}
</div>
{/* Mobile Menu Button */}
<div className="md:hidden">
<button
onClick={(e) => {
e.stopPropagation();
setIsMenuOpen(!isMenuOpen);
}}
className="p-2 rounded-md text-[var(--muted)] hover:text-[var(--primary)] transition-colors"
aria-label="Toggle menu"
aria-expanded={isMenuOpen}
>
<AnimatePresence mode="wait" initial={false}>
{isMenuOpen ? (
<motion.div
key="close"
initial={{ opacity: 0, rotate: -90 }}
animate={{ opacity: 1, rotate: 0 }}
exit={{ opacity: 0, rotate: 90 }}
transition={{ duration: 0.2 }}
>
<X className="h-6 w-6" />
</motion.div>
) : (
<motion.div
key="menu"
initial={{ opacity: 0, rotate: 90 }}
animate={{ opacity: 1, rotate: 0 }}
exit={{ opacity: 0, rotate: -90 }}
transition={{ duration: 0.2 }}
>
<Menu className="h-6 w-6" />
</motion.div>
)}
</AnimatePresence>
</button>
</div>
</div>
</div>
{/* Mobile Dropdown Menu - Outside constrained container for full width */}
<AnimatePresence>
{isMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
className="md:hidden overflow-hidden bg-[#7580DF]/70"
>
<div className="max-w-[85rem] mx-auto px-6 lg:px-8 py-3">
<div className="flex flex-col gap-0.5">
{navItems.map((item, index) => (
<motion.div
key={item.href}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.3,
delay: index * 0.05,
ease: [0.16, 1, 0.3, 1],
}}
>
<Link
href={item.href}
onClick={handleLinkClick}
className={cn(
"block px-0 py-1.5 border-b border-dashed transition-colors font-mono tracking-tighter",
"text-sm font-medium",
"text-white",
)}
>
{item.label}
</Link>
</motion.div>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</nav>
);
}

View file

@ -0,0 +1,65 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[7px] font-mono font-medium tracking-[-0.989px] transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-[#7780d9] border-[#4141b2] border-[1.562px] border-solid text-white hover:bg-[#7780d9]/90 text-base leading-[1.102]",
primary:
"bg-[#7780d9] border-[#4141b2] border-[1.562px] border-solid text-white hover:bg-[#7780d9]/90 text-base leading-[1.102]",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-[#edefff] border-[#d1d1d1] border-[1.562px] border-solid text-[#494949] hover:bg-[#edefff]/90 text-base leading-[1.102]",
secondaryDark:
"bg-neutral-600 border-[#d1d1d1]/20 border-[1.562px] border-solid text-white hover:bg-[#1a1a1a]/90 text-base leading-[1.102]",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-5 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-[7px] px-5 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View file

@ -0,0 +1,143 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "../../lib/utils";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};