updated search

This commit is contained in:
2026-03-15 16:20:43 -07:00
parent dfc0027c31
commit b1d64f795b
3 changed files with 180 additions and 18 deletions

View File

@@ -3419,6 +3419,30 @@
linear-gradient(180deg, rgba(30, 27, 75, 0.22), rgba(16, 16, 24, 0.96));
}
.alpha-text-reader-toggle {
display: flex;
gap: 10px;
align-items: center;
color: #e4e4e7;
font-size: 13px;
line-height: 1.4;
cursor: pointer;
-webkit-user-select: none;
user-select: none;
}
.alpha-text-reader-toggle input {
width: 16px;
height: 16px;
margin: 0;
accent-color: #818cf8;
cursor: pointer;
}
.alpha-text-reader-toggle-control {
min-width: 0;
}
.alpha-text-search-controls--detail {
padding: 14px;
border: 1px solid #2f2f39;
@@ -3436,7 +3460,7 @@
.alpha-text-search-inline {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 8px;
align-items: stretch;
}
@@ -3536,16 +3560,15 @@
min-width: 0;
}
.planet-layout.alpha-text-global-search-only {
.planet-layout.alpha-text-global-search-only.layout-sidebar-collapsed {
grid-template-columns: minmax(0, 1fr);
}
.planet-layout.alpha-text-global-search-only > .planet-list-panel,
.planet-layout.alpha-text-global-search-only.layout-sidebar-collapsed > .planet-list-panel,
.planet-layout.alpha-text-global-search-only > .planet-detail-panel > .alpha-text-detail-heading,
.planet-layout.alpha-text-global-search-only .alpha-text-heading-tools,
.planet-layout.alpha-text-global-search-only .alpha-text-controls--heading,
.planet-layout.alpha-text-global-search-only .alpha-text-search-controls--detail,
.planet-layout.alpha-text-global-search-only .alpha-text-search-inline {
.planet-layout.alpha-text-global-search-only .alpha-text-search-controls--detail {
display: none !important;
}
@@ -3778,6 +3801,10 @@
align-items: baseline;
}
.alpha-text-hide-verse-heads .alpha-text-verse-head {
display: none;
}
.alpha-text-verse-reference {
color: #c4b5fd;
font-size: 12px;

View File

@@ -2,6 +2,33 @@
"use strict";
const dataService = window.TarotDataService || {};
const STORAGE_KEYS = {
showVerseHeads: "tarotime.alphaText.showVerseHeads"
};
function readStoredBoolean(key, fallback) {
try {
const value = window.localStorage?.getItem?.(key);
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
} catch (error) {
// Ignore storage failures and keep in-memory defaults.
}
return fallback;
}
function writeStoredBoolean(key, value) {
try {
window.localStorage?.setItem?.(key, value ? "true" : "false");
} catch (error) {
// Ignore storage failures and keep in-memory state.
}
}
const state = {
initialized: false,
@@ -31,15 +58,18 @@
searchError: "",
searchRequestId: 0,
highlightedVerseId: "",
displayPreferencesBySource: {}
displayPreferencesBySource: {},
showVerseHeads: readStoredBoolean(STORAGE_KEYS.showVerseHeads, true)
};
let sourceListEl;
let sourceCountEl;
let globalSearchFormEl;
let globalSearchInputEl;
let globalSearchClearEl;
let localSearchFormEl;
let localSearchInputEl;
let localSearchClearEl;
let translationSelectEl;
let translationControlEl;
let compareSelectEl;
@@ -54,6 +84,7 @@
let detailHeadingToolsEl;
let detailBodyEl;
let textLayoutEl;
let showVerseHeadsEl;
let lexiconPopupEl;
let lexiconPopupTitleEl;
let lexiconPopupSubtitleEl;
@@ -72,8 +103,11 @@
sourceCountEl = document.getElementById("alpha-text-source-count");
globalSearchFormEl = document.getElementById("alpha-text-global-search-form");
globalSearchInputEl = document.getElementById("alpha-text-global-search-input");
globalSearchClearEl = document.getElementById("alpha-text-global-search-clear");
localSearchFormEl = document.getElementById("alpha-text-local-search-form");
localSearchInputEl = document.getElementById("alpha-text-local-search-input");
localSearchClearEl = document.getElementById("alpha-text-local-search-clear");
showVerseHeadsEl = document.getElementById("alpha-text-show-verse-heads");
translationSelectEl = document.getElementById("alpha-text-translation-select");
translationControlEl = translationSelectEl?.closest?.(".alpha-text-control") || null;
compareSelectEl = document.getElementById("alpha-text-compare-select");
@@ -88,9 +122,21 @@
detailHeadingToolsEl = document.querySelector("#alphabet-text-section .alpha-text-heading-tools");
detailBodyEl = document.getElementById("alpha-text-detail-body");
textLayoutEl = sourceListEl?.closest?.(".planet-layout") || detailBodyEl?.closest?.(".planet-layout") || null;
syncReaderDisplayControls();
ensureLexiconPopup();
}
function syncReaderDisplayControls() {
if (showVerseHeadsEl instanceof HTMLInputElement) {
showVerseHeadsEl.checked = Boolean(state.showVerseHeads);
}
if (textLayoutEl instanceof HTMLElement) {
textLayoutEl.classList.toggle("alpha-text-hide-verse-heads", !state.showVerseHeads);
textLayoutEl.setAttribute("data-show-verse-heads", state.showVerseHeads ? "true" : "false");
}
}
function setGlobalSearchHeadingMode(isGlobalSearchOnly) {
if (textLayoutEl instanceof HTMLElement) {
textLayoutEl.classList.toggle("alpha-text-global-search-only", Boolean(isGlobalSearchOnly));
@@ -121,6 +167,16 @@
window.TarotChromeUi?.showDetailOnly?.(textLayoutEl);
}
function showSidebarOnlyMode(persist = false) {
if (!(textLayoutEl instanceof HTMLElement)) {
return;
}
window.TarotChromeUi?.initializeSidebarPopouts?.();
window.TarotChromeUi?.initializeDetailPopouts?.();
window.TarotChromeUi?.showSidebarOnly?.(textLayoutEl, persist);
}
function ensureLexiconPopup() {
if (lexiconPopupEl instanceof HTMLElement) {
return;
@@ -345,6 +401,9 @@
if (normalizedCount === 1) {
return `${normalizedCount} ${baseLabel}`;
}
if (/[^aeiou]y$/i.test(baseLabel)) {
return `${normalizedCount} ${baseLabel.slice(0, -1)}ies`;
}
return `${normalizedCount} ${baseLabel.endsWith("s") ? baseLabel : `${baseLabel}s`}`;
}
@@ -577,7 +636,18 @@
}
function updateSearchControls() {
return;
const globalQuery = String(globalSearchInputEl?.value || state.globalSearchQuery || "").trim();
const localQuery = String(localSearchInputEl?.value || state.localSearchQuery || "").trim();
const hasGlobalSearch = Boolean(globalQuery) || (state.activeSearchScope === "global" && Boolean(state.searchQuery));
const hasLocalSearch = Boolean(localQuery) || (state.activeSearchScope === "source" && Boolean(state.searchQuery));
if (globalSearchClearEl instanceof HTMLButtonElement) {
globalSearchClearEl.disabled = !hasGlobalSearch;
}
if (localSearchClearEl instanceof HTMLButtonElement) {
localSearchClearEl.disabled = !hasLocalSearch;
}
}
function clearActiveSearchUi(options = {}) {
@@ -735,6 +805,15 @@
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function buildWholeWordMatcher(query, flags = "iu") {
const normalizedQuery = String(query || "").trim();
if (!normalizedQuery) {
return null;
}
return new RegExp(`(^|[^\\p{L}\\p{N}])(${escapeRegExp(normalizedQuery)})(?=$|[^\\p{L}\\p{N}])`, flags);
}
function appendHighlightedText(target, text, query) {
if (!(target instanceof HTMLElement)) {
return;
@@ -748,20 +827,30 @@
return;
}
const matcher = new RegExp(escapeRegExp(normalizedQuery), "ig");
const matcher = buildWholeWordMatcher(normalizedQuery, "giu");
if (!matcher) {
target.textContent = sourceText;
return;
}
let lastIndex = 0;
let match = matcher.exec(sourceText);
while (match) {
if (match.index > lastIndex) {
target.appendChild(document.createTextNode(sourceText.slice(lastIndex, match.index)));
const prefixLength = String(match[1] || "").length;
const matchedText = String(match[2] || "");
const matchStart = match.index + prefixLength;
const matchEnd = matchStart + matchedText.length;
if (matchStart > lastIndex) {
target.appendChild(document.createTextNode(sourceText.slice(lastIndex, matchStart)));
}
const mark = document.createElement("mark");
mark.className = "alpha-text-mark";
mark.textContent = sourceText.slice(match.index, match.index + match[0].length);
mark.textContent = sourceText.slice(matchStart, matchEnd);
target.appendChild(mark);
lastIndex = match.index + match[0].length;
lastIndex = matchEnd;
match = matcher.exec(sourceText);
}
@@ -1006,7 +1095,7 @@
fillSelect(translationSelectEl, variants, state.selectedSourceId, (entry) => buildTranslationOptionLabel(entry));
fillSelect(compareSelectEl, compareCandidates, compareSource?.id || "", (entry) => buildTranslationOptionLabel(entry));
fillSelect(workSelectEl, works, state.selectedWorkId, (entry) => `${entry.title} (${entry.sectionCount} ${String(source?.sectionLabel || "section").toLowerCase()}s)`);
fillSelect(workSelectEl, works, state.selectedWorkId, (entry) => `${entry.title} (${formatCountLabel(entry.sectionCount, String(source?.sectionLabel || "section").toLowerCase())})`);
fillSelect(sectionSelectEl, sections, state.selectedSectionId, (entry) => `${entry.label} · ${entry.verseCount} verses`);
if (translationSelectEl instanceof HTMLSelectElement) {
@@ -1703,15 +1792,28 @@
const summary = document.createElement("p");
summary.className = "alpha-text-search-summary";
const actions = document.createElement("div");
actions.className = "alpha-text-search-actions";
const closeButton = document.createElement("button");
closeButton.type = "button";
closeButton.className = "alpha-nav-btn";
closeButton.textContent = "Close Search";
closeButton.addEventListener("click", () => {
clearScopedSearch(state.activeSearchScope === "source" ? "source" : "global");
renderDetail();
});
actions.appendChild(closeButton);
if (state.searchLoading) {
summary.textContent = `Searching ${scopeLabel} for \"${state.searchQuery}\"...`;
card.appendChild(summary);
card.append(summary, actions);
return card;
}
if (state.searchError) {
summary.textContent = `Search scope: ${scopeLabel}`;
card.append(summary, createEmptyMessage(state.searchError));
card.append(summary, actions, createEmptyMessage(state.searchError));
return card;
}
@@ -1719,7 +1821,7 @@
const totalMatches = Number(payload?.totalMatches) || 0;
const truncatedNote = payload?.truncated ? ` Showing the first ${payload.resultCount} results.` : "";
summary.textContent = `${totalMatches} matches in ${scopeLabel}.${truncatedNote}`;
card.appendChild(summary);
card.append(summary, actions);
if (!Array.isArray(payload?.results) || !payload.results.length) {
card.appendChild(createEmptyMessage(`No matches found for \"${state.searchQuery}\".`));
@@ -2026,6 +2128,13 @@
});
}
if (globalSearchClearEl instanceof HTMLButtonElement) {
globalSearchClearEl.addEventListener("click", () => {
clearScopedSearch("global");
renderDetail();
});
}
if (localSearchFormEl instanceof HTMLFormElement) {
localSearchFormEl.addEventListener("submit", (event) => {
event.preventDefault();
@@ -2033,6 +2142,13 @@
});
}
if (localSearchClearEl instanceof HTMLButtonElement) {
localSearchClearEl.addEventListener("click", () => {
clearScopedSearch("source");
renderDetail();
});
}
if (localSearchInputEl instanceof HTMLInputElement) {
localSearchInputEl.addEventListener("input", () => {
state.localSearchQuery = String(localSearchInputEl.value || "").trim();
@@ -2044,6 +2160,14 @@
});
}
if (showVerseHeadsEl instanceof HTMLInputElement) {
showVerseHeadsEl.addEventListener("change", () => {
state.showVerseHeads = Boolean(showVerseHeadsEl.checked);
writeStoredBoolean(STORAGE_KEYS.showVerseHeads, state.showVerseHeads);
syncReaderDisplayControls();
});
}
if (translationSelectEl instanceof HTMLSelectElement) {
translationSelectEl.addEventListener("change", () => {
const sourceGroup = getSelectedSourceGroup();
@@ -2134,9 +2258,11 @@
}
await ensureCatalogLoaded();
syncReaderDisplayControls();
renderSourceList();
renderSelectors();
updateSearchControls();
showSidebarOnlyMode(false);
if (!state.currentPassage) {
await loadSelectedPassage();

View File

@@ -16,7 +16,7 @@
<link rel="stylesheet" href="node_modules/@fontsource/amiri/arabic-400.css">
<link rel="stylesheet" href="node_modules/@fontsource/amiri/arabic-700.css">
<link rel="stylesheet" href="node_modules/@fontsource/noto-naskh-arabic/arabic-400.css">
<link rel="stylesheet" href="app/styles.css?v=20260314-topbar-panel-toggle-01">
<link rel="stylesheet" href="app/styles.css?v=20260315-text-search-ui-01">
</head>
<body>
<div class="topbar">
@@ -776,6 +776,7 @@
<div class="alpha-text-search-inline">
<input id="alpha-text-global-search-input" class="alpha-text-search-input" type="search" placeholder="Search across every text source" aria-label="Search all texts">
<button id="alpha-text-global-search-submit" class="alpha-nav-btn alpha-text-search-submit-inline" type="submit">Search</button>
<button id="alpha-text-global-search-clear" class="alpha-nav-btn alpha-text-search-submit-inline" type="button">Clear</button>
</div>
</form>
<div id="alpha-text-source-list" class="planet-card-list" role="listbox" aria-label="Text sources"></div>
@@ -808,6 +809,13 @@
<span>Section</span>
<select id="alpha-text-section-select" class="alpha-text-select" aria-label="Select text section"></select>
</label>
<div class="alpha-text-control alpha-text-control--toggle alpha-text-reader-toggle-control">
<span>Reader</span>
<label class="alpha-text-reader-toggle" for="alpha-text-show-verse-heads">
<input id="alpha-text-show-verse-heads" type="checkbox" checked>
<span>Show headings and letter/word stats</span>
</label>
</div>
</div>
<form id="alpha-text-local-search-form" class="alpha-text-search-controls alpha-text-search-controls--detail alpha-text-search-controls--heading">
<label class="alpha-text-control" for="alpha-text-local-search-input">
@@ -816,6 +824,7 @@
<div class="alpha-text-search-inline">
<input id="alpha-text-local-search-input" class="alpha-text-search-input" type="search" placeholder="Search within the selected source" aria-label="Search current text source">
<button id="alpha-text-local-search-submit" class="alpha-nav-btn alpha-text-search-submit-inline" type="submit">Search</button>
<button id="alpha-text-local-search-clear" class="alpha-nav-btn alpha-text-search-submit-inline" type="button">Clear</button>
</div>
</form>
</div>
@@ -1069,7 +1078,7 @@
<script src="app/ui-alphabet-detail.js?v=20260309-enochian-api"></script>
<script src="app/ui-alphabet-kabbalah.js"></script>
<script src="app/ui-alphabet.js?v=20260308b"></script>
<script src="app/ui-alphabet-text.js?v=20260314-text-compare-01"></script>
<script src="app/ui-alphabet-text.js?v=20260315-text-search-ui-01"></script>
<script src="app/ui-zodiac-references.js"></script>
<script src="app/ui-zodiac.js"></script>
<script src="app/ui-quiz-bank-builtins-domains.js"></script>