Files
TaroTime/app/ui-tarot.js

1370 lines
42 KiB
JavaScript
Raw Normal View History

2026-03-07 01:09:00 -08:00
(function () {
const { resolveTarotCardImage, getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
2026-03-07 05:17:50 -08:00
const tarotHouseUi = window.TarotHouseUi || {};
const tarotRelationsUi = window.TarotRelationsUi || {};
2026-03-07 01:09:00 -08:00
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"
}
};
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"),
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"),
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, "");
}
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 resolveElementIdForCard(card) {
if (!card) {
return "";
}
const cardLookupName = normalizeTarotCardLookupName(card.name);
const rankKey = String(card.rank || "").trim().toLowerCase();
return ACE_ELEMENT_BY_CARD_NAME[cardLookupName] || COURT_ELEMENT_BY_RANK[rankKey] || "";
}
function createElementRelation(card, elementId, sourceKind, sourceLabel) {
if (!card || !elementId) {
return null;
}
const elementName = ELEMENT_NAME_BY_ID[elementId] || toTitleCase(elementId);
const hebrewLetter = ELEMENT_HEBREW_LETTER_BY_ID[elementId] || "";
const hebrewChar = ELEMENT_HEBREW_CHAR_BY_ID[elementId] || "";
const relationLabel = `${elementName}${hebrewChar ? ` (${hebrewChar})` : (hebrewLetter ? ` (${hebrewLetter})` : "")} · ${sourceLabel}`;
return {
type: "element",
id: elementId,
label: relationLabel,
data: {
elementId,
name: elementName,
tarotCard: card.name,
hebrewLetter,
hebrewChar,
sourceKind,
sourceLabel,
rank: card.rank || "",
suit: card.suit || ""
},
__key: `element|${elementId}|${sourceKind}|${normalizeRelationId(sourceLabel)}|${card.id || normalizeTarotCardLookupName(card.name)}`
};
}
function buildElementRelationsForCard(card, baseElementRelations = []) {
if (!card) {
return [];
}
if (card.arcana === "Major") {
return Array.isArray(baseElementRelations) ? [...baseElementRelations] : [];
}
const relations = [];
const suitKey = String(card.suit || "").trim().toLowerCase();
const suitElementId = SUIT_ELEMENT_BY_SUIT[suitKey] || "";
if (suitElementId) {
const suitRelation = createElementRelation(card, suitElementId, "suit", `Suit: ${card.suit}`);
if (suitRelation) {
relations.push(suitRelation);
}
}
const rankKey = String(card.rank || "").trim().toLowerCase();
const courtElementId = COURT_ELEMENT_BY_RANK[rankKey] || "";
if (courtElementId) {
const courtRelation = createElementRelation(card, courtElementId, "court", `Court: ${card.rank}`);
if (courtRelation) {
relations.push(courtRelation);
}
}
return relations;
}
function buildTetragrammatonRelationsForCard(card) {
if (!card) {
return [];
}
const elementId = resolveElementIdForCard(card);
if (!elementId) {
return [];
}
const letter = ELEMENT_HEBREW_LETTER_BY_ID[elementId] || "";
if (!letter) {
return [];
}
const elementName = ELEMENT_NAME_BY_ID[elementId] || elementId;
const letterKey = String(letter || "").trim().toLowerCase();
const hebrewLetterId = HEBREW_LETTER_ID_BY_TETRAGRAMMATON_LETTER[letterKey] || "";
return [{
type: "tetragrammaton",
id: `${letterKey}-${elementId}`,
label: `${letter} · ${elementName}`,
data: {
letter,
elementId,
elementName,
hebrewLetterId
},
__key: `tetragrammaton|${letterKey}|${elementId}|${card.id || normalizeTarotCardLookupName(card.name)}`
}];
}
function getSmallCardModality(rankNumber) {
const numeric = Number(rankNumber);
if (!Number.isFinite(numeric) || numeric < 2 || numeric > 10) {
return "";
}
if (numeric <= 4) {
return "cardinal";
}
if (numeric <= 7) {
return "fixed";
}
return "mutable";
}
function buildSmallCardRulershipRelation(card) {
if (!card || card.arcana !== "Minor") {
return null;
}
const rankKey = String(card.rank || "").trim().toLowerCase();
const rankNumber = MINOR_RANK_NUMBER_BY_NAME[rankKey];
const modality = getSmallCardModality(rankNumber);
if (!modality) {
return null;
}
const suitKey = String(card.suit || "").trim().toLowerCase();
const signId = SMALL_CARD_SIGN_BY_MODALITY_AND_SUIT[modality]?.[suitKey] || "";
if (!signId) {
return null;
}
const sign = (Array.isArray(state.referenceData?.signs) ? state.referenceData.signs : [])
.find((entry) => String(entry?.id || "").trim().toLowerCase() === signId);
const signName = String(sign?.name || toTitleCase(signId));
const signSymbol = String(sign?.symbol || "").trim();
const modalityName = toTitleCase(modality);
return {
type: "zodiacRulership",
id: `${signId}-${rankKey}-${suitKey}`,
label: `Sign type: ${modalityName} · ${signSymbol} ${signName}`.trim(),
data: {
signId,
signName,
symbol: signSymbol,
modality,
rank: card.rank,
suit: card.suit
},
__key: `zodiacRulership|${signId}|${rankKey}|${suitKey}`
};
}
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
}
function buildTypeLabel(card) {
if (card.arcana === "Major") {
return typeof card.number === "number"
? `Major Arcana · ${card.number}`
: "Major Arcana";
}
const parts = ["Minor Arcana"];
if (card.rank) {
parts.push(card.rank);
}
if (card.suit) {
parts.push(card.suit);
}
return parts.join(" · ");
}
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 findSephirahForMinorCard(card, kabTree) {
if (!card || card.arcana !== "Minor" || !kabTree) {
return null;
}
const rankKey = String(card.rank || "").trim().toLowerCase();
const plural = MINOR_PLURAL_BY_RANK[rankKey];
if (!plural) {
return null;
}
const matcher = new RegExp(`\\b4\\s+${plural}\\b`, "i");
return (kabTree.sephiroth || []).find((seph) => matcher.test(String(seph?.tarot || ""))) || null;
}
function formatRelation(relation) {
if (typeof relation === "string") {
return relation;
}
if (!relation || typeof relation !== "object") {
return "";
}
if (typeof relation.label === "string" && relation.label.trim()) {
return relation.label;
}
if (relation.type === "hebrewLetter" && relation.data) {
const glyph = relation.data.glyph || "";
const name = relation.data.name || relation.id || "Unknown";
const latin = relation.data.latin ? ` (${relation.data.latin})` : "";
const index = Number.isFinite(relation.data.index) ? relation.data.index : "?";
const value = Number.isFinite(relation.data.value) ? relation.data.value : "?";
const meaning = relation.data.meaning ? ` · ${relation.data.meaning}` : "";
return `Hebrew Letter: ${glyph} ${name}${latin} (index ${index}, value ${value})${meaning}`.trim();
}
if (typeof relation.type === "string" && typeof relation.id === "string") {
return `${relation.type}: ${relation.id}`;
}
return "";
}
function relationKey(relation, index) {
const safeType = String(relation?.type || "relation");
const safeId = String(relation?.id || index || "0");
const safeLabel = String(relation?.label || relation?.text || "");
return `${safeType}|${safeId}|${safeLabel}`;
}
function normalizeRelationObject(relation, index) {
if (relation && typeof relation === "object") {
const label = formatRelation(relation);
if (!label) {
return null;
}
return {
...relation,
label,
__key: relationKey(relation, index)
};
}
const text = formatRelation(relation);
if (!text) {
return null;
}
return {
type: "text",
id: `text-${index}`,
label: text,
data: { value: text },
__key: relationKey({ type: "text", id: `text-${index}`, label: text }, index)
};
}
function formatRelationDataLines(relation) {
if (!relation || typeof relation !== "object") {
return "--";
}
const data = relation.data;
if (!data || typeof data !== "object") {
return "(no additional relation data)";
}
const lines = Object.entries(data)
.filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== "")
.map(([key, value]) => `${key}: ${value}`);
return lines.length ? lines.join("\n") : "(no additional relation data)";
}
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
}
// Returns nav dispatch config for relations that have a corresponding section,
// null for informational-only relations.
function getRelationNavTarget(relation) {
const t = relation?.type;
const d = relation?.data || {};
if ((t === "planetCorrespondence" || t === "decanRuler") && d.planetId) {
return {
event: "nav:planet",
detail: { planetId: d.planetId },
label: `Open ${d.name || d.planetId} in Planets`
};
}
if (t === "planet") {
const planetId = normalizeRelationId(d.name || relation?.id || "");
if (!planetId) return null;
return {
event: "nav:planet",
detail: { planetId },
label: `Open ${d.name || planetId} in Planets`
};
}
if (t === "element") {
const elementId = d.elementId || relation?.id;
if (!elementId) {
return null;
}
return {
event: "nav:elements",
detail: { elementId },
label: `Open ${d.name || elementId} in Elements`
};
}
if (t === "tetragrammaton") {
if (!d.hebrewLetterId) {
return null;
}
return {
event: "nav:alphabet",
detail: { alphabet: "hebrew", hebrewLetterId: d.hebrewLetterId },
label: `Open ${d.letter || d.hebrewLetterId} in Alphabet`
};
}
if (t === "tarotCard") {
const cardName = d.cardName || relation?.id;
if (!cardName) {
return null;
}
return {
event: "nav:tarot-trump",
detail: { cardName },
label: `Open ${cardName} in Tarot`
};
}
if (t === "zodiacRulership") {
const signId = d.signId || relation?.id;
if (!signId) {
return null;
}
return {
event: "nav:zodiac",
detail: { signId },
label: `Open ${d.signName || signId} in Zodiac`
};
}
if (t === "zodiacCorrespondence" || t === "zodiac") {
const signId = d.signId || relation?.id || normalizeRelationId(d.name || "");
if (!signId) {
return null;
}
return {
event: "nav:zodiac",
detail: { signId },
label: `Open ${d.name || signId} in Zodiac`
};
}
if (t === "decan") {
const signId = d.signId || normalizeRelationId(d.signName || relation?.id || "");
if (!signId) {
return null;
}
return {
event: "nav:zodiac",
detail: { signId },
label: `Open ${d.signName || signId} in Zodiac`
};
}
if (t === "hebrewLetter") {
const hebrewLetterId = d.id || relation?.id;
if (!hebrewLetterId) {
return null;
}
return {
event: "nav:alphabet",
detail: { alphabet: "hebrew", hebrewLetterId },
label: `Open ${d.name || hebrewLetterId} in Alphabet`
};
}
if (t === "calendarMonth") {
const monthId = d.monthId || relation?.id;
if (!monthId) {
return null;
}
return {
event: "nav:calendar-month",
detail: { monthId },
label: `Open ${d.name || monthId} in Calendar`
};
}
if (t === "cubeFace") {
const wallId = d.wallId || relation?.id;
if (!wallId) {
return null;
}
return {
event: "nav:cube",
detail: { wallId, edgeId: "" },
label: `Open ${d.wallName || wallId} face in Cube`
};
}
if (t === "cubeEdge") {
const edgeId = d.edgeId || relation?.id;
if (!edgeId) {
return null;
}
return {
event: "nav:cube",
detail: { edgeId, wallId: d.wallId || undefined },
label: `Open ${d.edgeName || edgeId} edge in Cube`
};
}
if (t === "cubeConnector") {
const connectorId = d.connectorId || relation?.id;
if (!connectorId) {
return null;
}
return {
event: "nav:cube",
detail: { connectorId },
label: `Open ${d.connectorName || connectorId} connector in Cube`
};
}
if (t === "cubeCenter") {
return {
event: "nav:cube",
detail: { nodeType: "center", primalPoint: true },
label: "Open Primal Point in Cube"
};
}
return null;
}
function createRelationListItem(relation) {
const item = document.createElement("li");
const navTarget = getRelationNavTarget(relation);
const button = document.createElement("button");
button.type = "button";
button.className = "tarot-relation-btn";
button.dataset.relationKey = relation.__key;
button.textContent = relation.label;
item.appendChild(button);
if (!navTarget) {
button.classList.add("tarot-relation-btn-static");
}
button.addEventListener("click", () => {
if (navTarget) {
document.dispatchEvent(new CustomEvent(navTarget.event, { detail: navTarget.detail }));
}
});
if (navTarget) {
item.className = "tarot-rel-item";
const navBtn = document.createElement("button");
navBtn.type = "button";
navBtn.className = "tarot-rel-nav-btn";
navBtn.title = navTarget.label;
navBtn.textContent = "\u2197";
navBtn.addEventListener("click", (e) => {
e.stopPropagation();
document.dispatchEvent(new CustomEvent(navTarget.event, { detail: navTarget.detail }));
});
item.appendChild(navBtn);
}
return item;
}
function renderStaticRelationGroup(targetEl, cardEl, relations) {
clearChildren(targetEl);
if (!targetEl || !cardEl) return;
if (!relations.length) {
cardEl.hidden = true;
return;
}
cardEl.hidden = false;
relations.forEach((relation) => {
targetEl.appendChild(createRelationListItem(relation));
});
}
function renderDetail(card, elements) {
if (!card || !elements) {
return;
}
const cardDisplayName = getDisplayCardName(card);
const imageUrl = typeof resolveTarotCardImage === "function"
? resolveTarotCardImage(card.name)
: null;
if (elements.tarotDetailImageEl) {
if (imageUrl) {
elements.tarotDetailImageEl.src = imageUrl;
elements.tarotDetailImageEl.alt = cardDisplayName || card.name;
elements.tarotDetailImageEl.style.display = "block";
elements.tarotDetailImageEl.style.cursor = "zoom-in";
elements.tarotDetailImageEl.title = "Click to enlarge";
} else {
elements.tarotDetailImageEl.removeAttribute("src");
elements.tarotDetailImageEl.alt = "";
elements.tarotDetailImageEl.style.display = "none";
elements.tarotDetailImageEl.style.cursor = "default";
elements.tarotDetailImageEl.removeAttribute("title");
}
}
if (elements.tarotDetailNameEl) {
elements.tarotDetailNameEl.textContent = cardDisplayName || card.name;
}
if (elements.tarotDetailTypeEl) {
elements.tarotDetailTypeEl.textContent = buildTypeLabel(card);
}
if (elements.tarotDetailSummaryEl) {
elements.tarotDetailSummaryEl.textContent = card.summary || "--";
}
if (elements.tarotDetailUprightEl) {
elements.tarotDetailUprightEl.textContent = card.meanings?.upright || "--";
}
if (elements.tarotDetailReversedEl) {
elements.tarotDetailReversedEl.textContent = card.meanings?.reversed || "--";
}
const meaningText = String(card.meaning || card.meanings?.upright || "").trim();
if (elements.tarotMetaMeaningCardEl && elements.tarotDetailMeaningEl) {
if (meaningText) {
elements.tarotMetaMeaningCardEl.hidden = false;
elements.tarotDetailMeaningEl.textContent = meaningText;
} else {
elements.tarotMetaMeaningCardEl.hidden = true;
elements.tarotDetailMeaningEl.textContent = "--";
}
}
clearChildren(elements.tarotDetailKeywordsEl);
(card.keywords || []).forEach((keyword) => {
const chip = document.createElement("span");
chip.className = "tarot-keyword-chip";
chip.textContent = keyword;
elements.tarotDetailKeywordsEl?.appendChild(chip);
});
const allRelations = (card.relations || [])
.map((relation, index) => normalizeRelationObject(relation, index))
.filter(Boolean);
const uniqueByKey = new Set();
const dedupedRelations = allRelations.filter((relation) => {
const key = `${relation.type || "relation"}|${relation.id || ""}|${relation.label || ""}`;
if (uniqueByKey.has(key)) return false;
uniqueByKey.add(key);
return true;
});
const planetRelations = dedupedRelations.filter((relation) =>
relation.type === "planetCorrespondence" || relation.type === "decanRuler" || relation.type === "planet"
);
const zodiacRelations = dedupedRelations.filter((relation) =>
relation.type === "zodiacCorrespondence" || relation.type === "zodiac" || relation.type === "decan"
);
const courtDateRelations = dedupedRelations.filter((relation) => relation.type === "courtDateWindow");
const hebrewRelations = dedupedRelations.filter((relation) => relation.type === "hebrewLetter");
const baseElementRelations = dedupedRelations.filter((relation) => relation.type === "element");
const elementRelations = buildElementRelationsForCard(card, baseElementRelations);
const tetragrammatonRelations = buildTetragrammatonRelationsForCard(card);
const smallCardRulershipRelation = buildSmallCardRulershipRelation(card);
const zodiacRelationsWithRulership = smallCardRulershipRelation
? [...zodiacRelations, smallCardRulershipRelation]
: zodiacRelations;
const smallCardCourtLinkRelations = buildSmallCardCourtLinkRelations(card, dedupedRelations);
const mergedCourtDateRelations = [...courtDateRelations, ...smallCardCourtLinkRelations];
const cubeRelations = buildCubeRelationsForCard(card);
const monthRelations = (state.monthRefsByCardId.get(card.id) || []).map((month, index) => {
const dateRange = String(month?.dateRange || "").trim();
const context = String(month?.context || "").trim();
const labelBase = dateRange || month.name;
const label = context ? `${labelBase} · ${context}` : labelBase;
return {
type: "calendarMonth",
id: month.id,
label,
data: {
monthId: month.id,
name: month.name,
monthOrder: Number.isFinite(Number(month.order)) ? Number(month.order) : null,
dateRange: dateRange || null,
dateStart: month.startToken || null,
dateEnd: month.endToken || null,
context: context || null,
source: month.source || null
},
__key: `calendarMonth|${month.id}|${month.uniqueKey || index}`
};
});
const relationMonthRows = dedupedRelations
.filter((relation) => relation.type === "calendarMonth")
.map((relation) => {
const dateRange = String(relation?.data?.dateRange || "").trim();
const baseName = relation?.data?.name || relation.label;
const label = dateRange && baseName
? `${baseName} · ${dateRange}`
: baseName;
return {
type: "calendarMonth",
id: relation?.data?.monthId || relation.id,
label,
data: {
monthId: relation?.data?.monthId || relation.id,
name: relation?.data?.name || relation.label,
monthOrder: Number.isFinite(Number(relation?.data?.monthOrder))
? Number(relation.data.monthOrder)
: null,
dateRange: dateRange || null,
dateStart: relation?.data?.dateStart || null,
dateEnd: relation?.data?.dateEnd || null,
context: relation?.data?.signName || null
},
__key: relation.__key
};
})
.filter((entry) => entry.data.monthId);
const mergedMonthMap = new Map();
[...monthRelations, ...relationMonthRows].forEach((entry) => {
const monthId = entry?.data?.monthId;
if (!monthId) {
return;
}
const key = [
monthId,
String(entry?.data?.dateRange || "").trim().toLowerCase(),
String(entry?.data?.context || "").trim().toLowerCase(),
String(entry?.label || "").trim().toLowerCase()
].join("|");
if (!mergedMonthMap.has(key)) {
mergedMonthMap.set(key, entry);
}
});
const mergedMonthRelations = [...mergedMonthMap.values()].sort((left, right) => {
const orderLeft = Number.isFinite(Number(left?.data?.monthOrder)) ? Number(left.data.monthOrder) : 999;
const orderRight = Number.isFinite(Number(right?.data?.monthOrder)) ? Number(right.data.monthOrder) : 999;
if (orderLeft !== orderRight) {
return orderLeft - orderRight;
}
const startLeft = parseMonthDayToken(left?.data?.dateStart);
const startRight = parseMonthDayToken(right?.data?.dateStart);
const dayLeft = startLeft ? startLeft.day : 999;
const dayRight = startRight ? startRight.day : 999;
if (dayLeft !== dayRight) {
return dayLeft - dayRight;
}
return String(left.label || "").localeCompare(String(right.label || ""));
});
renderStaticRelationGroup(elements.tarotDetailPlanetEl, elements.tarotMetaPlanetCardEl, planetRelations);
renderStaticRelationGroup(elements.tarotDetailElementEl, elements.tarotMetaElementCardEl, elementRelations);
renderStaticRelationGroup(elements.tarotDetailTetragrammatonEl, elements.tarotMetaTetragrammatonCardEl, tetragrammatonRelations);
renderStaticRelationGroup(elements.tarotDetailZodiacEl, elements.tarotMetaZodiacCardEl, zodiacRelationsWithRulership);
renderStaticRelationGroup(elements.tarotDetailCourtDateEl, elements.tarotMetaCourtDateCardEl, mergedCourtDateRelations);
renderStaticRelationGroup(elements.tarotDetailHebrewEl, elements.tarotMetaHebrewCardEl, hebrewRelations);
renderStaticRelationGroup(elements.tarotDetailCubeEl, elements.tarotMetaCubeCardEl, cubeRelations);
renderStaticRelationGroup(elements.tarotDetailCalendarEl, elements.tarotMetaCalendarCardEl, mergedMonthRelations);
// ── Kabbalah Tree path cross-reference ─────────────────────────────────
const kabPathEl = elements.tarotKabPathEl;
if (kabPathEl) {
const kabTree = state.magickDataset?.grouped?.kabbalah?.["kabbalah-tree"];
const kabPath = (card.arcana === "Major" && typeof card.number === "number" && kabTree)
? kabTree.paths.find(p => p.tarot?.trumpNumber === card.number)
: null;
const kabSeph = !kabPath ? findSephirahForMinorCard(card, kabTree) : null;
if (kabPath) {
const letter = kabPath.hebrewLetter || {};
const fromName = kabTree.sephiroth.find(s => s.number === kabPath.connects.from)?.name || kabPath.connects.from;
const toName = kabTree.sephiroth.find(s => s.number === kabPath.connects.to)?.name || kabPath.connects.to;
const astro = kabPath.astrology ? `${kabPath.astrology.name} (${kabPath.astrology.type})` : "";
kabPathEl.innerHTML = `
<strong>Kabbalah Tree &#8212; Path ${kabPath.pathNumber}</strong>
<div class="tarot-kab-path-row">
<span class="tarot-kab-letter" title="${letter.transliteration || ""}">${letter.char || ""}</span>
<span class="tarot-kab-meta">
<span class="tarot-kab-name">${letter.transliteration || ""} &mdash; &ldquo;${letter.meaning || ""}&rdquo; &middot; ${letter.letterType || ""}</span>
<span class="tarot-kab-connects">${fromName} &rarr; ${toName}${astro ? " &middot; " + astro : ""}</span>
</span>
</div>`;
const btn = document.createElement("button");
btn.type = "button";
btn.className = "kab-tarot-link";
btn.textContent = `View Path ${kabPath.pathNumber} in Kabbalah Tree`;
btn.addEventListener("click", () => {
document.dispatchEvent(new CustomEvent("tarot:view-kab-path", {
detail: { pathNumber: kabPath.pathNumber }
}));
});
kabPathEl.appendChild(btn);
kabPathEl.hidden = false;
} else if (kabSeph) {
const hebrewName = kabSeph.nameHebrew ? ` (${kabSeph.nameHebrew})` : "";
const translation = kabSeph.translation ? `${kabSeph.translation}` : "";
const planetInfo = kabSeph.planet || "";
const tarotInfo = kabSeph.tarot ? ` · ${kabSeph.tarot}` : "";
kabPathEl.innerHTML = `
<strong>Kabbalah Tree &#8212; Sephirah ${kabSeph.number}</strong>
<div class="tarot-kab-path-row">
<span class="tarot-kab-letter" title="${kabSeph.name || ""}">${kabSeph.number}</span>
<span class="tarot-kab-meta">
<span class="tarot-kab-name">${kabSeph.name || ""}${hebrewName}${translation}</span>
<span class="tarot-kab-connects">${planetInfo}${tarotInfo}</span>
</span>
</div>`;
const btn = document.createElement("button");
btn.type = "button";
btn.className = "kab-tarot-link";
btn.textContent = `View Sephirah ${kabSeph.number} in Kabbalah Tree`;
btn.addEventListener("click", () => {
document.dispatchEvent(new CustomEvent("tarot:view-kab-path", {
detail: { pathNumber: kabSeph.number }
}));
});
kabPathEl.appendChild(btn);
kabPathEl.hidden = false;
} else {
kabPathEl.hidden = true;
kabPathEl.innerHTML = "";
}
}
}
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;
}
2026-03-07 05:17:50 -08:00
tarotHouseUi.init?.({
resolveTarotCardImage,
getDisplayCardName,
clearChildren,
normalizeTarotCardLookupName,
selectCardById,
getCards: () => state.cards,
getSelectedCardId: () => state.selectedCardId
});
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);
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;
}
2026-03-07 05:17:50 -08:00
window.TarotUiLightbox?.open?.(src, elements.tarotDetailImageEl.alt || "Tarot card enlarged image");
2026-03-07 01:09:00 -08:00
});
}
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
};
})();