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 ichingSectionEl = document.getElementById("iching-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 cubeSectionEl = document.getElementById("cube-section");
const alphabetSectionEl = document.getElementById("alphabet-section");
@@ -78,6 +81,10 @@ const openCyclesEl = document.getElementById("open-cycles");
const openElementsEl = document.getElementById("open-elements");
const openIChingEl = document.getElementById("open-iching");
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 openKabbalahCubeEl = document.getElementById("open-kabbalah-cube");
const openAlphabetEl = document.getElementById("open-alphabet");
@@ -327,15 +334,22 @@ function getConnectionSettings() {
};
}
function syncConnectionGateInputs() {
const connectionSettings = getConnectionSettings();
function normalizeConnectionSettingsInput(connectionSettings = null) {
return {
apiBaseUrl: String(connectionSettings?.apiBaseUrl || "").trim().replace(/\/+$/, ""),
apiKey: String(connectionSettings?.apiKey || "").trim()
};
}
function syncConnectionGateInputs(connectionSettings = getConnectionSettings()) {
const normalizedConnectionSettings = normalizeConnectionSettingsInput(connectionSettings);
if (connectionGateBaseUrlEl) {
connectionGateBaseUrlEl.value = String(connectionSettings.apiBaseUrl || "");
connectionGateBaseUrlEl.value = normalizedConnectionSettings.apiBaseUrl;
}
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") {
syncConnectionGateInputs();
function showConnectionGate(message, tone = "default", connectionSettings = null) {
syncConnectionGateInputs(connectionSettings || getConnectionSettings());
if (connectionGateEl) {
connectionGateEl.hidden = false;
}
@@ -369,33 +383,49 @@ function hideConnectionGate() {
}
function getConnectionSettingsFromGate() {
return {
return normalizeConnectionSettingsInput({
apiBaseUrl: String(connectionGateBaseUrlEl?.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) {
if (nextConnectionSettings) {
window.TarotAppConfig?.updateConnectionSettings?.(nextConnectionSettings);
const configuredConnection = nextConnectionSettings
? normalizeConnectionSettingsInput(nextConnectionSettings)
: getConnectionSettings();
if (!nextConnectionSettings) {
syncConnectionGateInputs(configuredConnection);
}
syncConnectionGateInputs();
const configuredConnection = getConnectionSettings();
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;
}
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) {
showConnectionGate(probeResult?.message || "Unable to reach the API.", "error");
showConnectionGate(probeResult?.message || "Unable to reach the API.", "error", configuredConnection);
return false;
}
if (nextConnectionSettings) {
window.TarotAppConfig?.updateConnectionSettings?.(configuredConnection);
syncConnectionGateInputs(configuredConnection);
}
hideConnectionGate();
if (!hasRenderedConnectedShell) {
sectionStateUi.setActiveSection?.("home");
@@ -405,6 +435,7 @@ async function ensureConnectedApp(nextConnectionSettings = null) {
setConnectionGateStatus("Connected.", "success");
setStatus(`Connected to ${configuredConnection.apiBaseUrl}.`);
await appRuntime.renderWeek?.();
warmAllDeckImagesInBackground();
return true;
}
@@ -491,6 +522,9 @@ sectionStateUi.init?.({
elementsSectionEl,
ichingSectionEl,
kabbalahSectionEl,
kabbalahWorldsSectionEl,
kabbalahPathsSectionEl,
kabbalahCrossSectionEl,
kabbalahTreeSectionEl,
cubeSectionEl,
alphabetSectionEl,
@@ -519,6 +553,10 @@ sectionStateUi.init?.({
openElementsEl,
openIChingEl,
openKabbalahEl,
openKabbalahSephirotEl,
openKabbalahWorldsEl,
openKabbalahPathsEl,
openKabbalahCrossEl,
openKabbalahTreeEl,
openKabbalahCubeEl,
openAlphabetEl,
@@ -626,6 +664,10 @@ navigationUi.init?.({
openElementsEl,
openIChingEl,
openKabbalahEl,
openKabbalahSephirotEl,
openKabbalahWorldsEl,
openKabbalahPathsEl,
openKabbalahCrossEl,
openKabbalahTreeEl,
openKabbalahCubeEl,
openAlphabetEl,
+34 -1
View File
@@ -137,7 +137,7 @@
const standardMinorSuits = ["Wands", "Cups", "Swords", "Disks"];
const standardDeckCardNames = buildStandardDeckCardNames();
let deckManifestSources = buildDeckManifestSources();
let deckManifestSources = null;
const manifestCache = new Map();
const cardBackCache = new Map();
@@ -161,6 +161,25 @@
.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) {
const normalizedBasePath = String(basePath || "").trim();
if (!normalizedBasePath) {
@@ -530,6 +549,9 @@
try {
const request = new XMLHttpRequest();
request.open("GET", encodeURI(path), false);
Object.entries(buildManifestRequestHeaders(path)).forEach(([headerName, headerValue]) => {
request.setRequestHeader(headerName, headerValue);
});
request.send(null);
const okStatus = (request.status >= 200 && request.status < 300) || request.status === 0;
@@ -1164,6 +1186,9 @@
})
.then((result) => {
markDeckAsWarmed(normalizedDeckId);
if (options.background) {
emitDeckPreloadStatus();
}
if (!options.background) {
setDeckPreloadStatus({
activeDeckId: normalizedDeckId,
@@ -1211,6 +1236,13 @@
}));
}
function scheduleAllDeckImagePreload(options = {}) {
return deferPreload(() => preloadAllDeckImages({
...defaultDeckWarmupOptions,
...options
}));
}
function resolveDisplayNameWithDeck(deckId, cardName, trumpNumber) {
const manifest = getDeckManifest(deckId);
const fallbackName = String(cardName || "").trim();
@@ -1333,6 +1365,7 @@
resolveTarotCardBackThumbnail,
preloadDeckImages,
preloadAllDeckImages,
scheduleAllDeckImagePreload,
ensureImageLoaded,
isImageLoaded,
getDeckPreloadStatus: () => emitDeckPreloadStatus(),
+24 -9
View File
@@ -102,8 +102,22 @@
pluto: "Pluto"
};
function buildRequestHeaders() {
const apiKey = getApiKey();
function resolveConnectionSettings(connectionSettings = null) {
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
? {
"x-api-key": apiKey
@@ -172,8 +186,8 @@
.join("/");
}
function buildApiUrl(path, query = {}) {
const apiBaseUrl = getApiBaseUrl();
function buildApiUrl(path, query = {}, connectionSettings = null) {
const { apiBaseUrl } = resolveConnectionSettings(connectionSettings);
if (!apiBaseUrl) {
return "";
}
@@ -569,8 +583,9 @@
}));
}
async function probeConnection() {
const apiBaseUrl = getApiBaseUrl();
async function probeConnection(connectionSettings = null) {
const resolvedConnection = resolveConnectionSettings(connectionSettings);
const apiBaseUrl = resolvedConnection.apiBaseUrl;
if (!apiBaseUrl) {
return {
ok: false,
@@ -580,11 +595,11 @@
}
const requestOptions = {
headers: buildRequestHeaders()
headers: buildRequestHeaders(resolvedConnection)
};
try {
const healthResponse = await fetch(buildApiUrl("/api/v1/health"), requestOptions);
const healthResponse = await fetch(buildApiUrl("/api/v1/health", {}, resolvedConnection), requestOptions);
if (!healthResponse.ok) {
return {
ok: false,
@@ -594,7 +609,7 @@
}
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) {
return {
+161 -1
View File
@@ -792,7 +792,7 @@
}
.tarot-detail-top {
display: grid;
grid-template-columns: 150px minmax(0, 1fr);
grid-template-columns: minmax(0, 1fr);
gap: 16px;
align-items: start;
}
@@ -866,6 +866,93 @@
text-transform: uppercase;
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 {
display: flex;
flex-wrap: wrap;
@@ -3190,6 +3277,43 @@
line-height: 1.45;
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 {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
@@ -3520,6 +3644,34 @@
}
#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 {
height: calc(100vh - 61px);
background: #18181b;
@@ -5692,6 +5844,14 @@
gap: 10px;
}
.detail-sequence-nav {
width: 100%;
}
.detail-sequence-position {
min-width: 0;
}
.alpha-text-controls--heading {
grid-template-columns: 1fr;
}
+71
View File
@@ -63,6 +63,7 @@
hebrewById: new Map(),
dayLinksCache: new Map()
};
let detailNavigator = null;
const TAROT_TRUMP_NUMBER_BY_NAME = {
"the fool": 0,
@@ -200,6 +201,7 @@
function getElements() {
return {
sectionEl: document.getElementById("calendar-section"),
monthListEl: document.getElementById("calendar-month-list"),
monthCountEl: document.getElementById("calendar-month-count"),
listTitleEl: document.getElementById("calendar-list-title"),
@@ -210,6 +212,9 @@
searchClearEl: document.getElementById("calendar-search-clear"),
detailNameEl: document.getElementById("calendar-detail-name"),
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")
};
}
@@ -523,6 +528,66 @@
function 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) {
@@ -613,6 +678,10 @@
}
}
function bindDetailNavigation(elements) {
getDetailNavigator()?.bind(elements);
}
function loadCalendarType(calendarId, elements) {
const months = state.calendarData[calendarId];
if (!Array.isArray(months)) {
@@ -745,6 +814,8 @@
bindYearInput(elements);
bindSearchInput(elements);
bindCalendarTypeSelect(elements);
bindDetailNavigation(elements);
bindKeyboardNavigation(elements);
}
applySearchFilter(elements);
+68
View File
@@ -42,6 +42,7 @@
hebrewById: new Map(),
calendarData: {}
};
let detailNavigator = null;
const TAROT_TRUMP_NUMBER_BY_NAME = {
"the fool": 0,
@@ -93,6 +94,7 @@
function getElements() {
return {
sectionEl: document.getElementById("holiday-section"),
sourceSelectEl: document.getElementById("holiday-source-select"),
yearInputEl: document.getElementById("holiday-year-input"),
searchInputEl: document.getElementById("holiday-search-input"),
@@ -101,6 +103,9 @@
listEl: document.getElementById("holiday-list"),
detailNameEl: document.getElementById("holiday-detail-name"),
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")
};
}
@@ -225,6 +230,7 @@
detailNameEl.textContent = "--";
detailSubEl.textContent = "Select a holiday to explore";
detailBodyEl.innerHTML = "";
syncDetailNavigation(elements);
return;
}
@@ -232,6 +238,66 @@
detailSubEl.textContent = `${holidayDataUi.calendarLabel(holiday?.calendarId)} - ${holidayDataUi.monthLabelForCalendar(state.calendarData, holiday?.calendarId, holiday?.monthId)}`;
detailBodyEl.innerHTML = renderHolidayDetail(holiday);
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) {
@@ -307,6 +373,8 @@
elements.searchInputEl.focus();
});
}
bindKeyboardNavigation(elements);
}
function attachNavHandlers(detailBodyEl) {
+61 -3
View File
@@ -309,6 +309,62 @@
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) {
return String(value || "")
.split(/,|;|·|\/|\bor\b|\band\b|\+/i)
@@ -419,12 +475,14 @@
function renderSephiraDetail(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 =
[seph.nameHebrew, seph.translation, seph.planet].filter(Boolean).join(" · ");
elements.detailBodyEl.innerHTML = "";
elements.detailBodyEl.appendChild(buildFourWorldsCard(tree, "", context));
elements.detailBodyEl.appendChild(buildPlanetLuminaryCard(seph.planet, context));
elements.detailBodyEl.appendChild(metaCard("Intelligence", seph.intelligence));
elements.detailBodyEl.appendChild(buildTarotAttributionCard(seph.tarot));
@@ -484,7 +542,6 @@
elements.detailSubEl.textContent = [path.tarot?.card, astro].filter(Boolean).join(" · ");
elements.detailBodyEl.innerHTML = "";
elements.detailBodyEl.appendChild(buildFourWorldsCard(tree, context.activeHebrewToken, context));
elements.detailBodyEl.appendChild(buildConnectsCard(path, fromName, toName));
elements.detailBodyEl.appendChild(buildHebrewLetterCard(letter, context));
elements.detailBodyEl.appendChild(buildAstrologyCard(path.astrology, context));
@@ -543,6 +600,7 @@
}
window.KabbalahDetailUi = {
renderWorldLayerDetail,
renderSephiraDetail,
renderPathDetail,
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;
}
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 = {
tarot: "#tarot-browse-view .tarot-layout",
cube: "#cube-layout",
@@ -31,6 +45,10 @@
iching: "#iching-section .planet-layout",
gods: "#gods-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",
planets: "#planet-section .planet-layout",
elements: "#elements-section .planet-layout"
@@ -167,12 +185,28 @@
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, () => {
setActiveSection(getActiveSection() === "kabbalah-tree" ? "home" : "kabbalah-tree");
setActiveSection("kabbalah-tree");
});
bindClick(elements.openKabbalahCubeEl, () => {
setActiveSection(getActiveSection() === "cube" ? "home" : "cube");
setActiveSection("cube");
});
bindClick(elements.openAlphabetWordEl, () => {
@@ -403,17 +437,21 @@
document.addEventListener("nav:kabbalah-path", (event) => {
const magickDataset = getMagickDataset();
const pathNo = event?.detail?.pathNo;
const pathNo = getKabbalahPathNo(event?.detail);
if (typeof ensure.ensureKabbalahSection === "function" && magickDataset) {
ensure.ensureKabbalahSection(magickDataset);
}
setActiveSection("kabbalah-tree");
if (pathNo != null) {
const targetSection = Number(pathNo) >= 11 ? "kabbalah-paths" : "kabbalah";
setActiveSection(targetSection);
requestAnimationFrame(() => {
window.KabbalahSectionUi?.selectNode?.(pathNo);
scheduleSectionDetailOnly("kabbalah-tree");
scheduleSectionDetailOnly(targetSection);
});
return;
}
setActiveSection("kabbalah-paths");
});
document.addEventListener("nav:planet", (event) => {
@@ -473,7 +511,7 @@
});
document.addEventListener("tarot:view-kab-path", (event) => {
setActiveSection("kabbalah-tree");
setActiveSection("kabbalah-paths");
const pathNumber = event?.detail?.pathNumber;
if (pathNumber != null) {
requestAnimationFrame(() => {
@@ -483,7 +521,7 @@
} else {
kabbalahUi?.selectPathByNumber?.(pathNumber);
}
scheduleSectionDetailOnly("kabbalah-tree");
scheduleSectionDetailOnly("kabbalah-paths");
});
}
});
+74 -3
View File
@@ -21,6 +21,7 @@
monthRefsByPlanetId: new Map(),
cubePlacementsByPlanetId: new Map()
};
let detailNavigator = null;
function normalizePlanetToken(value) {
return String(value || "")
@@ -80,12 +81,16 @@
function getElements() {
return {
planetSectionEl: document.getElementById("planet-section"),
planetCardListEl: document.getElementById("planet-card-list"),
planetSearchInputEl: document.getElementById("planet-search-input"),
planetSearchClearEl: document.getElementById("planet-search-clear"),
planetCountEl: document.getElementById("planet-card-count"),
planetDetailNameEl: document.getElementById("planet-detail-name"),
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"),
planetDetailFactsEl: document.getElementById("planet-detail-facts"),
planetDetailAtmosphereEl: document.getElementById("planet-detail-atmosphere"),
@@ -156,11 +161,14 @@
if (!state.filteredEntries.some((entry) => entry.id === state.selectedId)) {
if (state.filteredEntries.length > 0) {
selectById(state.filteredEntries[0].id, elements);
} else {
syncDetailNavigation(elements);
}
return;
}
updateSelection(elements);
syncDetailNavigation(elements);
}
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) {
const entry = state.entries.find((planet) => planet.id === id);
if (!entry) {
@@ -443,6 +513,7 @@
state.selectedId = entry.id;
updateSelection(elements);
renderDetail(entry, elements);
syncDetailNavigation(elements);
}
function renderList(elements) {
@@ -580,6 +651,8 @@
});
}
bindKeyboardNavigation(elements);
state.initialized = true;
}
@@ -594,9 +667,7 @@
);
if (!entry) return;
selectById(entry.id, el);
el.planetCardListEl
?.querySelector(`[data-planet-id="${entry.id}"]`)
?.scrollIntoView({ block: "nearest" });
scrollEntryIntoView(entry.id, el);
}
window.PlanetSectionUi = {
+15 -2
View File
@@ -19,6 +19,9 @@
"elements",
"iching",
"kabbalah",
"kabbalah-worlds",
"kabbalah-paths",
"kabbalah-cross",
"kabbalah-tree",
"cube",
"alphabet",
@@ -109,9 +112,12 @@
const isElementsOpen = activeSection === "elements";
const isIChingOpen = activeSection === "iching";
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 isCubeOpen = activeSection === "cube";
const isKabbalahMenuOpen = isKabbalahOpen || isKabbalahTreeOpen || isCubeOpen;
const isKabbalahMenuOpen = isKabbalahOpen || isKabbalahWorldsOpen || isKabbalahPathsOpen || isKabbalahCrossOpen || isKabbalahTreeOpen || isCubeOpen;
const isAlphabetOpen = activeSection === "alphabet";
const isAlphabetLettersOpen = activeSection === "alphabet-letters";
const isAlphabetTextOpen = activeSection === "alphabet-text";
@@ -137,6 +143,9 @@
setHidden(elements.elementsSectionEl, !isElementsOpen);
setHidden(elements.ichingSectionEl, !isIChingOpen);
setHidden(elements.kabbalahSectionEl, !isKabbalahOpen);
setHidden(elements.kabbalahWorldsSectionEl, !isKabbalahWorldsOpen);
setHidden(elements.kabbalahPathsSectionEl, !isKabbalahPathsOpen);
setHidden(elements.kabbalahCrossSectionEl, !isKabbalahCrossOpen);
setHidden(elements.kabbalahTreeSectionEl, !isKabbalahTreeOpen);
setHidden(elements.cubeSectionEl, !isCubeOpen);
setHidden(elements.alphabetSectionEl, !isAlphabetOpen);
@@ -168,6 +177,10 @@
setPressed(elements.openElementsEl, isElementsOpen);
setPressed(elements.openIChingEl, isIChingOpen);
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.openKabbalahCubeEl, isCubeOpen);
setPressed(elements.openAlphabetEl, isAlphabetMenuOpen);
@@ -249,7 +262,7 @@
return;
}
if (isKabbalahOpen || isKabbalahTreeOpen) {
if (isKabbalahOpen || isKabbalahWorldsOpen || isKabbalahPathsOpen || isKabbalahCrossOpen || isKabbalahTreeOpen) {
ensure.ensureKabbalahSection?.(magickDataset);
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 loadedCount = Math.max(0, Number(status?.selectedDeckLoadedCount) || 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 (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") {
@@ -222,9 +227,21 @@
}
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}).`;
}
if (totalDeckCount > 1 && warmedDeckCount > 0) {
return `Deck cache idle. Background warmup has ${Math.min(warmedDeckCount, totalDeckCount)}/${totalDeckCount} decks ready.`;
}
return "Deck cache idle.";
}
@@ -570,6 +587,18 @@
const previousConnectionSettings = getConnectionSettings();
const connectionSettings = getConnectionSettingsFromInputs();
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 normalized = applySettingsToInputs(settings);
syncSky(
+71
View File
@@ -5,6 +5,8 @@
getMagickDataset,
resolveTarotCardImage,
resolveTarotCardThumbnail,
getDeckVariantsForCard,
openDeckVariantLightbox,
getDisplayCardName,
buildTypeLabel,
clearChildren,
@@ -407,6 +409,73 @@
.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) {
if (!card || !elements) {
return;
@@ -454,6 +523,8 @@
elements.tarotDetailReversedEl.textContent = card.meanings?.reversed || "--";
}
renderDeckVariants(card, elements, cardDisplayName || card.name);
const meaningText = String(card.meaning || card.meanings?.upright || "").trim();
if (elements.tarotMetaMeaningCardEl && elements.tarotDetailMeaningEl) {
if (meaningText) {
+20 -12
View File
@@ -837,12 +837,14 @@
function normalizeCardRequest(request) {
const normalized = normalizeOpenRequest(request);
const label = String(normalized.label || normalized.altText || "Tarot card enlarged image").trim() || "Tarot card enlarged image";
const cardId = String(normalized.cardId || "").trim();
return {
src: String(normalized.src || "").trim(),
previewSrc: String(normalized.previewSrc || "").trim(),
altText: String(normalized.altText || label).trim() || label,
label,
cardId: String(normalized.cardId || "").trim(),
cardId,
sequenceId: String(normalized.sequenceId || cardId).trim(),
deckId: String(normalized.deckId || "").trim(),
deckLabel: String(normalized.deckLabel || normalized.deckId || "").trim(),
missingReason: String(normalized.missingReason || "").trim(),
@@ -995,19 +997,20 @@
});
}
function resolveCardRequestById(cardId) {
if (!cardId || typeof lightboxState.resolveCardById !== "function") {
function resolveCardRequestById(sequenceId) {
if (!sequenceId || typeof lightboxState.resolveCardById !== "function") {
return null;
}
const resolved = lightboxState.resolveCardById(cardId);
const resolved = lightboxState.resolveCardById(sequenceId);
if (!resolved) {
return null;
}
return normalizeCardRequest({
...resolved,
cardId
sequenceId: String(resolved.sequenceId || sequenceId).trim() || String(sequenceId),
cardId: String(resolved.cardId || sequenceId).trim()
});
}
@@ -3277,7 +3280,10 @@
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);
if (startIndex < 0) {
return;
@@ -3285,12 +3291,13 @@
for (let offset = 1; offset <= sequence.length; offset += 1) {
const nextIndex = (startIndex + direction * offset + sequence.length) % sequence.length;
const nextCardId = sequence[nextIndex];
if (!nextCardId || nextCardId === lightboxState.primaryCard?.cardId) {
const nextSequenceId = sequence[nextIndex];
const primarySequenceId = lightboxState.primaryCard?.sequenceId || lightboxState.primaryCard?.cardId;
if (!nextSequenceId || nextSequenceId === primarySequenceId) {
continue;
}
const nextCard = resolveCardRequestById(nextCardId);
const nextCard = resolveCardRequestById(nextSequenceId);
if (nextCard && setSecondaryCard(nextCard, true)) {
break;
}
@@ -3303,14 +3310,15 @@
return;
}
const startIndex = sequence.indexOf(lightboxState.primaryCard?.cardId);
const primarySequenceId = lightboxState.primaryCard?.sequenceId || lightboxState.primaryCard?.cardId;
const startIndex = sequence.indexOf(primarySequenceId);
if (startIndex < 0) {
return;
}
const nextIndex = (startIndex + direction + sequence.length) % sequence.length;
const nextCardId = sequence[nextIndex];
const nextCard = resolveCardRequestById(nextCardId);
const nextSequenceId = sequence[nextIndex];
const nextCard = resolveCardRequestById(nextSequenceId);
if (!nextCard?.src) {
return;
}
+227 -12
View File
@@ -47,6 +47,7 @@
courtCardByDecanId: new Map(),
loadingPromise: null
};
let detailNavigator = null;
const TAROT_TRUMP_NUMBER_BY_NAME = {
"the fool": 0,
@@ -255,9 +256,14 @@
tarotDetailImageEl: document.getElementById("tarot-detail-image"),
tarotDetailNameEl: document.getElementById("tarot-detail-name"),
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"),
tarotDetailUprightEl: document.getElementById("tarot-detail-upright"),
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"),
tarotDetailMeaningEl: document.getElementById("tarot-detail-meaning"),
tarotDetailKeywordsEl: document.getElementById("tarot-detail-keywords"),
@@ -355,6 +361,8 @@
getMagickDataset: () => state.magickDataset,
resolveTarotCardImage,
resolveTarotCardThumbnail,
getDeckVariantsForCard,
openDeckVariantLightbox,
getDisplayCardName,
buildTypeLabel,
clearChildren,
@@ -523,11 +531,14 @@
if (!state.filteredCards.some((card) => card.id === state.selectedCardId)) {
if (state.filteredCards.length > 0) {
selectCardById(state.filteredCards[0].id, elements);
} else {
syncDetailNavigation(elements);
}
return;
}
updateListSelection(elements);
syncDetailNavigation(elements);
}
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) {
const card = state.cards.find((entry) => entry.id === cardIdToSelect);
if (!card) {
@@ -733,6 +800,7 @@
updateListSelection(elements);
updateHouseSelection(elements);
renderDetail(card, elements);
syncDetailNavigation(elements);
}
function scrollCardIntoView(cardIdToReveal, elements) {
@@ -758,6 +826,47 @@
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 = "") {
const card = state.cards.find((entry) => entry.id === cardIdToResolve);
if (!card) {
@@ -790,8 +899,8 @@
};
}
function buildLightboxCardRequestById(cardIdToResolve) {
const request = buildDeckLightboxCardRequest(cardIdToResolve, getActiveDeck?.() || "");
function buildLightboxCardRequestById(cardIdToResolve, deckIdToResolve = "") {
const request = buildDeckLightboxCardRequest(cardIdToResolve, deckIdToResolve || getActiveDeck?.() || "");
if (!request?.src) {
return null;
}
@@ -799,19 +908,67 @@
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 = {}) {
const normalizedCardId = String(cardIdToOpen || "").trim();
if (!normalizedCardId) {
return;
}
const primaryCardRequest = buildLightboxCardRequestById(normalizedCardId);
const requestedDeckId = String(options?.deckId || getActiveDeck?.() || "").trim();
const primaryCardRequest = buildLightboxCardRequestById(normalizedCardId, requestedDeckId);
if (!primaryCardRequest?.src) {
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 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"
? options.onSelectCardId
: (nextCardId) => {
@@ -825,6 +982,7 @@
altText: primaryCardRequest.altText,
label: primaryCardRequest.label,
cardId: primaryCardRequest.cardId,
sequenceId: String(options?.sequenceId || primaryCardRequest.sequenceId || normalizedCardId).trim(),
deckId: primaryCardRequest.deckId || activeDeckId,
deckLabel: primaryCardRequest.deckLabel || "",
compareDetails: primaryCardRequest.compareDetails || [],
@@ -834,13 +992,72 @@
activeDeckLabel: primaryCardRequest.deckLabel || "",
availableCompareDecks,
maxCompareDecks: 2,
sequenceIds: state.cards.map((card) => card.id),
resolveCardById: buildLightboxCardRequestById,
sequenceIds,
resolveCardById,
resolveDeckCardById: buildDeckLightboxCardRequest,
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) {
if (!elements?.tarotCardListEl) {
return;
@@ -940,6 +1157,7 @@
const selected = state.cards.find((card) => card.id === state.selectedCardId);
if (selected) {
renderDetail(selected, elements);
syncDetailNavigation(elements);
}
}
return;
@@ -1007,6 +1225,8 @@
});
}
bindKeyboardNavigation(elements);
if (elements.tarotHouseTopCardsVisibleEl) {
elements.tarotHouseTopCardsVisibleEl.addEventListener("change", () => {
state.houseTopCardsVisible = Boolean(elements.tarotHouseTopCardsVisibleEl.checked);
@@ -1104,12 +1324,7 @@
return;
}
const request = buildLightboxCardRequestById(state.selectedCardId);
if (!request?.src) {
return;
}
window.TarotUiLightbox?.open?.(request);
openCardLightboxById(state.selectedCardId);
});
}
+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-700.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>
<body>
<div class="topbar">
@@ -63,8 +63,12 @@
<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>
<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-cube" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Cube</button>
</div>
</div>
<button id="open-numbers" class="settings-trigger" type="button" aria-pressed="false">Numbers</button>
@@ -216,6 +220,11 @@
<div class="planet-detail-heading">
<h2 id="calendar-detail-name">--</h2>
<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 id="calendar-detail-body"></div>
</section>
@@ -252,6 +261,11 @@
<div class="planet-detail-heading">
<h2 id="holiday-detail-name">--</h2>
<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 id="holiday-detail-body"></div>
</section>
@@ -273,10 +287,14 @@
</aside>
<section class="tarot-detail-panel" aria-live="polite">
<div class="tarot-detail-top">
<img id="tarot-detail-image" class="tarot-detail-image" alt="Tarot card image" />
<div class="tarot-detail-heading">
<h2 id="tarot-detail-name">--</h2>
<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>
</div>
@@ -291,6 +309,10 @@
</div>
</div>
<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>
<strong>Traditional Meaning</strong>
<div id="tarot-detail-meaning" class="planet-text">--</div>
@@ -539,6 +561,11 @@
<div class="planet-detail-heading">
<h2 id="planet-detail-name">--</h2>
<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>
<div class="planet-meta-grid">
@@ -774,19 +801,99 @@
</section>
<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">
<aside class="kab-rose-panel">
<div class="planet-list-header">
<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 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>
</aside>
<section class="kab-detail-panel" aria-live="polite">
<div class="planet-detail-heading">
<h2 id="kab-rose-detail-name">Rosicrucian Cross</h2>
<div id="kab-rose-detail-sub" class="planet-detail-type">Select a Hebrew letter petal to explore the path</div>
<div 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 id="kab-rose-detail-body" class="planet-meta-grid"></div>
</section>
@@ -821,6 +928,11 @@
<div class="planet-detail-heading">
<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 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 id="kab-detail-body" class="planet-meta-grid"></div>
</section>
@@ -1182,10 +1294,10 @@
<script src="node_modules/astronomy-engine/astronomy.browser.min.js"></script>
<script src="app/astro-calcs.js"></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/card-images.js?v=20260309-gate"></script>
<script src="app/ui-tarot-lightbox.js?v=20260404-lightbox-pinch-01"></script>
<script src="app/card-images.js?v=20260527-tarot-deck-gallery-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-relations.js"></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.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.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-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-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.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.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-elements.js?v=20260424-association-web-01"></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.js?v=20260424-association-web-01"></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.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-chassis.js?v=20260424-cube-fixes-01"></script>
<script src="app/ui-cube-math.js"></script>
@@ -1246,15 +1359,15 @@
<script src="app/ui-numbers.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-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-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-visuals.js?v=20260307b"></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.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>
</body>
</html>