improve search

This commit is contained in:
Reece Browne 2025-09-12 16:19:07 +01:00
parent 514956570c
commit 9901771572
3 changed files with 96 additions and 54 deletions

View File

@ -12,6 +12,7 @@ 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();
@ -214,15 +215,29 @@ 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>
<ActionIcon
variant="subtle"
radius="md"
className="right-rail-icon"
onClick={() => (window as any).togglePdfSearch?.()}
disabled={currentView !== 'viewer'}
>
<LocalIcon icon="search" width="1.5rem" height="1.5rem" />
</ActionIcon>
<Popover position="left" withArrow shadow="md" offset={8}>
<Popover.Target>
<div style={{ display: 'inline-flex' }}>
<ActionIcon
variant="subtle"
radius="md"
className="right-rail-icon"
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: 320 }}>
<SearchInterface
visible={true}
onClose={() => {}}
/>
</div>
</Popover.Dropdown>
</Popover>
</Tooltip>

View File

@ -8,7 +8,6 @@ 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 {
@ -29,7 +28,6 @@ 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
@ -122,16 +120,11 @@ 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;
};
}, []);
@ -227,11 +220,6 @@ const EmbedPdfViewer = ({
</div>
)}
{/* Search Interface Overlay */}
<SearchInterface
visible={isSearchVisible}
onClose={() => setIsSearchVisible(false)}
/>
{/* Thumbnail Sidebar */}
<ThumbnailSidebar

View File

@ -11,6 +11,7 @@ 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;
@ -54,7 +55,11 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
}, [visible]);
const handleSearch = async (query: string) => {
if (!query.trim()) return;
if (!query.trim()) {
// If query is empty, clear the search
handleClearSearch();
return;
}
const searchAPI = (window as any).embedPdfSearch;
if (searchAPI) {
@ -95,47 +100,60 @@ 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) {
searchAPI.goToResult(index);
}
};
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={{
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'
padding: '0px'
}}
>
{/* Header with close button */}
<Group justify="space-between" mb="md">
{/* Header */}
<Group 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 */}
@ -143,7 +161,14 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
<TextInput
placeholder={t('search.placeholder', 'Enter search term...')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.currentTarget.value)}
onChange={(e) => {
const newValue = e.currentTarget.value;
setSearchQuery(newValue);
// If user clears the input, clear the search highlights
if (!newValue.trim()) {
handleClearSearch();
}
}}
onKeyDown={handleKeyDown}
style={{ flex: 1 }}
rightSection={
@ -162,15 +187,29 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
{/* Results info and navigation */}
{resultInfo && (
<Group justify="space-between" align="center">
<Text size="sm" c="dimmed">
{resultInfo.totalResults === 0
? t('search.noResults', 'No results found')
: t('search.resultCount', '{{current}} of {{total}}', {
current: resultInfo.currentIndex,
total: resultInfo.totalResults
})
}
</Text>
{resultInfo.totalResults === 0 ? (
<Text size="sm" c="dimmed">
{t('search.noResults', 'No results found')}
</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: '50px' }}
type="number"
min="1"
max={resultInfo.totalResults}
/>
<Text size="sm" c="dimmed">
of {resultInfo.totalResults}
</Text>
</Group>
)}
{resultInfo.totalResults > 0 && (
<Group gap="xs">