From f970cdcf335cb4fb7680b2fc8ce3114e07f43abc Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Mon, 9 Feb 2026 23:38:05 -0800 Subject: [PATCH] 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 --- README.md | 12 +- web/app/(legal)/eula/page.tsx | 206 ++++++++ web/app/(legal)/layout.tsx | 16 + web/app/(legal)/privacy-policy/page.tsx | 162 +++++++ web/app/(legal)/terms-of-service/page.tsx | 187 ++++++++ web/app/blog/introducing-cmux/page.tsx | 70 +++ web/app/blog/layout.tsx | 31 ++ web/app/blog/page.tsx | 41 ++ web/app/community/page.tsx | 119 +++++ web/app/components/callout.tsx | 20 + web/app/components/code-block.tsx | 59 +++ web/app/components/docs-nav-items.ts | 9 + web/app/components/docs-pager.tsx | 41 ++ web/app/components/docs-sidebar.tsx | 31 ++ web/app/components/download-button.tsx | 22 + web/app/components/nav-links.tsx | 56 +++ web/app/components/site-header.tsx | 39 ++ web/app/components/spacing-control.tsx | 221 +++++++++ web/app/docs/api/page.tsx | 382 +++++++++++++++ web/app/docs/changelog/page.tsx | 127 +++++ web/app/docs/concepts/page.tsx | 212 +++++++++ web/app/docs/configuration/page.tsx | 127 +++++ web/app/docs/docs-nav.tsx | 137 ++++++ web/app/docs/getting-started/page.tsx | 77 +++ web/app/docs/keyboard-shortcuts/page.tsx | 19 + web/app/docs/layout.tsx | 30 ++ web/app/docs/notifications/page.tsx | 202 ++++++++ web/app/docs/page.tsx | 5 + web/app/globals.css | 140 ++++++ web/app/keyboard-shortcuts.tsx | 542 ++++++++++++++-------- web/app/layout.tsx | 12 +- web/app/page.tsx | 70 ++- web/app/robots.ts | 8 + web/app/sitemap.ts | 16 + web/app/typing.tsx | 56 +-- web/bun.lock | 92 ++++ web/package.json | 4 +- 37 files changed, 3304 insertions(+), 296 deletions(-) create mode 100644 web/app/(legal)/eula/page.tsx create mode 100644 web/app/(legal)/layout.tsx create mode 100644 web/app/(legal)/privacy-policy/page.tsx create mode 100644 web/app/(legal)/terms-of-service/page.tsx create mode 100644 web/app/blog/introducing-cmux/page.tsx create mode 100644 web/app/blog/layout.tsx create mode 100644 web/app/blog/page.tsx create mode 100644 web/app/community/page.tsx create mode 100644 web/app/components/callout.tsx create mode 100644 web/app/components/code-block.tsx create mode 100644 web/app/components/docs-nav-items.ts create mode 100644 web/app/components/docs-pager.tsx create mode 100644 web/app/components/docs-sidebar.tsx create mode 100644 web/app/components/download-button.tsx create mode 100644 web/app/components/nav-links.tsx create mode 100644 web/app/components/site-header.tsx create mode 100644 web/app/components/spacing-control.tsx create mode 100644 web/app/docs/api/page.tsx create mode 100644 web/app/docs/changelog/page.tsx create mode 100644 web/app/docs/concepts/page.tsx create mode 100644 web/app/docs/configuration/page.tsx create mode 100644 web/app/docs/docs-nav.tsx create mode 100644 web/app/docs/getting-started/page.tsx create mode 100644 web/app/docs/keyboard-shortcuts/page.tsx create mode 100644 web/app/docs/layout.tsx create mode 100644 web/app/docs/notifications/page.tsx create mode 100644 web/app/docs/page.tsx create mode 100644 web/app/robots.ts create mode 100644 web/app/sitemap.ts diff --git a/README.md b/README.md index fc1e4cc5..1e992e4d 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,12 @@ ## Features -- **Native macOS app** — Built with Swift and AppKit, not Electron. Fast startup, low memory. -- **Vertical tabs** — See all your terminals at a glance in a sidebar -- **Notification panel** — See which agents are waiting for input at a glance -- **Notification rings** — Tabs flash when AI agents (Claude Code, Codex) need your attention -- **Lightweight** — Small binary, minimal resource footprint. No bundled browser engine. -- **GPU-accelerated** — Powered by libghostty for smooth rendering +: **Native macOS app** — Built with Swift and AppKit, not Electron. Fast startup, low memory. +: **Vertical tabs** — See all your terminals at a glance in a sidebar +: **Notification panel** — See which agents are waiting for input at a glance +: **Notification rings** — Tabs flash when AI agents (Claude Code, Codex) need your attention +: **Lightweight** — Small binary, minimal resource footprint. No bundled browser engine. +: **GPU-accelerated** — Powered by libghostty for smooth rendering ## Install diff --git a/web/app/(legal)/eula/page.tsx b/web/app/(legal)/eula/page.tsx new file mode 100644 index 00000000..114f32bb --- /dev/null +++ b/web/app/(legal)/eula/page.tsx @@ -0,0 +1,206 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "EULA — cmux", + description: "End-User License Agreement for cmux", +}; + +export default function EulaPage() { + return ( + <> +

End-User License Agreement

+

Last updated: December 2, 2025

+ +

+ Please read this End-User License Agreement carefully before + downloading or using cmux. +

+ +

Interpretation and Definitions

+

For the purposes of this Agreement:

+ + +

Acknowledgment

+

+ By downloading or using the Application, You are agreeing to be bound + by the terms of this Agreement. If You do not agree, do not download or + use the Application. +

+

+ The Application is licensed, not sold, to You by the Company for use + strictly in accordance with the terms of this Agreement. +

+ +

License

+ +

Scope of License

+

+ The Company grants You a revocable, non-exclusive, non-transferable, + limited license to download, install and use the Application strictly in + accordance with this Agreement, for your personal or internal business + purposes including commercial use in connection with software + development. +

+ +

License Restrictions

+

You agree not to, and You will not permit others to:

+ + +

Intellectual Property

+

+ The Application, including all copyrights, patents, trademarks, trade + secrets and other intellectual property rights, is and shall remain the + sole and exclusive property of the Company. +

+

+ You retain ownership of any code or content you create using the + Application. +

+ +

Modifications and Updates

+

+ The Company reserves the right to modify, suspend or discontinue the + Application at any time, with or without notice and without liability to + You. +

+

+ The Company may provide updates, patches, bug fixes, and other + modifications. Updates may modify or remove certain features. You agree + that all updates are subject to the terms of this Agreement. +

+ +

Third-Party Services

+

+ The Application integrates with third-party services including Ghostty + (terminal rendering engine), Sentry (error tracking), and Sparkle + (auto-update framework). You acknowledge that the Company shall not be + responsible for any third-party services, including their accuracy, + completeness, or quality. +

+ +

Term and Termination

+

+ This Agreement shall remain in effect until terminated by You or the + Company. The Company may terminate this Agreement at any time for any + reason. +

+

+ This Agreement will terminate immediately if you fail to comply with any + provision. You may also terminate by deleting the Application and all + copies from your Device. +

+

+ Upon termination, You shall cease all use of the Application and delete + all copies from your Device. +

+ +

No Warranties

+

+ The Application is provided “AS IS” and “AS + AVAILABLE” without warranty of any kind. The Company expressly + disclaims all warranties, whether express, implied, statutory or + otherwise, including all implied warranties of merchantability, fitness + for a particular purpose, title and non-infringement. +

+

+ Some jurisdictions do not allow the exclusion of certain types of + warranties, so some of the above exclusions may not apply to You. +

+ +

Limitation of Liability

+

+ The entire liability of the Company under this Agreement shall be + limited to the amount actually paid by You for the Application, or 100 + USD if You haven’t purchased anything. +

+

+ To the maximum extent permitted by law, in no event shall the Company + be liable for any special, incidental, indirect, or consequential + damages whatsoever. +

+ +

Indemnification

+

+ You agree to indemnify and hold the Company harmless from any claim or + demand, including reasonable attorneys’ fees, due to or arising + out of your use of the Application or violation of this Agreement. +

+ +

Severability and Waiver

+

+ If any provision of this Agreement is held to be unenforceable, it will + be changed and interpreted to accomplish its objectives to the greatest + extent possible, and the remaining provisions will continue in full + force and effect. +

+ +

Governing Law

+

+ The laws of the United States, excluding conflicts of law rules, shall + govern this Agreement and your use of the Application. +

