Add docs, blog, community pages and polish landing page layout

- Add docs pages (getting-started, changelog, keyboard-shortcuts)
- Add blog, community, and legal pages (privacy, terms, EULA)
- Add site header, footer, download button, and nav components
- Add sitemap and robots.txt generation
- Narrow main page container (max-w-2xl), fix footer positioning
- Switch README feature list to colon style
This commit is contained in:
Lawrence Chen 2026-02-09 23:38:05 -08:00
parent 5febb66873
commit f970cdcf33
37 changed files with 3304 additions and 296 deletions

View file

@ -0,0 +1,20 @@
export function Callout({
type = "info",
children,
}: {
type?: "info" | "warn";
children: React.ReactNode;
}) {
const styles =
type === "warn"
? "border-l-amber-500 bg-amber-500/5"
: "border-l-blue-500 bg-blue-500/5";
return (
<div
className={`${styles} border-l-2 px-4 py-3 mb-4 rounded-r-lg text-[14px] text-muted leading-relaxed`}
>
{children}
</div>
);
}

View file

@ -0,0 +1,59 @@
import { codeToHtml } from "shiki";
export async function CodeBlock({
children,
title,
lang,
variant = "code",
}: {
children: string;
title?: string;
lang?: string;
variant?: "code" | "ascii";
}) {
const lineHeightClass =
variant === "ascii" ? "leading-[1.15]" : "leading-[1.45]";
if (lang && variant !== "ascii") {
const html = await codeToHtml(children, {
lang,
themes: { light: "github-light", dark: "github-dark" },
defaultColor: false,
});
return (
<div className="mb-4">
{title && (
<div className="text-[11px] font-mono text-muted px-4 py-1.5 bg-code-bg border border-border border-b-0 rounded-t-lg">
{title}
</div>
)}
<div
className={`[&_pre]:bg-code-bg [&_pre]:border [&_pre]:border-border [&_pre]:px-4 [&_pre]:py-3 [&_pre]:overflow-x-auto [&_pre]:text-[13px] [&_pre]:${lineHeightClass} [&_pre]:font-mono ${
title
? "[&_pre]:rounded-b-lg [&_pre]:border-t-0"
: "[&_pre]:rounded-lg"
} [&_code]:bg-transparent [&_code]:p-0`}
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
);
}
return (
<div className="mb-4">
{title && (
<div className="text-[11px] font-mono text-muted px-4 py-1.5 bg-code-bg border border-border border-b-0 rounded-t-lg">
{title}
</div>
)}
<pre
className={`bg-code-bg border border-border px-4 py-3 overflow-x-auto text-[13px] ${lineHeightClass} font-mono ${
title ? "rounded-b-lg" : "rounded-lg"
}`}
>
<code>{children}</code>
</pre>
</div>
);
}

View file

@ -0,0 +1,9 @@
export const navItems = [
{ title: "Getting Started", href: "/docs/getting-started" },
{ title: "Concepts", href: "/docs/concepts" },
{ title: "Configuration", href: "/docs/configuration" },
{ title: "Keyboard Shortcuts", href: "/docs/keyboard-shortcuts" },
{ title: "API Reference", href: "/docs/api" },
{ title: "Notifications", href: "/docs/notifications" },
{ title: "Changelog", href: "/docs/changelog" },
];

View file

@ -0,0 +1,41 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { navItems } from "./docs-nav-items";
export function DocsPager() {
const pathname = usePathname();
const index = navItems.findIndex((item) => item.href === pathname);
const prev = index > 0 ? navItems[index - 1] : null;
const next = index < navItems.length - 1 ? navItems[index + 1] : null;
if (!prev && !next) return null;
return (
<nav className="flex items-center justify-between mt-12 pt-6 border-t border-border text-[14px]">
{prev ? (
<Link
href={prev.href}
className="flex items-center gap-1.5 text-muted hover:text-foreground transition-colors"
>
<span aria-hidden>&larr;</span>
{prev.title}
</Link>
) : (
<span />
)}
{next ? (
<Link
href={next.href}
className="flex items-center gap-1.5 text-muted hover:text-foreground transition-colors"
>
{next.title}
<span aria-hidden>&rarr;</span>
</Link>
) : (
<span />
)}
</nav>
);
}

View file

@ -0,0 +1,31 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { navItems } from "./docs-nav-items";
export function DocsSidebar({ onNavigate }: { onNavigate?: () => void }) {
const pathname = usePathname();
return (
<nav className="space-y-0.5">
{navItems.map((item) => {
const active = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
onClick={onNavigate}
className={`block px-3 py-1.5 text-[14px] rounded-md transition-colors ${
active
? "text-foreground font-medium bg-code-bg"
: "text-muted hover:text-foreground"
}`}
>
{item.title}
</Link>
);
})}
</nav>
);
}

View file

