echo / code /docs /site.js
amonshano's picture
Add Echo-Memory codebase used for this run (CC BY 4.0, JD Echo Team) (part 2)
eafbe80 verified
Raw
History Blame Contribute Delete
11.7 kB
(function () {
"use strict";
const NAV_OFFSET = 72;
const REDUCED_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const SCROLL_BEHAVIOR = REDUCED_MOTION ? "auto" : "smooth";
const sectionEls = Array.from(document.querySelectorAll(".section[data-section]"));
const navLinks = Array.from(document.querySelectorAll("[data-nav]"));
const slideDots = Array.from(document.querySelectorAll(".slide-dots button[data-slide]"));
const navToggle = document.getElementById("nav-toggle");
const navMobile = document.getElementById("nav-menu-mobile");
const topNav = document.querySelector(".top-nav");
const progressBar = document.querySelector(".scroll-progress__bar");
const backTop = document.getElementById("back-top");
const lightbox = document.getElementById("lightbox");
const lightboxImg = document.getElementById("lightbox-img");
const lightboxCaption = document.getElementById("lightbox-caption");
const lightboxClose = lightbox ? lightbox.querySelector(".lightbox__close") : null;
function setActiveNav(id) {
navLinks.forEach(function (a) {
a.classList.toggle("is-active", a.getAttribute("data-nav") === id);
});
slideDots.forEach(function (btn) {
btn.classList.toggle("is-active", btn.getAttribute("data-slide") === id);
});
}
function scrollToSection(id) {
var el = document.getElementById(id);
if (!el) return;
el.scrollIntoView({ behavior: SCROLL_BEHAVIOR, block: "start" });
setActiveNav(id);
if (navMobile && !navMobile.hidden) {
navToggle.setAttribute("aria-expanded", "false");
navMobile.hidden = true;
if (topNav) topNav.classList.remove("is-open");
}
}
function sectionIndex(id) {
return sectionEls.findIndex(function (s) {
return s.getAttribute("data-section") === id;
});
}
function scrollAdjacent(delta) {
var activeId = null;
sectionEls.forEach(function (s) {
var rect = s.getBoundingClientRect();
if (rect.top <= NAV_OFFSET + 80 && rect.bottom > NAV_OFFSET + 80) {
activeId = s.getAttribute("data-section");
}
});
if (!activeId && sectionEls.length) activeId = sectionEls[0].getAttribute("data-section");
var idx = sectionIndex(activeId);
if (idx < 0) return;
var next = sectionEls[idx + delta];
if (next) scrollToSection(next.getAttribute("data-section"));
}
if (sectionEls.length) {
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
setActiveNav(entry.target.getAttribute("data-section"));
}
});
},
{ root: null, rootMargin: "-" + NAV_OFFSET + "px 0px -50% 0px", threshold: 0.12 }
);
sectionEls.forEach(function (section) {
observer.observe(section);
});
}
document.querySelectorAll(".reveal").forEach(function (el) {
var revealObserver = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
revealObserver.unobserve(entry.target);
}
});
},
{ root: null, rootMargin: "0px 0px -8% 0px", threshold: 0.08 }
);
if (REDUCED_MOTION) {
el.classList.add("is-visible");
} else {
revealObserver.observe(el);
}
});
function updateScrollUI() {
var scrollTop = window.scrollY || document.documentElement.scrollTop;
var docHeight = document.documentElement.scrollHeight - window.innerHeight;
var progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
if (progressBar) progressBar.style.width = progress + "%";
if (topNav) topNav.classList.toggle("is-scrolled", scrollTop > 24);
if (backTop) {
var show = scrollTop > window.innerHeight * 0.6;
backTop.hidden = !show;
backTop.classList.toggle("is-visible", show);
}
}
window.addEventListener("scroll", updateScrollUI, { passive: true });
updateScrollUI();
navLinks.forEach(function (a) {
a.addEventListener("click", function (e) {
e.preventDefault();
scrollToSection(a.getAttribute("data-nav"));
});
});
slideDots.forEach(function (btn) {
btn.addEventListener("click", function () {
scrollToSection(btn.getAttribute("data-slide"));
});
});
if (navToggle && navMobile) {
navToggle.addEventListener("click", function () {
var open = navMobile.hidden;
navMobile.hidden = !open;
navToggle.setAttribute("aria-expanded", String(open));
if (topNav) topNav.classList.toggle("is-open", open);
});
}
if (backTop) {
backTop.addEventListener("click", function () {
scrollToSection("hero");
});
}
function openLightbox(img, captionText) {
if (!lightbox || !lightboxImg) return;
lightboxImg.src = img.src;
lightboxImg.alt = img.alt || "";
if (lightboxCaption) {
lightboxCaption.textContent = captionText || "";
}
lightbox.hidden = false;
document.body.style.overflow = "hidden";
if (lightboxClose) lightboxClose.focus();
}
function closeLightbox() {
if (!lightbox) return;
lightbox.hidden = true;
document.body.style.overflow = "";
if (lightboxImg) lightboxImg.src = "";
}
document.querySelectorAll("[data-zoomable]").forEach(function (figure) {
figure.addEventListener("click", function (e) {
if (e.target.closest(".qual-chip, .copy-btn, button, a")) return;
var img = figure.querySelector("img.is-active, img[data-qual-image], img");
if (!img || !img.src) return;
var cap = figure.querySelector("figcaption");
var capText = cap ? cap.textContent.replace(/Click to expand/g, "").trim() : "";
openLightbox(img, capText);
});
});
if (lightboxClose) {
lightboxClose.addEventListener("click", closeLightbox);
}
if (lightbox) {
lightbox.addEventListener("click", function (e) {
if (e.target === lightbox) closeLightbox();
});
}
document.addEventListener("keydown", function (e) {
if (e.target.closest("input, textarea, pre, [contenteditable]")) return;
if (e.key === "Escape" && lightbox && !lightbox.hidden) {
closeLightbox();
return;
}
if (e.key === "ArrowDown" || e.key === "PageDown") {
e.preventDefault();
scrollAdjacent(1);
} else if (e.key === "ArrowUp" || e.key === "PageUp") {
e.preventDefault();
scrollAdjacent(-1);
}
});
function animateMetricValue(el, target) {
if (REDUCED_MOTION || !target || target === "—") {
el.textContent = target;
return;
}
var numeric = parseInt(String(target).replace(/,/g, ""), 10);
if (isNaN(numeric)) {
el.textContent = target;
return;
}
var start = 0;
var duration = 700;
var startTime = null;
function step(ts) {
if (!startTime) startTime = ts;
var t = Math.min((ts - startTime) / duration, 1);
var eased = 1 - Math.pow(1 - t, 3);
el.textContent = Math.round(start + (numeric - start) * eased).toLocaleString();
if (t < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
function hydrateMetric(card) {
var valueEl = card.querySelector("[data-metric-value]");
var badgeUrl = card.getAttribute("data-badge-url");
if (!valueEl || !badgeUrl) return;
fetch(badgeUrl, { cache: "no-store" })
.then(function (res) {
if (!res.ok) throw new Error("Request failed");
return res.json();
})
.then(function (data) {
var val = data.value || data.message || valueEl.dataset.fallback || "—";
animateMetricValue(valueEl, val);
})
.catch(function () {
valueEl.textContent = valueEl.dataset.fallback || "—";
});
}
document.querySelectorAll("[data-badge-url]").forEach(function (card) {
var metricObserver = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
hydrateMetric(entry.target);
metricObserver.unobserve(entry.target);
}
});
},
{ threshold: 0.4 }
);
metricObserver.observe(card);
});
document.querySelectorAll("[data-qual-viewer]").forEach(function (viewer) {
var image = viewer.querySelector("[data-qual-image]");
var caption = viewer.querySelector("[data-qual-caption]");
var chips = viewer.querySelectorAll(".qual-chip");
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
chips.forEach(function (c) {
c.classList.remove("is-active");
c.setAttribute("aria-selected", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-selected", "true");
if (!image) return;
var nextSrc = chip.dataset.src;
var nextAlt = chip.dataset.alt || "";
var nextCaption = chip.dataset.caption || "";
var currentPath = new URL(image.src, window.location.href).pathname;
if (currentPath.endsWith(nextSrc)) return;
image.classList.add("is-fading");
var preload = new Image();
preload.onload = function () {
image.src = nextSrc;
image.alt = nextAlt;
image.classList.remove("is-fading");
image.classList.add("is-active");
};
preload.onerror = function () {
image.classList.remove("is-fading");
};
preload.src = nextSrc;
if (caption) {
var hint = (window.EchoI18n && window.EchoI18n.t("zoom.hint")) || "Click to expand";
caption.innerHTML = nextCaption + ' <span class="zoom-hint">' + hint + "</span>";
}
});
});
});
function initCiteSwitcher() {
document.querySelectorAll("[data-cite-switcher]").forEach(function (switcher) {
var chips = switcher.querySelectorAll(".cite-chip");
var panels = switcher.querySelectorAll("[data-cite-panel]");
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
var key = chip.getAttribute("data-cite");
chips.forEach(function (c) {
c.classList.remove("is-active");
c.setAttribute("aria-selected", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-selected", "true");
panels.forEach(function (panel) {
var match = panel.getAttribute("data-cite-panel") === key;
panel.classList.toggle("is-active", match);
panel.hidden = !match;
});
});
});
});
}
initCiteSwitcher();
document.querySelectorAll("[data-copy-target]").forEach(function (btn) {
btn.addEventListener("click", function () {
var targetId = btn.getAttribute("data-copy-target");
var target = document.getElementById(targetId);
if (!target) return;
navigator.clipboard.writeText(target.textContent).then(
function () {
var copied = window.EchoI18n && window.EchoI18n.t("bibtex.copied");
var failed = window.EchoI18n && window.EchoI18n.t("bibtex.failed");
var copyLabel = window.EchoI18n && window.EchoI18n.t("bibtex.copy");
btn.textContent = copied || "Copied";
btn.classList.add("is-copied");
setTimeout(function () {
btn.textContent = copyLabel || "Copy";
btn.classList.remove("is-copied");
}, 2000);
},
function () {
btn.textContent = (window.EchoI18n && window.EchoI18n.t("bibtex.failed")) || "Failed";
}
);
});
});
})();