Files
com.gregkilmer.www/services/site/src/pages/workshop/index.astro
T
Claude f96079f631 Add an All makes page to the Workshop
Every make newest first with its craft, filterable by craft, linked as
See all makes from Recently finished. Makes without a craft now link
here instead of the Workshop page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
2026-10-18 16:55:20 +00:00

490 lines
13 KiB
Plaintext

---
import Button from "@gk/ui/components/Button.astro";
import GiltCorners from "@gk/ui/components/GiltCorners.astro";
import PageShell from "@gk/ui/components/PageShell.astro";
import Seal from "@gk/ui/components/Seal.astro";
import Star from "@gk/ui/components/Star.astro";
import StarChartArt from "@gk/ui/components/StarChartArt.astro";
import StatusPill from "@gk/ui/components/StatusPill.astro";
import TickRing from "@gk/ui/components/TickRing.astro";
import PageHero from "../../components/PageHero.astro";
import CtaBand from "../../components/world/CtaBand.astro";
import NowStrip, { type NowItem } from "../../components/world/NowStrip.astro";
import WorldSection from "../../components/world/WorldSection.astro";
import type { Note } from "../../lib/content.ts";
import { plural, site } from "../../lib/page.ts";
import { buildLogs, byDateDesc, byOrder, craftPill, craftSeal, isSword, makePhotos, makesOf, statusesFor, toolsAt, toolsOf } from "../../lib/world.ts";
const s = await site();
const text = s.pageText("/workshop");
const intro = s.render(text?.body);
const crafts = byOrder(s.all("craft")).map((craft) => {
const makes = byDateDesc(makesOf(s, craft));
const photos = makes.flatMap((m) => makePhotos(m).slice(0, 1)).slice(0, 3);
return { craft, makes, photos, logs: buildLogs(s, craft).length, tools: toolsOf(s, craft).map((t) => t.title) };
});
const stations = byOrder(s.all("station")).map((station) => ({ station, tools: toolsAt(s, station) }));
const loose = s.all("tool").filter((t) => !s.byName(t.data.station));
const allMakes = s.all("make");
const swords = allMakes.filter(isSword);
const recent = byDateDesc(allMakes.filter((m) => !isSword(m)))
.sort((a, b) => Number(!!b.data.date) - Number(!!a.data.date) || Number(makePhotos(b).length > 0) - Number(makePhotos(a).length > 0))
.slice(0, 4);
const nowItems: NowItem[] = statusesFor(s, "/workshop").map((st) => {
const subject = st.data.subject ? s.link(st.data.subject) : null;
const img = subject?.note?.type === "make" ? makePhotos(subject.note as Note<"make">)[0] : undefined;
return {
id: st.slug,
label: st.data.kind === "Making" ? "On the bench" : st.data.kind,
title: subject?.title ?? st.title,
note: st.data.note,
href: subject?.href,
img,
};
});
---
<PageShell title="Workshop" family="workshop" current="workshop" description={text?.data.lede ?? "What I make, what I make it with, and what I am learning next."}>
<Fragment slot="hero">
<PageHero eyebrow="My world" title={text?.title ?? "Workshop"} lede={text?.data.lede} />
</Fragment>
{intro && <div class="gk-prose intro" set:html={intro} />}
{nowItems.length > 0 && <NowStrip id="ws-now" heading="Right now" items={nowItems} seal="craft-print" />}
{
stations.length > 0 && (
<WorldSection id="ws-space" title="The workspace" intro={`${plural(stations.length, "station")} around the office.`}>
<ol class="stations">
{stations.map(({ station, tools }) => (
<li id={station.slug} class="station">
<span class="st-head">
<TickRing size={32}>{station.data.order}</TickRing>
<h3>{station.title}</h3>
</span>
<span class="where">{station.data.where}</span>
{tools.length > 0 && (
<ul class="tools">
{tools.map((t) => (
<li id={t.slug} title={t.data.blurb}>
<Star size={9} />
{t.title}
</li>
))}
</ul>
)}
{station.data.goodFor && <span class="good">{station.data.goodFor}</span>}
</li>
))}
{loose.length > 0 && (
<li class="station">
<span class="st-head">
<TickRing size={32}><Star size={10} /></TickRing>
<h3>Around the shop</h3>
</span>
<ul class="tools">
{loose.map((t) => (
<li id={t.slug}>
<Star size={9} />
{t.title}
</li>
))}
</ul>
</li>
)}
</ol>
</WorldSection>
)
}
<WorldSection id="ws-crafts" title="Crafts" intro="Each craft keeps its own makes, tools and build logs, so the page stays short as the collection grows.">
<ul class="crafts">
{
crafts.map(({ craft, makes, photos, logs, tools }) => (
<li>
<a href={s.href(craft) ?? undefined} class="gk-card craft">
<span class="c-top">
<h3 class="c-name">{craft.title}</h3>
<StatusPill kind={craftPill(craft.data.status)}>{craft.data.status}</StatusPill>
</span>
<span class="thumbs" aria-hidden="true">
{[0, 1, 2].map((i) =>
photos[i] ? (
<img src={photos[i]} alt="" loading="lazy" />
) : (
<span class="blank">
<Seal name={craftSeal(craft) ?? "craft-print"} size={30} />
</span>
),
)}
</span>
{craft.data.blurb && <span class="c-blurb">{craft.data.blurb}</span>}
{tools.length > 0 && <span class="c-tools">{tools.join(" · ")}</span>}
<span class="c-foot">
<span>
{plural(makes.length, "make")}
{logs > 0 && ` · ${plural(logs, "build log")}`}
</span>
<span class="open">
Open the craft
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true">
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</span>
</span>
</a>
</li>
))
}
</ul>
</WorldSection>
{
recent.length > 0 && (
<WorldSection id="ws-recent" title="Recently finished">
<a slot="action" href="/workshop/makes" class="gk-text all">
See all {allMakes.length} makes
</a>
<ul class="recent">
{recent.map((m) => {
const found = s.byName(m.data.craft);
const craft = found?.type === "craft" ? (found as Note<"craft">) : undefined;
const log = s.backlinks(m, { type: "post" })[0];
return (
<li class="r-card">
<a href={s.href(m) ?? undefined} class="r-art">
<StarChartArt seal={(craft && craftSeal(craft)) ?? "craft-print"} ratio="auto" src={makePhotos(m)[0]} alt={m.data.blurb ?? m.title} class="r-img" />
<span class="gk-visually-hidden">{m.title}</span>
</a>
<div class="r-txt">
{craft && <span class="r-craft">{craft.title}</span>}
<h3 class="r-name">
<a href={s.href(m) ?? undefined}>{m.title}</a>
</h3>
{m.data.blurb && <span class="r-blurb">{m.data.blurb}</span>}
{log && (
<a href={s.href(log) ?? undefined} class="gk-text r-log">
Blog: {log.title}
</a>
)}
</div>
</li>
);
})}
</ul>
</WorldSection>
)
}
{
swords.length > 0 && (
<section class="rack gk-featured" aria-labelledby="ws-swords">
<GiltCorners />
<div class="rack-words">
<span class="rack-kick">The sword rack</span>
<h2 id="ws-swords">Blades from the stories I love</h2>
<span class="rack-line" aria-hidden="true" />
<span class="rack-note">I always pick the sword class, and the spellsword when I can.</span>
</div>
<ul class="blades">
{swords.map((m) => (
<li>
<Star size={10} />
<a href={s.href(m) ?? undefined} title={m.data.blurb}>
{m.title}
</a>
</li>
))}
</ul>
</section>
)
}
<CtaBand title="Want one of these?" note="Some of it is in the Store. Anything else can start as an inquiry.">
<Button href="/store">Visit the Store</Button>
<Button href="/inquiries" variant="secondary">Start an inquiry</Button>
</CtaBand>
</PageShell>
<style>
.intro {
max-width: 70ch;
padding-top: 28px;
color: var(--gk-soft);
font-size: 16px;
line-height: 1.7;
}
.intro :global(p) {
margin: 0 0 14px;
}
/* Stations */
.stations {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin: 0;
padding: 0;
list-style: none;
}
.station {
flex: 1 1 250px;
min-width: 0;
padding: 20px;
background: var(--gk-midnight);
border: 1px solid var(--gk-lines);
border-top: 2px solid var(--gk-gold);
border-radius: var(--gk-radius);
display: flex;
flex-direction: column;
gap: 10px;
scroll-margin-top: 24px;
}
.st-head {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
}
.st-head h3 {
margin: 0;
font-size: 18px;
letter-spacing: 0.02em;
}
.where {
font-size: 13px;
color: var(--gk-muted);
}
.tools {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 6px;
font-size: 14px;
}
.tools li {
display: flex;
align-items: center;
gap: 8px;
}
.good {
margin-top: auto;
padding-top: 8px;
border-top: 1px solid var(--gk-lines);
font-size: 13px;
color: var(--gk-soft);
}
/* Crafts */
.crafts {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
gap: 16px;
margin: 0;
padding: 0;
list-style: none;
}
.craft {
height: 100%;
gap: 12px;
padding: 20px;
}
.c-top {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.c-name {
margin: 0;
font-size: 20px;
letter-spacing: 0.04em;
}
.thumbs {
display: flex;
gap: 6px;
}
.thumbs > * {
flex: 1;
min-width: 0;
height: 72px;
border-radius: 6px;
object-fit: cover;
}
.thumbs .blank {
display: flex;
align-items: center;
justify-content: center;
background: var(--gk-sky);
opacity: 0.6;
}
.c-blurb {
font-size: 14px;
color: var(--gk-soft);
line-height: 1.55;
}
.c-tools {
font-size: 13px;
color: var(--gk-muted);
}
.c-foot {
margin-top: auto;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 8px;
font-size: 13px;
color: var(--gk-soft);
}
.open {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--gk-teal-bright);
font-weight: 600;
}
/* Recently finished */
.recent {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin: 0;
padding: 0;
list-style: none;
}
.r-card {
flex: 1 1 240px;
min-width: 0;
display: flex;
flex-direction: column;
background: var(--gk-midnight);
border: 1px solid var(--gk-lines);
border-radius: var(--gk-radius);
overflow: hidden;
}
.r-art {
display: block;
}
.r-art :global(.r-img) {
height: 170px;
border-radius: 0;
}
.r-txt {
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 6px;
}
.r-craft {
font-size: 11px;
letter-spacing: 0.3em;
font-weight: 600;
color: var(--gk-muted);
text-transform: uppercase;
}
.r-name {
margin: 0;
font-size: 16px;
}
.r-name a {
color: var(--gk-text);
text-decoration: none;
}
.r-name a:hover {
color: var(--gk-gold-bright);
}
.r-blurb {
font-size: 13px;
color: var(--gk-muted);
}
.r-log {
font-size: 13px;
align-self: flex-start;
}
.all {
font-size: 14px;
white-space: nowrap;
}
/* Sword rack */
.rack {
margin-top: 40px;
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: center;
padding: 24px 28px;
}
.rack-words {
flex: 1 1 300px;
display: flex;
flex-direction: column;
gap: 8px;
}
.rack-kick {
font-size: 11px;
letter-spacing: 0.3em;
font-weight: 600;
color: var(--gk-gold-bright);
text-transform: uppercase;
}
.rack h2 {
margin: 0;
font-size: 24px;
letter-spacing: 0.05em;
}
.rack-line {
display: block;
width: 180px;
height: 2px;
border-radius: 2px;
background:
radial-gradient(circle at 18% 50%, var(--gk-teal-bright) 0 2px, transparent 3px),
linear-gradient(90deg, var(--gk-gold), rgba(201, 162, 74, 0.15) 70%, transparent);
box-shadow: 0 0 8px rgba(201, 162, 74, 0.35);
}
.rack-note {
font-size: 14px;
color: var(--gk-muted);
}
.blades {
flex: 2 1 480px;
min-width: 0;
margin: 0;
padding: 0;
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
gap: 8px 16px;
font-size: 15px;
}
.blades li {
display: flex;
align-items: center;
gap: 8px;
}
.blades a {
color: var(--gk-text);
text-decoration: none;
}
.blades a:hover {
color: var(--gk-gold-bright);
}
@media (max-width: 640px) {
.rack {
padding: 24px 20px;
}
.blades {
flex-basis: 100%;
}
}
</style>