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:
@@ -1739,6 +1739,7 @@ export const Header: React.FC = () => {
|
|||||||
<form
|
<form
|
||||||
ref={headerRenameFormRef}
|
ref={headerRenameFormRef}
|
||||||
className="flex w-full min-w-0 items-center gap-2 leading-tight"
|
className="flex w-full min-w-0 items-center gap-2 leading-tight"
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void saveHeaderSessionRename();
|
void saveHeaderSessionRename();
|
||||||
@@ -1755,7 +1756,7 @@ export const Header: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={t('sessions.sidebar.session.menu.rename')}
|
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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -1776,36 +1777,16 @@ export const Header: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</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}
|
{isNewSessionDraftOpen ? t('sessions.switcher.draftTitle') : currentSessionTitle}
|
||||||
</span>
|
</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>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'flex h-[18px] shrink-0 items-center justify-center',
|
'flex h-[18px] shrink-0 items-center justify-center',
|
||||||
// Top-aligned only when the title has a metadata line under it;
|
// Top-aligned only when the title has a metadata line under it;
|
||||||
// alone, the title is centred and the button must follow.
|
// alone, the title is centred and the button must follow.
|
||||||
showHeaderMetaRow ? 'self-start' : 'self-center',
|
'self-center',
|
||||||
)}>
|
)}>
|
||||||
{currentSessionId && !isNewSessionDraftOpen && !isRenamingHeaderSession ? (
|
{currentSessionId && !isNewSessionDraftOpen && !isRenamingHeaderSession ? (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
|
|||||||
@@ -45,15 +45,16 @@ const ActiveTabShell: React.FC<{ id: string; children: React.ReactNode }> = ({ i
|
|||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={{ transform: DndCSS.Translate.toString(transform), transition }}
|
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-session-tab
|
||||||
|
data-active="true"
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected
|
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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
@@ -85,7 +86,8 @@ const InactiveSessionTab: React.FC<{
|
|||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={{ transform: DndCSS.Translate.toString(transform), transition }}
|
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}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
>
|
>
|
||||||
@@ -107,13 +109,19 @@ const InactiveSessionTab: React.FC<{
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group/session-tab flex h-8 max-w-[200px] cursor-pointer touch-none select-none items-center rounded-[10px] px-3',
|
'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/40 hover:text-foreground',
|
'text-muted-foreground transition-colors duration-150 hover:bg-interactive-hover hover:text-foreground',
|
||||||
menuOpen && 'bg-interactive-hover/40 text-foreground',
|
menuOpen && 'bg-interactive-hover text-foreground',
|
||||||
)}
|
)}
|
||||||
title={title}
|
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}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
||||||
@@ -124,10 +132,10 @@ const InactiveSessionTab: React.FC<{
|
|||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
onPointerDown={(event) => event.stopPropagation()}
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
className={cn(
|
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',
|
'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',
|
'group-hover/session-tab:flex group-hover/session-tab:opacity-100',
|
||||||
menuOpen && 'ml-1.5 flex h-5 w-5 opacity-100',
|
menuOpen && 'flex opacity-100',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon name="more" className="size-4" />
|
<Icon name="more" className="size-4" />
|
||||||
@@ -298,7 +306,7 @@ export const SessionTabsStrip: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={updateEdges}
|
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}
|
style={maskImage ? { maskImage, WebkitMaskImage: maskImage } : undefined}
|
||||||
>
|
>
|
||||||
<DndContext
|
<DndContext
|
||||||
@@ -315,7 +323,8 @@ export const SessionTabsStrip: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
<div
|
<div
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected
|
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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1781,3 +1781,25 @@ html.desktop-runtime [class*="cursor-pointer"] {
|
|||||||
html.desktop-runtime .markdown-content [data-openchamber-file-link="true"] {
|
html.desktop-runtime .markdown-content [data-openchamber-file-link="true"] {
|
||||||
cursor: default;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user