/* Overlays: command palette (fuzzy file finder), content search, context menu, toast */
function fuzzy(q, str) {
q = q.toLowerCase(); const s = str.toLowerCase();
let i = 0; const idx = [];
for (let j = 0; j < s.length && i < q.length; j++) {
if (s[j] === q[i]) { idx.push(j); i++; }
}
return i === q.length ? idx : null;
}
function Highlight({ text, idx }) {
if (!idx || !idx.length) return {text};
const set = new Set(idx);
return {text.split("").map((ch, i) => set.has(i) ? {ch} : {ch})};
}
function SearchModal({ onOpen, onOpenAt, onClose, changeSet }) {
const [q, setQ] = useState("");
const [sel, setSel] = useState(0);
const inputRef = useRef(null);
const leftRef = useRef(null);
const allPaths = useMemo(() => Object.keys(PROJECT.files), []);
useEffect(() => { inputRef.current && inputRef.current.focus(); }, []);
// content hits (left)
const content = useMemo(() => {
const term = q.trim();
if (term.length < 2) return [];
const low = term.toLowerCase();
const groups = [];
for (const [path, src] of Object.entries(PROJECT.files)) {
const lines = src.split("\n");
const hits = [];
lines.forEach((ln, i) => {
const ix = ln.toLowerCase().indexOf(low);
if (ix >= 0) hits.push({ no: i + 1, ln, ix });
});
if (hits.length) groups.push({ path, hits });
}
return groups;
}, [q]);
// file-name matches (right)
const files = useMemo(() => {
const term = q.trim();
if (!term) return [];
const out = [];
for (const p of allPaths) {
const name = p.split("/").pop();
const ni = fuzzy(term, name);
if (ni) { out.push({ path: p, idx: ni, rank: 0, pos: ni[0] }); continue; }
const pi = fuzzy(term, p);
if (pi) out.push({ path: p, idx: null, rank: 1, pos: pi[0] });
}
out.sort((a, b) => a.rank - b.rank || a.pos - b.pos || a.path.length - b.path.length);
return out;
}, [q]);
// flat list of content hits for keyboard nav
const flat = useMemo(() => {
const arr = [];
content.forEach((g) => g.hits.forEach((h) => arr.push({ path: g.path, no: h.no })));
return arr;
}, [content]);
const totalHits = flat.length;
useEffect(() => { setSel(0); }, [q]);
useEffect(() => {
const el = leftRef.current && leftRef.current.querySelector(".sr-line.sel");
if (el) el.scrollIntoView({ block: "nearest" });
}, [sel]);
function onKey(e) {
if (e.key === "ArrowDown") { e.preventDefault(); setSel((s) => Math.min(s + 1, flat.length - 1)); }
else if (e.key === "ArrowUp") { e.preventDefault(); setSel((s) => Math.max(s - 1, 0)); }
else if (e.key === "Enter") {
e.preventDefault();
if (flat[sel]) { onOpenAt(flat[sel].path, flat[sel].no); onClose(); }
else if (files[0]) { onOpen(files[0].path); onClose(); }
} else if (e.key === "Escape") { e.preventDefault(); onClose(); }
}
function renderLine(ln, ix, len) {
const pre = ln.slice(0, ix), mid = ln.slice(ix, ix + len), post = ln.slice(ix + len);
return {pre}{mid}{post};
}
const term = q.trim();
let flatIx = -1;
return (
e.stopPropagation()}>
{Icon.search({ style: { color: "var(--fg-3)" } })}
setQ(e.target.value)} onKeyDown={onKey}
placeholder="Search content and file names…" spellCheck={false} />
{totalHits} hit{totalHits === 1 ? "" : "s"} · {files.length} file{files.length === 1 ? "" : "s"}
Content {totalHits > 0 && {totalHits}}
{term.length < 2 &&
Type at least 2 characters
}
{term.length >= 2 && content.length === 0 &&
No content matches
}
{content.map((g) => (
onOpenAt(g.path, g.hits[0].no)}>
{g.path}
{g.hits.length}
{g.hits.slice(0, 12).map((h) => {
flatIx++;
const me = flatIx;
return (
setSel(me)}
onClick={() => { onOpenAt(g.path, h.no); onClose(); }}>
{h.no}
{renderLine(h.ln, h.ix, term.length)}
);
})}
))}
Files {files.length > 0 && {files.length}}
{!term &&
Start typing…
}
{term && files.length === 0 &&
No file names match
}
{files.slice(0, 40).map((r) => {
const name = r.path.split("/").pop();
const dir = r.path.split("/").slice(0, -1).join("/");
return (
{ onOpen(r.path); onClose(); }} title={r.path}>
{dir && {dir}/}
{changeSet.has(r.path) &&
•}
);
})}
);
}
function ContextMenu({ menu, onClose }) {
const ref = useRef(null);
useEffect(() => {
const h = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
const k = (e) => { if (e.key === "Escape") onClose(); };
document.addEventListener("mousedown", h);
document.addEventListener("keydown", k);
return () => { document.removeEventListener("mousedown", h); document.removeEventListener("keydown", k); };
}, []);
if (!menu) return null;
const x = Math.min(menu.x, window.innerWidth - 270);
const y = Math.min(menu.y, window.innerHeight - (menu.items.length * 34 + 60));
return (
{menu.note &&
{menu.note}
}
{menu.items.map((it, i) => it.sep ?
: (
{ it.onClick(); onClose(); }}>
{it.icon}
{it.label}
{it.kbd && {it.kbd}}
))}
);
}
function Toasts({ toasts }) {
return (
{toasts.map((t) => (
{Icon.copy({ style: { color: "var(--accent)" } })}
{t.title}
{t.ref && {t.ref}}
))}
);
}
function PassPopup({ x, y, refStr, onConfirm, onCancel }) {
const [text, setText] = useState("");
const inputRef = useRef(null);
const boxRef = useRef(null);
useEffect(() => { inputRef.current && inputRef.current.focus(); }, []);
useEffect(() => {
const h = (e) => { if (boxRef.current && !boxRef.current.contains(e.target)) onCancel(); };
const k = (e) => { if (e.key === "Escape") { e.preventDefault(); onCancel(); } };
document.addEventListener("mousedown", h);
document.addEventListener("keydown", k, true);
return () => { document.removeEventListener("mousedown", h); document.removeEventListener("keydown", k, true); };
}, []);
const left = Math.min(x, window.innerWidth - 360);
const top = Math.min(y + 6, window.innerHeight - 150);
const preview = (text.trim() ? text.trim() + " " : "") + refStr;
return (
{Icon.spark({})}Pass on to Agentesc
setText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") { e.preventDefault(); onConfirm(text); }
else if (e.key === "Escape") { e.preventDefault(); onCancel(); }
}} />
inserts{preview}
↵ insert into agent · esc cancel
);
}
Object.assign(window, { SearchModal, ContextMenu, Toasts, PassPopup, fuzzy });