refactoring
This commit is contained in:
199
app/app-runtime.js
Normal file
199
app/app-runtime.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
calendar: null,
|
||||||
|
baseWeekOptions: null,
|
||||||
|
defaultSettings: null,
|
||||||
|
latEl: null,
|
||||||
|
lngEl: null,
|
||||||
|
nowElements: null,
|
||||||
|
calendarVisualsUi: null,
|
||||||
|
homeUi: null,
|
||||||
|
onStatus: null,
|
||||||
|
services: {},
|
||||||
|
ensure: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
let referenceData = null;
|
||||||
|
let magickDataset = null;
|
||||||
|
let currentGeo = null;
|
||||||
|
let nowInterval = null;
|
||||||
|
let centeredDayKey = "";
|
||||||
|
let renderInProgress = false;
|
||||||
|
let currentTimeFormat = "minutes";
|
||||||
|
let currentSettings = null;
|
||||||
|
|
||||||
|
function setStatus(text) {
|
||||||
|
config.onStatus?.(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReferenceData() {
|
||||||
|
return referenceData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMagickDataset() {
|
||||||
|
return magickDataset;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentGeo() {
|
||||||
|
return currentGeo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentTimeFormat() {
|
||||||
|
return currentTimeFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSettings() {
|
||||||
|
return currentSettings ? { ...currentSettings } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseGeoInput() {
|
||||||
|
const latitude = Number(config.latEl?.value);
|
||||||
|
const longitude = Number(config.lngEl?.value);
|
||||||
|
|
||||||
|
if (Number.isNaN(latitude) || Number.isNaN(longitude)) {
|
||||||
|
throw new Error("Latitude/Longitude must be valid numbers.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { latitude, longitude };
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCenteredWeekWindow(date) {
|
||||||
|
const startDayOfWeek = config.services.getCenteredWeekStartDay?.(date) ?? 0;
|
||||||
|
config.calendar?.setOptions?.({
|
||||||
|
week: {
|
||||||
|
...(config.baseWeekOptions || {}),
|
||||||
|
startDayOfWeek
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config.calendarVisualsUi?.applyTimeFormatTemplates?.();
|
||||||
|
config.calendar?.changeView?.("week");
|
||||||
|
config.calendar?.setDate?.(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startNowTicker() {
|
||||||
|
if (nowInterval) {
|
||||||
|
clearInterval(nowInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
if (!referenceData || !currentGeo || renderInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
config.homeUi?.syncNowPanelTheme?.(now);
|
||||||
|
const currentDayKey = config.services.getDateKey?.(now) || "";
|
||||||
|
if (currentDayKey !== centeredDayKey) {
|
||||||
|
centeredDayKey = currentDayKey;
|
||||||
|
void renderWeek();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.services.updateNowPanel?.(referenceData, currentGeo, config.nowElements, currentTimeFormat);
|
||||||
|
config.calendarVisualsUi?.applyDynamicNowIndicatorVisual?.(now);
|
||||||
|
};
|
||||||
|
|
||||||
|
tick();
|
||||||
|
nowInterval = setInterval(tick, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderWeek() {
|
||||||
|
if (renderInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInProgress = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
currentGeo = parseGeoInput();
|
||||||
|
config.homeUi?.syncNowPanelTheme?.(new Date());
|
||||||
|
config.homeUi?.syncNowSkyBackground?.(currentGeo);
|
||||||
|
|
||||||
|
if (!referenceData || !magickDataset) {
|
||||||
|
setStatus("Loading planetary, sign and decan tarot correspondences...");
|
||||||
|
const [loadedReference, loadedMagick] = await Promise.all([
|
||||||
|
referenceData ? Promise.resolve(referenceData) : config.services.loadReferenceData?.(),
|
||||||
|
magickDataset
|
||||||
|
? Promise.resolve(magickDataset)
|
||||||
|
: config.services.loadMagickDataset?.().catch(() => null)
|
||||||
|
]);
|
||||||
|
|
||||||
|
referenceData = loadedReference;
|
||||||
|
magickDataset = loadedMagick;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.ensure.ensureTarotSection?.(referenceData, magickDataset);
|
||||||
|
config.ensure.ensurePlanetSection?.(referenceData, magickDataset);
|
||||||
|
config.ensure.ensureCyclesSection?.(referenceData);
|
||||||
|
config.ensure.ensureIChingSection?.(referenceData);
|
||||||
|
config.ensure.ensureCalendarSection?.(referenceData, magickDataset);
|
||||||
|
config.ensure.ensureHolidaySection?.(referenceData, magickDataset);
|
||||||
|
config.ensure.ensureNatalPanel?.(referenceData);
|
||||||
|
config.ensure.ensureQuizSection?.(referenceData, magickDataset);
|
||||||
|
|
||||||
|
const anchorDate = new Date();
|
||||||
|
centeredDayKey = config.services.getDateKey?.(anchorDate) || "";
|
||||||
|
|
||||||
|
applyCenteredWeekWindow(anchorDate);
|
||||||
|
|
||||||
|
const events = config.services.buildWeekEvents?.(currentGeo, referenceData, anchorDate) || [];
|
||||||
|
config.calendar?.clear?.();
|
||||||
|
config.calendar?.createEvents?.(events);
|
||||||
|
config.calendarVisualsUi?.applySunRulerGradient?.(anchorDate);
|
||||||
|
config.calendarVisualsUi?.updateMonthStrip?.();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
config.calendarVisualsUi?.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 applySettings(settings) {
|
||||||
|
currentTimeFormat = settings?.timeFormat || "minutes";
|
||||||
|
currentSettings = settings ? { ...settings } : { ...(config.defaultSettings || {}) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(nextConfig = {}) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...nextConfig,
|
||||||
|
services: {
|
||||||
|
...(config.services || {}),
|
||||||
|
...(nextConfig.services || {})
|
||||||
|
},
|
||||||
|
ensure: {
|
||||||
|
...(config.ensure || {}),
|
||||||
|
...(nextConfig.ensure || {})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!currentSettings) {
|
||||||
|
currentSettings = { ...(config.defaultSettings || {}) };
|
||||||
|
currentTimeFormat = currentSettings.timeFormat || "minutes";
|
||||||
|
}
|
||||||
|
|
||||||
|
centeredDayKey = config.services.getDateKey?.(new Date()) || centeredDayKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotAppRuntime = {
|
||||||
|
...(window.TarotAppRuntime || {}),
|
||||||
|
init,
|
||||||
|
parseGeoInput,
|
||||||
|
applyCenteredWeekWindow,
|
||||||
|
renderWeek,
|
||||||
|
applySettings,
|
||||||
|
getReferenceData,
|
||||||
|
getMagickDataset,
|
||||||
|
getCurrentGeo,
|
||||||
|
getCurrentTimeFormat,
|
||||||
|
getCurrentSettings
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -101,9 +101,10 @@
|
|||||||
|
|
||||||
const DECK_REGISTRY_PATH = "asset/tarot deck/decks.json";
|
const DECK_REGISTRY_PATH = "asset/tarot deck/decks.json";
|
||||||
|
|
||||||
const deckManifestSources = buildDeckManifestSources();
|
let deckManifestSources = buildDeckManifestSources();
|
||||||
|
|
||||||
const manifestCache = new Map();
|
const manifestCache = new Map();
|
||||||
|
const cardBackCache = new Map();
|
||||||
let activeDeckId = DEFAULT_DECK_ID;
|
let activeDeckId = DEFAULT_DECK_ID;
|
||||||
|
|
||||||
function canonicalMajorName(cardName) {
|
function canonicalMajorName(cardName) {
|
||||||
@@ -132,16 +133,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDeckId(deckId) {
|
function normalizeDeckId(deckId) {
|
||||||
|
const sources = getDeckManifestSources();
|
||||||
const normalized = String(deckId || "").trim().toLowerCase();
|
const normalized = String(deckId || "").trim().toLowerCase();
|
||||||
if (deckManifestSources[normalized]) {
|
if (sources[normalized]) {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deckManifestSources[DEFAULT_DECK_ID]) {
|
if (sources[DEFAULT_DECK_ID]) {
|
||||||
return DEFAULT_DECK_ID;
|
return DEFAULT_DECK_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackId = Object.keys(deckManifestSources)[0];
|
const fallbackId = Object.keys(sources)[0];
|
||||||
return fallbackId || DEFAULT_DECK_ID;
|
return fallbackId || DEFAULT_DECK_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +243,41 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRemoteAssetPath(pathValue) {
|
||||||
|
return /^(https?:)?\/\//i.test(String(pathValue || ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDeckAssetPath(manifest, relativeOrAbsolutePath) {
|
||||||
|
const normalizedPath = String(relativeOrAbsolutePath || "").trim();
|
||||||
|
if (!normalizedPath) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRemoteAssetPath(normalizedPath) || normalizedPath.startsWith("/")) {
|
||||||
|
return normalizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${manifest.basePath}/${normalizedPath.replace(/^\.\//, "")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDeckCardBackPath(manifest) {
|
||||||
|
if (!manifest) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const explicitCardBack = String(manifest.cardBack || "").trim();
|
||||||
|
if (explicitCardBack) {
|
||||||
|
return toDeckAssetPath(manifest, explicitCardBack) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const detectedCardBack = String(manifest.cardBackPath || "").trim();
|
||||||
|
if (detectedCardBack) {
|
||||||
|
return toDeckAssetPath(manifest, detectedCardBack) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function readManifestJsonSync(path) {
|
function readManifestJsonSync(path) {
|
||||||
try {
|
try {
|
||||||
const request = new XMLHttpRequest();
|
const request = new XMLHttpRequest();
|
||||||
@@ -276,7 +313,8 @@
|
|||||||
id,
|
id,
|
||||||
label: String(entry?.label || id),
|
label: String(entry?.label || id),
|
||||||
basePath,
|
basePath,
|
||||||
manifestPath
|
manifestPath,
|
||||||
|
cardBackPath: String(entry?.cardBackPath || "").trim()
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -292,6 +330,14 @@
|
|||||||
return toDeckSourceMap(registryDecks);
|
return toDeckSourceMap(registryDecks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDeckManifestSources(forceRefresh = false) {
|
||||||
|
if (forceRefresh || !deckManifestSources || Object.keys(deckManifestSources).length === 0) {
|
||||||
|
deckManifestSources = buildDeckManifestSources();
|
||||||
|
}
|
||||||
|
|
||||||
|
return deckManifestSources || {};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeDeckManifest(source, rawManifest) {
|
function normalizeDeckManifest(source, rawManifest) {
|
||||||
if (!rawManifest || typeof rawManifest !== "object") {
|
if (!rawManifest || typeof rawManifest !== "object") {
|
||||||
return null;
|
return null;
|
||||||
@@ -337,6 +383,8 @@
|
|||||||
id: source.id,
|
id: source.id,
|
||||||
label: String(rawManifest.label || source.label || source.id),
|
label: String(rawManifest.label || source.label || source.id),
|
||||||
basePath: String(source.basePath || "").replace(/\/$/, ""),
|
basePath: String(source.basePath || "").replace(/\/$/, ""),
|
||||||
|
cardBack: String(rawManifest.cardBack || "").trim(),
|
||||||
|
cardBackPath: String(source.cardBackPath || "").trim(),
|
||||||
majors: rawManifest.majors || {},
|
majors: rawManifest.majors || {},
|
||||||
minors: rawManifest.minors || {},
|
minors: rawManifest.minors || {},
|
||||||
nameOverrides,
|
nameOverrides,
|
||||||
@@ -351,15 +399,22 @@
|
|||||||
return manifestCache.get(normalizedDeckId);
|
return manifestCache.get(normalizedDeckId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = deckManifestSources[normalizedDeckId];
|
let sources = getDeckManifestSources();
|
||||||
|
let source = sources[normalizedDeckId];
|
||||||
|
if (!source) {
|
||||||
|
sources = getDeckManifestSources(true);
|
||||||
|
source = sources[normalizedDeckId];
|
||||||
|
}
|
||||||
|
|
||||||
if (!source) {
|
if (!source) {
|
||||||
manifestCache.set(normalizedDeckId, null);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawManifest = readManifestJsonSync(source.manifestPath);
|
const rawManifest = readManifestJsonSync(source.manifestPath);
|
||||||
const normalizedManifest = normalizeDeckManifest(source, rawManifest);
|
const normalizedManifest = normalizeDeckManifest(source, rawManifest);
|
||||||
manifestCache.set(normalizedDeckId, normalizedManifest);
|
if (normalizedManifest) {
|
||||||
|
manifestCache.set(normalizedDeckId, normalizedManifest);
|
||||||
|
}
|
||||||
return normalizedManifest;
|
return normalizedManifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,11 +586,23 @@
|
|||||||
return encodeURI(activePath);
|
return encodeURI(activePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeDeckId !== DEFAULT_DECK_ID) {
|
return null;
|
||||||
const fallbackPath = resolveWithDeck(DEFAULT_DECK_ID, cardName);
|
}
|
||||||
if (fallbackPath) {
|
|
||||||
return encodeURI(fallbackPath);
|
function resolveTarotCardBackImage(optionsOrDeckId) {
|
||||||
}
|
const { resolvedDeckId } = resolveDeckOptions(optionsOrDeckId);
|
||||||
|
|
||||||
|
if (cardBackCache.has(resolvedDeckId)) {
|
||||||
|
const cachedPath = cardBackCache.get(resolvedDeckId);
|
||||||
|
return cachedPath ? encodeURI(cachedPath) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifest = getDeckManifest(resolvedDeckId);
|
||||||
|
const activeBackPath = resolveDeckCardBackPath(manifest);
|
||||||
|
cardBackCache.set(resolvedDeckId, activeBackPath || null);
|
||||||
|
|
||||||
|
if (activeBackPath) {
|
||||||
|
return encodeURI(activeBackPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -629,7 +696,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDeckOptions() {
|
function getDeckOptions() {
|
||||||
return Object.values(deckManifestSources).map((source) => {
|
return Object.values(getDeckManifestSources()).map((source) => {
|
||||||
const manifest = getDeckManifest(source.id);
|
const manifest = getDeckManifest(source.id);
|
||||||
return {
|
return {
|
||||||
id: source.id,
|
id: source.id,
|
||||||
@@ -645,6 +712,7 @@
|
|||||||
|
|
||||||
window.TarotCardImages = {
|
window.TarotCardImages = {
|
||||||
resolveTarotCardImage,
|
resolveTarotCardImage,
|
||||||
|
resolveTarotCardBackImage,
|
||||||
getTarotCardDisplayName,
|
getTarotCardDisplayName,
|
||||||
getTarotCardSearchAliases,
|
getTarotCardSearchAliases,
|
||||||
setActiveDeck,
|
setActiveDeck,
|
||||||
|
|||||||
387
app/styles.css
387
app/styles.css
@@ -1084,6 +1084,132 @@
|
|||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kab-rose-layout {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(520px, 1.2fr) minmax(320px, 1fr);
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-panel {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: auto;
|
||||||
|
border-right: 1px solid #27272a;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 52% 40%, rgba(255, 255, 255, 0.06), transparent 36%),
|
||||||
|
linear-gradient(180deg, #020617 0%, #02030a 100%);
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-intro {
|
||||||
|
padding: 8px 14px 0;
|
||||||
|
color: #a1a1aa;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-cross-container {
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 8px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-cross-container > .kab-rose-svg {
|
||||||
|
width: min(100%, 980px);
|
||||||
|
max-height: min(100%, 1160px);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-petal {
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-petal-shape {
|
||||||
|
transition: transform 120ms ease, filter 120ms ease, stroke 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-petal:hover .kab-rose-petal-shape,
|
||||||
|
.kab-rose-petal:focus-visible .kab-rose-petal-shape {
|
||||||
|
transform: scale(1.07);
|
||||||
|
filter: brightness(1.14);
|
||||||
|
stroke: rgba(255, 255, 255, 0.75);
|
||||||
|
stroke-width: 2.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-petal.kab-path-active .kab-rose-petal-shape {
|
||||||
|
filter: brightness(1.2);
|
||||||
|
stroke: #f8fafc;
|
||||||
|
stroke-width: 2.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-petal-letter {
|
||||||
|
font-family: "Noto Sans Hebrew", var(--font-script-main), serif;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 700;
|
||||||
|
pointer-events: none;
|
||||||
|
fill: #f8fafc;
|
||||||
|
text-shadow: 0 1px 5px rgba(0, 0, 0, 0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-petal-number {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
pointer-events: none;
|
||||||
|
fill: rgba(241, 245, 249, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-arm-glyph {
|
||||||
|
pointer-events: none;
|
||||||
|
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-petal--mother .kab-rose-petal-letter,
|
||||||
|
.kab-rose-petal--double .kab-rose-petal-letter {
|
||||||
|
fill: #111827;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-petal--mother .kab-rose-petal-number,
|
||||||
|
.kab-rose-petal--double .kab-rose-petal-number {
|
||||||
|
fill: rgba(17, 24, 39, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1220px) {
|
||||||
|
.kab-rose-layout {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
grid-template-rows: minmax(360px, auto) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-panel {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #27272a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-cross-container > .kab-rose-svg {
|
||||||
|
width: min(100%, 860px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.kab-rose-cross-container {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-cross-container > .kab-rose-svg {
|
||||||
|
width: min(100%, 700px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-rose-petal-letter {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.natal-chart-summary {
|
.natal-chart-summary {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@@ -1322,6 +1448,16 @@
|
|||||||
transition: filter 120ms ease, opacity 120ms ease;
|
transition: filter 120ms ease, opacity 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cube-tarot-image {
|
||||||
|
cursor: zoom-in;
|
||||||
|
transition: filter 120ms ease, transform 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cube-tarot-image:hover {
|
||||||
|
filter: drop-shadow(0 0 4px rgba(224, 231, 255, 0.92));
|
||||||
|
transform: translateY(-0.6px);
|
||||||
|
}
|
||||||
|
|
||||||
.cube-direction:hover .cube-direction-card,
|
.cube-direction:hover .cube-direction-card,
|
||||||
.cube-direction-card.is-active {
|
.cube-direction-card.is-active {
|
||||||
filter: drop-shadow(0 0 3px currentColor) drop-shadow(0 0 8px currentColor);
|
filter: drop-shadow(0 0 3px currentColor) drop-shadow(0 0 8px currentColor);
|
||||||
@@ -1598,6 +1734,15 @@
|
|||||||
filter: drop-shadow(0 0 5px rgba(112, 96, 176, 0.78));
|
filter: drop-shadow(0 0 5px rgba(112, 96, 176, 0.78));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kab-path-tarot {
|
||||||
|
cursor: zoom-in;
|
||||||
|
transition: filter 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-path-tarot:hover {
|
||||||
|
filter: drop-shadow(0 0 6px rgba(196, 181, 253, 0.85));
|
||||||
|
}
|
||||||
|
|
||||||
.kab-path-lbl.kab-path-active {
|
.kab-path-lbl.kab-path-active {
|
||||||
fill: #c8b8f8 !important;
|
fill: #c8b8f8 !important;
|
||||||
}
|
}
|
||||||
@@ -1933,6 +2078,14 @@
|
|||||||
grid-row: 1 / -1;
|
grid-row: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tarot-spread-board {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tarot-spread-meanings {
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
|
||||||
#tarot-spread-view[hidden] {
|
#tarot-spread-view[hidden] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@@ -1998,6 +2151,13 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
transition: background 120ms, border-color 120ms;
|
transition: background 120ms, border-color 120ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tarot-spread-reveal-all:disabled,
|
||||||
|
.tarot-spread-redraw-btn:disabled {
|
||||||
|
opacity: 0.56;
|
||||||
|
cursor: default;
|
||||||
|
filter: saturate(0.72);
|
||||||
|
}
|
||||||
.tarot-spread-redraw-btn:hover {
|
.tarot-spread-redraw-btn:hover {
|
||||||
background: #312e81;
|
background: #312e81;
|
||||||
border-color: #6366f1;
|
border-color: #6366f1;
|
||||||
@@ -2054,97 +2214,236 @@
|
|||||||
|
|
||||||
/* ── Spread Board ──────────────────────────────────── */
|
/* ── Spread Board ──────────────────────────────────── */
|
||||||
.tarot-spread-board {
|
.tarot-spread-board {
|
||||||
display: flex;
|
--spread-card-width: 116px;
|
||||||
flex-wrap: wrap;
|
--spread-card-height: 184px;
|
||||||
gap: 1.25rem;
|
display: grid;
|
||||||
justify-content: center;
|
gap: 1rem;
|
||||||
padding: 0.5rem 0 1.5rem;
|
align-items: start;
|
||||||
|
justify-items: center;
|
||||||
|
padding: 1.1rem 1rem 1.5rem;
|
||||||
|
border: 1px solid #27272a;
|
||||||
|
border-radius: 18px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 12%, rgba(236, 72, 153, 0.14), transparent 40%),
|
||||||
|
radial-gradient(circle at 84% 86%, rgba(59, 130, 246, 0.14), transparent 44%),
|
||||||
|
linear-gradient(180deg, #0f0f1d 0%, #13131f 38%, #10101a 100%);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 18px 30px rgba(2, 6, 23, 0.44);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tarot-spread-board--three {
|
.tarot-spread-board--three {
|
||||||
flex-wrap: nowrap;
|
grid-template-columns: repeat(3, var(--spread-card-width));
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 2rem;
|
column-gap: 1.25rem;
|
||||||
|
row-gap: 1rem;
|
||||||
|
width: max-content;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tarot-spread-board--celtic {
|
.tarot-spread-board--celtic {
|
||||||
display: grid;
|
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
". crown . out"
|
". crown . out ."
|
||||||
"past present near-fut hope"
|
"past present near-fut hope ."
|
||||||
". chall . env"
|
". chall . env ."
|
||||||
". found . self";
|
". found . self .";
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
grid-template-columns: repeat(5, var(--spread-card-width));
|
||||||
gap: 0.8rem 1rem;
|
justify-content: center;
|
||||||
justify-items: center;
|
column-gap: 1rem;
|
||||||
|
row-gap: 0.9rem;
|
||||||
|
width: max-content;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spread-position { grid-area: unset; }
|
.tarot-spread-board--three .spread-position {
|
||||||
.spread-position[data-pos="crown"] { grid-area: crown; }
|
grid-area: auto;
|
||||||
.spread-position[data-pos="out"] { grid-area: out; }
|
}
|
||||||
.spread-position[data-pos="past"] { grid-area: past; }
|
|
||||||
.spread-position[data-pos="present"] { grid-area: present; }
|
.tarot-spread-board--celtic .spread-position {
|
||||||
.spread-position[data-pos="near-fut"] { grid-area: near-fut; }
|
grid-area: unset;
|
||||||
.spread-position[data-pos="hope"] { grid-area: hope; }
|
}
|
||||||
.spread-position[data-pos="chall"] { grid-area: chall; }
|
|
||||||
.spread-position[data-pos="env"] { grid-area: env; }
|
.tarot-spread-board--celtic .spread-position[data-pos="crown"] { grid-area: crown; }
|
||||||
.spread-position[data-pos="found"] { grid-area: found; }
|
.tarot-spread-board--celtic .spread-position[data-pos="out"] { grid-area: out; }
|
||||||
.spread-position[data-pos="self"] { grid-area: self; }
|
.tarot-spread-board--celtic .spread-position[data-pos="past"] { grid-area: past; }
|
||||||
|
.tarot-spread-board--celtic .spread-position[data-pos="present"] { grid-area: present; }
|
||||||
|
.tarot-spread-board--celtic .spread-position[data-pos="near-fut"] { grid-area: near-fut; }
|
||||||
|
.tarot-spread-board--celtic .spread-position[data-pos="hope"] { grid-area: hope; }
|
||||||
|
.tarot-spread-board--celtic .spread-position[data-pos="chall"] { grid-area: chall; }
|
||||||
|
.tarot-spread-board--celtic .spread-position[data-pos="env"] { grid-area: env; }
|
||||||
|
.tarot-spread-board--celtic .spread-position[data-pos="found"] { grid-area: found; }
|
||||||
|
.tarot-spread-board--celtic .spread-position[data-pos="self"] { grid-area: self; }
|
||||||
|
|
||||||
.spread-position {
|
.spread-position {
|
||||||
|
width: var(--spread-card-width);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.5rem;
|
||||||
max-width: 130px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.spread-pos-label {
|
.spread-pos-label {
|
||||||
font-size: 0.68rem;
|
font-size: 0.66rem;
|
||||||
color: #a5b4fc;
|
color: #c4b5fd;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.07em;
|
letter-spacing: 0.09em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.45);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.17rem 0.55rem;
|
||||||
|
background: rgba(76, 29, 149, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.spread-card-wrap {
|
.spread-card-wrap {
|
||||||
border-radius: 8px;
|
appearance: none;
|
||||||
|
border: 1px solid rgba(168, 162, 158, 0.34);
|
||||||
|
border-radius: 13px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 4px 18px rgba(0,0,0,0.55);
|
background: #09090f;
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
width: 100%;
|
||||||
background: #18181b;
|
height: var(--spread-card-height);
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||||
|
transition: transform 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spread-card-wrap.is-reversed .spread-card-img {
|
.spread-card-wrap:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
border-color: rgba(196, 181, 253, 0.75);
|
||||||
|
box-shadow: 0 14px 30px rgba(2, 6, 23, 0.65), 0 0 0 1px rgba(196, 181, 253, 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spread-card-wrap:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
border-color: #c4b5fd;
|
||||||
|
box-shadow: 0 0 0 2px rgba(196, 181, 253, 0.36), 0 10px 24px rgba(2, 6, 23, 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spread-card-wrap.is-facedown {
|
||||||
|
background:
|
||||||
|
linear-gradient(150deg, rgba(190, 24, 93, 0.45), rgba(49, 46, 129, 0.55)),
|
||||||
|
repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgba(255, 255, 255, 0.08) 0,
|
||||||
|
rgba(255, 255, 255, 0.08) 6px,
|
||||||
|
rgba(0, 0, 0, 0.08) 6px,
|
||||||
|
rgba(0, 0, 0, 0.08) 12px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spread-card-wrap.is-revealed.is-reversed .spread-card-img {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.spread-card-img {
|
.spread-card-img,
|
||||||
width: 90px;
|
.spread-card-back-img {
|
||||||
height: auto;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spread-card-back-fallback {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
color: #e9d5ff;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spread-card-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.6rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #e4e4e7;
|
||||||
|
background: #18181b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spread-card-name {
|
.spread-card-name {
|
||||||
font-size: 0.74rem;
|
font-size: 0.66rem;
|
||||||
color: #d4d4d8;
|
color: #fda4af;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
|
min-height: 1.2em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spread-reveal-hint {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
color: #a1a1aa;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spread-reversed-tag {
|
.spread-reversed-tag {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.66rem;
|
font-size: 0.66rem;
|
||||||
color: #fb7185;
|
color: #fb7185;
|
||||||
margin-top: 0.1rem;
|
margin-top: 0.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tarot-spread-meanings-empty {
|
||||||
|
border: 1px dashed #3f3f46;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
color: #a1a1aa;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(9, 9, 11, 0.72);
|
||||||
}
|
}
|
||||||
|
|
||||||
.spread-empty {
|
.spread-empty {
|
||||||
color: #52525b;
|
color: #71717a;
|
||||||
padding: 2.5rem;
|
padding: 2.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.9rem;
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.tarot-spread-board--celtic {
|
||||||
|
grid-template-areas:
|
||||||
|
"crown out"
|
||||||
|
"past present"
|
||||||
|
"near-fut hope"
|
||||||
|
"chall env"
|
||||||
|
"found self";
|
||||||
|
grid-template-columns: repeat(2, var(--spread-card-width));
|
||||||
|
width: max-content;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.tarot-spread-board {
|
||||||
|
--spread-card-width: 106px;
|
||||||
|
--spread-card-height: 170px;
|
||||||
|
padding: 0.8rem 0.65rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tarot-spread-board--three {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
width: var(--spread-card-width);
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spread-position {
|
||||||
|
width: var(--spread-card-width);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.alpha-dl dd { margin: 0; }
|
.alpha-dl dd { margin: 0; }
|
||||||
.alpha-badge {
|
.alpha-badge {
|
||||||
|
|||||||
608
app/ui-alphabet-detail.js
Normal file
608
app/ui-alphabet-detail.js
Normal file
@@ -0,0 +1,608 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
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, digitalRoot) {
|
||||||
|
const normalized = Math.abs(Math.trunc(Number(value)));
|
||||||
|
if (!Number.isFinite(normalized) || !Number.isFinite(digitalRoot)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized < 10) {
|
||||||
|
return String(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${String(normalized).split("").join(" + ")} = ${digitalRoot}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPositionDigitalRootCard(letter, alphabet, context, orderLabel) {
|
||||||
|
const index = Number(letter?.index);
|
||||||
|
if (!Number.isFinite(index)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = Math.trunc(index);
|
||||||
|
if (position <= 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const digitalRoot = computeDigitalRoot(position);
|
||||||
|
if (!Number.isFinite(digitalRoot)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = Array.isArray(context.alphabets?.[alphabet]) ? context.alphabets[alphabet] : [];
|
||||||
|
const countText = entries.length ? ` of ${entries.length}` : "";
|
||||||
|
const orderText = orderLabel ? ` (${orderLabel})` : "";
|
||||||
|
const reductionText = describeDigitalRootReduction(position, digitalRoot);
|
||||||
|
const openNumberBtn = context.navBtn(`View Number ${digitalRoot}`, "nav:number", { value: digitalRoot });
|
||||||
|
|
||||||
|
return context.card("Position Digital Root", `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Position</dt><dd>#${position}${countText}${orderText}</dd>
|
||||||
|
<dt>Digital Root</dt><dd>${digitalRoot}${reductionText ? ` (${reductionText})` : ""}</dd>
|
||||||
|
</dl>
|
||||||
|
<div class="alpha-nav-btns">${openNumberBtn}</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function monthRefsForLetter(letter, context) {
|
||||||
|
const hebrewLetterId = context.normalizeId(letter?.hebrewLetterId);
|
||||||
|
if (!hebrewLetterId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return context.monthRefsByHebrewId.get(hebrewLetterId) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function calendarMonthsCard(monthRefs, titleLabel, context) {
|
||||||
|
if (!monthRefs.length) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthButtons = monthRefs
|
||||||
|
.map((month) => context.navBtn(month.label || month.name, "nav:calendar-month", { "month-id": month.id }))
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return context.card("Calendar Months", `
|
||||||
|
<div>${titleLabel}</div>
|
||||||
|
<div class="alpha-nav-btns">${monthButtons}</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAstrologyCard(astrology, context) {
|
||||||
|
if (!astrology) return "";
|
||||||
|
const { type, name } = astrology;
|
||||||
|
const id = (name || "").toLowerCase();
|
||||||
|
|
||||||
|
if (type === "planet") {
|
||||||
|
const sym = context.PLANET_SYMBOLS[id] || "";
|
||||||
|
const cubePlacement = context.getCubePlacementForPlanet(id);
|
||||||
|
const cubeBtn = context.cubePlacementBtn(cubePlacement, { "planet-id": id });
|
||||||
|
return context.card("Astrology", `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Type</dt><dd>Planet</dd>
|
||||||
|
<dt>Ruler</dt><dd>${sym} ${context.cap(id)}</dd>
|
||||||
|
</dl>
|
||||||
|
<div class="alpha-nav-btns">
|
||||||
|
<button class="alpha-nav-btn" data-event="nav:planet" data-planet-id="${id}">View ${context.cap(id)} ↗</button>
|
||||||
|
${cubeBtn}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
if (type === "zodiac") {
|
||||||
|
const sym = context.ZODIAC_SYMBOLS[id] || "";
|
||||||
|
const cubePlacement = context.getCubePlacementForSign(id);
|
||||||
|
const cubeBtn = context.cubePlacementBtn(cubePlacement, { "sign-id": id });
|
||||||
|
return context.card("Astrology", `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Type</dt><dd>Zodiac Sign</dd>
|
||||||
|
<dt>Sign</dt><dd>${sym} ${context.cap(id)}</dd>
|
||||||
|
</dl>
|
||||||
|
<div class="alpha-nav-btns">
|
||||||
|
<button class="alpha-nav-btn" data-event="nav:zodiac" data-sign-id="${id}">View ${context.cap(id)} ↗</button>
|
||||||
|
${cubeBtn}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
if (type === "element") {
|
||||||
|
const elemEmoji = { air: "💨", water: "💧", fire: "🔥", earth: "🌍" };
|
||||||
|
return context.card("Astrology", `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Type</dt><dd>Element</dd>
|
||||||
|
<dt>Element</dt><dd>${elemEmoji[id] || ""} ${context.cap(id)}</dd>
|
||||||
|
</dl>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
return context.card("Astrology", `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Type</dt><dd>${context.cap(type)}</dd>
|
||||||
|
<dt>Name</dt><dd>${context.cap(name)}</dd>
|
||||||
|
</dl>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHebrewDualityCard(letter, context) {
|
||||||
|
const duality = context.HEBREW_DOUBLE_DUALITY[context.normalizeId(letter?.hebrewLetterId)];
|
||||||
|
if (!duality) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.card("Duality", `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Polarity</dt><dd>${duality.left} / ${duality.right}</dd>
|
||||||
|
</dl>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHebrewFourWorldsCard(letter, context) {
|
||||||
|
const letterId = context.normalizeLetterId(letter?.hebrewLetterId || letter?.transliteration || letter?.char);
|
||||||
|
if (!letterId) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = (Array.isArray(context.fourWorldLayers) ? context.fourWorldLayers : [])
|
||||||
|
.filter((entry) => entry?.hebrewLetterId === letterId);
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = rows.map((entry) => {
|
||||||
|
const pathBtn = Number.isFinite(Number(entry?.pathNumber))
|
||||||
|
? context.navBtn(`View Path ${entry.pathNumber}`, "nav:kabbalah-path", { "path-no": Number(entry.pathNumber) })
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="cal-item-row">
|
||||||
|
<div class="cal-item-head">
|
||||||
|
<span class="cal-item-name">${entry.slot}: ${entry.letterChar} — ${entry.world}</span>
|
||||||
|
<span class="planet-list-meta">${entry.soulLayer}</span>
|
||||||
|
</div>
|
||||||
|
<div class="planet-text">${entry.worldLayer}${entry.worldDescription ? ` · ${entry.worldDescription}` : ""}</div>
|
||||||
|
<div class="planet-text">${entry.soulLayer}${entry.soulTitle ? ` — ${entry.soulTitle}` : ""}${entry.soulDescription ? `: ${entry.soulDescription}` : ""}</div>
|
||||||
|
<div class="alpha-nav-btns">${pathBtn}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return context.card("Qabalistic Worlds & Soul Layers", `<div class="cal-item-stack">${body}</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLatinLetter(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toUpperCase()
|
||||||
|
.replace(/[^A-Z]/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractEnglishLetterRefs(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return [...new Set(value.map((entry) => normalizeLatinLetter(entry)).filter(Boolean))];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(
|
||||||
|
String(value || "")
|
||||||
|
.split(/[\s,;|\/]+/)
|
||||||
|
.map((entry) => normalizeLatinLetter(entry))
|
||||||
|
.filter(Boolean)
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAlphabetEquivalentCard(activeAlphabet, letter, context) {
|
||||||
|
const hebrewLetters = Array.isArray(context.alphabets?.hebrew) ? context.alphabets.hebrew : [];
|
||||||
|
const greekLetters = Array.isArray(context.alphabets?.greek) ? context.alphabets.greek : [];
|
||||||
|
const englishLetters = Array.isArray(context.alphabets?.english) ? context.alphabets.english : [];
|
||||||
|
const arabicLetters = Array.isArray(context.alphabets?.arabic) ? context.alphabets.arabic : [];
|
||||||
|
const enochianLetters = Array.isArray(context.alphabets?.enochian) ? context.alphabets.enochian : [];
|
||||||
|
const linkedHebrewIds = new Set();
|
||||||
|
const linkedEnglishLetters = new Set();
|
||||||
|
const buttons = [];
|
||||||
|
|
||||||
|
function addHebrewId(value) {
|
||||||
|
const id = context.normalizeId(value);
|
||||||
|
if (id) {
|
||||||
|
linkedHebrewIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEnglishLetter(value) {
|
||||||
|
const code = normalizeLatinLetter(value);
|
||||||
|
if (!code) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
linkedEnglishLetters.add(code);
|
||||||
|
englishLetters
|
||||||
|
.filter((entry) => normalizeLatinLetter(entry?.letter) === code)
|
||||||
|
.forEach((entry) => addHebrewId(entry?.hebrewLetterId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeAlphabet === "hebrew") {
|
||||||
|
addHebrewId(letter?.hebrewLetterId);
|
||||||
|
} else if (activeAlphabet === "greek") {
|
||||||
|
addHebrewId(letter?.hebrewLetterId);
|
||||||
|
englishLetters
|
||||||
|
.filter((entry) => context.normalizeId(entry?.greekEquivalent) === context.normalizeId(letter?.name))
|
||||||
|
.forEach((entry) => addEnglishLetter(entry?.letter));
|
||||||
|
} else if (activeAlphabet === "english") {
|
||||||
|
addEnglishLetter(letter?.letter);
|
||||||
|
addHebrewId(letter?.hebrewLetterId);
|
||||||
|
} else if (activeAlphabet === "arabic") {
|
||||||
|
addHebrewId(letter?.hebrewLetterId);
|
||||||
|
} else if (activeAlphabet === "enochian") {
|
||||||
|
extractEnglishLetterRefs(letter?.englishLetters).forEach((code) => addEnglishLetter(code));
|
||||||
|
addHebrewId(letter?.hebrewLetterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!linkedHebrewIds.size && !linkedEnglishLetters.size) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeHebrewKey = context.normalizeId(letter?.hebrewLetterId);
|
||||||
|
const activeGreekKey = context.normalizeId(letter?.name);
|
||||||
|
const activeEnglishKey = normalizeLatinLetter(letter?.letter);
|
||||||
|
const activeArabicKey = context.normalizeId(letter?.name);
|
||||||
|
const activeEnochianKey = context.normalizeId(letter?.id || letter?.char || letter?.title);
|
||||||
|
|
||||||
|
hebrewLetters.forEach((heb) => {
|
||||||
|
const key = context.normalizeId(heb?.hebrewLetterId);
|
||||||
|
if (!key || !linkedHebrewIds.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeAlphabet === "hebrew" && key === activeHebrewKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(`<button class="alpha-sister-btn" data-alpha="hebrew" data-key="${heb.hebrewLetterId}">
|
||||||
|
<span class="alpha-sister-glyph">${heb.char}</span>
|
||||||
|
<span class="alpha-sister-name">Hebrew: ${heb.name} (${heb.transliteration}) · gematria ${heb.numerology}</span>
|
||||||
|
</button>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
greekLetters.forEach((grk) => {
|
||||||
|
const key = context.normalizeId(grk?.name);
|
||||||
|
const viaHebrew = linkedHebrewIds.has(context.normalizeId(grk?.hebrewLetterId));
|
||||||
|
const viaEnglish = englishLetters.some((eng) => (
|
||||||
|
linkedEnglishLetters.has(normalizeLatinLetter(eng?.letter))
|
||||||
|
&& context.normalizeId(eng?.greekEquivalent) === key
|
||||||
|
));
|
||||||
|
if (!(viaHebrew || viaEnglish)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeAlphabet === "greek" && key === activeGreekKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(`<button class="alpha-sister-btn" data-alpha="greek" data-key="${grk.name}">
|
||||||
|
<span class="alpha-sister-glyph">${grk.char}</span>
|
||||||
|
<span class="alpha-sister-name">Greek: ${grk.displayName} (${grk.transliteration}) · isopsephy ${grk.numerology}</span>
|
||||||
|
</button>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
englishLetters.forEach((eng) => {
|
||||||
|
const key = normalizeLatinLetter(eng?.letter);
|
||||||
|
const viaLetter = linkedEnglishLetters.has(key);
|
||||||
|
const viaHebrew = linkedHebrewIds.has(context.normalizeId(eng?.hebrewLetterId));
|
||||||
|
if (!(viaLetter || viaHebrew)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeAlphabet === "english" && key === activeEnglishKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(`<button class="alpha-sister-btn" data-alpha="english" data-key="${eng.letter}">
|
||||||
|
<span class="alpha-sister-glyph">${eng.letter}</span>
|
||||||
|
<span class="alpha-sister-name">English: ${eng.letter} · pythagorean ${eng.pythagorean}</span>
|
||||||
|
</button>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
arabicLetters.forEach((arb) => {
|
||||||
|
const key = context.normalizeId(arb?.name);
|
||||||
|
if (!linkedHebrewIds.has(context.normalizeId(arb?.hebrewLetterId))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeAlphabet === "arabic" && key === activeArabicKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(`<button class="alpha-sister-btn" data-alpha="arabic" data-key="${arb.name}">
|
||||||
|
<span class="alpha-sister-glyph alpha-list-glyph--arabic">${arb.char}</span>
|
||||||
|
<span class="alpha-sister-name">Arabic: ${context.arabicDisplayName(arb)} — ${arb.nameArabic} (${arb.transliteration}) · abjad ${arb.abjad}</span>
|
||||||
|
</button>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
enochianLetters.forEach((eno) => {
|
||||||
|
const key = context.normalizeId(eno?.id || eno?.char || eno?.title);
|
||||||
|
const englishRefs = extractEnglishLetterRefs(eno?.englishLetters);
|
||||||
|
const viaHebrew = linkedHebrewIds.has(context.normalizeId(eno?.hebrewLetterId));
|
||||||
|
const viaEnglish = englishRefs.some((code) => linkedEnglishLetters.has(code));
|
||||||
|
if (!(viaHebrew || viaEnglish)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeAlphabet === "enochian" && key === activeEnochianKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(`<button class="alpha-sister-btn" data-alpha="enochian" data-key="${eno.id}">
|
||||||
|
${context.enochianGlyphImageHtml(eno, "alpha-enochian-glyph-img alpha-enochian-glyph-img--sister")}
|
||||||
|
<span class="alpha-sister-name">Enochian: ${eno.title} (${eno.transliteration}) · English ${englishRefs.join("/") || "—"}</span>
|
||||||
|
</button>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!buttons.length) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.card("ALPHABET EQUIVALENT", `<div class="alpha-sister-wrap">${buttons.join("")}</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHebrewDetail(context) {
|
||||||
|
const { letter, detailSubEl, detailBodyEl } = context;
|
||||||
|
detailSubEl.textContent = `${letter.name} — ${letter.transliteration}`;
|
||||||
|
detailBodyEl.innerHTML = "";
|
||||||
|
|
||||||
|
const sections = [];
|
||||||
|
sections.push(context.card("Letter Details", `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Character</dt><dd>${letter.char}</dd>
|
||||||
|
<dt>Name</dt><dd>${letter.name}</dd>
|
||||||
|
<dt>Transliteration</dt><dd>${letter.transliteration}</dd>
|
||||||
|
<dt>Meaning</dt><dd>${letter.meaning}</dd>
|
||||||
|
<dt>Gematria Value</dt><dd>${letter.numerology}</dd>
|
||||||
|
<dt>Letter Type</dt><dd class="alpha-badge alpha-badge--${letter.letterType}">${letter.letterType}</dd>
|
||||||
|
<dt>Position</dt><dd>#${letter.index} of 22</dd>
|
||||||
|
</dl>
|
||||||
|
`));
|
||||||
|
|
||||||
|
const positionRootCard = renderPositionDigitalRootCard(letter, "hebrew", context);
|
||||||
|
if (positionRootCard) {
|
||||||
|
sections.push(positionRootCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (letter.letterType === "double") {
|
||||||
|
const dualityCard = renderHebrewDualityCard(letter, context);
|
||||||
|
if (dualityCard) {
|
||||||
|
sections.push(dualityCard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fourWorldsCard = renderHebrewFourWorldsCard(letter, context);
|
||||||
|
if (fourWorldsCard) {
|
||||||
|
sections.push(fourWorldsCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (letter.astrology) {
|
||||||
|
sections.push(renderAstrologyCard(letter.astrology, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (letter.kabbalahPathNumber) {
|
||||||
|
const tarotPart = letter.tarot
|
||||||
|
? `<dt>Tarot Card</dt><dd>${letter.tarot.card} (Trump ${letter.tarot.trumpNumber})</dd>`
|
||||||
|
: "";
|
||||||
|
const kabBtn = context.navBtn("View Kabbalah Path", "tarot:view-kab-path", { "path-number": letter.kabbalahPathNumber });
|
||||||
|
const tarotBtn = letter.tarot
|
||||||
|
? context.navBtn("View Tarot Card", "kab:view-trump", { "trump-number": letter.tarot.trumpNumber })
|
||||||
|
: "";
|
||||||
|
const cubePlacement = context.getCubePlacementForHebrewLetter(letter.hebrewLetterId, letter.kabbalahPathNumber);
|
||||||
|
const cubeBtn = context.cubePlacementBtn(cubePlacement, {
|
||||||
|
"hebrew-letter-id": letter.hebrewLetterId,
|
||||||
|
"path-no": letter.kabbalahPathNumber
|
||||||
|
});
|
||||||
|
sections.push(context.card("Kabbalah & Tarot", `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Path Number</dt><dd>${letter.kabbalahPathNumber}</dd>
|
||||||
|
${tarotPart}
|
||||||
|
</dl>
|
||||||
|
<div class="alpha-nav-btns">${kabBtn}${tarotBtn}${cubeBtn}</div>
|
||||||
|
`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthRefs = monthRefsForLetter(letter, context);
|
||||||
|
const monthCard = calendarMonthsCard(monthRefs, `Calendar correspondences linked to ${letter.name}.`, context);
|
||||||
|
if (monthCard) {
|
||||||
|
sections.push(monthCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const equivalentsCard = renderAlphabetEquivalentCard("hebrew", letter, context);
|
||||||
|
if (equivalentsCard) {
|
||||||
|
sections.push(equivalentsCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
detailBodyEl.innerHTML = sections.join("");
|
||||||
|
context.attachDetailListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGreekDetail(context) {
|
||||||
|
const { letter, detailSubEl, detailBodyEl } = context;
|
||||||
|
const archaicBadge = letter.archaic ? ' <span class="alpha-badge alpha-badge--archaic">archaic</span>' : "";
|
||||||
|
detailSubEl.textContent = `${letter.displayName}${letter.archaic ? " (archaic)" : ""} — ${letter.transliteration}`;
|
||||||
|
detailBodyEl.innerHTML = "";
|
||||||
|
|
||||||
|
const sections = [];
|
||||||
|
const charRow = letter.charFinal
|
||||||
|
? `<dt>Form (final)</dt><dd>${letter.charFinal}</dd>`
|
||||||
|
: "";
|
||||||
|
sections.push(context.card("Letter Details", `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Uppercase</dt><dd>${letter.char}</dd>
|
||||||
|
<dt>Lowercase</dt><dd>${letter.charLower || "—"}</dd>
|
||||||
|
${charRow}
|
||||||
|
<dt>Name</dt><dd>${letter.displayName}${archaicBadge}</dd>
|
||||||
|
<dt>Transliteration</dt><dd>${letter.transliteration}</dd>
|
||||||
|
<dt>IPA</dt><dd>${letter.ipa || "—"}</dd>
|
||||||
|
<dt>Isopsephy Value</dt><dd>${letter.numerology}</dd>
|
||||||
|
<dt>Meaning / Origin</dt><dd>${letter.meaning || "—"}</dd>
|
||||||
|
</dl>
|
||||||
|
`));
|
||||||
|
|
||||||
|
const positionRootCard = renderPositionDigitalRootCard(letter, "greek", context);
|
||||||
|
if (positionRootCard) {
|
||||||
|
sections.push(positionRootCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const equivalentsCard = renderAlphabetEquivalentCard("greek", letter, context);
|
||||||
|
if (equivalentsCard) {
|
||||||
|
sections.push(equivalentsCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthRefs = monthRefsForLetter(letter, context);
|
||||||
|
const monthCard = calendarMonthsCard(monthRefs, `Calendar correspondences inherited via ${letter.displayName}'s Hebrew origin.`, context);
|
||||||
|
if (monthCard) {
|
||||||
|
sections.push(monthCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
detailBodyEl.innerHTML = sections.join("");
|
||||||
|
context.attachDetailListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEnglishDetail(context) {
|
||||||
|
const { letter, detailSubEl, detailBodyEl } = context;
|
||||||
|
detailSubEl.textContent = `Letter ${letter.letter} · position #${letter.index}`;
|
||||||
|
detailBodyEl.innerHTML = "";
|
||||||
|
|
||||||
|
const sections = [];
|
||||||
|
sections.push(context.card("Letter Details", `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Letter</dt><dd>${letter.letter}</dd>
|
||||||
|
<dt>Position</dt><dd>#${letter.index} of 26</dd>
|
||||||
|
<dt>IPA</dt><dd>${letter.ipa || "—"}</dd>
|
||||||
|
<dt>Pythagorean Value</dt><dd>${letter.pythagorean}</dd>
|
||||||
|
</dl>
|
||||||
|
`));
|
||||||
|
|
||||||
|
const positionRootCard = renderPositionDigitalRootCard(letter, "english", context);
|
||||||
|
if (positionRootCard) {
|
||||||
|
sections.push(positionRootCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const equivalentsCard = renderAlphabetEquivalentCard("english", letter, context);
|
||||||
|
if (equivalentsCard) {
|
||||||
|
sections.push(equivalentsCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthRefs = monthRefsForLetter(letter, context);
|
||||||
|
const monthCard = calendarMonthsCard(monthRefs, "Calendar correspondences linked through this letter's Hebrew correspondence.", context);
|
||||||
|
if (monthCard) {
|
||||||
|
sections.push(monthCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
detailBodyEl.innerHTML = sections.join("");
|
||||||
|
context.attachDetailListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderArabicDetail(context) {
|
||||||
|
const { letter, detailSubEl, detailBodyEl } = context;
|
||||||
|
detailSubEl.textContent = `${context.arabicDisplayName(letter)} — ${letter.transliteration}`;
|
||||||
|
detailBodyEl.innerHTML = "";
|
||||||
|
|
||||||
|
const sections = [];
|
||||||
|
const forms = letter.forms || {};
|
||||||
|
const formParts = [
|
||||||
|
forms.isolated ? `<span class="alpha-arabic-form"><span class="alpha-arabic-glyph">${forms.isolated}</span><br>isolated</span>` : "",
|
||||||
|
forms.final ? `<span class="alpha-arabic-form"><span class="alpha-arabic-glyph">${forms.final}</span><br>final</span>` : "",
|
||||||
|
forms.medial ? `<span class="alpha-arabic-form"><span class="alpha-arabic-glyph">${forms.medial}</span><br>medial</span>` : "",
|
||||||
|
forms.initial ? `<span class="alpha-arabic-form"><span class="alpha-arabic-glyph">${forms.initial}</span><br>initial</span>` : ""
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
sections.push(context.card("Letter Details", `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Arabic Name</dt><dd class="alpha-arabic-inline">${letter.nameArabic}</dd>
|
||||||
|
<dt>Transliteration</dt><dd>${letter.transliteration}</dd>
|
||||||
|
<dt>IPA</dt><dd>${letter.ipa || "—"}</dd>
|
||||||
|
<dt>Abjad Value</dt><dd>${letter.abjad}</dd>
|
||||||
|
<dt>Meaning</dt><dd>${letter.meaning || "—"}</dd>
|
||||||
|
<dt>Category</dt><dd class="alpha-badge alpha-badge--${letter.category}">${letter.category}</dd>
|
||||||
|
<dt>Position</dt><dd>#${letter.index} of 28 (Abjad order)</dd>
|
||||||
|
</dl>
|
||||||
|
`));
|
||||||
|
|
||||||
|
const positionRootCard = renderPositionDigitalRootCard(letter, "arabic", context, "Abjad order");
|
||||||
|
if (positionRootCard) {
|
||||||
|
sections.push(positionRootCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formParts.length) {
|
||||||
|
sections.push(context.card("Letter Forms", `<div class="alpha-arabic-forms">${formParts.join("")}</div>`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const equivalentsCard = renderAlphabetEquivalentCard("arabic", letter, context);
|
||||||
|
if (equivalentsCard) {
|
||||||
|
sections.push(equivalentsCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
detailBodyEl.innerHTML = sections.join("");
|
||||||
|
context.attachDetailListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEnochianDetail(context) {
|
||||||
|
const { letter, detailSubEl, detailBodyEl } = context;
|
||||||
|
const englishRefs = extractEnglishLetterRefs(letter?.englishLetters);
|
||||||
|
detailSubEl.textContent = `${letter.title} — ${letter.transliteration}`;
|
||||||
|
detailBodyEl.innerHTML = "";
|
||||||
|
|
||||||
|
const sections = [];
|
||||||
|
sections.push(context.card("Letter Details", `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Character</dt><dd>${context.enochianGlyphImageHtml(letter, "alpha-enochian-glyph-img alpha-enochian-glyph-img--detail-row")}</dd>
|
||||||
|
<dt>Name</dt><dd>${letter.title}</dd>
|
||||||
|
<dt>English Letters</dt><dd>${englishRefs.join(" / ") || "—"}</dd>
|
||||||
|
<dt>Transliteration</dt><dd>${letter.transliteration || "—"}</dd>
|
||||||
|
<dt>Element / Planet</dt><dd>${letter.elementOrPlanet || "—"}</dd>
|
||||||
|
<dt>Tarot</dt><dd>${letter.tarot || "—"}</dd>
|
||||||
|
<dt>Numerology</dt><dd>${letter.numerology || "—"}</dd>
|
||||||
|
<dt>Glyph Source</dt><dd>Local cache: asset/img/enochian (sourced from dCode set)</dd>
|
||||||
|
<dt>Position</dt><dd>#${letter.index} of 21</dd>
|
||||||
|
</dl>
|
||||||
|
`));
|
||||||
|
|
||||||
|
const positionRootCard = renderPositionDigitalRootCard(letter, "enochian", context);
|
||||||
|
if (positionRootCard) {
|
||||||
|
sections.push(positionRootCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const equivalentsCard = renderAlphabetEquivalentCard("enochian", letter, context);
|
||||||
|
if (equivalentsCard) {
|
||||||
|
sections.push(equivalentsCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthRefs = monthRefsForLetter(letter, context);
|
||||||
|
const monthCard = calendarMonthsCard(monthRefs, "Calendar correspondences linked through this letter's Hebrew correspondence.", context);
|
||||||
|
if (monthCard) {
|
||||||
|
sections.push(monthCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
detailBodyEl.innerHTML = sections.join("");
|
||||||
|
context.attachDetailListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDetail(context) {
|
||||||
|
const alphabet = context.alphabet;
|
||||||
|
if (alphabet === "hebrew") {
|
||||||
|
renderHebrewDetail(context);
|
||||||
|
} else if (alphabet === "greek") {
|
||||||
|
renderGreekDetail(context);
|
||||||
|
} else if (alphabet === "english") {
|
||||||
|
renderEnglishDetail(context);
|
||||||
|
} else if (alphabet === "arabic") {
|
||||||
|
renderArabicDetail(context);
|
||||||
|
} else if (alphabet === "enochian") {
|
||||||
|
renderEnochianDetail(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.AlphabetDetailUi = { renderDetail };
|
||||||
|
})();
|
||||||
353
app/ui-alphabet-gematria.js
Normal file
353
app/ui-alphabet-gematria.js
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
getAlphabets: () => null,
|
||||||
|
getGematriaElements: () => ({
|
||||||
|
cipherEl: null,
|
||||||
|
inputEl: null,
|
||||||
|
resultEl: null,
|
||||||
|
breakdownEl: null
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
loadingPromise: null,
|
||||||
|
db: null,
|
||||||
|
listenersBound: false,
|
||||||
|
activeCipherId: "",
|
||||||
|
inputText: "",
|
||||||
|
scriptCharMap: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
function getAlphabets() {
|
||||||
|
return config.getAlphabets?.() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getElements() {
|
||||||
|
return config.getGematriaElements?.() || {
|
||||||
|
cipherEl: null,
|
||||||
|
inputEl: null,
|
||||||
|
resultEl: null,
|
||||||
|
breakdownEl: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFallbackGematriaDb() {
|
||||||
|
return {
|
||||||
|
baseAlphabet: "abcdefghijklmnopqrstuvwxyz",
|
||||||
|
ciphers: [
|
||||||
|
{
|
||||||
|
id: "simple-ordinal",
|
||||||
|
name: "Simple Ordinal",
|
||||||
|
description: "A=1 ... Z=26",
|
||||||
|
values: Array.from({ length: 26 }, (_, index) => index + 1)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGematriaText(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.normalize("NFD")
|
||||||
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function transliterationToBaseLetters(transliteration, baseAlphabet) {
|
||||||
|
const normalized = normalizeGematriaText(transliteration);
|
||||||
|
if (!normalized) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryVariant = normalized.split(/[\/,;|]/)[0] || normalized;
|
||||||
|
const primaryLetters = [...primaryVariant].filter((char) => baseAlphabet.includes(char));
|
||||||
|
if (primaryLetters.length) {
|
||||||
|
return primaryLetters[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const allLetters = [...normalized].filter((char) => baseAlphabet.includes(char));
|
||||||
|
return allLetters[0] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function addScriptCharMapEntry(map, scriptChar, mappedLetters) {
|
||||||
|
const key = String(scriptChar || "").trim();
|
||||||
|
const value = String(mappedLetters || "").trim();
|
||||||
|
if (!key || !value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
map.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGematriaScriptMap(baseAlphabet) {
|
||||||
|
const map = new Map();
|
||||||
|
const alphabets = getAlphabets() || {};
|
||||||
|
const hebrewLetters = Array.isArray(alphabets.hebrew) ? alphabets.hebrew : [];
|
||||||
|
const greekLetters = Array.isArray(alphabets.greek) ? alphabets.greek : [];
|
||||||
|
|
||||||
|
hebrewLetters.forEach((entry) => {
|
||||||
|
const mapped = transliterationToBaseLetters(entry?.transliteration, baseAlphabet);
|
||||||
|
addScriptCharMapEntry(map, entry?.char, mapped);
|
||||||
|
});
|
||||||
|
|
||||||
|
greekLetters.forEach((entry) => {
|
||||||
|
const mapped = transliterationToBaseLetters(entry?.transliteration, baseAlphabet);
|
||||||
|
addScriptCharMapEntry(map, entry?.char, mapped);
|
||||||
|
addScriptCharMapEntry(map, entry?.charLower, mapped);
|
||||||
|
addScriptCharMapEntry(map, entry?.charFinal, mapped);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hebrewFinalForms = {
|
||||||
|
ך: "k",
|
||||||
|
ם: "m",
|
||||||
|
ן: "n",
|
||||||
|
ף: "p",
|
||||||
|
ץ: "t"
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(hebrewFinalForms).forEach(([char, mapped]) => {
|
||||||
|
if (!map.has(char) && baseAlphabet.includes(mapped)) {
|
||||||
|
addScriptCharMapEntry(map, char, mapped);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!map.has("ς") && baseAlphabet.includes("s")) {
|
||||||
|
addScriptCharMapEntry(map, "ς", "s");
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshScriptMap(baseAlphabetOverride = "") {
|
||||||
|
const db = state.db || getFallbackGematriaDb();
|
||||||
|
const baseAlphabet = String(baseAlphabetOverride || db.baseAlphabet || "abcdefghijklmnopqrstuvwxyz").toLowerCase();
|
||||||
|
state.scriptCharMap = buildGematriaScriptMap(baseAlphabet);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeGematriaDb(db) {
|
||||||
|
const baseAlphabet = String(db?.baseAlphabet || "abcdefghijklmnopqrstuvwxyz").toLowerCase();
|
||||||
|
const ciphers = Array.isArray(db?.ciphers)
|
||||||
|
? db.ciphers
|
||||||
|
.map((cipher) => {
|
||||||
|
const id = String(cipher?.id || "").trim();
|
||||||
|
const name = String(cipher?.name || "").trim();
|
||||||
|
const values = Array.isArray(cipher?.values)
|
||||||
|
? cipher.values.map((value) => Number(value))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!id || !name || values.length !== baseAlphabet.length || values.some((value) => !Number.isFinite(value))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description: String(cipher?.description || "").trim(),
|
||||||
|
values
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!ciphers.length) {
|
||||||
|
return getFallbackGematriaDb();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseAlphabet,
|
||||||
|
ciphers
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGematriaDb() {
|
||||||
|
if (state.db) {
|
||||||
|
return state.db;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.loadingPromise) {
|
||||||
|
return state.loadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.loadingPromise = fetch("data/gematria-ciphers.json")
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load gematria ciphers (${response.status})`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((db) => {
|
||||||
|
state.db = sanitizeGematriaDb(db);
|
||||||
|
return state.db;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
state.db = getFallbackGematriaDb();
|
||||||
|
return state.db;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
state.loadingPromise = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return state.loadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveGematriaCipher() {
|
||||||
|
const db = state.db || getFallbackGematriaDb();
|
||||||
|
const ciphers = Array.isArray(db.ciphers) ? db.ciphers : [];
|
||||||
|
if (!ciphers.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedId = state.activeCipherId || ciphers[0].id;
|
||||||
|
return ciphers.find((cipher) => cipher.id === selectedId) || ciphers[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGematriaCipherOptions() {
|
||||||
|
const { cipherEl } = getElements();
|
||||||
|
if (!cipherEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = state.db || getFallbackGematriaDb();
|
||||||
|
const ciphers = Array.isArray(db.ciphers) ? db.ciphers : [];
|
||||||
|
|
||||||
|
cipherEl.innerHTML = "";
|
||||||
|
ciphers.forEach((cipher) => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = cipher.id;
|
||||||
|
option.textContent = cipher.name;
|
||||||
|
if (cipher.description) {
|
||||||
|
option.title = cipher.description;
|
||||||
|
}
|
||||||
|
cipherEl.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeCipher = getActiveGematriaCipher();
|
||||||
|
state.activeCipherId = activeCipher?.id || "";
|
||||||
|
cipherEl.value = state.activeCipherId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeGematria(text, cipher, baseAlphabet) {
|
||||||
|
const normalizedInput = normalizeGematriaText(text);
|
||||||
|
const scriptMap = state.scriptCharMap instanceof Map
|
||||||
|
? state.scriptCharMap
|
||||||
|
: new Map();
|
||||||
|
|
||||||
|
const letterParts = [];
|
||||||
|
let total = 0;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
[...normalizedInput].forEach((char) => {
|
||||||
|
const mappedLetters = baseAlphabet.includes(char)
|
||||||
|
? char
|
||||||
|
: (scriptMap.get(char) || "");
|
||||||
|
|
||||||
|
if (!mappedLetters) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[...mappedLetters].forEach((mappedChar) => {
|
||||||
|
const index = baseAlphabet.indexOf(mappedChar);
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = Number(cipher.values[index]);
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
total += value;
|
||||||
|
letterParts.push(`${mappedChar.toUpperCase()}(${value})`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
count,
|
||||||
|
breakdown: letterParts.join(" + ")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGematriaResult() {
|
||||||
|
const { resultEl, breakdownEl } = getElements();
|
||||||
|
if (!resultEl || !breakdownEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = state.db || getFallbackGematriaDb();
|
||||||
|
if (!(state.scriptCharMap instanceof Map) || !state.scriptCharMap.size) {
|
||||||
|
refreshScriptMap(db.baseAlphabet);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = getActiveGematriaCipher();
|
||||||
|
if (!cipher) {
|
||||||
|
resultEl.textContent = "Total: --";
|
||||||
|
breakdownEl.textContent = "No ciphers available.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { total, count, breakdown } = computeGematria(state.inputText, cipher, db.baseAlphabet);
|
||||||
|
|
||||||
|
resultEl.textContent = `Total: ${total}`;
|
||||||
|
if (!count) {
|
||||||
|
breakdownEl.textContent = `Using ${cipher.name}. Enter English, Greek, or Hebrew letters to calculate.`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
breakdownEl.textContent = `${cipher.name} · ${count} letters · ${breakdown} = ${total}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindGematriaListeners() {
|
||||||
|
const { cipherEl, inputEl } = getElements();
|
||||||
|
if (state.listenersBound || !cipherEl || !inputEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherEl.addEventListener("change", () => {
|
||||||
|
state.activeCipherId = String(cipherEl.value || "").trim();
|
||||||
|
renderGematriaResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
inputEl.addEventListener("input", () => {
|
||||||
|
state.inputText = inputEl.value || "";
|
||||||
|
renderGematriaResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
state.listenersBound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCalculator() {
|
||||||
|
const { cipherEl, inputEl, resultEl, breakdownEl } = getElements();
|
||||||
|
if (!cipherEl || !inputEl || !resultEl || !breakdownEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindGematriaListeners();
|
||||||
|
|
||||||
|
if (inputEl.value !== state.inputText) {
|
||||||
|
inputEl.value = state.inputText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadGematriaDb().then(() => {
|
||||||
|
refreshScriptMap((state.db || getFallbackGematriaDb()).baseAlphabet);
|
||||||
|
renderGematriaCipherOptions();
|
||||||
|
renderGematriaResult();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(nextConfig = {}) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...nextConfig
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.AlphabetGematriaUi = {
|
||||||
|
...(window.AlphabetGematriaUi || {}),
|
||||||
|
init,
|
||||||
|
refreshScriptMap,
|
||||||
|
ensureCalculator
|
||||||
|
};
|
||||||
|
})();
|
||||||
470
app/ui-alphabet-references.js
Normal file
470
app/ui-alphabet-references.js
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
/* ui-alphabet-references.js — Alphabet calendar and cube reference builders */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function normalizeId(value) {
|
||||||
|
return String(value || "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cap(value) {
|
||||||
|
return value ? value.charAt(0).toUpperCase() + value.slice(1) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMonthReferencesByHebrew(referenceData, alphabets) {
|
||||||
|
const map = new Map();
|
||||||
|
const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [];
|
||||||
|
const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
||||||
|
const monthById = new Map(months.map((month) => [month.id, month]));
|
||||||
|
const hebrewLetters = Array.isArray(alphabets?.hebrew) ? alphabets.hebrew : [];
|
||||||
|
|
||||||
|
const profiles = hebrewLetters
|
||||||
|
.filter((letter) => letter?.hebrewLetterId)
|
||||||
|
.map((letter) => {
|
||||||
|
const astrologyType = normalizeId(letter?.astrology?.type);
|
||||||
|
const astrologyName = normalizeId(letter?.astrology?.name);
|
||||||
|
return {
|
||||||
|
hebrewLetterId: normalizeId(letter.hebrewLetterId),
|
||||||
|
tarotTrumpNumber: Number.isFinite(Number(letter?.tarot?.trumpNumber))
|
||||||
|
? Number(letter.tarot.trumpNumber)
|
||||||
|
: null,
|
||||||
|
kabbalahPathNumber: Number.isFinite(Number(letter?.kabbalahPathNumber))
|
||||||
|
? Number(letter.kabbalahPathNumber)
|
||||||
|
: null,
|
||||||
|
planetId: astrologyType === "planet" ? astrologyName : "",
|
||||||
|
zodiacSignId: astrologyType === "zodiac" ? astrologyName : ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function parseMonthDayToken(value) {
|
||||||
|
const text = String(value || "").trim();
|
||||||
|
const match = text.match(/^(\d{1,2})-(\d{1,2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthNo = Number(match[1]);
|
||||||
|
const dayNo = Number(match[2]);
|
||||||
|
if (!Number.isInteger(monthNo) || !Number.isInteger(dayNo) || monthNo < 1 || monthNo > 12 || dayNo < 1 || dayNo > 31) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { month: monthNo, day: dayNo };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMonthDayTokensFromText(value) {
|
||||||
|
const text = String(value || "");
|
||||||
|
const matches = [...text.matchAll(/(\d{1,2})-(\d{1,2})/g)];
|
||||||
|
return matches
|
||||||
|
.map((match) => ({ month: Number(match[1]), day: Number(match[2]) }))
|
||||||
|
.filter((token) => Number.isInteger(token.month) && Number.isInteger(token.day) && token.month >= 1 && token.month <= 12 && token.day >= 1 && token.day <= 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDateToken(token, year) {
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Date(year, token.month - 1, token.day, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMonthDayRangeByMonth(startToken, endToken) {
|
||||||
|
const startDate = toDateToken(startToken, 2025);
|
||||||
|
const endBase = toDateToken(endToken, 2025);
|
||||||
|
if (!startDate || !endBase) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapsYear = endBase.getTime() < startDate.getTime();
|
||||||
|
const endDate = wrapsYear ? toDateToken(endToken, 2026) : endBase;
|
||||||
|
if (!endDate) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = [];
|
||||||
|
let cursor = new Date(startDate);
|
||||||
|
while (cursor.getTime() <= endDate.getTime()) {
|
||||||
|
const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0);
|
||||||
|
const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate;
|
||||||
|
|
||||||
|
segments.push({
|
||||||
|
monthNo: cursor.getMonth() + 1,
|
||||||
|
startDay: cursor.getDate(),
|
||||||
|
endDay: segmentEnd.getDate()
|
||||||
|
});
|
||||||
|
|
||||||
|
cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenToString(monthNo, dayNo) {
|
||||||
|
return `${String(monthNo).padStart(2, "0")}-${String(dayNo).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRangeLabel(monthName, startDay, endDay) {
|
||||||
|
if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) {
|
||||||
|
return monthName;
|
||||||
|
}
|
||||||
|
if (startDay === endDay) {
|
||||||
|
return `${monthName} ${startDay}`;
|
||||||
|
}
|
||||||
|
return `${monthName} ${startDay}-${endDay}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRangeForMonth(month, options = {}) {
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
const monthStart = parseMonthDayToken(month?.start);
|
||||||
|
const monthEnd = parseMonthDayToken(month?.end);
|
||||||
|
if (!Number.isFinite(monthOrder) || !monthStart || !monthEnd) {
|
||||||
|
return {
|
||||||
|
startToken: String(month?.start || "").trim() || null,
|
||||||
|
endToken: String(month?.end || "").trim() || null,
|
||||||
|
label: month?.name || month?.id || "",
|
||||||
|
isFullMonth: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let startToken = parseMonthDayToken(options.startToken);
|
||||||
|
let endToken = parseMonthDayToken(options.endToken);
|
||||||
|
|
||||||
|
if (!startToken || !endToken) {
|
||||||
|
const tokens = parseMonthDayTokensFromText(options.rawDateText);
|
||||||
|
if (tokens.length >= 2) {
|
||||||
|
startToken = tokens[0];
|
||||||
|
endToken = tokens[1];
|
||||||
|
} else if (tokens.length === 1) {
|
||||||
|
startToken = tokens[0];
|
||||||
|
endToken = tokens[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startToken || !endToken) {
|
||||||
|
startToken = monthStart;
|
||||||
|
endToken = monthEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = splitMonthDayRangeByMonth(startToken, endToken);
|
||||||
|
const segment = segments.find((entry) => entry.monthNo === monthOrder) || null;
|
||||||
|
|
||||||
|
const useStart = segment ? { month: monthOrder, day: segment.startDay } : startToken;
|
||||||
|
const useEnd = segment ? { month: monthOrder, day: segment.endDay } : endToken;
|
||||||
|
const startText = tokenToString(useStart.month, useStart.day);
|
||||||
|
const endText = tokenToString(useEnd.month, useEnd.day);
|
||||||
|
const isFullMonth = startText === month.start && endText === month.end;
|
||||||
|
|
||||||
|
return {
|
||||||
|
startToken: startText,
|
||||||
|
endToken: endText,
|
||||||
|
label: isFullMonth
|
||||||
|
? (month.name || month.id)
|
||||||
|
: formatRangeLabel(month.name || month.id, useStart.day, useEnd.day),
|
||||||
|
isFullMonth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushRef(hebrewLetterId, month, options = {}) {
|
||||||
|
if (!hebrewLetterId || !month?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.has(hebrewLetterId)) {
|
||||||
|
map.set(hebrewLetterId, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = map.get(hebrewLetterId);
|
||||||
|
const range = resolveRangeForMonth(month, options);
|
||||||
|
const rowKey = `${month.id}|${range.startToken || ""}|${range.endToken || ""}`;
|
||||||
|
if (rows.some((entry) => entry.key === rowKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
id: month.id,
|
||||||
|
name: month.name || month.id,
|
||||||
|
order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999,
|
||||||
|
label: range.label,
|
||||||
|
startToken: range.startToken,
|
||||||
|
endToken: range.endToken,
|
||||||
|
isFullMonth: range.isFullMonth,
|
||||||
|
key: rowKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRefs(associations, month, options = {}) {
|
||||||
|
if (!associations || typeof associations !== "object") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assocHebrewId = normalizeId(associations.hebrewLetterId);
|
||||||
|
const assocTarotTrump = Number.isFinite(Number(associations.tarotTrumpNumber))
|
||||||
|
? Number(associations.tarotTrumpNumber)
|
||||||
|
: null;
|
||||||
|
const assocPath = Number.isFinite(Number(associations.kabbalahPathNumber))
|
||||||
|
? Number(associations.kabbalahPathNumber)
|
||||||
|
: null;
|
||||||
|
const assocPlanetId = normalizeId(associations.planetId);
|
||||||
|
const assocSignId = normalizeId(associations.zodiacSignId);
|
||||||
|
|
||||||
|
profiles.forEach((profile) => {
|
||||||
|
if (!profile.hebrewLetterId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchesDirect = assocHebrewId && assocHebrewId === profile.hebrewLetterId;
|
||||||
|
const matchesTarot = assocTarotTrump != null && profile.tarotTrumpNumber === assocTarotTrump;
|
||||||
|
const matchesPath = assocPath != null && profile.kabbalahPathNumber === assocPath;
|
||||||
|
const matchesPlanet = profile.planetId && assocPlanetId && profile.planetId === assocPlanetId;
|
||||||
|
const matchesZodiac = profile.zodiacSignId && assocSignId && profile.zodiacSignId === assocSignId;
|
||||||
|
|
||||||
|
if (matchesDirect || matchesTarot || matchesPath || matchesPlanet || matchesZodiac) {
|
||||||
|
pushRef(profile.hebrewLetterId, month, options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
months.forEach((month) => {
|
||||||
|
collectRefs(month?.associations, month);
|
||||||
|
|
||||||
|
const events = Array.isArray(month?.events) ? month.events : [];
|
||||||
|
events.forEach((event) => {
|
||||||
|
collectRefs(event?.associations, month, {
|
||||||
|
rawDateText: event?.dateRange || event?.date || ""
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
holidays.forEach((holiday) => {
|
||||||
|
const month = monthById.get(holiday?.monthId);
|
||||||
|
if (!month) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
collectRefs(holiday?.associations, month, {
|
||||||
|
rawDateText: holiday?.dateRange || holiday?.date || ""
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
map.forEach((rows, key) => {
|
||||||
|
const preciseMonthIds = new Set(
|
||||||
|
rows
|
||||||
|
.filter((entry) => !entry.isFullMonth)
|
||||||
|
.map((entry) => entry.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const filtered = rows.filter((entry) => {
|
||||||
|
if (!entry.isFullMonth) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !preciseMonthIds.has(entry.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
filtered.sort((left, right) => {
|
||||||
|
if (left.order !== right.order) {
|
||||||
|
return left.order - right.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startLeft = parseMonthDayToken(left.startToken);
|
||||||
|
const startRight = parseMonthDayToken(right.startToken);
|
||||||
|
const dayLeft = startLeft ? startLeft.day : 999;
|
||||||
|
const dayRight = startRight ? startRight.day : 999;
|
||||||
|
if (dayLeft !== dayRight) {
|
||||||
|
return dayLeft - dayRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(left.label || left.name || "").localeCompare(String(right.label || right.name || ""));
|
||||||
|
});
|
||||||
|
|
||||||
|
map.set(key, filtered);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyCubeRefs() {
|
||||||
|
return {
|
||||||
|
hebrewPlacementById: new Map(),
|
||||||
|
signPlacementById: new Map(),
|
||||||
|
planetPlacementById: new Map(),
|
||||||
|
pathPlacementByNo: new Map()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLetterId(value) {
|
||||||
|
const key = normalizeId(value).replace(/[^a-z]/g, "");
|
||||||
|
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) => normalizeId(wallId)).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (explicitWalls.length >= 2) {
|
||||||
|
return explicitWalls.slice(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeId(edge?.id)
|
||||||
|
.split("-")
|
||||||
|
.map((wallId) => normalizeId(wallId))
|
||||||
|
.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 = normalizeId(wallId);
|
||||||
|
const edgeId = normalizeId(edge?.id);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCubePlacement(wall, edge = null) {
|
||||||
|
const wallId = normalizeId(wall?.id);
|
||||||
|
const edgeId = normalizeId(edge?.id);
|
||||||
|
return {
|
||||||
|
wallId,
|
||||||
|
edgeId,
|
||||||
|
wallName: wall?.name || cap(wallId),
|
||||||
|
edgeName: resolveCubeDirectionLabel(wallId, edge)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPlacementIfMissing(map, key, placement) {
|
||||||
|
if (!key || map.has(key) || !placement?.wallId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map.set(key, placement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCubeReferences(magickDataset) {
|
||||||
|
const refs = createEmptyCubeRefs();
|
||||||
|
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
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const wallById = new Map(
|
||||||
|
walls.map((wall) => [normalizeId(wall?.id), wall])
|
||||||
|
);
|
||||||
|
const firstEdgeByWallId = new Map();
|
||||||
|
|
||||||
|
const pathByLetterId = new Map(
|
||||||
|
paths
|
||||||
|
.map((path) => [normalizeLetterId(path?.hebrewLetter?.transliteration), path])
|
||||||
|
.filter(([letterId]) => Boolean(letterId))
|
||||||
|
);
|
||||||
|
|
||||||
|
edges.forEach((edge) => {
|
||||||
|
edgeWalls(edge).forEach((wallId) => {
|
||||||
|
if (!firstEdgeByWallId.has(wallId)) {
|
||||||
|
firstEdgeByWallId.set(wallId, edge);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
walls.forEach((wall) => {
|
||||||
|
const wallHebrewLetterId = normalizeLetterId(wall?.hebrewLetterId || wall?.associations?.hebrewLetterId);
|
||||||
|
|
||||||
|
let wallPlacement;
|
||||||
|
if (wallHebrewLetterId) {
|
||||||
|
wallPlacement = {
|
||||||
|
wallId: normalizeId(wall?.id),
|
||||||
|
edgeId: "",
|
||||||
|
wallName: wall?.name || cap(normalizeId(wall?.id)),
|
||||||
|
edgeName: "Face"
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const placementEdge = firstEdgeByWallId.get(normalizeId(wall?.id)) || null;
|
||||||
|
wallPlacement = makeCubePlacement(wall, placementEdge);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlacementIfMissing(refs.hebrewPlacementById, wallHebrewLetterId, wallPlacement);
|
||||||
|
|
||||||
|
const wallPath = pathByLetterId.get(wallHebrewLetterId) || null;
|
||||||
|
const wallSignId = normalizeId(wallPath?.astrology?.type) === "zodiac"
|
||||||
|
? normalizeId(wallPath?.astrology?.name)
|
||||||
|
: "";
|
||||||
|
setPlacementIfMissing(refs.signPlacementById, wallSignId, wallPlacement);
|
||||||
|
|
||||||
|
const wallPathNo = Number(wallPath?.pathNumber);
|
||||||
|
if (Number.isFinite(wallPathNo)) {
|
||||||
|
setPlacementIfMissing(refs.pathPlacementByNo, wallPathNo, wallPlacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallPlanet = normalizeId(wall?.associations?.planetId);
|
||||||
|
if (wallPlanet) {
|
||||||
|
setPlacementIfMissing(refs.planetPlacementById, wallPlanet, wallPlacement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
edges.forEach((edge) => {
|
||||||
|
const wallsForEdge = edgeWalls(edge);
|
||||||
|
const primaryWallId = wallsForEdge[0];
|
||||||
|
const primaryWall = wallById.get(primaryWallId) || {
|
||||||
|
id: primaryWallId,
|
||||||
|
name: cap(primaryWallId)
|
||||||
|
};
|
||||||
|
|
||||||
|
const placement = makeCubePlacement(primaryWall, edge);
|
||||||
|
const hebrewLetterId = normalizeLetterId(edge?.hebrewLetterId || edge?.associations?.hebrewLetterId);
|
||||||
|
setPlacementIfMissing(refs.hebrewPlacementById, hebrewLetterId, placement);
|
||||||
|
|
||||||
|
const path = pathByLetterId.get(hebrewLetterId) || null;
|
||||||
|
const signId = normalizeId(path?.astrology?.type) === "zodiac"
|
||||||
|
? normalizeId(path?.astrology?.name)
|
||||||
|
: "";
|
||||||
|
setPlacementIfMissing(refs.signPlacementById, signId, placement);
|
||||||
|
|
||||||
|
const pathNo = Number(path?.pathNumber);
|
||||||
|
if (Number.isFinite(pathNo)) {
|
||||||
|
setPlacementIfMissing(refs.pathPlacementByNo, pathNo, placement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.AlphabetReferenceBuilders = {
|
||||||
|
buildMonthReferencesByHebrew,
|
||||||
|
buildCubeReferences
|
||||||
|
};
|
||||||
|
})();
|
||||||
1391
app/ui-alphabet.js
1391
app/ui-alphabet.js
File diff suppressed because it is too large
Load Diff
651
app/ui-calendar-dates.js
Normal file
651
app/ui-calendar-dates.js
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const HEBREW_MONTH_ALIAS_BY_ID = {
|
||||||
|
nisan: ["nisan"],
|
||||||
|
iyar: ["iyar"],
|
||||||
|
sivan: ["sivan"],
|
||||||
|
tammuz: ["tamuz", "tammuz"],
|
||||||
|
av: ["av"],
|
||||||
|
elul: ["elul"],
|
||||||
|
tishrei: ["tishri", "tishrei"],
|
||||||
|
cheshvan: ["heshvan", "cheshvan", "marcheshvan"],
|
||||||
|
kislev: ["kislev"],
|
||||||
|
tevet: ["tevet"],
|
||||||
|
shvat: ["shevat", "shvat"],
|
||||||
|
adar: ["adar", "adar i", "adar 1"],
|
||||||
|
"adar-ii": ["adar ii", "adar 2"]
|
||||||
|
};
|
||||||
|
|
||||||
|
const MONTH_NAME_TO_INDEX = {
|
||||||
|
january: 0,
|
||||||
|
february: 1,
|
||||||
|
march: 2,
|
||||||
|
april: 3,
|
||||||
|
may: 4,
|
||||||
|
june: 5,
|
||||||
|
july: 6,
|
||||||
|
august: 7,
|
||||||
|
september: 8,
|
||||||
|
october: 9,
|
||||||
|
november: 10,
|
||||||
|
december: 11
|
||||||
|
};
|
||||||
|
|
||||||
|
const GREGORIAN_MONTH_ID_TO_ORDER = {
|
||||||
|
january: 1,
|
||||||
|
february: 2,
|
||||||
|
march: 3,
|
||||||
|
april: 4,
|
||||||
|
may: 5,
|
||||||
|
june: 6,
|
||||||
|
july: 7,
|
||||||
|
august: 8,
|
||||||
|
september: 9,
|
||||||
|
october: 10,
|
||||||
|
november: 11,
|
||||||
|
december: 12
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = {};
|
||||||
|
|
||||||
|
function getSelectedYear() {
|
||||||
|
return Number(config.getSelectedYear?.()) || new Date().getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedCalendar() {
|
||||||
|
return String(config.getSelectedCalendar?.() || "gregorian").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIslamicMonths() {
|
||||||
|
return config.getIslamicMonths?.() || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMonthDayToken(token) {
|
||||||
|
const [month, day] = String(token || "").split("-").map((part) => Number(part));
|
||||||
|
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { month, day };
|
||||||
|
}
|
||||||
|
|
||||||
|
function monthDayDate(monthDay, year) {
|
||||||
|
const parsed = parseMonthDayToken(monthDay);
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Date(year, parsed.month - 1, parsed.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSignDateBounds(sign) {
|
||||||
|
const start = monthDayDate(sign?.start, 2025);
|
||||||
|
const endBase = monthDayDate(sign?.end, 2025);
|
||||||
|
if (!start || !endBase) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapsYear = endBase.getTime() < start.getTime();
|
||||||
|
const end = wrapsYear ? monthDayDate(sign?.end, 2026) : endBase;
|
||||||
|
if (!end) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { start, end };
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDays(date, days) {
|
||||||
|
const next = new Date(date);
|
||||||
|
next.setDate(next.getDate() + days);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateLabel(date) {
|
||||||
|
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function monthDayOrdinal(month, day) {
|
||||||
|
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const base = new Date(2025, Math.trunc(month) - 1, Math.trunc(day), 12, 0, 0, 0);
|
||||||
|
if (Number.isNaN(base.getTime())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const start = new Date(2025, 0, 1, 12, 0, 0, 0);
|
||||||
|
const diff = base.getTime() - start.getTime();
|
||||||
|
return Math.floor(diff / (24 * 60 * 60 * 1000)) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMonthDayInRange(targetMonth, targetDay, startMonth, startDay, endMonth, endDay) {
|
||||||
|
const target = monthDayOrdinal(targetMonth, targetDay);
|
||||||
|
const start = monthDayOrdinal(startMonth, startDay);
|
||||||
|
const end = monthDayOrdinal(endMonth, endDay);
|
||||||
|
if (!Number.isFinite(target) || !Number.isFinite(start) || !Number.isFinite(end)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end >= start) {
|
||||||
|
return target >= start && target <= end;
|
||||||
|
}
|
||||||
|
return target >= start || target <= end;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMonthDayTokensFromText(value) {
|
||||||
|
const text = String(value || "");
|
||||||
|
const matches = [...text.matchAll(/(\d{2})-(\d{2})/g)];
|
||||||
|
return matches
|
||||||
|
.map((match) => ({ month: Number(match[1]), day: Number(match[2]) }))
|
||||||
|
.filter((token) => Number.isFinite(token.month) && Number.isFinite(token.day));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDayRangeFromText(value) {
|
||||||
|
const text = String(value || "");
|
||||||
|
const range = text.match(/\b(\d{1,2})\s*[–-]\s*(\d{1,2})\b/);
|
||||||
|
if (!range) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDay = Number(range[1]);
|
||||||
|
const endDay = Number(range[2]);
|
||||||
|
if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { startDay, endDay };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isoToDateAtNoon(iso) {
|
||||||
|
const text = String(iso || "").trim();
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parsed = new Date(`${text}T12:00:00`);
|
||||||
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDaysInMonth(year, monthOrder) {
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(monthOrder)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Date(year, monthOrder, 0).getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonthStartWeekday(year, monthOrder) {
|
||||||
|
const date = new Date(year, monthOrder - 1, 1);
|
||||||
|
return date.toLocaleDateString(undefined, { weekday: "long" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMonthRange(month) {
|
||||||
|
const startText = String(month?.start || "").trim();
|
||||||
|
const endText = String(month?.end || "").trim();
|
||||||
|
if (!startText || !endText) {
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
return `${startText} to ${endText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGregorianMonthOrderFromId(monthId) {
|
||||||
|
if (!monthId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const key = String(monthId).trim().toLowerCase();
|
||||||
|
const value = GREGORIAN_MONTH_ID_TO_ORDER[key];
|
||||||
|
return Number.isFinite(value) ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCalendarText(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.normalize("NFKD")
|
||||||
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
|
.replace(/['`´ʻ’]/g, "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNumericPart(parts, partType) {
|
||||||
|
const raw = parts.find((part) => part.type === partType)?.value;
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const digits = String(raw).replace(/[^0-9]/g, "");
|
||||||
|
if (!digits) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(digits);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGregorianReferenceDate(date) {
|
||||||
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleDateString(undefined, {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCalendarDateFromGregorian(date, calendarId) {
|
||||||
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
const locale = calendarId === "hebrew"
|
||||||
|
? "en-u-ca-hebrew"
|
||||||
|
: (calendarId === "islamic" ? "en-u-ca-islamic" : "en");
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(locale, {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric"
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGregorianMonthStartDate(monthOrder, year = getSelectedYear()) {
|
||||||
|
if (!Number.isFinite(monthOrder) || !Number.isFinite(year)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(Math.trunc(year), Math.trunc(monthOrder) - 1, 1, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHebrewMonthAliases(month) {
|
||||||
|
const aliases = [];
|
||||||
|
const idAliases = HEBREW_MONTH_ALIAS_BY_ID[String(month?.id || "").toLowerCase()] || [];
|
||||||
|
aliases.push(...idAliases);
|
||||||
|
|
||||||
|
const nameAlias = normalizeCalendarText(month?.name);
|
||||||
|
if (nameAlias) {
|
||||||
|
aliases.push(nameAlias);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(new Set(aliases.map((alias) => normalizeCalendarText(alias)).filter(Boolean)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function findHebrewMonthStartInGregorianYear(month, year) {
|
||||||
|
const aliases = getHebrewMonthAliases(month);
|
||||||
|
if (!aliases.length || !Number.isFinite(year)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat("en-u-ca-hebrew", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric"
|
||||||
|
});
|
||||||
|
|
||||||
|
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
||||||
|
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
||||||
|
|
||||||
|
while (cursor.getTime() <= end.getTime()) {
|
||||||
|
const parts = formatter.formatToParts(cursor);
|
||||||
|
const day = readNumericPart(parts, "day");
|
||||||
|
const monthName = normalizeCalendarText(parts.find((part) => part.type === "month")?.value);
|
||||||
|
if (day === 1 && aliases.includes(monthName)) {
|
||||||
|
return new Date(cursor);
|
||||||
|
}
|
||||||
|
cursor.setDate(cursor.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findIslamicMonthStartInGregorianYear(month, year) {
|
||||||
|
const targetOrder = Number(month?.order);
|
||||||
|
if (!Number.isFinite(targetOrder) || !Number.isFinite(year)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat("en-u-ca-islamic", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
year: "numeric"
|
||||||
|
});
|
||||||
|
|
||||||
|
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
||||||
|
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
||||||
|
|
||||||
|
while (cursor.getTime() <= end.getTime()) {
|
||||||
|
const parts = formatter.formatToParts(cursor);
|
||||||
|
const day = readNumericPart(parts, "day");
|
||||||
|
const monthNo = readNumericPart(parts, "month");
|
||||||
|
if (day === 1 && monthNo === Math.trunc(targetOrder)) {
|
||||||
|
return new Date(cursor);
|
||||||
|
}
|
||||||
|
cursor.setDate(cursor.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFirstMonthDayFromText(dateText) {
|
||||||
|
const text = String(dateText || "").replace(/~/g, " ");
|
||||||
|
const firstSegment = text.split("/")[0] || text;
|
||||||
|
const match = firstSegment.match(/(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2})/i);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthIndex = MONTH_NAME_TO_INDEX[String(match[1]).toLowerCase()];
|
||||||
|
const day = Number(match[2]);
|
||||||
|
if (!Number.isFinite(monthIndex) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { monthIndex, day };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMonthDayStartToken(token) {
|
||||||
|
const match = String(token || "").match(/(\d{2})-(\d{2})/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const month = Number(match[1]);
|
||||||
|
const day = Number(match[2]);
|
||||||
|
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { month, day };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDateAtNoon(year, monthIndex, dayOfMonth) {
|
||||||
|
return new Date(Math.trunc(year), monthIndex, Math.trunc(dayOfMonth), 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeWesternEasterDate(year) {
|
||||||
|
const y = Math.trunc(Number(year));
|
||||||
|
if (!Number.isFinite(y)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = y % 19;
|
||||||
|
const b = Math.floor(y / 100);
|
||||||
|
const c = y % 100;
|
||||||
|
const d = Math.floor(b / 4);
|
||||||
|
const e = b % 4;
|
||||||
|
const f = Math.floor((b + 8) / 25);
|
||||||
|
const g = Math.floor((b - f + 1) / 3);
|
||||||
|
const h = (19 * a + b - d - g + 15) % 30;
|
||||||
|
const i = Math.floor(c / 4);
|
||||||
|
const k = c % 4;
|
||||||
|
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
||||||
|
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
||||||
|
const month = Math.floor((h + l - 7 * m + 114) / 31);
|
||||||
|
const day = ((h + l - 7 * m + 114) % 31) + 1;
|
||||||
|
return createDateAtNoon(y, month - 1, day);
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeNthWeekdayOfMonth(year, monthIndex, weekday, ordinal) {
|
||||||
|
const y = Math.trunc(Number(year));
|
||||||
|
if (!Number.isFinite(y)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = createDateAtNoon(y, monthIndex, 1);
|
||||||
|
const firstWeekday = first.getDay();
|
||||||
|
const offset = (weekday - firstWeekday + 7) % 7;
|
||||||
|
const dayOfMonth = 1 + offset + (Math.trunc(ordinal) - 1) * 7;
|
||||||
|
const daysInMonth = new Date(y, monthIndex + 1, 0).getDate();
|
||||||
|
if (dayOfMonth > daysInMonth) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return createDateAtNoon(y, monthIndex, dayOfMonth);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveGregorianDateRule(rule, year = getSelectedYear()) {
|
||||||
|
const key = String(rule || "").trim().toLowerCase();
|
||||||
|
if (!key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "gregorian-easter-sunday") {
|
||||||
|
return computeWesternEasterDate(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "gregorian-good-friday") {
|
||||||
|
const easter = computeWesternEasterDate(year);
|
||||||
|
if (!(easter instanceof Date) || Number.isNaN(easter.getTime())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return createDateAtNoon(easter.getFullYear(), easter.getMonth(), easter.getDate() - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "gregorian-thanksgiving-us") {
|
||||||
|
return computeNthWeekdayOfMonth(year, 10, 4, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findHebrewMonthDayInGregorianYear(monthId, day, year) {
|
||||||
|
const aliases = HEBREW_MONTH_ALIAS_BY_ID[String(monthId || "").toLowerCase()] || [];
|
||||||
|
const targetDay = Number(day);
|
||||||
|
if (!aliases.length || !Number.isFinite(targetDay) || !Number.isFinite(year)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedAliases = aliases.map((alias) => normalizeCalendarText(alias)).filter(Boolean);
|
||||||
|
const formatter = new Intl.DateTimeFormat("en-u-ca-hebrew", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric"
|
||||||
|
});
|
||||||
|
|
||||||
|
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
||||||
|
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
||||||
|
|
||||||
|
while (cursor.getTime() <= end.getTime()) {
|
||||||
|
const parts = formatter.formatToParts(cursor);
|
||||||
|
const currentDay = readNumericPart(parts, "day");
|
||||||
|
const monthName = normalizeCalendarText(parts.find((part) => part.type === "month")?.value);
|
||||||
|
if (currentDay === Math.trunc(targetDay) && normalizedAliases.includes(monthName)) {
|
||||||
|
return new Date(cursor);
|
||||||
|
}
|
||||||
|
cursor.setDate(cursor.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIslamicMonthOrderById(monthId) {
|
||||||
|
const month = getIslamicMonths().find((item) => item?.id === monthId);
|
||||||
|
const order = Number(month?.order);
|
||||||
|
return Number.isFinite(order) ? Math.trunc(order) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findIslamicMonthDayInGregorianYear(monthId, day, year) {
|
||||||
|
const monthOrder = getIslamicMonthOrderById(monthId);
|
||||||
|
const targetDay = Number(day);
|
||||||
|
if (!Number.isFinite(monthOrder) || !Number.isFinite(targetDay) || !Number.isFinite(year)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat("en-u-ca-islamic", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
year: "numeric"
|
||||||
|
});
|
||||||
|
|
||||||
|
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
||||||
|
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
||||||
|
|
||||||
|
while (cursor.getTime() <= end.getTime()) {
|
||||||
|
const parts = formatter.formatToParts(cursor);
|
||||||
|
const currentDay = readNumericPart(parts, "day");
|
||||||
|
const currentMonth = readNumericPart(parts, "month");
|
||||||
|
if (currentDay === Math.trunc(targetDay) && currentMonth === monthOrder) {
|
||||||
|
return new Date(cursor);
|
||||||
|
}
|
||||||
|
cursor.setDate(cursor.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveHolidayGregorianDate(holiday) {
|
||||||
|
if (!holiday || typeof holiday !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarId = String(holiday.calendarId || "").trim().toLowerCase();
|
||||||
|
const monthId = String(holiday.monthId || "").trim().toLowerCase();
|
||||||
|
const day = Number(holiday.day);
|
||||||
|
const selectedYear = getSelectedYear();
|
||||||
|
|
||||||
|
if (calendarId === "gregorian") {
|
||||||
|
if (holiday?.dateRule) {
|
||||||
|
const ruledDate = resolveGregorianDateRule(holiday.dateRule, selectedYear);
|
||||||
|
if (ruledDate) {
|
||||||
|
return ruledDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthDay = parseMonthDayStartToken(holiday.monthDayStart) || parseMonthDayStartToken(holiday.dateText);
|
||||||
|
if (monthDay) {
|
||||||
|
return new Date(selectedYear, monthDay.month - 1, monthDay.day, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
const order = getGregorianMonthOrderFromId(monthId);
|
||||||
|
if (Number.isFinite(order) && Number.isFinite(day)) {
|
||||||
|
return new Date(selectedYear, order - 1, Math.trunc(day), 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calendarId === "hebrew") {
|
||||||
|
return findHebrewMonthDayInGregorianYear(monthId, day, selectedYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calendarId === "islamic") {
|
||||||
|
return findIslamicMonthDayInGregorianYear(monthId, day, selectedYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calendarId === "wheel-of-year") {
|
||||||
|
const monthDay = parseMonthDayStartToken(holiday.monthDayStart) || parseFirstMonthDayFromText(holiday.dateText);
|
||||||
|
if (monthDay?.month && monthDay?.day) {
|
||||||
|
return new Date(selectedYear, monthDay.month - 1, monthDay.day, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
if (monthDay?.monthIndex != null && monthDay?.day) {
|
||||||
|
return new Date(selectedYear, monthDay.monthIndex, monthDay.day, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findWheelMonthStartInGregorianYear(month, year) {
|
||||||
|
const parsed = parseFirstMonthDayFromText(month?.date);
|
||||||
|
if (!parsed || !Number.isFinite(year)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(Math.trunc(year), parsed.monthIndex, parsed.day, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGregorianReferenceDateForCalendarMonth(month) {
|
||||||
|
const calId = getSelectedCalendar();
|
||||||
|
const selectedYear = getSelectedYear();
|
||||||
|
if (calId === "gregorian") {
|
||||||
|
return getGregorianMonthStartDate(Number(month?.order), selectedYear);
|
||||||
|
}
|
||||||
|
if (calId === "hebrew") {
|
||||||
|
return findHebrewMonthStartInGregorianYear(month, selectedYear);
|
||||||
|
}
|
||||||
|
if (calId === "islamic") {
|
||||||
|
return findIslamicMonthStartInGregorianYear(month, selectedYear);
|
||||||
|
}
|
||||||
|
if (calId === "wheel-of-year") {
|
||||||
|
return findWheelMonthStartInGregorianYear(month, selectedYear);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatIsoDate(date) {
|
||||||
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = `${date.getMonth() + 1}`.padStart(2, "0");
|
||||||
|
const day = `${date.getDate()}`.padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCalendarDayToGregorian(month, dayNumber) {
|
||||||
|
const calId = getSelectedCalendar();
|
||||||
|
const selectedYear = getSelectedYear();
|
||||||
|
const day = Math.trunc(Number(dayNumber));
|
||||||
|
if (!Number.isFinite(day) || day <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calId === "gregorian") {
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
if (!Number.isFinite(monthOrder)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Date(selectedYear, monthOrder - 1, day, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calId === "hebrew") {
|
||||||
|
return findHebrewMonthDayInGregorianYear(month?.id, day, selectedYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calId === "islamic") {
|
||||||
|
return findIslamicMonthDayInGregorianYear(month?.id, day, selectedYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function intersectDateRanges(startA, endA, startB, endB) {
|
||||||
|
const start = startA.getTime() > startB.getTime() ? startA : startB;
|
||||||
|
const end = endA.getTime() < endB.getTime() ? endA : endB;
|
||||||
|
return start.getTime() <= end.getTime() ? { start, end } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(nextConfig = {}) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...nextConfig
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotCalendarDates = {
|
||||||
|
...(window.TarotCalendarDates || {}),
|
||||||
|
init,
|
||||||
|
parseMonthDayToken,
|
||||||
|
buildSignDateBounds,
|
||||||
|
addDays,
|
||||||
|
formatDateLabel,
|
||||||
|
isMonthDayInRange,
|
||||||
|
parseMonthDayTokensFromText,
|
||||||
|
parseDayRangeFromText,
|
||||||
|
isoToDateAtNoon,
|
||||||
|
getDaysInMonth,
|
||||||
|
getMonthStartWeekday,
|
||||||
|
parseMonthRange,
|
||||||
|
normalizeCalendarText,
|
||||||
|
formatGregorianReferenceDate,
|
||||||
|
formatCalendarDateFromGregorian,
|
||||||
|
getGregorianMonthStartDate,
|
||||||
|
findHebrewMonthStartInGregorianYear,
|
||||||
|
findIslamicMonthStartInGregorianYear,
|
||||||
|
parseFirstMonthDayFromText,
|
||||||
|
parseMonthDayStartToken,
|
||||||
|
resolveHolidayGregorianDate,
|
||||||
|
findWheelMonthStartInGregorianYear,
|
||||||
|
getGregorianReferenceDateForCalendarMonth,
|
||||||
|
formatIsoDate,
|
||||||
|
resolveCalendarDayToGregorian,
|
||||||
|
intersectDateRanges
|
||||||
|
};
|
||||||
|
})();
|
||||||
999
app/ui-calendar-detail.js
Normal file
999
app/ui-calendar-detail.js
Normal file
@@ -0,0 +1,999 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
getState: () => ({}),
|
||||||
|
getElements: () => ({}),
|
||||||
|
getSelectedMonth: () => null,
|
||||||
|
getSelectedDayFilterContext: () => null,
|
||||||
|
clearSelectedDayFilter: () => {},
|
||||||
|
toggleDayFilterEntry: () => {},
|
||||||
|
toggleDayRangeFilter: () => {},
|
||||||
|
getMonthSubtitle: () => "",
|
||||||
|
getMonthDayLinkRows: () => [],
|
||||||
|
buildDecanTarotRowsForMonth: () => [],
|
||||||
|
buildHolidayList: () => [],
|
||||||
|
matchesSearch: () => true,
|
||||||
|
eventSearchText: () => "",
|
||||||
|
holidaySearchText: () => "",
|
||||||
|
getDisplayTarotName: (cardName) => cardName || "",
|
||||||
|
cap: (value) => String(value || "").trim(),
|
||||||
|
formatGregorianReferenceDate: () => "--",
|
||||||
|
getDaysInMonth: () => null,
|
||||||
|
getMonthStartWeekday: () => "--",
|
||||||
|
getGregorianMonthStartDate: () => null,
|
||||||
|
formatCalendarDateFromGregorian: () => "--",
|
||||||
|
parseMonthDayToken: () => null,
|
||||||
|
parseMonthDayTokensFromText: () => [],
|
||||||
|
parseMonthDayStartToken: () => null,
|
||||||
|
parseDayRangeFromText: () => null,
|
||||||
|
parseMonthRange: () => "",
|
||||||
|
formatIsoDate: () => "",
|
||||||
|
resolveHolidayGregorianDate: () => null,
|
||||||
|
isMonthDayInRange: () => false,
|
||||||
|
intersectDateRanges: () => null,
|
||||||
|
getGregorianReferenceDateForCalendarMonth: () => null,
|
||||||
|
normalizeCalendarText: (value) => String(value || "").trim().toLowerCase(),
|
||||||
|
findGodIdByName: () => null
|
||||||
|
};
|
||||||
|
|
||||||
|
function init(config) {
|
||||||
|
Object.assign(api, config || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getState() {
|
||||||
|
return api.getState?.() || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function planetLabel(planetId) {
|
||||||
|
if (!planetId) {
|
||||||
|
return "Planet";
|
||||||
|
}
|
||||||
|
|
||||||
|
const planet = getState().planetsById?.get(planetId);
|
||||||
|
if (!planet) {
|
||||||
|
return api.cap(planetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${planet.symbol || ""} ${planet.name || api.cap(planetId)}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function zodiacLabel(signId) {
|
||||||
|
if (!signId) {
|
||||||
|
return "Zodiac";
|
||||||
|
}
|
||||||
|
|
||||||
|
const sign = getState().signsById?.get(signId);
|
||||||
|
if (!sign) {
|
||||||
|
return api.cap(signId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${sign.symbol || ""} ${sign.name || api.cap(signId)}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function godLabel(godId, godName) {
|
||||||
|
if (godName) {
|
||||||
|
return godName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!godId) {
|
||||||
|
return "Deity";
|
||||||
|
}
|
||||||
|
|
||||||
|
const god = getState().godsById?.get(godId);
|
||||||
|
return god?.name || api.cap(godId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hebrewLabel(hebrewLetterId) {
|
||||||
|
if (!hebrewLetterId) {
|
||||||
|
return "Hebrew Letter";
|
||||||
|
}
|
||||||
|
|
||||||
|
const letter = getState().hebrewById?.get(hebrewLetterId);
|
||||||
|
if (!letter) {
|
||||||
|
return api.cap(hebrewLetterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${letter.char || ""} ${letter.name || api.cap(hebrewLetterId)}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeDigitalRoot(value) {
|
||||||
|
let current = Math.abs(Math.trunc(Number(value)));
|
||||||
|
if (!Number.isFinite(current)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (current >= 10) {
|
||||||
|
current = String(current)
|
||||||
|
.split("")
|
||||||
|
.reduce((sum, digit) => sum + Number(digit), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAssociationButtons(associations) {
|
||||||
|
if (!associations || typeof associations !== "object") {
|
||||||
|
return '<div class="planet-text">--</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttons = [];
|
||||||
|
|
||||||
|
if (associations.planetId) {
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="planet" data-planet-id="${associations.planetId}">${planetLabel(associations.planetId)} ↗</button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associations.zodiacSignId) {
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="zodiac" data-sign-id="${associations.zodiacSignId}">${zodiacLabel(associations.zodiacSignId)} ↗</button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isFinite(Number(associations.numberValue))) {
|
||||||
|
const rawNumber = Math.trunc(Number(associations.numberValue));
|
||||||
|
if (rawNumber >= 0) {
|
||||||
|
const numberValue = computeDigitalRoot(rawNumber);
|
||||||
|
if (numberValue != null) {
|
||||||
|
const label = rawNumber === numberValue
|
||||||
|
? `Number ${numberValue}`
|
||||||
|
: `Number ${numberValue} (from ${rawNumber})`;
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="number" data-number-value="${numberValue}">${label} ↗</button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associations.tarotCard) {
|
||||||
|
const explicitTrumpNumber = Number(associations.tarotTrumpNumber);
|
||||||
|
const tarotTrumpNumber = Number.isFinite(explicitTrumpNumber) ? explicitTrumpNumber : null;
|
||||||
|
const tarotLabel = api.getDisplayTarotName(associations.tarotCard, tarotTrumpNumber);
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="tarot-card" data-card-name="${associations.tarotCard}" data-trump-number="${tarotTrumpNumber ?? ""}">${tarotLabel} ↗</button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associations.godId || associations.godName) {
|
||||||
|
const label = godLabel(associations.godId, associations.godName);
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="god" data-god-id="${associations.godId || ""}" data-god-name="${associations.godName || label}">${label} ↗</button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associations.hebrewLetterId) {
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="alphabet" data-alphabet="hebrew" data-hebrew-letter-id="${associations.hebrewLetterId}">${hebrewLabel(associations.hebrewLetterId)} ↗</button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associations.kabbalahPathNumber != null) {
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="kabbalah" data-path-no="${associations.kabbalahPathNumber}">Path ${associations.kabbalahPathNumber} ↗</button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associations.iChingPlanetaryInfluence) {
|
||||||
|
buttons.push(
|
||||||
|
`<button class="alpha-nav-btn" data-nav="iching" data-planetary-influence="${associations.iChingPlanetaryInfluence}">I Ching · ${associations.iChingPlanetaryInfluence} ↗</button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!buttons.length) {
|
||||||
|
return '<div class="planet-text">--</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<div class="alpha-nav-btns">${buttons.join("")}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFactsCard(month) {
|
||||||
|
const currentState = getState();
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
const daysInMonth = api.getDaysInMonth(currentState.selectedYear, monthOrder);
|
||||||
|
const hoursInMonth = Number.isFinite(daysInMonth) ? daysInMonth * 24 : null;
|
||||||
|
const firstWeekday = Number.isFinite(monthOrder)
|
||||||
|
? api.getMonthStartWeekday(currentState.selectedYear, monthOrder)
|
||||||
|
: "--";
|
||||||
|
const gregorianStartDate = api.getGregorianMonthStartDate(monthOrder);
|
||||||
|
const hebrewStartReference = api.formatCalendarDateFromGregorian(gregorianStartDate, "hebrew");
|
||||||
|
const islamicStartReference = api.formatCalendarDateFromGregorian(gregorianStartDate, "islamic");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Month Facts</strong>
|
||||||
|
<div class="planet-text">
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Year</dt><dd>${currentState.selectedYear}</dd>
|
||||||
|
<dt>Start Date (Gregorian)</dt><dd>${api.formatGregorianReferenceDate(gregorianStartDate)}</dd>
|
||||||
|
<dt>Days</dt><dd>${daysInMonth ?? "--"}</dd>
|
||||||
|
<dt>Hours</dt><dd>${hoursInMonth ?? "--"}</dd>
|
||||||
|
<dt>Starts On</dt><dd>${firstWeekday}</dd>
|
||||||
|
<dt>Hebrew On 1st</dt><dd>${hebrewStartReference}</dd>
|
||||||
|
<dt>Islamic On 1st</dt><dd>${islamicStartReference}</dd>
|
||||||
|
<dt>North Season</dt><dd>${month.seasonNorth || "--"}</dd>
|
||||||
|
<dt>South Season</dt><dd>${month.seasonSouth || "--"}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAssociationsCard(month) {
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
const associations = {
|
||||||
|
...(month?.associations || {}),
|
||||||
|
...(Number.isFinite(monthOrder) ? { numberValue: Math.trunc(monthOrder) } : {})
|
||||||
|
};
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Associations</strong>
|
||||||
|
<div class="planet-text">${month.coreTheme || "--"}</div>
|
||||||
|
${buildAssociationButtons(associations)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEventsCard(month) {
|
||||||
|
const currentState = getState();
|
||||||
|
const allEvents = Array.isArray(month?.events) ? month.events : [];
|
||||||
|
if (!allEvents.length) {
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Monthly Events</strong>
|
||||||
|
<div class="planet-text">No monthly events listed.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedDay = api.getSelectedDayFilterContext(month);
|
||||||
|
|
||||||
|
function eventMatchesDay(event) {
|
||||||
|
if (!selectedDay) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedDay.entries.some((entry) => {
|
||||||
|
const targetDate = entry.gregorianDate;
|
||||||
|
const targetMonth = targetDate?.getMonth() + 1;
|
||||||
|
const targetDayNo = targetDate?.getDate();
|
||||||
|
|
||||||
|
const explicitDate = api.parseMonthDayToken(event?.date);
|
||||||
|
if (explicitDate && Number.isFinite(targetMonth) && Number.isFinite(targetDayNo)) {
|
||||||
|
return explicitDate.month === targetMonth && explicitDate.day === targetDayNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeTokens = api.parseMonthDayTokensFromText(event?.dateRange || event?.dateText || "");
|
||||||
|
if (rangeTokens.length >= 2 && Number.isFinite(targetMonth) && Number.isFinite(targetDayNo)) {
|
||||||
|
const start = rangeTokens[0];
|
||||||
|
const end = rangeTokens[1];
|
||||||
|
return api.isMonthDayInRange(targetMonth, targetDayNo, start.month, start.day, end.month, end.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayRange = api.parseDayRangeFromText(event?.date || event?.dateRange || event?.dateText || "");
|
||||||
|
if (dayRange) {
|
||||||
|
return entry.dayNumber >= dayRange.startDay && entry.dayNumber <= dayRange.endDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayFiltered = allEvents.filter((event) => eventMatchesDay(event));
|
||||||
|
const events = currentState.searchQuery
|
||||||
|
? dayFiltered.filter((event) => api.matchesSearch(api.eventSearchText(event)))
|
||||||
|
: dayFiltered;
|
||||||
|
|
||||||
|
if (!events.length) {
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Monthly Events</strong>
|
||||||
|
<div class="planet-text">No monthly events match current search.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = events.map((event) => {
|
||||||
|
const dateText = event?.date || event?.dateRange || "--";
|
||||||
|
return `
|
||||||
|
<div class="cal-item-row">
|
||||||
|
<div class="cal-item-head">
|
||||||
|
<span class="cal-item-name">${event?.name || "Untitled"}</span>
|
||||||
|
<span class="planet-list-meta">${dateText}</span>
|
||||||
|
</div>
|
||||||
|
<div class="planet-text">${event?.description || ""}</div>
|
||||||
|
${buildAssociationButtons(event?.associations)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Monthly Events</strong>
|
||||||
|
<div class="cal-item-stack">${rows}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHolidaysCard(month, title = "Holiday Repository") {
|
||||||
|
const currentState = getState();
|
||||||
|
const allHolidays = api.buildHolidayList(month);
|
||||||
|
if (!allHolidays.length) {
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>${title}</strong>
|
||||||
|
<div class="planet-text">No holidays listed in the repository for this month.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedDay = api.getSelectedDayFilterContext(month);
|
||||||
|
|
||||||
|
function holidayMatchesDay(holiday) {
|
||||||
|
if (!selectedDay) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedDay.entries.some((entry) => {
|
||||||
|
const targetDate = entry.gregorianDate;
|
||||||
|
const targetMonth = targetDate?.getMonth() + 1;
|
||||||
|
const targetDayNo = targetDate?.getDate();
|
||||||
|
|
||||||
|
const exactResolved = api.resolveHolidayGregorianDate(holiday);
|
||||||
|
if (exactResolved instanceof Date && !Number.isNaN(exactResolved.getTime()) && targetDate instanceof Date) {
|
||||||
|
return api.formatIsoDate(exactResolved) === api.formatIsoDate(targetDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentState.selectedCalendar === "gregorian" && Number.isFinite(targetMonth) && Number.isFinite(targetDayNo)) {
|
||||||
|
const tokens = api.parseMonthDayTokensFromText(holiday?.dateText || holiday?.dateRange || "");
|
||||||
|
if (tokens.length >= 2) {
|
||||||
|
const start = tokens[0];
|
||||||
|
const end = tokens[1];
|
||||||
|
return api.isMonthDayInRange(targetMonth, targetDayNo, start.month, start.day, end.month, end.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens.length === 1) {
|
||||||
|
const single = tokens[0];
|
||||||
|
return single.month === targetMonth && single.day === targetDayNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const direct = api.parseMonthDayStartToken(holiday?.monthDayStart || holiday?.dateText || "");
|
||||||
|
if (direct) {
|
||||||
|
return direct.month === targetMonth && direct.day === targetDayNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isFinite(Number(holiday?.day))) {
|
||||||
|
return Number(holiday.day) === entry.dayNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const localRange = api.parseDayRangeFromText(holiday?.dateText || holiday?.dateRange || "");
|
||||||
|
if (localRange) {
|
||||||
|
return entry.dayNumber >= localRange.startDay && entry.dayNumber <= localRange.endDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayFiltered = allHolidays.filter((holiday) => holidayMatchesDay(holiday));
|
||||||
|
const holidays = currentState.searchQuery
|
||||||
|
? dayFiltered.filter((holiday) => api.matchesSearch(api.holidaySearchText(holiday)))
|
||||||
|
: dayFiltered;
|
||||||
|
|
||||||
|
if (!holidays.length) {
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>${title}</strong>
|
||||||
|
<div class="planet-text">No holidays match current filters.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = holidays.map((holiday) => {
|
||||||
|
const dateText = holiday?.dateText || holiday?.dateRange || holiday?.date || "--";
|
||||||
|
return `
|
||||||
|
<div class="cal-item-row">
|
||||||
|
<div class="cal-item-head">
|
||||||
|
<span class="cal-item-name">${holiday?.name || "Untitled"}</span>
|
||||||
|
<span class="planet-list-meta">${dateText}</span>
|
||||||
|
</div>
|
||||||
|
<div class="planet-text">${holiday?.description || holiday?.kind || ""}</div>
|
||||||
|
${buildAssociationButtons(holiday?.associations)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>${title}</strong>
|
||||||
|
<div class="cal-item-stack">${rows}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSignIdByAstrologyName(name) {
|
||||||
|
const token = api.normalizeCalendarText(name);
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [signId, sign] of getState().signsById || []) {
|
||||||
|
const idToken = api.normalizeCalendarText(signId);
|
||||||
|
const nameToken = api.normalizeCalendarText(sign?.name?.en || sign?.name || "");
|
||||||
|
if (token === idToken || token === nameToken) {
|
||||||
|
return signId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMajorArcanaRowsForMonth(month) {
|
||||||
|
const currentState = getState();
|
||||||
|
if (currentState.selectedCalendar !== "gregorian") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
if (!Number.isFinite(monthOrder)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthStart = new Date(currentState.selectedYear, monthOrder - 1, 1, 12, 0, 0, 0);
|
||||||
|
const monthEnd = new Date(currentState.selectedYear, monthOrder, 0, 12, 0, 0, 0);
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
currentState.hebrewById?.forEach((letter) => {
|
||||||
|
const astrologyType = api.normalizeCalendarText(letter?.astrology?.type);
|
||||||
|
if (astrologyType !== "zodiac") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signId = findSignIdByAstrologyName(letter?.astrology?.name);
|
||||||
|
const sign = signId ? currentState.signsById?.get(signId) : null;
|
||||||
|
if (!sign) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startToken = api.parseMonthDayToken(sign?.start);
|
||||||
|
const endToken = api.parseMonthDayToken(sign?.end);
|
||||||
|
if (!startToken || !endToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spanStart = new Date(currentState.selectedYear, startToken.month - 1, startToken.day, 12, 0, 0, 0);
|
||||||
|
const spanEnd = new Date(currentState.selectedYear, endToken.month - 1, endToken.day, 12, 0, 0, 0);
|
||||||
|
const wraps = spanEnd.getTime() < spanStart.getTime();
|
||||||
|
|
||||||
|
const segments = wraps
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
start: spanStart,
|
||||||
|
end: new Date(currentState.selectedYear, 11, 31, 12, 0, 0, 0)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: new Date(currentState.selectedYear, 0, 1, 12, 0, 0, 0),
|
||||||
|
end: spanEnd
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [{ start: spanStart, end: spanEnd }];
|
||||||
|
|
||||||
|
segments.forEach((segment) => {
|
||||||
|
const overlap = api.intersectDateRanges(segment.start, segment.end, monthStart, monthEnd);
|
||||||
|
if (!overlap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeStartDay = overlap.start.getDate();
|
||||||
|
const rangeEndDay = overlap.end.getDate();
|
||||||
|
const cardName = String(letter?.tarot?.card || "").trim();
|
||||||
|
const trumpNumber = Number(letter?.tarot?.trumpNumber);
|
||||||
|
if (!cardName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
id: `${signId}-${rangeStartDay}-${rangeEndDay}`,
|
||||||
|
signId,
|
||||||
|
signName: sign?.name?.en || sign?.name || signId,
|
||||||
|
signSymbol: sign?.symbol || "",
|
||||||
|
cardName,
|
||||||
|
trumpNumber: Number.isFinite(trumpNumber) ? Math.trunc(trumpNumber) : null,
|
||||||
|
hebrewLetterId: String(letter?.hebrewLetterId || "").trim(),
|
||||||
|
hebrewLetterName: String(letter?.name || "").trim(),
|
||||||
|
hebrewLetterChar: String(letter?.char || "").trim(),
|
||||||
|
dayStart: rangeStartDay,
|
||||||
|
dayEnd: rangeEndDay,
|
||||||
|
rangeLabel: `${month?.name || "Month"} ${rangeStartDay}-${rangeEndDay}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
rows.sort((left, right) => {
|
||||||
|
if (left.dayStart !== right.dayStart) {
|
||||||
|
return left.dayStart - right.dayStart;
|
||||||
|
}
|
||||||
|
return left.cardName.localeCompare(right.cardName);
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMajorArcanaCard(month) {
|
||||||
|
const selectedDay = api.getSelectedDayFilterContext(month);
|
||||||
|
const allRows = buildMajorArcanaRowsForMonth(month);
|
||||||
|
|
||||||
|
const rows = selectedDay
|
||||||
|
? allRows.filter((row) => selectedDay.entries.some((entry) => entry.dayNumber >= row.dayStart && entry.dayNumber <= row.dayEnd))
|
||||||
|
: allRows;
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Major Arcana Windows</strong>
|
||||||
|
<div class="planet-text">No major arcana windows for this month.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = rows.map((row) => {
|
||||||
|
const label = row.hebrewLetterId
|
||||||
|
? `${row.hebrewLetterChar ? `${row.hebrewLetterChar} ` : ""}${row.hebrewLetterName || row.hebrewLetterId}`
|
||||||
|
: "--";
|
||||||
|
const displayCardName = api.getDisplayTarotName(row.cardName, row.trumpNumber);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="cal-item-row">
|
||||||
|
<div class="cal-item-head">
|
||||||
|
<span class="cal-item-name">${displayCardName}${row.trumpNumber != null ? ` · Trump ${row.trumpNumber}` : ""}</span>
|
||||||
|
<span class="planet-list-meta">${row.rangeLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div class="planet-list-meta">${row.signSymbol} ${row.signName} · Hebrew: ${label}</div>
|
||||||
|
<div class="alpha-nav-btns">
|
||||||
|
<button class="alpha-nav-btn" data-nav="calendar-day-range" data-range-start="${row.dayStart}" data-range-end="${row.dayEnd}">${row.rangeLabel} ↗</button>
|
||||||
|
<button class="alpha-nav-btn" data-nav="tarot-card" data-card-name="${row.cardName}" data-trump-number="${row.trumpNumber ?? ""}">${displayCardName} ↗</button>
|
||||||
|
${row.hebrewLetterId ? `<button class="alpha-nav-btn" data-nav="alphabet" data-alphabet="hebrew" data-hebrew-letter-id="${row.hebrewLetterId}">${label} ↗</button>` : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Major Arcana Windows</strong>
|
||||||
|
<div class="cal-item-stack">${list}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDecanTarotCard(month) {
|
||||||
|
const selectedDay = api.getSelectedDayFilterContext(month);
|
||||||
|
const allRows = api.buildDecanTarotRowsForMonth(month);
|
||||||
|
const rows = selectedDay
|
||||||
|
? allRows.filter((row) => selectedDay.entries.some((entry) => {
|
||||||
|
const targetDate = entry.gregorianDate;
|
||||||
|
if (!(targetDate instanceof Date) || Number.isNaN(targetDate.getTime())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMonth = targetDate.getMonth() + 1;
|
||||||
|
const targetDayNo = targetDate.getDate();
|
||||||
|
return api.isMonthDayInRange(
|
||||||
|
targetMonth,
|
||||||
|
targetDayNo,
|
||||||
|
row.startMonth,
|
||||||
|
row.startDay,
|
||||||
|
row.endMonth,
|
||||||
|
row.endDay
|
||||||
|
);
|
||||||
|
}))
|
||||||
|
: allRows;
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Decan Tarot Windows</strong>
|
||||||
|
<div class="planet-text">No decan tarot windows for this month.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = rows.map((row) => {
|
||||||
|
const displayCardName = api.getDisplayTarotName(row.cardName);
|
||||||
|
return `
|
||||||
|
<div class="cal-item-row">
|
||||||
|
<div class="cal-item-head">
|
||||||
|
<span class="cal-item-name">${row.signSymbol} ${row.signName} · Decan ${row.decanIndex}</span>
|
||||||
|
<span class="planet-list-meta">${row.startDegree}°–${row.endDegree}° · ${row.dateRange}</span>
|
||||||
|
</div>
|
||||||
|
<div class="alpha-nav-btns">
|
||||||
|
<button class="alpha-nav-btn" data-nav="tarot-card" data-card-name="${row.cardName}">${displayCardName} ↗</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Decan Tarot Windows</strong>
|
||||||
|
<div class="cal-item-stack">${list}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDayLinksCard(month) {
|
||||||
|
const rows = api.getMonthDayLinkRows(month);
|
||||||
|
if (!rows.length) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedContext = api.getSelectedDayFilterContext(month);
|
||||||
|
const selectedDaySet = selectedContext?.dayNumbers || new Set();
|
||||||
|
const selectedDays = selectedContext?.entries?.map((entry) => entry.dayNumber) || [];
|
||||||
|
const selectedSummary = selectedDays.length ? selectedDays.join(", ") : "";
|
||||||
|
|
||||||
|
const links = rows.map((row) => {
|
||||||
|
if (!row.isResolved) {
|
||||||
|
return `<span class="planet-list-meta">${row.day}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSelected = selectedDaySet.has(Number(row.day));
|
||||||
|
return `<button class="alpha-nav-btn${isSelected ? " is-selected" : ""}" data-nav="calendar-day" data-day-number="${row.day}" data-gregorian-date="${row.gregorianDate}" aria-pressed="${isSelected ? "true" : "false"}" title="Filter this month by day ${row.day}">${row.day}</button>`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
const clearButton = selectedContext
|
||||||
|
? '<button class="alpha-nav-btn" data-nav="calendar-day-clear" type="button">Show All Days</button>'
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const helperText = selectedContext
|
||||||
|
? `<div class="planet-list-meta">Filtered to days: ${selectedSummary}</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Day Links</strong>
|
||||||
|
<div class="planet-text">Filter this month to events, holidays, and data connected to a specific day.</div>
|
||||||
|
${helperText}
|
||||||
|
<div class="alpha-nav-btns">${links}</div>
|
||||||
|
${clearButton ? `<div class="alpha-nav-btns">${clearButton}</div>` : ""}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHebrewMonthDetail(month) {
|
||||||
|
const currentState = getState();
|
||||||
|
const gregorianStartDate = api.getGregorianReferenceDateForCalendarMonth(month);
|
||||||
|
const factsRows = [
|
||||||
|
["Hebrew Name", month.nativeName || "--"],
|
||||||
|
["Month Order", month.leapYearOnly ? `${month.order} (leap year only)` : String(month.order)],
|
||||||
|
["Gregorian Reference Year", String(currentState.selectedYear)],
|
||||||
|
["Month Start (Gregorian)", api.formatGregorianReferenceDate(gregorianStartDate)],
|
||||||
|
["Days", month.daysVariant ? `${month.days}–${month.daysVariant} (varies)` : String(month.days || "--")],
|
||||||
|
["Season", month.season || "--"],
|
||||||
|
["Zodiac Sign", api.cap(month.zodiacSign) || "--"],
|
||||||
|
["Tribe of Israel", month.tribe || "--"],
|
||||||
|
["Sense", month.sense || "--"],
|
||||||
|
["Hebrew Letter", month.hebrewLetter || "--"]
|
||||||
|
].map(([dt, dd]) => `<dt>${dt}</dt><dd>${dd}</dd>`).join("");
|
||||||
|
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
const navButtons = buildAssociationButtons({
|
||||||
|
...(month?.associations || {}),
|
||||||
|
...(Number.isFinite(monthOrder) ? { numberValue: Math.trunc(monthOrder) } : {})
|
||||||
|
});
|
||||||
|
const connectionsCard = navButtons
|
||||||
|
? `<div class="planet-meta-card"><strong>Connections</strong>${navButtons}</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-grid">
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Month Facts</strong>
|
||||||
|
<div class="planet-text">
|
||||||
|
<dl class="alpha-dl">${factsRows}</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${connectionsCard}
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>About ${month.name}</strong>
|
||||||
|
<div class="planet-text">${month.description || "--"}</div>
|
||||||
|
</div>
|
||||||
|
${renderDayLinksCard(month)}
|
||||||
|
${renderHolidaysCard(month, "Holiday Repository")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIslamicMonthDetail(month) {
|
||||||
|
const currentState = getState();
|
||||||
|
const gregorianStartDate = api.getGregorianReferenceDateForCalendarMonth(month);
|
||||||
|
const factsRows = [
|
||||||
|
["Arabic Name", month.nativeName || "--"],
|
||||||
|
["Month Order", String(month.order)],
|
||||||
|
["Gregorian Reference Year", String(currentState.selectedYear)],
|
||||||
|
["Month Start (Gregorian)", api.formatGregorianReferenceDate(gregorianStartDate)],
|
||||||
|
["Meaning", month.meaning || "--"],
|
||||||
|
["Days", month.daysVariant ? `${month.days}–${month.daysVariant} (varies)` : String(month.days || "--")],
|
||||||
|
["Sacred Month", month.sacred ? "Yes - warfare prohibited" : "No"]
|
||||||
|
].map(([dt, dd]) => `<dt>${dt}</dt><dd>${dd}</dd>`).join("");
|
||||||
|
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
const hasNumberLink = Number.isFinite(monthOrder) && monthOrder >= 0;
|
||||||
|
const navButtons = hasNumberLink
|
||||||
|
? buildAssociationButtons({ numberValue: Math.trunc(monthOrder) })
|
||||||
|
: "";
|
||||||
|
const connectionsCard = hasNumberLink
|
||||||
|
? `<div class="planet-meta-card"><strong>Connections</strong>${navButtons}</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-grid">
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Month Facts</strong>
|
||||||
|
<div class="planet-text">
|
||||||
|
<dl class="alpha-dl">${factsRows}</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${connectionsCard}
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>About ${month.name}</strong>
|
||||||
|
<div class="planet-text">${month.description || "--"}</div>
|
||||||
|
</div>
|
||||||
|
${renderDayLinksCard(month)}
|
||||||
|
${renderHolidaysCard(month, "Holiday Repository")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWheelDeityButtons(deities) {
|
||||||
|
const buttons = [];
|
||||||
|
(Array.isArray(deities) ? deities : []).forEach((rawName) => {
|
||||||
|
const cleanName = String(rawName || "").replace(/\s*\/.*$/, "").replace(/\s*\(.*\)$/, "").trim();
|
||||||
|
const godId = api.findGodIdByName(cleanName) || api.findGodIdByName(rawName);
|
||||||
|
if (!godId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const god = getState().godsById?.get(godId);
|
||||||
|
const label = god?.name || cleanName;
|
||||||
|
buttons.push(`<button class="alpha-nav-btn" data-nav="god" data-god-id="${godId}" data-god-name="${label}">${label} ↗</button>`);
|
||||||
|
});
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWheelMonthDetail(month) {
|
||||||
|
const currentState = getState();
|
||||||
|
const gregorianStartDate = api.getGregorianReferenceDateForCalendarMonth(month);
|
||||||
|
const assoc = month?.associations;
|
||||||
|
const themes = Array.isArray(assoc?.themes) ? assoc.themes.join(", ") : "--";
|
||||||
|
const deities = Array.isArray(assoc?.deities) ? assoc.deities.join(", ") : "--";
|
||||||
|
const colors = Array.isArray(assoc?.colors) ? assoc.colors.join(", ") : "--";
|
||||||
|
const herbs = Array.isArray(assoc?.herbs) ? assoc.herbs.join(", ") : "--";
|
||||||
|
|
||||||
|
const factsRows = [
|
||||||
|
["Date", month.date || "--"],
|
||||||
|
["Type", api.cap(month.type) || "--"],
|
||||||
|
["Gregorian Reference Year", String(currentState.selectedYear)],
|
||||||
|
["Start (Gregorian)", api.formatGregorianReferenceDate(gregorianStartDate)],
|
||||||
|
["Season", month.season || "--"],
|
||||||
|
["Element", api.cap(month.element) || "--"],
|
||||||
|
["Direction", assoc?.direction || "--"]
|
||||||
|
].map(([dt, dd]) => `<dt>${dt}</dt><dd>${dd}</dd>`).join("");
|
||||||
|
|
||||||
|
const assocRows = [
|
||||||
|
["Themes", themes],
|
||||||
|
["Deities", deities],
|
||||||
|
["Colors", colors],
|
||||||
|
["Herbs", herbs]
|
||||||
|
].map(([dt, dd]) => `<dt>${dt}</dt><dd class="planet-text">${dd}</dd>`).join("");
|
||||||
|
|
||||||
|
const deityButtons = buildWheelDeityButtons(assoc?.deities);
|
||||||
|
const deityLinksCard = deityButtons.length
|
||||||
|
? `<div class="planet-meta-card"><strong>Linked Deities</strong><div class="alpha-nav-btns">${deityButtons.join("")}</div></div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const monthOrder = Number(month?.order);
|
||||||
|
const hasNumberLink = Number.isFinite(monthOrder) && monthOrder >= 0;
|
||||||
|
const numberButtons = hasNumberLink
|
||||||
|
? buildAssociationButtons({ numberValue: Math.trunc(monthOrder) })
|
||||||
|
: "";
|
||||||
|
const numberLinksCard = hasNumberLink
|
||||||
|
? `<div class="planet-meta-card"><strong>Connections</strong>${numberButtons}</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="planet-meta-grid">
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Sabbat Facts</strong>
|
||||||
|
<div class="planet-text">
|
||||||
|
<dl class="alpha-dl">${factsRows}</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>About ${month.name}</strong>
|
||||||
|
<div class="planet-text">${month.description || "--"}</div>
|
||||||
|
</div>
|
||||||
|
<div class="planet-meta-card">
|
||||||
|
<strong>Associations</strong>
|
||||||
|
<div class="planet-text">
|
||||||
|
<dl class="alpha-dl">${assocRows}</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${renderDayLinksCard(month)}
|
||||||
|
${numberLinksCard}
|
||||||
|
${deityLinksCard}
|
||||||
|
${renderHolidaysCard(month, "Holiday Repository")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachNavHandlers(detailBodyEl) {
|
||||||
|
if (!detailBodyEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
detailBodyEl.querySelectorAll("[data-nav]").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const navType = button.dataset.nav;
|
||||||
|
|
||||||
|
if (navType === "planet" && button.dataset.planetId) {
|
||||||
|
document.dispatchEvent(new CustomEvent("nav:planet", {
|
||||||
|
detail: { planetId: button.dataset.planetId }
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navType === "zodiac" && button.dataset.signId) {
|
||||||
|
document.dispatchEvent(new CustomEvent("nav:zodiac", {
|
||||||
|
detail: { signId: button.dataset.signId }
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navType === "number" && button.dataset.numberValue) {
|
||||||
|
document.dispatchEvent(new CustomEvent("nav:number", {
|
||||||
|
detail: { value: Number(button.dataset.numberValue) }
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navType === "tarot-card" && button.dataset.cardName) {
|
||||||
|
const trumpNumber = Number(button.dataset.trumpNumber);
|
||||||
|
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
|
||||||
|
detail: {
|
||||||
|
cardName: button.dataset.cardName,
|
||||||
|
trumpNumber: Number.isFinite(trumpNumber) ? trumpNumber : undefined
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navType === "god") {
|
||||||
|
document.dispatchEvent(new CustomEvent("nav:gods", {
|
||||||
|
detail: {
|
||||||
|
godId: button.dataset.godId || undefined,
|
||||||
|
godName: button.dataset.godName || undefined
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navType === "alphabet" && button.dataset.hebrewLetterId) {
|
||||||
|
document.dispatchEvent(new CustomEvent("nav:alphabet", {
|
||||||
|
detail: {
|
||||||
|
alphabet: "hebrew",
|
||||||
|
hebrewLetterId: button.dataset.hebrewLetterId
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navType === "kabbalah" && button.dataset.pathNo) {
|
||||||
|
document.dispatchEvent(new CustomEvent("nav:kabbalah-path", {
|
||||||
|
detail: { pathNo: Number(button.dataset.pathNo) }
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navType === "iching" && button.dataset.planetaryInfluence) {
|
||||||
|
document.dispatchEvent(new CustomEvent("nav:iching", {
|
||||||
|
detail: {
|
||||||
|
planetaryInfluence: button.dataset.planetaryInfluence
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navType === "calendar-month" && button.dataset.monthId) {
|
||||||
|
document.dispatchEvent(new CustomEvent("nav:calendar-month", {
|
||||||
|
detail: {
|
||||||
|
calendarId: button.dataset.calendarId || undefined,
|
||||||
|
monthId: button.dataset.monthId
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navType === "calendar-day" && button.dataset.dayNumber) {
|
||||||
|
const month = api.getSelectedMonth();
|
||||||
|
const dayNumber = Number(button.dataset.dayNumber);
|
||||||
|
if (!month || !Number.isFinite(dayNumber)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.toggleDayFilterEntry(month, dayNumber, button.dataset.gregorianDate);
|
||||||
|
renderDetail(api.getElements());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navType === "calendar-day-range" && button.dataset.rangeStart && button.dataset.rangeEnd) {
|
||||||
|
const month = api.getSelectedMonth();
|
||||||
|
if (!month) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.toggleDayRangeFilter(month, Number(button.dataset.rangeStart), Number(button.dataset.rangeEnd));
|
||||||
|
renderDetail(api.getElements());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navType === "calendar-day-clear") {
|
||||||
|
api.clearSelectedDayFilter();
|
||||||
|
renderDetail(api.getElements());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDetail(elements) {
|
||||||
|
const { detailNameEl, detailSubEl, detailBodyEl } = elements || {};
|
||||||
|
if (!detailBodyEl || !detailNameEl || !detailSubEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const month = api.getSelectedMonth();
|
||||||
|
if (!month) {
|
||||||
|
detailNameEl.textContent = "--";
|
||||||
|
detailSubEl.textContent = "Select a month to explore";
|
||||||
|
detailBodyEl.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
detailNameEl.textContent = month.name || month.id;
|
||||||
|
|
||||||
|
const currentState = getState();
|
||||||
|
if (currentState.selectedCalendar === "gregorian") {
|
||||||
|
detailSubEl.textContent = `${api.parseMonthRange(month)} · ${month.coreTheme || "Month correspondences"}`;
|
||||||
|
detailBodyEl.innerHTML = `
|
||||||
|
<div class="planet-meta-grid">
|
||||||
|
${renderFactsCard(month)}
|
||||||
|
${renderDayLinksCard(month)}
|
||||||
|
${renderAssociationsCard(month)}
|
||||||
|
${renderMajorArcanaCard(month)}
|
||||||
|
${renderDecanTarotCard(month)}
|
||||||
|
${renderEventsCard(month)}
|
||||||
|
${renderHolidaysCard(month, "Holiday Repository")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (currentState.selectedCalendar === "hebrew") {
|
||||||
|
detailSubEl.textContent = api.getMonthSubtitle(month);
|
||||||
|
detailBodyEl.innerHTML = renderHebrewMonthDetail(month);
|
||||||
|
} else if (currentState.selectedCalendar === "islamic") {
|
||||||
|
detailSubEl.textContent = api.getMonthSubtitle(month);
|
||||||
|
detailBodyEl.innerHTML = renderIslamicMonthDetail(month);
|
||||||
|
} else {
|
||||||
|
detailSubEl.textContent = api.getMonthSubtitle(month);
|
||||||
|
detailBodyEl.innerHTML = renderWheelMonthDetail(month);
|
||||||
|
}
|
||||||
|
|
||||||
|
attachNavHandlers(detailBodyEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotCalendarDetail = {
|
||||||
|
init,
|
||||||
|
renderDetail,
|
||||||
|
attachNavHandlers
|
||||||
|
};
|
||||||
|
})();
|
||||||
314
app/ui-calendar-formatting.js
Normal file
314
app/ui-calendar-formatting.js
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
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" }
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = {};
|
||||||
|
|
||||||
|
function getCurrentTimeFormat() {
|
||||||
|
return config.getCurrentTimeFormat?.() || "minutes";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReferenceData() {
|
||||||
|
return config.getReferenceData?.() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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) {
|
||||||
|
const currentTimeFormat = getCurrentTimeFormat();
|
||||||
|
if (currentTimeFormat === "hours") {
|
||||||
|
return formatHourStyle(dateLike);
|
||||||
|
}
|
||||||
|
if (currentTimeFormat === "seconds") {
|
||||||
|
return formatSecondStyle(dateLike);
|
||||||
|
}
|
||||||
|
return formatMinuteStyle(dateLike);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCalendarTimeFromTemplatePayload(payload) {
|
||||||
|
const currentTimeFormat = getCurrentTimeFormat();
|
||||||
|
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 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[4];
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
const currentTimeFormat = getCurrentTimeFormat();
|
||||||
|
|
||||||
|
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 createCalendarTemplates() {
|
||||||
|
const weekdayRulerLookup = buildWeekdayRulerLookup(getReferenceData()?.planets);
|
||||||
|
|
||||||
|
const getPlateFields = (event) => {
|
||||||
|
const fromRawSign = event?.raw?.planetSymbol;
|
||||||
|
const fromRawName = event?.raw?.planetName;
|
||||||
|
|
||||||
|
if (fromRawSign || fromRawName) {
|
||||||
|
return {
|
||||||
|
sign: fromRawSign || "",
|
||||||
|
name: fromRawName || ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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 init(nextConfig = {}) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...nextConfig
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotCalendarFormatting = {
|
||||||
|
...(window.TarotCalendarFormatting || {}),
|
||||||
|
init,
|
||||||
|
normalizeDateLike,
|
||||||
|
createCalendarTemplates,
|
||||||
|
forceAxisLabelFormat
|
||||||
|
};
|
||||||
|
})();
|
||||||
421
app/ui-calendar-visuals.js
Normal file
421
app/ui-calendar-visuals.js
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let config = {};
|
||||||
|
let monthStripResizeFrame = null;
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
function getCalendar() {
|
||||||
|
return config.calendar || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonthStripEl() {
|
||||||
|
return config.monthStripEl || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentGeo() {
|
||||||
|
return config.getCurrentGeo?.() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattingUi() {
|
||||||
|
return window.TarotCalendarFormatting || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCalendarDateLike(value) {
|
||||||
|
const formattingUi = getFormattingUi();
|
||||||
|
if (typeof formattingUi.normalizeDateLike === "function") {
|
||||||
|
return formattingUi.normalizeDateLike(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && typeof value.getTime === "function") {
|
||||||
|
return new Date(value.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
const currentGeo = getCurrentGeo();
|
||||||
|
if (currentGeo) {
|
||||||
|
return currentGeo;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return config.parseGeoInput?.() || null;
|
||||||
|
} 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 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()) {
|
||||||
|
const currentGeo = getCurrentGeo();
|
||||||
|
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 = config.getMoonPhaseName?.(moonIllum.phase) || "Waning Crescent";
|
||||||
|
|
||||||
|
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 getVisibleWeekDates() {
|
||||||
|
const calendar = getCalendar();
|
||||||
|
if (!calendar || typeof calendar.getDateRangeStart !== "function") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeStart = calendar.getDateRangeStart();
|
||||||
|
if (!rangeStart) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDateLike = normalizeCalendarDateLike(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() {
|
||||||
|
const monthStripEl = getMonthStripEl();
|
||||||
|
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() {
|
||||||
|
const monthStripEl = getMonthStripEl();
|
||||||
|
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 applyTimeFormatTemplates() {
|
||||||
|
const calendar = getCalendar();
|
||||||
|
const formattingUi = getFormattingUi();
|
||||||
|
if (!calendar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
calendar.setOptions({
|
||||||
|
template: formattingUi.createCalendarTemplates?.() || {}
|
||||||
|
});
|
||||||
|
calendar.render();
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
formattingUi.forceAxisLabelFormat?.();
|
||||||
|
applySunRulerGradient();
|
||||||
|
applyDynamicNowIndicatorVisual();
|
||||||
|
updateMonthStrip();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
formattingUi.forceAxisLabelFormat?.();
|
||||||
|
applySunRulerGradient();
|
||||||
|
applyDynamicNowIndicatorVisual();
|
||||||
|
updateMonthStrip();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindWindowResize() {
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
if (monthStripResizeFrame) {
|
||||||
|
cancelAnimationFrame(monthStripResizeFrame);
|
||||||
|
}
|
||||||
|
monthStripResizeFrame = requestAnimationFrame(() => {
|
||||||
|
monthStripResizeFrame = null;
|
||||||
|
updateMonthStrip();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(nextConfig = {}) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...nextConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
bindWindowResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotCalendarVisuals = {
|
||||||
|
...(window.TarotCalendarVisuals || {}),
|
||||||
|
init,
|
||||||
|
applySunRulerGradient,
|
||||||
|
applyDynamicNowIndicatorVisual,
|
||||||
|
updateMonthStrip,
|
||||||
|
applyTimeFormatTemplates
|
||||||
|
};
|
||||||
|
})();
|
||||||
1816
app/ui-calendar.js
1816
app/ui-calendar.js
File diff suppressed because it is too large
Load Diff
290
app/ui-chrome.js
Normal file
290
app/ui-chrome.js
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 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) {
|
||||||
|
const topbarDropdownEls = Array.from(document.querySelectorAll(".topbar-dropdown"));
|
||||||
|
topbarDropdownEls.forEach((dropdownEl) => {
|
||||||
|
if (exceptEl && dropdownEl === exceptEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTopbarDropdownOpen(dropdownEl, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindTopbarDropdownInteractions() {
|
||||||
|
const topbarDropdownEls = Array.from(document.querySelectorAll(".topbar-dropdown"));
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
const clickTarget = event.target;
|
||||||
|
if (clickTarget instanceof Node && topbarDropdownEls.some((dropdownEl) => dropdownEl.contains(clickTarget))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeTopbarDropdowns();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
closeTopbarDropdowns();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
initializeSidebarPopouts();
|
||||||
|
initializeDetailPopouts();
|
||||||
|
bindTopbarDropdownInteractions();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotChromeUi = {
|
||||||
|
...(window.TarotChromeUi || {}),
|
||||||
|
init,
|
||||||
|
initializeSidebarPopouts,
|
||||||
|
initializeDetailPopouts,
|
||||||
|
setTopbarDropdownOpen,
|
||||||
|
closeTopbarDropdowns,
|
||||||
|
bindTopbarDropdownInteractions
|
||||||
|
};
|
||||||
|
})();
|
||||||
538
app/ui-cube-detail.js
Normal file
538
app/ui-cube-detail.js
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
/* ui-cube-detail.js — Cube detail pane rendering */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function toDisplayText(value) {
|
||||||
|
return String(value ?? "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/\"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDetailValueMarkup(value) {
|
||||||
|
const text = toDisplayText(value);
|
||||||
|
return text ? escapeHtml(text) : '<span class="cube-missing-value">!</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMetaCard(title, bodyContent) {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "planet-meta-card";
|
||||||
|
|
||||||
|
const titleEl = document.createElement("strong");
|
||||||
|
titleEl.textContent = title;
|
||||||
|
card.appendChild(titleEl);
|
||||||
|
|
||||||
|
if (typeof bodyContent === "string") {
|
||||||
|
const bodyEl = document.createElement("p");
|
||||||
|
bodyEl.className = "planet-text";
|
||||||
|
bodyEl.textContent = bodyContent;
|
||||||
|
card.appendChild(bodyEl);
|
||||||
|
} else if (bodyContent instanceof Node) {
|
||||||
|
card.appendChild(bodyContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNavButton(label, eventName, detail) {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "kab-god-link";
|
||||||
|
button.textContent = `${label} ↗`;
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
document.dispatchEvent(new CustomEvent(eventName, { detail }));
|
||||||
|
});
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCenterDetail(context) {
|
||||||
|
const { state, elements, getCubeCenterData, getCenterLetterId, getCenterLetterSymbol, toFiniteNumber } = context;
|
||||||
|
if (!state.showPrimalPoint) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const center = getCubeCenterData();
|
||||||
|
if (!center || !elements?.detailNameEl || !elements?.detailSubEl || !elements?.detailBodyEl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerLetterId = getCenterLetterId(center);
|
||||||
|
const centerLetter = getCenterLetterSymbol(center);
|
||||||
|
const centerLetterText = centerLetterId
|
||||||
|
? `${centerLetter ? `${centerLetter} ` : ""}${toDisplayText(centerLetterId)}`
|
||||||
|
: "";
|
||||||
|
const centerElement = toDisplayText(center?.element);
|
||||||
|
|
||||||
|
elements.detailNameEl.textContent = "Primal Point";
|
||||||
|
elements.detailSubEl.textContent = [centerLetterText, centerElement].filter(Boolean).join(" · ") || "Center of the Cube";
|
||||||
|
|
||||||
|
const bodyEl = elements.detailBodyEl;
|
||||||
|
bodyEl.innerHTML = "";
|
||||||
|
|
||||||
|
const summary = document.createElement("div");
|
||||||
|
summary.className = "planet-text";
|
||||||
|
summary.innerHTML = `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Name</dt><dd>${toDetailValueMarkup(center?.name)}</dd>
|
||||||
|
<dt>Letter</dt><dd>${toDetailValueMarkup(centerLetterText)}</dd>
|
||||||
|
<dt>Element</dt><dd>${toDetailValueMarkup(center?.element)}</dd>
|
||||||
|
</dl>
|
||||||
|
`;
|
||||||
|
bodyEl.appendChild(createMetaCard("Center Details", summary));
|
||||||
|
|
||||||
|
if (Array.isArray(center?.keywords) && center.keywords.length) {
|
||||||
|
bodyEl.appendChild(createMetaCard("Keywords", center.keywords.join(", ")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (center?.description) {
|
||||||
|
bodyEl.appendChild(createMetaCard("Description", center.description));
|
||||||
|
}
|
||||||
|
|
||||||
|
const associations = center?.associations || {};
|
||||||
|
const links = document.createElement("div");
|
||||||
|
links.className = "kab-god-links";
|
||||||
|
|
||||||
|
if (centerLetterId) {
|
||||||
|
links.appendChild(createNavButton(centerLetter || "!", "nav:alphabet", {
|
||||||
|
alphabet: "hebrew",
|
||||||
|
hebrewLetterId: centerLetterId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerTrumpNo = toFiniteNumber(associations?.tarotTrumpNumber);
|
||||||
|
const centerTarotCard = toDisplayText(associations?.tarotCard);
|
||||||
|
if (centerTarotCard || centerTrumpNo != null) {
|
||||||
|
links.appendChild(createNavButton(centerTarotCard || `Trump ${centerTrumpNo}`, "nav:tarot-trump", {
|
||||||
|
cardName: centerTarotCard,
|
||||||
|
trumpNumber: centerTrumpNo
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerPathNo = toFiniteNumber(associations?.kabbalahPathNumber);
|
||||||
|
if (centerPathNo != null) {
|
||||||
|
links.appendChild(createNavButton(`Path ${centerPathNo}`, "nav:kabbalah-path", {
|
||||||
|
pathNo: centerPathNo
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (links.childElementCount) {
|
||||||
|
const linksCard = document.createElement("div");
|
||||||
|
linksCard.className = "planet-meta-card";
|
||||||
|
linksCard.innerHTML = "<strong>Correspondence Links</strong>";
|
||||||
|
linksCard.appendChild(links);
|
||||||
|
bodyEl.appendChild(linksCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderConnectorDetail(context) {
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
elements,
|
||||||
|
walls,
|
||||||
|
normalizeId,
|
||||||
|
normalizeLetterKey,
|
||||||
|
formatDirectionName,
|
||||||
|
getWallById,
|
||||||
|
getConnectorById,
|
||||||
|
getConnectorPathEntry,
|
||||||
|
getHebrewLetterSymbol,
|
||||||
|
toFiniteNumber
|
||||||
|
} = context;
|
||||||
|
|
||||||
|
const connector = getConnectorById(state.selectedConnectorId);
|
||||||
|
if (!connector || !elements?.detailNameEl || !elements?.detailSubEl || !elements?.detailBodyEl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromWallId = normalizeId(connector?.fromWallId);
|
||||||
|
const toWallId = normalizeId(connector?.toWallId);
|
||||||
|
const fromWall = getWallById(fromWallId) || walls.find((entry) => normalizeId(entry?.id) === fromWallId) || null;
|
||||||
|
const toWall = getWallById(toWallId) || walls.find((entry) => normalizeId(entry?.id) === toWallId) || null;
|
||||||
|
const connectorPath = getConnectorPathEntry(connector);
|
||||||
|
|
||||||
|
const letterId = normalizeLetterKey(connector?.hebrewLetterId);
|
||||||
|
const letterSymbol = getHebrewLetterSymbol(letterId);
|
||||||
|
const letterText = letterId
|
||||||
|
? `${letterSymbol ? `${letterSymbol} ` : ""}${toDisplayText(letterId)}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const pathNo = toFiniteNumber(connectorPath?.pathNumber);
|
||||||
|
const tarotCard = toDisplayText(connectorPath?.tarot?.card);
|
||||||
|
const tarotTrumpNumber = toFiniteNumber(connectorPath?.tarot?.trumpNumber);
|
||||||
|
const astrologyType = toDisplayText(connectorPath?.astrology?.type);
|
||||||
|
const astrologyName = toDisplayText(connectorPath?.astrology?.name);
|
||||||
|
const astrologySummary = [astrologyType, astrologyName].filter(Boolean).join(": ");
|
||||||
|
|
||||||
|
elements.detailNameEl.textContent = connector?.name || "Mother Connector";
|
||||||
|
elements.detailSubEl.textContent = ["Mother Letter", letterText].filter(Boolean).join(" · ") || "Mother Letter";
|
||||||
|
|
||||||
|
const bodyEl = elements.detailBodyEl;
|
||||||
|
bodyEl.innerHTML = "";
|
||||||
|
|
||||||
|
const summary = document.createElement("div");
|
||||||
|
summary.className = "planet-text";
|
||||||
|
summary.innerHTML = `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Letter</dt><dd>${toDetailValueMarkup(letterText)}</dd>
|
||||||
|
<dt>From</dt><dd>${toDetailValueMarkup(fromWall?.name || formatDirectionName(fromWallId))}</dd>
|
||||||
|
<dt>To</dt><dd>${toDetailValueMarkup(toWall?.name || formatDirectionName(toWallId))}</dd>
|
||||||
|
<dt>Tarot</dt><dd>${toDetailValueMarkup(tarotCard || (tarotTrumpNumber != null ? `Trump ${tarotTrumpNumber}` : ""))}</dd>
|
||||||
|
</dl>
|
||||||
|
`;
|
||||||
|
bodyEl.appendChild(createMetaCard("Connector Details", summary));
|
||||||
|
|
||||||
|
if (astrologySummary) {
|
||||||
|
bodyEl.appendChild(createMetaCard("Astrology", astrologySummary));
|
||||||
|
}
|
||||||
|
|
||||||
|
const links = document.createElement("div");
|
||||||
|
links.className = "kab-god-links";
|
||||||
|
|
||||||
|
if (letterId) {
|
||||||
|
links.appendChild(createNavButton(letterSymbol || "!", "nav:alphabet", {
|
||||||
|
alphabet: "hebrew",
|
||||||
|
hebrewLetterId: letterId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathNo != null) {
|
||||||
|
links.appendChild(createNavButton(`Path ${pathNo}`, "nav:kabbalah-path", { pathNo }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tarotCard || tarotTrumpNumber != null) {
|
||||||
|
links.appendChild(createNavButton(tarotCard || `Trump ${tarotTrumpNumber}`, "nav:tarot-trump", {
|
||||||
|
cardName: tarotCard,
|
||||||
|
trumpNumber: tarotTrumpNumber
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (links.childElementCount) {
|
||||||
|
const linksCard = document.createElement("div");
|
||||||
|
linksCard.className = "planet-meta-card";
|
||||||
|
linksCard.innerHTML = "<strong>Correspondence Links</strong>";
|
||||||
|
linksCard.appendChild(links);
|
||||||
|
bodyEl.appendChild(linksCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEdgeCard(context, wall, detailBodyEl, wallEdgeDirections) {
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
normalizeId,
|
||||||
|
normalizeEdgeId,
|
||||||
|
formatDirectionName,
|
||||||
|
formatEdgeName,
|
||||||
|
getEdgeById,
|
||||||
|
getEdgesForWall,
|
||||||
|
getEdges,
|
||||||
|
getEdgeWalls,
|
||||||
|
getEdgeLetterId,
|
||||||
|
getEdgeLetter,
|
||||||
|
getEdgePathEntry,
|
||||||
|
getEdgeAstrologySymbol,
|
||||||
|
toFiniteNumber
|
||||||
|
} = context;
|
||||||
|
|
||||||
|
const wallId = normalizeId(wall?.id);
|
||||||
|
const selectedEdge = getEdgeById(state.selectedEdgeId)
|
||||||
|
|| getEdgesForWall(wallId)[0]
|
||||||
|
|| getEdges()[0]
|
||||||
|
|| null;
|
||||||
|
if (!selectedEdge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.selectedEdgeId = normalizeEdgeId(selectedEdge.id);
|
||||||
|
|
||||||
|
const edgeDirection = wallEdgeDirections.get(normalizeEdgeId(selectedEdge.id));
|
||||||
|
const edgeName = edgeDirection
|
||||||
|
? formatDirectionName(edgeDirection)
|
||||||
|
: (toDisplayText(selectedEdge.name) || formatEdgeName(selectedEdge.id));
|
||||||
|
const edgeWalls = getEdgeWalls(selectedEdge)
|
||||||
|
.map((entry) => entry.charAt(0).toUpperCase() + entry.slice(1))
|
||||||
|
.join(" · ");
|
||||||
|
|
||||||
|
const edgeLetterId = getEdgeLetterId(selectedEdge);
|
||||||
|
const edgeLetter = getEdgeLetter(selectedEdge);
|
||||||
|
const edgePath = getEdgePathEntry(selectedEdge);
|
||||||
|
const astrologyType = toDisplayText(edgePath?.astrology?.type);
|
||||||
|
const astrologyName = toDisplayText(edgePath?.astrology?.name);
|
||||||
|
const astrologySymbol = getEdgeAstrologySymbol(selectedEdge);
|
||||||
|
const astrologyText = astrologySymbol && astrologyName
|
||||||
|
? `${astrologySymbol} ${astrologyName}`
|
||||||
|
: astrologySymbol || astrologyName;
|
||||||
|
|
||||||
|
const pathNo = toFiniteNumber(edgePath?.pathNumber);
|
||||||
|
const tarotCard = toDisplayText(edgePath?.tarot?.card);
|
||||||
|
const tarotTrumpNumber = toFiniteNumber(edgePath?.tarot?.trumpNumber);
|
||||||
|
|
||||||
|
const edgeCard = document.createElement("div");
|
||||||
|
edgeCard.className = "planet-meta-card";
|
||||||
|
|
||||||
|
const title = document.createElement("strong");
|
||||||
|
title.textContent = `Edge · ${edgeName}`;
|
||||||
|
edgeCard.appendChild(title);
|
||||||
|
|
||||||
|
const dlWrap = document.createElement("div");
|
||||||
|
dlWrap.className = "planet-text";
|
||||||
|
dlWrap.innerHTML = `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Direction</dt><dd>${toDetailValueMarkup(edgeName)}</dd>
|
||||||
|
<dt>Edge</dt><dd>${toDetailValueMarkup(edgeWalls)}</dd>
|
||||||
|
<dt>Letter</dt><dd>${toDetailValueMarkup(edgeLetter)}</dd>
|
||||||
|
<dt>Astrology</dt><dd>${toDetailValueMarkup(astrologyText)}</dd>
|
||||||
|
<dt>Tarot</dt><dd>${toDetailValueMarkup(tarotCard)}</dd>
|
||||||
|
</dl>
|
||||||
|
`;
|
||||||
|
edgeCard.appendChild(dlWrap);
|
||||||
|
|
||||||
|
if (Array.isArray(selectedEdge.keywords) && selectedEdge.keywords.length) {
|
||||||
|
const keywords = document.createElement("p");
|
||||||
|
keywords.className = "planet-text";
|
||||||
|
keywords.textContent = selectedEdge.keywords.join(", ");
|
||||||
|
edgeCard.appendChild(keywords);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedEdge.description) {
|
||||||
|
const description = document.createElement("p");
|
||||||
|
description.className = "planet-text";
|
||||||
|
description.textContent = selectedEdge.description;
|
||||||
|
edgeCard.appendChild(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
const links = document.createElement("div");
|
||||||
|
links.className = "kab-god-links";
|
||||||
|
|
||||||
|
if (edgeLetterId) {
|
||||||
|
links.appendChild(createNavButton(edgeLetter || "!", "nav:alphabet", {
|
||||||
|
alphabet: "hebrew",
|
||||||
|
hebrewLetterId: edgeLetterId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (astrologyType === "zodiac" && astrologyName) {
|
||||||
|
links.appendChild(createNavButton(astrologyName, "nav:zodiac", {
|
||||||
|
signId: normalizeId(astrologyName)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tarotCard) {
|
||||||
|
links.appendChild(createNavButton(tarotCard, "nav:tarot-trump", {
|
||||||
|
cardName: tarotCard,
|
||||||
|
trumpNumber: tarotTrumpNumber
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathNo != null) {
|
||||||
|
links.appendChild(createNavButton(`Path ${pathNo}`, "nav:kabbalah-path", { pathNo }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (links.childElementCount) {
|
||||||
|
edgeCard.appendChild(links);
|
||||||
|
}
|
||||||
|
|
||||||
|
detailBodyEl.appendChild(edgeCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWallDetail(context) {
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
elements,
|
||||||
|
walls,
|
||||||
|
normalizeId,
|
||||||
|
normalizeEdgeId,
|
||||||
|
formatDirectionName,
|
||||||
|
formatEdgeName,
|
||||||
|
getWallById,
|
||||||
|
getEdgesForWall,
|
||||||
|
getWallEdgeDirections,
|
||||||
|
getWallFaceLetterId,
|
||||||
|
getWallFaceLetter,
|
||||||
|
getHebrewLetterName,
|
||||||
|
getEdgeLetter,
|
||||||
|
localDirectionOrder,
|
||||||
|
localDirectionRank,
|
||||||
|
onSelectWall,
|
||||||
|
onSelectEdge
|
||||||
|
} = context;
|
||||||
|
|
||||||
|
const wall = getWallById(state.selectedWallId) || walls[0] || null;
|
||||||
|
if (!wall || !elements?.detailNameEl || !elements?.detailSubEl || !elements?.detailBodyEl) {
|
||||||
|
if (elements?.detailNameEl) {
|
||||||
|
elements.detailNameEl.textContent = "Cube data unavailable";
|
||||||
|
}
|
||||||
|
if (elements?.detailSubEl) {
|
||||||
|
elements.detailSubEl.textContent = "Could not load cube dataset.";
|
||||||
|
}
|
||||||
|
if (elements?.detailBodyEl) {
|
||||||
|
elements.detailBodyEl.innerHTML = "";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.selectedWallId = normalizeId(wall.id);
|
||||||
|
|
||||||
|
const wallPlanet = toDisplayText(wall?.planet) || "!";
|
||||||
|
const wallElement = toDisplayText(wall?.element) || "!";
|
||||||
|
const wallFaceLetterId = getWallFaceLetterId(wall);
|
||||||
|
const wallFaceLetter = getWallFaceLetter(wall);
|
||||||
|
const wallFaceLetterText = wallFaceLetterId
|
||||||
|
? `${wallFaceLetter ? `${wallFaceLetter} ` : ""}${toDisplayText(wallFaceLetterId)}`
|
||||||
|
: "";
|
||||||
|
elements.detailNameEl.textContent = `${wall.name} Wall`;
|
||||||
|
elements.detailSubEl.textContent = `${wallElement} · ${wallPlanet}`;
|
||||||
|
|
||||||
|
const bodyEl = elements.detailBodyEl;
|
||||||
|
bodyEl.innerHTML = "";
|
||||||
|
|
||||||
|
const summary = document.createElement("div");
|
||||||
|
summary.className = "planet-text";
|
||||||
|
summary.innerHTML = `
|
||||||
|
<dl class="alpha-dl">
|
||||||
|
<dt>Opposite</dt><dd>${toDetailValueMarkup(wall.opposite)}</dd>
|
||||||
|
<dt>Face Letter</dt><dd>${toDetailValueMarkup(wallFaceLetterText)}</dd>
|
||||||
|
<dt>Element</dt><dd>${toDetailValueMarkup(wall.element)}</dd>
|
||||||
|
<dt>Planet</dt><dd>${toDetailValueMarkup(wall.planet)}</dd>
|
||||||
|
<dt>Archangel</dt><dd>${toDetailValueMarkup(wall.archangel)}</dd>
|
||||||
|
</dl>
|
||||||
|
`;
|
||||||
|
bodyEl.appendChild(createMetaCard("Wall Details", summary));
|
||||||
|
|
||||||
|
if (Array.isArray(wall.keywords) && wall.keywords.length) {
|
||||||
|
bodyEl.appendChild(createMetaCard("Keywords", wall.keywords.join(", ")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wall.description) {
|
||||||
|
bodyEl.appendChild(createMetaCard("Description", wall.description));
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallLinksCard = document.createElement("div");
|
||||||
|
wallLinksCard.className = "planet-meta-card";
|
||||||
|
wallLinksCard.innerHTML = "<strong>Correspondence Links</strong>";
|
||||||
|
const wallLinks = document.createElement("div");
|
||||||
|
wallLinks.className = "kab-god-links";
|
||||||
|
|
||||||
|
if (wallFaceLetterId) {
|
||||||
|
const wallFaceLetterName = getHebrewLetterName(wallFaceLetterId) || toDisplayText(wallFaceLetterId);
|
||||||
|
const faceLetterText = [wallFaceLetter, wallFaceLetterName].filter(Boolean).join(" ");
|
||||||
|
const faceLetterLabel = faceLetterText
|
||||||
|
? `Face ${faceLetterText}`
|
||||||
|
: "Face !";
|
||||||
|
wallLinks.appendChild(createNavButton(faceLetterLabel, "nav:alphabet", {
|
||||||
|
alphabet: "hebrew",
|
||||||
|
hebrewLetterId: wallFaceLetterId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallAssociations = wall.associations || {};
|
||||||
|
if (wallAssociations.planetId) {
|
||||||
|
wallLinks.appendChild(createNavButton(toDisplayText(wall.planet) || "!", "nav:planet", {
|
||||||
|
planetId: wallAssociations.planetId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wallAssociations.godName) {
|
||||||
|
wallLinks.appendChild(createNavButton(wallAssociations.godName, "nav:gods", {
|
||||||
|
godName: wallAssociations.godName
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wall.oppositeWallId) {
|
||||||
|
const oppositeWall = getWallById(wall.oppositeWallId);
|
||||||
|
const internal = document.createElement("button");
|
||||||
|
internal.type = "button";
|
||||||
|
internal.className = "kab-god-link";
|
||||||
|
internal.textContent = `Opposite: ${oppositeWall?.name || wall.oppositeWallId}`;
|
||||||
|
internal.addEventListener("click", () => {
|
||||||
|
onSelectWall(wall.oppositeWallId);
|
||||||
|
});
|
||||||
|
wallLinks.appendChild(internal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wallLinks.childElementCount) {
|
||||||
|
wallLinksCard.appendChild(wallLinks);
|
||||||
|
bodyEl.appendChild(wallLinksCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const edgesCard = document.createElement("div");
|
||||||
|
edgesCard.className = "planet-meta-card";
|
||||||
|
edgesCard.innerHTML = "<strong>Wall Edges</strong>";
|
||||||
|
|
||||||
|
const chips = document.createElement("div");
|
||||||
|
chips.className = "kab-chips";
|
||||||
|
|
||||||
|
const wallEdgeDirections = getWallEdgeDirections(wall);
|
||||||
|
const wallEdges = getEdgesForWall(wall)
|
||||||
|
.slice()
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftDirection = wallEdgeDirections.get(normalizeEdgeId(left?.id));
|
||||||
|
const rightDirection = wallEdgeDirections.get(normalizeEdgeId(right?.id));
|
||||||
|
const leftRank = localDirectionRank[leftDirection] ?? localDirectionOrder.length;
|
||||||
|
const rightRank = localDirectionRank[rightDirection] ?? localDirectionOrder.length;
|
||||||
|
if (leftRank !== rightRank) {
|
||||||
|
return leftRank - rightRank;
|
||||||
|
}
|
||||||
|
return normalizeEdgeId(left?.id).localeCompare(normalizeEdgeId(right?.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
wallEdges.forEach((edge) => {
|
||||||
|
const id = normalizeEdgeId(edge.id);
|
||||||
|
const chipLetter = getEdgeLetter(edge);
|
||||||
|
const chipIsMissing = !chipLetter;
|
||||||
|
const direction = wallEdgeDirections.get(id);
|
||||||
|
const directionLabel = direction
|
||||||
|
? formatDirectionName(direction)
|
||||||
|
: (toDisplayText(edge.name) || formatEdgeName(edge.id));
|
||||||
|
const chip = document.createElement("span");
|
||||||
|
chip.className = `kab-chip${id === normalizeEdgeId(state.selectedEdgeId) ? " is-active" : ""}${chipIsMissing ? " is-missing" : ""}`;
|
||||||
|
chip.setAttribute("role", "button");
|
||||||
|
chip.setAttribute("tabindex", "0");
|
||||||
|
chip.textContent = `${directionLabel} · ${chipLetter || "!"}`;
|
||||||
|
|
||||||
|
const selectEdge = () => {
|
||||||
|
onSelectEdge(id, wall.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
chip.addEventListener("click", selectEdge);
|
||||||
|
chip.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
selectEdge();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chips.appendChild(chip);
|
||||||
|
});
|
||||||
|
|
||||||
|
edgesCard.appendChild(chips);
|
||||||
|
bodyEl.appendChild(edgesCard);
|
||||||
|
|
||||||
|
renderEdgeCard(context, wall, bodyEl, wallEdgeDirections);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDetail(context) {
|
||||||
|
if (context.state.selectedNodeType === "connector" && renderConnectorDetail(context)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.state.selectedNodeType === "center" && renderCenterDetail(context)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWallDetail(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.CubeDetailUi = {
|
||||||
|
renderDetail
|
||||||
|
};
|
||||||
|
})();
|
||||||
615
app/ui-cube.js
615
app/ui-cube.js
@@ -119,6 +119,7 @@
|
|||||||
above: { x: -90, y: 0 },
|
above: { x: -90, y: 0 },
|
||||||
below: { x: 90, y: 0 }
|
below: { x: 90, y: 0 }
|
||||||
};
|
};
|
||||||
|
const cubeDetailUi = window.CubeDetailUi || {};
|
||||||
|
|
||||||
function getElements() {
|
function getElements() {
|
||||||
return {
|
return {
|
||||||
@@ -655,6 +656,22 @@
|
|||||||
return window.TarotCardImages.resolveTarotCardImage(name) || null;
|
return window.TarotCardImages.resolveTarotCardImage(name) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openTarotCardLightbox(cardName, fallbackSrc = "", fallbackLabel = "") {
|
||||||
|
const openLightbox = window.TarotUiLightbox?.open;
|
||||||
|
if (typeof openLightbox !== "function") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const src = toDisplayText(fallbackSrc) || resolveCardImageUrl(cardName);
|
||||||
|
if (!src) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = toDisplayText(cardName) || toDisplayText(fallbackLabel) || "Tarot card";
|
||||||
|
openLightbox(src, label);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function applyPlacement(placement) {
|
function applyPlacement(placement) {
|
||||||
const fallbackWallId = normalizeId(getWalls()[0]?.id);
|
const fallbackWallId = normalizeId(getWalls()[0]?.id);
|
||||||
const nextWallId = normalizeId(placement?.wallId || placement?.wall?.id || state.selectedWallId || fallbackWallId);
|
const nextWallId = normalizeId(placement?.wallId || placement?.wall?.id || state.selectedWallId || fallbackWallId);
|
||||||
@@ -678,55 +695,10 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMetaCard(title, bodyContent) {
|
|
||||||
const card = document.createElement("div");
|
|
||||||
card.className = "planet-meta-card";
|
|
||||||
|
|
||||||
const titleEl = document.createElement("strong");
|
|
||||||
titleEl.textContent = title;
|
|
||||||
card.appendChild(titleEl);
|
|
||||||
|
|
||||||
if (typeof bodyContent === "string") {
|
|
||||||
const bodyEl = document.createElement("p");
|
|
||||||
bodyEl.className = "planet-text";
|
|
||||||
bodyEl.textContent = bodyContent;
|
|
||||||
card.appendChild(bodyEl);
|
|
||||||
} else if (bodyContent instanceof Node) {
|
|
||||||
card.appendChild(bodyContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNavButton(label, eventName, detail) {
|
|
||||||
const button = document.createElement("button");
|
|
||||||
button.type = "button";
|
|
||||||
button.className = "kab-god-link";
|
|
||||||
button.textContent = `${label} ↗`;
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
document.dispatchEvent(new CustomEvent(eventName, { detail }));
|
|
||||||
});
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toDisplayText(value) {
|
function toDisplayText(value) {
|
||||||
return String(value ?? "").trim();
|
return String(value ?? "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(value) {
|
|
||||||
return String(value)
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/\"/g, """)
|
|
||||||
.replace(/'/g, "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
function toDetailValueMarkup(value) {
|
|
||||||
const text = toDisplayText(value);
|
|
||||||
return text ? escapeHtml(text) : '<span class="cube-missing-value">!</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderFaceSvg(containerEl, walls) {
|
function renderFaceSvg(containerEl, walls) {
|
||||||
if (!containerEl) {
|
if (!containerEl) {
|
||||||
return;
|
return;
|
||||||
@@ -819,7 +791,9 @@
|
|||||||
defs.appendChild(clipPath);
|
defs.appendChild(clipPath);
|
||||||
|
|
||||||
const cardW = 40, cardH = 60;
|
const cardW = 40, cardH = 60;
|
||||||
|
const wallTarotCard = getWallTarotCard(wall);
|
||||||
const cardImg = document.createElementNS(svgNS, "image");
|
const cardImg = document.createElementNS(svgNS, "image");
|
||||||
|
cardImg.setAttribute("class", "cube-tarot-image cube-face-card");
|
||||||
cardImg.setAttribute("href", cardUrl);
|
cardImg.setAttribute("href", cardUrl);
|
||||||
cardImg.setAttribute("x", String((faceGlyphAnchor.x - cardW / 2).toFixed(2)));
|
cardImg.setAttribute("x", String((faceGlyphAnchor.x - cardW / 2).toFixed(2)));
|
||||||
cardImg.setAttribute("y", String((faceGlyphAnchor.y - cardH / 2).toFixed(2)));
|
cardImg.setAttribute("y", String((faceGlyphAnchor.y - cardH / 2).toFixed(2)));
|
||||||
@@ -828,13 +802,19 @@
|
|||||||
cardImg.setAttribute("clip-path", `url(#${clipId})`);
|
cardImg.setAttribute("clip-path", `url(#${clipId})`);
|
||||||
cardImg.setAttribute("role", "button");
|
cardImg.setAttribute("role", "button");
|
||||||
cardImg.setAttribute("tabindex", "0");
|
cardImg.setAttribute("tabindex", "0");
|
||||||
cardImg.setAttribute("aria-label", `Cube wall ${wall?.name || wallId}`);
|
cardImg.setAttribute("aria-label", `Open ${wallTarotCard || (wall?.name || wallId)} card image`);
|
||||||
cardImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
cardImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||||
cardImg.addEventListener("click", selectWall);
|
cardImg.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
selectWall();
|
||||||
|
openTarotCardLightbox(wallTarotCard, cardUrl, `${wall?.name || wallId} wall tarot card`);
|
||||||
|
});
|
||||||
cardImg.addEventListener("keydown", (event) => {
|
cardImg.addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
selectWall();
|
selectWall();
|
||||||
|
openTarotCardLightbox(wallTarotCard, cardUrl, `${wall?.name || wallId} wall tarot card`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
svg.appendChild(cardImg);
|
svg.appendChild(cardImg);
|
||||||
@@ -922,16 +902,40 @@
|
|||||||
const labelX = ((from.x + to.x) / 2) + (perpX * shift);
|
const labelX = ((from.x + to.x) / 2) + (perpX * shift);
|
||||||
const labelY = ((from.y + to.y) / 2) + (perpY * shift);
|
const labelY = ((from.y + to.y) / 2) + (perpY * shift);
|
||||||
|
|
||||||
|
const selectConnector = () => {
|
||||||
|
state.selectedNodeType = "connector";
|
||||||
|
state.selectedConnectorId = connectorId;
|
||||||
|
render(getElements());
|
||||||
|
};
|
||||||
|
|
||||||
if (state.markerDisplayMode === "tarot" && connectorCardUrl) {
|
if (state.markerDisplayMode === "tarot" && connectorCardUrl) {
|
||||||
const cardW = 18;
|
const cardW = 18;
|
||||||
const cardH = 27;
|
const cardH = 27;
|
||||||
|
const connectorTarotCard = getConnectorTarotCard(connector);
|
||||||
const connectorImg = document.createElementNS(svgNS, "image");
|
const connectorImg = document.createElementNS(svgNS, "image");
|
||||||
|
connectorImg.setAttribute("class", "cube-tarot-image cube-connector-card");
|
||||||
connectorImg.setAttribute("href", connectorCardUrl);
|
connectorImg.setAttribute("href", connectorCardUrl);
|
||||||
connectorImg.setAttribute("x", String((labelX - cardW / 2).toFixed(2)));
|
connectorImg.setAttribute("x", String((labelX - cardW / 2).toFixed(2)));
|
||||||
connectorImg.setAttribute("y", String((labelY - cardH / 2).toFixed(2)));
|
connectorImg.setAttribute("y", String((labelY - cardH / 2).toFixed(2)));
|
||||||
connectorImg.setAttribute("width", String(cardW));
|
connectorImg.setAttribute("width", String(cardW));
|
||||||
connectorImg.setAttribute("height", String(cardH));
|
connectorImg.setAttribute("height", String(cardH));
|
||||||
|
connectorImg.setAttribute("role", "button");
|
||||||
|
connectorImg.setAttribute("tabindex", "0");
|
||||||
|
connectorImg.setAttribute("aria-label", `Open ${connectorTarotCard || connector?.name || "connector"} card image`);
|
||||||
connectorImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
connectorImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||||
|
connectorImg.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
selectConnector();
|
||||||
|
openTarotCardLightbox(connectorTarotCard, connectorCardUrl, connector?.name || "Mother connector");
|
||||||
|
});
|
||||||
|
connectorImg.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
selectConnector();
|
||||||
|
openTarotCardLightbox(connectorTarotCard, connectorCardUrl, connector?.name || "Mother connector");
|
||||||
|
}
|
||||||
|
});
|
||||||
group.appendChild(connectorImg);
|
group.appendChild(connectorImg);
|
||||||
} else {
|
} else {
|
||||||
const connectorText = document.createElementNS(svgNS, "text");
|
const connectorText = document.createElementNS(svgNS, "text");
|
||||||
@@ -948,12 +952,6 @@
|
|||||||
group.appendChild(connectorText);
|
group.appendChild(connectorText);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectConnector = () => {
|
|
||||||
state.selectedNodeType = "connector";
|
|
||||||
state.selectedConnectorId = connectorId;
|
|
||||||
render(getElements());
|
|
||||||
};
|
|
||||||
|
|
||||||
group.addEventListener("click", selectConnector);
|
group.addEventListener("click", selectConnector);
|
||||||
group.addEventListener("keydown", (event) => {
|
group.addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
@@ -1049,14 +1047,31 @@
|
|||||||
if (edgeCardUrl) {
|
if (edgeCardUrl) {
|
||||||
const cardW = edgeIsActive ? 28 : 20;
|
const cardW = edgeIsActive ? 28 : 20;
|
||||||
const cardH = edgeIsActive ? 42 : 30;
|
const cardH = edgeIsActive ? 42 : 30;
|
||||||
|
const edgeTarotCard = getEdgeTarotCard(edge);
|
||||||
const cardImg = document.createElementNS(svgNS, "image");
|
const cardImg = document.createElementNS(svgNS, "image");
|
||||||
cardImg.setAttribute("class", `cube-direction-card${edgeIsActive ? " is-active" : ""}`);
|
cardImg.setAttribute("class", `cube-tarot-image cube-direction-card${edgeIsActive ? " is-active" : ""}`);
|
||||||
cardImg.setAttribute("href", edgeCardUrl);
|
cardImg.setAttribute("href", edgeCardUrl);
|
||||||
cardImg.setAttribute("x", String((labelX - cardW / 2).toFixed(2)));
|
cardImg.setAttribute("x", String((labelX - cardW / 2).toFixed(2)));
|
||||||
cardImg.setAttribute("y", String((labelY - cardH / 2).toFixed(2)));
|
cardImg.setAttribute("y", String((labelY - cardH / 2).toFixed(2)));
|
||||||
cardImg.setAttribute("width", String(cardW));
|
cardImg.setAttribute("width", String(cardW));
|
||||||
cardImg.setAttribute("height", String(cardH));
|
cardImg.setAttribute("height", String(cardH));
|
||||||
|
cardImg.setAttribute("role", "button");
|
||||||
|
cardImg.setAttribute("tabindex", "0");
|
||||||
|
cardImg.setAttribute("aria-label", `Open ${edgeTarotCard || edge?.name || "edge"} card image`);
|
||||||
cardImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
cardImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||||
|
cardImg.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
selectEdge();
|
||||||
|
openTarotCardLightbox(edgeTarotCard, edgeCardUrl, edge?.name || "Cube edge");
|
||||||
|
});
|
||||||
|
cardImg.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
selectEdge();
|
||||||
|
openTarotCardLightbox(edgeTarotCard, edgeCardUrl, edge?.name || "Cube edge");
|
||||||
|
}
|
||||||
|
});
|
||||||
marker.appendChild(cardImg);
|
marker.appendChild(cardImg);
|
||||||
} else {
|
} else {
|
||||||
const markerText = document.createElementNS(svgNS, "text");
|
const markerText = document.createElementNS(svgNS, "text");
|
||||||
@@ -1117,13 +1132,35 @@
|
|||||||
if (state.markerDisplayMode === "tarot" && centerCardUrl) {
|
if (state.markerDisplayMode === "tarot" && centerCardUrl) {
|
||||||
const cardW = 24;
|
const cardW = 24;
|
||||||
const cardH = 36;
|
const cardH = 36;
|
||||||
|
const centerTarotCard = getCenterTarotCard(center);
|
||||||
const centerImg = document.createElementNS(svgNS, "image");
|
const centerImg = document.createElementNS(svgNS, "image");
|
||||||
|
centerImg.setAttribute("class", "cube-tarot-image cube-center-card");
|
||||||
centerImg.setAttribute("href", centerCardUrl);
|
centerImg.setAttribute("href", centerCardUrl);
|
||||||
centerImg.setAttribute("x", String((CUBE_VIEW_CENTER.x - cardW / 2).toFixed(2)));
|
centerImg.setAttribute("x", String((CUBE_VIEW_CENTER.x - cardW / 2).toFixed(2)));
|
||||||
centerImg.setAttribute("y", String((CUBE_VIEW_CENTER.y - cardH / 2).toFixed(2)));
|
centerImg.setAttribute("y", String((CUBE_VIEW_CENTER.y - cardH / 2).toFixed(2)));
|
||||||
centerImg.setAttribute("width", String(cardW));
|
centerImg.setAttribute("width", String(cardW));
|
||||||
centerImg.setAttribute("height", String(cardH));
|
centerImg.setAttribute("height", String(cardH));
|
||||||
|
centerImg.setAttribute("role", "button");
|
||||||
|
centerImg.setAttribute("tabindex", "0");
|
||||||
|
centerImg.setAttribute("aria-label", `Open ${centerTarotCard || "Primal Point"} card image`);
|
||||||
centerImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
centerImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||||
|
centerImg.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
state.selectedNodeType = "center";
|
||||||
|
state.selectedConnectorId = null;
|
||||||
|
render(getElements());
|
||||||
|
openTarotCardLightbox(centerTarotCard, centerCardUrl, "Primal Point");
|
||||||
|
});
|
||||||
|
centerImg.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
state.selectedNodeType = "center";
|
||||||
|
state.selectedConnectorId = null;
|
||||||
|
render(getElements());
|
||||||
|
openTarotCardLightbox(centerTarotCard, centerCardUrl, "Primal Point");
|
||||||
|
}
|
||||||
|
});
|
||||||
centerMarker.appendChild(centerImg);
|
centerMarker.appendChild(centerImg);
|
||||||
} else {
|
} else {
|
||||||
const centerText = document.createElementNS(svgNS, "text");
|
const centerText = document.createElementNS(svgNS, "text");
|
||||||
@@ -1172,287 +1209,43 @@
|
|||||||
containerEl.replaceChildren(svg);
|
containerEl.replaceChildren(svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCenterDetail(elements) {
|
function selectEdgeById(edgeId, preferredWallId = "") {
|
||||||
if (!state.showPrimalPoint) {
|
const edge = getEdgeById(edgeId);
|
||||||
|
if (!edge) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const center = getCubeCenterData();
|
const currentWallId = normalizeId(state.selectedWallId);
|
||||||
if (!center || !elements?.detailNameEl || !elements?.detailSubEl || !elements?.detailBodyEl) {
|
const preferredId = normalizeId(preferredWallId);
|
||||||
return false;
|
const edgeWalls = getEdgeWalls(edge);
|
||||||
}
|
const nextWallId = preferredId && edgeWalls.includes(preferredId)
|
||||||
|
? preferredId
|
||||||
const centerLetterId = getCenterLetterId(center);
|
: (edgeWalls.includes(currentWallId) ? currentWallId : (edgeWalls[0] || currentWallId));
|
||||||
const centerLetter = getCenterLetterSymbol(center);
|
|
||||||
const centerLetterText = centerLetterId
|
state.selectedEdgeId = normalizeEdgeId(edge.id);
|
||||||
? `${centerLetter ? `${centerLetter} ` : ""}${toDisplayText(centerLetterId)}`
|
state.selectedNodeType = "wall";
|
||||||
: "";
|
state.selectedConnectorId = null;
|
||||||
const centerElement = toDisplayText(center?.element);
|
|
||||||
|
if (nextWallId) {
|
||||||
elements.detailNameEl.textContent = "Primal Point";
|
if (nextWallId !== currentWallId) {
|
||||||
elements.detailSubEl.textContent = [centerLetterText, centerElement].filter(Boolean).join(" · ") || "Center of the Cube";
|
state.selectedWallId = nextWallId;
|
||||||
|
snapRotationToWall(nextWallId);
|
||||||
const bodyEl = elements.detailBodyEl;
|
} else if (!state.selectedWallId) {
|
||||||
bodyEl.innerHTML = "";
|
state.selectedWallId = nextWallId;
|
||||||
|
}
|
||||||
const summary = document.createElement("div");
|
|
||||||
summary.className = "planet-text";
|
|
||||||
summary.innerHTML = `
|
|
||||||
<dl class="alpha-dl">
|
|
||||||
<dt>Name</dt><dd>${toDetailValueMarkup(center?.name)}</dd>
|
|
||||||
<dt>Letter</dt><dd>${toDetailValueMarkup(centerLetterText)}</dd>
|
|
||||||
<dt>Element</dt><dd>${toDetailValueMarkup(center?.element)}</dd>
|
|
||||||
</dl>
|
|
||||||
`;
|
|
||||||
bodyEl.appendChild(createMetaCard("Center Details", summary));
|
|
||||||
|
|
||||||
if (Array.isArray(center?.keywords) && center.keywords.length) {
|
|
||||||
bodyEl.appendChild(createMetaCard("Keywords", center.keywords.join(", ")));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (center?.description) {
|
|
||||||
bodyEl.appendChild(createMetaCard("Description", center.description));
|
|
||||||
}
|
|
||||||
|
|
||||||
const associations = center?.associations || {};
|
|
||||||
const links = document.createElement("div");
|
|
||||||
links.className = "kab-god-links";
|
|
||||||
|
|
||||||
if (centerLetterId) {
|
|
||||||
links.appendChild(createNavButton(centerLetter || "!", "nav:alphabet", {
|
|
||||||
alphabet: "hebrew",
|
|
||||||
hebrewLetterId: centerLetterId
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const centerTrumpNo = toFiniteNumber(associations?.tarotTrumpNumber);
|
|
||||||
const centerTarotCard = toDisplayText(associations?.tarotCard);
|
|
||||||
if (centerTarotCard || centerTrumpNo != null) {
|
|
||||||
links.appendChild(createNavButton(centerTarotCard || `Trump ${centerTrumpNo}`, "nav:tarot-trump", {
|
|
||||||
cardName: centerTarotCard,
|
|
||||||
trumpNumber: centerTrumpNo
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const centerPathNo = toFiniteNumber(associations?.kabbalahPathNumber);
|
|
||||||
if (centerPathNo != null) {
|
|
||||||
links.appendChild(createNavButton(`Path ${centerPathNo}`, "nav:kabbalah-path", {
|
|
||||||
pathNo: centerPathNo
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (links.childElementCount) {
|
|
||||||
const linksCard = document.createElement("div");
|
|
||||||
linksCard.className = "planet-meta-card";
|
|
||||||
linksCard.innerHTML = "<strong>Correspondence Links</strong>";
|
|
||||||
linksCard.appendChild(links);
|
|
||||||
bodyEl.appendChild(linksCard);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render(getElements());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderConnectorDetail(elements, walls) {
|
|
||||||
const connector = getConnectorById(state.selectedConnectorId);
|
|
||||||
if (!connector || !elements?.detailNameEl || !elements?.detailSubEl || !elements?.detailBodyEl) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromWallId = normalizeId(connector?.fromWallId);
|
|
||||||
const toWallId = normalizeId(connector?.toWallId);
|
|
||||||
const fromWall = getWallById(fromWallId) || walls.find((entry) => normalizeId(entry?.id) === fromWallId) || null;
|
|
||||||
const toWall = getWallById(toWallId) || walls.find((entry) => normalizeId(entry?.id) === toWallId) || null;
|
|
||||||
const connectorPath = getConnectorPathEntry(connector);
|
|
||||||
|
|
||||||
const letterId = normalizeLetterKey(connector?.hebrewLetterId);
|
|
||||||
const letterSymbol = getHebrewLetterSymbol(letterId);
|
|
||||||
const letterText = letterId
|
|
||||||
? `${letterSymbol ? `${letterSymbol} ` : ""}${toDisplayText(letterId)}`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const pathNo = toFiniteNumber(connectorPath?.pathNumber);
|
|
||||||
const tarotCard = toDisplayText(connectorPath?.tarot?.card);
|
|
||||||
const tarotTrumpNumber = toFiniteNumber(connectorPath?.tarot?.trumpNumber);
|
|
||||||
const astrologyType = toDisplayText(connectorPath?.astrology?.type);
|
|
||||||
const astrologyName = toDisplayText(connectorPath?.astrology?.name);
|
|
||||||
const astrologySummary = [astrologyType, astrologyName].filter(Boolean).join(": ");
|
|
||||||
|
|
||||||
elements.detailNameEl.textContent = connector?.name || "Mother Connector";
|
|
||||||
elements.detailSubEl.textContent = ["Mother Letter", letterText].filter(Boolean).join(" · ") || "Mother Letter";
|
|
||||||
|
|
||||||
const bodyEl = elements.detailBodyEl;
|
|
||||||
bodyEl.innerHTML = "";
|
|
||||||
|
|
||||||
const summary = document.createElement("div");
|
|
||||||
summary.className = "planet-text";
|
|
||||||
summary.innerHTML = `
|
|
||||||
<dl class="alpha-dl">
|
|
||||||
<dt>Letter</dt><dd>${toDetailValueMarkup(letterText)}</dd>
|
|
||||||
<dt>From</dt><dd>${toDetailValueMarkup(fromWall?.name || formatDirectionName(fromWallId))}</dd>
|
|
||||||
<dt>To</dt><dd>${toDetailValueMarkup(toWall?.name || formatDirectionName(toWallId))}</dd>
|
|
||||||
<dt>Tarot</dt><dd>${toDetailValueMarkup(tarotCard || (tarotTrumpNumber != null ? `Trump ${tarotTrumpNumber}` : ""))}</dd>
|
|
||||||
</dl>
|
|
||||||
`;
|
|
||||||
bodyEl.appendChild(createMetaCard("Connector Details", summary));
|
|
||||||
|
|
||||||
if (astrologySummary) {
|
|
||||||
bodyEl.appendChild(createMetaCard("Astrology", astrologySummary));
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = document.createElement("div");
|
|
||||||
links.className = "kab-god-links";
|
|
||||||
|
|
||||||
if (letterId) {
|
|
||||||
links.appendChild(createNavButton(letterSymbol || "!", "nav:alphabet", {
|
|
||||||
alphabet: "hebrew",
|
|
||||||
hebrewLetterId: letterId
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathNo != null) {
|
|
||||||
links.appendChild(createNavButton(`Path ${pathNo}`, "nav:kabbalah-path", {
|
|
||||||
pathNo
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tarotCard || tarotTrumpNumber != null) {
|
|
||||||
links.appendChild(createNavButton(tarotCard || `Trump ${tarotTrumpNumber}`, "nav:tarot-trump", {
|
|
||||||
cardName: tarotCard,
|
|
||||||
trumpNumber: tarotTrumpNumber
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (links.childElementCount) {
|
|
||||||
const linksCard = document.createElement("div");
|
|
||||||
linksCard.className = "planet-meta-card";
|
|
||||||
linksCard.innerHTML = "<strong>Correspondence Links</strong>";
|
|
||||||
linksCard.appendChild(links);
|
|
||||||
bodyEl.appendChild(linksCard);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderEdgeCard(wall, detailBodyEl, wallEdgeDirections = new Map()) {
|
|
||||||
const wallId = normalizeId(wall?.id);
|
|
||||||
const selectedEdge = getEdgeById(state.selectedEdgeId)
|
|
||||||
|| getEdgesForWall(wallId)[0]
|
|
||||||
|| getEdges()[0]
|
|
||||||
|| null;
|
|
||||||
if (!selectedEdge) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.selectedEdgeId = normalizeEdgeId(selectedEdge.id);
|
|
||||||
|
|
||||||
const edgeDirection = wallEdgeDirections.get(normalizeEdgeId(selectedEdge.id));
|
|
||||||
const edgeName = edgeDirection
|
|
||||||
? formatDirectionName(edgeDirection)
|
|
||||||
: (toDisplayText(selectedEdge.name) || formatEdgeName(selectedEdge.id));
|
|
||||||
const edgeWalls = getEdgeWalls(selectedEdge)
|
|
||||||
.map((entry) => entry.charAt(0).toUpperCase() + entry.slice(1))
|
|
||||||
.join(" · ");
|
|
||||||
|
|
||||||
const edgeLetterId = getEdgeLetterId(selectedEdge);
|
|
||||||
const edgeLetter = getEdgeLetter(selectedEdge);
|
|
||||||
const edgePath = getEdgePathEntry(selectedEdge);
|
|
||||||
const astrologyType = toDisplayText(edgePath?.astrology?.type);
|
|
||||||
const astrologyName = toDisplayText(edgePath?.astrology?.name);
|
|
||||||
const astrologySymbol = getEdgeAstrologySymbol(selectedEdge);
|
|
||||||
const astrologyText = astrologySymbol && astrologyName
|
|
||||||
? `${astrologySymbol} ${astrologyName}`
|
|
||||||
: astrologySymbol || astrologyName;
|
|
||||||
|
|
||||||
const pathNo = toFiniteNumber(edgePath?.pathNumber);
|
|
||||||
const tarotCard = toDisplayText(edgePath?.tarot?.card);
|
|
||||||
const tarotTrumpNumber = toFiniteNumber(edgePath?.tarot?.trumpNumber);
|
|
||||||
|
|
||||||
const edgeCard = document.createElement("div");
|
|
||||||
edgeCard.className = "planet-meta-card";
|
|
||||||
|
|
||||||
const title = document.createElement("strong");
|
|
||||||
title.textContent = `Edge · ${edgeName}`;
|
|
||||||
edgeCard.appendChild(title);
|
|
||||||
|
|
||||||
const dlWrap = document.createElement("div");
|
|
||||||
dlWrap.className = "planet-text";
|
|
||||||
dlWrap.innerHTML = `
|
|
||||||
<dl class="alpha-dl">
|
|
||||||
<dt>Direction</dt><dd>${toDetailValueMarkup(edgeName)}</dd>
|
|
||||||
<dt>Edge</dt><dd>${toDetailValueMarkup(edgeWalls)}</dd>
|
|
||||||
<dt>Letter</dt><dd>${toDetailValueMarkup(edgeLetter)}</dd>
|
|
||||||
<dt>Astrology</dt><dd>${toDetailValueMarkup(astrologyText)}</dd>
|
|
||||||
<dt>Tarot</dt><dd>${toDetailValueMarkup(tarotCard)}</dd>
|
|
||||||
</dl>
|
|
||||||
`;
|
|
||||||
edgeCard.appendChild(dlWrap);
|
|
||||||
|
|
||||||
if (Array.isArray(selectedEdge.keywords) && selectedEdge.keywords.length) {
|
|
||||||
const keywords = document.createElement("p");
|
|
||||||
keywords.className = "planet-text";
|
|
||||||
keywords.textContent = selectedEdge.keywords.join(", ");
|
|
||||||
edgeCard.appendChild(keywords);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedEdge.description) {
|
|
||||||
const description = document.createElement("p");
|
|
||||||
description.className = "planet-text";
|
|
||||||
description.textContent = selectedEdge.description;
|
|
||||||
edgeCard.appendChild(description);
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = document.createElement("div");
|
|
||||||
links.className = "kab-god-links";
|
|
||||||
|
|
||||||
if (edgeLetterId) {
|
|
||||||
links.appendChild(createNavButton(edgeLetter || "!", "nav:alphabet", {
|
|
||||||
alphabet: "hebrew",
|
|
||||||
hebrewLetterId: edgeLetterId
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (astrologyType === "zodiac" && astrologyName) {
|
|
||||||
links.appendChild(createNavButton(astrologyName, "nav:zodiac", {
|
|
||||||
signId: normalizeId(astrologyName)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tarotCard) {
|
|
||||||
links.appendChild(createNavButton(tarotCard, "nav:tarot-trump", {
|
|
||||||
cardName: tarotCard,
|
|
||||||
trumpNumber: tarotTrumpNumber
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathNo != null) {
|
|
||||||
links.appendChild(createNavButton(`Path ${pathNo}`, "nav:kabbalah-path", {
|
|
||||||
pathNo
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (links.childElementCount) {
|
|
||||||
edgeCard.appendChild(links);
|
|
||||||
}
|
|
||||||
|
|
||||||
detailBodyEl.appendChild(edgeCard);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDetail(elements, walls) {
|
function renderDetail(elements, walls) {
|
||||||
if (state.selectedNodeType === "connector" && renderConnectorDetail(elements, walls)) {
|
if (typeof cubeDetailUi.renderDetail !== "function") {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.selectedNodeType === "center" && renderCenterDetail(elements)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wall = getWallById(state.selectedWallId) || walls[0] || null;
|
|
||||||
if (!wall || !elements?.detailNameEl || !elements?.detailSubEl || !elements?.detailBodyEl) {
|
|
||||||
if (elements?.detailNameEl) {
|
if (elements?.detailNameEl) {
|
||||||
elements.detailNameEl.textContent = "Cube data unavailable";
|
elements.detailNameEl.textContent = "Cube data unavailable";
|
||||||
}
|
}
|
||||||
if (elements?.detailSubEl) {
|
if (elements?.detailSubEl) {
|
||||||
elements.detailSubEl.textContent = "Could not load cube dataset.";
|
elements.detailSubEl.textContent = "Cube detail renderer missing.";
|
||||||
}
|
}
|
||||||
if (elements?.detailBodyEl) {
|
if (elements?.detailBodyEl) {
|
||||||
elements.detailBodyEl.innerHTML = "";
|
elements.detailBodyEl.innerHTML = "";
|
||||||
@@ -1460,152 +1253,40 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.selectedWallId = normalizeId(wall.id);
|
cubeDetailUi.renderDetail({
|
||||||
|
state,
|
||||||
const wallPlanet = toDisplayText(wall?.planet) || "!";
|
elements,
|
||||||
const wallElement = toDisplayText(wall?.element) || "!";
|
walls,
|
||||||
const wallFaceLetterId = getWallFaceLetterId(wall);
|
normalizeId,
|
||||||
const wallFaceLetter = getWallFaceLetter(wall);
|
normalizeEdgeId,
|
||||||
const wallFaceLetterText = wallFaceLetterId
|
normalizeLetterKey,
|
||||||
? `${wallFaceLetter ? `${wallFaceLetter} ` : ""}${toDisplayText(wallFaceLetterId)}`
|
formatDirectionName,
|
||||||
: "";
|
formatEdgeName,
|
||||||
elements.detailNameEl.textContent = `${wall.name} Wall`;
|
toFiniteNumber,
|
||||||
elements.detailSubEl.textContent = `${wallElement} · ${wallPlanet}`;
|
getWallById,
|
||||||
|
getEdgeById,
|
||||||
const bodyEl = elements.detailBodyEl;
|
getEdges,
|
||||||
bodyEl.innerHTML = "";
|
getEdgeWalls,
|
||||||
|
getEdgesForWall,
|
||||||
const summary = document.createElement("div");
|
getWallEdgeDirections,
|
||||||
summary.className = "planet-text";
|
getConnectorById,
|
||||||
summary.innerHTML = `
|
getConnectorPathEntry,
|
||||||
<dl class="alpha-dl">
|
getCubeCenterData,
|
||||||
<dt>Opposite</dt><dd>${toDetailValueMarkup(wall.opposite)}</dd>
|
getCenterLetterId,
|
||||||
<dt>Face Letter</dt><dd>${toDetailValueMarkup(wallFaceLetterText)}</dd>
|
getCenterLetterSymbol,
|
||||||
<dt>Element</dt><dd>${toDetailValueMarkup(wall.element)}</dd>
|
getEdgeLetterId,
|
||||||
<dt>Planet</dt><dd>${toDetailValueMarkup(wall.planet)}</dd>
|
getEdgeLetter,
|
||||||
<dt>Archangel</dt><dd>${toDetailValueMarkup(wall.archangel)}</dd>
|
getEdgePathEntry,
|
||||||
</dl>
|
getEdgeAstrologySymbol,
|
||||||
`;
|
getWallFaceLetterId,
|
||||||
bodyEl.appendChild(createMetaCard("Wall Details", summary));
|
getWallFaceLetter,
|
||||||
|
getHebrewLetterName,
|
||||||
if (Array.isArray(wall.keywords) && wall.keywords.length) {
|
getHebrewLetterSymbol,
|
||||||
bodyEl.appendChild(createMetaCard("Keywords", wall.keywords.join(", ")));
|
localDirectionOrder: LOCAL_DIRECTION_ORDER,
|
||||||
}
|
localDirectionRank: LOCAL_DIRECTION_RANK,
|
||||||
|
onSelectWall: selectWallById,
|
||||||
if (wall.description) {
|
onSelectEdge: selectEdgeById
|
||||||
bodyEl.appendChild(createMetaCard("Description", wall.description));
|
|
||||||
}
|
|
||||||
|
|
||||||
const wallLinksCard = document.createElement("div");
|
|
||||||
wallLinksCard.className = "planet-meta-card";
|
|
||||||
wallLinksCard.innerHTML = "<strong>Correspondence Links</strong>";
|
|
||||||
const wallLinks = document.createElement("div");
|
|
||||||
wallLinks.className = "kab-god-links";
|
|
||||||
|
|
||||||
if (wallFaceLetterId) {
|
|
||||||
const wallFaceLetterName = getHebrewLetterName(wallFaceLetterId) || toDisplayText(wallFaceLetterId);
|
|
||||||
const faceLetterText = [wallFaceLetter, wallFaceLetterName].filter(Boolean).join(" ");
|
|
||||||
const faceLetterLabel = faceLetterText
|
|
||||||
? `Face ${faceLetterText}`
|
|
||||||
: "Face !";
|
|
||||||
wallLinks.appendChild(createNavButton(faceLetterLabel, "nav:alphabet", {
|
|
||||||
alphabet: "hebrew",
|
|
||||||
hebrewLetterId: wallFaceLetterId
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const wallAssociations = wall.associations || {};
|
|
||||||
if (wallAssociations.planetId) {
|
|
||||||
wallLinks.appendChild(createNavButton(toDisplayText(wall.planet) || "!", "nav:planet", {
|
|
||||||
planetId: wallAssociations.planetId
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wallAssociations.godName) {
|
|
||||||
wallLinks.appendChild(createNavButton(wallAssociations.godName, "nav:gods", {
|
|
||||||
godName: wallAssociations.godName
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wall.oppositeWallId) {
|
|
||||||
const oppositeWall = getWallById(wall.oppositeWallId);
|
|
||||||
const internal = document.createElement("button");
|
|
||||||
internal.type = "button";
|
|
||||||
internal.className = "kab-god-link";
|
|
||||||
internal.textContent = `Opposite: ${oppositeWall?.name || wall.oppositeWallId}`;
|
|
||||||
internal.addEventListener("click", () => {
|
|
||||||
state.selectedWallId = normalizeId(wall.oppositeWallId);
|
|
||||||
state.selectedEdgeId = normalizeEdgeId(getEdgesForWall(state.selectedWallId)[0]?.id || getEdges()[0]?.id);
|
|
||||||
state.selectedNodeType = "wall";
|
|
||||||
state.selectedConnectorId = null;
|
|
||||||
snapRotationToWall(state.selectedWallId);
|
|
||||||
render(getElements());
|
|
||||||
});
|
|
||||||
wallLinks.appendChild(internal);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wallLinks.childElementCount) {
|
|
||||||
wallLinksCard.appendChild(wallLinks);
|
|
||||||
bodyEl.appendChild(wallLinksCard);
|
|
||||||
}
|
|
||||||
|
|
||||||
const edgesCard = document.createElement("div");
|
|
||||||
edgesCard.className = "planet-meta-card";
|
|
||||||
edgesCard.innerHTML = "<strong>Wall Edges</strong>";
|
|
||||||
|
|
||||||
const chips = document.createElement("div");
|
|
||||||
chips.className = "kab-chips";
|
|
||||||
|
|
||||||
const wallEdgeDirections = getWallEdgeDirections(wall);
|
|
||||||
const wallEdges = getEdgesForWall(wall)
|
|
||||||
.slice()
|
|
||||||
.sort((left, right) => {
|
|
||||||
const leftDirection = wallEdgeDirections.get(normalizeEdgeId(left?.id));
|
|
||||||
const rightDirection = wallEdgeDirections.get(normalizeEdgeId(right?.id));
|
|
||||||
const leftRank = LOCAL_DIRECTION_RANK[leftDirection] ?? LOCAL_DIRECTION_ORDER.length;
|
|
||||||
const rightRank = LOCAL_DIRECTION_RANK[rightDirection] ?? LOCAL_DIRECTION_ORDER.length;
|
|
||||||
if (leftRank !== rightRank) {
|
|
||||||
return leftRank - rightRank;
|
|
||||||
}
|
|
||||||
return normalizeEdgeId(left?.id).localeCompare(normalizeEdgeId(right?.id));
|
|
||||||
});
|
|
||||||
|
|
||||||
wallEdges.forEach((edge) => {
|
|
||||||
const id = normalizeEdgeId(edge.id);
|
|
||||||
const chipLetter = getEdgeLetter(edge);
|
|
||||||
const chipIsMissing = !chipLetter;
|
|
||||||
const direction = wallEdgeDirections.get(id);
|
|
||||||
const directionLabel = direction
|
|
||||||
? formatDirectionName(direction)
|
|
||||||
: (toDisplayText(edge.name) || formatEdgeName(edge.id));
|
|
||||||
const chip = document.createElement("span");
|
|
||||||
chip.className = `kab-chip${id === normalizeEdgeId(state.selectedEdgeId) ? " is-active" : ""}${chipIsMissing ? " is-missing" : ""}`;
|
|
||||||
chip.setAttribute("role", "button");
|
|
||||||
chip.setAttribute("tabindex", "0");
|
|
||||||
chip.textContent = `${directionLabel} · ${chipLetter || "!"}`;
|
|
||||||
|
|
||||||
const selectEdge = () => {
|
|
||||||
state.selectedEdgeId = id;
|
|
||||||
state.selectedNodeType = "wall";
|
|
||||||
state.selectedConnectorId = null;
|
|
||||||
render(getElements());
|
|
||||||
};
|
|
||||||
|
|
||||||
chip.addEventListener("click", selectEdge);
|
|
||||||
chip.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
selectEdge();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
chips.appendChild(chip);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
edgesCard.appendChild(chips);
|
|
||||||
bodyEl.appendChild(edgesCard);
|
|
||||||
|
|
||||||
renderEdgeCard(wall, bodyEl, wallEdgeDirections);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(elements) {
|
function render(elements) {
|
||||||
|
|||||||
116
app/ui-home-calendar.js
Normal file
116
app/ui-home-calendar.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let config = {};
|
||||||
|
let lastNowSkyGeoKey = "";
|
||||||
|
let lastNowSkySourceUrl = "";
|
||||||
|
|
||||||
|
function getNowSkyLayerEl() {
|
||||||
|
return config.nowSkyLayerEl || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNowPanelEl() {
|
||||||
|
return config.nowPanelEl || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentGeo() {
|
||||||
|
return config.getCurrentGeo?.() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const nowSkyLayerEl = getNowSkyLayerEl();
|
||||||
|
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()) {
|
||||||
|
const nowPanelEl = getNowPanelEl();
|
||||||
|
if (!nowPanelEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentGeo = getCurrentGeo();
|
||||||
|
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 init(nextConfig = {}) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...nextConfig
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotHomeUi = {
|
||||||
|
...(window.TarotHomeUi || {}),
|
||||||
|
init,
|
||||||
|
syncNowSkyBackground,
|
||||||
|
syncNowPanelTheme
|
||||||
|
};
|
||||||
|
})();
|
||||||
509
app/ui-kabbalah-detail.js
Normal file
509
app/ui-kabbalah-detail.js
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const PLANET_ID_TO_LABEL = {
|
||||||
|
saturn: "Saturn",
|
||||||
|
jupiter: "Jupiter",
|
||||||
|
mars: "Mars",
|
||||||
|
sol: "Sol",
|
||||||
|
venus: "Venus",
|
||||||
|
mercury: "Mercury",
|
||||||
|
luna: "Luna"
|
||||||
|
};
|
||||||
|
|
||||||
|
const MINOR_RANK_BY_PLURAL = {
|
||||||
|
aces: "Ace",
|
||||||
|
twos: "Two",
|
||||||
|
threes: "Three",
|
||||||
|
fours: "Four",
|
||||||
|
fives: "Five",
|
||||||
|
sixes: "Six",
|
||||||
|
sevens: "Seven",
|
||||||
|
eights: "Eight",
|
||||||
|
nines: "Nine",
|
||||||
|
tens: "Ten"
|
||||||
|
};
|
||||||
|
|
||||||
|
const MINOR_SUITS = ["Wands", "Cups", "Swords", "Disks"];
|
||||||
|
|
||||||
|
const DEFAULT_FOUR_QABALISTIC_WORLD_LAYERS = [
|
||||||
|
{
|
||||||
|
slot: "Yod",
|
||||||
|
letterChar: "י",
|
||||||
|
hebrewToken: "yod",
|
||||||
|
world: "Atziluth",
|
||||||
|
worldLayer: "Archetypal World (God’s Will)",
|
||||||
|
worldDescription: "World of gods or specific facets or divine qualities.",
|
||||||
|
soulLayer: "Chiah",
|
||||||
|
soulTitle: "Life Force",
|
||||||
|
soulDescription: "The Chiah is the Life Force itself and our true identity as reflection of Supreme Consciousness."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slot: "Heh",
|
||||||
|
letterChar: "ה",
|
||||||
|
hebrewToken: "he",
|
||||||
|
world: "Briah",
|
||||||
|
worldLayer: "Creative World (God’s Love)",
|
||||||
|
worldDescription: "World of archangels, executors of divine qualities.",
|
||||||
|
soulLayer: "Neshamah",
|
||||||
|
soulTitle: "Soul-Intuition",
|
||||||
|
soulDescription: "The Neshamah is the part of our soul that transcends the thinking process."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slot: "Vav",
|
||||||
|
letterChar: "ו",
|
||||||
|
hebrewToken: "vav",
|
||||||
|
world: "Yetzirah",
|
||||||
|
worldLayer: "Formative World (God’s Mind)",
|
||||||
|
worldDescription: "World of angels who work under archangelic direction.",
|
||||||
|
soulLayer: "Ruach",
|
||||||
|
soulTitle: "Intellect",
|
||||||
|
soulDescription: "The Ruach is the thinking mind that often dominates attention and identity."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slot: "Heh (final)",
|
||||||
|
letterChar: "ה",
|
||||||
|
hebrewToken: "he",
|
||||||
|
world: "Assiah",
|
||||||
|
worldLayer: "Material World (God’s Creation)",
|
||||||
|
worldDescription: "World of spirits that infuse matter and energy through specialized duties.",
|
||||||
|
soulLayer: "Nephesh",
|
||||||
|
soulTitle: "Animal Soul",
|
||||||
|
soulDescription: "The Nephesh is instinctive consciousness expressed through appetite, emotion, sex drive, and survival."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function metaCard(label, value, wide) {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = wide ? "planet-meta-card kab-wide-card" : "planet-meta-card";
|
||||||
|
card.innerHTML = `<strong>${label}</strong><p class="planet-text">${value || "—"}</p>`;
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNavButton(label, eventName, detail) {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "kab-god-link";
|
||||||
|
btn.textContent = `${label} ↗`;
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
document.dispatchEvent(new CustomEvent(eventName, { detail }));
|
||||||
|
});
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendLinkRow(card, buttons) {
|
||||||
|
if (!buttons?.length) return;
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "kab-god-links";
|
||||||
|
buttons.forEach((button) => row.appendChild(button));
|
||||||
|
card.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPlanetLuminaryCard(planetValue, context) {
|
||||||
|
const card = metaCard("Planet / Luminary", planetValue);
|
||||||
|
const planetId = context.resolvePlanetId(planetValue);
|
||||||
|
if (planetId) {
|
||||||
|
appendLinkRow(card, [
|
||||||
|
createNavButton(`View ${PLANET_ID_TO_LABEL[planetId] || planetValue} in Planets`, "nav:planet", { planetId })
|
||||||
|
]);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
const zodiacId = context.resolveZodiacId(planetValue);
|
||||||
|
if (zodiacId) {
|
||||||
|
appendLinkRow(card, [
|
||||||
|
createNavButton(`View ${zodiacId.charAt(0).toUpperCase() + zodiacId.slice(1)} in Zodiac`, "nav:zodiac", { signId: zodiacId })
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractMinorRank(attribution) {
|
||||||
|
const match = String(attribution || "").match(/\bthe\s+4\s+(aces|twos|threes|fours|fives|sixes|sevens|eights|nines|tens)\b/i);
|
||||||
|
if (!match) return null;
|
||||||
|
return MINOR_RANK_BY_PLURAL[(match[1] || "").toLowerCase()] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMinorTarotNames(attribution) {
|
||||||
|
const rank = extractMinorRank(attribution);
|
||||||
|
if (!rank) return [];
|
||||||
|
return MINOR_SUITS.map((suit) => `${rank} of ${suit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTarotAttributionCard(attribution) {
|
||||||
|
const card = metaCard("Tarot Attribution", attribution);
|
||||||
|
const minorCards = buildMinorTarotNames(attribution);
|
||||||
|
if (minorCards.length) {
|
||||||
|
appendLinkRow(card, minorCards.map((cardName) =>
|
||||||
|
createNavButton(cardName, "nav:tarot-trump", { cardName })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAstrologyCard(astrology, context) {
|
||||||
|
const astroText = astrology ? `${astrology.name} (${astrology.type})` : "—";
|
||||||
|
const card = metaCard("Astrology", astroText);
|
||||||
|
if (astrology?.type === "planet") {
|
||||||
|
const planetId = context.resolvePlanetId(astrology.name);
|
||||||
|
if (planetId) {
|
||||||
|
appendLinkRow(card, [
|
||||||
|
createNavButton(`View ${PLANET_ID_TO_LABEL[planetId] || astrology.name} in Planets`, "nav:planet", { planetId })
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else if (astrology?.type === "zodiac") {
|
||||||
|
const signId = context.resolveZodiacId(astrology.name);
|
||||||
|
if (signId) {
|
||||||
|
appendLinkRow(card, [
|
||||||
|
createNavButton(`View ${signId.charAt(0).toUpperCase() + signId.slice(1)} in Zodiac`, "nav:zodiac", { signId })
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConnectsCard(path, fromName, toName) {
|
||||||
|
const card = metaCard("Connects", `${fromName} → ${toName}`);
|
||||||
|
appendLinkRow(card, [
|
||||||
|
createNavButton(`View ${fromName}`, "nav:kabbalah-path", { pathNo: Number(path.connects.from) }),
|
||||||
|
createNavButton(`View ${toName}`, "nav:kabbalah-path", { pathNo: Number(path.connects.to) })
|
||||||
|
]);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHebrewLetterCard(letter, context) {
|
||||||
|
const label = `${letter.char || ""} ${letter.transliteration || ""} — "${letter.meaning || ""}" (${letter.letterType || ""})`;
|
||||||
|
const card = metaCard("Hebrew Letter", label);
|
||||||
|
const hebrewLetterId = context.resolveHebrewLetterId(letter.transliteration || letter.char || "");
|
||||||
|
|
||||||
|
if (hebrewLetterId) {
|
||||||
|
appendLinkRow(card, [
|
||||||
|
createNavButton(`View ${letter.transliteration || letter.char || "Letter"} in Alphabet`, "nav:alphabet", {
|
||||||
|
alphabet: "hebrew",
|
||||||
|
hebrewLetterId
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFourWorldsCard(tree, activeHebrewToken, context) {
|
||||||
|
const activeToken = String(activeHebrewToken || "").trim().toLowerCase();
|
||||||
|
const worldLayers = Array.isArray(context.fourWorldLayers) && context.fourWorldLayers.length
|
||||||
|
? context.fourWorldLayers
|
||||||
|
: DEFAULT_FOUR_QABALISTIC_WORLD_LAYERS;
|
||||||
|
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "planet-meta-card kab-wide-card";
|
||||||
|
|
||||||
|
const title = document.createElement("strong");
|
||||||
|
title.textContent = "Four Qabalistic Worlds & Soul Layers";
|
||||||
|
card.appendChild(title);
|
||||||
|
|
||||||
|
const stack = document.createElement("div");
|
||||||
|
stack.className = "cal-item-stack";
|
||||||
|
|
||||||
|
worldLayers.forEach((layer) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "cal-item-row";
|
||||||
|
|
||||||
|
const isActive = Boolean(activeToken) && activeToken === String(layer.hebrewToken || "").trim().toLowerCase();
|
||||||
|
|
||||||
|
const head = document.createElement("div");
|
||||||
|
head.className = "cal-item-head";
|
||||||
|
head.innerHTML = `
|
||||||
|
<span class="cal-item-name">${layer.slot}: ${layer.letterChar} — ${layer.world}</span>
|
||||||
|
<span class="planet-list-meta">${layer.soulLayer}</span>
|
||||||
|
`;
|
||||||
|
row.appendChild(head);
|
||||||
|
|
||||||
|
const worldLine = document.createElement("div");
|
||||||
|
worldLine.className = "planet-text";
|
||||||
|
worldLine.textContent = `${layer.worldLayer} · ${layer.worldDescription}`;
|
||||||
|
row.appendChild(worldLine);
|
||||||
|
|
||||||
|
const soulLine = document.createElement("div");
|
||||||
|
soulLine.className = "planet-text";
|
||||||
|
soulLine.textContent = `${layer.soulLayer} — ${layer.soulTitle}: ${layer.soulDescription}`;
|
||||||
|
row.appendChild(soulLine);
|
||||||
|
|
||||||
|
const buttonRow = [];
|
||||||
|
const hebrewLetterId = context.resolveHebrewLetterId(layer.hebrewToken);
|
||||||
|
if (hebrewLetterId) {
|
||||||
|
buttonRow.push(
|
||||||
|
createNavButton(`View ${layer.letterChar} in Alphabet`, "nav:alphabet", {
|
||||||
|
alphabet: "hebrew",
|
||||||
|
hebrewLetterId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedPath = context.findPathByHebrewToken(tree, layer.hebrewToken);
|
||||||
|
if (linkedPath?.pathNumber != null) {
|
||||||
|
buttonRow.push(
|
||||||
|
createNavButton(`View Path ${linkedPath.pathNumber}`, "nav:kabbalah-path", { pathNo: Number(linkedPath.pathNumber) })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
appendLinkRow(row, buttonRow);
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
row.style.borderColor = "#818cf8";
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
card.appendChild(stack);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitCorrespondenceNames(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.split(/,|;|·|\/|\bor\b|\band\b|\+/i)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueNames(values) {
|
||||||
|
const seen = new Set();
|
||||||
|
const output = [];
|
||||||
|
values.forEach((name) => {
|
||||||
|
const key = String(name || "").toLowerCase();
|
||||||
|
if (seen.has(key)) return;
|
||||||
|
seen.add(key);
|
||||||
|
output.push(name);
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function godLinksCard(label, names, pathNo, metaText) {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "planet-meta-card";
|
||||||
|
|
||||||
|
const title = document.createElement("strong");
|
||||||
|
title.textContent = label;
|
||||||
|
card.appendChild(title);
|
||||||
|
|
||||||
|
if (metaText) {
|
||||||
|
const meta = document.createElement("p");
|
||||||
|
meta.className = "planet-text kab-god-meta";
|
||||||
|
meta.textContent = metaText;
|
||||||
|
card.appendChild(meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "kab-god-links";
|
||||||
|
|
||||||
|
names.forEach((name) => {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "kab-god-link";
|
||||||
|
btn.textContent = name;
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
document.dispatchEvent(new CustomEvent("nav:gods", {
|
||||||
|
detail: { godName: name, pathNo: Number(pathNo) }
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
row.appendChild(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
card.appendChild(row);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendGodsCards(pathNo, elements, godsData) {
|
||||||
|
const gd = godsData?.[String(pathNo)];
|
||||||
|
if (!gd) return;
|
||||||
|
|
||||||
|
const hasAny = gd.greek || gd.roman || gd.egyptian || gd.egyptianPractical
|
||||||
|
|| gd.elohim || gd.archangel || gd.angelicOrder;
|
||||||
|
if (!hasAny) return;
|
||||||
|
|
||||||
|
const sep = document.createElement("div");
|
||||||
|
sep.className = "planet-meta-card kab-wide-card";
|
||||||
|
sep.innerHTML = `<strong style="color:#a1a1aa;font-size:11px;text-transform:uppercase;letter-spacing:.05em">Divine Correspondences</strong>`;
|
||||||
|
elements.detailBodyEl.appendChild(sep);
|
||||||
|
|
||||||
|
const greekNames = uniqueNames(splitCorrespondenceNames(gd.greek));
|
||||||
|
const romanNames = uniqueNames(splitCorrespondenceNames(gd.roman));
|
||||||
|
const egyptNames = uniqueNames([
|
||||||
|
...splitCorrespondenceNames(gd.egyptianPractical),
|
||||||
|
...splitCorrespondenceNames(gd.egyptian)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (greekNames.length) {
|
||||||
|
elements.detailBodyEl.appendChild(godLinksCard("Greek", greekNames, pathNo));
|
||||||
|
}
|
||||||
|
if (romanNames.length) {
|
||||||
|
elements.detailBodyEl.appendChild(godLinksCard("Roman", romanNames, pathNo));
|
||||||
|
}
|
||||||
|
if (egyptNames.length) {
|
||||||
|
elements.detailBodyEl.appendChild(godLinksCard("Egyptian", egyptNames, pathNo));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gd.elohim) {
|
||||||
|
const g = gd.elohim;
|
||||||
|
const meta = `${g.hebrew}${g.meaning ? " — " + g.meaning : ""}`;
|
||||||
|
elements.detailBodyEl.appendChild(godLinksCard(
|
||||||
|
"God Name",
|
||||||
|
uniqueNames(splitCorrespondenceNames(g.transliteration)),
|
||||||
|
pathNo,
|
||||||
|
meta
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (gd.archangel) {
|
||||||
|
const a = gd.archangel;
|
||||||
|
const meta = `${a.hebrew}`;
|
||||||
|
elements.detailBodyEl.appendChild(godLinksCard(
|
||||||
|
"Archangel",
|
||||||
|
uniqueNames(splitCorrespondenceNames(a.transliteration)),
|
||||||
|
pathNo,
|
||||||
|
meta
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (gd.angelicOrder) {
|
||||||
|
const o = gd.angelicOrder;
|
||||||
|
elements.detailBodyEl.appendChild(metaCard(
|
||||||
|
"Angelic Order",
|
||||||
|
`${o.hebrew} ${o.transliteration}${o.meaning ? " — " + o.meaning : ""}`
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSephiraDetail(context) {
|
||||||
|
const { seph, tree, elements } = context;
|
||||||
|
elements.detailNameEl.textContent = `${seph.number} · ${seph.name}`;
|
||||||
|
elements.detailSubEl.textContent =
|
||||||
|
[seph.nameHebrew, seph.translation, seph.planet].filter(Boolean).join(" · ");
|
||||||
|
|
||||||
|
elements.detailBodyEl.innerHTML = "";
|
||||||
|
elements.detailBodyEl.appendChild(buildFourWorldsCard(tree, "", context));
|
||||||
|
elements.detailBodyEl.appendChild(buildPlanetLuminaryCard(seph.planet, context));
|
||||||
|
elements.detailBodyEl.appendChild(metaCard("Intelligence", seph.intelligence));
|
||||||
|
elements.detailBodyEl.appendChild(buildTarotAttributionCard(seph.tarot));
|
||||||
|
|
||||||
|
if (seph.description) {
|
||||||
|
elements.detailBodyEl.appendChild(
|
||||||
|
metaCard(seph.name, seph.description, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const connected = tree.paths.filter(
|
||||||
|
(entry) => entry.connects.from === seph.number || entry.connects.to === seph.number
|
||||||
|
);
|
||||||
|
if (connected.length) {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "planet-meta-card kab-wide-card";
|
||||||
|
const chips = connected.map((entry) =>
|
||||||
|
`<span class="kab-chip" data-path="${entry.pathNumber}" role="button" tabindex="0" title="Path ${entry.pathNumber}: ${entry.tarot?.card || ""}">`
|
||||||
|
+ `${entry.hebrewLetter?.char || ""} <span class="kab-chip-sub">${entry.pathNumber}</span>`
|
||||||
|
+ `</span>`
|
||||||
|
).join("");
|
||||||
|
card.innerHTML = `<strong>Connected Paths</strong><div class="kab-chips">${chips}</div>`;
|
||||||
|
elements.detailBodyEl.appendChild(card);
|
||||||
|
|
||||||
|
card.querySelectorAll(".kab-chip[data-path]").forEach((chip) => {
|
||||||
|
const handler = () => {
|
||||||
|
const path = tree.paths.find((entry) => entry.pathNumber === Number(chip.dataset.path));
|
||||||
|
if (path && typeof context.onPathSelect === "function") {
|
||||||
|
context.onPathSelect(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
chip.addEventListener("click", handler);
|
||||||
|
chip.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
appendGodsCards(seph.number, elements, context.godsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPathDetail(context) {
|
||||||
|
const { path, tree, elements } = context;
|
||||||
|
const letter = path.hebrewLetter || {};
|
||||||
|
const fromName = tree.sephiroth.find((entry) => entry.number === path.connects.from)?.name || path.connects.from;
|
||||||
|
const toName = tree.sephiroth.find((entry) => entry.number === path.connects.to)?.name || path.connects.to;
|
||||||
|
const astro = path.astrology ? `${path.astrology.name} (${path.astrology.type})` : "—";
|
||||||
|
const tarotStr = path.tarot?.card
|
||||||
|
? `${path.tarot.card}${path.tarot.trumpNumber != null ? " · Trump " + path.tarot.trumpNumber : ""}`
|
||||||
|
: "—";
|
||||||
|
|
||||||
|
elements.detailNameEl.textContent =
|
||||||
|
`Path ${path.pathNumber} · ${letter.char || ""} ${letter.transliteration || ""}`;
|
||||||
|
elements.detailSubEl.textContent = [path.tarot?.card, astro].filter(Boolean).join(" · ");
|
||||||
|
|
||||||
|
elements.detailBodyEl.innerHTML = "";
|
||||||
|
elements.detailBodyEl.appendChild(buildFourWorldsCard(tree, context.activeHebrewToken, context));
|
||||||
|
elements.detailBodyEl.appendChild(buildConnectsCard(path, fromName, toName));
|
||||||
|
elements.detailBodyEl.appendChild(buildHebrewLetterCard(letter, context));
|
||||||
|
elements.detailBodyEl.appendChild(buildAstrologyCard(path.astrology, context));
|
||||||
|
|
||||||
|
const tarotMetaCard = document.createElement("div");
|
||||||
|
tarotMetaCard.className = "planet-meta-card";
|
||||||
|
const tarotLabel = document.createElement("strong");
|
||||||
|
tarotLabel.textContent = "Tarot";
|
||||||
|
tarotMetaCard.appendChild(tarotLabel);
|
||||||
|
if (path.tarot?.card && path.tarot.trumpNumber != null) {
|
||||||
|
const tarotBtn = document.createElement("button");
|
||||||
|
tarotBtn.type = "button";
|
||||||
|
tarotBtn.className = "kab-tarot-link";
|
||||||
|
tarotBtn.textContent = `${path.tarot.card} · Trump ${path.tarot.trumpNumber}`;
|
||||||
|
tarotBtn.title = "Open in Tarot section";
|
||||||
|
tarotBtn.addEventListener("click", () => {
|
||||||
|
document.dispatchEvent(new CustomEvent("kab:view-trump", {
|
||||||
|
detail: { trumpNumber: path.tarot.trumpNumber }
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
tarotMetaCard.appendChild(tarotBtn);
|
||||||
|
} else {
|
||||||
|
const tarotP = document.createElement("p");
|
||||||
|
tarotP.className = "planet-text";
|
||||||
|
tarotP.textContent = tarotStr || "—";
|
||||||
|
tarotMetaCard.appendChild(tarotP);
|
||||||
|
}
|
||||||
|
elements.detailBodyEl.appendChild(tarotMetaCard);
|
||||||
|
|
||||||
|
elements.detailBodyEl.appendChild(metaCard("Intelligence", path.intelligence));
|
||||||
|
elements.detailBodyEl.appendChild(metaCard("Pillar", path.pillar));
|
||||||
|
|
||||||
|
if (path.description) {
|
||||||
|
const desc = document.createElement("div");
|
||||||
|
desc.className = "planet-meta-card kab-wide-card";
|
||||||
|
desc.innerHTML =
|
||||||
|
`<strong>Path ${path.pathNumber} — Sefer Yetzirah</strong>`
|
||||||
|
+ `<p class="planet-text">${path.description.replace(/\n/g, "<br><br>")}</p>`;
|
||||||
|
elements.detailBodyEl.appendChild(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
appendGodsCards(path.pathNumber, elements, context.godsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRoseLandingIntro(roseElements) {
|
||||||
|
if (!roseElements?.detailNameEl || !roseElements?.detailSubEl || !roseElements?.detailBodyEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
roseElements.detailNameEl.textContent = "Rosicrucian Cross";
|
||||||
|
roseElements.detailSubEl.textContent = "Select a Hebrew letter petal to explore a Tree path";
|
||||||
|
|
||||||
|
const introCard = document.createElement("div");
|
||||||
|
introCard.className = "planet-meta-card kab-wide-card";
|
||||||
|
introCard.innerHTML = "<strong>Interactive Path Crosswalk</strong>"
|
||||||
|
+ "<p class=\"planet-text\">Each petal maps to one of the 22 Hebrew letter paths (11-32). Click any large Hebrew letter to view astrology, tarot, and path intelligence details.</p>";
|
||||||
|
|
||||||
|
roseElements.detailBodyEl.innerHTML = "";
|
||||||
|
roseElements.detailBodyEl.appendChild(introCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.KabbalahDetailUi = {
|
||||||
|
renderSephiraDetail,
|
||||||
|
renderPathDetail,
|
||||||
|
renderRoseLandingIntro
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -61,6 +61,8 @@
|
|||||||
selectedPathNumber: null
|
selectedPathNumber: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const kabbalahDetailUi = window.KabbalahDetailUi || {};
|
||||||
|
|
||||||
const PLANET_NAME_TO_ID = {
|
const PLANET_NAME_TO_ID = {
|
||||||
saturn: "saturn",
|
saturn: "saturn",
|
||||||
jupiter: "jupiter",
|
jupiter: "jupiter",
|
||||||
@@ -88,31 +90,6 @@
|
|||||||
pisces: "pisces"
|
pisces: "pisces"
|
||||||
};
|
};
|
||||||
|
|
||||||
const PLANET_ID_TO_LABEL = {
|
|
||||||
saturn: "Saturn",
|
|
||||||
jupiter: "Jupiter",
|
|
||||||
mars: "Mars",
|
|
||||||
sol: "Sol",
|
|
||||||
venus: "Venus",
|
|
||||||
mercury: "Mercury",
|
|
||||||
luna: "Luna"
|
|
||||||
};
|
|
||||||
|
|
||||||
const MINOR_RANK_BY_PLURAL = {
|
|
||||||
aces: "Ace",
|
|
||||||
twos: "Two",
|
|
||||||
threes: "Three",
|
|
||||||
fours: "Four",
|
|
||||||
fives: "Five",
|
|
||||||
sixes: "Six",
|
|
||||||
sevens: "Seven",
|
|
||||||
eights: "Eight",
|
|
||||||
nines: "Nine",
|
|
||||||
tens: "Ten"
|
|
||||||
};
|
|
||||||
|
|
||||||
const MINOR_SUITS = ["Wands", "Cups", "Swords", "Disks"];
|
|
||||||
|
|
||||||
const HEBREW_LETTER_ALIASES = {
|
const HEBREW_LETTER_ALIASES = {
|
||||||
aleph: "alef",
|
aleph: "alef",
|
||||||
alef: "alef",
|
alef: "alef",
|
||||||
@@ -274,6 +251,22 @@
|
|||||||
pathLetterToggleEl: document.getElementById("kab-path-letter-toggle"),
|
pathLetterToggleEl: document.getElementById("kab-path-letter-toggle"),
|
||||||
pathNumberToggleEl: document.getElementById("kab-path-number-toggle"),
|
pathNumberToggleEl: document.getElementById("kab-path-number-toggle"),
|
||||||
pathTarotToggleEl: document.getElementById("kab-path-tarot-toggle"),
|
pathTarotToggleEl: document.getElementById("kab-path-tarot-toggle"),
|
||||||
|
roseCrossContainerEl: document.getElementById("kab-rose-cross-container"),
|
||||||
|
roseDetailNameEl: document.getElementById("kab-rose-detail-name"),
|
||||||
|
roseDetailSubEl: document.getElementById("kab-rose-detail-sub"),
|
||||||
|
roseDetailBodyEl: document.getElementById("kab-rose-detail-body"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoseDetailElements(elements) {
|
||||||
|
if (!elements) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
detailNameEl: elements.roseDetailNameEl,
|
||||||
|
detailSubEl: elements.roseDetailSubEl,
|
||||||
|
detailBodyEl: elements.roseDetailBodyEl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +279,37 @@
|
|||||||
return window.TarotCardImages.resolveTarotCardImage(cardName);
|
return window.TarotCardImages.resolveTarotCardImage(cardName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSvgImageHref(imageEl) {
|
||||||
|
if (!(imageEl instanceof SVGElement)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(
|
||||||
|
imageEl.getAttribute("href")
|
||||||
|
|| imageEl.getAttributeNS("http://www.w3.org/1999/xlink", "href")
|
||||||
|
|| ""
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTarotLightboxForPath(path, fallbackSrc = "") {
|
||||||
|
const openLightbox = window.TarotUiLightbox?.open;
|
||||||
|
if (typeof openLightbox !== "function") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardName = String(path?.tarot?.card || "").trim();
|
||||||
|
const src = String(fallbackSrc || resolvePathTarotImage(path) || "").trim();
|
||||||
|
if (!src) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackLabel = Number.isFinite(Number(path?.pathNumber))
|
||||||
|
? `Path ${path.pathNumber} tarot card`
|
||||||
|
: "Path tarot card";
|
||||||
|
openLightbox(src, cardName || fallbackLabel);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function getPathLabel(path) {
|
function getPathLabel(path) {
|
||||||
const glyph = String(path?.hebrewLetter?.char || "").trim();
|
const glyph = String(path?.hebrewLetter?.char || "").trim();
|
||||||
const pathNumber = Number(path?.pathNumber);
|
const pathNumber = Number(path?.pathNumber);
|
||||||
@@ -312,6 +336,8 @@
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rosicrucian cross SVG construction lives in app/ui-rosicrucian-cross.js.
|
||||||
|
|
||||||
// ─── build the full SVG tree ─────────────────────────────────────────────────
|
// ─── build the full SVG tree ─────────────────────────────────────────────────
|
||||||
function buildTreeSVG(tree) {
|
function buildTreeSVG(tree) {
|
||||||
const svg = svgEl("svg", {
|
const svg = svgEl("svg", {
|
||||||
@@ -491,14 +517,6 @@
|
|||||||
return svg;
|
return svg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── detail panel helpers ───────────────────────────────────────────────────
|
|
||||||
function metaCard(label, value, wide) {
|
|
||||||
const card = document.createElement("div");
|
|
||||||
card.className = wide ? "planet-meta-card kab-wide-card" : "planet-meta-card";
|
|
||||||
card.innerHTML = `<strong>${label}</strong><p class="planet-text">${value || "—"}</p>`;
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeText(value) {
|
function normalizeText(value) {
|
||||||
return String(value || "").trim().toLowerCase();
|
return String(value || "").trim().toLowerCase();
|
||||||
}
|
}
|
||||||
@@ -569,114 +587,6 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNavButton(label, eventName, detail) {
|
|
||||||
const btn = document.createElement("button");
|
|
||||||
btn.type = "button";
|
|
||||||
btn.className = "kab-god-link";
|
|
||||||
btn.textContent = `${label} ↗`;
|
|
||||||
btn.addEventListener("click", () => {
|
|
||||||
document.dispatchEvent(new CustomEvent(eventName, { detail }));
|
|
||||||
});
|
|
||||||
return btn;
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendLinkRow(card, buttons) {
|
|
||||||
if (!buttons?.length) return;
|
|
||||||
const row = document.createElement("div");
|
|
||||||
row.className = "kab-god-links";
|
|
||||||
buttons.forEach((button) => row.appendChild(button));
|
|
||||||
card.appendChild(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPlanetLuminaryCard(planetValue) {
|
|
||||||
const card = metaCard("Planet / Luminary", planetValue);
|
|
||||||
const planetId = resolvePlanetId(planetValue);
|
|
||||||
if (planetId) {
|
|
||||||
appendLinkRow(card, [
|
|
||||||
createNavButton(`View ${PLANET_ID_TO_LABEL[planetId] || planetValue} in Planets`, "nav:planet", { planetId })
|
|
||||||
]);
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
const zodiacId = resolveZodiacId(planetValue);
|
|
||||||
if (zodiacId) {
|
|
||||||
appendLinkRow(card, [
|
|
||||||
createNavButton(`View ${zodiacId.charAt(0).toUpperCase() + zodiacId.slice(1)} in Zodiac`, "nav:zodiac", { signId: zodiacId })
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractMinorRank(attribution) {
|
|
||||||
const match = String(attribution || "").match(/\bthe\s+4\s+(aces|twos|threes|fours|fives|sixes|sevens|eights|nines|tens)\b/i);
|
|
||||||
if (!match) return null;
|
|
||||||
return MINOR_RANK_BY_PLURAL[(match[1] || "").toLowerCase()] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMinorTarotNames(attribution) {
|
|
||||||
const rank = extractMinorRank(attribution);
|
|
||||||
if (!rank) return [];
|
|
||||||
return MINOR_SUITS.map((suit) => `${rank} of ${suit}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTarotAttributionCard(attribution) {
|
|
||||||
const card = metaCard("Tarot Attribution", attribution);
|
|
||||||
const minorCards = buildMinorTarotNames(attribution);
|
|
||||||
if (minorCards.length) {
|
|
||||||
appendLinkRow(card, minorCards.map((cardName) =>
|
|
||||||
createNavButton(cardName, "nav:tarot-trump", { cardName })
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAstrologyCard(astrology) {
|
|
||||||
const astroText = astrology ? `${astrology.name} (${astrology.type})` : "—";
|
|
||||||
const card = metaCard("Astrology", astroText);
|
|
||||||
if (astrology?.type === "planet") {
|
|
||||||
const planetId = resolvePlanetId(astrology.name);
|
|
||||||
if (planetId) {
|
|
||||||
appendLinkRow(card, [
|
|
||||||
createNavButton(`View ${PLANET_ID_TO_LABEL[planetId] || astrology.name} in Planets`, "nav:planet", { planetId })
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} else if (astrology?.type === "zodiac") {
|
|
||||||
const signId = resolveZodiacId(astrology.name);
|
|
||||||
if (signId) {
|
|
||||||
appendLinkRow(card, [
|
|
||||||
createNavButton(`View ${signId.charAt(0).toUpperCase() + signId.slice(1)} in Zodiac`, "nav:zodiac", { signId })
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildConnectsCard(path, fromName, toName) {
|
|
||||||
const card = metaCard("Connects", `${fromName} → ${toName}`);
|
|
||||||
appendLinkRow(card, [
|
|
||||||
createNavButton(`View ${fromName}`, "nav:kabbalah-path", { pathNo: Number(path.connects.from) }),
|
|
||||||
createNavButton(`View ${toName}`, "nav:kabbalah-path", { pathNo: Number(path.connects.to) })
|
|
||||||
]);
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildHebrewLetterCard(letter) {
|
|
||||||
const label = `${letter.char || ""} ${letter.transliteration || ""} — "${letter.meaning || ""}" (${letter.letterType || ""})`;
|
|
||||||
const card = metaCard("Hebrew Letter", label);
|
|
||||||
const hebrewLetterId = resolveHebrewLetterId(letter.transliteration || letter.char || "");
|
|
||||||
|
|
||||||
if (hebrewLetterId) {
|
|
||||||
appendLinkRow(card, [
|
|
||||||
createNavButton(`View ${letter.transliteration || letter.char || "Letter"} in Alphabet`, "nav:alphabet", {
|
|
||||||
alphabet: "hebrew",
|
|
||||||
hebrewLetterId
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findPathByHebrewToken(tree, hebrewToken) {
|
function findPathByHebrewToken(tree, hebrewToken) {
|
||||||
const canonicalToken = HEBREW_LETTER_ALIASES[normalizeLetterToken(hebrewToken)] || normalizeLetterToken(hebrewToken);
|
const canonicalToken = HEBREW_LETTER_ALIASES[normalizeLetterToken(hebrewToken)] || normalizeLetterToken(hebrewToken);
|
||||||
if (!canonicalToken) {
|
if (!canonicalToken) {
|
||||||
@@ -691,200 +601,27 @@
|
|||||||
}) || null;
|
}) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFourWorldsCard(tree, activeLetterToken = "") {
|
function getDetailRenderContext(tree, elements, extra = {}) {
|
||||||
const activeToken = HEBREW_LETTER_ALIASES[normalizeLetterToken(activeLetterToken)] || normalizeLetterToken(activeLetterToken);
|
return {
|
||||||
const worldLayers = Array.isArray(state.fourWorldLayers) && state.fourWorldLayers.length
|
tree,
|
||||||
? state.fourWorldLayers
|
elements,
|
||||||
: DEFAULT_FOUR_QABALISTIC_WORLD_LAYERS;
|
godsData: state.godsData,
|
||||||
|
fourWorldLayers: state.fourWorldLayers,
|
||||||
const card = document.createElement("div");
|
resolvePlanetId,
|
||||||
card.className = "planet-meta-card kab-wide-card";
|
resolveZodiacId,
|
||||||
|
resolveHebrewLetterId,
|
||||||
const title = document.createElement("strong");
|
findPathByHebrewToken,
|
||||||
title.textContent = "Four Qabalistic Worlds & Soul Layers";
|
...extra
|
||||||
card.appendChild(title);
|
};
|
||||||
|
|
||||||
const stack = document.createElement("div");
|
|
||||||
stack.className = "cal-item-stack";
|
|
||||||
|
|
||||||
worldLayers.forEach((layer) => {
|
|
||||||
const row = document.createElement("div");
|
|
||||||
row.className = "cal-item-row";
|
|
||||||
|
|
||||||
const isActive = Boolean(activeToken) && activeToken === layer.hebrewToken;
|
|
||||||
|
|
||||||
const head = document.createElement("div");
|
|
||||||
head.className = "cal-item-head";
|
|
||||||
head.innerHTML = `
|
|
||||||
<span class="cal-item-name">${layer.slot}: ${layer.letterChar} — ${layer.world}</span>
|
|
||||||
<span class="planet-list-meta">${layer.soulLayer}</span>
|
|
||||||
`;
|
|
||||||
row.appendChild(head);
|
|
||||||
|
|
||||||
const worldLine = document.createElement("div");
|
|
||||||
worldLine.className = "planet-text";
|
|
||||||
worldLine.textContent = `${layer.worldLayer} · ${layer.worldDescription}`;
|
|
||||||
row.appendChild(worldLine);
|
|
||||||
|
|
||||||
const soulLine = document.createElement("div");
|
|
||||||
soulLine.className = "planet-text";
|
|
||||||
soulLine.textContent = `${layer.soulLayer} — ${layer.soulTitle}: ${layer.soulDescription}`;
|
|
||||||
row.appendChild(soulLine);
|
|
||||||
|
|
||||||
const buttonRow = [];
|
|
||||||
const hebrewLetterId = resolveHebrewLetterId(layer.hebrewToken);
|
|
||||||
if (hebrewLetterId) {
|
|
||||||
buttonRow.push(
|
|
||||||
createNavButton(`View ${layer.letterChar} in Alphabet`, "nav:alphabet", {
|
|
||||||
alphabet: "hebrew",
|
|
||||||
hebrewLetterId
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkedPath = findPathByHebrewToken(tree, layer.hebrewToken);
|
|
||||||
if (linkedPath?.pathNumber != null) {
|
|
||||||
buttonRow.push(
|
|
||||||
createNavButton(`View Path ${linkedPath.pathNumber}`, "nav:kabbalah-path", { pathNo: Number(linkedPath.pathNumber) })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
appendLinkRow(row, buttonRow);
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
row.style.borderColor = "#818cf8";
|
|
||||||
}
|
|
||||||
|
|
||||||
stack.appendChild(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
card.appendChild(stack);
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitCorrespondenceNames(value) {
|
|
||||||
return String(value || "")
|
|
||||||
.split(/,|;|·|\/|\bor\b|\band\b|\+/i)
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
function uniqueNames(values) {
|
|
||||||
const seen = new Set();
|
|
||||||
const output = [];
|
|
||||||
values.forEach((name) => {
|
|
||||||
const key = String(name || "").toLowerCase();
|
|
||||||
if (seen.has(key)) return;
|
|
||||||
seen.add(key);
|
|
||||||
output.push(name);
|
|
||||||
});
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
function godLinksCard(label, names, pathNo, metaText) {
|
|
||||||
const card = document.createElement("div");
|
|
||||||
card.className = "planet-meta-card";
|
|
||||||
|
|
||||||
const title = document.createElement("strong");
|
|
||||||
title.textContent = label;
|
|
||||||
card.appendChild(title);
|
|
||||||
|
|
||||||
if (metaText) {
|
|
||||||
const meta = document.createElement("p");
|
|
||||||
meta.className = "planet-text kab-god-meta";
|
|
||||||
meta.textContent = metaText;
|
|
||||||
card.appendChild(meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = document.createElement("div");
|
|
||||||
row.className = "kab-god-links";
|
|
||||||
|
|
||||||
names.forEach((name) => {
|
|
||||||
const btn = document.createElement("button");
|
|
||||||
btn.type = "button";
|
|
||||||
btn.className = "kab-god-link";
|
|
||||||
btn.textContent = name;
|
|
||||||
btn.addEventListener("click", () => {
|
|
||||||
document.dispatchEvent(new CustomEvent("nav:gods", {
|
|
||||||
detail: { godName: name, pathNo: Number(pathNo) }
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
row.appendChild(btn);
|
|
||||||
});
|
|
||||||
|
|
||||||
card.appendChild(row);
|
|
||||||
return card;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearHighlights() {
|
function clearHighlights() {
|
||||||
document.querySelectorAll(".kab-node, .kab-node-glow")
|
document.querySelectorAll(".kab-node, .kab-node-glow")
|
||||||
.forEach(el => el.classList.remove("kab-node-active"));
|
.forEach(el => el.classList.remove("kab-node-active"));
|
||||||
document.querySelectorAll(".kab-path-hit, .kab-path-line, .kab-path-lbl, .kab-path-tarot")
|
document.querySelectorAll(".kab-path-hit, .kab-path-line, .kab-path-lbl, .kab-path-tarot, .kab-rose-petal")
|
||||||
.forEach(el => el.classList.remove("kab-path-active"));
|
.forEach(el => el.classList.remove("kab-path-active"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── helper: append divine correspondences from gods.json ─────────────────────
|
|
||||||
function appendGodsCards(pathNo, elements) {
|
|
||||||
const gd = state.godsData[String(pathNo)];
|
|
||||||
if (!gd) return;
|
|
||||||
|
|
||||||
const hasAny = gd.greek || gd.roman || gd.egyptian || gd.egyptianPractical
|
|
||||||
|| gd.elohim || gd.archangel || gd.angelicOrder;
|
|
||||||
if (!hasAny) return;
|
|
||||||
|
|
||||||
const sep = document.createElement("div");
|
|
||||||
sep.className = "planet-meta-card kab-wide-card";
|
|
||||||
sep.innerHTML = `<strong style="color:#a1a1aa;font-size:11px;text-transform:uppercase;letter-spacing:.05em">Divine Correspondences</strong>`;
|
|
||||||
elements.detailBodyEl.appendChild(sep);
|
|
||||||
|
|
||||||
const greekNames = uniqueNames(splitCorrespondenceNames(gd.greek));
|
|
||||||
const romanNames = uniqueNames(splitCorrespondenceNames(gd.roman));
|
|
||||||
const egyptNames = uniqueNames([
|
|
||||||
...splitCorrespondenceNames(gd.egyptianPractical),
|
|
||||||
...splitCorrespondenceNames(gd.egyptian)
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (greekNames.length) {
|
|
||||||
elements.detailBodyEl.appendChild(godLinksCard("Greek", greekNames, pathNo));
|
|
||||||
}
|
|
||||||
if (romanNames.length) {
|
|
||||||
elements.detailBodyEl.appendChild(godLinksCard("Roman", romanNames, pathNo));
|
|
||||||
}
|
|
||||||
if (egyptNames.length) {
|
|
||||||
elements.detailBodyEl.appendChild(godLinksCard("Egyptian", egyptNames, pathNo));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gd.elohim) {
|
|
||||||
const g = gd.elohim;
|
|
||||||
const meta = `${g.hebrew}${g.meaning ? " — " + g.meaning : ""}`;
|
|
||||||
elements.detailBodyEl.appendChild(godLinksCard(
|
|
||||||
"God Name",
|
|
||||||
uniqueNames(splitCorrespondenceNames(g.transliteration)),
|
|
||||||
pathNo,
|
|
||||||
meta
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if (gd.archangel) {
|
|
||||||
const a = gd.archangel;
|
|
||||||
const meta = `${a.hebrew}`;
|
|
||||||
elements.detailBodyEl.appendChild(godLinksCard(
|
|
||||||
"Archangel",
|
|
||||||
uniqueNames(splitCorrespondenceNames(a.transliteration)),
|
|
||||||
pathNo,
|
|
||||||
meta
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if (gd.angelicOrder) {
|
|
||||||
const o = gd.angelicOrder;
|
|
||||||
elements.detailBodyEl.appendChild(metaCard(
|
|
||||||
"Angelic Order",
|
|
||||||
`${o.hebrew} ${o.transliteration}${o.meaning ? " — " + o.meaning : ""}`
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── render sephira detail ───────────────────────────────────────────────────
|
|
||||||
function renderSephiraDetail(seph, tree, elements) {
|
function renderSephiraDetail(seph, tree, elements) {
|
||||||
state.selectedSephiraNumber = Number(seph?.number);
|
state.selectedSephiraNumber = Number(seph?.number);
|
||||||
state.selectedPathNumber = null;
|
state.selectedPathNumber = null;
|
||||||
@@ -893,53 +630,14 @@
|
|||||||
document.querySelectorAll(`.kab-node[data-sephira="${seph.number}"], .kab-node-glow[data-sephira="${seph.number}"]`)
|
document.querySelectorAll(`.kab-node[data-sephira="${seph.number}"], .kab-node-glow[data-sephira="${seph.number}"]`)
|
||||||
.forEach(el => el.classList.add("kab-node-active"));
|
.forEach(el => el.classList.add("kab-node-active"));
|
||||||
|
|
||||||
elements.detailNameEl.textContent = `${seph.number} · ${seph.name}`;
|
if (typeof kabbalahDetailUi.renderSephiraDetail === "function") {
|
||||||
elements.detailSubEl.textContent =
|
kabbalahDetailUi.renderSephiraDetail(getDetailRenderContext(tree, elements, {
|
||||||
[seph.nameHebrew, seph.translation, seph.planet].filter(Boolean).join(" · ");
|
seph,
|
||||||
|
onPathSelect: (path) => renderPathDetail(path, tree, elements)
|
||||||
elements.detailBodyEl.innerHTML = "";
|
}));
|
||||||
elements.detailBodyEl.appendChild(buildFourWorldsCard(tree));
|
|
||||||
elements.detailBodyEl.appendChild(buildPlanetLuminaryCard(seph.planet));
|
|
||||||
elements.detailBodyEl.appendChild(metaCard("Intelligence", seph.intelligence));
|
|
||||||
elements.detailBodyEl.appendChild(buildTarotAttributionCard(seph.tarot));
|
|
||||||
|
|
||||||
if (seph.description) {
|
|
||||||
elements.detailBodyEl.appendChild(
|
|
||||||
metaCard(seph.name, seph.description, true)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quick-access chips for connected paths
|
|
||||||
const connected = tree.paths.filter(
|
|
||||||
p => p.connects.from === seph.number || p.connects.to === seph.number
|
|
||||||
);
|
|
||||||
if (connected.length) {
|
|
||||||
const card = document.createElement("div");
|
|
||||||
card.className = "planet-meta-card kab-wide-card";
|
|
||||||
const chips = connected.map(p =>
|
|
||||||
`<span class="kab-chip" data-path="${p.pathNumber}" role="button" tabindex="0" title="Path ${p.pathNumber}: ${p.tarot?.card || ""}">`
|
|
||||||
+ `${p.hebrewLetter?.char || ""} <span class="kab-chip-sub">${p.pathNumber}</span>`
|
|
||||||
+ `</span>`
|
|
||||||
).join("");
|
|
||||||
card.innerHTML = `<strong>Connected Paths</strong><div class="kab-chips">${chips}</div>`;
|
|
||||||
elements.detailBodyEl.appendChild(card);
|
|
||||||
|
|
||||||
card.querySelectorAll(".kab-chip[data-path]").forEach(chip => {
|
|
||||||
const handler = () => {
|
|
||||||
const path = tree.paths.find(p => p.pathNumber === Number(chip.dataset.path));
|
|
||||||
if (path) renderPathDetail(path, tree, elements);
|
|
||||||
};
|
|
||||||
chip.addEventListener("click", handler);
|
|
||||||
chip.addEventListener("keydown", e => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handler(); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
appendGodsCards(seph.number, elements);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── render path detail ──────────────────────────────────────────────────────
|
|
||||||
function renderPathDetail(path, tree, elements) {
|
function renderPathDetail(path, tree, elements) {
|
||||||
state.selectedPathNumber = Number(path?.pathNumber);
|
state.selectedPathNumber = Number(path?.pathNumber);
|
||||||
state.selectedSephiraNumber = null;
|
state.selectedSephiraNumber = null;
|
||||||
@@ -948,70 +646,30 @@
|
|||||||
document.querySelectorAll(`[data-path="${path.pathNumber}"]`)
|
document.querySelectorAll(`[data-path="${path.pathNumber}"]`)
|
||||||
.forEach(el => el.classList.add("kab-path-active"));
|
.forEach(el => el.classList.add("kab-path-active"));
|
||||||
|
|
||||||
const letter = path.hebrewLetter || {};
|
if (typeof kabbalahDetailUi.renderPathDetail === "function") {
|
||||||
const fromName = tree.sephiroth.find(s => s.number === path.connects.from)?.name || path.connects.from;
|
kabbalahDetailUi.renderPathDetail(getDetailRenderContext(tree, elements, {
|
||||||
const toName = tree.sephiroth.find(s => s.number === path.connects.to)?.name || path.connects.to;
|
path,
|
||||||
const astro = path.astrology ? `${path.astrology.name} (${path.astrology.type})` : "—";
|
activeHebrewToken: normalizeLetterToken(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char || "")
|
||||||
const tarotStr = path.tarot?.card
|
}));
|
||||||
? `${path.tarot.card}${path.tarot.trumpNumber != null ? " · Trump " + path.tarot.trumpNumber : ""}`
|
|
||||||
: "—";
|
|
||||||
|
|
||||||
elements.detailNameEl.textContent =
|
|
||||||
`Path ${path.pathNumber} · ${letter.char || ""} ${letter.transliteration || ""}`;
|
|
||||||
elements.detailSubEl.textContent = [path.tarot?.card, astro].filter(Boolean).join(" · ");
|
|
||||||
|
|
||||||
elements.detailBodyEl.innerHTML = "";
|
|
||||||
elements.detailBodyEl.appendChild(buildFourWorldsCard(tree, letter.transliteration || letter.char || ""));
|
|
||||||
elements.detailBodyEl.appendChild(buildConnectsCard(path, fromName, toName));
|
|
||||||
elements.detailBodyEl.appendChild(buildHebrewLetterCard(letter));
|
|
||||||
elements.detailBodyEl.appendChild(buildAstrologyCard(path.astrology));
|
|
||||||
|
|
||||||
// Tarot card — clickable if a trump card is associated
|
|
||||||
const tarotMetaCard = document.createElement("div");
|
|
||||||
tarotMetaCard.className = "planet-meta-card";
|
|
||||||
const tarotLabel = document.createElement("strong");
|
|
||||||
tarotLabel.textContent = "Tarot";
|
|
||||||
tarotMetaCard.appendChild(tarotLabel);
|
|
||||||
if (path.tarot?.card && path.tarot.trumpNumber != null) {
|
|
||||||
const tarotBtn = document.createElement("button");
|
|
||||||
tarotBtn.type = "button";
|
|
||||||
tarotBtn.className = "kab-tarot-link";
|
|
||||||
tarotBtn.textContent = `${path.tarot.card} · Trump ${path.tarot.trumpNumber}`;
|
|
||||||
tarotBtn.title = "Open in Tarot section";
|
|
||||||
tarotBtn.addEventListener("click", () => {
|
|
||||||
document.dispatchEvent(new CustomEvent("kab:view-trump", {
|
|
||||||
detail: { trumpNumber: path.tarot.trumpNumber }
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
tarotMetaCard.appendChild(tarotBtn);
|
|
||||||
} else {
|
|
||||||
const tarotP = document.createElement("p");
|
|
||||||
tarotP.className = "planet-text";
|
|
||||||
tarotP.textContent = tarotStr || "—";
|
|
||||||
tarotMetaCard.appendChild(tarotP);
|
|
||||||
}
|
}
|
||||||
elements.detailBodyEl.appendChild(tarotMetaCard);
|
|
||||||
|
|
||||||
elements.detailBodyEl.appendChild(metaCard("Intelligence", path.intelligence));
|
|
||||||
elements.detailBodyEl.appendChild(metaCard("Pillar", path.pillar));
|
|
||||||
|
|
||||||
if (path.description) {
|
|
||||||
const desc = document.createElement("div");
|
|
||||||
desc.className = "planet-meta-card kab-wide-card";
|
|
||||||
desc.innerHTML =
|
|
||||||
`<strong>Path ${path.pathNumber} — Sefer Yetzirah</strong>`
|
|
||||||
+ `<p class="planet-text">${path.description.replace(/\n/g, "<br><br>")}</p>`;
|
|
||||||
elements.detailBodyEl.appendChild(desc);
|
|
||||||
}
|
|
||||||
|
|
||||||
appendGodsCards(path.pathNumber, elements);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindTreeInteractions(svg, tree, elements) {
|
function bindTreeInteractions(svg, tree, elements) {
|
||||||
// Delegate clicks via element's own data attributes
|
// Delegate clicks via element's own data attributes
|
||||||
svg.addEventListener("click", e => {
|
svg.addEventListener("click", e => {
|
||||||
const sephNum = e.target.dataset?.sephira;
|
const clickTarget = e.target instanceof Element ? e.target : null;
|
||||||
const pathNum = e.target.dataset?.path;
|
const sephNum = clickTarget?.dataset?.sephira;
|
||||||
|
const pathNum = clickTarget?.dataset?.path;
|
||||||
|
|
||||||
|
if (pathNum != null && clickTarget?.classList?.contains("kab-path-tarot")) {
|
||||||
|
const p = tree.paths.find(x => x.pathNumber === Number(pathNum));
|
||||||
|
if (p) {
|
||||||
|
openTarotLightboxForPath(p, getSvgImageHref(clickTarget));
|
||||||
|
renderPathDetail(p, tree, elements);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (sephNum != null) {
|
if (sephNum != null) {
|
||||||
const s = tree.sephiroth.find(x => x.number === Number(sephNum));
|
const s = tree.sephiroth.find(x => x.number === Number(sephNum));
|
||||||
if (s) renderSephiraDetail(s, tree, elements);
|
if (s) renderSephiraDetail(s, tree, elements);
|
||||||
@@ -1027,7 +685,12 @@
|
|||||||
if (e.key === "Enter" || e.key === " ") {
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const p = tree.paths.find(x => x.pathNumber === Number(el.dataset.path));
|
const p = tree.paths.find(x => x.pathNumber === Number(el.dataset.path));
|
||||||
if (p) renderPathDetail(p, tree, elements);
|
if (p) {
|
||||||
|
if (el.classList.contains("kab-path-tarot")) {
|
||||||
|
openTarotLightboxForPath(p, getSvgImageHref(el));
|
||||||
|
}
|
||||||
|
renderPathDetail(p, tree, elements);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1044,6 +707,94 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bindRoseCrossInteractions(svg, tree, roseElements) {
|
||||||
|
if (!svg || !roseElements?.detailBodyEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPathFromTarget = (targetEl) => {
|
||||||
|
if (!(targetEl instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const petal = targetEl.closest(".kab-rose-petal[data-path]");
|
||||||
|
if (!(petal instanceof SVGElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathNumber = Number(petal.dataset.path);
|
||||||
|
if (!Number.isFinite(pathNumber)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = tree.paths.find((entry) => entry.pathNumber === pathNumber);
|
||||||
|
if (path) {
|
||||||
|
renderPathDetail(path, tree, roseElements);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
svg.addEventListener("click", (event) => {
|
||||||
|
openPathFromTarget(event.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.querySelectorAll(".kab-rose-petal[data-path]").forEach((petal) => {
|
||||||
|
petal.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
openPathFromTarget(petal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRoseLandingIntro(roseElements) {
|
||||||
|
if (typeof kabbalahDetailUi.renderRoseLandingIntro === "function") {
|
||||||
|
kabbalahDetailUi.renderRoseLandingIntro(roseElements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRoseCross(elements) {
|
||||||
|
if (!state.tree || !elements?.roseCrossContainerEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roseElements = getRoseDetailElements(elements);
|
||||||
|
if (!roseElements?.detailBodyEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roseBuilder = window.KabbalahRosicrucianCross?.buildRosicrucianCrossSVG;
|
||||||
|
if (typeof roseBuilder !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roseSvg = roseBuilder(state.tree);
|
||||||
|
elements.roseCrossContainerEl.innerHTML = "";
|
||||||
|
elements.roseCrossContainerEl.appendChild(roseSvg);
|
||||||
|
bindRoseCrossInteractions(roseSvg, state.tree, roseElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRoseCurrentSelection(elements) {
|
||||||
|
if (!state.tree) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roseElements = getRoseDetailElements(elements);
|
||||||
|
if (!roseElements?.detailBodyEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isFinite(Number(state.selectedPathNumber))) {
|
||||||
|
const selectedPath = state.tree.paths.find((entry) => entry.pathNumber === Number(state.selectedPathNumber));
|
||||||
|
if (selectedPath) {
|
||||||
|
renderPathDetail(selectedPath, state.tree, roseElements);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRoseLandingIntro(roseElements);
|
||||||
|
}
|
||||||
|
|
||||||
function renderTree(elements) {
|
function renderTree(elements) {
|
||||||
if (!state.tree || !elements?.treeContainerEl) {
|
if (!state.tree || !elements?.treeContainerEl) {
|
||||||
return;
|
return;
|
||||||
@@ -1119,6 +870,8 @@
|
|||||||
|
|
||||||
renderTree(elements);
|
renderTree(elements);
|
||||||
renderCurrentSelection(elements);
|
renderCurrentSelection(elements);
|
||||||
|
renderRoseCross(elements);
|
||||||
|
renderRoseCurrentSelection(elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPathByNumber(pathNumber) {
|
function selectPathByNumber(pathNumber) {
|
||||||
|
|||||||
402
app/ui-navigation.js
Normal file
402
app/ui-navigation.js
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let config = {};
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
function getActiveSection() {
|
||||||
|
return typeof config.getActiveSection === "function"
|
||||||
|
? config.getActiveSection()
|
||||||
|
: "home";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveSection(section) {
|
||||||
|
config.setActiveSection?.(section);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReferenceData() {
|
||||||
|
return config.getReferenceData?.() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMagickDataset() {
|
||||||
|
return config.getMagickDataset?.() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindClick(element, handler) {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.addEventListener("click", handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindTopLevelNavButtons() {
|
||||||
|
const elements = config.elements || {};
|
||||||
|
|
||||||
|
bindClick(elements.openTarotEl, () => {
|
||||||
|
if (getActiveSection() === "tarot") {
|
||||||
|
setActiveSection("home");
|
||||||
|
} else {
|
||||||
|
setActiveSection("tarot");
|
||||||
|
config.tarotSpreadUi?.showCardsView?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openAstronomyEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "astronomy" ? "home" : "astronomy");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openPlanetsEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "planets" ? "home" : "planets");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openCyclesEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "cycles" ? "home" : "cycles");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openElementsEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "elements" ? "home" : "elements");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openIChingEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "iching" ? "home" : "iching");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openKabbalahEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "kabbalah" ? "home" : "kabbalah");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openKabbalahTreeEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "kabbalah-tree" ? "home" : "kabbalah-tree");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openKabbalahCubeEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "cube" ? "home" : "cube");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openAlphabetEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "alphabet" ? "home" : "alphabet");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openNumbersEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "numbers" ? "home" : "numbers");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openZodiacEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "zodiac" ? "home" : "zodiac");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openNatalEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "natal" ? "home" : "natal");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openQuizEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "quiz" ? "home" : "quiz");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openGodsEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "gods" ? "home" : "gods");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openEnochianEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "enochian" ? "home" : "enochian");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openCalendarEl, () => {
|
||||||
|
const activeSection = getActiveSection();
|
||||||
|
const isCalendarMenuActive = activeSection === "calendar" || activeSection === "holidays";
|
||||||
|
setActiveSection(isCalendarMenuActive ? "home" : "calendar");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openCalendarMonthsEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "calendar" ? "home" : "calendar");
|
||||||
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openHolidaysEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "holidays" ? "home" : "holidays");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindCustomNavEvents() {
|
||||||
|
const ensure = config.ensure || {};
|
||||||
|
|
||||||
|
document.addEventListener("nav:cube", (event) => {
|
||||||
|
const referenceData = getReferenceData();
|
||||||
|
const magickDataset = getMagickDataset();
|
||||||
|
if (typeof ensure.ensureCubeSection === "function" && magickDataset) {
|
||||||
|
ensure.ensureCubeSection(magickDataset, referenceData);
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveSection("cube");
|
||||||
|
|
||||||
|
const detail = event?.detail || {};
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const ui = window.CubeSectionUi;
|
||||||
|
const selected = ui?.selectPlacement?.(detail);
|
||||||
|
if (!selected && detail?.wallId) {
|
||||||
|
ui?.selectWallById?.(detail.wallId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("nav:zodiac", (event) => {
|
||||||
|
const referenceData = getReferenceData();
|
||||||
|
const magickDataset = getMagickDataset();
|
||||||
|
if (typeof ensure.ensureZodiacSection === "function" && referenceData && magickDataset) {
|
||||||
|
ensure.ensureZodiacSection(referenceData, magickDataset);
|
||||||
|
}
|
||||||
|
setActiveSection("zodiac");
|
||||||
|
const signId = event?.detail?.signId;
|
||||||
|
if (signId) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.ZodiacSectionUi?.selectBySignId?.(signId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("nav:alphabet", (event) => {
|
||||||
|
const referenceData = getReferenceData();
|
||||||
|
const magickDataset = getMagickDataset();
|
||||||
|
if (typeof ensure.ensureAlphabetSection === "function" && magickDataset) {
|
||||||
|
ensure.ensureAlphabetSection(magickDataset, referenceData);
|
||||||
|
}
|
||||||
|
setActiveSection("alphabet");
|
||||||
|
|
||||||
|
const alphabet = event?.detail?.alphabet;
|
||||||
|
const hebrewLetterId = event?.detail?.hebrewLetterId;
|
||||||
|
const greekName = event?.detail?.greekName;
|
||||||
|
const englishLetter = event?.detail?.englishLetter;
|
||||||
|
const arabicName = event?.detail?.arabicName;
|
||||||
|
const enochianId = event?.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", (event) => {
|
||||||
|
const rawValue = event?.detail?.value;
|
||||||
|
const normalizedValue = typeof config.normalizeNumberValue === "function"
|
||||||
|
? config.normalizeNumberValue(rawValue)
|
||||||
|
: 0;
|
||||||
|
if (normalizedValue === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveSection("numbers");
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (typeof config.selectNumberEntry === "function") {
|
||||||
|
config.selectNumberEntry(normalizedValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("nav:iching", (event) => {
|
||||||
|
const referenceData = getReferenceData();
|
||||||
|
if (typeof ensure.ensureIChingSection === "function" && referenceData) {
|
||||||
|
ensure.ensureIChingSection(referenceData);
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveSection("iching");
|
||||||
|
|
||||||
|
const hexagramNumber = event?.detail?.hexagramNumber;
|
||||||
|
const planetaryInfluence = event?.detail?.planetaryInfluence;
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const ui = window.IChingSectionUi;
|
||||||
|
if (hexagramNumber != null) {
|
||||||
|
ui?.selectByHexagramNumber?.(hexagramNumber);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (planetaryInfluence) {
|
||||||
|
ui?.selectByPlanetaryInfluence?.(planetaryInfluence);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("nav:gods", (event) => {
|
||||||
|
const referenceData = getReferenceData();
|
||||||
|
const magickDataset = getMagickDataset();
|
||||||
|
if (typeof ensure.ensureGodsSection === "function" && magickDataset) {
|
||||||
|
ensure.ensureGodsSection(magickDataset, referenceData);
|
||||||
|
}
|
||||||
|
setActiveSection("gods");
|
||||||
|
const godId = event?.detail?.godId;
|
||||||
|
const godName = event?.detail?.godName;
|
||||||
|
const pathNo = event?.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", (event) => {
|
||||||
|
const referenceData = getReferenceData();
|
||||||
|
const magickDataset = getMagickDataset();
|
||||||
|
const calendarId = event?.detail?.calendarId;
|
||||||
|
const monthId = event?.detail?.monthId;
|
||||||
|
if (!monthId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof ensure.ensureCalendarSection === "function" && referenceData) {
|
||||||
|
ensure.ensureCalendarSection(referenceData, magickDataset);
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveSection("calendar");
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (calendarId) {
|
||||||
|
window.CalendarSectionUi?.selectCalendarType?.(calendarId);
|
||||||
|
}
|
||||||
|
window.CalendarSectionUi?.selectByMonthId?.(monthId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("nav:kabbalah-path", (event) => {
|
||||||
|
const magickDataset = getMagickDataset();
|
||||||
|
const pathNo = event?.detail?.pathNo;
|
||||||
|
if (typeof ensure.ensureKabbalahSection === "function" && magickDataset) {
|
||||||
|
ensure.ensureKabbalahSection(magickDataset);
|
||||||
|
}
|
||||||
|
setActiveSection("kabbalah-tree");
|
||||||
|
if (pathNo != null) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.KabbalahSectionUi?.selectNode?.(pathNo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("nav:planet", (event) => {
|
||||||
|
const referenceData = getReferenceData();
|
||||||
|
const magickDataset = getMagickDataset();
|
||||||
|
const planetId = event?.detail?.planetId;
|
||||||
|
if (!planetId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof ensure.ensurePlanetSection === "function" && referenceData) {
|
||||||
|
ensure.ensurePlanetSection(referenceData, magickDataset);
|
||||||
|
}
|
||||||
|
setActiveSection("planets");
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.PlanetSectionUi?.selectByPlanetId?.(planetId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("nav:elements", (event) => {
|
||||||
|
const magickDataset = getMagickDataset();
|
||||||
|
const elementId = event?.detail?.elementId;
|
||||||
|
if (!elementId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof ensure.ensureElementsSection === "function" && magickDataset) {
|
||||||
|
ensure.ensureElementsSection(magickDataset);
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveSection("elements");
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.ElementsSectionUi?.selectByElementId?.(elementId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("nav:tarot-trump", (event) => {
|
||||||
|
const referenceData = getReferenceData();
|
||||||
|
const magickDataset = getMagickDataset();
|
||||||
|
if (typeof ensure.ensureTarotSection === "function" && referenceData) {
|
||||||
|
ensure.ensureTarotSection(referenceData, magickDataset);
|
||||||
|
}
|
||||||
|
setActiveSection("tarot");
|
||||||
|
const { trumpNumber, cardName } = event?.detail || {};
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (trumpNumber != null) {
|
||||||
|
window.TarotSectionUi?.selectCardByTrump?.(trumpNumber);
|
||||||
|
} else if (cardName) {
|
||||||
|
window.TarotSectionUi?.selectCardByName?.(cardName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("kab:view-trump", (event) => {
|
||||||
|
const referenceData = getReferenceData();
|
||||||
|
const magickDataset = getMagickDataset();
|
||||||
|
setActiveSection("tarot");
|
||||||
|
const trumpNumber = event?.detail?.trumpNumber;
|
||||||
|
if (trumpNumber != null) {
|
||||||
|
if (typeof ensure.ensureTarotSection === "function" && referenceData) {
|
||||||
|
ensure.ensureTarotSection(referenceData, magickDataset);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.TarotSectionUi?.selectCardByTrump?.(trumpNumber);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("tarot:view-kab-path", (event) => {
|
||||||
|
setActiveSection("kabbalah-tree");
|
||||||
|
const pathNumber = event?.detail?.pathNumber;
|
||||||
|
if (pathNumber != null) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const kabbalahUi = window.KabbalahSectionUi;
|
||||||
|
if (typeof kabbalahUi?.selectNode === "function") {
|
||||||
|
kabbalahUi.selectNode(pathNumber);
|
||||||
|
} else {
|
||||||
|
kabbalahUi?.selectPathByNumber?.(pathNumber);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(nextConfig = {}) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...nextConfig,
|
||||||
|
elements: {
|
||||||
|
...(config.elements || {}),
|
||||||
|
...(nextConfig.elements || {})
|
||||||
|
},
|
||||||
|
ensure: {
|
||||||
|
...(config.ensure || {}),
|
||||||
|
...(nextConfig.ensure || {})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
bindTopLevelNavButtons();
|
||||||
|
bindCustomNavEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotNavigationUi = {
|
||||||
|
...(window.TarotNavigationUi || {}),
|
||||||
|
init
|
||||||
|
};
|
||||||
|
})();
|
||||||
932
app/ui-numbers.js
Normal file
932
app/ui-numbers.js
Normal file
@@ -0,0 +1,932 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
let activeNumberValue = 0;
|
||||||
|
let config = {
|
||||||
|
getReferenceData: () => null,
|
||||||
|
getMagickDataset: () => null,
|
||||||
|
ensureTarotSection: null
|
||||||
|
};
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
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 }
|
||||||
|
];
|
||||||
|
|
||||||
|
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 getReferenceData() {
|
||||||
|
return typeof config.getReferenceData === "function" ? config.getReferenceData() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMagickDataset() {
|
||||||
|
return typeof config.getMagickDataset === "function" ? config.getMagickDataset() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getElements() {
|
||||||
|
return {
|
||||||
|
countEl: document.getElementById("numbers-count"),
|
||||||
|
listEl: document.getElementById("numbers-list"),
|
||||||
|
detailNameEl: document.getElementById("numbers-detail-name"),
|
||||||
|
detailTypeEl: document.getElementById("numbers-detail-type"),
|
||||||
|
detailSummaryEl: document.getElementById("numbers-detail-summary"),
|
||||||
|
detailBodyEl: document.getElementById("numbers-detail-body"),
|
||||||
|
specialPanelEl: document.getElementById("numbers-special-panel")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = getMagickDataset()?.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 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 getCalendarMonthLinksForNumber(value) {
|
||||||
|
const referenceData = getReferenceData();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = getMagickDataset()?.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) {
|
||||||
|
const { specialPanelEl } = getElements();
|
||||||
|
if (!specialPanelEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = getNumberEntryByValue(value);
|
||||||
|
const playingSuit = entry?.associations?.playingSuit || "hearts";
|
||||||
|
const boardCardEl = buildNumbersSpecialCardSlots(playingSuit);
|
||||||
|
specialPanelEl.replaceChildren(boardCardEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = getMagickDataset()?.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) {
|
||||||
|
const referenceData = getReferenceData();
|
||||||
|
const magickDataset = getMagickDataset();
|
||||||
|
if (typeof config.ensureTarotSection === "function" && referenceData) {
|
||||||
|
config.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() {
|
||||||
|
const { listEl, countEl } = getElements();
|
||||||
|
if (!listEl) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
listEl.replaceChildren(fragment);
|
||||||
|
if (countEl) {
|
||||||
|
countEl.textContent = `${entries.length} entries`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNumberDetail(value) {
|
||||||
|
const { detailNameEl, detailTypeEl, detailSummaryEl, detailBodyEl } = getElements();
|
||||||
|
const entry = getNumberEntryByValue(value);
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = entry.value;
|
||||||
|
const opposite = entry.opposite;
|
||||||
|
const rootTarget = normalizeNumberValue(entry.digitalRoot);
|
||||||
|
|
||||||
|
if (detailNameEl) {
|
||||||
|
detailNameEl.textContent = `Number ${normalized} · ${entry.label}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detailTypeEl) {
|
||||||
|
detailTypeEl.textContent = `Opposite: ${opposite}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detailSummaryEl) {
|
||||||
|
detailSummaryEl.textContent = entry.summary || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNumbersSpecialPanel(normalized);
|
||||||
|
|
||||||
|
if (!detailBodyEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
detailBodyEl.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);
|
||||||
|
|
||||||
|
detailBodyEl.append(pairCardEl, kabbalahCardEl, alphabetCardEl, tarotCardEl, calendarCardEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNumberEntry(value) {
|
||||||
|
const entry = getNumberEntryByValue(value);
|
||||||
|
activeNumberValue = entry ? entry.value : 0;
|
||||||
|
renderNumbersList();
|
||||||
|
renderNumberDetail(activeNumberValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNumbersSection() {
|
||||||
|
const { listEl } = getElements();
|
||||||
|
if (!listEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
listEl.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);
|
||||||
|
});
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNumbersList();
|
||||||
|
renderNumberDetail(activeNumberValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(nextConfig = {}) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...nextConfig
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotNumbersUi = {
|
||||||
|
...(window.TarotNumbersUi || {}),
|
||||||
|
init,
|
||||||
|
ensureNumbersSection,
|
||||||
|
selectNumberEntry,
|
||||||
|
normalizeNumberValue
|
||||||
|
};
|
||||||
|
})();
|
||||||
947
app/ui-quiz-bank.js
Normal file
947
app/ui-quiz-bank.js
Normal file
@@ -0,0 +1,947 @@
|
|||||||
|
/* ui-quiz-bank.js — Built-in quiz question bank generation */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function toTitleCase(value) {
|
||||||
|
const text = String(value || "").trim().toLowerCase();
|
||||||
|
if (!text) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOption(value) {
|
||||||
|
return String(value || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeKey(value) {
|
||||||
|
return normalizeOption(value).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUniqueOptionList(values) {
|
||||||
|
const seen = new Set();
|
||||||
|
const unique = [];
|
||||||
|
|
||||||
|
(values || []).forEach((value) => {
|
||||||
|
const formatted = normalizeOption(value);
|
||||||
|
if (!formatted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = normalizeKey(formatted);
|
||||||
|
if (seen.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(key);
|
||||||
|
unique.push(formatted);
|
||||||
|
});
|
||||||
|
|
||||||
|
return unique;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDifficultyValue(valueByDifficulty, difficulty = "normal") {
|
||||||
|
if (valueByDifficulty == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof valueByDifficulty !== "object" || Array.isArray(valueByDifficulty)) {
|
||||||
|
return valueByDifficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(valueByDifficulty, difficulty)) {
|
||||||
|
return valueByDifficulty[difficulty];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(valueByDifficulty, "normal")) {
|
||||||
|
return valueByDifficulty.normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(valueByDifficulty, "easy")) {
|
||||||
|
return valueByDifficulty.easy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(valueByDifficulty, "hard")) {
|
||||||
|
return valueByDifficulty.hard;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function createQuestionTemplate(payload, poolValues) {
|
||||||
|
const key = String(payload?.key || "").trim();
|
||||||
|
const promptByDifficulty = payload?.promptByDifficulty ?? payload?.prompt;
|
||||||
|
const answerByDifficulty = payload?.answerByDifficulty ?? payload?.answer;
|
||||||
|
const poolByDifficulty = poolValues;
|
||||||
|
const categoryId = String(payload?.categoryId || "").trim();
|
||||||
|
const category = String(payload?.category || "Correspondence").trim();
|
||||||
|
|
||||||
|
const defaultPrompt = String(resolveDifficultyValue(promptByDifficulty, "normal") || "").trim();
|
||||||
|
const defaultAnswer = normalizeOption(resolveDifficultyValue(answerByDifficulty, "normal"));
|
||||||
|
const defaultPool = toUniqueOptionList(resolveDifficultyValue(poolByDifficulty, "normal") || []);
|
||||||
|
|
||||||
|
if (!key || !defaultPrompt || !defaultAnswer || !categoryId || !category) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defaultPool.some((value) => normalizeKey(value) === normalizeKey(defaultAnswer))) {
|
||||||
|
defaultPool.push(defaultAnswer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const distractorCount = defaultPool.filter((value) => normalizeKey(value) !== normalizeKey(defaultAnswer)).length;
|
||||||
|
if (distractorCount < 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
categoryId,
|
||||||
|
category,
|
||||||
|
promptByDifficulty,
|
||||||
|
answerByDifficulty,
|
||||||
|
poolByDifficulty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQuestionBank(referenceData, magickDataset, dynamicCategoryRegistry) {
|
||||||
|
const grouped = magickDataset?.grouped || {};
|
||||||
|
const alphabets = grouped.alphabets || {};
|
||||||
|
const englishLetters = Array.isArray(alphabets?.english) ? alphabets.english : [];
|
||||||
|
const hebrewLetters = Array.isArray(alphabets?.hebrew) ? alphabets.hebrew : [];
|
||||||
|
const kabbalahTree = grouped?.kabbalah?.["kabbalah-tree"] || {};
|
||||||
|
const treePaths = Array.isArray(kabbalahTree?.paths) ? kabbalahTree.paths : [];
|
||||||
|
const treeSephiroth = Array.isArray(kabbalahTree?.sephiroth) ? kabbalahTree.sephiroth : [];
|
||||||
|
const sephirotById = grouped?.kabbalah?.sephirot && typeof grouped.kabbalah.sephirot === "object"
|
||||||
|
? grouped.kabbalah.sephirot
|
||||||
|
: {};
|
||||||
|
const cube = grouped?.kabbalah?.cube && typeof grouped.kabbalah.cube === "object"
|
||||||
|
? grouped.kabbalah.cube
|
||||||
|
: {};
|
||||||
|
const cubeWalls = Array.isArray(cube?.walls) ? cube.walls : [];
|
||||||
|
const cubeEdges = Array.isArray(cube?.edges) ? cube.edges : [];
|
||||||
|
const cubeCenter = cube?.center && typeof cube.center === "object" ? cube.center : null;
|
||||||
|
const playingCardsData = grouped?.["playing-cards-52"];
|
||||||
|
const playingCards = Array.isArray(playingCardsData)
|
||||||
|
? playingCardsData
|
||||||
|
: (Array.isArray(playingCardsData?.entries) ? playingCardsData.entries : []);
|
||||||
|
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
||||||
|
const planetsById = referenceData?.planets && typeof referenceData.planets === "object"
|
||||||
|
? referenceData.planets
|
||||||
|
: {};
|
||||||
|
const planets = Object.values(planetsById);
|
||||||
|
const decansBySign = referenceData?.decansBySign && typeof referenceData.decansBySign === "object"
|
||||||
|
? referenceData.decansBySign
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const normalizeId = (value) => String(value || "").trim().toLowerCase();
|
||||||
|
|
||||||
|
const toRomanNumeral = (value) => {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||||
|
return String(value || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const intValue = Math.trunc(numeric);
|
||||||
|
const lookup = [
|
||||||
|
[1000, "M"],
|
||||||
|
[900, "CM"],
|
||||||
|
[500, "D"],
|
||||||
|
[400, "CD"],
|
||||||
|
[100, "C"],
|
||||||
|
[90, "XC"],
|
||||||
|
[50, "L"],
|
||||||
|
[40, "XL"],
|
||||||
|
[10, "X"],
|
||||||
|
[9, "IX"],
|
||||||
|
[5, "V"],
|
||||||
|
[4, "IV"],
|
||||||
|
[1, "I"]
|
||||||
|
];
|
||||||
|
|
||||||
|
let current = intValue;
|
||||||
|
let result = "";
|
||||||
|
lookup.forEach(([size, symbol]) => {
|
||||||
|
while (current >= size) {
|
||||||
|
result += symbol;
|
||||||
|
current -= size;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result || String(intValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelFromId = (value) => {
|
||||||
|
const id = String(value || "").trim();
|
||||||
|
if (!id) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
.replace(/[_-]+/g, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim()
|
||||||
|
.split(" ")
|
||||||
|
.map((part) => part ? part.charAt(0).toUpperCase() + part.slice(1) : "")
|
||||||
|
.join(" ");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlanetLabelById = (planetId) => {
|
||||||
|
const normalized = normalizeId(planetId);
|
||||||
|
if (!normalized) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const directPlanet = planetsById[normalized];
|
||||||
|
if (directPlanet?.name) {
|
||||||
|
return directPlanet.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === "primum-mobile") {
|
||||||
|
return "Primum Mobile";
|
||||||
|
}
|
||||||
|
if (normalized === "olam-yesodot") {
|
||||||
|
return "Earth / Elements";
|
||||||
|
}
|
||||||
|
|
||||||
|
return labelFromId(normalized);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hebrewById = new Map(
|
||||||
|
hebrewLetters
|
||||||
|
.filter((entry) => entry?.hebrewLetterId)
|
||||||
|
.map((entry) => [normalizeId(entry.hebrewLetterId), entry])
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatHebrewLetterLabel = (entry, fallbackId = "") => {
|
||||||
|
if (entry?.name && entry?.char) {
|
||||||
|
return `${entry.name} (${entry.char})`;
|
||||||
|
}
|
||||||
|
if (entry?.name) {
|
||||||
|
return entry.name;
|
||||||
|
}
|
||||||
|
if (entry?.char) {
|
||||||
|
return entry.char;
|
||||||
|
}
|
||||||
|
return labelFromId(fallbackId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sephiraNameByNumber = new Map(
|
||||||
|
treeSephiroth
|
||||||
|
.filter((entry) => Number.isFinite(Number(entry?.number)) && entry?.name)
|
||||||
|
.map((entry) => [Math.trunc(Number(entry.number)), String(entry.name)])
|
||||||
|
);
|
||||||
|
|
||||||
|
const sephiraNameById = new Map(
|
||||||
|
treeSephiroth
|
||||||
|
.filter((entry) => entry?.sephiraId && entry?.name)
|
||||||
|
.map((entry) => [normalizeId(entry.sephiraId), String(entry.name)])
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSephiraName = (numberValue, idValue) => {
|
||||||
|
const numberKey = Number(numberValue);
|
||||||
|
if (Number.isFinite(numberKey)) {
|
||||||
|
const byNumber = sephiraNameByNumber.get(Math.trunc(numberKey));
|
||||||
|
if (byNumber) {
|
||||||
|
return byNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const byId = sephiraNameById.get(normalizeId(idValue));
|
||||||
|
if (byId) {
|
||||||
|
return byId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isFinite(numberKey)) {
|
||||||
|
return `Sephira ${Math.trunc(numberKey)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return labelFromId(idValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPathLetter = (path) => {
|
||||||
|
const transliteration = String(path?.hebrewLetter?.transliteration || "").trim();
|
||||||
|
const glyph = String(path?.hebrewLetter?.char || "").trim();
|
||||||
|
|
||||||
|
if (transliteration && glyph) {
|
||||||
|
return `${transliteration} (${glyph})`;
|
||||||
|
}
|
||||||
|
if (transliteration) {
|
||||||
|
return transliteration;
|
||||||
|
}
|
||||||
|
if (glyph) {
|
||||||
|
return glyph;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenDecans = Object.values(decansBySign)
|
||||||
|
.flatMap((entries) => (Array.isArray(entries) ? entries : []));
|
||||||
|
|
||||||
|
const signNameById = new Map(
|
||||||
|
signs
|
||||||
|
.filter((entry) => entry?.id && entry?.name)
|
||||||
|
.map((entry) => [normalizeId(entry.id), String(entry.name)])
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatDecanLabel = (decan) => {
|
||||||
|
const signName = signNameById.get(normalizeId(decan?.signId)) || labelFromId(decan?.signId);
|
||||||
|
const index = Number(decan?.index);
|
||||||
|
if (!signName || !Number.isFinite(index)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `${signName} Decan ${toRomanNumeral(index)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bank = [];
|
||||||
|
|
||||||
|
const englishGematriaPool = englishLetters
|
||||||
|
.map((item) => (Number.isFinite(Number(item?.pythagorean)) ? String(item.pythagorean) : ""))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const hebrewNumerologyPool = hebrewLetters
|
||||||
|
.map((item) => (Number.isFinite(Number(item?.numerology)) ? String(item.numerology) : ""))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const hebrewNameAndCharPool = hebrewLetters
|
||||||
|
.filter((item) => item?.name && item?.char)
|
||||||
|
.map((item) => `${item.name} (${item.char})`);
|
||||||
|
|
||||||
|
const hebrewCharPool = hebrewLetters
|
||||||
|
.map((item) => item?.char)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const planetNamePool = planets
|
||||||
|
.map((planet) => planet?.name)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const planetWeekdayPool = planets
|
||||||
|
.map((planet) => planet?.weekday)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const zodiacElementPool = signs
|
||||||
|
.map((sign) => toTitleCase(sign?.element))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const zodiacTarotPool = signs
|
||||||
|
.map((sign) => sign?.tarot?.majorArcana)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const pathNumberPool = toUniqueOptionList(
|
||||||
|
treePaths
|
||||||
|
.map((path) => {
|
||||||
|
const pathNo = Number(path?.pathNumber);
|
||||||
|
return Number.isFinite(pathNo) ? String(Math.trunc(pathNo)) : "";
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const pathLetterPool = toUniqueOptionList(treePaths.map((path) => formatPathLetter(path)));
|
||||||
|
const pathTarotPool = toUniqueOptionList(treePaths.map((path) => normalizeOption(path?.tarot?.card)));
|
||||||
|
|
||||||
|
const decanLabelPool = toUniqueOptionList(flattenDecans.map((decan) => formatDecanLabel(decan)));
|
||||||
|
const decanRulerPool = toUniqueOptionList(
|
||||||
|
flattenDecans.map((decan) => getPlanetLabelById(decan?.rulerPlanetId))
|
||||||
|
);
|
||||||
|
|
||||||
|
const cubeWallLabelPool = toUniqueOptionList(
|
||||||
|
cubeWalls.map((wall) => `${String(wall?.name || labelFromId(wall?.id)).trim()} Wall`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const cubeEdgeLabelPool = toUniqueOptionList(
|
||||||
|
cubeEdges.map((edge) => `${String(edge?.name || labelFromId(edge?.id)).trim()} Edge`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const cubeLocationPool = toUniqueOptionList([
|
||||||
|
...cubeWallLabelPool,
|
||||||
|
...cubeEdgeLabelPool,
|
||||||
|
"Center"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const cubeHebrewLetterPool = toUniqueOptionList([
|
||||||
|
...cubeWalls.map((wall) => {
|
||||||
|
const hebrew = hebrewById.get(normalizeId(wall?.hebrewLetterId));
|
||||||
|
return formatHebrewLetterLabel(hebrew, wall?.hebrewLetterId);
|
||||||
|
}),
|
||||||
|
...cubeEdges.map((edge) => {
|
||||||
|
const hebrew = hebrewById.get(normalizeId(edge?.hebrewLetterId));
|
||||||
|
return formatHebrewLetterLabel(hebrew, edge?.hebrewLetterId);
|
||||||
|
}),
|
||||||
|
formatHebrewLetterLabel(hebrewById.get(normalizeId(cubeCenter?.hebrewLetterId)), cubeCenter?.hebrewLetterId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const playingTarotPool = toUniqueOptionList(
|
||||||
|
playingCards.map((entry) => normalizeOption(entry?.tarotCard))
|
||||||
|
);
|
||||||
|
|
||||||
|
englishLetters.forEach((entry) => {
|
||||||
|
if (!entry?.letter || !Number.isFinite(Number(entry?.pythagorean))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `english-gematria:${entry.letter}`,
|
||||||
|
categoryId: "english-gematria",
|
||||||
|
category: "English Gematria",
|
||||||
|
promptByDifficulty: `${entry.letter} has a simple gematria value of`,
|
||||||
|
answerByDifficulty: String(entry.pythagorean)
|
||||||
|
},
|
||||||
|
englishGematriaPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
hebrewLetters.forEach((entry) => {
|
||||||
|
if (!entry?.name || !entry?.char || !Number.isFinite(Number(entry?.numerology))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `hebrew-number:${entry.hebrewLetterId || entry.name}`,
|
||||||
|
categoryId: "hebrew-numerology",
|
||||||
|
category: "Hebrew Gematria",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${entry.name} (${entry.char}) has a gematria value of`,
|
||||||
|
normal: `${entry.name} (${entry.char}) has a gematria value of`,
|
||||||
|
hard: `${entry.char} has a gematria value of`
|
||||||
|
},
|
||||||
|
answerByDifficulty: String(entry.numerology)
|
||||||
|
},
|
||||||
|
hebrewNumerologyPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
englishLetters.forEach((entry) => {
|
||||||
|
if (!entry?.letter || !entry?.hebrewLetterId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedHebrew = hebrewById.get(normalizeId(entry.hebrewLetterId));
|
||||||
|
if (!mappedHebrew?.name || !mappedHebrew?.char) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `english-hebrew:${entry.letter}`,
|
||||||
|
categoryId: "english-hebrew-mapping",
|
||||||
|
category: "Alphabet Mapping",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${entry.letter} maps to which Hebrew letter`,
|
||||||
|
normal: `${entry.letter} maps to which Hebrew letter`,
|
||||||
|
hard: `${entry.letter} maps to which Hebrew glyph`
|
||||||
|
},
|
||||||
|
answerByDifficulty: {
|
||||||
|
easy: `${mappedHebrew.name} (${mappedHebrew.char})`,
|
||||||
|
normal: `${mappedHebrew.name} (${mappedHebrew.char})`,
|
||||||
|
hard: mappedHebrew.char
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
easy: hebrewNameAndCharPool,
|
||||||
|
normal: hebrewNameAndCharPool,
|
||||||
|
hard: hebrewCharPool
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
signs.forEach((entry) => {
|
||||||
|
if (!entry?.name || !entry?.rulingPlanetId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rulerName = planetsById[normalizeId(entry.rulingPlanetId)]?.name;
|
||||||
|
if (!rulerName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `zodiac-ruler:${entry.id || entry.name}`,
|
||||||
|
categoryId: "zodiac-rulers",
|
||||||
|
category: "Zodiac Rulers",
|
||||||
|
promptByDifficulty: `${entry.name} is ruled by`,
|
||||||
|
answerByDifficulty: rulerName
|
||||||
|
},
|
||||||
|
planetNamePool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
signs.forEach((entry) => {
|
||||||
|
if (!entry?.name || !entry?.element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `zodiac-element:${entry.id || entry.name}`,
|
||||||
|
categoryId: "zodiac-elements",
|
||||||
|
category: "Zodiac Elements",
|
||||||
|
promptByDifficulty: `${entry.name} is`,
|
||||||
|
answerByDifficulty: toTitleCase(entry.element)
|
||||||
|
},
|
||||||
|
zodiacElementPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
planets.forEach((entry) => {
|
||||||
|
if (!entry?.name || !entry?.weekday) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `planet-weekday:${entry.id || entry.name}`,
|
||||||
|
categoryId: "planetary-weekdays",
|
||||||
|
category: "Planetary Weekdays",
|
||||||
|
promptByDifficulty: `${entry.name} corresponds to`,
|
||||||
|
answerByDifficulty: entry.weekday
|
||||||
|
},
|
||||||
|
planetWeekdayPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
signs.forEach((entry) => {
|
||||||
|
if (!entry?.name || !entry?.tarot?.majorArcana) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `zodiac-tarot:${entry.id || entry.name}`,
|
||||||
|
categoryId: "zodiac-tarot",
|
||||||
|
category: "Zodiac ↔ Tarot",
|
||||||
|
promptByDifficulty: `${entry.name} corresponds to`,
|
||||||
|
answerByDifficulty: entry.tarot.majorArcana
|
||||||
|
},
|
||||||
|
zodiacTarotPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
treePaths.forEach((path) => {
|
||||||
|
const pathNo = Number(path?.pathNumber);
|
||||||
|
if (!Number.isFinite(pathNo)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathNumberLabel = String(Math.trunc(pathNo));
|
||||||
|
const fromNo = Number(path?.connects?.from);
|
||||||
|
const toNo = Number(path?.connects?.to);
|
||||||
|
const fromName = getSephiraName(fromNo, path?.connectIds?.from);
|
||||||
|
const toName = getSephiraName(toNo, path?.connectIds?.to);
|
||||||
|
const pathLetter = formatPathLetter(path);
|
||||||
|
const tarotCard = normalizeOption(path?.tarot?.card);
|
||||||
|
|
||||||
|
if (fromName && toName) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `kabbalah-path-between:${pathNumberLabel}`,
|
||||||
|
categoryId: "kabbalah-path-between-sephirot",
|
||||||
|
category: "Kabbalah Paths",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `Which path is between ${fromName} and ${toName}`,
|
||||||
|
normal: `What path connects ${fromName} and ${toName}`,
|
||||||
|
hard: `${fromName} ↔ ${toName} is which path`
|
||||||
|
},
|
||||||
|
answerByDifficulty: pathNumberLabel
|
||||||
|
},
|
||||||
|
pathNumberPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathLetter) {
|
||||||
|
const numberToLetterTemplate = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `kabbalah-path-letter:${pathNumberLabel}`,
|
||||||
|
categoryId: "kabbalah-path-letter",
|
||||||
|
category: "Kabbalah Paths",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `Which letter is on Path ${pathNumberLabel}`,
|
||||||
|
normal: `Path ${pathNumberLabel} carries which Hebrew letter`,
|
||||||
|
hard: `Letter on Path ${pathNumberLabel}`
|
||||||
|
},
|
||||||
|
answerByDifficulty: pathLetter
|
||||||
|
},
|
||||||
|
pathLetterPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (numberToLetterTemplate) {
|
||||||
|
bank.push(numberToLetterTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const letterToNumberTemplate = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `kabbalah-letter-path-number:${pathNumberLabel}`,
|
||||||
|
categoryId: "kabbalah-path-letter",
|
||||||
|
category: "Kabbalah Paths",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${pathLetter} belongs to which path`,
|
||||||
|
normal: `${pathLetter} corresponds to Path`,
|
||||||
|
hard: `${pathLetter} is on Path`
|
||||||
|
},
|
||||||
|
answerByDifficulty: pathNumberLabel
|
||||||
|
},
|
||||||
|
pathNumberPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (letterToNumberTemplate) {
|
||||||
|
bank.push(letterToNumberTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tarotCard) {
|
||||||
|
const pathToTarotTemplate = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `kabbalah-path-tarot:${pathNumberLabel}`,
|
||||||
|
categoryId: "kabbalah-path-tarot",
|
||||||
|
category: "Kabbalah ↔ Tarot",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `Path ${pathNumberLabel} corresponds to which Tarot trump`,
|
||||||
|
normal: `Which Tarot trump is on Path ${pathNumberLabel}`,
|
||||||
|
hard: `Tarot trump on Path ${pathNumberLabel}`
|
||||||
|
},
|
||||||
|
answerByDifficulty: tarotCard
|
||||||
|
},
|
||||||
|
pathTarotPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pathToTarotTemplate) {
|
||||||
|
bank.push(pathToTarotTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tarotToPathTemplate = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `tarot-trump-path:${pathNumberLabel}`,
|
||||||
|
categoryId: "kabbalah-path-tarot",
|
||||||
|
category: "Tarot ↔ Kabbalah",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${tarotCard} is on which path`,
|
||||||
|
normal: `Which path corresponds to ${tarotCard}`,
|
||||||
|
hard: `${tarotCard} corresponds to Path`
|
||||||
|
},
|
||||||
|
answerByDifficulty: pathNumberLabel
|
||||||
|
},
|
||||||
|
pathNumberPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tarotToPathTemplate) {
|
||||||
|
bank.push(tarotToPathTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.values(sephirotById).forEach((sephira) => {
|
||||||
|
const sephiraName = String(sephira?.name?.roman || sephira?.name?.en || "").trim();
|
||||||
|
const planetLabel = getPlanetLabelById(sephira?.planetId);
|
||||||
|
if (!sephiraName || !planetLabel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `sephirot-planet:${normalizeId(sephira?.id || sephiraName)}`,
|
||||||
|
categoryId: "sephirot-planets",
|
||||||
|
category: "Sephirot ↔ Planet",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${sephiraName} corresponds to which planet`,
|
||||||
|
normal: `Planetary correspondence of ${sephiraName}`,
|
||||||
|
hard: `${sephiraName} corresponds to`
|
||||||
|
},
|
||||||
|
answerByDifficulty: planetLabel
|
||||||
|
},
|
||||||
|
toUniqueOptionList(Object.values(sephirotById).map((entry) => getPlanetLabelById(entry?.planetId)))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
flattenDecans.forEach((decan) => {
|
||||||
|
const decanId = String(decan?.id || "").trim();
|
||||||
|
const card = normalizeOption(decan?.tarotMinorArcana);
|
||||||
|
const decanLabel = formatDecanLabel(decan);
|
||||||
|
const rulerLabel = getPlanetLabelById(decan?.rulerPlanetId);
|
||||||
|
|
||||||
|
if (!decanId || !card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decanLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `tarot-decan-sign:${decanId}`,
|
||||||
|
categoryId: "tarot-decan-sign",
|
||||||
|
category: "Tarot Decans",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${card} belongs to which decan`,
|
||||||
|
normal: `Which decan contains ${card}`,
|
||||||
|
hard: `${card} is in`
|
||||||
|
},
|
||||||
|
answerByDifficulty: decanLabel
|
||||||
|
},
|
||||||
|
decanLabelPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rulerLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `tarot-decan-ruler:${decanId}`,
|
||||||
|
categoryId: "tarot-decan-ruler",
|
||||||
|
category: "Tarot Decans",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `The decan of ${card} is ruled by`,
|
||||||
|
normal: `Who rules the decan for ${card}`,
|
||||||
|
hard: `${card} decan ruler`
|
||||||
|
},
|
||||||
|
answerByDifficulty: rulerLabel
|
||||||
|
},
|
||||||
|
decanRulerPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cubeWalls.forEach((wall) => {
|
||||||
|
const wallName = String(wall?.name || labelFromId(wall?.id)).trim();
|
||||||
|
const wallLabel = wallName ? `${wallName} Wall` : "";
|
||||||
|
const tarotCard = normalizeOption(wall?.associations?.tarotCard);
|
||||||
|
const hebrew = hebrewById.get(normalizeId(wall?.hebrewLetterId));
|
||||||
|
const hebrewLabel = formatHebrewLetterLabel(hebrew, wall?.hebrewLetterId);
|
||||||
|
|
||||||
|
if (tarotCard && wallLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `tarot-cube-wall:${normalizeId(wall?.id || wallName)}`,
|
||||||
|
categoryId: "tarot-cube-location",
|
||||||
|
category: "Tarot ↔ Cube",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${tarotCard} is on which Cube wall`,
|
||||||
|
normal: `Where is ${tarotCard} on the Cube`,
|
||||||
|
hard: `${tarotCard} location on Cube`
|
||||||
|
},
|
||||||
|
answerByDifficulty: wallLabel
|
||||||
|
},
|
||||||
|
cubeLocationPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wallLabel && hebrewLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `cube-wall-letter:${normalizeId(wall?.id || wallName)}`,
|
||||||
|
categoryId: "cube-hebrew-letter",
|
||||||
|
category: "Cube ↔ Hebrew",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${wallLabel} corresponds to which Hebrew letter`,
|
||||||
|
normal: `Which Hebrew letter is on ${wallLabel}`,
|
||||||
|
hard: `${wallLabel} letter`
|
||||||
|
},
|
||||||
|
answerByDifficulty: hebrewLabel
|
||||||
|
},
|
||||||
|
cubeHebrewLetterPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cubeEdges.forEach((edge) => {
|
||||||
|
const edgeName = String(edge?.name || labelFromId(edge?.id)).trim();
|
||||||
|
const edgeLabel = edgeName ? `${edgeName} Edge` : "";
|
||||||
|
const hebrew = hebrewById.get(normalizeId(edge?.hebrewLetterId));
|
||||||
|
const hebrewLabel = formatHebrewLetterLabel(hebrew, edge?.hebrewLetterId);
|
||||||
|
const tarotCard = normalizeOption(hebrew?.tarot?.card);
|
||||||
|
|
||||||
|
if (tarotCard && edgeLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `tarot-cube-edge:${normalizeId(edge?.id || edgeName)}`,
|
||||||
|
categoryId: "tarot-cube-location",
|
||||||
|
category: "Tarot ↔ Cube",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${tarotCard} is on which Cube edge`,
|
||||||
|
normal: `Where is ${tarotCard} on the Cube edges`,
|
||||||
|
hard: `${tarotCard} edge location`
|
||||||
|
},
|
||||||
|
answerByDifficulty: edgeLabel
|
||||||
|
},
|
||||||
|
cubeLocationPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edgeLabel && hebrewLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `cube-edge-letter:${normalizeId(edge?.id || edgeName)}`,
|
||||||
|
categoryId: "cube-hebrew-letter",
|
||||||
|
category: "Cube ↔ Hebrew",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${edgeLabel} corresponds to which Hebrew letter`,
|
||||||
|
normal: `Which Hebrew letter is on ${edgeLabel}`,
|
||||||
|
hard: `${edgeLabel} letter`
|
||||||
|
},
|
||||||
|
answerByDifficulty: hebrewLabel
|
||||||
|
},
|
||||||
|
cubeHebrewLetterPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cubeCenter) {
|
||||||
|
const centerTarot = normalizeOption(cubeCenter?.associations?.tarotCard || cubeCenter?.tarotCard);
|
||||||
|
const centerHebrew = hebrewById.get(normalizeId(cubeCenter?.hebrewLetterId));
|
||||||
|
const centerHebrewLabel = formatHebrewLetterLabel(centerHebrew, cubeCenter?.hebrewLetterId);
|
||||||
|
|
||||||
|
if (centerTarot) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: "tarot-cube-center",
|
||||||
|
categoryId: "tarot-cube-location",
|
||||||
|
category: "Tarot ↔ Cube",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${centerTarot} is located at which Cube position`,
|
||||||
|
normal: `Where is ${centerTarot} on the Cube`,
|
||||||
|
hard: `${centerTarot} Cube location`
|
||||||
|
},
|
||||||
|
answerByDifficulty: "Center"
|
||||||
|
},
|
||||||
|
cubeLocationPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (centerHebrewLabel) {
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: "cube-center-letter",
|
||||||
|
categoryId: "cube-hebrew-letter",
|
||||||
|
category: "Cube ↔ Hebrew",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: "The Cube center corresponds to which Hebrew letter",
|
||||||
|
normal: "Which Hebrew letter is at the Cube center",
|
||||||
|
hard: "Cube center letter"
|
||||||
|
},
|
||||||
|
answerByDifficulty: centerHebrewLabel
|
||||||
|
},
|
||||||
|
cubeHebrewLetterPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playingCards.forEach((entry) => {
|
||||||
|
const cardId = String(entry?.id || "").trim();
|
||||||
|
const rankLabel = normalizeOption(entry?.rankLabel || entry?.rank);
|
||||||
|
const suitLabel = normalizeOption(entry?.suitLabel || labelFromId(entry?.suit));
|
||||||
|
const tarotCard = normalizeOption(entry?.tarotCard);
|
||||||
|
|
||||||
|
if (!cardId || !rankLabel || !suitLabel || !tarotCard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createQuestionTemplate(
|
||||||
|
{
|
||||||
|
key: `playing-card-tarot:${cardId}`,
|
||||||
|
categoryId: "playing-card-tarot",
|
||||||
|
category: "Playing Card ↔ Tarot",
|
||||||
|
promptByDifficulty: {
|
||||||
|
easy: `${rankLabel} of ${suitLabel} maps to which Tarot card`,
|
||||||
|
normal: `${rankLabel} of ${suitLabel} corresponds to`,
|
||||||
|
hard: `${rankLabel} of ${suitLabel} maps to`
|
||||||
|
},
|
||||||
|
answerByDifficulty: tarotCard
|
||||||
|
},
|
||||||
|
playingTarotPool
|
||||||
|
);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(dynamicCategoryRegistry || []).forEach(({ builder }) => {
|
||||||
|
try {
|
||||||
|
const dynamicTemplates = builder(referenceData, magickDataset);
|
||||||
|
if (Array.isArray(dynamicTemplates)) {
|
||||||
|
dynamicTemplates.forEach((template) => {
|
||||||
|
if (template) {
|
||||||
|
bank.push(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
// Skip broken plugins silently to preserve quiz availability.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return bank;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.QuizQuestionBank = {
|
||||||
|
buildQuestionBank,
|
||||||
|
createQuestionTemplate,
|
||||||
|
normalizeKey,
|
||||||
|
normalizeOption,
|
||||||
|
toTitleCase,
|
||||||
|
toUniqueOptionList
|
||||||
|
};
|
||||||
|
})();
|
||||||
872
app/ui-quiz.js
872
app/ui-quiz.js
@@ -45,6 +45,7 @@
|
|||||||
|
|
||||||
// Dynamic category plugin registry — populated by registerQuizCategory()
|
// Dynamic category plugin registry — populated by registerQuizCategory()
|
||||||
const DYNAMIC_CATEGORY_REGISTRY = [];
|
const DYNAMIC_CATEGORY_REGISTRY = [];
|
||||||
|
const quizQuestionBank = window.QuizQuestionBank || {};
|
||||||
|
|
||||||
function registerQuizCategory(id, label, builder) {
|
function registerQuizCategory(id, label, builder) {
|
||||||
if (typeof id !== "string" || !id || typeof builder !== "function") {
|
if (typeof id !== "string" || !id || typeof builder !== "function") {
|
||||||
@@ -238,41 +239,6 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createQuestionTemplate(payload, poolValues) {
|
|
||||||
const key = String(payload?.key || "").trim();
|
|
||||||
const promptByDifficulty = payload?.promptByDifficulty ?? payload?.prompt;
|
|
||||||
const answerByDifficulty = payload?.answerByDifficulty ?? payload?.answer;
|
|
||||||
const poolByDifficulty = poolValues;
|
|
||||||
const categoryId = String(payload?.categoryId || "").trim();
|
|
||||||
const category = String(payload?.category || "Correspondence").trim();
|
|
||||||
|
|
||||||
const defaultPrompt = String(resolveDifficultyValue(promptByDifficulty, "normal") || "").trim();
|
|
||||||
const defaultAnswer = normalizeOption(resolveDifficultyValue(answerByDifficulty, "normal"));
|
|
||||||
const defaultPool = toUniqueOptionList(resolveDifficultyValue(poolByDifficulty, "normal") || []);
|
|
||||||
|
|
||||||
if (!key || !defaultPrompt || !defaultAnswer || !categoryId || !category) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!defaultPool.some((value) => normalizeKey(value) === normalizeKey(defaultAnswer))) {
|
|
||||||
defaultPool.push(defaultAnswer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const distractorCount = defaultPool.filter((value) => normalizeKey(value) !== normalizeKey(defaultAnswer)).length;
|
|
||||||
if (distractorCount < 3) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
categoryId,
|
|
||||||
category,
|
|
||||||
promptByDifficulty,
|
|
||||||
answerByDifficulty,
|
|
||||||
poolByDifficulty
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function instantiateQuestion(template) {
|
function instantiateQuestion(template) {
|
||||||
if (!template) {
|
if (!template) {
|
||||||
return null;
|
return null;
|
||||||
@@ -303,837 +269,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildQuestionBank(referenceData, magickDataset) {
|
function buildQuestionBank(referenceData, magickDataset) {
|
||||||
const grouped = magickDataset?.grouped || {};
|
if (typeof quizQuestionBank.buildQuestionBank !== "function") {
|
||||||
const alphabets = grouped.alphabets || {};
|
return [];
|
||||||
const englishLetters = Array.isArray(alphabets?.english) ? alphabets.english : [];
|
|
||||||
const hebrewLetters = Array.isArray(alphabets?.hebrew) ? alphabets.hebrew : [];
|
|
||||||
const kabbalahTree = grouped?.kabbalah?.["kabbalah-tree"] || {};
|
|
||||||
const treePaths = Array.isArray(kabbalahTree?.paths) ? kabbalahTree.paths : [];
|
|
||||||
const treeSephiroth = Array.isArray(kabbalahTree?.sephiroth) ? kabbalahTree.sephiroth : [];
|
|
||||||
const sephirotById = grouped?.kabbalah?.sephirot && typeof grouped.kabbalah.sephirot === "object"
|
|
||||||
? grouped.kabbalah.sephirot
|
|
||||||
: {};
|
|
||||||
const cube = grouped?.kabbalah?.cube && typeof grouped.kabbalah.cube === "object"
|
|
||||||
? grouped.kabbalah.cube
|
|
||||||
: {};
|
|
||||||
const cubeWalls = Array.isArray(cube?.walls) ? cube.walls : [];
|
|
||||||
const cubeEdges = Array.isArray(cube?.edges) ? cube.edges : [];
|
|
||||||
const cubeCenter = cube?.center && typeof cube.center === "object" ? cube.center : null;
|
|
||||||
const playingCardsData = grouped?.["playing-cards-52"];
|
|
||||||
const playingCards = Array.isArray(playingCardsData)
|
|
||||||
? playingCardsData
|
|
||||||
: (Array.isArray(playingCardsData?.entries) ? playingCardsData.entries : []);
|
|
||||||
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
|
||||||
const planetsById = referenceData?.planets && typeof referenceData.planets === "object"
|
|
||||||
? referenceData.planets
|
|
||||||
: {};
|
|
||||||
const planets = Object.values(planetsById);
|
|
||||||
const decansBySign = referenceData?.decansBySign && typeof referenceData.decansBySign === "object"
|
|
||||||
? referenceData.decansBySign
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const normalizeId = (value) => String(value || "").trim().toLowerCase();
|
|
||||||
|
|
||||||
const toRomanNumeral = (value) => {
|
|
||||||
const numeric = Number(value);
|
|
||||||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
||||||
return String(value || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
const intValue = Math.trunc(numeric);
|
|
||||||
const lookup = [
|
|
||||||
[1000, "M"],
|
|
||||||
[900, "CM"],
|
|
||||||
[500, "D"],
|
|
||||||
[400, "CD"],
|
|
||||||
[100, "C"],
|
|
||||||
[90, "XC"],
|
|
||||||
[50, "L"],
|
|
||||||
[40, "XL"],
|
|
||||||
[10, "X"],
|
|
||||||
[9, "IX"],
|
|
||||||
[5, "V"],
|
|
||||||
[4, "IV"],
|
|
||||||
[1, "I"]
|
|
||||||
];
|
|
||||||
|
|
||||||
let current = intValue;
|
|
||||||
let result = "";
|
|
||||||
lookup.forEach(([size, symbol]) => {
|
|
||||||
while (current >= size) {
|
|
||||||
result += symbol;
|
|
||||||
current -= size;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result || String(intValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const labelFromId = (value) => {
|
|
||||||
const id = String(value || "").trim();
|
|
||||||
if (!id) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
.replace(/[_-]+/g, " ")
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim()
|
|
||||||
.split(" ")
|
|
||||||
.map((part) => part ? part.charAt(0).toUpperCase() + part.slice(1) : "")
|
|
||||||
.join(" ");
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlanetLabelById = (planetId) => {
|
|
||||||
const normalized = normalizeId(planetId);
|
|
||||||
if (!normalized) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const directPlanet = planetsById[normalized];
|
|
||||||
if (directPlanet?.name) {
|
|
||||||
return directPlanet.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalized === "primum-mobile") {
|
|
||||||
return "Primum Mobile";
|
|
||||||
}
|
|
||||||
if (normalized === "olam-yesodot") {
|
|
||||||
return "Earth / Elements";
|
|
||||||
}
|
|
||||||
|
|
||||||
return labelFromId(normalized);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hebrewById = new Map(
|
|
||||||
hebrewLetters
|
|
||||||
.filter((entry) => entry?.hebrewLetterId)
|
|
||||||
.map((entry) => [normalizeId(entry.hebrewLetterId), entry])
|
|
||||||
);
|
|
||||||
|
|
||||||
const formatHebrewLetterLabel = (entry, fallbackId = "") => {
|
|
||||||
if (entry?.name && entry?.char) {
|
|
||||||
return `${entry.name} (${entry.char})`;
|
|
||||||
}
|
|
||||||
if (entry?.name) {
|
|
||||||
return entry.name;
|
|
||||||
}
|
|
||||||
if (entry?.char) {
|
|
||||||
return entry.char;
|
|
||||||
}
|
|
||||||
return labelFromId(fallbackId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sephiraNameByNumber = new Map(
|
|
||||||
treeSephiroth
|
|
||||||
.filter((entry) => Number.isFinite(Number(entry?.number)) && entry?.name)
|
|
||||||
.map((entry) => [Math.trunc(Number(entry.number)), String(entry.name)])
|
|
||||||
);
|
|
||||||
|
|
||||||
const sephiraNameById = new Map(
|
|
||||||
treeSephiroth
|
|
||||||
.filter((entry) => entry?.sephiraId && entry?.name)
|
|
||||||
.map((entry) => [normalizeId(entry.sephiraId), String(entry.name)])
|
|
||||||
);
|
|
||||||
|
|
||||||
const getSephiraName = (numberValue, idValue) => {
|
|
||||||
const numberKey = Number(numberValue);
|
|
||||||
if (Number.isFinite(numberKey)) {
|
|
||||||
const byNumber = sephiraNameByNumber.get(Math.trunc(numberKey));
|
|
||||||
if (byNumber) {
|
|
||||||
return byNumber;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const byId = sephiraNameById.get(normalizeId(idValue));
|
|
||||||
if (byId) {
|
|
||||||
return byId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Number.isFinite(numberKey)) {
|
|
||||||
return `Sephira ${Math.trunc(numberKey)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return labelFromId(idValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatPathLetter = (path) => {
|
|
||||||
const transliteration = String(path?.hebrewLetter?.transliteration || "").trim();
|
|
||||||
const glyph = String(path?.hebrewLetter?.char || "").trim();
|
|
||||||
|
|
||||||
if (transliteration && glyph) {
|
|
||||||
return `${transliteration} (${glyph})`;
|
|
||||||
}
|
|
||||||
if (transliteration) {
|
|
||||||
return transliteration;
|
|
||||||
}
|
|
||||||
if (glyph) {
|
|
||||||
return glyph;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const flattenDecans = Object.values(decansBySign)
|
|
||||||
.flatMap((entries) => (Array.isArray(entries) ? entries : []));
|
|
||||||
|
|
||||||
const signNameById = new Map(
|
|
||||||
signs
|
|
||||||
.filter((entry) => entry?.id && entry?.name)
|
|
||||||
.map((entry) => [normalizeId(entry.id), String(entry.name)])
|
|
||||||
);
|
|
||||||
|
|
||||||
const formatDecanLabel = (decan) => {
|
|
||||||
const signName = signNameById.get(normalizeId(decan?.signId)) || labelFromId(decan?.signId);
|
|
||||||
const index = Number(decan?.index);
|
|
||||||
if (!signName || !Number.isFinite(index)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return `${signName} Decan ${toRomanNumeral(index)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const bank = [];
|
|
||||||
|
|
||||||
const englishGematriaPool = englishLetters
|
|
||||||
.map((item) => (Number.isFinite(Number(item?.pythagorean)) ? String(item.pythagorean) : ""))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const hebrewNumerologyPool = hebrewLetters
|
|
||||||
.map((item) => (Number.isFinite(Number(item?.numerology)) ? String(item.numerology) : ""))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const hebrewNameAndCharPool = hebrewLetters
|
|
||||||
.filter((item) => item?.name && item?.char)
|
|
||||||
.map((item) => `${item.name} (${item.char})`);
|
|
||||||
|
|
||||||
const hebrewCharPool = hebrewLetters
|
|
||||||
.map((item) => item?.char)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const planetNamePool = planets
|
|
||||||
.map((planet) => planet?.name)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const planetWeekdayPool = planets
|
|
||||||
.map((planet) => planet?.weekday)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const zodiacElementPool = signs
|
|
||||||
.map((sign) => toTitleCase(sign?.element))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const zodiacTarotPool = signs
|
|
||||||
.map((sign) => sign?.tarot?.majorArcana)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const pathNumberPool = toUniqueOptionList(
|
|
||||||
treePaths
|
|
||||||
.map((path) => {
|
|
||||||
const pathNo = Number(path?.pathNumber);
|
|
||||||
return Number.isFinite(pathNo) ? String(Math.trunc(pathNo)) : "";
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const pathLetterPool = toUniqueOptionList(treePaths.map((path) => formatPathLetter(path)));
|
|
||||||
const pathTarotPool = toUniqueOptionList(treePaths.map((path) => normalizeOption(path?.tarot?.card)));
|
|
||||||
|
|
||||||
const decanLabelPool = toUniqueOptionList(flattenDecans.map((decan) => formatDecanLabel(decan)));
|
|
||||||
const decanRulerPool = toUniqueOptionList(
|
|
||||||
flattenDecans.map((decan) => getPlanetLabelById(decan?.rulerPlanetId))
|
|
||||||
);
|
|
||||||
|
|
||||||
const cubeWallLabelPool = toUniqueOptionList(
|
|
||||||
cubeWalls.map((wall) => `${String(wall?.name || labelFromId(wall?.id)).trim()} Wall`)
|
|
||||||
);
|
|
||||||
|
|
||||||
const cubeEdgeLabelPool = toUniqueOptionList(
|
|
||||||
cubeEdges.map((edge) => `${String(edge?.name || labelFromId(edge?.id)).trim()} Edge`)
|
|
||||||
);
|
|
||||||
|
|
||||||
const cubeLocationPool = toUniqueOptionList([
|
|
||||||
...cubeWallLabelPool,
|
|
||||||
...cubeEdgeLabelPool,
|
|
||||||
"Center"
|
|
||||||
]);
|
|
||||||
|
|
||||||
const cubeHebrewLetterPool = toUniqueOptionList([
|
|
||||||
...cubeWalls.map((wall) => {
|
|
||||||
const hebrew = hebrewById.get(normalizeId(wall?.hebrewLetterId));
|
|
||||||
return formatHebrewLetterLabel(hebrew, wall?.hebrewLetterId);
|
|
||||||
}),
|
|
||||||
...cubeEdges.map((edge) => {
|
|
||||||
const hebrew = hebrewById.get(normalizeId(edge?.hebrewLetterId));
|
|
||||||
return formatHebrewLetterLabel(hebrew, edge?.hebrewLetterId);
|
|
||||||
}),
|
|
||||||
formatHebrewLetterLabel(hebrewById.get(normalizeId(cubeCenter?.hebrewLetterId)), cubeCenter?.hebrewLetterId)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const playingTarotPool = toUniqueOptionList(
|
|
||||||
playingCards.map((entry) => normalizeOption(entry?.tarotCard))
|
|
||||||
);
|
|
||||||
|
|
||||||
englishLetters.forEach((entry) => {
|
|
||||||
if (!entry?.letter || !Number.isFinite(Number(entry?.pythagorean))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `english-gematria:${entry.letter}`,
|
|
||||||
categoryId: "english-gematria",
|
|
||||||
category: "English Gematria",
|
|
||||||
promptByDifficulty: `${entry.letter} has a simple gematria value of`,
|
|
||||||
answerByDifficulty: String(entry.pythagorean)
|
|
||||||
},
|
|
||||||
englishGematriaPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
hebrewLetters.forEach((entry) => {
|
|
||||||
if (!entry?.name || !entry?.char || !Number.isFinite(Number(entry?.numerology))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `hebrew-number:${entry.hebrewLetterId || entry.name}`,
|
|
||||||
categoryId: "hebrew-numerology",
|
|
||||||
category: "Hebrew Gematria",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${entry.name} (${entry.char}) has a gematria value of`,
|
|
||||||
normal: `${entry.name} (${entry.char}) has a gematria value of`,
|
|
||||||
hard: `${entry.char} has a gematria value of`
|
|
||||||
},
|
|
||||||
answerByDifficulty: String(entry.numerology)
|
|
||||||
},
|
|
||||||
hebrewNumerologyPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
englishLetters.forEach((entry) => {
|
|
||||||
if (!entry?.letter || !entry?.hebrewLetterId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mappedHebrew = hebrewById.get(String(entry.hebrewLetterId));
|
|
||||||
if (!mappedHebrew?.name || !mappedHebrew?.char) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `english-hebrew:${entry.letter}`,
|
|
||||||
categoryId: "english-hebrew-mapping",
|
|
||||||
category: "Alphabet Mapping",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${entry.letter} maps to which Hebrew letter`,
|
|
||||||
normal: `${entry.letter} maps to which Hebrew letter`,
|
|
||||||
hard: `${entry.letter} maps to which Hebrew glyph`
|
|
||||||
},
|
|
||||||
answerByDifficulty: {
|
|
||||||
easy: `${mappedHebrew.name} (${mappedHebrew.char})`,
|
|
||||||
normal: `${mappedHebrew.name} (${mappedHebrew.char})`,
|
|
||||||
hard: mappedHebrew.char
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
easy: hebrewNameAndCharPool,
|
|
||||||
normal: hebrewNameAndCharPool,
|
|
||||||
hard: hebrewCharPool
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
signs.forEach((entry) => {
|
|
||||||
if (!entry?.name || !entry?.rulingPlanetId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rulerName = planetsById[String(entry.rulingPlanetId)]?.name;
|
|
||||||
if (!rulerName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `zodiac-ruler:${entry.id || entry.name}`,
|
|
||||||
categoryId: "zodiac-rulers",
|
|
||||||
category: "Zodiac Rulers",
|
|
||||||
promptByDifficulty: `${entry.name} is ruled by`,
|
|
||||||
answerByDifficulty: rulerName
|
|
||||||
},
|
|
||||||
planetNamePool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
signs.forEach((entry) => {
|
|
||||||
if (!entry?.name || !entry?.element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `zodiac-element:${entry.id || entry.name}`,
|
|
||||||
categoryId: "zodiac-elements",
|
|
||||||
category: "Zodiac Elements",
|
|
||||||
promptByDifficulty: `${entry.name} is`,
|
|
||||||
answerByDifficulty: toTitleCase(entry.element)
|
|
||||||
},
|
|
||||||
zodiacElementPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
planets.forEach((entry) => {
|
|
||||||
if (!entry?.name || !entry?.weekday) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `planet-weekday:${entry.id || entry.name}`,
|
|
||||||
categoryId: "planetary-weekdays",
|
|
||||||
category: "Planetary Weekdays",
|
|
||||||
promptByDifficulty: `${entry.name} corresponds to`,
|
|
||||||
answerByDifficulty: entry.weekday
|
|
||||||
},
|
|
||||||
planetWeekdayPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
signs.forEach((entry) => {
|
|
||||||
if (!entry?.name || !entry?.tarot?.majorArcana) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `zodiac-tarot:${entry.id || entry.name}`,
|
|
||||||
categoryId: "zodiac-tarot",
|
|
||||||
category: "Zodiac ↔ Tarot",
|
|
||||||
promptByDifficulty: `${entry.name} corresponds to`,
|
|
||||||
answerByDifficulty: entry.tarot.majorArcana
|
|
||||||
},
|
|
||||||
zodiacTarotPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
treePaths.forEach((path) => {
|
|
||||||
const pathNo = Number(path?.pathNumber);
|
|
||||||
if (!Number.isFinite(pathNo)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathNumberLabel = String(Math.trunc(pathNo));
|
|
||||||
const fromNo = Number(path?.connects?.from);
|
|
||||||
const toNo = Number(path?.connects?.to);
|
|
||||||
const fromName = getSephiraName(fromNo, path?.connectIds?.from);
|
|
||||||
const toName = getSephiraName(toNo, path?.connectIds?.to);
|
|
||||||
const pathLetter = formatPathLetter(path);
|
|
||||||
const tarotCard = normalizeOption(path?.tarot?.card);
|
|
||||||
|
|
||||||
if (fromName && toName) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `kabbalah-path-between:${pathNumberLabel}`,
|
|
||||||
categoryId: "kabbalah-path-between-sephirot",
|
|
||||||
category: "Kabbalah Paths",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `Which path is between ${fromName} and ${toName}`,
|
|
||||||
normal: `What path connects ${fromName} and ${toName}`,
|
|
||||||
hard: `${fromName} ↔ ${toName} is which path`
|
|
||||||
},
|
|
||||||
answerByDifficulty: pathNumberLabel
|
|
||||||
},
|
|
||||||
pathNumberPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathLetter) {
|
|
||||||
const numberToLetterTemplate = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `kabbalah-path-letter:${pathNumberLabel}`,
|
|
||||||
categoryId: "kabbalah-path-letter",
|
|
||||||
category: "Kabbalah Paths",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `Which letter is on Path ${pathNumberLabel}`,
|
|
||||||
normal: `Path ${pathNumberLabel} carries which Hebrew letter`,
|
|
||||||
hard: `Letter on Path ${pathNumberLabel}`
|
|
||||||
},
|
|
||||||
answerByDifficulty: pathLetter
|
|
||||||
},
|
|
||||||
pathLetterPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (numberToLetterTemplate) {
|
|
||||||
bank.push(numberToLetterTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
const letterToNumberTemplate = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `kabbalah-letter-path-number:${pathNumberLabel}`,
|
|
||||||
categoryId: "kabbalah-path-letter",
|
|
||||||
category: "Kabbalah Paths",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${pathLetter} belongs to which path`,
|
|
||||||
normal: `${pathLetter} corresponds to Path`,
|
|
||||||
hard: `${pathLetter} is on Path`
|
|
||||||
},
|
|
||||||
answerByDifficulty: pathNumberLabel
|
|
||||||
},
|
|
||||||
pathNumberPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (letterToNumberTemplate) {
|
|
||||||
bank.push(letterToNumberTemplate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tarotCard) {
|
|
||||||
const pathToTarotTemplate = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `kabbalah-path-tarot:${pathNumberLabel}`,
|
|
||||||
categoryId: "kabbalah-path-tarot",
|
|
||||||
category: "Kabbalah ↔ Tarot",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `Path ${pathNumberLabel} corresponds to which Tarot trump`,
|
|
||||||
normal: `Which Tarot trump is on Path ${pathNumberLabel}`,
|
|
||||||
hard: `Tarot trump on Path ${pathNumberLabel}`
|
|
||||||
},
|
|
||||||
answerByDifficulty: tarotCard
|
|
||||||
},
|
|
||||||
pathTarotPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pathToTarotTemplate) {
|
|
||||||
bank.push(pathToTarotTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tarotToPathTemplate = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `tarot-trump-path:${pathNumberLabel}`,
|
|
||||||
categoryId: "kabbalah-path-tarot",
|
|
||||||
category: "Tarot ↔ Kabbalah",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${tarotCard} is on which path`,
|
|
||||||
normal: `Which path corresponds to ${tarotCard}`,
|
|
||||||
hard: `${tarotCard} corresponds to Path`
|
|
||||||
},
|
|
||||||
answerByDifficulty: pathNumberLabel
|
|
||||||
},
|
|
||||||
pathNumberPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tarotToPathTemplate) {
|
|
||||||
bank.push(tarotToPathTemplate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.values(sephirotById).forEach((sephira) => {
|
|
||||||
const sephiraName = String(sephira?.name?.roman || sephira?.name?.en || "").trim();
|
|
||||||
const planetLabel = getPlanetLabelById(sephira?.planetId);
|
|
||||||
if (!sephiraName || !planetLabel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `sephirot-planet:${normalizeId(sephira?.id || sephiraName)}`,
|
|
||||||
categoryId: "sephirot-planets",
|
|
||||||
category: "Sephirot ↔ Planet",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${sephiraName} corresponds to which planet`,
|
|
||||||
normal: `Planetary correspondence of ${sephiraName}`,
|
|
||||||
hard: `${sephiraName} corresponds to`
|
|
||||||
},
|
|
||||||
answerByDifficulty: planetLabel
|
|
||||||
},
|
|
||||||
toUniqueOptionList(Object.values(sephirotById).map((entry) => getPlanetLabelById(entry?.planetId)))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
flattenDecans.forEach((decan) => {
|
|
||||||
const decanId = String(decan?.id || "").trim();
|
|
||||||
const card = normalizeOption(decan?.tarotMinorArcana);
|
|
||||||
const decanLabel = formatDecanLabel(decan);
|
|
||||||
const rulerLabel = getPlanetLabelById(decan?.rulerPlanetId);
|
|
||||||
|
|
||||||
if (!decanId || !card) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decanLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `tarot-decan-sign:${decanId}`,
|
|
||||||
categoryId: "tarot-decan-sign",
|
|
||||||
category: "Tarot Decans",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${card} belongs to which decan`,
|
|
||||||
normal: `Which decan contains ${card}`,
|
|
||||||
hard: `${card} is in`
|
|
||||||
},
|
|
||||||
answerByDifficulty: decanLabel
|
|
||||||
},
|
|
||||||
decanLabelPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rulerLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `tarot-decan-ruler:${decanId}`,
|
|
||||||
categoryId: "tarot-decan-ruler",
|
|
||||||
category: "Tarot Decans",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `The decan of ${card} is ruled by`,
|
|
||||||
normal: `Who rules the decan for ${card}`,
|
|
||||||
hard: `${card} decan ruler`
|
|
||||||
},
|
|
||||||
answerByDifficulty: rulerLabel
|
|
||||||
},
|
|
||||||
decanRulerPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cubeWalls.forEach((wall) => {
|
|
||||||
const wallName = String(wall?.name || labelFromId(wall?.id)).trim();
|
|
||||||
const wallLabel = wallName ? `${wallName} Wall` : "";
|
|
||||||
const tarotCard = normalizeOption(wall?.associations?.tarotCard);
|
|
||||||
const hebrew = hebrewById.get(normalizeId(wall?.hebrewLetterId));
|
|
||||||
const hebrewLabel = formatHebrewLetterLabel(hebrew, wall?.hebrewLetterId);
|
|
||||||
|
|
||||||
if (tarotCard && wallLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `tarot-cube-wall:${normalizeId(wall?.id || wallName)}`,
|
|
||||||
categoryId: "tarot-cube-location",
|
|
||||||
category: "Tarot ↔ Cube",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${tarotCard} is on which Cube wall`,
|
|
||||||
normal: `Where is ${tarotCard} on the Cube`,
|
|
||||||
hard: `${tarotCard} location on Cube`
|
|
||||||
},
|
|
||||||
answerByDifficulty: wallLabel
|
|
||||||
},
|
|
||||||
cubeLocationPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wallLabel && hebrewLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `cube-wall-letter:${normalizeId(wall?.id || wallName)}`,
|
|
||||||
categoryId: "cube-hebrew-letter",
|
|
||||||
category: "Cube ↔ Hebrew",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${wallLabel} corresponds to which Hebrew letter`,
|
|
||||||
normal: `Which Hebrew letter is on ${wallLabel}`,
|
|
||||||
hard: `${wallLabel} letter`
|
|
||||||
},
|
|
||||||
answerByDifficulty: hebrewLabel
|
|
||||||
},
|
|
||||||
cubeHebrewLetterPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cubeEdges.forEach((edge) => {
|
|
||||||
const edgeName = String(edge?.name || labelFromId(edge?.id)).trim();
|
|
||||||
const edgeLabel = edgeName ? `${edgeName} Edge` : "";
|
|
||||||
const hebrew = hebrewById.get(normalizeId(edge?.hebrewLetterId));
|
|
||||||
const hebrewLabel = formatHebrewLetterLabel(hebrew, edge?.hebrewLetterId);
|
|
||||||
const tarotCard = normalizeOption(hebrew?.tarot?.card);
|
|
||||||
|
|
||||||
if (tarotCard && edgeLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `tarot-cube-edge:${normalizeId(edge?.id || edgeName)}`,
|
|
||||||
categoryId: "tarot-cube-location",
|
|
||||||
category: "Tarot ↔ Cube",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${tarotCard} is on which Cube edge`,
|
|
||||||
normal: `Where is ${tarotCard} on the Cube edges`,
|
|
||||||
hard: `${tarotCard} edge location`
|
|
||||||
},
|
|
||||||
answerByDifficulty: edgeLabel
|
|
||||||
},
|
|
||||||
cubeLocationPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (edgeLabel && hebrewLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `cube-edge-letter:${normalizeId(edge?.id || edgeName)}`,
|
|
||||||
categoryId: "cube-hebrew-letter",
|
|
||||||
category: "Cube ↔ Hebrew",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${edgeLabel} corresponds to which Hebrew letter`,
|
|
||||||
normal: `Which Hebrew letter is on ${edgeLabel}`,
|
|
||||||
hard: `${edgeLabel} letter`
|
|
||||||
},
|
|
||||||
answerByDifficulty: hebrewLabel
|
|
||||||
},
|
|
||||||
cubeHebrewLetterPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (cubeCenter) {
|
|
||||||
const centerTarot = normalizeOption(cubeCenter?.associations?.tarotCard || cubeCenter?.tarotCard);
|
|
||||||
const centerHebrew = hebrewById.get(normalizeId(cubeCenter?.hebrewLetterId));
|
|
||||||
const centerHebrewLabel = formatHebrewLetterLabel(centerHebrew, cubeCenter?.hebrewLetterId);
|
|
||||||
|
|
||||||
if (centerTarot) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: "tarot-cube-center",
|
|
||||||
categoryId: "tarot-cube-location",
|
|
||||||
category: "Tarot ↔ Cube",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${centerTarot} is located at which Cube position`,
|
|
||||||
normal: `Where is ${centerTarot} on the Cube`,
|
|
||||||
hard: `${centerTarot} Cube location`
|
|
||||||
},
|
|
||||||
answerByDifficulty: "Center"
|
|
||||||
},
|
|
||||||
cubeLocationPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (centerHebrewLabel) {
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: "cube-center-letter",
|
|
||||||
categoryId: "cube-hebrew-letter",
|
|
||||||
category: "Cube ↔ Hebrew",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: "The Cube center corresponds to which Hebrew letter",
|
|
||||||
normal: "Which Hebrew letter is at the Cube center",
|
|
||||||
hard: "Cube center letter"
|
|
||||||
},
|
|
||||||
answerByDifficulty: centerHebrewLabel
|
|
||||||
},
|
|
||||||
cubeHebrewLetterPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playingCards.forEach((entry) => {
|
return quizQuestionBank.buildQuestionBank(
|
||||||
const cardId = String(entry?.id || "").trim();
|
referenceData,
|
||||||
const rankLabel = normalizeOption(entry?.rankLabel || entry?.rank);
|
magickDataset,
|
||||||
const suitLabel = normalizeOption(entry?.suitLabel || labelFromId(entry?.suit));
|
DYNAMIC_CATEGORY_REGISTRY
|
||||||
const tarotCard = normalizeOption(entry?.tarotCard);
|
);
|
||||||
|
|
||||||
if (!cardId || !rankLabel || !suitLabel || !tarotCard) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = createQuestionTemplate(
|
|
||||||
{
|
|
||||||
key: `playing-card-tarot:${cardId}`,
|
|
||||||
categoryId: "playing-card-tarot",
|
|
||||||
category: "Playing Card ↔ Tarot",
|
|
||||||
promptByDifficulty: {
|
|
||||||
easy: `${rankLabel} of ${suitLabel} maps to which Tarot card`,
|
|
||||||
normal: `${rankLabel} of ${suitLabel} corresponds to`,
|
|
||||||
hard: `${rankLabel} of ${suitLabel} maps to`
|
|
||||||
},
|
|
||||||
answerByDifficulty: tarotCard
|
|
||||||
},
|
|
||||||
playingTarotPool
|
|
||||||
);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
bank.push(template);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dynamic plugin categories
|
|
||||||
DYNAMIC_CATEGORY_REGISTRY.forEach(({ builder }) => {
|
|
||||||
try {
|
|
||||||
const dynamicTemplates = builder(referenceData, magickDataset);
|
|
||||||
if (Array.isArray(dynamicTemplates)) {
|
|
||||||
dynamicTemplates.forEach((t) => {
|
|
||||||
if (t) {
|
|
||||||
bank.push(t);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (_e) {
|
|
||||||
// skip broken plugins silently
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return bank;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshQuestionBank(referenceData, magickDataset) {
|
function refreshQuestionBank(referenceData, magickDataset) {
|
||||||
|
|||||||
270
app/ui-rosicrucian-cross.js
Normal file
270
app/ui-rosicrucian-cross.js
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
function normalizeText(value) {
|
||||||
|
return String(value || "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function svgEl(tag, attrs, text) {
|
||||||
|
const el = document.createElementNS(NS, tag);
|
||||||
|
Object.entries(attrs || {}).forEach(([key, value]) => {
|
||||||
|
el.setAttribute(key, String(value));
|
||||||
|
});
|
||||||
|
if (text != null) {
|
||||||
|
el.textContent = text;
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLetterType(value) {
|
||||||
|
const normalized = normalizeText(value);
|
||||||
|
if (normalized.includes("mother")) return "mother";
|
||||||
|
if (normalized.includes("double")) return "double";
|
||||||
|
if (normalized.includes("simple")) return "simple";
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRosePaletteForType(letterType) {
|
||||||
|
if (letterType === "mother") {
|
||||||
|
return ["#facc15", "#4ade80", "#f97316"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (letterType === "double") {
|
||||||
|
return ["#fde047", "#fb7185", "#fdba74", "#34d399", "#60a5fa", "#c084fc", "#fca5a5"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (letterType === "simple") {
|
||||||
|
return [
|
||||||
|
"#ef4444", "#f97316", "#f59e0b", "#eab308", "#84cc16", "#22c55e",
|
||||||
|
"#14b8a6", "#06b6d4", "#3b82f6", "#6366f1", "#8b5cf6", "#d946ef"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ["#71717a", "#a1a1aa", "#52525b"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendRosePetalRing(svg, paths, options) {
|
||||||
|
if (!Array.isArray(paths) || !paths.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cx = Number(options?.cx) || 490;
|
||||||
|
const cy = Number(options?.cy) || 560;
|
||||||
|
const ringRadius = Number(options?.ringRadius) || 200;
|
||||||
|
const petalRadius = Number(options?.petalRadius) || 38;
|
||||||
|
const startDeg = Number(options?.startDeg) || -90;
|
||||||
|
const letterType = String(options?.letterType || "other");
|
||||||
|
const className = String(options?.className || "");
|
||||||
|
const palette = getRosePaletteForType(letterType);
|
||||||
|
|
||||||
|
paths.forEach((path, index) => {
|
||||||
|
const angle = ((startDeg + ((360 / paths.length) * index)) * Math.PI) / 180;
|
||||||
|
const px = cx + Math.cos(angle) * ringRadius;
|
||||||
|
const py = cy + Math.sin(angle) * ringRadius;
|
||||||
|
const letterChar = String(path?.hebrewLetter?.char || "?").trim() || "?";
|
||||||
|
const transliteration = String(path?.hebrewLetter?.transliteration || "").trim();
|
||||||
|
const tarotCard = String(path?.tarot?.card || "").trim();
|
||||||
|
const fill = palette[index % palette.length];
|
||||||
|
|
||||||
|
const group = svgEl("g", {
|
||||||
|
class: `kab-rose-petal ${className}`.trim(),
|
||||||
|
"data-path": path.pathNumber,
|
||||||
|
role: "button",
|
||||||
|
tabindex: "0",
|
||||||
|
"aria-label": `Path ${path.pathNumber}: ${transliteration} ${letterChar}${tarotCard ? ` - ${tarotCard}` : ""}`
|
||||||
|
});
|
||||||
|
|
||||||
|
group.appendChild(svgEl("circle", {
|
||||||
|
cx: px.toFixed(2),
|
||||||
|
cy: py.toFixed(2),
|
||||||
|
r: petalRadius.toFixed(2),
|
||||||
|
class: "kab-rose-petal-shape",
|
||||||
|
fill,
|
||||||
|
stroke: "rgba(255,255,255,0.45)",
|
||||||
|
"stroke-width": "1.5",
|
||||||
|
style: "transform-box: fill-box; transform-origin: center;"
|
||||||
|
}));
|
||||||
|
|
||||||
|
group.appendChild(svgEl("text", {
|
||||||
|
x: px.toFixed(2),
|
||||||
|
y: (py + 4).toFixed(2),
|
||||||
|
class: "kab-rose-petal-letter",
|
||||||
|
"text-anchor": "middle",
|
||||||
|
"dominant-baseline": "middle"
|
||||||
|
}, letterChar));
|
||||||
|
|
||||||
|
group.appendChild(svgEl("text", {
|
||||||
|
x: px.toFixed(2),
|
||||||
|
y: (py + petalRadius - 10).toFixed(2),
|
||||||
|
class: "kab-rose-petal-number",
|
||||||
|
"text-anchor": "middle",
|
||||||
|
"dominant-baseline": "middle"
|
||||||
|
}, String(path.pathNumber)));
|
||||||
|
|
||||||
|
svg.appendChild(group);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRosicrucianCrossSVG(tree) {
|
||||||
|
const cx = 490;
|
||||||
|
const cy = 560;
|
||||||
|
|
||||||
|
const svg = svgEl("svg", {
|
||||||
|
viewBox: "0 0 980 1180",
|
||||||
|
width: "100%",
|
||||||
|
role: "img",
|
||||||
|
"aria-label": "Rosicrucian cross with Hebrew letter petals",
|
||||||
|
class: "kab-rose-svg"
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let index = 0; index < 16; index += 1) {
|
||||||
|
const angle = ((index * 22.5) - 90) * (Math.PI / 180);
|
||||||
|
const baseAngle = 7 * (Math.PI / 180);
|
||||||
|
const innerRadius = 232;
|
||||||
|
const outerRadius = index % 2 === 0 ? 350 : 320;
|
||||||
|
const x1 = cx + Math.cos(angle - baseAngle) * innerRadius;
|
||||||
|
const y1 = cy + Math.sin(angle - baseAngle) * innerRadius;
|
||||||
|
const x2 = cx + Math.cos(angle + baseAngle) * innerRadius;
|
||||||
|
const y2 = cy + Math.sin(angle + baseAngle) * innerRadius;
|
||||||
|
const x3 = cx + Math.cos(angle) * outerRadius;
|
||||||
|
const y3 = cy + Math.sin(angle) * outerRadius;
|
||||||
|
svg.appendChild(svgEl("polygon", {
|
||||||
|
points: `${x1.toFixed(2)},${y1.toFixed(2)} ${x2.toFixed(2)},${y2.toFixed(2)} ${x3.toFixed(2)},${y3.toFixed(2)}`,
|
||||||
|
fill: "#f8fafc",
|
||||||
|
stroke: "#0f172a",
|
||||||
|
"stroke-opacity": "0.18",
|
||||||
|
"stroke-width": "1"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.appendChild(svgEl("rect", { x: 408, y: 86, width: 164, height: 404, rx: 26, fill: "#f6e512", stroke: "#111827", "stroke-opacity": "0.55", "stroke-width": "1.6" }));
|
||||||
|
svg.appendChild(svgEl("rect", { x: 96, y: 462, width: 348, height: 154, rx: 22, fill: "#ef1c24", stroke: "#111827", "stroke-opacity": "0.55", "stroke-width": "1.6" }));
|
||||||
|
svg.appendChild(svgEl("rect", { x: 536, y: 462, width: 348, height: 154, rx: 22, fill: "#1537ee", stroke: "#111827", "stroke-opacity": "0.55", "stroke-width": "1.6" }));
|
||||||
|
svg.appendChild(svgEl("rect", { x: 408, y: 616, width: 164, height: 286, rx: 0, fill: "#f3f4f6", stroke: "#111827", "stroke-opacity": "0.45", "stroke-width": "1.3" }));
|
||||||
|
|
||||||
|
svg.appendChild(svgEl("polygon", { points: "408,902 490,902 408,980", fill: "#b3482f" }));
|
||||||
|
svg.appendChild(svgEl("polygon", { points: "490,902 572,902 572,980", fill: "#506b1c" }));
|
||||||
|
svg.appendChild(svgEl("polygon", { points: "408,902 490,902 490,980", fill: "#d4aa15" }));
|
||||||
|
svg.appendChild(svgEl("polygon", { points: "408,980 572,980 490,1106", fill: "#020617" }));
|
||||||
|
|
||||||
|
[
|
||||||
|
{ cx: 490, cy: 90, r: 52, fill: "#f6e512" },
|
||||||
|
{ cx: 430, cy: 154, r: 48, fill: "#f6e512" },
|
||||||
|
{ cx: 550, cy: 154, r: 48, fill: "#f6e512" },
|
||||||
|
{ cx: 90, cy: 539, r: 52, fill: "#ef1c24" },
|
||||||
|
{ cx: 154, cy: 480, r: 48, fill: "#ef1c24" },
|
||||||
|
{ cx: 154, cy: 598, r: 48, fill: "#ef1c24" },
|
||||||
|
{ cx: 890, cy: 539, r: 52, fill: "#1537ee" },
|
||||||
|
{ cx: 826, cy: 480, r: 48, fill: "#1537ee" },
|
||||||
|
{ cx: 826, cy: 598, r: 48, fill: "#1537ee" },
|
||||||
|
{ cx: 430, cy: 1038, r: 48, fill: "#b3482f" },
|
||||||
|
{ cx: 550, cy: 1038, r: 48, fill: "#506b1c" },
|
||||||
|
{ cx: 490, cy: 1110, r: 72, fill: "#020617" }
|
||||||
|
].forEach((entry) => {
|
||||||
|
svg.appendChild(svgEl("circle", {
|
||||||
|
cx: entry.cx,
|
||||||
|
cy: entry.cy,
|
||||||
|
r: entry.r,
|
||||||
|
fill: entry.fill,
|
||||||
|
stroke: "#111827",
|
||||||
|
"stroke-opacity": "0.56",
|
||||||
|
"stroke-width": "1.6"
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
{ x: 490, y: 128, t: "☿", c: "#a21caf", s: 50 },
|
||||||
|
{ x: 490, y: 206, t: "✶", c: "#a21caf", s: 56 },
|
||||||
|
{ x: 172, y: 539, t: "✶", c: "#16a34a", s: 62 },
|
||||||
|
{ x: 810, y: 539, t: "✶", c: "#fb923c", s: 62 },
|
||||||
|
{ x: 490, y: 778, t: "✡", c: "#52525b", s: 66 },
|
||||||
|
{ x: 490, y: 996, t: "✶", c: "#f8fafc", s: 62 },
|
||||||
|
{ x: 490, y: 1118, t: "☿", c: "#f8fafc", s: 56 }
|
||||||
|
].forEach((glyph) => {
|
||||||
|
svg.appendChild(svgEl("text", {
|
||||||
|
x: glyph.x,
|
||||||
|
y: glyph.y,
|
||||||
|
"text-anchor": "middle",
|
||||||
|
"dominant-baseline": "middle",
|
||||||
|
class: "kab-rose-arm-glyph",
|
||||||
|
fill: glyph.c,
|
||||||
|
"font-size": String(glyph.s),
|
||||||
|
"font-weight": "700"
|
||||||
|
}, glyph.t));
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.appendChild(svgEl("circle", { cx, cy, r: 286, fill: "rgba(2, 6, 23, 0.42)", stroke: "rgba(248, 250, 252, 0.24)", "stroke-width": "2" }));
|
||||||
|
svg.appendChild(svgEl("circle", { cx, cy, r: 252, fill: "rgba(15, 23, 42, 0.32)", stroke: "rgba(248, 250, 252, 0.2)", "stroke-width": "1.5" }));
|
||||||
|
|
||||||
|
const pathEntries = Array.isArray(tree?.paths)
|
||||||
|
? [...tree.paths].sort((left, right) => Number(left?.pathNumber) - Number(right?.pathNumber))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const grouped = {
|
||||||
|
mother: [],
|
||||||
|
double: [],
|
||||||
|
simple: [],
|
||||||
|
other: []
|
||||||
|
};
|
||||||
|
|
||||||
|
pathEntries.forEach((entry) => {
|
||||||
|
const letterType = normalizeLetterType(entry?.hebrewLetter?.letterType);
|
||||||
|
grouped[letterType].push(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
appendRosePetalRing(svg, grouped.simple, {
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
ringRadius: 216,
|
||||||
|
petalRadius: 34,
|
||||||
|
startDeg: -90,
|
||||||
|
letterType: "simple",
|
||||||
|
className: "kab-rose-petal--simple"
|
||||||
|
});
|
||||||
|
|
||||||
|
appendRosePetalRing(svg, grouped.double, {
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
ringRadius: 154,
|
||||||
|
petalRadius: 36,
|
||||||
|
startDeg: -78,
|
||||||
|
letterType: "double",
|
||||||
|
className: "kab-rose-petal--double"
|
||||||
|
});
|
||||||
|
|
||||||
|
appendRosePetalRing(svg, grouped.mother, {
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
ringRadius: 96,
|
||||||
|
petalRadius: 40,
|
||||||
|
startDeg: -90,
|
||||||
|
letterType: "mother",
|
||||||
|
className: "kab-rose-petal--mother"
|
||||||
|
});
|
||||||
|
|
||||||
|
appendRosePetalRing(svg, grouped.other, {
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
ringRadius: 274,
|
||||||
|
petalRadius: 30,
|
||||||
|
startDeg: -90,
|
||||||
|
letterType: "other",
|
||||||
|
className: "kab-rose-petal--other"
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.appendChild(svgEl("circle", { cx, cy, r: 64, fill: "#f8fafc", stroke: "#111827", "stroke-width": "1.7" }));
|
||||||
|
svg.appendChild(svgEl("circle", { cx, cy, r: 44, fill: "#facc15", stroke: "#111827", "stroke-width": "1.4" }));
|
||||||
|
svg.appendChild(svgEl("path", { d: "M490 516 L490 604 M446 560 L534 560", stroke: "#111827", "stroke-width": "8", "stroke-linecap": "round" }));
|
||||||
|
svg.appendChild(svgEl("circle", { cx, cy, r: 22, fill: "#db2777", stroke: "#111827", "stroke-width": "1.1" }));
|
||||||
|
svg.appendChild(svgEl("circle", { cx, cy, r: 10, fill: "#f59e0b", stroke: "#111827", "stroke-width": "1" }));
|
||||||
|
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.KabbalahRosicrucianCross = {
|
||||||
|
...(window.KabbalahRosicrucianCross || {}),
|
||||||
|
buildRosicrucianCrossSVG
|
||||||
|
};
|
||||||
|
})();
|
||||||
264
app/ui-section-state.js
Normal file
264
app/ui-section-state.js
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const VALID_SECTIONS = new Set([
|
||||||
|
"home",
|
||||||
|
"calendar",
|
||||||
|
"holidays",
|
||||||
|
"tarot",
|
||||||
|
"astronomy",
|
||||||
|
"planets",
|
||||||
|
"cycles",
|
||||||
|
"natal",
|
||||||
|
"elements",
|
||||||
|
"iching",
|
||||||
|
"kabbalah",
|
||||||
|
"kabbalah-tree",
|
||||||
|
"cube",
|
||||||
|
"alphabet",
|
||||||
|
"numbers",
|
||||||
|
"zodiac",
|
||||||
|
"quiz",
|
||||||
|
"gods",
|
||||||
|
"enochian"
|
||||||
|
]);
|
||||||
|
|
||||||
|
let activeSection = "home";
|
||||||
|
let config = {
|
||||||
|
elements: {},
|
||||||
|
ensure: {},
|
||||||
|
getReferenceData: () => null,
|
||||||
|
getMagickDataset: () => null,
|
||||||
|
calendarVisualsUi: null,
|
||||||
|
tarotSpreadUi: null,
|
||||||
|
settingsUi: null,
|
||||||
|
homeUi: null,
|
||||||
|
calendar: null
|
||||||
|
};
|
||||||
|
|
||||||
|
function setHidden(element, hidden) {
|
||||||
|
if (element) {
|
||||||
|
element.hidden = hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPressed(element, pressed) {
|
||||||
|
if (element) {
|
||||||
|
element.setAttribute("aria-pressed", pressed ? "true" : "false");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleActive(element, active) {
|
||||||
|
if (element) {
|
||||||
|
element.classList.toggle("is-active", active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReferenceData() {
|
||||||
|
return config.getReferenceData?.() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMagickDataset() {
|
||||||
|
return config.getMagickDataset?.() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHomeFallback() {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
config.calendar?.render?.();
|
||||||
|
config.calendarVisualsUi?.updateMonthStrip?.();
|
||||||
|
config.homeUi?.syncNowPanelTheme?.(new Date());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveSection(nextSection) {
|
||||||
|
const normalized = VALID_SECTIONS.has(nextSection) ? nextSection : "home";
|
||||||
|
activeSection = normalized;
|
||||||
|
|
||||||
|
const elements = config.elements || {};
|
||||||
|
const ensure = config.ensure || {};
|
||||||
|
const referenceData = getReferenceData();
|
||||||
|
const magickDataset = getMagickDataset();
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
setHidden(elements.calendarSectionEl, !isCalendarOpen);
|
||||||
|
setHidden(elements.holidaySectionEl, !isHolidaysOpen);
|
||||||
|
setHidden(elements.tarotSectionEl, !isTarotOpen);
|
||||||
|
setHidden(elements.astronomySectionEl, !isAstronomyOpen);
|
||||||
|
setHidden(elements.planetSectionEl, !isPlanetOpen);
|
||||||
|
setHidden(elements.cyclesSectionEl, !isCyclesOpen);
|
||||||
|
setHidden(elements.natalSectionEl, !isNatalOpen);
|
||||||
|
setHidden(elements.elementsSectionEl, !isElementsOpen);
|
||||||
|
setHidden(elements.ichingSectionEl, !isIChingOpen);
|
||||||
|
setHidden(elements.kabbalahSectionEl, !isKabbalahOpen);
|
||||||
|
setHidden(elements.kabbalahTreeSectionEl, !isKabbalahTreeOpen);
|
||||||
|
setHidden(elements.cubeSectionEl, !isCubeOpen);
|
||||||
|
setHidden(elements.alphabetSectionEl, !isAlphabetOpen);
|
||||||
|
setHidden(elements.numbersSectionEl, !isNumbersOpen);
|
||||||
|
setHidden(elements.zodiacSectionEl, !isZodiacOpen);
|
||||||
|
setHidden(elements.quizSectionEl, !isQuizOpen);
|
||||||
|
setHidden(elements.godsSectionEl, !isGodsOpen);
|
||||||
|
setHidden(elements.enochianSectionEl, !isEnochianOpen);
|
||||||
|
setHidden(elements.nowPanelEl, !isHomeOpen);
|
||||||
|
setHidden(elements.monthStripEl, !isHomeOpen);
|
||||||
|
setHidden(elements.calendarEl, !isHomeOpen);
|
||||||
|
|
||||||
|
setPressed(elements.openCalendarEl, isCalendarMenuOpen);
|
||||||
|
toggleActive(elements.openCalendarMonthsEl, isCalendarOpen);
|
||||||
|
toggleActive(elements.openHolidaysEl, isHolidaysOpen);
|
||||||
|
setPressed(elements.openTarotEl, isTarotOpen);
|
||||||
|
config.tarotSpreadUi?.applyViewState?.();
|
||||||
|
setPressed(elements.openAstronomyEl, isAstronomyMenuOpen);
|
||||||
|
toggleActive(elements.openPlanetsEl, isPlanetOpen);
|
||||||
|
toggleActive(elements.openCyclesEl, isCyclesOpen);
|
||||||
|
setPressed(elements.openElementsEl, isElementsOpen);
|
||||||
|
setPressed(elements.openIChingEl, isIChingOpen);
|
||||||
|
setPressed(elements.openKabbalahEl, isKabbalahMenuOpen);
|
||||||
|
toggleActive(elements.openKabbalahTreeEl, isKabbalahTreeOpen);
|
||||||
|
toggleActive(elements.openKabbalahCubeEl, isCubeOpen);
|
||||||
|
setPressed(elements.openAlphabetEl, isAlphabetOpen);
|
||||||
|
setPressed(elements.openNumbersEl, isNumbersOpen);
|
||||||
|
toggleActive(elements.openZodiacEl, isZodiacOpen);
|
||||||
|
toggleActive(elements.openNatalEl, isNatalOpen);
|
||||||
|
setPressed(elements.openQuizEl, isQuizOpen);
|
||||||
|
setPressed(elements.openGodsEl, isGodsOpen);
|
||||||
|
setPressed(elements.openEnochianEl, isEnochianOpen);
|
||||||
|
|
||||||
|
if (!isHomeOpen) {
|
||||||
|
config.settingsUi?.closeSettingsPopup?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCalendarOpen) {
|
||||||
|
ensure.ensureCalendarSection?.(referenceData, magickDataset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHolidaysOpen) {
|
||||||
|
ensure.ensureHolidaySection?.(referenceData, magickDataset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTarotOpen) {
|
||||||
|
if (typeof config.tarotSpreadUi?.handleSectionActivated === "function") {
|
||||||
|
config.tarotSpreadUi.handleSectionActivated();
|
||||||
|
} else {
|
||||||
|
ensure.ensureTarotSection?.(referenceData, magickDataset);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlanetOpen) {
|
||||||
|
ensure.ensurePlanetSection?.(referenceData, magickDataset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCyclesOpen) {
|
||||||
|
ensure.ensureCyclesSection?.(referenceData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isElementsOpen) {
|
||||||
|
ensure.ensureElementsSection?.(magickDataset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIChingOpen) {
|
||||||
|
ensure.ensureIChingSection?.(referenceData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isKabbalahOpen || isKabbalahTreeOpen) {
|
||||||
|
ensure.ensureKabbalahSection?.(magickDataset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCubeOpen) {
|
||||||
|
ensure.ensureCubeSection?.(magickDataset, referenceData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAlphabetOpen) {
|
||||||
|
ensure.ensureAlphabetSection?.(magickDataset, referenceData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNumbersOpen) {
|
||||||
|
ensure.ensureNumbersSection?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isZodiacOpen) {
|
||||||
|
ensure.ensureZodiacSection?.(referenceData, magickDataset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNatalOpen) {
|
||||||
|
ensure.ensureNatalPanel?.(referenceData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isQuizOpen) {
|
||||||
|
ensure.ensureQuizSection?.(referenceData, magickDataset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGodsOpen) {
|
||||||
|
ensure.ensureGodsSection?.(magickDataset, referenceData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEnochianOpen) {
|
||||||
|
ensure.ensureEnochianSection?.(magickDataset, referenceData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHomeFallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveSection() {
|
||||||
|
return activeSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(nextConfig = {}) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...nextConfig,
|
||||||
|
elements: {
|
||||||
|
...(config.elements || {}),
|
||||||
|
...(nextConfig.elements || {})
|
||||||
|
},
|
||||||
|
ensure: {
|
||||||
|
...(config.ensure || {}),
|
||||||
|
...(nextConfig.ensure || {})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotSectionStateUi = {
|
||||||
|
...(window.TarotSectionStateUi || {}),
|
||||||
|
init,
|
||||||
|
getActiveSection,
|
||||||
|
setActiveSection
|
||||||
|
};
|
||||||
|
})();
|
||||||
453
app/ui-settings.js
Normal file
453
app/ui-settings.js
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const SETTINGS_STORAGE_KEY = "tarot-time-settings-v1";
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
defaultSettings: {
|
||||||
|
latitude: 51.5074,
|
||||||
|
longitude: -0.1278,
|
||||||
|
timeFormat: "minutes",
|
||||||
|
birthDate: "",
|
||||||
|
tarotDeck: "ceremonial-magick"
|
||||||
|
},
|
||||||
|
onSettingsApplied: null,
|
||||||
|
onSyncSkyBackground: null,
|
||||||
|
onStatus: null,
|
||||||
|
onReopenActiveSection: null,
|
||||||
|
getActiveSection: null,
|
||||||
|
onRenderWeek: null
|
||||||
|
};
|
||||||
|
|
||||||
|
function getElements() {
|
||||||
|
return {
|
||||||
|
openSettingsEl: document.getElementById("open-settings"),
|
||||||
|
closeSettingsEl: document.getElementById("close-settings"),
|
||||||
|
settingsPopupEl: document.getElementById("settings-popup"),
|
||||||
|
settingsPopupCardEl: document.getElementById("settings-popup-card"),
|
||||||
|
latEl: document.getElementById("lat"),
|
||||||
|
lngEl: document.getElementById("lng"),
|
||||||
|
timeFormatEl: document.getElementById("time-format"),
|
||||||
|
birthDateEl: document.getElementById("birth-date"),
|
||||||
|
tarotDeckEl: document.getElementById("tarot-deck"),
|
||||||
|
saveSettingsEl: document.getElementById("save-settings"),
|
||||||
|
useLocationEl: document.getElementById("use-location")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(text) {
|
||||||
|
if (typeof config.onStatus === "function") {
|
||||||
|
config.onStatus(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyExternalSettings(settings) {
|
||||||
|
if (typeof config.onSettingsApplied === "function") {
|
||||||
|
config.onSettingsApplied(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncSky(geo, force) {
|
||||||
|
if (typeof config.onSyncSkyBackground === "function") {
|
||||||
|
config.onSyncSkyBackground(geo, force);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(String(config.defaultSettings?.tarotDeck || "ceremonial-magick").trim().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
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 String(config.defaultSettings?.tarotDeck || "ceremonial-magick").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
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, config.defaultSettings.latitude),
|
||||||
|
longitude: parseStoredNumber(settings?.longitude, config.defaultSettings.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 { ...config.defaultSettings };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return normalizeSettings(parsed);
|
||||||
|
} catch {
|
||||||
|
return { ...config.defaultSettings };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings(settings) {
|
||||||
|
try {
|
||||||
|
const normalized = normalizeSettings(settings);
|
||||||
|
window.localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(normalized));
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncTarotDeckInputOptions() {
|
||||||
|
const { tarotDeckEl } = getElements();
|
||||||
|
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 = String(config.defaultSettings?.tarotDeck || "ceremonial-magick").trim().toLowerCase();
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
tarotDeckEl.value = normalizeTarotDeck(previousValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySettingsToInputs(settings) {
|
||||||
|
const { latEl, lngEl, timeFormatEl, birthDateEl, tarotDeckEl } = getElements();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
applyExternalSettings(normalized);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSettingsFromInputs() {
|
||||||
|
const { latEl, lngEl, timeFormatEl, birthDateEl, tarotDeckEl } = getElements();
|
||||||
|
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 openSettingsPopup() {
|
||||||
|
const { settingsPopupEl, openSettingsEl } = getElements();
|
||||||
|
if (!settingsPopupEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsPopupEl.hidden = false;
|
||||||
|
if (openSettingsEl) {
|
||||||
|
openSettingsEl.setAttribute("aria-expanded", "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSettingsPopup() {
|
||||||
|
const { settingsPopupEl, openSettingsEl } = getElements();
|
||||||
|
if (!settingsPopupEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsPopupEl.hidden = true;
|
||||||
|
if (openSettingsEl) {
|
||||||
|
openSettingsEl.setAttribute("aria-expanded", "false");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveSettings() {
|
||||||
|
try {
|
||||||
|
const settings = getSettingsFromInputs();
|
||||||
|
const normalized = applySettingsToInputs(settings);
|
||||||
|
syncSky({ latitude: normalized.latitude, longitude: normalized.longitude }, true);
|
||||||
|
const didPersist = saveSettings(normalized);
|
||||||
|
emitSettingsUpdated(normalized);
|
||||||
|
if (typeof config.getActiveSection === "function" && config.getActiveSection() !== "home") {
|
||||||
|
config.onReopenActiveSection?.(config.getActiveSection());
|
||||||
|
}
|
||||||
|
closeSettingsPopup();
|
||||||
|
if (typeof config.onRenderWeek === "function") {
|
||||||
|
await config.onRenderWeek();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!didPersist) {
|
||||||
|
setStatus("Settings applied for this session. Browser storage is unavailable.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error?.message || "Unable to save settings.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestGeoLocation() {
|
||||||
|
const { latEl, lngEl } = getElements();
|
||||||
|
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);
|
||||||
|
syncSky({ 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 bindInteractions() {
|
||||||
|
const {
|
||||||
|
saveSettingsEl,
|
||||||
|
useLocationEl,
|
||||||
|
openSettingsEl,
|
||||||
|
closeSettingsEl,
|
||||||
|
settingsPopupEl,
|
||||||
|
settingsPopupCardEl
|
||||||
|
} = getElements();
|
||||||
|
|
||||||
|
if (saveSettingsEl) {
|
||||||
|
saveSettingsEl.addEventListener("click", () => {
|
||||||
|
void handleSaveSettings();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useLocationEl) {
|
||||||
|
useLocationEl.addEventListener("click", requestGeoLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openSettingsEl) {
|
||||||
|
openSettingsEl.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (settingsPopupEl?.hidden) {
|
||||||
|
openSettingsPopup();
|
||||||
|
} else {
|
||||||
|
closeSettingsPopup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeSettingsEl) {
|
||||||
|
closeSettingsEl.addEventListener("click", closeSettingsPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
const clickTarget = event.target;
|
||||||
|
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") {
|
||||||
|
closeSettingsPopup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(nextConfig = {}) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...nextConfig,
|
||||||
|
defaultSettings: {
|
||||||
|
...config.defaultSettings,
|
||||||
|
...(nextConfig.defaultSettings || {})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bindInteractions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadInitialSettingsAndApply() {
|
||||||
|
const initialSettings = loadSavedSettings();
|
||||||
|
const normalized = applySettingsToInputs(initialSettings);
|
||||||
|
emitSettingsUpdated(normalized);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotSettingsUi = {
|
||||||
|
...(window.TarotSettingsUi || {}),
|
||||||
|
init,
|
||||||
|
openSettingsPopup,
|
||||||
|
closeSettingsPopup,
|
||||||
|
loadInitialSettingsAndApply,
|
||||||
|
buildNatalContext,
|
||||||
|
normalizeSettings
|
||||||
|
};
|
||||||
|
})();
|
||||||
227
app/ui-tarot-house.js
Normal file
227
app/ui-tarot-house.js
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const HOUSE_MINOR_NUMBER_BANDS = [
|
||||||
|
[2, 3, 4],
|
||||||
|
[5, 6, 7],
|
||||||
|
[8, 9, 10],
|
||||||
|
[2, 3, 4],
|
||||||
|
[5, 6, 7],
|
||||||
|
[8, 9, 10]
|
||||||
|
];
|
||||||
|
const HOUSE_LEFT_SUITS = ["Wands", "Disks", "Swords", "Cups", "Wands", "Disks"];
|
||||||
|
const HOUSE_RIGHT_SUITS = ["Swords", "Cups", "Wands", "Disks", "Swords", "Cups"];
|
||||||
|
const HOUSE_MIDDLE_SUITS = ["Wands", "Cups", "Swords", "Disks"];
|
||||||
|
const HOUSE_MIDDLE_RANKS = ["Ace", "Knight", "Queen", "Prince", "Princess"];
|
||||||
|
const HOUSE_TRUMP_ROWS = [
|
||||||
|
[0],
|
||||||
|
[20, 21, 12],
|
||||||
|
[19, 10, 2, 1, 3, 16],
|
||||||
|
[18, 17, 15, 14, 13, 9, 8, 7, 6, 5, 4],
|
||||||
|
[11]
|
||||||
|
];
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
resolveTarotCardImage: null,
|
||||||
|
getDisplayCardName: (card) => card?.name || "",
|
||||||
|
clearChildren: () => {},
|
||||||
|
normalizeTarotCardLookupName: (value) => String(value || "").trim().toLowerCase(),
|
||||||
|
selectCardById: () => {},
|
||||||
|
getCards: () => [],
|
||||||
|
getSelectedCardId: () => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
function init(nextConfig = {}) {
|
||||||
|
Object.assign(config, nextConfig || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCardLookupMap(cards) {
|
||||||
|
const lookup = new Map();
|
||||||
|
(Array.isArray(cards) ? cards : []).forEach((card) => {
|
||||||
|
const key = config.normalizeTarotCardLookupName(card?.name);
|
||||||
|
if (key) {
|
||||||
|
lookup.set(key, card);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return lookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMinorCardName(rankNumber, suit) {
|
||||||
|
const number = Number(rankNumber);
|
||||||
|
const suitName = String(suit || "").trim();
|
||||||
|
const rankName = ({ 1: "Ace", 2: "Two", 3: "Three", 4: "Four", 5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine", 10: "Ten" })[number];
|
||||||
|
if (!rankName || !suitName) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `${rankName} of ${suitName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCourtCardName(rank, suit) {
|
||||||
|
const rankName = String(rank || "").trim();
|
||||||
|
const suitName = String(suit || "").trim();
|
||||||
|
if (!rankName || !suitName) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `${rankName} of ${suitName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCardByLookupName(cardLookupMap, cardName) {
|
||||||
|
const key = config.normalizeTarotCardLookupName(cardName);
|
||||||
|
if (!key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return cardLookupMap.get(key) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMajorCardByTrumpNumber(cards, trumpNumber) {
|
||||||
|
const target = Number(trumpNumber);
|
||||||
|
if (!Number.isFinite(target)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (Array.isArray(cards) ? cards : []).find((card) => card?.arcana === "Major" && Number(card?.number) === target) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHouseCardButton(card, elements) {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "tarot-house-card-btn";
|
||||||
|
|
||||||
|
if (!card) {
|
||||||
|
button.disabled = true;
|
||||||
|
const fallback = document.createElement("span");
|
||||||
|
fallback.className = "tarot-house-card-fallback";
|
||||||
|
fallback.textContent = "Missing";
|
||||||
|
button.appendChild(fallback);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardDisplayName = config.getDisplayCardName(card);
|
||||||
|
button.title = cardDisplayName || card.name;
|
||||||
|
button.setAttribute("aria-label", cardDisplayName || card.name);
|
||||||
|
button.dataset.houseCardId = card.id;
|
||||||
|
const imageUrl = typeof config.resolveTarotCardImage === "function"
|
||||||
|
? config.resolveTarotCardImage(card.name)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (imageUrl) {
|
||||||
|
const image = document.createElement("img");
|
||||||
|
image.className = "tarot-house-card-image";
|
||||||
|
image.src = imageUrl;
|
||||||
|
image.alt = cardDisplayName || card.name;
|
||||||
|
button.appendChild(image);
|
||||||
|
} else {
|
||||||
|
const fallback = document.createElement("span");
|
||||||
|
fallback.className = "tarot-house-card-fallback";
|
||||||
|
fallback.textContent = cardDisplayName || card.name;
|
||||||
|
button.appendChild(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
config.selectCardById(card.id, elements);
|
||||||
|
elements?.tarotCardListEl
|
||||||
|
?.querySelector(`[data-card-id="${card.id}"]`)
|
||||||
|
?.scrollIntoView({ block: "nearest" });
|
||||||
|
});
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelection(elements) {
|
||||||
|
if (!elements?.tarotHouseOfCardsEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedCardId = config.getSelectedCardId();
|
||||||
|
const buttons = elements.tarotHouseOfCardsEl.querySelectorAll(".tarot-house-card-btn[data-house-card-id]");
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
const isSelected = button.dataset.houseCardId === selectedCardId;
|
||||||
|
button.classList.toggle("is-selected", isSelected);
|
||||||
|
button.setAttribute("aria-current", isSelected ? "true" : "false");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendHouseMinorRow(columnEl, cardLookupMap, numbers, suit, elements) {
|
||||||
|
const rowEl = document.createElement("div");
|
||||||
|
rowEl.className = "tarot-house-row";
|
||||||
|
|
||||||
|
numbers.forEach((rankNumber) => {
|
||||||
|
const cardName = buildMinorCardName(rankNumber, suit);
|
||||||
|
const card = findCardByLookupName(cardLookupMap, cardName);
|
||||||
|
rowEl.appendChild(createHouseCardButton(card, elements));
|
||||||
|
});
|
||||||
|
|
||||||
|
columnEl.appendChild(rowEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendHouseCourtRow(columnEl, cardLookupMap, rank, elements) {
|
||||||
|
const rowEl = document.createElement("div");
|
||||||
|
rowEl.className = "tarot-house-row";
|
||||||
|
|
||||||
|
HOUSE_MIDDLE_SUITS.forEach((suit) => {
|
||||||
|
const cardName = buildCourtCardName(rank, suit);
|
||||||
|
const card = findCardByLookupName(cardLookupMap, cardName);
|
||||||
|
rowEl.appendChild(createHouseCardButton(card, elements));
|
||||||
|
});
|
||||||
|
|
||||||
|
columnEl.appendChild(rowEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendHouseTrumpRow(containerEl, trumpNumbers, elements, cards) {
|
||||||
|
const rowEl = document.createElement("div");
|
||||||
|
rowEl.className = "tarot-house-trump-row";
|
||||||
|
|
||||||
|
(trumpNumbers || []).forEach((trumpNumber) => {
|
||||||
|
const card = findMajorCardByTrumpNumber(cards, trumpNumber);
|
||||||
|
rowEl.appendChild(createHouseCardButton(card, elements));
|
||||||
|
});
|
||||||
|
|
||||||
|
containerEl.appendChild(rowEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(elements) {
|
||||||
|
if (!elements?.tarotHouseOfCardsEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = config.getCards();
|
||||||
|
config.clearChildren(elements.tarotHouseOfCardsEl);
|
||||||
|
const cardLookupMap = getCardLookupMap(cards);
|
||||||
|
|
||||||
|
const trumpSectionEl = document.createElement("div");
|
||||||
|
trumpSectionEl.className = "tarot-house-trumps";
|
||||||
|
HOUSE_TRUMP_ROWS.forEach((trumpRow) => {
|
||||||
|
appendHouseTrumpRow(trumpSectionEl, trumpRow, elements, cards);
|
||||||
|
});
|
||||||
|
|
||||||
|
const bottomGridEl = document.createElement("div");
|
||||||
|
bottomGridEl.className = "tarot-house-bottom-grid";
|
||||||
|
|
||||||
|
const leftColumnEl = document.createElement("div");
|
||||||
|
leftColumnEl.className = "tarot-house-column";
|
||||||
|
HOUSE_MINOR_NUMBER_BANDS.forEach((numbers, rowIndex) => {
|
||||||
|
appendHouseMinorRow(leftColumnEl, cardLookupMap, numbers, HOUSE_LEFT_SUITS[rowIndex], elements);
|
||||||
|
});
|
||||||
|
|
||||||
|
const middleColumnEl = document.createElement("div");
|
||||||
|
middleColumnEl.className = "tarot-house-column";
|
||||||
|
HOUSE_MIDDLE_RANKS.forEach((rank) => {
|
||||||
|
appendHouseCourtRow(middleColumnEl, cardLookupMap, rank, elements);
|
||||||
|
});
|
||||||
|
|
||||||
|
const rightColumnEl = document.createElement("div");
|
||||||
|
rightColumnEl.className = "tarot-house-column";
|
||||||
|
HOUSE_MINOR_NUMBER_BANDS.forEach((numbers, rowIndex) => {
|
||||||
|
appendHouseMinorRow(rightColumnEl, cardLookupMap, numbers, HOUSE_RIGHT_SUITS[rowIndex], elements);
|
||||||
|
});
|
||||||
|
|
||||||
|
bottomGridEl.append(leftColumnEl, middleColumnEl, rightColumnEl);
|
||||||
|
elements.tarotHouseOfCardsEl.append(trumpSectionEl, bottomGridEl);
|
||||||
|
updateSelection(elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotHouseUi = {
|
||||||
|
init,
|
||||||
|
render,
|
||||||
|
updateSelection
|
||||||
|
};
|
||||||
|
})();
|
||||||
176
app/ui-tarot-lightbox.js
Normal file
176
app/ui-tarot-lightbox.js
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let overlayEl = null;
|
||||||
|
let imageEl = null;
|
||||||
|
let zoomed = false;
|
||||||
|
|
||||||
|
const LIGHTBOX_ZOOM_SCALE = 6.66;
|
||||||
|
|
||||||
|
function resetZoom() {
|
||||||
|
if (!imageEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
zoomed = false;
|
||||||
|
imageEl.style.transform = "scale(1)";
|
||||||
|
imageEl.style.transformOrigin = "center center";
|
||||||
|
imageEl.style.cursor = "zoom-in";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateZoomOrigin(clientX, clientY) {
|
||||||
|
if (!zoomed || !imageEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = imageEl.getBoundingClientRect();
|
||||||
|
if (!rect.width || !rect.height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = Math.min(100, Math.max(0, ((clientX - rect.left) / rect.width) * 100));
|
||||||
|
const y = Math.min(100, Math.max(0, ((clientY - rect.top) / rect.height) * 100));
|
||||||
|
imageEl.style.transformOrigin = `${x}% ${y}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPointOnCard(clientX, clientY) {
|
||||||
|
if (!imageEl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = imageEl.getBoundingClientRect();
|
||||||
|
const naturalWidth = imageEl.naturalWidth;
|
||||||
|
const naturalHeight = imageEl.naturalHeight;
|
||||||
|
|
||||||
|
if (!rect.width || !rect.height || !naturalWidth || !naturalHeight) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameAspect = rect.width / rect.height;
|
||||||
|
const imageAspect = naturalWidth / naturalHeight;
|
||||||
|
|
||||||
|
let renderWidth = rect.width;
|
||||||
|
let renderHeight = rect.height;
|
||||||
|
if (imageAspect > frameAspect) {
|
||||||
|
renderHeight = rect.width / imageAspect;
|
||||||
|
} else {
|
||||||
|
renderWidth = rect.height * imageAspect;
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = rect.left + (rect.width - renderWidth) / 2;
|
||||||
|
const top = rect.top + (rect.height - renderHeight) / 2;
|
||||||
|
const right = left + renderWidth;
|
||||||
|
const bottom = top + renderHeight;
|
||||||
|
|
||||||
|
return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensure() {
|
||||||
|
if (overlayEl && imageEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayEl = document.createElement("div");
|
||||||
|
overlayEl.setAttribute("aria-hidden", "true");
|
||||||
|
overlayEl.style.position = "fixed";
|
||||||
|
overlayEl.style.inset = "0";
|
||||||
|
overlayEl.style.background = "rgba(0, 0, 0, 0.82)";
|
||||||
|
overlayEl.style.display = "none";
|
||||||
|
overlayEl.style.alignItems = "center";
|
||||||
|
overlayEl.style.justifyContent = "center";
|
||||||
|
overlayEl.style.zIndex = "9999";
|
||||||
|
overlayEl.style.padding = "0";
|
||||||
|
|
||||||
|
imageEl = document.createElement("img");
|
||||||
|
imageEl.alt = "Tarot card enlarged image";
|
||||||
|
imageEl.style.maxWidth = "100vw";
|
||||||
|
imageEl.style.maxHeight = "100vh";
|
||||||
|
imageEl.style.width = "100vw";
|
||||||
|
imageEl.style.height = "100vh";
|
||||||
|
imageEl.style.objectFit = "contain";
|
||||||
|
imageEl.style.borderRadius = "0";
|
||||||
|
imageEl.style.boxShadow = "none";
|
||||||
|
imageEl.style.border = "none";
|
||||||
|
imageEl.style.cursor = "zoom-in";
|
||||||
|
imageEl.style.transform = "scale(1)";
|
||||||
|
imageEl.style.transformOrigin = "center center";
|
||||||
|
imageEl.style.transition = "transform 120ms ease-out";
|
||||||
|
imageEl.style.userSelect = "none";
|
||||||
|
|
||||||
|
overlayEl.appendChild(imageEl);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (!overlayEl || !imageEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
overlayEl.style.display = "none";
|
||||||
|
overlayEl.setAttribute("aria-hidden", "true");
|
||||||
|
imageEl.removeAttribute("src");
|
||||||
|
resetZoom();
|
||||||
|
};
|
||||||
|
|
||||||
|
overlayEl.addEventListener("click", (event) => {
|
||||||
|
if (event.target === overlayEl) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
imageEl.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!isPointOnCard(event.clientX, event.clientY)) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!zoomed) {
|
||||||
|
zoomed = true;
|
||||||
|
imageEl.style.transform = `scale(${LIGHTBOX_ZOOM_SCALE})`;
|
||||||
|
imageEl.style.cursor = "zoom-out";
|
||||||
|
updateZoomOrigin(event.clientX, event.clientY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetZoom();
|
||||||
|
});
|
||||||
|
|
||||||
|
imageEl.addEventListener("mousemove", (event) => {
|
||||||
|
updateZoomOrigin(event.clientX, event.clientY);
|
||||||
|
});
|
||||||
|
|
||||||
|
imageEl.addEventListener("mouseleave", () => {
|
||||||
|
if (zoomed) {
|
||||||
|
imageEl.style.transformOrigin = "center center";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(overlayEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(src, altText) {
|
||||||
|
if (!src) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure();
|
||||||
|
if (!overlayEl || !imageEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
imageEl.src = src;
|
||||||
|
imageEl.alt = altText || "Tarot card enlarged image";
|
||||||
|
resetZoom();
|
||||||
|
overlayEl.style.display = "flex";
|
||||||
|
overlayEl.setAttribute("aria-hidden", "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotUiLightbox = {
|
||||||
|
...(window.TarotUiLightbox || {}),
|
||||||
|
open
|
||||||
|
};
|
||||||
|
})();
|
||||||
734
app/ui-tarot-relations.js
Normal file
734
app/ui-tarot-relations.js
Normal file
@@ -0,0 +1,734 @@
|
|||||||
|
/* ui-tarot-relations.js — Tarot relation builders */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const TAROT_TRUMP_NUMBER_BY_NAME = {
|
||||||
|
"the fool": 0,
|
||||||
|
fool: 0,
|
||||||
|
"the magus": 1,
|
||||||
|
magus: 1,
|
||||||
|
magician: 1,
|
||||||
|
"the high priestess": 2,
|
||||||
|
"high priestess": 2,
|
||||||
|
"the empress": 3,
|
||||||
|
empress: 3,
|
||||||
|
"the emperor": 4,
|
||||||
|
emperor: 4,
|
||||||
|
"the hierophant": 5,
|
||||||
|
hierophant: 5,
|
||||||
|
"the lovers": 6,
|
||||||
|
lovers: 6,
|
||||||
|
"the chariot": 7,
|
||||||
|
chariot: 7,
|
||||||
|
strength: 8,
|
||||||
|
lust: 8,
|
||||||
|
"the hermit": 9,
|
||||||
|
hermit: 9,
|
||||||
|
fortune: 10,
|
||||||
|
"wheel of fortune": 10,
|
||||||
|
justice: 11,
|
||||||
|
"the hanged man": 12,
|
||||||
|
"hanged man": 12,
|
||||||
|
death: 13,
|
||||||
|
temperance: 14,
|
||||||
|
art: 14,
|
||||||
|
"the devil": 15,
|
||||||
|
devil: 15,
|
||||||
|
"the tower": 16,
|
||||||
|
tower: 16,
|
||||||
|
"the star": 17,
|
||||||
|
star: 17,
|
||||||
|
"the moon": 18,
|
||||||
|
moon: 18,
|
||||||
|
"the sun": 19,
|
||||||
|
sun: 19,
|
||||||
|
aeon: 20,
|
||||||
|
judgement: 20,
|
||||||
|
judgment: 20,
|
||||||
|
universe: 21,
|
||||||
|
world: 21,
|
||||||
|
"the world": 21
|
||||||
|
};
|
||||||
|
|
||||||
|
const HEBREW_LETTER_ALIASES = {
|
||||||
|
aleph: "alef",
|
||||||
|
alef: "alef",
|
||||||
|
heh: "he",
|
||||||
|
he: "he",
|
||||||
|
beth: "bet",
|
||||||
|
bet: "bet",
|
||||||
|
cheth: "het",
|
||||||
|
chet: "het",
|
||||||
|
kaph: "kaf",
|
||||||
|
kaf: "kaf",
|
||||||
|
peh: "pe",
|
||||||
|
tzaddi: "tsadi",
|
||||||
|
tzadi: "tsadi",
|
||||||
|
tsadi: "tsadi",
|
||||||
|
qoph: "qof",
|
||||||
|
qof: "qof",
|
||||||
|
taw: "tav",
|
||||||
|
tau: "tav"
|
||||||
|
};
|
||||||
|
|
||||||
|
const CUBE_MOTHER_CONNECTOR_BY_LETTER = {
|
||||||
|
alef: { connectorId: "above-below", connectorName: "Above ↔ Below" },
|
||||||
|
mem: { connectorId: "east-west", connectorName: "East ↔ West" },
|
||||||
|
shin: { connectorId: "south-north", connectorName: "South ↔ North" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const MINOR_RANK_NUMBER_BY_NAME = {
|
||||||
|
ace: 1,
|
||||||
|
two: 2,
|
||||||
|
three: 3,
|
||||||
|
four: 4,
|
||||||
|
five: 5,
|
||||||
|
six: 6,
|
||||||
|
seven: 7,
|
||||||
|
eight: 8,
|
||||||
|
nine: 9,
|
||||||
|
ten: 10
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeRelationId(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/(^-|-$)/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTarotName(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHebrewLetterId(value) {
|
||||||
|
const key = String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z]/g, "");
|
||||||
|
return HEBREW_LETTER_ALIASES[key] || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTarotTrumpNumber(cardName) {
|
||||||
|
const key = normalizeTarotName(cardName);
|
||||||
|
if (!key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, key)) {
|
||||||
|
return TAROT_TRUMP_NUMBER_BY_NAME[key];
|
||||||
|
}
|
||||||
|
const withoutLeadingThe = key.replace(/^the\s+/, "");
|
||||||
|
if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, withoutLeadingThe)) {
|
||||||
|
return TAROT_TRUMP_NUMBER_BY_NAME[withoutLeadingThe];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cardMatchesTarotAssociation(card, tarotCardName) {
|
||||||
|
const associationName = normalizeTarotName(tarotCardName);
|
||||||
|
if (!associationName || !card) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardName = normalizeTarotName(card.name);
|
||||||
|
const cardBare = cardName.replace(/^the\s+/, "");
|
||||||
|
const assocBare = associationName.replace(/^the\s+/, "");
|
||||||
|
|
||||||
|
if (
|
||||||
|
associationName === cardName ||
|
||||||
|
associationName === cardBare ||
|
||||||
|
assocBare === cardName ||
|
||||||
|
assocBare === cardBare
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (card.arcana === "Major" && Number.isFinite(Number(card.number))) {
|
||||||
|
const trumpNumber = resolveTarotTrumpNumber(associationName);
|
||||||
|
if (trumpNumber != null) {
|
||||||
|
return trumpNumber === Number(card.number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCourtCardByDecanId(cards) {
|
||||||
|
const map = new Map();
|
||||||
|
|
||||||
|
(cards || []).forEach((card) => {
|
||||||
|
if (!card || card.arcana !== "Minor") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rankKey = String(card.rank || "").trim().toLowerCase();
|
||||||
|
if (rankKey !== "knight" && rankKey !== "queen" && rankKey !== "prince") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowRelation = (Array.isArray(card.relations) ? card.relations : [])
|
||||||
|
.find((relation) => relation && typeof relation === "object" && relation.type === "courtDateWindow");
|
||||||
|
|
||||||
|
const decanIds = Array.isArray(windowRelation?.data?.decanIds)
|
||||||
|
? windowRelation.data.decanIds
|
||||||
|
: [];
|
||||||
|
|
||||||
|
decanIds.forEach((decanId) => {
|
||||||
|
const decanKey = normalizeRelationId(decanId);
|
||||||
|
if (!decanKey || map.has(decanKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
map.set(decanKey, {
|
||||||
|
cardName: card.name,
|
||||||
|
rank: card.rank,
|
||||||
|
suit: card.suit,
|
||||||
|
dateRange: String(windowRelation?.data?.dateRange || "").trim()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSmallCardCourtLinkRelations(card, relations, courtCardByDecanId) {
|
||||||
|
if (!card || card.arcana !== "Minor") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rankKey = String(card.rank || "").trim().toLowerCase();
|
||||||
|
const rankNumber = MINOR_RANK_NUMBER_BY_NAME[rankKey];
|
||||||
|
if (!Number.isFinite(rankNumber) || rankNumber < 2 || rankNumber > 10) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const decans = (relations || []).filter((relation) => relation?.type === "decan");
|
||||||
|
if (!decans.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
const seenCourtCardNames = new Set();
|
||||||
|
|
||||||
|
decans.forEach((decan) => {
|
||||||
|
const signId = String(decan?.data?.signId || "").trim().toLowerCase();
|
||||||
|
const decanIndex = Number(decan?.data?.index);
|
||||||
|
if (!signId || !Number.isFinite(decanIndex)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decanId = normalizeRelationId(`${signId}-${decanIndex}`);
|
||||||
|
const linkedCourt = courtCardByDecanId?.get(decanId);
|
||||||
|
if (!linkedCourt?.cardName || seenCourtCardNames.has(linkedCourt.cardName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenCourtCardNames.add(linkedCourt.cardName);
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
type: "tarotCard",
|
||||||
|
id: `${decanId}-${normalizeRelationId(linkedCourt.cardName)}`,
|
||||||
|
label: `Shared court date window: ${linkedCourt.cardName}${linkedCourt.dateRange ? ` · ${linkedCourt.dateRange}` : ""}`,
|
||||||
|
data: {
|
||||||
|
cardName: linkedCourt.cardName,
|
||||||
|
dateRange: linkedCourt.dateRange || "",
|
||||||
|
decanId
|
||||||
|
},
|
||||||
|
__key: `tarotCard|${decanId}|${normalizeRelationId(linkedCourt.cardName)}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMonthDayToken(value) {
|
||||||
|
const text = String(value || "").trim();
|
||||||
|
const match = text.match(/^(\d{1,2})-(\d{1,2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const month = Number(match[1]);
|
||||||
|
const day = Number(match[2]);
|
||||||
|
if (!Number.isInteger(month) || !Number.isInteger(day) || month < 1 || month > 12 || day < 1 || day > 31) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { month, day };
|
||||||
|
}
|
||||||
|
|
||||||
|
function toReferenceDate(token, year) {
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Date(year, token.month - 1, token.day, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMonthDayRangeByMonth(startToken, endToken) {
|
||||||
|
const startDate = toReferenceDate(startToken, 2025);
|
||||||
|
const endBase = toReferenceDate(endToken, 2025);
|
||||||
|
if (!startDate || !endBase) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapsYear = endBase.getTime() < startDate.getTime();
|
||||||
|
const endDate = wrapsYear ? toReferenceDate(endToken, 2026) : endBase;
|
||||||
|
if (!endDate) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = [];
|
||||||
|
let cursor = new Date(startDate);
|
||||||
|
|
||||||
|
while (cursor.getTime() <= endDate.getTime()) {
|
||||||
|
const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0);
|
||||||
|
const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate;
|
||||||
|
|
||||||
|
segments.push({
|
||||||
|
monthNo: cursor.getMonth() + 1,
|
||||||
|
startDay: cursor.getDate(),
|
||||||
|
endDay: segmentEnd.getDate()
|
||||||
|
});
|
||||||
|
|
||||||
|
cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMonthDayRangeLabel(monthName, startDay, endDay) {
|
||||||
|
const start = Number(startDay);
|
||||||
|
const end = Number(endDay);
|
||||||
|
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
||||||
|
return monthName;
|
||||||
|
}
|
||||||
|
if (start === end) {
|
||||||
|
return `${monthName} ${start}`;
|
||||||
|
}
|
||||||
|
return `${monthName} ${start}-${end}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMonthReferencesByCard(referenceData, cards) {
|
||||||
|
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]));
|
||||||
|
|
||||||
|
function parseMonthFromDateToken(value) {
|
||||||
|
const token = parseMonthDayToken(value);
|
||||||
|
return token ? token.month : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMonthByNumber(monthNo) {
|
||||||
|
if (!Number.isInteger(monthNo) || monthNo < 1 || monthNo > 12) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const byOrder = months.find((month) => Number(month?.order) === monthNo);
|
||||||
|
if (byOrder) {
|
||||||
|
return byOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return months.find((month) => parseMonthFromDateToken(month?.start) === monthNo) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushRef(card, month, options = {}) {
|
||||||
|
if (!card?.id || !month?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.has(card.id)) {
|
||||||
|
map.set(card.id, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = map.get(card.id);
|
||||||
|
const monthOrder = Number.isFinite(Number(month.order)) ? Number(month.order) : 999;
|
||||||
|
const startToken = parseMonthDayToken(options.startToken || month.start);
|
||||||
|
const endToken = parseMonthDayToken(options.endToken || month.end);
|
||||||
|
const dateRange = String(options.dateRange || "").trim() || (
|
||||||
|
startToken && endToken
|
||||||
|
? formatMonthDayRangeLabel(month.name || month.id, startToken.day, endToken.day)
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const uniqueKey = [
|
||||||
|
month.id,
|
||||||
|
dateRange.toLowerCase(),
|
||||||
|
String(options.context || "").trim().toLowerCase(),
|
||||||
|
String(options.source || "").trim().toLowerCase()
|
||||||
|
].join("|");
|
||||||
|
|
||||||
|
if (rows.some((entry) => entry.uniqueKey === uniqueKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
id: month.id,
|
||||||
|
name: month.name || month.id,
|
||||||
|
order: monthOrder,
|
||||||
|
startToken: startToken ? `${String(startToken.month).padStart(2, "0")}-${String(startToken.day).padStart(2, "0")}` : null,
|
||||||
|
endToken: endToken ? `${String(endToken.month).padStart(2, "0")}-${String(endToken.day).padStart(2, "0")}` : null,
|
||||||
|
dateRange,
|
||||||
|
context: String(options.context || "").trim(),
|
||||||
|
source: String(options.source || "").trim(),
|
||||||
|
uniqueKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureRefs(associations, month) {
|
||||||
|
const tarotCardName = associations?.tarotCard;
|
||||||
|
if (!tarotCardName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cards.forEach((card) => {
|
||||||
|
if (cardMatchesTarotAssociation(card, tarotCardName)) {
|
||||||
|
pushRef(card, month);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
months.forEach((month) => {
|
||||||
|
captureRefs(month?.associations, month);
|
||||||
|
|
||||||
|
const events = Array.isArray(month?.events) ? month.events : [];
|
||||||
|
events.forEach((event) => {
|
||||||
|
const tarotCardName = event?.associations?.tarotCard;
|
||||||
|
if (!tarotCardName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cards.forEach((card) => {
|
||||||
|
if (!cardMatchesTarotAssociation(card, tarotCardName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pushRef(card, month, {
|
||||||
|
source: "month-event",
|
||||||
|
context: String(event?.name || "").trim()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
holidays.forEach((holiday) => {
|
||||||
|
const month = monthById.get(holiday?.monthId);
|
||||||
|
if (!month) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tarotCardName = holiday?.associations?.tarotCard;
|
||||||
|
if (!tarotCardName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cards.forEach((card) => {
|
||||||
|
if (!cardMatchesTarotAssociation(card, tarotCardName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pushRef(card, month, {
|
||||||
|
source: "holiday",
|
||||||
|
context: String(holiday?.name || "").trim()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
signs.forEach((sign) => {
|
||||||
|
const signTrumpNumber = Number(sign?.tarot?.number);
|
||||||
|
const signTarotName = sign?.tarot?.majorArcana || sign?.tarot?.card || sign?.tarotCard;
|
||||||
|
if (!Number.isFinite(signTrumpNumber) && !signTarotName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signName = String(sign?.name || sign?.id || "").trim();
|
||||||
|
const startToken = parseMonthDayToken(sign?.start);
|
||||||
|
const endToken = parseMonthDayToken(sign?.end);
|
||||||
|
const monthSegments = splitMonthDayRangeByMonth(startToken, endToken);
|
||||||
|
const fallbackStartMonthNo = parseMonthFromDateToken(sign?.start);
|
||||||
|
const fallbackEndMonthNo = parseMonthFromDateToken(sign?.end);
|
||||||
|
const fallbackStartMonth = findMonthByNumber(fallbackStartMonthNo);
|
||||||
|
const fallbackEndMonth = findMonthByNumber(fallbackEndMonthNo);
|
||||||
|
|
||||||
|
cards.forEach((card) => {
|
||||||
|
const cardTrumpNumber = Number(card?.number);
|
||||||
|
const matchesByTrump = card?.arcana === "Major"
|
||||||
|
&& Number.isFinite(cardTrumpNumber)
|
||||||
|
&& Number.isFinite(signTrumpNumber)
|
||||||
|
&& cardTrumpNumber === signTrumpNumber;
|
||||||
|
const matchesByName = signTarotName ? cardMatchesTarotAssociation(card, signTarotName) : false;
|
||||||
|
|
||||||
|
if (!matchesByTrump && !matchesByName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monthSegments.length) {
|
||||||
|
monthSegments.forEach((segment) => {
|
||||||
|
const month = findMonthByNumber(segment.monthNo);
|
||||||
|
if (!month) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushRef(card, month, {
|
||||||
|
source: "zodiac-window",
|
||||||
|
context: signName ? `${signName} window` : "",
|
||||||
|
startToken: `${String(segment.monthNo).padStart(2, "0")}-${String(segment.startDay).padStart(2, "0")}`,
|
||||||
|
endToken: `${String(segment.monthNo).padStart(2, "0")}-${String(segment.endDay).padStart(2, "0")}`,
|
||||||
|
dateRange: formatMonthDayRangeLabel(month.name || month.id, segment.startDay, segment.endDay)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallbackStartMonth) {
|
||||||
|
pushRef(card, fallbackStartMonth, {
|
||||||
|
source: "zodiac-window",
|
||||||
|
context: signName ? `${signName} window` : ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (fallbackEndMonth && (!fallbackStartMonth || fallbackEndMonth.id !== fallbackStartMonth.id)) {
|
||||||
|
pushRef(card, fallbackEndMonth, {
|
||||||
|
source: "zodiac-window",
|
||||||
|
context: signName ? `${signName} window` : ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
map.forEach((rows, key) => {
|
||||||
|
const monthIdsWithZodiacWindows = new Set(
|
||||||
|
rows
|
||||||
|
.filter((entry) => entry?.source === "zodiac-window" && entry?.id)
|
||||||
|
.map((entry) => entry.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredRows = rows.filter((entry) => {
|
||||||
|
if (!entry?.id || entry?.source === "zodiac-window") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!monthIdsWithZodiacWindows.has(entry.id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const month = monthById.get(entry.id);
|
||||||
|
if (!month) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFullMonth = String(entry.startToken || "") === String(month.start || "")
|
||||||
|
&& String(entry.endToken || "") === String(month.end || "");
|
||||||
|
|
||||||
|
return !isFullMonth;
|
||||||
|
});
|
||||||
|
|
||||||
|
filteredRows.sort((left, right) => {
|
||||||
|
if (left.order !== right.order) {
|
||||||
|
return left.order - right.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startLeft = parseMonthDayToken(left.startToken);
|
||||||
|
const startRight = parseMonthDayToken(right.startToken);
|
||||||
|
const dayLeft = startLeft ? startLeft.day : 999;
|
||||||
|
const dayRight = startRight ? startRight.day : 999;
|
||||||
|
if (dayLeft !== dayRight) {
|
||||||
|
return dayLeft - dayRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(left.dateRange || left.name || "").localeCompare(String(right.dateRange || right.name || ""));
|
||||||
|
});
|
||||||
|
map.set(key, filteredRows);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCubeFaceRelationsForCard(card, magickDataset) {
|
||||||
|
const cube = magickDataset?.grouped?.kabbalah?.cube;
|
||||||
|
const walls = Array.isArray(cube?.walls) ? cube.walls : [];
|
||||||
|
if (!card || !walls.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return walls
|
||||||
|
.map((wall, index) => {
|
||||||
|
const wallTarot = wall?.associations?.tarotCard || wall?.tarotCard;
|
||||||
|
if (!wallTarot || !cardMatchesTarotAssociation(card, wallTarot)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallId = String(wall?.id || "").trim();
|
||||||
|
const wallName = String(wall?.name || wallId || "").trim();
|
||||||
|
if (!wallId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "cubeFace",
|
||||||
|
id: wallId,
|
||||||
|
label: `Cube: ${wallName} Wall - Face`,
|
||||||
|
data: {
|
||||||
|
wallId,
|
||||||
|
wallName,
|
||||||
|
edgeId: ""
|
||||||
|
},
|
||||||
|
__key: `cubeFace|${wallId}|${index}`
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cardMatchesPathTarot(card, path) {
|
||||||
|
if (!card || !path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trumpNumber = Number(path?.tarot?.trumpNumber);
|
||||||
|
if (card?.arcana === "Major" && Number.isFinite(Number(card?.number)) && Number.isFinite(trumpNumber)) {
|
||||||
|
if (Number(card.number) === trumpNumber) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cardMatchesTarotAssociation(card, path?.tarot?.card);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCubeEdgeRelationsForCard(card, magickDataset) {
|
||||||
|
const cube = magickDataset?.grouped?.kabbalah?.cube;
|
||||||
|
const tree = magickDataset?.grouped?.kabbalah?.["kabbalah-tree"];
|
||||||
|
const edges = Array.isArray(cube?.edges) ? cube.edges : [];
|
||||||
|
const paths = Array.isArray(tree?.paths) ? tree.paths : [];
|
||||||
|
|
||||||
|
if (!card || !edges.length || !paths.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathByLetterId = new Map(
|
||||||
|
paths
|
||||||
|
.map((path) => [normalizeHebrewLetterId(path?.hebrewLetter?.transliteration), path])
|
||||||
|
.filter(([letterId]) => Boolean(letterId))
|
||||||
|
);
|
||||||
|
|
||||||
|
return edges
|
||||||
|
.map((edge, index) => {
|
||||||
|
const edgeLetterId = normalizeHebrewLetterId(edge?.hebrewLetterId || edge?.associations?.hebrewLetterId);
|
||||||
|
if (!edgeLetterId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathMatch = pathByLetterId.get(edgeLetterId);
|
||||||
|
if (!pathMatch || !cardMatchesPathTarot(card, pathMatch)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const edgeId = String(edge?.id || "").trim();
|
||||||
|
if (!edgeId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const edgeName = String(edge?.name || edgeId).trim();
|
||||||
|
const wallId = String(Array.isArray(edge?.walls) ? (edge.walls[0] || "") : "").trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "cubeEdge",
|
||||||
|
id: edgeId,
|
||||||
|
label: `Cube: ${edgeName} Edge`,
|
||||||
|
data: {
|
||||||
|
edgeId,
|
||||||
|
edgeName,
|
||||||
|
wallId: wallId || undefined,
|
||||||
|
hebrewLetterId: edgeLetterId
|
||||||
|
},
|
||||||
|
__key: `cubeEdge|${edgeId}|${index}`
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCubeMotherConnectorRelationsForCard(card, magickDataset) {
|
||||||
|
const tree = magickDataset?.grouped?.kabbalah?.["kabbalah-tree"];
|
||||||
|
const paths = Array.isArray(tree?.paths) ? tree.paths : [];
|
||||||
|
const relations = Array.isArray(card?.relations) ? card.relations : [];
|
||||||
|
|
||||||
|
return Object.entries(CUBE_MOTHER_CONNECTOR_BY_LETTER)
|
||||||
|
.map(([letterId, connector]) => {
|
||||||
|
const pathMatch = paths.find((path) => normalizeHebrewLetterId(path?.hebrewLetter?.transliteration) === letterId) || null;
|
||||||
|
|
||||||
|
const matchesByPath = cardMatchesPathTarot(card, pathMatch);
|
||||||
|
|
||||||
|
const matchesByHebrewRelation = relations.some((relation) => {
|
||||||
|
if (relation?.type !== "hebrewLetter") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const relationLetterId = normalizeHebrewLetterId(
|
||||||
|
relation?.data?.id || relation?.id || relation?.data?.latin || relation?.data?.name
|
||||||
|
);
|
||||||
|
return relationLetterId === letterId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!matchesByPath && !matchesByHebrewRelation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "cubeConnector",
|
||||||
|
id: connector.connectorId,
|
||||||
|
label: `Cube: ${connector.connectorName}`,
|
||||||
|
data: {
|
||||||
|
connectorId: connector.connectorId,
|
||||||
|
connectorName: connector.connectorName,
|
||||||
|
hebrewLetterId: letterId
|
||||||
|
},
|
||||||
|
__key: `cubeConnector|${connector.connectorId}|${letterId}`
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCubePrimalPointRelationsForCard(card, magickDataset) {
|
||||||
|
const center = magickDataset?.grouped?.kabbalah?.cube?.center;
|
||||||
|
if (!center || !card) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerTarot = center?.associations?.tarotCard || center?.tarotCard;
|
||||||
|
const centerTrump = Number(center?.associations?.tarotTrumpNumber);
|
||||||
|
const matchesByName = cardMatchesTarotAssociation(card, centerTarot);
|
||||||
|
const matchesByTrump = card?.arcana === "Major"
|
||||||
|
&& Number.isFinite(Number(card?.number))
|
||||||
|
&& Number.isFinite(centerTrump)
|
||||||
|
&& Number(card.number) === centerTrump;
|
||||||
|
|
||||||
|
if (!matchesByName && !matchesByTrump) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{
|
||||||
|
type: "cubeCenter",
|
||||||
|
id: "primal-point",
|
||||||
|
label: "Cube: Primal Point",
|
||||||
|
data: {
|
||||||
|
nodeType: "center",
|
||||||
|
primalPoint: true
|
||||||
|
},
|
||||||
|
__key: "cubeCenter|primal-point"
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCubeRelationsForCard(card, magickDataset) {
|
||||||
|
return [
|
||||||
|
...buildCubeFaceRelationsForCard(card, magickDataset),
|
||||||
|
...buildCubeEdgeRelationsForCard(card, magickDataset),
|
||||||
|
...buildCubePrimalPointRelationsForCard(card, magickDataset),
|
||||||
|
...buildCubeMotherConnectorRelationsForCard(card, magickDataset)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotRelationsUi = {
|
||||||
|
buildCourtCardByDecanId,
|
||||||
|
buildSmallCardCourtLinkRelations,
|
||||||
|
buildMonthReferencesByCard,
|
||||||
|
buildCubeRelationsForCard,
|
||||||
|
parseMonthDayToken
|
||||||
|
};
|
||||||
|
})();
|
||||||
425
app/ui-tarot-spread.js
Normal file
425
app/ui-tarot-spread.js
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
let activeTarotSpread = null;
|
||||||
|
let activeTarotSpreadDraw = [];
|
||||||
|
let config = {
|
||||||
|
ensureTarotSection: null,
|
||||||
|
getReferenceData: () => null,
|
||||||
|
getMagickDataset: () => null,
|
||||||
|
getActiveSection: () => "home",
|
||||||
|
setActiveSection: null
|
||||||
|
};
|
||||||
|
|
||||||
|
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 getElements() {
|
||||||
|
return {
|
||||||
|
openTarotCardsEl: document.getElementById("open-tarot-cards"),
|
||||||
|
openTarotSpreadEl: document.getElementById("open-tarot-spread"),
|
||||||
|
tarotBrowseViewEl: document.getElementById("tarot-browse-view"),
|
||||||
|
tarotSpreadViewEl: document.getElementById("tarot-spread-view"),
|
||||||
|
tarotSpreadBackEl: document.getElementById("tarot-spread-back"),
|
||||||
|
tarotSpreadBtnThreeEl: document.getElementById("tarot-spread-btn-three"),
|
||||||
|
tarotSpreadBtnCelticEl: document.getElementById("tarot-spread-btn-celtic"),
|
||||||
|
tarotSpreadRevealAllEl: document.getElementById("tarot-spread-reveal-all"),
|
||||||
|
tarotSpreadRedrawEl: document.getElementById("tarot-spread-redraw"),
|
||||||
|
tarotSpreadMeaningsEl: document.getElementById("tarot-spread-meanings"),
|
||||||
|
tarotSpreadBoardEl: document.getElementById("tarot-spread-board")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureTarotBrowseData() {
|
||||||
|
const referenceData = typeof config.getReferenceData === "function" ? config.getReferenceData() : null;
|
||||||
|
const magickDataset = typeof config.getMagickDataset === "function" ? config.getMagickDataset() : null;
|
||||||
|
if (typeof config.ensureTarotSection === "function" && referenceData) {
|
||||||
|
config.ensureTarotSection(referenceData, magickDataset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
revealed: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTarotSpreadMeanings() {
|
||||||
|
const { tarotSpreadMeaningsEl } = getElements();
|
||||||
|
if (!tarotSpreadMeaningsEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!activeTarotSpreadDraw.length || activeTarotSpreadDraw.some((entry) => !entry.card)) {
|
||||||
|
tarotSpreadMeaningsEl.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const revealedEntries = activeTarotSpreadDraw.filter((entry) => entry.card && entry.revealed);
|
||||||
|
if (!revealedEntries.length) {
|
||||||
|
tarotSpreadMeaningsEl.innerHTML = '<div class="tarot-spread-meanings-empty">Cards are face down. Click a card to reveal its meaning.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hiddenCount = activeTarotSpreadDraw.length - revealedEntries.length;
|
||||||
|
const hiddenHintMarkup = hiddenCount > 0
|
||||||
|
? `<div class="tarot-spread-meanings-empty">${hiddenCount} card${hiddenCount === 1 ? "" : "s"} still face down.</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
tarotSpreadMeaningsEl.innerHTML = revealedEntries.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("") + hiddenHintMarkup;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTarotSpread() {
|
||||||
|
const { tarotSpreadBoardEl, tarotSpreadMeaningsEl, tarotSpreadRevealAllEl } = getElements();
|
||||||
|
if (!tarotSpreadBoardEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedSpread = normalizeTarotSpread(activeTarotSpread);
|
||||||
|
const isCeltic = normalizedSpread === "celtic-cross";
|
||||||
|
const cardBackImageSrc = String(window.TarotCardImages?.resolveTarotCardBackImage?.() || "").trim();
|
||||||
|
|
||||||
|
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 = "";
|
||||||
|
}
|
||||||
|
if (tarotSpreadRevealAllEl) {
|
||||||
|
tarotSpreadRevealAllEl.disabled = true;
|
||||||
|
tarotSpreadRevealAllEl.textContent = "Reveal All";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tarotSpreadRevealAllEl) {
|
||||||
|
const totalCards = activeTarotSpreadDraw.length;
|
||||||
|
const revealedCount = activeTarotSpreadDraw.reduce((count, entry) => (
|
||||||
|
count + (entry?.card && entry.revealed ? 1 : 0)
|
||||||
|
), 0);
|
||||||
|
tarotSpreadRevealAllEl.disabled = revealedCount >= totalCards;
|
||||||
|
tarotSpreadRevealAllEl.textContent = revealedCount >= totalCards
|
||||||
|
? "All Revealed"
|
||||||
|
: `Reveal All (${totalCards - revealedCount})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTarotSpreadMeanings();
|
||||||
|
|
||||||
|
tarotSpreadBoardEl.innerHTML = activeTarotSpreadDraw.map((entry, index) => {
|
||||||
|
const position = entry.position;
|
||||||
|
const card = entry.card;
|
||||||
|
const imgSrc = window.TarotCardImages?.resolveTarotCardImage?.(card.name);
|
||||||
|
const isRevealed = Boolean(entry.revealed);
|
||||||
|
const cardBackAttr = cardBackImageSrc
|
||||||
|
? ` data-card-back-src="${escapeHtml(cardBackImageSrc)}"`
|
||||||
|
: "";
|
||||||
|
const reversed = card.reversed;
|
||||||
|
const wrapClass = [
|
||||||
|
"spread-card-wrap",
|
||||||
|
isRevealed ? "is-revealed" : "is-facedown",
|
||||||
|
(isRevealed && reversed) ? "is-reversed" : ""
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
|
||||||
|
let faceMarkup = "";
|
||||||
|
if (isRevealed) {
|
||||||
|
faceMarkup = imgSrc
|
||||||
|
? `<img class="spread-card-img" src="${imgSrc}" alt="${escapeHtml(card.name)}" loading="lazy">`
|
||||||
|
: `<div class="spread-card-placeholder">${escapeHtml(card.name)}</div>`;
|
||||||
|
} else if (cardBackImageSrc) {
|
||||||
|
faceMarkup = '<img class="spread-card-back-img" src="' + cardBackImageSrc + '" alt="Face-down tarot card" loading="lazy">';
|
||||||
|
} else {
|
||||||
|
faceMarkup = '<div class="spread-card-back-fallback">CARD BACK</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const reversedTag = isRevealed && reversed
|
||||||
|
? '<span class="spread-reversed-tag">Reversed</span>'
|
||||||
|
: "";
|
||||||
|
const buttonAriaLabel = isRevealed
|
||||||
|
? `Open ${escapeHtml(card.name)} for ${escapeHtml(position.label)} in fullscreen`
|
||||||
|
: `Reveal ${escapeHtml(position.label)} card`;
|
||||||
|
|
||||||
|
return `<div class="spread-position" data-pos="${position.pos}">`
|
||||||
|
+ `<div class="spread-pos-label">${escapeHtml(position.label)}</div>`
|
||||||
|
+ `<button type="button" class="${wrapClass}" data-spread-index="${index}" aria-label="${buttonAriaLabel}"${cardBackAttr}>${faceMarkup}</button>`
|
||||||
|
+ (reversedTag ? `<div class="spread-card-name">${reversedTag}</div>` : "")
|
||||||
|
+ `</div>`;
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyViewState() {
|
||||||
|
const {
|
||||||
|
openTarotCardsEl,
|
||||||
|
openTarotSpreadEl,
|
||||||
|
tarotBrowseViewEl,
|
||||||
|
tarotSpreadViewEl,
|
||||||
|
tarotSpreadBtnThreeEl,
|
||||||
|
tarotSpreadBtnCelticEl
|
||||||
|
} = getElements();
|
||||||
|
const isSpreadOpen = activeTarotSpread !== null;
|
||||||
|
const isCeltic = activeTarotSpread === "celtic-cross";
|
||||||
|
const isTarotActive = typeof config.getActiveSection === "function" && config.getActiveSection() === "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 showCardsView() {
|
||||||
|
activeTarotSpread = null;
|
||||||
|
activeTarotSpreadDraw = [];
|
||||||
|
applyViewState();
|
||||||
|
ensureTarotBrowseData();
|
||||||
|
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();
|
||||||
|
applyViewState();
|
||||||
|
ensureTarotBrowseData();
|
||||||
|
renderTarotSpread();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSpread(spreadId, openTarotSection = false) {
|
||||||
|
if (openTarotSection && typeof config.setActiveSection === "function") {
|
||||||
|
config.setActiveSection("tarot");
|
||||||
|
}
|
||||||
|
showTarotSpreadView(spreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function revealAll() {
|
||||||
|
if (!activeTarotSpreadDraw.length) {
|
||||||
|
regenerateTarotSpreadDraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTarotSpreadDraw.forEach((entry) => {
|
||||||
|
if (entry?.card) {
|
||||||
|
entry.revealed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderTarotSpread();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBoardClick(event) {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof Node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = target instanceof Element
|
||||||
|
? target.closest(".spread-card-wrap[data-spread-index]")
|
||||||
|
: null;
|
||||||
|
if (!(button instanceof HTMLButtonElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spreadIndex = Number(button.dataset.spreadIndex);
|
||||||
|
if (!Number.isInteger(spreadIndex) || spreadIndex < 0 || spreadIndex >= activeTarotSpreadDraw.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spreadEntry = activeTarotSpreadDraw[spreadIndex];
|
||||||
|
if (!spreadEntry?.card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!spreadEntry.revealed) {
|
||||||
|
spreadEntry.revealed = true;
|
||||||
|
renderTarotSpread();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageSrc = window.TarotCardImages?.resolveTarotCardImage?.(spreadEntry.card.name);
|
||||||
|
if (imageSrc) {
|
||||||
|
window.TarotUiLightbox?.open?.(imageSrc, `${spreadEntry.card.name} (${spreadEntry.position?.label || "Spread"})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
const {
|
||||||
|
openTarotCardsEl,
|
||||||
|
openTarotSpreadEl,
|
||||||
|
tarotSpreadBackEl,
|
||||||
|
tarotSpreadBtnThreeEl,
|
||||||
|
tarotSpreadBtnCelticEl,
|
||||||
|
tarotSpreadRevealAllEl,
|
||||||
|
tarotSpreadRedrawEl,
|
||||||
|
tarotSpreadBoardEl
|
||||||
|
} = getElements();
|
||||||
|
|
||||||
|
if (openTarotCardsEl) {
|
||||||
|
openTarotCardsEl.addEventListener("click", () => {
|
||||||
|
if (typeof config.setActiveSection === "function") {
|
||||||
|
config.setActiveSection("tarot");
|
||||||
|
}
|
||||||
|
showCardsView();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openTarotSpreadEl) {
|
||||||
|
openTarotSpreadEl.addEventListener("click", () => {
|
||||||
|
setSpread("three-card", true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tarotSpreadBackEl) {
|
||||||
|
tarotSpreadBackEl.addEventListener("click", () => {
|
||||||
|
showCardsView();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tarotSpreadBtnThreeEl) {
|
||||||
|
tarotSpreadBtnThreeEl.addEventListener("click", () => {
|
||||||
|
showTarotSpreadView("three-card");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tarotSpreadBtnCelticEl) {
|
||||||
|
tarotSpreadBtnCelticEl.addEventListener("click", () => {
|
||||||
|
showTarotSpreadView("celtic-cross");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tarotSpreadRedrawEl) {
|
||||||
|
tarotSpreadRedrawEl.addEventListener("click", () => {
|
||||||
|
regenerateTarotSpreadDraw();
|
||||||
|
renderTarotSpread();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tarotSpreadRevealAllEl) {
|
||||||
|
tarotSpreadRevealAllEl.addEventListener("click", revealAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tarotSpreadBoardEl) {
|
||||||
|
tarotSpreadBoardEl.addEventListener("click", handleBoardClick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSectionActivated() {
|
||||||
|
ensureTarotBrowseData();
|
||||||
|
applyViewState();
|
||||||
|
if (activeTarotSpread !== null) {
|
||||||
|
renderTarotSpread();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(nextConfig = {}) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...nextConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
if (initialized) {
|
||||||
|
applyViewState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents();
|
||||||
|
applyViewState();
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TarotSpreadUi = {
|
||||||
|
...(window.TarotSpreadUi || {}),
|
||||||
|
init,
|
||||||
|
applyViewState,
|
||||||
|
showCardsView,
|
||||||
|
showTarotSpreadView,
|
||||||
|
setSpread,
|
||||||
|
handleSectionActivated,
|
||||||
|
renderTarotSpread,
|
||||||
|
isSpreadOpen() {
|
||||||
|
return activeTarotSpread !== null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
1005
app/ui-tarot.js
1005
app/ui-tarot.js
File diff suppressed because it is too large
Load Diff
48
index.html
48
index.html
@@ -260,6 +260,7 @@
|
|||||||
<button id="tarot-spread-btn-three" class="tarot-spread-type-btn" type="button">3 Card</button>
|
<button id="tarot-spread-btn-three" class="tarot-spread-type-btn" type="button">3 Card</button>
|
||||||
<button id="tarot-spread-btn-celtic" class="tarot-spread-type-btn" type="button">Celtic Cross</button>
|
<button id="tarot-spread-btn-celtic" class="tarot-spread-type-btn" type="button">Celtic Cross</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button id="tarot-spread-reveal-all" class="tarot-spread-redraw-btn" type="button">Reveal All</button>
|
||||||
<button id="tarot-spread-redraw" class="tarot-spread-redraw-btn" type="button">⟳ Redraw</button>
|
<button id="tarot-spread-redraw" class="tarot-spread-redraw-btn" type="button">⟳ Redraw</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="tarot-spread-meanings" class="tarot-spread-meanings" aria-live="polite"></div>
|
<div id="tarot-spread-meanings" class="tarot-spread-meanings" aria-live="polite"></div>
|
||||||
@@ -474,11 +475,22 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="kabbalah-section" hidden>
|
<section id="kabbalah-section" hidden>
|
||||||
<div class="kabbalah-placeholder">
|
<div class="kab-rose-layout">
|
||||||
<div class="kabbalah-placeholder-card">
|
<aside class="kab-rose-panel">
|
||||||
<strong>Kabbalah</strong>
|
<div class="planet-list-header">
|
||||||
<div class="planet-text">This Kabbalah landing page is intentionally blank for now. Use the Kabbalah menu to open Tree or Cube.</div>
|
<strong>Rosicrucian Cross</strong>
|
||||||
</div>
|
<span class="planet-list-count">22 Hebrew Letter Paths</span>
|
||||||
|
</div>
|
||||||
|
<div class="kab-rose-intro planet-text">Click a Hebrew letter petal to open path correspondences.</div>
|
||||||
|
<div id="kab-rose-cross-container" class="kab-rose-cross-container"></div>
|
||||||
|
</aside>
|
||||||
|
<section class="kab-detail-panel" aria-live="polite">
|
||||||
|
<div class="planet-detail-heading">
|
||||||
|
<h2 id="kab-rose-detail-name">Rosicrucian Cross</h2>
|
||||||
|
<div id="kab-rose-detail-sub" class="planet-detail-type">Select a Hebrew letter petal to explore the path</div>
|
||||||
|
</div>
|
||||||
|
<div id="kab-rose-detail-body" class="planet-meta-grid"></div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -757,25 +769,47 @@
|
|||||||
<script src="app/astro-calcs.js"></script>
|
<script src="app/astro-calcs.js"></script>
|
||||||
<script src="app/data-service.js"></script>
|
<script src="app/data-service.js"></script>
|
||||||
<script src="app/calendar-events.js"></script>
|
<script src="app/calendar-events.js"></script>
|
||||||
<script src="app/card-images.js"></script>
|
<script src="app/card-images.js?v=20260307b"></script>
|
||||||
|
<script src="app/ui-tarot-lightbox.js?v=20260307b"></script>
|
||||||
|
<script src="app/ui-tarot-house.js?v=20260307b"></script>
|
||||||
|
<script src="app/ui-tarot-relations.js"></script>
|
||||||
<script src="app/ui-now.js"></script>
|
<script src="app/ui-now.js"></script>
|
||||||
<script src="app/ui-natal.js"></script>
|
<script src="app/ui-natal.js"></script>
|
||||||
<script src="app/tarot-database.js"></script>
|
<script src="app/tarot-database.js"></script>
|
||||||
|
<script src="app/ui-calendar-dates.js"></script>
|
||||||
|
<script src="app/ui-calendar-detail.js"></script>
|
||||||
<script src="app/ui-calendar.js"></script>
|
<script src="app/ui-calendar.js"></script>
|
||||||
<script src="app/ui-holidays.js"></script>
|
<script src="app/ui-holidays.js"></script>
|
||||||
<script src="app/ui-tarot.js"></script>
|
<script src="app/ui-tarot.js?v=20260307b"></script>
|
||||||
<script src="app/ui-planets.js"></script>
|
<script src="app/ui-planets.js"></script>
|
||||||
<script src="app/ui-cycles.js"></script>
|
<script src="app/ui-cycles.js"></script>
|
||||||
<script src="app/ui-elements.js"></script>
|
<script src="app/ui-elements.js"></script>
|
||||||
<script src="app/ui-iching.js"></script>
|
<script src="app/ui-iching.js"></script>
|
||||||
|
<script src="app/ui-rosicrucian-cross.js"></script>
|
||||||
|
<script src="app/ui-kabbalah-detail.js"></script>
|
||||||
<script src="app/ui-kabbalah.js"></script>
|
<script src="app/ui-kabbalah.js"></script>
|
||||||
|
<script src="app/ui-cube-detail.js"></script>
|
||||||
<script src="app/ui-cube.js"></script>
|
<script src="app/ui-cube.js"></script>
|
||||||
|
<script src="app/ui-alphabet-gematria.js"></script>
|
||||||
|
<script src="app/ui-alphabet-references.js"></script>
|
||||||
|
<script src="app/ui-alphabet-detail.js"></script>
|
||||||
<script src="app/ui-alphabet.js"></script>
|
<script src="app/ui-alphabet.js"></script>
|
||||||
<script src="app/ui-zodiac.js"></script>
|
<script src="app/ui-zodiac.js"></script>
|
||||||
|
<script src="app/ui-quiz-bank.js"></script>
|
||||||
<script src="app/ui-quiz.js"></script>
|
<script src="app/ui-quiz.js"></script>
|
||||||
<script src="app/quiz-calendars.js"></script>
|
<script src="app/quiz-calendars.js"></script>
|
||||||
<script src="app/ui-gods.js"></script>
|
<script src="app/ui-gods.js"></script>
|
||||||
<script src="app/ui-enochian.js"></script>
|
<script src="app/ui-enochian.js"></script>
|
||||||
|
<script src="app/ui-numbers.js"></script>
|
||||||
|
<script src="app/ui-tarot-spread.js"></script>
|
||||||
|
<script src="app/ui-settings.js"></script>
|
||||||
|
<script src="app/ui-chrome.js"></script>
|
||||||
|
<script src="app/ui-navigation.js"></script>
|
||||||
|
<script src="app/ui-calendar-formatting.js?v=20260307b"></script>
|
||||||
|
<script src="app/ui-calendar-visuals.js?v=20260307b"></script>
|
||||||
|
<script src="app/ui-home-calendar.js"></script>
|
||||||
|
<script src="app/ui-section-state.js"></script>
|
||||||
|
<script src="app/app-runtime.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
86
readme
Normal file
86
readme
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|

|
||||||
|
|
||||||
|
# Tarot Time
|
||||||
|
|
||||||
|
A web-based esoteric correspondence app for tarot, astrology, calendars, symbols, and related systems.
|
||||||
|
|
||||||
|
[](https://nodejs.org/)
|
||||||
|
[](https://git-scm.com/)
|
||||||
|
[](https://code.glowers.club/goyimnose/tarot-time)
|
||||||
|
[](https://code.glowers.club/goyimnose/tarot-deck)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Correspondence explorer for multiple occult/esoteric systems.
|
||||||
|
- Tarot deck support via a generated deck registry.
|
||||||
|
- Pluggable deck structure using per-deck `deck.json` manifests.
|
||||||
|
- Fast local static serving with `http-server`.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. Install Node.js: https://nodejs.org/en/download
|
||||||
|
2. Clone this repository.
|
||||||
|
3. Install dependencies.
|
||||||
|
4. Start the app.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git clone https://code.glowers.club/goyimnose/tarot-time.git
|
||||||
|
Set-Location .\tarot-time
|
||||||
|
npm install
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
The app opens in your browser (typically at `http://127.0.0.1:8080`).
|
||||||
|
|
||||||
|
## Deck Repository (Install Ready)
|
||||||
|
|
||||||
|
Use this companion repository for downloadable decks:
|
||||||
|
|
||||||
|
- https://code.glowers.club/goyimnose/tarot-deck
|
||||||
|
|
||||||
|
Typical flow:
|
||||||
|
|
||||||
|
1. Clone the deck repository somewhere local.
|
||||||
|
2. Copy one or more deck folders into `asset/tarot deck/`.
|
||||||
|
3. Validate and regenerate the deck registry.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git clone https://code.glowers.club/goyimnose/tarot-deck.git
|
||||||
|
# Copy selected deck folder(s) into Tarot Time deck directory.
|
||||||
|
npm run validate:decks
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deck Authoring and Validation
|
||||||
|
|
||||||
|
Deck discovery is registry-driven and generated automatically.
|
||||||
|
|
||||||
|
1. Copy `asset/tarot deck/_template/` to a new folder under `asset/tarot deck/`.
|
||||||
|
2. Rename the new folder and update its `deck.json`.
|
||||||
|
3. Add card image files matching the naming rules in the manifest.
|
||||||
|
4. Run `npm run validate:decks` before publishing/testing.
|
||||||
|
5. Run `npm run start` (or `npm run generate:decks`) to rebuild `asset/tarot deck/decks.json`.
|
||||||
|
|
||||||
|
Rules and behavior:
|
||||||
|
|
||||||
|
- Folders without `deck.json` are ignored.
|
||||||
|
- Folders beginning with `_` or `.` are ignored (safe for `_template`).
|
||||||
|
- `_template` includes `deck.canonical-map.example.json` for explicit major-card file mapping.
|
||||||
|
- `_template/STRUCTURE.md` documents recommended `majors/` and `minors/` layouts.
|
||||||
|
- Decks can define `cardBack` in `deck.json`; if omitted, `back.webp/png/jpg/jpeg/avif/gif` in the deck root is auto-detected.
|
||||||
|
- Manifests may override labels with `nameOverrides` and `minorNameOverrides`.
|
||||||
|
- Invalid manifests or missing mapped files are skipped with terminal warnings.
|
||||||
|
|
||||||
|
## NPM Scripts
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `npm run start` | Generate deck registry, then serve the app locally and open `index.html`. |
|
||||||
|
| `npm run dev` | Alias of `npm run start`. |
|
||||||
|
| `npm run generate:decks` | Rebuild `asset/tarot deck/decks.json`. |
|
||||||
|
| `npm run validate:decks` | Strict validation only (no write), exits on manifest/file problems. |
|
||||||
|
|
||||||
|
## Project Links
|
||||||
|
|
||||||
|
- Main app: https://code.glowers.club/goyimnose/tarot-time
|
||||||
|
- Deck repo: https://code.glowers.club/goyimnose/tarot-deck
|
||||||
@@ -8,6 +8,7 @@ const ignoredFolderNames = new Set(["template", "templates", "example", "example
|
|||||||
const tarotSuits = ["wands", "cups", "swords", "disks"];
|
const tarotSuits = ["wands", "cups", "swords", "disks"];
|
||||||
const majorTrumpNumbers = Array.from({ length: 22 }, (_, index) => index);
|
const majorTrumpNumbers = Array.from({ length: 22 }, (_, index) => index);
|
||||||
const expectedMinorCardCount = 56;
|
const expectedMinorCardCount = 56;
|
||||||
|
const cardBackCandidateExtensions = ["webp", "png", "jpg", "jpeg", "avif", "gif"];
|
||||||
|
|
||||||
function isPlainObject(value) {
|
function isPlainObject(value) {
|
||||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
@@ -29,6 +30,10 @@ function asNonEmptyString(value) {
|
|||||||
return normalized || null;
|
return normalized || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRemoteAssetPath(value) {
|
||||||
|
return /^(https?:)?\/\//i.test(String(value || "").trim());
|
||||||
|
}
|
||||||
|
|
||||||
function toTitleCase(value) {
|
function toTitleCase(value) {
|
||||||
const normalized = String(value || "").trim().toLowerCase();
|
const normalized = String(value || "").trim().toLowerCase();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
@@ -241,6 +246,10 @@ function validateDeckManifest(manifest) {
|
|||||||
errors.push("majorNameOverridesByTrump must be an object when provided");
|
errors.push("majorNameOverridesByTrump must be an object when provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (manifest.cardBack != null && !asNonEmptyString(manifest.cardBack)) {
|
||||||
|
errors.push("cardBack must be a non-empty string when provided");
|
||||||
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +367,15 @@ function getReferencedMinorFiles(manifest) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getReferencedCardBackFiles(manifest) {
|
||||||
|
const cardBack = asNonEmptyString(manifest?.cardBack);
|
||||||
|
if (!cardBack || isRemoteAssetPath(cardBack)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [cardBack];
|
||||||
|
}
|
||||||
|
|
||||||
function summarizeMissingFiles(fileList) {
|
function summarizeMissingFiles(fileList) {
|
||||||
const maxPreview = 8;
|
const maxPreview = 8;
|
||||||
const preview = fileList.slice(0, maxPreview).join(", ");
|
const preview = fileList.slice(0, maxPreview).join(", ");
|
||||||
@@ -368,11 +386,30 @@ function summarizeMissingFiles(fileList) {
|
|||||||
return `${preview}, ... (+${fileList.length - maxPreview} more)`;
|
return `${preview}, ... (+${fileList.length - maxPreview} more)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectDeckCardBackRelativePath(folderName, manifest) {
|
||||||
|
const explicitCardBack = asNonEmptyString(manifest?.cardBack);
|
||||||
|
if (explicitCardBack) {
|
||||||
|
return explicitCardBack;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deckFolderPath = path.join(decksRoot, folderName);
|
||||||
|
for (let index = 0; index < cardBackCandidateExtensions.length; index += 1) {
|
||||||
|
const extension = cardBackCandidateExtensions[index];
|
||||||
|
const candidateName = `back.${extension}`;
|
||||||
|
if (fs.existsSync(path.join(deckFolderPath, candidateName))) {
|
||||||
|
return candidateName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function auditDeckFiles(folderName, manifest) {
|
function auditDeckFiles(folderName, manifest) {
|
||||||
const deckFolderPath = path.join(decksRoot, folderName);
|
const deckFolderPath = path.join(decksRoot, folderName);
|
||||||
const referencedFiles = [
|
const referencedFiles = [
|
||||||
...getReferencedMajorFiles(manifest),
|
...getReferencedMajorFiles(manifest),
|
||||||
...getReferencedMinorFiles(manifest)
|
...getReferencedMinorFiles(manifest),
|
||||||
|
...getReferencedCardBackFiles(manifest)
|
||||||
]
|
]
|
||||||
.map((relativePath) => String(relativePath || "").trim())
|
.map((relativePath) => String(relativePath || "").trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
@@ -453,6 +490,7 @@ function compileDeckRegistry() {
|
|||||||
const id = (idFromManifest || fallbackId).toLowerCase();
|
const id = (idFromManifest || fallbackId).toLowerCase();
|
||||||
const label = labelFromManifest || folderName;
|
const label = labelFromManifest || folderName;
|
||||||
const basePath = `asset/tarot deck/${folderName}`;
|
const basePath = `asset/tarot deck/${folderName}`;
|
||||||
|
const cardBackPath = detectDeckCardBackRelativePath(folderName, manifest);
|
||||||
|
|
||||||
if (seenIds.has(id)) {
|
if (seenIds.has(id)) {
|
||||||
warnings.push(`Skipped '${folderName}': duplicate deck id '${id}'`);
|
warnings.push(`Skipped '${folderName}': duplicate deck id '${id}'`);
|
||||||
@@ -465,7 +503,8 @@ function compileDeckRegistry() {
|
|||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
basePath,
|
basePath,
|
||||||
manifestPath: `${basePath}/deck.json`
|
manifestPath: `${basePath}/deck.json`,
|
||||||
|
...(cardBackPath ? { cardBackPath } : {})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user