From d3bb0b3b2632457693851477b88a9802e37cfb9e Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 20 Apr 2026 17:08:27 -0700 Subject: [PATCH 01/18] draft text --- homepage.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 homepage.md diff --git a/homepage.md b/homepage.md new file mode 100644 index 0000000..a5573a0 --- /dev/null +++ b/homepage.md @@ -0,0 +1,17 @@ +# Stop watching terminals spin + +MouseTerm tracks activity the same way you do — visual motion. When a pane stops producing output for two seconds, MouseTerm marks it as complete and alerts you. + +Works with any CLI tool that prints to a terminal. No fragile plugins, no cumbersome configuration. Complete tasks in parallel without fragmenting your attention. + +# Soft as a mouse, sharp as tmux + +Run builds, agents, servers, and scripts side by side. Minimize the ones you're not watching to a compact status indicator. Every pane and alarm keeps running whether you can see it or not. + +Do it all with the mouse, or keep your hands on the keyboard and use the same keybinds as tmux. + +# Copy paste like you meant + +Did you know that when you click and drag in a "conformant" terminal, it actually sends escape code alpha niner beta? And Ctrl+C doesn't copy, it asks your program to kill itself? + +MouseTerm let's you copy paste like the well-adjusted normie you aspire to be. Automatically rewrap text so you can paste what you meant. \ No newline at end of file From 286ce86934bdac12ac84e5b8a0643aaf400f660f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 20 Apr 2026 17:34:21 -0700 Subject: [PATCH 02/18] Progress. --- homepage.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homepage.md b/homepage.md index a5573a0..f89e77b 100644 --- a/homepage.md +++ b/homepage.md @@ -1,17 +1,17 @@ # Stop watching terminals spin -MouseTerm tracks activity the same way you do — visual motion. When a pane stops producing output for two seconds, MouseTerm marks it as complete and alerts you. +MouseTerm tracks activity the same way you do — visual motion. When a pane stops changing for two seconds, MouseTerm marks it as complete and alerts you. Works with any CLI tool that prints to a terminal. No fragile plugins, no cumbersome configuration. Complete tasks in parallel without fragmenting your attention. -# Soft as a mouse, sharp as tmux +# Copy paste like you meant -Run builds, agents, servers, and scripts side by side. Minimize the ones you're not watching to a compact status indicator. Every pane and alarm keeps running whether you can see it or not. +Did you know that when you click and drag in a "mouse conformant" terminal, it does not select text but instead sends escape code `\e[<0;x;yM`? And `Ctrl+C` doesn't copy, it asks your program to kill itself? -Do it all with the mouse, or keep your hands on the keyboard and use the same keybinds as tmux. +And don't get us started on all the line-breaks - we make it easy to automatically rewrap text so you can paste what you meant. MouseTerm lets you copy paste like a human, not a terminal. -# Copy paste like you meant +# Soft as a mouse, sharp as tmux -Did you know that when you click and drag in a "conformant" terminal, it actually sends escape code alpha niner beta? And Ctrl+C doesn't copy, it asks your program to kill itself? +Run builds, agents, servers, and scripts side by side. Minimize the ones you're not watching to a compact status indicator. Every pane keeps running and every alert still fires whether you can see it or not. -MouseTerm let's you copy paste like the well-adjusted normie you aspire to be. Automatically rewrap text so you can paste what you meant. \ No newline at end of file +Do it all with the mouse, or keep your hands on the keyboard and use the same keybinds as tmux. From 6bba0db4a7b1441844363682b6a10add946c6ee5 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 20 Apr 2026 18:18:19 -0700 Subject: [PATCH 03/18] Improve marketing copy. --- website/src/pages/Home.tsx | 50 ++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx index e44aaae..103cf33 100644 --- a/website/src/pages/Home.tsx +++ b/website/src/pages/Home.tsx @@ -213,36 +213,56 @@ function Home() {

Stop watching terminals spin

- Run builds, agents, servers, and scripts side by side. MouseTerm - watches them all and tells you which ones finished — so you don't - have to. + MouseTerm tracks activity the same way you do — visual motion. When a + pane stops changing for two seconds, it marks the task complete and + alerts you.

- Split with a click. Resize with a drag. Minimize the ones you're not - watching to a compact status indicator. Every pane keeps running whether - you can see it or not. + Works with any CLI tool that prints to a terminal — no plugins, no + configuration.

-

TODO: Split, resize, and minimize panes

+

TODO: Completion detection in action

- {/* Section 2: text left, video right */} + {/* Section 2: image left, text right */} +
+
+

TODO: Copy/paste with line-break rewrap

+
+
+

Copy paste like you meant

+

+ Click and drag in a "mouse conformant" terminal doesn't select text; + it sends escape code{" "} + {"\\e[<0;x;yM"}. + And Ctrl+C{" "} + doesn't copy; it asks your program to kill itself. +

+

+ MouseTerm lets you copy paste like a human, not a terminal. +

+
+
+ + {/* Section 3: text left, image right */}
-

You'll know when it's done.

+

