1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
import { useRef, useEffect, useCallback, useState } from 'react'
import { useTabsStore } from '@/stores/useTabsStore'
import { useContextMenuStore } from '@/stores/useContextMenuStore'
function CloseIcon() {
return (
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
)
}
export default function TabsBar() {
const tabs = useTabsStore((s) => s.tabs)
const activeTabId = useTabsStore((s) => s.activeTabId)
const setActiveTab = useTabsStore((s) => s.setActiveTab)
const closeTab = useTabsStore((s) => s.closeTab)
const openContextMenu = useContextMenuStore((s) => s.open)
const scrollRef = useRef<HTMLDivElement>(null)
const activeTabRef = useRef<HTMLDivElement>(null)
const [showLeftFade, setShowLeftFade] = useState(false)
const [showRightFade, setShowRightFade] = useState(true)
const handleTabContextMenu = useCallback((e: React.MouseEvent, tabId: string) => {
e.preventDefault()
const numTabs = tabs.length
openContextMenu(e.clientX, e.clientY, [
{ id: 'close', label: 'Close', shortcut: 'Ctrl+W', icon: 'close', action: () => closeTab(tabId) },
{ id: 'closeOthers', label: 'Close Others', action: () => { tabs.forEach((t) => { if (t.id !== tabId) closeTab(t.id) }) }, disabled: numTabs < 2 },
{ id: 'closeAll', label: 'Close All', action: () => { tabs.forEach((t) => closeTab(t.id)) }, disabled: numTabs < 1 },
{ id: 'sep1', label: '', separator: true, action: () => {} },
{ id: 'copyPath', label: 'Copy Path', icon: 'link', action: () => { const tab = tabs.find((t) => t.id === tabId); if (tab) navigator.clipboard.writeText(tab.path) } },
])
}, [tabs, closeTab, openContextMenu])
const updateFades = useCallback(() => {
const el = scrollRef.current
if (!el) return
setShowLeftFade(el.scrollLeft > 4)
setShowRightFade(el.scrollLeft < el.scrollWidth - el.clientWidth - 4)
}, [])
useEffect(() => {
const el = scrollRef.current
if (!el) return
el.addEventListener('scroll', updateFades, { passive: true })
updateFades()
return () => el.removeEventListener('scroll', updateFades)
}, [updateFades, tabs.length])
useEffect(() => {
const el = scrollRef.current
const tabEl = activeTabRef.current
if (!el || !tabEl) return
const containerRect = el.getBoundingClientRect()
const tabRect = tabEl.getBoundingClientRect()
const isVisible =
tabRect.left >= containerRect.left && tabRect.right <= containerRect.right
if (!isVisible) {
const offset = tabRect.left - containerRect.left
const centerOffset = offset - containerRect.width / 2 + tabRect.width / 2
el.scrollBy({ left: centerOffset, behavior: 'smooth' })
}
}, [activeTabId])
return (
<div className="relative shrink-0 h-tabs">
{showLeftFade && (
<div className="absolute left-0 top-0 bottom-0 w-6 z-10 pointer-events-none bg-gradient-to-r from-panel-bg to-transparent" />
)}
{showRightFade && (
<div className="absolute right-0 top-0 bottom-0 w-6 z-10 pointer-events-none bg-gradient-to-l from-panel-bg to-transparent" />
)}
<div
ref={scrollRef}
className="flex overflow-x-auto overflow-y-hidden h-full scroll-smooth"
role="tablist"
aria-label="Open files"
>
{tabs.length === 0 && (
<div className="flex items-center px-3 text-ui-sm text-text-muted shrink-0">
No files open
</div>
)}
{tabs.map((tab) => {
const isActive = tab.id === activeTabId
return (
<div
key={tab.id}
ref={isActive ? activeTabRef : undefined}
role="tab"
aria-selected={isActive ? 'true' : 'false'}
aria-label={`${tab.name}${tab.modified ? ' (modified)' : ''}`}
className={`group relative flex items-center gap-1.5 px-4 py-2 text-ui-sm cursor-pointer shrink-0 transition-colors duration-100 select-none border-0 ${
isActive
? 'bg-editor-bg text-text-primary'
: 'bg-panel-bg text-text-muted hover:bg-panel-hover hover:text-text-secondary'
}`}
onClick={() => setActiveTab(tab.id)}
onContextMenu={(e) => handleTabContextMenu(e, tab.id)}
>
{isActive && (
<span className="absolute top-0 left-0 right-0 h-[2px] bg-accent rounded-b-sm" />
)}
<span className="truncate max-w-32">{tab.name}</span>
{tab.modified && (
<span className="w-[6px] h-[6px] rounded-full shrink-0 bg-text-muted group-hover:hidden" />
)}
<button
className={`flex items-center justify-center w-[18px] h-[18px] rounded-sm transition-all duration-100 -mr-1 shrink-0 hover:bg-panel-border text-text-muted hover:text-text-primary ${
tab.modified ? 'hidden group-hover:flex' : 'opacity-0 group-hover:opacity-100'
}`}
onClick={(e) => {
e.stopPropagation()
closeTab(tab.id)
}}
>
<CloseIcon />
</button>
</div>
)
})}
</div>
</div>
)
}
|