Files
"use client";
import { useDocumentState } from "@embedpdf/core/react";
import { LayoutGridIcon } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { PdfDocumentGrid } from "@/components/pdf-document-grid";
import { PdfMoreActionsMenu } from "@/components/pdf-more-actions-menu";
import { PdfPageNavigation } from "@/components/pdf-page-navigation";
import { PdfSearchPopover } from "@/components/pdf-search-panel";
import {
PdfToolbar,
PdfToolbarGroup,
PdfToolbarSeparator,
} from "@/components/pdf-toolbar";
import { PdfViewer, usePdfView } from "@/components/pdf-viewer";
import { PdfViewerContent } from "@/components/pdf-viewer-content";
import { PdfZoomControls } from "@/components/pdf-zoom-controls";
/**
* A document-library launcher assembled entirely from pdfcn's registry
* components, after PDFSlick's MultipleDocuments example. It has two modes on
* one engine:
* - **Library** — `<PdfDocumentGrid>` fills the frame: every open document is a
* navigable card, plus a card that opens more. Picking a card routes it into
* the shared view and flips to the reader.
* - **Reader** — a full-screen `<PdfViewerContent>` of the picked document with
* a toolbar. The toolbar's grid button returns to the library.
*
* Both modes read the *same* `<PdfViewer>`: the grid renders every document off
* the thumbnail plugin without a viewer per file, and the reader shows the
* focused view's active document — so switching modes never reloads anything.
*
* The root fills its parent, so give it a sized container (a viewport-height
* flex cell, or an explicit height).
*/
export function PdfLibraryApp({
className,
...props
}: Omit<React.ComponentProps<typeof PdfViewer>, "children">) {
const [mode, setMode] = React.useState<"library" | "reader">("library");
return (
<PdfViewer className={cn("h-full", className)} {...props}>
{mode === "library" ? (
<PdfDocumentGrid
className="min-h-0 flex-1"
onSelectDocument={() => setMode("reader")}
/>
) : (
<PdfLibraryReader onBackToLibrary={() => setMode("library")} />
)}
</PdfViewer>
);
}
/**
* The reader mode: a toolbar over the page canvas, scoped to the focused view's
* active document (the card the user just picked). The leading grid button is
* the way back to the library; the document's name sits beside it so it's clear
* which one is open.
*/
function PdfLibraryReader({
onBackToLibrary,
}: {
onBackToLibrary: () => void;
}) {
const { documentId } = usePdfView();
const documentName = useDocumentState(documentId)?.name;
return (
<div className="flex min-h-0 flex-1 flex-col">
{/*
`@container/toolbar` scopes the width queries to the toolbar itself, so
the exact-value controls (the zoom-level select, the page-number input)
drop below `@lg/toolbar` while the steppers, search, and back button
stay — the block reads the same embedded in a column as full-screen.
*/}
<PdfToolbar className="@container/toolbar">
<PdfToolbarGroup>
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="icon-sm"
aria-label="Back to library"
onClick={onBackToLibrary}
>
<LayoutGridIcon />
</Button>
}
/>
<TooltipContent>Back to library</TooltipContent>
</Tooltip>
</PdfToolbarGroup>
{documentName ? (
<span
className="text-muted-foreground min-w-0 truncate text-sm"
title={documentName}
>
{documentName}
</span>
) : null}
<div className="ml-auto flex items-center gap-1">
<PdfZoomControls className="**:data-[slot=pdf-zoom-controls-level]:@max-lg/toolbar:hidden" />
<PdfToolbarSeparator />
<PdfPageNavigation className="**:data-[slot=pdf-page-navigation-pages]:@max-lg/toolbar:hidden" />
<PdfSearchPopover />
<PdfToolbarSeparator />
<PdfMoreActionsMenu />
</div>
</PdfToolbar>
<PdfViewerContent />
</div>
);
}
Every open document is a card in a grid — navigate its pages in place, then pick one to open it in a full-screen reader and jump back to the library. It renders every document off one engine with embedpdf's thumbnail plugin, after PDFSlick's multiple-documents example.