Spaces:
Running
Running
| /* assets/site.js | |
| SILENTPATTERN — shared behaviors (Vanta, modals, lab navigator) | |
| IMPORTANT: This file must be pure JavaScript (no <script> tags inside). | |
| */ | |
| (function () { | |
| "use strict"; | |
| function q(id) { return document.getElementById(id); } | |
| function isShown(modal) { | |
| return modal && !modal.classList.contains("modal-hidden"); | |
| } | |
| function trapFocus(modal) { | |
| const focusable = modal.querySelectorAll( | |
| 'a,button,input,select,textarea,[tabindex]:not([tabindex="-1"])' | |
| ); | |
| if (!focusable.length) return; | |
| const first = focusable[0]; | |
| const last = focusable[focusable.length - 1]; | |
| function handler(e) { | |
| if (e.key === "Tab") { | |
| if (e.shiftKey) { | |
| if (document.activeElement === first) { e.preventDefault(); last.focus(); } | |
| } else { | |
| if (document.activeElement === last) { e.preventDefault(); first.focus(); } | |
| } | |
| } else if (e.key === "Escape") { | |
| // Close only THIS modal | |
| toggleModal(modal, false); | |
| } | |
| } | |
| modal.addEventListener("keydown", handler); | |
| modal._focusHandler = handler; | |
| } | |
| function untrapFocus(modal) { | |
| if (modal && modal._focusHandler) { | |
| modal.removeEventListener("keydown", modal._focusHandler); | |
| delete modal._focusHandler; | |
| } | |
| } | |
| function toggleModal(modal, show) { | |
| if (!modal) return; | |
| if (show) { | |
| modal.classList.remove("modal-hidden"); | |
| modal.classList.add("modal-visible"); | |
| modal.setAttribute("aria-hidden", "false"); | |
| document.body.style.overflow = "hidden"; | |
| // Ensure focus + focus trap | |
| setTimeout(() => { | |
| try { modal.focus(); } catch {} | |
| trapFocus(modal); | |
| }, 0); | |
| } else { | |
| modal.classList.remove("modal-visible"); | |
| modal.classList.add("modal-hidden"); | |
| modal.setAttribute("aria-hidden", "true"); | |
| document.body.style.overflow = ""; | |
| untrapFocus(modal); | |
| } | |
| } | |
| function initVanta() { | |
| const el = q("vanta-bg"); | |
| if (!el || !window.VANTA || !window.VANTA.NET) return null; | |
| const fx = window.VANTA.NET({ | |
| el: "#vanta-bg", | |
| mouseControls: true, | |
| touchControls: true, | |
| gyroControls: false, | |
| minHeight: 200.0, | |
| minWidth: 200.0, | |
| scale: 1.0, | |
| scaleMobile: 1.0, | |
| color: 0x4f46e5, | |
| backgroundColor: 0x020617, | |
| points: 12.0, | |
| maxDistance: 20.0, | |
| spacing: 15.0 | |
| }); | |
| window.addEventListener("resize", () => { | |
| try { fx.resize(); } catch {} | |
| }); | |
| return fx; | |
| } | |
| function setupAccessModal() { | |
| const accessModal = q("access-modal"); | |
| const accessBtn = q("access-btn"); | |
| const accessCta = q("access-cta"); | |
| const closeAccessModal = q("close-access-modal"); | |
| const form = q("access-form"); | |
| function openAccess() { | |
| toggleModal(accessModal, true); | |
| setTimeout(() => { | |
| const name = q("name"); | |
| if (name) name.focus(); | |
| }, 50); | |
| } | |
| if (accessBtn) accessBtn.addEventListener("click", openAccess); | |
| if (accessCta) accessCta.addEventListener("click", openAccess); | |
| if (closeAccessModal) closeAccessModal.addEventListener("click", () => toggleModal(accessModal, false)); | |
| if (accessModal) accessModal.addEventListener("click", (e) => { | |
| if (e.target === accessModal) toggleModal(accessModal, false); | |
| }); | |
| if (form) { | |
| form.addEventListener("submit", async (e) => { | |
| e.preventDefault(); | |
| const name = (q("name")?.value || "").trim(); | |
| const email = (q("email")?.value || "").trim(); | |
| const institution = (q("institution")?.value || "").trim(); | |
| const purpose = (q("purpose")?.value || "").trim(); | |
| if (!name || !email || !institution || !purpose) { | |
| alert("Please fill in all fields."); | |
| return; | |
| } | |
| // Optional server post; safe fallback | |
| try { | |
| const res = await fetch("/api/access", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ name, email, institution, purpose, page: location.pathname }) | |
| }); | |
| if (!res.ok) throw new Error("Request not accepted."); | |
| alert("Request received. You will be contacted after review."); | |
| } catch { | |
| alert("Request received. You will be contacted after review."); | |
| } | |
| form.reset(); | |
| toggleModal(accessModal, false); | |
| }); | |
| } | |
| return { accessModal, openAccess }; | |
| } | |
| function setupLabNavigator(dossiers, defaultKey) { | |
| const labNav = q("lab-navigator"); | |
| const labNavBtn = q("lab-nav-btn"); | |
| const labNavClose = q("lab-nav-close"); | |
| const dossierTitle = q("dossier-title"); | |
| const dossierSubtitle = q("dossier-subtitle"); | |
| const dossierStatus = q("dossier-status"); | |
| const dossierBody = q("dossier-body"); | |
| const dossierEvidence = q("dossier-evidence"); | |
| const dossierPrimary = q("dossier-primary"); | |
| const dossierSecondary = q("dossier-secondary"); | |
| const dossierMeta = q("dossier-meta"); | |
| function openLabNav() { toggleModal(labNav, true); } | |
| function closeLabNav() { toggleModal(labNav, false); } | |
| if (labNavBtn) labNavBtn.addEventListener("click", openLabNav); | |
| if (labNavClose) labNavClose.addEventListener("click", closeLabNav); | |
| if (labNav) { | |
| labNav.addEventListener("click", (e) => { | |
| const shouldClose = e.target && e.target.getAttribute("data-lab-close") === "true"; | |
| if (shouldClose) closeLabNav(); | |
| }); | |
| } | |
| function renderDossier(key) { | |
| const d = dossiers && dossiers[key]; | |
| if (!d) return; | |
| if (dossierTitle) dossierTitle.textContent = d.title || "Lab Dossier"; | |
| if (dossierSubtitle) dossierSubtitle.textContent = d.subtitle || ""; | |
| if (dossierStatus) dossierStatus.textContent = d.status || "READY"; | |
| if (dossierBody) dossierBody.textContent = d.body || ""; | |
| if (dossierEvidence) { | |
| dossierEvidence.innerHTML = ""; | |
| const items = Array.isArray(d.evidence) ? d.evidence : []; | |
| if (items.length) { | |
| items.forEach((item) => { | |
| const li = document.createElement("li"); | |
| li.textContent = item; | |
| dossierEvidence.appendChild(li); | |
| }); | |
| } else { | |
| const li = document.createElement("li"); | |
| li.className = "text-gray-500"; | |
| li.textContent = "No evidence items provided."; | |
| dossierEvidence.appendChild(li); | |
| } | |
| } | |
| if (dossierPrimary) { | |
| dossierPrimary.textContent = d.primary?.label || "Open"; | |
| dossierPrimary.onclick = typeof d.primary?.action === "function" ? d.primary.action : null; | |
| } | |
| if (dossierSecondary) { | |
| dossierSecondary.textContent = d.secondary?.label || "View Note"; | |
| dossierSecondary.onclick = typeof d.secondary?.action === "function" ? d.secondary.action : null; | |
| } | |
| if (dossierMeta) { | |
| const u = d.updated || "—"; | |
| dossierMeta.innerHTML = `Last updated: <span class="text-gray-300">${u}</span>`; | |
| } | |
| } | |
| // Bind dossier node clicks (if nodes exist on the page) | |
| document.querySelectorAll(".lab-node").forEach((btn) => { | |
| btn.addEventListener("click", () => { | |
| const key = btn.getAttribute("data-dossier"); | |
| renderDossier(key); | |
| }); | |
| }); | |
| // Global ESC behavior: close open modals if present | |
| document.addEventListener("keydown", (e) => { | |
| if (e.key !== "Escape") return; | |
| const accessModal = q("access-modal"); | |
| if (isShown(labNav)) closeLabNav(); | |
| if (isShown(accessModal)) toggleModal(accessModal, false); | |
| }); | |
| renderDossier(defaultKey || "start"); | |
| return { openLabNav, closeLabNav, renderDossier, labNav }; | |
| } | |
| // Export public API (library only; no auto-init to avoid duplication) | |
| window.SilentPattern = { | |
| toggleModal, | |
| initVanta, | |
| setupAccessModal, | |
| setupLabNavigator | |
| }; | |
| })(); | |