From 0aee62ba0e43e666b3fd7a82c74039ea31e20802 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 1 Apr 2026 04:32:51 -0500 Subject: [PATCH 1/2] Redesign marketing site with product-aligned landing page Replace the placeholder marketing homepage with a full landing page showcasing OK Code features. Adds Tailwind CSS 4, DM Sans font via next/font, and a component-based architecture with Hero (featuring an animated app mockup with chat + diff panels), Feature Grid, How It Works, and Get Started sections. All copy derived from actual product capabilities. Co-Authored-By: Claude Opus 4.6 --- apps/marketing/next-env.d.ts | 3 +- apps/marketing/package.json | 4 + apps/marketing/postcss.config.mjs | 5 + apps/marketing/src/components/CodeBlock.tsx | 68 +++ .../marketing/src/components/ExternalLink.tsx | 22 + apps/marketing/src/components/FeatureCard.tsx | 27 ++ apps/marketing/src/components/FeatureGrid.tsx | 149 +++++++ apps/marketing/src/components/Footer.tsx | 26 ++ apps/marketing/src/components/GetStarted.tsx | 24 ++ apps/marketing/src/components/Hero.tsx | 107 +++++ apps/marketing/src/components/HowItWorks.tsx | 49 +++ apps/marketing/src/components/Nav.tsx | 30 ++ .../marketing/src/components/OKCodeMockup.tsx | 393 ++++++++++++++++++ apps/marketing/src/components/links.ts | 5 + apps/marketing/src/pages/_app.tsx | 14 +- apps/marketing/src/pages/_document.tsx | 13 + apps/marketing/src/pages/index.tsx | 47 ++- apps/marketing/src/styles/globals.css | 141 ++++--- bun.lock | 24 +- 19 files changed, 1083 insertions(+), 68 deletions(-) create mode 100644 apps/marketing/postcss.config.mjs create mode 100644 apps/marketing/src/components/CodeBlock.tsx create mode 100644 apps/marketing/src/components/ExternalLink.tsx create mode 100644 apps/marketing/src/components/FeatureCard.tsx create mode 100644 apps/marketing/src/components/FeatureGrid.tsx create mode 100644 apps/marketing/src/components/Footer.tsx create mode 100644 apps/marketing/src/components/GetStarted.tsx create mode 100644 apps/marketing/src/components/Hero.tsx create mode 100644 apps/marketing/src/components/HowItWorks.tsx create mode 100644 apps/marketing/src/components/Nav.tsx create mode 100644 apps/marketing/src/components/OKCodeMockup.tsx create mode 100644 apps/marketing/src/components/links.ts create mode 100644 apps/marketing/src/pages/_document.tsx diff --git a/apps/marketing/next-env.d.ts b/apps/marketing/next-env.d.ts index 36a4fe488..254b73c16 100644 --- a/apps/marketing/next-env.d.ts +++ b/apps/marketing/next-env.d.ts @@ -1,7 +1,6 @@ /// /// -/// /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/apps/marketing/package.json b/apps/marketing/package.json index 0cae41280..3e9d91fc7 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -11,14 +11,18 @@ "test": "vitest run --passWithNoTests" }, "dependencies": { + "framer-motion": "^12.23.26", + "lucide-react": "^0.454.0", "next": "^15.4.6", "react": "^19.1.1", "react-dom": "^19.1.1" }, "devDependencies": { + "@tailwindcss/postcss": "^4.0.0", "@types/node": "catalog:", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", + "tailwindcss": "^4.0.0", "typescript": "catalog:", "vitest": "catalog:" } diff --git a/apps/marketing/postcss.config.mjs b/apps/marketing/postcss.config.mjs new file mode 100644 index 000000000..c2ddf7482 --- /dev/null +++ b/apps/marketing/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/apps/marketing/src/components/CodeBlock.tsx b/apps/marketing/src/components/CodeBlock.tsx new file mode 100644 index 000000000..558a1ab42 --- /dev/null +++ b/apps/marketing/src/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/src/components/ExternalLink.tsx b/apps/marketing/src/components/ExternalLink.tsx new file mode 100644 index 000000000..c8ff37370 --- /dev/null +++ b/apps/marketing/src/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/src/components/FeatureCard.tsx b/apps/marketing/src/components/FeatureCard.tsx new file mode 100644 index 000000000..d49095655 --- /dev/null +++ b/apps/marketing/src/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/src/components/FeatureGrid.tsx b/apps/marketing/src/components/FeatureGrid.tsx new file mode 100644 index 000000000..512f2b610 --- /dev/null +++ b/apps/marketing/src/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/src/components/Footer.tsx b/apps/marketing/src/components/Footer.tsx new file mode 100644 index 000000000..b7b61ae83 --- /dev/null +++ b/apps/marketing/src/components/Footer.tsx @@ -0,0 +1,26 @@ +import { ExternalLink } from "./ExternalLink"; +import { LINKS } from "./links"; + +export function Footer() { + return ( +
+
+ + OK Code · Built by{" "} + + OpenKnots + + + +
+ GitHub + Releases + Discord +
+
+
+ ); +} diff --git a/apps/marketing/src/components/GetStarted.tsx b/apps/marketing/src/components/GetStarted.tsx new file mode 100644 index 000000000..8ae6af6c0 --- /dev/null +++ b/apps/marketing/src/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/src/components/Hero.tsx b/apps/marketing/src/components/Hero.tsx new file mode 100644 index 000000000..2b043ea31 --- /dev/null +++ b/apps/marketing/src/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/src/components/HowItWorks.tsx b/apps/marketing/src/components/HowItWorks.tsx new file mode 100644 index 000000000..0f19893f1 --- /dev/null +++ b/apps/marketing/src/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/src/components/Nav.tsx b/apps/marketing/src/components/Nav.tsx new file mode 100644 index 000000000..e15cd1906 --- /dev/null +++ b/apps/marketing/src/components/Nav.tsx @@ -0,0 +1,30 @@ +import { ExternalLink } from "./ExternalLink"; +import { LINKS } from "./links"; + +export function Nav() { + return ( + + ); +} diff --git a/apps/marketing/src/components/OKCodeMockup.tsx b/apps/marketing/src/components/OKCodeMockup.tsx new file mode 100644 index 000000000..ce93cf7aa --- /dev/null +++ b/apps/marketing/src/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/src/components/links.ts b/apps/marketing/src/components/links.ts new file mode 100644 index 000000000..689332c2c --- /dev/null +++ b/apps/marketing/src/components/links.ts @@ -0,0 +1,5 @@ +export const LINKS = { + github: "https://github.com/OpenKnots/okcode", + releases: "https://github.com/OpenKnots/okcode/releases", + discord: "https://discord.gg/openknot", +} as const; diff --git a/apps/marketing/src/pages/_app.tsx b/apps/marketing/src/pages/_app.tsx index 6d7d07833..dfcbefdca 100644 --- a/apps/marketing/src/pages/_app.tsx +++ b/apps/marketing/src/pages/_app.tsx @@ -1,6 +1,18 @@ import type { AppProps } from "next/app"; +import { DM_Sans } from "next/font/google"; import "../styles/globals.css"; +const dmSans = DM_Sans({ + subsets: ["latin"], + weight: ["300", "400", "500", "600", "700", "800"], + display: "swap", + variable: "--font-dm-sans", +}); + export default function App({ Component, pageProps }: AppProps) { - return ; + return ( +
+ +
+ ); } diff --git a/apps/marketing/src/pages/_document.tsx b/apps/marketing/src/pages/_document.tsx new file mode 100644 index 000000000..2b953114e --- /dev/null +++ b/apps/marketing/src/pages/_document.tsx @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from "next/document"; + +export default function Document() { + return ( + + + +
+ + + + ); +} diff --git a/apps/marketing/src/pages/index.tsx b/apps/marketing/src/pages/index.tsx index 8fbae0ae0..563d80ca1 100644 --- a/apps/marketing/src/pages/index.tsx +++ b/apps/marketing/src/pages/index.tsx @@ -1,25 +1,52 @@ import Head from "next/head"; +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 ( <> - OK Code - + OK Code — A Minimal Web GUI for Coding Agents + + + + + + + + + + + + + -
-
-

Create Next App

-

Basic starter page

-

- Get started by editing src/pages/index.tsx. -

-
+ +