refraction almost completed
This commit is contained in:
772
app/ui-tarot.js
772
app/ui-tarot.js
@@ -2,6 +2,9 @@
|
||||
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,
|
||||
@@ -242,6 +245,56 @@
|
||||
.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,
|
||||
parseMonthDayToken,
|
||||
createRelationListItem,
|
||||
findSephirahForMinorCard
|
||||
});
|
||||
|
||||
function normalizeSearchValue(value) {
|
||||
return String(value || "").trim().toLowerCase();
|
||||
}
|
||||
@@ -299,165 +352,16 @@
|
||||
.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;
|
||||
return tarotCardDerivationsUi.buildElementRelationsForCard(card, baseElementRelations);
|
||||
}
|
||||
|
||||
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";
|
||||
return tarotCardDerivationsUi.buildTetragrammatonRelationsForCard(card);
|
||||
}
|
||||
|
||||
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}`
|
||||
};
|
||||
return tarotCardDerivationsUi.buildSmallCardRulershipRelation(card);
|
||||
}
|
||||
|
||||
function buildCourtCardByDecanId(cards) {
|
||||
@@ -566,21 +470,7 @@
|
||||
}
|
||||
|
||||
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(" · ");
|
||||
return tarotCardDerivationsUi.buildTypeLabel(card);
|
||||
}
|
||||
|
||||
const MINOR_PLURAL_BY_RANK = {
|
||||
@@ -597,100 +487,23 @@
|
||||
};
|
||||
|
||||
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;
|
||||
return tarotCardDerivationsUi.findSephirahForMinorCard(card, kabTree);
|
||||
}
|
||||
|
||||
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 "";
|
||||
return tarotRelationDisplayUi.formatRelation(relation);
|
||||
}
|
||||
|
||||
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}`;
|
||||
return tarotRelationDisplayUi.relationKey(relation, index);
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
return tarotRelationDisplayUi.normalizeRelationObject(relation, 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)";
|
||||
return tarotRelationDisplayUi.formatRelationDataLines(relation);
|
||||
}
|
||||
|
||||
function buildCubeRelationsForCard(card) {
|
||||
@@ -703,472 +516,19 @@
|
||||
// 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;
|
||||
return tarotRelationDisplayUi.getRelationNavTarget(relation);
|
||||
}
|
||||
|
||||
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;
|
||||
return tarotRelationDisplayUi.createRelationListItem(relation);
|
||||
}
|
||||
|
||||
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));
|
||||
});
|
||||
tarotDetailRenderer.renderStaticRelationGroup(targetEl, cardEl, relations);
|
||||
}
|
||||
|
||||
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 — 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 || ""} — “${letter.meaning || ""}” · ${letter.letterType || ""}</span>
|
||||
<span class="tarot-kab-connects">${fromName} → ${toName}${astro ? " · " + 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 — 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 = "";
|
||||
}
|
||||
}
|
||||
tarotDetailRenderer.renderDetail(card, elements);
|
||||
}
|
||||
|
||||
function updateListSelection(elements) {
|
||||
|
||||
Reference in New Issue
Block a user