Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/lib/releases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type RawRelease = {
};
export type Section = { title: string; items: string[] };
// The user-facing highlights of a release: the "## What's new" section its author writes by hand.
export type News = { headline: string; items: string[]; shot?: string };
export type News = { headline: string; items: { title: string; text: string }[]; shot?: string };
export type Release = { version: string; date: string; dateLabel: string; url: string; downloadUrl: string; sections: Section[]; news?: News };

// The site shows only what is in the app: no breaking-change notes, docs, CI or build work, no
Expand All @@ -36,12 +36,11 @@ const clean = (s: string) =>
.replace(/\*\*|`/g, '')
.trim();

// "## What's new" up to the next heading or "---": its bullets, the bold start of the first one as the
// headline ("- **Place SVG logos** on any face." → "Place SVG logos"), and an optional
// "<!-- shot: name -->" naming a picture in src/assets/shots.
// "## What's new" up to the next heading or "---": its bullets split into the bold start and the rest
// ("- **Place SVG logos:** on any face." → "Place SVG logos" + "on any face."), the first title as the
// headline, and an optional "<!-- shot: name -->" naming a picture in src/assets/shots.
export function parseNews(body: string | null): News | undefined {
const items: string[] = [];
let headline = '';
const items: News['items'] = [];
let shot: string | undefined;
let inside = false;
for (const line of (body ?? '').split(/\r?\n/)) {
Expand All @@ -50,10 +49,11 @@ export function parseNews(body: string | null): News | undefined {
shot ??= /<!--\s*shot:\s*([\w-]+)\s*-->/.exec(line)?.[1];
const bullet = /^\s*[-*]\s+(.+)/.exec(line)?.[1];
if (!bullet) continue;
if (!items.length) headline = (/^\*\*([^*]+)\*\*/.exec(bullet)?.[1] ?? clean(bullet)).replace(/[:.]\s*$/, '');
items.push(clean(bullet));
const bold = /^\*\*([^*]+)\*\*(.*)/.exec(bullet);
items.push(bold ? { title: bold[1].replace(/[:.]\s*$/, '').trim(), text: clean(bold[2].replace(/^\s*:/, '')).replace(/^\p{Ll}/u, (c) => c.toUpperCase()) } : { title: '', text: clean(bullet) });
}
}
const headline = items[0]?.title || items[0]?.text || '';
return items.length ? { headline, items, shot } : undefined;
}

Expand Down
2 changes: 1 addition & 1 deletion src/pages/changelog.astro
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const description = 'Every PartBuddy release with its date and changes.';
{r.news && (
<>
<h3>What's new</h3>
<ul>{r.news.items.map((i) => <li>{i}</li>)}</ul>
<ul>{r.news.items.map((i) => <li>{i.title && <strong>{i.title}: </strong>}{i.text}</li>)}</ul>
</>
)}
{r.sections.length ? r.sections.map((s) => (
Expand Down
29 changes: 14 additions & 15 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -87,23 +87,22 @@ const jsonLd = {
</section>

{latest?.news && (
<section id="new" class="section" aria-labelledby="new-title">
<section id="new" class="update-wrap" aria-labelledby="new-title">
<div class="wrap">
<div class="chapters">
<article class="chapter">
<div class="chapter-text reveal">
<p class="kicker">New in {latest.version} · {latest.dateLabel}</p>
<h2 id="new-title">{latest.news.headline}</h2>
<ul class="points">{latest.news.items.map((p) => <li><Check aria-hidden="true" />{p}</li>)}</ul>
<p><a class="link-arrow" href={`/changelog.html#v${latest.version}`}>All changes in {latest.version}</a></p>
<article class:list={['update reveal', { 'update-shot': latest.news.shot }]}>
<div class="update-text">
<p class="update-head"><span class="update-tag">New in {latest.version}</span><time datetime={latest.date}>{latest.dateLabel}</time></p>
<h2 id="new-title">PartBuddy {latest.version} is out.</h2>
<ol class="update-items">
{latest.news.items.map((i) => <li>{i.title && <h3>{i.title}</h3>}<p>{i.text}</p></li>)}
</ol>
<div class="ctas">
<a class="btn btn-primary" href={DOWNLOAD_URL} data-goatcounter-click="download-update"><Download aria-hidden="true" />Get {latest.version}</a>
<a class="btn" href={`/changelog.html#v${latest.version}`}>All changes<ArrowRight aria-hidden="true" /></a>
</div>
{latest.news.shot && (
<div class="chapter-shot reveal">
<Shot name={latest.news.shot} alt={latest.news.headline} sizes="(min-width: 1200px) 760px, (min-width: 960px) 60vw, 100vw" />
</div>
)}
</article>
</div>
</div>
{latest.news.shot && <Shot name={latest.news.shot} alt={latest.news.headline} sizes="(min-width: 1200px) 620px, (min-width: 960px) 50vw, 100vw" />}
</article>
</div>
</section>
)}
Expand Down
21 changes: 21 additions & 0 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,27 @@ a.eyebrow:hover { color: var(--ink); border-color: rgb(255 255 255 / 0.25); }
.logo svg { width: 22px; height: 22px; }
.logo .mark-mono { color: #e9ebee; }

/* ---------- Update card: the latest release's What's new, right under the hero ---------- */
.update-wrap { position: relative; isolation: isolate; margin-top: calc(-1 * clamp(24px, 4vw, 64px)); padding-bottom: clamp(80px, 11vw, 144px); }
.update {
position: relative; display: grid; gap: clamp(32px, 4vw, 56px); align-items: center; overflow: hidden;
padding: clamp(24px, 4vw, 48px); border-radius: calc(var(--radius) + 4px);
background: radial-gradient(120% 90% at 0% 0%, rgb(47 106 214 / 0.22), transparent 60%), var(--surface);
box-shadow: 0 0 0 1px rgb(122 167 246 / 0.35), 0 1px 0 rgb(255 255 255 / 0.12) inset, 0 40px 100px -40px var(--light);
}
.update-head { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; font-size: 0.8125rem; color: var(--muted); }
.update-tag { padding: 5px 12px; border-radius: 999px; background: var(--accent); color: #fff; font-weight: 600; letter-spacing: 0.02em; }
.update h2 { margin-top: 18px; font-size: clamp(1.75rem, 1.2rem + 2vw, 2.75rem); }
.update-items { display: grid; gap: 20px; margin-top: 28px; counter-reset: news; }
.update-items li { position: relative; padding-left: 44px; counter-increment: news; }
.update-items li::before {
content: counter(news); position: absolute; left: 0; top: -2px; display: grid; place-items: center; width: 28px; height: 28px;
border-radius: 8px; background: rgb(47 106 214 / 0.18); box-shadow: 0 0 0 1px rgb(122 167 246 / 0.4); color: var(--accent-text); font-weight: 600; font-size: 0.875rem;
}
.update-items h3 { font-size: 1.0625rem; }
.update-items p { margin-top: 4px; color: var(--muted); font-size: 0.9375rem; }
@media (min-width: 960px) { .update-shot { grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); } }

/* ---------- Sections ---------- */
/* Own stacking context, so the lights (z-index -1) sit above the section background. */
.section { padding: clamp(80px, 11vw, 144px) 0; position: relative; isolation: isolate; }
Expand Down
4 changes: 3 additions & 1 deletion test/releases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ test('release-please notes become versions, sections and a download link', async
test("the hand-written What's new becomes headline, items and shot, and stays out of the change lists", () => {
const body = "Intro.\n\n## What's new\n\n<!-- shot: logos -->\n- **Place SVG logos** on any face.\n- Drag them along an axis.\n\n---\n\n## [0.2.0](u) (2026-11-09)\n\n### Features\n\n* **desktop:** svg logos";
const [r] = parseReleases([{ tag_name: 'v0.2.0', published_at: '2026-11-09T00:00:00Z', html_url: 'u', body }]);
assert.deepEqual(r.news, { headline: 'Place SVG logos', items: ['Place SVG logos on any face.', 'Drag them along an axis.'], shot: 'logos' });
assert.deepEqual(r.news, { headline: 'Place SVG logos', items: [{ title: 'Place SVG logos', text: 'On any face.' }, { title: '', text: 'Drag them along an axis.' }], shot: 'logos' });
const [colon] = parseReleases([{ tag_name: 'v1', published_at: '2026-11-09T00:00:00Z', html_url: 'u', body: "## What's new\n- **Logos on the part:** engraved or raised." }]);
assert.deepEqual(colon.news?.items, [{ title: 'Logos on the part', text: 'Engraved or raised.' }]);
assert.deepEqual(r.sections, [{ title: 'Features', items: ['desktop: svg logos'] }]);
const [old] = parseReleases([{ tag_name: 'v0.1.0', published_at: '2026-11-08T00:00:00Z', html_url: 'u', body: '### Features\n\n* x' }]);
assert.equal(old.news, undefined);
Expand Down
Loading