feat(ui): Search+Full Book nav row under Home bar; search matches chapter titles

This commit is contained in:
avi 2026-09-22 17:18:16 -05:00
commit 6479656680
3 changed files with 92 additions and 30 deletions

View file

@ -95,27 +95,40 @@ export function createSearchFeature(ctx: BookCtx, editor: Editor): SearchFeature
void (async () => { void (async () => {
const list = await api.getChapterList(); const list = await api.getChapterList();
const chapters = Array.isArray(list) ? list : []; const chapters = Array.isArray(list) ? list : [];
const byChapter: { id: string; title: string; plain: string; positions: number[] }[] = []; const byChapter: { id: string; title: string; plain: string; positions: number[]; titleHits: number }[] = [];
let total = 0; let total = 0;
for (const ch of chapters) { for (const ch of chapters) {
if (total >= 200) break; if (total >= 200) break;
// Chapter titles count as matches too: a title hit links to the
// chapter itself (no in-body scroll position).
const title = ch.title || ch.id;
const titleHits = findMatches(title, term, matchCase).length;
const res = await api.getChapterContent(ch.id); const res = await api.getChapterContent(ch.id);
const md = typeof res === "string" ? res : ""; const md = typeof res === "string" ? res : "";
const plain = markdownToPlain(md); const plain = markdownToPlain(md);
const positions = findMatches(plain, term, matchCase); const positions = findMatches(plain, term, matchCase);
if (positions.length) { if (positions.length || titleHits) {
byChapter.push({ id: ch.id, title: ch.title || ch.id, plain, positions }); byChapter.push({ id: ch.id, title, plain, positions, titleHits });
total += positions.length; total += positions.length + titleHits;
} }
} }
searchMeta.textContent = total searchMeta.textContent = total
? `${total} match${total === 1 ? "" : "es"} in ${byChapter.length} chapter${byChapter.length === 1 ? "" : "s"}` ? `${total} match${total === 1 ? "" : "es"} in ${byChapter.length} chapter${byChapter.length === 1 ? "" : "s"}`
: "No matches."; : "No matches.";
for (const r of byChapter) { for (const r of byChapter) {
const head = document.createElement("li"); const head = document.createElement("li");
head.className = "search-chapter-head"; head.className = "search-chapter-head";
head.textContent = `${r.title} (${r.positions.length})`; const counts = [
searchResults.appendChild(head); r.titleHits ? `title: ${r.titleHits}` : "",
r.positions.length ? `text: ${r.positions.length}` : "",
].filter(Boolean).join(", ");
head.textContent = `${r.title} (${counts})`;
if (r.titleHits) {
head.classList.add("search-result");
head.title = "Open chapter";
head.addEventListener("click", () => void goToResult(r.id, 0, term, matchCase));
}
searchResults.appendChild(head);
for (const pos of r.positions) { for (const pos of r.positions) {
const li = document.createElement("li"); const li = document.createElement("li");
li.className = "search-result"; li.className = "search-result";

View file

@ -188,6 +188,13 @@ html, body {
margin-bottom: 10px; margin-bottom: 10px;
} }
.update-control {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 10px;
}
.backup-label { .backup-label {
font-size: 0.72rem; font-size: 0.72rem;
font-weight: 700; font-weight: 700;
@ -281,11 +288,24 @@ html, body {
word-break: break-all; word-break: break-all;
} }
/* ---- sidebar nav row (Search / Full Book, under the Home bar) ---- */
.sidebar-nav {
display: flex;
gap: 8px;
margin-bottom: var(--gap);
}
.sidebar-nav .nav-btn {
flex: 1;
min-width: 0;
width: auto;
}
/* ---- full book nav ---- */ /* ---- full book nav ---- */
.full-book-nav { .full-book-nav {
margin-top: var(--gap); display: flex;
padding-top: var(--gap); flex: 1;
border-top: 1px solid var(--color-border); min-width: 0;
} }
.full-book-btn { .full-book-btn {
@ -301,7 +321,8 @@ html, body {
padding: 6px 12px; padding: 6px 12px;
cursor: pointer; cursor: pointer;
transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease; transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
min-width: 100px; min-width: 0;
flex: 1;
} }
.full-book-btn:hover { .full-book-btn:hover {
@ -588,6 +609,47 @@ html, body {
color: var(--color-on-accent); color: var(--color-on-accent);
} }
.recent-list li.recent-item.selected {
background: var(--color-accent);
color: var(--color-on-accent);
outline: 1px solid var(--color-accent);
}
.recent-list li.recent-item:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: -2px;
}
/* Row actions appear only when the row is selected. */
.recent-item .recent-actions {
flex: 0 0 auto;
display: none;
align-items: center;
gap: 4px;
}
.recent-item.selected .recent-actions {
display: flex;
}
.recent-item .recent-action {
height: 20px;
line-height: 1;
font-size: 0.72rem;
padding: 0 6px;
border: none;
background: transparent;
color: inherit;
cursor: pointer;
border-radius: var(--radius);
opacity: 0.85;
}
.recent-item .recent-action:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.18);
}
.recent-item .recent-title { .recent-item .recent-title {
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
@ -597,24 +659,9 @@ html, body {
user-select: none; user-select: none;
} }
.recent-item .recent-delete {
flex: 0 0 auto;
width: 20px;
height: 20px;
line-height: 1;
font-size: 1rem;
padding: 0;
border: none;
background: transparent;
color: inherit;
cursor: pointer;
border-radius: var(--radius);
opacity: 0.6;
}
.recent-item .recent-delete:hover { .recent-item .recent-delete:hover {
opacity: 1; opacity: 1;
background: rgba(220, 53, 69, 0.18); background: rgba(220, 53, 69, 0.35);
color: #ff6b6b; color: #ff6b6b;
} }

View file

@ -24,12 +24,14 @@
<span class="book-title" id="bookTitle"></span> <span class="book-title" id="bookTitle"></span>
<span class="book-path" id="bookPath" title=""></span> <span class="book-path" id="bookPath" title=""></span>
</div> </div>
<ul class="chapter-list" id="chapterList"></ul> <div class="sidebar-nav">
<div class="full-book-nav hidden" id="fullBookNav"> <button class="button button-ghost nav-btn" id="searchBtn" title="Search chapter titles and text (Ctrl+F)">Search</button>
<button class="full-book-btn" id="fullBookBtn" title="View the entire book as one continuous manuscript">Full Book</button> <div class="full-book-nav hidden" id="fullBookNav">
<button class="full-book-btn nav-btn" id="fullBookBtn" title="View the entire book as one continuous manuscript">Full Book</button>
</div>
</div> </div>
<ul class="chapter-list" id="chapterList"></ul>
<div class="sidebar-footer"> <div class="sidebar-footer">
<button class="button button-ghost" id="searchBtn" title="Search all chapters (Ctrl+F)">Search</button>
<div class="theme-control"> <div class="theme-control">
<label class="theme-label" for="themeSelect">Theme</label> <label class="theme-label" for="themeSelect">Theme</label>
<select id="themeSelect" class="theme-select" title="Interface theme"> <select id="themeSelect" class="theme-select" title="Interface theme">