diff --git a/scripts/graph-viewer/sphere.template.html b/scripts/graph-viewer/sphere.template.html
index 0e46875..3e1312b 100644
--- a/scripts/graph-viewer/sphere.template.html
+++ b/scripts/graph-viewer/sphere.template.html
@@ -72,7 +72,8 @@
#hits{max-height:46vh;overflow-y:auto;border-top:1px solid var(--line)}
#hits:empty{display:none}
.hit{padding:7px 14px;cursor:pointer;font-size:12px;display:flex;align-items:baseline;gap:8px}
- .hit:hover{background:rgba(255,212,121,.1)}
+ .hit:hover,.hit.sel{background:rgba(255,212,121,.1)}
+ .hit.sel{box-shadow:inset 2px 0 0 var(--accent)}
.hit b{font-family:var(--mono);font-weight:500}
.hit i{font-style:normal;color:var(--fg-faint);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -846,15 +847,34 @@
Codebase sphere
document.getElementById('repo').onclick = () => { repoAt = (repoAt + 1) % repoKeys.length; load(repoKeys[repoAt]); };
const q = document.getElementById('q'), hits = document.getElementById('hits');
+// Separators (-, _, ., /) don't mean anything to a human typing a search —
+// "kb elicit" should find "kb-elicit.mjs" — so token matching normalizes
+// them all to spaces before comparing, on top of the plain substring tiers.
+const normSep = s => s.toLowerCase().replace(/[-_./]+/g, ' ');
+let selIdx = -1;
+function hitEls() { return Array.from(hits.children); }
+function setSel(idx) {
+ const els = hitEls();
+ if (!els.length) { selIdx = -1; return; }
+ selIdx = ((idx % els.length) + els.length) % els.length;
+ els.forEach((el, j) => el.classList.toggle('sel', j === selIdx));
+ els[selIdx].scrollIntoView({ block: 'nearest' });
+}
q.addEventListener('input', () => {
const v = q.value.trim().toLowerCase();
hits.innerHTML = '';
+ selIdx = -1;
if (!v) return;
+ const tokens = v.split(/\s+/).filter(Boolean);
const out = [];
for (let i = 0; i < M.n; i++) {
const f = M.nodes[i][0].toLowerCase(), p = M.nodes[i][1].toLowerCase();
let s = -1;
- if (f.startsWith(v)) s = 0; else if (f.includes(v)) s = 1; else if (p.includes(v)) s = 2;
+ if (f.startsWith(v)) s = 0;
+ else if (f.includes(v)) s = 1;
+ else if (p.includes(v)) s = 2;
+ else if (tokens.length > 1 && tokens.every(t => normSep(f).includes(t))) s = 3;
+ else if (tokens.every(t => normSep(p).includes(t))) s = 4;
if (s >= 0) out.push([s, -M.deg[i], i]);
}
out.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
@@ -865,10 +885,13 @@ Codebase sphere
el.onclick = () => { openFile(i); q.value = ''; hits.innerHTML = ''; q.blur(); };
hits.appendChild(el);
}
+ if (hits.children.length) setSel(0);
});
q.addEventListener('keydown', (e) => {
- if (e.key === 'Enter') { const f = hits.querySelector('.hit'); if (f) f.click(); }
- if (e.key === 'Escape') { q.value = ''; hits.innerHTML = ''; q.blur(); }
+ if (e.key === 'ArrowDown') { if (hits.children.length) { e.preventDefault(); setSel(selIdx < 0 ? 0 : selIdx + 1); } }
+ else if (e.key === 'ArrowUp') { if (hits.children.length) { e.preventDefault(); setSel(selIdx < 0 ? hits.children.length - 1 : selIdx - 1); } }
+ else if (e.key === 'Enter') { const f = hitEls()[selIdx] || hits.querySelector('.hit'); if (f) f.click(); }
+ else if (e.key === 'Escape') { q.value = ''; hits.innerHTML = ''; selIdx = -1; q.blur(); }
});
addEventListener('keydown', (e) => { if (e.key === 'Escape' && mode === 'map') exitMap(); });
diff --git a/src/graph/view-template.ts b/src/graph/view-template.ts
index 63157cd..83ff370 100644
--- a/src/graph/view-template.ts
+++ b/src/graph/view-template.ts
@@ -6,4 +6,4 @@
* `__TITLE__` (the tab title) and `__DATA_JSON__` (the baked graph payload).
* Shipped as a string because tsc does not copy non-.ts assets into dist.
*/
-export const GRAPH_TEMPLATE = "\n\n\n\n\n__TITLE__\n\n\n\n\n\n\n \n \n
Codebase sphere
\n
\n
files indexed—
\n
relations—
\n
directories—
\n
\n\n\n
\n
centre
\n
\n
\n
\n
\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n";
+export const GRAPH_TEMPLATE = "\n\n\n\n\n__TITLE__\n\n\n\n\n\n\n \n \n
Codebase sphere
\n
\n
files indexed—
\n
relations—
\n
directories—
\n
\n\n\n
\n
centre
\n
\n
\n
\n
\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n";