feat(theseus): plug-in dock under the Theseus menu

Aegis sat among the extension buttons and drifted as add-ons were
installed or removed. First-class Silent Mode components (manifest
category "plugin") now get a labelled chip at the right end of the
favorites row, directly below the Theseus menu — always visible, never
collapsed into the extensions overflow. The add-on host tags each
sidebar panel and toolbar menu with the category so the chrome can
split the two docks; the extensions dock keeps its behaviour.
This commit is contained in:
Local Dev 2026-09-22 00:31:18 +02:00
parent 0da15818ad
commit ed2d2e1d5a
2 changed files with 61 additions and 7 deletions

View file

@ -687,9 +687,15 @@ class AddonHost {
toolbarMenus: this.getToolbarMenus(), toolbarMenus: this.getToolbarMenus(),
}; };
} }
// `plugin` marks surfaces of a first-class Silent Mode component (manifest
// category "plugin", e.g. Aegis): the chrome pins those to their own dock
// instead of the extensions row.
getSidebarPanels() { getSidebarPanels() {
const out = []; const out = [];
for (const active of this._active.values()) out.push(...active.sidebarPanels); for (const active of this._active.values()) {
const plugin = active.manifest.category === "plugin";
for (const p of active.sidebarPanels) out.push({ ...p, plugin, addonName: active.manifest.name });
}
return out; return out;
} }
// Menu declarations from every active add-on that carries a toolbar-menu // Menu declarations from every active add-on that carries a toolbar-menu
@ -704,6 +710,7 @@ class AddonHost {
addonId: active.manifest.id, addonId: active.manifest.id,
title: tm.title, title: tm.title,
icon: tm.icon, icon: tm.icon,
plugin: active.manifest.category === "plugin",
items: tm.items.map((it) => ({ id: it.id, label: it.label, icon: it.icon })), items: tm.items.map((it) => ({ id: it.id, label: it.label, icon: it.icon })),
}); });
} }

View file

