Files
TaroTime/app/ui-tarot.js

1157 lines
36 KiB
JavaScript
Raw Permalink Normal View History

2026-03-07 01:09:00 -08:00
(function () {
2026-03-08 22:24:34 -07:00
const dataService = window.TarotDataService || {};
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,
date: false
2026-03-08 03:52:25 -07:00
},
houseBottomCardsVisible: true,
houseBottomInfoModes: {
zodiac: true,
decan: true,
month: true,
ruler: true,
date: false
},
houseExportInProgress: false,
houseExportFormat: "png",
houseSettingsOpen: false,
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"),
tarotHouseSettingsToggleEl: document.getElementById("tarot-house-settings-toggle"),
tarotHouseSettingsPanelEl: document.getElementById("tarot-house-settings-panel"),
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"),
tarotHouseTopInfoDateEl: document.getElementById("tarot-house-top-info-date"),
2026-03-08 03:52:25 -07:00
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"),
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,
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?.tarotHouseSettingsToggleEl) {
elements.tarotHouseSettingsToggleEl.setAttribute("aria-expanded", state.houseSettingsOpen ? "true" : "false");
elements.tarotHouseSettingsToggleEl.textContent = state.houseSettingsOpen ? "Hide Settings" : "Settings";
}
if (elements?.tarotHouseSettingsPanelEl) {
elements.tarotHouseSettingsPanelEl.hidden = !state.houseSettingsOpen;
}
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);
setHouseBottomInfoCheckboxState(elements?.tarotHouseTopInfoDateEl, state.houseTopInfoModes.date);
2026-03-08 03:52:25 -07:00
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?.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.";
}
}
}
2026-04-01 16:08:52 -07:00
function refreshHouseUi() {
if (!state.initialized) {
return;
}
const elements = getElements();
renderHouseOfCards(elements);
syncHouseControls(elements);
}
function setHouseTopCardsVisible(value) {
state.houseTopCardsVisible = Boolean(value);
refreshHouseUi();
}
function setHouseTopInfoMode(mode, value) {
const key = String(mode || "").trim();
if (!key || !Object.prototype.hasOwnProperty.call(state.houseTopInfoModes, key)) {
return;
}
state.houseTopInfoModes[key] = Boolean(value);
refreshHouseUi();
}
function setHouseBottomCardsVisible(value) {
state.houseBottomCardsVisible = Boolean(value);
refreshHouseUi();
}
function setHouseBottomInfoMode(mode, value) {
const key = String(mode || "").trim();
if (!key || !Object.prototype.hasOwnProperty.call(state.houseBottomInfoModes, key)) {
return;
}
state.houseBottomInfoModes[key] = Boolean(value);
refreshHouseUi();
}
2026-03-08 03:52:25 -07:00
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" });
}
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;
}
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"
? resolveTarotCardImage(card.name, deckOptions)
2026-03-08 03:52:25 -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,
deckId: resolvedDeckId,
deckLabel: deckMeta?.label || resolvedDeckId,
2026-03-08 03:52:25 -07:00
compareDetails: tarotDetailRenderer.buildCompareDetails?.(card) || []
};
}
function buildLightboxCardRequestById(cardIdToResolve) {
const request = buildDeckLightboxCardRequest(cardIdToResolve, getActiveDeck?.() || "");
if (!request?.src) {
return null;
}
return request;
}
2026-04-02 22:06:19 -07:00
function openCardLightboxById(cardIdToOpen, options = {}) {
const normalizedCardId = String(cardIdToOpen || "").trim();
if (!normalizedCardId) {
return;
}
const primaryCardRequest = buildLightboxCardRequestById(normalizedCardId);
if (!primaryCardRequest?.src) {
return;
}
const activeDeckId = String(getActiveDeck?.() || primaryCardRequest.deckId || "").trim();
const availableCompareDecks = getRegisteredDeckList().filter((deck) => deck.id && deck.id !== activeDeckId);
const onSelectCardId = typeof options?.onSelectCardId === "function"
? options.onSelectCardId
: (nextCardId) => {
const latestElements = getElements();
selectCardById(nextCardId, latestElements);
scrollCardIntoView(nextCardId, latestElements);
};
window.TarotUiLightbox?.open?.({
src: primaryCardRequest.src,
altText: primaryCardRequest.altText,
label: primaryCardRequest.label,
cardId: primaryCardRequest.cardId,
deckId: primaryCardRequest.deckId || activeDeckId,
deckLabel: primaryCardRequest.deckLabel || "",
compareDetails: primaryCardRequest.compareDetails || [],
allowOverlayCompare: true,
allowDeckCompare: true,
activeDeckId,
activeDeckLabel: primaryCardRequest.deckLabel || "",
availableCompareDecks,
maxCompareDecks: 2,
sequenceIds: state.cards.map((card) => card.id),
resolveCardById: buildLightboxCardRequestById,
resolveDeckCardById: buildDeckLightboxCardRequest,
onSelectCardId
});
}
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,
resolveTarotCardThumbnail,
getActiveDeck,
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();
2026-04-02 22:06:19 -07:00
if (cardId) {
openCardLightboxById(cardId);
return;
}
2026-03-08 03:52:25 -07:00
window.TarotUiLightbox?.open?.({
2026-04-02 22:06:19 -07:00
src,
altText: altText || "Tarot card enlarged image",
label: altText || "Tarot card enlarged image"
2026-03-08 03:52:25 -07:00
});
},
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 }),
getMagickDataset: () => state.magickDataset,
2026-03-08 03:52:25 -07:00
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.tarotHouseTopCardsVisibleEl) {
elements.tarotHouseTopCardsVisibleEl.addEventListener("change", () => {
state.houseTopCardsVisible = Boolean(elements.tarotHouseTopCardsVisibleEl.checked);
renderHouseOfCards(elements);
syncHouseControls(elements);
});
2026-03-08 03:52:25 -07:00
}
if (elements.tarotHouseSettingsToggleEl) {
elements.tarotHouseSettingsToggleEl.addEventListener("click", (event) => {
event.stopPropagation();
state.houseSettingsOpen = !state.houseSettingsOpen;
syncHouseControls(elements);
});
}
if (elements.tarotHouseSettingsPanelEl) {
elements.tarotHouseSettingsPanelEl.addEventListener("click", (event) => {
event.stopPropagation();
});
}
document.addEventListener("click", (event) => {
if (!state.houseSettingsOpen) {
return;
}
const target = event.target;
if (!(target instanceof Node)) {
return;
}
const settingsPanelEl = elements.tarotHouseSettingsPanelEl;
const settingsToggleEl = elements.tarotHouseSettingsToggleEl;
if (settingsPanelEl?.contains(target) || settingsToggleEl?.contains(target)) {
return;
}
state.houseSettingsOpen = false;
syncHouseControls(elements);
});
2026-03-08 22:24:34 -07:00
[
[elements.tarotHouseTopInfoHebrewEl, "hebrew"],
[elements.tarotHouseTopInfoPlanetEl, "planet"],
[elements.tarotHouseTopInfoZodiacEl, "zodiac"],
[elements.tarotHouseTopInfoTrumpEl, "trump"],
[elements.tarotHouseTopInfoPathEl, "path"],
[elements.tarotHouseTopInfoDateEl, "date"]
2026-03-08 22:24:34 -07:00
].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.tarotHouseExportWebpEl) {
elements.tarotHouseExportWebpEl.addEventListener("click", () => {
exportHouseOfCards(elements, "webp");
});
}
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 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,
2026-04-02 22:06:19 -07:00
openCardLightboxById,
2026-04-01 16:08:52 -07:00
getCards: () => state.cards,
getHouseTopCardsVisible: () => state.houseTopCardsVisible,
getHouseTopInfoModes: () => ({ ...state.houseTopInfoModes }),
getHouseBottomCardsVisible: () => state.houseBottomCardsVisible,
getHouseBottomInfoModes: () => ({ ...state.houseBottomInfoModes }),
setHouseTopCardsVisible,
setHouseTopInfoMode,
setHouseBottomCardsVisible,
setHouseBottomInfoMode
2026-03-07 01:09:00 -08:00
};
})();