diff --git a/apps/marketing/app/globals.css b/apps/marketing/app/globals.css index 5be67d608..e1f14def5 100644 --- a/apps/marketing/app/globals.css +++ b/apps/marketing/app/globals.css @@ -1,7 +1,7 @@ @import "tailwindcss"; @import "tw-animate-css"; -@custom-variant dark (&:is(.dark *)); +@custom-variant dark (&:is(.dark, .dark *)); /* Add scrollbar-hide utility */ @utility scrollbar-hide { @@ -12,162 +12,62 @@ } } -/* Custom thin scrollbar styling */ -* { - scrollbar-width: thin; - scrollbar-color: var(--muted) var(--secondary); -} - -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: var(--secondary); -} - -::-webkit-scrollbar-thumb { - background: var(--muted); - border-radius: 3px; -} - :root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); + color-scheme: dark; --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); - - /* Semantic status and UI tokens */ - --status-progress: oklch(0.795 0.184 86.047); - --status-success: oklch(0.765 0.177 163.223); - --status-warning: oklch(0.769 0.188 70.08); - --status-error: oklch(0.637 0.237 25.331); - --brand: oklch(0.585 0.233 277.117); - --brand-foreground: oklch(0.985 0 0); - --surface: oklch(0.97 0 0); - --surface-elevated: oklch(0.985 0 0); - --highlight: oklch(0.488 0.243 264.376); - --highlight-foreground: oklch(0.985 0 0); - - /* Semantic tokens for favorites/labels */ - --favorite-blue: oklch(0.646 0.222 41.116); - --favorite-purple: oklch(0.627 0.265 303.9); - --label-project: oklch(0.828 0.189 84.429); - - /* Code syntax highlighting tokens */ - --syntax-keyword: oklch(0.646 0.222 41.116); - --syntax-string: oklch(0.6 0.118 184.704); - --syntax-function: oklch(0.828 0.189 84.429); - --syntax-variable: oklch(0.769 0.188 70.08); - - /* Added new semantic tokens for landing page sections */ - /* Section accent colors */ - --accent-ai: oklch(0.623 0.214 259.815); - --accent-workflows: oklch(0.705 0.213 47.604); - --accent-planning: oklch(0.723 0.219 149.579); - /* Code block syntax colors (light mode) */ - --code-comment: oklch(0.556 0 0); - --code-keyword: oklch(0.627 0.265 303.9); - --code-string: oklch(0.723 0.219 149.579); - --code-function: oklch(0.769 0.188 70.08); - --code-variable: oklch(0.795 0.184 86.047); - --code-type: oklch(0.6 0.118 184.704); - --code-constant: oklch(0.705 0.213 47.604); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.145 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); - --primary-foreground: oklch(0.205 0 0); + /* Dark-mode-only palette */ + --background: color-mix(in srgb, #0a0a1a 95%, white); + --foreground: #f5f5f5; + --card: color-mix(in srgb, #0a0a1a 95%, white); + --card-foreground: #f5f5f5; + --popover: color-mix(in srgb, #0a0a1a 95%, white); + --popover-foreground: #f5f5f5; + --primary: oklch(0.588 0.217 264); + --primary-foreground: #f5f5f5; --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); + --secondary-foreground: #f5f5f5; --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); + --muted-foreground: color-mix(in srgb, #a3a3a3 90%, white); --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); + --accent-foreground: #f5f5f5; --destructive: oklch(0.396 0.141 25.723); --destructive-foreground: oklch(0.637 0.237 25.331); - --border: oklch(0.269 0 0); - --input: oklch(0.269 0 0); - --ring: oklch(0.439 0 0); + --border: oklch(1 0 0 / 0.06); + --input: oklch(1 0 0 / 0.06); + --ring: oklch(0.588 0.217 264 / 0.4); + + /* Chart colors */ --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.769 0.188 70.08); --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); + + /* Sidebar */ --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-foreground: #f5f5f5; + --sidebar-primary: oklch(0.588 0.217 264); + --sidebar-primary-foreground: #f5f5f5; --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(0.269 0 0); - --sidebar-ring: oklch(0.439 0 0); + --sidebar-accent-foreground: #f5f5f5; + --sidebar-border: oklch(1 0 0 / 0.06); + --sidebar-ring: oklch(0.588 0.217 264 / 0.4); - /* Dark mode semantic tokens */ - --status-progress: oklch(0.795 0.184 86.047); - --status-success: oklch(0.765 0.177 163.223); - --status-warning: oklch(0.769 0.188 70.08); - --status-error: oklch(0.637 0.237 25.331); + /* Semantic tokens */ --brand: oklch(0.585 0.233 277.117); - --brand-foreground: oklch(0.985 0 0); + --brand-foreground: #f5f5f5; --surface: oklch(0.205 0 0); --surface-elevated: oklch(0.269 0 0); --highlight: oklch(0.585 0.233 277.117); - --highlight-foreground: oklch(0.985 0 0); - - /* Dark mode favorites/labels */ - --favorite-blue: oklch(0.488 0.243 264.376); - --favorite-purple: oklch(0.627 0.265 303.9); - --label-project: oklch(0.627 0.265 303.9); + --highlight-foreground: #f5f5f5; - /* Code syntax highlighting tokens - dark mode */ - --syntax-keyword: oklch(0.488 0.243 264.376); - --syntax-string: oklch(0.696 0.17 162.48); - --syntax-function: oklch(0.627 0.265 303.9); - --syntax-variable: oklch(0.769 0.188 70.08); - - /* Dark mode landing page tokens */ + /* Landing page section accents */ --accent-ai: oklch(0.623 0.214 259.815); --accent-workflows: oklch(0.705 0.213 47.604); --accent-planning: oklch(0.723 0.219 149.579); - /* Code block syntax colors (dark mode) */ + /* Code syntax */ --code-comment: oklch(0.439 0 0); --code-keyword: oklch(0.627 0.265 303.9); --code-string: oklch(0.723 0.219 149.579); @@ -178,8 +78,10 @@ } @theme inline { - --font-sans: "Geist", "Geist Fallback"; - --font-mono: "Geist Mono", "Geist Mono Fallback"; + --font-sans: var(--font-dm-sans), "DM Sans", -apple-system, BlinkMacSystemFont, + "Segoe UI", system-ui, sans-serif; + --font-mono: "SF Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, + monospace; --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); @@ -216,29 +118,12 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); - - /* Register semantic tokens in theme */ - --color-status-progress: var(--status-progress); - --color-status-success: var(--status-success); - --color-status-warning: var(--status-warning); - --color-status-error: var(--status-error); --color-brand: var(--brand); --color-brand-foreground: var(--brand-foreground); --color-surface: var(--surface); --color-surface-elevated: var(--surface-elevated); --color-highlight: var(--highlight); --color-highlight-foreground: var(--highlight-foreground); - - /* Register favorites/labels tokens */ - --color-favorite-blue: var(--favorite-blue); - --color-favorite-purple: var(--favorite-purple); - --color-label-project: var(--label-project); - --color-syntax-keyword: var(--syntax-keyword); - --color-syntax-string: var(--syntax-string); - --color-syntax-function: var(--syntax-function); - --color-syntax-variable: var(--syntax-variable); - - /* Register new landing page semantic tokens */ --color-accent-ai: var(--accent-ai); --color-accent-workflows: var(--accent-workflows); --color-accent-planning: var(--accent-planning); @@ -258,9 +143,53 @@ body { @apply bg-background text-foreground; } - /* Add text selection styling */ ::selection { - background-color: var(--brand); + background-color: var(--primary); color: white; } } + +/* Fractal noise texture overlay */ +body::after { + content: ""; + position: fixed; + inset: 0; + z-index: 9999; + pointer-events: none; + opacity: 0.035; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E"); +} + +/* Heading scale */ +h1 { + font-size: clamp(2.25rem, 5vw, 3.75rem); + line-height: 1.1; + font-weight: 600; + letter-spacing: -0.025em; +} + +h2 { + font-size: clamp(1.5rem, 3.5vw, 2.25rem); + line-height: 1.2; + font-weight: 600; + letter-spacing: -0.02em; +} + +/* Custom thin scrollbar styling */ +* { + scrollbar-width: thin; + scrollbar-color: var(--muted) transparent; +} + +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--muted); + border-radius: 3px; +} diff --git a/apps/marketing/app/layout.tsx b/apps/marketing/app/layout.tsx index dd6999770..3edafb467 100644 --- a/apps/marketing/app/layout.tsx +++ b/apps/marketing/app/layout.tsx @@ -1,37 +1,41 @@ import type React from "react"; import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { DM_Sans } from "next/font/google"; import { Analytics } from "@vercel/analytics/next"; import "./globals.css"; -const _geist = Geist({ subsets: ["latin"] }); -const _geistMono = Geist_Mono({ subsets: ["latin"] }); +const dmSans = DM_Sans({ + subsets: ["latin"], + variable: "--font-dm-sans", +}); export const metadata: Metadata = { - title: "Sprint - Purpose-built tool for planning and building products", + title: "OK Code — A Minimal Web GUI for Coding Agents", description: - "Meet the system for modern software development. Streamline issues, projects, and product roadmaps with Sprint.", - generator: "v0.app", + "Chat with Codex and Claude in a modern web UI. Git worktree isolation, diff review, integrated terminal, and more. Run anywhere with npx okcodes.", keywords: [ - "project management", - "product development", - "issue tracking", - "roadmap planning", - "team collaboration", + "coding agents", + "AI coding", + "web GUI", + "git worktree", + "diff review", + "Claude", + "Codex", + "terminal", ], - authors: [{ name: "Sprint" }], + authors: [{ name: "OpenKnots" }], openGraph: { - title: "Sprint - Purpose-built tool for planning and building products", + title: "OK Code — A Minimal Web GUI for Coding Agents", description: - "Meet the system for modern software development. Streamline issues, projects, and product roadmaps.", + "Chat with Codex and Claude in a modern web UI. Git worktree isolation, diff review, integrated terminal, and more.", type: "website", locale: "en_US", }, twitter: { card: "summary_large_image", - title: "Sprint - Purpose-built tool for planning and building products", + title: "OK Code — A Minimal Web GUI for Coding Agents", description: - "Meet the system for modern software development. Streamline issues, projects, and product roadmaps.", + "Chat with Codex and Claude in a modern web UI. Git worktree isolation, diff review, integrated terminal, and more.", }, icons: { icon: [ @@ -58,8 +62,8 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - + + {children} diff --git a/apps/marketing/app/page.tsx b/apps/marketing/app/page.tsx index 7f31aeb2d..588ce14b1 100644 --- a/apps/marketing/app/page.tsx +++ b/apps/marketing/app/page.tsx @@ -1,9 +1,19 @@ -import { Hero3DStage } from "@/components/hero-3d-stage"; +import { Nav } from "@/components/Nav"; +import { Hero } from "@/components/Hero"; +import { FeatureGrid } from "@/components/FeatureGrid"; +import { HowItWorks } from "@/components/HowItWorks"; +import { GetStarted } from "@/components/GetStarted"; +import { Footer } from "@/components/Footer"; export default function Home() { return ( -
- +
+
); } diff --git a/apps/marketing/components/CodeBlock.tsx b/apps/marketing/components/CodeBlock.tsx new file mode 100644 index 000000000..558a1ab42 --- /dev/null +++ b/apps/marketing/components/CodeBlock.tsx @@ -0,0 +1,68 @@ +import { useState, useCallback, useRef, useEffect } from "react"; + +const svgProps = { + width: 16, + height: 16, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + strokeWidth: 2, + strokeLinecap: "round" as const, + strokeLinejoin: "round" as const, +}; + +const CheckIcon = ( + + + +); + +const CopyIcon = ( + + + + +); + +interface CodeBlockProps { + code: string; + label?: string; +} + +export function CodeBlock({ code, label }: CodeBlockProps) { + const [copied, setCopied] = useState(false); + const timerRef = useRef>(null); + + useEffect(() => { + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, []); + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(code); + if (timerRef.current) clearTimeout(timerRef.current); + setCopied(true); + timerRef.current = setTimeout(() => setCopied(false), 2000); + } catch { + // Fallback: clipboard API unavailable + } + }, [code]); + + return ( +
+ {label && ( + {label} + )} + {code} + +
+ ); +} diff --git a/apps/marketing/components/ExternalLink.tsx b/apps/marketing/components/ExternalLink.tsx new file mode 100644 index 000000000..c8ff37370 --- /dev/null +++ b/apps/marketing/components/ExternalLink.tsx @@ -0,0 +1,22 @@ +import type { AnchorHTMLAttributes, ReactNode } from "react"; + +interface ExternalLinkProps extends AnchorHTMLAttributes { + href: string; + children: ReactNode; +} + +export function ExternalLink({ href, children, className, ...rest }: ExternalLinkProps) { + return ( + + {children} + + ); +} diff --git a/apps/marketing/components/FeatureCard.tsx b/apps/marketing/components/FeatureCard.tsx new file mode 100644 index 000000000..d49095655 --- /dev/null +++ b/apps/marketing/components/FeatureCard.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from "react"; + +interface FeatureCardProps { + icon?: ReactNode; + title: string; + description: string; + compact?: boolean; +} + +export function FeatureCard({ icon, title, description, compact }: FeatureCardProps) { + if (compact) { + return ( +
+

{title}

+

{description}

+
+ ); + } + + return ( +
+ {icon &&
{icon}
} +

{title}

+

{description}

+
+ ); +} diff --git a/apps/marketing/components/FeatureGrid.tsx b/apps/marketing/components/FeatureGrid.tsx new file mode 100644 index 000000000..512f2b610 --- /dev/null +++ b/apps/marketing/components/FeatureGrid.tsx @@ -0,0 +1,149 @@ +import { FeatureCard } from "./FeatureCard"; + +/* Shared props to avoid repeating on every inline SVG icon */ +const svgProps = { + width: 20, + height: 20, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + strokeWidth: 2, + strokeLinecap: "round" as const, + strokeLinejoin: "round" as const, +}; + +const icons = { + messageSquare: ( + + + + ), + gitBranch: ( + + + + + + + ), + fileDiff: ( + + + + + + + ), + terminal: ( + + + + + ), + listChecks: ( + + + + + + + + ), + shieldCheck: ( + + + + + ), +}; + +const primaryFeatures = [ + { + icon: icons.messageSquare, + title: "Codex and Claude, one interface", + description: + "Switch providers per thread. Stream responses in real time. Attach images, terminal output, or file context.", + }, + { + icon: icons.gitBranch, + title: "Every thread, its own worktree", + description: + "Each conversation runs in an isolated git worktree. Your main branch stays clean. Merge when ready.", + }, + { + icon: icons.fileDiff, + title: "Review every change", + description: + "Inline and side-by-side diffs with syntax highlighting. Accept or reject changes per file before committing.", + }, + { + icon: icons.terminal, + title: "Built-in terminal", + description: + "Up to four terminal tabs per thread. Feed output directly to the agent. No window switching.", + }, + { + icon: icons.listChecks, + title: "Structured implementation plans", + description: + "AI-generated step-by-step plans with status tracking. See the full scope before any code changes.", + }, + { + icon: icons.shieldCheck, + title: "You stay in control", + description: + "Full-access or approval-required modes. Review every file write and command before execution.", + }, +]; + +const secondaryFeatures = [ + { + title: "GitHub PR review", + description: "Inline comments, conflict resolution, full review flow.", + }, + { + title: "Reusable skills", + description: "Built-in and custom skill catalog for automation.", + }, + { + title: "Restore any turn", + description: "Automatic snapshots. Roll back to any point in the conversation.", + }, + { + title: "Desktop, web, mobile", + description: "npx, Electron, or Capacitor. Same experience everywhere.", + }, +]; + +export function FeatureGrid() { + return ( +
+

+ Features +

+

Everything you need

+ +
+ {primaryFeatures.map((feature) => ( + + ))} +
+ +
+ {secondaryFeatures.map((feature) => ( + + ))} +
+
+ ); +} diff --git a/apps/marketing/components/GetStarted.tsx b/apps/marketing/components/GetStarted.tsx new file mode 100644 index 000000000..8ae6af6c0 --- /dev/null +++ b/apps/marketing/components/GetStarted.tsx @@ -0,0 +1,24 @@ +import { CodeBlock } from "./CodeBlock"; +import { ExternalLink } from "./ExternalLink"; +import { LINKS } from "./links"; + +export function GetStarted() { + return ( +
+
+

Start building.

+

One command. Zero config.

+ +
+ +
+ +
+ View on GitHub + Download Desktop + Join Discord +
+
+
+ ); +} diff --git a/apps/marketing/components/Hero.tsx b/apps/marketing/components/Hero.tsx new file mode 100644 index 000000000..2b043ea31 --- /dev/null +++ b/apps/marketing/components/Hero.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { motion } from "framer-motion"; +import { CodeBlock } from "./CodeBlock"; +import { ExternalLink } from "./ExternalLink"; +import { LINKS } from "./links"; +import { OKCodeMockup } from "./OKCodeMockup"; + +export function Hero() { + return ( +
+ {/* Ambient glow */} +
+ +
+ {/* Copy */} +
+ + A minimal web GUI for coding agents + + + + Ship code with AI agents. + + + + Chat with Codex and Claude in real time. Every thread runs in its own git worktree. + Review diffs, run terminals, and deploy — all from one interface. + + + + + + Download Desktop + + +
+ + {/* Product render */} + + {/* Bottom fade */} +
+ + {/* Mockup frame */} +
+ +
+ + {/* Subtle reflection glow under frame */} +
+ +
+
+ ); +} diff --git a/apps/marketing/components/HowItWorks.tsx b/apps/marketing/components/HowItWorks.tsx new file mode 100644 index 000000000..0f19893f1 --- /dev/null +++ b/apps/marketing/components/HowItWorks.tsx @@ -0,0 +1,49 @@ +const steps = [ + { + number: "01", + title: "Install a provider", + details: ["npm install -g @openai/codex", "npm install -g @anthropic-ai/claude-code"], + }, + { + number: "02", + title: "Launch OK Code", + details: ["npx okcodes"], + }, + { + number: "03", + title: "Start coding", + details: ["Open a thread, describe what you want, review the diff."], + }, +]; + +export function HowItWorks() { + return ( +
+

+ Quick start +

+

Get running in 60 seconds

+ +
+ {steps.map((step) => ( +
+ + {step.number} + +

{step.title}

+
+ {step.details.map((detail) => ( + + {detail} + + ))} +
+
+ ))} +
+
+ ); +} diff --git a/apps/marketing/components/Nav.tsx b/apps/marketing/components/Nav.tsx new file mode 100644 index 000000000..e15cd1906 --- /dev/null +++ b/apps/marketing/components/Nav.tsx @@ -0,0 +1,30 @@ +import { ExternalLink } from "./ExternalLink"; +import { LINKS } from "./links"; + +export function Nav() { + return ( + + ); +} diff --git a/apps/marketing/components/OKCodeMockup.tsx b/apps/marketing/components/OKCodeMockup.tsx new file mode 100644 index 000000000..ce93cf7aa --- /dev/null +++ b/apps/marketing/components/OKCodeMockup.tsx @@ -0,0 +1,393 @@ +"use client"; + +import type React from "react"; +import { motion } from "framer-motion"; +import { + ChevronDown, + ChevronRight, + Search, + Plus, + File, + Folder, + FolderOpen, + Terminal, + FileText, + Settings, + GitBranch, + MessageSquare, + Code, + Clock, + Monitor, +} from "lucide-react"; + +const containerVariants = { + hidden: {}, + visible: { + transition: { + staggerChildren: 0.25, + delayChildren: 0.4, + }, + }, +}; + +const panelVariants = { + hidden: { opacity: 0, y: 12 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.8, + ease: [0.22, 1, 0.36, 1] as const, + }, + }, +}; + +export function OKCodeMockup() { + return ( + + {/* Sidebar */} + + {/* Project header */} +
+ + Projects + +
+ + + + + + +
+
+ + {/* Active project */} +
+
+ + acme-app + 3m +
+
+ + {/* Active thread */} +
+
+ refactor auth flow +
+
+ + {/* File tree — static, minimal */} +
+
+ Files +
+
+
+ + Search files +
+
+ + + + + + + + + + + + + +
+ + {/* Bottom nav */} +
+ + + + +
+ +
+ + + + + + +
+
+ + {/* Center — Chat */} + + {/* Thread header */} +
+
+ refactor auth flow + acme-app +
+
+ + feat/auth-refactor +
+
+ + {/* Chat content — static, no scroll */} +
+ {/* User message */} +
+
+

+ Refactor the auth form to use server actions and add proper validation +

+
+
+ + {/* Agent response */} +
+

+ { + "I'll refactor the auth form to use Next.js server actions with Zod validation. Let me start by updating the form component." + } +

+ +
+ + + Edited 3 files + +47 -23 +
+ +

+ Done. The auth form now uses useActionState with a server action that + validates input through authSchema. Error messages render inline per + field. +

+ +
+
+ + RESPONSE · 8S +
+
+
+
+ + {/* Input — static */} +
+
+
+ Ask anything, @tag files, or / for commands +
+
+
+ + + Claude + + | + + + Chat + + | + + + Full access + +
+
+ + + + + + +
+
+
+
+
+ + {/* Right — Diff panel */} + + {/* Tabs */} +
+ + auth-form.tsx + + api.ts + schema.ts +
+ + {/* Diff content — static */} +
+ {`"use server";`} + {``} + {`import { z } from "zod";`} + {`import { authSchema } from "./schema";`} + {``} + {`export async function login(data) {`} + {`export async function login(`} + {` _prev: unknown,`} + {` formData: FormData`} + {`) {`} + {` const parsed = authSchema.safeParse({`} + {` email: formData.get("email"),`} + {` password: formData.get("pass"),`} + {` });`} + {``} + {` if (!parsed.success) {`} + {` return { errors: parsed.error.flatten() };`} + {` }`} +
+ + {/* Diff footer */} +
+
+ +47 + -23 + 3 files +
+
+ + Reject + + + Accept + +
+
+
+
+ ); +} + +/* Helper components — purely presentational, no state */ + +function TreeFolder({ + name, + children, + indent = 0, + open = false, +}: { + name: string; + children?: React.ReactNode; + indent?: number; + open?: boolean; +}) { + return ( +
+
+ {open ? ( + <> + + + + ) : ( + <> + + + + )} + {name} +
+ {open && children} +
+ ); +} + +function TreeFile({ + name, + indent = 0, + active = false, + dim = false, +}: { + name: string; + indent?: number; + active?: boolean; + dim?: boolean; +}) { + return ( +
+ + {name} +
+ ); +} + +function SidebarLink({ icon: Icon, label }: { icon: React.ElementType; label: string }) { + return ( +
+ + {label} +
+ ); +} + +function Pill({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +function Mono({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +function DiffLine({ + type, + num, + children, +}: { + type: "add" | "remove" | "context"; + num: string; + children: React.ReactNode; +}) { + const colors = { + add: "bg-emerald-500/[0.06] text-emerald-300/70", + remove: "bg-red-500/[0.06] text-red-300/60 line-through", + context: "text-white/30", + }; + const prefix = { add: "+", remove: "-", context: " " }; + + return ( +
+ {num} + {prefix[type]} + {children} +
+ ); +} diff --git a/apps/marketing/components/footer.tsx b/apps/marketing/components/footer.tsx index ffa1fac5b..b7b61ae83 100644 --- a/apps/marketing/components/footer.tsx +++ b/apps/marketing/components/footer.tsx @@ -1,78 +1,24 @@ -export function Footer() { - const footerLinks = { - Features: [ - "Plan", - "Build", - "Insights", - "Customer Requests", - "Sprint Asks", - "Security", - "Mobile", - ], - Product: [ - "Pricing", - "Method", - "Integrations", - "Changelog", - "Documentation", - "Download", - "Switch", - ], - Company: ["About", "Customers", "Careers", "Now", "README", "Quality", "Brand"], - Resources: [ - "Developers", - "Status", - "Startups", - "Report vulnerability", - "DPA", - "Privacy", - "Terms", - ], - Connect: ["Contact us", "Community", "X (Twitter)", "GitHub", "YouTube"], - }; +import { ExternalLink } from "./ExternalLink"; +import { LINKS } from "./links"; +export function Footer() { return ( -