@ -0,0 +1,22 @@
export function DownloadButton({ size = "default" }: { size?: "default" | "sm" }) {
const isSmall = size === "sm";
return (
<a
href="https://github.com/manaflow-ai/cmux/releases/latest/download/cmux-macos.dmg"
className={`inline-flex items-center rounded-full font-medium bg-foreground hover:opacity-85 transition-opacity ${
isSmall ? "gap-2 px-4 py-1.5 text-xs" : "gap-2.5 px-5 py-2.5 text-[15px]"
}`}
style={{ color: "var(--background)", textDecoration: "none" }}
>
<svg
width={isSmall ? 12 : 16}
height={isSmall ? 14 : 19}
viewBox="0 0 814 1000"
fill="currentColor"
>
<path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57.8-155.5-127.4c-58.3-81.6-105.6-208.4-105.6-328.6 0-193 125.6-295.5 249.2-295.5 65.7 0 120.5 43.1 161.7 43.1 39.2 0 100.4-45.8 175.1-45.8 28.3 0 130.3 2.6 197.2 99.2zM554.1 159.4c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.9 32.4-57.2 83.6-57.2 135.4 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 137.6-71.2z" />
</svg>
Download for Mac
</a>
);
}

View file

@ -0,0 +1,56 @@
import Link from "next/link";
import { DownloadButton } from "./download-button";
export function NavLinks() {
return (
<>
<Link
href="/docs/getting-started"
className="hover:text-foreground transition-colors"
>
Docs
</Link>
<Link
href="/blog"
className="hover:text-foreground transition-colors"
>
Blog
</Link>
<Link
href="/docs/changelog"
className="hover:text-foreground transition-colors"
>
Changelog
</Link>
<Link
href="/community"
className="hover:text-foreground transition-colors"
>
Community
</Link>
<Link
href="https://github.com/manaflow-ai/cmux"
className="hover:text-foreground transition-colors"
>
GitHub
</Link>
<DownloadButton size="sm" />
</>
);
}
export function SiteFooter() {
return (
<footer className="py-8 flex justify-center">
<div className="flex items-center gap-4 text-sm text-muted">
<a href="https://github.com/manaflow-ai/cmux" target="_blank" rel="noopener noreferrer" 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/SDbQmzQhRK" 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>
);
}

View file

@ -0,0 +1,39 @@
import Image from "next/image";
import Link from "next/link";
import { NavLinks } from "./nav-links";
export function SiteHeader({ section, hideLogo }: { section?: string; hideLogo?: boolean }) {
return (
<header className="sticky top-0 z-30 w-full bg-background/80 backdrop-blur-sm">
<div className="w-full max-w-5xl mx-auto flex items-center justify-between px-6 h-12">
<div className="flex items-center gap-3">
{!hideLogo && (
<>
<Link href="/" className="flex items-center gap-2.5">
<Image
src="/icon.png"
alt="cmux"
width={24}
height={24}
className="rounded-md"
/>
<span className="text-sm font-semibold tracking-tight">
cmux
</span>
</Link>
{section && (
<>
<span className="text-border text-[13px]">/</span>
<span className="text-[13px] text-muted">{section}</span>
</>
)}
</>
)}
</div>
<nav className="flex items-center gap-4 text-sm text-muted">
<NavLinks />
</nav>
</div>
</header>
);
}

View file

