repl: auto-grow input textarea to fit pasted multi-line programs

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.
This commit is contained in:
russell@unturf.com 2026-06-15 07:47:29 -04:00
parent d4380c64c7
commit 78d836fd09
No known key found for this signature in database
6 changed files with 60 additions and 0 deletions

View file

@ -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;

View file

@ -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) {

View file

@ -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;

View file

@ -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) {

View file

@ -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;

View file

@ -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) {