fix(tabs): clean up workspace chrome seams

Remove the duplicated 1px border below the drag strip where the rightmost
split pane's border-r stacked next to the right sidebar's borderLeft.
Extend the left-sidebar seam through the drag strip when splits exist so
it reaches y=0, matching the full-height right-sidebar line.
This commit is contained in:
brennanb2025
2026-04-24 18:21:09 -07:00
parent bb7ceb2e19
commit 88e5082a9e
8 changed files with 49 additions and 33 deletions
+1 -20
View File
@@ -961,26 +961,7 @@ function App(): React.JSX.Element {
filesystem watchers + cached directory trees survive across
open/close toggles. Unmount on the tasks view since that
surface is intentionally distraction-free. */}
{showRightSidebarControls ? (
workspaceActive ? (
/* Why: in workspace view the right sidebar must start at the
tab row, not the window edge, so the 8px drag strip that
runs across the top of the center column continues uninterrupted
over the right sidebar. Wrapping adds a matching 8px drag
surface above and shrinks the sidebar to fill the remainder. */
<div className="flex min-h-0 flex-col shrink-0">
<div
className="h-2 shrink-0 bg-card"
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
/>
<div className="flex min-h-0 flex-1">
<RightSidebar />
</div>
</div>
) : (
<RightSidebar />
)
) : null}
{showRightSidebarControls ? <RightSidebar /> : null}
</div>
<StatusBar />
{/* Why: NewWorkspaceComposerCard renders Radix <Tooltip>s that crash
@@ -111,7 +111,7 @@ export default function BrowserTab({
ref={setNodeRef}
{...attributes}
{...listeners}
className={`group relative flex items-center h-full px-1.5 text-xs cursor-pointer select-none shrink-0 border-t ${hasTabsToRight ? 'border-r' : ''} border-border bg-card ${getDropIndicatorClasses(dropIndicator ?? null)} ${
className={`group relative flex items-center h-full px-1.5 text-xs cursor-pointer select-none shrink-0 outline-none focus:outline-none focus-visible:outline-none border-t ${hasTabsToRight ? 'border-r' : ''} border-border bg-card ${getDropIndicatorClasses(dropIndicator ?? null)} ${
isActive ? 'text-foreground' : 'text-muted-foreground hover:text-foreground'
}`}
onPointerDown={(e) => {
@@ -175,7 +175,7 @@ export default function EditorFileTab({
ref={setNodeRef}
{...attributes}
{...listeners}
className={`group relative flex items-center h-full px-1.5 text-xs cursor-pointer select-none shrink-0 border-t ${hasTabsToRight ? 'border-r' : ''} border-border bg-card ${getDropIndicatorClasses(dropIndicator ?? null)} ${
className={`group relative flex items-center h-full px-1.5 text-xs cursor-pointer select-none shrink-0 outline-none focus:outline-none focus-visible:outline-none border-t ${hasTabsToRight ? 'border-r' : ''} border-border bg-card ${getDropIndicatorClasses(dropIndicator ?? null)} ${
isActive ? 'text-foreground' : 'text-muted-foreground hover:text-foreground'
}`}
onPointerDown={(e) => {
@@ -179,7 +179,7 @@ export default function SortableTab({
// tab still reads as "selected + has activity". The wash is
// rendered as an absolutely-positioned child below so the ::after
// pseudo-element stays free for the drop indicator.
className={`group relative flex items-center h-full px-1.5 text-xs cursor-pointer select-none shrink-0 border-t ${hasTabsToRight ? 'border-r' : ''} border-border bg-card ${getDropIndicatorClasses(dropIndicator ?? null)} ${
className={`group relative flex items-center h-full px-1.5 text-xs cursor-pointer select-none shrink-0 outline-none focus:outline-none focus-visible:outline-none border-t ${hasTabsToRight ? 'border-r' : ''} border-border bg-card ${getDropIndicatorClasses(dropIndicator ?? null)} ${
isActive ? 'text-foreground' : 'text-muted-foreground hover:text-foreground'
}`}
onDoubleClick={(e) => {
@@ -337,11 +337,13 @@ function TabBarInner({
"+" button remains window-draggable. */}
<div
ref={tabStripRef}
// Why: `border-r` on the strip itself guarantees a visible vertical
// separator between the last tab and the "+" button even when the
// strip is scrolled. Each tab's own right border is clipped by
// overflow-x-auto once tabs overflow, so relying on the per-tab
// border alone leaves no boundary on the trailing edge.
// Why: only `border-r` on the strip — the trailing edge must stay
// visible even when tabs overflow-scroll past the last tab. The
// left edge is instead painted by the FIRST tab's own `border-l`
// (see per-tab components) so its rendering is identical to every
// between-tab separator. A strip-level `border-l` would render at
// a different box than the tab's own `border-t`, producing a
// heavier-looking L-corner at the leftmost tab when inactive.
className="terminal-tab-strip flex items-stretch overflow-x-auto overflow-y-hidden border-r border-border"
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
>
@@ -19,6 +19,10 @@ export function getDropIndicatorClasses(dropIndicator: DropIndicator): string {
// positioned with z-10 so it overlays the tab chrome without shifting layout
// and without conflicting with drop-indicator pseudo-elements during a drag.
// `-top-px` pulls it onto the tab's own 1px top border so the blue bar
// REPLACES the faint gray line rather than stacking below it.
// REPLACES the faint gray line rather than stacking below it. Horizontal
// inset is 0 (not -1px): negative insets on the last tab bleed into the
// strip's scrollWidth, so clicking between active tabs flips the strip
// between "fits exactly" and "overflows by 1px", which jitters every tab by
// 1px because the browser preserves scrollLeft near the end.
export const ACTIVE_TAB_INDICATOR_CLASSES =
'pointer-events-none absolute inset-x-0 -top-px h-px bg-[#1e3d9c] z-10'
@@ -28,6 +28,7 @@ export default function TabGroupPanel({
isWorktreeActive,
isFocused,
hasSplitGroups,
touchesRightEdge,
reserveClosedExplorerToggleSpace,
reserveCollapsedSidebarHeaderSpace,
isTabDragActive = false,
@@ -39,6 +40,7 @@ export default function TabGroupPanel({
isWorktreeActive: boolean
isFocused: boolean
hasSplitGroups: boolean
touchesRightEdge: boolean
reserveClosedExplorerToggleSpace: boolean
reserveCollapsedSidebarHeaderSpace: boolean
isTabDragActive?: boolean
@@ -176,8 +178,27 @@ export default function TabGroupPanel({
return (
<div
// Why: vertical borders are always `border-border` so the focus
// highlight doesn't introduce a near-white strip next to the split
// resize handle (--accent is ~#f5f5f5 in light mode, which reads as a
// visible gap between the dragger and the tab row). Only the bottom
// border changes color on focus, which is enough to cue the focused
// group without painting a bright line along the vertical edges.
// Why: unfocused split groups dim very subtly so the focused group
// reads as "selected" without making the unfocused content look
// washed out or hard to read. Only applied when `hasSplitGroups`
// because a lone group has nothing to contrast against.
className={`group/tab-group flex flex-col flex-1 min-w-0 min-h-0 overflow-hidden${
hasSplitGroups ? ` border-x border-b ${isFocused ? 'border-accent' : 'border-border'}` : ''
hasSplitGroups
? // Why: drop only the RIGHT border on the rightmost group. The right
// sidebar paints its own `borderLeft` that already extends full
// height, so painting our own border-r in that spot stacks a
// second 1px line next to it — reading as a 2px-thick bar below
// the 8px drag strip (where the sidebar border continues alone
// above). The left edge has no such double-up, so the left
// border is always kept.
` border-l ${touchesRightEdge ? '' : 'border-r'} border-border border-b ${isFocused ? 'border-b-accent' : 'opacity-95'}`
: ''
}`}
onPointerDown={commands.focusGroup}
// Why: keyboard and assistive-tech users can move focus into an unfocused
@@ -125,6 +125,7 @@ function SplitNode({
// "focused", Cmd/Ctrl+W and split shortcuts can hit the wrong worktree.
isFocused={isWorktreeActive && node.groupId === focusedGroupId}
hasSplitGroups={hasSplitGroups}
touchesRightEdge={touchesRightEdge}
reserveClosedExplorerToggleSpace={touchesTopEdge && touchesRightEdge}
reserveCollapsedSidebarHeaderSpace={touchesTopEdge && touchesLeftEdge}
isTabDragActive={isTabDragActive}
@@ -198,6 +199,7 @@ export default function TabGroupSplitLayout({
isWorktreeActive: boolean
}): React.JSX.Element {
const dragSplit = useTabDragSplit({ worktreeId, enabled: isWorktreeActive })
const hasSplits = layout.type === 'split'
return (
<DndContext
@@ -219,10 +221,16 @@ export default function TabGroupSplitLayout({
each pane — so vertical split resize handles don't extend into the
window-drag region at the top. Only the split layout's own panes
own the resize handles, while this strip keeps the whole top of the
center column draggable regardless of how the splits are arranged. */}
center column draggable regardless of how the splits are arranged.
Why conditional `border-l`: when splits exist, the leftmost pane
paints its own `border-l` starting at y=8. Mirroring that border
on the drag strip extends the seam up to y=0 so the left-sidebar /
center-column line reaches the top of the window, matching the
full-height right-sidebar seam. Without splits the pane has no
`border-l`, so we skip it here too to avoid a dangling 8px stub. */}
<div className="flex flex-col flex-1 min-w-0 min-h-0 overflow-hidden">
<div
className="h-2 shrink-0 bg-card"
className={`h-2 shrink-0 bg-card${hasSplits ? ' border-l border-border' : ''}`}
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
/>
<div className="flex flex-1 min-w-0 min-h-0 overflow-hidden">
@@ -232,7 +240,7 @@ export default function TabGroupSplitLayout({
worktreeId={worktreeId}
focusedGroupId={focusedGroupId}
isWorktreeActive={isWorktreeActive}
hasSplitGroups={layout.type === 'split'}
hasSplitGroups={hasSplits}
touchesTopEdge={true}
touchesRightEdge={true}
touchesLeftEdge={true}