2026-06-01 09:03:59 +01:00

283 lines
9.3 KiB
JavaScript

(() => {
const roots = document.querySelectorAll(".scene-inspector-root");
if (!roots.length) {
return;
}
const slugify = (value) => value
.toLowerCase()
.replace(/&/g, "and")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
roots.forEach((root) => {
const sceneId = root.querySelector("[name='SceneID']")?.value || "new";
const storagePrefix = `plotline.inspector.${sceneId}.`;
const defaultOpen = new Set(["overview", "timing", "characters"]);
const sections = [...root.querySelectorAll(".inspector-section")];
sections.forEach((section) => {
if (section.dataset.enhanced === "true") {
return;
}
const heading = section.querySelector("h3, h2");
const title = section.dataset.sectionTitle || heading?.textContent?.trim() || "Overview";
const slug = slugify(title);
const sectionId = section.dataset.sectionId || `inspector-${slug}`;
const storageKey = sectionId.replace(/^inspector-/, "");
section.id = sectionId;
section.dataset.searchText = `${title} ${section.textContent}`.toLowerCase();
section.dataset.enhanced = "true";
const body = document.createElement("div");
body.className = "inspector-accordion-body";
while (section.firstChild) {
body.appendChild(section.firstChild);
}
const button = document.createElement("button");
button.type = "button";
button.className = "inspector-accordion-toggle";
button.setAttribute("aria-expanded", "false");
const count = section.querySelectorAll(".asset-event-item, .thread-event-item, .warning-mini-card, .dependency-card, .metric-slider").length;
button.innerHTML = `<span>${title}</span><span class="accordion-count">${count || ""}</span>`;
section.append(button, body);
const stored = localStorage.getItem(`${storagePrefix}${storageKey}`);
const isOpen = stored === null ? defaultOpen.has(storageKey) : stored === "open";
section.classList.toggle("is-open", isOpen);
button.setAttribute("aria-expanded", String(isOpen));
button.addEventListener("click", () => {
const nextOpen = !section.classList.contains("is-open");
section.classList.toggle("is-open", nextOpen);
button.setAttribute("aria-expanded", String(nextOpen));
localStorage.setItem(`${storagePrefix}${storageKey}`, nextOpen ? "open" : "closed");
});
});
root.querySelectorAll(".inspector-nav-links a").forEach((link) => {
link.addEventListener("click", (event) => {
const target = root.querySelector(link.getAttribute("href"));
if (!target) {
return;
}
event.preventDefault();
target.classList.add("is-open");
target.querySelector(".inspector-accordion-toggle")?.setAttribute("aria-expanded", "true");
target.scrollIntoView({ behavior: "smooth", block: "start" });
});
});
const search = root.querySelector("[data-inspector-search]");
search?.addEventListener("input", () => {
const query = search.value.trim().toLowerCase();
sections.forEach((section) => {
const matches = !query || section.dataset.searchText.includes(query);
section.classList.toggle("is-filtered-out", !matches);
if (matches && query) {
section.classList.add("is-open");
section.querySelector(".inspector-accordion-toggle")?.setAttribute("aria-expanded", "true");
}
});
});
if ("IntersectionObserver" in window) {
const navLinks = [...root.querySelectorAll(".inspector-nav-links a")];
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
navLinks.forEach((link) => {
link.classList.toggle("active", link.getAttribute("href") === `#${entry.target.id}`);
});
});
}, { root: root.closest(".scene-inspector-panel") || null, rootMargin: "-10% 0px -70% 0px" });
sections.forEach((section) => observer.observe(section));
}
});
})();
(() => {
const chartRoots = document.querySelectorAll("[data-metric-shape-chart]");
if (!chartRoots.length) {
return;
}
const colours = [
"#2f6f63",
"#b35f3b",
"#6750a4",
"#c58b2b",
"#7d3f52",
"#33759b",
"#9c4d8f",
"#a33d3d",
"#6f8f3d",
"#d08a2f",
"#444b57"
];
const defaultMetrics = new Set(["Overall Intensity", "Tension", "Emotional Weight", "Action"]);
const chapterGuidePlugin = {
id: "plotlineChapterGuides",
afterDraw(chart) {
const scenes = chart.config.options.plugins.plotlineScenes || [];
const { ctx, chartArea, scales } = chart;
if (!scenes.length || !scales.x) {
return;
}
ctx.save();
ctx.strokeStyle = "rgba(47, 111, 99, 0.16)";
ctx.fillStyle = "rgba(82, 75, 65, 0.58)";
ctx.lineWidth = 1;
ctx.font = "700 10px system-ui, -apple-system, Segoe UI, sans-serif";
let previousChapter = null;
scenes.forEach((scene, index) => {
if (scene.chapterLabel === previousChapter) {
return;
}
const x = scales.x.getPixelForValue(index);
ctx.beginPath();
ctx.moveTo(x, chartArea.top);
ctx.lineTo(x, chartArea.bottom);
ctx.stroke();
ctx.fillText(scene.chapterLabel, Math.min(x + 4, chartArea.right - 70), chartArea.bottom + 22);
previousChapter = scene.chapterLabel;
});
ctx.restore();
}
};
if (window.Chart) {
Chart.register(chapterGuidePlugin);
}
chartRoots.forEach((root) => {
const payloadScript = root.querySelector("script[type='application/json']");
const canvas = root.querySelector("canvas");
const toggleHost = root.querySelector("[data-metric-toggles]");
const fallback = root.querySelector("[data-chart-fallback]");
if (!payloadScript || !canvas) {
return;
}
let payload;
try {
payload = JSON.parse(payloadScript.textContent || "{}");
} catch {
fallback?.classList.remove("d-none");
return;
}
if (!window.Chart || !payload.scenes?.length || !payload.metrics?.length) {
fallback?.classList.remove("d-none");
return;
}
const labels = payload.scenes.map((scene) => scene.axisLabel);
const sceneIndexById = new Map(payload.scenes.map((scene, index) => [scene.sceneId, index]));
const datasets = payload.metrics.map((metric, metricIndex) => {
const pointMap = new Map(metric.points.map((point) => [point.sceneId, point.value]));
const colour = colours[metricIndex % colours.length];
return {
label: metric.metricName,
data: payload.scenes.map((scene) => pointMap.get(scene.sceneId) ?? null),
borderColor: colour,
backgroundColor: colour,
pointBackgroundColor: colour,
pointBorderColor: "#fff",
pointBorderWidth: 1.5,
pointRadius: 3,
pointHoverRadius: 5,
borderWidth: 2,
tension: 0.35,
spanGaps: true,
hidden: !defaultMetrics.has(metric.metricName)
};
});
toggleHost.innerHTML = "";
datasets.forEach((dataset, index) => {
const id = `metric-toggle-${Math.random().toString(36).slice(2)}-${index}`;
const label = document.createElement("label");
label.className = "metric-toggle-pill";
label.style.setProperty("--metric-colour", dataset.borderColor);
label.innerHTML = `<input type="checkbox" id="${id}" ${dataset.hidden ? "" : "checked"}><span>${dataset.label}</span>`;
toggleHost.appendChild(label);
label.querySelector("input").addEventListener("change", (event) => {
chart.setDatasetVisibility(index, event.target.checked);
chart.update();
});
});
const chart = new Chart(canvas, {
type: "line",
data: { labels, datasets },
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: "nearest", intersect: false },
scales: {
y: {
min: 1,
max: 10,
ticks: { stepSize: 1 },
grid: { color: "rgba(82, 75, 65, 0.10)" }
},
x: {
ticks: {
maxRotation: 0,
autoSkip: true,
maxTicksLimit: 16
},
grid: { display: false }
}
},
plugins: {
legend: { display: false },
plotlineScenes: payload.scenes,
tooltip: {
callbacks: {
title(items) {
const scene = payload.scenes[items[0].dataIndex];
return `${scene.chapterLabel}, Scene ${scene.sceneNumber}: ${scene.sceneTitle}`;
},
label(item) {
return `${item.dataset.label}: ${item.formattedValue}`;
},
afterBody(items) {
const scene = payload.scenes[items[0].dataIndex];
return scene.bookTitle ? scene.bookTitle : "";
}
}
}
}
}
});
canvas.addEventListener("click", (event) => {
const points = chart.getElementsAtEventForMode(event, "nearest", { intersect: true }, true);
if (!points.length) {
return;
}
const scene = payload.scenes[points[0].index];
if (scene?.timelineUrl) {
window.location.href = scene.timelineUrl;
}
});
});
})();