Skip to content

Commit c7badcb

Browse files
feat(tabs): uniform responsive tab widths (180px cap, equal shrink) (#596)
Give every tab in the tab bar a uniform, responsive width instead of content-driven sizing with a hard 5rem title cap: - TabBar: each sortable tab wrapper gets w-[180px] min-w-[100px] shrink, so tabs are 180px when space allows and shrink equally to a 100px floor before the strip scrolls (single-row and multirow modes). The DragOverlay preview wrapper gets w-[180px] to match. - TabItem: tab root is w-full min-w-0; title span uses flex-1 min-w-0 truncate (close button pinned to the right edge); icons, status dot, and close button are shrink-0; rename input uses flex-1 min-w-0. - Tests: title-width-class test asserts the new flexible classes. Verified: 121 tests pass across TabItem, TabBar, overflow, multirow, a11y, and mobile suites. Generated with Amplifier Co-authored-by: Amplifier <[email protected]>
1 parent 35fbf13 commit c7badcb

3 files changed

Lines changed: 23 additions & 10 deletions

File tree

src/components/TabBar.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,15 @@ function SortableTab({
121121
)
122122

123123
return (
124-
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
124+
<div
125+
ref={setNodeRef}
126+
style={style}
127+
{...attributes}
128+
{...listeners}
129+
// Uniform tab width: 180px when space allows, shrinking equally
130+
// (never below 100px) before the strip starts scrolling.
131+
className="w-[180px] min-w-[100px] shrink"
132+
>
125133
<TabItem
126134
tab={tabWithDisplayTitle}
127135
isActive={isActive}
@@ -585,6 +593,7 @@ export default function TabBar({ sidebarCollapsed, onToggleSidebar }: TabBarProp
585593
<DragOverlay>
586594
{activeTab ? (
587595
<div
596+
className="w-[180px]"
588597
style={{
589598
opacity: 0.9,
590599
transform: 'scale(1.02)',

src/components/TabItem.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function StatusDot({ status, busy }: { status: TerminalStatus; busy?: boolean })
2121
// `busy` is already the authoritative per-pane busy aggregate (busyPaneIds);
2222
// do NOT AND it with the last-writer-wins tab.status, which a sibling pane's
2323
// 'exited' can clobber and wrongly suppress blue.
24-
return <Circle className={cn('h-2 w-2', busy ? 'fill-blue-500 text-blue-500' : getTerminalStatusDotClassName(status))} />
24+
return <Circle className={cn('h-2 w-2 shrink-0', busy ? 'fill-blue-500 text-blue-500' : getTerminalStatusDotClassName(status))} />
2525
}
2626

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

128128
return (
129-
<span className="flex items-center gap-0.5">
129+
<span className="flex shrink-0 items-center gap-0.5">
130130
{groups.map((group) => (
131131
<span key={group.key} className="flex items-center gap-0.5">
132132
{group.info && repoIconKeys.has(group.key) && (
@@ -158,7 +158,7 @@ export default function TabItem({
158158
const tabContent = (
159159
<div
160160
className={cn(
161-
'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',
161+
'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',
162162
isActive
163163
? cn(
164164
"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-['']",
@@ -201,22 +201,22 @@ export default function TabItem({
201201
{isRenaming ? (
202202
<input
203203
ref={inputRef}
204-
className="bg-transparent outline-none w-32 text-sm"
204+
className="bg-transparent outline-none flex-1 min-w-0 text-sm"
205205
value={renameValue}
206206
onChange={(e) => onRenameChange(e.target.value)}
207207
onBlur={onRenameBlur}
208208
onKeyDown={onRenameKeyDown}
209209
onClick={(e) => e.stopPropagation()}
210210
/>
211211
) : (
212-
<span className="whitespace-nowrap truncate text-sm max-w-[5rem]">
212+
<span className="flex-1 min-w-0 whitespace-nowrap truncate text-sm">
213213
{tab.title}
214214
</span>
215215
)}
216216

217217
<button
218218
className={cn(
219-
'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',
219+
'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',
220220
isActive
221221
? 'opacity-60 hover:opacity-100'
222222
: 'opacity-0 group-hover:opacity-60 hover:!opacity-100'

test/unit/client/components/TabItem.test.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,14 +336,18 @@ describe('TabItem', () => {
336336
expect(onDoubleClick).toHaveBeenCalled()
337337
})
338338

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

344346
rerender(<TabItem {...defaultProps} isActive={true} />)
345347
title = screen.getByText('Test Tab')
346-
expect(title.className).toContain('max-w-[5rem]')
348+
expect(title.className).toContain('flex-1')
349+
expect(title.className).toContain('min-w-0')
350+
expect(title.className).toContain('truncate')
347351
})
348352

349353
it('does not vertically offset inactive tabs', () => {

0 commit comments

Comments
 (0)