DocumentCompare
v1.0.9Updated 20 Oct 2026Two stored documents side by side in a dialog, and ONE decision (fb_56743998): the system-generated original next to the uploaded signed scan, each pane fetching its OWN file with its own loading skeleton, its own error and its own Retry, plus a footer slot for the decision button. It is a compare SHELL around DocumentViewer, not a second viewer: every pane is a DocumentViewerSurface, so the credentialed fetch-then-render, the PDF frame, the image wrapper, the explicit cannot-be-shown state, print and download all behave exactly as in the single-document viewer. DocumentCompareSurface is the dialog-less form for embedding and SSR.
Preview
Live · 12 stories▸Why this is recommendeddocumentcompareside-by-sidepdf+9
The default, out-of-the-box DocumentCompare.
- ✓Compare the original document with the uploaded scan and confirm they are the same
- ✓Check a signed copy against what the system generated, by eye, then approve
- ✓Show two versions or revisions of a stored file next to each other
- ✓Any approval step where a human has to look at two files before deciding
- ✓A review dialog where one of the two files may be missing or may fail to load
- Each pane owns its request: one pane 404-ing leaves the other readable, with its own message, HTTP status and Retry — and retrying one pane re-requests only that pane
- A per-pane error BOUNDARY as well, so a pane that throws while rendering (a host node from a bad row) cannot blank the other document
- Reuses DocumentViewerSurface verbatim for the rendering, so PDFs, images, office types, print and download behave identically to the single-document viewer and cannot drift from it
- Phone answer: below md the panes become ONE pane at a time behind a radiogroup switch (arrow keys, one tab stop); above md they sit side by side. Both panes stay MOUNTED either way, so a switch never re-fetches a multi-megabyte scan and a pane that already failed keeps its error
- Source allow-list BEFORE anything is fetched: relative or http(s) only by default, so a javascript:, data: or blob: address stored in a row is refused and never requested — and the refused address is never echoed back into the page
- A cross-origin refusal is EXPLAINED: the pane names the site that declined instead of surfacing the browser bare Failed to fetch, and it is not blamed on CORS when the request did produce an HTTP status
- A pane with no file yet is a different screen from a pane whose address was refused, and both are different from a fetch that failed
- File names are rendered as text with control and direction-override characters stripped, so a scan cannot be named to read as a different file; a blank name falls back to a caption
- Layouts: auto (switch below md), columns (always side by side), rows (always stacked). Two panes is the designed case; three or more are laid out the same way with no extra code
- Object-URL hygiene inherited per pane: both blob URLs are revoked and both in-flight requests aborted when the dialog closes
- i18n: labels for the shell (DocumentCompareLabels) and viewerLabels for the viewers inside the panes, so one translation pass covers the whole dialog. English defaults, {origin} template
- ✕Comparing TEXT line by line (use DiffViewer)
- ✕Opening a single document (use DocumentViewer)
- ✕Browsing many files (use FileBrowser)
- ✕Editing or annotating an image (use ImageAnnotator)
- ✕Automatic matching of two documents: this component shows them, a human decides
Install
Ask your agent, or call the MCP tool directly. The resolved bundle is written into your project.
Usage
Tick variants in the left menu — each adds a tab with that variant’s real story source, the same code the preview above renders.
Props
openDialog visibility (controlled). DocumentCompareSurface does not take it
yesonOpenChangeOpen-state change callback. Closing unmounts both panes, which aborts both requests and revokes both blob URLs
yespanesThe documents: { id?, title?, badge?, description?, src?, srcDoc?, fileName, mimeType?, fetchInit? }. Two is the designed case. A duplicate or blank id is resolved rather than silently breaking the keys and the switch
yeslayoutauto (default): one pane at a time behind a switch below md, side by side above it. columns: always side by side, no switch. rows: always stacked, no switch
—viewControlled narrow-screen pane, by id. A view naming a pane that no longer exists still shows the first one
—defaultViewWhich pane a narrow screen opens on. Default: the first
—onViewChangeFires when the narrow-screen switch is used
—footerThe decision slot: the button the screen exists for (confirm same document). Sits at the start of the dialog footer, opposite Close
—titleDialog headline. Defaults to the title label
—descriptionLine under the headline. false removes it
—srcAllowedMay this document URL be fetched? Default isSafeDocumentSrc: relative or http(s) only. Widen it deliberately for blob: previews the host created itself
—pageOriginThe origin this page is served from, used only to explain a cross-origin refusal. Defaults to the browser own origin, read when a failure renders (never during the first render, so the server and the client agree)
—paneClassNameHeight of one pane. Default h-[60dvh], which fits a dialog; an embedded surface usually wants its own
—labelsOverride every built-in string of the shell (captions fallback, switch label, missing / blocked / cross-origin / crashed panels, retry, close) for i18n. English defaults exported as DEFAULT_DOCUMENT_COMPARE_LABELS
—viewerLabelsStrings of the viewer inside each pane (print, download, its own error headline, the loading name), so one translation pass covers the whole dialog
—