Theseus: link-hover status bar (bottom-left href pill)
Firefox / Chrome-style small pill at the bottom-left of the window showing the href when the pointer hovers a link, hidden when the pointer leaves. Fed by webContents.update-target-url on every tab; auto-sizes to its text via an IPC resize channel. Cleared on tab switch so a lingering hover pill from tab A doesn't carry across to tab B. Positioned by layout() so it tracks window resizes. New files link-status.html + link-status-preload.js registered in build.files (GOTCHAS.md rule: missing entries silently omit from the packaged app). Not shipped yet — bundles into the next Theseus release.
This commit is contained in:
parent
cc563b0037
commit
78d20d6197
4 changed files with 71 additions and 0 deletions
5
link-status-preload.js
Normal file
5
link-status-preload.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
contextBridge.exposeInMainWorld("linkstatus", {
|
||||||
|
onUrl: (cb) => ipcRenderer.on("link-status-url", (_e, url) => cb(url)),
|
||||||
|
resize: (w, h) => ipcRenderer.invoke("link-status-resize", w, h),
|
||||||
|
});
|
||||||
22
link-status.html
Normal file
22
link-status.html
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html><head><meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
|
||||||
|
html, body { margin: 0; background: transparent; overflow: hidden; }
|
||||||
|
.pill { background: #1c222c; border: 1px solid #ffffff26; border-top: none; border-left: none;
|
||||||
|
border-radius: 0 8px 0 0; box-shadow: 0 -2px 12px #0006;
|
||||||
|
color: #b9c2d0; font-size: 11.5px; padding: 4px 10px 4px 8px;
|
||||||
|
display: inline-block; max-width: 100%; overflow: hidden;
|
||||||
|
text-overflow: ellipsis; white-space: nowrap; direction: ltr; }
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
.pill { background: #ffffff; color: #3c4453; border-color: rgba(0,0,0,.15); }
|
||||||
|
}
|
||||||
|
</style></head>
|
||||||
|
<body><span class="pill" id="p"></span>
|
||||||
|
<script>
|
||||||
|
const p = document.getElementById("p");
|
||||||
|
window.linkstatus.onUrl((url) => {
|
||||||
|
p.textContent = String(url || "");
|
||||||
|
requestAnimationFrame(() => { try { window.linkstatus.resize(p.offsetWidth + 2, p.offsetHeight + 2); } catch (e) {} });
|
||||||
|
});
|
||||||
|
</script></body></html>
|
||||||
42
main.js
42
main.js
|
|
@ -916,6 +916,12 @@ let apW = 520; let apH = 60;
|
||||||
// site has matching credentials.
|
// site has matching credentials.
|
||||||
let pwFillPop, pwfVisible = false, pwfPos = { x: 8, y: 90 };
|
let pwFillPop, pwfVisible = false, pwfPos = { x: 8, y: 90 };
|
||||||
const PWF_W = 280; let pwfH = 80;
|
const PWF_W = 280; let pwfH = 80;
|
||||||
|
// Link-hover status bar — small pill at the bottom-left of the window
|
||||||
|
// showing the href when the mouse hovers a link (Chrome / Firefox style).
|
||||||
|
// Hidden when hover leaves. Fed by webContents.update-target-url on every
|
||||||
|
// tab; the pill auto-sizes to its text.
|
||||||
|
let linkStatus, linkStatusVisible = false;
|
||||||
|
let linkStatusW = 100, linkStatusH = 22;
|
||||||
// In-memory download list. Not persisted: closing the browser clears history
|
// In-memory download list. Not persisted: closing the browser clears history
|
||||||
// (the files are still on disk; only the list of "recent downloads" is dropped).
|
// (the files are still on disk; only the list of "recent downloads" is dropped).
|
||||||
const downloads = []; let nextDlId = 1; const dlItems = new Map(); // id -> DownloadItem
|
const downloads = []; let nextDlId = 1; const dlItems = new Map(); // id -> DownloadItem
|
||||||
|
|
@ -951,6 +957,7 @@ function layout() {
|
||||||
positionDownloads();
|
positionDownloads();
|
||||||
positionAddressPicker();
|
positionAddressPicker();
|
||||||
positionPwFill();
|
positionPwFill();
|
||||||
|
if (linkStatusVisible) positionLinkStatus();
|
||||||
}
|
}
|
||||||
function positionPopover() {
|
function positionPopover() {
|
||||||
if (!popover) return;
|
if (!popover) return;
|
||||||
|
|
@ -1013,6 +1020,26 @@ function positionPwFill() {
|
||||||
const x = Math.max(6, Math.min(pwfPos.x, width - PWF_W - 6));
|
const x = Math.max(6, Math.min(pwfPos.x, width - PWF_W - 6));
|
||||||
pwFillPop.setBounds({ x, y: pwfPos.y, width: PWF_W, height: pwfH });
|
pwFillPop.setBounds({ x, y: pwfPos.y, width: PWF_W, height: pwfH });
|
||||||
}
|
}
|
||||||
|
function positionLinkStatus() {
|
||||||
|
if (!linkStatus || !win) return;
|
||||||
|
const { width, height } = win.getContentBounds();
|
||||||
|
const w = Math.min(Math.max(120, linkStatusW), Math.max(200, width - 20));
|
||||||
|
const h = Math.max(20, linkStatusH);
|
||||||
|
linkStatus.setBounds({ x: 0, y: Math.max(0, height - h), width: w, height: h });
|
||||||
|
}
|
||||||
|
function showLinkStatus(url) {
|
||||||
|
if (!linkStatus) return;
|
||||||
|
const s = String(url || "");
|
||||||
|
if (!s) {
|
||||||
|
if (linkStatusVisible) { linkStatus.setVisible(false); linkStatusVisible = false; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
positionLinkStatus();
|
||||||
|
// Raise the pill above any tab view that was added after it.
|
||||||
|
try { win.contentView.removeChildView(linkStatus); win.contentView.addChildView(linkStatus); } catch {}
|
||||||
|
linkStatus.setVisible(true); linkStatusVisible = true;
|
||||||
|
try { linkStatus.webContents.send("link-status-url", s); } catch {}
|
||||||
|
}
|
||||||
function showPwFill(show, matches) {
|
function showPwFill(show, matches) {
|
||||||
if (!pwFillPop) return;
|
if (!pwFillPop) return;
|
||||||
if (show) {
|
if (show) {
|
||||||
|
|
@ -1134,6 +1161,7 @@ function setActive(id) {
|
||||||
activeId = id;
|
activeId = id;
|
||||||
if (popVisible) showPopover(false); // don't carry a stale popover across tabs
|
if (popVisible) showPopover(false); // don't carry a stale popover across tabs
|
||||||
if (epVisible) showEnginePicker(false);
|
if (epVisible) showEnginePicker(false);
|
||||||
|
if (linkStatusVisible) showLinkStatus(""); // clear any lingering hover pill
|
||||||
for (const t of tabs) t.view.setVisible(t.id === id);
|
for (const t of tabs) t.view.setVisible(t.id === id);
|
||||||
const t = activeTab();
|
const t = activeTab();
|
||||||
if (t?.prov) pushNav(t.prov);
|
if (t?.prov) pushNav(t.prov);
|
||||||
|
|
@ -1197,6 +1225,9 @@ function createTab(initial, opts = {}) {
|
||||||
wc.on("did-navigate-in-page", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); });
|
wc.on("did-navigate-in-page", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); });
|
||||||
wc.on("did-start-loading", () => setLoading(tab, true));
|
wc.on("did-start-loading", () => setLoading(tab, true));
|
||||||
wc.on("did-stop-loading", () => setLoading(tab, false));
|
wc.on("did-stop-loading", () => setLoading(tab, false));
|
||||||
|
// Firefox / Chrome-style bottom-left link preview: fires with the href
|
||||||
|
// when the pointer enters/leaves an anchor. Empty string = no hover.
|
||||||
|
wc.on("update-target-url", (_e, url) => { if (tab.id === activeId) showLinkStatus(url); });
|
||||||
wc.on("will-navigate", (e, u) => {
|
wc.on("will-navigate", (e, u) => {
|
||||||
try {
|
try {
|
||||||
// Skip our own programmatic loads. fallbackToWeb calls loadURL("https://<host>/")
|
// Skip our own programmatic loads. fallbackToWeb calls loadURL("https://<host>/")
|
||||||
|
|
@ -1328,6 +1359,12 @@ function createWindow() {
|
||||||
win.contentView.addChildView(pwFillPop);
|
win.contentView.addChildView(pwFillPop);
|
||||||
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).
|
||||||
|
linkStatus = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "link-status-preload.js") } });
|
||||||
|
try { linkStatus.setBackgroundColor("#00000000"); } catch {}
|
||||||
|
win.contentView.addChildView(linkStatus);
|
||||||
|
linkStatus.webContents.loadFile("link-status.html");
|
||||||
|
linkStatus.setVisible(false);
|
||||||
chrome.webContents.once("did-finish-load", () => {
|
chrome.webContents.once("did-finish-load", () => {
|
||||||
const saved = settings.restoreSession ? loadSession() : [];
|
const saved = settings.restoreSession ? loadSession() : [];
|
||||||
if (saved.length) saved.forEach((u) => createTab(u)); else createTab();
|
if (saved.length) saved.forEach((u) => createTab(u)); else createTab();
|
||||||
|
|
@ -1801,6 +1838,11 @@ ipcMain.handle("toggle-pw-fill", async (_e, rect) => {
|
||||||
showPwFill(true, matches);
|
showPwFill(true, matches);
|
||||||
});
|
});
|
||||||
ipcMain.handle("close-pw-fill", () => showPwFill(false));
|
ipcMain.handle("close-pw-fill", () => showPwFill(false));
|
||||||
|
ipcMain.handle("link-status-resize", (_e, w, h) => {
|
||||||
|
linkStatusW = Math.max(60, Math.min(2000, Math.round(w) || 100));
|
||||||
|
linkStatusH = Math.max(20, Math.min(60, Math.round(h) || 22));
|
||||||
|
if (linkStatusVisible) positionLinkStatus();
|
||||||
|
});
|
||||||
ipcMain.handle("pw-fill-resize", (_e, h) => {
|
ipcMain.handle("pw-fill-resize", (_e, h) => {
|
||||||
pwfH = Math.max(60, Math.min(300, Math.round(h) || 80));
|
pwfH = Math.max(60, Math.min(300, Math.round(h) || 80));
|
||||||
if (pwfVisible) positionPwFill();
|
if (pwfVisible) positionPwFill();
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@
|
||||||
"pw-fill.html",
|
"pw-fill.html",
|
||||||
"pw-fill-preload.js",
|
"pw-fill-preload.js",
|
||||||
"home-preload.js",
|
"home-preload.js",
|
||||||
|
"link-status.html",
|
||||||
|
"link-status-preload.js",
|
||||||
"collision.html",
|
"collision.html",
|
||||||
"collision-preload.js",
|
"collision-preload.js",
|
||||||
"messages.html",
|
"messages.html",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue