Menu

Change language

GitHub repo

Changelog

blockmarketing

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`.

Installation

$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/changelog.json
Manifest:https://uipkge.dev/r/svelte/changelog.jsonInstalls to:src/lib/components/blocks/changelog/

Variants

Loading interactive previews…

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