various ui improvements, including a new sequence nav component and a new kabbalah detail view

This commit is contained in:
2026-05-28 18:19:13 -07:00
parent c423f1191d
commit 1433ec1495
17 changed files with 2274 additions and 120 deletions
+59 -17
View File
@@ -50,6 +50,9 @@ const cyclesSectionEl = document.getElementById("cycles-section");
const elementsSectionEl = document.getElementById("elements-section"); const elementsSectionEl = document.getElementById("elements-section");
const ichingSectionEl = document.getElementById("iching-section"); const ichingSectionEl = document.getElementById("iching-section");
const kabbalahSectionEl = document.getElementById("kabbalah-section"); const kabbalahSectionEl = document.getElementById("kabbalah-section");
const kabbalahWorldsSectionEl = document.getElementById("kabbalah-worlds-section");
const kabbalahPathsSectionEl = document.getElementById("kabbalah-paths-section");
const kabbalahCrossSectionEl = document.getElementById("kabbalah-cross-section");
const kabbalahTreeSectionEl = document.getElementById("kabbalah-tree-section"); const kabbalahTreeSectionEl = document.getElementById("kabbalah-tree-section");
const cubeSectionEl = document.getElementById("cube-section"); const cubeSectionEl = document.getElementById("cube-section");
const alphabetSectionEl = document.getElementById("alphabet-section"); const alphabetSectionEl = document.getElementById("alphabet-section");
@@ -78,6 +81,10 @@ const openCyclesEl = document.getElementById("open-cycles");
const openElementsEl = document.getElementById("open-elements"); const openElementsEl = document.getElementById("open-elements");
const openIChingEl = document.getElementById("open-iching"); const openIChingEl = document.getElementById("open-iching");
const openKabbalahEl = document.getElementById("open-kabbalah"); const openKabbalahEl = document.getElementById("open-kabbalah");
const openKabbalahSephirotEl = document.getElementById("open-kabbalah-sephirot");
const openKabbalahWorldsEl = document.getElementById("open-kabbalah-worlds");
const openKabbalahPathsEl = document.getElementById("open-kabbalah-paths");
const openKabbalahCrossEl = document.getElementById("open-kabbalah-cross");
const openKabbalahTreeEl = document.getElementById("open-kabbalah-tree"); const openKabbalahTreeEl = document.getElementById("open-kabbalah-tree");
const openKabbalahCubeEl = document.getElementById("open-kabbalah-cube"); const openKabbalahCubeEl = document.getElementById("open-kabbalah-cube");
const openAlphabetEl = document.getElementById("open-alphabet"); const openAlphabetEl = document.getElementById("open-alphabet");
@@ -327,15 +334,22 @@ function getConnectionSettings() {
}; };
} }
function syncConnectionGateInputs() { function normalizeConnectionSettingsInput(connectionSettings = null) {
const connectionSettings = getConnectionSettings(); return {
apiBaseUrl: String(connectionSettings?.apiBaseUrl || "").trim().replace(/\/+$/, ""),
apiKey: String(connectionSettings?.apiKey || "").trim()
};
}
function syncConnectionGateInputs(connectionSettings = getConnectionSettings()) {
const normalizedConnectionSettings = normalizeConnectionSettingsInput(connectionSettings);
if (connectionGateBaseUrlEl) { if (connectionGateBaseUrlEl) {
connectionGateBaseUrlEl.value = String(connectionSettings.apiBaseUrl || ""); connectionGateBaseUrlEl.value = normalizedConnectionSettings.apiBaseUrl;
} }
if (connectionGateApiKeyEl) { if (connectionGateApiKeyEl) {
connectionGateApiKeyEl.value = String(connectionSettings.apiKey || ""); connectionGateApiKeyEl.value = normalizedConnectionSettings.apiKey;
} }
} }
@@ -352,8 +366,8 @@ function setConnectionGateStatus(text, tone = "default") {
} }
} }
function showConnectionGate(message, tone = "default") { function showConnectionGate(message, tone = "default", connectionSettings = null) {
syncConnectionGateInputs(); syncConnectionGateInputs(connectionSettings || getConnectionSettings());
if (connectionGateEl) { if (connectionGateEl) {
connectionGateEl.hidden = false; connectionGateEl.hidden = false;
} }
@@ -369,33 +383,49 @@ function hideConnectionGate() {
} }
function getConnectionSettingsFromGate() { function getConnectionSettingsFromGate() {
return { return normalizeConnectionSettingsInput({
apiBaseUrl: String(connectionGateBaseUrlEl?.value || "").trim(), apiBaseUrl: String(connectionGateBaseUrlEl?.value || "").trim(),
apiKey: String(connectionGateApiKeyEl?.value || "").trim() apiKey: String(connectionGateApiKeyEl?.value || "").trim()
}; });
}
function warmAllDeckImagesInBackground() {
const activeDeckId = String(window.TarotCardImages?.getActiveDeck?.() || "").trim();
window.TarotCardImages?.scheduleAllDeckImagePreload?.({
startDeckId: activeDeckId,
background: true,
includeThumbnails: true
});
} }
async function ensureConnectedApp(nextConnectionSettings = null) { async function ensureConnectedApp(nextConnectionSettings = null) {
if (nextConnectionSettings) { const configuredConnection = nextConnectionSettings
window.TarotAppConfig?.updateConnectionSettings?.(nextConnectionSettings); ? normalizeConnectionSettingsInput(nextConnectionSettings)
: getConnectionSettings();
if (!nextConnectionSettings) {
syncConnectionGateInputs(configuredConnection);
} }
syncConnectionGateInputs();
const configuredConnection = getConnectionSettings();
if (!configuredConnection.apiBaseUrl) { if (!configuredConnection.apiBaseUrl) {
showConnectionGate("Enter an API Base URL to load TaroTime.", "error"); showConnectionGate("Enter an API Base URL to load TaroTime.", "error", configuredConnection);
return false; return false;
} }
showConnectionGate("Connecting to the API...", "pending"); showConnectionGate("Connecting to the API...", "pending", configuredConnection);
const probeResult = await window.TarotDataService?.probeConnection?.(); const probeResult = await window.TarotDataService?.probeConnection?.(configuredConnection);
if (!probeResult?.ok) { if (!probeResult?.ok) {
showConnectionGate(probeResult?.message || "Unable to reach the API.", "error"); showConnectionGate(probeResult?.message || "Unable to reach the API.", "error", configuredConnection);
return false; return false;
} }
if (nextConnectionSettings) {
window.TarotAppConfig?.updateConnectionSettings?.(configuredConnection);
syncConnectionGateInputs(configuredConnection);
}
hideConnectionGate(); hideConnectionGate();
if (!hasRenderedConnectedShell) { if (!hasRenderedConnectedShell) {
sectionStateUi.setActiveSection?.("home"); sectionStateUi.setActiveSection?.("home");
@@ -405,6 +435,7 @@ async function ensureConnectedApp(nextConnectionSettings = null) {
setConnectionGateStatus("Connected.", "success"); setConnectionGateStatus("Connected.", "success");
setStatus(`Connected to ${configuredConnection.apiBaseUrl}.`); setStatus(`Connected to ${configuredConnection.apiBaseUrl}.`);
await appRuntime.renderWeek?.(); await appRuntime.renderWeek?.();
warmAllDeckImagesInBackground();
return true; return true;
} }
@@ -491,6 +522,9 @@ sectionStateUi.init?.({
elementsSectionEl, elementsSectionEl,
ichingSectionEl, ichingSectionEl,
kabbalahSectionEl, kabbalahSectionEl,
kabbalahWorldsSectionEl,
kabbalahPathsSectionEl,
kabbalahCrossSectionEl,
kabbalahTreeSectionEl, kabbalahTreeSectionEl,
cubeSectionEl, cubeSectionEl,
alphabetSectionEl, alphabetSectionEl,
@@ -519,6 +553,10 @@ sectionStateUi.init?.({
openElementsEl, openElementsEl,
openIChingEl, openIChingEl,
openKabbalahEl, openKabbalahEl,
openKabbalahSephirotEl,
openKabbalahWorldsEl,
openKabbalahPathsEl,
openKabbalahCrossEl,
openKabbalahTreeEl, openKabbalahTreeEl,
openKabbalahCubeEl, openKabbalahCubeEl,
openAlphabetEl, openAlphabetEl,
@@ -626,6 +664,10 @@ navigationUi.init?.({
openElementsEl, openElementsEl,
openIChingEl, openIChingEl,
openKabbalahEl, openKabbalahEl,
openKabbalahSephirotEl,
openKabbalahWorldsEl,
openKabbalahPathsEl,
openKabbalahCrossEl,
openKabbalahTreeEl, openKabbalahTreeEl,
openKabbalahCubeEl, openKabbalahCubeEl,
openAlphabetEl, openAlphabetEl,
+34 -1
View File
@@ -137,7 +137,7 @@
const standardMinorSuits = ["Wands", "Cups", "Swords", "Disks"]; const standardMinorSuits = ["Wands", "Cups", "Swords", "Disks"];
const standardDeckCardNames = buildStandardDeckCardNames(); const standardDeckCardNames = buildStandardDeckCardNames();
let deckManifestSources = buildDeckManifestSources(); let deckManifestSources = null;
const manifestCache = new Map(); const manifestCache = new Map();
const cardBackCache = new Map(); const cardBackCache = new Map();
@@ -161,6 +161,25 @@
.replace(/\/+$/, ""); .replace(/\/+$/, "");
} }
function buildManifestRequestHeaders(path) {
const normalizedPath = String(path || "").trim();
const apiBaseUrl = getApiBaseUrl();
const apiKey = String(
window.TarotDataService?.getApiKey?.()
|| window.TarotAppConfig?.getApiKey?.()
|| window.TarotAppConfig?.apiKey
|| ""
).trim();
if (!normalizedPath || !apiBaseUrl || !apiKey || !normalizedPath.startsWith(apiBaseUrl)) {
return {};
}
return {
"x-api-key": apiKey
};
}
function rewriteBasePathForApi(basePath) { function rewriteBasePathForApi(basePath) {
const normalizedBasePath = String(basePath || "").trim(); const normalizedBasePath = String(basePath || "").trim();
if (!normalizedBasePath) { if (!normalizedBasePath) {
@@ -530,6 +549,9 @@
try { try {
const request = new XMLHttpRequest(); const request = new XMLHttpRequest();
request.open("GET", encodeURI(path), false); request.open("GET", encodeURI(path), false);
Object.entries(buildManifestRequestHeaders(path)).forEach(([headerName, headerValue]) => {
request.setRequestHeader(headerName, headerValue);
});
request.send(null); request.send(null);
const okStatus = (request.status >= 200 && request.status < 300) || request.status === 0; const okStatus = (request.status >= 200 && request.status < 300) || request.status === 0;
@@ -1164,6 +1186,9 @@
}) })
.then((result) => { .then((result) => {
markDeckAsWarmed(normalizedDeckId); markDeckAsWarmed(normalizedDeckId);
if (options.background) {
emitDeckPreloadStatus();
}
if (!options.background) { if (!options.background) {
setDeckPreloadStatus({ setDeckPreloadStatus({
activeDeckId: normalizedDeckId, activeDeckId: normalizedDeckId,
@@ -1211,6 +1236,13 @@
})); }));
} }
function scheduleAllDeckImagePreload(options = {}) {
return deferPreload(() => preloadAllDeckImages({
...defaultDeckWarmupOptions,
...options
}));
}
function resolveDisplayNameWithDeck(deckId, cardName, trumpNumber) { function resolveDisplayNameWithDeck(deckId, cardName, trumpNumber) {
const manifest = getDeckManifest(deckId); const manifest = getDeckManifest(deckId);
const fallbackName = String(cardName || "").trim(); const fallbackName = String(cardName || "").trim();
@@ -1333,6 +1365,7 @@
resolveTarotCardBackThumbnail, resolveTarotCardBackThumbnail,
preloadDeckImages, preloadDeckImages,
preloadAllDeckImages, preloadAllDeckImages,
scheduleAllDeckImagePreload,
ensureImageLoaded, ensureImageLoaded,
isImageLoaded, isImageLoaded,
getDeckPreloadStatus: () => emitDeckPreloadStatus(), getDeckPreloadStatus: () => emitDeckPreloadStatus(),
+24 -9
View File
@@ -102,8 +102,22 @@
pluto: "Pluto" pluto: "Pluto"
}; };
function buildRequestHeaders() { function resolveConnectionSettings(connectionSettings = null) {
const apiKey = getApiKey(); if (!connectionSettings || typeof connectionSettings !== "object") {
return {
apiBaseUrl: getApiBaseUrl(),
apiKey: getApiKey()
};
}
return {
apiBaseUrl: normalizeApiBaseUrl(connectionSettings.apiBaseUrl),
apiKey: String(connectionSettings.apiKey || "").trim()
};
}
function buildRequestHeaders(connectionSettings = null) {
const { apiKey } = resolveConnectionSettings(connectionSettings);
return apiKey return apiKey
? { ? {
"x-api-key": apiKey "x-api-key": apiKey
@@ -172,8 +186,8 @@
.join("/"); .join("/");
} }
function buildApiUrl(path, query = {}) { function buildApiUrl(path, query = {}, connectionSettings = null) {
const apiBaseUrl = getApiBaseUrl(); const { apiBaseUrl } = resolveConnectionSettings(connectionSettings);
if (!apiBaseUrl) { if (!apiBaseUrl) {
return ""; return "";
} }
@@ -569,8 +583,9 @@
})); }));
} }
async function probeConnection() { async function probeConnection(connectionSettings = null) {
const apiBaseUrl = getApiBaseUrl(); const resolvedConnection = resolveConnectionSettings(connectionSettings);
const apiBaseUrl = resolvedConnection.apiBaseUrl;
if (!apiBaseUrl) { if (!apiBaseUrl) {
return { return {
ok: false, ok: false,
@@ -580,11 +595,11 @@
} }
const requestOptions = { const requestOptions = {
headers: buildRequestHeaders() headers: buildRequestHeaders(resolvedConnection)
}; };
try { try {
const healthResponse = await fetch(buildApiUrl("/api/v1/health"), requestOptions); const healthResponse = await fetch(buildApiUrl("/api/v1/health", {}, resolvedConnection), requestOptions);
if (!healthResponse.ok) { if (!healthResponse.ok) {
return { return {
ok: false, ok: false,
@@ -594,7 +609,7 @@
} }
const health = await healthResponse.json().catch(() => null); const health = await healthResponse.json().catch(() => null);
const protectedResponse = await fetch(buildApiUrl("/api/v1/decks/options"), requestOptions); const protectedResponse = await fetch(buildApiUrl("/api/v1/decks/options", {}, resolvedConnection), requestOptions);
if (protectedResponse.status === 401 || protectedResponse.status === 403) { if (protectedResponse.status === 401 || protectedResponse.status === 403) {
return { return {
+161 -1
View File
@@ -792,7 +792,7 @@
} }
.tarot-detail-top { .tarot-detail-top {
display: grid; display: grid;
grid-template-columns: 150px minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
gap: 16px; gap: 16px;
align-items: start; align-items: start;
} }
@@ -866,6 +866,93 @@
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.04em; letter-spacing: 0.04em;
} }
.tarot-deck-gallery-card {
grid-column: 1 / -1;
}
.tarot-deck-gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
}
.tarot-deck-variant {
display: grid;
gap: 8px;
align-content: start;
width: 100%;
padding: 8px;
border: 1px solid #3f3f46;
border-radius: 10px;
background: #18181b;
color: #e4e4e7;
cursor: pointer;
text-align: left;
transition: background 120ms, border-color 120ms, transform 120ms;
}
.tarot-deck-variant:hover {
background: #27272a;
border-color: #52525b;
transform: translateY(-1px);
}
.tarot-deck-variant.is-active {
border-color: #a5b4fc;
box-shadow: inset 0 0 0 1px rgba(165, 180, 252, 0.28);
}
.tarot-deck-variant-image {
width: 100%;
aspect-ratio: 2 / 3;
object-fit: contain;
object-position: center;
display: block;
padding: 4px;
box-sizing: border-box;
border-radius: 8px;
border: 1px solid #3f3f46;
background: #09090b;
}
.tarot-deck-variant-label {
display: grid;
gap: 2px;
font-size: 12px;
line-height: 1.3;
}
.tarot-deck-variant-deck {
font-weight: 600;
color: #f4f4f5;
}
.tarot-deck-variant-name {
color: #a1a1aa;
}
.tarot-deck-variant-active {
color: #a5b4fc;
font-size: 11px;
letter-spacing: 0.03em;
text-transform: uppercase;
}
@media (max-width: 720px) {
.tarot-deck-gallery {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.tarot-deck-variant {
padding: 6px;
gap: 6px;
}
.tarot-deck-variant-label {
font-size: 11px;
}
.tarot-deck-variant-active {
font-size: 10px;
}
}
@media (max-width: 420px) {
.tarot-deck-gallery {
grid-template-columns: minmax(0, 1fr);
}
}
.tarot-keywords { .tarot-keywords {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -3190,6 +3277,43 @@
line-height: 1.45; line-height: 1.45;
color: #e4e4e7; color: #e4e4e7;
} }
.detail-sequence-nav {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.detail-sequence-btn {
min-height: 34px;
padding: 7px 12px;
border-radius: 999px;
border: 1px solid #3f3f46;
background: #111118;
color: #f4f4f5;
cursor: pointer;
font-size: 12px;
line-height: 1;
transition: background 120ms, border-color 120ms, color 120ms;
}
.detail-sequence-btn:hover {
background: #27272a;
border-color: #52525b;
}
.detail-sequence-btn:disabled {
opacity: 0.45;
cursor: default;
}
.detail-sequence-btn:disabled:hover {
background: #111118;
border-color: #3f3f46;
}
.detail-sequence-position {
min-width: 78px;
color: #a1a1aa;
font-size: 12px;
line-height: 1.2;
}
.planet-meta-grid { .planet-meta-grid {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
@@ -3520,6 +3644,34 @@
} }
#kabbalah-section[hidden] { display: none; } #kabbalah-section[hidden] { display: none; }
#kabbalah-worlds-section {
height: calc(100vh - 61px);
background: #18181b;
box-sizing: border-box;
overflow: hidden;
}
#kabbalah-worlds-section[hidden] { display: none; }
#kabbalah-paths-section {
height: calc(100vh - 61px);
background: #18181b;
box-sizing: border-box;
overflow: hidden;
}
#kabbalah-paths-section[hidden] { display: none; }
#kabbalah-cross-section {
height: calc(100vh - 61px);
background: #18181b;
box-sizing: border-box;
overflow: hidden;
}
#kabbalah-cross-section[hidden] { display: none; }
.kab-browser-intro {
padding: 0 12px 8px;
}
#kabbalah-tree-section { #kabbalah-tree-section {
height: calc(100vh - 61px); height: calc(100vh - 61px);
background: #18181b; background: #18181b;
@@ -5692,6 +5844,14 @@
gap: 10px; gap: 10px;
} }
.detail-sequence-nav {
width: 100%;
}
.detail-sequence-position {
min-width: 0;
}
.alpha-text-controls--heading { .alpha-text-controls--heading {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
+71
View File
@@ -63,6 +63,7 @@
hebrewById: new Map(), hebrewById: new Map(),
dayLinksCache: new Map() dayLinksCache: new Map()
}; };
let detailNavigator = null;
const TAROT_TRUMP_NUMBER_BY_NAME = { const TAROT_TRUMP_NUMBER_BY_NAME = {
"the fool": 0, "the fool": 0,
@@ -200,6 +201,7 @@
function getElements() { function getElements() {
return { return {
sectionEl: document.getElementById("calendar-section"),
monthListEl: document.getElementById("calendar-month-list"), monthListEl: document.getElementById("calendar-month-list"),
monthCountEl: document.getElementById("calendar-month-count"), monthCountEl: document.getElementById("calendar-month-count"),
listTitleEl: document.getElementById("calendar-list-title"), listTitleEl: document.getElementById("calendar-list-title"),
@@ -210,6 +212,9 @@
searchClearEl: document.getElementById("calendar-search-clear"), searchClearEl: document.getElementById("calendar-search-clear"),
detailNameEl: document.getElementById("calendar-detail-name"), detailNameEl: document.getElementById("calendar-detail-name"),
detailSubEl: document.getElementById("calendar-detail-sub"), detailSubEl: document.getElementById("calendar-detail-sub"),
detailPrevEl: document.getElementById("calendar-detail-prev"),
detailPositionEl: document.getElementById("calendar-detail-position"),
detailNextEl: document.getElementById("calendar-detail-next"),
detailBodyEl: document.getElementById("calendar-detail-body") detailBodyEl: document.getElementById("calendar-detail-body")
}; };
} }
@@ -523,6 +528,66 @@
function renderDetail(elements) { function renderDetail(elements) {
calendarDetailUi.renderDetail?.(elements); calendarDetailUi.renderDetail?.(elements);
syncDetailNavigation(elements);
}
function getMonthSequenceState() {
const total = state.filteredMonths.length;
const currentIndex = state.filteredMonths.findIndex((month) => month.id === state.selectedMonthId);
return {
total,
currentIndex,
previousId: currentIndex > 0 ? state.filteredMonths[currentIndex - 1].id : "",
nextId: currentIndex >= 0 && currentIndex < total - 1 ? state.filteredMonths[currentIndex + 1].id : ""
};
}
function getDetailNavigator() {
if (detailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") {
return detailNavigator;
}
detailNavigator = window.TarotSequenceNav.createSequenceNavigator({
getElements,
isActive: (elements) => Boolean(elements?.sectionEl && elements.sectionEl.hidden === false),
getSequenceState: getMonthSequenceState,
getPrevButton: (elements) => elements?.detailPrevEl,
getNextButton: (elements) => elements?.detailNextEl,
getPositionEl: (elements) => elements?.detailPositionEl,
formatPositionText: ({ total, currentIndex }) => {
if (total > 0 && currentIndex >= 0) {
const suffix = state.searchQuery ? " shown" : "";
return `${currentIndex + 1} of ${total}${suffix}`;
}
return total > 0 ? `${total} months` : "No months";
},
selectTarget: (targetId, elements) => selectByMonthId(targetId, elements) !== false,
afterSelect: (targetId, elements) => {
scrollMonthIntoView(targetId, elements);
}
});
return detailNavigator;
}
function syncDetailNavigation(elements = getElements()) {
getDetailNavigator()?.sync(elements);
}
function scrollMonthIntoView(monthId, elements = getElements()) {
elements?.monthListEl
?.querySelector(`[data-month-id="${monthId}"]`)
?.scrollIntoView({ block: "nearest" });
}
function selectAdjacentMonth(offset, elements = getElements()) {
return getDetailNavigator()?.step(offset, elements) === true;
}
function bindKeyboardNavigation(elements) {
getDetailNavigator()?.bind(elements);
} }
function applySearchFilter(elements) { function applySearchFilter(elements) {
@@ -613,6 +678,10 @@
} }
} }
function bindDetailNavigation(elements) {
getDetailNavigator()?.bind(elements);
}
function loadCalendarType(calendarId, elements) { function loadCalendarType(calendarId, elements) {
const months = state.calendarData[calendarId]; const months = state.calendarData[calendarId];
if (!Array.isArray(months)) { if (!Array.isArray(months)) {
@@ -745,6 +814,8 @@
bindYearInput(elements); bindYearInput(elements);
bindSearchInput(elements); bindSearchInput(elements);
bindCalendarTypeSelect(elements); bindCalendarTypeSelect(elements);
bindDetailNavigation(elements);
bindKeyboardNavigation(elements);
} }
applySearchFilter(elements); applySearchFilter(elements);
+68
View File
@@ -42,6 +42,7 @@
hebrewById: new Map(), hebrewById: new Map(),
calendarData: {} calendarData: {}
}; };
let detailNavigator = null;
const TAROT_TRUMP_NUMBER_BY_NAME = { const TAROT_TRUMP_NUMBER_BY_NAME = {
"the fool": 0, "the fool": 0,
@@ -93,6 +94,7 @@
function getElements() { function getElements() {
return { return {
sectionEl: document.getElementById("holiday-section"),
sourceSelectEl: document.getElementById("holiday-source-select"), sourceSelectEl: document.getElementById("holiday-source-select"),
yearInputEl: document.getElementById("holiday-year-input"), yearInputEl: document.getElementById("holiday-year-input"),
searchInputEl: document.getElementById("holiday-search-input"), searchInputEl: document.getElementById("holiday-search-input"),
@@ -101,6 +103,9 @@
listEl: document.getElementById("holiday-list"), listEl: document.getElementById("holiday-list"),
detailNameEl: document.getElementById("holiday-detail-name"), detailNameEl: document.getElementById("holiday-detail-name"),
detailSubEl: document.getElementById("holiday-detail-sub"), detailSubEl: document.getElementById("holiday-detail-sub"),
detailPrevEl: document.getElementById("holiday-detail-prev"),
detailPositionEl: document.getElementById("holiday-detail-position"),
detailNextEl: document.getElementById("holiday-detail-next"),
detailBodyEl: document.getElementById("holiday-detail-body") detailBodyEl: document.getElementById("holiday-detail-body")
}; };
} }
@@ -225,6 +230,7 @@
detailNameEl.textContent = "--"; detailNameEl.textContent = "--";
detailSubEl.textContent = "Select a holiday to explore"; detailSubEl.textContent = "Select a holiday to explore";
detailBodyEl.innerHTML = ""; detailBodyEl.innerHTML = "";
syncDetailNavigation(elements);
return; return;
} }
@@ -232,6 +238,66 @@
detailSubEl.textContent = `${holidayDataUi.calendarLabel(holiday?.calendarId)} - ${holidayDataUi.monthLabelForCalendar(state.calendarData, holiday?.calendarId, holiday?.monthId)}`; detailSubEl.textContent = `${holidayDataUi.calendarLabel(holiday?.calendarId)} - ${holidayDataUi.monthLabelForCalendar(state.calendarData, holiday?.calendarId, holiday?.monthId)}`;
detailBodyEl.innerHTML = renderHolidayDetail(holiday); detailBodyEl.innerHTML = renderHolidayDetail(holiday);
attachNavHandlers(detailBodyEl); attachNavHandlers(detailBodyEl);
syncDetailNavigation(elements);
}
function getHolidaySequenceState() {
const total = state.filteredHolidays.length;
const currentIndex = state.filteredHolidays.findIndex((holiday) => holiday.id === state.selectedHolidayId);
return {
total,
currentIndex,
previousId: currentIndex > 0 ? state.filteredHolidays[currentIndex - 1].id : "",
nextId: currentIndex >= 0 && currentIndex < total - 1 ? state.filteredHolidays[currentIndex + 1].id : ""
};
}
function getDetailNavigator() {
if (detailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") {
return detailNavigator;
}
detailNavigator = window.TarotSequenceNav.createSequenceNavigator({
getElements,
isActive: (elements) => Boolean(elements?.sectionEl && elements.sectionEl.hidden === false),
getSequenceState: getHolidaySequenceState,
getPrevButton: (elements) => elements?.detailPrevEl,
getNextButton: (elements) => elements?.detailNextEl,
getPositionEl: (elements) => elements?.detailPositionEl,
formatPositionText: ({ total, currentIndex }) => {
if (total > 0 && currentIndex >= 0) {
const suffix = state.searchQuery ? " shown" : "";
return `${currentIndex + 1} of ${total}${suffix}`;
}
return total > 0 ? `${total} holidays` : "No holidays";
},
selectTarget: (targetId, elements) => selectByHolidayId(targetId, elements) !== false,
afterSelect: (targetId, elements) => {
scrollHolidayIntoView(targetId, elements);
}
});
return detailNavigator;
}
function syncDetailNavigation(elements = getElements()) {
getDetailNavigator()?.sync(elements);
}
function scrollHolidayIntoView(holidayId, elements = getElements()) {
elements?.listEl
?.querySelector(`[data-holiday-id="${holidayId}"]`)
?.scrollIntoView({ block: "nearest" });
}
function selectAdjacentHoliday(offset, elements = getElements()) {
return getDetailNavigator()?.step(offset, elements) === true;
}
function bindKeyboardNavigation(elements) {
getDetailNavigator()?.bind(elements);
} }
function applyFilters(elements) { function applyFilters(elements) {
@@ -307,6 +373,8 @@
elements.searchInputEl.focus(); elements.searchInputEl.focus();
}); });
} }
bindKeyboardNavigation(elements);
} }
function attachNavHandlers(detailBodyEl) { function attachNavHandlers(detailBodyEl) {
+61 -3
View File
@@ -309,6 +309,62 @@
return card; return card;
} }
function renderWorldLayerDetail(context) {
const { worldLayer, tree, elements } = context;
if (!worldLayer || !elements?.detailBodyEl) {
return;
}
elements.detailNameEl.textContent = String(worldLayer.world || "Qabalistic World");
elements.detailSubEl.textContent = [
worldLayer.slot ? `${worldLayer.slot}: ${worldLayer.letterChar || ""}`.trim() : "",
worldLayer.soulLayer
].filter(Boolean).join(" · ");
elements.detailBodyEl.innerHTML = "";
elements.detailBodyEl.appendChild(metaCard(
"World Layer",
`${worldLayer.worldLayer || "—"}${worldLayer.worldDescription ? ` · ${worldLayer.worldDescription}` : ""}`,
true
));
elements.detailBodyEl.appendChild(metaCard(
"Soul Layer",
`${worldLayer.soulLayer || "—"}${worldLayer.soulTitle ? `${worldLayer.soulTitle}` : ""}${worldLayer.soulDescription ? `: ${worldLayer.soulDescription}` : ""}`,
true
));
const linkedParts = [];
const hebrewLetterId = context.resolveHebrewLetterId(worldLayer.hebrewToken);
if (hebrewLetterId) {
linkedParts.push(createInlineEventLink(
`${worldLayer.letterChar || ""} ${worldLayer.hebrewToken || ""}`.replace(/\s+/g, " ").trim(),
"nav:alphabet",
{
alphabet: "hebrew",
hebrewLetterId
}
));
}
const linkedPath = context.findPathByHebrewToken(tree, worldLayer.hebrewToken);
if (linkedPath?.pathNumber != null) {
if (linkedParts.length) {
linkedParts.push(" · ");
}
linkedParts.push(createInlineEventLink(
`Path ${linkedPath.pathNumber}`,
"nav:kabbalah-path",
{ pathNo: Number(linkedPath.pathNumber) }
));
}
if (linkedParts.length) {
elements.detailBodyEl.appendChild(metaCard("Linked Attributions", inlineValue(linkedParts)));
}
elements.detailBodyEl.appendChild(buildFourWorldsCard(tree, worldLayer.hebrewToken, context));
}
function splitCorrespondenceNames(value) { function splitCorrespondenceNames(value) {
return String(value || "") return String(value || "")
.split(/,|;|·|\/|\bor\b|\band\b|\+/i) .split(/,|;|·|\/|\bor\b|\band\b|\+/i)
@@ -419,12 +475,14 @@
function renderSephiraDetail(context) { function renderSephiraDetail(context) {
const { seph, tree, elements } = context; const { seph, tree, elements } = context;
elements.detailNameEl.textContent = `${seph.number} · ${seph.name}`; const displayNumber = String(seph.displayNumber || seph.number || "").trim();
elements.detailNameEl.textContent = displayNumber
? `${displayNumber} · ${seph.name}`
: `${seph.name}`;
elements.detailSubEl.textContent = elements.detailSubEl.textContent =
[seph.nameHebrew, seph.translation, seph.planet].filter(Boolean).join(" · "); [seph.nameHebrew, seph.translation, seph.planet].filter(Boolean).join(" · ");
elements.detailBodyEl.innerHTML = ""; elements.detailBodyEl.innerHTML = "";
elements.detailBodyEl.appendChild(buildFourWorldsCard(tree, "", context));
elements.detailBodyEl.appendChild(buildPlanetLuminaryCard(seph.planet, context)); elements.detailBodyEl.appendChild(buildPlanetLuminaryCard(seph.planet, context));
elements.detailBodyEl.appendChild(metaCard("Intelligence", seph.intelligence)); elements.detailBodyEl.appendChild(metaCard("Intelligence", seph.intelligence));
elements.detailBodyEl.appendChild(buildTarotAttributionCard(seph.tarot)); elements.detailBodyEl.appendChild(buildTarotAttributionCard(seph.tarot));
@@ -484,7 +542,6 @@
elements.detailSubEl.textContent = [path.tarot?.card, astro].filter(Boolean).join(" · "); elements.detailSubEl.textContent = [path.tarot?.card, astro].filter(Boolean).join(" · ");
elements.detailBodyEl.innerHTML = ""; elements.detailBodyEl.innerHTML = "";
elements.detailBodyEl.appendChild(buildFourWorldsCard(tree, context.activeHebrewToken, context));
elements.detailBodyEl.appendChild(buildConnectsCard(path, fromName, toName)); elements.detailBodyEl.appendChild(buildConnectsCard(path, fromName, toName));
elements.detailBodyEl.appendChild(buildHebrewLetterCard(letter, context)); elements.detailBodyEl.appendChild(buildHebrewLetterCard(letter, context));
elements.detailBodyEl.appendChild(buildAstrologyCard(path.astrology, context)); elements.detailBodyEl.appendChild(buildAstrologyCard(path.astrology, context));
@@ -543,6 +600,7 @@
} }
window.KabbalahDetailUi = { window.KabbalahDetailUi = {
renderWorldLayerDetail,
renderSephiraDetail, renderSephiraDetail,
renderPathDetail, renderPathDetail,
renderRoseLandingIntro renderRoseLandingIntro
+992 -32
View File
File diff suppressed because it is too large Load Diff
+45 -7
View File
@@ -22,6 +22,20 @@
return config.getMagickDataset?.() || null; return config.getMagickDataset?.() || null;
} }
function getKabbalahPathNo(detail) {
if (!detail || typeof detail !== "object") {
return null;
}
const rawValue = detail.pathNo ?? detail["path-no"] ?? null;
if (rawValue === null || rawValue === undefined || rawValue === "") {
return null;
}
const numericValue = Number(rawValue);
return Number.isFinite(numericValue) ? numericValue : null;
}
const DETAIL_VIEW_SELECTOR_BY_SECTION = { const DETAIL_VIEW_SELECTOR_BY_SECTION = {
tarot: "#tarot-browse-view .tarot-layout", tarot: "#tarot-browse-view .tarot-layout",
cube: "#cube-layout", cube: "#cube-layout",
@@ -31,6 +45,10 @@
iching: "#iching-section .planet-layout", iching: "#iching-section .planet-layout",
gods: "#gods-section .planet-layout", gods: "#gods-section .planet-layout",
calendar: "#calendar-section .planet-layout", calendar: "#calendar-section .planet-layout",
kabbalah: "#kabbalah-section .planet-layout",
"kabbalah-worlds": "#kabbalah-worlds-section .planet-layout",
"kabbalah-paths": "#kabbalah-paths-section .planet-layout",
"kabbalah-cross": "#kabbalah-cross-section .kab-rose-layout",
"kabbalah-tree": "#kabbalah-tree-section .kab-layout", "kabbalah-tree": "#kabbalah-tree-section .kab-layout",
planets: "#planet-section .planet-layout", planets: "#planet-section .planet-layout",
elements: "#elements-section .planet-layout" elements: "#elements-section .planet-layout"
@@ -167,12 +185,28 @@
setActiveSection(getActiveSection() === "kabbalah" ? "home" : "kabbalah"); setActiveSection(getActiveSection() === "kabbalah" ? "home" : "kabbalah");
}); });
bindClick(elements.openKabbalahSephirotEl, () => {
setActiveSection("kabbalah");
});
bindClick(elements.openKabbalahWorldsEl, () => {
setActiveSection("kabbalah-worlds");
});
bindClick(elements.openKabbalahPathsEl, () => {
setActiveSection("kabbalah-paths");
});
bindClick(elements.openKabbalahCrossEl, () => {
setActiveSection("kabbalah-cross");
});
bindClick(elements.openKabbalahTreeEl, () => { bindClick(elements.openKabbalahTreeEl, () => {
setActiveSection(getActiveSection() === "kabbalah-tree" ? "home" : "kabbalah-tree"); setActiveSection("kabbalah-tree");
}); });
bindClick(elements.openKabbalahCubeEl, () => { bindClick(elements.openKabbalahCubeEl, () => {
setActiveSection(getActiveSection() === "cube" ? "home" : "cube"); setActiveSection("cube");
}); });
bindClick(elements.openAlphabetWordEl, () => { bindClick(elements.openAlphabetWordEl, () => {
@@ -403,17 +437,21 @@
document.addEventListener("nav:kabbalah-path", (event) => { document.addEventListener("nav:kabbalah-path", (event) => {
const magickDataset = getMagickDataset(); const magickDataset = getMagickDataset();
const pathNo = event?.detail?.pathNo; const pathNo = getKabbalahPathNo(event?.detail);
if (typeof ensure.ensureKabbalahSection === "function" && magickDataset) { if (typeof ensure.ensureKabbalahSection === "function" && magickDataset) {
ensure.ensureKabbalahSection(magickDataset); ensure.ensureKabbalahSection(magickDataset);
} }
setActiveSection("kabbalah-tree");
if (pathNo != null) { if (pathNo != null) {
const targetSection = Number(pathNo) >= 11 ? "kabbalah-paths" : "kabbalah";
setActiveSection(targetSection);
requestAnimationFrame(() => { requestAnimationFrame(() => {
window.KabbalahSectionUi?.selectNode?.(pathNo); window.KabbalahSectionUi?.selectNode?.(pathNo);
scheduleSectionDetailOnly("kabbalah-tree"); scheduleSectionDetailOnly(targetSection);
}); });
return;
} }
setActiveSection("kabbalah-paths");
}); });
document.addEventListener("nav:planet", (event) => { document.addEventListener("nav:planet", (event) => {
@@ -473,7 +511,7 @@
}); });
document.addEventListener("tarot:view-kab-path", (event) => { document.addEventListener("tarot:view-kab-path", (event) => {
setActiveSection("kabbalah-tree"); setActiveSection("kabbalah-paths");
const pathNumber = event?.detail?.pathNumber; const pathNumber = event?.detail?.pathNumber;
if (pathNumber != null) { if (pathNumber != null) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -483,7 +521,7 @@
} else { } else {
kabbalahUi?.selectPathByNumber?.(pathNumber); kabbalahUi?.selectPathByNumber?.(pathNumber);
} }
scheduleSectionDetailOnly("kabbalah-tree"); scheduleSectionDetailOnly("kabbalah-paths");
}); });
} }
}); });
+74 -3
View File
@@ -21,6 +21,7 @@
monthRefsByPlanetId: new Map(), monthRefsByPlanetId: new Map(),
cubePlacementsByPlanetId: new Map() cubePlacementsByPlanetId: new Map()
}; };
let detailNavigator = null;
function normalizePlanetToken(value) { function normalizePlanetToken(value) {
return String(value || "") return String(value || "")
@@ -80,12 +81,16 @@
function getElements() { function getElements() {
return { return {
planetSectionEl: document.getElementById("planet-section"),
planetCardListEl: document.getElementById("planet-card-list"), planetCardListEl: document.getElementById("planet-card-list"),
planetSearchInputEl: document.getElementById("planet-search-input"), planetSearchInputEl: document.getElementById("planet-search-input"),
planetSearchClearEl: document.getElementById("planet-search-clear"), planetSearchClearEl: document.getElementById("planet-search-clear"),
planetCountEl: document.getElementById("planet-card-count"), planetCountEl: document.getElementById("planet-card-count"),
planetDetailNameEl: document.getElementById("planet-detail-name"), planetDetailNameEl: document.getElementById("planet-detail-name"),
planetDetailTypeEl: document.getElementById("planet-detail-type"), planetDetailTypeEl: document.getElementById("planet-detail-type"),
planetDetailPrevEl: document.getElementById("planet-detail-prev"),
planetDetailPositionEl: document.getElementById("planet-detail-position"),
planetDetailNextEl: document.getElementById("planet-detail-next"),
planetDetailSummaryEl: document.getElementById("planet-detail-summary"), planetDetailSummaryEl: document.getElementById("planet-detail-summary"),
planetDetailFactsEl: document.getElementById("planet-detail-facts"), planetDetailFactsEl: document.getElementById("planet-detail-facts"),
planetDetailAtmosphereEl: document.getElementById("planet-detail-atmosphere"), planetDetailAtmosphereEl: document.getElementById("planet-detail-atmosphere"),
@@ -156,11 +161,14 @@
if (!state.filteredEntries.some((entry) => entry.id === state.selectedId)) { if (!state.filteredEntries.some((entry) => entry.id === state.selectedId)) {
if (state.filteredEntries.length > 0) { if (state.filteredEntries.length > 0) {
selectById(state.filteredEntries[0].id, elements); selectById(state.filteredEntries[0].id, elements);
} else {
syncDetailNavigation(elements);
} }
return; return;
} }
updateSelection(elements); updateSelection(elements);
syncDetailNavigation(elements);
} }
function clearChildren(element) { function clearChildren(element) {
@@ -434,6 +442,68 @@
}); });
} }
function getSequenceState() {
const total = state.filteredEntries.length;
const currentIndex = state.filteredEntries.findIndex((entry) => entry.id === state.selectedId);
return {
total,
currentIndex,
previousId: currentIndex > 0 ? state.filteredEntries[currentIndex - 1].id : "",
nextId: currentIndex >= 0 && currentIndex < total - 1 ? state.filteredEntries[currentIndex + 1].id : ""
};
}
function getDetailNavigator() {
if (detailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") {
return detailNavigator;
}
detailNavigator = window.TarotSequenceNav.createSequenceNavigator({
getElements,
isActive: (elements) => Boolean(elements?.planetSectionEl && elements.planetSectionEl.hidden === false),
getSequenceState,
getPrevButton: (elements) => elements?.planetDetailPrevEl,
getNextButton: (elements) => elements?.planetDetailNextEl,
getPositionEl: (elements) => elements?.planetDetailPositionEl,
formatPositionText: ({ total, currentIndex }) => {
if (total > 0 && currentIndex >= 0) {
const suffix = state.searchQuery ? " shown" : "";
return `${currentIndex + 1} of ${total}${suffix}`;
}
return total > 0 ? `${total} bodies` : "No bodies";
},
selectTarget: (targetId, elements) => {
selectById(targetId, elements);
return true;
},
afterSelect: (targetId, elements) => {
scrollEntryIntoView(targetId, elements);
}
});
return detailNavigator;
}
function syncDetailNavigation(elements) {
getDetailNavigator()?.sync(elements);
}
function scrollEntryIntoView(id, elements) {
elements?.planetCardListEl
?.querySelector(`[data-planet-id="${id}"]`)
?.scrollIntoView({ block: "nearest" });
}
function selectAdjacentEntry(offset, elements) {
return getDetailNavigator()?.step(offset, elements) === true;
}
function bindKeyboardNavigation(elements) {
getDetailNavigator()?.bind(elements);
}
function selectById(id, elements) { function selectById(id, elements) {
const entry = state.entries.find((planet) => planet.id === id); const entry = state.entries.find((planet) => planet.id === id);
if (!entry) { if (!entry) {
@@ -443,6 +513,7 @@
state.selectedId = entry.id; state.selectedId = entry.id;
updateSelection(elements); updateSelection(elements);
renderDetail(entry, elements); renderDetail(entry, elements);
syncDetailNavigation(elements);
} }
function renderList(elements) { function renderList(elements) {
@@ -580,6 +651,8 @@
}); });
} }
bindKeyboardNavigation(elements);
state.initialized = true; state.initialized = true;
} }
@@ -594,9 +667,7 @@
); );
if (!entry) return; if (!entry) return;
selectById(entry.id, el); selectById(entry.id, el);
el.planetCardListEl scrollEntryIntoView(entry.id, el);
?.querySelector(`[data-planet-id="${entry.id}"]`)
?.scrollIntoView({ block: "nearest" });
} }
window.PlanetSectionUi = { window.PlanetSectionUi = {
+15 -2
View File
@@ -19,6 +19,9 @@
"elements", "elements",
"iching", "iching",
"kabbalah", "kabbalah",
"kabbalah-worlds",
"kabbalah-paths",
"kabbalah-cross",
"kabbalah-tree", "kabbalah-tree",
"cube", "cube",
"alphabet", "alphabet",
@@ -109,9 +112,12 @@
const isElementsOpen = activeSection === "elements"; const isElementsOpen = activeSection === "elements";
const isIChingOpen = activeSection === "iching"; const isIChingOpen = activeSection === "iching";
const isKabbalahOpen = activeSection === "kabbalah"; const isKabbalahOpen = activeSection === "kabbalah";
const isKabbalahWorldsOpen = activeSection === "kabbalah-worlds";
const isKabbalahPathsOpen = activeSection === "kabbalah-paths";
const isKabbalahCrossOpen = activeSection === "kabbalah-cross";
const isKabbalahTreeOpen = activeSection === "kabbalah-tree"; const isKabbalahTreeOpen = activeSection === "kabbalah-tree";
const isCubeOpen = activeSection === "cube"; const isCubeOpen = activeSection === "cube";
const isKabbalahMenuOpen = isKabbalahOpen || isKabbalahTreeOpen || isCubeOpen; const isKabbalahMenuOpen = isKabbalahOpen || isKabbalahWorldsOpen || isKabbalahPathsOpen || isKabbalahCrossOpen || isKabbalahTreeOpen || isCubeOpen;
const isAlphabetOpen = activeSection === "alphabet"; const isAlphabetOpen = activeSection === "alphabet";
const isAlphabetLettersOpen = activeSection === "alphabet-letters"; const isAlphabetLettersOpen = activeSection === "alphabet-letters";
const isAlphabetTextOpen = activeSection === "alphabet-text"; const isAlphabetTextOpen = activeSection === "alphabet-text";
@@ -137,6 +143,9 @@
setHidden(elements.elementsSectionEl, !isElementsOpen); setHidden(elements.elementsSectionEl, !isElementsOpen);
setHidden(elements.ichingSectionEl, !isIChingOpen); setHidden(elements.ichingSectionEl, !isIChingOpen);
setHidden(elements.kabbalahSectionEl, !isKabbalahOpen); setHidden(elements.kabbalahSectionEl, !isKabbalahOpen);
setHidden(elements.kabbalahWorldsSectionEl, !isKabbalahWorldsOpen);
setHidden(elements.kabbalahPathsSectionEl, !isKabbalahPathsOpen);
setHidden(elements.kabbalahCrossSectionEl, !isKabbalahCrossOpen);
setHidden(elements.kabbalahTreeSectionEl, !isKabbalahTreeOpen); setHidden(elements.kabbalahTreeSectionEl, !isKabbalahTreeOpen);
setHidden(elements.cubeSectionEl, !isCubeOpen); setHidden(elements.cubeSectionEl, !isCubeOpen);
setHidden(elements.alphabetSectionEl, !isAlphabetOpen); setHidden(elements.alphabetSectionEl, !isAlphabetOpen);
@@ -168,6 +177,10 @@
setPressed(elements.openElementsEl, isElementsOpen); setPressed(elements.openElementsEl, isElementsOpen);
setPressed(elements.openIChingEl, isIChingOpen); setPressed(elements.openIChingEl, isIChingOpen);
setPressed(elements.openKabbalahEl, isKabbalahMenuOpen); setPressed(elements.openKabbalahEl, isKabbalahMenuOpen);
toggleActive(elements.openKabbalahSephirotEl, isKabbalahOpen);
toggleActive(elements.openKabbalahWorldsEl, isKabbalahWorldsOpen);
toggleActive(elements.openKabbalahPathsEl, isKabbalahPathsOpen);
toggleActive(elements.openKabbalahCrossEl, isKabbalahCrossOpen);
toggleActive(elements.openKabbalahTreeEl, isKabbalahTreeOpen); toggleActive(elements.openKabbalahTreeEl, isKabbalahTreeOpen);
toggleActive(elements.openKabbalahCubeEl, isCubeOpen); toggleActive(elements.openKabbalahCubeEl, isCubeOpen);
setPressed(elements.openAlphabetEl, isAlphabetMenuOpen); setPressed(elements.openAlphabetEl, isAlphabetMenuOpen);
@@ -249,7 +262,7 @@
return; return;
} }
if (isKabbalahOpen || isKabbalahTreeOpen) { if (isKabbalahOpen || isKabbalahWorldsOpen || isKabbalahPathsOpen || isKabbalahCrossOpen || isKabbalahTreeOpen) {
ensure.ensureKabbalahSection?.(magickDataset); ensure.ensureKabbalahSection?.(magickDataset);
return; return;
} }
+189
View File
@@ -0,0 +1,189 @@
(function () {
"use strict";
function normalizeSequenceState(sequence) {
return {
total: Math.max(0, Number(sequence?.total) || 0),
currentIndex: Number.isFinite(Number(sequence?.currentIndex)) ? Number(sequence.currentIndex) : -1,
previousKey: String(sequence?.previousKey ?? sequence?.previousId ?? "").trim(),
nextKey: String(sequence?.nextKey ?? sequence?.nextId ?? "").trim()
};
}
function isEditableKeyTarget(target) {
if (!(target instanceof HTMLElement)) {
return false;
}
return target instanceof HTMLInputElement
|| target instanceof HTMLTextAreaElement
|| target instanceof HTMLSelectElement
|| target.isContentEditable
|| Boolean(target.closest("[contenteditable='true']"));
}
function hasOpenModalDialog() {
return Boolean(document.querySelector("[role='dialog'][aria-modal='true'][aria-hidden='false']"));
}
function createSequenceNavigator(config = {}) {
const getElements = typeof config.getElements === "function"
? config.getElements
: () => ({});
let buttonsBound = false;
let keyboardBound = false;
function getSequenceState() {
return normalizeSequenceState(
typeof config.getSequenceState === "function"
? config.getSequenceState()
: null
);
}
function getPrevButton(elements) {
return typeof config.getPrevButton === "function" ? config.getPrevButton(elements) : null;
}
function getNextButton(elements) {
return typeof config.getNextButton === "function" ? config.getNextButton(elements) : null;
}
function getPositionEl(elements) {
return typeof config.getPositionEl === "function" ? config.getPositionEl(elements) : null;
}
function isActive(elements) {
return typeof config.isActive === "function" ? config.isActive(elements) !== false : true;
}
function getTargetKey(sequence, offset) {
return offset < 0 ? sequence.previousKey : sequence.nextKey;
}
function formatPositionText(sequence, elements) {
return typeof config.formatPositionText === "function"
? String(config.formatPositionText(sequence, elements) || "")
: "";
}
function selectTarget(targetKey, elements, offset) {
if (!targetKey || typeof config.selectTarget !== "function") {
return false;
}
return config.selectTarget(targetKey, elements, offset) !== false;
}
function afterSelect(targetKey, elements, offset) {
if (typeof config.afterSelect === "function") {
config.afterSelect(targetKey, elements, offset);
}
}
function shouldHandleKeyEvent(event, elements) {
if (!isActive(elements)) {
return false;
}
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return false;
}
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") {
return false;
}
if (hasOpenModalDialog()) {
return false;
}
return !isEditableKeyTarget(event.target);
}
function sync(elements = getElements()) {
const sequence = getSequenceState();
const previousKey = getTargetKey(sequence, -1);
const nextKey = getTargetKey(sequence, 1);
const prevButton = getPrevButton(elements);
const nextButton = getNextButton(elements);
const positionEl = getPositionEl(elements);
if (prevButton) {
prevButton.disabled = !previousKey;
}
if (nextButton) {
nextButton.disabled = !nextKey;
}
if (positionEl) {
positionEl.textContent = formatPositionText(sequence, elements);
}
}
function step(offset, elements = getElements()) {
const sequence = getSequenceState();
const targetKey = getTargetKey(sequence, offset);
if (!targetKey) {
return false;
}
const didSelect = selectTarget(targetKey, elements, offset);
if (didSelect) {
afterSelect(targetKey, elements, offset);
}
return didSelect;
}
function bind(elements = getElements()) {
if (!buttonsBound) {
getPrevButton(elements)?.addEventListener("click", () => {
step(-1, getElements());
});
getNextButton(elements)?.addEventListener("click", () => {
step(1, getElements());
});
buttonsBound = true;
}
if (!keyboardBound) {
document.addEventListener("keydown", (event) => {
const latestElements = getElements();
if (!shouldHandleKeyEvent(event, latestElements)) {
return;
}
const offset = event.key === "ArrowRight" ? 1 : -1;
const sequence = getSequenceState();
if (!getTargetKey(sequence, offset)) {
return;
}
event.preventDefault();
step(offset, latestElements);
});
keyboardBound = true;
}
sync(elements);
}
return {
bind,
step,
sync,
getSequenceState
};
}
window.TarotSequenceNav = {
...(window.TarotSequenceNav || {}),
createSequenceNavigator
};
})();
+31 -2
View File
@@ -209,12 +209,17 @@
const activeDeckId = String(status?.activeDeckId || normalizeTarotDeck(getElements().tarotDeckEl?.value)).trim().toLowerCase(); const activeDeckId = String(status?.activeDeckId || normalizeTarotDeck(getElements().tarotDeckEl?.value)).trim().toLowerCase();
const loadedCount = Math.max(0, Number(status?.selectedDeckLoadedCount) || 0); const loadedCount = Math.max(0, Number(status?.selectedDeckLoadedCount) || 0);
const totalCount = Math.max(0, Number(status?.selectedDeckTotalCount) || 0); const totalCount = Math.max(0, Number(status?.selectedDeckTotalCount) || 0);
const warmedDeckCount = Math.max(0, Number(status?.warmedDeckCount) || 0);
const totalDeckCount = Math.max(0, Number(status?.totalDeckCount) || 0);
const backgroundProgress = totalDeckCount > 1
? ` (${Math.min(warmedDeckCount, totalDeckCount)}/${totalDeckCount} decks warmed)`
: "";
if (status?.selectedDeckPhase === "loading") { if (status?.selectedDeckPhase === "loading") {
if (totalCount > 0) { if (totalCount > 0) {
return `Caching selected deck images to this browser... (${loadedCount}/${totalCount})`; return `Caching selected deck images to this browser... (${loadedCount}/${totalCount})${backgroundProgress}`;
} }
return "Caching selected deck images to this browser..."; return `Caching selected deck images to this browser...${backgroundProgress}`;
} }
if (status?.selectedDeckPhase === "error") { if (status?.selectedDeckPhase === "error") {
@@ -222,9 +227,21 @@
} }
if (status?.selectedDeckPhase === "ready") { if (status?.selectedDeckPhase === "ready") {
if (totalDeckCount > 1 && warmedDeckCount < totalDeckCount) {
return `Selected deck cached and ready for fullscreen use (${activeDeckId}). Warming the rest of the decks in background${backgroundProgress}.`;
}
if (totalDeckCount > 1) {
return `All connected deck images cached and ready (${totalDeckCount}/${totalDeckCount} decks warmed).`;
}
return `Selected deck cached and ready for fullscreen use (${activeDeckId}).`; return `Selected deck cached and ready for fullscreen use (${activeDeckId}).`;
} }
if (totalDeckCount > 1 && warmedDeckCount > 0) {
return `Deck cache idle. Background warmup has ${Math.min(warmedDeckCount, totalDeckCount)}/${totalDeckCount} decks ready.`;
}
return "Deck cache idle."; return "Deck cache idle.";
} }
@@ -570,6 +587,18 @@
const previousConnectionSettings = getConnectionSettings(); const previousConnectionSettings = getConnectionSettings();
const connectionSettings = getConnectionSettingsFromInputs(); const connectionSettings = getConnectionSettingsFromInputs();
const connectionChanged = hasConnectionChanged(previousConnectionSettings, connectionSettings); const connectionChanged = hasConnectionChanged(previousConnectionSettings, connectionSettings);
if (connectionChanged) {
setSettingsPageStatus("Validating API connection...", "info", {
savedAt: loadLastSavedAt()
});
const probeResult = await window.TarotDataService?.probeConnection?.(connectionSettings);
if (!probeResult?.ok) {
throw new Error(probeResult?.message || "Unable to validate the API connection.");
}
}
const connectionResult = window.TarotAppConfig?.updateConnectionSettings?.(connectionSettings) || { didPersist: true }; const connectionResult = window.TarotAppConfig?.updateConnectionSettings?.(connectionSettings) || { didPersist: true };
const normalized = applySettingsToInputs(settings); const normalized = applySettingsToInputs(settings);
syncSky( syncSky(
+71
View File
@@ -5,6 +5,8 @@
getMagickDataset, getMagickDataset,
resolveTarotCardImage, resolveTarotCardImage,
resolveTarotCardThumbnail, resolveTarotCardThumbnail,
getDeckVariantsForCard,
openDeckVariantLightbox,
getDisplayCardName, getDisplayCardName,
buildTypeLabel, buildTypeLabel,
clearChildren, clearChildren,
@@ -407,6 +409,73 @@
.filter((group) => group.items.length); .filter((group) => group.items.length);
} }
function renderDeckVariants(card, elements, cardDisplayName) {
const galleryCardEl = elements?.tarotMetaDeckGalleryCardEl;
const galleryEl = elements?.tarotDetailDeckGalleryEl;
if (!galleryCardEl || !galleryEl) {
return;
}
clearChildren(galleryEl);
const variants = typeof getDeckVariantsForCard === "function"
? getDeckVariantsForCard(card)
: [];
if (!Array.isArray(variants) || variants.length < 1) {
galleryCardEl.hidden = true;
return;
}
variants.forEach((variant) => {
const button = document.createElement("button");
button.type = "button";
button.className = `tarot-deck-variant${variant?.isActive ? " is-active" : ""}`;
button.dataset.deckId = String(variant?.deckId || "").trim().toLowerCase();
const imageEl = document.createElement("img");
imageEl.className = "tarot-deck-variant-image";
imageEl.src = String(variant?.src || "").trim();
imageEl.alt = `${String(variant?.label || "Deck").trim()}${cardDisplayName || card?.name || "Tarot card"}`;
imageEl.loading = "lazy";
imageEl.decoding = "async";
const labelEl = document.createElement("span");
labelEl.className = "tarot-deck-variant-label";
const deckNameEl = document.createElement("span");
deckNameEl.className = "tarot-deck-variant-deck";
deckNameEl.textContent = String(variant?.label || variant?.deckId || "Deck").trim() || "Deck";
labelEl.appendChild(deckNameEl);
const variantName = String(variant?.displayName || "").trim();
if (variantName && variantName !== (cardDisplayName || card?.name || "")) {
const variantNameEl = document.createElement("span");
variantNameEl.className = "tarot-deck-variant-name";
variantNameEl.textContent = variantName;
labelEl.appendChild(variantNameEl);
}
if (variant?.isActive) {
const activeEl = document.createElement("span");
activeEl.className = "tarot-deck-variant-active";
activeEl.textContent = "Current deck";
labelEl.appendChild(activeEl);
}
button.append(imageEl, labelEl);
if (typeof openDeckVariantLightbox === "function" && button.dataset.deckId) {
button.addEventListener("click", () => {
openDeckVariantLightbox(card?.id, button.dataset.deckId);
});
}
galleryEl.appendChild(button);
});
galleryCardEl.hidden = false;
}
function renderDetail(card, elements) { function renderDetail(card, elements) {
if (!card || !elements) { if (!card || !elements) {
return; return;
@@ -454,6 +523,8 @@
elements.tarotDetailReversedEl.textContent = card.meanings?.reversed || "--"; elements.tarotDetailReversedEl.textContent = card.meanings?.reversed || "--";
} }
renderDeckVariants(card, elements, cardDisplayName || card.name);
const meaningText = String(card.meaning || card.meanings?.upright || "").trim(); const meaningText = String(card.meaning || card.meanings?.upright || "").trim();
if (elements.tarotMetaMeaningCardEl && elements.tarotDetailMeaningEl) { if (elements.tarotMetaMeaningCardEl && elements.tarotDetailMeaningEl) {
if (meaningText) { if (meaningText) {
+20 -12
View File
@@ -837,12 +837,14 @@
function normalizeCardRequest(request) { function normalizeCardRequest(request) {
const normalized = normalizeOpenRequest(request); const normalized = normalizeOpenRequest(request);
const label = String(normalized.label || normalized.altText || "Tarot card enlarged image").trim() || "Tarot card enlarged image"; const label = String(normalized.label || normalized.altText || "Tarot card enlarged image").trim() || "Tarot card enlarged image";
const cardId = String(normalized.cardId || "").trim();
return { return {
src: String(normalized.src || "").trim(), src: String(normalized.src || "").trim(),
previewSrc: String(normalized.previewSrc || "").trim(), previewSrc: String(normalized.previewSrc || "").trim(),
altText: String(normalized.altText || label).trim() || label, altText: String(normalized.altText || label).trim() || label,
label, label,
cardId: String(normalized.cardId || "").trim(), cardId,
sequenceId: String(normalized.sequenceId || cardId).trim(),
deckId: String(normalized.deckId || "").trim(), deckId: String(normalized.deckId || "").trim(),
deckLabel: String(normalized.deckLabel || normalized.deckId || "").trim(), deckLabel: String(normalized.deckLabel || normalized.deckId || "").trim(),
missingReason: String(normalized.missingReason || "").trim(), missingReason: String(normalized.missingReason || "").trim(),
@@ -995,19 +997,20 @@
}); });
} }
function resolveCardRequestById(cardId) { function resolveCardRequestById(sequenceId) {
if (!cardId || typeof lightboxState.resolveCardById !== "function") { if (!sequenceId || typeof lightboxState.resolveCardById !== "function") {
return null; return null;
} }
const resolved = lightboxState.resolveCardById(cardId); const resolved = lightboxState.resolveCardById(sequenceId);
if (!resolved) { if (!resolved) {
return null; return null;
} }
return normalizeCardRequest({ return normalizeCardRequest({
...resolved, ...resolved,
cardId sequenceId: String(resolved.sequenceId || sequenceId).trim() || String(sequenceId),
cardId: String(resolved.cardId || sequenceId).trim()
}); });
} }
@@ -3277,7 +3280,10 @@
return; return;
} }
const anchorId = lightboxState.secondaryCard?.cardId || lightboxState.primaryCard?.cardId; const anchorId = lightboxState.secondaryCard?.sequenceId
|| lightboxState.secondaryCard?.cardId
|| lightboxState.primaryCard?.sequenceId
|| lightboxState.primaryCard?.cardId;
const startIndex = sequence.indexOf(anchorId); const startIndex = sequence.indexOf(anchorId);
if (startIndex < 0) { if (startIndex < 0) {
return; return;
@@ -3285,12 +3291,13 @@
for (let offset = 1; offset <= sequence.length; offset += 1) { for (let offset = 1; offset <= sequence.length; offset += 1) {
const nextIndex = (startIndex + direction * offset + sequence.length) % sequence.length; const nextIndex = (startIndex + direction * offset + sequence.length) % sequence.length;
const nextCardId = sequence[nextIndex]; const nextSequenceId = sequence[nextIndex];
if (!nextCardId || nextCardId === lightboxState.primaryCard?.cardId) { const primarySequenceId = lightboxState.primaryCard?.sequenceId || lightboxState.primaryCard?.cardId;
if (!nextSequenceId || nextSequenceId === primarySequenceId) {
continue; continue;
} }
const nextCard = resolveCardRequestById(nextCardId); const nextCard = resolveCardRequestById(nextSequenceId);
if (nextCard && setSecondaryCard(nextCard, true)) { if (nextCard && setSecondaryCard(nextCard, true)) {
break; break;
} }
@@ -3303,14 +3310,15 @@
return; return;
} }
const startIndex = sequence.indexOf(lightboxState.primaryCard?.cardId); const primarySequenceId = lightboxState.primaryCard?.sequenceId || lightboxState.primaryCard?.cardId;
const startIndex = sequence.indexOf(primarySequenceId);
if (startIndex < 0) { if (startIndex < 0) {
return; return;
} }
const nextIndex = (startIndex + direction + sequence.length) % sequence.length; const nextIndex = (startIndex + direction + sequence.length) % sequence.length;
const nextCardId = sequence[nextIndex]; const nextSequenceId = sequence[nextIndex];
const nextCard = resolveCardRequestById(nextCardId); const nextCard = resolveCardRequestById(nextSequenceId);
if (!nextCard?.src) { if (!nextCard?.src) {
return; return;
} }
+227 -12
View File
@@ -47,6 +47,7 @@
courtCardByDecanId: new Map(), courtCardByDecanId: new Map(),
loadingPromise: null loadingPromise: null
}; };
let detailNavigator = null;
const TAROT_TRUMP_NUMBER_BY_NAME = { const TAROT_TRUMP_NUMBER_BY_NAME = {
"the fool": 0, "the fool": 0,
@@ -255,9 +256,14 @@
tarotDetailImageEl: document.getElementById("tarot-detail-image"), tarotDetailImageEl: document.getElementById("tarot-detail-image"),
tarotDetailNameEl: document.getElementById("tarot-detail-name"), tarotDetailNameEl: document.getElementById("tarot-detail-name"),
tarotDetailTypeEl: document.getElementById("tarot-detail-type"), tarotDetailTypeEl: document.getElementById("tarot-detail-type"),
tarotDetailPrevEl: document.getElementById("tarot-detail-prev"),
tarotDetailPositionEl: document.getElementById("tarot-detail-position"),
tarotDetailNextEl: document.getElementById("tarot-detail-next"),
tarotDetailSummaryEl: document.getElementById("tarot-detail-summary"), tarotDetailSummaryEl: document.getElementById("tarot-detail-summary"),
tarotDetailUprightEl: document.getElementById("tarot-detail-upright"), tarotDetailUprightEl: document.getElementById("tarot-detail-upright"),
tarotDetailReversedEl: document.getElementById("tarot-detail-reversed"), tarotDetailReversedEl: document.getElementById("tarot-detail-reversed"),
tarotMetaDeckGalleryCardEl: document.getElementById("tarot-meta-deck-gallery-card"),
tarotDetailDeckGalleryEl: document.getElementById("tarot-detail-deck-gallery"),
tarotMetaMeaningCardEl: document.getElementById("tarot-meta-meaning-card"), tarotMetaMeaningCardEl: document.getElementById("tarot-meta-meaning-card"),
tarotDetailMeaningEl: document.getElementById("tarot-detail-meaning"), tarotDetailMeaningEl: document.getElementById("tarot-detail-meaning"),
tarotDetailKeywordsEl: document.getElementById("tarot-detail-keywords"), tarotDetailKeywordsEl: document.getElementById("tarot-detail-keywords"),
@@ -355,6 +361,8 @@
getMagickDataset: () => state.magickDataset, getMagickDataset: () => state.magickDataset,
resolveTarotCardImage, resolveTarotCardImage,
resolveTarotCardThumbnail, resolveTarotCardThumbnail,
getDeckVariantsForCard,
openDeckVariantLightbox,
getDisplayCardName, getDisplayCardName,
buildTypeLabel, buildTypeLabel,
clearChildren, clearChildren,
@@ -523,11 +531,14 @@
if (!state.filteredCards.some((card) => card.id === state.selectedCardId)) { if (!state.filteredCards.some((card) => card.id === state.selectedCardId)) {
if (state.filteredCards.length > 0) { if (state.filteredCards.length > 0) {
selectCardById(state.filteredCards[0].id, elements); selectCardById(state.filteredCards[0].id, elements);
} else {
syncDetailNavigation(elements);
} }
return; return;
} }
updateListSelection(elements); updateListSelection(elements);
syncDetailNavigation(elements);
} }
function clearChildren(element) { function clearChildren(element) {
@@ -723,6 +734,62 @@
}); });
} }
function getCardSequenceState() {
const total = state.filteredCards.length;
const currentIndex = state.filteredCards.findIndex((card) => card.id === state.selectedCardId);
return {
total,
currentIndex,
previousId: currentIndex > 0 ? state.filteredCards[currentIndex - 1].id : "",
nextId: currentIndex >= 0 && currentIndex < total - 1 ? state.filteredCards[currentIndex + 1].id : ""
};
}
function getDetailNavigator() {
if (detailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") {
return detailNavigator;
}
detailNavigator = window.TarotSequenceNav.createSequenceNavigator({
getElements,
isActive: (elements) => Boolean(elements?.tarotSectionEl && elements.tarotSectionEl.hidden === false),
getSequenceState: getCardSequenceState,
getPrevButton: (elements) => elements?.tarotDetailPrevEl,
getNextButton: (elements) => elements?.tarotDetailNextEl,
getPositionEl: (elements) => elements?.tarotDetailPositionEl,
formatPositionText: ({ total, currentIndex }) => {
if (total > 0 && currentIndex >= 0) {
const suffix = state.searchQuery ? " shown" : "";
return `${currentIndex + 1} of ${total}${suffix}`;
}
return total > 0 ? `${total} cards` : "No cards";
},
selectTarget: (targetId, elements) => {
selectCardById(targetId, elements);
return true;
},
afterSelect: (targetId, elements) => {
scrollCardIntoView(targetId, elements);
}
});
return detailNavigator;
}
function syncDetailNavigation(elements) {
getDetailNavigator()?.sync(elements);
}
function selectAdjacentCard(offset, elements = getElements()) {
return getDetailNavigator()?.step(offset, elements) === true;
}
function bindKeyboardNavigation(elements) {
getDetailNavigator()?.bind(elements);
}
function selectCardById(cardIdToSelect, elements) { function selectCardById(cardIdToSelect, elements) {
const card = state.cards.find((entry) => entry.id === cardIdToSelect); const card = state.cards.find((entry) => entry.id === cardIdToSelect);
if (!card) { if (!card) {
@@ -733,6 +800,7 @@
updateListSelection(elements); updateListSelection(elements);
updateHouseSelection(elements); updateHouseSelection(elements);
renderDetail(card, elements); renderDetail(card, elements);
syncDetailNavigation(elements);
} }
function scrollCardIntoView(cardIdToReveal, elements) { function scrollCardIntoView(cardIdToReveal, elements) {
@@ -758,6 +826,47 @@
return Array.from(getRegisteredDeckOptionMap().values()); return Array.from(getRegisteredDeckOptionMap().values());
} }
function getDeckVariantsForCard(card) {
if (!card) {
return [];
}
const trumpNumber = Number.isFinite(Number(card?.number)) ? Number(card.number) : undefined;
const activeDeckId = String(getActiveDeck?.() || "").trim().toLowerCase();
return getRegisteredDeckList()
.map((deck) => {
const deckId = String(deck?.id || "").trim().toLowerCase();
if (!deckId) {
return null;
}
const imageOptions = { deckId, trumpNumber };
const src = (typeof resolveTarotCardThumbnail === "function"
? resolveTarotCardThumbnail(card.name, imageOptions)
: "") || (typeof resolveTarotCardImage === "function"
? resolveTarotCardImage(card.name, imageOptions)
: "");
if (!src) {
return null;
}
const displayName = (typeof getTarotCardDisplayName === "function"
? String(getTarotCardDisplayName(card.name, imageOptions) || "").trim()
: "") || getDisplayCardName(card) || card.name;
return {
deckId,
label: String(deck?.label || deckId).trim() || deckId,
src,
displayName,
isActive: deckId === activeDeckId
};
})
.filter(Boolean);
}
function buildDeckLightboxCardRequest(cardIdToResolve, deckIdToResolve = "") { function buildDeckLightboxCardRequest(cardIdToResolve, deckIdToResolve = "") {
const card = state.cards.find((entry) => entry.id === cardIdToResolve); const card = state.cards.find((entry) => entry.id === cardIdToResolve);
if (!card) { if (!card) {
@@ -790,8 +899,8 @@
}; };
} }
function buildLightboxCardRequestById(cardIdToResolve) { function buildLightboxCardRequestById(cardIdToResolve, deckIdToResolve = "") {
const request = buildDeckLightboxCardRequest(cardIdToResolve, getActiveDeck?.() || ""); const request = buildDeckLightboxCardRequest(cardIdToResolve, deckIdToResolve || getActiveDeck?.() || "");
if (!request?.src) { if (!request?.src) {
return null; return null;
} }
@@ -799,19 +908,67 @@
return request; return request;
} }
function getDefaultLightboxSequenceIds(cardIdToOpen = "") {
const normalizedCardId = String(cardIdToOpen || "").trim();
const filteredIds = state.filteredCards
.map((card) => String(card?.id || "").trim())
.filter(Boolean);
if (normalizedCardId && filteredIds.includes(normalizedCardId)) {
return filteredIds;
}
return state.cards
.map((card) => String(card?.id || "").trim())
.filter(Boolean);
}
function getDeckVariantSequenceEntries(cardIdToResolve) {
const card = state.cards.find((entry) => entry.id === cardIdToResolve);
if (!card) {
return [];
}
return getDeckVariantsForCard(card)
.map((variant) => {
const deckId = String(variant?.deckId || "").trim().toLowerCase();
if (!deckId) {
return null;
}
return {
sequenceId: deckId,
deckId
};
})
.filter(Boolean);
}
function openCardLightboxById(cardIdToOpen, options = {}) { function openCardLightboxById(cardIdToOpen, options = {}) {
const normalizedCardId = String(cardIdToOpen || "").trim(); const normalizedCardId = String(cardIdToOpen || "").trim();
if (!normalizedCardId) { if (!normalizedCardId) {
return; return;
} }
const primaryCardRequest = buildLightboxCardRequestById(normalizedCardId); const requestedDeckId = String(options?.deckId || getActiveDeck?.() || "").trim();
const primaryCardRequest = buildLightboxCardRequestById(normalizedCardId, requestedDeckId);
if (!primaryCardRequest?.src) { if (!primaryCardRequest?.src) {
return; return;
} }
const activeDeckId = String(getActiveDeck?.() || primaryCardRequest.deckId || "").trim(); const activeDeckId = String(primaryCardRequest.deckId || requestedDeckId || getActiveDeck?.() || "").trim();
const availableCompareDecks = getRegisteredDeckList().filter((deck) => deck.id && deck.id !== activeDeckId); const availableCompareDecks = getRegisteredDeckList().filter((deck) => deck.id && deck.id !== activeDeckId);
const requestedSequenceIds = Array.isArray(options?.sequenceIds)
? options.sequenceIds
.map((sequenceId) => String(sequenceId || "").trim())
.filter(Boolean)
: [];
const sequenceIds = requestedSequenceIds.length > 0
? requestedSequenceIds
: getDefaultLightboxSequenceIds(normalizedCardId);
const resolveCardById = typeof options?.resolveCardById === "function"
? options.resolveCardById
: (nextCardId) => buildLightboxCardRequestById(nextCardId, activeDeckId);
const onSelectCardId = typeof options?.onSelectCardId === "function" const onSelectCardId = typeof options?.onSelectCardId === "function"
? options.onSelectCardId ? options.onSelectCardId
: (nextCardId) => { : (nextCardId) => {
@@ -825,6 +982,7 @@
altText: primaryCardRequest.altText, altText: primaryCardRequest.altText,
label: primaryCardRequest.label, label: primaryCardRequest.label,
cardId: primaryCardRequest.cardId, cardId: primaryCardRequest.cardId,
sequenceId: String(options?.sequenceId || primaryCardRequest.sequenceId || normalizedCardId).trim(),
deckId: primaryCardRequest.deckId || activeDeckId, deckId: primaryCardRequest.deckId || activeDeckId,
deckLabel: primaryCardRequest.deckLabel || "", deckLabel: primaryCardRequest.deckLabel || "",
compareDetails: primaryCardRequest.compareDetails || [], compareDetails: primaryCardRequest.compareDetails || [],
@@ -834,13 +992,72 @@
activeDeckLabel: primaryCardRequest.deckLabel || "", activeDeckLabel: primaryCardRequest.deckLabel || "",
availableCompareDecks, availableCompareDecks,
maxCompareDecks: 2, maxCompareDecks: 2,
sequenceIds: state.cards.map((card) => card.id), sequenceIds,
resolveCardById: buildLightboxCardRequestById, resolveCardById,
resolveDeckCardById: buildDeckLightboxCardRequest, resolveDeckCardById: buildDeckLightboxCardRequest,
onSelectCardId onSelectCardId
}); });
} }
function openDeckVariantLightbox(cardIdToOpen, deckIdToOpen = "") {
const normalizedCardId = String(cardIdToOpen || "").trim();
if (!normalizedCardId) {
return;
}
const variantEntries = getDeckVariantSequenceEntries(normalizedCardId);
if (variantEntries.length < 1) {
openCardLightboxById(normalizedCardId, { deckId: deckIdToOpen });
return;
}
const requestedDeckId = String(deckIdToOpen || getActiveDeck?.() || "").trim().toLowerCase();
const activeVariant = variantEntries.find((variant) => variant.deckId === requestedDeckId) || variantEntries[0];
if (!activeVariant?.deckId) {
openCardLightboxById(normalizedCardId);
return;
}
const primaryCardRequest = buildLightboxCardRequestById(normalizedCardId, activeVariant.deckId);
if (!primaryCardRequest?.src) {
return;
}
const availableCompareDecks = getRegisteredDeckList().filter((deck) => deck.id && deck.id !== activeVariant.deckId);
window.TarotUiLightbox?.open?.({
...primaryCardRequest,
deckId: activeVariant.deckId,
sequenceId: activeVariant.sequenceId,
activeDeckId: activeVariant.deckId,
activeDeckLabel: primaryCardRequest.deckLabel || "",
availableCompareDecks,
maxCompareDecks: 2,
allowOverlayCompare: true,
allowDeckCompare: true,
sequenceIds: variantEntries.map((variant) => variant.sequenceId),
resolveDeckCardById: buildDeckLightboxCardRequest,
resolveCardById: (sequenceId) => {
const normalizedSequenceId = String(sequenceId || "").trim().toLowerCase();
const matchingVariant = variantEntries.find((variant) => variant.sequenceId === normalizedSequenceId);
if (!matchingVariant?.deckId) {
return null;
}
const request = buildLightboxCardRequestById(normalizedCardId, matchingVariant.deckId);
return request
? {
...request,
sequenceId: matchingVariant.sequenceId,
deckId: matchingVariant.deckId
}
: null;
},
onSelectCardId: () => {
}
});
}
function renderList(elements) { function renderList(elements) {
if (!elements?.tarotCardListEl) { if (!elements?.tarotCardListEl) {
return; return;
@@ -940,6 +1157,7 @@
const selected = state.cards.find((card) => card.id === state.selectedCardId); const selected = state.cards.find((card) => card.id === state.selectedCardId);
if (selected) { if (selected) {
renderDetail(selected, elements); renderDetail(selected, elements);
syncDetailNavigation(elements);
} }
} }
return; return;
@@ -1007,6 +1225,8 @@
}); });
} }
bindKeyboardNavigation(elements);
if (elements.tarotHouseTopCardsVisibleEl) { if (elements.tarotHouseTopCardsVisibleEl) {
elements.tarotHouseTopCardsVisibleEl.addEventListener("change", () => { elements.tarotHouseTopCardsVisibleEl.addEventListener("change", () => {
state.houseTopCardsVisible = Boolean(elements.tarotHouseTopCardsVisibleEl.checked); state.houseTopCardsVisible = Boolean(elements.tarotHouseTopCardsVisibleEl.checked);
@@ -1104,12 +1324,7 @@
return; return;
} }
const request = buildLightboxCardRequestById(state.selectedCardId); openCardLightboxById(state.selectedCardId);
if (!request?.src) {
return;
}
window.TarotUiLightbox?.open?.(request);
}); });
} }
+132 -19
View File
@@ -16,7 +16,7 @@
<link rel="stylesheet" href="node_modules/@fontsource/amiri/arabic-400.css"> <link rel="stylesheet" href="node_modules/@fontsource/amiri/arabic-400.css">
<link rel="stylesheet" href="node_modules/@fontsource/amiri/arabic-700.css"> <link rel="stylesheet" href="node_modules/@fontsource/amiri/arabic-700.css">
<link rel="stylesheet" href="node_modules/@fontsource/noto-naskh-arabic/arabic-400.css"> <link rel="stylesheet" href="node_modules/@fontsource/noto-naskh-arabic/arabic-400.css">
<link rel="stylesheet" href="app/styles.css?v=20260424-detail-inline-links-02"> <link rel="stylesheet" href="app/styles.css?v=20260528-kabbalah-cross-split-06">
</head> </head>
<body> <body>
<div class="topbar"> <div class="topbar">
@@ -63,8 +63,12 @@
<div class="topbar-dropdown" aria-label="Kabbalah menu"> <div class="topbar-dropdown" aria-label="Kabbalah menu">
<button id="open-kabbalah" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="kabbalah-subpages" aria-expanded="false">Kabbalah ▾</button> <button id="open-kabbalah" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="kabbalah-subpages" aria-expanded="false">Kabbalah ▾</button>
<div id="kabbalah-subpages" class="topbar-dropdown-menu" role="menu" aria-label="Kabbalah subpages"> <div id="kabbalah-subpages" class="topbar-dropdown-menu" role="menu" aria-label="Kabbalah subpages">
<button id="open-kabbalah-cube" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Cube</button> <button id="open-kabbalah-sephirot" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Sefirot</button>
<button id="open-kabbalah-worlds" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Qabalistic Worlds</button>
<button id="open-kabbalah-paths" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Paths</button>
<button id="open-kabbalah-cross" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Rosicrucian Cross</button>
<button id="open-kabbalah-tree" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Tree</button> <button id="open-kabbalah-tree" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Tree</button>
<button id="open-kabbalah-cube" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Cube</button>
</div> </div>
</div> </div>
<button id="open-numbers" class="settings-trigger" type="button" aria-pressed="false">Numbers</button> <button id="open-numbers" class="settings-trigger" type="button" aria-pressed="false">Numbers</button>
@@ -216,6 +220,11 @@
<div class="planet-detail-heading"> <div class="planet-detail-heading">
<h2 id="calendar-detail-name">--</h2> <h2 id="calendar-detail-name">--</h2>
<div id="calendar-detail-sub" class="planet-detail-type">Select a month to explore</div> <div id="calendar-detail-sub" class="planet-detail-type">Select a month to explore</div>
<div class="detail-sequence-nav" aria-label="Browse months">
<button id="calendar-detail-prev" class="detail-sequence-btn" type="button" aria-label="Previous month">Back</button>
<div id="calendar-detail-position" class="detail-sequence-position" aria-live="polite">--</div>
<button id="calendar-detail-next" class="detail-sequence-btn" type="button" aria-label="Next month">Next</button>
</div>
</div> </div>
<div id="calendar-detail-body"></div> <div id="calendar-detail-body"></div>
</section> </section>
@@ -252,6 +261,11 @@
<div class="planet-detail-heading"> <div class="planet-detail-heading">
<h2 id="holiday-detail-name">--</h2> <h2 id="holiday-detail-name">--</h2>
<div id="holiday-detail-sub" class="planet-detail-type">Select a holiday to explore</div> <div id="holiday-detail-sub" class="planet-detail-type">Select a holiday to explore</div>
<div class="detail-sequence-nav" aria-label="Browse holidays">
<button id="holiday-detail-prev" class="detail-sequence-btn" type="button" aria-label="Previous holiday">Back</button>
<div id="holiday-detail-position" class="detail-sequence-position" aria-live="polite">--</div>
<button id="holiday-detail-next" class="detail-sequence-btn" type="button" aria-label="Next holiday">Next</button>
</div>
</div> </div>
<div id="holiday-detail-body"></div> <div id="holiday-detail-body"></div>
</section> </section>
@@ -273,10 +287,14 @@
</aside> </aside>
<section class="tarot-detail-panel" aria-live="polite"> <section class="tarot-detail-panel" aria-live="polite">
<div class="tarot-detail-top"> <div class="tarot-detail-top">
<img id="tarot-detail-image" class="tarot-detail-image" alt="Tarot card image" />
<div class="tarot-detail-heading"> <div class="tarot-detail-heading">
<h2 id="tarot-detail-name">--</h2> <h2 id="tarot-detail-name">--</h2>
<div id="tarot-detail-type" class="tarot-detail-type">--</div> <div id="tarot-detail-type" class="tarot-detail-type">--</div>
<div class="detail-sequence-nav" aria-label="Browse tarot cards">
<button id="tarot-detail-prev" class="detail-sequence-btn" type="button" aria-label="Previous tarot card">Back</button>
<div id="tarot-detail-position" class="detail-sequence-position" aria-live="polite">--</div>
<button id="tarot-detail-next" class="detail-sequence-btn" type="button" aria-label="Next tarot card">Next</button>
</div>
<div id="tarot-detail-summary" class="tarot-detail-summary">--</div> <div id="tarot-detail-summary" class="tarot-detail-summary">--</div>
</div> </div>
</div> </div>
@@ -291,6 +309,10 @@
</div> </div>
</div> </div>
<div class="tarot-meta-grid"> <div class="tarot-meta-grid">
<div id="tarot-meta-deck-gallery-card" class="tarot-meta-card tarot-deck-gallery-card" hidden>
<strong>Deck Variants</strong>
<div id="tarot-detail-deck-gallery" class="tarot-deck-gallery"></div>
</div>
<div id="tarot-meta-meaning-card" class="tarot-meta-card" hidden> <div id="tarot-meta-meaning-card" class="tarot-meta-card" hidden>
<strong>Traditional Meaning</strong> <strong>Traditional Meaning</strong>
<div id="tarot-detail-meaning" class="planet-text">--</div> <div id="tarot-detail-meaning" class="planet-text">--</div>
@@ -539,6 +561,11 @@
<div class="planet-detail-heading"> <div class="planet-detail-heading">
<h2 id="planet-detail-name">--</h2> <h2 id="planet-detail-name">--</h2>
<div id="planet-detail-type" class="planet-detail-type">--</div> <div id="planet-detail-type" class="planet-detail-type">--</div>
<div class="detail-sequence-nav" aria-label="Browse planets">
<button id="planet-detail-prev" class="detail-sequence-btn" type="button" aria-label="Previous planet">Back</button>
<div id="planet-detail-position" class="detail-sequence-position" aria-live="polite">--</div>
<button id="planet-detail-next" class="detail-sequence-btn" type="button" aria-label="Next planet">Next</button>
</div>
<div id="planet-detail-summary" class="planet-detail-summary">--</div> <div id="planet-detail-summary" class="planet-detail-summary">--</div>
</div> </div>
<div class="planet-meta-grid"> <div class="planet-meta-grid">
@@ -774,19 +801,99 @@
</section> </section>
<section id="kabbalah-section" hidden> <section id="kabbalah-section" hidden>
<div class="planet-layout">
<aside class="planet-list-panel">
<div class="planet-list-header">
<strong>Sefirot</strong>
<span id="kab-browser-count" class="planet-list-count">--</span>
</div>
<div class="kab-browser-intro planet-text">Browse the 11 sefiroth, including Daath, in a dedicated detail browser. Use Paths for the Rosicrucian Cross and Tree for the diagram.</div>
<div id="kab-browser-list" class="planet-card-list" role="listbox" aria-label="Kabbalah sefirot"></div>
</aside>
<section class="planet-detail-panel" aria-live="polite">
<div class="planet-detail-heading">
<h2 id="kab-browser-detail-name">Kabbalah Sefirot</h2>
<div id="kab-browser-detail-sub" class="planet-detail-type">Select a sephira or path to explore</div>
<div class="detail-sequence-nav" aria-label="Browse Kabbalah sefirot">
<button id="kab-browser-detail-prev" class="detail-sequence-btn" type="button" aria-label="Previous Kabbalah sephirah">Back</button>
<div id="kab-browser-detail-position" class="detail-sequence-position" aria-live="polite">--</div>
<button id="kab-browser-detail-next" class="detail-sequence-btn" type="button" aria-label="Next Kabbalah sephirah">Next</button>
</div>
</div>
<div id="kab-browser-detail-body" class="planet-meta-grid"></div>
</section>
</div>
</section>
<section id="kabbalah-worlds-section" hidden>
<div class="planet-layout">
<aside class="planet-list-panel">
<div class="planet-list-header">
<strong>Qabalistic Worlds</strong>
<span id="kab-worlds-count" class="planet-list-count">--</span>
</div>
<div class="kab-browser-intro planet-text">Browse the four Qabalistic Worlds and their corresponding soul layers as a dedicated page.</div>
<div id="kab-worlds-list" class="planet-card-list" role="listbox" aria-label="Qabalistic worlds"></div>
</aside>
<section class="planet-detail-panel" aria-live="polite">
<div class="planet-detail-heading">
<h2 id="kab-worlds-detail-name">Qabalistic Worlds</h2>
<div id="kab-worlds-detail-sub" class="planet-detail-type">Select a world layer to explore</div>
<div class="detail-sequence-nav" aria-label="Browse Qabalistic Worlds">
<button id="kab-worlds-detail-prev" class="detail-sequence-btn" type="button" aria-label="Previous Qabalistic world">Back</button>
<div id="kab-worlds-detail-position" class="detail-sequence-position" aria-live="polite">--</div>
<button id="kab-worlds-detail-next" class="detail-sequence-btn" type="button" aria-label="Next Qabalistic world">Next</button>
</div>
</div>
<div id="kab-worlds-detail-body" class="planet-meta-grid"></div>
</section>
</div>
</section>
<section id="kabbalah-paths-section" hidden>
<div class="planet-layout">
<aside class="planet-list-panel">
<div class="planet-list-header">
<strong>Paths</strong>
<span id="kab-paths-count" class="planet-list-count">--</span>
</div>
<div class="kab-browser-intro planet-text">Browse the 22 Hebrew letter paths as their own entry page. Use Rosicrucian Cross for the petal view.</div>
<div id="kab-paths-list" class="planet-card-list" role="listbox" aria-label="Kabbalah paths"></div>
</aside>
<section class="planet-detail-panel" aria-live="polite">
<div class="planet-detail-heading">
<h2 id="kab-paths-detail-name">Kabbalah Paths</h2>
<div id="kab-paths-detail-sub" class="planet-detail-type">Select a path to explore</div>
<div class="detail-sequence-nav" aria-label="Browse Kabbalah paths">
<button id="kab-paths-detail-prev" class="detail-sequence-btn" type="button" aria-label="Previous Kabbalah path">Back</button>
<div id="kab-paths-detail-position" class="detail-sequence-position" aria-live="polite">--</div>
<button id="kab-paths-detail-next" class="detail-sequence-btn" type="button" aria-label="Next Kabbalah path">Next</button>
</div>
</div>
<div id="kab-paths-detail-body" class="planet-meta-grid"></div>
</section>
</div>
</section>
<section id="kabbalah-cross-section" hidden>
<div class="kab-rose-layout"> <div class="kab-rose-layout">
<aside class="kab-rose-panel"> <aside class="kab-rose-panel">
<div class="planet-list-header"> <div class="planet-list-header">
<strong>Rosicrucian Cross</strong> <strong>Rosicrucian Cross</strong>
<span class="planet-list-count">22 Hebrew Letter Paths</span> <span class="planet-list-count">22 Hebrew Letter Petals</span>
</div> </div>
<div class="kab-rose-intro planet-text">Click a Hebrew letter petal to open path correspondences.</div> <div class="kab-rose-intro planet-text">Browse the Rosicrucian Cross as its own page. Click a Hebrew letter petal to explore the linked path from the cross view.</div>
<div id="kab-rose-cross-container" class="kab-rose-cross-container"></div> <div id="kab-rose-cross-container" class="kab-rose-cross-container"></div>
</aside> </aside>
<section class="kab-detail-panel" aria-live="polite"> <section class="kab-detail-panel" aria-live="polite">
<div class="planet-detail-heading"> <div class="planet-detail-heading">
<h2 id="kab-rose-detail-name">Rosicrucian Cross</h2> <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 id="kab-rose-detail-sub" class="planet-detail-type">Select a Hebrew letter petal to explore the path</div>
<div class="detail-sequence-nav" aria-label="Browse Rosicrucian Cross paths">
<button id="kab-rose-detail-prev" class="detail-sequence-btn" type="button" aria-label="Previous Rosicrucian Cross path">Back</button>
<div id="kab-rose-detail-position" class="detail-sequence-position" aria-live="polite">--</div>
<button id="kab-rose-detail-next" class="detail-sequence-btn" type="button" aria-label="Next Rosicrucian Cross path">Next</button>
</div>
</div> </div>
<div id="kab-rose-detail-body" class="planet-meta-grid"></div> <div id="kab-rose-detail-body" class="planet-meta-grid"></div>
</section> </section>
@@ -821,6 +928,11 @@
<div class="planet-detail-heading"> <div class="planet-detail-heading">
<h2 id="kab-detail-name">Kabbalah Tree of Life</h2> <h2 id="kab-detail-name">Kabbalah Tree of Life</h2>
<div id="kab-detail-sub" class="planet-detail-type">Select a sephira or path to explore</div> <div id="kab-detail-sub" class="planet-detail-type">Select a sephira or path to explore</div>
<div class="detail-sequence-nav" aria-label="Browse Kabbalah nodes">
<button id="kab-detail-prev" class="detail-sequence-btn" type="button" aria-label="Previous Kabbalah node">Back</button>
<div id="kab-detail-position" class="detail-sequence-position" aria-live="polite">--</div>
<button id="kab-detail-next" class="detail-sequence-btn" type="button" aria-label="Next Kabbalah node">Next</button>
</div>
</div> </div>
<div id="kab-detail-body" class="planet-meta-grid"></div> <div id="kab-detail-body" class="planet-meta-grid"></div>
</section> </section>
@@ -1182,10 +1294,10 @@
<script src="node_modules/astronomy-engine/astronomy.browser.min.js"></script> <script src="node_modules/astronomy-engine/astronomy.browser.min.js"></script>
<script src="app/astro-calcs.js"></script> <script src="app/astro-calcs.js"></script>
<script src="app/app-config.js?v=20260309-gate"></script> <script src="app/app-config.js?v=20260309-gate"></script>
<script src="app/data-service.js?v=20260319-word-dictionary-01"></script> <script src="app/data-service.js?v=20260527-api-connection-01"></script>
<script src="app/calendar-events.js"></script> <script src="app/calendar-events.js"></script>
<script src="app/card-images.js?v=20260309-gate"></script> <script src="app/card-images.js?v=20260527-tarot-deck-gallery-01"></script>
<script src="app/ui-tarot-lightbox.js?v=20260404-lightbox-pinch-01"></script> <script src="app/ui-tarot-lightbox.js?v=20260528-tarot-variant-sequence-02"></script>
<script src="app/ui-tarot-house.js?v=20260401-house-top-date-01"></script> <script src="app/ui-tarot-house.js?v=20260401-house-top-date-01"></script>
<script src="app/ui-tarot-relations.js"></script> <script src="app/ui-tarot-relations.js"></script>
<script src="app/ui-now-helpers.js?v=20260314-now-planets-grid-01"></script> <script src="app/ui-now-helpers.js?v=20260314-now-planets-grid-01"></script>
@@ -1198,16 +1310,17 @@
<script src="app/ui-calendar-detail-panels.js?v=20260424-association-web-02"></script> <script src="app/ui-calendar-detail-panels.js?v=20260424-association-web-02"></script>
<script src="app/ui-calendar-detail.js?v=20260424-detail-inline-links-02"></script> <script src="app/ui-calendar-detail.js?v=20260424-detail-inline-links-02"></script>
<script src="app/ui-calendar-data.js?v=20260424-decan-ranges-01"></script> <script src="app/ui-calendar-data.js?v=20260424-decan-ranges-01"></script>
<script src="app/ui-calendar.js"></script> <script src="app/ui-sequence-nav.js?v=20260528-sequence-nav-01"></script>
<script src="app/ui-calendar.js?v=20260528-sequence-nav-01"></script>
<script src="app/ui-holidays-data.js"></script> <script src="app/ui-holidays-data.js"></script>
<script src="app/ui-holidays-render.js?v=20260424-association-web-01"></script> <script src="app/ui-holidays-render.js?v=20260424-association-web-01"></script>
<script src="app/ui-holidays.js"></script> <script src="app/ui-holidays.js?v=20260528-sequence-nav-01"></script>
<script src="app/ui-tarot-card-derivations.js?v=20260307b"></script> <script src="app/ui-tarot-card-derivations.js?v=20260307b"></script>
<script src="app/ui-tarot-detail.js?v=20260424-association-web-04"></script> <script src="app/ui-tarot-detail.js?v=20260527-tarot-deck-gallery-02"></script>
<script src="app/ui-tarot-relation-display.js?v=20260307b"></script> <script src="app/ui-tarot-relation-display.js?v=20260307b"></script>
<script src="app/ui-tarot.js?v=20260402-frame-lightbox-01"></script> <script src="app/ui-tarot.js?v=20260528-tarot-variant-sequence-02"></script>
<script src="app/ui-planets-references.js"></script> <script src="app/ui-planets-references.js"></script>
<script src="app/ui-planets.js?v=20260424-detail-inline-links-01"></script> <script src="app/ui-planets.js?v=20260528-sequence-nav-01"></script>
<script src="app/ui-cycles.js?v=20260424-detail-inline-links-02"></script> <script src="app/ui-cycles.js?v=20260424-detail-inline-links-02"></script>
<script src="app/ui-elements.js?v=20260424-association-web-01"></script> <script src="app/ui-elements.js?v=20260424-association-web-01"></script>
<script src="app/ui-audio-notes.js?v=20260314-audio-notes-02"></script> <script src="app/ui-audio-notes.js?v=20260314-audio-notes-02"></script>
@@ -1215,9 +1328,9 @@
<script src="app/ui-iching-references.js"></script> <script src="app/ui-iching-references.js"></script>
<script src="app/ui-iching.js?v=20260424-association-web-01"></script> <script src="app/ui-iching.js?v=20260424-association-web-01"></script>
<script src="app/ui-rosicrucian-cross.js"></script> <script src="app/ui-rosicrucian-cross.js"></script>
<script src="app/ui-kabbalah-detail.js?v=20260424-detail-inline-links-01"></script> <script src="app/ui-kabbalah-detail.js?v=20260528-kabbalah-worlds-03"></script>
<script src="app/ui-kabbalah-views.js"></script> <script src="app/ui-kabbalah-views.js"></script>
<script src="app/ui-kabbalah.js?v=20260312-tree-export-01"></script> <script src="app/ui-kabbalah.js?v=20260528-kabbalah-cross-split-06"></script>
<script src="app/ui-cube-detail.js?v=20260424-association-web-02"></script> <script src="app/ui-cube-detail.js?v=20260424-association-web-02"></script>
<script src="app/ui-cube-chassis.js?v=20260424-cube-fixes-01"></script> <script src="app/ui-cube-chassis.js?v=20260424-cube-fixes-01"></script>
<script src="app/ui-cube-math.js"></script> <script src="app/ui-cube-math.js"></script>
@@ -1246,15 +1359,15 @@
<script src="app/ui-numbers.js"></script> <script src="app/ui-numbers.js"></script>
<script src="app/ui-tarot-spread.js"></script> <script src="app/ui-tarot-spread.js"></script>
<script src="app/ui-tarot-frame.js?v=20260424-frame-export-01"></script> <script src="app/ui-tarot-frame.js?v=20260424-frame-export-01"></script>
<script src="app/ui-settings.js?v=20260415-stellarium-toggle-01"></script> <script src="app/ui-settings.js?v=20260527-tarot-deck-gallery-01"></script>
<script src="app/ui-chrome.js?v=20260328-topbar-settings-01"></script> <script src="app/ui-chrome.js?v=20260328-topbar-settings-01"></script>
<script src="app/ui-navigation.js?v=20260401-tarot-frame-01"></script> <script src="app/ui-navigation.js?v=20260528-kabbalah-cross-split-06"></script>
<script src="app/ui-calendar-formatting.js?v=20260307b"></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-calendar-visuals.js?v=20260307b"></script>
<script src="app/ui-home-calendar.js?v=20260415-stellarium-toggle-01"></script> <script src="app/ui-home-calendar.js?v=20260415-stellarium-toggle-01"></script>
<script src="app/ui-section-state.js?v=20260401-tarot-frame-01"></script> <script src="app/ui-section-state.js?v=20260528-kabbalah-cross-split-06"></script>
<script src="app/app-runtime.js?v=20260309-gate"></script> <script src="app/app-runtime.js?v=20260309-gate"></script>
<script src="app.js?v=20260415-stellarium-toggle-01"></script> <script src="app.js?v=20260528-kabbalah-cross-split-06"></script>
<script src="app/navigation-detail-test-harness.js?v=20260401-universal-detail-02"></script> <script src="app/navigation-detail-test-harness.js?v=20260401-universal-detail-02"></script>
</body> </body>
</html> </html>