Compare commits

..

12 Commits

Author SHA1 Message Date
a09ed8c306 changed commtets 2026-04-23 23:13:00 -05:00
981dca9c3a docs fix titel 2026-04-23 23:11:47 -05:00
7f934b0d7b almost done 2026-04-23 23:05:37 -05:00
keshavananddev
aae0a9d8d6 Restore original documentation text and styling
Reverts the documentation page title, headings, and descriptive text to their original content while retaining the updated terminal-inspired visual styling, animations, and interactive elements.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 6def8112-39d2-4641-b93b-f39108179f33
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 6d69fc29-636f-4e74-b7ec-1c1c3972bb85
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/42ae33dd-8759-4196-85a5-434465c72ece/6def8112-39d2-4641-b93b-f39108179f33/OQEOUI5
Replit-Helium-Checkpoint-Created: true
2026-04-24 04:01:29 +00:00
keshavananddev
9de9cca1aa Unify website and documentation pages with a consistent terminal aesthetic
Extract shared canvas animation module, refactor docs page to a terminal man-page style, and align overall visual language.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 6def8112-39d2-4641-b93b-f39108179f33
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 87e11a5a-0718-4680-a1d4-0d2c471eca80
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/42ae33dd-8759-4196-85a5-434465c72ece/6def8112-39d2-4641-b93b-f39108179f33/Mumw6Ni
Replit-Helium-Checkpoint-Created: true
2026-04-24 03:59:27 +00:00
keshavananddev
8b307b42b3 Adjust typing animation and command display spacing
Refactor HTML structure for command input to use a single inline container with `white-space: pre` to precisely control spacing, and adjust JavaScript to add a trailing space to the typed text and increase the pause before the enter key press in the auto-type animation.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 6def8112-39d2-4641-b93b-f39108179f33
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3f816ee5-1289-41ac-aa73-2bd1494876d3
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/42ae33dd-8759-4196-85a5-434465c72ece/6def8112-39d2-4641-b93b-f39108179f33/roUqm0y
Replit-Helium-Checkpoint-Created: true
2026-04-24 03:55:18 +00:00
keshavananddev
4d0c742b02 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
2026-04-24 03:52:04 +00:00
keshavananddev
ac056ef7fb Update command line interface with interactive typing and visual feedback
Refactors the command line interface to include an interactive SSH command input with real-time visual feedback, dynamic button icons, and animated responses to user commands, including navigation to documentation.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 6def8112-39d2-4641-b93b-f39108179f33
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 49353dc6-fccb-41d3-8b94-6617238c72ba
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/42ae33dd-8759-4196-85a5-434465c72ece/6def8112-39d2-4641-b93b-f39108179f33/tF7dS0e
Replit-Helium-Checkpoint-Created: true
2026-04-24 03:48:17 +00:00
keshavananddev
9c878fb1d0 Enhance typing animation and background interaction with subtle new elements
Speed up the typing animation, introduce click-based ripple effects on the background dots, and replace the "how it works" link with a minimal "-h" flag and a faint byline.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 6def8112-39d2-4641-b93b-f39108179f33
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 6dcd9377-c004-4fa1-82a2-b1e55f7e9e41
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/42ae33dd-8759-4196-85a5-434465c72ece/6def8112-39d2-4641-b93b-f39108179f33/iilSvgd
Replit-Helium-Checkpoint-Created: true
2026-04-24 03:42:49 +00:00
keshavananddev
d5fde28b15 Improve homepage by adding interactive cursor effects and animation
Implement a canvas-based, cursor-reactive dot field background and update the typewriter animation to include a scramble effect.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 6def8112-39d2-4641-b93b-f39108179f33
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3df64324-7337-48d0-9099-7bab54585b37
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/42ae33dd-8759-4196-85a5-434465c72ece/6def8112-39d2-4641-b93b-f39108179f33/8hzGboW
Replit-Helium-Checkpoint-Created: true
2026-04-24 03:37:24 +00:00
keshavananddev
b79bf1d4ca Refine homepage design for a clean, minimalist, and handcrafted aesthetic
Replace the matrix rain animation with a subtle dot grid and typewriter effect, update Replit configuration, and add a project description.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 6def8112-39d2-4641-b93b-f39108179f33
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 2dc4d365-c6bd-42cd-afc9-bdc161d87351
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/42ae33dd-8759-4196-85a5-434465c72ece/6def8112-39d2-4641-b93b-f39108179f33/PurqiOp
Replit-Helium-Checkpoint-Created: true
2026-04-24 03:33:53 +00:00
4b0e449e68 test 2026-04-13 20:07:02 -05:00
12 changed files with 1113 additions and 418 deletions

35
.replit Normal file
View File

@@ -0,0 +1,35 @@
modules = ["nodejs-20"]
[nix]
channel = "stable-25_05"
[workflows]
runButton = "Project"
[[workflows.workflow]]
name = "Project"
mode = "parallel"
author = "agent"
[[workflows.workflow.tasks]]
task = "workflow.run"
args = "Start application"
[[workflows.workflow]]
name = "Start application"
author = "agent"
[[workflows.workflow.tasks]]
task = "shell.exec"
args = "npm run dev"
waitForPort = 5000
[workflows.workflow.metadata]
outputType = "webview"
[[ports]]
localPort = 5000
externalPort = 80
[agent]
expertMode = true

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

123
docs.html
View File

