diff --git a/app/styles.css b/app/styles.css index 6aee23a..5c748ae 100644 --- a/app/styles.css +++ b/app/styles.css @@ -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; diff --git a/app/ui-alphabet-text.js b/app/ui-alphabet-text.js index 4be6c43..7a31760 100644 --- a/app/ui-alphabet-text.js +++ b/app/ui-alphabet-text.js @@ -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(); diff --git a/index.html b/index.html index 51fd6a3..51a2798 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@ - +
@@ -1069,7 +1078,7 @@ - +