Files
TaroTime/app/ui-elements.js

455 lines
14 KiB
JavaScript
Raw Normal View History

2026-03-07 01:09:00 -08:00
(function () {
"use strict";
const { getTarotCardSearchAliases } = window.TarotCardImages || {};
const CLASSICAL_ELEMENT_IDS = ["fire", "water", "air", "earth"];
const ACE_BY_ELEMENT_ID = {
water: "Ace of Cups",
fire: "Ace of Wands",
air: "Ace of Swords",
earth: "Ace of Disks"
};
const HEBREW_LETTER_NAME_BY_ELEMENT_ID = {
fire: "Yod",
water: "Heh",
air: "Vav",
earth: "Heh"
};
const HEBREW_LETTER_CHAR_BY_ELEMENT_ID = {
fire: "י",
water: "ה",
air: "ו",
earth: "ה"
};
const COURT_RANK_BY_ELEMENT_ID = {
fire: "Knight",
water: "Queen",
air: "Prince",
earth: "Princess"
};
const COURT_SUITS = ["Wands", "Cups", "Swords", "Disks"];
const SUIT_BY_ELEMENT_ID = {
fire: "Wands",
water: "Cups",
air: "Swords",
earth: "Disks"
};
const SMALL_CARD_GROUPS = [
{ label: "24", modality: "Cardinal", numbers: [2, 3, 4] },
{ label: "57", modality: "Fixed", numbers: [5, 6, 7] },
{ label: "810", modality: "Mutable", numbers: [8, 9, 10] }
];
const SIGN_BY_ELEMENT_AND_MODALITY = {
fire: { cardinal: "aries", fixed: "leo", mutable: "sagittarius" },
water: { cardinal: "cancer", fixed: "scorpio", mutable: "pisces" },
air: { cardinal: "libra", fixed: "aquarius", mutable: "gemini" },
earth: { cardinal: "capricorn", fixed: "taurus", mutable: "virgo" }
};
const state = {
initialized: false,
entries: [],
filteredEntries: [],
selectedId: "",
searchQuery: ""
};
function getElements() {
return {
listEl: document.getElementById("elements-list"),
countEl: document.getElementById("elements-count"),
searchEl: document.getElementById("elements-search-input"),
searchClearEl: document.getElementById("elements-search-clear"),
detailNameEl: document.getElementById("elements-detail-name"),
detailSubEl: document.getElementById("elements-detail-sub"),
detailBodyEl: document.getElementById("elements-detail-body")
};
}
function normalize(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/\s+/g, " ");
}
function titleCase(value) {
return String(value || "")
.split(" ")
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
}
function buildTarotAliasText(cardNames) {
if (typeof getTarotCardSearchAliases !== "function") {
return Array.isArray(cardNames) ? cardNames.join(" ") : "";
}
const aliases = new Set();
(Array.isArray(cardNames) ? cardNames : []).forEach((cardName) => {
getTarotCardSearchAliases(cardName).forEach((alias) => aliases.add(alias));
});
return Array.from(aliases).join(" ");
}
function buildSmallCardGroupsForElement(elementId) {
const suit = SUIT_BY_ELEMENT_ID[elementId] || "";
const signByModality = SIGN_BY_ELEMENT_AND_MODALITY[elementId] || {};
return SMALL_CARD_GROUPS.map((group) => {
const signId = String(signByModality[String(group.modality || "").toLowerCase()] || "").trim();
const signName = titleCase(signId);
const cardNames = group.numbers.map((number) => `${number} of ${suit}`);
return {
rangeLabel: group.label,
modality: group.modality,
signId,
signName,
cardNames
};
});
}
function buildEntries(magickDataset) {
const source = magickDataset?.grouped?.alchemy?.elements;
if (!source || typeof source !== "object") {
return [];
}
return CLASSICAL_ELEMENT_IDS
.map((id) => {
const item = source[id];
if (!item || typeof item !== "object") {
return null;
}
const name = String(item?.name?.en || item?.name || titleCase(id)).trim() || titleCase(id);
const symbol = String(item?.symbol || "").trim();
const aceCardName = ACE_BY_ELEMENT_ID[id] || "";
const hebrewLetter = HEBREW_LETTER_CHAR_BY_ELEMENT_ID[id] || "";
const hebrewLetterName = HEBREW_LETTER_NAME_BY_ELEMENT_ID[id] || "";
const courtRank = COURT_RANK_BY_ELEMENT_ID[id] || "";
const courtCardNames = courtRank
? COURT_SUITS.map((suit) => `${courtRank} of ${suit}`)
: [];
const smallCardGroups = buildSmallCardGroupsForElement(id);
const smallCardNames = smallCardGroups.flatMap((group) => group.cardNames || []);
const tarotAliasText = buildTarotAliasText([aceCardName, ...courtCardNames, ...smallCardNames]);
return {
id,
name,
symbol,
elementalId: String(item?.elementalId || "").trim(),
aceCardName,
hebrewLetter,
hebrewLetterName,
courtRank,
courtCardNames,
smallCardGroups,
searchText: normalize(`${id} ${name} ${symbol} ${aceCardName} ${hebrewLetter} ${hebrewLetterName} ${courtRank} ${courtCardNames.join(" ")} ${smallCardGroups.map((group) => `${group.modality} ${group.signName} ${group.cardNames.join(" ")}`).join(" ")} ${tarotAliasText}`)
};
})
.filter(Boolean);
}
function findEntryById(id) {
const normalizedId = normalize(id);
return state.entries.find((entry) => entry.id === normalizedId) || null;
}
function renderList(elements) {
if (!elements?.listEl) {
return;
}
elements.listEl.replaceChildren();
state.filteredEntries.forEach((entry) => {
const button = document.createElement("button");
button.type = "button";
button.className = "planet-list-item";
button.dataset.elementId = entry.id;
button.setAttribute("role", "option");
const isSelected = entry.id === state.selectedId;
button.classList.toggle("is-selected", isSelected);
button.setAttribute("aria-selected", isSelected ? "true" : "false");
const name = document.createElement("span");
name.className = "planet-list-name";
name.textContent = `${entry.symbol} ${entry.name}`.trim();
const meta = document.createElement("span");
meta.className = "planet-list-meta";
meta.textContent = `Letter: ${entry.hebrewLetter || "--"} · Ace: ${entry.aceCardName || "--"} · Court: ${entry.courtRank || "--"}`;
button.append(name, meta);
elements.listEl.appendChild(button);
});
if (elements.countEl) {
elements.countEl.textContent = `${state.filteredEntries.length} elements`;
}
if (!state.filteredEntries.length) {
const empty = document.createElement("div");
empty.className = "planet-text";
empty.style.padding = "16px";
empty.style.color = "#71717a";
empty.textContent = "No elements match your search.";
elements.listEl.appendChild(empty);
}
}
function renderDetail(elements) {
if (!elements?.detailNameEl || !elements.detailSubEl || !elements.detailBodyEl) {
return;
}
const entry = findEntryById(state.selectedId);
elements.detailBodyEl.replaceChildren();
if (!entry) {
elements.detailNameEl.textContent = "--";
elements.detailSubEl.textContent = "Select an element to explore";
return;
}
elements.detailNameEl.textContent = `${entry.symbol} ${entry.name}`.trim();
elements.detailSubEl.textContent = "Classical Element";
const grid = document.createElement("div");
grid.className = "planet-meta-grid";
const detailsCard = document.createElement("div");
detailsCard.className = "planet-meta-card";
detailsCard.innerHTML = `
<strong>Element Details</strong>
<dl class="alpha-dl">
<dt>Name</dt><dd>${entry.name}</dd>
<dt>Symbol</dt><dd>${entry.symbol || "--"}</dd>
<dt>Hebrew Letter</dt><dd>${entry.hebrewLetter || "--"}</dd>
<dt>Court Rank</dt><dd>${entry.courtRank || "--"}</dd>
<dt>ID</dt><dd>${entry.id}</dd>
</dl>
`;
const tarotCard = document.createElement("div");
tarotCard.className = "planet-meta-card";
const tarotTitle = document.createElement("strong");
tarotTitle.textContent = "Tarot Correspondence";
const tarotText = document.createElement("div");
tarotText.className = "planet-text";
tarotText.textContent = [
entry.aceCardName ? `Ace: ${entry.aceCardName}` : "",
entry.courtRank ? `Court Rank: ${entry.courtRank} (all suits)` : ""
].filter(Boolean).join(" · ") || "--";
tarotCard.append(tarotTitle, tarotText);
if (entry.aceCardName || entry.courtCardNames.length) {
const navWrap = document.createElement("div");
navWrap.className = "alpha-nav-btns";
if (entry.aceCardName) {
const tarotBtn = document.createElement("button");
tarotBtn.type = "button";
tarotBtn.className = "alpha-nav-btn";
tarotBtn.textContent = `Open ${entry.aceCardName}`;
tarotBtn.addEventListener("click", () => {
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
detail: { cardName: entry.aceCardName }
}));
});
navWrap.appendChild(tarotBtn);
}
entry.courtCardNames.forEach((cardName) => {
const courtBtn = document.createElement("button");
courtBtn.type = "button";
courtBtn.className = "alpha-nav-btn";
courtBtn.textContent = `Open ${cardName}`;
courtBtn.addEventListener("click", () => {
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
detail: { cardName }
}));
});
navWrap.appendChild(courtBtn);
});
tarotCard.appendChild(navWrap);
}
const smallCardCard = document.createElement("div");
smallCardCard.className = "planet-meta-card";
const smallCardTitle = document.createElement("strong");
smallCardTitle.textContent = "Small Card Sign Types";
smallCardCard.appendChild(smallCardTitle);
const smallCardStack = document.createElement("div");
smallCardStack.className = "cal-item-stack";
(entry.smallCardGroups || []).forEach((group) => {
const row = document.createElement("div");
row.className = "cal-item-row";
const head = document.createElement("div");
head.className = "cal-item-head";
head.innerHTML = `
<span class="cal-item-name">${group.rangeLabel} · ${group.modality}</span>
<span class="planet-list-meta">${group.signName || "--"}</span>
`;
row.appendChild(head);
const navWrap = document.createElement("div");
navWrap.className = "alpha-nav-btns";
if (group.signId) {
const signBtn = document.createElement("button");
signBtn.type = "button";
signBtn.className = "alpha-nav-btn";
signBtn.textContent = `Open ${group.signName}`;
signBtn.addEventListener("click", () => {
document.dispatchEvent(new CustomEvent("nav:zodiac", {
detail: { signId: group.signId }
}));
});
navWrap.appendChild(signBtn);
}
(group.cardNames || []).forEach((cardName) => {
const cardBtn = document.createElement("button");
cardBtn.type = "button";
cardBtn.className = "alpha-nav-btn";
cardBtn.textContent = `Open ${cardName}`;
cardBtn.addEventListener("click", () => {
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
detail: { cardName }
}));
});
navWrap.appendChild(cardBtn);
});
row.appendChild(navWrap);
smallCardStack.appendChild(row);
});
smallCardCard.appendChild(smallCardStack);
grid.append(detailsCard, tarotCard, smallCardCard);
elements.detailBodyEl.appendChild(grid);
}
function applyFilter(elements) {
const query = normalize(state.searchQuery);
state.filteredEntries = query
? state.entries.filter((entry) => entry.searchText.includes(query))
: [...state.entries];
if (elements?.searchClearEl) {
elements.searchClearEl.disabled = !query;
}
if (!state.filteredEntries.some((entry) => entry.id === state.selectedId)) {
state.selectedId = state.filteredEntries[0]?.id || "";
}
renderList(elements);
renderDetail(elements);
}
function selectByElementId(elementId) {
const target = findEntryById(elementId);
if (!target) {
return false;
}
const elements = getElements();
state.selectedId = target.id;
renderList(elements);
renderDetail(elements);
const listItem = elements.listEl?.querySelector(`[data-element-id="${target.id}"]`);
listItem?.scrollIntoView({ block: "nearest" });
return true;
}
function ensureElementsSection(magickDataset) {
const elements = getElements();
if (!elements.listEl || !elements.detailBodyEl) {
return;
}
state.entries = buildEntries(magickDataset);
if (!state.selectedId && state.entries.length) {
state.selectedId = state.entries[0].id;
}
applyFilter(elements);
if (state.initialized) {
return;
}
elements.listEl.addEventListener("click", (event) => {
const target = event.target instanceof Element
? event.target.closest(".planet-list-item")
: null;
if (!(target instanceof HTMLButtonElement)) {
return;
}
const elementId = target.dataset.elementId;
if (!elementId) {
return;
}
state.selectedId = elementId;
renderList(elements);
renderDetail(elements);
});
if (elements.searchEl) {
elements.searchEl.addEventListener("input", () => {
state.searchQuery = elements.searchEl.value || "";
applyFilter(elements);
});
}
if (elements.searchClearEl && elements.searchEl) {
elements.searchClearEl.addEventListener("click", () => {
state.searchQuery = "";
elements.searchEl.value = "";
applyFilter(elements);
elements.searchEl.focus();
});
}
state.initialized = true;
}
window.ElementsSectionUi = {
ensureElementsSection,
selectByElementId
};
})();