@@ -573,6 +579,16 @@
// progress). Click a live button → open the sidebar on that panel; if
// the sidebar was already open on that panel, collapse it.
const extDock = $("extdock");
+ // Add-on icon → HTML. Text by default (an emoji like 🛡, or "•"). An
+ // add-on that wants a branded mark sets manifest.icon to a data: URI
+ // (svg or png) and we render it as an in the same slot. Anything
+ // else is HTML-escaped and stays text. Used by the dock buttons, the
+ // collapsed dock button and the collapsed dropdown alike.
+ function addonIconHtml(icon, size = 18) {
+ const s = String(icon || "•");
+ if (/^data:image\//i.test(s)) return ` `;
+ return s.replace(/&/g,"&").replace(//g,">");
+ }
const extButtons = $("extbuttons");
// Flat list of dock buttons — an addon that registers a sidebar-panel and
// an addon that declares a toolbar-menu both get one entry here. panels
@@ -611,13 +627,7 @@
// URI (svg or png) and we render it as an instead — same size
// slot, no layout change. Anything else is HTML-escaped and stays
// text so the emoji case keeps working.
- let iconHtml;
- if (/^data:image\//i.test(b.icon)) {
- iconHtml = ` `;
- } else {
- iconHtml = b.icon.replace(/&/g,"&").replace(//g,">");
- }
- return `${iconHtml} `;
+ return `${addonIconHtml(b.icon)} `;
}).join("");
extButtons.querySelectorAll(".extbtn").forEach((btn) => {
btn.onclick = () => {
@@ -635,7 +645,9 @@
// to 🧩 if nothing is registered). Clicking it opens a dropdown of
// every registered surface so the user can pick.
const first = lastButtons[0];
- $("extmore").textContent = first ? first.icon : "🧩";
+ // Same renderer as the full-size buttons — a data: URI icon used to be
+ // dumped here as text ("data:image/svg+xml…") once the dock collapsed.
+ $("extmore").innerHTML = first ? addonIconHtml(first.icon) : "🧩";
$("extmore").title = first ? "Extensions — " + first.title : "Extensions";
$("extmore").classList.toggle("active", (sidebarOpen && !!activePanelId) || !!openMenuAddonId);
}
@@ -697,10 +709,10 @@
const rows = [];
for (const p of lastPanels) {
const isActive = sidebarOpen && p.panelId === activePanelId;
- rows.push(`${String(p.icon || "•").replace(/${(p.title || "Extension").replace(/
`);
+ rows.push(`${addonIconHtml(p.icon, 16)} ${(p.title || "Extension").replace(/
`);
}
for (const m of lastMenus) {
- rows.push(`${String(m.icon || "•").replace(/${(m.title || m.addonId).replace(/
`);
+ rows.push(`${addonIconHtml(m.icon, 16)} ${(m.title || m.addonId).replace(/
`);
}
pop.innerHTML = rows.join("") || `No extensions installed
`;
document.body.appendChild(pop);
@@ -1122,8 +1134,11 @@
});
// ---- tabs ----
+ $("zoomchip").onclick = () => T.zoomReset && T.zoomReset();
T.onTabs((d) => {
$("back").disabled = !d.canBack; $("fwd").disabled = !d.canForward;
+ const zc = $("zoomchip"), z = Number(d.zoom) || 100;
+ zc.hidden = z === 100; zc.textContent = z + "%";
// loading indicator: progress bar + reload↔stop button
$("loadbar").classList.toggle("on", !!d.loading);
// Any tabs event that fires while no menu is open is a good moment to
@@ -1493,12 +1508,11 @@
const urlsearchEl = $("urlsearch");
// Absolute minimums track the CSS min-widths so the drag can't push either
// bar below the size where its content stops fitting. On top of that we
- // enforce a 30 % share floor for both bars: the URL bar can never take more
- // than 70 % of the combined budget, and the search bar can never be less
- // than 30 %. Combined with the CSS min-widths, the tighter of the two rules
- // wins at any given container width.
+ // enforce a 20 % share floor for both bars: the ratio can run from 80/20
+ // to 20/80 (URL bar / search bar). Combined with the CSS min-widths, the
+ // tighter of the two rules wins at any given container width.
const MIN_URL_PX = 220, MIN_SEARCH_PX = 180, HANDLE_W = 6, GAP = 6;
- const MIN_SHARE = 0.30;
+ const MIN_SHARE = 0.20;
let dragging = false, startX = 0, startW = 0, lastW = 0;
handleEl.addEventListener("pointerdown", (e) => {
if (e.button !== 0) return;
diff --git a/main.js b/main.js
index dcaefa59..44345df6 100644
--- a/main.js
+++ b/main.js
@@ -2415,9 +2415,35 @@ function emitTabs() {
loading: !!t?.loading,
canBack: wc ? wc.navigationHistory.canGoBack() : false,
canForward: wc ? wc.navigationHistory.canGoForward() : false,
+ zoom: zoomPercent(t),
});
}
function setLoading(tab, on) { if (tab && tab.loading !== on) { tab.loading = on; emitTabs(); } }
+// ---- page zoom ----
+// Chrome's preset ladder. Zoom is applied with setZoomFactor, which
+// Chromium keys per host for the session — so every tab on the same site
+// shares the level, and navigating back to a site restores it, exactly
+// like Chrome. Settings and add-on tabs never zoom.
+const ZOOM_STEPS = [25, 33, 50, 67, 75, 80, 90, 100, 110, 125, 150, 175, 200, 250, 300, 400, 500];
+function zoomPercent(t) {
+ if (!t || t.settings || t.addonId) return 100;
+ try { return Math.round(t.view.webContents.getZoomFactor() * 100); } catch { return 100; }
+}
+function zoomStep(t, dir) {
+ if (!t || t.settings || t.addonId) return;
+ const cur = zoomPercent(t);
+ const next = dir > 0
+ ? (ZOOM_STEPS.find((z) => z > cur) ?? ZOOM_STEPS[ZOOM_STEPS.length - 1])
+ : ([...ZOOM_STEPS].reverse().find((z) => z < cur) ?? ZOOM_STEPS[0]);
+ zoomSet(t, next);
+}
+function zoomSet(t, percent) {
+ if (!t || t.settings || t.addonId) return;
+ try { t.view.webContents.setZoomFactor(Math.max(25, Math.min(500, percent)) / 100); } catch {}
+ emitTabs();
+}
+ipcMain.handle("zoom-step", (_e, dir) => zoomStep(activeTab(), Number(dir) > 0 ? 1 : -1));
+ipcMain.handle("zoom-reset", () => zoomSet(activeTab(), 100));
// Pull the tab's real URL from webContents after Electron navigates, so in-page
// clicks (subpages of a BCNR site, subdomain hops, cross-origin redirects) update
// the address bar. Without this, t.url is only refreshed on programmatic loads —
@@ -2564,6 +2590,9 @@ function createTab(initial, opts = {}) {
});
wc.on("did-navigate", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); });
wc.on("did-navigate-in-page", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); });
+ // Ctrl+wheel / pinch: Chromium only reports the intent on Windows and
+ // Linux, the zoom itself is up to us.
+ wc.on("zoom-changed", (_e, dir) => zoomStep(tab, dir === "in" ? 1 : -1));
wc.on("did-start-loading", () => setLoading(tab, true));
wc.on("did-stop-loading", () => setLoading(tab, false));
// Failed loads: NAME_NOT_RESOLVED, CONNECTION_REFUSED, cert errors, etc.
@@ -4831,6 +4860,14 @@ app.on("web-contents-created", (_event, wc) => {
toggleSidebar();
return e.preventDefault();
}
+ // Zoom: Ctrl + / Ctrl = / numpad + zoom in, Ctrl - / numpad - zoom out,
+ // Ctrl 0 reset. Always targets the active tab, whichever view has focus.
+ if (input.control && !input.alt) {
+ const k = input.key, code = input.code;
+ if (k === "+" || k === "=" || code === "NumpadAdd") { zoomStep(activeTab(), 1); return e.preventDefault(); }
+ if (k === "-" || k === "_" || code === "NumpadSubtract") { zoomStep(activeTab(), -1); return e.preventDefault(); }
+ if ((k === "0" || code === "Numpad0") && !input.shift) { zoomSet(activeTab(), 100); return e.preventDefault(); }
+ }
// Find-in-page: Ctrl+F opens the find bar in chrome. Chrome renderer
// owns the UI (input, next/prev, count, close); it drives the active
// tab's webContents.findInPage via IPC (find-in-page / find-stop).
diff --git a/preload.js b/preload.js
index 78fb8832..8da50ec3 100644
--- a/preload.js
+++ b/preload.js
@@ -90,6 +90,9 @@ contextBridge.exposeInMainWorld("theseus", {
// Ariadne's Thread menu behind the registry button at the end of the
// address bar (BCDN/ICANN switch, remembered choices, policy, settings).
registryMenuPopup: (rect) => ipcRenderer.invoke("registry-menu-popup", rect),
+ // Page zoom on the active tab: step +1 / -1 along Chrome's ladder, or reset to 100 %.
+ zoomStep: (dir) => ipcRenderer.invoke("zoom-step", dir),
+ zoomReset: () => ipcRenderer.invoke("zoom-reset"),
// Chrome listens so it can drop the "active" tint when the native menu
// dismisses (Esc, outside click, item picked — main fires all three).
onToolbarMenuClosed: (cb) => ipcRenderer.on("toolbar-menu-closed", () => cb()),