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-svelte@latest add https://uipkge.dev/r/svelte/changelog.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/changelog.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/changelog.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/changelog.jsonManifest:
https://uipkge.dev/r/svelte/changelog.jsonInstalls to:src/lib/components/blocks/changelog/Variants
Loading interactive previews…
Loading interactive previews…
npm dependencies
Includes
Files installed (1)
src/lib/components/blocks/changelog/Changelog.svelte6.1 kB
<script lang="ts" module> import type { BadgeVariants } from '$lib/components/ui/badge' export type ChangelogEntryType = 'new' | 'improved' | 'fixed' | 'breaking' export interface ChangelogEntry { type: ChangelogEntryType title: string description?: string } export interface ChangelogRelease { 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[] } export interface ChangelogProps { releases?: ChangelogRelease[] title?: string description?: string subscribe?: boolean subscribeVariant?: 'default' | 'outline' scrollable?: boolean class?: string } </script> <script lang="ts"> import { Bell } from '@lucide/svelte' import { cn } from '$lib/utils' import { Badge } from '$lib/components/ui/badge' import { Button } from '$lib/components/ui/button' import { SectionCard } from '$lib/components/ui/section-card' let { releases, title = 'Changelog', description = 'New features, improvements, and fixes — newest first.', subscribe = true, subscribeVariant = 'outline', scrollable = false, class: className, }: ChangelogProps = $props() const entryTone: Record<ChangelogEntryType, BadgeVariants['variant']> = { new: 'success', improved: 'info', fixed: 'warning', breaking: 'destructive', } const entryLabel: Record<ChangelogEntryType, string> = { new: 'New', improved: 'Improved', fixed: 'Fixed', breaking: 'Breaking', } const stubReleases: ChangelogRelease[] = [ { 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 allReleases = $derived(releases ?? stubReleases) function isLatest(release: ChangelogRelease, index: number) { return release.latest ?? index === 0 } 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`)) } </script> <SectionCard data-slot="changelog" {title} {description} class={className}> {#snippet headerAction()} {#if subscribe} <Button variant={subscribeVariant} size="sm"> <Bell aria-hidden="true" /> Subscribe </Button> {/if} {/snippet} {#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={cn('space-y-8', scrollable && 'max-h-96 overflow-y-auto pr-1')}> {#each allReleases as release, ri (release.version)} <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)} <Badge>Latest</Badge> {/if} <time datetime={release.date} class="text-muted-foreground ml-auto text-xs"> {formatDate(release.date)} </time> </div> <ol class="mt-3"> {#each release.entries as entry, ei (`${release.version}-${ei}`)} <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"> <Badge variant={entryTone[entry.type]}>{entryLabel[entry.type]}</Badge> <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> {/if} </div> </div> </li> {/each} </ol> </li> {/each} </ol> {/if} </SectionCard>
Raw manifest:https://uipkge.dev/r/svelte/changelog.json