Add homepage wall of love, FAQ, blog post, footer redesign, and SEO improvements
## Summary - Add Community (testimonials) section to homepage with inline avatars - Add FAQ section sourced from HN discussion questions - Add hero screenshot with next/image optimization - Add Show HN blog post with react-tweet embeds, star history chart, and HN quotes - Redesign footer with 4-column grid layout (Product, Resources, Legal, Social) - Add Download/GitHub CTA buttons at bottom of homepage and blog post - Add dev spacing controls for features, FAQ, and community sections - Fix hydration error (JSON-LD moved to head) - SEO: full metadata on blog posts, robots.txt, blog pages in sitemap, canonical URLs - Replace em dashes site-wide, fix notification descriptions ## Testing - `bun tsc --noEmit` passes clean - Dev server verified on port 3001 ## Related - Task: Add wall of love to main web page + landing screenshot
This commit is contained in:
parent
8ac554fb06
commit
be9b994a79
18 changed files with 8219 additions and 192 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -38,6 +38,7 @@ zig-out/
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.next/
|
||||||
|
|
||||||
# Test outputs
|
# Test outputs
|
||||||
tests/visual_output/
|
tests/visual_output/
|
||||||
|
|
|
||||||
5
web/app/assets/images.d.ts
vendored
Normal file
5
web/app/assets/images.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
declare module "*.png" {
|
||||||
|
import type { StaticImageData } from "next/image";
|
||||||
|
const content: StaticImageData;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
BIN
web/app/assets/landing-image.png
Normal file
BIN
web/app/assets/landing-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
|
|
@ -5,6 +5,35 @@ export const metadata: Metadata = {
|
||||||
title: "Introducing cmux",
|
title: "Introducing cmux",
|
||||||
description:
|
description:
|
||||||
"A native macOS terminal built on Ghostty, designed for running multiple AI coding agents side by side.",
|
"A native macOS terminal built on Ghostty, designed for running multiple AI coding agents side by side.",
|
||||||
|
keywords: [
|
||||||
|
"cmux",
|
||||||
|
"terminal",
|
||||||
|
"macOS",
|
||||||
|
"Ghostty",
|
||||||
|
"libghostty",
|
||||||
|
"AI coding agents",
|
||||||
|
"Claude Code",
|
||||||
|
"vertical tabs",
|
||||||
|
"split panes",
|
||||||
|
"socket API",
|
||||||
|
],
|
||||||
|
openGraph: {
|
||||||
|
title: "Introducing cmux",
|
||||||
|
description:
|
||||||
|
"A native macOS terminal built on Ghostty, designed for running multiple AI coding agents side by side.",
|
||||||
|
type: "article",
|
||||||
|
publishedTime: "2026-02-12T00:00:00Z",
|
||||||
|
url: "https://cmux.dev/blog/introducing-cmux",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary",
|
||||||
|
title: "Introducing cmux",
|
||||||
|
description:
|
||||||
|
"A native macOS terminal built on Ghostty, designed for running multiple AI coding agents side by side.",
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://cmux.dev/blog/introducing-cmux",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function IntroducingCmuxPage() {
|
export default function IntroducingCmuxPage() {
|
||||||
|
|
@ -20,7 +49,7 @@ export default function IntroducingCmuxPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>Introducing cmux</h1>
|
<h1>Introducing cmux</h1>
|
||||||
<time className="text-sm text-muted">February 12, 2026</time>
|
<time dateTime="2026-02-12" className="text-sm text-muted">February 12, 2026</time>
|
||||||
|
|
||||||
<p className="mt-6">
|
<p className="mt-6">
|
||||||
cmux is a native macOS terminal application built on top of Ghostty,
|
cmux is a native macOS terminal application built on top of Ghostty,
|
||||||
|
|
@ -31,7 +60,7 @@ export default function IntroducingCmuxPage() {
|
||||||
<h2>Why cmux?</h2>
|
<h2>Why cmux?</h2>
|
||||||
<p>
|
<p>
|
||||||
Modern development workflows often involve running several agents at
|
Modern development workflows often involve running several agents at
|
||||||
once — Claude Code, Codex, and other tools each in their own
|
once. Claude Code, Codex, and other tools each in their own
|
||||||
terminal. Keeping track of which ones need attention and switching
|
terminal. Keeping track of which ones need attention and switching
|
||||||
between them quickly is the problem cmux solves.
|
between them quickly is the problem cmux solves.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -39,23 +68,23 @@ export default function IntroducingCmuxPage() {
|
||||||
<h2>Key features</h2>
|
<h2>Key features</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<strong>Vertical tabs</strong> — see all your terminals at a
|
<strong>Vertical tabs</strong> : see all your terminals at a
|
||||||
glance in a sidebar
|
glance in a sidebar
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Notification rings</strong> — tabs flash when an agent
|
<strong>Notification rings</strong> : tabs flash when an agent
|
||||||
needs your input
|
needs your input
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Split panes</strong> — horizontal and vertical splits
|
<strong>Split panes</strong> : horizontal and vertical splits
|
||||||
within each workspace
|
within each workspace
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Socket API</strong> — programmatic control for creating
|
<strong>Socket API</strong> : programmatic control for creating
|
||||||
tabs and sending input
|
tabs and sending input
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>GPU-accelerated</strong> — powered by libghostty for
|
<strong>GPU-accelerated</strong> : powered by libghostty for
|
||||||
smooth rendering
|
smooth rendering
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,13 @@ export const metadata: Metadata = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const posts = [
|
const posts = [
|
||||||
|
{
|
||||||
|
slug: "show-hn-launch",
|
||||||
|
title: "Launching cmux on Show HN",
|
||||||
|
date: "2026-02-21",
|
||||||
|
summary:
|
||||||
|
"cmux hit the front page, went viral in Japan, and shipped 18 releases in 48 hours.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
slug: "introducing-cmux",
|
slug: "introducing-cmux",
|
||||||
title: "Introducing cmux",
|
title: "Introducing cmux",
|
||||||
|
|
|
||||||
212
web/app/blog/show-hn-launch/page.tsx
Normal file
212
web/app/blog/show-hn-launch/page.tsx
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Tweet } from "react-tweet";
|
||||||
|
import { DownloadButton } from "../../components/download-button";
|
||||||
|
import { GitHubButton } from "../../components/github-button";
|
||||||
|
import starHistory from "./star-history.png";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Launching cmux on Show HN",
|
||||||
|
description:
|
||||||
|
"cmux launched on Hacker News, hit #2, went viral in Japan, and people started building extensions on the CLI. Here's what happened.",
|
||||||
|
keywords: [
|
||||||
|
"cmux",
|
||||||
|
"Show HN",
|
||||||
|
"Hacker News",
|
||||||
|
"terminal",
|
||||||
|
"macOS",
|
||||||
|
"Ghostty",
|
||||||
|
"libghostty",
|
||||||
|
"AI coding agents",
|
||||||
|
"Claude Code",
|
||||||
|
"Codex",
|
||||||
|
"launch",
|
||||||
|
"vertical tabs",
|
||||||
|
"notification rings",
|
||||||
|
],
|
||||||
|
openGraph: {
|
||||||
|
title: "Launching cmux on Show HN",
|
||||||
|
description:
|
||||||
|
"cmux launched on Hacker News, hit #2, went viral in Japan, and people started building extensions on the CLI.",
|
||||||
|
type: "article",
|
||||||
|
publishedTime: "2026-02-21T00:00:00Z",
|
||||||
|
url: "https://cmux.dev/blog/show-hn-launch",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary",
|
||||||
|
title: "Launching cmux on Show HN",
|
||||||
|
description:
|
||||||
|
"cmux launched on Hacker News, hit #2, went viral in Japan, and people started building extensions on the CLI.",
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://cmux.dev/blog/show-hn-launch",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ShowHNLaunchPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-8">
|
||||||
|
<Link
|
||||||
|
href="/blog"
|
||||||
|
className="text-sm text-muted hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
← Back to blog
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>Launching cmux on Show HN</h1>
|
||||||
|
<time dateTime="2026-02-21" className="text-sm text-muted">February 21, 2026</time>
|
||||||
|
|
||||||
|
<p className="mt-6">
|
||||||
|
We posted cmux on{" "}
|
||||||
|
<a href="https://news.ycombinator.com/item?id=47079718">Show HN</a>{" "}
|
||||||
|
on Feb 19:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<blockquote className="border-l-2 border-border pl-4 my-6 text-muted space-y-3 text-[15px]">
|
||||||
|
<p>
|
||||||
|
I run a lot of Claude Code and Codex sessions in parallel. I was using
|
||||||
|
Ghostty with a bunch of split panes, and relying on native macOS
|
||||||
|
notifications to know when an agent needed me. But Claude Code's
|
||||||
|
notification body is always just "Claude is waiting for your
|
||||||
|
input" with no context, and with enough tabs open, I couldn't
|
||||||
|
even read the titles anymore.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I tried a few coding orchestrators but most of them were Electron/Tauri
|
||||||
|
apps and the performance bugged me. I also just prefer the terminal
|
||||||
|
since GUI orchestrators lock you into their workflow. So I built cmux as
|
||||||
|
a native macOS app in Swift/AppKit. It uses libghostty for terminal
|
||||||
|
rendering and reads your existing Ghostty config for themes, fonts,
|
||||||
|
colors, and more.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The main additions are the sidebar and notification system. The sidebar
|
||||||
|
has vertical tabs that show git branch, working directory, listening
|
||||||
|
ports, and the latest notification text for each workspace. The
|
||||||
|
notification system picks up terminal sequences (OSC 9/99/777) and has a
|
||||||
|
CLI (cmux notify) you can wire into agent hooks for Claude Code,
|
||||||
|
OpenCode, etc. When an agent is waiting, its pane gets a blue ring and
|
||||||
|
the tab lights up in the sidebar, so I can tell which one needs me
|
||||||
|
across splits and tabs. Cmd+Shift+U jumps to the most recent unread.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The in-app browser has a scriptable API. Agents can snapshot the
|
||||||
|
accessibility tree, get element refs, click, fill forms, evaluate JS,
|
||||||
|
and read console logs. You can split a browser pane next to your
|
||||||
|
terminal and have Claude Code interact with your dev server directly.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Everything is scriptable through the CLI and socket API: create
|
||||||
|
workspaces/tabs, split panes, send keystrokes, open URLs in the browser.
|
||||||
|
</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
At peak it hit #2 on Hacker News. Mitchell Hashimoto shared it:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Tweet id="2024913161238053296" />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
My favorite comment from the{" "}
|
||||||
|
<a href="https://news.ycombinator.com/item?id=47079718">HN thread</a>:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<blockquote className="border-l-2 border-border pl-4 my-6 text-muted space-y-3 text-[15px]">
|
||||||
|
<p>
|
||||||
|
Hey, this looks seriously awesome. Love the ideas here, specifically:
|
||||||
|
the programmability (I haven't tried it yet, but had been
|
||||||
|
considering learning tmux partly for this), layered UI, browser w/
|
||||||
|
api. Looking forward to giving this a spin. Also want to add that I
|
||||||
|
really appreciate Mitchell Hashimoto creating libghostty; it feels
|
||||||
|
like an exciting time to be a terminal user.
|
||||||
|
</p>
|
||||||
|
<p>Some feedback (since you were asking for it elsewhere in the thread!):</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li>
|
||||||
|
It's not obvious/easy to open browser dev tools (cmd-alt-i
|
||||||
|
didn't work), and when I did find it (right click page →
|
||||||
|
inspect element) none of the controls were visible but I could see
|
||||||
|
stuff happening when I moved my mouse over the panel
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Would be cool to borrow more of ghostty's behavior:
|
||||||
|
<ul className="list-disc pl-5 mt-1 space-y-1">
|
||||||
|
<li>
|
||||||
|
hotkey overrides – I have some things explicitly unmapped /
|
||||||
|
remapped in my ghostty config that conflict with some cmux
|
||||||
|
keybindings and weren't respected
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
command palette (cmd-shift-p) for less-often-used actions +
|
||||||
|
discoverability
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
cmd-z to "zoom in" to a pane is enormously useful imo
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-xs">
|
||||||
|
—{" "}
|
||||||
|
<a href="https://news.ycombinator.com/item?id=47083596" className="hover:text-foreground transition-colors">
|
||||||
|
johnthedebs
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Surprisingly, cmux went semi-viral in Japan!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Tweet id="2025129675262251026" />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Translation: "This looks good. A Ghostty-based terminal app
|
||||||
|
designed so you don't get lost running multiple CLIs like Claude
|
||||||
|
Code in parallel. The waiting-for-input panel gets a blue frame, and
|
||||||
|
it has its own notification system."
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Another exciting thing was seeing people build on top of the cmux
|
||||||
|
CLI. sasha built a pi-cmux extension that shows model info, token
|
||||||
|
usage, and agent state in the sidebar:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Tweet id="2024978414822916358" />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Everything in cmux is scriptable through the CLI: creating workspaces,
|
||||||
|
sending keystrokes, controlling the browser, reading notifications.
|
||||||
|
Part of the cmux philosophy is being programmable and composable, so
|
||||||
|
people can customize the way they work with coding agents. The
|
||||||
|
state of the art for coding agents is changing fast, and you don't
|
||||||
|
want to be locked into an inflexible GUI orchestrator that can't
|
||||||
|
keep up.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If you're running multiple coding agents,{" "}
|
||||||
|
<a href="https://github.com/manaflow-ai/cmux">give cmux a try</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="my-6">
|
||||||
|
<Image
|
||||||
|
src={starHistory}
|
||||||
|
alt="cmux GitHub star history showing growth from near 0 to 900+ stars after the Show HN launch"
|
||||||
|
placeholder="blur"
|
||||||
|
className="w-full rounded-xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-3 mt-12">
|
||||||
|
<DownloadButton location="blog-bottom" />
|
||||||
|
<GitHubButton />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
web/app/blog/show-hn-launch/star-history.png
Normal file
BIN
web/app/blog/show-hn-launch/star-history.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 428 KiB |
|
|
@ -43,26 +43,3 @@ export function NavLinks() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SiteFooter() {
|
|
||||||
return (
|
|
||||||
<footer className="py-8 flex justify-center">
|
|
||||||
<div className="flex flex-wrap justify-center items-center gap-4 text-sm text-muted px-6">
|
|
||||||
<a
|
|
||||||
href="https://github.com/manaflow-ai/cmux"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
onClick={() => posthog.capture("cmuxterm_github_clicked", { location: "footer" })}
|
|
||||||
className="hover:text-foreground transition-colors"
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
<a href="https://twitter.com/manaflowai" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">Twitter</a>
|
|
||||||
<a href="https://discord.gg/xsgFEVrWCZ" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">Discord</a>
|
|
||||||
<Link href="/privacy-policy" className="hover:text-foreground transition-colors">Privacy</Link>
|
|
||||||
<Link href="/terms-of-service" className="hover:text-foreground transition-colors">Terms</Link>
|
|
||||||
<Link href="/eula" className="hover:text-foreground transition-colors">EULA</Link>
|
|
||||||
<a href="mailto:founders@manaflow.com" className="hover:text-foreground transition-colors">Contact</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
85
web/app/components/site-footer.tsx
Normal file
85
web/app/components/site-footer.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
heading: "Product",
|
||||||
|
links: [
|
||||||
|
{ label: "Blog", href: "/blog" },
|
||||||
|
{ label: "Community", href: "/community" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "Resources",
|
||||||
|
links: [
|
||||||
|
{ label: "Docs", href: "/docs/getting-started" },
|
||||||
|
{ label: "Changelog", href: "/docs/changelog" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "Legal",
|
||||||
|
links: [
|
||||||
|
{ label: "Privacy", href: "/privacy-policy" },
|
||||||
|
{ label: "Terms", href: "/terms-of-service" },
|
||||||
|
{ label: "EULA", href: "/eula" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "Social",
|
||||||
|
links: [
|
||||||
|
{ label: "GitHub", href: "https://github.com/manaflow-ai/cmux" },
|
||||||
|
{ label: "X / Twitter", href: "https://twitter.com/manaflowai" },
|
||||||
|
{ label: "Discord", href: "https://discord.gg/xsgFEVrWCZ" },
|
||||||
|
{ label: "Contact", href: "mailto:founders@manaflow.com" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function isExternal(href: string) {
|
||||||
|
return href.startsWith("http") || href.startsWith("mailto:");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SiteFooter() {
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="mt-16">
|
||||||
|
<div className="max-w-2xl mx-auto px-6 py-12">
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-8">
|
||||||
|
{columns.map((col) => (
|
||||||
|
<div key={col.heading}>
|
||||||
|
<h3 className="text-xs font-medium text-muted tracking-tight mb-3">
|
||||||
|
{col.heading}
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{col.links.map((link) => (
|
||||||
|
<li key={link.href}>
|
||||||
|
{isExternal(link.href) ? (
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-sm text-muted hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
href={link.href}
|
||||||
|
className="text-sm text-muted hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted mt-10">
|
||||||
|
© {year} Manaflow
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,10 @@ type DevValues = {
|
||||||
downloadAbove: number;
|
downloadAbove: number;
|
||||||
downloadBelow: number;
|
downloadBelow: number;
|
||||||
featuresLh: number;
|
featuresLh: number;
|
||||||
featuresMb: number;
|
featuresPt: number;
|
||||||
|
featuresPb: number;
|
||||||
|
communityGap: number;
|
||||||
|
faqPt: number;
|
||||||
docsPt: number;
|
docsPt: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -20,9 +23,12 @@ const defaults: DevValues = {
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
subtitleLh: 1.5,
|
subtitleLh: 1.5,
|
||||||
downloadAbove: 21,
|
downloadAbove: 21,
|
||||||
downloadBelow: 33,
|
downloadBelow: 16,
|
||||||
featuresLh: 1.275,
|
featuresLh: 1.275,
|
||||||
featuresMb: 23,
|
featuresPt: 12,
|
||||||
|
featuresPb: 15,
|
||||||
|
communityGap: 6,
|
||||||
|
faqPt: 0,
|
||||||
docsPt: 8,
|
docsPt: 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -67,8 +73,21 @@ function applyToDOM(v: DevValues) {
|
||||||
const featuresUl = el("features-ul");
|
const featuresUl = el("features-ul");
|
||||||
if (featuresUl) featuresUl.style.lineHeight = `${v.featuresLh}`;
|
if (featuresUl) featuresUl.style.lineHeight = `${v.featuresLh}`;
|
||||||
|
|
||||||
const featuresSpacer = el("features-spacer");
|
const features = el("features");
|
||||||
if (featuresSpacer) featuresSpacer.style.height = `${v.featuresMb}px`;
|
if (features) {
|
||||||
|
features.style.paddingTop = `${v.featuresPt}px`;
|
||||||
|
features.style.paddingBottom = `${v.featuresPb}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const communityUl = el("community-ul");
|
||||||
|
if (communityUl) {
|
||||||
|
communityUl.style.display = "flex";
|
||||||
|
communityUl.style.flexDirection = "column";
|
||||||
|
communityUl.style.gap = `${v.communityGap}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const faqTopSpacer = el("faq-top-spacer");
|
||||||
|
if (faqTopSpacer) faqTopSpacer.style.height = `${v.faqPt}px`;
|
||||||
|
|
||||||
const docsContent = el("docs-content");
|
const docsContent = el("docs-content");
|
||||||
if (docsContent) docsContent.style.paddingTop = `${v.docsPt}px`;
|
if (docsContent) docsContent.style.paddingTop = `${v.docsPt}px`;
|
||||||
|
|
@ -156,7 +175,16 @@ export function DevPanel() {
|
||||||
|
|
||||||
<Section label="Features">
|
<Section label="Features">
|
||||||
<Row label="line-h" value={vals.featuresLh} onChange={(v) => update({ featuresLh: v })} min={1} max={2.5} step={0.025} unit="" w={16} />
|
<Row label="line-h" value={vals.featuresLh} onChange={(v) => update({ featuresLh: v })} min={1} max={2.5} step={0.025} unit="" w={16} />
|
||||||
<Row label="mb" value={vals.featuresMb} onChange={(v) => update({ featuresMb: v })} />
|
<Row label="pt" value={vals.featuresPt} onChange={(v) => update({ featuresPt: v })} />
|
||||||
|
<Row label="pb" value={vals.featuresPb} onChange={(v) => update({ featuresPb: v })} />
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section label="Community">
|
||||||
|
<Row label="gap" value={vals.communityGap} onChange={(v) => update({ communityGap: v })} />
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section label="FAQ">
|
||||||
|
<Row label="pt" value={vals.faqPt} onChange={(v) => update({ faqPt: v })} />
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section label="Docs">
|
<Section label="Docs">
|
||||||
|
|
@ -173,7 +201,10 @@ export function DevPanel() {
|
||||||
`download-above: ${vals.downloadAbove}px`,
|
`download-above: ${vals.downloadAbove}px`,
|
||||||
`download-below: ${vals.downloadBelow}px`,
|
`download-below: ${vals.downloadBelow}px`,
|
||||||
`features-lh: ${vals.featuresLh}`,
|
`features-lh: ${vals.featuresLh}`,
|
||||||
`features-mb: ${vals.featuresMb}px`,
|
`features-pt: ${vals.featuresPt}px`,
|
||||||
|
`features-pb: ${vals.featuresPb}px`,
|
||||||
|
`community-gap: ${vals.communityGap}px`,
|
||||||
|
`faq-pt: ${vals.faqPt}px`,
|
||||||
`docs-pt: ${vals.docsPt}px`,
|
`docs-pt: ${vals.docsPt}px`,
|
||||||
].join(", ");
|
].join(", ");
|
||||||
navigator.clipboard.writeText(text);
|
navigator.clipboard.writeText(text);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import { Providers } from "./providers";
|
import { Providers } from "./providers";
|
||||||
|
|
||||||
import { DevPanel } from "./components/spacing-control";
|
import { DevPanel } from "./components/spacing-control";
|
||||||
import { SiteFooter } from "./components/nav-links";
|
import { SiteFooter } from "./components/site-footer";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
|
|
@ -76,6 +76,10 @@ export default function RootLayout({
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<meta name="theme-color" content="#0a0a0a" />
|
<meta name="theme-color" content="#0a0a0a" />
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `(function(){try{var t=localStorage.getItem("theme");var light=t==="light"||(t==="system"&&window.matchMedia("(prefers-color-scheme:light)").matches);if(!light)document.documentElement.classList.add("dark");var m=document.querySelector('meta[name="theme-color"]');if(m)m.content=light?"#fafafa":"#0a0a0a"}catch(e){}})()`,
|
__html: `(function(){try{var t=localStorage.getItem("theme");var light=t==="light"||(t==="system"&&window.matchMedia("(prefers-color-scheme:light)").matches);if(!light)document.documentElement.classList.add("dark");var m=document.querySelector('meta[name="theme-color"]');if(m)m.content=light?"#fafafa":"#0a0a0a"}catch(e){}})()`,
|
||||||
|
|
@ -85,10 +89,6 @@ export default function RootLayout({
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}
|
||||||
>
|
>
|
||||||
<script
|
|
||||||
type="application/ld+json"
|
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
||||||
/>
|
|
||||||
<Providers>
|
<Providers>
|
||||||
{children}
|
{children}
|
||||||
<SiteFooter />
|
<SiteFooter />
|
||||||
|
|
|
||||||
140
web/app/page.tsx
140
web/app/page.tsx
|
|
@ -1,8 +1,11 @@
|
||||||
|
import Image from "next/image";
|
||||||
import Balancer from "react-wrap-balancer";
|
import Balancer from "react-wrap-balancer";
|
||||||
|
import landingImage from "./assets/landing-image.png";
|
||||||
import { TypingTagline } from "./typing";
|
import { TypingTagline } from "./typing";
|
||||||
import { DownloadButton } from "./components/download-button";
|
import { DownloadButton } from "./components/download-button";
|
||||||
import { GitHubButton } from "./components/github-button";
|
import { GitHubButton } from "./components/github-button";
|
||||||
import { SiteHeader } from "./components/site-header";
|
import { SiteHeader } from "./components/site-header";
|
||||||
|
import { testimonials } from "./testimonials";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -35,13 +38,13 @@ export default function Home() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Download */}
|
{/* Download */}
|
||||||
<div className="flex flex-wrap items-center gap-3" data-dev="download" style={{ marginTop: 21, marginBottom: 33 }}>
|
<div className="flex flex-wrap items-center gap-3" data-dev="download" style={{ marginTop: 21, marginBottom: 16 }}>
|
||||||
<DownloadButton location="hero" />
|
<DownloadButton location="hero" />
|
||||||
<GitHubButton />
|
<GitHubButton />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Features */}
|
{/* Features */}
|
||||||
<section data-dev="features">
|
<section data-dev="features" style={{ paddingTop: 12, paddingBottom: 15 }}>
|
||||||
<h2 className="text-xs font-medium text-muted tracking-tight mb-3">
|
<h2 className="text-xs font-medium text-muted tracking-tight mb-3">
|
||||||
Features
|
Features
|
||||||
</h2>
|
</h2>
|
||||||
|
|
@ -110,9 +113,140 @@ export default function Home() {
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div data-dev="features-spacer" style={{ height: 23 }} />
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Screenshot - break out of max-w-2xl to be wider */}
|
||||||
|
<div data-dev="screenshot" className="mb-12 -mx-6 sm:-mx-24 md:-mx-40 lg:-mx-72 xl:-mx-96">
|
||||||
|
<Image
|
||||||
|
src={landingImage}
|
||||||
|
alt="cmux terminal app screenshot"
|
||||||
|
priority
|
||||||
|
placeholder="blur"
|
||||||
|
className="w-full rounded-xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FAQ */}
|
||||||
|
<div data-dev="faq-top-spacer" style={{ height: 0 }} />
|
||||||
|
<section data-dev="faq" className="mb-10">
|
||||||
|
<h2 className="text-xs font-medium text-muted tracking-tight mb-3">
|
||||||
|
FAQ
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-5 text-[15px]" style={{ lineHeight: 1.5 }}>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium mb-1">How does cmux relate to Ghostty?</p>
|
||||||
|
<p className="text-muted">
|
||||||
|
cmux is not a fork of Ghostty. It uses{" "}
|
||||||
|
<a href="https://github.com/ghostty-org/ghostty" className="underline underline-offset-2 decoration-border hover:decoration-foreground transition-colors">libghostty</a>{" "}
|
||||||
|
as a library for terminal rendering, the same way apps use WebKit for web views.
|
||||||
|
Ghostty is a standalone terminal; cmux is a different app built on top of its rendering engine.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium mb-1">What platforms does it support?</p>
|
||||||
|
<p className="text-muted">
|
||||||
|
macOS only, for now. cmux is a native Swift + AppKit app.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium mb-1">What coding agents does cmux work with?</p>
|
||||||
|
<p className="text-muted">
|
||||||
|
All of them. cmux is a terminal, so any agent that runs in a terminal works out of the
|
||||||
|
box: Claude Code, Codex, OpenCode, Gemini CLI, Kiro, Aider, Goose, Amp, Cline,
|
||||||
|
Cursor Agent, and anything else you can launch from the command line.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium mb-1">How do notifications work?</p>
|
||||||
|
<p className="text-muted">
|
||||||
|
When a process needs attention, cmux shows notification rings around panes,
|
||||||
|
unread badges in the sidebar, a notification popover, and a macOS desktop
|
||||||
|
notification. These fire automatically via standard terminal escape sequences
|
||||||
|
(OSC 9/99/777), or you can trigger them with the{" "}
|
||||||
|
<a href="/docs/notifications" className="underline underline-offset-2 decoration-border hover:decoration-foreground transition-colors">cmux CLI</a>{" "}
|
||||||
|
and{" "}
|
||||||
|
<a href="/docs/notifications" className="underline underline-offset-2 decoration-border hover:decoration-foreground transition-colors">Claude Code hooks</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium mb-1">Can I customize keyboard shortcuts?</p>
|
||||||
|
<p className="text-muted">
|
||||||
|
Terminal keybindings are read from your Ghostty config
|
||||||
|
file (<code className="text-xs bg-code-bg px-1.5 py-0.5 rounded">~/.config/ghostty/config</code>).
|
||||||
|
cmux-specific shortcuts (workspaces, splits, browser, notifications) can be
|
||||||
|
customized in Settings. See the{" "}
|
||||||
|
<a href="/docs/keyboard-shortcuts" className="underline underline-offset-2 decoration-border hover:decoration-foreground transition-colors">default shortcuts</a>{" "}
|
||||||
|
for a full list.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium mb-1">How does it compare to tmux?</p>
|
||||||
|
<p className="text-muted">
|
||||||
|
tmux is a terminal multiplexer that runs inside any terminal. cmux is a native macOS app
|
||||||
|
with a GUI: vertical tabs, split panes, an embedded browser, and a socket API are all
|
||||||
|
built in. No config files or prefix keys needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium mb-1">Is cmux free?</p>
|
||||||
|
<p className="text-muted">
|
||||||
|
Yes, cmux is free to use. The source code is available on{" "}
|
||||||
|
<a href="https://github.com/manaflow-ai/cmux" className="underline underline-offset-2 decoration-border hover:decoration-foreground transition-colors">GitHub</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Community */}
|
||||||
|
<section data-dev="community" className="mb-10">
|
||||||
|
<h2 className="text-xs font-medium text-muted tracking-tight mb-3">
|
||||||
|
Community
|
||||||
|
</h2>
|
||||||
|
<ul data-dev="community-ul" className="text-[15px]" style={{ lineHeight: 1.5, display: "flex", flexDirection: "column", gap: 6 }}>
|
||||||
|
{testimonials.map((t) => (
|
||||||
|
<li key={t.url}>
|
||||||
|
<span>
|
||||||
|
<a
|
||||||
|
href={t.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<span className="text-muted group-hover:text-foreground transition-colors">
|
||||||
|
"{t.text}"
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{" "}
|
||||||
|
<a
|
||||||
|
href={t.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 text-muted hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
—
|
||||||
|
{t.avatar && (
|
||||||
|
<img
|
||||||
|
src={t.avatar}
|
||||||
|
alt={t.name}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="rounded-full inline-block"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{t.name}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Bottom CTA */}
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-3 mt-12">
|
||||||
|
<DownloadButton location="bottom" />
|
||||||
|
<GitHubButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ url: base, lastModified: new Date(), changeFrequency: "weekly", priority: 1 },
|
{ url: base, lastModified: new Date(), changeFrequency: "weekly", priority: 1 },
|
||||||
|
{ url: `${base}/blog`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.8 },
|
||||||
|
{ url: `${base}/blog/show-hn-launch`, lastModified: "2026-02-21", changeFrequency: "monthly", priority: 0.7 },
|
||||||
|
{ url: `${base}/blog/introducing-cmux`, lastModified: "2026-02-12", changeFrequency: "monthly", priority: 0.7 },
|
||||||
{ url: `${base}/docs/getting-started`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.9 },
|
{ url: `${base}/docs/getting-started`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.9 },
|
||||||
{ url: `${base}/docs/concepts`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/docs/concepts`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/docs/configuration`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/docs/configuration`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
||||||
|
|
@ -12,5 +15,6 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
{ url: `${base}/docs/api`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/docs/api`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/docs/notifications`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/docs/notifications`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/docs/changelog`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.5 },
|
{ url: `${base}/docs/changelog`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.5 },
|
||||||
|
{ url: `${base}/community`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.5 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
130
web/app/testimonials.tsx
Normal file
130
web/app/testimonials.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
export const testimonials = [
|
||||||
|
{
|
||||||
|
name: "Mitchell Hashimoto",
|
||||||
|
handle: "@mitchellh",
|
||||||
|
avatar:
|
||||||
|
"https://pbs.twimg.com/profile_images/1141762999838842880/64_Y4_XB_400x400.jpg",
|
||||||
|
text: "Another day another libghostty-based project, this time a macOS terminal with vertical tabs, better organization/notifications, embedded/scriptable browser specifically targeted towards people who use a ton of terminal-based agentic workflows.",
|
||||||
|
url: "https://x.com/mitchellh/status/2024913161238053296",
|
||||||
|
platform: "x" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "johnthedebs",
|
||||||
|
handle: "johnthedebs",
|
||||||
|
avatar: null,
|
||||||
|
text: "Hey, this looks seriously awesome. Love the ideas here, specifically: the programmability, layered UI, browser w/ api. Looking forward to giving this a spin. Also want to add that I really appreciate Mitchell Hashimoto creating libghostty; it feels like an exciting time to be a terminal user.",
|
||||||
|
url: "https://news.ycombinator.com/item?id=47083596",
|
||||||
|
platform: "hn" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Joe Riddle",
|
||||||
|
handle: "@joeriddles10",
|
||||||
|
avatar:
|
||||||
|
"https://pbs.twimg.com/profile_images/1466920091707076608/pxfGMeC0_400x400.jpg",
|
||||||
|
text: "Vertical tabs in my terminal \u{1F924} I never thought of that before. I use and love Firefox vertical tabs.",
|
||||||
|
url: "https://x.com/joeriddles10/status/2024914132416561465",
|
||||||
|
platform: "x" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dchu17",
|
||||||
|
handle: "dchu17",
|
||||||
|
avatar: null,
|
||||||
|
text: "Gave this a run and it was pretty intuitive. Good work!",
|
||||||
|
url: "https://news.ycombinator.com/item?id=47082577",
|
||||||
|
platform: "hn" as const,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export type Testimonial = (typeof testimonials)[number];
|
||||||
|
|
||||||
|
export function PlatformIcon({ platform }: { platform: "x" | "hn" }) {
|
||||||
|
if (platform === "x") {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="text-muted"
|
||||||
|
>
|
||||||
|
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
className="text-muted"
|
||||||
|
>
|
||||||
|
<rect width="256" height="256" rx="28" fill="#ff6600" />
|
||||||
|
<text
|
||||||
|
x="128"
|
||||||
|
y="188"
|
||||||
|
fontSize="180"
|
||||||
|
fontWeight="bold"
|
||||||
|
fontFamily="sans-serif"
|
||||||
|
fill="white"
|
||||||
|
textAnchor="middle"
|
||||||
|
>
|
||||||
|
Y
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Initials({ name }: { name: string }) {
|
||||||
|
const initials = name
|
||||||
|
.split(/[\s_-]+/)
|
||||||
|
.map((w) => w[0])
|
||||||
|
.join("")
|
||||||
|
.toUpperCase()
|
||||||
|
.slice(0, 2);
|
||||||
|
return (
|
||||||
|
<div className="w-10 h-10 rounded-full bg-code-bg border border-border flex items-center justify-center text-xs font-medium text-muted shrink-0">
|
||||||
|
{initials}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TestimonialCard({
|
||||||
|
testimonial,
|
||||||
|
}: {
|
||||||
|
testimonial: Testimonial;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={testimonial.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group block rounded-xl border border-border p-5 hover:bg-code-bg transition-colors break-inside-avoid mb-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
{testimonial.avatar ? (
|
||||||
|
<img
|
||||||
|
src={testimonial.avatar}
|
||||||
|
alt={testimonial.name}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="rounded-full shrink-0"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Initials name={testimonial.name} />
|
||||||
|
)}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="font-medium text-sm truncate">
|
||||||
|
{testimonial.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted truncate">
|
||||||
|
{testimonial.handle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PlatformIcon platform={testimonial.platform} />
|
||||||
|
</div>
|
||||||
|
<p className="text-[15px] leading-relaxed text-muted group-hover:text-foreground transition-colors">
|
||||||
|
{testimonial.text}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { SiteHeader } from "../components/site-header";
|
import { SiteHeader } from "../components/site-header";
|
||||||
|
import { testimonials, TestimonialCard } from "../testimonials";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Wall of Love — cmux",
|
title: "Wall of Love — cmux",
|
||||||
|
|
@ -7,153 +8,6 @@ export const metadata: Metadata = {
|
||||||
"What people are saying about cmux, the terminal built for multitasking.",
|
"What people are saying about cmux, the terminal built for multitasking.",
|
||||||
};
|
};
|
||||||
|
|
||||||
const testimonials = [
|
|
||||||
{
|
|
||||||
name: "Mitchell Hashimoto",
|
|
||||||
handle: "@mitchellh",
|
|
||||||
avatar:
|
|
||||||
"https://pbs.twimg.com/profile_images/1141762999838842880/64_Y4_XB_400x400.jpg",
|
|
||||||
text: "Another day another libghostty-based project, this time a macOS terminal with vertical tabs, better organization/notifications, embedded/scriptable browser specifically targeted towards people who use a ton of terminal-based agentic workflows.",
|
|
||||||
url: "https://x.com/mitchellh/status/2024913161238053296",
|
|
||||||
platform: "x" as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Oliver Kriška",
|
|
||||||
handle: "@quatermain32",
|
|
||||||
avatar:
|
|
||||||
"https://pbs.twimg.com/profile_images/674992361974464512/ClmHiw_P_400x400.jpg",
|
|
||||||
text: "I have used it for whole day and it's really great. Some bugs like opening file in integrated agent browser (yes, it has browser) but other than that it's good.",
|
|
||||||
url: "https://x.com/quatermain32/status/2024919743484891629",
|
|
||||||
platform: "x" as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "johnthedebs",
|
|
||||||
handle: "johnthedebs",
|
|
||||||
avatar: null,
|
|
||||||
text: "Hey, this looks seriously awesome. Love the ideas here, specifically: the programmability, layered UI, browser w/ api. Looking forward to giving this a spin. Also want to add that I really appreciate Mitchell Hashimoto creating libghostty; it feels like an exciting time to be a terminal user.",
|
|
||||||
url: "https://news.ycombinator.com/item?id=47079718",
|
|
||||||
platform: "hn" as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Joe Riddle",
|
|
||||||
handle: "@joeriddles10",
|
|
||||||
avatar:
|
|
||||||
"https://pbs.twimg.com/profile_images/1466920091707076608/pxfGMeC0_400x400.jpg",
|
|
||||||
text: "Vertical tabs in my terminal \u{1F924} I never thought of that before. I use and love Firefox vertical tabs.",
|
|
||||||
url: "https://x.com/joeriddles10/status/2024914132416561465",
|
|
||||||
platform: "x" as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Marc",
|
|
||||||
handle: "@prodigy00",
|
|
||||||
avatar:
|
|
||||||
"https://pbs.twimg.com/profile_images/1726697382337724417/AGafbkp1_400x400.jpg",
|
|
||||||
text: "This is niceeeeee!",
|
|
||||||
url: "https://x.com/prodigy00/status/2024946851401613399",
|
|
||||||
platform: "x" as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dchu17",
|
|
||||||
handle: "dchu17",
|
|
||||||
avatar: null,
|
|
||||||
text: "Gave this a run and it was pretty intuitive. Good work!",
|
|
||||||
url: "https://news.ycombinator.com/item?id=47082577",
|
|
||||||
platform: "hn" as const,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function PlatformIcon({ platform }: { platform: "x" | "hn" }) {
|
|
||||||
if (platform === "x") {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
className="text-muted"
|
|
||||||
>
|
|
||||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 0 256 256"
|
|
||||||
className="text-muted"
|
|
||||||
>
|
|
||||||
<rect width="256" height="256" rx="28" fill="#ff6600" />
|
|
||||||
<text
|
|
||||||
x="128"
|
|
||||||
y="188"
|
|
||||||
fontSize="180"
|
|
||||||
fontWeight="bold"
|
|
||||||
fontFamily="sans-serif"
|
|
||||||
fill="white"
|
|
||||||
textAnchor="middle"
|
|
||||||
>
|
|
||||||
Y
|
|
||||||
</text>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Initials({ name }: { name: string }) {
|
|
||||||
const initials = name
|
|
||||||
.split(/[\s_-]+/)
|
|
||||||
.map((w) => w[0])
|
|
||||||
.join("")
|
|
||||||
.toUpperCase()
|
|
||||||
.slice(0, 2);
|
|
||||||
return (
|
|
||||||
<div className="w-10 h-10 rounded-full bg-code-bg border border-border flex items-center justify-center text-xs font-medium text-muted shrink-0">
|
|
||||||
{initials}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TestimonialCard({
|
|
||||||
testimonial,
|
|
||||||
}: {
|
|
||||||
testimonial: (typeof testimonials)[number];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={testimonial.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="group block rounded-xl border border-border p-5 hover:bg-code-bg transition-colors break-inside-avoid mb-4"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 mb-3">
|
|
||||||
{testimonial.avatar ? (
|
|
||||||
<img
|
|
||||||
src={testimonial.avatar}
|
|
||||||
alt={testimonial.name}
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
className="rounded-full shrink-0"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Initials name={testimonial.name} />
|
|
||||||
)}
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="font-medium text-sm truncate">
|
|
||||||
{testimonial.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted truncate">
|
|
||||||
{testimonial.handle}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<PlatformIcon platform={testimonial.platform} />
|
|
||||||
</div>
|
|
||||||
<p className="text-[15px] leading-relaxed text-muted group-hover:text-foreground transition-colors">
|
|
||||||
{testimonial.text}
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function WallOfLovePage() {
|
export default function WallOfLovePage() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
"posthog-js": "^1.350.0",
|
"posthog-js": "^1.350.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
|
"react-tweet": "^3.3.0",
|
||||||
"react-wrap-balancer": "^1.1.1",
|
"react-wrap-balancer": "^1.1.1",
|
||||||
"shiki": "^3.22.0",
|
"shiki": "^3.22.0",
|
||||||
},
|
},
|
||||||
|
|
@ -430,6 +431,8 @@
|
||||||
|
|
||||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
@ -842,6 +845,8 @@
|
||||||
|
|
||||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
|
"react-tweet": ["react-tweet@3.3.0", "", { "dependencies": { "@swc/helpers": "^0.5.3", "clsx": "^2.0.0", "swr": "^2.2.4" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-gSIG2169ZK7UH6rBzuU+j1xnQbH3IlOTLEkuGrRiJJTMgETik+h+26yHyyVKrLkzwrOaYPk4K3OtEKycqKgNLw=="],
|
||||||
|
|
||||||
"react-wrap-balancer": ["react-wrap-balancer@1.1.1", "", { "peerDependencies": { "react": ">=16.8.0 || ^17.0.0 || ^18" } }, "sha512-AB+l7FPRWl6uZ28VcJ8skkwLn2+UC62bjiw8tQUrZPlEWDVnR9MG0lghyn7EyxuJSsFEpht4G+yh2WikEqQ/5Q=="],
|
"react-wrap-balancer": ["react-wrap-balancer@1.1.1", "", { "peerDependencies": { "react": ">=16.8.0 || ^17.0.0 || ^18" } }, "sha512-AB+l7FPRWl6uZ28VcJ8skkwLn2+UC62bjiw8tQUrZPlEWDVnR9MG0lghyn7EyxuJSsFEpht4G+yh2WikEqQ/5Q=="],
|
||||||
|
|
||||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||||
|
|
@ -928,6 +933,8 @@
|
||||||
|
|
||||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
|
"swr": ["swr@2.4.0", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-sUlC20T8EOt1pHmDiqueUWMmRRX03W7w5YxovWX7VR2KHEPCTMly85x05vpkP5i6Bu4h44ePSMD9Tc+G2MItFw=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||||
|
|
||||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
@ -978,6 +985,8 @@
|
||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
|
|
||||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
|
|
||||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||||
|
|
|
||||||
7548
web/package-lock.json
generated
Normal file
7548
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -15,6 +15,7 @@
|
||||||
"posthog-js": "^1.350.0",
|
"posthog-js": "^1.350.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
|
"react-tweet": "^3.3.0",
|
||||||
"react-wrap-balancer": "^1.1.1",
|
"react-wrap-balancer": "^1.1.1",
|
||||||
"shiki": "^3.22.0"
|
"shiki": "^3.22.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue