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.
|
||||
let pwFillPop, pwfVisible = false, pwfPos = { x: 8, y: 90 };
|
||||
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
|
||||
// (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
|
||||
|
|
@ -951,6 +957,7 @@ function layout() {
|
|||
positionDownloads();
|
||||
positionAddressPicker();
|
||||
positionPwFill();
|
||||
if (linkStatusVisible) positionLinkStatus();
|
||||
}
|
||||
function positionPopover() {
|
||||
if (!popover) return;
|
||||
|
|
@ -1013,6 +1020,26 @@ function positionPwFill() {
|
|||
const x = Math.max(6, Math.min(pwfPos.x, width - PWF_W - 6));
|
||||
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) {
|
||||
if (!pwFillPop) return;
|
||||
if (show) {
|
||||
|
|
@ -1134,6 +1161,7 @@ function setActive(id) {
|
|||
activeId = id;
|
||||
if (popVisible) showPopover(false); // don't carry a stale popover across tabs
|
||||
if (epVisible) showEnginePicker(false);
|
||||
if (linkStatusVisible) showLinkStatus(""); // clear any lingering hover pill
|
||||
for (const t of tabs) t.view.setVisible(t.id === id);
|
||||
const t = activeTab();
|
||||
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-start-loading", () => setLoading(tab, true));
|
||||
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) => {
|
||||
try {
|
||||
// Skip our own programmatic loads. fallbackToWeb calls loadURL("https://<host>/")
|
||||
|
|
@ -1328,6 +1359,12 @@ function createWindow() {
|
|||
win.contentView.addChildView(pwFillPop);
|
||||
pwFillPop.webContents.loadFile("pw-fill.html");
|
||||
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", () => {
|
||||
const saved = settings.restoreSession ? loadSession() : [];
|
||||
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);
|
||||
});
|
||||
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) => {
|
||||
pwfH = Math.max(60, Math.min(300, Math.round(h) || 80));
|
||||
if (pwfVisible) positionPwFill();
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@
|
|||
"pw-fill.html",
|
||||
"pw-fill-preload.js",
|
||||
"home-preload.js",
|
||||
"link-status.html",
|
||||
"link-status-preload.js",
|
||||
"collision.html",
|
||||
"collision-preload.js",
|
||||
"messages.html",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue