Files
TaroTime/app/ui-kabbalah.js

824 lines
27 KiB
JavaScript
Raw Normal View History

2026-03-07 01:09:00 -08:00
(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,
2026-03-12 21:01:32 -07:00
selectedPathNumber: null,
exportInProgress: false,
exportFormat: ""
2026-03-07 01:09:00 -08:00
};
2026-03-12 21:01:32 -07:00
const TREE_EXPORT_FORMATS = {
webp: {
mimeType: "image/webp",
extension: "webp",
quality: 0.98
}
};
const TREE_EXPORT_BACKGROUND = "#02030a";
2026-03-07 01:09:00 -08:00
2026-03-07 05:17:50 -08:00
const kabbalahDetailUi = window.KabbalahDetailUi || {};
2026-03-07 13:38:13 -08:00
const kabbalahViewsUi = window.KabbalahViewsUi || {};
2026-03-12 21:01:32 -07:00
let webpExportSupported = null;
2026-03-07 13:38:13 -08:00
if (
typeof kabbalahViewsUi.renderTree !== "function"
|| typeof kabbalahViewsUi.renderRoseCross !== "function"
) {
throw new Error("KabbalahViewsUi module must load before ui-kabbalah.js");
}
2026-03-07 05:17:50 -08:00
2026-03-07 01:09:00 -08:00
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 (Gods 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 (Gods 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 (Gods 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 (Gods 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"),
2026-03-12 21:01:32 -07:00
treeExportWebpEl: document.getElementById("kab-tree-export-webp"),
2026-03-07 05:17:50 -08:00
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
2026-03-07 01:09:00 -08:00
};
}
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;
}
2026-03-07 05:17:50 -08:00
function getDetailRenderContext(tree, elements, extra = {}) {
return {
tree,
elements,
godsData: state.godsData,
fourWorldLayers: state.fourWorldLayers,
resolvePlanetId,
resolveZodiacId,
resolveHebrewLetterId,
findPathByHebrewToken,
...extra
};
2026-03-07 01:09:00 -08:00
}
function clearHighlights() {
document.querySelectorAll(".kab-node, .kab-node-glow")
.forEach(el => el.classList.remove("kab-node-active"));
2026-03-07 05:17:50 -08:00
document.querySelectorAll(".kab-path-hit, .kab-path-line, .kab-path-lbl, .kab-path-tarot, .kab-rose-petal")
2026-03-07 01:09:00 -08:00
.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"));
2026-03-07 05:17:50 -08:00
if (typeof kabbalahDetailUi.renderSephiraDetail === "function") {
kabbalahDetailUi.renderSephiraDetail(getDetailRenderContext(tree, elements, {
seph,
onPathSelect: (path) => renderPathDetail(path, tree, elements)
}));
2026-03-07 01:09:00 -08:00
}
}
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"));
2026-03-07 05:17:50 -08:00
if (typeof kabbalahDetailUi.renderPathDetail === "function") {
kabbalahDetailUi.renderPathDetail(getDetailRenderContext(tree, elements, {
path,
activeHebrewToken: normalizeLetterToken(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char || "")
}));
2026-03-07 01:09:00 -08:00
}
}
2026-03-07 05:17:50 -08:00
function renderRoseLandingIntro(roseElements) {
if (typeof kabbalahDetailUi.renderRoseLandingIntro === "function") {
kabbalahDetailUi.renderRoseLandingIntro(roseElements);
}
}
2026-03-07 13:38:13 -08:00
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
};
2026-03-07 05:17:50 -08:00
}
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);
}
2026-03-07 13:38:13 -08:00
function renderRoseCross(elements) {
kabbalahViewsUi.renderRoseCross(getViewRenderContext(elements));
}
2026-03-07 01:09:00 -08:00
2026-03-07 13:38:13 -08:00
function renderTree(elements) {
kabbalahViewsUi.renderTree(getViewRenderContext(elements));
2026-03-07 01:09:00 -08:00
}
2026-03-12 21:01:32 -07:00
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());
}
}
2026-03-07 01:09:00 -08:00
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");
2026-03-12 21:01:32 -07:00
syncExportControls(elements);
if (elements.treeExportWebpEl && !elements.treeExportWebpEl.dataset.bound) {
elements.treeExportWebpEl.addEventListener("click", () => {
void exportTreeView("webp");
});
elements.treeExportWebpEl.dataset.bound = "true";
}
2026-03-07 01:09:00 -08:00
renderTree(elements);
renderCurrentSelection(elements);
2026-03-07 05:17:50 -08:00
renderRoseCross(elements);
renderRoseCurrentSelection(elements);
2026-03-07 01:09:00 -08:00
}
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 };
})();