Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/components/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,15 @@ function SortableTab({
)

return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
// Uniform tab width: 180px when space allows, shrinking equally
// (never below 100px) before the strip starts scrolling.
className="w-[180px] min-w-[100px] shrink"
>
<TabItem
tab={tabWithDisplayTitle}
isActive={isActive}
Expand Down Expand Up @@ -585,6 +593,7 @@ export default function TabBar({ sidebarCollapsed, onToggleSidebar }: TabBarProp
<DragOverlay>
{activeTab ? (
<div
className="w-[180px]"
style={{
opacity: 0.9,
transform: 'scale(1.02)',
Expand Down
12 changes: 6 additions & 6 deletions src/components/TabItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function StatusDot({ status, busy }: { status: TerminalStatus; busy?: boolean })
// `busy` is already the authoritative per-pane busy aggregate (busyPaneIds);
// do NOT AND it with the last-writer-wins tab.status, which a sibling pane's
// 'exited' can clobber and wrongly suppress blue.
return <Circle className={cn('h-2 w-2', busy ? 'fill-blue-500 text-blue-500' : getTerminalStatusDotClassName(status))} />
return <Circle className={cn('h-2 w-2 shrink-0', busy ? 'fill-blue-500 text-blue-500' : getTerminalStatusDotClassName(status))} />
}

/** Max pane-type icons shown per tab; panes beyond this fold into the '+N' badge. */
Expand Down Expand Up @@ -126,7 +126,7 @@ export default function TabItem({
)

return (
<span className="flex items-center gap-0.5">
<span className="flex shrink-0 items-center gap-0.5">
{groups.map((group) => (
<span key={group.key} className="flex items-center gap-0.5">
{group.info && repoIconKeys.has(group.key) && (
Expand Down Expand Up @@ -158,7 +158,7 @@ export default function TabItem({
const tabContent = (
<div
className={cn(
'group relative flex items-center gap-2 h-8 px-3 rounded-t-md border-x border-t border-muted-foreground/45 text-sm cursor-pointer transition-colors',
'group relative flex w-full min-w-0 items-center gap-2 h-8 px-3 rounded-t-md border-x border-t border-muted-foreground/45 text-sm cursor-pointer transition-colors',
isActive
? cn(
"z-30 border-b border-b-background bg-background text-foreground after:pointer-events-none after:absolute after:inset-x-0 after:-bottom-px after:h-[2px] after:bg-background after:content-['']",
Expand Down Expand Up @@ -201,22 +201,22 @@ export default function TabItem({
{isRenaming ? (
<input
ref={inputRef}
className="bg-transparent outline-none w-32 text-sm"
className="bg-transparent outline-none flex-1 min-w-0 text-sm"
value={renameValue}
onChange={(e) => onRenameChange(e.target.value)}
onBlur={onRenameBlur}
onKeyDown={onRenameKeyDown}
onClick={(e) => e.stopPropagation()}
/>
) : (
<span className="whitespace-nowrap truncate text-sm max-w-[5rem]">
<span className="flex-1 min-w-0 whitespace-nowrap truncate text-sm">
{tab.title}
</span>
)}

<button
className={cn(
'ml-0.5 p-0.5 min-h-11 min-w-11 md:min-h-0 md:min-w-0 flex items-center justify-center rounded transition-opacity',
'ml-0.5 p-0.5 min-h-11 min-w-11 md:min-h-0 md:min-w-0 flex shrink-0 items-center justify-center rounded transition-opacity',
isActive
? 'opacity-60 hover:opacity-100'
: 'opacity-0 group-hover:opacity-60 hover:!opacity-100'
Expand Down
10 changes: 7 additions & 3 deletions test/unit/client/components/TabItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -336,14 +336,18 @@ describe('TabItem', () => {
expect(onDoubleClick).toHaveBeenCalled()
})

it('uses the same title width class for active and inactive tabs', () => {
it('uses the same flexible title width classes for active and inactive tabs', () => {
const { rerender } = render(<TabItem {...defaultProps} isActive={false} />)
let title = screen.getByText('Test Tab')
expect(title.className).toContain('max-w-[5rem]')
expect(title.className).toContain('flex-1')
expect(title.className).toContain('min-w-0')
expect(title.className).toContain('truncate')

rerender(<TabItem {...defaultProps} isActive={true} />)
title = screen.getByText('Test Tab')
expect(title.className).toContain('max-w-[5rem]')
expect(title.className).toContain('flex-1')
expect(title.className).toContain('min-w-0')
expect(title.className).toContain('truncate')
})

it('does not vertically offset inactive tabs', () => {
Expand Down
Loading