mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 15:39:41 +02:00
feat: tab action icons on the active tab only, middle-click closes tabs
Rework of the previous hover-overlay approach after feedback: sliding the title under incoming icons made names hard to read, and icons appearing under the cursor caused accidental gear/close clicks while switching tabs. Now the gear/pop-out/close icons expand in flow on the ACTIVE tab only. Selection is a deliberate click, so the strip's geometry never changes while the pointer is aiming at a tab; hovering a background tab changes nothing (the full title stays readable) and a stray click can only switch sessions. Middle-click closes any tab (session tabs via the existing close-confirm modal, web tabs via closeWebviewTab), matching browser muscle memory so background tabs still close in one action. The pop-out button stays opt-in via App Settings -> Tab Bar (per-device showTabDetachButton, default off), and a detached tab keeps its icon as the re-focus affordance. Phone layouts already used the active-only pattern; tablets keep their always-visible touch fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+15
-57
@@ -827,7 +827,7 @@ class CodemanApp {
|
||||
this.applyLocalization();
|
||||
this.applyTabWrapSettings();
|
||||
this.applyMonitorVisibility();
|
||||
this._setupTabHoverSlide();
|
||||
this._setupTabMiddleClickClose();
|
||||
// Must run before the first session:created can arrive: markSessionTabEntering()
|
||||
// ignores ids until this sets up its state, which is what keeps the tabs
|
||||
// restored on page load from animating.
|
||||
@@ -3549,59 +3549,23 @@ class CodemanApp {
|
||||
container.classList.toggle('tabs-auto-wrap', shouldWrap);
|
||||
}
|
||||
|
||||
// Tab hover reveal (hover-capable devices): the tab keeps its width — the
|
||||
// .tab-actions overlay slides in over the right edge while the title (and any
|
||||
// badges) slide left just far enough to clear it, clipped at the LEFT edge of
|
||||
// .tab-info so the name's tail stays readable. CSS owns the reveal itself
|
||||
// (:hover/:focus-within in styles.css); JS only computes the per-tab slide
|
||||
// distance (--tab-slide), because text width is unknowable in CSS and short
|
||||
// names should barely move.
|
||||
_setupTabHoverSlide() {
|
||||
// Middle-click closes a tab, mirroring browser tab strips. Session tabs go
|
||||
// through requestCloseSession (the same confirm modal as the x button), web
|
||||
// tabs through closeWebviewTab (same as theirs). Delegated on the container:
|
||||
// tabs are re-rendered wholesale, the container is stable.
|
||||
_setupTabMiddleClickClose() {
|
||||
const container = this.$('sessionTabs');
|
||||
if (!container || this._tabHoverSlideBound) return;
|
||||
this._tabHoverSlideBound = true;
|
||||
const enter = (e) => {
|
||||
if (!container || this._tabAuxClickBound) return;
|
||||
this._tabAuxClickBound = true;
|
||||
container.addEventListener('auxclick', (e) => {
|
||||
if (e.button !== 1) return;
|
||||
const tab = e.target.closest?.('.session-tab');
|
||||
if (!tab || !container.contains(tab)) return;
|
||||
// Moving within the tab: skip unless the slide was cleared mid-hover
|
||||
// (focusout can clear it while the pointer never left).
|
||||
if (e.relatedTarget && tab.contains(e.relatedTarget) && tab.style.getPropertyValue('--tab-slide')) return;
|
||||
this._applyTabHoverSlide(tab);
|
||||
};
|
||||
const leave = (e) => {
|
||||
const tab = e.target.closest?.('.session-tab');
|
||||
if (!tab || (e.relatedTarget && tab.contains(e.relatedTarget))) return;
|
||||
if (e.type === 'focusout' && tab.matches(':hover')) return; // pointer still owns the reveal
|
||||
tab.style.removeProperty('--tab-slide');
|
||||
};
|
||||
container.addEventListener('pointerover', enter);
|
||||
container.addEventListener('pointerout', leave);
|
||||
// Keyboard path: tabbing onto an icon reveals the overlay via
|
||||
// .tab-actions:has(:focus-visible). Clicking a tab also focuses it
|
||||
// (tabindex=0), so only focus INSIDE the actions wrapper may trigger the
|
||||
// slide — else every click would slide the title with no icons showing.
|
||||
container.addEventListener('focusin', (e) => {
|
||||
if (e.target.closest?.('.tab-actions')) enter(e);
|
||||
if (!tab) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (tab.dataset.id) this.requestCloseSession(tab.dataset.id);
|
||||
else if (tab.dataset.webviewId) this.closeWebviewTab?.(tab.dataset.webviewId);
|
||||
});
|
||||
container.addEventListener('focusout', leave);
|
||||
}
|
||||
|
||||
_applyTabHoverSlide(tab) {
|
||||
const actions = tab.querySelector('.tab-actions');
|
||||
// Zero width = overlay inactive (touch layout keeps icons in-flow) or no icons.
|
||||
if (!actions || !actions.offsetWidth) return;
|
||||
// offsetLeft/offsetWidth are pre-transform layout values, and offsetParent is
|
||||
// the tab itself (position: relative), so both sides share the same origin.
|
||||
let contentRight = 0;
|
||||
for (const child of tab.children) {
|
||||
if (child === actions) continue;
|
||||
const cl = child.classList;
|
||||
if (cl.contains('tab-number') || cl.contains('tab-status') || cl.contains('tab-load-spinner') || cl.contains('tab-web-icon')) continue;
|
||||
contentRight = Math.max(contentRight, child.offsetLeft + child.offsetWidth);
|
||||
}
|
||||
const shift = Math.min(Math.max(0, contentRight + 4 - actions.offsetLeft), actions.offsetWidth + 12);
|
||||
if (shift > 0) tab.style.setProperty('--tab-slide', shift + 'px');
|
||||
else tab.style.removeProperty('--tab-slide');
|
||||
}
|
||||
|
||||
_fullRenderSessionTabs() {
|
||||
@@ -3686,12 +3650,6 @@ class CodemanApp {
|
||||
// Set up keyboard navigation for tabs
|
||||
this.setupTabKeyboardNavigation(container);
|
||||
|
||||
// A rebuild replaces a hovered tab mid-hover (SSE-driven renders): the fresh
|
||||
// element matches :hover immediately but its inline --tab-slide is gone, so
|
||||
// re-derive it or the title stops clearing the action icons.
|
||||
const hoveredTab = container.querySelector('.session-tab:hover');
|
||||
if (hoveredTab) this._applyTabHoverSlide(hoveredTab);
|
||||
|
||||
// Update connection lines after tabs change (positions may have shifted)
|
||||
this.updateConnectionLines();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user