Portfolio — Terminal / Dev Portfolio
A full single-page portfolio reskinned as a hacker CLI: black screen, phosphor-green and amber monospace, blinking caret, ASCII banner and CRT scanlines. A typed boot sequence loads the page, then every section renders as command output — whoami, ls projects, cat about, git log experience, a skills report and a contact form. A working mini command processor parses real input, with clickable command chips, history, project case files and an alias map.
MCP
Code
/* ============================================================
Portfolio — Terminal / Dev Portfolio
Black + phosphor green / amber · monospace · CLI aesthetic
============================================================ */
:root {
/* palette */
--bg: #05080a;
--bg-deep: #02050a;
--panel: #0a1014;
--panel-2: #0d1519;
--line: #143524;
--line-soft: #0f261b;
--green: #3df58b; /* phosphor green */
--green-dim: #1f8c52;
--green-soft: #9fffce;
--amber: #ffb454; /* warm accent / highlights */
--amber-dim: #b9772a;
--cyan: #5cd2ff;
--magenta: #ff7ed4;
--red: #ff5f6e;
--ink: #c8ffe2; /* default text — soft phosphor */
--ink-dim: #6fae8b;
--ink-faint: #3f6a55;
--mono: "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Code",
Menlo, Consolas, monospace;
--radius: 10px;
--glow: 0 0 1px currentColor;
--shadow: 0 24px 80px -24px rgba(0, 0, 0, 0.9);
--maxw: 960px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
font-family: var(--mono);
font-size: 15px;
line-height: 1.55;
color: var(--ink);
background:
radial-gradient(1200px 700px at 50% -10%, #0b1c14 0%, transparent 60%),
radial-gradient(900px 600px at 100% 110%, #0a141c 0%, transparent 55%),
var(--bg-deep);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: clamp(12px, 3vw, 40px);
display: flex;
justify-content: center;
}
::selection {
background: var(--green);
color: #05140c;
}
.skip-link {
position: absolute;
left: -999px;
top: 8px;
background: var(--green);
color: #05140c;
padding: 8px 14px;
border-radius: 6px;
z-index: 50;
font-weight: 700;
}
.skip-link:focus {
left: 12px;
}
:focus-visible {
outline: 2px solid var(--amber);
outline-offset: 2px;
border-radius: 4px;
}
/* ---------- terminal window ---------- */
.terminal {
width: 100%;
max-width: var(--maxw);
background: linear-gradient(180deg, var(--panel) 0%, var(--bg) 100%);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow), inset 0 0 0 1px rgba(61, 245, 139, 0.04);
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
}
/* CRT scanlines overlay */
.terminal::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0px,
rgba(0, 0, 0, 0) 2px,
rgba(0, 0, 0, 0.16) 3px
);
mix-blend-mode: multiply;
opacity: 0.55;
z-index: 3;
transition: opacity 0.2s ease;
}
.terminal.no-crt::after {
opacity: 0;
}
/* ---------- window bar ---------- */
.win-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: linear-gradient(180deg, #0e1a14, #091210);
border-bottom: 1px solid var(--line);
position: relative;
z-index: 4;
}
.win-dots {
display: flex;
gap: 7px;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: block;
}
.dot-close { background: #ff5f56; }
.dot-min { background: #ffbd2e; }
.dot-max { background: #27c93f; }
.win-title {
flex: 1;
text-align: center;
font-size: 12px;
color: var(--ink-dim);
letter-spacing: 0.02em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.win-meta {
display: flex;
align-items: center;
gap: 10px;
}
.win-btn {
font: inherit;
font-size: 11px;
color: var(--ink-dim);
background: transparent;
border: 1px solid var(--line);
border-radius: 5px;
padding: 3px 7px;
cursor: pointer;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.win-btn:hover {
color: var(--green);
border-color: var(--green-dim);
}
.win-btn[aria-pressed="true"] {
color: var(--green);
border-color: var(--green-dim);
background: rgba(61, 245, 139, 0.08);
}
.clock {
font-size: 11px;
color: var(--ink-faint);
font-variant-numeric: tabular-nums;
}
/* ---------- screen / scrollback ---------- */
.screen {
position: relative;
z-index: 2;
padding: clamp(14px, 3vw, 26px);
overflow-y: auto;
flex: 1;
min-height: 320px;
max-height: min(70vh, 720px);
scrollbar-width: thin;
scrollbar-color: var(--green-dim) transparent;
}
.screen::-webkit-scrollbar {
width: 10px;
}
.screen::-webkit-scrollbar-thumb {
background: var(--line);
border-radius: 6px;
}
.screen:focus-visible {
outline-offset: -3px;
}
.ascii {
margin: 0 0 14px;
color: var(--green);
text-shadow: 0 0 12px rgba(61, 245, 139, 0.35);
font-size: clamp(8px, 1.9vw, 12px);
line-height: 1.25;
overflow-x: auto;
}
.boot {
color: var(--ink-dim);
font-size: 13px;
margin-bottom: 6px;
white-space: pre-wrap;
}
.boot .ok { color: var(--green); }
.boot .warn { color: var(--amber); }
/* ---------- command blocks ---------- */
.cmd-block {
margin: 18px 0;
animation: reveal 0.32s ease both;
}
@keyframes reveal {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: none; }
}
.cmdline {
margin: 0 0 8px;
word-break: break-word;
}
.prompt { color: var(--green); font-weight: 700; }
.sep { color: var(--ink-faint); }
.path { color: var(--cyan); }
.dollar {
color: var(--amber);
margin: 0 8px 0 4px;
font-weight: 700;
}
.out {
border-left: 2px solid var(--line);
padding-left: 14px;
margin-left: 2px;
}
/* whoami */
.who-name {
font-size: clamp(1.9rem, 7vw, 2.9rem);
margin: 0;
line-height: 1.05;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--green);
text-shadow: 0 0 18px rgba(61, 245, 139, 0.3);
}
.who-role {
margin: 4px 0 12px;
color: var(--amber);
font-weight: 500;
}
.who-line {
display: flex;
flex-wrap: wrap;
gap: 8px 18px;
margin: 0 0 12px;
font-size: 13px;
}
.kv .k {
color: var(--ink-faint);
margin-right: 6px;
}
.ok { color: var(--green); }
.who-bio {
color: var(--ink-dim);
max-width: 62ch;
margin: 0;
}
/* projects ls */
.ls-head {
color: var(--ink-faint);
margin: 0 0 10px;
font-size: 12px;
}
.proj-list {
list-style: none;
margin: 0;
padding: 0;
}
.proj-row {
display: grid;
grid-template-columns: 64px 1fr auto;
gap: 12px;
align-items: baseline;
padding: 9px 10px;
border-radius: 7px;
cursor: pointer;
border: 1px solid transparent;
transition: background 0.14s, border-color 0.14s, transform 0.14s;
font: inherit;
width: 100%;
text-align: left;
background: transparent;
color: inherit;
}
.proj-row:hover,
.proj-row:focus-visible {
background: rgba(61, 245, 139, 0.06);
border-color: var(--line);
transform: translateX(2px);
}
.proj-perm {
color: var(--ink-faint);
font-size: 12px;
}
.proj-name {
color: var(--green-soft);
font-weight: 500;
}
.proj-name b {
color: var(--green);
font-weight: 700;
}
.proj-name .arrow {
color: var(--amber);
margin-right: 6px;
}
.proj-desc {
display: block;
color: var(--ink-dim);
font-size: 12.5px;
margin-top: 2px;
}
.proj-tag {
color: var(--cyan);
font-size: 11px;
white-space: nowrap;
}
.hint {
color: var(--ink-faint);
font-size: 12px;
margin: 12px 0 0;
}
.hint code {
color: var(--amber);
background: rgba(255, 180, 84, 0.1);
padding: 1px 5px;
border-radius: 4px;
}
/* about */
.out-about p {
margin: 0 0 10px;
color: var(--ink-dim);
max-width: 66ch;
}
.hl { color: var(--green); }
.dl {
list-style: none;
margin: 6px 0 0;
padding: 0;
}
.dl li {
margin: 6px 0;
color: var(--ink);
font-size: 13px;
}
.dl .k {
color: var(--amber);
margin-right: 10px;
display: inline-block;
min-width: 64px;
}
/* experience git-log */
.git-log {
list-style: none;
margin: 0;
padding: 0;
}
.git-log li {
margin: 6px 0;
font-size: 13.5px;
color: var(--ink-dim);
}
.git-log .sha {
color: var(--amber);
margin-right: 10px;
}
.git-log .branch {
color: var(--cyan);
margin-right: 8px;
}
.git-log .msg {
color: var(--ink);
}
/* skills */
.skills {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 12px;
}
.skill {
display: grid;
grid-template-columns: 110px 1fr 44px;
gap: 12px;
align-items: center;
font-size: 13px;
}
.skill-name { color: var(--green-soft); }
.skill-bar {
height: 12px;
background: var(--panel-2);
border: 1px solid var(--line);
border-radius: 3px;
overflow: hidden;
position: relative;
}
.skill-fill {
display: block;
height: 100%;
width: var(--w, 0%);
background: repeating-linear-gradient(
90deg,
var(--green) 0,
var(--green) 6px,
var(--green-dim) 6px,
var(--green-dim) 8px
);
box-shadow: 0 0 8px rgba(61, 245, 139, 0.4);
transition: width 0.9s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.skill-pct {
color: var(--amber);
font-variant-numeric: tabular-nums;
text-align: right;
font-size: 12px;
}
/* contact */
.out-contact p {
color: var(--ink-dim);
margin: 0 0 12px;
}
.contact-form {
display: grid;
gap: 12px;
max-width: 540px;
}
.field {
display: grid;
gap: 4px;
}
.field .k {
color: var(--amber);
font-size: 12px;
}
.contact-form input,
.contact-form textarea {
font: inherit;
font-size: 14px;
color: var(--green-soft);
background: var(--bg-deep);
border: 1px solid var(--line);
border-radius: 6px;
padding: 9px 12px;
resize: vertical;
}
.contact-form input::placeholder,
.contact-form textarea::placeholder {
color: var(--ink-faint);
}
.contact-form input:focus,
.contact-form textarea:focus {
outline: none;
border-color: var(--green-dim);
box-shadow: 0 0 0 2px rgba(61, 245, 139, 0.18);
}
.contact-actions {
display: flex;
flex-wrap: wrap;
gap: 14px;
align-items: center;
}
.run-btn {
font: inherit;
font-weight: 700;
color: #05140c;
background: var(--green);
border: none;
border-radius: 6px;
padding: 8px 18px;
cursor: pointer;
transition: transform 0.12s, box-shadow 0.12s, background 0.12s;
}
.run-btn:hover {
background: var(--green-soft);
box-shadow: 0 0 18px rgba(61, 245, 139, 0.4);
}
.run-btn:active {
transform: translateY(1px);
}
.link {
color: var(--cyan);
text-decoration: none;
border-bottom: 1px dotted var(--line);
transition: color 0.14s, border-color 0.14s;
}
.link:hover {
color: var(--green);
border-color: var(--green-dim);
}
.form-status {
margin: 0;
font-size: 13px;
min-height: 1.2em;
color: var(--green);
}
.form-status.err { color: var(--red); }
.ascii-rule {
color: var(--line);
margin-top: 22px;
overflow: hidden;
white-space: nowrap;
user-select: none;
}
/* ---------- live prompt row ---------- */
.prompt-row {
position: relative;
z-index: 4;
display: flex;
align-items: center;
gap: 4px;
padding: 12px clamp(14px, 3vw, 26px);
border-top: 1px solid var(--line);
background: linear-gradient(180deg, var(--bg), var(--bg-deep));
}
.prompt-label {
white-space: nowrap;
flex-shrink: 0;
}
.prompt-input {
flex: 1;
font: inherit;
color: var(--green-soft);
background: transparent;
border: none;
outline: none;
padding: 4px 0;
caret-color: transparent;
min-width: 0;
}
.prompt-input::placeholder {
color: var(--ink-faint);
}
.caret {
color: var(--green);
font-weight: 700;
animation: blink 1.05s step-end infinite;
margin-left: -6px;
pointer-events: none;
}
.caret.typing {
animation: none;
opacity: 1;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.prompt-help {
position: relative;
z-index: 4;
display: flex;
flex-wrap: wrap;
gap: 6px 8px;
align-items: center;
margin: 0;
padding: 10px clamp(14px, 3vw, 26px) 16px;
font-size: 12px;
color: var(--ink-faint);
background: var(--bg-deep);
border-top: 1px solid var(--line-soft);
}
.ghost-cmd {
font: inherit;
font-size: 12px;
color: var(--ink-dim);
background: var(--panel);
border: 1px solid var(--line);
border-radius: 5px;
padding: 3px 8px;
cursor: pointer;
transition: color 0.14s, border-color 0.14s, background 0.14s, transform 0.1s;
}
.ghost-cmd:hover,
.ghost-cmd:focus-visible {
color: var(--green);
border-color: var(--green-dim);
background: rgba(61, 245, 139, 0.08);
}
.ghost-cmd:active {
transform: translateY(1px);
}
/* ---------- case-file dialog ---------- */
.dialog-wrap {
position: fixed;
inset: 0;
z-index: 60;
display: grid;
place-items: center;
padding: 16px;
background: rgba(2, 5, 8, 0.78);
backdrop-filter: blur(3px);
animation: fade 0.18s ease both;
}
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
.dialog-panel {
width: min(560px, 100%);
max-height: 86vh;
overflow-y: auto;
background: var(--panel);
border: 1px solid var(--green-dim);
border-radius: var(--radius);
box-shadow: 0 0 60px rgba(61, 245, 139, 0.12), var(--shadow);
animation: reveal 0.22s ease both;
}
.dlg-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid var(--line);
background: #091210;
position: sticky;
top: 0;
}
.dlg-tag {
color: var(--cyan);
font-size: 12px;
}
.dlg-close {
font: inherit;
color: var(--ink-dim);
background: transparent;
border: 1px solid var(--line);
border-radius: 5px;
width: 26px;
height: 26px;
cursor: pointer;
transition: color 0.14s, border-color 0.14s;
}
.dlg-close:hover {
color: var(--red);
border-color: var(--red);
}
.dlg-title {
margin: 16px 18px 4px;
color: var(--green);
font-size: 1.4rem;
}
.dlg-meta {
margin: 0 18px 12px;
color: var(--amber);
font-size: 12.5px;
}
.dlg-body {
padding: 0 18px 20px;
}
.dlg-body p {
color: var(--ink-dim);
margin: 0 0 12px;
}
.dlg-body ul {
margin: 0 0 14px;
padding-left: 18px;
color: var(--ink);
}
.dlg-body li {
margin: 5px 0;
font-size: 13.5px;
}
.dlg-body .metrics {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 8px;
}
.dlg-body .metrics li {
border: 1px solid var(--line);
border-radius: 7px;
padding: 8px 10px;
background: var(--bg-deep);
}
.dlg-body .metrics b {
display: block;
color: var(--green);
font-size: 1.15rem;
}
.dlg-body .metrics span {
color: var(--ink-faint);
font-size: 11px;
}
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 22px;
transform: translate(-50%, 14px);
z-index: 70;
background: var(--panel);
color: var(--green);
border: 1px solid var(--green-dim);
border-radius: 8px;
padding: 10px 18px;
font-size: 13px;
box-shadow: 0 12px 40px -12px rgba(0, 0, 0, 0.8);
opacity: 0;
transition: opacity 0.2s, transform 0.2s;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
.toast::before {
content: "› ";
color: var(--amber);
}
/* ---------- responsive ---------- */
@media (max-width: 620px) {
body { font-size: 14px; padding: 8px; }
.win-title { display: none; }
.win-bar { justify-content: space-between; }
.proj-row {
grid-template-columns: 1fr;
gap: 2px;
}
.proj-perm { display: none; }
.proj-tag { margin-top: 2px; }
.skill {
grid-template-columns: 92px 1fr 40px;
gap: 8px;
}
}
@media (max-width: 380px) {
.who-line { gap: 6px 10px; }
.dl .k { min-width: 52px; }
}
/* ---------- reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
.caret { opacity: 1; }
}/* ============================================================
Terminal / Dev Portfolio — vanilla JS
- typed boot intro
- working mini command processor (reveals sections)
- blinking caret synced to input
- project case-file dialog
- skills bars, clock, scanline toggle, contact form, toast
============================================================ */
(function () {
"use strict";
var reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
/* ---------- data ---------- */
var PROJECTS = [
{
id: "northwind-pay",
perm: "-rwxr-xr-x",
name: "northwind-pay",
desc: "End-to-end design system + checkout for a EU fintech.",
tag: "[systems]",
year: "2024",
role: "Lead Product Designer",
blurb:
"Rebuilt Northwind's payment flow and the component library behind it. Shipped a tokenized design system consumed by web, iOS and three internal tools, then redesigned the checkout to cut drop-off.",
bullets: [
"Token pipeline (Figma → JSON → CSS vars) adopted by 4 squads.",
"Checkout redesign rolled out behind a feature flag with A/B guardrails.",
"Authored the accessibility playbook now used company-wide."
],
metrics: [
{ n: "−31%", l: "checkout drop-off" },
{ n: "4 → 1", l: "color systems" },
{ n: "AA+", l: "contrast audit" }
]
},
{
id: "mirage-os",
perm: "-rw-r--r--",
name: "mirage-os",
desc: "Internal design OS — 180+ components, live docs.",
tag: "[systems]",
year: "2022",
role: "Senior Designer",
blurb:
"A single source of truth for Mirage Studio's client work: Storybook-backed components, usage docs and a contribution model that let engineers ship UI without a designer in the loop.",
bullets: [
"180+ documented components with live props playground.",
"Cut new-screen build time roughly in half across teams.",
"Set up visual-regression checks in CI to stop drift."
],
metrics: [
{ n: "180+", l: "components" },
{ n: "~2×", l: "faster builds" },
{ n: "12", l: "teams onboard" }
]
},
{
id: "cadence-vitals",
perm: "-rw-r--r--",
name: "cadence-vitals",
desc: "Patient vitals dashboard for clinicians, dark-first.",
tag: "[product]",
year: "2021",
role: "Product Designer",
blurb:
"A glanceable vitals dashboard for night-shift clinicians. Dark-first, high-contrast, and tuned for triage at speed — every alert one keystroke away.",
bullets: [
"Triage view designed around colour-blind-safe status coding.",
"Keyboard-first navigation validated with five working nurses.",
"Reduced average time-to-acknowledge a critical alert."
],
metrics: [
{ n: "−40%", l: "ack time" },
{ n: "5", l: "field tests" },
{ n: "WCAG", l: "AA verified" }
]
},
{
id: "type-foundry",
perm: "-rw-r--r--",
name: "type-foundry.zine",
desc: "Self-published variable-type specimen + newsletter.",
tag: "[brand]",
year: "2023",
role: "Maker",
blurb:
"A side project that became a habit: a quarterly specimen zine and newsletter about variable fonts, built end to end — writing, layout, type pairing and a tiny static-site generator.",
bullets: [
"2,400 subscribers, fully organic, zero ad spend.",
"Interactive specimens built with the variable-font API.",
"Open-sourced the static generator behind it."
],
metrics: [
{ n: "2.4k", l: "subscribers" },
{ n: "9", l: "issues" },
{ n: "MIT", l: "open source" }
]
},
{
id: "atlas-maps",
perm: "-rw-r--r--",
name: "atlas-maps",
desc: "Wayfinding + motion system for a transit app.",
tag: "[product]",
year: "2020",
role: "Product Designer",
blurb:
"Wayfinding and a calm motion language for a city transit app — turn-by-turn that stays legible at a glance while you're moving, in sun or in a dark tunnel.",
bullets: [
"Motion spec (durations + easing) shared across iOS/Android.",
"Glance-test rig to validate legibility under 0.4s exposure.",
"Adaptive contrast for in-tunnel dark conditions."
],
metrics: [
{ n: "0.4s", l: "glance target" },
{ n: "2", l: "platforms" },
{ n: "+18", l: "SUS points" }
]
}
];
var SKILLS = [
{ name: "Design systems", pct: 96 },
{ name: "Product design", pct: 92 },
{ name: "TypeScript/React", pct: 84 },
{ name: "CSS / motion", pct: 90 },
{ name: "Prototyping", pct: 88 },
{ name: "Accessibility", pct: 86 }
];
/* ---------- helpers ---------- */
function $(sel, ctx) {
return (ctx || document).querySelector(sel);
}
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.hidden = false;
// force reflow so the transition fires
void toastEl.offsetWidth;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
setTimeout(function () {
toastEl.hidden = true;
}, 220);
}, 2400);
}
var screen = $("#screen");
function scrollToEnd() {
if (screen) screen.scrollTop = screen.scrollHeight;
}
/* ---------- clock ---------- */
var clockEl = $("#clock");
function tick() {
if (!clockEl) return;
var d = new Date();
var p = function (n) {
return String(n).padStart(2, "0");
};
clockEl.textContent =
p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
}
tick();
setInterval(tick, 1000);
/* ---------- populate project list ---------- */
var projList = $("#projList");
if (projList) {
PROJECTS.forEach(function (p, i) {
var li = document.createElement("li");
var btn = document.createElement("button");
btn.type = "button";
btn.className = "proj-row";
btn.setAttribute("data-id", p.id);
btn.innerHTML =
'<span class="proj-perm">' +
p.perm +
"</span>" +
'<span class="proj-name"><span class="arrow">→</span><b>' +
p.name +
"</b> " +
'<span class="proj-desc">' +
p.desc +
"</span></span>" +
'<span class="proj-tag">' +
p.tag +
" " +
p.year +
"</span>";
btn.addEventListener("click", function () {
openProject(p.id);
});
li.appendChild(btn);
projList.appendChild(li);
});
}
/* ---------- populate skills ---------- */
var skillList = $("#skillList");
function renderSkills() {
if (!skillList) return;
skillList.innerHTML = "";
SKILLS.forEach(function (s) {
var li = document.createElement("li");
li.className = "skill";
li.innerHTML =
'<span class="skill-name">' +
s.name +
"</span>" +
'<span class="skill-bar"><span class="skill-fill"></span></span>' +
'<span class="skill-pct">' +
s.pct +
"%</span>";
skillList.appendChild(li);
});
// animate fills on next frame
requestAnimationFrame(function () {
var fills = skillList.querySelectorAll(".skill-fill");
SKILLS.forEach(function (s, i) {
if (fills[i]) fills[i].style.setProperty("--w", s.pct + "%");
});
});
}
/* ---------- section reveal ---------- */
var BLOCKS = {
whoami: "block-whoami",
projects: "block-projects",
about: "block-about",
experience: "block-experience",
skills: "block-skills",
contact: "block-contact"
};
function reveal(key) {
var el = document.getElementById(BLOCKS[key]);
if (!el) return false;
var wasHidden = el.hidden;
el.hidden = false;
if (key === "skills" && wasHidden) renderSkills();
if (wasHidden) {
// re-trigger reveal animation
el.style.animation = "none";
void el.offsetWidth;
el.style.animation = "";
}
scrollToEnd();
return true;
}
/* ---------- command processor ---------- */
function printSystem(text, cls) {
// append a transient system line into the boot/output area
var boot = $("#boot");
if (!boot) return;
var line = document.createElement("div");
if (cls) line.className = cls;
line.textContent = text;
boot.appendChild(line);
scrollToEnd();
}
function clearScreen() {
Object.keys(BLOCKS).forEach(function (k) {
var el = document.getElementById(BLOCKS[k]);
if (el) el.hidden = true;
});
var boot = $("#boot");
if (boot) boot.innerHTML = "";
printSystem("screen cleared. type `help` to list commands.", "warn");
}
function runCommand(raw) {
var cmd = String(raw || "").trim().toLowerCase();
if (!cmd) return;
// normalize: strip leading $ / ./ , collapse spaces
cmd = cmd.replace(/^\$\s*/, "").replace(/\s+/g, " ");
// map of aliases -> section key
if (/^(whoami|who|me|id)$/.test(cmd)) return done(reveal("whoami"), "whoami");
if (/^(ls( -?[al]+)?( projects\/?)?|projects|ls|work|portfolio)$/.test(cmd))
return done(reveal("projects"), "projects");
if (/^(cat about(\.md)?|about|bio)$/.test(cmd))
return done(reveal("about"), "about");
if (/^(git log.*|experience|exp|history|work history)$/.test(cmd))
return done(reveal("experience"), "experience");
if (/^(\.?\/?skills( --report)?|skills|stack)$/.test(cmd))
return done(reveal("skills"), "skills");
if (/^(contact( --send)?|email|hire|reach)$/.test(cmd)) {
reveal("contact");
done(true, "contact");
var inp = $("#cEmail");
if (inp) setTimeout(function () { inp.focus(); }, 60);
return;
}
// open <n> or open <name>
var openMatch = cmd.match(/^open\s+(.+)$/);
if (openMatch) {
var arg = openMatch[1].trim();
var idx = parseInt(arg, 10);
var proj;
if (!isNaN(idx) && idx >= 1 && idx <= PROJECTS.length) {
proj = PROJECTS[idx - 1];
} else {
proj = PROJECTS.filter(function (p) {
return p.name.toLowerCase().indexOf(arg) !== -1 || p.id === arg;
})[0];
}
if (proj) {
reveal("projects");
openProject(proj.id);
printSystem("opening case file: " + proj.name, "ok");
} else {
printSystem("open: no such project '" + arg + "'", "warn");
}
return;
}
if (cmd === "all") {
["whoami", "projects", "about", "experience", "skills", "contact"].forEach(
reveal
);
printSystem("rendered full portfolio.", "ok");
scrollToEnd();
return;
}
if (/^(clear|cls)$/.test(cmd)) return clearScreen();
if (/^(help|\?|man|ls -h|--help)$/.test(cmd)) {
printSystem(
"available: whoami · ls projects · cat about · experience · skills · contact · open <n> · all · clear",
"ok"
);
return;
}
if (cmd === "sudo" || /^sudo /.test(cmd)) {
printSystem("nice try. maya@okafor is not in the sudoers file. 😏", "warn");
return;
}
if (cmd === "pwd") {
printSystem("/home/maya/portfolio", "ok");
return;
}
if (cmd === "date") {
printSystem(new Date().toString(), "ok");
return;
}
if (cmd === "echo hi" || cmd === "hi" || cmd === "hello") {
printSystem("hello — type `all` to render the whole portfolio.", "ok");
return;
}
if (cmd === "exit" || cmd === "quit") {
printSystem("you can check out any time you like… (this is a portfolio)", "warn");
return;
}
printSystem(
"command not found: " + raw + " — type `help` for options.",
"warn"
);
}
function done(ok, label) {
if (ok) {
printSystem("→ rendered " + label, "ok");
}
}
/* ---------- prompt input ---------- */
var form = $("#promptForm");
var input = $("#prompt-input");
var caret = $("#caret");
// position the caret so it sits right after typed text
function syncCaret() {
if (!input || !caret) return;
caret.classList.toggle("typing", document.activeElement === input);
// show caret only when input empty/at end for a believable look
}
if (input) {
input.addEventListener("input", syncCaret);
input.addEventListener("focus", syncCaret);
input.addEventListener("blur", function () {
caret.classList.remove("typing");
});
// command history (up/down)
var history = [];
var hIdx = -1;
input.addEventListener("keydown", function (e) {
if (e.key === "ArrowUp") {
if (history.length) {
hIdx = hIdx <= 0 ? history.length - 1 : hIdx - 1;
input.value = history[hIdx];
e.preventDefault();
}
} else if (e.key === "ArrowDown") {
if (history.length) {
hIdx = hIdx >= history.length - 1 ? -1 : hIdx + 1;
input.value = hIdx === -1 ? "" : history[hIdx];
e.preventDefault();
}
}
});
if (form) {
form.addEventListener("submit", function (e) {
e.preventDefault();
var v = input.value;
if (v.trim()) {
history.push(v.trim());
hIdx = -1;
}
runCommand(v);
input.value = "";
syncCaret();
});
}
}
// ghost command buttons + run on click
document.querySelectorAll(".ghost-cmd").forEach(function (btn) {
btn.addEventListener("click", function () {
var c = btn.getAttribute("data-cmd");
runCommand(c);
if (input) input.focus();
});
});
/* ---------- case-file dialog ---------- */
var dialog = $("#dialog");
var dlgTitle = $("#dlgTitle");
var dlgMeta = $("#dlgMeta");
var dlgBody = $("#dlgBody");
var dlgTag = $("#dlgTag");
var dlgClose = $("#dlgClose");
var lastFocus = null;
function openProject(id) {
var p = PROJECTS.filter(function (x) {
return x.id === id;
})[0];
if (!p || !dialog) return;
lastFocus = document.activeElement;
dlgTag.textContent = "~/projects/" + p.name;
dlgTitle.textContent = p.name;
dlgMeta.textContent = p.role + " · " + p.year + " · " + p.tag;
var bullets = p.bullets
.map(function (b) {
return "<li>" + b + "</li>";
})
.join("");
var metrics = p.metrics
.map(function (m) {
return "<li><b>" + m.n + "</b><span>" + m.l + "</span></li>";
})
.join("");
dlgBody.innerHTML =
"<p>" +
p.blurb +
"</p><ul>" +
bullets +
'</ul><ul class="metrics">' +
metrics +
"</ul>";
dialog.hidden = false;
document.body.style.overflow = "hidden";
setTimeout(function () {
dlgClose.focus();
}, 40);
}
function closeDialog() {
if (!dialog || dialog.hidden) return;
dialog.hidden = true;
document.body.style.overflow = "";
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
if (dlgClose) dlgClose.addEventListener("click", closeDialog);
if (dialog) {
dialog.addEventListener("click", function (e) {
if (e.target === dialog) closeDialog();
});
}
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") closeDialog();
});
/* ---------- scanline toggle ---------- */
var crtBtn = $("#scanlines");
var terminal = $(".terminal");
if (crtBtn && terminal) {
crtBtn.addEventListener("click", function () {
var on = crtBtn.getAttribute("aria-pressed") === "true";
crtBtn.setAttribute("aria-pressed", String(!on));
terminal.classList.toggle("no-crt", on);
toast(on ? "CRT scanlines off" : "CRT scanlines on");
});
}
/* ---------- contact form ---------- */
var contactForm = $("#contactForm");
var formStatus = $("#formStatus");
if (contactForm) {
contactForm.addEventListener("submit", function (e) {
e.preventDefault();
var email = $("#cEmail");
var msg = $("#cMsg");
var emailOk = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(
(email.value || "").trim()
);
if (!emailOk) {
formStatus.textContent = "✕ invalid --from address";
formStatus.classList.add("err");
email.focus();
return;
}
if (!(msg.value || "").trim()) {
formStatus.textContent = "✕ --message cannot be empty";
formStatus.classList.add("err");
msg.focus();
return;
}
formStatus.classList.remove("err");
formStatus.textContent = "sending…";
var btn = $(".run-btn", contactForm);
if (btn) btn.disabled = true;
setTimeout(function () {
formStatus.textContent = "✓ message queued — I'll reply within 2 days.";
toast("message sent (demo). thanks!");
contactForm.reset();
if (btn) btn.disabled = false;
}, 900);
});
}
// simple toast links
document.querySelectorAll("[data-toast]").forEach(function (a) {
a.addEventListener("click", function (e) {
e.preventDefault();
toast(a.getAttribute("data-toast"));
});
});
/* ---------- boot sequence (typed) ---------- */
var bootLines = [
{ t: "[ ok ] mounting /home/maya … ", s: "ok" },
{ t: "[ ok ] loading portfolio.sh v3.2.1", s: "ok" },
{ t: "[ ok ] 5 projects indexed · 6 skills calibrated", s: "ok" },
{ t: "[info] welcome — try `whoami` or `all`. (`help` for commands)", s: "warn" }
];
var boot = $("#boot");
function runBoot(done) {
if (!boot) return done && done();
if (reduceMotion) {
bootLines.forEach(function (l) {
var d = document.createElement("div");
d.className = l.s;
d.textContent = l.t;
boot.appendChild(d);
});
return done && done();
}
var i = 0;
function next() {
if (i >= bootLines.length) return done && done();
var l = bootLines[i++];
var d = document.createElement("div");
d.className = l.s;
boot.appendChild(d);
typeInto(d, l.t, 14, function () {
setTimeout(next, 140);
});
scrollToEnd();
}
next();
}
function typeInto(el, text, speed, cb) {
var i = 0;
(function step() {
el.textContent = text.slice(0, i);
if (i++ <= text.length) {
setTimeout(step, speed);
} else if (cb) {
cb();
}
})();
}
// kick off: boot, then reveal whoami automatically so the page is never empty
runBoot(function () {
reveal("whoami");
reveal("projects");
});
// keep input focused-feeling: click anywhere on screen focuses prompt
if (screen && input) {
screen.addEventListener("dblclick", function () {
input.focus();
});
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>maya@okafor:~ — Maya Okafor, Product Designer</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:ital,wght@0,400;0,500;0,700;0,800;1,400&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#prompt-input">Skip to command input</a>
<main class="terminal" role="main" aria-label="Maya Okafor terminal portfolio">
<!-- Window chrome -->
<header class="win-bar" role="banner">
<div class="win-dots" aria-hidden="true">
<span class="dot dot-close"></span>
<span class="dot dot-min"></span>
<span class="dot dot-max"></span>
</div>
<span class="win-title">maya@okafor: ~/portfolio — zsh — 96×40</span>
<div class="win-meta">
<button id="scanlines" class="win-btn" type="button" aria-pressed="true" title="Toggle CRT scanlines">
<span aria-hidden="true">▦</span> crt
</button>
<span class="clock" id="clock" aria-label="Session clock">--:--:--</span>
</div>
</header>
<!-- Scrollback / output -->
<section class="screen" id="screen" aria-label="Terminal output" tabindex="0">
<pre class="ascii" aria-hidden="true">
┌──────────────────────────────────────────────┐
│ __ __ _ _ _ _ │
│ | \/ | / \ | | | | / \ o k a f o r │
│ | |\/| | / _ \ | |_| | / _ \ portfolio.sh │
│ | | | |/ ___ \| _ |/ ___ \ v3.2.1 │
│ |_| |_/_/ \_\_| |_/_/ \_\ │
└──────────────────────────────────────────────┘</pre>
<div class="boot" id="boot" aria-live="polite" aria-label="Boot sequence"></div>
<!-- Sections are revealed as command output. They start hidden until "run". -->
<article class="cmd-block" id="block-whoami" hidden>
<p class="cmdline"><span class="prompt">maya@okafor</span><span class="sep">:</span><span class="path">~</span><span class="dollar">$</span> whoami</p>
<div class="out out-whoami">
<h1 class="who-name">Maya Okafor</h1>
<p class="who-role">Product Designer · Design Systems · Front-of-frontend</p>
<p class="who-line">
<span class="kv"><span class="k">loc</span> Lisbon, PT (UTC+0)</span>
<span class="kv"><span class="k">exp</span> 8 yrs</span>
<span class="kv"><span class="k">status</span> <span class="ok">● open to select work</span></span>
</p>
<p class="who-bio">// I design calm, fast product interfaces and the systems that keep them honest — shipping from first sketch to merged PR.</p>
</div>
</article>
<article class="cmd-block" id="block-projects" hidden>
<p class="cmdline"><span class="prompt">maya@okafor</span><span class="sep">:</span><span class="path">~</span><span class="dollar">$</span> ls -la projects/</p>
<div class="out">
<p class="ls-head">total 5 · drwxr-xr-x · selected 2019—2026</p>
<ul class="proj-list" id="projList"></ul>
<p class="hint">// tip: <code>open <n></code> or click a row to read the case file.</p>
</div>
</article>
<article class="cmd-block" id="block-about" hidden>
<p class="cmdline"><span class="prompt">maya@okafor</span><span class="sep">:</span><span class="path">~</span><span class="dollar">$</span> cat about.md</p>
<div class="out out-about">
<p>I started in motion graphics, detoured through front-end engineering, and
landed where the two meet: <span class="hl">interface systems</span> that are as
rigorous in code as they are in Figma. I like the unglamorous parts — token
naming, empty states, the 3am latency budget.</p>
<p>Off-screen I keep a mechanical-keyboard habit, run a small type-foundry
newsletter, and over-document everything in plain markdown.</p>
<ul class="dl">
<li><span class="k">values</span> clarity over cleverness · ship to learn · accessible by default</li>
<li><span class="k">stack</span> Figma · TypeScript · React · CSS · Storybook · Playwright</li>
<li><span class="k">now</span> building a component library for a fintech in private beta</li>
</ul>
</div>
</article>
<article class="cmd-block" id="block-experience" hidden>
<p class="cmdline"><span class="prompt">maya@okafor</span><span class="sep">:</span><span class="path">~</span><span class="dollar">$</span> git log --experience --oneline</p>
<div class="out">
<ul class="git-log">
<li><span class="sha">9f2c1ab</span><span class="branch">(HEAD → now)</span> <span class="msg">Lead Product Designer · Northwind Pay — 2023→present</span></li>
<li><span class="sha">4d80e7c</span> <span class="msg">Senior Designer · Mirage Studio — 2020→2023</span></li>
<li><span class="sha">a17fce0</span> <span class="msg">Product Designer · Cadence Health — 2018→2020</span></li>
<li><span class="sha">0b3e992</span> <span class="msg">Motion & UI · freelance — 2016→2018</span></li>
</ul>
</div>
</article>
<article class="cmd-block" id="block-skills" hidden>
<p class="cmdline"><span class="prompt">maya@okafor</span><span class="sep">:</span><span class="path">~</span><span class="dollar">$</span> ./skills --report</p>
<div class="out">
<ul class="skills" id="skillList"></ul>
</div>
</article>
<article class="cmd-block" id="block-contact" hidden>
<p class="cmdline"><span class="prompt">maya@okafor</span><span class="sep">:</span><span class="path">~</span><span class="dollar">$</span> contact --send</p>
<div class="out out-contact">
<p>// fastest path is email; I reply within two working days.</p>
<form id="contactForm" class="contact-form" novalidate>
<label class="field">
<span class="k">--from</span>
<input type="email" id="cEmail" name="email" placeholder="[email protected]" autocomplete="email" required />
</label>
<label class="field">
<span class="k">--message</span>
<textarea id="cMsg" name="message" rows="2" placeholder="what are we building?" required></textarea>
</label>
<div class="contact-actions">
<button class="run-btn" type="submit">send →</button>
<a class="link" href="#" data-toast="opening mail client…">[email protected]</a>
<a class="link" href="#" data-toast="opening github…">github</a>
<a class="link" href="#" data-toast="opening read.cv…">read.cv</a>
</div>
<p class="form-status" id="formStatus" role="status" aria-live="polite"></p>
</form>
</div>
</article>
<div class="ascii-rule" aria-hidden="true">────────────────────────────────────────────────────────</div>
</section>
<!-- Live prompt -->
<form class="prompt-row" id="promptForm" aria-label="Command input">
<label class="prompt-label" for="prompt-input">
<span class="prompt">maya@okafor</span><span class="sep">:</span><span class="path">~</span><span class="dollar">$</span>
</label>
<input
class="prompt-input"
id="prompt-input"
type="text"
name="command"
autocomplete="off"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
aria-describedby="prompt-help"
placeholder="type a command — try: help"
/>
<span class="caret" id="caret" aria-hidden="true">▋</span>
</form>
<p class="prompt-help" id="prompt-help">
commands:
<button class="ghost-cmd" type="button" data-cmd="help">help</button>
<button class="ghost-cmd" type="button" data-cmd="whoami">whoami</button>
<button class="ghost-cmd" type="button" data-cmd="ls projects">ls projects</button>
<button class="ghost-cmd" type="button" data-cmd="cat about">cat about</button>
<button class="ghost-cmd" type="button" data-cmd="experience">experience</button>
<button class="ghost-cmd" type="button" data-cmd="skills">skills</button>
<button class="ghost-cmd" type="button" data-cmd="contact">contact</button>
<button class="ghost-cmd" type="button" data-cmd="all">all</button>
<button class="ghost-cmd" type="button" data-cmd="clear">clear</button>
</p>
</main>
<!-- Case-file dialog -->
<div class="dialog-wrap" id="dialog" role="dialog" aria-modal="true" aria-labelledby="dlgTitle" hidden>
<div class="dialog-panel">
<header class="dlg-bar">
<span class="dlg-tag" id="dlgTag">~/projects/</span>
<button class="dlg-close" id="dlgClose" type="button" aria-label="Close case file">✕</button>
</header>
<h2 class="dlg-title" id="dlgTitle">Project</h2>
<p class="dlg-meta" id="dlgMeta"></p>
<div class="dlg-body" id="dlgBody"></div>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script src="script.js"></script>
</body>
</html>Terminal / Dev Portfolio
A complete one-page portfolio dressed as a developer terminal. The window opens
with a typed boot sequence over an ASCII banner, then the same portfolio content
that drives the neutral base — hero, projects, about, experience, skills and
contact — is reframed as command output: whoami, ls -la projects/,
cat about.md, git log --experience, a ./skills --report bar chart and a
contact --send form. The aesthetic is pure CLI: black screen, phosphor-green
and amber on monospace, a blinking caret, ASCII dividers and toggleable CRT
scanlines.
The centrepiece is a working mini command processor. The prompt input parses
real commands through an alias map (who, ls, work, cat about, exp,
stack, hire, all, clear, plus easter eggs like sudo and pwd),
reveals the matching section, and supports open <n> to launch a project case
file. Up/down arrows walk command history, the ghost-command chips run the same
parser on click, and each project row opens a focus-trapped dialog with a blurb,
highlights and metric tiles. The contact form validates the --from address and
message before queueing, and a toast confirms.
Everything is self-contained vanilla JS with no libraries: typed output respects
prefers-reduced-motion, controls are keyboard-operable with visible focus
rings, the layout collapses cleanly to roughly 360px, and photos are evoked with
gradients and ASCII rather than external images.
Illustrative portfolio — fictional person and projects.