@ -0,0 +1,221 @@
"use client";
import { useState, useEffect, useRef, useCallback, useSyncExternalStore } from "react";
type DevValues = {
headerTx: number;
cursorTop: number;
cursorBlink: boolean;
subtitleLh: number;
downloadAbove: number;
downloadBelow: number;
featuresLh: number;
featuresMb: number;
docsPt: number;
};
const defaults: DevValues = {
headerTx: -4,
cursorTop: 2.5,
cursorBlink: true,
subtitleLh: 1.5,
downloadAbove: 21,
downloadBelow: 33,
featuresLh: 1.275,
featuresMb: 23,
docsPt: 8,
};
// Tiny external store (avoids setState-during-render)
let snapshot = { ...defaults };
const listeners = new Set<() => void>();
function getSnapshot() { return snapshot; }
function getServerSnapshot() { return defaults; }
function setStore(patch: Partial<DevValues>) {
snapshot = { ...snapshot, ...patch };
listeners.forEach((l) => l());
}
function subscribe(cb: () => void) {
listeners.add(cb);
return () => { listeners.delete(cb); };
}
export function useDevValues() {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
function el(name: string) {
return document.querySelector(`[data-dev="${name}"]`) as HTMLElement | null;
}
function applyToDOM(v: DevValues) {
const header = el("header");
if (header) header.style.transform = `translateX(${v.headerTx}px)`;
const subtitle = el("subtitle");
if (subtitle) subtitle.style.lineHeight = `${v.subtitleLh}`;
const download = el("download");
if (download) {
download.style.marginTop = `${v.downloadAbove}px`;
download.style.marginBottom = `${v.downloadBelow}px`;
}
const featuresUl = el("features-ul");
if (featuresUl) featuresUl.style.lineHeight = `${v.featuresLh}`;
const featuresSpacer = el("features-spacer");
if (featuresSpacer) featuresSpacer.style.height = `${v.featuresMb}px`;
const docsContent = el("docs-content");
if (docsContent) docsContent.style.paddingTop = `${v.docsPt}px`;
}
export function DevPanel() {
const [visible, setVisible] = useState(false);
const [pos, setPos] = useState({ x: 0, y: 0 });
const [dragging, setDragging] = useState(false);
const [copied, setCopied] = useState(false);
const vals = useDevValues();
const dragOffset = useRef({ x: 0, y: 0 });
useEffect(() => {
setPos({ x: window.innerWidth - 340, y: window.innerHeight - 320 });
}, []);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === ".") {
e.preventDefault();
setVisible((v) => !v);
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);
const update = useCallback((patch: Partial<DevValues>) => {
setStore(patch);
applyToDOM({ ...snapshot, ...patch });
}, []);
const onPointerDown = useCallback((e: React.PointerEvent) => {
if ((e.target as HTMLElement).closest("input, button, label")) return;
setDragging(true);
dragOffset.current = { x: e.clientX - pos.x, y: e.clientY - pos.y };
(e.target as HTMLElement).setPointerCapture(e.pointerId);
}, [pos]);
const onPointerMove = useCallback((e: React.PointerEvent) => {
if (!dragging) return;
setPos({ x: e.clientX - dragOffset.current.x, y: e.clientY - dragOffset.current.y });
}, [dragging]);
const onPointerUp = useCallback(() => setDragging(false), []);
if (process.env.NODE_ENV !== "development" || !visible) return null;
return (
<div
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
style={{ left: pos.x, top: pos.y, cursor: dragging ? "grabbing" : "grab" }}
className="fixed z-[9999] bg-[#222] text-white text-xs rounded-xl p-4 space-y-3 font-mono shadow-lg select-none"
>
<div className="flex items-center justify-between gap-4">
<span className="text-white/50">Dev Controls</span>
<span className="text-white/20">.</span>
</div>
<Section label="Header">
<Row label="tx" value={vals.headerTx} onChange={(v) => update({ headerTx: v })} min={-50} max={50} step={1} unit="px" />
</Section>
<Section label="Cursor">
<div className="flex items-center gap-3">
<Row label="top" value={vals.cursorTop} onChange={(v) => update({ cursorTop: v })} min={-5} max={5} step={0.5} unit="px" />
<label className="flex items-center gap-2 text-white/70 cursor-pointer">
<input type="checkbox" checked={vals.cursorBlink} onChange={(e) => update({ cursorBlink: e.target.checked })} />
blink
</label>
</div>
</Section>
<Section label="Subtitle">
<Row label="line-h" value={vals.subtitleLh} onChange={(v) => update({ subtitleLh: v })} min={1} max={2.5} step={0.025} unit="" w={16} />
</Section>
<Section label="Download buttons">
<Row label="above" value={vals.downloadAbove} onChange={(v) => update({ downloadAbove: v })} />
<Row label="below" value={vals.downloadBelow} onChange={(v) => update({ downloadBelow: v })} />
</Section>
<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="mb" value={vals.featuresMb} onChange={(v) => update({ featuresMb: v })} />
</Section>
<Section label="Docs">
<Row label="pt" value={vals.docsPt} onChange={(v) => update({ docsPt: v })} />
</Section>
<button
onClick={() => {
const text = [
`header-tx: ${vals.headerTx}px`,
`cursor-top: ${vals.cursorTop}px`,
`cursor-blink: ${vals.cursorBlink}`,
`subtitle-lh: ${vals.subtitleLh}`,
`download-above: ${vals.downloadAbove}px`,
`download-below: ${vals.downloadBelow}px`,
`features-lh: ${vals.featuresLh}`,
`features-mb: ${vals.featuresMb}px`,
`docs-pt: ${vals.docsPt}px`,
].join(", ");
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}}
className="w-full py-1.5 rounded-lg bg-white/10 hover:bg-white/20 text-white/70 cursor-pointer transition-colors"
>
{copied ? "Copied!" : "Copy values"}
</button>
</div>
);
}
function Section({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="space-y-2">
<div className="text-white/40 text-[10px] uppercase tracking-wider">{label}</div>
{children}
</div>
);
}
function Row({ label, value, onChange, min = 0, max = 128, step = 1, unit = "px", w = 10 }: {
label: string; value: number; onChange: (v: number) => void;
min?: number; max?: number; step?: number; unit?: string; w?: number;
}) {
return (
<div className="flex items-center gap-2">
<span className="w-12 text-white/70">{label}</span>
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => onChange(parseFloat(e.target.value))}
className="w-28 accent-blue-500 cursor-pointer"
/>
<span className="text-right tabular-nums" style={{ width: `${w * 4}px` }}>
{Number.isInteger(step) ? value : value.toFixed(step < 0.1 ? 2 : 1)}{unit}
</span>
</div>
);
}