Document
A reading surface for PDFs, built on @phauna/ds/document over pdf.js. Because pdf.js is browser-only, every viewer here is mounted through next/dynamic({ ssr: false }) from a 'use client' island with a self-hosted, version-matched worker — so nothing pdf.js touches ever runs during SSR or the build, and a load failure degrades to a calm 'preview unavailable' state instead of throwing.
- DocumentViewer
- The full reading surface: a toolbar (page back/forward, zoom out/in, in-document search) over a scrollable page canvas. Takes src (PDF URL), optional workerSrc, initialPage and onPageChange, plus an optional rich-PDF manifest for a text layer / critical facts.
- PdfThumbnail
- A single rendered page as a poster frame — src, page (default 1), width and workerSrc. No toolbar; ideal for cards, lists and pickers.
- SSR safety
- next/dynamic({ ssr: false }) keeps the pdf.js chunk out of the server render and the build's SSR pass; the underlying module also defers `import('pdfjs-dist')` into effects, so window/canvas are never touched on the server.
- Worker
- pdf.js needs a worker script. We self-host /pdf.worker.min.mjs (copied from pdfjs-dist/build, version-matched) and pass it as workerSrc, which the DS assigns to pdfjs GlobalWorkerOptions.workerSrc. Omitting workerSrc falls back to a CDN URL pinned to the resolved pdfjs version.
- Degrade, never throw
- DocumentViewer renders its own EmptyState on a PDF load error, and each mount is wrapped in an error boundary that catches a failed chunk load and shows a 'preview unavailable' EmptyState. The route stays up no matter what.
DocumentViewer
The full PDF reading surface on a tiny bundled sample (/sample.pdf), rendered entirely client-side with the self-hosted worker. Use the toolbar to page through, zoom, and search. While the chunk and first page load you see a calm skeleton — that fallback is all the server ever renders.
PdfThumbnail
A poster-frame render of a single page at a fixed width — no toolbar, no scroll. Drop it into cards, attachment lists or a page picker. Same self-hosted worker, same SSR-safe dynamic mount.
Graceful degradation
Point the viewer at a source it cannot read (here an existing asset that isn't a valid PDF) and it does NOT throw: the viewer renders its own in-component error state, and an outer error boundary would catch a failed chunk load and swap in a 'preview unavailable' EmptyState. The surrounding page never crashes.