Skip to content

Commit 61fdf01

Browse files
committed
feat(plugin-thumbnail): integrate thumbnail plugin and add thumbnail sidebar UI
1 parent a9f03fb commit 61fdf01

File tree

8 files changed

+215
-106
lines changed

8 files changed

+215
-106
lines changed

components/pdf-container/pdf-container.tsx

Lines changed: 118 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { RenderLayer, RenderPluginPackage } from "@embedpdf/plugin-render/react"
1313
import { Scroller, ScrollPluginPackage, ScrollStrategy } from "@embedpdf/plugin-scroll/react"
1414
import { SearchLayer, SearchPluginPackage } from "@embedpdf/plugin-search/react"
1515
import { SelectionLayer, SelectionPluginPackage } from "./plugin-selection-2"
16+
import { ThumbnailPluginPackage } from "@embedpdf/plugin-thumbnail/react"
1617
import { TilingLayer, TilingPluginPackage } from "@embedpdf/plugin-tiling/react"
1718
import { Viewport, ViewportPluginPackage } from "@embedpdf/plugin-viewport/react"
1819
import { PinchWrapper, ZoomMode, ZoomPluginPackage } from "@embedpdf/plugin-zoom/react"
@@ -21,6 +22,8 @@ import AnnotationStoreSync from "../annotation-store/annotation-store-sync"
2122
import { Spinner } from "../shadcn-ui/spinner"
2223
import { AnnotationLayer, AnnotationPluginPackage } from "./plugin-annotation-2"
2324
import Toolbar from "./toolbar"
25+
import ThumbnailSidebar from "./thumbnail-sidebar"
26+
import { Sidebar, SidebarContent, SidebarProvider } from "@/components/shadcn-ui/sidebar"
2427

2528
const logger = new ConsoleLogger()
2629

@@ -46,106 +49,121 @@ export default function PDFContainer({ url }: PDFContainerProps) {
4649
}
4750

