Spaces:
Running
Search inside article bodies from the top-left box; ⌘K focuses it
Browse filesThe top-left search now matches article *content*, not just titles.
- matchPage also tests each article's body, indexed for free during the existing ensureCitemap() background pass (state.searchText — no new fetches). A body-only match shows a one-line highlighted snippet under the topic so it's clear why and where it matched.
- Responsive: typing kicks off indexing immediately and results fill in as articles get indexed.
- Cmd/Ctrl-K (and '/') and the Cmd-K badge now just focus the top-left search — one box for everything. The separate command palette (which felt broken) is removed entirely: DOM, JS (paletteResults / renderPalette / fuzzyScore / buildPaletteIndex / open-close-palette / palKeydown), and CSS.
Verified in-browser: 48 article bodies indexed; 'variance' surfaces 19 topics each with a bolded snippet (e.g. MDP Formulation, whose title lacks the word); title search still works; Cmd-K / '/' / badge all focus the box.
- app.js +46 -77
- index.html +2 -15
- styles.css +9 -48
|
@@ -26,10 +26,8 @@ const state = {
|
|
| 26 |
collapsed: new Set(JSON.parse(localStorage.getItem('viz-collapsed') || '[]')),
|
| 27 |
treeCollapsed: new Set(JSON.parse(localStorage.getItem('viz-tree-collapsed') || '[]')),
|
| 28 |
srcNs: null, // selected source-namespace filter (null = all)
|
| 29 |
-
palItems: [], // command-palette index
|
| 30 |
-
palFiltered: [],
|
| 31 |
-
palSel: 0,
|
| 32 |
pageContent: new Map(), // path -> article markdown (cached)
|
|
|
|
| 33 |
citedBy: new Map(), // sourceId -> Set(topic path) that cite it
|
| 34 |
citeCount: new Map(), // sourceId -> # distinct articles citing it
|
| 35 |
wordCount: new Map(), // topic path -> prose word count
|
|
@@ -957,7 +955,9 @@ function renderTopicsNav(list, q) {
|
|
| 957 |
matches.forEach(n => {
|
| 958 |
const it = el('div', 'nav-item');
|
| 959 |
it.dataset.path = n.page.path;
|
| 960 |
-
|
|
|
|
|
|
|
| 961 |
it.addEventListener('click', () => go(`#/topic/${encodeURIComponent(n.page.path)}`));
|
| 962 |
body.append(it);
|
| 963 |
});
|
|
@@ -1049,7 +1049,36 @@ function sortSources(items, mode) {
|
|
| 1049 |
else arr.sort((a, b) => (state.citeCount.get(b.id) || 0) - (state.citeCount.get(a.id) || 0) || (a.title || a.id).localeCompare(b.title || b.id));
|
| 1050 |
return arr;
|
| 1051 |
}
|
| 1052 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1053 |
const matchSource = (s, q) => (s.title || '').toLowerCase().includes(q) || s.id.toLowerCase().includes(q);
|
| 1054 |
function toggleCat(cat) {
|
| 1055 |
if (state.collapsed.has(cat)) state.collapsed.delete(cat); else state.collapsed.add(cat);
|
|
@@ -1071,30 +1100,6 @@ function highlightActive() {
|
|
| 1071 |
Filters an in-memory index, so it stays instant at thousands of items.
|
| 1072 |
============================================================ */
|
| 1073 |
// `text` and `q` must already be lowercased (callers pre-lower for speed).
|
| 1074 |
-
function fuzzyScore(q, text) {
|
| 1075 |
-
const idx = text.indexOf(q);
|
| 1076 |
-
if (idx === 0) return 1000; // prefix match — best
|
| 1077 |
-
if (idx > 0) return 700 - Math.min(idx, 300);
|
| 1078 |
-
// subsequence fallback — start at the first occurrence of q[0]; bail cheaply if absent.
|
| 1079 |
-
let start = text.indexOf(q[0]);
|
| 1080 |
-
if (start < 0) return -1;
|
| 1081 |
-
let qi = 1, gaps = 0, last = start;
|
| 1082 |
-
for (let ti = start + 1; ti < text.length && qi < q.length; ti++) {
|
| 1083 |
-
if (text[ti] === q[qi]) { gaps += ti - last - 1; last = ti; qi++; }
|
| 1084 |
-
}
|
| 1085 |
-
return qi === q.length ? 300 - Math.min(gaps, 200) - Math.min(start, 50) : -1;
|
| 1086 |
-
}
|
| 1087 |
-
function paletteResults(raw) {
|
| 1088 |
-
const q = raw.toLowerCase().trim();
|
| 1089 |
-
if (!q) return state.palItems.slice(0, 40);
|
| 1090 |
-
const scored = [];
|
| 1091 |
-
for (const it of state.palItems) {
|
| 1092 |
-
const s = Math.max(fuzzyScore(q, it._l), fuzzyScore(q, it._s) - 60);
|
| 1093 |
-
if (s > -1) scored.push({ it, s });
|
| 1094 |
-
}
|
| 1095 |
-
scored.sort((a, b) => b.s - a.s || a.it.label.localeCompare(b.it.label));
|
| 1096 |
-
return scored.slice(0, 40).map(x => x.it);
|
| 1097 |
-
}
|
| 1098 |
function highlightMatch(text, raw) {
|
| 1099 |
const q = raw.trim();
|
| 1100 |
if (!q) return esc(text);
|
|
@@ -1102,37 +1107,6 @@ function highlightMatch(text, raw) {
|
|
| 1102 |
if (i < 0) return esc(text);
|
| 1103 |
return esc(text.slice(0, i)) + '<b>' + esc(text.slice(i, i + q.length)) + '</b>' + esc(text.slice(i + q.length));
|
| 1104 |
}
|
| 1105 |
-
function renderPalette() {
|
| 1106 |
-
const q = $('#palInput').value;
|
| 1107 |
-
state.palFiltered = paletteResults(q);
|
| 1108 |
-
if (state.palSel >= state.palFiltered.length) state.palSel = 0;
|
| 1109 |
-
const box = $('#palResults');
|
| 1110 |
-
if (!state.palFiltered.length) { box.innerHTML = `<div class="pal-empty">No matches for “${esc(q)}”.</div>`; return; }
|
| 1111 |
-
box.innerHTML = state.palFiltered.map((it, i) => `
|
| 1112 |
-
<div class="pal-item${i === state.palSel ? ' sel' : ''}" data-i="${i}">
|
| 1113 |
-
<span class="pal-kind ${it.kind}">${it.kind}</span>
|
| 1114 |
-
<span class="pal-text"><span class="pal-label">${highlightMatch(it.label, q)}</span><span class="pal-sub">${esc(it.sub)}</span></span>
|
| 1115 |
-
</div>`).join('');
|
| 1116 |
-
$$('#palResults .pal-item').forEach(n => {
|
| 1117 |
-
n.addEventListener('mousemove', () => { if (state.palSel !== +n.dataset.i) { state.palSel = +n.dataset.i; markPalSel(); } });
|
| 1118 |
-
n.addEventListener('click', () => choosePal(+n.dataset.i));
|
| 1119 |
-
});
|
| 1120 |
-
}
|
| 1121 |
-
function markPalSel() { $$('#palResults .pal-item').forEach((n, i) => n.classList.toggle('sel', i === state.palSel)); }
|
| 1122 |
-
function scrollPalSel() { $$('#palResults .pal-item')[state.palSel]?.scrollIntoView({ block: 'nearest' }); }
|
| 1123 |
-
function choosePal(i) { const it = state.palFiltered[i]; if (it) { closePalette(); go(it.hash); } }
|
| 1124 |
-
function openPalette() {
|
| 1125 |
-
const sc = $('#palScrim'); sc.hidden = false;
|
| 1126 |
-
const inp = $('#palInput'); inp.value = ''; state.palSel = 0;
|
| 1127 |
-
renderPalette(); inp.focus();
|
| 1128 |
-
}
|
| 1129 |
-
function closePalette() { $('#palScrim').hidden = true; }
|
| 1130 |
-
function palKeydown(e) {
|
| 1131 |
-
if (e.key === 'ArrowDown') { e.preventDefault(); state.palSel = Math.min(state.palSel + 1, state.palFiltered.length - 1); markPalSel(); scrollPalSel(); }
|
| 1132 |
-
else if (e.key === 'ArrowUp') { e.preventDefault(); state.palSel = Math.max(state.palSel - 1, 0); markPalSel(); scrollPalSel(); }
|
| 1133 |
-
else if (e.key === 'Enter') { e.preventDefault(); choosePal(state.palSel); }
|
| 1134 |
-
else if (e.key === 'Escape') { e.preventDefault(); closePalette(); }
|
| 1135 |
-
}
|
| 1136 |
|
| 1137 |
/* ── mobile nav ────────────────────────────────────────── */
|
| 1138 |
function closeNav() { document.body.classList.remove('nav-open'); }
|
|
@@ -1181,7 +1155,6 @@ async function loadCore() {
|
|
| 1181 |
}
|
| 1182 |
try { state.taxonomy = taxT && window.jsyaml ? jsyaml.load(taxT) : null; } catch (e) { state.taxonomy = null; }
|
| 1183 |
buildTaxonomy();
|
| 1184 |
-
buildPaletteIndex();
|
| 1185 |
}
|
| 1186 |
const SOURCES_CACHE_KEY = 'viz-sources-v1';
|
| 1187 |
function setSources(items) {
|
|
@@ -1192,7 +1165,7 @@ function setSources(items) {
|
|
| 1192 |
if (items.some(s => s.title)) state.sourceTitlesLoaded = true; // tree stage has none yet
|
| 1193 |
}
|
| 1194 |
function refreshSourceViews() {
|
| 1195 |
-
|
| 1196 |
softRefresh(); // re-renders the current view (incl. the sources browser), upgrading titles/resolution
|
| 1197 |
}
|
| 1198 |
// Reverse the filename sanitization (`:`/`/` → `-`): `arxiv-1707.06347.md` → `arxiv:1707.06347`.
|
|
@@ -1273,13 +1246,6 @@ function buildTaxonomy() {
|
|
| 1273 |
state.tax = cats;
|
| 1274 |
}
|
| 1275 |
|
| 1276 |
-
function buildPaletteIndex() {
|
| 1277 |
-
const mk = (kind, label, sub, hash) => ({ kind, label, sub, hash, _l: label.toLowerCase(), _s: sub.toLowerCase() });
|
| 1278 |
-
state.palItems = [
|
| 1279 |
-
...state.pages.map(p => mk('topic', p.title || p.path, catLabel(p._cat || p.parent || ''), `#/topic/${encodeURIComponent(p.path)}`)),
|
| 1280 |
-
...state.sources.map(s => mk('source', s.title || s.id, s.id, `#/source/${encodeURIComponent(s.id)}`)),
|
| 1281 |
-
];
|
| 1282 |
-
}
|
| 1283 |
const catLabel = (c) => prettyTitle((c || '').replace(/-/g, ' '));
|
| 1284 |
// Sentence-cased category for headings, e.g. "reward-modeling" → "Reward modeling".
|
| 1285 |
const catDisplay = (c) => { const s = catLabel(c); return s.charAt(0).toUpperCase() + s.slice(1); };
|
|
@@ -1327,6 +1293,8 @@ async function ensureCitemap() {
|
|
| 1327 |
ids.forEach(id => { if (!state.citedBy.has(id)) state.citedBy.set(id, new Set()); state.citedBy.get(id).add(path); });
|
| 1328 |
state.refCount.set(path, refs);
|
| 1329 |
state.wordCount.set(path, countWords(content));
|
|
|
|
|
|
|
| 1330 |
} catch (e) { /* skip unreadable article */ }
|
| 1331 |
}
|
| 1332 |
};
|
|
@@ -1379,18 +1347,19 @@ async function boot() {
|
|
| 1379 |
$('#navScrim').addEventListener('click', closeNav);
|
| 1380 |
$('#brandHome').addEventListener('click', () => go('#/'));
|
| 1381 |
let st;
|
| 1382 |
-
$('#navSearch').addEventListener('input', () => {
|
|
|
|
|
|
|
|
|
|
| 1383 |
window.addEventListener('hashchange', route);
|
| 1384 |
|
| 1385 |
-
//
|
| 1386 |
-
$('#
|
| 1387 |
-
$('#
|
| 1388 |
-
$('#palInput').addEventListener('keydown', palKeydown);
|
| 1389 |
-
$('#palScrim').addEventListener('click', (e) => { if (e.target === $('#palScrim')) closePalette(); });
|
| 1390 |
window.addEventListener('keydown', (e) => {
|
| 1391 |
-
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault();
|
| 1392 |
const typing = /^(input|textarea)$/i.test(document.activeElement?.tagName || '');
|
| 1393 |
-
if (e.key === '/' && !typing
|
| 1394 |
});
|
| 1395 |
initHovercards();
|
| 1396 |
|
|
|
|
| 26 |
collapsed: new Set(JSON.parse(localStorage.getItem('viz-collapsed') || '[]')),
|
| 27 |
treeCollapsed: new Set(JSON.parse(localStorage.getItem('viz-tree-collapsed') || '[]')),
|
| 28 |
srcNs: null, // selected source-namespace filter (null = all)
|
|
|
|
|
|
|
|
|
|
| 29 |
pageContent: new Map(), // path -> article markdown (cached)
|
| 30 |
+
searchText: new Map(), // topic path -> lowercased body, for nav content search
|
| 31 |
citedBy: new Map(), // sourceId -> Set(topic path) that cite it
|
| 32 |
citeCount: new Map(), // sourceId -> # distinct articles citing it
|
| 33 |
wordCount: new Map(), // topic path -> prose word count
|
|
|
|
| 955 |
matches.forEach(n => {
|
| 956 |
const it = el('div', 'nav-item');
|
| 957 |
it.dataset.path = n.page.path;
|
| 958 |
+
const snip = q && bodyOnlyMatch(n.page, q) ? bodySnippet(n.page.path, q) : '';
|
| 959 |
+
it.innerHTML = `<span class="nav-item-title">${esc(n.page.title || n.slug)}</span>` +
|
| 960 |
+
(snip ? `<span class="nav-snippet">${snip}</span>` : '');
|
| 961 |
it.addEventListener('click', () => go(`#/topic/${encodeURIComponent(n.page.path)}`));
|
| 962 |
body.append(it);
|
| 963 |
});
|
|
|
|
| 1049 |
else arr.sort((a, b) => (state.citeCount.get(b.id) || 0) - (state.citeCount.get(a.id) || 0) || (a.title || a.id).localeCompare(b.title || b.id));
|
| 1050 |
return arr;
|
| 1051 |
}
|
| 1052 |
+
// A page matches on its title, its path, or anywhere in its (background-indexed)
|
| 1053 |
+
// body. `q` is already lowercased by callers.
|
| 1054 |
+
const matchPage = (p, q) =>
|
| 1055 |
+
(p.title || '').toLowerCase().includes(q) ||
|
| 1056 |
+
p.path.toLowerCase().includes(q) ||
|
| 1057 |
+
(state.searchText.get(p.path) || '').includes(q);
|
| 1058 |
+
// True when *only* the body matched — those items get a snippet so it's clear why.
|
| 1059 |
+
const bodyOnlyMatch = (p, q) =>
|
| 1060 |
+
!(p.title || '').toLowerCase().includes(q) && !p.path.toLowerCase().includes(q) &&
|
| 1061 |
+
(state.searchText.get(p.path) || '').includes(q);
|
| 1062 |
+
// A one-line excerpt around the first body hit, lightly de-marked, query bolded.
|
| 1063 |
+
function bodySnippet(path, q) {
|
| 1064 |
+
const text = state.pageContent.get(path), lc = state.searchText.get(path);
|
| 1065 |
+
if (!text || !lc) return '';
|
| 1066 |
+
const i = lc.indexOf(q);
|
| 1067 |
+
if (i < 0) return '';
|
| 1068 |
+
const start = Math.max(0, i - 30);
|
| 1069 |
+
let clip = text.slice(start, i + q.length + 40)
|
| 1070 |
+
.replace(/\[source:[^\]]*\]/g, ' ')
|
| 1071 |
+
.replace(/[#>*_`~|]+/g, ' ')
|
| 1072 |
+
.replace(/\s+/g, ' ').trim();
|
| 1073 |
+
clip = (start > 0 ? '…' : '') + clip + '…';
|
| 1074 |
+
return highlightMatch(clip, q);
|
| 1075 |
+
}
|
| 1076 |
+
// Throttled nav re-render, used while background indexing fills in during a search.
|
| 1077 |
+
let _navRefreshT = null;
|
| 1078 |
+
function scheduleNavRefresh() {
|
| 1079 |
+
if (_navRefreshT) return;
|
| 1080 |
+
_navRefreshT = setTimeout(() => { _navRefreshT = null; if (($('#navSearch').value || '').trim()) renderNav(); }, 200);
|
| 1081 |
+
}
|
| 1082 |
const matchSource = (s, q) => (s.title || '').toLowerCase().includes(q) || s.id.toLowerCase().includes(q);
|
| 1083 |
function toggleCat(cat) {
|
| 1084 |
if (state.collapsed.has(cat)) state.collapsed.delete(cat); else state.collapsed.add(cat);
|
|
|
|
| 1100 |
Filters an in-memory index, so it stays instant at thousands of items.
|
| 1101 |
============================================================ */
|
| 1102 |
// `text` and `q` must already be lowercased (callers pre-lower for speed).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1103 |
function highlightMatch(text, raw) {
|
| 1104 |
const q = raw.trim();
|
| 1105 |
if (!q) return esc(text);
|
|
|
|
| 1107 |
if (i < 0) return esc(text);
|
| 1108 |
return esc(text.slice(0, i)) + '<b>' + esc(text.slice(i, i + q.length)) + '</b>' + esc(text.slice(i + q.length));
|
| 1109 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1110 |
|
| 1111 |
/* ── mobile nav ────────────────────────────────────────── */
|
| 1112 |
function closeNav() { document.body.classList.remove('nav-open'); }
|
|
|
|
| 1155 |
}
|
| 1156 |
try { state.taxonomy = taxT && window.jsyaml ? jsyaml.load(taxT) : null; } catch (e) { state.taxonomy = null; }
|
| 1157 |
buildTaxonomy();
|
|
|
|
| 1158 |
}
|
| 1159 |
const SOURCES_CACHE_KEY = 'viz-sources-v1';
|
| 1160 |
function setSources(items) {
|
|
|
|
| 1165 |
if (items.some(s => s.title)) state.sourceTitlesLoaded = true; // tree stage has none yet
|
| 1166 |
}
|
| 1167 |
function refreshSourceViews() {
|
| 1168 |
+
updateCounts();
|
| 1169 |
softRefresh(); // re-renders the current view (incl. the sources browser), upgrading titles/resolution
|
| 1170 |
}
|
| 1171 |
// Reverse the filename sanitization (`:`/`/` → `-`): `arxiv-1707.06347.md` → `arxiv:1707.06347`.
|
|
|
|
| 1246 |
state.tax = cats;
|
| 1247 |
}
|
| 1248 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1249 |
const catLabel = (c) => prettyTitle((c || '').replace(/-/g, ' '));
|
| 1250 |
// Sentence-cased category for headings, e.g. "reward-modeling" → "Reward modeling".
|
| 1251 |
const catDisplay = (c) => { const s = catLabel(c); return s.charAt(0).toUpperCase() + s.slice(1); };
|
|
|
|
| 1293 |
ids.forEach(id => { if (!state.citedBy.has(id)) state.citedBy.set(id, new Set()); state.citedBy.get(id).add(path); });
|
| 1294 |
state.refCount.set(path, refs);
|
| 1295 |
state.wordCount.set(path, countWords(content));
|
| 1296 |
+
state.searchText.set(path, content.toLowerCase()); // for nav content search
|
| 1297 |
+
if (($('#navSearch').value || '').trim()) scheduleNavRefresh(); // live-fill an in-progress search
|
| 1298 |
} catch (e) { /* skip unreadable article */ }
|
| 1299 |
}
|
| 1300 |
};
|
|
|
|
| 1347 |
$('#navScrim').addEventListener('click', closeNav);
|
| 1348 |
$('#brandHome').addEventListener('click', () => go('#/'));
|
| 1349 |
let st;
|
| 1350 |
+
$('#navSearch').addEventListener('input', () => {
|
| 1351 |
+
ensureCitemap(); // kick off body indexing on first keystroke so content matches fill in fast
|
| 1352 |
+
clearTimeout(st); st = setTimeout(renderNav, 120);
|
| 1353 |
+
});
|
| 1354 |
window.addEventListener('hashchange', route);
|
| 1355 |
|
| 1356 |
+
// ⌘K (and "/") focus the top-left search — one box that filters titles + bodies.
|
| 1357 |
+
const focusNavSearch = () => { const s = $('#navSearch'); s.focus(); s.select(); };
|
| 1358 |
+
$('#paletteOpen').addEventListener('click', focusNavSearch);
|
|
|
|
|
|
|
| 1359 |
window.addEventListener('keydown', (e) => {
|
| 1360 |
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); focusNavSearch(); return; }
|
| 1361 |
const typing = /^(input|textarea)$/i.test(document.activeElement?.tagName || '');
|
| 1362 |
+
if (e.key === '/' && !typing) { e.preventDefault(); focusNavSearch(); }
|
| 1363 |
});
|
| 1364 |
initHovercards();
|
| 1365 |
|
|
@@ -63,8 +63,8 @@
|
|
| 63 |
<aside class="sidebar" id="sidebar">
|
| 64 |
<div class="side-head">Topics</div>
|
| 65 |
<div class="search-wrap">
|
| 66 |
-
<input class="search" id="navSearch" type="text" placeholder="Search topics…" autocomplete="off">
|
| 67 |
-
<kbd class="search-k" id="paletteOpen" title="
|
| 68 |
</div>
|
| 69 |
<nav id="navList"></nav>
|
| 70 |
</aside>
|
|
@@ -76,19 +76,6 @@
|
|
| 76 |
<aside class="aside" id="aside"></aside>
|
| 77 |
</div>
|
| 78 |
|
| 79 |
-
<!-- Command palette: universal jump to any topic or source -->
|
| 80 |
-
<div class="palette-scrim" id="palScrim" hidden>
|
| 81 |
-
<div class="palette" role="dialog" aria-label="Search">
|
| 82 |
-
<div class="palette-search">
|
| 83 |
-
<span class="pal-icon">⌕</span>
|
| 84 |
-
<input id="palInput" class="palette-input" type="text" placeholder="Jump to a topic or source…" autocomplete="off" spellcheck="false">
|
| 85 |
-
<kbd class="pal-esc">esc</kbd>
|
| 86 |
-
</div>
|
| 87 |
-
<div id="palResults" class="palette-results"></div>
|
| 88 |
-
<div class="palette-foot"><span><kbd>↑</kbd><kbd>↓</kbd> navigate</span><span><kbd>↵</kbd> open</span><span><span class="pal-dot pal-t"></span> topic <span class="pal-dot pal-s"></span> source</span></div>
|
| 89 |
-
</div>
|
| 90 |
-
</div>
|
| 91 |
-
|
| 92 |
<script defer src="app.js"></script>
|
| 93 |
</body>
|
| 94 |
</html>
|
|
|
|
| 63 |
<aside class="sidebar" id="sidebar">
|
| 64 |
<div class="side-head">Topics</div>
|
| 65 |
<div class="search-wrap">
|
| 66 |
+
<input class="search" id="navSearch" type="text" placeholder="Search topics & text…" autocomplete="off">
|
| 67 |
+
<kbd class="search-k" id="paletteOpen" title="Focus search (⌘K)">⌘K</kbd>
|
| 68 |
</div>
|
| 69 |
<nav id="navList"></nav>
|
| 70 |
</aside>
|
|
|
|
| 76 |
<aside class="aside" id="aside"></aside>
|
| 77 |
</div>
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
<script defer src="app.js"></script>
|
| 80 |
</body>
|
| 81 |
</html>
|
|
@@ -159,7 +159,7 @@ a:hover { color: var(--accent-deep); }
|
|
| 159 |
content: "⌕"; position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
|
| 160 |
color: var(--muted-3); font-size: 15px;
|
| 161 |
}
|
| 162 |
-
/* the
|
| 163 |
.search-k {
|
| 164 |
position: absolute; right: 7px; top: 50%; transform: translateY(-50%);
|
| 165 |
font-family: var(--mono); font-size: 10px; padding: 2px 6px; border-radius: 4px;
|
|
@@ -191,6 +191,13 @@ a:hover { color: var(--accent-deep); }
|
|
| 191 |
display: block; font-family: var(--mono); font-size: 9.5px;
|
| 192 |
color: var(--muted-4); margin-top: 1px; letter-spacing: 0.2px;
|
| 193 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
.nav-empty { color: var(--muted-4); font-size: 11.5px; padding: 8px; font-style: italic; }
|
| 195 |
.mini-spin { display: inline-block; width: 10px; height: 10px; vertical-align: -1px; margin-right: 4px;
|
| 196 |
border: 1.5px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .7s linear infinite; }
|
|
@@ -560,52 +567,6 @@ a:hover { color: var(--accent-deep); }
|
|
| 560 |
}
|
| 561 |
@media (min-width: 901px) { .nav-scrim { display: none !important; } }
|
| 562 |
|
| 563 |
-
/* ── command palette ───────────────────────────────────── */
|
| 564 |
-
.palette-foot kbd, .pal-esc {
|
| 565 |
-
font-family: var(--mono); font-size: 10px; padding: 1.5px 5px; border-radius: 4px;
|
| 566 |
-
border: 1px solid var(--border); background: var(--bg-soft); color: var(--muted-2); line-height: 1.4;
|
| 567 |
-
}
|
| 568 |
-
|
| 569 |
-
.palette-scrim {
|
| 570 |
-
position: fixed; inset: 0; z-index: 200; background: rgba(15,18,22,0.42);
|
| 571 |
-
backdrop-filter: blur(2px); display: flex; align-items: flex-start; justify-content: center;
|
| 572 |
-
padding: 12vh 16px 16px;
|
| 573 |
-
}
|
| 574 |
-
.palette-scrim[hidden] { display: none; }
|
| 575 |
-
.palette {
|
| 576 |
-
width: 100%; max-width: 580px; background: var(--bg-card); border: 1px solid var(--border);
|
| 577 |
-
border-radius: 12px; box-shadow: 0 12px 48px rgba(0,0,0,0.28); overflow: hidden;
|
| 578 |
-
display: flex; flex-direction: column; max-height: 70vh;
|
| 579 |
-
}
|
| 580 |
-
.palette-search { display: flex; align-items: center; gap: 10px; padding: 12px 15px; border-bottom: 1px solid var(--border); }
|
| 581 |
-
.pal-icon { color: var(--muted-3); font-size: 17px; }
|
| 582 |
-
.palette-input {
|
| 583 |
-
flex: 1; border: none; background: none; outline: none; color: var(--ink);
|
| 584 |
-
font-family: var(--sans); font-size: 15px; font-weight: 300;
|
| 585 |
-
}
|
| 586 |
-
.palette-results { overflow-y: auto; padding: 6px; }
|
| 587 |
-
.pal-item {
|
| 588 |
-
display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 7px; cursor: pointer;
|
| 589 |
-
}
|
| 590 |
-
.pal-item.sel { background: var(--accent-soft); }
|
| 591 |
-
.pal-kind {
|
| 592 |
-
flex: 0 0 auto; font-family: var(--mono); font-size: 9px; font-weight: 600; text-transform: uppercase;
|
| 593 |
-
letter-spacing: 0.5px; padding: 2px 6px; border-radius: 4px; width: 50px; text-align: center;
|
| 594 |
-
}
|
| 595 |
-
.pal-kind.topic { background: var(--accent-soft); color: var(--accent-deep); }
|
| 596 |
-
.pal-kind.source { background: var(--bg-soft); color: var(--muted-2); border: 1px solid var(--border-soft); }
|
| 597 |
-
.pal-text { flex: 1; min-width: 0; }
|
| 598 |
-
.pal-label { font-size: 13.5px; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 599 |
-
.pal-label b { color: var(--accent-deep); font-weight: 600; }
|
| 600 |
-
.pal-sub { font-family: var(--mono); font-size: 10px; color: var(--muted-4); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 601 |
-
.pal-empty { padding: 24px; text-align: center; color: var(--muted-3); font-size: 13px; }
|
| 602 |
-
.palette-foot {
|
| 603 |
-
display: flex; gap: 14px; align-items: center; padding: 8px 14px; border-top: 1px solid var(--border-soft);
|
| 604 |
-
font-size: 10.5px; color: var(--muted-3); font-family: var(--mono); flex-wrap: wrap;
|
| 605 |
-
}
|
| 606 |
-
.pal-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; vertical-align: middle; margin: 0 2px 0 6px; }
|
| 607 |
-
.pal-dot.pal-t { background: var(--accent); }
|
| 608 |
-
.pal-dot.pal-s { background: var(--muted-4); }
|
| 609 |
|
| 610 |
/* ── Collapsible taxonomy tree (topics sidebar) ────────── */
|
| 611 |
.nav-cat { margin-bottom: 10px; }
|
|
@@ -779,7 +740,7 @@ body.book-mode .main { padding: 0; }
|
|
| 779 |
|
| 780 |
/* ── Print (browser "Save as PDF") ─────────────────────── */
|
| 781 |
@media print {
|
| 782 |
-
.topbar, .sidebar, .aside, .nav-scrim, .book-toolbar,
|
| 783 |
html, body { background: #fff; color: #000; }
|
| 784 |
.layout, .main, body.book-mode .main { display: block; padding: 0; margin: 0; }
|
| 785 |
.book { max-width: none; margin: 0; padding: 0; color: #111; font-size: 10.5pt; }
|
|
|
|
| 159 |
content: "⌕"; position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
|
| 160 |
color: var(--muted-3); font-size: 15px;
|
| 161 |
}
|
| 162 |
+
/* the ⌘K badge on the search box: click (or ⌘K) → focus the search input */
|
| 163 |
.search-k {
|
| 164 |
position: absolute; right: 7px; top: 50%; transform: translateY(-50%);
|
| 165 |
font-family: var(--mono); font-size: 10px; padding: 2px 6px; border-radius: 4px;
|
|
|
|
| 191 |
display: block; font-family: var(--mono); font-size: 9.5px;
|
| 192 |
color: var(--muted-4); margin-top: 1px; letter-spacing: 0.2px;
|
| 193 |
}
|
| 194 |
+
/* content-search: a matched-body excerpt under the topic name, query bolded */
|
| 195 |
+
.nav-item .nav-item-title { display: block; }
|
| 196 |
+
.nav-item .nav-snippet {
|
| 197 |
+
display: block; margin-top: 2px; font-size: 11px; line-height: 1.4;
|
| 198 |
+
color: var(--muted-3); font-weight: 400;
|
| 199 |
+
}
|
| 200 |
+
.nav-item .nav-snippet b { color: var(--accent-deep); font-weight: 600; }
|
| 201 |
.nav-empty { color: var(--muted-4); font-size: 11.5px; padding: 8px; font-style: italic; }
|
| 202 |
.mini-spin { display: inline-block; width: 10px; height: 10px; vertical-align: -1px; margin-right: 4px;
|
| 203 |
border: 1.5px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .7s linear infinite; }
|
|
|
|
| 567 |
}
|
| 568 |
@media (min-width: 901px) { .nav-scrim { display: none !important; } }
|
| 569 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
|
| 571 |
/* ── Collapsible taxonomy tree (topics sidebar) ────────── */
|
| 572 |
.nav-cat { margin-bottom: 10px; }
|
|
|
|
| 740 |
|
| 741 |
/* ── Print (browser "Save as PDF") ─────────────────────── */
|
| 742 |
@media print {
|
| 743 |
+
.topbar, .sidebar, .aside, .nav-scrim, .book-toolbar, #hovercard, #toast, .foot, .menu-toggle { display: none !important; }
|
| 744 |
html, body { background: #fff; color: #000; }
|
| 745 |
.layout, .main, body.book-mode .main { display: block; padding: 0; margin: 0; }
|
| 746 |
.book { max-width: none; margin: 0; padding: 0; color: #111; font-size: 10.5pt; }
|