+ +

Changes to This Agreement

+

+ The Company reserves the right to modify this Agreement at any time. If + a revision is material, we will provide at least 30 days’ notice. + By continuing to use the Application after revisions become effective, + You agree to be bound by the revised terms. +

+ +

Contact Us

+

If you have any questions about this Agreement:

+ + + ); +} diff --git a/web/app/(legal)/layout.tsx b/web/app/(legal)/layout.tsx new file mode 100644 index 00000000..fa8a1862 --- /dev/null +++ b/web/app/(legal)/layout.tsx @@ -0,0 +1,16 @@ +import { SiteHeader } from "../components/site-header"; + +export default function LegalLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ +
+
{children}
+
+
+ ); +} diff --git a/web/app/(legal)/privacy-policy/page.tsx b/web/app/(legal)/privacy-policy/page.tsx new file mode 100644 index 00000000..982b07f6 --- /dev/null +++ b/web/app/(legal)/privacy-policy/page.tsx @@ -0,0 +1,162 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Privacy Policy — cmux", + description: "Privacy policy for cmux", +}; + +export default function PrivacyPolicyPage() { + return ( + <> +

Privacy Policy

+

Last updated: December 2, 2025

+ +

+ Manaflow (the “Company”) is committed to maintaining robust + privacy protections for its users. This Privacy Policy is designed to + help you understand how we collect, use and safeguard the information you + provide to us. +

+

+ For purposes of this policy, “Site” refers to the + Company’s website at{" "} + cmux.dev. + “Application” refers to the cmux desktop application for + macOS. “Service” refers to the Site and Application + collectively. The terms “we,” “us,” and + “our” refer to the Company. “You” refers to + you, as a user of our Service. +

+

+ By using our Service, you accept this Privacy Policy and our{" "} + Terms of Service, and you consent to + our collection, storage, use and disclosure of your information as + described here. +

+ +

I. Information We Collect

+

+ We collect “Non-Personal Information” and “Personal + Information.” Non-Personal Information includes information that + cannot be used to personally identify you, such as anonymous usage data, + platform types, and crash diagnostics. Personal Information includes + your email address if you choose to contact us. +

+ +

1. Information collected via Technology

+

+ The Application may collect the following information automatically: +

+ +

+ The Application checks for updates via Sparkle, which may transmit your + operating system version and application version to our update server. +

+ +

2. Information you provide directly

+

+ If you contact us via email or our contact page, we collect the + information you provide such as your name and email address. +

+ +

3. Children’s Privacy

+

+ The Service is not directed to anyone under the age of 13. We do not + knowingly collect information from anyone under 13. If you believe we + have collected such information, please contact us at{" "} + founders@manaflow.com. +

+ +

II. Third-Party Services

+

+ The Application integrates with the following third-party services: +

+ +

+ Each of these services has its own privacy policy governing the + collection and use of your data. +

+ +

III. How We Use and Share Information

+

+ We do not sell, trade, rent or otherwise share your Personal Information + with third parties for marketing purposes. We use crash reports and + diagnostics solely to improve the Application. We may share information + if we have a good-faith belief that disclosure is necessary to meet + legal process or protect against harm. +

+ +

IV. How We Protect Information

+

+ We implement security measures designed to protect your information from + unauthorized access, including encryption and secure server software. + However, no method of transmission or storage is 100% secure. By using + our Service, you acknowledge and agree to assume these risks. +

+ +

V. Your Rights

+

+ Depending on your location, you may have rights under applicable data + protection laws (such as GDPR or CCPA), including: +

+ +

+ To exercise any of these rights, please contact us at{" "} + founders@manaflow.com. +

+ +

VI. Links to Other Websites

+

+ The Service may provide links to third-party websites. We are not + responsible for the privacy practices of those websites. This Privacy + Policy applies solely to information collected by us. +

+ +

VII. Changes to This Policy

+

+ We reserve the right to change this policy at any time. Significant + changes will go into effect 30 days following notification. You should + periodically check the Site for updates. +

+ +

VIII. Contact Us

+

+ If you have any questions regarding this Privacy Policy, please contact + us at{" "} + founders@manaflow.com. +

+ +

IX. Data Retention

+

+ Crash reports and diagnostics are retained only as long as needed to + diagnose and fix issues. You may request deletion of any data associated + with you by contacting us at{" "} + founders@manaflow.com. +

+ + ); +} diff --git a/web/app/(legal)/terms-of-service/page.tsx b/web/app/(legal)/terms-of-service/page.tsx new file mode 100644 index 00000000..6a0a72ec --- /dev/null +++ b/web/app/(legal)/terms-of-service/page.tsx @@ -0,0 +1,187 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Terms of Service — cmux", + description: "Terms of service for cmux", +}; + +export default function TermsOfServicePage() { + return ( + <> +

Terms of Service

+

Last revised on: December 2, 2025

+ +

+ The website located at{" "} + cmux.dev (the + “Site”) and the cmux desktop application (the + “Application”) are copyrighted works belonging to Manaflow + (“Company”, “us”, “our”, and + “we”). These Terms of Use (these “Terms”) set + forth the legally binding terms and conditions that govern your use of + the Site and Application. +

+

+ By accessing or using the Site or Application, you are accepting these + Terms and you represent and warrant that you have the right, authority, + and capacity to enter into these Terms. You may not access or use the + Site or Application if you are not at least 18 years old. If you do not + agree with all of the provisions of these Terms, do not access and/or + use the Site or Application. +

+ +

1. License

+

+ Subject to these Terms, Company grants you a non-transferable, + non-exclusive, revocable, limited license to use and access the Site and + Application for your personal or internal business purposes, including + commercial use in connection with your software development activities. +

+ +

Restrictions

+

The rights granted to you are subject to the following restrictions:

+ + +

Modification

+

+ Company reserves the right, at any time, to modify, suspend, or + discontinue the Site or Application with or without notice to you. + Company will not be liable to you or any third party for any + modification, suspension, or discontinuation. +

+ +

Ownership

+

+ You acknowledge that all intellectual property rights, including + copyrights, patents, trademarks, and trade secrets, in the Application + and its content are owned by Company or Company’s suppliers. + These Terms do not transfer to you any rights, title or interest in such + intellectual property, except for the limited license above. Company and + its suppliers reserve all rights not granted in these Terms. +

+ +

Feedback

+

+ If you provide Company with any feedback or suggestions regarding the + Application, you hereby assign to Company all rights in such feedback + and agree that Company shall have the right to use such feedback in any + manner it deems appropriate. +

+ +

2. User Content

+

+ You retain full ownership of all code, files, and content you create or + process using the Application. The Application runs locally on your + device and your content is not transmitted to our servers during normal + use. +

+ +

3. Indemnification

+

+ You agree to indemnify and hold Company (and its officers, employees, + and agents) harmless, including costs and attorneys’ fees, from + any claim or demand made by any third party due to or arising out of (a) + your use of the Application, (b) your violation of these Terms, or (c) + your violation of applicable laws or regulations. +

+ +

4. Third-Party Links

+

+ The Site may contain links to third-party websites and services. Such + links are not under the control of Company, and Company is not + responsible for them. You use all third-party links at your own risk. +

+ +

5. Disclaimers

+

+ THE APPLICATION IS PROVIDED ON AN “AS-IS” AND “AS + AVAILABLE” BASIS. COMPANY EXPRESSLY DISCLAIMS ANY AND ALL + WARRANTIES AND CONDITIONS OF ANY KIND, WHETHER EXPRESS, IMPLIED, OR + STATUTORY, INCLUDING ALL WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT. +

+

+ SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO + THE ABOVE EXCLUSION MAY NOT APPLY TO YOU. +

+ +

6. Limitation on Liability

+

+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL COMPANY BE + LIABLE TO YOU OR ANY THIRD PARTY FOR ANY LOST PROFITS, LOST DATA, OR ANY + INDIRECT, CONSEQUENTIAL, EXEMPLARY, INCIDENTAL, SPECIAL OR PUNITIVE + DAMAGES ARISING FROM OR RELATING TO THESE TERMS OR YOUR USE OF THE + APPLICATION. +

+

+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, OUR LIABILITY TO YOU FOR ANY + DAMAGES WILL AT ALL TIMES BE LIMITED TO FIFTY US DOLLARS ($50). +

+ +

7. Term and Termination

+

+ These Terms will remain in effect while you use the Application. We may + suspend or terminate your rights at any time for any reason at our sole + discretion. Upon termination, you shall cease all use of the Application + and delete all copies from your devices. +

