";
var seedTag = "<" + "script>window.__initialTheme=" + JSON.stringify(theme) + ";" + "script>";
return html.replace(openTag, openTag + seedTag);
}
function pushThemeToFrame(frame, theme) {
try {
if (frame.contentWindow && frame.contentWindow.__applyExternalTheme) {
frame.contentWindow.__applyExternalTheme(theme);
}
} catch (err) {}
}
function setShellTheme(theme) {
shellTheme = theme;
document.documentElement.setAttribute("data-theme", theme);
btnShellTheme.textContent = theme === "dark" ? "☀️" : "🌙";
btnShellTheme.setAttribute("aria-label", theme === "dark" ? "Switch to light mode" : "Switch to dark mode");
Array.prototype.forEach.call(container.querySelectorAll(".section-frame"), function (frame) {
pushThemeToFrame(frame, theme);
});
}
function watchSystemShellTheme() {
if (!window.matchMedia) return;
var query = window.matchMedia("(prefers-color-scheme: dark)");
var onChange = function (e) {
if (!shellThemeManuallySet) setShellTheme(e.matches ? "dark" : "light");
};
if (query.addEventListener) query.addEventListener("change", onChange);
else if (query.addListener) query.addListener(onChange); // Safari < 14
}
btnShellTheme.addEventListener("click", function () {
shellThemeManuallySet = true;
setShellTheme(shellTheme === "dark" ? "light" : "dark");
});
setShellTheme(shellTheme);
watchSystemShellTheme();
// Global chart accent color — same lockstep pattern as the theme toggle
// above: one picker in the shell header pushes into every section's
// iframe, and seeds new sections so they don't flash the default palette.
var shellColorWrap = document.getElementById("shellColorWrap");
var btnShellChartColor = document.getElementById("btnShellChartColor");
var shellColorPop = document.getElementById("shellColorPop");
var shellColorInput = document.getElementById("shellColorInput");
var shellColorHexInput = document.getElementById("shellColorHexInput");
var btnShellColorReset = document.getElementById("btnShellColorReset");
var shellChartAccent = null;
var HEX_COLOR_RE = /^#?([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/;
function normalizeHex(value) {
var m = HEX_COLOR_RE.exec((value || "").trim());
if (!m) return null;
var hex = m[1];
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
return "#" + hex.toLowerCase();
}
function seedInitialChartAccent(html, hex) {
var openTag = "<" + "head>";
var seedTag = "<" + "script>window.__initialChartAccent=" + JSON.stringify(hex) + ";" + "script>";
return html.replace(openTag, openTag + seedTag);
}
function pushChartAccentToFrame(frame, hex) {
try {
if (frame.contentWindow && frame.contentWindow.__applyExternalChartAccent) {
frame.contentWindow.__applyExternalChartAccent(hex);
}
} catch (err) {}
}
function setShellChartAccent(hex) {
shellChartAccent = hex || null;
var resolved = shellChartAccent || (shellTheme === "dark" ? "#3987e5" : "#2a78d6");
shellColorInput.value = resolved;
if (document.activeElement !== shellColorHexInput) shellColorHexInput.value = resolved;
shellColorHexInput.classList.remove("invalid");
btnShellColorReset.disabled = !shellChartAccent;
if (shellChartAccent) document.documentElement.style.setProperty("--shell-accent", shellChartAccent);
else document.documentElement.style.removeProperty("--shell-accent");
Array.prototype.forEach.call(container.querySelectorAll(".section-frame"), function (frame) {
pushChartAccentToFrame(frame, shellChartAccent);
});
}
btnShellChartColor.addEventListener("click", function (e) {
e.stopPropagation();
shellColorPop.hidden = !shellColorPop.hidden;
});
shellColorPop.addEventListener("click", function (e) { e.stopPropagation(); });
document.addEventListener("click", function () { shellColorPop.hidden = true; });
shellColorInput.addEventListener("input", function (e) { setShellChartAccent(e.target.value); });
shellColorHexInput.addEventListener("input", function (e) {
var normalized = normalizeHex(e.target.value);
shellColorHexInput.classList.toggle("invalid", !normalized);
if (normalized) setShellChartAccent(normalized);
});
shellColorHexInput.addEventListener("blur", function () {
shellColorHexInput.value = shellChartAccent || shellColorInput.value;
shellColorHexInput.classList.remove("invalid");
});
btnShellColorReset.addEventListener("click", function () {
setShellChartAccent(null);
shellColorPop.hidden = true;
});
function addSection(title) {
count += 1;
var n = count;
var card = document.createElement("div");
card.className = "section-card";
var header = document.createElement("div");
header.className = "section-card-header";
var toggleBtn = document.createElement("button");
toggleBtn.className = "section-toggle";
toggleBtn.type = "button";
toggleBtn.textContent = "▾";
toggleBtn.setAttribute("aria-label", "Collapse section");
toggleBtn.addEventListener("click", function () {
var collapsed = card.classList.toggle("collapsed");
toggleBtn.textContent = collapsed ? "▸" : "▾";
toggleBtn.setAttribute("aria-label", collapsed ? "Expand section" : "Collapse section");
});
var badge = document.createElement("div");
badge.className = "section-index";
badge.textContent = String(n);
var titleInput = document.createElement("input");
titleInput.className = "section-title-input";
titleInput.type = "text";
titleInput.value = title || ("Data " + n);
titleInput.setAttribute("aria-label", "Section name");
header.appendChild(toggleBtn);
header.appendChild(badge);
header.appendChild(titleInput);
if (n > 1) {
var removeBtn = document.createElement("button");
removeBtn.className = "section-btn danger";
removeBtn.type = "button";
removeBtn.textContent = "Remove section";
removeBtn.addEventListener("click", function () {
if (!confirm('Remove "' + titleInput.value + '" and its charts? This cannot be undone.')) return;
card.remove();
renumberSections();
});
header.appendChild(removeBtn);
}
var frame = document.createElement("iframe");
frame.className = "section-frame";
frame.setAttribute("title", (title || ("Data " + n)) + " dashboard");
frame.srcdoc = seedInitialChartAccent(seedInitialTheme(templateHtml, shellTheme), shellChartAccent);
card.dataset.span = String(GRID_COLS);
var handleE = document.createElement("div");
handleE.className = "section-resize-e";
handleE.setAttribute("aria-hidden", "true");
var handleS = document.createElement("div");
handleS.className = "section-resize-s";
handleS.setAttribute("aria-hidden", "true");
var handleSE = document.createElement("div");
handleSE.className = "section-resize-se";
handleSE.setAttribute("aria-hidden", "true");
wireSectionResize(card, frame, { e: handleE, s: handleS, se: handleSE });
card.appendChild(header);
card.appendChild(frame);
card.appendChild(handleE);
card.appendChild(handleS);
card.appendChild(handleSE);
container.appendChild(card);
renumberSections();
}
// Sections live on a 12-column grid (like Bootstrap's col-lg-N), so
// dragging a card's right edge to half width leaves the other half of
// the row free for the next section to sit alongside it.
var GRID_COLS = 12;
var MIN_SPAN = 3;
function computeColStep() {
var gap = parseFloat(getComputedStyle(container).columnGap) || 16;
var colWidth = (container.clientWidth - gap * (GRID_COLS - 1)) / GRID_COLS;
return { gap: gap, colWidth: colWidth };
}
function setSpan(card, span) {
span = Math.max(MIN_SPAN, Math.min(GRID_COLS, Math.round(span)));
card.dataset.span = String(span);
card.style.gridColumn = "span " + span;
}
function setFramesInert(inert) {
Array.prototype.forEach.call(container.querySelectorAll(".section-frame"), function (f) {
f.style.pointerEvents = inert ? "none" : "";
});
}
// Custom pointer-driven drag on the card's edges/corner, mirroring the
// chart resize handles inside each dashboard. A plain CSS `resize` on the
// iframe works but loses pointer events to the iframe's own document
// mid-drag, so pointer capture + disabling pointer-events on every
// section's iframe while dragging keeps the resize smooth over embedded
// pages (including neighboring sections the cursor passes over).
function wireSectionResize(card, frame, handles) {
var startX, startY, startWidthPx, startHeight, dir = null, resizing = false;
function begin(e, d) {
resizing = true;
dir = d;
startX = e.clientX; startY = e.clientY;
startWidthPx = card.getBoundingClientRect().width;
startHeight = frame.getBoundingClientRect().height;
setFramesInert(true);
card.classList.add("resizing");
e.currentTarget.setPointerCapture(e.pointerId);
}
function move(e) {
if (!resizing) return;
if (dir === "e" || dir === "se") {
var step = computeColStep();
var dx = e.clientX - startX;
setSpan(card, (startWidthPx + dx + step.gap) / (step.colWidth + step.gap));
}
if (dir === "s" || dir === "se") {
var newHeight = Math.max(240, startHeight + (e.clientY - startY));
frame.style.height = newHeight + "px";
}
}
function end(e) {
if (!resizing) return;
resizing = false;
dir = null;
setFramesInert(false);
card.classList.remove("resizing");
try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (err) {}
}
[["e", handles.e], ["s", handles.s], ["se", handles.se]].forEach(function (pair) {
var d = pair[0], h = pair[1];
h.addEventListener("pointerdown", function (e) { begin(e, d); });
h.addEventListener("pointermove", move);
h.addEventListener("pointerup", end);
h.addEventListener("pointercancel", end);
});
}
function renumberSections() {
Array.prototype.forEach.call(container.children, function (card, i) {
var badge = card.querySelector(".section-index");
if (badge) badge.textContent = String(i + 1);
});
}
document.getElementById("btnAddSection").addEventListener("click", function () { addSection(); });
addSection("Data 1");
})();