3493 lines
103 KiB
JavaScript
3493 lines
103 KiB
JavaScript
|
|
const { getCenteredWeekStartDay, getDateKey, getMoonPhaseName } = window.TarotCalc;
|
||
|
|
const { loadReferenceData, loadMagickDataset } = window.TarotDataService;
|
||
|
|
const { buildWeekEvents } = window.TarotEventBuilder;
|
||
|
|
const { updateNowPanel } = window.TarotNowUi;
|
||
|
|
const { ensureTarotSection } = window.TarotSectionUi || {};
|
||
|
|
const { ensurePlanetSection } = window.PlanetSectionUi || {};
|
||
|
|
const { ensureCyclesSection } = window.CyclesSectionUi || {};
|
||
|
|
const { ensureElementsSection } = window.ElementsSectionUi || {};
|
||
|
|
const { ensureIChingSection } = window.IChingSectionUi || {};
|
||
|
|
const { ensureKabbalahSection } = window.KabbalahSectionUi || {};
|
||
|
|
const { ensureCubeSection } = window.CubeSectionUi || {};
|
||
|
|
const { ensureAlphabetSection } = window.AlphabetSectionUi || {};
|
||
|
|
const { ensureZodiacSection } = window.ZodiacSectionUi || {};
|
||
|
|
const { ensureQuizSection, registerQuizCategory } = window.QuizSectionUi || {};
|
||
|
|
const { ensureGodsSection } = window.GodsSectionUi || {};
|
||
|
|
const { ensureEnochianSection } = window.EnochianSectionUi || {};
|
||
|
|
const { ensureCalendarSection } = window.CalendarSectionUi || {};
|
||
|
|
const { ensureHolidaySection } = window.HolidaySectionUi || {};
|
||
|
|
const { ensureNatalPanel } = window.TarotNatalUi || {};
|
||
|
|
|
||
|
|
const statusEl = document.getElementById("status");
|
||
|
|
const monthStripEl = document.getElementById("month-strip");
|
||
|
|
const calendarEl = document.getElementById("calendar");
|
||
|
|
const calendarSectionEl = document.getElementById("calendar-section");
|
||
|
|
const holidaySectionEl = document.getElementById("holiday-section");
|
||
|
|
const tarotSectionEl = document.getElementById("tarot-section");
|
||
|
|
const astronomySectionEl = document.getElementById("astronomy-section");
|
||
|
|
const natalSectionEl = document.getElementById("natal-section");
|
||
|
|
const planetSectionEl = document.getElementById("planet-section");
|
||
|
|
const cyclesSectionEl = document.getElementById("cycles-section");
|
||
|
|
const elementsSectionEl = document.getElementById("elements-section");
|
||
|
|
const ichingSectionEl = document.getElementById("iching-section");
|
||
|
|
const kabbalahSectionEl = document.getElementById("kabbalah-section");
|
||
|
|
const kabbalahTreeSectionEl = document.getElementById("kabbalah-tree-section");
|
||
|
|
const cubeSectionEl = document.getElementById("cube-section");
|
||
|
|
const alphabetSectionEl = document.getElementById("alphabet-section");
|
||
|
|
const numbersSectionEl = document.getElementById("numbers-section");
|
||
|
|
const zodiacSectionEl = document.getElementById("zodiac-section");
|
||
|
|
const quizSectionEl = document.getElementById("quiz-section");
|
||
|
|
const godsSectionEl = document.getElementById("gods-section");
|
||
|
|
const enochianSectionEl = document.getElementById("enochian-section");
|
||
|
|
const openCalendarEl = document.getElementById("open-calendar");
|
||
|
|
const openCalendarMonthsEl = document.getElementById("open-calendar-months");
|
||
|
|
const openHolidaysEl = document.getElementById("open-holidays");
|
||
|
|
const openTarotEl = document.getElementById("open-tarot");
|
||
|
|
const openTarotCardsEl = document.getElementById("open-tarot-cards");
|
||
|
|
const openTarotSpreadEl = document.getElementById("open-tarot-spread");
|
||
|
|
const openAstronomyEl = document.getElementById("open-astronomy");
|
||
|
|
const openPlanetsEl = document.getElementById("open-planets");
|
||
|
|
const openCyclesEl = document.getElementById("open-cycles");
|
||
|
|
const openElementsEl = document.getElementById("open-elements");
|
||
|
|
const openIChingEl = document.getElementById("open-iching");
|
||
|
|
const openKabbalahEl = document.getElementById("open-kabbalah");
|
||
|
|
const openKabbalahTreeEl = document.getElementById("open-kabbalah-tree");
|
||
|
|
const openKabbalahCubeEl = document.getElementById("open-kabbalah-cube");
|
||
|
|
const openAlphabetEl = document.getElementById("open-alphabet");
|
||
|
|
const openNumbersEl = document.getElementById("open-numbers");
|
||
|
|
const openZodiacEl = document.getElementById("open-zodiac");
|
||
|
|
const openNatalEl = document.getElementById("open-natal");
|
||
|
|
const openQuizEl = document.getElementById("open-quiz");
|
||
|
|
const openGodsEl = document.getElementById("open-gods");
|
||
|
|
const openEnochianEl = document.getElementById("open-enochian");
|
||
|
|
const openSettingsEl = document.getElementById("open-settings");
|
||
|
|
const closeSettingsEl = document.getElementById("close-settings");
|
||
|
|
const settingsPopupEl = document.getElementById("settings-popup");
|
||
|
|
const settingsPopupCardEl = document.getElementById("settings-popup-card");
|
||
|
|
const topbarDropdownEls = Array.from(document.querySelectorAll(".topbar-dropdown"));
|
||
|
|
const latEl = document.getElementById("lat");
|
||
|
|
const lngEl = document.getElementById("lng");
|
||
|
|
const timeFormatEl = document.getElementById("time-format");
|
||
|
|
const birthDateEl = document.getElementById("birth-date");
|
||
|
|
const tarotDeckEl = document.getElementById("tarot-deck");
|
||
|
|
const saveSettingsEl = document.getElementById("save-settings");
|
||
|
|
const useLocationEl = document.getElementById("use-location");
|
||
|
|
const nowSkyLayerEl = document.getElementById("now-sky-layer");
|
||
|
|
const nowPanelEl = document.getElementById("now-panel");
|
||
|
|
const tarotBrowseViewEl = document.getElementById("tarot-browse-view");
|
||
|
|
const tarotSpreadViewEl = document.getElementById("tarot-spread-view");
|
||
|
|
const tarotSpreadBackEl = document.getElementById("tarot-spread-back");
|
||
|
|
const tarotSpreadBtnThreeEl = document.getElementById("tarot-spread-btn-three");
|
||
|
|
const tarotSpreadBtnCelticEl = document.getElementById("tarot-spread-btn-celtic");
|
||
|
|
const tarotSpreadRedrawEl = document.getElementById("tarot-spread-redraw");
|
||
|
|
const tarotSpreadMeaningsEl = document.getElementById("tarot-spread-meanings");
|
||
|
|
const tarotSpreadBoardEl = document.getElementById("tarot-spread-board");
|
||
|
|
const numbersCountEl = document.getElementById("numbers-count");
|
||
|
|
const numbersListEl = document.getElementById("numbers-list");
|
||
|
|
const numbersDetailNameEl = document.getElementById("numbers-detail-name");
|
||
|
|
const numbersDetailTypeEl = document.getElementById("numbers-detail-type");
|
||
|
|
const numbersDetailSummaryEl = document.getElementById("numbers-detail-summary");
|
||
|
|
const numbersDetailBodyEl = document.getElementById("numbers-detail-body");
|
||
|
|
const numbersSpecialPanelEl = document.getElementById("numbers-special-panel");
|
||
|
|
|
||
|
|
const nowElements = {
|
||
|
|
nowHourEl: document.getElementById("now-hour"),
|
||
|
|
nowHourTarotEl: document.getElementById("now-hour-tarot"),
|
||
|
|
nowCountdownEl: document.getElementById("now-countdown"),
|
||
|
|
nowHourNextEl: document.getElementById("now-hour-next"),
|
||
|
|
nowHourCardEl: document.getElementById("now-hour-card"),
|
||
|
|
nowMoonEl: document.getElementById("now-moon"),
|
||
|
|
nowMoonTarotEl: document.getElementById("now-moon-tarot"),
|
||
|
|
nowMoonCountdownEl: document.getElementById("now-moon-countdown"),
|
||
|
|
nowMoonNextEl: document.getElementById("now-moon-next"),
|
||
|
|
nowMoonCardEl: document.getElementById("now-moon-card"),
|
||
|
|
nowDecanEl: document.getElementById("now-decan"),
|
||
|
|
nowDecanTarotEl: document.getElementById("now-decan-tarot"),
|
||
|
|
nowDecanCountdownEl: document.getElementById("now-decan-countdown"),
|
||
|
|
nowDecanNextEl: document.getElementById("now-decan-next"),
|
||
|
|
nowDecanCardEl: document.getElementById("now-decan-card"),
|
||
|
|
nowStatsSabianEl: document.getElementById("now-stats-sabian"),
|
||
|
|
nowStatsPlanetsEl: document.getElementById("now-stats-planets")
|
||
|
|
};
|
||
|
|
|
||
|
|
const baseWeekOptions = {
|
||
|
|
hourStart: 0,
|
||
|
|
hourEnd: 24,
|
||
|
|
eventView: ["allday", "time"],
|
||
|
|
taskView: false
|
||
|
|
};
|
||
|
|
|
||
|
|
const PLANET_CALENDAR_ORDER = ["saturn", "jupiter", "mars", "sol", "venus", "mercury", "luna"];
|
||
|
|
const SETTINGS_STORAGE_KEY = "tarot-time-settings-v1";
|
||
|
|
const DEFAULT_TAROT_DECK = "ceremonial-magick";
|
||
|
|
const SIDEBAR_COLLAPSE_STORAGE_PREFIX = "tarot-sidebar-collapsed:";
|
||
|
|
const DETAIL_COLLAPSE_STORAGE_PREFIX = "tarot-detail-collapsed:";
|
||
|
|
const DEFAULT_DATASET_ENTRY_COLLAPSED = true;
|
||
|
|
const DEFAULT_DATASET_DETAIL_COLLAPSED = false;
|
||
|
|
const DEFAULT_SETTINGS = {
|
||
|
|
latitude: 51.5074,
|
||
|
|
longitude: -0.1278,
|
||
|
|
timeFormat: "minutes",
|
||
|
|
birthDate: "",
|
||
|
|
tarotDeck: DEFAULT_TAROT_DECK
|
||
|
|
};
|
||
|
|
|
||
|
|
const PLANET_CALENDAR_STYLES = {
|
||
|
|
saturn: {
|
||
|
|
name: "♄ Saturn",
|
||
|
|
color: "#f4f4f5",
|
||
|
|
backgroundColor: "#0a0a0a",
|
||
|
|
borderColor: "#0a0a0a"
|
||
|
|
},
|
||
|
|
jupiter: {
|
||
|
|
name: "♃ Jupiter",
|
||
|
|
color: "#eff6ff",
|
||
|
|
backgroundColor: "#1d4ed8",
|
||
|
|
borderColor: "#1d4ed8"
|
||
|
|
},
|
||
|
|
mars: {
|
||
|
|
name: "♂ Mars",
|
||
|
|
color: "#fff1f2",
|
||
|
|
backgroundColor: "#dc2626",
|
||
|
|
borderColor: "#dc2626"
|
||
|
|
},
|
||
|
|
sol: {
|
||
|
|
name: "☉ Sol",
|
||
|
|
color: "#111827",
|
||
|
|
backgroundColor: "#facc15",
|
||
|
|
borderColor: "#eab308"
|
||
|
|
},
|
||
|
|
venus: {
|
||
|
|
name: "♀ Venus",
|
||
|
|
color: "#ecfdf5",
|
||
|
|
backgroundColor: "#16a34a",
|
||
|
|
borderColor: "#15803d"
|
||
|
|
},
|
||
|
|
mercury: {
|
||
|
|
name: "☿ Mercury",
|
||
|
|
color: "#111827",
|
||
|
|
backgroundColor: "#fb923c",
|
||
|
|
borderColor: "#f97316"
|
||
|
|
},
|
||
|
|
luna: {
|
||
|
|
name: "☾ Luna",
|
||
|
|
color: "#111827",
|
||
|
|
backgroundColor: "#e2e8f0",
|
||
|
|
borderColor: "#cbd5e1"
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const planetaryCalendars = PLANET_CALENDAR_ORDER.map((planetId) => {
|
||
|
|
const style = PLANET_CALENDAR_STYLES[planetId];
|
||
|
|
return {
|
||
|
|
id: `planet-${planetId}`,
|
||
|
|
name: style.name,
|
||
|
|
color: style.color,
|
||
|
|
backgroundColor: style.backgroundColor,
|
||
|
|
dragBackgroundColor: style.backgroundColor,
|
||
|
|
borderColor: style.borderColor
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
const calendar = new tui.Calendar("#calendar", {
|
||
|
|
defaultView: "week",
|
||
|
|
usageStatistics: false,
|
||
|
|
isReadOnly: true,
|
||
|
|
useFormPopup: false,
|
||
|
|
useDetailPopup: false,
|
||
|
|
gridSelection: false,
|
||
|
|
calendars: [
|
||
|
|
...planetaryCalendars,
|
||
|
|
{
|
||
|
|
id: "planetary",
|
||
|
|
name: "Planetary (Fallback)",
|
||
|
|
color: "#f4f4f5",
|
||
|
|
backgroundColor: "#52525b",
|
||
|
|
dragBackgroundColor: "#52525b",
|
||
|
|
borderColor: "#52525b"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: "astrology",
|
||
|
|
name: "Astrology & Tarot",
|
||
|
|
color: "#18181b",
|
||
|
|
backgroundColor: "#fcd34d",
|
||
|
|
dragBackgroundColor: "#fcd34d",
|
||
|
|
borderColor: "#fcd34d"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
week: {
|
||
|
|
...baseWeekOptions,
|
||
|
|
startDayOfWeek: getCenteredWeekStartDay(new Date())
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
let referenceData = null;
|
||
|
|
let magickDataset = null;
|
||
|
|
let currentGeo = null;
|
||
|
|
let nowInterval = null;
|
||
|
|
let centeredDayKey = getDateKey(new Date());
|
||
|
|
let renderInProgress = false;
|
||
|
|
let currentTimeFormat = "minutes";
|
||
|
|
let currentSettings = { ...DEFAULT_SETTINGS };
|
||
|
|
let monthStripResizeFrame = null;
|
||
|
|
let lastNowSkyGeoKey = "";
|
||
|
|
let lastNowSkySourceUrl = "";
|
||
|
|
let activeSection = "home";
|
||
|
|
let activeTarotSpread = null; // null = browse view; "three-card" | "celtic-cross" = spread view
|
||
|
|
let activeTarotSpreadDraw = [];
|
||
|
|
let numbersSectionInitialized = false;
|
||
|
|
let activeNumberValue = 0;
|
||
|
|
const NUMBERS_SPECIAL_BASE_VALUES = [1, 2, 3, 4];
|
||
|
|
const numbersSpecialFlipState = new Map();
|
||
|
|
|
||
|
|
const DEFAULT_NUMBER_ENTRIES = Array.from({ length: 10 }, (_, value) => ({
|
||
|
|
value,
|
||
|
|
label: `${value}`,
|
||
|
|
opposite: 9 - value,
|
||
|
|
digitalRoot: value,
|
||
|
|
summary: "",
|
||
|
|
keywords: [],
|
||
|
|
associations: {
|
||
|
|
kabbalahNode: value === 0 ? 10 : value,
|
||
|
|
playingSuit: "hearts"
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
|
||
|
|
function normalizeNumberValue(value) {
|
||
|
|
const parsed = Number(value);
|
||
|
|
if (!Number.isFinite(parsed)) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
const normalized = Math.trunc(parsed);
|
||
|
|
if (normalized < 0) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
if (normalized > 9) {
|
||
|
|
return 9;
|
||
|
|
}
|
||
|
|
return normalized;
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeNumberEntry(rawEntry) {
|
||
|
|
if (!rawEntry || typeof rawEntry !== "object") {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const value = normalizeNumberValue(rawEntry.value);
|
||
|
|
const oppositeRaw = Number(rawEntry.opposite);
|
||
|
|
const opposite = Number.isFinite(oppositeRaw)
|
||
|
|
? normalizeNumberValue(oppositeRaw)
|
||
|
|
: (9 - value);
|
||
|
|
const digitalRootRaw = Number(rawEntry.digitalRoot);
|
||
|
|
const digitalRoot = Number.isFinite(digitalRootRaw)
|
||
|
|
? normalizeNumberValue(digitalRootRaw)
|
||
|
|
: value;
|
||
|
|
const kabbalahNodeRaw = Number(rawEntry?.associations?.kabbalahNode);
|
||
|
|
const kabbalahNode = Number.isFinite(kabbalahNodeRaw)
|
||
|
|
? Math.max(1, Math.trunc(kabbalahNodeRaw))
|
||
|
|
: (value === 0 ? 10 : value);
|
||
|
|
const tarotTrumpNumbersRaw = Array.isArray(rawEntry?.associations?.tarotTrumpNumbers)
|
||
|
|
? rawEntry.associations.tarotTrumpNumbers
|
||
|
|
: [];
|
||
|
|
const tarotTrumpNumbers = Array.from(new Set(
|
||
|
|
tarotTrumpNumbersRaw
|
||
|
|
.map((item) => Number(item))
|
||
|
|
.filter((item) => Number.isFinite(item))
|
||
|
|
.map((item) => Math.trunc(item))
|
||
|
|
));
|
||
|
|
const playingSuitRaw = String(rawEntry?.associations?.playingSuit || "").trim().toLowerCase();
|
||
|
|
const playingSuit = ["hearts", "diamonds", "clubs", "spades"].includes(playingSuitRaw)
|
||
|
|
? playingSuitRaw
|
||
|
|
: "hearts";
|
||
|
|
|
||
|
|
return {
|
||
|
|
value,
|
||
|
|
label: String(rawEntry.label || value),
|
||
|
|
opposite,
|
||
|
|
digitalRoot,
|
||
|
|
summary: String(rawEntry.summary || ""),
|
||
|
|
keywords: Array.isArray(rawEntry.keywords)
|
||
|
|
? rawEntry.keywords.map((keyword) => String(keyword || "").trim()).filter(Boolean)
|
||
|
|
: [],
|
||
|
|
associations: {
|
||
|
|
kabbalahNode,
|
||
|
|
tarotTrumpNumbers,
|
||
|
|
playingSuit
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function getNumbersDatasetEntries() {
|
||
|
|
const numbersData = magickDataset?.grouped?.numbers;
|
||
|
|
const rawEntries = Array.isArray(numbersData)
|
||
|
|
? numbersData
|
||
|
|
: (Array.isArray(numbersData?.entries) ? numbersData.entries : []);
|
||
|
|
|
||
|
|
const normalizedEntries = rawEntries
|
||
|
|
.map((entry) => normalizeNumberEntry(entry))
|
||
|
|
.filter(Boolean)
|
||
|
|
.sort((left, right) => left.value - right.value);
|
||
|
|
|
||
|
|
return normalizedEntries.length
|
||
|
|
? normalizedEntries
|
||
|
|
: DEFAULT_NUMBER_ENTRIES;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getNumberEntryByValue(value) {
|
||
|
|
const entries = getNumbersDatasetEntries();
|
||
|
|
const normalized = normalizeNumberValue(value);
|
||
|
|
return entries.find((entry) => entry.value === normalized) || entries[0] || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getCalendarMonthLinksForNumber(value) {
|
||
|
|
const normalized = normalizeNumberValue(value);
|
||
|
|
const calendarGroups = [
|
||
|
|
{
|
||
|
|
calendarId: "gregorian",
|
||
|
|
calendarLabel: "Gregorian",
|
||
|
|
months: Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : []
|
||
|
|
},
|
||
|
|
{
|
||
|
|
calendarId: "hebrew",
|
||
|
|
calendarLabel: "Hebrew",
|
||
|
|
months: Array.isArray(referenceData?.hebrewCalendar?.months) ? referenceData.hebrewCalendar.months : []
|
||
|
|
},
|
||
|
|
{
|
||
|
|
calendarId: "islamic",
|
||
|
|
calendarLabel: "Islamic",
|
||
|
|
months: Array.isArray(referenceData?.islamicCalendar?.months) ? referenceData.islamicCalendar.months : []
|
||
|
|
},
|
||
|
|
{
|
||
|
|
calendarId: "wheel-of-year",
|
||
|
|
calendarLabel: "Wheel of the Year",
|
||
|
|
months: Array.isArray(referenceData?.wheelOfYear?.months) ? referenceData.wheelOfYear.months : []
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const links = [];
|
||
|
|
calendarGroups.forEach((group) => {
|
||
|
|
group.months.forEach((month) => {
|
||
|
|
const monthOrder = Number(month?.order);
|
||
|
|
const normalizedOrder = Number.isFinite(monthOrder) ? Math.trunc(monthOrder) : null;
|
||
|
|
const monthRoot = normalizedOrder != null ? computeDigitalRoot(normalizedOrder) : null;
|
||
|
|
if (monthRoot !== normalized) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
links.push({
|
||
|
|
calendarId: group.calendarId,
|
||
|
|
calendarLabel: group.calendarLabel,
|
||
|
|
monthId: String(month.id || "").trim(),
|
||
|
|
monthName: String(month.name || month.id || "Month").trim(),
|
||
|
|
monthOrder: normalizedOrder
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
return links.filter((link) => link.monthId);
|
||
|
|
}
|
||
|
|
|
||
|
|
const PLAYING_SUIT_SYMBOL = {
|
||
|
|
hearts: "♥",
|
||
|
|
diamonds: "♦",
|
||
|
|
clubs: "♣",
|
||
|
|
spades: "♠"
|
||
|
|
};
|
||
|
|
|
||
|
|
const PLAYING_SUIT_LABEL = {
|
||
|
|
hearts: "Hearts",
|
||
|
|
diamonds: "Diamonds",
|
||
|
|
clubs: "Clubs",
|
||
|
|
spades: "Spades"
|
||
|
|
};
|
||
|
|
|
||
|
|
const PLAYING_SUIT_TO_TAROT = {
|
||
|
|
hearts: "Cups",
|
||
|
|
diamonds: "Pentacles",
|
||
|
|
clubs: "Wands",
|
||
|
|
spades: "Swords"
|
||
|
|
};
|
||
|
|
|
||
|
|
const PLAYING_RANKS = [
|
||
|
|
{ rank: "A", rankLabel: "Ace", rankValue: 1 },
|
||
|
|
{ rank: "2", rankLabel: "Two", rankValue: 2 },
|
||
|
|
{ rank: "3", rankLabel: "Three", rankValue: 3 },
|
||
|
|
{ rank: "4", rankLabel: "Four", rankValue: 4 },
|
||
|
|
{ rank: "5", rankLabel: "Five", rankValue: 5 },
|
||
|
|
{ rank: "6", rankLabel: "Six", rankValue: 6 },
|
||
|
|
{ rank: "7", rankLabel: "Seven", rankValue: 7 },
|
||
|
|
{ rank: "8", rankLabel: "Eight", rankValue: 8 },
|
||
|
|
{ rank: "9", rankLabel: "Nine", rankValue: 9 },
|
||
|
|
{ rank: "10", rankLabel: "Ten", rankValue: 10 },
|
||
|
|
{ rank: "J", rankLabel: "Jack", rankValue: null },
|
||
|
|
{ rank: "Q", rankLabel: "Queen", rankValue: null },
|
||
|
|
{ rank: "K", rankLabel: "King", rankValue: null }
|
||
|
|
];
|
||
|
|
|
||
|
|
function rankLabelToTarotMinorRank(rankLabel) {
|
||
|
|
const key = String(rankLabel || "").trim().toLowerCase();
|
||
|
|
if (key === "10" || key === "ten") return "Princess";
|
||
|
|
if (key === "j" || key === "jack") return "Prince";
|
||
|
|
if (key === "q" || key === "queen") return "Queen";
|
||
|
|
if (key === "k" || key === "king") return "Knight";
|
||
|
|
return String(rankLabel || "").trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildFallbackPlayingDeckEntries() {
|
||
|
|
const entries = [];
|
||
|
|
Object.keys(PLAYING_SUIT_SYMBOL).forEach((suit) => {
|
||
|
|
PLAYING_RANKS.forEach((rank) => {
|
||
|
|
const tarotSuit = PLAYING_SUIT_TO_TAROT[suit];
|
||
|
|
const tarotRank = rankLabelToTarotMinorRank(rank.rankLabel);
|
||
|
|
entries.push({
|
||
|
|
id: `${rank.rank}${PLAYING_SUIT_SYMBOL[suit]}`,
|
||
|
|
suit,
|
||
|
|
suitLabel: PLAYING_SUIT_LABEL[suit],
|
||
|
|
suitSymbol: PLAYING_SUIT_SYMBOL[suit],
|
||
|
|
rank: rank.rank,
|
||
|
|
rankLabel: rank.rankLabel,
|
||
|
|
rankValue: rank.rankValue,
|
||
|
|
tarotSuit,
|
||
|
|
tarotCard: `${tarotRank} of ${tarotSuit}`
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
return entries;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPlayingDeckEntries() {
|
||
|
|
const deckData = magickDataset?.grouped?.["playing-cards-52"];
|
||
|
|
const rawEntries = Array.isArray(deckData)
|
||
|
|
? deckData
|
||
|
|
: (Array.isArray(deckData?.entries) ? deckData.entries : []);
|
||
|
|
|
||
|
|
if (!rawEntries.length) {
|
||
|
|
return buildFallbackPlayingDeckEntries();
|
||
|
|
}
|
||
|
|
|
||
|
|
return rawEntries
|
||
|
|
.map((entry) => {
|
||
|
|
const suit = String(entry?.suit || "").trim().toLowerCase();
|
||
|
|
const rankLabel = String(entry?.rankLabel || "").trim();
|
||
|
|
const rank = String(entry?.rank || "").trim();
|
||
|
|
if (!suit || !rank) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const suitSymbol = String(entry?.suitSymbol || PLAYING_SUIT_SYMBOL[suit] || "").trim();
|
||
|
|
const tarotSuit = String(entry?.tarotSuit || PLAYING_SUIT_TO_TAROT[suit] || "").trim();
|
||
|
|
const tarotCard = String(entry?.tarotCard || "").trim();
|
||
|
|
const rankValueRaw = Number(entry?.rankValue);
|
||
|
|
const rankValue = Number.isFinite(rankValueRaw) ? Math.trunc(rankValueRaw) : null;
|
||
|
|
|
||
|
|
return {
|
||
|
|
id: String(entry?.id || `${rank}${suitSymbol}`).trim(),
|
||
|
|
suit,
|
||
|
|
suitLabel: String(entry?.suitLabel || PLAYING_SUIT_LABEL[suit] || suit).trim(),
|
||
|
|
suitSymbol,
|
||
|
|
rank,
|
||
|
|
rankLabel: rankLabel || rank,
|
||
|
|
rankValue,
|
||
|
|
tarotSuit,
|
||
|
|
tarotCard: tarotCard || `${rankLabelToTarotMinorRank(rankLabel || rank)} of ${tarotSuit}`
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.filter(Boolean);
|
||
|
|
}
|
||
|
|
|
||
|
|
function findPlayingCardBySuitAndValue(entries, suit, value) {
|
||
|
|
const normalizedSuit = String(suit || "").trim().toLowerCase();
|
||
|
|
const targetValue = Number(value);
|
||
|
|
return entries.find((entry) => entry.suit === normalizedSuit && Number(entry.rankValue) === targetValue) || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildNumbersSpecialCardSlots(playingSuit) {
|
||
|
|
const suit = String(playingSuit || "hearts").trim().toLowerCase();
|
||
|
|
const selectedSuit = ["hearts", "diamonds", "clubs", "spades"].includes(suit) ? suit : "hearts";
|
||
|
|
const deckEntries = getPlayingDeckEntries();
|
||
|
|
|
||
|
|
const cardEl = document.createElement("div");
|
||
|
|
cardEl.className = "numbers-detail-card numbers-special-card-section";
|
||
|
|
|
||
|
|
const headingEl = document.createElement("strong");
|
||
|
|
headingEl.textContent = "4 Card Arrangement";
|
||
|
|
|
||
|
|
const subEl = document.createElement("div");
|
||
|
|
subEl.className = "numbers-detail-text numbers-detail-text--muted";
|
||
|
|
subEl.textContent = `Click a card to flip to its opposite (${PLAYING_SUIT_LABEL[selectedSuit]} ↔ ${PLAYING_SUIT_TO_TAROT[selectedSuit]}).`;
|
||
|
|
|
||
|
|
const boardEl = document.createElement("div");
|
||
|
|
boardEl.className = "numbers-special-board";
|
||
|
|
|
||
|
|
NUMBERS_SPECIAL_BASE_VALUES.forEach((baseValue) => {
|
||
|
|
const oppositeValue = 9 - baseValue;
|
||
|
|
const frontCard = findPlayingCardBySuitAndValue(deckEntries, selectedSuit, baseValue);
|
||
|
|
const backCard = findPlayingCardBySuitAndValue(deckEntries, selectedSuit, oppositeValue);
|
||
|
|
if (!frontCard || !backCard) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const slotKey = `${selectedSuit}:${baseValue}`;
|
||
|
|
const isFlipped = Boolean(numbersSpecialFlipState.get(slotKey));
|
||
|
|
|
||
|
|
const faceBtn = document.createElement("button");
|
||
|
|
faceBtn.type = "button";
|
||
|
|
faceBtn.className = `numbers-special-card${isFlipped ? " is-flipped" : ""}`;
|
||
|
|
faceBtn.setAttribute("aria-pressed", isFlipped ? "true" : "false");
|
||
|
|
faceBtn.setAttribute("aria-label", `${frontCard.rankLabel} of ${frontCard.suitLabel}. Click to flip to ${backCard.rankLabel}.`);
|
||
|
|
faceBtn.dataset.suit = selectedSuit;
|
||
|
|
|
||
|
|
const innerEl = document.createElement("div");
|
||
|
|
innerEl.className = "numbers-special-card-inner";
|
||
|
|
|
||
|
|
const frontFaceEl = document.createElement("div");
|
||
|
|
frontFaceEl.className = "numbers-special-card-face numbers-special-card-face--front";
|
||
|
|
|
||
|
|
const frontRankEl = document.createElement("div");
|
||
|
|
frontRankEl.className = "numbers-special-card-rank";
|
||
|
|
frontRankEl.textContent = frontCard.rankLabel;
|
||
|
|
|
||
|
|
const frontSuitEl = document.createElement("div");
|
||
|
|
frontSuitEl.className = "numbers-special-card-suit";
|
||
|
|
frontSuitEl.textContent = frontCard.suitSymbol;
|
||
|
|
|
||
|
|
const frontMetaEl = document.createElement("div");
|
||
|
|
frontMetaEl.className = "numbers-special-card-meta";
|
||
|
|
frontMetaEl.textContent = frontCard.tarotCard;
|
||
|
|
|
||
|
|
frontFaceEl.append(frontRankEl, frontSuitEl, frontMetaEl);
|
||
|
|
|
||
|
|
const backFaceEl = document.createElement("div");
|
||
|
|
backFaceEl.className = "numbers-special-card-face numbers-special-card-face--back";
|
||
|
|
|
||
|
|
const backTagEl = document.createElement("div");
|
||
|
|
backTagEl.className = "numbers-special-card-tag";
|
||
|
|
backTagEl.textContent = "Opposite";
|
||
|
|
|
||
|
|
const backRankEl = document.createElement("div");
|
||
|
|
backRankEl.className = "numbers-special-card-rank";
|
||
|
|
backRankEl.textContent = backCard.rankLabel;
|
||
|
|
|
||
|
|
const backSuitEl = document.createElement("div");
|
||
|
|
backSuitEl.className = "numbers-special-card-suit";
|
||
|
|
backSuitEl.textContent = backCard.suitSymbol;
|
||
|
|
|
||
|
|
const backMetaEl = document.createElement("div");
|
||
|
|
backMetaEl.className = "numbers-special-card-meta";
|
||
|
|
backMetaEl.textContent = backCard.tarotCard;
|
||
|
|
|
||
|
|
backFaceEl.append(backTagEl, backRankEl, backSuitEl, backMetaEl);
|
||
|
|
|
||
|
|
innerEl.append(frontFaceEl, backFaceEl);
|
||
|
|
faceBtn.append(innerEl);
|
||
|
|
|
||
|
|
faceBtn.addEventListener("click", () => {
|
||
|
|
const next = !Boolean(numbersSpecialFlipState.get(slotKey));
|
||
|
|
numbersSpecialFlipState.set(slotKey, next);
|
||
|
|
faceBtn.classList.toggle("is-flipped", next);
|
||
|
|
faceBtn.setAttribute("aria-pressed", next ? "true" : "false");
|
||
|
|
});
|
||
|
|
|
||
|
|
boardEl.appendChild(faceBtn);
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!boardEl.childElementCount) {
|
||
|
|
const emptyEl = document.createElement("div");
|
||
|
|
emptyEl.className = "numbers-detail-text numbers-detail-text--muted";
|
||
|
|
emptyEl.textContent = "No card slots available for this mapping yet.";
|
||
|
|
boardEl.appendChild(emptyEl);
|
||
|
|
}
|
||
|
|
|
||
|
|
cardEl.append(headingEl, subEl, boardEl);
|
||
|
|
return cardEl;
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderNumbersSpecialPanel(value) {
|
||
|
|
if (!numbersSpecialPanelEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const entry = getNumberEntryByValue(value);
|
||
|
|
const playingSuit = entry?.associations?.playingSuit || "hearts";
|
||
|
|
const boardCardEl = buildNumbersSpecialCardSlots(playingSuit);
|
||
|
|
numbersSpecialPanelEl.replaceChildren(boardCardEl);
|
||
|
|
}
|
||
|
|
|
||
|
|
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 describeDigitalRootReduction(value) {
|
||
|
|
let current = Math.abs(Math.trunc(Number(value)));
|
||
|
|
if (!Number.isFinite(current)) {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
if (current < 10) {
|
||
|
|
return `${current} → ${current}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
const parts = [`${current}`];
|
||
|
|
while (current >= 10) {
|
||
|
|
const digits = String(current).split("").map((digit) => Number(digit));
|
||
|
|
const sum = digits.reduce((acc, digit) => acc + digit, 0);
|
||
|
|
parts.push(`${digits.join(" + ")} = ${sum}`);
|
||
|
|
current = sum;
|
||
|
|
}
|
||
|
|
|
||
|
|
return parts.join(" → ");
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseTarotCardNumber(rawValue) {
|
||
|
|
if (typeof rawValue === "number") {
|
||
|
|
return Number.isFinite(rawValue) ? Math.trunc(rawValue) : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof rawValue === "string") {
|
||
|
|
const trimmed = rawValue.trim();
|
||
|
|
if (!trimmed || !/^-?\d+$/.test(trimmed)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return Number(trimmed);
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const TAROT_RANK_NUMBER_MAP = {
|
||
|
|
ace: 1,
|
||
|
|
two: 2,
|
||
|
|
three: 3,
|
||
|
|
four: 4,
|
||
|
|
five: 5,
|
||
|
|
six: 6,
|
||
|
|
seven: 7,
|
||
|
|
eight: 8,
|
||
|
|
nine: 9,
|
||
|
|
ten: 10
|
||
|
|
};
|
||
|
|
|
||
|
|
function extractTarotCardNumericValue(card) {
|
||
|
|
const directNumber = parseTarotCardNumber(card?.number);
|
||
|
|
if (directNumber !== null) {
|
||
|
|
return directNumber;
|
||
|
|
}
|
||
|
|
|
||
|
|
const rankKey = String(card?.rank || "").trim().toLowerCase();
|
||
|
|
if (Object.prototype.hasOwnProperty.call(TAROT_RANK_NUMBER_MAP, rankKey)) {
|
||
|
|
return TAROT_RANK_NUMBER_MAP[rankKey];
|
||
|
|
}
|
||
|
|
|
||
|
|
const numerologyRelation = Array.isArray(card?.relations)
|
||
|
|
? card.relations.find((relation) => String(relation?.type || "").trim().toLowerCase() === "numerology")
|
||
|
|
: null;
|
||
|
|
const relationValue = Number(numerologyRelation?.data?.value);
|
||
|
|
if (Number.isFinite(relationValue)) {
|
||
|
|
return Math.trunc(relationValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getAlphabetPositionLinksForDigitalRoot(targetRoot) {
|
||
|
|
const alphabets = magickDataset?.grouped?.alphabets;
|
||
|
|
if (!alphabets || typeof alphabets !== "object") {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
const links = [];
|
||
|
|
|
||
|
|
const addLink = (alphabetLabel, entry, buttonLabel, detail) => {
|
||
|
|
const index = Number(entry?.index);
|
||
|
|
if (!Number.isFinite(index)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const normalizedIndex = Math.trunc(index);
|
||
|
|
if (computeDigitalRoot(normalizedIndex) !== targetRoot) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
links.push({
|
||
|
|
alphabet: alphabetLabel,
|
||
|
|
index: normalizedIndex,
|
||
|
|
label: buttonLabel,
|
||
|
|
detail
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const toTitle = (value) => String(value || "")
|
||
|
|
.trim()
|
||
|
|
.replace(/[_-]+/g, " ")
|
||
|
|
.replace(/\s+/g, " ")
|
||
|
|
.toLowerCase()
|
||
|
|
.replace(/\b([a-z])/g, (match, ch) => ch.toUpperCase());
|
||
|
|
|
||
|
|
const englishEntries = Array.isArray(alphabets.english) ? alphabets.english : [];
|
||
|
|
englishEntries.forEach((entry) => {
|
||
|
|
const letter = String(entry?.letter || "").trim();
|
||
|
|
if (!letter) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
addLink(
|
||
|
|
"English",
|
||
|
|
entry,
|
||
|
|
`${letter}`,
|
||
|
|
{
|
||
|
|
alphabet: "english",
|
||
|
|
englishLetter: letter
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
const greekEntries = Array.isArray(alphabets.greek) ? alphabets.greek : [];
|
||
|
|
greekEntries.forEach((entry) => {
|
||
|
|
const greekName = String(entry?.name || "").trim();
|
||
|
|
if (!greekName) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const glyph = String(entry?.char || "").trim();
|
||
|
|
const displayName = String(entry?.displayName || toTitle(greekName)).trim();
|
||
|
|
addLink(
|
||
|
|
"Greek",
|
||
|
|
entry,
|
||
|
|
glyph ? `${displayName} - ${glyph}` : displayName,
|
||
|
|
{
|
||
|
|
alphabet: "greek",
|
||
|
|
greekName
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
const hebrewEntries = Array.isArray(alphabets.hebrew) ? alphabets.hebrew : [];
|
||
|
|
hebrewEntries.forEach((entry) => {
|
||
|
|
const hebrewLetterId = String(entry?.hebrewLetterId || "").trim();
|
||
|
|
if (!hebrewLetterId) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const glyph = String(entry?.char || "").trim();
|
||
|
|
const name = String(entry?.name || hebrewLetterId).trim();
|
||
|
|
const displayName = toTitle(name);
|
||
|
|
addLink(
|
||
|
|
"Hebrew",
|
||
|
|
entry,
|
||
|
|
glyph ? `${displayName} - ${glyph}` : displayName,
|
||
|
|
{
|
||
|
|
alphabet: "hebrew",
|
||
|
|
hebrewLetterId
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
const arabicEntries = Array.isArray(alphabets.arabic) ? alphabets.arabic : [];
|
||
|
|
arabicEntries.forEach((entry) => {
|
||
|
|
const arabicName = String(entry?.name || "").trim();
|
||
|
|
if (!arabicName) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const glyph = String(entry?.char || "").trim();
|
||
|
|
const displayName = toTitle(arabicName);
|
||
|
|
addLink(
|
||
|
|
"Arabic",
|
||
|
|
entry,
|
||
|
|
glyph ? `${displayName} - ${glyph}` : displayName,
|
||
|
|
{
|
||
|
|
alphabet: "arabic",
|
||
|
|
arabicName
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
const enochianEntries = Array.isArray(alphabets.enochian) ? alphabets.enochian : [];
|
||
|
|
enochianEntries.forEach((entry) => {
|
||
|
|
const enochianId = String(entry?.id || "").trim();
|
||
|
|
if (!enochianId) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const title = String(entry?.title || enochianId).trim();
|
||
|
|
const displayName = toTitle(title);
|
||
|
|
addLink(
|
||
|
|
"Enochian",
|
||
|
|
entry,
|
||
|
|
`${displayName}`,
|
||
|
|
{
|
||
|
|
alphabet: "enochian",
|
||
|
|
enochianId
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
return links.sort((left, right) => {
|
||
|
|
if (left.index !== right.index) {
|
||
|
|
return left.index - right.index;
|
||
|
|
}
|
||
|
|
const alphabetCompare = left.alphabet.localeCompare(right.alphabet);
|
||
|
|
if (alphabetCompare !== 0) {
|
||
|
|
return alphabetCompare;
|
||
|
|
}
|
||
|
|
return left.label.localeCompare(right.label);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function getTarotCardsForDigitalRoot(targetRoot, numberEntry = null) {
|
||
|
|
if (typeof ensureTarotSection === "function" && referenceData) {
|
||
|
|
ensureTarotSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
|
||
|
|
const allCards = window.TarotSectionUi?.getCards?.() || [];
|
||
|
|
const explicitTrumpNumbers = Array.isArray(numberEntry?.associations?.tarotTrumpNumbers)
|
||
|
|
? numberEntry.associations.tarotTrumpNumbers
|
||
|
|
.map((value) => Number(value))
|
||
|
|
.filter((value) => Number.isFinite(value))
|
||
|
|
.map((value) => Math.trunc(value))
|
||
|
|
: [];
|
||
|
|
|
||
|
|
const filteredCards = explicitTrumpNumbers.length
|
||
|
|
? allCards.filter((card) => {
|
||
|
|
const numberValue = parseTarotCardNumber(card?.number);
|
||
|
|
return card?.arcana === "Major" && numberValue !== null && explicitTrumpNumbers.includes(numberValue);
|
||
|
|
})
|
||
|
|
: allCards.filter((card) => {
|
||
|
|
const numberValue = extractTarotCardNumericValue(card);
|
||
|
|
return numberValue !== null && computeDigitalRoot(numberValue) === targetRoot;
|
||
|
|
});
|
||
|
|
|
||
|
|
return filteredCards
|
||
|
|
.sort((left, right) => {
|
||
|
|
const leftNumber = extractTarotCardNumericValue(left);
|
||
|
|
const rightNumber = extractTarotCardNumericValue(right);
|
||
|
|
if (leftNumber !== rightNumber) {
|
||
|
|
return (leftNumber ?? 0) - (rightNumber ?? 0);
|
||
|
|
}
|
||
|
|
if (left?.arcana !== right?.arcana) {
|
||
|
|
return left?.arcana === "Major" ? -1 : 1;
|
||
|
|
}
|
||
|
|
return String(left?.name || "").localeCompare(String(right?.name || ""));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderNumbersList() {
|
||
|
|
if (!numbersListEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const entries = getNumbersDatasetEntries();
|
||
|
|
if (!entries.some((entry) => entry.value === activeNumberValue)) {
|
||
|
|
activeNumberValue = entries[0]?.value ?? 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
const fragment = document.createDocumentFragment();
|
||
|
|
entries.forEach((entry) => {
|
||
|
|
const button = document.createElement("button");
|
||
|
|
button.type = "button";
|
||
|
|
button.className = `planet-list-item${entry.value === activeNumberValue ? " is-selected" : ""}`;
|
||
|
|
button.dataset.numberValue = String(entry.value);
|
||
|
|
button.setAttribute("role", "option");
|
||
|
|
button.setAttribute("aria-selected", entry.value === activeNumberValue ? "true" : "false");
|
||
|
|
|
||
|
|
const nameEl = document.createElement("span");
|
||
|
|
nameEl.className = "planet-list-name";
|
||
|
|
nameEl.textContent = `${entry.label}`;
|
||
|
|
|
||
|
|
const metaEl = document.createElement("span");
|
||
|
|
metaEl.className = "planet-list-meta";
|
||
|
|
metaEl.textContent = `Opposite ${entry.opposite}`;
|
||
|
|
|
||
|
|
button.append(nameEl, metaEl);
|
||
|
|
fragment.appendChild(button);
|
||
|
|
});
|
||
|
|
|
||
|
|
numbersListEl.replaceChildren(fragment);
|
||
|
|
if (numbersCountEl) {
|
||
|
|
numbersCountEl.textContent = `${entries.length} entries`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderNumberDetail(value) {
|
||
|
|
const entry = getNumberEntryByValue(value);
|
||
|
|
if (!entry) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const normalized = entry.value;
|
||
|
|
const opposite = entry.opposite;
|
||
|
|
const rootTarget = normalizeNumberValue(entry.digitalRoot);
|
||
|
|
|
||
|
|
if (numbersDetailNameEl) {
|
||
|
|
numbersDetailNameEl.textContent = `Number ${normalized} · ${entry.label}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (numbersDetailTypeEl) {
|
||
|
|
numbersDetailTypeEl.textContent = `Opposite: ${opposite}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (numbersDetailSummaryEl) {
|
||
|
|
numbersDetailSummaryEl.textContent = entry.summary || "";
|
||
|
|
}
|
||
|
|
|
||
|
|
renderNumbersSpecialPanel(normalized);
|
||
|
|
|
||
|
|
if (!numbersDetailBodyEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
numbersDetailBodyEl.replaceChildren();
|
||
|
|
|
||
|
|
const pairCardEl = document.createElement("div");
|
||
|
|
pairCardEl.className = "numbers-detail-card";
|
||
|
|
|
||
|
|
const pairHeadingEl = document.createElement("strong");
|
||
|
|
pairHeadingEl.textContent = "Number Pair";
|
||
|
|
|
||
|
|
const pairTextEl = document.createElement("div");
|
||
|
|
pairTextEl.className = "numbers-detail-text";
|
||
|
|
pairTextEl.textContent = `Opposite: ${opposite}`;
|
||
|
|
|
||
|
|
const keywordText = entry.keywords.length
|
||
|
|
? `Keywords: ${entry.keywords.join(", ")}`
|
||
|
|
: "Keywords: --";
|
||
|
|
const pairKeywordsEl = document.createElement("div");
|
||
|
|
pairKeywordsEl.className = "numbers-detail-text numbers-detail-text--muted";
|
||
|
|
pairKeywordsEl.textContent = keywordText;
|
||
|
|
|
||
|
|
const oppositeBtn = document.createElement("button");
|
||
|
|
oppositeBtn.type = "button";
|
||
|
|
oppositeBtn.className = "numbers-nav-btn";
|
||
|
|
oppositeBtn.textContent = `Open Opposite Number ${opposite}`;
|
||
|
|
oppositeBtn.addEventListener("click", () => {
|
||
|
|
selectNumberEntry(opposite);
|
||
|
|
});
|
||
|
|
|
||
|
|
pairCardEl.append(pairHeadingEl, pairTextEl, pairKeywordsEl, oppositeBtn);
|
||
|
|
|
||
|
|
const kabbalahCardEl = document.createElement("div");
|
||
|
|
kabbalahCardEl.className = "numbers-detail-card";
|
||
|
|
|
||
|
|
const kabbalahHeadingEl = document.createElement("strong");
|
||
|
|
kabbalahHeadingEl.textContent = "Kabbalah Link";
|
||
|
|
|
||
|
|
const kabbalahNode = Number(entry?.associations?.kabbalahNode);
|
||
|
|
const kabbalahTextEl = document.createElement("div");
|
||
|
|
kabbalahTextEl.className = "numbers-detail-text";
|
||
|
|
kabbalahTextEl.textContent = `Tree node target: ${kabbalahNode}`;
|
||
|
|
|
||
|
|
const kabbalahBtn = document.createElement("button");
|
||
|
|
kabbalahBtn.type = "button";
|
||
|
|
kabbalahBtn.className = "numbers-nav-btn";
|
||
|
|
kabbalahBtn.textContent = `Open Kabbalah Tree Node ${kabbalahNode}`;
|
||
|
|
kabbalahBtn.addEventListener("click", () => {
|
||
|
|
document.dispatchEvent(new CustomEvent("nav:kabbalah-path", {
|
||
|
|
detail: { pathNo: kabbalahNode }
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
kabbalahCardEl.append(kabbalahHeadingEl, kabbalahTextEl, kabbalahBtn);
|
||
|
|
|
||
|
|
const alphabetCardEl = document.createElement("div");
|
||
|
|
alphabetCardEl.className = "numbers-detail-card";
|
||
|
|
|
||
|
|
const alphabetHeadingEl = document.createElement("strong");
|
||
|
|
alphabetHeadingEl.textContent = "Alphabet Links";
|
||
|
|
|
||
|
|
const alphabetLinksWrapEl = document.createElement("div");
|
||
|
|
alphabetLinksWrapEl.className = "numbers-links-wrap";
|
||
|
|
|
||
|
|
const alphabetLinks = getAlphabetPositionLinksForDigitalRoot(rootTarget);
|
||
|
|
if (!alphabetLinks.length) {
|
||
|
|
const emptyAlphabetEl = document.createElement("div");
|
||
|
|
emptyAlphabetEl.className = "numbers-detail-text numbers-detail-text--muted";
|
||
|
|
emptyAlphabetEl.textContent = "No alphabet position entries found for this digital root yet.";
|
||
|
|
alphabetLinksWrapEl.appendChild(emptyAlphabetEl);
|
||
|
|
} else {
|
||
|
|
alphabetLinks.forEach((link) => {
|
||
|
|
const button = document.createElement("button");
|
||
|
|
button.type = "button";
|
||
|
|
button.className = "numbers-nav-btn";
|
||
|
|
button.textContent = `${link.alphabet}: ${link.label}`;
|
||
|
|
button.addEventListener("click", () => {
|
||
|
|
document.dispatchEvent(new CustomEvent("nav:alphabet", {
|
||
|
|
detail: link.detail
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
alphabetLinksWrapEl.appendChild(button);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
alphabetCardEl.append(alphabetHeadingEl, alphabetLinksWrapEl);
|
||
|
|
|
||
|
|
const tarotCardEl = document.createElement("div");
|
||
|
|
tarotCardEl.className = "numbers-detail-card";
|
||
|
|
|
||
|
|
const tarotHeadingEl = document.createElement("strong");
|
||
|
|
tarotHeadingEl.textContent = "Tarot Links";
|
||
|
|
|
||
|
|
const tarotLinksWrapEl = document.createElement("div");
|
||
|
|
tarotLinksWrapEl.className = "numbers-links-wrap";
|
||
|
|
|
||
|
|
const tarotCards = getTarotCardsForDigitalRoot(rootTarget, entry);
|
||
|
|
if (!tarotCards.length) {
|
||
|
|
const emptyEl = document.createElement("div");
|
||
|
|
emptyEl.className = "numbers-detail-text numbers-detail-text--muted";
|
||
|
|
emptyEl.textContent = "No tarot numeric entries found yet for this root. Add card numbers to map them.";
|
||
|
|
tarotLinksWrapEl.appendChild(emptyEl);
|
||
|
|
} else {
|
||
|
|
tarotCards.forEach((card) => {
|
||
|
|
const button = document.createElement("button");
|
||
|
|
button.type = "button";
|
||
|
|
button.className = "numbers-nav-btn";
|
||
|
|
button.textContent = `${card.name}`;
|
||
|
|
button.addEventListener("click", () => {
|
||
|
|
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
|
||
|
|
detail: { cardName: card.name }
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
tarotLinksWrapEl.appendChild(button);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
tarotCardEl.append(tarotHeadingEl, tarotLinksWrapEl);
|
||
|
|
|
||
|
|
const calendarCardEl = document.createElement("div");
|
||
|
|
calendarCardEl.className = "numbers-detail-card";
|
||
|
|
|
||
|
|
const calendarHeadingEl = document.createElement("strong");
|
||
|
|
calendarHeadingEl.textContent = "Calendar Links";
|
||
|
|
|
||
|
|
const calendarLinksWrapEl = document.createElement("div");
|
||
|
|
calendarLinksWrapEl.className = "numbers-links-wrap";
|
||
|
|
|
||
|
|
const calendarLinks = getCalendarMonthLinksForNumber(normalized);
|
||
|
|
if (!calendarLinks.length) {
|
||
|
|
const emptyCalendarEl = document.createElement("div");
|
||
|
|
emptyCalendarEl.className = "numbers-detail-text numbers-detail-text--muted";
|
||
|
|
emptyCalendarEl.textContent = "No calendar months currently mapped to this number.";
|
||
|
|
calendarLinksWrapEl.appendChild(emptyCalendarEl);
|
||
|
|
} else {
|
||
|
|
calendarLinks.forEach((link) => {
|
||
|
|
const button = document.createElement("button");
|
||
|
|
button.type = "button";
|
||
|
|
button.className = "numbers-nav-btn";
|
||
|
|
button.textContent = `${link.calendarLabel}: ${link.monthName} (Month ${link.monthOrder})`;
|
||
|
|
button.addEventListener("click", () => {
|
||
|
|
document.dispatchEvent(new CustomEvent("nav:calendar-month", {
|
||
|
|
detail: {
|
||
|
|
calendarId: link.calendarId,
|
||
|
|
monthId: link.monthId
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
calendarLinksWrapEl.appendChild(button);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
calendarCardEl.append(calendarHeadingEl, calendarLinksWrapEl);
|
||
|
|
|
||
|
|
numbersDetailBodyEl.append(pairCardEl, kabbalahCardEl, alphabetCardEl, tarotCardEl, calendarCardEl);
|
||
|
|
}
|
||
|
|
|
||
|
|
function selectNumberEntry(value) {
|
||
|
|
const entry = getNumberEntryByValue(value);
|
||
|
|
activeNumberValue = entry ? entry.value : 0;
|
||
|
|
renderNumbersList();
|
||
|
|
renderNumberDetail(activeNumberValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
function ensureNumbersSection() {
|
||
|
|
if (!numbersListEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!numbersSectionInitialized) {
|
||
|
|
numbersListEl.addEventListener("click", (event) => {
|
||
|
|
const target = event.target;
|
||
|
|
if (!(target instanceof Node)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const button = target instanceof Element
|
||
|
|
? target.closest(".planet-list-item")
|
||
|
|
: null;
|
||
|
|
if (!(button instanceof HTMLButtonElement)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const value = Number(button.dataset.numberValue);
|
||
|
|
if (!Number.isFinite(value)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
selectNumberEntry(value);
|
||
|
|
});
|
||
|
|
|
||
|
|
numbersSectionInitialized = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
renderNumbersList();
|
||
|
|
renderNumberDetail(activeNumberValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
const THREE_CARD_POSITIONS = [
|
||
|
|
{ pos: "past", label: "Past" },
|
||
|
|
{ pos: "present", label: "Present" },
|
||
|
|
{ pos: "future", label: "Future" }
|
||
|
|
];
|
||
|
|
|
||
|
|
const CELTIC_CROSS_POSITIONS = [
|
||
|
|
{ pos: "crown", label: "Crown" },
|
||
|
|
{ pos: "out", label: "Outcome" },
|
||
|
|
{ pos: "past", label: "Recent Past" },
|
||
|
|
{ pos: "present", label: "Present" },
|
||
|
|
{ pos: "near-fut", label: "Near Future" },
|
||
|
|
{ pos: "hope", label: "Hopes & Fears" },
|
||
|
|
{ pos: "chall", label: "Challenge" },
|
||
|
|
{ pos: "env", label: "Environment" },
|
||
|
|
{ pos: "found", label: "Foundation" },
|
||
|
|
{ pos: "self", label: "Self" }
|
||
|
|
];
|
||
|
|
|
||
|
|
function normalizeTarotSpread(value) {
|
||
|
|
return value === "celtic-cross" ? "celtic-cross" : "three-card";
|
||
|
|
}
|
||
|
|
|
||
|
|
function drawNFromDeck(n) {
|
||
|
|
const allCards = window.TarotSectionUi?.getCards?.() || [];
|
||
|
|
if (!allCards.length) return [];
|
||
|
|
|
||
|
|
const shuffled = [...allCards];
|
||
|
|
for (let index = shuffled.length - 1; index > 0; index -= 1) {
|
||
|
|
const swapIndex = Math.floor(Math.random() * (index + 1));
|
||
|
|
[shuffled[index], shuffled[swapIndex]] = [shuffled[swapIndex], shuffled[index]];
|
||
|
|
}
|
||
|
|
|
||
|
|
return shuffled.slice(0, n).map((card) => ({
|
||
|
|
...card,
|
||
|
|
reversed: Math.random() < 0.3
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
function escapeHtml(value) {
|
||
|
|
return String(value || "")
|
||
|
|
.replace(/&/g, "&")
|
||
|
|
.replace(/</g, "<")
|
||
|
|
.replace(/>/g, ">")
|
||
|
|
.replace(/\"/g, """)
|
||
|
|
.replace(/'/g, "'");
|
||
|
|
}
|
||
|
|
|
||
|
|
function getSpreadPositions(spreadId) {
|
||
|
|
return spreadId === "celtic-cross" ? CELTIC_CROSS_POSITIONS : THREE_CARD_POSITIONS;
|
||
|
|
}
|
||
|
|
|
||
|
|
function regenerateTarotSpreadDraw() {
|
||
|
|
const normalizedSpread = normalizeTarotSpread(activeTarotSpread);
|
||
|
|
const positions = getSpreadPositions(normalizedSpread);
|
||
|
|
const cards = drawNFromDeck(positions.length);
|
||
|
|
activeTarotSpreadDraw = positions.map((position, index) => ({
|
||
|
|
position,
|
||
|
|
card: cards[index] || null
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderTarotSpreadMeanings() {
|
||
|
|
if (!tarotSpreadMeaningsEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!activeTarotSpreadDraw.length || activeTarotSpreadDraw.some((entry) => !entry.card)) {
|
||
|
|
tarotSpreadMeaningsEl.innerHTML = "";
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
tarotSpreadMeaningsEl.innerHTML = activeTarotSpreadDraw.map((entry) => {
|
||
|
|
const positionLabel = escapeHtml(entry.position.label).toUpperCase();
|
||
|
|
const card = entry.card;
|
||
|
|
const cardName = escapeHtml(card.name || "Unknown Card");
|
||
|
|
const meaningText = escapeHtml(card.reversed ? (card.meanings?.reversed || card.summary || "--") : (card.meanings?.upright || card.summary || "--"));
|
||
|
|
const keywords = Array.isArray(card.keywords)
|
||
|
|
? card.keywords.map((keyword) => String(keyword || "").trim()).filter(Boolean)
|
||
|
|
: [];
|
||
|
|
const keywordMarkup = keywords.length
|
||
|
|
? `<div class=\"tarot-spread-meaning-keywords\">Keywords: ${escapeHtml(keywords.join(", "))}</div>`
|
||
|
|
: "";
|
||
|
|
const orientationMarkup = card.reversed
|
||
|
|
? " <span class=\"tarot-spread-meaning-orientation\">(Reversed)</span>"
|
||
|
|
: "";
|
||
|
|
|
||
|
|
return `<div class=\"tarot-spread-meaning-item\">`
|
||
|
|
+ `<div class=\"tarot-spread-meaning-head\">${positionLabel}: <span class=\"tarot-spread-meaning-card\">${cardName}</span>${orientationMarkup}</div>`
|
||
|
|
+ `<div class=\"tarot-spread-meaning-text\">${meaningText}</div>`
|
||
|
|
+ keywordMarkup
|
||
|
|
+ `</div>`;
|
||
|
|
}).join("");
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderTarotSpread() {
|
||
|
|
if (!tarotSpreadBoardEl) return;
|
||
|
|
const normalizedSpread = normalizeTarotSpread(activeTarotSpread);
|
||
|
|
const isCeltic = normalizedSpread === "celtic-cross";
|
||
|
|
|
||
|
|
if (!activeTarotSpreadDraw.length) {
|
||
|
|
regenerateTarotSpreadDraw();
|
||
|
|
}
|
||
|
|
|
||
|
|
tarotSpreadBoardEl.className = `tarot-spread-board tarot-spread-board--${isCeltic ? "celtic" : "three"}`;
|
||
|
|
|
||
|
|
if (!activeTarotSpreadDraw.length || activeTarotSpreadDraw.some((entry) => !entry.card)) {
|
||
|
|
tarotSpreadBoardEl.innerHTML = `<div class="spread-empty">Tarot deck not loaded yet — open Cards first, then return to Spread.</div>`;
|
||
|
|
if (tarotSpreadMeaningsEl) {
|
||
|
|
tarotSpreadMeaningsEl.innerHTML = "";
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
renderTarotSpreadMeanings();
|
||
|
|
|
||
|
|
tarotSpreadBoardEl.innerHTML = activeTarotSpreadDraw.map((entry) => {
|
||
|
|
const position = entry.position;
|
||
|
|
const card = entry.card;
|
||
|
|
const imgSrc = window.TarotCardImages?.resolveTarotCardImage?.(card.name);
|
||
|
|
const reversed = card.reversed;
|
||
|
|
const wrapClass = reversed ? "spread-card-wrap is-reversed" : "spread-card-wrap";
|
||
|
|
const imgHtml = imgSrc
|
||
|
|
? `<img class="spread-card-img" src="${imgSrc}" alt="${escapeHtml(card.name)}" loading="lazy">`
|
||
|
|
: `<div class="spread-card-placeholder">${escapeHtml(card.name)}</div>`;
|
||
|
|
const reversedTag = reversed ? `<span class="spread-reversed-tag">Reversed</span>` : "";
|
||
|
|
return `<div class="spread-position" data-pos="${position.pos}">`
|
||
|
|
+ `<div class="spread-pos-label">${escapeHtml(position.label)}</div>`
|
||
|
|
+ `<div class="${wrapClass}">${imgHtml}</div>`
|
||
|
|
+ `<div class="spread-card-name">${escapeHtml(card.name)}${reversedTag}</div>`
|
||
|
|
+ `</div>`;
|
||
|
|
}).join("");
|
||
|
|
}
|
||
|
|
|
||
|
|
function applyTarotSpreadViewState() {
|
||
|
|
const isSpreadOpen = activeTarotSpread !== null;
|
||
|
|
const isCeltic = activeTarotSpread === "celtic-cross";
|
||
|
|
const isTarotActive = activeSection === "tarot";
|
||
|
|
|
||
|
|
if (tarotBrowseViewEl) tarotBrowseViewEl.hidden = isSpreadOpen;
|
||
|
|
if (tarotSpreadViewEl) tarotSpreadViewEl.hidden = !isSpreadOpen;
|
||
|
|
|
||
|
|
if (tarotSpreadBtnThreeEl) tarotSpreadBtnThreeEl.classList.toggle("is-active", isSpreadOpen && !isCeltic);
|
||
|
|
if (tarotSpreadBtnCelticEl) tarotSpreadBtnCelticEl.classList.toggle("is-active", isSpreadOpen && isCeltic);
|
||
|
|
|
||
|
|
if (openTarotCardsEl) openTarotCardsEl.classList.toggle("is-active", isTarotActive && !isSpreadOpen);
|
||
|
|
if (openTarotSpreadEl) openTarotSpreadEl.classList.toggle("is-active", isTarotActive && isSpreadOpen);
|
||
|
|
}
|
||
|
|
|
||
|
|
function showTarotCardsView() {
|
||
|
|
activeTarotSpread = null;
|
||
|
|
activeTarotSpreadDraw = [];
|
||
|
|
applyTarotSpreadViewState();
|
||
|
|
if (typeof ensureTarotSection === "function" && referenceData) {
|
||
|
|
ensureTarotSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
const detailPanelEl = document.querySelector("#tarot-browse-view .tarot-detail-panel");
|
||
|
|
if (detailPanelEl instanceof HTMLElement) {
|
||
|
|
detailPanelEl.scrollTop = 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function showTarotSpreadView(spreadId = "three-card") {
|
||
|
|
activeTarotSpread = normalizeTarotSpread(spreadId);
|
||
|
|
regenerateTarotSpreadDraw();
|
||
|
|
applyTarotSpreadViewState();
|
||
|
|
if (typeof ensureTarotSection === "function" && referenceData) {
|
||
|
|
ensureTarotSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
renderTarotSpread();
|
||
|
|
}
|
||
|
|
|
||
|
|
function setTarotSpread(spreadId, openTarotSection = false) {
|
||
|
|
if (openTarotSection) {
|
||
|
|
setActiveSection("tarot");
|
||
|
|
}
|
||
|
|
showTarotSpreadView(spreadId);
|
||
|
|
}
|
||
|
|
|
||
|
|
const DEFAULT_WEEKDAY_RULERS = {
|
||
|
|
0: { symbol: "☉", name: "Sol" },
|
||
|
|
1: { symbol: "☾", name: "Luna" },
|
||
|
|
2: { symbol: "♂", name: "Mars" },
|
||
|
|
3: { symbol: "☿", name: "Mercury" },
|
||
|
|
4: { symbol: "♃", name: "Jupiter" },
|
||
|
|
5: { symbol: "♀", name: "Venus" },
|
||
|
|
6: { symbol: "♄", name: "Saturn" }
|
||
|
|
};
|
||
|
|
|
||
|
|
function getWeekdayIndexFromName(weekdayName) {
|
||
|
|
const normalized = String(weekdayName || "").trim().toLowerCase();
|
||
|
|
if (normalized === "sunday") return 0;
|
||
|
|
if (normalized === "monday") return 1;
|
||
|
|
if (normalized === "tuesday") return 2;
|
||
|
|
if (normalized === "wednesday") return 3;
|
||
|
|
if (normalized === "thursday") return 4;
|
||
|
|
if (normalized === "friday") return 5;
|
||
|
|
if (normalized === "saturday") return 6;
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildWeekdayRulerLookup(planets) {
|
||
|
|
const lookup = { ...DEFAULT_WEEKDAY_RULERS };
|
||
|
|
if (!planets || typeof planets !== "object") {
|
||
|
|
return lookup;
|
||
|
|
}
|
||
|
|
|
||
|
|
Object.values(planets).forEach((planet) => {
|
||
|
|
const weekdayIndex = getWeekdayIndexFromName(planet?.weekday);
|
||
|
|
if (weekdayIndex === null) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
lookup[weekdayIndex] = {
|
||
|
|
symbol: planet?.symbol || lookup[weekdayIndex].symbol,
|
||
|
|
name: planet?.name || lookup[weekdayIndex].name
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
return lookup;
|
||
|
|
}
|
||
|
|
|
||
|
|
function clamp(value, min, max) {
|
||
|
|
return Math.min(max, Math.max(min, value));
|
||
|
|
}
|
||
|
|
|
||
|
|
function lerp(start, end, t) {
|
||
|
|
return start + (end - start) * t;
|
||
|
|
}
|
||
|
|
|
||
|
|
function lerpRgb(from, to, t) {
|
||
|
|
return [
|
||
|
|
Math.round(lerp(from[0], to[0], t)),
|
||
|
|
Math.round(lerp(from[1], to[1], t)),
|
||
|
|
Math.round(lerp(from[2], to[2], t))
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
function rgbString(rgb) {
|
||
|
|
return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getActiveGeoForRuler() {
|
||
|
|
if (currentGeo) {
|
||
|
|
return currentGeo;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
return parseGeoInput();
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildSunRulerGradient(geo, date) {
|
||
|
|
if (!window.SunCalc || !geo || !date) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const dayStart = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
|
||
|
|
const sampleCount = 48;
|
||
|
|
const samples = [];
|
||
|
|
|
||
|
|
for (let index = 0; index <= sampleCount; index += 1) {
|
||
|
|
const sampleDate = new Date(dayStart.getTime() + index * 30 * 60 * 1000);
|
||
|
|
const position = window.SunCalc.getPosition(sampleDate, geo.latitude, geo.longitude);
|
||
|
|
const altitudeDeg = (position.altitude * 180) / Math.PI;
|
||
|
|
samples.push(altitudeDeg);
|
||
|
|
}
|
||
|
|
|
||
|
|
const maxAltitude = Math.max(...samples);
|
||
|
|
|
||
|
|
const NIGHT = [6, 7, 10];
|
||
|
|
const PRE_DAWN = [22, 26, 38];
|
||
|
|
const SUN_RED = [176, 45, 36];
|
||
|
|
const SUN_ORANGE = [246, 133, 54];
|
||
|
|
const SKY_BLUE = [58, 134, 255];
|
||
|
|
|
||
|
|
const nightFloor = -8;
|
||
|
|
const twilightEdge = -2;
|
||
|
|
const redToOrangeEdge = 2;
|
||
|
|
const orangeToBlueEdge = 8;
|
||
|
|
const daylightRange = Math.max(1, maxAltitude - orangeToBlueEdge);
|
||
|
|
|
||
|
|
const stops = samples.map((altitudeDeg, index) => {
|
||
|
|
let color;
|
||
|
|
|
||
|
|
if (altitudeDeg <= nightFloor) {
|
||
|
|
color = NIGHT;
|
||
|
|
} else if (altitudeDeg <= twilightEdge) {
|
||
|
|
const t = clamp((altitudeDeg - nightFloor) / (twilightEdge - nightFloor), 0, 1);
|
||
|
|
color = lerpRgb(NIGHT, PRE_DAWN, t);
|
||
|
|
} else if (altitudeDeg <= redToOrangeEdge) {
|
||
|
|
const t = clamp((altitudeDeg - twilightEdge) / (redToOrangeEdge - twilightEdge), 0, 1);
|
||
|
|
color = lerpRgb(PRE_DAWN, SUN_RED, t);
|
||
|
|
} else if (altitudeDeg <= orangeToBlueEdge) {
|
||
|
|
const t = clamp((altitudeDeg - redToOrangeEdge) / (orangeToBlueEdge - redToOrangeEdge), 0, 1);
|
||
|
|
color = lerpRgb(SUN_RED, SUN_ORANGE, t);
|
||
|
|
} else {
|
||
|
|
const t = clamp((altitudeDeg - orangeToBlueEdge) / daylightRange, 0, 1);
|
||
|
|
color = lerpRgb(SUN_ORANGE, SKY_BLUE, t);
|
||
|
|
}
|
||
|
|
|
||
|
|
const pct = ((index / sampleCount) * 100).toFixed(2);
|
||
|
|
return `${rgbString(color)} ${pct}%`;
|
||
|
|
});
|
||
|
|
|
||
|
|
return `linear-gradient(to bottom, ${stops.join(", ")})`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function applySunRulerGradient(referenceDate = new Date()) {
|
||
|
|
const geo = getActiveGeoForRuler();
|
||
|
|
if (!geo) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const gradient = buildSunRulerGradient(geo, referenceDate);
|
||
|
|
if (!gradient) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const rulerColumns = document.querySelectorAll(".toastui-calendar-timegrid-time-column");
|
||
|
|
rulerColumns.forEach((column) => {
|
||
|
|
column.style.backgroundImage = gradient;
|
||
|
|
column.style.backgroundRepeat = "no-repeat";
|
||
|
|
column.style.backgroundSize = "100% 100%";
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeDateLike(value) {
|
||
|
|
if (value instanceof Date) {
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
if (value && typeof value.getTime === "function") {
|
||
|
|
return new Date(value.getTime());
|
||
|
|
}
|
||
|
|
return new Date(value);
|
||
|
|
}
|
||
|
|
|
||
|
|
function getTimeParts(dateLike) {
|
||
|
|
const date = normalizeDateLike(dateLike);
|
||
|
|
const hours = date.getHours();
|
||
|
|
const minutes = date.getMinutes();
|
||
|
|
return {
|
||
|
|
hours,
|
||
|
|
minutes,
|
||
|
|
totalMinutes: hours * 60 + minutes
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatHourStyle(dateLike) {
|
||
|
|
const { totalMinutes } = getTimeParts(dateLike);
|
||
|
|
return `${Math.floor(totalMinutes / 60)}hr`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatMinuteStyle(dateLike) {
|
||
|
|
const { totalMinutes } = getTimeParts(dateLike);
|
||
|
|
return `${totalMinutes}m`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatSecondStyle(dateLike) {
|
||
|
|
const { totalMinutes } = getTimeParts(dateLike);
|
||
|
|
const totalSeconds = totalMinutes * 60;
|
||
|
|
return `${totalSeconds}s`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatCalendarTime(dateLike) {
|
||
|
|
if (currentTimeFormat === "hours") {
|
||
|
|
return formatHourStyle(dateLike);
|
||
|
|
}
|
||
|
|
if (currentTimeFormat === "seconds") {
|
||
|
|
return formatSecondStyle(dateLike);
|
||
|
|
}
|
||
|
|
return formatMinuteStyle(dateLike);
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatCalendarTimeFromTemplatePayload(payload) {
|
||
|
|
if (payload && typeof payload.hour === "number") {
|
||
|
|
const hours = payload.hour;
|
||
|
|
const minutes = typeof payload.minutes === "number" ? payload.minutes : 0;
|
||
|
|
const totalMinutes = hours * 60 + minutes;
|
||
|
|
|
||
|
|
if (currentTimeFormat === "hours") {
|
||
|
|
return `${Math.floor(totalMinutes / 60)}hr`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (currentTimeFormat === "seconds") {
|
||
|
|
return `${totalMinutes * 60}s`;
|
||
|
|
}
|
||
|
|
|
||
|
|
return `${totalMinutes}m`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (payload && payload.time) {
|
||
|
|
return formatCalendarTime(payload.time);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (currentTimeFormat === "hours") {
|
||
|
|
return "12am";
|
||
|
|
}
|
||
|
|
if (currentTimeFormat === "seconds") {
|
||
|
|
return "0s";
|
||
|
|
}
|
||
|
|
return "0m";
|
||
|
|
}
|
||
|
|
|
||
|
|
function getMoonPhaseGlyph(phaseName) {
|
||
|
|
if (phaseName === "New Moon") return "🌑";
|
||
|
|
if (phaseName === "Waxing Crescent") return "🌒";
|
||
|
|
if (phaseName === "First Quarter") return "🌓";
|
||
|
|
if (phaseName === "Waxing Gibbous") return "🌔";
|
||
|
|
if (phaseName === "Full Moon") return "🌕";
|
||
|
|
if (phaseName === "Waning Gibbous") return "🌖";
|
||
|
|
if (phaseName === "Last Quarter") return "🌗";
|
||
|
|
return "🌘";
|
||
|
|
}
|
||
|
|
|
||
|
|
function applyDynamicNowIndicatorVisual(referenceDate = new Date()) {
|
||
|
|
if (!currentGeo || !window.SunCalc) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const labelEl = document.querySelector(
|
||
|
|
".toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-current-time"
|
||
|
|
);
|
||
|
|
const markerEl = document.querySelector(
|
||
|
|
".toastui-calendar-timegrid .toastui-calendar-timegrid-now-indicator .toastui-calendar-timegrid-now-indicator-marker"
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!labelEl || !markerEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const sunPosition = window.SunCalc.getPosition(referenceDate, currentGeo.latitude, currentGeo.longitude);
|
||
|
|
const sunAltitudeDeg = (sunPosition.altitude * 180) / Math.PI;
|
||
|
|
const isSunMode = sunAltitudeDeg >= -4;
|
||
|
|
|
||
|
|
let icon = "☀️";
|
||
|
|
let visualKey = "sun-0";
|
||
|
|
|
||
|
|
labelEl.classList.remove("is-sun", "is-moon");
|
||
|
|
markerEl.classList.remove("is-sun", "is-moon");
|
||
|
|
|
||
|
|
if (isSunMode) {
|
||
|
|
const intensity = clamp((sunAltitudeDeg + 4) / 70, 0, 1);
|
||
|
|
const intensityPercent = Math.round(intensity * 100);
|
||
|
|
|
||
|
|
icon = "☀️";
|
||
|
|
visualKey = `sun-${intensityPercent}`;
|
||
|
|
|
||
|
|
labelEl.classList.add("is-sun");
|
||
|
|
markerEl.classList.add("is-sun");
|
||
|
|
|
||
|
|
labelEl.style.setProperty("--sun-glow-size", `${Math.round(8 + intensity * 16)}px`);
|
||
|
|
labelEl.style.setProperty("--sun-glow-alpha", (0.35 + intensity * 0.55).toFixed(2));
|
||
|
|
markerEl.style.setProperty("--sun-marker-glow-size", `${Math.round(10 + intensity * 24)}px`);
|
||
|
|
markerEl.style.setProperty("--sun-marker-ray-opacity", (0.45 + intensity * 0.5).toFixed(2));
|
||
|
|
|
||
|
|
labelEl.title = `Sun altitude ${sunAltitudeDeg.toFixed(1)}°`;
|
||
|
|
} else {
|
||
|
|
const moonIllum = window.SunCalc.getMoonIllumination(referenceDate);
|
||
|
|
const moonPct = Math.round(moonIllum.fraction * 100);
|
||
|
|
const moonPhaseName = getMoonPhaseName(moonIllum.phase);
|
||
|
|
|
||
|
|
icon = getMoonPhaseGlyph(moonPhaseName);
|
||
|
|
visualKey = `moon-${moonPct}-${moonPhaseName}`;
|
||
|
|
|
||
|
|
labelEl.classList.add("is-moon");
|
||
|
|
markerEl.classList.add("is-moon");
|
||
|
|
|
||
|
|
labelEl.style.setProperty("--moon-glow-alpha", (0.2 + moonIllum.fraction * 0.45).toFixed(2));
|
||
|
|
markerEl.style.setProperty("--moon-glow-alpha", (0.2 + moonIllum.fraction * 0.45).toFixed(2));
|
||
|
|
|
||
|
|
labelEl.title = `${moonPhaseName} (${moonPct}%)`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (labelEl.dataset.celestialKey !== visualKey) {
|
||
|
|
labelEl.innerHTML = [
|
||
|
|
'<span class="toastui-calendar-template-timegridNowIndicatorLabel now-celestial-chip">',
|
||
|
|
`<span class="now-celestial-icon">${icon}</span>`,
|
||
|
|
"</span>"
|
||
|
|
].join("");
|
||
|
|
labelEl.dataset.celestialKey = visualKey;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function convertAxisTimeToMinutes(text) {
|
||
|
|
const normalized = String(text || "").trim().toLowerCase();
|
||
|
|
if (!normalized) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const minuteMatch = normalized.match(/^(\d{1,4})m$/);
|
||
|
|
if (minuteMatch) {
|
||
|
|
return `${Number(minuteMatch[1])}m`;
|
||
|
|
}
|
||
|
|
|
||
|
|
const secondMatch = normalized.match(/^(\d{1,6})s$/);
|
||
|
|
if (secondMatch) {
|
||
|
|
return `${Math.floor(Number(secondMatch[1]) / 60)}m`;
|
||
|
|
}
|
||
|
|
|
||
|
|
const hourMatch = normalized.match(/^(\d{1,2})hr$/);
|
||
|
|
if (hourMatch) {
|
||
|
|
return `${Number(hourMatch[1]) * 60}m`;
|
||
|
|
}
|
||
|
|
|
||
|
|
const ampmMatch = normalized.match(/^(\d{1,2})(?::(\d{2}))?(?::(\d{2}))?\s*(am|pm)$/);
|
||
|
|
if (ampmMatch) {
|
||
|
|
let hour = Number(ampmMatch[1]) % 12;
|
||
|
|
const minutes = Number(ampmMatch[2] || "0");
|
||
|
|
const suffix = ampmMatch[3];
|
||
|
|
if (suffix === "pm") {
|
||
|
|
hour += 12;
|
||
|
|
}
|
||
|
|
return `${hour * 60 + minutes}m`;
|
||
|
|
}
|
||
|
|
|
||
|
|
const twentyFourMatch = normalized.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
|
||
|
|
if (twentyFourMatch) {
|
||
|
|
const hour = Number(twentyFourMatch[1]);
|
||
|
|
const minutes = Number(twentyFourMatch[2]);
|
||
|
|
return `${hour * 60 + minutes}m`;
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function convertAxisTimeToSeconds(text) {
|
||
|
|
const minuteLabel = convertAxisTimeToMinutes(text);
|
||
|
|
if (!minuteLabel) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const minutes = Number(minuteLabel.replace("m", ""));
|
||
|
|
if (Number.isNaN(minutes)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return `${minutes * 60}s`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function convertAxisTimeToHours(text) {
|
||
|
|
const minuteLabel = convertAxisTimeToMinutes(text);
|
||
|
|
if (!minuteLabel) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const minutes = Number(minuteLabel.replace("m", ""));
|
||
|
|
if (Number.isNaN(minutes)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return `${Math.floor(minutes / 60)}hr`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function forceAxisLabelFormat() {
|
||
|
|
const labelNodes = document.querySelectorAll(
|
||
|
|
".toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-time-label"
|
||
|
|
);
|
||
|
|
|
||
|
|
labelNodes.forEach((node) => {
|
||
|
|
if (!node.dataset.originalLabel) {
|
||
|
|
node.dataset.originalLabel = node.textContent;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (currentTimeFormat === "minutes") {
|
||
|
|
const converted = convertAxisTimeToMinutes(node.dataset.originalLabel);
|
||
|
|
if (converted) {
|
||
|
|
node.textContent = converted;
|
||
|
|
}
|
||
|
|
} else if (currentTimeFormat === "seconds") {
|
||
|
|
const converted = convertAxisTimeToSeconds(node.dataset.originalLabel);
|
||
|
|
if (converted) {
|
||
|
|
node.textContent = converted;
|
||
|
|
}
|
||
|
|
} else if (currentTimeFormat === "hours") {
|
||
|
|
const converted = convertAxisTimeToHours(node.dataset.originalLabel);
|
||
|
|
if (converted) {
|
||
|
|
node.textContent = converted;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
node.textContent = node.dataset.originalLabel;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function getVisibleWeekDates() {
|
||
|
|
if (typeof calendar.getDateRangeStart !== "function") {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
const rangeStart = calendar.getDateRangeStart();
|
||
|
|
if (!rangeStart) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
const startDateLike = normalizeDateLike(rangeStart);
|
||
|
|
const startDate = new Date(
|
||
|
|
startDateLike.getFullYear(),
|
||
|
|
startDateLike.getMonth(),
|
||
|
|
startDateLike.getDate(),
|
||
|
|
0,
|
||
|
|
0,
|
||
|
|
0,
|
||
|
|
0
|
||
|
|
);
|
||
|
|
|
||
|
|
return Array.from({ length: 7 }, (_, dayOffset) => {
|
||
|
|
const day = new Date(startDate);
|
||
|
|
day.setDate(startDate.getDate() + dayOffset);
|
||
|
|
return day;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildMonthSpans(days) {
|
||
|
|
if (!Array.isArray(days) || days.length === 0) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
const monthFormatter = new Intl.DateTimeFormat(undefined, {
|
||
|
|
month: "long",
|
||
|
|
year: "numeric"
|
||
|
|
});
|
||
|
|
|
||
|
|
const spans = [];
|
||
|
|
let currentStart = 1;
|
||
|
|
let currentMonth = days[0].getMonth();
|
||
|
|
let currentYear = days[0].getFullYear();
|
||
|
|
|
||
|
|
for (let index = 1; index <= days.length; index += 1) {
|
||
|
|
const day = days[index];
|
||
|
|
const monthChanged = !day || day.getMonth() !== currentMonth || day.getFullYear() !== currentYear;
|
||
|
|
|
||
|
|
if (!monthChanged) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const spanEnd = index;
|
||
|
|
spans.push({
|
||
|
|
start: currentStart,
|
||
|
|
end: spanEnd,
|
||
|
|
label: monthFormatter.format(new Date(currentYear, currentMonth, 1))
|
||
|
|
});
|
||
|
|
|
||
|
|
if (day) {
|
||
|
|
currentStart = index + 1;
|
||
|
|
currentMonth = day.getMonth();
|
||
|
|
currentYear = day.getFullYear();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return spans;
|
||
|
|
}
|
||
|
|
|
||
|
|
function syncMonthStripGeometry() {
|
||
|
|
if (!monthStripEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const calendarEl = document.getElementById("calendar");
|
||
|
|
if (!calendarEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const dayNameItems = calendarEl.querySelectorAll(
|
||
|
|
".toastui-calendar-week-view-day-names .toastui-calendar-day-name-item.toastui-calendar-week"
|
||
|
|
);
|
||
|
|
|
||
|
|
if (dayNameItems.length < 7) {
|
||
|
|
monthStripEl.style.paddingLeft = "0";
|
||
|
|
monthStripEl.style.paddingRight = "0";
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const calendarRect = calendarEl.getBoundingClientRect();
|
||
|
|
const firstRect = dayNameItems[0].getBoundingClientRect();
|
||
|
|
const lastRect = dayNameItems[6].getBoundingClientRect();
|
||
|
|
|
||
|
|
const leftPad = Math.max(0, firstRect.left - calendarRect.left);
|
||
|
|
const rightPad = Math.max(0, calendarRect.right - lastRect.right);
|
||
|
|
|
||
|
|
monthStripEl.style.paddingLeft = `${leftPad}px`;
|
||
|
|
monthStripEl.style.paddingRight = `${rightPad}px`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateMonthStrip() {
|
||
|
|
if (!monthStripEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const days = getVisibleWeekDates();
|
||
|
|
const spans = buildMonthSpans(days);
|
||
|
|
|
||
|
|
monthStripEl.replaceChildren();
|
||
|
|
|
||
|
|
if (!spans.length) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const trackEl = document.createElement("div");
|
||
|
|
trackEl.className = "month-strip-track";
|
||
|
|
|
||
|
|
spans.forEach((span) => {
|
||
|
|
const segmentEl = document.createElement("div");
|
||
|
|
segmentEl.className = "month-strip-segment";
|
||
|
|
segmentEl.style.gridColumn = `${span.start} / ${span.end + 1}`;
|
||
|
|
segmentEl.textContent = span.label;
|
||
|
|
trackEl.appendChild(segmentEl);
|
||
|
|
});
|
||
|
|
|
||
|
|
monthStripEl.appendChild(trackEl);
|
||
|
|
syncMonthStripGeometry();
|
||
|
|
}
|
||
|
|
|
||
|
|
function createCalendarTemplates() {
|
||
|
|
const weekdayRulerLookup = buildWeekdayRulerLookup(referenceData?.planets);
|
||
|
|
|
||
|
|
// TIME / SIGN / NAME formatter for week time plates.
|
||
|
|
// This intentionally keeps each event compact and visually consistent.
|
||
|
|
const getPlateFields = (event) => {
|
||
|
|
const fromRawSign = event?.raw?.planetSymbol;
|
||
|
|
const fromRawName = event?.raw?.planetName;
|
||
|
|
|
||
|
|
if (fromRawSign || fromRawName) {
|
||
|
|
return {
|
||
|
|
sign: fromRawSign || "",
|
||
|
|
name: fromRawName || ""
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fallback parser for any time event that does not provide `raw` planet metadata.
|
||
|
|
// Example title pattern: "♂ Mars · The Tower"
|
||
|
|
const title = String(event?.title || "").trim();
|
||
|
|
const beforeTarot = title.split("·")[0].trim();
|
||
|
|
const parts = beforeTarot.split(/\s+/).filter(Boolean);
|
||
|
|
|
||
|
|
if (parts.length >= 2) {
|
||
|
|
return {
|
||
|
|
sign: parts[0],
|
||
|
|
name: parts.slice(1).join(" ")
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
sign: "",
|
||
|
|
name: beforeTarot
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
// Returns exactly three lines for the event block text:
|
||
|
|
// 1) TIME 2) SIGN 3) NAME
|
||
|
|
const formatEventPlateText = (event) => {
|
||
|
|
const timeLabel = formatCalendarTime(event.start);
|
||
|
|
const { sign, name } = getPlateFields(event);
|
||
|
|
const safeName = name || String(event?.title || "").trim();
|
||
|
|
const safeSign = sign || "•";
|
||
|
|
return `${timeLabel}\n${safeSign}\n${safeName}`;
|
||
|
|
};
|
||
|
|
|
||
|
|
const renderWeekDayHeader = (weekDayNameData) => {
|
||
|
|
const dateNumber = String(weekDayNameData?.date ?? "").padStart(2, "0");
|
||
|
|
const dayLabel = String(weekDayNameData?.dayName || "");
|
||
|
|
const ruler = weekdayRulerLookup[weekDayNameData?.day] || { symbol: "•", name: "" };
|
||
|
|
|
||
|
|
return [
|
||
|
|
'<div class="weekday-header-template">',
|
||
|
|
`<span class="weekday-header-number">${dateNumber}</span>`,
|
||
|
|
`<span class="weekday-header-name">${dayLabel}</span>`,
|
||
|
|
`<span class="weekday-header-ruler" title="${ruler.name}">${ruler.symbol}</span>`,
|
||
|
|
"</div>"
|
||
|
|
].join("");
|
||
|
|
};
|
||
|
|
|
||
|
|
return {
|
||
|
|
timegridDisplayPrimaryTime: (props) => formatCalendarTimeFromTemplatePayload(props),
|
||
|
|
timegridDisplayTime: (props) => formatCalendarTimeFromTemplatePayload(props),
|
||
|
|
timegridNowIndicatorLabel: (props) => formatCalendarTimeFromTemplatePayload(props),
|
||
|
|
weekDayName: (weekDayNameData) => renderWeekDayHeader(weekDayNameData),
|
||
|
|
time: (event) => formatEventPlateText(event)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function applyTimeFormatTemplates() {
|
||
|
|
calendar.setOptions({
|
||
|
|
template: createCalendarTemplates()
|
||
|
|
});
|
||
|
|
calendar.render();
|
||
|
|
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
forceAxisLabelFormat();
|
||
|
|
applySunRulerGradient();
|
||
|
|
applyDynamicNowIndicatorVisual();
|
||
|
|
updateMonthStrip();
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
forceAxisLabelFormat();
|
||
|
|
applySunRulerGradient();
|
||
|
|
applyDynamicNowIndicatorVisual();
|
||
|
|
updateMonthStrip();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function setStatus(text) {
|
||
|
|
if (!statusEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
statusEl.textContent = text;
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeGeoForSky(geo) {
|
||
|
|
const latitude = Number(geo?.latitude);
|
||
|
|
const longitude = Number(geo?.longitude);
|
||
|
|
|
||
|
|
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
latitude: Number(latitude.toFixed(4)),
|
||
|
|
longitude: Number(longitude.toFixed(4))
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildStellariumObserverUrl(geo) {
|
||
|
|
const normalizedGeo = normalizeGeoForSky(geo);
|
||
|
|
if (!normalizedGeo) {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
const stellariumUrl = new URL("https://stellarium-web.org/");
|
||
|
|
stellariumUrl.searchParams.set("lat", String(normalizedGeo.latitude));
|
||
|
|
stellariumUrl.searchParams.set("lng", String(normalizedGeo.longitude));
|
||
|
|
stellariumUrl.searchParams.set("elev", "0");
|
||
|
|
stellariumUrl.searchParams.set("date", new Date().toISOString());
|
||
|
|
stellariumUrl.searchParams.set("az", "0");
|
||
|
|
stellariumUrl.searchParams.set("alt", "90");
|
||
|
|
stellariumUrl.searchParams.set("fov", "180");
|
||
|
|
|
||
|
|
return stellariumUrl.toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
function syncNowSkyBackground(geo, force = false) {
|
||
|
|
if (!nowSkyLayerEl || !geo) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const normalizedGeo = normalizeGeoForSky(geo);
|
||
|
|
if (!normalizedGeo) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const geoKey = `${normalizedGeo.latitude.toFixed(4)},${normalizedGeo.longitude.toFixed(4)}`;
|
||
|
|
const stellariumUrl = buildStellariumObserverUrl(normalizedGeo);
|
||
|
|
if (!stellariumUrl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!force && geoKey === lastNowSkyGeoKey && stellariumUrl === lastNowSkySourceUrl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (stellariumUrl === lastNowSkySourceUrl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
nowSkyLayerEl.src = stellariumUrl;
|
||
|
|
lastNowSkyGeoKey = geoKey;
|
||
|
|
lastNowSkySourceUrl = stellariumUrl;
|
||
|
|
}
|
||
|
|
|
||
|
|
function syncNowPanelTheme(referenceDate = new Date()) {
|
||
|
|
if (!nowPanelEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!currentGeo || !window.SunCalc) {
|
||
|
|
nowPanelEl.classList.remove("is-day");
|
||
|
|
nowPanelEl.classList.add("is-night");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const sunPosition = window.SunCalc.getPosition(referenceDate, currentGeo.latitude, currentGeo.longitude);
|
||
|
|
const sunAltitudeDeg = (sunPosition.altitude * 180) / Math.PI;
|
||
|
|
const isDaytime = sunAltitudeDeg >= -4;
|
||
|
|
|
||
|
|
nowPanelEl.classList.toggle("is-day", isDaytime);
|
||
|
|
nowPanelEl.classList.toggle("is-night", !isDaytime);
|
||
|
|
}
|
||
|
|
|
||
|
|
function openSettingsPopup() {
|
||
|
|
if (!settingsPopupEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
settingsPopupEl.hidden = false;
|
||
|
|
if (openSettingsEl) {
|
||
|
|
openSettingsEl.setAttribute("aria-expanded", "true");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeSettingsPopup() {
|
||
|
|
if (!settingsPopupEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
settingsPopupEl.hidden = true;
|
||
|
|
if (openSettingsEl) {
|
||
|
|
openSettingsEl.setAttribute("aria-expanded", "false");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function loadSidebarCollapsedState(storageKey) {
|
||
|
|
try {
|
||
|
|
const raw = window.localStorage?.getItem(storageKey);
|
||
|
|
if (raw === "1") {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (raw === "0") {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function saveSidebarCollapsedState(storageKey, collapsed) {
|
||
|
|
try {
|
||
|
|
window.localStorage?.setItem(storageKey, collapsed ? "1" : "0");
|
||
|
|
} catch {
|
||
|
|
// Ignore storage failures silently.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function initializeSidebarPopouts() {
|
||
|
|
const layouts = document.querySelectorAll(".planet-layout, .tarot-layout, .kab-layout");
|
||
|
|
|
||
|
|
layouts.forEach((layout, index) => {
|
||
|
|
if (!(layout instanceof HTMLElement)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const panel = Array.from(layout.children).find((child) => (
|
||
|
|
child instanceof HTMLElement
|
||
|
|
&& child.matches("aside.planet-list-panel, aside.tarot-list-panel, aside.kab-tree-panel")
|
||
|
|
));
|
||
|
|
|
||
|
|
if (!(panel instanceof HTMLElement) || panel.dataset.sidebarPopoutReady === "1") {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const header = panel.querySelector(".planet-list-header, .tarot-list-header");
|
||
|
|
if (!(header instanceof HTMLElement)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
panel.dataset.sidebarPopoutReady = "1";
|
||
|
|
|
||
|
|
const sectionId = layout.closest("section")?.id || `layout-${index + 1}`;
|
||
|
|
const panelId = panel.id || `${sectionId}-entry-panel`;
|
||
|
|
panel.id = panelId;
|
||
|
|
|
||
|
|
const storageKey = `${SIDEBAR_COLLAPSE_STORAGE_PREFIX}${sectionId}`;
|
||
|
|
|
||
|
|
const collapseBtn = document.createElement("button");
|
||
|
|
collapseBtn.type = "button";
|
||
|
|
collapseBtn.className = "sidebar-toggle-inline";
|
||
|
|
collapseBtn.textContent = "Hide Panel";
|
||
|
|
collapseBtn.setAttribute("aria-label", "Hide entry panel");
|
||
|
|
collapseBtn.setAttribute("aria-controls", panelId);
|
||
|
|
header.appendChild(collapseBtn);
|
||
|
|
|
||
|
|
const openBtn = document.createElement("button");
|
||
|
|
openBtn.type = "button";
|
||
|
|
openBtn.className = "sidebar-popout-open";
|
||
|
|
openBtn.textContent = "Show Panel";
|
||
|
|
openBtn.setAttribute("aria-label", "Show entry panel");
|
||
|
|
openBtn.setAttribute("aria-controls", panelId);
|
||
|
|
openBtn.hidden = true;
|
||
|
|
layout.appendChild(openBtn);
|
||
|
|
|
||
|
|
const applyCollapsedState = (collapsed, persist = true) => {
|
||
|
|
layout.classList.toggle("layout-sidebar-collapsed", collapsed);
|
||
|
|
collapseBtn.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||
|
|
openBtn.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||
|
|
openBtn.hidden = !collapsed;
|
||
|
|
|
||
|
|
if (persist) {
|
||
|
|
saveSidebarCollapsedState(storageKey, collapsed);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
collapseBtn.addEventListener("click", () => {
|
||
|
|
applyCollapsedState(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
openBtn.addEventListener("click", () => {
|
||
|
|
applyCollapsedState(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
const storedCollapsed = loadSidebarCollapsedState(storageKey);
|
||
|
|
applyCollapsedState(storedCollapsed == null ? DEFAULT_DATASET_ENTRY_COLLAPSED : storedCollapsed, false);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function initializeDetailPopouts() {
|
||
|
|
const layouts = document.querySelectorAll(".planet-layout, .tarot-layout, .kab-layout");
|
||
|
|
|
||
|
|
layouts.forEach((layout, index) => {
|
||
|
|
if (!(layout instanceof HTMLElement)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const detailPanel = Array.from(layout.children).find((child) => (
|
||
|
|
child instanceof HTMLElement
|
||
|
|
&& child.matches("section.planet-detail-panel, section.tarot-detail-panel, section.kab-detail-panel")
|
||
|
|
));
|
||
|
|
|
||
|
|
if (!(detailPanel instanceof HTMLElement) || detailPanel.dataset.detailPopoutReady === "1") {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const heading = detailPanel.querySelector(".planet-detail-heading, .tarot-detail-heading");
|
||
|
|
if (!(heading instanceof HTMLElement)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
detailPanel.dataset.detailPopoutReady = "1";
|
||
|
|
|
||
|
|
const sectionId = layout.closest("section")?.id || `layout-${index + 1}`;
|
||
|
|
const panelId = detailPanel.id || `${sectionId}-detail-panel`;
|
||
|
|
detailPanel.id = panelId;
|
||
|
|
|
||
|
|
const detailStorageKey = `${DETAIL_COLLAPSE_STORAGE_PREFIX}${sectionId}`;
|
||
|
|
const sidebarStorageKey = `${SIDEBAR_COLLAPSE_STORAGE_PREFIX}${sectionId}`;
|
||
|
|
|
||
|
|
const collapseBtn = document.createElement("button");
|
||
|
|
collapseBtn.type = "button";
|
||
|
|
collapseBtn.className = "detail-toggle-inline";
|
||
|
|
collapseBtn.textContent = "Hide Detail";
|
||
|
|
collapseBtn.setAttribute("aria-label", "Hide detail panel");
|
||
|
|
collapseBtn.setAttribute("aria-controls", panelId);
|
||
|
|
heading.appendChild(collapseBtn);
|
||
|
|
|
||
|
|
const openBtn = document.createElement("button");
|
||
|
|
openBtn.type = "button";
|
||
|
|
openBtn.className = "detail-popout-open";
|
||
|
|
openBtn.textContent = "Show Detail";
|
||
|
|
openBtn.setAttribute("aria-label", "Show detail panel");
|
||
|
|
openBtn.setAttribute("aria-controls", panelId);
|
||
|
|
openBtn.hidden = true;
|
||
|
|
layout.appendChild(openBtn);
|
||
|
|
|
||
|
|
const applyCollapsedState = (collapsed, persist = true) => {
|
||
|
|
if (collapsed && layout.classList.contains("layout-sidebar-collapsed")) {
|
||
|
|
layout.classList.remove("layout-sidebar-collapsed");
|
||
|
|
const sidebarOpenBtn = layout.querySelector(".sidebar-popout-open");
|
||
|
|
if (sidebarOpenBtn instanceof HTMLButtonElement) {
|
||
|
|
sidebarOpenBtn.hidden = true;
|
||
|
|
sidebarOpenBtn.setAttribute("aria-expanded", "true");
|
||
|
|
}
|
||
|
|
const sidebarCollapseBtn = layout.querySelector(".sidebar-toggle-inline");
|
||
|
|
if (sidebarCollapseBtn instanceof HTMLButtonElement) {
|
||
|
|
sidebarCollapseBtn.setAttribute("aria-expanded", "true");
|
||
|
|
}
|
||
|
|
saveSidebarCollapsedState(sidebarStorageKey, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
layout.classList.toggle("layout-detail-collapsed", collapsed);
|
||
|
|
collapseBtn.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||
|
|
openBtn.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||
|
|
openBtn.hidden = !collapsed;
|
||
|
|
|
||
|
|
if (persist) {
|
||
|
|
saveSidebarCollapsedState(detailStorageKey, collapsed);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
collapseBtn.addEventListener("click", () => {
|
||
|
|
applyCollapsedState(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
openBtn.addEventListener("click", () => {
|
||
|
|
applyCollapsedState(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
const storedCollapsed = loadSidebarCollapsedState(detailStorageKey);
|
||
|
|
const shouldForceOpenForTarot = sectionId === "tarot-section";
|
||
|
|
const initialCollapsed = shouldForceOpenForTarot
|
||
|
|
? false
|
||
|
|
: (storedCollapsed == null ? DEFAULT_DATASET_DETAIL_COLLAPSED : storedCollapsed);
|
||
|
|
applyCollapsedState(initialCollapsed, false);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function setActiveSection(nextSection) {
|
||
|
|
const normalized = nextSection === "home" || nextSection === "calendar" || nextSection === "holidays" || nextSection === "tarot" || nextSection === "astronomy" || nextSection === "planets" || nextSection === "cycles" || nextSection === "natal" || nextSection === "elements" || nextSection === "iching" || nextSection === "kabbalah" || nextSection === "kabbalah-tree" || nextSection === "cube" || nextSection === "alphabet" || nextSection === "numbers" || nextSection === "zodiac" || nextSection === "quiz" || nextSection === "gods" || nextSection === "enochian"
|
||
|
|
? nextSection
|
||
|
|
: "home";
|
||
|
|
activeSection = normalized;
|
||
|
|
|
||
|
|
const isHomeOpen = activeSection === "home";
|
||
|
|
const isCalendarOpen = activeSection === "calendar";
|
||
|
|
const isHolidaysOpen = activeSection === "holidays";
|
||
|
|
const isCalendarMenuOpen = isCalendarOpen || isHolidaysOpen;
|
||
|
|
const isTarotOpen = activeSection === "tarot";
|
||
|
|
const isAstronomyOpen = activeSection === "astronomy";
|
||
|
|
const isPlanetOpen = activeSection === "planets";
|
||
|
|
const isCyclesOpen = activeSection === "cycles";
|
||
|
|
const isNatalOpen = activeSection === "natal";
|
||
|
|
const isZodiacOpen = activeSection === "zodiac";
|
||
|
|
const isAstronomyMenuOpen = isAstronomyOpen || isPlanetOpen || isCyclesOpen || isZodiacOpen || isNatalOpen;
|
||
|
|
const isElementsOpen = activeSection === "elements";
|
||
|
|
const isIChingOpen = activeSection === "iching";
|
||
|
|
const isKabbalahOpen = activeSection === "kabbalah";
|
||
|
|
const isKabbalahTreeOpen = activeSection === "kabbalah-tree";
|
||
|
|
const isCubeOpen = activeSection === "cube";
|
||
|
|
const isKabbalahMenuOpen = isKabbalahOpen || isKabbalahTreeOpen || isCubeOpen;
|
||
|
|
const isAlphabetOpen = activeSection === "alphabet";
|
||
|
|
const isNumbersOpen = activeSection === "numbers";
|
||
|
|
const isQuizOpen = activeSection === "quiz";
|
||
|
|
const isGodsOpen = activeSection === "gods";
|
||
|
|
const isEnochianOpen = activeSection === "enochian";
|
||
|
|
|
||
|
|
if (calendarSectionEl) {
|
||
|
|
calendarSectionEl.hidden = !isCalendarOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (holidaySectionEl) {
|
||
|
|
holidaySectionEl.hidden = !isHolidaysOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (tarotSectionEl) {
|
||
|
|
tarotSectionEl.hidden = !isTarotOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (astronomySectionEl) {
|
||
|
|
astronomySectionEl.hidden = !isAstronomyOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (planetSectionEl) {
|
||
|
|
planetSectionEl.hidden = !isPlanetOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (cyclesSectionEl) {
|
||
|
|
cyclesSectionEl.hidden = !isCyclesOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (natalSectionEl) {
|
||
|
|
natalSectionEl.hidden = !isNatalOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (elementsSectionEl) {
|
||
|
|
elementsSectionEl.hidden = !isElementsOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ichingSectionEl) {
|
||
|
|
ichingSectionEl.hidden = !isIChingOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (kabbalahSectionEl) {
|
||
|
|
kabbalahSectionEl.hidden = !isKabbalahOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (kabbalahTreeSectionEl) {
|
||
|
|
kabbalahTreeSectionEl.hidden = !isKabbalahTreeOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (cubeSectionEl) {
|
||
|
|
cubeSectionEl.hidden = !isCubeOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (alphabetSectionEl) {
|
||
|
|
alphabetSectionEl.hidden = !isAlphabetOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (numbersSectionEl) {
|
||
|
|
numbersSectionEl.hidden = !isNumbersOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (zodiacSectionEl) {
|
||
|
|
zodiacSectionEl.hidden = !isZodiacOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (quizSectionEl) {
|
||
|
|
quizSectionEl.hidden = !isQuizOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (godsSectionEl) {
|
||
|
|
godsSectionEl.hidden = !isGodsOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (enochianSectionEl) {
|
||
|
|
enochianSectionEl.hidden = !isEnochianOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (nowPanelEl) {
|
||
|
|
nowPanelEl.hidden = !isHomeOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (monthStripEl) {
|
||
|
|
monthStripEl.hidden = !isHomeOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (calendarEl) {
|
||
|
|
calendarEl.hidden = !isHomeOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openCalendarEl) {
|
||
|
|
openCalendarEl.setAttribute("aria-pressed", isCalendarMenuOpen ? "true" : "false");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openCalendarMonthsEl) {
|
||
|
|
openCalendarMonthsEl.classList.toggle("is-active", isCalendarOpen);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openHolidaysEl) {
|
||
|
|
openHolidaysEl.classList.toggle("is-active", isHolidaysOpen);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openTarotEl) {
|
||
|
|
openTarotEl.setAttribute("aria-pressed", isTarotOpen ? "true" : "false");
|
||
|
|
}
|
||
|
|
|
||
|
|
applyTarotSpreadViewState();
|
||
|
|
|
||
|
|
if (openAstronomyEl) {
|
||
|
|
openAstronomyEl.setAttribute("aria-pressed", isAstronomyMenuOpen ? "true" : "false");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openPlanetsEl) {
|
||
|
|
openPlanetsEl.classList.toggle("is-active", isPlanetOpen);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openCyclesEl) {
|
||
|
|
openCyclesEl.classList.toggle("is-active", isCyclesOpen);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openElementsEl) {
|
||
|
|
openElementsEl.setAttribute("aria-pressed", isElementsOpen ? "true" : "false");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openIChingEl) {
|
||
|
|
openIChingEl.setAttribute("aria-pressed", isIChingOpen ? "true" : "false");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openKabbalahEl) {
|
||
|
|
openKabbalahEl.setAttribute("aria-pressed", isKabbalahMenuOpen ? "true" : "false");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openKabbalahTreeEl) {
|
||
|
|
openKabbalahTreeEl.classList.toggle("is-active", isKabbalahTreeOpen);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openKabbalahCubeEl) {
|
||
|
|
openKabbalahCubeEl.classList.toggle("is-active", isCubeOpen);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openAlphabetEl) {
|
||
|
|
openAlphabetEl.setAttribute("aria-pressed", isAlphabetOpen ? "true" : "false");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openNumbersEl) {
|
||
|
|
openNumbersEl.setAttribute("aria-pressed", isNumbersOpen ? "true" : "false");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openZodiacEl) {
|
||
|
|
openZodiacEl.classList.toggle("is-active", isZodiacOpen);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openNatalEl) {
|
||
|
|
openNatalEl.classList.toggle("is-active", isNatalOpen);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openQuizEl) {
|
||
|
|
openQuizEl.setAttribute("aria-pressed", isQuizOpen ? "true" : "false");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openGodsEl) {
|
||
|
|
openGodsEl.setAttribute("aria-pressed", isGodsOpen ? "true" : "false");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openEnochianEl) {
|
||
|
|
openEnochianEl.setAttribute("aria-pressed", isEnochianOpen ? "true" : "false");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!isHomeOpen) {
|
||
|
|
closeSettingsPopup();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isCalendarOpen) {
|
||
|
|
if (typeof ensureCalendarSection === "function" && referenceData) {
|
||
|
|
ensureCalendarSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isHolidaysOpen) {
|
||
|
|
if (typeof ensureHolidaySection === "function" && referenceData) {
|
||
|
|
ensureHolidaySection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isTarotOpen) {
|
||
|
|
if (typeof ensureTarotSection === "function" && referenceData) {
|
||
|
|
ensureTarotSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
if (activeTarotSpread !== null) {
|
||
|
|
renderTarotSpread();
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isPlanetOpen) {
|
||
|
|
if (typeof ensurePlanetSection === "function" && referenceData) {
|
||
|
|
ensurePlanetSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isCyclesOpen) {
|
||
|
|
if (typeof ensureCyclesSection === "function" && referenceData) {
|
||
|
|
ensureCyclesSection(referenceData);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isElementsOpen) {
|
||
|
|
if (typeof ensureElementsSection === "function" && magickDataset) {
|
||
|
|
ensureElementsSection(magickDataset);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isIChingOpen) {
|
||
|
|
if (typeof ensureIChingSection === "function" && referenceData) {
|
||
|
|
ensureIChingSection(referenceData);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isKabbalahTreeOpen) {
|
||
|
|
if (typeof ensureKabbalahSection === "function" && magickDataset) {
|
||
|
|
ensureKabbalahSection(magickDataset);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isCubeOpen) {
|
||
|
|
if (typeof ensureCubeSection === "function" && magickDataset) {
|
||
|
|
ensureCubeSection(magickDataset, referenceData);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isAlphabetOpen) {
|
||
|
|
if (typeof ensureAlphabetSection === "function" && magickDataset) {
|
||
|
|
ensureAlphabetSection(magickDataset, referenceData);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isNumbersOpen) {
|
||
|
|
ensureNumbersSection();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isZodiacOpen) {
|
||
|
|
if (typeof ensureZodiacSection === "function" && referenceData && magickDataset) {
|
||
|
|
ensureZodiacSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isNatalOpen) {
|
||
|
|
if (typeof ensureNatalPanel === "function") {
|
||
|
|
ensureNatalPanel(referenceData);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isQuizOpen) {
|
||
|
|
if (typeof ensureQuizSection === "function" && referenceData && magickDataset) {
|
||
|
|
ensureQuizSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isGodsOpen) {
|
||
|
|
if (typeof ensureGodsSection === "function" && magickDataset) {
|
||
|
|
ensureGodsSection(magickDataset, referenceData);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isEnochianOpen) {
|
||
|
|
if (typeof ensureEnochianSection === "function" && magickDataset) {
|
||
|
|
ensureEnochianSection(magickDataset, referenceData);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
calendar.render();
|
||
|
|
updateMonthStrip();
|
||
|
|
syncNowPanelTheme(new Date());
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function applyCenteredWeekWindow(date) {
|
||
|
|
const startDayOfWeek = getCenteredWeekStartDay(date);
|
||
|
|
calendar.setOptions({
|
||
|
|
week: {
|
||
|
|
...baseWeekOptions,
|
||
|
|
startDayOfWeek
|
||
|
|
}
|
||
|
|
});
|
||
|
|
applyTimeFormatTemplates();
|
||
|
|
calendar.changeView("week");
|
||
|
|
calendar.setDate(date);
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseGeoInput() {
|
||
|
|
const latitude = Number(latEl.value);
|
||
|
|
const longitude = Number(lngEl.value);
|
||
|
|
|
||
|
|
if (Number.isNaN(latitude) || Number.isNaN(longitude)) {
|
||
|
|
throw new Error("Latitude/Longitude must be valid numbers.");
|
||
|
|
}
|
||
|
|
|
||
|
|
return { latitude, longitude };
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeTimeFormat(value) {
|
||
|
|
if (value === "hours") {
|
||
|
|
return "hours";
|
||
|
|
}
|
||
|
|
|
||
|
|
if (value === "seconds") {
|
||
|
|
return "seconds";
|
||
|
|
}
|
||
|
|
|
||
|
|
return "minutes";
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeBirthDate(value) {
|
||
|
|
const normalized = String(value || "").trim();
|
||
|
|
if (!normalized) {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
return /^\d{4}-\d{2}-\d{2}$/.test(normalized) ? normalized : "";
|
||
|
|
}
|
||
|
|
|
||
|
|
function getKnownTarotDeckIds() {
|
||
|
|
const knownDeckIds = new Set();
|
||
|
|
const deckOptions = window.TarotCardImages?.getDeckOptions?.();
|
||
|
|
|
||
|
|
if (Array.isArray(deckOptions)) {
|
||
|
|
deckOptions.forEach((option) => {
|
||
|
|
const id = String(option?.id || "").trim().toLowerCase();
|
||
|
|
if (id) {
|
||
|
|
knownDeckIds.add(id);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!knownDeckIds.size) {
|
||
|
|
knownDeckIds.add(DEFAULT_TAROT_DECK);
|
||
|
|
}
|
||
|
|
|
||
|
|
return knownDeckIds;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getFallbackTarotDeckId() {
|
||
|
|
const deckOptions = window.TarotCardImages?.getDeckOptions?.();
|
||
|
|
if (Array.isArray(deckOptions)) {
|
||
|
|
for (let i = 0; i < deckOptions.length; i += 1) {
|
||
|
|
const id = String(deckOptions[i]?.id || "").trim().toLowerCase();
|
||
|
|
if (id) {
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return DEFAULT_TAROT_DECK;
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeTarotDeck(value) {
|
||
|
|
const normalized = String(value || "").trim().toLowerCase();
|
||
|
|
const knownDeckIds = getKnownTarotDeckIds();
|
||
|
|
|
||
|
|
if (knownDeckIds.has(normalized)) {
|
||
|
|
return normalized;
|
||
|
|
}
|
||
|
|
|
||
|
|
return getFallbackTarotDeckId();
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseStoredNumber(value, fallback) {
|
||
|
|
const parsed = Number(value);
|
||
|
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeSettings(settings) {
|
||
|
|
return {
|
||
|
|
latitude: parseStoredNumber(settings?.latitude, DEFAULT_SETTINGS.latitude),
|
||
|
|
longitude: parseStoredNumber(settings?.longitude, DEFAULT_SETTINGS.longitude),
|
||
|
|
timeFormat: normalizeTimeFormat(settings?.timeFormat),
|
||
|
|
birthDate: normalizeBirthDate(settings?.birthDate),
|
||
|
|
tarotDeck: normalizeTarotDeck(settings?.tarotDeck)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function getResolvedTimeZone() {
|
||
|
|
try {
|
||
|
|
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||
|
|
return String(timeZone || "");
|
||
|
|
} catch {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildBirthDateParts(birthDate) {
|
||
|
|
const normalized = normalizeBirthDate(birthDate);
|
||
|
|
if (!normalized) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const [year, month, day] = normalized.split("-").map((value) => Number(value));
|
||
|
|
if (!year || !month || !day) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const localNoon = new Date(year, month - 1, day, 12, 0, 0, 0);
|
||
|
|
const utcNoon = new Date(Date.UTC(year, month - 1, day, 12, 0, 0, 0));
|
||
|
|
|
||
|
|
return {
|
||
|
|
year,
|
||
|
|
month,
|
||
|
|
day,
|
||
|
|
isoDate: normalized,
|
||
|
|
localNoonIso: localNoon.toISOString(),
|
||
|
|
utcNoonIso: utcNoon.toISOString(),
|
||
|
|
timezoneOffsetMinutesAtNoon: localNoon.getTimezoneOffset()
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildNatalContext(settings) {
|
||
|
|
const normalized = normalizeSettings(settings);
|
||
|
|
const birthDateParts = buildBirthDateParts(normalized.birthDate);
|
||
|
|
const timeZone = getResolvedTimeZone();
|
||
|
|
|
||
|
|
return {
|
||
|
|
latitude: normalized.latitude,
|
||
|
|
longitude: normalized.longitude,
|
||
|
|
birthDate: normalized.birthDate || null,
|
||
|
|
birthDateParts,
|
||
|
|
timeZone: timeZone || "UTC",
|
||
|
|
timezoneOffsetMinutesNow: new Date().getTimezoneOffset(),
|
||
|
|
timezoneOffsetMinutesAtBirthDateNoon: birthDateParts?.timezoneOffsetMinutesAtNoon ?? null
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function emitSettingsUpdated(settings) {
|
||
|
|
const normalized = normalizeSettings(settings);
|
||
|
|
const natalContext = buildNatalContext(normalized);
|
||
|
|
document.dispatchEvent(new CustomEvent("settings:updated", {
|
||
|
|
detail: {
|
||
|
|
settings: normalized,
|
||
|
|
natalContext
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
function loadSavedSettings() {
|
||
|
|
try {
|
||
|
|
const raw = window.localStorage.getItem(SETTINGS_STORAGE_KEY);
|
||
|
|
if (!raw) {
|
||
|
|
return { ...DEFAULT_SETTINGS };
|
||
|
|
}
|
||
|
|
|
||
|
|
const parsed = JSON.parse(raw);
|
||
|
|
return normalizeSettings(parsed);
|
||
|
|
} catch {
|
||
|
|
return { ...DEFAULT_SETTINGS };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function saveSettings(settings) {
|
||
|
|
try {
|
||
|
|
const normalized = normalizeSettings(settings);
|
||
|
|
window.localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(normalized));
|
||
|
|
return true;
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function syncTarotDeckInputOptions() {
|
||
|
|
if (!tarotDeckEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const deckOptions = window.TarotCardImages?.getDeckOptions?.();
|
||
|
|
const previousValue = String(tarotDeckEl.value || "").trim().toLowerCase();
|
||
|
|
tarotDeckEl.innerHTML = "";
|
||
|
|
|
||
|
|
if (!Array.isArray(deckOptions) || !deckOptions.length) {
|
||
|
|
const emptyOption = document.createElement("option");
|
||
|
|
emptyOption.value = DEFAULT_TAROT_DECK;
|
||
|
|
emptyOption.textContent = "No deck manifests found";
|
||
|
|
tarotDeckEl.appendChild(emptyOption);
|
||
|
|
tarotDeckEl.disabled = true;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
tarotDeckEl.disabled = false;
|
||
|
|
|
||
|
|
deckOptions.forEach((option) => {
|
||
|
|
const id = String(option?.id || "").trim().toLowerCase();
|
||
|
|
if (!id) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const label = String(option?.label || id);
|
||
|
|
const optionEl = document.createElement("option");
|
||
|
|
optionEl.value = id;
|
||
|
|
optionEl.textContent = label;
|
||
|
|
tarotDeckEl.appendChild(optionEl);
|
||
|
|
});
|
||
|
|
|
||
|
|
const normalizedPrevious = normalizeTarotDeck(previousValue);
|
||
|
|
tarotDeckEl.value = normalizedPrevious;
|
||
|
|
}
|
||
|
|
|
||
|
|
function applySettingsToInputs(settings) {
|
||
|
|
syncTarotDeckInputOptions();
|
||
|
|
const normalized = normalizeSettings(settings);
|
||
|
|
latEl.value = String(normalized.latitude);
|
||
|
|
lngEl.value = String(normalized.longitude);
|
||
|
|
timeFormatEl.value = normalized.timeFormat;
|
||
|
|
birthDateEl.value = normalized.birthDate;
|
||
|
|
if (tarotDeckEl) {
|
||
|
|
tarotDeckEl.value = normalized.tarotDeck;
|
||
|
|
}
|
||
|
|
if (window.TarotCardImages?.setActiveDeck) {
|
||
|
|
window.TarotCardImages.setActiveDeck(normalized.tarotDeck);
|
||
|
|
}
|
||
|
|
currentTimeFormat = normalized.timeFormat;
|
||
|
|
currentSettings = normalized;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getSettingsFromInputs() {
|
||
|
|
const latitude = Number(latEl.value);
|
||
|
|
const longitude = Number(lngEl.value);
|
||
|
|
|
||
|
|
if (Number.isNaN(latitude) || Number.isNaN(longitude)) {
|
||
|
|
throw new Error("Latitude/Longitude must be valid numbers.");
|
||
|
|
}
|
||
|
|
|
||
|
|
return normalizeSettings({
|
||
|
|
latitude,
|
||
|
|
longitude,
|
||
|
|
timeFormat: normalizeTimeFormat(timeFormatEl.value),
|
||
|
|
birthDate: normalizeBirthDate(birthDateEl.value),
|
||
|
|
tarotDeck: normalizeTarotDeck(tarotDeckEl?.value)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleSaveSettings() {
|
||
|
|
try {
|
||
|
|
const settings = getSettingsFromInputs();
|
||
|
|
applySettingsToInputs(settings);
|
||
|
|
syncNowSkyBackground({ latitude: settings.latitude, longitude: settings.longitude }, true);
|
||
|
|
const didPersist = saveSettings(settings);
|
||
|
|
emitSettingsUpdated(currentSettings);
|
||
|
|
if (activeSection !== "home") {
|
||
|
|
setActiveSection(activeSection);
|
||
|
|
}
|
||
|
|
closeSettingsPopup();
|
||
|
|
void renderWeek();
|
||
|
|
|
||
|
|
if (!didPersist) {
|
||
|
|
setStatus("Settings applied for this session. Browser storage is unavailable.");
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
setStatus(error.message || "Unable to save settings.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function startNowTicker() {
|
||
|
|
if (nowInterval) {
|
||
|
|
clearInterval(nowInterval);
|
||
|
|
}
|
||
|
|
|
||
|
|
const tick = () => {
|
||
|
|
if (!referenceData || !currentGeo || renderInProgress) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const now = new Date();
|
||
|
|
syncNowPanelTheme(now);
|
||
|
|
const currentDayKey = getDateKey(now);
|
||
|
|
if (currentDayKey !== centeredDayKey) {
|
||
|
|
centeredDayKey = currentDayKey;
|
||
|
|
void renderWeek();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
updateNowPanel(referenceData, currentGeo, nowElements, currentTimeFormat);
|
||
|
|
applyDynamicNowIndicatorVisual(now);
|
||
|
|
};
|
||
|
|
|
||
|
|
tick();
|
||
|
|
nowInterval = setInterval(tick, 1000);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function renderWeek() {
|
||
|
|
if (renderInProgress) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
renderInProgress = true;
|
||
|
|
|
||
|
|
try {
|
||
|
|
currentGeo = parseGeoInput();
|
||
|
|
syncNowPanelTheme(new Date());
|
||
|
|
syncNowSkyBackground(currentGeo);
|
||
|
|
|
||
|
|
if (!referenceData || !magickDataset) {
|
||
|
|
setStatus("Loading planetary, sign and decan tarot correspondences...");
|
||
|
|
const [loadedReference, loadedMagick] = await Promise.all([
|
||
|
|
referenceData ? Promise.resolve(referenceData) : loadReferenceData(),
|
||
|
|
magickDataset
|
||
|
|
? Promise.resolve(magickDataset)
|
||
|
|
: loadMagickDataset().catch(() => null)
|
||
|
|
]);
|
||
|
|
|
||
|
|
referenceData = loadedReference;
|
||
|
|
magickDataset = loadedMagick;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof ensureTarotSection === "function") {
|
||
|
|
ensureTarotSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof ensurePlanetSection === "function") {
|
||
|
|
ensurePlanetSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof ensureCyclesSection === "function") {
|
||
|
|
ensureCyclesSection(referenceData);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof ensureIChingSection === "function") {
|
||
|
|
ensureIChingSection(referenceData);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof ensureCalendarSection === "function") {
|
||
|
|
ensureCalendarSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof ensureHolidaySection === "function") {
|
||
|
|
ensureHolidaySection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof ensureNatalPanel === "function") {
|
||
|
|
ensureNatalPanel(referenceData);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof ensureQuizSection === "function") {
|
||
|
|
ensureQuizSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
|
||
|
|
const anchorDate = new Date();
|
||
|
|
centeredDayKey = getDateKey(anchorDate);
|
||
|
|
|
||
|
|
applyCenteredWeekWindow(anchorDate);
|
||
|
|
|
||
|
|
const events = buildWeekEvents(currentGeo, referenceData, anchorDate);
|
||
|
|
calendar.clear();
|
||
|
|
calendar.createEvents(events);
|
||
|
|
applySunRulerGradient(anchorDate);
|
||
|
|
updateMonthStrip();
|
||
|
|
requestAnimationFrame(updateMonthStrip);
|
||
|
|
|
||
|
|
setStatus(`Rendered ${events.length} planetary + tarot events for lat ${currentGeo.latitude}, lng ${currentGeo.longitude}.`);
|
||
|
|
startNowTicker();
|
||
|
|
} catch (error) {
|
||
|
|
setStatus(error.message || "Failed to render calendar.");
|
||
|
|
} finally {
|
||
|
|
renderInProgress = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function requestGeoLocation() {
|
||
|
|
if (!navigator.geolocation) {
|
||
|
|
setStatus("Geolocation not available in this browser.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setStatus("Getting your location...");
|
||
|
|
navigator.geolocation.getCurrentPosition(
|
||
|
|
({ coords }) => {
|
||
|
|
latEl.value = coords.latitude.toFixed(4);
|
||
|
|
lngEl.value = coords.longitude.toFixed(4);
|
||
|
|
syncNowSkyBackground({ latitude: coords.latitude, longitude: coords.longitude }, true);
|
||
|
|
setStatus("Location set from browser. Click Save Settings to refresh.");
|
||
|
|
},
|
||
|
|
(err) => {
|
||
|
|
const detail = err?.message || `code ${err?.code ?? "unknown"}`;
|
||
|
|
setStatus(`Could not get location (${detail}).`);
|
||
|
|
},
|
||
|
|
{ enableHighAccuracy: true, timeout: 10000 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function setTopbarDropdownOpen(dropdownEl, isOpen) {
|
||
|
|
if (!(dropdownEl instanceof HTMLElement)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
dropdownEl.classList.toggle("is-open", Boolean(isOpen));
|
||
|
|
const trigger = dropdownEl.querySelector("button[aria-haspopup='menu']");
|
||
|
|
if (trigger) {
|
||
|
|
trigger.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeTopbarDropdowns(exceptEl = null) {
|
||
|
|
topbarDropdownEls.forEach((dropdownEl) => {
|
||
|
|
if (exceptEl && dropdownEl === exceptEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
setTopbarDropdownOpen(dropdownEl, false);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function bindTopbarDropdownInteractions() {
|
||
|
|
if (!topbarDropdownEls.length) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
topbarDropdownEls.forEach((dropdownEl) => {
|
||
|
|
const trigger = dropdownEl.querySelector("button[aria-haspopup='menu']");
|
||
|
|
if (!(trigger instanceof HTMLElement)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setTopbarDropdownOpen(dropdownEl, false);
|
||
|
|
|
||
|
|
dropdownEl.addEventListener("mouseenter", () => {
|
||
|
|
setTopbarDropdownOpen(dropdownEl, true);
|
||
|
|
});
|
||
|
|
|
||
|
|
dropdownEl.addEventListener("mouseleave", () => {
|
||
|
|
setTopbarDropdownOpen(dropdownEl, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
dropdownEl.addEventListener("focusout", (event) => {
|
||
|
|
const nextTarget = event.relatedTarget;
|
||
|
|
if (!(nextTarget instanceof Node) || !dropdownEl.contains(nextTarget)) {
|
||
|
|
setTopbarDropdownOpen(dropdownEl, false);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
trigger.addEventListener("click", (event) => {
|
||
|
|
event.stopPropagation();
|
||
|
|
const nextOpen = !dropdownEl.classList.contains("is-open");
|
||
|
|
closeTopbarDropdowns(dropdownEl);
|
||
|
|
setTopbarDropdownOpen(dropdownEl, nextOpen);
|
||
|
|
});
|
||
|
|
|
||
|
|
const menuItems = dropdownEl.querySelectorAll(".topbar-dropdown-menu [role='menuitem']");
|
||
|
|
menuItems.forEach((menuItem) => {
|
||
|
|
menuItem.addEventListener("click", () => {
|
||
|
|
closeTopbarDropdowns();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (saveSettingsEl) {
|
||
|
|
saveSettingsEl.addEventListener("click", handleSaveSettings);
|
||
|
|
}
|
||
|
|
|
||
|
|
useLocationEl.addEventListener("click", requestGeoLocation);
|
||
|
|
|
||
|
|
if (openSettingsEl) {
|
||
|
|
openSettingsEl.addEventListener("click", (event) => {
|
||
|
|
event.stopPropagation();
|
||
|
|
if (settingsPopupEl?.hidden) {
|
||
|
|
openSettingsPopup();
|
||
|
|
} else {
|
||
|
|
closeSettingsPopup();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openTarotEl) {
|
||
|
|
openTarotEl.addEventListener("click", () => {
|
||
|
|
if (activeSection === "tarot") {
|
||
|
|
setActiveSection("home");
|
||
|
|
} else {
|
||
|
|
setActiveSection("tarot");
|
||
|
|
showTarotCardsView();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openTarotCardsEl) {
|
||
|
|
openTarotCardsEl.addEventListener("click", () => {
|
||
|
|
setActiveSection("tarot");
|
||
|
|
showTarotCardsView();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openTarotSpreadEl) {
|
||
|
|
openTarotSpreadEl.addEventListener("click", () => {
|
||
|
|
setTarotSpread("three-card", true);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (tarotSpreadBackEl) {
|
||
|
|
tarotSpreadBackEl.addEventListener("click", () => {
|
||
|
|
showTarotCardsView();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (tarotSpreadBtnThreeEl) {
|
||
|
|
tarotSpreadBtnThreeEl.addEventListener("click", () => {
|
||
|
|
showTarotSpreadView("three-card");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (tarotSpreadBtnCelticEl) {
|
||
|
|
tarotSpreadBtnCelticEl.addEventListener("click", () => {
|
||
|
|
showTarotSpreadView("celtic-cross");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (tarotSpreadRedrawEl) {
|
||
|
|
tarotSpreadRedrawEl.addEventListener("click", () => {
|
||
|
|
regenerateTarotSpreadDraw();
|
||
|
|
renderTarotSpread();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openAstronomyEl) {
|
||
|
|
openAstronomyEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "astronomy" ? "home" : "astronomy");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openPlanetsEl) {
|
||
|
|
openPlanetsEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "planets" ? "home" : "planets");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openCyclesEl) {
|
||
|
|
openCyclesEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "cycles" ? "home" : "cycles");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openElementsEl) {
|
||
|
|
openElementsEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "elements" ? "home" : "elements");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openIChingEl) {
|
||
|
|
openIChingEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "iching" ? "home" : "iching");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openKabbalahEl) {
|
||
|
|
openKabbalahEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "kabbalah" ? "home" : "kabbalah");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openKabbalahTreeEl) {
|
||
|
|
openKabbalahTreeEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "kabbalah-tree" ? "home" : "kabbalah-tree");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openKabbalahCubeEl) {
|
||
|
|
openKabbalahCubeEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "cube" ? "home" : "cube");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openAlphabetEl) {
|
||
|
|
openAlphabetEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "alphabet" ? "home" : "alphabet");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openNumbersEl) {
|
||
|
|
openNumbersEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "numbers" ? "home" : "numbers");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openZodiacEl) {
|
||
|
|
openZodiacEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "zodiac" ? "home" : "zodiac");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openNatalEl) {
|
||
|
|
openNatalEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "natal" ? "home" : "natal");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openQuizEl) {
|
||
|
|
openQuizEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "quiz" ? "home" : "quiz");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openGodsEl) {
|
||
|
|
openGodsEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "gods" ? "home" : "gods");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openEnochianEl) {
|
||
|
|
openEnochianEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "enochian" ? "home" : "enochian");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openCalendarEl) {
|
||
|
|
openCalendarEl.addEventListener("click", () => {
|
||
|
|
const isCalendarMenuActive = activeSection === "calendar" || activeSection === "holidays";
|
||
|
|
setActiveSection(isCalendarMenuActive ? "home" : "calendar");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openCalendarMonthsEl) {
|
||
|
|
openCalendarMonthsEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "calendar" ? "home" : "calendar");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (openHolidaysEl) {
|
||
|
|
openHolidaysEl.addEventListener("click", () => {
|
||
|
|
setActiveSection(activeSection === "holidays" ? "home" : "holidays");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
bindTopbarDropdownInteractions();
|
||
|
|
|
||
|
|
document.addEventListener("nav:cube", (e) => {
|
||
|
|
if (typeof ensureCubeSection === "function" && magickDataset) {
|
||
|
|
ensureCubeSection(magickDataset, referenceData);
|
||
|
|
}
|
||
|
|
|
||
|
|
setActiveSection("cube");
|
||
|
|
|
||
|
|
const detail = e?.detail || {};
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
const ui = window.CubeSectionUi;
|
||
|
|
const selected = ui?.selectPlacement?.(detail);
|
||
|
|
if (!selected && detail?.wallId) {
|
||
|
|
ui?.selectWallById?.(detail.wallId);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("nav:zodiac", (e) => {
|
||
|
|
if (typeof ensureZodiacSection === "function" && referenceData && magickDataset) {
|
||
|
|
ensureZodiacSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
setActiveSection("zodiac");
|
||
|
|
const signId = e?.detail?.signId;
|
||
|
|
if (signId) {
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
window.ZodiacSectionUi?.selectBySignId?.(signId);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("nav:alphabet", (e) => {
|
||
|
|
if (typeof ensureAlphabetSection === "function" && magickDataset) {
|
||
|
|
ensureAlphabetSection(magickDataset, referenceData);
|
||
|
|
}
|
||
|
|
setActiveSection("alphabet");
|
||
|
|
|
||
|
|
const alphabet = e?.detail?.alphabet;
|
||
|
|
const hebrewLetterId = e?.detail?.hebrewLetterId;
|
||
|
|
const greekName = e?.detail?.greekName;
|
||
|
|
const englishLetter = e?.detail?.englishLetter;
|
||
|
|
const arabicName = e?.detail?.arabicName;
|
||
|
|
const enochianId = e?.detail?.enochianId;
|
||
|
|
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
const ui = window.AlphabetSectionUi;
|
||
|
|
if ((alphabet === "hebrew" || (!alphabet && hebrewLetterId)) && hebrewLetterId) {
|
||
|
|
ui?.selectLetterByHebrewId?.(hebrewLetterId);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (alphabet === "greek" && greekName) {
|
||
|
|
ui?.selectGreekLetterByName?.(greekName);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (alphabet === "english" && englishLetter) {
|
||
|
|
ui?.selectEnglishLetter?.(englishLetter);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (alphabet === "arabic" && arabicName) {
|
||
|
|
ui?.selectArabicLetter?.(arabicName);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (alphabet === "enochian" && enochianId) {
|
||
|
|
ui?.selectEnochianLetter?.(enochianId);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("nav:number", (e) => {
|
||
|
|
const rawValue = e?.detail?.value;
|
||
|
|
const normalizedValue = normalizeNumberValue(rawValue);
|
||
|
|
if (normalizedValue === null) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setActiveSection("numbers");
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
selectNumberEntry(normalizedValue);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("nav:iching", (e) => {
|
||
|
|
if (typeof ensureIChingSection === "function" && referenceData) {
|
||
|
|
ensureIChingSection(referenceData);
|
||
|
|
}
|
||
|
|
|
||
|
|
setActiveSection("iching");
|
||
|
|
|
||
|
|
const hexagramNumber = e?.detail?.hexagramNumber;
|
||
|
|
const planetaryInfluence = e?.detail?.planetaryInfluence;
|
||
|
|
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
const ui = window.IChingSectionUi;
|
||
|
|
if (hexagramNumber != null) {
|
||
|
|
ui?.selectByHexagramNumber?.(hexagramNumber);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (planetaryInfluence) {
|
||
|
|
ui?.selectByPlanetaryInfluence?.(planetaryInfluence);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("nav:gods", (e) => {
|
||
|
|
if (typeof ensureGodsSection === "function" && magickDataset) {
|
||
|
|
ensureGodsSection(magickDataset, referenceData);
|
||
|
|
}
|
||
|
|
setActiveSection("gods");
|
||
|
|
const godId = e?.detail?.godId;
|
||
|
|
const godName = e?.detail?.godName;
|
||
|
|
const pathNo = e?.detail?.pathNo;
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
const ui = window.GodsSectionUi;
|
||
|
|
const viaId = godId ? ui?.selectById?.(godId) : false;
|
||
|
|
const viaName = !viaId && godName ? ui?.selectByName?.(godName) : false;
|
||
|
|
if (!viaId && !viaName && pathNo != null) {
|
||
|
|
ui?.selectByPathNo?.(pathNo);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("nav:calendar-month", (e) => {
|
||
|
|
const calendarId = e?.detail?.calendarId;
|
||
|
|
const monthId = e?.detail?.monthId;
|
||
|
|
if (!monthId) return;
|
||
|
|
|
||
|
|
if (typeof ensureCalendarSection === "function" && referenceData) {
|
||
|
|
ensureCalendarSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
|
||
|
|
setActiveSection("calendar");
|
||
|
|
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
if (calendarId) {
|
||
|
|
window.CalendarSectionUi?.selectCalendarType?.(calendarId);
|
||
|
|
}
|
||
|
|
window.CalendarSectionUi?.selectByMonthId?.(monthId);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("nav:kabbalah-path", (e) => {
|
||
|
|
const pathNo = e?.detail?.pathNo;
|
||
|
|
const { ensureKabbalahSection } = window.KabbalahSectionUi || {};
|
||
|
|
if (typeof ensureKabbalahSection === "function" && magickDataset) {
|
||
|
|
ensureKabbalahSection(magickDataset);
|
||
|
|
}
|
||
|
|
setActiveSection("kabbalah-tree");
|
||
|
|
if (pathNo != null) {
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
window.KabbalahSectionUi?.selectNode?.(pathNo);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("nav:planet", (e) => {
|
||
|
|
const planetId = e?.detail?.planetId;
|
||
|
|
if (!planetId) return;
|
||
|
|
if (typeof ensurePlanetSection === "function" && referenceData) {
|
||
|
|
ensurePlanetSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
setActiveSection("planets");
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
window.PlanetSectionUi?.selectByPlanetId?.(planetId);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("nav:elements", (e) => {
|
||
|
|
const elementId = e?.detail?.elementId;
|
||
|
|
if (!elementId) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof ensureElementsSection === "function" && magickDataset) {
|
||
|
|
ensureElementsSection(magickDataset);
|
||
|
|
}
|
||
|
|
|
||
|
|
setActiveSection("elements");
|
||
|
|
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
window.ElementsSectionUi?.selectByElementId?.(elementId);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("nav:tarot-trump", (e) => {
|
||
|
|
if (typeof ensureTarotSection === "function" && referenceData) {
|
||
|
|
ensureTarotSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
setActiveSection("tarot");
|
||
|
|
const { trumpNumber, cardName } = e?.detail || {};
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
if (trumpNumber != null) {
|
||
|
|
window.TarotSectionUi?.selectCardByTrump?.(trumpNumber);
|
||
|
|
} else if (cardName) {
|
||
|
|
window.TarotSectionUi?.selectCardByName?.(cardName);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("kab:view-trump", (e) => {
|
||
|
|
setActiveSection("tarot");
|
||
|
|
const trumpNumber = e?.detail?.trumpNumber;
|
||
|
|
if (trumpNumber != null) {
|
||
|
|
if (typeof ensureTarotSection === "function" && referenceData) {
|
||
|
|
ensureTarotSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
window.TarotSectionUi?.selectCardByTrump?.(trumpNumber);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("tarot:view-kab-path", (e) => {
|
||
|
|
setActiveSection("kabbalah-tree");
|
||
|
|
const pathNumber = e?.detail?.pathNumber;
|
||
|
|
if (pathNumber != null) {
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
const kabbalahUi = window.KabbalahSectionUi;
|
||
|
|
if (typeof kabbalahUi?.selectNode === "function") {
|
||
|
|
kabbalahUi.selectNode(pathNumber);
|
||
|
|
} else {
|
||
|
|
kabbalahUi?.selectPathByNumber?.(pathNumber);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (closeSettingsEl) {
|
||
|
|
closeSettingsEl.addEventListener("click", closeSettingsPopup);
|
||
|
|
}
|
||
|
|
|
||
|
|
document.addEventListener("click", (event) => {
|
||
|
|
const clickTarget = event.target;
|
||
|
|
if (clickTarget instanceof Node && topbarDropdownEls.some((dropdownEl) => dropdownEl.contains(clickTarget))) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
closeTopbarDropdowns();
|
||
|
|
|
||
|
|
if (!settingsPopupEl || settingsPopupEl.hidden) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!(clickTarget instanceof Node)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (settingsPopupCardEl?.contains(clickTarget) || openSettingsEl?.contains(clickTarget)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
closeSettingsPopup();
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("keydown", (event) => {
|
||
|
|
if (event.key === "Escape") {
|
||
|
|
closeTopbarDropdowns();
|
||
|
|
closeSettingsPopup();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
window.addEventListener("resize", () => {
|
||
|
|
if (monthStripResizeFrame) {
|
||
|
|
cancelAnimationFrame(monthStripResizeFrame);
|
||
|
|
}
|
||
|
|
monthStripResizeFrame = requestAnimationFrame(() => {
|
||
|
|
monthStripResizeFrame = null;
|
||
|
|
updateMonthStrip();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
window.TarotNatal = {
|
||
|
|
...(window.TarotNatal || {}),
|
||
|
|
getSettings() {
|
||
|
|
return { ...currentSettings };
|
||
|
|
},
|
||
|
|
getContext() {
|
||
|
|
return buildNatalContext(currentSettings);
|
||
|
|
},
|
||
|
|
buildContextFromSettings(settings) {
|
||
|
|
return buildNatalContext(settings);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const initialSettings = loadSavedSettings();
|
||
|
|
applySettingsToInputs(initialSettings);
|
||
|
|
emitSettingsUpdated(currentSettings);
|
||
|
|
initializeSidebarPopouts();
|
||
|
|
initializeDetailPopouts();
|
||
|
|
syncNowSkyBackground({ latitude: initialSettings.latitude, longitude: initialSettings.longitude }, true);
|
||
|
|
setActiveSection("home");
|
||
|
|
|
||
|
|
void renderWeek();
|