diff --git a/app/ui-alphabet-browser.js b/app/ui-alphabet-browser.js new file mode 100644 index 0000000..d8100dc --- /dev/null +++ b/app/ui-alphabet-browser.js @@ -0,0 +1,405 @@ +(function () { + "use strict"; + + const ARABIC_DISPLAY_NAMES = { + alif: "Alif", ba: "Ba", jeem: "Jeem", dal: "Dal", ha: "Ha", + waw: "Waw", zayn: "Zayn", ha_khaa: "Haa", ta_tay: "Tta", ya: "Ya", + kaf: "Kaf", lam: "Lam", meem: "Meem", nun: "Nun", seen: "Seen", + ayn: "Ayn", fa: "Fa", sad: "Sad", qaf: "Qaf", ra: "Ra", + sheen: "Sheen", ta: "Ta", tha: "Tha", kha: "Kha", + dhal: "Dhal", dad: "Dad", dha: "Dha", ghayn: "Ghayn" + }; + + function createAlphabetBrowser(dependencies) { + const { + state, + normalizeId, + getDomRefs, + renderDetail + } = dependencies || {}; + + function capitalize(value) { + return value ? value.charAt(0).toUpperCase() + value.slice(1) : ""; + } + + function arabicDisplayName(letter) { + return ARABIC_DISPLAY_NAMES[letter && letter.name] + || (String(letter && letter.name || "").charAt(0).toUpperCase() + String(letter && letter.name || "").slice(1)); + } + + function getLetters() { + if (!state.alphabets) return []; + if (state.activeAlphabet === "all") { + const alphabetOrder = ["hebrew", "greek", "english", "arabic", "enochian"]; + return alphabetOrder.flatMap((alphabet) => { + const rows = Array.isArray(state.alphabets?.[alphabet]) ? state.alphabets[alphabet] : []; + return rows.map((row) => ({ ...row, __alphabet: alphabet })); + }); + } + return state.alphabets[state.activeAlphabet] || []; + } + + function alphabetForLetter(letter) { + if (state.activeAlphabet === "all") { + return String(letter?.__alphabet || "").trim().toLowerCase(); + } + return state.activeAlphabet; + } + + function letterKeyByAlphabet(alphabet, letter) { + if (alphabet === "hebrew") return letter.hebrewLetterId; + if (alphabet === "greek") return letter.name; + if (alphabet === "english") return letter.letter; + if (alphabet === "arabic") return letter.name; + if (alphabet === "enochian") return letter.id; + return String(letter.index); + } + + function letterKey(letter) { + const alphabet = alphabetForLetter(letter); + const key = letterKeyByAlphabet(alphabet, letter); + if (state.activeAlphabet === "all") { + return `${alphabet}:${key}`; + } + return key; + } + + function displayGlyph(letter) { + const alphabet = alphabetForLetter(letter); + if (alphabet === "hebrew") return letter.char; + if (alphabet === "greek") return letter.char; + if (alphabet === "english") return letter.letter; + if (alphabet === "arabic") return letter.char; + if (alphabet === "enochian") return letter.char; + return "?"; + } + + function displayLabel(letter) { + const alphabet = alphabetForLetter(letter); + if (alphabet === "hebrew") return letter.name; + if (alphabet === "greek") return letter.displayName; + if (alphabet === "english") return letter.letter; + if (alphabet === "arabic") return arabicDisplayName(letter); + if (alphabet === "enochian") return letter.title; + return "?"; + } + + function displaySub(letter) { + const alphabet = alphabetForLetter(letter); + if (alphabet === "hebrew") return `${letter.transliteration} · ${letter.letterType} · ${letter.numerology}`; + if (alphabet === "greek") return `${letter.transliteration} · isopsephy ${letter.numerology}${letter.archaic ? " · archaic" : ""}`; + if (alphabet === "english") return `Pythagorean ${letter.pythagorean}`; + if (alphabet === "arabic") return `${letter.transliteration} · abjad ${letter.abjad} · ${letter.nameArabic}`; + if (alphabet === "enochian") return `${letter.transliteration} · ${Array.isArray(letter.englishLetters) ? letter.englishLetters.join("/") : ""} · value ${letter.numerology}`; + return ""; + } + + function normalizeLetterType(value) { + const key = String(value || "").trim().toLowerCase(); + if (["mother", "double", "simple"].includes(key)) { + return key; + } + return ""; + } + + function getHebrewLetterTypeMap() { + const map = new Map(); + const hebrewLetters = Array.isArray(state.alphabets?.hebrew) ? state.alphabets.hebrew : []; + hebrewLetters.forEach((entry) => { + const hebrewId = normalizeId(entry?.hebrewLetterId); + const letterType = normalizeLetterType(entry?.letterType); + if (hebrewId && letterType) { + map.set(hebrewId, letterType); + } + }); + return map; + } + + function resolveLetterType(letter) { + const direct = normalizeLetterType(letter?.letterType); + if (direct) { + return direct; + } + + const hebrewId = normalizeId(letter?.hebrewLetterId); + if (!hebrewId) { + return ""; + } + + return getHebrewLetterTypeMap().get(hebrewId) || ""; + } + + function buildLetterSearchText(letter) { + const chunks = []; + + chunks.push(String(displayLabel(letter) || "")); + chunks.push(String(displayGlyph(letter) || "")); + chunks.push(String(displaySub(letter) || "")); + chunks.push(String(letter?.transliteration || "")); + chunks.push(String(letter?.meaning || "")); + chunks.push(String(letter?.nameArabic || "")); + chunks.push(String(letter?.title || "")); + chunks.push(String(letter?.letter || "")); + chunks.push(String(letter?.displayName || "")); + chunks.push(String(letter?.name || "")); + chunks.push(String(letter?.index || "")); + chunks.push(String(resolveLetterType(letter) || "")); + + return chunks.join(" ").toLowerCase(); + } + + function getFilteredLetters() { + const letters = getLetters(); + const query = String(state.filters.query || "").trim().toLowerCase(); + const letterTypeFilter = normalizeLetterType(state.filters.letterType); + const numericPosition = /^\d+$/.test(query) ? Number(query) : null; + + return letters.filter((letter) => { + if (letterTypeFilter) { + const entryType = resolveLetterType(letter); + if (entryType !== letterTypeFilter) { + return false; + } + } + + if (!query) { + return true; + } + + const index = Number(letter?.index); + if (Number.isFinite(numericPosition) && Number.isFinite(index) && index === numericPosition) { + return true; + } + + return buildLetterSearchText(letter).includes(query); + }); + } + + function syncFilterControls() { + const { searchInputEl, typeFilterEl, searchClearEl } = getDomRefs(); + + if (searchInputEl && searchInputEl.value !== state.filters.query) { + searchInputEl.value = state.filters.query; + } + + if (typeFilterEl && typeFilterEl.value !== state.filters.letterType) { + typeFilterEl.value = state.filters.letterType; + } + + if (searchClearEl) { + const hasFilter = Boolean(String(state.filters.query || "").trim()) || Boolean(state.filters.letterType); + searchClearEl.disabled = !hasFilter; + } + } + + function applyFiltersAndRender() { + const filteredLetters = getFilteredLetters(); + const selectedInFiltered = filteredLetters.some((letter) => letterKey(letter) === state.selectedKey); + + if (!selectedInFiltered) { + state.selectedKey = filteredLetters[0] ? letterKey(filteredLetters[0]) : null; + } + + renderList(); + + const selected = filteredLetters.find((letter) => letterKey(letter) === state.selectedKey) + || filteredLetters[0] + || null; + + if (selected) { + renderDetail(selected); + return; + } + + resetDetail(); + const { detailSubEl } = getDomRefs(); + if (detailSubEl) { + detailSubEl.textContent = "No letters match the current filter."; + } + } + + function bindFilterControls() { + const { searchInputEl, typeFilterEl, searchClearEl } = getDomRefs(); + + if (searchInputEl) { + searchInputEl.addEventListener("input", () => { + state.filters.query = String(searchInputEl.value || ""); + syncFilterControls(); + applyFiltersAndRender(); + }); + } + + if (typeFilterEl) { + typeFilterEl.addEventListener("change", () => { + state.filters.letterType = normalizeLetterType(typeFilterEl.value); + syncFilterControls(); + applyFiltersAndRender(); + }); + } + + if (searchClearEl) { + searchClearEl.addEventListener("click", () => { + state.filters.query = ""; + state.filters.letterType = ""; + syncFilterControls(); + applyFiltersAndRender(); + }); + } + } + + function enochianGlyphKey(letter) { + return String(letter?.id || letter?.char || "").trim().toUpperCase(); + } + + function enochianGlyphCode(letter) { + const key = enochianGlyphKey(letter); + return key ? key.codePointAt(0) || 0 : 0; + } + + function enochianGlyphUrl(letter) { + const code = enochianGlyphCode(letter); + return code ? `asset/img/enochian/char(${code}).png` : ""; + } + + function enochianGlyphImageHtml(letter, className) { + const src = enochianGlyphUrl(letter); + const key = enochianGlyphKey(letter) || "?"; + if (!src) { + return `${key}`; + } + return `Enochian ${key}`; + } + + function renderList() { + const { listEl, countEl } = getDomRefs(); + if (!listEl) return; + const allLetters = getLetters(); + const letters = getFilteredLetters(); + if (countEl) { + countEl.textContent = letters.length === allLetters.length + ? `${letters.length}` + : `${letters.length}/${allLetters.length}`; + } + + listEl.innerHTML = ""; + letters.forEach((letter) => { + const key = letterKey(letter); + const item = document.createElement("div"); + item.className = "planet-list-item alpha-list-item" + (key === state.selectedKey ? " is-selected" : ""); + item.setAttribute("role", "option"); + item.setAttribute("aria-selected", key === state.selectedKey ? "true" : "false"); + item.dataset.key = key; + + const glyph = document.createElement("span"); + const alphabet = alphabetForLetter(letter); + const glyphVariantClass = alphabet === "arabic" + ? " alpha-list-glyph--arabic" + : alphabet === "enochian" + ? " alpha-list-glyph--enochian" + : ""; + glyph.className = "alpha-list-glyph" + glyphVariantClass; + if (alphabet === "enochian") { + const image = document.createElement("img"); + image.className = "alpha-enochian-glyph-img alpha-enochian-glyph-img--list"; + image.src = enochianGlyphUrl(letter); + image.alt = `Enochian ${enochianGlyphKey(letter) || "?"}`; + image.loading = "lazy"; + image.decoding = "async"; + image.addEventListener("error", () => { + glyph.textContent = enochianGlyphKey(letter) || "?"; + }); + glyph.appendChild(image); + } else { + glyph.textContent = displayGlyph(letter); + } + + const meta = document.createElement("span"); + meta.className = "alpha-list-meta"; + const alphaLabel = alphabet ? `${capitalize(alphabet)} · ` : ""; + meta.innerHTML = `${alphaLabel}${displayLabel(letter)}
${displaySub(letter)}`; + + item.appendChild(glyph); + item.appendChild(meta); + item.addEventListener("click", () => selectLetter(key)); + listEl.appendChild(item); + }); + } + + function resetDetail() { + const { detailNameEl, detailSubEl, detailBodyEl } = getDomRefs(); + if (detailNameEl) { + detailNameEl.textContent = "--"; + detailNameEl.classList.remove("alpha-detail-glyph"); + } + if (detailSubEl) detailSubEl.textContent = "Select a letter to explore"; + if (detailBodyEl) detailBodyEl.innerHTML = ""; + } + + function selectLetter(key) { + state.selectedKey = key; + renderList(); + const letters = getFilteredLetters(); + const letter = letters.find((entry) => letterKey(entry) === key) || getLetters().find((entry) => letterKey(entry) === key); + if (letter) { + renderDetail(letter); + } + } + + function updateTabs() { + const { tabAll, tabHebrew, tabGreek, tabEnglish, tabArabic, tabEnochian } = getDomRefs(); + [tabAll, tabHebrew, tabGreek, tabEnglish, tabArabic, tabEnochian].forEach((btn) => { + if (!btn) return; + btn.classList.toggle("alpha-tab-active", btn.dataset.alpha === state.activeAlphabet); + }); + } + + function switchAlphabet(alpha, selectKey) { + state.activeAlphabet = alpha; + state.selectedKey = selectKey || null; + updateTabs(); + syncFilterControls(); + renderList(); + if (selectKey) { + const letters = getFilteredLetters(); + const letter = letters.find((entry) => letterKey(entry) === selectKey) || getLetters().find((entry) => letterKey(entry) === selectKey); + if (letter) { + renderDetail(letter); + } + } else { + resetDetail(); + } + } + + return { + arabicDisplayName, + getLetters, + alphabetForLetter, + letterKeyByAlphabet, + letterKey, + displayGlyph, + displayLabel, + displaySub, + normalizeLetterType, + getHebrewLetterTypeMap, + resolveLetterType, + buildLetterSearchText, + getFilteredLetters, + syncFilterControls, + applyFiltersAndRender, + bindFilterControls, + enochianGlyphKey, + enochianGlyphCode, + enochianGlyphUrl, + enochianGlyphImageHtml, + renderList, + resetDetail, + selectLetter, + switchAlphabet, + updateTabs + }; + } + + window.AlphabetBrowserUi = { + createAlphabetBrowser + }; +})(); \ No newline at end of file diff --git a/app/ui-alphabet.js b/app/ui-alphabet.js index e9d878f..813a506 100644 --- a/app/ui-alphabet.js +++ b/app/ui-alphabet.js @@ -3,12 +3,14 @@ "use strict"; const alphabetGematriaUi = window.AlphabetGematriaUi || {}; + const alphabetBrowserUi = window.AlphabetBrowserUi || {}; const alphabetKabbalahUi = window.AlphabetKabbalahUi || {}; const alphabetReferenceBuilders = window.AlphabetReferenceBuilders || {}; const alphabetDetailUi = window.AlphabetDetailUi || {}; if ( - typeof alphabetKabbalahUi.buildCubePlacementButton !== "function" + typeof alphabetBrowserUi.createAlphabetBrowser !== "function" + || typeof alphabetKabbalahUi.buildCubePlacementButton !== "function" || typeof alphabetKabbalahUi.buildFourWorldLayersFromDataset !== "function" || typeof alphabetKabbalahUi.createEmptyCubeRefs !== "function" || typeof alphabetKabbalahUi.getCubePlacementForHebrewLetter !== "function" @@ -18,7 +20,7 @@ || typeof alphabetKabbalahUi.normalizeLetterId !== "function" || typeof alphabetKabbalahUi.titleCase !== "function" ) { - throw new Error("AlphabetKabbalahUi module must load before ui-alphabet.js"); + throw new Error("AlphabetBrowserUi and AlphabetKabbalahUi modules must load before ui-alphabet.js"); } const state = { @@ -40,18 +42,8 @@ } }; - // ── Arabic display name table ───────────────────────────────────────── - const ARABIC_DISPLAY_NAMES = { - alif: "Alif", ba: "Ba", jeem: "Jeem", dal: "Dal", ha: "H\u0101", - waw: "W\u0101w", zayn: "Zayn", ha_khaa: "\u1e24\u0101", ta_tay: "\u1e6c\u0101", ya: "Y\u0101", - kaf: "K\u0101f", lam: "L\u0101m", meem: "M\u012bm", nun: "N\u016bn", seen: "S\u012bn", - ayn: "\u02bfAyn", fa: "F\u0101", sad: "\u1e62\u0101d", qaf: "Q\u0101f", ra: "R\u0101", - sheen: "Sh\u012bn", ta: "T\u0101", tha: "Th\u0101", kha: "Kh\u0101", - dhal: "Dh\u0101l", dad: "\u1e0c\u0101d", dha: "\u1e92\u0101", ghayn: "Ghayn" - }; - function arabicDisplayName(letter) { - return ARABIC_DISPLAY_NAMES[letter && letter.name] || (String(letter && letter.name || "").charAt(0).toUpperCase() + String(letter && letter.name || "").slice(1)); + return browserUi.arabicDisplayName(letter); } // ── Element cache ──────────────────────────────────────────────────── @@ -101,299 +93,84 @@ // ── Data helpers ───────────────────────────────────────────────────── function getLetters() { - if (!state.alphabets) return []; - if (state.activeAlphabet === "all") { - const alphabetOrder = ["hebrew", "greek", "english", "arabic", "enochian"]; - return alphabetOrder.flatMap((alphabet) => { - const rows = Array.isArray(state.alphabets?.[alphabet]) ? state.alphabets[alphabet] : []; - return rows.map((row) => ({ ...row, __alphabet: alphabet })); - }); - } - return state.alphabets[state.activeAlphabet] || []; + return browserUi.getLetters(); } function alphabetForLetter(letter) { - if (state.activeAlphabet === "all") { - return String(letter?.__alphabet || "").trim().toLowerCase(); - } - return state.activeAlphabet; + return browserUi.alphabetForLetter(letter); } function letterKeyByAlphabet(alphabet, letter) { - if (alphabet === "hebrew") return letter.hebrewLetterId; - if (alphabet === "greek") return letter.name; - if (alphabet === "english") return letter.letter; - if (alphabet === "arabic") return letter.name; - if (alphabet === "enochian") return letter.id; - return String(letter.index); + return browserUi.letterKeyByAlphabet(alphabet, letter); } function letterKey(letter) { - // Stable unique key per alphabet + entry - const alphabet = alphabetForLetter(letter); - const key = letterKeyByAlphabet(alphabet, letter); - if (state.activeAlphabet === "all") { - return `${alphabet}:${key}`; - } - return key; + return browserUi.letterKey(letter); } function displayGlyph(letter) { - const alphabet = alphabetForLetter(letter); - if (alphabet === "hebrew") return letter.char; - if (alphabet === "greek") return letter.char; - if (alphabet === "english") return letter.letter; - if (alphabet === "arabic") return letter.char; - if (alphabet === "enochian") return letter.char; - return "?"; + return browserUi.displayGlyph(letter); } function displayLabel(letter) { - const alphabet = alphabetForLetter(letter); - if (alphabet === "hebrew") return letter.name; - if (alphabet === "greek") return letter.displayName; - if (alphabet === "english") return letter.letter; - if (alphabet === "arabic") return arabicDisplayName(letter); - if (alphabet === "enochian") return letter.title; - return "?"; + return browserUi.displayLabel(letter); } function displaySub(letter) { - const alphabet = alphabetForLetter(letter); - if (alphabet === "hebrew") return `${letter.transliteration} · ${letter.letterType} · ${letter.numerology}`; - if (alphabet === "greek") return `${letter.transliteration} · isopsephy ${letter.numerology}${letter.archaic ? " · archaic" : ""}`; - if (alphabet === "english") return `Pythagorean ${letter.pythagorean}`; - if (alphabet === "arabic") return `${letter.transliteration} · abjad ${letter.abjad} · ${letter.nameArabic}`; - if (alphabet === "enochian") return `${letter.transliteration} · ${Array.isArray(letter.englishLetters) ? letter.englishLetters.join("/") : ""} · value ${letter.numerology}`; - return ""; + return browserUi.displaySub(letter); } function normalizeLetterType(value) { - const key = String(value || "").trim().toLowerCase(); - if (["mother", "double", "simple"].includes(key)) { - return key; - } - return ""; + return browserUi.normalizeLetterType(value); } function getHebrewLetterTypeMap() { - const map = new Map(); - const hebrewLetters = Array.isArray(state.alphabets?.hebrew) ? state.alphabets.hebrew : []; - hebrewLetters.forEach((entry) => { - const hebrewId = normalizeId(entry?.hebrewLetterId); - const letterType = normalizeLetterType(entry?.letterType); - if (hebrewId && letterType) { - map.set(hebrewId, letterType); - } - }); - return map; + return browserUi.getHebrewLetterTypeMap(); } function resolveLetterType(letter) { - const direct = normalizeLetterType(letter?.letterType); - if (direct) { - return direct; - } - - const hebrewId = normalizeId(letter?.hebrewLetterId); - if (!hebrewId) { - return ""; - } - - return getHebrewLetterTypeMap().get(hebrewId) || ""; + return browserUi.resolveLetterType(letter); } function buildLetterSearchText(letter) { - const chunks = []; - - chunks.push(String(displayLabel(letter) || "")); - chunks.push(String(displayGlyph(letter) || "")); - chunks.push(String(displaySub(letter) || "")); - chunks.push(String(letter?.transliteration || "")); - chunks.push(String(letter?.meaning || "")); - chunks.push(String(letter?.nameArabic || "")); - chunks.push(String(letter?.title || "")); - chunks.push(String(letter?.letter || "")); - chunks.push(String(letter?.displayName || "")); - chunks.push(String(letter?.name || "")); - chunks.push(String(letter?.index || "")); - chunks.push(String(resolveLetterType(letter) || "")); - - return chunks - .join(" ") - .toLowerCase(); + return browserUi.buildLetterSearchText(letter); } function getFilteredLetters() { - const letters = getLetters(); - const query = String(state.filters.query || "").trim().toLowerCase(); - const letterTypeFilter = normalizeLetterType(state.filters.letterType); - const numericPosition = /^\d+$/.test(query) ? Number(query) : null; - - return letters.filter((letter) => { - if (letterTypeFilter) { - const entryType = resolveLetterType(letter); - if (entryType !== letterTypeFilter) { - return false; - } - } - - if (!query) { - return true; - } - - const index = Number(letter?.index); - if (Number.isFinite(numericPosition) && Number.isFinite(index) && index === numericPosition) { - return true; - } - - return buildLetterSearchText(letter).includes(query); - }); + return browserUi.getFilteredLetters(); } function syncFilterControls() { - if (searchInputEl && searchInputEl.value !== state.filters.query) { - searchInputEl.value = state.filters.query; - } - - if (typeFilterEl && typeFilterEl.value !== state.filters.letterType) { - typeFilterEl.value = state.filters.letterType; - } - - if (searchClearEl) { - const hasFilter = Boolean(String(state.filters.query || "").trim()) || Boolean(state.filters.letterType); - searchClearEl.disabled = !hasFilter; - } + browserUi.syncFilterControls(); } function applyFiltersAndRender() { - const filteredLetters = getFilteredLetters(); - const selectedInFiltered = filteredLetters.some((letter) => letterKey(letter) === state.selectedKey); - - if (!selectedInFiltered) { - state.selectedKey = filteredLetters[0] ? letterKey(filteredLetters[0]) : null; - } - - renderList(); - - const selected = filteredLetters.find((letter) => letterKey(letter) === state.selectedKey) - || filteredLetters[0] - || null; - - if (selected) { - renderDetail(selected); - return; - } - - resetDetail(); - if (detailSubEl) { - detailSubEl.textContent = "No letters match the current filter."; - } + browserUi.applyFiltersAndRender(); } function bindFilterControls() { - if (searchInputEl) { - searchInputEl.addEventListener("input", () => { - state.filters.query = String(searchInputEl.value || ""); - syncFilterControls(); - applyFiltersAndRender(); - }); - } - - if (typeFilterEl) { - typeFilterEl.addEventListener("change", () => { - state.filters.letterType = normalizeLetterType(typeFilterEl.value); - syncFilterControls(); - applyFiltersAndRender(); - }); - } - - if (searchClearEl) { - searchClearEl.addEventListener("click", () => { - state.filters.query = ""; - state.filters.letterType = ""; - syncFilterControls(); - applyFiltersAndRender(); - }); - } + browserUi.bindFilterControls(); } function enochianGlyphKey(letter) { - return String(letter?.id || letter?.char || "").trim().toUpperCase(); + return browserUi.enochianGlyphKey(letter); } function enochianGlyphCode(letter) { - const key = enochianGlyphKey(letter); - return key ? key.codePointAt(0) || 0 : 0; + return browserUi.enochianGlyphCode(letter); } function enochianGlyphUrl(letter) { - const code = enochianGlyphCode(letter); - return code ? `asset/img/enochian/char(${code}).png` : ""; + return browserUi.enochianGlyphUrl(letter); } function enochianGlyphImageHtml(letter, className) { - const src = enochianGlyphUrl(letter); - const key = enochianGlyphKey(letter) || "?"; - if (!src) { - return `${key}`; - } - return `Enochian ${key}`; + return browserUi.enochianGlyphImageHtml(letter, className); } // ── List rendering ──────────────────────────────────────────────────── function renderList() { - if (!listEl) return; - const allLetters = getLetters(); - const letters = getFilteredLetters(); - if (countEl) { - countEl.textContent = letters.length === allLetters.length - ? `${letters.length}` - : `${letters.length}/${allLetters.length}`; - } - - listEl.innerHTML = ""; - letters.forEach((letter) => { - const key = letterKey(letter); - const item = document.createElement("div"); - item.className = "planet-list-item alpha-list-item" + (key === state.selectedKey ? " is-selected" : ""); - item.setAttribute("role", "option"); - item.setAttribute("aria-selected", key === state.selectedKey ? "true" : "false"); - item.dataset.key = key; - - const glyph = document.createElement("span"); - const alphabet = alphabetForLetter(letter); - const glyphVariantClass = alphabet === "arabic" - ? " alpha-list-glyph--arabic" - : alphabet === "enochian" - ? " alpha-list-glyph--enochian" - : ""; - glyph.className = "alpha-list-glyph" + glyphVariantClass; - if (alphabet === "enochian") { - const image = document.createElement("img"); - image.className = "alpha-enochian-glyph-img alpha-enochian-glyph-img--list"; - image.src = enochianGlyphUrl(letter); - image.alt = `Enochian ${enochianGlyphKey(letter) || "?"}`; - image.loading = "lazy"; - image.decoding = "async"; - image.addEventListener("error", () => { - glyph.textContent = enochianGlyphKey(letter) || "?"; - }); - glyph.appendChild(image); - } else { - glyph.textContent = displayGlyph(letter); - } - - const meta = document.createElement("span"); - meta.className = "alpha-list-meta"; - const alphaLabel = alphabet ? `${cap(alphabet)} · ` : ""; - meta.innerHTML = `${alphaLabel}${displayLabel(letter)}
${displaySub(letter)}`; - - item.appendChild(glyph); - item.appendChild(meta); - item.addEventListener("click", () => selectLetter(key)); - listEl.appendChild(item); - }); + browserUi.renderList(); } // ── Detail rendering ────────────────────────────────────────────────── @@ -537,6 +314,28 @@ }; } + const browserUi = alphabetBrowserUi.createAlphabetBrowser({ + state, + normalizeId, + getDomRefs: () => ({ + listEl, + countEl, + detailNameEl, + detailSubEl, + detailBodyEl, + tabAll, + tabHebrew, + tabGreek, + tabEnglish, + tabArabic, + tabEnochian, + searchInputEl, + searchClearEl, + typeFilterEl + }), + renderDetail + }); + // ── Event delegation on detail body ────────────────────────────────── function attachDetailListeners() { if (!detailBodyEl) return; @@ -568,43 +367,20 @@ // ── Selection ───────────────────────────────────────────────────────── function selectLetter(key) { - state.selectedKey = key; - renderList(); - const letters = getFilteredLetters(); - const letter = letters.find((l) => letterKey(l) === key) || getLetters().find((l) => letterKey(l) === key); - if (letter) renderDetail(letter); + browserUi.selectLetter(key); } // ── Alphabet switching ──────────────────────────────────────────────── function switchAlphabet(alpha, selectKey) { - state.activeAlphabet = alpha; - state.selectedKey = selectKey || null; - updateTabs(); - syncFilterControls(); - renderList(); - if (selectKey) { - const letters = getFilteredLetters(); - const letter = letters.find((l) => letterKey(l) === selectKey) || getLetters().find((l) => letterKey(l) === selectKey); - if (letter) renderDetail(letter); - } else { - resetDetail(); - } + browserUi.switchAlphabet(alpha, selectKey); } function updateTabs() { - [tabAll, tabHebrew, tabGreek, tabEnglish, tabArabic, tabEnochian].forEach((btn) => { - if (!btn) return; - btn.classList.toggle("alpha-tab-active", btn.dataset.alpha === state.activeAlphabet); - }); + browserUi.updateTabs(); } function resetDetail() { - if (detailNameEl) { - detailNameEl.textContent = "--"; - detailNameEl.classList.remove("alpha-detail-glyph"); - } - if (detailSubEl) detailSubEl.textContent = "Select a letter to explore"; - if (detailBodyEl) detailBodyEl.innerHTML = ""; + browserUi.resetDetail(); } // ── Public init ─────────────────────────────────────────────────────── diff --git a/app/ui-cube-selection.js b/app/ui-cube-selection.js new file mode 100644 index 0000000..62e114b --- /dev/null +++ b/app/ui-cube-selection.js @@ -0,0 +1,277 @@ +(function () { + "use strict"; + + function createCubeSelectionHelpers(dependencies) { + const { + state, + normalizeId, + normalizeEdgeId, + normalizeLetterKey, + toFiniteNumber, + getWalls, + getWallById, + getEdges, + getEdgeById, + getEdgeWalls, + getEdgesForWall, + getEdgeLetterId, + getWallFaceLetterId, + getEdgePathEntry, + getConnectorById, + snapRotationToWall, + render, + getElements + } = dependencies || {}; + + function applyPlacement(placement) { + const fallbackWallId = normalizeId(getWalls()[0]?.id); + const nextWallId = normalizeId(placement?.wallId || placement?.wall?.id || state.selectedWallId || fallbackWallId); + const wall = getWallById(nextWallId); + if (!wall) { + return false; + } + + state.selectedWallId = normalizeId(wall.id); + + const candidateEdgeId = normalizeEdgeId(placement?.edgeId || placement?.edge?.id); + const wallEdges = getEdgesForWall(state.selectedWallId); + const resolvedEdgeId = candidateEdgeId && getEdgeById(candidateEdgeId) + ? candidateEdgeId + : normalizeEdgeId(wallEdges[0]?.id || getEdges()[0]?.id); + + state.selectedEdgeId = resolvedEdgeId; + state.selectedNodeType = "wall"; + state.selectedConnectorId = null; + render(getElements()); + return true; + } + + function selectEdgeById(edgeId, preferredWallId = "") { + const edge = getEdgeById(edgeId); + if (!edge) { + return false; + } + + const currentWallId = normalizeId(state.selectedWallId); + const preferredId = normalizeId(preferredWallId); + const edgeWalls = getEdgeWalls(edge); + const nextWallId = preferredId && edgeWalls.includes(preferredId) + ? preferredId + : (edgeWalls.includes(currentWallId) ? currentWallId : (edgeWalls[0] || currentWallId)); + + state.selectedEdgeId = normalizeEdgeId(edge.id); + state.selectedNodeType = "wall"; + state.selectedConnectorId = null; + + if (nextWallId) { + if (nextWallId !== currentWallId) { + state.selectedWallId = nextWallId; + snapRotationToWall(nextWallId); + } else if (!state.selectedWallId) { + state.selectedWallId = nextWallId; + } + } + + render(getElements()); + return true; + } + + function selectWallById(wallId) { + if (!state.initialized) { + return false; + } + + const wall = getWallById(wallId); + if (!wall) { + return false; + } + + state.selectedWallId = normalizeId(wall.id); + state.selectedEdgeId = normalizeEdgeId(getEdgesForWall(state.selectedWallId)[0]?.id || getEdges()[0]?.id); + state.selectedNodeType = "wall"; + state.selectedConnectorId = null; + snapRotationToWall(state.selectedWallId); + render(getElements()); + return true; + } + + function selectConnectorById(connectorId) { + if (!state.initialized) { + return false; + } + + const connector = getConnectorById(connectorId); + if (!connector) { + return false; + } + + const fromWallId = normalizeId(connector.fromWallId); + if (fromWallId && getWallById(fromWallId)) { + state.selectedWallId = fromWallId; + state.selectedEdgeId = normalizeEdgeId(getEdgesForWall(fromWallId)[0]?.id || getEdges()[0]?.id); + snapRotationToWall(fromWallId); + } + + state.showConnectorLines = true; + state.selectedNodeType = "connector"; + state.selectedConnectorId = normalizeId(connector.id); + render(getElements()); + return true; + } + + function selectCenterNode() { + if (!state.initialized) { + return false; + } + + state.showPrimalPoint = true; + state.selectedNodeType = "center"; + state.selectedConnectorId = null; + render(getElements()); + return true; + } + + function selectPlacement(criteria = {}) { + if (!state.initialized) { + return false; + } + + const wallId = normalizeId(criteria.wallId); + const connectorId = normalizeId(criteria.connectorId); + const edgeId = normalizeEdgeId(criteria.edgeId || criteria.directionId); + const hebrewLetterId = normalizeLetterKey(criteria.hebrewLetterId); + const signId = normalizeId(criteria.signId || criteria.zodiacSignId); + const planetId = normalizeId(criteria.planetId); + const pathNo = toFiniteNumber(criteria.pathNo || criteria.kabbalahPathNumber); + const trumpNo = toFiniteNumber(criteria.trumpNumber || criteria.tarotTrumpNumber); + const nodeType = normalizeId(criteria.nodeType); + const centerRequested = nodeType === "center" + || Boolean(criteria.center) + || Boolean(criteria.primalPoint) + || normalizeId(criteria.centerId) === "primal-point"; + const edges = getEdges(); + + const findEdgeBy = (predicate) => edges.find((edge) => predicate(edge)) || null; + + const findWallForEdge = (edge, preferredWallId) => { + const edgeWalls = getEdgeWalls(edge); + if (preferredWallId && edgeWalls.includes(preferredWallId)) { + return preferredWallId; + } + return edgeWalls[0] || normalizeId(getWalls()[0]?.id); + }; + + if (connectorId) { + return selectConnectorById(connectorId); + } + + if (centerRequested) { + return selectCenterNode(); + } + + if (edgeId) { + const edge = getEdgeById(edgeId); + if (!edge) { + return false; + } + return applyPlacement({ + wallId: findWallForEdge(edge, wallId), + edgeId + }); + } + + if (wallId) { + const wall = getWallById(wallId); + if (!wall) { + return false; + } + + if (!edgeId) { + return selectWallById(wallId); + } + + const firstEdge = getEdgesForWall(wallId)[0] || null; + return applyPlacement({ wallId, edgeId: firstEdge?.id }); + } + + if (hebrewLetterId) { + const byHebrew = findEdgeBy((edge) => getEdgeLetterId(edge) === hebrewLetterId); + if (byHebrew) { + return applyPlacement({ + wallId: findWallForEdge(byHebrew), + edgeId: byHebrew.id + }); + } + + const byWallFace = getWalls().find((wall) => getWallFaceLetterId(wall) === hebrewLetterId) || null; + if (byWallFace) { + const byWallFaceId = normalizeId(byWallFace.id); + const firstEdge = getEdgesForWall(byWallFaceId)[0] || null; + return applyPlacement({ wallId: byWallFaceId, edgeId: firstEdge?.id }); + } + } + + if (signId) { + const bySign = findEdgeBy((edge) => { + const astrology = getEdgePathEntry(edge)?.astrology || {}; + return normalizeId(astrology.type) === "zodiac" && normalizeId(astrology.name) === signId; + }); + if (bySign) { + return applyPlacement({ + wallId: findWallForEdge(bySign), + edgeId: bySign.id + }); + } + } + + if (pathNo != null) { + const byPath = findEdgeBy((edge) => toFiniteNumber(getEdgePathEntry(edge)?.pathNumber) === pathNo); + if (byPath) { + return applyPlacement({ + wallId: findWallForEdge(byPath), + edgeId: byPath.id + }); + } + } + + if (trumpNo != null) { + const byTrump = findEdgeBy((edge) => { + const tarot = getEdgePathEntry(edge)?.tarot || {}; + return toFiniteNumber(tarot.trumpNumber) === trumpNo; + }); + if (byTrump) { + return applyPlacement({ + wallId: findWallForEdge(byTrump), + edgeId: byTrump.id + }); + } + } + + if (planetId) { + const wall = getWalls().find((entry) => normalizeId(entry?.associations?.planetId) === planetId); + if (wall) { + const wallIdByPlanet = normalizeId(wall.id); + return applyPlacement({ + wallId: wallIdByPlanet, + edgeId: getEdgesForWall(wallIdByPlanet)[0]?.id + }); + } + } + + return false; + } + + return { + applyPlacement, + selectEdgeById, + selectWallById, + selectConnectorById, + selectCenterNode, + selectPlacement + }; + } + + window.CubeSelectionHelpers = { + createCubeSelectionHelpers + }; +})(); \ No newline at end of file diff --git a/app/ui-cube.js b/app/ui-cube.js index 3886a80..52ff254 100644 --- a/app/ui-cube.js +++ b/app/ui-cube.js @@ -122,6 +122,7 @@ const cubeDetailUi = window.CubeDetailUi || {}; const cubeChassisUi = window.CubeChassisUi || {}; const cubeMathHelpers = window.CubeMathHelpers || {}; + const cubeSelectionHelpers = window.CubeSelectionHelpers || {}; function getElements() { return { @@ -256,6 +257,10 @@ throw new Error("CubeMathHelpers.createCubeMathHelpers is unavailable. Ensure app/ui-cube-math.js loads before app/ui-cube.js."); } + if (typeof cubeSelectionHelpers.createCubeSelectionHelpers !== "function") { + throw new Error("CubeSelectionHelpers.createCubeSelectionHelpers is unavailable. Ensure app/ui-cube-selection.js loads before app/ui-cube.js."); + } + function normalizeAngle(angle) { return cubeMathUi.normalizeAngle(angle); } @@ -432,6 +437,27 @@ getCubeCenterData }); + const cubeSelectionUi = cubeSelectionHelpers.createCubeSelectionHelpers({ + state, + normalizeId, + normalizeEdgeId, + normalizeLetterKey, + toFiniteNumber, + getWalls, + getWallById, + getEdges, + getEdgeById, + getEdgeWalls, + getEdgesForWall, + getEdgeLetterId, + getWallFaceLetterId, + getEdgePathEntry, + getConnectorById, + snapRotationToWall, + render, + getElements + }); + function getEdgeAstrologySymbol(edge) { return cubeMathUi.getEdgeAstrologySymbol(edge); } @@ -488,26 +514,7 @@ } function applyPlacement(placement) { - const fallbackWallId = normalizeId(getWalls()[0]?.id); - const nextWallId = normalizeId(placement?.wallId || placement?.wall?.id || state.selectedWallId || fallbackWallId); - const wall = getWallById(nextWallId); - if (!wall) { - return false; - } - - state.selectedWallId = normalizeId(wall.id); - - const candidateEdgeId = normalizeEdgeId(placement?.edgeId || placement?.edge?.id); - const wallEdges = getEdgesForWall(state.selectedWallId); - const resolvedEdgeId = candidateEdgeId && getEdgeById(candidateEdgeId) - ? candidateEdgeId - : normalizeEdgeId(wallEdges[0]?.id || getEdges()[0]?.id); - - state.selectedEdgeId = resolvedEdgeId; - state.selectedNodeType = "wall"; - state.selectedConnectorId = null; - render(getElements()); - return true; + return cubeSelectionUi.applyPlacement(placement); } function toDisplayText(value) { @@ -559,33 +566,7 @@ } function selectEdgeById(edgeId, preferredWallId = "") { - const edge = getEdgeById(edgeId); - if (!edge) { - return false; - } - - const currentWallId = normalizeId(state.selectedWallId); - const preferredId = normalizeId(preferredWallId); - const edgeWalls = getEdgeWalls(edge); - const nextWallId = preferredId && edgeWalls.includes(preferredId) - ? preferredId - : (edgeWalls.includes(currentWallId) ? currentWallId : (edgeWalls[0] || currentWallId)); - - state.selectedEdgeId = normalizeEdgeId(edge.id); - state.selectedNodeType = "wall"; - state.selectedConnectorId = null; - - if (nextWallId) { - if (nextWallId !== currentWallId) { - state.selectedWallId = nextWallId; - snapRotationToWall(nextWallId); - } else if (!state.selectedWallId) { - state.selectedWallId = nextWallId; - } - } - - render(getElements()); - return true; + return cubeSelectionUi.selectEdgeById(edgeId, preferredWallId); } function renderDetail(elements, walls) { @@ -710,195 +691,19 @@ } function selectWallById(wallId) { - if (!state.initialized) { - return false; - } - - const wall = getWallById(wallId); - if (!wall) { - return false; - } - - state.selectedWallId = normalizeId(wall.id); - state.selectedEdgeId = normalizeEdgeId(getEdgesForWall(state.selectedWallId)[0]?.id || getEdges()[0]?.id); - state.selectedNodeType = "wall"; - state.selectedConnectorId = null; - snapRotationToWall(state.selectedWallId); - render(getElements()); - return true; + return cubeSelectionUi.selectWallById(wallId); } function selectConnectorById(connectorId) { - if (!state.initialized) { - return false; - } - - const connector = getConnectorById(connectorId); - if (!connector) { - return false; - } - - const fromWallId = normalizeId(connector.fromWallId); - if (fromWallId && getWallById(fromWallId)) { - state.selectedWallId = fromWallId; - state.selectedEdgeId = normalizeEdgeId(getEdgesForWall(fromWallId)[0]?.id || getEdges()[0]?.id); - snapRotationToWall(fromWallId); - } - - state.showConnectorLines = true; - state.selectedNodeType = "connector"; - state.selectedConnectorId = normalizeId(connector.id); - render(getElements()); - return true; + return cubeSelectionUi.selectConnectorById(connectorId); } function selectCenterNode() { - if (!state.initialized) { - return false; - } - - state.showPrimalPoint = true; - state.selectedNodeType = "center"; - state.selectedConnectorId = null; - render(getElements()); - return true; + return cubeSelectionUi.selectCenterNode(); } function selectPlacement(criteria = {}) { - if (!state.initialized) { - return false; - } - - const wallId = normalizeId(criteria.wallId); - const connectorId = normalizeId(criteria.connectorId); - const edgeId = normalizeEdgeId(criteria.edgeId || criteria.directionId); - const hebrewLetterId = normalizeLetterKey(criteria.hebrewLetterId); - const signId = normalizeId(criteria.signId || criteria.zodiacSignId); - const planetId = normalizeId(criteria.planetId); - const pathNo = toFiniteNumber(criteria.pathNo || criteria.kabbalahPathNumber); - const trumpNo = toFiniteNumber(criteria.trumpNumber || criteria.tarotTrumpNumber); - const nodeType = normalizeId(criteria.nodeType); - const centerRequested = nodeType === "center" - || Boolean(criteria.center) - || Boolean(criteria.primalPoint) - || normalizeId(criteria.centerId) === "primal-point"; - const edges = getEdges(); - - const findEdgeBy = (predicate) => edges.find((edge) => predicate(edge)) || null; - - const findWallForEdge = (edge, preferredWallId) => { - const edgeWalls = getEdgeWalls(edge); - if (preferredWallId && edgeWalls.includes(preferredWallId)) { - return preferredWallId; - } - return edgeWalls[0] || normalizeId(getWalls()[0]?.id); - }; - - if (connectorId) { - return selectConnectorById(connectorId); - } - - if (centerRequested) { - return selectCenterNode(); - } - - if (edgeId) { - const edge = getEdgeById(edgeId); - if (!edge) { - return false; - } - return applyPlacement({ - wallId: findWallForEdge(edge, wallId), - edgeId - }); - } - - if (wallId) { - const wall = getWallById(wallId); - if (!wall) { - return false; - } - - // if an explicit edge id was not provided (or was empty) we treat this - // as a request to show the wall/face itself rather than any particular - // edge direction. `applyPlacement` only knows how to highlight edges, - // so we fall back to selecting the wall directly in that case. this - // is the behaviour we want when navigating from a "face" letter like - // dalet, where the placement computed by ui-alphabet leaves edgeId - // blank. - if (!edgeId) { - return selectWallById(wallId); - } - - const firstEdge = getEdgesForWall(wallId)[0] || null; - return applyPlacement({ wallId, edgeId: firstEdge?.id }); - } - - if (hebrewLetterId) { - const byHebrew = findEdgeBy((edge) => getEdgeLetterId(edge) === hebrewLetterId); - if (byHebrew) { - return applyPlacement({ - wallId: findWallForEdge(byHebrew), - edgeId: byHebrew.id - }); - } - - const byWallFace = getWalls().find((wall) => getWallFaceLetterId(wall) === hebrewLetterId) || null; - if (byWallFace) { - const byWallFaceId = normalizeId(byWallFace.id); - const firstEdge = getEdgesForWall(byWallFaceId)[0] || null; - return applyPlacement({ wallId: byWallFaceId, edgeId: firstEdge?.id }); - } - } - - if (signId) { - const bySign = findEdgeBy((edge) => { - const astrology = getEdgePathEntry(edge)?.astrology || {}; - return normalizeId(astrology.type) === "zodiac" && normalizeId(astrology.name) === signId; - }); - if (bySign) { - return applyPlacement({ - wallId: findWallForEdge(bySign), - edgeId: bySign.id - }); - } - } - - if (pathNo != null) { - const byPath = findEdgeBy((edge) => toFiniteNumber(getEdgePathEntry(edge)?.pathNumber) === pathNo); - if (byPath) { - return applyPlacement({ - wallId: findWallForEdge(byPath), - edgeId: byPath.id - }); - } - } - - if (trumpNo != null) { - const byTrump = findEdgeBy((edge) => { - const tarot = getEdgePathEntry(edge)?.tarot || {}; - return toFiniteNumber(tarot.trumpNumber) === trumpNo; - }); - if (byTrump) { - return applyPlacement({ - wallId: findWallForEdge(byTrump), - edgeId: byTrump.id - }); - } - } - - if (planetId) { - const wall = getWalls().find((entry) => normalizeId(entry?.associations?.planetId) === planetId); - if (wall) { - const wallIdByPlanet = normalizeId(wall.id); - return applyPlacement({ - wallId: wallIdByPlanet, - edgeId: getEdgesForWall(wallIdByPlanet)[0]?.id - }); - } - } - - return false; + return cubeSelectionUi.selectPlacement(criteria); } function selectByHebrewLetterId(hebrewLetterId) { diff --git a/app/ui-now-helpers.js b/app/ui-now-helpers.js index 6e42e7d..2facf1c 100644 --- a/app/ui-now-helpers.js +++ b/app/ui-now-helpers.js @@ -509,6 +509,7 @@ } window.NowUiHelpers = { + getSignStartDate, findNextDecanTransition, findNextMoonPhaseTransition, formatCountdown, diff --git a/app/ui-now.js b/app/ui-now.js index 7070bff..bbab26d 100644 --- a/app/ui-now.js +++ b/app/ui-now.js @@ -4,6 +4,7 @@ const { DAY_IN_MS, getDateKey, + getDecanForDate, getMoonPhaseName, calcPlanetaryHoursForDayAndLocation } = window.TarotCalc; @@ -13,6 +14,7 @@ typeof nowUiHelpers.findNextDecanTransition !== "function" || typeof nowUiHelpers.findNextMoonPhaseTransition !== "function" || typeof nowUiHelpers.formatCountdown !== "function" + || typeof nowUiHelpers.getSignStartDate !== "function" || typeof nowUiHelpers.getDisplayTarotName !== "function" || typeof nowUiHelpers.setNowCardImage !== "function" || typeof nowUiHelpers.updateNowStats !== "function" @@ -128,7 +130,7 @@ ? `${sunInfo.sign.id}-${sunInfo.decan?.index || 1}` : "no-decan"; if (sunInfo?.sign) { - const signStartDate = getSignStartDate(now, sunInfo.sign); + const signStartDate = nowUiHelpers.getSignStartDate(now, sunInfo.sign); const daysSinceSignStart = (now.getTime() - signStartDate.getTime()) / DAY_IN_MS; const signDegree = Math.min(29.9, Math.max(0, daysSinceSignStart)); const signMajorName = nowUiHelpers.getDisplayTarotName(sunInfo.sign.tarot.majorArcana, sunInfo.sign.tarot.trumpNumber); diff --git a/app/ui-tarot.js b/app/ui-tarot.js index d392c66..cf754e5 100644 --- a/app/ui-tarot.js +++ b/app/ui-tarot.js @@ -188,6 +188,19 @@ } }; + const MINOR_PLURAL_BY_RANK = { + ace: "aces", + two: "twos", + three: "threes", + four: "fours", + five: "fives", + six: "sixes", + seven: "sevens", + eight: "eights", + nine: "nines", + ten: "tens" + }; + function slugify(value) { return String(value || "") .trim() @@ -473,19 +486,6 @@ return tarotCardDerivationsUi.buildTypeLabel(card); } - const MINOR_PLURAL_BY_RANK = { - ace: "aces", - two: "twos", - three: "threes", - four: "fours", - five: "fives", - six: "sixes", - seven: "sevens", - eight: "eights", - nine: "nines", - ten: "tens" - }; - function findSephirahForMinorCard(card, kabTree) { return tarotCardDerivationsUi.findSephirahForMinorCard(card, kabTree); } diff --git a/index.html b/index.html index 5aa3fb6..5283302 100644 --- a/index.html +++ b/index.html @@ -804,8 +804,10 @@ + +