Files
TaroTime/app/ui-calendar.js
2026-03-07 01:09:00 -08:00

2529 lines
79 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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
};
})();