Skip to content

Commit 9e60a41

Browse files
committed
Add customizing UI to EmbedPDF viewer
1 parent 0b8ddf2 commit 9e60a41

5 files changed

Lines changed: 635 additions & 13 deletions

File tree

viewers/snippet/src/config/theme.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,9 @@ export interface ThemeColors {
119119
}
120120

121121
/**
122-
* A complete theme definition
122+
* A complete theme definition (internal use)
123123
*/
124124
export interface Theme {
125-
/** Theme identifier */
126-
name: string;
127125
/** Color tokens */
128126
colors: ThemeColors;
129127
}
@@ -165,7 +163,6 @@ export interface ThemeConfig {
165163
// ─────────────────────────────────────────────────────────
166164

167165
export const lightTheme: Theme = {
168-
name: 'light',
169166
colors: {
170167
background: {
171168
app: '#f3f4f6', // gray-100
@@ -228,7 +225,6 @@ export const lightTheme: Theme = {
228225
// ─────────────────────────────────────────────────────────
229226

230227
export const darkTheme: Theme = {
231-
name: 'dark',
232228
colors: {
233229
background: {
234230
app: '#111827', // gray-900
@@ -320,13 +316,8 @@ function deepMerge<T extends Record<string, any>>(target: T, source: DeepPartial
320316
/**
321317
* Creates a custom theme by extending a base theme with color overrides
322318
*/
323-
export function createTheme(
324-
base: Theme,
325-
overrides: DeepPartial<ThemeColors>,
326-
name?: string,
327-
): Theme {
319+
export function createTheme(base: Theme, overrides: DeepPartial<ThemeColors>): Theme {
328320
return {
329-
name: name || `${base.name}-custom`,
330321
colors: deepMerge(base.colors, overrides),
331322
};
332323
}

viewers/snippet/src/embedpdf.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,72 @@ export { SearchPlugin, type SearchPluginConfig } from '@embedpdf/plugin-search/p
2929
export { SelectionPlugin, type SelectionPluginConfig } from '@embedpdf/plugin-selection/preact';
3030
export { CapturePlugin, type CapturePluginConfig } from '@embedpdf/plugin-capture/preact';
3131
export { RedactionPlugin, type RedactionPluginConfig } from '@embedpdf/plugin-redaction/preact';
32-
export { UIPlugin, type UIPluginConfig } from '@embedpdf/plugin-ui/preact';
32+
export { UIPlugin, type UIPluginConfig, type UICapability } from '@embedpdf/plugin-ui/preact';
33+
34+
// UI Schema Types - for customizing toolbars, menus, sidebars, etc.
35+
export type {
36+
// Top-level schema
37+
UISchema,
38+
39+
// Toolbar types
40+
ToolbarSchema,
41+
ToolbarPosition,
42+
ToolbarItem,
43+
CommandButtonItem,
44+
GroupItem,
45+
DividerItem,
46+
SpacerItem,
47+
TabGroupItem,
48+
TabItem,
49+
CustomComponentItem,
50+
51+
// Menu types
52+
MenuSchema,
53+
MenuItem,
54+
MenuCommandItem,
55+
MenuDividerItem,
56+
MenuSectionItem,
57+
MenuSubmenuItem,
58+
MenuCustomItem,
59+
60+
// Sidebar types
61+
SidebarSchema,
62+
SidebarPosition,
63+
PanelContent,
64+
TabsPanelContent,
65+
ComponentPanelContent,
66+
PanelTab,
67+
68+
// Modal types
69+
ModalSchema,
70+
71+
// Overlay types
72+
OverlaySchema,
73+
OverlayPosition,
74+
OverlayAnchor,
75+
76+
// Selection menu types
77+
SelectionMenuSchema,
78+
SelectionMenuItem,
79+
SelectionMenuCommandItem,
80+
SelectionMenuDividerItem,
81+
SelectionMenuGroupItem,
82+
83+
// Responsive types
84+
ResponsiveRules,
85+
BreakpointRule,
86+
87+
// Utility types
88+
VisibilityDependency,
89+
} from '@embedpdf/plugin-ui/preact';
3390
export { I18nPlugin, type I18nPluginConfig } from '@embedpdf/plugin-i18n/preact';
34-
export { CommandsPlugin, type CommandsPluginConfig } from '@embedpdf/plugin-commands/preact';
91+
export {
92+
CommandsPlugin,
93+
type CommandsPluginConfig,
94+
type Command,
95+
type ResolvedCommand,
96+
type CommandsCapability,
97+
} from '@embedpdf/plugin-commands/preact';
3598
export {
3699
DocumentManagerPlugin,
37100
type DocumentManagerPluginConfig,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export default {
22
introduction: 'Introduction',
33
theme: 'Theme',
4+
'customizing-ui': 'Customizing the UI',
45
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
'use client'
2+
import {
3+
CommandsPlugin,
4+
PDFViewer,
5+
PDFViewerRef,
6+
UIPlugin,
7+
ToolbarItem,
8+
GroupItem,
9+
MenuItem,
10+
} from '@embedpdf/react-pdf-viewer'
11+
import { useRef, useState, useEffect, useCallback } from 'react'
12+
13+
interface UICustomizationExampleProps {
14+
themePreference?: 'light' | 'dark'
15+
}
16+
17+
// Type guard to check if an item is a GroupItem
18+
function isGroupItem(item: ToolbarItem): item is GroupItem {
19+
return item.type === 'group'
20+
}
21+
22+
export default function UICustomizationExample({
23+
themePreference = 'light',
24+
}: UICustomizationExampleProps) {
25+
const viewerRef = useRef<PDFViewerRef>(null)
26+
const [isReady, setIsReady] = useState(false)
27+
const [lastAction, setLastAction] = useState<string | null>(null)
28+
29+
// Update theme when preference changes
30+
useEffect(() => {
31+
viewerRef.current?.container?.setTheme({ preference: themePreference })
32+
}, [themePreference])
33+
34+
// Setup custom commands and UI when viewer is ready
35+
const handleReady = useCallback(async () => {
36+
const container = viewerRef.current?.container
37+
if (!container) return
38+
39+
const registry = await container.registry
40+
41+
const commands = registry.getPlugin<CommandsPlugin>('commands')?.provides()
42+
const ui = registry.getPlugin<UIPlugin>('ui')?.provides()
43+
44+
if (!commands || !ui) return
45+
46+
// ─────────────────────────────────────────────────────────
47+
// 1. Register custom icons (stroke-based Tabler icons)
48+
// ─────────────────────────────────────────────────────────
49+
container.registerIcons({
50+
customSmiley: {
51+
viewBox: '0 0 24 24',
52+
paths: [
53+
{
54+
d: 'M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0',
55+
stroke: 'currentColor',
56+
fill: 'none',
57+
},
58+
{ d: 'M9 10l.01 0', stroke: 'currentColor', fill: 'none' },
59+
{ d: 'M15 10l.01 0', stroke: 'currentColor', fill: 'none' },
60+
{
61+
d: 'M9.5 15a3.5 3.5 0 0 0 5 0',
62+
stroke: 'currentColor',
63+
fill: 'none',
64+
},
65+
],
66+
},
67+
customStar: {
68+
viewBox: '0 0 24 24',
69+
paths: [
70+
{
71+
d: 'M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z',
72+
stroke: 'currentColor',
73+
fill: 'none',
74+
},
75+
],
76+
},
77+
})
78+
79+
// ─────────────────────────────────────────────────────────
80+
// 2. Register custom commands
81+
// ─────────────────────────────────────────────────────────
82+
commands.registerCommand({
83+
id: 'custom.smiley',
84+
label: 'Say Hello',
85+
icon: 'customSmiley',
86+
action: () => {
87+
setLastAction('Hello! 👋')
88+
setTimeout(() => setLastAction(null), 2000)
89+
},
90+
})
91+
92+
commands.registerCommand({
93+
id: 'custom.star',
94+
label: 'Add to Favorites',
95+
icon: 'customStar',
96+
action: () => {
97+
setLastAction('Added to favorites! ⭐')
98+
setTimeout(() => setLastAction(null), 2000)
99+
},
100+
})
101+
102+
// ─────────────────────────────────────────────────────────
103+
// 3. Modify the UI: Replace comment button with smiley
104+
// ─────────────────────────────────────────────────────────
105+
const currentSchema = ui.getSchema()
106+
const mainToolbar = currentSchema.toolbars['main-toolbar']
107+
108+
if (mainToolbar) {
109+
// Clone items with proper typing using structuredClone
110+
const items: ToolbarItem[] = structuredClone(mainToolbar.items)
111+
112+
// Find the right-group using type guard
113+
const rightGroup = items.find(
114+
(item): item is GroupItem =>
115+
isGroupItem(item) && item.id === 'right-group',
116+
)
117+
118+
if (rightGroup) {
119+
// Find and replace the comment button with our smiley button
120+
const commentIndex = rightGroup.items.findIndex(
121+
(item) => item.id === 'comment-button',
122+
)
123+
124+
if (commentIndex !== -1) {
125+
rightGroup.items[commentIndex] = {
126+
type: 'command-button',
127+
id: 'smiley-button',
128+
commandId: 'custom.smiley',
129+
variant: 'icon',
130+
}
131+
}
132+
}
133+
134+
ui.mergeSchema({
135+
toolbars: {
136+
'main-toolbar': {
137+
...mainToolbar,
138+
items,
139+
},
140+
},
141+
})
142+
}
143+
144+
// ─────────────────────────────────────────────────────────
145+
// 4. Add star command to the document menu
146+
// ─────────────────────────────────────────────────────────
147+
const documentMenu = currentSchema.menus['document-menu']
148+
149+
if (documentMenu) {
150+
const menuItems: MenuItem[] = [
151+
...documentMenu.items,
152+
{ type: 'divider', id: 'custom-divider' },
153+
{ type: 'command', id: 'star-menu-item', commandId: 'custom.star' },
154+
]
155+
156+
ui.mergeSchema({
157+
menus: {
158+
'document-menu': {
159+
...documentMenu,
160+
items: menuItems,
161+
},
162+
},
163+
})
164+
}
165+
166+
setIsReady(true)
167+
}, [])
168+
169+
return (
170+
<div className="flex flex-col gap-4">
171+
{/* Status indicator */}
172+
<div className="flex flex-wrap items-center gap-4 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800">
173+
<div className="flex items-center gap-2">
174+
<span
175+
className={`inline-block h-2 w-2 rounded-full ${isReady ? 'bg-green-500' : 'bg-yellow-500'}`}
176+
/>
177+
<span className="text-sm text-gray-600 dark:text-gray-300">
178+
{isReady ? 'UI customized!' : 'Loading...'}
179+
</span>
180+
</div>
181+
182+
{lastAction && (
183+
<div className="animate-pulse rounded bg-green-100 px-3 py-1 text-sm font-medium text-green-800 dark:bg-green-900 dark:text-green-200">
184+
{lastAction}
185+
</div>
186+
)}
187+
188+
{isReady && (
189+
<div className="ml-auto text-xs text-gray-400">
190+
😊 replaced comment button • ⭐ added to document menu
191+
</div>
192+
)}
193+
</div>
194+
195+
{/* Viewer Container */}
196+
<div className="h-[600px] w-full overflow-hidden rounded-xl border border-gray-300 shadow-lg dark:border-gray-600">
197+
<PDFViewer
198+
ref={viewerRef}
199+
config={{
200+
src: 'https://snippet.embedpdf.com/ebook.pdf',
201+
theme: { preference: themePreference },
202+
}}
203+
style={{ width: '100%', height: '100%' }}
204+
onReady={handleReady}
205+
/>
206+
</div>
207+
</div>
208+
)
209+
}

0 commit comments

Comments
 (0)