Initial commit

This commit is contained in:
2026-03-07 01:09:00 -08:00
commit af7d63717e
102 changed files with 68739 additions and 0 deletions

459
app/ui-enochian.js Normal file
View File

@@ -0,0 +1,459 @@
(function () {
"use strict";
const TABLET_META = {
union: { label: "Enochian Tablet of Union", element: "Spirit", order: 0 },
spirit: { label: "Enochian Tablet of Union", element: "Spirit", order: 0 },
earth: { label: "Enochian Tablet of Earth", element: "Earth", order: 1 },
air: { label: "Enochian Tablet of Air", element: "Air", order: 2 },
water: { label: "Enochian Tablet of Water", element: "Water", order: 3 },
fire: { label: "Enochian Tablet of Fire", element: "Fire", order: 4 }
};
const TAROT_NAME_ALIASES = {
juggler: "magus",
magician: "magus",
strength: "lust",
temperance: "art",
judgement: "aeon",
judgment: "aeon",
charit: "chariot"
};
const state = {
initialized: false,
entries: [],
filteredEntries: [],
selectedId: "",
selectedCell: null,
searchQuery: "",
lettersById: new Map()
};
function getElements() {
return {
listEl: document.getElementById("enochian-list"),
countEl: document.getElementById("enochian-count"),
searchEl: document.getElementById("enochian-search-input"),
searchClearEl: document.getElementById("enochian-search-clear"),
detailNameEl: document.getElementById("enochian-detail-name"),
detailSubEl: document.getElementById("enochian-detail-sub"),
detailBodyEl: document.getElementById("enochian-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 getTabletMeta(id) {
const normalizedId = normalize(id);
return TABLET_META[normalizedId] || {
label: `Enochian Tablet of ${titleCase(normalizedId || "Unknown")}`,
element: titleCase(normalizedId || "Unknown"),
order: 99
};
}
function buildSearchText(entry) {
return normalize([
entry.id,
entry.label,
entry.element,
entry.rowCount,
entry.colCount,
...entry.uniqueLetters
].join(" "));
}
function buildEntries(magickDataset) {
const tablets = magickDataset?.grouped?.enochian?.tablets;
if (!tablets || typeof tablets !== "object") {
return [];
}
return Object.entries(tablets)
.map(([key, value]) => {
const id = normalize(value?.id || key);
const grid = Array.isArray(value?.grid)
? value.grid.map((row) => (Array.isArray(row)
? row.map((cell) => String(cell || "").trim())
: []))
: [];
const rowCount = grid.length;
const colCount = grid.reduce((max, row) => Math.max(max, row.length), 0);
const uniqueLetters = [...new Set(
grid
.flat()
.map((cell) => String(cell || "").trim().toUpperCase())
.filter(Boolean)
)].sort((left, right) => left.localeCompare(right));
const meta = getTabletMeta(id);
return {
id,
grid,
rowCount,
colCount,
uniqueLetters,
element: meta.element,
label: meta.label,
order: Number(meta.order)
};
})
.sort((left, right) => left.order - right.order || left.label.localeCompare(right.label));
}
function buildLetterMap(magickDataset) {
const letters = magickDataset?.grouped?.enochian?.letters;
if (!letters || typeof letters !== "object") {
return new Map();
}
return new Map(
Object.entries(letters)
.map(([key, value]) => [String(key || "").trim().toUpperCase(), value])
.filter(([key]) => Boolean(key))
);
}
function findEntryById(id) {
return state.entries.find((entry) => entry.id === id) || null;
}
function getDefaultCell(entry) {
if (!entry || !Array.isArray(entry.grid)) {
return null;
}
for (let rowIndex = 0; rowIndex < entry.grid.length; rowIndex += 1) {
const row = entry.grid[rowIndex];
for (let colIndex = 0; colIndex < row.length; colIndex += 1) {
const value = String(row[colIndex] || "").trim();
if (value) {
return { rowIndex, colIndex, value };
}
}
}
return 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 = "enoch-list-item";
button.dataset.tabletId = 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 = "enoch-list-name";
name.textContent = entry.label;
const meta = document.createElement("span");
meta.className = "enoch-list-meta";
meta.textContent = `${entry.element} · ${entry.rowCount}×${entry.colCount} · ${entry.uniqueLetters.length} letters`;
button.append(name, meta);
elements.listEl.appendChild(button);
});
if (elements.countEl) {
elements.countEl.textContent = `${state.filteredEntries.length} tablets`;
}
if (!state.filteredEntries.length) {
const empty = document.createElement("div");
empty.className = "planet-text";
empty.style.padding = "16px";
empty.style.color = "#71717a";
empty.textContent = "No Enochian tablets match your search.";
elements.listEl.appendChild(empty);
}
}
function resolveTarotCardName(value) {
const normalized = normalize(value);
if (!normalized) {
return "";
}
return TAROT_NAME_ALIASES[normalized] || normalized;
}
function renderDetail(elements) {
if (!elements?.detailBodyEl || !elements.detailNameEl || !elements.detailSubEl) {
return;
}
const entry = findEntryById(state.selectedId);
elements.detailBodyEl.replaceChildren();
if (!entry) {
elements.detailNameEl.textContent = "--";
elements.detailSubEl.textContent = "Select a tablet to explore";
return;
}
elements.detailNameEl.textContent = entry.label;
elements.detailSubEl.textContent = `${entry.element} Tablet · ${entry.rowCount} rows × ${entry.colCount} columns`;
if (!state.selectedCell) {
state.selectedCell = getDefaultCell(entry);
}
const detailGrid = document.createElement("div");
detailGrid.className = "planet-meta-grid";
const summaryCard = document.createElement("div");
summaryCard.className = "planet-meta-card";
summaryCard.innerHTML = `
<strong>Tablet Overview</strong>
<div class="enoch-letter-meta">
<div>${entry.label}</div>
<div>${entry.rowCount} rows × ${entry.colCount} columns</div>
<div>Unique letters: ${entry.uniqueLetters.length}</div>
</div>
`;
const gridCard = document.createElement("div");
gridCard.className = "planet-meta-card";
const gridTitle = document.createElement("strong");
gridTitle.textContent = "Tablet Grid";
const gridEl = document.createElement("div");
gridEl.className = "enoch-grid";
entry.grid.forEach((row, rowIndex) => {
const rowEl = document.createElement("div");
rowEl.className = "enoch-grid-row";
row.forEach((cell, colIndex) => {
const value = String(cell || "").trim();
const cellBtn = document.createElement("button");
cellBtn.type = "button";
cellBtn.className = "enoch-grid-cell";
cellBtn.textContent = value || "·";
const isSelectedCell = state.selectedCell
&& state.selectedCell.rowIndex === rowIndex
&& state.selectedCell.colIndex === colIndex;
cellBtn.classList.toggle("is-selected", Boolean(isSelectedCell));
cellBtn.addEventListener("click", () => {
state.selectedCell = { rowIndex, colIndex, value };
renderDetail(elements);
});
rowEl.appendChild(cellBtn);
});
gridEl.appendChild(rowEl);
});
gridCard.append(gridTitle, gridEl);
const letterCard = document.createElement("div");
letterCard.className = "planet-meta-card";
const letterTitle = document.createElement("strong");
letterTitle.textContent = "Selected Letter";
const selectedLetter = String(state.selectedCell?.value || "").trim().toUpperCase();
const letterData = selectedLetter ? state.lettersById.get(selectedLetter) : null;
const letterContent = document.createElement("div");
letterContent.className = "enoch-letter-meta";
if (!selectedLetter) {
letterContent.textContent = "Select any grid cell to inspect its correspondence data.";
} else {
const firstRow = document.createElement("div");
firstRow.className = "enoch-letter-row";
const chip = document.createElement("span");
chip.className = "enoch-letter-chip";
chip.textContent = selectedLetter;
firstRow.appendChild(chip);
const title = document.createElement("span");
title.textContent = letterData?.title
? `${letterData.title}${letterData.english ? ` · ${letterData.english}` : ""}`
: "No letter metadata yet";
firstRow.appendChild(title);
letterContent.appendChild(firstRow);
if (letterData) {
const detailRows = [
["Pronunciation", letterData.pronounciation],
["Planet / Element", letterData["planet/element"]],
["Tarot", letterData.tarot],
["Gematria", letterData.gematria]
];
detailRows.forEach(([label, value]) => {
if (value === undefined || value === null || String(value).trim() === "") {
return;
}
const row = document.createElement("div");
row.className = "enoch-letter-row";
row.innerHTML = `<span style="color:#a1a1aa">${label}:</span><span>${value}</span>`;
letterContent.appendChild(row);
});
const navRow = document.createElement("div");
navRow.className = "enoch-letter-row";
const tarotCardName = resolveTarotCardName(letterData.tarot);
if (tarotCardName) {
const tarotBtn = document.createElement("button");
tarotBtn.type = "button";
tarotBtn.className = "enoch-nav-btn";
tarotBtn.textContent = `Open Tarot (${titleCase(tarotCardName)}) ↗`;
tarotBtn.addEventListener("click", () => {
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
detail: { cardName: tarotCardName }
}));
});
navRow.appendChild(tarotBtn);
}
const alphabetBtn = document.createElement("button");
alphabetBtn.type = "button";
alphabetBtn.className = "enoch-nav-btn";
alphabetBtn.textContent = "Open Alphabet ↗";
alphabetBtn.addEventListener("click", () => {
document.dispatchEvent(new CustomEvent("nav:alphabet", {
detail: {
alphabet: "english",
englishLetter: selectedLetter
}
}));
});
navRow.appendChild(alphabetBtn);
letterContent.appendChild(navRow);
}
}
letterCard.append(letterTitle, letterContent);
detailGrid.append(summaryCard, letterCard, gridCard);
elements.detailBodyEl.appendChild(detailGrid);
}
function applyFilter(elements) {
const query = normalize(state.searchQuery);
state.filteredEntries = query
? state.entries.filter((entry) => buildSearchText(entry).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 || "";
state.selectedCell = state.selectedId ? getDefaultCell(findEntryById(state.selectedId)) : null;
}
renderList(elements);
renderDetail(elements);
}
function selectByTabletId(tabletId) {
const elements = getElements();
const target = findEntryById(normalize(tabletId));
if (!target) {
return false;
}
state.selectedId = target.id;
state.selectedCell = getDefaultCell(target);
renderList(elements);
renderDetail(elements);
return true;
}
function ensureEnochianSection(magickDataset) {
const elements = getElements();
if (!elements.listEl || !elements.detailBodyEl) {
return;
}
state.entries = buildEntries(magickDataset);
state.lettersById = buildLetterMap(magickDataset);
if (!state.selectedId && state.entries.length) {
state.selectedId = state.entries[0].id;
state.selectedCell = getDefaultCell(state.entries[0]);
}
applyFilter(elements);
if (state.initialized) {
return;
}
elements.listEl.addEventListener("click", (event) => {
const target = event.target instanceof Element
? event.target.closest(".enoch-list-item")
: null;
if (!(target instanceof HTMLButtonElement)) {
return;
}
const tabletId = target.dataset.tabletId;
if (!tabletId) {
return;
}
state.selectedId = tabletId;
state.selectedCell = getDefaultCell(findEntryById(tabletId));
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.EnochianSectionUi = {
ensureEnochianSection,
selectByTabletId
};
})();