Inital commit

This commit is contained in:
2026-06-26 17:50:39 -05:00
commit 645572b2f4
33 changed files with 6372 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
node_modules
dist
dist-ssr
*.local
.DS_Store
.vscode/*
!.vscode/extensions.json
.idea
*.log
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
+29
View File
@@ -0,0 +1,29 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
},
);
+21
View File
@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/cass-logo-transparent.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0b0d10" />
<meta name="description" content="Cassady — a clean, simple terminal coding agent written in Rust. Install with npm install -g cassady, then run cass." />
<meta property="og:title" content="Cassady — terminal coding agent" />
<meta property="og:description" content="A clean, simple terminal coding agent written in Rust." />
<meta property="og:image" content="/cass-banner.png" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="/cass-banner.png" />
<title>Cassady — terminal coding agent</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+4710
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
{
"name": "cassady-landing",
"private": true,
"version": "0.3.5",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"typecheck": "tsc -b --noEmit",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/ibm-plex-sans": "^5.2.8",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.469.0",
"motion": "^11.15.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^2.6.0"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@tailwindcss/vite": "^4.0.0",
"@types/node": "^22.10.5",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@typescript-eslint/eslint-plugin": "^8.19.1",
"@typescript-eslint/parser": "^8.19.1",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.15.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.62.0",
"vite": "^6.0.7"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 KiB

+26
View File
@@ -0,0 +1,26 @@
import { TooltipProvider } from "@/components/ui/tooltip";
import { Nav } from "@/components/Nav";
import { Hero } from "@/components/Hero";
import { TerminalDemo } from "@/components/TerminalDemo";
import { Features } from "@/components/Features";
import { HowItWorks } from "@/components/HowItWorks";
import { Faq } from "@/components/Faq";
import { Footer } from "@/components/Footer";
export default function App() {
return (
<TooltipProvider delayDuration={150}>
<div className="vignette scanlines grain relative min-h-screen bg-[var(--color-bg)]">
<Nav />
<main className="relative z-10">
<Hero />
<TerminalDemo />
<Features />
<HowItWorks />
<Faq />
</main>
<Footer />
</div>
</TooltipProvider>
);
}
+62
View File
@@ -0,0 +1,62 @@
import { motion } from "motion/react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { SectionHeading, SectionLabel } from "@/components/Features";
const faqs = [
{
q: "Is Cassady free and open source?",
a: "Yes. Cassady is open source under the MIT license. The source lives on GitHub — link above. The npm package is and will stay free.",
},
{
q: "Do I need Rust installed?",
a: "No. The npm package ships a prebuilt Rust binary for your platform, so `npm install -g cassady` is all you need. You never touch a Rust toolchain.",
},
{
q: "Which models does Cassady support?",
a: "Cassady works with any OpenAI-compatible endpoint. Point it at GPT, Claude, or a local model via your API key. Defaults are sane; you can override everything.",
},
{
q: "Does it modify my files without asking?",
a: "Cassady plans before it edits and shows you the diff. For destructive or ambiguous changes it asks for confirmation. You're always one keystroke from saying no.",
},
{
q: "What systems does it run on?",
a: "macOS, Linux, and Windows. Anywhere your shell runs and Node can install an npm package — tmux, iTerm, kitty, Alacritty, Windows Terminal, SSH sessions.",
},
{
q: "How is it different from other coding agents?",
a: "Cassady is deliberately small. One binary, one job, no editor to learn. If you want a 200MB IDE with an agent bolted on, this isn't it.",
},
];
export function Faq() {
return (
<section id="faq" className="mx-auto max-w-3xl px-6 py-20">
<SectionLabel>faq</SectionLabel>
<SectionHeading>Questions, answered.</SectionHeading>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
className="mt-10"
>
<Accordion type="single" collapsible className="border-t border-[var(--color-line)]">
{faqs.map((f, i) => (
<AccordionItem key={i} value={`item-${i}`}>
<AccordionTrigger>{f.q}</AccordionTrigger>
<AccordionContent>{f.a}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</motion.div>
</section>
);
}
+88
View File
@@ -0,0 +1,88 @@
import { motion } from "motion/react";
import { Boxes, Feather, TerminalSquare, Package } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
const features = [
{
icon: Boxes,
title: "Written in Rust",
body: "A single fast binary. Low memory, instant startup, and rock-solid reliability — no Node runtime required to run the agent itself.",
glyph: "[rs]",
},
{
icon: Feather,
title: "Clean & simple UX",
body: "Opinionated defaults, minimal friction. Cassady asks only what it needs and gets out of your way. No dashboards, no config sprawl.",
glyph: "[ux]",
},
{
icon: TerminalSquare,
title: "Runs in your terminal",
body: "No editor lock-in. Works wherever your shell works — tmux, iTerm, kitty, SSH. Your workflow, your keybindings, your theme.",
glyph: "[tm]",
},
{
icon: Package,
title: "npm-installable",
body: "One command to install, one to run. Ship it through npm and the Rust binary lands in your PATH without a toolchain to manage.",
glyph: "[np]",
},
];
export function Features() {
return (
<section id="features" className="mx-auto max-w-6xl px-6 py-20">
<SectionLabel>features</SectionLabel>
<SectionHeading>What you get out of the box.</SectionHeading>
<div className="mt-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{features.map((f, i) => (
<motion.div
key={f.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.5, delay: i * 0.08, ease: [0.22, 1, 0.36, 1] }}
>
<Card className="group h-full transition-all duration-300 hover:-translate-y-1 hover:border-[var(--color-accent)]/40 hover:bg-[var(--color-bg-elev)]/50">
<CardHeader>
<div className="mb-4 flex items-center justify-between">
<div className="flex h-9 w-9 items-center justify-center border border-[var(--color-line)] bg-[var(--color-bg)] text-[var(--color-accent)] transition-colors group-hover:border-[var(--color-accent)]/50">
<f.icon className="h-4 w-4" />
</div>
<span className="font-mono text-[10px] uppercase tracking-[0.16em] text-[var(--color-fg-dim)]">
{f.glyph}
</span>
</div>
<CardTitle>{f.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="font-sans text-sm leading-relaxed text-[var(--color-fg-muted)]">
{f.body}
</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
</section>
);
}
export function SectionLabel({ children }: { children: React.ReactNode }) {
return (
<div className="mb-3 flex items-center gap-2 font-mono text-xs uppercase tracking-[0.22em] text-[var(--color-fg-dim)]">
<span className="text-[var(--color-accent)]">//</span>
<span>{children}</span>
</div>
);
}
export function SectionHeading({ children }: { children: React.ReactNode }) {
return (
<h2 className="max-w-2xl text-balance font-mono text-3xl font-medium leading-tight tracking-tight text-[var(--color-fg)] sm:text-4xl">
{children}
</h2>
);
}
+110
View File
@@ -0,0 +1,110 @@
import { Github, Twitter, FileText } from "lucide-react";
const columns = [
{
heading: "product",
links: [
{ label: "Features", href: "#features" },
{ label: "How it works", href: "#how" },
{ label: "FAQ", href: "#faq" },
{ label: "Changelog", href: "#" },
],
},
{
heading: "resources",
links: [
{ label: "Docs", href: "#" },
{ label: "npm package", href: "#" },
{ label: "GitHub", href: "#" },
{ label: "Report a bug", href: "#" },
],
},
{
heading: "connect",
links: [
{ label: "Twitter / X", href: "#" },
{ label: "Discord", href: "#" },
{ label: "Blog", href: "#" },
{ label: "Contact", href: "#" },
],
},
];
export function Footer() {
return (
<footer className="relative mt-10 border-t border-[var(--color-line)]">
<div className="mx-auto max-w-6xl px-6 py-14">
<div className="grid gap-10 md:grid-cols-[1.5fr_1fr_1fr_1fr]">
<div>
<a href="#top" className="flex items-center gap-2.5">
<img
src="/cass-logo-transparent.png"
alt="Cassady"
className="h-7 w-7"
/>
<span className="font-mono text-base font-medium tracking-tight text-[var(--color-fg)]">
cassady
</span>
</a>
<p className="mt-4 max-w-xs font-sans text-sm text-[var(--color-fg-muted)]">
A clean, simple terminal coding agent. Written in Rust, installed
with npm.
</p>
<div className="mt-5 flex gap-2">
<a
href="#"
aria-label="GitHub"
className="flex h-8 w-8 items-center justify-center border border-[var(--color-line)] text-[var(--color-fg-muted)] transition-colors hover:border-[var(--color-accent)]/50 hover:text-[var(--color-accent)]"
>
<Github className="h-4 w-4" />
</a>
<a
href="#"
aria-label="Twitter"
className="flex h-8 w-8 items-center justify-center border border-[var(--color-line)] text-[var(--color-fg-muted)] transition-colors hover:border-[var(--color-accent)]/50 hover:text-[var(--color-accent)]"
>
<Twitter className="h-4 w-4" />
</a>
<a
href="#"
aria-label="Docs"
className="flex h-8 w-8 items-center justify-center border border-[var(--color-line)] text-[var(--color-fg-muted)] transition-colors hover:border-[var(--color-accent)]/50 hover:text-[var(--color-accent)]"
>
<FileText className="h-4 w-4" />
</a>
</div>
</div>
{columns.map((col) => (
<div key={col.heading}>
<h4 className="mb-4 font-mono text-xs uppercase tracking-[0.2em] text-[var(--color-fg-dim)]">
{col.heading}
</h4>
<ul className="space-y-2.5">
{col.links.map((l) => (
<li key={l.label}>
<a
href={l.href}
className="font-sans text-sm text-[var(--color-fg-muted)] transition-colors hover:text-[var(--color-accent)]"
>
{l.label}
</a>
</li>
))}
</ul>
</div>
))}
</div>
<div className="mt-12 flex flex-col items-center justify-between gap-4 border-t border-[var(--color-line)] pt-6 sm:flex-row">
<p className="font-mono text-xs text-[var(--color-fg-dim)]">
© {new Date().getFullYear()} cassady — MIT license
</p>
<p className="font-mono text-xs text-[var(--color-fg-dim)]">
built in rust · shipped with npm
</p>
</div>
</div>
</footer>
);
}
+106
View File
@@ -0,0 +1,106 @@
import { motion } from "motion/react";
import { ArrowRight, BookOpen } from "lucide-react";
import { Button } from "@/components/ui/button";
import { InstallCommand } from "@/components/InstallCommand";
const reveal = {
hidden: { opacity: 0, y: 18 },
show: (i: number) => ({
opacity: 1,
y: 0,
transition: { duration: 0.6, delay: 0.1 + i * 0.08, ease: [0.22, 1, 0.36, 1] },
}),
};
export function Hero() {
return (
<section id="top" className="relative overflow-hidden">
<div
aria-hidden
className="pointer-events-none absolute inset-0 grid-fade opacity-[0.5]"
style={{
backgroundImage:
"linear-gradient(to right, var(--color-line-soft) 1px, transparent 1px), linear-gradient(to bottom, var(--color-line-soft) 1px, transparent 1px)",
backgroundSize: "44px 44px",
}}
/>
<div
aria-hidden
className="pointer-events-none absolute left-1/2 top-[-12rem] h-[32rem] w-[32rem] -translate-x-1/2 rounded-full opacity-40 blur-[120px]"
style={{ background: "radial-gradient(circle, var(--color-accent-glow), transparent 70%)" }}
/>
<div className="relative mx-auto max-w-6xl px-6 pb-20 pt-20 sm:pt-28 md:pb-28 md:pt-36">
<div className="mx-auto max-w-3xl text-center">
<motion.div
custom={0}
variants={reveal}
initial="hidden"
animate="show"
className="mb-8 inline-flex items-center gap-2 border border-[var(--color-line)] bg-[var(--color-bg-soft)]/60 px-3 py-1.5 font-mono text-xs text-[var(--color-fg-muted)] backdrop-blur-sm"
>
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--color-accent)] opacity-60" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-[var(--color-accent)]" />
</span>
<span>v0.3.5 — now in public beta</span>
</motion.div>
<motion.h1
custom={1}
variants={reveal}
initial="hidden"
animate="show"
className="text-balance font-mono text-4xl font-medium leading-[1.08] tracking-tight text-[var(--color-fg)] sm:text-5xl md:text-6xl"
>
A coding agent that
<br className="hidden sm:block" />{" "}
<span className="text-[var(--color-accent)] text-glow">lives in your terminal.</span>
</motion.h1>
<motion.p
custom={2}
variants={reveal}
initial="hidden"
animate="show"
className="mx-auto mt-6 max-w-xl text-balance font-sans text-base text-[var(--color-fg-muted)] sm:text-lg"
>
Cassady is a clean, simple terminal coding agent written in Rust.
No editor lock-in, no bloat — just a fast agent that gets work done.
</motion.p>
<motion.div
custom={3}
variants={reveal}
initial="hidden"
animate="show"
className="mx-auto mt-10 grid max-w-xl gap-3 sm:grid-cols-2"
>
<InstallCommand command="npm install -g cassady" label="install" />
<InstallCommand command="cass" label="run" />
</motion.div>
<motion.div
custom={4}
variants={reveal}
initial="hidden"
animate="show"
className="mt-8 flex flex-wrap items-center justify-center gap-3"
>
<Button asChild size="lg">
<a href="#how">
Get started <ArrowRight className="h-4 w-4" />
</a>
</Button>
<Button asChild size="lg" variant="outline">
<a href="#">
<BookOpen className="h-4 w-4" /> Read the docs
</a>
</Button>
</motion.div>
</div>
</div>
</section>
);
}
+65
View File
@@ -0,0 +1,65 @@
import { motion } from "motion/react";
import { SectionHeading, SectionLabel } from "@/components/Features";
const steps = [
{
n: "01",
title: "Install",
code: "npm install -g cassady",
body: "One command. The Rust binary lands in your PATH via npm — no Rust toolchain, no brew, no fuss.",
},
{
n: "02",
title: "Run",
code: "cass \"refactor the auth module\"",
body: "Describe what you want in plain English. Cassady reads your code, plans the change, and applies it — asking only when it must.",
},
{
n: "03",
title: "Ship",
code: "git diff && git commit",
body: "Review the diff, run your tests, and commit. You stay in control of the repo; Cassady just gets you there faster.",
},
];
export function HowItWorks() {
return (
<section id="how" className="relative mx-auto max-w-6xl px-6 py-20">
<SectionLabel>how it works</SectionLabel>
<SectionHeading>From zero to shipped in three steps.</SectionHeading>
<div className="mt-12 grid gap-px overflow-hidden rounded-md border border-[var(--color-line)] bg-[var(--color-line)] md:grid-cols-3">
{steps.map((s, i) => (
<motion.div
key={s.n}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.5, delay: i * 0.1, ease: [0.22, 1, 0.36, 1] }}
className="relative bg-[var(--color-bg-soft)]/60 p-7"
>
<div className="flex items-baseline justify-between">
<span className="font-mono text-3xl font-medium text-[var(--color-accent)] text-glow">
{s.n}
</span>
<span className="font-mono text-xs uppercase tracking-[0.2em] text-[var(--color-fg-dim)]">
step
</span>
</div>
<h3 className="mt-6 font-mono text-lg font-medium text-[var(--color-fg)]">
{s.title}
</h3>
<div className="mt-3 border border-[var(--color-line)] bg-[var(--color-bg)] px-3 py-2 font-mono text-xs text-[var(--color-fg-muted)]">
<span className="text-[var(--color-accent)]">▸ </span>
{s.code}
</div>
<p className="mt-4 font-sans text-sm leading-relaxed text-[var(--color-fg-muted)]">
{s.body}
</p>
</motion.div>
))}
</div>
</section>
);
}
+53
View File
@@ -0,0 +1,53 @@
import { useState } from "react";
import { Check, Copy } from "lucide-react";
import { cn } from "@/lib/utils";
interface InstallCommandProps {
command: string;
label: string;
className?: string;
}
export function InstallCommand({ command, label, className }: InstallCommandProps) {
const [copied, setCopied] = useState(false);
const copy = async () => {
try {
await navigator.clipboard.writeText(command);
setCopied(true);
setTimeout(() => setCopied(false), 1600);
} catch {
// noop
}
};
return (
<div className={cn("group", className)}>
<div className="mb-1.5 flex items-center gap-2 font-mono text-[11px] uppercase tracking-[0.18em] text-[var(--color-fg-dim)]">
<span className="text-[var(--color-accent)]">$</span>
<span>{label}</span>
</div>
<button
onClick={copy}
className="flex w-full items-center justify-between gap-3 border border-[var(--color-line)] bg-[var(--color-bg-soft)]/70 px-4 py-3 font-mono text-sm text-[var(--color-fg)] transition-all duration-200 hover:border-[var(--color-accent)]/50 hover:bg-[var(--color-bg-elev)]/70"
aria-label={`Copy ${command}`}
>
<span className="flex items-center gap-2 overflow-hidden">
<span className="text-[var(--color-accent)]">▸</span>
<span className="truncate">{command}</span>
</span>
<span className="flex shrink-0 items-center gap-1.5 text-[var(--color-fg-dim)] transition-colors group-hover:text-[var(--color-fg-muted)]">
{copied ? (
<>
<Check className="h-3.5 w-3.5 text-[var(--color-accent)]" />
<span className="text-xs text-[var(--color-accent)]">copied</span>
</>
) : (
<Copy className="h-3.5 w-3.5" />
)}
</span>
</button>
</div>
);
}
+108
View File
@@ -0,0 +1,108 @@
import { useEffect, useState } from "react";
import { motion } from "motion/react";
import { Github, Menu, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
const links = [
{ label: "Features", href: "#features" },
{ label: "How it works", href: "#how" },
{ label: "FAQ", href: "#faq" },
];
export function Nav() {
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
<motion.header
initial={{ y: -24, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
className={cn(
"sticky top-0 z-50 w-full backdrop-blur-md transition-colors duration-300",
scrolled
? "border-b border-[var(--color-line)] bg-[var(--color-bg)]/85"
: "border-b border-transparent bg-transparent",
)}
>
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-6">
<a href="#top" className="group flex items-center gap-2.5">
<img
src="/cass-logo-transparent.png"
alt="Cassady"
className="h-7 w-7 transition-transform duration-300 group-hover:scale-105"
/>
<span className="font-mono text-base font-medium tracking-tight text-[var(--color-fg)]">
cassady
</span>
</a>
<nav className="hidden items-center gap-1 md:flex">
{links.map((l) => (
<a
key={l.href}
href={l.href}
className="px-3 py-2 font-mono text-sm text-[var(--color-fg-muted)] transition-colors hover:text-[var(--color-accent)]"
>
{l.label}
</a>
))}
</nav>
<div className="hidden items-center gap-3 md:flex">
<Button asChild variant="ghost" size="sm">
<a href="#" aria-label="GitHub">
<Github className="h-4 w-4" />
<span>0.1k</span>
</a>
</Button>
<Button asChild variant="outline" size="sm">
<a href="#">npm</a>
</Button>
</div>
<button
className="flex h-9 w-9 items-center justify-center text-[var(--color-fg)] md:hidden"
onClick={() => setOpen((v) => !v)}
aria-label="Toggle menu"
>
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
{open && (
<div className="border-t border-[var(--color-line)] bg-[var(--color-bg)]/95 px-6 py-4 md:hidden">
<nav className="flex flex-col gap-1">
{links.map((l) => (
<a
key={l.href}
href={l.href}
onClick={() => setOpen(false)}
className="px-2 py-2.5 font-mono text-sm text-[var(--color-fg-muted)] transition-colors hover:text-[var(--color-accent)]"
>
{l.label}
</a>
))}
<div className="mt-2 flex gap-3">
<Button asChild variant="outline" size="sm" className="flex-1">
<a href="#">GitHub</a>
</Button>
<Button asChild variant="default" size="sm" className="flex-1">
<a href="#">npm</a>
</Button>
</div>
</nav>
</div>
)}
</motion.header>
);
}
+399
View File
@@ -0,0 +1,399 @@
import { useEffect, useRef, useState } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
type BlockKind = "user" | "assistant" | "tool" | "toolDone" | "status";
interface Block {
kind: BlockKind;
title: string;
content: string[];
summary?: string;
}
type FooterState = "idle" | "running" | "approval";
interface Footer {
mode: string;
state: FooterState;
model: string;
cwd: string;
id: string;
effort: string;
extra?: string;
}
const DEFAULT_FOOTER: Footer = {
mode: "workspace-edit",
state: "idle",
model: "gpt-5.5",
cwd: "~/cassady",
id: "a1b2c3…d4e5",
effort: "medium",
};
type Event =
| { ev: "wait"; ms: number }
| { ev: "footer"; state?: FooterState; extra?: string }
| { ev: "busy"; busy: boolean }
| { ev: "type"; text: string }
| { ev: "send" }
| { ev: "block"; block: Block }
| { ev: "stream"; text: string }
| { ev: "doneTool"; title: string; summary: string }
| { ev: "resolveApproval" }
| { ev: "print"; text: string }
| { ev: "reset" };
const session: Event[] = [
{ ev: "wait", ms: 600 },
{ ev: "type", text: "add a debounce hook to src/hooks/use-search.ts" },
{ ev: "wait", ms: 350 },
{ ev: "send" },
{ ev: "busy", busy: true },
{ ev: "footer", state: "running" },
{ ev: "wait", ms: 300 },
{ ev: "block", block: { kind: "assistant", title: "cass", content: [""] } },
{ ev: "stream", text: "I'll read the file first, then add the hook." },
{ ev: "block", block: { kind: "tool", title: "read … (call_1)", content: ["file: src/hooks/use-search.ts"] } },
{ ev: "wait", ms: 650 },
{ ev: "doneTool", title: "read ✓ (call_1)", summary: "14 lines · 0.4 KB · tool output hidden" },
{ ev: "block", block: { kind: "tool", title: "edit … (call_2)", content: ["file: src/hooks/use-search.ts", "edits: 1"] } },
{ ev: "wait", ms: 650 },
{ ev: "doneTool", title: "edit ✓ (call_2)", summary: "6 lines · 0.2 KB · tool output hidden" },
{ ev: "block", block: { kind: "assistant", title: "cass", content: [""] } },
{ ev: "stream", text: "Done. Added a 250ms debounce to the search query." },
{ ev: "block", block: { kind: "tool", title: "shell … (call_3)", content: ["command: npm test -- use-search"] } },
{ ev: "block", block: { kind: "status", title: "approval required (call_3)", content: ["shell requires approval before execution.", "", "Press y to approve, n or Esc to deny."] } },
{ ev: "footer", state: "approval", extra: "approval required: press y to approve, n to deny" },
{ ev: "wait", ms: 900 },
{ ev: "resolveApproval" },
{ ev: "footer", state: "running", extra: "approval accepted" },
{ ev: "wait", ms: 700 },
{ ev: "doneTool", title: "shell ✓ (call_3)", summary: "4 lines · 0.2 KB · tool output hidden" },
{ ev: "block", block: { kind: "assistant", title: "cass", content: [""] } },
{ ev: "stream", text: "Tests pass — 3 passed in 0.42s." },
{ ev: "busy", busy: false },
{ ev: "footer", state: "idle", extra: "turn finished" },
{ ev: "wait", ms: 900 },
{ ev: "wait", ms: 3200 },
{ ev: "reset" },
];
export function TerminalDemo() {
const [blocks, setBlocks] = useState<Block[]>([]);
const [input, setInput] = useState("");
const [busy, setBusy] = useState(false);
const [footer, setFooter] = useState<Footer>(DEFAULT_FOOTER);
const [printed, setPrinted] = useState<string[]>([]);
const [typing, setTyping] = useState(false);
const bodyRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const live = { current: true };
let timer: ReturnType<typeof setTimeout>;
const delay = (ms: number) =>
new Promise<void>((resolve) => {
timer = setTimeout(() => resolve(), ms);
});
const refs = {
blocks: [] as Block[],
input: "",
busy: false,
footer: { ...DEFAULT_FOOTER },
printed: [] as string[],
};
const commit = () => {
if (!live.current) return;
setBlocks(refs.blocks.slice());
setInput(refs.input);
setBusy(refs.busy);
setFooter({ ...refs.footer });
setPrinted(refs.printed.slice());
};
const appendStream = (text: string) => {
const last = refs.blocks[refs.blocks.length - 1];
if (last && last.kind === "assistant") {
const lines = text.split("\n");
last.content[0] = (last.content[0] || "") + lines[0];
for (let i = 1; i < lines.length; i++) last.content.push(lines[i]);
}
};
const process = async (ev: Event) => {
if (!live.current) return;
switch (ev.ev) {
case "wait":
await delay(ev.ms);
break;
case "footer":
if (ev.state) refs.footer.state = ev.state;
refs.footer.extra = ev.extra;
commit();
break;
case "busy":
refs.busy = ev.busy;
commit();
break;
case "type":
setTyping(true);
for (const ch of ev.text) {
if (!live.current) return;
refs.input += ch;
commit();
await delay(38 + Math.random() * 52);
}
setTyping(false);
break;
case "send":
refs.blocks.push({
kind: "user",
title: "› you",
content: [refs.input],
});
refs.input = "";
commit();
break;
case "block":
refs.blocks.push({ ...ev.block, content: [...ev.block.content] });
commit();
break;
case "stream":
for (let i = 0; i < ev.text.length; i++) {
if (!live.current) return;
appendStream(ev.text[i]);
commit();
await delay(14 + Math.random() * 22);
}
await delay(160);
break;
case "doneTool":
if (refs.blocks.length) {
const last = refs.blocks[refs.blocks.length - 1];
if (last && (last.kind === "tool" || last.kind === "status")) {
refs.blocks[refs.blocks.length - 1] = {
kind: "toolDone",
title: ev.title,
content: [],
summary: ev.summary,
};
}
}
commit();
break;
case "resolveApproval":
// drop a trailing status (approval) block if present
const last = refs.blocks[refs.blocks.length - 1];
if (last && last.kind === "status") refs.blocks.pop();
commit();
break;
case "print":
refs.printed.push(ev.text);
commit();
break;
case "reset":
refs.blocks = [];
refs.input = "";
refs.busy = false;
refs.footer = { ...DEFAULT_FOOTER };
refs.printed = [];
commit();
await delay(400);
break;
}
};
const run = async () => {
while (live.current) {
for (const ev of session) {
if (!live.current) return;
await process(ev);
}
}
};
run();
return () => {
live.current = false;
clearTimeout(timer);
};
}, []);
useEffect(() => {
if (bodyRef.current) {
bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
}
}, [blocks, input, busy, footer, printed, typing]);
const stateColor =
footer.state === "running"
? "text-[var(--color-accent)]"
: footer.state === "approval"
? "text-[var(--color-amber)]"
: "text-[var(--color-fg-dim)]";
const stateLabel =
footer.state === "approval" ? "approval required" : footer.state;
return (
<motion.section
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.7, ease: [0.22, 1, 0.36, 1] }}
className="mx-auto max-w-4xl px-6 py-16"
>
<div className="terminal-shadow overflow-hidden rounded-md border border-[var(--color-line)] bg-[var(--color-bg)]/95">
<div className="flex items-center gap-2 border-b border-[var(--color-line)] bg-[var(--color-bg-soft)]/80 px-4 py-2.5">
<span className="h-2.5 w-2.5 rounded-full bg-[#3a3f47]" />
<span className="h-2.5 w-2.5 rounded-full bg-[#3a3f47]" />
<span className="h-2.5 w-2.5 rounded-full bg-[#3a3f47]" />
<span className="ml-3 font-mono text-xs text-[var(--color-fg-dim)]">
cass — workspace-edit — 96×28
</span>
</div>
<div
ref={bodyRef}
className="relative h-[380px] overflow-y-auto px-5 py-4 font-mono text-[13px] leading-relaxed"
>
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-[0.04]"
style={{
backgroundImage:
"repeating-linear-gradient(to bottom, transparent 0, transparent 2px, #fff 2px, #fff 3px)",
}}
/>
{blocks.length === 0 && printed.length === 0 && (
<div className="text-[var(--color-fg-dim)]">
Ask a question or request a code change.
</div>
)}
{blocks.map((b, i) => (
<BlockView key={i} block={b} />
))}
{printed.map((p, i) => (
<div key={`p-${i}`} className="mt-2 text-[var(--color-fg-muted)]">
{p}
</div>
))}
<div className="mt-1 flex items-start gap-1">
<span className={cn("shrink-0", busy ? "text-[var(--color-accent)]" : "text-[var(--color-accent)]")}>
{busy ? "…" : "›"}
</span>
<span className="text-[var(--color-fg)]">
{input}
{typing && (
<span className="cursor-blink ml-px inline-block h-[1.05em] w-[0.55em] translate-y-[0.18em] bg-[var(--color-accent)]" />
)}
</span>
{!typing && !busy && blocks.length > 0 && (
<span className="cursor-blink ml-px inline-block h-[1.05em] w-[0.55em] translate-y-[0.18em] bg-[var(--color-fg-dim)]" />
)}
</div>
</div>
<div className="flex items-center gap-1.5 border-t border-[var(--color-line)] bg-[var(--color-bg-soft)]/60 px-5 py-2 font-mono text-[11px] text-[var(--color-fg-dim)]">
<span>cass</span>
<span>·</span>
<span>{footer.mode}</span>
<span>·</span>
<span className={stateColor}>{stateLabel}</span>
<span>·</span>
<span>{footer.model}</span>
<span>·</span>
<span>{footer.cwd}</span>
<span>·</span>
<span>{footer.id}</span>
<span>·</span>
<span>reasoning:{footer.effort}</span>
{footer.extra && (
<>
<span>·</span>
<span className="text-[var(--color-fg-dim)]">{footer.extra}</span>
</>
)}
</div>
</div>
<p className="mt-4 text-center font-mono text-xs text-[var(--color-fg-dim)]">
a real Cassady session — animated, loops on replay
</p>
</motion.section>
);
}
function BlockView({ block }: { block: Block }) {
switch (block.kind) {
case "user":
return (
<div className="mb-2">
<div className="text-[var(--color-fg)]">
<span className="text-[var(--color-accent)]">›</span> you
</div>
{block.content.map((line, i) => (
<div key={i} className="whitespace-pre-wrap text-[var(--color-fg-muted)]">
{" "}{line}
</div>
))}
</div>
);
case "assistant":
return (
<div className="mb-2">
<div className="text-[var(--color-accent)]">cass</div>
{block.content.map((line, i) => (
<div key={i} className="whitespace-pre-wrap text-[var(--color-fg)]">
{" "}{line}
</div>
))}
</div>
);
case "tool":
return (
<div className="mb-2">
<div className="text-[var(--color-fg-dim)]">· {block.title}</div>
{block.content.map((line, i) => (
<div key={i} className="whitespace-pre-wrap text-[var(--color-fg-dim)]">
{" "}{line}
</div>
))}
</div>
);
case "toolDone": {
const title = block.title;
const checkIdx = title.indexOf("✓");
return (
<div className="mb-2">
<div className="text-[var(--color-fg-dim)]">
· {title.slice(0, checkIdx)}
<span className="text-[var(--color-accent)]">✓</span>
{title.slice(checkIdx + 1)}
<span> · {block.summary}</span>
</div>
</div>
);
}
case "status":
return (
<div className="mb-2">
<div className="text-[var(--color-amber)]">· {block.title}</div>
{block.content.map((line, i) => (
<div key={i} className="whitespace-pre-wrap text-[var(--color-fg-dim)]">
{" "}{line}
</div>
))}
</div>
);
}
}
+65
View File
@@ -0,0 +1,65 @@
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn(
"border-b border-[var(--color-line)] last:border-b-0",
className,
)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"group flex flex-1 items-center justify-between py-5 font-mono text-sm font-medium text-[var(--color-fg)] transition-all hover:text-[var(--color-accent)] [&[data-state=open]>svg]:rotate-90",
className,
)}
{...props}
>
{children}
<ChevronRight className="h-4 w-4 shrink-0 text-[var(--color-fg-dim)] transition-transform duration-200 group-data-[state=open]:text-[var(--color-accent)]" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-[accordion-up_0.2s_ease-out] data-[state=open]:animate-[accordion-down_0.2s_ease-out]"
{...props}
>
<div
className={cn(
"pb-5 pr-8 font-sans text-[var(--color-fg-muted)] leading-relaxed",
className,
)}
>
{children}
</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
+54
View File
@@ -0,0 +1,54 @@
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 font-mono text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-bg)] disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-[var(--color-accent)] text-[#0b0d10] hover:bg-[var(--color-accent)]/90 hover:shadow-[0_0_24px_-4px_var(--color-accent-glow)]",
outline:
"border border-[var(--color-line)] bg-transparent text-[var(--color-fg)] hover:border-[var(--color-accent)]/60 hover:text-[var(--color-accent)] hover:bg-[var(--color-accent-glow)]",
ghost:
"text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] hover:bg-white/[0.03]",
link: "text-[var(--color-accent)] underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-5 py-2",
sm: "h-8 px-3 text-xs",
lg: "h-12 px-7 text-base",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
+89
View File
@@ -0,0 +1,89 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"relative border border-[var(--color-line)] bg-[var(--color-bg-soft)]/60 text-[var(--color-fg)] backdrop-blur-sm",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"font-mono text-base font-medium leading-none tracking-tight text-[var(--color-fg)]",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"font-sans text-sm text-[var(--color-fg-muted)]",
className,
)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
+29
View File
@@ -0,0 +1,29 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-[var(--color-line)]",
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
className,
)}
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };
+28
View File
@@ -0,0 +1,28 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 6, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden border border-[var(--color-line)] bg-[var(--color-bg-elev)] px-2.5 py-1.5 font-mono text-xs text-[var(--color-fg)] shadow-lg",
className,
)}
{...props}
/>
</TooltipPrimitive.Portal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+161
View File
@@ -0,0 +1,161 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
@theme {
--font-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", "Menlo", monospace;
--font-sans: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
--color-bg: #0b0d10;
--color-bg-soft: #111418;
--color-bg-elev: #161a1f;
--color-line: #232830;
--color-line-soft: #1a1f25;
--color-fg: #c7ccd4;
--color-fg-muted: #8b929d;
--color-fg-dim: #5b626c;
--color-accent: #5b9fe0;
--color-accent-soft: #3d6fa8;
--color-accent-glow: rgba(91, 159, 224, 0.18);
--color-amber: #d4a55a;
--radius: 6px;
}
:root {
color-scheme: dark;
}
* {
border-color: var(--color-line);
}
html {
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
background-color: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-sans);
font-size: 15px;
line-height: 1.6;
overflow-x: hidden;
}
::selection {
background: var(--color-accent-glow);
color: var(--color-accent);
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--color-bg);
}
::-webkit-scrollbar-thumb {
background: var(--color-line);
border-radius: 0;
border: 2px solid var(--color-bg);
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-line-soft);
}
@layer utilities {
.font-mono {
font-family: var(--font-mono);
}
.font-sans {
font-family: var(--font-sans);
}
.text-balance {
text-wrap: balance;
}
.scanlines::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1;
background: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 2px,
rgba(255, 255, 255, 0.016) 2px,
rgba(255, 255, 255, 0.016) 3px
);
mix-blend-mode: overlay;
}
.grain::after {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 2;
opacity: 0.04;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
.vignette::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
background: radial-gradient(
ellipse at center,
transparent 40%,
rgba(0, 0, 0, 0.4) 100%
);
}
.text-glow {
text-shadow: 0 0 12px var(--color-accent-glow);
}
.grid-fade {
-webkit-mask-image: radial-gradient(ellipse 60% 50% at 50% 0%, black 30%, transparent 75%);
mask-image: radial-gradient(ellipse 60% 50% at 50% 0%, black 30%, transparent 75%);
}
.terminal-shadow {
box-shadow:
0 0 0 1px var(--color-line),
0 24px 60px -20px rgba(0, 0, 0, 0.7),
0 0 80px -30px var(--color-accent-glow);
}
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
.cursor-blink {
animation: blink 1.05s steps(1) infinite;
}
@keyframes scan-drift {
0% { transform: translateY(-100%); }
100% { transform: translateY(100%); }
}
@keyframes accordion-down {
from { height: 0; opacity: 0; }
to { height: var(--radix-accordion-content-height); opacity: 1; }
}
@keyframes accordion-up {
from { height: var(--radix-accordion-content-height); opacity: 1; }
to { height: 0; opacity: 0; }
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+17
View File
@@ -0,0 +1,17 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "@fontsource/ibm-plex-mono/400.css";
import "@fontsource/ibm-plex-mono/500.css";
import "@fontsource/ibm-plex-mono/600.css";
import "@fontsource/ibm-plex-sans/400.css";
import "@fontsource/ibm-plex-sans/500.css";
import App from "./App.tsx";
import "./index.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
+1
View File
@@ -0,0 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/components/faq.tsx","./src/components/features.tsx","./src/components/footer.tsx","./src/components/hero.tsx","./src/components/howitworks.tsx","./src/components/installcommand.tsx","./src/components/nav.tsx","./src/components/terminaldemo.tsx","./src/components/ui/accordion.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/separator.tsx","./src/components/ui/tooltip.tsx","./src/lib/utils.ts"],"version":"5.9.3"}
+13
View File
@@ -0,0 +1,13 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
+1
View File
@@ -0,0 +1 @@
{"root":["./vite.config.ts"],"version":"5.9.3"}
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import path from "path";
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});