0
Files
app/compare/page.tsx
"use client";

import dynamic from "next/dynamic";

// embedpdf's plugin packages and the PDFium worker are browser-only, so the
// whole app is loaded client-side behind an `ssr: false` boundary.
const PdfCompareApp = dynamic(
  () =>
    import("@/components/pdf-compare-app").then(
      (mod) => mod.PdfCompareApp,
    ),
  {
    ssr: false,
    loading: () => <div className="bg-muted h-full w-full" />,
  },
);

export default function Page() {
  return (
    // The panes fill their container and scroll inside themselves, so pin an
    // explicit height here rather than letting the document grow the page.
    <div className="h-svh w-full overflow-hidden">
      {/* Two revisions of the same eight pages, a handful of sentences apart —
          both generated by `scripts/build-sample-pdf.ts` from one set of page
          specs, so the panes line up and the edits are the only difference to
          spot. */}
      <PdfCompareApp
        documents={[
          { url: "/sample.pdf", name: "sample.pdf" },
          { url: "/sample-revised.pdf", name: "sample-revised.pdf" },
        ]}
      />
    </div>
  );
}

Two revisions of the same document, one per pane, scrolling together — the pane under the pointer leads and the others follow it to the same page and the same place on it, whatever their zoom or width. embedpdf's view-manager has no notion of one view following another, so the block wires it up itself over the public scroll and viewport capabilities — about forty lines, and a worked example of where that seam is.