AI Agent Platform Landing New
A framed, grid-lined landing page for an AI dev product: auto-advancing feature tabs, live agent demos, constellation, marquee testimonials, pricing and FAQ.
Installation
01
Install with the shadcn CLI. The NiceUI theme comes along the first time.
$ npx shadcn@latest add https://niceui.dev/r/landing-agent-platform-landing.json02
Or copy the source below (3 files). Dependencies: lucide-react.
Source
components/niceui/pages/landing/LandingPageNorthstar.tsx
import { type ReactNode, useEffect, useState } from 'react';
import {
ArrowRight,
ArrowUpRight,
Bot,
Check,
ChevronDown,
Copy,
GitPullRequest,
Globe,
Plug,
Plus,
Rocket,
Search,
ShieldCheck,
Sparkles,
Terminal,
Workflow,
} from 'lucide-react';
import { cn } from '@/lib/cn';
import { Reveal, useCountUp, useInView } from './shared/motion';
import { AgentTerminal, DiffSuggestion, McpToolPicker, NetworkPulse, ParallelAgents, agentsDemo, diffDemoProps, terminalDemoProps, toolDemo } from '@/components/niceui/ai/AgentWorkflow';
type PageProps = { onNavigate?: (to: string) => void };
/* ─── frame primitives ─────────────────────────────────────────────── */
function Cross({ className }: { className?: string }) {
return (
<span aria-hidden className={cn('pointer-events-none absolute z-10 h-3 w-3', className)}>
<span className="absolute left-1/2 top-0 h-full w-px -translate-x-1/2 bg-white/25" />
<span className="absolute left-0 top-1/2 h-px w-full -translate-y-1/2 bg-white/25" />
</span>
);
}
function Band({ children, className, crosses = true }: { children: ReactNode; className?: string; crosses?: boolean }) {
return (
<section className={cn('relative border-t border-white/[.07]', className)}>
{crosses && (
<>
<Cross className="-left-1.5 -top-1.5" />
<Cross className="-right-1.5 -top-1.5" />
</>
)}
{children}
</section>
);
}
function Eyebrow({ icon: Icon = Sparkles, children }: { icon?: typeof Sparkles; children: ReactNode }) {
return (
<span className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[.02] px-3 py-1 font-mono text-[11px] text-ink-400">
<Icon className="h-3 w-3 text-brand-300" />
{children}
</span>
);
}
function Accent({ children }: { children: ReactNode }) {
return <em className="font-serif font-normal italic text-brand-300">{children}</em>;
}
/** Sky-tinted dot field used as a section divider. */
function Grain() {
return (
<div
aria-hidden
className="h-10 border-t border-white/[.07] opacity-70 [animation:ns-grain_14s_linear_infinite]"
style={{
backgroundImage:
'radial-gradient(rgba(125,211,252,.55) 1px, transparent 1.3px), radial-gradient(rgba(255,255,255,.25) 1px, transparent 1.3px)',
backgroundSize: '7px 7px, 11px 11px',
backgroundPosition: '0 0, 3px 4px',
maskImage: 'linear-gradient(90deg, transparent, black 12%, black 88%, transparent)',
WebkitMaskImage: 'linear-gradient(90deg, transparent, black 12%, black 88%, transparent)',
}}
/>
);
}
/* ─── nav + hero ───────────────────────────────────────────────────── */
function Logo() {
return (
<span className="flex items-center gap-2 text-[15px] font-medium tracking-[-0.02em] text-ink-50">
<svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden>
<path d="M12 2v20M2 12h20" stroke="#7dd3fc" strokeWidth="1.6" strokeLinecap="round" />
<path d="M5 5l14 14M19 5 5 19" stroke="white" strokeOpacity=".35" strokeWidth="1.2" strokeLinecap="round" />
<circle cx="12" cy="12" r="2.4" fill="#7dd3fc" />
</svg>
northstar
</span>
);
}
function Nav() {
return (
<header className="sticky top-0 z-50 border-b border-white/[.07] bg-ink-950/75 backdrop-blur-xl">
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-6">
<Logo />
<nav className="hidden items-center gap-7 text-[13px] text-ink-400 md:flex">
{['Product', 'Agents', 'Integrations', 'Pricing', 'Docs'].map((l) => (
<a key={l} href="#" className="transition-colors hover:text-ink-50">
{l}
</a>
))}
</nav>
<div className="flex items-center gap-2">
<a href="#" className="hidden px-3 text-[13px] text-ink-300 hover:text-ink-50 sm:block">
Sign in
</a>
<a href="#" className="inline-flex h-8 items-center rounded-full bg-brand-300 px-3.5 text-[13px] font-medium text-brand-950 shadow-glow transition hover:bg-brand-200">
Start building
</a>
</div>
</div>
</header>
);
}
function Hero() {
const [copied, setCopied] = useState(false);
return (
<div className="relative overflow-hidden">
<div aria-hidden className="absolute inset-0 [background-image:linear-gradient(rgba(255,255,255,.035)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,.035)_1px,transparent_1px)] [background-size:64px_64px] [mask-image:radial-gradient(ellipse_70%_60%_at_50%_0%,black,transparent)]" />
<div className="relative px-6 pb-0 pt-24 text-center sm:pt-28">
<Reveal>
<Eyebrow icon={Bot}>Multi-agent runs are here</Eyebrow>
</Reveal>
<Reveal delay={80}>
<h1 className="mx-auto mt-7 max-w-3xl text-[44px] font-medium leading-[1] tracking-[-0.055em] text-ink-50 sm:text-7xl">
Your frontend, <Accent>shipped</Accent> by agents.
</h1>
</Reveal>
<Reveal delay={160}>
<p className="mx-auto mt-6 max-w-lg text-[16px] leading-relaxed text-ink-400">
Northstar turns a ticket into a reviewed, tested pull request — components, styles and stories included. You stay the editor, not the typist.
</p>
</Reveal>
<Reveal delay={240}>
<div className="mt-9 flex flex-wrap items-center justify-center gap-3">
<a href="#" className="group inline-flex h-11 items-center gap-2 rounded-full bg-brand-300 px-5 text-sm font-medium text-brand-950 shadow-glow transition hover:bg-brand-200">
Start for free <ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
</a>
<button
onClick={() => {
navigator.clipboard?.writeText('npx northstar init');
setCopied(true);
setTimeout(() => setCopied(false), 1400);
}}
className="inline-flex h-11 items-center gap-2.5 rounded-full border border-white/15 px-4 font-mono text-[13px] text-ink-200 transition hover:border-brand-300/50"
>
<span className="text-brand-300">$</span> npx northstar init
{copied ? <Check className="h-3.5 w-3.5 text-brand-300" /> : <Copy className="h-3.5 w-3.5 text-ink-500" />}
</button>
</div>
</Reveal>
</div>
<div aria-hidden className="relative mt-14 h-56 [mask-image:linear-gradient(black_50%,transparent)]">
<div className="absolute left-1/2 top-20 h-[1800px] w-[1800px] -translate-x-1/2 rounded-full border-t border-brand-200/80 bg-ink-950 shadow-[0_-1px_48px_rgba(125,211,252,.5),inset_0_40px_80px_-40px_rgba(125,211,252,.25)]" />
<div className="absolute left-1/2 top-0 h-56 w-[70%] -translate-x-1/2 bg-[radial-gradient(50%_60%_at_50%_75%,rgba(56,189,248,.32),transparent_70%)] blur-2xl [animation:ns-breathe_7s_ease-in-out_infinite]" />
</div>
</div>
);
}
/* ─── feature tabs ─────────────────────────────────────────────────── */
const TABS = [
{ id: 'gen', label: 'Generate', icon: Sparkles, title: 'Components from a sentence', body: 'Describe the UI. Get typed React, Tailwind and a story for every state.' },
{ id: 'rev', label: 'Review', icon: GitPullRequest, title: 'Reviews that read the diff', body: 'Line-level suggestions, explained — accept with one click.' },
{ id: 'test', label: 'Test', icon: ShieldCheck, title: 'Visual tests on every push', body: 'Screenshots across breakpoints, diffs flagged before you merge.' },
{ id: 'ship', label: 'Ship', icon: Rocket, title: 'Preview links in seconds', body: 'Every branch gets a live URL your designer can comment on.' },
];
function TabMock({ id }: { id: string }) {
if (id === 'gen')
return (
<div className="grid gap-3 sm:grid-cols-[1fr_1.2fr]">
<div className="rounded-xl border border-white/10 bg-ink-900/70 p-4 text-left text-[13px] text-ink-300">
<p className="font-mono text-[11px] text-ink-500">prompt</p>
<p className="mt-2 text-ink-100">A pricing card with a yearly toggle and a highlighted Pro plan.</p>
<p className="mt-4 font-mono text-[11px] text-brand-300">✓ PricingCard.tsx · PricingCard.stories.tsx</p>
</div>
<div className="grid grid-cols-3 gap-2 rounded-xl border border-white/10 bg-ink-900/40 p-3">
{['Starter', 'Pro', 'Team'].map((t, i) => (
<div key={t} className={cn('rounded-lg border p-3 text-left', i === 1 ? 'border-brand-300/50 bg-brand-300/[.06]' : 'border-white/10')}>
<p className="text-[11px] text-ink-400">{t}</p>
<p className="mt-1 text-lg font-medium text-ink-50">${[0, 19, 49][i]}</p>
<div className="mt-3 space-y-1.5">
{[0, 1, 2].map((k) => (
<div key={k} className="h-1.5 rounded bg-white/10" style={{ width: `${90 - k * 18}%` }} />
))}
</div>
</div>
))}
</div>
</div>
);
if (id === 'rev')
return (
<div className="rounded-xl border border-white/10 bg-ink-900/70 p-4 text-left font-mono text-[12px] leading-6">
<p className="text-ink-500">Button.tsx</p>
<p className="mt-1 bg-rose-500/10 px-2 text-rose-300">- <button className="btn">{'{label}'}</button></p>
<p className="bg-emerald-500/10 px-2 text-emerald-300">+ <button type="button" aria-label={'{label}'} className="btn"></p>
<p className="mt-3 font-sans text-[12px] text-ink-400">
<span className="text-brand-300">northstar</span> · Buttons without an explicit type submit their parent form. Added type and an accessible label.
</p>
</div>
);
if (id === 'test')
return (
<div className="grid grid-cols-3 gap-3">
{[
['Mobile', 'pass'],
['Tablet', 'diff'],
['Desktop', 'pass'],
].map(([v, s]) => (
<div key={v} className="rounded-xl border border-white/10 bg-ink-900/60 p-3 text-left">
<div className={cn('mx-auto rounded-md border border-white/10 bg-ink-950 p-2', v === 'Mobile' ? 'w-1/2' : v === 'Tablet' ? 'w-3/4' : 'w-full')}>
<div className="h-1.5 w-1/2 rounded bg-white/15" />
<div className={cn('mt-2 h-8 rounded', s === 'diff' ? 'bg-amber-400/25 ring-1 ring-amber-300/50' : 'bg-white/[.06]')} />
</div>
<p className="mt-2 flex items-center justify-between text-[11px] text-ink-400">
{v}
<span className={s === 'diff' ? 'text-amber-300' : 'text-brand-300'}>{s === 'diff' ? '1 change' : 'passed'}</span>
</p>
</div>
))}
</div>
);
return (
<div className="rounded-xl border border-white/10 bg-ink-900/70 p-4 text-left">
<div className="flex items-center gap-2 font-mono text-[12px] text-ink-300">
<Globe className="h-3.5 w-3.5 text-brand-300" /> feat-pricing-card.northstar.app
<span className="ml-auto rounded-full bg-brand-300/10 px-2 py-0.5 text-[10px] text-brand-300 ring-1 ring-brand-300/30">Ready</span>
</div>
<div className="mt-4 flex gap-2">
{['Build 14s', 'Lighthouse 99', '2 comments'].map((x) => (
<span key={x} className="rounded-md border border-white/10 px-2 py-1 text-[11px] text-ink-400">
{x}
</span>
))}
</div>
</div>
);
}
function FeatureTabs() {
const [active, setActive] = useState(0);
const [tick, setTick] = useState(0);
useEffect(() => {
const t = setInterval(() => setTick((n) => n + 1), 60);
return () => clearInterval(t);
}, []);
useEffect(() => {
if (tick >= 100) {
setActive((a) => (a + 1) % TABS.length);
setTick(0);
}
}, [tick]);
const tab = TABS[active];
return (
<Band crosses={false}>
<div className="grid grid-cols-2 border-b border-white/[.07] md:grid-cols-4">
{TABS.map((t, i) => (
<button
key={t.id}
onClick={() => {
setActive(i);
setTick(0);
}}
className={cn(
'relative flex items-center justify-center gap-2 border-white/[.07] py-4 text-[13px] transition-colors [&:not(:last-child)]:border-r',
i === active ? 'text-ink-50' : 'text-ink-500 hover:text-ink-300',
)}
>
<t.icon className={cn('h-3.5 w-3.5', i === active && 'text-brand-300')} />
{t.label}
<span className="absolute inset-x-0 bottom-0 h-px bg-white/[.04]">
{i === active && <span className="block h-full bg-brand-300 shadow-[0_0_8px_#7dd3fc]" style={{ width: `${tick}%` }} />}
</span>
</button>
))}
</div>
<div className="grid items-center gap-10 px-6 py-14 md:grid-cols-[1fr_1.4fr] md:px-12">
<div key={tab.id} className="animate-fade-up text-left">
<h3 className="text-2xl font-medium tracking-[-0.03em] text-ink-50">{tab.title}</h3>
<p className="mt-3 max-w-sm text-[15px] text-ink-400">{tab.body}</p>
</div>
<div key={tab.id + '-m'} className="animate-fade-up">
<TabMock id={tab.id} />
</div>
</div>
</Band>
);
}
/* ─── logos ────────────────────────────────────────────────────────── */
const LOGOS = [
['Halcyon', 'font-serif italic text-[22px]'],
['PARALLAX', 'font-mono tracking-[0.2em] text-[13px]'],
['kite', 'font-semibold text-[21px] lowercase'],
['Monolith', 'font-medium tracking-[-0.04em] text-[19px]'],
['Arcadia', 'font-serif text-[21px]'],
['fieldnote', 'font-mono text-[15px]'],
['QUANTA', 'font-semibold tracking-[0.12em] text-[14px]'],
['Sable&Co', 'font-medium italic text-[18px]'],
];
function Logos() {
return (
<Band>
<div className="grid md:grid-cols-[240px_1fr]">
<p className="flex items-center px-6 py-8 text-[13px] text-ink-400 md:border-r md:border-white/[.07]">
<span>Trusted by product teams that <Accent>ship weekly.</Accent></span>
</p>
<div className="grid grid-cols-2 sm:grid-cols-4">
{LOGOS.map(([name, cls], i) => (
<div
key={name}
className={cn(
'flex h-20 items-center justify-center border-white/[.07] text-ink-300 transition-colors hover:text-ink-50',
i % 4 !== 3 && 'sm:border-r',
i < 4 && 'sm:border-b',
i % 2 === 0 && 'border-r sm:border-r',
i < 6 && 'border-b',
)}
>
<span className={cls}>{name}</span>
</div>
))}
</div>
</div>
</Band>
);
}
/* ─── split rows ───────────────────────────────────────────────────── */
function Split({ title, body, mock, caption, icon: Icon }: { title: ReactNode; body: string; mock: ReactNode; caption: string; icon: typeof Sparkles }) {
return (
<Band>
<div className="grid md:grid-cols-[minmax(0,1fr)_minmax(0,1.55fr)]">
<div className="px-6 py-12 md:border-r md:border-white/[.07] md:px-10 md:py-16">
<Reveal>
<h3 className="text-[28px] font-medium leading-[1.08] tracking-[-0.04em] text-ink-50">{title}</h3>
<p className="mt-4 max-w-xs text-[14.5px] leading-relaxed text-ink-400">{body}</p>
<a href="#" className="group mt-6 inline-flex h-8 items-center gap-1.5 rounded-full border border-white/12 px-3 text-[12.5px] text-ink-200 transition hover:border-brand-300/50">
Learn more <ArrowUpRight className="h-3.5 w-3.5 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" />
</a>
</Reveal>
</div>
<div className="flex flex-col">
<div className="flex min-h-[320px] flex-1 items-center justify-center bg-[radial-gradient(rgba(255,255,255,.05)_1px,transparent_1px)] [background-size:18px_18px] px-6 py-12">
<Reveal className="w-full max-w-md">{mock}</Reveal>
</div>
<p className="flex items-start gap-2 border-t border-white/[.07] px-6 py-4 text-[13px] text-ink-400">
<Icon className="mt-0.5 h-3.5 w-3.5 shrink-0 text-brand-300" />
{caption}
</p>
</div>
</div>
</Band>
);
}
/* ─── statement + stats ────────────────────────────────────────────── */
function Stat({ value, suffix, label, decimals = 0 }: { value: number; suffix: string; label: string; decimals?: number }) {
const { ref, inView } = useInView<HTMLDivElement>(0.4);
const v = useCountUp(value, inView);
return (
<div ref={ref} className="px-6 py-8 text-left">
<p className="text-4xl font-medium tabular-nums tracking-[-0.04em] text-ink-50">
{v.toFixed(decimals)}
<span className="text-brand-300">{suffix}</span>
</p>
<p className="mt-1.5 text-[13px] text-ink-400">{label}</p>
</div>
);
}
function Statement() {
return (
<Band>
<div className="px-6 pb-4 pt-20 text-center">
<Reveal>
<Eyebrow icon={Workflow}>Outcomes</Eyebrow>
<h2 className="mx-auto mt-6 max-w-2xl text-4xl font-medium leading-[1.04] tracking-[-0.05em] text-ink-50 sm:text-6xl">
Less boilerplate. <Accent>More product.</Accent>
</h2>
<p className="mx-auto mt-5 max-w-md text-[15px] text-ink-400">The repetitive 80% is handled. Your team spends its time on the decisions only people can make.</p>
</Reveal>
</div>
<div className="mt-12 grid border-t border-white/[.07] sm:grid-cols-3 [&>*:not(:last-child)]:sm:border-r [&>*:not(:last-child)]:border-white/[.07]">
<Stat value={4.2} decimals={1} suffix="×" label="faster from ticket to merged PR" />
<Stat value={61} suffix="%" label="fewer UI regressions reaching production" />
<Stat value={12} suffix="k+" label="teams running agents every day" />
</div>
</Band>
);
}
/* ─── constellation ────────────────────────────────────────────────── */
function Constellation() {
return (
<Band>
<div className="grid md:grid-cols-[minmax(0,1fr)_minmax(0,1.55fr)]">
<div className="px-6 py-12 md:border-r md:border-white/[.07] md:px-10 md:py-16">
<Reveal>
<h3 className="text-[28px] font-medium leading-[1.08] tracking-[-0.04em] text-ink-50">
From one developer to a <Accent>thousand.</Accent>
</h3>
<p className="mt-4 max-w-xs text-[14.5px] leading-relaxed text-ink-400">Shared memory, shared rules, one review queue. Agents learn your design system once and apply it everywhere.</p>
</Reveal>
</div>
<div className="relative flex items-center justify-center overflow-hidden px-6 py-10">
<NetworkPulse />
<p className="absolute bottom-4 left-6 right-6 flex items-start gap-2 text-[13px] text-ink-400">
<Globe className="mt-0.5 h-3.5 w-3.5 shrink-0 text-brand-300" /> Running in 90+ countries, across 40k repositories.
</p>
</div>
</div>
</Band>
);
}
/* ─── testimonials ─────────────────────────────────────────────────── */
const QUOTES = [
['Our design system finally gets used. Northstar reaches for our tokens before I do.', 'Imani Okafor', 'Design Eng Lead, Halcyon'],
['Review time went from days to an afternoon. The suggestions explain themselves.', 'Luca Bianchi', 'Staff Engineer, Monolith'],
['We shipped the new onboarding in a week with two people.', 'Hana Sato', 'Founder, kite'],
['The visual tests caught a broken checkout on tablet before any customer did.', 'Diego Ramos', 'CTO, Arcadia'],
['It feels like a senior frontend engineer who never gets tired.', 'Priya Nair', 'VP Product, Quanta'],
['Preview links changed how design and engineering talk to each other.', 'Tom Becker', 'Head of Design, fieldnote'],
];
function QuoteCard({ q }: { q: string[] }) {
return (
<figure className="w-[300px] shrink-0 rounded-2xl border border-white/10 bg-ink-900/60 p-5">
<blockquote className="text-[14px] leading-relaxed text-ink-200">“{q[0]}”</blockquote>
<figcaption className="mt-4 flex items-center gap-3">
<span className="grid h-8 w-8 place-items-center rounded-full bg-brand-300/15 text-[11px] font-medium text-brand-200">
{q[1].split(' ').map((p) => p[0]).join('')}
</span>
<span>
<span className="block text-[13px] text-ink-100">{q[1]}</span>
<span className="block text-[11.5px] text-ink-500">{q[2]}</span>
</span>
</figcaption>
</figure>
);
}
function Testimonials() {
const row = (reverse: boolean) => (
<div className="flex overflow-hidden [mask-image:linear-gradient(90deg,transparent,black_10%,black_90%,transparent)]">
<div className={cn('flex w-max gap-4 pr-4 hover:[animation-play-state:paused]', reverse ? '[animation:ns-marquee_46s_linear_infinite_reverse]' : '[animation:ns-marquee_40s_linear_infinite]')}>
{[...QUOTES, ...QUOTES].map((q, i) => (
<QuoteCard key={i} q={q} />
))}
</div>
</div>
);
return (
<Band>
<div className="px-6 pb-12 pt-20 text-center">
<Reveal>
<Eyebrow icon={Sparkles}>Teams</Eyebrow>
<h2 className="mx-auto mt-6 max-w-xl text-4xl font-medium leading-[1.05] tracking-[-0.05em] text-ink-50 sm:text-5xl">
Loved by people who <Accent>ship.</Accent>
</h2>
</Reveal>
</div>
<div className="space-y-4 pb-16">
{row(false)}
{row(true)}
</div>
</Band>
);
}
/* ─── pricing ──────────────────────────────────────────────────────── */
function Pricing() {
const [yearly, setYearly] = useState(true);
const plans = [
{ name: 'Solo', m: 0, y: 0, d: 'For side projects', f: ['1 agent', '50 runs / month', 'Preview links'] },
{ name: 'Team', m: 29, y: 24, d: 'For product teams', f: ['5 parallel agents', 'Unlimited runs', 'Visual tests', 'MCP integrations'], pop: true },
{ name: 'Scale', m: 79, y: 64, d: 'For organisations', f: ['Unlimited agents', 'SSO + audit log', 'Private models', 'Dedicated support'] },
];
return (
<Band>
<div className="grid md:grid-cols-[minmax(0,1fr)_minmax(0,2.2fr)]">
<div className="px-6 py-12 md:border-r md:border-white/[.07] md:px-10 md:py-16">
<h3 className="text-[28px] font-medium leading-[1.08] tracking-[-0.04em] text-ink-50">
Simple pricing. <Accent>No surprises.</Accent>
</h3>
<p className="mt-4 max-w-xs text-[14.5px] text-ink-400">Start free. Upgrade when the agents earn it.</p>
<div className="mt-6 inline-flex rounded-full border border-white/12 p-[3px] text-[12.5px]">
{['Monthly', 'Yearly'].map((l) => (
<button key={l} onClick={() => setYearly(l === 'Yearly')} className={cn('rounded-full px-3.5 py-1.5 transition', (l === 'Yearly') === yearly ? 'bg-brand-300 font-medium text-brand-950' : 'text-ink-400')}>
{l}
</button>
))}
</div>
</div>
<div className="grid sm:grid-cols-3 [&>*:not(:last-child)]:sm:border-r [&>*:not(:last-child)]:border-b [&>*:not(:last-child)]:sm:border-b-0 [&>*]:border-white/[.07]">
{plans.map((p) => (
<div key={p.name} className={cn('relative flex flex-col p-6', p.pop && 'bg-[radial-gradient(90%_60%_at_50%_0%,rgba(56,189,248,.12),transparent)]')}>
{p.pop && <span className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-brand-300 to-transparent" />}
<p className="flex items-center justify-between text-[14px] font-medium text-ink-50">
{p.name}
{p.pop && <span className="rounded-full bg-brand-300/10 px-2 py-0.5 text-[10.5px] font-normal text-brand-300 ring-1 ring-brand-300/30">Popular</span>}
</p>
<p className="mt-4 text-4xl font-medium tracking-[-0.04em] text-ink-50">
${yearly ? p.y : p.m}
<span className="ml-1 text-[13px] font-normal tracking-normal text-ink-500">/ seat / mo</span>
</p>
<p className="mt-1 text-[13px] text-ink-400">{p.d}</p>
<ul className="mt-6 flex-1 space-y-2.5 text-[13px] text-ink-300">
{p.f.map((f) => (
<li key={f} className="flex items-center gap-2">
<Check className="h-3.5 w-3.5 text-brand-300" /> {f}
</li>
))}
</ul>
<a href="#" className={cn('mt-7 inline-flex h-9 items-center justify-center rounded-full text-[13px] font-medium transition', p.pop ? 'bg-brand-300 text-brand-950 shadow-glow hover:bg-brand-200' : 'border border-white/15 text-ink-100 hover:border-white/30')}>
{p.m === 0 ? 'Start free' : `Choose ${p.name}`}
</a>
</div>
))}
</div>
</div>
</Band>
);
}
/* ─── faq + cta + footer ───────────────────────────────────────────── */
const FAQS = [
['What does an agent actually change?', 'Only what the ticket asks for. Every change arrives as a pull request with a plain-language summary — nothing merges without a human.'],
['Does it follow our design system?', 'Yes. Point Northstar at your tokens and components once; agents reuse them instead of inventing new styles.'],
['Which stacks are supported?', 'React, Next.js, Vue and Svelte with Tailwind, CSS Modules or vanilla-extract. More arrive monthly.'],
['Where does our code go?', 'Runs execute in isolated sandboxes and are deleted afterwards. Scale plans can bring their own model and region.'],
['Can we try it on a real repo?', 'The Solo plan is free forever and works on private repositories.'],
];
function Faq() {
const [open, setOpen] = useState(0);
return (
<Band>
<div className="grid md:grid-cols-[minmax(0,1fr)_minmax(0,2.2fr)]">
<div className="px-6 py-12 md:border-r md:border-white/[.07] md:px-10 md:py-16">
<h3 className="text-[28px] font-medium leading-[1.08] tracking-[-0.04em] text-ink-50">
Questions, <Accent>answered.</Accent>
</h3>
<p className="mt-4 max-w-xs text-[14.5px] text-ink-400">Anything else? Our engineers answer support themselves.</p>
</div>
<div className="px-6 py-6 md:px-10">
{FAQS.map(([q, a], i) => (
<div key={q} className="border-b border-white/[.07] last:border-0">
<button onClick={() => setOpen(open === i ? -1 : i)} className="flex w-full items-center justify-between gap-6 py-5 text-left text-[15px] text-ink-100 transition hover:text-brand-200">
{q}
<ChevronDown className={cn('h-4 w-4 shrink-0 text-ink-500 transition-transform duration-300', open === i && 'rotate-180 text-brand-300')} />
</button>
<div className={cn('grid transition-all duration-300 ease-smooth', open === i ? 'grid-rows-[1fr] pb-5 opacity-100' : 'grid-rows-[0fr] opacity-0')}>
<p className="overflow-hidden text-[14px] leading-relaxed text-ink-400">{a}</p>
</div>
</div>
))}
</div>
</div>
</Band>
);
}
function Closing() {
return (
<Band>
<div className="relative overflow-hidden px-6 pb-0 pt-24 text-center">
<Reveal>
<h2 className="mx-auto max-w-2xl text-4xl font-medium leading-[1.04] tracking-[-0.05em] text-ink-50 sm:text-6xl">
Ship your next feature <Accent>tonight.</Accent>
</h2>
<p className="mx-auto mt-5 max-w-md text-[15px] text-ink-400">Connect a repository and hand Northstar its first ticket. Free forever for solo builders.</p>
<a href="#" className="group mt-9 inline-flex h-11 items-center gap-2 rounded-full bg-brand-300 px-5 text-sm font-medium text-brand-950 shadow-glow transition hover:bg-brand-200">
Start for free <ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
</a>
</Reveal>
<div aria-hidden className="relative mt-16 h-40 [mask-image:linear-gradient(black_45%,transparent)]">
<div className="absolute left-1/2 top-14 h-[1600px] w-[1600px] -translate-x-1/2 rounded-full border-t border-brand-200/70 bg-ink-950 shadow-[0_-1px_40px_rgba(125,211,252,.45)]" />
<div className="absolute left-1/2 top-0 h-40 w-[60%] -translate-x-1/2 bg-[radial-gradient(50%_60%_at_50%_80%,rgba(56,189,248,.3),transparent_70%)] blur-2xl" />
</div>
</div>
</Band>
);
}
function Footer() {
const cols: [string, string[]][] = [
['Product', ['Agents', 'Review', 'Visual tests', 'Preview links', 'Changelog']],
['Developers', ['Docs', 'API', 'MCP servers', 'Status', 'Security']],
['Company', ['About', 'Careers', 'Blog', 'Contact', 'Legal']],
];
return (
<Band>
<div className="grid gap-10 px-6 py-14 md:grid-cols-[1.3fr_repeat(3,1fr)] md:px-10">
<div>
<Logo />
<p className="mt-3 max-w-[220px] text-[13px] text-ink-500">AI agents for frontend teams who care about craft.</p>
</div>
{cols.map(([t, links]) => (
<div key={t}>
<p className="text-[13px] font-medium text-ink-100">{t}</p>
<ul className="mt-4 space-y-2.5 text-[13px] text-ink-500">
{links.map((l) => (
<li key={l}>
<a href="#" className="transition-colors hover:text-ink-100">
{l}
</a>
</li>
))}
</ul>
</div>
))}
</div>
<div className="flex items-center justify-between border-t border-white/[.07] px-6 py-5 text-[12px] text-ink-500 md:px-10">
<span>© 2026 Northstar Labs</span>
<span className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-brand-300 shadow-[0_0_8px_#7dd3fc]" /> All systems normal
</span>
</div>
</Band>
);
}
/* ─── page ─────────────────────────────────────────────────────────── */
export function LandingPageNorthstar(_props: PageProps) {
return (
<div className="min-h-screen bg-ink-950 font-sans text-ink-100 antialiased">
<style>{`
@keyframes ns-marquee { to { transform: translateX(-50%); } }
@keyframes ns-grain { to { background-position: 70px 0, -77px 4px; } }
@keyframes ns-breathe { 50% { opacity: .55; } }
`}</style>
<Nav />
<main className="mx-auto max-w-6xl border-x border-white/[.07]">
<Hero />
<FeatureTabs />
<Logos />
<Statement />
<Grain />
<Split
icon={Terminal}
title={
<>
From ticket to <Accent>pull request.</Accent>
</>
}
body="Assign a ticket and watch the plan, the code and the tests come together — in the open, step by step."
mock={<AgentTerminal {...terminalDemoProps} />}
caption="Agents work in your repository, on a branch, with a full log of every decision."
/>
<Split
icon={GitPullRequest}
title={
<>
Review that <Accent>reads</Accent> the diff.
</>
}
body="Suggestions arrive inline with the reason attached. Accept, tweak or ignore — you stay in charge."
mock={<DiffSuggestion {...diffDemoProps} />}
caption="Click Apply suggestion to see the fix land."
/>
<Grain />
<Split
icon={Plug}
title={
<>
Plug in <Accent>every</Accent> tool.
</>
}
body="Figma, GitHub, Linear, Storybook and hundreds of MCP servers. Agents use the same context your team does."
mock={<McpToolPicker tools={toolDemo} />}
caption="Search and toggle servers — integrations install with a single click."
/>
<Split
icon={Plus}
title={
<>
Many agents, <Accent>one</Accent> queue.
</>
}
body="Run design, build and test agents in parallel. Track progress and token use in one place."
mock={<ParallelAgents agents={agentsDemo} />}
caption="Parallel runs finish together and land as a single, reviewable PR."
/>
<Constellation />
<Testimonials />
<Pricing />
<Faq />
<Closing />
<Footer />
</main>
</div>
);
}components/niceui/pages/landing/shared/motion.tsx
import { type ReactNode, useEffect, useRef, useState } from 'react';
import { cn } from '@/lib/cn';
export function useInView<T extends Element>(threshold = 0.2) {
const ref = useRef<T>(null);
const [inView, setInView] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setInView(true);
io.disconnect();
}
},
{ threshold },
);
io.observe(el);
return () => io.disconnect();
}, [threshold]);
return { ref, inView };
}
export function Reveal({ children, delay = 0, className }: { children: ReactNode; delay?: number; className?: string }) {
const { ref, inView } = useInView<HTMLDivElement>(0.15);
return (
<div
ref={ref}
className={cn('transition-all duration-1000 ease-smooth', inView ? 'translate-y-0 opacity-100 blur-0' : 'translate-y-6 opacity-0 blur-[2px]', className)}
style={{ transitionDelay: `${delay}ms` }}
>
{children}
</div>
);
}
export function useCountUp(target: number, active: boolean, duration = 1800) {
const [val, setVal] = useState(0);
useEffect(() => {
if (!active) return;
let raf = 0;
const start = performance.now();
const loop = (now: number) => {
const t = Math.min(1, (now - start) / duration);
setVal(target * (1 - Math.pow(2, -10 * t)));
if (t < 1) raf = requestAnimationFrame(loop);
else setVal(target);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, [target, active, duration]);
return val;
}components/niceui/ai/AgentWorkflow.tsx
import { type ComponentType, type ReactNode, useEffect, useState } from 'react';
import { Bot, RotateCcw, Search, Terminal } from 'lucide-react';
import { cn } from '@/lib/cn';
import { useInView } from '@/components/niceui/pages/landing/shared/motion';
/* ─── AgentTerminal ────────────────────────────────────────────────── */
export type AgentTerminalProps = {
command: string;
lines: string[];
/** Milliseconds between lines. */
interval?: number;
label?: string;
className?: string;
};
/** A terminal that types out an agent's run log line by line once it scrolls into view. */
export function AgentTerminal({ command, lines, interval = 650, label = 'agent', className }: AgentTerminalProps) {
const { ref, inView } = useInView<HTMLDivElement>(0.3);
const [n, setN] = useState(0);
useEffect(() => {
if (!inView || n >= lines.length) return;
const t = setTimeout(() => setN((x) => x + 1), interval);
return () => clearTimeout(t);
}, [inView, n, lines.length, interval]);
const done = n >= lines.length;
return (
<div ref={ref} className={cn('w-full overflow-hidden rounded-xl border border-white/10 bg-ink-900/80 shadow-pop', className)}>
<div className="flex items-center gap-1.5 border-b border-white/[.07] px-3 py-2.5">
<span className="h-2.5 w-2.5 rounded-full bg-white/15" />
<span className="h-2.5 w-2.5 rounded-full bg-white/15" />
<span className="h-2.5 w-2.5 rounded-full bg-white/15" />
<span className="ml-auto inline-flex items-center gap-1.5 rounded-full bg-brand-300/10 px-2 py-0.5 text-[10px] text-brand-300">
<Terminal className="h-3 w-3" /> {label}
</span>
{done && (
<button onClick={() => setN(0)} aria-label="Replay" className="rounded-md p-1 text-ink-500 transition hover:bg-white/5 hover:text-ink-100">
<RotateCcw className="h-3 w-3" />
</button>
)}
</div>
<div className="min-h-[160px] space-y-1.5 p-4 text-left font-mono text-[12px] leading-relaxed">
<p className="text-ink-200">
<span className="text-brand-300">$</span> {command}
</p>
{lines.slice(0, n).map((l, i) => (
<p key={i} className={cn('animate-fade-in', i === lines.length - 1 ? 'text-brand-300' : 'text-ink-400')}>
{l}
</p>
))}
{!done && <span className="inline-block h-3.5 w-2 animate-blink bg-brand-300" />}
</div>
</div>
);
}
/* ─── DiffSuggestion ───────────────────────────────────────────────── */
export type DiffSuggestionProps = {
file: string;
before: string[];
after: string[];
/** Context lines shown above and below the change. */
head?: string;
tail?: string;
issue: string;
onApply?: (applied: boolean) => void;
className?: string;
};
/** An inline code-review suggestion that swaps removed lines for added lines when applied. */
export function DiffSuggestion({ file, before, after, head, tail, issue, onApply, className }: DiffSuggestionProps) {
const [applied, setApplied] = useState(false);
const toggle = () => {
setApplied((a) => !a);
onApply?.(!applied);
};
return (
<div className={cn('w-full overflow-hidden rounded-xl border border-white/10 bg-ink-900/80 text-left shadow-pop', className)}>
<div className="flex items-center justify-between border-b border-white/[.07] px-4 py-2.5 font-mono text-[11px] text-ink-500">
{file}
<span className="text-ink-400">
<span className="text-emerald-300">+{after.length}</span> <span className="text-rose-300">−{before.length}</span>
</span>
</div>
<div className="p-3 font-mono text-[11.5px] leading-6">
{head && <p className="px-2 text-ink-400">{head}</p>}
{(applied ? after : before).map((l, i) => (
<p
key={(applied ? 'a' : 'b') + i}
className={cn('animate-fade-in px-2', applied ? 'bg-emerald-500/10 text-emerald-300' : 'bg-rose-500/10 text-rose-300/90 line-through decoration-rose-300/40')}
>
{applied ? '+ ' : '- '}
{l}
</p>
))}
{tail && <p className="px-2 text-ink-400">{tail}</p>}
</div>
<div className="flex items-center justify-between gap-3 border-t border-white/[.07] px-4 py-3">
<span className="text-[12px] text-ink-400">{applied ? 'Suggestion applied' : issue}</span>
<button
onClick={toggle}
className={cn(
'shrink-0 rounded-full px-3 py-1.5 text-[12px] font-medium transition',
applied ? 'border border-white/15 text-ink-300 hover:border-white/30' : 'bg-brand-300 text-brand-950 shadow-glow hover:bg-brand-200',
)}
>
{applied ? 'Undo' : 'Apply suggestion'}
</button>
</div>
</div>
);
}
/* ─── McpToolPicker ────────────────────────────────────────────────── */
export type McpTool = { name: string; installed?: boolean; hint?: string };
export type McpToolPickerProps = {
tools: McpTool[];
placeholder?: string;
onChange?: (tools: McpTool[]) => void;
className?: string;
};
/** A searchable list of MCP servers with one-click install toggles. */
export function McpToolPicker({ tools: initial, placeholder = 'Search MCP servers…', onChange, className }: McpToolPickerProps) {
const [tools, setTools] = useState(initial);
const [q, setQ] = useState('');
const shown = tools.filter((t) => t.name.toLowerCase().includes(q.toLowerCase()));
const toggle = (name: string) => {
const next = tools.map((t) => (t.name === name ? { ...t, installed: !t.installed } : t));
setTools(next);
onChange?.(next);
};
return (
<div className={cn('w-full overflow-hidden rounded-xl border border-white/10 bg-ink-900/80 text-left shadow-pop', className)}>
<div className="flex items-center gap-2 border-b border-white/[.07] px-3.5 py-3 text-ink-500">
<Search className="h-3.5 w-3.5" />
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder={placeholder} className="min-w-0 flex-1 bg-transparent text-[13px] text-ink-100 outline-none placeholder:text-ink-500" />
<span className="font-mono text-[10.5px] text-ink-500">{tools.filter((t) => t.installed).length} on</span>
</div>
<ul className="p-1.5">
{shown.length === 0 && <li className="px-3 py-6 text-center text-[13px] text-ink-500">No servers match “{q}”</li>}
{shown.map((t) => (
<li key={t.name} className="flex items-center justify-between rounded-lg px-2.5 py-2 text-[13px] text-ink-200 transition-colors hover:bg-white/[.03]">
<span className="flex min-w-0 items-center gap-2.5">
<span className="grid h-6 w-6 shrink-0 place-items-center rounded-md border border-white/10 font-mono text-[10px] text-ink-400">{t.name[0]}</span>
<span className="truncate">{t.name}</span>
{t.hint && <span className="hidden truncate text-[11.5px] text-ink-500 sm:inline">{t.hint}</span>}
</span>
<button
onClick={() => toggle(t.name)}
className={cn(
'shrink-0 rounded-full px-2.5 py-0.5 text-[11px] transition',
t.installed ? 'bg-brand-300/10 text-brand-300 ring-1 ring-brand-300/30' : 'text-ink-400 ring-1 ring-white/15 hover:text-ink-100',
)}
>
{t.installed ? 'Installed' : 'Install'}
</button>
</li>
))}
</ul>
</div>
);
}
/* ─── ParallelAgents ───────────────────────────────────────────────── */
export type AgentRun = { who: string; task: string; progress: number; tokens?: string };
/** Agents running side by side on a shared timeline, progress bars fill when in view. */
export function ParallelAgents({ agents, className }: { agents: AgentRun[]; className?: string }) {
const { ref, inView } = useInView<HTMLDivElement>(0.3);
return (
<div ref={ref} className={cn('relative w-full space-y-3 text-left', className)}>
<span aria-hidden className="absolute bottom-6 left-[22px] top-6 w-px bg-gradient-to-b from-brand-300/60 via-brand-300/20 to-transparent" />
{agents.map((a, i) => (
<div
key={a.who}
className={cn('relative ml-10 rounded-xl border border-white/10 bg-ink-900/80 p-3.5 shadow-pop transition-all duration-700 ease-smooth', inView ? 'translate-x-0 opacity-100' : 'translate-x-3 opacity-0')}
style={{ transitionDelay: `${i * 140}ms` }}
>
<span aria-hidden className="absolute -left-[26px] top-1/2 h-2.5 w-2.5 -translate-y-1/2 rounded-full bg-brand-300 shadow-[0_0_10px_#7dd3fc]" />
<div className="flex items-center justify-between gap-3 font-mono text-[11.5px]">
<span className="flex min-w-0 items-center gap-1.5 truncate text-ink-100">
<Bot className="h-3.5 w-3.5 shrink-0 text-brand-300" /> {a.who}
</span>
{a.tokens && <span className="shrink-0 text-ink-500">{a.tokens} tokens</span>}
</div>
<p className="mt-1.5 text-[12px] text-ink-400">{a.task}…</p>
<div className="mt-2.5 h-1 overflow-hidden rounded-full bg-white/[.06]">
<div className="h-full rounded-full bg-brand-300 transition-[width] duration-[1600ms] ease-smooth" style={{ width: inView ? `${a.progress}%` : '0%', transitionDelay: `${300 + i * 140}ms` }} />
</div>
</div>
))}
</div>
);
}
/* ─── FeatureTabs ──────────────────────────────────────────────────── */
export type FeatureTab = { label: string; icon?: ComponentType<{ className?: string }>; title: string; body: string; content: ReactNode };
/** Auto-advancing feature tabs with a progress underline; click any tab to jump. */
export function FeatureTabs({ tabs, duration = 6000, className }: { tabs: FeatureTab[]; duration?: number; className?: string }) {
const [active, setActive] = useState(0);
const [pct, setPct] = useState(0);
useEffect(() => {
const start = performance.now();
let raf = 0;
const loop = (now: number) => {
const p = Math.min(100, ((now - start) / duration) * 100);
setPct(p);
if (p >= 100) setActive((a) => (a + 1) % tabs.length);
else raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, [active, duration, tabs.length]);
const tab = tabs[active];
return (
<div className={cn('w-full', className)}>
<div className="grid grid-cols-2 border-b border-white/[.07] md:grid-cols-4">
{tabs.map((t, i) => {
const Icon = t.icon;
return (
<button
key={t.label}
onClick={() => setActive(i)}
className={cn(
'relative flex items-center justify-center gap-2 border-white/[.07] py-4 text-[13px] transition-colors [&:not(:last-child)]:border-r',
i === active ? 'text-ink-50' : 'text-ink-500 hover:text-ink-300',
)}
>
{Icon && <Icon className={cn('h-3.5 w-3.5', i === active && 'text-brand-300')} />}
{t.label}
<span className="absolute inset-x-0 bottom-0 h-px bg-white/[.04]">
{i === active && <span className="block h-full bg-brand-300 shadow-[0_0_8px_#7dd3fc]" style={{ width: `${pct}%` }} />}
</span>
</button>
);
})}
</div>
<div className="grid items-center gap-10 px-6 py-12 md:grid-cols-[1fr_1.4fr] md:px-10">
<div key={tab.label} className="animate-fade-up text-left">
<h3 className="text-2xl font-medium tracking-[-0.03em] text-ink-50">{tab.title}</h3>
<p className="mt-3 max-w-sm text-[15px] text-ink-400">{tab.body}</p>
</div>
<div key={tab.label + '-content'} className="animate-fade-up">
{tab.content}
</div>
</div>
</div>
);
}
/* ─── NetworkPulse ─────────────────────────────────────────────────── */
/** A hub-and-spoke constellation with pulses travelling from the core to each node. */
export function NetworkPulse({ nodes = 9, className }: { nodes?: number; className?: string }) {
const points = Array.from({ length: nodes }, (_, i) => {
const a = (i / nodes) * Math.PI * 2 + 0.3;
const r = i % 2 ? 118 : 160;
return { x: 200 + Math.cos(a) * r, y: 170 + Math.sin(a) * r * 0.78, d: i * 0.35 };
});
return (
<svg viewBox="0 0 400 340" className={cn('w-full max-w-md', className)} aria-hidden>
<defs>
<radialGradient id="np-core">
<stop offset="0" stopColor="#7dd3fc" stopOpacity=".55" />
<stop offset="1" stopColor="#7dd3fc" stopOpacity="0" />
</radialGradient>
</defs>
<circle cx="200" cy="170" r="90" fill="url(#np-core)" />
{points.map((n, i) => (
<g key={i}>
<line x1="200" y1="170" x2={n.x} y2={n.y} stroke="rgba(255,255,255,.12)" />
<circle r="2" fill="#7dd3fc">
<animateMotion dur={`${2.4 + (i % 3) * 0.6}s`} begin={`${n.d}s`} repeatCount="indefinite" path={`M200,170 L${n.x},${n.y}`} />
</circle>
<circle cx={n.x} cy={n.y} r="5.5" fill="#0b0c0e" stroke="rgba(125,211,252,.7)">
<animate attributeName="r" values="5.5;7;5.5" dur="3s" begin={`${n.d}s`} repeatCount="indefinite" />
</circle>
</g>
))}
<circle cx="200" cy="170" r="11" fill="#0b0c0e" stroke="#7dd3fc" strokeWidth="1.5" />
<circle cx="200" cy="170" r="3.5" fill="#7dd3fc" />
</svg>
);
}
/* ─── demos (used by the docs registry) ────────────────────────────── */
export const terminalDemoProps: AgentTerminalProps = {
command: 'northstar run NS-482',
lines: ['Reading ticket NS-482 …', 'Planning 3 components', 'Writing PricingCard.tsx', 'Running visual tests · 12/12', '✓ Opened PR #318 — ready for review'],
};
export const diffDemoProps: DiffSuggestionProps = {
file: 'useCart.ts',
head: 'export function useCart() {',
tail: '}',
before: ['const [items, setItems] = useState([])', 'useEffect(() => save(items))'],
after: ['const [items, setItems] = useState<Item[]>(load)', 'useEffect(() => save(items), [items])'],
issue: 'Missing dependency array',
};
export const toolDemo: McpTool[] = [
{ name: 'Figma', installed: true, hint: 'Design files' },
{ name: 'GitHub', installed: true, hint: 'Repos & PRs' },
{ name: 'Linear', hint: 'Issues' },
{ name: 'Storybook', installed: true, hint: 'Stories' },
{ name: 'Sentry', hint: 'Errors' },
];
export const agentsDemo: AgentRun[] = [
{ who: 'designer@team', task: 'Drafting variants', progress: 82, tokens: '18k' },
{ who: 'builder@team', task: 'Writing components', progress: 54, tokens: '31k' },
{ who: 'tester@team', task: 'Capturing screenshots', progress: 27, tokens: '6k' },
];
export const AgentTerminalDemo = () => (
<div className="w-full max-w-md">
<AgentTerminal {...terminalDemoProps} />
</div>
);
export const DiffSuggestionDemo = () => (
<div className="w-full max-w-md">
<DiffSuggestion {...diffDemoProps} />
</div>
);
export const McpToolPickerDemo = () => (
<div className="w-full max-w-sm">
<McpToolPicker tools={toolDemo} />
</div>
);
export const ParallelAgentsDemo = () => (
<div className="w-full max-w-md">
<ParallelAgents agents={agentsDemo} />
</div>
);
export const NetworkPulseDemo = () => <NetworkPulse />;
export const FeatureTabsDemo = () => (
<div className="w-full overflow-hidden rounded-2xl border border-white/[.07] bg-ink-950">
<FeatureTabs
duration={5000}
tabs={[
{ label: 'Generate', title: 'Components from a sentence', body: 'Describe the UI and get typed React with stories.', content: <AgentTerminal {...terminalDemoProps} /> },
{ label: 'Review', title: 'Reviews that read the diff', body: 'Line-level suggestions you can apply in one click.', content: <DiffSuggestion {...diffDemoProps} /> },
{ label: 'Integrate', title: 'Plug in every tool', body: 'Hundreds of MCP servers, one toggle away.', content: <McpToolPicker tools={toolDemo} /> },
{ label: 'Scale', title: 'Many agents, one queue', body: 'Parallel runs with live progress and token use.', content: <ParallelAgents agents={agentsDemo} /> },
]}
/>
</div>
);