caliper uidocs

Changelog template

Block

What shipped, newest first: dates that stay in view, linkable versions, tag filters with a count and a product view per release.

Installs components/ui/changelog-template.tsxDepends on lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/changelog-template.json
Skip to content

Changelog

What’s new

Every release of Halden, newest first.

3 releases
  1. v3.8
    • New

    NetSuite, both ways

    Matches now write back to NetSuite as cleared transactions, and edits made in NetSuite flow back into the close.

    • Two-way sync every four hours, or on demand.
    • Subsidiaries map to entities automatically.
    halden.app/settings/netsuite
    Sync directionBoth ways
    Every4 hours
    Last sync09:12 · 418 transactions
    Subsidiaries3 mapped to entities
  2. v3.7
    • Improved

    Rules that explain themselves

    Each rule now shows how many lines it matched last month, so you can retire the ones that never fire.

    halden.app/rules
    Stripe payoutsmatches 1,204 lines / month
    SaaS annual plansmatches 86 lines / month
    Payroll providersnew · matches 31 lines / month
  3. v3.6.2
    • Fixed

    Faster exports

    Audit trail exports over 50,000 rows no longer time out. A 4,000-row export now takes about two seconds.

When to use #

Use it for

  • A public changelog or release notes page.
  • Releases with a screen or a log worth showing.

Skip it when

  • Internal commit history; link to the repository.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/changelog-template.json

Adds its 6 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above.

example.tsx
import { ChangelogTemplate } from "@/components/ui/changelog-template"

const siteNav = (current?: string) => [
  { label: "Product", href: "#product" },
  { label: "Pricing", href: "#pricing", current: current === "pricing" },
  { label: "Customers", href: "#customers" },
  { label: "Changelog", href: "#changelog", current: current === "changelog" },
];

function ReleaseRules() {
  return (
    <div className="grid gap-2 text-[13px]">
      {[["Stripe payouts", "matches 1,204 lines / month"], ["SaaS annual plans", "matches 86 lines / month"], ["Payroll providers", "new · matches 31 lines / month"]].map(([name, note]) => (
        <div key={name} className="flex items-center justify-between gap-3 rounded-[10px] px-3.5 py-2.5 shadow-[0_0_0_1px_var(--k-line)]"><b className="font-medium">{name}</b><span className="text-[var(--k-muted)]">{note}</span></div>
      ))}
    </div>
  );
}

const releases: Parameters<typeof ChangelogTemplate>[0]["entries"] = [
    { version: "3.8", date: "2026-10-07", title: "NetSuite, both ways", tags: ["New"], body: <><p>Matches now write back to NetSuite as cleared transactions, and edits made in NetSuite flow back into the close.</p><ul><li><strong>Two-way sync</strong> every four hours, or on demand.</li><li>Subsidiaries map to entities automatically.</li></ul></>, media: <Ledgerish />, mediaTitle: "halden.app/settings/netsuite" },
    { version: "3.7", date: "2026-09-22", title: "Rules that explain themselves", tags: ["Improved"], body: <p>Each rule now shows how many lines it matched last month, so you can retire the ones that never fire.</p>, media: <ReleaseRules />, mediaTitle: "halden.app/rules" },
    { version: "3.6.2", date: "2026-09-09", title: "Faster exports", tags: ["Fixed"], body: <p>Audit trail exports over 50,000 rows no longer time out. A 4,000-row export now takes about two seconds.</p> },
];

function Ledgerish() {
  return (
    <div className="grid gap-2 text-[13px]">
      {[["Sync direction", "Both ways"], ["Every", "4 hours"], ["Last sync", "09:12 · 418 transactions"], ["Subsidiaries", "3 mapped to entities"]].map(([label, value]) => (
        <div key={label} className="flex justify-between gap-3 border-t border-[var(--k-line)] pt-2 first:border-t-0 first:pt-0"><span className="text-[var(--k-muted)]">{label}</span><span className="tabular-nums">{value}</span></div>
      ))}
    </div>
  );
}

export function ChangelogDemo() {
  return (
    <div className="h-[720px] w-full overflow-auto rounded-[14px] shadow-[0_0_0_1px_var(--border)]">
      <ChangelogTemplate
        brand="Halden"
        nav={siteNav("changelog")}
        signIn={{ label: "Sign in", href: "#" }}
        primary={heroes.primary!}
        title="What’s new"
        lead="Every release of Halden, newest first."
        entries={releases}
        footer={footers}
      />
    </div>
  );
}

API reference #

ChangelogTemplate

component
PropTypeDefault
brand *string—
navreadonly BlockNavLink[][]
signInBlockAction—
primaryBlockAction—
eyebrowstring"Changelog"
title *string—
leadstring—
entries *readonly ChangelogTemplateEntry[]—
footer(Omit<Without<{ brand: string; tagline?: string | undefined; columns: readonly BlockFooterColumn[]; newsletter?: { label: string; note?: string | undefined; …—
classNamestring—

Accessibility #

It exposes group and status semantics, with aria-label, aria-pressed and aria-hidden kept in step with state. Changes that matter are announced through a live region.

No custom key handling; Tab and the native activation keys do the work.

Motion #

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.