2082 lines
71 KiB
JavaScript
2082 lines
71 KiB
JavaScript
/* ui-alphabet.js — Multi-alphabet browser (English / Hebrew / Greek / Arabic / Enochian) */
|
||
(function () {
|
||
"use strict";
|
||
|
||
const state = {
|
||
initialized: false,
|
||
alphabets: null,
|
||
activeAlphabet: "all",
|
||
selectedKey: null,
|
||
filters: {
|
||
query: "",
|
||
letterType: ""
|
||
},
|
||
fourWorldLayers: [],
|
||
monthRefsByHebrewId: new Map(),
|
||
cubeRefs: {
|
||
hebrewPlacementById: new Map(),
|
||
signPlacementById: new Map(),
|
||
planetPlacementById: new Map(),
|
||
pathPlacementByNo: new Map()
|
||
},
|
||
gematria: {
|
||
loadingPromise: null,
|
||
db: null,
|
||
listenersBound: false,
|
||
activeCipherId: "",
|
||
inputText: "",
|
||
scriptCharMap: new Map()
|
||
}
|
||
};
|
||
|
||
// ── 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));
|
||
}
|
||
|
||
// ── Element cache ────────────────────────────────────────────────────
|
||
let listEl, countEl, detailNameEl, detailSubEl, detailBodyEl;
|
||
let tabAll, tabHebrew, tabGreek, tabEnglish, tabArabic, tabEnochian;
|
||
let searchInputEl, searchClearEl, typeFilterEl;
|
||
let gematriaCipherEl, gematriaInputEl, gematriaResultEl, gematriaBreakdownEl;
|
||
|
||
function getElements() {
|
||
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");
|
||
tabAll = document.getElementById("alpha-tab-all");
|
||
tabHebrew = document.getElementById("alpha-tab-hebrew");
|
||
tabGreek = document.getElementById("alpha-tab-greek");
|
||
tabEnglish = document.getElementById("alpha-tab-english");
|
||
tabArabic = document.getElementById("alpha-tab-arabic");
|
||
tabEnochian = document.getElementById("alpha-tab-enochian");
|
||
searchInputEl = document.getElementById("alpha-search-input");
|
||
searchClearEl = document.getElementById("alpha-search-clear");
|
||
typeFilterEl = document.getElementById("alpha-type-filter");
|
||
gematriaCipherEl = document.getElementById("alpha-gematria-cipher");
|
||
gematriaInputEl = document.getElementById("alpha-gematria-input");
|
||
gematriaResultEl = document.getElementById("alpha-gematria-result");
|
||
gematriaBreakdownEl = document.getElementById("alpha-gematria-breakdown");
|
||
}
|
||
|
||
function getFallbackGematriaDb() {
|
||
return {
|
||
baseAlphabet: "abcdefghijklmnopqrstuvwxyz",
|
||
ciphers: [
|
||
{
|
||
id: "simple-ordinal",
|
||
name: "Simple Ordinal",
|
||
description: "A=1 ... Z=26",
|
||
values: Array.from({ length: 26 }, (_, index) => index + 1)
|
||
}
|
||
]
|
||
};
|
||
}
|
||
|
||
function normalizeGematriaText(value) {
|
||
return String(value || "")
|
||
.normalize("NFD")
|
||
.replace(/[\u0300-\u036f]/g, "")
|
||
.toLowerCase();
|
||
}
|
||
|
||
function transliterationToBaseLetters(transliteration, baseAlphabet) {
|
||
const normalized = normalizeGematriaText(transliteration);
|
||
if (!normalized) {
|
||
return "";
|
||
}
|
||
|
||
const primaryVariant = normalized.split(/[\/,;|]/)[0] || normalized;
|
||
const primaryLetters = [...primaryVariant].filter((char) => baseAlphabet.includes(char));
|
||
if (primaryLetters.length) {
|
||
return primaryLetters[0];
|
||
}
|
||
|
||
const allLetters = [...normalized].filter((char) => baseAlphabet.includes(char));
|
||
return allLetters[0] || "";
|
||
}
|
||
|
||
function addScriptCharMapEntry(map, scriptChar, mappedLetters) {
|
||
const key = String(scriptChar || "").trim();
|
||
const value = String(mappedLetters || "").trim();
|
||
if (!key || !value) {
|
||
return;
|
||
}
|
||
map.set(key, value);
|
||
}
|
||
|
||
function buildGematriaScriptMap(baseAlphabet) {
|
||
const map = new Map();
|
||
const hebrewLetters = Array.isArray(state.alphabets?.hebrew) ? state.alphabets.hebrew : [];
|
||
const greekLetters = Array.isArray(state.alphabets?.greek) ? state.alphabets.greek : [];
|
||
|
||
hebrewLetters.forEach((entry) => {
|
||
const mapped = transliterationToBaseLetters(entry?.transliteration, baseAlphabet);
|
||
addScriptCharMapEntry(map, entry?.char, mapped);
|
||
});
|
||
|
||
greekLetters.forEach((entry) => {
|
||
const mapped = transliterationToBaseLetters(entry?.transliteration, baseAlphabet);
|
||
addScriptCharMapEntry(map, entry?.char, mapped);
|
||
addScriptCharMapEntry(map, entry?.charLower, mapped);
|
||
addScriptCharMapEntry(map, entry?.charFinal, mapped);
|
||
});
|
||
|
||
const hebrewFinalForms = {
|
||
ך: "k",
|
||
ם: "m",
|
||
ן: "n",
|
||
ף: "p",
|
||
ץ: "t"
|
||
};
|
||
|
||
Object.entries(hebrewFinalForms).forEach(([char, mapped]) => {
|
||
if (!map.has(char) && baseAlphabet.includes(mapped)) {
|
||
addScriptCharMapEntry(map, char, mapped);
|
||
}
|
||
});
|
||
|
||
if (!map.has("ς") && baseAlphabet.includes("s")) {
|
||
addScriptCharMapEntry(map, "ς", "s");
|
||
}
|
||
|
||
return map;
|
||
}
|
||
|
||
function refreshGematriaScriptMap(baseAlphabet) {
|
||
state.gematria.scriptCharMap = buildGematriaScriptMap(baseAlphabet);
|
||
}
|
||
|
||
function sanitizeGematriaDb(db) {
|
||
const baseAlphabet = String(db?.baseAlphabet || "abcdefghijklmnopqrstuvwxyz").toLowerCase();
|
||
const ciphers = Array.isArray(db?.ciphers)
|
||
? db.ciphers
|
||
.map((cipher) => {
|
||
const id = String(cipher?.id || "").trim();
|
||
const name = String(cipher?.name || "").trim();
|
||
const values = Array.isArray(cipher?.values)
|
||
? cipher.values.map((value) => Number(value))
|
||
: [];
|
||
|
||
if (!id || !name || values.length !== baseAlphabet.length || values.some((value) => !Number.isFinite(value))) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
id,
|
||
name,
|
||
description: String(cipher?.description || "").trim(),
|
||
values
|
||
};
|
||
})
|
||
.filter(Boolean)
|
||
: [];
|
||
|
||
if (!ciphers.length) {
|
||
return getFallbackGematriaDb();
|
||
}
|
||
|
||
return {
|
||
baseAlphabet,
|
||
ciphers
|
||
};
|
||
}
|
||
|
||
async function loadGematriaDb() {
|
||
if (state.gematria.db) {
|
||
return state.gematria.db;
|
||
}
|
||
|
||
if (state.gematria.loadingPromise) {
|
||
return state.gematria.loadingPromise;
|
||
}
|
||
|
||
state.gematria.loadingPromise = fetch("data/gematria-ciphers.json")
|
||
.then((response) => {
|
||
if (!response.ok) {
|
||
throw new Error(`Failed to load gematria ciphers (${response.status})`);
|
||
}
|
||
return response.json();
|
||
})
|
||
.then((db) => {
|
||
state.gematria.db = sanitizeGematriaDb(db);
|
||
return state.gematria.db;
|
||
})
|
||
.catch(() => {
|
||
state.gematria.db = getFallbackGematriaDb();
|
||
return state.gematria.db;
|
||
})
|
||
.finally(() => {
|
||
state.gematria.loadingPromise = null;
|
||
});
|
||
|
||
return state.gematria.loadingPromise;
|
||
}
|
||
|
||
function getActiveGematriaCipher() {
|
||
const db = state.gematria.db || getFallbackGematriaDb();
|
||
const ciphers = Array.isArray(db.ciphers) ? db.ciphers : [];
|
||
if (!ciphers.length) {
|
||
return null;
|
||
}
|
||
|
||
const selectedId = state.gematria.activeCipherId || ciphers[0].id;
|
||
return ciphers.find((cipher) => cipher.id === selectedId) || ciphers[0];
|
||
}
|
||
|
||
function renderGematriaCipherOptions() {
|
||
if (!gematriaCipherEl) {
|
||
return;
|
||
}
|
||
|
||
const db = state.gematria.db || getFallbackGematriaDb();
|
||
const ciphers = Array.isArray(db.ciphers) ? db.ciphers : [];
|
||
|
||
gematriaCipherEl.innerHTML = "";
|
||
ciphers.forEach((cipher) => {
|
||
const option = document.createElement("option");
|
||
option.value = cipher.id;
|
||
option.textContent = cipher.name;
|
||
if (cipher.description) {
|
||
option.title = cipher.description;
|
||
}
|
||
gematriaCipherEl.appendChild(option);
|
||
});
|
||
|
||
const activeCipher = getActiveGematriaCipher();
|
||
state.gematria.activeCipherId = activeCipher?.id || "";
|
||
gematriaCipherEl.value = state.gematria.activeCipherId;
|
||
}
|
||
|
||
function computeGematria(text, cipher, baseAlphabet) {
|
||
const normalizedInput = normalizeGematriaText(text);
|
||
const scriptMap = state.gematria.scriptCharMap instanceof Map
|
||
? state.gematria.scriptCharMap
|
||
: new Map();
|
||
|
||
const letterParts = [];
|
||
let total = 0;
|
||
let count = 0;
|
||
|
||
[...normalizedInput].forEach((char) => {
|
||
const mappedLetters = baseAlphabet.includes(char)
|
||
? char
|
||
: (scriptMap.get(char) || "");
|
||
|
||
if (!mappedLetters) {
|
||
return;
|
||
}
|
||
|
||
[...mappedLetters].forEach((mappedChar) => {
|
||
const index = baseAlphabet.indexOf(mappedChar);
|
||
if (index < 0) {
|
||
return;
|
||
}
|
||
|
||
const value = Number(cipher.values[index]);
|
||
if (!Number.isFinite(value)) {
|
||
return;
|
||
}
|
||
|
||
count += 1;
|
||
total += value;
|
||
letterParts.push(`${mappedChar.toUpperCase()}(${value})`);
|
||
});
|
||
});
|
||
|
||
return {
|
||
total,
|
||
count,
|
||
breakdown: letterParts.join(" + ")
|
||
};
|
||
}
|
||
|
||
function renderGematriaResult() {
|
||
if (!gematriaResultEl || !gematriaBreakdownEl) {
|
||
return;
|
||
}
|
||
|
||
const db = state.gematria.db || getFallbackGematriaDb();
|
||
if (!(state.gematria.scriptCharMap instanceof Map) || !state.gematria.scriptCharMap.size) {
|
||
refreshGematriaScriptMap(db.baseAlphabet);
|
||
}
|
||
const cipher = getActiveGematriaCipher();
|
||
if (!cipher) {
|
||
gematriaResultEl.textContent = "Total: --";
|
||
gematriaBreakdownEl.textContent = "No ciphers available.";
|
||
return;
|
||
}
|
||
|
||
const { total, count, breakdown } = computeGematria(state.gematria.inputText, cipher, db.baseAlphabet);
|
||
|
||
gematriaResultEl.textContent = `Total: ${total}`;
|
||
if (!count) {
|
||
gematriaBreakdownEl.textContent = `Using ${cipher.name}. Enter English, Greek, or Hebrew letters to calculate.`;
|
||
return;
|
||
}
|
||
|
||
gematriaBreakdownEl.textContent = `${cipher.name} · ${count} letters · ${breakdown} = ${total}`;
|
||
}
|
||
|
||
function bindGematriaListeners() {
|
||
if (state.gematria.listenersBound || !gematriaCipherEl || !gematriaInputEl) {
|
||
return;
|
||
}
|
||
|
||
gematriaCipherEl.addEventListener("change", () => {
|
||
state.gematria.activeCipherId = String(gematriaCipherEl.value || "").trim();
|
||
renderGematriaResult();
|
||
});
|
||
|
||
gematriaInputEl.addEventListener("input", () => {
|
||
state.gematria.inputText = gematriaInputEl.value || "";
|
||
renderGematriaResult();
|
||
});
|
||
|
||
state.gematria.listenersBound = true;
|
||
}
|
||
|
||
function ensureGematriaCalculator() {
|
||
getElements();
|
||
if (!gematriaCipherEl || !gematriaInputEl || !gematriaResultEl || !gematriaBreakdownEl) {
|
||
return;
|
||
}
|
||
|
||
bindGematriaListeners();
|
||
|
||
if (gematriaInputEl.value !== state.gematria.inputText) {
|
||
gematriaInputEl.value = state.gematria.inputText;
|
||
}
|
||
|
||
void loadGematriaDb().then(() => {
|
||
refreshGematriaScriptMap((state.gematria.db || getFallbackGematriaDb()).baseAlphabet);
|
||
renderGematriaCipherOptions();
|
||
renderGematriaResult();
|
||
});
|
||
}
|
||
|
||
// ── 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] || [];
|
||
}
|
||
|
||
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) {
|
||
// Stable unique key per alphabet + entry
|
||
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() {
|
||
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();
|
||
if (detailSubEl) {
|
||
detailSubEl.textContent = "No letters match the current filter.";
|
||
}
|
||
}
|
||
|
||
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();
|
||
});
|
||
}
|
||
}
|
||
|
||
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 `<span class="${className}">${key}</span>`;
|
||
}
|
||
return `<img class="${className}" src="${src}" alt="Enochian ${key}" loading="lazy" decoding="async">`;
|
||
}
|
||
|
||
// ── 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 = `<strong>${alphaLabel}${displayLabel(letter)}</strong><br><span class="alpha-list-sub">${displaySub(letter)}</span>`;
|
||
|
||
item.appendChild(glyph);
|
||
item.appendChild(meta);
|
||
item.addEventListener("click", () => selectLetter(key));
|
||
listEl.appendChild(item);
|
||
});
|
||
}
|
||
|
||
// ── Detail rendering ──────────────────────────────────────────────────
|
||
function renderDetail(letter) {
|
||
if (!detailNameEl) return;
|
||
|
||
const alphabet = alphabetForLetter(letter);
|
||
|
||
detailNameEl.replaceChildren();
|
||
if (alphabet === "enochian") {
|
||
const image = document.createElement("img");
|
||
image.className = "alpha-enochian-glyph-img alpha-enochian-glyph-img--detail";
|
||
image.src = enochianGlyphUrl(letter);
|
||
image.alt = `Enochian ${enochianGlyphKey(letter) || "?"}`;
|
||
image.loading = "lazy";
|
||
image.decoding = "async";
|
||
image.addEventListener("error", () => {
|
||
detailNameEl.textContent = enochianGlyphKey(letter) || "?";
|
||
});
|
||
detailNameEl.appendChild(image);
|
||
} else {
|
||
detailNameEl.textContent = displayGlyph(letter);
|
||
}
|
||
detailNameEl.classList.add("alpha-detail-glyph");
|
||
detailNameEl.classList.toggle("alpha-detail-glyph--arabic", alphabet === "arabic");
|
||
detailNameEl.classList.toggle("alpha-detail-glyph--enochian", alphabet === "enochian");
|
||
|
||
if (alphabet === "hebrew") renderHebrewDetail(letter);
|
||
else if (alphabet === "greek") renderGreekDetail(letter);
|
||
else if (alphabet === "english") renderEnglishDetail(letter);
|
||
else if (alphabet === "arabic") renderArabicDetail(letter);
|
||
else if (alphabet === "enochian") renderEnochianDetail(letter);
|
||
}
|
||
|
||
function card(title, bodyHTML) {
|
||
return `<div class="planet-meta-card"><strong>${title}</strong><div class="planet-text">${bodyHTML}</div></div>`;
|
||
}
|
||
|
||
const PLANET_SYMBOLS = {
|
||
mercury: "☿︎", luna: "☾︎", venus: "♀︎", sol: "☉︎",
|
||
jupiter: "♃︎", mars: "♂︎", saturn: "♄︎"
|
||
};
|
||
|
||
const ZODIAC_SYMBOLS = {
|
||
aries: "♈︎", taurus: "♉︎", gemini: "♊︎", cancer: "♋︎",
|
||
leo: "♌︎", virgo: "♍︎", libra: "♎︎", scorpio: "♏︎",
|
||
sagittarius: "♐︎", capricorn: "♑︎", aquarius: "♒︎", pisces: "♓︎"
|
||
};
|
||
|
||
const HEBREW_DOUBLE_DUALITY = {
|
||
bet: { left: "Life", right: "Death" },
|
||
gimel: { left: "Peace", right: "War" },
|
||
dalet: { left: "Wisdom", right: "Folly" },
|
||
kaf: { left: "Wealth", right: "Poverty" },
|
||
pe: { left: "Beauty", right: "Ugliness" },
|
||
resh: { left: "Fruitfulness", right: "Sterility" },
|
||
tav: { left: "Dominion", right: "Slavery" }
|
||
};
|
||
|
||
function normalizeId(value) {
|
||
return String(value || "").trim().toLowerCase();
|
||
}
|
||
|
||
function normalizeSoulId(value) {
|
||
return String(value || "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[^a-z]/g, "");
|
||
}
|
||
|
||
function buildFourWorldLayersFromDataset(magickDataset) {
|
||
const worlds = magickDataset?.grouped?.kabbalah?.fourWorlds;
|
||
const souls = magickDataset?.grouped?.kabbalah?.souls;
|
||
const paths = Array.isArray(magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]?.paths)
|
||
? magickDataset.grouped.kabbalah["kabbalah-tree"].paths
|
||
: [];
|
||
|
||
if (!worlds || typeof worlds !== "object") {
|
||
return [];
|
||
}
|
||
|
||
const soulAliases = {
|
||
chiah: "chaya",
|
||
chaya: "chaya",
|
||
neshamah: "neshama",
|
||
neshama: "neshama",
|
||
ruach: "ruach",
|
||
nephesh: "nephesh"
|
||
};
|
||
|
||
const pathByLetterId = new Map();
|
||
paths.forEach((path) => {
|
||
const letterId = normalizeLetterId(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char);
|
||
const pathNo = Number(path?.pathNumber);
|
||
if (!letterId || !Number.isFinite(pathNo) || pathByLetterId.has(letterId)) {
|
||
return;
|
||
}
|
||
pathByLetterId.set(letterId, pathNo);
|
||
});
|
||
|
||
const worldOrder = ["atzilut", "briah", "yetzirah", "assiah"];
|
||
|
||
return worldOrder
|
||
.map((worldId) => {
|
||
const world = worlds?.[worldId];
|
||
if (!world || typeof world !== "object") {
|
||
return null;
|
||
}
|
||
|
||
const tetragrammaton = world?.tetragrammaton && typeof world.tetragrammaton === "object"
|
||
? world.tetragrammaton
|
||
: {};
|
||
|
||
const letterId = normalizeLetterId(tetragrammaton?.hebrewLetterId);
|
||
const rawSoulId = normalizeSoulId(world?.soulId);
|
||
const soulId = soulAliases[rawSoulId] || rawSoulId;
|
||
const soul = souls?.[soulId] && typeof souls[soulId] === "object"
|
||
? souls[soulId]
|
||
: null;
|
||
|
||
const slot = tetragrammaton?.isFinal
|
||
? `${String(tetragrammaton?.slot || "Heh")} (final)`
|
||
: String(tetragrammaton?.slot || "");
|
||
|
||
return {
|
||
slot,
|
||
letterChar: String(tetragrammaton?.letterChar || ""),
|
||
hebrewLetterId: letterId,
|
||
world: String(world?.name?.roman || titleCase(worldId)),
|
||
worldLayer: String(world?.worldLayer?.en || world?.desc?.en || ""),
|
||
worldDescription: String(world?.worldDescription?.en || ""),
|
||
soulLayer: String(soul?.name?.roman || titleCase(rawSoulId || soulId)),
|
||
soulTitle: String(soul?.title?.en || titleCase(soul?.name?.en || "")),
|
||
soulDescription: String(soul?.desc?.en || ""),
|
||
pathNumber: pathByLetterId.get(letterId) || null
|
||
};
|
||
})
|
||
.filter(Boolean);
|
||
}
|
||
|
||
function buildMonthReferencesByHebrew(referenceData, alphabets) {
|
||
const map = new Map();
|
||
const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [];
|
||
const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
||
const monthById = new Map(months.map((month) => [month.id, month]));
|
||
const hebrewLetters = Array.isArray(alphabets?.hebrew) ? alphabets.hebrew : [];
|
||
|
||
const profiles = hebrewLetters
|
||
.filter((letter) => letter?.hebrewLetterId)
|
||
.map((letter) => {
|
||
const astrologyType = normalizeId(letter?.astrology?.type);
|
||
const astrologyName = normalizeId(letter?.astrology?.name);
|
||
return {
|
||
hebrewLetterId: normalizeId(letter.hebrewLetterId),
|
||
tarotTrumpNumber: Number.isFinite(Number(letter?.tarot?.trumpNumber))
|
||
? Number(letter.tarot.trumpNumber)
|
||
: null,
|
||
kabbalahPathNumber: Number.isFinite(Number(letter?.kabbalahPathNumber))
|
||
? Number(letter.kabbalahPathNumber)
|
||
: null,
|
||
planetId: astrologyType === "planet" ? astrologyName : "",
|
||
zodiacSignId: astrologyType === "zodiac" ? astrologyName : ""
|
||
};
|
||
});
|
||
|
||
function parseMonthDayToken(value) {
|
||
const text = String(value || "").trim();
|
||
const match = text.match(/^(\d{1,2})-(\d{1,2})$/);
|
||
if (!match) {
|
||
return null;
|
||
}
|
||
|
||
const monthNo = Number(match[1]);
|
||
const dayNo = Number(match[2]);
|
||
if (!Number.isInteger(monthNo) || !Number.isInteger(dayNo) || monthNo < 1 || monthNo > 12 || dayNo < 1 || dayNo > 31) {
|
||
return null;
|
||
}
|
||
|
||
return { month: monthNo, day: dayNo };
|
||
}
|
||
|
||
function parseMonthDayTokensFromText(value) {
|
||
const text = String(value || "");
|
||
const matches = [...text.matchAll(/(\d{1,2})-(\d{1,2})/g)];
|
||
return matches
|
||
.map((match) => ({ month: Number(match[1]), day: Number(match[2]) }))
|
||
.filter((token) => Number.isInteger(token.month) && Number.isInteger(token.day) && token.month >= 1 && token.month <= 12 && token.day >= 1 && token.day <= 31);
|
||
}
|
||
|
||
function toDateToken(token, year) {
|
||
if (!token) {
|
||
return null;
|
||
}
|
||
return new Date(year, token.month - 1, token.day, 12, 0, 0, 0);
|
||
}
|
||
|
||
function splitMonthDayRangeByMonth(startToken, endToken) {
|
||
const startDate = toDateToken(startToken, 2025);
|
||
const endBase = toDateToken(endToken, 2025);
|
||
if (!startDate || !endBase) {
|
||
return [];
|
||
}
|
||
|
||
const wrapsYear = endBase.getTime() < startDate.getTime();
|
||
const endDate = wrapsYear ? toDateToken(endToken, 2026) : endBase;
|
||
if (!endDate) {
|
||
return [];
|
||
}
|
||
|
||
const segments = [];
|
||
let cursor = new Date(startDate);
|
||
while (cursor.getTime() <= endDate.getTime()) {
|
||
const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0);
|
||
const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate;
|
||
|
||
segments.push({
|
||
monthNo: cursor.getMonth() + 1,
|
||
startDay: cursor.getDate(),
|
||
endDay: segmentEnd.getDate()
|
||
});
|
||
|
||
cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0);
|
||
}
|
||
|
||
return segments;
|
||
}
|
||
|
||
function tokenToString(monthNo, dayNo) {
|
||
return `${String(monthNo).padStart(2, "0")}-${String(dayNo).padStart(2, "0")}`;
|
||
}
|
||
|
||
function formatRangeLabel(monthName, startDay, endDay) {
|
||
if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) {
|
||
return monthName;
|
||
}
|
||
if (startDay === endDay) {
|
||
return `${monthName} ${startDay}`;
|
||
}
|
||
return `${monthName} ${startDay}-${endDay}`;
|
||
}
|
||
|
||
function resolveRangeForMonth(month, options = {}) {
|
||
const monthOrder = Number(month?.order);
|
||
const monthStart = parseMonthDayToken(month?.start);
|
||
const monthEnd = parseMonthDayToken(month?.end);
|
||
if (!Number.isFinite(monthOrder) || !monthStart || !monthEnd) {
|
||
return {
|
||
startToken: String(month?.start || "").trim() || null,
|
||
endToken: String(month?.end || "").trim() || null,
|
||
label: month?.name || month?.id || "",
|
||
isFullMonth: true
|
||
};
|
||
}
|
||
|
||
let startToken = parseMonthDayToken(options.startToken);
|
||
let endToken = parseMonthDayToken(options.endToken);
|
||
|
||
if (!startToken || !endToken) {
|
||
const tokens = parseMonthDayTokensFromText(options.rawDateText);
|
||
if (tokens.length >= 2) {
|
||
startToken = tokens[0];
|
||
endToken = tokens[1];
|
||
} else if (tokens.length === 1) {
|
||
startToken = tokens[0];
|
||
endToken = tokens[0];
|
||
}
|
||
}
|
||
|
||
if (!startToken || !endToken) {
|
||
startToken = monthStart;
|
||
endToken = monthEnd;
|
||
}
|
||
|
||
const segments = splitMonthDayRangeByMonth(startToken, endToken);
|
||
const segment = segments.find((entry) => entry.monthNo === monthOrder) || null;
|
||
|
||
const useStart = segment ? { month: monthOrder, day: segment.startDay } : startToken;
|
||
const useEnd = segment ? { month: monthOrder, day: segment.endDay } : endToken;
|
||
const startText = tokenToString(useStart.month, useStart.day);
|
||
const endText = tokenToString(useEnd.month, useEnd.day);
|
||
const isFullMonth = startText === month.start && endText === month.end;
|
||
|
||
return {
|
||
startToken: startText,
|
||
endToken: endText,
|
||
label: isFullMonth
|
||
? (month.name || month.id)
|
||
: formatRangeLabel(month.name || month.id, useStart.day, useEnd.day),
|
||
isFullMonth
|
||
};
|
||
}
|
||
|
||
function pushRef(hebrewLetterId, month, options = {}) {
|
||
if (!hebrewLetterId || !month?.id) {
|
||
return;
|
||
}
|
||
|
||
if (!map.has(hebrewLetterId)) {
|
||
map.set(hebrewLetterId, []);
|
||
}
|
||
|
||
const rows = map.get(hebrewLetterId);
|
||
const range = resolveRangeForMonth(month, options);
|
||
const rowKey = `${month.id}|${range.startToken || ""}|${range.endToken || ""}`;
|
||
if (rows.some((entry) => entry.key === rowKey)) {
|
||
return;
|
||
}
|
||
|
||
rows.push({
|
||
id: month.id,
|
||
name: month.name || month.id,
|
||
order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999,
|
||
label: range.label,
|
||
startToken: range.startToken,
|
||
endToken: range.endToken,
|
||
isFullMonth: range.isFullMonth,
|
||
key: rowKey
|
||
});
|
||
}
|
||
|
||
function collectRefs(associations, month, options = {}) {
|
||
if (!associations || typeof associations !== "object") {
|
||
return;
|
||
}
|
||
|
||
const assocHebrewId = normalizeId(associations.hebrewLetterId);
|
||
const assocTarotTrump = Number.isFinite(Number(associations.tarotTrumpNumber))
|
||
? Number(associations.tarotTrumpNumber)
|
||
: null;
|
||
const assocPath = Number.isFinite(Number(associations.kabbalahPathNumber))
|
||
? Number(associations.kabbalahPathNumber)
|
||
: null;
|
||
const assocPlanetId = normalizeId(associations.planetId);
|
||
const assocSignId = normalizeId(associations.zodiacSignId);
|
||
|
||
profiles.forEach((profile) => {
|
||
if (!profile.hebrewLetterId) {
|
||
return;
|
||
}
|
||
|
||
const matchesDirect = assocHebrewId && assocHebrewId === profile.hebrewLetterId;
|
||
const matchesTarot = assocTarotTrump != null && profile.tarotTrumpNumber === assocTarotTrump;
|
||
const matchesPath = assocPath != null && profile.kabbalahPathNumber === assocPath;
|
||
const matchesPlanet = profile.planetId && assocPlanetId && profile.planetId === assocPlanetId;
|
||
const matchesZodiac = profile.zodiacSignId && assocSignId && profile.zodiacSignId === assocSignId;
|
||
|
||
if (matchesDirect || matchesTarot || matchesPath || matchesPlanet || matchesZodiac) {
|
||
pushRef(profile.hebrewLetterId, month, options);
|
||
}
|
||
});
|
||
}
|
||
|
||
months.forEach((month) => {
|
||
collectRefs(month?.associations, month);
|
||
|
||
const events = Array.isArray(month?.events) ? month.events : [];
|
||
events.forEach((event) => {
|
||
collectRefs(event?.associations, month, {
|
||
rawDateText: event?.dateRange || event?.date || ""
|
||
});
|
||
});
|
||
});
|
||
|
||
holidays.forEach((holiday) => {
|
||
const month = monthById.get(holiday?.monthId);
|
||
if (!month) {
|
||
return;
|
||
}
|
||
collectRefs(holiday?.associations, month, {
|
||
rawDateText: holiday?.dateRange || holiday?.date || ""
|
||
});
|
||
});
|
||
|
||
map.forEach((rows, key) => {
|
||
const preciseMonthIds = new Set(
|
||
rows
|
||
.filter((entry) => !entry.isFullMonth)
|
||
.map((entry) => entry.id)
|
||
);
|
||
|
||
const filtered = rows.filter((entry) => {
|
||
if (!entry.isFullMonth) {
|
||
return true;
|
||
}
|
||
return !preciseMonthIds.has(entry.id);
|
||
});
|
||
|
||
filtered.sort((left, right) => {
|
||
if (left.order !== right.order) {
|
||
return left.order - right.order;
|
||
}
|
||
|
||
const startLeft = parseMonthDayToken(left.startToken);
|
||
const startRight = parseMonthDayToken(right.startToken);
|
||
const dayLeft = startLeft ? startLeft.day : 999;
|
||
const dayRight = startRight ? startRight.day : 999;
|
||
if (dayLeft !== dayRight) {
|
||
return dayLeft - dayRight;
|
||
}
|
||
|
||
return String(left.label || left.name || "").localeCompare(String(right.label || right.name || ""));
|
||
});
|
||
|
||
map.set(key, filtered);
|
||
});
|
||
|
||
return map;
|
||
}
|
||
|
||
function createEmptyCubeRefs() {
|
||
return {
|
||
hebrewPlacementById: new Map(),
|
||
signPlacementById: new Map(),
|
||
planetPlacementById: new Map(),
|
||
pathPlacementByNo: new Map()
|
||
};
|
||
}
|
||
|
||
function normalizeLetterId(value) {
|
||
const key = normalizeId(value).replace(/[^a-z]/g, "");
|
||
const aliases = {
|
||
aleph: "alef",
|
||
beth: "bet",
|
||
zain: "zayin",
|
||
cheth: "het",
|
||
chet: "het",
|
||
daleth: "dalet",
|
||
teth: "tet",
|
||
peh: "pe",
|
||
tzaddi: "tsadi",
|
||
tzadi: "tsadi",
|
||
tzade: "tsadi",
|
||
tsaddi: "tsadi",
|
||
qoph: "qof",
|
||
taw: "tav",
|
||
tau: "tav"
|
||
};
|
||
return aliases[key] || key;
|
||
}
|
||
|
||
function edgeWalls(edge) {
|
||
const explicitWalls = Array.isArray(edge?.walls)
|
||
? edge.walls.map((wallId) => normalizeId(wallId)).filter(Boolean)
|
||
: [];
|
||
|
||
if (explicitWalls.length >= 2) {
|
||
return explicitWalls.slice(0, 2);
|
||
}
|
||
|
||
return normalizeId(edge?.id)
|
||
.split("-")
|
||
.map((wallId) => normalizeId(wallId))
|
||
.filter(Boolean)
|
||
.slice(0, 2);
|
||
}
|
||
|
||
function edgeLabel(edge) {
|
||
const explicitName = String(edge?.name || "").trim();
|
||
if (explicitName) {
|
||
return explicitName;
|
||
}
|
||
|
||
return edgeWalls(edge)
|
||
.map((part) => cap(part))
|
||
.join(" ");
|
||
}
|
||
|
||
function resolveCubeDirectionLabel(wallId, edge) {
|
||
const normalizedWallId = normalizeId(wallId);
|
||
const edgeId = normalizeId(edge?.id);
|
||
if (!normalizedWallId || !edgeId) {
|
||
return "";
|
||
}
|
||
|
||
const cubeUi = window.CubeSectionUi;
|
||
if (cubeUi && typeof cubeUi.getEdgeDirectionLabelForWall === "function") {
|
||
const directionLabel = String(cubeUi.getEdgeDirectionLabelForWall(normalizedWallId, edgeId) || "").trim();
|
||
if (directionLabel) {
|
||
return directionLabel;
|
||
}
|
||
}
|
||
|
||
return edgeLabel(edge);
|
||
}
|
||
|
||
function makeCubePlacement(wall, edge = null) {
|
||
const wallId = normalizeId(wall?.id);
|
||
const edgeId = normalizeId(edge?.id);
|
||
return {
|
||
wallId,
|
||
edgeId,
|
||
wallName: wall?.name || cap(wallId),
|
||
edgeName: resolveCubeDirectionLabel(wallId, edge)
|
||
};
|
||
}
|
||
|
||
function setPlacementIfMissing(map, key, placement) {
|
||
if (!key || map.has(key) || !placement?.wallId) {
|
||
return;
|
||
}
|
||
map.set(key, placement);
|
||
}
|
||
|
||
function buildCubeReferences(magickDataset) {
|
||
const refs = createEmptyCubeRefs();
|
||
const cube = magickDataset?.grouped?.kabbalah?.cube || {};
|
||
const walls = Array.isArray(cube?.walls)
|
||
? cube.walls
|
||
: [];
|
||
const edges = Array.isArray(cube?.edges)
|
||
? cube.edges
|
||
: [];
|
||
const paths = Array.isArray(magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]?.paths)
|
||
? magickDataset.grouped.kabbalah["kabbalah-tree"].paths
|
||
: [];
|
||
|
||
const wallById = new Map(
|
||
walls.map((wall) => [normalizeId(wall?.id), wall])
|
||
);
|
||
const firstEdgeByWallId = new Map();
|
||
|
||
const pathByLetterId = new Map(
|
||
paths
|
||
.map((path) => [normalizeLetterId(path?.hebrewLetter?.transliteration), path])
|
||
.filter(([letterId]) => Boolean(letterId))
|
||
);
|
||
|
||
edges.forEach((edge) => {
|
||
edgeWalls(edge).forEach((wallId) => {
|
||
if (!firstEdgeByWallId.has(wallId)) {
|
||
firstEdgeByWallId.set(wallId, edge);
|
||
}
|
||
});
|
||
});
|
||
|
||
walls.forEach((wall) => {
|
||
// each wall has a "face" letter; when we build a cube reference for that
|
||
// letter we want the label to read “Face” rather than arbitrarily using
|
||
// the first edge we encounter on that wall. previously we always
|
||
// computed `placementEdge` from the first edge, which produced labels
|
||
// like “East Wall – North” for the dalet face. instead we create a
|
||
// custom placement object for face letters with an empty edge id and a
|
||
// fixed edgeName of “Face”.
|
||
const wallHebrewLetterId = normalizeLetterId(wall?.hebrewLetterId || wall?.associations?.hebrewLetterId);
|
||
|
||
let wallPlacement;
|
||
if (wallHebrewLetterId) {
|
||
// face letter; label should emphasise the face rather than a direction
|
||
wallPlacement = {
|
||
wallId: normalizeId(wall?.id),
|
||
edgeId: "",
|
||
wallName: wall?.name || cap(normalizeId(wall?.id)),
|
||
edgeName: "Face"
|
||
};
|
||
} else {
|
||
// fall back to normal edge-based placement
|
||
const placementEdge = firstEdgeByWallId.get(normalizeId(wall?.id)) || null;
|
||
wallPlacement = makeCubePlacement(wall, placementEdge);
|
||
}
|
||
|
||
setPlacementIfMissing(refs.hebrewPlacementById, wallHebrewLetterId, wallPlacement);
|
||
|
||
const wallPath = pathByLetterId.get(wallHebrewLetterId) || null;
|
||
const wallSignId = normalizeId(wallPath?.astrology?.type) === "zodiac"
|
||
? normalizeId(wallPath?.astrology?.name)
|
||
: "";
|
||
setPlacementIfMissing(refs.signPlacementById, wallSignId, wallPlacement);
|
||
|
||
const wallPathNo = Number(wallPath?.pathNumber);
|
||
if (Number.isFinite(wallPathNo)) {
|
||
setPlacementIfMissing(refs.pathPlacementByNo, wallPathNo, wallPlacement);
|
||
}
|
||
|
||
const wallPlanet = normalizeId(wall?.associations?.planetId);
|
||
if (wallPlanet) {
|
||
setPlacementIfMissing(refs.planetPlacementById, wallPlanet, wallPlacement);
|
||
}
|
||
});
|
||
|
||
edges.forEach((edge) => {
|
||
const wallsForEdge = edgeWalls(edge);
|
||
const primaryWallId = wallsForEdge[0];
|
||
const primaryWall = wallById.get(primaryWallId) || {
|
||
id: primaryWallId,
|
||
name: cap(primaryWallId)
|
||
};
|
||
|
||
const placement = makeCubePlacement(primaryWall, edge);
|
||
const hebrewLetterId = normalizeLetterId(edge?.hebrewLetterId || edge?.associations?.hebrewLetterId);
|
||
setPlacementIfMissing(refs.hebrewPlacementById, hebrewLetterId, placement);
|
||
|
||
const path = pathByLetterId.get(hebrewLetterId) || null;
|
||
const signId = normalizeId(path?.astrology?.type) === "zodiac"
|
||
? normalizeId(path?.astrology?.name)
|
||
: "";
|
||
setPlacementIfMissing(refs.signPlacementById, signId, placement);
|
||
|
||
const pathNo = Number(path?.pathNumber);
|
||
if (Number.isFinite(pathNo)) {
|
||
setPlacementIfMissing(refs.pathPlacementByNo, pathNo, placement);
|
||
}
|
||
});
|
||
|
||
return refs;
|
||
}
|
||
|
||
function getCubePlacementForHebrewLetter(hebrewLetterId, pathNo = null) {
|
||
const normalizedLetterId = normalizeId(hebrewLetterId);
|
||
if (normalizedLetterId && state.cubeRefs.hebrewPlacementById.has(normalizedLetterId)) {
|
||
return state.cubeRefs.hebrewPlacementById.get(normalizedLetterId);
|
||
}
|
||
|
||
const numericPath = Number(pathNo);
|
||
if (Number.isFinite(numericPath) && state.cubeRefs.pathPlacementByNo.has(numericPath)) {
|
||
return state.cubeRefs.pathPlacementByNo.get(numericPath);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function getCubePlacementForPlanet(planetId) {
|
||
const normalizedPlanetId = normalizeId(planetId);
|
||
return normalizedPlanetId ? state.cubeRefs.planetPlacementById.get(normalizedPlanetId) || null : null;
|
||
}
|
||
|
||
function getCubePlacementForSign(signId) {
|
||
const normalizedSignId = normalizeId(signId);
|
||
return normalizedSignId ? state.cubeRefs.signPlacementById.get(normalizedSignId) || null : null;
|
||
}
|
||
|
||
function cubePlacementLabel(placement) {
|
||
const wallName = placement?.wallName || "Wall";
|
||
const edgeName = placement?.edgeName || "Direction";
|
||
return `Cube: ${wallName} Wall - ${edgeName}`;
|
||
}
|
||
|
||
function cubePlacementBtn(placement, fallbackDetail = null) {
|
||
if (!placement) {
|
||
return "";
|
||
}
|
||
|
||
const detail = {
|
||
"wall-id": placement.wallId,
|
||
"edge-id": placement.edgeId
|
||
};
|
||
|
||
if (fallbackDetail && typeof fallbackDetail === "object") {
|
||
Object.entries(fallbackDetail).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && value !== "") {
|
||
detail[key] = value;
|
||
}
|
||
});
|
||
}
|
||
|
||
return navBtn(cubePlacementLabel(placement), "nav:cube", detail);
|
||
}
|
||
|
||
function cap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ""; }
|
||
|
||
function renderAstrologyCard(astrology) {
|
||
if (!astrology) return "";
|
||
const { type, name } = astrology;
|
||
const id = (name || "").toLowerCase();
|
||
|
||
if (type === "planet") {
|
||
const sym = PLANET_SYMBOLS[id] || "";
|
||
const cubePlacement = getCubePlacementForPlanet(id);
|
||
const cubeBtn = cubePlacementBtn(cubePlacement, { "planet-id": id });
|
||
return card("Astrology", `
|
||
<dl class="alpha-dl">
|
||
<dt>Type</dt><dd>Planet</dd>
|
||
<dt>Ruler</dt><dd>${sym} ${cap(id)}</dd>
|
||
</dl>
|
||
<div class="alpha-nav-btns">
|
||
<button class="alpha-nav-btn" data-event="nav:planet" data-planet-id="${id}">View ${cap(id)} ↗</button>
|
||
${cubeBtn}
|
||
</div>
|
||
`);
|
||
}
|
||
if (type === "zodiac") {
|
||
const sym = ZODIAC_SYMBOLS[id] || "";
|
||
const cubePlacement = getCubePlacementForSign(id);
|
||
const cubeBtn = cubePlacementBtn(cubePlacement, { "sign-id": id });
|
||
return card("Astrology", `
|
||
<dl class="alpha-dl">
|
||
<dt>Type</dt><dd>Zodiac Sign</dd>
|
||
<dt>Sign</dt><dd>${sym} ${cap(id)}</dd>
|
||
</dl>
|
||
<div class="alpha-nav-btns">
|
||
<button class="alpha-nav-btn" data-event="nav:zodiac" data-sign-id="${id}">View ${cap(id)} ↗</button>
|
||
${cubeBtn}
|
||
</div>
|
||
`);
|
||
}
|
||
if (type === "element") {
|
||
const elemEmoji = { air: "💨", water: "💧", fire: "🔥", earth: "🌍" };
|
||
return card("Astrology", `
|
||
<dl class="alpha-dl">
|
||
<dt>Type</dt><dd>Element</dd>
|
||
<dt>Element</dt><dd>${elemEmoji[id] || ""} ${cap(id)}</dd>
|
||
</dl>
|
||
`);
|
||
}
|
||
return card("Astrology", `
|
||
<dl class="alpha-dl">
|
||
<dt>Type</dt><dd>${cap(type)}</dd>
|
||
<dt>Name</dt><dd>${cap(name)}</dd>
|
||
</dl>
|
||
`);
|
||
}
|
||
function navBtn(label, event, detail) {
|
||
const attrs = Object.entries(detail).map(([k, v]) => `data-${k}="${v}"`).join(" ");
|
||
return `<button class="alpha-nav-btn" data-event="${event}" ${attrs}>${label} ↗</button>`;
|
||
}
|
||
|
||
function computeDigitalRoot(value) {
|
||
let current = Math.abs(Math.trunc(Number(value)));
|
||
if (!Number.isFinite(current)) {
|
||
return null;
|
||
}
|
||
|
||
while (current >= 10) {
|
||
current = String(current)
|
||
.split("")
|
||
.reduce((sum, digit) => sum + Number(digit), 0);
|
||
}
|
||
|
||
return current;
|
||
}
|
||
|
||
function describeDigitalRootReduction(value, digitalRoot) {
|
||
const normalized = Math.abs(Math.trunc(Number(value)));
|
||
if (!Number.isFinite(normalized) || !Number.isFinite(digitalRoot)) {
|
||
return "";
|
||
}
|
||
|
||
if (normalized < 10) {
|
||
return String(normalized);
|
||
}
|
||
|
||
return `${String(normalized).split("").join(" + ")} = ${digitalRoot}`;
|
||
}
|
||
|
||
function renderPositionDigitalRootCard(letter, alphabet, orderLabel) {
|
||
const index = Number(letter?.index);
|
||
if (!Number.isFinite(index)) {
|
||
return "";
|
||
}
|
||
|
||
const position = Math.trunc(index);
|
||
if (position <= 0) {
|
||
return "";
|
||
}
|
||
|
||
const digitalRoot = computeDigitalRoot(position);
|
||
if (!Number.isFinite(digitalRoot)) {
|
||
return "";
|
||
}
|
||
|
||
const entries = Array.isArray(state.alphabets?.[alphabet]) ? state.alphabets[alphabet] : [];
|
||
const countText = entries.length ? ` of ${entries.length}` : "";
|
||
const orderText = orderLabel ? ` (${orderLabel})` : "";
|
||
const reductionText = describeDigitalRootReduction(position, digitalRoot);
|
||
const openNumberBtn = navBtn(`View Number ${digitalRoot}`, "nav:number", { value: digitalRoot });
|
||
|
||
return card("Position Digital Root", `
|
||
<dl class="alpha-dl">
|
||
<dt>Position</dt><dd>#${position}${countText}${orderText}</dd>
|
||
<dt>Digital Root</dt><dd>${digitalRoot}${reductionText ? ` (${reductionText})` : ""}</dd>
|
||
</dl>
|
||
<div class="alpha-nav-btns">${openNumberBtn}</div>
|
||
`);
|
||
}
|
||
|
||
function monthRefsForLetter(letter) {
|
||
const hebrewLetterId = normalizeId(letter?.hebrewLetterId);
|
||
if (!hebrewLetterId) {
|
||
return [];
|
||
}
|
||
return state.monthRefsByHebrewId.get(hebrewLetterId) || [];
|
||
}
|
||
|
||
function calendarMonthsCard(monthRefs, titleLabel) {
|
||
if (!monthRefs.length) {
|
||
return "";
|
||
}
|
||
|
||
const monthButtons = monthRefs
|
||
.map((month) => navBtn(month.label || month.name, "nav:calendar-month", { "month-id": month.id }))
|
||
.join("");
|
||
|
||
return card("Calendar Months", `
|
||
<div>${titleLabel}</div>
|
||
<div class="alpha-nav-btns">${monthButtons}</div>
|
||
`);
|
||
}
|
||
|
||
function renderHebrewDualityCard(letter) {
|
||
const duality = HEBREW_DOUBLE_DUALITY[normalizeId(letter?.hebrewLetterId)];
|
||
if (!duality) {
|
||
return "";
|
||
}
|
||
|
||
return card("Duality", `
|
||
<dl class="alpha-dl">
|
||
<dt>Polarity</dt><dd>${duality.left} / ${duality.right}</dd>
|
||
</dl>
|
||
`);
|
||
}
|
||
|
||
function renderHebrewFourWorldsCard(letter) {
|
||
const letterId = normalizeLetterId(letter?.hebrewLetterId || letter?.transliteration || letter?.char);
|
||
if (!letterId) {
|
||
return "";
|
||
}
|
||
|
||
const rows = (Array.isArray(state.fourWorldLayers) ? state.fourWorldLayers : [])
|
||
.filter((entry) => entry?.hebrewLetterId === letterId);
|
||
|
||
if (!rows.length) {
|
||
return "";
|
||
}
|
||
|
||
const body = rows.map((entry) => {
|
||
const pathBtn = Number.isFinite(Number(entry?.pathNumber))
|
||
? navBtn(`View Path ${entry.pathNumber}`, "nav:kabbalah-path", { "path-no": Number(entry.pathNumber) })
|
||
: "";
|
||
|
||
return `
|
||
<div class="cal-item-row">
|
||
<div class="cal-item-head">
|
||
<span class="cal-item-name">${entry.slot}: ${entry.letterChar} — ${entry.world}</span>
|
||
<span class="planet-list-meta">${entry.soulLayer}</span>
|
||
</div>
|
||
<div class="planet-text">${entry.worldLayer}${entry.worldDescription ? ` · ${entry.worldDescription}` : ""}</div>
|
||
<div class="planet-text">${entry.soulLayer}${entry.soulTitle ? ` — ${entry.soulTitle}` : ""}${entry.soulDescription ? `: ${entry.soulDescription}` : ""}</div>
|
||
<div class="alpha-nav-btns">${pathBtn}</div>
|
||
</div>
|
||
`;
|
||
}).join("");
|
||
|
||
return card("Qabalistic Worlds & Soul Layers", `<div class="cal-item-stack">${body}</div>`);
|
||
}
|
||
|
||
function normalizeLatinLetter(value) {
|
||
return String(value || "")
|
||
.trim()
|
||
.toUpperCase()
|
||
.replace(/[^A-Z]/g, "");
|
||
}
|
||
|
||
function extractEnglishLetterRefs(value) {
|
||
if (Array.isArray(value)) {
|
||
return [...new Set(value.map((entry) => normalizeLatinLetter(entry)).filter(Boolean))];
|
||
}
|
||
|
||
return [...new Set(
|
||
String(value || "")
|
||
.split(/[\s,;|\/]+/)
|
||
.map((entry) => normalizeLatinLetter(entry))
|
||
.filter(Boolean)
|
||
)];
|
||
}
|
||
|
||
function renderAlphabetEquivalentCard(activeAlphabet, letter) {
|
||
const hebrewLetters = Array.isArray(state.alphabets?.hebrew) ? state.alphabets.hebrew : [];
|
||
const greekLetters = Array.isArray(state.alphabets?.greek) ? state.alphabets.greek : [];
|
||
const englishLetters = Array.isArray(state.alphabets?.english) ? state.alphabets.english : [];
|
||
const arabicLetters = Array.isArray(state.alphabets?.arabic) ? state.alphabets.arabic : [];
|
||
const enochianLetters = Array.isArray(state.alphabets?.enochian) ? state.alphabets.enochian : [];
|
||
const linkedHebrewIds = new Set();
|
||
const linkedEnglishLetters = new Set();
|
||
const buttons = [];
|
||
|
||
function addHebrewId(value) {
|
||
const id = normalizeId(value);
|
||
if (id) {
|
||
linkedHebrewIds.add(id);
|
||
}
|
||
}
|
||
|
||
function addEnglishLetter(value) {
|
||
const code = normalizeLatinLetter(value);
|
||
if (!code) {
|
||
return;
|
||
}
|
||
|
||
linkedEnglishLetters.add(code);
|
||
englishLetters
|
||
.filter((entry) => normalizeLatinLetter(entry?.letter) === code)
|
||
.forEach((entry) => addHebrewId(entry?.hebrewLetterId));
|
||
}
|
||
|
||
if (activeAlphabet === "hebrew") {
|
||
addHebrewId(letter?.hebrewLetterId);
|
||
} else if (activeAlphabet === "greek") {
|
||
addHebrewId(letter?.hebrewLetterId);
|
||
englishLetters
|
||
.filter((entry) => normalizeId(entry?.greekEquivalent) === normalizeId(letter?.name))
|
||
.forEach((entry) => addEnglishLetter(entry?.letter));
|
||
} else if (activeAlphabet === "english") {
|
||
addEnglishLetter(letter?.letter);
|
||
addHebrewId(letter?.hebrewLetterId);
|
||
} else if (activeAlphabet === "arabic") {
|
||
addHebrewId(letter?.hebrewLetterId);
|
||
} else if (activeAlphabet === "enochian") {
|
||
extractEnglishLetterRefs(letter?.englishLetters).forEach((code) => addEnglishLetter(code));
|
||
addHebrewId(letter?.hebrewLetterId);
|
||
}
|
||
|
||
if (!linkedHebrewIds.size && !linkedEnglishLetters.size) {
|
||
return "";
|
||
}
|
||
|
||
const activeHebrewKey = normalizeId(letter?.hebrewLetterId);
|
||
const activeGreekKey = normalizeId(letter?.name);
|
||
const activeEnglishKey = normalizeLatinLetter(letter?.letter);
|
||
const activeArabicKey = normalizeId(letter?.name);
|
||
const activeEnochianKey = normalizeId(letter?.id || letter?.char || letter?.title);
|
||
|
||
hebrewLetters.forEach((heb) => {
|
||
const key = normalizeId(heb?.hebrewLetterId);
|
||
if (!key || !linkedHebrewIds.has(key)) {
|
||
return;
|
||
}
|
||
if (activeAlphabet === "hebrew" && key === activeHebrewKey) {
|
||
return;
|
||
}
|
||
|
||
buttons.push(`<button class="alpha-sister-btn" data-alpha="hebrew" data-key="${heb.hebrewLetterId}">
|
||
<span class="alpha-sister-glyph">${heb.char}</span>
|
||
<span class="alpha-sister-name">Hebrew: ${heb.name} (${heb.transliteration}) · gematria ${heb.numerology}</span>
|
||
</button>`);
|
||
});
|
||
|
||
greekLetters.forEach((grk) => {
|
||
const key = normalizeId(grk?.name);
|
||
const viaHebrew = linkedHebrewIds.has(normalizeId(grk?.hebrewLetterId));
|
||
const viaEnglish = englishLetters.some((eng) => (
|
||
linkedEnglishLetters.has(normalizeLatinLetter(eng?.letter))
|
||
&& normalizeId(eng?.greekEquivalent) === key
|
||
));
|
||
if (!(viaHebrew || viaEnglish)) {
|
||
return;
|
||
}
|
||
if (activeAlphabet === "greek" && key === activeGreekKey) {
|
||
return;
|
||
}
|
||
|
||
buttons.push(`<button class="alpha-sister-btn" data-alpha="greek" data-key="${grk.name}">
|
||
<span class="alpha-sister-glyph">${grk.char}</span>
|
||
<span class="alpha-sister-name">Greek: ${grk.displayName} (${grk.transliteration}) · isopsephy ${grk.numerology}</span>
|
||
</button>`);
|
||
});
|
||
|
||
englishLetters.forEach((eng) => {
|
||
const key = normalizeLatinLetter(eng?.letter);
|
||
const viaLetter = linkedEnglishLetters.has(key);
|
||
const viaHebrew = linkedHebrewIds.has(normalizeId(eng?.hebrewLetterId));
|
||
if (!(viaLetter || viaHebrew)) {
|
||
return;
|
||
}
|
||
if (activeAlphabet === "english" && key === activeEnglishKey) {
|
||
return;
|
||
}
|
||
|
||
buttons.push(`<button class="alpha-sister-btn" data-alpha="english" data-key="${eng.letter}">
|
||
<span class="alpha-sister-glyph">${eng.letter}</span>
|
||
<span class="alpha-sister-name">English: ${eng.letter} · pythagorean ${eng.pythagorean}</span>
|
||
</button>`);
|
||
});
|
||
|
||
arabicLetters.forEach((arb) => {
|
||
const key = normalizeId(arb?.name);
|
||
if (!linkedHebrewIds.has(normalizeId(arb?.hebrewLetterId))) {
|
||
return;
|
||
}
|
||
if (activeAlphabet === "arabic" && key === activeArabicKey) {
|
||
return;
|
||
}
|
||
|
||
buttons.push(`<button class="alpha-sister-btn" data-alpha="arabic" data-key="${arb.name}">
|
||
<span class="alpha-sister-glyph alpha-list-glyph--arabic">${arb.char}</span>
|
||
<span class="alpha-sister-name">Arabic: ${arabicDisplayName(arb)} — ${arb.nameArabic} (${arb.transliteration}) · abjad ${arb.abjad}</span>
|
||
</button>`);
|
||
});
|
||
|
||
enochianLetters.forEach((eno) => {
|
||
const key = normalizeId(eno?.id || eno?.char || eno?.title);
|
||
const englishRefs = extractEnglishLetterRefs(eno?.englishLetters);
|
||
const viaHebrew = linkedHebrewIds.has(normalizeId(eno?.hebrewLetterId));
|
||
const viaEnglish = englishRefs.some((code) => linkedEnglishLetters.has(code));
|
||
if (!(viaHebrew || viaEnglish)) {
|
||
return;
|
||
}
|
||
if (activeAlphabet === "enochian" && key === activeEnochianKey) {
|
||
return;
|
||
}
|
||
|
||
buttons.push(`<button class="alpha-sister-btn" data-alpha="enochian" data-key="${eno.id}">
|
||
${enochianGlyphImageHtml(eno, "alpha-enochian-glyph-img alpha-enochian-glyph-img--sister")}
|
||
<span class="alpha-sister-name">Enochian: ${eno.title} (${eno.transliteration}) · English ${englishRefs.join("/") || "—"}</span>
|
||
</button>`);
|
||
});
|
||
|
||
if (!buttons.length) {
|
||
return "";
|
||
}
|
||
|
||
return card("ALPHABET EQUIVALENT", `<div class="alpha-sister-wrap">${buttons.join("")}</div>`);
|
||
}
|
||
|
||
function renderHebrewDetail(letter) {
|
||
detailSubEl.textContent = `${letter.name} — ${letter.transliteration}`;
|
||
detailBodyEl.innerHTML = "";
|
||
|
||
const sections = [];
|
||
|
||
// Basics
|
||
sections.push(card("Letter Details", `
|
||
<dl class="alpha-dl">
|
||
<dt>Character</dt><dd>${letter.char}</dd>
|
||
<dt>Name</dt><dd>${letter.name}</dd>
|
||
<dt>Transliteration</dt><dd>${letter.transliteration}</dd>
|
||
<dt>Meaning</dt><dd>${letter.meaning}</dd>
|
||
<dt>Gematria Value</dt><dd>${letter.numerology}</dd>
|
||
<dt>Letter Type</dt><dd class="alpha-badge alpha-badge--${letter.letterType}">${letter.letterType}</dd>
|
||
<dt>Position</dt><dd>#${letter.index} of 22</dd>
|
||
</dl>
|
||
`));
|
||
|
||
const positionRootCard = renderPositionDigitalRootCard(letter, "hebrew");
|
||
if (positionRootCard) {
|
||
sections.push(positionRootCard);
|
||
}
|
||
|
||
if (letter.letterType === "double") {
|
||
const dualityCard = renderHebrewDualityCard(letter);
|
||
if (dualityCard) {
|
||
sections.push(dualityCard);
|
||
}
|
||
}
|
||
|
||
const fourWorldsCard = renderHebrewFourWorldsCard(letter);
|
||
if (fourWorldsCard) {
|
||
sections.push(fourWorldsCard);
|
||
}
|
||
|
||
// Astrology
|
||
if (letter.astrology) {
|
||
sections.push(renderAstrologyCard(letter.astrology));
|
||
}
|
||
|
||
// Kabbalah Path + Tarot
|
||
if (letter.kabbalahPathNumber) {
|
||
const tarotPart = letter.tarot
|
||
? `<dt>Tarot Card</dt><dd>${letter.tarot.card} (Trump ${letter.tarot.trumpNumber})</dd>`
|
||
: "";
|
||
const kabBtn = navBtn("View Kabbalah Path", "tarot:view-kab-path", { "path-number": letter.kabbalahPathNumber });
|
||
const tarotBtn = letter.tarot
|
||
? navBtn("View Tarot Card", "kab:view-trump", { "trump-number": letter.tarot.trumpNumber })
|
||
: "";
|
||
const cubePlacement = getCubePlacementForHebrewLetter(letter.hebrewLetterId, letter.kabbalahPathNumber);
|
||
const cubeBtn = cubePlacementBtn(cubePlacement, {
|
||
"hebrew-letter-id": letter.hebrewLetterId,
|
||
"path-no": letter.kabbalahPathNumber
|
||
});
|
||
sections.push(card("Kabbalah & Tarot", `
|
||
<dl class="alpha-dl">
|
||
<dt>Path Number</dt><dd>${letter.kabbalahPathNumber}</dd>
|
||
${tarotPart}
|
||
</dl>
|
||
<div class="alpha-nav-btns">${kabBtn}${tarotBtn}${cubeBtn}</div>
|
||
`));
|
||
}
|
||
|
||
const monthRefs = monthRefsForLetter(letter);
|
||
const monthCard = calendarMonthsCard(monthRefs, `Calendar correspondences linked to ${letter.name}.`);
|
||
if (monthCard) {
|
||
sections.push(monthCard);
|
||
}
|
||
|
||
const equivalentsCard = renderAlphabetEquivalentCard("hebrew", letter);
|
||
if (equivalentsCard) {
|
||
sections.push(equivalentsCard);
|
||
}
|
||
|
||
detailBodyEl.innerHTML = sections.join("");
|
||
attachDetailListeners();
|
||
}
|
||
|
||
function renderGreekDetail(letter) {
|
||
const archaicBadge = letter.archaic ? ' <span class="alpha-badge alpha-badge--archaic">archaic</span>' : "";
|
||
detailSubEl.textContent = `${letter.displayName}${letter.archaic ? " (archaic)" : ""} — ${letter.transliteration}`;
|
||
detailBodyEl.innerHTML = "";
|
||
|
||
const sections = [];
|
||
|
||
const charRow = letter.charFinal
|
||
? `<dt>Form (final)</dt><dd>${letter.charFinal}</dd>`
|
||
: "";
|
||
sections.push(card("Letter Details", `
|
||
<dl class="alpha-dl">
|
||
<dt>Uppercase</dt><dd>${letter.char}</dd>
|
||
<dt>Lowercase</dt><dd>${letter.charLower || "—"}</dd>
|
||
${charRow}
|
||
<dt>Name</dt><dd>${letter.displayName}${archaicBadge}</dd>
|
||
<dt>Transliteration</dt><dd>${letter.transliteration}</dd>
|
||
<dt>IPA</dt><dd>${letter.ipa || "—"}</dd>
|
||
<dt>Isopsephy Value</dt><dd>${letter.numerology}</dd>
|
||
<dt>Meaning / Origin</dt><dd>${letter.meaning || "—"}</dd>
|
||
</dl>
|
||
`));
|
||
|
||
const positionRootCard = renderPositionDigitalRootCard(letter, "greek");
|
||
if (positionRootCard) {
|
||
sections.push(positionRootCard);
|
||
}
|
||
|
||
const equivalentsCard = renderAlphabetEquivalentCard("greek", letter);
|
||
if (equivalentsCard) {
|
||
sections.push(equivalentsCard);
|
||
}
|
||
|
||
const monthRefs = monthRefsForLetter(letter);
|
||
const monthCard = calendarMonthsCard(monthRefs, `Calendar correspondences inherited via ${letter.displayName}'s Hebrew origin.`);
|
||
if (monthCard) {
|
||
sections.push(monthCard);
|
||
}
|
||
|
||
detailBodyEl.innerHTML = sections.join("");
|
||
attachDetailListeners();
|
||
}
|
||
|
||
function renderEnglishDetail(letter) {
|
||
detailSubEl.textContent = `Letter ${letter.letter} · position #${letter.index}`;
|
||
detailBodyEl.innerHTML = "";
|
||
|
||
const sections = [];
|
||
|
||
sections.push(card("Letter Details", `
|
||
<dl class="alpha-dl">
|
||
<dt>Letter</dt><dd>${letter.letter}</dd>
|
||
<dt>Position</dt><dd>#${letter.index} of 26</dd>
|
||
<dt>IPA</dt><dd>${letter.ipa || "—"}</dd>
|
||
<dt>Pythagorean Value</dt><dd>${letter.pythagorean}</dd>
|
||
</dl>
|
||
`));
|
||
|
||
const positionRootCard = renderPositionDigitalRootCard(letter, "english");
|
||
if (positionRootCard) {
|
||
sections.push(positionRootCard);
|
||
}
|
||
|
||
const equivalentsCard = renderAlphabetEquivalentCard("english", letter);
|
||
if (equivalentsCard) {
|
||
sections.push(equivalentsCard);
|
||
}
|
||
|
||
const monthRefs = monthRefsForLetter(letter);
|
||
const monthCard = calendarMonthsCard(monthRefs, `Calendar correspondences linked through this letter's Hebrew correspondence.`);
|
||
if (monthCard) {
|
||
sections.push(monthCard);
|
||
}
|
||
|
||
detailBodyEl.innerHTML = sections.join("");
|
||
attachDetailListeners();
|
||
}
|
||
|
||
function renderArabicDetail(letter) {
|
||
detailSubEl.textContent = `${arabicDisplayName(letter)} — ${letter.transliteration}`;
|
||
detailBodyEl.innerHTML = "";
|
||
|
||
const sections = [];
|
||
|
||
// Letter forms row
|
||
const f = letter.forms || {};
|
||
const formParts = [
|
||
f.isolated ? `<span class="alpha-arabic-form"><span class="alpha-arabic-glyph">${f.isolated}</span><br>isolated</span>` : "",
|
||
f.final ? `<span class="alpha-arabic-form"><span class="alpha-arabic-glyph">${f.final}</span><br>final</span>` : "",
|
||
f.medial ? `<span class="alpha-arabic-form"><span class="alpha-arabic-glyph">${f.medial}</span><br>medial</span>` : "",
|
||
f.initial ? `<span class="alpha-arabic-form"><span class="alpha-arabic-glyph">${f.initial}</span><br>initial</span>` : ""
|
||
].filter(Boolean);
|
||
|
||
sections.push(card("Letter Details", `
|
||
<dl class="alpha-dl">
|
||
<dt>Arabic Name</dt><dd class="alpha-arabic-inline">${letter.nameArabic}</dd>
|
||
<dt>Transliteration</dt><dd>${letter.transliteration}</dd>
|
||
<dt>IPA</dt><dd>${letter.ipa || "—"}</dd>
|
||
<dt>Abjad Value</dt><dd>${letter.abjad}</dd>
|
||
<dt>Meaning</dt><dd>${letter.meaning || "—"}</dd>
|
||
<dt>Category</dt><dd class="alpha-badge alpha-badge--${letter.category}">${letter.category}</dd>
|
||
<dt>Position</dt><dd>#${letter.index} of 28 (Abjad order)</dd>
|
||
</dl>
|
||
`));
|
||
|
||
const positionRootCard = renderPositionDigitalRootCard(letter, "arabic", "Abjad order");
|
||
if (positionRootCard) {
|
||
sections.push(positionRootCard);
|
||
}
|
||
|
||
if (formParts.length) {
|
||
sections.push(card("Letter Forms", `<div class="alpha-arabic-forms">${formParts.join("")}</div>`));
|
||
}
|
||
|
||
const equivalentsCard = renderAlphabetEquivalentCard("arabic", letter);
|
||
if (equivalentsCard) {
|
||
sections.push(equivalentsCard);
|
||
}
|
||
|
||
detailBodyEl.innerHTML = sections.join("");
|
||
attachDetailListeners();
|
||
}
|
||
|
||
function renderEnochianDetail(letter) {
|
||
const englishRefs = extractEnglishLetterRefs(letter?.englishLetters);
|
||
detailSubEl.textContent = `${letter.title} — ${letter.transliteration}`;
|
||
detailBodyEl.innerHTML = "";
|
||
|
||
const sections = [];
|
||
|
||
sections.push(card("Letter Details", `
|
||
<dl class="alpha-dl">
|
||
<dt>Character</dt><dd>${enochianGlyphImageHtml(letter, "alpha-enochian-glyph-img alpha-enochian-glyph-img--detail-row")}</dd>
|
||
<dt>Name</dt><dd>${letter.title}</dd>
|
||
<dt>English Letters</dt><dd>${englishRefs.join(" / ") || "—"}</dd>
|
||
<dt>Transliteration</dt><dd>${letter.transliteration || "—"}</dd>
|
||
<dt>Element / Planet</dt><dd>${letter.elementOrPlanet || "—"}</dd>
|
||
<dt>Tarot</dt><dd>${letter.tarot || "—"}</dd>
|
||
<dt>Numerology</dt><dd>${letter.numerology || "—"}</dd>
|
||
<dt>Glyph Source</dt><dd>Local cache: asset/img/enochian (sourced from dCode set)</dd>
|
||
<dt>Position</dt><dd>#${letter.index} of 21</dd>
|
||
</dl>
|
||
`));
|
||
|
||
const positionRootCard = renderPositionDigitalRootCard(letter, "enochian");
|
||
if (positionRootCard) {
|
||
sections.push(positionRootCard);
|
||
}
|
||
|
||
const equivalentsCard = renderAlphabetEquivalentCard("enochian", letter);
|
||
if (equivalentsCard) {
|
||
sections.push(equivalentsCard);
|
||
}
|
||
|
||
const monthRefs = monthRefsForLetter(letter);
|
||
const monthCard = calendarMonthsCard(monthRefs, `Calendar correspondences linked through this letter's Hebrew correspondence.`);
|
||
if (monthCard) {
|
||
sections.push(monthCard);
|
||
}
|
||
|
||
detailBodyEl.innerHTML = sections.join("");
|
||
attachDetailListeners();
|
||
}
|
||
|
||
// ── Event delegation on detail body ──────────────────────────────────
|
||
function attachDetailListeners() {
|
||
if (!detailBodyEl) return;
|
||
|
||
// Nav buttons — generic: forward all data-* (except data-event) as the event detail
|
||
detailBodyEl.querySelectorAll(".alpha-nav-btn[data-event]").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
const evtName = btn.dataset.event;
|
||
const detail = {};
|
||
Object.entries(btn.dataset).forEach(([key, val]) => {
|
||
if (key === "event") return;
|
||
// Convert kebab data keys to camelCase (e.g. planet-id → planetId)
|
||
const camel = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||
detail[camel] = isNaN(Number(val)) || val === "" ? val : Number(val);
|
||
});
|
||
document.dispatchEvent(new CustomEvent(evtName, { detail }));
|
||
});
|
||
});
|
||
|
||
// Sister letter cross-navigation within this section
|
||
detailBodyEl.querySelectorAll(".alpha-sister-btn[data-alpha]").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
const alpha = btn.dataset.alpha;
|
||
const key = btn.dataset.key;
|
||
switchAlphabet(alpha, key);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── 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);
|
||
}
|
||
|
||
// ── 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();
|
||
}
|
||
}
|
||
|
||
function updateTabs() {
|
||
[tabAll, tabHebrew, tabGreek, tabEnglish, tabArabic, tabEnochian].forEach((btn) => {
|
||
if (!btn) return;
|
||
btn.classList.toggle("alpha-tab-active", btn.dataset.alpha === state.activeAlphabet);
|
||
});
|
||
}
|
||
|
||
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 = "";
|
||
}
|
||
|
||
// ── Public init ───────────────────────────────────────────────────────
|
||
function ensureAlphabetSection(magickDataset, referenceData = null) {
|
||
const grouped = magickDataset?.grouped || {};
|
||
const alphabetData = (grouped["alphabets"] && grouped["alphabets"]["hebrew"])
|
||
? grouped["alphabets"]
|
||
: null;
|
||
|
||
if (alphabetData) {
|
||
state.alphabets = alphabetData;
|
||
if (state.gematria.db?.baseAlphabet) {
|
||
refreshGematriaScriptMap(state.gematria.db.baseAlphabet);
|
||
}
|
||
}
|
||
|
||
state.fourWorldLayers = buildFourWorldLayersFromDataset(magickDataset);
|
||
|
||
state.cubeRefs = buildCubeReferences(magickDataset);
|
||
|
||
if (referenceData && state.alphabets) {
|
||
state.monthRefsByHebrewId = buildMonthReferencesByHebrew(referenceData, state.alphabets);
|
||
}
|
||
|
||
if (state.initialized) {
|
||
ensureGematriaCalculator();
|
||
syncFilterControls();
|
||
renderList();
|
||
const letters = getFilteredLetters();
|
||
const selected = letters.find((entry) => letterKey(entry) === state.selectedKey) || letters[0];
|
||
if (selected) {
|
||
renderDetail(selected);
|
||
} else {
|
||
resetDetail();
|
||
if (detailSubEl) {
|
||
detailSubEl.textContent = "No letters match the current filter.";
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
state.initialized = true;
|
||
|
||
// alphabets.json is a top-level file → grouped["alphabets"] = the data object
|
||
|
||
getElements();
|
||
ensureGematriaCalculator();
|
||
bindFilterControls();
|
||
syncFilterControls();
|
||
|
||
if (!state.alphabets) {
|
||
if (detailSubEl) detailSubEl.textContent = "Alphabet data not loaded.";
|
||
return;
|
||
}
|
||
|
||
// Attach tab listeners
|
||
[tabAll, tabHebrew, tabGreek, tabEnglish, tabArabic, tabEnochian].forEach((btn) => {
|
||
if (!btn) return;
|
||
btn.addEventListener("click", () => {
|
||
switchAlphabet(btn.dataset.alpha, null);
|
||
});
|
||
});
|
||
|
||
switchAlphabet("all", null);
|
||
}
|
||
|
||
// ── Incoming navigation ───────────────────────────────────────────────
|
||
function selectLetterByHebrewId(hebrewLetterId) {
|
||
switchAlphabet("hebrew", hebrewLetterId);
|
||
}
|
||
|
||
function selectGreekLetterByName(name) {
|
||
switchAlphabet("greek", name);
|
||
}
|
||
|
||
function selectEnglishLetter(letter) {
|
||
switchAlphabet("english", letter);
|
||
}
|
||
|
||
function selectArabicLetter(name) {
|
||
switchAlphabet("arabic", name);
|
||
}
|
||
|
||
function selectEnochianLetter(id) {
|
||
switchAlphabet("enochian", id);
|
||
}
|
||
|
||
window.AlphabetSectionUi = {
|
||
ensureAlphabetSection,
|
||
selectLetterByHebrewId,
|
||
selectGreekLetterByName,
|
||
selectEnglishLetter,
|
||
selectArabicLetter,
|
||
selectEnochianLetter
|
||
};
|
||
})();
|