536 lines
23 KiB
JavaScript
536 lines
23 KiB
JavaScript
(() => {
|
|
const $ = (sel, root = document) => root.querySelector(sel);
|
|
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
|
const toast = $("#miniToast");
|
|
const showToast = (message) => {
|
|
if (!toast) return;
|
|
toast.textContent = message;
|
|
toast.classList.add("show");
|
|
window.clearTimeout(showToast.t);
|
|
showToast.t = window.setTimeout(() => toast.classList.remove("show"), 2400);
|
|
};
|
|
|
|
const bottomNav = $("[data-bottom-nav]");
|
|
const syncNav = () => bottomNav?.classList.toggle("visible", window.scrollY > 8);
|
|
window.addEventListener("scroll", syncNav, { passive: true });
|
|
syncNav();
|
|
|
|
$$("[data-section-jump], .bottom-icons a, .top-links a, .scroll-cue, .chapter-link").forEach((link) => {
|
|
link.addEventListener("click", () => {
|
|
const label = link.getAttribute("data-title") || link.textContent.trim() || "section";
|
|
showToast(`Navigating to ${label}`);
|
|
});
|
|
});
|
|
|
|
const themeToggle = $("[data-theme-toggle]");
|
|
const storedTheme = localStorage.getItem("landscaper-theme");
|
|
if (storedTheme === "night") {
|
|
document.documentElement.dataset.theme = "night";
|
|
if (themeToggle) {
|
|
themeToggle.textContent = "Night";
|
|
themeToggle.setAttribute("aria-pressed", "true");
|
|
}
|
|
}
|
|
themeToggle?.addEventListener("click", () => {
|
|
const next = document.documentElement.dataset.theme === "night" ? "day" : "night";
|
|
if (next === "night") document.documentElement.dataset.theme = "night";
|
|
else delete document.documentElement.dataset.theme;
|
|
themeToggle.textContent = next === "night" ? "Night" : "Day";
|
|
themeToggle.setAttribute("aria-pressed", String(next === "night"));
|
|
localStorage.setItem("landscaper-theme", next === "night" ? "night" : "day");
|
|
showToast(`${next === "night" ? "Night" : "Day"} mode enabled`);
|
|
});
|
|
|
|
const grid = $("[data-section-grid]");
|
|
$$(".mode-switch button").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
$$(".mode-switch button").forEach((b) => b.classList.remove("active"));
|
|
button.classList.add("active");
|
|
grid?.classList.remove("blocks", "files", "text");
|
|
grid?.classList.add(button.dataset.mode);
|
|
showToast(`${button.dataset.mode} mode`);
|
|
});
|
|
});
|
|
|
|
const pdfLibrary = $("[data-pdf-library]");
|
|
$$("[data-pdf-view]").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
$$("[data-pdf-view]").forEach((b) => b.classList.remove("active"));
|
|
button.classList.add("active");
|
|
if (pdfLibrary) pdfLibrary.dataset.pdfMode = button.dataset.pdfView || "intro";
|
|
showToast(`PDF view: ${button.textContent.trim()}`);
|
|
});
|
|
});
|
|
|
|
const referencePage = $("[data-reference-page]");
|
|
const referenceModeButtons = $$("button[data-reference-mode]");
|
|
const referencePanels = $$("[data-mode-panel]");
|
|
const filter = $("#linkFilter");
|
|
const filterDimToggle = $("#filterDimToggle");
|
|
const filterStatus = $("#filterStatus");
|
|
const filterModeLabel = $("[data-filter-mode-label]");
|
|
|
|
try {
|
|
const savedFilterMode = localStorage.getItem("curated-filter-mode");
|
|
if (filterDimToggle && savedFilterMode === "hide") filterDimToggle.checked = false;
|
|
} catch {}
|
|
|
|
function setReferenceMode(mode, options = {}) {
|
|
if (!referencePage || !mode) return;
|
|
referencePage.dataset.referenceMode = mode;
|
|
referenceModeButtons.forEach((button) => {
|
|
const active = button.dataset.referenceMode === mode;
|
|
button.classList.toggle("active", active);
|
|
button.setAttribute("aria-selected", String(active));
|
|
});
|
|
referencePanels.forEach((panel) => {
|
|
const active = panel.dataset.modePanel === mode;
|
|
panel.hidden = !active;
|
|
panel.classList.toggle("is-active", active);
|
|
});
|
|
if (!options.quiet) showToast(mode === "pdfs" ? "PDF block ready" : (mode === "chapters" ? "Chapter groups ready" : "Icon boxes ready"));
|
|
scheduleVisibleHighlights();
|
|
}
|
|
|
|
function panelForMode(mode) {
|
|
return referencePanels.find((panel) => panel.dataset.modePanel === mode) || null;
|
|
}
|
|
|
|
referenceModeButtons.forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const mode = button.dataset.referenceMode;
|
|
setReferenceMode(mode, { quiet: true });
|
|
const panel = panelForMode(mode);
|
|
if (panel?.id) window.setTimeout(() => markTarget(panel.id, { scroll: false, switchMode: false }), 30);
|
|
});
|
|
});
|
|
|
|
$$("[data-reference-mode-jump]").forEach((link) => {
|
|
link.addEventListener("click", () => setReferenceMode(link.dataset.referenceModeJump, { quiet: true }));
|
|
});
|
|
|
|
document.addEventListener("click", (event) => {
|
|
const control = event.target.closest("[data-highlight-target]");
|
|
if (!control) return;
|
|
const targetId = control.dataset.highlightTarget;
|
|
if (!targetId) return;
|
|
const href = control.getAttribute("href") || "";
|
|
const external = control.matches("a[href]") && href && !href.startsWith("#");
|
|
if (!external) event.preventDefault();
|
|
markTarget(targetId, { scroll: !external, switchMode: true });
|
|
});
|
|
|
|
document.addEventListener("click", (event) => {
|
|
const link = event.target.closest("a[href^=\"#\"]");
|
|
if (!link || link.matches("[data-highlight-target]") || link.matches("[data-map-target]")) return;
|
|
const raw = link.getAttribute("href") || "";
|
|
if (raw.length <= 1) return;
|
|
const targetId = decodeURIComponent(raw.slice(1));
|
|
if (!document.getElementById(targetId)) return;
|
|
event.preventDefault();
|
|
if (link.dataset.referenceModeJump) setReferenceMode(link.dataset.referenceModeJump, { quiet: true });
|
|
window.setTimeout(() => markTarget(targetId, { scroll: true, switchMode: true }), 30);
|
|
});
|
|
|
|
function previewOwner(control, persist = false) {
|
|
const targetId = control?.dataset?.ownerTarget;
|
|
if (!targetId) return;
|
|
$$(".is-owner-preview").forEach((el) => el.classList.remove("is-owner-preview"));
|
|
$$(".is-owner-link").forEach((el) => el.classList.remove("is-owner-link"));
|
|
control.classList.add("is-owner-link");
|
|
markRelatedControls(targetId, true);
|
|
const target = document.getElementById(targetId);
|
|
const hiddenPanel = target?.closest("[data-mode-panel]")?.hidden;
|
|
if (target && !hiddenPanel) target.classList.add("is-owner-preview");
|
|
if (persist && target && !hiddenPanel) markTarget(targetId, { scroll: false, switchMode: false, toast: false });
|
|
}
|
|
|
|
document.addEventListener("mouseover", (event) => {
|
|
const control = event.target.closest("[data-owner-target]");
|
|
if (control) previewOwner(control, false);
|
|
});
|
|
document.addEventListener("focusin", (event) => {
|
|
const control = event.target.closest("[data-owner-target]");
|
|
if (control) previewOwner(control, false);
|
|
});
|
|
document.addEventListener("click", (event) => {
|
|
const control = event.target.closest("[data-owner-target]");
|
|
if (control) previewOwner(control, true);
|
|
});
|
|
|
|
const applyFilter = () => {
|
|
const anchorState = captureScrollAnchor();
|
|
const q = filter?.value.trim().toLowerCase() || "";
|
|
const dimOut = filterDimToggle ? filterDimToggle.checked : true;
|
|
let total = 0;
|
|
let hits = 0;
|
|
|
|
const setFiltered = (el, hit) => {
|
|
const miss = Boolean(q && !hit);
|
|
el.classList.toggle("is-filter-muted", miss && dimOut);
|
|
el.classList.toggle("is-hidden", miss && !dimOut);
|
|
el.classList.toggle("is-filter-hit", Boolean(q && hit));
|
|
};
|
|
|
|
$$("[data-pdf-card]").forEach((card) => {
|
|
total += 1;
|
|
const hit = !q || (card.dataset.search || "").includes(q);
|
|
if (hit) hits += 1;
|
|
setFiltered(card, hit);
|
|
});
|
|
|
|
$$("[data-section-card]").forEach((card) => {
|
|
const sectionMatch = !q || (card.dataset.title || "").includes(q) || (card.dataset.chapter || "").toLowerCase().includes(q);
|
|
let any = sectionMatch;
|
|
$$("[data-link-row]", card).forEach((row) => {
|
|
total += 1;
|
|
const hit = !q || (row.dataset.search || "").includes(q) || sectionMatch;
|
|
if (hit) hits += 1;
|
|
if (hit) any = true;
|
|
setFiltered(row, hit);
|
|
});
|
|
setFiltered(card, any);
|
|
});
|
|
|
|
if (filterModeLabel) filterModeLabel.textContent = dimOut ? "dim" : "hide";
|
|
try { localStorage.setItem("curated-filter-mode", dimOut ? "dim" : "hide"); } catch {}
|
|
if (filterStatus) filterStatus.textContent = q ? `${hits}/${total} · ${dimOut ? "dim" : "hide"}` : `All · ${dimOut ? "dim" : "hide"}`;
|
|
restoreScrollAnchor(anchorState);
|
|
scheduleVisibleHighlights();
|
|
};
|
|
|
|
filter?.addEventListener("input", applyFilter);
|
|
filterDimToggle?.addEventListener("change", applyFilter);
|
|
applyFilter();
|
|
|
|
const mapBtn = $("[data-open-map]");
|
|
mapBtn?.addEventListener("click", () => {
|
|
const modalElement = $("#mapModal");
|
|
if (!modalElement || !window.bootstrap) return;
|
|
const modal = new bootstrap.Modal(modalElement);
|
|
modal.show();
|
|
});
|
|
|
|
$$("[data-map-target]").forEach((link) => {
|
|
link.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
const targetId = link.getAttribute("data-map-target");
|
|
const modalEl = $("#mapModal");
|
|
if (modalEl && window.bootstrap) bootstrap.Modal.getInstance(modalEl)?.hide();
|
|
window.setTimeout(() => openSection(targetId), 170);
|
|
});
|
|
});
|
|
|
|
const scanBtn = $("#scanLinks");
|
|
const auditStatus = $("#auditStatus");
|
|
const auditResults = $("#auditResults");
|
|
scanBtn?.addEventListener("click", async () => {
|
|
scanBtn.disabled = true;
|
|
auditStatus.textContent = "Scanning live source…";
|
|
auditResults.innerHTML = "";
|
|
try {
|
|
const response = await fetch("api/scan_links.php", { headers: { "Accept": "application/json" }});
|
|
if (!response.ok) throw new Error("Scanner returned " + response.status);
|
|
const payload = await response.json();
|
|
const known = new Set(JSON.parse(auditResults.dataset.known || "[]").map((u) => normalize(u)));
|
|
const rows = (payload.links || []).map((link) => ({ ...link, known: known.has(normalize(link.href)) }));
|
|
const newCount = rows.filter((row) => !row.known).length;
|
|
const ignored = Number(payload.ignored_count || 0);
|
|
const suppressed = Number(payload.suppressed_count || 0);
|
|
auditStatus.textContent = `${rows.length} visible source links found · ${newCount} not curated · ${ignored} noise/asset links ignored · ${suppressed} deliberately removed · scanned ${payload.scanned_at || "now"}.`;
|
|
auditResults.innerHTML = rows.map(renderAuditRow).join("");
|
|
showToast("Live scan complete");
|
|
} catch (error) {
|
|
auditStatus.textContent = "Scan failed. The curated structure remains available.";
|
|
showToast("Scanner unavailable");
|
|
} finally {
|
|
scanBtn.disabled = false;
|
|
}
|
|
});
|
|
|
|
const structureButtons = [$("#loadStructure"), $("#loadStructureBottom")].filter(Boolean);
|
|
const structureStatus = $("#sourceStructureStatus");
|
|
const structureTarget = $("#sourceStructure");
|
|
structureButtons.forEach((button) => {
|
|
button.addEventListener("click", () => loadSourceStructure());
|
|
});
|
|
|
|
async function loadSourceStructure() {
|
|
if (!structureTarget || !structureStatus) return;
|
|
structureButtons.forEach((button) => { button.disabled = true; });
|
|
structureStatus.textContent = "Reading live source icon ownership…";
|
|
structureTarget.innerHTML = "";
|
|
try {
|
|
const response = await fetch("api/scan_structure.php", { headers: { "Accept": "application/json" }});
|
|
if (!response.ok) throw new Error("Structure scanner returned " + response.status);
|
|
const payload = await response.json();
|
|
if (!payload.success) throw new Error(payload.error || "Structure scanner failed");
|
|
structureStatus.textContent = `${payload.section_count} source owner icons · ${payload.link_count} grouped branch links · ${Number(payload.utility_count || 0)} separated utility links · scanned ${payload.scanned_at}.`;
|
|
structureTarget.innerHTML = `
|
|
<div class="shadow-bonus-live">
|
|
<strong>Bonus shadow icon</strong>
|
|
${(payload.shadow_bonus || []).map((item) => `<a href="${escapeAttr(item.href)}" target="_blank" rel="noopener">${escapeHtml(item.icon)} ${escapeHtml(item.title)}</a>`).join("")}
|
|
</div>
|
|
${renderUtilityChrome(payload.utility_chrome || [])}
|
|
${(payload.sections || []).map(renderSourceSection).join("")}
|
|
`;
|
|
showToast("Source ownership loaded");
|
|
} catch (error) {
|
|
structureStatus.textContent = "Could not load live structure. The curated snapshot above remains available.";
|
|
showToast("Source map unavailable");
|
|
scheduleVisibleHighlights();
|
|
} finally {
|
|
structureButtons.forEach((button) => { button.disabled = false; });
|
|
}
|
|
}
|
|
|
|
function renderAuditRow(row) {
|
|
const badgeText = row.known ? "curated" : (row.safety === "review" ? "review external" : (row.category || "new"));
|
|
const badgeClass = row.known ? "audit-badge-known" : (row.safety === "review" ? "audit-badge-review" : (row.safety === "source" ? "audit-badge-source" : "audit-badge-new"));
|
|
const category = String(row.category || "external").replace(/[^a-z0-9-]/gi, "-").toLowerCase();
|
|
return `
|
|
<div class="audit-row link-kind-${escapeAttr(category)}">
|
|
<div><a href="${escapeAttr(row.href)}" target="_blank" rel="noopener nofollow">${escapeHtml(row.text || row.href)}</a><small>${escapeHtml(row.category_label || row.category || "link")} · ${escapeHtml(row.host || "")} · ${escapeHtml(row.href)}</small></div>
|
|
<span class="badge-soft ${badgeClass}">${escapeHtml(badgeText)}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
|
|
function renderUtilityChrome(items) {
|
|
if (!items.length) return "";
|
|
return `
|
|
<div class="shadow-bonus-live utility-live">
|
|
<strong>Separated utility chrome</strong>
|
|
${items.slice(0, 18).map((item) => `<a href="${escapeAttr(item.href)}" target="_blank" rel="noopener">${escapeHtml(item.text || item.href)}</a>`).join("")}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderSourceSection(section) {
|
|
return `
|
|
<article class="source-owner-card" id="live-${escapeAttr(section.id)}" data-live-owner="${escapeAttr(section.id)}">
|
|
<div class="source-owner-head">
|
|
<span class="source-owner-icon">${escapeHtml(section.icon)}</span>
|
|
<div>
|
|
<h3>${escapeHtml(section.title)}</h3>
|
|
<small>${escapeHtml(section.source_anchor)} owns ${Number(section.link_count || 0)} links until ${escapeHtml(section.owns_until || "next")}</small>
|
|
</div>
|
|
</div>
|
|
<p>${escapeHtml(section.excerpt || "")}</p>
|
|
<button class="manual-highlight" type="button" data-highlight-target="live-${escapeAttr(section.id)}">✨ Pin live box</button>
|
|
${section.layout ? `<div class="layout-note"><b>${escapeHtml(section.layout.zone || "layout")}</b><span>${escapeHtml(section.layout.computed || "")}</span><small>${escapeHtml(section.layout.parent || "")} · ${escapeHtml(section.layout.position || "")}</small><em>${escapeHtml(section.layout.note || "")}</em></div>` : ""}
|
|
<div class="source-owner-links">
|
|
${(section.links || []).map((link) => {
|
|
const category = String(link.category || "external").replace(/[^a-z0-9-]/gi, "-").toLowerCase();
|
|
const context = [link.category_label || category, link.context || "", link.layout_hint || "", link.host || ""].filter(Boolean).join(" · ");
|
|
return `<a class="source-link link-kind-${escapeAttr(category)}" href="${escapeAttr(link.href)}" target="_blank" rel="noopener" data-owner-target="live-${escapeAttr(section.id)}"><span>${escapeHtml(link.text || link.href)}</span><small>${escapeHtml(context)}</small></a>`;
|
|
}).join("") || `<span class="empty-state">No visible links inside this owner in the current source scan.</span>`}
|
|
</div>
|
|
</article>
|
|
`;
|
|
}
|
|
|
|
const highlightableTargetSelector = [
|
|
"[data-section-card][id]",
|
|
".section-card[id]",
|
|
".chapter-card[id]",
|
|
".chapter-reference-card[id]",
|
|
".layout-zone-card[id]",
|
|
".layout-audit[id]",
|
|
".ownership-strip[id]",
|
|
".reference-tools[id]",
|
|
".reference-hero[id]",
|
|
".pdf-library[id]",
|
|
".reference-mode-panel[id]",
|
|
".utility-chrome-panel[id]",
|
|
".source-owner-card[id]",
|
|
".audit-panel[id]",
|
|
".document-shell[id]",
|
|
".console-screen[id]"
|
|
].join(",");
|
|
const boxClickTargetSelector = [
|
|
"[data-section-card][id]",
|
|
".section-card[id]",
|
|
".chapter-card[id]",
|
|
".chapter-reference-card[id]",
|
|
".layout-zone-card[id]",
|
|
".layout-audit[id]",
|
|
".ownership-strip[id]",
|
|
".reference-tools[id]",
|
|
".reference-hero[id]",
|
|
".pdf-library[id]",
|
|
".reference-mode-panel[id]",
|
|
".utility-chrome-panel[id]",
|
|
".source-owner-card[id]",
|
|
".audit-panel[id]"
|
|
].join(",");
|
|
const targetControlSelector = "[data-highlight-target],[data-owner-target],[data-map-target],a[href^='#']";
|
|
const visibleTargetSelector = highlightableTargetSelector;
|
|
const interactiveSelector = "a,button,input,select,textarea,label,summary,[role='button'],[tabindex]:not([tabindex='-1'])";
|
|
|
|
function controlTargetId(control) {
|
|
if (!control) return "";
|
|
const direct = control.dataset?.highlightTarget || control.dataset?.ownerTarget || control.dataset?.mapTarget || "";
|
|
if (direct) return direct;
|
|
const href = control.getAttribute?.("href") || "";
|
|
if (!href.startsWith("#") || href.length <= 1) return "";
|
|
try { return decodeURIComponent(href.slice(1)); }
|
|
catch { return href.slice(1); }
|
|
}
|
|
|
|
function isElementHidden(el) {
|
|
if (!el || !el.isConnected || el.hidden || el.closest("[hidden]")) return true;
|
|
const style = window.getComputedStyle(el);
|
|
return style.display === "none" || style.visibility === "hidden" || Number(style.opacity || 1) === 0;
|
|
}
|
|
|
|
function visiblePixels(el) {
|
|
const rect = el.getBoundingClientRect();
|
|
const width = Math.max(0, Math.min(rect.right, window.innerWidth) - Math.max(rect.left, 0));
|
|
const height = Math.max(0, Math.min(rect.bottom, window.innerHeight) - Math.max(rect.top, 0));
|
|
return { width, height, area: width * height };
|
|
}
|
|
|
|
function isMeaningfullyVisible(el) {
|
|
if (!el?.id || isElementHidden(el)) return false;
|
|
const visible = visiblePixels(el);
|
|
return visible.width >= 20 && visible.height >= 20 && visible.area >= 400;
|
|
}
|
|
|
|
function visibleTargetIds() {
|
|
const ids = new Set();
|
|
$$(visibleTargetSelector).forEach((target) => {
|
|
if (isMeaningfullyVisible(target)) ids.add(target.id);
|
|
});
|
|
return ids;
|
|
}
|
|
|
|
function syncVisibleHighlights() {
|
|
const visibleIds = visibleTargetIds();
|
|
$$(".is-in-view-target").forEach((el) => el.classList.remove("is-in-view-target"));
|
|
$$(".is-visible-owner-link").forEach((el) => el.classList.remove("is-visible-owner-link"));
|
|
|
|
visibleIds.forEach((id) => {
|
|
document.getElementById(id)?.classList.add("is-in-view-target");
|
|
});
|
|
|
|
$$(targetControlSelector).forEach((control) => {
|
|
const targetId = controlTargetId(control);
|
|
control.classList.toggle("is-visible-owner-link", Boolean(targetId && visibleIds.has(targetId)));
|
|
});
|
|
}
|
|
|
|
function scheduleVisibleHighlights() {
|
|
window.cancelAnimationFrame(scheduleVisibleHighlights.raf || 0);
|
|
scheduleVisibleHighlights.raf = window.requestAnimationFrame(syncVisibleHighlights);
|
|
}
|
|
|
|
window.addEventListener("scroll", scheduleVisibleHighlights, { passive: true });
|
|
window.addEventListener("resize", scheduleVisibleHighlights, { passive: true });
|
|
window.addEventListener("hashchange", scheduleVisibleHighlights);
|
|
|
|
document.addEventListener("click", (event) => {
|
|
const box = event.target.closest?.(boxClickTargetSelector);
|
|
if (!box?.id) return;
|
|
const interactive = event.target.closest(interactiveSelector);
|
|
if (interactive && box.contains(interactive)) {
|
|
const explicitTarget = controlTargetId(interactive);
|
|
if (explicitTarget || !interactive.matches("a[href]")) return;
|
|
}
|
|
markTarget(box.id, { scroll: false, switchMode: false, toast: !interactive });
|
|
});
|
|
|
|
function openSection(id) {
|
|
markTarget(id, { scroll: true, switchMode: true });
|
|
}
|
|
|
|
function clearTargetHighlights() {
|
|
const activeSelector = [
|
|
".section-card.is-active",
|
|
".chapter-reference-card.is-active",
|
|
".pdf-card.is-active",
|
|
".layout-zone-card.is-active",
|
|
".layout-audit.is-active",
|
|
".ownership-strip.is-active",
|
|
".reference-tools.is-active",
|
|
".utility-chrome-panel.is-active",
|
|
".source-owner-card.is-active",
|
|
".audit-panel.is-active",
|
|
".reference-hero.is-active",
|
|
".chapter-card.is-active",
|
|
".document-shell.is-active",
|
|
".console-screen.is-active",
|
|
".pdf-library.is-active"
|
|
].join(",");
|
|
$$(activeSelector).forEach((el) => el.classList.remove("is-active"));
|
|
$$(".is-focus-target,.is-owner-preview,.is-owner-link").forEach((el) => el.classList.remove("is-focus-target", "is-owner-preview", "is-owner-link"));
|
|
$$("[aria-current=\"location\"]").forEach((el) => el.removeAttribute("aria-current"));
|
|
}
|
|
|
|
function markRelatedControls(targetId, lightweight = false) {
|
|
$$(targetControlSelector).forEach((el) => {
|
|
const match = controlTargetId(el) === targetId;
|
|
el.classList.toggle("is-owner-link", match);
|
|
if (match && !lightweight) el.setAttribute("aria-current", "location");
|
|
});
|
|
}
|
|
|
|
function markTarget(id, options = {}) {
|
|
const target = id ? document.getElementById(id) : null;
|
|
if (!target) return;
|
|
const modePanel = target.closest("[data-mode-panel]");
|
|
if (modePanel && modePanel.hidden && options.switchMode !== false) setReferenceMode(modePanel.dataset.modePanel || "icons", { quiet: true });
|
|
clearTargetHighlights();
|
|
target.classList.add("is-focus-target");
|
|
if (!target.classList.contains("reference-mode-panel")) target.classList.add("is-active");
|
|
markRelatedControls(id, false);
|
|
if (options.scroll !== false) {
|
|
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
if (history.replaceState) history.replaceState(null, "", `#${id}`);
|
|
}
|
|
window.clearTimeout(markTarget.pulseTimer);
|
|
markTarget.pulseTimer = window.setTimeout(() => target.classList.remove("is-focus-target"), 4200);
|
|
scheduleVisibleHighlights();
|
|
window.setTimeout(scheduleVisibleHighlights, options.scroll !== false ? 520 : 40);
|
|
if (options.toast !== false) showToast(`Pinned ${target.querySelector("h1,h2,h3")?.textContent?.trim() || id}`);
|
|
}
|
|
|
|
function captureScrollAnchor() {
|
|
const y = Math.min(Math.max(120, window.innerHeight * 0.25), window.innerHeight - 1);
|
|
const el = document.elementFromPoint(Math.min(90, window.innerWidth - 1), y);
|
|
return el ? { el, top: el.getBoundingClientRect().top } : null;
|
|
}
|
|
|
|
function restoreScrollAnchor(state) {
|
|
if (!state || !state.el || !state.el.isConnected) return;
|
|
window.requestAnimationFrame(() => {
|
|
const diff = state.el.getBoundingClientRect().top - state.top;
|
|
if (Math.abs(diff) > 1) window.scrollBy(0, diff);
|
|
});
|
|
}
|
|
|
|
|
|
scheduleVisibleHighlights();
|
|
|
|
if (window.location.hash && document.getElementById(window.location.hash.slice(1))) {
|
|
window.setTimeout(() => openSection(window.location.hash.slice(1)), 250);
|
|
}
|
|
|
|
function normalize(url) {
|
|
try { return new URL(url, "https://spireason.neocities.org/").href.replace(/#$/, ""); }
|
|
catch { return String(url || "").trim(); }
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value || "").replace(/[&<>"\x27]/g, (char) => {
|
|
if (char === "&") return "&";
|
|
if (char === "<") return "<";
|
|
if (char === ">") return ">";
|
|
if (char.charCodeAt(0) === 34) return """;
|
|
return "'";
|
|
});
|
|
}
|
|
function escapeAttr(value) { return escapeHtml(value); }
|
|
})();
|