mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-07 19:58:24 +00:00
f96079f631
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
490 lines
13 KiB
Plaintext
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>
|