Compare commits

..

No commits in common. "1709ca904925e2c6f672fcbeeabe806eb48d79f6" and "423617db52203f4de20c20188aae317b83fa851f" have entirely different histories.

14 changed files with 146 additions and 168 deletions

View File

@ -7,16 +7,16 @@ import { useRightRail } from '../../contexts/RightRailContext';
import { useFileState, useFileSelection, useFileManagement } from '../../contexts/FileContext';
import { useNavigationState } from '../../contexts/NavigationContext';
import { useTranslation } from 'react-i18next';
import { usePanState } from '../../hooks/usePanState';
import LanguageSelector from '../shared/LanguageSelector';
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
import { Tooltip } from '../shared/Tooltip';
import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel';
import { SearchInterface } from '../viewer/SearchInterface';
export default function RightRail() {
const { t } = useTranslation();
const [isPanning, setIsPanning] = useState(false);
const isPanning = usePanState();
const { toggleTheme } = useRainbowThemeContext();
const { buttons, actions } = useRightRail();
const topButtons = useMemo(() => buttons.filter(b => (b.section || 'top') === 'top' && (b.visible ?? true)), [buttons]);
@ -215,29 +215,15 @@ export default function RightRail() {
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
{/* Search */}
<Tooltip content={t('rightRail.search', 'Search PDF')} position="left" offset={12} arrow>
<Popover position="left" withArrow shadow="md" offset={8}>
<Popover.Target>
<div style={{ display: 'inline-flex' }}>
<ActionIcon
variant="subtle"
radius="md"
className="right-rail-icon"
onClick={() => (window as any).togglePdfSearch?.()}
disabled={currentView !== 'viewer'}
aria-label={typeof t === 'function' ? t('rightRail.search', 'Search PDF') : 'Search PDF'}
>
<LocalIcon icon="search" width="1.5rem" height="1.5rem" />
</ActionIcon>
</div>
</Popover.Target>
<Popover.Dropdown>
<div style={{ minWidth: '20rem' }}>
<SearchInterface
visible={true}
onClose={() => {}}
/>
</div>
</Popover.Dropdown>
</Popover>
</Tooltip>
@ -248,10 +234,7 @@ export default function RightRail() {
color={isPanning ? "blue" : undefined}
radius="md"
className="right-rail-icon"
onClick={() => {
(window as any).embedPdfPan?.togglePan();
setIsPanning(!isPanning);
}}
onClick={() => (window as any).embedPdfPan?.togglePan()}
disabled={currentView !== 'viewer'}
>
<LocalIcon icon="pan-tool-rounded" width="1.5rem" height="1.5rem" />
@ -343,7 +326,7 @@ export default function RightRail() {
</div>
</Popover.Target>
<Popover.Dropdown>
<div style={{ minWidth: '17.5rem' }}>
<div style={{ minWidth: 280 }}>
<BulkSelectionPanel
csvInput={csvInput}
setCsvInput={setCsvInput}

View File

@ -8,6 +8,7 @@ import { useFileState } from "../../contexts/FileContext";
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
import { LocalEmbedPDF } from './LocalEmbedPDF';
import { PdfViewerToolbar } from './PdfViewerToolbar';
import { SearchInterface } from './SearchInterface';
import { ThumbnailSidebar } from './ThumbnailSidebar';
export interface EmbedPdfViewerProps {
@ -28,6 +29,7 @@ const EmbedPdfViewer = ({
const { colorScheme } = useMantineColorScheme();
const viewerRef = React.useRef<HTMLDivElement>(null);
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
const [isSearchVisible, setIsSearchVisible] = React.useState(false);
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = React.useState(false);
// Get current file from FileContext
@ -120,11 +122,16 @@ const EmbedPdfViewer = ({
// Expose toggle functions globally for right rail buttons
React.useEffect(() => {
(window as any).togglePdfSearch = () => {
setIsSearchVisible(prev => !prev);
};
(window as any).toggleThumbnailSidebar = () => {
setIsThumbnailSidebarVisible(prev => !prev);
};
return () => {
delete (window as any).togglePdfSearch;
delete (window as any).toggleThumbnailSidebar;
};
}, []);
@ -220,6 +227,11 @@ const EmbedPdfViewer = ({
</div>
)}
{/* Search Interface Overlay */}
<SearchInterface
visible={isSearchVisible}
onClose={() => setIsSearchVisible(false)}
/>
{/* Thumbnail Sidebar */}
<ThumbnailSidebar

View File

@ -17,13 +17,13 @@ import { SpreadPluginPackage, SpreadMode } from '@embedpdf/plugin-spread/react';
import { SearchPluginPackage } from '@embedpdf/plugin-search/react';
import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react';
import { CustomSearchLayer } from './CustomSearchLayer';
import { ZoomAPIBridge } from './ZoomAPIBridge';
import { ScrollAPIBridge } from './ScrollAPIBridge';
import { SelectionAPIBridge } from './SelectionAPIBridge';
import { PanAPIBridge } from './PanAPIBridge';
import { SpreadAPIBridge } from './SpreadAPIBridge';
import { SearchAPIBridge } from './SearchAPIBridge';
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
import { ZoomControlsExporter } from './ZoomControlsExporter';
import { ScrollControlsExporter } from './ScrollControlsExporter';
import { SelectionControlsExporter } from './SelectionControlsExporter';
import { PanControlsExporter } from './PanControlsExporter';
import { SpreadControlsExporter } from './SpreadControlsExporter';
import { SearchControlsExporter } from './SearchControlsExporter';
import { ThumbnailControlsExporter } from './ThumbnailControlsExporter';
interface LocalEmbedPDFProps {
file?: File | Blob;
@ -180,13 +180,13 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) {
minWidth: 0
}}>
<EmbedPDF engine={engine} plugins={plugins}>
<ZoomAPIBridge />
<ScrollAPIBridge />
<SelectionAPIBridge />
<PanAPIBridge />
<SpreadAPIBridge />
<SearchAPIBridge />
<ThumbnailAPIBridge />
<ZoomControlsExporter />
<ScrollControlsExporter />
<SelectionControlsExporter />
<PanControlsExporter />
<SpreadControlsExporter />
<SearchControlsExporter />
<ThumbnailControlsExporter />
<GlobalPointerProvider>
<Viewport
style={{

View File

@ -2,9 +2,9 @@ import { useEffect, useState } from 'react';
import { usePan } from '@embedpdf/plugin-pan/react';
/**
* Component that runs inside EmbedPDF context and bridges pan controls to global window
* Component that runs inside EmbedPDF context and exports pan controls globally
*/
export function PanAPIBridge() {
export function PanControlsExporter() {
const { provides: pan, isPanning } = usePan();
const [panStateListeners, setPanStateListeners] = useState<Array<(isPanning: boolean) => void>>([]);
@ -21,10 +21,11 @@ export function PanAPIBridge() {
pan.disablePan();
},
togglePan: () => {
console.log('EmbedPDF: Toggling pan mode, current isPanning:', isPanning);
pan.togglePan();
},
makePanDefault: () => pan.makePanDefault(),
get isPanning() { return isPanning; }, // Use getter to always return current value
isPanning: isPanning,
// Subscribe to pan state changes for reactive UI
onPanStateChange: (callback: (isPanning: boolean) => void) => {
setPanStateListeners(prev => [...prev, callback]);
@ -35,6 +36,11 @@ export function PanAPIBridge() {
},
};
console.log('EmbedPDF pan controls exported to window.embedPdfPan', {
isPanning,
panAPI: pan,
availableMethods: Object.keys(pan)
});
} else {
console.warn('EmbedPDF pan API not available yet');
}
@ -45,5 +51,5 @@ export function PanAPIBridge() {
panStateListeners.forEach(callback => callback(isPanning));
}, [isPanning, panStateListeners]);
return null;
return null; // This component doesn't render anything
}

View File

@ -35,7 +35,6 @@ export function PdfViewerToolbar({
const [dynamicZoom, setDynamicZoom] = useState(currentZoom);
const [dynamicPage, setDynamicPage] = useState(currentPage);
const [dynamicTotalPages, setDynamicTotalPages] = useState(totalPages);
const [isPanning, setIsPanning] = useState(false);
// Update zoom and scroll state from EmbedPDF APIs
useEffect(() => {
@ -54,12 +53,6 @@ export function PdfViewerToolbar({
setDynamicTotalPages(totalPagesNum);
setPageInput(currentPageNum);
}
// Update pan mode state
if ((window as any).embedPdfPan) {
const panState = (window as any).embedPdfPan.isPanning || false;
setIsPanning(panState);
}
};
// Update state immediately
@ -140,7 +133,7 @@ export function PdfViewerToolbar({
borderBottomRightRadius: 0,
boxShadow: "0 -2px 8px rgba(0,0,0,0.04)",
pointerEvents: "auto",
minWidth: '26.5rem',
minWidth: 420,
}}
>
{/* First Page Button */}
@ -152,7 +145,7 @@ export function PdfViewerToolbar({
radius="xl"
onClick={handleFirstPage}
disabled={dynamicPage === 1}
style={{ minWidth: '2.5rem' }}
style={{ minWidth: 36 }}
title={t("viewer.firstPage", "First Page")}
>
<FirstPageIcon fontSize="small" />
@ -167,7 +160,7 @@ export function PdfViewerToolbar({
radius="xl"
onClick={handlePreviousPage}
disabled={dynamicPage === 1}
style={{ minWidth: '2.5rem' }}
style={{ minWidth: 36 }}
title={t("viewer.previousPage", "Previous Page")}
>
<ArrowBackIosIcon fontSize="small" />
@ -204,7 +197,7 @@ export function PdfViewerToolbar({
radius="xl"
onClick={handleNextPage}
disabled={dynamicPage === dynamicTotalPages}
style={{ minWidth: '2.5rem' }}
style={{ minWidth: 36 }}
title={t("viewer.nextPage", "Next Page")}
>
<ArrowForwardIosIcon fontSize="small" />
@ -219,7 +212,7 @@ export function PdfViewerToolbar({
radius="xl"
onClick={handleLastPage}
disabled={dynamicPage === dynamicTotalPages}
style={{ minWidth: '2.5rem' }}
style={{ minWidth: 36 }}
title={t("viewer.lastPage", "Last Page")}
>
<LastPageIcon fontSize="small" />
@ -232,7 +225,7 @@ export function PdfViewerToolbar({
size="md"
radius="xl"
onClick={onDualPageToggle}
style={{ minWidth: '2.5rem' }}
style={{ minWidth: 36 }}
title={dualPage ? t("viewer.singlePageView", "Single Page View") : t("viewer.dualPageView", "Dual Page View")}
>
{dualPage ? <DescriptionIcon fontSize="small" /> : <ViewWeekIcon fontSize="small" />}
@ -246,12 +239,12 @@ export function PdfViewerToolbar({
size="md"
radius="xl"
onClick={handleZoomOut}
style={{ minWidth: '2rem', padding: 0 }}
style={{ minWidth: 32, padding: 0 }}
title={t("viewer.zoomOut", "Zoom out")}
>
</Button>
<span style={{ minWidth: '2.5rem', textAlign: "center" }}>
<span style={{ minWidth: 40, textAlign: "center" }}>
{dynamicZoom}%
</span>
<Button
@ -260,7 +253,7 @@ export function PdfViewerToolbar({
size="md"
radius="xl"
onClick={handleZoomIn}
style={{ minWidth: '2rem', padding: 0 }}
style={{ minWidth: 32, padding: 0 }}
title={t("viewer.zoomIn", "Zoom in")}
>
+

View File

@ -4,7 +4,7 @@ import { useScroll } from '@embedpdf/plugin-scroll/react';
/**
* Component that runs inside EmbedPDF context and exports scroll controls globally
*/
export function ScrollAPIBridge() {
export function ScrollControlsExporter() {
const { provides: scroll, state: scrollState } = useScroll();
useEffect(() => {
@ -23,5 +23,5 @@ export function ScrollAPIBridge() {
}
}, [scroll, scrollState]);
return null;
return null; // This component doesn't render anything
}

View File

@ -2,9 +2,9 @@ import { useEffect } from 'react';
import { useSearch } from '@embedpdf/plugin-search/react';
/**
* Component that runs inside EmbedPDF context and bridges search controls to global window
* Component that runs inside EmbedPDF context and exports search controls globally
*/
export function SearchAPIBridge() {
export function SearchControlsExporter() {
const { provides: search, state } = useSearch();
useEffect(() => {
@ -48,5 +48,5 @@ export function SearchAPIBridge() {
}
}, [search, state]);
return null;
return null; // This component doesn't render anything
}

View File

@ -11,7 +11,6 @@ interface SearchInterfaceProps {
export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
const { t } = useTranslation();
const [searchQuery, setSearchQuery] = useState('');
const [jumpToValue, setJumpToValue] = useState('');
const [resultInfo, setResultInfo] = useState<{
currentIndex: number;
totalResults: number;
@ -55,11 +54,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
}, [visible]);
const handleSearch = async (query: string) => {
if (!query.trim()) {
// If query is empty, clear the search
handleClearSearch();
return;
}
if (!query.trim()) return;
const searchAPI = (window as any).embedPdfSearch;
if (searchAPI) {
@ -100,61 +95,47 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
const searchAPI = (window as any).embedPdfSearch;
if (searchAPI) {
searchAPI.clearSearch();
// Also try to explicitly clear highlights if available
if (searchAPI.searchAPI && searchAPI.searchAPI.clearHighlights) {
searchAPI.searchAPI.clearHighlights();
}
}
setSearchQuery('');
setResultInfo(null);
};
// Sync search query with API state on mount
useEffect(() => {
const searchAPI = (window as any).embedPdfSearch;
if (searchAPI && searchAPI.state && searchAPI.state.query) {
setSearchQuery(searchAPI.state.query);
}
}, []);
const handleJumpToResult = (index: number) => {
const searchAPI = (window as any).embedPdfSearch;
if (searchAPI && resultInfo && index >= 1 && index <= resultInfo.totalResults) {
// Convert 1-based user input to 0-based API index
searchAPI.goToResult(index - 1);
}
};
const handleJumpToSubmit = () => {
const index = parseInt(jumpToValue);
if (index && resultInfo && index >= 1 && index <= resultInfo.totalResults) {
handleJumpToResult(index);
}
};
const handleJumpToKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleJumpToSubmit();
}
};
const handleClose = () => {
handleClearSearch();
onClose();
};
if (!visible) return null;
return (
<Box
style={{
padding: '0px'
position: 'fixed',
top: '20px',
right: '20px',
zIndex: 1000,
backgroundColor: 'var(--mantine-color-body)',
border: '1px solid var(--mantine-color-gray-3)',
borderRadius: '8px',
padding: '16px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
minWidth: '320px',
maxWidth: '400px'
}}
>
{/* Header */}
<Group mb="md">
{/* Header with close button */}
<Group justify="space-between" mb="md">
<Text size="sm" fw={600}>
{t('search.title', 'Search PDF')}
</Text>
<ActionIcon
variant="subtle"
size="sm"
onClick={handleClose}
aria-label="Close search"
>
<LocalIcon icon="close" width="1rem" height="1rem" />
</ActionIcon>
</Group>
{/* Search input */}
@ -162,14 +143,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
<TextInput
placeholder={t('search.placeholder', 'Enter search term...')}
value={searchQuery}
onChange={(e) => {
const newValue = e.currentTarget.value;
setSearchQuery(newValue);
// If user clears the input, clear the search highlights
if (!newValue.trim()) {
handleClearSearch();
}
}}
onChange={(e) => setSearchQuery(e.currentTarget.value)}
onKeyDown={handleKeyDown}
style={{ flex: 1 }}
rightSection={
@ -188,29 +162,15 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
{/* Results info and navigation */}
{resultInfo && (
<Group justify="space-between" align="center">
{resultInfo.totalResults === 0 ? (
<Text size="sm" c="dimmed">
{t('search.noResults', 'No results found')}
{resultInfo.totalResults === 0
? t('search.noResults', 'No results found')
: t('search.resultCount', '{{current}} of {{total}}', {
current: resultInfo.currentIndex,
total: resultInfo.totalResults
})
}
</Text>
) : (
<Group gap="xs" align="center">
<TextInput
size="xs"
value={jumpToValue}
onChange={(e) => setJumpToValue(e.currentTarget.value)}
onKeyDown={handleJumpToKeyDown}
onBlur={handleJumpToSubmit}
placeholder={resultInfo.currentIndex.toString()}
style={{ width: '3rem' }}
type="number"
min="1"
max={resultInfo.totalResults}
/>
<Text size="sm" c="dimmed">
of {resultInfo.totalResults}
</Text>
</Group>
)}
{resultInfo.totalResults > 0 && (
<Group gap="xs">

View File

@ -4,7 +4,7 @@ import { useSelectionCapability, SelectionRangeX } from '@embedpdf/plugin-select
/**
* Component that runs inside EmbedPDF context and exports selection controls globally
*/
export function SelectionAPIBridge() {
export function SelectionControlsExporter() {
const { provides: selection } = useSelectionCapability();
const [hasSelection, setHasSelection] = useState(false);
@ -47,5 +47,5 @@ export function SelectionAPIBridge() {
}
}, [selection, hasSelection]);
return null;
return null; // This component doesn't render anything
}

View File

@ -4,7 +4,7 @@ import { useSpread, SpreadMode } from '@embedpdf/plugin-spread/react';
/**
* Component that runs inside EmbedPDF context and exports spread controls globally
*/
export function SpreadAPIBridge() {
export function SpreadControlsExporter() {
const { provides: spread, spreadMode } = useSpread();
useEffect(() => {
@ -38,5 +38,5 @@ export function SpreadAPIBridge() {
}
}, [spread, spreadMode]);
return null;
return null; // This component doesn't render anything
}

View File

@ -4,11 +4,11 @@ import { useThumbnailCapability } from '@embedpdf/plugin-thumbnail/react';
/**
* Component that runs inside EmbedPDF context and exports thumbnail controls globally
*/
export function ThumbnailAPIBridge() {
export function ThumbnailControlsExporter() {
const { provides: thumbnail } = useThumbnailCapability();
useEffect(() => {
console.log('📄 ThumbnailAPIBridge useEffect:', { thumbnail: !!thumbnail });
console.log('📄 ThumbnailControlsExporter useEffect:', { thumbnail: !!thumbnail });
if (thumbnail) {
console.log('📄 Exporting thumbnail controls to window:', {
availableMethods: Object.keys(thumbnail),
@ -22,5 +22,5 @@ export function ThumbnailAPIBridge() {
}
}, [thumbnail]);
return null;
return null; // This component doesn't render anything
}

View File

@ -111,7 +111,7 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
right: 0,
top: 0,
bottom: 0,
width: '15rem',
width: '240px',
backgroundColor: actualColorScheme === 'dark' ? '#1a1b1e' : '#f8f9fa',
borderLeft: `1px solid ${actualColorScheme === 'dark' ? '#373A40' : '#e9ecef'}`,
zIndex: 998,
@ -174,8 +174,8 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
/>
) : thumbnails[pageIndex] === 'error' ? (
<div style={{
width: '11.5rem',
height: '15rem',
width: '180px',
height: '240px',
backgroundColor: actualColorScheme === 'dark' ? '#2d1b1b' : '#ffebee',
border: `1px solid ${actualColorScheme === 'dark' ? '#5d3737' : '#ffcdd2'}`,
borderRadius: '4px',
@ -189,8 +189,8 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
</div>
) : (
<div style={{
width: '11.5rem',
height: '15rem',
width: '180px',
height: '240px',
backgroundColor: actualColorScheme === 'dark' ? '#25262b' : '#f8f9fa',
border: `1px solid ${actualColorScheme === 'dark' ? '#373A40' : '#e9ecef'}`,
borderRadius: '4px',

View File

@ -4,7 +4,7 @@ import { useZoom } from '@embedpdf/plugin-zoom/react';
/**
* Component that runs inside EmbedPDF context and exports zoom controls globally
*/
export function ZoomAPIBridge() {
export function ZoomControlsExporter() {
const { provides: zoom, state: zoomState } = useZoom();
useEffect(() => {
@ -22,5 +22,5 @@ export function ZoomAPIBridge() {
}
}, [zoom, zoomState]);
return null;
return null; // This component doesn't render anything
}

View File

@ -0,0 +1,24 @@
import { useState, useEffect } from 'react';
/**
* Hook to track EmbedPDF pan state for reactive UI components
*/
export function usePanState() {
const [isPanning, setIsPanning] = useState(false);
useEffect(() => {
// Subscribe to pan state changes
const unsubscribe = (window as any).embedPdfPan?.onPanStateChange?.((newIsPanning: boolean) => {
setIsPanning(newIsPanning);
});
// Get initial state
if ((window as any).embedPdfPan?.isPanning !== undefined) {
setIsPanning((window as any).embedPdfPan.isPanning);
}
return unsubscribe || (() => {});
}, []);
return isPanning;
}