feat: add InspectorPanel component with document management, navigation, and search tabs

This commit is contained in:
azeeee05
2026-06-10 15:20:52 +05:30
parent 7f43262385
commit 9f30da5c03
2 changed files with 70 additions and 17 deletions
+67 -14
View File
@@ -6,7 +6,7 @@ import { Thumbnail } from './Thumbnail';
import { EmptyState, Popover } from './ui';
import {
PagesIcon, NotesIcon, SearchIcon, PropertiesIcon, FontsIcon, OutlineIcon, FormsIcon,
ChevronDownIcon, SearchIcon as SearchGlyph,
ChevronDownIcon, ChevronUpIcon, SearchIcon as SearchGlyph,
} from './icons';
export type InspectorTab = 'pages' | 'notes' | 'search' | 'properties' | 'fonts' | 'outline' | 'forms';
@@ -32,6 +32,10 @@ interface InspectorPanelProps {
searchQuery: string;
onSearchQueryChange: (q: string) => void;
searchCaseSensitive: boolean;
onSearchCaseSensitiveChange: (c: boolean) => void;
searchWholeWords: boolean;
onSearchWholeWordsChange: (w: boolean) => void;
searchResults: SearchResult[];
searchCurrentMatch: number;
onSelectSearchMatch: (i: number) => void;
@@ -216,10 +220,12 @@ const NotesTab: React.FC<{ annotations: Annotation[]; onNavigate: (a: Annotation
/* ---------------------------------------------------------------- Search */
const SearchTab: React.FC<InspectorPanelProps> = (p) => {
let flatIndex = -1;
return (
<div className="flex h-full flex-col">
<div className="border-b border-[#ebedf0] p-3">
<div className="flex items-center justify-between mb-3">
<span className="text-[13px] font-bold text-[#18212e]">Find</span>
</div>
<div className="flex items-center gap-2 rounded-[8px] border border-[#ebedf0] bg-[#f6f7f9] px-2.5 py-1.5 focus-within:border-[#2563eb]">
<SearchGlyph size={15} className="text-[#98a1ad]" />
<input
@@ -230,23 +236,70 @@ const SearchTab: React.FC<InspectorPanelProps> = (p) => {
className="w-full bg-transparent text-[13px] text-[#18212e] outline-none placeholder:text-[#98a1ad]"
/>
</div>
{p.searchQuery && (
<p className="mt-2 px-0.5 text-[11px] font-medium text-[#5b6573]">
{p.searchResults.length > 0 ? `${p.searchResults.length} match${p.searchResults.length > 1 ? 'es' : ''}` : 'No matches'}
</p>
)}
<div className="mt-3 flex items-center justify-between px-0.5">
<span className="text-[11.5px] font-medium text-[#5b6573]">
{p.searchResults.length > 0 ? `Search results: ${p.searchCurrentMatch + 1}/${p.searchResults.length}` : (p.searchQuery ? 'No matches' : '')}
</span>
{p.searchResults.length > 0 && (
<div className="flex items-center gap-1 text-[#5b6573]">
<CustomButton variant="icon" size={24} onClick={() => p.onSelectSearchMatch((p.searchCurrentMatch - 1 + p.searchResults.length) % p.searchResults.length)}><ChevronUpIcon size={14} /></CustomButton>
<CustomButton variant="icon" size={24} onClick={() => p.onSelectSearchMatch((p.searchCurrentMatch + 1) % p.searchResults.length)}><ChevronDownIcon size={14} /></CustomButton>
</div>
)}
</div>
<div className="mt-2 flex flex-col gap-2 px-0.5">
<label className="flex items-center gap-2 text-[12px] text-[#5b6573] cursor-pointer">
<input type="checkbox" checked={p.searchCaseSensitive} onChange={(e) => p.onSearchCaseSensitiveChange(e.target.checked)} className="rounded border-[#dadde2] text-[#2563eb] focus:ring-[#2563eb]" />
Case sensitive
</label>
<label className="flex items-center gap-2 text-[12px] text-[#5b6573] cursor-pointer">
<input type="checkbox" checked={p.searchWholeWords} onChange={(e) => p.onSearchWholeWordsChange(e.target.checked)} className="rounded border-[#dadde2] text-[#2563eb] focus:ring-[#2563eb]" />
Whole words only
</label>
</div>
</div>
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto p-2">
{p.searchResults.map((r) => {
flatIndex++;
const i = flatIndex;
{p.searchResults.map((r, i) => {
const isSelected = i === p.searchCurrentMatch;
const highlightText = (text: string, query: string) => {
if (!query) return text;
const flags = p.searchCaseSensitive ? 'g' : 'gi';
let regexStr = query.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
if (p.searchWholeWords) regexStr = `\\b${regexStr}\\b`;
try {
const regex = new RegExp(`(${regexStr})`, flags);
const parts = text.split(regex);
return (
<span>
{parts.map((part, index) =>
regex.test(part) ? (
<mark key={index} className={`font-bold rounded-[2px] px-0.5 ${isSelected ? 'bg-[#fef08a] text-black' : 'bg-[#eef4ff] text-[#2563eb]'}`}>{part}</mark>
) : (
<span key={index}>{part}</span>
)
)}
</span>
);
} catch (e) {
return text;
}
};
return (
<CustomButton variant="unstyled" key={i} onClick={() => p.onSelectSearchMatch(i)}
className={`mb-1 flex w-full items-center gap-2 rounded-[6px] px-2.5 py-2 text-left text-[12px] transition-colors ${
i === p.searchCurrentMatch ? 'bg-[#eef4ff] text-[#18212e]' : 'hover:bg-[#f6f7f9] text-[#5b6573]'
className={`mb-1 flex w-full flex-col gap-1.5 rounded-[6px] px-2.5 py-2 text-left transition-colors ${
isSelected ? 'bg-[#2563eb] text-white' : 'hover:bg-[#f6f7f9] text-[#5b6573]'
}`}>
<span className="rounded bg-[#edeff2] px-1.5 py-0.5 text-[10px] font-semibold text-[#98a1ad]">p.{r.pageIndex + 1}</span>
<span className="truncate">{r.text || p.searchQuery}</span>
<span className={`text-[12.5px] leading-relaxed ${isSelected ? 'text-white' : 'text-[#18212e]'}`}>
{highlightText(r.text || p.searchQuery, p.searchQuery)}
</span>
<span className={`self-start rounded px-1.5 py-0.5 text-[10px] font-semibold ${isSelected ? 'bg-[#1d4ed8] text-white' : 'bg-[#edeff2] text-[#98a1ad]'}`}>
Page {r.pageIndex + 1}
</span>
</CustomButton>
);
})}
+3 -3
View File
@@ -41,10 +41,10 @@ export const SearchOverlayLayer: React.FC<SearchOverlayLayerProps> = ({
return (
<div
key={rectIdx}
className={`absolute rounded-sm border ${
className={`absolute ${
isActive
? 'bg-orange-500/50 border-orange-600/80 shadow-[0_0_8px_rgba(249,115,22,0.6)] z-30'
: 'bg-yellow-400/40 border-yellow-500/60'
? 'bg-[#f97316]/40 shadow-[0_0_4px_3px_rgba(249,115,22,0.3)] rounded-[4px] z-30'
: 'bg-[#fef08a]/50 rounded-[3px]'
}`}
style={{
left: `${rect.x * zoom}px`,