0.3.38: brand-green scrollbars everywhere + captureTab widens the viewport when sidebar shrinks the tab

Two changes shipped together (main.js touched by both this session and a
parallel session in different regions):

Scrollbars — from the "empty white space should be grey, thumb should
be Bitcoin Cash green" ask:
  * new SCROLLBAR_CSS constant + styleScrollbars(wc) helper injects the
    theme on every dom-ready
  * thumb #0AC18E (BCH primary), track rgba(120,130,150,0.18) subtle
    neutral grey so it works on both dark and light surfaces without
    hardcoding either; 6px radius, 2px inset via background-clip:padding-box
  * modern scrollbar-color on <html> for Chromium 121+; ::-webkit- rules
    with !important as the fallback / override for sites that theme
    their own scrollbars — scrollbar-width intentionally left alone so
    a page that hides scrollbars entirely keeps that behaviour
  * hooked into every wc we own: createTab, chrome, popover, enginePicker,
    downloadsPop, addressPicker, pwFillPop, linkStatus, sidebar (so every
    add-on panel like Aegis picks it up), approvalPop
  * fires once immediately if the wc is already past dom-ready when we
    attach — fixed views load fast during startup, we'd otherwise miss

captureTab full-page — from the parallel session's screenshot work:
  * before Page.captureScreenshot with captureBeyondViewport we now
    override Emulation.setDeviceMetricsOverride to the window's full
    content width so an open sidebar (or other on-screen chrome that
    narrowed the tab view) doesn't clip the shot — capture comes back at
    the page's natural full width, not the visible width
  * attach the debugger for the call if it isn't attached, detach on
    return; clear the metrics override in finally so the tab returns to
    its normal layout regardless of success
This commit is contained in:
Local Dev 2026-09-09 00:42:38 +02:00
parent e36d2b361c
commit d7d4e7eb4e
2 changed files with 85 additions and 6 deletions

78
main.js
View file

