feat(design): reader view — strip terminal chrome, add typography controls

Replace monospace-terminal aesthetic with clean reader layout. System
sans-serif default, typeface picker (sans/serif/mono) with per-face
optical tuning. Warm color palettes (slate dark, bone light), crafted
link underlines, WCAG AA contrast on all text tiers. Semantic HTML
throughout: proper heading hierarchy, <time> elements, role=group,
<dl>/<table>/<article> where appropriate. Net -140 lines.
This commit is contained in:
2026-05-24 18:28:02 -04:00
parent 0c5d9e03b1
commit 32455bf7a7
11 changed files with 267 additions and 407 deletions
+17 -62
View File
@@ -2,13 +2,7 @@
import Widget from "./Widget.astro";
import { timeline, type TimelineType } from "@/data/timeline";
const typeColor: Record<TimelineType, string> = {
career: "var(--color-timeline-career)",
education: "var(--color-timeline-education)",
cert: "var(--color-timeline-cert)",
project: "var(--color-timeline-project)",
homelab: "var(--color-timeline-homelab)",
};
const isDate = (d: string) => /^\d{4}/.test(d);
const typeLabel: Record<TimelineType, string> = {
career: "career",
@@ -19,70 +13,31 @@ const typeLabel: Record<TimelineType, string> = {
};
---
<Widget title="tyler/journey">
<ol class="relative border-l border-[var(--color-border)] ml-[2px] flex flex-col gap-0">
<Widget title="Journey">
<ol class="flex flex-col gap-0">
{timeline.map((entry) => (
<li data-tl-entry class="pl-[3ch] pb-2lh last:pb-0 relative">
<span
class="absolute -left-[7px] top-[3px] w-3 h-3 rounded-full border border-[var(--color-bg)] shrink-0"
style={`background-color: ${typeColor[entry.type]}`}
aria-hidden="true"
/>
<div class="flex items-center gap-1ch mb-half-lh">
<span class="font-mono text-sm text-[var(--color-text-dim)]">{entry.date}</span>
<span
class="font-mono text-sm px-1 border"
style={`color: ${typeColor[entry.type]}; border-color: ${typeColor[entry.type]}; opacity: 0.7;`}
>
{typeLabel[entry.type]}
</span>
</div>
<p class="font-mono text-sm font-semibold text-[var(--color-text)] mb-half-lh">
{entry.title}
<li class="pb-2lh last:pb-0">
<p class="text-[var(--color-text-dim)] mb-qtr-lh">
{isDate(entry.date)
? <time datetime={entry.date}>{entry.date}</time>
: <span>{entry.date}</span>
}
<span class="mx-[0.5ch]">·</span>
<em>{typeLabel[entry.type]}</em>
</p>
<p class="font-mono text-sm text-[var(--color-text)] opacity-70 leading-relaxed mb-half-lh">
<h3 class="font-semibold mb-half-lh">{entry.title}</h3>
<p class="text-[var(--color-text-label)] leading-relaxed mb-half-lh">
{entry.description}
</p>
{entry.tags && entry.tags.length > 0 && (
<div class="flex flex-wrap gap-x-1ch gap-y-half-lh">
{entry.tags.map((tag) => (
<span class="font-mono text-sm text-[var(--color-text-dim)]">
{tag}
</span>
))}
</div>
<p class="text-[var(--color-text-dim)]">
{entry.tags.join(" · ")}
</p>
)}
</li>
))}
</ol>
</Widget>
<script>
if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
const entries = document.querySelectorAll<HTMLElement>("[data-tl-entry]");
const observer = new IntersectionObserver(
(observed) => {
observed.forEach((entry) => {
if (entry.isIntersecting) {
(entry.target as HTMLElement).style.opacity = "1";
(entry.target as HTMLElement).style.transform = "translateY(0)";
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.15 },
);
entries.forEach((el) => {
el.style.opacity = "0";
el.style.transform = "translateY(8px)";
el.style.transition = "opacity 240ms linear, transform 240ms linear";
observer.observe(el);
});
}
</script>