style(header): session tabs follow the titlebar tab design language

Uniform compact tabs (h-7, 6px radius, 13px medium) that share the strip
width and shrink before scrolling, thin separators between inactive
neighbours that disappear around the active or hovered tab, a
hover-revealed menu button anchored to the tab's end, a soft selection
pill for the active tab, and hidden scrollbars with fade edges. The
active tab is single-line: the project/branch meta row stays only in the
VS Code header, which keeps the plain title.
This commit is contained in:
Bohdan Triapitsyn
2026-08-24 17:41:34 +03:00
parent a3813f57e9
commit 447e43eac5
3 changed files with 47 additions and 35 deletions
+4 -23
View File
@@ -1739,6 +1739,7 @@ export const Header: React.FC = () => {
<form
ref={headerRenameFormRef}
className="flex w-full min-w-0 items-center gap-2 leading-tight"
onPointerDown={(event) => event.stopPropagation()}
onSubmit={(event) => {
event.preventDefault();
void saveHeaderSessionRename();
@@ -1755,7 +1756,7 @@ export const Header: React.FC = () => {
}
}}
placeholder={t('sessions.sidebar.session.menu.rename')}
className="min-w-0 flex-1 bg-transparent typography-ui-label text-[14px] font-normal leading-tight outline-none placeholder:text-muted-foreground"
className="min-w-0 flex-1 bg-transparent text-[13px] font-medium leading-4 outline-none placeholder:text-muted-foreground"
/>
<button
type="submit"
@@ -1776,36 +1777,16 @@ export const Header: React.FC = () => {
</button>
</form>
) : (
<span className="truncate typography-ui-label text-[14px] font-normal leading-tight text-foreground max-w-full">
<span className="truncate text-[13px] font-medium leading-4 text-foreground max-w-full">
{isNewSessionDraftOpen ? t('sessions.switcher.draftTitle') : currentSessionTitle}
</span>
)}
{showHeaderMetaRow ? (
<span className="flex min-w-0 max-w-full items-center gap-1.5 truncate typography-micro text-[10.5px] font-normal leading-tight text-muted-foreground/75">
{activeProjectLabel ? <span className="truncate">{activeProjectLabel}</span> : null}
{currentBranchLabel ? (
<span className="inline-flex min-w-0 items-center gap-0.5">
<Icon name="git-branch" className="h-3 w-3 flex-shrink-0 text-muted-foreground/70" />
<span className="truncate">{currentBranchLabel}</span>
</span>
) : null}
{!isNewSessionDraftOpen && worktreeBadgeKind ? (
<span className={cn(
"inline-flex min-w-0 items-center gap-0.5",
worktreeBadgeKind === 'attention' || worktreeBadgeKind === 'invalid' || worktreeBadgeKind === 'missing' ? 'text-status-warning' : 'text-muted-foreground/60'
)}>
<Icon name="alert" className="h-3 w-3 flex-shrink-0" />
<span className="truncate">{worktreeBadge}</span>
</span>
) : null}
</span>
) : null}
</div>
<div className={cn(
'flex h-[18px] shrink-0 items-center justify-center',
// Top-aligned only when the title has a metadata line under it;
// alone, the title is centred and the button must follow.
showHeaderMetaRow ? 'self-start' : 'self-center',
'self-center',
)}>
{currentSessionId && !isNewSessionDraftOpen && !isRenamingHeaderSession ? (
<DropdownMenu
@@ -45,15 +45,16 @@ const ActiveTabShell: React.FC<{ id: string; children: React.ReactNode }> = ({ i
<div
ref={setNodeRef}
style={{ transform: DndCSS.Translate.toString(transform), transition }}
className={cn('h-9 shrink-0 touch-none', isDragging && 'z-10 opacity-60')}
className={cn('session-tab-slot flex h-7 w-56 min-w-16 max-w-56 flex-shrink touch-none', isDragging && 'z-10 opacity-60')}
data-active-session-tab
data-active="true"
{...attributes}
{...listeners}
>
<div
role="tab"
aria-selected
className="flex h-9 min-w-0 max-w-[340px] items-center rounded-[10px] bg-interactive-selection px-3"
className="flex h-7 w-full min-w-0 items-center rounded-md bg-interactive-selection px-2"
>
{children}
</div>
@@ -85,7 +86,8 @@ const InactiveSessionTab: React.FC<{
<div
ref={setNodeRef}
style={{ transform: DndCSS.Translate.toString(transform), transition }}
className={cn('h-8 shrink-0', isDragging && 'z-10 opacity-60')}
className={cn('session-tab-slot flex h-7 w-56 min-w-10 max-w-56 flex-shrink', isDragging && 'z-10 opacity-60')}
data-active="false"
{...attributes}
{...listeners}
>
@@ -107,13 +109,19 @@ const InactiveSessionTab: React.FC<{
}
}}
className={cn(
'group/session-tab flex h-8 max-w-[200px] cursor-pointer touch-none select-none items-center rounded-[10px] px-3',
'text-muted-foreground transition-colors duration-150 hover:bg-interactive-hover/40 hover:text-foreground',
menuOpen && 'bg-interactive-hover/40 text-foreground',
'group/session-tab relative flex h-7 w-full min-w-0 cursor-pointer touch-none select-none items-center rounded-md px-2',
'text-muted-foreground transition-colors duration-150 hover:bg-interactive-hover hover:text-foreground',
menuOpen && 'bg-interactive-hover text-foreground',
)}
title={title}
>
<span className="min-w-0 truncate typography-ui-label text-[13px] font-normal leading-tight">
<span
className={cn(
'min-w-0 flex-1 truncate text-[13px] font-medium leading-4',
'group-hover/session-tab:pr-5',
menuOpen && 'pr-5',
)}
>
{title}
</span>
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
@@ -124,10 +132,10 @@ const InactiveSessionTab: React.FC<{
onClick={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
className={cn(
'ml-0 hidden w-0 shrink-0 items-center justify-center overflow-hidden rounded-md text-muted-foreground',
'absolute right-1 top-1/2 hidden size-5 -translate-y-1/2 items-center justify-center rounded text-muted-foreground',
'opacity-0 transition-opacity duration-150 hover:text-foreground',
'group-hover/session-tab:ml-1.5 group-hover/session-tab:flex group-hover/session-tab:h-5 group-hover/session-tab:w-5 group-hover/session-tab:opacity-100',
menuOpen && 'ml-1.5 flex h-5 w-5 opacity-100',
'group-hover/session-tab:flex group-hover/session-tab:opacity-100',
menuOpen && 'flex opacity-100',
)}
>
<Icon name="more" className="size-4" />
@@ -298,7 +306,7 @@ export const SessionTabsStrip: React.FC<{ children: React.ReactNode }> = ({ chil
<div
ref={scrollRef}
onScroll={updateEdges}
className="flex min-w-0 items-center gap-1 overflow-x-auto py-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
className="flex min-w-0 flex-1 items-center gap-1.5 overflow-x-auto overscroll-x-contain [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
style={maskImage ? { maskImage, WebkitMaskImage: maskImage } : undefined}
>
<DndContext
@@ -315,7 +323,8 @@ export const SessionTabsStrip: React.FC<{ children: React.ReactNode }> = ({ chil
<div
role="tab"
aria-selected
className="flex h-9 min-w-0 max-w-[340px] shrink-0 items-center rounded-[10px] bg-interactive-selection px-3"
className="session-tab-slot flex h-7 w-56 min-w-16 max-w-56 flex-shrink items-center rounded-md bg-interactive-selection px-2"
data-active="true"
>
{children}
</div>
+22
View File
@@ -1781,3 +1781,25 @@ html.desktop-runtime [class*="cursor-pointer"] {
html.desktop-runtime .markdown-content [data-openchamber-file-link="true"] {
cursor: default;
}
/* Header session tabs: thin separators between inactive neighbours, hidden
around the active or hovered tab (mirrors the titlebar tab language). */
.session-tab-slot {
position: relative;
}
.session-tab-slot:not(:first-child):not([data-active='true'])::before {
content: '';
position: absolute;
top: 8px;
inset-inline-start: -2.75px;
width: 1.5px;
height: 12px;
border-radius: 9999px;
background: var(--border);
opacity: 0.6;
}
.session-tab-slot[data-active='true'] + .session-tab-slot::before,
.session-tab-slot:not([data-active='true']):hover::before,
.session-tab-slot:not([data-active='true']):hover + .session-tab-slot::before {
display: none;
}