(function () { const { resolveTarotCardImage, getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {}; const tarotHouseUi = window.TarotHouseUi || {}; const tarotRelationsUi = window.TarotRelationsUi || {}; const tarotCardDerivations = window.TarotCardDerivations || {}; const tarotDetailUi = window.TarotDetailUi || {}; const tarotRelationDisplay = window.TarotRelationDisplay || {}; const state = { initialized: false, cards: [], filteredCards: [], searchQuery: "", selectedCardId: "", magickDataset: null, referenceData: null, monthRefsByCardId: new Map(), courtCardByDecanId: new Map() }; 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" } }; 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" }; 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 { 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"), tarotMetaIChingCardEl: document.getElementById("tarot-meta-iching-card"), 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"), tarotDetailIChingEl: document.getElementById("tarot-detail-iching"), tarotDetailCalendarEl: document.getElementById("tarot-detail-calendar"), tarotKabPathEl: document.getElementById("tarot-kab-path"), tarotHouseOfCardsEl: document.getElementById("tarot-house-of-cards") }; } function normalizeRelationId(value) { return String(value || "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, ""); } 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, getDisplayCardName, buildTypeLabel, clearChildren, normalizeRelationObject, buildElementRelationsForCard, buildTetragrammatonRelationsForCard, buildSmallCardRulershipRelation, buildSmallCardCourtLinkRelations, buildCubeRelationsForCard, buildIChingRelationsForCard, parseMonthDayToken, createRelationListItem, findSephirahForMinorCard }); 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 = []) { return tarotCardDerivationsUi.buildElementRelationsForCard(card, baseElementRelations); } function buildTetragrammatonRelationsForCard(card) { return tarotCardDerivationsUi.buildTetragrammatonRelationsForCard(card); } function buildSmallCardRulershipRelation(card) { return tarotCardDerivationsUi.buildSmallCardRulershipRelation(card); } function buildCourtCardByDecanId(cards) { if (typeof tarotRelationsUi.buildCourtCardByDecanId !== "function") { return new Map(); } return tarotRelationsUi.buildCourtCardByDecanId(cards); } function buildSmallCardCourtLinkRelations(card, relations) { if (typeof tarotRelationsUi.buildSmallCardCourtLinkRelations !== "function") { return []; } return tarotRelationsUi.buildSmallCardCourtLinkRelations(card, relations, state.courtCardByDecanId); } function parseMonthDayToken(value) { if (typeof tarotRelationsUi.parseMonthDayToken !== "function") { return null; } return tarotRelationsUi.parseMonthDayToken(value); } function buildMonthReferencesByCard(referenceData, cards) { if (typeof tarotRelationsUi.buildMonthReferencesByCard !== "function") { return new Map(); } return tarotRelationsUi.buildMonthReferencesByCard(referenceData, cards); } 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) { tarotHouseUi.updateSelection?.(elements); } function renderHouseOfCards(elements) { tarotHouseUi.render?.(elements); } function buildTypeLabel(card) { return tarotCardDerivationsUi.buildTypeLabel(card); } function findSephirahForMinorCard(card, kabTree) { return tarotCardDerivationsUi.findSephirahForMinorCard(card, kabTree); } function formatRelation(relation) { return tarotRelationDisplayUi.formatRelation(relation); } function relationKey(relation, index) { return tarotRelationDisplayUi.relationKey(relation, index); } function normalizeRelationObject(relation, index) { return tarotRelationDisplayUi.normalizeRelationObject(relation, index); } function formatRelationDataLines(relation) { return tarotRelationDisplayUi.formatRelationDataLines(relation); } function buildCubeRelationsForCard(card) { if (typeof tarotRelationsUi.buildCubeRelationsForCard !== "function") { return []; } return tarotRelationsUi.buildCubeRelationsForCard(card, state.magickDataset); } function buildIChingRelationsForCard(card) { if (typeof tarotRelationsUi.buildIChingRelationsForCard !== "function") { return []; } return tarotRelationsUi.buildIChingRelationsForCard(card, state.referenceData); } // Returns nav dispatch config for relations that have a corresponding section, // null for informational-only relations. function getRelationNavTarget(relation) { return tarotRelationDisplayUi.getRelationNavTarget(relation); } function createRelationListItem(relation) { return tarotRelationDisplayUi.createRelationListItem(relation); } function renderStaticRelationGroup(targetEl, cardEl, relations) { tarotDetailRenderer.renderStaticRelationGroup(targetEl, cardEl, relations); } function renderDetail(card, elements) { tarotDetailRenderer.renderDetail(card, elements); } 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); } 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`; } } function ensureTarotSection(referenceData, magickDataset = null) { state.referenceData = referenceData || state.referenceData; if (magickDataset) { state.magickDataset = magickDataset; } tarotHouseUi.init?.({ resolveTarotCardImage, getDisplayCardName, clearChildren, normalizeTarotCardLookupName, selectCardById, getCards: () => state.cards, getSelectedCardId: () => state.selectedCardId }); const elements = getElements(); if (state.initialized) { state.monthRefsByCardId = buildMonthReferencesByCard(referenceData, state.cards); state.courtCardByDecanId = buildCourtCardByDecanId(state.cards); renderHouseOfCards(elements); 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; } const databaseBuilder = window.TarotCardDatabase?.buildTarotDatabase; if (typeof databaseBuilder !== "function") { return; } const cards = databaseBuilder(referenceData, magickDataset).map((card) => ({ ...card, id: cardId(card) })); state.cards = cards; state.monthRefsByCardId = buildMonthReferencesByCard(referenceData, cards); state.courtCardByDecanId = buildCourtCardByDecanId(cards); state.filteredCards = [...cards]; renderList(elements); renderHouseOfCards(elements); if (cards.length > 0) { selectCardById(cards[0].id, elements); } elements.tarotCardListEl.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof Node)) { return; } const button = target instanceof Element ? target.closest(".tarot-list-item") : null; if (!(button instanceof HTMLButtonElement)) { return; } const selectedId = button.dataset.cardId; if (!selectedId) { return; } selectCardById(selectedId, elements); }); if (elements.tarotSearchInputEl) { elements.tarotSearchInputEl.addEventListener("input", () => { state.searchQuery = elements.tarotSearchInputEl.value || ""; applySearchFilter(elements); }); } if (elements.tarotSearchClearEl && elements.tarotSearchInputEl) { elements.tarotSearchClearEl.addEventListener("click", () => { elements.tarotSearchInputEl.value = ""; state.searchQuery = ""; applySearchFilter(elements); elements.tarotSearchInputEl.focus(); }); } if (elements.tarotDetailImageEl) { elements.tarotDetailImageEl.addEventListener("click", () => { const src = elements.tarotDetailImageEl.getAttribute("src") || ""; if (!src || elements.tarotDetailImageEl.style.display === "none") { return; } window.TarotUiLightbox?.open?.(src, elements.tarotDetailImageEl.alt || "Tarot card enlarged image"); }); } state.initialized = true; } 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); const listItem = el.tarotCardListEl?.querySelector(`[data-card-id="${card.id}"]`); listItem?.scrollIntoView({ block: "nearest" }); } 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); el.tarotCardListEl ?.querySelector(`[data-card-id="${card.id}"]`) ?.scrollIntoView({ block: "nearest" }); } window.TarotSectionUi = { ensureTarotSection, selectCardByTrump, selectCardByName, getCards: () => state.cards }; })();