svrnty-hermes-webui-plugin/static/svrnty_nav.js
Svrnty 4b1f2075ae
Some checks failed
plugin-tests / test (push) Failing after 5s
Add Project Graph nav link
2026-05-26 05:26:15 -04:00

184 lines
6.5 KiB
JavaScript
Raw 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"/>',
graph:
'<circle cx="5" cy="12" r="2"/><circle cx="12" cy="5" r="2"/><circle cx="19" cy="12" r="2"/><circle cx="12" cy="19" r="2"/><path d="M6.7 10.6l3.9-4"/><path d="M13.4 6.6l3.9 4"/><path d="M17.3 13.4l-3.9 4"/><path d="M10.6 17.4l-3.9-4"/><path d="M7 12h10"/>',
};
const TABS = [
{ id: "adwright", label: "Adwright", tooltip: "Adwright — marketing intelligence" },
{ id: "bte", label: "BTE", tooltip: "BTE — brand creative studio" },
{
id: "project-graph",
label: "Project Graph",
tooltip: "Project Graph — open workspace graph",
href: "/plugins/svrnty/umbrella.html",
icon: "graph",
},
];
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 (t.href) {
window.open(t.href, "_blank", "noopener,noreferrer");
return;
}
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.filter((t) => !t.href).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 };
})();