fix terminal styling: scaling banner, mobile padding, typing caret, echoed prompt
site / generated-is-current (push) Successful in 21s
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:
+7
-4
@@ -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>
|
||||||
|
<span class="relative flex-1 min-w-0">
|
||||||
<input id="cmd" aria-label="Run a command" placeholder="type a command — try `help`"
|
<input id="cmd" aria-label="Run a command" placeholder="type a command — try `help`"
|
||||||
class="flex-1 min-w-0 bg-transparent outline-none border-0 p-0 text-fg placeholder:text-dim/60 caret-transparent"
|
class="relative block w-full bg-transparent outline-none border-0 p-0 text-fg placeholder:text-dim/60 caret-transparent overflow-hidden"
|
||||||
autocapitalize="off" autocorrect="off" spellcheck="false">
|
autocapitalize="off" autocorrect="off" spellcheck="false">
|
||||||
<span aria-hidden="true" class="cursor -ml-[0.6em]"></span>
|
<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
@@ -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
File diff suppressed because one or more lines are too long
+7
-4
@@ -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>
|
||||||
|
<span class="relative flex-1 min-w-0">
|
||||||
<input id="cmd" aria-label="Run a command" placeholder="type a command — try `help`"
|
<input id="cmd" aria-label="Run a command" placeholder="type a command — try `help`"
|
||||||
class="flex-1 min-w-0 bg-transparent outline-none border-0 p-0 text-fg placeholder:text-dim/60 caret-transparent"
|
class="relative block w-full bg-transparent outline-none border-0 p-0 text-fg placeholder:text-dim/60 caret-transparent overflow-hidden"
|
||||||
autocapitalize="off" autocorrect="off" spellcheck="false">
|
autocapitalize="off" autocorrect="off" spellcheck="false">
|
||||||
<span aria-hidden="true" class="cursor -ml-[0.6em]"></span>
|
<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
@@ -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: [] }, "")
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user