fix(plugin): inject sidebar buttons into BOTH .rail (desktop) + .sidebar-nav (mobile)
Some checks failed
plugin-tests / test (push) Failing after 5s

ROOT CAUSE of JP's "buttons not showing" report: hermes-webui has two nav
containers — `<nav class="rail">` (desktop ≥641px, rail-btn 20×20 stroke
1.5) and `.sidebar-nav` (mobile, nav-tab 18×18 stroke 2). My previous
injection only touched .sidebar-nav, so on desktop the buttons literally
didn't exist anywhere visible.

This patch:
- Defines per-container button templates (class list + svg size/stroke)
  matching exactly how upstream renders its native rail-btn vs nav-tab
- Iterates both containers on init + retries
- MutationObserver re-injects into either if something re-renders it
- Tooltip modifier (--bottom) only applied for sidebar-nav (mobile pattern)
- SVG paths centralized so adding new tabs is one entry

Karpathy 4 rules: surfaced root cause via console logs + DOM inspection
instead of guessing, simplest fix (data-driven container list), surgical
(no other behavior changed).
This commit is contained in:
Svrnty 2026-05-24 12:58:06 -04:00
parent 7a5c48c775
commit b43e6496f5

View File

@ -15,55 +15,84 @@
const ERR = (...a) => console.error("[svrnty-nav]", ...a); const ERR = (...a) => console.error("[svrnty-nav]", ...a);
LOG("loaded", { readyState: document.readyState }); 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"/>',
};
const TABS = [ const TABS = [
{ id: "adwright", label: "Adwright", tooltip: "Adwright — marketing intelligence" },
{ id: "bte", label: "BTE", tooltip: "BTE — brand creative studio" },
];
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 = [
{ {
id: "adwright", selector: ".rail",
label: "Adwright", btnClass: "rail-btn nav-tab has-tooltip svrnty-nav-tab",
tooltip: "Adwright — marketing intelligence", size: 20,
// Bullseye / target icon — marketing focus stroke: 1.5,
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>', tooltipMod: "",
}, },
{ {
id: "bte", selector: ".sidebar-nav",
label: "BTE", btnClass: "nav-tab has-tooltip has-tooltip--bottom svrnty-nav-tab",
tooltip: "BTE — brand creative studio", size: 18,
// Palette/sparkle icon — creative stroke: 2,
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>', tooltipMod: "--bottom",
}, },
]; ];
function _injectButtons() { function _injectButtons() {
const nav = document.querySelector(".sidebar-nav"); let anyContainerFound = false;
if (!nav) { let totalAdded = 0;
LOG("_injectButtons: .sidebar-nav not found yet"); 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.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; return false;
} }
let added = 0; LOG("_injectButtons: added", totalAdded, "buttons across containers");
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; return true;
} }
@ -119,12 +148,14 @@
_init(); _init();
} }
// Re-inject buttons if something re-renders the sidebar (defensive). // Re-inject buttons if something re-renders rail or sidebar-nav (defensive).
const obs = new MutationObserver(() => { const obs = new MutationObserver(() => {
const nav = document.querySelector(".sidebar-nav"); CONTAINERS.forEach((c) => {
if (!nav) return; const container = document.querySelector(c.selector);
const missing = TABS.some((t) => !nav.querySelector('[data-panel="' + t.id + '"]')); if (!container) return;
if (missing) _injectButtons(); const missing = TABS.some((t) => !container.querySelector('[data-panel="' + t.id + '"]'));
if (missing) _injectButtons();
});
}); });
if (document.body) { if (document.body) {
obs.observe(document.body, { childList: true, subtree: true }); obs.observe(document.body, { childList: true, subtree: true });