feat(theseus/chrome): page zoom, 80/20 address/search ratio, collapsed dock renders icon images
- Per-tab page zoom on Chrome's ladder (25–500 %) via setZoomFactor, so
Chromium keys it per host: every tab on a site shares the level and it
persists across navigations and restarts. Ctrl +/=/numpad+ in,
Ctrl -/numpad- out, Ctrl 0 reset, Ctrl+wheel via zoom-changed. A
percentage chip appears in the address bar when a tab isn't at 100 %;
clicking it resets. Settings and add-on tabs never zoom.
- Address bar / search bar drag ratio floor lowered from 30 % to 20 %,
so the split runs 80/20 to 20/80 (pixel floors still apply).
- The collapsed extension-dock button and its dropdown printed a data:
URI icon as text ("data:image/svg+xml…"). One addonIconHtml() renderer
now serves the dock buttons, the collapsed button and the dropdown.
This commit is contained in:
parent
7b8539fb8d
commit
56eff58fda
3 changed files with 70 additions and 16 deletions
46
chrome.html
46
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 @@
|
|||
</svg>
|
||||
</button>
|
||||
<input id="url" placeholder="Ask a search engine or enter web address" spellcheck="false">
|
||||
<button class="zoomchip" id="zoomchip" title="Reset zoom (Ctrl+0)" hidden></button>
|
||||
<button class="regbtn" id="reg" title="Ariadne's Thread" hidden>
|
||||
<!-- Ariadne's thread: a spiral of semicircles (radii 1 → 5,
|
||||
centres alternating 8 / 9) unwinding into a loose tail. -->
|
||||
|
|
@ -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 <img> 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 `<img src="${s.replace(/"/g,""")}" alt="" style="width:${size}px;height:${size}px;display:block;margin:0 auto">`;
|
||||
return s.replace(/&/g,"&").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 <img> 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 = `<img src="${b.icon.replace(/"/g,""")}" alt="" style="width:18px;height:18px;display:block;margin:0 auto">`;
|
||||
} else {
|
||||
iconHtml = b.icon.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||||
}
|
||||
return `<button class="extbtn${cls}" ${dataAttr} title="${b.title.replace(/"/g,""")}">${iconHtml}</button>`;
|
||||
return `<button class="extbtn${cls}" ${dataAttr} title="${b.title.replace(/"/g,""")}">${addonIconHtml(b.icon)}</button>`;
|
||||
}).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(`<div class="epit${isActive ? " active" : ""}" data-panel="${p.panelId.replace(/"/g,""")}"><span class="ic">${String(p.icon || "•").replace(/</g,"<")}</span><span class="lbl">${(p.title || "Extension").replace(/</g,"<")}</span></div>`);
|
||||
rows.push(`<div class="epit${isActive ? " active" : ""}" data-panel="${p.panelId.replace(/"/g,""")}"><span class="ic">${addonIconHtml(p.icon, 16)}</span><span class="lbl">${(p.title || "Extension").replace(/</g,"<")}</span></div>`);
|
||||
}
|
||||
for (const m of lastMenus) {
|
||||
rows.push(`<div class="epit" data-menu="${m.addonId.replace(/"/g,""")}"><span class="ic">${String(m.icon || "•").replace(/</g,"<")}</span><span class="lbl">${(m.title || m.addonId).replace(/</g,"<")}</span></div>`);
|
||||
rows.push(`<div class="epit" data-menu="${m.addonId.replace(/"/g,""")}"><span class="ic">${addonIconHtml(m.icon, 16)}</span><span class="lbl">${(m.title || m.addonId).replace(/</g,"<")}</span></div>`);
|
||||
}
|
||||
pop.innerHTML = rows.join("") || `<div class="epit" style="opacity:.6;cursor:default">No extensions installed</div>`;
|
||||
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;
|
||||
|
|
|
|||
37
main.js
37
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).
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue