fix(app): restore tab layout consistency (#33804)

This commit is contained in:
Brendan Allan 2026-06-25 15:00:12 +08:00 committed by GitHub
parent 717b740d24
commit b44af11e03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 73 additions and 21 deletions

View file

@ -56,6 +56,28 @@ describe("titlebar tab drag", () => {
expect(captureTabDragLayout(list, ["a", "b"]).dividerWidth).toBe(13)
list.remove()
})
test("uses the list gap as the divider width", () => {
const list = document.createElement("div")
const first = document.createElement("div")
const second = document.createElement("div")
const firstTab = document.createElement("div")
const secondTab = document.createElement("div")
first.dataset.titlebarTabSlot = ""
first.dataset.tabKey = "a"
second.dataset.titlebarTabSlot = ""
second.dataset.tabKey = "b"
firstTab.dataset.titlebarTab = ""
secondTab.dataset.titlebarTab = ""
first.append(firstTab)
second.append(secondTab)
list.append(first, second)
list.style.columnGap = "13.5px"
document.body.append(list)
expect(captureTabDragLayout(list, ["a", "b"]).dividerWidth).toBe(13.5)
list.remove()
})
})
describe("titlebar tab gestures", () => {

View file

@ -29,6 +29,7 @@ export function captureTabDragLayout(list: HTMLElement, order: string[]) {
let dividerWidth = 0
if (order.length >= 2) {
const gap = Number.parseFloat(getComputedStyle(list).columnGap) || 0
const secondId = order[1]
for (const slot of slots) {
if (slot.dataset.tabKey !== secondId) continue
@ -36,10 +37,11 @@ export function captureTabDragLayout(list: HTMLElement, order: string[]) {
if (!tab) break
const style = getComputedStyle(slot)
dividerWidth =
gap ||
slot.getBoundingClientRect().width -
tab.getBoundingClientRect().width +
(Number.parseFloat(style.marginLeft) || 0) +
(Number.parseFloat(style.marginRight) || 0)
tab.getBoundingClientRect().width +
(Number.parseFloat(style.marginLeft) || 0) +
(Number.parseFloat(style.marginRight) || 0)
break
}
}

View file

@ -9,6 +9,29 @@
justify-content: center;
}
[data-slot="titlebar-tabs"] {
container-type: inline-size;
}
[data-titlebar-tab-list] {
gap: calc(min(12px, 2cqw) + 1.5px);
}
[data-titlebar-tab-slot] {
position: relative;
}
[data-titlebar-tab-slot]:not(:first-child)::before {
content: "";
position: absolute;
top: 8px;
left: calc(0px - min(6px, 1cqw) - 0.75px);
width: 1.5px;
height: 12px;
border-radius: 9999px;
background: var(--v2-background-bg-layer-02);
}
[data-titlebar-tab] [data-slot="tab-close"]::before {
content: "";
position: absolute;
@ -62,8 +85,14 @@
display: none;
}
[data-titlebar-tab]:hover [data-slot="tab-close"],
[data-titlebar-tab][data-active="true"] [data-slot="tab-close"],
[data-titlebar-tab][data-editing="true"] [data-slot="tab-close"] {
background: var(--tab-bg);
@container (max-width: 64px) {
[data-titlebar-tab-link] {
justify-content: center;
gap: 0;
padding-inline: 0;
}
[data-titlebar-tab-title] {
display: none;
}
}

View file

@ -178,7 +178,7 @@ export function TabNavItem(props: {
data-slot="titlebar-tab-item"
data-title-overflow={titleOverflowing()}
data-editing={editing()}
class="group relative flex h-7 min-w-24 max-w-60 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
class="group relative flex h-7 w-full min-w-0 max-w-56 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [container-type:inline-size] [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
classList={{ invisible: props.hidden }}
data-active={props.active}
data-dragging={props.dragging}
@ -193,6 +193,7 @@ export function TabNavItem(props: {
return (
<a
data-slot="tab-link"
data-titlebar-tab-link
href={props.href}
draggable={false}
onDragStart={(event) => {
@ -221,6 +222,7 @@ export function TabNavItem(props: {
titleEl.textContent = session().title
}}
data-slot="tab-title"
data-titlebar-tab-title
class="min-w-0 flex-1 outline-none leading-4"
classList={{
"overflow-hidden text-clip whitespace-nowrap": !editing(),
@ -297,7 +299,7 @@ export function DraftTabItem(props: {
data-active={props.active}
data-dragging={props.dragging}
data-pressed={props.pressed}
class="group relative shrink-0 flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] pl-1.5 pr-8 whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-overlay-simple-overlay-pressed)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
class="group relative flex h-7 w-full min-w-0 max-w-56 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] pl-1.5 pr-8 [container-type:inline-size] whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-overlay-simple-overlay-pressed)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true'][data-pressed='true']:[--tab-bg:var(--v2-overlay-simple-overlay-pressed)]"
classList={{ invisible: props.hidden }}
onMouseDown={(event) => {
if (event.button !== 1) return
@ -306,6 +308,7 @@ export function DraftTabItem(props: {
>
<a
data-slot="tab-link"
data-titlebar-tab-link
href={props.href}
draggable={false}
onDragStart={(event) => {
@ -322,7 +325,9 @@ export function DraftTabItem(props: {
<span class="flex size-4 shrink-0 items-center justify-center">
<IconV2 name="edit" />
</span>
<span class="truncate leading-5">{props.title}</span>
<span data-titlebar-tab-title class="truncate leading-5">
{props.title}
</span>
</a>
<div data-slot="tab-close" class="absolute right-0 inset-y-0 flex w-7 items-center justify-center">
<IconButtonV2

View file

@ -45,7 +45,6 @@ import {
function SessionTabSlot(props: {
tab: SessionTab
id: string
first: () => boolean
active: () => boolean
activeServerKey: ServerConnection.Key
forceTruncate: boolean
@ -104,10 +103,9 @@ function SessionTabSlot(props: {
<div
data-titlebar-tab-slot
data-tab-key={props.id}
class="flex shrink-0"
class="flex min-w-0 max-w-56 flex-1 basis-0"
classList={{
hidden: !session(),
"ml-1.5 border-l border-[var(--v2-background-bg-layer-02)] pl-1.5": !props.first(),
"pointer-events-none": props.dragActive,
}}
onPointerDown={props.onPointerDown}
@ -461,17 +459,16 @@ export function TitlebarTabStrip(props: {
return (
<>
<div data-slot="titlebar-tabs" class="relative min-w-0">
<div data-slot="titlebar-tabs" class="relative min-w-0 flex-1">
<div
data-slot="titlebar-tabs-scroll"
class="flex min-w-0 flex-row items-center gap-1.5 overflow-x-auto no-scrollbar [app-region:no-drag]"
ref={scrollRef}
>
<div class="flex min-w-0 flex-row items-center" ref={listRef}>
<div data-titlebar-tab-list class="flex w-full min-w-0 flex-row items-center" ref={listRef}>
<For each={displayTabs()}>
{(tab, index) => {
const id = tabKey(tab)
const first = () => index() === 0
let ref!: HTMLDivElement
useTabShortcut(index, () => props.onNavigate(tab, ref))
@ -487,7 +484,6 @@ export function TitlebarTabStrip(props: {
<SessionTabSlot
tab={tab}
id={id}
first={first}
active={() => props.currentTab() === tab}
activeServerKey={props.activeServerKey}
forceTruncate={props.forceTruncate}
@ -510,9 +506,8 @@ export function TitlebarTabStrip(props: {
<div
data-titlebar-tab-slot
data-tab-key={id}
class="flex shrink-0"
class="flex min-w-0 max-w-56 flex-1 basis-0"
classList={{
"ml-1.5 border-l border-[var(--v2-background-bg-layer-02)] pl-1.5": !first(),
"pointer-events-none": drag.active,
}}
onPointerDown={(event) => {

View file

@ -481,7 +481,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
/>
</TooltipV2>
</Show>
<div class="flex-1" />
<TitlebarV2Right state={v2RightState()} />
<Show when={windows() && !electronWindows()}>
<div data-tauri-decorum-tb class="flex flex-row" />

View file

@ -252,7 +252,7 @@ export function SessionComposerRegion(props: {
</Show>
<div
classList={{
"relative z-10": true,
"relative z-30": true,
}}
style={{
"margin-top": `${-lift()}px`,