40247-vm/assets/js/main.js
2026-06-12 09:15:36 +00:00

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 "&amp;";
if (char === "<") return "&lt;";
if (char === ">") return "&gt;";
if (char.charCodeAt(0) === 34) return "&quot;";
return "&#39;";
});
}
function escapeAttr(value) { return escapeHtml(value); }
})();