diff --git a/app.js b/app.js index 747daf5..f6e2129 100644 --- a/app.js +++ b/app.js @@ -278,6 +278,8 @@ appRuntime.init?.({ calendarVisualsUi, homeUi, hasTarotAccess: () => hasTarotFeatureAccess(), + shouldPollNow: () => (sectionStateUi.getActiveSection?.() || "home") === "home" && document.hidden !== true, + nowPollIntervalMs: 5 * 60 * 1000, onStatus: (text) => setStatus(text), services: { getCenteredWeekStartDay, diff --git a/app/app-runtime.js b/app/app-runtime.js index 048af24..032a864 100644 --- a/app/app-runtime.js +++ b/app/app-runtime.js @@ -12,6 +12,8 @@ homeUi: null, onStatus: null, hasTarotAccess: () => false, + shouldPollNow: () => true, + nowPollIntervalMs: 5 * 60 * 1000, services: {}, ensure: {} }; @@ -20,6 +22,7 @@ let magickDataset = null; let currentGeo = null; let nowInterval = null; + let runtimeListenersBound = false; let centeredDayKey = ""; let renderInProgress = false; let currentTimeFormat = "minutes"; @@ -74,11 +77,17 @@ } function startNowTicker() { - if (nowInterval) { - clearInterval(nowInterval); - } + stopNowTicker(); + + const pollIntervalMs = Number.isFinite(Number(config.nowPollIntervalMs)) + ? Math.max(1000, Math.trunc(Number(config.nowPollIntervalMs))) + : 5 * 60 * 1000; const tick = async () => { + if (config.shouldPollNow?.() === false) { + return; + } + if (!referenceData || !currentGeo || renderInProgress) { return; } @@ -102,7 +111,27 @@ void tick(); nowInterval = setInterval(() => { void tick(); - }, 1000); + }, pollIntervalMs); + } + + function stopNowTicker() { + if (!nowInterval) { + return; + } + + clearInterval(nowInterval); + nowInterval = null; + } + + function syncNowTickerState() { + if (config.shouldPollNow?.() === false) { + stopNowTicker(); + return; + } + + if (!nowInterval && referenceData && currentGeo) { + startNowTicker(); + } } async function renderWeek() { @@ -194,6 +223,18 @@ } centeredDayKey = config.services.getDateKey?.(new Date()) || centeredDayKey; + + if (!runtimeListenersBound) { + document.addEventListener("section:changed", () => { + syncNowTickerState(); + }); + + document.addEventListener("visibilitychange", () => { + syncNowTickerState(); + }); + + runtimeListenersBound = true; + } } window.TarotAppRuntime = { diff --git a/app/data-service.js b/app/data-service.js index c8533d6..bf1a0ee 100644 --- a/app/data-service.js +++ b/app/data-service.js @@ -11,6 +11,14 @@ const textLexiconCache = new Map(); const textLexiconOccurrencesCache = new Map(); const textSearchCache = new Map(); + const NOW_SNAPSHOT_MIN_INTERVAL_MS = 5 * 60 * 1000; + let nowSnapshotCache = { + geoKey: "", + fetchedAtMs: 0, + value: null, + pendingGeoKey: "", + pendingPromise: null + }; const DATA_ROOT = "data"; const MAGICK_ROOT = DATA_ROOT; @@ -215,6 +223,26 @@ return url.toString(); } + function normalizeGeoKey(geo) { + const latitude = Number(geo?.latitude); + const longitude = Number(geo?.longitude); + + if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) { + return ""; + } + + return `${latitude.toFixed(6)},${longitude.toFixed(6)}`; + } + + function isNowSnapshotPollingEnabled() { + if (typeof document !== "undefined" && document.hidden === true) { + return false; + } + + const activeSection = String(window.TarotSectionStateUi?.getActiveSection?.() || "home").trim(); + return activeSection === "home"; + } + function toApiAssetUrl(assetPath) { const { apiBaseUrl, apiKey } = resolveConnectionSettings(); const normalizedAssetPath = String(assetPath || "") @@ -247,6 +275,13 @@ textLexiconCache.clear(); textLexiconOccurrencesCache.clear(); textSearchCache.clear(); + nowSnapshotCache = { + geoKey: "", + fetchedAtMs: 0, + value: null, + pendingGeoKey: "", + pendingPromise: null + }; } function normalizeTarotName(value) { @@ -370,11 +405,58 @@ } async function fetchNowSnapshot(geo, timestamp = new Date()) { - return fetchJson(buildApiUrl("/api/v1/now", { + const geoKey = normalizeGeoKey(geo); + const nowMs = Date.now(); + + if (!isNowSnapshotPollingEnabled()) { + if (geoKey && nowSnapshotCache.pendingPromise && nowSnapshotCache.pendingGeoKey === geoKey) { + return nowSnapshotCache.pendingPromise; + } + + if (geoKey && geoKey === nowSnapshotCache.geoKey && nowSnapshotCache.value) { + return nowSnapshotCache.value; + } + + return null; + } + + if ( + geoKey + && geoKey === nowSnapshotCache.geoKey + && nowSnapshotCache.value + && Number.isFinite(nowSnapshotCache.fetchedAtMs) + && (nowMs - nowSnapshotCache.fetchedAtMs) < NOW_SNAPSHOT_MIN_INTERVAL_MS + ) { + return nowSnapshotCache.value; + } + + if (geoKey && nowSnapshotCache.pendingPromise && nowSnapshotCache.pendingGeoKey === geoKey) { + return nowSnapshotCache.pendingPromise; + } + + const requestPromise = fetchJson(buildApiUrl("/api/v1/now", { latitude: geo?.latitude, longitude: geo?.longitude, date: timestamp instanceof Date ? timestamp.toISOString() : timestamp - })); + })) + .then((snapshot) => { + if (geoKey) { + nowSnapshotCache.geoKey = geoKey; + nowSnapshotCache.fetchedAtMs = Date.now(); + nowSnapshotCache.value = snapshot; + } + return snapshot; + }) + .finally(() => { + if (nowSnapshotCache.pendingPromise === requestPromise) { + nowSnapshotCache.pendingPromise = null; + nowSnapshotCache.pendingGeoKey = ""; + } + }); + + nowSnapshotCache.pendingPromise = requestPromise; + nowSnapshotCache.pendingGeoKey = geoKey; + return requestPromise; } async function loadTarotCards(filters = {}) { diff --git a/app/ui-alphabet-browser.js b/app/ui-alphabet-browser.js index ca0701f..a010197 100644 --- a/app/ui-alphabet-browser.js +++ b/app/ui-alphabet-browser.js @@ -10,6 +10,91 @@ dhal: "Dhal", dad: "Dad", dha: "Dha", ghayn: "Ghayn" }; + const GREEK_NATIVE_NAMES = { + alpha: { classical: "ἄλφα", koine: "άλφα" }, + beta: { classical: "βῆτα", koine: "βήτα" }, + gamma: { classical: "γάμμα", koine: "γάμμα" }, + delta: { classical: "δέλτα", koine: "δέλτα" }, + epsilon: { classical: "ἒ ψιλόν", koine: "έψιλον" }, + zeta: { classical: "ζῆτα", koine: "ζήτα" }, + eta: { classical: "ἦτα", koine: "ήτα" }, + theta: { classical: "θῆτα", koine: "θήτα" }, + iota: { classical: "ἰῶτα", koine: "ιώτα" }, + kappa: { classical: "κάππα", koine: "κάππα" }, + lambda: { classical: "λάμβδα", koine: "λάμδα" }, + mu: { classical: "μῦ", koine: "μι" }, + nu: { classical: "νῦ", koine: "νι" }, + xi: { classical: "ξῖ", koine: "ξι" }, + omicron: { classical: "ὂ μικρόν", koine: "όμικρον" }, + pi: { classical: "πῖ", koine: "πι" }, + rho: { classical: "ῥῶ", koine: "ρω" }, + sigma: { classical: "σῖγμα", koine: "σίγμα" }, + tau: { classical: "ταῦ", koine: "ταυ" }, + upsilon: { classical: "ὖ ψιλόν", koine: "ύψιλον" }, + phi: { classical: "φῖ", koine: "φι" }, + chi: { classical: "χῖ", koine: "χι" }, + psi: { classical: "ψῖ", koine: "ψι" }, + omega: { classical: "ὦ μέγα", koine: "ωμέγα" }, + digamma: { classical: "δίγαμμα", koine: "δίγαμμα" }, + qoppa: { classical: "κόππα", koine: "κόππα" }, + sampi: { classical: "σαμπί", koine: "σαμπί" } + }; + + const GREEK_TRANSLITERATIONS = { + alpha: { classical: "A", koine: "A" }, + beta: { classical: "B", koine: "V" }, + gamma: { classical: "G", koine: "G" }, + delta: { classical: "D", koine: "Th" }, + epsilon: { classical: "E", koine: "E" }, + zeta: { classical: "Z", koine: "Z" }, + eta: { classical: "E", koine: "I" }, + theta: { classical: "Th", koine: "Th" }, + iota: { classical: "I", koine: "I" }, + kappa: { classical: "K", koine: "K" }, + lambda: { classical: "L", koine: "L" }, + mu: { classical: "M", koine: "M" }, + nu: { classical: "N", koine: "N" }, + xi: { classical: "X", koine: "X" }, + omicron: { classical: "O", koine: "O" }, + pi: { classical: "P", koine: "P" }, + rho: { classical: "R", koine: "R" }, + sigma: { classical: "S", koine: "S" }, + tau: { classical: "T", koine: "T" }, + upsilon: { classical: "U/Y", koine: "I" }, + phi: { classical: "Ph", koine: "F" }, + chi: { classical: "Kh/Ch", koine: "Ch" }, + psi: { classical: "Ps", koine: "Ps" }, + omega: { classical: "O", koine: "O" }, + digamma: { classical: "W", koine: "V" }, + qoppa: { classical: "Q", koine: "Q" }, + sampi: { classical: "Ss/Ts", koine: "Ss/Ts" } + }; + + const HEBREW_NATIVE_NAMES = { + alef: "אלף", + bet: "בית", + gimel: "גימל", + dalet: "דלת", + he: "הא", + vav: "וו", + zayin: "זין", + het: "חית", + tet: "טית", + yod: "יוד", + kaf: "כף", + lamed: "למד", + mem: "מם", + nun: "נון", + samekh: "סמך", + ayin: "עין", + pe: "פה", + tsadi: "צדי", + qof: "קוף", + resh: "ריש", + shin: "שין", + tav: "תו" + }; + function createAlphabetBrowser(dependencies) { const { state, @@ -22,15 +107,109 @@ return value ? value.charAt(0).toUpperCase() + value.slice(1) : ""; } + function alphabetDisplayLabel(alphabet) { + if (alphabet === "greek") return "Greek (Classical/Koine)"; + if (alphabet === "greekArchaic") return "Greek (Archaic)"; + return capitalize(alphabet); + } + function arabicDisplayName(letter) { return ARABIC_DISPLAY_NAMES[letter && letter.name] || (String(letter && letter.name || "").charAt(0).toUpperCase() + String(letter && letter.name || "").slice(1)); } + function greekNativeNames(letter) { + const key = String(letter?.name || "").trim().toLowerCase(); + const names = GREEK_NATIVE_NAMES[key]; + if (!names) { + return { classical: "", koine: "" }; + } + + return { + classical: String(names.classical || "").trim(), + koine: String(names.koine || "").trim() + }; + } + + function toGreekUppercase(value) { + return String(value || "") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toUpperCase(); + } + + function formatGreekNativeNames(letter, options = {}) { + const useUppercase = options?.uppercase === true; + const names = greekNativeNames(letter); + const classicalName = useUppercase ? toGreekUppercase(names.classical) : names.classical; + const koineName = useUppercase ? toGreekUppercase(names.koine) : names.koine; + if (classicalName && koineName && classicalName !== koineName) { + return `${classicalName} / ${koineName}`; + } + + return classicalName || koineName || ""; + } + + function hebrewNativeName(letter) { + const key = String(letter?.hebrewLetterId || "").trim().toLowerCase(); + return HEBREW_NATIVE_NAMES[key] || ""; + } + + function greekTransliterationVariants(letter) { + const key = String(letter?.name || "").trim().toLowerCase(); + const variants = GREEK_TRANSLITERATIONS[key]; + if (!variants) { + const fallback = String(letter?.transliteration || "").trim(); + return { + classical: fallback, + koine: fallback + }; + } + + return { + classical: String(variants.classical || "").trim(), + koine: String(variants.koine || "").trim() + }; + } + + function formatGreekTransliteration(letter) { + const variants = greekTransliterationVariants(letter); + if (variants.classical && variants.koine && variants.classical !== variants.koine) { + return `${variants.classical} / ${variants.koine}`; + } + return variants.classical || variants.koine || String(letter?.transliteration || "").trim(); + } + + function greekPlaceValueByIndex(index, alphabet) { + const value = Number(index); + if (!Number.isFinite(value)) { + return null; + } + + const position = Math.trunc(value); + if (position <= 0) { + return null; + } + + if (alphabet === "greekArchaic") { + return null; + } + + if (position <= 9) { + return position; + } + + if (position <= 18) { + return (position - 9) * 10; + } + + return (position - 18) * 100; + } + function getLetters() { if (!state.alphabets) return []; if (state.activeAlphabet === "all") { - const alphabetOrder = ["hebrew", "greek", "english", "arabic", "enochian"]; + const alphabetOrder = ["hebrew", "greek", "greekArchaic", "english", "arabic", "enochian"]; return alphabetOrder.flatMap((alphabet) => { const rows = Array.isArray(state.alphabets?.[alphabet]) ? state.alphabets[alphabet] : []; return rows.map((row) => ({ ...row, __alphabet: alphabet })); @@ -49,6 +228,7 @@ function letterKeyByAlphabet(alphabet, letter) { if (alphabet === "hebrew") return letter.hebrewLetterId; if (alphabet === "greek") return letter.name; + if (alphabet === "greekArchaic") return letter.name; if (alphabet === "english") return letter.letter; if (alphabet === "arabic") return letter.name; if (alphabet === "enochian") return letter.id; @@ -68,6 +248,7 @@ const alphabet = alphabetForLetter(letter); if (alphabet === "hebrew") return letter.char; if (alphabet === "greek") return letter.char; + if (alphabet === "greekArchaic") return letter.char; if (alphabet === "english") return letter.letter; if (alphabet === "arabic") return letter.char; if (alphabet === "enochian") return letter.char; @@ -78,6 +259,7 @@ const alphabet = alphabetForLetter(letter); if (alphabet === "hebrew") return letter.name; if (alphabet === "greek") return letter.displayName; + if (alphabet === "greekArchaic") return letter.displayName; if (alphabet === "english") return letter.letter; if (alphabet === "arabic") return arabicDisplayName(letter); if (alphabet === "enochian") return letter.title; @@ -86,8 +268,23 @@ 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 === "hebrew") { + const nativeName = hebrewNativeName(letter); + return `${nativeName ? `${nativeName} · ` : ""}${letter.transliteration} · ${letter.letterType} · ${letter.numerology}`; + } + if (alphabet === "greek") { + const nativeName = formatGreekNativeNames(letter, { uppercase: true }); + const transliteration = formatGreekTransliteration(letter); + const orderlyValue = Number.isFinite(Number(letter?.numerology)) ? Math.trunc(Number(letter.numerology)) : "—"; + const placeValue = greekPlaceValueByIndex(letter?.index, alphabet); + return `${nativeName ? `${nativeName} · ` : ""}${transliteration} · isopsephy orderly ${orderlyValue}${Number.isFinite(placeValue) ? ` · 1-10-100 ${placeValue}` : ""}${letter.archaic ? " · archaic" : ""}`; + } + if (alphabet === "greekArchaic") { + const nativeName = formatGreekNativeNames(letter, { uppercase: true }); + const transliteration = formatGreekTransliteration(letter); + const orderlyValue = Number.isFinite(Number(letter?.numerology)) ? Math.trunc(Number(letter.numerology)) : "—"; + return `${nativeName ? `${nativeName} · ` : ""}${transliteration} · isopsephy orderly ${orderlyValue} · 1-10-100 ${orderlyValue} · 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}`; @@ -315,7 +512,7 @@ const meta = document.createElement("span"); meta.className = "alpha-list-meta"; - const alphaLabel = alphabet ? `${capitalize(alphabet)} · ` : ""; + const alphaLabel = alphabet ? `${alphabetDisplayLabel(alphabet)} · ` : ""; meta.innerHTML = `${alphaLabel}${displayLabel(letter)}${displaySub(letter)}`; item.appendChild(glyph); @@ -346,8 +543,8 @@ } function updateTabs() { - const { tabAll, tabHebrew, tabGreek, tabEnglish, tabArabic, tabEnochian } = getDomRefs(); - [tabAll, tabHebrew, tabGreek, tabEnglish, tabArabic, tabEnochian].forEach((btn) => { + const { tabAll, tabHebrew, tabGreek, tabGreekArchaic, tabEnglish, tabArabic, tabEnochian } = getDomRefs(); + [tabAll, tabHebrew, tabGreek, tabGreekArchaic, tabEnglish, tabArabic, tabEnochian].forEach((btn) => { if (!btn) return; btn.classList.toggle("alpha-tab-active", btn.dataset.alpha === state.activeAlphabet); }); diff --git a/app/ui-alphabet-detail.js b/app/ui-alphabet-detail.js index 9d7a346..a4dc9be 100644 --- a/app/ui-alphabet-detail.js +++ b/app/ui-alphabet-detail.js @@ -215,6 +215,329 @@ .replace(/[^A-Z]/g, ""); } + const GREEK_NATIVE_NAMES = { + alpha: { classical: "ἄλφα", koine: "άλφα" }, + beta: { classical: "βῆτα", koine: "βήτα" }, + gamma: { classical: "γάμμα", koine: "γάμμα" }, + delta: { classical: "δέλτα", koine: "δέλτα" }, + epsilon: { classical: "ἒ ψιλόν", koine: "έψιλον" }, + zeta: { classical: "ζῆτα", koine: "ζήτα" }, + eta: { classical: "ἦτα", koine: "ήτα" }, + theta: { classical: "θῆτα", koine: "θήτα" }, + iota: { classical: "ἰῶτα", koine: "ιώτα" }, + kappa: { classical: "κάππα", koine: "κάππα" }, + lambda: { classical: "λάμβδα", koine: "λάμδα" }, + mu: { classical: "μῦ", koine: "μι" }, + nu: { classical: "νῦ", koine: "νι" }, + xi: { classical: "ξῖ", koine: "ξι" }, + omicron: { classical: "ὂ μικρόν", koine: "όμικρον" }, + pi: { classical: "πῖ", koine: "πι" }, + rho: { classical: "ῥῶ", koine: "ρω" }, + sigma: { classical: "σῖγμα", koine: "σίγμα" }, + tau: { classical: "ταῦ", koine: "ταυ" }, + upsilon: { classical: "ὖ ψιλόν", koine: "ύψιλον" }, + phi: { classical: "φῖ", koine: "φι" }, + chi: { classical: "χῖ", koine: "χι" }, + psi: { classical: "ψῖ", koine: "ψι" }, + omega: { classical: "ὦ μέγα", koine: "ωμέγα" }, + digamma: { classical: "δίγαμμα", koine: "δίγαμμα" }, + qoppa: { classical: "κόππα", koine: "κόππα" }, + sampi: { classical: "σαμπί", koine: "σαμπί" } + }; + + const GREEK_TRANSLITERATIONS = { + alpha: { classical: "A", koine: "A" }, + beta: { classical: "B", koine: "V" }, + gamma: { classical: "G", koine: "G" }, + delta: { classical: "D", koine: "Th" }, + epsilon: { classical: "E", koine: "E" }, + zeta: { classical: "Z", koine: "Z" }, + eta: { classical: "E", koine: "I" }, + theta: { classical: "Th", koine: "Th" }, + iota: { classical: "I", koine: "I" }, + kappa: { classical: "K", koine: "K" }, + lambda: { classical: "L", koine: "L" }, + mu: { classical: "M", koine: "M" }, + nu: { classical: "N", koine: "N" }, + xi: { classical: "X", koine: "X" }, + omicron: { classical: "O", koine: "O" }, + pi: { classical: "P", koine: "P" }, + rho: { classical: "R", koine: "R" }, + sigma: { classical: "S", koine: "S" }, + tau: { classical: "T", koine: "T" }, + upsilon: { classical: "U/Y", koine: "I" }, + phi: { classical: "Ph", koine: "F" }, + chi: { classical: "Kh/Ch", koine: "Ch" }, + psi: { classical: "Ps", koine: "Ps" }, + omega: { classical: "O", koine: "O" }, + digamma: { classical: "W", koine: "V" }, + qoppa: { classical: "Q", koine: "Q" }, + sampi: { classical: "Ss/Ts", koine: "Ss/Ts" } + }; + + const HEBREW_NATIVE_NAMES = { + alef: "אלף", + bet: "בית", + gimel: "גימל", + dalet: "דלת", + he: "הא", + vav: "וו", + zayin: "זין", + het: "חית", + tet: "טית", + yod: "יוד", + kaf: "כף", + lamed: "למד", + mem: "מם", + nun: "נון", + samekh: "סמך", + ayin: "עין", + pe: "פה", + tsadi: "צדי", + qof: "קוף", + resh: "ריש", + shin: "שין", + tav: "תו" + }; + + function greekNativeNamesByLetter(letter) { + const key = String(letter?.name || "").trim().toLowerCase(); + const names = GREEK_NATIVE_NAMES[key]; + if (!names) { + return { classical: "", koine: "" }; + } + + return { + classical: String(names.classical || "").trim(), + koine: String(names.koine || "").trim() + }; + } + + function formatGreekNativeNamesByLetter(letter) { + const names = greekNativeNamesByLetter(letter); + if (names.classical && names.koine && names.classical !== names.koine) { + return `${names.classical} / ${names.koine}`; + } + + return names.classical || names.koine || ""; + } + + function toGreekUppercase(value) { + return String(value || "") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toUpperCase(); + } + + function formatGreekNativeUppercaseByLetter(letter) { + const names = greekNativeNamesByLetter(letter); + const classicalName = toGreekUppercase(names.classical); + const koineName = toGreekUppercase(names.koine); + if (classicalName && koineName && classicalName !== koineName) { + return `${classicalName} / ${koineName}`; + } + + return classicalName || koineName || ""; + } + + function greekTransliterationVariantsByLetter(letter) { + const key = String(letter?.name || "").trim().toLowerCase(); + const variants = GREEK_TRANSLITERATIONS[key]; + if (!variants) { + const fallback = String(letter?.transliteration || "").trim(); + return { + classical: fallback, + koine: fallback + }; + } + + return { + classical: String(variants.classical || "").trim(), + koine: String(variants.koine || "").trim() + }; + } + + function formatGreekTransliterationByLetter(letter) { + const variants = greekTransliterationVariantsByLetter(letter); + if (variants.classical && variants.koine && variants.classical !== variants.koine) { + return `${variants.classical} / ${variants.koine}`; + } + + return variants.classical || variants.koine || String(letter?.transliteration || "").trim(); + } + + function greekPlaceValueByIndex(index) { + const value = Number(index); + if (!Number.isFinite(value)) { + return null; + } + + const position = Math.trunc(value); + if (position <= 0) { + return null; + } + + if (position <= 9) { + return position; + } + + if (position <= 18) { + return (position - 9) * 10; + } + + return (position - 18) * 100; + } + + function buildGreekGematriaMap(alphabets, mode = "orderly") { + const map = new Map(); + const greekClassical = Array.isArray(alphabets?.greek) ? alphabets.greek : []; + const greekArchaic = Array.isArray(alphabets?.greekArchaic) ? alphabets.greekArchaic : []; + + [...greekClassical, ...greekArchaic].forEach((entry) => { + const usePlaceValue = mode === "place-value"; + const value = usePlaceValue && !entry?.archaic + ? greekPlaceValueByIndex(entry?.index) + : Number(entry?.numerology); + if (!Number.isFinite(value)) { + return; + } + + [entry?.char, entry?.charLower, entry?.charFinal].forEach((glyph) => { + const key = String(glyph || "").trim(); + if (!key) { + return; + } + map.set(key, Math.trunc(value)); + }); + }); + + if (!map.has("ς") && map.has("σ")) { + map.set("ς", map.get("σ")); + } + + return map; + } + + function buildHebrewGematriaMap(alphabets) { + const map = new Map(); + const hebrewLetters = Array.isArray(alphabets?.hebrew) ? alphabets.hebrew : []; + + hebrewLetters.forEach((entry) => { + const value = Number(entry?.numerology); + const glyph = String(entry?.char || "").trim(); + if (!glyph || !Number.isFinite(value)) { + return; + } + map.set(glyph, Math.trunc(value)); + }); + + const finals = { + ך: "כ", + ם: "מ", + ן: "נ", + ף: "פ", + ץ: "צ" + }; + + Object.entries(finals).forEach(([finalForm, baseForm]) => { + if (!map.has(finalForm) && map.has(baseForm)) { + map.set(finalForm, map.get(baseForm)); + } + }); + + return map; + } + + function computeNameGematria(name, valueMap, options = {}) { + if (!(valueMap instanceof Map) || valueMap.size === 0) { + return null; + } + + const normalizeGreek = options?.normalizeGreek === true; + const normalized = normalizeGreek + ? String(name || "").normalize("NFD").replace(/[\u0300-\u036f]/g, "") + : String(name || ""); + + let sum = 0; + let matched = 0; + for (const glyph of normalized) { + const key = String(glyph || "").trim(); + if (!key) { + continue; + } + const value = valueMap.get(key); + if (!Number.isFinite(value)) { + continue; + } + sum += value; + matched += 1; + } + + if (!matched) { + return null; + } + + return sum; + } + + function computeNameGematriaWithBreakdown(name, valueMap, options = {}) { + if (!(valueMap instanceof Map) || valueMap.size === 0) { + return null; + } + + const normalizeGreek = options?.normalizeGreek === true; + const normalized = normalizeGreek + ? String(name || "").normalize("NFD").replace(/[\u0300-\u036f]/g, "") + : String(name || ""); + + let total = 0; + const parts = []; + + for (const glyph of normalized) { + const key = String(glyph || "").trim(); + if (!key) { + continue; + } + + const value = valueMap.get(key); + if (!Number.isFinite(value)) { + continue; + } + + const numericValue = Math.trunc(value); + total += numericValue; + parts.push(`${key}(${numericValue})`); + } + + if (!parts.length) { + return null; + } + + return { + total, + breakdown: `${parts.join(" + ")} = ${total}` + }; + } + + function hebrewNativeNameByLetter(letter) { + const key = String(letter?.hebrewLetterId || "").trim().toLowerCase(); + return HEBREW_NATIVE_NAMES[key] || ""; + } + + function findGreekEquivalentForHebrew(letter, greekLetters) { + const greekEquivalentKey = String(letter?.greekEquivalent || "").trim().toLowerCase(); + if (greekEquivalentKey) { + return greekLetters.find((entry) => String(entry?.name || "").trim().toLowerCase() === greekEquivalentKey) || null; + } + + const hebrewId = String(letter?.hebrewLetterId || "").trim().toLowerCase(); + if (!hebrewId) { + return null; + } + + return greekLetters.find((entry) => String(entry?.hebrewLetterId || "").trim().toLowerCase() === hebrewId) || null; + } + function extractEnglishLetterRefs(value) { if (Array.isArray(value)) { return [...new Set(value.map((entry) => normalizeLatinLetter(entry)).filter(Boolean))]; @@ -230,7 +553,9 @@ function renderAlphabetEquivalentCard(activeAlphabet, letter, context) { const hebrewLetters = Array.isArray(context.alphabets?.hebrew) ? context.alphabets.hebrew : []; - const greekLetters = Array.isArray(context.alphabets?.greek) ? context.alphabets.greek : []; + const greekClassicalLetters = Array.isArray(context.alphabets?.greek) ? context.alphabets.greek : []; + const greekArchaicLetters = Array.isArray(context.alphabets?.greekArchaic) ? context.alphabets.greekArchaic : []; + const greekLetters = [...greekClassicalLetters, ...greekArchaicLetters]; const englishLetters = Array.isArray(context.alphabets?.english) ? context.alphabets.english : []; const arabicLetters = Array.isArray(context.alphabets?.arabic) ? context.alphabets.arabic : []; const enochianLetters = Array.isArray(context.alphabets?.enochian) ? context.alphabets.enochian : []; @@ -259,7 +584,7 @@ if (activeAlphabet === "hebrew") { addHebrewId(letter?.hebrewLetterId); - } else if (activeAlphabet === "greek") { + } else if (activeAlphabet === "greek" || activeAlphabet === "greekArchaic") { addHebrewId(letter?.hebrewLetterId); englishLetters .filter((entry) => context.normalizeId(entry?.greekEquivalent) === context.normalizeId(letter?.name)) @@ -309,13 +634,14 @@ if (!(viaHebrew || viaEnglish)) { return; } - if (activeAlphabet === "greek" && key === activeGreekKey) { + const greekAlphabet = grk?.archaic ? "greekArchaic" : "greek"; + if ((activeAlphabet === "greek" || activeAlphabet === "greekArchaic") && key === activeGreekKey) { return; } - buttons.push(` + buttons.push(` ${grk.char} - Greek: ${grk.displayName} (${grk.transliteration}) · isopsephy ${grk.numerology} + Greek${grk?.archaic ? " (Archaic)" : ""}: ${grk.displayName} (${formatGreekTransliterationByLetter(grk)}) · isopsephy ${grk.numerology} `); }); @@ -378,6 +704,14 @@ function renderHebrewDetail(context) { const { letter, detailSubEl, detailBodyEl } = context; + const greekClassicalLetters = Array.isArray(context.alphabets?.greek) ? context.alphabets.greek : []; + const greekArchaicLetters = Array.isArray(context.alphabets?.greekArchaic) ? context.alphabets.greekArchaic : []; + const greekLetters = [...greekClassicalLetters, ...greekArchaicLetters]; + const greekEquivalent = findGreekEquivalentForHebrew(letter, greekLetters); + const greekEquivalentNativeName = formatGreekNativeNamesByLetter(greekEquivalent); + const hebrewNativeName = hebrewNativeNameByLetter(letter); + const hebrewNameGematria = computeNameGematriaWithBreakdown(hebrewNativeName, buildHebrewGematriaMap(context.alphabets)); + detailSubEl.textContent = `${letter.name} — ${letter.transliteration}`; detailBodyEl.innerHTML = ""; @@ -385,8 +719,12 @@ sections.push(context.card("Letter Details", ` Character${letter.char} - Name${letter.name} + Name (English)${letter.name} + Name (Hebrew)${hebrewNativeName || "—"} + Name Gematria (Hebrew)${Number.isFinite(hebrewNameGematria?.total) ? hebrewNameGematria.total : "—"} + Name Gematria Breakdown (Hebrew)${hebrewNameGematria?.breakdown || "—"} Transliteration${letter.transliteration} + Greek Equivalent${greekEquivalent ? `${greekEquivalent.char} ${greekEquivalent.displayName}${greekEquivalentNativeName ? ` (${greekEquivalentNativeName})` : ""}` : "—"} Meaning${letter.meaning} Gematria Value${letter.numerology} Letter Type${letter.letterType} @@ -450,10 +788,26 @@ context.attachDetailListeners(); } - function renderGreekDetail(context) { + function renderGreekDetail(context, alphabetKey = "greek") { const { letter, detailSubEl, detailBodyEl } = context; const archaicBadge = letter.archaic ? ' archaic' : ""; - detailSubEl.textContent = `${letter.displayName}${letter.archaic ? " (archaic)" : ""} — ${letter.transliteration}`; + const setLabel = alphabetKey === "greekArchaic" ? "Archaic" : "Classical/Koine"; + const greekNativeNames = greekNativeNamesByLetter(letter); + const greekNativeNameCombined = formatGreekNativeNamesByLetter(letter); + const greekNativeUppercase = formatGreekNativeUppercaseByLetter(letter); + const greekClassicalUppercase = toGreekUppercase(greekNativeNames.classical); + const greekKoineUppercase = toGreekUppercase(greekNativeNames.koine); + const greekTranslit = greekTransliterationVariantsByLetter(letter); + const greekTranslitCombined = formatGreekTransliterationByLetter(letter); + const greekOrderlyMap = buildGreekGematriaMap(context.alphabets, "orderly"); + const greekPlaceValueMap = buildGreekGematriaMap(context.alphabets, "place-value"); + const greekClassicalNameGematriaOrderly = computeNameGematriaWithBreakdown(greekNativeNames.classical, greekOrderlyMap, { normalizeGreek: true }); + const greekClassicalNameGematriaPlace = computeNameGematriaWithBreakdown(greekNativeNames.classical, greekPlaceValueMap, { normalizeGreek: true }); + const greekKoineNameGematriaOrderly = computeNameGematriaWithBreakdown(greekNativeNames.koine, greekOrderlyMap, { normalizeGreek: true }); + const greekKoineNameGematriaPlace = computeNameGematriaWithBreakdown(greekNativeNames.koine, greekPlaceValueMap, { normalizeGreek: true }); + const orderlyLetterValue = Number.isFinite(Number(letter?.numerology)) ? Math.trunc(Number(letter.numerology)) : null; + const placeLetterValue = letter?.archaic ? orderlyLetterValue : greekPlaceValueByIndex(letter?.index); + detailSubEl.textContent = `${letter.displayName}${letter.archaic ? " (archaic)" : ""} — ${greekTranslitCombined} · ${setLabel}`; detailBodyEl.innerHTML = ""; const sections = []; @@ -465,20 +819,33 @@ Uppercase${letter.char} Lowercase${letter.charLower || "—"} ${charRow} - Name${letter.displayName}${archaicBadge} - Transliteration${letter.transliteration} + Name (English)${letter.displayName}${archaicBadge} + Name (Greek)${greekNativeNameCombined || "—"} + Name (Greek Uppercase)${greekNativeUppercase || "—"} + Name (Greek Classical)${greekNativeNames.classical || "—"} + Name (Greek Classical Uppercase)${greekClassicalUppercase || "—"} + Name (Greek Koine)${greekNativeNames.koine || "—"} + Name (Greek Koine Uppercase)${greekKoineUppercase || "—"} + Transliteration${greekTranslitCombined || "—"} + Transliteration (Classical)${greekTranslit.classical || "—"} + Transliteration (Koine)${greekTranslit.koine || "—"} IPA${letter.ipa || "—"} - Isopsephy Value${letter.numerology} + Isopsephy Value (Orderly)${Number.isFinite(orderlyLetterValue) ? orderlyLetterValue : "—"} + Isopsephy Value (1-10-100)${Number.isFinite(placeLetterValue) ? placeLetterValue : "—"} + Name Gematria (Greek Classical)orderly ${Number.isFinite(greekClassicalNameGematriaOrderly?.total) ? greekClassicalNameGematriaOrderly.total : "—"} · 1-10-100 ${Number.isFinite(greekClassicalNameGematriaPlace?.total) ? greekClassicalNameGematriaPlace.total : "—"} + Name Breakdown (Greek Classical)orderly: ${greekClassicalNameGematriaOrderly?.breakdown || "—"} · 1-10-100: ${greekClassicalNameGematriaPlace?.breakdown || "—"} + Name Gematria (Greek Koine)orderly ${Number.isFinite(greekKoineNameGematriaOrderly?.total) ? greekKoineNameGematriaOrderly.total : "—"} · 1-10-100 ${Number.isFinite(greekKoineNameGematriaPlace?.total) ? greekKoineNameGematriaPlace.total : "—"} + Name Breakdown (Greek Koine)orderly: ${greekKoineNameGematriaOrderly?.breakdown || "—"} · 1-10-100: ${greekKoineNameGematriaPlace?.breakdown || "—"} Meaning / Origin${letter.meaning || "—"} `)); - const positionRootCard = renderPositionDigitalRootCard(letter, "greek", context); + const positionRootCard = renderPositionDigitalRootCard(letter, alphabetKey, context); if (positionRootCard) { sections.push(positionRootCard); } - const equivalentsCard = renderAlphabetEquivalentCard("greek", letter, context); + const equivalentsCard = renderAlphabetEquivalentCard(alphabetKey, letter, context); if (equivalentsCard) { sections.push(equivalentsCard); } @@ -617,8 +984,8 @@ const alphabet = context.alphabet; if (alphabet === "hebrew") { renderHebrewDetail(context); - } else if (alphabet === "greek") { - renderGreekDetail(context); + } else if (alphabet === "greek" || alphabet === "greekArchaic") { + renderGreekDetail(context, alphabet); } else if (alphabet === "english") { renderEnglishDetail(context); } else if (alphabet === "arabic") { diff --git a/app/ui-alphabet-gematria.js b/app/ui-alphabet-gematria.js index 234eae2..4323771 100644 --- a/app/ui-alphabet-gematria.js +++ b/app/ui-alphabet-gematria.js @@ -154,7 +154,9 @@ const map = new Map(); const alphabets = getAlphabets() || {}; const hebrewLetters = Array.isArray(alphabets.hebrew) ? alphabets.hebrew : []; - const greekLetters = Array.isArray(alphabets.greek) ? alphabets.greek : []; + const greekClassicalLetters = Array.isArray(alphabets.greek) ? alphabets.greek : []; + const greekArchaicLetters = Array.isArray(alphabets.greekArchaic) ? alphabets.greekArchaic : []; + const greekLetters = [...greekClassicalLetters, ...greekArchaicLetters]; hebrewLetters.forEach((entry) => { const mapped = transliterationToBaseLetters(entry?.transliteration, baseAlphabet); diff --git a/app/ui-alphabet.js b/app/ui-alphabet.js index 2cba150..10f3d53 100644 --- a/app/ui-alphabet.js +++ b/app/ui-alphabet.js @@ -49,7 +49,7 @@ // ── Element cache ──────────────────────────────────────────────────── let listEl, countEl, detailNameEl, detailSubEl, detailBodyEl; - let tabAll, tabHebrew, tabGreek, tabEnglish, tabArabic, tabEnochian; + let tabAll, tabHebrew, tabGreek, tabGreekArchaic, tabEnglish, tabArabic, tabEnochian; let searchInputEl, searchClearEl, typeFilterEl; let gematriaCipherEl, gematriaInputEl, gematriaResultEl, gematriaBreakdownEl; let gematriaModeEls, gematriaMatchesEl, gematriaInputLabelEl, gematriaCipherLabelEl; @@ -64,6 +64,7 @@ tabAll = document.getElementById("alpha-tab-all"); tabHebrew = document.getElementById("alpha-tab-hebrew"); tabGreek = document.getElementById("alpha-tab-greek"); + tabGreekArchaic = document.getElementById("alpha-tab-greek-archaic"); tabEnglish = document.getElementById("alpha-tab-english"); tabArabic = document.getElementById("alpha-tab-arabic"); tabEnochian = document.getElementById("alpha-tab-enochian"); @@ -356,6 +357,7 @@ tabAll, tabHebrew, tabGreek, + tabGreekArchaic, tabEnglish, tabArabic, tabEnochian, @@ -466,7 +468,7 @@ } // Attach tab listeners - [tabAll, tabHebrew, tabGreek, tabEnglish, tabArabic, tabEnochian].forEach((btn) => { + [tabAll, tabHebrew, tabGreek, tabGreekArchaic, tabEnglish, tabArabic, tabEnochian].forEach((btn) => { if (!btn) return; btn.addEventListener("click", () => { switchAlphabet(btn.dataset.alpha, null); @@ -482,6 +484,20 @@ } function selectGreekLetterByName(name) { + const greekLetters = Array.isArray(state.alphabets?.greek) ? state.alphabets.greek : []; + const archaicLetters = Array.isArray(state.alphabets?.greekArchaic) ? state.alphabets.greekArchaic : []; + const targetKey = String(name || "").trim().toLowerCase(); + + if (archaicLetters.some((entry) => String(entry?.name || "").trim().toLowerCase() === targetKey)) { + switchAlphabet("greekArchaic", name); + return; + } + + if (greekLetters.some((entry) => String(entry?.name || "").trim().toLowerCase() === targetKey)) { + switchAlphabet("greek", name); + return; + } + switchAlphabet("greek", name); } diff --git a/app/ui-section-state.js b/app/ui-section-state.js index 91cad42..6eac40e 100644 --- a/app/ui-section-state.js +++ b/app/ui-section-state.js @@ -83,12 +83,20 @@ } function setActiveSection(nextSection) { + const previousSection = activeSection; const requestedSection = VALID_SECTIONS.has(nextSection) ? nextSection : "home"; const normalized = config.isSectionAccessible?.(requestedSection) === false ? "home" : requestedSection; activeSection = normalized; + document.dispatchEvent(new CustomEvent("section:changed", { + detail: { + previousSection, + activeSection + } + })); + const elements = config.elements || {}; const ensure = config.ensure || {}; const referenceData = getReferenceData(); diff --git a/index.html b/index.html index da672c8..182f8d3 100644 --- a/index.html +++ b/index.html @@ -1012,7 +1012,8 @@ All Hebrew - Greek + Greek (Classical/Koine) + Greek (Archaic) English Arabic Enochian @@ -1312,14 +1313,14 @@ - + - + @@ -1355,9 +1356,9 @@ - + - + @@ -1383,9 +1384,9 @@ - - - + + +