@@ -3,99 +3,102 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documentation - Terminal Portfolio</title>
<title>Docs</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="header">
<a href="/" class="back-link">$ cd ..</a>
<h1>Documentation</h1>
<p class="subtitle">The who, what, when, where, why</p>
</div>
<body class="docs">
<canvas id="field" aria-hidden="true"></canvas>
<div class="command-reference">
<div class="command-box">
<code id="sshCommand">ssh portfolio@keshavanand.net</code>
<button class="copy-btn" id="copyBtn" aria-label="Copy command">
<svg class="copy-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<svg class="check-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"></polyline>
<main class="page">
<a href="/" class="back">$ cd ..</a>
<header class="head">
<h1>Documentation</h1>
<p class="sub">The who, what, when, where, why</p>
</header>
<div class="rule" aria-hidden="true"></div>
<div class="command-box" id="commandBox">
<span class="line"><span class="dollar">$ </span><span class="typed">ssh portfolio@keshavanand.net</span></span>
<button class="action-btn" id="copyBtn" aria-label="Copy command">
<svg id="copyIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path id="copyPath" 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" />
</svg>
</button>
</div>
</div>
<div class="sections">
<section class="section">
<h2>What is it?</h2>
<section class="block">
<h2><span class="hash">#</span> What is it?</h2>
<p>
When executed in your terminal, this command renders a fully interactive shell portfolio experience.
Much like a digital resume or personal site, it allows you to explore my work and background through
a safe, <strong>secure</strong>, and purely text-based interface.
<strong>It is completely harmless to your system.</strong>
a safe, <span class="fg">secure</span>, and purely text-based interface.
<span class="fg">It is completely harmless to your system.</span>
</p>
</section>
<section class="section">
<h2>Command Breakdown</h2>
<div class="breakdown-grid">
<div class="breakdown-item">
<h3>ssh →</h3>
<p><strong>Secure Shell:</strong> The standard terminal protocol for logging into and controlling remote computers safely.</p>
<section class="block">
<h2><span class="hash">#</span> Command Breakdown</h2>
<dl class="defs">
<div class="def">
<dt><span class="accent">ssh</span> <span class="arrow"></span></dt>
<dd><span class="fg">Secure Shell:</span> The standard terminal protocol for logging into and controlling remote computers safely.</dd>
</div>
<div class="breakdown-item">
<h3>portfolio →</h3>
<p><strong>User:</strong> A passwordless, restricted user on my homeserver meant for public access.</p>
</div>
<div class="breakdown-item">
<h3>keshavanand.net →</h3>
<p><strong>Domain:</strong> Points to a web record that directs your request to my server's public IP (gateway).</p>
<div class="def">
<dt><span class="accent">portfolio</span> <span class="arrow"></span></dt>
<dd><span class="fg">User:</span> A passwordless, restricted user on my homeserver meant for public access.</dd>
</div>
<div class="def">
<dt><span class="accent">keshavanand.net</span> <span class="arrow"></span></dt>
<dd><span class="fg">Domain:</span> Points to a web record that directs your request to my server's public IP (gateway).</dd>
</div>
</dl>
</section>
<section class="section">
<h2>How to Run</h2>
<section class="block">
<h2><span class="hash">#</span> How to Run</h2>
<ol class="steps">
<li>Copy the command above using the clipboard tool.</li>
<li>Open your native terminal (Terminal on Mac/Linux, PowerShell on Windows).</li>
<li>Paste the command and accept the host identity by typing 'yes' if prompted.</li>
<li>Press Enter to launch the interactive environment.</li>
<li>Terminate the process at any time by pressing <kbd>Ctrl + C</kbd>.</li>
<li><span class="step">01</span> Copy the command above using the clipboard tool.</li>
<li><span class="step">02</span> Open your native terminal (Terminal on Mac/Linux, PowerShell on Windows).</li>
<li><span class="step">03</span> Paste the command and accept the host identity by typing 'yes' if prompted.</li>
<li><span class="step">04</span> Press Enter to launch the interactive environment.</li>
<li><span class="step">05</span> Terminate the process at any time by pressing <kbd>Ctrl</kbd>+<kbd>C</kbd>.</li>
</ol>
</section>
<section class="section">
<h2>Code Logic</h2>
<section class="block">
<h2><span class="hash">#</span> Code Logic</h2>
<p>
This command gives authenticated access to the <strong>'portfolio'</strong> user account on my private home server.
This command gives authenticated access to the <span class="fg">'portfolio'</span> user account on my private home server.
</p>
<p>
Instead of a standard bash or zsh shell, the user session triggers a custom-coded <strong>C++ executable</strong>.
This binary utilizes <strong>FTXUI</strong>—a sophisticated functional terminal user interface library—to handle
Instead of a standard bash or zsh shell, the user session triggers a custom-coded <span class="fg">C++ executable</span>.
This binary utilizes <span class="fg">FTXUI</span>—a sophisticated functional terminal user interface library—to handle
real-time rendering and input.
</p>
</section>
<section class="section">
<h2>Resources</h2>
<div class="resources-grid">
<a href="https://git.keshavanand.net/KeshavAnandCode/terminal-portfolio" target="_blank" rel="noopener noreferrer" class="resource-card">
<span>View Source Code</span>
<section class="block">
<h2><span class="hash">#</span> Resources</h2>
<ul class="links">
<li>
<a href="https://git.keshavanand.net/KeshavAnandCode/terminal-portfolio" target="_blank" rel="noopener noreferrer">
<span class="arrow"></span> View Source Code
</a>
</li>
<li>
<a href="/webshell">
<span class="arrow"></span> Run Web Shell
</a>
</li>
</ul>
</section>
</main>
<a href="/webshell" class="resource-card">
<span>Run Web Shell</span>
</a>
</div>
</div>
</div>
<div class="byline">// using vibe code to present human code</div>
<script type="module" src="/src/docs.ts"></script>
</body>

View File

@@ -10,23 +10,23 @@
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
</head>
<body>
<canvas id="matrix"></canvas>
<div class="container">
<div class="command-container">
<canvas id="field" aria-hidden="true"></canvas>
<main class="stage">
<div class="command-box" id="commandBox">
<span class="dollar">$</span>
<span class="command">ssh portfolio@keshavanand.net</span>
</div>
<button class="copy-btn" id="copyBtn" aria-label="Copy to clipboard">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path 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" />
<span class="line"><span class="dollar">$ </span><span class="typed" id="typed"></span><span class="user-input" id="userInput"></span><span class="caret" id="caret" aria-hidden="true"></span></span>
<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">
<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" />
</svg>
</button>
<div class="tooltip" id="tooltip">Copied!</div>
</div>
<a href="/docs.html" class="docs-link" id="docsLink">how it works</a>
</div>
<a href="/docs.html" class="docs-hint" id="docsLink">how it works</a>
</main>
<span class="byline">// using vibe code to present human code</span>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

