Spaces:
Running
Running
Update assets/site.js
Browse files- assets/site.js +112 -177
assets/site.js
CHANGED
|
@@ -1,32 +1,20 @@
|
|
| 1 |
/* assets/site.js
|
| 2 |
-
SILENTPATTERN — shared
|
|
|
|
| 3 |
*/
|
|
|
|
| 4 |
(function () {
|
| 5 |
-
|
| 6 |
|
| 7 |
function q(id) { return document.getElementById(id); }
|
| 8 |
-
function qs(sel, root) { return (root || document).querySelector(sel); }
|
| 9 |
-
function qsa(sel, root) { return Array.from((root || document).querySelectorAll(sel)); }
|
| 10 |
-
|
| 11 |
-
// Namespace
|
| 12 |
-
const SilentPattern = (window.SilentPattern = window.SilentPattern || {});
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
try { modal._sp_prevActive = document.activeElement; } catch { /* ignore */ }
|
| 17 |
-
}
|
| 18 |
-
function restoreActiveElement(modal) {
|
| 19 |
-
const prev = modal && modal._sp_prevActive;
|
| 20 |
-
if (prev && typeof prev.focus === 'function') {
|
| 21 |
-
setTimeout(() => { try { prev.focus(); } catch {} }, 0);
|
| 22 |
-
}
|
| 23 |
-
if (modal) delete modal._sp_prevActive;
|
| 24 |
}
|
| 25 |
|
| 26 |
function trapFocus(modal) {
|
| 27 |
-
const focusable =
|
| 28 |
-
'a
|
| 29 |
-
modal
|
| 30 |
);
|
| 31 |
if (!focusable.length) return;
|
| 32 |
|
|
@@ -34,40 +22,26 @@
|
|
| 34 |
const last = focusable[focusable.length - 1];
|
| 35 |
|
| 36 |
function handler(e) {
|
| 37 |
-
if (e.key ===
|
| 38 |
if (e.shiftKey) {
|
| 39 |
if (document.activeElement === first) { e.preventDefault(); last.focus(); }
|
| 40 |
} else {
|
| 41 |
if (document.activeElement === last) { e.preventDefault(); first.focus(); }
|
| 42 |
}
|
| 43 |
-
} else if (e.key ===
|
|
|
|
| 44 |
toggleModal(modal, false);
|
| 45 |
}
|
| 46 |
}
|
| 47 |
|
| 48 |
-
modal.addEventListener(
|
| 49 |
-
modal.
|
| 50 |
}
|
| 51 |
|
| 52 |
function untrapFocus(modal) {
|
| 53 |
-
if (modal && modal.
|
| 54 |
-
modal.removeEventListener(
|
| 55 |
-
delete modal.
|
| 56 |
-
}
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
// Scroll lock that won't stomp existing inline styles
|
| 60 |
-
let _prevOverflow = null;
|
| 61 |
-
function lockScroll() {
|
| 62 |
-
if (_prevOverflow === null) _prevOverflow = document.body.style.overflow || '';
|
| 63 |
-
document.body.style.overflow = 'hidden';
|
| 64 |
-
}
|
| 65 |
-
function unlockScroll() {
|
| 66 |
-
if (_prevOverflow !== null) {
|
| 67 |
-
document.body.style.overflow = _prevOverflow;
|
| 68 |
-
_prevOverflow = null;
|
| 69 |
-
} else {
|
| 70 |
-
document.body.style.overflow = '';
|
| 71 |
}
|
| 72 |
}
|
| 73 |
|
|
@@ -75,38 +49,31 @@
|
|
| 75 |
if (!modal) return;
|
| 76 |
|
| 77 |
if (show) {
|
| 78 |
-
|
| 79 |
-
modal.classList.
|
| 80 |
-
modal.
|
| 81 |
-
|
| 82 |
-
lockScroll();
|
| 83 |
-
|
| 84 |
-
// Ensure modal is focusable
|
| 85 |
-
if (!modal.hasAttribute('tabindex')) modal.setAttribute('tabindex', '-1');
|
| 86 |
|
|
|
|
| 87 |
setTimeout(() => {
|
| 88 |
try { modal.focus(); } catch {}
|
| 89 |
trapFocus(modal);
|
| 90 |
}, 0);
|
| 91 |
} else {
|
| 92 |
-
modal.classList.remove(
|
| 93 |
-
modal.classList.add(
|
| 94 |
-
modal.setAttribute(
|
|
|
|
| 95 |
untrapFocus(modal);
|
| 96 |
-
unlockScroll();
|
| 97 |
-
restoreActiveElement(modal);
|
| 98 |
}
|
| 99 |
}
|
| 100 |
|
| 101 |
function initVanta() {
|
| 102 |
-
const el = q(
|
| 103 |
if (!el || !window.VANTA || !window.VANTA.NET) return null;
|
| 104 |
|
| 105 |
-
// Avoid double-init if a page re-renders or scripts reload
|
| 106 |
-
if (el._sp_vanta) return el._sp_vanta;
|
| 107 |
-
|
| 108 |
const fx = window.VANTA.NET({
|
| 109 |
-
el:
|
| 110 |
mouseControls: true,
|
| 111 |
touchControls: true,
|
| 112 |
gyroControls: false,
|
|
@@ -121,69 +88,64 @@
|
|
| 121 |
spacing: 15.0
|
| 122 |
});
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
const onResize = () => { try { fx.resize(); } catch {} };
|
| 128 |
-
window.addEventListener('resize', onResize);
|
| 129 |
|
| 130 |
return fx;
|
| 131 |
}
|
| 132 |
|
| 133 |
function setupAccessModal() {
|
| 134 |
-
const accessModal = q(
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
const
|
| 138 |
-
const
|
| 139 |
-
const closeAccessModal = q('close-access-modal');
|
| 140 |
|
| 141 |
function openAccess() {
|
| 142 |
toggleModal(accessModal, true);
|
| 143 |
setTimeout(() => {
|
| 144 |
-
const name = q(
|
| 145 |
if (name) name.focus();
|
| 146 |
}, 50);
|
| 147 |
}
|
| 148 |
|
| 149 |
-
if (accessBtn) accessBtn.addEventListener(
|
| 150 |
-
if (accessCta) accessCta.addEventListener(
|
| 151 |
-
if (closeAccessModal) closeAccessModal.addEventListener('click', () => toggleModal(accessModal, false));
|
| 152 |
|
| 153 |
-
|
| 154 |
-
accessModal.addEventListener(
|
| 155 |
if (e.target === accessModal) toggleModal(accessModal, false);
|
| 156 |
});
|
| 157 |
|
| 158 |
-
const form = q('access-form');
|
| 159 |
if (form) {
|
| 160 |
-
form.addEventListener(
|
| 161 |
e.preventDefault();
|
| 162 |
|
| 163 |
-
const name = (q(
|
| 164 |
-
const email = (q(
|
| 165 |
-
const institution = (q(
|
| 166 |
-
const purpose = (q(
|
| 167 |
|
| 168 |
if (!name || !email || !institution || !purpose) {
|
| 169 |
-
alert(
|
| 170 |
return;
|
| 171 |
}
|
| 172 |
|
| 173 |
-
//
|
| 174 |
try {
|
| 175 |
-
const res = await fetch(
|
| 176 |
-
method:
|
| 177 |
-
headers: {
|
| 178 |
body: JSON.stringify({ name, email, institution, purpose, page: location.pathname })
|
| 179 |
});
|
| 180 |
-
if (!res.ok) throw new Error(
|
| 181 |
-
alert(
|
| 182 |
} catch {
|
| 183 |
-
alert(
|
| 184 |
}
|
| 185 |
|
| 186 |
-
|
| 187 |
toggleModal(accessModal, false);
|
| 188 |
});
|
| 189 |
}
|
|
@@ -192,126 +154,99 @@
|
|
| 192 |
}
|
| 193 |
|
| 194 |
function setupLabNavigator(dossiers, defaultKey) {
|
| 195 |
-
const labNav = q(
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
function openLabNav() { toggleModal(labNav, true); }
|
| 202 |
function closeLabNav() { toggleModal(labNav, false); }
|
| 203 |
|
| 204 |
-
if (labNavBtn) labNavBtn.addEventListener(
|
| 205 |
-
if (labNavClose) labNavClose.addEventListener(
|
| 206 |
-
|
| 207 |
-
// Close when clicking the overlay element marked for close
|
| 208 |
-
labNav.addEventListener('click', (e) => {
|
| 209 |
-
const t = e.target;
|
| 210 |
-
const shouldClose = t && typeof t.getAttribute === 'function' && t.getAttribute('data-lab-close') === 'true';
|
| 211 |
-
if (shouldClose) closeLabNav();
|
| 212 |
-
});
|
| 213 |
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
const dossierSecondary = q('dossier-secondary');
|
| 221 |
-
const dossierMeta = q('dossier-meta');
|
| 222 |
|
| 223 |
function renderDossier(key) {
|
| 224 |
const d = dossiers && dossiers[key];
|
| 225 |
if (!d) return;
|
| 226 |
|
| 227 |
-
if (dossierTitle) dossierTitle.textContent = d.title ||
|
| 228 |
-
if (dossierSubtitle) dossierSubtitle.textContent = d.subtitle ||
|
| 229 |
-
if (dossierStatus) dossierStatus.textContent = d.status ||
|
| 230 |
-
if (dossierBody) dossierBody.textContent = d.body ||
|
| 231 |
|
| 232 |
if (dossierEvidence) {
|
| 233 |
-
dossierEvidence.innerHTML =
|
| 234 |
-
const
|
| 235 |
-
if (
|
| 236 |
-
|
| 237 |
-
const li = document.createElement(
|
| 238 |
-
li.textContent =
|
| 239 |
dossierEvidence.appendChild(li);
|
| 240 |
});
|
| 241 |
} else {
|
| 242 |
-
const li = document.createElement(
|
| 243 |
-
li.className =
|
| 244 |
-
li.textContent =
|
| 245 |
dossierEvidence.appendChild(li);
|
| 246 |
}
|
| 247 |
}
|
| 248 |
|
| 249 |
if (dossierPrimary) {
|
| 250 |
-
dossierPrimary.textContent =
|
| 251 |
-
dossierPrimary.onclick =
|
| 252 |
}
|
|
|
|
| 253 |
if (dossierSecondary) {
|
| 254 |
-
dossierSecondary.textContent =
|
| 255 |
-
dossierSecondary.onclick =
|
| 256 |
}
|
| 257 |
|
| 258 |
if (dossierMeta) {
|
| 259 |
-
const u = d.updated ||
|
| 260 |
dossierMeta.innerHTML = `Last updated: <span class="text-gray-300">${u}</span>`;
|
| 261 |
}
|
| 262 |
}
|
| 263 |
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
|
|
|
| 268 |
});
|
| 269 |
});
|
| 270 |
|
| 271 |
-
// Global ESC
|
| 272 |
-
document.addEventListener(
|
| 273 |
-
if (e.key !==
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
if (labNav && !labNav.classList.contains('modal-hidden')) closeLabNav();
|
| 278 |
-
if (accessModal && !accessModal.classList.contains('modal-hidden')) toggleModal(accessModal, false);
|
| 279 |
});
|
| 280 |
|
| 281 |
-
renderDossier(defaultKey ||
|
| 282 |
return { openLabNav, closeLabNav, renderDossier, labNav };
|
| 283 |
}
|
| 284 |
|
| 285 |
-
//
|
| 286 |
-
SilentPattern
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
function boot() {
|
| 293 |
-
// Vanta only if present + libs loaded
|
| 294 |
-
initVanta();
|
| 295 |
-
|
| 296 |
-
// Access modal only if markup exists
|
| 297 |
-
const access = setupAccessModal();
|
| 298 |
-
|
| 299 |
-
// Lab navigator: requires markup + dossiers provided by page
|
| 300 |
-
// Pages should define: window.SilentPatternDossiers = {...}; window.SilentPatternDefaultDossier = 'start';
|
| 301 |
-
const dossiers = window.SilentPatternDossiers;
|
| 302 |
-
if (dossiers && q('lab-navigator')) {
|
| 303 |
-
setupLabNavigator(dossiers, window.SilentPatternDefaultDossier || 'start');
|
| 304 |
-
}
|
| 305 |
-
|
| 306 |
-
// Optional convenience: expose openAccess if available
|
| 307 |
-
if (access && typeof access.openAccess === 'function') {
|
| 308 |
-
SilentPattern.openAccess = access.openAccess;
|
| 309 |
-
}
|
| 310 |
-
}
|
| 311 |
-
|
| 312 |
-
if (document.readyState === 'loading') {
|
| 313 |
-
document.addEventListener('DOMContentLoaded', boot);
|
| 314 |
-
} else {
|
| 315 |
-
boot();
|
| 316 |
-
}
|
| 317 |
})();
|
|
|
|
| 1 |
/* assets/site.js
|
| 2 |
+
SILENTPATTERN — shared behaviors (Vanta, modals, lab navigator)
|
| 3 |
+
IMPORTANT: This file must be pure JavaScript (no <script> tags inside).
|
| 4 |
*/
|
| 5 |
+
|
| 6 |
(function () {
|
| 7 |
+
"use strict";
|
| 8 |
|
| 9 |
function q(id) { return document.getElementById(id); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
function isShown(modal) {
|
| 12 |
+
return modal && !modal.classList.contains("modal-hidden");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
}
|
| 14 |
|
| 15 |
function trapFocus(modal) {
|
| 16 |
+
const focusable = modal.querySelectorAll(
|
| 17 |
+
'a,button,input,select,textarea,[tabindex]:not([tabindex="-1"])'
|
|
|
|
| 18 |
);
|
| 19 |
if (!focusable.length) return;
|
| 20 |
|
|
|
|
| 22 |
const last = focusable[focusable.length - 1];
|
| 23 |
|
| 24 |
function handler(e) {
|
| 25 |
+
if (e.key === "Tab") {
|
| 26 |
if (e.shiftKey) {
|
| 27 |
if (document.activeElement === first) { e.preventDefault(); last.focus(); }
|
| 28 |
} else {
|
| 29 |
if (document.activeElement === last) { e.preventDefault(); first.focus(); }
|
| 30 |
}
|
| 31 |
+
} else if (e.key === "Escape") {
|
| 32 |
+
// Close only THIS modal
|
| 33 |
toggleModal(modal, false);
|
| 34 |
}
|
| 35 |
}
|
| 36 |
|
| 37 |
+
modal.addEventListener("keydown", handler);
|
| 38 |
+
modal._focusHandler = handler;
|
| 39 |
}
|
| 40 |
|
| 41 |
function untrapFocus(modal) {
|
| 42 |
+
if (modal && modal._focusHandler) {
|
| 43 |
+
modal.removeEventListener("keydown", modal._focusHandler);
|
| 44 |
+
delete modal._focusHandler;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
}
|
| 46 |
}
|
| 47 |
|
|
|
|
| 49 |
if (!modal) return;
|
| 50 |
|
| 51 |
if (show) {
|
| 52 |
+
modal.classList.remove("modal-hidden");
|
| 53 |
+
modal.classList.add("modal-visible");
|
| 54 |
+
modal.setAttribute("aria-hidden", "false");
|
| 55 |
+
document.body.style.overflow = "hidden";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
// Ensure focus + focus trap
|
| 58 |
setTimeout(() => {
|
| 59 |
try { modal.focus(); } catch {}
|
| 60 |
trapFocus(modal);
|
| 61 |
}, 0);
|
| 62 |
} else {
|
| 63 |
+
modal.classList.remove("modal-visible");
|
| 64 |
+
modal.classList.add("modal-hidden");
|
| 65 |
+
modal.setAttribute("aria-hidden", "true");
|
| 66 |
+
document.body.style.overflow = "";
|
| 67 |
untrapFocus(modal);
|
|
|
|
|
|
|
| 68 |
}
|
| 69 |
}
|
| 70 |
|
| 71 |
function initVanta() {
|
| 72 |
+
const el = q("vanta-bg");
|
| 73 |
if (!el || !window.VANTA || !window.VANTA.NET) return null;
|
| 74 |
|
|
|
|
|
|
|
|
|
|
| 75 |
const fx = window.VANTA.NET({
|
| 76 |
+
el: "#vanta-bg",
|
| 77 |
mouseControls: true,
|
| 78 |
touchControls: true,
|
| 79 |
gyroControls: false,
|
|
|
|
| 88 |
spacing: 15.0
|
| 89 |
});
|
| 90 |
|
| 91 |
+
window.addEventListener("resize", () => {
|
| 92 |
+
try { fx.resize(); } catch {}
|
| 93 |
+
});
|
|
|
|
|
|
|
| 94 |
|
| 95 |
return fx;
|
| 96 |
}
|
| 97 |
|
| 98 |
function setupAccessModal() {
|
| 99 |
+
const accessModal = q("access-modal");
|
| 100 |
+
const accessBtn = q("access-btn");
|
| 101 |
+
const accessCta = q("access-cta");
|
| 102 |
+
const closeAccessModal = q("close-access-modal");
|
| 103 |
+
const form = q("access-form");
|
|
|
|
| 104 |
|
| 105 |
function openAccess() {
|
| 106 |
toggleModal(accessModal, true);
|
| 107 |
setTimeout(() => {
|
| 108 |
+
const name = q("name");
|
| 109 |
if (name) name.focus();
|
| 110 |
}, 50);
|
| 111 |
}
|
| 112 |
|
| 113 |
+
if (accessBtn) accessBtn.addEventListener("click", openAccess);
|
| 114 |
+
if (accessCta) accessCta.addEventListener("click", openAccess);
|
|
|
|
| 115 |
|
| 116 |
+
if (closeAccessModal) closeAccessModal.addEventListener("click", () => toggleModal(accessModal, false));
|
| 117 |
+
if (accessModal) accessModal.addEventListener("click", (e) => {
|
| 118 |
if (e.target === accessModal) toggleModal(accessModal, false);
|
| 119 |
});
|
| 120 |
|
|
|
|
| 121 |
if (form) {
|
| 122 |
+
form.addEventListener("submit", async (e) => {
|
| 123 |
e.preventDefault();
|
| 124 |
|
| 125 |
+
const name = (q("name")?.value || "").trim();
|
| 126 |
+
const email = (q("email")?.value || "").trim();
|
| 127 |
+
const institution = (q("institution")?.value || "").trim();
|
| 128 |
+
const purpose = (q("purpose")?.value || "").trim();
|
| 129 |
|
| 130 |
if (!name || !email || !institution || !purpose) {
|
| 131 |
+
alert("Please fill in all fields.");
|
| 132 |
return;
|
| 133 |
}
|
| 134 |
|
| 135 |
+
// Optional server post; safe fallback
|
| 136 |
try {
|
| 137 |
+
const res = await fetch("/api/access", {
|
| 138 |
+
method: "POST",
|
| 139 |
+
headers: { "Content-Type": "application/json" },
|
| 140 |
body: JSON.stringify({ name, email, institution, purpose, page: location.pathname })
|
| 141 |
});
|
| 142 |
+
if (!res.ok) throw new Error("Request not accepted.");
|
| 143 |
+
alert("Request received. You will be contacted after review.");
|
| 144 |
} catch {
|
| 145 |
+
alert("Request received. You will be contacted after review.");
|
| 146 |
}
|
| 147 |
|
| 148 |
+
form.reset();
|
| 149 |
toggleModal(accessModal, false);
|
| 150 |
});
|
| 151 |
}
|
|
|
|
| 154 |
}
|
| 155 |
|
| 156 |
function setupLabNavigator(dossiers, defaultKey) {
|
| 157 |
+
const labNav = q("lab-navigator");
|
| 158 |
+
const labNavBtn = q("lab-nav-btn");
|
| 159 |
+
const labNavClose = q("lab-nav-close");
|
| 160 |
+
|
| 161 |
+
const dossierTitle = q("dossier-title");
|
| 162 |
+
const dossierSubtitle = q("dossier-subtitle");
|
| 163 |
+
const dossierStatus = q("dossier-status");
|
| 164 |
+
const dossierBody = q("dossier-body");
|
| 165 |
+
const dossierEvidence = q("dossier-evidence");
|
| 166 |
+
const dossierPrimary = q("dossier-primary");
|
| 167 |
+
const dossierSecondary = q("dossier-secondary");
|
| 168 |
+
const dossierMeta = q("dossier-meta");
|
| 169 |
|
| 170 |
function openLabNav() { toggleModal(labNav, true); }
|
| 171 |
function closeLabNav() { toggleModal(labNav, false); }
|
| 172 |
|
| 173 |
+
if (labNavBtn) labNavBtn.addEventListener("click", openLabNav);
|
| 174 |
+
if (labNavClose) labNavClose.addEventListener("click", closeLabNav);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
+
if (labNav) {
|
| 177 |
+
labNav.addEventListener("click", (e) => {
|
| 178 |
+
const shouldClose = e.target && e.target.getAttribute("data-lab-close") === "true";
|
| 179 |
+
if (shouldClose) closeLabNav();
|
| 180 |
+
});
|
| 181 |
+
}
|
|
|
|
|
|
|
| 182 |
|
| 183 |
function renderDossier(key) {
|
| 184 |
const d = dossiers && dossiers[key];
|
| 185 |
if (!d) return;
|
| 186 |
|
| 187 |
+
if (dossierTitle) dossierTitle.textContent = d.title || "Lab Dossier";
|
| 188 |
+
if (dossierSubtitle) dossierSubtitle.textContent = d.subtitle || "";
|
| 189 |
+
if (dossierStatus) dossierStatus.textContent = d.status || "READY";
|
| 190 |
+
if (dossierBody) dossierBody.textContent = d.body || "";
|
| 191 |
|
| 192 |
if (dossierEvidence) {
|
| 193 |
+
dossierEvidence.innerHTML = "";
|
| 194 |
+
const items = Array.isArray(d.evidence) ? d.evidence : [];
|
| 195 |
+
if (items.length) {
|
| 196 |
+
items.forEach((item) => {
|
| 197 |
+
const li = document.createElement("li");
|
| 198 |
+
li.textContent = item;
|
| 199 |
dossierEvidence.appendChild(li);
|
| 200 |
});
|
| 201 |
} else {
|
| 202 |
+
const li = document.createElement("li");
|
| 203 |
+
li.className = "text-gray-500";
|
| 204 |
+
li.textContent = "No evidence items provided.";
|
| 205 |
dossierEvidence.appendChild(li);
|
| 206 |
}
|
| 207 |
}
|
| 208 |
|
| 209 |
if (dossierPrimary) {
|
| 210 |
+
dossierPrimary.textContent = d.primary?.label || "Open";
|
| 211 |
+
dossierPrimary.onclick = typeof d.primary?.action === "function" ? d.primary.action : null;
|
| 212 |
}
|
| 213 |
+
|
| 214 |
if (dossierSecondary) {
|
| 215 |
+
dossierSecondary.textContent = d.secondary?.label || "View Note";
|
| 216 |
+
dossierSecondary.onclick = typeof d.secondary?.action === "function" ? d.secondary.action : null;
|
| 217 |
}
|
| 218 |
|
| 219 |
if (dossierMeta) {
|
| 220 |
+
const u = d.updated || "—";
|
| 221 |
dossierMeta.innerHTML = `Last updated: <span class="text-gray-300">${u}</span>`;
|
| 222 |
}
|
| 223 |
}
|
| 224 |
|
| 225 |
+
// Bind dossier node clicks (if nodes exist on the page)
|
| 226 |
+
document.querySelectorAll(".lab-node").forEach((btn) => {
|
| 227 |
+
btn.addEventListener("click", () => {
|
| 228 |
+
const key = btn.getAttribute("data-dossier");
|
| 229 |
+
renderDossier(key);
|
| 230 |
});
|
| 231 |
});
|
| 232 |
|
| 233 |
+
// Global ESC behavior: close open modals if present
|
| 234 |
+
document.addEventListener("keydown", (e) => {
|
| 235 |
+
if (e.key !== "Escape") return;
|
| 236 |
+
const accessModal = q("access-modal");
|
| 237 |
+
if (isShown(labNav)) closeLabNav();
|
| 238 |
+
if (isShown(accessModal)) toggleModal(accessModal, false);
|
|
|
|
|
|
|
| 239 |
});
|
| 240 |
|
| 241 |
+
renderDossier(defaultKey || "start");
|
| 242 |
return { openLabNav, closeLabNav, renderDossier, labNav };
|
| 243 |
}
|
| 244 |
|
| 245 |
+
// Export public API (library only; no auto-init to avoid duplication)
|
| 246 |
+
window.SilentPattern = {
|
| 247 |
+
toggleModal,
|
| 248 |
+
initVanta,
|
| 249 |
+
setupAccessModal,
|
| 250 |
+
setupLabNavigator
|
| 251 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
})();
|