redesign the site as a technical minimalist TUI

Replace the glassy hero treatment with a terminal and TUI vocabulary
while keeping the layout minimal:

- Switch the type stack to JetBrains Mono and flatten every surface:
  hairline borders, square corners, no blur or glow, and a faint
  graph-paper grid in place of the radial gradients.
- Rebuild the home hero as a terminal window whose output is typed in
  on load; the old mouse-parallax glow script becomes that typing
  sequence, and it no-ops under prefers-reduced-motion.
- Give the projects, about, and entry panels title bars, and render
  cards with a metadata strip, status dot, ISO date, and prompt-style
  links.
- Hide the inline nav behind the hamburger on narrow screens; the old
  rule was outranked by the #menu id selector and never applied.
- Add an SVG favicon, which also clears the only 404 on the site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
SowinskiBraedenandClaude Opus 5 committed 2026-10-17 23:41:38 -07:00
1 parent 2e67914fd6
commit 8595290318
10 files changed
+1270 -1167

No files matched your search

+39 -12
View File
@@ -134,12 +134,12 @@
const renderLinks = (entry, className) => {
const links = [];
const sourceLabel = entry.github && entry.github.includes("git.sowinski.dev") ? "Gitea" : "GitHub";
const sourceLabel = entry.github && entry.github.includes("git.sowinski.dev") ? "gitea" : "github";
links.push(`<a class="${className} ${className}-primary" href="./entry.html?type=${encodeURIComponent(entry.type)}&slug=${encodeURIComponent(entry.slug)}">Read More</a>`);
links.push(`<a class="${className} ${className}-primary" href="./entry.html?type=${encodeURIComponent(entry.type)}&slug=${encodeURIComponent(entry.slug)}">details</a>`);
if (entry.demo) {
links.push(`<a class="${className}" href="${escapeHtml(entry.demo)}" target="_blank" rel="noopener noreferrer">Live</a>`);
links.push(`<a class="${className}" href="${escapeHtml(entry.demo)}" target="_blank" rel="noopener noreferrer">live</a>`);
} else if (entry.github) {
links.push(`<a class="${className}" href="${escapeHtml(entry.github)}" target="_blank" rel="noopener noreferrer">${sourceLabel}</a>`);
}
@@ -147,8 +147,21 @@
return `<div class="${className.replace("card-link", "card-links")}">${links.join("")}</div>`;
};
const statusState = (entry) => {
const status = String(entry.status || "").toLowerCase();
if (status.startsWith("active")) {
return "active";
}
if (entry.type === "updates") {
return "attention";
}
return "idle";
};
const createCard = (entry) => {
const formattedDate = formatDate(entry.date);
const featuredClass = entry.featured ? " featured" : "";
const anchorId = entry.slug ? ` id="${escapeHtml(entry.slug)}"` : "";
@@ -156,7 +169,8 @@
<article class="content-card${featuredClass}"${anchorId}>
<div class="card-meta">
<span class="card-kicker">${escapeHtml(entry.label || entry.type)}</span>
${formattedDate ? `<span class="card-date">${escapeHtml(formattedDate)}</span>` : ""}
${entry.date ? `<span class="card-date">${escapeHtml(entry.date)}</span>` : ""}
${entry.status ? `<span class="card-status" data-state="${statusState(entry)}">${escapeHtml(entry.status.toLowerCase())}</span>` : ""}
</div>
<h3>${escapeHtml(entry.title)}</h3>
<p class="card-summary">${escapeHtml(entry.summary || "")}</p>
@@ -166,6 +180,15 @@
`;
};
const setSectionCount = (countId, entries) => {
const element = document.getElementById(countId);
if (!element) {
return;
}
element.textContent = `${entries.length} ${entries.length === 1 ? "entry" : "entries"}`;
};
const renderSection = (containerId, entries, emptyMessage) => {
const container = document.getElementById(containerId);
if (!container) {
@@ -201,7 +224,7 @@
card.classList.toggle("is-hidden", !expanded && index >= defaultVisibleCount);
});
toggleButton.textContent = expanded ? "Show Fewer Projects" : "Show More Projects";
toggleButton.textContent = expanded ? "show fewer projects" : "show more projects";
toggleButton.setAttribute("aria-expanded", String(expanded));
};
@@ -255,8 +278,12 @@
const index = await getIndex();
const sortedProjects = [...(index.projects || [])].sort(compareByPriorityOrder);
renderSection("projects-grid", sortedProjects, "No projects added yet.");
setSectionCount("projects-count", sortedProjects);
initProjectsToggle(sortedProjects);
renderSection("updates-grid", [...(index.updates || [])].sort(compareByNewestDate), "No updates added yet.");
const sortedUpdates = [...(index.updates || [])].sort(compareByNewestDate);
renderSection("updates-grid", sortedUpdates, "No updates added yet.");
setSectionCount("updates-count", sortedUpdates);
} catch (error) {
renderSection("projects-grid", [], error.message);
renderSection("updates-grid", [], error.message);
@@ -279,7 +306,7 @@
<div class="entry-empty">
<p class="eyebrow">Not found</p>
<h1 class="entry-title">This entry could not be loaded.</h1>
<a class="entry-back" href="./projects.html">Back to projects</a>
<a class="entry-back" href="./projects.html">cd ../projects</a>
</div>
`;
return;
@@ -305,7 +332,7 @@
shell.innerHTML = `
<div class="entry-header">
<a class="entry-back" href="./projects.html">Back to projects</a>
<a class="entry-back" href="./projects.html">cd ../projects</a>
<div class="entry-meta">
<span class="entry-pill entry-pill-primary">${escapeHtml(entry.label || entry.type)}</span>
${entry.date ? `<span class="entry-pill entry-pill-secondary">${escapeHtml(formatDate(entry.date))}</span>` : ""}
@@ -315,8 +342,8 @@
<p class="entry-summary">${escapeHtml(entry.summary || "")}</p>
${renderTags(entry.tags || [], "entry-tag-list")}
<div class="entry-links">
${entry.demo ? `<a class="inline-button inline-button-primary" href="${escapeHtml(entry.demo)}" target="_blank" rel="noopener noreferrer">Live</a>` : ""}
${entry.github ? `<a class="inline-button" href="${escapeHtml(entry.github)}" target="_blank" rel="noopener noreferrer">${entry.github.includes("git.sowinski.dev") ? "Gitea" : "GitHub"}</a>` : ""}
${entry.demo ? `<a class="inline-button inline-button-primary" href="${escapeHtml(entry.demo)}" target="_blank" rel="noopener noreferrer">live</a>` : ""}
${entry.github ? `<a class="inline-button" href="${escapeHtml(entry.github)}" target="_blank" rel="noopener noreferrer">${entry.github.includes("git.sowinski.dev") ? "gitea" : "github"}</a>` : ""}
</div>
</div>
<article class="entry-content">
@@ -328,7 +355,7 @@
<div class="entry-empty">
<p class="eyebrow">Not found</p>
<h1 class="entry-title">${escapeHtml(error.message)}</h1>
<a class="entry-back" href="./projects.html">Back to projects</a>
<a class="entry-back" href="./projects.html">cd ../projects</a>
</div>
`;
}
+41 -41
View File
@@ -1,62 +1,62 @@
(function() {
const heroShell = document.querySelector(".hero-main .hero-shell");
const stream = document.querySelector(".term__stream");
if (!heroShell) {
if (!stream) {
return;
}
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const coarsePointer = window.matchMedia("(pointer: coarse)").matches;
if (prefersReducedMotion || coarsePointer) {
if (prefersReducedMotion) {
return;
}
let targetX = 0;
let targetY = 0;
let currentX = 0;
let currentY = 0;
const CHAR_DELAY = 18;
const AFTER_COMMAND_DELAY = 90;
const AFTER_OUTPUT_DELAY = 130;
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
// Build the playback script before hiding anything, so a thrown error here
// leaves the page in its fully rendered state.
const steps = Array.from(stream.children).map((element) => ({
element,
isCommand: element.classList.contains("cmd"),
text: element.textContent
}));
const animate = () => {
currentX += (targetX - currentX) * 0.065;
currentY += (targetY - currentY) * 0.065;
if (!steps.some((step) => step.isCommand)) {
return;
}
const bgX = 50 + currentX * 6.8;
const bgY = 18 + currentY * 5.6;
const bgXAlt = 50 - currentX * 5.2;
const bgYAlt = 30 - currentY * 4.6;
const glowRotate = currentX * 16 + currentY * 10;
const glowShiftX = currentX * 54;
const glowShiftY = currentY * 42;
steps.forEach((step) => {
if (step.isCommand) {
step.element.textContent = "";
return;
}
document.body.style.setProperty("--hero-glow-x", `${bgX}%`);
document.body.style.setProperty("--hero-glow-y", `${bgY}%`);
document.body.style.setProperty("--hero-glow-alt-x", `${bgXAlt}%`);
document.body.style.setProperty("--hero-glow-alt-y", `${bgYAlt}%`);
document.body.style.setProperty("--hero-glow-rotate", `${glowRotate}deg`);
document.body.style.setProperty("--hero-glow-shift-x", `${glowShiftX}px`);
document.body.style.setProperty("--hero-glow-shift-y", `${glowShiftY}px`);
step.element.classList.add("is-typing-hidden");
});
heroShell.style.setProperty("--hero-shell-spot-x", `${50 + currentX * 14}%`);
heroShell.style.setProperty("--hero-shell-spot-y", `${28 + currentY * 10}%`);
const wait = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
requestAnimationFrame(animate);
const typeCommand = async (element, text) => {
for (let index = 0; index < text.length; index += 1) {
element.textContent = text.slice(0, index + 1);
await wait(CHAR_DELAY);
}
};
window.addEventListener("mousemove", (event) => {
const x = clamp((event.clientX / window.innerWidth - 0.5) * 2, -1, 1);
const y = clamp((event.clientY / window.innerHeight - 0.5) * 2, -1, 1);
const play = async () => {
for (const step of steps) {
if (step.isCommand) {
await typeCommand(step.element, step.text);
await wait(AFTER_COMMAND_DELAY);
continue;
}
targetX = x;
targetY = y;
});
step.element.classList.remove("is-typing-hidden");
await wait(AFTER_OUTPUT_DELAY);
}
};
window.addEventListener("mouseleave", () => {
targetX = 0;
targetY = 0;
});
requestAnimationFrame(animate);
play();
})();