Add docs, blog, community pages and polish landing page layout
- Add docs pages (getting-started, changelog, keyboard-shortcuts) - Add blog, community, and legal pages (privacy, terms, EULA) - Add site header, footer, download button, and nav components - Add sitemap and robots.txt generation - Narrow main page container (max-w-2xl), fix footer positioning - Switch README feature list to colon style
This commit is contained in:
parent
5febb66873
commit
f970cdcf33
37 changed files with 3304 additions and 296 deletions
12
README.md
12
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
|
||||
|
||||
|
|
|
|||
206
web/app/(legal)/eula/page.tsx
Normal file
206
web/app/(legal)/eula/page.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<h1>End-User License Agreement</h1>
|
||||
<p>Last updated: December 2, 2025</p>
|
||||
|
||||
<p>
|
||||
Please read this End-User License Agreement carefully before
|
||||
downloading or using cmux.
|
||||
</p>
|
||||
|
||||
<h2>Interpretation and Definitions</h2>
|
||||
<p>For the purposes of this Agreement:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>“Agreement”</strong> means this End-User License
|
||||
Agreement that forms the entire agreement between You and the Company
|
||||
regarding the use of the Application.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“Application”</strong> means the cmux desktop
|
||||
application for macOS, a native terminal application built on Ghostty.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“Company”</strong> (referred to as “the
|
||||
Company”, “We”, “Us” or
|
||||
“Our”) refers to Manaflow.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“Content”</strong> refers to content such as text,
|
||||
code, images, or other information that can be created, processed, or
|
||||
displayed by the Application.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“Country”</strong> refers to the United States.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“Device”</strong> means any macOS computer that
|
||||
can run the Application.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“You”</strong> means the individual accessing or
|
||||
using the Application.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Acknowledgment</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
The Application is licensed, not sold, to You by the Company for use
|
||||
strictly in accordance with the terms of this Agreement.
|
||||
</p>
|
||||
|
||||
<h2>License</h2>
|
||||
|
||||
<h3>Scope of License</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h3>License Restrictions</h3>
|
||||
<p>You agree not to, and You will not permit others to:</p>
|
||||
<ul>
|
||||
<li>
|
||||
License, sell, rent, lease, assign, distribute, transmit, host, or
|
||||
otherwise commercially exploit the Application or make it available to
|
||||
any third party
|
||||
</li>
|
||||
<li>
|
||||
Remove, alter or obscure any proprietary notice (including copyright
|
||||
or trademark) of the Company
|
||||
</li>
|
||||
<li>
|
||||
Modify, make derivative works of, disassemble, decrypt, reverse
|
||||
compile or reverse engineer any part of the Application
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Intellectual Property</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
You retain ownership of any code or content you create using the
|
||||
Application.
|
||||
</p>
|
||||
|
||||
<h2>Modifications and Updates</h2>
|
||||
<p>
|
||||
The Company reserves the right to modify, suspend or discontinue the
|
||||
Application at any time, with or without notice and without liability to
|
||||
You.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>Third-Party Services</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>Term and Termination</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
Upon termination, You shall cease all use of the Application and delete
|
||||
all copies from your Device.
|
||||
</p>
|
||||
|
||||
<h2>No Warranties</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
Some jurisdictions do not allow the exclusion of certain types of
|
||||
warranties, so some of the above exclusions may not apply to You.
|
||||
</p>
|
||||
|
||||
<h2>Limitation of Liability</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
To the maximum extent permitted by law, in no event shall the Company
|
||||
be liable for any special, incidental, indirect, or consequential
|
||||
damages whatsoever.
|
||||
</p>
|
||||
|
||||
<h2>Indemnification</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>Severability and Waiver</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>Governing Law</h2>
|
||||
<p>
|
||||
The laws of the United States, excluding conflicts of law rules, shall
|
||||
govern this Agreement and your use of the Application.
|
||||
</p>
|
||||
|
||||
<h2>Changes to This Agreement</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>Contact Us</h2>
|
||||
<p>If you have any questions about this Agreement:</p>
|
||||
<ul>
|
||||
<li>
|
||||
Email us at{" "}
|
||||
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
16
web/app/(legal)/layout.tsx
Normal file
16
web/app/(legal)/layout.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { SiteHeader } from "../components/site-header";
|
||||
|
||||
export default function LegalLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<SiteHeader />
|
||||
<main className="w-full max-w-5xl mx-auto px-6 py-10">
|
||||
<div className="docs-content text-[15px]">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
162
web/app/(legal)/privacy-policy/page.tsx
Normal file
162
web/app/(legal)/privacy-policy/page.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>Last updated: December 2, 2025</p>
|
||||
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
For purposes of this policy, “Site” refers to the
|
||||
Company’s website at{" "}
|
||||
<a href="https://cmux.dev">cmux.dev</a>.
|
||||
“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.
|
||||
</p>
|
||||
<p>
|
||||
By using our Service, you accept this Privacy Policy and our{" "}
|
||||
<a href="/terms-of-service">Terms of Service</a>, and you consent to
|
||||
our collection, storage, use and disclosure of your information as
|
||||
described here.
|
||||
</p>
|
||||
|
||||
<h2>I. Information We Collect</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h3>1. Information collected via Technology</h3>
|
||||
<p>
|
||||
The Application may collect the following information automatically:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Crash reports and error diagnostics (via Sentry)</li>
|
||||
<li>Operating system version and application version</li>
|
||||
<li>Anonymous usage patterns</li>
|
||||
</ul>
|
||||
<p>
|
||||
The Application checks for updates via Sparkle, which may transmit your
|
||||
operating system version and application version to our update server.
|
||||
</p>
|
||||
|
||||
<h3>2. Information you provide directly</h3>
|
||||
<p>
|
||||
If you contact us via email or our contact page, we collect the
|
||||
information you provide such as your name and email address.
|
||||
</p>
|
||||
|
||||
<h3>3. Children’s Privacy</h3>
|
||||
<p>
|
||||
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{" "}
|
||||
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a>.
|
||||
</p>
|
||||
|
||||
<h2>II. Third-Party Services</h2>
|
||||
<p>
|
||||
The Application integrates with the following third-party services:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Sentry</strong> — error tracking and crash reporting.
|
||||
May collect error logs, stack traces, device information, and OS
|
||||
version.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Sparkle</strong> — auto-update framework. Transmits
|
||||
application and OS version to check for updates.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Ghostty / libghostty</strong> — terminal rendering
|
||||
engine. Runs entirely locally on your device.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Each of these services has its own privacy policy governing the
|
||||
collection and use of your data.
|
||||
</p>
|
||||
|
||||
<h2>III. How We Use and Share Information</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>IV. How We Protect Information</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>V. Your Rights</h2>
|
||||
<p>
|
||||
Depending on your location, you may have rights under applicable data
|
||||
protection laws (such as GDPR or CCPA), including:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Right to access a copy of data we hold about you</li>
|
||||
<li>Right to request correction of inaccurate data</li>
|
||||
<li>Right to request deletion of your data</li>
|
||||
<li>Right to data portability</li>
|
||||
<li>Right to restrict or object to processing</li>
|
||||
</ul>
|
||||
<p>
|
||||
To exercise any of these rights, please contact us at{" "}
|
||||
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a>.
|
||||
</p>
|
||||
|
||||
<h2>VI. Links to Other Websites</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>VII. Changes to This Policy</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>VIII. Contact Us</h2>
|
||||
<p>
|
||||
If you have any questions regarding this Privacy Policy, please contact
|
||||
us at{" "}
|
||||
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a>.
|
||||
</p>
|
||||
|
||||
<h2>IX. Data Retention</h2>
|
||||
<p>
|
||||
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{" "}
|
||||
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a>.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
187
web/app/(legal)/terms-of-service/page.tsx
Normal file
187
web/app/(legal)/terms-of-service/page.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<h1>Terms of Service</h1>
|
||||
<p>Last revised on: December 2, 2025</p>
|
||||
|
||||
<p>
|
||||
The website located at{" "}
|
||||
<a href="https://cmux.dev">cmux.dev</a> (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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>1. License</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h3>Restrictions</h3>
|
||||
<p>The rights granted to you are subject to the following restrictions:</p>
|
||||
<ul>
|
||||
<li>
|
||||
You shall not license, sell, rent, lease, transfer, assign,
|
||||
distribute, host, or otherwise commercially exploit the Application
|
||||
</li>
|
||||
<li>
|
||||
You shall not modify, make derivative works of, disassemble, reverse
|
||||
compile or reverse engineer any part of the Application
|
||||
</li>
|
||||
<li>
|
||||
You shall not access the Application in order to build a similar or
|
||||
competitive product
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Modification</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h3>Ownership</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h3>Feedback</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>2. User Content</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>3. Indemnification</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>4. Third-Party Links</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>5. Disclaimers</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO
|
||||
THE ABOVE EXCLUSION MAY NOT APPLY TO YOU.
|
||||
</p>
|
||||
|
||||
<h2>6. Limitation on Liability</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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).
|
||||
</p>
|
||||
|
||||
<h2>7. Term and Termination</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>8. Dispute Resolution</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
You have the right to opt out of this arbitration agreement by sending
|
||||
written notice to{" "}
|
||||
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a> within 30
|
||||
days of first becoming subject to it.
|
||||
</p>
|
||||
|
||||
<h2>9. General</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>10. Contact</h2>
|
||||
<p>
|
||||
Questions about these Terms should be sent to{" "}
|
||||
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Copyright © 2025 Manaflow. All rights reserved.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
70
web/app/blog/introducing-cmux/page.tsx
Normal file
70
web/app/blog/introducing-cmux/page.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="text-sm text-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
← Back to blog
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<h1>Introducing cmux</h1>
|
||||
<time className="text-sm text-muted">June 1, 2025</time>
|
||||
|
||||
<p className="mt-6">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>Why cmux?</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>Key features</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Vertical tabs</strong> — see all your terminals at a
|
||||
glance in a sidebar
|
||||
</li>
|
||||
<li>
|
||||
<strong>Notification rings</strong> — tabs flash when an agent
|
||||
needs your input
|
||||
</li>
|
||||
<li>
|
||||
<strong>Split panes</strong> — horizontal and vertical splits
|
||||
within each workspace
|
||||
</li>
|
||||
<li>
|
||||
<strong>Socket API</strong> — programmatic control for creating
|
||||
tabs and sending input
|
||||
</li>
|
||||
<li>
|
||||
<strong>GPU-accelerated</strong> — powered by libghostty for
|
||||
smooth rendering
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Get started</h2>
|
||||
<p>
|
||||
Install cmux via Homebrew or download the DMG from the{" "}
|
||||
<Link href="/docs/getting-started">getting started guide</Link>.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
31
web/app/blog/layout.tsx
Normal file
31
web/app/blog/layout.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="min-h-screen">
|
||||
<SiteHeader section="blog" />
|
||||
<main className="w-full max-w-5xl mx-auto px-6 py-10">
|
||||
<div className="docs-content text-[15px]">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
web/app/blog/page.tsx
Normal file
41
web/app/blog/page.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<h1>Blog</h1>
|
||||
<div className="space-y-8 mt-6">
|
||||
{posts.map((post) => (
|
||||
<article key={post.slug}>
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
className="block group"
|
||||
>
|
||||
<h2 className="text-lg font-medium group-hover:underline">
|
||||
{post.title}
|
||||
</h2>
|
||||
<time className="text-sm text-muted">{post.date}</time>
|
||||
<p className="mt-1 text-muted">{post.summary}</p>
|
||||
</Link>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
119
web/app/community/page.tsx
Normal file
119
web/app/community/page.tsx
Normal file
|
|
@ -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 (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-start gap-4 rounded-xl border border-border p-5 hover:bg-code-bg transition-colors"
|
||||
>
|
||||
<div className="shrink-0 mt-0.5 text-muted group-hover:text-foreground transition-colors">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium text-[15px]">{name}</div>
|
||||
<div className="text-sm text-muted mt-0.5">{description}</div>
|
||||
<div className="text-xs font-medium text-muted mt-2 group-hover:text-foreground transition-colors">
|
||||
{action} →
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CommunityPage() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<SiteHeader section="community" />
|
||||
<main className="w-full max-w-5xl mx-auto px-6 py-10">
|
||||
<h1 className="text-2xl font-semibold tracking-tight mb-2">
|
||||
Community
|
||||
</h1>
|
||||
<p className="text-muted text-[15px] mb-8">
|
||||
Connect with other cmux users and the team behind it.
|
||||
</p>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<CommunityLink
|
||||
href="https://discord.gg/SDbQmzQhRK"
|
||||
name="Discord"
|
||||
action="Join our Discord"
|
||||
description="Chat with the community, get help, and share feedback"
|
||||
icon={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
<CommunityLink
|
||||
href="https://github.com/manaflow-ai/cmux"
|
||||
name="GitHub"
|
||||
action="View on GitHub"
|
||||
description="Star the repo, report issues, and contribute"
|
||||
icon={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
<CommunityLink
|
||||
href="https://twitter.com/manaflowai"
|
||||
name="Twitter"
|
||||
action="Follow on X"
|
||||
description="Updates, announcements, and tips"
|
||||
icon={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
<CommunityLink
|
||||
href="https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw"
|
||||
name="YouTube"
|
||||
action="Subscribe"
|
||||
description="Demos, tutorials, and walkthroughs"
|
||||
icon={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
<CommunityLink
|
||||
href="https://www.linkedin.com/company/manaflow-ai/"
|
||||
name="LinkedIn"
|
||||
action="Follow us"
|
||||
description="Company news and engineering updates"
|
||||
icon={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
web/app/components/callout.tsx
Normal file
20
web/app/components/callout.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export function Callout({
|
||||
type = "info",
|
||||
children,
|
||||
}: {
|
||||
type?: "info" | "warn";
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const styles =
|
||||
type === "warn"
|
||||
? "border-l-amber-500 bg-amber-500/5"
|
||||
: "border-l-blue-500 bg-blue-500/5";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles} border-l-2 px-4 py-3 mb-4 rounded-r-lg text-[14px] text-muted leading-relaxed`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
59
web/app/components/code-block.tsx
Normal file
59
web/app/components/code-block.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { codeToHtml } from "shiki";
|
||||
|
||||
export async function CodeBlock({
|
||||
children,
|
||||
title,
|
||||
lang,
|
||||
variant = "code",
|
||||
}: {
|
||||
children: string;
|
||||
title?: string;
|
||||
lang?: string;
|
||||
variant?: "code" | "ascii";
|
||||
}) {
|
||||
const lineHeightClass =
|
||||
variant === "ascii" ? "leading-[1.15]" : "leading-[1.45]";
|
||||
|
||||
if (lang && variant !== "ascii") {
|
||||
const html = await codeToHtml(children, {
|
||||
lang,
|
||||
themes: { light: "github-light", dark: "github-dark" },
|
||||
defaultColor: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
{title && (
|
||||
<div className="text-[11px] font-mono text-muted px-4 py-1.5 bg-code-bg border border-border border-b-0 rounded-t-lg">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`[&_pre]:bg-code-bg [&_pre]:border [&_pre]:border-border [&_pre]:px-4 [&_pre]:py-3 [&_pre]:overflow-x-auto [&_pre]:text-[13px] [&_pre]:${lineHeightClass} [&_pre]:font-mono ${
|
||||
title
|
||||
? "[&_pre]:rounded-b-lg [&_pre]:border-t-0"
|
||||
: "[&_pre]:rounded-lg"
|
||||
} [&_code]:bg-transparent [&_code]:p-0`}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
{title && (
|
||||
<div className="text-[11px] font-mono text-muted px-4 py-1.5 bg-code-bg border border-border border-b-0 rounded-t-lg">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<pre
|
||||
className={`bg-code-bg border border-border px-4 py-3 overflow-x-auto text-[13px] ${lineHeightClass} font-mono ${
|
||||
title ? "rounded-b-lg" : "rounded-lg"
|
||||
}`}
|
||||
>
|
||||
<code>{children}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
web/app/components/docs-nav-items.ts
Normal file
9
web/app/components/docs-nav-items.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export const navItems = [
|
||||
{ title: "Getting Started", href: "/docs/getting-started" },
|
||||
{ title: "Concepts", href: "/docs/concepts" },
|
||||
{ title: "Configuration", href: "/docs/configuration" },
|
||||
{ title: "Keyboard Shortcuts", href: "/docs/keyboard-shortcuts" },
|
||||
{ title: "API Reference", href: "/docs/api" },
|
||||
{ title: "Notifications", href: "/docs/notifications" },
|
||||
{ title: "Changelog", href: "/docs/changelog" },
|
||||
];
|
||||
41
web/app/components/docs-pager.tsx
Normal file
41
web/app/components/docs-pager.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { navItems } from "./docs-nav-items";
|
||||
|
||||
export function DocsPager() {
|
||||
const pathname = usePathname();
|
||||
const index = navItems.findIndex((item) => item.href === pathname);
|
||||
const prev = index > 0 ? navItems[index - 1] : null;
|
||||
const next = index < navItems.length - 1 ? navItems[index + 1] : null;
|
||||
|
||||
if (!prev && !next) return null;
|
||||
|
||||
return (
|
||||
<nav className="flex items-center justify-between mt-12 pt-6 border-t border-border text-[14px]">
|
||||
{prev ? (
|
||||
<Link
|
||||
href={prev.href}
|
||||
className="flex items-center gap-1.5 text-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
<span aria-hidden>←</span>
|
||||
{prev.title}
|
||||
</Link>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{next ? (
|
||||
<Link
|
||||
href={next.href}
|
||||
className="flex items-center gap-1.5 text-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
{next.title}
|
||||
<span aria-hidden>→</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
31
web/app/components/docs-sidebar.tsx
Normal file
31
web/app/components/docs-sidebar.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { navItems } from "./docs-nav-items";
|
||||
|
||||
export function DocsSidebar({ onNavigate }: { onNavigate?: () => void }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<nav className="space-y-0.5">
|
||||
{navItems.map((item) => {
|
||||
const active = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={onNavigate}
|
||||
className={`block px-3 py-1.5 text-[14px] rounded-md transition-colors ${
|
||||
active
|
||||
? "text-foreground font-medium bg-code-bg"
|
||||
: "text-muted hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
22
web/app/components/download-button.tsx
Normal file
22
web/app/components/download-button.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export function DownloadButton({ size = "default" }: { size?: "default" | "sm" }) {
|
||||
const isSmall = size === "sm";
|
||||
return (
|
||||
<a
|
||||
href="https://github.com/manaflow-ai/cmux/releases/latest/download/cmux-macos.dmg"
|
||||
className={`inline-flex items-center rounded-full font-medium bg-foreground hover:opacity-85 transition-opacity ${
|
||||
isSmall ? "gap-2 px-4 py-1.5 text-xs" : "gap-2.5 px-5 py-2.5 text-[15px]"
|
||||
}`}
|
||||
style={{ color: "var(--background)", textDecoration: "none" }}
|
||||
>
|
||||
<svg
|
||||
width={isSmall ? 12 : 16}
|
||||
height={isSmall ? 14 : 19}
|
||||
viewBox="0 0 814 1000"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57.8-155.5-127.4c-58.3-81.6-105.6-208.4-105.6-328.6 0-193 125.6-295.5 249.2-295.5 65.7 0 120.5 43.1 161.7 43.1 39.2 0 100.4-45.8 175.1-45.8 28.3 0 130.3 2.6 197.2 99.2zM554.1 159.4c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.9 32.4-57.2 83.6-57.2 135.4 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 137.6-71.2z" />
|
||||
</svg>
|
||||
Download for Mac
|
||||
</a>
|
||||
);
|
||||
}
|
||||
56
web/app/components/nav-links.tsx
Normal file
56
web/app/components/nav-links.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import Link from "next/link";
|
||||
import { DownloadButton } from "./download-button";
|
||||
|
||||
export function NavLinks() {
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
href="/docs/getting-started"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
Blog
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs/changelog"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
Changelog
|
||||
</Link>
|
||||
<Link
|
||||
href="/community"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
Community
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/manaflow-ai/cmux"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
GitHub
|
||||
</Link>
|
||||
<DownloadButton size="sm" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className="py-8 flex justify-center">
|
||||
<div className="flex items-center gap-4 text-sm text-muted">
|
||||
<a href="https://github.com/manaflow-ai/cmux" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">GitHub</a>
|
||||
<a href="https://twitter.com/manaflowai" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">Twitter</a>
|
||||
<a href="https://discord.gg/SDbQmzQhRK" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">Discord</a>
|
||||
<Link href="/privacy-policy" className="hover:text-foreground transition-colors">Privacy</Link>
|
||||
<Link href="/terms-of-service" className="hover:text-foreground transition-colors">Terms</Link>
|
||||
<Link href="/eula" className="hover:text-foreground transition-colors">EULA</Link>
|
||||
<a href="mailto:founders@manaflow.com" className="hover:text-foreground transition-colors">Contact</a>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
39
web/app/components/site-header.tsx
Normal file
39
web/app/components/site-header.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { NavLinks } from "./nav-links";
|
||||
|
||||
export function SiteHeader({ section, hideLogo }: { section?: string; hideLogo?: boolean }) {
|
||||
return (
|
||||
<header className="sticky top-0 z-30 w-full bg-background/80 backdrop-blur-sm">
|
||||
<div className="w-full max-w-5xl mx-auto flex items-center justify-between px-6 h-12">
|
||||
<div className="flex items-center gap-3">
|
||||
{!hideLogo && (
|
||||
<>
|
||||
<Link href="/" className="flex items-center gap-2.5">
|
||||
<Image
|
||||
src="/icon.png"
|
||||
alt="cmux"
|
||||
width={24}
|
||||
height={24}
|
||||
className="rounded-md"
|
||||
/>
|
||||
<span className="text-sm font-semibold tracking-tight">
|
||||
cmux
|
||||
</span>
|
||||
</Link>
|
||||
{section && (
|
||||
<>
|
||||
<span className="text-border text-[13px]">/</span>
|
||||
<span className="text-[13px] text-muted">{section}</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<nav className="flex items-center gap-4 text-sm text-muted">
|
||||
<NavLinks />
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
221
web/app/components/spacing-control.tsx
Normal file
221
web/app/components/spacing-control.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef, useCallback, useSyncExternalStore } from "react";
|
||||
|
||||
type DevValues = {
|
||||
headerTx: number;
|
||||
cursorTop: number;
|
||||
cursorBlink: boolean;
|
||||
subtitleLh: number;
|
||||
downloadAbove: number;
|
||||
downloadBelow: number;
|
||||
featuresLh: number;
|
||||
featuresMb: number;
|
||||
docsPt: number;
|
||||
};
|
||||
|
||||
const defaults: DevValues = {
|
||||
headerTx: -4,
|
||||
cursorTop: 2.5,
|
||||
cursorBlink: true,
|
||||
subtitleLh: 1.5,
|
||||
downloadAbove: 21,
|
||||
downloadBelow: 33,
|
||||
featuresLh: 1.275,
|
||||
featuresMb: 23,
|
||||
docsPt: 8,
|
||||
};
|
||||
|
||||
// Tiny external store (avoids setState-during-render)
|
||||
let snapshot = { ...defaults };
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function getSnapshot() { return snapshot; }
|
||||
function getServerSnapshot() { return defaults; }
|
||||
|
||||
function setStore(patch: Partial<DevValues>) {
|
||||
snapshot = { ...snapshot, ...patch };
|
||||
listeners.forEach((l) => l());
|
||||
}
|
||||
|
||||
function subscribe(cb: () => void) {
|
||||
listeners.add(cb);
|
||||
return () => { listeners.delete(cb); };
|
||||
}
|
||||
|
||||
export function useDevValues() {
|
||||
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
||||
}
|
||||
|
||||
function el(name: string) {
|
||||
return document.querySelector(`[data-dev="${name}"]`) as HTMLElement | null;
|
||||
}
|
||||
|
||||
function applyToDOM(v: DevValues) {
|
||||
const header = el("header");
|
||||
if (header) header.style.transform = `translateX(${v.headerTx}px)`;
|
||||
|
||||
const subtitle = el("subtitle");
|
||||
if (subtitle) subtitle.style.lineHeight = `${v.subtitleLh}`;
|
||||
|
||||
const download = el("download");
|
||||
if (download) {
|
||||
download.style.marginTop = `${v.downloadAbove}px`;
|
||||
download.style.marginBottom = `${v.downloadBelow}px`;
|
||||
}
|
||||
|
||||
const featuresUl = el("features-ul");
|
||||
if (featuresUl) featuresUl.style.lineHeight = `${v.featuresLh}`;
|
||||
|
||||
const featuresSpacer = el("features-spacer");
|
||||
if (featuresSpacer) featuresSpacer.style.height = `${v.featuresMb}px`;
|
||||
|
||||
const docsContent = el("docs-content");
|
||||
if (docsContent) docsContent.style.paddingTop = `${v.docsPt}px`;
|
||||
}
|
||||
|
||||
export function DevPanel() {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [pos, setPos] = useState({ x: 0, y: 0 });
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const vals = useDevValues();
|
||||
const dragOffset = useRef({ x: 0, y: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
setPos({ x: window.innerWidth - 340, y: window.innerHeight - 320 });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.metaKey && e.key === ".") {
|
||||
e.preventDefault();
|
||||
setVisible((v) => !v);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, []);
|
||||
|
||||
const update = useCallback((patch: Partial<DevValues>) => {
|
||||
setStore(patch);
|
||||
applyToDOM({ ...snapshot, ...patch });
|
||||
}, []);
|
||||
|
||||
const onPointerDown = useCallback((e: React.PointerEvent) => {
|
||||
if ((e.target as HTMLElement).closest("input, button, label")) return;
|
||||
setDragging(true);
|
||||
dragOffset.current = { x: e.clientX - pos.x, y: e.clientY - pos.y };
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
}, [pos]);
|
||||
|
||||
const onPointerMove = useCallback((e: React.PointerEvent) => {
|
||||
if (!dragging) return;
|
||||
setPos({ x: e.clientX - dragOffset.current.x, y: e.clientY - dragOffset.current.y });
|
||||
}, [dragging]);
|
||||
|
||||
const onPointerUp = useCallback(() => setDragging(false), []);
|
||||
|
||||
if (process.env.NODE_ENV !== "development" || !visible) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
style={{ left: pos.x, top: pos.y, cursor: dragging ? "grabbing" : "grab" }}
|
||||
className="fixed z-[9999] bg-[#222] text-white text-xs rounded-xl p-4 space-y-3 font-mono shadow-lg select-none"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-white/50">Dev Controls</span>
|
||||
<span className="text-white/20">⌘.</span>
|
||||
</div>
|
||||
|
||||
<Section label="Header">
|
||||
<Row label="tx" value={vals.headerTx} onChange={(v) => update({ headerTx: v })} min={-50} max={50} step={1} unit="px" />
|
||||
</Section>
|
||||
|
||||
<Section label="Cursor">
|
||||
<div className="flex items-center gap-3">
|
||||
<Row label="top" value={vals.cursorTop} onChange={(v) => update({ cursorTop: v })} min={-5} max={5} step={0.5} unit="px" />
|
||||
<label className="flex items-center gap-2 text-white/70 cursor-pointer">
|
||||
<input type="checkbox" checked={vals.cursorBlink} onChange={(e) => update({ cursorBlink: e.target.checked })} />
|
||||
blink
|
||||
</label>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section label="Subtitle">
|
||||
<Row label="line-h" value={vals.subtitleLh} onChange={(v) => update({ subtitleLh: v })} min={1} max={2.5} step={0.025} unit="" w={16} />
|
||||
</Section>
|
||||
|
||||
<Section label="Download buttons">
|
||||
<Row label="above" value={vals.downloadAbove} onChange={(v) => update({ downloadAbove: v })} />
|
||||
<Row label="below" value={vals.downloadBelow} onChange={(v) => update({ downloadBelow: v })} />
|
||||
</Section>
|
||||
|
||||
<Section label="Features">
|
||||
<Row label="line-h" value={vals.featuresLh} onChange={(v) => update({ featuresLh: v })} min={1} max={2.5} step={0.025} unit="" w={16} />
|
||||
<Row label="mb" value={vals.featuresMb} onChange={(v) => update({ featuresMb: v })} />
|
||||
</Section>
|
||||
|
||||
<Section label="Docs">
|
||||
<Row label="pt" value={vals.docsPt} onChange={(v) => update({ docsPt: v })} />
|
||||
</Section>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
const text = [
|
||||
`header-tx: ${vals.headerTx}px`,
|
||||
`cursor-top: ${vals.cursorTop}px`,
|
||||
`cursor-blink: ${vals.cursorBlink}`,
|
||||
`subtitle-lh: ${vals.subtitleLh}`,
|
||||
`download-above: ${vals.downloadAbove}px`,
|
||||
`download-below: ${vals.downloadBelow}px`,
|
||||
`features-lh: ${vals.featuresLh}`,
|
||||
`features-mb: ${vals.featuresMb}px`,
|
||||
`docs-pt: ${vals.docsPt}px`,
|
||||
].join(", ");
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
}}
|
||||
className="w-full py-1.5 rounded-lg bg-white/10 hover:bg-white/20 text-white/70 cursor-pointer transition-colors"
|
||||
>
|
||||
{copied ? "Copied!" : "Copy values"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-white/40 text-[10px] uppercase tracking-wider">{label}</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ label, value, onChange, min = 0, max = 128, step = 1, unit = "px", w = 10 }: {
|
||||
label: string; value: number; onChange: (v: number) => void;
|
||||
min?: number; max?: number; step?: number; unit?: string; w?: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-12 text-white/70">{label}</span>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={(e) => onChange(parseFloat(e.target.value))}
|
||||
className="w-28 accent-blue-500 cursor-pointer"
|
||||
/>
|
||||
<span className="text-right tabular-nums" style={{ width: `${w * 4}px` }}>
|
||||
{Number.isInteger(step) ? value : value.toFixed(step < 0.1 ? 2 : 1)}{unit}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
382
web/app/docs/api/page.tsx
Normal file
382
web/app/docs/api/page.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="mb-6">
|
||||
<h4>{name}</h4>
|
||||
<p>{desc}</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<CodeBlock title="CLI" lang="bash">{cli}</CodeBlock>
|
||||
<CodeBlock title="Socket" lang="json">{socket}</CodeBlock>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ApiPage() {
|
||||
return (
|
||||
<>
|
||||
<h1>API Reference</h1>
|
||||
<p>
|
||||
cmux provides both a CLI tool and a Unix socket for programmatic
|
||||
control. Every command is available through both interfaces.
|
||||
</p>
|
||||
|
||||
<h2>Socket</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Build</th>
|
||||
<th>Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Release</td>
|
||||
<td>
|
||||
<code>/tmp/cmux.sock</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Debug</td>
|
||||
<td>
|
||||
<code>/tmp/cmux-debug.sock</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
Override with the <code>CMUX_SOCKET_PATH</code> environment variable.
|
||||
Commands are newline-terminated JSON:
|
||||
</p>
|
||||
<CodeBlock lang="json">{`{"command": "command-name", "arg1": "value1"}
|
||||
// Response:
|
||||
{"success": true, "data": {...}}`}</CodeBlock>
|
||||
|
||||
<h2>Access modes</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mode</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Off</strong>
|
||||
</td>
|
||||
<td>Socket disabled</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Notifications only</strong>
|
||||
</td>
|
||||
<td>Only notification commands allowed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Full control</strong>
|
||||
</td>
|
||||
<td>All commands enabled</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<Callout type="warn">
|
||||
On shared machines, use “Notifications only” mode to prevent
|
||||
other users from controlling your terminals.
|
||||
</Callout>
|
||||
|
||||
<h2>CLI options</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Flag</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--socket PATH</code>
|
||||
</td>
|
||||
<td>Custom socket path</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--json</code>
|
||||
</td>
|
||||
<td>Output in JSON format</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--workspace ID</code>
|
||||
</td>
|
||||
<td>Target a specific workspace</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--surface ID</code>
|
||||
</td>
|
||||
<td>Target a specific surface</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Workspace commands</h2>
|
||||
|
||||
<Cmd
|
||||
name="list-workspaces"
|
||||
desc="List all open workspaces."
|
||||
cli={`cmux list-workspaces
|
||||
cmux list-workspaces --json`}
|
||||
socket={`{"command": "list-workspaces"}`}
|
||||
/>
|
||||
<Cmd
|
||||
name="new-workspace"
|
||||
desc="Create a new workspace."
|
||||
cli={`cmux new-workspace`}
|
||||
socket={`{"command": "new-workspace"}`}
|
||||
/>
|
||||
<Cmd
|
||||
name="select-workspace"
|
||||
desc="Switch to a specific workspace."
|
||||
cli={`cmux select-workspace --workspace <id>`}
|
||||
socket={`{"command": "select-workspace", "id": "<id>"}`}
|
||||
/>
|
||||
<Cmd
|
||||
name="current-workspace"
|
||||
desc="Get the currently active workspace."
|
||||
cli={`cmux current-workspace
|
||||
cmux current-workspace --json`}
|
||||
socket={`{"command": "current-workspace"}`}
|
||||
/>
|
||||
<Cmd
|
||||
name="close-workspace"
|
||||
desc="Close a workspace."
|
||||
cli={`cmux close-workspace --workspace <id>`}
|
||||
socket={`{"command": "close-workspace", "id": "<id>"}`}
|
||||
/>
|
||||
|
||||
<h2>Split commands</h2>
|
||||
|
||||
<Cmd
|
||||
name="new-split"
|
||||
desc="Create a new split pane. Directions: left, right, up, down."
|
||||
cli={`cmux new-split right
|
||||
cmux new-split down`}
|
||||
socket={`{"command": "new-split", "direction": "right"}`}
|
||||
/>
|
||||
<Cmd
|
||||
name="list-surfaces"
|
||||
desc="List all surfaces in the current workspace."
|
||||
cli={`cmux list-surfaces
|
||||
cmux list-surfaces --json`}
|
||||
socket={`{"command": "list-surfaces"}`}
|
||||
/>
|
||||
<Cmd
|
||||
name="focus-surface"
|
||||
desc="Focus a specific surface."
|
||||
cli={`cmux focus-surface --surface <id>`}
|
||||
socket={`{"command": "focus-surface", "id": "<id>"}`}
|
||||
/>
|
||||
|
||||
<h2>Input commands</h2>
|
||||
|
||||
<Cmd
|
||||
name="send"
|
||||
desc="Send text input to the focused terminal."
|
||||
cli={`cmux send "echo hello"
|
||||
cmux send "ls -la\\n"`}
|
||||
socket={`{"command": "send", "text": "echo hello\\n"}`}
|
||||
/>
|
||||
<Cmd
|
||||
name="send-key"
|
||||
desc="Send a key press. Keys: enter, tab, escape, backspace, delete, up, down, left, right."
|
||||
cli={`cmux send-key enter`}
|
||||
socket={`{"command": "send-key", "key": "enter"}`}
|
||||
/>
|
||||
<Cmd
|
||||
name="send-surface"
|
||||
desc="Send text to a specific surface."
|
||||
cli={`cmux send-surface --surface <id> "command"`}
|
||||
socket={`{"command": "send-surface", "id": "<id>", "text": "command"}`}
|
||||
/>
|
||||
<Cmd
|
||||
name="send-key-surface"
|
||||
desc="Send a key press to a specific surface."
|
||||
cli={`cmux send-key-surface --surface <id> enter`}
|
||||
socket={`{"command": "send-key-surface", "id": "<id>", "key": "enter"}`}
|
||||
/>
|
||||
|
||||
<h2>Notification commands</h2>
|
||||
|
||||
<Cmd
|
||||
name="notify"
|
||||
desc="Send a notification."
|
||||
cli={`cmux notify --title "Title" --body "Body"
|
||||
cmux notify --title "T" --subtitle "S" --body "B"`}
|
||||
socket={`{"command": "notify", "title": "Title",
|
||||
"subtitle": "S", "body": "Body"}`}
|
||||
/>
|
||||
<Cmd
|
||||
name="list-notifications"
|
||||
desc="List all notifications."
|
||||
cli={`cmux list-notifications
|
||||
cmux list-notifications --json`}
|
||||
socket={`{"command": "list-notifications"}`}
|
||||
/>
|
||||
<Cmd
|
||||
name="clear-notifications"
|
||||
desc="Clear all notifications."
|
||||
cli={`cmux clear-notifications`}
|
||||
socket={`{"command": "clear-notifications"}`}
|
||||
/>
|
||||
|
||||
<h2>Utility commands</h2>
|
||||
|
||||
<Cmd
|
||||
name="ping"
|
||||
desc="Check if cmux is running and responsive."
|
||||
cli={`cmux ping`}
|
||||
socket={`{"command": "ping"}
|
||||
// Response: {"success": true, "pong": true}`}
|
||||
/>
|
||||
|
||||
<h2>Environment variables</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>CMUX_SOCKET_PATH</code>
|
||||
</td>
|
||||
<td>Override the default socket path</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>CMUX_SOCKET_ENABLE</code>
|
||||
</td>
|
||||
<td>
|
||||
Enable/disable socket (<code>1</code>/<code>0</code>)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>CMUX_SOCKET_MODE</code>
|
||||
</td>
|
||||
<td>
|
||||
Override access mode (<code>full</code>,{" "}
|
||||
<code>notifications</code>, <code>off</code>)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>CMUX_WORKSPACE_ID</code>
|
||||
</td>
|
||||
<td>Auto-set: current workspace ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>CMUX_SURFACE_ID</code>
|
||||
</td>
|
||||
<td>Auto-set: current surface ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>TERM_PROGRAM</code>
|
||||
</td>
|
||||
<td>
|
||||
Set to <code>ghostty</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>TERM</code>
|
||||
</td>
|
||||
<td>
|
||||
Set to <code>xterm-ghostty</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<Callout>
|
||||
Environment variables override app settings. Use the socket check to
|
||||
distinguish cmux from regular Ghostty.
|
||||
</Callout>
|
||||
|
||||
<h2>Detecting cmux</h2>
|
||||
<CodeBlock title="bash" lang="bash">{`# 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"`}</CodeBlock>
|
||||
|
||||
<h2>Examples</h2>
|
||||
|
||||
<h3>Python client</h3>
|
||||
<CodeBlock title="python" lang="python">{`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!"
|
||||
})`}</CodeBlock>
|
||||
|
||||
<h3>Shell script</h3>
|
||||
<CodeBlock title="bash" lang="bash">{`#!/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"}'`}</CodeBlock>
|
||||
|
||||
<h3>Build script with notification</h3>
|
||||
<CodeBlock title="bash" lang="bash">{`#!/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`}</CodeBlock>
|
||||
</>
|
||||
);
|
||||
}
|
||||
127
web/app/docs/changelog/page.tsx
Normal file
127
web/app/docs/changelog/page.tsx
Normal file
|
|
@ -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("`") ? (
|
||||
<code key={i}>{part.slice(1, -1)}</code>
|
||||
) : (
|
||||
<span key={i}>{part}</span>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ChangelogPage() {
|
||||
const changelogPath = path.join(process.cwd(), "..", "CHANGELOG.md");
|
||||
const markdown = fs.readFileSync(changelogPath, "utf-8");
|
||||
const versions = parseChangelog(markdown);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Changelog</h1>
|
||||
<p>All notable changes to cmux are documented here.</p>
|
||||
|
||||
{versions.map((v) => (
|
||||
<div key={v.version} className="mb-8">
|
||||
<h2>
|
||||
{v.version}{" "}
|
||||
<span className="text-muted font-normal text-[14px]">
|
||||
— {v.date}
|
||||
</span>
|
||||
</h2>
|
||||
{v.intro && <p>{v.intro}</p>}
|
||||
{v.sections.map((section, i) => (
|
||||
<div key={i}>
|
||||
{section.heading && <h3>{section.heading}</h3>}
|
||||
<ul>
|
||||
{section.items.map((item, j) => (
|
||||
<li key={j}>
|
||||
<InlineCode text={item} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
212
web/app/docs/concepts/page.tsx
Normal file
212
web/app/docs/concepts/page.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<h1>Concepts</h1>
|
||||
<p>
|
||||
cmux organizes your terminals in a four-level hierarchy. Understanding
|
||||
these levels helps when using the socket API, CLI, and keyboard
|
||||
shortcuts.
|
||||
</p>
|
||||
|
||||
<h2>Hierarchy</h2>
|
||||
<CodeBlock lang="text">{`Window
|
||||
└── Workspace (sidebar entry)
|
||||
└── Pane (split region)
|
||||
└── Surface (tab within pane)
|
||||
└── Panel (terminal or browser content)`}</CodeBlock>
|
||||
|
||||
<h3>Window</h3>
|
||||
<p>
|
||||
A macOS window. Open multiple windows with <code>⌘⇧N</code>. Each
|
||||
window has its own sidebar with independent workspaces.
|
||||
</p>
|
||||
|
||||
<h3>Workspace</h3>
|
||||
<p>
|
||||
A sidebar entry. Each workspace contains one or more split panes.
|
||||
Workspaces are what you see listed in the left sidebar.
|
||||
</p>
|
||||
<p>
|
||||
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”.
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Context</th>
|
||||
<th>Term used</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Sidebar UI</td>
|
||||
<td>Tab</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Keyboard shortcuts</td>
|
||||
<td>Workspace or tab</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Socket API</td>
|
||||
<td>
|
||||
<code>workspace</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Environment variable</td>
|
||||
<td>
|
||||
<code>CMUX_WORKSPACE_ID</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
<strong>Shortcuts:</strong> <code>⌘N</code> (new),{" "}
|
||||
<code>⌘1</code>–<code>⌘9</code> (jump), <code>⌘⇧W</code> (close),{" "}
|
||||
<code>⌘⇧[</code> / <code>⌘⇧]</code> (prev/next)
|
||||
</p>
|
||||
|
||||
<h3>Pane</h3>
|
||||
<p>
|
||||
A split region within a workspace. Created by splitting with{" "}
|
||||
<code>⌘D</code> (right) or <code>⌘⇧D</code> (down). Navigate between
|
||||
panes with <code>⌥⌘</code> + arrow keys.
|
||||
</p>
|
||||
<p>Each pane can hold multiple surfaces (tabs within the pane).</p>
|
||||
|
||||
<h3>Surface</h3>
|
||||
<p>
|
||||
A tab within a pane. Each pane has its own tab bar and can hold multiple
|
||||
surfaces. Created with <code>⌘T</code>, navigated with{" "}
|
||||
<code>⌘[</code> / <code>⌘]</code> or <code>⌃1</code>–
|
||||
<code>⌃9</code>.
|
||||
</p>
|
||||
<p>
|
||||
Surfaces are the individual terminal or browser sessions you interact
|
||||
with. Each surface has its own <code>CMUX_SURFACE_ID</code> environment
|
||||
variable.
|
||||
</p>
|
||||
|
||||
<h3>Panel</h3>
|
||||
<p>The content inside a surface. Currently two types:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Terminal</strong> — a Ghostty terminal session
|
||||
</li>
|
||||
<li>
|
||||
<strong>Browser</strong> — an embedded web view
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Panel is mostly an internal concept. In the socket API and CLI, you
|
||||
interact with surfaces rather than panels directly.
|
||||
</p>
|
||||
|
||||
<h2>Visual example</h2>
|
||||
<CodeBlock variant="ascii">{`┌──────────────────────────────────────────────────────┐
|
||||
│ ┌──────────┐ ┌─────────────────────────────────────┐ │
|
||||
│ │ Sidebar │ │ Workspace "dev" │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ ┌───────────────┬─────────────────┐ │ │
|
||||
│ │ > dev │ │ │ Pane 1 │ Pane 2 │ │ │
|
||||
│ │ server │ │ │ [S1] [S2] │ [S1] │ │ │
|
||||
│ │ logs │ │ │ │ │ │ │
|
||||
│ │ │ │ │ Terminal │ Terminal │ │ │
|
||||
│ │ │ │ │ │ │ │ │
|
||||
│ │ │ │ └───────────────┴─────────────────┘ │ │
|
||||
│ └──────────┘ └─────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────┘`}</CodeBlock>
|
||||
<p>In this example:</p>
|
||||
<ul>
|
||||
<li>
|
||||
The <strong>window</strong> contains a sidebar with three workspaces
|
||||
(dev, server, logs)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Workspace “dev”</strong> is selected, showing two{" "}
|
||||
<strong>panes</strong> side by side
|
||||
</li>
|
||||
<li>
|
||||
<strong>Pane 1</strong> has two <strong>surfaces</strong> ([S1] and
|
||||
[S2] in the tab bar), with S1 active
|
||||
</li>
|
||||
<li>
|
||||
<strong>Pane 2</strong> has one surface
|
||||
</li>
|
||||
<li>
|
||||
Each surface contains a <strong>panel</strong> (a terminal in this
|
||||
case)
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Summary</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Level</th>
|
||||
<th>What it is</th>
|
||||
<th>Created by</th>
|
||||
<th>Identified by</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Window</td>
|
||||
<td>macOS window</td>
|
||||
<td>
|
||||
<code>⌘⇧N</code>
|
||||
</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Workspace</td>
|
||||
<td>Sidebar entry</td>
|
||||
<td>
|
||||
<code>⌘N</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>CMUX_WORKSPACE_ID</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pane</td>
|
||||
<td>Split region</td>
|
||||
<td>
|
||||
<code>⌘D</code> / <code>⌘⇧D</code>
|
||||
</td>
|
||||
<td>Pane ID (socket API)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Surface</td>
|
||||
<td>Tab within pane</td>
|
||||
<td>
|
||||
<code>⌘T</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>CMUX_SURFACE_ID</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Panel</td>
|
||||
<td>Terminal or browser</td>
|
||||
<td>Automatic</td>
|
||||
<td>Panel ID (internal)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
127
web/app/docs/configuration/page.tsx
Normal file
127
web/app/docs/configuration/page.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<h1>Configuration</h1>
|
||||
<p>
|
||||
cmux reads configuration from Ghostty config files, giving you familiar
|
||||
options if you're coming from Ghostty.
|
||||
</p>
|
||||
|
||||
<h2>Config file locations</h2>
|
||||
<p>cmux looks for configuration in these locations (in order):</p>
|
||||
<ol>
|
||||
<li>
|
||||
<code>~/.config/ghostty/config</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>~/Library/Application Support/com.mitchellh.ghostty/config</code>
|
||||
</li>
|
||||
</ol>
|
||||
<p>Create the config file if it doesn't exist:</p>
|
||||
<CodeBlock lang="bash">{`mkdir -p ~/.config/ghostty
|
||||
touch ~/.config/ghostty/config`}</CodeBlock>
|
||||
|
||||
<h2>Appearance</h2>
|
||||
|
||||
<h3>Font</h3>
|
||||
<CodeBlock title="~/.config/ghostty/config" lang="ini">{`font-family = JetBrains Mono
|
||||
font-size = 14`}</CodeBlock>
|
||||
|
||||
<h3>Colors</h3>
|
||||
<CodeBlock title="~/.config/ghostty/config" lang="ini">{`# 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`}</CodeBlock>
|
||||
|
||||
<h3>Split panes</h3>
|
||||
<CodeBlock title="~/.config/ghostty/config" lang="ini">{`# 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`}</CodeBlock>
|
||||
|
||||
<h2>Behavior</h2>
|
||||
|
||||
<h3>Scrollback</h3>
|
||||
<CodeBlock title="~/.config/ghostty/config" lang="ini">{`# Number of lines to keep in scrollback buffer
|
||||
scrollback-limit = 10000`}</CodeBlock>
|
||||
|
||||
<h3>Working directory</h3>
|
||||
<CodeBlock title="~/.config/ghostty/config" lang="ini">{`# Default directory for new terminals
|
||||
working-directory = ~/Projects`}</CodeBlock>
|
||||
|
||||
<h2>App settings</h2>
|
||||
<p>
|
||||
In-app settings are available via <strong>cmux → Settings</strong> (
|
||||
<code>⌘,</code>):
|
||||
</p>
|
||||
|
||||
<h3>Theme mode</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>System</strong> — follow macOS appearance
|
||||
</li>
|
||||
<li>
|
||||
<strong>Light</strong> — always light mode
|
||||
</li>
|
||||
<li>
|
||||
<strong>Dark</strong> — always dark mode
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Automation mode</h3>
|
||||
<p>Control socket access level:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Off</strong> — no socket control (most secure)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Notifications only</strong> — only allow notification commands
|
||||
</li>
|
||||
<li>
|
||||
<strong>Full control</strong> — allow all socket commands
|
||||
</li>
|
||||
</ul>
|
||||
<Callout type="warn">
|
||||
On shared machines, consider using “Notifications only” mode
|
||||
to prevent other processes from controlling your terminals.
|
||||
</Callout>
|
||||
|
||||
<h2>Example config</h2>
|
||||
<CodeBlock title="~/.config/ghostty/config" lang="ini">{`# 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`}</CodeBlock>
|
||||
</>
|
||||
);
|
||||
}
|
||||
137
web/app/docs/docs-nav.tsx
Normal file
137
web/app/docs/docs-nav.tsx
Normal file
|
|
@ -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<HTMLElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(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<HTMLElement>(
|
||||
'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 (
|
||||
<div className="max-w-5xl mx-auto flex px-4">
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => setOpen(!open)}
|
||||
aria-expanded={open}
|
||||
aria-controls="docs-sidebar"
|
||||
className="fixed bottom-4 right-4 z-40 md:hidden w-10 h-10 rounded-full bg-foreground text-background flex items-center justify-center shadow-lg"
|
||||
aria-label={open ? "Close navigation" : "Open navigation"}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{open ? (
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
) : (
|
||||
<>
|
||||
<path d="M3 6h18" />
|
||||
<path d="M3 12h18" />
|
||||
<path d="M3 18h18" />
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Mobile overlay */}
|
||||
{open && (
|
||||
<div
|
||||
className="fixed inset-0 z-30 bg-black/50 md:hidden"
|
||||
aria-hidden="true"
|
||||
onClick={close}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
ref={sidebarRef}
|
||||
id="docs-sidebar"
|
||||
role="navigation"
|
||||
aria-label="Documentation"
|
||||
style={{ height: "calc(100dvh - 3rem)" }}
|
||||
className={`fixed top-12 left-0 z-40 w-56 bg-background py-4 pr-4 overflow-y-auto transition-transform md:sticky md:top-12 md:shrink-0 md:translate-x-0 ${
|
||||
open ? "translate-x-0" : "-translate-x-full"
|
||||
}`}
|
||||
>
|
||||
<DocsSidebar onNavigate={close} />
|
||||
</aside>
|
||||
|
||||
{/* Content */}
|
||||
<main className="flex-1 min-w-0">
|
||||
<div className="max-w-2xl px-6 pb-10 ml-0" data-dev="docs-content" style={{ paddingTop: 8 }}>
|
||||
<div className="docs-content text-[15px]">{children}</div>
|
||||
<DocsPager />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
77
web/app/docs/getting-started/page.tsx
Normal file
77
web/app/docs/getting-started/page.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<h1>Getting Started</h1>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>Install</h2>
|
||||
|
||||
<h3>DMG (recommended)</h3>
|
||||
<div className="my-4">
|
||||
<DownloadButton />
|
||||
</div>
|
||||
<p>
|
||||
Open the <code>.dmg</code> and drag cmux to your Applications folder.
|
||||
cmux auto-updates via Sparkle, so you only need to download once.
|
||||
</p>
|
||||
|
||||
<h3>Homebrew</h3>
|
||||
<CodeBlock lang="bash">{`brew tap manaflow-ai/cmux
|
||||
brew install --cask cmux`}</CodeBlock>
|
||||
<p>To update later:</p>
|
||||
<CodeBlock lang="bash">{`brew upgrade --cask cmux`}</CodeBlock>
|
||||
|
||||
<Callout>
|
||||
On first launch, macOS may ask you to confirm opening an app from an
|
||||
identified developer. Click <strong>Open</strong> to proceed.
|
||||
</Callout>
|
||||
|
||||
<h2>Verify installation</h2>
|
||||
<p>Open cmux and you should see:</p>
|
||||
<ul>
|
||||
<li>A terminal window with a vertical tab sidebar on the left</li>
|
||||
<li>One initial workspace already open</li>
|
||||
<li>The Ghostty-powered terminal ready for input</li>
|
||||
</ul>
|
||||
|
||||
<h2>CLI setup</h2>
|
||||
<p>
|
||||
cmux includes a command-line tool for automation. Inside cmux terminals
|
||||
it works automatically. To use the CLI from outside cmux, create a
|
||||
symlink:
|
||||
</p>
|
||||
<CodeBlock lang="bash">{`sudo ln -sf "/Applications/cmux.app/Contents/MacOS/cmux" /usr/local/bin/cmux`}</CodeBlock>
|
||||
<p>Then you can run commands like:</p>
|
||||
<CodeBlock lang="bash">{`cmux list-workspaces
|
||||
cmux notify --title "Build Complete" --body "Your build finished"`}</CodeBlock>
|
||||
|
||||
<h2>Auto-updates</h2>
|
||||
<p>
|
||||
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 <strong>cmux → Check for Updates</strong> in the menu
|
||||
bar.
|
||||
</p>
|
||||
|
||||
<h2>Requirements</h2>
|
||||
<ul>
|
||||
<li>macOS 14.0 or later</li>
|
||||
<li>Apple Silicon or Intel Mac</li>
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
19
web/app/docs/keyboard-shortcuts/page.tsx
Normal file
19
web/app/docs/keyboard-shortcuts/page.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<h1>Keyboard Shortcuts</h1>
|
||||
<p>
|
||||
All keyboard shortcuts available in cmux, grouped by category.
|
||||
</p>
|
||||
<KeyboardShortcuts />
|
||||
</>
|
||||
);
|
||||
}
|
||||
30
web/app/docs/layout.tsx
Normal file
30
web/app/docs/layout.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="min-h-screen">
|
||||
<SiteHeader section="docs" />
|
||||
<DocsNav>{children}</DocsNav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
202
web/app/docs/notifications/page.tsx
Normal file
202
web/app/docs/notifications/page.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<h1>Notifications</h1>
|
||||
<p>
|
||||
cmux supports desktop notifications, allowing AI agents and scripts to
|
||||
alert you when they need attention.
|
||||
</p>
|
||||
|
||||
<h2>Lifecycle</h2>
|
||||
<ol>
|
||||
<li>
|
||||
<strong>Received</strong> — notification appears in panel, desktop
|
||||
alert fires (if not suppressed)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Unread</strong> — badge shown on workspace tab
|
||||
</li>
|
||||
<li>
|
||||
<strong>Read</strong> — cleared when you view that workspace
|
||||
</li>
|
||||
<li>
|
||||
<strong>Cleared</strong> — removed from panel
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h3>Suppression</h3>
|
||||
<p>Desktop alerts are suppressed when:</p>
|
||||
<ul>
|
||||
<li>The cmux window is focused</li>
|
||||
<li>The specific workspace sending the notification is active</li>
|
||||
<li>The notification panel is open</li>
|
||||
</ul>
|
||||
|
||||
<h3>Notification panel</h3>
|
||||
<p>
|
||||
Press <code>⌘⇧I</code> to open the notification panel. Click a
|
||||
notification to jump to that workspace. Press <code>⌘⇧U</code> to jump
|
||||
directly to the workspace with the most recent unread notification.
|
||||
</p>
|
||||
|
||||
<h2>Sending notifications</h2>
|
||||
|
||||
<h3>CLI</h3>
|
||||
<CodeBlock lang="bash">{`cmux notify --title "Task Complete" --body "Your build finished"
|
||||
cmux notify --title "Claude Code" --subtitle "Waiting" --body "Agent needs input"`}</CodeBlock>
|
||||
|
||||
<h3>OSC 777 (simple)</h3>
|
||||
<p>
|
||||
The RXVT protocol uses a fixed format with title and body:
|
||||
</p>
|
||||
<CodeBlock lang="bash">{`printf '\\e]777;notify;My Title;Message body here\\a'`}</CodeBlock>
|
||||
<CodeBlock title="Shell function" lang="bash">{`notify_osc777() {
|
||||
local title="$1"
|
||||
local body="$2"
|
||||
printf '\\e]777;notify;%s;%s\\a' "$title" "$body"
|
||||
}
|
||||
|
||||
notify_osc777 "Build Complete" "All tests passed"`}</CodeBlock>
|
||||
|
||||
<h3>OSC 99 (rich)</h3>
|
||||
<p>
|
||||
The Kitty protocol supports subtitles and notification IDs:
|
||||
</p>
|
||||
<CodeBlock lang="bash">{`# Format: ESC ] 99 ; <params> ; <payload> 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\\\\'`}</CodeBlock>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>OSC 99</th>
|
||||
<th>OSC 777</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Title + body</td>
|
||||
<td>Yes</td>
|
||||
<td>Yes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subtitle</td>
|
||||
<td>Yes</td>
|
||||
<td>No</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Notification ID</td>
|
||||
<td>Yes</td>
|
||||
<td>No</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Complexity</td>
|
||||
<td>Higher</td>
|
||||
<td>Lower</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Callout>
|
||||
Use OSC 777 for simple notifications. Use OSC 99 when you need subtitles
|
||||
or notification IDs. Use the CLI (<code>cmux notify</code>) for the
|
||||
easiest integration.
|
||||
</Callout>
|
||||
|
||||
<h2>Claude Code hooks</h2>
|
||||
<p>
|
||||
cmux integrates with{" "}
|
||||
<a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a>{" "}
|
||||
via hooks to notify you when tasks complete.
|
||||
</p>
|
||||
|
||||
<h3>1. Create the hook script</h3>
|
||||
<CodeBlock title="~/.claude/hooks/cmux-notify.sh" lang="bash">{`#!/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`}</CodeBlock>
|
||||
<CodeBlock lang="bash">{`chmod +x ~/.claude/hooks/cmux-notify.sh`}</CodeBlock>
|
||||
|
||||
<h3>2. Configure Claude Code</h3>
|
||||
<CodeBlock title="~/.claude/settings.json" lang="json">{`{
|
||||
"hooks": {
|
||||
"Stop": ["~/.claude/hooks/cmux-notify.sh"],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Task",
|
||||
"hooks": ["~/.claude/hooks/cmux-notify.sh"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}`}</CodeBlock>
|
||||
<p>Restart Claude Code to apply the hooks.</p>
|
||||
|
||||
<h2>Integration examples</h2>
|
||||
|
||||
<h3>Notify after long command</h3>
|
||||
<CodeBlock title="~/.zshrc" lang="bash">{`# 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`}</CodeBlock>
|
||||
|
||||
<h3>Python</h3>
|
||||
<CodeBlock title="python" lang="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")`}</CodeBlock>
|
||||
|
||||
<h3>Node.js</h3>
|
||||
<CodeBlock title="node" lang="javascript">{`function notify(title, body) {
|
||||
process.stdout.write(\`\\x1b]777;notify;\${title};\${body}\\x07\`);
|
||||
}
|
||||
|
||||
notify('Build Done', 'webpack finished');`}</CodeBlock>
|
||||
|
||||
<h3>tmux passthrough</h3>
|
||||
<p>If using tmux inside cmux, enable passthrough:</p>
|
||||
<CodeBlock title=".tmux.conf" lang="bash">{`set -g allow-passthrough on`}</CodeBlock>
|
||||
<CodeBlock lang="bash">{`printf '\\ePtmux;\\e\\e]777;notify;Title;Body\\a\\e\\\\'`}</CodeBlock>
|
||||
</>
|
||||
);
|
||||
}
|
||||
5
web/app/docs/page.tsx
Normal file
5
web/app/docs/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function DocsPage() {
|
||||
redirect("/docs/getting-started");
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<section className="mb-12">
|
||||
<h2 className="text-xs font-medium text-muted tracking-tight mb-6">
|
||||
Keyboard Shortcuts
|
||||
</h2>
|
||||
<div className="space-y-8 text-[15px]">
|
||||
{/* Workspaces */}
|
||||
<div>
|
||||
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Workspaces</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ N</span>
|
||||
<span className="text-muted">New workspace</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ 1 – 8</span>
|
||||
<span className="text-muted">Jump to workspace 1–8</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ 9</span>
|
||||
<span className="text-muted">Jump to last workspace</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ ⇧ W</span>
|
||||
<span className="text-muted">Close workspace</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Surfaces */}
|
||||
<div>
|
||||
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Surfaces</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ T</span>
|
||||
<span className="text-muted">New surface</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ ⇧ [</span>
|
||||
<span className="text-muted">Previous surface</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌃ ⇧ Tab</span>
|
||||
<span className="text-muted">Previous surface</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌃ 1 – 8</span>
|
||||
<span className="text-muted">Jump to surface 1–8</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌃ 9</span>
|
||||
<span className="text-muted">Jump to last surface</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ W</span>
|
||||
<span className="text-muted">Close surface</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Split Panes */}
|
||||
<div>
|
||||
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Split Panes</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ D</span>
|
||||
<span className="text-muted">Split right</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ ⇧ D</span>
|
||||
<span className="text-muted">Split down</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌥ ⌘ ← → ↑ ↓</span>
|
||||
<span className="text-muted">Focus pane directionally</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Browser */}
|
||||
<div>
|
||||
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Browser</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ ⇧ B</span>
|
||||
<span className="text-muted">Open browser in split</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ L</span>
|
||||
<span className="text-muted">Focus address bar</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ ]</span>
|
||||
<span className="text-muted">Forward</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ R</span>
|
||||
<span className="text-muted">Reload page</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌥ ⌘ I</span>
|
||||
<span className="text-muted">Open Developer Tools</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
<div>
|
||||
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Notifications</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ ⇧ I</span>
|
||||
<span className="text-muted">Show notifications panel</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ ⇧ U</span>
|
||||
<span className="text-muted">Jump to latest unread</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Find */}
|
||||
<div>
|
||||
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Find</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ F</span>
|
||||
<span className="text-muted">Find</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ G / ⌘ ⇧ G</span>
|
||||
<span className="text-muted">Find next / previous</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ ⇧ F</span>
|
||||
<span className="text-muted">Hide find bar</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ E</span>
|
||||
<span className="text-muted">Use selection for find</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Terminal */}
|
||||
<div>
|
||||
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Terminal</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ K</span>
|
||||
<span className="text-muted">Clear scrollback</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ C</span>
|
||||
<span className="text-muted">Copy (with selection)</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ V</span>
|
||||
<span className="text-muted">Paste</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ + / ⌘ -</span>
|
||||
<span className="text-muted">Increase / decrease font size</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ 0</span>
|
||||
<span className="text-muted">Reset font size</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Window */}
|
||||
<div>
|
||||
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Window</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ ⇧ N</span>
|
||||
<span className="text-muted">New window</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ ,</span>
|
||||
<span className="text-muted">Settings</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ ⇧ R</span>
|
||||
<span className="text-muted">Reload configuration</span>
|
||||
</li>
|
||||
<li className="flex items-baseline justify-between">
|
||||
<span className="font-mono text-[13px]">⌘ Q</span>
|
||||
<span className="text-muted">Quit</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<span className="inline-flex items-center">
|
||||
{combo.map((k, idx) => (
|
||||
<span key={`${k}-${idx}`} className="inline-flex items-center">
|
||||
<kbd>{k}</kbd>
|
||||
{idx < combo.length - 1 && (
|
||||
<span className="text-muted/30 text-[10px] mx-[3px] select-none font-mono">
|
||||
+
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ShortcutRow({ shortcut }: { shortcut: Shortcut }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 py-[11px] px-4 hover:bg-foreground/[0.025] transition-colors">
|
||||
<div className="min-w-0">
|
||||
<span className="text-[14px] text-foreground/90">
|
||||
{shortcut.description}
|
||||
</span>
|
||||
{shortcut.note && (
|
||||
<span className="text-[12px] text-muted/50 ml-2">
|
||||
{shortcut.note}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
{shortcut.combos.map((combo, idx) => (
|
||||
<span
|
||||
key={`${shortcut.id}-combo-${idx}`}
|
||||
className="inline-flex items-center"
|
||||
>
|
||||
{idx > 0 && (
|
||||
<span className="text-muted/30 text-[11px] select-none mr-3 font-mono">
|
||||
/
|
||||
</span>
|
||||
)}
|
||||
<KeyCombo combo={combo} />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="mt-2 mb-12">
|
||||
{/* Search */}
|
||||
<div className="relative mb-8">
|
||||
<div className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-muted/40">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="M21 21l-4.3-4.3" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category jump links */}
|
||||
{!query && (
|
||||
<nav className="flex flex-wrap items-center gap-y-2 mb-10">
|
||||
{CATEGORIES.map((cat, idx) => (
|
||||
<span key={cat.id} className="inline-flex items-center">
|
||||
<a
|
||||
href={`#${cat.id}`}
|
||||
className="text-[13px] text-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
{cat.title}
|
||||
</a>
|
||||
{idx < CATEGORIES.length - 1 && (
|
||||
<span className="text-border mx-2.5 text-[10px] select-none">
|
||||
·
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
{filtered.length === 0 ? (
|
||||
<div className="py-16 text-center">
|
||||
<p className="text-[14px] text-muted/70">No shortcuts found</p>
|
||||
<p className="text-[13px] text-muted/40 mt-1.5">
|
||||
Try a different search term
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-10">
|
||||
{filtered.map((cat) => (
|
||||
<section key={cat.id} id={cat.id} className="scroll-mt-20">
|
||||
<div className="mb-3">
|
||||
<div className="text-[13px] font-medium text-muted/60">
|
||||
{cat.title}
|
||||
</div>
|
||||
{cat.blurb && (
|
||||
<p className="text-[13px] text-muted/50 mt-1">{cat.blurb}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-xl border border-border overflow-hidden">
|
||||
<div className="divide-y divide-border/60">
|
||||
{cat.shortcuts.map((s) => (
|
||||
<ShortcutRow key={s.id} shortcut={s} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}
|
||||
>
|
||||
<Providers>{children}</Providers>
|
||||
<Providers>
|
||||
<div className="fixed top-2 right-4 z-50">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
{children}
|
||||
<SiteFooter />
|
||||
<DevPanel />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex min-h-screen justify-center px-6 py-20 sm:py-32">
|
||||
{/* Theme toggle */}
|
||||
<div className="fixed top-5 right-5">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<div className="min-h-screen">
|
||||
<SiteHeader hideLogo />
|
||||
|
||||
<main className="w-full max-w-xl">
|
||||
<main className="w-full max-w-2xl mx-auto px-6 py-16 sm:py-24">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4 mb-10">
|
||||
<div className="flex items-center gap-4 mb-10" data-dev="header">
|
||||
<Image
|
||||
src="/icon.png"
|
||||
alt="cmux icon"
|
||||
|
|
@ -27,25 +25,19 @@ export default function Home() {
|
|||
|
||||
{/* Tagline */}
|
||||
<p className="text-lg leading-relaxed mb-3 text-foreground">
|
||||
A terminal built for <TypingTagline />
|
||||
The terminal built for <TypingTagline />
|
||||
</p>
|
||||
<p className="text-base leading-relaxed text-muted mb-12">
|
||||
Native macOS app built on Ghostty. Vertical tabs, notification rings
|
||||
when agents need attention, split panes, and a socket API for
|
||||
automation.
|
||||
<p className="text-base text-muted" data-dev="subtitle" style={{ lineHeight: 1.5 }}>
|
||||
<Balancer>
|
||||
Native macOS app built on Ghostty. Vertical tabs, notification rings
|
||||
when agents need attention, split panes, and a socket API for
|
||||
automation.
|
||||
</Balancer>
|
||||
</p>
|
||||
|
||||
{/* Download */}
|
||||
<div className="mb-12 flex items-center gap-3">
|
||||
<a
|
||||
href="https://github.com/manaflow-ai/cmux/releases/latest/download/cmux-macos.dmg"
|
||||
className="inline-flex items-center gap-2.5 rounded-full px-5 py-2.5 text-[15px] font-medium bg-foreground text-background hover:opacity-85 transition-opacity"
|
||||
>
|
||||
<svg width="16" height="19" viewBox="0 0 814 1000" fill="currentColor">
|
||||
<path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57.8-155.5-127.4c-58.3-81.6-105.6-208.4-105.6-328.6 0-193 125.6-295.5 249.2-295.5 65.7 0 120.5 43.1 161.7 43.1 39.2 0 100.4-45.8 175.1-45.8 28.3 0 130.3 2.6 197.2 99.2zM554.1 159.4c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.9 32.4-57.2 83.6-57.2 135.4 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 137.6-71.2z" />
|
||||
</svg>
|
||||
Download for Mac
|
||||
</a>
|
||||
<div className="flex items-center gap-3" data-dev="download" style={{ marginTop: 21, marginBottom: 33 }}>
|
||||
<DownloadButton />
|
||||
<a
|
||||
href="https://github.com/manaflow-ai/cmux"
|
||||
target="_blank"
|
||||
|
|
@ -60,11 +52,11 @@ export default function Home() {
|
|||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<section className="mb-12">
|
||||
<section data-dev="features">
|
||||
<h2 className="text-xs font-medium text-muted tracking-tight mb-3">
|
||||
Features
|
||||
</h2>
|
||||
<ul className="space-y-3 text-[15px] leading-relaxed">
|
||||
<ul className="space-y-3 text-[15px]" data-dev="features-ul" style={{ lineHeight: 1.275 }}>
|
||||
<li className="flex gap-3">
|
||||
<span className="text-muted shrink-0">-</span>
|
||||
<span>
|
||||
|
|
@ -119,25 +111,21 @@ export default function Home() {
|
|||
</span>
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="text-muted shrink-0">-</span>
|
||||
<span>
|
||||
<strong className="font-medium">Keyboard shortcuts</strong>
|
||||
<span className="text-muted">
|
||||
: <a href="/docs/keyboard-shortcuts" className="underline underline-offset-2 decoration-border hover:decoration-foreground transition-colors">extensive shortcuts</a> for workspaces, splits, browser, and more
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div data-dev="features-spacer" style={{ height: 23 }} />
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="flex items-center gap-4 text-sm text-muted pt-4 border-t border-border">
|
||||
<a
|
||||
href="https://github.com/manaflow-ai/cmux"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://cmux.term.sh"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
8
web/app/robots.ts
Normal file
8
web/app/robots.ts
Normal file
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
16
web/app/sitemap.ts
Normal file
16
web/app/sitemap.ts
Normal file
|
|
@ -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 },
|
||||
];
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<span>
|
||||
{displayed}
|
||||
<span
|
||||
className={`inline-block w-[2px] h-[1.1em] bg-foreground/70 ml-[1px] ${blink ? "animate-blink" : ""}`}
|
||||
style={{ position: "relative", top: "2.5px" }}
|
||||
onDoubleClick={() => 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 && (
|
||||
<span className="fixed bottom-5 right-5 z-50 flex w-[420px] items-center gap-3 rounded-xl bg-[#222] px-4 py-3 font-mono text-xs text-white shadow-lg">
|
||||
<label className="flex items-center gap-2">
|
||||
top:
|
||||
<input
|
||||
type="range"
|
||||
min={-5}
|
||||
max={5}
|
||||
step={0.5}
|
||||
value={topOffset}
|
||||
onChange={(e) => setTopOffset(parseFloat(e.target.value))}
|
||||
className="w-24"
|
||||
/>
|
||||
<span className="w-12">{topOffset}px</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={blink}
|
||||
onChange={(e) => setBlink(e.target.checked)}
|
||||
/>
|
||||
blink
|
||||
</label>
|
||||
<code className="select-all cursor-pointer rounded bg-[#333] px-2 py-0.5">
|
||||
{tailwindClass || "0px"}
|
||||
</code>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
92
web/bun.lock
92
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=="],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue