diff --git a/chrome.html b/chrome.html
index 2a03174..9a0188b 100644
--- a/chrome.html
+++ b/chrome.html
@@ -36,7 +36,14 @@
content means scrollHeight is the natural chrome height, and syncHeight
shrinks correctly after every menu-close. */
body { margin: 0; background: var(--bg); color: var(--ink); user-select: none; overflow: hidden; }
- .tabs { display: flex; align-items: flex-end; gap: 4px; padding: 6px 8px 0; height: 34px; overflow: hidden; }
+ /* The tab row doubles as the window's title bar on Windows (main.js hides
+ the native frame): the row is a drag region, everything clickable in it
+ opts out, and --wco-reserve keeps tabs clear of the native minimise /
+ maximise / close overlay (set from navigator.windowControlsOverlay, or a
+ fixed 140px when that API isn't exposed). */
+ .tabs { display: flex; align-items: flex-end; gap: 4px; padding: 6px 8px 0; padding-right: calc(8px + var(--wco-reserve, 0px));
+ height: 34px; overflow: hidden; -webkit-app-region: drag; }
+ .tabs > *, .grouppop, .ctxmenu { -webkit-app-region: no-drag; }
/* Same-size tabs: each tab claims an equal share of the row, capped at
200px so 2 tabs don't stretch across the whole window. min-width lets
many tabs shrink cleanly while still showing a couple letters. */
@@ -46,7 +53,12 @@
.tab.dragging { opacity: .45; cursor: grabbing; }
.tab.dropbefore { box-shadow: -2px 0 0 var(--acid); }
.tab.dropafter { box-shadow: 2px 0 0 var(--acid); }
- .tab.active { background: var(--active); color: var(--ink); }
+ /* The selected tab: brighter fill plus an accent stripe and outline, so it
+ reads at a glance among a dozen same-size tabs. A grouped tab keeps its
+ group colour on the stripe and still gets the accent outline. */
+ .tab.active { background: var(--active); color: var(--ink); border-color: rgb(from var(--acid) r g b / .7);
+ box-shadow: inset 0 2px 0 var(--acid); }
+ .tab.active.grp { box-shadow: inset 0 2px 0 var(--gc, var(--acid)); }
.tab .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.tab .fav { width: 14px; height: 14px; flex: none; border-radius: 2px; object-fit: contain; }
.tab .spin { width: 10px; height: 10px; flex: none; border: 1.5px solid #ffffff2e; border-top-color: var(--acid); border-radius: 50%; animation: spin .7s linear infinite; }
@@ -467,6 +479,24 @@
const $ = (id) => document.getElementById(id);
const T = window.theseus;
function syncHeight() { requestAnimationFrame(() => T.setChromeHeight(document.body.scrollHeight)); }
+ // Reserve room for the native window-controls overlay at the right of the
+ // tab row (Windows only — the frame is hidden there, see main.js). Use the
+ // overlay's real geometry when Chromium exposes it to this view; otherwise
+ // the standard three 46px buttons.
+ (function reserveWindowControls() {
+ if (!/Windows/i.test(navigator.userAgent)) return;
+ const apply = () => {
+ const wco = navigator.windowControlsOverlay;
+ let reserve = 140;
+ try {
+ if (wco && wco.visible) { const r = wco.getTitlebarAreaRect(); reserve = Math.max(0, Math.round(window.innerWidth - (r.x + r.width))); }
+ } catch {}
+ document.documentElement.style.setProperty("--wco-reserve", reserve + "px");
+ };
+ apply();
+ try { navigator.windowControlsOverlay?.addEventListener("geometrychange", apply); } catch {}
+ window.addEventListener("resize", apply);
+ })();
const RELOAD_SVG = ``;
const STOP_SVG = ``;
diff --git a/main.js b/main.js
index bd32c46..14bfbb9 100644
--- a/main.js
+++ b/main.js
@@ -2800,8 +2800,18 @@ function createTab(initial, opts = {}) {
try { wc.setWebRTCIPHandlingPolicy(webrtcPolicy()); } catch {}
try { wc.setBackgroundThrottling(settings.backgroundThrottle); } catch {}
applyFingerprint(wc);
- const tab = { id, view, title: opts.settings ? "Settings" : "New Tab", url: "", favicon: null, prov: null, settings: !!opts.settings, muted: false, group: null };
- tabs.push(tab);
+ const tab = { id, view, title: opts.settings ? "Settings" : "New Tab", url: "", favicon: null, prov: null, settings: !!opts.settings, muted: false, group: null, openerId: opts.after ?? null };
+ // A tab opened from a link goes right after the tab it came from — and
+ // after any siblings that tab already opened, so a run of links from one
+ // page reads left-to-right — instead of at the far end of the strip.
+ // Everything else (+ button, restore, add-on requests) appends as before.
+ const openerIdx = opts.after != null ? tabs.findIndex((t) => t.id === opts.after) : -1;
+ if (openerIdx < 0) tabs.push(tab);
+ else {
+ let at = openerIdx + 1;
+ while (at < tabs.length && tabs[at].openerId === opts.after) at++;
+ tabs.splice(at, 0, tab);
+ }
win.contentView.addChildView(view);
wc.on("page-title-updated", (_e, title) => {
tab.title = title; emitTabs();
@@ -2910,7 +2920,7 @@ function createTab(initial, opts = {}) {
});
// Links that open a new tab: target="_blank", window.open, Ctrl/middle-click.
wc.setWindowOpenHandler(({ url, disposition }) => {
- if (url && url !== "about:blank") createTab(url, { background: disposition === "background-tab" });
+ if (url && url !== "about:blank") createTab(url, { background: disposition === "background-tab", after: tab.id });
return { action: "deny" };
});
// Right-click context menu.
@@ -2918,8 +2928,8 @@ function createTab(initial, opts = {}) {
const items = [];
if (p.linkURL) {
items.push(
- { label: "Open link in new tab", click: () => createTab(p.linkURL) },
- { label: "Open link in new background tab", click: () => createTab(p.linkURL, { background: true }) },
+ { label: "Open link in new tab", click: () => createTab(p.linkURL, { after: tab.id }) },
+ { label: "Open link in new background tab", click: () => createTab(p.linkURL, { background: true, after: tab.id }) },
{ label: "Open link in new window", click: () => openLinkWindow(p.linkURL) },
{ label: "Copy link address", click: () => clipboard.writeText(p.linkURL) },
{ type: "separator" },
@@ -2930,7 +2940,7 @@ function createTab(initial, opts = {}) {
// preset savePath, so Electron shows the native Save As dialog.
if (p.mediaType === "image" && p.srcURL) {
items.push(
- { label: "Open image in new tab", click: () => createTab(p.srcURL) },
+ { label: "Open image in new tab", click: () => createTab(p.srcURL, { after: tab.id }) },
{ label: "Save image as…", click: () => wc.downloadURL(p.srcURL) },
{ label: "Copy image", click: () => { try { wc.copyImageAt(p.x, p.y); } catch {} } },
{ label: "Copy image address", click: () => clipboard.writeText(p.srcURL) },
@@ -2947,7 +2957,7 @@ function createTab(initial, opts = {}) {
if (raw) {
const excerpt = raw.length > 40 ? raw.slice(0, 40) + "…" : raw;
items.push(
- { label: `Search for "${excerpt.replace(/&/g, "&&")}"`, click: () => createTab(SEARCH(raw)) },
+ { label: `Search for "${excerpt.replace(/&/g, "&&")}"`, click: () => createTab(SEARCH(raw), { after: tab.id }) },
{ type: "separator" },
);
}
@@ -3000,14 +3010,28 @@ function createWindow() {
// until chrome.html painted, which is the "white strip over a dark
// window" users saw on a slow launch.
const uiBg = nativeTheme.shouldUseDarkColors ? "#0f1420" : "#e6e8ec";
+ // On Windows the tab strip lives in the title bar: the frame is hidden and
+ // the native minimise/maximise/close buttons are drawn as an overlay over
+ // our chrome, whose tab row is a drag region (chrome.html). That returns
+ // the OS title bar's height to the page. Kept native elsewhere.
+ const titleBarOverlay = { color: uiBg, symbolColor: nativeTheme.shouldUseDarkColors ? "#e7eaf1" : "#1a1f28", height: 40 };
+ const frameOpts = process.platform === "win32" ? { titleBarStyle: "hidden", titleBarOverlay } : {};
win = new BrowserWindow({
- width: 1220, height: 840, title: "Theseus Navigator", backgroundColor: uiBg,
+ width: 1220, height: 840, title: "Theseus Navigator", backgroundColor: uiBg, ...frameOpts,
// Taskbar / titlebar icon. Packaged builds ship build/icon.ico as
// extraResource; dev reads the source file directly.
icon: app.isPackaged
? path.join(process.resourcesPath, "icon.ico")
: path.join(__dirname, "build", "icon.ico"),
});
+ // Keep the overlay buttons on the theme when it flips at runtime.
+ if (process.platform === "win32") {
+ nativeTheme.on("updated", () => {
+ if (!win || win.isDestroyed()) return;
+ const dark = nativeTheme.shouldUseDarkColors;
+ try { win.setTitleBarOverlay({ color: dark ? "#0f1420" : "#e6e8ec", symbolColor: dark ? "#e7eaf1" : "#1a1f28", height: 40 }); } catch {}
+ });
+ }
chrome = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "preload.js") } });
try { chrome.setBackgroundColor(uiBg); } catch {}
win.contentView.addChildView(chrome);
@@ -3332,7 +3356,7 @@ ipcMain.handle("tab-context-menu-popup", (e, tabId, rect) => {
const bmDisabled = !t.url;
const template = [
{ label: "Reload", click: () => { try { t.view.webContents.reload(); } catch {} } },
- { label: "Duplicate", click: () => { if (t.url) createTab(t.url); else createTab(); } },
+ { label: "Duplicate", click: () => { if (t.url) createTab(t.url, { after: t.id }); else createTab(null, { after: t.id }); } },
{ label: "Group", submenu: groupSubmenu },
{ label: "Add to Bookmarks", enabled: !bmDisabled, click: () => {
if (bmDisabled) return;