fix: refine floating terminal chrome

This commit is contained in:
Jinjing
2026-06-08 11:24:52 -07:00
parent 8526f02c77
commit 6dc0be6a98
7 changed files with 56 additions and 10 deletions
@@ -1238,12 +1238,15 @@ export function FloatingTerminalPanel({
return (
// Why: root notification cards use z-40; keep the floating workspace below
// them so alerts are never hidden behind an open terminal panel.
// Drop shadow on the outer shell, border on an inner shell — mixing both on
// one rounded node made corners look stubby. Floating tabs skip their top
// border so the titlebar curve stays clean.
<div
ref={setPanelNode}
data-floating-terminal-panel
aria-hidden={!open}
tabIndex={-1}
className={`fixed z-30 flex min-h-[280px] min-w-[420px] overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-[0_10px_24px_rgba(0,0,0,0.18)] outline-none ${open ? 'opacity-100' : 'invisible pointer-events-none opacity-0'}`}
className={`fixed z-30 flex min-h-[280px] min-w-[420px] rounded-lg bg-transparent text-card-foreground shadow-[0_4px_12px_rgba(0,0,0,0.16),0_24px_64px_rgba(0,0,0,0.32)] outline-none dark:shadow-[0_8px_20px_rgba(0,0,0,0.35),0_28px_72px_rgba(0,0,0,0.58)] ${open ? 'opacity-100' : 'invisible pointer-events-none opacity-0'}`}
style={{
visibility: open ? 'visible' : 'hidden',
left: bounds.left,
@@ -1262,7 +1265,7 @@ export function FloatingTerminalPanel({
onBlurCapture={(event) => setFloatingTerminalInputFocused(event.relatedTarget)}
onKeyDownCapture={handleShortcutSurfaceKeyDown}
>
<div className="flex min-h-0 flex-1 flex-col">
<div className="relative flex h-full w-full min-h-0 flex-col overflow-hidden rounded-lg border border-black/14 bg-card dark:border-white/14">
<div
className="flex h-9 shrink-0 cursor-grab items-center border-b border-border bg-[var(--bg-titlebar,var(--card))] active:cursor-grabbing"
data-floating-terminal-shortcut-surface
@@ -1330,6 +1333,7 @@ export function FloatingTerminalPanel({
onMakePreviewFilePermanent={makePreviewFilePermanent}
onPinFile={pinFile}
tabBarOrder={tabBarOrder}
tabStripChrome="floating-panel"
/>
</div>
<FloatingTerminalWindowControls
@@ -19,6 +19,7 @@ import {
ACTIVE_TAB_INDICATOR_CLASSES,
getDropIndicatorClasses,
getTabRootStateClasses,
getTabStripBorderClasses,
type DropIndicator
} from './drop-indicator'
import { preventMiddleButtonDefault } from './middle-button-default-guard'
@@ -108,7 +109,8 @@ export default function BrowserTab({
onDuplicate,
onTogglePin,
dragData,
dropIndicator
dropIndicator,
includeTopTabBorder = true
}: {
tab: BrowserTabState
isActive: boolean
@@ -122,6 +124,7 @@ export default function BrowserTab({
onTogglePin: () => void
dragData: TabDragItemData
dropIndicator?: DropIndicator
includeTopTabBorder?: boolean
}): React.JSX.Element {
// Why: no transform/transition/isDragging styling — the drag design is
// that tabs stay visually anchored; only the blue insertion bar moves.
@@ -171,7 +174,7 @@ export default function BrowserTab({
data-pinned={isPinned ? 'true' : 'false'}
{...attributes}
{...listeners}
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 ${getDropIndicatorClasses(dropIndicator ?? null)} ${getTabRootStateClasses(isActive)}`}
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 ${getTabStripBorderClasses(hasTabsToRight, { includeTopBorder: includeTopTabBorder })} ${getDropIndicatorClasses(dropIndicator ?? null)} ${getTabRootStateClasses(isActive)}`}
onPointerDown={(e) => {
if (e.button !== 0) {
return
@@ -21,6 +21,7 @@ import {
ACTIVE_TAB_INDICATOR_CLASSES,
getDropIndicatorClasses,
getTabRootStateClasses,
getTabStripBorderClasses,
type DropIndicator
} from './drop-indicator'
import { canOpenMarkdownPreview } from '@/components/editor/markdown-preview-controls'
@@ -40,7 +41,8 @@ export default function EditorFileTab({
onTogglePin,
onSplitGroup,
dragData,
dropIndicator
dropIndicator,
includeTopTabBorder = true
}: {
file: OpenFile & { tabId?: string }
isActive: boolean
@@ -56,6 +58,7 @@ export default function EditorFileTab({
onSplitGroup: (direction: 'left' | 'right' | 'up' | 'down', sourceVisibleTabId: string) => void
dragData: TabDragItemData
dropIndicator?: DropIndicator
includeTopTabBorder?: boolean
}): React.JSX.Element {
const worktree = useWorktreeById(file.worktreeId)
const repo = useRepoById(worktree?.repoId ?? null)
@@ -200,7 +203,7 @@ export default function EditorFileTab({
data-pinned={isPinned ? 'true' : 'false'}
{...attributes}
{...listeners}
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 ${getDropIndicatorClasses(dropIndicator ?? null)} ${getTabRootStateClasses(isActive)}`}
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 ${getTabStripBorderClasses(hasTabsToRight, { includeTopBorder: includeTopTabBorder })} ${getDropIndicatorClasses(dropIndicator ?? null)} ${getTabRootStateClasses(isActive)}`}
onPointerDown={(e) => {
if (e.button !== 0) {
return
@@ -15,6 +15,7 @@ import {
ACTIVE_TAB_INDICATOR_CLASSES,
getDropIndicatorClasses,
getTabRootStateClasses,
getTabStripBorderClasses,
type DropIndicator
} from './drop-indicator'
import { preventMiddleButtonDefault } from './middle-button-default-guard'
@@ -38,6 +39,7 @@ type SortableTabProps = {
onSplitGroup: (direction: 'left' | 'right' | 'up' | 'down', sourceVisibleTabId: string) => void
dragData: TabDragItemData
dropIndicator?: DropIndicator
includeTopTabBorder?: boolean
}
export const CLOSE_ALL_CONTEXT_MENUS_EVENT = 'orca-close-all-context-menus'
@@ -59,7 +61,8 @@ export default function SortableTab({
onToggleExpand,
onSplitGroup,
dragData,
dropIndicator
dropIndicator,
includeTopTabBorder = true
}: SortableTabProps): React.JSX.Element {
// Why: subscribe to the per-tab boolean directly so only the tab whose unread
// status actually flipped re-renders. Reading the whole `unreadTerminalTabs`
@@ -213,7 +216,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 outline-none focus:outline-none focus-visible:outline-none border-t ${hasTabsToRight ? 'border-r' : ''} border-border ${getDropIndicatorClasses(dropIndicator ?? null)} ${getTabRootStateClasses(isActive)}`}
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 ${getTabStripBorderClasses(hasTabsToRight, { includeTopBorder: includeTopTabBorder })} ${getDropIndicatorClasses(dropIndicator ?? null)} ${getTabRootStateClasses(isActive)}`}
onDoubleClick={(e) => {
if (isEditing) {
return
@@ -108,6 +108,8 @@ type TabBarProps = {
sourceVisibleTabId?: string
) => void
hoveredTabInsertion?: HoveredTabInsertion | null
/** Floating workspace panels are rounded; skip tab top borders that clash with the curve. */
tabStripChrome?: 'default' | 'floating-panel'
}
type TabItem =
@@ -206,8 +208,10 @@ function TabBarInner({
onPinFile,
tabBarOrder,
onCreateSplitGroup,
hoveredTabInsertion
hoveredTabInsertion,
tabStripChrome = 'default'
}: TabBarProps): React.JSX.Element {
const includeTopTabBorder = tabStripChrome !== 'floating-panel'
const newTerminalShortcut = useShortcutLabel('tab.newTerminal')
const newBrowserShortcut = useShortcutLabel('tab.newBrowser')
const newSimulatorShortcut = useShortcutLabel('tab.newSimulator')
@@ -865,6 +869,7 @@ function TabBarInner({
}
dragData={dragData}
dropIndicator={dropIndicatorByVisibleId.get(item.id) ?? null}
includeTopTabBorder={includeTopTabBorder}
/>
)
}
@@ -886,6 +891,7 @@ function TabBarInner({
onTogglePin={() => togglePinned(item)}
dragData={dragData}
dropIndicator={dropIndicatorByVisibleId.get(item.id) ?? null}
includeTopTabBorder={includeTopTabBorder}
/>
)
}
@@ -921,6 +927,7 @@ function TabBarInner({
}
dragData={dragData}
dropIndicator={dropIndicatorByVisibleId.get(item.id) ?? null}
includeTopTabBorder={includeTopTabBorder}
/>
)
}
@@ -946,6 +953,7 @@ function TabBarInner({
}
dragData={dragData}
dropIndicator={dropIndicatorByVisibleId.get(item.id) ?? null}
includeTopTabBorder={includeTopTabBorder}
/>
)
})}
@@ -2,7 +2,8 @@ import { describe, expect, it } from 'vitest'
import {
ACTIVE_TAB_INDICATOR_CLASSES,
getDropIndicatorClasses,
getTabRootStateClasses
getTabRootStateClasses,
getTabStripBorderClasses
} from './drop-indicator'
describe('getDropIndicatorClasses', () => {
@@ -55,6 +56,20 @@ describe('ACTIVE_TAB_INDICATOR_CLASSES', () => {
})
})
describe('getTabStripBorderClasses', () => {
it('includes top and right borders by default', () => {
expect(getTabStripBorderClasses(true)).toBe('border-t border-r border-border')
expect(getTabStripBorderClasses(false)).toBe('border-t border-border')
})
it('can omit the top border for rounded floating panel titlebars', () => {
expect(getTabStripBorderClasses(true, { includeTopBorder: false })).toBe(
'border-r border-border'
)
expect(getTabStripBorderClasses(false, { includeTopBorder: false })).toBe('border-border')
})
})
describe('getTabRootStateClasses', () => {
it('returns the shared selected-tab surface treatment', () => {
const classes = getTabRootStateClasses(true)
@@ -33,3 +33,13 @@ export function getTabRootStateClasses(isActive: boolean): string {
? 'bg-[color-mix(in_srgb,var(--foreground)_6%,var(--card))] text-foreground'
: 'bg-card text-muted-foreground hover:text-foreground'
}
export function getTabStripBorderClasses(
hasTabsToRight: boolean,
options?: { includeTopBorder?: boolean }
): string {
const includeTopBorder = options?.includeTopBorder ?? true
return [includeTopBorder ? 'border-t' : '', hasTabsToRight ? 'border-r' : '', 'border-border']
.filter(Boolean)
.join(' ')
}