From f3214feeeb7c6beb3e70b0bef9dce890bea44be7 Mon Sep 17 00:00:00 2001 From: Manuel Staggl Date: Mon, 9 Nov 2026 18:00:11 +0100 Subject: [PATCH] feat(site): turn What's new into a highlighted update card with numbered items and download Co-Authored-By: Claude Opus 5.5 --- src/lib/releases.ts | 16 ++++++++-------- src/pages/changelog.astro | 2 +- src/pages/index.astro | 29 ++++++++++++++--------------- src/styles/global.css | 21 +++++++++++++++++++++ test/releases.test.ts | 4 +++- 5 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/lib/releases.ts b/src/lib/releases.ts index 59d7b48..4227632 100644 --- a/src/lib/releases.ts +++ b/src/lib/releases.ts @@ -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 @@ -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 -// "" 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 "" 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/)) { @@ -50,10 +49,11 @@ export function parseNews(body: string | null): News | undefined { shot ??= //.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; } diff --git a/src/pages/changelog.astro b/src/pages/changelog.astro index 2a07e56..93ceb6e 100644 --- a/src/pages/changelog.astro +++ b/src/pages/changelog.astro @@ -24,7 +24,7 @@ const description = 'Every PartBuddy release with its date and changes.'; {r.news && ( <>

What's new

- + )} {r.sections.length ? r.sections.map((s) => ( diff --git a/src/pages/index.astro b/src/pages/index.astro index 293b28a..8d5f0b1 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -87,23 +87,22 @@ const jsonLd = { {latest?.news && ( -
+
-
-
-
-

New in {latest.version} · {latest.dateLabel}

-

{latest.news.headline}

-
    {latest.news.items.map((p) =>
  • )}
-

All changes in {latest.version}

+
+
+

New in {latest.version}

+

PartBuddy {latest.version} is out.

+
    + {latest.news.items.map((i) =>
  1. {i.title &&

    {i.title}

    }

    {i.text}

  2. )} +
+ - {latest.news.shot && ( -
- -
- )} -
-
+
+ {latest.news.shot && } +
)} diff --git a/src/styles/global.css b/src/styles/global.css index 4926dc6..49c55b2 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -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; } diff --git a/test/releases.test.ts b/test/releases.test.ts index 8b30aa7..4e16ef8 100644 --- a/test/releases.test.ts +++ b/test/releases.test.ts @@ -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\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);