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 { EmptyState, Popover } from './ui';
|
||||||
import {
|
import {
|
||||||
PagesIcon, NotesIcon, SearchIcon, PropertiesIcon, FontsIcon, OutlineIcon, FormsIcon,
|
PagesIcon, NotesIcon, SearchIcon, PropertiesIcon, FontsIcon, OutlineIcon, FormsIcon,
|
||||||
ChevronDownIcon, SearchIcon as SearchGlyph,
|
ChevronDownIcon, ChevronUpIcon, SearchIcon as SearchGlyph,
|
||||||
} from './icons';
|
} from './icons';
|
||||||
|
|
||||||
export type InspectorTab = 'pages' | 'notes' | 'search' | 'properties' | 'fonts' | 'outline' | 'forms';
|
export type InspectorTab = 'pages' | 'notes' | 'search' | 'properties' | 'fonts' | 'outline' | 'forms';
|
||||||
@@ -32,6 +32,10 @@ interface InspectorPanelProps {
|
|||||||
|
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
onSearchQueryChange: (q: string) => void;
|
onSearchQueryChange: (q: string) => void;
|
||||||
|
searchCaseSensitive: boolean;
|
||||||
|
onSearchCaseSensitiveChange: (c: boolean) => void;
|
||||||
|
searchWholeWords: boolean;
|
||||||
|
onSearchWholeWordsChange: (w: boolean) => void;
|
||||||
searchResults: SearchResult[];
|
searchResults: SearchResult[];
|
||||||
searchCurrentMatch: number;
|
searchCurrentMatch: number;
|
||||||
onSelectSearchMatch: (i: number) => void;
|
onSelectSearchMatch: (i: number) => void;
|
||||||
@@ -216,10 +220,12 @@ const NotesTab: React.FC<{ annotations: Annotation[]; onNavigate: (a: Annotation
|
|||||||
|
|
||||||
/* ---------------------------------------------------------------- Search */
|
/* ---------------------------------------------------------------- Search */
|
||||||
const SearchTab: React.FC<InspectorPanelProps> = (p) => {
|
const SearchTab: React.FC<InspectorPanelProps> = (p) => {
|
||||||
let flatIndex = -1;
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="border-b border-[#ebedf0] p-3">
|
<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]">
|
<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]" />
|
<SearchGlyph size={15} className="text-[#98a1ad]" />
|
||||||
<input
|
<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]"
|
className="w-full bg-transparent text-[13px] text-[#18212e] outline-none placeholder:text-[#98a1ad]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{p.searchQuery && (
|
|
||||||
<p className="mt-2 px-0.5 text-[11px] font-medium text-[#5b6573]">
|
<div className="mt-3 flex items-center justify-between px-0.5">
|
||||||
{p.searchResults.length > 0 ? `${p.searchResults.length} match${p.searchResults.length > 1 ? 'es' : ''}` : 'No matches'}
|
<span className="text-[11.5px] font-medium text-[#5b6573]">
|
||||||
</p>
|
{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>
|
||||||
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto p-2">
|
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto p-2">
|
||||||
{p.searchResults.map((r) => {
|
{p.searchResults.map((r, i) => {
|
||||||
flatIndex++;
|
const isSelected = i === p.searchCurrentMatch;
|
||||||
const i = flatIndex;
|
|
||||||
|
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 (
|
return (
|
||||||
<CustomButton variant="unstyled" key={i} onClick={() => p.onSelectSearchMatch(i)}
|
<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 ${
|
className={`mb-1 flex w-full flex-col gap-1.5 rounded-[6px] px-2.5 py-2 text-left transition-colors ${
|
||||||
i === p.searchCurrentMatch ? 'bg-[#eef4ff] text-[#18212e]' : 'hover:bg-[#f6f7f9] text-[#5b6573]'
|
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={`text-[12.5px] leading-relaxed ${isSelected ? 'text-white' : 'text-[#18212e]'}`}>
|
||||||
<span className="truncate">{r.text || p.searchQuery}</span>
|
{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>
|
</CustomButton>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ export const SearchOverlayLayer: React.FC<SearchOverlayLayerProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={rectIdx}
|
key={rectIdx}
|
||||||
className={`absolute rounded-sm border ${
|
className={`absolute ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-orange-500/50 border-orange-600/80 shadow-[0_0_8px_rgba(249,115,22,0.6)] z-30'
|
? 'bg-[#f97316]/40 shadow-[0_0_4px_3px_rgba(249,115,22,0.3)] rounded-[4px] z-30'
|
||||||
: 'bg-yellow-400/40 border-yellow-500/60'
|
: 'bg-[#fef08a]/50 rounded-[3px]'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
left: `${rect.x * zoom}px`,
|
left: `${rect.x * zoom}px`,
|
||||||
|
|||||||
Reference in New Issue
Block a user