1154 lines
40 KiB
JavaScript
1154 lines
40 KiB
JavaScript
(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 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 PLANET_ID_TO_LABEL = {
|
||
saturn: "Saturn",
|
||
jupiter: "Jupiter",
|
||
mars: "Mars",
|
||
sol: "Sol",
|
||
venus: "Venus",
|
||
mercury: "Mercury",
|
||
luna: "Luna"
|
||
};
|
||
|
||
const MINOR_RANK_BY_PLURAL = {
|
||
aces: "Ace",
|
||
twos: "Two",
|
||
threes: "Three",
|
||
fours: "Four",
|
||
fives: "Five",
|
||
sixes: "Six",
|
||
sevens: "Seven",
|
||
eights: "Eight",
|
||
nines: "Nine",
|
||
tens: "Ten"
|
||
};
|
||
|
||
const MINOR_SUITS = ["Wands", "Cups", "Swords", "Disks"];
|
||
|
||
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"),
|
||
};
|
||
}
|
||
|
||
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 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;
|
||
}
|
||
|
||
// ─── 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;
|
||
}
|
||
|
||
// ─── detail panel helpers ───────────────────────────────────────────────────
|
||
function metaCard(label, value, wide) {
|
||
const card = document.createElement("div");
|
||
card.className = wide ? "planet-meta-card kab-wide-card" : "planet-meta-card";
|
||
card.innerHTML = `<strong>${label}</strong><p class="planet-text">${value || "—"}</p>`;
|
||
return card;
|
||
}
|
||
|
||
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 createNavButton(label, eventName, detail) {
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.className = "kab-god-link";
|
||
btn.textContent = `${label} ↗`;
|
||
btn.addEventListener("click", () => {
|
||
document.dispatchEvent(new CustomEvent(eventName, { detail }));
|
||
});
|
||
return btn;
|
||
}
|
||
|
||
function appendLinkRow(card, buttons) {
|
||
if (!buttons?.length) return;
|
||
const row = document.createElement("div");
|
||
row.className = "kab-god-links";
|
||
buttons.forEach((button) => row.appendChild(button));
|
||
card.appendChild(row);
|
||
}
|
||
|
||
function buildPlanetLuminaryCard(planetValue) {
|
||
const card = metaCard("Planet / Luminary", planetValue);
|
||
const planetId = resolvePlanetId(planetValue);
|
||
if (planetId) {
|
||
appendLinkRow(card, [
|
||
createNavButton(`View ${PLANET_ID_TO_LABEL[planetId] || planetValue} in Planets`, "nav:planet", { planetId })
|
||
]);
|
||
return card;
|
||
}
|
||
|
||
const zodiacId = resolveZodiacId(planetValue);
|
||
if (zodiacId) {
|
||
appendLinkRow(card, [
|
||
createNavButton(`View ${zodiacId.charAt(0).toUpperCase() + zodiacId.slice(1)} in Zodiac`, "nav:zodiac", { signId: zodiacId })
|
||
]);
|
||
}
|
||
return card;
|
||
}
|
||
|
||
function extractMinorRank(attribution) {
|
||
const match = String(attribution || "").match(/\bthe\s+4\s+(aces|twos|threes|fours|fives|sixes|sevens|eights|nines|tens)\b/i);
|
||
if (!match) return null;
|
||
return MINOR_RANK_BY_PLURAL[(match[1] || "").toLowerCase()] || null;
|
||
}
|
||
|
||
function buildMinorTarotNames(attribution) {
|
||
const rank = extractMinorRank(attribution);
|
||
if (!rank) return [];
|
||
return MINOR_SUITS.map((suit) => `${rank} of ${suit}`);
|
||
}
|
||
|
||
function buildTarotAttributionCard(attribution) {
|
||
const card = metaCard("Tarot Attribution", attribution);
|
||
const minorCards = buildMinorTarotNames(attribution);
|
||
if (minorCards.length) {
|
||
appendLinkRow(card, minorCards.map((cardName) =>
|
||
createNavButton(cardName, "nav:tarot-trump", { cardName })
|
||
));
|
||
}
|
||
return card;
|
||
}
|
||
|
||
function buildAstrologyCard(astrology) {
|
||
const astroText = astrology ? `${astrology.name} (${astrology.type})` : "—";
|
||
const card = metaCard("Astrology", astroText);
|
||
if (astrology?.type === "planet") {
|
||
const planetId = resolvePlanetId(astrology.name);
|
||
if (planetId) {
|
||
appendLinkRow(card, [
|
||
createNavButton(`View ${PLANET_ID_TO_LABEL[planetId] || astrology.name} in Planets`, "nav:planet", { planetId })
|
||
]);
|
||
}
|
||
} else if (astrology?.type === "zodiac") {
|
||
const signId = resolveZodiacId(astrology.name);
|
||
if (signId) {
|
||
appendLinkRow(card, [
|
||
createNavButton(`View ${signId.charAt(0).toUpperCase() + signId.slice(1)} in Zodiac`, "nav:zodiac", { signId })
|
||
]);
|
||
}
|
||
}
|
||
return card;
|
||
}
|
||
|
||
function buildConnectsCard(path, fromName, toName) {
|
||
const card = metaCard("Connects", `${fromName} → ${toName}`);
|
||
appendLinkRow(card, [
|
||
createNavButton(`View ${fromName}`, "nav:kabbalah-path", { pathNo: Number(path.connects.from) }),
|
||
createNavButton(`View ${toName}`, "nav:kabbalah-path", { pathNo: Number(path.connects.to) })
|
||
]);
|
||
return card;
|
||
}
|
||
|
||
function buildHebrewLetterCard(letter) {
|
||
const label = `${letter.char || ""} ${letter.transliteration || ""} — "${letter.meaning || ""}" (${letter.letterType || ""})`;
|
||
const card = metaCard("Hebrew Letter", label);
|
||
const hebrewLetterId = resolveHebrewLetterId(letter.transliteration || letter.char || "");
|
||
|
||
if (hebrewLetterId) {
|
||
appendLinkRow(card, [
|
||
createNavButton(`View ${letter.transliteration || letter.char || "Letter"} in Alphabet`, "nav:alphabet", {
|
||
alphabet: "hebrew",
|
||
hebrewLetterId
|
||
})
|
||
]);
|
||
}
|
||
|
||
return card;
|
||
}
|
||
|
||
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 buildFourWorldsCard(tree, activeLetterToken = "") {
|
||
const activeToken = HEBREW_LETTER_ALIASES[normalizeLetterToken(activeLetterToken)] || normalizeLetterToken(activeLetterToken);
|
||
const worldLayers = Array.isArray(state.fourWorldLayers) && state.fourWorldLayers.length
|
||
? state.fourWorldLayers
|
||
: DEFAULT_FOUR_QABALISTIC_WORLD_LAYERS;
|
||
|
||
const card = document.createElement("div");
|
||
card.className = "planet-meta-card kab-wide-card";
|
||
|
||
const title = document.createElement("strong");
|
||
title.textContent = "Four Qabalistic Worlds & Soul Layers";
|
||
card.appendChild(title);
|
||
|
||
const stack = document.createElement("div");
|
||
stack.className = "cal-item-stack";
|
||
|
||
worldLayers.forEach((layer) => {
|
||
const row = document.createElement("div");
|
||
row.className = "cal-item-row";
|
||
|
||
const isActive = Boolean(activeToken) && activeToken === layer.hebrewToken;
|
||
|
||
const head = document.createElement("div");
|
||
head.className = "cal-item-head";
|
||
head.innerHTML = `
|
||
<span class="cal-item-name">${layer.slot}: ${layer.letterChar} — ${layer.world}</span>
|
||
<span class="planet-list-meta">${layer.soulLayer}</span>
|
||
`;
|
||
row.appendChild(head);
|
||
|
||
const worldLine = document.createElement("div");
|
||
worldLine.className = "planet-text";
|
||
worldLine.textContent = `${layer.worldLayer} · ${layer.worldDescription}`;
|
||
row.appendChild(worldLine);
|
||
|
||
const soulLine = document.createElement("div");
|
||
soulLine.className = "planet-text";
|
||
soulLine.textContent = `${layer.soulLayer} — ${layer.soulTitle}: ${layer.soulDescription}`;
|
||
row.appendChild(soulLine);
|
||
|
||
const buttonRow = [];
|
||
const hebrewLetterId = resolveHebrewLetterId(layer.hebrewToken);
|
||
if (hebrewLetterId) {
|
||
buttonRow.push(
|
||
createNavButton(`View ${layer.letterChar} in Alphabet`, "nav:alphabet", {
|
||
alphabet: "hebrew",
|
||
hebrewLetterId
|
||
})
|
||
);
|
||
}
|
||
|
||
const linkedPath = findPathByHebrewToken(tree, layer.hebrewToken);
|
||
if (linkedPath?.pathNumber != null) {
|
||
buttonRow.push(
|
||
createNavButton(`View Path ${linkedPath.pathNumber}`, "nav:kabbalah-path", { pathNo: Number(linkedPath.pathNumber) })
|
||
);
|
||
}
|
||
|
||
appendLinkRow(row, buttonRow);
|
||
|
||
if (isActive) {
|
||
row.style.borderColor = "#818cf8";
|
||
}
|
||
|
||
stack.appendChild(row);
|
||
});
|
||
|
||
card.appendChild(stack);
|
||
return card;
|
||
}
|
||
|
||
function splitCorrespondenceNames(value) {
|
||
return String(value || "")
|
||
.split(/,|;|·|\/|\bor\b|\band\b|\+/i)
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
}
|
||
|
||
function uniqueNames(values) {
|
||
const seen = new Set();
|
||
const output = [];
|
||
values.forEach((name) => {
|
||
const key = String(name || "").toLowerCase();
|
||
if (seen.has(key)) return;
|
||
seen.add(key);
|
||
output.push(name);
|
||
});
|
||
return output;
|
||
}
|
||
|
||
function godLinksCard(label, names, pathNo, metaText) {
|
||
const card = document.createElement("div");
|
||
card.className = "planet-meta-card";
|
||
|
||
const title = document.createElement("strong");
|
||
title.textContent = label;
|
||
card.appendChild(title);
|
||
|
||
if (metaText) {
|
||
const meta = document.createElement("p");
|
||
meta.className = "planet-text kab-god-meta";
|
||
meta.textContent = metaText;
|
||
card.appendChild(meta);
|
||
}
|
||
|
||
const row = document.createElement("div");
|
||
row.className = "kab-god-links";
|
||
|
||
names.forEach((name) => {
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.className = "kab-god-link";
|
||
btn.textContent = name;
|
||
btn.addEventListener("click", () => {
|
||
document.dispatchEvent(new CustomEvent("nav:gods", {
|
||
detail: { godName: name, pathNo: Number(pathNo) }
|
||
}));
|
||
});
|
||
row.appendChild(btn);
|
||
});
|
||
|
||
card.appendChild(row);
|
||
return card;
|
||
}
|
||
|
||
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")
|
||
.forEach(el => el.classList.remove("kab-path-active"));
|
||
}
|
||
|
||
// ─── helper: append divine correspondences from gods.json ─────────────────────
|
||
function appendGodsCards(pathNo, elements) {
|
||
const gd = state.godsData[String(pathNo)];
|
||
if (!gd) return;
|
||
|
||
const hasAny = gd.greek || gd.roman || gd.egyptian || gd.egyptianPractical
|
||
|| gd.elohim || gd.archangel || gd.angelicOrder;
|
||
if (!hasAny) return;
|
||
|
||
const sep = document.createElement("div");
|
||
sep.className = "planet-meta-card kab-wide-card";
|
||
sep.innerHTML = `<strong style="color:#a1a1aa;font-size:11px;text-transform:uppercase;letter-spacing:.05em">Divine Correspondences</strong>`;
|
||
elements.detailBodyEl.appendChild(sep);
|
||
|
||
const greekNames = uniqueNames(splitCorrespondenceNames(gd.greek));
|
||
const romanNames = uniqueNames(splitCorrespondenceNames(gd.roman));
|
||
const egyptNames = uniqueNames([
|
||
...splitCorrespondenceNames(gd.egyptianPractical),
|
||
...splitCorrespondenceNames(gd.egyptian)
|
||
]);
|
||
|
||
if (greekNames.length) {
|
||
elements.detailBodyEl.appendChild(godLinksCard("Greek", greekNames, pathNo));
|
||
}
|
||
if (romanNames.length) {
|
||
elements.detailBodyEl.appendChild(godLinksCard("Roman", romanNames, pathNo));
|
||
}
|
||
if (egyptNames.length) {
|
||
elements.detailBodyEl.appendChild(godLinksCard("Egyptian", egyptNames, pathNo));
|
||
}
|
||
|
||
if (gd.elohim) {
|
||
const g = gd.elohim;
|
||
const meta = `${g.hebrew}${g.meaning ? " — " + g.meaning : ""}`;
|
||
elements.detailBodyEl.appendChild(godLinksCard(
|
||
"God Name",
|
||
uniqueNames(splitCorrespondenceNames(g.transliteration)),
|
||
pathNo,
|
||
meta
|
||
));
|
||
}
|
||
if (gd.archangel) {
|
||
const a = gd.archangel;
|
||
const meta = `${a.hebrew}`;
|
||
elements.detailBodyEl.appendChild(godLinksCard(
|
||
"Archangel",
|
||
uniqueNames(splitCorrespondenceNames(a.transliteration)),
|
||
pathNo,
|
||
meta
|
||
));
|
||
}
|
||
if (gd.angelicOrder) {
|
||
const o = gd.angelicOrder;
|
||
elements.detailBodyEl.appendChild(metaCard(
|
||
"Angelic Order",
|
||
`${o.hebrew} ${o.transliteration}${o.meaning ? " — " + o.meaning : ""}`
|
||
));
|
||
}
|
||
|
||
}
|
||
|
||
// ─── render sephira detail ───────────────────────────────────────────────────
|
||
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"));
|
||
|
||
elements.detailNameEl.textContent = `${seph.number} · ${seph.name}`;
|
||
elements.detailSubEl.textContent =
|
||
[seph.nameHebrew, seph.translation, seph.planet].filter(Boolean).join(" · ");
|
||
|
||
elements.detailBodyEl.innerHTML = "";
|
||
elements.detailBodyEl.appendChild(buildFourWorldsCard(tree));
|
||
elements.detailBodyEl.appendChild(buildPlanetLuminaryCard(seph.planet));
|
||
elements.detailBodyEl.appendChild(metaCard("Intelligence", seph.intelligence));
|
||
elements.detailBodyEl.appendChild(buildTarotAttributionCard(seph.tarot));
|
||
|
||
if (seph.description) {
|
||
elements.detailBodyEl.appendChild(
|
||
metaCard(seph.name, seph.description, true)
|
||
);
|
||
}
|
||
|
||
// Quick-access chips for connected paths
|
||
const connected = tree.paths.filter(
|
||
p => p.connects.from === seph.number || p.connects.to === seph.number
|
||
);
|
||
if (connected.length) {
|
||
const card = document.createElement("div");
|
||
card.className = "planet-meta-card kab-wide-card";
|
||
const chips = connected.map(p =>
|
||
`<span class="kab-chip" data-path="${p.pathNumber}" role="button" tabindex="0" title="Path ${p.pathNumber}: ${p.tarot?.card || ""}">`
|
||
+ `${p.hebrewLetter?.char || ""} <span class="kab-chip-sub">${p.pathNumber}</span>`
|
||
+ `</span>`
|
||
).join("");
|
||
card.innerHTML = `<strong>Connected Paths</strong><div class="kab-chips">${chips}</div>`;
|
||
elements.detailBodyEl.appendChild(card);
|
||
|
||
card.querySelectorAll(".kab-chip[data-path]").forEach(chip => {
|
||
const handler = () => {
|
||
const path = tree.paths.find(p => p.pathNumber === Number(chip.dataset.path));
|
||
if (path) renderPathDetail(path, tree, elements);
|
||
};
|
||
chip.addEventListener("click", handler);
|
||
chip.addEventListener("keydown", e => {
|
||
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handler(); }
|
||
});
|
||
});
|
||
}
|
||
|
||
appendGodsCards(seph.number, elements);
|
||
}
|
||
|
||
// ─── render path detail ──────────────────────────────────────────────────────
|
||
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"));
|
||
|
||
const letter = path.hebrewLetter || {};
|
||
const fromName = tree.sephiroth.find(s => s.number === path.connects.from)?.name || path.connects.from;
|
||
const toName = tree.sephiroth.find(s => s.number === path.connects.to)?.name || path.connects.to;
|
||
const astro = path.astrology ? `${path.astrology.name} (${path.astrology.type})` : "—";
|
||
const tarotStr = path.tarot?.card
|
||
? `${path.tarot.card}${path.tarot.trumpNumber != null ? " · Trump " + path.tarot.trumpNumber : ""}`
|
||
: "—";
|
||
|
||
elements.detailNameEl.textContent =
|
||
`Path ${path.pathNumber} · ${letter.char || ""} ${letter.transliteration || ""}`;
|
||
elements.detailSubEl.textContent = [path.tarot?.card, astro].filter(Boolean).join(" · ");
|
||
|
||
elements.detailBodyEl.innerHTML = "";
|
||
elements.detailBodyEl.appendChild(buildFourWorldsCard(tree, letter.transliteration || letter.char || ""));
|
||
elements.detailBodyEl.appendChild(buildConnectsCard(path, fromName, toName));
|
||
elements.detailBodyEl.appendChild(buildHebrewLetterCard(letter));
|
||
elements.detailBodyEl.appendChild(buildAstrologyCard(path.astrology));
|
||
|
||
// Tarot card — clickable if a trump card is associated
|
||
const tarotMetaCard = document.createElement("div");
|
||
tarotMetaCard.className = "planet-meta-card";
|
||
const tarotLabel = document.createElement("strong");
|
||
tarotLabel.textContent = "Tarot";
|
||
tarotMetaCard.appendChild(tarotLabel);
|
||
if (path.tarot?.card && path.tarot.trumpNumber != null) {
|
||
const tarotBtn = document.createElement("button");
|
||
tarotBtn.type = "button";
|
||
tarotBtn.className = "kab-tarot-link";
|
||
tarotBtn.textContent = `${path.tarot.card} · Trump ${path.tarot.trumpNumber}`;
|
||
tarotBtn.title = "Open in Tarot section";
|
||
tarotBtn.addEventListener("click", () => {
|
||
document.dispatchEvent(new CustomEvent("kab:view-trump", {
|
||
detail: { trumpNumber: path.tarot.trumpNumber }
|
||
}));
|
||
});
|
||
tarotMetaCard.appendChild(tarotBtn);
|
||
} else {
|
||
const tarotP = document.createElement("p");
|
||
tarotP.className = "planet-text";
|
||
tarotP.textContent = tarotStr || "—";
|
||
tarotMetaCard.appendChild(tarotP);
|
||
}
|
||
elements.detailBodyEl.appendChild(tarotMetaCard);
|
||
|
||
elements.detailBodyEl.appendChild(metaCard("Intelligence", path.intelligence));
|
||
elements.detailBodyEl.appendChild(metaCard("Pillar", path.pillar));
|
||
|
||
if (path.description) {
|
||
const desc = document.createElement("div");
|
||
desc.className = "planet-meta-card kab-wide-card";
|
||
desc.innerHTML =
|
||
`<strong>Path ${path.pathNumber} — Sefer Yetzirah</strong>`
|
||
+ `<p class="planet-text">${path.description.replace(/\n/g, "<br><br>")}</p>`;
|
||
elements.detailBodyEl.appendChild(desc);
|
||
}
|
||
|
||
appendGodsCards(path.pathNumber, elements);
|
||
}
|
||
|
||
function bindTreeInteractions(svg, tree, elements) {
|
||
// Delegate clicks via element's own data attributes
|
||
svg.addEventListener("click", e => {
|
||
const sephNum = e.target.dataset?.sephira;
|
||
const pathNum = e.target.dataset?.path;
|
||
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) 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 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);
|
||
}
|
||
|
||
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 };
|
||
})();
|