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",