From 5daec097ed0f534a10ad740fed38876221566fd5 Mon Sep 17 00:00:00 2001 From: Thomas Jeffery Date: Wed, 10 Jun 2026 21:45:30 -0600 Subject: [PATCH] feat(#3904): add release notes page --- docs/src/components/ReleaseNotesFeed.tsx | 582 ++++++++++++++++++ docs/src/components/search/quick-links.ts | 2 +- docs/src/content/config.ts | 73 +++ .../src/content/release-notes/2023-01-31.json | 49 ++ .../src/content/release-notes/2024-07-22.json | 14 + .../src/content/release-notes/2026-05-14.json | 21 + .../src/content/release-notes/2026-05-20.json | 69 +++ docs/src/lib/get-started-nav.ts | 5 + docs/src/pages/release-notes/index.astro | 36 ++ 9 files changed, 850 insertions(+), 1 deletion(-) create mode 100644 docs/src/components/ReleaseNotesFeed.tsx create mode 100644 docs/src/content/release-notes/2023-01-31.json create mode 100644 docs/src/content/release-notes/2024-07-22.json create mode 100644 docs/src/content/release-notes/2026-05-14.json create mode 100644 docs/src/content/release-notes/2026-05-20.json create mode 100644 docs/src/pages/release-notes/index.astro diff --git a/docs/src/components/ReleaseNotesFeed.tsx b/docs/src/components/ReleaseNotesFeed.tsx new file mode 100644 index 0000000000..0730424ea2 --- /dev/null +++ b/docs/src/components/ReleaseNotesFeed.tsx @@ -0,0 +1,582 @@ +/** + * ReleaseNotesFeed.tsx + * + * The release notes page (Brief 123, v1 in-site mock). Releases are grouped + * under month headings with a right-side month jump-nav. Inside each + * release, changes are grouped by category in priority order (Breaking + * changes -> New additions -> Feature changes -> Bug fixes -> Design + * system website). Experimental and breaking changes wrap in a + * low-emphasis callout, with a status badge sitting inline next to the + * change title (same shape as the experimental marker in PR #3915, so the + * team sees one pattern across the docs). Package versions render as small + * dark links to each package's npm version page. + */ + +import { useMemo } from "react"; +import { GoabCallout, GoabLink, GoabText } from "@abgov/react-components"; + +import { withBase } from "@/lib/base-url"; + +// --- Types (mirror the release-notes content collection schema) ---------- + +type ChangeType = + | "breaking" + | "addition" + | "feature-change" + | "fix" + | "website"; + +interface ReleaseVersion { + package: "web-components" | "react" | "angular" | "common"; + version: string; +} + +interface DocLink { + label: string; + href: string; +} + +interface Change { + title: string; + type: ChangeType; + detail?: string; + bullets?: string[]; + components?: string[]; + issue?: string; + links?: DocLink[]; + experimental?: boolean; + migration?: { before?: string; after?: string; link?: string }; +} + +interface Release { + date: string; + intro?: string; + versions: ReleaseVersion[]; + changes: Change[]; +} + +interface ReleaseNotesFeedProps { + releases: Release[]; +} + +// --- Display constants ---------------------------------------------------- + +const PACKAGE_LABEL: Record = { + "web-components": "Web components", + react: "React", + angular: "Angular", + common: "Common", +}; + +const PACKAGE_NPM: Record = { + "web-components": "@abgov/web-components", + react: "@abgov/react-components", + angular: "@abgov/angular-components", + common: "@abgov/ui-components-common", +}; + +const CHANGE_TYPE_LABEL: Record = { + breaking: "Breaking changes", + addition: "New additions", + "feature-change": "Feature changes", + fix: "Bug fixes", + website: "Design system website", +}; + +// Priority order for category sections within a release. +const CHANGE_TYPE_ORDER: ChangeType[] = [ + "breaking", + "addition", + "feature-change", + "fix", + "website", +]; + +const MONTHS = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December", +]; + +// --- Helpers -------------------------------------------------------------- + +// "2026-05-20" -> "May 20, 2026" +function formatDate(iso: string): string { + const [y, m, d] = iso.split("-").map(Number); + if (!y || !m || !d) return iso; + return `${MONTHS[m - 1]} ${d}, ${y}`; +} + +// "2026-05-20" -> "May 2026" +function monthLabel(iso: string): string { + const [y, m] = iso.split("-").map(Number); + return `${MONTHS[m - 1]} ${y}`; +} + +// "2026-05-20" -> "month-2026-05" (anchor id) +function monthId(iso: string): string { + const [y, m] = iso.split("-"); + return `month-${y}-${m}`; +} + +// "work-side-menu-item" -> "Work Side Menu Item" +function prettifyComponent(slug: string): string { + return slug + .split("-") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); +} + +function issueLink(issue: string): { href: string; label: string } { + if (issue.startsWith("http")) return { href: issue, label: "Details" }; + return { + href: `https://github.com/GovAlta/ui-components/issues/${issue}`, + label: `#${issue}`, + }; +} + +// npm version URL for a package + version. +function npmVersionUrl(pkg: ReleaseVersion["package"], version: string): string { + return `https://www.npmjs.com/package/${PACKAGE_NPM[pkg]}/v/${version}`; +} + +// Group a release's changes by type, preserving the priority order. Empty +// categories are dropped. +function groupChangesByType( + changes: Change[], +): { type: ChangeType; changes: Change[] }[] { + const groups = new Map(); + for (const c of changes) { + const arr = groups.get(c.type) ?? []; + arr.push(c); + groups.set(c.type, arr); + } + return CHANGE_TYPE_ORDER.filter((t) => groups.has(t)).map((t) => ({ + type: t, + changes: groups.get(t)!, + })); +} + +// --- Small shared pieces -------------------------------------------------- + +function ComponentLinks({ slugs }: { slugs: string[] }) { + return ( + <> + {slugs.map((slug, i) => ( + + {i > 0 && ", "} + + {prettifyComponent(slug)} + + + ))} + + ); +} + +function IssueLink({ issue }: { issue: string }) { + const { href, label } = issueLink(issue); + return ( + + {label} + + ); +} + +// Quiet line of component link(s) + issue ref under a change. +function ChangeMeta({ change }: { change: Change }) { + const hasComponents = !!change.components && change.components.length > 0; + if (!hasComponents && !change.issue) return null; + return ( + + {hasComponents && } + {hasComponents && change.issue && " · "} + {change.issue && } + + ); +} + +function Bullets({ items }: { items: string[] }) { + return ( +
    + {items.map((b, i) => ( +
  • {b}
  • + ))} +
+ ); +} + +// "Learn more" / documentation links for a change. Internal docs navigate in +// place; external URLs open in a new tab. +function DocLinks({ links }: { links?: DocLink[] }) { + if (!links || links.length === 0) return null; + return ( +
    + {links.map((link, i) => { + const external = link.href.startsWith("http"); + return ( +
  • + + + {link.label} + + +
  • + ); + })} +
