@4admin/pdf-viewer
v0.2.9
Published
Drop-in, pdf.js-based replacement for the archived @react-pdf-viewer — virtualized, HeroUI-styled, built for the 4admin client.
Maintainers
Readme
@4admin/pdf-viewer
A standalone, pdf.js-based, drop-in replacement for the subset of
@react-pdf-viewer
that the 4admin client uses. The upstream library is archived; this package
re-implements its public API (the parts 4admin depends on) on top of a current,
maintained pdfjs-dist, so 4admin migrates by changing import paths only.
@react-pdf-viewer/<module> → @4admin/pdf-viewer/<module>Built with TypeScript + Vite library mode. Ships ESM + .d.ts + per-module CSS
at the same subpaths the original used.
Why
react-pdf-viewer is no longer maintained. Rather than fork the whole plugin
library or inline a viewer into 4admin, this is a small, self-contained package
that mirrors only the API surface 4admin actually couples to, engineered for a
no-touch swap. See docs/SPEC.md for the exact compatibility
contract and docs/ARCHITECTURE.md for how it's built.
Install
Private package. Publish to your registry, then:
pnpm add @4admin/pdf-viewerPeer dependencies (all already present in the 4admin client at these versions):
react/react-dom >=18, @heroui/react >=2.8.5 <3, @tabler/icons-react ^3,
framer-motion >=11. pdfjs-dist is bundled. The built-in chrome (find
popover, default toolbar, loader/error/password states) renders with HeroUI +
Tabler icons matching the 4admin design language; HeroUI's classes are
generated by the host's heroui() Tailwind plugin (4admin's Tailwind already
scans @heroui/theme — no config change needed).
Modules (subpath exports)
Each mirrors the matching @react-pdf-viewer/* package, with a stylesheet at
<module>/lib/styles/index.css:
core · default-layout · toolbar · search · highlight · zoom ·
rotate · page-navigation · scroll-mode
import { Viewer, Worker, SpecialZoomLevel, ScrollMode } from '@4admin/pdf-viewer/core';
import '@4admin/pdf-viewer/core/lib/styles/index.css';
import { defaultLayoutPlugin } from '@4admin/pdf-viewer/default-layout';
import { highlightPlugin, Trigger } from '@4admin/pdf-viewer/highlight';
// …etc — identical shapes to @react-pdf-viewer/*The PDF worker (no setup required)
The pdf.js API is bundled and pinned, and by default the matching worker is
loaded from the unpkg CDN (https://unpkg.com/pdfjs-dist@<version>/build/pdf.worker.min.mjs)
— the same pattern 4admin already used. The <Worker workerUrl="…"> wrapper is
honored for compatibility but its workerUrl is ignored (to prevent an
API/worker version mismatch).
To self-host the worker instead of using the CDN:
import { setWorkerSrc } from '@4admin/pdf-viewer/core';
setWorkerSrc('/pdf.worker.min.mjs'); // call once before the first <Viewer>getDocument always runs with isEvalSupported: false (defense-in-depth vs
CVE-2024-4367 — the reason 4admin set this in _app.js).
Next.js
The viewer is client-only (it touches window/canvas). 4admin's call sites
already render it inside client components, so no change is needed. Add the
package to transpilePackages so "use client" is honored:
// next.config.mjs
const nextConfig = { transpilePackages: ['@4admin/pdf-viewer'] };If you ever hit an SSR/hydration warning at a new call site, wrap it with
next/dynamic ({ ssr: false }).
Migrating 4admin
A single mechanical find-replace of import specifiers — no logic changes:
# from this repo, against the 4admin client (dry run first)
./scripts/migrate-4admin.sh /path/to/4admin/client
./scripts/migrate-4admin.sh /path/to/4admin/client --applyThen in 4admin/client/package.json: remove the @react-pdf-viewer/*
dependencies, add "@4admin/pdf-viewer", and reinstall. The existing
<Worker workerUrl="…"> JSX and jumpToPagePlugin.jsx (which uses
createStore) keep working unchanged.
Affected files (9): ReconciliationPdfView.jsx, SinglePagePdfViewer.jsx,
ModalSinglePagePdfView.jsx, jumpToPagePlugin.jsx, ReconciliationModal.jsx,
MatchingUpload.jsx, HITLModal.jsx, CreateAnalysisFromLoaDrawer.jsx,
pages/analysis/[runId].js.
API coverage
Implemented with real behavior (everything 4admin uses):
| Module | Real behavior |
|---|---|
| core | Viewer (fileUrl, theme, scrollMode, defaultScale, plugins, ref, onDocumentLoad→doc, onPageChange), Worker, createStore, enums, Plugin/PluginFunctions, PdfJs |
| default-layout | defaultLayoutPlugin({ renderToolbar, sidebarTabs }) — toolbar shell, empty-sidebar collapse |
| toolbar | Toolbar render-prop + ToolbarSlot (CurrentPageInput, NumberOfPages, ZoomIn/Out/Zoom, GoToNext/Prev, ShowSearchPopover, Download, …) |
| search | highlight(keywords, { matchCase }), clearHighlights, jumpToNext/PreviousMatch, onHighlightKeyword, find popover |
| highlight | renderHighlights + getCssProperties(area, rotation), Trigger, jumpToHighlightArea |
| zoom | zoomTo(number | PageFit | PageWidth | ActualSize), ZoomIn/Out |
| rotate | Rotate (Forward/Backward), whole-doc rotation |
| page-navigation | jumpToPage, next/prev, CurrentPageInput, NumberOfPages |
| scroll-mode | switchScrollMode(Page | Vertical) — state-preserving |
Provided as typed COMPAT stubs so incidental imports/destructures compile (not full implementations): thumbnail/bookmark/attachment sidebars, full-screen, print, selection mode, dual-page view modes, per-page rotation UI, link annotation layer.
Performance & resilience
The engine is built for large documents and live panel resizing (see
docs/CONFIDENCE.md for the proof matrix):
- Window virtualization with teardown — only pages near the viewport hold a canvas/text layer (≤ ~10 at any time on a 300-page doc); placeholder boxes preserve exact scroll geometry.
- Progressive page sizing — first paint after page 1's size; the rest
stream in, with pages up to
initialPageresolved first. - Two-tier zoom — layout tracks container resizes every frame (CSS-stretch
of existing bitmaps), a debounced commit re-renders crisp, double-buffered
(no blank flashes). Manual zoom suppresses auto-refit (4admin's
hasCustomZoomparity). - Search at scale — per-document text-span cache, generation-based cancellation of superseded scans, event-loop yields; ~400 ms across 300 pages.
- Occurrence semantics match 4admin's backend counting exactly (start-position dedup across value patterns), proven against a scanned-style fixture built with the api's Azure-DI overlay formulas.
Development
pnpm install
pnpm gen:sample && pnpm gen:fixtures # demo + proof PDFs (300p, scanned-style)
pnpm dev # demo (faithful port of 4admin's ReconciliationPdfView)
# ?doc=large → 300-page fixture, ?doc=scanned → OCR fixture
pnpm typecheck
pnpm build # dist/ (ESM + .d.ts + per-module CSS)
pnpm test # pure-logic + engine parity proofs (node)
pnpm test:browser # virtualization/resize/search/highlight proofs (needs demo
# preview on :4317 + Chrome with --remote-debugging-port=9222)Status / known gaps
- Horizontal / Wrapped scroll modes and dual-page view modes are best-effort.
- Link annotation layer is not rendered (4admin doesn't depend on it).
- Intrinsic
/Rotatesource pages: overlay placement replicates the original library's (and 4admin's own) behavior, which ignores intrinsic rotation.
