fix(desktop): fade tab separators on hover, correct their weight (MUL-4811) (#5500)

The tab strip's hairlines used --border, which is authored for near-white
surfaces: its other uses sit on --surface-raised (L=1.0) where it gives a
reasonable dL 0.055, but on --app-shell (L=0.964) it collapses to dL 0.019 --
about a quarter of the 0.068-0.080 the rest of the chrome's 1px keylines run
at, so it read as almost invisible. Move both the tab separator and the
pinned-zone divider to --surface-border, which the surrounding chrome (the
active tab's border, the flare arcs, the content card's ring) already uses,
and which is what the system draws for separators on --sidebar -- a surface
whose lightness is identical to --app-shell.

Also fade a hairline out while either tab it divides is hovered, so it no
longer lingers 2px off the hover pill's rounded edge. The hairline sits on a
tab's own left edge, so the pair's other half belongs to the neighbouring
component instance and no ancestor-based variant can reach it; hence the
adjacent-sibling custom variant.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Naiyuan Qing
2026-07-16 14:49:34 +08:00
committed by GitHub
parent 24ea38dcb2
commit 849df8bb3b
2 changed files with 23 additions and 3 deletions

View File

@@ -185,7 +185,10 @@ function SortableTabItem({
canCloseOthers: boolean;
isNew: boolean;
shouldReduceMotion: boolean;
/** Hairline on the tab's left edge — hidden next to the active tab. */
/**
* Hairline on the tab's left edge — hidden next to the active tab, and
* faded out while either of the two tabs it divides is hovered.
*/
showSeparator: boolean;
}) {
const setActiveTab = useTabStore((s) => s.setActiveTab);
@@ -356,9 +359,12 @@ function SortableTabItem({
/>
)}
{showSeparator && (
// Fades in step with the neighbouring hover pill: both use a bare
// transition-opacity, so the hairline clears exactly as the pill
// arrives rather than lingering 2px off its rounded edge.
<span
aria-hidden
className="pointer-events-none absolute left-0 top-1/2 h-4 w-px -translate-y-1/2 bg-border"
className="pointer-events-none absolute left-0 top-1/2 h-4 w-px -translate-y-1/2 bg-surface-border transition-opacity group-hover/tab:opacity-0 prev-tab-hover:opacity-0"
/>
)}
<ContextMenu>
@@ -650,7 +656,7 @@ export function TabBar() {
unpinnedCount > 0 && (
<div
aria-hidden
className="mx-1 mb-2.5 h-4 w-px shrink-0 self-end bg-border"
className="mx-1 mb-2.5 h-4 w-px shrink-0 self-end bg-surface-border"
/>
)}
</Fragment>

View File

@@ -6,6 +6,20 @@
@custom-variant dark (&:is(.dark *));
/* Tab strip hairlines sit on each tab's own left edge, so the hairline between
two tabs belongs to the tab on the right. Hovering the left tab has to fade
it out as well, which no ancestor-based variant can reach — hence the
adjacent-sibling selector. The (hover: hover) gate mirrors what Tailwind
already wraps group-hover in, so both halves of the fade stay in step on
devices without a pointer. */
@custom-variant prev-tab-hover {
@media (hover: hover) {
[data-tab-frame]:hover + [data-tab-frame] & {
@slot;
}
}
}
/* Font stack: Inter for Latin UI text + system CJK fonts for localized content.
Web app uses the same stack via next/font/google in apps/web/app/layout.tsx —
keep the CJK fallback tail in sync across both files. The Inter primary family