+ +

8. Dispute Resolution

+

+ You agree that any dispute between you and Company relating to the + Application or these Terms will be resolved by binding arbitration, + rather than in court, except that either party may assert individualized + claims in small claims court or seek equitable relief for intellectual + property misuse. The arbitration will be conducted by JAMS under their + applicable rules. +

+

+ YOU AND COMPANY WAIVE ANY CONSTITUTIONAL AND STATUTORY RIGHTS TO SUE IN + COURT AND HAVE A TRIAL IN FRONT OF A JUDGE OR A JURY. +

+

+ YOU AND COMPANY AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY + ON AN INDIVIDUAL BASIS AND NOT ON A CLASS, REPRESENTATIVE, OR COLLECTIVE + BASIS. +

+

+ You have the right to opt out of this arbitration agreement by sending + written notice to{" "} + founders@manaflow.com within 30 + days of first becoming subject to it. +

+ +

9. General

+

+ These Terms constitute the entire agreement between you and Company + regarding the use of the Application. Our failure to exercise or enforce + any right or provision shall not operate as a waiver. If any provision + is held to be invalid, the remaining provisions will remain in full + force and effect. +

+ +

10. Contact

+

+ Questions about these Terms should be sent to{" "} + founders@manaflow.com. +

+ +

+ Copyright © 2025 Manaflow. All rights reserved. +

+ + ); +} diff --git a/web/app/blog/introducing-cmux/page.tsx b/web/app/blog/introducing-cmux/page.tsx new file mode 100644 index 00000000..e1f73354 --- /dev/null +++ b/web/app/blog/introducing-cmux/page.tsx @@ -0,0 +1,70 @@ +import type { Metadata } from "next"; +import Link from "next/link"; + +export const metadata: Metadata = { + title: "Introducing cmux", + description: + "A native macOS terminal built on Ghostty, designed for running multiple AI coding agents side by side.", +}; + +export default function IntroducingCmuxPage() { + return ( + <> +
+ + ← Back to blog + +
+ +

Introducing cmux

+ + +

+ cmux is a native macOS terminal application built on top of Ghostty, + designed from the ground up for developers who run multiple AI coding + agents simultaneously. +

+ +

Why cmux?

+

+ Modern development workflows often involve running several agents at + once — Claude Code, Codex, and other tools each in their own + terminal. Keeping track of which ones need attention and switching + between them quickly is the problem cmux solves. +

+ +

Key features

+ + +

Get started

+

+ Install cmux via Homebrew or download the DMG from the{" "} + getting started guide. +

+ + ); +} diff --git a/web/app/blog/layout.tsx b/web/app/blog/layout.tsx new file mode 100644 index 00000000..0c3dca8f --- /dev/null +++ b/web/app/blog/layout.tsx @@ -0,0 +1,31 @@ +import type { Metadata } from "next"; +import { SiteHeader } from "../components/site-header"; + +export const metadata: Metadata = { + title: { + template: "%s — cmux blog", + default: "cmux blog", + }, + openGraph: { + siteName: "cmux", + type: "article", + }, + alternates: { + canonical: "./", + }, +}; + +export default function BlogLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ +
+
{children}
+
+
+ ); +} diff --git a/web/app/blog/page.tsx b/web/app/blog/page.tsx new file mode 100644 index 00000000..976c43a5 --- /dev/null +++ b/web/app/blog/page.tsx @@ -0,0 +1,41 @@ +import type { Metadata } from "next"; +import Link from "next/link"; + +export const metadata: Metadata = { + title: "Blog", + description: "News and updates from the cmux team", +}; + +const posts = [ + { + slug: "introducing-cmux", + title: "Introducing cmux", + date: "2025-06-01", + summary: + "A native macOS terminal built on Ghostty, designed for running multiple AI coding agents side by side.", + }, +]; + +export default function BlogPage() { + return ( + <> +

Blog

+
+ {posts.map((post) => ( +
+ +

+ {post.title} +

+ +

{post.summary}

+ +
+ ))} +
+ + ); +} diff --git a/web/app/community/page.tsx b/web/app/community/page.tsx new file mode 100644 index 00000000..f0add642 --- /dev/null +++ b/web/app/community/page.tsx @@ -0,0 +1,119 @@ +import type { Metadata } from "next"; +import { SiteHeader } from "../components/site-header"; + +export const metadata: Metadata = { + title: "Community — cmux", + description: "Join the cmux community on Discord, Twitter, GitHub, and more", +}; + +function CommunityLink({ + href, + icon, + name, + action, + description, +}: { + href: string; + icon: React.ReactNode; + name: string; + action: string; + description: string; +}) { + return ( + +
+ {icon} +
+
+
{name}
+
{description}
+
+ {action} → +
+
+
+ ); +} + +export default function CommunityPage() { + return ( +
+ +
+

+ Community +

+

+ Connect with other cmux users and the team behind it. +

+ +
+ + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> +
+
+
+ ); +} diff --git a/web/app/components/callout.tsx b/web/app/components/callout.tsx new file mode 100644 index 00000000..eb5443f6 --- /dev/null +++ b/web/app/components/callout.tsx @@ -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 ( +
+ {children} +
+ ); +} diff --git a/web/app/components/code-block.tsx b/web/app/components/code-block.tsx new file mode 100644 index 00000000..e915e9d6 --- /dev/null +++ b/web/app/components/code-block.tsx @@ -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 ( +
+ {title && ( +
+ {title} +
+ )} +
+
+ ); + } + + return ( +
+ {title && ( +
+ {title} +
+ )} +
+        {children}
+      
+
+ ); +} diff --git a/web/app/components/docs-nav-items.ts b/web/app/components/docs-nav-items.ts new file mode 100644 index 00000000..e182b9eb --- /dev/null +++ b/web/app/components/docs-nav-items.ts @@ -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" }, +]; diff --git a/web/app/components/docs-pager.tsx b/web/app/components/docs-pager.tsx new file mode 100644 index 00000000..43fb1866 --- /dev/null +++ b/web/app/components/docs-pager.tsx @@ -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 ( + + ); +} diff --git a/web/app/components/docs-sidebar.tsx b/web/app/components/docs-sidebar.tsx new file mode 100644 index 00000000..9e930c9b --- /dev/null +++ b/web/app/components/docs-sidebar.tsx @@ -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 ( + + ); +} diff --git a/web/app/components/download-button.tsx b/web/app/components/download-button.tsx new file mode 100644 index 00000000..1fd8d099 --- /dev/null +++ b/web/app/components/download-button.tsx @@ -0,0 +1,22 @@ +export function DownloadButton({ size = "default" }: { size?: "default" | "sm" }) { + const isSmall = size === "sm"; + return ( + + + + + Download for Mac + + ); +} diff --git a/web/app/components/nav-links.tsx b/web/app/components/nav-links.tsx new file mode 100644 index 00000000..3dc01da8 --- /dev/null +++ b/web/app/components/nav-links.tsx @@ -0,0 +1,56 @@ +import Link from "next/link"; +import { DownloadButton } from "./download-button"; + +export function NavLinks() { + return ( + <> + + Docs + + + Blog + + + Changelog + + + Community + + + GitHub + + + + ); +} + +export function SiteFooter() { + return ( + + ); +} diff --git a/web/app/components/site-header.tsx b/web/app/components/site-header.tsx new file mode 100644 index 00000000..2d21fd57 --- /dev/null +++ b/web/app/components/site-header.tsx @@ -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 ( +
+
+
+ {!hideLogo && ( + <> + + cmux + + cmux + + + {section && ( + <> + / + {section} + + )} + + )} +
+ +
+
+ ); +} diff --git a/web/app/components/spacing-control.tsx b/web/app/components/spacing-control.tsx new file mode 100644 index 00000000..0a525617 --- /dev/null +++ b/web/app/components/spacing-control.tsx @@ -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) { + 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) => { + 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 ( +
+
+ Dev Controls + ⌘. +
+ +
+ update({ headerTx: v })} min={-50} max={50} step={1} unit="px" /> +
+ +
+
+ update({ cursorTop: v })} min={-5} max={5} step={0.5} unit="px" /> + +
+
+ +
+ update({ subtitleLh: v })} min={1} max={2.5} step={0.025} unit="" w={16} /> +
+ +
+ update({ downloadAbove: v })} /> + update({ downloadBelow: v })} /> +
+ +
+ update({ featuresLh: v })} min={1} max={2.5} step={0.025} unit="" w={16} /> + update({ featuresMb: v })} /> +
+ +
+ update({ docsPt: v })} /> +
+ + +
+ ); +} + +function Section({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+
{label}
+ {children} +
+ ); +} + +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 ( +
+ {label} + onChange(parseFloat(e.target.value))} + className="w-28 accent-blue-500 cursor-pointer" + /> + + {Number.isInteger(step) ? value : value.toFixed(step < 0.1 ? 2 : 1)}{unit} + +
+ ); +} diff --git a/web/app/docs/api/page.tsx b/web/app/docs/api/page.tsx new file mode 100644 index 00000000..9300f118 --- /dev/null +++ b/web/app/docs/api/page.tsx @@ -0,0 +1,382 @@ +import type { Metadata } from "next"; +import { CodeBlock } from "../../components/code-block"; +import { Callout } from "../../components/callout"; + +export const metadata: Metadata = { + title: "API Reference", + description: "CLI and socket API reference for cmux", +}; + +function Cmd({ + name, + desc, + cli, + socket, +}: { + name: string; + desc: string; + cli: string; + socket: string; +}) { + return ( +
+

{name}

+

{desc}

+
+ {cli} + {socket} +
+
+ ); +} + +export default function ApiPage() { + return ( + <> +

API Reference

+

+ cmux provides both a CLI tool and a Unix socket for programmatic + control. Every command is available through both interfaces. +

+ +

Socket

+ + + + + + + + + + + + + + + + + +
BuildPath
Release + /tmp/cmux.sock +
Debug + /tmp/cmux-debug.sock +
+

+ Override with the CMUX_SOCKET_PATH environment variable. + Commands are newline-terminated JSON: +

+ {`{"command": "command-name", "arg1": "value1"} +// Response: +{"success": true, "data": {...}}`} + +

Access modes

+ + + + + + + + + + + + + + + + + + + + + +
ModeDescription
+ Off + Socket disabled
+ Notifications only + Only notification commands allowed
+ Full control + All commands enabled
+ + On shared machines, use “Notifications only” mode to prevent + other users from controlling your terminals. + + +

CLI options

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FlagDescription
+ --socket PATH + Custom socket path
+ --json + Output in JSON format
+ --workspace ID + Target a specific workspace
+ --surface ID + Target a specific surface
+ +

Workspace commands

+ + + + `} + socket={`{"command": "select-workspace", "id": ""}`} + /> + + `} + socket={`{"command": "close-workspace", "id": ""}`} + /> + +

Split commands

+ + + + `} + socket={`{"command": "focus-surface", "id": ""}`} + /> + +

Input commands

+ + + + "command"`} + socket={`{"command": "send-surface", "id": "", "text": "command"}`} + /> + enter`} + socket={`{"command": "send-key-surface", "id": "", "key": "enter"}`} + /> + +

Notification commands

+ + + + + +

Utility commands

+ + + +

Environment variables

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDescription
+ CMUX_SOCKET_PATH + Override the default socket path
+ CMUX_SOCKET_ENABLE + + Enable/disable socket (1/0) +
+ CMUX_SOCKET_MODE + + Override access mode (full,{" "} + notifications, off) +
+ CMUX_WORKSPACE_ID + Auto-set: current workspace ID
+ CMUX_SURFACE_ID + Auto-set: current surface ID
+ TERM_PROGRAM + + Set to ghostty +
+ TERM + + Set to xterm-ghostty +
+ + Environment variables override app settings. Use the socket check to + distinguish cmux from regular Ghostty. + + +

Detecting cmux

+ {`# Check for the socket +[ -S /tmp/cmux.sock ] && echo "In cmux" + +# Check for the CLI +command -v cmux &>/dev/null && echo "cmux available" + +# Distinguish from regular Ghostty +[ "$TERM_PROGRAM" = "ghostty" ] && [ -S /tmp/cmux.sock ] && echo "In cmux"`} + +

