Files
TaroTime/app/ui-numbers-detail.js
2026-03-07 13:38:13 -08:00

634 lines
23 KiB
JavaScript

(function () {
"use strict";
function getCalendarMonthLinksForNumber(context, value) {
const { getReferenceData, normalizeNumberValue, computeDigitalRoot } = context;
const referenceData = getReferenceData();
const normalized = normalizeNumberValue(value);
const calendarGroups = [
{
calendarId: "gregorian",
calendarLabel: "Gregorian",
months: Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : []
},
{
calendarId: "hebrew",
calendarLabel: "Hebrew",
months: Array.isArray(referenceData?.hebrewCalendar?.months) ? referenceData.hebrewCalendar.months : []
},
{
calendarId: "islamic",
calendarLabel: "Islamic",
months: Array.isArray(referenceData?.islamicCalendar?.months) ? referenceData.islamicCalendar.months : []
},
{
calendarId: "wheel-of-year",
calendarLabel: "Wheel of the Year",
months: Array.isArray(referenceData?.wheelOfYear?.months) ? referenceData.wheelOfYear.months : []
}
];
const links = [];
calendarGroups.forEach((group) => {
group.months.forEach((month) => {
const monthOrder = Number(month?.order);
const normalizedOrder = Number.isFinite(monthOrder) ? Math.trunc(monthOrder) : null;
const monthRoot = normalizedOrder != null ? computeDigitalRoot(normalizedOrder) : null;
if (monthRoot !== normalized) {
return;
}
links.push({
calendarId: group.calendarId,
calendarLabel: group.calendarLabel,
monthId: String(month.id || "").trim(),
monthName: String(month.name || month.id || "Month").trim(),
monthOrder: normalizedOrder
});
});
});
return links.filter((link) => link.monthId);
}
function buildFallbackPlayingDeckEntries(context) {
const { PLAYING_SUIT_SYMBOL, PLAYING_SUIT_LABEL, PLAYING_SUIT_TO_TAROT, PLAYING_RANKS, rankLabelToTarotMinorRank } = context;
const entries = [];
Object.keys(PLAYING_SUIT_SYMBOL).forEach((suit) => {
PLAYING_RANKS.forEach((rank) => {
const tarotSuit = PLAYING_SUIT_TO_TAROT[suit];
const tarotRank = rankLabelToTarotMinorRank(rank.rankLabel);
entries.push({
id: `${rank.rank}${PLAYING_SUIT_SYMBOL[suit]}`,
suit,
suitLabel: PLAYING_SUIT_LABEL[suit],
suitSymbol: PLAYING_SUIT_SYMBOL[suit],
rank: rank.rank,
rankLabel: rank.rankLabel,
rankValue: rank.rankValue,
tarotSuit,
tarotCard: `${tarotRank} of ${tarotSuit}`
});
});
});
return entries;
}
function getPlayingDeckEntries(context) {
const {
getMagickDataset,
PLAYING_SUIT_SYMBOL,
PLAYING_SUIT_LABEL,
PLAYING_SUIT_TO_TAROT,
rankLabelToTarotMinorRank
} = context;
const deckData = getMagickDataset()?.grouped?.["playing-cards-52"];
const rawEntries = Array.isArray(deckData)
? deckData
: (Array.isArray(deckData?.entries) ? deckData.entries : []);
if (!rawEntries.length) {
return buildFallbackPlayingDeckEntries(context);
}
return rawEntries
.map((entry) => {
const suit = String(entry?.suit || "").trim().toLowerCase();
const rankLabel = String(entry?.rankLabel || "").trim();
const rank = String(entry?.rank || "").trim();
if (!suit || !rank) {
return null;
}
const suitSymbol = String(entry?.suitSymbol || PLAYING_SUIT_SYMBOL[suit] || "").trim();
const tarotSuit = String(entry?.tarotSuit || PLAYING_SUIT_TO_TAROT[suit] || "").trim();
const tarotCard = String(entry?.tarotCard || "").trim();
const rankValueRaw = Number(entry?.rankValue);
const rankValue = Number.isFinite(rankValueRaw) ? Math.trunc(rankValueRaw) : null;
return {
id: String(entry?.id || `${rank}${suitSymbol}`).trim(),
suit,
suitLabel: String(entry?.suitLabel || PLAYING_SUIT_LABEL[suit] || suit).trim(),
suitSymbol,
rank,
rankLabel: rankLabel || rank,
rankValue,
tarotSuit,
tarotCard: tarotCard || `${rankLabelToTarotMinorRank(rankLabel || rank)} of ${tarotSuit}`
};
})
.filter(Boolean);
}
function findPlayingCardBySuitAndValue(entries, suit, value) {
const normalizedSuit = String(suit || "").trim().toLowerCase();
const targetValue = Number(value);
return entries.find((entry) => entry.suit === normalizedSuit && Number(entry.rankValue) === targetValue) || null;
}
function buildNumbersSpecialCardSlots(context, playingSuit) {
const {
PLAYING_SUIT_LABEL,
PLAYING_SUIT_TO_TAROT,
NUMBERS_SPECIAL_BASE_VALUES,
numbersSpecialFlipState
} = context;
const suit = String(playingSuit || "hearts").trim().toLowerCase();
const selectedSuit = ["hearts", "diamonds", "clubs", "spades"].includes(suit) ? suit : "hearts";
const deckEntries = getPlayingDeckEntries(context);
const cardEl = document.createElement("div");
cardEl.className = "numbers-detail-card numbers-special-card-section";
const headingEl = document.createElement("strong");
headingEl.textContent = "4 Card Arrangement";
const subEl = document.createElement("div");
subEl.className = "numbers-detail-text numbers-detail-text--muted";
subEl.textContent = `Click a card to flip to its opposite (${PLAYING_SUIT_LABEL[selectedSuit]}${PLAYING_SUIT_TO_TAROT[selectedSuit]}).`;
const boardEl = document.createElement("div");
boardEl.className = "numbers-special-board";
NUMBERS_SPECIAL_BASE_VALUES.forEach((baseValue) => {
const oppositeValue = 9 - baseValue;
const frontCard = findPlayingCardBySuitAndValue(deckEntries, selectedSuit, baseValue);
const backCard = findPlayingCardBySuitAndValue(deckEntries, selectedSuit, oppositeValue);
if (!frontCard || !backCard) {
return;
}
const slotKey = `${selectedSuit}:${baseValue}`;
const isFlipped = Boolean(numbersSpecialFlipState.get(slotKey));
const faceBtn = document.createElement("button");
faceBtn.type = "button";
faceBtn.className = `numbers-special-card${isFlipped ? " is-flipped" : ""}`;
faceBtn.setAttribute("aria-pressed", isFlipped ? "true" : "false");
faceBtn.setAttribute("aria-label", `${frontCard.rankLabel} of ${frontCard.suitLabel}. Click to flip to ${backCard.rankLabel}.`);
faceBtn.dataset.suit = selectedSuit;
const innerEl = document.createElement("div");
innerEl.className = "numbers-special-card-inner";
const frontFaceEl = document.createElement("div");
frontFaceEl.className = "numbers-special-card-face numbers-special-card-face--front";
const frontRankEl = document.createElement("div");
frontRankEl.className = "numbers-special-card-rank";
frontRankEl.textContent = frontCard.rankLabel;
const frontSuitEl = document.createElement("div");
frontSuitEl.className = "numbers-special-card-suit";
frontSuitEl.textContent = frontCard.suitSymbol;
const frontMetaEl = document.createElement("div");
frontMetaEl.className = "numbers-special-card-meta";
frontMetaEl.textContent = frontCard.tarotCard;
frontFaceEl.append(frontRankEl, frontSuitEl, frontMetaEl);
const backFaceEl = document.createElement("div");
backFaceEl.className = "numbers-special-card-face numbers-special-card-face--back";
const backTagEl = document.createElement("div");
backTagEl.className = "numbers-special-card-tag";
backTagEl.textContent = "Opposite";
const backRankEl = document.createElement("div");
backRankEl.className = "numbers-special-card-rank";
backRankEl.textContent = backCard.rankLabel;
const backSuitEl = document.createElement("div");
backSuitEl.className = "numbers-special-card-suit";
backSuitEl.textContent = backCard.suitSymbol;
const backMetaEl = document.createElement("div");
backMetaEl.className = "numbers-special-card-meta";
backMetaEl.textContent = backCard.tarotCard;
backFaceEl.append(backTagEl, backRankEl, backSuitEl, backMetaEl);
innerEl.append(frontFaceEl, backFaceEl);
faceBtn.append(innerEl);
faceBtn.addEventListener("click", () => {
const next = !Boolean(numbersSpecialFlipState.get(slotKey));
numbersSpecialFlipState.set(slotKey, next);
faceBtn.classList.toggle("is-flipped", next);
faceBtn.setAttribute("aria-pressed", next ? "true" : "false");
});
boardEl.appendChild(faceBtn);
});
if (!boardEl.childElementCount) {
const emptyEl = document.createElement("div");
emptyEl.className = "numbers-detail-text numbers-detail-text--muted";
emptyEl.textContent = "No card slots available for this mapping yet.";
boardEl.appendChild(emptyEl);
}
cardEl.append(headingEl, subEl, boardEl);
return cardEl;
}
function renderNumbersSpecialPanel(context, value, entry) {
const { specialPanelEl } = context.elements;
if (!specialPanelEl) {
return;
}
const playingSuit = entry?.associations?.playingSuit || "hearts";
const boardCardEl = buildNumbersSpecialCardSlots(context, playingSuit);
specialPanelEl.replaceChildren(boardCardEl);
}
function parseTarotCardNumber(rawValue) {
if (typeof rawValue === "number") {
return Number.isFinite(rawValue) ? Math.trunc(rawValue) : null;
}
if (typeof rawValue === "string") {
const trimmed = rawValue.trim();
if (!trimmed || !/^-?\d+$/.test(trimmed)) {
return null;
}
return Number(trimmed);
}
return null;
}
function extractTarotCardNumericValue(context, card) {
const { TAROT_RANK_NUMBER_MAP } = context;
const directNumber = parseTarotCardNumber(card?.number);
if (directNumber !== null) {
return directNumber;
}
const rankKey = String(card?.rank || "").trim().toLowerCase();
if (Object.prototype.hasOwnProperty.call(TAROT_RANK_NUMBER_MAP, rankKey)) {
return TAROT_RANK_NUMBER_MAP[rankKey];
}
const numerologyRelation = Array.isArray(card?.relations)
? card.relations.find((relation) => String(relation?.type || "").trim().toLowerCase() === "numerology")
: null;
const relationValue = Number(numerologyRelation?.data?.value);
if (Number.isFinite(relationValue)) {
return Math.trunc(relationValue);
}
return null;
}
function getAlphabetPositionLinksForDigitalRoot(context, targetRoot) {
const { getMagickDataset, computeDigitalRoot } = context;
const alphabets = getMagickDataset()?.grouped?.alphabets;
if (!alphabets || typeof alphabets !== "object") {
return [];
}
const links = [];
const addLink = (alphabetLabel, entry, buttonLabel, detail) => {
const index = Number(entry?.index);
if (!Number.isFinite(index)) {
return;
}
const normalizedIndex = Math.trunc(index);
if (computeDigitalRoot(normalizedIndex) !== targetRoot) {
return;
}
links.push({
alphabet: alphabetLabel,
index: normalizedIndex,
label: buttonLabel,
detail
});
};
const toTitle = (value) => String(value || "")
.trim()
.replace(/[_-]+/g, " ")
.replace(/\s+/g, " ")
.toLowerCase()
.replace(/\b([a-z])/g, (match, ch) => ch.toUpperCase());
const englishEntries = Array.isArray(alphabets.english) ? alphabets.english : [];
englishEntries.forEach((entry) => {
const letter = String(entry?.letter || "").trim();
if (!letter) {
return;
}
addLink("English", entry, `${letter}`, {
alphabet: "english",
englishLetter: letter
});
});
const greekEntries = Array.isArray(alphabets.greek) ? alphabets.greek : [];
greekEntries.forEach((entry) => {
const greekName = String(entry?.name || "").trim();
if (!greekName) {
return;
}
const glyph = String(entry?.char || "").trim();
const displayName = String(entry?.displayName || toTitle(greekName)).trim();
addLink("Greek", entry, glyph ? `${displayName} - ${glyph}` : displayName, {
alphabet: "greek",
greekName
});
});
const hebrewEntries = Array.isArray(alphabets.hebrew) ? alphabets.hebrew : [];
hebrewEntries.forEach((entry) => {
const hebrewLetterId = String(entry?.hebrewLetterId || "").trim();
if (!hebrewLetterId) {
return;
}
const glyph = String(entry?.char || "").trim();
const name = String(entry?.name || hebrewLetterId).trim();
const displayName = toTitle(name);
addLink("Hebrew", entry, glyph ? `${displayName} - ${glyph}` : displayName, {
alphabet: "hebrew",
hebrewLetterId
});
});
const arabicEntries = Array.isArray(alphabets.arabic) ? alphabets.arabic : [];
arabicEntries.forEach((entry) => {
const arabicName = String(entry?.name || "").trim();
if (!arabicName) {
return;
}
const glyph = String(entry?.char || "").trim();
const displayName = toTitle(arabicName);
addLink("Arabic", entry, glyph ? `${displayName} - ${glyph}` : displayName, {
alphabet: "arabic",
arabicName
});
});
const enochianEntries = Array.isArray(alphabets.enochian) ? alphabets.enochian : [];
enochianEntries.forEach((entry) => {
const enochianId = String(entry?.id || "").trim();
if (!enochianId) {
return;
}
const title = String(entry?.title || enochianId).trim();
const displayName = toTitle(title);
addLink("Enochian", entry, `${displayName}`, {
alphabet: "enochian",
enochianId
});
});
return links.sort((left, right) => {
if (left.index !== right.index) {
return left.index - right.index;
}
const alphabetCompare = left.alphabet.localeCompare(right.alphabet);
if (alphabetCompare !== 0) {
return alphabetCompare;
}
return left.label.localeCompare(right.label);
});
}
function getTarotCardsForDigitalRoot(context, targetRoot, numberEntry = null) {
const { getReferenceData, getMagickDataset, ensureTarotSection, computeDigitalRoot } = context;
const referenceData = getReferenceData();
const magickDataset = getMagickDataset();
if (typeof ensureTarotSection === "function" && referenceData) {
ensureTarotSection(referenceData, magickDataset);
}
const allCards = window.TarotSectionUi?.getCards?.() || [];
const explicitTrumpNumbers = Array.isArray(numberEntry?.associations?.tarotTrumpNumbers)
? numberEntry.associations.tarotTrumpNumbers
.map((value) => Number(value))
.filter((value) => Number.isFinite(value))
.map((value) => Math.trunc(value))
: [];
const filteredCards = explicitTrumpNumbers.length
? allCards.filter((card) => {
const numberValue = parseTarotCardNumber(card?.number);
return card?.arcana === "Major" && numberValue !== null && explicitTrumpNumbers.includes(numberValue);
})
: allCards.filter((card) => {
const numberValue = extractTarotCardNumericValue(context, card);
return numberValue !== null && computeDigitalRoot(numberValue) === targetRoot;
});
return filteredCards.sort((left, right) => {
const leftNumber = extractTarotCardNumericValue(context, left);
const rightNumber = extractTarotCardNumericValue(context, right);
if (leftNumber !== rightNumber) {
return (leftNumber ?? 0) - (rightNumber ?? 0);
}
if (left?.arcana !== right?.arcana) {
return left?.arcana === "Major" ? -1 : 1;
}
return String(left?.name || "").localeCompare(String(right?.name || ""));
});
}
function renderNumberDetail(context) {
const { elements, getNumberEntryByValue, normalizeNumberValue, selectNumberEntry } = context;
const { detailNameEl, detailTypeEl, detailSummaryEl, detailBodyEl } = elements;
const entry = getNumberEntryByValue(context.value);
if (!entry) {
return;
}
const normalized = entry.value;
const opposite = entry.opposite;
const rootTarget = normalizeNumberValue(entry.digitalRoot);
if (detailNameEl) {
detailNameEl.textContent = `Number ${normalized} · ${entry.label}`;
}
if (detailTypeEl) {
detailTypeEl.textContent = `Opposite: ${opposite}`;
}
if (detailSummaryEl) {
detailSummaryEl.textContent = entry.summary || "";
}
renderNumbersSpecialPanel(context, normalized, entry);
if (!detailBodyEl) {
return;
}
detailBodyEl.replaceChildren();
const pairCardEl = document.createElement("div");
pairCardEl.className = "numbers-detail-card";
const pairHeadingEl = document.createElement("strong");
pairHeadingEl.textContent = "Number Pair";
const pairTextEl = document.createElement("div");
pairTextEl.className = "numbers-detail-text";
pairTextEl.textContent = `Opposite: ${opposite}`;
const keywordText = entry.keywords.length
? `Keywords: ${entry.keywords.join(", ")}`
: "Keywords: --";
const pairKeywordsEl = document.createElement("div");
pairKeywordsEl.className = "numbers-detail-text numbers-detail-text--muted";
pairKeywordsEl.textContent = keywordText;
const oppositeBtn = document.createElement("button");
oppositeBtn.type = "button";
oppositeBtn.className = "numbers-nav-btn";
oppositeBtn.textContent = `Open Opposite Number ${opposite}`;
oppositeBtn.addEventListener("click", () => {
selectNumberEntry(opposite);
});
pairCardEl.append(pairHeadingEl, pairTextEl, pairKeywordsEl, oppositeBtn);
const kabbalahCardEl = document.createElement("div");
kabbalahCardEl.className = "numbers-detail-card";
const kabbalahHeadingEl = document.createElement("strong");
kabbalahHeadingEl.textContent = "Kabbalah Link";
const kabbalahNode = Number(entry?.associations?.kabbalahNode);
const kabbalahTextEl = document.createElement("div");
kabbalahTextEl.className = "numbers-detail-text";
kabbalahTextEl.textContent = `Tree node target: ${kabbalahNode}`;
const kabbalahBtn = document.createElement("button");
kabbalahBtn.type = "button";
kabbalahBtn.className = "numbers-nav-btn";
kabbalahBtn.textContent = `Open Kabbalah Tree Node ${kabbalahNode}`;
kabbalahBtn.addEventListener("click", () => {
document.dispatchEvent(new CustomEvent("nav:kabbalah-path", {
detail: { pathNo: kabbalahNode }
}));
});
kabbalahCardEl.append(kabbalahHeadingEl, kabbalahTextEl, kabbalahBtn);
const alphabetCardEl = document.createElement("div");
alphabetCardEl.className = "numbers-detail-card";
const alphabetHeadingEl = document.createElement("strong");
alphabetHeadingEl.textContent = "Alphabet Links";
const alphabetLinksWrapEl = document.createElement("div");
alphabetLinksWrapEl.className = "numbers-links-wrap";
const alphabetLinks = getAlphabetPositionLinksForDigitalRoot(context, rootTarget);
if (!alphabetLinks.length) {
const emptyAlphabetEl = document.createElement("div");
emptyAlphabetEl.className = "numbers-detail-text numbers-detail-text--muted";
emptyAlphabetEl.textContent = "No alphabet position entries found for this digital root yet.";
alphabetLinksWrapEl.appendChild(emptyAlphabetEl);
} else {
alphabetLinks.forEach((link) => {
const button = document.createElement("button");
button.type = "button";
button.className = "numbers-nav-btn";
button.textContent = `${link.alphabet}: ${link.label}`;
button.addEventListener("click", () => {
document.dispatchEvent(new CustomEvent("nav:alphabet", {
detail: link.detail
}));
});
alphabetLinksWrapEl.appendChild(button);
});
}
alphabetCardEl.append(alphabetHeadingEl, alphabetLinksWrapEl);
const tarotCardEl = document.createElement("div");
tarotCardEl.className = "numbers-detail-card";
const tarotHeadingEl = document.createElement("strong");
tarotHeadingEl.textContent = "Tarot Links";
const tarotLinksWrapEl = document.createElement("div");
tarotLinksWrapEl.className = "numbers-links-wrap";
const tarotCards = getTarotCardsForDigitalRoot(context, rootTarget, entry);
if (!tarotCards.length) {
const emptyEl = document.createElement("div");
emptyEl.className = "numbers-detail-text numbers-detail-text--muted";
emptyEl.textContent = "No tarot numeric entries found yet for this root. Add card numbers to map them.";
tarotLinksWrapEl.appendChild(emptyEl);
} else {
tarotCards.forEach((card) => {
const button = document.createElement("button");
button.type = "button";
button.className = "numbers-nav-btn";
button.textContent = `${card.name}`;
button.addEventListener("click", () => {
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
detail: { cardName: card.name }
}));
});
tarotLinksWrapEl.appendChild(button);
});
}
tarotCardEl.append(tarotHeadingEl, tarotLinksWrapEl);
const calendarCardEl = document.createElement("div");
calendarCardEl.className = "numbers-detail-card";
const calendarHeadingEl = document.createElement("strong");
calendarHeadingEl.textContent = "Calendar Links";
const calendarLinksWrapEl = document.createElement("div");
calendarLinksWrapEl.className = "numbers-links-wrap";
const calendarLinks = getCalendarMonthLinksForNumber(context, normalized);
if (!calendarLinks.length) {
const emptyCalendarEl = document.createElement("div");
emptyCalendarEl.className = "numbers-detail-text numbers-detail-text--muted";
emptyCalendarEl.textContent = "No calendar months currently mapped to this number.";
calendarLinksWrapEl.appendChild(emptyCalendarEl);
} else {
calendarLinks.forEach((link) => {
const button = document.createElement("button");
button.type = "button";
button.className = "numbers-nav-btn";
button.textContent = `${link.calendarLabel}: ${link.monthName} (Month ${link.monthOrder})`;
button.addEventListener("click", () => {
document.dispatchEvent(new CustomEvent("nav:calendar-month", {
detail: {
calendarId: link.calendarId,
monthId: link.monthId
}
}));
});
calendarLinksWrapEl.appendChild(button);
});
}
calendarCardEl.append(calendarHeadingEl, calendarLinksWrapEl);
detailBodyEl.append(pairCardEl, kabbalahCardEl, alphabetCardEl, tarotCardEl, calendarCardEl);
}
window.NumbersDetailUi = {
renderNumberDetail
};
})();