refraction almost completed
This commit is contained in:
318
app/tarot-database-assembly.js
Normal file
318
app/tarot-database-assembly.js
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
/* tarot-database-assembly.js — Tarot card assembly helpers */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function createTarotDatabaseAssembly(dependencies) {
|
||||||
|
const getTarotDbConfig = dependencies?.getTarotDbConfig;
|
||||||
|
const canonicalCardName = dependencies?.canonicalCardName;
|
||||||
|
const formatMonthDayLabel = dependencies?.formatMonthDayLabel;
|
||||||
|
const applyMeaningText = dependencies?.applyMeaningText;
|
||||||
|
const buildDecanMetadata = dependencies?.buildDecanMetadata;
|
||||||
|
const listMonthNumbersBetween = dependencies?.listMonthNumbersBetween;
|
||||||
|
const buildHebrewLetterLookup = dependencies?.buildHebrewLetterLookup;
|
||||||
|
const createRelation = dependencies?.createRelation;
|
||||||
|
const parseLegacyRelation = dependencies?.parseLegacyRelation;
|
||||||
|
const buildHebrewLetterRelation = dependencies?.buildHebrewLetterRelation;
|
||||||
|
const buildMajorDynamicRelations = dependencies?.buildMajorDynamicRelations;
|
||||||
|
const buildMinorDecanRelations = dependencies?.buildMinorDecanRelations;
|
||||||
|
|
||||||
|
const monthNameByNumber = dependencies?.monthNameByNumber && typeof dependencies.monthNameByNumber === "object"
|
||||||
|
? dependencies.monthNameByNumber
|
||||||
|
: {};
|
||||||
|
const monthIdByNumber = dependencies?.monthIdByNumber && typeof dependencies.monthIdByNumber === "object"
|
||||||
|
? dependencies.monthIdByNumber
|
||||||
|
: {};
|
||||||
|
const majorHebrewLetterIdByCard = dependencies?.majorHebrewLetterIdByCard && typeof dependencies.majorHebrewLetterIdByCard === "object"
|
||||||
|
? dependencies.majorHebrewLetterIdByCard
|
||||||
|
: {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof getTarotDbConfig !== "function"
|
||||||
|
|| typeof canonicalCardName !== "function"
|
||||||
|
|| typeof formatMonthDayLabel !== "function"
|
||||||
|
|| typeof applyMeaningText !== "function"
|
||||||
|
|| typeof buildDecanMetadata !== "function"
|
||||||
|
|| typeof listMonthNumbersBetween !== "function"
|
||||||
|
|| typeof buildHebrewLetterLookup !== "function"
|
||||||
|
|| typeof createRelation !== "function"
|
||||||
|
|| typeof parseLegacyRelation !== "function"
|
||||||
|
|| typeof buildHebrewLetterRelation !== "function"
|
||||||
|
|| typeof buildMajorDynamicRelations !== "function"
|
||||||
|
|| typeof buildMinorDecanRelations !== "function"
|
||||||
|
) {
|
||||||
|
throw new Error("Tarot database assembly dependencies are incomplete");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMajorCards(referenceData, magickDataset) {
|
||||||
|
const tarotDb = getTarotDbConfig(referenceData);
|
||||||
|
const dynamicRelations = buildMajorDynamicRelations(referenceData);
|
||||||
|
const hebrewLookup = buildHebrewLetterLookup(magickDataset);
|
||||||
|
|
||||||
|
return tarotDb.majorCards.map((card) => {
|
||||||
|
const canonicalName = canonicalCardName(card.name);
|
||||||
|
const dynamic = dynamicRelations.get(canonicalName) || [];
|
||||||
|
const hebrewLetterId = majorHebrewLetterIdByCard[canonicalName] || null;
|
||||||
|
const hebrewLetterRelation = buildHebrewLetterRelation(hebrewLetterId, hebrewLookup);
|
||||||
|
const staticRelations = (card.relations || [])
|
||||||
|
.map((relation) => parseLegacyRelation(relation))
|
||||||
|
.filter((relation) => relation.type !== "hebrewLetter" && relation.type !== "zodiac" && relation.type !== "planet");
|
||||||
|
|
||||||
|
return {
|
||||||
|
arcana: "Major",
|
||||||
|
name: card.name,
|
||||||
|
number: card.number,
|
||||||
|
suit: null,
|
||||||
|
rank: null,
|
||||||
|
hebrewLetterId,
|
||||||
|
hebrewLetter: hebrewLetterRelation?.data || null,
|
||||||
|
summary: card.summary,
|
||||||
|
meanings: {
|
||||||
|
upright: card.upright,
|
||||||
|
reversed: card.reversed
|
||||||
|
},
|
||||||
|
keywords: [...card.keywords],
|
||||||
|
relations: [
|
||||||
|
...staticRelations,
|
||||||
|
...(hebrewLetterRelation ? [hebrewLetterRelation] : []),
|
||||||
|
...dynamic
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNumberMinorCards(referenceData) {
|
||||||
|
const tarotDb = getTarotDbConfig(referenceData);
|
||||||
|
const decanRelations = buildMinorDecanRelations(referenceData);
|
||||||
|
const cards = [];
|
||||||
|
|
||||||
|
tarotDb.suits.forEach((suit) => {
|
||||||
|
const suitKey = suit.toLowerCase();
|
||||||
|
const suitInfo = tarotDb.suitInfo[suitKey];
|
||||||
|
if (!suitInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tarotDb.numberRanks.forEach((rank) => {
|
||||||
|
const rankKey = rank.toLowerCase();
|
||||||
|
const rankInfo = tarotDb.rankInfo[rankKey];
|
||||||
|
if (!rankInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cardName = `${rank} of ${suit}`;
|
||||||
|
const dynamicRelations = decanRelations.get(canonicalCardName(cardName)) || [];
|
||||||
|
|
||||||
|
cards.push({
|
||||||
|
arcana: "Minor",
|
||||||
|
name: cardName,
|
||||||
|
number: null,
|
||||||
|
suit,
|
||||||
|
rank,
|
||||||
|
summary: `${rank} energy expressed through ${suitInfo.domain}.`,
|
||||||
|
meanings: {
|
||||||
|
upright: `${rankInfo.upright} In ${suit}, this emphasizes ${suitInfo.domain}.`,
|
||||||
|
reversed: `${rankInfo.reversed} In ${suit}, this may distort ${suitInfo.domain}.`
|
||||||
|
},
|
||||||
|
keywords: [...rankInfo.keywords, ...suitInfo.keywords],
|
||||||
|
relations: [
|
||||||
|
createRelation("element", suitInfo.element, `Element: ${suitInfo.element}`, {
|
||||||
|
name: suitInfo.element
|
||||||
|
}),
|
||||||
|
createRelation("suitDomain", `${suitKey}-${rankKey}`, `Suit domain: ${suitInfo.domain}`, {
|
||||||
|
suit,
|
||||||
|
rank,
|
||||||
|
domain: suitInfo.domain
|
||||||
|
}),
|
||||||
|
createRelation("numerology", rankInfo.number, `Numerology: ${rankInfo.number}`, {
|
||||||
|
value: rankInfo.number
|
||||||
|
}),
|
||||||
|
...dynamicRelations
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCourtMinorCards(referenceData) {
|
||||||
|
const tarotDb = getTarotDbConfig(referenceData);
|
||||||
|
const cards = [];
|
||||||
|
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
||||||
|
const signById = Object.fromEntries(signs.map((sign) => [sign.id, sign]));
|
||||||
|
|
||||||
|
const decanById = new Map();
|
||||||
|
const decansBySign = referenceData?.decansBySign || {};
|
||||||
|
Object.entries(decansBySign).forEach(([signId, decans]) => {
|
||||||
|
const sign = signById[signId];
|
||||||
|
if (!sign || !Array.isArray(decans)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
decans.forEach((decan) => {
|
||||||
|
if (!decan?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const meta = buildDecanMetadata(decan, sign);
|
||||||
|
if (meta) {
|
||||||
|
decanById.set(decan.id, meta);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tarotDb.suits.forEach((suit) => {
|
||||||
|
const suitKey = suit.toLowerCase();
|
||||||
|
const suitInfo = tarotDb.suitInfo[suitKey];
|
||||||
|
if (!suitInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tarotDb.courtRanks.forEach((rank) => {
|
||||||
|
const rankKey = rank.toLowerCase();
|
||||||
|
const courtInfo = tarotDb.courtInfo[rankKey];
|
||||||
|
if (!courtInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cardName = `${rank} of ${suit}`;
|
||||||
|
const windowDecanIds = tarotDb.courtDecanWindows[cardName] || [];
|
||||||
|
const windowDecans = windowDecanIds
|
||||||
|
.map((decanId) => decanById.get(decanId) || null)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const dynamicRelations = [];
|
||||||
|
const monthKeys = new Set();
|
||||||
|
|
||||||
|
windowDecans.forEach((meta) => {
|
||||||
|
dynamicRelations.push(
|
||||||
|
createRelation(
|
||||||
|
"decan",
|
||||||
|
`${meta.signId}-${meta.index}-${rankKey}-${suitKey}`,
|
||||||
|
`Decan ${meta.index}: ${meta.signSymbol} ${meta.signName} (${meta.startDegree}°–${meta.endDegree}°)${meta.dateRange ? ` · ${meta.dateRange.label}` : ""}`.trim(),
|
||||||
|
{
|
||||||
|
signId: meta.signId,
|
||||||
|
signName: meta.signName,
|
||||||
|
signSymbol: meta.signSymbol,
|
||||||
|
index: meta.index,
|
||||||
|
startDegree: meta.startDegree,
|
||||||
|
endDegree: meta.endDegree,
|
||||||
|
dateStart: meta.dateRange?.startToken || null,
|
||||||
|
dateEnd: meta.dateRange?.endToken || null,
|
||||||
|
dateRange: meta.dateRange?.label || null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const dateRange = meta.dateRange;
|
||||||
|
if (dateRange?.start && dateRange?.end) {
|
||||||
|
const monthNumbers = listMonthNumbersBetween(dateRange.start, dateRange.end);
|
||||||
|
monthNumbers.forEach((monthNo) => {
|
||||||
|
const monthId = monthIdByNumber[monthNo];
|
||||||
|
const monthName = monthNameByNumber[monthNo] || `Month ${monthNo}`;
|
||||||
|
const monthKey = `${monthId}:${meta.signId}:${meta.index}`;
|
||||||
|
if (!monthId || monthKeys.has(monthKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
monthKeys.add(monthKey);
|
||||||
|
|
||||||
|
dynamicRelations.push(
|
||||||
|
createRelation(
|
||||||
|
"calendarMonth",
|
||||||
|
`${monthId}-${meta.signId}-${meta.index}-${rankKey}-${suitKey}`,
|
||||||
|
`Calendar month: ${monthName} (${meta.signName} decan ${meta.index})`,
|
||||||
|
{
|
||||||
|
monthId,
|
||||||
|
name: monthName,
|
||||||
|
monthOrder: monthNo,
|
||||||
|
signId: meta.signId,
|
||||||
|
signName: meta.signName,
|
||||||
|
decanIndex: meta.index,
|
||||||
|
dateRange: meta.dateRange?.label || null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (windowDecans.length) {
|
||||||
|
const firstRange = windowDecans[0].dateRange;
|
||||||
|
const lastRange = windowDecans[windowDecans.length - 1].dateRange;
|
||||||
|
const windowLabel = firstRange && lastRange
|
||||||
|
? `${formatMonthDayLabel(firstRange.start)}–${formatMonthDayLabel(lastRange.end)}`
|
||||||
|
: "--";
|
||||||
|
|
||||||
|
dynamicRelations.unshift(
|
||||||
|
createRelation(
|
||||||
|
"courtDateWindow",
|
||||||
|
`${rankKey}-${suitKey}`,
|
||||||
|
`Court date window: ${windowLabel}`,
|
||||||
|
{
|
||||||
|
dateStart: firstRange?.startToken || null,
|
||||||
|
dateEnd: lastRange?.endToken || null,
|
||||||
|
dateRange: windowLabel,
|
||||||
|
decanIds: windowDecanIds
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cards.push({
|
||||||
|
arcana: "Minor",
|
||||||
|
name: cardName,
|
||||||
|
number: null,
|
||||||
|
suit,
|
||||||
|
rank,
|
||||||
|
summary: `${rank} as ${courtInfo.role} within ${suitInfo.domain}.`,
|
||||||
|
meanings: {
|
||||||
|
upright: `${courtInfo.upright} In ${suit}, this guides ${suitInfo.domain}.`,
|
||||||
|
reversed: `${courtInfo.reversed} In ${suit}, this complicates ${suitInfo.domain}.`
|
||||||
|
},
|
||||||
|
keywords: [...courtInfo.keywords, ...suitInfo.keywords],
|
||||||
|
relations: [
|
||||||
|
createRelation("element", suitInfo.element, `Element: ${suitInfo.element}`, {
|
||||||
|
name: suitInfo.element
|
||||||
|
}),
|
||||||
|
createRelation(
|
||||||
|
"elementalFace",
|
||||||
|
`${rankKey}-${suitKey}`,
|
||||||
|
`${courtInfo.elementalFace} ${suitInfo.element}`,
|
||||||
|
{
|
||||||
|
rank,
|
||||||
|
suit,
|
||||||
|
elementalFace: courtInfo.elementalFace,
|
||||||
|
element: suitInfo.element
|
||||||
|
}
|
||||||
|
),
|
||||||
|
createRelation("courtRole", rankKey, `Court role: ${courtInfo.role}`, {
|
||||||
|
rank,
|
||||||
|
role: courtInfo.role
|
||||||
|
}),
|
||||||
|
...dynamicRelations
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTarotDatabase(referenceData, magickDataset = null) {
|
||||||
|
const cards = [
|
||||||
|
...buildMajorCards(referenceData, magickDataset),
|
||||||
|
...buildNumberMinorCards(referenceData),
|
||||||
|
...buildCourtMinorCards(referenceData)
|
||||||
|
];
|
||||||
|
|
||||||
|
return applyMeaningText(cards, referenceData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buildCourtMinorCards,
|
||||||
|
buildMajorCards,
|
||||||
|
buildNumberMinorCards,
|
||||||
|
buildTarotDatabase
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotDatabaseAssembly = {
|
||||||
|
createTarotDatabaseAssembly
|
||||||
|
};
|
||||||
|
})();
|
||||||
620
app/tarot-database-builders.js
Normal file
620
app/tarot-database-builders.js
Normal file
@@ -0,0 +1,620 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function createTarotDatabaseHelpers(config) {
|
||||||
|
const majorCards = Array.isArray(config?.majorCards) ? config.majorCards : [];
|
||||||
|
const suits = Array.isArray(config?.suits) ? config.suits : [];
|
||||||
|
const numberRanks = Array.isArray(config?.numberRanks) ? config.numberRanks : [];
|
||||||
|
const courtRanks = Array.isArray(config?.courtRanks) ? config.courtRanks : [];
|
||||||
|
const suitInfo = config?.suitInfo && typeof config.suitInfo === "object" ? config.suitInfo : {};
|
||||||
|
const rankInfo = config?.rankInfo && typeof config.rankInfo === "object" ? config.rankInfo : {};
|
||||||
|
const courtInfo = config?.courtInfo && typeof config.courtInfo === "object" ? config.courtInfo : {};
|
||||||
|
const courtDecanWindows = config?.courtDecanWindows && typeof config.courtDecanWindows === "object" ? config.courtDecanWindows : {};
|
||||||
|
const majorAliases = config?.majorAliases && typeof config.majorAliases === "object" ? config.majorAliases : {};
|
||||||
|
const minorNumeralAliases = config?.minorNumeralAliases && typeof config.minorNumeralAliases === "object" ? config.minorNumeralAliases : {};
|
||||||
|
const monthNameByNumber = config?.monthNameByNumber && typeof config.monthNameByNumber === "object" ? config.monthNameByNumber : {};
|
||||||
|
const monthIdByNumber = config?.monthIdByNumber && typeof config.monthIdByNumber === "object" ? config.monthIdByNumber : {};
|
||||||
|
const monthShort = Array.isArray(config?.monthShort) ? config.monthShort : [];
|
||||||
|
const hebrewLetterAliases = config?.hebrewLetterAliases && typeof config.hebrewLetterAliases === "object" ? config.hebrewLetterAliases : {};
|
||||||
|
|
||||||
|
function getTarotDbConfig(referenceData) {
|
||||||
|
const db = referenceData?.tarotDatabase;
|
||||||
|
const hasDb = db && typeof db === "object";
|
||||||
|
|
||||||
|
return {
|
||||||
|
majorCards: hasDb && Array.isArray(db.majorCards) && db.majorCards.length ? db.majorCards : majorCards,
|
||||||
|
suits: hasDb && Array.isArray(db.suits) && db.suits.length ? db.suits : suits,
|
||||||
|
numberRanks: hasDb && Array.isArray(db.numberRanks) && db.numberRanks.length ? db.numberRanks : numberRanks,
|
||||||
|
courtRanks: hasDb && Array.isArray(db.courtRanks) && db.courtRanks.length ? db.courtRanks : courtRanks,
|
||||||
|
suitInfo: hasDb && db.suitInfo && typeof db.suitInfo === "object" ? db.suitInfo : suitInfo,
|
||||||
|
rankInfo: hasDb && db.rankInfo && typeof db.rankInfo === "object" ? db.rankInfo : rankInfo,
|
||||||
|
courtInfo: hasDb && db.courtInfo && typeof db.courtInfo === "object" ? db.courtInfo : courtInfo,
|
||||||
|
courtDecanWindows: hasDb && db.courtDecanWindows && typeof db.courtDecanWindows === "object" ? db.courtDecanWindows : courtDecanWindows
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCardName(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/^the\s+/, "")
|
||||||
|
.replace(/\s+/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalCardName(value) {
|
||||||
|
const normalized = normalizeCardName(value);
|
||||||
|
const majorCanonical = majorAliases[normalized] || normalized;
|
||||||
|
const withSuitAliases = majorCanonical.replace(/\bof\s+(pentacles?|coins?)\b/i, "of disks");
|
||||||
|
const numberMatch = withSuitAliases.match(/^(\d{1,2})\s+of\s+(.+)$/i);
|
||||||
|
|
||||||
|
if (numberMatch) {
|
||||||
|
const number = Number(numberMatch[1]);
|
||||||
|
const suit = String(numberMatch[2] || "").trim();
|
||||||
|
const numberWord = minorNumeralAliases[number];
|
||||||
|
if (numberWord && suit) {
|
||||||
|
return `${numberWord} of ${suit}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return withSuitAliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMonthDay(value) {
|
||||||
|
const [month, day] = String(value || "").split("-").map((part) => Number(part));
|
||||||
|
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { month, day };
|
||||||
|
}
|
||||||
|
|
||||||
|
function monthDayToDate(monthDay, year) {
|
||||||
|
const parsed = parseMonthDay(monthDay);
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Date(year, parsed.month - 1, parsed.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDays(date, days) {
|
||||||
|
const next = new Date(date);
|
||||||
|
next.setDate(next.getDate() + days);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMonthDayLabel(date) {
|
||||||
|
if (!(date instanceof Date)) {
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
return `${monthShort[date.getMonth()]} ${date.getDate()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMonthDayToken(date) {
|
||||||
|
if (!(date instanceof Date)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
return `${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMinorTarotCardName(value) {
|
||||||
|
const normalized = canonicalCardName(value);
|
||||||
|
return normalized
|
||||||
|
.split(" ")
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveSummaryFromMeaning(meaningText, fallbackSummary) {
|
||||||
|
const normalized = String(meaningText || "")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if (!normalized) {
|
||||||
|
return fallbackSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentenceMatch = normalized.match(/^(.+?[.!?])(?:\s|$)/);
|
||||||
|
if (sentenceMatch && sentenceMatch[1]) {
|
||||||
|
return sentenceMatch[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.length <= 220) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${normalized.slice(0, 217).trimEnd()}…`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMeaningText(cards, referenceData) {
|
||||||
|
const majorByTrumpNumber = referenceData?.tarotDatabase?.meanings?.majorByTrumpNumber;
|
||||||
|
const byCardName = referenceData?.tarotDatabase?.meanings?.byCardName;
|
||||||
|
|
||||||
|
if ((!majorByTrumpNumber || typeof majorByTrumpNumber !== "object")
|
||||||
|
&& (!byCardName || typeof byCardName !== "object")) {
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards.map((card) => {
|
||||||
|
const trumpNumber = Number(card?.number);
|
||||||
|
const isMajorTrumpCard = card?.arcana === "Major" && Number.isFinite(trumpNumber);
|
||||||
|
const canonicalName = canonicalCardName(card?.name);
|
||||||
|
|
||||||
|
const majorMeaning = isMajorTrumpCard
|
||||||
|
? String(majorByTrumpNumber?.[trumpNumber] || "").trim()
|
||||||
|
: "";
|
||||||
|
const nameMeaning = String(byCardName?.[canonicalName] || "").trim();
|
||||||
|
const selectedMeaning = majorMeaning || nameMeaning;
|
||||||
|
|
||||||
|
if (!selectedMeaning) {
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...card,
|
||||||
|
meaning: selectedMeaning,
|
||||||
|
summary: deriveSummaryFromMeaning(selectedMeaning, card.summary),
|
||||||
|
meanings: {
|
||||||
|
upright: selectedMeaning,
|
||||||
|
reversed: card?.meanings?.reversed || ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSignDateBounds(sign) {
|
||||||
|
const start = monthDayToDate(sign?.start, 2025);
|
||||||
|
const endBase = monthDayToDate(sign?.end, 2025);
|
||||||
|
if (!start || !endBase) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wraps = endBase.getTime() < start.getTime();
|
||||||
|
const end = wraps ? monthDayToDate(sign?.end, 2026) : endBase;
|
||||||
|
if (!end) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { start, end };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDecanDateRange(sign, decanIndex) {
|
||||||
|
const bounds = getSignDateBounds(sign);
|
||||||
|
if (!bounds || !Number.isFinite(Number(decanIndex))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = Number(decanIndex);
|
||||||
|
const start = addDays(bounds.start, (index - 1) * 10);
|
||||||
|
const nominalEnd = addDays(start, 9);
|
||||||
|
const end = nominalEnd.getTime() > bounds.end.getTime() ? bounds.end : nominalEnd;
|
||||||
|
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
startMonth: start.getMonth() + 1,
|
||||||
|
endMonth: end.getMonth() + 1,
|
||||||
|
startToken: formatMonthDayToken(start),
|
||||||
|
endToken: formatMonthDayToken(end),
|
||||||
|
label: `${formatMonthDayLabel(start)}–${formatMonthDayLabel(end)}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function listMonthNumbersBetween(start, end) {
|
||||||
|
if (!(start instanceof Date) || !(end instanceof Date)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
const seen = new Set();
|
||||||
|
const cursor = new Date(start.getFullYear(), start.getMonth(), 1);
|
||||||
|
const limit = new Date(end.getFullYear(), end.getMonth(), 1);
|
||||||
|
|
||||||
|
while (cursor.getTime() <= limit.getTime()) {
|
||||||
|
const monthNo = cursor.getMonth() + 1;
|
||||||
|
if (!seen.has(monthNo)) {
|
||||||
|
seen.add(monthNo);
|
||||||
|
result.push(monthNo);
|
||||||
|
}
|
||||||
|
cursor.setMonth(cursor.getMonth() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSignName(sign, fallback) {
|
||||||
|
return sign?.name?.en || sign?.name || sign?.id || fallback || "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDecanMetadata(decan, sign) {
|
||||||
|
if (!decan || !sign) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = Number(decan.index);
|
||||||
|
if (!Number.isFinite(index)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDegree = (index - 1) * 10;
|
||||||
|
const endDegree = startDegree + 10;
|
||||||
|
const dateRange = buildDecanDateRange(sign, index);
|
||||||
|
|
||||||
|
return {
|
||||||
|
decan,
|
||||||
|
sign,
|
||||||
|
index,
|
||||||
|
signId: sign.id,
|
||||||
|
signName: getSignName(sign, decan.signId),
|
||||||
|
signSymbol: sign.symbol || "",
|
||||||
|
startDegree,
|
||||||
|
endDegree,
|
||||||
|
dateRange,
|
||||||
|
normalizedCardName: normalizeMinorTarotCardName(decan.tarotMinorArcana || "")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRelationId(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/(^-|-$)/g, "") || "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRelation(type, id, label, data = null) {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
id: normalizeRelationId(id),
|
||||||
|
label: String(label || "").trim(),
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function relationSignature(value) {
|
||||||
|
if (!value || typeof value !== "object") {
|
||||||
|
return String(value || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${value.type || "text"}|${value.id || ""}|${value.label || ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLegacyRelation(text) {
|
||||||
|
const raw = String(text || "").trim();
|
||||||
|
const match = raw.match(/^([^:]+):\s*(.+)$/);
|
||||||
|
if (!match) {
|
||||||
|
return createRelation("note", raw, raw, { value: raw });
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = normalizeRelationId(match[1]);
|
||||||
|
const value = String(match[2] || "").trim();
|
||||||
|
|
||||||
|
if (key === "element") {
|
||||||
|
return createRelation("element", value, `Element: ${value}`, { name: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "planet") {
|
||||||
|
return createRelation("planet", value, `Planet: ${value}`, { name: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "zodiac") {
|
||||||
|
return createRelation("zodiac", value, `Zodiac: ${value}`, { name: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "suit-domain") {
|
||||||
|
return createRelation("suitDomain", value, `Suit domain: ${value}`, { value });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "numerology") {
|
||||||
|
const numeric = Number(value);
|
||||||
|
return createRelation("numerology", value, `Numerology: ${value}`, {
|
||||||
|
value: Number.isFinite(numeric) ? numeric : value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "court-role") {
|
||||||
|
return createRelation("courtRole", value, `Court role: ${value}`, { value });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "hebrew-letter") {
|
||||||
|
const normalized = normalizeHebrewKey(value);
|
||||||
|
const canonical = hebrewLetterAliases[normalized] || normalized;
|
||||||
|
return createRelation("hebrewLetter", canonical, `Hebrew Letter: ${value}`, {
|
||||||
|
requestedName: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return createRelation(key || "relation", value, raw, { value });
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHebrewKey(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z]/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHebrewLetterLookup(magickDataset) {
|
||||||
|
const letters = magickDataset?.grouped?.hebrewLetters;
|
||||||
|
const lookup = new Map();
|
||||||
|
|
||||||
|
if (!letters || typeof letters !== "object") {
|
||||||
|
return lookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(letters).forEach(([letterId, entry]) => {
|
||||||
|
const idKey = normalizeHebrewKey(letterId);
|
||||||
|
const canonicalKey = hebrewLetterAliases[idKey] || idKey;
|
||||||
|
|
||||||
|
if (canonicalKey && !lookup.has(canonicalKey)) {
|
||||||
|
lookup.set(canonicalKey, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameKey = normalizeHebrewKey(entry?.letter?.name);
|
||||||
|
const canonicalNameKey = hebrewLetterAliases[nameKey] || nameKey;
|
||||||
|
if (canonicalNameKey && !lookup.has(canonicalNameKey)) {
|
||||||
|
lookup.set(canonicalNameKey, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryIdKey = normalizeHebrewKey(entry?.id);
|
||||||
|
const canonicalEntryIdKey = hebrewLetterAliases[entryIdKey] || entryIdKey;
|
||||||
|
if (canonicalEntryIdKey && !lookup.has(canonicalEntryIdKey)) {
|
||||||
|
lookup.set(canonicalEntryIdKey, entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return lookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHebrewLetterRelation(hebrewLetterId, hebrewLookup) {
|
||||||
|
if (!hebrewLetterId || !hebrewLookup) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedId = normalizeHebrewKey(hebrewLetterId);
|
||||||
|
const canonicalId = hebrewLetterAliases[normalizedId] || normalizedId;
|
||||||
|
const entry = hebrewLookup.get(canonicalId);
|
||||||
|
if (!entry) {
|
||||||
|
return createRelation("hebrewLetter", canonicalId, `Hebrew Letter: ${hebrewLetterId}`, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const glyph = entry?.letter?.he || "";
|
||||||
|
const name = entry?.letter?.name || hebrewLetterId;
|
||||||
|
const latin = entry?.letter?.latin || "";
|
||||||
|
const index = Number.isFinite(entry?.index) ? entry.index : null;
|
||||||
|
const value = Number.isFinite(entry?.value) ? entry.value : null;
|
||||||
|
const meaning = entry?.meaning?.en || "";
|
||||||
|
|
||||||
|
const indexText = index !== null ? index : "?";
|
||||||
|
const valueText = value !== null ? value : "?";
|
||||||
|
const meaningText = meaning ? ` · ${meaning}` : "";
|
||||||
|
|
||||||
|
return createRelation(
|
||||||
|
"hebrewLetter",
|
||||||
|
entry?.id || canonicalId,
|
||||||
|
`Hebrew Letter: ${glyph} ${name} (${latin}) (index ${indexText}, value ${valueText})${meaningText}`.trim(),
|
||||||
|
{
|
||||||
|
id: entry?.id || canonicalId,
|
||||||
|
glyph,
|
||||||
|
name,
|
||||||
|
latin,
|
||||||
|
index,
|
||||||
|
value,
|
||||||
|
meaning
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushMapValue(map, key, value) {
|
||||||
|
if (!key || !value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = map.get(key);
|
||||||
|
const signature = relationSignature(value);
|
||||||
|
const duplicate = existing.some((entry) => relationSignature(entry) === signature);
|
||||||
|
|
||||||
|
if (!duplicate) {
|
||||||
|
existing.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectCalendarMonthRelationsFromDecan(targetKey, relationMap, decanMeta) {
|
||||||
|
const dateRange = decanMeta?.dateRange;
|
||||||
|
if (!dateRange?.start || !dateRange?.end) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthNumbers = listMonthNumbersBetween(dateRange.start, dateRange.end);
|
||||||
|
monthNumbers.forEach((monthNo) => {
|
||||||
|
const monthId = monthIdByNumber[monthNo];
|
||||||
|
const monthName = monthNameByNumber[monthNo] || `Month ${monthNo}`;
|
||||||
|
if (!monthId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushMapValue(
|
||||||
|
relationMap,
|
||||||
|
targetKey,
|
||||||
|
createRelation(
|
||||||
|
"calendarMonth",
|
||||||
|
`${monthId}-${decanMeta.signId}-${decanMeta.index}`,
|
||||||
|
`Calendar month: ${monthName} (${decanMeta.signName} decan ${decanMeta.index})`,
|
||||||
|
{
|
||||||
|
monthId,
|
||||||
|
name: monthName,
|
||||||
|
monthOrder: monthNo,
|
||||||
|
signId: decanMeta.signId,
|
||||||
|
signName: decanMeta.signName,
|
||||||
|
decanIndex: decanMeta.index,
|
||||||
|
dateRange: dateRange.label
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMajorDynamicRelations(referenceData) {
|
||||||
|
const relationMap = new Map();
|
||||||
|
|
||||||
|
const planets = referenceData?.planets && typeof referenceData.planets === "object"
|
||||||
|
? Object.values(referenceData.planets)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
planets.forEach((planet) => {
|
||||||
|
const cardName = planet?.tarot?.majorArcana;
|
||||||
|
if (!cardName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relation = createRelation(
|
||||||
|
"planetCorrespondence",
|
||||||
|
planet?.id || planet?.name || cardName,
|
||||||
|
`Planet correspondence: ${planet.symbol || ""} ${planet.name || ""}`.trim(),
|
||||||
|
{
|
||||||
|
planetId: planet?.id || null,
|
||||||
|
symbol: planet?.symbol || "",
|
||||||
|
name: planet?.name || ""
|
||||||
|
}
|
||||||
|
);
|
||||||
|
pushMapValue(relationMap, canonicalCardName(cardName), relation);
|
||||||
|
});
|
||||||
|
|
||||||
|
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
||||||
|
signs.forEach((sign) => {
|
||||||
|
const cardName = sign?.tarot?.majorArcana;
|
||||||
|
if (!cardName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relation = createRelation(
|
||||||
|
"zodiacCorrespondence",
|
||||||
|
sign?.id || sign?.name || cardName,
|
||||||
|
`Zodiac correspondence: ${sign.symbol || ""} ${sign.name || ""}`.trim(),
|
||||||
|
{
|
||||||
|
signId: sign?.id || null,
|
||||||
|
symbol: sign?.symbol || "",
|
||||||
|
name: sign?.name || ""
|
||||||
|
}
|
||||||
|
);
|
||||||
|
pushMapValue(relationMap, canonicalCardName(cardName), relation);
|
||||||
|
});
|
||||||
|
|
||||||
|
return relationMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMinorDecanRelations(referenceData) {
|
||||||
|
const relationMap = new Map();
|
||||||
|
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
||||||
|
const signById = Object.fromEntries(signs.map((sign) => [sign.id, sign]));
|
||||||
|
const planets = referenceData?.planets || {};
|
||||||
|
|
||||||
|
if (!referenceData?.decansBySign || typeof referenceData.decansBySign !== "object") {
|
||||||
|
return relationMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(referenceData.decansBySign).forEach(([signId, decans]) => {
|
||||||
|
const sign = signById[signId];
|
||||||
|
if (!Array.isArray(decans) || !sign) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
decans.forEach((decan) => {
|
||||||
|
const cardName = decan?.tarotMinorArcana;
|
||||||
|
if (!cardName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decanMeta = buildDecanMetadata(decan, sign);
|
||||||
|
if (!decanMeta) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { startDegree, endDegree, dateRange, signId: metaSignId, signName, signSymbol, index } = decanMeta;
|
||||||
|
const ruler = planets[decan.rulerPlanetId] || null;
|
||||||
|
const cardKey = canonicalCardName(cardName);
|
||||||
|
|
||||||
|
pushMapValue(
|
||||||
|
relationMap,
|
||||||
|
cardKey,
|
||||||
|
createRelation(
|
||||||
|
"zodiac",
|
||||||
|
metaSignId,
|
||||||
|
`Zodiac: ${sign.symbol || ""} ${signName}`.trim(),
|
||||||
|
{
|
||||||
|
signId: metaSignId,
|
||||||
|
signName,
|
||||||
|
symbol: sign.symbol || ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
pushMapValue(
|
||||||
|
relationMap,
|
||||||
|
cardKey,
|
||||||
|
createRelation(
|
||||||
|
"decan",
|
||||||
|
`${metaSignId}-${index}`,
|
||||||
|
`Decan ${decan.index}: ${sign.symbol || ""} ${signName} (${startDegree}°–${endDegree}°)${dateRange ? ` · ${dateRange.label}` : ""}`.trim(),
|
||||||
|
{
|
||||||
|
signId: metaSignId,
|
||||||
|
signName,
|
||||||
|
signSymbol,
|
||||||
|
index,
|
||||||
|
startDegree,
|
||||||
|
endDegree,
|
||||||
|
dateStart: dateRange?.startToken || null,
|
||||||
|
dateEnd: dateRange?.endToken || null,
|
||||||
|
dateRange: dateRange?.label || null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
collectCalendarMonthRelationsFromDecan(cardKey, relationMap, decanMeta);
|
||||||
|
|
||||||
|
if (ruler) {
|
||||||
|
pushMapValue(
|
||||||
|
relationMap,
|
||||||
|
cardKey,
|
||||||
|
createRelation(
|
||||||
|
"decanRuler",
|
||||||
|
`${metaSignId}-${index}-${decan.rulerPlanetId}`,
|
||||||
|
`Decan ruler: ${ruler.symbol || ""} ${ruler.name || decan.rulerPlanetId}`.trim(),
|
||||||
|
{
|
||||||
|
signId: metaSignId,
|
||||||
|
decanIndex: index,
|
||||||
|
planetId: decan.rulerPlanetId,
|
||||||
|
symbol: ruler.symbol || "",
|
||||||
|
name: ruler.name || decan.rulerPlanetId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return relationMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getTarotDbConfig,
|
||||||
|
canonicalCardName,
|
||||||
|
formatMonthDayLabel,
|
||||||
|
applyMeaningText,
|
||||||
|
buildDecanMetadata,
|
||||||
|
listMonthNumbersBetween,
|
||||||
|
buildHebrewLetterLookup,
|
||||||
|
createRelation,
|
||||||
|
parseLegacyRelation,
|
||||||
|
buildHebrewLetterRelation,
|
||||||
|
buildMajorDynamicRelations,
|
||||||
|
buildMinorDecanRelations
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotDatabaseBuilders = { createTarotDatabaseHelpers };
|
||||||
|
})();
|
||||||
@@ -459,52 +459,49 @@
|
|||||||
tav: "tav"
|
tav: "tav"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createTarotDatabaseHelpers = window.TarotDatabaseBuilders?.createTarotDatabaseHelpers;
|
||||||
|
const createTarotDatabaseAssembly = window.TarotDatabaseAssembly?.createTarotDatabaseAssembly;
|
||||||
|
if (typeof createTarotDatabaseHelpers !== "function" || typeof createTarotDatabaseAssembly !== "function") {
|
||||||
|
throw new Error("TarotDatabaseBuilders and TarotDatabaseAssembly modules must load before tarot-database.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tarotDatabaseBuilders = createTarotDatabaseHelpers({
|
||||||
|
majorCards: MAJOR_CARDS,
|
||||||
|
suits: SUITS,
|
||||||
|
numberRanks: NUMBER_RANKS,
|
||||||
|
courtRanks: COURT_RANKS,
|
||||||
|
suitInfo: SUIT_INFO,
|
||||||
|
rankInfo: RANK_INFO,
|
||||||
|
courtInfo: COURT_INFO,
|
||||||
|
courtDecanWindows: COURT_DECAN_WINDOWS,
|
||||||
|
majorAliases: MAJOR_ALIASES,
|
||||||
|
minorNumeralAliases: MINOR_NUMERAL_ALIASES,
|
||||||
|
monthNameByNumber: MONTH_NAME_BY_NUMBER,
|
||||||
|
monthIdByNumber: MONTH_ID_BY_NUMBER,
|
||||||
|
monthShort: MONTH_SHORT,
|
||||||
|
hebrewLetterAliases: HEBREW_LETTER_ALIASES
|
||||||
|
});
|
||||||
|
|
||||||
|
const tarotDatabaseAssembly = createTarotDatabaseAssembly({
|
||||||
|
getTarotDbConfig: tarotDatabaseBuilders.getTarotDbConfig,
|
||||||
|
canonicalCardName: tarotDatabaseBuilders.canonicalCardName,
|
||||||
|
formatMonthDayLabel: tarotDatabaseBuilders.formatMonthDayLabel,
|
||||||
|
applyMeaningText: tarotDatabaseBuilders.applyMeaningText,
|
||||||
|
buildDecanMetadata: tarotDatabaseBuilders.buildDecanMetadata,
|
||||||
|
listMonthNumbersBetween: tarotDatabaseBuilders.listMonthNumbersBetween,
|
||||||
|
buildHebrewLetterLookup: tarotDatabaseBuilders.buildHebrewLetterLookup,
|
||||||
|
createRelation: tarotDatabaseBuilders.createRelation,
|
||||||
|
parseLegacyRelation: tarotDatabaseBuilders.parseLegacyRelation,
|
||||||
|
buildHebrewLetterRelation: tarotDatabaseBuilders.buildHebrewLetterRelation,
|
||||||
|
buildMajorDynamicRelations: tarotDatabaseBuilders.buildMajorDynamicRelations,
|
||||||
|
buildMinorDecanRelations: tarotDatabaseBuilders.buildMinorDecanRelations,
|
||||||
|
monthNameByNumber: MONTH_NAME_BY_NUMBER,
|
||||||
|
monthIdByNumber: MONTH_ID_BY_NUMBER,
|
||||||
|
majorHebrewLetterIdByCard: MAJOR_HEBREW_LETTER_ID_BY_CARD
|
||||||
|
});
|
||||||
|
|
||||||
function getTarotDbConfig(referenceData) {
|
function getTarotDbConfig(referenceData) {
|
||||||
const db = referenceData?.tarotDatabase;
|
return tarotDatabaseBuilders.getTarotDbConfig(referenceData);
|
||||||
const hasDb = db && typeof db === "object";
|
|
||||||
|
|
||||||
const majorCards = hasDb && Array.isArray(db.majorCards) && db.majorCards.length
|
|
||||||
? db.majorCards
|
|
||||||
: MAJOR_CARDS;
|
|
||||||
|
|
||||||
const suits = hasDb && Array.isArray(db.suits) && db.suits.length
|
|
||||||
? db.suits
|
|
||||||
: SUITS;
|
|
||||||
|
|
||||||
const numberRanks = hasDb && Array.isArray(db.numberRanks) && db.numberRanks.length
|
|
||||||
? db.numberRanks
|
|
||||||
: NUMBER_RANKS;
|
|
||||||
|
|
||||||
const courtRanks = hasDb && Array.isArray(db.courtRanks) && db.courtRanks.length
|
|
||||||
? db.courtRanks
|
|
||||||
: COURT_RANKS;
|
|
||||||
|
|
||||||
const suitInfo = hasDb && db.suitInfo && typeof db.suitInfo === "object"
|
|
||||||
? db.suitInfo
|
|
||||||
: SUIT_INFO;
|
|
||||||
|
|
||||||
const rankInfo = hasDb && db.rankInfo && typeof db.rankInfo === "object"
|
|
||||||
? db.rankInfo
|
|
||||||
: RANK_INFO;
|
|
||||||
|
|
||||||
const courtInfo = hasDb && db.courtInfo && typeof db.courtInfo === "object"
|
|
||||||
? db.courtInfo
|
|
||||||
: COURT_INFO;
|
|
||||||
|
|
||||||
const courtDecanWindows = hasDb && db.courtDecanWindows && typeof db.courtDecanWindows === "object"
|
|
||||||
? db.courtDecanWindows
|
|
||||||
: COURT_DECAN_WINDOWS;
|
|
||||||
|
|
||||||
return {
|
|
||||||
majorCards,
|
|
||||||
suits,
|
|
||||||
numberRanks,
|
|
||||||
courtRanks,
|
|
||||||
suitInfo,
|
|
||||||
rankInfo,
|
|
||||||
courtInfo,
|
|
||||||
courtDecanWindows
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeCardName(value) {
|
function normalizeCardName(value) {
|
||||||
@@ -516,290 +513,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function canonicalCardName(value) {
|
function canonicalCardName(value) {
|
||||||
const normalized = normalizeCardName(value);
|
return tarotDatabaseBuilders.canonicalCardName(value);
|
||||||
const majorCanonical = MAJOR_ALIASES[normalized] || normalized;
|
|
||||||
const withSuitAliases = majorCanonical.replace(/\bof\s+(pentacles?|coins?)\b/i, "of disks");
|
|
||||||
const numberMatch = withSuitAliases.match(/^(\d{1,2})\s+of\s+(.+)$/i);
|
|
||||||
|
|
||||||
if (numberMatch) {
|
|
||||||
const number = Number(numberMatch[1]);
|
|
||||||
const suit = String(numberMatch[2] || "").trim();
|
|
||||||
const numberWord = MINOR_NUMERAL_ALIASES[number];
|
|
||||||
if (numberWord && suit) {
|
|
||||||
return `${numberWord} of ${suit}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return withSuitAliases;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMonthDay(value) {
|
|
||||||
const [month, day] = String(value || "").split("-").map((part) => Number(part));
|
|
||||||
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return { month, day };
|
|
||||||
}
|
|
||||||
|
|
||||||
function monthDayToDate(monthDay, year) {
|
|
||||||
const parsed = parseMonthDay(monthDay);
|
|
||||||
if (!parsed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Date(year, parsed.month - 1, parsed.day);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addDays(date, days) {
|
|
||||||
const next = new Date(date);
|
|
||||||
next.setDate(next.getDate() + days);
|
|
||||||
return next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMonthDayLabel(date) {
|
function formatMonthDayLabel(date) {
|
||||||
if (!(date instanceof Date)) {
|
return tarotDatabaseBuilders.formatMonthDayLabel(date);
|
||||||
return "--";
|
|
||||||
}
|
|
||||||
return `${MONTH_SHORT[date.getMonth()]} ${date.getDate()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMonthDayToken(date) {
|
|
||||||
if (!(date instanceof Date)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
||||||
const day = String(date.getDate()).padStart(2, "0");
|
|
||||||
return `${month}-${day}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeMinorTarotCardName(value) {
|
|
||||||
const normalized = canonicalCardName(value);
|
|
||||||
return normalized
|
|
||||||
.split(" ")
|
|
||||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
||||||
.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function deriveSummaryFromMeaning(meaningText, fallbackSummary) {
|
|
||||||
const normalized = String(meaningText || "")
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
if (!normalized) {
|
|
||||||
return fallbackSummary;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sentenceMatch = normalized.match(/^(.+?[.!?])(?:\s|$)/);
|
|
||||||
if (sentenceMatch && sentenceMatch[1]) {
|
|
||||||
return sentenceMatch[1].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalized.length <= 220) {
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${normalized.slice(0, 217).trimEnd()}…`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyMeaningText(cards, referenceData) {
|
function applyMeaningText(cards, referenceData) {
|
||||||
const majorByTrumpNumber = referenceData?.tarotDatabase?.meanings?.majorByTrumpNumber;
|
return tarotDatabaseBuilders.applyMeaningText(cards, referenceData);
|
||||||
const byCardName = referenceData?.tarotDatabase?.meanings?.byCardName;
|
|
||||||
|
|
||||||
if ((!majorByTrumpNumber || typeof majorByTrumpNumber !== "object")
|
|
||||||
&& (!byCardName || typeof byCardName !== "object")) {
|
|
||||||
return cards;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cards.map((card) => {
|
|
||||||
const trumpNumber = Number(card?.number);
|
|
||||||
const isMajorTrumpCard = card?.arcana === "Major" && Number.isFinite(trumpNumber);
|
|
||||||
const canonicalName = canonicalCardName(card?.name);
|
|
||||||
|
|
||||||
const majorMeaning = isMajorTrumpCard
|
|
||||||
? String(majorByTrumpNumber?.[trumpNumber] || "").trim()
|
|
||||||
: "";
|
|
||||||
const nameMeaning = String(byCardName?.[canonicalName] || "").trim();
|
|
||||||
const selectedMeaning = majorMeaning || nameMeaning;
|
|
||||||
|
|
||||||
if (!selectedMeaning) {
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...card,
|
|
||||||
meaning: selectedMeaning,
|
|
||||||
summary: deriveSummaryFromMeaning(selectedMeaning, card.summary),
|
|
||||||
meanings: {
|
|
||||||
upright: selectedMeaning,
|
|
||||||
reversed: card?.meanings?.reversed || ""
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSignDateBounds(sign) {
|
|
||||||
const start = monthDayToDate(sign?.start, 2025);
|
|
||||||
const endBase = monthDayToDate(sign?.end, 2025);
|
|
||||||
if (!start || !endBase) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wraps = endBase.getTime() < start.getTime();
|
|
||||||
const end = wraps ? monthDayToDate(sign?.end, 2026) : endBase;
|
|
||||||
if (!end) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildDecanDateRange(sign, decanIndex) {
|
|
||||||
const bounds = getSignDateBounds(sign);
|
|
||||||
if (!bounds || !Number.isFinite(Number(decanIndex))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = Number(decanIndex);
|
|
||||||
const start = addDays(bounds.start, (index - 1) * 10);
|
|
||||||
const nominalEnd = addDays(start, 9);
|
|
||||||
const end = nominalEnd.getTime() > bounds.end.getTime() ? bounds.end : nominalEnd;
|
|
||||||
|
|
||||||
return {
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
startMonth: start.getMonth() + 1,
|
|
||||||
endMonth: end.getMonth() + 1,
|
|
||||||
startToken: formatMonthDayToken(start),
|
|
||||||
endToken: formatMonthDayToken(end),
|
|
||||||
label: `${formatMonthDayLabel(start)}–${formatMonthDayLabel(end)}`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function listMonthNumbersBetween(start, end) {
|
function listMonthNumbersBetween(start, end) {
|
||||||
if (!(start instanceof Date) || !(end instanceof Date)) {
|
return tarotDatabaseBuilders.listMonthNumbersBetween(start, end);
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = [];
|
|
||||||
const seen = new Set();
|
|
||||||
const cursor = new Date(start.getFullYear(), start.getMonth(), 1);
|
|
||||||
const limit = new Date(end.getFullYear(), end.getMonth(), 1);
|
|
||||||
|
|
||||||
while (cursor.getTime() <= limit.getTime()) {
|
|
||||||
const monthNo = cursor.getMonth() + 1;
|
|
||||||
if (!seen.has(monthNo)) {
|
|
||||||
seen.add(monthNo);
|
|
||||||
result.push(monthNo);
|
|
||||||
}
|
|
||||||
cursor.setMonth(cursor.getMonth() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSignName(sign, fallback) {
|
|
||||||
return sign?.name?.en || sign?.name || sign?.id || fallback || "Unknown";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDecanMetadata(decan, sign) {
|
function buildDecanMetadata(decan, sign) {
|
||||||
if (!decan || !sign) {
|
return tarotDatabaseBuilders.buildDecanMetadata(decan, sign);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = Number(decan.index);
|
|
||||||
if (!Number.isFinite(index)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startDegree = (index - 1) * 10;
|
|
||||||
const endDegree = startDegree + 10;
|
|
||||||
const dateRange = buildDecanDateRange(sign, index);
|
|
||||||
|
|
||||||
return {
|
|
||||||
decan,
|
|
||||||
sign,
|
|
||||||
index,
|
|
||||||
signId: sign.id,
|
|
||||||
signName: getSignName(sign, decan.signId),
|
|
||||||
signSymbol: sign.symbol || "",
|
|
||||||
startDegree,
|
|
||||||
endDegree,
|
|
||||||
dateRange,
|
|
||||||
normalizedCardName: normalizeMinorTarotCardName(decan.tarotMinorArcana || "")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectCalendarMonthRelationsFromDecan(targetKey, relationMap, decanMeta) {
|
|
||||||
const dateRange = decanMeta?.dateRange;
|
|
||||||
if (!dateRange?.start || !dateRange?.end) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthNumbers = listMonthNumbersBetween(dateRange.start, dateRange.end);
|
|
||||||
monthNumbers.forEach((monthNo) => {
|
|
||||||
const monthId = MONTH_ID_BY_NUMBER[monthNo];
|
|
||||||
const monthName = MONTH_NAME_BY_NUMBER[monthNo] || `Month ${monthNo}`;
|
|
||||||
if (!monthId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pushMapValue(
|
|
||||||
relationMap,
|
|
||||||
targetKey,
|
|
||||||
createRelation(
|
|
||||||
"calendarMonth",
|
|
||||||
`${monthId}-${decanMeta.signId}-${decanMeta.index}`,
|
|
||||||
`Calendar month: ${monthName} (${decanMeta.signName} decan ${decanMeta.index})`,
|
|
||||||
{
|
|
||||||
monthId,
|
|
||||||
name: monthName,
|
|
||||||
monthOrder: monthNo,
|
|
||||||
signId: decanMeta.signId,
|
|
||||||
signName: decanMeta.signName,
|
|
||||||
decanIndex: decanMeta.index,
|
|
||||||
dateRange: dateRange.label
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeHebrewKey(value) {
|
|
||||||
return String(value || "")
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z]/g, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildHebrewLetterLookup(magickDataset) {
|
function buildHebrewLetterLookup(magickDataset) {
|
||||||
const letters = magickDataset?.grouped?.hebrewLetters;
|
return tarotDatabaseBuilders.buildHebrewLetterLookup(magickDataset);
|
||||||
const lookup = new Map();
|
|
||||||
|
|
||||||
if (!letters || typeof letters !== "object") {
|
|
||||||
return lookup;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(letters).forEach(([letterId, entry]) => {
|
|
||||||
const idKey = normalizeHebrewKey(letterId);
|
|
||||||
const canonicalKey = HEBREW_LETTER_ALIASES[idKey] || idKey;
|
|
||||||
|
|
||||||
if (canonicalKey && !lookup.has(canonicalKey)) {
|
|
||||||
lookup.set(canonicalKey, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nameKey = normalizeHebrewKey(entry?.letter?.name);
|
|
||||||
const canonicalNameKey = HEBREW_LETTER_ALIASES[nameKey] || nameKey;
|
|
||||||
if (canonicalNameKey && !lookup.has(canonicalNameKey)) {
|
|
||||||
lookup.set(canonicalNameKey, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
const entryIdKey = normalizeHebrewKey(entry?.id);
|
|
||||||
const canonicalEntryIdKey = HEBREW_LETTER_ALIASES[entryIdKey] || entryIdKey;
|
|
||||||
if (canonicalEntryIdKey && !lookup.has(canonicalEntryIdKey)) {
|
|
||||||
lookup.set(canonicalEntryIdKey, entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return lookup;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeRelationId(value) {
|
function normalizeRelationId(value) {
|
||||||
@@ -811,528 +545,39 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createRelation(type, id, label, data = null) {
|
function createRelation(type, id, label, data = null) {
|
||||||
return {
|
return tarotDatabaseBuilders.createRelation(type, id, label, data);
|
||||||
type,
|
|
||||||
id: normalizeRelationId(id),
|
|
||||||
label: String(label || "").trim(),
|
|
||||||
data
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function relationSignature(value) {
|
|
||||||
if (!value || typeof value !== "object") {
|
|
||||||
return String(value || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${value.type || "text"}|${value.id || ""}|${value.label || ""}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLegacyRelation(text) {
|
function parseLegacyRelation(text) {
|
||||||
const raw = String(text || "").trim();
|
return tarotDatabaseBuilders.parseLegacyRelation(text);
|
||||||
const match = raw.match(/^([^:]+):\s*(.+)$/);
|
|
||||||
if (!match) {
|
|
||||||
return createRelation("note", raw, raw, { value: raw });
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = normalizeRelationId(match[1]);
|
|
||||||
const value = String(match[2] || "").trim();
|
|
||||||
|
|
||||||
if (key === "element") {
|
|
||||||
return createRelation("element", value, `Element: ${value}`, { name: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === "planet") {
|
|
||||||
return createRelation("planet", value, `Planet: ${value}`, { name: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === "zodiac") {
|
|
||||||
return createRelation("zodiac", value, `Zodiac: ${value}`, { name: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === "suit-domain") {
|
|
||||||
return createRelation("suitDomain", value, `Suit domain: ${value}`, { value });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === "numerology") {
|
|
||||||
const numeric = Number(value);
|
|
||||||
return createRelation("numerology", value, `Numerology: ${value}`, {
|
|
||||||
value: Number.isFinite(numeric) ? numeric : value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === "court-role") {
|
|
||||||
return createRelation("courtRole", value, `Court role: ${value}`, { value });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === "hebrew-letter") {
|
|
||||||
const normalized = normalizeHebrewKey(value);
|
|
||||||
const canonical = HEBREW_LETTER_ALIASES[normalized] || normalized;
|
|
||||||
return createRelation("hebrewLetter", canonical, `Hebrew Letter: ${value}`, {
|
|
||||||
requestedName: value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return createRelation(key || "relation", value, raw, { value });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildHebrewLetterRelation(hebrewLetterId, hebrewLookup) {
|
function buildHebrewLetterRelation(hebrewLetterId, hebrewLookup) {
|
||||||
if (!hebrewLetterId || !hebrewLookup) {
|
return tarotDatabaseBuilders.buildHebrewLetterRelation(hebrewLetterId, hebrewLookup);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedId = normalizeHebrewKey(hebrewLetterId);
|
|
||||||
const canonicalId = HEBREW_LETTER_ALIASES[normalizedId] || normalizedId;
|
|
||||||
const entry = hebrewLookup.get(canonicalId);
|
|
||||||
if (!entry) {
|
|
||||||
return createRelation("hebrewLetter", canonicalId, `Hebrew Letter: ${hebrewLetterId}`, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const glyph = entry?.letter?.he || "";
|
|
||||||
const name = entry?.letter?.name || hebrewLetterId;
|
|
||||||
const latin = entry?.letter?.latin || "";
|
|
||||||
const index = Number.isFinite(entry?.index) ? entry.index : null;
|
|
||||||
const value = Number.isFinite(entry?.value) ? entry.value : null;
|
|
||||||
const meaning = entry?.meaning?.en || "";
|
|
||||||
|
|
||||||
const indexText = index !== null ? index : "?";
|
|
||||||
const valueText = value !== null ? value : "?";
|
|
||||||
const meaningText = meaning ? ` · ${meaning}` : "";
|
|
||||||
|
|
||||||
return createRelation(
|
|
||||||
"hebrewLetter",
|
|
||||||
entry?.id || canonicalId,
|
|
||||||
`Hebrew Letter: ${glyph} ${name} (${latin}) (index ${indexText}, value ${valueText})${meaningText}`.trim(),
|
|
||||||
{
|
|
||||||
id: entry?.id || canonicalId,
|
|
||||||
glyph,
|
|
||||||
name,
|
|
||||||
latin,
|
|
||||||
index,
|
|
||||||
value,
|
|
||||||
meaning
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushMapValue(map, key, value) {
|
|
||||||
if (!key || !value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map.has(key)) {
|
|
||||||
map.set(key, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existing = map.get(key);
|
|
||||||
const signature = relationSignature(value);
|
|
||||||
const duplicate = existing.some((entry) => relationSignature(entry) === signature);
|
|
||||||
|
|
||||||
if (!duplicate) {
|
|
||||||
existing.push(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMajorDynamicRelations(referenceData) {
|
function buildMajorDynamicRelations(referenceData) {
|
||||||
const relationMap = new Map();
|
return tarotDatabaseBuilders.buildMajorDynamicRelations(referenceData);
|
||||||
|
|
||||||
const planets = referenceData?.planets && typeof referenceData.planets === "object"
|
|
||||||
? Object.values(referenceData.planets)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
planets.forEach((planet) => {
|
|
||||||
const cardName = planet?.tarot?.majorArcana;
|
|
||||||
if (!cardName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relation = createRelation(
|
|
||||||
"planetCorrespondence",
|
|
||||||
planet?.id || planet?.name || cardName,
|
|
||||||
`Planet correspondence: ${planet.symbol || ""} ${planet.name || ""}`.trim(),
|
|
||||||
{
|
|
||||||
planetId: planet?.id || null,
|
|
||||||
symbol: planet?.symbol || "",
|
|
||||||
name: planet?.name || ""
|
|
||||||
}
|
|
||||||
);
|
|
||||||
pushMapValue(relationMap, canonicalCardName(cardName), relation);
|
|
||||||
});
|
|
||||||
|
|
||||||
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
|
||||||
signs.forEach((sign) => {
|
|
||||||
const cardName = sign?.tarot?.majorArcana;
|
|
||||||
if (!cardName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relation = createRelation(
|
|
||||||
"zodiacCorrespondence",
|
|
||||||
sign?.id || sign?.name || cardName,
|
|
||||||
`Zodiac correspondence: ${sign.symbol || ""} ${sign.name || ""}`.trim(),
|
|
||||||
{
|
|
||||||
signId: sign?.id || null,
|
|
||||||
symbol: sign?.symbol || "",
|
|
||||||
name: sign?.name || ""
|
|
||||||
}
|
|
||||||
);
|
|
||||||
pushMapValue(relationMap, canonicalCardName(cardName), relation);
|
|
||||||
});
|
|
||||||
|
|
||||||
return relationMap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMinorDecanRelations(referenceData) {
|
function buildMinorDecanRelations(referenceData) {
|
||||||
const relationMap = new Map();
|
return tarotDatabaseBuilders.buildMinorDecanRelations(referenceData);
|
||||||
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
|
||||||
const signById = Object.fromEntries(signs.map((sign) => [sign.id, sign]));
|
|
||||||
const planets = referenceData?.planets || {};
|
|
||||||
|
|
||||||
if (!referenceData?.decansBySign || typeof referenceData.decansBySign !== "object") {
|
|
||||||
return relationMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(referenceData.decansBySign).forEach(([signId, decans]) => {
|
|
||||||
const sign = signById[signId];
|
|
||||||
if (!Array.isArray(decans) || !sign) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
decans.forEach((decan) => {
|
|
||||||
const cardName = decan?.tarotMinorArcana;
|
|
||||||
if (!cardName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const decanMeta = buildDecanMetadata(decan, sign);
|
|
||||||
if (!decanMeta) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { startDegree, endDegree, dateRange, signId, signName, signSymbol, index } = decanMeta;
|
|
||||||
const ruler = planets[decan.rulerPlanetId] || null;
|
|
||||||
const cardKey = canonicalCardName(cardName);
|
|
||||||
|
|
||||||
pushMapValue(
|
|
||||||
relationMap,
|
|
||||||
cardKey,
|
|
||||||
createRelation(
|
|
||||||
"zodiac",
|
|
||||||
signId,
|
|
||||||
`Zodiac: ${sign.symbol || ""} ${signName}`.trim(),
|
|
||||||
{
|
|
||||||
signId,
|
|
||||||
signName,
|
|
||||||
symbol: sign.symbol || ""
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
pushMapValue(
|
|
||||||
relationMap,
|
|
||||||
cardKey,
|
|
||||||
createRelation(
|
|
||||||
"decan",
|
|
||||||
`${signId}-${index}`,
|
|
||||||
`Decan ${decan.index}: ${sign.symbol || ""} ${signName} (${startDegree}°–${endDegree}°)${dateRange ? ` · ${dateRange.label}` : ""}`.trim(),
|
|
||||||
{
|
|
||||||
signId,
|
|
||||||
signName,
|
|
||||||
signSymbol,
|
|
||||||
index,
|
|
||||||
startDegree,
|
|
||||||
endDegree,
|
|
||||||
dateStart: dateRange?.startToken || null,
|
|
||||||
dateEnd: dateRange?.endToken || null,
|
|
||||||
dateRange: dateRange?.label || null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
collectCalendarMonthRelationsFromDecan(cardKey, relationMap, decanMeta);
|
|
||||||
|
|
||||||
if (ruler) {
|
|
||||||
pushMapValue(
|
|
||||||
relationMap,
|
|
||||||
cardKey,
|
|
||||||
createRelation(
|
|
||||||
"decanRuler",
|
|
||||||
`${signId}-${index}-${decan.rulerPlanetId}`,
|
|
||||||
`Decan ruler: ${ruler.symbol || ""} ${ruler.name || decan.rulerPlanetId}`.trim(),
|
|
||||||
{
|
|
||||||
signId,
|
|
||||||
decanIndex: index,
|
|
||||||
planetId: decan.rulerPlanetId,
|
|
||||||
symbol: ruler.symbol || "",
|
|
||||||
name: ruler.name || decan.rulerPlanetId
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return relationMap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMajorCards(referenceData, magickDataset) {
|
function buildMajorCards(referenceData, magickDataset) {
|
||||||
const tarotDb = getTarotDbConfig(referenceData);
|
return tarotDatabaseAssembly.buildMajorCards(referenceData, magickDataset);
|
||||||
const dynamicRelations = buildMajorDynamicRelations(referenceData);
|
|
||||||
const hebrewLookup = buildHebrewLetterLookup(magickDataset);
|
|
||||||
|
|
||||||
return tarotDb.majorCards.map((card) => {
|
|
||||||
const canonicalName = canonicalCardName(card.name);
|
|
||||||
const dynamic = dynamicRelations.get(canonicalName) || [];
|
|
||||||
const hebrewLetterId = MAJOR_HEBREW_LETTER_ID_BY_CARD[canonicalName] || null;
|
|
||||||
const hebrewLetterRelation = buildHebrewLetterRelation(hebrewLetterId, hebrewLookup);
|
|
||||||
const staticRelations = (card.relations || [])
|
|
||||||
.map((relation) => parseLegacyRelation(relation))
|
|
||||||
.filter((relation) => relation.type !== "hebrewLetter" && relation.type !== "zodiac" && relation.type !== "planet");
|
|
||||||
|
|
||||||
return {
|
|
||||||
arcana: "Major",
|
|
||||||
name: card.name,
|
|
||||||
number: card.number,
|
|
||||||
suit: null,
|
|
||||||
rank: null,
|
|
||||||
hebrewLetterId,
|
|
||||||
hebrewLetter: hebrewLetterRelation?.data || null,
|
|
||||||
summary: card.summary,
|
|
||||||
meanings: {
|
|
||||||
upright: card.upright,
|
|
||||||
reversed: card.reversed
|
|
||||||
},
|
|
||||||
keywords: [...card.keywords],
|
|
||||||
relations: [
|
|
||||||
...staticRelations,
|
|
||||||
...(hebrewLetterRelation ? [hebrewLetterRelation] : []),
|
|
||||||
...dynamic
|
|
||||||
]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildNumberMinorCards(referenceData) {
|
function buildNumberMinorCards(referenceData) {
|
||||||
const tarotDb = getTarotDbConfig(referenceData);
|
return tarotDatabaseAssembly.buildNumberMinorCards(referenceData);
|
||||||
const decanRelations = buildMinorDecanRelations(referenceData);
|
|
||||||
const cards = [];
|
|
||||||
|
|
||||||
tarotDb.suits.forEach((suit) => {
|
|
||||||
const suitKey = suit.toLowerCase();
|
|
||||||
const suitInfo = tarotDb.suitInfo[suitKey];
|
|
||||||
if (!suitInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tarotDb.numberRanks.forEach((rank) => {
|
|
||||||
const rankKey = rank.toLowerCase();
|
|
||||||
const rankInfo = tarotDb.rankInfo[rankKey];
|
|
||||||
if (!rankInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const cardName = `${rank} of ${suit}`;
|
|
||||||
const dynamicRelations = decanRelations.get(canonicalCardName(cardName)) || [];
|
|
||||||
|
|
||||||
cards.push({
|
|
||||||
arcana: "Minor",
|
|
||||||
name: cardName,
|
|
||||||
number: null,
|
|
||||||
suit,
|
|
||||||
rank,
|
|
||||||
summary: `${rank} energy expressed through ${suitInfo.domain}.`,
|
|
||||||
meanings: {
|
|
||||||
upright: `${rankInfo.upright} In ${suit}, this emphasizes ${suitInfo.domain}.`,
|
|
||||||
reversed: `${rankInfo.reversed} In ${suit}, this may distort ${suitInfo.domain}.`
|
|
||||||
},
|
|
||||||
keywords: [...rankInfo.keywords, ...suitInfo.keywords],
|
|
||||||
relations: [
|
|
||||||
createRelation("element", suitInfo.element, `Element: ${suitInfo.element}`, {
|
|
||||||
name: suitInfo.element
|
|
||||||
}),
|
|
||||||
createRelation("suitDomain", `${suitKey}-${rankKey}`, `Suit domain: ${suitInfo.domain}`, {
|
|
||||||
suit: suit,
|
|
||||||
rank,
|
|
||||||
domain: suitInfo.domain
|
|
||||||
}),
|
|
||||||
createRelation("numerology", rankInfo.number, `Numerology: ${rankInfo.number}`, {
|
|
||||||
value: rankInfo.number
|
|
||||||
}),
|
|
||||||
...dynamicRelations
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return cards;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCourtMinorCards(referenceData) {
|
function buildCourtMinorCards(referenceData) {
|
||||||
const tarotDb = getTarotDbConfig(referenceData);
|
return tarotDatabaseAssembly.buildCourtMinorCards(referenceData);
|
||||||
const cards = [];
|
|
||||||
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
|
||||||
const signById = Object.fromEntries(signs.map((sign) => [sign.id, sign]));
|
|
||||||
|
|
||||||
const decanById = new Map();
|
|
||||||
const decansBySign = referenceData?.decansBySign || {};
|
|
||||||
Object.entries(decansBySign).forEach(([signId, decans]) => {
|
|
||||||
const sign = signById[signId];
|
|
||||||
if (!sign || !Array.isArray(decans)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
decans.forEach((decan) => {
|
|
||||||
if (!decan?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const meta = buildDecanMetadata(decan, sign);
|
|
||||||
if (meta) {
|
|
||||||
decanById.set(decan.id, meta);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tarotDb.suits.forEach((suit) => {
|
|
||||||
const suitKey = suit.toLowerCase();
|
|
||||||
const suitInfo = tarotDb.suitInfo[suitKey];
|
|
||||||
if (!suitInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tarotDb.courtRanks.forEach((rank) => {
|
|
||||||
const rankKey = rank.toLowerCase();
|
|
||||||
const courtInfo = tarotDb.courtInfo[rankKey];
|
|
||||||
if (!courtInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const cardName = `${rank} of ${suit}`;
|
|
||||||
const windowDecanIds = tarotDb.courtDecanWindows[cardName] || [];
|
|
||||||
const windowDecans = windowDecanIds
|
|
||||||
.map((decanId) => decanById.get(decanId) || null)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const dynamicRelations = [];
|
|
||||||
const monthKeys = new Set();
|
|
||||||
|
|
||||||
windowDecans.forEach((meta) => {
|
|
||||||
dynamicRelations.push(
|
|
||||||
createRelation(
|
|
||||||
"decan",
|
|
||||||
`${meta.signId}-${meta.index}-${rankKey}-${suitKey}`,
|
|
||||||
`Decan ${meta.index}: ${meta.signSymbol} ${meta.signName} (${meta.startDegree}°–${meta.endDegree}°)${meta.dateRange ? ` · ${meta.dateRange.label}` : ""}`.trim(),
|
|
||||||
{
|
|
||||||
signId: meta.signId,
|
|
||||||
signName: meta.signName,
|
|
||||||
signSymbol: meta.signSymbol,
|
|
||||||
index: meta.index,
|
|
||||||
startDegree: meta.startDegree,
|
|
||||||
endDegree: meta.endDegree,
|
|
||||||
dateStart: meta.dateRange?.startToken || null,
|
|
||||||
dateEnd: meta.dateRange?.endToken || null,
|
|
||||||
dateRange: meta.dateRange?.label || null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const dateRange = meta.dateRange;
|
|
||||||
if (dateRange?.start && dateRange?.end) {
|
|
||||||
const monthNumbers = listMonthNumbersBetween(dateRange.start, dateRange.end);
|
|
||||||
monthNumbers.forEach((monthNo) => {
|
|
||||||
const monthId = MONTH_ID_BY_NUMBER[monthNo];
|
|
||||||
const monthName = MONTH_NAME_BY_NUMBER[monthNo] || `Month ${monthNo}`;
|
|
||||||
const monthKey = `${monthId}:${meta.signId}:${meta.index}`;
|
|
||||||
if (!monthId || monthKeys.has(monthKey)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
monthKeys.add(monthKey);
|
|
||||||
|
|
||||||
dynamicRelations.push(
|
|
||||||
createRelation(
|
|
||||||
"calendarMonth",
|
|
||||||
`${monthId}-${meta.signId}-${meta.index}-${rankKey}-${suitKey}`,
|
|
||||||
`Calendar month: ${monthName} (${meta.signName} decan ${meta.index})`,
|
|
||||||
{
|
|
||||||
monthId,
|
|
||||||
name: monthName,
|
|
||||||
monthOrder: monthNo,
|
|
||||||
signId: meta.signId,
|
|
||||||
signName: meta.signName,
|
|
||||||
decanIndex: meta.index,
|
|
||||||
dateRange: meta.dateRange?.label || null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (windowDecans.length) {
|
|
||||||
const firstRange = windowDecans[0].dateRange;
|
|
||||||
const lastRange = windowDecans[windowDecans.length - 1].dateRange;
|
|
||||||
const windowLabel = firstRange && lastRange
|
|
||||||
? `${formatMonthDayLabel(firstRange.start)}–${formatMonthDayLabel(lastRange.end)}`
|
|
||||||
: "--";
|
|
||||||
|
|
||||||
dynamicRelations.unshift(
|
|
||||||
createRelation(
|
|
||||||
"courtDateWindow",
|
|
||||||
`${rankKey}-${suitKey}`,
|
|
||||||
`Court date window: ${windowLabel}`,
|
|
||||||
{
|
|
||||||
dateStart: firstRange?.startToken || null,
|
|
||||||
dateEnd: lastRange?.endToken || null,
|
|
||||||
dateRange: windowLabel,
|
|
||||||
decanIds: windowDecanIds
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cards.push({
|
|
||||||
arcana: "Minor",
|
|
||||||
name: cardName,
|
|
||||||
number: null,
|
|
||||||
suit,
|
|
||||||
rank,
|
|
||||||
summary: `${rank} as ${courtInfo.role} within ${suitInfo.domain}.`,
|
|
||||||
meanings: {
|
|
||||||
upright: `${courtInfo.upright} In ${suit}, this guides ${suitInfo.domain}.`,
|
|
||||||
reversed: `${courtInfo.reversed} In ${suit}, this complicates ${suitInfo.domain}.`
|
|
||||||
},
|
|
||||||
keywords: [...courtInfo.keywords, ...suitInfo.keywords],
|
|
||||||
relations: [
|
|
||||||
createRelation("element", suitInfo.element, `Element: ${suitInfo.element}`, {
|
|
||||||
name: suitInfo.element
|
|
||||||
}),
|
|
||||||
createRelation(
|
|
||||||
"elementalFace",
|
|
||||||
`${rankKey}-${suitKey}`,
|
|
||||||
`${courtInfo.elementalFace} ${suitInfo.element}`,
|
|
||||||
{
|
|
||||||
rank,
|
|
||||||
suit,
|
|
||||||
elementalFace: courtInfo.elementalFace,
|
|
||||||
element: suitInfo.element
|
|
||||||
}
|
|
||||||
),
|
|
||||||
createRelation("courtRole", rankKey, `Court role: ${courtInfo.role}`, {
|
|
||||||
rank,
|
|
||||||
role: courtInfo.role
|
|
||||||
}),
|
|
||||||
...dynamicRelations
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return cards;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTarotDatabase(referenceData, magickDataset = null) {
|
function buildTarotDatabase(referenceData, magickDataset = null) {
|
||||||
const cards = [
|
return tarotDatabaseAssembly.buildTarotDatabase(referenceData, magickDataset);
|
||||||
...buildMajorCards(referenceData, magickDataset),
|
|
||||||
...buildNumberMinorCards(referenceData),
|
|
||||||
...buildCourtMinorCards(referenceData)
|
|
||||||
];
|
|
||||||
|
|
||||||
return applyMeaningText(cards, referenceData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.TarotCardDatabase = {
|
window.TarotCardDatabase = {
|
||||||
|
|||||||
173
app/ui-alphabet-kabbalah.js
Normal file
173
app/ui-alphabet-kabbalah.js
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
/* ui-alphabet-kabbalah.js — Shared Kabbalah and cube helpers for the alphabet section */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function normalizeId(value) {
|
||||||
|
return String(value || "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLetterId(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z]/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function titleCase(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.split(/[\s_-]+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSoulId(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z]/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFourWorldLayersFromDataset(magickDataset) {
|
||||||
|
const worlds = magickDataset?.grouped?.kabbalah?.fourWorlds;
|
||||||
|
const souls = magickDataset?.grouped?.kabbalah?.souls;
|
||||||
|
const paths = Array.isArray(magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]?.paths)
|
||||||
|
? magickDataset.grouped.kabbalah["kabbalah-tree"].paths
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!worlds || typeof worlds !== "object") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const soulAliases = {
|
||||||
|
chiah: "chaya",
|
||||||
|
chaya: "chaya",
|
||||||
|
neshamah: "neshama",
|
||||||
|
neshama: "neshama",
|
||||||
|
ruach: "ruach",
|
||||||
|
nephesh: "nephesh"
|
||||||
|
};
|
||||||
|
|
||||||
|
const pathByLetterId = new Map();
|
||||||
|
paths.forEach((path) => {
|
||||||
|
const letterId = normalizeLetterId(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char);
|
||||||
|
const pathNo = Number(path?.pathNumber);
|
||||||
|
if (!letterId || !Number.isFinite(pathNo) || pathByLetterId.has(letterId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pathByLetterId.set(letterId, pathNo);
|
||||||
|
});
|
||||||
|
|
||||||
|
const worldOrder = ["atzilut", "briah", "yetzirah", "assiah"];
|
||||||
|
|
||||||
|
return worldOrder
|
||||||
|
.map((worldId) => {
|
||||||
|
const world = worlds?.[worldId];
|
||||||
|
if (!world || typeof world !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tetragrammaton = world?.tetragrammaton && typeof world.tetragrammaton === "object"
|
||||||
|
? world.tetragrammaton
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const letterId = normalizeLetterId(tetragrammaton?.hebrewLetterId);
|
||||||
|
const rawSoulId = normalizeSoulId(world?.soulId);
|
||||||
|
const soulId = soulAliases[rawSoulId] || rawSoulId;
|
||||||
|
const soul = souls?.[soulId] && typeof souls[soulId] === "object"
|
||||||
|
? souls[soulId]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const slot = tetragrammaton?.isFinal
|
||||||
|
? `${String(tetragrammaton?.slot || "Heh")} (final)`
|
||||||
|
: String(tetragrammaton?.slot || "");
|
||||||
|
|
||||||
|
return {
|
||||||
|
slot,
|
||||||
|
letterChar: String(tetragrammaton?.letterChar || ""),
|
||||||
|
hebrewLetterId: letterId,
|
||||||
|
world: String(world?.name?.roman || titleCase(worldId)),
|
||||||
|
worldLayer: String(world?.worldLayer?.en || world?.desc?.en || ""),
|
||||||
|
worldDescription: String(world?.worldDescription?.en || ""),
|
||||||
|
soulLayer: String(soul?.name?.roman || titleCase(rawSoulId || soulId)),
|
||||||
|
soulTitle: String(soul?.title?.en || titleCase(soul?.name?.en || "")),
|
||||||
|
soulDescription: String(soul?.desc?.en || ""),
|
||||||
|
pathNumber: pathByLetterId.get(letterId) || null
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyCubeRefs() {
|
||||||
|
return {
|
||||||
|
hebrewPlacementById: new Map(),
|
||||||
|
signPlacementById: new Map(),
|
||||||
|
planetPlacementById: new Map(),
|
||||||
|
pathPlacementByNo: new Map()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCubePlacementForHebrewLetter(cubeRefs, hebrewLetterId, pathNo = null) {
|
||||||
|
const normalizedLetterId = normalizeId(hebrewLetterId);
|
||||||
|
if (normalizedLetterId && cubeRefs?.hebrewPlacementById?.has(normalizedLetterId)) {
|
||||||
|
return cubeRefs.hebrewPlacementById.get(normalizedLetterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericPath = Number(pathNo);
|
||||||
|
if (Number.isFinite(numericPath) && cubeRefs?.pathPlacementByNo?.has(numericPath)) {
|
||||||
|
return cubeRefs.pathPlacementByNo.get(numericPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCubePlacementForPlanet(cubeRefs, planetId) {
|
||||||
|
const normalizedPlanetId = normalizeId(planetId);
|
||||||
|
return normalizedPlanetId ? cubeRefs?.planetPlacementById?.get(normalizedPlanetId) || null : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCubePlacementForSign(cubeRefs, signId) {
|
||||||
|
const normalizedSignId = normalizeId(signId);
|
||||||
|
return normalizedSignId ? cubeRefs?.signPlacementById?.get(normalizedSignId) || null : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cubePlacementLabel(placement) {
|
||||||
|
const wallName = placement?.wallName || "Wall";
|
||||||
|
const edgeName = placement?.edgeName || "Direction";
|
||||||
|
return `Cube: ${wallName} Wall - ${edgeName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCubePlacementButton(placement, navBtn, fallbackDetail = null) {
|
||||||
|
if (!placement || typeof navBtn !== "function") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const detail = {
|
||||||
|
"wall-id": placement.wallId,
|
||||||
|
"edge-id": placement.edgeId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fallbackDetail && typeof fallbackDetail === "object") {
|
||||||
|
Object.entries(fallbackDetail).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null && value !== "") {
|
||||||
|
detail[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return navBtn(cubePlacementLabel(placement), "nav:cube", detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.AlphabetKabbalahUi = {
|
||||||
|
buildCubePlacementButton,
|
||||||
|
buildFourWorldLayersFromDataset,
|
||||||
|
createEmptyCubeRefs,
|
||||||
|
cubePlacementLabel,
|
||||||
|
getCubePlacementForHebrewLetter,
|
||||||
|
getCubePlacementForPlanet,
|
||||||
|
getCubePlacementForSign,
|
||||||
|
normalizeId,
|
||||||
|
normalizeLetterId,
|
||||||
|
titleCase
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -3,6 +3,23 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const alphabetGematriaUi = window.AlphabetGematriaUi || {};
|
const alphabetGematriaUi = window.AlphabetGematriaUi || {};
|
||||||
|
const alphabetKabbalahUi = window.AlphabetKabbalahUi || {};
|
||||||
|
const alphabetReferenceBuilders = window.AlphabetReferenceBuilders || {};
|
||||||
|
const alphabetDetailUi = window.AlphabetDetailUi || {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof alphabetKabbalahUi.buildCubePlacementButton !== "function"
|
||||||
|
|| typeof alphabetKabbalahUi.buildFourWorldLayersFromDataset !== "function"
|
||||||
|
|| typeof alphabetKabbalahUi.createEmptyCubeRefs !== "function"
|
||||||
|
|| typeof alphabetKabbalahUi.getCubePlacementForHebrewLetter !== "function"
|
||||||
|
|| typeof alphabetKabbalahUi.getCubePlacementForPlanet !== "function"
|
||||||
|
|| typeof alphabetKabbalahUi.getCubePlacementForSign !== "function"
|
||||||
|
|| typeof alphabetKabbalahUi.normalizeId !== "function"
|
||||||
|
|| typeof alphabetKabbalahUi.normalizeLetterId !== "function"
|
||||||
|
|| typeof alphabetKabbalahUi.titleCase !== "function"
|
||||||
|
) {
|
||||||
|
throw new Error("AlphabetKabbalahUi module must load before ui-alphabet.js");
|
||||||
|
}
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@@ -15,7 +32,6 @@
|
|||||||
},
|
},
|
||||||
fourWorldLayers: [],
|
fourWorldLayers: [],
|
||||||
monthRefsByHebrewId: new Map(),
|
monthRefsByHebrewId: new Map(),
|
||||||
const alphabetReferenceBuilders = window.AlphabetReferenceBuilders || {};
|
|
||||||
cubeRefs: {
|
cubeRefs: {
|
||||||
hebrewPlacementById: new Map(),
|
hebrewPlacementById: new Map(),
|
||||||
signPlacementById: new Map(),
|
signPlacementById: new Map(),
|
||||||
@@ -24,7 +40,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const alphabetDetailUi = window.AlphabetDetailUi || {};
|
|
||||||
// ── Arabic display name table ─────────────────────────────────────────
|
// ── Arabic display name table ─────────────────────────────────────────
|
||||||
const ARABIC_DISPLAY_NAMES = {
|
const ARABIC_DISPLAY_NAMES = {
|
||||||
alif: "Alif", ba: "Ba", jeem: "Jeem", dal: "Dal", ha: "H\u0101",
|
alif: "Alif", ba: "Ba", jeem: "Jeem", dal: "Dal", ha: "H\u0101",
|
||||||
@@ -437,84 +452,19 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
function normalizeId(value) {
|
function normalizeId(value) {
|
||||||
return String(value || "").trim().toLowerCase();
|
return alphabetKabbalahUi.normalizeId(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSoulId(value) {
|
function normalizeLetterId(value) {
|
||||||
return String(value || "")
|
return alphabetKabbalahUi.normalizeLetterId(value);
|
||||||
.trim()
|
}
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z]/g, "");
|
function titleCase(value) {
|
||||||
|
return alphabetKabbalahUi.titleCase(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFourWorldLayersFromDataset(magickDataset) {
|
function buildFourWorldLayersFromDataset(magickDataset) {
|
||||||
const worlds = magickDataset?.grouped?.kabbalah?.fourWorlds;
|
return alphabetKabbalahUi.buildFourWorldLayersFromDataset(magickDataset);
|
||||||
const souls = magickDataset?.grouped?.kabbalah?.souls;
|
|
||||||
const paths = Array.isArray(magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]?.paths)
|
|
||||||
? magickDataset.grouped.kabbalah["kabbalah-tree"].paths
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!worlds || typeof worlds !== "object") {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const soulAliases = {
|
|
||||||
chiah: "chaya",
|
|
||||||
chaya: "chaya",
|
|
||||||
neshamah: "neshama",
|
|
||||||
neshama: "neshama",
|
|
||||||
ruach: "ruach",
|
|
||||||
nephesh: "nephesh"
|
|
||||||
};
|
|
||||||
|
|
||||||
const pathByLetterId = new Map();
|
|
||||||
paths.forEach((path) => {
|
|
||||||
const letterId = normalizeLetterId(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char);
|
|
||||||
const pathNo = Number(path?.pathNumber);
|
|
||||||
if (!letterId || !Number.isFinite(pathNo) || pathByLetterId.has(letterId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pathByLetterId.set(letterId, pathNo);
|
|
||||||
});
|
|
||||||
|
|
||||||
const worldOrder = ["atzilut", "briah", "yetzirah", "assiah"];
|
|
||||||
|
|
||||||
return worldOrder
|
|
||||||
.map((worldId) => {
|
|
||||||
const world = worlds?.[worldId];
|
|
||||||
if (!world || typeof world !== "object") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tetragrammaton = world?.tetragrammaton && typeof world.tetragrammaton === "object"
|
|
||||||
? world.tetragrammaton
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const letterId = normalizeLetterId(tetragrammaton?.hebrewLetterId);
|
|
||||||
const rawSoulId = normalizeSoulId(world?.soulId);
|
|
||||||
const soulId = soulAliases[rawSoulId] || rawSoulId;
|
|
||||||
const soul = souls?.[soulId] && typeof souls[soulId] === "object"
|
|
||||||
? souls[soulId]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const slot = tetragrammaton?.isFinal
|
|
||||||
? `${String(tetragrammaton?.slot || "Heh")} (final)`
|
|
||||||
: String(tetragrammaton?.slot || "");
|
|
||||||
|
|
||||||
return {
|
|
||||||
slot,
|
|
||||||
letterChar: String(tetragrammaton?.letterChar || ""),
|
|
||||||
hebrewLetterId: letterId,
|
|
||||||
world: String(world?.name?.roman || titleCase(worldId)),
|
|
||||||
worldLayer: String(world?.worldLayer?.en || world?.desc?.en || ""),
|
|
||||||
worldDescription: String(world?.worldDescription?.en || ""),
|
|
||||||
soulLayer: String(soul?.name?.roman || titleCase(rawSoulId || soulId)),
|
|
||||||
soulTitle: String(soul?.title?.en || titleCase(soul?.name?.en || "")),
|
|
||||||
soulDescription: String(soul?.desc?.en || ""),
|
|
||||||
pathNumber: pathByLetterId.get(letterId) || null
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMonthReferencesByHebrew(referenceData, alphabets) {
|
function buildMonthReferencesByHebrew(referenceData, alphabets) {
|
||||||
@@ -526,12 +476,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createEmptyCubeRefs() {
|
function createEmptyCubeRefs() {
|
||||||
return {
|
return alphabetKabbalahUi.createEmptyCubeRefs();
|
||||||
hebrewPlacementById: new Map(),
|
|
||||||
signPlacementById: new Map(),
|
|
||||||
planetPlacementById: new Map(),
|
|
||||||
pathPlacementByNo: new Map()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCubeReferences(magickDataset) {
|
function buildCubeReferences(magickDataset) {
|
||||||
@@ -543,54 +488,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCubePlacementForHebrewLetter(hebrewLetterId, pathNo = null) {
|
function getCubePlacementForHebrewLetter(hebrewLetterId, pathNo = null) {
|
||||||
const normalizedLetterId = normalizeId(hebrewLetterId);
|
return alphabetKabbalahUi.getCubePlacementForHebrewLetter(state.cubeRefs, hebrewLetterId, pathNo);
|
||||||
if (normalizedLetterId && state.cubeRefs.hebrewPlacementById.has(normalizedLetterId)) {
|
|
||||||
return state.cubeRefs.hebrewPlacementById.get(normalizedLetterId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const numericPath = Number(pathNo);
|
|
||||||
if (Number.isFinite(numericPath) && state.cubeRefs.pathPlacementByNo.has(numericPath)) {
|
|
||||||
return state.cubeRefs.pathPlacementByNo.get(numericPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCubePlacementForPlanet(planetId) {
|
function getCubePlacementForPlanet(planetId) {
|
||||||
const normalizedPlanetId = normalizeId(planetId);
|
return alphabetKabbalahUi.getCubePlacementForPlanet(state.cubeRefs, planetId);
|
||||||
return normalizedPlanetId ? state.cubeRefs.planetPlacementById.get(normalizedPlanetId) || null : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCubePlacementForSign(signId) {
|
function getCubePlacementForSign(signId) {
|
||||||
const normalizedSignId = normalizeId(signId);
|
return alphabetKabbalahUi.getCubePlacementForSign(state.cubeRefs, signId);
|
||||||
return normalizedSignId ? state.cubeRefs.signPlacementById.get(normalizedSignId) || null : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cubePlacementLabel(placement) {
|
|
||||||
const wallName = placement?.wallName || "Wall";
|
|
||||||
const edgeName = placement?.edgeName || "Direction";
|
|
||||||
return `Cube: ${wallName} Wall - ${edgeName}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cubePlacementBtn(placement, fallbackDetail = null) {
|
function cubePlacementBtn(placement, fallbackDetail = null) {
|
||||||
if (!placement) {
|
return alphabetKabbalahUi.buildCubePlacementButton(placement, navBtn, fallbackDetail);
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const detail = {
|
|
||||||
"wall-id": placement.wallId,
|
|
||||||
"edge-id": placement.edgeId
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fallbackDetail && typeof fallbackDetail === "object") {
|
|
||||||
Object.entries(fallbackDetail).forEach(([key, value]) => {
|
|
||||||
if (value !== undefined && value !== null && value !== "") {
|
|
||||||
detail[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return navBtn(cubePlacementLabel(placement), "nav:cube", detail);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ""; }
|
function cap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ""; }
|
||||||
|
|||||||
329
app/ui-calendar-data.js
Normal file
329
app/ui-calendar-data.js
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function buildDecanWindow(context, sign, decanIndex) {
|
||||||
|
const { buildSignDateBounds, addDays, formatDateLabel } = context;
|
||||||
|
const bounds = buildSignDateBounds(sign);
|
||||||
|
const index = Number(decanIndex);
|
||||||
|
if (!bounds || !Number.isFinite(index)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = addDays(bounds.start, (index - 1) * 10);
|
||||||
|
const nominalEnd = addDays(start, 9);
|
||||||
|
const end = nominalEnd.getTime() > bounds.end.getTime() ? bounds.end : nominalEnd;
|
||||||
|
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
label: `${formatDateLabel(start)}–${formatDateLabel(end)}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function listMonthNumbersBetween(start, end) {
|
||||||
|
const result = [];
|
||||||
|
const seen = new Set();
|
||||||
|
const cursor = new Date(start.getFullYear(), start.getMonth(), 1);
|
||||||
|
const limit = new Date(end.getFullYear(), end.getMonth(), 1);
|
||||||
|
|
||||||
|
while (cursor.getTime() <= limit.getTime()) {
|
||||||
|
const monthNo = cursor.getMonth() + 1;
|
||||||
|
if (!seen.has(monthNo)) {
|
||||||
|
seen.add(monthNo);
|
||||||
|
result.push(monthNo);
|
||||||
|
}
|
||||||
|
cursor.setMonth(cursor.getMonth() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDecanTarotRowsForMonth(context, month) {
|
||||||
|
const { state, normalizeMinorTarotCardName } = context;
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
if (!Number.isFinite(monthOrder)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
const seen = new Set();
|
||||||
|
const decansBySign = state.referenceData?.decansBySign || {};
|
||||||
|
|
||||||
|
Object.entries(decansBySign).forEach(([signId, decans]) => {
|
||||||
|
const sign = state.signsById.get(signId);
|
||||||
|
if (!sign || !Array.isArray(decans)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
decans.forEach((decan) => {
|
||||||
|
const window = buildDecanWindow(context, sign, decan?.index);
|
||||||
|
if (!window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthsTouched = listMonthNumbersBetween(window.start, window.end);
|
||||||
|
if (!monthsTouched.includes(monthOrder)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardName = normalizeMinorTarotCardName(decan?.tarotMinorArcana);
|
||||||
|
if (!cardName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${cardName}|${signId}|${decan.index}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
|
||||||
|
const startDegree = (Number(decan.index) - 1) * 10;
|
||||||
|
const endDegree = startDegree + 10;
|
||||||
|
const signName = sign?.name?.en || sign?.name || signId;
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
cardName,
|
||||||
|
signId,
|
||||||
|
signName,
|
||||||
|
signSymbol: sign?.symbol || "",
|
||||||
|
decanIndex: Number(decan.index),
|
||||||
|
startDegree,
|
||||||
|
endDegree,
|
||||||
|
startTime: window.start.getTime(),
|
||||||
|
endTime: window.end.getTime(),
|
||||||
|
startMonth: window.start.getMonth() + 1,
|
||||||
|
startDay: window.start.getDate(),
|
||||||
|
endMonth: window.end.getMonth() + 1,
|
||||||
|
endDay: window.end.getDate(),
|
||||||
|
dateRange: window.label
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
rows.sort((left, right) => {
|
||||||
|
if (left.startTime !== right.startTime) {
|
||||||
|
return left.startTime - right.startTime;
|
||||||
|
}
|
||||||
|
if (left.decanIndex !== right.decanIndex) {
|
||||||
|
return left.decanIndex - right.decanIndex;
|
||||||
|
}
|
||||||
|
return left.cardName.localeCompare(right.cardName);
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonthDayLinkRows(context, month) {
|
||||||
|
const { state, getDaysInMonth, resolveCalendarDayToGregorian, formatIsoDate } = context;
|
||||||
|
const cacheKey = `${state.selectedCalendar}|${state.selectedYear}|${month?.id || ""}`;
|
||||||
|
if (state.dayLinksCache.has(cacheKey)) {
|
||||||
|
return state.dayLinksCache.get(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dayCount = null;
|
||||||
|
if (state.selectedCalendar === "gregorian") {
|
||||||
|
dayCount = getDaysInMonth(state.selectedYear, Number(month?.order));
|
||||||
|
} else if (state.selectedCalendar === "hebrew" || state.selectedCalendar === "islamic") {
|
||||||
|
const baseDays = Number(month?.days);
|
||||||
|
const variantDays = Number(month?.daysVariant);
|
||||||
|
if (Number.isFinite(baseDays) && Number.isFinite(variantDays)) {
|
||||||
|
dayCount = Math.max(Math.trunc(baseDays), Math.trunc(variantDays));
|
||||||
|
} else if (Number.isFinite(baseDays)) {
|
||||||
|
dayCount = Math.trunc(baseDays);
|
||||||
|
} else if (Number.isFinite(variantDays)) {
|
||||||
|
dayCount = Math.trunc(variantDays);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(dayCount) || dayCount <= 0) {
|
||||||
|
state.dayLinksCache.set(cacheKey, []);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (let day = 1; day <= dayCount; day += 1) {
|
||||||
|
const gregorianDate = resolveCalendarDayToGregorian(month, day);
|
||||||
|
rows.push({
|
||||||
|
day,
|
||||||
|
gregorianDate: formatIsoDate(gregorianDate),
|
||||||
|
isResolved: Boolean(gregorianDate && !Number.isNaN(gregorianDate.getTime()))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
state.dayLinksCache.set(cacheKey, rows);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function associationSearchText(context, associations) {
|
||||||
|
const { getTarotCardSearchAliases } = context;
|
||||||
|
if (!associations || typeof associations !== "object") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const tarotAliases = associations.tarotCard && typeof getTarotCardSearchAliases === "function"
|
||||||
|
? getTarotCardSearchAliases(associations.tarotCard, { trumpNumber: associations.tarotTrumpNumber })
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
associations.planetId,
|
||||||
|
associations.zodiacSignId,
|
||||||
|
associations.numberValue,
|
||||||
|
associations.tarotCard,
|
||||||
|
associations.tarotTrumpNumber,
|
||||||
|
...tarotAliases,
|
||||||
|
associations.godId,
|
||||||
|
associations.godName,
|
||||||
|
associations.hebrewLetterId,
|
||||||
|
associations.kabbalahPathNumber,
|
||||||
|
associations.iChingPlanetaryInfluence
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventSearchText(context, event) {
|
||||||
|
const { normalizeSearchValue } = context;
|
||||||
|
return normalizeSearchValue([
|
||||||
|
event?.name,
|
||||||
|
event?.date,
|
||||||
|
event?.dateRange,
|
||||||
|
event?.description,
|
||||||
|
associationSearchText(context, event?.associations)
|
||||||
|
].filter(Boolean).join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
function holidaySearchText(context, holiday) {
|
||||||
|
const { normalizeSearchValue } = context;
|
||||||
|
return normalizeSearchValue([
|
||||||
|
holiday?.name,
|
||||||
|
holiday?.kind,
|
||||||
|
holiday?.date,
|
||||||
|
holiday?.dateRange,
|
||||||
|
holiday?.dateText,
|
||||||
|
holiday?.monthDayStart,
|
||||||
|
holiday?.calendarId,
|
||||||
|
holiday?.description,
|
||||||
|
associationSearchText(context, holiday?.associations)
|
||||||
|
].filter(Boolean).join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHolidayList(context, month) {
|
||||||
|
const { state, normalizeText, resolveHolidayGregorianDate } = context;
|
||||||
|
const calendarId = state.selectedCalendar;
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
|
||||||
|
const fromRepo = state.calendarHolidays.filter((holiday) => {
|
||||||
|
const holidayCalendarId = normalizeText(holiday?.calendarId).toLowerCase();
|
||||||
|
if (holidayCalendarId !== calendarId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDirectMonthMatch = normalizeText(holiday?.monthId).toLowerCase() === normalizeText(month?.id).toLowerCase();
|
||||||
|
if (isDirectMonthMatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calendarId === "gregorian" && holiday?.dateRule && Number.isFinite(monthOrder)) {
|
||||||
|
const computedDate = resolveHolidayGregorianDate(holiday);
|
||||||
|
return computedDate instanceof Date
|
||||||
|
&& !Number.isNaN(computedDate.getTime())
|
||||||
|
&& (computedDate.getMonth() + 1) === Math.trunc(monthOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fromRepo.length) {
|
||||||
|
return [...fromRepo].sort((left, right) => {
|
||||||
|
const leftDate = resolveHolidayGregorianDate(left);
|
||||||
|
const rightDate = resolveHolidayGregorianDate(right);
|
||||||
|
const leftDay = Number.isFinite(Number(left?.day))
|
||||||
|
? Number(left.day)
|
||||||
|
: ((leftDate instanceof Date && !Number.isNaN(leftDate.getTime())) ? leftDate.getDate() : NaN);
|
||||||
|
const rightDay = Number.isFinite(Number(right?.day))
|
||||||
|
? Number(right.day)
|
||||||
|
: ((rightDate instanceof Date && !Number.isNaN(rightDate.getTime())) ? rightDate.getDate() : NaN);
|
||||||
|
if (Number.isFinite(leftDay) && Number.isFinite(rightDay) && leftDay !== rightDay) {
|
||||||
|
return leftDay - rightDay;
|
||||||
|
}
|
||||||
|
return normalizeText(left?.name).localeCompare(normalizeText(right?.name));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
const ordered = [];
|
||||||
|
|
||||||
|
(month?.holidayIds || []).forEach((holidayId) => {
|
||||||
|
const holiday = state.holidays.find((item) => item.id === holidayId);
|
||||||
|
if (holiday && !seen.has(holiday.id)) {
|
||||||
|
seen.add(holiday.id);
|
||||||
|
ordered.push(holiday);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
state.holidays.forEach((holiday) => {
|
||||||
|
if (holiday?.monthId === month.id && !seen.has(holiday.id)) {
|
||||||
|
seen.add(holiday.id);
|
||||||
|
ordered.push(holiday);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMonthSearchText(context, month) {
|
||||||
|
const { state, normalizeSearchValue } = context;
|
||||||
|
const monthHolidays = buildHolidayList(context, month);
|
||||||
|
const holidayText = monthHolidays.map((holiday) => holidaySearchText(context, holiday)).join(" ");
|
||||||
|
|
||||||
|
if (state.selectedCalendar === "gregorian") {
|
||||||
|
const events = Array.isArray(month?.events) ? month.events : [];
|
||||||
|
return normalizeSearchValue([
|
||||||
|
month?.name,
|
||||||
|
month?.id,
|
||||||
|
month?.start,
|
||||||
|
month?.end,
|
||||||
|
month?.coreTheme,
|
||||||
|
month?.seasonNorth,
|
||||||
|
month?.seasonSouth,
|
||||||
|
associationSearchText(context, month?.associations),
|
||||||
|
events.map((event) => eventSearchText(context, event)).join(" "),
|
||||||
|
holidayText
|
||||||
|
].filter(Boolean).join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
const wheelAssocText = month?.associations
|
||||||
|
? [
|
||||||
|
Array.isArray(month.associations.themes) ? month.associations.themes.join(" ") : "",
|
||||||
|
Array.isArray(month.associations.deities) ? month.associations.deities.join(" ") : "",
|
||||||
|
month.associations.element,
|
||||||
|
month.associations.direction
|
||||||
|
].filter(Boolean).join(" ")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return normalizeSearchValue([
|
||||||
|
month?.name,
|
||||||
|
month?.id,
|
||||||
|
month?.nativeName,
|
||||||
|
month?.meaning,
|
||||||
|
month?.season,
|
||||||
|
month?.description,
|
||||||
|
month?.zodiacSign,
|
||||||
|
month?.tribe,
|
||||||
|
month?.element,
|
||||||
|
month?.type,
|
||||||
|
month?.date,
|
||||||
|
month?.hebrewLetter,
|
||||||
|
holidayText,
|
||||||
|
wheelAssocText
|
||||||
|
].filter(Boolean).join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.CalendarDataUi = {
|
||||||
|
getMonthDayLinkRows,
|
||||||
|
buildDecanTarotRowsForMonth,
|
||||||
|
associationSearchText,
|
||||||
|
eventSearchText,
|
||||||
|
holidaySearchText,
|
||||||
|
buildHolidayList,
|
||||||
|
buildMonthSearchText
|
||||||
|
};
|
||||||
|
})();
|
||||||
457
app/ui-calendar-detail-panels.js
Normal file
457
app/ui-calendar-detail-panels.js
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function findSignIdByAstrologyName(name, context) {
|
||||||
|
const { api, getState } = context;
|
||||||
|
const token = api.normalizeCalendarText(name);
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [signId, sign] of getState().signsById || []) {
|
||||||
|
const idToken = api.normalizeCalendarText(signId);
|
||||||
|
const nameToken = api.normalizeCalendarText(sign?.name?.en || sign?.name || "");
|
||||||
|
if (token === idToken || token === nameToken) {
|
||||||
|
return signId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMajorArcanaRowsForMonth(context) {
|
||||||
|
const { month, api, getState } = context;
|
||||||
|
const currentState = getState();
|
||||||
|
if (currentState.selectedCalendar !== "gregorian") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
if (!Number.isFinite(monthOrder)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthStart = new Date(currentState.selectedYear, monthOrder - 1, 1, 12, 0, 0, 0);
|
||||||
|
const monthEnd = new Date(currentState.selectedYear, monthOrder, 0, 12, 0, 0, 0);
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
currentState.hebrewById?.forEach((letter) => {
|
||||||
|
const astrologyType = api.normalizeCalendarText(letter?.astrology?.type);
|
||||||
|
if (astrologyType !== "zodiac") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signId = findSignIdByAstrologyName(letter?.astrology?.name, context);
|
||||||
|
const sign = signId ? currentState.signsById?.get(signId) : null;
|
||||||
|
if (!sign) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startToken = api.parseMonthDayToken(sign?.start);
|
||||||
|
const endToken = api.parseMonthDayToken(sign?.end);
|
||||||
|
if (!startToken || !endToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spanStart = new Date(currentState.selectedYear, startToken.month - 1, startToken.day, 12, 0, 0, 0);
|
||||||
|
const spanEnd = new Date(currentState.selectedYear, endToken.month - 1, endToken.day, 12, 0, 0, 0);
|
||||||
|
const wraps = spanEnd.getTime() < spanStart.getTime();
|
||||||
|
|
||||||
|
const segments = wraps
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
start: spanStart,
|
||||||
|
end: new Date(currentState.selectedYear, 11, 31, 12, 0, 0, 0)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: new Date(currentState.selectedYear, 0, 1, 12, 0, 0, 0),
|
||||||
|
end: spanEnd
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [{ start: spanStart, end: spanEnd }];
|
||||||
|
|
||||||
|
segments.forEach((segment) => {
|
||||||
|
const overlap = api.intersectDateRanges(segment.start, segment.end, monthStart, monthEnd);
|
||||||
|
if (!overlap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeStartDay = overlap.start.getDate();
|
||||||
|
const rangeEndDay = overlap.end.getDate();
|
||||||
|
const cardName = String(letter?.tarot?.card || "").trim();
|
||||||
|
const trumpNumber = Number(letter?.tarot?.trumpNumber);
|
||||||
|
if (!cardName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
id: `${signId}-${rangeStartDay}-${rangeEndDay}`,
|
||||||
|
signId,
|
||||||
|
signName: sign?.name?.en || sign?.name || signId,
|
||||||
|
signSymbol: sign?.symbol || "",
|
||||||
|
cardName,
|
||||||
|
trumpNumber: Number.isFinite(trumpNumber) ? Math.trunc(trumpNumber) : null,
|
||||||
|
hebrewLetterId: String(letter?.hebrewLetterId || "").trim(),
|
||||||
|
hebrewLetterName: String(letter?.name || "").trim(),
|
||||||
|
hebrewLetterChar: String(letter?.char || "").trim(),
|
||||||
|
dayStart: rangeStartDay,
|
||||||
|
dayEnd: rangeEndDay,
|
||||||
|
rangeLabel: `${month?.name || "Month"} ${rangeStartDay}-${rangeEndDay}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
rows.sort((left, right) => {
|
||||||
|
if (left.dayStart !== right.dayStart) {
|
||||||
|
return left.dayStart - right.dayStart;
|
||||||
|
}
|
||||||
|
return left.cardName.localeCompare(right.cardName);
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMajorArcanaCard(context) {
|
||||||
|
const { month, api } = context;
|
||||||
|
const selectedDay = api.getSelectedDayFilterContext(month);
|
||||||
|
const allRows = buildMajorArcanaRowsForMonth(context);
|
||||||
|
|
||||||
|
const rows = selectedDay
|
||||||
|
? allRows.filter((row) => selectedDay.entries.some((entry) => entry.dayNumber >= row.dayStart && entry.dayNumber <= row.dayEnd))
|
||||||
|
: allRows;
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Major Arcana Windows</strong>
|
||||||
|
<div class="planet-text">No major arcana windows for this month.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = rows.map((row) => {
|
||||||
|
const label = row.hebrewLetterId
|
||||||
|
? `${row.hebrewLetterChar ? `${row.hebrewLetterChar} ` : ""}${row.hebrewLetterName || row.hebrewLetterId}`
|
||||||
|
: "--";
|
||||||
|
const displayCardName = api.getDisplayTarotName(row.cardName, row.trumpNumber);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="cal-item-row">
|
||||||
|
<div class="cal-item-head">
|
||||||
|
<span class="cal-item-name">${displayCardName}${row.trumpNumber != null ? ` · Trump ${row.trumpNumber}` : ""}</span>
|
||||||
|
<span class="planet-list-meta">${row.rangeLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div class="planet-list-meta">${row.signSymbol} ${row.signName} · Hebrew: ${label}</div>
|
||||||
|
<div class="alpha-nav-btns">
|
||||||
|
<button class="alpha-nav-btn" data-nav="calendar-day-range" data-range-start="${row.dayStart}" data-range-end="${row.dayEnd}">${row.rangeLabel} ↗</button>
|
||||||
|
<button class="alpha-nav-btn" data-nav="tarot-card" data-card-name="${row.cardName}" data-trump-number="${row.trumpNumber ?? ""}">${displayCardName} ↗</button>
|
||||||
|
${row.hebrewLetterId ? `<button class="alpha-nav-btn" data-nav="alphabet" data-alphabet="hebrew" data-hebrew-letter-id="${row.hebrewLetterId}">${label} ↗</button>` : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Major Arcana Windows</strong>
|
||||||
|
<div class="cal-item-stack">${list}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDecanTarotCard(context) {
|
||||||
|
const { month, api } = context;
|
||||||
|
const selectedDay = api.getSelectedDayFilterContext(month);
|
||||||
|
const allRows = api.buildDecanTarotRowsForMonth(month);
|
||||||
|
const rows = selectedDay
|
||||||
|
? allRows.filter((row) => selectedDay.entries.some((entry) => {
|
||||||
|
const targetDate = entry.gregorianDate;
|
||||||
|
if (!(targetDate instanceof Date) || Number.isNaN(targetDate.getTime())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMonth = targetDate.getMonth() + 1;
|
||||||
|
const targetDayNo = targetDate.getDate();
|
||||||
|
return api.isMonthDayInRange(
|
||||||
|
targetMonth,
|
||||||
|
targetDayNo,
|
||||||
|
row.startMonth,
|
||||||
|
row.startDay,
|
||||||
|
row.endMonth,
|
||||||
|
row.endDay
|
||||||
|
);
|
||||||
|
}))
|
||||||
|
: allRows;
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Decan Tarot Windows</strong>
|
||||||
|
<div class="planet-text">No decan tarot windows for this month.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = rows.map((row) => {
|
||||||
|
const displayCardName = api.getDisplayTarotName(row.cardName);
|
||||||
|
return `
|
||||||
|
<div class="cal-item-row">
|
||||||
|
<div class="cal-item-head">
|
||||||
|
<span class="cal-item-name">${row.signSymbol} ${row.signName} · Decan ${row.decanIndex}</span>
|
||||||
|
<span class="planet-list-meta">${row.startDegree}°–${row.endDegree}° · ${row.dateRange}</span>
|
||||||
|
</div>
|
||||||
|
<div class="alpha-nav-btns">
|
||||||
|
<button class="alpha-nav-btn" data-nav="tarot-card" data-card-name="${row.cardName}">${displayCardName} ↗</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Decan Tarot Windows</strong>
|
||||||
|
<div class="cal-item-stack">${list}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDayLinksCard(context) {
|
||||||
|
const { month, api } = context;
|
||||||
|
const rows = api.getMonthDayLinkRows(month);
|
||||||
|
if (!rows.length) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedContext = api.getSelectedDayFilterContext(month);
|
||||||
|
const selectedDaySet = selectedContext?.dayNumbers || new Set();
|
||||||
|
const selectedDays = selectedContext?.entries?.map((entry) => entry.dayNumber) || [];
|
||||||
|
const selectedSummary = selectedDays.length ? selectedDays.join(", ") : "";
|
||||||
|
|
||||||
|
const links = rows.map((row) => {
|
||||||
|
if (!row.isResolved) {
|
||||||
|
return `<span class="planet-list-meta">${row.day}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSelected = selectedDaySet.has(Number(row.day));
|
||||||
|
return `<button class="alpha-nav-btn${isSelected ? " is-selected" : ""}" data-nav="calendar-day" data-day-number="${row.day}" data-gregorian-date="${row.gregorianDate}" aria-pressed="${isSelected ? "true" : "false"}" title="Filter this month by day ${row.day}">${row.day}</button>`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
const clearButton = selectedContext
|
||||||
|
? '<button class="alpha-nav-btn" data-nav="calendar-day-clear" type="button">Show All Days</button>'
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const helperText = selectedContext
|
||||||
|
? `<div class="planet-list-meta">Filtered to days: ${selectedSummary}</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Day Links</strong>
|
||||||
|
<div class="planet-text">Filter this month to events, holidays, and data connected to a specific day.</div>
|
||||||
|
${helperText}
|
||||||
|
<div class="alpha-nav-btns">${links}</div>
|
||||||
|
${clearButton ? `<div class="alpha-nav-btns">${clearButton}</div>` : ""}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGregorianMonthDetail(context) {
|
||||||
|
const {
|
||||||
|
renderFactsCard,
|
||||||
|
renderAssociationsCard,
|
||||||
|
renderEventsCard,
|
||||||
|
renderHolidaysCard,
|
||||||
|
month
|
||||||
|
} = context;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-grid">
|
||||||
|
${renderFactsCard(month)}
|
||||||
|
${renderDayLinksCard(context)}
|
||||||
|
${renderAssociationsCard(month)}
|
||||||
|
${renderMajorArcanaCard(context)}
|
||||||
|
${renderDecanTarotCard(context)}
|
||||||
|
${renderEventsCard(month)}
|
||||||
|
${renderHolidaysCard(month, "Holiday Repository")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHebrewMonthDetail(context) {
|
||||||
|
const { month, api, getState, buildAssociationButtons, renderHolidaysCard } = context;
|
||||||
|
const currentState = getState();
|
||||||
|
const gregorianStartDate = api.getGregorianReferenceDateForCalendarMonth(month);
|
||||||
|
const factsRows = [
|
||||||
|
["Hebrew Name", month.nativeName || "--"],
|
||||||
|
["Month Order", month.leapYearOnly ? `${month.order} (leap year only)` : String(month.order)],
|
||||||
|
["Gregorian Reference Year", String(currentState.selectedYear)],
|
||||||
|
["Month Start (Gregorian)", api.formatGregorianReferenceDate(gregorianStartDate)],
|
||||||
|
["Days", month.daysVariant ? `${month.days}–${month.daysVariant} (varies)` : String(month.days || "--")],
|
||||||
|
["Season", month.season || "--"],
|
||||||
|
["Zodiac Sign", api.cap(month.zodiacSign) || "--"],
|
||||||
|
["Tribe of Israel", month.tribe || "--"],
|
||||||
|
["Sense", month.sense || "--"],
|
||||||
|
["Hebrew Letter", month.hebrewLetter || "--"]
|
||||||
|
].map(([dt, dd]) => `<dt>${dt}</dt><dd>${dd}</dd>`).join("");
|
||||||
|
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
const navButtons = buildAssociationButtons({
|
||||||
|
...(month?.associations || {}),
|
||||||
|
...(Number.isFinite(monthOrder) ? { numberValue: Math.trunc(monthOrder) } : {})
|
||||||
|
});
|
||||||
|
const connectionsCard = navButtons
|
||||||
|
? `<div class="planet-meta-card"><strong>Connections</strong>${navButtons}</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-grid">
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Month Facts</strong>
|
||||||
|
<div class="planet-text">
|
||||||
|
<dl class="alpha-dl">${factsRows}</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${connectionsCard}
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>About ${month.name}</strong>
|
||||||
|
<div class="planet-text">${month.description || "--"}</div>
|
||||||
|
</div>
|
||||||
|
${renderDayLinksCard(context)}
|
||||||
|
${renderHolidaysCard(month, "Holiday Repository")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIslamicMonthDetail(context) {
|
||||||
|
const { month, api, getState, buildAssociationButtons, renderHolidaysCard } = context;
|
||||||
|
const currentState = getState();
|
||||||
|
const gregorianStartDate = api.getGregorianReferenceDateForCalendarMonth(month);
|
||||||
|
const factsRows = [
|
||||||
|
["Arabic Name", month.nativeName || "--"],
|
||||||
|
["Month Order", String(month.order)],
|
||||||
|
["Gregorian Reference Year", String(currentState.selectedYear)],
|
||||||
|
["Month Start (Gregorian)", api.formatGregorianReferenceDate(gregorianStartDate)],
|
||||||
|
["Meaning", month.meaning || "--"],
|
||||||
|
["Days", month.daysVariant ? `${month.days}–${month.daysVariant} (varies)` : String(month.days || "--")],
|
||||||
|
["Sacred Month", month.sacred ? "Yes - warfare prohibited" : "No"]
|
||||||
|
].map(([dt, dd]) => `<dt>${dt}</dt><dd>${dd}</dd>`).join("");
|
||||||
|
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
const hasNumberLink = Number.isFinite(monthOrder) && monthOrder >= 0;
|
||||||
|
const navButtons = hasNumberLink
|
||||||
|
? buildAssociationButtons({ numberValue: Math.trunc(monthOrder) })
|
||||||
|
: "";
|
||||||
|
const connectionsCard = hasNumberLink
|
||||||
|
? `<div class="planet-meta-card"><strong>Connections</strong>${navButtons}</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-grid">
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Month Facts</strong>
|
||||||
|
<div class="planet-text">
|
||||||
|
<dl class="alpha-dl">${factsRows}</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${connectionsCard}
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>About ${month.name}</strong>
|
||||||
|
<div class="planet-text">${month.description || "--"}</div>
|
||||||
|
</div>
|
||||||
|
${renderDayLinksCard(context)}
|
||||||
|
${renderHolidaysCard(month, "Holiday Repository")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWheelDeityButtons(deities, context) {
|
||||||
|
const { api, getState } = context;
|
||||||
|
const buttons = [];
|
||||||
|
(Array.isArray(deities) ? deities : []).forEach((rawName) => {
|
||||||
|
const cleanName = String(rawName || "").replace(/\s*\/.*$/, "").replace(/\s*\(.*\)$/, "").trim();
|
||||||
|
const godId = api.findGodIdByName(cleanName) || api.findGodIdByName(rawName);
|
||||||
|
if (!godId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const god = getState().godsById?.get(godId);
|
||||||
|
const label = god?.name || cleanName;
|
||||||
|
buttons.push(`<button class="alpha-nav-btn" data-nav="god" data-god-id="${godId}" data-god-name="${label}">${label} ↗</button>`);
|
||||||
|
});
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWheelMonthDetail(context) {
|
||||||
|
const { month, api, getState, buildAssociationButtons, renderHolidaysCard } = context;
|
||||||
|
const currentState = getState();
|
||||||
|
const gregorianStartDate = api.getGregorianReferenceDateForCalendarMonth(month);
|
||||||
|
const assoc = month?.associations;
|
||||||
|
const themes = Array.isArray(assoc?.themes) ? assoc.themes.join(", ") : "--";
|
||||||
|
const deities = Array.isArray(assoc?.deities) ? assoc.deities.join(", ") : "--";
|
||||||
|
const colors = Array.isArray(assoc?.colors) ? assoc.colors.join(", ") : "--";
|
||||||
|
const herbs = Array.isArray(assoc?.herbs) ? assoc.herbs.join(", ") : "--";
|
||||||
|
|
||||||
|
const factsRows = [
|
||||||
|
["Date", month.date || "--"],
|
||||||
|
["Type", api.cap(month.type) || "--"],
|
||||||
|
["Gregorian Reference Year", String(currentState.selectedYear)],
|
||||||
|
["Start (Gregorian)", api.formatGregorianReferenceDate(gregorianStartDate)],
|
||||||
|
["Season", month.season || "--"],
|
||||||
|
["Element", api.cap(month.element) || "--"],
|
||||||
|
["Direction", assoc?.direction || "--"]
|
||||||
|
].map(([dt, dd]) => `<dt>${dt}</dt><dd>${dd}</dd>`).join("");
|
||||||
|
|
||||||
|
const assocRows = [
|
||||||
|
["Themes", themes],
|
||||||
|
["Deities", deities],
|
||||||
|
["Colors", colors],
|
||||||
|
["Herbs", herbs]
|
||||||
|
].map(([dt, dd]) => `<dt>${dt}</dt><dd class="planet-text">${dd}</dd>`).join("");
|
||||||
|
|
||||||
|
const deityButtons = buildWheelDeityButtons(assoc?.deities, context);
|
||||||
|
const deityLinksCard = deityButtons.length
|
||||||
|
? `<div class="planet-meta-card"><strong>Linked Deities</strong><div class="alpha-nav-btns">${deityButtons.join("")}</div></div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
const hasNumberLink = Number.isFinite(monthOrder) && monthOrder >= 0;
|
||||||
|
const numberButtons = hasNumberLink
|
||||||
|
? buildAssociationButtons({ numberValue: Math.trunc(monthOrder) })
|
||||||
|
: "";
|
||||||
|
const numberLinksCard = hasNumberLink
|
||||||
|
? `<div class="planet-meta-card"><strong>Connections</strong>${numberButtons}</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-grid">
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Sabbat Facts</strong>
|
||||||
|
<div class="planet-text">
|
||||||
|
<dl class="alpha-dl">${factsRows}</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>About ${month.name}</strong>
|
||||||
|
<div class="planet-text">${month.description || "--"}</div>
|
||||||
|
</div>
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Associations</strong>
|
||||||
|
<div class="planet-text">
|
||||||
|
<dl class="alpha-dl">${assocRows}</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${renderDayLinksCard(context)}
|
||||||
|
${numberLinksCard}
|
||||||
|
${deityLinksCard}
|
||||||
|
${renderHolidaysCard(month, "Holiday Repository")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.CalendarDetailPanelsUi = {
|
||||||
|
renderGregorianMonthDetail,
|
||||||
|
renderHebrewMonthDetail,
|
||||||
|
renderIslamicMonthDetail,
|
||||||
|
renderWheelMonthDetail
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -37,6 +37,17 @@
|
|||||||
findGodIdByName: () => null
|
findGodIdByName: () => null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const calendarDetailPanelsUi = window.CalendarDetailPanelsUi || {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof calendarDetailPanelsUi.renderGregorianMonthDetail !== "function"
|
||||||
|
|| typeof calendarDetailPanelsUi.renderHebrewMonthDetail !== "function"
|
||||||
|
|| typeof calendarDetailPanelsUi.renderIslamicMonthDetail !== "function"
|
||||||
|
|| typeof calendarDetailPanelsUi.renderWheelMonthDetail !== "function"
|
||||||
|
) {
|
||||||
|
throw new Error("CalendarDetailPanelsUi module must load before ui-calendar-detail.js");
|
||||||
|
}
|
||||||
|
|
||||||
function init(config) {
|
function init(config) {
|
||||||
Object.assign(api, config || {});
|
Object.assign(api, config || {});
|
||||||
}
|
}
|
||||||
@@ -413,420 +424,17 @@
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findSignIdByAstrologyName(name) {
|
function getPanelRenderContext(month) {
|
||||||
const token = api.normalizeCalendarText(name);
|
return {
|
||||||
if (!token) {
|
month,
|
||||||
return null;
|
api,
|
||||||
}
|
getState,
|
||||||
|
buildAssociationButtons,
|
||||||
for (const [signId, sign] of getState().signsById || []) {
|
renderFactsCard,
|
||||||
const idToken = api.normalizeCalendarText(signId);
|
renderAssociationsCard,
|
||||||
const nameToken = api.normalizeCalendarText(sign?.name?.en || sign?.name || "");
|
renderEventsCard,
|
||||||
if (token === idToken || token === nameToken) {
|
renderHolidaysCard
|
||||||
return signId;
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMajorArcanaRowsForMonth(month) {
|
|
||||||
const currentState = getState();
|
|
||||||
if (currentState.selectedCalendar !== "gregorian") {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthOrder = Number(month?.order);
|
|
||||||
if (!Number.isFinite(monthOrder)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthStart = new Date(currentState.selectedYear, monthOrder - 1, 1, 12, 0, 0, 0);
|
|
||||||
const monthEnd = new Date(currentState.selectedYear, monthOrder, 0, 12, 0, 0, 0);
|
|
||||||
const rows = [];
|
|
||||||
|
|
||||||
currentState.hebrewById?.forEach((letter) => {
|
|
||||||
const astrologyType = api.normalizeCalendarText(letter?.astrology?.type);
|
|
||||||
if (astrologyType !== "zodiac") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signId = findSignIdByAstrologyName(letter?.astrology?.name);
|
|
||||||
const sign = signId ? currentState.signsById?.get(signId) : null;
|
|
||||||
if (!sign) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startToken = api.parseMonthDayToken(sign?.start);
|
|
||||||
const endToken = api.parseMonthDayToken(sign?.end);
|
|
||||||
if (!startToken || !endToken) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const spanStart = new Date(currentState.selectedYear, startToken.month - 1, startToken.day, 12, 0, 0, 0);
|
|
||||||
const spanEnd = new Date(currentState.selectedYear, endToken.month - 1, endToken.day, 12, 0, 0, 0);
|
|
||||||
const wraps = spanEnd.getTime() < spanStart.getTime();
|
|
||||||
|
|
||||||
const segments = wraps
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
start: spanStart,
|
|
||||||
end: new Date(currentState.selectedYear, 11, 31, 12, 0, 0, 0)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
start: new Date(currentState.selectedYear, 0, 1, 12, 0, 0, 0),
|
|
||||||
end: spanEnd
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: [{ start: spanStart, end: spanEnd }];
|
|
||||||
|
|
||||||
segments.forEach((segment) => {
|
|
||||||
const overlap = api.intersectDateRanges(segment.start, segment.end, monthStart, monthEnd);
|
|
||||||
if (!overlap) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rangeStartDay = overlap.start.getDate();
|
|
||||||
const rangeEndDay = overlap.end.getDate();
|
|
||||||
const cardName = String(letter?.tarot?.card || "").trim();
|
|
||||||
const trumpNumber = Number(letter?.tarot?.trumpNumber);
|
|
||||||
if (!cardName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rows.push({
|
|
||||||
id: `${signId}-${rangeStartDay}-${rangeEndDay}`,
|
|
||||||
signId,
|
|
||||||
signName: sign?.name?.en || sign?.name || signId,
|
|
||||||
signSymbol: sign?.symbol || "",
|
|
||||||
cardName,
|
|
||||||
trumpNumber: Number.isFinite(trumpNumber) ? Math.trunc(trumpNumber) : null,
|
|
||||||
hebrewLetterId: String(letter?.hebrewLetterId || "").trim(),
|
|
||||||
hebrewLetterName: String(letter?.name || "").trim(),
|
|
||||||
hebrewLetterChar: String(letter?.char || "").trim(),
|
|
||||||
dayStart: rangeStartDay,
|
|
||||||
dayEnd: rangeEndDay,
|
|
||||||
rangeLabel: `${month?.name || "Month"} ${rangeStartDay}-${rangeEndDay}`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
rows.sort((left, right) => {
|
|
||||||
if (left.dayStart !== right.dayStart) {
|
|
||||||
return left.dayStart - right.dayStart;
|
|
||||||
}
|
|
||||||
return left.cardName.localeCompare(right.cardName);
|
|
||||||
});
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMajorArcanaCard(month) {
|
|
||||||
const selectedDay = api.getSelectedDayFilterContext(month);
|
|
||||||
const allRows = buildMajorArcanaRowsForMonth(month);
|
|
||||||
|
|
||||||
const rows = selectedDay
|
|
||||||
? allRows.filter((row) => selectedDay.entries.some((entry) => entry.dayNumber >= row.dayStart && entry.dayNumber <= row.dayEnd))
|
|
||||||
: allRows;
|
|
||||||
|
|
||||||
if (!rows.length) {
|
|
||||||
return `
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>Major Arcana Windows</strong>
|
|
||||||
<div class="planet-text">No major arcana windows for this month.</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = rows.map((row) => {
|
|
||||||
const label = row.hebrewLetterId
|
|
||||||
? `${row.hebrewLetterChar ? `${row.hebrewLetterChar} ` : ""}${row.hebrewLetterName || row.hebrewLetterId}`
|
|
||||||
: "--";
|
|
||||||
const displayCardName = api.getDisplayTarotName(row.cardName, row.trumpNumber);
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="cal-item-row">
|
|
||||||
<div class="cal-item-head">
|
|
||||||
<span class="cal-item-name">${displayCardName}${row.trumpNumber != null ? ` · Trump ${row.trumpNumber}` : ""}</span>
|
|
||||||
<span class="planet-list-meta">${row.rangeLabel}</span>
|
|
||||||
</div>
|
|
||||||
<div class="planet-list-meta">${row.signSymbol} ${row.signName} · Hebrew: ${label}</div>
|
|
||||||
<div class="alpha-nav-btns">
|
|
||||||
<button class="alpha-nav-btn" data-nav="calendar-day-range" data-range-start="${row.dayStart}" data-range-end="${row.dayEnd}">${row.rangeLabel} ↗</button>
|
|
||||||
<button class="alpha-nav-btn" data-nav="tarot-card" data-card-name="${row.cardName}" data-trump-number="${row.trumpNumber ?? ""}">${displayCardName} ↗</button>
|
|
||||||
${row.hebrewLetterId ? `<button class="alpha-nav-btn" data-nav="alphabet" data-alphabet="hebrew" data-hebrew-letter-id="${row.hebrewLetterId}">${label} ↗</button>` : ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join("");
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>Major Arcana Windows</strong>
|
|
||||||
<div class="cal-item-stack">${list}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDecanTarotCard(month) {
|
|
||||||
const selectedDay = api.getSelectedDayFilterContext(month);
|
|
||||||
const allRows = api.buildDecanTarotRowsForMonth(month);
|
|
||||||
const rows = selectedDay
|
|
||||||
? allRows.filter((row) => selectedDay.entries.some((entry) => {
|
|
||||||
const targetDate = entry.gregorianDate;
|
|
||||||
if (!(targetDate instanceof Date) || Number.isNaN(targetDate.getTime())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetMonth = targetDate.getMonth() + 1;
|
|
||||||
const targetDayNo = targetDate.getDate();
|
|
||||||
return api.isMonthDayInRange(
|
|
||||||
targetMonth,
|
|
||||||
targetDayNo,
|
|
||||||
row.startMonth,
|
|
||||||
row.startDay,
|
|
||||||
row.endMonth,
|
|
||||||
row.endDay
|
|
||||||
);
|
|
||||||
}))
|
|
||||||
: allRows;
|
|
||||||
|
|
||||||
if (!rows.length) {
|
|
||||||
return `
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>Decan Tarot Windows</strong>
|
|
||||||
<div class="planet-text">No decan tarot windows for this month.</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = rows.map((row) => {
|
|
||||||
const displayCardName = api.getDisplayTarotName(row.cardName);
|
|
||||||
return `
|
|
||||||
<div class="cal-item-row">
|
|
||||||
<div class="cal-item-head">
|
|
||||||
<span class="cal-item-name">${row.signSymbol} ${row.signName} · Decan ${row.decanIndex}</span>
|
|
||||||
<span class="planet-list-meta">${row.startDegree}°–${row.endDegree}° · ${row.dateRange}</span>
|
|
||||||
</div>
|
|
||||||
<div class="alpha-nav-btns">
|
|
||||||
<button class="alpha-nav-btn" data-nav="tarot-card" data-card-name="${row.cardName}">${displayCardName} ↗</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join("");
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>Decan Tarot Windows</strong>
|
|
||||||
<div class="cal-item-stack">${list}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDayLinksCard(month) {
|
|
||||||
const rows = api.getMonthDayLinkRows(month);
|
|
||||||
if (!rows.length) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedContext = api.getSelectedDayFilterContext(month);
|
|
||||||
const selectedDaySet = selectedContext?.dayNumbers || new Set();
|
|
||||||
const selectedDays = selectedContext?.entries?.map((entry) => entry.dayNumber) || [];
|
|
||||||
const selectedSummary = selectedDays.length ? selectedDays.join(", ") : "";
|
|
||||||
|
|
||||||
const links = rows.map((row) => {
|
|
||||||
if (!row.isResolved) {
|
|
||||||
return `<span class="planet-list-meta">${row.day}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSelected = selectedDaySet.has(Number(row.day));
|
|
||||||
return `<button class="alpha-nav-btn${isSelected ? " is-selected" : ""}" data-nav="calendar-day" data-day-number="${row.day}" data-gregorian-date="${row.gregorianDate}" aria-pressed="${isSelected ? "true" : "false"}" title="Filter this month by day ${row.day}">${row.day}</button>`;
|
|
||||||
}).join("");
|
|
||||||
|
|
||||||
const clearButton = selectedContext
|
|
||||||
? '<button class="alpha-nav-btn" data-nav="calendar-day-clear" type="button">Show All Days</button>'
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const helperText = selectedContext
|
|
||||||
? `<div class="planet-list-meta">Filtered to days: ${selectedSummary}</div>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>Day Links</strong>
|
|
||||||
<div class="planet-text">Filter this month to events, holidays, and data connected to a specific day.</div>
|
|
||||||
${helperText}
|
|
||||||
<div class="alpha-nav-btns">${links}</div>
|
|
||||||
${clearButton ? `<div class="alpha-nav-btns">${clearButton}</div>` : ""}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderHebrewMonthDetail(month) {
|
|
||||||
const currentState = getState();
|
|
||||||
const gregorianStartDate = api.getGregorianReferenceDateForCalendarMonth(month);
|
|
||||||
const factsRows = [
|
|
||||||
["Hebrew Name", month.nativeName || "--"],
|
|
||||||
["Month Order", month.leapYearOnly ? `${month.order} (leap year only)` : String(month.order)],
|
|
||||||
["Gregorian Reference Year", String(currentState.selectedYear)],
|
|
||||||
["Month Start (Gregorian)", api.formatGregorianReferenceDate(gregorianStartDate)],
|
|
||||||
["Days", month.daysVariant ? `${month.days}–${month.daysVariant} (varies)` : String(month.days || "--")],
|
|
||||||
["Season", month.season || "--"],
|
|
||||||
["Zodiac Sign", api.cap(month.zodiacSign) || "--"],
|
|
||||||
["Tribe of Israel", month.tribe || "--"],
|
|
||||||
["Sense", month.sense || "--"],
|
|
||||||
["Hebrew Letter", month.hebrewLetter || "--"]
|
|
||||||
].map(([dt, dd]) => `<dt>${dt}</dt><dd>${dd}</dd>`).join("");
|
|
||||||
|
|
||||||
const monthOrder = Number(month?.order);
|
|
||||||
const navButtons = buildAssociationButtons({
|
|
||||||
...(month?.associations || {}),
|
|
||||||
...(Number.isFinite(monthOrder) ? { numberValue: Math.trunc(monthOrder) } : {})
|
|
||||||
});
|
|
||||||
const connectionsCard = navButtons
|
|
||||||
? `<div class="planet-meta-card"><strong>Connections</strong>${navButtons}</div>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="planet-meta-grid">
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>Month Facts</strong>
|
|
||||||
<div class="planet-text">
|
|
||||||
<dl class="alpha-dl">${factsRows}</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${connectionsCard}
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>About ${month.name}</strong>
|
|
||||||
<div class="planet-text">${month.description || "--"}</div>
|
|
||||||
</div>
|
|
||||||
${renderDayLinksCard(month)}
|
|
||||||
${renderHolidaysCard(month, "Holiday Repository")}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderIslamicMonthDetail(month) {
|
|
||||||
const currentState = getState();
|
|
||||||
const gregorianStartDate = api.getGregorianReferenceDateForCalendarMonth(month);
|
|
||||||
const factsRows = [
|
|
||||||
["Arabic Name", month.nativeName || "--"],
|
|
||||||
["Month Order", String(month.order)],
|
|
||||||
["Gregorian Reference Year", String(currentState.selectedYear)],
|
|
||||||
["Month Start (Gregorian)", api.formatGregorianReferenceDate(gregorianStartDate)],
|
|
||||||
["Meaning", month.meaning || "--"],
|
|
||||||
["Days", month.daysVariant ? `${month.days}–${month.daysVariant} (varies)` : String(month.days || "--")],
|
|
||||||
["Sacred Month", month.sacred ? "Yes - warfare prohibited" : "No"]
|
|
||||||
].map(([dt, dd]) => `<dt>${dt}</dt><dd>${dd}</dd>`).join("");
|
|
||||||
|
|
||||||
const monthOrder = Number(month?.order);
|
|
||||||
const hasNumberLink = Number.isFinite(monthOrder) && monthOrder >= 0;
|
|
||||||
const navButtons = hasNumberLink
|
|
||||||
? buildAssociationButtons({ numberValue: Math.trunc(monthOrder) })
|
|
||||||
: "";
|
|
||||||
const connectionsCard = hasNumberLink
|
|
||||||
? `<div class="planet-meta-card"><strong>Connections</strong>${navButtons}</div>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="planet-meta-grid">
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>Month Facts</strong>
|
|
||||||
<div class="planet-text">
|
|
||||||
<dl class="alpha-dl">${factsRows}</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${connectionsCard}
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>About ${month.name}</strong>
|
|
||||||
<div class="planet-text">${month.description || "--"}</div>
|
|
||||||
</div>
|
|
||||||
${renderDayLinksCard(month)}
|
|
||||||
${renderHolidaysCard(month, "Holiday Repository")}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildWheelDeityButtons(deities) {
|
|
||||||
const buttons = [];
|
|
||||||
(Array.isArray(deities) ? deities : []).forEach((rawName) => {
|
|
||||||
const cleanName = String(rawName || "").replace(/\s*\/.*$/, "").replace(/\s*\(.*\)$/, "").trim();
|
|
||||||
const godId = api.findGodIdByName(cleanName) || api.findGodIdByName(rawName);
|
|
||||||
if (!godId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const god = getState().godsById?.get(godId);
|
|
||||||
const label = god?.name || cleanName;
|
|
||||||
buttons.push(`<button class="alpha-nav-btn" data-nav="god" data-god-id="${godId}" data-god-name="${label}">${label} ↗</button>`);
|
|
||||||
});
|
|
||||||
return buttons;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderWheelMonthDetail(month) {
|
|
||||||
const currentState = getState();
|
|
||||||
const gregorianStartDate = api.getGregorianReferenceDateForCalendarMonth(month);
|
|
||||||
const assoc = month?.associations;
|
|
||||||
const themes = Array.isArray(assoc?.themes) ? assoc.themes.join(", ") : "--";
|
|
||||||
const deities = Array.isArray(assoc?.deities) ? assoc.deities.join(", ") : "--";
|
|
||||||
const colors = Array.isArray(assoc?.colors) ? assoc.colors.join(", ") : "--";
|
|
||||||
const herbs = Array.isArray(assoc?.herbs) ? assoc.herbs.join(", ") : "--";
|
|
||||||
|
|
||||||
const factsRows = [
|
|
||||||
["Date", month.date || "--"],
|
|
||||||
["Type", api.cap(month.type) || "--"],
|
|
||||||
["Gregorian Reference Year", String(currentState.selectedYear)],
|
|
||||||
["Start (Gregorian)", api.formatGregorianReferenceDate(gregorianStartDate)],
|
|
||||||
["Season", month.season || "--"],
|
|
||||||
["Element", api.cap(month.element) || "--"],
|
|
||||||
["Direction", assoc?.direction || "--"]
|
|
||||||
].map(([dt, dd]) => `<dt>${dt}</dt><dd>${dd}</dd>`).join("");
|
|
||||||
|
|
||||||
const assocRows = [
|
|
||||||
["Themes", themes],
|
|
||||||
["Deities", deities],
|
|
||||||
["Colors", colors],
|
|
||||||
["Herbs", herbs]
|
|
||||||
].map(([dt, dd]) => `<dt>${dt}</dt><dd class="planet-text">${dd}</dd>`).join("");
|
|
||||||
|
|
||||||
const deityButtons = buildWheelDeityButtons(assoc?.deities);
|
|
||||||
const deityLinksCard = deityButtons.length
|
|
||||||
? `<div class="planet-meta-card"><strong>Linked Deities</strong><div class="alpha-nav-btns">${deityButtons.join("")}</div></div>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const monthOrder = Number(month?.order);
|
|
||||||
const hasNumberLink = Number.isFinite(monthOrder) && monthOrder >= 0;
|
|
||||||
const numberButtons = hasNumberLink
|
|
||||||
? buildAssociationButtons({ numberValue: Math.trunc(monthOrder) })
|
|
||||||
: "";
|
|
||||||
const numberLinksCard = hasNumberLink
|
|
||||||
? `<div class="planet-meta-card"><strong>Connections</strong>${numberButtons}</div>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="planet-meta-grid">
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>Sabbat Facts</strong>
|
|
||||||
<div class="planet-text">
|
|
||||||
<dl class="alpha-dl">${factsRows}</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>About ${month.name}</strong>
|
|
||||||
<div class="planet-text">${month.description || "--"}</div>
|
|
||||||
</div>
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>Associations</strong>
|
|
||||||
<div class="planet-text">
|
|
||||||
<dl class="alpha-dl">${assocRows}</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${renderDayLinksCard(month)}
|
|
||||||
${numberLinksCard}
|
|
||||||
${deityLinksCard}
|
|
||||||
${renderHolidaysCard(month, "Holiday Repository")}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachNavHandlers(detailBodyEl) {
|
function attachNavHandlers(detailBodyEl) {
|
||||||
@@ -964,28 +572,19 @@
|
|||||||
detailNameEl.textContent = month.name || month.id;
|
detailNameEl.textContent = month.name || month.id;
|
||||||
|
|
||||||
const currentState = getState();
|
const currentState = getState();
|
||||||
|
const panelContext = getPanelRenderContext(month);
|
||||||
if (currentState.selectedCalendar === "gregorian") {
|
if (currentState.selectedCalendar === "gregorian") {
|
||||||
detailSubEl.textContent = `${api.parseMonthRange(month)} · ${month.coreTheme || "Month correspondences"}`;
|
detailSubEl.textContent = `${api.parseMonthRange(month)} · ${month.coreTheme || "Month correspondences"}`;
|
||||||
detailBodyEl.innerHTML = `
|
detailBodyEl.innerHTML = calendarDetailPanelsUi.renderGregorianMonthDetail(panelContext);
|
||||||
<div class="planet-meta-grid">
|
|
||||||
${renderFactsCard(month)}
|
|
||||||
${renderDayLinksCard(month)}
|
|
||||||
${renderAssociationsCard(month)}
|
|
||||||
${renderMajorArcanaCard(month)}
|
|
||||||
${renderDecanTarotCard(month)}
|
|
||||||
${renderEventsCard(month)}
|
|
||||||
${renderHolidaysCard(month, "Holiday Repository")}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else if (currentState.selectedCalendar === "hebrew") {
|
} else if (currentState.selectedCalendar === "hebrew") {
|
||||||
detailSubEl.textContent = api.getMonthSubtitle(month);
|
detailSubEl.textContent = api.getMonthSubtitle(month);
|
||||||
detailBodyEl.innerHTML = renderHebrewMonthDetail(month);
|
detailBodyEl.innerHTML = calendarDetailPanelsUi.renderHebrewMonthDetail(panelContext);
|
||||||
} else if (currentState.selectedCalendar === "islamic") {
|
} else if (currentState.selectedCalendar === "islamic") {
|
||||||
detailSubEl.textContent = api.getMonthSubtitle(month);
|
detailSubEl.textContent = api.getMonthSubtitle(month);
|
||||||
detailBodyEl.innerHTML = renderIslamicMonthDetail(month);
|
detailBodyEl.innerHTML = calendarDetailPanelsUi.renderIslamicMonthDetail(panelContext);
|
||||||
} else {
|
} else {
|
||||||
detailSubEl.textContent = api.getMonthSubtitle(month);
|
detailSubEl.textContent = api.getMonthSubtitle(month);
|
||||||
detailBodyEl.innerHTML = renderWheelMonthDetail(month);
|
detailBodyEl.innerHTML = calendarDetailPanelsUi.renderWheelMonthDetail(panelContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
attachNavHandlers(detailBodyEl);
|
attachNavHandlers(detailBodyEl);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
const { getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
|
const { getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
|
||||||
const calendarDatesUi = window.TarotCalendarDates || {};
|
const calendarDatesUi = window.TarotCalendarDates || {};
|
||||||
const calendarDetailUi = window.TarotCalendarDetail || {};
|
const calendarDetailUi = window.TarotCalendarDetail || {};
|
||||||
|
const calendarDataUi = window.CalendarDataUi || {};
|
||||||
const {
|
const {
|
||||||
addDays,
|
addDays,
|
||||||
buildSignDateBounds,
|
buildSignDateBounds,
|
||||||
@@ -29,6 +30,17 @@
|
|||||||
resolveHolidayGregorianDate
|
resolveHolidayGregorianDate
|
||||||
} = calendarDatesUi;
|
} = calendarDatesUi;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof calendarDataUi.getMonthDayLinkRows !== "function"
|
||||||
|
|| typeof calendarDataUi.buildDecanTarotRowsForMonth !== "function"
|
||||||
|
|| typeof calendarDataUi.eventSearchText !== "function"
|
||||||
|
|| typeof calendarDataUi.holidaySearchText !== "function"
|
||||||
|
|| typeof calendarDataUi.buildHolidayList !== "function"
|
||||||
|
|| typeof calendarDataUi.buildMonthSearchText !== "function"
|
||||||
|
) {
|
||||||
|
throw new Error("CalendarDataUi module must load before ui-calendar.js");
|
||||||
|
}
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
referenceData: null,
|
referenceData: null,
|
||||||
@@ -317,116 +329,6 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDecanWindow(sign, decanIndex) {
|
|
||||||
const bounds = buildSignDateBounds(sign);
|
|
||||||
const index = Number(decanIndex);
|
|
||||||
if (!bounds || !Number.isFinite(index)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = addDays(bounds.start, (index - 1) * 10);
|
|
||||||
const nominalEnd = addDays(start, 9);
|
|
||||||
const end = nominalEnd.getTime() > bounds.end.getTime() ? bounds.end : nominalEnd;
|
|
||||||
|
|
||||||
return {
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
label: `${formatDateLabel(start)}–${formatDateLabel(end)}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function listMonthNumbersBetween(start, end) {
|
|
||||||
const result = [];
|
|
||||||
const seen = new Set();
|
|
||||||
const cursor = new Date(start.getFullYear(), start.getMonth(), 1);
|
|
||||||
const limit = new Date(end.getFullYear(), end.getMonth(), 1);
|
|
||||||
|
|
||||||
while (cursor.getTime() <= limit.getTime()) {
|
|
||||||
const monthNo = cursor.getMonth() + 1;
|
|
||||||
if (!seen.has(monthNo)) {
|
|
||||||
seen.add(monthNo);
|
|
||||||
result.push(monthNo);
|
|
||||||
}
|
|
||||||
cursor.setMonth(cursor.getMonth() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildDecanTarotRowsForMonth(month) {
|
|
||||||
const monthOrder = Number(month?.order);
|
|
||||||
if (!Number.isFinite(monthOrder)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = [];
|
|
||||||
const seen = new Set();
|
|
||||||
const decansBySign = state.referenceData?.decansBySign || {};
|
|
||||||
|
|
||||||
Object.entries(decansBySign).forEach(([signId, decans]) => {
|
|
||||||
const sign = state.signsById.get(signId);
|
|
||||||
if (!sign || !Array.isArray(decans)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
decans.forEach((decan) => {
|
|
||||||
const window = buildDecanWindow(sign, decan?.index);
|
|
||||||
if (!window) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthsTouched = listMonthNumbersBetween(window.start, window.end);
|
|
||||||
if (!monthsTouched.includes(monthOrder)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cardName = normalizeMinorTarotCardName(decan?.tarotMinorArcana);
|
|
||||||
if (!cardName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = `${cardName}|${signId}|${decan.index}`;
|
|
||||||
if (seen.has(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
seen.add(key);
|
|
||||||
|
|
||||||
const startDegree = (Number(decan.index) - 1) * 10;
|
|
||||||
const endDegree = startDegree + 10;
|
|
||||||
const signName = sign?.name?.en || sign?.name || signId;
|
|
||||||
|
|
||||||
rows.push({
|
|
||||||
cardName,
|
|
||||||
signId,
|
|
||||||
signName,
|
|
||||||
signSymbol: sign?.symbol || "",
|
|
||||||
decanIndex: Number(decan.index),
|
|
||||||
startDegree,
|
|
||||||
endDegree,
|
|
||||||
startTime: window.start.getTime(),
|
|
||||||
endTime: window.end.getTime(),
|
|
||||||
startMonth: window.start.getMonth() + 1,
|
|
||||||
startDay: window.start.getDate(),
|
|
||||||
endMonth: window.end.getMonth() + 1,
|
|
||||||
endDay: window.end.getDate(),
|
|
||||||
dateRange: window.label
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
rows.sort((left, right) => {
|
|
||||||
if (left.startTime !== right.startTime) {
|
|
||||||
return left.startTime - right.startTime;
|
|
||||||
}
|
|
||||||
if (left.decanIndex !== right.decanIndex) {
|
|
||||||
return left.decanIndex - right.decanIndex;
|
|
||||||
}
|
|
||||||
return left.cardName.localeCompare(right.cardName);
|
|
||||||
});
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPlanetMap(planetsObj) {
|
function buildPlanetMap(planetsObj) {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
if (!planetsObj || typeof planetsObj !== "object") {
|
if (!planetsObj || typeof planetsObj !== "object") {
|
||||||
@@ -525,46 +427,6 @@
|
|||||||
return parseMonthRange(month);
|
return parseMonthRange(month);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMonthDayLinkRows(month) {
|
|
||||||
const cacheKey = `${state.selectedCalendar}|${state.selectedYear}|${month?.id || ""}`;
|
|
||||||
if (state.dayLinksCache.has(cacheKey)) {
|
|
||||||
return state.dayLinksCache.get(cacheKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dayCount = null;
|
|
||||||
if (state.selectedCalendar === "gregorian") {
|
|
||||||
dayCount = getDaysInMonth(state.selectedYear, Number(month?.order));
|
|
||||||
} else if (state.selectedCalendar === "hebrew" || state.selectedCalendar === "islamic") {
|
|
||||||
const baseDays = Number(month?.days);
|
|
||||||
const variantDays = Number(month?.daysVariant);
|
|
||||||
if (Number.isFinite(baseDays) && Number.isFinite(variantDays)) {
|
|
||||||
dayCount = Math.max(Math.trunc(baseDays), Math.trunc(variantDays));
|
|
||||||
} else if (Number.isFinite(baseDays)) {
|
|
||||||
dayCount = Math.trunc(baseDays);
|
|
||||||
} else if (Number.isFinite(variantDays)) {
|
|
||||||
dayCount = Math.trunc(variantDays);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Number.isFinite(dayCount) || dayCount <= 0) {
|
|
||||||
state.dayLinksCache.set(cacheKey, []);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = [];
|
|
||||||
for (let day = 1; day <= dayCount; day += 1) {
|
|
||||||
const gregorianDate = resolveCalendarDayToGregorian(month, day);
|
|
||||||
rows.push({
|
|
||||||
day,
|
|
||||||
gregorianDate: formatIsoDate(gregorianDate),
|
|
||||||
isResolved: Boolean(gregorianDate && !Number.isNaN(gregorianDate.getTime()))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
state.dayLinksCache.set(cacheKey, rows);
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderList(elements) {
|
function renderList(elements) {
|
||||||
const { monthListEl, monthCountEl, listTitleEl } = elements;
|
const { monthListEl, monthCountEl, listTitleEl } = elements;
|
||||||
if (!monthListEl) {
|
if (!monthListEl) {
|
||||||
@@ -601,162 +463,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function associationSearchText(associations) {
|
|
||||||
if (!associations || typeof associations !== "object") {
|
function getCalendarDataContext() {
|
||||||
return "";
|
return {
|
||||||
|
state,
|
||||||
|
normalizeText,
|
||||||
|
normalizeSearchValue,
|
||||||
|
normalizeMinorTarotCardName,
|
||||||
|
getTarotCardSearchAliases,
|
||||||
|
addDays,
|
||||||
|
buildSignDateBounds,
|
||||||
|
formatDateLabel,
|
||||||
|
formatIsoDate,
|
||||||
|
getDaysInMonth,
|
||||||
|
resolveCalendarDayToGregorian,
|
||||||
|
resolveHolidayGregorianDate
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const tarotAliases = associations.tarotCard && typeof getTarotCardSearchAliases === "function"
|
function buildDecanTarotRowsForMonth(month) {
|
||||||
? getTarotCardSearchAliases(associations.tarotCard, { trumpNumber: associations.tarotTrumpNumber })
|
return calendarDataUi.buildDecanTarotRowsForMonth(getCalendarDataContext(), month);
|
||||||
: [];
|
}
|
||||||
|
|
||||||
return [
|
function getMonthDayLinkRows(month) {
|
||||||
associations.planetId,
|
return calendarDataUi.getMonthDayLinkRows(getCalendarDataContext(), month);
|
||||||
associations.zodiacSignId,
|
|
||||||
associations.numberValue,
|
|
||||||
associations.tarotCard,
|
|
||||||
associations.tarotTrumpNumber,
|
|
||||||
...tarotAliases,
|
|
||||||
associations.godId,
|
|
||||||
associations.godName,
|
|
||||||
associations.hebrewLetterId,
|
|
||||||
associations.kabbalahPathNumber,
|
|
||||||
associations.iChingPlanetaryInfluence
|
|
||||||
].filter(Boolean).join(" ");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventSearchText(event) {
|
function eventSearchText(event) {
|
||||||
return normalizeSearchValue([
|
return calendarDataUi.eventSearchText(getCalendarDataContext(), event);
|
||||||
event?.name,
|
|
||||||
event?.date,
|
|
||||||
event?.dateRange,
|
|
||||||
event?.description,
|
|
||||||
associationSearchText(event?.associations)
|
|
||||||
].filter(Boolean).join(" "));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function holidaySearchText(holiday) {
|
function holidaySearchText(holiday) {
|
||||||
return normalizeSearchValue([
|
return calendarDataUi.holidaySearchText(getCalendarDataContext(), holiday);
|
||||||
holiday?.name,
|
|
||||||
holiday?.kind,
|
|
||||||
holiday?.date,
|
|
||||||
holiday?.dateRange,
|
|
||||||
holiday?.dateText,
|
|
||||||
holiday?.monthDayStart,
|
|
||||||
holiday?.calendarId,
|
|
||||||
holiday?.description,
|
|
||||||
associationSearchText(holiday?.associations)
|
|
||||||
].filter(Boolean).join(" "));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildHolidayList(month) {
|
function buildHolidayList(month) {
|
||||||
const calendarId = state.selectedCalendar;
|
return calendarDataUi.buildHolidayList(getCalendarDataContext(), month);
|
||||||
const monthOrder = Number(month?.order);
|
|
||||||
|
|
||||||
const fromRepo = state.calendarHolidays.filter((holiday) => {
|
|
||||||
const holidayCalendarId = normalizeText(holiday?.calendarId).toLowerCase();
|
|
||||||
if (holidayCalendarId !== calendarId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDirectMonthMatch = normalizeText(holiday?.monthId).toLowerCase() === normalizeText(month?.id).toLowerCase();
|
|
||||||
if (isDirectMonthMatch) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (calendarId === "gregorian" && holiday?.dateRule && Number.isFinite(monthOrder)) {
|
|
||||||
const computedDate = resolveHolidayGregorianDate(holiday);
|
|
||||||
return computedDate instanceof Date
|
|
||||||
&& !Number.isNaN(computedDate.getTime())
|
|
||||||
&& (computedDate.getMonth() + 1) === Math.trunc(monthOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fromRepo.length) {
|
|
||||||
return [...fromRepo].sort((left, right) => {
|
|
||||||
const leftDate = resolveHolidayGregorianDate(left);
|
|
||||||
const rightDate = resolveHolidayGregorianDate(right);
|
|
||||||
const leftDay = Number.isFinite(Number(left?.day))
|
|
||||||
? Number(left.day)
|
|
||||||
: ((leftDate instanceof Date && !Number.isNaN(leftDate.getTime())) ? leftDate.getDate() : NaN);
|
|
||||||
const rightDay = Number.isFinite(Number(right?.day))
|
|
||||||
? Number(right.day)
|
|
||||||
: ((rightDate instanceof Date && !Number.isNaN(rightDate.getTime())) ? rightDate.getDate() : NaN);
|
|
||||||
if (Number.isFinite(leftDay) && Number.isFinite(rightDay) && leftDay !== rightDay) {
|
|
||||||
return leftDay - rightDay;
|
|
||||||
}
|
|
||||||
return normalizeText(left?.name).localeCompare(normalizeText(right?.name));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const seen = new Set();
|
|
||||||
const ordered = [];
|
|
||||||
|
|
||||||
(month?.holidayIds || []).forEach((holidayId) => {
|
|
||||||
const holiday = state.holidays.find((item) => item.id === holidayId);
|
|
||||||
if (holiday && !seen.has(holiday.id)) {
|
|
||||||
seen.add(holiday.id);
|
|
||||||
ordered.push(holiday);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
state.holidays.forEach((holiday) => {
|
|
||||||
if (holiday?.monthId === month.id && !seen.has(holiday.id)) {
|
|
||||||
seen.add(holiday.id);
|
|
||||||
ordered.push(holiday);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ordered;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMonthSearchText(month) {
|
function buildMonthSearchText(month) {
|
||||||
const monthHolidays = buildHolidayList(month);
|
return calendarDataUi.buildMonthSearchText(getCalendarDataContext(), month);
|
||||||
const holidayText = monthHolidays.map((holiday) => holidaySearchText(holiday)).join(" ");
|
|
||||||
|
|
||||||
if (state.selectedCalendar === "gregorian") {
|
|
||||||
const events = Array.isArray(month?.events) ? month.events : [];
|
|
||||||
return normalizeSearchValue([
|
|
||||||
month?.name,
|
|
||||||
month?.id,
|
|
||||||
month?.start,
|
|
||||||
month?.end,
|
|
||||||
month?.coreTheme,
|
|
||||||
month?.seasonNorth,
|
|
||||||
month?.seasonSouth,
|
|
||||||
associationSearchText(month?.associations),
|
|
||||||
events.map((event) => eventSearchText(event)).join(" "),
|
|
||||||
holidayText
|
|
||||||
].filter(Boolean).join(" "));
|
|
||||||
}
|
|
||||||
|
|
||||||
const wheelAssocText = month?.associations
|
|
||||||
? [
|
|
||||||
Array.isArray(month.associations.themes) ? month.associations.themes.join(" ") : "",
|
|
||||||
Array.isArray(month.associations.deities) ? month.associations.deities.join(" ") : "",
|
|
||||||
month.associations.element,
|
|
||||||
month.associations.direction
|
|
||||||
].filter(Boolean).join(" ")
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return normalizeSearchValue([
|
|
||||||
month?.name,
|
|
||||||
month?.id,
|
|
||||||
month?.nativeName,
|
|
||||||
month?.meaning,
|
|
||||||
month?.season,
|
|
||||||
month?.description,
|
|
||||||
month?.zodiacSign,
|
|
||||||
month?.tribe,
|
|
||||||
month?.element,
|
|
||||||
month?.type,
|
|
||||||
month?.date,
|
|
||||||
month?.hebrewLetter,
|
|
||||||
holidayText,
|
|
||||||
wheelAssocText
|
|
||||||
].filter(Boolean).join(" "));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesSearch(searchText) {
|
function matchesSearch(searchText) {
|
||||||
|
|||||||
550
app/ui-cube-chassis.js
Normal file
550
app/ui-cube-chassis.js
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function renderFaceSvg(context) {
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
containerEl,
|
||||||
|
walls,
|
||||||
|
normalizeId,
|
||||||
|
projectVertices,
|
||||||
|
FACE_GEOMETRY,
|
||||||
|
facePoint,
|
||||||
|
normalizeEdgeId,
|
||||||
|
getEdges,
|
||||||
|
EDGE_GEOMETRY,
|
||||||
|
EDGE_GEOMETRY_KEYS,
|
||||||
|
formatEdgeName,
|
||||||
|
getEdgeWalls,
|
||||||
|
getElements,
|
||||||
|
render,
|
||||||
|
snapRotationToWall,
|
||||||
|
getWallFaceLetter,
|
||||||
|
getWallTarotCard,
|
||||||
|
resolveCardImageUrl,
|
||||||
|
openTarotCardLightbox,
|
||||||
|
MOTHER_CONNECTORS,
|
||||||
|
formatDirectionName,
|
||||||
|
getConnectorTarotCard,
|
||||||
|
getHebrewLetterSymbol,
|
||||||
|
toDisplayText,
|
||||||
|
CUBE_VIEW_CENTER,
|
||||||
|
getEdgeMarkerDisplay,
|
||||||
|
getEdgeTarotCard,
|
||||||
|
getCubeCenterData,
|
||||||
|
getCenterTarotCard,
|
||||||
|
getCenterLetterSymbol
|
||||||
|
} = context;
|
||||||
|
|
||||||
|
if (!containerEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const svgNS = "http://www.w3.org/2000/svg";
|
||||||
|
const svg = document.createElementNS(svgNS, "svg");
|
||||||
|
svg.setAttribute("viewBox", "0 0 240 220");
|
||||||
|
svg.setAttribute("width", "100%");
|
||||||
|
svg.setAttribute("class", "cube-svg");
|
||||||
|
svg.setAttribute("role", "img");
|
||||||
|
svg.setAttribute("aria-label", "Cube of Space interactive chassis");
|
||||||
|
|
||||||
|
const wallById = new Map(walls.map((wall) => [normalizeId(wall?.id), wall]));
|
||||||
|
const projectedVertices = projectVertices();
|
||||||
|
const faces = Object.entries(FACE_GEOMETRY)
|
||||||
|
.map(([wallId, indices]) => {
|
||||||
|
const wall = wallById.get(wallId);
|
||||||
|
if (!wall) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quad = indices.map((index) => projectedVertices[index]);
|
||||||
|
const avgDepth = quad.reduce((sum, point) => sum + point.z, 0) / quad.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
wallId,
|
||||||
|
wall,
|
||||||
|
quad,
|
||||||
|
depth: avgDepth,
|
||||||
|
pointsText: quad.map((point) => `${point.x.toFixed(2)},${point.y.toFixed(2)}`).join(" ")
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((left, right) => left.depth - right.depth);
|
||||||
|
|
||||||
|
faces.forEach((faceData) => {
|
||||||
|
const { wallId, wall, quad, pointsText } = faceData;
|
||||||
|
|
||||||
|
const isActive = wallId === normalizeId(state.selectedWallId);
|
||||||
|
const polygon = document.createElementNS(svgNS, "polygon");
|
||||||
|
polygon.setAttribute("points", pointsText);
|
||||||
|
polygon.setAttribute("class", `cube-face${isActive ? " is-active" : ""}`);
|
||||||
|
polygon.setAttribute("fill", "#000");
|
||||||
|
polygon.setAttribute("fill-opacity", isActive ? "0.78" : "0.62");
|
||||||
|
polygon.setAttribute("stroke", "currentColor");
|
||||||
|
polygon.setAttribute("stroke-opacity", isActive ? "0.92" : "0.68");
|
||||||
|
polygon.setAttribute("stroke-width", isActive ? "2.5" : "1");
|
||||||
|
polygon.setAttribute("data-wall-id", wallId);
|
||||||
|
polygon.setAttribute("role", "button");
|
||||||
|
polygon.setAttribute("tabindex", "0");
|
||||||
|
polygon.setAttribute("aria-label", `Cube wall ${wall?.name || wallId}`);
|
||||||
|
|
||||||
|
const selectWall = () => {
|
||||||
|
state.selectedWallId = wallId;
|
||||||
|
state.selectedEdgeId = normalizeEdgeId(context.getEdgesForWall(wallId)[0]?.id || getEdges()[0]?.id);
|
||||||
|
state.selectedNodeType = "wall";
|
||||||
|
state.selectedConnectorId = null;
|
||||||
|
snapRotationToWall(wallId);
|
||||||
|
render(getElements());
|
||||||
|
};
|
||||||
|
|
||||||
|
polygon.addEventListener("click", selectWall);
|
||||||
|
polygon.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
selectWall();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.appendChild(polygon);
|
||||||
|
|
||||||
|
const wallFaceLetter = getWallFaceLetter(wall);
|
||||||
|
const faceGlyphAnchor = facePoint(quad, 0, 0);
|
||||||
|
|
||||||
|
if (state.markerDisplayMode === "tarot") {
|
||||||
|
const cardUrl = resolveCardImageUrl(getWallTarotCard(wall));
|
||||||
|
if (cardUrl) {
|
||||||
|
let defs = svg.querySelector("defs");
|
||||||
|
if (!defs) {
|
||||||
|
defs = document.createElementNS(svgNS, "defs");
|
||||||
|
svg.insertBefore(defs, svg.firstChild);
|
||||||
|
}
|
||||||
|
const clipId = `face-clip-${wallId}`;
|
||||||
|
const clipPath = document.createElementNS(svgNS, "clipPath");
|
||||||
|
clipPath.setAttribute("id", clipId);
|
||||||
|
const clipPoly = document.createElementNS(svgNS, "polygon");
|
||||||
|
clipPoly.setAttribute("points", pointsText);
|
||||||
|
clipPath.appendChild(clipPoly);
|
||||||
|
defs.appendChild(clipPath);
|
||||||
|
|
||||||
|
const cardW = 40;
|
||||||
|
const cardH = 60;
|
||||||
|
const wallTarotCard = getWallTarotCard(wall);
|
||||||
|
const cardImg = document.createElementNS(svgNS, "image");
|
||||||
|
cardImg.setAttribute("class", "cube-tarot-image cube-face-card");
|
||||||
|
cardImg.setAttribute("href", cardUrl);
|
||||||
|
cardImg.setAttribute("x", String((faceGlyphAnchor.x - cardW / 2).toFixed(2)));
|
||||||
|
cardImg.setAttribute("y", String((faceGlyphAnchor.y - cardH / 2).toFixed(2)));
|
||||||
|
cardImg.setAttribute("width", String(cardW));
|
||||||
|
cardImg.setAttribute("height", String(cardH));
|
||||||
|
cardImg.setAttribute("clip-path", `url(#${clipId})`);
|
||||||
|
cardImg.setAttribute("role", "button");
|
||||||
|
cardImg.setAttribute("tabindex", "0");
|
||||||
|
cardImg.setAttribute("aria-label", `Open ${wallTarotCard || (wall?.name || wallId)} card image`);
|
||||||
|
cardImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||||
|
cardImg.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
selectWall();
|
||||||
|
openTarotCardLightbox(wallTarotCard, cardUrl, `${wall?.name || wallId} wall tarot card`);
|
||||||
|
});
|
||||||
|
cardImg.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
selectWall();
|
||||||
|
openTarotCardLightbox(wallTarotCard, cardUrl, `${wall?.name || wallId} wall tarot card`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
svg.appendChild(cardImg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const faceGlyph = document.createElementNS(svgNS, "text");
|
||||||
|
faceGlyph.setAttribute(
|
||||||
|
"class",
|
||||||
|
`cube-face-symbol${isActive ? " is-active" : ""}${wallFaceLetter ? "" : " is-missing"}`
|
||||||
|
);
|
||||||
|
faceGlyph.setAttribute("x", String(faceGlyphAnchor.x));
|
||||||
|
faceGlyph.setAttribute("y", String(faceGlyphAnchor.y));
|
||||||
|
faceGlyph.setAttribute("text-anchor", "middle");
|
||||||
|
faceGlyph.setAttribute("dominant-baseline", "middle");
|
||||||
|
faceGlyph.setAttribute("pointer-events", "none");
|
||||||
|
faceGlyph.textContent = wallFaceLetter || "!";
|
||||||
|
svg.appendChild(faceGlyph);
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelAnchor = facePoint(quad, 0, 0.9);
|
||||||
|
const label = document.createElementNS(svgNS, "text");
|
||||||
|
label.setAttribute("class", `cube-face-label${isActive ? " is-active" : ""}`);
|
||||||
|
label.setAttribute("x", String(labelAnchor.x));
|
||||||
|
label.setAttribute("y", String(labelAnchor.y));
|
||||||
|
label.setAttribute("text-anchor", "middle");
|
||||||
|
label.setAttribute("dominant-baseline", "middle");
|
||||||
|
label.setAttribute("pointer-events", "none");
|
||||||
|
label.textContent = wall?.name || wallId;
|
||||||
|
svg.appendChild(label);
|
||||||
|
});
|
||||||
|
|
||||||
|
const faceCenterByWallId = new Map(
|
||||||
|
faces.map((faceData) => [faceData.wallId, facePoint(faceData.quad, 0, 0)])
|
||||||
|
);
|
||||||
|
|
||||||
|
if (state.showConnectorLines) {
|
||||||
|
MOTHER_CONNECTORS.forEach((connector, connectorIndex) => {
|
||||||
|
const fromWallId = normalizeId(connector?.fromWallId);
|
||||||
|
const toWallId = normalizeId(connector?.toWallId);
|
||||||
|
const from = faceCenterByWallId.get(fromWallId);
|
||||||
|
const to = faceCenterByWallId.get(toWallId);
|
||||||
|
if (!from || !to) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectorId = normalizeId(connector?.id);
|
||||||
|
const isActive = state.selectedNodeType === "connector"
|
||||||
|
&& normalizeId(state.selectedConnectorId) === connectorId;
|
||||||
|
const connectorLetter = getHebrewLetterSymbol(connector?.hebrewLetterId);
|
||||||
|
const connectorCardUrl = state.markerDisplayMode === "tarot"
|
||||||
|
? resolveCardImageUrl(getConnectorTarotCard(connector))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const group = document.createElementNS(svgNS, "g");
|
||||||
|
group.setAttribute("class", `cube-connector${isActive ? " is-active" : ""}`);
|
||||||
|
group.setAttribute("role", "button");
|
||||||
|
group.setAttribute("tabindex", "0");
|
||||||
|
group.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
`Mother connector ${formatDirectionName(fromWallId)} to ${formatDirectionName(toWallId)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const connectorLine = document.createElementNS(svgNS, "line");
|
||||||
|
connectorLine.setAttribute("class", `cube-connector-line${isActive ? " is-active" : ""}`);
|
||||||
|
connectorLine.setAttribute("x1", from.x.toFixed(2));
|
||||||
|
connectorLine.setAttribute("y1", from.y.toFixed(2));
|
||||||
|
connectorLine.setAttribute("x2", to.x.toFixed(2));
|
||||||
|
connectorLine.setAttribute("y2", to.y.toFixed(2));
|
||||||
|
group.appendChild(connectorLine);
|
||||||
|
|
||||||
|
const connectorHit = document.createElementNS(svgNS, "line");
|
||||||
|
connectorHit.setAttribute("class", "cube-connector-hit");
|
||||||
|
connectorHit.setAttribute("x1", from.x.toFixed(2));
|
||||||
|
connectorHit.setAttribute("y1", from.y.toFixed(2));
|
||||||
|
connectorHit.setAttribute("x2", to.x.toFixed(2));
|
||||||
|
connectorHit.setAttribute("y2", to.y.toFixed(2));
|
||||||
|
group.appendChild(connectorHit);
|
||||||
|
|
||||||
|
const dx = to.x - from.x;
|
||||||
|
const dy = to.y - from.y;
|
||||||
|
const length = Math.hypot(dx, dy) || 1;
|
||||||
|
const perpX = -dy / length;
|
||||||
|
const perpY = dx / length;
|
||||||
|
const shift = (connectorIndex - 1) * 12;
|
||||||
|
const labelX = ((from.x + to.x) / 2) + (perpX * shift);
|
||||||
|
const labelY = ((from.y + to.y) / 2) + (perpY * shift);
|
||||||
|
|
||||||
|
const selectConnector = () => {
|
||||||
|
state.selectedNodeType = "connector";
|
||||||
|
state.selectedConnectorId = connectorId;
|
||||||
|
render(getElements());
|
||||||
|
};
|
||||||
|
|
||||||
|
if (state.markerDisplayMode === "tarot" && connectorCardUrl) {
|
||||||
|
const cardW = 18;
|
||||||
|
const cardH = 27;
|
||||||
|
const connectorTarotCard = getConnectorTarotCard(connector);
|
||||||
|
const connectorImg = document.createElementNS(svgNS, "image");
|
||||||
|
connectorImg.setAttribute("class", "cube-tarot-image cube-connector-card");
|
||||||
|
connectorImg.setAttribute("href", connectorCardUrl);
|
||||||
|
connectorImg.setAttribute("x", String((labelX - cardW / 2).toFixed(2)));
|
||||||
|
connectorImg.setAttribute("y", String((labelY - cardH / 2).toFixed(2)));
|
||||||
|
connectorImg.setAttribute("width", String(cardW));
|
||||||
|
connectorImg.setAttribute("height", String(cardH));
|
||||||
|
connectorImg.setAttribute("role", "button");
|
||||||
|
connectorImg.setAttribute("tabindex", "0");
|
||||||
|
connectorImg.setAttribute("aria-label", `Open ${connectorTarotCard || connector?.name || "connector"} card image`);
|
||||||
|
connectorImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||||
|
connectorImg.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
selectConnector();
|
||||||
|
openTarotCardLightbox(connectorTarotCard, connectorCardUrl, connector?.name || "Mother connector");
|
||||||
|
});
|
||||||
|
connectorImg.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
selectConnector();
|
||||||
|
openTarotCardLightbox(connectorTarotCard, connectorCardUrl, connector?.name || "Mother connector");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
group.appendChild(connectorImg);
|
||||||
|
} else {
|
||||||
|
const connectorText = document.createElementNS(svgNS, "text");
|
||||||
|
connectorText.setAttribute(
|
||||||
|
"class",
|
||||||
|
`cube-connector-symbol${isActive ? " is-active" : ""}${connectorLetter ? "" : " is-missing"}`
|
||||||
|
);
|
||||||
|
connectorText.setAttribute("x", String(labelX));
|
||||||
|
connectorText.setAttribute("y", String(labelY));
|
||||||
|
connectorText.setAttribute("text-anchor", "middle");
|
||||||
|
connectorText.setAttribute("dominant-baseline", "middle");
|
||||||
|
connectorText.setAttribute("pointer-events", "none");
|
||||||
|
connectorText.textContent = connectorLetter || "!";
|
||||||
|
group.appendChild(connectorText);
|
||||||
|
}
|
||||||
|
|
||||||
|
group.addEventListener("click", selectConnector);
|
||||||
|
group.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
selectConnector();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.appendChild(group);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const edgeById = new Map(
|
||||||
|
getEdges().map((edge) => [normalizeEdgeId(edge?.id), edge])
|
||||||
|
);
|
||||||
|
|
||||||
|
EDGE_GEOMETRY.forEach(([fromIndex, toIndex], edgeIndex) => {
|
||||||
|
const edgeId = EDGE_GEOMETRY_KEYS[edgeIndex];
|
||||||
|
const edge = edgeById.get(edgeId) || {
|
||||||
|
id: edgeId,
|
||||||
|
name: formatEdgeName(edgeId),
|
||||||
|
walls: edgeId.split("-")
|
||||||
|
};
|
||||||
|
const markerDisplay = getEdgeMarkerDisplay(edge);
|
||||||
|
const edgeWalls = getEdgeWalls(edge);
|
||||||
|
|
||||||
|
const wallIsActive = edgeWalls.includes(normalizeId(state.selectedWallId));
|
||||||
|
const edgeIsActive = normalizeEdgeId(state.selectedEdgeId) === edgeId;
|
||||||
|
|
||||||
|
const from = projectedVertices[fromIndex];
|
||||||
|
const to = projectedVertices[toIndex];
|
||||||
|
|
||||||
|
const line = document.createElementNS(svgNS, "line");
|
||||||
|
line.setAttribute("x1", from.x.toFixed(2));
|
||||||
|
line.setAttribute("y1", from.y.toFixed(2));
|
||||||
|
line.setAttribute("x2", to.x.toFixed(2));
|
||||||
|
line.setAttribute("y2", to.y.toFixed(2));
|
||||||
|
line.setAttribute("stroke", "currentColor");
|
||||||
|
line.setAttribute("stroke-opacity", edgeIsActive ? "0.94" : (wallIsActive ? "0.70" : "0.32"));
|
||||||
|
line.setAttribute("stroke-width", edgeIsActive ? "2.4" : (wallIsActive ? "1.9" : "1.4"));
|
||||||
|
line.setAttribute("class", `cube-edge-line${edgeIsActive ? " is-active" : ""}`);
|
||||||
|
line.setAttribute("role", "button");
|
||||||
|
line.setAttribute("tabindex", "0");
|
||||||
|
line.setAttribute("aria-label", `Cube edge ${toDisplayText(edge?.name) || formatEdgeName(edgeId)}`);
|
||||||
|
|
||||||
|
const selectEdge = () => {
|
||||||
|
state.selectedEdgeId = edgeId;
|
||||||
|
state.selectedNodeType = "wall";
|
||||||
|
state.selectedConnectorId = null;
|
||||||
|
if (!edgeWalls.includes(normalizeId(state.selectedWallId)) && edgeWalls[0]) {
|
||||||
|
state.selectedWallId = edgeWalls[0];
|
||||||
|
snapRotationToWall(state.selectedWallId);
|
||||||
|
}
|
||||||
|
render(getElements());
|
||||||
|
};
|
||||||
|
|
||||||
|
line.addEventListener("click", selectEdge);
|
||||||
|
line.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
selectEdge();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
svg.appendChild(line);
|
||||||
|
|
||||||
|
const dx = to.x - from.x;
|
||||||
|
const dy = to.y - from.y;
|
||||||
|
const length = Math.hypot(dx, dy) || 1;
|
||||||
|
const normalX = -dy / length;
|
||||||
|
const normalY = dx / length;
|
||||||
|
const midpointX = (from.x + to.x) / 2;
|
||||||
|
const midpointY = (from.y + to.y) / 2;
|
||||||
|
|
||||||
|
const centerVectorX = midpointX - CUBE_VIEW_CENTER.x;
|
||||||
|
const centerVectorY = midpointY - CUBE_VIEW_CENTER.y;
|
||||||
|
const normalSign = (centerVectorX * normalX + centerVectorY * normalY) >= 0 ? 1 : -1;
|
||||||
|
|
||||||
|
const markerOffset = edgeIsActive ? 17 : (wallIsActive ? 13 : 12);
|
||||||
|
const labelX = midpointX + (normalX * markerOffset * normalSign);
|
||||||
|
const labelY = midpointY + (normalY * markerOffset * normalSign);
|
||||||
|
|
||||||
|
const marker = document.createElementNS(svgNS, "g");
|
||||||
|
marker.setAttribute(
|
||||||
|
"class",
|
||||||
|
`cube-direction${wallIsActive ? " is-wall-active" : ""}${edgeIsActive ? " is-active" : ""}`
|
||||||
|
);
|
||||||
|
marker.setAttribute("role", "button");
|
||||||
|
marker.setAttribute("tabindex", "0");
|
||||||
|
marker.setAttribute("aria-label", `Cube edge ${toDisplayText(edge?.name) || formatEdgeName(edgeId)}`);
|
||||||
|
|
||||||
|
if (state.markerDisplayMode === "tarot") {
|
||||||
|
const edgeCardUrl = resolveCardImageUrl(getEdgeTarotCard(edge));
|
||||||
|
if (edgeCardUrl) {
|
||||||
|
const cardW = edgeIsActive ? 28 : 20;
|
||||||
|
const cardH = edgeIsActive ? 42 : 30;
|
||||||
|
const edgeTarotCard = getEdgeTarotCard(edge);
|
||||||
|
const cardImg = document.createElementNS(svgNS, "image");
|
||||||
|
cardImg.setAttribute("class", `cube-tarot-image cube-direction-card${edgeIsActive ? " is-active" : ""}`);
|
||||||
|
cardImg.setAttribute("href", edgeCardUrl);
|
||||||
|
cardImg.setAttribute("x", String((labelX - cardW / 2).toFixed(2)));
|
||||||
|
cardImg.setAttribute("y", String((labelY - cardH / 2).toFixed(2)));
|
||||||
|
cardImg.setAttribute("width", String(cardW));
|
||||||
|
cardImg.setAttribute("height", String(cardH));
|
||||||
|
cardImg.setAttribute("role", "button");
|
||||||
|
cardImg.setAttribute("tabindex", "0");
|
||||||
|
cardImg.setAttribute("aria-label", `Open ${edgeTarotCard || edge?.name || "edge"} card image`);
|
||||||
|
cardImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||||
|
cardImg.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
selectEdge();
|
||||||
|
openTarotCardLightbox(edgeTarotCard, edgeCardUrl, edge?.name || "Cube edge");
|
||||||
|
});
|
||||||
|
cardImg.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
selectEdge();
|
||||||
|
openTarotCardLightbox(edgeTarotCard, edgeCardUrl, edge?.name || "Cube edge");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
marker.appendChild(cardImg);
|
||||||
|
} else {
|
||||||
|
const markerText = document.createElementNS(svgNS, "text");
|
||||||
|
markerText.setAttribute("class", "cube-direction-letter is-missing");
|
||||||
|
markerText.setAttribute("x", String(labelX));
|
||||||
|
markerText.setAttribute("y", String(labelY));
|
||||||
|
markerText.setAttribute("text-anchor", "middle");
|
||||||
|
markerText.setAttribute("dominant-baseline", "middle");
|
||||||
|
markerText.textContent = "!";
|
||||||
|
marker.appendChild(markerText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const markerText = document.createElementNS(svgNS, "text");
|
||||||
|
markerText.setAttribute(
|
||||||
|
"class",
|
||||||
|
`cube-direction-letter${markerDisplay.isMissing ? " is-missing" : ""}`
|
||||||
|
);
|
||||||
|
markerText.setAttribute("x", String(labelX));
|
||||||
|
markerText.setAttribute("y", String(labelY));
|
||||||
|
markerText.setAttribute("text-anchor", "middle");
|
||||||
|
markerText.setAttribute("dominant-baseline", "middle");
|
||||||
|
markerText.textContent = markerDisplay.text;
|
||||||
|
marker.appendChild(markerText);
|
||||||
|
}
|
||||||
|
|
||||||
|
marker.addEventListener("click", selectEdge);
|
||||||
|
marker.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
selectEdge();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.appendChild(marker);
|
||||||
|
});
|
||||||
|
|
||||||
|
const center = getCubeCenterData();
|
||||||
|
if (center && state.showPrimalPoint) {
|
||||||
|
const centerLetter = getCenterLetterSymbol(center);
|
||||||
|
const centerCardUrl = state.markerDisplayMode === "tarot"
|
||||||
|
? resolveCardImageUrl(getCenterTarotCard(center))
|
||||||
|
: null;
|
||||||
|
const centerActive = state.selectedNodeType === "center";
|
||||||
|
|
||||||
|
const centerMarker = document.createElementNS(svgNS, "g");
|
||||||
|
centerMarker.setAttribute("class", `cube-center${centerActive ? " is-active" : ""}`);
|
||||||
|
centerMarker.setAttribute("role", "button");
|
||||||
|
centerMarker.setAttribute("tabindex", "0");
|
||||||
|
centerMarker.setAttribute("aria-label", "Cube primal point");
|
||||||
|
|
||||||
|
const centerHit = document.createElementNS(svgNS, "circle");
|
||||||
|
centerHit.setAttribute("class", "cube-center-hit");
|
||||||
|
centerHit.setAttribute("cx", String(CUBE_VIEW_CENTER.x));
|
||||||
|
centerHit.setAttribute("cy", String(CUBE_VIEW_CENTER.y));
|
||||||
|
centerHit.setAttribute("r", "18");
|
||||||
|
centerMarker.appendChild(centerHit);
|
||||||
|
|
||||||
|
if (state.markerDisplayMode === "tarot" && centerCardUrl) {
|
||||||
|
const cardW = 24;
|
||||||
|
const cardH = 36;
|
||||||
|
const centerTarotCard = getCenterTarotCard(center);
|
||||||
|
const centerImg = document.createElementNS(svgNS, "image");
|
||||||
|
centerImg.setAttribute("class", "cube-tarot-image cube-center-card");
|
||||||
|
centerImg.setAttribute("href", centerCardUrl);
|
||||||
|
centerImg.setAttribute("x", String((CUBE_VIEW_CENTER.x - cardW / 2).toFixed(2)));
|
||||||
|
centerImg.setAttribute("y", String((CUBE_VIEW_CENTER.y - cardH / 2).toFixed(2)));
|
||||||
|
centerImg.setAttribute("width", String(cardW));
|
||||||
|
centerImg.setAttribute("height", String(cardH));
|
||||||
|
centerImg.setAttribute("role", "button");
|
||||||
|
centerImg.setAttribute("tabindex", "0");
|
||||||
|
centerImg.setAttribute("aria-label", `Open ${centerTarotCard || "Primal Point"} card image`);
|
||||||
|
centerImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||||
|
centerImg.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
state.selectedNodeType = "center";
|
||||||
|
state.selectedConnectorId = null;
|
||||||
|
render(getElements());
|
||||||
|
openTarotCardLightbox(centerTarotCard, centerCardUrl, "Primal Point");
|
||||||
|
});
|
||||||
|
centerImg.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
state.selectedNodeType = "center";
|
||||||
|
state.selectedConnectorId = null;
|
||||||
|
render(getElements());
|
||||||
|
openTarotCardLightbox(centerTarotCard, centerCardUrl, "Primal Point");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
centerMarker.appendChild(centerImg);
|
||||||
|
} else {
|
||||||
|
const centerText = document.createElementNS(svgNS, "text");
|
||||||
|
centerText.setAttribute(
|
||||||
|
"class",
|
||||||
|
`cube-center-symbol${centerActive ? " is-active" : ""}${centerLetter ? "" : " is-missing"}`
|
||||||
|
);
|
||||||
|
centerText.setAttribute("x", String(CUBE_VIEW_CENTER.x));
|
||||||
|
centerText.setAttribute("y", String(CUBE_VIEW_CENTER.y));
|
||||||
|
centerText.setAttribute("text-anchor", "middle");
|
||||||
|
centerText.setAttribute("dominant-baseline", "middle");
|
||||||
|
centerText.setAttribute("pointer-events", "none");
|
||||||
|
centerText.textContent = centerLetter || "!";
|
||||||
|
centerMarker.appendChild(centerText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectCenter = () => {
|
||||||
|
state.selectedNodeType = "center";
|
||||||
|
state.selectedConnectorId = null;
|
||||||
|
render(getElements());
|
||||||
|
};
|
||||||
|
|
||||||
|
centerMarker.addEventListener("click", selectCenter);
|
||||||
|
centerMarker.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
selectCenter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.appendChild(centerMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.markerDisplayMode === "tarot") {
|
||||||
|
Array.from(svg.querySelectorAll("g.cube-direction")).forEach((group) => {
|
||||||
|
svg.appendChild(group);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state.showConnectorLines) {
|
||||||
|
Array.from(svg.querySelectorAll("g.cube-connector")).forEach((group) => {
|
||||||
|
svg.appendChild(group);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
containerEl.replaceChildren(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.CubeChassisUi = { renderFaceSvg };
|
||||||
|
})();
|
||||||
331
app/ui-cube-math.js
Normal file
331
app/ui-cube-math.js
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function createCubeMathHelpers(dependencies) {
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
CUBE_VERTICES,
|
||||||
|
FACE_GEOMETRY,
|
||||||
|
EDGE_GEOMETRY,
|
||||||
|
EDGE_GEOMETRY_KEYS,
|
||||||
|
CUBE_VIEW_CENTER,
|
||||||
|
WALL_FRONT_ROTATIONS,
|
||||||
|
LOCAL_DIRECTION_VIEW_MAP,
|
||||||
|
normalizeId,
|
||||||
|
normalizeLetterKey,
|
||||||
|
normalizeEdgeId,
|
||||||
|
formatDirectionName,
|
||||||
|
getEdgesForWall,
|
||||||
|
getEdgePathEntry,
|
||||||
|
getEdgeLetterId,
|
||||||
|
getCubeCenterData
|
||||||
|
} = dependencies || {};
|
||||||
|
|
||||||
|
function normalizeAngle(angle) {
|
||||||
|
let next = angle;
|
||||||
|
while (next > 180) {
|
||||||
|
next -= 360;
|
||||||
|
}
|
||||||
|
while (next <= -180) {
|
||||||
|
next += 360;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRotation(nextX, nextY) {
|
||||||
|
state.rotationX = normalizeAngle(nextX);
|
||||||
|
state.rotationY = normalizeAngle(nextY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapRotationToWall(wallId) {
|
||||||
|
const target = WALL_FRONT_ROTATIONS[normalizeId(wallId)];
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRotation(target.x, target.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function facePoint(quad, u, v) {
|
||||||
|
const weight0 = ((1 - u) * (1 - v)) / 4;
|
||||||
|
const weight1 = ((1 + u) * (1 - v)) / 4;
|
||||||
|
const weight2 = ((1 + u) * (1 + v)) / 4;
|
||||||
|
const weight3 = ((1 - u) * (1 + v)) / 4;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: quad[0].x * weight0 + quad[1].x * weight1 + quad[2].x * weight2 + quad[3].x * weight3,
|
||||||
|
y: quad[0].y * weight0 + quad[1].y * weight1 + quad[2].y * weight2 + quad[3].y * weight3
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectVerticesForRotation(rotationX, rotationY) {
|
||||||
|
const yaw = (rotationY * Math.PI) / 180;
|
||||||
|
const pitch = (rotationX * Math.PI) / 180;
|
||||||
|
|
||||||
|
const cosY = Math.cos(yaw);
|
||||||
|
const sinY = Math.sin(yaw);
|
||||||
|
const cosX = Math.cos(pitch);
|
||||||
|
const sinX = Math.sin(pitch);
|
||||||
|
|
||||||
|
const centerX = CUBE_VIEW_CENTER.x;
|
||||||
|
const centerY = CUBE_VIEW_CENTER.y;
|
||||||
|
const scale = 54;
|
||||||
|
const camera = 4.6;
|
||||||
|
|
||||||
|
return CUBE_VERTICES.map(([x, y, z]) => {
|
||||||
|
const x1 = x * cosY + z * sinY;
|
||||||
|
const z1 = -x * sinY + z * cosY;
|
||||||
|
|
||||||
|
const y2 = y * cosX - z1 * sinX;
|
||||||
|
const z2 = y * sinX + z1 * cosX;
|
||||||
|
|
||||||
|
const perspective = camera / (camera - z2);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: centerX + x1 * scale * perspective,
|
||||||
|
y: centerY + y2 * scale * perspective,
|
||||||
|
z: z2
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectVertices() {
|
||||||
|
return projectVerticesForRotation(state.rotationX, state.rotationY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEdgeGeometryById(edgeId) {
|
||||||
|
const canonicalId = normalizeEdgeId(edgeId);
|
||||||
|
const geometryIndex = EDGE_GEOMETRY_KEYS.indexOf(canonicalId);
|
||||||
|
if (geometryIndex < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return EDGE_GEOMETRY[geometryIndex] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWallEdgeDirections(wallOrWallId) {
|
||||||
|
const wallId = normalizeId(typeof wallOrWallId === "string" ? wallOrWallId : wallOrWallId?.id);
|
||||||
|
const faceIndices = FACE_GEOMETRY[wallId];
|
||||||
|
if (!Array.isArray(faceIndices) || faceIndices.length !== 4) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontRotation = WALL_FRONT_ROTATIONS[wallId] || {
|
||||||
|
x: state.rotationX,
|
||||||
|
y: state.rotationY
|
||||||
|
};
|
||||||
|
const projectedVertices = projectVerticesForRotation(frontRotation.x, frontRotation.y);
|
||||||
|
const quad = faceIndices.map((index) => projectedVertices[index]);
|
||||||
|
const center = facePoint(quad, 0, 0);
|
||||||
|
const directionsByEdgeId = new Map();
|
||||||
|
|
||||||
|
getEdgesForWall(wallId).forEach((edge) => {
|
||||||
|
const geometry = getEdgeGeometryById(edge?.id);
|
||||||
|
if (!geometry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [fromIndex, toIndex] = geometry;
|
||||||
|
const from = projectedVertices[fromIndex];
|
||||||
|
const to = projectedVertices[toIndex];
|
||||||
|
if (!from || !to) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const midpointX = (from.x + to.x) / 2;
|
||||||
|
const midpointY = (from.y + to.y) / 2;
|
||||||
|
const dx = midpointX - center.x;
|
||||||
|
const dy = midpointY - center.y;
|
||||||
|
|
||||||
|
const directionByPosition = Math.abs(dx) >= Math.abs(dy)
|
||||||
|
? (dx >= 0 ? "east" : "west")
|
||||||
|
: (dy >= 0 ? "south" : "north");
|
||||||
|
const direction = LOCAL_DIRECTION_VIEW_MAP[directionByPosition] || directionByPosition;
|
||||||
|
|
||||||
|
directionsByEdgeId.set(normalizeEdgeId(edge?.id), direction);
|
||||||
|
});
|
||||||
|
|
||||||
|
return directionsByEdgeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEdgeDirectionForWall(wallId, edgeId) {
|
||||||
|
const wallKey = normalizeId(wallId);
|
||||||
|
const edgeKey = normalizeEdgeId(edgeId);
|
||||||
|
if (!wallKey || !edgeKey) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const directions = getWallEdgeDirections(wallKey);
|
||||||
|
return directions.get(edgeKey) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEdgeDirectionLabelForWall(wallId, edgeId) {
|
||||||
|
return formatDirectionName(getEdgeDirectionForWall(wallId, edgeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHebrewLetterSymbol(hebrewLetterId) {
|
||||||
|
const id = normalizeLetterKey(hebrewLetterId);
|
||||||
|
if (!id || !state.hebrewLetters) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = state.hebrewLetters[id];
|
||||||
|
if (!entry || typeof entry !== "object") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbol = String(
|
||||||
|
entry?.letter?.he || entry?.he || entry?.glyph || entry?.symbol || ""
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
return symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHebrewLetterName(hebrewLetterId) {
|
||||||
|
const id = normalizeLetterKey(hebrewLetterId);
|
||||||
|
if (!id || !state.hebrewLetters) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = state.hebrewLetters[id];
|
||||||
|
if (!entry || typeof entry !== "object") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = String(entry?.letter?.name || entry?.name || "").trim();
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAstrologySymbol(type, name) {
|
||||||
|
const normalizedType = normalizeId(type);
|
||||||
|
const normalizedName = normalizeId(name);
|
||||||
|
|
||||||
|
const planetSymbols = {
|
||||||
|
mercury: "☿︎",
|
||||||
|
venus: "♀︎",
|
||||||
|
mars: "♂︎",
|
||||||
|
jupiter: "♃︎",
|
||||||
|
saturn: "♄︎",
|
||||||
|
sol: "☉︎",
|
||||||
|
sun: "☉︎",
|
||||||
|
luna: "☾︎",
|
||||||
|
moon: "☾︎",
|
||||||
|
earth: "⊕",
|
||||||
|
uranus: "♅︎",
|
||||||
|
neptune: "♆︎",
|
||||||
|
pluto: "♇︎"
|
||||||
|
};
|
||||||
|
|
||||||
|
const zodiacSymbols = {
|
||||||
|
aries: "♈︎",
|
||||||
|
taurus: "♉︎",
|
||||||
|
gemini: "♊︎",
|
||||||
|
cancer: "♋︎",
|
||||||
|
leo: "♌︎",
|
||||||
|
virgo: "♍︎",
|
||||||
|
libra: "♎︎",
|
||||||
|
scorpio: "♏︎",
|
||||||
|
sagittarius: "♐︎",
|
||||||
|
capricorn: "♑︎",
|
||||||
|
aquarius: "♒︎",
|
||||||
|
pisces: "♓︎"
|
||||||
|
};
|
||||||
|
|
||||||
|
const elementSymbols = {
|
||||||
|
fire: "🜂",
|
||||||
|
water: "🜄",
|
||||||
|
air: "🜁",
|
||||||
|
earth: "🜃",
|
||||||
|
spirit: "🜀"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (normalizedType === "planet") {
|
||||||
|
return planetSymbols[normalizedName] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedType === "zodiac") {
|
||||||
|
return zodiacSymbols[normalizedName] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedType === "element") {
|
||||||
|
return elementSymbols[normalizedName] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCenterLetterId(center = null) {
|
||||||
|
const entry = center || getCubeCenterData();
|
||||||
|
return normalizeLetterKey(entry?.hebrewLetterId || entry?.associations?.hebrewLetterId || entry?.letter);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCenterLetterSymbol(center = null) {
|
||||||
|
const centerLetterId = getCenterLetterId(center);
|
||||||
|
if (!centerLetterId) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return getHebrewLetterSymbol(centerLetterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEdgeAstrologySymbol(edge) {
|
||||||
|
const pathEntry = getEdgePathEntry(edge);
|
||||||
|
const astrology = pathEntry?.astrology || {};
|
||||||
|
return getAstrologySymbol(astrology.type, astrology.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEdgeLetter(edge) {
|
||||||
|
const hebrewLetterId = getEdgeLetterId(edge);
|
||||||
|
if (!hebrewLetterId) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return getHebrewLetterSymbol(hebrewLetterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEdgeMarkerDisplay(edge) {
|
||||||
|
const letter = getEdgeLetter(edge);
|
||||||
|
const astro = getEdgeAstrologySymbol(edge);
|
||||||
|
|
||||||
|
if (state.markerDisplayMode === "letter") {
|
||||||
|
return letter
|
||||||
|
? { text: letter, isMissing: false }
|
||||||
|
: { text: "!", isMissing: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.markerDisplayMode === "astro") {
|
||||||
|
return astro
|
||||||
|
? { text: astro, isMissing: false }
|
||||||
|
: { text: "!", isMissing: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (letter && astro) {
|
||||||
|
return { text: `${letter} ${astro}`, isMissing: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: "!", isMissing: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
normalizeAngle,
|
||||||
|
setRotation,
|
||||||
|
snapRotationToWall,
|
||||||
|
facePoint,
|
||||||
|
projectVerticesForRotation,
|
||||||
|
projectVertices,
|
||||||
|
getEdgeGeometryById,
|
||||||
|
getWallEdgeDirections,
|
||||||
|
getEdgeDirectionForWall,
|
||||||
|
getEdgeDirectionLabelForWall,
|
||||||
|
getHebrewLetterSymbol,
|
||||||
|
getHebrewLetterName,
|
||||||
|
getAstrologySymbol,
|
||||||
|
getCenterLetterId,
|
||||||
|
getCenterLetterSymbol,
|
||||||
|
getEdgeAstrologySymbol,
|
||||||
|
getEdgeMarkerDisplay,
|
||||||
|
getEdgeLetter
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.CubeMathHelpers = {
|
||||||
|
createCubeMathHelpers
|
||||||
|
};
|
||||||
|
})();
|
||||||
811
app/ui-cube.js
811
app/ui-cube.js
@@ -120,6 +120,8 @@
|
|||||||
below: { x: 90, y: 0 }
|
below: { x: 90, y: 0 }
|
||||||
};
|
};
|
||||||
const cubeDetailUi = window.CubeDetailUi || {};
|
const cubeDetailUi = window.CubeDetailUi || {};
|
||||||
|
const cubeChassisUi = window.CubeChassisUi || {};
|
||||||
|
const cubeMathHelpers = window.CubeMathHelpers || {};
|
||||||
|
|
||||||
function getElements() {
|
function getElements() {
|
||||||
return {
|
return {
|
||||||
@@ -250,144 +252,48 @@
|
|||||||
return Number.isFinite(numeric) ? numeric : null;
|
return Number.isFinite(numeric) ? numeric : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof cubeMathHelpers.createCubeMathHelpers !== "function") {
|
||||||
|
throw new Error("CubeMathHelpers.createCubeMathHelpers is unavailable. Ensure app/ui-cube-math.js loads before app/ui-cube.js.");
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeAngle(angle) {
|
function normalizeAngle(angle) {
|
||||||
let next = angle;
|
return cubeMathUi.normalizeAngle(angle);
|
||||||
while (next > 180) {
|
|
||||||
next -= 360;
|
|
||||||
}
|
|
||||||
while (next <= -180) {
|
|
||||||
next += 360;
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRotation(nextX, nextY) {
|
function setRotation(nextX, nextY) {
|
||||||
state.rotationX = normalizeAngle(nextX);
|
cubeMathUi.setRotation(nextX, nextY);
|
||||||
state.rotationY = normalizeAngle(nextY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapRotationToWall(wallId) {
|
function snapRotationToWall(wallId) {
|
||||||
const target = WALL_FRONT_ROTATIONS[normalizeId(wallId)];
|
cubeMathUi.snapRotationToWall(wallId);
|
||||||
if (!target) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setRotation(target.x, target.y);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function facePoint(quad, u, v) {
|
function facePoint(quad, u, v) {
|
||||||
const weight0 = ((1 - u) * (1 - v)) / 4;
|
return cubeMathUi.facePoint(quad, u, v);
|
||||||
const weight1 = ((1 + u) * (1 - v)) / 4;
|
|
||||||
const weight2 = ((1 + u) * (1 + v)) / 4;
|
|
||||||
const weight3 = ((1 - u) * (1 + v)) / 4;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: quad[0].x * weight0 + quad[1].x * weight1 + quad[2].x * weight2 + quad[3].x * weight3,
|
|
||||||
y: quad[0].y * weight0 + quad[1].y * weight1 + quad[2].y * weight2 + quad[3].y * weight3
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectVerticesForRotation(rotationX, rotationY) {
|
function projectVerticesForRotation(rotationX, rotationY) {
|
||||||
const yaw = (rotationY * Math.PI) / 180;
|
return cubeMathUi.projectVerticesForRotation(rotationX, rotationY);
|
||||||
const pitch = (rotationX * Math.PI) / 180;
|
|
||||||
|
|
||||||
const cosY = Math.cos(yaw);
|
|
||||||
const sinY = Math.sin(yaw);
|
|
||||||
const cosX = Math.cos(pitch);
|
|
||||||
const sinX = Math.sin(pitch);
|
|
||||||
|
|
||||||
const centerX = CUBE_VIEW_CENTER.x;
|
|
||||||
const centerY = CUBE_VIEW_CENTER.y;
|
|
||||||
const scale = 54;
|
|
||||||
const camera = 4.6;
|
|
||||||
|
|
||||||
return CUBE_VERTICES.map(([x, y, z]) => {
|
|
||||||
const x1 = x * cosY + z * sinY;
|
|
||||||
const z1 = -x * sinY + z * cosY;
|
|
||||||
|
|
||||||
const y2 = y * cosX - z1 * sinX;
|
|
||||||
const z2 = y * sinX + z1 * cosX;
|
|
||||||
|
|
||||||
const perspective = camera / (camera - z2);
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: centerX + x1 * scale * perspective,
|
|
||||||
y: centerY + y2 * scale * perspective,
|
|
||||||
z: z2
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectVertices() {
|
function projectVertices() {
|
||||||
return projectVerticesForRotation(state.rotationX, state.rotationY);
|
return cubeMathUi.projectVertices();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEdgeGeometryById(edgeId) {
|
function getEdgeGeometryById(edgeId) {
|
||||||
const canonicalId = normalizeEdgeId(edgeId);
|
return cubeMathUi.getEdgeGeometryById(edgeId);
|
||||||
const geometryIndex = EDGE_GEOMETRY_KEYS.indexOf(canonicalId);
|
|
||||||
if (geometryIndex < 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return EDGE_GEOMETRY[geometryIndex] || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWallEdgeDirections(wallOrWallId) {
|
function getWallEdgeDirections(wallOrWallId) {
|
||||||
const wallId = normalizeId(typeof wallOrWallId === "string" ? wallOrWallId : wallOrWallId?.id);
|
return cubeMathUi.getWallEdgeDirections(wallOrWallId);
|
||||||
const faceIndices = FACE_GEOMETRY[wallId];
|
|
||||||
if (!Array.isArray(faceIndices) || faceIndices.length !== 4) {
|
|
||||||
return new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
const frontRotation = WALL_FRONT_ROTATIONS[wallId] || {
|
|
||||||
x: state.rotationX,
|
|
||||||
y: state.rotationY
|
|
||||||
};
|
|
||||||
const projectedVertices = projectVerticesForRotation(frontRotation.x, frontRotation.y);
|
|
||||||
const quad = faceIndices.map((index) => projectedVertices[index]);
|
|
||||||
const center = facePoint(quad, 0, 0);
|
|
||||||
const directionsByEdgeId = new Map();
|
|
||||||
|
|
||||||
getEdgesForWall(wallId).forEach((edge) => {
|
|
||||||
const geometry = getEdgeGeometryById(edge?.id);
|
|
||||||
if (!geometry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [fromIndex, toIndex] = geometry;
|
|
||||||
const from = projectedVertices[fromIndex];
|
|
||||||
const to = projectedVertices[toIndex];
|
|
||||||
if (!from || !to) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const midpointX = (from.x + to.x) / 2;
|
|
||||||
const midpointY = (from.y + to.y) / 2;
|
|
||||||
const dx = midpointX - center.x;
|
|
||||||
const dy = midpointY - center.y;
|
|
||||||
|
|
||||||
const directionByPosition = Math.abs(dx) >= Math.abs(dy)
|
|
||||||
? (dx >= 0 ? "east" : "west")
|
|
||||||
: (dy >= 0 ? "south" : "north");
|
|
||||||
const direction = LOCAL_DIRECTION_VIEW_MAP[directionByPosition] || directionByPosition;
|
|
||||||
|
|
||||||
directionsByEdgeId.set(normalizeEdgeId(edge?.id), direction);
|
|
||||||
});
|
|
||||||
|
|
||||||
return directionsByEdgeId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEdgeDirectionForWall(wallId, edgeId) {
|
function getEdgeDirectionForWall(wallId, edgeId) {
|
||||||
const wallKey = normalizeId(wallId);
|
return cubeMathUi.getEdgeDirectionForWall(wallId, edgeId);
|
||||||
const edgeKey = normalizeEdgeId(edgeId);
|
|
||||||
if (!wallKey || !edgeKey) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const directions = getWallEdgeDirections(wallKey);
|
|
||||||
return directions.get(edgeKey) || "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEdgeDirectionLabelForWall(wallId, edgeId) {
|
function getEdgeDirectionLabelForWall(wallId, edgeId) {
|
||||||
return formatDirectionName(getEdgeDirectionForWall(wallId, edgeId));
|
return cubeMathUi.getEdgeDirectionLabelForWall(wallId, edgeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindRotationControls(elements) {
|
function bindRotationControls(elements) {
|
||||||
@@ -444,94 +350,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getHebrewLetterSymbol(hebrewLetterId) {
|
function getHebrewLetterSymbol(hebrewLetterId) {
|
||||||
const id = normalizeLetterKey(hebrewLetterId);
|
return cubeMathUi.getHebrewLetterSymbol(hebrewLetterId);
|
||||||
if (!id || !state.hebrewLetters) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = state.hebrewLetters[id];
|
|
||||||
if (!entry || typeof entry !== "object") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const symbol = String(
|
|
||||||
entry?.letter?.he || entry?.he || entry?.glyph || entry?.symbol || ""
|
|
||||||
).trim();
|
|
||||||
|
|
||||||
return symbol;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHebrewLetterName(hebrewLetterId) {
|
function getHebrewLetterName(hebrewLetterId) {
|
||||||
const id = normalizeLetterKey(hebrewLetterId);
|
return cubeMathUi.getHebrewLetterName(hebrewLetterId);
|
||||||
if (!id || !state.hebrewLetters) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = state.hebrewLetters[id];
|
|
||||||
if (!entry || typeof entry !== "object") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = String(entry?.letter?.name || entry?.name || "").trim();
|
|
||||||
return name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAstrologySymbol(type, name) {
|
function getAstrologySymbol(type, name) {
|
||||||
const normalizedType = normalizeId(type);
|
return cubeMathUi.getAstrologySymbol(type, name);
|
||||||
const normalizedName = normalizeId(name);
|
|
||||||
|
|
||||||
const planetSymbols = {
|
|
||||||
mercury: "☿︎",
|
|
||||||
venus: "♀︎",
|
|
||||||
mars: "♂︎",
|
|
||||||
jupiter: "♃︎",
|
|
||||||
saturn: "♄︎",
|
|
||||||
sol: "☉︎",
|
|
||||||
sun: "☉︎",
|
|
||||||
luna: "☾︎",
|
|
||||||
moon: "☾︎",
|
|
||||||
earth: "⊕",
|
|
||||||
uranus: "♅︎",
|
|
||||||
neptune: "♆︎",
|
|
||||||
pluto: "♇︎"
|
|
||||||
};
|
|
||||||
|
|
||||||
const zodiacSymbols = {
|
|
||||||
aries: "♈︎",
|
|
||||||
taurus: "♉︎",
|
|
||||||
gemini: "♊︎",
|
|
||||||
cancer: "♋︎",
|
|
||||||
leo: "♌︎",
|
|
||||||
virgo: "♍︎",
|
|
||||||
libra: "♎︎",
|
|
||||||
scorpio: "♏︎",
|
|
||||||
sagittarius: "♐︎",
|
|
||||||
capricorn: "♑︎",
|
|
||||||
aquarius: "♒︎",
|
|
||||||
pisces: "♓︎"
|
|
||||||
};
|
|
||||||
|
|
||||||
const elementSymbols = {
|
|
||||||
fire: "🜂",
|
|
||||||
water: "🜄",
|
|
||||||
air: "🜁",
|
|
||||||
earth: "🜃",
|
|
||||||
spirit: "🜀"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (normalizedType === "planet") {
|
|
||||||
return planetSymbols[normalizedName] || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalizedType === "zodiac") {
|
|
||||||
return zodiacSymbols[normalizedName] || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalizedType === "element") {
|
|
||||||
return elementSymbols[normalizedName] || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEdgeLetterId(edge) {
|
function getEdgeLetterId(edge) {
|
||||||
@@ -556,16 +383,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCenterLetterId(center = null) {
|
function getCenterLetterId(center = null) {
|
||||||
const entry = center || getCubeCenterData();
|
return cubeMathUi.getCenterLetterId(center);
|
||||||
return normalizeLetterKey(entry?.hebrewLetterId || entry?.associations?.hebrewLetterId || entry?.letter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCenterLetterSymbol(center = null) {
|
function getCenterLetterSymbol(center = null) {
|
||||||
const centerLetterId = getCenterLetterId(center);
|
return cubeMathUi.getCenterLetterSymbol(center);
|
||||||
if (!centerLetterId) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return getHebrewLetterSymbol(centerLetterId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConnectorById(connectorId) {
|
function getConnectorById(connectorId) {
|
||||||
@@ -591,42 +413,35 @@
|
|||||||
return state.kabbalahPathsByLetterId.get(hebrewLetterId) || null;
|
return state.kabbalahPathsByLetterId.get(hebrewLetterId) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cubeMathUi = cubeMathHelpers.createCubeMathHelpers({
|
||||||
|
state,
|
||||||
|
CUBE_VERTICES,
|
||||||
|
FACE_GEOMETRY,
|
||||||
|
EDGE_GEOMETRY,
|
||||||
|
EDGE_GEOMETRY_KEYS,
|
||||||
|
CUBE_VIEW_CENTER,
|
||||||
|
WALL_FRONT_ROTATIONS,
|
||||||
|
LOCAL_DIRECTION_VIEW_MAP,
|
||||||
|
normalizeId,
|
||||||
|
normalizeLetterKey,
|
||||||
|
normalizeEdgeId,
|
||||||
|
formatDirectionName,
|
||||||
|
getEdgesForWall,
|
||||||
|
getEdgePathEntry,
|
||||||
|
getEdgeLetterId,
|
||||||
|
getCubeCenterData
|
||||||
|
});
|
||||||
|
|
||||||
function getEdgeAstrologySymbol(edge) {
|
function getEdgeAstrologySymbol(edge) {
|
||||||
const pathEntry = getEdgePathEntry(edge);
|
return cubeMathUi.getEdgeAstrologySymbol(edge);
|
||||||
const astrology = pathEntry?.astrology || {};
|
|
||||||
return getAstrologySymbol(astrology.type, astrology.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEdgeMarkerDisplay(edge) {
|
function getEdgeMarkerDisplay(edge) {
|
||||||
const letter = getEdgeLetter(edge);
|
return cubeMathUi.getEdgeMarkerDisplay(edge);
|
||||||
const astro = getEdgeAstrologySymbol(edge);
|
|
||||||
|
|
||||||
if (state.markerDisplayMode === "letter") {
|
|
||||||
return letter
|
|
||||||
? { text: letter, isMissing: false }
|
|
||||||
: { text: "!", isMissing: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.markerDisplayMode === "astro") {
|
|
||||||
return astro
|
|
||||||
? { text: astro, isMissing: false }
|
|
||||||
: { text: "!", isMissing: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (letter && astro) {
|
|
||||||
return { text: `${letter} ${astro}`, isMissing: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { text: "!", isMissing: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEdgeLetter(edge) {
|
function getEdgeLetter(edge) {
|
||||||
const hebrewLetterId = getEdgeLetterId(edge);
|
return cubeMathUi.getEdgeLetter(edge);
|
||||||
if (!hebrewLetterId) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return getHebrewLetterSymbol(hebrewLetterId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWallTarotCard(wall) {
|
function getWallTarotCard(wall) {
|
||||||
@@ -700,513 +515,47 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderFaceSvg(containerEl, walls) {
|
function renderFaceSvg(containerEl, walls) {
|
||||||
if (!containerEl) {
|
if (typeof cubeChassisUi.renderFaceSvg !== "function") {
|
||||||
|
if (containerEl) {
|
||||||
|
containerEl.replaceChildren();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const svgNS = "http://www.w3.org/2000/svg";
|
cubeChassisUi.renderFaceSvg({
|
||||||
const svg = document.createElementNS(svgNS, "svg");
|
state,
|
||||||
svg.setAttribute("viewBox", "0 0 240 220");
|
containerEl,
|
||||||
svg.setAttribute("width", "100%");
|
walls,
|
||||||
svg.setAttribute("class", "cube-svg");
|
normalizeId,
|
||||||
svg.setAttribute("role", "img");
|
projectVertices,
|
||||||
svg.setAttribute("aria-label", "Cube of Space interactive chassis");
|
FACE_GEOMETRY,
|
||||||
|
facePoint,
|
||||||
const wallById = new Map(walls.map((wall) => [normalizeId(wall?.id), wall]));
|
normalizeEdgeId,
|
||||||
const projectedVertices = projectVertices();
|
getEdges,
|
||||||
const faces = Object.entries(FACE_GEOMETRY)
|
getEdgesForWall,
|
||||||
.map(([wallId, indices]) => {
|
EDGE_GEOMETRY,
|
||||||
const wall = wallById.get(wallId);
|
EDGE_GEOMETRY_KEYS,
|
||||||
if (!wall) {
|
formatEdgeName,
|
||||||
return null;
|
getEdgeWalls,
|
||||||
}
|
getElements,
|
||||||
|
render,
|
||||||
const quad = indices.map((index) => projectedVertices[index]);
|
snapRotationToWall,
|
||||||
const avgDepth = quad.reduce((sum, point) => sum + point.z, 0) / quad.length;
|
getWallFaceLetter,
|
||||||
|
getWallTarotCard,
|
||||||
return {
|
resolveCardImageUrl,
|
||||||
wallId,
|
openTarotCardLightbox,
|
||||||
wall,
|
MOTHER_CONNECTORS,
|
||||||
quad,
|
formatDirectionName,
|
||||||
depth: avgDepth,
|
getConnectorTarotCard,
|
||||||
pointsText: quad.map((point) => `${point.x.toFixed(2)},${point.y.toFixed(2)}`).join(" ")
|
getHebrewLetterSymbol,
|
||||||
};
|
toDisplayText,
|
||||||
})
|
CUBE_VIEW_CENTER,
|
||||||
.filter(Boolean)
|
getEdgeMarkerDisplay,
|
||||||
.sort((left, right) => left.depth - right.depth);
|
getEdgeTarotCard,
|
||||||
|
getCubeCenterData,
|
||||||
faces.forEach((faceData) => {
|
getCenterTarotCard,
|
||||||
const { wallId, wall, quad, pointsText } = faceData;
|
getCenterLetterSymbol
|
||||||
|
|
||||||
const isActive = wallId === normalizeId(state.selectedWallId);
|
|
||||||
const polygon = document.createElementNS(svgNS, "polygon");
|
|
||||||
polygon.setAttribute("points", pointsText);
|
|
||||||
polygon.setAttribute("class", `cube-face${isActive ? " is-active" : ""}`);
|
|
||||||
polygon.setAttribute("fill", "#000");
|
|
||||||
polygon.setAttribute("fill-opacity", isActive ? "0.78" : "0.62");
|
|
||||||
polygon.setAttribute("stroke", "currentColor");
|
|
||||||
polygon.setAttribute("stroke-opacity", isActive ? "0.92" : "0.68");
|
|
||||||
polygon.setAttribute("stroke-width", isActive ? "2.5" : "1");
|
|
||||||
polygon.setAttribute("data-wall-id", wallId);
|
|
||||||
polygon.setAttribute("role", "button");
|
|
||||||
polygon.setAttribute("tabindex", "0");
|
|
||||||
polygon.setAttribute("aria-label", `Cube wall ${wall?.name || wallId}`);
|
|
||||||
|
|
||||||
const selectWall = () => {
|
|
||||||
state.selectedWallId = wallId;
|
|
||||||
state.selectedEdgeId = normalizeEdgeId(getEdgesForWall(wallId)[0]?.id || getEdges()[0]?.id);
|
|
||||||
state.selectedNodeType = "wall";
|
|
||||||
state.selectedConnectorId = null;
|
|
||||||
snapRotationToWall(wallId);
|
|
||||||
render(getElements());
|
|
||||||
};
|
|
||||||
|
|
||||||
polygon.addEventListener("click", selectWall);
|
|
||||||
polygon.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
selectWall();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
svg.appendChild(polygon);
|
|
||||||
|
|
||||||
const wallFaceLetter = getWallFaceLetter(wall);
|
|
||||||
const faceGlyphAnchor = facePoint(quad, 0, 0);
|
|
||||||
|
|
||||||
if (state.markerDisplayMode === "tarot") {
|
|
||||||
const cardUrl = resolveCardImageUrl(getWallTarotCard(wall));
|
|
||||||
if (cardUrl) {
|
|
||||||
let defs = svg.querySelector("defs");
|
|
||||||
if (!defs) {
|
|
||||||
defs = document.createElementNS(svgNS, "defs");
|
|
||||||
svg.insertBefore(defs, svg.firstChild);
|
|
||||||
}
|
|
||||||
const clipId = `face-clip-${wallId}`;
|
|
||||||
const clipPath = document.createElementNS(svgNS, "clipPath");
|
|
||||||
clipPath.setAttribute("id", clipId);
|
|
||||||
const clipPoly = document.createElementNS(svgNS, "polygon");
|
|
||||||
clipPoly.setAttribute("points", pointsText);
|
|
||||||
clipPath.appendChild(clipPoly);
|
|
||||||
defs.appendChild(clipPath);
|
|
||||||
|
|
||||||
const cardW = 40, cardH = 60;
|
|
||||||
const wallTarotCard = getWallTarotCard(wall);
|
|
||||||
const cardImg = document.createElementNS(svgNS, "image");
|
|
||||||
cardImg.setAttribute("class", "cube-tarot-image cube-face-card");
|
|
||||||
cardImg.setAttribute("href", cardUrl);
|
|
||||||
cardImg.setAttribute("x", String((faceGlyphAnchor.x - cardW / 2).toFixed(2)));
|
|
||||||
cardImg.setAttribute("y", String((faceGlyphAnchor.y - cardH / 2).toFixed(2)));
|
|
||||||
cardImg.setAttribute("width", String(cardW));
|
|
||||||
cardImg.setAttribute("height", String(cardH));
|
|
||||||
cardImg.setAttribute("clip-path", `url(#${clipId})`);
|
|
||||||
cardImg.setAttribute("role", "button");
|
|
||||||
cardImg.setAttribute("tabindex", "0");
|
|
||||||
cardImg.setAttribute("aria-label", `Open ${wallTarotCard || (wall?.name || wallId)} card image`);
|
|
||||||
cardImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
||||||
cardImg.addEventListener("click", (event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
selectWall();
|
|
||||||
openTarotCardLightbox(wallTarotCard, cardUrl, `${wall?.name || wallId} wall tarot card`);
|
|
||||||
});
|
|
||||||
cardImg.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
selectWall();
|
|
||||||
openTarotCardLightbox(wallTarotCard, cardUrl, `${wall?.name || wallId} wall tarot card`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
svg.appendChild(cardImg);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const faceGlyph = document.createElementNS(svgNS, "text");
|
|
||||||
faceGlyph.setAttribute(
|
|
||||||
"class",
|
|
||||||
`cube-face-symbol${isActive ? " is-active" : ""}${wallFaceLetter ? "" : " is-missing"}`
|
|
||||||
);
|
|
||||||
faceGlyph.setAttribute("x", String(faceGlyphAnchor.x));
|
|
||||||
faceGlyph.setAttribute("y", String(faceGlyphAnchor.y));
|
|
||||||
faceGlyph.setAttribute("text-anchor", "middle");
|
|
||||||
faceGlyph.setAttribute("dominant-baseline", "middle");
|
|
||||||
faceGlyph.setAttribute("pointer-events", "none");
|
|
||||||
faceGlyph.textContent = wallFaceLetter || "!";
|
|
||||||
svg.appendChild(faceGlyph);
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelAnchor = facePoint(quad, 0, 0.9);
|
|
||||||
const label = document.createElementNS(svgNS, "text");
|
|
||||||
label.setAttribute("class", `cube-face-label${isActive ? " is-active" : ""}`);
|
|
||||||
label.setAttribute("x", String(labelAnchor.x));
|
|
||||||
label.setAttribute("y", String(labelAnchor.y));
|
|
||||||
label.setAttribute("text-anchor", "middle");
|
|
||||||
label.setAttribute("dominant-baseline", "middle");
|
|
||||||
label.setAttribute("pointer-events", "none");
|
|
||||||
label.textContent = wall?.name || wallId;
|
|
||||||
svg.appendChild(label);
|
|
||||||
});
|
|
||||||
|
|
||||||
const faceCenterByWallId = new Map(
|
|
||||||
faces.map((faceData) => [faceData.wallId, facePoint(faceData.quad, 0, 0)])
|
|
||||||
);
|
|
||||||
|
|
||||||
if (state.showConnectorLines) {
|
|
||||||
MOTHER_CONNECTORS.forEach((connector, connectorIndex) => {
|
|
||||||
const fromWallId = normalizeId(connector?.fromWallId);
|
|
||||||
const toWallId = normalizeId(connector?.toWallId);
|
|
||||||
const from = faceCenterByWallId.get(fromWallId);
|
|
||||||
const to = faceCenterByWallId.get(toWallId);
|
|
||||||
if (!from || !to) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectorId = normalizeId(connector?.id);
|
|
||||||
const isActive = state.selectedNodeType === "connector"
|
|
||||||
&& normalizeId(state.selectedConnectorId) === connectorId;
|
|
||||||
const connectorLetter = getHebrewLetterSymbol(connector?.hebrewLetterId);
|
|
||||||
const connectorCardUrl = state.markerDisplayMode === "tarot"
|
|
||||||
? resolveCardImageUrl(getConnectorTarotCard(connector))
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const group = document.createElementNS(svgNS, "g");
|
|
||||||
group.setAttribute("class", `cube-connector${isActive ? " is-active" : ""}`);
|
|
||||||
group.setAttribute("role", "button");
|
|
||||||
group.setAttribute("tabindex", "0");
|
|
||||||
group.setAttribute(
|
|
||||||
"aria-label",
|
|
||||||
`Mother connector ${formatDirectionName(fromWallId)} to ${formatDirectionName(toWallId)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const connectorLine = document.createElementNS(svgNS, "line");
|
|
||||||
connectorLine.setAttribute("class", `cube-connector-line${isActive ? " is-active" : ""}`);
|
|
||||||
connectorLine.setAttribute("x1", from.x.toFixed(2));
|
|
||||||
connectorLine.setAttribute("y1", from.y.toFixed(2));
|
|
||||||
connectorLine.setAttribute("x2", to.x.toFixed(2));
|
|
||||||
connectorLine.setAttribute("y2", to.y.toFixed(2));
|
|
||||||
group.appendChild(connectorLine);
|
|
||||||
|
|
||||||
const connectorHit = document.createElementNS(svgNS, "line");
|
|
||||||
connectorHit.setAttribute("class", "cube-connector-hit");
|
|
||||||
connectorHit.setAttribute("x1", from.x.toFixed(2));
|
|
||||||
connectorHit.setAttribute("y1", from.y.toFixed(2));
|
|
||||||
connectorHit.setAttribute("x2", to.x.toFixed(2));
|
|
||||||
connectorHit.setAttribute("y2", to.y.toFixed(2));
|
|
||||||
group.appendChild(connectorHit);
|
|
||||||
|
|
||||||
const dx = to.x - from.x;
|
|
||||||
const dy = to.y - from.y;
|
|
||||||
const length = Math.hypot(dx, dy) || 1;
|
|
||||||
const perpX = -dy / length;
|
|
||||||
const perpY = dx / length;
|
|
||||||
const shift = (connectorIndex - 1) * 12;
|
|
||||||
const labelX = ((from.x + to.x) / 2) + (perpX * shift);
|
|
||||||
const labelY = ((from.y + to.y) / 2) + (perpY * shift);
|
|
||||||
|
|
||||||
const selectConnector = () => {
|
|
||||||
state.selectedNodeType = "connector";
|
|
||||||
state.selectedConnectorId = connectorId;
|
|
||||||
render(getElements());
|
|
||||||
};
|
|
||||||
|
|
||||||
if (state.markerDisplayMode === "tarot" && connectorCardUrl) {
|
|
||||||
const cardW = 18;
|
|
||||||
const cardH = 27;
|
|
||||||
const connectorTarotCard = getConnectorTarotCard(connector);
|
|
||||||
const connectorImg = document.createElementNS(svgNS, "image");
|
|
||||||
connectorImg.setAttribute("class", "cube-tarot-image cube-connector-card");
|
|
||||||
connectorImg.setAttribute("href", connectorCardUrl);
|
|
||||||
connectorImg.setAttribute("x", String((labelX - cardW / 2).toFixed(2)));
|
|
||||||
connectorImg.setAttribute("y", String((labelY - cardH / 2).toFixed(2)));
|
|
||||||
connectorImg.setAttribute("width", String(cardW));
|
|
||||||
connectorImg.setAttribute("height", String(cardH));
|
|
||||||
connectorImg.setAttribute("role", "button");
|
|
||||||
connectorImg.setAttribute("tabindex", "0");
|
|
||||||
connectorImg.setAttribute("aria-label", `Open ${connectorTarotCard || connector?.name || "connector"} card image`);
|
|
||||||
connectorImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
||||||
connectorImg.addEventListener("click", (event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
selectConnector();
|
|
||||||
openTarotCardLightbox(connectorTarotCard, connectorCardUrl, connector?.name || "Mother connector");
|
|
||||||
});
|
|
||||||
connectorImg.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
selectConnector();
|
|
||||||
openTarotCardLightbox(connectorTarotCard, connectorCardUrl, connector?.name || "Mother connector");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
group.appendChild(connectorImg);
|
|
||||||
} else {
|
|
||||||
const connectorText = document.createElementNS(svgNS, "text");
|
|
||||||
connectorText.setAttribute(
|
|
||||||
"class",
|
|
||||||
`cube-connector-symbol${isActive ? " is-active" : ""}${connectorLetter ? "" : " is-missing"}`
|
|
||||||
);
|
|
||||||
connectorText.setAttribute("x", String(labelX));
|
|
||||||
connectorText.setAttribute("y", String(labelY));
|
|
||||||
connectorText.setAttribute("text-anchor", "middle");
|
|
||||||
connectorText.setAttribute("dominant-baseline", "middle");
|
|
||||||
connectorText.setAttribute("pointer-events", "none");
|
|
||||||
connectorText.textContent = connectorLetter || "!";
|
|
||||||
group.appendChild(connectorText);
|
|
||||||
}
|
|
||||||
|
|
||||||
group.addEventListener("click", selectConnector);
|
|
||||||
group.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
selectConnector();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
svg.appendChild(group);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const edgeById = new Map(
|
|
||||||
getEdges().map((edge) => [normalizeEdgeId(edge?.id), edge])
|
|
||||||
);
|
|
||||||
|
|
||||||
EDGE_GEOMETRY.forEach(([fromIndex, toIndex], edgeIndex) => {
|
|
||||||
const edgeId = EDGE_GEOMETRY_KEYS[edgeIndex];
|
|
||||||
const edge = edgeById.get(edgeId) || {
|
|
||||||
id: edgeId,
|
|
||||||
name: formatEdgeName(edgeId),
|
|
||||||
walls: edgeId.split("-")
|
|
||||||
};
|
|
||||||
const markerDisplay = getEdgeMarkerDisplay(edge);
|
|
||||||
const edgeWalls = getEdgeWalls(edge);
|
|
||||||
|
|
||||||
const wallIsActive = edgeWalls.includes(normalizeId(state.selectedWallId));
|
|
||||||
const edgeIsActive = normalizeEdgeId(state.selectedEdgeId) === edgeId;
|
|
||||||
|
|
||||||
const from = projectedVertices[fromIndex];
|
|
||||||
const to = projectedVertices[toIndex];
|
|
||||||
|
|
||||||
const line = document.createElementNS(svgNS, "line");
|
|
||||||
line.setAttribute("x1", from.x.toFixed(2));
|
|
||||||
line.setAttribute("y1", from.y.toFixed(2));
|
|
||||||
line.setAttribute("x2", to.x.toFixed(2));
|
|
||||||
line.setAttribute("y2", to.y.toFixed(2));
|
|
||||||
line.setAttribute("stroke", "currentColor");
|
|
||||||
line.setAttribute("stroke-opacity", edgeIsActive ? "0.94" : (wallIsActive ? "0.70" : "0.32"));
|
|
||||||
line.setAttribute("stroke-width", edgeIsActive ? "2.4" : (wallIsActive ? "1.9" : "1.4"));
|
|
||||||
line.setAttribute("class", `cube-edge-line${edgeIsActive ? " is-active" : ""}`);
|
|
||||||
line.setAttribute("role", "button");
|
|
||||||
line.setAttribute("tabindex", "0");
|
|
||||||
line.setAttribute("aria-label", `Cube edge ${toDisplayText(edge?.name) || formatEdgeName(edgeId)}`);
|
|
||||||
|
|
||||||
const selectEdge = () => {
|
|
||||||
state.selectedEdgeId = edgeId;
|
|
||||||
state.selectedNodeType = "wall";
|
|
||||||
state.selectedConnectorId = null;
|
|
||||||
if (!edgeWalls.includes(normalizeId(state.selectedWallId)) && edgeWalls[0]) {
|
|
||||||
state.selectedWallId = edgeWalls[0];
|
|
||||||
snapRotationToWall(state.selectedWallId);
|
|
||||||
}
|
|
||||||
render(getElements());
|
|
||||||
};
|
|
||||||
|
|
||||||
line.addEventListener("click", selectEdge);
|
|
||||||
line.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
selectEdge();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
svg.appendChild(line);
|
|
||||||
|
|
||||||
const dx = to.x - from.x;
|
|
||||||
const dy = to.y - from.y;
|
|
||||||
const length = Math.hypot(dx, dy) || 1;
|
|
||||||
const normalX = -dy / length;
|
|
||||||
const normalY = dx / length;
|
|
||||||
const midpointX = (from.x + to.x) / 2;
|
|
||||||
const midpointY = (from.y + to.y) / 2;
|
|
||||||
|
|
||||||
const centerVectorX = midpointX - CUBE_VIEW_CENTER.x;
|
|
||||||
const centerVectorY = midpointY - CUBE_VIEW_CENTER.y;
|
|
||||||
const normalSign = (centerVectorX * normalX + centerVectorY * normalY) >= 0 ? 1 : -1;
|
|
||||||
|
|
||||||
const markerOffset = edgeIsActive ? 17 : (wallIsActive ? 13 : 12);
|
|
||||||
const labelX = midpointX + (normalX * markerOffset * normalSign);
|
|
||||||
const labelY = midpointY + (normalY * markerOffset * normalSign);
|
|
||||||
|
|
||||||
const marker = document.createElementNS(svgNS, "g");
|
|
||||||
marker.setAttribute(
|
|
||||||
"class",
|
|
||||||
`cube-direction${wallIsActive ? " is-wall-active" : ""}${edgeIsActive ? " is-active" : ""}`
|
|
||||||
);
|
|
||||||
marker.setAttribute("role", "button");
|
|
||||||
marker.setAttribute("tabindex", "0");
|
|
||||||
marker.setAttribute("aria-label", `Cube edge ${toDisplayText(edge?.name) || formatEdgeName(edgeId)}`);
|
|
||||||
|
|
||||||
if (state.markerDisplayMode === "tarot") {
|
|
||||||
const edgeCardUrl = resolveCardImageUrl(getEdgeTarotCard(edge));
|
|
||||||
if (edgeCardUrl) {
|
|
||||||
const cardW = edgeIsActive ? 28 : 20;
|
|
||||||
const cardH = edgeIsActive ? 42 : 30;
|
|
||||||
const edgeTarotCard = getEdgeTarotCard(edge);
|
|
||||||
const cardImg = document.createElementNS(svgNS, "image");
|
|
||||||
cardImg.setAttribute("class", `cube-tarot-image cube-direction-card${edgeIsActive ? " is-active" : ""}`);
|
|
||||||
cardImg.setAttribute("href", edgeCardUrl);
|
|
||||||
cardImg.setAttribute("x", String((labelX - cardW / 2).toFixed(2)));
|
|
||||||
cardImg.setAttribute("y", String((labelY - cardH / 2).toFixed(2)));
|
|
||||||
cardImg.setAttribute("width", String(cardW));
|
|
||||||
cardImg.setAttribute("height", String(cardH));
|
|
||||||
cardImg.setAttribute("role", "button");
|
|
||||||
cardImg.setAttribute("tabindex", "0");
|
|
||||||
cardImg.setAttribute("aria-label", `Open ${edgeTarotCard || edge?.name || "edge"} card image`);
|
|
||||||
cardImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
||||||
cardImg.addEventListener("click", (event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
selectEdge();
|
|
||||||
openTarotCardLightbox(edgeTarotCard, edgeCardUrl, edge?.name || "Cube edge");
|
|
||||||
});
|
|
||||||
cardImg.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
selectEdge();
|
|
||||||
openTarotCardLightbox(edgeTarotCard, edgeCardUrl, edge?.name || "Cube edge");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
marker.appendChild(cardImg);
|
|
||||||
} else {
|
|
||||||
const markerText = document.createElementNS(svgNS, "text");
|
|
||||||
markerText.setAttribute("class", "cube-direction-letter is-missing");
|
|
||||||
markerText.setAttribute("x", String(labelX));
|
|
||||||
markerText.setAttribute("y", String(labelY));
|
|
||||||
markerText.setAttribute("text-anchor", "middle");
|
|
||||||
markerText.setAttribute("dominant-baseline", "middle");
|
|
||||||
markerText.textContent = "!";
|
|
||||||
marker.appendChild(markerText);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const markerText = document.createElementNS(svgNS, "text");
|
|
||||||
markerText.setAttribute(
|
|
||||||
"class",
|
|
||||||
`cube-direction-letter${markerDisplay.isMissing ? " is-missing" : ""}`
|
|
||||||
);
|
|
||||||
markerText.setAttribute("x", String(labelX));
|
|
||||||
markerText.setAttribute("y", String(labelY));
|
|
||||||
markerText.setAttribute("text-anchor", "middle");
|
|
||||||
markerText.setAttribute("dominant-baseline", "middle");
|
|
||||||
markerText.textContent = markerDisplay.text;
|
|
||||||
marker.appendChild(markerText);
|
|
||||||
}
|
|
||||||
|
|
||||||
marker.addEventListener("click", selectEdge);
|
|
||||||
marker.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
selectEdge();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
svg.appendChild(marker);
|
|
||||||
});
|
|
||||||
|
|
||||||
const center = getCubeCenterData();
|
|
||||||
if (center && state.showPrimalPoint) {
|
|
||||||
const centerLetter = getCenterLetterSymbol(center);
|
|
||||||
const centerCardUrl = state.markerDisplayMode === "tarot"
|
|
||||||
? resolveCardImageUrl(getCenterTarotCard(center))
|
|
||||||
: null;
|
|
||||||
const centerActive = state.selectedNodeType === "center";
|
|
||||||
|
|
||||||
const centerMarker = document.createElementNS(svgNS, "g");
|
|
||||||
centerMarker.setAttribute("class", `cube-center${centerActive ? " is-active" : ""}`);
|
|
||||||
centerMarker.setAttribute("role", "button");
|
|
||||||
centerMarker.setAttribute("tabindex", "0");
|
|
||||||
centerMarker.setAttribute("aria-label", "Cube primal point");
|
|
||||||
|
|
||||||
const centerHit = document.createElementNS(svgNS, "circle");
|
|
||||||
centerHit.setAttribute("class", "cube-center-hit");
|
|
||||||
centerHit.setAttribute("cx", String(CUBE_VIEW_CENTER.x));
|
|
||||||
centerHit.setAttribute("cy", String(CUBE_VIEW_CENTER.y));
|
|
||||||
centerHit.setAttribute("r", "18");
|
|
||||||
centerMarker.appendChild(centerHit);
|
|
||||||
|
|
||||||
if (state.markerDisplayMode === "tarot" && centerCardUrl) {
|
|
||||||
const cardW = 24;
|
|
||||||
const cardH = 36;
|
|
||||||
const centerTarotCard = getCenterTarotCard(center);
|
|
||||||
const centerImg = document.createElementNS(svgNS, "image");
|
|
||||||
centerImg.setAttribute("class", "cube-tarot-image cube-center-card");
|
|
||||||
centerImg.setAttribute("href", centerCardUrl);
|
|
||||||
centerImg.setAttribute("x", String((CUBE_VIEW_CENTER.x - cardW / 2).toFixed(2)));
|
|
||||||
centerImg.setAttribute("y", String((CUBE_VIEW_CENTER.y - cardH / 2).toFixed(2)));
|
|
||||||
centerImg.setAttribute("width", String(cardW));
|
|
||||||
centerImg.setAttribute("height", String(cardH));
|
|
||||||
centerImg.setAttribute("role", "button");
|
|
||||||
centerImg.setAttribute("tabindex", "0");
|
|
||||||
centerImg.setAttribute("aria-label", `Open ${centerTarotCard || "Primal Point"} card image`);
|
|
||||||
centerImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
||||||
centerImg.addEventListener("click", (event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
state.selectedNodeType = "center";
|
|
||||||
state.selectedConnectorId = null;
|
|
||||||
render(getElements());
|
|
||||||
openTarotCardLightbox(centerTarotCard, centerCardUrl, "Primal Point");
|
|
||||||
});
|
|
||||||
centerImg.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
state.selectedNodeType = "center";
|
|
||||||
state.selectedConnectorId = null;
|
|
||||||
render(getElements());
|
|
||||||
openTarotCardLightbox(centerTarotCard, centerCardUrl, "Primal Point");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
centerMarker.appendChild(centerImg);
|
|
||||||
} else {
|
|
||||||
const centerText = document.createElementNS(svgNS, "text");
|
|
||||||
centerText.setAttribute(
|
|
||||||
"class",
|
|
||||||
`cube-center-symbol${centerActive ? " is-active" : ""}${centerLetter ? "" : " is-missing"}`
|
|
||||||
);
|
|
||||||
centerText.setAttribute("x", String(CUBE_VIEW_CENTER.x));
|
|
||||||
centerText.setAttribute("y", String(CUBE_VIEW_CENTER.y));
|
|
||||||
centerText.setAttribute("text-anchor", "middle");
|
|
||||||
centerText.setAttribute("dominant-baseline", "middle");
|
|
||||||
centerText.setAttribute("pointer-events", "none");
|
|
||||||
centerText.textContent = centerLetter || "!";
|
|
||||||
centerMarker.appendChild(centerText);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectCenter = () => {
|
|
||||||
state.selectedNodeType = "center";
|
|
||||||
state.selectedConnectorId = null;
|
|
||||||
render(getElements());
|
|
||||||
};
|
|
||||||
|
|
||||||
centerMarker.addEventListener("click", selectCenter);
|
|
||||||
centerMarker.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
selectCenter();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
svg.appendChild(centerMarker);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.markerDisplayMode === "tarot") {
|
|
||||||
Array.from(svg.querySelectorAll("g.cube-direction")).forEach((group) => {
|
|
||||||
svg.appendChild(group);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (state.showConnectorLines) {
|
|
||||||
Array.from(svg.querySelectorAll("g.cube-connector")).forEach((group) => {
|
|
||||||
svg.appendChild(group);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
containerEl.replaceChildren(svg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectEdgeById(edgeId, preferredWallId = "") {
|
function selectEdgeById(edgeId, preferredWallId = "") {
|
||||||
|
|||||||
225
app/ui-gods-references.js
Normal file
225
app/ui-gods-references.js
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
/* ui-gods-references.js — Month reference builders for the gods section */
|
||||||
|
(() => {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function buildMonthReferencesByGod(referenceData) {
|
||||||
|
const map = new Map();
|
||||||
|
const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [];
|
||||||
|
const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
||||||
|
const monthById = new Map(months.map((month) => [month.id, month]));
|
||||||
|
|
||||||
|
function parseMonthDayToken(value) {
|
||||||
|
const text = String(value || "").trim();
|
||||||
|
const match = text.match(/^(\d{1,2})-(\d{1,2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthNo = Number(match[1]);
|
||||||
|
const dayNo = Number(match[2]);
|
||||||
|
if (!Number.isInteger(monthNo) || !Number.isInteger(dayNo) || monthNo < 1 || monthNo > 12 || dayNo < 1 || dayNo > 31) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { month: monthNo, day: dayNo };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMonthDayTokensFromText(value) {
|
||||||
|
const text = String(value || "");
|
||||||
|
const matches = [...text.matchAll(/(\d{1,2})-(\d{1,2})/g)];
|
||||||
|
return matches
|
||||||
|
.map((match) => ({ month: Number(match[1]), day: Number(match[2]) }))
|
||||||
|
.filter((token) => Number.isInteger(token.month) && Number.isInteger(token.day) && token.month >= 1 && token.month <= 12 && token.day >= 1 && token.day <= 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDateToken(token, year) {
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Date(year, token.month - 1, token.day, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMonthDayRangeByMonth(startToken, endToken) {
|
||||||
|
const startDate = toDateToken(startToken, 2025);
|
||||||
|
const endBase = toDateToken(endToken, 2025);
|
||||||
|
if (!startDate || !endBase) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapsYear = endBase.getTime() < startDate.getTime();
|
||||||
|
const endDate = wrapsYear ? toDateToken(endToken, 2026) : endBase;
|
||||||
|
if (!endDate) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = [];
|
||||||
|
let cursor = new Date(startDate);
|
||||||
|
while (cursor.getTime() <= endDate.getTime()) {
|
||||||
|
const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0);
|
||||||
|
const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate;
|
||||||
|
|
||||||
|
segments.push({
|
||||||
|
monthNo: cursor.getMonth() + 1,
|
||||||
|
startDay: cursor.getDate(),
|
||||||
|
endDay: segmentEnd.getDate()
|
||||||
|
});
|
||||||
|
|
||||||
|
cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenToString(monthNo, dayNo) {
|
||||||
|
return `${String(monthNo).padStart(2, "0")}-${String(dayNo).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRangeLabel(monthName, startDay, endDay) {
|
||||||
|
if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) {
|
||||||
|
return monthName;
|
||||||
|
}
|
||||||
|
if (startDay === endDay) {
|
||||||
|
return `${monthName} ${startDay}`;
|
||||||
|
}
|
||||||
|
return `${monthName} ${startDay}-${endDay}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRangeForMonth(month, options = {}) {
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
const monthStart = parseMonthDayToken(month?.start);
|
||||||
|
const monthEnd = parseMonthDayToken(month?.end);
|
||||||
|
if (!Number.isFinite(monthOrder) || !monthStart || !monthEnd) {
|
||||||
|
return {
|
||||||
|
startToken: String(month?.start || "").trim() || null,
|
||||||
|
endToken: String(month?.end || "").trim() || null,
|
||||||
|
label: month?.name || month?.id || "",
|
||||||
|
isFullMonth: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let startToken = parseMonthDayToken(options.startToken);
|
||||||
|
let endToken = parseMonthDayToken(options.endToken);
|
||||||
|
|
||||||
|
if (!startToken || !endToken) {
|
||||||
|
const tokens = parseMonthDayTokensFromText(options.rawDateText);
|
||||||
|
if (tokens.length >= 2) {
|
||||||
|
startToken = tokens[0];
|
||||||
|
endToken = tokens[1];
|
||||||
|
} else if (tokens.length === 1) {
|
||||||
|
startToken = tokens[0];
|
||||||
|
endToken = tokens[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startToken || !endToken) {
|
||||||
|
startToken = monthStart;
|
||||||
|
endToken = monthEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = splitMonthDayRangeByMonth(startToken, endToken);
|
||||||
|
const segment = segments.find((entry) => entry.monthNo === monthOrder) || null;
|
||||||
|
|
||||||
|
const useStart = segment ? { month: monthOrder, day: segment.startDay } : startToken;
|
||||||
|
const useEnd = segment ? { month: monthOrder, day: segment.endDay } : endToken;
|
||||||
|
const startText = tokenToString(useStart.month, useStart.day);
|
||||||
|
const endText = tokenToString(useEnd.month, useEnd.day);
|
||||||
|
const isFullMonth = startText === month.start && endText === month.end;
|
||||||
|
|
||||||
|
return {
|
||||||
|
startToken: startText,
|
||||||
|
endToken: endText,
|
||||||
|
label: isFullMonth
|
||||||
|
? (month.name || month.id)
|
||||||
|
: formatRangeLabel(month.name || month.id, useStart.day, useEnd.day),
|
||||||
|
isFullMonth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushRef(godId, month, options = {}) {
|
||||||
|
if (!godId || !month?.id) return;
|
||||||
|
const key = String(godId).trim().toLowerCase();
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = map.get(key);
|
||||||
|
const range = resolveRangeForMonth(month, options);
|
||||||
|
const rowKey = `${month.id}|${range.startToken || ""}|${range.endToken || ""}`;
|
||||||
|
if (rows.some((entry) => entry.key === rowKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
id: month.id,
|
||||||
|
name: month.name || month.id,
|
||||||
|
order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999,
|
||||||
|
label: range.label,
|
||||||
|
startToken: range.startToken,
|
||||||
|
endToken: range.endToken,
|
||||||
|
isFullMonth: range.isFullMonth,
|
||||||
|
key: rowKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
months.forEach((month) => {
|
||||||
|
pushRef(month?.associations?.godId, month);
|
||||||
|
|
||||||
|
const events = Array.isArray(month?.events) ? month.events : [];
|
||||||
|
events.forEach((event) => {
|
||||||
|
pushRef(event?.associations?.godId, month, {
|
||||||
|
rawDateText: event?.dateRange || event?.date || ""
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
holidays.forEach((holiday) => {
|
||||||
|
const month = monthById.get(holiday?.monthId);
|
||||||
|
if (month) {
|
||||||
|
pushRef(holiday?.associations?.godId, month, {
|
||||||
|
rawDateText: holiday?.dateRange || holiday?.date || ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
map.forEach((rows, key) => {
|
||||||
|
const preciseMonthIds = new Set(
|
||||||
|
rows
|
||||||
|
.filter((entry) => !entry.isFullMonth)
|
||||||
|
.map((entry) => entry.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const filtered = rows.filter((entry) => {
|
||||||
|
if (!entry.isFullMonth) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !preciseMonthIds.has(entry.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
filtered.sort((left, right) => {
|
||||||
|
if (left.order !== right.order) {
|
||||||
|
return left.order - right.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startLeft = parseMonthDayToken(left.startToken);
|
||||||
|
const startRight = parseMonthDayToken(right.startToken);
|
||||||
|
const dayLeft = startLeft ? startLeft.day : 999;
|
||||||
|
const dayRight = startRight ? startRight.day : 999;
|
||||||
|
if (dayLeft !== dayRight) {
|
||||||
|
return dayLeft - dayRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(left.label || left.name || "").localeCompare(String(right.label || right.name || ""));
|
||||||
|
});
|
||||||
|
|
||||||
|
map.set(key, filtered);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.GodReferenceBuilders = {
|
||||||
|
buildMonthReferencesByGod
|
||||||
|
};
|
||||||
|
})();
|
||||||
227
app/ui-gods.js
227
app/ui-gods.js
@@ -3,6 +3,14 @@
|
|||||||
* Kabbalah paths are shown only as a reference at the bottom of each detail view.
|
* Kabbalah paths are shown only as a reference at the bottom of each detail view.
|
||||||
*/
|
*/
|
||||||
(() => {
|
(() => {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const godReferenceBuilders = window.GodReferenceBuilders || {};
|
||||||
|
|
||||||
|
if (typeof godReferenceBuilders.buildMonthReferencesByGod !== "function") {
|
||||||
|
throw new Error("GodReferenceBuilders module must load before ui-gods.js");
|
||||||
|
}
|
||||||
|
|
||||||
// ── State ──────────────────────────────────────────────────────────────────
|
// ── State ──────────────────────────────────────────────────────────────────
|
||||||
const state = {
|
const state = {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@@ -61,223 +69,6 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMonthReferencesByGod(referenceData) {
|
|
||||||
const map = new Map();
|
|
||||||
const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [];
|
|
||||||
const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
|
||||||
const monthById = new Map(months.map((month) => [month.id, month]));
|
|
||||||
|
|
||||||
function parseMonthDayToken(value) {
|
|
||||||
const text = String(value || "").trim();
|
|
||||||
const match = text.match(/^(\d{1,2})-(\d{1,2})$/);
|
|
||||||
if (!match) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthNo = Number(match[1]);
|
|
||||||
const dayNo = Number(match[2]);
|
|
||||||
if (!Number.isInteger(monthNo) || !Number.isInteger(dayNo) || monthNo < 1 || monthNo > 12 || dayNo < 1 || dayNo > 31) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { month: monthNo, day: dayNo };
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMonthDayTokensFromText(value) {
|
|
||||||
const text = String(value || "");
|
|
||||||
const matches = [...text.matchAll(/(\d{1,2})-(\d{1,2})/g)];
|
|
||||||
return matches
|
|
||||||
.map((match) => ({ month: Number(match[1]), day: Number(match[2]) }))
|
|
||||||
.filter((token) => Number.isInteger(token.month) && Number.isInteger(token.day) && token.month >= 1 && token.month <= 12 && token.day >= 1 && token.day <= 31);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toDateToken(token, year) {
|
|
||||||
if (!token) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Date(year, token.month - 1, token.day, 12, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitMonthDayRangeByMonth(startToken, endToken) {
|
|
||||||
const startDate = toDateToken(startToken, 2025);
|
|
||||||
const endBase = toDateToken(endToken, 2025);
|
|
||||||
if (!startDate || !endBase) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapsYear = endBase.getTime() < startDate.getTime();
|
|
||||||
const endDate = wrapsYear ? toDateToken(endToken, 2026) : endBase;
|
|
||||||
if (!endDate) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const segments = [];
|
|
||||||
let cursor = new Date(startDate);
|
|
||||||
while (cursor.getTime() <= endDate.getTime()) {
|
|
||||||
const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0);
|
|
||||||
const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate;
|
|
||||||
|
|
||||||
segments.push({
|
|
||||||
monthNo: cursor.getMonth() + 1,
|
|
||||||
startDay: cursor.getDate(),
|
|
||||||
endDay: segmentEnd.getDate()
|
|
||||||
});
|
|
||||||
|
|
||||||
cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return segments;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenToString(monthNo, dayNo) {
|
|
||||||
return `${String(monthNo).padStart(2, "0")}-${String(dayNo).padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatRangeLabel(monthName, startDay, endDay) {
|
|
||||||
if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) {
|
|
||||||
return monthName;
|
|
||||||
}
|
|
||||||
if (startDay === endDay) {
|
|
||||||
return `${monthName} ${startDay}`;
|
|
||||||
}
|
|
||||||
return `${monthName} ${startDay}-${endDay}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveRangeForMonth(month, options = {}) {
|
|
||||||
const monthOrder = Number(month?.order);
|
|
||||||
const monthStart = parseMonthDayToken(month?.start);
|
|
||||||
const monthEnd = parseMonthDayToken(month?.end);
|
|
||||||
if (!Number.isFinite(monthOrder) || !monthStart || !monthEnd) {
|
|
||||||
return {
|
|
||||||
startToken: String(month?.start || "").trim() || null,
|
|
||||||
endToken: String(month?.end || "").trim() || null,
|
|
||||||
label: month?.name || month?.id || "",
|
|
||||||
isFullMonth: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let startToken = parseMonthDayToken(options.startToken);
|
|
||||||
let endToken = parseMonthDayToken(options.endToken);
|
|
||||||
|
|
||||||
if (!startToken || !endToken) {
|
|
||||||
const tokens = parseMonthDayTokensFromText(options.rawDateText);
|
|
||||||
if (tokens.length >= 2) {
|
|
||||||
startToken = tokens[0];
|
|
||||||
endToken = tokens[1];
|
|
||||||
} else if (tokens.length === 1) {
|
|
||||||
startToken = tokens[0];
|
|
||||||
endToken = tokens[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!startToken || !endToken) {
|
|
||||||
startToken = monthStart;
|
|
||||||
endToken = monthEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
const segments = splitMonthDayRangeByMonth(startToken, endToken);
|
|
||||||
const segment = segments.find((entry) => entry.monthNo === monthOrder) || null;
|
|
||||||
|
|
||||||
const useStart = segment ? { month: monthOrder, day: segment.startDay } : startToken;
|
|
||||||
const useEnd = segment ? { month: monthOrder, day: segment.endDay } : endToken;
|
|
||||||
const startText = tokenToString(useStart.month, useStart.day);
|
|
||||||
const endText = tokenToString(useEnd.month, useEnd.day);
|
|
||||||
const isFullMonth = startText === month.start && endText === month.end;
|
|
||||||
|
|
||||||
return {
|
|
||||||
startToken: startText,
|
|
||||||
endToken: endText,
|
|
||||||
label: isFullMonth
|
|
||||||
? (month.name || month.id)
|
|
||||||
: formatRangeLabel(month.name || month.id, useStart.day, useEnd.day),
|
|
||||||
isFullMonth
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushRef(godId, month, options = {}) {
|
|
||||||
if (!godId || !month?.id) return;
|
|
||||||
const key = String(godId).trim().toLowerCase();
|
|
||||||
if (!key) return;
|
|
||||||
|
|
||||||
if (!map.has(key)) {
|
|
||||||
map.set(key, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = map.get(key);
|
|
||||||
const range = resolveRangeForMonth(month, options);
|
|
||||||
const rowKey = `${month.id}|${range.startToken || ""}|${range.endToken || ""}`;
|
|
||||||
if (rows.some((entry) => entry.key === rowKey)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rows.push({
|
|
||||||
id: month.id,
|
|
||||||
name: month.name || month.id,
|
|
||||||
order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999,
|
|
||||||
label: range.label,
|
|
||||||
startToken: range.startToken,
|
|
||||||
endToken: range.endToken,
|
|
||||||
isFullMonth: range.isFullMonth,
|
|
||||||
key: rowKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
months.forEach((month) => {
|
|
||||||
pushRef(month?.associations?.godId, month);
|
|
||||||
|
|
||||||
const events = Array.isArray(month?.events) ? month.events : [];
|
|
||||||
events.forEach((event) => {
|
|
||||||
pushRef(event?.associations?.godId, month, {
|
|
||||||
rawDateText: event?.dateRange || event?.date || ""
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
holidays.forEach((holiday) => {
|
|
||||||
const month = monthById.get(holiday?.monthId);
|
|
||||||
if (month) {
|
|
||||||
pushRef(holiday?.associations?.godId, month, {
|
|
||||||
rawDateText: holiday?.dateRange || holiday?.date || ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
map.forEach((rows, key) => {
|
|
||||||
const preciseMonthIds = new Set(
|
|
||||||
rows
|
|
||||||
.filter((entry) => !entry.isFullMonth)
|
|
||||||
.map((entry) => entry.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const filtered = rows.filter((entry) => {
|
|
||||||
if (!entry.isFullMonth) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !preciseMonthIds.has(entry.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
filtered.sort((left, right) => {
|
|
||||||
if (left.order !== right.order) {
|
|
||||||
return left.order - right.order;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startLeft = parseMonthDayToken(left.startToken);
|
|
||||||
const startRight = parseMonthDayToken(right.startToken);
|
|
||||||
const dayLeft = startLeft ? startLeft.day : 999;
|
|
||||||
const dayRight = startRight ? startRight.day : 999;
|
|
||||||
if (dayLeft !== dayRight) {
|
|
||||||
return dayLeft - dayRight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(left.label || left.name || "").localeCompare(String(right.label || right.name || ""));
|
|
||||||
});
|
|
||||||
|
|
||||||
map.set(key, filtered);
|
|
||||||
});
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Filter ─────────────────────────────────────────────────────────────────
|
// ── Filter ─────────────────────────────────────────────────────────────────
|
||||||
function applyFilter() {
|
function applyFilter() {
|
||||||
const q = state.searchQuery.toLowerCase();
|
const q = state.searchQuery.toLowerCase();
|
||||||
@@ -557,7 +348,7 @@
|
|||||||
// ── Init ───────────────────────────────────────────────────────────────────
|
// ── Init ───────────────────────────────────────────────────────────────────
|
||||||
function ensureGodsSection(magickDataset, referenceData = null) {
|
function ensureGodsSection(magickDataset, referenceData = null) {
|
||||||
if (referenceData) {
|
if (referenceData) {
|
||||||
state.monthRefsByGodId = buildMonthReferencesByGod(referenceData);
|
state.monthRefsByGodId = godReferenceBuilders.buildMonthReferencesByGod(referenceData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.initialized) {
|
if (state.initialized) {
|
||||||
|
|||||||
472
app/ui-holidays-data.js
Normal file
472
app/ui-holidays-data.js
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
/* ui-holidays-data.js - Holiday data and date resolution helpers */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const HEBREW_MONTH_ALIAS_BY_ID = {
|
||||||
|
nisan: ["nisan"],
|
||||||
|
iyar: ["iyar"],
|
||||||
|
sivan: ["sivan"],
|
||||||
|
tammuz: ["tamuz", "tammuz"],
|
||||||
|
av: ["av"],
|
||||||
|
elul: ["elul"],
|
||||||
|
tishrei: ["tishri", "tishrei"],
|
||||||
|
cheshvan: ["heshvan", "cheshvan", "marcheshvan"],
|
||||||
|
kislev: ["kislev"],
|
||||||
|
tevet: ["tevet"],
|
||||||
|
shvat: ["shevat", "shvat"],
|
||||||
|
adar: ["adar", "adar i", "adar 1"],
|
||||||
|
"adar-ii": ["adar ii", "adar 2"]
|
||||||
|
};
|
||||||
|
|
||||||
|
const MONTH_NAME_TO_INDEX = {
|
||||||
|
january: 0,
|
||||||
|
february: 1,
|
||||||
|
march: 2,
|
||||||
|
april: 3,
|
||||||
|
may: 4,
|
||||||
|
june: 5,
|
||||||
|
july: 6,
|
||||||
|
august: 7,
|
||||||
|
september: 8,
|
||||||
|
october: 9,
|
||||||
|
november: 10,
|
||||||
|
december: 11
|
||||||
|
};
|
||||||
|
|
||||||
|
const GREGORIAN_MONTH_ID_TO_ORDER = {
|
||||||
|
january: 1,
|
||||||
|
february: 2,
|
||||||
|
march: 3,
|
||||||
|
april: 4,
|
||||||
|
may: 5,
|
||||||
|
june: 6,
|
||||||
|
july: 7,
|
||||||
|
august: 8,
|
||||||
|
september: 9,
|
||||||
|
october: 10,
|
||||||
|
november: 11,
|
||||||
|
december: 12
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeCalendarText(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.normalize("NFKD")
|
||||||
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
|
.replace(/['`]/g, "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNumericPart(parts, partType) {
|
||||||
|
const raw = parts.find((part) => part.type === partType)?.value;
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const digits = String(raw).replace(/[^0-9]/g, "");
|
||||||
|
if (!digits) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(digits);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGregorianMonthOrderFromId(monthId) {
|
||||||
|
if (!monthId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const key = String(monthId).trim().toLowerCase();
|
||||||
|
const value = GREGORIAN_MONTH_ID_TO_ORDER[key];
|
||||||
|
return Number.isFinite(value) ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMonthDayStartToken(token) {
|
||||||
|
const match = String(token || "").match(/(\d{2})-(\d{2})/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const month = Number(match[1]);
|
||||||
|
const day = Number(match[2]);
|
||||||
|
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { month, day };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDateAtNoon(year, monthIndex, dayOfMonth) {
|
||||||
|
return new Date(Math.trunc(year), monthIndex, Math.trunc(dayOfMonth), 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeWesternEasterDate(year) {
|
||||||
|
const y = Math.trunc(Number(year));
|
||||||
|
if (!Number.isFinite(y)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = y % 19;
|
||||||
|
const b = Math.floor(y / 100);
|
||||||
|
const c = y % 100;
|
||||||
|
const d = Math.floor(b / 4);
|
||||||
|
const e = b % 4;
|
||||||
|
const f = Math.floor((b + 8) / 25);
|
||||||
|
const g = Math.floor((b - f + 1) / 3);
|
||||||
|
const h = (19 * a + b - d - g + 15) % 30;
|
||||||
|
const i = Math.floor(c / 4);
|
||||||
|
const k = c % 4;
|
||||||
|
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
||||||
|
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
||||||
|
const month = Math.floor((h + l - 7 * m + 114) / 31);
|
||||||
|
const day = ((h + l - 7 * m + 114) % 31) + 1;
|
||||||
|
return createDateAtNoon(y, month - 1, day);
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeNthWeekdayOfMonth(year, monthIndex, weekday, ordinal) {
|
||||||
|
const y = Math.trunc(Number(year));
|
||||||
|
if (!Number.isFinite(y)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = createDateAtNoon(y, monthIndex, 1);
|
||||||
|
const firstWeekday = first.getDay();
|
||||||
|
const offset = (weekday - firstWeekday + 7) % 7;
|
||||||
|
const dayOfMonth = 1 + offset + (Math.trunc(ordinal) - 1) * 7;
|
||||||
|
const daysInMonth = new Date(y, monthIndex + 1, 0).getDate();
|
||||||
|
if (dayOfMonth > daysInMonth) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return createDateAtNoon(y, monthIndex, dayOfMonth);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveGregorianDateRule(rule, selectedYear) {
|
||||||
|
const key = String(rule || "").trim().toLowerCase();
|
||||||
|
if (!key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "gregorian-easter-sunday") {
|
||||||
|
return computeWesternEasterDate(selectedYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "gregorian-good-friday") {
|
||||||
|
const easter = computeWesternEasterDate(selectedYear);
|
||||||
|
if (!(easter instanceof Date) || Number.isNaN(easter.getTime())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return createDateAtNoon(easter.getFullYear(), easter.getMonth(), easter.getDate() - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "gregorian-thanksgiving-us") {
|
||||||
|
return computeNthWeekdayOfMonth(selectedYear, 10, 4, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFirstMonthDayFromText(dateText) {
|
||||||
|
const text = String(dateText || "").replace(/~/g, " ");
|
||||||
|
const firstSegment = text.split("/")[0] || text;
|
||||||
|
const match = firstSegment.match(/(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2})/i);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthIndex = MONTH_NAME_TO_INDEX[String(match[1]).toLowerCase()];
|
||||||
|
const day = Number(match[2]);
|
||||||
|
if (!Number.isFinite(monthIndex) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { monthIndex, day };
|
||||||
|
}
|
||||||
|
|
||||||
|
function findHebrewMonthDayInGregorianYear(monthId, day, year) {
|
||||||
|
const aliases = HEBREW_MONTH_ALIAS_BY_ID[String(monthId || "").toLowerCase()] || [];
|
||||||
|
const targetDay = Number(day);
|
||||||
|
if (!aliases.length || !Number.isFinite(targetDay) || !Number.isFinite(year)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedAliases = aliases.map((alias) => normalizeCalendarText(alias)).filter(Boolean);
|
||||||
|
const formatter = new Intl.DateTimeFormat("en-u-ca-hebrew", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric"
|
||||||
|
});
|
||||||
|
|
||||||
|
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
||||||
|
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
||||||
|
|
||||||
|
while (cursor.getTime() <= end.getTime()) {
|
||||||
|
const parts = formatter.formatToParts(cursor);
|
||||||
|
const currentDay = readNumericPart(parts, "day");
|
||||||
|
const monthName = normalizeCalendarText(parts.find((part) => part.type === "month")?.value);
|
||||||
|
if (currentDay === Math.trunc(targetDay) && normalizedAliases.includes(monthName)) {
|
||||||
|
return new Date(cursor);
|
||||||
|
}
|
||||||
|
cursor.setDate(cursor.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIslamicMonthOrderById(monthId, calendarData) {
|
||||||
|
const month = (calendarData?.islamic || []).find((item) => item?.id === monthId);
|
||||||
|
const order = Number(month?.order);
|
||||||
|
return Number.isFinite(order) ? Math.trunc(order) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findIslamicMonthDayInGregorianYear(monthId, day, year, calendarData) {
|
||||||
|
const monthOrder = getIslamicMonthOrderById(monthId, calendarData);
|
||||||
|
const targetDay = Number(day);
|
||||||
|
if (!Number.isFinite(monthOrder) || !Number.isFinite(targetDay) || !Number.isFinite(year)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat("en-u-ca-islamic", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
year: "numeric"
|
||||||
|
});
|
||||||
|
|
||||||
|
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
||||||
|
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
||||||
|
|
||||||
|
while (cursor.getTime() <= end.getTime()) {
|
||||||
|
const parts = formatter.formatToParts(cursor);
|
||||||
|
const currentDay = readNumericPart(parts, "day");
|
||||||
|
const currentMonth = readNumericPart(parts, "month");
|
||||||
|
if (currentDay === Math.trunc(targetDay) && currentMonth === monthOrder) {
|
||||||
|
return new Date(cursor);
|
||||||
|
}
|
||||||
|
cursor.setDate(cursor.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveHolidayGregorianDate(holiday, options = {}) {
|
||||||
|
if (!holiday || typeof holiday !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedYear = Number(options.selectedYear);
|
||||||
|
const calendarData = options.calendarData || {};
|
||||||
|
const calendarId = String(holiday.calendarId || "").trim().toLowerCase();
|
||||||
|
const monthId = String(holiday.monthId || "").trim().toLowerCase();
|
||||||
|
const day = Number(holiday.day);
|
||||||
|
|
||||||
|
if (calendarId === "gregorian") {
|
||||||
|
if (holiday?.dateRule) {
|
||||||
|
const ruledDate = resolveGregorianDateRule(holiday.dateRule, selectedYear);
|
||||||
|
if (ruledDate) {
|
||||||
|
return ruledDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthDay = parseMonthDayStartToken(holiday.monthDayStart) || parseMonthDayStartToken(holiday.dateText);
|
||||||
|
if (monthDay) {
|
||||||
|
return new Date(selectedYear, monthDay.month - 1, monthDay.day, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
const order = getGregorianMonthOrderFromId(monthId);
|
||||||
|
if (Number.isFinite(order) && Number.isFinite(day)) {
|
||||||
|
return new Date(selectedYear, order - 1, Math.trunc(day), 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calendarId === "hebrew") {
|
||||||
|
return findHebrewMonthDayInGregorianYear(monthId, day, selectedYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calendarId === "islamic") {
|
||||||
|
return findIslamicMonthDayInGregorianYear(monthId, day, selectedYear, calendarData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calendarId === "wheel-of-year") {
|
||||||
|
const monthDay = parseMonthDayStartToken(holiday.monthDayStart) || parseFirstMonthDayFromText(holiday.dateText);
|
||||||
|
if (monthDay?.month && monthDay?.day) {
|
||||||
|
return new Date(selectedYear, monthDay.month - 1, monthDay.day, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
if (monthDay?.monthIndex != null && monthDay?.day) {
|
||||||
|
return new Date(selectedYear, monthDay.monthIndex, monthDay.day, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGregorianReferenceDate(date) {
|
||||||
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleDateString(undefined, {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCalendarDateFromGregorian(date, calendarId) {
|
||||||
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
const locale = calendarId === "hebrew"
|
||||||
|
? "en-u-ca-hebrew"
|
||||||
|
: (calendarId === "islamic" ? "en-u-ca-islamic" : "en");
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(locale, {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric"
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPlanetMap(planetsObj) {
|
||||||
|
const map = new Map();
|
||||||
|
if (!planetsObj || typeof planetsObj !== "object") {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.values(planetsObj).forEach((planet) => {
|
||||||
|
if (!planet?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map.set(planet.id, planet);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSignsMap(signs) {
|
||||||
|
const map = new Map();
|
||||||
|
if (!Array.isArray(signs)) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
signs.forEach((sign) => {
|
||||||
|
if (!sign?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map.set(sign.id, sign);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGodsMap(magickDataset) {
|
||||||
|
const gods = magickDataset?.grouped?.gods?.gods;
|
||||||
|
const map = new Map();
|
||||||
|
|
||||||
|
if (!Array.isArray(gods)) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
gods.forEach((god) => {
|
||||||
|
if (!god?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map.set(god.id, god);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHebrewMap(magickDataset) {
|
||||||
|
const map = new Map();
|
||||||
|
const letters = magickDataset?.grouped?.alphabets?.hebrew;
|
||||||
|
if (!Array.isArray(letters)) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
letters.forEach((letter) => {
|
||||||
|
if (!letter?.hebrewLetterId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map.set(letter.hebrewLetterId, letter);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCalendarData(referenceData) {
|
||||||
|
return {
|
||||||
|
gregorian: Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [],
|
||||||
|
hebrew: Array.isArray(referenceData?.hebrewCalendar?.months) ? referenceData.hebrewCalendar.months : [],
|
||||||
|
islamic: Array.isArray(referenceData?.islamicCalendar?.months) ? referenceData.islamicCalendar.months : [],
|
||||||
|
"wheel-of-year": Array.isArray(referenceData?.wheelOfYear?.months) ? referenceData.wheelOfYear.months : []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function calendarLabel(calendarId) {
|
||||||
|
const key = String(calendarId || "").trim().toLowerCase();
|
||||||
|
if (key === "hebrew") return "Hebrew";
|
||||||
|
if (key === "islamic") return "Islamic";
|
||||||
|
if (key === "wheel-of-year") return "Wheel of the Year";
|
||||||
|
return "Gregorian";
|
||||||
|
}
|
||||||
|
|
||||||
|
function monthLabelForCalendar(calendarData, calendarId, monthId) {
|
||||||
|
const months = calendarData?.[calendarId];
|
||||||
|
if (!Array.isArray(months)) {
|
||||||
|
return monthId || "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
const month = months.find((entry) => String(entry?.id || "").toLowerCase() === String(monthId || "").toLowerCase());
|
||||||
|
return month?.name || monthId || "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSourceFilter(value) {
|
||||||
|
const key = String(value || "").trim().toLowerCase();
|
||||||
|
if (key === "gregorian" || key === "hebrew" || key === "islamic" || key === "wheel-of-year") {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
return "all";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAllHolidays(referenceData) {
|
||||||
|
if (Array.isArray(referenceData?.calendarHolidays) && referenceData.calendarHolidays.length) {
|
||||||
|
return [...referenceData.calendarHolidays].sort((left, right) => {
|
||||||
|
const calCmp = calendarLabel(left?.calendarId).localeCompare(calendarLabel(right?.calendarId));
|
||||||
|
if (calCmp !== 0) return calCmp;
|
||||||
|
|
||||||
|
const leftDay = Number(left?.day);
|
||||||
|
const rightDay = Number(right?.day);
|
||||||
|
if (Number.isFinite(leftDay) && Number.isFinite(rightDay) && leftDay !== rightDay) {
|
||||||
|
return leftDay - rightDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(left?.name || "").localeCompare(String(right?.name || ""));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacy = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
||||||
|
return legacy.map((holiday) => ({
|
||||||
|
...holiday,
|
||||||
|
calendarId: "gregorian",
|
||||||
|
dateText: holiday?.date || holiday?.dateRange || ""
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.HolidayDataUi = {
|
||||||
|
buildAllHolidays,
|
||||||
|
buildCalendarData,
|
||||||
|
buildGodsMap,
|
||||||
|
buildHebrewMap,
|
||||||
|
buildPlanetMap,
|
||||||
|
buildSignsMap,
|
||||||
|
calendarLabel,
|
||||||
|
formatCalendarDateFromGregorian,
|
||||||
|
formatGregorianReferenceDate,
|
||||||
|
monthLabelForCalendar,
|
||||||
|
normalizeSourceFilter,
|
||||||
|
resolveHolidayGregorianDate
|
||||||
|
};
|
||||||
|
})();
|
||||||
311
app/ui-holidays-render.js
Normal file
311
app/ui-holidays-render.js
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
/* ui-holidays-render.js - Render/search helpers for the holiday repository */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function planetLabel(planetId, context) {
|
||||||
|
const { state, cap } = context;
|
||||||
|
if (!planetId) {
|
||||||
|
return "Planet";
|
||||||
|
}
|
||||||
|
|
||||||
|
const planet = state.planetsById.get(planetId);
|
||||||
|
if (!planet) {
|
||||||
|
return cap(planetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${planet.symbol || ""} ${planet.name || cap(planetId)}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function zodiacLabel(signId, context) {
|
||||||
|
const { state, cap } = context;
|
||||||
|
if (!signId) {
|
||||||
|
return "Zodiac";
|
||||||
|
}
|
||||||
|
|
||||||
|
const sign = state.signsById.get(signId);
|
||||||
|
if (!sign) {
|
||||||
|
return cap(signId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${sign.symbol || ""} ${sign.name || cap(signId)}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function godLabel(godId, godName, context) {
|
||||||
|
const { state, cap } = context;
|
||||||
|
if (godName) {
|
||||||
|
return godName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!godId) {
|
||||||
|
return "Deity";
|
||||||
|
}
|
||||||
|
|
||||||
|
const god = state.godsById.get(godId);
|
||||||
|
return god?.name || cap(godId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hebrewLabel(hebrewLetterId, context) {
|
||||||
|
const { state, cap } = context;
|
||||||
|
if (!hebrewLetterId) {
|
||||||
|
return "Hebrew Letter";
|
||||||
|
}
|
||||||
|
|
||||||
|
const letter = state.hebrewById.get(hebrewLetterId);
|
||||||
|
if (!letter) {
|
||||||
|
return cap(hebrewLetterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${letter.char || ""} ${letter.name || cap(hebrewLetterId)}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeDigitalRoot(value) {
|
||||||
|
let current = Math.abs(Math.trunc(Number(value)));
|
||||||
|
if (!Number.isFinite(current)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (current >= 10) {
|
||||||
|
current = String(current)
|
||||||
|
.split("")
|
||||||
|
.reduce((sum, digit) => sum + Number(digit), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAssociationButtons(associations, context) {
|
||||||
|
const { getDisplayTarotName, resolveTarotTrumpNumber } = context;
|
||||||
|
if (!associations || typeof associations !== "object") {
|
||||||
|
return '<div class="planet-text">--</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttons = [];
|
||||||
|
|
||||||
|
if (associations.planetId) {
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="planet" data-planet-id="${associations.planetId}">${planetLabel(associations.planetId, context)} -></button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associations.zodiacSignId) {
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="zodiac" data-sign-id="${associations.zodiacSignId}">${zodiacLabel(associations.zodiacSignId, context)} -></button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isFinite(Number(associations.numberValue))) {
|
||||||
|
const rawNumber = Math.trunc(Number(associations.numberValue));
|
||||||
|
if (rawNumber >= 0) {
|
||||||
|
const numberValue = computeDigitalRoot(rawNumber);
|
||||||
|
if (numberValue != null) {
|
||||||
|
const label = rawNumber === numberValue
|
||||||
|
? `Number ${numberValue}`
|
||||||
|
: `Number ${numberValue} (from ${rawNumber})`;
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="number" data-number-value="${numberValue}">${label} -></button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associations.tarotCard) {
|
||||||
|
const trumpNumber = resolveTarotTrumpNumber(associations.tarotCard);
|
||||||
|
const explicitTrumpNumber = Number(associations.tarotTrumpNumber);
|
||||||
|
const tarotTrumpNumber = Number.isFinite(explicitTrumpNumber) ? explicitTrumpNumber : trumpNumber;
|
||||||
|
const tarotLabel = getDisplayTarotName(associations.tarotCard, tarotTrumpNumber);
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="tarot-card" data-card-name="${associations.tarotCard}" data-trump-number="${tarotTrumpNumber ?? ""}">${tarotLabel} -></button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associations.godId || associations.godName) {
|
||||||
|
const label = godLabel(associations.godId, associations.godName, context);
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="god" data-god-id="${associations.godId || ""}" data-god-name="${associations.godName || label}">${label} -></button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associations.hebrewLetterId) {
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="alphabet" data-alphabet="hebrew" data-hebrew-letter-id="${associations.hebrewLetterId}">${hebrewLabel(associations.hebrewLetterId, context)} -></button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associations.kabbalahPathNumber != null) {
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="kabbalah" data-path-no="${associations.kabbalahPathNumber}">Path ${associations.kabbalahPathNumber} -></button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associations.iChingPlanetaryInfluence) {
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="iching" data-planetary-influence="${associations.iChingPlanetaryInfluence}">I Ching - ${associations.iChingPlanetaryInfluence} -></button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!buttons.length) {
|
||||||
|
return '<div class="planet-text">--</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<div class="alpha-nav-btns">${buttons.join("")}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function associationSearchText(associations, context) {
|
||||||
|
const { getTarotCardSearchAliases } = context;
|
||||||
|
if (!associations || typeof associations !== "object") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const tarotAliases = associations.tarotCard && typeof getTarotCardSearchAliases === "function"
|
||||||
|
? getTarotCardSearchAliases(associations.tarotCard, { trumpNumber: associations.tarotTrumpNumber })
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
associations.planetId,
|
||||||
|
associations.zodiacSignId,
|
||||||
|
associations.numberValue,
|
||||||
|
associations.tarotCard,
|
||||||
|
associations.tarotTrumpNumber,
|
||||||
|
...tarotAliases,
|
||||||
|
associations.godId,
|
||||||
|
associations.godName,
|
||||||
|
associations.hebrewLetterId,
|
||||||
|
associations.kabbalahPathNumber,
|
||||||
|
associations.iChingPlanetaryInfluence
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function holidaySearchText(holiday, context) {
|
||||||
|
const { normalizeSearchValue } = context;
|
||||||
|
return normalizeSearchValue([
|
||||||
|
holiday?.name,
|
||||||
|
holiday?.kind,
|
||||||
|
holiday?.date,
|
||||||
|
holiday?.dateRange,
|
||||||
|
holiday?.dateText,
|
||||||
|
holiday?.monthDayStart,
|
||||||
|
holiday?.calendarId,
|
||||||
|
holiday?.description,
|
||||||
|
associationSearchText(holiday?.associations, context)
|
||||||
|
].filter(Boolean).join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList(context) {
|
||||||
|
const {
|
||||||
|
elements,
|
||||||
|
state,
|
||||||
|
filterBySource,
|
||||||
|
normalizeSourceFilter,
|
||||||
|
calendarLabel,
|
||||||
|
monthLabelForCalendar,
|
||||||
|
selectByHolidayId
|
||||||
|
} = context;
|
||||||
|
const { listEl, countEl } = elements;
|
||||||
|
if (!listEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
listEl.innerHTML = "";
|
||||||
|
|
||||||
|
state.filteredHolidays.forEach((holiday) => {
|
||||||
|
const isSelected = holiday.id === state.selectedHolidayId;
|
||||||
|
const itemEl = document.createElement("div");
|
||||||
|
itemEl.className = `planet-list-item${isSelected ? " is-selected" : ""}`;
|
||||||
|
itemEl.setAttribute("role", "option");
|
||||||
|
itemEl.setAttribute("aria-selected", isSelected ? "true" : "false");
|
||||||
|
itemEl.dataset.holidayId = holiday.id;
|
||||||
|
|
||||||
|
const sourceCalendar = calendarLabel(holiday.calendarId);
|
||||||
|
const sourceMonth = monthLabelForCalendar(holiday.calendarId, holiday.monthId);
|
||||||
|
const sourceDate = holiday?.dateText || holiday?.date || holiday?.dateRange || "--";
|
||||||
|
|
||||||
|
itemEl.innerHTML = `
|
||||||
|
<div class="planet-list-name">${holiday?.name || holiday?.id || "Holiday"}</div>
|
||||||
|
<div class="planet-list-meta">${sourceCalendar} - ${sourceMonth} - ${sourceDate}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
itemEl.addEventListener("click", () => {
|
||||||
|
selectByHolidayId(holiday.id, elements);
|
||||||
|
});
|
||||||
|
|
||||||
|
listEl.appendChild(itemEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (countEl) {
|
||||||
|
const sourceFiltered = filterBySource(state.holidays);
|
||||||
|
const activeFilter = normalizeSourceFilter(state.selectedSource);
|
||||||
|
const sourceLabel = activeFilter === "all"
|
||||||
|
? ""
|
||||||
|
: ` (${calendarLabel(activeFilter)})`;
|
||||||
|
countEl.textContent = state.searchQuery
|
||||||
|
? `${state.filteredHolidays.length} of ${sourceFiltered.length} holidays${sourceLabel}`
|
||||||
|
: `${sourceFiltered.length} holidays${sourceLabel}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHolidayDetail(holiday, context) {
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
calendarLabel,
|
||||||
|
monthLabelForCalendar,
|
||||||
|
resolveHolidayGregorianDate,
|
||||||
|
formatGregorianReferenceDate,
|
||||||
|
formatCalendarDateFromGregorian
|
||||||
|
} = context;
|
||||||
|
const gregorianDate = resolveHolidayGregorianDate(holiday);
|
||||||
|
const gregorianRef = formatGregorianReferenceDate(gregorianDate);
|
||||||
|
const hebrewRef = formatCalendarDateFromGregorian(gregorianDate, "hebrew");
|
||||||
|
const islamicRef = formatCalendarDateFromGregorian(gregorianDate, "islamic");
|
||||||
|
const confidence = String(holiday?.conversionConfidence || holiday?.datePrecision || "approximate").toLowerCase();
|
||||||
|
const confidenceLabel = (!(gregorianDate instanceof Date) || Number.isNaN(gregorianDate.getTime()))
|
||||||
|
? "unresolved"
|
||||||
|
: (confidence === "exact" ? "exact" : "approximate");
|
||||||
|
const monthName = monthLabelForCalendar(holiday?.calendarId, holiday?.monthId);
|
||||||
|
const holidayDate = holiday?.dateText || holiday?.date || holiday?.dateRange || "--";
|
||||||
|
const sourceMonthLink = holiday?.monthId
|
||||||
|
? `<div class="alpha-nav-btns"><button class="alpha-nav-btn" data-nav="calendar-month" data-calendar-id="${holiday.calendarId || ""}" data-month-id="${holiday.monthId}">Open ${calendarLabel(holiday?.calendarId)} ${monthName} -></button></div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-grid">
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Holiday Facts</strong>
|
||||||
|
<div class="planet-text">
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Source Calendar</dt><dd>${calendarLabel(holiday?.calendarId)}</dd>
|
||||||
|
<dt>Source Month</dt><dd>${monthName}</dd>
|
||||||
|
<dt>Source Date</dt><dd>${holidayDate}</dd>
|
||||||
|
<dt>Reference Year</dt><dd>${state.selectedYear}</dd>
|
||||||
|
<dt>Conversion</dt><dd>${confidenceLabel}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Cross-Calendar Dates</strong>
|
||||||
|
<div class="planet-text">
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Gregorian</dt><dd>${gregorianRef}</dd>
|
||||||
|
<dt>Hebrew</dt><dd>${hebrewRef}</dd>
|
||||||
|
<dt>Islamic</dt><dd>${islamicRef}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Description</strong>
|
||||||
|
<div class="planet-text">${holiday?.description || "--"}</div>
|
||||||
|
${sourceMonthLink}
|
||||||
|
</div>
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Associations</strong>
|
||||||
|
${buildAssociationButtons(holiday?.associations, context)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.HolidayRenderUi = {
|
||||||
|
holidaySearchText,
|
||||||
|
renderList,
|
||||||
|
renderHolidayDetail
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -3,6 +3,28 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const { getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
|
const { getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
|
||||||
|
const holidayDataUi = window.HolidayDataUi || {};
|
||||||
|
const holidayRenderUi = window.HolidayRenderUi || {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof holidayDataUi.buildAllHolidays !== "function"
|
||||||
|
|| typeof holidayDataUi.buildCalendarData !== "function"
|
||||||
|
|| typeof holidayDataUi.buildGodsMap !== "function"
|
||||||
|
|| typeof holidayDataUi.buildHebrewMap !== "function"
|
||||||
|
|| typeof holidayDataUi.buildPlanetMap !== "function"
|
||||||
|
|| typeof holidayDataUi.buildSignsMap !== "function"
|
||||||
|
|| typeof holidayDataUi.calendarLabel !== "function"
|
||||||
|
|| typeof holidayDataUi.formatCalendarDateFromGregorian !== "function"
|
||||||
|
|| typeof holidayDataUi.formatGregorianReferenceDate !== "function"
|
||||||
|
|| typeof holidayDataUi.monthLabelForCalendar !== "function"
|
||||||
|
|| typeof holidayDataUi.normalizeSourceFilter !== "function"
|
||||||
|
|| typeof holidayDataUi.resolveHolidayGregorianDate !== "function"
|
||||||
|
|| typeof holidayRenderUi.holidaySearchText !== "function"
|
||||||
|
|| typeof holidayRenderUi.renderList !== "function"
|
||||||
|
|| typeof holidayRenderUi.renderHolidayDetail !== "function"
|
||||||
|
) {
|
||||||
|
throw new Error("HolidayDataUi and HolidayRenderUi modules must load before ui-holidays.js");
|
||||||
|
}
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@@ -69,52 +91,6 @@
|
|||||||
"the world": 21
|
"the world": 21
|
||||||
};
|
};
|
||||||
|
|
||||||
const HEBREW_MONTH_ALIAS_BY_ID = {
|
|
||||||
nisan: ["nisan"],
|
|
||||||
iyar: ["iyar"],
|
|
||||||
sivan: ["sivan"],
|
|
||||||
tammuz: ["tamuz", "tammuz"],
|
|
||||||
av: ["av"],
|
|
||||||
elul: ["elul"],
|
|
||||||
tishrei: ["tishri", "tishrei"],
|
|
||||||
cheshvan: ["heshvan", "cheshvan", "marcheshvan"],
|
|
||||||
kislev: ["kislev"],
|
|
||||||
tevet: ["tevet"],
|
|
||||||
shvat: ["shevat", "shvat"],
|
|
||||||
adar: ["adar", "adar i", "adar 1"],
|
|
||||||
"adar-ii": ["adar ii", "adar 2"]
|
|
||||||
};
|
|
||||||
|
|
||||||
const MONTH_NAME_TO_INDEX = {
|
|
||||||
january: 0,
|
|
||||||
february: 1,
|
|
||||||
march: 2,
|
|
||||||
april: 3,
|
|
||||||
may: 4,
|
|
||||||
june: 5,
|
|
||||||
july: 6,
|
|
||||||
august: 7,
|
|
||||||
september: 8,
|
|
||||||
october: 9,
|
|
||||||
november: 10,
|
|
||||||
december: 11
|
|
||||||
};
|
|
||||||
|
|
||||||
const GREGORIAN_MONTH_ID_TO_ORDER = {
|
|
||||||
january: 1,
|
|
||||||
february: 2,
|
|
||||||
march: 3,
|
|
||||||
april: 4,
|
|
||||||
may: 5,
|
|
||||||
june: 6,
|
|
||||||
july: 7,
|
|
||||||
august: 8,
|
|
||||||
september: 9,
|
|
||||||
october: 10,
|
|
||||||
november: 11,
|
|
||||||
december: 12
|
|
||||||
};
|
|
||||||
|
|
||||||
function getElements() {
|
function getElements() {
|
||||||
return {
|
return {
|
||||||
sourceSelectEl: document.getElementById("holiday-source-select"),
|
sourceSelectEl: document.getElementById("holiday-source-select"),
|
||||||
@@ -180,583 +156,27 @@
|
|||||||
return getTarotCardDisplayName(cardName) || cardName;
|
return getTarotCardDisplayName(cardName) || cardName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeCalendarText(value) {
|
function getRenderContext(elements = getElements()) {
|
||||||
return String(value || "")
|
return {
|
||||||
.normalize("NFKD")
|
elements,
|
||||||
.replace(/[\u0300-\u036f]/g, "")
|
state,
|
||||||
.replace(/['`]/g, "")
|
cap,
|
||||||
.toLowerCase()
|
normalizeSearchValue,
|
||||||
.replace(/[^a-z0-9]+/g, " ")
|
getDisplayTarotName,
|
||||||
.trim();
|
resolveTarotTrumpNumber,
|
||||||
}
|
getTarotCardSearchAliases,
|
||||||
|
calendarLabel: holidayDataUi.calendarLabel,
|
||||||
function readNumericPart(parts, partType) {
|
monthLabelForCalendar: (calendarId, monthId) => holidayDataUi.monthLabelForCalendar(state.calendarData, calendarId, monthId),
|
||||||
const raw = parts.find((part) => part.type === partType)?.value;
|
normalizeSourceFilter: holidayDataUi.normalizeSourceFilter,
|
||||||
if (!raw) {
|
filterBySource,
|
||||||
return null;
|
resolveHolidayGregorianDate: (holiday) => holidayDataUi.resolveHolidayGregorianDate(holiday, {
|
||||||
}
|
selectedYear: state.selectedYear,
|
||||||
|
calendarData: state.calendarData
|
||||||
const digits = String(raw).replace(/[^0-9]/g, "");
|
}),
|
||||||
if (!digits) {
|
formatGregorianReferenceDate: holidayDataUi.formatGregorianReferenceDate,
|
||||||
return null;
|
formatCalendarDateFromGregorian: holidayDataUi.formatCalendarDateFromGregorian,
|
||||||
}
|
selectByHolidayId
|
||||||
|
};
|
||||||
const parsed = Number(digits);
|
|
||||||
return Number.isFinite(parsed) ? parsed : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGregorianMonthOrderFromId(monthId) {
|
|
||||||
if (!monthId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const key = String(monthId).trim().toLowerCase();
|
|
||||||
const value = GREGORIAN_MONTH_ID_TO_ORDER[key];
|
|
||||||
return Number.isFinite(value) ? value : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMonthDayStartToken(token) {
|
|
||||||
const match = String(token || "").match(/(\d{2})-(\d{2})/);
|
|
||||||
if (!match) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const month = Number(match[1]);
|
|
||||||
const day = Number(match[2]);
|
|
||||||
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { month, day };
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDateAtNoon(year, monthIndex, dayOfMonth) {
|
|
||||||
return new Date(Math.trunc(year), monthIndex, Math.trunc(dayOfMonth), 12, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeWesternEasterDate(year) {
|
|
||||||
const y = Math.trunc(Number(year));
|
|
||||||
if (!Number.isFinite(y)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Meeus/Jones/Butcher Gregorian algorithm.
|
|
||||||
const a = y % 19;
|
|
||||||
const b = Math.floor(y / 100);
|
|
||||||
const c = y % 100;
|
|
||||||
const d = Math.floor(b / 4);
|
|
||||||
const e = b % 4;
|
|
||||||
const f = Math.floor((b + 8) / 25);
|
|
||||||
const g = Math.floor((b - f + 1) / 3);
|
|
||||||
const h = (19 * a + b - d - g + 15) % 30;
|
|
||||||
const i = Math.floor(c / 4);
|
|
||||||
const k = c % 4;
|
|
||||||
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
|
||||||
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
|
||||||
const month = Math.floor((h + l - 7 * m + 114) / 31);
|
|
||||||
const day = ((h + l - 7 * m + 114) % 31) + 1;
|
|
||||||
return createDateAtNoon(y, month - 1, day);
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeNthWeekdayOfMonth(year, monthIndex, weekday, ordinal) {
|
|
||||||
const y = Math.trunc(Number(year));
|
|
||||||
if (!Number.isFinite(y)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const first = createDateAtNoon(y, monthIndex, 1);
|
|
||||||
const firstWeekday = first.getDay();
|
|
||||||
const offset = (weekday - firstWeekday + 7) % 7;
|
|
||||||
const dayOfMonth = 1 + offset + (Math.trunc(ordinal) - 1) * 7;
|
|
||||||
const daysInMonth = new Date(y, monthIndex + 1, 0).getDate();
|
|
||||||
if (dayOfMonth > daysInMonth) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return createDateAtNoon(y, monthIndex, dayOfMonth);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveGregorianDateRule(rule) {
|
|
||||||
const key = String(rule || "").trim().toLowerCase();
|
|
||||||
if (!key) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === "gregorian-easter-sunday") {
|
|
||||||
return computeWesternEasterDate(state.selectedYear);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === "gregorian-good-friday") {
|
|
||||||
const easter = computeWesternEasterDate(state.selectedYear);
|
|
||||||
if (!(easter instanceof Date) || Number.isNaN(easter.getTime())) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return createDateAtNoon(easter.getFullYear(), easter.getMonth(), easter.getDate() - 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === "gregorian-thanksgiving-us") {
|
|
||||||
// US Thanksgiving: 4th Thursday of November.
|
|
||||||
return computeNthWeekdayOfMonth(state.selectedYear, 10, 4, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseFirstMonthDayFromText(dateText) {
|
|
||||||
const text = String(dateText || "").replace(/~/g, " ");
|
|
||||||
const firstSegment = text.split("/")[0] || text;
|
|
||||||
const match = firstSegment.match(/(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2})/i);
|
|
||||||
if (!match) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthIndex = MONTH_NAME_TO_INDEX[String(match[1]).toLowerCase()];
|
|
||||||
const day = Number(match[2]);
|
|
||||||
if (!Number.isFinite(monthIndex) || !Number.isFinite(day)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { monthIndex, day };
|
|
||||||
}
|
|
||||||
|
|
||||||
function findHebrewMonthDayInGregorianYear(monthId, day, year) {
|
|
||||||
const aliases = HEBREW_MONTH_ALIAS_BY_ID[String(monthId || "").toLowerCase()] || [];
|
|
||||||
const targetDay = Number(day);
|
|
||||||
if (!aliases.length || !Number.isFinite(targetDay) || !Number.isFinite(year)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedAliases = aliases.map((alias) => normalizeCalendarText(alias)).filter(Boolean);
|
|
||||||
const formatter = new Intl.DateTimeFormat("en-u-ca-hebrew", {
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric"
|
|
||||||
});
|
|
||||||
|
|
||||||
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
|
||||||
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
|
||||||
|
|
||||||
while (cursor.getTime() <= end.getTime()) {
|
|
||||||
const parts = formatter.formatToParts(cursor);
|
|
||||||
const currentDay = readNumericPart(parts, "day");
|
|
||||||
const monthName = normalizeCalendarText(parts.find((part) => part.type === "month")?.value);
|
|
||||||
if (currentDay === Math.trunc(targetDay) && normalizedAliases.includes(monthName)) {
|
|
||||||
return new Date(cursor);
|
|
||||||
}
|
|
||||||
cursor.setDate(cursor.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIslamicMonthOrderById(monthId) {
|
|
||||||
const month = (state.calendarData?.islamic || []).find((item) => item?.id === monthId);
|
|
||||||
const order = Number(month?.order);
|
|
||||||
return Number.isFinite(order) ? Math.trunc(order) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findIslamicMonthDayInGregorianYear(monthId, day, year) {
|
|
||||||
const monthOrder = getIslamicMonthOrderById(monthId);
|
|
||||||
const targetDay = Number(day);
|
|
||||||
if (!Number.isFinite(monthOrder) || !Number.isFinite(targetDay) || !Number.isFinite(year)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatter = new Intl.DateTimeFormat("en-u-ca-islamic", {
|
|
||||||
day: "numeric",
|
|
||||||
month: "numeric",
|
|
||||||
year: "numeric"
|
|
||||||
});
|
|
||||||
|
|
||||||
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
|
||||||
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
|
||||||
|
|
||||||
while (cursor.getTime() <= end.getTime()) {
|
|
||||||
const parts = formatter.formatToParts(cursor);
|
|
||||||
const currentDay = readNumericPart(parts, "day");
|
|
||||||
const currentMonth = readNumericPart(parts, "month");
|
|
||||||
if (currentDay === Math.trunc(targetDay) && currentMonth === monthOrder) {
|
|
||||||
return new Date(cursor);
|
|
||||||
}
|
|
||||||
cursor.setDate(cursor.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveHolidayGregorianDate(holiday) {
|
|
||||||
if (!holiday || typeof holiday !== "object") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const calendarId = String(holiday.calendarId || "").trim().toLowerCase();
|
|
||||||
const monthId = String(holiday.monthId || "").trim().toLowerCase();
|
|
||||||
const day = Number(holiday.day);
|
|
||||||
|
|
||||||
if (calendarId === "gregorian") {
|
|
||||||
if (holiday?.dateRule) {
|
|
||||||
const ruledDate = resolveGregorianDateRule(holiday.dateRule);
|
|
||||||
if (ruledDate) {
|
|
||||||
return ruledDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthDay = parseMonthDayStartToken(holiday.monthDayStart) || parseMonthDayStartToken(holiday.dateText);
|
|
||||||
if (monthDay) {
|
|
||||||
return new Date(state.selectedYear, monthDay.month - 1, monthDay.day, 12, 0, 0, 0);
|
|
||||||
}
|
|
||||||
const order = getGregorianMonthOrderFromId(monthId);
|
|
||||||
if (Number.isFinite(order) && Number.isFinite(day)) {
|
|
||||||
return new Date(state.selectedYear, order - 1, Math.trunc(day), 12, 0, 0, 0);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (calendarId === "hebrew") {
|
|
||||||
return findHebrewMonthDayInGregorianYear(monthId, day, state.selectedYear);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (calendarId === "islamic") {
|
|
||||||
return findIslamicMonthDayInGregorianYear(monthId, day, state.selectedYear);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (calendarId === "wheel-of-year") {
|
|
||||||
const monthDay = parseMonthDayStartToken(holiday.monthDayStart) || parseFirstMonthDayFromText(holiday.dateText);
|
|
||||||
if (monthDay?.month && monthDay?.day) {
|
|
||||||
return new Date(state.selectedYear, monthDay.month - 1, monthDay.day, 12, 0, 0, 0);
|
|
||||||
}
|
|
||||||
if (monthDay?.monthIndex != null && monthDay?.day) {
|
|
||||||
return new Date(state.selectedYear, monthDay.monthIndex, monthDay.day, 12, 0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatGregorianReferenceDate(date) {
|
|
||||||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
||||||
return "--";
|
|
||||||
}
|
|
||||||
|
|
||||||
return date.toLocaleDateString(undefined, {
|
|
||||||
weekday: "long",
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCalendarDateFromGregorian(date, calendarId) {
|
|
||||||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
||||||
return "--";
|
|
||||||
}
|
|
||||||
|
|
||||||
const locale = calendarId === "hebrew"
|
|
||||||
? "en-u-ca-hebrew"
|
|
||||||
: (calendarId === "islamic" ? "en-u-ca-islamic" : "en");
|
|
||||||
|
|
||||||
return new Intl.DateTimeFormat(locale, {
|
|
||||||
weekday: "long",
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric"
|
|
||||||
}).format(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPlanetMap(planetsObj) {
|
|
||||||
const map = new Map();
|
|
||||||
if (!planetsObj || typeof planetsObj !== "object") {
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.values(planetsObj).forEach((planet) => {
|
|
||||||
if (!planet?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
map.set(planet.id, planet);
|
|
||||||
});
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSignsMap(signs) {
|
|
||||||
const map = new Map();
|
|
||||||
if (!Array.isArray(signs)) {
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
signs.forEach((sign) => {
|
|
||||||
if (!sign?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
map.set(sign.id, sign);
|
|
||||||
});
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildGodsMap(magickDataset) {
|
|
||||||
const gods = magickDataset?.grouped?.gods?.gods;
|
|
||||||
const map = new Map();
|
|
||||||
|
|
||||||
if (!Array.isArray(gods)) {
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
gods.forEach((god) => {
|
|
||||||
if (!god?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
map.set(god.id, god);
|
|
||||||
});
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildHebrewMap(magickDataset) {
|
|
||||||
const map = new Map();
|
|
||||||
const letters = magickDataset?.grouped?.alphabets?.hebrew;
|
|
||||||
if (!Array.isArray(letters)) {
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
letters.forEach((letter) => {
|
|
||||||
if (!letter?.hebrewLetterId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
map.set(letter.hebrewLetterId, letter);
|
|
||||||
});
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calendarLabel(calendarId) {
|
|
||||||
const key = String(calendarId || "").trim().toLowerCase();
|
|
||||||
if (key === "hebrew") return "Hebrew";
|
|
||||||
if (key === "islamic") return "Islamic";
|
|
||||||
if (key === "wheel-of-year") return "Wheel of the Year";
|
|
||||||
return "Gregorian";
|
|
||||||
}
|
|
||||||
|
|
||||||
function monthLabelForCalendar(calendarId, monthId) {
|
|
||||||
const months = state.calendarData?.[calendarId];
|
|
||||||
if (!Array.isArray(months)) {
|
|
||||||
return monthId || "--";
|
|
||||||
}
|
|
||||||
|
|
||||||
const month = months.find((entry) => String(entry?.id || "").toLowerCase() === String(monthId || "").toLowerCase());
|
|
||||||
return month?.name || monthId || "--";
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeSourceFilter(value) {
|
|
||||||
const key = String(value || "").trim().toLowerCase();
|
|
||||||
if (key === "gregorian" || key === "hebrew" || key === "islamic" || key === "wheel-of-year") {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
return "all";
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAllHolidays() {
|
|
||||||
if (Array.isArray(state.referenceData?.calendarHolidays) && state.referenceData.calendarHolidays.length) {
|
|
||||||
return [...state.referenceData.calendarHolidays].sort((left, right) => {
|
|
||||||
const calCmp = calendarLabel(left?.calendarId).localeCompare(calendarLabel(right?.calendarId));
|
|
||||||
if (calCmp !== 0) return calCmp;
|
|
||||||
|
|
||||||
const leftDay = Number(left?.day);
|
|
||||||
const rightDay = Number(right?.day);
|
|
||||||
if (Number.isFinite(leftDay) && Number.isFinite(rightDay) && leftDay !== rightDay) {
|
|
||||||
return leftDay - rightDay;
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(left?.name || "").localeCompare(String(right?.name || ""));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const legacy = Array.isArray(state.referenceData?.celestialHolidays) ? state.referenceData.celestialHolidays : [];
|
|
||||||
return legacy.map((holiday) => ({
|
|
||||||
...holiday,
|
|
||||||
calendarId: "gregorian",
|
|
||||||
dateText: holiday?.date || holiday?.dateRange || ""
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function planetLabel(planetId) {
|
|
||||||
if (!planetId) {
|
|
||||||
return "Planet";
|
|
||||||
}
|
|
||||||
|
|
||||||
const planet = state.planetsById.get(planetId);
|
|
||||||
if (!planet) {
|
|
||||||
return cap(planetId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${planet.symbol || ""} ${planet.name || cap(planetId)}`.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function zodiacLabel(signId) {
|
|
||||||
if (!signId) {
|
|
||||||
return "Zodiac";
|
|
||||||
}
|
|
||||||
|
|
||||||
const sign = state.signsById.get(signId);
|
|
||||||
if (!sign) {
|
|
||||||
return cap(signId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${sign.symbol || ""} ${sign.name || cap(signId)}`.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function godLabel(godId, godName) {
|
|
||||||
if (godName) {
|
|
||||||
return godName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!godId) {
|
|
||||||
return "Deity";
|
|
||||||
}
|
|
||||||
|
|
||||||
const god = state.godsById.get(godId);
|
|
||||||
return god?.name || cap(godId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hebrewLabel(hebrewLetterId) {
|
|
||||||
if (!hebrewLetterId) {
|
|
||||||
return "Hebrew Letter";
|
|
||||||
}
|
|
||||||
|
|
||||||
const letter = state.hebrewById.get(hebrewLetterId);
|
|
||||||
if (!letter) {
|
|
||||||
return cap(hebrewLetterId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${letter.char || ""} ${letter.name || cap(hebrewLetterId)}`.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeDigitalRoot(value) {
|
|
||||||
let current = Math.abs(Math.trunc(Number(value)));
|
|
||||||
if (!Number.isFinite(current)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (current >= 10) {
|
|
||||||
current = String(current)
|
|
||||||
.split("")
|
|
||||||
.reduce((sum, digit) => sum + Number(digit), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAssociationButtons(associations) {
|
|
||||||
if (!associations || typeof associations !== "object") {
|
|
||||||
return "<div class=\"planet-text\">--</div>";
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttons = [];
|
|
||||||
|
|
||||||
if (associations.planetId) {
|
|
||||||
buttons.push(
|
|
||||||
`<button class="alpha-nav-btn" data-nav="planet" data-planet-id="${associations.planetId}">${planetLabel(associations.planetId)} -></button>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (associations.zodiacSignId) {
|
|
||||||
buttons.push(
|
|
||||||
`<button class="alpha-nav-btn" data-nav="zodiac" data-sign-id="${associations.zodiacSignId}">${zodiacLabel(associations.zodiacSignId)} -></button>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Number.isFinite(Number(associations.numberValue))) {
|
|
||||||
const rawNumber = Math.trunc(Number(associations.numberValue));
|
|
||||||
if (rawNumber >= 0) {
|
|
||||||
const numberValue = computeDigitalRoot(rawNumber);
|
|
||||||
if (numberValue != null) {
|
|
||||||
const label = rawNumber === numberValue
|
|
||||||
? `Number ${numberValue}`
|
|
||||||
: `Number ${numberValue} (from ${rawNumber})`;
|
|
||||||
buttons.push(
|
|
||||||
`<button class="alpha-nav-btn" data-nav="number" data-number-value="${numberValue}">${label} -></button>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (associations.tarotCard) {
|
|
||||||
const trumpNumber = resolveTarotTrumpNumber(associations.tarotCard);
|
|
||||||
const explicitTrumpNumber = Number(associations.tarotTrumpNumber);
|
|
||||||
const tarotTrumpNumber = Number.isFinite(explicitTrumpNumber) ? explicitTrumpNumber : trumpNumber;
|
|
||||||
const tarotLabel = getDisplayTarotName(associations.tarotCard, tarotTrumpNumber);
|
|
||||||
buttons.push(
|
|
||||||
`<button class="alpha-nav-btn" data-nav="tarot-card" data-card-name="${associations.tarotCard}" data-trump-number="${tarotTrumpNumber ?? ""}">${tarotLabel} -></button>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (associations.godId || associations.godName) {
|
|
||||||
const label = godLabel(associations.godId, associations.godName);
|
|
||||||
buttons.push(
|
|
||||||
`<button class="alpha-nav-btn" data-nav="god" data-god-id="${associations.godId || ""}" data-god-name="${associations.godName || label}">${label} -></button>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (associations.hebrewLetterId) {
|
|
||||||
buttons.push(
|
|
||||||
`<button class="alpha-nav-btn" data-nav="alphabet" data-alphabet="hebrew" data-hebrew-letter-id="${associations.hebrewLetterId}">${hebrewLabel(associations.hebrewLetterId)} -></button>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (associations.kabbalahPathNumber != null) {
|
|
||||||
buttons.push(
|
|
||||||
`<button class="alpha-nav-btn" data-nav="kabbalah" data-path-no="${associations.kabbalahPathNumber}">Path ${associations.kabbalahPathNumber} -></button>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (associations.iChingPlanetaryInfluence) {
|
|
||||||
buttons.push(
|
|
||||||
`<button class="alpha-nav-btn" data-nav="iching" data-planetary-influence="${associations.iChingPlanetaryInfluence}">I Ching - ${associations.iChingPlanetaryInfluence} -></button>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!buttons.length) {
|
|
||||||
return "<div class=\"planet-text\">--</div>";
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<div class="alpha-nav-btns">${buttons.join("")}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function associationSearchText(associations) {
|
|
||||||
if (!associations || typeof associations !== "object") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const tarotAliases = associations.tarotCard && typeof getTarotCardSearchAliases === "function"
|
|
||||||
? getTarotCardSearchAliases(associations.tarotCard, { trumpNumber: associations.tarotTrumpNumber })
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return [
|
|
||||||
associations.planetId,
|
|
||||||
associations.zodiacSignId,
|
|
||||||
associations.numberValue,
|
|
||||||
associations.tarotCard,
|
|
||||||
associations.tarotTrumpNumber,
|
|
||||||
...tarotAliases,
|
|
||||||
associations.godId,
|
|
||||||
associations.godName,
|
|
||||||
associations.hebrewLetterId,
|
|
||||||
associations.kabbalahPathNumber,
|
|
||||||
associations.iChingPlanetaryInfluence
|
|
||||||
].filter(Boolean).join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function holidaySearchText(holiday) {
|
|
||||||
return normalizeSearchValue([
|
|
||||||
holiday?.name,
|
|
||||||
holiday?.kind,
|
|
||||||
holiday?.date,
|
|
||||||
holiday?.dateRange,
|
|
||||||
holiday?.dateText,
|
|
||||||
holiday?.monthDayStart,
|
|
||||||
holiday?.calendarId,
|
|
||||||
holiday?.description,
|
|
||||||
associationSearchText(holiday?.associations)
|
|
||||||
].filter(Boolean).join(" "));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedHoliday() {
|
function getSelectedHoliday() {
|
||||||
@@ -764,7 +184,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function filterBySource(holidays) {
|
function filterBySource(holidays) {
|
||||||
const source = normalizeSourceFilter(state.selectedSource);
|
const source = holidayDataUi.normalizeSourceFilter(state.selectedSource);
|
||||||
if (source === "all") {
|
if (source === "all") {
|
||||||
return [...holidays];
|
return [...holidays];
|
||||||
}
|
}
|
||||||
@@ -773,7 +193,7 @@
|
|||||||
|
|
||||||
function syncControls(elements) {
|
function syncControls(elements) {
|
||||||
if (elements.sourceSelectEl) {
|
if (elements.sourceSelectEl) {
|
||||||
elements.sourceSelectEl.value = normalizeSourceFilter(state.selectedSource);
|
elements.sourceSelectEl.value = holidayDataUi.normalizeSourceFilter(state.selectedSource);
|
||||||
}
|
}
|
||||||
if (elements.yearInputEl) {
|
if (elements.yearInputEl) {
|
||||||
elements.yearInputEl.value = String(state.selectedYear);
|
elements.yearInputEl.value = String(state.selectedYear);
|
||||||
@@ -787,99 +207,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderList(elements) {
|
function renderList(elements) {
|
||||||
const { listEl, countEl } = elements;
|
holidayRenderUi.renderList(getRenderContext(elements));
|
||||||
if (!listEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
listEl.innerHTML = "";
|
|
||||||
|
|
||||||
state.filteredHolidays.forEach((holiday) => {
|
|
||||||
const isSelected = holiday.id === state.selectedHolidayId;
|
|
||||||
const itemEl = document.createElement("div");
|
|
||||||
itemEl.className = `planet-list-item${isSelected ? " is-selected" : ""}`;
|
|
||||||
itemEl.setAttribute("role", "option");
|
|
||||||
itemEl.setAttribute("aria-selected", isSelected ? "true" : "false");
|
|
||||||
itemEl.dataset.holidayId = holiday.id;
|
|
||||||
|
|
||||||
const sourceCalendar = calendarLabel(holiday.calendarId);
|
|
||||||
const sourceMonth = monthLabelForCalendar(holiday.calendarId, holiday.monthId);
|
|
||||||
const sourceDate = holiday?.dateText || holiday?.date || holiday?.dateRange || "--";
|
|
||||||
|
|
||||||
itemEl.innerHTML = `
|
|
||||||
<div class="planet-list-name">${holiday?.name || holiday?.id || "Holiday"}</div>
|
|
||||||
<div class="planet-list-meta">${sourceCalendar} - ${sourceMonth} - ${sourceDate}</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
itemEl.addEventListener("click", () => {
|
|
||||||
selectByHolidayId(holiday.id, elements);
|
|
||||||
});
|
|
||||||
|
|
||||||
listEl.appendChild(itemEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (countEl) {
|
|
||||||
const sourceFiltered = filterBySource(state.holidays);
|
|
||||||
const activeFilter = normalizeSourceFilter(state.selectedSource);
|
|
||||||
const sourceLabel = activeFilter === "all"
|
|
||||||
? ""
|
|
||||||
: ` (${calendarLabel(activeFilter)})`;
|
|
||||||
countEl.textContent = state.searchQuery
|
|
||||||
? `${state.filteredHolidays.length} of ${sourceFiltered.length} holidays${sourceLabel}`
|
|
||||||
: `${sourceFiltered.length} holidays${sourceLabel}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHolidayDetail(holiday) {
|
function renderHolidayDetail(holiday) {
|
||||||
const gregorianDate = resolveHolidayGregorianDate(holiday);
|
return holidayRenderUi.renderHolidayDetail(holiday, getRenderContext());
|
||||||
const gregorianRef = formatGregorianReferenceDate(gregorianDate);
|
|
||||||
const hebrewRef = formatCalendarDateFromGregorian(gregorianDate, "hebrew");
|
|
||||||
const islamicRef = formatCalendarDateFromGregorian(gregorianDate, "islamic");
|
|
||||||
const confidence = String(holiday?.conversionConfidence || holiday?.datePrecision || "approximate").toLowerCase();
|
|
||||||
const confidenceLabel = (!(gregorianDate instanceof Date) || Number.isNaN(gregorianDate.getTime()))
|
|
||||||
? "unresolved"
|
|
||||||
: (confidence === "exact" ? "exact" : "approximate");
|
|
||||||
const monthName = monthLabelForCalendar(holiday?.calendarId, holiday?.monthId);
|
|
||||||
const holidayDate = holiday?.dateText || holiday?.date || holiday?.dateRange || "--";
|
|
||||||
const sourceMonthLink = holiday?.monthId
|
|
||||||
? `<div class="alpha-nav-btns"><button class="alpha-nav-btn" data-nav="calendar-month" data-calendar-id="${holiday.calendarId || ""}" data-month-id="${holiday.monthId}">Open ${calendarLabel(holiday?.calendarId)} ${monthName} -></button></div>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="planet-meta-grid">
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>Holiday Facts</strong>
|
|
||||||
<div class="planet-text">
|
|
||||||
<dl class="alpha-dl">
|
|
||||||
<dt>Source Calendar</dt><dd>${calendarLabel(holiday?.calendarId)}</dd>
|
|
||||||
<dt>Source Month</dt><dd>${monthName}</dd>
|
|
||||||
<dt>Source Date</dt><dd>${holidayDate}</dd>
|
|
||||||
<dt>Reference Year</dt><dd>${state.selectedYear}</dd>
|
|
||||||
<dt>Conversion</dt><dd>${confidenceLabel}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>Cross-Calendar Dates</strong>
|
|
||||||
<div class="planet-text">
|
|
||||||
<dl class="alpha-dl">
|
|
||||||
<dt>Gregorian</dt><dd>${gregorianRef}</dd>
|
|
||||||
<dt>Hebrew</dt><dd>${hebrewRef}</dd>
|
|
||||||
<dt>Islamic</dt><dd>${islamicRef}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>Description</strong>
|
|
||||||
<div class="planet-text">${holiday?.description || "--"}</div>
|
|
||||||
${sourceMonthLink}
|
|
||||||
</div>
|
|
||||||
<div class="planet-meta-card">
|
|
||||||
<strong>Associations</strong>
|
|
||||||
${buildAssociationButtons(holiday?.associations)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDetail(elements) {
|
function renderDetail(elements) {
|
||||||
@@ -897,7 +229,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
detailNameEl.textContent = holiday?.name || holiday?.id || "Holiday";
|
detailNameEl.textContent = holiday?.name || holiday?.id || "Holiday";
|
||||||
detailSubEl.textContent = `${calendarLabel(holiday?.calendarId)} - ${monthLabelForCalendar(holiday?.calendarId, holiday?.monthId)}`;
|
detailSubEl.textContent = `${holidayDataUi.calendarLabel(holiday?.calendarId)} - ${holidayDataUi.monthLabelForCalendar(state.calendarData, holiday?.calendarId, holiday?.monthId)}`;
|
||||||
detailBodyEl.innerHTML = renderHolidayDetail(holiday);
|
detailBodyEl.innerHTML = renderHolidayDetail(holiday);
|
||||||
attachNavHandlers(detailBodyEl);
|
attachNavHandlers(detailBodyEl);
|
||||||
}
|
}
|
||||||
@@ -905,7 +237,7 @@
|
|||||||
function applyFilters(elements) {
|
function applyFilters(elements) {
|
||||||
const sourceFiltered = filterBySource(state.holidays);
|
const sourceFiltered = filterBySource(state.holidays);
|
||||||
state.filteredHolidays = state.searchQuery
|
state.filteredHolidays = state.searchQuery
|
||||||
? sourceFiltered.filter((holiday) => holidaySearchText(holiday).includes(state.searchQuery))
|
? sourceFiltered.filter((holiday) => holidayRenderUi.holidaySearchText(holiday, getRenderContext()).includes(state.searchQuery))
|
||||||
: sourceFiltered;
|
: sourceFiltered;
|
||||||
|
|
||||||
if (!state.filteredHolidays.some((holiday) => holiday.id === state.selectedHolidayId)) {
|
if (!state.filteredHolidays.some((holiday) => holiday.id === state.selectedHolidayId)) {
|
||||||
@@ -924,12 +256,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const targetCalendar = String(target.calendarId || "").trim().toLowerCase();
|
const targetCalendar = String(target.calendarId || "").trim().toLowerCase();
|
||||||
const activeFilter = normalizeSourceFilter(state.selectedSource);
|
const activeFilter = holidayDataUi.normalizeSourceFilter(state.selectedSource);
|
||||||
if (activeFilter !== "all" && activeFilter !== targetCalendar) {
|
if (activeFilter !== "all" && activeFilter !== targetCalendar) {
|
||||||
state.selectedSource = targetCalendar || "all";
|
state.selectedSource = targetCalendar || "all";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.searchQuery && !holidaySearchText(target).includes(state.searchQuery)) {
|
if (state.searchQuery && !holidayRenderUi.holidaySearchText(target, getRenderContext()).includes(state.searchQuery)) {
|
||||||
state.searchQuery = "";
|
state.searchQuery = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -941,7 +273,7 @@
|
|||||||
function bindControls(elements) {
|
function bindControls(elements) {
|
||||||
if (elements.sourceSelectEl) {
|
if (elements.sourceSelectEl) {
|
||||||
elements.sourceSelectEl.addEventListener("change", () => {
|
elements.sourceSelectEl.addEventListener("change", () => {
|
||||||
state.selectedSource = normalizeSourceFilter(elements.sourceSelectEl.value);
|
state.selectedSource = holidayDataUi.normalizeSourceFilter(elements.sourceSelectEl.value);
|
||||||
applyFilters(elements);
|
applyFilters(elements);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1073,19 +405,13 @@
|
|||||||
|
|
||||||
state.referenceData = referenceData;
|
state.referenceData = referenceData;
|
||||||
state.magickDataset = magickDataset || null;
|
state.magickDataset = magickDataset || null;
|
||||||
state.planetsById = buildPlanetMap(referenceData.planets);
|
state.planetsById = holidayDataUi.buildPlanetMap(referenceData.planets);
|
||||||
state.signsById = buildSignsMap(referenceData.signs);
|
state.signsById = holidayDataUi.buildSignsMap(referenceData.signs);
|
||||||
state.godsById = buildGodsMap(state.magickDataset);
|
state.godsById = holidayDataUi.buildGodsMap(state.magickDataset);
|
||||||
state.hebrewById = buildHebrewMap(state.magickDataset);
|
state.hebrewById = holidayDataUi.buildHebrewMap(state.magickDataset);
|
||||||
|
|
||||||
state.calendarData = {
|
state.calendarData = holidayDataUi.buildCalendarData(referenceData);
|
||||||
gregorian: Array.isArray(referenceData.calendarMonths) ? referenceData.calendarMonths : [],
|
state.holidays = holidayDataUi.buildAllHolidays(state.referenceData);
|
||||||
hebrew: Array.isArray(referenceData.hebrewCalendar?.months) ? referenceData.hebrewCalendar.months : [],
|
|
||||||
islamic: Array.isArray(referenceData.islamicCalendar?.months) ? referenceData.islamicCalendar.months : [],
|
|
||||||
"wheel-of-year": Array.isArray(referenceData.wheelOfYear?.months) ? referenceData.wheelOfYear.months : []
|
|
||||||
};
|
|
||||||
|
|
||||||
state.holidays = buildAllHolidays();
|
|
||||||
if (!state.selectedHolidayId || !state.holidays.some((holiday) => holiday.id === state.selectedHolidayId)) {
|
if (!state.selectedHolidayId || !state.holidays.some((holiday) => holiday.id === state.selectedHolidayId)) {
|
||||||
state.selectedHolidayId = state.holidays[0]?.id || null;
|
state.selectedHolidayId = state.holidays[0]?.id || null;
|
||||||
}
|
}
|
||||||
|
|||||||
253
app/ui-iching-references.js
Normal file
253
app/ui-iching-references.js
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
/* ui-iching-references.js — Month reference builders for the I Ching section */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function buildMonthReferencesByHexagram(context) {
|
||||||
|
const {
|
||||||
|
referenceData,
|
||||||
|
hexagrams,
|
||||||
|
normalizePlanetInfluence,
|
||||||
|
resolveAssociationPlanetInfluence
|
||||||
|
} = context || {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof normalizePlanetInfluence !== "function"
|
||||||
|
|| typeof resolveAssociationPlanetInfluence !== "function"
|
||||||
|
) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = new Map();
|
||||||
|
const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [];
|
||||||
|
const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
||||||
|
const monthById = new Map(months.map((month) => [month.id, month]));
|
||||||
|
|
||||||
|
function parseMonthDayToken(value) {
|
||||||
|
const text = String(value || "").trim();
|
||||||
|
const match = text.match(/^(\d{1,2})-(\d{1,2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthNo = Number(match[1]);
|
||||||
|
const dayNo = Number(match[2]);
|
||||||
|
if (!Number.isInteger(monthNo) || !Number.isInteger(dayNo) || monthNo < 1 || monthNo > 12 || dayNo < 1 || dayNo > 31) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { month: monthNo, day: dayNo };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMonthDayTokensFromText(value) {
|
||||||
|
const text = String(value || "");
|
||||||
|
const matches = [...text.matchAll(/(\d{1,2})-(\d{1,2})/g)];
|
||||||
|
return matches
|
||||||
|
.map((match) => ({ month: Number(match[1]), day: Number(match[2]) }))
|
||||||
|
.filter((token) => Number.isInteger(token.month) && Number.isInteger(token.day) && token.month >= 1 && token.month <= 12 && token.day >= 1 && token.day <= 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDateToken(token, year) {
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Date(year, token.month - 1, token.day, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMonthDayRangeByMonth(startToken, endToken) {
|
||||||
|
const startDate = toDateToken(startToken, 2025);
|
||||||
|
const endBase = toDateToken(endToken, 2025);
|
||||||
|
if (!startDate || !endBase) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapsYear = endBase.getTime() < startDate.getTime();
|
||||||
|
const endDate = wrapsYear ? toDateToken(endToken, 2026) : endBase;
|
||||||
|
if (!endDate) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = [];
|
||||||
|
let cursor = new Date(startDate);
|
||||||
|
while (cursor.getTime() <= endDate.getTime()) {
|
||||||
|
const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0);
|
||||||
|
const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate;
|
||||||
|
|
||||||
|
segments.push({
|
||||||
|
monthNo: cursor.getMonth() + 1,
|
||||||
|
startDay: cursor.getDate(),
|
||||||
|
endDay: segmentEnd.getDate()
|
||||||
|
});
|
||||||
|
|
||||||
|
cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenToString(monthNo, dayNo) {
|
||||||
|
return `${String(monthNo).padStart(2, "0")}-${String(dayNo).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRangeLabel(monthName, startDay, endDay) {
|
||||||
|
if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) {
|
||||||
|
return monthName;
|
||||||
|
}
|
||||||
|
if (startDay === endDay) {
|
||||||
|
return `${monthName} ${startDay}`;
|
||||||
|
}
|
||||||
|
return `${monthName} ${startDay}-${endDay}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRangeForMonth(month, options = {}) {
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
const monthStart = parseMonthDayToken(month?.start);
|
||||||
|
const monthEnd = parseMonthDayToken(month?.end);
|
||||||
|
if (!Number.isFinite(monthOrder) || !monthStart || !monthEnd) {
|
||||||
|
return {
|
||||||
|
startToken: String(month?.start || "").trim() || null,
|
||||||
|
endToken: String(month?.end || "").trim() || null,
|
||||||
|
label: month?.name || month?.id || "",
|
||||||
|
isFullMonth: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let startToken = parseMonthDayToken(options.startToken);
|
||||||
|
let endToken = parseMonthDayToken(options.endToken);
|
||||||
|
|
||||||
|
if (!startToken || !endToken) {
|
||||||
|
const tokens = parseMonthDayTokensFromText(options.rawDateText);
|
||||||
|
if (tokens.length >= 2) {
|
||||||
|
startToken = tokens[0];
|
||||||
|
endToken = tokens[1];
|
||||||
|
} else if (tokens.length === 1) {
|
||||||
|
startToken = tokens[0];
|
||||||
|
endToken = tokens[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startToken || !endToken) {
|
||||||
|
startToken = monthStart;
|
||||||
|
endToken = monthEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = splitMonthDayRangeByMonth(startToken, endToken);
|
||||||
|
const segment = segments.find((entry) => entry.monthNo === monthOrder) || null;
|
||||||
|
|
||||||
|
const useStart = segment ? { month: monthOrder, day: segment.startDay } : startToken;
|
||||||
|
const useEnd = segment ? { month: monthOrder, day: segment.endDay } : endToken;
|
||||||
|
const startText = tokenToString(useStart.month, useStart.day);
|
||||||
|
const endText = tokenToString(useEnd.month, useEnd.day);
|
||||||
|
const isFullMonth = startText === month.start && endText === month.end;
|
||||||
|
|
||||||
|
return {
|
||||||
|
startToken: startText,
|
||||||
|
endToken: endText,
|
||||||
|
label: isFullMonth
|
||||||
|
? (month.name || month.id)
|
||||||
|
: formatRangeLabel(month.name || month.id, useStart.day, useEnd.day),
|
||||||
|
isFullMonth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushRef(hexagramNumber, month, options = {}) {
|
||||||
|
if (!Number.isFinite(hexagramNumber) || !month?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.has(hexagramNumber)) {
|
||||||
|
map.set(hexagramNumber, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = map.get(hexagramNumber);
|
||||||
|
const range = resolveRangeForMonth(month, options);
|
||||||
|
const rowKey = `${month.id}|${range.startToken || ""}|${range.endToken || ""}`;
|
||||||
|
if (rows.some((entry) => entry.key === rowKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
id: month.id,
|
||||||
|
name: month.name || month.id,
|
||||||
|
order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999,
|
||||||
|
label: range.label,
|
||||||
|
startToken: range.startToken,
|
||||||
|
endToken: range.endToken,
|
||||||
|
isFullMonth: range.isFullMonth,
|
||||||
|
key: rowKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRefs(associations, month, options = {}) {
|
||||||
|
const associationInfluence = resolveAssociationPlanetInfluence(associations);
|
||||||
|
if (!associationInfluence) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(hexagrams || []).forEach((hexagram) => {
|
||||||
|
const hexagramInfluence = normalizePlanetInfluence(hexagram?.planetaryInfluence);
|
||||||
|
if (hexagramInfluence && hexagramInfluence === associationInfluence) {
|
||||||
|
pushRef(hexagram.number, month, options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
months.forEach((month) => {
|
||||||
|
collectRefs(month?.associations, month);
|
||||||
|
const events = Array.isArray(month?.events) ? month.events : [];
|
||||||
|
events.forEach((event) => {
|
||||||
|
collectRefs(event?.associations, month, {
|
||||||
|
rawDateText: event?.dateRange || event?.date || ""
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
holidays.forEach((holiday) => {
|
||||||
|
const month = monthById.get(holiday?.monthId);
|
||||||
|
if (!month) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
collectRefs(holiday?.associations, month, {
|
||||||
|
rawDateText: holiday?.dateRange || holiday?.date || ""
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
map.forEach((rows, key) => {
|
||||||
|
const preciseMonthIds = new Set(
|
||||||
|
rows
|
||||||
|
.filter((entry) => !entry.isFullMonth)
|
||||||
|
.map((entry) => entry.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const filtered = rows.filter((entry) => {
|
||||||
|
if (!entry.isFullMonth) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !preciseMonthIds.has(entry.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
filtered.sort((left, right) => {
|
||||||
|
if (left.order !== right.order) {
|
||||||
|
return left.order - right.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startLeft = parseMonthDayToken(left.startToken);
|
||||||
|
const startRight = parseMonthDayToken(right.startToken);
|
||||||
|
const dayLeft = startLeft ? startLeft.day : 999;
|
||||||
|
const dayRight = startRight ? startRight.day : 999;
|
||||||
|
if (dayLeft !== dayRight) {
|
||||||
|
return dayLeft - dayRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(left.label || left.name || "").localeCompare(String(right.label || right.name || ""));
|
||||||
|
});
|
||||||
|
|
||||||
|
map.set(key, filtered);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.IChingReferenceBuilders = {
|
||||||
|
buildMonthReferencesByHexagram
|
||||||
|
};
|
||||||
|
})();
|
||||||
253
app/ui-iching.js
253
app/ui-iching.js
@@ -1,4 +1,12 @@
|
|||||||
(function () {
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const iChingReferenceBuilders = window.IChingReferenceBuilders || {};
|
||||||
|
|
||||||
|
if (typeof iChingReferenceBuilders.buildMonthReferencesByHexagram !== "function") {
|
||||||
|
throw new Error("IChingReferenceBuilders module must load before ui-iching.js");
|
||||||
|
}
|
||||||
|
|
||||||
const { getTarotCardSearchAliases } = window.TarotCardImages || {};
|
const { getTarotCardSearchAliases } = window.TarotCardImages || {};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
@@ -87,237 +95,6 @@
|
|||||||
return normalizePlanetInfluence(ICHING_PLANET_BY_PLANET_ID[planetId]);
|
return normalizePlanetInfluence(ICHING_PLANET_BY_PLANET_ID[planetId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMonthReferencesByHexagram(referenceData, hexagrams) {
|
|
||||||
const map = new Map();
|
|
||||||
const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [];
|
|
||||||
const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
|
||||||
const monthById = new Map(months.map((month) => [month.id, month]));
|
|
||||||
|
|
||||||
function parseMonthDayToken(value) {
|
|
||||||
const text = String(value || "").trim();
|
|
||||||
const match = text.match(/^(\d{1,2})-(\d{1,2})$/);
|
|
||||||
if (!match) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthNo = Number(match[1]);
|
|
||||||
const dayNo = Number(match[2]);
|
|
||||||
if (!Number.isInteger(monthNo) || !Number.isInteger(dayNo) || monthNo < 1 || monthNo > 12 || dayNo < 1 || dayNo > 31) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { month: monthNo, day: dayNo };
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMonthDayTokensFromText(value) {
|
|
||||||
const text = String(value || "");
|
|
||||||
const matches = [...text.matchAll(/(\d{1,2})-(\d{1,2})/g)];
|
|
||||||
return matches
|
|
||||||
.map((match) => ({ month: Number(match[1]), day: Number(match[2]) }))
|
|
||||||
.filter((token) => Number.isInteger(token.month) && Number.isInteger(token.day) && token.month >= 1 && token.month <= 12 && token.day >= 1 && token.day <= 31);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toDateToken(token, year) {
|
|
||||||
if (!token) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Date(year, token.month - 1, token.day, 12, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitMonthDayRangeByMonth(startToken, endToken) {
|
|
||||||
const startDate = toDateToken(startToken, 2025);
|
|
||||||
const endBase = toDateToken(endToken, 2025);
|
|
||||||
if (!startDate || !endBase) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapsYear = endBase.getTime() < startDate.getTime();
|
|
||||||
const endDate = wrapsYear ? toDateToken(endToken, 2026) : endBase;
|
|
||||||
if (!endDate) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const segments = [];
|
|
||||||
let cursor = new Date(startDate);
|
|
||||||
while (cursor.getTime() <= endDate.getTime()) {
|
|
||||||
const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0);
|
|
||||||
const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate;
|
|
||||||
|
|
||||||
segments.push({
|
|
||||||
monthNo: cursor.getMonth() + 1,
|
|
||||||
startDay: cursor.getDate(),
|
|
||||||
endDay: segmentEnd.getDate()
|
|
||||||
});
|
|
||||||
|
|
||||||
cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return segments;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenToString(monthNo, dayNo) {
|
|
||||||
return `${String(monthNo).padStart(2, "0")}-${String(dayNo).padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatRangeLabel(monthName, startDay, endDay) {
|
|
||||||
if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) {
|
|
||||||
return monthName;
|
|
||||||
}
|
|
||||||
if (startDay === endDay) {
|
|
||||||
return `${monthName} ${startDay}`;
|
|
||||||
}
|
|
||||||
return `${monthName} ${startDay}-${endDay}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveRangeForMonth(month, options = {}) {
|
|
||||||
const monthOrder = Number(month?.order);
|
|
||||||
const monthStart = parseMonthDayToken(month?.start);
|
|
||||||
const monthEnd = parseMonthDayToken(month?.end);
|
|
||||||
if (!Number.isFinite(monthOrder) || !monthStart || !monthEnd) {
|
|
||||||
return {
|
|
||||||
startToken: String(month?.start || "").trim() || null,
|
|
||||||
endToken: String(month?.end || "").trim() || null,
|
|
||||||
label: month?.name || month?.id || "",
|
|
||||||
isFullMonth: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let startToken = parseMonthDayToken(options.startToken);
|
|
||||||
let endToken = parseMonthDayToken(options.endToken);
|
|
||||||
|
|
||||||
if (!startToken || !endToken) {
|
|
||||||
const tokens = parseMonthDayTokensFromText(options.rawDateText);
|
|
||||||
if (tokens.length >= 2) {
|
|
||||||
startToken = tokens[0];
|
|
||||||
endToken = tokens[1];
|
|
||||||
} else if (tokens.length === 1) {
|
|
||||||
startToken = tokens[0];
|
|
||||||
endToken = tokens[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!startToken || !endToken) {
|
|
||||||
startToken = monthStart;
|
|
||||||
endToken = monthEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
const segments = splitMonthDayRangeByMonth(startToken, endToken);
|
|
||||||
const segment = segments.find((entry) => entry.monthNo === monthOrder) || null;
|
|
||||||
|
|
||||||
const useStart = segment ? { month: monthOrder, day: segment.startDay } : startToken;
|
|
||||||
const useEnd = segment ? { month: monthOrder, day: segment.endDay } : endToken;
|
|
||||||
const startText = tokenToString(useStart.month, useStart.day);
|
|
||||||
const endText = tokenToString(useEnd.month, useEnd.day);
|
|
||||||
const isFullMonth = startText === month.start && endText === month.end;
|
|
||||||
|
|
||||||
return {
|
|
||||||
startToken: startText,
|
|
||||||
endToken: endText,
|
|
||||||
label: isFullMonth
|
|
||||||
? (month.name || month.id)
|
|
||||||
: formatRangeLabel(month.name || month.id, useStart.day, useEnd.day),
|
|
||||||
isFullMonth
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushRef(hexagramNumber, month, options = {}) {
|
|
||||||
if (!Number.isFinite(hexagramNumber) || !month?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map.has(hexagramNumber)) {
|
|
||||||
map.set(hexagramNumber, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = map.get(hexagramNumber);
|
|
||||||
const range = resolveRangeForMonth(month, options);
|
|
||||||
const rowKey = `${month.id}|${range.startToken || ""}|${range.endToken || ""}`;
|
|
||||||
if (rows.some((entry) => entry.key === rowKey)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rows.push({
|
|
||||||
id: month.id,
|
|
||||||
name: month.name || month.id,
|
|
||||||
order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999,
|
|
||||||
label: range.label,
|
|
||||||
startToken: range.startToken,
|
|
||||||
endToken: range.endToken,
|
|
||||||
isFullMonth: range.isFullMonth,
|
|
||||||
key: rowKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectRefs(associations, month, options = {}) {
|
|
||||||
const associationInfluence = resolveAssociationPlanetInfluence(associations);
|
|
||||||
if (!associationInfluence) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hexagrams.forEach((hexagram) => {
|
|
||||||
const hexagramInfluence = normalizePlanetInfluence(hexagram?.planetaryInfluence);
|
|
||||||
if (hexagramInfluence && hexagramInfluence === associationInfluence) {
|
|
||||||
pushRef(hexagram.number, month, options);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
months.forEach((month) => {
|
|
||||||
collectRefs(month?.associations, month);
|
|
||||||
const events = Array.isArray(month?.events) ? month.events : [];
|
|
||||||
events.forEach((event) => {
|
|
||||||
collectRefs(event?.associations, month, {
|
|
||||||
rawDateText: event?.dateRange || event?.date || ""
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
holidays.forEach((holiday) => {
|
|
||||||
const month = monthById.get(holiday?.monthId);
|
|
||||||
if (!month) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
collectRefs(holiday?.associations, month, {
|
|
||||||
rawDateText: holiday?.dateRange || holiday?.date || ""
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
map.forEach((rows, key) => {
|
|
||||||
const preciseMonthIds = new Set(
|
|
||||||
rows
|
|
||||||
.filter((entry) => !entry.isFullMonth)
|
|
||||||
.map((entry) => entry.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const filtered = rows.filter((entry) => {
|
|
||||||
if (!entry.isFullMonth) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !preciseMonthIds.has(entry.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
filtered.sort((left, right) => {
|
|
||||||
if (left.order !== right.order) {
|
|
||||||
return left.order - right.order;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startLeft = parseMonthDayToken(left.startToken);
|
|
||||||
const startRight = parseMonthDayToken(right.startToken);
|
|
||||||
const dayLeft = startLeft ? startLeft.day : 999;
|
|
||||||
const dayRight = startRight ? startRight.day : 999;
|
|
||||||
if (dayLeft !== dayRight) {
|
|
||||||
return dayLeft - dayRight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(left.label || left.name || "").localeCompare(String(right.label || right.name || ""));
|
|
||||||
});
|
|
||||||
|
|
||||||
map.set(key, filtered);
|
|
||||||
});
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBinaryPattern(value, expectedLength = 0) {
|
function getBinaryPattern(value, expectedLength = 0) {
|
||||||
const raw = String(value || "").trim();
|
const raw = String(value || "").trim();
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
@@ -723,7 +500,12 @@
|
|||||||
const elements = getElements();
|
const elements = getElements();
|
||||||
|
|
||||||
if (state.initialized) {
|
if (state.initialized) {
|
||||||
state.monthRefsByHexagramNumber = buildMonthReferencesByHexagram(referenceData, state.hexagrams);
|
state.monthRefsByHexagramNumber = iChingReferenceBuilders.buildMonthReferencesByHexagram({
|
||||||
|
referenceData,
|
||||||
|
hexagrams: state.hexagrams,
|
||||||
|
normalizePlanetInfluence,
|
||||||
|
resolveAssociationPlanetInfluence
|
||||||
|
});
|
||||||
const selected = state.hexagrams.find((hexagram) => hexagram.number === state.selectedNumber);
|
const selected = state.hexagrams.find((hexagram) => hexagram.number === state.selectedNumber);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
renderDetail(selected, elements);
|
renderDetail(selected, elements);
|
||||||
@@ -780,7 +562,12 @@
|
|||||||
.filter((entry) => Number.isFinite(entry.number))
|
.filter((entry) => Number.isFinite(entry.number))
|
||||||
.sort((a, b) => a.number - b.number);
|
.sort((a, b) => a.number - b.number);
|
||||||
|
|
||||||
state.monthRefsByHexagramNumber = buildMonthReferencesByHexagram(referenceData, state.hexagrams);
|
state.monthRefsByHexagramNumber = iChingReferenceBuilders.buildMonthReferencesByHexagram({
|
||||||
|
referenceData,
|
||||||
|
hexagrams: state.hexagrams,
|
||||||
|
normalizePlanetInfluence,
|
||||||
|
resolveAssociationPlanetInfluence
|
||||||
|
});
|
||||||
|
|
||||||
state.filteredHexagrams = [...state.hexagrams];
|
state.filteredHexagrams = [...state.hexagrams];
|
||||||
renderList(elements);
|
renderList(elements);
|
||||||
|
|||||||
388
app/ui-kabbalah-views.js
Normal file
388
app/ui-kabbalah-views.js
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function resolvePathTarotImage(path) {
|
||||||
|
const cardName = String(path?.tarot?.card || "").trim();
|
||||||
|
if (!cardName || typeof window.TarotCardImages?.resolveTarotCardImage !== "function") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.TarotCardImages.resolveTarotCardImage(cardName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSvgImageHref(imageEl) {
|
||||||
|
if (!(imageEl instanceof SVGElement)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(
|
||||||
|
imageEl.getAttribute("href")
|
||||||
|
|| imageEl.getAttributeNS("http://www.w3.org/1999/xlink", "href")
|
||||||
|
|| ""
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTarotLightboxForPath(path, fallbackSrc = "") {
|
||||||
|
const openLightbox = window.TarotUiLightbox?.open;
|
||||||
|
if (typeof openLightbox !== "function") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardName = String(path?.tarot?.card || "").trim();
|
||||||
|
const src = String(fallbackSrc || resolvePathTarotImage(path) || "").trim();
|
||||||
|
if (!src) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackLabel = Number.isFinite(Number(path?.pathNumber))
|
||||||
|
? `Path ${path.pathNumber} tarot card`
|
||||||
|
: "Path tarot card";
|
||||||
|
openLightbox(src, cardName || fallbackLabel);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPathLabel(context, path) {
|
||||||
|
const glyph = String(path?.hebrewLetter?.char || "").trim();
|
||||||
|
const pathNumber = Number(path?.pathNumber);
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
if (context.state.showPathLetters && glyph) {
|
||||||
|
parts.push(glyph);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.state.showPathNumbers && Number.isFinite(pathNumber)) {
|
||||||
|
parts.push(String(pathNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function svgEl(context, tag, attrs, text) {
|
||||||
|
const el = document.createElementNS(context.NS, tag);
|
||||||
|
for (const [key, value] of Object.entries(attrs || {})) {
|
||||||
|
el.setAttribute(key, String(value));
|
||||||
|
}
|
||||||
|
if (text != null) {
|
||||||
|
el.textContent = text;
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTreeSVG(context) {
|
||||||
|
const {
|
||||||
|
tree,
|
||||||
|
state,
|
||||||
|
NODE_POS,
|
||||||
|
SEPH_FILL,
|
||||||
|
DARK_TEXT,
|
||||||
|
DAAT,
|
||||||
|
PATH_LABEL_RADIUS,
|
||||||
|
PATH_LABEL_FONT_SIZE,
|
||||||
|
PATH_TAROT_WIDTH,
|
||||||
|
PATH_TAROT_HEIGHT,
|
||||||
|
PATH_LABEL_OFFSET_WITH_TAROT,
|
||||||
|
PATH_TAROT_OFFSET_WITH_LABEL,
|
||||||
|
PATH_TAROT_OFFSET_NO_LABEL,
|
||||||
|
R
|
||||||
|
} = context;
|
||||||
|
const svg = svgEl(context, "svg", {
|
||||||
|
viewBox: "0 0 240 470",
|
||||||
|
width: "100%",
|
||||||
|
role: "img",
|
||||||
|
"aria-label": "Kabbalah Tree of Life diagram",
|
||||||
|
class: "kab-svg"
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.appendChild(svgEl(context, "rect", {
|
||||||
|
x: 113, y: 30, width: 14, height: 420,
|
||||||
|
rx: 7, fill: "#ffffff07", "pointer-events": "none"
|
||||||
|
}));
|
||||||
|
svg.appendChild(svgEl(context, "rect", {
|
||||||
|
x: 33, y: 88, width: 14, height: 255,
|
||||||
|
rx: 7, fill: "#ff220010", "pointer-events": "none"
|
||||||
|
}));
|
||||||
|
svg.appendChild(svgEl(context, "rect", {
|
||||||
|
x: 193, y: 88, width: 14, height: 255,
|
||||||
|
rx: 7, fill: "#2244ff10", "pointer-events": "none"
|
||||||
|
}));
|
||||||
|
|
||||||
|
[
|
||||||
|
{ x: 198, y: 73, text: "Mercy", anchor: "middle" },
|
||||||
|
{ x: 120, y: 17, text: "Balance", anchor: "middle" },
|
||||||
|
{ x: 42, y: 73, text: "Severity", anchor: "middle" }
|
||||||
|
].forEach(({ x, y, text, anchor }) => {
|
||||||
|
svg.appendChild(svgEl(context, "text", {
|
||||||
|
x, y, "text-anchor": anchor, "dominant-baseline": "auto",
|
||||||
|
fill: "#42425a", "font-size": "6", "pointer-events": "none"
|
||||||
|
}, text));
|
||||||
|
});
|
||||||
|
|
||||||
|
tree.paths.forEach((path) => {
|
||||||
|
const [x1, y1] = NODE_POS[path.connects.from];
|
||||||
|
const [x2, y2] = NODE_POS[path.connects.to];
|
||||||
|
const mx = (x1 + x2) / 2;
|
||||||
|
const my = (y1 + y2) / 2;
|
||||||
|
const tarotImage = state.showPathTarotCards ? resolvePathTarotImage(path) : null;
|
||||||
|
const hasTarotImage = Boolean(tarotImage);
|
||||||
|
const pathLabel = getPathLabel(context, path);
|
||||||
|
const hasLabel = Boolean(pathLabel);
|
||||||
|
const labelY = hasTarotImage && hasLabel ? my - PATH_LABEL_OFFSET_WITH_TAROT : my;
|
||||||
|
|
||||||
|
svg.appendChild(svgEl(context, "line", {
|
||||||
|
x1, y1, x2, y2,
|
||||||
|
class: "kab-path-line",
|
||||||
|
"data-path": path.pathNumber,
|
||||||
|
stroke: "#3c3c5c",
|
||||||
|
"stroke-width": "1.5",
|
||||||
|
"pointer-events": "none"
|
||||||
|
}));
|
||||||
|
|
||||||
|
svg.appendChild(svgEl(context, "line", {
|
||||||
|
x1, y1, x2, y2,
|
||||||
|
class: "kab-path-hit",
|
||||||
|
"data-path": path.pathNumber,
|
||||||
|
stroke: "transparent",
|
||||||
|
"stroke-width": String(12 * context.PATH_MARKER_SCALE),
|
||||||
|
role: "button",
|
||||||
|
tabindex: "0",
|
||||||
|
"aria-label": `Path ${path.pathNumber}: ${path.hebrewLetter?.transliteration || ""} — ${path.tarot?.card || ""}`,
|
||||||
|
style: "cursor:pointer"
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (hasLabel) {
|
||||||
|
svg.appendChild(svgEl(context, "circle", {
|
||||||
|
cx: mx, cy: labelY, r: PATH_LABEL_RADIUS.toFixed(2),
|
||||||
|
fill: "#0d0d1c", opacity: "0.82",
|
||||||
|
"pointer-events": "none"
|
||||||
|
}));
|
||||||
|
|
||||||
|
svg.appendChild(svgEl(context, "text", {
|
||||||
|
x: mx, y: labelY + 1,
|
||||||
|
"text-anchor": "middle",
|
||||||
|
"dominant-baseline": "middle",
|
||||||
|
class: "kab-path-lbl",
|
||||||
|
"data-path": path.pathNumber,
|
||||||
|
fill: "#a8a8e0",
|
||||||
|
"font-size": PATH_LABEL_FONT_SIZE.toFixed(2),
|
||||||
|
"pointer-events": "none"
|
||||||
|
}, pathLabel));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasTarotImage) {
|
||||||
|
const tarotY = hasLabel
|
||||||
|
? my + PATH_TAROT_OFFSET_WITH_LABEL
|
||||||
|
: my - PATH_TAROT_OFFSET_NO_LABEL;
|
||||||
|
svg.appendChild(svgEl(context, "image", {
|
||||||
|
href: tarotImage,
|
||||||
|
x: (mx - (PATH_TAROT_WIDTH / 2)).toFixed(2),
|
||||||
|
y: tarotY.toFixed(2),
|
||||||
|
width: PATH_TAROT_WIDTH.toFixed(2),
|
||||||
|
height: PATH_TAROT_HEIGHT.toFixed(2),
|
||||||
|
preserveAspectRatio: "xMidYMid meet",
|
||||||
|
class: "kab-path-tarot",
|
||||||
|
"data-path": path.pathNumber,
|
||||||
|
role: "button",
|
||||||
|
tabindex: "0",
|
||||||
|
"aria-label": `Path ${path.pathNumber} Tarot card ${path.tarot?.card || ""}`,
|
||||||
|
style: "cursor:pointer"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.appendChild(svgEl(context, "circle", {
|
||||||
|
cx: DAAT[0], cy: DAAT[1], r: "9",
|
||||||
|
fill: "none", stroke: "#3c3c5c",
|
||||||
|
"stroke-dasharray": "3 2", "stroke-width": "1",
|
||||||
|
"pointer-events": "none"
|
||||||
|
}));
|
||||||
|
svg.appendChild(svgEl(context, "text", {
|
||||||
|
x: DAAT[0] + 13, y: DAAT[1] + 1,
|
||||||
|
"text-anchor": "start", "dominant-baseline": "middle",
|
||||||
|
fill: "#3c3c5c", "font-size": "6.5", "pointer-events": "none"
|
||||||
|
}, "Da'at"));
|
||||||
|
|
||||||
|
tree.sephiroth.forEach((seph) => {
|
||||||
|
const [cx, cy] = NODE_POS[seph.number];
|
||||||
|
const fill = SEPH_FILL[seph.number] || "#555";
|
||||||
|
const isLeft = cx < 80;
|
||||||
|
const isMid = cx === 120;
|
||||||
|
|
||||||
|
svg.appendChild(svgEl(context, "circle", {
|
||||||
|
cx, cy, r: "16",
|
||||||
|
fill, opacity: "0.12",
|
||||||
|
class: "kab-node-glow",
|
||||||
|
"data-sephira": seph.number,
|
||||||
|
"pointer-events": "none"
|
||||||
|
}));
|
||||||
|
|
||||||
|
svg.appendChild(svgEl(context, "circle", {
|
||||||
|
cx, cy, r: R,
|
||||||
|
fill, stroke: "#00000040", "stroke-width": "1",
|
||||||
|
class: "kab-node",
|
||||||
|
"data-sephira": seph.number,
|
||||||
|
role: "button",
|
||||||
|
tabindex: "0",
|
||||||
|
"aria-label": `Sephira ${seph.number}: ${seph.name}`,
|
||||||
|
style: "cursor:pointer"
|
||||||
|
}));
|
||||||
|
|
||||||
|
svg.appendChild(svgEl(context, "text", {
|
||||||
|
x: cx, y: cy + 0.5,
|
||||||
|
"text-anchor": "middle", "dominant-baseline": "middle",
|
||||||
|
fill: DARK_TEXT.has(seph.number) ? "#111" : "#fff",
|
||||||
|
"font-size": "8", "font-weight": "bold",
|
||||||
|
"pointer-events": "none"
|
||||||
|
}, String(seph.number)));
|
||||||
|
|
||||||
|
const lx = isLeft ? cx - R - 4 : cx + R + 4;
|
||||||
|
svg.appendChild(svgEl(context, "text", {
|
||||||
|
x: isMid ? cx : lx,
|
||||||
|
y: isMid ? cy + R + 8 : cy,
|
||||||
|
"text-anchor": isMid ? "middle" : (isLeft ? "end" : "start"),
|
||||||
|
"dominant-baseline": isMid ? "auto" : "middle",
|
||||||
|
fill: "#c0c0d4",
|
||||||
|
"font-size": "7.5", "pointer-events": "none",
|
||||||
|
class: "kab-node-lbl"
|
||||||
|
}, seph.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindTreeInteractions(context, svg) {
|
||||||
|
const { tree, elements, renderSephiraDetail, renderPathDetail } = context;
|
||||||
|
svg.addEventListener("click", (event) => {
|
||||||
|
const clickTarget = event.target instanceof Element ? event.target : null;
|
||||||
|
const sephNum = clickTarget?.dataset?.sephira;
|
||||||
|
const pathNum = clickTarget?.dataset?.path;
|
||||||
|
|
||||||
|
if (pathNum != null && clickTarget?.classList?.contains("kab-path-tarot")) {
|
||||||
|
const path = tree.paths.find((entry) => entry.pathNumber === Number(pathNum));
|
||||||
|
if (path) {
|
||||||
|
openTarotLightboxForPath(path, getSvgImageHref(clickTarget));
|
||||||
|
renderPathDetail(path, tree, elements);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sephNum != null) {
|
||||||
|
const seph = tree.sephiroth.find((entry) => entry.number === Number(sephNum));
|
||||||
|
if (seph) {
|
||||||
|
renderSephiraDetail(seph, tree, elements);
|
||||||
|
}
|
||||||
|
} else if (pathNum != null) {
|
||||||
|
const path = tree.paths.find((entry) => entry.pathNumber === Number(pathNum));
|
||||||
|
if (path) {
|
||||||
|
renderPathDetail(path, tree, elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.querySelectorAll(".kab-path-hit, .kab-path-tarot").forEach((element) => {
|
||||||
|
element.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
const path = tree.paths.find((entry) => entry.pathNumber === Number(element.dataset.path));
|
||||||
|
if (path) {
|
||||||
|
if (element.classList.contains("kab-path-tarot")) {
|
||||||
|
openTarotLightboxForPath(path, getSvgImageHref(element));
|
||||||
|
}
|
||||||
|
renderPathDetail(path, tree, elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.querySelectorAll(".kab-node").forEach((element) => {
|
||||||
|
element.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
const seph = tree.sephiroth.find((entry) => entry.number === Number(element.dataset.sephira));
|
||||||
|
if (seph) {
|
||||||
|
renderSephiraDetail(seph, tree, elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindRoseCrossInteractions(context, svg, roseElements) {
|
||||||
|
const { tree, renderPathDetail } = context;
|
||||||
|
if (!svg || !roseElements?.detailBodyEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPathFromTarget = (targetEl) => {
|
||||||
|
if (!(targetEl instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const petal = targetEl.closest(".kab-rose-petal[data-path]");
|
||||||
|
if (!(petal instanceof SVGElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathNumber = Number(petal.dataset.path);
|
||||||
|
if (!Number.isFinite(pathNumber)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = tree.paths.find((entry) => entry.pathNumber === pathNumber);
|
||||||
|
if (path) {
|
||||||
|
renderPathDetail(path, tree, roseElements);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
svg.addEventListener("click", (event) => {
|
||||||
|
openPathFromTarget(event.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.querySelectorAll(".kab-rose-petal[data-path]").forEach((petal) => {
|
||||||
|
petal.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
openPathFromTarget(petal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRoseCross(context) {
|
||||||
|
const { state, elements, getRoseDetailElements } = context;
|
||||||
|
if (!state.tree || !elements?.roseCrossContainerEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roseElements = getRoseDetailElements(elements);
|
||||||
|
if (!roseElements?.detailBodyEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roseBuilder = window.KabbalahRosicrucianCross?.buildRosicrucianCrossSVG;
|
||||||
|
if (typeof roseBuilder !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roseSvg = roseBuilder(state.tree);
|
||||||
|
elements.roseCrossContainerEl.innerHTML = "";
|
||||||
|
elements.roseCrossContainerEl.appendChild(roseSvg);
|
||||||
|
bindRoseCrossInteractions(context, roseSvg, roseElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTree(context) {
|
||||||
|
const { state, elements } = context;
|
||||||
|
if (!state.tree || !elements?.treeContainerEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = buildTreeSVG(context);
|
||||||
|
elements.treeContainerEl.innerHTML = "";
|
||||||
|
elements.treeContainerEl.appendChild(svg);
|
||||||
|
bindTreeInteractions(context, svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.KabbalahViewsUi = {
|
||||||
|
renderTree,
|
||||||
|
renderRoseCross
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -62,6 +62,14 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const kabbalahDetailUi = window.KabbalahDetailUi || {};
|
const kabbalahDetailUi = window.KabbalahDetailUi || {};
|
||||||
|
const kabbalahViewsUi = window.KabbalahViewsUi || {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof kabbalahViewsUi.renderTree !== "function"
|
||||||
|
|| typeof kabbalahViewsUi.renderRoseCross !== "function"
|
||||||
|
) {
|
||||||
|
throw new Error("KabbalahViewsUi module must load before ui-kabbalah.js");
|
||||||
|
}
|
||||||
|
|
||||||
const PLANET_NAME_TO_ID = {
|
const PLANET_NAME_TO_ID = {
|
||||||
saturn: "saturn",
|
saturn: "saturn",
|
||||||
@@ -270,253 +278,6 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePathTarotImage(path) {
|
|
||||||
const cardName = String(path?.tarot?.card || "").trim();
|
|
||||||
if (!cardName || typeof window.TarotCardImages?.resolveTarotCardImage !== "function") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return window.TarotCardImages.resolveTarotCardImage(cardName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSvgImageHref(imageEl) {
|
|
||||||
if (!(imageEl instanceof SVGElement)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(
|
|
||||||
imageEl.getAttribute("href")
|
|
||||||
|| imageEl.getAttributeNS("http://www.w3.org/1999/xlink", "href")
|
|
||||||
|| ""
|
|
||||||
).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function openTarotLightboxForPath(path, fallbackSrc = "") {
|
|
||||||
const openLightbox = window.TarotUiLightbox?.open;
|
|
||||||
if (typeof openLightbox !== "function") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cardName = String(path?.tarot?.card || "").trim();
|
|
||||||
const src = String(fallbackSrc || resolvePathTarotImage(path) || "").trim();
|
|
||||||
if (!src) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallbackLabel = Number.isFinite(Number(path?.pathNumber))
|
|
||||||
? `Path ${path.pathNumber} tarot card`
|
|
||||||
: "Path tarot card";
|
|
||||||
openLightbox(src, cardName || fallbackLabel);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPathLabel(path) {
|
|
||||||
const glyph = String(path?.hebrewLetter?.char || "").trim();
|
|
||||||
const pathNumber = Number(path?.pathNumber);
|
|
||||||
const parts = [];
|
|
||||||
|
|
||||||
if (state.showPathLetters && glyph) {
|
|
||||||
parts.push(glyph);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.showPathNumbers && Number.isFinite(pathNumber)) {
|
|
||||||
parts.push(String(pathNumber));
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── SVG element factory ────────────────────────────────────────────────────
|
|
||||||
function svgEl(tag, attrs, text) {
|
|
||||||
const el = document.createElementNS(NS, tag);
|
|
||||||
for (const [k, v] of Object.entries(attrs || {})) {
|
|
||||||
el.setAttribute(k, String(v));
|
|
||||||
}
|
|
||||||
if (text != null) el.textContent = text;
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rosicrucian cross SVG construction lives in app/ui-rosicrucian-cross.js.
|
|
||||||
|
|
||||||
// ─── build the full SVG tree ─────────────────────────────────────────────────
|
|
||||||
function buildTreeSVG(tree) {
|
|
||||||
const svg = svgEl("svg", {
|
|
||||||
viewBox: "0 0 240 470",
|
|
||||||
width: "100%",
|
|
||||||
role: "img",
|
|
||||||
"aria-label": "Kabbalah Tree of Life diagram",
|
|
||||||
class: "kab-svg",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Subtle pillar background tracks
|
|
||||||
svg.appendChild(svgEl("rect", {
|
|
||||||
x: 113, y: 30, width: 14, height: 420,
|
|
||||||
rx: 7, fill: "#ffffff07", "pointer-events": "none",
|
|
||||||
}));
|
|
||||||
svg.appendChild(svgEl("rect", {
|
|
||||||
x: 33, y: 88, width: 14, height: 255,
|
|
||||||
rx: 7, fill: "#ff220010", "pointer-events": "none",
|
|
||||||
}));
|
|
||||||
svg.appendChild(svgEl("rect", {
|
|
||||||
x: 193, y: 88, width: 14, height: 255,
|
|
||||||
rx: 7, fill: "#2244ff10", "pointer-events": "none",
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Pillar labels
|
|
||||||
[
|
|
||||||
{ x: 198, y: 73, text: "Mercy", anchor: "middle" },
|
|
||||||
{ x: 120, y: 17, text: "Balance", anchor: "middle" },
|
|
||||||
{ x: 42, y: 73, text: "Severity", anchor: "middle" },
|
|
||||||
].forEach(({ x, y, text, anchor }) => {
|
|
||||||
svg.appendChild(svgEl("text", {
|
|
||||||
x, y, "text-anchor": anchor, "dominant-baseline": "auto",
|
|
||||||
fill: "#42425a", "font-size": "6", "pointer-events": "none",
|
|
||||||
}, text));
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── path lines (drawn before sephiroth so nodes sit on top) ──────────────
|
|
||||||
tree.paths.forEach(path => {
|
|
||||||
const [x1, y1] = NODE_POS[path.connects.from];
|
|
||||||
const [x2, y2] = NODE_POS[path.connects.to];
|
|
||||||
const mx = (x1 + x2) / 2;
|
|
||||||
const my = (y1 + y2) / 2;
|
|
||||||
const tarotImage = state.showPathTarotCards ? resolvePathTarotImage(path) : null;
|
|
||||||
const hasTarotImage = Boolean(tarotImage);
|
|
||||||
const pathLabel = getPathLabel(path);
|
|
||||||
const hasLabel = Boolean(pathLabel);
|
|
||||||
const labelY = hasTarotImage && hasLabel ? my - PATH_LABEL_OFFSET_WITH_TAROT : my;
|
|
||||||
|
|
||||||
// Visual line (thin)
|
|
||||||
svg.appendChild(svgEl("line", {
|
|
||||||
x1, y1, x2, y2,
|
|
||||||
class: "kab-path-line",
|
|
||||||
"data-path": path.pathNumber,
|
|
||||||
stroke: "#3c3c5c",
|
|
||||||
"stroke-width": "1.5",
|
|
||||||
"pointer-events": "none",
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Invisible wide hit area for easy clicking
|
|
||||||
svg.appendChild(svgEl("line", {
|
|
||||||
x1, y1, x2, y2,
|
|
||||||
class: "kab-path-hit",
|
|
||||||
"data-path": path.pathNumber,
|
|
||||||
stroke: "transparent",
|
|
||||||
"stroke-width": String(12 * PATH_MARKER_SCALE),
|
|
||||||
role: "button",
|
|
||||||
tabindex: "0",
|
|
||||||
"aria-label": `Path ${path.pathNumber}: ${path.hebrewLetter?.transliteration || ""} — ${path.tarot?.card || ""}`,
|
|
||||||
style: "cursor:pointer",
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (hasLabel) {
|
|
||||||
// Background disc for legibility behind path label
|
|
||||||
svg.appendChild(svgEl("circle", {
|
|
||||||
cx: mx, cy: labelY, r: PATH_LABEL_RADIUS.toFixed(2),
|
|
||||||
fill: "#0d0d1c", opacity: "0.82",
|
|
||||||
"pointer-events": "none",
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Path label at path midpoint
|
|
||||||
svg.appendChild(svgEl("text", {
|
|
||||||
x: mx, y: labelY + 1,
|
|
||||||
"text-anchor": "middle",
|
|
||||||
"dominant-baseline": "middle",
|
|
||||||
class: "kab-path-lbl",
|
|
||||||
"data-path": path.pathNumber,
|
|
||||||
fill: "#a8a8e0",
|
|
||||||
"font-size": PATH_LABEL_FONT_SIZE.toFixed(2),
|
|
||||||
"pointer-events": "none",
|
|
||||||
}, pathLabel));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasTarotImage) {
|
|
||||||
const tarotY = hasLabel
|
|
||||||
? my + PATH_TAROT_OFFSET_WITH_LABEL
|
|
||||||
: my - PATH_TAROT_OFFSET_NO_LABEL;
|
|
||||||
svg.appendChild(svgEl("image", {
|
|
||||||
href: tarotImage,
|
|
||||||
x: (mx - (PATH_TAROT_WIDTH / 2)).toFixed(2),
|
|
||||||
y: tarotY.toFixed(2),
|
|
||||||
width: PATH_TAROT_WIDTH.toFixed(2),
|
|
||||||
height: PATH_TAROT_HEIGHT.toFixed(2),
|
|
||||||
preserveAspectRatio: "xMidYMid meet",
|
|
||||||
class: "kab-path-tarot",
|
|
||||||
"data-path": path.pathNumber,
|
|
||||||
role: "button",
|
|
||||||
tabindex: "0",
|
|
||||||
"aria-label": `Path ${path.pathNumber} Tarot card ${path.tarot?.card || ""}`,
|
|
||||||
style: "cursor:pointer"
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Da'at — phantom sephira (dashed, informational only) ────────────────
|
|
||||||
svg.appendChild(svgEl("circle", {
|
|
||||||
cx: DAAT[0], cy: DAAT[1], r: "9",
|
|
||||||
fill: "none", stroke: "#3c3c5c",
|
|
||||||
"stroke-dasharray": "3 2", "stroke-width": "1",
|
|
||||||
"pointer-events": "none",
|
|
||||||
}));
|
|
||||||
svg.appendChild(svgEl("text", {
|
|
||||||
x: DAAT[0] + 13, y: DAAT[1] + 1,
|
|
||||||
"text-anchor": "start", "dominant-baseline": "middle",
|
|
||||||
fill: "#3c3c5c", "font-size": "6.5", "pointer-events": "none",
|
|
||||||
}, "Da'at"));
|
|
||||||
|
|
||||||
// ── sephiroth circles (drawn last, on top of paths) ──────────────────────
|
|
||||||
tree.sephiroth.forEach(seph => {
|
|
||||||
const [cx, cy] = NODE_POS[seph.number];
|
|
||||||
const fill = SEPH_FILL[seph.number] || "#555";
|
|
||||||
const isLeft = cx < 80;
|
|
||||||
const isMid = cx === 120;
|
|
||||||
|
|
||||||
// Glow halo (subtle, pointer-events:none)
|
|
||||||
svg.appendChild(svgEl("circle", {
|
|
||||||
cx, cy, r: "16",
|
|
||||||
fill, opacity: "0.12",
|
|
||||||
class: "kab-node-glow",
|
|
||||||
"data-sephira": seph.number,
|
|
||||||
"pointer-events": "none",
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Main clickable circle
|
|
||||||
svg.appendChild(svgEl("circle", {
|
|
||||||
cx, cy, r: R,
|
|
||||||
fill, stroke: "#00000040", "stroke-width": "1",
|
|
||||||
class: "kab-node",
|
|
||||||
"data-sephira": seph.number,
|
|
||||||
role: "button",
|
|
||||||
tabindex: "0",
|
|
||||||
"aria-label": `Sephira ${seph.number}: ${seph.name}`,
|
|
||||||
style: "cursor:pointer",
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Sephira number inside the circle
|
|
||||||
svg.appendChild(svgEl("text", {
|
|
||||||
x: cx, y: cy + 0.5,
|
|
||||||
"text-anchor": "middle", "dominant-baseline": "middle",
|
|
||||||
fill: DARK_TEXT.has(seph.number) ? "#111" : "#fff",
|
|
||||||
"font-size": "8", "font-weight": "bold",
|
|
||||||
"pointer-events": "none",
|
|
||||||
}, String(seph.number)));
|
|
||||||
|
|
||||||
// Name label beside the circle
|
|
||||||
const lx = isLeft ? cx - R - 4 : cx + R + 4;
|
|
||||||
svg.appendChild(svgEl("text", {
|
|
||||||
x: isMid ? cx : lx,
|
|
||||||
y: isMid ? cy + R + 8 : cy,
|
|
||||||
"text-anchor": isMid ? "middle" : (isLeft ? "end" : "start"),
|
|
||||||
"dominant-baseline": isMid ? "auto" : "middle",
|
|
||||||
fill: "#c0c0d4",
|
|
||||||
"font-size": "7.5", "pointer-events": "none",
|
|
||||||
class: "kab-node-lbl",
|
|
||||||
}, seph.name));
|
|
||||||
});
|
|
||||||
|
|
||||||
return svg;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeText(value) {
|
function normalizeText(value) {
|
||||||
return String(value || "").trim().toLowerCase();
|
return String(value || "").trim().toLowerCase();
|
||||||
}
|
}
|
||||||
@@ -654,98 +415,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindTreeInteractions(svg, tree, elements) {
|
|
||||||
// Delegate clicks via element's own data attributes
|
|
||||||
svg.addEventListener("click", e => {
|
|
||||||
const clickTarget = e.target instanceof Element ? e.target : null;
|
|
||||||
const sephNum = clickTarget?.dataset?.sephira;
|
|
||||||
const pathNum = clickTarget?.dataset?.path;
|
|
||||||
|
|
||||||
if (pathNum != null && clickTarget?.classList?.contains("kab-path-tarot")) {
|
|
||||||
const p = tree.paths.find(x => x.pathNumber === Number(pathNum));
|
|
||||||
if (p) {
|
|
||||||
openTarotLightboxForPath(p, getSvgImageHref(clickTarget));
|
|
||||||
renderPathDetail(p, tree, elements);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sephNum != null) {
|
|
||||||
const s = tree.sephiroth.find(x => x.number === Number(sephNum));
|
|
||||||
if (s) renderSephiraDetail(s, tree, elements);
|
|
||||||
} else if (pathNum != null) {
|
|
||||||
const p = tree.paths.find(x => x.pathNumber === Number(pathNum));
|
|
||||||
if (p) renderPathDetail(p, tree, elements);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keyboard access for path hit-areas and tarot images
|
|
||||||
svg.querySelectorAll(".kab-path-hit, .kab-path-tarot").forEach(el => {
|
|
||||||
el.addEventListener("keydown", e => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
const p = tree.paths.find(x => x.pathNumber === Number(el.dataset.path));
|
|
||||||
if (p) {
|
|
||||||
if (el.classList.contains("kab-path-tarot")) {
|
|
||||||
openTarotLightboxForPath(p, getSvgImageHref(el));
|
|
||||||
}
|
|
||||||
renderPathDetail(p, tree, elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keyboard access for sephira circles
|
|
||||||
svg.querySelectorAll(".kab-node").forEach(el => {
|
|
||||||
el.addEventListener("keydown", e => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
const s = tree.sephiroth.find(x => x.number === Number(el.dataset.sephira));
|
|
||||||
if (s) renderSephiraDetail(s, tree, elements);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindRoseCrossInteractions(svg, tree, roseElements) {
|
|
||||||
if (!svg || !roseElements?.detailBodyEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const openPathFromTarget = (targetEl) => {
|
|
||||||
if (!(targetEl instanceof Element)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const petal = targetEl.closest(".kab-rose-petal[data-path]");
|
|
||||||
if (!(petal instanceof SVGElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathNumber = Number(petal.dataset.path);
|
|
||||||
if (!Number.isFinite(pathNumber)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = tree.paths.find((entry) => entry.pathNumber === pathNumber);
|
|
||||||
if (path) {
|
|
||||||
renderPathDetail(path, tree, roseElements);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
svg.addEventListener("click", (event) => {
|
|
||||||
openPathFromTarget(event.target);
|
|
||||||
});
|
|
||||||
|
|
||||||
svg.querySelectorAll(".kab-rose-petal[data-path]").forEach((petal) => {
|
|
||||||
petal.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
openPathFromTarget(petal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRoseLandingIntro(roseElements) {
|
function renderRoseLandingIntro(roseElements) {
|
||||||
if (typeof kabbalahDetailUi.renderRoseLandingIntro === "function") {
|
if (typeof kabbalahDetailUi.renderRoseLandingIntro === "function") {
|
||||||
@@ -753,25 +422,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRoseCross(elements) {
|
function getViewRenderContext(elements) {
|
||||||
if (!state.tree || !elements?.roseCrossContainerEl) {
|
return {
|
||||||
return;
|
state,
|
||||||
}
|
tree: state.tree,
|
||||||
|
elements,
|
||||||
const roseElements = getRoseDetailElements(elements);
|
getRoseDetailElements,
|
||||||
if (!roseElements?.detailBodyEl) {
|
renderSephiraDetail,
|
||||||
return;
|
renderPathDetail,
|
||||||
}
|
NS,
|
||||||
|
R,
|
||||||
const roseBuilder = window.KabbalahRosicrucianCross?.buildRosicrucianCrossSVG;
|
NODE_POS,
|
||||||
if (typeof roseBuilder !== "function") {
|
SEPH_FILL,
|
||||||
return;
|
DARK_TEXT,
|
||||||
}
|
DAAT,
|
||||||
|
PATH_MARKER_SCALE,
|
||||||
const roseSvg = roseBuilder(state.tree);
|
PATH_LABEL_RADIUS,
|
||||||
elements.roseCrossContainerEl.innerHTML = "";
|
PATH_LABEL_FONT_SIZE,
|
||||||
elements.roseCrossContainerEl.appendChild(roseSvg);
|
PATH_TAROT_WIDTH,
|
||||||
bindRoseCrossInteractions(roseSvg, state.tree, roseElements);
|
PATH_TAROT_HEIGHT,
|
||||||
|
PATH_LABEL_OFFSET_WITH_TAROT,
|
||||||
|
PATH_TAROT_OFFSET_WITH_LABEL,
|
||||||
|
PATH_TAROT_OFFSET_NO_LABEL
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRoseCurrentSelection(elements) {
|
function renderRoseCurrentSelection(elements) {
|
||||||
@@ -795,15 +468,12 @@
|
|||||||
renderRoseLandingIntro(roseElements);
|
renderRoseLandingIntro(roseElements);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTree(elements) {
|
function renderRoseCross(elements) {
|
||||||
if (!state.tree || !elements?.treeContainerEl) {
|
kabbalahViewsUi.renderRoseCross(getViewRenderContext(elements));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const svg = buildTreeSVG(state.tree);
|
function renderTree(elements) {
|
||||||
elements.treeContainerEl.innerHTML = "";
|
kabbalahViewsUi.renderTree(getViewRenderContext(elements));
|
||||||
elements.treeContainerEl.appendChild(svg);
|
|
||||||
bindTreeInteractions(svg, state.tree, elements);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCurrentSelection(elements) {
|
function renderCurrentSelection(elements) {
|
||||||
|
|||||||
519
app/ui-now-helpers.js
Normal file
519
app/ui-now-helpers.js
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
/* ui-now-helpers.js — Lightbox and astronomy/countdown helpers for the Now panel */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { DAY_IN_MS, getMoonPhaseName, getDecanForDate } = window.TarotCalc || {};
|
||||||
|
const { resolveTarotCardImage, getTarotCardDisplayName } = window.TarotCardImages || {};
|
||||||
|
|
||||||
|
let nowLightboxOverlayEl = null;
|
||||||
|
let nowLightboxImageEl = null;
|
||||||
|
let nowLightboxZoomed = false;
|
||||||
|
|
||||||
|
const LIGHTBOX_ZOOM_SCALE = 6.66;
|
||||||
|
|
||||||
|
const PLANETARY_BODIES = [
|
||||||
|
{ id: "sol", astronomyBody: "Sun", fallbackName: "Sun", fallbackSymbol: "☉︎" },
|
||||||
|
{ id: "luna", astronomyBody: "Moon", fallbackName: "Moon", fallbackSymbol: "☾︎" },
|
||||||
|
{ id: "mercury", astronomyBody: "Mercury", fallbackName: "Mercury", fallbackSymbol: "☿︎" },
|
||||||
|
{ id: "venus", astronomyBody: "Venus", fallbackName: "Venus", fallbackSymbol: "♀︎" },
|
||||||
|
{ id: "mars", astronomyBody: "Mars", fallbackName: "Mars", fallbackSymbol: "♂︎" },
|
||||||
|
{ id: "jupiter", astronomyBody: "Jupiter", fallbackName: "Jupiter", fallbackSymbol: "♃︎" },
|
||||||
|
{ id: "saturn", astronomyBody: "Saturn", fallbackName: "Saturn", fallbackSymbol: "♄︎" },
|
||||||
|
{ id: "uranus", astronomyBody: "Uranus", fallbackName: "Uranus", fallbackSymbol: "♅︎" },
|
||||||
|
{ id: "neptune", astronomyBody: "Neptune", fallbackName: "Neptune", fallbackSymbol: "♆︎" },
|
||||||
|
{ id: "pluto", astronomyBody: "Pluto", fallbackName: "Pluto", fallbackSymbol: "♇︎" }
|
||||||
|
];
|
||||||
|
|
||||||
|
function resetNowLightboxZoom() {
|
||||||
|
if (!nowLightboxImageEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nowLightboxZoomed = false;
|
||||||
|
nowLightboxImageEl.style.transform = "scale(1)";
|
||||||
|
nowLightboxImageEl.style.transformOrigin = "center center";
|
||||||
|
nowLightboxImageEl.style.cursor = "zoom-in";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNowLightboxZoomOrigin(clientX, clientY) {
|
||||||
|
if (!nowLightboxZoomed || !nowLightboxImageEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = nowLightboxImageEl.getBoundingClientRect();
|
||||||
|
if (!rect.width || !rect.height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = Math.min(100, Math.max(0, ((clientX - rect.left) / rect.width) * 100));
|
||||||
|
const y = Math.min(100, Math.max(0, ((clientY - rect.top) / rect.height) * 100));
|
||||||
|
nowLightboxImageEl.style.transformOrigin = `${x}% ${y}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNowLightboxPointOnCard(clientX, clientY) {
|
||||||
|
if (!nowLightboxImageEl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = nowLightboxImageEl.getBoundingClientRect();
|
||||||
|
const naturalWidth = nowLightboxImageEl.naturalWidth;
|
||||||
|
const naturalHeight = nowLightboxImageEl.naturalHeight;
|
||||||
|
|
||||||
|
if (!rect.width || !rect.height || !naturalWidth || !naturalHeight) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameAspect = rect.width / rect.height;
|
||||||
|
const imageAspect = naturalWidth / naturalHeight;
|
||||||
|
|
||||||
|
let renderWidth = rect.width;
|
||||||
|
let renderHeight = rect.height;
|
||||||
|
if (imageAspect > frameAspect) {
|
||||||
|
renderHeight = rect.width / imageAspect;
|
||||||
|
} else {
|
||||||
|
renderWidth = rect.height * imageAspect;
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = rect.left + (rect.width - renderWidth) / 2;
|
||||||
|
const top = rect.top + (rect.height - renderHeight) / 2;
|
||||||
|
const right = left + renderWidth;
|
||||||
|
const bottom = top + renderHeight;
|
||||||
|
|
||||||
|
return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNowImageLightbox() {
|
||||||
|
if (nowLightboxOverlayEl && nowLightboxImageEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nowLightboxOverlayEl = document.createElement("div");
|
||||||
|
nowLightboxOverlayEl.setAttribute("aria-hidden", "true");
|
||||||
|
nowLightboxOverlayEl.style.position = "fixed";
|
||||||
|
nowLightboxOverlayEl.style.inset = "0";
|
||||||
|
nowLightboxOverlayEl.style.background = "rgba(0, 0, 0, 0.82)";
|
||||||
|
nowLightboxOverlayEl.style.display = "none";
|
||||||
|
nowLightboxOverlayEl.style.alignItems = "center";
|
||||||
|
nowLightboxOverlayEl.style.justifyContent = "center";
|
||||||
|
nowLightboxOverlayEl.style.zIndex = "9999";
|
||||||
|
nowLightboxOverlayEl.style.padding = "0";
|
||||||
|
|
||||||
|
const image = document.createElement("img");
|
||||||
|
image.alt = "Now card enlarged image";
|
||||||
|
image.style.maxWidth = "100vw";
|
||||||
|
image.style.maxHeight = "100vh";
|
||||||
|
image.style.width = "100vw";
|
||||||
|
image.style.height = "100vh";
|
||||||
|
image.style.objectFit = "contain";
|
||||||
|
image.style.borderRadius = "0";
|
||||||
|
image.style.boxShadow = "none";
|
||||||
|
image.style.border = "none";
|
||||||
|
image.style.cursor = "zoom-in";
|
||||||
|
image.style.transform = "scale(1)";
|
||||||
|
image.style.transformOrigin = "center center";
|
||||||
|
image.style.transition = "transform 120ms ease-out";
|
||||||
|
image.style.userSelect = "none";
|
||||||
|
|
||||||
|
nowLightboxImageEl = image;
|
||||||
|
nowLightboxOverlayEl.appendChild(image);
|
||||||
|
|
||||||
|
const closeLightbox = () => {
|
||||||
|
if (!nowLightboxOverlayEl || !nowLightboxImageEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nowLightboxOverlayEl.style.display = "none";
|
||||||
|
nowLightboxOverlayEl.setAttribute("aria-hidden", "true");
|
||||||
|
nowLightboxImageEl.removeAttribute("src");
|
||||||
|
resetNowLightboxZoom();
|
||||||
|
};
|
||||||
|
|
||||||
|
nowLightboxOverlayEl.addEventListener("click", (event) => {
|
||||||
|
if (event.target === nowLightboxOverlayEl) {
|
||||||
|
closeLightbox();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nowLightboxImageEl.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!isNowLightboxPointOnCard(event.clientX, event.clientY)) {
|
||||||
|
closeLightbox();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!nowLightboxZoomed) {
|
||||||
|
nowLightboxZoomed = true;
|
||||||
|
nowLightboxImageEl.style.transform = `scale(${LIGHTBOX_ZOOM_SCALE})`;
|
||||||
|
nowLightboxImageEl.style.cursor = "zoom-out";
|
||||||
|
updateNowLightboxZoomOrigin(event.clientX, event.clientY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetNowLightboxZoom();
|
||||||
|
});
|
||||||
|
|
||||||
|
nowLightboxImageEl.addEventListener("mousemove", (event) => {
|
||||||
|
updateNowLightboxZoomOrigin(event.clientX, event.clientY);
|
||||||
|
});
|
||||||
|
|
||||||
|
nowLightboxImageEl.addEventListener("mouseleave", () => {
|
||||||
|
if (nowLightboxZoomed) {
|
||||||
|
nowLightboxImageEl.style.transformOrigin = "center center";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
closeLightbox();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(nowLightboxOverlayEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNowImageLightbox(src, altText) {
|
||||||
|
if (!src) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureNowImageLightbox();
|
||||||
|
if (!nowLightboxOverlayEl || !nowLightboxImageEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nowLightboxImageEl.src = src;
|
||||||
|
nowLightboxImageEl.alt = altText || "Now card enlarged image";
|
||||||
|
resetNowLightboxZoom();
|
||||||
|
nowLightboxOverlayEl.style.display = "flex";
|
||||||
|
nowLightboxOverlayEl.setAttribute("aria-hidden", "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayTarotName(cardName, trumpNumber) {
|
||||||
|
if (!cardName) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof getTarotCardDisplayName !== "function") {
|
||||||
|
return cardName;
|
||||||
|
}
|
||||||
|
if (Number.isFinite(Number(trumpNumber))) {
|
||||||
|
return getTarotCardDisplayName(cardName, { trumpNumber: Number(trumpNumber) }) || cardName;
|
||||||
|
}
|
||||||
|
return getTarotCardDisplayName(cardName) || cardName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindNowCardLightbox(imageEl) {
|
||||||
|
if (!(imageEl instanceof HTMLImageElement) || imageEl.dataset.lightboxBound === "true") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
imageEl.dataset.lightboxBound = "true";
|
||||||
|
imageEl.style.cursor = "zoom-in";
|
||||||
|
imageEl.title = "Click to enlarge";
|
||||||
|
imageEl.addEventListener("click", () => {
|
||||||
|
const src = imageEl.getAttribute("src");
|
||||||
|
if (!src || imageEl.style.display === "none") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openNowImageLightbox(src, imageEl.alt || "Now card enlarged image");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLongitude(value) {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (!Number.isFinite(numeric)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((numeric % 360) + 360) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSortedSigns(signs) {
|
||||||
|
if (!Array.isArray(signs)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...signs].sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSignForLongitude(longitude, signs) {
|
||||||
|
const normalized = normalizeLongitude(longitude);
|
||||||
|
if (normalized === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedSigns = getSortedSigns(signs);
|
||||||
|
if (!sortedSigns.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signIndex = Math.min(sortedSigns.length - 1, Math.floor(normalized / 30));
|
||||||
|
const sign = sortedSigns[signIndex] || null;
|
||||||
|
if (!sign) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sign,
|
||||||
|
degreeInSign: normalized - signIndex * 30,
|
||||||
|
absoluteLongitude: normalized
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSabianSymbolForLongitude(longitude, sabianSymbols) {
|
||||||
|
const normalized = normalizeLongitude(longitude);
|
||||||
|
if (normalized === null || !Array.isArray(sabianSymbols) || !sabianSymbols.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absoluteDegree = Math.floor(normalized) + 1;
|
||||||
|
return sabianSymbols.find((entry) => Number(entry?.absoluteDegree) === absoluteDegree) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatePlanetPositions(referenceData, now) {
|
||||||
|
if (!window.Astronomy || !referenceData) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = [];
|
||||||
|
|
||||||
|
PLANETARY_BODIES.forEach((body) => {
|
||||||
|
try {
|
||||||
|
const geoVector = window.Astronomy.GeoVector(body.astronomyBody, now, true);
|
||||||
|
const ecliptic = window.Astronomy.Ecliptic(geoVector);
|
||||||
|
const signInfo = getSignForLongitude(ecliptic?.elon, referenceData.signs);
|
||||||
|
if (!signInfo?.sign) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const planetInfo = referenceData.planets?.[body.id] || null;
|
||||||
|
const symbol = planetInfo?.symbol || body.fallbackSymbol;
|
||||||
|
const name = planetInfo?.name || body.fallbackName;
|
||||||
|
|
||||||
|
positions.push({
|
||||||
|
id: body.id,
|
||||||
|
symbol,
|
||||||
|
name,
|
||||||
|
longitude: signInfo.absoluteLongitude,
|
||||||
|
sign: signInfo.sign,
|
||||||
|
degreeInSign: signInfo.degreeInSign,
|
||||||
|
label: `${symbol} ${name}: ${signInfo.sign.symbol} ${signInfo.sign.name} ${signInfo.degreeInSign.toFixed(1)}°`
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNowStats(referenceData, elements, now) {
|
||||||
|
const planetPositions = calculatePlanetPositions(referenceData, now);
|
||||||
|
|
||||||
|
if (elements.nowStatsPlanetsEl) {
|
||||||
|
elements.nowStatsPlanetsEl.replaceChildren();
|
||||||
|
|
||||||
|
if (!planetPositions.length) {
|
||||||
|
elements.nowStatsPlanetsEl.textContent = "--";
|
||||||
|
} else {
|
||||||
|
planetPositions.forEach((position) => {
|
||||||
|
const item = document.createElement("div");
|
||||||
|
item.className = "now-stats-planet";
|
||||||
|
item.textContent = position.label;
|
||||||
|
elements.nowStatsPlanetsEl.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.nowStatsSabianEl) {
|
||||||
|
const sunPosition = planetPositions.find((entry) => entry.id === "sol") || null;
|
||||||
|
const moonPosition = planetPositions.find((entry) => entry.id === "luna") || null;
|
||||||
|
const sunSabianSymbol = sunPosition
|
||||||
|
? getSabianSymbolForLongitude(sunPosition.longitude, referenceData.sabianSymbols)
|
||||||
|
: null;
|
||||||
|
const moonSabianSymbol = moonPosition
|
||||||
|
? getSabianSymbolForLongitude(moonPosition.longitude, referenceData.sabianSymbols)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const sunLine = sunSabianSymbol?.phrase
|
||||||
|
? `Sun Sabian ${sunSabianSymbol.absoluteDegree}: ${sunSabianSymbol.phrase}`
|
||||||
|
: "Sun Sabian: --";
|
||||||
|
const moonLine = moonSabianSymbol?.phrase
|
||||||
|
? `Moon Sabian ${moonSabianSymbol.absoluteDegree}: ${moonSabianSymbol.phrase}`
|
||||||
|
: "Moon Sabian: --";
|
||||||
|
|
||||||
|
elements.nowStatsSabianEl.textContent = `${sunLine}\n${moonLine}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCountdown(ms, mode) {
|
||||||
|
if (!Number.isFinite(ms) || ms <= 0) {
|
||||||
|
if (mode === "hours") {
|
||||||
|
return "0.0 hours";
|
||||||
|
}
|
||||||
|
if (mode === "seconds") {
|
||||||
|
return "0s";
|
||||||
|
}
|
||||||
|
return "0m";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "hours") {
|
||||||
|
return `${(ms / 3600000).toFixed(1)} hours`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "seconds") {
|
||||||
|
return `${Math.floor(ms / 1000)}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${Math.floor(ms / 60000)}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMonthDay(monthDay) {
|
||||||
|
const [month, day] = String(monthDay || "").split("-").map(Number);
|
||||||
|
return { month, day };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentPhaseName(date) {
|
||||||
|
return getMoonPhaseName(window.SunCalc.getMoonIllumination(date).phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNextMoonPhaseTransition(now) {
|
||||||
|
const currentPhase = getCurrentPhaseName(now);
|
||||||
|
const stepMs = 15 * 60 * 1000;
|
||||||
|
const maxMs = 40 * DAY_IN_MS;
|
||||||
|
|
||||||
|
let previousTime = now.getTime();
|
||||||
|
let previousPhase = currentPhase;
|
||||||
|
|
||||||
|
for (let t = previousTime + stepMs; t <= previousTime + maxMs; t += stepMs) {
|
||||||
|
const phaseName = getCurrentPhaseName(new Date(t));
|
||||||
|
if (phaseName !== previousPhase) {
|
||||||
|
let low = previousTime;
|
||||||
|
let high = t;
|
||||||
|
while (high - low > 1000) {
|
||||||
|
const mid = Math.floor((low + high) / 2);
|
||||||
|
const midPhase = getCurrentPhaseName(new Date(mid));
|
||||||
|
if (midPhase === currentPhase) {
|
||||||
|
low = mid;
|
||||||
|
} else {
|
||||||
|
high = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const transitionAt = new Date(high);
|
||||||
|
const nextPhase = getCurrentPhaseName(new Date(high + 1000));
|
||||||
|
return {
|
||||||
|
fromPhase: currentPhase,
|
||||||
|
nextPhase,
|
||||||
|
changeAt: transitionAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
previousTime = t;
|
||||||
|
previousPhase = phaseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSignStartDate(now, sign) {
|
||||||
|
const { month: startMonth, day: startDay } = parseMonthDay(sign.start);
|
||||||
|
const { month: endMonth } = parseMonthDay(sign.end);
|
||||||
|
const wrapsYear = startMonth > endMonth;
|
||||||
|
|
||||||
|
let year = now.getFullYear();
|
||||||
|
const nowMonth = now.getMonth() + 1;
|
||||||
|
const nowDay = now.getDate();
|
||||||
|
|
||||||
|
if (wrapsYear && (nowMonth < startMonth || (nowMonth === startMonth && nowDay < startDay))) {
|
||||||
|
year -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(year, startMonth - 1, startDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextSign(signs, currentSign) {
|
||||||
|
const sorted = [...signs].sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||||
|
const index = sorted.findIndex((entry) => entry.id === currentSign.id);
|
||||||
|
if (index < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return sorted[(index + 1) % sorted.length] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDecanByIndex(decansBySign, signId, index) {
|
||||||
|
const signDecans = decansBySign[signId] || [];
|
||||||
|
return signDecans.find((entry) => entry.index === index) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNextDecanTransition(now, signs, decansBySign) {
|
||||||
|
const currentInfo = getDecanForDate(now, signs, decansBySign);
|
||||||
|
if (!currentInfo?.sign) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = currentInfo.decan?.index || 1;
|
||||||
|
const signStart = getSignStartDate(now, currentInfo.sign);
|
||||||
|
|
||||||
|
if (currentIndex < 3) {
|
||||||
|
const changeAt = new Date(signStart.getTime() + currentIndex * 10 * DAY_IN_MS);
|
||||||
|
const nextDecan = getDecanByIndex(decansBySign, currentInfo.sign.id, currentIndex + 1);
|
||||||
|
const nextLabel = nextDecan?.tarotMinorArcana || `${currentInfo.sign.name} Decan ${currentIndex + 1}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: `${currentInfo.sign.id}-${currentIndex}`,
|
||||||
|
changeAt,
|
||||||
|
nextLabel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSign = getNextSign(signs, currentInfo.sign);
|
||||||
|
if (!nextSign) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { month: nextMonth, day: nextDay } = parseMonthDay(nextSign.start);
|
||||||
|
let year = now.getFullYear();
|
||||||
|
let changeAt = new Date(year, nextMonth - 1, nextDay);
|
||||||
|
if (changeAt.getTime() <= now.getTime()) {
|
||||||
|
changeAt = new Date(year + 1, nextMonth - 1, nextDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextDecan = getDecanByIndex(decansBySign, nextSign.id, 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: `${currentInfo.sign.id}-${currentIndex}`,
|
||||||
|
changeAt,
|
||||||
|
nextLabel: nextDecan?.tarotMinorArcana || `${nextSign.name} Decan 1`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNowCardImage(imageEl, cardName, fallbackLabel, trumpNumber) {
|
||||||
|
if (!imageEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindNowCardLightbox(imageEl);
|
||||||
|
|
||||||
|
if (!cardName || typeof resolveTarotCardImage !== "function") {
|
||||||
|
imageEl.style.display = "none";
|
||||||
|
imageEl.removeAttribute("src");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const src = resolveTarotCardImage(cardName);
|
||||||
|
if (!src) {
|
||||||
|
imageEl.style.display = "none";
|
||||||
|
imageEl.removeAttribute("src");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
imageEl.src = src;
|
||||||
|
const displayName = getDisplayTarotName(cardName, trumpNumber);
|
||||||
|
imageEl.alt = `${fallbackLabel}: ${displayName}`;
|
||||||
|
imageEl.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
window.NowUiHelpers = {
|
||||||
|
findNextDecanTransition,
|
||||||
|
findNextMoonPhaseTransition,
|
||||||
|
formatCountdown,
|
||||||
|
getDisplayTarotName,
|
||||||
|
setNowCardImage,
|
||||||
|
updateNowStats
|
||||||
|
};
|
||||||
|
})();
|
||||||
554
app/ui-now.js
554
app/ui-now.js
@@ -1,517 +1,27 @@
|
|||||||
(function () {
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
DAY_IN_MS,
|
DAY_IN_MS,
|
||||||
getDateKey,
|
getDateKey,
|
||||||
getMoonPhaseName,
|
getMoonPhaseName,
|
||||||
getDecanForDate,
|
|
||||||
calcPlanetaryHoursForDayAndLocation
|
calcPlanetaryHoursForDayAndLocation
|
||||||
} = window.TarotCalc;
|
} = window.TarotCalc;
|
||||||
const { resolveTarotCardImage, getTarotCardDisplayName } = window.TarotCardImages || {};
|
const nowUiHelpers = window.NowUiHelpers || {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof nowUiHelpers.findNextDecanTransition !== "function"
|
||||||
|
|| typeof nowUiHelpers.findNextMoonPhaseTransition !== "function"
|
||||||
|
|| typeof nowUiHelpers.formatCountdown !== "function"
|
||||||
|
|| typeof nowUiHelpers.getDisplayTarotName !== "function"
|
||||||
|
|| typeof nowUiHelpers.setNowCardImage !== "function"
|
||||||
|
|| typeof nowUiHelpers.updateNowStats !== "function"
|
||||||
|
) {
|
||||||
|
throw new Error("NowUiHelpers module must load before ui-now.js");
|
||||||
|
}
|
||||||
|
|
||||||
let moonCountdownCache = null;
|
let moonCountdownCache = null;
|
||||||
let decanCountdownCache = null;
|
let decanCountdownCache = null;
|
||||||
let nowLightboxOverlayEl = null;
|
|
||||||
let nowLightboxImageEl = null;
|
|
||||||
let nowLightboxZoomed = false;
|
|
||||||
|
|
||||||
const LIGHTBOX_ZOOM_SCALE = 6.66;
|
|
||||||
|
|
||||||
const PLANETARY_BODIES = [
|
|
||||||
{ id: "sol", astronomyBody: "Sun", fallbackName: "Sun", fallbackSymbol: "☉︎" },
|
|
||||||
{ id: "luna", astronomyBody: "Moon", fallbackName: "Moon", fallbackSymbol: "☾︎" },
|
|
||||||
{ id: "mercury", astronomyBody: "Mercury", fallbackName: "Mercury", fallbackSymbol: "☿︎" },
|
|
||||||
{ id: "venus", astronomyBody: "Venus", fallbackName: "Venus", fallbackSymbol: "♀︎" },
|
|
||||||
{ id: "mars", astronomyBody: "Mars", fallbackName: "Mars", fallbackSymbol: "♂︎" },
|
|
||||||
{ id: "jupiter", astronomyBody: "Jupiter", fallbackName: "Jupiter", fallbackSymbol: "♃︎" },
|
|
||||||
{ id: "saturn", astronomyBody: "Saturn", fallbackName: "Saturn", fallbackSymbol: "♄︎" },
|
|
||||||
{ id: "uranus", astronomyBody: "Uranus", fallbackName: "Uranus", fallbackSymbol: "♅︎" },
|
|
||||||
{ id: "neptune", astronomyBody: "Neptune", fallbackName: "Neptune", fallbackSymbol: "♆︎" },
|
|
||||||
{ id: "pluto", astronomyBody: "Pluto", fallbackName: "Pluto", fallbackSymbol: "♇︎" }
|
|
||||||
];
|
|
||||||
|
|
||||||
function resetNowLightboxZoom() {
|
|
||||||
if (!nowLightboxImageEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
nowLightboxZoomed = false;
|
|
||||||
nowLightboxImageEl.style.transform = "scale(1)";
|
|
||||||
nowLightboxImageEl.style.transformOrigin = "center center";
|
|
||||||
nowLightboxImageEl.style.cursor = "zoom-in";
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateNowLightboxZoomOrigin(clientX, clientY) {
|
|
||||||
if (!nowLightboxZoomed || !nowLightboxImageEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = nowLightboxImageEl.getBoundingClientRect();
|
|
||||||
if (!rect.width || !rect.height) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const x = Math.min(100, Math.max(0, ((clientX - rect.left) / rect.width) * 100));
|
|
||||||
const y = Math.min(100, Math.max(0, ((clientY - rect.top) / rect.height) * 100));
|
|
||||||
nowLightboxImageEl.style.transformOrigin = `${x}% ${y}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNowLightboxPointOnCard(clientX, clientY) {
|
|
||||||
if (!nowLightboxImageEl) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = nowLightboxImageEl.getBoundingClientRect();
|
|
||||||
const naturalWidth = nowLightboxImageEl.naturalWidth;
|
|
||||||
const naturalHeight = nowLightboxImageEl.naturalHeight;
|
|
||||||
|
|
||||||
if (!rect.width || !rect.height || !naturalWidth || !naturalHeight) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const frameAspect = rect.width / rect.height;
|
|
||||||
const imageAspect = naturalWidth / naturalHeight;
|
|
||||||
|
|
||||||
let renderWidth = rect.width;
|
|
||||||
let renderHeight = rect.height;
|
|
||||||
if (imageAspect > frameAspect) {
|
|
||||||
renderHeight = rect.width / imageAspect;
|
|
||||||
} else {
|
|
||||||
renderWidth = rect.height * imageAspect;
|
|
||||||
}
|
|
||||||
|
|
||||||
const left = rect.left + (rect.width - renderWidth) / 2;
|
|
||||||
const top = rect.top + (rect.height - renderHeight) / 2;
|
|
||||||
const right = left + renderWidth;
|
|
||||||
const bottom = top + renderHeight;
|
|
||||||
|
|
||||||
return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNowImageLightbox() {
|
|
||||||
if (nowLightboxOverlayEl && nowLightboxImageEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
nowLightboxOverlayEl = document.createElement("div");
|
|
||||||
nowLightboxOverlayEl.setAttribute("aria-hidden", "true");
|
|
||||||
nowLightboxOverlayEl.style.position = "fixed";
|
|
||||||
nowLightboxOverlayEl.style.inset = "0";
|
|
||||||
nowLightboxOverlayEl.style.background = "rgba(0, 0, 0, 0.82)";
|
|
||||||
nowLightboxOverlayEl.style.display = "none";
|
|
||||||
nowLightboxOverlayEl.style.alignItems = "center";
|
|
||||||
nowLightboxOverlayEl.style.justifyContent = "center";
|
|
||||||
nowLightboxOverlayEl.style.zIndex = "9999";
|
|
||||||
nowLightboxOverlayEl.style.padding = "0";
|
|
||||||
|
|
||||||
const image = document.createElement("img");
|
|
||||||
image.alt = "Now card enlarged image";
|
|
||||||
image.style.maxWidth = "100vw";
|
|
||||||
image.style.maxHeight = "100vh";
|
|
||||||
image.style.width = "100vw";
|
|
||||||
image.style.height = "100vh";
|
|
||||||
image.style.objectFit = "contain";
|
|
||||||
image.style.borderRadius = "0";
|
|
||||||
image.style.boxShadow = "none";
|
|
||||||
image.style.border = "none";
|
|
||||||
image.style.cursor = "zoom-in";
|
|
||||||
image.style.transform = "scale(1)";
|
|
||||||
image.style.transformOrigin = "center center";
|
|
||||||
image.style.transition = "transform 120ms ease-out";
|
|
||||||
image.style.userSelect = "none";
|
|
||||||
|
|
||||||
nowLightboxImageEl = image;
|
|
||||||
nowLightboxOverlayEl.appendChild(image);
|
|
||||||
|
|
||||||
const closeLightbox = () => {
|
|
||||||
if (!nowLightboxOverlayEl || !nowLightboxImageEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
nowLightboxOverlayEl.style.display = "none";
|
|
||||||
nowLightboxOverlayEl.setAttribute("aria-hidden", "true");
|
|
||||||
nowLightboxImageEl.removeAttribute("src");
|
|
||||||
resetNowLightboxZoom();
|
|
||||||
};
|
|
||||||
|
|
||||||
nowLightboxOverlayEl.addEventListener("click", (event) => {
|
|
||||||
if (event.target === nowLightboxOverlayEl) {
|
|
||||||
closeLightbox();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
nowLightboxImageEl.addEventListener("click", (event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
if (!isNowLightboxPointOnCard(event.clientX, event.clientY)) {
|
|
||||||
closeLightbox();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!nowLightboxZoomed) {
|
|
||||||
nowLightboxZoomed = true;
|
|
||||||
nowLightboxImageEl.style.transform = `scale(${LIGHTBOX_ZOOM_SCALE})`;
|
|
||||||
nowLightboxImageEl.style.cursor = "zoom-out";
|
|
||||||
updateNowLightboxZoomOrigin(event.clientX, event.clientY);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resetNowLightboxZoom();
|
|
||||||
});
|
|
||||||
|
|
||||||
nowLightboxImageEl.addEventListener("mousemove", (event) => {
|
|
||||||
updateNowLightboxZoomOrigin(event.clientX, event.clientY);
|
|
||||||
});
|
|
||||||
|
|
||||||
nowLightboxImageEl.addEventListener("mouseleave", () => {
|
|
||||||
if (nowLightboxZoomed) {
|
|
||||||
nowLightboxImageEl.style.transformOrigin = "center center";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
closeLightbox();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.appendChild(nowLightboxOverlayEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openNowImageLightbox(src, altText) {
|
|
||||||
if (!src) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureNowImageLightbox();
|
|
||||||
if (!nowLightboxOverlayEl || !nowLightboxImageEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
nowLightboxImageEl.src = src;
|
|
||||||
nowLightboxImageEl.alt = altText || "Now card enlarged image";
|
|
||||||
resetNowLightboxZoom();
|
|
||||||
nowLightboxOverlayEl.style.display = "flex";
|
|
||||||
nowLightboxOverlayEl.setAttribute("aria-hidden", "false");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisplayTarotName(cardName, trumpNumber) {
|
|
||||||
if (!cardName) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (typeof getTarotCardDisplayName !== "function") {
|
|
||||||
return cardName;
|
|
||||||
}
|
|
||||||
if (Number.isFinite(Number(trumpNumber))) {
|
|
||||||
return getTarotCardDisplayName(cardName, { trumpNumber: Number(trumpNumber) }) || cardName;
|
|
||||||
}
|
|
||||||
return getTarotCardDisplayName(cardName) || cardName;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindNowCardLightbox(imageEl) {
|
|
||||||
if (!(imageEl instanceof HTMLImageElement) || imageEl.dataset.lightboxBound === "true") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
imageEl.dataset.lightboxBound = "true";
|
|
||||||
imageEl.style.cursor = "zoom-in";
|
|
||||||
imageEl.title = "Click to enlarge";
|
|
||||||
imageEl.addEventListener("click", () => {
|
|
||||||
const src = imageEl.getAttribute("src");
|
|
||||||
if (!src || imageEl.style.display === "none") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
openNowImageLightbox(src, imageEl.alt || "Now card enlarged image");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeLongitude(value) {
|
|
||||||
const numeric = Number(value);
|
|
||||||
if (!Number.isFinite(numeric)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ((numeric % 360) + 360) % 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSortedSigns(signs) {
|
|
||||||
if (!Array.isArray(signs)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...signs].sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSignForLongitude(longitude, signs) {
|
|
||||||
const normalized = normalizeLongitude(longitude);
|
|
||||||
if (normalized === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedSigns = getSortedSigns(signs);
|
|
||||||
if (!sortedSigns.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signIndex = Math.min(sortedSigns.length - 1, Math.floor(normalized / 30));
|
|
||||||
const sign = sortedSigns[signIndex] || null;
|
|
||||||
if (!sign) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
sign,
|
|
||||||
degreeInSign: normalized - signIndex * 30,
|
|
||||||
absoluteLongitude: normalized
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSabianSymbolForLongitude(longitude, sabianSymbols) {
|
|
||||||
const normalized = normalizeLongitude(longitude);
|
|
||||||
if (normalized === null || !Array.isArray(sabianSymbols) || !sabianSymbols.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const absoluteDegree = Math.floor(normalized) + 1;
|
|
||||||
return sabianSymbols.find((entry) => Number(entry?.absoluteDegree) === absoluteDegree) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculatePlanetPositions(referenceData, now) {
|
|
||||||
if (!window.Astronomy || !referenceData) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const positions = [];
|
|
||||||
|
|
||||||
PLANETARY_BODIES.forEach((body) => {
|
|
||||||
try {
|
|
||||||
const geoVector = window.Astronomy.GeoVector(body.astronomyBody, now, true);
|
|
||||||
const ecliptic = window.Astronomy.Ecliptic(geoVector);
|
|
||||||
const signInfo = getSignForLongitude(ecliptic?.elon, referenceData.signs);
|
|
||||||
if (!signInfo?.sign) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const planetInfo = referenceData.planets?.[body.id] || null;
|
|
||||||
const symbol = planetInfo?.symbol || body.fallbackSymbol;
|
|
||||||
const name = planetInfo?.name || body.fallbackName;
|
|
||||||
|
|
||||||
positions.push({
|
|
||||||
id: body.id,
|
|
||||||
symbol,
|
|
||||||
name,
|
|
||||||
longitude: signInfo.absoluteLongitude,
|
|
||||||
sign: signInfo.sign,
|
|
||||||
degreeInSign: signInfo.degreeInSign,
|
|
||||||
label: `${symbol} ${name}: ${signInfo.sign.symbol} ${signInfo.sign.name} ${signInfo.degreeInSign.toFixed(1)}°`
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return positions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateNowStats(referenceData, elements, now) {
|
|
||||||
const planetPositions = calculatePlanetPositions(referenceData, now);
|
|
||||||
|
|
||||||
if (elements.nowStatsPlanetsEl) {
|
|
||||||
elements.nowStatsPlanetsEl.replaceChildren();
|
|
||||||
|
|
||||||
if (!planetPositions.length) {
|
|
||||||
elements.nowStatsPlanetsEl.textContent = "--";
|
|
||||||
} else {
|
|
||||||
planetPositions.forEach((position) => {
|
|
||||||
const item = document.createElement("div");
|
|
||||||
item.className = "now-stats-planet";
|
|
||||||
item.textContent = position.label;
|
|
||||||
elements.nowStatsPlanetsEl.appendChild(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (elements.nowStatsSabianEl) {
|
|
||||||
const sunPosition = planetPositions.find((entry) => entry.id === "sol") || null;
|
|
||||||
const moonPosition = planetPositions.find((entry) => entry.id === "luna") || null;
|
|
||||||
const sunSabianSymbol = sunPosition
|
|
||||||
? getSabianSymbolForLongitude(sunPosition.longitude, referenceData.sabianSymbols)
|
|
||||||
: null;
|
|
||||||
const moonSabianSymbol = moonPosition
|
|
||||||
? getSabianSymbolForLongitude(moonPosition.longitude, referenceData.sabianSymbols)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const sunLine = sunSabianSymbol?.phrase
|
|
||||||
? `Sun Sabian ${sunSabianSymbol.absoluteDegree}: ${sunSabianSymbol.phrase}`
|
|
||||||
: "Sun Sabian: --";
|
|
||||||
const moonLine = moonSabianSymbol?.phrase
|
|
||||||
? `Moon Sabian ${moonSabianSymbol.absoluteDegree}: ${moonSabianSymbol.phrase}`
|
|
||||||
: "Moon Sabian: --";
|
|
||||||
|
|
||||||
elements.nowStatsSabianEl.textContent = `${sunLine}\n${moonLine}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCountdown(ms, mode) {
|
|
||||||
if (!Number.isFinite(ms) || ms <= 0) {
|
|
||||||
if (mode === "hours") {
|
|
||||||
return "0.0 hours";
|
|
||||||
}
|
|
||||||
if (mode === "seconds") {
|
|
||||||
return "0s";
|
|
||||||
}
|
|
||||||
return "0m";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === "hours") {
|
|
||||||
return `${(ms / 3600000).toFixed(1)} hours`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === "seconds") {
|
|
||||||
return `${Math.floor(ms / 1000)}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${Math.floor(ms / 60000)}m`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMonthDay(monthDay) {
|
|
||||||
const [month, day] = String(monthDay || "").split("-").map(Number);
|
|
||||||
return { month, day };
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentPhaseName(date) {
|
|
||||||
return getMoonPhaseName(window.SunCalc.getMoonIllumination(date).phase);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findNextMoonPhaseTransition(now) {
|
|
||||||
const currentPhase = getCurrentPhaseName(now);
|
|
||||||
const stepMs = 15 * 60 * 1000;
|
|
||||||
const maxMs = 40 * DAY_IN_MS;
|
|
||||||
|
|
||||||
let previousTime = now.getTime();
|
|
||||||
let previousPhase = currentPhase;
|
|
||||||
|
|
||||||
for (let t = previousTime + stepMs; t <= previousTime + maxMs; t += stepMs) {
|
|
||||||
const phaseName = getCurrentPhaseName(new Date(t));
|
|
||||||
if (phaseName !== previousPhase) {
|
|
||||||
let low = previousTime;
|
|
||||||
let high = t;
|
|
||||||
while (high - low > 1000) {
|
|
||||||
const mid = Math.floor((low + high) / 2);
|
|
||||||
const midPhase = getCurrentPhaseName(new Date(mid));
|
|
||||||
if (midPhase === currentPhase) {
|
|
||||||
low = mid;
|
|
||||||
} else {
|
|
||||||
high = mid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const transitionAt = new Date(high);
|
|
||||||
const nextPhase = getCurrentPhaseName(new Date(high + 1000));
|
|
||||||
return {
|
|
||||||
fromPhase: currentPhase,
|
|
||||||
nextPhase,
|
|
||||||
changeAt: transitionAt
|
|
||||||
};
|
|
||||||
}
|
|
||||||
previousTime = t;
|
|
||||||
previousPhase = phaseName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSignStartDate(now, sign) {
|
|
||||||
const { month: startMonth, day: startDay } = parseMonthDay(sign.start);
|
|
||||||
const { month: endMonth } = parseMonthDay(sign.end);
|
|
||||||
const wrapsYear = startMonth > endMonth;
|
|
||||||
|
|
||||||
let year = now.getFullYear();
|
|
||||||
const nowMonth = now.getMonth() + 1;
|
|
||||||
const nowDay = now.getDate();
|
|
||||||
|
|
||||||
if (wrapsYear && (nowMonth < startMonth || (nowMonth === startMonth && nowDay < startDay))) {
|
|
||||||
year -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Date(year, startMonth - 1, startDay);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNextSign(signs, currentSign) {
|
|
||||||
const sorted = [...signs].sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
||||||
const index = sorted.findIndex((entry) => entry.id === currentSign.id);
|
|
||||||
if (index < 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return sorted[(index + 1) % sorted.length] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDecanByIndex(decansBySign, signId, index) {
|
|
||||||
const signDecans = decansBySign[signId] || [];
|
|
||||||
return signDecans.find((entry) => entry.index === index) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findNextDecanTransition(now, signs, decansBySign) {
|
|
||||||
const currentInfo = getDecanForDate(now, signs, decansBySign);
|
|
||||||
if (!currentInfo?.sign) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentIndex = currentInfo.decan?.index || 1;
|
|
||||||
const signStart = getSignStartDate(now, currentInfo.sign);
|
|
||||||
|
|
||||||
if (currentIndex < 3) {
|
|
||||||
const changeAt = new Date(signStart.getTime() + currentIndex * 10 * DAY_IN_MS);
|
|
||||||
const nextDecan = getDecanByIndex(decansBySign, currentInfo.sign.id, currentIndex + 1);
|
|
||||||
const nextLabel = nextDecan?.tarotMinorArcana || `${currentInfo.sign.name} Decan ${currentIndex + 1}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
key: `${currentInfo.sign.id}-${currentIndex}`,
|
|
||||||
changeAt,
|
|
||||||
nextLabel
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextSign = getNextSign(signs, currentInfo.sign);
|
|
||||||
if (!nextSign) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { month: nextMonth, day: nextDay } = parseMonthDay(nextSign.start);
|
|
||||||
let year = now.getFullYear();
|
|
||||||
let changeAt = new Date(year, nextMonth - 1, nextDay);
|
|
||||||
if (changeAt.getTime() <= now.getTime()) {
|
|
||||||
changeAt = new Date(year + 1, nextMonth - 1, nextDay);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextDecan = getDecanByIndex(decansBySign, nextSign.id, 1);
|
|
||||||
|
|
||||||
return {
|
|
||||||
key: `${currentInfo.sign.id}-${currentIndex}`,
|
|
||||||
changeAt,
|
|
||||||
nextLabel: nextDecan?.tarotMinorArcana || `${nextSign.name} Decan 1`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNowCardImage(imageEl, cardName, fallbackLabel, trumpNumber) {
|
|
||||||
if (!imageEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bindNowCardLightbox(imageEl);
|
|
||||||
|
|
||||||
if (!cardName || typeof resolveTarotCardImage !== "function") {
|
|
||||||
imageEl.style.display = "none";
|
|
||||||
imageEl.removeAttribute("src");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const src = resolveTarotCardImage(cardName);
|
|
||||||
if (!src) {
|
|
||||||
imageEl.style.display = "none";
|
|
||||||
imageEl.removeAttribute("src");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
imageEl.src = src;
|
|
||||||
const displayName = getDisplayTarotName(cardName, trumpNumber);
|
|
||||||
imageEl.alt = `${fallbackLabel}: ${displayName}`;
|
|
||||||
imageEl.style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateNowPanel(referenceData, geo, elements, timeFormat = "minutes") {
|
function updateNowPanel(referenceData, geo, elements, timeFormat = "minutes") {
|
||||||
if (!referenceData || !geo || !elements) {
|
if (!referenceData || !geo || !elements) {
|
||||||
@@ -543,12 +53,12 @@
|
|||||||
const hourCardName = planet?.tarot?.majorArcana || "";
|
const hourCardName = planet?.tarot?.majorArcana || "";
|
||||||
const hourTrumpNumber = planet?.tarot?.number;
|
const hourTrumpNumber = planet?.tarot?.number;
|
||||||
elements.nowHourTarotEl.textContent = hourCardName
|
elements.nowHourTarotEl.textContent = hourCardName
|
||||||
? getDisplayTarotName(hourCardName, hourTrumpNumber)
|
? nowUiHelpers.getDisplayTarotName(hourCardName, hourTrumpNumber)
|
||||||
: "--";
|
: "--";
|
||||||
}
|
}
|
||||||
|
|
||||||
const msLeft = Math.max(0, currentHour.end.getTime() - now.getTime());
|
const msLeft = Math.max(0, currentHour.end.getTime() - now.getTime());
|
||||||
elements.nowCountdownEl.textContent = formatCountdown(msLeft, timeFormat);
|
elements.nowCountdownEl.textContent = nowUiHelpers.formatCountdown(msLeft, timeFormat);
|
||||||
|
|
||||||
if (elements.nowHourNextEl) {
|
if (elements.nowHourNextEl) {
|
||||||
const nextHour = allHours.find(
|
const nextHour = allHours.find(
|
||||||
@@ -564,7 +74,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setNowCardImage(
|
nowUiHelpers.setNowCardImage(
|
||||||
elements.nowHourCardEl,
|
elements.nowHourCardEl,
|
||||||
planet?.tarot?.majorArcana,
|
planet?.tarot?.majorArcana,
|
||||||
"Current planetary hour card",
|
"Current planetary hour card",
|
||||||
@@ -579,15 +89,15 @@
|
|||||||
if (elements.nowHourNextEl) {
|
if (elements.nowHourNextEl) {
|
||||||
elements.nowHourNextEl.textContent = "> --";
|
elements.nowHourNextEl.textContent = "> --";
|
||||||
}
|
}
|
||||||
setNowCardImage(elements.nowHourCardEl, null, "Current planetary hour card");
|
nowUiHelpers.setNowCardImage(elements.nowHourCardEl, null, "Current planetary hour card");
|
||||||
}
|
}
|
||||||
|
|
||||||
const moonIllum = window.SunCalc.getMoonIllumination(now);
|
const moonIllum = window.SunCalc.getMoonIllumination(now);
|
||||||
const moonPhase = getMoonPhaseName(moonIllum.phase);
|
const moonPhase = getMoonPhaseName(moonIllum.phase);
|
||||||
const moonTarot = referenceData.planets.luna?.tarot?.majorArcana || "The High Priestess";
|
const moonTarot = referenceData.planets.luna?.tarot?.majorArcana || "The High Priestess";
|
||||||
elements.nowMoonEl.textContent = `${moonPhase} (${Math.round(moonIllum.fraction * 100)}%)`;
|
elements.nowMoonEl.textContent = `${moonPhase} (${Math.round(moonIllum.fraction * 100)}%)`;
|
||||||
elements.nowMoonTarotEl.textContent = getDisplayTarotName(moonTarot, referenceData.planets.luna?.tarot?.number);
|
elements.nowMoonTarotEl.textContent = nowUiHelpers.getDisplayTarotName(moonTarot, referenceData.planets.luna?.tarot?.number);
|
||||||
setNowCardImage(
|
nowUiHelpers.setNowCardImage(
|
||||||
elements.nowMoonCardEl,
|
elements.nowMoonCardEl,
|
||||||
moonTarot,
|
moonTarot,
|
||||||
"Current moon phase card",
|
"Current moon phase card",
|
||||||
@@ -595,13 +105,13 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!moonCountdownCache || moonCountdownCache.fromPhase !== moonPhase || now >= moonCountdownCache.changeAt) {
|
if (!moonCountdownCache || moonCountdownCache.fromPhase !== moonPhase || now >= moonCountdownCache.changeAt) {
|
||||||
moonCountdownCache = findNextMoonPhaseTransition(now);
|
moonCountdownCache = nowUiHelpers.findNextMoonPhaseTransition(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elements.nowMoonCountdownEl) {
|
if (elements.nowMoonCountdownEl) {
|
||||||
if (moonCountdownCache?.changeAt) {
|
if (moonCountdownCache?.changeAt) {
|
||||||
const remaining = moonCountdownCache.changeAt.getTime() - now.getTime();
|
const remaining = moonCountdownCache.changeAt.getTime() - now.getTime();
|
||||||
elements.nowMoonCountdownEl.textContent = formatCountdown(remaining, timeFormat);
|
elements.nowMoonCountdownEl.textContent = nowUiHelpers.formatCountdown(remaining, timeFormat);
|
||||||
if (elements.nowMoonNextEl) {
|
if (elements.nowMoonNextEl) {
|
||||||
elements.nowMoonNextEl.textContent = `> ${moonCountdownCache.nextPhase}`;
|
elements.nowMoonNextEl.textContent = `> ${moonCountdownCache.nextPhase}`;
|
||||||
}
|
}
|
||||||
@@ -621,24 +131,24 @@
|
|||||||
const signStartDate = getSignStartDate(now, sunInfo.sign);
|
const signStartDate = getSignStartDate(now, sunInfo.sign);
|
||||||
const daysSinceSignStart = (now.getTime() - signStartDate.getTime()) / DAY_IN_MS;
|
const daysSinceSignStart = (now.getTime() - signStartDate.getTime()) / DAY_IN_MS;
|
||||||
const signDegree = Math.min(29.9, Math.max(0, daysSinceSignStart));
|
const signDegree = Math.min(29.9, Math.max(0, daysSinceSignStart));
|
||||||
const signMajorName = getDisplayTarotName(sunInfo.sign.tarot.majorArcana, sunInfo.sign.tarot.trumpNumber);
|
const signMajorName = nowUiHelpers.getDisplayTarotName(sunInfo.sign.tarot.majorArcana, sunInfo.sign.tarot.trumpNumber);
|
||||||
elements.nowDecanEl.textContent = `${sunInfo.sign.symbol} ${sunInfo.sign.name} · ${signMajorName} (${signDegree.toFixed(1)}°)`;
|
elements.nowDecanEl.textContent = `${sunInfo.sign.symbol} ${sunInfo.sign.name} · ${signMajorName} (${signDegree.toFixed(1)}°)`;
|
||||||
|
|
||||||
const currentDecanKey = `${sunInfo.sign.id}-${sunInfo.decan?.index || 1}`;
|
const currentDecanKey = `${sunInfo.sign.id}-${sunInfo.decan?.index || 1}`;
|
||||||
if (!decanCountdownCache || decanCountdownCache.key !== currentDecanKey || now >= decanCountdownCache.changeAt) {
|
if (!decanCountdownCache || decanCountdownCache.key !== currentDecanKey || now >= decanCountdownCache.changeAt) {
|
||||||
decanCountdownCache = findNextDecanTransition(now, referenceData.signs, referenceData.decansBySign);
|
decanCountdownCache = nowUiHelpers.findNextDecanTransition(now, referenceData.signs, referenceData.decansBySign);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sunInfo.decan) {
|
if (sunInfo.decan) {
|
||||||
const decanCardName = sunInfo.decan.tarotMinorArcana;
|
const decanCardName = sunInfo.decan.tarotMinorArcana;
|
||||||
elements.nowDecanTarotEl.textContent = getDisplayTarotName(decanCardName);
|
elements.nowDecanTarotEl.textContent = nowUiHelpers.getDisplayTarotName(decanCardName);
|
||||||
setNowCardImage(elements.nowDecanCardEl, sunInfo.decan.tarotMinorArcana, "Current decan card");
|
nowUiHelpers.setNowCardImage(elements.nowDecanCardEl, sunInfo.decan.tarotMinorArcana, "Current decan card");
|
||||||
} else {
|
} else {
|
||||||
const signTarotName = sunInfo.sign.tarot?.majorArcana || "--";
|
const signTarotName = sunInfo.sign.tarot?.majorArcana || "--";
|
||||||
elements.nowDecanTarotEl.textContent = signTarotName === "--"
|
elements.nowDecanTarotEl.textContent = signTarotName === "--"
|
||||||
? "--"
|
? "--"
|
||||||
: getDisplayTarotName(signTarotName, sunInfo.sign.tarot?.trumpNumber);
|
: nowUiHelpers.getDisplayTarotName(signTarotName, sunInfo.sign.tarot?.trumpNumber);
|
||||||
setNowCardImage(
|
nowUiHelpers.setNowCardImage(
|
||||||
elements.nowDecanCardEl,
|
elements.nowDecanCardEl,
|
||||||
sunInfo.sign.tarot?.majorArcana,
|
sunInfo.sign.tarot?.majorArcana,
|
||||||
"Current decan card",
|
"Current decan card",
|
||||||
@@ -649,9 +159,9 @@
|
|||||||
if (elements.nowDecanCountdownEl) {
|
if (elements.nowDecanCountdownEl) {
|
||||||
if (decanCountdownCache?.changeAt) {
|
if (decanCountdownCache?.changeAt) {
|
||||||
const remaining = decanCountdownCache.changeAt.getTime() - now.getTime();
|
const remaining = decanCountdownCache.changeAt.getTime() - now.getTime();
|
||||||
elements.nowDecanCountdownEl.textContent = formatCountdown(remaining, timeFormat);
|
elements.nowDecanCountdownEl.textContent = nowUiHelpers.formatCountdown(remaining, timeFormat);
|
||||||
if (elements.nowDecanNextEl) {
|
if (elements.nowDecanNextEl) {
|
||||||
elements.nowDecanNextEl.textContent = `> ${getDisplayTarotName(decanCountdownCache.nextLabel)}`;
|
elements.nowDecanNextEl.textContent = `> ${nowUiHelpers.getDisplayTarotName(decanCountdownCache.nextLabel)}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
elements.nowDecanCountdownEl.textContent = "--";
|
elements.nowDecanCountdownEl.textContent = "--";
|
||||||
@@ -663,7 +173,7 @@
|
|||||||
} else {
|
} else {
|
||||||
elements.nowDecanEl.textContent = "--";
|
elements.nowDecanEl.textContent = "--";
|
||||||
elements.nowDecanTarotEl.textContent = "--";
|
elements.nowDecanTarotEl.textContent = "--";
|
||||||
setNowCardImage(elements.nowDecanCardEl, null, "Current decan card");
|
nowUiHelpers.setNowCardImage(elements.nowDecanCardEl, null, "Current decan card");
|
||||||
if (elements.nowDecanCountdownEl) {
|
if (elements.nowDecanCountdownEl) {
|
||||||
elements.nowDecanCountdownEl.textContent = "--";
|
elements.nowDecanCountdownEl.textContent = "--";
|
||||||
}
|
}
|
||||||
@@ -672,7 +182,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNowStats(referenceData, elements, now);
|
nowUiHelpers.updateNowStats(referenceData, elements, now);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dayKey,
|
dayKey,
|
||||||
|
|||||||
634
app/ui-numbers-detail.js
Normal file
634
app/ui-numbers-detail.js
Normal file
@@ -0,0 +1,634 @@
|
|||||||
|
(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
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -11,6 +11,11 @@
|
|||||||
|
|
||||||
const NUMBERS_SPECIAL_BASE_VALUES = [1, 2, 3, 4];
|
const NUMBERS_SPECIAL_BASE_VALUES = [1, 2, 3, 4];
|
||||||
const numbersSpecialFlipState = new Map();
|
const numbersSpecialFlipState = new Map();
|
||||||
|
const numbersDetailUi = window.NumbersDetailUi || {};
|
||||||
|
|
||||||
|
if (typeof numbersDetailUi.renderNumberDetail !== "function") {
|
||||||
|
throw new Error("NumbersDetailUi module must load before ui-numbers.js");
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_NUMBER_ENTRIES = Array.from({ length: 10 }, (_, value) => ({
|
const DEFAULT_NUMBER_ENTRIES = Array.from({ length: 10 }, (_, value) => ({
|
||||||
value,
|
value,
|
||||||
@@ -194,55 +199,6 @@
|
|||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCalendarMonthLinksForNumber(value) {
|
|
||||||
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 rankLabelToTarotMinorRank(rankLabel) {
|
function rankLabelToTarotMinorRank(rankLabel) {
|
||||||
const key = String(rankLabel || "").trim().toLowerCase();
|
const key = String(rankLabel || "").trim().toLowerCase();
|
||||||
if (key === "10" || key === "ten") return "Princess";
|
if (key === "10" || key === "ten") return "Princess";
|
||||||
@@ -252,409 +208,6 @@
|
|||||||
return String(rankLabel || "").trim();
|
return String(rankLabel || "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFallbackPlayingDeckEntries() {
|
|
||||||
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() {
|
|
||||||
const deckData = getMagickDataset()?.grouped?.["playing-cards-52"];
|
|
||||||
const rawEntries = Array.isArray(deckData)
|
|
||||||
? deckData
|
|
||||||
: (Array.isArray(deckData?.entries) ? deckData.entries : []);
|
|
||||||
|
|
||||||
if (!rawEntries.length) {
|
|
||||||
return buildFallbackPlayingDeckEntries();
|
|
||||||
}
|
|
||||||
|
|
||||||
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(playingSuit) {
|
|
||||||
const suit = String(playingSuit || "hearts").trim().toLowerCase();
|
|
||||||
const selectedSuit = ["hearts", "diamonds", "clubs", "spades"].includes(suit) ? suit : "hearts";
|
|
||||||
const deckEntries = getPlayingDeckEntries();
|
|
||||||
|
|
||||||
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(value) {
|
|
||||||
const { specialPanelEl } = getElements();
|
|
||||||
if (!specialPanelEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = getNumberEntryByValue(value);
|
|
||||||
const playingSuit = entry?.associations?.playingSuit || "hearts";
|
|
||||||
const boardCardEl = buildNumbersSpecialCardSlots(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(card) {
|
|
||||||
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(targetRoot) {
|
|
||||||
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(targetRoot, numberEntry = null) {
|
|
||||||
const referenceData = getReferenceData();
|
|
||||||
const magickDataset = getMagickDataset();
|
|
||||||
if (typeof config.ensureTarotSection === "function" && referenceData) {
|
|
||||||
config.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(card);
|
|
||||||
return numberValue !== null && computeDigitalRoot(numberValue) === targetRoot;
|
|
||||||
});
|
|
||||||
|
|
||||||
return filteredCards
|
|
||||||
.sort((left, right) => {
|
|
||||||
const leftNumber = extractTarotCardNumericValue(left);
|
|
||||||
const rightNumber = extractTarotCardNumericValue(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 renderNumbersList() {
|
function renderNumbersList() {
|
||||||
const { listEl, countEl } = getElements();
|
const { listEl, countEl } = getElements();
|
||||||
if (!listEl) {
|
if (!listEl) {
|
||||||
@@ -693,187 +246,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDetailRenderContext(value) {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
elements: getElements(),
|
||||||
|
getReferenceData,
|
||||||
|
getMagickDataset,
|
||||||
|
getNumberEntryByValue,
|
||||||
|
normalizeNumberValue,
|
||||||
|
computeDigitalRoot,
|
||||||
|
rankLabelToTarotMinorRank,
|
||||||
|
ensureTarotSection: config.ensureTarotSection,
|
||||||
|
selectNumberEntry,
|
||||||
|
NUMBERS_SPECIAL_BASE_VALUES,
|
||||||
|
numbersSpecialFlipState,
|
||||||
|
PLAYING_SUIT_SYMBOL,
|
||||||
|
PLAYING_SUIT_LABEL,
|
||||||
|
PLAYING_SUIT_TO_TAROT,
|
||||||
|
PLAYING_RANKS,
|
||||||
|
TAROT_RANK_NUMBER_MAP
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function renderNumberDetail(value) {
|
function renderNumberDetail(value) {
|
||||||
const { detailNameEl, detailTypeEl, detailSummaryEl, detailBodyEl } = getElements();
|
numbersDetailUi.renderNumberDetail(getDetailRenderContext(value));
|
||||||
const entry = getNumberEntryByValue(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(normalized);
|
|
||||||
|
|
||||||
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(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(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(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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectNumberEntry(value) {
|
function selectNumberEntry(value) {
|
||||||
|
|||||||
330
app/ui-planets-references.js
Normal file
330
app/ui-planets-references.js
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function buildMonthReferencesByPlanet(context) {
|
||||||
|
const { referenceData, toPlanetId, normalizePlanetToken } = context;
|
||||||
|
const map = new Map();
|
||||||
|
const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [];
|
||||||
|
const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
||||||
|
const monthById = new Map(months.map((month) => [month.id, month]));
|
||||||
|
|
||||||
|
function parseMonthDayToken(value) {
|
||||||
|
const text = String(value || "").trim();
|
||||||
|
const match = text.match(/^(\d{1,2})-(\d{1,2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthNo = Number(match[1]);
|
||||||
|
const dayNo = Number(match[2]);
|
||||||
|
if (!Number.isInteger(monthNo) || !Number.isInteger(dayNo) || monthNo < 1 || monthNo > 12 || dayNo < 1 || dayNo > 31) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { month: monthNo, day: dayNo };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMonthDayTokensFromText(value) {
|
||||||
|
const text = String(value || "");
|
||||||
|
const matches = [...text.matchAll(/(\d{1,2})-(\d{1,2})/g)];
|
||||||
|
return matches
|
||||||
|
.map((match) => ({ month: Number(match[1]), day: Number(match[2]) }))
|
||||||
|
.filter((token) => Number.isInteger(token.month) && Number.isInteger(token.day) && token.month >= 1 && token.month <= 12 && token.day >= 1 && token.day <= 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDateToken(token, year) {
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Date(year, token.month - 1, token.day, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMonthDayRangeByMonth(startToken, endToken) {
|
||||||
|
const startDate = toDateToken(startToken, 2025);
|
||||||
|
const endBase = toDateToken(endToken, 2025);
|
||||||
|
if (!startDate || !endBase) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapsYear = endBase.getTime() < startDate.getTime();
|
||||||
|
const endDate = wrapsYear ? toDateToken(endToken, 2026) : endBase;
|
||||||
|
if (!endDate) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = [];
|
||||||
|
let cursor = new Date(startDate);
|
||||||
|
while (cursor.getTime() <= endDate.getTime()) {
|
||||||
|
const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0);
|
||||||
|
const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate;
|
||||||
|
|
||||||
|
segments.push({
|
||||||
|
monthNo: cursor.getMonth() + 1,
|
||||||
|
startDay: cursor.getDate(),
|
||||||
|
endDay: segmentEnd.getDate()
|
||||||
|
});
|
||||||
|
|
||||||
|
cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenToString(monthNo, dayNo) {
|
||||||
|
return `${String(monthNo).padStart(2, "0")}-${String(dayNo).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRangeLabel(monthName, startDay, endDay) {
|
||||||
|
if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) {
|
||||||
|
return monthName;
|
||||||
|
}
|
||||||
|
if (startDay === endDay) {
|
||||||
|
return `${monthName} ${startDay}`;
|
||||||
|
}
|
||||||
|
return `${monthName} ${startDay}-${endDay}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRangeForMonth(month, options = {}) {
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
const monthStart = parseMonthDayToken(month?.start);
|
||||||
|
const monthEnd = parseMonthDayToken(month?.end);
|
||||||
|
if (!Number.isFinite(monthOrder) || !monthStart || !monthEnd) {
|
||||||
|
return {
|
||||||
|
startToken: String(month?.start || "").trim() || null,
|
||||||
|
endToken: String(month?.end || "").trim() || null,
|
||||||
|
label: month?.name || month?.id || "",
|
||||||
|
isFullMonth: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let startToken = parseMonthDayToken(options.startToken);
|
||||||
|
let endToken = parseMonthDayToken(options.endToken);
|
||||||
|
|
||||||
|
if (!startToken || !endToken) {
|
||||||
|
const tokens = parseMonthDayTokensFromText(options.rawDateText);
|
||||||
|
if (tokens.length >= 2) {
|
||||||
|
startToken = tokens[0];
|
||||||
|
endToken = tokens[1];
|
||||||
|
} else if (tokens.length === 1) {
|
||||||
|
startToken = tokens[0];
|
||||||
|
endToken = tokens[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startToken || !endToken) {
|
||||||
|
startToken = monthStart;
|
||||||
|
endToken = monthEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = splitMonthDayRangeByMonth(startToken, endToken);
|
||||||
|
const segment = segments.find((entry) => entry.monthNo === monthOrder) || null;
|
||||||
|
|
||||||
|
const useStart = segment ? { month: monthOrder, day: segment.startDay } : startToken;
|
||||||
|
const useEnd = segment ? { month: monthOrder, day: segment.endDay } : endToken;
|
||||||
|
const startText = tokenToString(useStart.month, useStart.day);
|
||||||
|
const endText = tokenToString(useEnd.month, useEnd.day);
|
||||||
|
const isFullMonth = startText === month.start && endText === month.end;
|
||||||
|
|
||||||
|
return {
|
||||||
|
startToken: startText,
|
||||||
|
endToken: endText,
|
||||||
|
label: isFullMonth
|
||||||
|
? (month.name || month.id)
|
||||||
|
: formatRangeLabel(month.name || month.id, useStart.day, useEnd.day),
|
||||||
|
isFullMonth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushRef(planetToken, month, options = {}) {
|
||||||
|
const planetId = toPlanetId(planetToken) || normalizePlanetToken(planetToken);
|
||||||
|
if (!planetId || !month?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.has(planetId)) {
|
||||||
|
map.set(planetId, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = map.get(planetId);
|
||||||
|
const range = resolveRangeForMonth(month, options);
|
||||||
|
const key = `${month.id}|${range.startToken || ""}|${range.endToken || ""}`;
|
||||||
|
if (rows.some((entry) => entry.key === key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
id: month.id,
|
||||||
|
name: month.name || month.id,
|
||||||
|
order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999,
|
||||||
|
label: range.label,
|
||||||
|
startToken: range.startToken,
|
||||||
|
endToken: range.endToken,
|
||||||
|
isFullMonth: range.isFullMonth,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
months.forEach((month) => {
|
||||||
|
pushRef(month?.associations?.planetId, month);
|
||||||
|
const events = Array.isArray(month?.events) ? month.events : [];
|
||||||
|
events.forEach((event) => {
|
||||||
|
pushRef(event?.associations?.planetId, month, {
|
||||||
|
rawDateText: event?.dateRange || event?.date || ""
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
holidays.forEach((holiday) => {
|
||||||
|
const month = monthById.get(holiday?.monthId);
|
||||||
|
if (!month) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pushRef(holiday?.associations?.planetId, month, {
|
||||||
|
rawDateText: holiday?.dateRange || holiday?.date || ""
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
map.forEach((rows, key) => {
|
||||||
|
const preciseMonthIds = new Set(
|
||||||
|
rows
|
||||||
|
.filter((entry) => !entry.isFullMonth)
|
||||||
|
.map((entry) => entry.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const filtered = rows.filter((entry) => {
|
||||||
|
if (!entry.isFullMonth) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !preciseMonthIds.has(entry.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
filtered.sort((left, right) => {
|
||||||
|
if (left.order !== right.order) {
|
||||||
|
return left.order - right.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startLeft = parseMonthDayToken(left.startToken);
|
||||||
|
const startRight = parseMonthDayToken(right.startToken);
|
||||||
|
const dayLeft = startLeft ? startLeft.day : 999;
|
||||||
|
const dayRight = startRight ? startRight.day : 999;
|
||||||
|
if (dayLeft !== dayRight) {
|
||||||
|
return dayLeft - dayRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(left.label || left.name || "").localeCompare(String(right.label || right.name || ""));
|
||||||
|
});
|
||||||
|
|
||||||
|
map.set(key, filtered);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCubePlacementsByPlanet(context) {
|
||||||
|
const { magickDataset, toPlanetId } = context;
|
||||||
|
const map = new Map();
|
||||||
|
const cube = magickDataset?.grouped?.kabbalah?.cube || {};
|
||||||
|
const walls = Array.isArray(cube?.walls)
|
||||||
|
? cube.walls
|
||||||
|
: [];
|
||||||
|
const edges = Array.isArray(cube?.edges)
|
||||||
|
? cube.edges
|
||||||
|
: [];
|
||||||
|
|
||||||
|
function edgeWalls(edge) {
|
||||||
|
const explicitWalls = Array.isArray(edge?.walls)
|
||||||
|
? edge.walls.map((wallId) => String(wallId || "").trim().toLowerCase()).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (explicitWalls.length >= 2) {
|
||||||
|
return explicitWalls.slice(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(edge?.id || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.split("-")
|
||||||
|
.map((wallId) => wallId.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function edgeLabel(edge) {
|
||||||
|
const explicitName = String(edge?.name || "").trim();
|
||||||
|
if (explicitName) {
|
||||||
|
return explicitName;
|
||||||
|
}
|
||||||
|
return edgeWalls(edge)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCubeDirectionLabel(wallId, edge) {
|
||||||
|
const normalizedWallId = String(wallId || "").trim().toLowerCase();
|
||||||
|
const edgeId = String(edge?.id || "").trim().toLowerCase();
|
||||||
|
if (!normalizedWallId || !edgeId) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const cubeUi = window.CubeSectionUi;
|
||||||
|
if (cubeUi && typeof cubeUi.getEdgeDirectionLabelForWall === "function") {
|
||||||
|
const directionLabel = String(cubeUi.getEdgeDirectionLabelForWall(normalizedWallId, edgeId) || "").trim();
|
||||||
|
if (directionLabel) {
|
||||||
|
return directionLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return edgeLabel(edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstEdgeByWallId = new Map();
|
||||||
|
edges.forEach((edge) => {
|
||||||
|
edgeWalls(edge).forEach((wallId) => {
|
||||||
|
if (!firstEdgeByWallId.has(wallId)) {
|
||||||
|
firstEdgeByWallId.set(wallId, edge);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function pushPlacement(planetId, placement) {
|
||||||
|
if (!planetId || !placement?.wallId || !placement?.edgeId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.has(planetId)) {
|
||||||
|
map.set(planetId, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = map.get(planetId);
|
||||||
|
const key = `${placement.wallId}:${placement.edgeId}`;
|
||||||
|
if (rows.some((row) => `${row.wallId}:${row.edgeId}` === key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push(placement);
|
||||||
|
}
|
||||||
|
|
||||||
|
walls.forEach((wall) => {
|
||||||
|
const planetId = toPlanetId(wall?.associations?.planetId || wall?.planet);
|
||||||
|
if (!planetId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallId = String(wall?.id || "").trim().toLowerCase();
|
||||||
|
const edge = firstEdgeByWallId.get(wallId) || null;
|
||||||
|
|
||||||
|
pushPlacement(planetId, {
|
||||||
|
wallId,
|
||||||
|
edgeId: String(edge?.id || "").trim().toLowerCase(),
|
||||||
|
label: `Cube: ${wall?.name || "Wall"} Wall - ${resolveCubeDirectionLabel(wallId, edge) || "Direction"}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.PlanetReferenceBuilders = {
|
||||||
|
buildMonthReferencesByPlanet,
|
||||||
|
buildCubePlacementsByPlanet
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -1,5 +1,15 @@
|
|||||||
(function () {
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
const { getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
|
const { getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
|
||||||
|
const planetReferenceBuilders = window.PlanetReferenceBuilders || {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof planetReferenceBuilders.buildMonthReferencesByPlanet !== "function"
|
||||||
|
|| typeof planetReferenceBuilders.buildCubePlacementsByPlanet !== "function"
|
||||||
|
) {
|
||||||
|
throw new Error("PlanetReferenceBuilders module must load before ui-planets.js");
|
||||||
|
}
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@@ -68,326 +78,6 @@
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMonthReferencesByPlanet(referenceData) {
|
|
||||||
const map = new Map();
|
|
||||||
const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [];
|
|
||||||
const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
|
||||||
const monthById = new Map(months.map((month) => [month.id, month]));
|
|
||||||
|
|
||||||
function parseMonthDayToken(value) {
|
|
||||||
const text = String(value || "").trim();
|
|
||||||
const match = text.match(/^(\d{1,2})-(\d{1,2})$/);
|
|
||||||
if (!match) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthNo = Number(match[1]);
|
|
||||||
const dayNo = Number(match[2]);
|
|
||||||
if (!Number.isInteger(monthNo) || !Number.isInteger(dayNo) || monthNo < 1 || monthNo > 12 || dayNo < 1 || dayNo > 31) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { month: monthNo, day: dayNo };
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMonthDayTokensFromText(value) {
|
|
||||||
const text = String(value || "");
|
|
||||||
const matches = [...text.matchAll(/(\d{1,2})-(\d{1,2})/g)];
|
|
||||||
return matches
|
|
||||||
.map((match) => ({ month: Number(match[1]), day: Number(match[2]) }))
|
|
||||||
.filter((token) => Number.isInteger(token.month) && Number.isInteger(token.day) && token.month >= 1 && token.month <= 12 && token.day >= 1 && token.day <= 31);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toDateToken(token, year) {
|
|
||||||
if (!token) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Date(year, token.month - 1, token.day, 12, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitMonthDayRangeByMonth(startToken, endToken) {
|
|
||||||
const startDate = toDateToken(startToken, 2025);
|
|
||||||
const endBase = toDateToken(endToken, 2025);
|
|
||||||
if (!startDate || !endBase) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapsYear = endBase.getTime() < startDate.getTime();
|
|
||||||
const endDate = wrapsYear ? toDateToken(endToken, 2026) : endBase;
|
|
||||||
if (!endDate) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const segments = [];
|
|
||||||
let cursor = new Date(startDate);
|
|
||||||
while (cursor.getTime() <= endDate.getTime()) {
|
|
||||||
const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0);
|
|
||||||
const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate;
|
|
||||||
|
|
||||||
segments.push({
|
|
||||||
monthNo: cursor.getMonth() + 1,
|
|
||||||
startDay: cursor.getDate(),
|
|
||||||
endDay: segmentEnd.getDate()
|
|
||||||
});
|
|
||||||
|
|
||||||
cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return segments;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenToString(monthNo, dayNo) {
|
|
||||||
return `${String(monthNo).padStart(2, "0")}-${String(dayNo).padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatRangeLabel(monthName, startDay, endDay) {
|
|
||||||
if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) {
|
|
||||||
return monthName;
|
|
||||||
}
|
|
||||||
if (startDay === endDay) {
|
|
||||||
return `${monthName} ${startDay}`;
|
|
||||||
}
|
|
||||||
return `${monthName} ${startDay}-${endDay}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveRangeForMonth(month, options = {}) {
|
|
||||||
const monthOrder = Number(month?.order);
|
|
||||||
const monthStart = parseMonthDayToken(month?.start);
|
|
||||||
const monthEnd = parseMonthDayToken(month?.end);
|
|
||||||
if (!Number.isFinite(monthOrder) || !monthStart || !monthEnd) {
|
|
||||||
return {
|
|
||||||
startToken: String(month?.start || "").trim() || null,
|
|
||||||
endToken: String(month?.end || "").trim() || null,
|
|
||||||
label: month?.name || month?.id || "",
|
|
||||||
isFullMonth: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let startToken = parseMonthDayToken(options.startToken);
|
|
||||||
let endToken = parseMonthDayToken(options.endToken);
|
|
||||||
|
|
||||||
if (!startToken || !endToken) {
|
|
||||||
const tokens = parseMonthDayTokensFromText(options.rawDateText);
|
|
||||||
if (tokens.length >= 2) {
|
|
||||||
startToken = tokens[0];
|
|
||||||
endToken = tokens[1];
|
|
||||||
} else if (tokens.length === 1) {
|
|
||||||
startToken = tokens[0];
|
|
||||||
endToken = tokens[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!startToken || !endToken) {
|
|
||||||
startToken = monthStart;
|
|
||||||
endToken = monthEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
const segments = splitMonthDayRangeByMonth(startToken, endToken);
|
|
||||||
const segment = segments.find((entry) => entry.monthNo === monthOrder) || null;
|
|
||||||
|
|
||||||
const useStart = segment ? { month: monthOrder, day: segment.startDay } : startToken;
|
|
||||||
const useEnd = segment ? { month: monthOrder, day: segment.endDay } : endToken;
|
|
||||||
const startText = tokenToString(useStart.month, useStart.day);
|
|
||||||
const endText = tokenToString(useEnd.month, useEnd.day);
|
|
||||||
const isFullMonth = startText === month.start && endText === month.end;
|
|
||||||
|
|
||||||
return {
|
|
||||||
startToken: startText,
|
|
||||||
endToken: endText,
|
|
||||||
label: isFullMonth
|
|
||||||
? (month.name || month.id)
|
|
||||||
: formatRangeLabel(month.name || month.id, useStart.day, useEnd.day),
|
|
||||||
isFullMonth
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushRef(planetToken, month, options = {}) {
|
|
||||||
const planetId = toPlanetId(planetToken) || normalizePlanetToken(planetToken);
|
|
||||||
if (!planetId || !month?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map.has(planetId)) {
|
|
||||||
map.set(planetId, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = map.get(planetId);
|
|
||||||
const range = resolveRangeForMonth(month, options);
|
|
||||||
const key = `${month.id}|${range.startToken || ""}|${range.endToken || ""}`;
|
|
||||||
if (rows.some((entry) => entry.key === key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rows.push({
|
|
||||||
id: month.id,
|
|
||||||
name: month.name || month.id,
|
|
||||||
order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999,
|
|
||||||
label: range.label,
|
|
||||||
startToken: range.startToken,
|
|
||||||
endToken: range.endToken,
|
|
||||||
isFullMonth: range.isFullMonth,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
months.forEach((month) => {
|
|
||||||
pushRef(month?.associations?.planetId, month);
|
|
||||||
const events = Array.isArray(month?.events) ? month.events : [];
|
|
||||||
events.forEach((event) => {
|
|
||||||
pushRef(event?.associations?.planetId, month, {
|
|
||||||
rawDateText: event?.dateRange || event?.date || ""
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
holidays.forEach((holiday) => {
|
|
||||||
const month = monthById.get(holiday?.monthId);
|
|
||||||
if (!month) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pushRef(holiday?.associations?.planetId, month, {
|
|
||||||
rawDateText: holiday?.dateRange || holiday?.date || ""
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
map.forEach((rows, key) => {
|
|
||||||
const preciseMonthIds = new Set(
|
|
||||||
rows
|
|
||||||
.filter((entry) => !entry.isFullMonth)
|
|
||||||
.map((entry) => entry.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const filtered = rows.filter((entry) => {
|
|
||||||
if (!entry.isFullMonth) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !preciseMonthIds.has(entry.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
filtered.sort((left, right) => {
|
|
||||||
if (left.order !== right.order) {
|
|
||||||
return left.order - right.order;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startLeft = parseMonthDayToken(left.startToken);
|
|
||||||
const startRight = parseMonthDayToken(right.startToken);
|
|
||||||
const dayLeft = startLeft ? startLeft.day : 999;
|
|
||||||
const dayRight = startRight ? startRight.day : 999;
|
|
||||||
if (dayLeft !== dayRight) {
|
|
||||||
return dayLeft - dayRight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(left.label || left.name || "").localeCompare(String(right.label || right.name || ""));
|
|
||||||
});
|
|
||||||
|
|
||||||
map.set(key, filtered);
|
|
||||||
});
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCubePlacementsByPlanet(magickDataset) {
|
|
||||||
const map = new Map();
|
|
||||||
const cube = magickDataset?.grouped?.kabbalah?.cube || {};
|
|
||||||
const walls = Array.isArray(cube?.walls)
|
|
||||||
? cube.walls
|
|
||||||
: [];
|
|
||||||
const edges = Array.isArray(cube?.edges)
|
|
||||||
? cube.edges
|
|
||||||
: [];
|
|
||||||
|
|
||||||
function edgeWalls(edge) {
|
|
||||||
const explicitWalls = Array.isArray(edge?.walls)
|
|
||||||
? edge.walls.map((wallId) => String(wallId || "").trim().toLowerCase()).filter(Boolean)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (explicitWalls.length >= 2) {
|
|
||||||
return explicitWalls.slice(0, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(edge?.id || "")
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.split("-")
|
|
||||||
.map((wallId) => wallId.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
.slice(0, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
function edgeLabel(edge) {
|
|
||||||
const explicitName = String(edge?.name || "").trim();
|
|
||||||
if (explicitName) {
|
|
||||||
return explicitName;
|
|
||||||
}
|
|
||||||
return edgeWalls(edge)
|
|
||||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
||||||
.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveCubeDirectionLabel(wallId, edge) {
|
|
||||||
const normalizedWallId = String(wallId || "").trim().toLowerCase();
|
|
||||||
const edgeId = String(edge?.id || "").trim().toLowerCase();
|
|
||||||
if (!normalizedWallId || !edgeId) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const cubeUi = window.CubeSectionUi;
|
|
||||||
if (cubeUi && typeof cubeUi.getEdgeDirectionLabelForWall === "function") {
|
|
||||||
const directionLabel = String(cubeUi.getEdgeDirectionLabelForWall(normalizedWallId, edgeId) || "").trim();
|
|
||||||
if (directionLabel) {
|
|
||||||
return directionLabel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return edgeLabel(edge);
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstEdgeByWallId = new Map();
|
|
||||||
edges.forEach((edge) => {
|
|
||||||
edgeWalls(edge).forEach((wallId) => {
|
|
||||||
if (!firstEdgeByWallId.has(wallId)) {
|
|
||||||
firstEdgeByWallId.set(wallId, edge);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function pushPlacement(planetId, placement) {
|
|
||||||
if (!planetId || !placement?.wallId || !placement?.edgeId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map.has(planetId)) {
|
|
||||||
map.set(planetId, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = map.get(planetId);
|
|
||||||
const key = `${placement.wallId}:${placement.edgeId}`;
|
|
||||||
if (rows.some((row) => `${row.wallId}:${row.edgeId}` === key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rows.push(placement);
|
|
||||||
}
|
|
||||||
|
|
||||||
walls.forEach((wall) => {
|
|
||||||
const planetId = toPlanetId(wall?.associations?.planetId || wall?.planet);
|
|
||||||
if (!planetId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wallId = String(wall?.id || "").trim().toLowerCase();
|
|
||||||
const edge = firstEdgeByWallId.get(wallId) || null;
|
|
||||||
|
|
||||||
pushPlacement(planetId, {
|
|
||||||
wallId,
|
|
||||||
edgeId: String(edge?.id || "").trim().toLowerCase(),
|
|
||||||
label: `Cube: ${wall?.name || "Wall"} Wall - ${resolveCubeDirectionLabel(wallId, edge) || "Direction"}`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getElements() {
|
function getElements() {
|
||||||
return {
|
return {
|
||||||
planetCardListEl: document.getElementById("planet-card-list"),
|
planetCardListEl: document.getElementById("planet-card-list"),
|
||||||
@@ -815,8 +505,15 @@
|
|||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
state.kabbalahTargetsByPlanetId = buildKabbalahTargetsByPlanet(magickDataset);
|
state.kabbalahTargetsByPlanetId = buildKabbalahTargetsByPlanet(magickDataset);
|
||||||
state.monthRefsByPlanetId = buildMonthReferencesByPlanet(referenceData);
|
state.monthRefsByPlanetId = planetReferenceBuilders.buildMonthReferencesByPlanet({
|
||||||
state.cubePlacementsByPlanetId = buildCubePlacementsByPlanet(magickDataset);
|
referenceData,
|
||||||
|
toPlanetId,
|
||||||
|
normalizePlanetToken
|
||||||
|
});
|
||||||
|
state.cubePlacementsByPlanetId = planetReferenceBuilders.buildCubePlacementsByPlanet({
|
||||||
|
magickDataset,
|
||||||
|
toPlanetId
|
||||||
|
});
|
||||||
|
|
||||||
state.entries = baseList.map((entry) => {
|
state.entries = baseList.map((entry) => {
|
||||||
const byId = correspondences[entry.id] || null;
|
const byId = correspondences[entry.id] || null;
|
||||||
|
|||||||
613
app/ui-quiz-bank-builtins-domains.js
Normal file
613
app/ui-quiz-bank-builtins-domains.js
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
/* ui-quiz-bank-builtins-domains.js — Built-in quiz domain template generation */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function appendBuiltInQuestionBankDomains(context) {
|
||||||
|
const {
|
||||||
|
bank,
|
||||||
|
helpers,
|
||||||
|
englishLetters,
|
||||||
|
hebrewLetters,
|
||||||
|
hebrewById,
|
||||||
|
signs,
|
||||||
|
planets,
|
||||||
|
planetsById,
|
||||||
|
treePaths,
|
||||||
|
sephirotById,
|
||||||
|
flattenDecans,
|
||||||
|
cubeWalls,
|
||||||
|
cubeEdges,
|
||||||
|
cubeCenter,
|
||||||
|
playingCards,
|
||||||
|
pools
|
||||||
|
} = context || {};
|
||||||
|
|
||||||
|
const {
|
||||||
|
createQuestionTemplate,
|
||||||
|
normalizeId,
|
||||||
|
normalizeOption,
|
||||||
|
toTitleCase,
|
||||||
|
formatHebrewLetterLabel,
|
||||||
|
getPlanetLabelById,
|
||||||
|
getSephiraName,
|
||||||
|
formatPathLetter,
|
||||||
|
formatDecanLabel,
|
||||||
|
labelFromId
|
||||||
|
} = helpers || {};
|
||||||
|
|
||||||
|
if (!Array.isArray(bank) || typeof createQuestionTemplate !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
englishGematriaPool,
|
||||||
|
hebrewNumerologyPool,
|
||||||
|
hebrewNameAndCharPool,
|
||||||
|
hebrewCharPool,
|
||||||
|
planetNamePool,
|
||||||
|
planetWeekdayPool,
|
||||||
|
zodiacElementPool,
|
||||||
|
zodiacTarotPool,
|
||||||
|
pathNumberPool,
|
||||||
|
pathLetterPool,
|
||||||
|
pathTarotPool,
|
||||||
|
sephirotPlanetPool,
|
||||||
|
decanLabelPool,
|
||||||
|
decanRulerPool,
|
||||||
|
cubeLocationPool,
|
||||||
|
cubeHebrewLetterPool,
|
||||||
|
playingTarotPool
|
||||||
|
} = pools || {};
|
||||||
|
|
||||||
|
(englishLetters || []).forEach((entry) => {
|
||||||
|
if (!entry?.letter || !Number.isFinite(Number(entry?.pythagorean))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `english-gematria:${entry.letter}`,
|
||||||
|
categoryId: "english-gematria",
|
||||||
|
category: "English Gematria",
|
||||||
|
promptByDifficulty: `${entry.letter} has a simple gematria value of`,
|
||||||
|
answerByDifficulty: String(entry.pythagorean)
|
||||||
|
},
|
||||||
|
englishGematriaPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(hebrewLetters || []).forEach((entry) => {
|
||||||
|
if (!entry?.name || !entry?.char || !Number.isFinite(Number(entry?.numerology))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `hebrew-number:${entry.hebrewLetterId || entry.name}`,
|
||||||
|
categoryId: "hebrew-numerology",
|
||||||
|
category: "Hebrew Gematria",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${entry.name} (${entry.char}) has a gematria value of`,
|
||||||
|
normal: `${entry.name} (${entry.char}) has a gematria value of`,
|
||||||
|
hard: `${entry.char} has a gematria value of`
|
||||||
|
},
|
||||||
|
answerByDifficulty: String(entry.numerology)
|
||||||
|
},
|
||||||
|
hebrewNumerologyPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(englishLetters || []).forEach((entry) => {
|
||||||
|
if (!entry?.letter || !entry?.hebrewLetterId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedHebrew = hebrewById.get(normalizeId(entry.hebrewLetterId));
|
||||||
|
if (!mappedHebrew?.name || !mappedHebrew?.char) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `english-hebrew:${entry.letter}`,
|
||||||
|
categoryId: "english-hebrew-mapping",
|
||||||
|
category: "Alphabet Mapping",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${entry.letter} maps to which Hebrew letter`,
|
||||||
|
normal: `${entry.letter} maps to which Hebrew letter`,
|
||||||
|
hard: `${entry.letter} maps to which Hebrew glyph`
|
||||||
|
},
|
||||||
|
answerByDifficulty: {
|
||||||
|
easy: `${mappedHebrew.name} (${mappedHebrew.char})`,
|
||||||
|
normal: `${mappedHebrew.name} (${mappedHebrew.char})`,
|
||||||
|
hard: mappedHebrew.char
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
easy: hebrewNameAndCharPool,
|
||||||
|
normal: hebrewNameAndCharPool,
|
||||||
|
hard: hebrewCharPool
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(signs || []).forEach((entry) => {
|
||||||
|
if (!entry?.name || !entry?.rulingPlanetId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rulerName = planetsById[normalizeId(entry.rulingPlanetId)]?.name;
|
||||||
|
if (!rulerName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `zodiac-ruler:${entry.id || entry.name}`,
|
||||||
|
categoryId: "zodiac-rulers",
|
||||||
|
category: "Zodiac Rulers",
|
||||||
|
promptByDifficulty: `${entry.name} is ruled by`,
|
||||||
|
answerByDifficulty: rulerName
|
||||||
|
},
|
||||||
|
planetNamePool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(signs || []).forEach((entry) => {
|
||||||
|
if (!entry?.name || !entry?.element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `zodiac-element:${entry.id || entry.name}`,
|
||||||
|
categoryId: "zodiac-elements",
|
||||||
|
category: "Zodiac Elements",
|
||||||
|
promptByDifficulty: `${entry.name} is`,
|
||||||
|
answerByDifficulty: toTitleCase(entry.element)
|
||||||
|
},
|
||||||
|
zodiacElementPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(planets || []).forEach((entry) => {
|
||||||
|
if (!entry?.name || !entry?.weekday) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `planet-weekday:${entry.id || entry.name}`,
|
||||||
|
categoryId: "planetary-weekdays",
|
||||||
|
category: "Planetary Weekdays",
|
||||||
|
promptByDifficulty: `${entry.name} corresponds to`,
|
||||||
|
answerByDifficulty: entry.weekday
|
||||||
|
},
|
||||||
|
planetWeekdayPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(signs || []).forEach((entry) => {
|
||||||
|
if (!entry?.name || !entry?.tarot?.majorArcana) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `zodiac-tarot:${entry.id || entry.name}`,
|
||||||
|
categoryId: "zodiac-tarot",
|
||||||
|
category: "Zodiac ↔ Tarot",
|
||||||
|
promptByDifficulty: `${entry.name} corresponds to`,
|
||||||
|
answerByDifficulty: entry.tarot.majorArcana
|
||||||
|
},
|
||||||
|
zodiacTarotPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(treePaths || []).forEach((path) => {
|
||||||
|
const pathNo = Number(path?.pathNumber);
|
||||||
|
if (!Number.isFinite(pathNo)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathNumberLabel = String(Math.trunc(pathNo));
|
||||||
|
const fromNo = Number(path?.connects?.from);
|
||||||
|
const toNo = Number(path?.connects?.to);
|
||||||
|
const fromName = getSephiraName(fromNo, path?.connectIds?.from);
|
||||||
|
const toName = getSephiraName(toNo, path?.connectIds?.to);
|
||||||
|
const pathLetter = formatPathLetter(path);
|
||||||
|
const tarotCard = normalizeOption(path?.tarot?.card);
|
||||||
|
|
||||||
|
if (fromName && toName) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `kabbalah-path-between:${pathNumberLabel}`,
|
||||||
|
categoryId: "kabbalah-path-between-sephirot",
|
||||||
|
category: "Kabbalah Paths",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `Which path is between ${fromName} and ${toName}`,
|
||||||
|
normal: `What path connects ${fromName} and ${toName}`,
|
||||||
|
hard: `${fromName} ↔ ${toName} is which path`
|
||||||
|
},
|
||||||
|
answerByDifficulty: pathNumberLabel
|
||||||
|
},
|
||||||
|
pathNumberPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathLetter) {
|
||||||
|
const numberToLetterTemplate = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `kabbalah-path-letter:${pathNumberLabel}`,
|
||||||
|
categoryId: "kabbalah-path-letter",
|
||||||
|
category: "Kabbalah Paths",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `Which letter is on Path ${pathNumberLabel}`,
|
||||||
|
normal: `Path ${pathNumberLabel} carries which Hebrew letter`,
|
||||||
|
hard: `Letter on Path ${pathNumberLabel}`
|
||||||
|
},
|
||||||
|
answerByDifficulty: pathLetter
|
||||||
|
},
|
||||||
|
pathLetterPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (numberToLetterTemplate) {
|
||||||
|
bank.push(numberToLetterTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const letterToNumberTemplate = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `kabbalah-letter-path-number:${pathNumberLabel}`,
|
||||||
|
categoryId: "kabbalah-path-letter",
|
||||||
|
category: "Kabbalah Paths",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${pathLetter} belongs to which path`,
|
||||||
|
normal: `${pathLetter} corresponds to Path`,
|
||||||
|
hard: `${pathLetter} is on Path`
|
||||||
|
},
|
||||||
|
answerByDifficulty: pathNumberLabel
|
||||||
|
},
|
||||||
|
pathNumberPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (letterToNumberTemplate) {
|
||||||
|
bank.push(letterToNumberTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tarotCard) {
|
||||||
|
const pathToTarotTemplate = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `kabbalah-path-tarot:${pathNumberLabel}`,
|
||||||
|
categoryId: "kabbalah-path-tarot",
|
||||||
|
category: "Kabbalah ↔ Tarot",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `Path ${pathNumberLabel} corresponds to which Tarot trump`,
|
||||||
|
normal: `Which Tarot trump is on Path ${pathNumberLabel}`,
|
||||||
|
hard: `Tarot trump on Path ${pathNumberLabel}`
|
||||||
|
},
|
||||||
|
answerByDifficulty: tarotCard
|
||||||
|
},
|
||||||
|
pathTarotPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pathToTarotTemplate) {
|
||||||
|
bank.push(pathToTarotTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tarotToPathTemplate = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `tarot-trump-path:${pathNumberLabel}`,
|
||||||
|
categoryId: "kabbalah-path-tarot",
|
||||||
|
category: "Tarot ↔ Kabbalah",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${tarotCard} is on which path`,
|
||||||
|
normal: `Which path corresponds to ${tarotCard}`,
|
||||||
|
hard: `${tarotCard} corresponds to Path`
|
||||||
|
},
|
||||||
|
answerByDifficulty: pathNumberLabel
|
||||||
|
},
|
||||||
|
pathNumberPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tarotToPathTemplate) {
|
||||||
|
bank.push(tarotToPathTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.values(sephirotById || {}).forEach((sephira) => {
|
||||||
|
const sephiraName = String(sephira?.name?.roman || sephira?.name?.en || "").trim();
|
||||||
|
const planetLabel = getPlanetLabelById(sephira?.planetId);
|
||||||
|
if (!sephiraName || !planetLabel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `sephirot-planet:${normalizeId(sephira?.id || sephiraName)}`,
|
||||||
|
categoryId: "sephirot-planets",
|
||||||
|
category: "Sephirot ↔ Planet",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${sephiraName} corresponds to which planet`,
|
||||||
|
normal: `Planetary correspondence of ${sephiraName}`,
|
||||||
|
hard: `${sephiraName} corresponds to`
|
||||||
|
},
|
||||||
|
answerByDifficulty: planetLabel
|
||||||
|
},
|
||||||
|
sephirotPlanetPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(flattenDecans || []).forEach((decan) => {
|
||||||
|
const decanId = String(decan?.id || "").trim();
|
||||||
|
const card = normalizeOption(decan?.tarotMinorArcana);
|
||||||
|
const decanLabel = formatDecanLabel(decan);
|
||||||
|
const rulerLabel = getPlanetLabelById(decan?.rulerPlanetId);
|
||||||
|
|
||||||
|
if (!decanId || !card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decanLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `tarot-decan-sign:${decanId}`,
|
||||||
|
categoryId: "tarot-decan-sign",
|
||||||
|
category: "Tarot Decans",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${card} belongs to which decan`,
|
||||||
|
normal: `Which decan contains ${card}`,
|
||||||
|
hard: `${card} is in`
|
||||||
|
},
|
||||||
|
answerByDifficulty: decanLabel
|
||||||
|
},
|
||||||
|
decanLabelPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rulerLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `tarot-decan-ruler:${decanId}`,
|
||||||
|
categoryId: "tarot-decan-ruler",
|
||||||
|
category: "Tarot Decans",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `The decan of ${card} is ruled by`,
|
||||||
|
normal: `Who rules the decan for ${card}`,
|
||||||
|
hard: `${card} decan ruler`
|
||||||
|
},
|
||||||
|
answerByDifficulty: rulerLabel
|
||||||
|
},
|
||||||
|
decanRulerPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(cubeWalls || []).forEach((wall) => {
|
||||||
|
const wallName = String(wall?.name || labelFromId(wall?.id)).trim();
|
||||||
|
const wallLabel = wallName ? `${wallName} Wall` : "";
|
||||||
|
const tarotCard = normalizeOption(wall?.associations?.tarotCard);
|
||||||
|
const hebrew = hebrewById.get(normalizeId(wall?.hebrewLetterId));
|
||||||
|
const hebrewLabel = formatHebrewLetterLabel(hebrew, wall?.hebrewLetterId);
|
||||||
|
|
||||||
|
if (tarotCard && wallLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `tarot-cube-wall:${normalizeId(wall?.id || wallName)}`,
|
||||||
|
categoryId: "tarot-cube-location",
|
||||||
|
category: "Tarot ↔ Cube",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${tarotCard} is on which Cube wall`,
|
||||||
|
normal: `Where is ${tarotCard} on the Cube`,
|
||||||
|
hard: `${tarotCard} location on Cube`
|
||||||
|
},
|
||||||
|
answerByDifficulty: wallLabel
|
||||||
|
},
|
||||||
|
cubeLocationPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wallLabel && hebrewLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `cube-wall-letter:${normalizeId(wall?.id || wallName)}`,
|
||||||
|
categoryId: "cube-hebrew-letter",
|
||||||
|
category: "Cube ↔ Hebrew",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${wallLabel} corresponds to which Hebrew letter`,
|
||||||
|
normal: `Which Hebrew letter is on ${wallLabel}`,
|
||||||
|
hard: `${wallLabel} letter`
|
||||||
|
},
|
||||||
|
answerByDifficulty: hebrewLabel
|
||||||
|
},
|
||||||
|
cubeHebrewLetterPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(cubeEdges || []).forEach((edge) => {
|
||||||
|
const edgeName = String(edge?.name || labelFromId(edge?.id)).trim();
|
||||||
|
const edgeLabel = edgeName ? `${edgeName} Edge` : "";
|
||||||
|
const hebrew = hebrewById.get(normalizeId(edge?.hebrewLetterId));
|
||||||
|
const hebrewLabel = formatHebrewLetterLabel(hebrew, edge?.hebrewLetterId);
|
||||||
|
const tarotCard = normalizeOption(hebrew?.tarot?.card);
|
||||||
|
|
||||||
|
if (tarotCard && edgeLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `tarot-cube-edge:${normalizeId(edge?.id || edgeName)}`,
|
||||||
|
categoryId: "tarot-cube-location",
|
||||||
|
category: "Tarot ↔ Cube",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${tarotCard} is on which Cube edge`,
|
||||||
|
normal: `Where is ${tarotCard} on the Cube edges`,
|
||||||
|
hard: `${tarotCard} edge location`
|
||||||
|
},
|
||||||
|
answerByDifficulty: edgeLabel
|
||||||
|
},
|
||||||
|
cubeLocationPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edgeLabel && hebrewLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `cube-edge-letter:${normalizeId(edge?.id || edgeName)}`,
|
||||||
|
categoryId: "cube-hebrew-letter",
|
||||||
|
category: "Cube ↔ Hebrew",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${edgeLabel} corresponds to which Hebrew letter`,
|
||||||
|
normal: `Which Hebrew letter is on ${edgeLabel}`,
|
||||||
|
hard: `${edgeLabel} letter`
|
||||||
|
},
|
||||||
|
answerByDifficulty: hebrewLabel
|
||||||
|
},
|
||||||
|
cubeHebrewLetterPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cubeCenter) {
|
||||||
|
const centerTarot = normalizeOption(cubeCenter?.associations?.tarotCard || cubeCenter?.tarotCard);
|
||||||
|
const centerHebrew = hebrewById.get(normalizeId(cubeCenter?.hebrewLetterId));
|
||||||
|
const centerHebrewLabel = formatHebrewLetterLabel(centerHebrew, cubeCenter?.hebrewLetterId);
|
||||||
|
|
||||||
|
if (centerTarot) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: "tarot-cube-center",
|
||||||
|
categoryId: "tarot-cube-location",
|
||||||
|
category: "Tarot ↔ Cube",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${centerTarot} is located at which Cube position`,
|
||||||
|
normal: `Where is ${centerTarot} on the Cube`,
|
||||||
|
hard: `${centerTarot} Cube location`
|
||||||
|
},
|
||||||
|
answerByDifficulty: "Center"
|
||||||
|
},
|
||||||
|
cubeLocationPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (centerHebrewLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: "cube-center-letter",
|
||||||
|
categoryId: "cube-hebrew-letter",
|
||||||
|
category: "Cube ↔ Hebrew",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: "The Cube center corresponds to which Hebrew letter",
|
||||||
|
normal: "Which Hebrew letter is at the Cube center",
|
||||||
|
hard: "Cube center letter"
|
||||||
|
},
|
||||||
|
answerByDifficulty: centerHebrewLabel
|
||||||
|
},
|
||||||
|
cubeHebrewLetterPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(playingCards || []).forEach((entry) => {
|
||||||
|
const cardId = String(entry?.id || "").trim();
|
||||||
|
const rankLabel = normalizeOption(entry?.rankLabel || entry?.rank);
|
||||||
|
const suitLabel = normalizeOption(entry?.suitLabel || labelFromId(entry?.suit));
|
||||||
|
const tarotCard = normalizeOption(entry?.tarotCard);
|
||||||
|
|
||||||
|
if (!cardId || !rankLabel || !suitLabel || !tarotCard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `playing-card-tarot:${cardId}`,
|
||||||
|
categoryId: "playing-card-tarot",
|
||||||
|
category: "Playing Card ↔ Tarot",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${rankLabel} of ${suitLabel} maps to which Tarot card`,
|
||||||
|
normal: `${rankLabel} of ${suitLabel} corresponds to`,
|
||||||
|
hard: `${rankLabel} of ${suitLabel} maps to`
|
||||||
|
},
|
||||||
|
answerByDifficulty: tarotCard
|
||||||
|
},
|
||||||
|
playingTarotPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.QuizQuestionBankBuiltInDomains = {
|
||||||
|
appendBuiltInQuestionBankDomains
|
||||||
|
};
|
||||||
|
})();
|
||||||
358
app/ui-quiz-bank-builtins.js
Normal file
358
app/ui-quiz-bank-builtins.js
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
/* ui-quiz-bank-builtins.js — Built-in quiz template generation */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const quizQuestionBankBuiltInDomains = window.QuizQuestionBankBuiltInDomains || {};
|
||||||
|
|
||||||
|
if (typeof quizQuestionBankBuiltInDomains.appendBuiltInQuestionBankDomains !== "function") {
|
||||||
|
throw new Error("QuizQuestionBankBuiltInDomains module must load before ui-quiz-bank-builtins.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBuiltInQuestionBank(context) {
|
||||||
|
const {
|
||||||
|
referenceData,
|
||||||
|
magickDataset,
|
||||||
|
helpers
|
||||||
|
} = context || {};
|
||||||
|
|
||||||
|
const {
|
||||||
|
toTitleCase,
|
||||||
|
normalizeOption,
|
||||||
|
toUniqueOptionList,
|
||||||
|
createQuestionTemplate
|
||||||
|
} = helpers || {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof toTitleCase !== "function"
|
||||||
|
|| typeof normalizeOption !== "function"
|
||||||
|
|| typeof toUniqueOptionList !== "function"
|
||||||
|
|| typeof createQuestionTemplate !== "function"
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const grouped = magickDataset?.grouped || {};
|
||||||
|
const alphabets = grouped.alphabets || {};
|
||||||
|
const englishLetters = Array.isArray(alphabets?.english) ? alphabets.english : [];
|
||||||
|
const hebrewLetters = Array.isArray(alphabets?.hebrew) ? alphabets.hebrew : [];
|
||||||
|
const kabbalahTree = grouped?.kabbalah?.["kabbalah-tree"] || {};
|
||||||
|
const treePaths = Array.isArray(kabbalahTree?.paths) ? kabbalahTree.paths : [];
|
||||||
|
const treeSephiroth = Array.isArray(kabbalahTree?.sephiroth) ? kabbalahTree.sephiroth : [];
|
||||||
|
const sephirotById = grouped?.kabbalah?.sephirot && typeof grouped.kabbalah.sephirot === "object"
|
||||||
|
? grouped.kabbalah.sephirot
|
||||||
|
: {};
|
||||||
|
const cube = grouped?.kabbalah?.cube && typeof grouped.kabbalah.cube === "object"
|
||||||
|
? grouped.kabbalah.cube
|
||||||
|
: {};
|
||||||
|
const cubeWalls = Array.isArray(cube?.walls) ? cube.walls : [];
|
||||||
|
const cubeEdges = Array.isArray(cube?.edges) ? cube.edges : [];
|
||||||
|
const cubeCenter = cube?.center && typeof cube.center === "object" ? cube.center : null;
|
||||||
|
const playingCardsData = grouped?.["playing-cards-52"];
|
||||||
|
const playingCards = Array.isArray(playingCardsData)
|
||||||
|
? playingCardsData
|
||||||
|
: (Array.isArray(playingCardsData?.entries) ? playingCardsData.entries : []);
|
||||||
|
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
||||||
|
const planetsById = referenceData?.planets && typeof referenceData.planets === "object"
|
||||||
|
? referenceData.planets
|
||||||
|
: {};
|
||||||
|
const planets = Object.values(planetsById);
|
||||||
|
const decansBySign = referenceData?.decansBySign && typeof referenceData.decansBySign === "object"
|
||||||
|
? referenceData.decansBySign
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const normalizeId = (value) => String(value || "").trim().toLowerCase();
|
||||||
|
|
||||||
|
const toRomanNumeral = (value) => {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||||
|
return String(value || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const intValue = Math.trunc(numeric);
|
||||||
|
const lookup = [
|
||||||
|
[1000, "M"],
|
||||||
|
[900, "CM"],
|
||||||
|
[500, "D"],
|
||||||
|
[400, "CD"],
|
||||||
|
[100, "C"],
|
||||||
|
[90, "XC"],
|
||||||
|
[50, "L"],
|
||||||
|
[40, "XL"],
|
||||||
|
[10, "X"],
|
||||||
|
[9, "IX"],
|
||||||
|
[5, "V"],
|
||||||
|
[4, "IV"],
|
||||||
|
[1, "I"]
|
||||||
|
];
|
||||||
|
|
||||||
|
let current = intValue;
|
||||||
|
let result = "";
|
||||||
|
lookup.forEach(([size, symbol]) => {
|
||||||
|
while (current >= size) {
|
||||||
|
result += symbol;
|
||||||
|
current -= size;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result || String(intValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelFromId = (value) => {
|
||||||
|
const id = String(value || "").trim();
|
||||||
|
if (!id) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
.replace(/[_-]+/g, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim()
|
||||||
|
.split(" ")
|
||||||
|
.map((part) => part ? part.charAt(0).toUpperCase() + part.slice(1) : "")
|
||||||
|
.join(" ");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlanetLabelById = (planetId) => {
|
||||||
|
const normalized = normalizeId(planetId);
|
||||||
|
if (!normalized) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const directPlanet = planetsById[normalized];
|
||||||
|
if (directPlanet?.name) {
|
||||||
|
return directPlanet.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === "primum-mobile") {
|
||||||
|
return "Primum Mobile";
|
||||||
|
}
|
||||||
|
if (normalized === "olam-yesodot") {
|
||||||
|
return "Earth / Elements";
|
||||||
|
}
|
||||||
|
|
||||||
|
return labelFromId(normalized);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hebrewById = new Map(
|
||||||
|
hebrewLetters
|
||||||
|
.filter((entry) => entry?.hebrewLetterId)
|
||||||
|
.map((entry) => [normalizeId(entry.hebrewLetterId), entry])
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatHebrewLetterLabel = (entry, fallbackId = "") => {
|
||||||
|
if (entry?.name && entry?.char) {
|
||||||
|
return `${entry.name} (${entry.char})`;
|
||||||
|
}
|
||||||
|
if (entry?.name) {
|
||||||
|
return entry.name;
|
||||||
|
}
|
||||||
|
if (entry?.char) {
|
||||||
|
return entry.char;
|
||||||
|
}
|
||||||
|
return labelFromId(fallbackId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sephiraNameByNumber = new Map(
|
||||||
|
treeSephiroth
|
||||||
|
.filter((entry) => Number.isFinite(Number(entry?.number)) && entry?.name)
|
||||||
|
.map((entry) => [Math.trunc(Number(entry.number)), String(entry.name)])
|
||||||
|
);
|
||||||
|
|
||||||
|
const sephiraNameById = new Map(
|
||||||
|
treeSephiroth
|
||||||
|
.filter((entry) => entry?.sephiraId && entry?.name)
|
||||||
|
.map((entry) => [normalizeId(entry.sephiraId), String(entry.name)])
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSephiraName = (numberValue, idValue) => {
|
||||||
|
const numberKey = Number(numberValue);
|
||||||
|
if (Number.isFinite(numberKey)) {
|
||||||
|
const byNumber = sephiraNameByNumber.get(Math.trunc(numberKey));
|
||||||
|
if (byNumber) {
|
||||||
|
return byNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const byId = sephiraNameById.get(normalizeId(idValue));
|
||||||
|
if (byId) {
|
||||||
|
return byId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isFinite(numberKey)) {
|
||||||
|
return `Sephira ${Math.trunc(numberKey)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return labelFromId(idValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPathLetter = (path) => {
|
||||||
|
const transliteration = String(path?.hebrewLetter?.transliteration || "").trim();
|
||||||
|
const glyph = String(path?.hebrewLetter?.char || "").trim();
|
||||||
|
|
||||||
|
if (transliteration && glyph) {
|
||||||
|
return `${transliteration} (${glyph})`;
|
||||||
|
}
|
||||||
|
if (transliteration) {
|
||||||
|
return transliteration;
|
||||||
|
}
|
||||||
|
if (glyph) {
|
||||||
|
return glyph;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenDecans = Object.values(decansBySign)
|
||||||
|
.flatMap((entries) => (Array.isArray(entries) ? entries : []));
|
||||||
|
|
||||||
|
const signNameById = new Map(
|
||||||
|
signs
|
||||||
|
.filter((entry) => entry?.id && entry?.name)
|
||||||
|
.map((entry) => [normalizeId(entry.id), String(entry.name)])
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatDecanLabel = (decan) => {
|
||||||
|
const signName = signNameById.get(normalizeId(decan?.signId)) || labelFromId(decan?.signId);
|
||||||
|
const index = Number(decan?.index);
|
||||||
|
if (!signName || !Number.isFinite(index)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `${signName} Decan ${toRomanNumeral(index)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bank = [];
|
||||||
|
|
||||||
|
const englishGematriaPool = englishLetters
|
||||||
|
.map((item) => (Number.isFinite(Number(item?.pythagorean)) ? String(item.pythagorean) : ""))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const hebrewNumerologyPool = hebrewLetters
|
||||||
|
.map((item) => (Number.isFinite(Number(item?.numerology)) ? String(item.numerology) : ""))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const hebrewNameAndCharPool = hebrewLetters
|
||||||
|
.filter((item) => item?.name && item?.char)
|
||||||
|
.map((item) => `${item.name} (${item.char})`);
|
||||||
|
|
||||||
|
const hebrewCharPool = hebrewLetters
|
||||||
|
.map((item) => item?.char)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const planetNamePool = planets
|
||||||
|
.map((planet) => planet?.name)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const planetWeekdayPool = planets
|
||||||
|
.map((planet) => planet?.weekday)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const zodiacElementPool = signs
|
||||||
|
.map((sign) => toTitleCase(sign?.element))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const zodiacTarotPool = signs
|
||||||
|
.map((sign) => sign?.tarot?.majorArcana)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const pathNumberPool = toUniqueOptionList(
|
||||||
|
treePaths
|
||||||
|
.map((path) => {
|
||||||
|
const pathNo = Number(path?.pathNumber);
|
||||||
|
return Number.isFinite(pathNo) ? String(Math.trunc(pathNo)) : "";
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const pathLetterPool = toUniqueOptionList(treePaths.map((path) => formatPathLetter(path)));
|
||||||
|
const pathTarotPool = toUniqueOptionList(treePaths.map((path) => normalizeOption(path?.tarot?.card)));
|
||||||
|
const sephirotPlanetPool = toUniqueOptionList(
|
||||||
|
Object.values(sephirotById).map((entry) => getPlanetLabelById(entry?.planetId))
|
||||||
|
);
|
||||||
|
|
||||||
|
const decanLabelPool = toUniqueOptionList(flattenDecans.map((decan) => formatDecanLabel(decan)));
|
||||||
|
const decanRulerPool = toUniqueOptionList(
|
||||||
|
flattenDecans.map((decan) => getPlanetLabelById(decan?.rulerPlanetId))
|
||||||
|
);
|
||||||
|
|
||||||
|
const cubeWallLabelPool = toUniqueOptionList(
|
||||||
|
cubeWalls.map((wall) => `${String(wall?.name || labelFromId(wall?.id)).trim()} Wall`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const cubeEdgeLabelPool = toUniqueOptionList(
|
||||||
|
cubeEdges.map((edge) => `${String(edge?.name || labelFromId(edge?.id)).trim()} Edge`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const cubeLocationPool = toUniqueOptionList([
|
||||||
|
...cubeWallLabelPool,
|
||||||
|
...cubeEdgeLabelPool,
|
||||||
|
"Center"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const cubeHebrewLetterPool = toUniqueOptionList([
|
||||||
|
...cubeWalls.map((wall) => {
|
||||||
|
const hebrew = hebrewById.get(normalizeId(wall?.hebrewLetterId));
|
||||||
|
return formatHebrewLetterLabel(hebrew, wall?.hebrewLetterId);
|
||||||
|
}),
|
||||||
|
...cubeEdges.map((edge) => {
|
||||||
|
const hebrew = hebrewById.get(normalizeId(edge?.hebrewLetterId));
|
||||||
|
return formatHebrewLetterLabel(hebrew, edge?.hebrewLetterId);
|
||||||
|
}),
|
||||||
|
formatHebrewLetterLabel(hebrewById.get(normalizeId(cubeCenter?.hebrewLetterId)), cubeCenter?.hebrewLetterId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const playingTarotPool = toUniqueOptionList(
|
||||||
|
playingCards.map((entry) => normalizeOption(entry?.tarotCard))
|
||||||
|
);
|
||||||
|
|
||||||
|
quizQuestionBankBuiltInDomains.appendBuiltInQuestionBankDomains({
|
||||||
|
bank,
|
||||||
|
englishLetters,
|
||||||
|
hebrewLetters,
|
||||||
|
hebrewById,
|
||||||
|
signs,
|
||||||
|
planets,
|
||||||
|
planetsById,
|
||||||
|
treePaths,
|
||||||
|
sephirotById,
|
||||||
|
flattenDecans,
|
||||||
|
cubeWalls,
|
||||||
|
cubeEdges,
|
||||||
|
cubeCenter,
|
||||||
|
playingCards,
|
||||||
|
pools: {
|
||||||
|
englishGematriaPool,
|
||||||
|
hebrewNumerologyPool,
|
||||||
|
hebrewNameAndCharPool,
|
||||||
|
hebrewCharPool,
|
||||||
|
planetNamePool,
|
||||||
|
planetWeekdayPool,
|
||||||
|
zodiacElementPool,
|
||||||
|
zodiacTarotPool,
|
||||||
|
pathNumberPool,
|
||||||
|
pathLetterPool,
|
||||||
|
pathTarotPool,
|
||||||
|
sephirotPlanetPool,
|
||||||
|
decanLabelPool,
|
||||||
|
decanRulerPool,
|
||||||
|
cubeLocationPool,
|
||||||
|
cubeHebrewLetterPool,
|
||||||
|
playingTarotPool
|
||||||
|
},
|
||||||
|
helpers: {
|
||||||
|
createQuestionTemplate,
|
||||||
|
normalizeId,
|
||||||
|
normalizeOption,
|
||||||
|
toTitleCase,
|
||||||
|
formatHebrewLetterLabel,
|
||||||
|
getPlanetLabelById,
|
||||||
|
getSephiraName,
|
||||||
|
formatPathLetter,
|
||||||
|
formatDecanLabel,
|
||||||
|
labelFromId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return bank;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.QuizQuestionBankBuiltins = {
|
||||||
|
buildBuiltInQuestionBank
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -2,6 +2,12 @@
|
|||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const quizQuestionBankBuiltins = window.QuizQuestionBankBuiltins || {};
|
||||||
|
|
||||||
|
if (typeof quizQuestionBankBuiltins.buildBuiltInQuestionBank !== "function") {
|
||||||
|
throw new Error("QuizQuestionBankBuiltins module must load before ui-quiz-bank.js");
|
||||||
|
}
|
||||||
|
|
||||||
function toTitleCase(value) {
|
function toTitleCase(value) {
|
||||||
const text = String(value || "").trim().toLowerCase();
|
const text = String(value || "").trim().toLowerCase();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -104,817 +110,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildQuestionBank(referenceData, magickDataset, dynamicCategoryRegistry) {
|
function buildQuestionBank(referenceData, magickDataset, dynamicCategoryRegistry) {
|
||||||
const grouped = magickDataset?.grouped || {};
|
const bank = quizQuestionBankBuiltins.buildBuiltInQuestionBank({
|
||||||
const alphabets = grouped.alphabets || {};
|
referenceData,
|
||||||
const englishLetters = Array.isArray(alphabets?.english) ? alphabets.english : [];
|
magickDataset,
|
||||||
const hebrewLetters = Array.isArray(alphabets?.hebrew) ? alphabets.hebrew : [];
|
helpers: {
|
||||||
const kabbalahTree = grouped?.kabbalah?.["kabbalah-tree"] || {};
|
toTitleCase,
|
||||||
const treePaths = Array.isArray(kabbalahTree?.paths) ? kabbalahTree.paths : [];
|
normalizeOption,
|
||||||
const treeSephiroth = Array.isArray(kabbalahTree?.sephiroth) ? kabbalahTree.sephiroth : [];
|
toUniqueOptionList,
|
||||||
const sephirotById = grouped?.kabbalah?.sephirot && typeof grouped.kabbalah.sephirot === "object"
|
createQuestionTemplate
|
||||||
? grouped.kabbalah.sephirot
|
|
||||||
: {};
|
|
||||||
const cube = grouped?.kabbalah?.cube && typeof grouped.kabbalah.cube === "object"
|
|
||||||
? grouped.kabbalah.cube
|
|
||||||
: {};
|
|
||||||
const cubeWalls = Array.isArray(cube?.walls) ? cube.walls : [];
|
|
||||||
const cubeEdges = Array.isArray(cube?.edges) ? cube.edges : [];
|
|
||||||
const cubeCenter = cube?.center && typeof cube.center === "object" ? cube.center : null;
|
|
||||||
const playingCardsData = grouped?.["playing-cards-52"];
|
|
||||||
const playingCards = Array.isArray(playingCardsData)
|
|
||||||
? playingCardsData
|
|
||||||
: (Array.isArray(playingCardsData?.entries) ? playingCardsData.entries : []);
|
|
||||||
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
|
||||||
const planetsById = referenceData?.planets && typeof referenceData.planets === "object"
|
|
||||||
? referenceData.planets
|
|
||||||
: {};
|
|
||||||
const planets = Object.values(planetsById);
|
|
||||||
const decansBySign = referenceData?.decansBySign && typeof referenceData.decansBySign === "object"
|
|
||||||
? referenceData.decansBySign
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const normalizeId = (value) => String(value || "").trim().toLowerCase();
|
|
||||||
|
|
||||||
const toRomanNumeral = (value) => {
|
|
||||||
const numeric = Number(value);
|
|
||||||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
||||||
return String(value || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
const intValue = Math.trunc(numeric);
|
|
||||||
const lookup = [
|
|
||||||
[1000, "M"],
|
|
||||||
[900, "CM"],
|
|
||||||
[500, "D"],
|
|
||||||
[400, "CD"],
|
|
||||||
[100, "C"],
|
|
||||||
[90, "XC"],
|
|
||||||
[50, "L"],
|
|
||||||
[40, "XL"],
|
|
||||||
[10, "X"],
|
|
||||||
[9, "IX"],
|
|
||||||
[5, "V"],
|
|
||||||
[4, "IV"],
|
|
||||||
[1, "I"]
|
|
||||||
];
|
|
||||||
|
|
||||||
let current = intValue;
|
|
||||||
let result = "";
|
|
||||||
lookup.forEach(([size, symbol]) => {
|
|
||||||
while (current >= size) {
|
|
||||||
result += symbol;
|
|
||||||
current -= size;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result || String(intValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const labelFromId = (value) => {
|
|
||||||
const id = String(value || "").trim();
|
|
||||||
if (!id) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
.replace(/[_-]+/g, " ")
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim()
|
|
||||||
.split(" ")
|
|
||||||
.map((part) => part ? part.charAt(0).toUpperCase() + part.slice(1) : "")
|
|
||||||
.join(" ");
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlanetLabelById = (planetId) => {
|
|
||||||
const normalized = normalizeId(planetId);
|
|
||||||
if (!normalized) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const directPlanet = planetsById[normalized];
|
|
||||||
if (directPlanet?.name) {
|
|
||||||
return directPlanet.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalized === "primum-mobile") {
|
|
||||||
return "Primum Mobile";
|
|
||||||
}
|
|
||||||
if (normalized === "olam-yesodot") {
|
|
||||||
return "Earth / Elements";
|
|
||||||
}
|
|
||||||
|
|
||||||
return labelFromId(normalized);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hebrewById = new Map(
|
|
||||||
hebrewLetters
|
|
||||||
.filter((entry) => entry?.hebrewLetterId)
|
|
||||||
.map((entry) => [normalizeId(entry.hebrewLetterId), entry])
|
|
||||||
);
|
|
||||||
|
|
||||||
const formatHebrewLetterLabel = (entry, fallbackId = "") => {
|
|
||||||
if (entry?.name && entry?.char) {
|
|
||||||
return `${entry.name} (${entry.char})`;
|
|
||||||
}
|
|
||||||
if (entry?.name) {
|
|
||||||
return entry.name;
|
|
||||||
}
|
|
||||||
if (entry?.char) {
|
|
||||||
return entry.char;
|
|
||||||
}
|
|
||||||
return labelFromId(fallbackId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sephiraNameByNumber = new Map(
|
|
||||||
treeSephiroth
|
|
||||||
.filter((entry) => Number.isFinite(Number(entry?.number)) && entry?.name)
|
|
||||||
.map((entry) => [Math.trunc(Number(entry.number)), String(entry.name)])
|
|
||||||
);
|
|
||||||
|
|
||||||
const sephiraNameById = new Map(
|
|
||||||
treeSephiroth
|
|
||||||
.filter((entry) => entry?.sephiraId && entry?.name)
|
|
||||||
.map((entry) => [normalizeId(entry.sephiraId), String(entry.name)])
|
|
||||||
);
|
|
||||||
|
|
||||||
const getSephiraName = (numberValue, idValue) => {
|
|
||||||
const numberKey = Number(numberValue);
|
|
||||||
if (Number.isFinite(numberKey)) {
|
|
||||||
const byNumber = sephiraNameByNumber.get(Math.trunc(numberKey));
|
|
||||||
if (byNumber) {
|
|
||||||
return byNumber;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const byId = sephiraNameById.get(normalizeId(idValue));
|
|
||||||
if (byId) {
|
|
||||||
return byId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Number.isFinite(numberKey)) {
|
|
||||||
return `Sephira ${Math.trunc(numberKey)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return labelFromId(idValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatPathLetter = (path) => {
|
|
||||||
const transliteration = String(path?.hebrewLetter?.transliteration || "").trim();
|
|
||||||
const glyph = String(path?.hebrewLetter?.char || "").trim();
|
|
||||||
|
|
||||||
if (transliteration && glyph) {
|
|
||||||
return `${transliteration} (${glyph})`;
|
|
||||||
}
|
|
||||||
if (transliteration) {
|
|
||||||
return transliteration;
|
|
||||||
}
|
|
||||||
if (glyph) {
|
|
||||||
return glyph;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const flattenDecans = Object.values(decansBySign)
|
|
||||||
.flatMap((entries) => (Array.isArray(entries) ? entries : []));
|
|
||||||
|
|
||||||
const signNameById = new Map(
|
|
||||||
signs
|
|
||||||
.filter((entry) => entry?.id && entry?.name)
|
|
||||||
.map((entry) => [normalizeId(entry.id), String(entry.name)])
|
|
||||||
);
|
|
||||||
|
|
||||||
const formatDecanLabel = (decan) => {
|
|
||||||
const signName = signNameById.get(normalizeId(decan?.signId)) || labelFromId(decan?.signId);
|
|
||||||
const index = Number(decan?.index);
|
|
||||||
if (!signName || !Number.isFinite(index)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return `${signName} Decan ${toRomanNumeral(index)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const bank = [];
|
|
||||||
|
|
||||||
const englishGematriaPool = englishLetters
|
|
||||||
.map((item) => (Number.isFinite(Number(item?.pythagorean)) ? String(item.pythagorean) : ""))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const hebrewNumerologyPool = hebrewLetters
|
|
||||||
.map((item) => (Number.isFinite(Number(item?.numerology)) ? String(item.numerology) : ""))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const hebrewNameAndCharPool = hebrewLetters
|
|
||||||
.filter((item) => item?.name && item?.char)
|
|
||||||
.map((item) => `${item.name} (${item.char})`);
|
|
||||||
|
|
||||||
const hebrewCharPool = hebrewLetters
|
|
||||||
.map((item) => item?.char)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const planetNamePool = planets
|
|
||||||
.map((planet) => planet?.name)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const planetWeekdayPool = planets
|
|
||||||
.map((planet) => planet?.weekday)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const zodiacElementPool = signs
|
|
||||||
.map((sign) => toTitleCase(sign?.element))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const zodiacTarotPool = signs
|
|
||||||
.map((sign) => sign?.tarot?.majorArcana)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const pathNumberPool = toUniqueOptionList(
|
|
||||||
treePaths
|
|
||||||
.map((path) => {
|
|
||||||
const pathNo = Number(path?.pathNumber);
|
|
||||||
return Number.isFinite(pathNo) ? String(Math.trunc(pathNo)) : "";
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const pathLetterPool = toUniqueOptionList(treePaths.map((path) => formatPathLetter(path)));
|
|
||||||
const pathTarotPool = toUniqueOptionList(treePaths.map((path) => normalizeOption(path?.tarot?.card)));
|
|
||||||
|
|
||||||
const decanLabelPool = toUniqueOptionList(flattenDecans.map((decan) => formatDecanLabel(decan)));
|
|
||||||
const decanRulerPool = toUniqueOptionList(
|
|
||||||
flattenDecans.map((decan) => getPlanetLabelById(decan?.rulerPlanetId))
|
|
||||||
);
|
|
||||||
|
|
||||||
const cubeWallLabelPool = toUniqueOptionList(
|
|
||||||
cubeWalls.map((wall) => `${String(wall?.name || labelFromId(wall?.id)).trim()} Wall`)
|
|
||||||
);
|
|
||||||
|
|
||||||
const cubeEdgeLabelPool = toUniqueOptionList(
|
|
||||||
cubeEdges.map((edge) => `${String(edge?.name || labelFromId(edge?.id)).trim()} Edge`)
|
|
||||||
);
|
|
||||||
|
|
||||||
const cubeLocationPool = toUniqueOptionList([
|
|
||||||
...cubeWallLabelPool,
|
|
||||||
...cubeEdgeLabelPool,
|
|
||||||
"Center"
|
|
||||||
]);
|
|
||||||
|
|
||||||
const cubeHebrewLetterPool = toUniqueOptionList([
|
|
||||||
...cubeWalls.map((wall) => {
|
|
||||||
const hebrew = hebrewById.get(normalizeId(wall?.hebrewLetterId));
|
|
||||||
return formatHebrewLetterLabel(hebrew, wall?.hebrewLetterId);
|
|
||||||
}),
|
|
||||||
...cubeEdges.map((edge) => {
|
|
||||||
const hebrew = hebrewById.get(normalizeId(edge?.hebrewLetterId));
|
|
||||||
return formatHebrewLetterLabel(hebrew, edge?.hebrewLetterId);
|
|
||||||
}),
|
|
||||||
formatHebrewLetterLabel(hebrewById.get(normalizeId(cubeCenter?.hebrewLetterId)), cubeCenter?.hebrewLetterId)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const playingTarotPool = toUniqueOptionList(
|
|
||||||
playingCards.map((entry) => normalizeOption(entry?.tarotCard))
|
|
||||||
);
|
|
||||||
|
|
||||||
englishLetters.forEach((entry) => {
|
|
||||||
if (!entry?.letter || !Number.isFinite(Number(entry?.pythagorean))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `english-gematria:${entry.letter}`,
|
|
||||||
categoryId: "english-gematria",
|
|
||||||
category: "English Gematria",
|
|
||||||
promptByDifficulty: `${entry.letter} has a simple gematria value of`,
|
|
||||||
answerByDifficulty: String(entry.pythagorean)
|
|
||||||
},
|
|
||||||
englishGematriaPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
hebrewLetters.forEach((entry) => {
|
|
||||||
if (!entry?.name || !entry?.char || !Number.isFinite(Number(entry?.numerology))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `hebrew-number:${entry.hebrewLetterId || entry.name}`,
|
|
||||||
categoryId: "hebrew-numerology",
|
|
||||||
category: "Hebrew Gematria",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${entry.name} (${entry.char}) has a gematria value of`,
|
|
||||||
normal: `${entry.name} (${entry.char}) has a gematria value of`,
|
|
||||||
hard: `${entry.char} has a gematria value of`
|
|
||||||
},
|
|
||||||
answerByDifficulty: String(entry.numerology)
|
|
||||||
},
|
|
||||||
hebrewNumerologyPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
englishLetters.forEach((entry) => {
|
|
||||||
if (!entry?.letter || !entry?.hebrewLetterId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mappedHebrew = hebrewById.get(normalizeId(entry.hebrewLetterId));
|
|
||||||
if (!mappedHebrew?.name || !mappedHebrew?.char) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `english-hebrew:${entry.letter}`,
|
|
||||||
categoryId: "english-hebrew-mapping",
|
|
||||||
category: "Alphabet Mapping",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${entry.letter} maps to which Hebrew letter`,
|
|
||||||
normal: `${entry.letter} maps to which Hebrew letter`,
|
|
||||||
hard: `${entry.letter} maps to which Hebrew glyph`
|
|
||||||
},
|
|
||||||
answerByDifficulty: {
|
|
||||||
easy: `${mappedHebrew.name} (${mappedHebrew.char})`,
|
|
||||||
normal: `${mappedHebrew.name} (${mappedHebrew.char})`,
|
|
||||||
hard: mappedHebrew.char
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
easy: hebrewNameAndCharPool,
|
|
||||||
normal: hebrewNameAndCharPool,
|
|
||||||
hard: hebrewCharPool
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
signs.forEach((entry) => {
|
|
||||||
if (!entry?.name || !entry?.rulingPlanetId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rulerName = planetsById[normalizeId(entry.rulingPlanetId)]?.name;
|
|
||||||
if (!rulerName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `zodiac-ruler:${entry.id || entry.name}`,
|
|
||||||
categoryId: "zodiac-rulers",
|
|
||||||
category: "Zodiac Rulers",
|
|
||||||
promptByDifficulty: `${entry.name} is ruled by`,
|
|
||||||
answerByDifficulty: rulerName
|
|
||||||
},
|
|
||||||
planetNamePool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
signs.forEach((entry) => {
|
|
||||||
if (!entry?.name || !entry?.element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `zodiac-element:${entry.id || entry.name}`,
|
|
||||||
categoryId: "zodiac-elements",
|
|
||||||
category: "Zodiac Elements",
|
|
||||||
promptByDifficulty: `${entry.name} is`,
|
|
||||||
answerByDifficulty: toTitleCase(entry.element)
|
|
||||||
},
|
|
||||||
zodiacElementPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
planets.forEach((entry) => {
|
|
||||||
if (!entry?.name || !entry?.weekday) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `planet-weekday:${entry.id || entry.name}`,
|
|
||||||
categoryId: "planetary-weekdays",
|
|
||||||
category: "Planetary Weekdays",
|
|
||||||
promptByDifficulty: `${entry.name} corresponds to`,
|
|
||||||
answerByDifficulty: entry.weekday
|
|
||||||
},
|
|
||||||
planetWeekdayPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
signs.forEach((entry) => {
|
|
||||||
if (!entry?.name || !entry?.tarot?.majorArcana) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `zodiac-tarot:${entry.id || entry.name}`,
|
|
||||||
categoryId: "zodiac-tarot",
|
|
||||||
category: "Zodiac ↔ Tarot",
|
|
||||||
promptByDifficulty: `${entry.name} corresponds to`,
|
|
||||||
answerByDifficulty: entry.tarot.majorArcana
|
|
||||||
},
|
|
||||||
zodiacTarotPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
treePaths.forEach((path) => {
|
|
||||||
const pathNo = Number(path?.pathNumber);
|
|
||||||
if (!Number.isFinite(pathNo)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathNumberLabel = String(Math.trunc(pathNo));
|
|
||||||
const fromNo = Number(path?.connects?.from);
|
|
||||||
const toNo = Number(path?.connects?.to);
|
|
||||||
const fromName = getSephiraName(fromNo, path?.connectIds?.from);
|
|
||||||
const toName = getSephiraName(toNo, path?.connectIds?.to);
|
|
||||||
const pathLetter = formatPathLetter(path);
|
|
||||||
const tarotCard = normalizeOption(path?.tarot?.card);
|
|
||||||
|
|
||||||
if (fromName && toName) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `kabbalah-path-between:${pathNumberLabel}`,
|
|
||||||
categoryId: "kabbalah-path-between-sephirot",
|
|
||||||
category: "Kabbalah Paths",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `Which path is between ${fromName} and ${toName}`,
|
|
||||||
normal: `What path connects ${fromName} and ${toName}`,
|
|
||||||
hard: `${fromName} ↔ ${toName} is which path`
|
|
||||||
},
|
|
||||||
answerByDifficulty: pathNumberLabel
|
|
||||||
},
|
|
||||||
pathNumberPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathLetter) {
|
|
||||||
const numberToLetterTemplate = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `kabbalah-path-letter:${pathNumberLabel}`,
|
|
||||||
categoryId: "kabbalah-path-letter",
|
|
||||||
category: "Kabbalah Paths",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `Which letter is on Path ${pathNumberLabel}`,
|
|
||||||
normal: `Path ${pathNumberLabel} carries which Hebrew letter`,
|
|
||||||
hard: `Letter on Path ${pathNumberLabel}`
|
|
||||||
},
|
|
||||||
answerByDifficulty: pathLetter
|
|
||||||
},
|
|
||||||
pathLetterPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (numberToLetterTemplate) {
|
|
||||||
bank.push(numberToLetterTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
const letterToNumberTemplate = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `kabbalah-letter-path-number:${pathNumberLabel}`,
|
|
||||||
categoryId: "kabbalah-path-letter",
|
|
||||||
category: "Kabbalah Paths",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${pathLetter} belongs to which path`,
|
|
||||||
normal: `${pathLetter} corresponds to Path`,
|
|
||||||
hard: `${pathLetter} is on Path`
|
|
||||||
},
|
|
||||||
answerByDifficulty: pathNumberLabel
|
|
||||||
},
|
|
||||||
pathNumberPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (letterToNumberTemplate) {
|
|
||||||
bank.push(letterToNumberTemplate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tarotCard) {
|
|
||||||
const pathToTarotTemplate = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `kabbalah-path-tarot:${pathNumberLabel}`,
|
|
||||||
categoryId: "kabbalah-path-tarot",
|
|
||||||
category: "Kabbalah ↔ Tarot",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `Path ${pathNumberLabel} corresponds to which Tarot trump`,
|
|
||||||
normal: `Which Tarot trump is on Path ${pathNumberLabel}`,
|
|
||||||
hard: `Tarot trump on Path ${pathNumberLabel}`
|
|
||||||
},
|
|
||||||
answerByDifficulty: tarotCard
|
|
||||||
},
|
|
||||||
pathTarotPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pathToTarotTemplate) {
|
|
||||||
bank.push(pathToTarotTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tarotToPathTemplate = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `tarot-trump-path:${pathNumberLabel}`,
|
|
||||||
categoryId: "kabbalah-path-tarot",
|
|
||||||
category: "Tarot ↔ Kabbalah",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${tarotCard} is on which path`,
|
|
||||||
normal: `Which path corresponds to ${tarotCard}`,
|
|
||||||
hard: `${tarotCard} corresponds to Path`
|
|
||||||
},
|
|
||||||
answerByDifficulty: pathNumberLabel
|
|
||||||
},
|
|
||||||
pathNumberPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tarotToPathTemplate) {
|
|
||||||
bank.push(tarotToPathTemplate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.values(sephirotById).forEach((sephira) => {
|
|
||||||
const sephiraName = String(sephira?.name?.roman || sephira?.name?.en || "").trim();
|
|
||||||
const planetLabel = getPlanetLabelById(sephira?.planetId);
|
|
||||||
if (!sephiraName || !planetLabel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `sephirot-planet:${normalizeId(sephira?.id || sephiraName)}`,
|
|
||||||
categoryId: "sephirot-planets",
|
|
||||||
category: "Sephirot ↔ Planet",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${sephiraName} corresponds to which planet`,
|
|
||||||
normal: `Planetary correspondence of ${sephiraName}`,
|
|
||||||
hard: `${sephiraName} corresponds to`
|
|
||||||
},
|
|
||||||
answerByDifficulty: planetLabel
|
|
||||||
},
|
|
||||||
toUniqueOptionList(Object.values(sephirotById).map((entry) => getPlanetLabelById(entry?.planetId)))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
flattenDecans.forEach((decan) => {
|
|
||||||
const decanId = String(decan?.id || "").trim();
|
|
||||||
const card = normalizeOption(decan?.tarotMinorArcana);
|
|
||||||
const decanLabel = formatDecanLabel(decan);
|
|
||||||
const rulerLabel = getPlanetLabelById(decan?.rulerPlanetId);
|
|
||||||
|
|
||||||
if (!decanId || !card) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decanLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `tarot-decan-sign:${decanId}`,
|
|
||||||
categoryId: "tarot-decan-sign",
|
|
||||||
category: "Tarot Decans",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${card} belongs to which decan`,
|
|
||||||
normal: `Which decan contains ${card}`,
|
|
||||||
hard: `${card} is in`
|
|
||||||
},
|
|
||||||
answerByDifficulty: decanLabel
|
|
||||||
},
|
|
||||||
decanLabelPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rulerLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `tarot-decan-ruler:${decanId}`,
|
|
||||||
categoryId: "tarot-decan-ruler",
|
|
||||||
category: "Tarot Decans",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `The decan of ${card} is ruled by`,
|
|
||||||
normal: `Who rules the decan for ${card}`,
|
|
||||||
hard: `${card} decan ruler`
|
|
||||||
},
|
|
||||||
answerByDifficulty: rulerLabel
|
|
||||||
},
|
|
||||||
decanRulerPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cubeWalls.forEach((wall) => {
|
|
||||||
const wallName = String(wall?.name || labelFromId(wall?.id)).trim();
|
|
||||||
const wallLabel = wallName ? `${wallName} Wall` : "";
|
|
||||||
const tarotCard = normalizeOption(wall?.associations?.tarotCard);
|
|
||||||
const hebrew = hebrewById.get(normalizeId(wall?.hebrewLetterId));
|
|
||||||
const hebrewLabel = formatHebrewLetterLabel(hebrew, wall?.hebrewLetterId);
|
|
||||||
|
|
||||||
if (tarotCard && wallLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `tarot-cube-wall:${normalizeId(wall?.id || wallName)}`,
|
|
||||||
categoryId: "tarot-cube-location",
|
|
||||||
category: "Tarot ↔ Cube",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${tarotCard} is on which Cube wall`,
|
|
||||||
normal: `Where is ${tarotCard} on the Cube`,
|
|
||||||
hard: `${tarotCard} location on Cube`
|
|
||||||
},
|
|
||||||
answerByDifficulty: wallLabel
|
|
||||||
},
|
|
||||||
cubeLocationPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wallLabel && hebrewLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `cube-wall-letter:${normalizeId(wall?.id || wallName)}`,
|
|
||||||
categoryId: "cube-hebrew-letter",
|
|
||||||
category: "Cube ↔ Hebrew",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${wallLabel} corresponds to which Hebrew letter`,
|
|
||||||
normal: `Which Hebrew letter is on ${wallLabel}`,
|
|
||||||
hard: `${wallLabel} letter`
|
|
||||||
},
|
|
||||||
answerByDifficulty: hebrewLabel
|
|
||||||
},
|
|
||||||
cubeHebrewLetterPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cubeEdges.forEach((edge) => {
|
|
||||||
const edgeName = String(edge?.name || labelFromId(edge?.id)).trim();
|
|
||||||
const edgeLabel = edgeName ? `${edgeName} Edge` : "";
|
|
||||||
const hebrew = hebrewById.get(normalizeId(edge?.hebrewLetterId));
|
|
||||||
const hebrewLabel = formatHebrewLetterLabel(hebrew, edge?.hebrewLetterId);
|
|
||||||
const tarotCard = normalizeOption(hebrew?.tarot?.card);
|
|
||||||
|
|
||||||
if (tarotCard && edgeLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `tarot-cube-edge:${normalizeId(edge?.id || edgeName)}`,
|
|
||||||
categoryId: "tarot-cube-location",
|
|
||||||
category: "Tarot ↔ Cube",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${tarotCard} is on which Cube edge`,
|
|
||||||
normal: `Where is ${tarotCard} on the Cube edges`,
|
|
||||||
hard: `${tarotCard} edge location`
|
|
||||||
},
|
|
||||||
answerByDifficulty: edgeLabel
|
|
||||||
},
|
|
||||||
cubeLocationPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (edgeLabel && hebrewLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `cube-edge-letter:${normalizeId(edge?.id || edgeName)}`,
|
|
||||||
categoryId: "cube-hebrew-letter",
|
|
||||||
category: "Cube ↔ Hebrew",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${edgeLabel} corresponds to which Hebrew letter`,
|
|
||||||
normal: `Which Hebrew letter is on ${edgeLabel}`,
|
|
||||||
hard: `${edgeLabel} letter`
|
|
||||||
},
|
|
||||||
answerByDifficulty: hebrewLabel
|
|
||||||
},
|
|
||||||
cubeHebrewLetterPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (cubeCenter) {
|
|
||||||
const centerTarot = normalizeOption(cubeCenter?.associations?.tarotCard || cubeCenter?.tarotCard);
|
|
||||||
const centerHebrew = hebrewById.get(normalizeId(cubeCenter?.hebrewLetterId));
|
|
||||||
const centerHebrewLabel = formatHebrewLetterLabel(centerHebrew, cubeCenter?.hebrewLetterId);
|
|
||||||
|
|
||||||
if (centerTarot) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: "tarot-cube-center",
|
|
||||||
categoryId: "tarot-cube-location",
|
|
||||||
category: "Tarot ↔ Cube",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${centerTarot} is located at which Cube position`,
|
|
||||||
normal: `Where is ${centerTarot} on the Cube`,
|
|
||||||
hard: `${centerTarot} Cube location`
|
|
||||||
},
|
|
||||||
answerByDifficulty: "Center"
|
|
||||||
},
|
|
||||||
cubeLocationPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (centerHebrewLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: "cube-center-letter",
|
|
||||||
categoryId: "cube-hebrew-letter",
|
|
||||||
category: "Cube ↔ Hebrew",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: "The Cube center corresponds to which Hebrew letter",
|
|
||||||
normal: "Which Hebrew letter is at the Cube center",
|
|
||||||
hard: "Cube center letter"
|
|
||||||
},
|
|
||||||
answerByDifficulty: centerHebrewLabel
|
|
||||||
},
|
|
||||||
cubeHebrewLetterPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playingCards.forEach((entry) => {
|
|
||||||
const cardId = String(entry?.id || "").trim();
|
|
||||||
const rankLabel = normalizeOption(entry?.rankLabel || entry?.rank);
|
|
||||||
const suitLabel = normalizeOption(entry?.suitLabel || labelFromId(entry?.suit));
|
|
||||||
const tarotCard = normalizeOption(entry?.tarotCard);
|
|
||||||
|
|
||||||
if (!cardId || !rankLabel || !suitLabel || !tarotCard) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `playing-card-tarot:${cardId}`,
|
|
||||||
categoryId: "playing-card-tarot",
|
|
||||||
category: "Playing Card ↔ Tarot",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${rankLabel} of ${suitLabel} maps to which Tarot card`,
|
|
||||||
normal: `${rankLabel} of ${suitLabel} corresponds to`,
|
|
||||||
hard: `${rankLabel} of ${suitLabel} maps to`
|
|
||||||
},
|
|
||||||
answerByDifficulty: tarotCard
|
|
||||||
},
|
|
||||||
playingTarotPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
231
app/ui-tarot-card-derivations.js
Normal file
231
app/ui-tarot-card-derivations.js
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
(function () {
|
||||||
|
function createTarotCardDerivations(dependencies) {
|
||||||
|
const {
|
||||||
|
normalizeRelationId,
|
||||||
|
normalizeTarotCardLookupName,
|
||||||
|
toTitleCase,
|
||||||
|
getReferenceData,
|
||||||
|
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
|
||||||
|
} = dependencies || {};
|
||||||
|
|
||||||
|
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(" · ");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
wands: "fire",
|
||||||
|
cups: "water",
|
||||||
|
swords: "air",
|
||||||
|
disks: "earth"
|
||||||
|
}[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 referenceData = typeof getReferenceData === "function" ? getReferenceData() : null;
|
||||||
|
const sign = (Array.isArray(referenceData?.signs) ? 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 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 {
|
||||||
|
buildTypeLabel,
|
||||||
|
buildElementRelationsForCard,
|
||||||
|
buildTetragrammatonRelationsForCard,
|
||||||
|
buildSmallCardRulershipRelation,
|
||||||
|
findSephirahForMinorCard
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotCardDerivations = {
|
||||||
|
createTarotCardDerivations
|
||||||
|
};
|
||||||
|
})();
|
||||||
312
app/ui-tarot-detail.js
Normal file
312
app/ui-tarot-detail.js
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
(function () {
|
||||||
|
function createTarotDetailRenderer(dependencies) {
|
||||||
|
const {
|
||||||
|
getMonthRefsByCardId,
|
||||||
|
getMagickDataset,
|
||||||
|
resolveTarotCardImage,
|
||||||
|
getDisplayCardName,
|
||||||
|
buildTypeLabel,
|
||||||
|
clearChildren,
|
||||||
|
normalizeRelationObject,
|
||||||
|
buildElementRelationsForCard,
|
||||||
|
buildTetragrammatonRelationsForCard,
|
||||||
|
buildSmallCardRulershipRelation,
|
||||||
|
buildSmallCardCourtLinkRelations,
|
||||||
|
buildCubeRelationsForCard,
|
||||||
|
parseMonthDayToken,
|
||||||
|
createRelationListItem,
|
||||||
|
findSephirahForMinorCard
|
||||||
|
} = dependencies || {};
|
||||||
|
|
||||||
|
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 = (getMonthRefsByCardId().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);
|
||||||
|
|
||||||
|
const kabPathEl = elements.tarotKabPathEl;
|
||||||
|
if (kabPathEl) {
|
||||||
|
const kabTree = getMagickDataset()?.grouped?.kabbalah?.["kabbalah-tree"];
|
||||||
|
const kabPath = (card.arcana === "Major" && typeof card.number === "number" && kabTree)
|
||||||
|
? kabTree.paths.find((path) => path.tarot?.trumpNumber === card.number)
|
||||||
|
: null;
|
||||||
|
const kabSeph = !kabPath ? findSephirahForMinorCard(card, kabTree) : null;
|
||||||
|
|
||||||
|
if (kabPath) {
|
||||||
|
const letter = kabPath.hebrewLetter || {};
|
||||||
|
const fromName = kabTree.sephiroth.find((seph) => seph.number === kabPath.connects.from)?.name || kabPath.connects.from;
|
||||||
|
const toName = kabTree.sephiroth.find((seph) => seph.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 = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderStaticRelationGroup,
|
||||||
|
renderDetail
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotDetailUi = {
|
||||||
|
createTarotDetailRenderer
|
||||||
|
};
|
||||||
|
})();
|
||||||
286
app/ui-tarot-relation-display.js
Normal file
286
app/ui-tarot-relation-display.js
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
(function () {
|
||||||
|
function createTarotRelationDisplay(dependencies) {
|
||||||
|
const { normalizeRelationId } = dependencies || {};
|
||||||
|
|
||||||
|
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)";
|
||||||
|
}
|
||||||
|
|
||||||
|
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", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
document.dispatchEvent(new CustomEvent(navTarget.event, { detail: navTarget.detail }));
|
||||||
|
});
|
||||||
|
item.appendChild(navBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatRelation,
|
||||||
|
relationKey,
|
||||||
|
normalizeRelationObject,
|
||||||
|
formatRelationDataLines,
|
||||||
|
getRelationNavTarget,
|
||||||
|
createRelationListItem
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotRelationDisplay = {
|
||||||
|
createTarotRelationDisplay
|
||||||
|
};
|
||||||
|
})();
|
||||||
772
app/ui-tarot.js
772
app/ui-tarot.js
@@ -2,6 +2,9 @@
|
|||||||
const { resolveTarotCardImage, getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
|
const { resolveTarotCardImage, getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
|
||||||
const tarotHouseUi = window.TarotHouseUi || {};
|
const tarotHouseUi = window.TarotHouseUi || {};
|
||||||
const tarotRelationsUi = window.TarotRelationsUi || {};
|
const tarotRelationsUi = window.TarotRelationsUi || {};
|
||||||
|
const tarotCardDerivations = window.TarotCardDerivations || {};
|
||||||
|
const tarotDetailUi = window.TarotDetailUi || {};
|
||||||
|
const tarotRelationDisplay = window.TarotRelationDisplay || {};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@@ -242,6 +245,56 @@
|
|||||||
.replace(/(^-|-$)/g, "");
|
.replace(/(^-|-$)/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof tarotRelationDisplay.createTarotRelationDisplay !== "function") {
|
||||||
|
throw new Error("TarotRelationDisplay.createTarotRelationDisplay is unavailable. Ensure app/ui-tarot-relation-display.js loads before app/ui-tarot.js.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof tarotCardDerivations.createTarotCardDerivations !== "function") {
|
||||||
|
throw new Error("TarotCardDerivations.createTarotCardDerivations is unavailable. Ensure app/ui-tarot-card-derivations.js loads before app/ui-tarot.js.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof tarotDetailUi.createTarotDetailRenderer !== "function") {
|
||||||
|
throw new Error("TarotDetailUi.createTarotDetailRenderer is unavailable. Ensure app/ui-tarot-detail.js loads before app/ui-tarot.js.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tarotCardDerivationsUi = tarotCardDerivations.createTarotCardDerivations({
|
||||||
|
normalizeRelationId,
|
||||||
|
normalizeTarotCardLookupName,
|
||||||
|
toTitleCase,
|
||||||
|
getReferenceData: () => state.referenceData,
|
||||||
|
ELEMENT_NAME_BY_ID,
|
||||||
|
ELEMENT_HEBREW_LETTER_BY_ID,
|
||||||
|
ELEMENT_HEBREW_CHAR_BY_ID,
|
||||||
|
HEBREW_LETTER_ID_BY_TETRAGRAMMATON_LETTER,
|
||||||
|
ACE_ELEMENT_BY_CARD_NAME,
|
||||||
|
COURT_ELEMENT_BY_RANK,
|
||||||
|
MINOR_RANK_NUMBER_BY_NAME,
|
||||||
|
SMALL_CARD_SIGN_BY_MODALITY_AND_SUIT,
|
||||||
|
MINOR_PLURAL_BY_RANK
|
||||||
|
});
|
||||||
|
|
||||||
|
const tarotRelationDisplayUi = tarotRelationDisplay.createTarotRelationDisplay({
|
||||||
|
normalizeRelationId
|
||||||
|
});
|
||||||
|
|
||||||
|
const tarotDetailRenderer = tarotDetailUi.createTarotDetailRenderer({
|
||||||
|
getMonthRefsByCardId: () => state.monthRefsByCardId,
|
||||||
|
getMagickDataset: () => state.magickDataset,
|
||||||
|
resolveTarotCardImage,
|
||||||
|
getDisplayCardName,
|
||||||
|
buildTypeLabel,
|
||||||
|
clearChildren,
|
||||||
|
normalizeRelationObject,
|
||||||
|
buildElementRelationsForCard,
|
||||||
|
buildTetragrammatonRelationsForCard,
|
||||||
|
buildSmallCardRulershipRelation,
|
||||||
|
buildSmallCardCourtLinkRelations,
|
||||||
|
buildCubeRelationsForCard,
|
||||||
|
parseMonthDayToken,
|
||||||
|
createRelationListItem,
|
||||||
|
findSephirahForMinorCard
|
||||||
|
});
|
||||||
|
|
||||||
function normalizeSearchValue(value) {
|
function normalizeSearchValue(value) {
|
||||||
return String(value || "").trim().toLowerCase();
|
return String(value || "").trim().toLowerCase();
|
||||||
}
|
}
|
||||||
@@ -299,165 +352,16 @@
|
|||||||
.join(" ");
|
.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 = []) {
|
function buildElementRelationsForCard(card, baseElementRelations = []) {
|
||||||
if (!card) {
|
return tarotCardDerivationsUi.buildElementRelationsForCard(card, baseElementRelations);
|
||||||
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) {
|
function buildTetragrammatonRelationsForCard(card) {
|
||||||
if (!card) {
|
return tarotCardDerivationsUi.buildTetragrammatonRelationsForCard(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) {
|
function buildSmallCardRulershipRelation(card) {
|
||||||
if (!card || card.arcana !== "Minor") {
|
return tarotCardDerivationsUi.buildSmallCardRulershipRelation(card);
|
||||||
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) {
|
function buildCourtCardByDecanId(cards) {
|
||||||
@@ -566,21 +470,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildTypeLabel(card) {
|
function buildTypeLabel(card) {
|
||||||
if (card.arcana === "Major") {
|
return tarotCardDerivationsUi.buildTypeLabel(card);
|
||||||
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 = {
|
const MINOR_PLURAL_BY_RANK = {
|
||||||
@@ -597,100 +487,23 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
function findSephirahForMinorCard(card, kabTree) {
|
function findSephirahForMinorCard(card, kabTree) {
|
||||||
if (!card || card.arcana !== "Minor" || !kabTree) {
|
return tarotCardDerivationsUi.findSephirahForMinorCard(card, 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) {
|
function formatRelation(relation) {
|
||||||
if (typeof relation === "string") {
|
return tarotRelationDisplayUi.formatRelation(relation);
|
||||||
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) {
|
function relationKey(relation, index) {
|
||||||
const safeType = String(relation?.type || "relation");
|
return tarotRelationDisplayUi.relationKey(relation, index);
|
||||||
const safeId = String(relation?.id || index || "0");
|
|
||||||
const safeLabel = String(relation?.label || relation?.text || "");
|
|
||||||
return `${safeType}|${safeId}|${safeLabel}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeRelationObject(relation, index) {
|
function normalizeRelationObject(relation, index) {
|
||||||
if (relation && typeof relation === "object") {
|
return tarotRelationDisplayUi.normalizeRelationObject(relation, index);
|
||||||
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) {
|
function formatRelationDataLines(relation) {
|
||||||
if (!relation || typeof relation !== "object") {
|
return tarotRelationDisplayUi.formatRelationDataLines(relation);
|
||||||
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)";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCubeRelationsForCard(card) {
|
function buildCubeRelationsForCard(card) {
|
||||||
@@ -703,472 +516,19 @@
|
|||||||
// Returns nav dispatch config for relations that have a corresponding section,
|
// Returns nav dispatch config for relations that have a corresponding section,
|
||||||
// null for informational-only relations.
|
// null for informational-only relations.
|
||||||
function getRelationNavTarget(relation) {
|
function getRelationNavTarget(relation) {
|
||||||
const t = relation?.type;
|
return tarotRelationDisplayUi.getRelationNavTarget(relation);
|
||||||
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) {
|
function createRelationListItem(relation) {
|
||||||
const item = document.createElement("li");
|
return tarotRelationDisplayUi.createRelationListItem(relation);
|
||||||
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) {
|
function renderStaticRelationGroup(targetEl, cardEl, relations) {
|
||||||
clearChildren(targetEl);
|
tarotDetailRenderer.renderStaticRelationGroup(targetEl, cardEl, relations);
|
||||||
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) {
|
function renderDetail(card, elements) {
|
||||||
if (!card || !elements) {
|
tarotDetailRenderer.renderDetail(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 = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateListSelection(elements) {
|
function updateListSelection(elements) {
|
||||||
|
|||||||
246
app/ui-zodiac-references.js
Normal file
246
app/ui-zodiac-references.js
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
/* ui-zodiac-references.js — Month and cube reference builders for the zodiac section */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||||
|
|
||||||
|
function cap(value) {
|
||||||
|
return String(value || "").charAt(0).toUpperCase() + String(value || "").slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateRange(rulesFrom) {
|
||||||
|
if (!Array.isArray(rulesFrom) || rulesFrom.length < 2) return "—";
|
||||||
|
const [from, to] = rulesFrom;
|
||||||
|
const fMonth = MONTH_NAMES[(from[0] || 1) - 1];
|
||||||
|
const tMonth = MONTH_NAMES[(to[0] || 1) - 1];
|
||||||
|
return `${fMonth} ${from[1]} – ${tMonth} ${to[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMonthReferencesBySign(referenceData) {
|
||||||
|
const map = new Map();
|
||||||
|
const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [];
|
||||||
|
const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
||||||
|
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
||||||
|
const monthById = new Map(months.map((month) => [month.id, month]));
|
||||||
|
const monthByOrder = new Map(
|
||||||
|
months
|
||||||
|
.filter((month) => Number.isFinite(Number(month?.order)))
|
||||||
|
.map((month) => [Number(month.order), month])
|
||||||
|
);
|
||||||
|
|
||||||
|
function parseMonthDay(value) {
|
||||||
|
const [month, day] = String(value || "").split("-").map((part) => Number(part));
|
||||||
|
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { month, day };
|
||||||
|
}
|
||||||
|
|
||||||
|
function monthOrdersInRange(startMonth, endMonth) {
|
||||||
|
const orders = [];
|
||||||
|
let cursor = startMonth;
|
||||||
|
let guard = 0;
|
||||||
|
|
||||||
|
while (guard < 13) {
|
||||||
|
orders.push(cursor);
|
||||||
|
if (cursor === endMonth) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cursor = cursor === 12 ? 1 : cursor + 1;
|
||||||
|
guard += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return orders;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushRef(signId, month) {
|
||||||
|
const key = String(signId || "").trim().toLowerCase();
|
||||||
|
if (!key || !month?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = map.get(key);
|
||||||
|
if (rows.some((entry) => entry.id === month.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
id: month.id,
|
||||||
|
name: month.name || month.id,
|
||||||
|
order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
months.forEach((month) => {
|
||||||
|
pushRef(month?.associations?.zodiacSignId, month);
|
||||||
|
const events = Array.isArray(month?.events) ? month.events : [];
|
||||||
|
events.forEach((event) => {
|
||||||
|
pushRef(event?.associations?.zodiacSignId, month);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
holidays.forEach((holiday) => {
|
||||||
|
const month = monthById.get(holiday?.monthId);
|
||||||
|
if (!month) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pushRef(holiday?.associations?.zodiacSignId, month);
|
||||||
|
});
|
||||||
|
|
||||||
|
signs.forEach((sign) => {
|
||||||
|
const start = parseMonthDay(sign?.start);
|
||||||
|
const end = parseMonthDay(sign?.end);
|
||||||
|
if (!start || !end || !sign?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
monthOrdersInRange(start.month, end.month).forEach((monthOrder) => {
|
||||||
|
const month = monthByOrder.get(monthOrder);
|
||||||
|
if (month) {
|
||||||
|
pushRef(sign.id, month);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
map.forEach((rows, key) => {
|
||||||
|
rows.sort((left, right) => left.order - right.order || left.name.localeCompare(right.name));
|
||||||
|
map.set(key, rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCubeSignPlacements(magickDataset) {
|
||||||
|
const placements = new Map();
|
||||||
|
const cube = magickDataset?.grouped?.kabbalah?.cube || {};
|
||||||
|
const walls = Array.isArray(cube?.walls)
|
||||||
|
? cube.walls
|
||||||
|
: [];
|
||||||
|
const edges = Array.isArray(cube?.edges)
|
||||||
|
? cube.edges
|
||||||
|
: [];
|
||||||
|
const paths = Array.isArray(magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]?.paths)
|
||||||
|
? magickDataset.grouped.kabbalah["kabbalah-tree"].paths
|
||||||
|
: [];
|
||||||
|
|
||||||
|
function normalizeLetterId(value) {
|
||||||
|
const key = String(value || "").toLowerCase().replace(/[^a-z]/g, "").trim();
|
||||||
|
const aliases = {
|
||||||
|
aleph: "alef",
|
||||||
|
beth: "bet",
|
||||||
|
zain: "zayin",
|
||||||
|
cheth: "het",
|
||||||
|
chet: "het",
|
||||||
|
daleth: "dalet",
|
||||||
|
teth: "tet",
|
||||||
|
peh: "pe",
|
||||||
|
tzaddi: "tsadi",
|
||||||
|
tzadi: "tsadi",
|
||||||
|
tzade: "tsadi",
|
||||||
|
tsaddi: "tsadi",
|
||||||
|
qoph: "qof",
|
||||||
|
taw: "tav",
|
||||||
|
tau: "tav"
|
||||||
|
};
|
||||||
|
return aliases[key] || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function edgeWalls(edge) {
|
||||||
|
const explicitWalls = Array.isArray(edge?.walls)
|
||||||
|
? edge.walls.map((wallId) => String(wallId || "").trim().toLowerCase()).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (explicitWalls.length >= 2) {
|
||||||
|
return explicitWalls.slice(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(edge?.id || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.split("-")
|
||||||
|
.map((wallId) => wallId.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function edgeLabel(edge) {
|
||||||
|
const explicitName = String(edge?.name || "").trim();
|
||||||
|
if (explicitName) {
|
||||||
|
return explicitName;
|
||||||
|
}
|
||||||
|
return edgeWalls(edge)
|
||||||
|
.map((part) => cap(part))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCubeDirectionLabel(wallId, edge) {
|
||||||
|
const normalizedWallId = String(wallId || "").trim().toLowerCase();
|
||||||
|
const edgeId = String(edge?.id || "").trim().toLowerCase();
|
||||||
|
if (!normalizedWallId || !edgeId) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const cubeUi = window.CubeSectionUi;
|
||||||
|
if (cubeUi && typeof cubeUi.getEdgeDirectionLabelForWall === "function") {
|
||||||
|
const directionLabel = String(cubeUi.getEdgeDirectionLabelForWall(normalizedWallId, edgeId) || "").trim();
|
||||||
|
if (directionLabel) {
|
||||||
|
return directionLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return edgeLabel(edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallById = new Map(
|
||||||
|
walls.map((wall) => [String(wall?.id || "").trim().toLowerCase(), wall])
|
||||||
|
);
|
||||||
|
|
||||||
|
const pathByLetterId = new Map(
|
||||||
|
paths
|
||||||
|
.map((path) => [normalizeLetterId(path?.hebrewLetter?.transliteration), path])
|
||||||
|
.filter(([letterId]) => Boolean(letterId))
|
||||||
|
);
|
||||||
|
|
||||||
|
edges.forEach((edge) => {
|
||||||
|
const letterId = normalizeLetterId(edge?.hebrewLetterId || edge?.associations?.hebrewLetterId);
|
||||||
|
const path = pathByLetterId.get(letterId) || null;
|
||||||
|
const signId = path?.astrology?.type === "zodiac"
|
||||||
|
? String(path?.astrology?.name || "").trim().toLowerCase()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (!signId || placements.has(signId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallsForEdge = edgeWalls(edge);
|
||||||
|
const primaryWallId = wallsForEdge[0] || "";
|
||||||
|
const primaryWall = wallById.get(primaryWallId);
|
||||||
|
|
||||||
|
placements.set(signId, {
|
||||||
|
wallId: primaryWallId,
|
||||||
|
edgeId: String(edge?.id || "").trim().toLowerCase(),
|
||||||
|
wallName: primaryWall?.name || cap(primaryWallId || "wall"),
|
||||||
|
edgeName: resolveCubeDirectionLabel(primaryWallId, edge)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return placements;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cubePlacementLabel(placement) {
|
||||||
|
const wallName = placement?.wallName || "Wall";
|
||||||
|
const edgeName = placement?.edgeName || "Direction";
|
||||||
|
return `Cube: ${wallName} Wall - ${edgeName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ZodiacReferenceBuilders = {
|
||||||
|
buildCubeSignPlacements,
|
||||||
|
buildMonthReferencesBySign,
|
||||||
|
cubePlacementLabel,
|
||||||
|
formatDateRange
|
||||||
|
};
|
||||||
|
})();
|
||||||
235
app/ui-zodiac.js
235
app/ui-zodiac.js
@@ -2,6 +2,17 @@
|
|||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const zodiacReferenceBuilders = window.ZodiacReferenceBuilders || {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof zodiacReferenceBuilders.buildCubeSignPlacements !== "function"
|
||||||
|
|| typeof zodiacReferenceBuilders.buildMonthReferencesBySign !== "function"
|
||||||
|
|| typeof zodiacReferenceBuilders.cubePlacementLabel !== "function"
|
||||||
|
|| typeof zodiacReferenceBuilders.formatDateRange !== "function"
|
||||||
|
) {
|
||||||
|
throw new Error("ZodiacReferenceBuilders module must load before ui-zodiac.js");
|
||||||
|
}
|
||||||
|
|
||||||
const ELEMENT_STYLE = {
|
const ELEMENT_STYLE = {
|
||||||
fire: { emoji: "🔥", badge: "zod-badge--fire", label: "Fire" },
|
fire: { emoji: "🔥", badge: "zod-badge--fire", label: "Fire" },
|
||||||
earth: { emoji: "🌍", badge: "zod-badge--earth", label: "Earth" },
|
earth: { emoji: "🌍", badge: "zod-badge--earth", label: "Earth" },
|
||||||
@@ -14,8 +25,6 @@
|
|||||||
venus: "♀︎", mercury: "☿︎", luna: "☾︎"
|
venus: "♀︎", mercury: "☿︎", luna: "☾︎"
|
||||||
};
|
};
|
||||||
|
|
||||||
const MONTH_NAMES = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
entries: [],
|
entries: [],
|
||||||
@@ -58,233 +67,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatDateRange(rulesFrom) {
|
function formatDateRange(rulesFrom) {
|
||||||
if (!Array.isArray(rulesFrom) || rulesFrom.length < 2) return "—";
|
return zodiacReferenceBuilders.formatDateRange(rulesFrom);
|
||||||
const [from, to] = rulesFrom;
|
|
||||||
const fMonth = MONTH_NAMES[(from[0] || 1) - 1];
|
|
||||||
const tMonth = MONTH_NAMES[(to[0] || 1) - 1];
|
|
||||||
return `${fMonth} ${from[1]} – ${tMonth} ${to[1]}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMonthReferencesBySign(referenceData) {
|
function buildMonthReferencesBySign(referenceData) {
|
||||||
const map = new Map();
|
return zodiacReferenceBuilders.buildMonthReferencesBySign(referenceData);
|
||||||
const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [];
|
|
||||||
const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
|
||||||
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
|
||||||
const monthById = new Map(months.map((month) => [month.id, month]));
|
|
||||||
const monthByOrder = new Map(
|
|
||||||
months
|
|
||||||
.filter((month) => Number.isFinite(Number(month?.order)))
|
|
||||||
.map((month) => [Number(month.order), month])
|
|
||||||
);
|
|
||||||
|
|
||||||
function parseMonthDay(value) {
|
|
||||||
const [month, day] = String(value || "").split("-").map((part) => Number(part));
|
|
||||||
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return { month, day };
|
|
||||||
}
|
|
||||||
|
|
||||||
function monthOrdersInRange(startMonth, endMonth) {
|
|
||||||
const orders = [];
|
|
||||||
let cursor = startMonth;
|
|
||||||
let guard = 0;
|
|
||||||
|
|
||||||
while (guard < 13) {
|
|
||||||
orders.push(cursor);
|
|
||||||
if (cursor === endMonth) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
cursor = cursor === 12 ? 1 : cursor + 1;
|
|
||||||
guard += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return orders;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushRef(signId, month) {
|
|
||||||
const key = String(signId || "").trim().toLowerCase();
|
|
||||||
if (!key || !month?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map.has(key)) {
|
|
||||||
map.set(key, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = map.get(key);
|
|
||||||
if (rows.some((entry) => entry.id === month.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rows.push({
|
|
||||||
id: month.id,
|
|
||||||
name: month.name || month.id,
|
|
||||||
order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
months.forEach((month) => {
|
|
||||||
pushRef(month?.associations?.zodiacSignId, month);
|
|
||||||
const events = Array.isArray(month?.events) ? month.events : [];
|
|
||||||
events.forEach((event) => {
|
|
||||||
pushRef(event?.associations?.zodiacSignId, month);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
holidays.forEach((holiday) => {
|
|
||||||
const month = monthById.get(holiday?.monthId);
|
|
||||||
if (!month) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pushRef(holiday?.associations?.zodiacSignId, month);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Structural month coverage from sign date ranges (e.g., Scorpio spans Oct+Nov).
|
|
||||||
signs.forEach((sign) => {
|
|
||||||
const start = parseMonthDay(sign?.start);
|
|
||||||
const end = parseMonthDay(sign?.end);
|
|
||||||
if (!start || !end || !sign?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
monthOrdersInRange(start.month, end.month).forEach((monthOrder) => {
|
|
||||||
const month = monthByOrder.get(monthOrder);
|
|
||||||
if (month) {
|
|
||||||
pushRef(sign.id, month);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
map.forEach((rows, key) => {
|
|
||||||
rows.sort((left, right) => left.order - right.order || left.name.localeCompare(right.name));
|
|
||||||
map.set(key, rows);
|
|
||||||
});
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCubeSignPlacements(magickDataset) {
|
function buildCubeSignPlacements(magickDataset) {
|
||||||
const placements = new Map();
|
return zodiacReferenceBuilders.buildCubeSignPlacements(magickDataset);
|
||||||
const cube = magickDataset?.grouped?.kabbalah?.cube || {};
|
|
||||||
const walls = Array.isArray(cube?.walls)
|
|
||||||
? cube.walls
|
|
||||||
: [];
|
|
||||||
const edges = Array.isArray(cube?.edges)
|
|
||||||
? cube.edges
|
|
||||||
: [];
|
|
||||||
const paths = Array.isArray(magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]?.paths)
|
|
||||||
? magickDataset.grouped.kabbalah["kabbalah-tree"].paths
|
|
||||||
: [];
|
|
||||||
|
|
||||||
function normalizeLetterId(value) {
|
|
||||||
const key = String(value || "").toLowerCase().replace(/[^a-z]/g, "").trim();
|
|
||||||
const aliases = {
|
|
||||||
aleph: "alef",
|
|
||||||
beth: "bet",
|
|
||||||
zain: "zayin",
|
|
||||||
cheth: "het",
|
|
||||||
chet: "het",
|
|
||||||
daleth: "dalet",
|
|
||||||
teth: "tet",
|
|
||||||
peh: "pe",
|
|
||||||
tzaddi: "tsadi",
|
|
||||||
tzadi: "tsadi",
|
|
||||||
tzade: "tsadi",
|
|
||||||
tsaddi: "tsadi",
|
|
||||||
qoph: "qof",
|
|
||||||
taw: "tav",
|
|
||||||
tau: "tav"
|
|
||||||
};
|
|
||||||
return aliases[key] || key;
|
|
||||||
}
|
|
||||||
|
|
||||||
function edgeWalls(edge) {
|
|
||||||
const explicitWalls = Array.isArray(edge?.walls)
|
|
||||||
? edge.walls.map((wallId) => String(wallId || "").trim().toLowerCase()).filter(Boolean)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (explicitWalls.length >= 2) {
|
|
||||||
return explicitWalls.slice(0, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(edge?.id || "")
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.split("-")
|
|
||||||
.map((wallId) => wallId.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
.slice(0, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
function edgeLabel(edge) {
|
|
||||||
const explicitName = String(edge?.name || "").trim();
|
|
||||||
if (explicitName) {
|
|
||||||
return explicitName;
|
|
||||||
}
|
|
||||||
return edgeWalls(edge)
|
|
||||||
.map((part) => cap(part))
|
|
||||||
.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveCubeDirectionLabel(wallId, edge) {
|
|
||||||
const normalizedWallId = String(wallId || "").trim().toLowerCase();
|
|
||||||
const edgeId = String(edge?.id || "").trim().toLowerCase();
|
|
||||||
if (!normalizedWallId || !edgeId) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const cubeUi = window.CubeSectionUi;
|
|
||||||
if (cubeUi && typeof cubeUi.getEdgeDirectionLabelForWall === "function") {
|
|
||||||
const directionLabel = String(cubeUi.getEdgeDirectionLabelForWall(normalizedWallId, edgeId) || "").trim();
|
|
||||||
if (directionLabel) {
|
|
||||||
return directionLabel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return edgeLabel(edge);
|
|
||||||
}
|
|
||||||
|
|
||||||
const wallById = new Map(
|
|
||||||
walls.map((wall) => [String(wall?.id || "").trim().toLowerCase(), wall])
|
|
||||||
);
|
|
||||||
|
|
||||||
const pathByLetterId = new Map(
|
|
||||||
paths
|
|
||||||
.map((path) => [normalizeLetterId(path?.hebrewLetter?.transliteration), path])
|
|
||||||
.filter(([letterId]) => Boolean(letterId))
|
|
||||||
);
|
|
||||||
|
|
||||||
edges.forEach((edge) => {
|
|
||||||
const letterId = normalizeLetterId(edge?.hebrewLetterId || edge?.associations?.hebrewLetterId);
|
|
||||||
const path = pathByLetterId.get(letterId) || null;
|
|
||||||
const signId = path?.astrology?.type === "zodiac"
|
|
||||||
? String(path?.astrology?.name || "").trim().toLowerCase()
|
|
||||||
: "";
|
|
||||||
|
|
||||||
if (!signId || placements.has(signId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wallsForEdge = edgeWalls(edge);
|
|
||||||
const primaryWallId = wallsForEdge[0] || "";
|
|
||||||
const primaryWall = wallById.get(primaryWallId);
|
|
||||||
|
|
||||||
placements.set(signId, {
|
|
||||||
wallId: primaryWallId,
|
|
||||||
edgeId: String(edge?.id || "").trim().toLowerCase(),
|
|
||||||
wallName: primaryWall?.name || cap(primaryWallId || "wall"),
|
|
||||||
edgeName: resolveCubeDirectionLabel(primaryWallId, edge)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return placements;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cubePlacementLabel(placement) {
|
function cubePlacementLabel(placement) {
|
||||||
const wallName = placement?.wallName || "Wall";
|
return zodiacReferenceBuilders.cubePlacementLabel(placement);
|
||||||
const edgeName = placement?.edgeName || "Direction";
|
|
||||||
return `Cube: ${wallName} Wall - ${edgeName}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── List ──────────────────────────────────────────────────────────────
|
// ── List ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
21
index.html
21
index.html
@@ -773,33 +773,54 @@
|
|||||||
<script src="app/ui-tarot-lightbox.js?v=20260307b"></script>
|
<script src="app/ui-tarot-lightbox.js?v=20260307b"></script>
|
||||||
<script src="app/ui-tarot-house.js?v=20260307b"></script>
|
<script src="app/ui-tarot-house.js?v=20260307b"></script>
|
||||||
<script src="app/ui-tarot-relations.js"></script>
|
<script src="app/ui-tarot-relations.js"></script>
|
||||||
|
<script src="app/ui-now-helpers.js"></script>
|
||||||
<script src="app/ui-now.js"></script>
|
<script src="app/ui-now.js"></script>
|
||||||
<script src="app/ui-natal.js"></script>
|
<script src="app/ui-natal.js"></script>
|
||||||
|
<script src="app/tarot-database-builders.js"></script>
|
||||||
|
<script src="app/tarot-database-assembly.js"></script>
|
||||||
<script src="app/tarot-database.js"></script>
|
<script src="app/tarot-database.js"></script>
|
||||||
<script src="app/ui-calendar-dates.js"></script>
|
<script src="app/ui-calendar-dates.js"></script>
|
||||||
|
<script src="app/ui-calendar-detail-panels.js"></script>
|
||||||
<script src="app/ui-calendar-detail.js"></script>
|
<script src="app/ui-calendar-detail.js"></script>
|
||||||
|
<script src="app/ui-calendar-data.js"></script>
|
||||||
<script src="app/ui-calendar.js"></script>
|
<script src="app/ui-calendar.js"></script>
|
||||||
|
<script src="app/ui-holidays-data.js"></script>
|
||||||
|
<script src="app/ui-holidays-render.js"></script>
|
||||||
<script src="app/ui-holidays.js"></script>
|
<script src="app/ui-holidays.js"></script>
|
||||||
|
<script src="app/ui-tarot-card-derivations.js?v=20260307b"></script>
|
||||||
|
<script src="app/ui-tarot-detail.js?v=20260307b"></script>
|
||||||
|
<script src="app/ui-tarot-relation-display.js?v=20260307b"></script>
|
||||||
<script src="app/ui-tarot.js?v=20260307b"></script>
|
<script src="app/ui-tarot.js?v=20260307b"></script>
|
||||||
|
<script src="app/ui-planets-references.js"></script>
|
||||||
<script src="app/ui-planets.js"></script>
|
<script src="app/ui-planets.js"></script>
|
||||||
<script src="app/ui-cycles.js"></script>
|
<script src="app/ui-cycles.js"></script>
|
||||||
<script src="app/ui-elements.js"></script>
|
<script src="app/ui-elements.js"></script>
|
||||||
|
<script src="app/ui-iching-references.js"></script>
|
||||||
<script src="app/ui-iching.js"></script>
|
<script src="app/ui-iching.js"></script>
|
||||||
<script src="app/ui-rosicrucian-cross.js"></script>
|
<script src="app/ui-rosicrucian-cross.js"></script>
|
||||||
<script src="app/ui-kabbalah-detail.js"></script>
|
<script src="app/ui-kabbalah-detail.js"></script>
|
||||||
|
<script src="app/ui-kabbalah-views.js"></script>
|
||||||
<script src="app/ui-kabbalah.js"></script>
|
<script src="app/ui-kabbalah.js"></script>
|
||||||
<script src="app/ui-cube-detail.js"></script>
|
<script src="app/ui-cube-detail.js"></script>
|
||||||
|
<script src="app/ui-cube-chassis.js"></script>
|
||||||
|
<script src="app/ui-cube-math.js"></script>
|
||||||
<script src="app/ui-cube.js"></script>
|
<script src="app/ui-cube.js"></script>
|
||||||
<script src="app/ui-alphabet-gematria.js"></script>
|
<script src="app/ui-alphabet-gematria.js"></script>
|
||||||
<script src="app/ui-alphabet-references.js"></script>
|
<script src="app/ui-alphabet-references.js"></script>
|
||||||
<script src="app/ui-alphabet-detail.js"></script>
|
<script src="app/ui-alphabet-detail.js"></script>
|
||||||
|
<script src="app/ui-alphabet-kabbalah.js"></script>
|
||||||
<script src="app/ui-alphabet.js"></script>
|
<script src="app/ui-alphabet.js"></script>
|
||||||
|
<script src="app/ui-zodiac-references.js"></script>
|
||||||
<script src="app/ui-zodiac.js"></script>
|
<script src="app/ui-zodiac.js"></script>
|
||||||
|
<script src="app/ui-quiz-bank-builtins-domains.js"></script>
|
||||||
|
<script src="app/ui-quiz-bank-builtins.js"></script>
|
||||||
<script src="app/ui-quiz-bank.js"></script>
|
<script src="app/ui-quiz-bank.js"></script>
|
||||||
<script src="app/ui-quiz.js"></script>
|
<script src="app/ui-quiz.js"></script>
|
||||||
<script src="app/quiz-calendars.js"></script>
|
<script src="app/quiz-calendars.js"></script>
|
||||||
|
<script src="app/ui-gods-references.js"></script>
|
||||||
<script src="app/ui-gods.js"></script>
|
<script src="app/ui-gods.js"></script>
|
||||||
<script src="app/ui-enochian.js"></script>
|
<script src="app/ui-enochian.js"></script>
|
||||||
|
<script src="app/ui-numbers-detail.js"></script>
|
||||||
<script src="app/ui-numbers.js"></script>
|
<script src="app/ui-numbers.js"></script>
|
||||||
<script src="app/ui-tarot-spread.js"></script>
|
<script src="app/ui-tarot-spread.js"></script>
|
||||||
<script src="app/ui-settings.js"></script>
|
<script src="app/ui-settings.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user