0
Files
pdf-library-browser.tsx
"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.