@ -1610,10 +1610,41 @@ function initAddons() {
} }
if (mode === "full") { if (mode === "full") {
// Page.captureScreenshot with captureBeyondViewport does the whole // Page.captureScreenshot with captureBeyondViewport does the whole
// scrollable page in one shot, no setBounds gymnastics needed. // scrollable page. Before we shoot, force the layout viewport to the
// window's full content width via Emulation.setDeviceMetricsOverride
// so an open sidebar (or any other on-screen chrome that narrowed
// the tab view) doesn't clip the capture — the shot always comes
// back at the page's natural full width, not the visible width.
const wasAttached = wc.debugger.isAttached();
if (!wasAttached) {
try { wc.debugger.attach("1.3"); }
catch (e) { if (!/already attached/i.test(String(e?.message))) throw e; }
}
let overrode = false;
try {
const winW = (win?.getContentBounds()?.width) || 0;
const tabB = t.view.getBounds();
const need = winW > tabB.width + 24 ? winW : 0;
if (need > 0) {
// dsf 0 = "let Chromium keep the real device scale factor".
// mobile false, deviceScaleFactor 0 keeps typography sane;
// height 0 tells CDP "use the current viewport height".
await wc.debugger.sendCommand("Emulation.setDeviceMetricsOverride", {
width: need, height: 0, deviceScaleFactor: 0, mobile: false,
});
overrode = true;
// A frame or two so the reflow settles before we snapshot.
await new Promise((r) => setTimeout(r, 250));
}
const r = await cdpCapture({ full: true }); const r = await cdpCapture({ full: true });
console.log(`[addons] [${addonId}] captureTab full ${r.width}x${r.height}`); console.log(`[addons] [${addonId}] captureTab full ${r.width}x${r.height}${overrode ? ` (viewport widened to ${need}px)` : ""}`);
return { dataUrl: r.dataUrl, width: r.width, height: r.height, host, format }; return { dataUrl: r.dataUrl, width: r.width, height: r.height, host, format };
} finally {
if (overrode) {
try { await wc.debugger.sendCommand("Emulation.clearDeviceMetricsOverride"); } catch {}
}
if (!wasAttached) { try { wc.debugger.detach(); } catch {} }
}
} }
if (mode === "region") { if (mode === "region") {
const src = String(opts?.overlaySource || ""); const src = String(opts?.overlaySource || "");
@ -2177,6 +2208,34 @@ function loadErrorPage(t, id, { url, code, desc }) {
if (id === activeId) pushNav(t.prov); if (id === activeId) pushNav(t.prov);
emitTabs(); emitTabs();
} }
// Scrollbar theme injected into every webContents we own — tabs, chrome,
// sidebar, all the floating popovers, and every add-on panel host. Track
// picks up a subtle neutral grey (works on both dark and light surfaces
// without hardcoding either); thumb is the BCH primary #0AC18E so every
// scroll surface reads as Silent Mode's. `scrollbar-color` is the modern
// standard (Chromium ≥ 121); the ::-webkit- fallback gives us fine control
// over width, radius and hover state on older engines. `!important` on the
// track / thumb wins over per-page overrides so the branding stays visible
// even on sites that theme their own scrollbars — but we deliberately don't
// force `scrollbar-width` so a page that has hidden its scrollbars entirely
// keeps that behaviour.
const SCROLLBAR_CSS = `
html { scrollbar-color: #0AC18E rgba(120,130,150,0.18); }
::-webkit-scrollbar { width: 12px; height: 12px; background: rgba(120,130,150,0.18) !important; }
::-webkit-scrollbar-track { background: rgba(120,130,150,0.18) !important; }
::-webkit-scrollbar-thumb { background: #0AC18E !important; border-radius: 6px;
border: 2px solid transparent; background-clip: padding-box !important; }
::-webkit-scrollbar-thumb:hover { background: #14e0a5 !important; background-clip: padding-box !important; }
::-webkit-scrollbar-corner { background: transparent !important; }
`;
function styleScrollbars(wc) {
if (!wc) return;
const inject = () => { try { wc.insertCSS(SCROLLBAR_CSS); } catch {} };
wc.on("dom-ready", inject);
// For a wc that's already past dom-ready when we attach (fixed views load
// fast during startup), fire once explicitly.
try { if (!wc.isLoading()) inject(); } catch {}
}
function createTab(initial, opts = {}) { function createTab(initial, opts = {}) {
const id = ++tabSeq; const id = ++tabSeq;
// Non-settings tabs get home-preload so the built-in home page can round- // Non-settings tabs get home-preload so the built-in home page can round-
@ -2197,6 +2256,7 @@ function createTab(initial, opts = {}) {
// mode users don't get a dark stub while a page paints. // mode users don't get a dark stub while a page paints.
try { view.setBackgroundColor(nativeTheme.shouldUseDarkColors ? "#0b0e14" : "#ffffff"); } catch {} try { view.setBackgroundColor(nativeTheme.shouldUseDarkColors ? "#0b0e14" : "#ffffff"); } catch {}
const wc = view.webContents; const wc = view.webContents;
styleScrollbars(wc);
try { wc.setWebRTCIPHandlingPolicy(webrtcPolicy()); } catch {} try { wc.setWebRTCIPHandlingPolicy(webrtcPolicy()); } catch {}
try { wc.setBackgroundThrottling(settings.backgroundThrottle); } catch {} try { wc.setBackgroundThrottling(settings.backgroundThrottle); } catch {}
applyFingerprint(wc); applyFingerprint(wc);
@ -2398,52 +2458,64 @@ function createWindow() {
}); });
chrome = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "preload.js") } }); chrome = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "preload.js") } });
win.contentView.addChildView(chrome); win.contentView.addChildView(chrome);
styleScrollbars(chrome.webContents);
chrome.webContents.loadFile("chrome.html"); chrome.webContents.loadFile("chrome.html");
// Floating site-info overlay (hidden until the address-bar badge is clicked). // Floating site-info overlay (hidden until the address-bar badge is clicked).
popover = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "popover-preload.js") } }); popover = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "popover-preload.js") } });
try { popover.setBackgroundColor("#00000000"); } catch {} try { popover.setBackgroundColor("#00000000"); } catch {}
win.contentView.addChildView(popover); win.contentView.addChildView(popover);
styleScrollbars(popover.webContents);
popover.webContents.loadFile("popover.html"); popover.webContents.loadFile("popover.html");
popover.setVisible(false); popover.setVisible(false);
// Floating engine-picker overlay (custom dropdown with real favicons). // Floating engine-picker overlay (custom dropdown with real favicons).
enginePicker = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "engine-picker-preload.js") } }); enginePicker = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "engine-picker-preload.js") } });
try { enginePicker.setBackgroundColor("#00000000"); } catch {} try { enginePicker.setBackgroundColor("#00000000"); } catch {}
win.contentView.addChildView(enginePicker); win.contentView.addChildView(enginePicker);
styleScrollbars(enginePicker.webContents);
enginePicker.webContents.loadFile("engine-picker.html"); enginePicker.webContents.loadFile("engine-picker.html");
enginePicker.setVisible(false); enginePicker.setVisible(false);
// Floating downloads panel — shows active + recent downloads. // Floating downloads panel — shows active + recent downloads.
downloadsPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "downloads-preload.js") } }); downloadsPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "downloads-preload.js") } });
try { downloadsPop.setBackgroundColor("#00000000"); } catch {} try { downloadsPop.setBackgroundColor("#00000000"); } catch {}
win.contentView.addChildView(downloadsPop); win.contentView.addChildView(downloadsPop);
styleScrollbars(downloadsPop.webContents);
downloadsPop.webContents.loadFile("downloads.html"); downloadsPop.webContents.loadFile("downloads.html");
downloadsPop.setVisible(false); downloadsPop.setVisible(false);
// Floating address-bar suggestions dropdown. // Floating address-bar suggestions dropdown.
addressPicker = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "address-picker-preload.js") } }); addressPicker = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "address-picker-preload.js") } });
try { addressPicker.setBackgroundColor("#00000000"); } catch {} try { addressPicker.setBackgroundColor("#00000000"); } catch {}
win.contentView.addChildView(addressPicker); win.contentView.addChildView(addressPicker);
styleScrollbars(addressPicker.webContents);
addressPicker.webContents.loadFile("address-picker.html"); addressPicker.webContents.loadFile("address-picker.html");
addressPicker.setVisible(false); addressPicker.setVisible(false);
// Floating password-fill picker. // Floating password-fill picker.
pwFillPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "pw-fill-preload.js") } }); pwFillPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "pw-fill-preload.js") } });
try { pwFillPop.setBackgroundColor("#00000000"); } catch {} try { pwFillPop.setBackgroundColor("#00000000"); } catch {}
win.contentView.addChildView(pwFillPop); win.contentView.addChildView(pwFillPop);
styleScrollbars(pwFillPop.webContents);
pwFillPop.webContents.loadFile("pw-fill.html"); pwFillPop.webContents.loadFile("pw-fill.html");
pwFillPop.setVisible(false); pwFillPop.setVisible(false);
// Link-hover status pill (bottom-left of window). // Link-hover status pill (bottom-left of window).
linkStatus = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "link-status-preload.js") } }); linkStatus = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "link-status-preload.js") } });
try { linkStatus.setBackgroundColor("#00000000"); } catch {} try { linkStatus.setBackgroundColor("#00000000"); } catch {}
win.contentView.addChildView(linkStatus); win.contentView.addChildView(linkStatus);
styleScrollbars(linkStatus.webContents);
linkStatus.webContents.loadFile("link-status.html"); linkStatus.webContents.loadFile("link-status.html");
linkStatus.setVisible(false); linkStatus.setVisible(false);
// Add-on sidebar host. Doesn't loadFile until an add-on panel is opened. // Add-on sidebar host. Doesn't loadFile until an add-on panel is opened —
// styleScrollbars hooks dom-ready, which fires per navigation, so every
// panel loaded into this view (Aegis, Screenshot, etc.) picks up the
// brand scrollbar the moment its DOM is ready.
sidebar = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "sidebar-preload.js") } }); sidebar = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "sidebar-preload.js") } });
win.contentView.addChildView(sidebar); win.contentView.addChildView(sidebar);
styleScrollbars(sidebar.webContents);
sidebar.setVisible(false); sidebar.setVisible(false);
// Add-on approval overlay (approval-modal capability). Transparent view // Add-on approval overlay (approval-modal capability). Transparent view
// over the tab area, loaded once, shown per request. // over the tab area, loaded once, shown per request.
approvalPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "approval-preload.js") } }); approvalPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "approval-preload.js") } });
try { approvalPop.setBackgroundColor("#00000000"); } catch {} try { approvalPop.setBackgroundColor("#00000000"); } catch {}
win.contentView.addChildView(approvalPop); win.contentView.addChildView(approvalPop);
styleScrollbars(approvalPop.webContents);
approvalPop.webContents.loadFile("approval.html"); approvalPop.webContents.loadFile("approval.html");
approvalPop.setVisible(false); approvalPop.setVisible(false);
chrome.webContents.once("did-finish-load", () => { chrome.webContents.once("did-finish-load", () => {

View file

@ -1,6 +1,6 @@
{ {
"name": "theseus-navigator", "name": "theseus-navigator",
"version": "0.3.37", "version": "0.3.38",
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
"author": "Silent Mode", "author": "Silent Mode",
"main": "main.js", "main": "main.js",
@ -11,15 +11,22 @@
"dist": "electron-builder --win nsis portable" "dist": "electron-builder --win nsis portable"
}, },
"dependencies": { "dependencies": {
"@bch-wc2/interfaces": "^0.0.16",
"@bitauth/libauth": "^3.1.0-next.8",
"@bitcoinerlab/secp256k1": "^1.2.0", "@bitcoinerlab/secp256k1": "^1.2.0",
"@noble/curves": "^2.0.1", "@noble/curves": "^2.0.1",
"@noble/hashes": "^2.0.1", "@noble/hashes": "^2.0.1",
"@scure/bip32": "^2.0.1", "@scure/bip32": "^2.0.1",
"@wizardconnect/core": "^0.2.4",
"@wizardconnect/wallet": "^0.2.3",
"bip32": "^4.0.0", "bip32": "^4.0.0",
"bip39": "^3.1.0", "bip39": "^3.1.0",
"bitcoinjs-lib": "^6.1.7", "bitcoinjs-lib": "^6.1.7",
"ecpair": "^2.1.0", "ecpair": "^2.1.0",
"eventemitter3": "^5.0.1",
"fetch-socks": "^1.3.3", "fetch-socks": "^1.3.3",
"isomorphic-ws": "^5.0.0",
"lossless-json": "^4.0.2",
"nostr-tools": "^2.10.4", "nostr-tools": "^2.10.4",
"psl": "^1.15.0", "psl": "^1.15.0",
"socks-proxy-agent": "^10.1.0", "socks-proxy-agent": "^10.1.0",