30
replit.md Normal file
View File

@@ -0,0 +1,30 @@
# Terminal Portfolio
A minimalist landing page that points visitors to an SSH-accessible portfolio (`ssh portfolio@keshavanand.net`). The webshell behind the SSH endpoint is a separate concern; this repo is just the marketing/landing surface.
## Stack
- Vite + TypeScript (vanilla, no framework)
- Plain CSS, JetBrains Mono via Google Fonts
- Two pages: `index.html` (homepage) and `docs.html` (how it works)
## Structure
- `index.html` — homepage markup
- `src/main.ts` — homepage typewriter + copy-to-clipboard logic
- `src/style.css` — homepage styles (dark, dot-grid background, soft glow)
- `docs.html` / `src/docs.ts` / `src/docs.css` — docs page
- `vite.config.ts` — multi-page build config; dev server on `0.0.0.0:5000` with `allowedHosts: true` for Replit
## Design notes
The homepage intentionally avoids busy background animation. It uses:
- A static CSS dot grid + a single soft radial glow (no JS animation loop)
- A short typewriter intro for the SSH command with a humanized cadence
- A blinking block caret that persists after typing
- Subtle opacity-only fade-ins (no transform-based slide-ins)
- Honors `prefers-reduced-motion`
## Run
- Workflow `Start application` runs `npm run dev` on port 5000.

View File

