aboutsummaryrefslogtreecommitdiff
path: root/src/components/TabsBar/TabsBar.tsx
blob: 86c77ea7587b5311d7a7d9408c82c4cf26270f15 (plain)
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>
  )
}