Examples

+ +

Python client

+ {`import socket, json + +def send_command(cmd): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect('/tmp/cmux.sock') + sock.send(json.dumps(cmd).encode() + b'\\n') + response = sock.recv(4096).decode() + sock.close() + return json.loads(response) + +# List workspaces +print(send_command({"command": "list-workspaces"})) + +# Send notification +send_command({ + "command": "notify", + "title": "Hello", + "body": "From Python!" +})`} + +

Shell script

+ {`#!/bin/bash +cmux_cmd() { + echo "$1" | nc -U /tmp/cmux.sock +} + +cmux_cmd '{"command": "list-workspaces"}' +cmux_cmd '{"command": "notify", "title": "Done", "body": "Task complete"}'`} + +

Build script with notification

+ {`#!/bin/bash +npm run build +if [ $? -eq 0 ]; then + cmux notify --title "✓ Build Success" --body "Ready to deploy" +else + cmux notify --title "✗ Build Failed" --body "Check the logs" +fi`} + + ); +} diff --git a/web/app/docs/changelog/page.tsx b/web/app/docs/changelog/page.tsx new file mode 100644 index 00000000..258cfab6 --- /dev/null +++ b/web/app/docs/changelog/page.tsx @@ -0,0 +1,127 @@ +import type { Metadata } from "next"; +import fs from "fs"; +import path from "path"; + +export const metadata: Metadata = { + title: "Changelog", + description: "Release notes and version history for cmux", +}; + +interface ChangelogSection { + heading: string; + items: string[]; +} + +interface ChangelogVersion { + version: string; + date: string; + intro?: string; + sections: ChangelogSection[]; +} + +function parseChangelog(markdown: string): ChangelogVersion[] { + const versions: ChangelogVersion[] = []; + let current: ChangelogVersion | null = null; + let currentSection: ChangelogSection | null = null; + + for (const line of markdown.split("\n")) { + const versionMatch = line.match(/^## \[(.+?)\] - (.+)$/); + if (versionMatch) { + if (current) versions.push(current); + current = { + version: versionMatch[1], + date: versionMatch[2], + sections: [], + }; + currentSection = null; + continue; + } + + if (!current) continue; + + const sectionMatch = line.match(/^### (.+)$/); + if (sectionMatch) { + currentSection = { heading: sectionMatch[1], items: [] }; + current.sections.push(currentSection); + continue; + } + + const itemMatch = line.match(/^- (.+)$/); + if (itemMatch) { + 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); + } + current.sections[current.sections.length - 1].items.push( + itemMatch[1] + ); + } + continue; + } + + // Non-empty lines that aren't headings or items (intro text) + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith("#")) { + current.intro = trimmed; + } + } + + if (current) versions.push(current); + return versions; +} + +function InlineCode({ text }: { text: string }) { + const parts = text.split(/(`[^`]+`)/g); + return ( + <> + {parts.map((part, i) => + part.startsWith("`") && part.endsWith("`") ? ( + {part.slice(1, -1)} + ) : ( + {part} + ) + )} + + ); +} + +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.

+ + {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) => ( +
  • + +
  • + ))} +
+
+ ))} +
+ ))} + + ); +} diff --git a/web/app/docs/concepts/page.tsx b/web/app/docs/concepts/page.tsx new file mode 100644 index 00000000..ae60f211 --- /dev/null +++ b/web/app/docs/concepts/page.tsx @@ -0,0 +1,212 @@ +import type { Metadata } from "next"; +import { CodeBlock } from "../../components/code-block"; + +export const metadata: Metadata = { + title: "Concepts", + description: + "Understanding cmux's window, workspace, pane, and surface hierarchy", +}; + +export default function ConceptsPage() { + return ( + <> +

Concepts

+

+ cmux organizes your terminals in a four-level hierarchy. Understanding + these levels helps when using the socket API, CLI, and keyboard + shortcuts. +

+ +

Hierarchy

+ {`Window + └── Workspace (sidebar entry) + └── Pane (split region) + └── Surface (tab within pane) + └── Panel (terminal or browser content)`} + +

Window

+

+ A macOS window. Open multiple windows with ⌘⇧N. Each + window has its own sidebar with independent workspaces. +

+ +

Workspace

+

+ A sidebar entry. Each workspace contains one or more split panes. + Workspaces are what you see listed in the left sidebar. +

+

+ In the UI and keyboard shortcuts, workspaces are often called + “tabs” since they behave like tabs in the sidebar. The + socket API and environment variables use the term + “workspace”. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ContextTerm used
Sidebar UITab
Keyboard shortcutsWorkspace or tab
Socket API + workspace +
Environment variable + CMUX_WORKSPACE_ID +
+ +

+ Shortcuts: ⌘N (new),{" "} + ⌘1⌘9 (jump), ⌘⇧W (close),{" "} + ⌘⇧[ / ⌘⇧] (prev/next) +

+ +

Pane

+

+ A split region within a workspace. Created by splitting with{" "} + ⌘D (right) or ⌘⇧D (down). Navigate between + panes with ⌥⌘ + arrow keys. +

+

Each pane can hold multiple surfaces (tabs within the pane).

+ +

Surface

+

+ A tab within a pane. Each pane has its own tab bar and can hold multiple + surfaces. Created with ⌘T, navigated with{" "} + ⌘[ / ⌘] or ⌃1– + ⌃9. +

+

+ Surfaces are the individual terminal or browser sessions you interact + with. Each surface has its own CMUX_SURFACE_ID environment + variable. +

+ +

Panel

+

The content inside a surface. Currently two types:

+
    +
  • + Terminal — a Ghostty terminal session +
  • +
  • + Browser — an embedded web view +
  • +
+

+ Panel is mostly an internal concept. In the socket API and CLI, you + interact with surfaces rather than panels directly. +

+ +

Visual example

+ {`┌──────────────────────────────────────────────────────┐ +│ ┌──────────┐ ┌─────────────────────────────────────┐ │ +│ │ Sidebar │ │ Workspace "dev" │ │ +│ │ │ │ │ │ +│ │ │ │ ┌───────────────┬─────────────────┐ │ │ +│ │ > dev │ │ │ Pane 1 │ Pane 2 │ │ │ +│ │ server │ │ │ [S1] [S2] │ [S1] │ │ │ +│ │ logs │ │ │ │ │ │ │ +│ │ │ │ │ Terminal │ Terminal │ │ │ +│ │ │ │ │ │ │ │ │ +│ │ │ │ └───────────────┴─────────────────┘ │ │ +│ └──────────┘ └─────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────┘`} +

In this example:

+
    +
  • + The window contains a sidebar with three workspaces + (dev, server, logs) +
  • +
  • + Workspace “dev” is selected, showing two{" "} + panes side by side +
  • +
  • + Pane 1 has two surfaces ([S1] and + [S2] in the tab bar), with S1 active +
  • +
  • + Pane 2 has one surface +
  • +
  • + Each surface contains a panel (a terminal in this + case) +
  • +
+ +

Summary

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LevelWhat it isCreated byIdentified by
WindowmacOS window + ⌘⇧N +
WorkspaceSidebar entry + ⌘N + + CMUX_WORKSPACE_ID +
PaneSplit region + ⌘D / ⌘⇧D + Pane ID (socket API)
SurfaceTab within pane + ⌘T + + CMUX_SURFACE_ID +
PanelTerminal or browserAutomaticPanel ID (internal)
+ + ); +} diff --git a/web/app/docs/configuration/page.tsx b/web/app/docs/configuration/page.tsx new file mode 100644 index 00000000..48592c52 --- /dev/null +++ b/web/app/docs/configuration/page.tsx @@ -0,0 +1,127 @@ +import type { Metadata } from "next"; +import { CodeBlock } from "../../components/code-block"; +import { Callout } from "../../components/callout"; + +export const metadata: Metadata = { + title: "Configuration", + description: "Configure cmux appearance and behavior", +}; + +export default function ConfigurationPage() { + return ( + <> +

Configuration

+

+ cmux reads configuration from Ghostty config files, giving you familiar + options if you're coming from Ghostty. +

+ +

Config file locations

+

cmux looks for configuration in these locations (in order):

+
    +
  1. + ~/.config/ghostty/config +
  2. +
  3. + ~/Library/Application Support/com.mitchellh.ghostty/config +
  4. +
+

Create the config file if it doesn't exist:

+ {`mkdir -p ~/.config/ghostty +touch ~/.config/ghostty/config`} + +

Appearance

+ +

Font

+ {`font-family = JetBrains Mono +font-size = 14`} + +

Colors

+ {`# Theme (or use individual colors below) +theme = Dracula + +# Custom colors +background = #1e1e2e +foreground = #cdd6f4 +cursor-color = #f5e0dc +cursor-text = #1e1e2e +selection-background = #585b70 +selection-foreground = #cdd6f4`} + +

Split panes

+ {`# Opacity for unfocused splits (0.0 to 1.0) +unfocused-split-opacity = 0.7 + +# Fill color for unfocused splits +unfocused-split-fill = #1e1e2e + +# Divider color between splits +split-divider-color = #45475a`} + +

Behavior

+ +

Scrollback

+ {`# Number of lines to keep in scrollback buffer +scrollback-limit = 10000`} + +

Working directory

+ {`# Default directory for new terminals +working-directory = ~/Projects`} + +

App settings

+

+ In-app settings are available via cmux → Settings ( + ⌘,): +

+ +

Theme mode

+
    +
  • + System — follow macOS appearance +
  • +
  • + Light — always light mode +
  • +
  • + Dark — always dark mode +
  • +
+ +

Automation mode

+

Control socket access level:

+
    +
  • + Off — no socket control (most secure) +
  • +
  • + Notifications only — only allow notification commands +
  • +
  • + Full control — allow all socket commands +
  • +
+ + On shared machines, consider using “Notifications only” mode + to prevent other processes from controlling your terminals. + + +

Example config

+ {`# Font +font-family = SF Mono +font-size = 13 + +# Colors +theme = One Dark + +# Scrollback +scrollback-limit = 50000 + +# Splits +unfocused-split-opacity = 0.85 +split-divider-color = #3e4451 + +# Working directory +working-directory = ~/code`} + + ); +} diff --git a/web/app/docs/docs-nav.tsx b/web/app/docs/docs-nav.tsx new file mode 100644 index 00000000..1e0a69de --- /dev/null +++ b/web/app/docs/docs-nav.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useState, useEffect, useRef, useCallback } from "react"; +import { DocsSidebar } from "../components/docs-sidebar"; +import { DocsPager } from "../components/docs-pager"; + +export function DocsNav({ children }: { children: React.ReactNode }) { + const [open, setOpen] = useState(false); + const sidebarRef = useRef(null); + const buttonRef = useRef(null); + + const close = useCallback(() => { + setOpen(false); + buttonRef.current?.focus(); + }, []); + + // Close on Escape + useEffect(() => { + if (!open) return; + const handler = (e: KeyboardEvent) => { + if (e.key === "Escape") close(); + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [open, close]); + + // Trap focus inside sidebar when open on mobile + useEffect(() => { + if (!open || !sidebarRef.current) return; + + const sidebar = sidebarRef.current; + const focusable = sidebar.querySelectorAll( + 'a[href], button, [tabindex]:not([tabindex="-1"])' + ); + if (focusable.length === 0) return; + + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + + // Focus first link + first.focus(); + + const trap = (e: KeyboardEvent) => { + if (e.key !== "Tab") return; + if (e.shiftKey) { + if (document.activeElement === first) { + e.preventDefault(); + last.focus(); + } + } else { + if (document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + }; + + sidebar.addEventListener("keydown", trap); + return () => sidebar.removeEventListener("keydown", trap); + }, [open]); + + // Lock body scroll when open on mobile + useEffect(() => { + if (!open) return; + const mq = window.matchMedia("(min-width: 768px)"); + if (mq.matches) return; // don't lock on desktop + document.body.style.overflow = "hidden"; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + return ( +
+ {/* Mobile menu button */} + + + {/* Mobile overlay */} + {open && ( + + ); +} diff --git a/web/app/docs/getting-started/page.tsx b/web/app/docs/getting-started/page.tsx new file mode 100644 index 00000000..3cefd65b --- /dev/null +++ b/web/app/docs/getting-started/page.tsx @@ -0,0 +1,77 @@ +import type { Metadata } from "next"; +import { CodeBlock } from "../../components/code-block"; +import { Callout } from "../../components/callout"; +import { DownloadButton } from "../../components/download-button"; + +export const metadata: Metadata = { + title: "Getting Started", + description: "Install and set up cmux on macOS", +}; + +export default function GettingStartedPage() { + return ( + <> +

Getting Started

+

+ cmux is a lightweight, native macOS terminal built on Ghostty for + managing multiple AI coding agents. It features vertical tabs, a + notification panel, and a socket-based control API. +

+ +

Install

+ +

DMG (recommended)

+
+ +
+

+ Open the .dmg and drag cmux to your Applications folder. + cmux auto-updates via Sparkle, so you only need to download once. +

+ +

Homebrew

+ {`brew tap manaflow-ai/cmux +brew install --cask cmux`} +

To update later:

+ {`brew upgrade --cask cmux`} + + + On first launch, macOS may ask you to confirm opening an app from an + identified developer. Click Open to proceed. + + +

Verify installation

+

Open cmux and you should see:

+
    +
  • A terminal window with a vertical tab sidebar on the left
  • +
  • One initial workspace already open
  • +
  • The Ghostty-powered terminal ready for input
  • +
+ +

CLI setup

+

+ cmux includes a command-line tool for automation. Inside cmux terminals + it works automatically. To use the CLI from outside cmux, create a + symlink: +

+ {`sudo ln -sf "/Applications/cmux.app/Contents/MacOS/cmux" /usr/local/bin/cmux`} +

Then you can run commands like:

+ {`cmux list-workspaces +cmux notify --title "Build Complete" --body "Your build finished"`} + +

Auto-updates

+

+ cmux checks for updates automatically via Sparkle. When an update is + available you'll see an update pill in the titlebar. You can also + check manually via cmux → Check for Updates in the menu + bar. +

+ +

Requirements

+
    +
  • macOS 14.0 or later
  • +
  • Apple Silicon or Intel Mac
  • +
+ + ); +} diff --git a/web/app/docs/keyboard-shortcuts/page.tsx b/web/app/docs/keyboard-shortcuts/page.tsx new file mode 100644 index 00000000..9995de49 --- /dev/null +++ b/web/app/docs/keyboard-shortcuts/page.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from "next"; +import { KeyboardShortcuts } from "../../keyboard-shortcuts"; + +export const metadata: Metadata = { + title: "Keyboard Shortcuts", + description: "Complete list of cmux keyboard shortcuts", +}; + +export default function KeyboardShortcutsPage() { + return ( + <> +

Keyboard Shortcuts

+

+ All keyboard shortcuts available in cmux, grouped by category. +

+ + + ); +} diff --git a/web/app/docs/layout.tsx b/web/app/docs/layout.tsx new file mode 100644 index 00000000..2cfde020 --- /dev/null +++ b/web/app/docs/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import { DocsNav } from "./docs-nav"; +import { SiteHeader } from "../components/site-header"; + +export const metadata: Metadata = { + title: { + template: "%s — cmux docs", + default: "cmux docs", + }, + openGraph: { + siteName: "cmux", + type: "article", + }, + alternates: { + canonical: "./", + }, +}; + +export default function DocsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ + {children} +
+ ); +} diff --git a/web/app/docs/notifications/page.tsx b/web/app/docs/notifications/page.tsx new file mode 100644 index 00000000..075f2de0 --- /dev/null +++ b/web/app/docs/notifications/page.tsx @@ -0,0 +1,202 @@ +import type { Metadata } from "next"; +import { CodeBlock } from "../../components/code-block"; +import { Callout } from "../../components/callout"; + +export const metadata: Metadata = { + title: "Notifications", + description: "Desktop notifications in cmux for AI agents and scripts", +}; + +export default function NotificationsPage() { + return ( + <> +

Notifications

+

+ cmux supports desktop notifications, allowing AI agents and scripts to + alert you when they need attention. +

+ +

Lifecycle

+
    +
  1. + Received — notification appears in panel, desktop + alert fires (if not suppressed) +
  2. +
  3. + Unread — badge shown on workspace tab +
  4. +
  5. + Read — cleared when you view that workspace +
  6. +
  7. + Cleared — removed from panel +
  8. +
+ +

Suppression

+

Desktop alerts are suppressed when:

+
    +
  • The cmux window is focused
  • +
  • The specific workspace sending the notification is active
  • +
  • The notification panel is open
  • +
+ +

Notification panel

+

+ Press ⌘⇧I to open the notification panel. Click a + notification to jump to that workspace. Press ⌘⇧U to jump + directly to the workspace with the most recent unread notification. +

+ +

Sending notifications

+ +

CLI

+ {`cmux notify --title "Task Complete" --body "Your build finished" +cmux notify --title "Claude Code" --subtitle "Waiting" --body "Agent needs input"`} + +

OSC 777 (simple)

+

+ The RXVT protocol uses a fixed format with title and body: +

+ {`printf '\\e]777;notify;My Title;Message body here\\a'`} + {`notify_osc777() { + local title="$1" + local body="$2" + printf '\\e]777;notify;%s;%s\\a' "$title" "$body" +} + +notify_osc777 "Build Complete" "All tests passed"`} + +

OSC 99 (rich)

+

+ The Kitty protocol supports subtitles and notification IDs: +

+ {`# Format: ESC ] 99 ; ; ESC \\ + +# Simple notification +printf '\\e]99;i=1;e=1;d=0:Hello World\\e\\\\' + +# With title, subtitle, and body +printf '\\e]99;i=1;e=1;d=0;p=title:Build Complete\\e\\\\' +printf '\\e]99;i=1;e=1;d=0;p=subtitle:Project X\\e\\\\' +printf '\\e]99;i=1;e=1;d=1;p=body:All tests passed\\e\\\\'`} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureOSC 99OSC 777
Title + bodyYesYes
SubtitleYesNo
Notification IDYesNo
ComplexityHigherLower
+ + + Use OSC 777 for simple notifications. Use OSC 99 when you need subtitles + or notification IDs. Use the CLI (cmux notify) for the + easiest integration. + + +

Claude Code hooks

+

+ cmux integrates with{" "} + Claude Code{" "} + via hooks to notify you when tasks complete. +

+ +

1. Create the hook script

+ {`#!/bin/bash +# Skip if not in cmux +[ -S /tmp/cmux.sock ] || exit 0 + +EVENT=$(cat) +EVENT_TYPE=$(echo "$EVENT" | jq -r '.event // "unknown"') +TOOL=$(echo "$EVENT" | jq -r '.tool_name // ""') + +case "$EVENT_TYPE" in + "Stop") + cmux notify --title "Claude Code" --body "Session complete" + ;; + "PostToolUse") + [ "$TOOL" = "Task" ] && cmux notify --title "Claude Code" --body "Agent finished" + ;; +esac`} + {`chmod +x ~/.claude/hooks/cmux-notify.sh`} + +

2. Configure Claude Code

+ {`{ + "hooks": { + "Stop": ["~/.claude/hooks/cmux-notify.sh"], + "PostToolUse": [ + { + "matcher": "Task", + "hooks": ["~/.claude/hooks/cmux-notify.sh"] + } + ] + } +}`} +

Restart Claude Code to apply the hooks.

+ +

Integration examples

+ +

Notify after long command

+ {`# Add to your shell config +notify-after() { + "$@" + local exit_code=$? + if [ $exit_code -eq 0 ]; then + cmux notify --title "✓ Command Complete" --body "$1" + else + cmux notify --title "✗ Command Failed" --body "$1 (exit $exit_code)" + fi + return $exit_code +} + +# Usage: notify-after npm run build`} + +

Python

+ {`import sys + +def notify(title: str, body: str): + """Send OSC 777 notification.""" + sys.stdout.write(f'\\x1b]777;notify;{title};{body}\\x07') + sys.stdout.flush() + +notify("Script Complete", "Processing finished")`} + +

Node.js

+ {`function notify(title, body) { + process.stdout.write(\`\\x1b]777;notify;\${title};\${body}\\x07\`); +} + +notify('Build Done', 'webpack finished');`} + +

tmux passthrough

+

If using tmux inside cmux, enable passthrough:

+ {`set -g allow-passthrough on`} + {`printf '\\ePtmux;\\e\\e]777;notify;Title;Body\\a\\e\\\\'`} + + ); +} diff --git a/web/app/docs/page.tsx b/web/app/docs/page.tsx new file mode 100644 index 00000000..d85e70c1 --- /dev/null +++ b/web/app/docs/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function DocsPage() { + redirect("/docs/getting-started"); +} diff --git a/web/app/globals.css b/web/app/globals.css index 8ce72285..92263fbf 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -72,3 +72,143 @@ body { .animate-blink { 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-content h2 { + font-size: 1.25rem; + font-weight: 600; + margin-top: 2.5rem; + margin-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 h4 { + font-size: 0.9375rem; + font-weight: 600; + margin-top: 1.25rem; + margin-bottom: 0.375rem; + font-family: var(--font-geist-mono); +} + +.docs-content > p { + line-height: 1.7; + margin-bottom: 1rem; + color: var(--muted); +} + +.docs-content ul, +.docs-content ol { + padding-left: 1.5rem; + margin-bottom: 1rem; +} + +.docs-content ul { + list-style: disc; +} + +.docs-content ol { + list-style: decimal; +} + +.docs-content li { + line-height: 1.7; + margin-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 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); +} + +.docs-content pre code { + background: none; + padding: 0; + font-size: 1em; +} + +/* Shiki dual theme */ +.shiki, +.shiki span { + color: var(--shiki-light) !important; + background-color: transparent !important; +} + +.dark .shiki, +.dark .shiki span { + color: var(--shiki-dark) !important; +} + +.docs-content table { + width: 100%; + border-collapse: collapse; + margin-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 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 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); +} diff --git a/web/app/keyboard-shortcuts.tsx b/web/app/keyboard-shortcuts.tsx index e0143003..e70d174a 100644 --- a/web/app/keyboard-shortcuts.tsx +++ b/web/app/keyboard-shortcuts.tsx @@ -1,198 +1,350 @@ -export function KeyboardShortcuts() { +"use client"; + +import { useMemo, useState } from "react"; + +type Shortcut = { + id: string; + combos: string[][]; + description: string; + note?: string; +}; + +type ShortcutCategory = { + id: string; + title: string; + blurb?: string; + shortcuts: Shortcut[]; +}; + +const CATEGORIES: ShortcutCategory[] = [ + { + id: "workspaces", + title: "Workspaces", + blurb: "Workspaces live in the sidebar. Each workspace has its own set of panes and surfaces.", + shortcuts: [ + { id: "ws-new", combos: [["⌘", "N"]], description: "New workspace" }, + { + id: "ws-jump-1-8", + combos: [["⌘", "1–8"]], + description: "Jump to workspace 1–8", + }, + { + id: "ws-jump-last", + combos: [["⌘", "9"]], + description: "Jump to last workspace", + }, + { + id: "ws-close", + combos: [["⌘", "⇧", "W"]], + description: "Close workspace", + }, + ], + }, + { + id: "surfaces", + title: "Surfaces", + blurb: "Surfaces are tabs inside a pane.", + shortcuts: [ + { id: "sf-new", combos: [["⌘", "T"]], description: "New surface" }, + { + id: "sf-prev-1", + combos: [["⌘", "⇧", "["]], + description: "Previous surface", + }, + { + id: "sf-prev-2", + combos: [["⌃", "⇧", "Tab"]], + description: "Previous surface", + }, + { + id: "sf-jump-1-8", + combos: [["⌃", "1–8"]], + description: "Jump to surface 1–8", + }, + { + id: "sf-jump-last", + combos: [["⌃", "9"]], + description: "Jump to last surface", + }, + { id: "sf-close", combos: [["⌘", "W"]], description: "Close surface" }, + ], + }, + { + id: "split-panes", + title: "Split Panes", + shortcuts: [ + { id: "sp-right", combos: [["⌘", "D"]], description: "Split right" }, + { id: "sp-down", combos: [["⌘", "⇧", "D"]], description: "Split down" }, + { + id: "sp-focus", + combos: [["⌥", "⌘", "←/→/↑/↓"]], + description: "Focus pane directionally", + }, + ], + }, + { + id: "browser", + title: "Browser", + shortcuts: [ + { + id: "br-open", + combos: [["⌘", "⇧", "B"]], + description: "Open browser in split", + }, + { id: "br-addr", combos: [["⌘", "L"]], description: "Focus address bar" }, + { id: "br-forward", combos: [["⌘", "]"]], description: "Forward" }, + { id: "br-reload", combos: [["⌘", "R"]], description: "Reload page" }, + { + id: "br-devtools", + combos: [["⌥", "⌘", "I"]], + description: "Open Developer Tools", + }, + ], + }, + { + id: "notifications", + title: "Notifications", + shortcuts: [ + { + id: "nt-panel", + combos: [["⌘", "⇧", "I"]], + description: "Show notifications panel", + }, + { + id: "nt-latest", + combos: [["⌘", "⇧", "U"]], + description: "Jump to latest unread", + }, + { + id: "nt-flash", + combos: [["⌘", "⇧", "L"]], + description: "Trigger flash", + }, + ], + }, + { + id: "find", + title: "Find", + shortcuts: [ + { id: "fd-find", combos: [["⌘", "F"]], description: "Find" }, + { + id: "fd-next-prev", + combos: [ + ["⌘", "G"], + ["⌘", "⇧", "G"], + ], + description: "Find next / previous", + }, + { + id: "fd-hide", + combos: [["⌘", "⇧", "F"]], + description: "Hide find bar", + }, + { + id: "fd-selection", + combos: [["⌘", "E"]], + description: "Use selection for find", + }, + ], + }, + { + id: "terminal", + title: "Terminal", + shortcuts: [ + { + id: "tm-clear", + combos: [["⌘", "K"]], + description: "Clear scrollback", + }, + { + id: "tm-copy", + combos: [["⌘", "C"]], + description: "Copy (with selection)", + }, + { id: "tm-paste", combos: [["⌘", "V"]], description: "Paste" }, + { + id: "tm-font", + combos: [ + ["⌘", "+"], + ["⌘", "-"], + ], + description: "Increase / decrease font size", + }, + { id: "tm-reset", combos: [["⌘", "0"]], description: "Reset font size" }, + ], + }, + { + id: "window", + title: "Window", + shortcuts: [ + { id: "wn-new", combos: [["⌘", "⇧", "N"]], description: "New window" }, + { id: "wn-settings", combos: [["⌘", ","]], description: "Settings" }, + { + id: "wn-reload", + combos: [["⌘", "⇧", "R"]], + description: "Reload configuration", + }, + { id: "wn-quit", combos: [["⌘", "Q"]], description: "Quit" }, + ], + }, +]; + +function normalize(s: string) { + return s.toLowerCase().replace(/\s+/g, " ").trim(); +} + +function comboToText(combo: string[]) { + return combo.join(" "); +} + +function shortcutSearchText(category: ShortcutCategory, s: Shortcut) { + const combos = s.combos.map(comboToText).join(" "); + return normalize(`${category.title} ${combos} ${s.description} ${s.note ?? ""}`); +} + +function KeyCombo({ combo }: { combo: string[] }) { return ( -
-

- Keyboard Shortcuts -

-
- {/* Workspaces */} -
-

Workspaces

-
    -
  • - ⌘ N - New workspace -
  • -
  • - ⌘ 1 – 8 - Jump to workspace 1–8 -
  • -
  • - ⌘ 9 - Jump to last workspace -
  • -
  • - ⌘ ⇧ W - Close workspace -
  • -
-
- - {/* Surfaces */} -
-

Surfaces

-
    -
  • - ⌘ T - New surface -
  • -
  • - ⌘ ⇧ [ - Previous surface -
  • -
  • - ⌃ ⇧ Tab - Previous surface -
  • -
  • - ⌃ 1 – 8 - Jump to surface 1–8 -
  • -
  • - ⌃ 9 - Jump to last surface -
  • -
  • - ⌘ W - Close surface -
  • -
-
- - {/* Split Panes */} -
-

Split Panes

-
    -
  • - ⌘ D - Split right -
  • -
  • - ⌘ ⇧ D - Split down -
  • -
  • - ⌥ ⌘ ← → ↑ ↓ - Focus pane directionally -
  • -
-
- - {/* Browser */} -
-

Browser

-
    -
  • - ⌘ ⇧ B - Open browser in split -
  • -
  • - ⌘ L - Focus address bar -
  • -
  • - ⌘ ] - Forward -
  • -
  • - ⌘ R - Reload page -
  • -
  • - ⌥ ⌘ I - Open Developer Tools -
  • -
-
- - {/* Notifications */} -
-

Notifications

-
    -
  • - ⌘ ⇧ I - Show notifications panel -
  • -
  • - ⌘ ⇧ U - Jump to latest unread -
  • -
-
- - {/* Find */} -
-

Find

-
    -
  • - ⌘ F - Find -
  • -
  • - ⌘ G  /  ⌘ ⇧ G - Find next / previous -
  • -
  • - ⌘ ⇧ F - Hide find bar -
  • -
  • - ⌘ E - Use selection for find -
  • -
-
- - {/* Terminal */} -
-

Terminal

-
    -
  • - ⌘ K - Clear scrollback -
  • -
  • - ⌘ C - Copy (with selection) -
  • -
  • - ⌘ V - Paste -
  • -
  • - ⌘ +  /  ⌘ - - Increase / decrease font size -
  • -
  • - ⌘ 0 - Reset font size -
  • -
-
- - {/* Window */} -
-

Window

-
    -
  • - ⌘ ⇧ N - New window -
  • -
  • - ⌘ , - Settings -
  • -
  • - ⌘ ⇧ R - Reload configuration -
  • -
  • - ⌘ Q - Quit -
  • -
-
-
-
+ + {combo.map((k, idx) => ( + + {k} + {idx < combo.length - 1 && ( + + + + + )} + + ))} + + ); +} + +function ShortcutRow({ shortcut }: { shortcut: Shortcut }) { + return ( +
+
+ + {shortcut.description} + + {shortcut.note && ( + + {shortcut.note} + + )} +
+
+ {shortcut.combos.map((combo, idx) => ( + + {idx > 0 && ( + + / + + )} + + + ))} +
+
+ ); +} + +export function KeyboardShortcuts() { + const [query, setQuery] = useState(""); + + const filtered = useMemo(() => { + const q = normalize(query); + if (!q) return CATEGORIES; + return CATEGORIES.map((cat) => ({ + ...cat, + shortcuts: cat.shortcuts.filter((s) => + shortcutSearchText(cat, s).includes(q), + ), + })).filter((cat) => cat.shortcuts.length > 0); + }, [query]); + + return ( +
+ {/* Search */} +
+
+ + + + +
+ setQuery(e.target.value)} + placeholder="Search shortcuts..." + className="w-full pl-9 pr-3 py-1.5 rounded-lg border border-border bg-transparent text-[13px] placeholder:text-muted/40 focus:outline-none focus:border-foreground/20 transition-colors" + aria-label="Search keyboard shortcuts" + /> +
+ + {/* Category jump links */} + {!query && ( + + )} + + {/* Content */} + {filtered.length === 0 ? ( +
+

No shortcuts found

+

+ Try a different search term +

+
+ ) : ( +
+ {filtered.map((cat) => ( +
+
+
+ {cat.title} +
+ {cat.blurb && ( +

{cat.blurb}

+ )} +
+
+
+ {cat.shortcuts.map((s) => ( + + ))} +
+
+
+ ))} +
+ )} +
); } diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 2c4d47fe..e1b7aa57 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,6 +1,9 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import { Providers } from "./providers"; +import { ThemeToggle } from "./theme"; +import { DevPanel } from "./components/spacing-control"; +import { SiteFooter } from "./components/nav-links"; import "./globals.css"; const geistSans = Geist({ @@ -53,7 +56,14 @@ export default function RootLayout({ - {children} + +
+ +
+ {children} + + +
); diff --git a/web/app/page.tsx b/web/app/page.tsx index 292148dd..71f01c86 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -1,19 +1,17 @@ import Image from "next/image"; +import Balancer from "react-wrap-balancer"; import { TypingTagline } from "./typing"; -import { ThemeToggle } from "./theme"; - +import { DownloadButton } from "./components/download-button"; +import { SiteHeader } from "./components/site-header"; export default function Home() { return ( -
- {/* Theme toggle */} -
- -
+
+ -
+
{/* Header */} -
+
cmux icon - A terminal built for + The terminal built for

-

- Native macOS app built on Ghostty. Vertical tabs, notification rings - when agents need attention, split panes, and a socket API for - automation. +

+ + Native macOS app built on Ghostty. Vertical tabs, notification rings + when agents need attention, split panes, and a socket API for + automation. +

{/* Download */} -
+
); } diff --git a/web/app/robots.ts b/web/app/robots.ts new file mode 100644 index 00000000..8cb44e16 --- /dev/null +++ b/web/app/robots.ts @@ -0,0 +1,8 @@ +import type { MetadataRoute } from "next"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { userAgent: "*", allow: "/" }, + sitemap: "https://cmux.dev/sitemap.xml", + }; +} diff --git a/web/app/sitemap.ts b/web/app/sitemap.ts new file mode 100644 index 00000000..ef80f09e --- /dev/null +++ b/web/app/sitemap.ts @@ -0,0 +1,16 @@ +import type { MetadataRoute } from "next"; + +export default function sitemap(): MetadataRoute.Sitemap { + const base = "https://cmux.dev"; + + return [ + { url: base, lastModified: new Date(), changeFrequency: "weekly", priority: 1 }, + { 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/configuration`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 }, + { url: `${base}/docs/keyboard-shortcuts`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.7 }, + { 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/changelog`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.5 }, + ]; +} diff --git a/web/app/typing.tsx b/web/app/typing.tsx index 8a9a93ef..345c0b63 100644 --- a/web/app/typing.tsx +++ b/web/app/typing.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { useDevValues } from "./components/spacing-control"; const phrases = [ "coding agents", @@ -15,21 +16,7 @@ export function TypingTagline() { const [phraseIndex, setPhraseIndex] = useState(0); const [charIndex, setCharIndex] = useState(0); const [deleting, setDeleting] = useState(false); - const [showControls, setShowControls] = useState(false); - const [topOffset, setTopOffset] = useState(0); - const [blink, setBlink] = useState(true); - - useEffect(() => { - if (process.env.NODE_ENV !== "development") return; - const handler = (e: KeyboardEvent) => { - if (e.key === "." && e.metaKey) { - e.preventDefault(); - setShowControls((s) => !s); - } - }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, []); + const dev = useDevValues(); useEffect(() => { const phrase = phrases[phraseIndex]; @@ -57,49 +44,14 @@ export function TypingTagline() { const phrase = phrases[phraseIndex]; const displayed = phrase.slice(0, charIndex); - const tailwindClass = - topOffset > 0 - ? `-top-[${topOffset}px]` - : topOffset < 0 - ? `top-[${Math.abs(topOffset)}px]` - : ""; return ( {displayed} setShowControls((s) => !s)} + className={`inline-block w-[2px] h-[1.1em] bg-foreground/70 ml-[1px] ${dev.cursorBlink ? "animate-blink" : ""}`} + style={{ position: "relative", top: `${dev.cursorTop}px` }} /> - {showControls && ( - - - - - {tailwindClass || "0px"} - - - )} ); } diff --git a/web/bun.lock b/web/bun.lock index af2962f5..21fc81c0 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -9,6 +9,8 @@ "next-themes": "^0.4.6", "react": "19.2.3", "react-dom": "19.2.3", + "react-wrap-balancer": "^1.1.1", + "shiki": "^3.22.0", }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -185,6 +187,20 @@ "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + "@shikijs/core": ["@shikijs/core@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA=="], + + "@shikijs/langs": ["@shikijs/langs@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA=="], + + "@shikijs/themes": ["@shikijs/themes@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g=="], + + "@shikijs/types": ["@shikijs/types@3.22.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], @@ -221,16 +237,22 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], "@types/react": ["@types/react@19.2.13", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.55.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/type-utils": "8.55.0", "@typescript-eslint/utils": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.55.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.55.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", "@typescript-eslint/typescript-estree": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw=="], @@ -251,6 +273,8 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.55.0", "", { "dependencies": { "@typescript-eslint/types": "8.55.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], @@ -347,14 +371,22 @@ "caniuse-lite": ["caniuse-lite@1.0.30001769", "", {}, "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], "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=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -379,8 +411,12 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -509,10 +545,16 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -641,8 +683,20 @@ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -679,6 +733,10 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], @@ -707,6 +765,8 @@ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -717,8 +777,16 @@ "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "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=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], @@ -753,6 +821,8 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shiki": ["shiki@3.22.0", "", { "dependencies": { "@shikijs/core": "3.22.0", "@shikijs/engine-javascript": "3.22.0", "@shikijs/engine-oniguruma": "3.22.0", "@shikijs/langs": "3.22.0", "@shikijs/themes": "3.22.0", "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -763,6 +833,8 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], @@ -779,6 +851,8 @@ "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], @@ -797,6 +871,8 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], @@ -821,12 +897,26 @@ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "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=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -847,6 +937,8 @@ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], diff --git a/web/package.json b/web/package.json index d9707c9b..3fa347d1 100644 --- a/web/package.json +++ b/web/package.json @@ -12,7 +12,9 @@ "next": "16.1.6", "next-themes": "^0.4.6", "react": "19.2.3", - "react-dom": "19.2.3" + "react-dom": "19.2.3", + "react-wrap-balancer": "^1.1.1", + "shiki": "^3.22.0" }, "devDependencies": { "@tailwindcss/postcss": "^4",