@@ -1,11 +1,13 @@
:root {
--base: #000000;
--text: #cdd6f4;
--subtext0: #a6adc8;
--surface0: #1a1a1a;
--surface1: #2a2a2a;
--green: #a6e3a1;
--blue: #89b4fa;
--bg: #0b0b0d;
--fg: #e6e6e6;
--dim: #6a6a72;
--dimmer: #4a4a52;
--line: #1c1c20;
--accent: #a6e3a1;
--accent-soft: rgba(166, 227, 161, 0.22);
--accent-strong: rgba(166, 227, 161, 0.55);
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}
* {
@@ -14,228 +16,304 @@
box-sizing: border-box;
}
body {
font-family: "JetBrains Mono", monospace;
background-color: var(--base);
color: var(--text);
min-height: 100vh;
padding: 2rem 1.5rem;
html { height: 100%; }
body.docs {
font-family: var(--mono);
background: var(--bg);
color: var(--fg);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100%;
overflow-x: hidden;
overflow-y: auto;
position: relative;
line-height: 1.7;
}
.container {
max-width: 700px;
margin: 0 auto;
}
.header {
margin-bottom: 3rem;
}
.back-link {
color: var(--subtext0);
text-decoration: none;
font-size: 0.9rem;
margin-bottom: 2rem;
display: inline-block;
transition: color 0.3s ease;
}
.back-link:hover {
color: var(--green);
}
h1 {
color: var(--text);
font-size: 2.5rem;
margin: 1rem 0 0.5rem 0;
font-weight: 600;
}
.subtitle {
color: var(--subtext0);
font-size: 1rem;
}
.command-reference {
margin: 2rem 0 3rem 0;
}
.command-box {
background-color: rgba(26, 26, 26, 0.8);
border: 1px solid var(--green);
padding: 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.command-box code {
color: var(--green);
font-size: 1.1rem;
flex: 1;
}
.copy-btn {
background: none;
border: none;
color: var(--subtext0);
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
position: relative;
}
.copy-btn:hover {
color: var(--green);
background-color: var(--surface0);
}
.copy-btn .check-icon {
display: none;
color: var(--green);
}
.copy-btn.copied .copy-icon {
display: none;
}
.copy-btn.copied .check-icon {
#field {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
display: block;
}
.sections {
display: flex;
flex-direction: column;
gap: 3rem;
.page {
position: relative;
z-index: 1;
max-width: 720px;
margin: 0 auto;
padding: 3rem 1.75rem 6rem;
opacity: 0;
animation: fade 500ms ease-out 60ms forwards;
}
.section {
@keyframes fade { to { opacity: 1; } }
/* back link — same accent treatment as the home dollar */
.back {
display: inline-block;
color: var(--dim);
text-decoration: none;
font-size: 0.78rem;
letter-spacing: 0.02em;
padding: 0.15rem 0;
transition: color 180ms ease, transform 180ms ease;
}
.back:hover {
color: var(--accent);
transform: translateX(-2px);
}
.head {
margin-top: 2.5rem;
}
h1 {
font-size: 2.2rem;
font-weight: 600;
letter-spacing: 0.005em;
color: var(--fg);
}
.sub {
margin-top: 0.5rem;
color: var(--dim);
font-size: 0.95rem;
letter-spacing: 0.01em;
}
.rule {
height: 1px;
background: var(--line);
margin: 1.6rem 0 1.5rem;
}
/* command box — visually identical to the home version, sans caret/input */
.command-box {
width: 100%;
background: rgba(10, 10, 12, 0.55);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
border: 1px solid var(--line);
border-radius: 4px;
padding: 1.05rem 1.15rem;
font-size: 1.05rem;
font-weight: 500;
color: var(--fg);
display: flex;
align-items: center;
gap: 0;
transition: border-color 200ms ease, box-shadow 200ms ease;
position: relative;
overflow: hidden;
}
.command-box:hover {
border-color: var(--accent-soft);
}
.command-box.copied {
border-color: var(--accent-strong);
box-shadow: 0 0 0 1px rgba(166, 227, 161, 0.10), 0 0 24px rgba(166, 227, 161, 0.10);
}
.line {
flex: 1 1 auto;
min-width: 0;
white-space: pre;
overflow: hidden;
text-overflow: clip;
display: block;
}
.dollar {
color: var(--accent);
font-weight: 600;
}
.typed { color: var(--fg); }
.action-btn {
margin-left: 0.6rem;
background: none;
border: none;
color: var(--dim);
cursor: pointer;
padding: 0.2rem;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
transition: color 180ms ease, transform 180ms ease;
}
.action-btn:hover { color: var(--fg); }
.action-btn svg { width: 16px; height: 16px; }
/* sections — terminal-style "# heading" then prose */
.block {
margin-top: 2.6rem;
}
h2 {
color: var(--green);
font-size: 1.5rem;
margin-bottom: 1.25rem;
font-weight: 600;
}
p {
color: var(--subtext0);
font-size: 1.05rem;
font-weight: 500;
color: var(--fg);
letter-spacing: 0.005em;
margin-bottom: 1rem;
font-size: 0.95rem;
}
strong {
color: var(--text);
h2 .hash {
color: var(--accent);
margin-right: 0.4rem;
font-weight: 600;
}
.breakdown-grid {
.block p {
color: var(--dim);
font-size: 0.88rem;
line-height: 1.75;
}
.block p .fg { color: var(--fg); }
.block p a {
color: var(--accent);
text-decoration: none;
border-bottom: 1px dashed transparent;
transition: border-color 180ms ease;
}
.block p a:hover { border-bottom-color: var(--accent-soft); }
/* code block — quieter, leaner than the command box */
.code {
font-family: var(--mono);
font-size: 0.92rem;
background: rgba(10, 10, 12, 0.4);
border-left: 1px solid var(--accent-soft);
padding: 0.8rem 1rem;
color: var(--fg);
overflow-x: auto;
}
.code .dim { color: var(--dim); margin-right: 0.5rem; }
.code .accent { color: var(--accent); }
/* arguments — definition list, mono leader */
.defs {
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.def {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
grid-template-columns: minmax(160px, 200px) 1fr;
gap: 1rem;
padding: 0.5rem 0;
border-bottom: 1px dashed var(--line);
align-items: baseline;
}
.breakdown-item {
padding: 1rem;
.def:last-child { border-bottom: none; }
.def dt {
font-size: 0.92rem;
font-weight: 500;
display: inline-flex;
align-items: baseline;
gap: 0.5rem;
}
.breakdown-item h3 {
color: var(--blue);
font-size: 1.1rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.breakdown-item p {
.def dt .arrow { color: var(--dim); }
.def dd {
color: var(--dim);
font-size: 0.9rem;
line-height: 1.6;
line-height: 1.7;
}
.def dd .fg { color: var(--fg); font-weight: 500; }
/* steps — numbered, monospace numerals */
.steps {
list-style: none;
counter-reset: step;
margin-top: 1rem;
}
.steps li {
counter-increment: step;
margin-bottom: 1.25rem;
display: flex;
gap: 1rem;
color: var(--subtext0);
font-size: 0.95rem;
flex-direction: column;
gap: 0.55rem;
}
.steps li::before {
content: counter(step, decimal-leading-zero);
color: var(--green);
font-weight: 600;
min-width: 2rem;
.steps li {
color: var(--dim);
font-size: 0.88rem;
line-height: 1.7;
}
.steps .step {
display: inline-block;
color: var(--accent);
margin-right: 0.85rem;
font-weight: 500;
}
kbd {
background-color: var(--surface0);
border: 1px solid var(--surface1);
padding: 0.2rem 0.5rem;
border-radius: 4px;
color: var(--text);
font-size: 0.9em;
font-family: inherit;
font-family: var(--mono);
font-size: 0.78em;
color: var(--fg);
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.03);
padding: 0.05rem 0.35rem;
border-radius: 3px;
margin: 0 0.1rem;
}
.resources-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-top: 1.5rem;
/* see also — link list */
.links {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.resource-card {
display: block;
padding: 0.85rem 1rem;
background-color: rgba(26, 26, 26, 0.5);
border: 1px solid rgba(166, 227, 161, 0.2);
.links a {
display: inline-flex;
align-items: baseline;
gap: 0.55rem;
color: var(--fg);
text-decoration: none;
color: var(--text);
margin: 0.5rem 0;
transition: border-color 0.2s ease, background-color 0.2s ease;
font-size: 0.88rem;
padding: 0.25rem 0;
transition: color 180ms ease, transform 180ms ease;
}
.links a:hover {
color: var(--accent);
transform: translateX(3px);
}
.links .arrow { color: var(--accent); }
/* whisper-quiet byline, identical to home */
.byline {
position: fixed;
left: 50%;
bottom: 1rem;
transform: translateX(-50%);
z-index: 1;
font-family: var(--mono);
font-size: 0.62rem;
letter-spacing: 0.02em;
color: rgba(106, 106, 114, 0.28);
pointer-events: none;
opacity: 0;
animation: fadeWhisper 900ms ease-out 1500ms forwards;
white-space: nowrap;
}
.resource-card:hover {
border-color: var(--green);
background-color: rgba(26, 26, 26, 0.7);
}
@keyframes fadeWhisper { to { opacity: 1; } }
.resource-card span {
font-size: 0.9rem;
}
@media (max-width: 768px) {
body {
padding: 1.5rem 1rem;
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.3rem;
}
.command-box code {
font-size: 0.9rem;
}
.breakdown-grid,
.resources-grid {
@media (max-width: 640px) {
.page { padding: 2rem 1.25rem 5rem; }
h1 { font-size: 1.35rem; }
.command-box { font-size: 0.92rem; padding: 0.9rem 1rem; }
.def {
grid-template-columns: 1fr;
gap: 0.25rem;
}
.byline { font-size: 0.56rem; }
}
@media (prefers-reduced-motion: reduce) {
.page, .byline { animation: none; opacity: 1; }
}

View File

@@ -1,16 +1,41 @@
import './docs.css';
import { initField } from './field';
const copyBtn = document.getElementById('copyBtn') as HTMLButtonElement;
const command = 'ssh portfolio@keshavanand.net';
initField('field');
if (copyBtn) {
copyBtn.addEventListener('click', async () => {
const COMMAND = 'ssh portfolio@keshavanand.net';
const copyBtn = document.getElementById('copyBtn') as HTMLButtonElement | null;
const commandBox = document.getElementById('commandBox') as HTMLDivElement | null;
const copyPath = document.getElementById('copyPath') as unknown as SVGPathElement | null;
const COPY_PATH =
'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';
const CHECK_PATH = 'M5 13l4 4L19 7';
let resetTimer: number | undefined;
async function copy() {
if (!commandBox || !copyPath) return;
try {
await navigator.clipboard.writeText(command);
copyBtn.classList.add('copied');
setTimeout(() => copyBtn.classList.remove('copied'), 2000);
} catch (err) {
console.error('Failed to copy:', err);
await navigator.clipboard.writeText(COMMAND);
copyPath.setAttribute('d', CHECK_PATH);
commandBox.classList.add('copied');
window.clearTimeout(resetTimer);
resetTimer = window.setTimeout(() => {
copyPath.setAttribute('d', COPY_PATH);
commandBox.classList.remove('copied');
}, 1400);
} catch {
/* clipboard blocked */
}
}
copyBtn?.addEventListener('click', (e) => {
e.stopPropagation();
copy();
});
commandBox?.addEventListener('click', (e) => {
if ((e.target as HTMLElement).closest('.action-btn')) return;
copy();
});
}

205
src/field.ts Normal file
View File

@@ -0,0 +1,205 @@
type Dot = { bx: number; by: number; ox: number; oy: number };
type Ripple = { x: number; y: number; born: number };
const SPACING = 26;
const RADIUS = 170;
const PUSH = 18;
const RIPPLE_LIFE = 900;
const RIPPLE_MAX = 520;
const RIPPLE_BAND = 70;
const RIPPLE_PUSH = 36;
const BASE_RGB = '230, 230, 230';
const ACCENT_RGB = '166, 227, 161';
const BASE_ALPHA = 0.11;
const PEAK_ALPHA = 0.9;
let canvas: HTMLCanvasElement | null = null;
let ctx: CanvasRenderingContext2D | null = null;
let dots: Dot[] = [];
const ripples: Ripple[] = [];
let dpr = 1;
let w = 0;
let h = 0;
let targetX = -9999;
let targetY = -9999;
let curX = -9999;
let curY = -9999;
let active = false;
let started = false;
function build() {
if (!canvas || !ctx) return;
dpr = Math.max(1, window.devicePixelRatio || 1);
w = window.innerWidth;
h = window.innerHeight;
canvas.width = Math.floor(w * dpr);
canvas.height = Math.floor(h * dpr);
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
dots = [];
const pad = SPACING;
for (let y = -pad; y < h + pad; y += SPACING) {
for (let x = -pad; x < w + pad; x += SPACING) {
dots.push({ bx: x, by: y, ox: 0, oy: 0 });
}
}
}
function onMove(x: number, y: number) {
targetX = x;
targetY = y;
active = true;
}
export function spawnRipple(x: number, y: number) {
ripples.push({ x, y, born: performance.now() });
if (ripples.length > 8) ripples.shift();
}
function frame() {
if (!ctx) return;
const now = performance.now();
if (!active) {
targetX = -9999;
targetY = -9999;
}
curX += (targetX - curX) * 0.18;
curY += (targetY - curY) * 0.18;
for (let i = ripples.length - 1; i >= 0; i--) {
if (now - ripples[i].born > RIPPLE_LIFE) ripples.splice(i, 1);
}
const rippleState = ripples.map((rp) => {
const p = (now - rp.born) / RIPPLE_LIFE;
return { x: rp.x, y: rp.y, radius: p * RIPPLE_MAX, fade: 1 - p };
});
ctx.clearRect(0, 0, w, h);
const r2 = RADIUS * RADIUS;
for (let i = 0; i < dots.length; i++) {
const d = dots[i];
const dx = d.bx - curX;
const dy = d.by - curY;
const d2 = dx * dx + dy * dy;
let tx = 0;
let ty = 0;
let strength = 0;
if (d2 < r2) {
const dist = Math.sqrt(d2) || 0.0001;
const t = 1 - dist / RADIUS;
strength = t * t;
tx = (dx / dist) * strength * PUSH;
ty = (dy / dist) * strength * PUSH;
}
let rippleStrength = 0;
for (let k = 0; k < rippleState.length; k++) {
const rp = rippleState[k];
const rdx = d.bx - rp.x;
const rdy = d.by - rp.y;
const rdist = Math.sqrt(rdx * rdx + rdy * rdy) || 0.0001;
const delta = Math.abs(rdist - rp.radius);
if (delta < RIPPLE_BAND) {
const ringT = 1 - delta / RIPPLE_BAND;
const s = ringT * ringT * rp.fade;
tx += (rdx / rdist) * s * RIPPLE_PUSH;
ty += (rdy / rdist) * s * RIPPLE_PUSH;
if (s > rippleStrength) rippleStrength = s;
}
}
d.ox += (tx - d.ox) * 0.22;
d.oy += (ty - d.oy) * 0.22;
const px = d.bx + d.ox;
const py = d.by + d.oy;
const visStrength = Math.min(1, strength + rippleStrength);
const alpha = BASE_ALPHA + (PEAK_ALPHA - BASE_ALPHA) * visStrength;
const size = 1 + visStrength * 1.6;
if (visStrength > 0.04) {
const blend = visStrength;
const r = Math.round(230 * (1 - blend) + 166 * blend);
const g = Math.round(230 * (1 - blend) + 227 * blend);
const b = Math.round(230 * (1 - blend) + 161 * blend);
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
} else {
ctx.fillStyle = `rgba(${BASE_RGB}, ${alpha})`;
}
ctx.fillRect(px - size / 2, py - size / 2, size, size);
}
if (active) {
const grad = ctx.createRadialGradient(curX, curY, 0, curX, curY, RADIUS);
grad.addColorStop(0, `rgba(${ACCENT_RGB}, 0.10)`);
grad.addColorStop(1, `rgba(${ACCENT_RGB}, 0)`);
ctx.fillStyle = grad;
ctx.fillRect(curX - RADIUS, curY - RADIUS, RADIUS * 2, RADIUS * 2);
}
for (let k = 0; k < rippleState.length; k++) {
const rp = rippleState[k];
if (rp.radius < 4) continue;
ctx.strokeStyle = `rgba(${ACCENT_RGB}, ${0.16 * rp.fade})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(rp.x, rp.y, rp.radius, 0, Math.PI * 2);
ctx.stroke();
}
requestAnimationFrame(frame);
}
export function initField(canvasId = 'field') {
if (started) return;
const el = document.getElementById(canvasId) as HTMLCanvasElement | null;
if (!el) return;
canvas = el;
ctx = canvas.getContext('2d');
if (!ctx) return;
started = true;
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
build();
window.addEventListener('mousemove', (e) => onMove(e.clientX, e.clientY), { passive: true });
window.addEventListener('mouseleave', () => { active = false; });
window.addEventListener('click', (e) => spawnRipple(e.clientX, e.clientY));
window.addEventListener('touchmove', (e) => {
if (e.touches[0]) onMove(e.touches[0].clientX, e.touches[0].clientY);
}, { passive: true });
window.addEventListener('touchstart', (e) => {
if (e.touches[0]) spawnRipple(e.touches[0].clientX, e.touches[0].clientY);
}, { passive: true });
window.addEventListener('touchend', () => { active = false; });
window.addEventListener('resize', build);
window.addEventListener('scroll', () => {
// keep ripples / dots independent of page scroll — recompute baselines
}, { passive: true });
if (reduceMotion) {
if (!ctx) return;
ctx.fillStyle = `rgba(${BASE_RGB}, ${BASE_ALPHA})`;
for (const d of dots) ctx.fillRect(d.bx, d.by, 1, 1);
} else {
requestAnimationFrame(frame);
}
}

View File

@@ -1,35 +1,286 @@
import './style.css';
import { initField, spawnRipple } from './field';
// Copy functionality
const commandBox = document.getElementById('commandBox') as HTMLDivElement;
const copyBtn = document.getElementById('copyBtn') as HTMLButtonElement;
const tooltip = document.getElementById('tooltip') as HTMLDivElement;
const COMMAND = 'ssh portfolio@keshavanand.net';
async function copyToClipboard() {
try {
await navigator.clipboard.writeText(COMMAND);
copyBtn.classList.add('copied');
tooltip.classList.add('show');
setTimeout(() => {
copyBtn.classList.remove('copied');
tooltip.classList.remove('show');
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
const typedEl = document.getElementById('typed') as HTMLSpanElement;
const userEl = document.getElementById('userInput') as HTMLSpanElement;
const caretEl = document.getElementById('caret') as HTMLSpanElement;
const commandBox = document.getElementById('commandBox') as HTMLDivElement;
const actionBtn = document.getElementById('actionBtn') as HTMLButtonElement;
const iconPath = document.getElementById('iconPath') as unknown as SVGPathElement;
const docsLink = document.getElementById('docsLink') as HTMLAnchorElement;
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
/* ---------- icons ---------- */
const COPY_PATH =
'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';
// "↵" return arrow — a hooked left arrow (very recognizable as Enter)
const ENTER_PATH = 'M9 10l-5 5m0 0l5 5m-5-5h12a4 4 0 004-4V5';
const CHECK_PATH = 'M5 13l4 4L19 7';
function setIcon(d: string) {
iconPath.setAttribute('d', d);
}
/* ---------- state ---------- */
let userInput = '';
let inputEnabled = false; // true once typewriter completes
let isAutoTyping = false; // true while clicking-how-it-works animation runs
function refreshUI() {
userEl.textContent = userInput;
if (userInput.length > 0) {
setIcon(ENTER_PATH);
actionBtn.setAttribute('aria-label', 'Run command');
commandBox.classList.add('armed');
} else {
setIcon(COPY_PATH);
actionBtn.setAttribute('aria-label', 'Copy command');
commandBox.classList.remove('armed');
}
}
copyBtn.addEventListener('click', (e) => {
/* ---------- typewriter intro (with scramble) ---------- */
const SCRAMBLE = '!<>-_\\/[]{}—=+*^?#abcdef0123456789';
function sleep(ms: number) {
return new Promise<void>((r) => setTimeout(r, ms));
}
function typeIntro(target: string, onDone?: () => void) {
let resolved = '';
let i = 0;
function nextChar() {
if (i >= target.length) {
// append a trailing space so the caret rests one char past the prompt
typedEl.textContent = resolved + ' ';
onDone?.();
return;
}
let cycles = target[i] === ' ' ? 0 : 1 + Math.floor(Math.random() * 2);
function flicker() {
if (cycles <= 0) {
resolved += target[i];
typedEl.textContent = resolved;
i++;
setTimeout(nextChar, 8 + Math.random() * 18);
return;
}
const ch = SCRAMBLE[Math.floor(Math.random() * SCRAMBLE.length)];
typedEl.textContent = resolved + ch;
cycles--;
setTimeout(flicker, 14);
}
flicker();
}
nextChar();
}
if (reduceMotion) {
typedEl.textContent = COMMAND + ' ';
inputEnabled = true;
} else {
setTimeout(() => typeIntro(COMMAND, () => {
inputEnabled = true;
}), 180);
}
/* ---------- copy ---------- */
let iconResetTimer: number | undefined;
async function copyCommand() {
try {
await navigator.clipboard.writeText(COMMAND);
setIcon(CHECK_PATH);
window.clearTimeout(iconResetTimer);
iconResetTimer = window.setTimeout(() => {
// only reset if user hasn't started typing in the meantime
if (userInput.length === 0) setIcon(COPY_PATH);
}, 1100);
} catch {
/* clipboard can be blocked */
}
}
/* ---------- submit ---------- */
function submit() {
const cmd = userInput.trim().toLowerCase();
if (cmd === '') return;
if (cmd === '-h' || cmd === '--help' || cmd === 'h' || cmd === 'help') {
// brief flash, then navigate
commandBox.classList.add('submit');
setTimeout(() => {
window.location.href = '/docs.html';
}, 260);
return;
}
if (cmd === 'clear' || cmd === 'cls') {
userInput = '';
refreshUI();
return;
}
// unknown command — shake, then clear after a beat
commandBox.classList.remove('shake');
// force reflow so the animation restarts on rapid presses
void commandBox.offsetWidth;
commandBox.classList.add('shake');
setTimeout(() => {
commandBox.classList.remove('shake');
userInput = '';
refreshUI();
}, 420);
}
/* ---------- keyboard input (after typewriter completes) ---------- */
function onKeydown(e: KeyboardEvent) {
if (!inputEnabled || isAutoTyping) return;
// let browser shortcuts pass
if (e.metaKey || e.ctrlKey || e.altKey) return;
// ignore navigation keys we don't care about
if (
e.key === 'Tab' || e.key === 'ArrowLeft' || e.key === 'ArrowRight' ||
e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Shift' ||
e.key === 'CapsLock' || e.key === 'Meta' || e.key === 'Control' ||
e.key === 'Alt'
) return;
if (e.key === 'Enter') {
e.preventDefault();
submit();
return;
}
if (e.key === 'Backspace') {
e.preventDefault();
if (userInput.length > 0) {
userInput = userInput.slice(0, -1);
refreshUI();
}
return;
}
if (e.key === 'Escape') {
e.preventDefault();
userInput = '';
refreshUI();
return;
}
if (e.key.length === 1) {
e.preventDefault();
// cap input length so it doesn't overflow
if (userInput.length < 32) {
userInput += e.key;
refreshUI();
}
}
}
document.addEventListener('keydown', onKeydown);
/* ---------- click handlers on the box / action button ---------- */
function actionClick() {
if (userInput.length > 0) submit();
else copyCommand();
}
actionBtn.addEventListener('click', (e) => {
e.stopPropagation();
copyToClipboard();
actionClick();
});
commandBox.addEventListener('click', (e) => {
if (!(e.target as HTMLElement).closest('.copy-btn')) copyToClipboard();
// don't double-fire when clicking the button
if ((e.target as HTMLElement).closest('.action-btn')) return;
actionClick();
});
// Fade in docs link after 2 seconds
setTimeout(() => {
document.getElementById('docsLink')?.classList.add('show');
}, 2000);
/* ---------- 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() {
if (!inputEnabled || isAutoTyping) return;
isAutoTyping = true;
// clear current input, arm the box
userInput = '';
refreshUI();
commandBox.classList.add('armed');
// ripple from where the user clicked the link
const linkRect = docsLink.getBoundingClientRect();
spawnRipple(linkRect.left + linkRect.width / 2, linkRect.top + linkRect.height / 2);
await sleep(90);
// PHASE 1 — heavy glitch flicker across both character slots,
// with a tiny box jitter for chromatic-aberration energy
commandBox.classList.add('glitch');
const glitchSteps = 14;
for (let s = 0; s < glitchSteps; s++) {
const a = rand(SCRAMBLE);
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;
refreshUI();
await sleep(60);
}
// PHASE 3 — let the user read "-h" before it fires, then ripple from the box
await sleep(850);
const boxRect = commandBox.getBoundingClientRect();
spawnRipple(boxRect.right - 24, boxRect.top + boxRect.height / 2);
// PHASE 4 — Enter "press": punch the action icon down + a confirming ripple
actionBtn.style.transition = 'transform 90ms ease-out';
actionBtn.style.transform = 'scale(0.78)';
await sleep(110);
actionBtn.style.transform = 'scale(1)';
await sleep(140);
actionBtn.style.transition = '';
actionBtn.style.transform = '';
isAutoTyping = false;
submit();
}
docsLink.addEventListener('click', (e) => {
e.preventDefault();
autoTypeHelp();
});
/* ---------- cursor-reactive dot field + click ripples ---------- */
initField('field');
void caretEl;

View File

@@ -1,15 +1,12 @@
:root {
/* Catppuccin Mocha - Darker Hacker Theme */
--base: #000000;
--mantle: #0a0a0a;
--crust: #050505;
--text: #cdd6f4;
--subtext0: #a6adc8;
--surface0: #1a1a1a;
--surface1: #2a2a2a;
--green: #a6e3a1;
--blue: #89b4fa;
--mauve: #cba6f7;
--bg: #0b0b0d;
--fg: #e6e6e6;
--dim: #6a6a72;
--line: #1c1c20;
--accent: #a6e3a1;
--accent-soft: rgba(166, 227, 161, 0.22);
--accent-strong: rgba(166, 227, 161, 0.55);
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}
* {
@@ -18,170 +15,232 @@
box-sizing: border-box;
}
html, body { height: 100%; }
body {
font-family: "JetBrains Mono", monospace;
background-color: var(--base);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 2rem;
line-height: 1.6;
font-family: var(--mono);
background: var(--bg);
color: var(--fg);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
overflow: hidden;
position: relative;
}
.container {
#field {
position: fixed;
inset: 0;
width: 100%;
max-width: 600px;
height: 100%;
z-index: 0;
pointer-events: none;
display: block;
}
.stage {
position: relative;
z-index: 1;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
gap: 3rem;
padding: 2rem;
justify-content: center;
gap: 0.85rem;
max-width: 580px;
margin: 0 auto;
padding: 2rem 1.75rem;
opacity: 0;
animation: fade 500ms ease-out 60ms forwards;
}
.terminal-header {
margin-bottom: 3rem;
text-align: left;
width: 100%;
}
.terminal-line {
color: var(--subtext0);
font-size: 0.95rem;
line-height: 1.8;
margin-bottom: 0.5rem;
}
.prompt {
color: var(--green);
}
.user {
color: var(--blue);
}
.path {
color: var(--mauve);
}
.command-container {
position: relative;
width: 100%;
@keyframes fade {
to { opacity: 1; }
}
.command-box {
background-color: rgba(0, 0, 0, 0.85);
border: 1px solid #a6e3a1;
padding: 1.5rem 2.5rem;
font-size: 1.4rem;
color: var(--text);
width: 100%;
background: rgba(10, 10, 12, 0.55);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
border: 1px solid var(--line);
border-radius: 4px;
padding: 1.05rem 1.15rem;
font-size: 1.05rem;
font-weight: 500;
color: var(--fg);
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
gap: 0;
cursor: text;
user-select: none;
transition: border-color 200ms ease, box-shadow 200ms ease;
position: relative;
overflow: hidden;
}
/* one inline container for all the prompt text — keeps spacing exact */
.line {
flex: 1 1 auto;
min-width: 0;
white-space: pre;
overflow: hidden;
text-overflow: clip;
display: block;
}
.command-box:hover {
border-color: #a6e3a1;
border-color: var(--accent-soft);
}
/* "armed" once user starts typing — invites Enter */
.command-box.armed {
border-color: var(--accent-strong);
box-shadow: 0 0 0 1px rgba(166, 227, 161, 0.08), 0 0 24px rgba(166, 227, 161, 0.06);
}
/* shake on invalid command */
.command-box.shake {
animation: shake 380ms cubic-bezier(.36,.07,.19,.97);
border-color: rgba(243, 139, 168, 0.55);
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
15% { transform: translateX(-4px); }
30% { transform: translateX(4px); }
45% { transform: translateX(-3px); }
60% { transform: translateX(3px); }
80% { transform: translateX(-1px); }
}
/* brief flash before navigating */
.command-box.submit {
border-color: var(--accent);
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 {
color: var(--green);
color: var(--accent);
font-weight: 600;
font-size: 1.3rem;
text-shadow: 0 0 10px rgba(166, 227, 161, 0.5);
}
.command {
color: var(--text);
flex: 1;
.typed {
color: var(--fg);
}
.copy-btn {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
.user-input {
color: var(--fg);
}
/* a real terminal block caret — exactly one monospace char wide, no offset */
.caret {
display: inline-block;
width: 0.6em;
height: 1.05em;
background: var(--accent);
vertical-align: text-bottom;
margin-bottom: 0.05em;
animation: blink 1.05s steps(1, end) infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
50.01%, 100% { opacity: 0; }
}
.action-btn {
margin-left: 0.6rem;
background: none;
border: none;
color: var(--subtext0);
color: var(--dim);
cursor: pointer;
padding: 0.25rem 0.5rem;
display: flex;
padding: 0.2rem;
display: inline-flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
flex: 0 0 auto;
transition: color 180ms ease, transform 180ms ease;
}
.copy-btn:hover {
color: var(--green);
.action-btn:hover { color: var(--fg); }
/* when armed, the action button is the Enter key — pulses gently to invite */
.command-box.armed .action-btn {
color: var(--accent);
animation: pulse 1.6s ease-in-out infinite;
}
.copy-btn.copied {
color: var(--green);
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.08); opacity: 0.85; }
}
.copy-btn svg {
width: 20px;
height: 20px;
.action-btn svg {
width: 16px;
height: 16px;
transition: transform 180ms ease;
}
.tooltip {
position: absolute;
top: -2rem;
right: 0;
background-color: var(--surface1);
color: var(--green);
padding: 0.35rem 0.6rem;
border-radius: 4px;
/* very minimal "how it works" subtitle — even fainter than before */
.docs-hint {
color: rgba(106, 106, 114, 0.45);
text-decoration: none;
font-size: 0.7rem;
letter-spacing: 0.02em;
padding: 0.15rem 0.35rem;
opacity: 0;
animation: fadeFaint 700ms ease-out 1100ms forwards;
transition: color 200ms ease;
cursor: pointer;
}
.docs-hint:hover { color: var(--fg); }
/* whisper-quiet byline, centered along the bottom */
.byline {
position: fixed;
left: 50%;
bottom: 1rem;
transform: translateX(-50%);
z-index: 1;
font-family: var(--mono);
font-size: 0.62rem;
letter-spacing: 0.02em;
color: rgba(106, 106, 114, 0.28);
pointer-events: none;
transition: opacity 0.2s ease;
opacity: 0;
animation: fadeWhisper 900ms ease-out 1500ms forwards;
white-space: nowrap;
}
.tooltip.show {
opacity: 1;
}
@keyframes fadeFaint { to { opacity: 1; } }
@keyframes fadeWhisper { to { opacity: 1; } }
@media (max-width: 640px) {
body {
padding: 1.5rem;
}
.stage { padding: 1.5rem 1.25rem; }
.command-box {
font-size: 1.1rem;
padding: 1.25rem 1.75rem;
font-size: 0.92rem;
padding: 0.9rem 1rem;
}
.docs-hint { font-size: 0.66rem; }
.byline { font-size: 0.56rem; }
}
.copy-btn svg {
width: 16px;
height: 16px;
}
}
.docs-link {
color: var(--subtext0);
text-decoration: none;
font-size: 0.85rem;
opacity: 0;
transition: opacity 0.4s ease;
position: relative;
padding: 0.5rem 1rem;
}
.docs-link:hover {
color: var(--green);
}
.docs-link.show {
opacity: 1;
@media (prefers-reduced-motion: reduce) {
.stage, .docs-hint, .byline { animation: none; opacity: 1; }
.caret { animation: none; }
.command-box.armed .action-btn { animation: none; }
}

View File

@@ -2,6 +2,16 @@ import { defineConfig } from 'vite';
import { resolve } from 'path';
export default defineConfig({
server: {
host: '0.0.0.0',
port: 5000,
allowedHosts: true,
},
preview: {
host: '0.0.0.0',
port: 5000,
allowedHosts: true,
},
build: {
rollupOptions: {
input: {