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 shadcn@latest add https://uipkge.dev/r/lit/changelog.json$npx shadcn@latest add https://uipkge.dev/r/lit/changelog.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/changelog.json$bunx shadcn@latest add https://uipkge.dev/r/lit/changelog.jsonManifest:
https://uipkge.dev/r/lit/changelog.jsonInstalls to:src/uip/blocks/changelog/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
releases | Release[] | — | optional |
heading | string | Changelog | optional |
description | string | New features, improvements, and fixes — newest first. | optional |
subscribe | boolean | true | optional |
subscribe-variant | 'default' | 'outline' | outline | optional |
scrollable | boolean | false | optional |
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[]
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-changelog': UipChangelog
}
}Files installed (1)
src/uip/blocks/changelog/changelog.ts7.4 kB
import { LitElement, css, html, nothing } from 'lit' import { Bell } from 'lucide' import { cn } from '../../lib/utils' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' // Side-effect imports: register the composed elements for standalone use // (consumers who install only this block still get working primitives). import '../../components/badge/badge' import '../../components/button/button' import '../../components/section-card/section-card' 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, 'success' | 'info' | 'warning' | 'destructive'> = { 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' }) function formatDate(date: 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`)) } /** * <uip-changelog> — the registry Changelog block as a web component. * * Class strings are React's Changelog verbatim. `heading` is React's `title` * (`title` is taken by HTMLElement). `releases` is a property (arrays don't * cross attributes); the remaining props are kebab-case attributes. The * SectionCard chrome is a <uip-section-card> with the Subscribe action in * its header-action slot. */ export class UipChangelog extends LitElement { static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } // :host display can't be a utility class (the host has no template of its own). static styles = [tailwind, css`:host { display: block; }`] static properties = { releases: {}, heading: {}, description: {}, subscribe: { type: Boolean }, subscribeVariant: { attribute: 'subscribe-variant' }, scrollable: { type: Boolean }, } releases?: Release[] heading = 'Changelog' description = 'New features, improvements, and fixes — newest first.' subscribe = true subscribeVariant: 'default' | 'outline' = 'outline' scrollable = false constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'changelog') } protected updated() { // React passes data-slot="changelog" through SectionCard; <uip-section-card> // labels itself "section-card" on connect, so re-label it for DOM parity. this.renderRoot.querySelector('uip-section-card')?.setAttribute('data-slot', 'changelog') } render() { const source = this.releases ?? stubReleases return html` <uip-section-card data-slot="changelog" heading=${this.heading} description=${this.description}> ${this.subscribe ? html`<span slot="header-action" ><uip-button variant=${this.subscribeVariant} size="sm" >${icon(Bell, 'bell')} Subscribe</uip-button ></span >` : nothing} ${source.length === 0 ? html`<p class="text-muted-foreground py-6 text-center text-sm">No releases yet.</p>` : html`<ol class=${cn('space-y-8', this.scrollable && 'max-h-96 overflow-y-auto pr-1')}> ${source.map( (release, ri) => html`<li> <div class="flex items-center gap-2.5"> <h3 class="font-mono text-sm font-semibold tracking-tight break-all">${release.version}</h3> ${(release.latest ?? ri === 0) ? html`<uip-badge>Latest</uip-badge>` : nothing} <time dateTime=${release.date} class="text-muted-foreground ml-auto text-xs"> ${formatDate(release.date)} </time> </div> <ol class="mt-3"> ${release.entries.map( (entry) => html`<li class="border-border border-l pb-5 pl-6 last:pb-0" > <div class="flex items-start gap-3"> <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"> <uip-badge variant=${entryTone[entry.type]}>${entryLabel[entry.type]}</uip-badge> <p class="text-sm font-medium">${entry.title}</p> </div> ${entry.description ? html`<p class="text-muted-foreground mt-1 text-sm leading-relaxed"> ${entry.description} </p>` : nothing} </div> </div> </li>`, )} </ol> </li>`, )} </ol>`} </uip-section-card> ` } } customElements.get('uip-changelog') || customElements.define('uip-changelog', UipChangelog) declare global { interface HTMLElementTagNameMap { 'uip-changelog': UipChangelog } }
Raw manifest:https://uipkge.dev/r/lit/changelog.json