fix terminal styling: scaling banner, mobile padding, typing caret, echoed prompt
site / generated-is-current (push) Successful in 21s

- banner scales down to fit narrow screens instead of scrolling sideways
- smaller base font/padding under sm: to stop the crooked mobile look
- block caret tracks the cursor position inside the input (ghost-span
  measurement) instead of sitting at the line end
- executed commands echo with the momme@world:~$ prompt in front
This commit is contained in:
momme
2026-10-14 02:56:53 +02:00
parent bbf1852842
commit c88dd8326d
5 changed files with 75 additions and 15 deletions
+9 -6
View File
@@ -26,8 +26,8 @@
<span class="ml-2 text-xs text-dim select-none">momme@world: ~</span> <span class="ml-2 text-xs text-dim select-none">momme@world: ~</span>
</div> </div>
<div id="screen" tabindex="-1" class="h-[62dvh] overflow-y-auto px-4 py-4 text-[13.5px] sm:text-[15px] leading-[1.35] cursor-text"> <div id="screen" tabindex="-1" class="h-[62dvh] overflow-y-auto px-2.5 py-3 sm:px-4 sm:py-4 text-[12.5px] sm:text-[15px] leading-[1.35] cursor-text">
<pre class="text-dim overflow-x-auto not-prose"> _ _ <pre id="banner" class="text-dim whitespace-pre w-full origin-top-left not-prose"> _ _
| | | | | | | |
_ __ ___ ___ _ __ ___ _ __ ___ ___ __ _____ _ __| | __| | _ __ ___ ___ _ __ ___ _ __ ___ ___ __ _____ _ __| | __| |
| '_ ` _ \ / _ \| '_ ` _ \| '_ ` _ \ / _ \ \ \ /\ / / _ \| '__| |/ _` | | '_ ` _ \ / _ \| '_ ` _ \| '_ ` _ \ / _ \ \ \ /\ / / _ \| '__| |/ _` |
@@ -40,10 +40,13 @@
<form id="prompt" autocomplete="off" class="flex items-baseline gap-2 mt-1"> <form id="prompt" autocomplete="off" class="flex items-baseline gap-2 mt-1">
<span aria-hidden="true" class="text-accent shrink-0">momme@world:~$</span> <span aria-hidden="true" class="text-accent shrink-0">momme@world:~$</span>
<input id="cmd" aria-label="Run a command" placeholder="type a command — try `help`" <span class="relative flex-1 min-w-0">
class="flex-1 min-w-0 bg-transparent outline-none border-0 p-0 text-fg placeholder:text-dim/60 caret-transparent" <input id="cmd" aria-label="Run a command" placeholder="type a command — try `help`"
autocapitalize="off" autocorrect="off" spellcheck="false"> class="relative block w-full bg-transparent outline-none border-0 p-0 text-fg placeholder:text-dim/60 caret-transparent overflow-hidden"
<span aria-hidden="true" class="cursor -ml-[0.6em]"></span> autocapitalize="off" autocorrect="off" spellcheck="false">
<span id="ghost" aria-hidden="true" class="absolute left-0 top-0 invisible whitespace-pre pointer-events-none"></span>
<span id="caret" aria-hidden="true" class="cursor absolute top-[0.05em] left-0 pointer-events-none"></span>
</span>
</form> </form>
</div> </div>
</div> </div>
+28 -1
View File
@@ -63,6 +63,28 @@ const make = (tag, cls, value) => {
const scrollDown = () => (screen.scrollTop = screen.scrollHeight); const scrollDown = () => (screen.scrollTop = screen.scrollHeight);
// banner: shrink to fit instead of scrolling sideways
const banner = document.getElementById("banner");
const fitBanner = () => {
banner.style.transform = "";
banner.style.height = "";
// clientWidth = box the banner had to fit in; scrollWidth = what it wants
const scale = Math.min(1, banner.clientWidth / banner.scrollWidth);
banner.style.transform = `scale(${scale})`;
banner.style.height = `${banner.offsetHeight * scale}px`;
};
addEventListener("resize", fitBanner);
// caret: follow the text actually typed in the input (the ghost span measures it)
const caretEl = document.getElementById("caret");
const ghost = document.getElementById("ghost");
const moveCaret = () => {
const pos = input.selectionStart ?? input.value.length;
ghost.textContent = input.value.slice(0, pos);
caretEl.style.left = `${ghost.getBoundingClientRect().width - input.scrollLeft}px`;
};
for (const ev of ["input", "keyup", "click", "focus"]) input.addEventListener(ev, moveCaret);
// ---------- typewriter ---------- // ---------- typewriter ----------
// print() never renders directly: it queues fully-built line nodes and this // print() never renders directly: it queues fully-built line nodes and this
// machine writes them character by character. A keypress or click while // machine writes them character by character. A keypress or click while
@@ -239,7 +261,9 @@ function lookup(words) {
function execute(raw) { function execute(raw) {
flush(); flush();
out.append(make("div", "block text-fg mt-4", raw)); const echo = make("div", "block mt-4");
echo.append(make("span", "text-accent", "momme@world:~$ "), make("span", "text-fg", raw));
out.append(echo);
const words = raw.trim().toLowerCase().split(/\s+/).filter(Boolean); const words = raw.trim().toLowerCase().split(/\s+/).filter(Boolean);
if (!words.length) return scrollDown(); if (!words.length) return scrollDown();
if (words[0] === "clear") return kill(), (out.textContent = ""); if (words[0] === "clear") return kill(), (out.textContent = "");
@@ -268,6 +292,7 @@ form.addEventListener("submit", (event) => {
event.preventDefault(); event.preventDefault();
const raw = input.value; const raw = input.value;
input.value = ""; input.value = "";
moveCaret();
if (raw.trim()) history.items.push(raw.trim()); if (raw.trim()) history.items.push(raw.trim());
history.at = history.items.length; history.at = history.items.length;
execute(raw); execute(raw);
@@ -296,8 +321,10 @@ load().then(
input.focus(); input.focus();
}) })
); );
fitBanner();
print("hello — type help for the rest of it.", ""); print("hello — type help for the rest of it.", "");
input.focus(); input.focus();
moveCaret();
}, },
() => print({ error: "commands.json did not load — did the site generator run?", more: [] }, "") () => print({ error: "commands.json did not load — did the site generator run?", more: [] }, "")
); );
+1 -1
View File
File diff suppressed because one or more lines are too long
+9 -6
View File
@@ -26,17 +26,20 @@
<span class="ml-2 text-xs text-dim select-none">momme@world: ~</span> <span class="ml-2 text-xs text-dim select-none">momme@world: ~</span>
</div> </div>
<div id="screen" tabindex="-1" class="h-[62dvh] overflow-y-auto px-4 py-4 text-[13.5px] sm:text-[15px] leading-[1.35] cursor-text"> <div id="screen" tabindex="-1" class="h-[62dvh] overflow-y-auto px-2.5 py-3 sm:px-4 sm:py-4 text-[12.5px] sm:text-[15px] leading-[1.35] cursor-text">
<pre class="text-dim overflow-x-auto not-prose"><!--@banner--></pre> <pre id="banner" class="text-dim whitespace-pre w-full origin-top-left not-prose"><!--@banner--></pre>
<div id="out" class="whitespace-pre-wrap break-words"></div> <div id="out" class="whitespace-pre-wrap break-words"></div>
<form id="prompt" autocomplete="off" class="flex items-baseline gap-2 mt-1"> <form id="prompt" autocomplete="off" class="flex items-baseline gap-2 mt-1">
<span aria-hidden="true" class="text-accent shrink-0">momme@world:~$</span> <span aria-hidden="true" class="text-accent shrink-0">momme@world:~$</span>
<input id="cmd" aria-label="Run a command" placeholder="type a command — try `help`" <span class="relative flex-1 min-w-0">
class="flex-1 min-w-0 bg-transparent outline-none border-0 p-0 text-fg placeholder:text-dim/60 caret-transparent" <input id="cmd" aria-label="Run a command" placeholder="type a command — try `help`"
autocapitalize="off" autocorrect="off" spellcheck="false"> class="relative block w-full bg-transparent outline-none border-0 p-0 text-fg placeholder:text-dim/60 caret-transparent overflow-hidden"
<span aria-hidden="true" class="cursor -ml-[0.6em]"></span> autocapitalize="off" autocorrect="off" spellcheck="false">
<span id="ghost" aria-hidden="true" class="absolute left-0 top-0 invisible whitespace-pre pointer-events-none"></span>
<span id="caret" aria-hidden="true" class="cursor absolute top-[0.05em] left-0 pointer-events-none"></span>
</span>
</form> </form>
</div> </div>
</div> </div>
+28 -1
View File
@@ -63,6 +63,28 @@ const make = (tag, cls, value) => {
const scrollDown = () => (screen.scrollTop = screen.scrollHeight); const scrollDown = () => (screen.scrollTop = screen.scrollHeight);
// banner: shrink to fit instead of scrolling sideways
const banner = document.getElementById("banner");
const fitBanner = () => {
banner.style.transform = "";
banner.style.height = "";
// clientWidth = box the banner had to fit in; scrollWidth = what it wants
const scale = Math.min(1, banner.clientWidth / banner.scrollWidth);
banner.style.transform = `scale(${scale})`;
banner.style.height = `${banner.offsetHeight * scale}px`;
};
addEventListener("resize", fitBanner);
// caret: follow the text actually typed in the input (the ghost span measures it)
const caretEl = document.getElementById("caret");
const ghost = document.getElementById("ghost");
const moveCaret = () => {
const pos = input.selectionStart ?? input.value.length;
ghost.textContent = input.value.slice(0, pos);
caretEl.style.left = `${ghost.getBoundingClientRect().width - input.scrollLeft}px`;
};
for (const ev of ["input", "keyup", "click", "focus"]) input.addEventListener(ev, moveCaret);
// ---------- typewriter ---------- // ---------- typewriter ----------
// print() never renders directly: it queues fully-built line nodes and this // print() never renders directly: it queues fully-built line nodes and this
// machine writes them character by character. A keypress or click while // machine writes them character by character. A keypress or click while
@@ -239,7 +261,9 @@ function lookup(words) {
function execute(raw) { function execute(raw) {
flush(); flush();
out.append(make("div", "block text-fg mt-4", raw)); const echo = make("div", "block mt-4");
echo.append(make("span", "text-accent", "momme@world:~$ "), make("span", "text-fg", raw));
out.append(echo);
const words = raw.trim().toLowerCase().split(/\s+/).filter(Boolean); const words = raw.trim().toLowerCase().split(/\s+/).filter(Boolean);
if (!words.length) return scrollDown(); if (!words.length) return scrollDown();
if (words[0] === "clear") return kill(), (out.textContent = ""); if (words[0] === "clear") return kill(), (out.textContent = "");
@@ -268,6 +292,7 @@ form.addEventListener("submit", (event) => {
event.preventDefault(); event.preventDefault();
const raw = input.value; const raw = input.value;
input.value = ""; input.value = "";
moveCaret();
if (raw.trim()) history.items.push(raw.trim()); if (raw.trim()) history.items.push(raw.trim());
history.at = history.items.length; history.at = history.items.length;
execute(raw); execute(raw);
@@ -296,8 +321,10 @@ load().then(
input.focus(); input.focus();
}) })
); );
fitBanner();
print("hello — type help for the rest of it.", ""); print("hello — type help for the rest of it.", "");
input.focus(); input.focus();
moveCaret();
}, },
() => print({ error: "commands.json did not load — did the site generator run?", more: [] }, "") () => print({ error: "commands.json did not load — did the site generator run?", more: [] }, "")
); );