mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 00:03:09 +00:00
fix: refine floating terminal chrome
This commit is contained in:
@@ -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(' ')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user