Files
"use client";
import { useDocumentState } from "@embedpdf/core/react";
import { ThumbImg } from "@embedpdf/plugin-thumbnail/react";
import * as React from "react";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { PdfDocumentGrid } from "@/components/pdf-document-grid";
import { PdfDocumentInfo } from "@/components/pdf-document-info";
import { PdfViewer, usePdfView } from "@/components/pdf-viewer";
/**
* A document library with an information sidebar, assembled from pdfcn's
* registry components after PDFSlick's MultipleDocuments example. The grid is
* the shelf — every open document a navigable card, plus a card that opens
* more; the sidebar inspects whichever card you pick.
*
* Both halves read the *same* `<PdfViewer>` off one engine: `<PdfDocumentGrid>`
* previews every document off the thumbnail plugin, and picking a card routes
* it into the shared view — which is exactly what `<PdfDocumentInfo>` reads, so
* the sidebar tracks the selection without any wiring of its own.
*
* The root fills its parent, so give it a sized container (a viewport-height
* flex cell, or an explicit height).
*/
export function PdfLibraryBrowser({
className,
...props
}: Omit<React.ComponentProps<typeof PdfViewer>, "children">) {
return (
<PdfViewer className={cn("h-full", className)} {...props}>
<ResizablePanelGroup>
<ResizablePanel className="min-w-0">
<PdfDocumentGrid className="h-full" />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={320}
minSize={260}
maxSize={480}
className="min-w-0"
>
<PdfLibraryInfoSidebar />
</ResizablePanel>
</ResizablePanelGroup>
</PdfViewer>
);
}
/**
* The inspector: a preview of the selected document over its PDF properties.
* Everything here reads the focused view's active document (the picked card),
* so it re-populates itself whenever the selection in the grid changes.
*/
function PdfLibraryInfoSidebar() {
const { documentId } = usePdfView();
const documentState = useDocumentState(documentId);
const loaded = documentState?.status === "loaded";
return (
<ScrollArea className="bg-background h-full">
<div className="flex flex-col gap-4 p-4">
<h2 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
Details
</h2>
<PdfLibraryInfoPreview documentId={documentId} loaded={loaded} />
{/* The file name isn't captioned under the preview: `<PdfDocumentInfo>`
already lists it under **File**, and it says the same thing about an
empty selection. Two lines for one fact is what a details panel is
supposed to save the reader from. */}
<PdfDocumentInfo documentId={documentId} />
</div>
</ScrollArea>
);
}
// US Letter (8.5×11) as a width/height ratio — the frame before the document
// reports its own first-page size.
const FALLBACK_ASPECT_RATIO = 8.5 / 11;
/**
* A single-page preview of the selected document, sized to its own first page.
* It shares the grid's thumbnail plugin, so the bitmap is already rendered.
*/
function PdfLibraryInfoPreview({
documentId,
loaded,
}: {
documentId: string | null;
loaded: boolean;
}) {
const documentState = useDocumentState(documentId);
const firstPage = documentState?.document?.pages[0];
const aspectRatio = React.useMemo(() => {
if (!firstPage) return FALLBACK_ASPECT_RATIO;
const { width, height } = firstPage.size;
const quarterTurns =
(firstPage.rotation + (documentState?.rotation ?? 0)) % 2;
const [w, h] = quarterTurns === 0 ? [width, height] : [height, width];
return w > 0 && h > 0 ? w / h : FALLBACK_ASPECT_RATIO;
}, [firstPage, documentState?.rotation]);
if (!documentId || !loaded) {
return (
<div
className="border-border text-muted-foreground flex items-center justify-center rounded-lg border border-dashed p-4 text-center text-xs"
style={{ aspectRatio: FALLBACK_ASPECT_RATIO }}
>
Pick a document to inspect it
</div>
);
}
return (
<div
// A page preview, so it takes the page colour rather than a theme token.
// See `PDF_PAGE_COLOR` in `<PdfViewerContent>`.
className="ring-border relative mx-auto w-2/3 overflow-hidden rounded-lg bg-(--pdf-page,#fff) ring-1"
style={{ aspectRatio }}
>
<ThumbImg
documentId={documentId}
meta={{
pageIndex: 0,
width: 0,
height: 0,
wrapperHeight: 0,
top: 0,
labelHeight: 0,
}}
className="relative size-full object-contain"
/>
</div>
);
}
A grid of every open document beside a sidebar that inspects whichever card you pick — its preview and PDF properties, read straight from the engine. Every document renders off one engine with embedpdf's thumbnail plugin, after PDFSlick's multiple-documents example.