{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "changelog",
  "title": "Changelog",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/changelog/changelog.component.ts",
      "content": "// Release-notes timeline in a SectionCard: Subscribe header action, version groups with a\n// monospace tag, Latest badge and right-aligned date, and entries pairing a typed badge\n// (New / Improved / Fixed / Breaking) with a title and optional description along a\n// continuous border-l rail with dot markers. Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'\nimport { Bell, LucideAngularModule } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiSectionCardComponent } from '@/ui/section-card/section-card.component'\n\nexport type ChangelogEntryType = 'new' | 'improved' | 'fixed' | 'breaking'\n\nexport interface ChangelogEntry {\n  type: ChangelogEntryType\n  title: string\n  description?: string\n}\n\nexport interface Release {\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\nconst entryTone: Record<ChangelogEntryType, BadgeVariant> = {\n  new: 'success',\n  improved: 'info',\n  fixed: 'warning',\n  breaking: 'destructive',\n}\n\nconst entryLabel: Record<ChangelogEntryType, string> = {\n  new: 'New',\n  improved: 'Improved',\n  fixed: 'Fixed',\n  breaking: 'Breaking',\n}\n\nconst stubReleases: Release[] = [\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\nconst dateFormatter = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' })\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-changelog, [ui-changelog]',\n  standalone: true,\n  // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiSectionCardComponent],\n  template: `\n    <div data-slot=\"changelog\" [class]=\"rootClass\">\n      <div ui-section-card data-slot=\"changelog\" [title]=\"title()\" [description]=\"description()\">\n        @if (subscribe()) {\n          <button ui-button [variant]=\"subscribeVariant()\" size=\"sm\" slot=\"header-action\">\n            <lucide-icon [img]=\"Bell\" class=\"size-3.5\" aria-hidden=\"true\" />\n            Subscribe\n          </button>\n        }\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]=\"listClass\">\n            @for (release of allReleases(); track release.version; let ri = $index) {\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                    <span ui-badge>Latest</span>\n                  }\n                  <time [attr.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                  @for (entry of release.entries; track entry.title; let ei = $index) {\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                            <span ui-badge [variant]=\"tone(entry.type)\">{{ label(entry.type) }}</span>\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\">{{ entry.description }}</p>\n                          }\n                        </div>\n                      </div>\n                    </li>\n                  }\n                </ol>\n              </li>\n            }\n          </ol>\n        }\n      </div>\n    </div>\n  `,\n})\nexport class UiChangelogComponent {\n  protected readonly Bell = Bell\n\n  private readonly _releases = signal<Release[] | undefined>(undefined)\n  @Input() set releases(v: Release[] | undefined) {\n    this._releases.set(v)\n  }\n  readonly title = signal('Changelog')\n  @Input('title') set titleInput(v: string | undefined) {\n    if (v !== undefined) this.title.set(v)\n  }\n  readonly description = signal('New features, improvements, and fixes — newest first.')\n  @Input('description') set descriptionInput(v: string | undefined) {\n    if (v !== undefined) this.description.set(v)\n  }\n  readonly subscribe = signal(true)\n  @Input('subscribe') set subscribeInput(v: boolean) {\n    this.subscribe.set(v)\n  }\n  readonly subscribeVariant = signal<'default' | 'outline'>('outline')\n  @Input('subscribeVariant') set subscribeVariantInput(v: 'default' | 'outline') {\n    if (v) this.subscribeVariant.set(v)\n  }\n  readonly scrollable = signal(false)\n  @Input('scrollable') set scrollableInput(v: boolean) {\n    this.scrollable.set(v)\n  }\n  @Input('class') className?: string\n\n  readonly allReleases = computed(() => this._releases() ?? stubReleases)\n\n  get rootClass(): string {\n    return cn('w-full', this.className)\n  }\n\n  get listClass(): string {\n    return cn('space-y-8', this.scrollable() && 'max-h-96 overflow-y-auto pr-1')\n  }\n\n  tone(t: ChangelogEntryType): BadgeVariant {\n    return entryTone[t]\n  }\n\n  label(t: ChangelogEntryType): string {\n    return entryLabel[t]\n  }\n\n  isLatest(release: Release, index: number): boolean {\n    return release.latest ?? index === 0\n  }\n\n  formatDate(date: string): 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}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/changelog/changelog.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/changelog/index.ts",
      "content": "export { UiChangelogComponent, type ChangelogEntry, type ChangelogEntryType, type Release } from './changelog.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/changelog/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/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"
  ]
}