Skip to content

Commit 63ff4ff

Browse files
committed
Add UI config, new components, and toolbar updates
Introduces new config files for commands and UI schema, adds several sidebar and toolbar components, and updates existing components for improved flexibility and styling. Also adds new icons, refactors toolbar button and divider for better usability, and registers new plugins and dependencies in the example app.
1 parent 6bd4af6 commit 63ff4ff

38 files changed

Lines changed: 4657 additions & 137 deletions

examples/react-tailwind/package.json

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,37 @@
1212
},
1313
"dependencies": {
1414
"@embedpdf/core": "workspace:*",
15+
"@embedpdf/engines": "workspace:*",
16+
"@embedpdf/models": "workspace:*",
17+
"@embedpdf/pdfium": "workspace:*",
18+
"@embedpdf/plugin-annotation": "workspace:*",
19+
"@embedpdf/plugin-capture": "workspace:*",
20+
"@embedpdf/plugin-commands": "workspace:*",
1521
"@embedpdf/plugin-document-manager": "workspace:*",
16-
"@embedpdf/plugin-viewport": "workspace:*",
17-
"@embedpdf/plugin-scroll": "workspace:*",
18-
"@embedpdf/plugin-zoom": "workspace:*",
19-
"@embedpdf/plugin-render": "workspace:*",
20-
"@embedpdf/plugin-tiling": "workspace:*",
21-
"@embedpdf/plugin-search": "workspace:*",
22+
"@embedpdf/plugin-export": "workspace:*",
23+
"@embedpdf/plugin-fullscreen": "workspace:*",
24+
"@embedpdf/plugin-history": "workspace:*",
25+
"@embedpdf/plugin-i18n": "workspace:*",
2226
"@embedpdf/plugin-interaction-manager": "workspace:*",
2327
"@embedpdf/plugin-pan": "workspace:*",
28+
"@embedpdf/plugin-print": "workspace:*",
29+
"@embedpdf/plugin-redaction": "workspace:*",
30+
"@embedpdf/plugin-render": "workspace:*",
2431
"@embedpdf/plugin-rotate": "workspace:*",
32+
"@embedpdf/plugin-scroll": "workspace:*",
33+
"@embedpdf/plugin-search": "workspace:*",
34+
"@embedpdf/plugin-selection": "workspace:*",
2535
"@embedpdf/plugin-spread": "workspace:*",
26-
"@embedpdf/plugin-fullscreen": "workspace:*",
27-
"@embedpdf/plugin-export": "workspace:*",
2836
"@embedpdf/plugin-thumbnail": "workspace:*",
29-
"@embedpdf/plugin-selection": "workspace:*",
30-
"@embedpdf/plugin-print": "workspace:*",
31-
"@embedpdf/plugin-redaction": "workspace:*",
32-
"@embedpdf/plugin-capture": "workspace:*",
33-
"@embedpdf/plugin-history": "workspace:*",
34-
"@embedpdf/plugin-annotation": "workspace:*",
37+
"@embedpdf/plugin-tiling": "workspace:*",
38+
"@embedpdf/plugin-ui": "workspace:*",
3539
"@embedpdf/plugin-view-manager": "workspace:*",
36-
"@embedpdf/plugin-commands": "workspace:*",
37-
"@embedpdf/plugin-i18n": "workspace:*",
40+
"@embedpdf/plugin-viewport": "workspace:*",
41+
"@embedpdf/plugin-zoom": "workspace:*",
3842
"@embedpdf/utils": "workspace:*",
39-
"@embedpdf/models": "workspace:*",
40-
"@embedpdf/pdfium": "workspace:*",
41-
"@embedpdf/engines": "workspace:*",
4243
"react": "^18.2.0",
43-
"react-dom": "^18.2.0"
44+
"react-dom": "^18.2.0",
45+
"tailwind-merge": "^3.4.0"
4446
},
4547
"devDependencies": {
4648
"@tailwindcss/vite": "^4.1.11",

examples/react-tailwind/src/application.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { useHashRoute } from './router';
22
import { HomePage } from './pages/home';
33
import { AboutPage } from './pages/about';
44
import { ViewerPage } from './pages/viewer';
5-
import { ViewerSimplePage } from './pages/viewer-simple'; // Changed import
5+
import { ViewerSimplePage } from './pages/viewer-simple';
6+
import { ViewerSchemaPage } from './pages/viewer-schema';
67

78
export default function App() {
89
const { route } = useHashRoute();
@@ -15,7 +16,9 @@ export default function App() {
1516
case '/viewer':
1617
return <ViewerPage />;
1718
case '/viewer-simple':
18-
return <ViewerSimplePage />; // Changed component name
19+
return <ViewerSimplePage />;
20+
case '/viewer-schema':
21+
return <ViewerSchemaPage />;
1922
default:
2023
return <HomePage />;
2124
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { useCommand } from '@embedpdf/plugin-commands/react';
2+
import { useEffect, useRef } from 'react';
3+
import { ToolbarButton } from './ui';
4+
import * as Icons from './icons';
5+
import { useAnchorRegistry } from '../ui/anchor-registry';
6+
7+
type CommandButtonProps = {
8+
commandId: string;
9+
documentId: string;
10+
variant?: 'icon' | 'text' | 'icon-text' | 'tab';
11+
itemId?: string; // Unique ID for this button instance (for anchor registry)
12+
className?: string;
13+
};
14+
15+
/**
16+
* A button that executes a command when clicked.
17+
* Uses the useCommand hook to get the command state and execution function.
18+
* The icon is automatically retrieved from the command definition.
19+
*
20+
* Automatically registers itself with the anchor registry so menus can anchor to it.
21+
*/
22+
export function CommandButton({
23+
commandId,
24+
documentId,
25+
variant = 'icon',
26+
itemId,
27+
className,
28+
}: CommandButtonProps) {
29+
const command = useCommand(commandId, documentId);
30+
const buttonRef = useRef<HTMLButtonElement>(null);
31+
const anchorRegistry = useAnchorRegistry();
32+
33+
// Register this button with the anchor registry
34+
// This allows menus to anchor to it when opened via UI state changes
35+
// Scoped by documentId to support multiple documents
36+
useEffect(() => {
37+
if (itemId && buttonRef.current) {
38+
anchorRegistry.register(documentId, itemId, buttonRef.current);
39+
return () => anchorRegistry.unregister(documentId, itemId);
40+
}
41+
}, [documentId, itemId, anchorRegistry]);
42+
43+
if (!command) return null;
44+
45+
// Get the icon component from the command's icon property
46+
// Add 'Icon' suffix to match the exported icon component names
47+
const iconName = command.icon ? `${command.icon}Icon` : null;
48+
const IconComponent = iconName ? Icons[iconName as keyof typeof Icons] : null;
49+
50+
// Get iconProps if available (for dynamic colors, etc.)
51+
const iconProps = command.iconProps || {};
52+
53+
return (
54+
<ToolbarButton
55+
ref={buttonRef}
56+
onClick={() => command.execute()}
57+
isActive={command.active}
58+
disabled={command.disabled || !command.visible}
59+
aria-label={command.label}
60+
title={command.label}
61+
className={className}
62+
>
63+
{variant === 'text' ? (
64+
<span className="text-sm">{command.label}</span>
65+
) : variant === 'icon-text' ? (
66+
<>
67+
{IconComponent && (
68+
<IconComponent
69+
className="mr-2 h-5 w-5"
70+
title={command.label}
71+
style={{ color: iconProps.primaryColor }}
72+
/>
73+
)}
74+
<span>{command.label}</span>
75+
</>
76+
) : variant === 'tab' ? (
77+
<span className="px-3 py-1">{command.label}</span>
78+
) : // Default: icon only
79+
IconComponent ? (
80+
<IconComponent
81+
className="h-5 w-5"
82+
title={command.label}
83+
style={{
84+
color: iconProps.primaryColor,
85+
fill: iconProps.secondaryColor,
86+
}}
87+
/>
88+
) : (
89+
<span>{command.label}</span>
90+
)}
91+
</ToolbarButton>
92+
);
93+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useZoom } from '@embedpdf/plugin-zoom/react';
2+
import { ZoomMode } from '@embedpdf/plugin-zoom';
3+
import { useState } from 'react';
4+
import {
5+
ChevronDownIcon,
6+
FitPageIcon,
7+
FitWidthIcon,
8+
SearchMinusIcon,
9+
SearchPlusIcon,
10+
MarqueeIcon,
11+
} from './icons';
12+
import { DropdownMenu, DropdownItem, DropdownDivider } from './ui';
13+
import { CommandButton } from './command-button';
14+
15+
/**
16+
* Custom Zoom Toolbar Component
17+
*
18+
* This component is designed to be registered with the UI plugin and used
19+
* as a custom component in the UI schema.
20+
*
21+
* Props:
22+
* - documentId: The document ID (passed by the UI renderer)
23+
*/
24+
interface CustomZoomToolbarProps {
25+
documentId: string;
26+
}
27+
28+
interface ZoomPreset {
29+
value: number;
30+
label: string;
31+
}
32+
33+
interface ZoomModeItem {
34+
value: ZoomMode;
35+
label: string;
36+
}
37+
38+
const ZOOM_PRESETS: ZoomPreset[] = [
39+
{ value: 0.5, label: '50%' },
40+
{ value: 1, label: '100%' },
41+
{ value: 1.5, label: '150%' },
42+
{ value: 2, label: '200%' },
43+
{ value: 4, label: '400%' },
44+
{ value: 8, label: '800%' },
45+
];
46+
47+
const ZOOM_MODES: ZoomModeItem[] = [
48+
{ value: ZoomMode.FitPage, label: 'Fit to Page' },
49+
{ value: ZoomMode.FitWidth, label: 'Fit to Width' },
50+
];
51+
52+
/**
53+
* Custom Zoom Toolbar
54+
*
55+
* A comprehensive zoom control with:
56+
* - Zoom in/out buttons
57+
* - Current zoom percentage display
58+
* - Dropdown menu with presets, modes, and marquee zoom
59+
*/
60+
export function CustomZoomToolbar({ documentId }: CustomZoomToolbarProps) {
61+
const { state, provides } = useZoom(documentId);
62+
const [isMenuOpen, setIsMenuOpen] = useState(false);
63+
64+
if (!provides) return null;
65+
66+
const zoomPercentage = Math.round(state.currentZoomLevel * 100);
67+
68+
const handleZoomIn = () => {
69+
provides.zoomIn();
70+
setIsMenuOpen(false);
71+
};
72+
73+
const handleZoomOut = () => {
74+
provides.zoomOut();
75+
setIsMenuOpen(false);
76+
};
77+
78+
const handleSelectZoom = (value: number | ZoomMode) => {
79+
provides.requestZoom(value);
80+
setIsMenuOpen(false);
81+
};
82+
83+
const handleToggleMarquee = () => {
84+
provides.toggleMarqueeZoom();
85+
setIsMenuOpen(false);
86+
};
87+
88+
return (
89+
<div className="relative">
90+
<div className="flex items-center rounded bg-gray-100 pl-2">
91+
{/* Zoom Percentage Display */}
92+
<span className="text-sm">{zoomPercentage}%</span>
93+
<CommandButton
94+
commandId="zoom:toggle-menu"
95+
documentId={documentId}
96+
itemId="zoom-menu-button"
97+
/>
98+
{/* Zoom Out Button */}
99+
<CommandButton commandId="zoom:out" documentId={documentId} />
100+
{/* Zoom In Button */}
101+
<CommandButton commandId="zoom:in" documentId={documentId} />
102+
</div>
103+
</div>
104+
);
105+
}

examples/react-tailwind/src/components/icons/index.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,3 +1099,88 @@ export function RedactAreaIcon({ className, title }: IconProps) {
10991099
</svg>
11001100
);
11011101
}
1102+
1103+
export function PhotoIcon({ className, title }: IconProps) {
1104+
return (
1105+
<svg
1106+
className={className}
1107+
fill="none"
1108+
stroke="currentColor"
1109+
viewBox="0 0 24 24"
1110+
strokeWidth={2}
1111+
strokeLinecap="round"
1112+
strokeLinejoin="round"
1113+
aria-hidden={!title}
1114+
role={title ? 'img' : 'presentation'}
1115+
>
1116+
{title ? <title>{title}</title> : null}
1117+
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
1118+
<path d="M15 8h.01" />
1119+
<path d="M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-12z" />
1120+
<path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5" />
1121+
<path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3" />
1122+
</svg>
1123+
);
1124+
}
1125+
1126+
export function ArrowBackUpIcon({ className, title }: IconProps) {
1127+
return (
1128+
<svg
1129+
className={className}
1130+
fill="none"
1131+
stroke="currentColor"
1132+
viewBox="0 0 24 24"
1133+
strokeWidth={2}
1134+
strokeLinecap="round"
1135+
strokeLinejoin="round"
1136+
aria-hidden={!title}
1137+
role={title ? 'img' : 'presentation'}
1138+
>
1139+
{title ? <title>{title}</title> : null}
1140+
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
1141+
<path d="M9 14l-4 -4l4 -4" />
1142+
<path d="M5 10h11a4 4 0 1 1 0 8h-1" />
1143+
</svg>
1144+
);
1145+
}
1146+
1147+
export function ArrowForwardUpIcon({ className, title }: IconProps) {
1148+
return (
1149+
<svg
1150+
className={className}
1151+
fill="none"
1152+
stroke="currentColor"
1153+
viewBox="0 0 24 24"
1154+
strokeWidth={2}
1155+
strokeLinecap="round"
1156+
strokeLinejoin="round"
1157+
aria-hidden={!title}
1158+
role={title ? 'img' : 'presentation'}
1159+
>
1160+
{title ? <title>{title}</title> : null}
1161+
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
1162+
<path d="M15 14l4 -4l-4 -4" />
1163+
<path d="M19 10h-11a4 4 0 1 0 0 8h1" />
1164+
</svg>
1165+
);
1166+
}
1167+
1168+
export function PointerIcon({ className, title }: IconProps) {
1169+
return (
1170+
<svg
1171+
className={className}
1172+
fill="none"
1173+
stroke="currentColor"
1174+
viewBox="0 0 24 24"
1175+
strokeWidth={2}
1176+
strokeLinecap="round"
1177+
strokeLinejoin="round"
1178+
aria-hidden={!title}
1179+
role={title ? 'img' : 'presentation'}
1180+
>
1181+
{title ? <title>{title}</title> : null}
1182+
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
1183+
<path d="M7.904 17.563a1.2 1.2 0 0 0 2.228 .308l2.09 -3.093l4.907 4.907a1.067 1.067 0 0 0 1.509 0l1.047 -1.047a1.067 1.067 0 0 0 0 -1.509l-4.907 -4.907l3.113 -2.09a1.2 1.2 0 0 0 -.309 -2.228l-13.582 -3.904l3.904 13.563z" />
1184+
</svg>
1185+
);
1186+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
type OutlineSidebarProps = {
2+
documentId: string;
3+
};
4+
5+
/**
6+
* Placeholder Outline Sidebar
7+
*
8+
* This component will eventually render the document outline / table of contents.
9+
* For now it simply renders a placeholder so that we can test tabbed panels.
10+
*/
11+
export function OutlineSidebar({ documentId }: OutlineSidebarProps) {
12+
return (
13+
<div className="flex h-full flex-col gap-3 p-4 text-sm text-gray-600">
14+
<div className="font-medium text-gray-900">Outline (Coming Soon)</div>
15+
<p>
16+
Placeholder outline for document{' '}
17+
<code className="rounded bg-gray-100 px-1 py-0.5">{documentId}</code>.
18+
</p>
19+
<p className="text-xs">
20+
Implement the actual outline sidebar by replacing this placeholder with a component that
21+
reads the document outline from the appropriate plugin.
22+
</p>
23+
</div>
24+
);
25+
}

0 commit comments

Comments
 (0)