From b281134fdd9aa0fbd0306a2a7b8d5ab4ec38feeb Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Wed, 22 Jul 2026 10:00:55 -0400 Subject: [PATCH] feat(desktop): hover X close button on zone tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a hover-to-close X button to PaneTab (the fancy-zones tab shell). The button slot is always reserved inline (shrink-0) so the tab width stays stable whether the X is visible or not — no layout shift on hover. Visible on group-hover/tab, hidden by default via opacity transition. The dirty dot yields to the X when both are present (closeable + dirty): the X wins on hover, the dot shows otherwise. Vertical tabs skip the X (writing-mode:vertical-rl makes an inline button awkward) and keep the absolute-positioned dirty dot. Pointerdown on the X is stopped so the tab's drag/activate handlers never fire — the X is a leaf close action, not a drag start. --- .../src/components/ui/pane-tab.test.tsx | 51 +++++++++++++++++++ apps/desktop/src/components/ui/pane-tab.tsx | 35 +++++++++++-- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/components/ui/pane-tab.test.tsx b/apps/desktop/src/components/ui/pane-tab.test.tsx index c36f03fb8ada..60943b85b848 100644 --- a/apps/desktop/src/components/ui/pane-tab.test.tsx +++ b/apps/desktop/src/components/ui/pane-tab.test.tsx @@ -90,3 +90,54 @@ describe('PaneTab close gestures', () => { expect(onPointerDown).toHaveBeenCalledTimes(1) }) }) + +describe('PaneTab hover close button', () => { + it('renders a close button when onClose is set on a horizontal tab', () => { + const onClose = vi.fn() + render( + + tab + + ) + + expect(screen.getByRole('button', { name: 'Close tab' })).toBeTruthy() + }) + + it('clicking the close button calls onClose and stops propagation', () => { + const onClose = vi.fn() + const onTabPointerDown = vi.fn() + render( + + tab + + ) + + const closeBtn = screen.getByRole('button', { name: 'Close tab' }) + fireEvent.pointerDown(closeBtn) + fireEvent.click(closeBtn) + expect(onClose).toHaveBeenCalledTimes(1) + // The tab's own pointerdown handler must NOT fire — the X is a leaf action. + expect(onTabPointerDown).not.toHaveBeenCalled() + }) + + it('does not render a close button on vertical tabs', () => { + const onClose = vi.fn() + render( + + tab + + ) + + expect(screen.queryByRole('button', { name: 'Close tab' })).toBeNull() + }) + + it('does not render a close button without onClose', () => { + render( + + tab + + ) + + expect(screen.queryByRole('button', { name: 'Close tab' })).toBeNull() + }) +}) diff --git a/apps/desktop/src/components/ui/pane-tab.tsx b/apps/desktop/src/components/ui/pane-tab.tsx index da8baae2aec9..3e3b4851ec20 100644 --- a/apps/desktop/src/components/ui/pane-tab.tsx +++ b/apps/desktop/src/components/ui/pane-tab.tsx @@ -1,5 +1,6 @@ import * as React from 'react' +import { Codicon } from '@/components/ui/codicon' import { cn } from '@/lib/utils' /** Inset bottom stroke for a horizontal tab strip — titlebar color, cut by the active tab. */ @@ -27,8 +28,8 @@ const TAB_IDLE = interface PaneTabProps extends React.ComponentProps<'div'> { active?: boolean dirty?: boolean - /** Close gesture, no hover X (too easy to hit on small tabs): middle-click, - * or ⌘-click as the trackpad-friendly Mac equivalent. */ + /** Close gesture: hover X on horizontal tabs, plus middle-click and ⌘-click + * (the trackpad-friendly Mac equivalent). */ onClose?: () => void /** Vertical rail form (collapsed sidebar zones). */ vertical?: boolean @@ -127,7 +128,35 @@ export const PaneTab = React.forwardRef(function P {...props} > {children} - {dirty && ( + {onClose && !vertical && ( + + )} + {dirty && !(onClose && !vertical) && (