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
+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);
});
}