Soft as a mouse, sharp as tmux

- MouseTerm tracks activity the same way you do — visual motion. When a - pane stops producing output for two seconds, MouseTerm marks it as - complete. + Run builds, agents, servers, and scripts side by side. Minimize the + ones you're not watching to a compact status indicator. Every pane + keeps running and every alert still fires whether you can see it or + not.

- Works with any CLI tool that prints to a terminal. No fragile plugins, - no configuration, no per-tool setup. + Do it all with the mouse, or keep your hands on the keyboard with + tmux keybinds.

-

TODO: Activity detection and completion marking

+

TODO: Tiling layout and tmux keybinds

From bd0c0ae3f6c75461085fff3d0d731aa2a35a6168 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 21 Apr 2026 09:18:42 -0700 Subject: [PATCH 04/18] Remove the homepage copy draft. --- homepage.md | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 homepage.md diff --git a/homepage.md b/homepage.md deleted file mode 100644 index f89e77b..0000000 --- a/homepage.md +++ /dev/null @@ -1,17 +0,0 @@ -# Stop watching terminals spin - -MouseTerm tracks activity the same way you do — visual motion. When a pane stops changing for two seconds, MouseTerm marks it as complete and alerts you. - -Works with any CLI tool that prints to a terminal. No fragile plugins, no cumbersome configuration. Complete tasks in parallel without fragmenting your attention. - -# Copy paste like you meant - -Did you know that when you click and drag in a "mouse conformant" terminal, it does not select text but instead sends escape code `\e[<0;x;yM`? And `Ctrl+C` doesn't copy, it asks your program to kill itself? - -And don't get us started on all the line-breaks - we make it easy to automatically rewrap text so you can paste what you meant. MouseTerm lets you copy paste like a human, not a terminal. - -# Soft as a mouse, sharp as tmux - -Run builds, agents, servers, and scripts side by side. Minimize the ones you're not watching to a compact status indicator. Every pane keeps running and every alert still fires whether you can see it or not. - -Do it all with the mouse, or keep your hands on the keyboard and use the same keybinds as tmux. From 91982baacfa0d4cf14722d72617d3e8c12a053df Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 21 Apr 2026 09:19:37 -0700 Subject: [PATCH 05/18] Update fonts and layout. --- website/index.html | 2 +- website/src/components/SiteHeader.tsx | 8 ++--- website/src/index.css | 4 ++- website/src/pages/Dependencies.tsx | 8 ++--- website/src/pages/Home.tsx | 44 +++++++++++++-------------- 5 files changed, 34 insertions(+), 32 deletions(-) diff --git a/website/index.html b/website/index.html index e405260..636b250 100644 --- a/website/index.html +++ b/website/index.html @@ -7,7 +7,7 @@ - +
diff --git a/website/src/components/SiteHeader.tsx b/website/src/components/SiteHeader.tsx index 5ec8649..8cfb295 100644 --- a/website/src/components/SiteHeader.tsx +++ b/website/src/components/SiteHeader.tsx @@ -52,7 +52,7 @@ const SiteHeader = forwardRef( return ( <>
(
( href="/" className={ brandVisible - ? `text-xl font-semibold tracking-tight transition-opacity ${ + ? `text-xl transition-opacity ${ themeAware ? "opacity-80 hover:opacity-100" : "opacity-50 hover:opacity-100 text-[var(--color-caramel)]" }` - : `text-xl font-semibold tracking-tight ${ + : `text-xl ${ themeAware ? "" : "text-[var(--color-caramel)]" }` } diff --git a/website/src/index.css b/website/src/index.css index 214ae9c..f14411d 100644 --- a/website/src/index.css +++ b/website/src/index.css @@ -1,7 +1,8 @@ @import "tailwindcss"; @theme { - --font-display: "Instrument Sans", ui-sans-serif, system-ui, sans-serif; + --font-display: "Ubuntu Sans Mono", ui-monospace, monospace; + --font-sans: "Ubuntu Mono", ui-monospace, monospace; --color-bg: oklch(10% 0.01 60); --color-surface: oklch(18% 0.015 60); --color-text: oklch(92% 0.01 60); @@ -12,6 +13,7 @@ html { background: var(--color-bg); color: var(--color-text); + font-family: var(--font-sans); font-kerning: normal; } diff --git a/website/src/pages/Dependencies.tsx b/website/src/pages/Dependencies.tsx index 282b475..b3a22d7 100644 --- a/website/src/pages/Dependencies.tsx +++ b/website/src/pages/Dependencies.tsx @@ -11,7 +11,7 @@ export function Component() {
-

+

Dependencies

@@ -21,9 +21,9 @@ export function Component() { - - - + + + diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx index 103cf33..230bdc8 100644 --- a/website/src/pages/Home.tsx +++ b/website/src/pages/Home.tsx @@ -18,7 +18,7 @@ const ASTERISK_THRESHOLD = 0.50; const UNPIN_THRESHOLD = 0.8; const PILL = - "inline-block px-4 py-1.5 rounded-md border border-[var(--color-caramel)]/30 text-[var(--color-caramel)] text-sm font-display font-medium hover:bg-[var(--color-caramel)]/10 hover:border-[var(--color-caramel)]/60 hover:-translate-y-0.5 active:translate-y-0 transition-all duration-150"; + "inline-block px-4 py-1.5 rounded-md border border-[var(--color-caramel)]/30 text-[var(--color-caramel)] text-sm font-display hover:bg-[var(--color-caramel)]/10 hover:border-[var(--color-caramel)]/60 hover:-translate-y-0.5 active:translate-y-0 transition-all duration-150"; const INSTALL_STEPS: Record = { "darwin-aarch64": { @@ -184,7 +184,7 @@ function Home() {
{/* Hero words — sits above the video */}
-
+
Multitasking @@ -198,7 +198,7 @@ function Home() {

* supports (and teaches) tmux shortcuts @@ -210,14 +210,14 @@ function Home() { {/* ── Content sections — pulled up to appear as video starts scrolling ── */}

-
-

Stop watching terminals spin

-

+

+

Stop watching terminals spin

+

MouseTerm tracks activity the same way you do — visual motion. When a pane stops changing for two seconds, it marks the task complete and alerts you.

-

+

Works with any CLI tool that prints to a terminal — no plugins, no configuration.

@@ -232,15 +232,15 @@ function Home() {

TODO: Copy/paste with line-break rewrap

-

Copy paste like you meant

-

+

Copy paste like you meant

+

Click and drag in a "mouse conformant" terminal doesn't select text; it sends escape code{" "} - {"\\e[<0;x;yM"}. - And Ctrl+C{" "} + {"\\e[<0;x;yM"}. + And Ctrl+C{" "} doesn't copy; it asks your program to kill itself.

-

+

MouseTerm lets you copy paste like a human, not a terminal.

@@ -249,14 +249,14 @@ function Home() { {/* Section 3: text left, image right */}
-

Soft as a mouse, sharp as tmux

-

+

Soft as a mouse, sharp as tmux

+

Run builds, agents, servers, and scripts side by side. Minimize the ones you're not watching to a compact status indicator. Every pane keeps running and every alert still fires whether you can see it or not.

-

+

Do it all with the mouse, or keep your hands on the keyboard with tmux keybinds.

@@ -266,26 +266,26 @@ function Home() {
-
-

Get MouseTerm

+
+

Get MouseTerm

Try it in the Playground -
+
-

VSCode extension

+

VSCode extension

-

Standalone app

+

Standalone app

{(["darwin-aarch64", "windows-x86_64", "linux-x86_64"] as const).map((key) => ( {installGuide && INSTALL_STEPS[installGuide] && (
-

{INSTALL_STEPS[installGuide].title}

+

{INSTALL_STEPS[installGuide].title}

    {INSTALL_STEPS[installGuide].steps.map((step, i) => (
  1. From 36731a23a2a933ddd0dbaa1db4870fc870f405c5 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 21 Apr 2026 09:45:00 -0700 Subject: [PATCH 06/18] Remove another draft. --- website/README.mdx | 202 --------------------------------------------- 1 file changed, 202 deletions(-) delete mode 100644 website/README.mdx diff --git a/website/README.mdx b/website/README.mdx deleted file mode 100644 index 7e7cded..0000000 --- a/website/README.mdx +++ /dev/null @@ -1,202 +0,0 @@ -{/* ─── Hero: Scroll-driven logo animation ─── */} - - - - {/* - - As user scrolls, feature words stack in one at a time: - - "Multitasking." - - "Terminal." - - "For mice." - - "supports (and teaches) tmux shortcuts" - - Subtitle fades in with inline links. - Stage 5: Continued scroll pushes the logo up and off-screen, - revealing the first content section beneath. - */} - - Inside VSCode and its forks - Microsoft VSCode Marketplace{" / "} - OpenVSX. - Or standalone - Mac{" / "} - Windows{" / "} - Linux. - - - -{/* ─── Section 1: The pitch ─── */} - -
    - -## Stop watching terminals spin - -Run builds, agents, servers, and scripts side by side. MouseTerm watches -them all and tells you which ones finished — so you don't have to. - -Split with a click. Resize with a drag. Minimize the ones you're not -watching to a compact status indicator. Restore them with a click. -Every pane keeps running whether you can see it or not. - -
    - -{/* ─── Section 2: Completion detection ─── */} - -
    - -## You'll know when it's done. - -You know the feeling — alt-tabbing back to a terminal just to see -it's still running. MouseTerm watches for you. - -When a pane stops producing output for two seconds, MouseTerm marks it -as done. No plugins, no configuration, no per-tool setup. Works with -any CLI tool that prints to a terminal. - -Waiting on a build or an agent? Sleep the pane and keep working. The status indicator -updates whether the pane is visible or not. - -
    - -{/* ─── Section 3: Mouse + keyboard ─── */} - -
    - -## Click everything. Or keyboard everything. - -Already know tmux? Same shortcuts. Nothing new to learn. - -Never used tmux? Click to split, drag to resize, hover to learn the -shortcuts if you want. Every action works with the mouse, the keyboard, -or both. - - - - **Split anywhere** - Click to split horizontally or vertically. Drag borders to resize. - - - **Navigate spatially** - Arrow keys or click to move between panes. Swap positions with a drag. - - - **Sleep and wake** - Minimize panes to compact status indicators. Restore with a click. - Tasks keep running. - - - **Zoom in** - Maximize any pane to full screen. One key to toggle back. - - - **Your theme, everywhere** - Uses your VSCode theme. Looks native from the moment you open it. - - - -
    - -{/* ─── Section 4: Use cases ─── */} - -
    - -## Built for how developers actually work. - - - - Claude Code in one pane, Codex in another, your dev server in a third. - See which one finishes first. - - - Start a build. Sleep the pane. Come back when it's done — - the status indicator already told you. - - - Using Claude Code or another CLI tool for the first time? MouseTerm - makes the terminal approachable. Click everything, learn shortcuts later. - - - Same keybindings you already know, plus VSCode themes and completion detection you can't get in tmux. - - - -
    - -{/* ─── Section 5: Keyboard reference ─── */} - -
    - -## Two modes. That's it. - -**Command mode** for managing panes. **Passthrough mode** where every -keypress goes straight to the terminal. - -`Enter` to go in. Quick double-tap `Cmd` to come back out. - - - -| Key | Action | -|-----|--------| -| `"` | Split horizontally | -| `%` | Split vertically | -| Arrow keys | Navigate between panes | -| `Cmd+Arrow` | Swap pane positions | -| `Enter` | Enter passthrough mode | -| `z` | Zoom / unzoom pane | -| `d` | Sleep pane to status bar | -| `x` | Close pane | -| `,` | Rename pane | - - - -
    - -{/* ─── Section 6: Get it ─── */} - -
    - -## Download - - - - Runs inside your editor. Open the command palette and search MouseTerm. - - VSCode Marketplace{" / "} - OpenVSX - - - Runs on its own. Free forever. Hack the source and run your own fork. - - Mac{" / "} - Windows{" / "} - Linux - - - -
    - -{/* ─── Footer ─── */} - - From 200de9f7f8213291e000191573d6c7b064c65d37 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 21 Apr 2026 09:45:11 -0700 Subject: [PATCH 07/18] Fixup copy. --- website/src/pages/Home.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx index 230bdc8..7d1286a 100644 --- a/website/src/pages/Home.tsx +++ b/website/src/pages/Home.tsx @@ -278,14 +278,14 @@ function Home() {
{/* Section 2: image left, text right */} -
+

TODO: Copy/paste with line-break rewrap

-

Copy paste like you meant

+

Copy paste like you meant

Click and drag in a "mouse conformant" terminal doesn't select text; it sends escape code{" "} @@ -247,9 +247,9 @@ function Home() {

{/* Section 3: text left, image right */} -
+
-

Soft as a mouse, sharp as tmux

+

Soft as a mouse, sharp as tmux

Run builds, agents, servers, and scripts side by side. Minimize the ones you're not watching to a compact status indicator. Every pane From eabe1d572fa497f21cd242d5ebcaed729dec4a4f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 21 Apr 2026 13:29:42 -0700 Subject: [PATCH 09/18] More homepage fixup. --- website/src/components/SiteHeader.tsx | 2 +- website/src/index.css | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/website/src/components/SiteHeader.tsx b/website/src/components/SiteHeader.tsx index 8cfb295..9280cc6 100644 --- a/website/src/components/SiteHeader.tsx +++ b/website/src/components/SiteHeader.tsx @@ -39,7 +39,7 @@ const SiteHeader = forwardRef( ? { color: "var(--vscode-editor-foreground, #cccccc)", fontFamily: - "var(--vscode-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif)", + "var(--vscode-font-family, var(--font-display))", backgroundColor: "color-mix(in oklab, var(--vscode-editorGroupHeader-tabsBackground, var(--vscode-sideBar-background, #252526)) 92%, transparent)", borderColor: "var(--vscode-panel-border, #2b2b2b)", diff --git a/website/src/index.css b/website/src/index.css index f14411d..5063504 100644 --- a/website/src/index.css +++ b/website/src/index.css @@ -2,7 +2,7 @@ @theme { --font-display: "Ubuntu Sans Mono", ui-monospace, monospace; - --font-sans: "Ubuntu Mono", ui-monospace, monospace; + --font-body: "Ubuntu Mono", ui-monospace, monospace; --color-bg: oklch(10% 0.01 60); --color-surface: oklch(18% 0.015 60); --color-text: oklch(92% 0.01 60); @@ -13,14 +13,17 @@ html { background: var(--color-bg); color: var(--color-text); - font-family: var(--font-sans); + font-family: var(--font-body); font-kerning: normal; } -/* Override lib's terminal-app styles (body overflow:hidden, #root height:100vh) - which Vite loads globally. The Playground page re-applies them when it mounts. */ -body { +/* Override lib's terminal-app styles (body overflow:hidden, #root height:100vh, + body font-family system font) which Vite loads globally. The Playground page + re-applies them when it mounts. Higher specificity (html body) is required so + we beat the lib's `body` rule which loads after ours on /playground. */ +html body { overflow: auto; + font-family: var(--font-body); } #root { From 6212a588523c5fd0cc274b4f441b0b12d7a9534b Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 21 Apr 2026 13:35:52 -0700 Subject: [PATCH 10/18] Improve layout. --- website/src/pages/Home.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx index a852951..98d06e8 100644 --- a/website/src/pages/Home.tsx +++ b/website/src/pages/Home.tsx @@ -226,12 +226,9 @@ function Home() {

- {/* Section 2: image left, text right */} -
-
-

TODO: Copy/paste with line-break rewrap

-
-
+ {/* Section 2: text left, image right */} +
+

Copy paste like you meant

Click and drag in a "mouse conformant" terminal doesn't select text; @@ -244,11 +241,17 @@ function Home() { MouseTerm lets you copy paste like a human, not a terminal.

+
+

TODO: Copy/paste with line-break rewrap

+
- {/* Section 3: text left, image right */} -
-
+ {/* Section 3: image left, text right */} +
+
+

TODO: Tiling layout and tmux keybinds

+
+

Soft as a mouse, sharp as tmux

Run builds, agents, servers, and scripts side by side. Minimize the @@ -261,9 +264,6 @@ function Home() { tmux keybinds.

-
-

TODO: Tiling layout and tmux keybinds

-
From 6a452632e2c8173e3b1933c1d9c803a249670ff5 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 21 Apr 2026 13:40:39 -0700 Subject: [PATCH 11/18] Remove the `Under construction` stuff. --- website/src/components/SiteHeader.tsx | 16 +--------------- website/src/pages/Playground.tsx | 2 +- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/website/src/components/SiteHeader.tsx b/website/src/components/SiteHeader.tsx index 9280cc6..f663209 100644 --- a/website/src/components/SiteHeader.tsx +++ b/website/src/components/SiteHeader.tsx @@ -51,23 +51,9 @@ const SiteHeader = forwardRef( return ( <> -
- 🚧 Under construction — check back soon! 🚧 -
} /> -
+
{PondModule ? ( Date: Wed, 22 Apr 2026 11:18:34 -0700 Subject: [PATCH 12/18] Homepage improve. --- website/src/pages/Home.tsx | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx index 98d06e8..7c88e3e 100644 --- a/website/src/pages/Home.tsx +++ b/website/src/pages/Home.tsx @@ -119,7 +119,7 @@ function Home() { (fraction - ASTERISK_THRESHOLD) / 0.08 )); if (asteriskRef.current) asteriskRef.current.style.opacity = String(astProgress); - if (footnoteRef.current) footnoteRef.current.style.opacity = String(astProgress * 0.5); + if (footnoteRef.current) footnoteRef.current.style.opacity = String(astProgress * 0.7); // Header: reveal brand + background at unpin threshold const headerProgress = Math.min(1, Math.max(0, @@ -198,7 +198,7 @@ function Home() {

* supports (and teaches) tmux shortcuts @@ -271,7 +271,7 @@ function Home() { Try it in the Playground @@ -316,13 +316,17 @@ function Home() {

-
From 910f264f6099cd3c62c043555e77ec42e6b0b8c1 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 22 Apr 2026 12:29:17 -0700 Subject: [PATCH 13/18] Much more progress on the homepage. --- website/src/pages/Home.tsx | 79 +++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx index 7c88e3e..9d1d758 100644 --- a/website/src/pages/Home.tsx +++ b/website/src/pages/Home.tsx @@ -10,8 +10,10 @@ export { Home as Component }; const RUNWAY_VH = 300; /** Scroll thresholds within the pinned runway (0–1) */ -const WORD_THRESHOLDS = [0.0, 0.28, 0.41] as const; -const ASTERISK_THRESHOLD = 0.50; +const ICON_INITIAL_HIDE_FRAC = 0.67; // Fraction of icon's rendered height hidden at load — leaves top third visible +const HOOK_FADE_REMAINING = 0.10; // Hook begins fading when bottom 10% of icon enters viewport +const WORD_THRESHOLDS = [0.25, 0.40, 0.55] as const; +const ASTERISK_THRESHOLD = 0.65; /** Fraction of runway where the hero text unpins and scrolls away (0–1). * The video keeps scrubbing underneath. */ @@ -58,6 +60,7 @@ function Home() { const footnoteRef = useRef(null); const headerRef = useRef(null); const headerBrandRef = useRef(null); + const hookRef = useRef(null); const [installGuide, setInstallGuide] = useState(null); useEffect(() => { @@ -98,9 +101,33 @@ function Home() { const fraction = runwayHeight > 0 ? Math.min(1, Math.max(0, runwayScroll / runwayHeight)) : 0; - // Scrub video + + // Compute rendered video content height (object-contain fits within container + // while preserving the video's native aspect ratio). This is the icon's actual + // on-screen size and drives the icon rise distance, the hook fade timing, and + // the video scrub start point. + const naturalAspect = video.videoWidth && video.videoHeight + ? video.videoWidth / video.videoHeight + : 1.22; // fallback before metadata loads + const containerAspect = video.offsetWidth / video.offsetHeight; + const iconHeight = naturalAspect > containerAspect + ? video.offsetWidth / naturalAspect // width-limited + : video.offsetHeight; // height-limited + const initialOffset = iconHeight * ICON_INITIAL_HIDE_FRAC; + + // Scrub video: hold on frame 0 while the icon is still rising into position. + // Once the icon reaches its static position (runwayScroll >= initialOffset), + // scrub the remaining scroll range across the video's duration. if (video.duration && isFinite(video.duration)) { - video.currentTime = fraction * video.duration; + if (runwayScroll < initialOffset) { + video.currentTime = 0; + } else { + const videoRunway = runwayHeight - initialOffset; + const videoProgress = videoRunway > 0 + ? Math.min(1, (runwayScroll - initialOffset) / videoRunway) + : 0; + video.currentTime = videoProgress * video.duration; + } } // Reveal words @@ -121,6 +148,8 @@ function Home() { if (asteriskRef.current) asteriskRef.current.style.opacity = String(astProgress); if (footnoteRef.current) footnoteRef.current.style.opacity = String(astProgress * 0.7); + // (Hook fade handled below, after iconHeight is computed for the icon rise.) + // Header: reveal brand + background at unpin threshold const headerProgress = Math.min(1, Math.max(0, (fraction - UNPIN_THRESHOLD) / 0.08 @@ -139,10 +168,40 @@ function Home() { const contentEnterScroll = runway.offsetHeight * UNPIN_THRESHOLD - window.innerHeight; const slideAmount = Math.max(0, runwayScroll - contentEnterScroll); - video.style.transform = slideAmount > 0 - ? `translateY(-${Math.round(slideAmount)}px)` + // Video transform combines two behaviors: + // 1. Icon-rise (runwayScroll 0 → initialOffset px): 1:1 with scroll. + // At load the icon is translated +initialOffset so only the top third + // is visible; each pixel of scroll lifts it by one pixel until fully + // in view. + // 2. Existing unpin slide (fraction > UNPIN_THRESHOLD): translate up with + // content as hero unpins. + let videoTranslateY = 0; + let iconCurrentOffset = 0; + if (runwayScroll < initialOffset) { + iconCurrentOffset = initialOffset - runwayScroll; + videoTranslateY = iconCurrentOffset; + } else if (slideAmount > 0) { + videoTranslateY = -Math.round(slideAmount); + } + video.style.transform = videoTranslateY !== 0 + ? `translateY(${Math.round(videoTranslateY)}px)` : ''; + // Hook text: holds visible until the bottom ~10% of the icon enters the + // viewport, then fades out as the icon completes its rise. Tied to icon + // position (not scroll fraction) so the timing matches 1:1 icon motion. + if (hookRef.current) { + const remainingHidden = iconHeight > 0 ? iconCurrentOffset / iconHeight : 0; + let fadeProgress = 0; + if (runwayScroll >= initialOffset) { + fadeProgress = 1; + } else if (remainingHidden < HOOK_FADE_REMAINING) { + fadeProgress = 1 - remainingHidden / HOOK_FADE_REMAINING; + } + hookRef.current.style.opacity = String(1 - fadeProgress); + hookRef.current.style.transform = `translateY(${-fadeProgress * 24}px)`; + } + // Hero: cap so it stops at unstick (fraction = 1); natural scroll takes over. const maxHeroOffset = runway.offsetHeight * (1 - UNPIN_THRESHOLD); const heroOffset = Math.min(slideAmount, maxHeroOffset); @@ -182,6 +241,14 @@ function Home() { {/* ── Pinned scroll runway: hero text overlay ── */}
+ {/* Hook copy — visible on load, fades out on first scroll */} +
+ Too many terminals. + Not enough focus. +
{/* Hero words — sits above the video */}
From 8d74f2441ee098e35c1580c204111d099ce9ad1f Mon Sep 17 00:00:00 2001 From: nedtwigg Date: Wed, 22 Apr 2026 19:53:31 +0000 Subject: [PATCH 14/18] Claude Code simplify: extract clamp01 helper, flatten icon-rise/hook-fade logic, trim verbose comments --- website/src/pages/Home.tsx | 67 ++++++++++++++------------------------ 1 file changed, 25 insertions(+), 42 deletions(-) diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx index 9d1d758..12a4bc4 100644 --- a/website/src/pages/Home.tsx +++ b/website/src/pages/Home.tsx @@ -19,6 +19,9 @@ const ASTERISK_THRESHOLD = 0.65; * The video keeps scrubbing underneath. */ const UNPIN_THRESHOLD = 0.8; +/** Clamp a value to 0–1. */ +const clamp01 = (v: number) => Math.min(1, Math.max(0, v)); + const PILL = "inline-block px-4 py-1.5 rounded-md border border-[var(--color-caramel)]/30 text-[var(--color-caramel)] text-sm font-display hover:bg-[var(--color-caramel)]/10 hover:border-[var(--color-caramel)]/60 hover:-translate-y-0.5 active:translate-y-0 transition-all duration-150"; @@ -99,13 +102,10 @@ function Home() { const runwayScroll = -rect.top; const runwayHeight = runway.offsetHeight - window.innerHeight; const fraction = runwayHeight > 0 - ? Math.min(1, Math.max(0, runwayScroll / runwayHeight)) + ? clamp01(runwayScroll / runwayHeight) : 0; - // Compute rendered video content height (object-contain fits within container - // while preserving the video's native aspect ratio). This is the icon's actual - // on-screen size and drives the icon rise distance, the hook fade timing, and - // the video scrub start point. + // Rendered icon height (object-contain preserves aspect ratio within container). const naturalAspect = video.videoWidth && video.videoHeight ? video.videoWidth / video.videoHeight : 1.22; // fallback before metadata loads @@ -115,16 +115,13 @@ function Home() { : video.offsetHeight; // height-limited const initialOffset = iconHeight * ICON_INITIAL_HIDE_FRAC; - // Scrub video: hold on frame 0 while the icon is still rising into position. - // Once the icon reaches its static position (runwayScroll >= initialOffset), - // scrub the remaining scroll range across the video's duration. + // Scrub video: hold frame 0 during icon rise, then scrub remaining range. if (video.duration && isFinite(video.duration)) { if (runwayScroll < initialOffset) { video.currentTime = 0; } else { - const videoRunway = runwayHeight - initialOffset; - const videoProgress = videoRunway > 0 - ? Math.min(1, (runwayScroll - initialOffset) / videoRunway) + const videoProgress = (runwayHeight - initialOffset) > 0 + ? clamp01((runwayScroll - initialOffset) / (runwayHeight - initialOffset)) : 0; video.currentTime = videoProgress * video.duration; } @@ -134,26 +131,24 @@ function Home() { for (let i = 0; i < WORD_THRESHOLDS.length; i++) { const el = wordRefs[i].current; if (!el) continue; - const progress = Math.min(1, Math.max(0, + const progress = clamp01( (fraction - WORD_THRESHOLDS[i]) / 0.08 - )); + ); el.style.opacity = String(progress); el.style.transform = `translateY(${(1 - progress) * 12}px)`; } // Asterisk + footnote - const astProgress = Math.min(1, Math.max(0, + const astProgress = clamp01( (fraction - ASTERISK_THRESHOLD) / 0.08 - )); + ); if (asteriskRef.current) asteriskRef.current.style.opacity = String(astProgress); if (footnoteRef.current) footnoteRef.current.style.opacity = String(astProgress * 0.7); - // (Hook fade handled below, after iconHeight is computed for the icon rise.) - // Header: reveal brand + background at unpin threshold - const headerProgress = Math.min(1, Math.max(0, + const headerProgress = clamp01( (fraction - UNPIN_THRESHOLD) / 0.08 - )); + ); if (headerBrandRef.current) { headerBrandRef.current.style.opacity = String(headerProgress); } @@ -169,35 +164,23 @@ function Home() { const slideAmount = Math.max(0, runwayScroll - contentEnterScroll); // Video transform combines two behaviors: - // 1. Icon-rise (runwayScroll 0 → initialOffset px): 1:1 with scroll. - // At load the icon is translated +initialOffset so only the top third - // is visible; each pixel of scroll lifts it by one pixel until fully - // in view. - // 2. Existing unpin slide (fraction > UNPIN_THRESHOLD): translate up with - // content as hero unpins. - let videoTranslateY = 0; - let iconCurrentOffset = 0; - if (runwayScroll < initialOffset) { - iconCurrentOffset = initialOffset - runwayScroll; - videoTranslateY = iconCurrentOffset; - } else if (slideAmount > 0) { - videoTranslateY = -Math.round(slideAmount); - } + // 1. Icon-rise (runwayScroll 0 → initialOffset): translate down so only + // the top third is visible; scroll lifts it 1:1 until fully in view. + // 2. Unpin slide (fraction > UNPIN_THRESHOLD): translate up with content. + const iconCurrentOffset = Math.max(0, initialOffset - runwayScroll); + const videoTranslateY = iconCurrentOffset > 0 + ? iconCurrentOffset + : slideAmount > 0 ? -Math.round(slideAmount) : 0; video.style.transform = videoTranslateY !== 0 ? `translateY(${Math.round(videoTranslateY)}px)` : ''; - // Hook text: holds visible until the bottom ~10% of the icon enters the - // viewport, then fades out as the icon completes its rise. Tied to icon - // position (not scroll fraction) so the timing matches 1:1 icon motion. + // Hook text: visible until the icon nearly finishes rising, then fades out. if (hookRef.current) { const remainingHidden = iconHeight > 0 ? iconCurrentOffset / iconHeight : 0; - let fadeProgress = 0; - if (runwayScroll >= initialOffset) { - fadeProgress = 1; - } else if (remainingHidden < HOOK_FADE_REMAINING) { - fadeProgress = 1 - remainingHidden / HOOK_FADE_REMAINING; - } + const fadeProgress = iconCurrentOffset === 0 + ? 1 + : clamp01(1 - remainingHidden / HOOK_FADE_REMAINING); hookRef.current.style.opacity = String(1 - fadeProgress); hookRef.current.style.transform = `translateY(${-fadeProgress * 24}px)`; } From fcb7a63e4ceac7c6fbfa38c0170cde159ce95e65 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 22 Apr 2026 14:37:43 -0700 Subject: [PATCH 15/18] Fix mobile header overlap; anchor hook + hero words just below header Previously hero words were bottom-anchored above a fixed 520px video, pushing "Multitasking" up into the header and colliding with the GitHub link on short mobile viewports. Position both the hook and hero words absolutely at top-20 md:top-24 so they crossfade in place right below the header. Also skip sub-frame video seeks that force a redundant decode. Co-Authored-By: Claude Opus 4.7 (1M context) --- website/src/pages/Home.tsx | 54 ++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx index 12a4bc4..893b529 100644 --- a/website/src/pages/Home.tsx +++ b/website/src/pages/Home.tsx @@ -116,14 +116,18 @@ function Home() { const initialOffset = iconHeight * ICON_INITIAL_HIDE_FRAC; // Scrub video: hold frame 0 during icon rise, then scrub remaining range. + // Skip redundant seeks whose delta is less than one frame's duration — + // each seek forces a decode, and sub-frame seeks produce the same output. if (video.duration && isFinite(video.duration)) { - if (runwayScroll < initialOffset) { - video.currentTime = 0; - } else { + let target = 0; + if (runwayScroll >= initialOffset) { const videoProgress = (runwayHeight - initialOffset) > 0 ? clamp01((runwayScroll - initialOffset) / (runwayHeight - initialOffset)) : 0; - video.currentTime = videoProgress * video.duration; + target = videoProgress * video.duration; + } + if (Math.abs(video.currentTime - target) > 1 / 24) { + video.currentTime = target; } } @@ -227,33 +231,31 @@ function Home() { {/* Hook copy — visible on load, fades out on first scroll */}
Too many terminals. Not enough focus.
- {/* Hero words — sits above the video */} -
-
- - Multitasking - - - Terminal - - - - for Mice* - + {/* Hero words — crossfade in place with the hook, just below the header */} +
+ + Multitasking + + + Terminal + + + + for Mice* -

- * supports (and teaches) tmux shortcuts -

-
+
+

+ * supports (and teaches) tmux shortcuts +

From 69268631a828c114534855d2f092824f2f5db174 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 22 Apr 2026 14:39:28 -0700 Subject: [PATCH 16/18] Keep "for Mice" centered regardless of asterisk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Absolute-position the footnote asterisk so it sits after "Mice" without contributing to the span's width — otherwise the invisible asterisk reserved layout space and shifted "for Mice" off-center. Co-Authored-By: Claude Opus 4.7 (1M context) --- website/src/pages/Home.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx index 893b529..465f880 100644 --- a/website/src/pages/Home.tsx +++ b/website/src/pages/Home.tsx @@ -245,8 +245,8 @@ function Home() { Terminal - - for Mice* + + for Mice*

Date: Wed, 22 Apr 2026 14:45:58 -0700 Subject: [PATCH 17/18] Improve centering. --- website/src/pages/Home.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx index 465f880..93710b9 100644 --- a/website/src/pages/Home.tsx +++ b/website/src/pages/Home.tsx @@ -246,7 +246,7 @@ function Home() { - for Mice* + for Mice*

- * supports (and teaches) tmux shortcuts + *supports (and teaches) tmux shortcuts

From 64b7ac4bcb4d4d27b1886c8ab872f357c831b6df Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 22 Apr 2026 15:15:56 -0700 Subject: [PATCH 18/18] Cap icon height so it never collides with the hero words With hero words anchored at top-20 md:top-24, their bottom edge could reach y=~370 on the wide-font end. Icon is bottom-fixed at 500px, so on viewports around 800-900px tall its top (y=viewport-500) landed right on top of "for Mice*" and the footnote. Clamp icon height to min(500px, calc(100vh - 420px)) so the icon always leaves room below the word block; tall viewports still see the full 500px. Co-Authored-By: Claude Opus 4.7 (1M context) --- website/src/pages/Home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx index 93710b9..d97f17e 100644 --- a/website/src/pages/Home.tsx +++ b/website/src/pages/Home.tsx @@ -222,7 +222,7 @@ function Home() { playsInline preload="auto" className="fixed bottom-0 left-0 w-full object-contain object-bottom z-0" - style={{ height: "500px" }} + style={{ height: "min(500px, calc(100vh - 420px))" }} /> {/* ── Pinned scroll runway: hero text overlay ── */}
PackageVersionLicensePackageVersionLicense