From adb768db04bf6b3f73bdc83fd5a1dede68852eca Mon Sep 17 00:00:00 2001 From: Nose Date: Thu, 25 Jun 2026 07:35:16 -0700 Subject: [PATCH] update --- app/ui-alphabet-browser.js | 18 ++- app/ui-alphabet.js | 105 ++++++++++++++- app/ui-sequence-nav.js | 260 ++++++++++++++++++++++++++++++++++++- index.html | 7 +- 4 files changed, 386 insertions(+), 4 deletions(-) diff --git a/app/ui-alphabet-browser.js b/app/ui-alphabet-browser.js index a010197..063d315 100644 --- a/app/ui-alphabet-browser.js +++ b/app/ui-alphabet-browser.js @@ -100,9 +100,16 @@ state, normalizeId, getDomRefs, - renderDetail + renderDetail, + onStateChange } = dependencies || {}; + function notifyStateChange() { + if (typeof onStateChange === "function") { + onStateChange(); + } + } + function capitalize(value) { return value ? value.charAt(0).toUpperCase() + value.slice(1) : ""; } @@ -405,6 +412,7 @@ if (selected) { renderDetail(selected); + notifyStateChange(); return; } @@ -413,6 +421,7 @@ if (detailSubEl) { detailSubEl.textContent = "No letters match the current filter."; } + notifyStateChange(); } function bindFilterControls() { @@ -530,6 +539,7 @@ } if (detailSubEl) detailSubEl.textContent = "Select a letter to explore"; if (detailBodyEl) detailBodyEl.innerHTML = ""; + notifyStateChange(); } function selectLetter(key) { @@ -539,6 +549,8 @@ const letter = letters.find((entry) => letterKey(entry) === key) || getLetters().find((entry) => letterKey(entry) === key); if (letter) { renderDetail(letter); + } else { + notifyStateChange(); } } @@ -561,10 +573,14 @@ const letter = letters.find((entry) => letterKey(entry) === selectKey) || getLetters().find((entry) => letterKey(entry) === selectKey); if (letter) { renderDetail(letter); + } else { + notifyStateChange(); } } else { resetDetail(); } + + notifyStateChange(); } return { diff --git a/app/ui-alphabet.js b/app/ui-alphabet.js index 10f3d53..7487555 100644 --- a/app/ui-alphabet.js +++ b/app/ui-alphabet.js @@ -42,13 +42,16 @@ }, gematriaDb: null }; + let detailNavigator = null; function arabicDisplayName(letter) { return browserUi.arabicDisplayName(letter); } // ── Element cache ──────────────────────────────────────────────────── + let alphabetLettersSectionEl; let listEl, countEl, detailNameEl, detailSubEl, detailBodyEl; + let detailPrevEl, detailPositionEl, detailNextEl; let tabAll, tabHebrew, tabGreek, tabGreekArchaic, tabEnglish, tabArabic, tabEnochian; let searchInputEl, searchClearEl, typeFilterEl; let gematriaCipherEl, gematriaInputEl, gematriaResultEl, gematriaBreakdownEl; @@ -56,11 +59,15 @@ let gematriaReverseCiphersEl, gematriaReverseCipherHintEl; function getElements() { + alphabetLettersSectionEl = document.getElementById("alphabet-letters-section"); listEl = document.getElementById("alpha-letter-list"); countEl = document.getElementById("alpha-letter-count"); detailNameEl = document.getElementById("alpha-detail-name"); detailSubEl = document.getElementById("alpha-detail-sub"); detailBodyEl = document.getElementById("alpha-detail-body"); + detailPrevEl = document.getElementById("alpha-detail-prev"); + detailPositionEl = document.getElementById("alpha-detail-position"); + detailNextEl = document.getElementById("alpha-detail-next"); tabAll = document.getElementById("alpha-tab-all"); tabHebrew = document.getElementById("alpha-tab-hebrew"); tabGreek = document.getElementById("alpha-tab-greek"); @@ -218,6 +225,99 @@ if (typeof alphabetDetailUi.renderDetail === "function") { alphabetDetailUi.renderDetail(getDetailRenderContext(letter, alphabet)); } + + syncDetailNavigation(); + } + + function getLetterSequenceState() { + const filteredLetters = getFilteredLetters(); + const selectedLetter = filteredLetters.find((letter) => letterKey(letter) === state.selectedKey) + || getLetters().find((letter) => letterKey(letter) === state.selectedKey) + || null; + + const selectedAlphabet = state.activeAlphabet === "all" + ? String(alphabetForLetter(selectedLetter) || "").trim().toLowerCase() + : state.activeAlphabet; + + const letters = state.activeAlphabet === "all" && selectedAlphabet + ? filteredLetters.filter((letter) => alphabetForLetter(letter) === selectedAlphabet) + : filteredLetters; + + const total = letters.length; + const currentIndex = letters.findIndex((letter) => letterKey(letter) === state.selectedKey); + + return { + total, + currentIndex, + previousKey: currentIndex > 0 ? letterKey(letters[currentIndex - 1]) : "", + nextKey: currentIndex >= 0 && currentIndex < total - 1 ? letterKey(letters[currentIndex + 1]) : "" + }; + } + + function scrollLetterIntoView(keyToReveal, elements) { + if (!elements?.listEl || !keyToReveal) { + return; + } + + const match = Array.from(elements.listEl.querySelectorAll(".alpha-list-item")) + .find((entry) => entry.dataset.key === keyToReveal); + + match?.scrollIntoView({ block: "nearest" }); + } + + function getDetailNavigator() { + if (detailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") { + return detailNavigator; + } + + detailNavigator = window.TarotSequenceNav.createSequenceNavigator({ + getElements: () => ({ + alphabetLettersSectionEl, + listEl, + detailPrevEl, + detailPositionEl, + detailNextEl + }), + isActive: (elements) => Boolean(elements?.alphabetLettersSectionEl && elements.alphabetLettersSectionEl.hidden === false), + getSequenceState: getLetterSequenceState, + getPrevButton: (elements) => elements?.detailPrevEl, + getNextButton: (elements) => elements?.detailNextEl, + getPositionEl: (elements) => elements?.detailPositionEl, + formatPositionText: ({ total, currentIndex }) => { + const sequenceState = getLetterSequenceState(); + const alphabetLabel = sequenceState.selectedAlphabet + ? `${sequenceState.selectedAlphabet} ` + : ""; + + if (total > 0 && currentIndex >= 0) { + const suffix = (state.activeAlphabet !== "all" || String(state.filters.query || "").trim() || state.filters.letterType) + ? " shown" + : ""; + return `${currentIndex + 1} of ${total} ${alphabetLabel}letters${suffix}`.trim(); + } + + return total > 0 + ? `${total} ${alphabetLabel}letters`.trim() + : "No letters"; + }, + selectTarget: (targetKey) => { + selectLetter(targetKey); + return true; + }, + afterSelect: (targetKey, elements) => { + scrollLetterIntoView(targetKey, elements); + } + }); + + return detailNavigator; + } + + function syncDetailNavigation() { + getDetailNavigator()?.sync(); + } + + function bindDetailNavigation() { + getDetailNavigator()?.bind(); } function card(title, bodyHTML) { @@ -365,7 +465,8 @@ searchClearEl, typeFilterEl }), - renderDetail + renderDetail, + onStateChange: syncDetailNavigation }); // ── Event delegation on detail body ────────────────────────────────── @@ -458,6 +559,7 @@ // alphabets.json is a top-level file → grouped["alphabets"] = the data object getElements(); + bindDetailNavigation(); ensureGematriaCalculator(); bindFilterControls(); syncFilterControls(); @@ -476,6 +578,7 @@ }); switchAlphabet("all", null); + syncDetailNavigation(); } // ── Incoming navigation ─────────────────────────────────────────────── diff --git a/app/ui-sequence-nav.js b/app/ui-sequence-nav.js index 2ee466b..aebdc37 100644 --- a/app/ui-sequence-nav.js +++ b/app/ui-sequence-nav.js @@ -26,6 +26,261 @@ return Boolean(document.querySelector("[role='dialog'][aria-modal='true'][aria-hidden='false']")); } + function isHiddenElement(element) { + if (!(element instanceof HTMLElement)) { + return true; + } + + if (element.hidden || element.getAttribute("aria-hidden") === "true") { + return true; + } + + return false; + } + + function getAutoSequenceOptions(listEl) { + if (!(listEl instanceof HTMLElement)) { + return []; + } + + return Array.from(listEl.querySelectorAll("[role='option']")) + .filter((option) => option instanceof HTMLElement) + .filter((option) => !isHiddenElement(option)); + } + + function getAutoSelectedIndex(options) { + const selectedIndex = options.findIndex((option) => { + if (!(option instanceof HTMLElement)) { + return false; + } + + return option.getAttribute("aria-selected") === "true" + || option.classList.contains("is-selected"); + }); + + return selectedIndex; + } + + function createAutomaticSequenceControls(sectionEl, headingEl) { + if (!(headingEl instanceof HTMLElement)) { + return null; + } + + if (headingEl.querySelector(".detail-sequence-nav")) { + return null; + } + + const titleText = String(headingEl.querySelector("h2")?.textContent || "items").trim() || "items"; + const normalizedTitle = titleText + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, ""); + + const sectionId = String(sectionEl?.id || "section").trim() || "section"; + const navPrefix = `${sectionId}-${normalizedTitle || "items"}`; + + const navEl = document.createElement("div"); + navEl.className = "detail-sequence-nav"; + navEl.dataset.autoSequenceNav = "true"; + navEl.setAttribute("aria-label", `Browse ${titleText}`); + + const prevButton = document.createElement("button"); + prevButton.type = "button"; + prevButton.id = `${navPrefix}-detail-prev-auto`; + prevButton.className = "detail-sequence-btn"; + prevButton.textContent = "Back"; + prevButton.setAttribute("aria-label", `Previous ${titleText}`); + + const positionEl = document.createElement("div"); + positionEl.id = `${navPrefix}-detail-position-auto`; + positionEl.className = "detail-sequence-position"; + positionEl.setAttribute("aria-live", "polite"); + positionEl.textContent = "--"; + + const nextButton = document.createElement("button"); + nextButton.type = "button"; + nextButton.id = `${navPrefix}-detail-next-auto`; + nextButton.className = "detail-sequence-btn"; + nextButton.textContent = "Next"; + nextButton.setAttribute("aria-label", `Next ${titleText}`); + + navEl.append(prevButton, positionEl, nextButton); + + const summaryEl = headingEl.querySelector(".planet-detail-summary"); + if (summaryEl && summaryEl.parentElement === headingEl) { + headingEl.insertBefore(navEl, summaryEl); + } else { + headingEl.appendChild(navEl); + } + + return { + sectionEl, + listEl: sectionEl.querySelector(".planet-list-panel [role='listbox']"), + prevButton, + nextButton, + positionEl + }; + } + + const automaticNavigators = new WeakMap(); + let autoScanObserver = null; + let autoScanQueued = false; + let autoNavigationEnabled = false; + + function createAutomaticNavigatorForSection(sectionEl) { + if (!(sectionEl instanceof HTMLElement) || automaticNavigators.has(sectionEl)) { + return; + } + + const listEl = sectionEl.querySelector(".planet-list-panel [role='listbox']"); + const headingEl = sectionEl.querySelector(".planet-detail-panel .planet-detail-heading"); + if (!(listEl instanceof HTMLElement) || !(headingEl instanceof HTMLElement)) { + return; + } + + if (headingEl.querySelector(".detail-sequence-nav")) { + return; + } + + const controls = createAutomaticSequenceControls(sectionEl, headingEl); + if (!controls || !(controls.listEl instanceof HTMLElement)) { + return; + } + + const navigator = createSequenceNavigator({ + getElements: () => controls, + isActive: (elements) => Boolean(elements?.sectionEl && elements.sectionEl.hidden === false), + getSequenceState: () => { + const options = getAutoSequenceOptions(controls.listEl); + const total = options.length; + const currentIndex = getAutoSelectedIndex(options); + + if (!total) { + return { + total: 0, + currentIndex: -1, + previousKey: "", + nextKey: "" + }; + } + + return { + total, + currentIndex, + previousKey: currentIndex > 0 ? String(currentIndex - 1) : "", + nextKey: currentIndex >= 0 && currentIndex < total - 1 + ? String(currentIndex + 1) + : currentIndex < 0 + ? "0" + : "" + }; + }, + getPrevButton: (elements) => elements?.prevButton, + getNextButton: (elements) => elements?.nextButton, + getPositionEl: (elements) => elements?.positionEl, + formatPositionText: ({ total, currentIndex }) => { + if (total > 0 && currentIndex >= 0) { + return `${currentIndex + 1} of ${total}`; + } + + return total > 0 ? `${total} items` : "No items"; + }, + selectTarget: (targetKey) => { + const options = getAutoSequenceOptions(controls.listEl); + const targetIndex = Number(targetKey); + if (!Number.isFinite(targetIndex) || targetIndex < 0 || targetIndex >= options.length) { + return false; + } + + const targetOption = options[targetIndex]; + if (!(targetOption instanceof HTMLElement)) { + return false; + } + + if (targetOption instanceof HTMLButtonElement && targetOption.disabled) { + return false; + } + + targetOption.dispatchEvent(new MouseEvent("click", { + bubbles: true, + cancelable: true, + view: window + })); + + return true; + }, + afterSelect: (targetKey) => { + const options = getAutoSequenceOptions(controls.listEl); + const targetIndex = Number(targetKey); + if (!Number.isFinite(targetIndex) || targetIndex < 0 || targetIndex >= options.length) { + return; + } + + const targetOption = options[targetIndex]; + if (targetOption instanceof HTMLElement) { + targetOption.scrollIntoView({ block: "nearest" }); + } + } + }); + + navigator.bind(controls); + + const listObserver = new MutationObserver(() => { + navigator.sync(controls); + }); + + listObserver.observe(controls.listEl, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["aria-selected", "class", "hidden", "aria-hidden"] + }); + + automaticNavigators.set(sectionEl, { + controls, + navigator, + listObserver + }); + } + + function scanForAutomaticNavigators() { + document.querySelectorAll("section").forEach((sectionEl) => { + createAutomaticNavigatorForSection(sectionEl); + }); + } + + function queueAutomaticNavigationScan() { + if (autoScanQueued) { + return; + } + + autoScanQueued = true; + window.requestAnimationFrame(() => { + autoScanQueued = false; + scanForAutomaticNavigators(); + }); + } + + function enableAutomaticSequenceNavigation() { + if (autoNavigationEnabled) { + return; + } + + autoNavigationEnabled = true; + scanForAutomaticNavigators(); + + if (document.body && !autoScanObserver) { + autoScanObserver = new MutationObserver(() => { + queueAutomaticNavigationScan(); + }); + + autoScanObserver.observe(document.body, { + childList: true, + subtree: true + }); + } + } + function createSequenceNavigator(config = {}) { const getElements = typeof config.getElements === "function" ? config.getElements @@ -184,6 +439,9 @@ window.TarotSequenceNav = { ...(window.TarotSequenceNav || {}), - createSequenceNavigator + createSequenceNavigator, + enableAutomaticSequenceNavigation }; + + enableAutomaticSequenceNavigation(); })(); \ No newline at end of file diff --git a/index.html b/index.html index a9ca3e6..e155974 100644 --- a/index.html +++ b/index.html @@ -1052,6 +1052,11 @@

--

Select a letter to explore
+
+ +
--
+ +
@@ -1354,7 +1359,7 @@ - +