/* ui-calendar.js — Month and celestial holiday browser */ (function () { "use strict"; const { getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {}; const state = { initialized: false, referenceData: null, magickDataset: null, selectedCalendar: "gregorian", calendarData: {}, months: [], filteredMonths: [], holidays: [], calendarHolidays: [], selectedMonthId: null, searchQuery: "", selectedYear: new Date().getFullYear(), selectedDayMonthId: null, selectedDayCalendarId: null, selectedDayEntries: [], planetsById: new Map(), signsById: new Map(), godsById: new Map(), hebrewById: new Map(), dayLinksCache: new Map() }; const TAROT_TRUMP_NUMBER_BY_NAME = { "the fool": 0, fool: 0, "the magus": 1, magus: 1, magician: 1, "the high priestess": 2, "high priestess": 2, "the empress": 3, empress: 3, "the emperor": 4, emperor: 4, "the hierophant": 5, hierophant: 5, "the lovers": 6, lovers: 6, "the chariot": 7, chariot: 7, strength: 8, lust: 8, "the hermit": 9, hermit: 9, fortune: 10, "wheel of fortune": 10, justice: 11, "the hanged man": 12, "hanged man": 12, death: 13, temperance: 14, art: 14, "the devil": 15, devil: 15, "the tower": 16, tower: 16, "the star": 17, star: 17, "the moon": 18, moon: 18, "the sun": 19, sun: 19, aeon: 20, judgement: 20, judgment: 20, universe: 21, world: 21, "the world": 21 }; const MINOR_NUMBER_WORD = { 1: "Ace", 2: "Two", 3: "Three", 4: "Four", 5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine", 10: "Ten" }; 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() { return { monthListEl: document.getElementById("calendar-month-list"), listTitleEl: document.getElementById("calendar-list-title"), monthCountEl: document.getElementById("calendar-month-count"), yearInputEl: document.getElementById("calendar-year-input"), calendarYearWrapEl: document.getElementById("calendar-year-wrap"), calendarTypeEl: document.getElementById("calendar-type-select"), searchInputEl: document.getElementById("calendar-search-input"), searchClearEl: document.getElementById("calendar-search-clear"), detailNameEl: document.getElementById("calendar-detail-name"), detailSubEl: document.getElementById("calendar-detail-sub"), detailBodyEl: document.getElementById("calendar-detail-body") }; } function normalizeText(value) { return String(value || "").trim(); } function normalizeSearchValue(value) { return String(value || "").trim().toLowerCase(); } function cap(value) { const text = normalizeText(value); return text ? text.charAt(0).toUpperCase() + text.slice(1) : text; } function normalizeTarotName(value) { return String(value || "") .trim() .toLowerCase() .replace(/\s+/g, " "); } function resolveTarotTrumpNumber(cardName) { const key = normalizeTarotName(cardName); if (!key) { return null; } if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, key)) { return TAROT_TRUMP_NUMBER_BY_NAME[key]; } const withoutLeadingThe = key.replace(/^the\s+/, ""); if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, withoutLeadingThe)) { return TAROT_TRUMP_NUMBER_BY_NAME[withoutLeadingThe]; } return null; } 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 normalizeMinorTarotCardName(cardName) { const text = String(cardName || "").trim(); if (!text) { return ""; } const match = text.match(/^(\d{1,2})\s+of\s+(.+)$/i); if (!match) { return text.replace(/\b(pentacles?|coins?)\b/i, "Disks"); } const numeric = Number(match[1]); const suitRaw = String(match[2] || "").trim(); const rank = MINOR_NUMBER_WORD[numeric] || String(numeric); const suit = suitRaw.replace(/\b(pentacles?|coins?)\b/i, "Disks"); return `${rank} of ${suit}`; } function parseMonthDayToken(token) { const [month, day] = String(token || "").split("-").map((part) => Number(part)); if (!Number.isFinite(month) || !Number.isFinite(day)) { return null; } return { month, day }; } function monthDayDate(monthDay, year) { const parsed = parseMonthDayToken(monthDay); if (!parsed) { return null; } return new Date(year, parsed.month - 1, parsed.day); } function buildSignDateBounds(sign) { const start = monthDayDate(sign?.start, 2025); const endBase = monthDayDate(sign?.end, 2025); if (!start || !endBase) { return null; } const wrapsYear = endBase.getTime() < start.getTime(); const end = wrapsYear ? monthDayDate(sign?.end, 2026) : endBase; if (!end) { return null; } return { start, end }; } function addDays(date, days) { const next = new Date(date); next.setDate(next.getDate() + days); return next; } function formatDateLabel(date) { return date.toLocaleDateString(undefined, { month: "short", day: "numeric" }); } function monthDayOrdinal(month, day) { if (!Number.isFinite(month) || !Number.isFinite(day)) { return null; } const base = new Date(2025, Math.trunc(month) - 1, Math.trunc(day), 12, 0, 0, 0); if (Number.isNaN(base.getTime())) { return null; } const start = new Date(2025, 0, 1, 12, 0, 0, 0); const diff = base.getTime() - start.getTime(); return Math.floor(diff / (24 * 60 * 60 * 1000)) + 1; } function isMonthDayInRange(targetMonth, targetDay, startMonth, startDay, endMonth, endDay) { const target = monthDayOrdinal(targetMonth, targetDay); const start = monthDayOrdinal(startMonth, startDay); const end = monthDayOrdinal(endMonth, endDay); if (!Number.isFinite(target) || !Number.isFinite(start) || !Number.isFinite(end)) { return false; } if (end >= start) { return target >= start && target <= end; } return target >= start || target <= end; } function parseMonthDayTokensFromText(value) { const text = String(value || ""); const matches = [...text.matchAll(/(\d{2})-(\d{2})/g)]; return matches .map((match) => ({ month: Number(match[1]), day: Number(match[2]) })) .filter((token) => Number.isFinite(token.month) && Number.isFinite(token.day)); } function parseDayRangeFromText(value) { const text = String(value || ""); const range = text.match(/\b(\d{1,2})\s*[–-]\s*(\d{1,2})\b/); if (!range) { return null; } const startDay = Number(range[1]); const endDay = Number(range[2]); if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) { return null; } return { startDay, endDay }; } function isoToDateAtNoon(iso) { const text = String(iso || "").trim(); if (!text) { return null; } const parsed = new Date(`${text}T12:00:00`); return Number.isNaN(parsed.getTime()) ? null : parsed; } function normalizeDayFilterEntry(dayNumber, gregorianIso) { const day = Math.trunc(Number(dayNumber)); if (!Number.isFinite(day) || day <= 0) { return null; } const iso = String(gregorianIso || "").trim(); if (!iso) { return null; } return { dayNumber: day, gregorianIso: iso }; } function sortDayFilterEntries(entries) { return [...entries].sort((left, right) => left.dayNumber - right.dayNumber || left.gregorianIso.localeCompare(right.gregorianIso)); } function ensureDayFilterContext(month) { if (!month) { return; } const sameContext = state.selectedDayMonthId === month.id && state.selectedDayCalendarId === state.selectedCalendar; if (!sameContext) { state.selectedDayMonthId = month.id; state.selectedDayCalendarId = state.selectedCalendar; state.selectedDayEntries = []; } } function clearSelectedDayFilter() { state.selectedDayMonthId = null; state.selectedDayCalendarId = null; state.selectedDayEntries = []; } function toggleDayFilterEntry(month, dayNumber, gregorianIso) { ensureDayFilterContext(month); const next = normalizeDayFilterEntry(dayNumber, gregorianIso); if (!next) { return; } const entries = state.selectedDayEntries; const existingIndex = entries.findIndex((entry) => entry.dayNumber === next.dayNumber); if (existingIndex >= 0) { entries.splice(existingIndex, 1); } else { entries.push(next); } state.selectedDayEntries = sortDayFilterEntries(entries); } function toggleDayRangeFilter(month, startDay, endDay) { ensureDayFilterContext(month); const start = Math.trunc(Number(startDay)); const end = Math.trunc(Number(endDay)); if (!Number.isFinite(start) || !Number.isFinite(end) || start <= 0 || end <= 0) { return; } const minDay = Math.min(start, end); const maxDay = Math.max(start, end); const rows = getMonthDayLinkRows(month) .filter((row) => row.isResolved && row.day >= minDay && row.day <= maxDay) .map((row) => normalizeDayFilterEntry(row.day, row.gregorianDate)) .filter(Boolean); if (!rows.length) { return; } const existingSet = new Set(state.selectedDayEntries.map((entry) => entry.dayNumber)); const allSelected = rows.every((row) => existingSet.has(row.dayNumber)); if (allSelected) { const removeSet = new Set(rows.map((row) => row.dayNumber)); state.selectedDayEntries = state.selectedDayEntries.filter((entry) => !removeSet.has(entry.dayNumber)); return; } rows.forEach((row) => { if (!existingSet.has(row.dayNumber)) { state.selectedDayEntries.push(row); } }); state.selectedDayEntries = sortDayFilterEntries(state.selectedDayEntries); } function getSelectedDayFilterContext(month) { if (!month) { return null; } if (state.selectedDayMonthId !== month.id) { return null; } if (state.selectedDayCalendarId !== state.selectedCalendar) { return null; } if (!Array.isArray(state.selectedDayEntries) || !state.selectedDayEntries.length) { return null; } const entries = state.selectedDayEntries .map((entry) => { const normalized = normalizeDayFilterEntry(entry.dayNumber, entry.gregorianIso); if (!normalized) { return null; } return { ...normalized, gregorianDate: isoToDateAtNoon(normalized.gregorianIso) }; }) .filter(Boolean); if (!entries.length) { return null; } return { entries, dayNumbers: new Set(entries.map((entry) => entry.dayNumber)) }; } 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) { 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 findGodIdByName(name) { if (!name || !state.godsById) return null; const normalized = String(name).trim().toLowerCase().replace(/^the\s+/, ""); for (const [id, god] of state.godsById) { const godName = String(god.name || "").trim().toLowerCase().replace(/^the\s+/, ""); if (godName === normalized || id.toLowerCase() === normalized) return id; } return null; } 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 sortMonths(months) { return [...months].sort((left, right) => Number(left?.order || 0) - Number(right?.order || 0)); } function getSelectedMonth() { return state.months.find((month) => month.id === state.selectedMonthId) || null; } function getDaysInMonth(year, monthOrder) { if (!Number.isFinite(year) || !Number.isFinite(monthOrder)) { return null; } return new Date(year, monthOrder, 0).getDate(); } function getMonthStartWeekday(year, monthOrder) { const date = new Date(year, monthOrder - 1, 1); return date.toLocaleDateString(undefined, { weekday: "long" }); } function parseMonthRange(month) { const startText = normalizeText(month?.start); const endText = normalizeText(month?.end); if (!startText || !endText) { return "--"; } return `${startText} to ${endText}`; } 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 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 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 getGregorianMonthStartDate(monthOrder, year = state.selectedYear) { if (!Number.isFinite(monthOrder) || !Number.isFinite(year)) { return null; } return new Date(Math.trunc(year), Math.trunc(monthOrder) - 1, 1, 12, 0, 0, 0); } function getHebrewMonthAliases(month) { const aliases = []; const idAliases = HEBREW_MONTH_ALIAS_BY_ID[String(month?.id || "").toLowerCase()] || []; aliases.push(...idAliases); const nameAlias = normalizeCalendarText(month?.name); if (nameAlias) { aliases.push(nameAlias); } return Array.from(new Set(aliases.map((alias) => normalizeCalendarText(alias)).filter(Boolean))); } function findHebrewMonthStartInGregorianYear(month, year) { const aliases = getHebrewMonthAliases(month); if (!aliases.length || !Number.isFinite(year)) { return null; } 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 day = readNumericPart(parts, "day"); const monthName = normalizeCalendarText(parts.find((part) => part.type === "month")?.value); if (day === 1 && aliases.includes(monthName)) { return new Date(cursor); } cursor.setDate(cursor.getDate() + 1); } return null; } function findIslamicMonthStartInGregorianYear(month, year) { const targetOrder = Number(month?.order); if (!Number.isFinite(targetOrder) || !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 day = readNumericPart(parts, "day"); const monthNo = readNumericPart(parts, "month"); if (day === 1 && monthNo === Math.trunc(targetOrder)) { return new Date(cursor); } cursor.setDate(cursor.getDate() + 1); } 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 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 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 findWheelMonthStartInGregorianYear(month, year) { const parsed = parseFirstMonthDayFromText(month?.date); if (!parsed || !Number.isFinite(year)) { return null; } return new Date(Math.trunc(year), parsed.monthIndex, parsed.day, 12, 0, 0, 0); } function getGregorianReferenceDateForCalendarMonth(month) { const calId = state.selectedCalendar; if (calId === "gregorian") { return getGregorianMonthStartDate(Number(month?.order)); } if (calId === "hebrew") { return findHebrewMonthStartInGregorianYear(month, state.selectedYear); } if (calId === "islamic") { return findIslamicMonthStartInGregorianYear(month, state.selectedYear); } if (calId === "wheel-of-year") { return findWheelMonthStartInGregorianYear(month, state.selectedYear); } return null; } function getMonthSubtitle(month) { const calId = state.selectedCalendar; if (calId === "hebrew" || calId === "islamic") { const native = month.nativeName ? ` · ${month.nativeName}` : ""; const days = month.days ? ` · ${month.days} days` : ""; return `${month.season || ""}${native}${days}`; } if (calId === "wheel-of-year") { return [month.date, month.type, month.season].filter(Boolean).join(" · "); } return parseMonthRange(month); } function formatIsoDate(date) { if (!(date instanceof Date) || Number.isNaN(date.getTime())) { return ""; } const year = date.getFullYear(); const month = `${date.getMonth() + 1}`.padStart(2, "0"); const day = `${date.getDate()}`.padStart(2, "0"); return `${year}-${month}-${day}`; } function resolveCalendarDayToGregorian(month, dayNumber) { const calId = state.selectedCalendar; const day = Math.trunc(Number(dayNumber)); if (!Number.isFinite(day) || day <= 0) { return null; } if (calId === "gregorian") { const monthOrder = Number(month?.order); if (!Number.isFinite(monthOrder)) { return null; } return new Date(state.selectedYear, monthOrder - 1, day, 12, 0, 0, 0); } if (calId === "hebrew") { return findHebrewMonthDayInGregorianYear(month?.id, day, state.selectedYear); } if (calId === "islamic") { return findIslamicMonthDayInGregorianYear(month?.id, day, state.selectedYear); } return null; } 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 renderDayLinksCard(month) { const rows = getMonthDayLinkRows(month); if (!rows.length) { return ""; } const selectedContext = 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 `${row.day}`; } const isSelected = selectedDaySet.has(Number(row.day)); return ``; }).join(""); const clearButton = selectedContext ? `` : ""; const helperText = selectedContext ? `
Filtered to days: ${selectedSummary}
` : ""; return `
Day Links
Filter this month to events, holidays, and data connected to a specific day.
${helperText}
${links}
${clearButton ? `
${clearButton}
` : ""}
`; } function renderList(elements) { const { monthListEl, monthCountEl, listTitleEl } = elements; if (!monthListEl) { return; } monthListEl.innerHTML = ""; state.filteredMonths.forEach((month) => { const isSelected = month.id === state.selectedMonthId; 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.monthId = month.id; itemEl.innerHTML = `
${month.name || month.id}
${getMonthSubtitle(month)}
`; itemEl.addEventListener("click", () => { selectByMonthId(month.id, elements); }); monthListEl.appendChild(itemEl); }); if (monthCountEl) { monthCountEl.textContent = state.searchQuery ? `${state.filteredMonths.length} of ${state.months.length} months` : `${state.months.length} months`; } if (listTitleEl) { listTitleEl.textContent = "Calendar > Months"; } } 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 "
--
"; } const buttons = []; if (associations.planetId) { buttons.push( `` ); } if (associations.zodiacSignId) { buttons.push( `` ); } 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( `` ); } } } 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( `` ); } if (associations.godId || associations.godName) { const label = godLabel(associations.godId, associations.godName); buttons.push( `` ); } if (associations.hebrewLetterId) { buttons.push( `` ); } if (associations.kabbalahPathNumber != null) { buttons.push( `` ); } if (associations.iChingPlanetaryInfluence) { buttons.push( `` ); } if (!buttons.length) { return "
--
"; } return `
${buttons.join("")}
`; } 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 eventSearchText(event) { return normalizeSearchValue([ event?.name, event?.date, event?.dateRange, event?.description, associationSearchText(event?.associations) ].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 buildHolidayList(month) { const calendarId = state.selectedCalendar; const monthOrder = Number(month?.order); const fromRepo = state.calendarHolidays.filter((holiday) => { const holidayCalendarId = String(holiday?.calendarId || "").trim().toLowerCase(); if (holidayCalendarId !== calendarId) { return false; } const isDirectMonthMatch = String(holiday?.monthId || "").trim().toLowerCase() === String(month?.id || "").trim().toLowerCase(); if (isDirectMonthMatch) { return true; } // For movable Gregorian holidays, place the holiday under the computed month for the selected year. 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 String(left?.name || "").localeCompare(String(right?.name || "")); }); } // Legacy fallback for old Gregorian-only holiday structure. 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)) { return; } seen.add(holiday.id); ordered.push(holiday); }); state.holidays.forEach((holiday) => { if (holiday?.monthId !== month.id || seen.has(holiday.id)) { return; } seen.add(holiday.id); ordered.push(holiday); }); return ordered; } function buildMonthSearchText(month) { const calId = state.selectedCalendar; const monthHolidays = buildHolidayList(month); const holidayText = monthHolidays.map((holiday) => holidaySearchText(holiday)).join(" "); if (calId === "gregorian") { const events = Array.isArray(month?.events) ? month.events : []; const searchable = [ month?.name, month?.id, month?.start, month?.end, month?.coreTheme, month?.seasonNorth, month?.seasonSouth, associationSearchText(month?.associations), events.map((event) => eventSearchText(event)).join(" "), holidayText ]; return normalizeSearchValue(searchable.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(" ") : ""; const searchable = [ 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 ]; return normalizeSearchValue(searchable.filter(Boolean).join(" ")); } function matchesSearch(searchText) { if (!state.searchQuery) { return true; } return searchText.includes(state.searchQuery); } function syncSearchControls(elements) { if (elements.searchInputEl) { elements.searchInputEl.value = state.searchQuery; } if (elements.searchClearEl) { elements.searchClearEl.disabled = !state.searchQuery; } } function applySearchFilter(elements) { state.filteredMonths = state.searchQuery ? state.months.filter((month) => matchesSearch(buildMonthSearchText(month))) : [...state.months]; if (!state.filteredMonths.some((month) => month.id === state.selectedMonthId)) { state.selectedMonthId = state.filteredMonths[0]?.id || null; } syncSearchControls(elements); renderList(elements); renderDetail(elements); } function renderFactsCard(month) { const monthOrder = Number(month?.order); const daysInMonth = getDaysInMonth(state.selectedYear, monthOrder); const hoursInMonth = Number.isFinite(daysInMonth) ? daysInMonth * 24 : null; const firstWeekday = Number.isFinite(monthOrder) ? getMonthStartWeekday(state.selectedYear, monthOrder) : "--"; const gregorianStartDate = getGregorianMonthStartDate(monthOrder); const hebrewStartReference = formatCalendarDateFromGregorian(gregorianStartDate, "hebrew"); const islamicStartReference = formatCalendarDateFromGregorian(gregorianStartDate, "islamic"); return `
Month Facts
Year
${state.selectedYear}
Start Date (Gregorian)
${formatGregorianReferenceDate(gregorianStartDate)}
Days
${daysInMonth ?? "--"}
Hours
${hoursInMonth ?? "--"}
Starts On
${firstWeekday}
Hebrew On 1st
${hebrewStartReference}
Islamic On 1st
${islamicStartReference}
North Season
${month.seasonNorth || "--"}
South Season
${month.seasonSouth || "--"}
`; } function renderAssociationsCard(month) { const monthOrder = Number(month?.order); const associations = { ...(month?.associations || {}), ...(Number.isFinite(monthOrder) ? { numberValue: Math.trunc(monthOrder) } : {}) }; return `
Associations
${month.coreTheme || "--"}
${buildAssociationButtons(associations)}
`; } function renderEventsCard(month) { const allEvents = Array.isArray(month?.events) ? month.events : []; if (!allEvents.length) { return `
Monthly Events
No monthly events listed.
`; } const selectedDay = getSelectedDayFilterContext(month); function eventMatchesDay(event) { if (!selectedDay) { return true; } return selectedDay.entries.some((entry) => { const targetDate = entry.gregorianDate; const targetMonth = targetDate?.getMonth() + 1; const targetDayNo = targetDate?.getDate(); const explicitDate = parseMonthDayToken(event?.date); if (explicitDate && Number.isFinite(targetMonth) && Number.isFinite(targetDayNo)) { return explicitDate.month === targetMonth && explicitDate.day === targetDayNo; } const rangeTokens = parseMonthDayTokensFromText(event?.dateRange || event?.dateText || ""); if (rangeTokens.length >= 2 && Number.isFinite(targetMonth) && Number.isFinite(targetDayNo)) { const start = rangeTokens[0]; const end = rangeTokens[1]; return isMonthDayInRange(targetMonth, targetDayNo, start.month, start.day, end.month, end.day); } const dayRange = parseDayRangeFromText(event?.date || event?.dateRange || event?.dateText || ""); if (dayRange) { return entry.dayNumber >= dayRange.startDay && entry.dayNumber <= dayRange.endDay; } return false; }); } const dayFiltered = allEvents.filter((event) => eventMatchesDay(event)); const events = state.searchQuery ? dayFiltered.filter((event) => matchesSearch(eventSearchText(event))) : dayFiltered; if (!events.length) { return `
Monthly Events
No monthly events match current search.
`; } const rows = events.map((event) => { const dateText = event?.date || event?.dateRange || "--"; return `
${event?.name || "Untitled"} ${dateText}
${event?.description || ""}
${buildAssociationButtons(event?.associations)}
`; }).join(""); return `
Monthly Events
${rows}
`; } function renderHolidaysCard(month, title = "Holiday Repository") { const allHolidays = buildHolidayList(month); if (!allHolidays.length) { return `
${title}
No holidays listed in the repository for this month.
`; } const selectedDay = getSelectedDayFilterContext(month); function holidayMatchesDay(holiday) { if (!selectedDay) { return true; } return selectedDay.entries.some((entry) => { const targetDate = entry.gregorianDate; const targetMonth = targetDate?.getMonth() + 1; const targetDayNo = targetDate?.getDate(); const exactResolved = resolveHolidayGregorianDate(holiday); if (exactResolved instanceof Date && !Number.isNaN(exactResolved.getTime()) && targetDate instanceof Date) { return formatIsoDate(exactResolved) === formatIsoDate(targetDate); } if (state.selectedCalendar === "gregorian" && Number.isFinite(targetMonth) && Number.isFinite(targetDayNo)) { const tokens = parseMonthDayTokensFromText(holiday?.dateText || holiday?.dateRange || ""); if (tokens.length >= 2) { const start = tokens[0]; const end = tokens[1]; return isMonthDayInRange(targetMonth, targetDayNo, start.month, start.day, end.month, end.day); } if (tokens.length === 1) { const single = tokens[0]; return single.month === targetMonth && single.day === targetDayNo; } const direct = parseMonthDayStartToken(holiday?.monthDayStart || holiday?.dateText || ""); if (direct) { return direct.month === targetMonth && direct.day === targetDayNo; } if (Number.isFinite(Number(holiday?.day))) { return Number(holiday.day) === entry.dayNumber; } } const localRange = parseDayRangeFromText(holiday?.dateText || holiday?.dateRange || ""); if (localRange) { return entry.dayNumber >= localRange.startDay && entry.dayNumber <= localRange.endDay; } if (Number.isFinite(Number(holiday?.day))) { return Number(holiday.day) === entry.dayNumber; } return false; }); } const dayFiltered = allHolidays.filter((holiday) => holidayMatchesDay(holiday)); const holidays = state.searchQuery ? dayFiltered.filter((holiday) => matchesSearch(holidaySearchText(holiday))) : dayFiltered; if (!holidays.length) { return `
${title}
No holidays match current search.
`; } const rows = holidays.map((holiday) => { const dateText = holiday?.dateText || holiday?.date || holiday?.dateRange || "--"; const gregorianDate = resolveHolidayGregorianDate(holiday); const gregorianRef = formatGregorianReferenceDate(gregorianDate); const hebrewRef = formatCalendarDateFromGregorian(gregorianDate, "hebrew"); const islamicRef = formatCalendarDateFromGregorian(gregorianDate, "islamic"); const conversionConfidence = String(holiday?.conversionConfidence || holiday?.datePrecision || "approximate").toLowerCase(); const conversionLabel = (!(gregorianDate instanceof Date) || Number.isNaN(gregorianDate.getTime())) ? "Conversion: unresolved" : (conversionConfidence === "exact" ? "Conversion: exact" : "Conversion: approximate"); return `
${holiday?.name || "Untitled"} ${dateText}
${cap(holiday?.kind || holiday?.calendarId || "observance")}
${conversionLabel}
Gregorian: ${gregorianRef}
Hebrew: ${hebrewRef}
Islamic: ${islamicRef}
${holiday?.description || ""}
${buildAssociationButtons(holiday?.associations)}
`; }).join(""); return `
${title}
${rows}
`; } function findSignIdByAstrologyName(name) { const token = normalizeCalendarText(name); if (!token) { return null; } for (const [signId, sign] of state.signsById) { const idToken = normalizeCalendarText(signId); const nameToken = normalizeCalendarText(sign?.name?.en || sign?.name || ""); if (token === idToken || token === nameToken) { return signId; } } return null; } function intersectDateRanges(startA, endA, startB, endB) { const start = startA.getTime() > startB.getTime() ? startA : startB; const end = endA.getTime() < endB.getTime() ? endA : endB; return start.getTime() <= end.getTime() ? { start, end } : null; } function buildMajorArcanaRowsForMonth(month) { if (state.selectedCalendar !== "gregorian") { return []; } const monthOrder = Number(month?.order); if (!Number.isFinite(monthOrder)) { return []; } const monthStart = new Date(state.selectedYear, monthOrder - 1, 1, 12, 0, 0, 0); const monthEnd = new Date(state.selectedYear, monthOrder, 0, 12, 0, 0, 0); const rows = []; state.hebrewById.forEach((letter) => { const astrologyType = normalizeCalendarText(letter?.astrology?.type); if (astrologyType !== "zodiac") { return; } const signId = findSignIdByAstrologyName(letter?.astrology?.name); const sign = signId ? state.signsById.get(signId) : null; if (!sign) { return; } const startToken = parseMonthDayToken(sign?.start); const endToken = parseMonthDayToken(sign?.end); if (!startToken || !endToken) { return; } const spanStart = new Date(state.selectedYear, startToken.month - 1, startToken.day, 12, 0, 0, 0); const spanEnd = new Date(state.selectedYear, endToken.month - 1, endToken.day, 12, 0, 0, 0); const wraps = spanEnd.getTime() < spanStart.getTime(); const segments = wraps ? [ { start: spanStart, end: new Date(state.selectedYear, 11, 31, 12, 0, 0, 0) }, { start: new Date(state.selectedYear, 0, 1, 12, 0, 0, 0), end: spanEnd } ] : [{ start: spanStart, end: spanEnd }]; segments.forEach((segment) => { const overlap = 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 = 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 `
Major Arcana Windows
No major arcana windows for this month.
`; } const list = rows.map((row) => { const hebrewLabel = row.hebrewLetterId ? `${row.hebrewLetterChar ? `${row.hebrewLetterChar} ` : ""}${row.hebrewLetterName || row.hebrewLetterId}` : "--"; const displayCardName = getDisplayTarotName(row.cardName, row.trumpNumber); return `
${displayCardName}${row.trumpNumber != null ? ` · Trump ${row.trumpNumber}` : ""} ${row.rangeLabel}
${row.signSymbol} ${row.signName} · Hebrew: ${hebrewLabel}
${row.hebrewLetterId ? `` : ""}
`; }).join(""); return `
Major Arcana Windows
${list}
`; } function renderDecanTarotCard(month) { const selectedDay = getSelectedDayFilterContext(month); const allRows = 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 isMonthDayInRange( targetMonth, targetDayNo, row.startMonth, row.startDay, row.endMonth, row.endDay ); })) : allRows; if (!rows.length) { return `
Decan Tarot Windows
No decan tarot windows for this month.
`; } const list = rows.map((row) => { const displayCardName = getDisplayTarotName(row.cardName); return `
${row.signSymbol} ${row.signName} · Decan ${row.decanIndex} ${row.startDegree}°–${row.endDegree}° · ${row.dateRange}
`; }).join(""); return `
Decan Tarot Windows
${list}
`; } function attachNavHandlers(detailBodyEl) { if (!detailBodyEl) { return; } detailBodyEl.querySelectorAll("[data-nav]").forEach((button) => { button.addEventListener("click", () => { const navType = button.dataset.nav; if (navType === "planet" && button.dataset.planetId) { document.dispatchEvent(new CustomEvent("nav:planet", { detail: { planetId: button.dataset.planetId } })); return; } if (navType === "zodiac" && button.dataset.signId) { document.dispatchEvent(new CustomEvent("nav:zodiac", { detail: { signId: button.dataset.signId } })); return; } if (navType === "number" && button.dataset.numberValue) { document.dispatchEvent(new CustomEvent("nav:number", { detail: { value: Number(button.dataset.numberValue) } })); return; } if (navType === "tarot-card" && button.dataset.cardName) { const trumpNumber = Number(button.dataset.trumpNumber); document.dispatchEvent(new CustomEvent("nav:tarot-trump", { detail: { cardName: button.dataset.cardName, trumpNumber: Number.isFinite(trumpNumber) ? trumpNumber : undefined } })); return; } if (navType === "god") { document.dispatchEvent(new CustomEvent("nav:gods", { detail: { godId: button.dataset.godId || undefined, godName: button.dataset.godName || undefined } })); return; } if (navType === "alphabet" && button.dataset.hebrewLetterId) { document.dispatchEvent(new CustomEvent("nav:alphabet", { detail: { alphabet: "hebrew", hebrewLetterId: button.dataset.hebrewLetterId } })); return; } if (navType === "kabbalah" && button.dataset.pathNo) { document.dispatchEvent(new CustomEvent("nav:kabbalah-path", { detail: { pathNo: Number(button.dataset.pathNo) } })); return; } if (navType === "iching" && button.dataset.planetaryInfluence) { document.dispatchEvent(new CustomEvent("nav:iching", { detail: { planetaryInfluence: button.dataset.planetaryInfluence } })); return; } if (navType === "calendar-month" && button.dataset.monthId) { document.dispatchEvent(new CustomEvent("nav:calendar-month", { detail: { calendarId: button.dataset.calendarId || undefined, monthId: button.dataset.monthId } })); return; } if (navType === "calendar-day" && button.dataset.dayNumber) { const month = getSelectedMonth(); const dayNumber = Number(button.dataset.dayNumber); if (!month || !Number.isFinite(dayNumber)) { return; } toggleDayFilterEntry(month, dayNumber, button.dataset.gregorianDate); renderDetail(getElements()); return; } if (navType === "calendar-day-range" && button.dataset.rangeStart && button.dataset.rangeEnd) { const month = getSelectedMonth(); if (!month) { return; } toggleDayRangeFilter(month, Number(button.dataset.rangeStart), Number(button.dataset.rangeEnd)); renderDetail(getElements()); return; } if (navType === "calendar-day-clear") { clearSelectedDayFilter(); renderDetail(getElements()); } }); }); } function renderHebrewMonthDetail(month) { const gregorianStartDate = getGregorianReferenceDateForCalendarMonth(month); const factsRows = [ ["Hebrew Name", month.nativeName || "--"], ["Month Order", month.leapYearOnly ? `${month.order} (leap year only)` : String(month.order)], ["Gregorian Reference Year", String(state.selectedYear)], ["Month Start (Gregorian)", formatGregorianReferenceDate(gregorianStartDate)], ["Days", month.daysVariant ? `${month.days}–${month.daysVariant} (varies)` : String(month.days || "--")], ["Season", month.season || "--"], ["Zodiac Sign", cap(month.zodiacSign) || "--"], ["Tribe of Israel", month.tribe || "--"], ["Sense", month.sense || "--"], ["Hebrew Letter", month.hebrewLetter || "--"] ].map(([dt, dd]) => `
${dt}
${dd}
`).join(""); const monthOrder = Number(month?.order); const navButtons = buildAssociationButtons({ ...(month?.associations || {}), ...(Number.isFinite(monthOrder) ? { numberValue: Math.trunc(monthOrder) } : {}) }); const connectionsCard = navButtons ? `
Connections${navButtons}
` : ""; return `
Month Facts
${factsRows}
${connectionsCard}
About ${month.name}
${month.description || "--"}
${renderDayLinksCard(month)} ${renderHolidaysCard(month, "Holiday Repository")}
`; } function renderIslamicMonthDetail(month) { const gregorianStartDate = getGregorianReferenceDateForCalendarMonth(month); const factsRows = [ ["Arabic Name", month.nativeName || "--"], ["Month Order", String(month.order)], ["Gregorian Reference Year", String(state.selectedYear)], ["Month Start (Gregorian)", 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}
${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 ? `
Connections${navButtons}
` : ""; return `
Month Facts
${factsRows}
${connectionsCard}
About ${month.name}
${month.description || "--"}
${renderDayLinksCard(month)} ${renderHolidaysCard(month, "Holiday Repository")}
`; } function buildWheelDeityButtons(deities) { const buttons = []; (Array.isArray(deities) ? deities : []).forEach((rawName) => { // Strip qualifiers like "(early)" or "/ Father Christmas" before matching const cleanName = String(rawName || "").replace(/\s*\/.*$/, "").replace(/\s*\(.*\)$/, "").trim(); const godId = findGodIdByName(cleanName) || findGodIdByName(rawName); if (!godId) return; const god = state.godsById.get(godId); const label = god?.name || cleanName; buttons.push(``); }); return buttons; } function renderWheelMonthDetail(month) { const gregorianStartDate = 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", cap(month.type) || "--"], ["Gregorian Reference Year", String(state.selectedYear)], ["Start (Gregorian)", formatGregorianReferenceDate(gregorianStartDate)], ["Season", month.season || "--"], ["Element", cap(month.element) || "--"], ["Direction", assoc?.direction || "--"] ].map(([dt, dd]) => `
${dt}
${dd}
`).join(""); const assocRows = [ ["Themes", themes], ["Deities", deities], ["Colors", colors], ["Herbs", herbs] ].map(([dt, dd]) => `
${dt}
${dd}
`).join(""); const deityButtons = buildWheelDeityButtons(assoc?.deities); const deityLinksCard = deityButtons.length ? `
Linked Deities
${deityButtons.join("")}
` : ""; const monthOrder = Number(month?.order); const hasNumberLink = Number.isFinite(monthOrder) && monthOrder >= 0; const numberButtons = hasNumberLink ? buildAssociationButtons({ numberValue: Math.trunc(monthOrder) }) : ""; const numberLinksCard = hasNumberLink ? `
Connections${numberButtons}
` : ""; return `
Sabbat Facts
${factsRows}
About ${month.name}
${month.description || "--"}
Associations
${assocRows}
${renderDayLinksCard(month)} ${numberLinksCard} ${deityLinksCard} ${renderHolidaysCard(month, "Holiday Repository")}
`; } function renderDetail(elements) { const { detailNameEl, detailSubEl, detailBodyEl } = elements; if (!detailBodyEl || !detailNameEl || !detailSubEl) { return; } const month = getSelectedMonth(); if (!month) { detailNameEl.textContent = "--"; detailSubEl.textContent = "Select a month to explore"; detailBodyEl.innerHTML = ""; return; } detailNameEl.textContent = month.name || month.id; const calId = state.selectedCalendar; if (calId === "gregorian") { detailSubEl.textContent = `${parseMonthRange(month)} · ${month.coreTheme || "Month correspondences"}`; detailBodyEl.innerHTML = `
${renderFactsCard(month)} ${renderDayLinksCard(month)} ${renderAssociationsCard(month)} ${renderMajorArcanaCard(month)} ${renderDecanTarotCard(month)} ${renderEventsCard(month)} ${renderHolidaysCard(month, "Holiday Repository")}
`; } else if (calId === "hebrew") { detailSubEl.textContent = getMonthSubtitle(month); detailBodyEl.innerHTML = renderHebrewMonthDetail(month); } else if (calId === "islamic") { detailSubEl.textContent = getMonthSubtitle(month); detailBodyEl.innerHTML = renderIslamicMonthDetail(month); } else { detailSubEl.textContent = getMonthSubtitle(month); detailBodyEl.innerHTML = renderWheelMonthDetail(month); } attachNavHandlers(detailBodyEl); } function selectByMonthId(monthId, elements = getElements()) { const target = state.months.find((month) => month.id === monthId); if (!target) { return false; } if (state.searchQuery && !state.filteredMonths.some((month) => month.id === target.id)) { state.searchQuery = ""; state.filteredMonths = [...state.months]; } if (state.selectedMonthId !== target.id) { clearSelectedDayFilter(); } state.selectedMonthId = target.id; syncSearchControls(elements); renderList(elements); renderDetail(elements); return true; } function selectCalendarType(calendarId, elements = getElements()) { if (!state.calendarData || !Array.isArray(state.calendarData[calendarId])) { return false; } if (elements.calendarTypeEl) { elements.calendarTypeEl.value = calendarId; } loadCalendarType(calendarId, elements); return true; } function bindYearInput(elements) { if (!elements.yearInputEl) { return; } elements.yearInputEl.value = String(state.selectedYear); elements.yearInputEl.addEventListener("change", () => { const nextYear = Number(elements.yearInputEl.value); if (!Number.isFinite(nextYear)) { elements.yearInputEl.value = String(state.selectedYear); return; } state.selectedYear = Math.min(2500, Math.max(1900, Math.round(nextYear))); elements.yearInputEl.value = String(state.selectedYear); state.dayLinksCache = new Map(); clearSelectedDayFilter(); renderDetail(elements); }); } function bindSearchInput(elements) { if (elements.searchInputEl) { elements.searchInputEl.addEventListener("input", () => { state.searchQuery = normalizeSearchValue(elements.searchInputEl.value); applySearchFilter(elements); }); } if (elements.searchClearEl && elements.searchInputEl) { elements.searchClearEl.addEventListener("click", () => { state.searchQuery = ""; elements.searchInputEl.value = ""; applySearchFilter(elements); elements.searchInputEl.focus(); }); } } function loadCalendarType(calendarId, elements) { const months = state.calendarData[calendarId]; if (!Array.isArray(months)) { return; } state.selectedCalendar = calendarId; state.dayLinksCache = new Map(); clearSelectedDayFilter(); state.months = sortMonths(months); state.filteredMonths = [...state.months]; state.selectedMonthId = state.months[0]?.id || null; state.searchQuery = ""; if (elements.calendarYearWrapEl) { elements.calendarYearWrapEl.hidden = false; } syncSearchControls(elements); applySearchFilter(elements); } function bindCalendarTypeSelect(elements) { if (!elements.calendarTypeEl) { return; } elements.calendarTypeEl.value = state.selectedCalendar; elements.calendarTypeEl.addEventListener("change", () => { const calId = String(elements.calendarTypeEl.value || "gregorian"); loadCalendarType(calId, elements); }); } function ensureCalendarSection(referenceData, magickDataset) { if (!referenceData) { return; } state.referenceData = referenceData; state.magickDataset = magickDataset || null; state.dayLinksCache = new Map(); clearSelectedDayFilter(); state.holidays = Array.isArray(referenceData.celestialHolidays) ? referenceData.celestialHolidays : []; state.calendarHolidays = Array.isArray(referenceData.calendarHolidays) ? referenceData.calendarHolidays : []; state.planetsById = buildPlanetMap(referenceData.planets); state.signsById = buildSignsMap(referenceData.signs); state.godsById = buildGodsMap(state.magickDataset); state.hebrewById = buildHebrewMap(state.magickDataset); state.calendarData = { 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 : [] }; const currentCalMonths = state.calendarData[state.selectedCalendar] || state.calendarData.gregorian || []; state.months = sortMonths(currentCalMonths); state.filteredMonths = [...state.months]; const elements = getElements(); if (elements.calendarYearWrapEl) { elements.calendarYearWrapEl.hidden = false; } if (!state.months.length) { if (elements.detailNameEl) { elements.detailNameEl.textContent = "Calendar"; } if (elements.detailSubEl) { elements.detailSubEl.textContent = "No month data available."; } if (elements.detailBodyEl) { elements.detailBodyEl.innerHTML = ""; } if (elements.monthListEl) { elements.monthListEl.innerHTML = ""; } return; } if (!state.selectedMonthId || !state.months.some((month) => month.id === state.selectedMonthId)) { state.selectedMonthId = state.months[0].id; } if (!state.initialized) { state.initialized = true; bindYearInput(elements); bindSearchInput(elements); bindCalendarTypeSelect(elements); } applySearchFilter(elements); } window.CalendarSectionUi = { ensureCalendarSection, selectByMonthId, selectCalendarType }; })();