svrnty-hermes-webui-plugin/static/svrnty_nav.js

177 lines
6.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// svrnty_nav.js — injects Svrnty 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 });
// SVG paths only — sizes/strokes templated per container (rail uses 20×20
// stroke 1.5; sidebar-nav uses 18×18 stroke 2). Matches existing buttons.
const ICONS = {
adwright:
'<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
bte:
'<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"/>',
canvas:
'<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 3v18M16 3v18M3 8h18M3 16h18"/>',
"cortex-os":
'<path d="M4 7h16M4 12h16M4 17h10"/><circle cx="17" cy="17" r="3"/>',
};
const TABS = [
{ id: "adwright", label: "Adwright", tooltip: "Adwright — marketing intelligence" },
{ id: "bte", label: "BTE", tooltip: "BTE — brand creative studio" },
{ id: "canvas", label: "Canvas", tooltip: "Canvas — sovereign UI design" },
{ id: "cortex-os", label: "Cortex OS", tooltip: "Cortex OS — Runtime Health" },
];
function _svg(iconPath, size, stroke) {
return (
'<svg width="' + size + '" height="' + size + '" viewBox="0 0 24 24" ' +
'fill="none" stroke="currentColor" stroke-width="' + stroke + '" ' +
'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
iconPath + "</svg>"
);
}
// Two containers: .rail (desktop, .rail-btn nav-tab 20×20 stroke-1.5) +
// .sidebar-nav (mobile, .nav-tab 18×18 stroke-2). Inject into BOTH if present.
const CONTAINERS = [
{
selector: ".rail",
btnClass: "rail-btn nav-tab has-tooltip svrnty-nav-tab",
size: 20,
stroke: 1.5,
tooltipMod: "",
},
{
selector: ".sidebar-nav",
btnClass: "nav-tab has-tooltip has-tooltip--bottom svrnty-nav-tab",
size: 18,
stroke: 2,
tooltipMod: "--bottom",
},
];
function _injectButtons() {
let anyContainerFound = false;
let totalAdded = 0;
CONTAINERS.forEach((c) => {
const container = document.querySelector(c.selector);
if (!container) return;
anyContainerFound = true;
TABS.forEach((t) => {
if (container.querySelector('[data-panel="' + t.id + '"]')) return;
try {
const btn = document.createElement("button");
btn.className = c.btnClass;
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 = _svg(ICONS[t.icon || t.id], c.size, c.stroke);
btn.addEventListener("click", () => {
LOG("clicked:", t.id);
if (typeof window.switchPanel === "function") {
window.switchPanel(t.id, { fromRailClick: true });
} else {
ERR("switchPanel undefined — cannot route click");
}
});
container.appendChild(btn);
totalAdded++;
} catch (e) {
ERR("inject failed", c.selector, t.id, e);
}
});
});
if (!anyContainerFound) {
LOG("_injectButtons: neither .rail nor .sidebar-nav present yet");
return false;
}
LOG("_injectButtons: added", totalAdded, "buttons across containers");
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 rail or sidebar-nav (defensive).
const obs = new MutationObserver(() => {
CONTAINERS.forEach((c) => {
const container = document.querySelector(c.selector);
if (!container) return;
const missing = TABS.some((t) => !container.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 };
})();