From 4c433a9ea015e0ae8df4c8b57cc0e46e500d5cbe Mon Sep 17 00:00:00 2001 From: fylorn <249551762+fylorn@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:39:32 +0800 Subject: [PATCH] Changelog and license pages in the neon layout Changelog: - A hero with the latest release of each product as tiles, Lite first. - The product filter is the site's segmented control, with a count per product. - Releases are grouped by month, the month held at the side, each as a hairline row: date, product (one colour per product throughout), version linking to its anchor, GitHub. A row without a GitHub release keeps the same columns. - Enterprise notes show their title and highlights; the full text opens in place. Filtering also hides months left empty. License: - A hero with the three products and their licenses as tiles, MIT in cyan and BSL 1.1 in amber. - Terms as hairline rows, the commercial tiers in a framed table with the free tier set off, usage measurement as two tiles, and the full text and commercial contact as hairline columns. - Where the copy summarised Enterprise's terms it said "ThinkWatch"; it now says "ThinkWatch Enterprise", as the rest of the site does. The definitions taken from LICENSING.md are unchanged. Co-Authored-By: Claude Opus 5.5 --- src/components/pages/ChangelogPage.astro | 587 +++++++++++++++++------ src/components/pages/LicensePage.astro | 447 ++++++++++++----- src/i18n/pages/changelog.ts | 19 +- src/i18n/pages/license.ts | 16 +- 4 files changed, 784 insertions(+), 285 deletions(-) diff --git a/src/components/pages/ChangelogPage.astro b/src/components/pages/ChangelogPage.astro index c82cf6d..03f68a8 100644 --- a/src/components/pages/ChangelogPage.astro +++ b/src/components/pages/ChangelogPage.astro @@ -1,16 +1,18 @@ --- // The /changelog page: the releases of ThinkWatch Enterprise, ThinkWatch Lite -// and ThinkWatch Core, newest first, filterable by product. Each release shows -// its version and date and links to its page on GitHub; the Enterprise releases -// written up on this site also show their notes. Copy lives in +// and ThinkWatch Core, newest first and grouped by month, filterable by +// product, under the latest release of each. Each release shows its version +// and date and links to its page on GitHub; the Enterprise releases written up +// on this site also show their notes. Copy lives in // src/i18n/pages/changelog.ts, the list in src/lib/changelog.ts. import { render } from "astro:content"; import Base from "~/layouts/Base.astro"; import SiteHeader from "~/components/SiteHeader.astro"; import SiteFooter from "~/components/SiteFooter.astro"; +import NeonMotion from "~/components/NeonMotion.astro"; import { getLang } from "~/i18n"; import { changelogCopy } from "~/i18n/pages/changelog"; -import { getChangelog, releaseProductName } from "~/lib/changelog"; +import { getChangelog, releaseProductName, type ChangelogEntry } from "~/lib/changelog"; import { breadcrumbs, webPage } from "~/lib/structured-data"; const lang = getLang(Astro); @@ -22,171 +24,476 @@ const jsonLd = [ breadcrumbs(lang, "/changelog", c.meta.breadcrumbHome, c.meta.breadcrumbPage), ]; -const typeStyles: Record = { - release: "border-brand-1/40 bg-brand-1/10 text-brand-1", - feature: "border-white/15 bg-white/5 text-[var(--color-text)]", - fix: "border-emerald-500/30 bg-emerald-500/10 text-emerald-300", - security: "border-accent/40 bg-accent/10 text-accent", -}; - // Release times are UTC; format them in UTC so the date does not depend on where the site is built. const fmt = new Intl.DateTimeFormat(c.dateLocale, { ...c.dateFormat, timeZone: "UTC" }); +const fmtFull = new Intl.DateTimeFormat(c.dateLocale, { ...c.fullDateFormat, timeZone: "UTC" }); +const fmtMonth = new Intl.DateTimeFormat(c.dateLocale, { ...c.monthFormat, timeZone: "UTC" }); -const filters = [ - { id: "all", label: c.all }, - ...(["enterprise", "lite", "core"] as const).map((id) => ({ id, label: c.filters[id] })), -]; +const products = ["lite", "enterprise", "core"] as const; +const count = (id: string) => (id === "all" ? entries.length : entries.filter((e) => e.product === id).length); +const filters = [{ id: "all", label: c.all }, ...products.map((id) => ({ id, label: c.filters[id] }))]; + +const latest = products + .map((id) => entries.find((e) => e.product === id)) + .filter((e): e is ChangelogEntry => Boolean(e)); + +// Newest month first; releases keep their newest-first order inside it. +const months: { key: string; label: string; items: ChangelogEntry[] }[] = []; +for (const e of entries) { + const key = `${e.date.getUTCFullYear()}-${e.date.getUTCMonth()}`; + if (months.at(-1)?.key !== key) months.push({ key, label: fmtMonth.format(e.date), items: [] }); + months.at(-1)!.items.push(e); +} --- -
-
-
- {c.eyebrow} -
-

- {c.titleA}{c.titleHighlight} -

-

- {c.intro}{c.rss}{c.introEnd} -

-
- -
- {filters.map((f, i) => ( - - ))} -
- -
    - {entries.map(async (entry) => { - const name = releaseProductName(entry.product, lang); - const notes = entry.notes; - const Content = notes ? (await render(notes)).Content : null; - return ( -
  1. - {/* The anchor the release had while the page listed Enterprise alone (#v0.4.0). */} - {entry.legacyId && } - - -
    - - {name} +
    +
    + + +
    +

    {c.eyebrow}

    +

    {c.titleA}{c.titleHighlight}

    +

    + {c.intro}{c.rss}{c.introEnd} +

    + +

    {c.latest}

    + - - {notes && ( - -

    {notes.data.title}

    - - {notes.data.highlights && ( -
      - {notes.data.highlights.map((h) => ( -
    • - - {h} -
    • - ))} -
    - )} - -
    - {Content && } -
    -
    - )} -
  2. - ); - })} -
+ + ))} + + + + +
+
+ {filters.map((f, i) => ( + + ))} +
+ +
+ {months.map((m) => ( +
+

{m.label}

+
    + {m.items.map(async (entry) => { + const name = releaseProductName(entry.product, lang); + const notes = entry.notes; + const Content = notes ? (await render(notes)).Content : null; + return ( +
  1. + {/* The anchor the release had while the page listed Enterprise alone (#v0.4.0). */} + {entry.legacyId && } +
    + + {name} + {entry.tag} + {entry.url ? ( + + {c.github} ↗ + + ) : } +
    + + {notes && ( +
    +

    {notes.data.title}

    + {notes.data.highlights && ( +
      + {notes.data.highlights.map((h) =>
    • {h}
    • )} +
    + )} + {Content && ( +
    + {c.readNotes} +
    +
    + )} +
    + )} +
  2. + ); + })} +
+
+ ))} +
+
+