i18n: bilingual de/en, bundle js from assets via hugo pipes, css refresh
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,9 @@
|
||||
outline: none;
|
||||
caret-color: transparent;
|
||||
}
|
||||
/* a command "typed" by the file browser on hover/freeze — dimmed so it reads
|
||||
as a hint, not something you committed. any real keystroke discards it. */
|
||||
.prompt__input.is-ghost { color: var(--color-text-muted); font-weight: 400; }
|
||||
|
||||
/* blinking terminal cursor, sits right after the typed text */
|
||||
.caret {
|
||||
@@ -363,3 +366,31 @@ img { filter: none; }
|
||||
/* frozen/locked preview indicator */
|
||||
.filebrowser.is-frozen .tui__status { color: var(--color-text-primary); }
|
||||
.filebrowser.is-frozen .tui__status::after { content: " ● locked"; }
|
||||
|
||||
/* hide every scrollbar site-wide — content still scrolls (wheel/touch/keys),
|
||||
just no native chrome. keeps the clean terminal surface everywhere. */
|
||||
* {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* old Edge/IE */
|
||||
}
|
||||
*::-webkit-scrollbar { /* WebKit/Blink */
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ─────────────────── LANGUAGE SWITCH ─────────────────── */
|
||||
/* top-right toggle on the shell row: shows only the *other* language,
|
||||
click to switch. you never see both at once. */
|
||||
.topbar { position: relative; }
|
||||
.lang-switch {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin-left: 0;
|
||||
font-family: var(--font-family-mono);
|
||||
color: var(--color-text-muted);
|
||||
border-bottom: 1px dotted;
|
||||
z-index: 2;
|
||||
}
|
||||
.lang-switch:hover { color: var(--color-text-primary); border-bottom: 1px solid; }
|
||||
/* keep the prompt text clear of the toggle so long paths never slide under it */
|
||||
.prompt { padding-right: 3.5ch; }
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/* boot splash — a short systemd/Arch-Linux-style boot, shown once per session
|
||||
(first visit + every new tab). pjax means it never replays on navigation.
|
||||
click or press any key to skip. safe: head fallback timeout clears it. */
|
||||
(function () {
|
||||
var html = document.documentElement;
|
||||
if (!html.classList.contains("booting")) return;
|
||||
var boot = document.getElementById("boot");
|
||||
var log = boot && boot.querySelector(".boot__log");
|
||||
|
||||
var ended = false;
|
||||
function done() {
|
||||
if (ended) return;
|
||||
ended = true;
|
||||
document.removeEventListener("keydown", skip, true);
|
||||
document.removeEventListener("click", skip, true);
|
||||
try { sessionStorage.setItem("kgv_booted", "1"); } catch (e) {}
|
||||
if (!boot) { html.classList.remove("booting"); return; }
|
||||
boot.style.transition = "opacity .3s ease";
|
||||
boot.style.opacity = "0";
|
||||
setTimeout(function () {
|
||||
html.classList.remove("booting");
|
||||
if (boot.parentNode) boot.parentNode.removeChild(boot);
|
||||
}, 300);
|
||||
}
|
||||
function skip() { done(); }
|
||||
|
||||
if (!log) { done(); return; }
|
||||
document.addEventListener("keydown", skip, true);
|
||||
document.addEventListener("click", skip, true);
|
||||
|
||||
// [type, text, delay-after-ms]. type: 'k' plain kernel/info, 'ok' green [ OK ],
|
||||
// 'st' "Starting…", 'b' blank, 'd' dim.
|
||||
var L = [
|
||||
["k", "karimgabrielevarano.xyz 1.0 (tty1)", 90],
|
||||
["d", "kgv-sh · hugo static runtime · pjax router", 320],
|
||||
["b", "", 60],
|
||||
["k", "Welcome to karimgabrielevarano.xyz", 120],
|
||||
["ok", "Loaded index.html", 70],
|
||||
["ok", "Loaded css/bundle.css", 60],
|
||||
["ok", "Loaded fonts/iosevka-curly-slab.woff2", 90],
|
||||
["ok", "Loaded js/bundle.js", 80],
|
||||
["ok", "Parsed DOM — 0 errors", 120],
|
||||
["st", "Mounting routes...", 240],
|
||||
["ok", "Mounted /", 50],
|
||||
["ok", "Mounted /portfolio", 50],
|
||||
["ok", "Mounted /about", 50],
|
||||
["ok", "Mounted /studio", 50],
|
||||
["ok", "Mounted /openbureau", 50],
|
||||
["ok", "Mounted /cv", 50],
|
||||
["ok", "Mounted /colophon", 110],
|
||||
["st", "Prerendering portfolio...", 220],
|
||||
["ok", "rendered portfolio/textilsteinwerk", 60],
|
||||
["ok", "rendered portfolio/the-shadow-over-emmen", 60],
|
||||
["ok", "rendered portfolio/studytrip-trieste", 60],
|
||||
["ok", "rendered portfolio/dojo", 60],
|
||||
["ok", "rendered portfolio/museo", 110],
|
||||
["ok", "Hydrated shell · cursor · theme", 100],
|
||||
["ok", "Reached target Interactive.", 260],
|
||||
["b", "", 110],
|
||||
["k", "karimgabrielevarano login: karim (automatic)", 180],
|
||||
["k", "welcome_", 500]
|
||||
];
|
||||
|
||||
function render(item) {
|
||||
var type = item[0], text = item[1];
|
||||
var line = document.createElement("div");
|
||||
line.className = "boot__line";
|
||||
if (type === "ok") {
|
||||
var s = document.createElement("span");
|
||||
s.className = "boot__ok";
|
||||
s.textContent = "[ OK ] ";
|
||||
line.appendChild(s);
|
||||
line.appendChild(document.createTextNode(text));
|
||||
} else if (type === "st") {
|
||||
line.className += " boot__dim";
|
||||
line.appendChild(document.createTextNode("[ ] " + text));
|
||||
} else if (type === "d") {
|
||||
line.className += " boot__dim";
|
||||
line.textContent = text;
|
||||
} else if (type === "b") {
|
||||
line.innerHTML = " ";
|
||||
} else {
|
||||
line.textContent = text;
|
||||
}
|
||||
log.appendChild(line);
|
||||
boot.scrollTop = boot.scrollHeight; // keep the newest line in view
|
||||
}
|
||||
|
||||
var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
if (reduce) {
|
||||
L.forEach(render);
|
||||
setTimeout(done, 700);
|
||||
return;
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
(function next() {
|
||||
if (ended) return;
|
||||
if (i >= L.length) { setTimeout(done, 450); return; }
|
||||
var item = L[i];
|
||||
render(item);
|
||||
i++;
|
||||
setTimeout(next, item[2] || 110);
|
||||
})();
|
||||
})();
|
||||
@@ -0,0 +1,58 @@
|
||||
/* custom pixel cursor — appears only once we know the real pointer position,
|
||||
so it never flashes at a stale spot on page changes. native cursor is hidden
|
||||
via CSS (*{cursor:none}). */
|
||||
(function () {
|
||||
if (!window.matchMedia("(pointer: fine)").matches) return;
|
||||
|
||||
var c = document.createElement("canvas");
|
||||
c.style.cssText =
|
||||
"position:fixed;left:0;top:0;pointer-events:none;z-index:9999;" +
|
||||
"image-rendering:pixelated;will-change:transform;opacity:0;" +
|
||||
"transform:translate3d(-200px,-200px,0);";
|
||||
document.body.appendChild(c);
|
||||
|
||||
var ctx = c.getContext("2d");
|
||||
var px = 2;
|
||||
var art = [
|
||||
"_BB_______", "BRRB______", "BRRRB_____", "BRRRRB____", "BRRRRRB___",
|
||||
"BRRRRRRB__", "BRRRRRRB__", "BRRRRSB___", "BRRSRRB___", "_BBBRRB___", "____BBB___"
|
||||
];
|
||||
var rows = art.length;
|
||||
var cols = Math.max.apply(null, art.map(function (r) { return r.length; }));
|
||||
c.width = cols * px; c.height = rows * px;
|
||||
c.style.width = cols * px + "px"; c.style.height = rows * px + "px";
|
||||
art.forEach(function (line, y) {
|
||||
for (var x = 0; x < line.length; x++) {
|
||||
var ch = line[x];
|
||||
if (ch === "B") ctx.fillStyle = "#1a1a1a";
|
||||
else if (ch === "R") ctx.fillStyle = "#ffffff";
|
||||
else if (ch === "S") ctx.fillStyle = "#b0b0b0";
|
||||
else continue;
|
||||
ctx.fillRect(x * px, y * px, px, px);
|
||||
}
|
||||
});
|
||||
|
||||
var mx = -200, my = -200, raf = 0, shown = false;
|
||||
function place() {
|
||||
raf = 0;
|
||||
c.style.transform = "translate3d(" + mx + "px," + my + "px,0)";
|
||||
if (!shown) { shown = true; c.style.opacity = "1"; }
|
||||
}
|
||||
function move(e) {
|
||||
mx = e.clientX; my = e.clientY;
|
||||
if (!raf) raf = requestAnimationFrame(place);
|
||||
}
|
||||
function hide() { shown = false; c.style.opacity = "0"; }
|
||||
|
||||
document.addEventListener("mousemove", move, { passive: true });
|
||||
document.addEventListener("mouseenter", function (e) { mx = e.clientX; my = e.clientY; place(); }, { passive: true });
|
||||
// hide when the pointer leaves the window or the page is navigating away
|
||||
document.addEventListener("mouseleave", hide, { passive: true });
|
||||
window.addEventListener("blur", hide);
|
||||
window.addEventListener("pagehide", hide);
|
||||
// over a cross-origin iframe (vimeo, doom) we get no mousemove, so hide ours
|
||||
// instead of leaving it frozen at the edge; it returns on the next move out
|
||||
document.addEventListener("mouseover", function (e) {
|
||||
if (e.target && e.target.tagName === "IFRAME") hide();
|
||||
}, true);
|
||||
})();
|
||||
@@ -0,0 +1,122 @@
|
||||
/* tui file browser — preview pane (image OR text panes), hover + freeze +
|
||||
keyboard. re-binds itself after pjax navigation (document listeners are added
|
||||
once; rows/preview are re-queried per page). */
|
||||
(function () {
|
||||
"use strict";
|
||||
var rows = [], img = null, texts = [], cap = null, cur = 0, frozen = false, container = null, player = null, pv = null;
|
||||
|
||||
function hideTexts() { texts.forEach(function (t) { t.hidden = true; }); }
|
||||
function showText(k) { texts.forEach(function (t) { t.hidden = (t.getAttribute("data-text") !== k); }); }
|
||||
function hidePlayer() { if (player) { player.hidden = true; if (pv && !pv.paused) pv.pause(); } }
|
||||
function showPlayer() { if (player) player.hidden = false; }
|
||||
function setFrozen(v) { frozen = v; if (container) container.classList.toggle("is-frozen", v); }
|
||||
|
||||
// tell the shell to "type" the matching command. navigable rows (those with an
|
||||
// href: ".." and the portfolio project folders) are directories → `cd`; the
|
||||
// preview-only leaves inside a project (images, info.md, video) are files → `cat`.
|
||||
function ghostCmd(a) {
|
||||
if (!a) return;
|
||||
var name = a.getAttribute("data-name") || "";
|
||||
var isDir = a.hasAttribute("href");
|
||||
document.dispatchEvent(new CustomEvent("shell:ghost", { detail: (isDir ? "cd " : "cat ") + name }));
|
||||
}
|
||||
function ghostClear() { document.dispatchEvent(new CustomEvent("shell:ghost-clear")); }
|
||||
function showImg(src) {
|
||||
if (!img) return;
|
||||
img.hidden = false;
|
||||
if (img.getAttribute("src") === src) { img.style.opacity = "1"; return; }
|
||||
img.style.opacity = "0";
|
||||
img.onload = img.onerror = function () { img.style.opacity = "1"; };
|
||||
img.src = src;
|
||||
if (img.complete) img.style.opacity = "1"; // already cached → no fade
|
||||
}
|
||||
|
||||
function select(i, silent) {
|
||||
if (!rows.length) return;
|
||||
if (i < 0) i = 0;
|
||||
if (i >= rows.length) i = rows.length - 1;
|
||||
cur = i;
|
||||
for (var r = 0; r < rows.length; r++) rows[r].classList.remove("is-selected");
|
||||
var a = rows[i];
|
||||
a.classList.add("is-selected");
|
||||
if (a.scrollIntoView) a.scrollIntoView({ block: "nearest" });
|
||||
|
||||
var src = a.getAttribute("data-img");
|
||||
var tkey = a.getAttribute("data-text");
|
||||
var vid = a.getAttribute("data-video");
|
||||
if (src) { showImg(src); hideTexts(); hidePlayer(); }
|
||||
else if (vid) { if (img) img.hidden = true; hideTexts(); showPlayer(); }
|
||||
else if (tkey) { if (img) img.hidden = true; hidePlayer(); showText(tkey); }
|
||||
else { if (img) img.hidden = true; hideTexts(); hidePlayer(); }
|
||||
|
||||
if (cap) {
|
||||
var n = a.querySelector(".tui__name");
|
||||
var y = a.querySelector(".tui__date");
|
||||
cap.textContent = (n ? n.textContent.trim() : a.getAttribute("data-name")) + (y ? " " + y.textContent.trim() : "");
|
||||
}
|
||||
|
||||
if (!silent) ghostCmd(a); // echo into the shell, except on the initial load
|
||||
}
|
||||
|
||||
function bind() {
|
||||
var list = document.querySelector(".filebrowser .tui__ls");
|
||||
container = list ? list.closest(".filebrowser") : null;
|
||||
rows = list ? Array.prototype.slice.call(list.querySelectorAll("a[data-name]")) : [];
|
||||
img = document.querySelector(".tui__preview img");
|
||||
texts = Array.prototype.slice.call(document.querySelectorAll(".tui__preview .tui__text"));
|
||||
player = document.querySelector(".tui__preview .player");
|
||||
pv = player ? player.querySelector("video") : null;
|
||||
cap = document.querySelector(".tui__status");
|
||||
cur = 0; frozen = false;
|
||||
if (!rows.length) return;
|
||||
|
||||
rows.forEach(function (a, i) {
|
||||
a.addEventListener("mouseenter", function () { if (!frozen) select(i); });
|
||||
a.addEventListener("click", function (e) {
|
||||
if (a.getAttribute("href")) return; // navigation rows behave normally
|
||||
e.preventDefault();
|
||||
if (frozen && cur === i) { setFrozen(false); ghostClear(); }
|
||||
else { select(i); setFrozen(true); }
|
||||
});
|
||||
});
|
||||
|
||||
var start = rows.findIndex(function (a) { return a.classList.contains("is-selected"); });
|
||||
select(start < 0 ? 0 : start, true); // initial selection: don't echo into the shell yet
|
||||
|
||||
// warm the cache so switching between gallery images is instant
|
||||
var srcs = rows.map(function (a) { return a.getAttribute("data-img"); }).filter(Boolean);
|
||||
if (srcs.length) {
|
||||
var preload = function () { srcs.forEach(function (s) { var im = new Image(); im.src = s; }); };
|
||||
if (window.requestIdleCallback) requestIdleCallback(preload); else setTimeout(preload, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// one-time document listeners (reference the current bound state)
|
||||
document.addEventListener("click", function (e) {
|
||||
if (e.target.closest && e.target.closest(".tui__ls a")) return;
|
||||
if (frozen) { setFrozen(false); ghostClear(); }
|
||||
});
|
||||
document.addEventListener("keydown", function (e) {
|
||||
var t = e.target;
|
||||
if (t && (t.isContentEditable || /^(input|textarea|select)$/i.test(t.tagName))) return;
|
||||
if (!rows.length) return;
|
||||
if (e.key === "ArrowDown") { e.preventDefault(); select(cur + 1); }
|
||||
else if (e.key === "ArrowUp") { e.preventDefault(); select(cur - 1); }
|
||||
else if (e.key === "Enter") {
|
||||
var href = rows[cur] && rows[cur].getAttribute("href");
|
||||
if (href) { if (window.pjax) window.pjax(href); else window.location.href = href; }
|
||||
}
|
||||
});
|
||||
|
||||
// the always-open home menu lives in the persistent header (outside the
|
||||
// filebrowser and untouched by pjax). hovering a folder ghost-types `cd <name>`
|
||||
// so the shell echoes where a click would take you, until you type for real.
|
||||
Array.prototype.forEach.call(document.querySelectorAll(".menu .tui__ls a[data-name]"), function (a) {
|
||||
a.addEventListener("mouseenter", function () {
|
||||
document.dispatchEvent(new CustomEvent("shell:ghost", { detail: "cd " + (a.getAttribute("data-name") || "") }));
|
||||
});
|
||||
});
|
||||
|
||||
bind();
|
||||
document.addEventListener("pjax:load", bind);
|
||||
})();
|
||||
@@ -0,0 +1,101 @@
|
||||
/* pjax — client-side navigation. swaps the page content instead of a full
|
||||
reload, so the cursor, shell and theme stay alive (no flashing, no re-fetch
|
||||
of css/js). pure progressive enhancement: if fetch/history/DOMParser are
|
||||
missing, or anything goes wrong, links fall back to normal navigation. */
|
||||
(function () {
|
||||
"use strict";
|
||||
if (!window.history || !window.fetch || !window.DOMParser) return;
|
||||
var MAIN = "#main-content";
|
||||
if (!document.querySelector(MAIN)) return;
|
||||
|
||||
function internal(a) {
|
||||
return a && a.href && a.origin === location.origin &&
|
||||
a.getAttribute("href").charAt(0) !== "#" &&
|
||||
!a.hasAttribute("download") &&
|
||||
(a.getAttribute("target") || "") !== "_blank";
|
||||
}
|
||||
|
||||
function swap(html, url) {
|
||||
var doc = new DOMParser().parseFromString(html, "text/html");
|
||||
var nm = doc.querySelector(MAIN), om = document.querySelector(MAIN);
|
||||
if (!nm || !om) { location.href = url; return; }
|
||||
|
||||
om.innerHTML = nm.innerHTML;
|
||||
if (doc.title) document.title = doc.title;
|
||||
document.body.className = doc.body.className;
|
||||
|
||||
// breadcrumb (in the persistent header) — swap its inner links
|
||||
var np = doc.querySelector(".prompt__path"), op = document.querySelector(".prompt__path");
|
||||
if (np && op) op.innerHTML = np.innerHTML;
|
||||
|
||||
// per-section typeable routes live in .topbar — replace them
|
||||
var topbar = document.querySelector(".topbar");
|
||||
|
||||
// language switch (persistent header) — repoint it at THIS page's
|
||||
// translation, otherwise it keeps the target of the first-loaded page
|
||||
var nl = doc.querySelector(".lang-switch"), ol = document.querySelector(".lang-switch");
|
||||
if (ol && ol.parentNode) ol.parentNode.removeChild(ol);
|
||||
if (nl && topbar) topbar.insertBefore(document.importNode(nl, true), topbar.firstChild);
|
||||
var oldR = document.querySelector(".routes");
|
||||
if (oldR && oldR.parentNode) oldR.parentNode.removeChild(oldR);
|
||||
var newR = doc.querySelector(".routes");
|
||||
if (newR && topbar) topbar.appendChild(document.importNode(newR, true));
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
document.dispatchEvent(new CustomEvent("pjax:load"));
|
||||
}
|
||||
|
||||
// prefetch cache: hovering a link fetches its HTML so the click is instant
|
||||
var cache = {};
|
||||
function fetchHTML(url) {
|
||||
return fetch(url, { credentials: "same-origin" })
|
||||
.then(function (r) { if (!r.ok) throw new Error(r.status); return r.text(); });
|
||||
}
|
||||
function prefetch(url) {
|
||||
if (!cache[url]) cache[url] = fetchHTML(url).catch(function () { delete cache[url]; return null; });
|
||||
}
|
||||
|
||||
// which language a URL belongs to (de lives under /de/, en is the default)
|
||||
function langOf(u) {
|
||||
var p;
|
||||
try { p = new URL(u, location.origin).pathname; } catch (e) { p = u; }
|
||||
return /^\/de(\/|$)/.test(p) ? "de" : "en";
|
||||
}
|
||||
|
||||
var busy = false;
|
||||
function go(url, push) {
|
||||
if (busy) return;
|
||||
// a language change rebuilds the whole chrome (menu, html lang, switch),
|
||||
// so do a real reload instead of a partial content swap
|
||||
if (langOf(url) !== (document.documentElement.lang || "en")) { location.href = url; return; }
|
||||
busy = true;
|
||||
(cache[url] || fetchHTML(url))
|
||||
.then(function (html) {
|
||||
busy = false;
|
||||
if (!html) { location.href = url; return; } // prefetch had failed
|
||||
if (push) history.pushState({ pjax: true }, "", url);
|
||||
swap(html, url);
|
||||
})
|
||||
.catch(function () { busy = false; location.href = url; });
|
||||
}
|
||||
|
||||
document.addEventListener("mouseover", function (e) {
|
||||
var a = e.target.closest ? e.target.closest("a[href]") : null;
|
||||
if (internal(a) && a.href.split("#")[0] !== location.href.split("#")[0]) prefetch(a.href);
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener("click", function (e) {
|
||||
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
||||
var a = e.target.closest ? e.target.closest("a[href]") : null;
|
||||
if (!internal(a)) return;
|
||||
if (a.href.split("#")[0] === location.href.split("#")[0]) return; // same document
|
||||
e.preventDefault();
|
||||
go(a.href, true);
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", function () { go(location.href, false); });
|
||||
history.replaceState({ pjax: true }, "", location.href);
|
||||
|
||||
// bridge for programmatic navigation (shell commands, filebrowser Enter)
|
||||
window.pjax = function (url) { go(url, true); };
|
||||
})();
|
||||
@@ -0,0 +1,50 @@
|
||||
/* minimal terminal-style video player — custom controls in our own font.
|
||||
re-inits after pjax navigation. */
|
||||
(function () {
|
||||
"use strict";
|
||||
function fmt(t) {
|
||||
t = Math.max(0, t | 0);
|
||||
var m = (t / 60) | 0, s = t % 60;
|
||||
return (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s;
|
||||
}
|
||||
function init(root) {
|
||||
var v = root.querySelector("video");
|
||||
var play = root.querySelector(".player__play");
|
||||
var big = root.querySelector(".player__big");
|
||||
var scrub = root.querySelector(".player__scrub");
|
||||
var fill = root.querySelector(".player__fill");
|
||||
var time = root.querySelector(".player__time");
|
||||
var mute = root.querySelector(".player__mute");
|
||||
var full = root.querySelector(".player__full");
|
||||
if (!v) return;
|
||||
|
||||
function toggle() { if (v.paused) v.play(); else v.pause(); }
|
||||
if (play) play.addEventListener("click", toggle);
|
||||
if (big) big.addEventListener("click", toggle);
|
||||
v.addEventListener("click", toggle);
|
||||
|
||||
v.addEventListener("play", function () { root.classList.add("is-playing"); if (play) play.textContent = "❚❚"; });
|
||||
v.addEventListener("pause", function () { root.classList.remove("is-playing"); if (play) play.textContent = "▶"; });
|
||||
v.addEventListener("loadedmetadata", function () { if (time) time.textContent = "00:00 / " + fmt(v.duration); });
|
||||
v.addEventListener("timeupdate", function () {
|
||||
var p = v.duration ? v.currentTime / v.duration : 0;
|
||||
if (fill) fill.style.width = (p * 100) + "%";
|
||||
if (time) time.textContent = fmt(v.currentTime) + " / " + fmt(v.duration || 0);
|
||||
});
|
||||
if (scrub) scrub.addEventListener("click", function (e) {
|
||||
var r = scrub.getBoundingClientRect();
|
||||
if (v.duration) v.currentTime = ((e.clientX - r.left) / r.width) * v.duration;
|
||||
});
|
||||
if (mute) mute.addEventListener("click", function () { v.muted = !v.muted; mute.textContent = v.muted ? "muted" : "vol"; });
|
||||
if (full) full.addEventListener("click", function () {
|
||||
if (document.fullscreenElement) document.exitFullscreen();
|
||||
else if (root.requestFullscreen) root.requestFullscreen();
|
||||
});
|
||||
}
|
||||
function initAll() {
|
||||
var ps = document.querySelectorAll(".player");
|
||||
for (var i = 0; i < ps.length; i++) if (!ps[i].__init) { ps[i].__init = 1; init(ps[i]); }
|
||||
}
|
||||
initAll();
|
||||
document.addEventListener("pjax:load", initAll);
|
||||
})();
|
||||
@@ -0,0 +1,345 @@
|
||||
/* header prompt → a tiny playful shell.
|
||||
type a command + Enter: known targets navigate, anything else is an
|
||||
easter egg (a fake shell error). the menu below is always "open" and
|
||||
clickable, so typing is optional. */
|
||||
(function () {
|
||||
"use strict";
|
||||
var bar = document.querySelector(".topbar");
|
||||
var input = document.querySelector(".prompt__input");
|
||||
var out = document.querySelector(".prompt__out");
|
||||
var menu = document.querySelector(".menu");
|
||||
if (!bar || !input) return;
|
||||
|
||||
var home = bar.getAttribute("data-home") || "/";
|
||||
|
||||
// command → url: the always-open menu, plus any context routes (e.g. the
|
||||
// portfolio project slugs, only present in the DOM while inside /portfolio/).
|
||||
// rebuilt after each pjax navigation since .routes changes per section.
|
||||
var routes = {};
|
||||
function buildRoutes() {
|
||||
routes = {};
|
||||
Array.prototype.forEach.call(document.querySelectorAll(".menu a, .routes a, footer a[data-name], .lang-switch"), function (a) {
|
||||
var key = (a.getAttribute("data-name") || a.textContent).trim().toLowerCase().replace(/\/+$/, "");
|
||||
if (key) routes[key] = a.getAttribute("href");
|
||||
});
|
||||
}
|
||||
buildRoutes();
|
||||
document.addEventListener("pjax:load", buildRoutes);
|
||||
|
||||
function pwd() {
|
||||
var p = document.querySelector(".prompt__path");
|
||||
return p ? p.textContent.replace(/^:/, "").replace(/\$$/, "") : "~";
|
||||
}
|
||||
|
||||
function say(msg) { if (out) out.textContent = msg; }
|
||||
function navigate(u) { clearGhost(); input.textContent = ""; if (window.pjax) window.pjax(u); else window.location.href = u; }
|
||||
|
||||
// ghost command: the file browser "types" e.g. `cat 03.webp` into the prompt as
|
||||
// you hover/freeze a file, so you always see where you are. it's only a hint —
|
||||
// any real keystroke (or navigation) discards it and the line is yours again.
|
||||
var ghostActive = false;
|
||||
function setGhost(text) {
|
||||
if (!ghostActive && input.textContent) return; // never clobber a real command in progress
|
||||
input.textContent = text;
|
||||
ghostActive = true;
|
||||
input.classList.add("is-ghost");
|
||||
caretToEnd();
|
||||
}
|
||||
function clearGhost() {
|
||||
if (!ghostActive) return;
|
||||
input.textContent = "";
|
||||
ghostActive = false;
|
||||
input.classList.remove("is-ghost");
|
||||
}
|
||||
document.addEventListener("shell:ghost", function (e) { setGhost(e.detail); });
|
||||
document.addEventListener("shell:ghost-clear", clearGhost);
|
||||
|
||||
function pad(n) { return (n < 10 ? "0" : "") + n; }
|
||||
function dateStr() {
|
||||
var d = new Date();
|
||||
var days = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
||||
var mons = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
||||
return days[d.getDay()] + " " + mons[d.getMonth()] + " " + pad(d.getDate()) + " " + d.getFullYear();
|
||||
}
|
||||
function timeStr() {
|
||||
var d = new Date();
|
||||
return pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds());
|
||||
}
|
||||
function infoStr() {
|
||||
var lines = [];
|
||||
var info = bar.getAttribute("data-info");
|
||||
var email = bar.getAttribute("data-email");
|
||||
if (info) lines.push(info);
|
||||
if (email) lines.push(email);
|
||||
lines.push("type: portfolio · about · studio · openbureau");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// command history, kept for the whole tab session (survives navigation)
|
||||
var hist = [];
|
||||
try { hist = JSON.parse(sessionStorage.getItem("kgv_hist") || "[]"); } catch (e) { hist = []; }
|
||||
var hpos = hist.length;
|
||||
function remember(cmd) {
|
||||
if (hist[hist.length - 1] !== cmd) hist.push(cmd);
|
||||
hpos = hist.length;
|
||||
try { sessionStorage.setItem("kgv_hist", JSON.stringify(hist.slice(-100))); } catch (e) {}
|
||||
}
|
||||
function historyStr() {
|
||||
if (!hist.length) return "(empty)";
|
||||
return hist.map(function (c, i) { return (" " + (i + 1)).slice(-4) + " " + c; }).join("\n");
|
||||
}
|
||||
|
||||
function unameStr(lc) {
|
||||
return lc === "uname -a" ? "kgv-sh 1.0 architecture lucerne ch" : "kgv-sh";
|
||||
}
|
||||
|
||||
// ls -a reveals the hidden dotfiles
|
||||
function lsa() {
|
||||
var items = ["./", "../"];
|
||||
Array.prototype.forEach.call(document.querySelectorAll(".menu a"), function (a) {
|
||||
items.push((a.getAttribute("data-name") || "") + "/");
|
||||
});
|
||||
items.push(".colophon", ".legal-notice");
|
||||
return items.join(" ");
|
||||
}
|
||||
|
||||
function tree() {
|
||||
var work = [];
|
||||
Array.prototype.forEach.call(document.querySelectorAll(".routes a"), function (a) {
|
||||
work.push(a.getAttribute("data-name"));
|
||||
});
|
||||
var l = ["."];
|
||||
l.push("├── portfolio/");
|
||||
work.forEach(function (s, i) { l.push("│ " + (i === work.length - 1 ? "└── " : "├── ") + s + "/"); });
|
||||
l.push("├── about/");
|
||||
l.push("├── studio/");
|
||||
l.push("├── openbureau/");
|
||||
l.push("├── .colophon");
|
||||
l.push("└── .legal-notice");
|
||||
return l.join("\n");
|
||||
}
|
||||
|
||||
function fetchStr() {
|
||||
var up = Math.max(1, Math.round((window.performance && performance.now ? performance.now() : 0) / 1000));
|
||||
return [
|
||||
" _ karimgabrielevarano",
|
||||
" | | __ -------------------",
|
||||
" | |/ / os: kgv-sh 1.0",
|
||||
" | < location: lucerne",
|
||||
" |_|\\_\\ degree: ba architecture",
|
||||
" status: ma in progress",
|
||||
" uptime: " + up + "s",
|
||||
" ~: portfolio · about · studio · openbureau"
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function setTheme(mode) {
|
||||
var dark = mode === "dark";
|
||||
document.documentElement.classList.toggle("dark", dark);
|
||||
try { localStorage.setItem("kgv_theme", dark ? "dark" : "light"); } catch (e) {}
|
||||
}
|
||||
|
||||
// `sl` — a steam locomotive rolls across the screen (the classic `ls` typo gag)
|
||||
var TRAIN = [
|
||||
" ==== ________ ___________ ",
|
||||
" _D _| |_______/ \\__I_I_____===__|_________| ",
|
||||
" |(_)--- | H\\________/ | | =|___ ___| ",
|
||||
" / | | H | | | | ||_| |_|| ",
|
||||
" | | | H |__--------------------| [___] | ",
|
||||
" | ________|___H__/__|_____/[][]~\\_______| | ",
|
||||
" |/ | |-----------I_____I [][] [] D |=======|__ ",
|
||||
"__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ",
|
||||
" |/-=|___|= || || || |_____/~\\___/ ",
|
||||
" \\_/ \\O=====O=====O=====O_/ \\_/ "
|
||||
].join("\n");
|
||||
function sl() {
|
||||
if (document.querySelector(".sl")) return;
|
||||
var pre = document.createElement("pre");
|
||||
pre.className = "sl";
|
||||
pre.textContent = TRAIN;
|
||||
document.body.appendChild(pre);
|
||||
pre.style.transform = "translateX(" + window.innerWidth + "px)";
|
||||
requestAnimationFrame(function () {
|
||||
pre.style.transition = "transform 4s linear";
|
||||
pre.style.transform = "translateX(-" + (pre.offsetWidth + 40) + "px)";
|
||||
});
|
||||
setTimeout(function () { pre.remove(); }, 4200);
|
||||
}
|
||||
|
||||
// secret: `doom` boots shareware DOOM (id Software) in a draggable window
|
||||
function doom() {
|
||||
if (document.querySelector(".doom-window")) return;
|
||||
var win = document.createElement("div");
|
||||
win.className = "doom-window";
|
||||
var bar = document.createElement("div");
|
||||
bar.className = "doom-window__bar";
|
||||
var label = document.createElement("span");
|
||||
label.className = "doom-window__title";
|
||||
label.textContent = "doom";
|
||||
var close = document.createElement("button");
|
||||
close.className = "doom-window__close";
|
||||
close.setAttribute("aria-label", "close");
|
||||
close.textContent = "✕";
|
||||
var frame = document.createElement("iframe");
|
||||
frame.src = "https://archive.org/embed/DoomsharewareEpisode";
|
||||
frame.setAttribute("allowfullscreen", "");
|
||||
frame.allow = "fullscreen; autoplay; gamepad";
|
||||
bar.appendChild(label);
|
||||
bar.appendChild(close);
|
||||
win.appendChild(bar);
|
||||
win.appendChild(frame);
|
||||
document.body.appendChild(win);
|
||||
|
||||
function kill() { win.remove(); document.removeEventListener("keydown", esc); }
|
||||
function esc(e) { if (e.key === "Escape") kill(); }
|
||||
close.addEventListener("click", kill);
|
||||
document.addEventListener("keydown", esc);
|
||||
|
||||
// drag by the title bar (shield the iframe so it doesn't swallow the mouse)
|
||||
bar.addEventListener("mousedown", function (e) {
|
||||
if (e.target === close) return;
|
||||
var r = win.getBoundingClientRect();
|
||||
win.style.left = r.left + "px";
|
||||
win.style.top = r.top + "px";
|
||||
win.style.transform = "none";
|
||||
var dx = e.clientX - r.left, dy = e.clientY - r.top;
|
||||
frame.style.pointerEvents = "none";
|
||||
function mv(ev) { win.style.left = (ev.clientX - dx) + "px"; win.style.top = (ev.clientY - dy) + "px"; }
|
||||
function up() { frame.style.pointerEvents = ""; document.removeEventListener("mousemove", mv); document.removeEventListener("mouseup", up); }
|
||||
document.addEventListener("mousemove", mv);
|
||||
document.addEventListener("mouseup", up);
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
function egg(lc, raw) {
|
||||
var arg0 = raw.split(/\s+/)[0];
|
||||
if (/^sudo\s+make\s+me\s+a\s+sandwich$/.test(lc)) return "okay.";
|
||||
if (/^make\s+me\s+a\s+sandwich$/.test(lc)) return "what? make it yourself.";
|
||||
if (/^sudo\b/.test(lc) || lc === "su") return "permission denied: nice try.";
|
||||
if (/^rm\b/.test(lc)) return "rm: some things you keep.";
|
||||
if (lc === "whoami") return "karim";
|
||||
if (lc === "pwd") return pwd();
|
||||
if (lc === "help" || /^man\b/.test(lc)) return "nav: portfolio · about · studio · openbureau · cv\nbasics: info · contact · date · time · tree · fetch · history\ntheme: dark · light";
|
||||
if (lc === "hello" || lc === "hi" || lc === "hey") return "hi :)";
|
||||
if (lc === "exit" || lc === "logout") return "there is no exit. only architecture.";
|
||||
if (lc === "coffee" || lc === "brew") return "☕ brewing… (418: i'm a teapot)";
|
||||
if (/^(cat|open|vim|nano|emacs|less)\b/.test(lc)) return arg0 + ": permission denied";
|
||||
return "zsh: command not found: " + arg0;
|
||||
}
|
||||
|
||||
// tab-completion over known commands + navigable routes
|
||||
var COMMANDS = ["help", "man", "info", "contact", "date", "time", "tree", "fetch", "neofetch",
|
||||
"uname", "history", "clear", "echo", "pwd", "whoami", "ls", "cd", "cat", "open",
|
||||
"dark", "light", "theme", "sl", "doom", "coffee"];
|
||||
function complete() {
|
||||
var val = input.textContent;
|
||||
var parts = val.split(" ");
|
||||
var last = (parts[parts.length - 1] || "").toLowerCase();
|
||||
if (!last) return;
|
||||
var pool = parts.length > 1 ? Object.keys(routes) : COMMANDS.concat(Object.keys(routes));
|
||||
var matches = [];
|
||||
for (var i = 0; i < pool.length; i++) {
|
||||
if (pool[i].indexOf(last) === 0 && matches.indexOf(pool[i]) < 0) matches.push(pool[i]);
|
||||
}
|
||||
if (!matches.length) return;
|
||||
var common = matches.reduce(function (a, b) {
|
||||
var n = 0; while (n < a.length && a.charAt(n) === b.charAt(n)) n++; return a.slice(0, n);
|
||||
});
|
||||
parts[parts.length - 1] = matches.length === 1 ? matches[0] : common;
|
||||
input.textContent = parts.join(" ");
|
||||
caretToEnd();
|
||||
say(matches.length > 1 ? matches.join(" ") : ""); // list options when ambiguous
|
||||
}
|
||||
|
||||
function run(raw) {
|
||||
var cmd = raw.replace(/\s+/g, " ").trim();
|
||||
if (!cmd) return;
|
||||
var lc = cmd.toLowerCase();
|
||||
remember(cmd);
|
||||
|
||||
if (lc === "clear") { say(""); input.textContent = ""; return; }
|
||||
if (lc === "doom") { doom(); say(""); input.textContent = ""; return; }
|
||||
if (lc === "sl") { sl(); say(""); input.textContent = ""; return; }
|
||||
if (lc === "dark" || lc === "light") { setTheme(lc); say(""); input.textContent = ""; return; }
|
||||
if (lc === "theme") { var next = document.documentElement.classList.contains("dark") ? "light" : "dark"; setTheme(next); say("theme: " + next); input.textContent = ""; return; }
|
||||
|
||||
// strip a leading verb, leading ./ and trailing slash
|
||||
var t = lc.replace(/^(cd|ls|cat|open)\b\s*/, "").replace(/^\.\//, "").replace(/\/+$/, "").trim();
|
||||
|
||||
// `ls` of the current/home dir — the listing is already on screen
|
||||
if (/^ls\b/.test(lc) && (t === "" || t === "~" || t === ".")) { say(""); input.textContent = ""; return; }
|
||||
// home / up
|
||||
if (t === "" || t === "~" || t === ".." || t === "." || t === "home" || t === "/") { navigate(home); return; }
|
||||
// a known menu target
|
||||
if (routes[t]) { navigate(routes[t]); return; }
|
||||
|
||||
// basics that just print
|
||||
if (lc === "info") { say(infoStr()); input.textContent = ""; return; }
|
||||
if (lc === "contact") { say(bar.getAttribute("data-email") || ""); input.textContent = ""; return; }
|
||||
if (lc === "date") { say(dateStr()); input.textContent = ""; return; }
|
||||
if (lc === "time") { say(timeStr()); input.textContent = ""; return; }
|
||||
if (lc === "history") { say(historyStr()); input.textContent = ""; return; }
|
||||
if (lc === "tree") { say(tree()); input.textContent = ""; return; }
|
||||
if (lc === "fetch" || lc === "neofetch") { say(fetchStr()); input.textContent = ""; return; }
|
||||
if (lc === "uname" || lc === "uname -a") { say(unameStr(lc)); input.textContent = ""; return; }
|
||||
if (/^ls\s+-(la|al|a)$/.test(lc) || lc === "ls --all") { say(lsa()); input.textContent = ""; return; }
|
||||
if (lc === "echo" || lc.indexOf("echo ") === 0) { say(cmd.slice(4).trim()); input.textContent = ""; return; }
|
||||
|
||||
say(egg(lc, cmd)); // unknown → shell error, fresh empty prompt
|
||||
input.textContent = "";
|
||||
}
|
||||
|
||||
input.addEventListener("keydown", function (e) {
|
||||
// a real keystroke wins over the file-browser's ghost command
|
||||
if (ghostActive && !/^(Shift|Control|Alt|Meta|CapsLock)$/.test(e.key)) clearGhost();
|
||||
if (e.key === "Enter") { e.preventDefault(); run(input.textContent); return; }
|
||||
if (e.key === "Tab") { e.preventDefault(); complete(); return; }
|
||||
if (e.key === "ArrowUp" && hist.length) {
|
||||
e.preventDefault();
|
||||
hpos = Math.max(0, hpos - 1);
|
||||
input.textContent = hist[hpos] || "";
|
||||
caretToEnd(); say("");
|
||||
} else if (e.key === "ArrowDown" && hist.length) {
|
||||
e.preventDefault();
|
||||
hpos = Math.min(hist.length, hpos + 1);
|
||||
input.textContent = hist[hpos] || "";
|
||||
caretToEnd(); say("");
|
||||
}
|
||||
});
|
||||
input.addEventListener("input", function () { say(""); });
|
||||
|
||||
function caretToEnd() {
|
||||
var r = document.createRange();
|
||||
r.selectNodeContents(input);
|
||||
r.collapse(false);
|
||||
var s = window.getSelection();
|
||||
s.removeAllRanges();
|
||||
s.addRange(r);
|
||||
}
|
||||
|
||||
// start typing anywhere → the keystroke goes straight into the command line
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (document.querySelector(".doom-window")) return; // playing doom
|
||||
var t = e.target;
|
||||
if (t === input) return; // already typing
|
||||
if (t && (t.isContentEditable || /^(input|textarea|select)$/i.test(t.tagName))) return;
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
if (e.key && e.key.length === 1) { // a printable character
|
||||
e.preventDefault();
|
||||
if (ghostActive) clearGhost(); // discard the ghost, type for real
|
||||
input.focus();
|
||||
input.textContent += e.key;
|
||||
caretToEnd();
|
||||
say("");
|
||||
}
|
||||
});
|
||||
|
||||
// clicking anywhere on the bar focuses the prompt (but let links + text selection through)
|
||||
bar.addEventListener("click", function (e) {
|
||||
if (e.target.closest("a")) return;
|
||||
if (window.getSelection && String(window.getSelection())) return;
|
||||
input.focus();
|
||||
caretToEnd();
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user