svrnty-hermes-webui-plugin/static/svrnty_nav.js
2026-05-24 12:55:35 -04:00

140 lines
5.1 KiB
JavaScript

// svrnty_nav.js — injects Adwright + BTE sidebar buttons into hermes-webui's
// .sidebar-nav and wraps switchPanel so our panels participate in the existing
// main-view show/hide system (showing-<name> on <main>).
//
// Each panel module (adwright.js, bte.js) mounts its content inside <main>
// and CSS keys visibility off main.showing-adwright / main.showing-bte.
//
// IIFE, idempotent — guarded by window.__svrntyNavLoaded.
(function () {
"use strict";
if (window.__svrntyNavLoaded) return;
window.__svrntyNavLoaded = true;
const LOG = (...a) => console.log("[svrnty-nav]", ...a);
const ERR = (...a) => console.error("[svrnty-nav]", ...a);
LOG("loaded", { readyState: document.readyState });
const TABS = [
{
id: "adwright",
label: "Adwright",
tooltip: "Adwright — marketing intelligence",
// Bullseye / target icon — marketing focus
svg: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>',
},
{
id: "bte",
label: "BTE",
tooltip: "BTE — brand creative studio",
// Palette/sparkle icon — creative
svg: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2l1.8 5.6L19.4 9.4l-4.5 3.3 1.7 5.7L12 15l-4.6 3.4 1.7-5.7L4.6 9.4l5.6-1.8L12 2z"/></svg>',
},
];
function _injectButtons() {
const nav = document.querySelector(".sidebar-nav");
if (!nav) {
LOG("_injectButtons: .sidebar-nav not found yet");
return false;
}
let added = 0;
TABS.forEach((t) => {
if (nav.querySelector('[data-panel="' + t.id + '"]')) return;
try {
const btn = document.createElement("button");
btn.className = "nav-tab has-tooltip has-tooltip--bottom svrnty-nav-tab";
btn.setAttribute("data-panel", t.id);
btn.setAttribute("data-label", t.label);
btn.setAttribute("data-tooltip", t.tooltip);
btn.setAttribute("aria-label", t.label);
btn.innerHTML = t.svg;
btn.addEventListener("click", () => {
LOG("button clicked:", t.id);
if (typeof window.switchPanel === "function") {
window.switchPanel(t.id, { fromRailClick: true });
} else {
ERR("switchPanel is not a function — cannot route click");
}
});
nav.appendChild(btn);
added++;
} catch (e) {
ERR("failed to inject button", t.id, e);
}
});
LOG("_injectButtons: added", added, "of", TABS.length, "(nav children:", nav.children.length, ")");
return true;
}
// Wrap switchPanel to add showing-<id> class on <main> for our IDs.
// We chain the original so all upstream behavior (collapse rail, hide other
// panels, toggle data-panel active) keeps working unchanged.
function _wrapSwitchPanel() {
if (typeof window.switchPanel !== "function") return false;
if (window.switchPanel.__svrntyWrapped) return true;
const original = window.switchPanel;
const OUR_IDS = TABS.map((t) => t.id);
async function wrapped(name, opts) {
const result = await original(name, opts);
const main = document.querySelector("main.main");
if (main) {
OUR_IDS.forEach((id) => {
main.classList.toggle("svrnty-showing-" + id, name === id);
});
}
// Notify panel modules so they can lazy-init/refresh.
try {
window.dispatchEvent(
new CustomEvent("svrnty:panel-switch", { detail: { name } }),
);
} catch (_) {}
return result;
}
wrapped.__svrntyWrapped = true;
window.switchPanel = wrapped;
return true;
}
let _initAttempts = 0;
function _init() {
_initAttempts++;
const buttonsOk = _injectButtons();
const wrapOk = _wrapSwitchPanel();
if (!buttonsOk || !wrapOk) {
if (_initAttempts < 100) {
requestAnimationFrame(_init);
} else {
ERR("_init gave up after", _initAttempts, "attempts. buttonsOk=", buttonsOk, "wrapOk=", wrapOk);
}
return;
}
LOG("_init completed in", _initAttempts, "attempt(s)");
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", _init);
} else {
_init();
}
// Re-inject buttons if something re-renders the sidebar (defensive).
const obs = new MutationObserver(() => {
const nav = document.querySelector(".sidebar-nav");
if (!nav) return;
const missing = TABS.some((t) => !nav.querySelector('[data-panel="' + t.id + '"]'));
if (missing) _injectButtons();
});
if (document.body) {
obs.observe(document.body, { childList: true, subtree: true });
} else {
document.addEventListener("DOMContentLoaded", () =>
obs.observe(document.body, { childList: true, subtree: true }),
);
}
// Expose namespace
window.SvrntyNav = { TABS, _injectButtons, _wrapSwitchPanel };
})();