Changelog
blockmarketingRelease-notes timeline in a SectionCard: Subscribe header action, version groups with a monospace tag, Latest badge and right-aligned date, and entries pairing a typed badge (New / Improved / Fixed / Breaking) with a title and optional description along a continuous border-l rail with dot markers. Pass `releases` to replace the stub; first entry is treated as latest unless a release sets `latest`.
Installation
$pnpm dlx uipkge-ng@latest add changelog$npx uipkge-ng@latest add changelog$yarn dlx uipkge-ng@latest add changelog$bunx uipkge-ng@latest add changelogManifest:
https://uipkge.dev/r/angular/changelog.jsonInstalls to:src/app/components/blocks/changelog/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ChangelogEntryinterface ChangelogEntry {
type: ChangelogEntryType
title: string
description?: string
}Releaseinterface Release {
version: string
/** ISO date string, e.g. '2026-08-14'. */
date: string
/** Overrides the default "first release in the array is the latest" rule. */
latest?: boolean
entries: ChangelogEntry[]
}npm dependencies
Includes
Files installed (2)
src/app/components/blocks/changelog/changelog.component.ts7.9 kB
// Release-notes timeline in a SectionCard: Subscribe header action, version groups with a // monospace tag, Latest badge and right-aligned date, and entries pairing a typed badge // (New / Improved / Fixed / Breaking) with a title and optional description along a // continuous border-l rail with dot markers. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { Bell, LucideAngularModule } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiSectionCardComponent } from '@/ui/section-card/section-card.component' export type ChangelogEntryType = 'new' | 'improved' | 'fixed' | 'breaking' export interface ChangelogEntry { type: ChangelogEntryType title: string description?: string } export interface Release { version: string /** ISO date string, e.g. '2026-08-14'. */ date: string /** Overrides the default "first release in the array is the latest" rule. */ latest?: boolean entries: ChangelogEntry[] } const entryTone: Record<ChangelogEntryType, BadgeVariant> = { new: 'success', improved: 'info', fixed: 'warning', breaking: 'destructive', } const entryLabel: Record<ChangelogEntryType, string> = { new: 'New', improved: 'Improved', fixed: 'Fixed', breaking: 'Breaking', } const stubReleases: Release[] = [ { version: 'v2.4.0', date: '2026-08-14', entries: [ { type: 'new', title: 'Command palette', description: 'Global ⌘K palette with fuzzy search and full keyboard navigation.', }, { type: 'improved', title: 'Table virtualization', description: 'Tables stay at 60fps past 10k rows.' }, { type: 'fixed', title: 'Dark mode contrast', description: 'Sidebar badges now meet AA contrast in dark mode.' }, ], }, { version: 'v2.3.0', date: '2026-07-30', entries: [ { type: 'new', title: 'Saved views', description: 'Pin filtered table views to the sidebar and share them with your team.', }, { type: 'improved', title: 'CSV import', description: 'Merged cells and formula columns are flattened on import.', }, { type: 'fixed', title: 'Scheduled exports', description: 'Exports no longer drift across DST boundaries.' }, ], }, { version: 'v2.2.1', date: '2026-07-08', entries: [ { type: 'breaking', title: 'Node 20+ required', description: 'Node 18 reached end of life; the CLI now targets Node 20 and newer.', }, { type: 'fixed', title: 'Safari login loop', description: 'Session cookies set on a redirect were dropped by ITP.', }, { type: 'fixed', title: 'Duplicate webhooks', description: 'Retries after a timeout no longer double-deliver events.', }, ], }, ] const dateFormatter = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-changelog, [ui-changelog]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiSectionCardComponent], template: ` <div data-slot="changelog" [class]="rootClass"> <div ui-section-card data-slot="changelog" [title]="title()" [description]="description()"> @if (subscribe()) { <button ui-button [variant]="subscribeVariant()" size="sm" slot="header-action"> <lucide-icon [img]="Bell" class="size-3.5" aria-hidden="true" /> Subscribe </button> } @if (allReleases().length === 0) { <p class="text-muted-foreground py-6 text-center text-sm">No releases yet.</p> } @else { <!-- Version groups. Each entry li carries its own border-l segment so the rail stays continuous through row spacing but breaks at version headers. --> <ol [class]="listClass"> @for (release of allReleases(); track release.version; let ri = $index) { <li> <div class="flex items-center gap-2.5"> <h3 class="font-mono text-sm font-semibold tracking-tight break-all">{{ release.version }}</h3> @if (isLatest(release, ri)) { <span ui-badge>Latest</span> } <time [attr.datetime]="release.date" class="text-muted-foreground ml-auto text-xs"> {{ formatDate(release.date) }} </time> </div> <ol class="mt-3"> @for (entry of release.entries; track entry.title; let ei = $index) { <li class="border-border border-l pb-5 pl-6 last:pb-0"> <div class="flex items-start gap-3"> <!-- In-flow dot pulled back over the rail; ring punches out the line behind it (same trick as TimelineMedia's connector). --> <span aria-hidden="true" class="bg-muted-foreground/40 ring-background mt-1.5 -ml-[30px] size-2.5 shrink-0 rounded-full ring-4" ></span> <div class="min-w-0"> <div class="flex flex-wrap items-center gap-x-2 gap-y-1"> <span ui-badge [variant]="tone(entry.type)">{{ label(entry.type) }}</span> <p class="text-sm font-medium">{{ entry.title }}</p> </div> @if (entry.description) { <p class="text-muted-foreground mt-1 text-sm leading-relaxed">{{ entry.description }}</p> } </div> </div> </li> } </ol> </li> } </ol> } </div> </div> `, }) export class UiChangelogComponent { protected readonly Bell = Bell private readonly _releases = signal<Release[] | undefined>(undefined) @Input() set releases(v: Release[] | undefined) { this._releases.set(v) } readonly title = signal('Changelog') @Input('title') set titleInput(v: string | undefined) { if (v !== undefined) this.title.set(v) } readonly description = signal('New features, improvements, and fixes — newest first.') @Input('description') set descriptionInput(v: string | undefined) { if (v !== undefined) this.description.set(v) } readonly subscribe = signal(true) @Input('subscribe') set subscribeInput(v: boolean) { this.subscribe.set(v) } readonly subscribeVariant = signal<'default' | 'outline'>('outline') @Input('subscribeVariant') set subscribeVariantInput(v: 'default' | 'outline') { if (v) this.subscribeVariant.set(v) } readonly scrollable = signal(false) @Input('scrollable') set scrollableInput(v: boolean) { this.scrollable.set(v) } @Input('class') className?: string readonly allReleases = computed(() => this._releases() ?? stubReleases) get rootClass(): string { return cn('w-full', this.className) } get listClass(): string { return cn('space-y-8', this.scrollable() && 'max-h-96 overflow-y-auto pr-1') } tone(t: ChangelogEntryType): BadgeVariant { return entryTone[t] } label(t: ChangelogEntryType): string { return entryLabel[t] } isLatest(release: Release, index: number): boolean { return release.latest ?? index === 0 } formatDate(date: string): string { // Pin the time so a UTC-stored ISO date cannot shift a day in negative offsets. return dateFormatter.format(new Date(`${date}T00:00:00`)) } }src/app/components/blocks/changelog/index.ts0.1 kB
export { UiChangelogComponent, type ChangelogEntry, type ChangelogEntryType, type Release } from './changelog.component'
Raw manifest:https://uipkge.dev/r/angular/changelog.json