From 78d836fd09c09ed25c43d48d8414dd2c999f2951 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 15 Jun 2026 07:47:29 -0400 Subject: [PATCH] repl: auto-grow input textarea to fit pasted multi-line programs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until now the input was pinned at rows="1" — paste a 40-line program and you edited it through a one-line keyhole. autosizeInput() now sets height to scrollHeight on every input event, history navigation, and post-send clear. CSS min/max-height (1.5em / 30vh) clamp the bounds; once the textarea hits 30vh it scrolls internally instead of pushing the prompt-bar off screen. Prompt sigil top-aligns (align-self: start + matching padding-top) so the λ> stays on the first line of input instead of drifting to vertical-center as the textarea grows. Smoke-tested headless: 1-line stays 22px, 6-line grows to 97px, 40-line caps at 216px (30vh of 720px) with scrollHeight 608 (internal scroll engaged), send empties back to 22px. Zero page errors. --- wasm/dist-repl/repl.css | 6 ++++++ wasm/dist-repl/repl.js | 14 ++++++++++++++ wasm/repl/repl.css | 6 ++++++ wasm/repl/repl.js | 14 ++++++++++++++ www/repl/repl.css | 6 ++++++ www/repl/repl.js | 14 ++++++++++++++ 6 files changed, 60 insertions(+) diff --git a/wasm/dist-repl/repl.css b/wasm/dist-repl/repl.css index 7ea2382..66ca2d4 100644 --- a/wasm/dist-repl/repl.css +++ b/wasm/dist-repl/repl.css @@ -310,6 +310,12 @@ body.repl { color: var(--green); font-weight: 600; font-size: 0.95em; + /* Top-align so multi-line input keeps λ> on the first line instead + * of drifting to vertical-center as the textarea grows. The padding + * matches the textarea's padding-top so the sigil sits on the same + * baseline as the first character. */ + align-self: start; + padding-top: 0.4rem; } .prompt-bar textarea { resize: none; diff --git a/wasm/dist-repl/repl.js b/wasm/dist-repl/repl.js index da5f681..623a2ce 100644 --- a/wasm/dist-repl/repl.js +++ b/wasm/dist-repl/repl.js @@ -534,6 +534,7 @@ function historyPrev() { } inputEl.value = entries[history.idx]; inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length); + autosizeInput(); } function historyNext() { @@ -548,6 +549,7 @@ function historyNext() { inputEl.value = entries[history.idx]; } inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length); + autosizeInput(); } function resetHistory() { @@ -555,6 +557,16 @@ function resetHistory() { history.draft = ""; } +// Auto-grow the textarea to fit its content (up to the max-height the +// CSS pins it at, after which the textarea scrolls internally). Paste +// a 40-line program and the prompt-bar expands instead of leaving you +// editing the program through a one-line keyhole. Fires on every input +// event and every programmatic value set (sendInput, history, etc.). +function autosizeInput() { + inputEl.style.height = "auto"; + inputEl.style.height = inputEl.scrollHeight + "px"; +} + // ─── Input handling ───────────────────────────────────────────────── async function sendInput() { const src = inputEl.value.trim(); @@ -577,6 +589,7 @@ async function sendInput() { }; tab.transcript.push(entry); inputEl.value = ""; + autosizeInput(); resetHistory(); sendBtn.disabled = true; cancelBtn.disabled = false; @@ -737,6 +750,7 @@ inputEl.addEventListener("keydown", (e) => { } }); inputEl.addEventListener("input", () => { + autosizeInput(); // Any keystroke that isn't an arrow drops history navigation — // edits are now the user's own draft, not the historical entry. if (history.idx !== null && document.activeElement === inputEl) { diff --git a/wasm/repl/repl.css b/wasm/repl/repl.css index 7ea2382..66ca2d4 100644 --- a/wasm/repl/repl.css +++ b/wasm/repl/repl.css @@ -310,6 +310,12 @@ body.repl { color: var(--green); font-weight: 600; font-size: 0.95em; + /* Top-align so multi-line input keeps λ> on the first line instead + * of drifting to vertical-center as the textarea grows. The padding + * matches the textarea's padding-top so the sigil sits on the same + * baseline as the first character. */ + align-self: start; + padding-top: 0.4rem; } .prompt-bar textarea { resize: none; diff --git a/wasm/repl/repl.js b/wasm/repl/repl.js index da5f681..623a2ce 100644 --- a/wasm/repl/repl.js +++ b/wasm/repl/repl.js @@ -534,6 +534,7 @@ function historyPrev() { } inputEl.value = entries[history.idx]; inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length); + autosizeInput(); } function historyNext() { @@ -548,6 +549,7 @@ function historyNext() { inputEl.value = entries[history.idx]; } inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length); + autosizeInput(); } function resetHistory() { @@ -555,6 +557,16 @@ function resetHistory() { history.draft = ""; } +// Auto-grow the textarea to fit its content (up to the max-height the +// CSS pins it at, after which the textarea scrolls internally). Paste +// a 40-line program and the prompt-bar expands instead of leaving you +// editing the program through a one-line keyhole. Fires on every input +// event and every programmatic value set (sendInput, history, etc.). +function autosizeInput() { + inputEl.style.height = "auto"; + inputEl.style.height = inputEl.scrollHeight + "px"; +} + // ─── Input handling ───────────────────────────────────────────────── async function sendInput() { const src = inputEl.value.trim(); @@ -577,6 +589,7 @@ async function sendInput() { }; tab.transcript.push(entry); inputEl.value = ""; + autosizeInput(); resetHistory(); sendBtn.disabled = true; cancelBtn.disabled = false; @@ -737,6 +750,7 @@ inputEl.addEventListener("keydown", (e) => { } }); inputEl.addEventListener("input", () => { + autosizeInput(); // Any keystroke that isn't an arrow drops history navigation — // edits are now the user's own draft, not the historical entry. if (history.idx !== null && document.activeElement === inputEl) { diff --git a/www/repl/repl.css b/www/repl/repl.css index 7ea2382..66ca2d4 100644 --- a/www/repl/repl.css +++ b/www/repl/repl.css @@ -310,6 +310,12 @@ body.repl { color: var(--green); font-weight: 600; font-size: 0.95em; + /* Top-align so multi-line input keeps λ> on the first line instead + * of drifting to vertical-center as the textarea grows. The padding + * matches the textarea's padding-top so the sigil sits on the same + * baseline as the first character. */ + align-self: start; + padding-top: 0.4rem; } .prompt-bar textarea { resize: none; diff --git a/www/repl/repl.js b/www/repl/repl.js index da5f681..623a2ce 100644 --- a/www/repl/repl.js +++ b/www/repl/repl.js @@ -534,6 +534,7 @@ function historyPrev() { } inputEl.value = entries[history.idx]; inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length); + autosizeInput(); } function historyNext() { @@ -548,6 +549,7 @@ function historyNext() { inputEl.value = entries[history.idx]; } inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length); + autosizeInput(); } function resetHistory() { @@ -555,6 +557,16 @@ function resetHistory() { history.draft = ""; } +// Auto-grow the textarea to fit its content (up to the max-height the +// CSS pins it at, after which the textarea scrolls internally). Paste +// a 40-line program and the prompt-bar expands instead of leaving you +// editing the program through a one-line keyhole. Fires on every input +// event and every programmatic value set (sendInput, history, etc.). +function autosizeInput() { + inputEl.style.height = "auto"; + inputEl.style.height = inputEl.scrollHeight + "px"; +} + // ─── Input handling ───────────────────────────────────────────────── async function sendInput() { const src = inputEl.value.trim(); @@ -577,6 +589,7 @@ async function sendInput() { }; tab.transcript.push(entry); inputEl.value = ""; + autosizeInput(); resetHistory(); sendBtn.disabled = true; cancelBtn.disabled = false; @@ -737,6 +750,7 @@ inputEl.addEventListener("keydown", (e) => { } }); inputEl.addEventListener("input", () => { + autosizeInput(); // Any keystroke that isn't an arrow drops history navigation — // edits are now the user's own draft, not the historical entry. if (history.idx !== null && document.activeElement === inputEl) {