building new tarot frame component for custom layout

This commit is contained in:
2026-04-01 12:31:56 -07:00
parent d47e63df6d
commit a7d956cee8
11 changed files with 2359 additions and 79 deletions

View File

@@ -4,6 +4,8 @@
let overlayEl = null;
let backdropEl = null;
let toolbarEl = null;
let settingsButtonEl = null;
let settingsPanelEl = null;
let helpButtonEl = null;
let helpPanelEl = null;
let compareButtonEl = null;
@@ -79,6 +81,7 @@
onSelectCardId: null,
overlayOpacity: LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY,
zoomScale: LIGHTBOX_ZOOM_SCALE,
settingsMenuOpen: false,
helpOpen: false,
primaryRotated: false,
overlayRotated: false,
@@ -350,6 +353,27 @@
}
}
function closeSettingsMenu() {
lightboxState.settingsMenuOpen = false;
if (settingsPanelEl) {
settingsPanelEl.style.display = "none";
}
}
function toggleSettingsMenu() {
if (!lightboxState.isOpen || zoomed) {
return;
}
const nextOpen = !lightboxState.settingsMenuOpen;
lightboxState.settingsMenuOpen = nextOpen;
if (nextOpen) {
lightboxState.helpOpen = false;
closeDeckComparePanel();
}
applyComparePresentation();
}
function suppressDeckCompareToggle(durationMs = 400) {
suppressDeckCompareToggleUntil = Date.now() + Math.max(0, Number(durationMs) || 0);
}
@@ -417,6 +441,7 @@
function toggleDeckComparePanel() {
if (!lightboxState.allowDeckCompare) {
closeSettingsMenu();
lightboxState.deckComparePickerOpen = true;
lightboxState.deckCompareMessage = "Add another registered deck to use deck compare.";
applyComparePresentation();
@@ -426,6 +451,7 @@
if (lightboxState.deckComparePickerOpen) {
closeDeckComparePanel();
} else {
closeSettingsMenu();
lightboxState.deckComparePickerOpen = true;
}
lightboxState.deckCompareMessage = lightboxState.availableCompareDecks.length
@@ -798,13 +824,33 @@
&& mobileInfoPanelEl
&& mobileInfoPanelEl.style.display !== "none"
);
const settingsPanelVisible = Boolean(
lightboxState.settingsMenuOpen
&& settingsPanelEl
&& settingsPanelEl.style.display !== "none"
);
const helpPanelVisible = Boolean(
lightboxState.helpOpen
&& helpPanelEl
&& helpPanelEl.style.display !== "none"
);
const deckPickerVisible = Boolean(
lightboxState.deckComparePickerOpen
&& deckComparePanelEl
&& deckComparePanelEl.style.display !== "none"
);
const toolbarHeight = toolbarEl instanceof HTMLElement && toolbarEl.style.display !== "none"
? toolbarEl.offsetHeight
: 0;
const infoPanelHeight = mobileInfoPanelVisible && mobileInfoPanelEl instanceof HTMLElement
? mobileInfoPanelEl.offsetHeight
: 0;
const bottomOffset = toolbarHeight + (mobileInfoPanelVisible ? infoPanelHeight + 32 : 24);
const floatingPanelHeight = Math.max(
settingsPanelVisible && settingsPanelEl instanceof HTMLElement ? settingsPanelEl.offsetHeight + 12 : 0,
helpPanelVisible && helpPanelEl instanceof HTMLElement ? helpPanelEl.offsetHeight + 12 : 0,
deckPickerVisible && deckComparePanelEl instanceof HTMLElement ? deckComparePanelEl.offsetHeight + 12 : 0
);
const bottomOffset = toolbarHeight + floatingPanelHeight + (mobileInfoPanelVisible ? infoPanelHeight + 32 : 24);
mobilePrevButtonEl.style.top = "auto";
mobileNextButtonEl.style.top = "auto";
@@ -902,6 +948,19 @@
setOverlayOpacity(lightboxState.overlayOpacity);
}
function syncSettingsUi() {
if (!settingsButtonEl || !settingsPanelEl) {
return;
}
const canShow = lightboxState.isOpen && !zoomed;
settingsButtonEl.style.display = canShow ? "inline-flex" : "none";
settingsButtonEl.textContent = lightboxState.settingsMenuOpen ? "Hide Settings" : "Settings";
settingsButtonEl.setAttribute("aria-expanded", canShow && lightboxState.settingsMenuOpen ? "true" : "false");
settingsPanelEl.style.display = canShow && lightboxState.settingsMenuOpen ? "flex" : "none";
settingsPanelEl.style.pointerEvents = canShow && lightboxState.settingsMenuOpen ? "auto" : "none";
}
function syncDeckComparePicker() {
if (!deckCompareButtonEl || !deckComparePanelEl || !deckCompareMessageEl || !deckCompareDeckListEl) {
return;
@@ -1077,21 +1136,6 @@
return;
}
const isCompact = isCompactLightboxLayout();
if (isCompact) {
if (helpButtonEl.parentElement !== toolbarEl) {
toolbarEl.insertBefore(helpButtonEl, zoomControlEl || null);
}
helpButtonEl.style.position = "static";
helpButtonEl.style.zIndex = "auto";
} else {
if (helpButtonEl.parentElement !== overlayEl) {
overlayEl.appendChild(helpButtonEl);
}
helpButtonEl.style.position = "fixed";
helpButtonEl.style.zIndex = "2";
}
const canShow = lightboxState.isOpen && !zoomed;
helpButtonEl.style.display = canShow ? "inline-flex" : "none";
helpPanelEl.style.display = canShow && lightboxState.helpOpen ? "flex" : "none";
@@ -1121,18 +1165,22 @@
const isCompact = isCompactLightboxLayout();
if (!isCompact) {
helpButtonEl.style.right = "auto";
helpButtonEl.style.top = "24px";
helpButtonEl.style.left = "24px";
settingsPanelEl.style.top = "72px";
settingsPanelEl.style.right = "24px";
settingsPanelEl.style.bottom = "auto";
settingsPanelEl.style.left = "auto";
settingsPanelEl.style.width = "min(320px, calc(100vw - 48px))";
settingsPanelEl.style.maxHeight = "none";
settingsPanelEl.style.overflowY = "visible";
helpPanelEl.style.top = "72px";
helpPanelEl.style.right = "auto";
helpPanelEl.style.right = "24px";
helpPanelEl.style.bottom = "auto";
helpPanelEl.style.left = "24px";
helpPanelEl.style.left = "auto";
helpPanelEl.style.width = "min(320px, calc(100vw - 48px))";
helpPanelEl.style.maxHeight = "none";
helpPanelEl.style.overflowY = "visible";
deckComparePanelEl.style.top = "24px";
deckComparePanelEl.style.right = "176px";
deckComparePanelEl.style.top = "72px";
deckComparePanelEl.style.right = "24px";
deckComparePanelEl.style.bottom = "auto";
deckComparePanelEl.style.left = "auto";
deckComparePanelEl.style.width = "min(280px, calc(100vw - 48px))";
@@ -1145,6 +1193,7 @@
|| !lightboxState.allowOverlayCompare
|| (!isCompact && lightboxState.compareMode && !hasSecondaryCard());
compareButtonEl.textContent = lightboxState.compareMode ? "Done Overlay" : "Overlay";
syncSettingsUi();
syncHelpUi();
syncZoomControl();
syncOpacityControl();
@@ -1205,10 +1254,13 @@
toolbarEl.style.flexWrap = "wrap";
toolbarEl.style.alignItems = "center";
toolbarEl.style.justifyContent = "center";
helpButtonEl.style.top = "auto";
helpButtonEl.style.right = "auto";
helpButtonEl.style.bottom = "auto";
helpButtonEl.style.left = "auto";
settingsPanelEl.style.top = "auto";
settingsPanelEl.style.right = "12px";
settingsPanelEl.style.bottom = "calc(72px + env(safe-area-inset-bottom, 0px))";
settingsPanelEl.style.left = "12px";
settingsPanelEl.style.width = "auto";
settingsPanelEl.style.maxHeight = "min(56svh, 440px)";
settingsPanelEl.style.overflowY = "auto";
helpPanelEl.style.top = "auto";
helpPanelEl.style.right = "12px";
helpPanelEl.style.bottom = "calc(72px + env(safe-area-inset-bottom, 0px))";
@@ -1540,15 +1592,28 @@
overlayEl.style.pointerEvents = "none";
overlayEl.style.overscrollBehavior = "contain";
settingsButtonEl = document.createElement("button");
settingsButtonEl.type = "button";
settingsButtonEl.textContent = "Settings";
settingsButtonEl.style.display = "none";
settingsButtonEl.style.alignItems = "center";
settingsButtonEl.style.justifyContent = "center";
settingsButtonEl.style.border = "1px solid rgba(255, 255, 255, 0.2)";
settingsButtonEl.style.background = "rgba(15, 23, 42, 0.84)";
settingsButtonEl.style.color = "#f8fafc";
settingsButtonEl.style.borderRadius = "999px";
settingsButtonEl.style.padding = "10px 14px";
settingsButtonEl.style.font = "600 13px/1.1 sans-serif";
settingsButtonEl.style.cursor = "pointer";
settingsButtonEl.style.backdropFilter = "blur(12px)";
helpButtonEl = document.createElement("button");
helpButtonEl.type = "button";
helpButtonEl.textContent = "Help";
helpButtonEl.style.position = "fixed";
helpButtonEl.style.top = "24px";
helpButtonEl.style.left = "24px";
helpButtonEl.style.display = "none";
helpButtonEl.style.alignItems = "center";
helpButtonEl.style.justifyContent = "center";
helpButtonEl.style.width = "100%";
helpButtonEl.style.border = "1px solid rgba(255, 255, 255, 0.2)";
helpButtonEl.style.background = "rgba(15, 23, 42, 0.84)";
helpButtonEl.style.color = "#f8fafc";
@@ -1557,8 +1622,28 @@
helpButtonEl.style.font = "600 13px/1.1 sans-serif";
helpButtonEl.style.cursor = "pointer";
helpButtonEl.style.backdropFilter = "blur(12px)";
helpButtonEl.style.pointerEvents = "auto";
helpButtonEl.style.zIndex = "2";
settingsPanelEl = document.createElement("div");
settingsPanelEl.style.position = "fixed";
settingsPanelEl.style.top = "72px";
settingsPanelEl.style.right = "24px";
settingsPanelEl.style.display = "none";
settingsPanelEl.style.flexDirection = "column";
settingsPanelEl.style.gap = "10px";
settingsPanelEl.style.width = "min(320px, calc(100vw - 48px))";
settingsPanelEl.style.padding = "14px 16px";
settingsPanelEl.style.borderRadius = "18px";
settingsPanelEl.style.background = "rgba(2, 6, 23, 0.88)";
settingsPanelEl.style.border = "1px solid rgba(148, 163, 184, 0.16)";
settingsPanelEl.style.color = "#f8fafc";
settingsPanelEl.style.boxShadow = "0 16px 42px rgba(0, 0, 0, 0.34)";
settingsPanelEl.style.backdropFilter = "blur(12px)";
settingsPanelEl.style.pointerEvents = "auto";
settingsPanelEl.style.zIndex = "3";
const settingsTitleEl = document.createElement("div");
settingsTitleEl.textContent = "Lightbox Settings";
settingsTitleEl.style.font = "700 13px/1.3 sans-serif";
helpPanelEl = document.createElement("div");
helpPanelEl.style.position = "fixed";
@@ -1640,6 +1725,10 @@
compareButtonEl.style.font = "600 13px/1.1 sans-serif";
compareButtonEl.style.cursor = "pointer";
compareButtonEl.style.backdropFilter = "blur(12px)";
compareButtonEl.style.display = "inline-flex";
compareButtonEl.style.alignItems = "center";
compareButtonEl.style.justifyContent = "center";
compareButtonEl.style.width = "100%";
deckCompareButtonEl = document.createElement("button");
deckCompareButtonEl.type = "button";
@@ -1652,10 +1741,15 @@
deckCompareButtonEl.style.font = "600 13px/1.1 sans-serif";
deckCompareButtonEl.style.cursor = "pointer";
deckCompareButtonEl.style.backdropFilter = "blur(12px)";
deckCompareButtonEl.style.alignItems = "center";
deckCompareButtonEl.style.justifyContent = "center";
deckCompareButtonEl.style.width = "100%";
zoomControlEl = document.createElement("label");
zoomControlEl.style.display = "flex";
zoomControlEl.style.alignItems = "center";
zoomControlEl.style.justifyContent = "space-between";
zoomControlEl.style.width = "100%";
zoomControlEl.style.gap = "8px";
zoomControlEl.style.padding = "10px 14px";
zoomControlEl.style.border = "1px solid rgba(255, 255, 255, 0.2)";
@@ -1687,6 +1781,8 @@
opacityControlEl = document.createElement("label");
opacityControlEl.style.display = "none";
opacityControlEl.style.alignItems = "center";
opacityControlEl.style.justifyContent = "space-between";
opacityControlEl.style.width = "100%";
opacityControlEl.style.gap = "8px";
opacityControlEl.style.padding = "10px 14px";
opacityControlEl.style.border = "1px solid rgba(255, 255, 255, 0.2)";
@@ -1780,6 +1876,9 @@
mobileInfoButtonEl.style.font = "600 13px/1.1 sans-serif";
mobileInfoButtonEl.style.cursor = "pointer";
mobileInfoButtonEl.style.backdropFilter = "blur(12px)";
mobileInfoButtonEl.style.alignItems = "center";
mobileInfoButtonEl.style.justifyContent = "center";
mobileInfoButtonEl.style.width = "100%";
mobileInfoPrimaryTabEl = document.createElement("button");
mobileInfoPrimaryTabEl.type = "button";
@@ -1793,6 +1892,9 @@
mobileInfoPrimaryTabEl.style.font = "600 13px/1.1 sans-serif";
mobileInfoPrimaryTabEl.style.cursor = "pointer";
mobileInfoPrimaryTabEl.style.backdropFilter = "blur(12px)";
mobileInfoPrimaryTabEl.style.alignItems = "center";
mobileInfoPrimaryTabEl.style.justifyContent = "center";
mobileInfoPrimaryTabEl.style.width = "100%";
mobileInfoSecondaryTabEl = document.createElement("button");
mobileInfoSecondaryTabEl.type = "button";
@@ -1806,16 +1908,22 @@
mobileInfoSecondaryTabEl.style.font = "600 13px/1.1 sans-serif";
mobileInfoSecondaryTabEl.style.cursor = "pointer";
mobileInfoSecondaryTabEl.style.backdropFilter = "blur(12px)";
mobileInfoSecondaryTabEl.style.alignItems = "center";
mobileInfoSecondaryTabEl.style.justifyContent = "center";
mobileInfoSecondaryTabEl.style.width = "100%";
toolbarEl.append(
settingsPanelEl.append(
settingsTitleEl,
compareButtonEl,
deckCompareButtonEl,
mobileInfoButtonEl,
mobileInfoPrimaryTabEl,
mobileInfoSecondaryTabEl,
helpButtonEl,
zoomControlEl,
opacityControlEl
);
toolbarEl.append(settingsButtonEl);
stageEl = document.createElement("div");
stageEl.style.position = "fixed";
@@ -2106,7 +2214,7 @@
overlayLayerEl.appendChild(overlayImageEl);
frameEl.append(baseLayerEl, overlayLayerEl, mobileInfoPanelEl);
stageEl.append(frameEl, compareGridEl, primaryInfoEl, secondaryInfoEl);
overlayEl.append(backdropEl, stageEl, toolbarEl, deckComparePanelEl, helpButtonEl, helpPanelEl, mobilePrevButtonEl, mobileNextButtonEl);
overlayEl.append(backdropEl, stageEl, toolbarEl, settingsPanelEl, deckComparePanelEl, helpPanelEl, mobilePrevButtonEl, mobileNextButtonEl);
const close = () => {
if (!overlayEl || !imageEl || !overlayImageEl) {
@@ -2134,6 +2242,7 @@
lightboxState.onSelectCardId = null;
lightboxState.overlayOpacity = LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY;
lightboxState.zoomScale = LIGHTBOX_ZOOM_SCALE;
lightboxState.settingsMenuOpen = false;
lightboxState.helpOpen = false;
lightboxState.primaryRotated = false;
lightboxState.overlayRotated = false;
@@ -2169,8 +2278,6 @@
lightboxState.compareMode = !lightboxState.compareMode;
if (!lightboxState.compareMode) {
clearSecondaryCard();
} else if (isCompactLightboxLayout()) {
lightboxState.mobileInfoOpen = true;
}
applyComparePresentation();
}
@@ -2311,9 +2418,24 @@
backdropEl.addEventListener("click", close);
helpButtonEl.addEventListener("click", () => {
lightboxState.helpOpen = !lightboxState.helpOpen;
if (lightboxState.helpOpen) {
closeSettingsMenu();
}
syncHelpUi();
restoreLightboxFocus();
});
settingsButtonEl.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
toggleSettingsMenu();
restoreLightboxFocus();
});
settingsPanelEl.addEventListener("pointerdown", (event) => {
event.stopPropagation();
});
settingsPanelEl.addEventListener("click", (event) => {
event.stopPropagation();
});
compareButtonEl.addEventListener("click", () => {
toggleCompareMode();
restoreLightboxFocus();
@@ -2646,10 +2768,11 @@
: null;
lightboxState.overlayOpacity = LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY;
lightboxState.zoomScale = LIGHTBOX_ZOOM_SCALE;
lightboxState.settingsMenuOpen = false;
lightboxState.helpOpen = false;
lightboxState.primaryRotated = false;
lightboxState.overlayRotated = false;
lightboxState.mobileInfoOpen = isCompactLightboxLayout();
lightboxState.mobileInfoOpen = false;
lightboxState.mobileInfoView = "primary";
imageEl.src = normalizedPrimary.src;