diff --git a/web/app/docs/changelog/changelog-media.ts b/web/app/docs/changelog/changelog-media.ts new file mode 100644 index 00000000..77cf63fe --- /dev/null +++ b/web/app/docs/changelog/changelog-media.ts @@ -0,0 +1,107 @@ +/** + * Supplementary media and narrative for changelog versions. + * + * CHANGELOG.md remains the source of truth for the raw list of changes. + * This file adds titles, feature highlights, and narrative descriptions + * for major releases. Versions not listed here render as plain bullet lists. + * + * Images live in public/changelog/ and should be 2x (e.g. 1600×900 for a + * 800px display width). Use PNG for UI screenshots, WebP for photos. + */ + +export interface FeatureHighlight { + title: string; + description: string; + /** Path relative to /public, e.g. "/changelog/0.61.0-command-palette.png" */ + image?: string; +} + +export interface VersionMedia { + /** Big title shown as a heading, summarizing the main features. */ + title: string; + /** Hero image shown at the top of the version entry. */ + hero?: string; + /** Feature highlights shown inline below the title. */ + features?: FeatureHighlight[]; +} + +export const changelogMedia: Record = { + "0.61.0": { + title: "Tab Colors, Command Palette, Pin Workspaces", + features: [ + { + title: "Tab Colors", + description: + "Right-click any workspace in the sidebar to assign it a color. There are 17 presets to choose from, or pick a custom color. Colors show on the tab itself and on the workspace indicator rail.", + image: "/changelog/0.61.0-tab-colors.png", + }, + { + title: "Command Palette", + description: + "Hit Cmd+Shift+P to open a searchable command palette. Every action in cmux is here: creating workspaces, toggling the sidebar, checking for updates, switching windows. Keyboard shortcuts are shown inline so you can learn them as you go.", + image: "/changelog/0.61.0-command-palette.png", + }, + { + title: "Open With", + description: + "You can now open your current directory in VS Code, Cursor, Zed, Xcode, Finder, or any other editor directly from the command palette. Type \"open\" and pick your editor.", + image: "/changelog/0.61.0-open-with.png", + }, + { + title: "Pin Workspaces", + description: + "Pin a workspace to keep it at the top of the sidebar. Pinned workspaces stay put when other workspaces reorder from notifications or activity.", + image: "/changelog/0.61.0-pin-workspace.png", + }, + { + title: "Workspace Metadata", + description: + "The sidebar now shows richer context for each workspace: PR links that open in the browser, listening ports, git branches, and working directories across all panes.", + image: "/changelog/0.61.0-workspace-metadata.png", + }, + ], + }, + "0.60.0": { + title: "Tab Context Menu, DevTools, Notification Rings, CJK Input", + features: [ + { + title: "Tab Context Menu", + description: + "Right-click any tab in a pane to rename it, close tabs to the left or right, move it to another pane, or create a new terminal or browser tab next to it. You can also zoom a pane to full size and mark tabs as unread.", + image: "/changelog/0.60.0-tab-context-menu.png", + }, + { + title: "Browser DevTools", + description: + "The embedded browser now has full WebKit DevTools. Open them with the standard shortcut and they persist across tab switches. Inspect elements, debug JavaScript, and monitor network requests without leaving cmux.", + image: "/changelog/0.60.0-devtools.png", + }, + { + title: "Notification Rings", + description: + "When a background process sends a notification (like a long build finishing), the terminal pane shows an animated ring so you can spot it at a glance without switching workspaces.", + }, + { + title: "CJK Input", + description: + "Full IME support for Korean, Chinese, and Japanese. Preedit text renders inline with proper anchoring and sizing, so composing characters works the way you'd expect.", + image: "/changelog/0.60.0-cjk-input.png", + }, + { + title: "Claude Code", + description: + "Claude Code integration is now enabled by default. Each workspace gets its own routing context, and agents can read terminal screen contents via the API.", + }, + ], + }, + "0.32.0": { + title: "Sidebar Metadata", + features: [ + { + title: "Sidebar Metadata", + description: + "The sidebar now displays git branch, listening ports, log entries, progress bars, and status pills for each workspace.", + }, + ], + }, +}; diff --git a/web/app/docs/changelog/page.tsx b/web/app/docs/changelog/page.tsx index f3f3140a..43760e74 100644 --- a/web/app/docs/changelog/page.tsx +++ b/web/app/docs/changelog/page.tsx @@ -1,6 +1,18 @@ import type { Metadata } from "next"; import fs from "fs"; import path from "path"; +import Image from "next/image"; +import { changelogMedia, type VersionMedia } from "./changelog-media"; + +/** Read PNG dimensions from the IHDR chunk (bytes 16-23). */ +function pngDimensions(filePath: string): { width: number; height: number } { + const abs = path.join(process.cwd(), "public", filePath); + const buf = fs.readFileSync(abs); + return { + width: buf.readUInt32BE(16), + height: buf.readUInt32BE(24), + }; +} export const metadata: Metadata = { title: "Changelog", @@ -52,7 +64,6 @@ function parseChangelog(markdown: string): ChangelogVersion[] { if (currentSection) { currentSection.items.push(itemMatch[1]); } else { - // Items without a ### heading (e.g. 1.0.x initial release) if (!current.sections.length) { currentSection = { heading: "", items: [] }; current.sections.push(currentSection); @@ -64,7 +75,6 @@ function parseChangelog(markdown: string): ChangelogVersion[] { continue; } - // Non-empty lines that aren't headings or items (intro text) const trimmed = line.trim(); if (trimmed && !trimmed.startsWith("#")) { current.intro = trimmed; @@ -97,39 +107,226 @@ function InlineMarkdown({ text }: { text: string }) { ); } +function formatDate(dateStr: string): string { + const d = new Date(dateStr + "T00:00:00"); + return d.toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }); +} + +function HeroImage({ src, version }: { src: string; version: string }) { + const { width, height } = pngDimensions(src); + return ( +
+
+ {`cmux +
+
+ ); +} + +function FeatureImage({ src, alt }: { src: string; alt: string }) { + const { width, height } = pngDimensions(src); + return ( +
+
+ {alt} +
+
+ ); +} + +function FeatureList({ media }: { media: VersionMedia }) { + if (!media.features?.length) return null; + + return ( +
+ {media.features.map((feature, i) => ( +
+

+ {feature.title}.{" "} + {feature.description} +

+ {feature.image && ( + + )} +
+ ))} +
+ ); +} + +function ContributorList({ items }: { items: string[] }) { + return ( +
+ {items.map((item, i) => { + const match = item.match( + /\[@([^\]]+)\]\((https:\/\/github\.com\/[^)]+)\)/ + ); + if (match) { + return ( + + {match[1]} + {match[1]} + + ); + } + return ( + + + + ); + })} +
+ ); +} + +function SectionBadge({ heading }: { heading: string }) { + const lower = heading.toLowerCase(); + + let color = "bg-border/50 text-muted"; + let label = heading; + + if (lower === "added") { + color = "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"; + label = "Added"; + } else if (lower === "changed") { + color = "bg-blue-500/10 text-blue-600 dark:text-blue-400"; + label = "Changed"; + } else if (lower === "fixed") { + color = "bg-amber-500/10 text-amber-600 dark:text-amber-400"; + label = "Fixed"; + } else if (lower.startsWith("thanks")) { + color = "bg-purple-500/10 text-purple-600 dark:text-purple-400"; + label = "Contributors"; + } + + return ( + + {label} + + ); +} + export default function ChangelogPage() { const changelogPath = path.join(process.cwd(), "..", "CHANGELOG.md"); const markdown = fs.readFileSync(changelogPath, "utf-8"); const versions = parseChangelog(markdown); return ( - <> -

Changelog

-

All notable changes to cmux are documented here.

+
+

Changelog

- {versions.map((v) => ( -
-

- {v.version}{" "} - - — {v.date} - -

- {v.intro &&

{v.intro}

} - {v.sections.map((section, i) => ( -
- {section.heading &&

{section.heading}

} -
    - {section.items.map((item, j) => ( -
  • - -
  • - ))} -
-
- ))} -
- ))} - +
+ {versions.map((v) => { + const media = changelogMedia[v.version]; + + return ( +
+
+ + + {v.version} + + + +
+ + {media?.title && ( +
+ {media.title} +
+ )} + + {media?.hero && ( + + )} + + {media && } + + {v.intro && !media && ( +
+ {v.intro.replace(/^_/, "").replace(/_$/, "")} +
+ )} + +
+ {v.sections.map((section, i) => { + const isContributors = section.heading + .toLowerCase() + .startsWith("thanks"); + + if (isContributors) { + return ( +
+ + +
+ ); + } + + return ( +
+ {section.heading && ( + + )} +
    + {section.items.map((item, j) => ( +
  • + +
  • + ))} +
+
+ ); + })} +
+
+ ); + })} +
+
); } diff --git a/web/app/globals.css b/web/app/globals.css index 0ecea6bb..0ffe42e2 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -75,100 +75,102 @@ body { animation: blink 1s step-end infinite; } -/* Docs prose styles */ -.docs-content h1 { - font-size: 1.5rem; - font-weight: 700; - letter-spacing: -0.025em; - margin-bottom: 0.75rem; -} +/* Docs prose styles — in @layer base so Tailwind utilities can override */ +@layer base { + .docs-content h1 { + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.025em; + padding-bottom: 0.75rem; + } -.docs-content h2 { - font-size: 1.25rem; - font-weight: 600; - margin-top: 2.5rem; - margin-bottom: 0.75rem; - letter-spacing: -0.01em; -} + .docs-content h2 { + font-size: 1.25rem; + font-weight: 600; + padding-top: 2.5rem; + padding-bottom: 0.75rem; + letter-spacing: -0.01em; + } -.docs-content h3 { - font-size: 1rem; - font-weight: 600; - margin-top: 1.75rem; - margin-bottom: 0.5rem; -} + .docs-content h3 { + font-size: 1rem; + font-weight: 600; + padding-top: 1.75rem; + padding-bottom: 0.5rem; + } -.docs-content h4 { - font-size: 0.9375rem; - font-weight: 600; - margin-top: 1.25rem; - margin-bottom: 0.375rem; - font-family: var(--font-geist-mono); -} + .docs-content h4 { + font-size: 0.9375rem; + font-weight: 600; + padding-top: 1.25rem; + padding-bottom: 0.375rem; + font-family: var(--font-geist-mono); + } -.docs-content > p { - line-height: 1.7; - margin-bottom: 1rem; - color: var(--muted); -} + .docs-content > p { + line-height: 1.7; + padding-bottom: 1rem; + color: var(--muted); + } -.docs-content ul, -.docs-content ol { - padding-left: 1.5rem; - margin-bottom: 1rem; -} + .docs-content ul, + .docs-content ol { + padding-left: 1.5rem; + padding-bottom: 1rem; + } -.docs-content ul { - list-style: disc; -} + .docs-content ul { + list-style: disc; + } -.docs-content ol { - list-style: decimal; -} + .docs-content ol { + list-style: decimal; + } -.docs-content li { - line-height: 1.7; - margin-bottom: 0.25rem; - color: var(--muted); -} + .docs-content li { + line-height: 1.7; + padding-bottom: 0.25rem; + color: var(--muted); + } -.docs-content code { - font-family: var(--font-geist-mono); - font-size: 0.8125em; - background: var(--code-bg); - padding: 0.125rem 0.375rem; - border-radius: 0.25rem; -} + .docs-content code { + font-family: var(--font-geist-mono); + font-size: 0.8125em; + background: var(--code-bg); + padding: 0.125rem 0.375rem; + border-radius: 0.25rem; + } -.docs-content kbd { - font-family: var(--font-geist-mono); - font-size: 0.75em; - line-height: 1; - white-space: nowrap; - background: var(--code-bg); - border: 1px solid var(--border); - border-radius: 0.3125rem; - padding: 0.2rem 0.375rem; - min-width: 1.4em; - text-align: center; - display: inline-block; - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08); -} + .docs-content kbd { + font-family: var(--font-geist-mono); + font-size: 0.75em; + line-height: 1; + white-space: nowrap; + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: 0.3125rem; + padding: 0.2rem 0.375rem; + min-width: 1.4em; + text-align: center; + display: inline-block; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08); + } -.dark .docs-content kbd { - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.4); -} + .dark .docs-content kbd { + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.4); + } -.docs-content pre { - overflow-x: auto; - max-width: 100%; -} + .docs-content pre { + overflow-x: auto; + max-width: 100%; + } -.docs-content pre code { - background: none; - padding: 0; - font-size: 1em; - font-family: inherit; + .docs-content pre code { + background: none; + padding: 0; + font-size: 1em; + font-family: inherit; + } } /* Shiki dual theme */ @@ -183,40 +185,42 @@ body { color: var(--shiki-dark) !important; } -.docs-content table { - width: 100%; - border-collapse: collapse; - margin-bottom: 1.5rem; - font-size: 0.875rem; -} +@layer base { + .docs-content table { + width: 100%; + border-collapse: collapse; + padding-bottom: 1.5rem; + font-size: 0.875rem; + } -.docs-content th { - text-align: left; - font-weight: 600; - padding: 0.5rem 0.75rem; - border-bottom: 1px solid var(--border); -} + .docs-content th { + text-align: left; + font-weight: 600; + padding: 0.5rem 0.75rem; + border-bottom: 1px solid var(--border); + } -.docs-content td { - padding: 0.5rem 0.75rem; - border-bottom: 1px solid var(--border); - color: var(--muted); -} + .docs-content td { + padding: 0.5rem 0.75rem; + border-bottom: 1px solid var(--border); + color: var(--muted); + } -.docs-content strong { - font-weight: 600; - color: var(--foreground); -} + .docs-content strong { + font-weight: 600; + color: var(--foreground); + } -.docs-content a:not([class]) { - color: var(--foreground); - text-decoration: underline; - text-decoration-skip-ink: none; - text-underline-offset: 3px; - text-decoration-thickness: 1px; - text-decoration-color: var(--border); -} + .docs-content a:not([class]) { + color: var(--foreground); + text-decoration: underline; + text-decoration-skip-ink: none; + text-underline-offset: 3px; + text-decoration-thickness: 1px; + text-decoration-color: var(--border); + } -.docs-content a:not([class]):hover { - text-decoration-color: var(--foreground); + .docs-content a:not([class]):hover { + text-decoration-color: var(--foreground); + } } diff --git a/web/app/page.tsx b/web/app/page.tsx index 2093dd33..379ad9b2 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -259,13 +259,19 @@ export default function Home() { -
+
Read the Docs + + View Changelog +
diff --git a/web/next.config.ts b/web/next.config.ts index 49afc836..52213e80 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -2,6 +2,15 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { skipTrailingSlashRedirect: true, + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "github.com", + pathname: "/*.png", + }, + ], + }, async rewrites() { return [ { diff --git a/web/public/changelog/0.60.0-cjk-input.png b/web/public/changelog/0.60.0-cjk-input.png new file mode 100644 index 00000000..adc8ec2b Binary files /dev/null and b/web/public/changelog/0.60.0-cjk-input.png differ diff --git a/web/public/changelog/0.60.0-devtools.png b/web/public/changelog/0.60.0-devtools.png new file mode 100644 index 00000000..706453cc Binary files /dev/null and b/web/public/changelog/0.60.0-devtools.png differ diff --git a/web/public/changelog/0.60.0-tab-context-menu.png b/web/public/changelog/0.60.0-tab-context-menu.png new file mode 100644 index 00000000..625f902a Binary files /dev/null and b/web/public/changelog/0.60.0-tab-context-menu.png differ diff --git a/web/public/changelog/0.61.0-command-palette.png b/web/public/changelog/0.61.0-command-palette.png new file mode 100644 index 00000000..353ff344 Binary files /dev/null and b/web/public/changelog/0.61.0-command-palette.png differ diff --git a/web/public/changelog/0.61.0-open-with.png b/web/public/changelog/0.61.0-open-with.png new file mode 100644 index 00000000..bda36741 Binary files /dev/null and b/web/public/changelog/0.61.0-open-with.png differ diff --git a/web/public/changelog/0.61.0-pin-workspace.png b/web/public/changelog/0.61.0-pin-workspace.png new file mode 100644 index 00000000..ebd6a90c Binary files /dev/null and b/web/public/changelog/0.61.0-pin-workspace.png differ diff --git a/web/public/changelog/0.61.0-tab-colors.png b/web/public/changelog/0.61.0-tab-colors.png new file mode 100644 index 00000000..91eb8a1e Binary files /dev/null and b/web/public/changelog/0.61.0-tab-colors.png differ diff --git a/web/public/changelog/0.61.0-workspace-metadata.png b/web/public/changelog/0.61.0-workspace-metadata.png new file mode 100644 index 00000000..40f29317 Binary files /dev/null and b/web/public/changelog/0.61.0-workspace-metadata.png differ