mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
feat(desktop): hover X close button on zone tabs
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.
This commit is contained in:
parent
b1201213b7
commit
b281134fdd
2 changed files with 83 additions and 3 deletions
|
|
@ -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(
|
||||
<PaneTab onClose={onClose}>
|
||||
<PaneTabLabel>tab</PaneTabLabel>
|
||||
</PaneTab>
|
||||
)
|
||||
|
||||
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(
|
||||
<PaneTab onClose={onClose} onPointerDown={onTabPointerDown}>
|
||||
<PaneTabLabel>tab</PaneTabLabel>
|
||||
</PaneTab>
|
||||
)
|
||||
|
||||
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(
|
||||
<PaneTab onClose={onClose} vertical>
|
||||
<PaneTabLabel>tab</PaneTabLabel>
|
||||
</PaneTab>
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Close tab' })).toBeNull()
|
||||
})
|
||||
|
||||
it('does not render a close button without onClose', () => {
|
||||
render(
|
||||
<PaneTab>
|
||||
<PaneTabLabel>tab</PaneTabLabel>
|
||||
</PaneTab>
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Close tab' })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement, PaneTabProps>(function P
|
|||
{...props}
|
||||
>
|
||||
{children}
|
||||
{dirty && (
|
||||
{onClose && !vertical && (
|
||||
<button
|
||||
aria-label="Close tab"
|
||||
className={cn(
|
||||
'grid size-4 shrink-0 place-items-center self-center rounded-sm text-(--ui-text-tertiary) transition-opacity',
|
||||
// Always reserve the slot; visible on hover. The dirty dot
|
||||
// (below) yields to the X on hover so the two never overlap.
|
||||
// Opacity transitions keep the layout stable (no width shift
|
||||
// when the X appears).
|
||||
'mr-1.5 opacity-0 group-hover/tab:opacity-100',
|
||||
'hover:bg-(--ui-control-hover-background) hover:text-foreground'
|
||||
)}
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
onClose()
|
||||
}}
|
||||
onPointerDown={event => {
|
||||
// Claim the press so the tab's drag/activate pointerdown handler
|
||||
// can't fire — the X is a leaf action, never a drag start.
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Codicon name="close" size="0.625rem" />
|
||||
</button>
|
||||
)}
|
||||
{dirty && !(onClose && !vertical) && (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue