{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "changelog",
  "title": "Changelog",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-svelte/blocks/changelog/Changelog.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { BadgeVariants } from '$lib/components/ui/badge'\n\n  export type ChangelogEntryType = 'new' | 'improved' | 'fixed' | 'breaking'\n\n  export interface ChangelogEntry {\n    type: ChangelogEntryType\n    title: string\n    description?: string\n  }\n\n  export interface ChangelogRelease {\n    version: string\n    /** ISO date string, e.g. '2026-08-14'. */\n    date: string\n    /** Overrides the default \"first release in the array is the latest\" rule. */\n    latest?: boolean\n    entries: ChangelogEntry[]\n  }\n\n  export interface ChangelogProps {\n    releases?: ChangelogRelease[]\n    title?: string\n    description?: string\n    subscribe?: boolean\n    subscribeVariant?: 'default' | 'outline'\n    scrollable?: boolean\n    class?: string\n  }\n</script>\n\n<script lang=\"ts\">\n  import { Bell } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n  import { Badge } from '$lib/components/ui/badge'\n  import { Button } from '$lib/components/ui/button'\n  import { SectionCard } from '$lib/components/ui/section-card'\n\n  let {\n    releases,\n    title = 'Changelog',\n    description = 'New features, improvements, and fixes — newest first.',\n    subscribe = true,\n    subscribeVariant = 'outline',\n    scrollable = false,\n    class: className,\n  }: ChangelogProps = $props()\n\n  const entryTone: Record<ChangelogEntryType, BadgeVariants['variant']> = {\n    new: 'success',\n    improved: 'info',\n    fixed: 'warning',\n    breaking: 'destructive',\n  }\n\n  const entryLabel: Record<ChangelogEntryType, string> = {\n    new: 'New',\n    improved: 'Improved',\n    fixed: 'Fixed',\n    breaking: 'Breaking',\n  }\n\n  const stubReleases: ChangelogRelease[] = [\n    {\n      version: 'v2.4.0',\n      date: '2026-08-14',\n      entries: [\n        {\n          type: 'new',\n          title: 'Command palette',\n          description: 'Global ⌘K palette with fuzzy search and full keyboard navigation.',\n        },\n        { type: 'improved', title: 'Table virtualization', description: 'Tables stay at 60fps past 10k rows.' },\n        { type: 'fixed', title: 'Dark mode contrast', description: 'Sidebar badges now meet AA contrast in dark mode.' },\n      ],\n    },\n    {\n      version: 'v2.3.0',\n      date: '2026-07-30',\n      entries: [\n        {\n          type: 'new',\n          title: 'Saved views',\n          description: 'Pin filtered table views to the sidebar and share them with your team.',\n        },\n        {\n          type: 'improved',\n          title: 'CSV import',\n          description: 'Merged cells and formula columns are flattened on import.',\n        },\n        { type: 'fixed', title: 'Scheduled exports', description: 'Exports no longer drift across DST boundaries.' },\n      ],\n    },\n    {\n      version: 'v2.2.1',\n      date: '2026-07-08',\n      entries: [\n        {\n          type: 'breaking',\n          title: 'Node 20+ required',\n          description: 'Node 18 reached end of life; the CLI now targets Node 20 and newer.',\n        },\n        {\n          type: 'fixed',\n          title: 'Safari login loop',\n          description: 'Session cookies set on a redirect were dropped by ITP.',\n        },\n        {\n          type: 'fixed',\n          title: 'Duplicate webhooks',\n          description: 'Retries after a timeout no longer double-deliver events.',\n        },\n      ],\n    },\n  ]\n\n  const allReleases = $derived(releases ?? stubReleases)\n\n  function isLatest(release: ChangelogRelease, index: number) {\n    return release.latest ?? index === 0\n  }\n\n  const dateFormatter = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' })\n\n  function formatDate(date: string) {\n    // Pin the time so a UTC-stored ISO date cannot shift a day in negative offsets.\n    return dateFormatter.format(new Date(`${date}T00:00:00`))\n  }\n</script>\n\n<SectionCard data-slot=\"changelog\" {title} {description} class={className}>\n  {#snippet headerAction()}\n    {#if subscribe}\n      <Button variant={subscribeVariant} size=\"sm\">\n        <Bell aria-hidden=\"true\" />\n        Subscribe\n      </Button>\n    {/if}\n  {/snippet}\n\n  {#if allReleases.length === 0}\n    <p class=\"text-muted-foreground py-6 text-center text-sm\">No releases yet.</p>\n  {:else}\n    <!-- Version groups. Each entry li carries its own border-l segment so the\n         rail stays continuous through row spacing but breaks at version headers. -->\n    <ol class={cn('space-y-8', scrollable && 'max-h-96 overflow-y-auto pr-1')}>\n      {#each allReleases as release, ri (release.version)}\n        <li>\n          <div class=\"flex items-center gap-2.5\">\n            <h3 class=\"font-mono text-sm font-semibold tracking-tight break-all\">{release.version}</h3>\n            {#if isLatest(release, ri)}\n              <Badge>Latest</Badge>\n            {/if}\n            <time datetime={release.date} class=\"text-muted-foreground ml-auto text-xs\">\n              {formatDate(release.date)}\n            </time>\n          </div>\n\n          <ol class=\"mt-3\">\n            {#each release.entries as entry, ei (`${release.version}-${ei}`)}\n              <li class=\"border-border border-l pb-5 pl-6 last:pb-0\">\n                <div class=\"flex items-start gap-3\">\n                  <!-- In-flow dot pulled back over the rail; ring punches out the line\n                       behind it (same trick as TimelineMedia's connector). -->\n                  <span\n                    aria-hidden=\"true\"\n                    class=\"bg-muted-foreground/40 ring-background mt-1.5 -ml-[30px] size-2.5 shrink-0 rounded-full ring-4\"\n                  ></span>\n                  <div class=\"min-w-0\">\n                    <div class=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n                      <Badge variant={entryTone[entry.type]}>{entryLabel[entry.type]}</Badge>\n                      <p class=\"text-sm font-medium\">{entry.title}</p>\n                    </div>\n                    {#if entry.description}\n                      <p class=\"text-muted-foreground mt-1 text-sm leading-relaxed\">\n                        {entry.description}\n                      </p>\n                    {/if}\n                  </div>\n                </div>\n              </li>\n            {/each}\n          </ol>\n        </li>\n      {/each}\n    </ol>\n  {/if}\n</SectionCard>\n",
      "type": "registry:block",
      "target": "~/src/lib/components/blocks/changelog/Changelog.svelte"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/badge.json",
    "https://uipkge.dev/r/svelte/button.json",
    "https://uipkge.dev/r/svelte/section-card.json"
  ],
  "description": "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. Pass `releases` to replace the stub; first entry is treated as latest unless a release sets `latest`.",
  "categories": [
    "marketing"
  ]
}