Files
TaroTime/app/ui-calendar.js

2529 lines
79 KiB
JavaScript
Raw Normal View History

2026-03-07 01:09:00 -08:00
/* 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 `<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 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 = `
<div class="planet-list-name">${month.name || month.id}</div>
<div class="planet-list-meta">${getMonthSubtitle(month)}</div>
`;
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 "<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 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 `
<div class="planet-meta-card">
<strong>Month Facts</strong>
<div class="planet-text">
<dl class="alpha-dl">
<dt>Year</dt><dd>${state.selectedYear}</dd>
<dt>Start Date (Gregorian)</dt><dd>${formatGregorianReferenceDate(gregorianStartDate)}</dd>
<dt>Days</dt><dd>${daysInMonth ?? "--"}</dd>
<dt>Hours</dt><dd>${hoursInMonth ?? "--"}</dd>
<dt>Starts On</dt><dd>${firstWeekday}</dd>
<dt>Hebrew On 1st</dt><dd>${hebrewStartReference}</dd>
<dt>Islamic On 1st</dt><dd>${islamicStartReference}</dd>
<dt>North Season</dt><dd>${month.seasonNorth || "--"}</dd>
<dt>South Season</dt><dd>${month.seasonSouth || "--"}</dd>
</dl>
</div>
</div>
`;
}
function renderAssociationsCard(month) {
const monthOrder = Number(month?.order);
const associations = {
...(month?.associations || {}),
...(Number.isFinite(monthOrder) ? { numberValue: Math.trunc(monthOrder) } : {})
};
return `
<div class="planet-meta-card">
<strong>Associations</strong>
<div class="planet-text">${month.coreTheme || "--"}</div>
${buildAssociationButtons(associations)}
</div>
`;
}
function renderEventsCard(month) {
const allEvents = Array.isArray(month?.events) ? month.events : [];
if (!allEvents.length) {
return `
<div class="planet-meta-card">
<strong>Monthly Events</strong>
<div class="planet-text">No monthly events listed.</div>
</div>
`;
}
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 `
<div class="planet-meta-card">
<strong>Monthly Events</strong>
<div class="planet-text">No monthly events match current search.</div>
</div>
`;
}
const rows = events.map((event) => {
const dateText = event?.date || event?.dateRange || "--";
return `
<div class="cal-item-row">
<div class="cal-item-head">
<span class="cal-item-name">${event?.name || "Untitled"}</span>
<span class="planet-list-meta">${dateText}</span>
</div>
<div class="planet-text">${event?.description || ""}</div>
${buildAssociationButtons(event?.associations)}
</div>
`;
}).join("");
return `
<div class="planet-meta-card">
<strong>Monthly Events</strong>
<div class="cal-item-stack">${rows}</div>
</div>
`;
}
function renderHolidaysCard(month, title = "Holiday Repository") {
const allHolidays = buildHolidayList(month);
if (!allHolidays.length) {
return `
<div class="planet-meta-card">
<strong>${title}</strong>
<div class="planet-text">No holidays listed in the repository for this month.</div>
</div>
`;
}
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 `
<div class="planet-meta-card">
<strong>${title}</strong>
<div class="planet-text">No holidays match current search.</div>
</div>
`;
}
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 `
<div class="cal-item-row">
<div class="cal-item-head">
<span class="cal-item-name">${holiday?.name || "Untitled"}</span>
<span class="planet-list-meta">${dateText}</span>
</div>
<div class="planet-list-meta">${cap(holiday?.kind || holiday?.calendarId || "observance")}</div>
<div class="planet-list-meta">${conversionLabel}</div>
<div class="planet-text"><strong>Gregorian:</strong> ${gregorianRef}</div>
<div class="planet-text"><strong>Hebrew:</strong> ${hebrewRef}</div>
<div class="planet-text"><strong>Islamic:</strong> ${islamicRef}</div>
<div class="planet-text">${holiday?.description || ""}</div>
${buildAssociationButtons(holiday?.associations)}
</div>
`;
}).join("");
return `
<div class="planet-meta-card">
<strong>${title}</strong>
<div class="cal-item-stack">${rows}</div>
</div>
`;
}
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 `
<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 hebrewLabel = row.hebrewLetterId
? `${row.hebrewLetterChar ? `${row.hebrewLetterChar} ` : ""}${row.hebrewLetterName || row.hebrewLetterId}`
: "--";
const displayCardName = 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: ${hebrewLabel}</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}">${hebrewLabel} ↗</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 = 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 `
<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 = 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 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>${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 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>${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) => {
// 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(`<button class="alpha-nav-btn" data-nav="god" data-god-id="${godId}" data-god-name="${label}">${label} \u2197</button>`);
});
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>${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 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 = `
<div class="planet-meta-grid">
${renderFactsCard(month)}
${renderDayLinksCard(month)}
${renderAssociationsCard(month)}
${renderMajorArcanaCard(month)}
${renderDecanTarotCard(month)}
${renderEventsCard(month)}
${renderHolidaysCard(month, "Holiday Repository")}
</div>
`;
} 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
};
})();