Skip to content
Component installs need the registry setup
VegaStack Design

Version Compare

A document's version history at full size — the selected version or its changes beside a day-grouped version list, with Restore and Copy pinned.

Status
stable
Since
0.25.90
Accessibility pattern
named preview region, version listbox, labelled switch

Last updated

Version history

CurrentMKManoj Kumar
30-min checkpointMKManoj Kumar
Before an agent's editsClaude via Manoj Kumar, agentClaude edited 3 blocks
Before reviewMKManoj Kumar

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Version Compare before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify version-compare

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/version-compare

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

The same command installs the registry items it composes: @vegastack/button, @vegastack/diff-view, @vegastack/label, @vegastack/resizable, @vegastack/switch, @vegastack/use-media-query, @vegastack/version-list.

Usage

VersionCompare is a document's history at full size, for DialogContent size="fullscreen": the selected version's text on the start side, the versions beside it, and the actions on the selected version in a footer that stays put while both sides scroll.

import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { VersionCompare } from "@/components/ui/version-compare";

<Dialog open={open} onOpenChange={setOpen}>
  <DialogContent size="fullscreen">
    <DialogHeader>
      <DialogTitle>History · Pricing v2</DialogTitle>
    </DialogHeader>
    <VersionCompare
      versions={versions}
      currentId={versions[0]?.id}
      selectedId={selected}
      onSelect={setSelected}
      showChanges={showChanges}
      onShowChangesChange={setShowChanges}
      preview={<MarkdownView typeScale="document">{text}</MarkdownView>}
      diff={showChanges ? { before: previousText, after: text } : null}
      actions={
        <>
          <Button variant="outline" onClick={copy}>
            Copy
          </Button>
          <Button onClick={restore}>Restore this version</Button>
        </>
      }
    />
  </DialogContent>
</Dialog>;
  • The list is a VersionList grouped by day: "Current", the kind badges, an agent's versions under the agent avatar (author.kind: "agent"), Named only (namedOnly, onNamedOnlyChange) and scroll paging (loadMore) pass straight through.
  • The preview is preview, the host's own rendering of the selected version (MarkdownView, a read-only TextEdit). With "Show changes" on (showChanges, its switch shown when onShowChangesChange is set) and a diff given, the preview is a DiffView of the two texts instead; without a diff the host's preview stays, so a host can show its own formatted diff (TextEdit's diff view).
  • toolbar sits beside "Show changes": a compare-with toggle, "Name this version".
  • actions — Restore this version, Copy — sit in the footer.

The preview and the list share a ResizablePanels group: drag the handle to give the list more room (it keeps at least 240px). Below 640px one side shows at a time: the list first; a tap (or Enter) on a version pushes its preview in, with the actions, and a "Versions" button goes back.

A host that opens the history on one version — a "View changes" link to an agent's checkpoint — sets selectedId, showChanges and defaultPhoneView="preview", so a phone opens on that version's changes rather than the list; "Versions" still goes back. To drive the side itself, pass phoneView with onPhoneViewChange, which reports "preview" on a pick and "list" from the back button. A side the host sets moves no focus; a side the reader picks takes focus with it.

// Opened from "View changes" on a checkpoint: start on it, compared with now.
const [selected, setSelected] = React.useState(checkpointId);
const [showChanges, setShowChanges] = React.useState(true);

<VersionCompare
  versions={versions}
  selectedId={selected}
  onSelect={setSelected}
  showChanges={showChanges}
  onShowChangesChange={setShowChanges}
  diff={showChanges ? { before: textOf(selected), after: currentText } : null}
  defaultPhoneView="preview"
/>;

Version history

CurrentMKManoj Kumar
30-min checkpointMKManoj Kumar
Before an agent's editsClaude via Manoj Kumar, agentClaude edited 3 blocks
Before reviewMKManoj Kumar

Examples

In a full-screen dialog

Its home: DialogContent size="fullscreen" fills the viewport less a 16px inset (the whole screen on a phone), so the dialog's header stays at the top and VersionCompare fills the rest.

API Reference

PropTypeDefaultDescription
versions*readonly VersionItem[]—The versions, newest first — grouped by day in the list.
actionsReact.ReactNode—The actions on the selected version — Restore this version, Copy — in a footer that stays put while the preview and the list scroll.
backLabelstring"Versions"The phone's back button, from the preview to the list.
currentIdstring | nullnullThe version that is the document's current text: a "Current" badge.
defaultPhoneView"list" | "preview""list"The side a phone shows first (uncontrolled).
diff{ before: string; after: string; } | null—The two texts to compare while showChanges is on: the preview shows them through DiffView (added and removed words in reading order).
emptyStateReact.ReactNode"No versions yet"Shown when there are no versions.
headingstring"Version history"The list's heading ("History" where the host calls it that).
loadingbooleanfalseShow the skeleton instead of the list.
loadMore{ hasMore: boolean; onLoadMore: () => void; loading?: boolean; }—More versions exist: the next page loads as the list's end scrolls into view (keyset lists have no totals).
namedOnlybooleanfalseShow named versions only (controlled).
nownumber—Pin the relative times' clock (docs, tests).
onNamedOnlyChange((namedOnly: boolean) => void)—Called from the header's "Named only" switch; the switch shows when set.
onPhoneViewChange((view: "list" | "preview") => void)—Called when the reader moves between the sides on a phone: "preview" on picking a version, "list" from the back button.
onSelect((id: string) => void)—Called when a version is picked (a click, or the arrow keys in the list).
onShowChangesChange((showChanges: boolean) => void)—Called from the "Show changes" switch; the switch shows when set.
phoneView"list" | "preview"—The side a phone shows (controlled) — "preview" opens straight on the selected version, as a link to one checkpoint's changes does. Pair with onPhoneViewChange. Ignored at 640px and up, where both sides show.
previewReact.ReactNode—The selected version as it reads — the host's renderer (MarkdownView, a read-only TextEdit), or its own diff rendering while showChanges is on and no diff is given.
previewLabelstring"Selected version"The preview region's name.
selectedIdstring | nullnullThe selected version, shown in the preview.
showChangesbooleanfalseShow the selected version's changes (controlled).
showChangesLabelstring"Show changes"The "Show changes" switch's label.
toolbarReact.ReactNode—More controls beside "Show changes" (a compare-with toggle, Name this version).

Data attributes and CSS variables on VersionCompare

AttributeValues
data-slot"version-compare" | "version-compare-actions" | "version-compare-list" | "version-compare-panels" | "version-compare-preview" | "version-compare-preview-body" | "version-compare-toolbar"
data-viewmirrors a prop or state value

Accessibility

  • The preview is a region named "Selected version" (previewLabel); the list is the VersionList listbox, each day a group named by its heading, one tab stop with the selection following the arrow keys.
  • "Show changes" is a switch labelled by its text; the resize handle is a focusable separator.
  • On a phone the back button is a labelled "Versions" button.
KeyAction
↑ / ↓Select the previous / next version
EnterOn a phone, open the selected version's preview
← / →On the focused handle, resize the preview and the list
TabMove through the toolbar, the preview, the list, the actions
ContractStates tested
Behaviourselected, show-changes, diff, phone-list, phone-preview, loading
Accessibilitylabeled, browser-accessibility-test, keyboard
Visualdefault, show-changes, phone

Do / Don't

Do
Open it in DialogContent size="fullscreen" with the document's name in the title, and keep Restore this version in actions so it is always in view.
Don't
Squeeze it into a side sheet or a default-size dialog, or restore a version from a row click.

On this page