feat: add InspectorPanel component with document management, navigation, and search tabs
This commit is contained in:
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user