Ship Theseus 0.3.3 e016d665 (fix tab menu clip + always-on bookmarks + real tab groups)
Setup e016d66543976b7a4df5ac16033319d59cb0f418e54d7e838ef16de274525a83 Portable b9881a62c39ecfe22644611f5aa16a48f32f97f9ec5751b5888ccbd870338c3e Three 0.3.2 misses fixed: Tab context menu was clipped by the chrome view's fixed height — the menu rendered with position:fixed inside a WebContentsView whose bottom edge stops at CHROME_H, so items below the tab strip weren't visible. New growChromeForMenu() measures every open menu's bounding box and calls T.setChromeHeight(needed) so main.js grows the chrome view to fit; closeAllMenus() calls syncHeight() to restore. Bookmarks bar was hidden on every tab except Home, so Add-to- Bookmarks had no on-screen feedback. Now always visible. Tab groups now actually cluster. Assigning a color to a tab moves it adjacent to its group-mates in the tabs array (main-side splice, no render-only trick). The chrome renderer inserts a colored group chip before the first tab of each group; clicking the chip toggles a per-group collapsed state (in-memory, resets on relaunch). Collapsed groups hide their member tabs and the chip shows the member count. Expanded groups keep the per-tab top color stripe from 0.3.2 so the cluster reads as one visual block. Deployed: scp + sia-upload, verified LIVE 0.3.3 on dl.silentmode.st.
This commit is contained in:
parent
5ea4515085
commit
3bf41f92ae
4 changed files with 100 additions and 16 deletions
82
chrome.html
82
chrome.html
|
|
@ -39,13 +39,24 @@
|
|||
even when tabs are next to each other. */
|
||||
.tab .gdot { width: 8px; height: 8px; flex: none; border-radius: 50%; }
|
||||
.tab.grp { box-shadow: inset 0 2px 0 var(--gc, transparent); }
|
||||
.tab.g-red, .gdot.g-red { --gc: #f6768a; } .gdot.g-red { background: #f6768a; }
|
||||
.tab.g-orange, .gdot.g-orange { --gc: #ffa96a; } .gdot.g-orange { background: #ffa96a; }
|
||||
.tab.g-yellow, .gdot.g-yellow { --gc: #ffd44f; } .gdot.g-yellow { background: #ffd44f; }
|
||||
.tab.g-green, .gdot.g-green { --gc: #4fd1a5; } .gdot.g-green { background: #4fd1a5; }
|
||||
.tab.g-cyan, .gdot.g-cyan { --gc: #4fd1e5; } .gdot.g-cyan { background: #4fd1e5; }
|
||||
.tab.g-blue, .gdot.g-blue { --gc: #4b7bec; } .gdot.g-blue { background: #4b7bec; }
|
||||
.tab.g-purple, .gdot.g-purple { --gc: #b39ddb; } .gdot.g-purple { background: #b39ddb; }
|
||||
/* Group chip that appears before the first tab of a group. Clicking
|
||||
collapses the group (hides its member tabs) and shows a count. */
|
||||
.gchip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px;
|
||||
background: var(--surface); border: 1px solid var(--line2); border-bottom: none;
|
||||
border-radius: 8px 8px 0 0; cursor: pointer; height: 28px;
|
||||
font: 12px/1 system-ui, sans-serif; color: var(--mut);
|
||||
box-shadow: inset 0 2px 0 var(--gc, transparent); }
|
||||
.gchip:hover { color: var(--ink); }
|
||||
.gchip .gcdot { width: 10px; height: 10px; border-radius: 50%; background: var(--gc); flex: none; }
|
||||
.gchip .gccnt { color: var(--ink); font-weight: 600; }
|
||||
.gchip.collapsed { padding: 4px 10px; }
|
||||
.tab.g-red, .gdot.g-red, .gcdot.g-red { --gc: #f6768a; } .gdot.g-red { background: #f6768a; }
|
||||
.tab.g-orange, .gdot.g-orange, .gcdot.g-orange { --gc: #ffa96a; } .gdot.g-orange { background: #ffa96a; }
|
||||
.tab.g-yellow, .gdot.g-yellow, .gcdot.g-yellow { --gc: #ffd44f; } .gdot.g-yellow { background: #ffd44f; }
|
||||
.tab.g-green, .gdot.g-green, .gcdot.g-green { --gc: #4fd1a5; } .gdot.g-green { background: #4fd1a5; }
|
||||
.tab.g-cyan, .gdot.g-cyan, .gcdot.g-cyan { --gc: #4fd1e5; } .gdot.g-cyan { background: #4fd1e5; }
|
||||
.tab.g-blue, .gdot.g-blue, .gcdot.g-blue { --gc: #4b7bec; } .gdot.g-blue { background: #4b7bec; }
|
||||
.tab.g-purple, .gdot.g-purple, .gcdot.g-purple { --gc: #b39ddb; } .gdot.g-purple { background: #b39ddb; }
|
||||
/* Submenu popover for the tab context menu (Group → color). */
|
||||
.ctxmenu .mi.sub { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.ctxmenu .mi.sub::after { content: "▸"; color: var(--dim); font-size: 11px; }
|
||||
|
|
@ -573,17 +584,39 @@
|
|||
current.title = active ? active.title : "";
|
||||
updateStar();
|
||||
const box = $("tabs");
|
||||
// Render tabs with group awareness. For every group, emit a chip BEFORE
|
||||
// the group's first tab. If the group is collapsed, tabs inside it are
|
||||
// hidden and the chip shows the count as "[● 3]"; expanded chips show
|
||||
// just the color dot.
|
||||
const collapsed = new Set(d.collapsedGroups || []);
|
||||
const groupsSeen = new Set();
|
||||
const groupsCount = {};
|
||||
for (const t of d.tabs) if (t.group) groupsCount[t.group] = (groupsCount[t.group] || 0) + 1;
|
||||
box.innerHTML = d.tabs.map((t) => {
|
||||
let out = "";
|
||||
// Chip appears once, at the FIRST tab of the group in render order.
|
||||
if (t.group && !groupsSeen.has(t.group)) {
|
||||
groupsSeen.add(t.group);
|
||||
const isCollapsed = collapsed.has(t.group);
|
||||
out += `<button class="gchip g-${t.group}${isCollapsed ? " collapsed" : ""}" data-group="${t.group}" title="${isCollapsed ? "Expand" : "Collapse"} ${t.group} group"><span class="gcdot g-${t.group}"></span>${isCollapsed ? `<span class="gccnt">${groupsCount[t.group]}</span>` : ""}</button>`;
|
||||
}
|
||||
// Skip rendering the tab itself if its group is collapsed.
|
||||
if (t.group && collapsed.has(t.group)) return out;
|
||||
// Loading spinner takes the icon slot while a page is loading, then
|
||||
// hands it back to the favicon once page-favicon-updated fires.
|
||||
const icon = t.loading
|
||||
? '<span class="spin"></span>'
|
||||
: (t.favicon ? `<img class="fav" src="${String(t.favicon).replace(/"/g,""")}" onerror="this.remove()">` : '');
|
||||
const groupDot = t.group ? `<span class="gdot g-${t.group}" title="Group: ${t.group}"></span>` : "";
|
||||
const mute = t.muted ? `<span class="mute" title="Muted">🔇</span>` : "";
|
||||
return `<div class="tab ${t.active ? "active" : ""}${t.group ? " grp g-" + t.group : ""}" data-id="${t.id}" draggable="true">${groupDot}${icon}<span class="t">${(t.title||"New Tab").replace(/</g,"<")}</span>${mute}<span class="x" data-close="${t.id}">✕</span></div>`;
|
||||
out += `<div class="tab ${t.active ? "active" : ""}${t.group ? " grp g-" + t.group : ""}" data-id="${t.id}" draggable="true">${icon}<span class="t">${(t.title||"New Tab").replace(/</g,"<")}</span>${mute}<span class="x" data-close="${t.id}">✕</span></div>`;
|
||||
return out;
|
||||
}).join("") +
|
||||
`<span class="newtab" id="newtab">+</span>`;
|
||||
// Group chips toggle collapsed state.
|
||||
box.querySelectorAll(".gchip").forEach((chip) => chip.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
T.tabGroupToggle && T.tabGroupToggle(chip.dataset.group);
|
||||
});
|
||||
box.querySelectorAll(".tab").forEach((el) => el.onclick = (e) => {
|
||||
if (e.target.dataset.close) T.closeTab(Number(e.target.dataset.close));
|
||||
else T.switchTab(Number(el.dataset.id));
|
||||
|
|
@ -639,7 +672,12 @@
|
|||
{ id: "cyan", label: "Cyan" }, { id: "blue", label: "Blue" },
|
||||
{ id: "purple", label: "Purple" },
|
||||
];
|
||||
function closeAllMenus() { document.querySelectorAll(".ctxmenu").forEach((m) => m.remove()); }
|
||||
function closeAllMenus() {
|
||||
document.querySelectorAll(".ctxmenu").forEach((m) => m.remove());
|
||||
// Menus were rendered outside the chrome's normal flow (position:fixed),
|
||||
// so closing them means the natural chrome height wins again.
|
||||
syncHeight();
|
||||
}
|
||||
function openTabContextMenu(x, y, t) {
|
||||
closeAllMenus();
|
||||
const m = document.createElement("div");
|
||||
|
|
@ -670,10 +708,23 @@
|
|||
const sep = document.createElement("div"); sep.className = "sep"; m.appendChild(sep);
|
||||
m.appendChild(item("Close", "danger", () => T.closeTab(t.id)));
|
||||
document.body.appendChild(m);
|
||||
// Clamp inside viewport
|
||||
// Clamp horizontally to the viewport. VERTICALLY: the chrome view has a
|
||||
// fixed height (main.js CHROME_H), which clips anything below that line.
|
||||
// Grow the chrome view so the menu is fully visible — restored on close.
|
||||
const r = m.getBoundingClientRect();
|
||||
if (r.right > window.innerWidth) m.style.left = Math.max(4, window.innerWidth - r.width - 4) + "px";
|
||||
if (r.bottom > window.innerHeight) m.style.top = Math.max(4, window.innerHeight - r.height - 4) + "px";
|
||||
growChromeForMenu();
|
||||
}
|
||||
function growChromeForMenu() {
|
||||
// Wait a frame so all newly-added menus contribute to the bounding box.
|
||||
requestAnimationFrame(() => {
|
||||
let needed = document.body.scrollHeight;
|
||||
for (const m of document.querySelectorAll(".ctxmenu")) {
|
||||
const r = m.getBoundingClientRect();
|
||||
needed = Math.max(needed, Math.ceil(r.bottom) + 4);
|
||||
}
|
||||
T.setChromeHeight(needed);
|
||||
});
|
||||
}
|
||||
function openGroupSubmenu(x, y, t) {
|
||||
document.querySelectorAll(".ctxmenu.sub2").forEach((m) => m.remove());
|
||||
|
|
@ -694,7 +745,7 @@
|
|||
document.body.appendChild(m);
|
||||
const r = m.getBoundingClientRect();
|
||||
if (r.right > window.innerWidth) m.style.left = Math.max(4, x - r.width - 6) + "px";
|
||||
if (r.bottom > window.innerHeight) m.style.top = Math.max(4, window.innerHeight - r.height - 4) + "px";
|
||||
growChromeForMenu();
|
||||
}
|
||||
document.addEventListener("click", (e) => {
|
||||
// Close any open menus on outside click.
|
||||
|
|
@ -710,7 +761,10 @@
|
|||
else if (d.host && !$("url").value) $("url").value = d.host;
|
||||
}
|
||||
// Favorites bar shows only on the new-tab / home page (hides once a page loads).
|
||||
$("bookmarks").hidden = !(!d || d.kind === "home");
|
||||
// Bookmarks bar was originally home-only; users adding a bookmark from
|
||||
// a real tab saw nothing visible. Keep it visible on every tab so the
|
||||
// add / remove has an immediate on-screen effect.
|
||||
$("bookmarks").hidden = false;
|
||||
syncHeight();
|
||||
});
|
||||
|
||||
|
|
|
|||
31
main.js
31
main.js
|
|
@ -1669,6 +1669,7 @@ function emitTabs() {
|
|||
const wc = t?.view.webContents;
|
||||
chrome?.webContents.send("tabs", {
|
||||
tabs: tabs.map((x) => ({ id: x.id, title: x.title || "New Tab", active: x.id === activeId, loading: !!x.loading, favicon: x.favicon || null, muted: !!x.muted, group: x.group || null, url: x.url || "" })),
|
||||
collapsedGroups: [...tabGroupCollapsed],
|
||||
url: t?.url || "",
|
||||
loading: !!t?.loading,
|
||||
canBack: wc ? wc.navigationHistory.canGoBack() : false,
|
||||
|
|
@ -2126,10 +2127,38 @@ ipcMain.handle("tab-group", (_e, id, color) => {
|
|||
const t = tabById(id); if (!t) return null;
|
||||
// color: null | "red" | "orange" | "yellow" | "green" | "cyan" | "blue" | "purple"
|
||||
const allowed = new Set(["red","orange","yellow","green","cyan","blue","purple"]);
|
||||
t.group = allowed.has(color) ? color : null;
|
||||
const next = allowed.has(color) ? color : null;
|
||||
t.group = next;
|
||||
// Cluster: move this tab so all same-group tabs sit contiguously. Place
|
||||
// it right after the LAST existing tab of that group; if there are no
|
||||
// other members yet, leave it in place. When a tab is removed from a
|
||||
// group (color === null) we don't reorder — the visual break is enough.
|
||||
if (next) {
|
||||
const idx = tabs.indexOf(t);
|
||||
let insertAfter = -1;
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
if (i !== idx && tabs[i].group === next) insertAfter = i;
|
||||
}
|
||||
if (insertAfter !== -1) {
|
||||
tabs.splice(idx, 1);
|
||||
const dst = insertAfter > idx ? insertAfter : insertAfter + 1;
|
||||
tabs.splice(dst, 0, t);
|
||||
}
|
||||
}
|
||||
emitTabs();
|
||||
return t.group;
|
||||
});
|
||||
// Groups get a collapsed/expanded state, per-color, in-memory only (resets
|
||||
// on relaunch). Flipping this doesn't touch tabs — the renderer just hides
|
||||
// tabs whose group is collapsed and shows the group chip in their place.
|
||||
const tabGroupCollapsed = new Set(); // colors currently collapsed
|
||||
ipcMain.handle("tab-group-toggle", (_e, color) => {
|
||||
if (!color) return false;
|
||||
if (tabGroupCollapsed.has(color)) tabGroupCollapsed.delete(color); else tabGroupCollapsed.add(color);
|
||||
// Piggy-back on emitTabs so the chrome renderer receives the change.
|
||||
emitTabs();
|
||||
return tabGroupCollapsed.has(color);
|
||||
});
|
||||
ipcMain.handle("tab-bookmark", (_e, id) => {
|
||||
const t = tabById(id); if (!t) return false;
|
||||
const url = t.url; const title = t.title || url;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "theseus-navigator",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||
"author": "Silent Mode",
|
||||
"main": "main.js",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ contextBridge.exposeInMainWorld("theseus", {
|
|||
tabDuplicate: (id) => ipcRenderer.invoke("tab-duplicate", id),
|
||||
tabMute: (id, on) => ipcRenderer.invoke("tab-mute", id, on),
|
||||
tabGroup: (id, color) => ipcRenderer.invoke("tab-group", id, color),
|
||||
tabGroupToggle: (color) => ipcRenderer.invoke("tab-group-toggle", color),
|
||||
tabBookmark: (id) => ipcRenderer.invoke("tab-bookmark", id),
|
||||
onAddressPicked: (cb) => ipcRenderer.on("address-picked", (_e, url) => cb(url)),
|
||||
onBcnrOffer: (cb) => ipcRenderer.on("bcnr-offer", (_e, d) => cb(d)),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue