- 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.
What shipped, newest first: dates that stay in view, linkable versions, tag filters with a count and a product view per release.
pnpm dlx shadcn@latest add https://caliperui.site/r/changelog-template.jsonChangelog
Every release of Halden, newest first.
Matches now write back to NetSuite as cleared transactions, and edits made in NetSuite flow back into the close.
Each rule now shows how many lines it matched last month, so you can retire the ones that never fire.
Audit trail exports over 50,000 rows no longer time out. A 4,000-row export now takes about two seconds.
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.
This is the code behind the preview above.
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> ); }
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.
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.