mirror of
https://github.com/katanemo/plano.git
synced 2026-06-20 15:28:07 +02:00
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.
This commit is contained in:
parent
151cefd528
commit
0d9147456f
102 changed files with 3876 additions and 52 deletions
90
apps/www/src/components/AsciiDiagram.tsx
Normal file
90
apps/www/src/components/AsciiDiagram.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import React from "react";
|
||||
|
||||
interface AsciiDiagramProps {
|
||||
title?: string;
|
||||
content: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AsciiDiagram: React.FC<AsciiDiagramProps> = ({
|
||||
title,
|
||||
content,
|
||||
className = "",
|
||||
}) => {
|
||||
return (
|
||||
<div className={`max-w-4xl mx-auto mb-8 ${className}`}>
|
||||
{title && (
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-zinc-50 mb-4">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-6 shadow-xl overflow-x-auto">
|
||||
<pre
|
||||
className="relative font-mono text-xs leading-none text-white m-0 whitespace-pre"
|
||||
style={{ fontFamily: "var(--font-jetbrains-mono), monospace" }}
|
||||
>
|
||||
<code>{content}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Programmatic diagram builder for non-coders
|
||||
interface DiagramStep {
|
||||
id: string;
|
||||
label: string;
|
||||
type?: "input" | "inner" | "regular";
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface FlowConnection {
|
||||
from: string;
|
||||
to: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface DiagramConfig {
|
||||
title: string;
|
||||
steps: DiagramStep[];
|
||||
connections: FlowConnection[];
|
||||
}
|
||||
|
||||
// Simple ASCII diagram generator
|
||||
export const createDiagram = (config: DiagramConfig): string => {
|
||||
// This is a simplified version - you can extend this to automatically generate
|
||||
// the ASCII art from the config
|
||||
// For now, return the manually created diagrams
|
||||
return "";
|
||||
};
|
||||
|
||||
// Helper to create boxes
|
||||
export const createBox = (
|
||||
label: string,
|
||||
type: "input" | "inner" | "regular" = "regular",
|
||||
width: number = 20,
|
||||
): string[] => {
|
||||
const padding = Math.max(0, Math.floor((width - label.length) / 2));
|
||||
const spaces = " ".repeat(padding);
|
||||
const remaining = width - label.length - padding;
|
||||
|
||||
let chars;
|
||||
switch (type) {
|
||||
case "input":
|
||||
chars = { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║" };
|
||||
break;
|
||||
case "inner":
|
||||
chars = { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" };
|
||||
break;
|
||||
case "regular":
|
||||
default:
|
||||
chars = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" };
|
||||
}
|
||||
|
||||
return [
|
||||
`${chars.tl}${chars.h.repeat(width)}${chars.tr}`,
|
||||
`${chars.v}${spaces}${label}${" ".repeat(remaining)}${chars.v}`,
|
||||
`${chars.bl}${chars.h.repeat(width)}${chars.br}`,
|
||||
];
|
||||
};
|
||||
34
apps/www/src/components/DiagramBuilder.tsx
Normal file
34
apps/www/src/components/DiagramBuilder.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import React from "react";
|
||||
import { createFlowDiagram, FlowDiagramConfig } from "@/utils/asciiBuilder";
|
||||
import { AsciiDiagram } from "./AsciiDiagram";
|
||||
|
||||
interface DiagramBuilderProps {
|
||||
config: FlowDiagramConfig;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple Diagram Builder Component
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* <DiagramBuilder
|
||||
* config={{
|
||||
* title: "My Process",
|
||||
* width: 60,
|
||||
* steps: [
|
||||
* { label: "Start", type: "regular" },
|
||||
* { label: "Process", type: "inner" },
|
||||
* { label: "End", type: "regular" }
|
||||
* ]
|
||||
* }}
|
||||
* />
|
||||
*/
|
||||
export const DiagramBuilder: React.FC<DiagramBuilderProps> = ({
|
||||
config,
|
||||
title,
|
||||
}) => {
|
||||
const asciiDiagram = createFlowDiagram(config);
|
||||
|
||||
return <AsciiDiagram content={asciiDiagram} title={title} />;
|
||||
};
|
||||
100
apps/www/src/components/Footer.tsx
Normal file
100
apps/www/src/components/Footer.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
const footerLinks = {
|
||||
company: [
|
||||
{ label: "Product", href: "/product" },
|
||||
{ label: "Use Cases", href: "/use-cases" },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{ label: "Plano LLMs", href: "/llms" },
|
||||
],
|
||||
developerResources: [{ label: "Documentation", href: "/docs" }],
|
||||
};
|
||||
|
||||
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="font-sans font-normal text-lg sm:text-xl lg:text-2xl text-black tracking-[-1.2px] sm:tracking-[-1.5px] lg:tracking-[-1.7px]! leading-7 mb-6 sm:mb-8">
|
||||
Plano is the powerful, intelligent platform that empowers teams to
|
||||
seamlessly build, automate, and scale agentic systems with ease.
|
||||
</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">
|
||||
{/* Company 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">
|
||||
Company
|
||||
</h3>
|
||||
<nav className="space-y-3 sm:space-y-4">
|
||||
{footerLinks.company.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
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>
|
||||
|
||||
{/* Developer Resources */}
|
||||
<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">
|
||||
Developer Resources
|
||||
</h3>
|
||||
<nav className="space-y-3 sm:space-y-4">
|
||||
{footerLinks.developerResources.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
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>
|
||||
);
|
||||
}
|
||||
61
apps/www/src/components/Hero.tsx
Normal file
61
apps/www/src/components/Hero.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import React from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import Link from "next/link";
|
||||
import { NetworkAnimation } from "./NetworkAnimation";
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<section className="relative pt-8 sm:pt-12 lg:pt-1 pb-6 px-4 sm:px-6 lg:px-8">
|
||||
<div className="hidden lg:block">
|
||||
<NetworkAnimation />
|
||||
</div>
|
||||
<div className="max-w-[81rem] mx-auto relative z-10">
|
||||
<div className="max-w-3xl mb-6 sm:mb-4">
|
||||
{/* Version Badge */}
|
||||
<div className="mb-4 sm:mb-6">
|
||||
<div className="inline-flex flex-wrap items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1 rounded-full bg-[rgba(185,191,255,0.4)] border border-[var(--secondary)] shadow backdrop-blur">
|
||||
<span className="text-xs sm:text-sm font-medium text-black/65">
|
||||
v0.4
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm font-medium text-black hidden sm:inline">
|
||||
—
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm font-[600] tracking-[-0.6px]! text-black leading-tight">
|
||||
<span className="hidden sm:inline">
|
||||
Unified /v1/responses API with state management
|
||||
</span>
|
||||
<span className="sm:hidden">Unified /v1/responses API</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Heading */}
|
||||
<h1 className="text-4xl sm:text-4xl md:text-5xl lg:text-7xl font-normal leading-tight tracking-tighter text-black mb-4 sm:mb-6 flex flex-col gap-0 sm:-space-y-2 lg:-space-y-3">
|
||||
<span className="font-sans">Models-native </span>
|
||||
<span className="font-sans font-medium text-[var(--secondary)]">
|
||||
dataplane for agents
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Subheading with CTA Buttons */}
|
||||
<div className="max-w-7xl flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6">
|
||||
<p className="text-base sm:text-lg md:text-xl lg:text-2xl font-sans font-[400] tracking-[-1.2px] sm:tracking-[-1.92px]! text-black max-w-4xl">
|
||||
Build agents faster, and scale them reliably by offloading the
|
||||
plumbing work in AI.
|
||||
</p>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="mb-0.5 flex flex-col sm:flex-row items-stretch sm:items-center gap-3 sm:gap-4 w-full sm:w-auto lg:justify-end">
|
||||
<Button asChild className="w-full sm:w-auto">
|
||||
<Link href="/get-started">Get started</Link>
|
||||
</Button>
|
||||
<Button variant="secondary" asChild className="w-full sm:w-auto">
|
||||
<Link href="/docs">Documentation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
70
apps/www/src/components/HowItWorksSection.tsx
Normal file
70
apps/www/src/components/HowItWorksSection.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
export function HowItWorksSection() {
|
||||
return (
|
||||
<section className="bg-[#1a1a1a] text-white pb-16 sm:pb-20 lg:pb-28 sm:pt-0 pt-20">
|
||||
<div className="max-w-312 mx-auto sm:pl-0">
|
||||
<div className="flex flex-col gap-8 sm:gap-12 lg:gap-16">
|
||||
{/* Header and Description */}
|
||||
<div className="max-w-4xl lg:-ml-[102px] lg:pl-[102px] sm:pl-0 pl-4">
|
||||
<h2 className="font-sans font-normal text-xl sm:text-2xl lg:text-3xl tracking-[-1.6px] sm:tracking-[-2px]! text-white leading-[1.03] mb-6 sm:mb-8">
|
||||
One configuration file to orchestrate
|
||||
</h2>
|
||||
<div className="font-mono text-white w-100 sm:w-full text-sm sm:text-lg lg:text-lg tracking-[-0.8px] sm:tracking-[-1.2px]!">
|
||||
<p className="mb-0">
|
||||
Plano offers a delightful developer experience with a simple
|
||||
configuration file that describes the types of prompts your
|
||||
agentic app supports, a set of APIs that need to be plugged in
|
||||
for agentic scenarios (including retrieval queries) and your
|
||||
choice of LLMs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Large Diagram - Scrollable on mobile, normal on desktop */}
|
||||
{/* Mobile: Full-width scrollable container that extends to viewport edges */}
|
||||
<div
|
||||
className="mt-5 lg:hidden relative left-1/2 right-1/2 -ml-[50vw] -mr-[50vw] w-screen overflow-x-auto overflow-y-visible"
|
||||
style={{
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
}}
|
||||
>
|
||||
<style jsx>{`
|
||||
.diagram-scroll-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
<div className="diagram-scroll-container inline-block">
|
||||
<Image
|
||||
src="/HowItWorks.svg"
|
||||
alt="How Plano Works Diagram"
|
||||
width={1200}
|
||||
height={600}
|
||||
className="h-auto"
|
||||
style={{ width: "1200px", maxWidth: "none", display: "block" }}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop: Extends to container edges */}
|
||||
<div className="hidden lg:block -w-[calc(10%+20px)] -mx-[10px]">
|
||||
<Image
|
||||
src="/HowItWorks.svg"
|
||||
alt="How Plano Works Diagram"
|
||||
width={10}
|
||||
height={10}
|
||||
className="w-full h-auto"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
194
apps/www/src/components/IdeaToAgentSection.tsx
Normal file
194
apps/www/src/components/IdeaToAgentSection.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
const carouselData = [
|
||||
{
|
||||
id: 1,
|
||||
category: "LAUNCH FASTER",
|
||||
title: "Focus on core objectives",
|
||||
description:
|
||||
"Building AI agents is hard enough (iterate on prompts and evaluate LLMs, etc), the plumbing work shouldn't add to that complexity. Plano takes care of the critical plumbing work like routing and orchestration to agents that slows you down and locks you into rigid frameworks, freeing developers to innovate on what truly matters.",
|
||||
image: "/LaunchFaster.svg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: "BUILD WITH CHOICE",
|
||||
title: "Rapidly incorporate LLMs",
|
||||
description:
|
||||
"Build with multiple LLMs or model versions with a single unified API. Plano centralizes access controls, offers resiliency for traffic to 100+ LLMs -- all without you having to write a single line of code.",
|
||||
image: "/BuildWithChoice.svg",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category: "RICH LEARNING SIGNALS",
|
||||
title: "Hyper-rich agent traces and logs",
|
||||
description:
|
||||
"Knowing when agents fail or delight users is a critical signal that feeds into a reinforcement learning and optimization cycle. Plano makes this trivial by sampling hyper-rich information traces from live production agentic interactions so that you can improve agent performance faster.",
|
||||
image: "/Telemetry.svg",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category: "SHIP CONFIDENTLY",
|
||||
title: "Centrally apply guardrail policies",
|
||||
description:
|
||||
"Plano comes built-in with a state-of-the-art guardrail model you can use for things like jailbreak detection. But you can easily extend those capabilities via plano's agent filter chain to apply custom policy checks in a centralized way and keep users engaged on topics relevant to your requirements.",
|
||||
image: "/ShipConfidently.svg",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
category: "SCALABLE ARCHITECTURE",
|
||||
title: "Protocol-Native Infrastructure",
|
||||
description:
|
||||
"Plano's sidecar deployment model avoids library-based abstractions - operating as a protocol-native data plane that integrates seamlessly with your existing agents via agentic APIs (like v1/responses). This decouples your core agent logic from plumbing concerns - run it alongside any framework without code changes, vendor lock-in, or performance overhead.",
|
||||
image: "/Contextual.svg",
|
||||
},
|
||||
];
|
||||
|
||||
export function IdeaToAgentSection() {
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
|
||||
|
||||
// Auto-advance slides
|
||||
useEffect(() => {
|
||||
if (!isAutoPlaying) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentSlide((prev) => (prev + 1) % carouselData.length);
|
||||
}, 10000); // 10 seconds per slide
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isAutoPlaying]);
|
||||
|
||||
const handleSlideClick = (index: number) => {
|
||||
setCurrentSlide(index);
|
||||
setIsAutoPlaying(false);
|
||||
// Resume auto-play after 10 seconds
|
||||
setTimeout(() => setIsAutoPlaying(true), 10000);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative py-12 sm:py-16 lg:py-24 px-4 sm:px-6 lg:px-[102px]">
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
{/* Main Heading */}
|
||||
<h2 className="font-sans font-normal text-2xl sm:text-3xl lg:text-4xl tracking-[-2px] sm:tracking-[-2.96px]! text-black mb-6 sm:mb-8 lg:mb-10">
|
||||
Idea to agent — without overhead
|
||||
</h2>
|
||||
|
||||
{/* Progress Indicators */}
|
||||
<div className="flex gap-1.5 sm:gap-2 mb-4 sm:mb-6 lg:mb-6 w-full">
|
||||
{carouselData.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleSlideClick(index)}
|
||||
className={`relative h-1.5 sm:h-2 rounded-full overflow-hidden transition-all duration-300 hover:opacity-80 ${
|
||||
index === currentSlide
|
||||
? "flex-1 sm:w-16 md:w-20 lg:w-[292px]"
|
||||
: "flex-1 sm:w-16 md:w-20 lg:w-[293px]"
|
||||
}`}
|
||||
>
|
||||
{/* Background */}
|
||||
<div className="absolute inset-0 bg-black/6 rounded-full" />
|
||||
|
||||
{/* Active Progress */}
|
||||
{index === currentSlide && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-[#7780d9] rounded-full"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: "100%" }}
|
||||
transition={{ duration: 10, ease: "linear" }}
|
||||
key={currentSlide}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Completed State */}
|
||||
{index < currentSlide && (
|
||||
<div className="absolute inset-0 bg-purple-200/90 rounded-full" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Carousel Content - Fixed height to prevent layout shift */}
|
||||
<div className="relative h-[500px] sm:h-[550px] md:h-[600px] lg:h-[500px]">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentSlide}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.4, ease: "easeInOut" }}
|
||||
className="absolute inset-0"
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-center lg:gap-12 h-full">
|
||||
{/* Left Content */}
|
||||
<div className="flex-1 order-1 lg:order-1 flex flex-col justify-center">
|
||||
<div className="max-w-[692px] mt-0 lg:mt-0">
|
||||
{/* Category */}
|
||||
<p className="font-mono font-bold text-[#2a3178] text-sm sm:text-base lg:text-xl tracking-[1.44px] sm:tracking-[1.92px]! mb-3 sm:mb-4 leading-[1.102]">
|
||||
{carouselData[currentSlide].category}
|
||||
</p>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-sans font-medium text-[#9797ea] text-2xl sm:text-3xl lg:text-5xl tracking-tight sm:tracking-[-2.96px]! mb-4 sm:mb-6 lg:mb-7">
|
||||
{carouselData[currentSlide].title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<div className="font-mono text-black text-sm sm:text-base lg:text-lg max-w-full lg:max-w-140 tracking-[-0.8px] sm:tracking-[-1.2px]!">
|
||||
<p className="mb-0">
|
||||
{carouselData[currentSlide].description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button className="mt-6 sm:mt-8 w-full sm:w-auto">
|
||||
Learn more
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image - Show below on mobile, right side on desktop */}
|
||||
{carouselData[currentSlide].image && (
|
||||
<div className="flex lg:hidden shrink-0 w-full justify-center items-center mb-6 sm:mb-8 order-0 lg:order-2">
|
||||
<img
|
||||
src={carouselData[currentSlide].image}
|
||||
alt={carouselData[currentSlide].category}
|
||||
className={`w-full h-auto object-contain ${
|
||||
carouselData[currentSlide].image === "/Telemetry.svg"
|
||||
? "max-w-md sm:max-w-lg max-h-[300px] sm:max-h-[350px]"
|
||||
: "max-w-sm sm:max-w-md max-h-[250px] sm:max-h-[300px]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right Image - Desktop only */}
|
||||
{carouselData[currentSlide].image && (
|
||||
<div
|
||||
className={`hidden lg:flex shrink-0 justify-end items-center order-2 ${
|
||||
carouselData[currentSlide].image === "/Telemetry.svg"
|
||||
? "w-[500px] xl:w-[600px]"
|
||||
: "w-[400px] xl:w-[500px]"
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={carouselData[currentSlide].image}
|
||||
alt={carouselData[currentSlide].category}
|
||||
className={`w-full h-auto object-contain ${
|
||||
carouselData[currentSlide].image === "/Telemetry.svg"
|
||||
? "max-h-[550px]"
|
||||
: "max-h-[450px]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
62
apps/www/src/components/IntroSection.tsx
Normal file
62
apps/www/src/components/IntroSection.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
export function IntroSection() {
|
||||
return (
|
||||
<section className="relative bg-[#1a1a1a] text-white py-20 px-6 lg:px-[102px]">
|
||||
<div className="max-w-324 mx-auto">
|
||||
<div className="flex flex-col lg:flex-row gap-12 items-center">
|
||||
{/* Left Content */}
|
||||
<div className="flex-1">
|
||||
{/* Heading */}
|
||||
<p className="font-mono font-bold text-primary-light text-xl tracking-[1.92px]! mb-4 leading-[1.102]">
|
||||
WHY PLANO?
|
||||
</p>
|
||||
<h2 className="font-sans font-medium tracking-[-1.92px]! text-[#9797ea] text-4xl leading-[1.102] mb-6 max-w-[633px]">
|
||||
Ship prototypes to production
|
||||
<span className="italic">—fast.</span>
|
||||
</h2>
|
||||
|
||||
{/* Body Text */}
|
||||
<div className="font-mono tracking-[-0.96px]! text-white text-sm sm:text-base lg:text-lg max-w-[713px]">
|
||||
<p className="mb-0">
|
||||
Plano is a framework-friendly proxy server and dataplane for
|
||||
agents, deployed as a sidecar. Plano handles the critical
|
||||
plumbing work in AI like agent routing and orchestration,
|
||||
comprehensive traces for agentic interactions, guardrail hooks,
|
||||
unified APIs for LLMs —
|
||||
</p>
|
||||
<p className="mb-0 mt-4">
|
||||
<strong>
|
||||
<u>Developers</u>
|
||||
</strong>{" "}
|
||||
can focus more on modeling workflows,{" "}
|
||||
<strong>
|
||||
<u>product teams</u>
|
||||
</strong>{" "}
|
||||
can accelerate feedback loops for reinforcement learning and{" "}
|
||||
<strong>
|
||||
<u>engineering teams</u>
|
||||
</strong>{" "}
|
||||
can standardize policies and access controls across every agent
|
||||
and LLM for safer, more reliable scaling.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Diagram */}
|
||||
<div className="flex-1 relative w-full">
|
||||
<Image
|
||||
src="/IntroDiagram.svg"
|
||||
alt="Network Path Diagram"
|
||||
width={800}
|
||||
height={600}
|
||||
className="w-full h-auto"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
17
apps/www/src/components/Logo.tsx
Normal file
17
apps/www/src/components/Logo.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
55
apps/www/src/components/LogoCloud.tsx
Normal file
55
apps/www/src/components/LogoCloud.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
const customerLogos = [
|
||||
{
|
||||
name: "HuggingFace",
|
||||
src: "/logos/huggingface.svg",
|
||||
},
|
||||
{
|
||||
name: "T-Mobile",
|
||||
src: "/logos/tmobile.svg",
|
||||
},
|
||||
{
|
||||
name: "Chase",
|
||||
src: "/logos/chase.svg",
|
||||
},
|
||||
{
|
||||
name: "SanDisk",
|
||||
src: "/logos/sandisk.svg",
|
||||
},
|
||||
{
|
||||
name: "Oracle",
|
||||
src: "/logos/oracle.svg",
|
||||
},
|
||||
];
|
||||
|
||||
export function LogoCloud() {
|
||||
return (
|
||||
<section className="relative py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 sm:gap-6 md:gap-8 lg:gap-12 items-center justify-items-center">
|
||||
{customerLogos.map((logo, index) => {
|
||||
const isLast = index === customerLogos.length - 1;
|
||||
return (
|
||||
<div
|
||||
key={logo.name}
|
||||
className={`flex items-center justify-center opacity-60 hover:opacity-80 transition-opacity duration-300 w-full max-w-32 sm:max-w-40 md:max-w-48 h-10 sm:h-12 md:h-16 ${
|
||||
isLast ? "col-span-2 md:col-span-3 lg:col-span-1" : ""
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={logo.src}
|
||||
alt={`${logo.name} logo`}
|
||||
width={128}
|
||||
height={40}
|
||||
className="w-full h-full object-contain filter grayscale hover:grayscale-0 transition-all duration-300"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
290
apps/www/src/components/Navbar.tsx
Normal file
290
apps/www/src/components/Navbar.tsx
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
"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: "/start", label: "start locally" },
|
||||
{ href: "/docs", label: "docs" },
|
||||
{ href: "/model-research", label: "models research" },
|
||||
{ href: "/blog", label: "blog" },
|
||||
{ href: "/why", label: "why plano?" },
|
||||
];
|
||||
|
||||
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="top-0 left-0 right-0 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>
|
||||
);
|
||||
}
|
||||
203
apps/www/src/components/NetworkAnimation.tsx
Normal file
203
apps/www/src/components/NetworkAnimation.tsx
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
interface Node {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
delay: number;
|
||||
}
|
||||
|
||||
interface Connection {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
interface Particle {
|
||||
id: string;
|
||||
connectionIndex: number;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
// 4 separate groups positioned to the right of the heading text, aligned with the text vertically
|
||||
const nodes: Node[] = [
|
||||
// Group 1
|
||||
{ id: "1", x: 20, y: 35, size: 18, delay: 0 },
|
||||
{ id: "2", x: 70, y: 42, size: 12, delay: 0.8 },
|
||||
{ id: "3", x: 76, y: 38, size: 16, delay: 1.6 },
|
||||
|
||||
// // Group 2
|
||||
// { id: "4", x: 80, y: 30, size: 18, delay: 0.4 },
|
||||
// { id: "5", x: 87, y: 36, size: 12, delay: 1.2 },
|
||||
// { id: "6", x: 92, y: 33, size: 16, delay: 2.0 },
|
||||
|
||||
// Group 3
|
||||
{ id: "7", x: 62, y: 48, size: 10, delay: 0.6 },
|
||||
{ id: "8", x: 65, y: 52, size: 18, delay: 1.4 },
|
||||
{ id: "9", x: 75, y: 48, size: 14, delay: 0.6 },
|
||||
];
|
||||
|
||||
const connections: Connection[] = [
|
||||
// Group 1 connections
|
||||
{ from: "1", to: "2" },
|
||||
{ from: "2", to: "3" },
|
||||
|
||||
// // Group 2 connections
|
||||
// { from: "4", to: "5" },
|
||||
// { from: "5", to: "6" },
|
||||
|
||||
// Group 3 connections
|
||||
{ from: "7", to: "8" },
|
||||
{ from: "8", to: "9" },
|
||||
];
|
||||
|
||||
export function NetworkAnimation() {
|
||||
const [particles, setParticles] = useState<Particle[]>([]);
|
||||
|
||||
// Create and animate particles along connections - much slower
|
||||
useEffect(() => {
|
||||
const createParticle = () => {
|
||||
const connectionIndex = Math.floor(Math.random() * connections.length);
|
||||
const particle: Particle = {
|
||||
id: `particle-${Date.now()}-${Math.random()}`,
|
||||
connectionIndex,
|
||||
progress: 0,
|
||||
};
|
||||
|
||||
setParticles((prev) => [...prev, particle]);
|
||||
|
||||
// Remove particle after animation completes
|
||||
setTimeout(() => {
|
||||
setParticles((prev) => prev.filter((p) => p.id !== particle.id));
|
||||
}, 5000); // Slower duration
|
||||
};
|
||||
|
||||
// Create particles at slower intervals
|
||||
const interval = setInterval(createParticle, 2500);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Animate particles - slower movement
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setParticles((prev) =>
|
||||
prev
|
||||
.map((p) => ({ ...p, progress: p.progress + 0.008 })) // Much slower
|
||||
.filter((p) => p.progress <= 1),
|
||||
);
|
||||
}, 50);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 pointer-events-none z-0 opacity-8 rotate-20 translate-x-40 -translate-y-40 scale-75 lg:scale-100 xl:scale-125 2xl:scale-150 [@media(min-width:1800px)]:scale-[1.60]">
|
||||
<svg
|
||||
className="absolute inset-0 w-full h-full rotate-10"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
viewBox="790 -90 1020 840"
|
||||
style={{ overflow: "visible" }}
|
||||
>
|
||||
<defs>
|
||||
{/* Simple glow filter */}
|
||||
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Render connections - simple lines with lower opacity */}
|
||||
{connections.map((conn, index) => {
|
||||
const fromNode = nodes.find((n) => n.id === conn.from);
|
||||
const toNode = nodes.find((n) => n.id === conn.to);
|
||||
if (!fromNode || !toNode) return null;
|
||||
|
||||
const x1 = (fromNode.x / 100) * 1920;
|
||||
const y1 = (fromNode.y / 100) * 800;
|
||||
const x2 = (toNode.x / 100) * 1920;
|
||||
const y2 = (toNode.y / 100) * 800;
|
||||
|
||||
return (
|
||||
<line
|
||||
key={`line-${conn.from}-${conn.to}`}
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
stroke="#8b91e8"
|
||||
strokeWidth="1.5"
|
||||
opacity={1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Render data particles with lower opacity */}
|
||||
{particles.map((particle) => {
|
||||
const conn = connections[particle.connectionIndex];
|
||||
const fromNode = nodes.find((n) => n.id === conn.from);
|
||||
const toNode = nodes.find((n) => n.id === conn.to);
|
||||
if (!fromNode || !toNode) return null;
|
||||
|
||||
const x =
|
||||
((fromNode.x + (toNode.x - fromNode.x) * particle.progress) / 100) *
|
||||
1920;
|
||||
const y =
|
||||
((fromNode.y + (toNode.y - fromNode.y) * particle.progress) / 100) *
|
||||
800;
|
||||
|
||||
return (
|
||||
<motion.circle
|
||||
key={particle.id}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={4}
|
||||
fill="#b9bfff"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: [0, 0.4, 0.4, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 5,
|
||||
times: [0, 0.2, 0.8, 1],
|
||||
ease: "linear",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Render nodes with subtle floating animation */}
|
||||
{nodes.map((node) => {
|
||||
const size = node.size;
|
||||
const baseX = (node.x / 100) * 1920;
|
||||
const baseY = (node.y / 100) * 800;
|
||||
|
||||
return (
|
||||
<motion.circle
|
||||
key={node.id}
|
||||
cx={baseX}
|
||||
cy={baseY}
|
||||
r={size}
|
||||
fill="#8b91e8"
|
||||
animate={{
|
||||
cx: [baseX - 2, baseX + 2, baseX - 2],
|
||||
cy: [baseY - 2, baseY + 2, baseY - 2],
|
||||
}}
|
||||
transition={{
|
||||
duration: 10 + node.delay,
|
||||
delay: node.delay,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
apps/www/src/components/UnlockPotentialSection.tsx
Normal file
40
apps/www/src/components/UnlockPotentialSection.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import React from "react";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
interface UnlockPotentialSectionProps {
|
||||
variant?: "transparent" | "black";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function UnlockPotentialSection({
|
||||
variant = "transparent",
|
||||
className = "",
|
||||
}: UnlockPotentialSectionProps) {
|
||||
const backgroundClass = variant === "black" ? "bg-[#1a1a1a]" : "";
|
||||
const textColor = variant === "black" ? "text-white" : "text-black";
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`relative py-24 px-6 lg:px-[102px]`} style={{ background: "linear-gradient(to top right, #ffffff, #dcdfff)" }}
|
||||
>
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
<div className="max-w-4xl">
|
||||
<h2
|
||||
className={`font-sans font-normal text-[1.8rem] lg:text-4xl tracking-[-2.55px]! ${textColor} leading-[1.4] mb-8`}
|
||||
>
|
||||
Focus on prompting, not plumbing.
|
||||
<br />
|
||||
Build with{" "}
|
||||
<strong className="font-medium text-primary">plano</strong>, get
|
||||
started in less than a minute.
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-5">
|
||||
<Button>Deploy today</Button>
|
||||
<Button variant="secondaryDark">Documentation</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
296
apps/www/src/components/UseCasesSection.tsx
Normal file
296
apps/www/src/components/UseCasesSection.tsx
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
Network,
|
||||
Filter,
|
||||
TrendingUp,
|
||||
Shield,
|
||||
Server,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogClose,
|
||||
} from "./ui/dialog";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
interface UseCase {
|
||||
id: number;
|
||||
category: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
fullContent: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
gradient: string;
|
||||
}
|
||||
|
||||
const useCasesData: UseCase[] = [
|
||||
{
|
||||
id: 1,
|
||||
category: "AGENT ORCHESTRATION",
|
||||
title: "Multi-agent systems without framework lock-in",
|
||||
summary:
|
||||
"Seamless routing and orchestration for complex agent interactions",
|
||||
fullContent:
|
||||
"Plano manages agent routing and orchestration without framework dependencies, allowing seamless multi-agent interactions. This is ideal for building complex systems like automated customer support or data processing pipelines, where agents hand off tasks efficiently to deliver end-to-end solutions faster.",
|
||||
icon: Network,
|
||||
gradient:
|
||||
"from-[rgba(119,128,217,0.15)] via-[rgba(119,128,217,0.08)] to-[rgba(17,28,132,0.05)]",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: "CONTEXT ENGINEERING",
|
||||
title: "Reusable filters for smarter agents",
|
||||
summary:
|
||||
"Inject data, reformulate queries, and enforce policies efficiently",
|
||||
fullContent:
|
||||
"Plano's filter chain encourages reuse and decoupling for context engineering tasks like injecting data, reformulating queries, and enforcing policy before calls reach an agent or LLM. This means faster debugging, cleaner architecture, and more accurate, on-policy agents —without bespoke glue code.",
|
||||
icon: Filter,
|
||||
gradient:
|
||||
"from-[rgba(177,184,255,0.15)] via-[rgba(177,184,255,0.08)] to-[rgba(17,28,132,0.05)]",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category: "REINFORCEMENT LEARNING",
|
||||
title: "Production signals for continuous improvement",
|
||||
summary: "Capture rich traces to accelerate training and refinement",
|
||||
fullContent:
|
||||
"Plano captures hyper-rich tracing and log samples from production traffic, feeding into reinforcement learning and fine-tuning cycles. This accelerates iteration in areas like recommendation engines, helping teams quickly identify failures, refine prompts, and boost agent effectiveness based on real-user signals.",
|
||||
icon: TrendingUp,
|
||||
gradient:
|
||||
"from-[rgba(185,191,255,0.15)] via-[rgba(185,191,255,0.08)] to-[rgba(17,28,132,0.05)]",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category: "CENTRALIZED SECURITY",
|
||||
title: "Built-in guardrails and centralized policies",
|
||||
summary: "Safe scaling with jailbreak detection and access controls",
|
||||
fullContent:
|
||||
"With built-in guardrails, centralized policies, and access controls, Plano ensures safe scaling across LLMs, detecting issues like jailbreak attempts. This is critical for deployments in regulated fields like finance or healthcare, and minimizing risks while standardizing reliability and security of agents.",
|
||||
icon: Shield,
|
||||
gradient:
|
||||
"from-[rgba(119,128,217,0.15)] via-[rgba(119,128,217,0.08)] to-[rgba(17,28,132,0.05)]",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
category: "ON-PREMISES DEPLOYMENT",
|
||||
title: "Full data control in regulated environments",
|
||||
summary: "Deploy on private infrastructure without compromising features",
|
||||
fullContent:
|
||||
"Plano's lightweight sidecar model deploys effortlessly on your private infrastructure, empowering teams in regulated sectors to maintain full data control while benefiting from unified LLM access, custom filter chains, and production-grade tracing—without compromising on security or scalability.",
|
||||
icon: Server,
|
||||
gradient:
|
||||
"from-[rgba(177,184,255,0.15)] via-[rgba(177,184,255,0.08)] to-[rgba(17,28,132,0.05)]",
|
||||
},
|
||||
];
|
||||
|
||||
export function UseCasesSection() {
|
||||
const [selectedUseCase, setSelectedUseCase] = useState<UseCase | null>(null);
|
||||
|
||||
return (
|
||||
<section className="relative py-12 sm:py-16 lg:py-10 px-4 sm:px-6 lg:px-[102px]">
|
||||
<div className="max-w-[81rem] mx-auto">
|
||||
{/* Section Header */}
|
||||
<div className="mb-8 sm:mb-12 lg:mb-14">
|
||||
{/* USE CASES Badge */}
|
||||
<div className="mb-4 sm:mb-6">
|
||||
<div className="inline-flex items-center gap-2 px-3 sm:px-4 py-1 rounded-full bg-[rgba(185,191,255,0.4)] border border-[var(--secondary)] shadow backdrop-blur">
|
||||
<span className="font-mono font-bold text-[#2a3178] text-xs sm:text-sm tracking-[1.44px] sm:tracking-[1.62px]!">
|
||||
USE CASES
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Heading and CTA Button */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6 sm:gap-6">
|
||||
<h2 className="font-sans font-normal text-2xl sm:text-3xl lg:text-4xl tracking-[-2px] sm:tracking-[-2.88px]! text-black leading-[1.03]">
|
||||
What's possible with Plano
|
||||
</h2>
|
||||
<Button className="hidden lg:block">Start building</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 5 Card Grid - Horizontal Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
{useCasesData.map((useCase) => (
|
||||
<motion.div
|
||||
key={useCase.id}
|
||||
whileHover={{ y: -4 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="bg-gradient-to-b from-[rgba(177,184,255,0.16)] to-[rgba(17,28,132,0.035)] border-2 border-[rgba(171,178,250,0.27)] rounded-md p-4 sm:p-6 lg:p-6 h-auto sm:h-64 md:h-72 lg:h-90 flex flex-col justify-between cursor-pointer"
|
||||
onClick={() => setSelectedUseCase(useCase)}
|
||||
>
|
||||
{/* Category */}
|
||||
<div className="mb-4 sm:mb-6">
|
||||
<p className="font-mono font-bold text-[#2a3178] text-sm sm:text-sm tracking-[1.44px] sm:tracking-[1.92px]! mb-3 sm:mb-4">
|
||||
{useCase.category}
|
||||
</p>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-sans font-normal text-black text-lg sm:text-xl lg:text-2xl tracking-[-1.2px]! leading-[1.102]">
|
||||
{useCase.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Learn More Link */}
|
||||
<div className="mt-auto">
|
||||
<button className="group flex items-center gap-2 font-mono font-bold text-[var(--primary)] text-sm sm:text-base tracking-[1.44px] sm:tracking-[1.92px]! leading-[1.45] hover:text-[var(--primary-dark)] transition-colors">
|
||||
LEARN MORE
|
||||
<ArrowRightIcon className="w-3.5 h-3.5 sm:w-4 sm:h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Start building button - Mobile only, appears last */}
|
||||
<div className="lg:hidden mt-8">
|
||||
<Button className="w-full">Start building</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal */}
|
||||
<Dialog
|
||||
open={selectedUseCase !== null}
|
||||
onOpenChange={(open) => !open && setSelectedUseCase(null)}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{selectedUseCase &&
|
||||
(() => {
|
||||
const IconComponent = selectedUseCase.icon;
|
||||
return (
|
||||
<DialogContent
|
||||
key={selectedUseCase.id}
|
||||
className="max-w-[90rem]! p-0 overflow-hidden"
|
||||
showCloseButton={false}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.98, y: 8 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.98, y: 8 }}
|
||||
transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="relative"
|
||||
>
|
||||
{/* Gradient Background */}
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-br ${selectedUseCase.gradient} opacity-50`}
|
||||
/>
|
||||
|
||||
{/* Decorative Border */}
|
||||
<div className="absolute inset-0 border-2 border-[rgba(171,178,250,0.3)] rounded-lg pointer-events-none" />
|
||||
|
||||
{/* Custom Close Button */}
|
||||
<DialogClose className="absolute top-4 right-4 z-50 rounded-xs opacity-70 hover:opacity-100 transition-opacity focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgba(171,178,250,0.5)] bg-white/80 backdrop-blur-sm p-2 hover:bg-white/90">
|
||||
<XIcon className="w-5 h-5 text-[#2a3178]" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
|
||||
{/* Content Container */}
|
||||
<div className="relative z-10 p-5 sm:p-8 md:p-10 lg:p-14">
|
||||
{/* Header Section with Icon */}
|
||||
<DialogHeader className="mb-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start gap-4 sm:gap-8 mb-8">
|
||||
{/* Icon Container - hidden on mobile */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
delay: 0.1,
|
||||
}}
|
||||
className="hidden sm:flex shrink-0 w-14 h-14 sm:w-16 sm:h-16 rounded-xl bg-gradient-to-br from-[rgba(119,128,217,0.2)] to-[rgba(17,28,132,0.1)] border-2 border-[rgba(171,178,250,0.4)] items-center justify-center shadow-lg backdrop-blur-sm mx-0"
|
||||
>
|
||||
<IconComponent className="w-8 h-8 text-[#2a3178]" />
|
||||
</motion.div>
|
||||
|
||||
{/* Title Section */}
|
||||
<div className="flex-1 text-left mt-4 sm:mt-0">
|
||||
<motion.p
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
delay: 0.15,
|
||||
}}
|
||||
className="font-mono font-bold text-[#2a3178] text-xs tracking-[1.62px]! mb-1 uppercase"
|
||||
>
|
||||
USE CASE
|
||||
</motion.p>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
delay: 0.2,
|
||||
}}
|
||||
>
|
||||
<DialogTitle className="font-sans font-medium text-2xl sm:text-3xl lg:text-4xl xl:text-4xl tracking-[-1.5px]! text-black leading-[1.1] mb-4">
|
||||
{selectedUseCase.title}
|
||||
</DialogTitle>
|
||||
<div className="inline-flex items-center px-3 py-1 rounded-full bg-[rgba(185,191,255,0.3)] border border-[rgba(171,178,250,0.4)] backdrop-blur-sm">
|
||||
<span className="font-mono font-bold text-[#2a3178] text-xs tracking-[1.44px]!">
|
||||
{selectedUseCase.category}
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
delay: 0.3,
|
||||
}}
|
||||
className="mb-10"
|
||||
>
|
||||
<DialogDescription className="font-mono text-[#494949] text-base lg:text-base xl:text-lg leading-relaxed tracking-tight max-w-none mb-0">
|
||||
{selectedUseCase.fullContent}
|
||||
</DialogDescription>
|
||||
</motion.div>
|
||||
|
||||
{/* Footer with CTA - mobile friendly */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
delay: 0.35,
|
||||
}}
|
||||
className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4 pt-8 border-t border-[rgba(171,178,250,0.2)]"
|
||||
>
|
||||
{/* "Ready to get started?" is now first in column on mobile */}
|
||||
<div className="flex items-center gap-2 text-sm font-mono text-[#494949] justify-center sm:justify-start order-0">
|
||||
<span>Ready to get started?</span>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto order-1">
|
||||
<Button className="w-full sm:w-auto">
|
||||
Start building
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</DialogContent>
|
||||
);
|
||||
})()}
|
||||
</AnimatePresence>
|
||||
</Dialog>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
186
apps/www/src/components/VerticalCarouselSection.tsx
Normal file
186
apps/www/src/components/VerticalCarouselSection.tsx
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
const verticalCarouselData = [
|
||||
{
|
||||
id: 1,
|
||||
category: "INTRODUCTION",
|
||||
title: "Simple to revolutionary",
|
||||
description:
|
||||
"Plano is an intelligent (edge and LLM) proxy server designed for agents - to help you focus on core business objectives. Arch handles critical but the pesky tasks related to the handling and processing of prompts, which includes detecting and rejecting jailbreak attempts, intelligent task routing for improved accuracy, mapping user requests into 'backend' functions, and managing the observability of prompts and LLM in a centralized way.",
|
||||
diagram: "/IntroDiagram.svg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: "OPEN SOURCE",
|
||||
title: "Freedom to extend & deploy",
|
||||
description:
|
||||
"No lock-in. No black boxes. Just an open, intelligent (edge and LLM) proxy for building smarter, agentic AI applications. Created by contributors to Envoy Proxy, Arch brings enterprise-grade reliability to prompt orchestration, while giving you the flexibility to shape, extend, and integrate it into your AI workflows.",
|
||||
diagram: "/OpenSource.svg",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category: "BUILT ON ENVOY",
|
||||
title: "Production-proven infrastructure",
|
||||
description:
|
||||
"Plano takes a dependency on Envoy and is a self-contained process designed to run alongside your application servers. Plano extends Envoy's HTTP connection management subsystem, filtering, and telemetry capabilities exclusively for prompts and LLMs. Use Plano with any application language or framework, and use Plano with any LLM provider.",
|
||||
diagram: "/BuiltOnEnvoy.svg",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category: "PURPOSE-BUILT",
|
||||
title: "Task-optimized, efficient LLMs",
|
||||
description:
|
||||
"Unlike generic API gateways, Plano is purpose-built for AI agent workloads. Every feature is designed with prompt processing, model routing, and agent orchestration in mind, providing optimal performance for your AI applications.",
|
||||
diagram: "/PurposeBuilt.svg",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
category: "PROMPT ROUTING",
|
||||
title: "Intelligent request handling",
|
||||
description:
|
||||
"Prompt Targets are a core concept in Plano, enabling developers to define how different types of user prompts should get processed and routed. Define prompt targets, so you can seperate business logic from the complexities of processing and handling of prompts, focusing on the quality of your application and a cleaner seperation of concerns in your codebase.",
|
||||
diagram: "/PromptRouting.svg",
|
||||
},
|
||||
];
|
||||
|
||||
export function VerticalCarouselSection() {
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
|
||||
const handleSlideClick = (index: number) => {
|
||||
setActiveSlide(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative bg-[#1a1a1a] text-white pt-20 pb-0 lg:pb-4 px-4 sm:px-6 lg:px-[102px] h-auto sm:h-[650px]">
|
||||
<div className="max-w-324 mx-auto">
|
||||
{/* Main Heading */}
|
||||
<h2 className="font-sans font-normal text-2xl sm:text-3xl lg:text-4xl tracking-[-2px] sm:tracking-[-2.88px]! text-white leading-[1.03] mb-8 sm:mb-12 lg:mb-12 max-w-4xl">
|
||||
Under the hood
|
||||
</h2>
|
||||
|
||||
{/* Mobile: Horizontal Scroller Navigation */}
|
||||
<div className="lg:hidden mb-8 -mx-4 sm:mx-0 px-4 sm:px-0">
|
||||
<div
|
||||
className="relative overflow-x-auto pb-2"
|
||||
style={{
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
}}
|
||||
>
|
||||
<style jsx>{`
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
<div className="flex gap-4 min-w-max hide-scrollbar">
|
||||
{verticalCarouselData.map((item, index) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => handleSlideClick(index)}
|
||||
className={`relative px-4 py-2 rounded transition-all duration-300 whitespace-nowrap ${
|
||||
index === activeSlide
|
||||
? "bg-[#6363d2]/90 text-[#f9faff]"
|
||||
: "bg-[#6363d2]/10 text-[rgba(182,188,255,0.71)] hover:bg-[#6363d2]/15"
|
||||
}`}
|
||||
>
|
||||
<span className="font-mono font-bold text-sm tracking-[1.44px]!">
|
||||
{item.category}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop: Vertical Carousel Layout */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-start">
|
||||
{/* Left Sidebar Navigation - Desktop Only */}
|
||||
<div className="hidden lg:block lg:w-72 shrink-0 lg:pt-0">
|
||||
<div className="relative space-y-6">
|
||||
<motion.div
|
||||
className="absolute left-0 top-0 w-2 h-4 bg-[#6363d2] z-10 rounded-xs"
|
||||
animate={{
|
||||
y: activeSlide * 52 + 6, // Each item is ~28px text + 24px gap = 52px, +10px to center smaller rectangle
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
duration: 0.6,
|
||||
}}
|
||||
/>
|
||||
|
||||
{verticalCarouselData.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => handleSlideClick(index)}
|
||||
className="cursor-pointer relative pl-6 transition-all duration-300"
|
||||
>
|
||||
{/* Category Text */}
|
||||
<span
|
||||
className={`font-mono font-bold text-lg tracking-[1.69px]! transition-colors duration-300 ${
|
||||
index === activeSlide
|
||||
? "text-[#acb3fe]"
|
||||
: "text-[rgba(172,179,254,0.71)]"
|
||||
}`}
|
||||
>
|
||||
{item.category}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Content Area - Fixed height to prevent layout shift */}
|
||||
<div className="flex-1 h-[600px] sm:h-[650px] lg:h-[600px] relative lg:-ml-8">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeSlide}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.4, ease: "easeInOut" }}
|
||||
className="w-full h-full"
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row gap-6 sm:gap-8 lg:gap-12 items-start h-full">
|
||||
{/* Diagram - Above on mobile, Right Side on desktop */}
|
||||
<div className="w-full lg:flex-1 flex items-center justify-center lg:justify-start order-first lg:order-last shrink-0">
|
||||
<div className="relative w-full max-w-full sm:max-w-md lg:max-w-[600px] aspect-4/3">
|
||||
<Image
|
||||
src={verticalCarouselData[activeSlide].diagram}
|
||||
alt={verticalCarouselData[activeSlide].title}
|
||||
fill
|
||||
className="object-contain object-top"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text Content */}
|
||||
<div className="flex-1 max-w-2xl order-last lg:order-first flex flex-col justify-start">
|
||||
{/* Title
|
||||
<h3 className="font-sans font-medium text-primary text-xl sm:text-2xl lg:text-[34px] tracking-[-1px]! leading-[1.03] mb-4 sm:mb-6">
|
||||
{verticalCarouselData[activeSlide].title}
|
||||
</h3> */}
|
||||
|
||||
{/* Description */}
|
||||
<div className="font-mono text-white text-sm sm:text-base lg:text-lg tracking-[-0.8px] sm:tracking-[-1.2px]! max-w-full lg:max-w-md">
|
||||
<p className="mb-0">
|
||||
{verticalCarouselData[activeSlide].description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
65
apps/www/src/components/ui/button.tsx
Normal file
65
apps/www/src/components/ui/button.tsx
Normal 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 };
|
||||
143
apps/www/src/components/ui/dialog.tsx
Normal file
143
apps/www/src/components/ui/dialog.tsx
Normal 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,
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue