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 ,
2026-05-28 18:19:13 -07:00
selectedWorldLayerIndex : 0 ,
2026-03-07 01:09:00 -08:00
selectedSephiraNumber : null ,
2026-03-12 21:01:32 -07:00
selectedPathNumber : null ,
2026-05-28 18:19:13 -07:00
activeNodeKey : "" ,
2026-03-12 21:01:32 -07:00
exportInProgress : false ,
exportFormat : ""
2026-03-07 01:09:00 -08:00
};
2026-05-28 18:19:13 -07:00
let detailNavigator = null ;
let browserDetailNavigator = null ;
let worldsDetailNavigator = null ;
let pathsDetailNavigator = null ;
let roseDetailNavigator = null ;
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-05-28 18:19:13 -07:00
const DAATH_SEPHIRA = Object . freeze ({
number : 0 ,
displayNumber : "Daath" ,
sephiraId : "daath" ,
name : "Daath" ,
nameHebrew : "דעת" ,
translation : "Knowledge" ,
planet : "Abyss / Hidden Sephirah" ,
intelligence : "Invisible Sephirah of Knowledge" ,
tarot : "No fixed trump attribution" ,
description : "Daath is the hidden or invisible sephirah placed beneath Kether and between Chokmah and Binah. In Hermetic Qabalah it often marks the threshold of the Abyss rather than a stable emanation like the ten manifest sephiroth."
});
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 (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 {
2026-05-28 18:19:13 -07:00
browserSectionEl : document . getElementById ( "kabbalah-section" ),
browserListEl : document . getElementById ( "kab-browser-list" ),
browserCountEl : document . getElementById ( "kab-browser-count" ),
browserDetailNameEl : document . getElementById ( "kab-browser-detail-name" ),
browserDetailSubEl : document . getElementById ( "kab-browser-detail-sub" ),
browserDetailBodyEl : document . getElementById ( "kab-browser-detail-body" ),
browserDetailPrevEl : document . getElementById ( "kab-browser-detail-prev" ),
browserDetailPositionEl : document . getElementById ( "kab-browser-detail-position" ),
browserDetailNextEl : document . getElementById ( "kab-browser-detail-next" ),
worldsSectionEl : document . getElementById ( "kabbalah-worlds-section" ),
worldsListEl : document . getElementById ( "kab-worlds-list" ),
worldsCountEl : document . getElementById ( "kab-worlds-count" ),
worldsDetailNameEl : document . getElementById ( "kab-worlds-detail-name" ),
worldsDetailSubEl : document . getElementById ( "kab-worlds-detail-sub" ),
worldsDetailBodyEl : document . getElementById ( "kab-worlds-detail-body" ),
worldsDetailPrevEl : document . getElementById ( "kab-worlds-detail-prev" ),
worldsDetailPositionEl : document . getElementById ( "kab-worlds-detail-position" ),
worldsDetailNextEl : document . getElementById ( "kab-worlds-detail-next" ),
pathsSectionEl : document . getElementById ( "kabbalah-paths-section" ),
pathsListEl : document . getElementById ( "kab-paths-list" ),
pathsCountEl : document . getElementById ( "kab-paths-count" ),
pathsDetailNameEl : document . getElementById ( "kab-paths-detail-name" ),
pathsDetailSubEl : document . getElementById ( "kab-paths-detail-sub" ),
pathsDetailBodyEl : document . getElementById ( "kab-paths-detail-body" ),
pathsDetailPrevEl : document . getElementById ( "kab-paths-detail-prev" ),
pathsDetailPositionEl : document . getElementById ( "kab-paths-detail-position" ),
pathsDetailNextEl : document . getElementById ( "kab-paths-detail-next" ),
crossSectionEl : document . getElementById ( "kabbalah-cross-section" ),
sectionEl : document . getElementById ( "kabbalah-tree-section" ),
2026-03-07 01:09:00 -08:00
treeContainerEl : document . getElementById ( "kab-tree-container" ),
detailNameEl : document . getElementById ( "kab-detail-name" ),
detailSubEl : document . getElementById ( "kab-detail-sub" ),
detailBodyEl : document . getElementById ( "kab-detail-body" ),
2026-05-28 18:19:13 -07:00
detailPrevEl : document . getElementById ( "kab-detail-prev" ),
detailPositionEl : document . getElementById ( "kab-detail-position" ),
detailNextEl : document . getElementById ( "kab-detail-next" ),
2026-03-07 01:09:00 -08:00
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" ),
2026-05-28 18:19:13 -07:00
roseDetailPrevEl : document . getElementById ( "kab-rose-detail-prev" ),
roseDetailPositionEl : document . getElementById ( "kab-rose-detail-position" ),
roseDetailNextEl : document . getElementById ( "kab-rose-detail-next" ),
};
}
function getTreeDetailElements ( elements ) {
if ( ! elements ) {
return null ;
}
return {
detailNameEl : elements . detailNameEl ,
detailSubEl : elements . detailSubEl ,
detailBodyEl : elements . detailBodyEl
2026-03-07 05:17:50 -08:00
};
}
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
};
}
2026-05-28 18:19:13 -07:00
function getPathDetailElements ( elements ) {
if ( ! elements ) {
return null ;
}
return {
detailNameEl : elements . pathsDetailNameEl ,
detailSubEl : elements . pathsDetailSubEl ,
detailBodyEl : elements . pathsDetailBodyEl
};
}
function getBrowserDetailElements ( elements ) {
if ( ! elements ) {
return null ;
}
return {
detailNameEl : elements . browserDetailNameEl ,
detailSubEl : elements . browserDetailSubEl ,
detailBodyEl : elements . browserDetailBodyEl
};
}
function getWorldDetailElements ( elements ) {
if ( ! elements ) {
return null ;
}
return {
detailNameEl : elements . worldsDetailNameEl ,
detailSubEl : elements . worldsDetailSubEl ,
detailBodyEl : elements . worldsDetailBodyEl
};
}
function normalizeDetailElements ( elements ) {
if ( ! elements ) {
return null ;
}
return {
detailNameEl : elements . detailNameEl || null ,
detailSubEl : elements . detailSubEl || null ,
detailBodyEl : elements . detailBodyEl || null
};
}
function getDetailRenderTargets ( primaryElements ) {
const elements = getElements ();
const candidates = [
normalizeDetailElements ( primaryElements ),
getTreeDetailElements ( elements ),
getBrowserDetailElements ( elements )
];
const seen = new Set ();
return candidates . filter (( target ) => {
const bodyEl = target ? . detailBodyEl ;
if ( ! ( bodyEl instanceof Element ) || seen . has ( bodyEl )) {
return false ;
}
seen . add ( bodyEl );
return true ;
});
}
function hasFiniteSelectionNumber ( value ) {
if ( value === null || value === undefined || value === "" ) {
return false ;
}
return Number . isFinite ( Number ( value ));
}
function isDaathToken ( value ) {
return String ( value || "" ). trim (). toLowerCase () === "daath" ;
}
function buildSephiraKey ( value ) {
if ( isDaathToken ( value ) || Number ( value ) === 0 ) {
return "sephira:daath" ;
}
return hasFiniteSelectionNumber ( value ) ? `sephira: ${ Number ( value ) } ` : "" ;
}
function buildPathKey ( value ) {
return hasFiniteSelectionNumber ( value ) ? `path: ${ Number ( value ) } ` : "" ;
}
function buildWorldKey ( value ) {
return hasFiniteSelectionNumber ( value ) ? `world: ${ Number ( value ) } ` : "" ;
}
function getSelectedSephiraKey () {
return buildSephiraKey ( state . selectedSephiraNumber );
}
function getSelectedPathKey () {
return buildPathKey ( state . selectedPathNumber );
}
function getSelectedWorldKey () {
return buildWorldKey ( state . selectedWorldLayerIndex );
}
function getSephiraByNumber ( number ) {
if ( isDaathToken ( number ) || Number ( number ) === 0 ) {
return DAATH_SEPHIRA ;
}
if ( ! state . tree ) {
return null ;
}
return state . tree . sephiroth . find (( entry ) => Number ( entry ? . number ) === Number ( number )) || null ;
}
function getPathByNumber ( number ) {
if ( ! state . tree ) {
return null ;
}
return state . tree . paths . find (( entry ) => Number ( entry ? . pathNumber ) === Number ( number )) || null ;
}
function getWorldLayerByIndex ( index ) {
if ( ! Array . isArray ( state . fourWorldLayers )) {
return null ;
}
const targetIndex = Number ( index );
return Number . isInteger ( targetIndex ) && targetIndex >= 0 && targetIndex < state . fourWorldLayers . length
? state . fourWorldLayers [ targetIndex ]
: null ;
}
function getSephirotSequenceEntries () {
if ( ! state . tree ) {
return [];
}
const entries = Array . isArray ( state . tree . sephiroth )
? [... state . tree . sephiroth ]
. sort (( left , right ) => Number ( left ? . number || 0 ) - Number ( right ? . number || 0 ))
. map (( entry ) => ({
key : buildSephiraKey ( entry ? . number ),
type : "sephira" ,
number : Number ( entry ? . number )
}))
: [];
const daathEntry = {
key : buildSephiraKey ( 0 ),
type : "sephira" ,
number : 0
};
const insertIndex = entries . findIndex (( entry ) => entry . number === 3 );
if ( insertIndex >= 0 ) {
entries . splice ( insertIndex + 1 , 0 , daathEntry );
} else {
entries . push ( daathEntry );
}
return entries ;
}
function getPathSequenceEntries () {
if ( ! state . tree ) {
return [];
}
return Array . isArray ( state . tree . paths )
? [... state . tree . paths ]
. sort (( left , right ) => Number ( left ? . pathNumber || 0 ) - Number ( right ? . pathNumber || 0 ))
. map (( entry ) => ({
key : buildPathKey ( entry ? . pathNumber ),
type : "path" ,
number : Number ( entry ? . pathNumber )
}))
: [];
}
function getWorldSequenceEntries () {
return Array . isArray ( state . fourWorldLayers )
? state . fourWorldLayers . map (( layer , index ) => ({
key : buildWorldKey ( index ),
type : "world" ,
index ,
world : String ( layer ? . world || "" )
}))
: [];
}
function getNodeSequenceEntries () {
if ( ! state . tree ) {
return [];
}
const sephiroth = Array . isArray ( state . tree . sephiroth )
? [... state . tree . sephiroth ]
. sort (( left , right ) => Number ( left ? . number || 0 ) - Number ( right ? . number || 0 ))
. map (( entry ) => ({
key : buildSephiraKey ( entry ? . number ),
type : "sephira" ,
number : Number ( entry ? . number )
}))
: [];
const paths = getPathSequenceEntries ();
return [... sephiroth , ... paths ];
}
function getSelectedNodeKey () {
return String ( state . activeNodeKey || "" ). trim () || getSelectedPathKey () || getSelectedSephiraKey ();
}
function buildSequenceState ( entries , currentKey ) {
const currentIndex = entries . findIndex (( entry ) => entry . key === currentKey );
return {
total : entries . length ,
currentIndex ,
previousKey : currentIndex > 0 ? entries [ currentIndex - 1 ]. key : "" ,
nextKey : currentIndex >= 0 && currentIndex < entries . length - 1 ? entries [ currentIndex + 1 ]. key : ""
};
}
function getNodeSequenceState () {
return buildSequenceState ( getNodeSequenceEntries (), getSelectedNodeKey ());
}
function getSephirotSequenceState () {
return buildSequenceState ( getSephirotSequenceEntries (), getSelectedSephiraKey ());
}
function getPathSequenceState () {
return buildSequenceState ( getPathSequenceEntries (), getSelectedPathKey ());
}
function getWorldSequenceState () {
return buildSequenceState ( getWorldSequenceEntries (), getSelectedWorldKey ());
}
function getBrowserListItemMeta ( entry ) {
const seph = getSephiraByNumber ( entry . number );
const displayNumber = String ( seph ? . displayNumber || entry . number || "" ). trim ();
return {
title : displayNumber ? ` ${ displayNumber } · ${ seph ? . name || "Sephirah" } ` : ` ${ seph ? . name || "Sephirah" } ` ,
meta : [ seph ? . nameHebrew , seph ? . translation , seph ? . planet ]. filter ( Boolean ). join ( " · " ) || "Sephirah"
};
}
function getWorldListItemMeta ( entry ) {
const layer = getWorldLayerByIndex ( entry . index );
return {
title : String ( layer ? . world || `World ${ entry . index + 1 } ` ),
meta : [
layer ? . slot ? ` ${ layer . slot } : ${ layer . letterChar || "" } ` . trim () : "" ,
layer ? . soulLayer
]. filter ( Boolean ). join ( " · " ) || "Qabalistic World"
};
}
function getPathListItemMeta ( entry ) {
const path = getPathByNumber ( entry . number );
const fromName = getSephiraByNumber ( path ? . connects ? . from ) ? . name || `Node ${ path ? . connects ? . from || "?" } ` ;
const toName = getSephiraByNumber ( path ? . connects ? . to ) ? . name || `Node ${ path ? . connects ? . to || "?" } ` ;
const letterLabel = [ path ? . hebrewLetter ? . char , path ? . hebrewLetter ? . transliteration ]. filter ( Boolean ). join ( " " ). trim ();
return {
title : `Path ${ entry . number }${ letterLabel ? ` · ${ letterLabel } ` : "" } ` ,
meta : [
` ${ fromName } -> ${ toName } ` ,
String ( path ? . tarot ? . card || "" ). trim ()
]. filter ( Boolean ). join ( " · " ) || "Path"
};
}
function syncBrowserListSelection ( elements = getElements ()) {
if ( ! elements ? . browserListEl ) {
return ;
}
const selectedKey = getSelectedSephiraKey ();
elements . browserListEl . querySelectorAll ( ".planet-list-item[data-node-key]" ). forEach (( button ) => {
const isSelected = button . dataset . nodeKey === selectedKey ;
button . classList . toggle ( "is-selected" , isSelected );
button . setAttribute ( "aria-selected" , isSelected ? "true" : "false" );
});
}
function renderBrowserList ( elements = getElements ()) {
if ( ! elements ? . browserListEl ) {
return ;
}
const entries = getSephirotSequenceEntries ();
elements . browserListEl . innerHTML = "" ;
entries . forEach (( entry ) => {
const button = document . createElement ( "button" );
const { title , meta } = getBrowserListItemMeta ( entry );
button . type = "button" ;
button . className = "planet-list-item" ;
button . setAttribute ( "role" , "option" );
button . dataset . nodeKey = entry . key ;
button . innerHTML = `
<div class="planet-list-name"> ${ title } </div>
<div class="planet-list-meta"> ${ meta } </div>
` ;
elements . browserListEl . appendChild ( button );
});
if ( elements . browserCountEl ) {
elements . browserCountEl . textContent = ` ${ entries . length } sephiroth` ;
}
syncBrowserListSelection ( elements );
}
function syncWorldListSelection ( elements = getElements ()) {
if ( ! elements ? . worldsListEl ) {
return ;
}
const selectedKey = getSelectedWorldKey ();
elements . worldsListEl . querySelectorAll ( ".planet-list-item[data-world-key]" ). forEach (( button ) => {
const isSelected = button . dataset . worldKey === selectedKey ;
button . classList . toggle ( "is-selected" , isSelected );
button . setAttribute ( "aria-selected" , isSelected ? "true" : "false" );
});
}
function syncPathsListSelection ( elements = getElements ()) {
if ( ! elements ? . pathsListEl ) {
return ;
}
const selectedKey = getSelectedPathKey ();
elements . pathsListEl . querySelectorAll ( ".planet-list-item[data-path-key]" ). forEach (( button ) => {
const isSelected = button . dataset . pathKey === selectedKey ;
button . classList . toggle ( "is-selected" , isSelected );
button . setAttribute ( "aria-selected" , isSelected ? "true" : "false" );
});
}
function renderWorldsList ( elements = getElements ()) {
if ( ! elements ? . worldsListEl ) {
return ;
}
const entries = getWorldSequenceEntries ();
elements . worldsListEl . innerHTML = "" ;
entries . forEach (( entry ) => {
const button = document . createElement ( "button" );
const { title , meta } = getWorldListItemMeta ( entry );
button . type = "button" ;
button . className = "planet-list-item" ;
button . setAttribute ( "role" , "option" );
button . dataset . worldKey = entry . key ;
button . innerHTML = `
<div class="planet-list-name"> ${ title } </div>
<div class="planet-list-meta"> ${ meta } </div>
` ;
elements . worldsListEl . appendChild ( button );
});
if ( elements . worldsCountEl ) {
elements . worldsCountEl . textContent = ` ${ entries . length } worlds` ;
}
syncWorldListSelection ( elements );
}
function renderPathsList ( elements = getElements ()) {
if ( ! elements ? . pathsListEl ) {
return ;
}
const entries = getPathSequenceEntries ();
elements . pathsListEl . innerHTML = "" ;
entries . forEach (( entry ) => {
const button = document . createElement ( "button" );
const { title , meta } = getPathListItemMeta ( entry );
button . type = "button" ;
button . className = "planet-list-item" ;
button . setAttribute ( "role" , "option" );
button . dataset . pathKey = entry . key ;
button . innerHTML = `
<div class="planet-list-name"> ${ title } </div>
<div class="planet-list-meta"> ${ meta } </div>
` ;
elements . pathsListEl . appendChild ( button );
});
if ( elements . pathsCountEl ) {
elements . pathsCountEl . textContent = ` ${ entries . length } paths` ;
}
syncPathsListSelection ( elements );
}
function bindBrowserList ( elements = getElements ()) {
if ( ! elements ? . browserListEl || elements . browserListEl . dataset . bound ) {
return ;
}
elements . browserListEl . addEventListener ( "click" , ( event ) => {
const target = event . target instanceof Element
? event . target . closest ( ".planet-list-item[data-node-key]" )
: null ;
if ( ! ( target instanceof HTMLButtonElement )) {
return ;
}
const targetKey = String ( target . dataset . nodeKey || "" ). trim ();
if ( ! targetKey ) {
return ;
}
selectNodeBySequenceKey ( targetKey , getBrowserDetailElements ( getElements ()));
});
elements . browserListEl . dataset . bound = "true" ;
}
function bindWorldList ( elements = getElements ()) {
if ( ! elements ? . worldsListEl || elements . worldsListEl . dataset . bound ) {
return ;
}
elements . worldsListEl . addEventListener ( "click" , ( event ) => {
const target = event . target instanceof Element
? event . target . closest ( ".planet-list-item[data-world-key]" )
: null ;
if ( ! ( target instanceof HTMLButtonElement )) {
return ;
}
const targetKey = String ( target . dataset . worldKey || "" ). trim ();
if ( ! targetKey ) {
return ;
}
selectNodeBySequenceKey ( targetKey , getWorldDetailElements ( getElements ()));
});
elements . worldsListEl . dataset . bound = "true" ;
}
function bindPathsList ( elements = getElements ()) {
if ( ! elements ? . pathsListEl || elements . pathsListEl . dataset . bound ) {
return ;
}
elements . pathsListEl . addEventListener ( "click" , ( event ) => {
const target = event . target instanceof Element
? event . target . closest ( ".planet-list-item[data-path-key]" )
: null ;
if ( ! ( target instanceof HTMLButtonElement )) {
return ;
}
const targetKey = String ( target . dataset . pathKey || "" ). trim ();
if ( ! targetKey ) {
return ;
}
selectNodeBySequenceKey ( targetKey , getPathDetailElements ( getElements ()));
});
elements . pathsListEl . dataset . bound = "true" ;
}
function selectNodeBySequenceKey ( targetKey , elements = getElements ()) {
if ( ! state . tree ) {
return false ;
}
const [ type , rawToken ] = String ( targetKey || "" ). split ( ":" );
if ( type === "sephira" ) {
const seph = getSephiraByNumber ( isDaathToken ( rawToken ) ? 0 : Number ( rawToken ));
if ( ! seph ) {
return false ;
}
renderSephiraDetail ( seph , state . tree , elements );
return true ;
}
if ( type === "path" ) {
const path = getPathByNumber ( Number ( rawToken ));
if ( ! path ) {
return false ;
}
renderPathDetail ( path , state . tree , elements );
return true ;
}
if ( type === "world" ) {
const worldLayer = getWorldLayerByIndex ( Number ( rawToken ));
if ( ! worldLayer ) {
return false ;
}
renderWorldLayerDetail ( worldLayer , Number ( rawToken ), state . tree , elements );
return true ;
}
return false ;
}
function getDetailNavigator () {
if ( detailNavigator || typeof window . TarotSequenceNav ? . createSequenceNavigator !== "function" ) {
return detailNavigator ;
}
detailNavigator = window . TarotSequenceNav . createSequenceNavigator ({
getElements ,
isActive : ( elements ) => Boolean ( elements ? . sectionEl && elements . sectionEl . hidden === false ),
getSequenceState : getNodeSequenceState ,
getPrevButton : ( elements ) => elements ? . detailPrevEl ,
getNextButton : ( elements ) => elements ? . detailNextEl ,
getPositionEl : ( elements ) => elements ? . detailPositionEl ,
formatPositionText : ({ total , currentIndex }) => {
if ( total > 0 && currentIndex >= 0 ) {
return ` ${ currentIndex + 1 } of ${ total } nodes` ;
}
return total > 0 ? ` ${ total } nodes` : "No nodes" ;
},
selectTarget : ( targetKey , elements ) => selectNodeBySequenceKey ( targetKey , elements )
});
return detailNavigator ;
}
function getBrowserDetailNavigator () {
if ( browserDetailNavigator || typeof window . TarotSequenceNav ? . createSequenceNavigator !== "function" ) {
return browserDetailNavigator ;
}
browserDetailNavigator = window . TarotSequenceNav . createSequenceNavigator ({
getElements ,
isActive : ( elements ) => Boolean ( elements ? . browserSectionEl && elements . browserSectionEl . hidden === false ),
getSequenceState : getSephirotSequenceState ,
getPrevButton : ( elements ) => elements ? . browserDetailPrevEl ,
getNextButton : ( elements ) => elements ? . browserDetailNextEl ,
getPositionEl : ( elements ) => elements ? . browserDetailPositionEl ,
formatPositionText : ({ total , currentIndex }) => {
if ( total > 0 && currentIndex >= 0 ) {
return ` ${ currentIndex + 1 } of ${ total } sephiroth` ;
}
return total > 0 ? ` ${ total } sephiroth` : "No sephiroth" ;
},
selectTarget : ( targetKey ) => selectNodeBySequenceKey ( targetKey , getBrowserDetailElements ( getElements ()))
});
return browserDetailNavigator ;
}
function getPathsDetailNavigator () {
if ( pathsDetailNavigator || typeof window . TarotSequenceNav ? . createSequenceNavigator !== "function" ) {
return pathsDetailNavigator ;
}
pathsDetailNavigator = window . TarotSequenceNav . createSequenceNavigator ({
getElements ,
isActive : ( elements ) => Boolean ( elements ? . pathsSectionEl && elements . pathsSectionEl . hidden === false ),
getSequenceState : getPathSequenceState ,
getPrevButton : ( elements ) => elements ? . pathsDetailPrevEl ,
getNextButton : ( elements ) => elements ? . pathsDetailNextEl ,
getPositionEl : ( elements ) => elements ? . pathsDetailPositionEl ,
formatPositionText : ({ total , currentIndex }) => {
if ( total > 0 && currentIndex >= 0 ) {
return ` ${ currentIndex + 1 } of ${ total } paths` ;
}
return total > 0 ? ` ${ total } paths` : "No paths" ;
},
selectTarget : ( targetKey ) => selectNodeBySequenceKey ( targetKey , getPathDetailElements ( getElements ()))
});
return pathsDetailNavigator ;
}
function getRoseDetailNavigator () {
if ( roseDetailNavigator || typeof window . TarotSequenceNav ? . createSequenceNavigator !== "function" ) {
return roseDetailNavigator ;
}
roseDetailNavigator = window . TarotSequenceNav . createSequenceNavigator ({
getElements ,
isActive : ( elements ) => Boolean ( elements ? . crossSectionEl && elements . crossSectionEl . hidden === false ),
getSequenceState : getPathSequenceState ,
getPrevButton : ( elements ) => elements ? . roseDetailPrevEl ,
getNextButton : ( elements ) => elements ? . roseDetailNextEl ,
getPositionEl : ( elements ) => elements ? . roseDetailPositionEl ,
formatPositionText : ({ total , currentIndex }) => {
if ( total > 0 && currentIndex >= 0 ) {
return ` ${ currentIndex + 1 } of ${ total } paths` ;
}
return total > 0 ? ` ${ total } paths` : "No paths" ;
},
selectTarget : ( targetKey ) => selectNodeBySequenceKey ( targetKey , getRoseDetailElements ( getElements ()))
});
return roseDetailNavigator ;
}
function getWorldDetailNavigator () {
if ( worldsDetailNavigator || typeof window . TarotSequenceNav ? . createSequenceNavigator !== "function" ) {
return worldsDetailNavigator ;
}
worldsDetailNavigator = window . TarotSequenceNav . createSequenceNavigator ({
getElements ,
isActive : ( elements ) => Boolean ( elements ? . worldsSectionEl && elements . worldsSectionEl . hidden === false ),
getSequenceState : getWorldSequenceState ,
getPrevButton : ( elements ) => elements ? . worldsDetailPrevEl ,
getNextButton : ( elements ) => elements ? . worldsDetailNextEl ,
getPositionEl : ( elements ) => elements ? . worldsDetailPositionEl ,
formatPositionText : ({ total , currentIndex }) => {
if ( total > 0 && currentIndex >= 0 ) {
return ` ${ currentIndex + 1 } of ${ total } worlds` ;
}
return total > 0 ? ` ${ total } worlds` : "No worlds" ;
},
selectTarget : ( targetKey ) => selectNodeBySequenceKey ( targetKey , getWorldDetailElements ( getElements ()))
});
return worldsDetailNavigator ;
}
function syncDetailNavigation ( elements = getElements ()) {
getDetailNavigator () ? . sync ( elements );
}
function syncBrowserDetailNavigation ( elements = getElements ()) {
getBrowserDetailNavigator () ? . sync ( elements );
}
function syncPathsDetailNavigation ( elements = getElements ()) {
getPathsDetailNavigator () ? . sync ( elements );
}
function syncRoseDetailNavigation ( elements = getElements ()) {
getRoseDetailNavigator () ? . sync ( elements );
}
function syncWorldDetailNavigation ( elements = getElements ()) {
getWorldDetailNavigator () ? . sync ( elements );
}
function bindDetailNavigation ( elements = getElements ()) {
getDetailNavigator () ? . bind ( elements );
}
function bindBrowserDetailNavigation ( elements = getElements ()) {
getBrowserDetailNavigator () ? . bind ( elements );
}
function bindPathsDetailNavigation ( elements = getElements ()) {
getPathsDetailNavigator () ? . bind ( elements );
}
function bindRoseDetailNavigation ( elements = getElements ()) {
getRoseDetailNavigator () ? . bind ( elements );
}
function bindWorldDetailNavigation ( elements = getElements ()) {
getWorldDetailNavigator () ? . bind ( elements );
}
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" ));
}
2026-05-28 18:19:13 -07:00
function renderSephiraDetailIntoElements ( seph , tree , elements , options = {}) {
2026-03-07 05:17:50 -08:00
if ( typeof kabbalahDetailUi . renderSephiraDetail === "function" ) {
kabbalahDetailUi . renderSephiraDetail ( getDetailRenderContext ( tree , elements , {
seph ,
2026-05-28 18:19:13 -07:00
onPathSelect : typeof options . onPathSelect === "function"
? options . onPathSelect
: null
2026-03-07 05:17:50 -08:00
}));
2026-03-07 01:09:00 -08:00
}
}
2026-05-28 18:19:13 -07:00
function renderPathDetailIntoElements ( path , tree , elements ) {
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-05-28 18:19:13 -07:00
function renderWorldLayerDetailIntoElements ( worldLayer , tree , elements , worldIndex ) {
if ( typeof kabbalahDetailUi . renderWorldLayerDetail === "function" ) {
kabbalahDetailUi . renderWorldLayerDetail ( getDetailRenderContext ( tree , elements , {
worldLayer ,
worldIndex
}));
}
}
function renderSephiraDetail ( seph , tree , elements ) {
state . selectedSephiraNumber = Number ( seph ? . number );
if ( buildSephiraKey ( seph ? . number ) !== "sephira:daath" ) {
state . activeNodeKey = buildSephiraKey ( seph ? . number );
}
clearHighlights ();
document . querySelectorAll ( `.kab-node[data-sephira=" ${ seph . number } "], .kab-node-glow[data-sephira=" ${ seph . number } "]` )
. forEach ( el => el . classList . add ( "kab-node-active" ));
const allElements = getElements ();
const treeElements = getTreeDetailElements ( allElements );
const browserElements = getBrowserDetailElements ( allElements );
if ( treeElements ? . detailBodyEl ) {
renderSephiraDetailIntoElements ( seph , tree , treeElements , {
onPathSelect : ( path ) => renderPathDetail ( path , tree , treeElements )
});
}
if ( browserElements ? . detailBodyEl ) {
renderSephiraDetailIntoElements ( seph , tree , browserElements , {
onPathSelect : ( path ) => {
document . dispatchEvent ( new CustomEvent ( "nav:kabbalah-path" , {
detail : { pathNo : Number ( path ? . pathNumber ) }
}));
}
});
}
syncDetailNavigation ();
syncBrowserDetailNavigation ();
syncBrowserListSelection ();
}
function renderPathDetail ( path , tree , elements ) {
state . selectedPathNumber = Number ( path ? . pathNumber );
state . activeNodeKey = buildPathKey ( path ? . pathNumber );
clearHighlights ();
document . querySelectorAll ( `[data-path=" ${ path . pathNumber } "]` )
. forEach ( el => el . classList . add ( "kab-path-active" ));
const allElements = getElements ();
const treeElements = getTreeDetailElements ( allElements );
const pathElements = getPathDetailElements ( allElements );
const roseElements = getRoseDetailElements ( allElements );
if ( treeElements ? . detailBodyEl ) {
renderPathDetailIntoElements ( path , tree , treeElements );
}
if ( pathElements ? . detailBodyEl ) {
renderPathDetailIntoElements ( path , tree , pathElements );
}
if ( roseElements ? . detailBodyEl ) {
renderPathDetailIntoElements ( path , tree , roseElements );
}
syncDetailNavigation ();
syncPathsDetailNavigation ();
syncRoseDetailNavigation ();
syncPathsListSelection ();
syncBrowserListSelection ();
}
function renderWorldLayerDetail ( worldLayer , worldIndex , tree , elements ) {
state . selectedWorldLayerIndex = Number ( worldIndex );
const worldElements = getWorldDetailElements ( getElements ());
if ( worldElements ? . detailBodyEl ) {
renderWorldLayerDetailIntoElements ( worldLayer , tree , worldElements , worldIndex );
}
syncWorldDetailNavigation ();
syncWorldListSelection ();
}
2026-03-07 05:17:50 -08:00
function renderRoseLandingIntro ( roseElements ) {
if ( typeof kabbalahDetailUi . renderRoseLandingIntro === "function" ) {
kabbalahDetailUi . renderRoseLandingIntro ( roseElements );
}
}
2026-05-28 18:19:13 -07:00
function renderBrowserCurrentSelection ( elements ) {
if ( ! state . tree ) {
return ;
}
const browserElements = getBrowserDetailElements ( elements );
if ( ! browserElements ? . detailBodyEl ) {
return ;
}
const selectedSephira = getSephiraByNumber ( state . selectedSephiraNumber );
if ( selectedSephira ) {
renderSephiraDetail ( selectedSephira , state . tree , browserElements );
return ;
}
const fallbackSephira = getSephiraByNumber ( getSephirotSequenceEntries ()[ 0 ] ? . number );
if ( fallbackSephira ) {
renderSephiraDetail ( fallbackSephira , state . tree , browserElements );
}
}
function renderPathsCurrentSelection ( elements ) {
if ( ! state . tree ) {
return ;
}
const pathElements = getPathDetailElements ( elements );
if ( ! pathElements ? . detailBodyEl ) {
return ;
}
if ( hasFiniteSelectionNumber ( state . selectedPathNumber )) {
const selectedPath = getPathByNumber ( state . selectedPathNumber );
if ( selectedPath ) {
renderPathDetail ( selectedPath , state . tree , pathElements );
return ;
}
}
const fallbackPath = getPathByNumber ( getPathSequenceEntries ()[ 0 ] ? . number );
if ( fallbackPath ) {
renderPathDetail ( fallbackPath , state . tree , pathElements );
}
}
function renderWorldCurrentSelection ( elements ) {
const worldElements = getWorldDetailElements ( elements );
if ( ! worldElements ? . detailBodyEl ) {
return ;
}
const selectedWorld = getWorldLayerByIndex ( state . selectedWorldLayerIndex );
if ( selectedWorld ) {
renderWorldLayerDetail ( selectedWorld , state . selectedWorldLayerIndex , state . tree , worldElements );
return ;
}
const fallbackWorld = getWorldLayerByIndex ( 0 );
if ( fallbackWorld ) {
renderWorldLayerDetail ( fallbackWorld , 0 , state . tree , worldElements );
}
}
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 ;
}
2026-05-28 18:19:13 -07:00
if ( hasFiniteSelectionNumber ( state . selectedPathNumber )) {
const selectedPath = getPathByNumber ( state . selectedPathNumber );
2026-03-07 05:17:50 -08:00
if ( selectedPath ) {
renderPathDetail ( selectedPath , state . tree , roseElements );
return ;
}
}
renderRoseLandingIntro ( roseElements );
2026-05-28 18:19:13 -07:00
syncRoseDetailNavigation ( elements );
2026-03-07 05:17:50 -08:00
}
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 ;
}
2026-05-28 18:19:13 -07:00
const activeNodeKey = String ( state . activeNodeKey || "" ). trim ();
if ( activeNodeKey . startsWith ( "path:" )) {
const selectedPath = getPathByNumber ( Number ( activeNodeKey . split ( ":" )[ 1 ]));
2026-03-07 01:09:00 -08:00
if ( selectedPath ) {
renderPathDetail ( selectedPath , state . tree , elements );
return ;
}
}
2026-05-28 18:19:13 -07:00
if ( activeNodeKey . startsWith ( "sephira:" ) && ! activeNodeKey . endsWith ( ":daath" )) {
const selectedSephira = getSephiraByNumber ( Number ( activeNodeKey . split ( ":" )[ 1 ]));
2026-03-07 01:09:00 -08:00
if ( selectedSephira ) {
renderSephiraDetail ( selectedSephira , state . tree , elements );
return ;
}
}
renderSephiraDetail ( state . tree . sephiroth [ 0 ], state . tree , elements );
}
2026-05-28 18:19:13 -07:00
function renderVisibleKabbalahViews ( elements = getElements ()) {
renderBrowserList ( elements );
renderWorldsList ( elements );
renderPathsList ( elements );
if ( elements . browserSectionEl && elements . browserSectionEl . hidden === false ) {
renderBrowserCurrentSelection ( elements );
} else {
syncBrowserListSelection ( elements );
syncBrowserDetailNavigation ( elements );
}
if ( elements . worldsSectionEl && elements . worldsSectionEl . hidden === false ) {
renderWorldCurrentSelection ( elements );
} else {
syncWorldListSelection ( elements );
syncWorldDetailNavigation ( elements );
}
if ( elements . pathsSectionEl && elements . pathsSectionEl . hidden === false ) {
renderPathsCurrentSelection ( elements );
} else {
syncPathsListSelection ( elements );
syncPathsDetailNavigation ( elements );
}
if ( elements . crossSectionEl && elements . crossSectionEl . hidden === false ) {
renderRoseCross ( elements );
renderRoseCurrentSelection ( elements );
} else {
syncRoseDetailNavigation ( elements );
}
if ( elements . sectionEl && elements . sectionEl . hidden === false ) {
renderTree ( elements );
renderCurrentSelection ( elements );
} else {
syncDetailNavigation ( elements );
}
}
2026-03-07 01:09:00 -08:00
// ─── 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 );
2026-05-28 18:19:13 -07:00
if ( ! hasFiniteSelectionNumber ( state . selectedWorldLayerIndex )
|| Number ( state . selectedWorldLayerIndex ) < 0
|| Number ( state . selectedWorldLayerIndex ) >= state . fourWorldLayers . length ) {
state . selectedWorldLayerIndex = 0 ;
}
if ( ! hasFiniteSelectionNumber ( state . selectedSephiraNumber )) {
state . selectedSephiraNumber = Number ( state . tree . sephiroth [ 0 ] ? . number || 1 );
}
if ( ! String ( state . activeNodeKey || "" ). trim ()) {
state . activeNodeKey = buildSephiraKey ( state . selectedSephiraNumber );
}
2026-03-07 01:09:00 -08:00
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-05-28 18:19:13 -07:00
bindDetailNavigation ( elements );
bindBrowserDetailNavigation ( elements );
bindPathsDetailNavigation ( elements );
bindWorldDetailNavigation ( elements );
bindRoseDetailNavigation ( elements );
bindBrowserList ( elements );
bindWorldList ( elements );
bindPathsList ( elements );
2026-03-07 01:09:00 -08:00
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-05-28 18:19:13 -07:00
renderVisibleKabbalahViews ( elements );
2026-03-07 01:09:00 -08:00
}
function selectPathByNumber ( pathNumber ) {
if ( ! state . initialized || ! state . tree ) return ;
const el = getElements ();
2026-05-28 18:19:13 -07:00
const path = getPathByNumber ( pathNumber );
2026-03-07 01:09:00 -08:00
if ( path ) renderPathDetail ( path , state . tree , el );
}
function selectSephiraByNumber ( n ) {
if ( ! state . initialized || ! state . tree ) return ;
const el = getElements ();
2026-05-28 18:19:13 -07:00
const seph = getSephiraByNumber ( n );
2026-03-07 01:09:00 -08:00
if ( seph ) renderSephiraDetail ( seph , state . tree , el );
}
// select sephirah (1-10) or path (11+) by a single number
function selectNode ( n ) {
2026-05-28 18:19:13 -07:00
if ( isDaathToken ( n ) || Number ( n ) === 0 ) selectSephiraByNumber ( 0 );
else if ( n >= 1 && n <= 10 ) selectSephiraByNumber ( n );
2026-03-07 01:09:00 -08:00
else selectPathByNumber ( n );
}
// ─── public API ────────────────────────────────────────────────────────
function ensureKabbalahSection ( magickDataset ) {
const elements = getElements ();
2026-05-28 18:19:13 -07:00
if ( state . initialized ) {
renderVisibleKabbalahViews ( elements );
return ;
}
state . initialized = true ;
2026-03-07 01:09:00 -08:00
init ( magickDataset , elements );
}
window . KabbalahSectionUi = { ensureKabbalahSection , selectPathByNumber , selectSephiraByNumber , selectNode };
})();