Inital commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
.DS_Store
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
@@ -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>
|
||||||
Generated
+4710
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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 };
|
||||||
@@ -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
@@ -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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -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>,
|
||||||
|
);
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"root":["./vite.config.ts"],"version":"5.9.3"}
|
||||||
@@ -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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user