diff --git a/chrome.html b/chrome.html index cc1c1a29..b9128e84 100644 --- a/chrome.html +++ b/chrome.html @@ -248,7 +248,12 @@ its width and pushed the registry button and the star out past the bar's right edge. */ #url { flex: 1 1 0; min-width: 0; } - .secbadge, .pwchip, .star, .regbtn { flex: none; } + .secbadge, .pwchip, .star, .regbtn, .zoomchip { flex: none; } + /* Zoom indicator — only shown when the active tab isn't at 100 %. + Click resets (Ctrl+0); Ctrl +/- and Ctrl+wheel step the level. */ + .zoomchip { border: none; background: transparent; color: var(--mut); cursor: pointer; padding: 2px 5px; + border-radius: 6px; font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums; } + .zoomchip:hover { background: var(--line2); color: var(--ink); } /* Ariadne's Thread button at the END of the address bar. One icon in place of the old BCDN/ICANN chips: tinted acid when the page is served from BCDN, blue for ICANN, neutral otherwise. A small caret marks a @@ -410,6 +415,7 @@ + `; + return ``; }).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()),