(() => { 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 = `${title}${count || ""}`; 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 = `${dataset.label}`; 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; } }); }); })();