Phase 5: every page of the site, the resume PDF and RSS

Landing, About, Now, Resume (with a generated PDF), Work and case studies,
Inquiries and the sent page, Library and the Shelf, Workshop and craft pages,
Tabletop, Blog with topics and posts, RSS, the Store, product lines, the
order form and the 404, all rendered from the content service's notes.

Schema: experience previousRoles, skills stack core items, page stats,
make tags and deck favorite. Icon names from notes map to brand icons in one
place, and HTML comments are dropped when Markdown renders.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
Claude
2026-10-11 21:33:49 +00:00
parent 7025e95f89
commit 99ecdc2113
83 changed files with 11681 additions and 14 deletions
+435
View File
@@ -0,0 +1,435 @@
---
/**
* About me: the "I am Greg" intro beside the portrait, the longer story with
* quick facts and what is happening right now, the two wings of the site, and
* where else to find Greg. Text from the Page text note for /about.
*/
import GiltCorners from "@gk/ui/components/GiltCorners.astro";
import Icon from "@gk/ui/components/Icon.astro";
import PageShell from "@gk/ui/components/PageShell.astro";
import Seal from "@gk/ui/components/Seal.astro";
import SectionHeading from "@gk/ui/components/SectionHeading.astro";
import Star from "@gk/ui/components/Star.astro";
import TickScale from "@gk/ui/components/TickScale.astro";
import PageHero from "../components/PageHero.astro";
import DoorLink from "../components/home/DoorLink.astro";
import Portrait from "../components/home/Portrait.astro";
import { splitIntro, viewStatus } from "../lib/home.ts";
import { brandIcon } from "../lib/site.ts";
import { site } from "../lib/page.ts";
import { formatDate } from "../lib/site.ts";
const s = await site();
const text = s.pageText("/about");
const settings = s.settings();
const { intro, sections } = splitIntro(text?.body ?? "");
const introHtml = s.render(intro);
const facts = text?.data.quickFacts ?? [];
const now = s.statuses().slice(0, 4).map((st) => viewStatus(s, st));
const nowUpdated = now.map((v) => v.date).sort().pop();
const socials = settings?.socials ?? [];
const SOCIAL_ICONS: Record<string, string> = { github: "web", linkedin: "briefcase", archidekt: "proxy-cards" };
---
<PageShell title="About me" family="about" current="about" description={text?.data.lede ?? "The person behind the projects."}>
<PageHero
slot="hero"
eyebrow="About"
title="The person behind the projects"
lede={text?.data.lede ?? "Two wings of the same house. This page is the bridge between them."}
/>
<section aria-label="Introduction" class="intro">
<div class="intro-text">
{introHtml ? <blockquote class="iam" set:html={introHtml} /> : <blockquote class="iam"><p>[PLACEHOLDER]</p></blockquote>}
</div>
<div class="intro-portrait">
<Portrait variant="panel" seal="astrolabe" />
</div>
</section>
<div class="story">
<div class="arc">
{
sections.map((sec, i) => (
<section aria-labelledby={`ab-${i}`} class="arc-section">
<SectionHeading id={`ab-${i}`}>
<Star size={14} />
{sec.heading}
</SectionHeading>
<div class="prose" set:html={s.render(sec.body)} />
</section>
))
}
</div>
<aside aria-label="Quick facts" class="facts">
<div class="panel">
<span class="panel-kicker"><Star size={11} /><span>Quick facts</span></span>
<TickScale width={200} height={10} count={30} class="panel-ticks" />
{
facts.length > 0 && (
<ul class="fact-list">
{facts.map((f) => (
<li>
<Icon name={brandIcon(f.icon)} size={18} class="fact-icon" />
<span>{f.text}</span>
</li>
))}
</ul>
)
}
{
now.length > 0 && (
<div class="right-now">
<span class="rn-label">Right now</span>
<ul class="rn-list">
{now.map((v) => (
<li>
<span class="rn-kind">{v.kind}</span>
<a class="rn-title" href={`/now#${v.slug}`}>{v.title}</a>
</li>
))}
</ul>
<p class="rn-note">
Same status notes as the <a href="/now">Now page</a>, so both change together.
{nowUpdated && <> Updated <time datetime={nowUpdated}>{formatDate(nowUpdated)}</time>.</>}
</p>
</div>
)
}
</div>
{
socials.length > 0 && (
<div class="panel">
<span class="panel-kicker"><Star size={11} /><span>Elsewhere</span></span>
<TickScale width={200} height={10} count={30} class="panel-ticks" />
<ul class="socials">
{socials.map((x) => (
<li>
<a href={x.url} rel="me noopener" class="social">
<Icon name={SOCIAL_ICONS[x.label.toLowerCase()] ?? "link"} size={18} />
<span class="s-label">{x.label}</span>
<span class="s-handle">{x.handle}</span>
<Icon name="external" size={14} class="s-ext" />
</a>
</li>
))}
</ul>
</div>
)
}
</aside>
</div>
<section aria-labelledby="ab-doors" class="wings">
<SectionHeading id="ab-doors">
<Star size={14} />
Two wings of the same house
</SectionHeading>
<div class="wing-wrap">
<div class="wing-seal" aria-hidden="true"><Seal name="astrolabe" size={360} /></div>
<div class="wing-row">
<div class="wing featured">
<GiltCorners inset={6} size={30} />
<span class="wing-eyebrow">For employers and clients</span>
<h3 class="wing-title">Work with me</h3>
<ul class="wing-links">
<DoorLink size="small" href="/resume" title="Resume" />
<DoorLink size="small" href="/work" title="Work" />
<DoorLink size="small" href="/inquiries" title="Inquiries" />
</ul>
</div>
<div class="wing">
<span class="wing-eyebrow">For friends and fellow nerds</span>
<h3 class="wing-title">My world</h3>
<ul class="wing-links">
<DoorLink size="small" href="/library" title="Library" />
<DoorLink size="small" href="/workshop" title="Workshop" />
<DoorLink size="small" href="/tabletop" title="Tabletop" />
<DoorLink size="small" href="/blog" title="Blog" />
</ul>
</div>
</div>
</div>
</section>
</PageShell>
<style>
.intro {
padding-top: 40px;
padding-bottom: 8px;
display: flex;
flex-wrap: wrap;
gap: 40px;
align-items: center;
}
.intro-text {
flex: 3 1 520px;
min-width: 0;
}
.iam {
margin: 0;
padding: 4px 0 4px 22px;
border-left: 3px solid var(--gk-gold);
font-size: 22px;
line-height: 1.6;
max-width: 60ch;
}
.iam :global(p) {
margin: 0;
}
.iam :global(p + p) {
margin-top: 14px;
}
.intro-portrait {
flex: 1 1 260px;
max-width: 340px;
min-width: 0;
}
.story {
padding-top: 48px;
display: flex;
flex-wrap: wrap;
gap: 40px;
align-items: flex-start;
}
.arc {
flex: 3 1 560px;
min-width: 0;
display: flex;
flex-direction: column;
gap: 32px;
}
.arc-section {
display: flex;
flex-direction: column;
gap: 18px;
}
.arc-section :global(h2),
.wings :global(h2) {
display: flex;
align-items: center;
gap: 10px;
font-size: 26px;
}
.prose {
font-size: 17px;
line-height: 1.75;
color: var(--gk-soft);
max-width: 68ch;
}
.prose :global(p) {
margin: 0 0 18px;
}
.prose :global(p:last-child) {
margin-bottom: 0;
}
.facts {
flex: 1 1 280px;
max-width: 340px;
min-width: 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.panel {
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px;
border-radius: var(--gk-radius);
background: var(--gk-midnight);
border: 1px solid var(--gk-lines);
}
.panel-kicker {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
letter-spacing: 0.3em;
font-weight: 600;
color: var(--gk-gold-bright);
text-transform: uppercase;
}
.panel :global(.panel-ticks) {
margin: -4px 0 2px;
}
.fact-list,
.rn-list,
.socials {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 10px;
}
.fact-list li {
display: flex;
gap: 10px;
align-items: flex-start;
font-size: 14px;
line-height: 1.5;
}
.fact-list :global(.fact-icon) {
flex: none;
color: var(--gk-gold-bright);
margin-top: 1px;
}
.right-now {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 6px;
padding-top: 12px;
border-top: 1px solid var(--gk-lines);
}
.rn-label {
font-size: 11px;
letter-spacing: 0.3em;
font-weight: 600;
color: var(--gk-muted);
text-transform: uppercase;
}
.rn-list {
gap: 8px;
}
.rn-list li {
display: flex;
flex-direction: column;
gap: 1px;
}
.rn-kind {
font-size: 11px;
letter-spacing: 0.16em;
font-weight: 600;
color: var(--gk-teal-bright);
text-transform: uppercase;
}
.rn-title {
font-size: 14px;
font-weight: 600;
color: var(--gk-text);
text-decoration: none;
}
.rn-title:hover {
color: var(--gk-teal-bright);
}
.rn-note {
margin: 0;
font-size: 12px;
color: var(--gk-muted);
line-height: 1.5;
}
.social {
display: flex;
align-items: center;
gap: 10px;
min-height: 40px;
color: var(--gk-text);
text-decoration: none;
font-size: 14px;
}
.social:hover {
color: var(--gk-teal-bright);
}
.s-label {
font-weight: 600;
}
.s-handle {
color: var(--gk-muted);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.social :global(.s-ext) {
margin-left: auto;
flex: none;
}
.wings {
padding-top: 56px;
padding-bottom: 8px;
display: flex;
flex-direction: column;
gap: 18px;
}
.wing-wrap {
position: relative;
isolation: isolate;
}
.wing-seal {
position: absolute;
inset: -70px 0;
z-index: -1;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
opacity: 0.12;
overflow: hidden;
}
.wing-row {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.wing {
flex: 1 1 380px;
min-width: 0;
position: relative;
display: flex;
flex-direction: column;
gap: 10px;
padding: 26px;
border-radius: var(--gk-radius);
background: rgba(19, 28, 43, 0.7);
border: 1px solid var(--gk-lines);
}
.wing.featured {
border-color: rgba(201, 162, 74, 0.55);
}
.wing-eyebrow {
font-size: 11px;
letter-spacing: 0.3em;
font-weight: 600;
color: var(--gk-gold-bright);
text-transform: uppercase;
}
.wing-title {
margin: 0;
font-size: 26px;
letter-spacing: 0.06em;
}
.wing-links {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.wing-links :global(.door-link) {
color: var(--gk-text);
}
.wing-links :global(.door-link:hover) {
color: var(--gk-gold-bright);
}
@media (max-width: 900px) {
.facts {
max-width: none;
}
}
@media (max-width: 640px) {
.iam {
font-size: 19px;
}
.wing {
padding: 22px 20px;
}
.intro-portrait {
max-width: none;
}
}
</style>