Add native-style menus, search and presentation controls, typography resources, and Xmind import/export styling support. Bundle canvas styles for preview and production, show marquee borders and selection fills, and align editor density with the project UI. Include interaction regression coverage and client comparison notes.
46 lines
3.8 KiB
TypeScript
46 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { ChevronDown, ChevronUp, Search, X } from "lucide-react";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { searchTopics, type SearchSheet, type TopicSearchResult } from "@/lib/flow-board/topic-search";
|
|
|
|
export function XmindSearchPanel({ sheets, activeSheetId, onJump, onReplace, onClose }: {
|
|
sheets: SearchSheet[];
|
|
activeSheetId: string;
|
|
onJump: (result: TopicSearchResult) => void;
|
|
onReplace: (results: TopicSearchResult[], query: string, replacement: string, caseSensitive: boolean) => void;
|
|
onClose: () => void;
|
|
}) {
|
|
const [query, setQuery] = useState("");
|
|
const [replacement, setReplacement] = useState("");
|
|
const [caseSensitive, setCaseSensitive] = useState(false);
|
|
const [scope, setScope] = useState("current");
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
const input = useRef<HTMLInputElement>(null);
|
|
const results = searchTopics(scope === "all" ? sheets : sheets.filter(sheet => sheet.id === activeSheetId),query,caseSensitive);
|
|
useEffect(() => { input.current?.focus(); }, []);
|
|
useEffect(() => { setActiveIndex(0); }, [query,caseSensitive,scope]);
|
|
const navigate = (step: number) => {
|
|
if (!results.length) return;
|
|
const next = (Math.min(activeIndex,results.length - 1) + step + results.length) % results.length;
|
|
setActiveIndex(next); onJump(results[next]);
|
|
};
|
|
return <aside className="xmind-search-panel" aria-label="查找与替换" data-flow-board-shell-control="true" onKeyDown={event => {
|
|
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); onClose(); }
|
|
}}>
|
|
<div className="xmind-panel-heading"><span>查找与替换</span><button type="button" aria-label="关闭查找" onClick={onClose}><X size={16} /></button></div>
|
|
<div className="xmind-panel-content">
|
|
<label className="xmind-search-box"><Search size={15} /><input ref={input} aria-label="查找主题" placeholder="查找主题" value={query} onChange={e => setQuery(e.target.value)} onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); navigate(e.shiftKey ? -1 : 1); } }} /></label>
|
|
<div className="xmind-search-options"><label><input type="checkbox" checked={caseSensitive} onChange={e => setCaseSensitive(e.target.checked)} /> 区分大小写</label></div>
|
|
<select aria-label="查找范围" value={scope} onChange={e => setScope(e.target.value)}><option value="current">当前画布</option><option value="all">所有画布</option></select>
|
|
<label className="xmind-search-box"><input aria-label="替换为" placeholder="替换为" value={replacement} onChange={e => setReplacement(e.target.value)} /></label>
|
|
<div className="xmind-search-replace-buttons">
|
|
<button type="button" disabled={!results.length} onClick={() => onReplace([results[Math.min(activeIndex,results.length - 1)]],query,replacement,caseSensitive)}>替换</button>
|
|
<button type="button" disabled={!results.length} onClick={() => onReplace(results,query,replacement,caseSensitive)}>全部替换</button>
|
|
</div>
|
|
<div className="xmind-search-navigation"><span role="status">{query ? `${results.length} 个匹配主题` : "输入内容开始查找"}</span><button type="button" aria-label="上一个结果" disabled={!results.length} onClick={() => navigate(-1)}><ChevronUp size={16} /></button><button type="button" aria-label="下一个结果" disabled={!results.length} onClick={() => navigate(1)}><ChevronDown size={16} /></button></div>
|
|
<div aria-label="查找结果">{results.map((result,index) => <button type="button" key={`${result.sheetId}:${result.id}`} aria-label={`定位 ${result.title}`} className={`xmind-search-result ${index === activeIndex ? "is-active" : ""}`} onClick={() => { setActiveIndex(index); onJump(result); }}><span>{result.title}</span><small>{result.sheetTitle} · {result.occurrences} 处匹配</small></button>)}</div>
|
|
</div>
|
|
</aside>;
|
|
}
|