aboutsummaryrefslogtreecommitdiff
path: root/src/components/TabsBar/TabsBar.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/TabsBar/TabsBar.tsx')
-rw-r--r--src/components/TabsBar/TabsBar.tsx136
1 files changed, 136 insertions, 0 deletions
diff --git a/src/components/TabsBar/TabsBar.tsx b/src/components/TabsBar/TabsBar.tsx
new file mode 100644
index 0000000..86c77ea
--- /dev/null
+++ b/src/components/TabsBar/TabsBar.tsx
@@ -0,0 +1,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>
+ )
+}