diff --git a/chrome.html b/chrome.html
index 109ce4b..4708acf 100644
--- a/chrome.html
+++ b/chrome.html
@@ -50,6 +50,28 @@
.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; }
+ /* Drag hover state — a tab being dragged onto a group chip. */
+ .gchip.droptarget { background: rgba(214,255,61,.15); border-color: var(--acid); }
+ /* Vertical popover: click a collapsed chip → list its member tabs. */
+ .grouppop { position: fixed; z-index: 9999; background: var(--surface, #1c222c); border: 1px solid var(--line2);
+ border-radius: 10px; box-shadow: 0 12px 34px #000c; padding: 4px; min-width: 220px; max-width: 360px;
+ font-size: 13px; color: var(--ink); }
+ .grouppop .gph { padding: 6px 10px 4px; font-size: 11px; color: var(--dim); letter-spacing: .08em; text-transform: uppercase;
+ display: flex; align-items: center; gap: 8px; }
+ .grouppop .gph .gcdot { width: 8px; height: 8px; border-radius: 50%; }
+ .grouppop .gpit { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 6px; cursor: pointer;
+ white-space: nowrap; overflow: hidden; }
+ .grouppop .gpit:hover { background: var(--hover, #ffffff10); }
+ .grouppop .gpit.active { background: rgba(214,255,61,.08); color: var(--acid); }
+ .grouppop .gpit .gfav { width: 14px; height: 14px; flex: none; object-fit: contain; border-radius: 2px; }
+ .grouppop .gpit .gt { overflow: hidden; text-overflow: ellipsis; flex: 1; }
+ .grouppop .gpit .gx { opacity: .5; padding: 0 4px; font-size: 12px; }
+ .grouppop .gpit:hover .gx { opacity: .9; }
+ .grouppop .gpit .gx:hover { color: #f6768a; opacity: 1; }
+ @media (prefers-color-scheme: light) {
+ .grouppop { background: #ffffff; border-color: rgba(0,0,0,.15); }
+ .grouppop .gpit:hover { background: rgba(0,0,0,.05); }
+ }
.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; }
@@ -595,6 +617,11 @@
$("back").disabled = !d.canBack; $("fwd").disabled = !d.canForward;
// 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
+ // reset the chrome view height to the natural body-scrollHeight — this
+ // eliminates the "chrome stays huge after a menu action" gap that could
+ // otherwise linger if a menu-close path missed syncHeight().
+ if (!document.querySelector(".ctxmenu, .grouppop")) syncHeight();
const rb = $("reload");
if (d.loading) { rb.innerHTML = STOP_SVG; rb.title = "Stop"; rb.onclick = () => T.stop(); }
else { rb.innerHTML = RELOAD_SVG; rb.title = "Reload (Shift-click: hard reload)"; rb.onclick = (ev) => T.reload(ev.shiftKey); }
@@ -632,10 +659,36 @@
return out;
}).join("") +
`+`;
- // Group chips toggle collapsed state.
- box.querySelectorAll(".gchip").forEach((chip) => chip.onclick = (ev) => {
- ev.stopPropagation();
- T.tabGroupToggle && T.tabGroupToggle(chip.dataset.group);
+ // dragId is shared between tab-reorder handlers and group-chip drop
+ // handlers; declared here so both scopes see the same identity.
+ let dragId = null;
+ // Group chip clicks:
+ // collapsed chip → show a vertical popover listing the group's tabs
+ // expanded chip → toggle to collapsed
+ // Also acts as a drop target: drag any tab onto a chip to assign it
+ // to that group.
+ box.querySelectorAll(".gchip").forEach((chip) => {
+ chip.onclick = (ev) => {
+ ev.stopPropagation();
+ const color = chip.dataset.group;
+ if (chip.classList.contains("collapsed")) {
+ openGroupPopover(chip, color, d);
+ } else {
+ T.tabGroupToggle && T.tabGroupToggle(color);
+ }
+ };
+ chip.addEventListener("dragover", (ev) => {
+ if (dragId == null) return;
+ ev.preventDefault(); ev.dataTransfer.dropEffect = "move";
+ chip.classList.add("droptarget");
+ });
+ chip.addEventListener("dragleave", () => chip.classList.remove("droptarget"));
+ chip.addEventListener("drop", (ev) => {
+ ev.preventDefault();
+ chip.classList.remove("droptarget");
+ if (dragId != null) T.tabGroup(dragId, chip.dataset.group);
+ dragId = null;
+ });
});
box.querySelectorAll(".tab").forEach((el) => el.onclick = (e) => {
if (e.target.dataset.close) T.closeTab(Number(e.target.dataset.close));
@@ -652,7 +705,7 @@
}));
// Drag-reorder — HTML5 drag events. Drop-side chosen by whether the pointer
// is on the left or right half of the target tab (matches Chrome UX).
- let dragId = null;
+ // dragId already declared above so group-chip drop targets share it.
box.querySelectorAll(".tab").forEach((row) => {
row.addEventListener("dragstart", (e) => {
dragId = Number(row.dataset.id);
@@ -693,11 +746,54 @@
{ id: "purple", label: "Purple" },
];
function closeAllMenus() {
- document.querySelectorAll(".ctxmenu").forEach((m) => m.remove());
+ document.querySelectorAll(".ctxmenu, .grouppop").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();
}
+ // Vertical dropdown of a collapsed group's tabs. Click a row → switch to
+ // that tab (and expand the group so the newly-active tab is visible in
+ // the strip). The X on a row closes just that tab. Popover closes on
+ // outside click via the shared document listener at the bottom.
+ function openGroupPopover(chip, color, d) {
+ closeAllMenus();
+ const rect = chip.getBoundingClientRect();
+ const pop = document.createElement("div");
+ pop.className = "grouppop";
+ pop.dataset.group = color;
+ const groupTabs = d.tabs.filter((t) => t.group === color);
+ const header = document.createElement("div");
+ header.className = "gph";
+ header.innerHTML = '' + color + ' group · ' + groupTabs.length + ' tab' + (groupTabs.length === 1 ? '' : 's') + '';
+ pop.appendChild(header);
+ for (const t of groupTabs) {
+ const row = document.createElement("div");
+ row.className = "gpit" + (t.active ? " active" : "");
+ const fav = t.favicon
+ ? '
'
+ : '';
+ row.innerHTML = fav + '' + String(t.title || "New Tab").replace(/✕';
+ row.addEventListener("click", (ev) => {
+ if (ev.target.dataset.close) {
+ ev.stopPropagation();
+ T.closeTab(Number(ev.target.dataset.close));
+ // Rebuild popover after close (next onTabs update will refresh anyway).
+ return;
+ }
+ // Switch to this tab and expand the group so it's visible in the strip.
+ T.switchTab(t.id);
+ if (T.tabGroupToggle) T.tabGroupToggle(color); // was collapsed, now expand
+ closeAllMenus();
+ });
+ pop.appendChild(row);
+ }
+ pop.style.left = Math.round(rect.left) + "px";
+ pop.style.top = Math.round(rect.bottom + 4) + "px";
+ document.body.appendChild(pop);
+ const r = pop.getBoundingClientRect();
+ if (r.right > window.innerWidth) pop.style.left = Math.max(4, window.innerWidth - r.width - 4) + "px";
+ growChromeForMenu();
+ }
function openTabContextMenu(x, y, t) {
closeAllMenus();
const m = document.createElement("div");
@@ -736,10 +832,10 @@
growChromeForMenu();
}
function growChromeForMenu() {
- // Wait a frame so all newly-added menus contribute to the bounding box.
+ // Wait a frame so all newly-added overlays contribute to the bounding box.
requestAnimationFrame(() => {
let needed = document.body.scrollHeight;
- for (const m of document.querySelectorAll(".ctxmenu")) {
+ for (const m of document.querySelectorAll(".ctxmenu, .grouppop")) {
const r = m.getBoundingClientRect();
needed = Math.max(needed, Math.ceil(r.bottom) + 4);
}
@@ -769,7 +865,7 @@
}
document.addEventListener("click", (e) => {
// Close any open menus on outside click.
- if (!e.target.closest(".ctxmenu")) closeAllMenus();
+ if (!e.target.closest(".ctxmenu, .grouppop, .gchip")) closeAllMenus();
});
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeAllMenus(); });
diff --git a/package.json b/package.json
index bbaa846..a1a7b6d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "theseus-navigator",
- "version": "0.3.4",
+ "version": "0.3.5",
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
"author": "Silent Mode",
"main": "main.js",