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

591 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ui-zodiac.js — Zodiac sign browser section */
(function () {
"use strict";
const ELEMENT_STYLE = {
fire: { emoji: "🔥", badge: "zod-badge--fire", label: "Fire" },
earth: { emoji: "🌍", badge: "zod-badge--earth", label: "Earth" },
air: { emoji: "💨", badge: "zod-badge--air", label: "Air" },
water: { emoji: "💧", badge: "zod-badge--water", label: "Water" }
};
const PLANET_SYMBOLS = {
saturn: "♄︎", jupiter: "♃︎", mars: "♂︎", sol: "☉︎",
venus: "♀︎", mercury: "☿︎", luna: "☾︎"
};
const MONTH_NAMES = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
const state = {
initialized: false,
entries: [],
filteredEntries: [],
selectedId: null,
searchQuery: "",
kabPaths: [],
decansBySign: {},
monthRefsBySignId: new Map(),
cubePlacementBySignId: new Map()
};
// ── Elements ──────────────────────────────────────────────────────────
function getElements() {
return {
listEl: document.getElementById("zodiac-sign-list"),
countEl: document.getElementById("zodiac-sign-count"),
searchEl: document.getElementById("zodiac-search-input"),
searchClearEl: document.getElementById("zodiac-search-clear"),
detailNameEl: document.getElementById("zodiac-detail-name"),
detailSubEl: document.getElementById("zodiac-detail-sub"),
detailBodyEl: document.getElementById("zodiac-detail-body")
};
}
// ── Normalise ─────────────────────────────────────────────────────────
function norm(s) {
return String(s || "").toLowerCase().replace(/[^a-z0-9 ]/g, "").trim();
}
function cap(s) {
return String(s || "").charAt(0).toUpperCase() + String(s || "").slice(1);
}
function buildSearchText(sign) {
return norm([
sign.name?.en, sign.meaning?.en, sign.elementId, sign.quadruplicity,
sign.planetId, sign.id
].join(" "));
}
function formatDateRange(rulesFrom) {
if (!Array.isArray(rulesFrom) || rulesFrom.length < 2) return "—";
const [from, to] = rulesFrom;
const fMonth = MONTH_NAMES[(from[0] || 1) - 1];
const tMonth = MONTH_NAMES[(to[0] || 1) - 1];
return `${fMonth} ${from[1]} ${tMonth} ${to[1]}`;
}
function buildMonthReferencesBySign(referenceData) {
const map = new Map();
const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [];
const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
const monthById = new Map(months.map((month) => [month.id, month]));
const monthByOrder = new Map(
months
.filter((month) => Number.isFinite(Number(month?.order)))
.map((month) => [Number(month.order), month])
);
function parseMonthDay(value) {
const [month, day] = String(value || "").split("-").map((part) => Number(part));
if (!Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
return { month, day };
}
function monthOrdersInRange(startMonth, endMonth) {
const orders = [];
let cursor = startMonth;
let guard = 0;
while (guard < 13) {
orders.push(cursor);
if (cursor === endMonth) {
break;
}
cursor = cursor === 12 ? 1 : cursor + 1;
guard += 1;
}
return orders;
}
function pushRef(signId, month) {
const key = String(signId || "").trim().toLowerCase();
if (!key || !month?.id) {
return;
}
if (!map.has(key)) {
map.set(key, []);
}
const rows = map.get(key);
if (rows.some((entry) => entry.id === month.id)) {
return;
}
rows.push({
id: month.id,
name: month.name || month.id,
order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999
});
}
months.forEach((month) => {
pushRef(month?.associations?.zodiacSignId, month);
const events = Array.isArray(month?.events) ? month.events : [];
events.forEach((event) => {
pushRef(event?.associations?.zodiacSignId, month);
});
});
holidays.forEach((holiday) => {
const month = monthById.get(holiday?.monthId);
if (!month) {
return;
}
pushRef(holiday?.associations?.zodiacSignId, month);
});
// Structural month coverage from sign date ranges (e.g., Scorpio spans Oct+Nov).
signs.forEach((sign) => {
const start = parseMonthDay(sign?.start);
const end = parseMonthDay(sign?.end);
if (!start || !end || !sign?.id) {
return;
}
monthOrdersInRange(start.month, end.month).forEach((monthOrder) => {
const month = monthByOrder.get(monthOrder);
if (month) {
pushRef(sign.id, month);
}
});
});
map.forEach((rows, key) => {
rows.sort((left, right) => left.order - right.order || left.name.localeCompare(right.name));
map.set(key, rows);
});
return map;
}
function buildCubeSignPlacements(magickDataset) {
const placements = new Map();
const cube = magickDataset?.grouped?.kabbalah?.cube || {};
const walls = Array.isArray(cube?.walls)
? cube.walls
: [];
const edges = Array.isArray(cube?.edges)
? cube.edges
: [];
const paths = Array.isArray(magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]?.paths)
? magickDataset.grouped.kabbalah["kabbalah-tree"].paths
: [];
function normalizeLetterId(value) {
const key = String(value || "").toLowerCase().replace(/[^a-z]/g, "").trim();
const aliases = {
aleph: "alef",
beth: "bet",
zain: "zayin",
cheth: "het",
chet: "het",
daleth: "dalet",
teth: "tet",
peh: "pe",
tzaddi: "tsadi",
tzadi: "tsadi",
tzade: "tsadi",
tsaddi: "tsadi",
qoph: "qof",
taw: "tav",
tau: "tav"
};
return aliases[key] || key;
}
function edgeWalls(edge) {
const explicitWalls = Array.isArray(edge?.walls)
? edge.walls.map((wallId) => String(wallId || "").trim().toLowerCase()).filter(Boolean)
: [];
if (explicitWalls.length >= 2) {
return explicitWalls.slice(0, 2);
}
return String(edge?.id || "")
.trim()
.toLowerCase()
.split("-")
.map((wallId) => wallId.trim())
.filter(Boolean)
.slice(0, 2);
}
function edgeLabel(edge) {
const explicitName = String(edge?.name || "").trim();
if (explicitName) {
return explicitName;
}
return edgeWalls(edge)
.map((part) => cap(part))
.join(" ");
}
function resolveCubeDirectionLabel(wallId, edge) {
const normalizedWallId = String(wallId || "").trim().toLowerCase();
const edgeId = String(edge?.id || "").trim().toLowerCase();
if (!normalizedWallId || !edgeId) {
return "";
}
const cubeUi = window.CubeSectionUi;
if (cubeUi && typeof cubeUi.getEdgeDirectionLabelForWall === "function") {
const directionLabel = String(cubeUi.getEdgeDirectionLabelForWall(normalizedWallId, edgeId) || "").trim();
if (directionLabel) {
return directionLabel;
}
}
return edgeLabel(edge);
}
const wallById = new Map(
walls.map((wall) => [String(wall?.id || "").trim().toLowerCase(), wall])
);
const pathByLetterId = new Map(
paths
.map((path) => [normalizeLetterId(path?.hebrewLetter?.transliteration), path])
.filter(([letterId]) => Boolean(letterId))
);
edges.forEach((edge) => {
const letterId = normalizeLetterId(edge?.hebrewLetterId || edge?.associations?.hebrewLetterId);
const path = pathByLetterId.get(letterId) || null;
const signId = path?.astrology?.type === "zodiac"
? String(path?.astrology?.name || "").trim().toLowerCase()
: "";
if (!signId || placements.has(signId)) {
return;
}
const wallsForEdge = edgeWalls(edge);
const primaryWallId = wallsForEdge[0] || "";
const primaryWall = wallById.get(primaryWallId);
placements.set(signId, {
wallId: primaryWallId,
edgeId: String(edge?.id || "").trim().toLowerCase(),
wallName: primaryWall?.name || cap(primaryWallId || "wall"),
edgeName: resolveCubeDirectionLabel(primaryWallId, edge)
});
});
return placements;
}
function cubePlacementLabel(placement) {
const wallName = placement?.wallName || "Wall";
const edgeName = placement?.edgeName || "Direction";
return `Cube: ${wallName} Wall - ${edgeName}`;
}
// ── List ──────────────────────────────────────────────────────────────
function applyFilter() {
const q = norm(state.searchQuery);
state.filteredEntries = q
? state.entries.filter((s) => buildSearchText(s).includes(q))
: [...state.entries];
}
function renderList(els) {
if (!els.listEl) return;
els.listEl.innerHTML = "";
state.filteredEntries.forEach((sign) => {
const active = sign.id === state.selectedId;
const el = document.createElement("div");
el.className = "planet-list-item" + (active ? " is-selected" : "");
el.setAttribute("role", "option");
el.setAttribute("aria-selected", active ? "true" : "false");
el.dataset.id = sign.id;
const elemStyle = ELEMENT_STYLE[sign.elementId] || {};
el.innerHTML = `
<div class="zod-list-row">
<span class="zod-list-symbol">${sign.symbol || "?"}</span>
<span class="planet-list-name">${sign.name?.en || sign.id}</span>
<span class="zod-list-elem ${elemStyle.badge || ""}">${elemStyle.emoji || ""}</span>
</div>
<div class="planet-list-meta">${cap(sign.elementId)} · ${cap(sign.quadruplicity)} · ${cap(sign.planetId)}</div>
`;
el.addEventListener("click", () => { selectById(sign.id, els); });
els.listEl.appendChild(el);
});
if (els.countEl) {
els.countEl.textContent = state.searchQuery
? `${state.filteredEntries.length} of ${state.entries.length} signs`
: `${state.entries.length} signs`;
}
}
// ── Detail ────────────────────────────────────────────────────────────
function renderDetail(sign, els) {
if (!els.detailNameEl) return;
const elemStyle = ELEMENT_STYLE[sign.elementId] || {};
const polarity = ["fire", "air"].includes(sign.elementId) ? "Masculine / Positive" : "Feminine / Negative";
const kabPath = state.kabPaths.find(
(p) => p.astrology?.type === "zodiac" &&
p.astrology?.name?.toLowerCase() === sign.id
);
const decans = state.decansBySign[sign.id] || [];
const monthRefs = state.monthRefsBySignId.get(String(sign.id || "").toLowerCase()) || [];
const cubePlacement = state.cubePlacementBySignId.get(String(sign.id || "").toLowerCase()) || null;
// Heading
els.detailNameEl.textContent = sign.symbol || sign.id;
els.detailSubEl.textContent = `${sign.name?.en || ""}${sign.meaning?.en || ""}`;
const sections = [];
// ── Sign Details ──────────────────────────────────────────────────
const elemBadge = `<span class="zod-badge ${elemStyle.badge || ""}">${elemStyle.emoji || ""} ${cap(sign.elementId)}</span>`;
const quadBadge = `<span class="zod-badge zod-badge--quad">${cap(sign.quadruplicity)}</span>`;
sections.push(`<div class="planet-meta-card">
<strong>Sign Details</strong>
<div class="planet-text">
<dl class="alpha-dl">
<dt>Symbol</dt><dd>${sign.symbol || "—"}</dd>
<dt>Meaning</dt><dd>${sign.meaning?.en || "—"}</dd>
<dt>Element</dt><dd>${elemBadge}</dd>
<dt>Modality</dt><dd>${quadBadge}</dd>
<dt>Polarity</dt><dd>${polarity}</dd>
<dt>Dates</dt><dd>${formatDateRange(sign.rulesFrom)}</dd>
<dt>Position</dt><dd>#${sign.no} of 12</dd>
</dl>
</div>
</div>`);
// ── Ruling Planet ─────────────────────────────────────────────────
const planetSym = PLANET_SYMBOLS[sign.planetId] || "";
sections.push(`<div class="planet-meta-card">
<strong>Ruling Planet</strong>
<div class="planet-text">
<p style="font-size:22px;margin:0 0 6px">${planetSym} ${cap(sign.planetId)}</p>
<button class="alpha-nav-btn" data-nav="planet" data-planet-id="${sign.planetId}">
View ${cap(sign.planetId)}
</button>
</div>
</div>`);
if (cubePlacement) {
sections.push(`<div class="planet-meta-card">
<strong>Cube of Space</strong>
<div class="planet-text">This sign appears in Cube edge correspondences.</div>
<div class="alpha-nav-btns">
<button class="alpha-nav-btn" data-nav="cube-sign" data-sign-id="${sign.id}" data-wall-id="${cubePlacement.wallId}" data-edge-id="${cubePlacement.edgeId}">
${cubePlacementLabel(cubePlacement)}
</button>
</div>
</div>`);
}
// ── Kabbalah Path + Trump ─────────────────────────────────────────
if (kabPath) {
const hl = kabPath.hebrewLetter || {};
sections.push(`<div class="planet-meta-card">
<strong>Kabbalah & Major Arcana</strong>
<div class="planet-text">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
<span class="zod-hebrew-glyph">${hl.char || ""}</span>
<div>
<div style="font-weight:600">${hl.transliteration || ""} (${hl.meaning || ""})</div>
<div class="planet-list-meta">${cap(hl.letterType || "")} letter · Path ${kabPath.pathNumber}</div>
</div>
</div>
<dl class="alpha-dl" style="margin-bottom:8px">
<dt>Trump Card</dt><dd>${kabPath.tarot?.card || "—"}</dd>
<dt>Intelligence</dt><dd>${kabPath.intelligence || "—"}</dd>
</dl>
<div class="alpha-nav-btns">
<button class="alpha-nav-btn" data-nav="kab-path" data-path-number="${kabPath.pathNumber}">
Kabbalah Path ${kabPath.pathNumber}
</button>
<button class="alpha-nav-btn" data-nav="trump" data-trump-number="${kabPath.tarot?.trumpNumber}">
${kabPath.tarot?.card || "Tarot Card"}
</button>
</div>
</div>
</div>`);
}
// ── Decans & Minor Arcana ─────────────────────────────────────────
if (decans.length) {
const decanRows = decans.map((d) => {
const ord = ["1st","2nd","3rd"][d.index - 1] || d.index;
const sym = PLANET_SYMBOLS[d.rulerPlanetId] || "";
return `<div class="zod-decan-row">
<span class="zod-decan-ord">${ord}</span>
<span class="zod-decan-planet">${sym} ${cap(d.rulerPlanetId)}</span>
<button class="zod-decan-card-btn" data-nav="tarot-card" data-card-name="${d.tarotMinorArcana}">
${d.tarotMinorArcana}
</button>
</div>`;
}).join("");
sections.push(`<div class="planet-meta-card">
<strong>Decans & Minor Arcana</strong>
<div class="planet-text">${decanRows}</div>
</div>`);
}
if (monthRefs.length) {
const monthButtons = monthRefs.map((month) =>
`<button class="alpha-nav-btn" data-nav="calendar-month" data-month-id="${month.id}">${month.name} ↗</button>`
).join("");
sections.push(`<div class="planet-meta-card">
<strong>Calendar Months</strong>
<div class="planet-text">Month correspondences linked to ${sign.name?.en || sign.id}.</div>
<div class="alpha-nav-btns">${monthButtons}</div>
</div>`);
}
// ── Kabbalah extras ───────────────────────────────────────────────
if (sign.tribeOfIsraelId || sign.tetragrammatonPermutation) {
sections.push(`<div class="planet-meta-card">
<strong>Kabbalah Correspondences</strong>
<div class="planet-text">
<dl class="alpha-dl">
${sign.tribeOfIsraelId ? `<dt>Tribe of Israel</dt><dd>${cap(sign.tribeOfIsraelId)}</dd>` : ""}
${sign.tetragrammatonPermutation ? `<dt>Tetragrammaton</dt><dd class="zod-tetra">${sign.tetragrammatonPermutation}</dd>` : ""}
</dl>
</div>
</div>`);
}
els.detailBodyEl.innerHTML = `<div class="planet-meta-grid">${sections.join("")}</div>`;
// Attach button listeners
els.detailBodyEl.querySelectorAll("[data-nav]").forEach((btn) => {
btn.addEventListener("click", () => {
const nav = btn.dataset.nav;
if (nav === "planet") {
document.dispatchEvent(new CustomEvent("nav:planet", {
detail: { planetId: btn.dataset.planetId }
}));
} else if (nav === "kab-path") {
document.dispatchEvent(new CustomEvent("tarot:view-kab-path", {
detail: { pathNumber: Number(btn.dataset.pathNumber) }
}));
} else if (nav === "trump") {
document.dispatchEvent(new CustomEvent("kab:view-trump", {
detail: { trumpNumber: Number(btn.dataset.trumpNumber) }
}));
} else if (nav === "tarot-card") {
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
detail: { cardName: btn.dataset.cardName }
}));
} else if (nav === "calendar-month") {
document.dispatchEvent(new CustomEvent("nav:calendar-month", {
detail: { monthId: btn.dataset.monthId }
}));
} else if (nav === "cube-sign") {
document.dispatchEvent(new CustomEvent("nav:cube", {
detail: {
signId: btn.dataset.signId,
wallId: btn.dataset.wallId,
edgeId: btn.dataset.edgeId
}
}));
}
});
});
}
function resetDetail(els) {
if (els.detailNameEl) els.detailNameEl.textContent = "--";
if (els.detailSubEl) els.detailSubEl.textContent = "Select a sign to explore";
if (els.detailBodyEl) els.detailBodyEl.innerHTML = "";
}
// ── Selection ─────────────────────────────────────────────────────────
function selectById(id, els) {
const sign = state.entries.find((s) => s.id === id);
if (!sign) return;
state.selectedId = id;
renderList(els);
renderDetail(sign, els);
}
// ── Public select (for incoming navigation) ───────────────────────────
function selectBySignId(signId) {
const els = getElements();
if (!state.initialized) return;
const sign = state.entries.find((s) => s.id === signId);
if (sign) selectById(signId, els);
}
// ── Init ───────────────────────────────────────────────────────────────
function ensureZodiacSection(referenceData, magickDataset) {
state.monthRefsBySignId = buildMonthReferencesBySign(referenceData);
state.cubePlacementBySignId = buildCubeSignPlacements(magickDataset);
if (state.initialized) {
const els = getElements();
const current = state.entries.find((entry) => entry.id === state.selectedId);
if (current) {
renderDetail(current, els);
}
return;
}
state.initialized = true;
const zodiacObj = magickDataset?.grouped?.astrology?.zodiac || {};
state.entries = Object.values(zodiacObj).sort((a, b) => (a.no || 0) - (b.no || 0));
const kabTree = magickDataset?.grouped?.kabbalah?.["kabbalah-tree"];
state.kabPaths = Array.isArray(kabTree?.paths) ? kabTree.paths : [];
state.decansBySign = referenceData?.decansBySign || {};
const els = getElements();
applyFilter();
renderList(els);
if (state.entries.length > 0) {
selectById(state.entries[0].id, els);
}
// Search
if (els.searchEl) {
els.searchEl.addEventListener("input", () => {
state.searchQuery = els.searchEl.value;
if (els.searchClearEl) els.searchClearEl.disabled = !state.searchQuery;
applyFilter();
renderList(els);
if (!state.filteredEntries.some((s) => s.id === state.selectedId)) {
if (state.filteredEntries.length > 0) {
selectById(state.filteredEntries[0].id, els);
} else {
state.selectedId = null;
resetDetail(els);
}
}
});
}
if (els.searchClearEl) {
els.searchClearEl.addEventListener("click", () => {
state.searchQuery = "";
if (els.searchEl) els.searchEl.value = "";
els.searchClearEl.disabled = true;
applyFilter();
renderList(els);
if (state.entries.length > 0) selectById(state.entries[0].id, els);
});
}
}
window.ZodiacSectionUi = {
ensureZodiacSection,
selectBySignId
};
})();