4851
return (
49-
<div className="flex h-screen flex-1 flex-col overflow-hidden" ref={containerRef}>
50-
<div className="flex flex-1 overflow-hidden" data-testid="embedpdf">
51-
<EmbedPDF
52-
logger={logger}
53-
engine={engine}
54-
plugins={[
55-
// register Loader first
56-
createPluginRegistration(LoaderPluginPackage, {
57-
loadingOptions: {
58-
type: "url",
59-
pdfFile: {
60-
id: "1",
61-
url: url,
62-
},
63-
options: {
64-
mode: "full-fetch",
65-
},
66-
},
67-
}),
68-
createPluginRegistration(ViewportPluginPackage, {
69-
viewportGap: 5,
70-
}),
71-
createPluginRegistration(ScrollPluginPackage, {
72-
strategy: ScrollStrategy.Vertical,
73-
}),
74-
createPluginRegistration(RenderPluginPackage),
75-
createPluginRegistration(InteractionManagerPluginPackage),
76-
createPluginRegistration(TilingPluginPackage, {
77-
tileSize: 768,
78-
overlapPx: 2.5,
79-
extraRings: 0,
80-
}),
81-
createPluginRegistration(SelectionPluginPackage),
82-
// register Annotation after InteractionManager, Seletion
83-
createPluginRegistration(AnnotationPluginPackage),
84-
// register Export after Annotation
85-
createPluginRegistration(ExportPluginPackage),
86-
// register Zoom after InteractionManager, Viewport, Scroll
87-
createPluginRegistration(ZoomPluginPackage, {
88-
defaultZoomLevel: ZoomMode.Automatic,
89-
}),
90-
createPluginRegistration(SearchPluginPackage),
91-
]}
92-
>
93-
{({ pluginsReady }) => {
94-
return (
95-
<GlobalPointerProvider>
96-
<AnnotationStoreSync />
97-
<Toolbar data-testid="annotation-toolbar" />
98-
<Viewport className="h-full w-full flex-1 overflow-auto bg-gray-100 select-none">
99-
{!pluginsReady && (
100-
<div className="flex h-full w-full items-center justify-center">
101-
<Spinner data-testid="spinner1" />
102-
</div>
103-
)}
104-
{pluginsReady && (
105-
<PinchWrapper>
106-
<Scroller
107-
renderPage={({ pageIndex, scale, width, height }) => (
108-
<PagePointerProvider
109-
pageIndex={pageIndex}
110-
scale={scale}
111-
pageWidth={width}
112-
pageHeight={height}
113-
rotation={0}
114-
>
115-
{/* RednerLayer must go first */}
116-
<RenderLayer pageIndex={pageIndex} className="pointer-events-none" />
117-
<TilingLayer
118-
pageIndex={pageIndex}
119-
scale={scale}
120-
className="pointer-events-none"
121-
/>
122-
<AnnotationLayer
123-
pageIndex={pageIndex}
124-
scale={scale}
125-
pageWidth={width}
126-
pageHeight={height}
127-
rotation={0}
128-
data-testid="annotation-layer"
129-
/>
130-
<SearchLayer
131-
pageIndex={pageIndex}
132-
scale={scale}
133-
highlightColor={"#FFFF00"}
134-
activeHighlightColor={"#FFFF00"}
135-
/>
136-
{/* SelectionLayer must go last */}
137-
<SelectionLayer pageIndex={pageIndex} scale={scale} />
138-
</PagePointerProvider>
139-
)}
140-
/>
141-
</PinchWrapper>
142-
)}
143-
</Viewport>
144-
</GlobalPointerProvider>
145-
)
146-
}}
147-
</EmbedPDF>
52+
<SidebarProvider>
53+
<div className="flex h-screen w-full" ref={containerRef}>
54+
<div className="flex flex-1 flex-col overflow-hidden">
55+
<div className="flex flex-1 overflow-hidden" data-testid="embedpdf">
56+
<EmbedPDF
57+
logger={logger}
58+
engine={engine}
59+
plugins={[
60+
// register Loader first
61+
createPluginRegistration(LoaderPluginPackage, {
62+
loadingOptions: {
63+
type: "url",
64+
pdfFile: {
65+
id: "1",
66+
url: url,
67+
},
68+
options: {
69+
mode: "full-fetch",
70+
},
71+
},
72+
}),
73+
createPluginRegistration(ViewportPluginPackage, {
74+
viewportGap: 5,
75+
}),
76+
createPluginRegistration(ScrollPluginPackage, {
77+
strategy: ScrollStrategy.Vertical,
78+
}),
79+
createPluginRegistration(RenderPluginPackage),
80+
createPluginRegistration(InteractionManagerPluginPackage),
81+
createPluginRegistration(TilingPluginPackage, {
82+
tileSize: 768,
83+
overlapPx: 2.5,
84+
extraRings: 0,
85+
}),
86+
createPluginRegistration(SelectionPluginPackage),
87+
// register Annotation after InteractionManager, Seletion
88+
createPluginRegistration(AnnotationPluginPackage),
89+
// register Export after Annotation
90+
createPluginRegistration(ExportPluginPackage),
91+
// register Zoom after InteractionManager, Viewport, Scroll
92+
createPluginRegistration(ZoomPluginPackage, {
93+
defaultZoomLevel: ZoomMode.Automatic,
94+
}),
95+
createPluginRegistration(SearchPluginPackage),
96+
createPluginRegistration(ThumbnailPluginPackage, {
97+
width: 120,
98+
}),
99+
]}
100+
>
101+
{({ pluginsReady }) => {
102+
return (
103+
<GlobalPointerProvider>
104+
<AnnotationStoreSync />
105+
<Toolbar data-testid="annotation-toolbar" />
106+
<Viewport className="h-full w-full flex-1 overflow-auto bg-gray-100 select-none">
107+
{!pluginsReady && (
108+
<div className="flex h-full w-full items-center justify-center">
109+
<Spinner data-testid="spinner1" />
110+
</div>
111+
)}
112+
{pluginsReady && (
113+
<PinchWrapper>
114+
<Scroller
115+
renderPage={({ pageIndex, scale, width, height }) => (
116+
<PagePointerProvider
117+
pageIndex={pageIndex}
118+
scale={scale}
119+
pageWidth={width}
120+
pageHeight={height}
121+
rotation={0}
122+
>
123+
{/* RednerLayer must go first */}
124+
<RenderLayer
125+
pageIndex={pageIndex}
126+
className="pointer-events-none"
127+
/>
128+
<TilingLayer
129+
pageIndex={pageIndex}
130+
scale={scale}
131+
className="pointer-events-none"
132+
/>
133+
<AnnotationLayer
134+
pageIndex={pageIndex}
135+
scale={scale}
136+
pageWidth={width}
137+
pageHeight={height}
138+
rotation={0}
139+
data-testid="annotation-layer"
140+
/>
141+
<SearchLayer
142+
pageIndex={pageIndex}
143+
scale={scale}
144+
highlightColor={"#FFFF00"}
145+
activeHighlightColor={"#FFFF00"}
146+
/>
147+
{/* SelectionLayer must go last */}
148+
<SelectionLayer pageIndex={pageIndex} scale={scale} />
149+
</PagePointerProvider>
150+
)}
151+
/>
152+
</PinchWrapper>
153+
)}
154+
</Viewport>
155+
</GlobalPointerProvider>
156+
)
157+
}}
158+
</EmbedPDF>
159+
</div>
160+
</div>
161+
<Sidebar side="right" variant="floating" collapsible="none">
162+
<SidebarContent className="p-0">
163+
<ThumbnailSidebar />
164+
</SidebarContent>
165+
</Sidebar>
148166
</div>
149-
</div>
167+
</SidebarProvider>
150168
)
151169
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export * from './selection-layer';
2-
export * from './copy-to-clipboard';
1+
export * from "./selection-layer"
2+
export * from "./copy-to-clipboard"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export * from './use-selection';
1+
export * from "./use-selection"
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"use client"
2+
3+
import { useScroll } from "@embedpdf/plugin-scroll/react"
4+
import { ThumbImg, ThumbnailsPane } from "@embedpdf/plugin-thumbnail/react"
5+
import { ScrollArea } from "@/components/shadcn-ui/scroll-area"
6+
7+
const ThumbnailSidebar = () => {
8+
const { state, provides } = useScroll()
9+
10+
return (
11+
<ScrollArea className="h-full w-[120px] border-l border-gray-200 bg-white">
12+
<div className="relative">
13+
<ThumbnailsPane>
14+
{(m) => {
15+
const isActive = state.currentPage === m.pageIndex + 1
16+
return (
17+
<div
18+
key={m.pageIndex}
19+
style={{
20+
position: "absolute",
21+
top: m.top,
22+
height: m.wrapperHeight,
23+
}}
24+
onClick={() => provides?.scrollToPage({ pageNumber: m.pageIndex + 1 })}
25+
className="w-full cursor-pointer transition-colors hover:bg-gray-50"
26+
>
27+
<div
28+
style={{
29+
border: `2px solid ${isActive ? "#3b82f6" : "#d1d5db"}`,
30+
width: m.width,
31+
height: m.height,
32+
}}
33+
className="mx-auto"
34+
>
35+
<ThumbImg meta={m} />
36+
</div>
37+
<span
38+
style={{ height: m.labelHeight }}
39+
className="flex items-center justify-center text-xs font-medium"
40+
>
41+
{m.pageIndex + 1}
42+
</span>
43+
</div>
44+
)
45+
}}
46+
</ThumbnailsPane>
47+
</div>
48+
</ScrollArea>
49+
)
50+
}
51+
52+
export default ThumbnailSidebar

