diff options
| author | zwlucas <lucas.fariamo08@gmail.com> | 2026-06-05 20:52:54 +0000 |
|---|---|---|
| committer | zwlucas <lucas.fariamo08@gmail.com> | 2026-06-05 20:52:54 +0000 |
| commit | 09f964451d7d92e9891430ec4595c1276d486aab (patch) | |
| tree | 28da4483f5c28924a8c47fceb648b1baebe88224 /src/components/TabsBar/TabsBar.tsx | |
| download | yace-master.tar.gz yace-master.zip | |
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'src/components/TabsBar/TabsBar.tsx')
| -rw-r--r-- | src/components/TabsBar/TabsBar.tsx | 136 |
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> + ) +} |