@ -345,6 +345,17 @@
uniform fixed width so the row reads as a neat grid regardless of the uniform fixed width so the row reads as a neat grid regardless of the
underlying title length; the strip is 28px tall to fit the roomier chips. */ underlying title length; the strip is 28px tall to fit the roomier chips. */
.bookmarks { display: flex; align-items: center; gap: 6px; padding: 4px 10px 6px; height: 28px; overflow: hidden; } .bookmarks { display: flex; align-items: center; gap: 6px; padding: 4px 10px 6px; height: 28px; overflow: hidden; }
#bmlist { display: flex; align-items: center; gap: 6px; flex: 1 1 auto; min-width: 0; overflow: hidden; }
/* Plug-in dock: first-class Silent Mode components (Aegis) get a labelled
chip at the right end of the favorites row, directly under the Theseus
menu — always visible, never collapsed with the extensions. */
.plugdock { margin-left: auto; flex: none; display: flex; align-items: center; gap: 4px; padding-left: 8px; }
.plugbtn { display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 10px 0 8px; border-radius: 6px;
font: inherit; font-size: 12px; font-weight: 600; color: var(--mut); cursor: pointer; white-space: nowrap;
border: 1px solid var(--line); background: transparent; }
.plugbtn:hover { background: var(--line2); color: var(--ink); }
.plugbtn.active { background: rgb(from var(--acid) r g b / .12); color: var(--acid); border-color: color-mix(in srgb, var(--acid) 35%, transparent); }
.plugbtn img { display: block; }
/* Ctrl+F find-in-page bar. Sits below the address bar (above tabs body), /* Ctrl+F find-in-page bar. Sits below the address bar (above tabs body),
~32px tall so it's part of the chrome view height like .tordisc / .bcnrbar. ~32px tall so it's part of the chrome view height like .tordisc / .bcnrbar.
syncHeight in the tabs handler picks it up automatically. */ syncHeight in the tabs handler picks it up automatically. */
@ -477,7 +488,7 @@
<button class="logo" id="logo" title="Settings"><span>⛓ Theseus</span><span class="gear"></span></button> <button class="logo" id="logo" title="Settings"><span>⛓ Theseus</span><span class="gear"></span></button>
</div> </div>
<div class="loadbar" id="loadbar"></div> <div class="loadbar" id="loadbar"></div>
<div class="bookmarks" id="bookmarks" hidden></div> <div class="bookmarks" id="bookmarks" hidden><div id="bmlist"></div><span class="plugdock" id="plugdock" hidden></span></div>
<div class="findbar" id="findbar" hidden> <div class="findbar" id="findbar" hidden>
<input id="findInput" type="text" placeholder="Find in page" spellcheck="false" autocomplete="off"> <input id="findInput" type="text" placeholder="Find in page" spellcheck="false" autocomplete="off">
<span class="findcount" id="findCount"></span> <span class="findcount" id="findCount"></span>
@ -666,15 +677,21 @@
// an addon that declares a toolbar-menu both get one entry here. panels // an addon that declares a toolbar-menu both get one entry here. panels
// stay clickable-toggle; menus open a floating dropdown positioned under // stay clickable-toggle; menus open a floating dropdown positioned under
// the icon and dispatch the picked item back to the add-on. // the icon and dispatch the picked item back to the add-on.
let lastPanels = [], lastMenus = [], lastButtons = []; let lastPanels = [], lastMenus = [], lastButtons = [], lastAllPanels = [], lastAllMenus = [];
let sidebarOpen = false, activePanelId = null; let sidebarOpen = false, activePanelId = null;
let openMenuAddonId = null; // which addon's menu dropdown is currently open let openMenuAddonId = null; // which addon's menu dropdown is currently open
function renderSidebarState(d) { function renderSidebarState(d) {
if (!d) return; if (!d) return;
lastPanels = Array.isArray(d.panels) ? d.panels : []; const allPanels = Array.isArray(d.panels) ? d.panels : [];
lastMenus = Array.isArray(d.toolbarMenus) ? d.toolbarMenus : []; const allMenus = Array.isArray(d.toolbarMenus) ? d.toolbarMenus : [];
sidebarOpen = !!d.visible; sidebarOpen = !!d.visible;
activePanelId = d.active || null; activePanelId = d.active || null;
// Plug-ins (Aegis) live in their own dock on the favorites row, under
// the Theseus menu; everything else is an extension in the toolbar dock.
lastAllPanels = allPanels; lastAllMenus = allMenus;
lastPanels = allPanels.filter((p) => !p.plugin);
lastMenus = allMenus.filter((m) => !m.plugin);
renderPlugDock(allPanels.filter((p) => p.plugin), allMenus.filter((m) => m.plugin));
// Build the flat button list, panels first (matches shipped order) then // Build the flat button list, panels first (matches shipped order) then
// toolbar-menu buttons in registration order. // toolbar-menu buttons in registration order.
lastButtons = []; lastButtons = [];
@ -724,6 +741,36 @@
$("extmore").classList.toggle("active", (sidebarOpen && !!activePanelId) || !!openMenuAddonId); $("extmore").classList.toggle("active", (sidebarOpen && !!activePanelId) || !!openMenuAddonId);
} }
// Plug-in dock (favorites row, right end). A chip per plug-in surface:
// icon + name, tinted while its sidebar panel or menu is open. Same
// actions as the toolbar dock buttons, never collapsed.
function renderPlugDock(panels, menus) {
const dock = $("plugdock");
const chips = [];
// Chip label is the plug-in's product name (e.g. "Aegis Wallet"), the
// panel title goes in the tooltip.
for (const p of panels) chips.push({ kind: "panel", panelId: p.panelId, icon: p.icon, title: p.title || "Plug-in", name: p.addonName || p.title || "Plug-in", active: sidebarOpen && p.panelId === activePanelId });
for (const m of menus) chips.push({ kind: "menu", addonId: m.addonId, icon: m.icon, title: m.title || m.addonId, name: m.title || m.addonId, active: openMenuAddonId === m.addonId });
dock.hidden = chips.length === 0;
dock.innerHTML = chips.map((c) => {
const dataAttr = c.kind === "panel" ? `data-panel="${c.panelId.replace(/"/g,"&quot;")}"` : `data-menu="${c.addonId.replace(/"/g,"&quot;")}"`;
const label = String(c.name).split(/\s+[—–-]\s+/, 1)[0].replace(/</g,"&lt;");
return `<button class="plugbtn${c.active ? " active" : ""}" ${dataAttr} title="${c.title.replace(/"/g,"&quot;")}">${addonIconHtml(c.icon, 14)}<span>${label}</span></button>`;
}).join("");
dock.querySelectorAll(".plugbtn").forEach((btn) => {
btn.onclick = () => {
if (btn.dataset.panel) {
const pid = btn.dataset.panel;
closeToolbarMenu();
if (sidebarOpen && pid === activePanelId) { T.closeSidebar && T.closeSidebar(); }
else { T.openSidebar && T.openSidebar(pid); }
} else if (btn.dataset.menu) {
openToolbarMenu(btn.dataset.menu, btn);
}
};
});
}
// ---- toolbar-menu dropdown (per-addon) ---- // ---- toolbar-menu dropdown (per-addon) ----
// Positions under the clicked icon, styled after .extpop. Clicking an // Positions under the clicked icon, styled after .extpop. Clicking an
// item dispatches to main via addonMenuSelect(addonId, itemId) and // item dispatches to main via addonMenuSelect(addonId, itemId) and
@ -734,7 +781,7 @@
if (openMenuAddonId) { if (openMenuAddonId) {
openMenuAddonId = null; openMenuAddonId = null;
// Re-render dock so the "active" tint drops off the icon. // Re-render dock so the "active" tint drops off the icon.
renderSidebarState({ visible: sidebarOpen, active: activePanelId, panels: lastPanels, toolbarMenus: lastMenus }); renderSidebarState({ visible: sidebarOpen, active: activePanelId, panels: lastAllPanels, toolbarMenus: lastAllMenus });
} }
} }
function openToolbarMenu(addonId, anchorBtn) { function openToolbarMenu(addonId, anchorBtn) {
@ -933,7 +980,7 @@
return bookmarkBrandCase(cut); return bookmarkBrandCase(cut);
} }
function renderBookmarks() { function renderBookmarks() {
const box = $("bookmarks"); const box = $("bmlist");
box.innerHTML = bookmarks.length box.innerHTML = bookmarks.length
? bookmarks.map((b, i) => { ? bookmarks.map((b, i) => {
const fav = b.favicon const fav = b.favicon