components/pdf-container/toolbar.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,24 @@ import { useZoom } from "@embedpdf/plugin-zoom/react"
44
import {
55
Download,
66
Highlighter,
7+
Images,
78
Redo2,
89
Trash2,
910
Underline,
1011
Undo2,
1112
ZoomIn,
1213
ZoomOut,
1314
} from "lucide-react"
15+
import { useSidebar } from "@/components/shadcn-ui/sidebar"
1416

15-
const Toolbar = () => {
17+
interface ToolbarProps {
18+
"data-testid"?: string
19+
}
20+
21+
const Toolbar = ({ "data-testid": dataTestId }: ToolbarProps) => {
1622
const { provides: exportApi } = useExportCapability()
1723
const { provides: zoom } = useZoom()
24+
const { toggleSidebar } = useSidebar()
1825

1926
const {
2027
capability: annotationApi,
@@ -36,7 +43,10 @@ const Toolbar = () => {
3643
]
3744

3845
return (
39-
<div className="mt-4 mb-4 flex flex-wrap items-center gap-2 rounded-lg border border-gray-200 bg-white p-2 shadow-sm">
46+
<div
47+
className="mt-4 mb-4 flex flex-wrap items-center gap-2 rounded-lg border border-gray-200 bg-white p-2 shadow-sm"
48+
data-testid={dataTestId}
49+
>
4050
{tools.map((tool) => (
4151
<button
4252
key={tool.id}
@@ -54,6 +64,16 @@ const Toolbar = () => {
5464

5565
<div className="h-6 w-px bg-gray-200" />
5666

67+
<button
68+
onClick={() => toggleSidebar()}
69+
className="rounded-md bg-gray-100 px-3 py-1 text-sm font-medium transition-colors hover:bg-gray-200"
70+
title="Toggle Thumbnails Sidebar"
71+
>
72+
<Images size={18} />
73+
</button>
74+
75+
<div className="h-6 w-px bg-gray-200" />
76+
5777
<button
5878
onClick={() => zoom?.zoomOut()}
5979
disabled={!zoom}

package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@embedpdf/plugin-scroll": "1.3.16",
3535
"@embedpdf/plugin-search": "1.3.16",
3636
"@embedpdf/plugin-selection": "1.3.16",
37+
"@embedpdf/plugin-thumbnail": "1.3.16",
3738
"@embedpdf/plugin-tiling": "1.3.16",
3839
"@embedpdf/plugin-viewport": "1.3.16",
3940
"@embedpdf/plugin-zoom": "1.3.16",

0 commit comments

Comments
 (0)