Improve command input and "how it works" animation

Fixes cursor positioning errors by explicitly managing the space between the command prompt and user input, and enhances the "how it works" animation with glitch effects and a more dynamic visual sequence.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 6def8112-39d2-4641-b93b-f39108179f33
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 9956cf72-7ee5-4600-91e5-0d22e4fbc583
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/42ae33dd-8759-4196-85a5-434465c72ece/6def8112-39d2-4641-b93b-f39108179f33/HyFeWjl
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
keshavananddev
2026-04-24 03:52:04 +00:00
parent ac056ef7fb
commit 4d0c742b02
4 changed files with 72 additions and 27 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -14,7 +14,7 @@
<main class="stage"> <main class="stage">
<div class="command-box" id="commandBox"> <div class="command-box" id="commandBox">
<span class="dollar">$</span><span class="typed" id="typed"></span><span class="user-input" id="userInput"></span><span class="caret" id="caret">&#9608;</span> <span class="dollar">$</span><span class="typed" id="typed"></span><span class="prompt-space" aria-hidden="true"> </span><span class="user-input" id="userInput"></span><span class="caret" id="caret" aria-hidden="true"></span>
<button class="action-btn" id="actionBtn" aria-label="Copy to clipboard"> <button class="action-btn" id="actionBtn" aria-label="Copy to clipboard">
<svg id="actionIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg id="actionIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path id="iconPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> <path id="iconPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />

View File

@@ -1,7 +1,6 @@
import './style.css'; import './style.css';
const COMMAND = 'ssh portfolio@keshavanand.net'; const COMMAND = 'ssh portfolio@keshavanand.net';
const PROMPT_TAIL = ' '; // single space between prefix and user input
const typedEl = document.getElementById('typed') as HTMLSpanElement; const typedEl = document.getElementById('typed') as HTMLSpanElement;
const userEl = document.getElementById('userInput') as HTMLSpanElement; const userEl = document.getElementById('userInput') as HTMLSpanElement;
@@ -58,7 +57,7 @@ function typeIntro(target: string, onDone?: () => void) {
function nextChar() { function nextChar() {
if (i >= target.length) { if (i >= target.length) {
typedEl.textContent = resolved + PROMPT_TAIL; typedEl.textContent = resolved;
onDone?.(); onDone?.();
return; return;
} }
@@ -82,7 +81,7 @@ function typeIntro(target: string, onDone?: () => void) {
} }
if (reduceMotion) { if (reduceMotion) {
typedEl.textContent = COMMAND + PROMPT_TAIL; typedEl.textContent = COMMAND;
inputEnabled = true; inputEnabled = true;
} else { } else {
setTimeout(() => typeIntro(COMMAND, () => { setTimeout(() => typeIntro(COMMAND, () => {
@@ -206,40 +205,68 @@ commandBox.addEventListener('click', (e) => {
/* ---------- auto-type from "how it works" link ---------- */ /* ---------- auto-type from "how it works" link ---------- */
function rand<T>(a: ArrayLike<T>): T {
return a[Math.floor(Math.random() * a.length)];
}
async function autoTypeHelp() { async function autoTypeHelp() {
if (!inputEnabled || isAutoTyping) return; if (!inputEnabled || isAutoTyping) return;
isAutoTyping = true; isAutoTyping = true;
// clear whatever the user had typed // clear current input, arm the box
userInput = ''; userInput = '';
refreshUI(); refreshUI();
commandBox.classList.add('armed'); commandBox.classList.add('armed');
await sleep(120); // ripple from where the user clicked the link
const linkRect = docsLink.getBoundingClientRect();
spawnRipple(linkRect.left + linkRect.width / 2, linkRect.top + linkRect.height / 2);
const text = '-h'; await sleep(90);
for (const ch of text) {
// small scramble for flair, then settle // PHASE 1 — heavy glitch flicker across both character slots,
let cycles = 2; // with a tiny box jitter for chromatic-aberration energy
while (cycles > 0) { commandBox.classList.add('glitch');
const sc = SCRAMBLE[Math.floor(Math.random() * SCRAMBLE.length)]; const glitchSteps = 14;
userEl.textContent = userInput + sc; for (let s = 0; s < glitchSteps; s++) {
await sleep(38); const a = rand(SCRAMBLE);
cycles--; const b = Math.random() > 0.4 ? rand(SCRAMBLE) : '';
userEl.textContent = a + b;
// micro-jitter the box
const jx = (Math.random() - 0.5) * 3;
const jy = (Math.random() - 0.5) * 1.5;
commandBox.style.transform = `translate(${jx.toFixed(2)}px, ${jy.toFixed(2)}px)`;
await sleep(28 + Math.random() * 18);
}
commandBox.style.transform = '';
commandBox.classList.remove('glitch');
// PHASE 2 — settle to "-h" with a brief micro-flicker on each char
userInput = '';
for (const ch of '-h') {
// 1-2 quick scramble flickers, then resolve
for (let c = 0; c < 2; c++) {
userEl.textContent = userInput + rand(SCRAMBLE);
await sleep(34);
} }
userInput += ch; userInput += ch;
refreshUI(); refreshUI();
await sleep(70); await sleep(60);
} }
// brief pause so the user reads it // PHASE 3 — quick read pause + ripple from the box itself
await sleep(260); await sleep(220);
const boxRect = commandBox.getBoundingClientRect();
spawnRipple(boxRect.right - 24, boxRect.top + boxRect.height / 2);
// visual "Enter pressed" — pulse the action button // PHASE 4 — Enter "press": punch the action icon down + a confirming ripple
actionBtn.style.transform = 'scale(0.85)'; actionBtn.style.transition = 'transform 90ms ease-out';
setTimeout(() => { actionBtn.style.transform = ''; }, 140); actionBtn.style.transform = 'scale(0.78)';
await sleep(110);
await sleep(180); actionBtn.style.transform = 'scale(1)';
await sleep(140);
actionBtn.style.transition = '';
actionBtn.style.transform = '';
isAutoTyping = false; isAutoTyping = false;
submit(); submit();

View File

@@ -111,6 +111,18 @@ body {
box-shadow: 0 0 0 1px var(--accent-soft), 0 0 28px rgba(166, 227, 161, 0.18); box-shadow: 0 0 0 1px var(--accent-soft), 0 0 28px rgba(166, 227, 161, 0.18);
} }
/* glitch state — subtle chromatic-aberration shadow on the typed text */
.command-box.glitch {
border-color: var(--accent-strong);
box-shadow: 0 0 0 1px rgba(166, 227, 161, 0.10), 0 0 32px rgba(166, 227, 161, 0.10);
}
.command-box.glitch .typed,
.command-box.glitch .user-input {
text-shadow:
-1px 0 rgba(243, 139, 168, 0.7),
1px 0 rgba(137, 180, 250, 0.7);
}
.dollar { .dollar {
color: var(--accent); color: var(--accent);
font-weight: 600; font-weight: 600;
@@ -122,17 +134,23 @@ body {
white-space: pre; white-space: pre;
} }
.prompt-space {
white-space: pre;
}
.user-input { .user-input {
color: var(--fg); color: var(--fg);
white-space: pre; white-space: pre;
} }
/* a real terminal block caret — exactly one monospace char wide, no offset */
.caret { .caret {
display: inline-block; display: inline-block;
color: var(--accent); width: 0.6em;
font-size: 0.85em; height: 1.05em;
transform: translateY(-1px); background: var(--accent);
margin-left: 1px; vertical-align: text-bottom;
margin-bottom: 0.05em;
animation: blink 1.05s steps(1, end) infinite; animation: blink 1.05s steps(1, end) infinite;
} }