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 } ;
} ) ( ) ;