(function () { "use strict"; // ─── SVG tree layout constants ────────────────────────────────────────────── const NS = "http://www.w3.org/2000/svg"; const R = 11; // sephira circle radius // Standard Hermetic GD Tree of Life positions in a 240×470 viewBox const NODE_POS = { 1: [120, 30], // Kether — crown of middle pillar 2: [200, 88], // Chokmah — right pillar 3: [40, 88], // Binah — left pillar 4: [200, 213], // Chesed — right pillar 5: [40, 213], // Geburah — left pillar 6: [120, 273], // Tiphareth — middle pillar 7: [200, 343], // Netzach — right pillar 8: [40, 343], // Hod — left pillar 9: [120, 398], // Yesod — middle pillar 10: [120, 448], // Malkuth — bottom of middle pillar }; // King-scale fill colours const SEPH_FILL = { 1: "#e8e8e8", // Kether — white brilliance 2: "#87ceeb", // Chokmah — soft sky blue 3: "#7d2b5a", // Binah — dark crimson 4: "#1a56e0", // Chesed — deep blue 5: "#d44014", // Geburah — scarlet 6: "#d4a017", // Tiphareth — gold 7: "#22aa60", // Netzach — emerald 8: "#cc5500", // Hod — orange 9: "#6030c0", // Yesod — violet 10: "#c8b000", // Malkuth — citrine / amber }; // Nodes with light-ish fills get dark number text; others get white const DARK_TEXT = new Set([1, 2, 6, 10]); // Da'at – phantom sephira drawn as a dashed circle, not clickable const DAAT = [120, 148]; const PATH_MARKER_SCALE = 1.33; const PATH_LABEL_RADIUS = 9 * PATH_MARKER_SCALE; const PATH_LABEL_FONT_SIZE = 8.8 * PATH_MARKER_SCALE; const PATH_TAROT_WIDTH = 16 * PATH_MARKER_SCALE; const PATH_TAROT_HEIGHT = 24 * PATH_MARKER_SCALE; const PATH_LABEL_OFFSET_WITH_TAROT = 11 * PATH_MARKER_SCALE; const PATH_TAROT_OFFSET_WITH_LABEL = 1 * PATH_MARKER_SCALE; const PATH_TAROT_OFFSET_NO_LABEL = 12 * PATH_MARKER_SCALE; // ─── state ────────────────────────────────────────────────────────────────── const state = { initialized: false, tree: null, godsData: {}, hebrewLetterIdByToken: {}, fourWorldLayers: [], showPathLetters: true, showPathNumbers: true, showPathTarotCards: false, selectedWorldLayerIndex: 0, selectedSephiraNumber: null, selectedPathNumber: null, activeNodeKey: "", exportInProgress: false, exportFormat: "" }; let detailNavigator = null; let browserDetailNavigator = null; let worldsDetailNavigator = null; let pathsDetailNavigator = null; let roseDetailNavigator = null; const TREE_EXPORT_FORMATS = { webp: { mimeType: "image/webp", extension: "webp", quality: 0.98 } }; const TREE_EXPORT_BACKGROUND = "#02030a"; const DAATH_SEPHIRA = Object.freeze({ number: 0, displayNumber: "Daath", sephiraId: "daath", name: "Daath", nameHebrew: "דעת", translation: "Knowledge", planet: "Abyss / Hidden Sephirah", intelligence: "Invisible Sephirah of Knowledge", tarot: "No fixed trump attribution", description: "Daath is the hidden or invisible sephirah placed beneath Kether and between Chokmah and Binah. In Hermetic Qabalah it often marks the threshold of the Abyss rather than a stable emanation like the ten manifest sephiroth." }); const kabbalahDetailUi = window.KabbalahDetailUi || {}; const kabbalahViewsUi = window.KabbalahViewsUi || {}; let webpExportSupported = null; if ( typeof kabbalahViewsUi.renderTree !== "function" || typeof kabbalahViewsUi.renderRoseCross !== "function" ) { throw new Error("KabbalahViewsUi module must load before ui-kabbalah.js"); } const PLANET_NAME_TO_ID = { saturn: "saturn", jupiter: "jupiter", mars: "mars", sol: "sol", sun: "sol", venus: "venus", mercury: "mercury", luna: "luna", moon: "luna" }; const ZODIAC_NAME_TO_ID = { aries: "aries", taurus: "taurus", gemini: "gemini", cancer: "cancer", leo: "leo", virgo: "virgo", libra: "libra", scorpio: "scorpio", sagittarius: "sagittarius", capricorn: "capricorn", aquarius: "aquarius", pisces: "pisces" }; const HEBREW_LETTER_ALIASES = { aleph: "alef", alef: "alef", beth: "bet", bet: "bet", gimel: "gimel", daleth: "dalet", dalet: "dalet", he: "he", vav: "vav", zayin: "zayin", cheth: "het", chet: "het", het: "het", teth: "tet", tet: "tet", yod: "yod", kaph: "kaf", kaf: "kaf", lamed: "lamed", mem: "mem", nun: "nun", samekh: "samekh", ayin: "ayin", pe: "pe", tzaddi: "tsadi", tzadi: "tsadi", tsadi: "tsadi", qoph: "qof", qof: "qof", resh: "resh", shin: "shin", tav: "tav" }; const DEFAULT_FOUR_QABALISTIC_WORLD_LAYERS = [ { slot: "Yod", letterChar: "י", hebrewToken: "yod", world: "Atziluth", worldLayer: "Archetypal World (God’s Will)", worldDescription: "World of gods or specific facets or divine qualities.", soulLayer: "Chiah", soulTitle: "Life Force", soulDescription: "The Chiah is the Life Force itself and our true identity as reflection of Supreme Consciousness." }, { slot: "Heh", letterChar: "ה", hebrewToken: "he", world: "Briah", worldLayer: "Creative World (God’s Love)", worldDescription: "World of archangels, executors of divine qualities.", soulLayer: "Neshamah", soulTitle: "Soul-Intuition", soulDescription: "The Neshamah is the part of our soul that transcends the thinking process." }, { slot: "Vav", letterChar: "ו", hebrewToken: "vav", world: "Yetzirah", worldLayer: "Formative World (God’s Mind)", worldDescription: "World of angels who work under archangelic direction.", soulLayer: "Ruach", soulTitle: "Intellect", soulDescription: "The Ruach is the thinking mind that often dominates attention and identity." }, { slot: "Heh (final)", letterChar: "ה", hebrewToken: "he", world: "Assiah", worldLayer: "Material World (God’s Creation)", worldDescription: "World of spirits that infuse matter and energy through specialized duties.", soulLayer: "Nephesh", soulTitle: "Animal Soul", soulDescription: "The Nephesh is instinctive consciousness expressed through appetite, emotion, sex drive, and survival." } ]; function titleCase(value) { return String(value || "") .split(/[\s-_]+/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } 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; if (!worlds || typeof worlds !== "object") { return [...DEFAULT_FOUR_QABALISTIC_WORLD_LAYERS]; } const worldOrder = ["atzilut", "briah", "yetzirah", "assiah"]; const soulAliases = { chiah: "chaya", chaya: "chaya", neshamah: "neshama", neshama: "neshama", ruach: "ruach", nephesh: "nephesh" }; return worldOrder.map((worldId, index) => { const fallback = DEFAULT_FOUR_QABALISTIC_WORLD_LAYERS[index] || {}; const worldEntry = worlds?.[worldId] || null; if (!worldEntry || typeof worldEntry !== "object") { return fallback; } const tetragrammaton = worldEntry?.tetragrammaton && typeof worldEntry.tetragrammaton === "object" ? worldEntry.tetragrammaton : {}; const rawSoulId = normalizeSoulId(worldEntry?.soulId); const soulId = soulAliases[rawSoulId] || rawSoulId; const soulEntry = souls?.[soulId] && typeof souls[soulId] === "object" ? souls[soulId] : null; const soulLayer = soulEntry?.name?.roman || fallback.soulLayer || titleCase(rawSoulId || soulId); const soulTitle = soulEntry?.title?.en || fallback.soulTitle || titleCase(soulEntry?.name?.en || ""); const soulDescription = soulEntry?.desc?.en || fallback.soulDescription || ""; return { slot: tetragrammaton?.isFinal ? `${String(tetragrammaton?.slot || fallback.slot || "Heh")} (final)` : String(tetragrammaton?.slot || fallback.slot || ""), letterChar: String(tetragrammaton?.letterChar || fallback.letterChar || ""), hebrewToken: String(tetragrammaton?.hebrewLetterId || fallback.hebrewToken || "").toLowerCase(), world: String(worldEntry?.name?.roman || fallback.world || titleCase(worldEntry?.id || worldId)), worldLayer: String(worldEntry?.worldLayer?.en || fallback.worldLayer || worldEntry?.desc?.en || ""), worldDescription: String(worldEntry?.worldDescription?.en || fallback.worldDescription || ""), soulLayer: String(soulLayer || ""), soulTitle: String(soulTitle || ""), soulDescription: String(soulDescription || "") }; }).filter(Boolean); } // ─── element references ───────────────────────────────────────────────────── function getElements() { return { browserSectionEl: document.getElementById("kabbalah-section"), browserListEl: document.getElementById("kab-browser-list"), browserCountEl: document.getElementById("kab-browser-count"), browserDetailNameEl: document.getElementById("kab-browser-detail-name"), browserDetailSubEl: document.getElementById("kab-browser-detail-sub"), browserDetailBodyEl: document.getElementById("kab-browser-detail-body"), browserDetailPrevEl: document.getElementById("kab-browser-detail-prev"), browserDetailPositionEl: document.getElementById("kab-browser-detail-position"), browserDetailNextEl: document.getElementById("kab-browser-detail-next"), worldsSectionEl: document.getElementById("kabbalah-worlds-section"), worldsListEl: document.getElementById("kab-worlds-list"), worldsCountEl: document.getElementById("kab-worlds-count"), worldsDetailNameEl: document.getElementById("kab-worlds-detail-name"), worldsDetailSubEl: document.getElementById("kab-worlds-detail-sub"), worldsDetailBodyEl: document.getElementById("kab-worlds-detail-body"), worldsDetailPrevEl: document.getElementById("kab-worlds-detail-prev"), worldsDetailPositionEl: document.getElementById("kab-worlds-detail-position"), worldsDetailNextEl: document.getElementById("kab-worlds-detail-next"), pathsSectionEl: document.getElementById("kabbalah-paths-section"), pathsListEl: document.getElementById("kab-paths-list"), pathsCountEl: document.getElementById("kab-paths-count"), pathsDetailNameEl: document.getElementById("kab-paths-detail-name"), pathsDetailSubEl: document.getElementById("kab-paths-detail-sub"), pathsDetailBodyEl: document.getElementById("kab-paths-detail-body"), pathsDetailPrevEl: document.getElementById("kab-paths-detail-prev"), pathsDetailPositionEl: document.getElementById("kab-paths-detail-position"), pathsDetailNextEl: document.getElementById("kab-paths-detail-next"), crossSectionEl: document.getElementById("kabbalah-cross-section"), sectionEl: document.getElementById("kabbalah-tree-section"), treeContainerEl: document.getElementById("kab-tree-container"), detailNameEl: document.getElementById("kab-detail-name"), detailSubEl: document.getElementById("kab-detail-sub"), detailBodyEl: document.getElementById("kab-detail-body"), detailPrevEl: document.getElementById("kab-detail-prev"), detailPositionEl: document.getElementById("kab-detail-position"), detailNextEl: document.getElementById("kab-detail-next"), pathLetterToggleEl: document.getElementById("kab-path-letter-toggle"), pathNumberToggleEl: document.getElementById("kab-path-number-toggle"), pathTarotToggleEl: document.getElementById("kab-path-tarot-toggle"), treeExportWebpEl: document.getElementById("kab-tree-export-webp"), roseCrossContainerEl: document.getElementById("kab-rose-cross-container"), roseDetailNameEl: document.getElementById("kab-rose-detail-name"), roseDetailSubEl: document.getElementById("kab-rose-detail-sub"), roseDetailBodyEl: document.getElementById("kab-rose-detail-body"), roseDetailPrevEl: document.getElementById("kab-rose-detail-prev"), roseDetailPositionEl: document.getElementById("kab-rose-detail-position"), roseDetailNextEl: document.getElementById("kab-rose-detail-next"), }; } function getTreeDetailElements(elements) { if (!elements) { return null; } return { detailNameEl: elements.detailNameEl, detailSubEl: elements.detailSubEl, detailBodyEl: elements.detailBodyEl }; } function getRoseDetailElements(elements) { if (!elements) { return null; } return { detailNameEl: elements.roseDetailNameEl, detailSubEl: elements.roseDetailSubEl, detailBodyEl: elements.roseDetailBodyEl }; } function getPathDetailElements(elements) { if (!elements) { return null; } return { detailNameEl: elements.pathsDetailNameEl, detailSubEl: elements.pathsDetailSubEl, detailBodyEl: elements.pathsDetailBodyEl }; } function getBrowserDetailElements(elements) { if (!elements) { return null; } return { detailNameEl: elements.browserDetailNameEl, detailSubEl: elements.browserDetailSubEl, detailBodyEl: elements.browserDetailBodyEl }; } function getWorldDetailElements(elements) { if (!elements) { return null; } return { detailNameEl: elements.worldsDetailNameEl, detailSubEl: elements.worldsDetailSubEl, detailBodyEl: elements.worldsDetailBodyEl }; } function normalizeDetailElements(elements) { if (!elements) { return null; } return { detailNameEl: elements.detailNameEl || null, detailSubEl: elements.detailSubEl || null, detailBodyEl: elements.detailBodyEl || null }; } function getDetailRenderTargets(primaryElements) { const elements = getElements(); const candidates = [ normalizeDetailElements(primaryElements), getTreeDetailElements(elements), getBrowserDetailElements(elements) ]; const seen = new Set(); return candidates.filter((target) => { const bodyEl = target?.detailBodyEl; if (!(bodyEl instanceof Element) || seen.has(bodyEl)) { return false; } seen.add(bodyEl); return true; }); } function hasFiniteSelectionNumber(value) { if (value === null || value === undefined || value === "") { return false; } return Number.isFinite(Number(value)); } function isDaathToken(value) { return String(value || "").trim().toLowerCase() === "daath"; } function buildSephiraKey(value) { if (isDaathToken(value) || Number(value) === 0) { return "sephira:daath"; } return hasFiniteSelectionNumber(value) ? `sephira:${Number(value)}` : ""; } function buildPathKey(value) { return hasFiniteSelectionNumber(value) ? `path:${Number(value)}` : ""; } function buildWorldKey(value) { return hasFiniteSelectionNumber(value) ? `world:${Number(value)}` : ""; } function getSelectedSephiraKey() { return buildSephiraKey(state.selectedSephiraNumber); } function getSelectedPathKey() { return buildPathKey(state.selectedPathNumber); } function getSelectedWorldKey() { return buildWorldKey(state.selectedWorldLayerIndex); } function getSephiraByNumber(number) { if (isDaathToken(number) || Number(number) === 0) { return DAATH_SEPHIRA; } if (!state.tree) { return null; } return state.tree.sephiroth.find((entry) => Number(entry?.number) === Number(number)) || null; } function getPathByNumber(number) { if (!state.tree) { return null; } return state.tree.paths.find((entry) => Number(entry?.pathNumber) === Number(number)) || null; } function getWorldLayerByIndex(index) { if (!Array.isArray(state.fourWorldLayers)) { return null; } const targetIndex = Number(index); return Number.isInteger(targetIndex) && targetIndex >= 0 && targetIndex < state.fourWorldLayers.length ? state.fourWorldLayers[targetIndex] : null; } function getSephirotSequenceEntries() { if (!state.tree) { return []; } const entries = Array.isArray(state.tree.sephiroth) ? [...state.tree.sephiroth] .sort((left, right) => Number(left?.number || 0) - Number(right?.number || 0)) .map((entry) => ({ key: buildSephiraKey(entry?.number), type: "sephira", number: Number(entry?.number) })) : []; const daathEntry = { key: buildSephiraKey(0), type: "sephira", number: 0 }; const insertIndex = entries.findIndex((entry) => entry.number === 3); if (insertIndex >= 0) { entries.splice(insertIndex + 1, 0, daathEntry); } else { entries.push(daathEntry); } return entries; } function getPathSequenceEntries() { if (!state.tree) { return []; } return Array.isArray(state.tree.paths) ? [...state.tree.paths] .sort((left, right) => Number(left?.pathNumber || 0) - Number(right?.pathNumber || 0)) .map((entry) => ({ key: buildPathKey(entry?.pathNumber), type: "path", number: Number(entry?.pathNumber) })) : []; } function getWorldSequenceEntries() { return Array.isArray(state.fourWorldLayers) ? state.fourWorldLayers.map((layer, index) => ({ key: buildWorldKey(index), type: "world", index, world: String(layer?.world || "") })) : []; } function getNodeSequenceEntries() { if (!state.tree) { return []; } const sephiroth = Array.isArray(state.tree.sephiroth) ? [...state.tree.sephiroth] .sort((left, right) => Number(left?.number || 0) - Number(right?.number || 0)) .map((entry) => ({ key: buildSephiraKey(entry?.number), type: "sephira", number: Number(entry?.number) })) : []; const paths = getPathSequenceEntries(); return [...sephiroth, ...paths]; } function getSelectedNodeKey() { return String(state.activeNodeKey || "").trim() || getSelectedPathKey() || getSelectedSephiraKey(); } function buildSequenceState(entries, currentKey) { const currentIndex = entries.findIndex((entry) => entry.key === currentKey); return { total: entries.length, currentIndex, previousKey: currentIndex > 0 ? entries[currentIndex - 1].key : "", nextKey: currentIndex >= 0 && currentIndex < entries.length - 1 ? entries[currentIndex + 1].key : "" }; } function getNodeSequenceState() { return buildSequenceState(getNodeSequenceEntries(), getSelectedNodeKey()); } function getSephirotSequenceState() { return buildSequenceState(getSephirotSequenceEntries(), getSelectedSephiraKey()); } function getPathSequenceState() { return buildSequenceState(getPathSequenceEntries(), getSelectedPathKey()); } function getWorldSequenceState() { return buildSequenceState(getWorldSequenceEntries(), getSelectedWorldKey()); } function getBrowserListItemMeta(entry) { const seph = getSephiraByNumber(entry.number); const displayNumber = String(seph?.displayNumber || entry.number || "").trim(); return { title: displayNumber ? `${displayNumber} · ${seph?.name || "Sephirah"}` : `${seph?.name || "Sephirah"}`, meta: [seph?.nameHebrew, seph?.translation, seph?.planet].filter(Boolean).join(" · ") || "Sephirah" }; } function getWorldListItemMeta(entry) { const layer = getWorldLayerByIndex(entry.index); return { title: String(layer?.world || `World ${entry.index + 1}`), meta: [ layer?.slot ? `${layer.slot}: ${layer.letterChar || ""}`.trim() : "", layer?.soulLayer ].filter(Boolean).join(" · ") || "Qabalistic World" }; } function getPathListItemMeta(entry) { const path = getPathByNumber(entry.number); const fromName = getSephiraByNumber(path?.connects?.from)?.name || `Node ${path?.connects?.from || "?"}`; const toName = getSephiraByNumber(path?.connects?.to)?.name || `Node ${path?.connects?.to || "?"}`; const letterLabel = [path?.hebrewLetter?.char, path?.hebrewLetter?.transliteration].filter(Boolean).join(" ").trim(); return { title: `Path ${entry.number}${letterLabel ? ` · ${letterLabel}` : ""}`, meta: [ `${fromName} -> ${toName}`, String(path?.tarot?.card || "").trim() ].filter(Boolean).join(" · ") || "Path" }; } function syncBrowserListSelection(elements = getElements()) { if (!elements?.browserListEl) { return; } const selectedKey = getSelectedSephiraKey(); elements.browserListEl.querySelectorAll(".planet-list-item[data-node-key]").forEach((button) => { const isSelected = button.dataset.nodeKey === selectedKey; button.classList.toggle("is-selected", isSelected); button.setAttribute("aria-selected", isSelected ? "true" : "false"); }); } function renderBrowserList(elements = getElements()) { if (!elements?.browserListEl) { return; } const entries = getSephirotSequenceEntries(); elements.browserListEl.innerHTML = ""; entries.forEach((entry) => { const button = document.createElement("button"); const { title, meta } = getBrowserListItemMeta(entry); button.type = "button"; button.className = "planet-list-item"; button.setAttribute("role", "option"); button.dataset.nodeKey = entry.key; button.innerHTML = `
${title}
${meta}
`; elements.browserListEl.appendChild(button); }); if (elements.browserCountEl) { elements.browserCountEl.textContent = `${entries.length} sephiroth`; } syncBrowserListSelection(elements); } function syncWorldListSelection(elements = getElements()) { if (!elements?.worldsListEl) { return; } const selectedKey = getSelectedWorldKey(); elements.worldsListEl.querySelectorAll(".planet-list-item[data-world-key]").forEach((button) => { const isSelected = button.dataset.worldKey === selectedKey; button.classList.toggle("is-selected", isSelected); button.setAttribute("aria-selected", isSelected ? "true" : "false"); }); } function syncPathsListSelection(elements = getElements()) { if (!elements?.pathsListEl) { return; } const selectedKey = getSelectedPathKey(); elements.pathsListEl.querySelectorAll(".planet-list-item[data-path-key]").forEach((button) => { const isSelected = button.dataset.pathKey === selectedKey; button.classList.toggle("is-selected", isSelected); button.setAttribute("aria-selected", isSelected ? "true" : "false"); }); } function renderWorldsList(elements = getElements()) { if (!elements?.worldsListEl) { return; } const entries = getWorldSequenceEntries(); elements.worldsListEl.innerHTML = ""; entries.forEach((entry) => { const button = document.createElement("button"); const { title, meta } = getWorldListItemMeta(entry); button.type = "button"; button.className = "planet-list-item"; button.setAttribute("role", "option"); button.dataset.worldKey = entry.key; button.innerHTML = `
${title}
${meta}
`; elements.worldsListEl.appendChild(button); }); if (elements.worldsCountEl) { elements.worldsCountEl.textContent = `${entries.length} worlds`; } syncWorldListSelection(elements); } function renderPathsList(elements = getElements()) { if (!elements?.pathsListEl) { return; } const entries = getPathSequenceEntries(); elements.pathsListEl.innerHTML = ""; entries.forEach((entry) => { const button = document.createElement("button"); const { title, meta } = getPathListItemMeta(entry); button.type = "button"; button.className = "planet-list-item"; button.setAttribute("role", "option"); button.dataset.pathKey = entry.key; button.innerHTML = `
${title}
${meta}
`; elements.pathsListEl.appendChild(button); }); if (elements.pathsCountEl) { elements.pathsCountEl.textContent = `${entries.length} paths`; } syncPathsListSelection(elements); } function bindBrowserList(elements = getElements()) { if (!elements?.browserListEl || elements.browserListEl.dataset.bound) { return; } elements.browserListEl.addEventListener("click", (event) => { const target = event.target instanceof Element ? event.target.closest(".planet-list-item[data-node-key]") : null; if (!(target instanceof HTMLButtonElement)) { return; } const targetKey = String(target.dataset.nodeKey || "").trim(); if (!targetKey) { return; } selectNodeBySequenceKey(targetKey, getBrowserDetailElements(getElements())); }); elements.browserListEl.dataset.bound = "true"; } function bindWorldList(elements = getElements()) { if (!elements?.worldsListEl || elements.worldsListEl.dataset.bound) { return; } elements.worldsListEl.addEventListener("click", (event) => { const target = event.target instanceof Element ? event.target.closest(".planet-list-item[data-world-key]") : null; if (!(target instanceof HTMLButtonElement)) { return; } const targetKey = String(target.dataset.worldKey || "").trim(); if (!targetKey) { return; } selectNodeBySequenceKey(targetKey, getWorldDetailElements(getElements())); }); elements.worldsListEl.dataset.bound = "true"; } function bindPathsList(elements = getElements()) { if (!elements?.pathsListEl || elements.pathsListEl.dataset.bound) { return; } elements.pathsListEl.addEventListener("click", (event) => { const target = event.target instanceof Element ? event.target.closest(".planet-list-item[data-path-key]") : null; if (!(target instanceof HTMLButtonElement)) { return; } const targetKey = String(target.dataset.pathKey || "").trim(); if (!targetKey) { return; } selectNodeBySequenceKey(targetKey, getPathDetailElements(getElements())); }); elements.pathsListEl.dataset.bound = "true"; } function selectNodeBySequenceKey(targetKey, elements = getElements()) { if (!state.tree) { return false; } const [type, rawToken] = String(targetKey || "").split(":"); if (type === "sephira") { const seph = getSephiraByNumber(isDaathToken(rawToken) ? 0 : Number(rawToken)); if (!seph) { return false; } renderSephiraDetail(seph, state.tree, elements); return true; } if (type === "path") { const path = getPathByNumber(Number(rawToken)); if (!path) { return false; } renderPathDetail(path, state.tree, elements); return true; } if (type === "world") { const worldLayer = getWorldLayerByIndex(Number(rawToken)); if (!worldLayer) { return false; } renderWorldLayerDetail(worldLayer, Number(rawToken), state.tree, elements); return true; } return false; } function getDetailNavigator() { if (detailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") { return detailNavigator; } detailNavigator = window.TarotSequenceNav.createSequenceNavigator({ getElements, isActive: (elements) => Boolean(elements?.sectionEl && elements.sectionEl.hidden === false), getSequenceState: getNodeSequenceState, getPrevButton: (elements) => elements?.detailPrevEl, getNextButton: (elements) => elements?.detailNextEl, getPositionEl: (elements) => elements?.detailPositionEl, formatPositionText: ({ total, currentIndex }) => { if (total > 0 && currentIndex >= 0) { return `${currentIndex + 1} of ${total} nodes`; } return total > 0 ? `${total} nodes` : "No nodes"; }, selectTarget: (targetKey, elements) => selectNodeBySequenceKey(targetKey, elements) }); return detailNavigator; } function getBrowserDetailNavigator() { if (browserDetailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") { return browserDetailNavigator; } browserDetailNavigator = window.TarotSequenceNav.createSequenceNavigator({ getElements, isActive: (elements) => Boolean(elements?.browserSectionEl && elements.browserSectionEl.hidden === false), getSequenceState: getSephirotSequenceState, getPrevButton: (elements) => elements?.browserDetailPrevEl, getNextButton: (elements) => elements?.browserDetailNextEl, getPositionEl: (elements) => elements?.browserDetailPositionEl, formatPositionText: ({ total, currentIndex }) => { if (total > 0 && currentIndex >= 0) { return `${currentIndex + 1} of ${total} sephiroth`; } return total > 0 ? `${total} sephiroth` : "No sephiroth"; }, selectTarget: (targetKey) => selectNodeBySequenceKey(targetKey, getBrowserDetailElements(getElements())) }); return browserDetailNavigator; } function getPathsDetailNavigator() { if (pathsDetailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") { return pathsDetailNavigator; } pathsDetailNavigator = window.TarotSequenceNav.createSequenceNavigator({ getElements, isActive: (elements) => Boolean(elements?.pathsSectionEl && elements.pathsSectionEl.hidden === false), getSequenceState: getPathSequenceState, getPrevButton: (elements) => elements?.pathsDetailPrevEl, getNextButton: (elements) => elements?.pathsDetailNextEl, getPositionEl: (elements) => elements?.pathsDetailPositionEl, formatPositionText: ({ total, currentIndex }) => { if (total > 0 && currentIndex >= 0) { return `${currentIndex + 1} of ${total} paths`; } return total > 0 ? `${total} paths` : "No paths"; }, selectTarget: (targetKey) => selectNodeBySequenceKey(targetKey, getPathDetailElements(getElements())) }); return pathsDetailNavigator; } function getRoseDetailNavigator() { if (roseDetailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") { return roseDetailNavigator; } roseDetailNavigator = window.TarotSequenceNav.createSequenceNavigator({ getElements, isActive: (elements) => Boolean(elements?.crossSectionEl && elements.crossSectionEl.hidden === false), getSequenceState: getPathSequenceState, getPrevButton: (elements) => elements?.roseDetailPrevEl, getNextButton: (elements) => elements?.roseDetailNextEl, getPositionEl: (elements) => elements?.roseDetailPositionEl, formatPositionText: ({ total, currentIndex }) => { if (total > 0 && currentIndex >= 0) { return `${currentIndex + 1} of ${total} paths`; } return total > 0 ? `${total} paths` : "No paths"; }, selectTarget: (targetKey) => selectNodeBySequenceKey(targetKey, getRoseDetailElements(getElements())) }); return roseDetailNavigator; } function getWorldDetailNavigator() { if (worldsDetailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") { return worldsDetailNavigator; } worldsDetailNavigator = window.TarotSequenceNav.createSequenceNavigator({ getElements, isActive: (elements) => Boolean(elements?.worldsSectionEl && elements.worldsSectionEl.hidden === false), getSequenceState: getWorldSequenceState, getPrevButton: (elements) => elements?.worldsDetailPrevEl, getNextButton: (elements) => elements?.worldsDetailNextEl, getPositionEl: (elements) => elements?.worldsDetailPositionEl, formatPositionText: ({ total, currentIndex }) => { if (total > 0 && currentIndex >= 0) { return `${currentIndex + 1} of ${total} worlds`; } return total > 0 ? `${total} worlds` : "No worlds"; }, selectTarget: (targetKey) => selectNodeBySequenceKey(targetKey, getWorldDetailElements(getElements())) }); return worldsDetailNavigator; } function syncDetailNavigation(elements = getElements()) { getDetailNavigator()?.sync(elements); } function syncBrowserDetailNavigation(elements = getElements()) { getBrowserDetailNavigator()?.sync(elements); } function syncPathsDetailNavigation(elements = getElements()) { getPathsDetailNavigator()?.sync(elements); } function syncRoseDetailNavigation(elements = getElements()) { getRoseDetailNavigator()?.sync(elements); } function syncWorldDetailNavigation(elements = getElements()) { getWorldDetailNavigator()?.sync(elements); } function bindDetailNavigation(elements = getElements()) { getDetailNavigator()?.bind(elements); } function bindBrowserDetailNavigation(elements = getElements()) { getBrowserDetailNavigator()?.bind(elements); } function bindPathsDetailNavigation(elements = getElements()) { getPathsDetailNavigator()?.bind(elements); } function bindRoseDetailNavigation(elements = getElements()) { getRoseDetailNavigator()?.bind(elements); } function bindWorldDetailNavigation(elements = getElements()) { getWorldDetailNavigator()?.bind(elements); } function normalizeText(value) { return String(value || "").trim().toLowerCase(); } function normalizeLetterToken(value) { return String(value || "") .trim() .toLowerCase() .replace(/[^a-z]/g, ""); } function buildHebrewLetterLookup(magickDataset) { const letters = magickDataset?.grouped?.hebrewLetters; const lookup = {}; if (!letters || typeof letters !== "object") { return lookup; } Object.entries(letters).forEach(([letterId, entry]) => { const entryId = String(entry?.id || letterId || ""); const idToken = normalizeLetterToken(letterId); const canonicalIdToken = HEBREW_LETTER_ALIASES[idToken] || idToken; if (canonicalIdToken && !lookup[canonicalIdToken]) { lookup[canonicalIdToken] = entryId; } const nameToken = normalizeLetterToken(entry?.letter?.name); const canonicalNameToken = HEBREW_LETTER_ALIASES[nameToken] || nameToken; if (canonicalNameToken && !lookup[canonicalNameToken]) { lookup[canonicalNameToken] = entryId; } }); return lookup; } function resolveHebrewLetterId(value) { const token = normalizeLetterToken(value); if (!token) return null; const canonical = HEBREW_LETTER_ALIASES[token] || token; return state.hebrewLetterIdByToken[canonical] || state.hebrewLetterIdByToken[token] || null; } function resolvePlanetId(value) { const text = normalizeText(value); if (!text) return null; for (const [key, planetId] of Object.entries(PLANET_NAME_TO_ID)) { if (text === key || text.includes(key)) { return planetId; } } return null; } function resolveZodiacId(value) { const text = normalizeText(value); if (!text) return null; for (const [name, zodiacId] of Object.entries(ZODIAC_NAME_TO_ID)) { if (text === name || text.includes(name)) { return zodiacId; } } return null; } function findPathByHebrewToken(tree, hebrewToken) { const canonicalToken = HEBREW_LETTER_ALIASES[normalizeLetterToken(hebrewToken)] || normalizeLetterToken(hebrewToken); if (!canonicalToken) { return null; } const paths = Array.isArray(tree?.paths) ? tree.paths : []; return paths.find((path) => { const letterToken = normalizeLetterToken(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char); const canonicalLetterToken = HEBREW_LETTER_ALIASES[letterToken] || letterToken; return canonicalLetterToken === canonicalToken; }) || null; } function getDetailRenderContext(tree, elements, extra = {}) { return { tree, elements, godsData: state.godsData, fourWorldLayers: state.fourWorldLayers, resolvePlanetId, resolveZodiacId, resolveHebrewLetterId, findPathByHebrewToken, ...extra }; } function clearHighlights() { document.querySelectorAll(".kab-node, .kab-node-glow") .forEach(el => el.classList.remove("kab-node-active")); document.querySelectorAll(".kab-path-hit, .kab-path-line, .kab-path-lbl, .kab-path-tarot, .kab-rose-petal") .forEach(el => el.classList.remove("kab-path-active")); } function renderSephiraDetailIntoElements(seph, tree, elements, options = {}) { if (typeof kabbalahDetailUi.renderSephiraDetail === "function") { kabbalahDetailUi.renderSephiraDetail(getDetailRenderContext(tree, elements, { seph, onPathSelect: typeof options.onPathSelect === "function" ? options.onPathSelect : null })); } } function renderPathDetailIntoElements(path, tree, elements) { if (typeof kabbalahDetailUi.renderPathDetail === "function") { kabbalahDetailUi.renderPathDetail(getDetailRenderContext(tree, elements, { path, activeHebrewToken: normalizeLetterToken(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char || "") })); } } function renderWorldLayerDetailIntoElements(worldLayer, tree, elements, worldIndex) { if (typeof kabbalahDetailUi.renderWorldLayerDetail === "function") { kabbalahDetailUi.renderWorldLayerDetail(getDetailRenderContext(tree, elements, { worldLayer, worldIndex })); } } function renderSephiraDetail(seph, tree, elements) { state.selectedSephiraNumber = Number(seph?.number); if (buildSephiraKey(seph?.number) !== "sephira:daath") { state.activeNodeKey = buildSephiraKey(seph?.number); } clearHighlights(); document.querySelectorAll(`.kab-node[data-sephira="${seph.number}"], .kab-node-glow[data-sephira="${seph.number}"]`) .forEach(el => el.classList.add("kab-node-active")); const allElements = getElements(); const treeElements = getTreeDetailElements(allElements); const browserElements = getBrowserDetailElements(allElements); if (treeElements?.detailBodyEl) { renderSephiraDetailIntoElements(seph, tree, treeElements, { onPathSelect: (path) => renderPathDetail(path, tree, treeElements) }); } if (browserElements?.detailBodyEl) { renderSephiraDetailIntoElements(seph, tree, browserElements, { onPathSelect: (path) => { document.dispatchEvent(new CustomEvent("nav:kabbalah-path", { detail: { pathNo: Number(path?.pathNumber) } })); } }); } syncDetailNavigation(); syncBrowserDetailNavigation(); syncBrowserListSelection(); } function renderPathDetail(path, tree, elements) { state.selectedPathNumber = Number(path?.pathNumber); state.activeNodeKey = buildPathKey(path?.pathNumber); clearHighlights(); document.querySelectorAll(`[data-path="${path.pathNumber}"]`) .forEach(el => el.classList.add("kab-path-active")); const allElements = getElements(); const treeElements = getTreeDetailElements(allElements); const pathElements = getPathDetailElements(allElements); const roseElements = getRoseDetailElements(allElements); if (treeElements?.detailBodyEl) { renderPathDetailIntoElements(path, tree, treeElements); } if (pathElements?.detailBodyEl) { renderPathDetailIntoElements(path, tree, pathElements); } if (roseElements?.detailBodyEl) { renderPathDetailIntoElements(path, tree, roseElements); } syncDetailNavigation(); syncPathsDetailNavigation(); syncRoseDetailNavigation(); syncPathsListSelection(); syncBrowserListSelection(); } function renderWorldLayerDetail(worldLayer, worldIndex, tree, elements) { state.selectedWorldLayerIndex = Number(worldIndex); const worldElements = getWorldDetailElements(getElements()); if (worldElements?.detailBodyEl) { renderWorldLayerDetailIntoElements(worldLayer, tree, worldElements, worldIndex); } syncWorldDetailNavigation(); syncWorldListSelection(); } function renderRoseLandingIntro(roseElements) { if (typeof kabbalahDetailUi.renderRoseLandingIntro === "function") { kabbalahDetailUi.renderRoseLandingIntro(roseElements); } } function renderBrowserCurrentSelection(elements) { if (!state.tree) { return; } const browserElements = getBrowserDetailElements(elements); if (!browserElements?.detailBodyEl) { return; } const selectedSephira = getSephiraByNumber(state.selectedSephiraNumber); if (selectedSephira) { renderSephiraDetail(selectedSephira, state.tree, browserElements); return; } const fallbackSephira = getSephiraByNumber(getSephirotSequenceEntries()[0]?.number); if (fallbackSephira) { renderSephiraDetail(fallbackSephira, state.tree, browserElements); } } function renderPathsCurrentSelection(elements) { if (!state.tree) { return; } const pathElements = getPathDetailElements(elements); if (!pathElements?.detailBodyEl) { return; } if (hasFiniteSelectionNumber(state.selectedPathNumber)) { const selectedPath = getPathByNumber(state.selectedPathNumber); if (selectedPath) { renderPathDetail(selectedPath, state.tree, pathElements); return; } } const fallbackPath = getPathByNumber(getPathSequenceEntries()[0]?.number); if (fallbackPath) { renderPathDetail(fallbackPath, state.tree, pathElements); } } function renderWorldCurrentSelection(elements) { const worldElements = getWorldDetailElements(elements); if (!worldElements?.detailBodyEl) { return; } const selectedWorld = getWorldLayerByIndex(state.selectedWorldLayerIndex); if (selectedWorld) { renderWorldLayerDetail(selectedWorld, state.selectedWorldLayerIndex, state.tree, worldElements); return; } const fallbackWorld = getWorldLayerByIndex(0); if (fallbackWorld) { renderWorldLayerDetail(fallbackWorld, 0, state.tree, worldElements); } } function getViewRenderContext(elements) { return { state, tree: state.tree, elements, getRoseDetailElements, renderSephiraDetail, renderPathDetail, NS, R, NODE_POS, SEPH_FILL, DARK_TEXT, DAAT, PATH_MARKER_SCALE, PATH_LABEL_RADIUS, PATH_LABEL_FONT_SIZE, PATH_TAROT_WIDTH, PATH_TAROT_HEIGHT, PATH_LABEL_OFFSET_WITH_TAROT, PATH_TAROT_OFFSET_WITH_LABEL, PATH_TAROT_OFFSET_NO_LABEL }; } function renderRoseCurrentSelection(elements) { if (!state.tree) { return; } const roseElements = getRoseDetailElements(elements); if (!roseElements?.detailBodyEl) { return; } if (hasFiniteSelectionNumber(state.selectedPathNumber)) { const selectedPath = getPathByNumber(state.selectedPathNumber); if (selectedPath) { renderPathDetail(selectedPath, state.tree, roseElements); return; } } renderRoseLandingIntro(roseElements); syncRoseDetailNavigation(elements); } function renderRoseCross(elements) { kabbalahViewsUi.renderRoseCross(getViewRenderContext(elements)); } function renderTree(elements) { kabbalahViewsUi.renderTree(getViewRenderContext(elements)); } function isExportFormatSupported(format) { const exportFormat = TREE_EXPORT_FORMATS[format]; if (!exportFormat) { return false; } if (format === "webp" && typeof webpExportSupported === "boolean") { return webpExportSupported; } const probeCanvas = document.createElement("canvas"); const dataUrl = probeCanvas.toDataURL(exportFormat.mimeType); const isSupported = dataUrl.startsWith(`data:${exportFormat.mimeType}`); if (format === "webp") { webpExportSupported = isSupported; } return isSupported; } function syncExportControls(elements) { if (!(elements?.treeExportWebpEl instanceof HTMLButtonElement)) { return; } const supportsWebp = isExportFormatSupported("webp"); elements.treeExportWebpEl.hidden = !supportsWebp; elements.treeExportWebpEl.disabled = Boolean(state.exportInProgress) || !supportsWebp; elements.treeExportWebpEl.textContent = state.exportInProgress && state.exportFormat === "webp" ? "Exporting..." : "Export WebP"; if (supportsWebp) { elements.treeExportWebpEl.title = "Download the current Tree of Life view as a WebP image."; } } function copyComputedStyles(sourceEl, targetEl) { if (!(sourceEl instanceof Element) || !(targetEl instanceof Element)) { return; } const computedStyle = window.getComputedStyle(sourceEl); Array.from(computedStyle).forEach((propertyName) => { targetEl.style.setProperty( propertyName, computedStyle.getPropertyValue(propertyName), computedStyle.getPropertyPriority(propertyName) ); }); targetEl.style.setProperty("animation", "none"); targetEl.style.setProperty("transition", "none"); } function inlineSvgStyles(sourceNode, targetNode) { if (!(sourceNode instanceof Element) || !(targetNode instanceof Element)) { return; } copyComputedStyles(sourceNode, targetNode); const sourceChildren = Array.from(sourceNode.children); const targetChildren = Array.from(targetNode.children); const childCount = Math.min(sourceChildren.length, targetChildren.length); for (let index = 0; index < childCount; index += 1) { inlineSvgStyles(sourceChildren[index], targetChildren[index]); } } function absolutizeSvgImageLinks(svgEl) { if (!(svgEl instanceof SVGSVGElement)) { return; } svgEl.querySelectorAll("image").forEach((imageEl) => { const href = imageEl.getAttribute("href") || imageEl.getAttributeNS("http://www.w3.org/1999/xlink", "href"); if (!href) { return; } const absoluteHref = new URL(href, document.baseURI).href; imageEl.setAttribute("href", absoluteHref); imageEl.setAttributeNS("http://www.w3.org/1999/xlink", "href", absoluteHref); }); } function prepareSvgMarkupForExport(svgEl) { if (!(svgEl instanceof SVGSVGElement)) { throw new Error("Tree view is not ready to export yet."); } const bounds = svgEl.getBoundingClientRect(); const viewBox = svgEl.viewBox?.baseVal || null; const width = Math.max( 240, Math.round(bounds.width), Number.isFinite(viewBox?.width) ? Math.round(viewBox.width) : 0 ); const height = Math.max( 470, Math.round(bounds.height), Number.isFinite(viewBox?.height) ? Math.round(viewBox.height) : 0 ); const clone = svgEl.cloneNode(true); if (!(clone instanceof SVGSVGElement)) { throw new Error("Tree export could not clone the current SVG view."); } clone.setAttribute("xmlns", "http://www.w3.org/2000/svg"); clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); clone.setAttribute("width", String(width)); clone.setAttribute("height", String(height)); clone.setAttribute("preserveAspectRatio", "xMidYMid meet"); inlineSvgStyles(svgEl, clone); absolutizeSvgImageLinks(clone); const backgroundRect = document.createElementNS(NS, "rect"); backgroundRect.setAttribute("x", "0"); backgroundRect.setAttribute("y", "0"); backgroundRect.setAttribute("width", "100%"); backgroundRect.setAttribute("height", "100%"); backgroundRect.setAttribute("fill", TREE_EXPORT_BACKGROUND); backgroundRect.setAttribute("pointer-events", "none"); clone.insertBefore(backgroundRect, clone.firstChild); return { width, height, markup: new XMLSerializer().serializeToString(clone) }; } function loadSvgImage(markup) { return new Promise((resolve, reject) => { const svgBlob = new Blob([markup], { type: "image/svg+xml;charset=utf-8" }); const svgUrl = URL.createObjectURL(svgBlob); const image = new Image(); image.decoding = "async"; image.onload = () => { URL.revokeObjectURL(svgUrl); resolve(image); }; image.onerror = () => { URL.revokeObjectURL(svgUrl); reject(new Error("Tree export renderer could not load the current SVG view.")); }; image.src = svgUrl; }); } function canvasToBlobByFormat(canvas, format) { const exportFormat = TREE_EXPORT_FORMATS[format]; if (!exportFormat) { return Promise.reject(new Error("Unsupported export format.")); } return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) { resolve(blob); return; } reject(new Error("Canvas export failed.")); }, exportFormat.mimeType, exportFormat.quality); }); } async function exportTreeView(format = "webp") { const exportFormat = TREE_EXPORT_FORMATS[format]; if (!exportFormat || state.exportInProgress) { return; } const elements = getElements(); const svgEl = elements.treeContainerEl?.querySelector("svg.kab-svg"); if (!(svgEl instanceof SVGSVGElement)) { window.alert("Tree view is not ready to export yet."); return; } state.exportInProgress = true; state.exportFormat = format; syncExportControls(elements); try { const { width, height, markup } = prepareSvgMarkupForExport(svgEl); const image = await loadSvgImage(markup); const scale = Math.max(2, Math.min(4, Number(window.devicePixelRatio) || 1)); const canvas = document.createElement("canvas"); canvas.width = Math.max(1, Math.ceil(width * scale)); canvas.height = Math.max(1, Math.ceil(height * scale)); const context = canvas.getContext("2d"); if (!context) { throw new Error("Canvas context is unavailable."); } context.scale(scale, scale); context.imageSmoothingEnabled = true; context.imageSmoothingQuality = "high"; context.fillStyle = TREE_EXPORT_BACKGROUND; context.fillRect(0, 0, width, height); context.drawImage(image, 0, 0, width, height); const blob = await canvasToBlobByFormat(canvas, format); const blobUrl = URL.createObjectURL(blob); const downloadLink = document.createElement("a"); const stamp = new Date().toISOString().slice(0, 10); downloadLink.href = blobUrl; downloadLink.download = `tree-of-life-${stamp}.${exportFormat.extension}`; document.body.appendChild(downloadLink); downloadLink.click(); downloadLink.remove(); setTimeout(() => URL.revokeObjectURL(blobUrl), 1000); } catch (error) { window.alert(error instanceof Error ? error.message : "Unable to export the current Tree of Life view."); } finally { state.exportInProgress = false; state.exportFormat = ""; syncExportControls(getElements()); } } function renderCurrentSelection(elements) { if (!state.tree) { return; } const activeNodeKey = String(state.activeNodeKey || "").trim(); if (activeNodeKey.startsWith("path:")) { const selectedPath = getPathByNumber(Number(activeNodeKey.split(":")[1])); if (selectedPath) { renderPathDetail(selectedPath, state.tree, elements); return; } } if (activeNodeKey.startsWith("sephira:") && !activeNodeKey.endsWith(":daath")) { const selectedSephira = getSephiraByNumber(Number(activeNodeKey.split(":")[1])); if (selectedSephira) { renderSephiraDetail(selectedSephira, state.tree, elements); return; } } renderSephiraDetail(state.tree.sephiroth[0], state.tree, elements); } function renderVisibleKabbalahViews(elements = getElements()) { renderBrowserList(elements); renderWorldsList(elements); renderPathsList(elements); if (elements.browserSectionEl && elements.browserSectionEl.hidden === false) { renderBrowserCurrentSelection(elements); } else { syncBrowserListSelection(elements); syncBrowserDetailNavigation(elements); } if (elements.worldsSectionEl && elements.worldsSectionEl.hidden === false) { renderWorldCurrentSelection(elements); } else { syncWorldListSelection(elements); syncWorldDetailNavigation(elements); } if (elements.pathsSectionEl && elements.pathsSectionEl.hidden === false) { renderPathsCurrentSelection(elements); } else { syncPathsListSelection(elements); syncPathsDetailNavigation(elements); } if (elements.crossSectionEl && elements.crossSectionEl.hidden === false) { renderRoseCross(elements); renderRoseCurrentSelection(elements); } else { syncRoseDetailNavigation(elements); } if (elements.sectionEl && elements.sectionEl.hidden === false) { renderTree(elements); renderCurrentSelection(elements); } else { syncDetailNavigation(elements); } } // ─── initialise section ────────────────────────────────────────────────────── function init(magickDataset, elements) { const tree = magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]; if (!tree) { if (elements.detailNameEl) { elements.detailNameEl.textContent = "Kabbalah data unavailable"; elements.detailSubEl.textContent = "Could not load kabbalah-tree.json"; } return; } state.tree = tree; state.godsData = magickDataset?.grouped?.["gods"]?.byPath || {}; state.hebrewLetterIdByToken = buildHebrewLetterLookup(magickDataset); state.fourWorldLayers = buildFourWorldLayersFromDataset(magickDataset); if (!hasFiniteSelectionNumber(state.selectedWorldLayerIndex) || Number(state.selectedWorldLayerIndex) < 0 || Number(state.selectedWorldLayerIndex) >= state.fourWorldLayers.length) { state.selectedWorldLayerIndex = 0; } if (!hasFiniteSelectionNumber(state.selectedSephiraNumber)) { state.selectedSephiraNumber = Number(state.tree.sephiroth[0]?.number || 1); } if (!String(state.activeNodeKey || "").trim()) { state.activeNodeKey = buildSephiraKey(state.selectedSephiraNumber); } const bindPathDisplayToggle = (toggleEl, stateKey) => { if (!toggleEl) { return; } toggleEl.checked = Boolean(state[stateKey]); if (toggleEl.dataset.bound) { return; } toggleEl.addEventListener("change", () => { state[stateKey] = Boolean(toggleEl.checked); renderTree(elements); renderCurrentSelection(elements); }); toggleEl.dataset.bound = "true"; }; bindPathDisplayToggle(elements.pathLetterToggleEl, "showPathLetters"); bindPathDisplayToggle(elements.pathNumberToggleEl, "showPathNumbers"); bindPathDisplayToggle(elements.pathTarotToggleEl, "showPathTarotCards"); bindDetailNavigation(elements); bindBrowserDetailNavigation(elements); bindPathsDetailNavigation(elements); bindWorldDetailNavigation(elements); bindRoseDetailNavigation(elements); bindBrowserList(elements); bindWorldList(elements); bindPathsList(elements); syncExportControls(elements); if (elements.treeExportWebpEl && !elements.treeExportWebpEl.dataset.bound) { elements.treeExportWebpEl.addEventListener("click", () => { void exportTreeView("webp"); }); elements.treeExportWebpEl.dataset.bound = "true"; } renderVisibleKabbalahViews(elements); } function selectPathByNumber(pathNumber) { if (!state.initialized || !state.tree) return; const el = getElements(); const path = getPathByNumber(pathNumber); if (path) renderPathDetail(path, state.tree, el); } function selectSephiraByNumber(n) { if (!state.initialized || !state.tree) return; const el = getElements(); const seph = getSephiraByNumber(n); if (seph) renderSephiraDetail(seph, state.tree, el); } // select sephirah (1-10) or path (11+) by a single number function selectNode(n) { if (isDaathToken(n) || Number(n) === 0) selectSephiraByNumber(0); else if (n >= 1 && n <= 10) selectSephiraByNumber(n); else selectPathByNumber(n); } // ─── public API ──────────────────────────────────────────────────────── function ensureKabbalahSection(magickDataset) { const elements = getElements(); if (state.initialized) { renderVisibleKabbalahViews(elements); return; } state.initialized = true; init(magickDataset, elements); } window.KabbalahSectionUi = { ensureKabbalahSection, selectPathByNumber, selectSephiraByNumber, selectNode }; })();