(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, selectedSephiraNumber: null, selectedPathNumber: null, exportInProgress: false, exportFormat: "" }; const TREE_EXPORT_FORMATS = { webp: { mimeType: "image/webp", extension: "webp", quality: 0.98 } }; const TREE_EXPORT_BACKGROUND = "#02030a"; 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 { treeContainerEl: document.getElementById("kab-tree-container"), detailNameEl: document.getElementById("kab-detail-name"), detailSubEl: document.getElementById("kab-detail-sub"), detailBodyEl: document.getElementById("kab-detail-body"), 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"), }; } function getRoseDetailElements(elements) { if (!elements) { return null; } return { detailNameEl: elements.roseDetailNameEl, detailSubEl: elements.roseDetailSubEl, detailBodyEl: elements.roseDetailBodyEl }; } 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 renderSephiraDetail(seph, tree, elements) { state.selectedSephiraNumber = Number(seph?.number); state.selectedPathNumber = null; clearHighlights(); document.querySelectorAll(`.kab-node[data-sephira="${seph.number}"], .kab-node-glow[data-sephira="${seph.number}"]`) .forEach(el => el.classList.add("kab-node-active")); if (typeof kabbalahDetailUi.renderSephiraDetail === "function") { kabbalahDetailUi.renderSephiraDetail(getDetailRenderContext(tree, elements, { seph, onPathSelect: (path) => renderPathDetail(path, tree, elements) })); } } function renderPathDetail(path, tree, elements) { state.selectedPathNumber = Number(path?.pathNumber); state.selectedSephiraNumber = null; clearHighlights(); document.querySelectorAll(`[data-path="${path.pathNumber}"]`) .forEach(el => el.classList.add("kab-path-active")); if (typeof kabbalahDetailUi.renderPathDetail === "function") { kabbalahDetailUi.renderPathDetail(getDetailRenderContext(tree, elements, { path, activeHebrewToken: normalizeLetterToken(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char || "") })); } } function renderRoseLandingIntro(roseElements) { if (typeof kabbalahDetailUi.renderRoseLandingIntro === "function") { kabbalahDetailUi.renderRoseLandingIntro(roseElements); } } 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 (Number.isFinite(Number(state.selectedPathNumber))) { const selectedPath = state.tree.paths.find((entry) => entry.pathNumber === Number(state.selectedPathNumber)); if (selectedPath) { renderPathDetail(selectedPath, state.tree, roseElements); return; } } renderRoseLandingIntro(roseElements); } 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; } if (Number.isFinite(Number(state.selectedPathNumber))) { const selectedPath = state.tree.paths.find((entry) => entry.pathNumber === Number(state.selectedPathNumber)); if (selectedPath) { renderPathDetail(selectedPath, state.tree, elements); return; } } if (Number.isFinite(Number(state.selectedSephiraNumber))) { const selectedSephira = state.tree.sephiroth.find((entry) => entry.number === Number(state.selectedSephiraNumber)); if (selectedSephira) { renderSephiraDetail(selectedSephira, state.tree, elements); return; } } renderSephiraDetail(state.tree.sephiroth[0], state.tree, 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); 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"); syncExportControls(elements); if (elements.treeExportWebpEl && !elements.treeExportWebpEl.dataset.bound) { elements.treeExportWebpEl.addEventListener("click", () => { void exportTreeView("webp"); }); elements.treeExportWebpEl.dataset.bound = "true"; } renderTree(elements); renderCurrentSelection(elements); renderRoseCross(elements); renderRoseCurrentSelection(elements); } function selectPathByNumber(pathNumber) { if (!state.initialized || !state.tree) return; const el = getElements(); const path = state.tree.paths.find(p => p.pathNumber === pathNumber); if (path) renderPathDetail(path, state.tree, el); } function selectSephiraByNumber(n) { if (!state.initialized || !state.tree) return; const el = getElements(); const seph = state.tree.sephiroth.find(s => s.number === n); if (seph) renderSephiraDetail(seph, state.tree, el); } // select sephirah (1-10) or path (11+) by a single number function selectNode(n) { if (n >= 1 && n <= 10) selectSephiraByNumber(n); else selectPathByNumber(n); } // ─── public API ──────────────────────────────────────────────────────── function ensureKabbalahSection(magickDataset) { if (state.initialized) return; state.initialized = true; const elements = getElements(); init(magickDataset, elements); } window.KabbalahSectionUi = { ensureKabbalahSection, selectPathByNumber, selectSephiraByNumber, selectNode }; })();