2026-03-07 01:09:00 -08:00
( function ( ) {
2026-03-08 22:24:34 -07:00
const dataService = window . TarotDataService || { } ;
2026-03-08 06:22:27 -07:00
const {
resolveTarotCardImage ,
resolveTarotCardThumbnail ,
getTarotCardDisplayName ,
getTarotCardSearchAliases ,
getDeckOptions ,
getActiveDeck
} = window . TarotCardImages || { } ;
2026-03-07 05:17:50 -08:00
const tarotHouseUi = window . TarotHouseUi || { } ;
const tarotRelationsUi = window . TarotRelationsUi || { } ;
2026-03-07 13:38:13 -08:00
const tarotCardDerivations = window . TarotCardDerivations || { } ;
const tarotDetailUi = window . TarotDetailUi || { } ;
const tarotRelationDisplay = window . TarotRelationDisplay || { } ;
2026-03-07 01:09:00 -08:00
const state = {
initialized : false ,
cards : [ ] ,
filteredCards : [ ] ,
searchQuery : "" ,
selectedCardId : "" ,
2026-03-08 03:52:25 -07:00
houseFocusMode : false ,
houseTopCardsVisible : true ,
houseTopInfoModes : {
hebrew : true ,
planet : true ,
zodiac : true ,
trump : true ,
path : true
} ,
houseBottomCardsVisible : true ,
houseBottomInfoModes : {
zodiac : true ,
decan : true ,
month : true ,
ruler : true ,
date : false
} ,
houseExportInProgress : false ,
houseExportFormat : "png" ,
2026-03-07 01:09:00 -08:00
magickDataset : null ,
referenceData : null ,
monthRefsByCardId : new Map ( ) ,
2026-03-08 22:24:34 -07:00
courtCardByDecanId : new Map ( ) ,
loadingPromise : null
2026-03-07 01:09:00 -08:00
} ;
const TAROT _TRUMP _NUMBER _BY _NAME = {
"the fool" : 0 ,
fool : 0 ,
"the magus" : 1 ,
magus : 1 ,
magician : 1 ,
"the high priestess" : 2 ,
"high priestess" : 2 ,
"the empress" : 3 ,
empress : 3 ,
"the emperor" : 4 ,
emperor : 4 ,
"the hierophant" : 5 ,
hierophant : 5 ,
"the lovers" : 6 ,
lovers : 6 ,
"the chariot" : 7 ,
chariot : 7 ,
strength : 8 ,
lust : 8 ,
"the hermit" : 9 ,
hermit : 9 ,
fortune : 10 ,
"wheel of fortune" : 10 ,
justice : 11 ,
"the hanged man" : 12 ,
"hanged man" : 12 ,
death : 13 ,
temperance : 14 ,
art : 14 ,
"the devil" : 15 ,
devil : 15 ,
"the tower" : 16 ,
tower : 16 ,
"the star" : 17 ,
star : 17 ,
"the moon" : 18 ,
moon : 18 ,
"the sun" : 19 ,
sun : 19 ,
aeon : 20 ,
judgement : 20 ,
judgment : 20 ,
universe : 21 ,
world : 21 ,
"the world" : 21
} ;
const MINOR _NUMBER _WORD _BY _VALUE = {
1 : "ace" ,
2 : "two" ,
3 : "three" ,
4 : "four" ,
5 : "five" ,
6 : "six" ,
7 : "seven" ,
8 : "eight" ,
9 : "nine" ,
10 : "ten"
} ;
const HEBREW _LETTER _ALIASES = {
aleph : "alef" ,
alef : "alef" ,
heh : "he" ,
he : "he" ,
beth : "bet" ,
bet : "bet" ,
cheth : "het" ,
chet : "het" ,
kaph : "kaf" ,
kaf : "kaf" ,
peh : "pe" ,
tzaddi : "tsadi" ,
tzadi : "tsadi" ,
tsadi : "tsadi" ,
qoph : "qof" ,
qof : "qof" ,
taw : "tav" ,
tau : "tav"
} ;
const CUBE _MOTHER _CONNECTOR _BY _LETTER = {
alef : { connectorId : "above-below" , connectorName : "Above ↔ Below" } ,
mem : { connectorId : "east-west" , connectorName : "East ↔ West" } ,
shin : { connectorId : "south-north" , connectorName : "South ↔ North" }
} ;
const ELEMENT _NAME _BY _ID = {
water : "Water" ,
fire : "Fire" ,
air : "Air" ,
earth : "Earth"
} ;
const ELEMENT _HEBREW _LETTER _BY _ID = {
fire : "Yod" ,
water : "Heh" ,
air : "Vav" ,
earth : "Heh"
} ;
const ELEMENT _HEBREW _CHAR _BY _ID = {
fire : "י " ,
water : "ה" ,
air : "ו " ,
earth : "ה"
} ;
const HEBREW _LETTER _ID _BY _TETRAGRAMMATON _LETTER = {
yod : "yod" ,
heh : "he" ,
vav : "vav"
} ;
const ACE _ELEMENT _BY _CARD _NAME = {
"ace of cups" : "water" ,
"ace of wands" : "fire" ,
"ace of swords" : "air" ,
"ace of disks" : "earth"
} ;
const COURT _ELEMENT _BY _RANK = {
knight : "fire" ,
queen : "water" ,
prince : "air" ,
princess : "earth"
} ;
const SUIT _ELEMENT _BY _SUIT = {
wands : "fire" ,
cups : "water" ,
swords : "air" ,
disks : "earth"
} ;
const MINOR _RANK _NUMBER _BY _NAME = {
ace : 1 ,
two : 2 ,
three : 3 ,
four : 4 ,
five : 5 ,
six : 6 ,
seven : 7 ,
eight : 8 ,
nine : 9 ,
ten : 10
} ;
const SMALL _CARD _SIGN _BY _MODALITY _AND _SUIT = {
cardinal : {
wands : "aries" ,
cups : "cancer" ,
swords : "libra" ,
disks : "capricorn"
} ,
fixed : {
wands : "leo" ,
cups : "scorpio" ,
swords : "aquarius" ,
disks : "taurus"
} ,
mutable : {
wands : "sagittarius" ,
cups : "pisces" ,
swords : "gemini" ,
disks : "virgo"
}
} ;
2026-03-07 14:15:09 -08:00
const MINOR _PLURAL _BY _RANK = {
ace : "aces" ,
two : "twos" ,
three : "threes" ,
four : "fours" ,
five : "fives" ,
six : "sixes" ,
seven : "sevens" ,
eight : "eights" ,
nine : "nines" ,
ten : "tens"
} ;
2026-03-07 01:09:00 -08:00
function slugify ( value ) {
return String ( value || "" )
. trim ( )
. toLowerCase ( )
. replace ( /[^a-z0-9]+/g , "-" )
. replace ( /(^-|-$)/g , "" ) ;
}
function cardId ( card ) {
const suitPart = card . suit ? ` - ${ slugify ( card . suit ) } ` : "" ;
return ` ${ slugify ( card . arcana ) } ${ suitPart } - ${ slugify ( card . name ) } ` ;
}
function getElements ( ) {
return {
2026-03-12 21:01:32 -07:00
tarotSectionEl : document . getElementById ( "tarot-section" ) ,
tarotHouseSectionEl : document . getElementById ( "tarot-house-section" ) ,
2026-03-07 01:09:00 -08:00
tarotCardListEl : document . getElementById ( "tarot-card-list" ) ,
tarotSearchInputEl : document . getElementById ( "tarot-search-input" ) ,
tarotSearchClearEl : document . getElementById ( "tarot-search-clear" ) ,
tarotCountEl : document . getElementById ( "tarot-card-count" ) ,
tarotDetailImageEl : document . getElementById ( "tarot-detail-image" ) ,
tarotDetailNameEl : document . getElementById ( "tarot-detail-name" ) ,
tarotDetailTypeEl : document . getElementById ( "tarot-detail-type" ) ,
tarotDetailSummaryEl : document . getElementById ( "tarot-detail-summary" ) ,
tarotDetailUprightEl : document . getElementById ( "tarot-detail-upright" ) ,
tarotDetailReversedEl : document . getElementById ( "tarot-detail-reversed" ) ,
tarotMetaMeaningCardEl : document . getElementById ( "tarot-meta-meaning-card" ) ,
tarotDetailMeaningEl : document . getElementById ( "tarot-detail-meaning" ) ,
tarotDetailKeywordsEl : document . getElementById ( "tarot-detail-keywords" ) ,
tarotMetaPlanetCardEl : document . getElementById ( "tarot-meta-planet-card" ) ,
tarotMetaElementCardEl : document . getElementById ( "tarot-meta-element-card" ) ,
tarotMetaTetragrammatonCardEl : document . getElementById ( "tarot-meta-tetragrammaton-card" ) ,
tarotMetaZodiacCardEl : document . getElementById ( "tarot-meta-zodiac-card" ) ,
tarotMetaCourtDateCardEl : document . getElementById ( "tarot-meta-courtdate-card" ) ,
tarotMetaHebrewCardEl : document . getElementById ( "tarot-meta-hebrew-card" ) ,
tarotMetaCubeCardEl : document . getElementById ( "tarot-meta-cube-card" ) ,
2026-03-07 16:13:58 -08:00
tarotMetaIChingCardEl : document . getElementById ( "tarot-meta-iching-card" ) ,
2026-03-07 01:09:00 -08:00
tarotMetaCalendarCardEl : document . getElementById ( "tarot-meta-calendar-card" ) ,
tarotDetailPlanetEl : document . getElementById ( "tarot-detail-planet" ) ,
tarotDetailElementEl : document . getElementById ( "tarot-detail-element" ) ,
tarotDetailTetragrammatonEl : document . getElementById ( "tarot-detail-tetragrammaton" ) ,
tarotDetailZodiacEl : document . getElementById ( "tarot-detail-zodiac" ) ,
tarotDetailCourtDateEl : document . getElementById ( "tarot-detail-courtdate" ) ,
tarotDetailHebrewEl : document . getElementById ( "tarot-detail-hebrew" ) ,
tarotDetailCubeEl : document . getElementById ( "tarot-detail-cube" ) ,
2026-03-07 16:13:58 -08:00
tarotDetailIChingEl : document . getElementById ( "tarot-detail-iching" ) ,
2026-03-07 01:09:00 -08:00
tarotDetailCalendarEl : document . getElementById ( "tarot-detail-calendar" ) ,
tarotKabPathEl : document . getElementById ( "tarot-kab-path" ) ,
2026-03-08 03:52:25 -07:00
tarotHouseOfCardsEl : document . getElementById ( "tarot-house-of-cards" ) ,
tarotBrowseViewEl : document . getElementById ( "tarot-browse-view" ) ,
2026-03-12 21:01:32 -07:00
tarotHouseViewEl : document . getElementById ( "tarot-house-view" ) ,
2026-03-08 03:52:25 -07:00
tarotHouseTopCardsVisibleEl : document . getElementById ( "tarot-house-top-cards-visible" ) ,
tarotHouseTopInfoHebrewEl : document . getElementById ( "tarot-house-top-info-hebrew" ) ,
tarotHouseTopInfoPlanetEl : document . getElementById ( "tarot-house-top-info-planet" ) ,
tarotHouseTopInfoZodiacEl : document . getElementById ( "tarot-house-top-info-zodiac" ) ,
tarotHouseTopInfoTrumpEl : document . getElementById ( "tarot-house-top-info-trump" ) ,
tarotHouseTopInfoPathEl : document . getElementById ( "tarot-house-top-info-path" ) ,
tarotHouseBottomCardsVisibleEl : document . getElementById ( "tarot-house-bottom-cards-visible" ) ,
tarotHouseBottomInfoZodiacEl : document . getElementById ( "tarot-house-bottom-info-zodiac" ) ,
tarotHouseBottomInfoDecanEl : document . getElementById ( "tarot-house-bottom-info-decan" ) ,
tarotHouseBottomInfoMonthEl : document . getElementById ( "tarot-house-bottom-info-month" ) ,
tarotHouseBottomInfoRulerEl : document . getElementById ( "tarot-house-bottom-info-ruler" ) ,
tarotHouseBottomInfoDateEl : document . getElementById ( "tarot-house-bottom-info-date" ) ,
tarotHouseFocusToggleEl : document . getElementById ( "tarot-house-focus-toggle" ) ,
tarotHouseExportEl : document . getElementById ( "tarot-house-export" ) ,
tarotHouseExportWebpEl : document . getElementById ( "tarot-house-export-webp" )
2026-03-07 01:09:00 -08:00
} ;
}
2026-03-08 03:52:25 -07:00
function setHouseBottomInfoCheckboxState ( checkbox , enabled ) {
if ( ! checkbox ) {
return ;
}
checkbox . checked = Boolean ( enabled ) ;
checkbox . disabled = Boolean ( state . houseExportInProgress ) ;
}
2026-03-07 01:09:00 -08:00
function normalizeRelationId ( value ) {
return String ( value || "" )
. trim ( )
. toLowerCase ( )
. replace ( /[^a-z0-9]+/g , "-" )
. replace ( /(^-|-$)/g , "" ) ;
}
2026-03-07 13:38:13 -08:00
if ( typeof tarotRelationDisplay . createTarotRelationDisplay !== "function" ) {
throw new Error ( "TarotRelationDisplay.createTarotRelationDisplay is unavailable. Ensure app/ui-tarot-relation-display.js loads before app/ui-tarot.js." ) ;
}
if ( typeof tarotCardDerivations . createTarotCardDerivations !== "function" ) {
throw new Error ( "TarotCardDerivations.createTarotCardDerivations is unavailable. Ensure app/ui-tarot-card-derivations.js loads before app/ui-tarot.js." ) ;
}
if ( typeof tarotDetailUi . createTarotDetailRenderer !== "function" ) {
throw new Error ( "TarotDetailUi.createTarotDetailRenderer is unavailable. Ensure app/ui-tarot-detail.js loads before app/ui-tarot.js." ) ;
}
const tarotCardDerivationsUi = tarotCardDerivations . createTarotCardDerivations ( {
normalizeRelationId ,
normalizeTarotCardLookupName ,
toTitleCase ,
getReferenceData : ( ) => state . referenceData ,
ELEMENT _NAME _BY _ID ,
ELEMENT _HEBREW _LETTER _BY _ID ,
ELEMENT _HEBREW _CHAR _BY _ID ,
HEBREW _LETTER _ID _BY _TETRAGRAMMATON _LETTER ,
ACE _ELEMENT _BY _CARD _NAME ,
COURT _ELEMENT _BY _RANK ,
MINOR _RANK _NUMBER _BY _NAME ,
SMALL _CARD _SIGN _BY _MODALITY _AND _SUIT ,
MINOR _PLURAL _BY _RANK
} ) ;
const tarotRelationDisplayUi = tarotRelationDisplay . createTarotRelationDisplay ( {
normalizeRelationId
} ) ;
const tarotDetailRenderer = tarotDetailUi . createTarotDetailRenderer ( {
getMonthRefsByCardId : ( ) => state . monthRefsByCardId ,
getMagickDataset : ( ) => state . magickDataset ,
resolveTarotCardImage ,
2026-03-08 05:40:53 -07:00
resolveTarotCardThumbnail ,
2026-03-07 13:38:13 -08:00
getDisplayCardName ,
buildTypeLabel ,
clearChildren ,
normalizeRelationObject ,
buildElementRelationsForCard ,
buildTetragrammatonRelationsForCard ,
buildSmallCardRulershipRelation ,
buildSmallCardCourtLinkRelations ,
buildCubeRelationsForCard ,
2026-03-07 16:13:58 -08:00
buildIChingRelationsForCard ,
2026-03-07 13:38:13 -08:00
parseMonthDayToken ,
createRelationListItem ,
findSephirahForMinorCard
} ) ;
2026-03-07 01:09:00 -08:00
function normalizeSearchValue ( value ) {
return String ( value || "" ) . trim ( ) . toLowerCase ( ) ;
}
function normalizeTarotName ( value ) {
return String ( value || "" )
. trim ( )
. toLowerCase ( )
. replace ( /\s+/g , " " ) ;
}
function normalizeTarotCardLookupName ( value ) {
const text = normalizeTarotName ( value )
. replace ( /\b(pentacles?|coins?)\b/g , "disks" ) ;
const match = text . match ( /^(\d{1,2})\s+of\s+(.+)$/i ) ;
if ( ! match ) {
return text ;
}
const numeric = Number ( match [ 1 ] ) ;
const suit = String ( match [ 2 ] || "" ) . trim ( ) ;
const rankWord = MINOR _NUMBER _WORD _BY _VALUE [ numeric ] ;
if ( ! rankWord || ! suit ) {
return text ;
}
return ` ${ rankWord } of ${ suit } ` ;
}
function getDisplayCardName ( cardOrName , trumpNumber ) {
const cardName = typeof cardOrName === "object"
? String ( cardOrName ? . name || "" )
: String ( cardOrName || "" ) ;
const resolvedTrumpNumber = typeof cardOrName === "object"
? cardOrName ? . number
: trumpNumber ;
if ( typeof getTarotCardDisplayName === "function" ) {
const display = String ( getTarotCardDisplayName ( cardName , { trumpNumber : resolvedTrumpNumber } ) || "" ) . trim ( ) ;
if ( display ) {
return display ;
}
}
return cardName . trim ( ) ;
}
function toTitleCase ( value ) {
return String ( value || "" )
. split ( " " )
. filter ( Boolean )
. map ( ( part ) => part . charAt ( 0 ) . toUpperCase ( ) + part . slice ( 1 ) )
. join ( " " ) ;
}
function buildElementRelationsForCard ( card , baseElementRelations = [ ] ) {
2026-03-07 13:38:13 -08:00
return tarotCardDerivationsUi . buildElementRelationsForCard ( card , baseElementRelations ) ;
2026-03-07 01:09:00 -08:00
}
function buildTetragrammatonRelationsForCard ( card ) {
2026-03-07 13:38:13 -08:00
return tarotCardDerivationsUi . buildTetragrammatonRelationsForCard ( card ) ;
2026-03-07 01:09:00 -08:00
}
function buildSmallCardRulershipRelation ( card ) {
2026-03-07 13:38:13 -08:00
return tarotCardDerivationsUi . buildSmallCardRulershipRelation ( card ) ;
2026-03-07 01:09:00 -08:00
}
function buildCourtCardByDecanId ( cards ) {
2026-03-07 05:17:50 -08:00
if ( typeof tarotRelationsUi . buildCourtCardByDecanId !== "function" ) {
return new Map ( ) ;
}
return tarotRelationsUi . buildCourtCardByDecanId ( cards ) ;
2026-03-07 01:09:00 -08:00
}
function buildSmallCardCourtLinkRelations ( card , relations ) {
2026-03-07 05:17:50 -08:00
if ( typeof tarotRelationsUi . buildSmallCardCourtLinkRelations !== "function" ) {
2026-03-07 01:09:00 -08:00
return [ ] ;
}
2026-03-07 05:17:50 -08:00
return tarotRelationsUi . buildSmallCardCourtLinkRelations ( card , relations , state . courtCardByDecanId ) ;
2026-03-07 01:09:00 -08:00
}
function parseMonthDayToken ( value ) {
2026-03-07 05:17:50 -08:00
if ( typeof tarotRelationsUi . parseMonthDayToken !== "function" ) {
2026-03-07 01:09:00 -08:00
return null ;
}
2026-03-07 05:17:50 -08:00
return tarotRelationsUi . parseMonthDayToken ( value ) ;
2026-03-07 01:09:00 -08:00
}
function buildMonthReferencesByCard ( referenceData , cards ) {
2026-03-07 05:17:50 -08:00
if ( typeof tarotRelationsUi . buildMonthReferencesByCard !== "function" ) {
return new Map ( ) ;
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
return tarotRelationsUi . buildMonthReferencesByCard ( referenceData , cards ) ;
2026-03-07 01:09:00 -08:00
}
function relationToSearchText ( relation ) {
if ( ! relation ) {
return "" ;
}
if ( typeof relation === "string" ) {
return relation ;
}
const relationParts = [
relation . label ,
relation . type ,
relation . id ,
relation . data && typeof relation . data === "object"
? Object . values ( relation . data ) . join ( " " )
: ""
] ;
return relationParts . filter ( Boolean ) . join ( " " ) ;
}
function buildCardSearchText ( card ) {
const displayName = getDisplayCardName ( card ) ;
const tarotAliases = typeof getTarotCardSearchAliases === "function"
? getTarotCardSearchAliases ( card ? . name , { trumpNumber : card ? . number } )
: [ ] ;
const parts = [
card . name ,
displayName ,
card . arcana ,
card . rank ,
card . suit ,
card . summary ,
... tarotAliases ,
... ( Array . isArray ( card . keywords ) ? card . keywords : [ ] ) ,
... ( Array . isArray ( card . relations ) ? card . relations . map ( relationToSearchText ) : [ ] )
] ;
return normalizeSearchValue ( parts . join ( " " ) ) ;
}
function applySearchFilter ( elements ) {
const query = normalizeSearchValue ( state . searchQuery ) ;
state . filteredCards = query
? state . cards . filter ( ( card ) => buildCardSearchText ( card ) . includes ( query ) )
: [ ... state . cards ] ;
if ( elements ? . tarotSearchClearEl ) {
elements . tarotSearchClearEl . disabled = ! query ;
}
renderList ( elements ) ;
if ( ! state . filteredCards . some ( ( card ) => card . id === state . selectedCardId ) ) {
if ( state . filteredCards . length > 0 ) {
selectCardById ( state . filteredCards [ 0 ] . id , elements ) ;
}
return ;
}
updateListSelection ( elements ) ;
}
function clearChildren ( element ) {
if ( element ) {
element . replaceChildren ( ) ;
}
}
function updateHouseSelection ( elements ) {
2026-03-07 05:17:50 -08:00
tarotHouseUi . updateSelection ? . ( elements ) ;
2026-03-07 01:09:00 -08:00
}
function renderHouseOfCards ( elements ) {
2026-03-07 05:17:50 -08:00
tarotHouseUi . render ? . ( elements ) ;
2026-03-07 01:09:00 -08:00
}
2026-03-08 03:52:25 -07:00
function syncHouseControls ( elements ) {
2026-03-12 21:01:32 -07:00
if ( elements ? . tarotHouseViewEl ) {
elements . tarotHouseViewEl . classList . toggle ( "is-house-focus" , Boolean ( state . houseFocusMode ) ) ;
2026-03-08 03:52:25 -07:00
}
if ( elements ? . tarotHouseTopCardsVisibleEl ) {
elements . tarotHouseTopCardsVisibleEl . checked = Boolean ( state . houseTopCardsVisible ) ;
elements . tarotHouseTopCardsVisibleEl . disabled = Boolean ( state . houseExportInProgress ) ;
}
setHouseBottomInfoCheckboxState ( elements ? . tarotHouseTopInfoHebrewEl , state . houseTopInfoModes . hebrew ) ;
setHouseBottomInfoCheckboxState ( elements ? . tarotHouseTopInfoPlanetEl , state . houseTopInfoModes . planet ) ;
setHouseBottomInfoCheckboxState ( elements ? . tarotHouseTopInfoZodiacEl , state . houseTopInfoModes . zodiac ) ;
setHouseBottomInfoCheckboxState ( elements ? . tarotHouseTopInfoTrumpEl , state . houseTopInfoModes . trump ) ;
setHouseBottomInfoCheckboxState ( elements ? . tarotHouseTopInfoPathEl , state . houseTopInfoModes . path ) ;
if ( elements ? . tarotHouseBottomCardsVisibleEl ) {
elements . tarotHouseBottomCardsVisibleEl . checked = Boolean ( state . houseBottomCardsVisible ) ;
elements . tarotHouseBottomCardsVisibleEl . disabled = Boolean ( state . houseExportInProgress ) ;
}
setHouseBottomInfoCheckboxState ( elements ? . tarotHouseBottomInfoZodiacEl , state . houseBottomInfoModes . zodiac ) ;
setHouseBottomInfoCheckboxState ( elements ? . tarotHouseBottomInfoDecanEl , state . houseBottomInfoModes . decan ) ;
setHouseBottomInfoCheckboxState ( elements ? . tarotHouseBottomInfoMonthEl , state . houseBottomInfoModes . month ) ;
setHouseBottomInfoCheckboxState ( elements ? . tarotHouseBottomInfoRulerEl , state . houseBottomInfoModes . ruler ) ;
setHouseBottomInfoCheckboxState ( elements ? . tarotHouseBottomInfoDateEl , state . houseBottomInfoModes . date ) ;
if ( elements ? . tarotHouseFocusToggleEl ) {
elements . tarotHouseFocusToggleEl . setAttribute ( "aria-pressed" , state . houseFocusMode ? "true" : "false" ) ;
elements . tarotHouseFocusToggleEl . textContent = state . houseFocusMode ? "Show Full Tarot" : "Focus House" ;
}
if ( elements ? . tarotHouseExportEl ) {
elements . tarotHouseExportEl . disabled = Boolean ( state . houseExportInProgress ) ;
elements . tarotHouseExportEl . textContent = state . houseExportInProgress ? "Exporting..." : "Export PNG" ;
}
if ( elements ? . tarotHouseExportWebpEl ) {
const supportsWebp = tarotHouseUi . isExportFormatSupported ? . ( "webp" ) === true ;
elements . tarotHouseExportWebpEl . disabled = Boolean ( state . houseExportInProgress ) || ! supportsWebp ;
elements . tarotHouseExportWebpEl . hidden = ! supportsWebp ;
elements . tarotHouseExportWebpEl . textContent = state . houseExportInProgress && state . houseExportFormat === "webp"
? "Exporting..."
: "Export WebP" ;
if ( supportsWebp ) {
elements . tarotHouseExportWebpEl . title = "Smaller file size, but not guaranteed lossless like PNG." ;
}
}
}
async function exportHouseOfCards ( elements , format = "png" ) {
if ( state . houseExportInProgress ) {
return ;
}
state . houseExportInProgress = true ;
state . houseExportFormat = format ;
syncHouseControls ( elements ) ;
try {
await tarotHouseUi . exportImage ? . ( format ) ;
} catch ( error ) {
window . alert ( error instanceof Error ? error . message : "Unable to export the House of Cards image." ) ;
} finally {
state . houseExportInProgress = false ;
state . houseExportFormat = "png" ;
syncHouseControls ( elements ) ;
}
}
2026-03-07 01:09:00 -08:00
function buildTypeLabel ( card ) {
2026-03-07 13:38:13 -08:00
return tarotCardDerivationsUi . buildTypeLabel ( card ) ;
2026-03-07 01:09:00 -08:00
}
function findSephirahForMinorCard ( card , kabTree ) {
2026-03-07 13:38:13 -08:00
return tarotCardDerivationsUi . findSephirahForMinorCard ( card , kabTree ) ;
2026-03-07 01:09:00 -08:00
}
function formatRelation ( relation ) {
2026-03-07 13:38:13 -08:00
return tarotRelationDisplayUi . formatRelation ( relation ) ;
2026-03-07 01:09:00 -08:00
}
function relationKey ( relation , index ) {
2026-03-07 13:38:13 -08:00
return tarotRelationDisplayUi . relationKey ( relation , index ) ;
2026-03-07 01:09:00 -08:00
}
function normalizeRelationObject ( relation , index ) {
2026-03-07 13:38:13 -08:00
return tarotRelationDisplayUi . normalizeRelationObject ( relation , index ) ;
2026-03-07 01:09:00 -08:00
}
function formatRelationDataLines ( relation ) {
2026-03-07 13:38:13 -08:00
return tarotRelationDisplayUi . formatRelationDataLines ( relation ) ;
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
function buildCubeRelationsForCard ( card ) {
if ( typeof tarotRelationsUi . buildCubeRelationsForCard !== "function" ) {
2026-03-07 01:09:00 -08:00
return [ ] ;
}
2026-03-07 05:17:50 -08:00
return tarotRelationsUi . buildCubeRelationsForCard ( card , state . magickDataset ) ;
2026-03-07 01:09:00 -08:00
}
2026-03-07 16:13:58 -08:00
function buildIChingRelationsForCard ( card ) {
if ( typeof tarotRelationsUi . buildIChingRelationsForCard !== "function" ) {
return [ ] ;
}
return tarotRelationsUi . buildIChingRelationsForCard ( card , state . referenceData ) ;
}
2026-03-07 01:09:00 -08:00
// Returns nav dispatch config for relations that have a corresponding section,
// null for informational-only relations.
function getRelationNavTarget ( relation ) {
2026-03-07 13:38:13 -08:00
return tarotRelationDisplayUi . getRelationNavTarget ( relation ) ;
2026-03-07 01:09:00 -08:00
}
function createRelationListItem ( relation ) {
2026-03-07 13:38:13 -08:00
return tarotRelationDisplayUi . createRelationListItem ( relation ) ;
2026-03-07 01:09:00 -08:00
}
function renderStaticRelationGroup ( targetEl , cardEl , relations ) {
2026-03-07 13:38:13 -08:00
tarotDetailRenderer . renderStaticRelationGroup ( targetEl , cardEl , relations ) ;
2026-03-07 01:09:00 -08:00
}
function renderDetail ( card , elements ) {
2026-03-07 13:38:13 -08:00
tarotDetailRenderer . renderDetail ( card , elements ) ;
2026-03-07 01:09:00 -08:00
}
function updateListSelection ( elements ) {
if ( ! elements ? . tarotCardListEl ) {
return ;
}
const buttons = elements . tarotCardListEl . querySelectorAll ( ".tarot-list-item" ) ;
buttons . forEach ( ( button ) => {
const isSelected = button . dataset . cardId === state . selectedCardId ;
button . classList . toggle ( "is-selected" , isSelected ) ;
button . setAttribute ( "aria-selected" , isSelected ? "true" : "false" ) ;
} ) ;
}
function selectCardById ( cardIdToSelect , elements ) {
const card = state . cards . find ( ( entry ) => entry . id === cardIdToSelect ) ;
if ( ! card ) {
return ;
}
state . selectedCardId = card . id ;
updateListSelection ( elements ) ;
updateHouseSelection ( elements ) ;
renderDetail ( card , elements ) ;
}
2026-03-08 03:52:25 -07:00
function scrollCardIntoView ( cardIdToReveal , elements ) {
elements ? . tarotCardListEl
? . querySelector ( ` [data-card-id=" ${ cardIdToReveal } "] ` )
? . scrollIntoView ( { block : "nearest" } ) ;
}
2026-03-08 06:22:27 -07:00
function getRegisteredDeckOptionMap ( ) {
const entries = typeof getDeckOptions === "function" ? getDeckOptions ( ) : [ ] ;
return new Map (
( Array . isArray ( entries ) ? entries : [ ] )
. map ( ( entry ) => ( {
id : String ( entry ? . id || "" ) . trim ( ) ,
label : String ( entry ? . label || entry ? . id || "" ) . trim ( )
} ) )
. filter ( ( entry ) => entry . id )
. map ( ( entry ) => [ entry . id , entry ] )
) ;
}
function getRegisteredDeckList ( ) {
return Array . from ( getRegisteredDeckOptionMap ( ) . values ( ) ) ;
}
function buildDeckLightboxCardRequest ( cardIdToResolve , deckIdToResolve = "" ) {
2026-03-08 03:52:25 -07:00
const card = state . cards . find ( ( entry ) => entry . id === cardIdToResolve ) ;
if ( ! card ) {
return null ;
}
2026-03-08 06:22:27 -07:00
const resolvedDeckId = String ( deckIdToResolve || getActiveDeck ? . ( ) || "" ) . trim ( ) ;
const trumpNumber = Number . isFinite ( Number ( card ? . number ) ) ? Number ( card . number ) : undefined ;
const deckOptions = resolvedDeckId ? { deckId : resolvedDeckId , trumpNumber } : { trumpNumber } ;
2026-03-08 03:52:25 -07:00
const src = typeof resolveTarotCardImage === "function"
2026-03-08 06:22:27 -07:00
? resolveTarotCardImage ( card . name , deckOptions )
2026-03-08 03:52:25 -07:00
: "" ;
2026-03-08 06:22:27 -07:00
const deckMeta = resolvedDeckId ? getRegisteredDeckOptionMap ( ) . get ( resolvedDeckId ) : null ;
const label = ( typeof getTarotCardDisplayName === "function"
? getTarotCardDisplayName ( card . name , deckOptions )
: "" ) || getDisplayCardName ( card ) || card . name || "Tarot card enlarged image" ;
2026-03-08 03:52:25 -07:00
return {
src ,
altText : label ,
label ,
cardId : card . id ,
2026-03-08 06:22:27 -07:00
deckId : resolvedDeckId ,
deckLabel : deckMeta ? . label || resolvedDeckId ,
2026-03-08 03:52:25 -07:00
compareDetails : tarotDetailRenderer . buildCompareDetails ? . ( card ) || [ ]
} ;
}
2026-03-08 06:22:27 -07:00
function buildLightboxCardRequestById ( cardIdToResolve ) {
const request = buildDeckLightboxCardRequest ( cardIdToResolve , getActiveDeck ? . ( ) || "" ) ;
if ( ! request ? . src ) {
return null ;
}
return request ;
}
2026-03-07 01:09:00 -08:00
function renderList ( elements ) {
if ( ! elements ? . tarotCardListEl ) {
return ;
}
clearChildren ( elements . tarotCardListEl ) ;
state . filteredCards . forEach ( ( card ) => {
const cardDisplayName = getDisplayCardName ( card ) ;
const button = document . createElement ( "button" ) ;
button . type = "button" ;
button . className = "tarot-list-item" ;
button . dataset . cardId = card . id ;
button . setAttribute ( "role" , "option" ) ;
const nameEl = document . createElement ( "span" ) ;
nameEl . className = "tarot-list-name" ;
nameEl . textContent = cardDisplayName || card . name ;
const metaEl = document . createElement ( "span" ) ;
metaEl . className = "tarot-list-meta" ;
metaEl . textContent = buildTypeLabel ( card ) ;
button . append ( nameEl , metaEl ) ;
elements . tarotCardListEl . appendChild ( button ) ;
} ) ;
if ( elements . tarotCountEl ) {
elements . tarotCountEl . textContent = state . searchQuery
? ` ${ state . filteredCards . length } of ${ state . cards . length } cards `
: ` ${ state . cards . length } cards ` ;
}
}
2026-03-08 22:24:34 -07:00
async function loadCards ( referenceData , magickDataset ) {
const payload = await dataService . loadTarotCards ? . ( ) ;
const cards = Array . isArray ( payload ? . cards )
? payload . cards
: ( Array . isArray ( payload ) ? payload : [ ] ) ;
return cards . map ( ( card ) => ( {
... card ,
id : String ( card ? . id || "" ) . trim ( ) || cardId ( card )
} ) ) ;
}
async function ensureTarotSection ( referenceData , magickDataset = null ) {
2026-03-07 01:09:00 -08:00
state . referenceData = referenceData || state . referenceData ;
if ( magickDataset ) {
state . magickDataset = magickDataset ;
}
2026-03-07 05:17:50 -08:00
tarotHouseUi . init ? . ( {
resolveTarotCardImage ,
2026-03-08 05:40:53 -07:00
resolveTarotCardThumbnail ,
2026-03-07 05:17:50 -08:00
getDisplayCardName ,
clearChildren ,
normalizeTarotCardLookupName ,
selectCardById ,
2026-03-08 03:52:25 -07:00
openCardLightbox : ( src , altText , options = { } ) => {
const cardId = String ( options ? . cardId || "" ) . trim ( ) ;
const primaryCardRequest = cardId ? buildLightboxCardRequestById ( cardId ) : null ;
2026-03-08 06:22:27 -07:00
const activeDeckId = String ( getActiveDeck ? . ( ) || primaryCardRequest ? . deckId || "" ) . trim ( ) ;
const availableCompareDecks = getRegisteredDeckList ( ) . filter ( ( deck ) => deck . id && deck . id !== activeDeckId ) ;
2026-03-08 03:52:25 -07:00
window . TarotUiLightbox ? . open ? . ( {
src : primaryCardRequest ? . src || src ,
altText : primaryCardRequest ? . altText || altText || "Tarot card enlarged image" ,
label : primaryCardRequest ? . label || altText || "Tarot card enlarged image" ,
cardId : primaryCardRequest ? . cardId || cardId ,
2026-03-08 06:22:27 -07:00
deckId : primaryCardRequest ? . deckId || activeDeckId ,
deckLabel : primaryCardRequest ? . deckLabel || "" ,
2026-03-08 03:52:25 -07:00
compareDetails : primaryCardRequest ? . compareDetails || [ ] ,
allowOverlayCompare : true ,
2026-03-08 06:22:27 -07:00
allowDeckCompare : Boolean ( cardId ) ,
activeDeckId ,
activeDeckLabel : primaryCardRequest ? . deckLabel || "" ,
availableCompareDecks ,
maxCompareDecks : 2 ,
2026-03-08 03:52:25 -07:00
sequenceIds : state . cards . map ( ( card ) => card . id ) ,
resolveCardById : buildLightboxCardRequestById ,
2026-03-08 06:22:27 -07:00
resolveDeckCardById : buildDeckLightboxCardRequest ,
2026-03-08 03:52:25 -07:00
onSelectCardId : ( nextCardId ) => {
const latestElements = getElements ( ) ;
selectCardById ( nextCardId , latestElements ) ;
scrollCardIntoView ( nextCardId , latestElements ) ;
}
} ) ;
} ,
2026-03-12 21:01:32 -07:00
shouldOpenCardLightboxOnSelect : ( latestElements ) => Boolean (
latestElements ? . tarotHouseSectionEl instanceof HTMLElement
&& latestElements . tarotHouseSectionEl . hidden === false
) ,
2026-03-08 03:52:25 -07:00
isHouseFocusMode : ( ) => state . houseFocusMode ,
2026-03-07 05:17:50 -08:00
getCards : ( ) => state . cards ,
2026-03-08 03:52:25 -07:00
getSelectedCardId : ( ) => state . selectedCardId ,
getHouseTopCardsVisible : ( ) => state . houseTopCardsVisible ,
getHouseTopInfoModes : ( ) => ( { ... state . houseTopInfoModes } ) ,
getHouseBottomCardsVisible : ( ) => state . houseBottomCardsVisible ,
getHouseBottomInfoModes : ( ) => ( { ... state . houseBottomInfoModes } )
2026-03-07 05:17:50 -08:00
} ) ;
2026-03-07 01:09:00 -08:00
const elements = getElements ( ) ;
if ( state . initialized ) {
state . monthRefsByCardId = buildMonthReferencesByCard ( referenceData , state . cards ) ;
state . courtCardByDecanId = buildCourtCardByDecanId ( state . cards ) ;
renderHouseOfCards ( elements ) ;
2026-03-08 03:52:25 -07:00
syncHouseControls ( elements ) ;
2026-03-07 01:09:00 -08:00
if ( state . selectedCardId ) {
const selected = state . cards . find ( ( card ) => card . id === state . selectedCardId ) ;
if ( selected ) {
renderDetail ( selected , elements ) ;
}
}
return ;
}
if ( ! elements . tarotCardListEl || ! elements . tarotDetailNameEl ) {
return ;
}
2026-03-08 22:24:34 -07:00
if ( state . loadingPromise ) {
await state . loadingPromise ;
2026-03-07 01:09:00 -08:00
return ;
}
2026-03-08 22:24:34 -07:00
state . loadingPromise = ( async ( ) => {
const cards = await loadCards ( referenceData , magickDataset ) ;
2026-03-07 01:09:00 -08:00
2026-03-08 22:24:34 -07:00
state . cards = cards ;
state . monthRefsByCardId = buildMonthReferencesByCard ( referenceData , cards ) ;
state . courtCardByDecanId = buildCourtCardByDecanId ( cards ) ;
state . filteredCards = [ ... cards ] ;
renderList ( elements ) ;
renderHouseOfCards ( elements ) ;
syncHouseControls ( elements ) ;
2026-03-07 01:09:00 -08:00
2026-03-08 22:24:34 -07:00
if ( cards . length > 0 ) {
selectCardById ( cards [ 0 ] . id , elements ) ;
2026-03-07 01:09:00 -08:00
}
2026-03-08 22:24:34 -07:00
elements . tarotCardListEl . addEventListener ( "click" , ( event ) => {
const target = event . target ;
if ( ! ( target instanceof Node ) ) {
return ;
}
2026-03-07 01:09:00 -08:00
2026-03-08 22:24:34 -07:00
const button = target instanceof Element
? target . closest ( ".tarot-list-item" )
: null ;
2026-03-07 01:09:00 -08:00
2026-03-08 22:24:34 -07:00
if ( ! ( button instanceof HTMLButtonElement ) ) {
return ;
}
2026-03-07 01:09:00 -08:00
2026-03-08 22:24:34 -07:00
const selectedId = button . dataset . cardId ;
if ( ! selectedId ) {
return ;
}
2026-03-07 01:09:00 -08:00
2026-03-08 22:24:34 -07:00
selectCardById ( selectedId , elements ) ;
2026-03-07 01:09:00 -08:00
} ) ;
2026-03-08 22:24:34 -07:00
if ( elements . tarotSearchInputEl ) {
elements . tarotSearchInputEl . addEventListener ( "input" , ( ) => {
state . searchQuery = elements . tarotSearchInputEl . value || "" ;
applySearchFilter ( elements ) ;
} ) ;
}
2026-03-07 01:09:00 -08:00
2026-03-08 22:24:34 -07:00
if ( elements . tarotSearchClearEl && elements . tarotSearchInputEl ) {
elements . tarotSearchClearEl . addEventListener ( "click" , ( ) => {
elements . tarotSearchInputEl . value = "" ;
state . searchQuery = "" ;
applySearchFilter ( elements ) ;
elements . tarotSearchInputEl . focus ( ) ;
} ) ;
}
2026-03-08 03:52:25 -07:00
2026-03-08 22:24:34 -07:00
if ( elements . tarotHouseFocusToggleEl ) {
elements . tarotHouseFocusToggleEl . addEventListener ( "click" , ( ) => {
state . houseFocusMode = ! state . houseFocusMode ;
syncHouseControls ( elements ) ;
} ) ;
}
2026-03-08 03:52:25 -07:00
2026-03-08 22:24:34 -07:00
if ( elements . tarotHouseTopCardsVisibleEl ) {
elements . tarotHouseTopCardsVisibleEl . addEventListener ( "change" , ( ) => {
state . houseTopCardsVisible = Boolean ( elements . tarotHouseTopCardsVisibleEl . checked ) ;
renderHouseOfCards ( elements ) ;
syncHouseControls ( elements ) ;
} ) ;
2026-03-08 03:52:25 -07:00
}
2026-03-08 22:24:34 -07:00
[
[ elements . tarotHouseTopInfoHebrewEl , "hebrew" ] ,
[ elements . tarotHouseTopInfoPlanetEl , "planet" ] ,
[ elements . tarotHouseTopInfoZodiacEl , "zodiac" ] ,
[ elements . tarotHouseTopInfoTrumpEl , "trump" ] ,
[ elements . tarotHouseTopInfoPathEl , "path" ]
] . forEach ( ( [ checkbox , key ] ) => {
if ( ! checkbox ) {
return ;
}
checkbox . addEventListener ( "change" , ( ) => {
state . houseTopInfoModes [ key ] = Boolean ( checkbox . checked ) ;
renderHouseOfCards ( elements ) ;
syncHouseControls ( elements ) ;
} ) ;
2026-03-08 03:52:25 -07:00
} ) ;
2026-03-08 22:24:34 -07:00
if ( elements . tarotHouseBottomCardsVisibleEl ) {
elements . tarotHouseBottomCardsVisibleEl . addEventListener ( "change" , ( ) => {
state . houseBottomCardsVisible = Boolean ( elements . tarotHouseBottomCardsVisibleEl . checked ) ;
renderHouseOfCards ( elements ) ;
syncHouseControls ( elements ) ;
} ) ;
2026-03-08 03:52:25 -07:00
}
2026-03-08 22:24:34 -07:00
[
[ elements . tarotHouseBottomInfoZodiacEl , "zodiac" ] ,
[ elements . tarotHouseBottomInfoDecanEl , "decan" ] ,
[ elements . tarotHouseBottomInfoMonthEl , "month" ] ,
[ elements . tarotHouseBottomInfoRulerEl , "ruler" ] ,
[ elements . tarotHouseBottomInfoDateEl , "date" ]
] . forEach ( ( [ checkbox , key ] ) => {
if ( ! checkbox ) {
return ;
}
checkbox . addEventListener ( "change" , ( ) => {
state . houseBottomInfoModes [ key ] = Boolean ( checkbox . checked ) ;
renderHouseOfCards ( elements ) ;
syncHouseControls ( elements ) ;
} ) ;
2026-03-08 03:52:25 -07:00
} ) ;
2026-03-08 22:24:34 -07:00
if ( elements . tarotHouseExportEl ) {
elements . tarotHouseExportEl . addEventListener ( "click" , ( ) => {
exportHouseOfCards ( elements , "png" ) ;
} ) ;
}
2026-03-08 03:52:25 -07:00
2026-03-08 22:24:34 -07:00
if ( elements . tarotHouseExportWebpEl ) {
elements . tarotHouseExportWebpEl . addEventListener ( "click" , ( ) => {
exportHouseOfCards ( elements , "webp" ) ;
} ) ;
}
2026-03-08 05:40:53 -07:00
2026-03-08 22:24:34 -07:00
if ( elements . tarotDetailImageEl ) {
elements . tarotDetailImageEl . addEventListener ( "click" , ( ) => {
if ( elements . tarotDetailImageEl . style . display === "none" || ! state . selectedCardId ) {
return ;
}
2026-03-08 05:40:53 -07:00
2026-03-08 22:24:34 -07:00
const request = buildLightboxCardRequestById ( state . selectedCardId ) ;
if ( ! request ? . src ) {
return ;
}
window . TarotUiLightbox ? . open ? . ( request ) ;
} ) ;
}
2026-03-07 01:09:00 -08:00
2026-03-08 22:24:34 -07:00
state . initialized = true ;
} ) ( ) ;
try {
await state . loadingPromise ;
} finally {
state . loadingPromise = null ;
}
2026-03-07 01:09:00 -08:00
}
function selectCardByTrump ( trumpNumber ) {
if ( ! state . initialized ) return ;
const el = getElements ( ) ;
const card = state . cards . find ( c => c . arcana === "Major" && c . number === trumpNumber ) ;
if ( ! card ) return ;
selectCardById ( card . id , el ) ;
2026-03-08 03:52:25 -07:00
scrollCardIntoView ( card . id , el ) ;
2026-03-07 01:09:00 -08:00
}
function selectCardByName ( name ) {
if ( ! state . initialized ) return ;
const el = getElements ( ) ;
const needle = normalizeTarotCardLookupName ( name ) ;
const card = state . cards . find ( ( entry ) => normalizeTarotCardLookupName ( entry . name ) === needle ) ;
if ( ! card ) return ;
selectCardById ( card . id , el ) ;
2026-03-08 03:52:25 -07:00
scrollCardIntoView ( card . id , el ) ;
2026-03-07 01:09:00 -08:00
}
window . TarotSectionUi = {
ensureTarotSection ,
selectCardByTrump ,
selectCardByName ,
getCards : ( ) => state . cards
} ;
} ) ( ) ;