Files
TaroTime/app/ui-kabbalah.js
2026-03-07 05:17:50 -08:00

907 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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
};
const kabbalahDetailUi = window.KabbalahDetailUi || {};
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"),
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 resolvePathTarotImage(path) {
const cardName = String(path?.tarot?.card || "").trim();
if (!cardName || typeof window.TarotCardImages?.resolveTarotCardImage !== "function") {
return null;
}
return window.TarotCardImages.resolveTarotCardImage(cardName);
}
function getSvgImageHref(imageEl) {
if (!(imageEl instanceof SVGElement)) {
return "";
}
return String(
imageEl.getAttribute("href")
|| imageEl.getAttributeNS("http://www.w3.org/1999/xlink", "href")
|| ""
).trim();
}
function openTarotLightboxForPath(path, fallbackSrc = "") {
const openLightbox = window.TarotUiLightbox?.open;
if (typeof openLightbox !== "function") {
return false;
}
const cardName = String(path?.tarot?.card || "").trim();
const src = String(fallbackSrc || resolvePathTarotImage(path) || "").trim();
if (!src) {
return false;
}
const fallbackLabel = Number.isFinite(Number(path?.pathNumber))
? `Path ${path.pathNumber} tarot card`
: "Path tarot card";
openLightbox(src, cardName || fallbackLabel);
return true;
}
function getPathLabel(path) {
const glyph = String(path?.hebrewLetter?.char || "").trim();
const pathNumber = Number(path?.pathNumber);
const parts = [];
if (state.showPathLetters && glyph) {
parts.push(glyph);
}
if (state.showPathNumbers && Number.isFinite(pathNumber)) {
parts.push(String(pathNumber));
}
return parts.join(" ");
}
// ─── SVG element factory ────────────────────────────────────────────────────
function svgEl(tag, attrs, text) {
const el = document.createElementNS(NS, tag);
for (const [k, v] of Object.entries(attrs || {})) {
el.setAttribute(k, String(v));
}
if (text != null) el.textContent = text;
return el;
}
// Rosicrucian cross SVG construction lives in app/ui-rosicrucian-cross.js.
// ─── build the full SVG tree ─────────────────────────────────────────────────
function buildTreeSVG(tree) {
const svg = svgEl("svg", {
viewBox: "0 0 240 470",
width: "100%",
role: "img",
"aria-label": "Kabbalah Tree of Life diagram",
class: "kab-svg",
});
// Subtle pillar background tracks
svg.appendChild(svgEl("rect", {
x: 113, y: 30, width: 14, height: 420,
rx: 7, fill: "#ffffff07", "pointer-events": "none",
}));
svg.appendChild(svgEl("rect", {
x: 33, y: 88, width: 14, height: 255,
rx: 7, fill: "#ff220010", "pointer-events": "none",
}));
svg.appendChild(svgEl("rect", {
x: 193, y: 88, width: 14, height: 255,
rx: 7, fill: "#2244ff10", "pointer-events": "none",
}));
// Pillar labels
[
{ x: 198, y: 73, text: "Mercy", anchor: "middle" },
{ x: 120, y: 17, text: "Balance", anchor: "middle" },
{ x: 42, y: 73, text: "Severity", anchor: "middle" },
].forEach(({ x, y, text, anchor }) => {
svg.appendChild(svgEl("text", {
x, y, "text-anchor": anchor, "dominant-baseline": "auto",
fill: "#42425a", "font-size": "6", "pointer-events": "none",
}, text));
});
// ── path lines (drawn before sephiroth so nodes sit on top) ──────────────
tree.paths.forEach(path => {
const [x1, y1] = NODE_POS[path.connects.from];
const [x2, y2] = NODE_POS[path.connects.to];
const mx = (x1 + x2) / 2;
const my = (y1 + y2) / 2;
const tarotImage = state.showPathTarotCards ? resolvePathTarotImage(path) : null;
const hasTarotImage = Boolean(tarotImage);
const pathLabel = getPathLabel(path);
const hasLabel = Boolean(pathLabel);
const labelY = hasTarotImage && hasLabel ? my - PATH_LABEL_OFFSET_WITH_TAROT : my;
// Visual line (thin)
svg.appendChild(svgEl("line", {
x1, y1, x2, y2,
class: "kab-path-line",
"data-path": path.pathNumber,
stroke: "#3c3c5c",
"stroke-width": "1.5",
"pointer-events": "none",
}));
// Invisible wide hit area for easy clicking
svg.appendChild(svgEl("line", {
x1, y1, x2, y2,
class: "kab-path-hit",
"data-path": path.pathNumber,
stroke: "transparent",
"stroke-width": String(12 * PATH_MARKER_SCALE),
role: "button",
tabindex: "0",
"aria-label": `Path ${path.pathNumber}: ${path.hebrewLetter?.transliteration || ""}${path.tarot?.card || ""}`,
style: "cursor:pointer",
}));
if (hasLabel) {
// Background disc for legibility behind path label
svg.appendChild(svgEl("circle", {
cx: mx, cy: labelY, r: PATH_LABEL_RADIUS.toFixed(2),
fill: "#0d0d1c", opacity: "0.82",
"pointer-events": "none",
}));
// Path label at path midpoint
svg.appendChild(svgEl("text", {
x: mx, y: labelY + 1,
"text-anchor": "middle",
"dominant-baseline": "middle",
class: "kab-path-lbl",
"data-path": path.pathNumber,
fill: "#a8a8e0",
"font-size": PATH_LABEL_FONT_SIZE.toFixed(2),
"pointer-events": "none",
}, pathLabel));
}
if (hasTarotImage) {
const tarotY = hasLabel
? my + PATH_TAROT_OFFSET_WITH_LABEL
: my - PATH_TAROT_OFFSET_NO_LABEL;
svg.appendChild(svgEl("image", {
href: tarotImage,
x: (mx - (PATH_TAROT_WIDTH / 2)).toFixed(2),
y: tarotY.toFixed(2),
width: PATH_TAROT_WIDTH.toFixed(2),
height: PATH_TAROT_HEIGHT.toFixed(2),
preserveAspectRatio: "xMidYMid meet",
class: "kab-path-tarot",
"data-path": path.pathNumber,
role: "button",
tabindex: "0",
"aria-label": `Path ${path.pathNumber} Tarot card ${path.tarot?.card || ""}`,
style: "cursor:pointer"
}));
}
});
// ── Da'at — phantom sephira (dashed, informational only) ────────────────
svg.appendChild(svgEl("circle", {
cx: DAAT[0], cy: DAAT[1], r: "9",
fill: "none", stroke: "#3c3c5c",
"stroke-dasharray": "3 2", "stroke-width": "1",
"pointer-events": "none",
}));
svg.appendChild(svgEl("text", {
x: DAAT[0] + 13, y: DAAT[1] + 1,
"text-anchor": "start", "dominant-baseline": "middle",
fill: "#3c3c5c", "font-size": "6.5", "pointer-events": "none",
}, "Da'at"));
// ── sephiroth circles (drawn last, on top of paths) ──────────────────────
tree.sephiroth.forEach(seph => {
const [cx, cy] = NODE_POS[seph.number];
const fill = SEPH_FILL[seph.number] || "#555";
const isLeft = cx < 80;
const isMid = cx === 120;
// Glow halo (subtle, pointer-events:none)
svg.appendChild(svgEl("circle", {
cx, cy, r: "16",
fill, opacity: "0.12",
class: "kab-node-glow",
"data-sephira": seph.number,
"pointer-events": "none",
}));
// Main clickable circle
svg.appendChild(svgEl("circle", {
cx, cy, r: R,
fill, stroke: "#00000040", "stroke-width": "1",
class: "kab-node",
"data-sephira": seph.number,
role: "button",
tabindex: "0",
"aria-label": `Sephira ${seph.number}: ${seph.name}`,
style: "cursor:pointer",
}));
// Sephira number inside the circle
svg.appendChild(svgEl("text", {
x: cx, y: cy + 0.5,
"text-anchor": "middle", "dominant-baseline": "middle",
fill: DARK_TEXT.has(seph.number) ? "#111" : "#fff",
"font-size": "8", "font-weight": "bold",
"pointer-events": "none",
}, String(seph.number)));
// Name label beside the circle
const lx = isLeft ? cx - R - 4 : cx + R + 4;
svg.appendChild(svgEl("text", {
x: isMid ? cx : lx,
y: isMid ? cy + R + 8 : cy,
"text-anchor": isMid ? "middle" : (isLeft ? "end" : "start"),
"dominant-baseline": isMid ? "auto" : "middle",
fill: "#c0c0d4",
"font-size": "7.5", "pointer-events": "none",
class: "kab-node-lbl",
}, seph.name));
});
return svg;
}
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 bindTreeInteractions(svg, tree, elements) {
// Delegate clicks via element's own data attributes
svg.addEventListener("click", e => {
const clickTarget = e.target instanceof Element ? e.target : null;
const sephNum = clickTarget?.dataset?.sephira;
const pathNum = clickTarget?.dataset?.path;
if (pathNum != null && clickTarget?.classList?.contains("kab-path-tarot")) {
const p = tree.paths.find(x => x.pathNumber === Number(pathNum));
if (p) {
openTarotLightboxForPath(p, getSvgImageHref(clickTarget));
renderPathDetail(p, tree, elements);
}
return;
}
if (sephNum != null) {
const s = tree.sephiroth.find(x => x.number === Number(sephNum));
if (s) renderSephiraDetail(s, tree, elements);
} else if (pathNum != null) {
const p = tree.paths.find(x => x.pathNumber === Number(pathNum));
if (p) renderPathDetail(p, tree, elements);
}
});
// Keyboard access for path hit-areas and tarot images
svg.querySelectorAll(".kab-path-hit, .kab-path-tarot").forEach(el => {
el.addEventListener("keydown", e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
const p = tree.paths.find(x => x.pathNumber === Number(el.dataset.path));
if (p) {
if (el.classList.contains("kab-path-tarot")) {
openTarotLightboxForPath(p, getSvgImageHref(el));
}
renderPathDetail(p, tree, elements);
}
}
});
});
// Keyboard access for sephira circles
svg.querySelectorAll(".kab-node").forEach(el => {
el.addEventListener("keydown", e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
const s = tree.sephiroth.find(x => x.number === Number(el.dataset.sephira));
if (s) renderSephiraDetail(s, tree, elements);
}
});
});
}
function bindRoseCrossInteractions(svg, tree, roseElements) {
if (!svg || !roseElements?.detailBodyEl) {
return;
}
const openPathFromTarget = (targetEl) => {
if (!(targetEl instanceof Element)) {
return;
}
const petal = targetEl.closest(".kab-rose-petal[data-path]");
if (!(petal instanceof SVGElement)) {
return;
}
const pathNumber = Number(petal.dataset.path);
if (!Number.isFinite(pathNumber)) {
return;
}
const path = tree.paths.find((entry) => entry.pathNumber === pathNumber);
if (path) {
renderPathDetail(path, tree, roseElements);
}
};
svg.addEventListener("click", (event) => {
openPathFromTarget(event.target);
});
svg.querySelectorAll(".kab-rose-petal[data-path]").forEach((petal) => {
petal.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
openPathFromTarget(petal);
}
});
});
}
function renderRoseLandingIntro(roseElements) {
if (typeof kabbalahDetailUi.renderRoseLandingIntro === "function") {
kabbalahDetailUi.renderRoseLandingIntro(roseElements);
}
}
function renderRoseCross(elements) {
if (!state.tree || !elements?.roseCrossContainerEl) {
return;
}
const roseElements = getRoseDetailElements(elements);
if (!roseElements?.detailBodyEl) {
return;
}
const roseBuilder = window.KabbalahRosicrucianCross?.buildRosicrucianCrossSVG;
if (typeof roseBuilder !== "function") {
return;
}
const roseSvg = roseBuilder(state.tree);
elements.roseCrossContainerEl.innerHTML = "";
elements.roseCrossContainerEl.appendChild(roseSvg);
bindRoseCrossInteractions(roseSvg, state.tree, roseElements);
}
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 renderTree(elements) {
if (!state.tree || !elements?.treeContainerEl) {
return;
}
const svg = buildTreeSVG(state.tree);
elements.treeContainerEl.innerHTML = "";
elements.treeContainerEl.appendChild(svg);
bindTreeInteractions(svg, state.tree, elements);
}
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");
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 };
})();