update sequence nav to use new ui-sequence-nav component

This commit is contained in:
2026-07-06 19:38:49 -07:00
parent 5b1df48883
commit 3b666611c6
8 changed files with 1106 additions and 119 deletions
+162
View File
@@ -8,6 +8,7 @@
const sidebarControllers = new WeakMap();
const sidebarControllerRegistry = new Map();
const detailControllers = new WeakMap();
const detailExportInProgress = new WeakSet();
const AUTO_COLLAPSE_ENTRY_SELECTOR = [
".planet-list-item",
".tarot-list-item",
@@ -403,6 +404,7 @@
const sectionId = layout.closest("section")?.id || `layout-${index + 1}`;
const panelId = detailPanel.id || `${sectionId}-detail-panel`;
detailPanel.id = panelId;
ensureDetailPaneExportControl(detailPanel, sectionId);
const detailStorageKey = `${DETAIL_COLLAPSE_STORAGE_PREFIX}${sectionId}`;
const sidebarStorageKey = `${SIDEBAR_COLLAPSE_STORAGE_PREFIX}${sectionId}`;
@@ -439,6 +441,166 @@
});
}
function sanitizeExportToken(value, fallback = "detail") {
const normalized = String(value || "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return normalized || fallback;
}
function canvasToWebpBlob(canvas, quality = 1) {
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
return;
}
reject(new Error("Detail pane export failed."));
}, "image/webp", quality);
});
}
async function exportDetailPaneAsWebp(detailPanel, sectionId, buttonEl = null) {
if (!(detailPanel instanceof HTMLElement) || detailExportInProgress.has(detailPanel)) {
return;
}
detailExportInProgress.add(detailPanel);
const originalButtonLabel = buttonEl instanceof HTMLButtonElement ? buttonEl.textContent : "";
if (buttonEl instanceof HTMLButtonElement) {
buttonEl.disabled = true;
buttonEl.textContent = "Exporting...";
}
let exportBlobUrl = "";
let sandboxEl = null;
try {
const html2canvas = window.html2canvas;
if (typeof html2canvas !== "function") {
throw new Error("Detail export library is unavailable. Refresh the page and try again.");
}
const exportClone = detailPanel.cloneNode(true);
if (!(exportClone instanceof HTMLElement)) {
throw new Error("Detail pane is not ready to export.");
}
exportClone.querySelectorAll(".detail-pane-export-controls").forEach((node) => node.remove());
const contentWidth = Math.max(
320,
Math.ceil(detailPanel.clientWidth || detailPanel.getBoundingClientRect().width || 0)
);
const contentHeight = Math.max(
240,
Math.ceil(detailPanel.scrollHeight || detailPanel.getBoundingClientRect().height || 0)
);
sandboxEl = document.createElement("div");
sandboxEl.style.position = "fixed";
sandboxEl.style.left = "-100000px";
sandboxEl.style.top = "0";
sandboxEl.style.width = `${contentWidth}px`;
sandboxEl.style.height = `${contentHeight}px`;
sandboxEl.style.overflow = "hidden";
sandboxEl.style.pointerEvents = "none";
sandboxEl.style.opacity = "0";
exportClone.style.width = `${contentWidth}px`;
exportClone.style.minWidth = `${contentWidth}px`;
exportClone.style.maxWidth = `${contentWidth}px`;
exportClone.style.height = `${contentHeight}px`;
exportClone.style.minHeight = `${contentHeight}px`;
exportClone.style.maxHeight = `${contentHeight}px`;
exportClone.style.overflow = "visible";
sandboxEl.appendChild(exportClone);
document.body.appendChild(sandboxEl);
const exportScale = Math.max(2, Math.min(2.5, Number(window.devicePixelRatio) || 1));
const canvas = await html2canvas(exportClone, {
backgroundColor: null,
scale: exportScale,
useCORS: true,
allowTaint: false,
logging: false,
width: contentWidth,
height: contentHeight,
windowWidth: contentWidth,
windowHeight: contentHeight,
scrollX: 0,
scrollY: 0,
imageTimeout: 12000,
onclone(clonedDocument) {
clonedDocument.querySelectorAll(".detail-pane-export-controls").forEach((node) => node.remove());
}
});
const exportBlob = await canvasToWebpBlob(canvas, 1);
exportBlobUrl = URL.createObjectURL(exportBlob);
const headingEl = detailPanel.querySelector("h1, h2, h3");
const labelToken = sanitizeExportToken(headingEl?.textContent || detailPanel.id || "detail-pane", "detail-pane");
const sectionToken = sanitizeExportToken(sectionId, "section");
const dateToken = new Date().toISOString().slice(0, 10);
const downloadLink = document.createElement("a");
downloadLink.href = exportBlobUrl;
downloadLink.download = `${sectionToken}-${labelToken}-${dateToken}.webp`;
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove();
} catch (error) {
window.alert(error instanceof Error ? error.message : "Unable to export this detail pane.");
} finally {
detailExportInProgress.delete(detailPanel);
if (buttonEl instanceof HTMLButtonElement) {
buttonEl.disabled = false;
buttonEl.textContent = originalButtonLabel || "Export WebP";
}
if (sandboxEl instanceof HTMLElement) {
sandboxEl.remove();
}
if (exportBlobUrl) {
URL.revokeObjectURL(exportBlobUrl);
}
}
}
function ensureDetailPaneExportControl(detailPanel, sectionId) {
if (!(detailPanel instanceof HTMLElement) || detailPanel.querySelector(".detail-pane-export-controls")) {
return;
}
const hostEl = detailPanel.querySelector(".detail-sequence-nav")
|| detailPanel.querySelector(".planet-detail-heading")
|| detailPanel.querySelector(".tarot-detail-heading")
|| detailPanel.querySelector(".kab-detail-heading")
|| detailPanel;
if (!(hostEl instanceof HTMLElement)) {
return;
}
const controlsEl = document.createElement("div");
controlsEl.className = "detail-pane-export-controls";
const exportButtonEl = document.createElement("button");
exportButtonEl.type = "button";
exportButtonEl.className = "detail-sequence-btn detail-export-btn";
exportButtonEl.textContent = "Export WebP";
exportButtonEl.addEventListener("click", () => {
exportDetailPaneAsWebp(detailPanel, sectionId, exportButtonEl);
});
controlsEl.appendChild(exportButtonEl);
hostEl.appendChild(controlsEl);
}
function setTopbarDropdownOpen(dropdownEl, isOpen) {
if (!(dropdownEl instanceof HTMLElement)) {
return;