feat(deps): pdfjs-dist + react-pdf for consistent in-app PDF preview

Replaces the `<iframe src={presignedUrl}>` preview path which
delegated rendering to the browser's built-in PDF viewer. The iframe
worked on desktop but failed on mobile (older Android Chrome
refuses inline PDFs; iOS Safari opens a new tab).

`<PdfViewer>` renders via pdfjs-dist + react-pdf so the experience
is identical across all browsers + form factors. Adds page nav,
zoom controls, and per-page accessibility labels.

Lazy-loaded via next/dynamic with ssr:false — pdfjs is ~150kb gzip,
no route ships it unless a PDF is actually previewed.

pdfjs worker + CMaps + fonts loaded from unpkg CDN pinned to the
matched pdfjs-dist version (first-load cost paid once per user, no
bundle-size impact on routes that never preview a PDF).

Verified: tsc clean, vitest 1315/1315, next build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 22:56:42 +02:00
parent 75920a2540
commit d0a3a054b6
4 changed files with 366 additions and 3 deletions

View File

@@ -1,11 +1,23 @@
'use client';
import { useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import { ExternalLink } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { apiFetch } from '@/lib/api/client';
// pdfjs-dist is ~150kb gzip — lazy-load so routes that never preview
// PDFs don't ship it. ssr:false because the worker setup needs window.
const PdfViewer = dynamic(() => import('./pdf-viewer').then((m) => ({ default: m.PdfViewer })), {
ssr: false,
loading: () => (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
Loading PDF viewer
</div>
),
});
interface FilePreviewDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
@@ -94,7 +106,7 @@ export function FilePreviewDialog({
)}
{!loading && !error && previewUrl && isPdf && (
<iframe src={previewUrl} title={fileName ?? 'PDF Preview'} className="h-full w-full" />
<PdfViewer url={previewUrl} fileName={fileName} />
)}
</div>
</DialogContent>