+ ); +} + +// --- Change treatments ---------------------------------------------------- + +// Status badge sitting inline next to the change title. Marks experimental +// or breaking changes with a quiet badge inside their callout. Same shape +// as the experimental marker in PR #3915 so the team sees a single pattern +// across the docs; revisit extracting a shared once we see +// how this lands in review. +function StatusBadge({ + label, + type, +}: { + label: string; + type: "information" | "emergency"; +}) { + return ; +} + +// Migration block for breaking changes: optional before/after code and an +// optional link out to a fuller migration guide. +function MigrationBlock({ + migration, +}: { + migration: NonNullable; +}) { + return ( +
+ {migration.before && ( +
+ Before +
+            {migration.before}
+          
+
+ )} + {migration.after && ( +
+ After +
+            {migration.after}
+          
+
+ )} + {migration.link && ( + + Migration guide + + )} +
+ ); +} + +function ChangeEntry({ change }: { change: Change }) { + const status = + change.type === "breaking" + ? { label: "Breaking change", type: "emergency" as const } + : change.experimental + ? { label: "Experimental", type: "information" as const } + : null; + + const titleRow = status ? ( +
+ {change.title} + +
+ ) : ( + {change.title} + ); + + const content = ( + <> + {titleRow} + {change.detail && ( + {change.detail} + )} + {change.bullets && change.bullets.length > 0 && ( + + )} + {change.migration && } + + + + ); + + // Experimental and breaking wrap in a low-emphasis callout. The callout's + // heading prop is "" deliberately: Callout.svelte has specific CSS for + // the emphasis-low + empty-heading case (collapses the heading bar, + // adjusts body padding). We render our own title row (title + status + // badge inline) at the top of the body, matching the experimental + // marker pattern from PR #3915. + if (status) { + return ( + + {content} + + ); + } + + return
{content}
; +} + +// --- Release + month ------------------------------------------------------ + +function ReleaseEntry({ release }: { release: Release }) { + const groupedChanges = groupChangesByType(release.changes); + return ( +
+ {formatDate(release.date)} + {release.versions.length > 0 && ( + + {release.versions.map((v, i) => ( + + {i > 0 && ( + + )} + + + {PACKAGE_LABEL[v.package]} {v.version} + + + + ))} + + )} + {release.intro && {release.intro}} + {groupedChanges.map((group) => { + const headingId = `category-${release.date}-${group.type}`; + return ( +
+ + {CHANGE_TYPE_LABEL[group.type]} + +
+ {group.changes.map((c, i) => ( + + ))} +
+
+ ); + })} +
+ ); +} + +// --- Main component ------------------------------------------------------- + +export function ReleaseNotesFeed({ releases }: ReleaseNotesFeedProps) { + // Group releases (already newest-first) into months, preserving order. + const months = useMemo(() => { + const groups: { label: string; id: string; releases: Release[] }[] = []; + const byLabel = new Map(); + for (const r of releases) { + const label = monthLabel(r.date); + let group = byLabel.get(label); + if (!group) { + group = { label, id: monthId(r.date), releases: [] }; + byLabel.set(label, group); + groups.push(group); + } + group.releases.push(r); + } + return groups; + }, [releases]); + + return ( +
+ {months.map((month, mi) => ( +
+ {/* The first month's heading duplicates the date right below it, so + hide it visually. Kept in the DOM as the TOC anchor + section + label; later months show it as a section divider. */} + {mi === 0 ? ( +
+ + {month.label} + +
+ ) : ( + + {month.label} + + )} + {month.releases.map((r) => ( + + ))} +
+ ))} + + +
+ ); +} + +export default ReleaseNotesFeed; diff --git a/docs/src/components/search/quick-links.ts b/docs/src/components/search/quick-links.ts index 930be504f6..1117c2c8ff 100644 --- a/docs/src/components/search/quick-links.ts +++ b/docs/src/components/search/quick-links.ts @@ -22,7 +22,7 @@ export const quickLinks: QuickLink[] = [ { label: "Components", href: "/components", icon: "shapes" }, { label: "Design tokens", href: "/tokens", icon: "code-slash" }, { label: "Get support", href: "/support", icon: "help-circle" }, - { label: "Release notes", href: "https://github.com/GovAlta/ui-components/releases", icon: "open" }, + { label: "Release notes", href: "/release-notes", icon: "flag" }, ]; /** diff --git a/docs/src/content/config.ts b/docs/src/content/config.ts index 24c1a69adc..4f61dcc0c8 100644 --- a/docs/src/content/config.ts +++ b/docs/src/content/config.ts @@ -274,6 +274,78 @@ const getStarted = defineCollection({ }), }); +/** + * Release Notes Collection + * One entry per coordinated release, date-spined (not per-package). Each + * change has a `type` (breaking / addition / feature-change / fix / + * website) used to group changes under category headings within a + * release, in that priority order. The orthogonal `experimental` flag + * marks status (still gathering feedback) and shows an inline badge + * inside a low-emphasis information callout. Affected package versions + * are quiet metadata, listed only for the packages that actually changed. + * + * Deliberately loose, provisional schema for the v1 in-site mock (Brief 123). + * A `data` collection (not MDX) so changes stay structured. Finalized schema, + * history backfill, and GitHub scaffold automation are deferred to later PRs. + */ +const releaseNotes = defineCollection({ + type: "data", + schema: z.object({ + // Date-spine: ISO date of the coordinated release (newest sorts first). + date: z.string(), + // Optional one-line intro/context Dustin sometimes adds. + intro: z.string().optional(), + // Packages that actually changed this release (quiet version line, not badges). + versions: z.array( + z.object({ + package: z.enum(["web-components", "react", "angular", "common"]), + version: z.string(), + }), + ), + // Changes grouped by `type` within each release (priority order: + // breaking -> addition -> feature-change -> fix -> website). + changes: z.array( + z.object({ + title: z.string(), + // Category for grouping inside a release. + type: z.enum([ + "breaking", + "addition", + "feature-change", + "fix", + "website", + ]), + detail: z.string().optional(), + // Optional sub-points (Claude/Notion-style bullets under a title). + bullets: z.array(z.string()).optional(), + // Component slug(s) this change touches -> quiet live links. + components: z.array(z.string()).optional(), + // Optional issue/PR reference for developers (number or URL). + issue: z.string().optional(), + // Doc / "learn more" links: internal docs (e.g. /get-started/...) or + // external URLs. Internal links navigate in-place; external open in a tab. + links: z + .array(z.object({ label: z.string(), href: z.string() })) + .optional(), + // Status flag: still gathering feedback. Shows an inline "Experimental" + // badge with the title, inside a low-emphasis information callout. + // Orthogonal to type (an experimental addition is type: "addition" + + // experimental: true). + experimental: z.boolean().optional(), + // Migration help for type: "breaking" changes: optional before/after + // snippets and/or a link to a fuller migration guide. + migration: z + .object({ + before: z.string().optional(), + after: z.string().optional(), + link: z.string().optional(), + }) + .optional(), + }), + ), + }), +}); + export const collections = { components, guidance, @@ -281,4 +353,5 @@ export const collections = { foundations, productTypes, "get-started": getStarted, + "release-notes": releaseNotes, }; diff --git a/docs/src/content/release-notes/2023-01-31.json b/docs/src/content/release-notes/2023-01-31.json new file mode 100644 index 0000000000..9e2ce94089 --- /dev/null +++ b/docs/src/content/release-notes/2023-01-31.json @@ -0,0 +1,49 @@ +{ + "date": "2023-01-31", + "versions": [ + { "package": "react", "version": "4.5.0" }, + { "package": "web-components", "version": "1.5.0" } + ], + "changes": [ + { + "title": "Stylesheet import path changed", + "type": "breaking", + "detail": "If you update web-components, your styles need a small change: remove @abgov/styles from your package.json, then point your stylesheet import at the web-components package instead.", + "migration": { + "before": "@import \"@abgov/styles/styles.esm.css\";", + "after": "@import \"@abgov/web-components/index.css\";" + } + }, + { + "title": "Pagination component", + "type": "addition", + "detail": "A new Pagination component is now available.", + "components": ["pagination"] + }, + { + "title": "Design tokens across the system", + "type": "feature-change", + "detail": "The whole design system now uses design tokens, so colour, font, and other value updates reach your team quickly and easily." + }, + { + "title": "Input background colour", + "type": "feature-change", + "detail": "The background colour for input components is now white.", + "components": ["input"] + }, + { + "title": "Dynamic dropdown items", + "type": "fix", + "detail": "Items populated dynamically now update correctly in the dropdown.", + "components": ["dropdown"] + }, + { + "title": "Documentation updates", + "type": "website", + "bullets": [ + "New Skeleton examples for line count and max width.", + "Added documentation on our supported browsers." + ] + } + ] +} diff --git a/docs/src/content/release-notes/2024-07-22.json b/docs/src/content/release-notes/2024-07-22.json new file mode 100644 index 0000000000..ab1db280e0 --- /dev/null +++ b/docs/src/content/release-notes/2024-07-22.json @@ -0,0 +1,14 @@ +{ + "date": "2024-07-22", + "versions": [ + { "package": "web-components", "version": "1.23.1" } + ], + "changes": [ + { + "title": "App Header link alignment", + "type": "fix", + "detail": "Active header links are now vertically centered.", + "components": ["app-header"] + } + ] +} diff --git a/docs/src/content/release-notes/2026-05-14.json b/docs/src/content/release-notes/2026-05-14.json new file mode 100644 index 0000000000..7561a5946b --- /dev/null +++ b/docs/src/content/release-notes/2026-05-14.json @@ -0,0 +1,21 @@ +{ + "date": "2026-05-14", + "versions": [ + { "package": "web-components", "version": "2.1.2" }, + { "package": "react", "version": "7.1.2" } + ], + "changes": [ + { + "title": "Tooltip clipping near scroll edges", + "type": "fix", + "detail": "Tooltips no longer clip at the edge of a scrolling container.", + "components": ["tooltip"] + }, + { + "title": "Dropdown keyboard navigation", + "type": "fix", + "detail": "Arrow keys now move through filtered options in the correct order.", + "components": ["dropdown"] + } + ] +} diff --git a/docs/src/content/release-notes/2026-05-20.json b/docs/src/content/release-notes/2026-05-20.json new file mode 100644 index 0000000000..fb372efdb3 --- /dev/null +++ b/docs/src/content/release-notes/2026-05-20.json @@ -0,0 +1,69 @@ +{ + "date": "2026-05-20", + "versions": [ + { "package": "web-components", "version": "2.2.0" }, + { "package": "react", "version": "7.2.0" }, + { "package": "angular", "version": "5.2.0" }, + { "package": "common", "version": "2.2.0" } + ], + "changes": [ + { + "title": "Dark mode", + "type": "addition", + "detail": "Components now support dark mode. This is experimental while we gather feedback. Try it in your service and share what you learn, it will help shape the production version.", + "experimental": true, + "issue": "3873", + "links": [ + { "label": "Designing for dark mode", "href": "/get-started/designers/designing-for-dark-mode" }, + { "label": "Dark mode theme for developers", "href": "/get-started/developers/dark-mode-theme" } + ] + }, + { + "title": "Accordion actions slot and filled heading", + "type": "addition", + "detail": "Added support for an actions slot in the Accordion header, plus a new headingType property that can be set to \"filled\".", + "components": ["accordion"] + }, + { + "title": "Badges in the Work Side Menu Item", + "type": "addition", + "detail": "Added a trailingContent slot so you can place badges in a work side menu item.", + "components": ["work-side-menu-item"], + "issue": "3814" + }, + { + "title": "Consistent file upload validation", + "type": "fix", + "detail": "Invalid uploaded files now render consistently as FileUploadCard components, including their validation error states.", + "components": ["file-upload-card"] + }, + { + "title": "Interactive components no longer close their parent", + "type": "fix", + "detail": "Fixed bubbling close events, so using a popover, date picker, dropdown, or drawer inside a parent component no longer closes the parent.", + "components": ["popover", "date-picker", "dropdown", "drawer"] + }, + { + "title": "Temporary Notification progress indicators", + "type": "fix", + "detail": "Progress and indeterminate notification types now render their progress indicators correctly.", + "components": ["temporary-notification"] + }, + { + "title": "Visual design refinements", + "type": "feature-change", + "detail": "Container, Drawer, and Dropdown were updated to better match the latest visual design.", + "components": ["container", "drawer", "dropdown"] + }, + { + "title": "Documentation updates", + "type": "website", + "bullets": [ + "Corrected all React and Angular property and event documentation.", + "Added documentation for the Notification Panel component.", + "Added a new \"Updating your product\" guide.", + "Fixed the Work Side Menu \"with user profile\" example to include the profile menu." + ] + } + ] +} diff --git a/docs/src/lib/get-started-nav.ts b/docs/src/lib/get-started-nav.ts index d73df4cb40..35d164fbbf 100644 --- a/docs/src/lib/get-started-nav.ts +++ b/docs/src/lib/get-started-nav.ts @@ -63,6 +63,11 @@ export async function getGetStartedNav(): Promise { const published = entries.filter((e) => e.data.status !== "deprecated"); const topPages = bySection(published, "intro").map(entryToItem); + // Release notes lives in its own collection/page (/release-notes), not a + // get-started entry, so surface it in the Get Started nav right after + // "Start with the design system" (the /get-started landing). + const startIndex = topPages.findIndex((p) => p.url === "/get-started"); + topPages.splice(startIndex + 1, 0, { label: "Release notes", url: "/release-notes" }); const bottomPages = bySection(published, "appendix").map(entryToItem); const groups = GROUP_ORDER.filter((slug) => diff --git a/docs/src/pages/release-notes/index.astro b/docs/src/pages/release-notes/index.astro new file mode 100644 index 0000000000..b7dbbf4f84 --- /dev/null +++ b/docs/src/pages/release-notes/index.astro @@ -0,0 +1,36 @@ +--- +/** + * /release-notes - Release Notes Page (Brief 123) + * + * Uses the shared DocumentationPageLayout like other content pages. + * section="parent" => no false nav highlight (release-notes isn't a nav section). + * The month headings drive the layout's standard right-side table of contents. + */ +import DocumentationPageLayout from '../../layouts/DocumentationPageLayout.astro'; +import { ReleaseNotesFeed } from '../../components/ReleaseNotesFeed'; +import { getCollection } from 'astro:content'; + +const entries = await getCollection('release-notes'); +const releases = entries + .map((entry) => entry.data) + .sort((a, b) => b.date.localeCompare(a.date)); + +const title = 'Release notes'; +const description = + 'What changed in each release of the GoA Design System: new components, features, fixes, and documentation updates.'; +--- + + + Release notes + + What changed in each release of the GoA Design System. Each entry covers a + coordinated release across the web components, React, Angular, and common packages. + + + +