import { useRef, useEffect, useState } from "react"; import { Button } from "@/components/button"; import { Separator } from "@/components/separator"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/dropdown-menu"; import { ChevronDown, ChevronUp, RefreshCw, X } from "lucide-react"; import { tabIcon } from "@/shell/tabUtils"; import type { Tab, TabType } from "@/types/ui-types"; const CONNECTION_TAB_TYPES: TabType[] = ["terminal", "rdp", "vnc", "telnet"]; export function TabBar({ tabs, activeTabId, onSetActiveTab, onCloseTab, onRefreshTab, onReorderTabs, }: { tabs: Tab[]; activeTabId: string; onSetActiveTab: (id: string) => void; onCloseTab: (id: string) => void; onRefreshTab: (id: string) => void; onReorderTabs: (tabs: Tab[]) => void; }) { const [open, setOpen] = useState(true); const [dragTabId, setDragTabId] = useState(null); const [dragTargetIndex, setDragTargetIndex] = useState(null); const [dragPos, setDragPos] = useState<{ x: number; y: number } | null>(null); const tabBarRef = useRef(null); const tabEls = useRef>(new Map()); const dragData = useRef<{ id: string; index: number; startX: number; startY: number; offsetX: number; width: number; barTop: number; barHeight: number; x: number; y: number; } | null>(null); const dragTargetRef = useRef(null); const didDrag = useRef(false); useEffect(() => { const el = tabBarRef.current; if (!el) return; const handleWheel = (e: WheelEvent) => { if (e.deltaY !== 0) { e.preventDefault(); el.scrollLeft += e.deltaY; } }; el.addEventListener("wheel", handleWheel, { passive: false }); return () => el.removeEventListener("wheel", handleWheel); }, []); useEffect(() => { if (!dragTabId) return; function onPointerMove(e: PointerEvent) { if (!dragData.current || !tabBarRef.current) return; const d = dragData.current; if (Math.abs(e.clientX - d.startX) > 5) didDrag.current = true; const barRect = tabBarRef.current.getBoundingClientRect(); const x = Math.max( barRect.left + 2, Math.min(barRect.right - d.width - 6, e.clientX - d.offsetX), ); const y = d.barTop; setDragPos({ x, y }); const centerX = e.clientX - d.offsetX + d.width / 2; let newTarget = d.index; tabEls.current.forEach((el, id) => { if (id === d.id) return; const rect = el.getBoundingClientRect(); const mid = rect.left + rect.width / 2; const idx = tabs.findIndex((t) => t.id === id); if (idx < d.index && centerX < mid) newTarget = Math.min(newTarget, idx); if (idx > d.index && centerX > mid) newTarget = Math.max(newTarget, idx); }); if (tabs[0].type === "dashboard") newTarget = Math.max(1, newTarget); dragTargetRef.current = newTarget; setDragTargetIndex(newTarget); } function onPointerUp() { if (!dragData.current) return; const { id, index } = dragData.current; const to = dragTargetRef.current ?? index; if (to !== index) { const next = [...tabs]; if (next[0].id !== id) next.splice(to, 0, next.splice(index, 1)[0]); onReorderTabs(next); } dragData.current = null; dragTargetRef.current = null; setDragTabId(null); setDragTargetIndex(null); setDragPos(null); setTimeout(() => { didDrag.current = false; }, 0); } window.addEventListener("pointermove", onPointerMove); window.addEventListener("pointerup", onPointerUp); return () => { window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("pointerup", onPointerUp); }; }, [dragTabId, tabs, onReorderTabs]); const dragIdx = tabs.findIndex((t) => t.id === dragTabId); const target = dragTargetIndex ?? dragIdx; return (
{tabs.map((tab, index) => { const active = tab.id === activeTabId; const isDragging = dragTabId === tab.id; let translateX = 0; if ( dragTabId && !isDragging && dragIdx !== -1 && target !== null && target !== dragIdx ) { const draggedWidth = tabEls.current.get(dragTabId)?.offsetWidth ?? 0; if (dragIdx < target && index > dragIdx && index <= target) translateX = -draggedWidth; else if (dragIdx > target && index < dragIdx && index >= target) translateX = draggedWidth; } return (
{ if (el) tabEls.current.set(tab.id, el); else tabEls.current.delete(tab.id); }} onClick={() => !dragTabId && !didDrag.current && onSetActiveTab(tab.id) } onMouseDown={(e) => { if (e.button === 1 && tab.type !== "dashboard") { e.preventDefault(); onCloseTab(tab.id); } }} onPointerDown={(e) => { if (e.button !== 0 || tab.type === "dashboard") return; e.preventDefault(); const el = tabEls.current.get(tab.id); if (!el || !tabBarRef.current) return; const rect = el.getBoundingClientRect(); const barRect = tabBarRef.current.getBoundingClientRect(); dragData.current = { id: tab.id, index, startX: e.clientX, startY: e.clientY, offsetX: e.clientX - rect.left, width: rect.width, barTop: barRect.top, barHeight: barRect.height, x: rect.left, y: barRect.top, }; setDragTabId(tab.id); setDragTargetIndex(index); setDragPos({ x: rect.left, y: barRect.top }); (e.currentTarget as HTMLElement).setPointerCapture( e.pointerId, ); }} style={{ transform: isDragging ? "none" : `translateX(${translateX}px)`, transition: dragTabId && !isDragging ? "transform 200ms ease" : "none", opacity: isDragging ? 0 : 1, cursor: tab.type === "dashboard" ? "pointer" : isDragging ? "grabbing" : "grab", userSelect: "none", }} className={`group/tab flex items-center gap-2 shrink-0 transition-colors border-r border-border text-sm ${index === 0 && tab.type !== "dashboard" ? "border-l border-border" : ""} ${ tab.type === "dashboard" ? `px-2.5 md:px-3.5 ${active ? "border-b-2 border-b-accent-brand bg-surface text-foreground" : "text-muted-foreground hover:text-foreground hover:bg-surface"}` : `px-2.5 md:px-4 font-medium ${active ? "border-b-2 border-b-accent-brand bg-surface text-foreground" : "text-muted-foreground hover:text-foreground hover:bg-surface"}` }`} > {tabIcon(tab.type)} {tab.type !== "dashboard" && tab.label} {tab.type !== "dashboard" && (
{CONNECTION_TAB_TYPES.includes(tab.type) && ( )}
)}
); })} {dragTabId && dragPos && (() => { const tab = tabs.find((t) => t.id === dragTabId)!; const active = tab.id === activeTabId; return (
{tabIcon(tab.type)} {tab.type !== "dashboard" && tab.label}
); })()}
{tabs.map((tab) => (
onSetActiveTab(tab.id)} className={`flex items-center justify-between px-2 py-2 text-xs cursor-default hover:bg-accent hover:text-accent-foreground ${tab.id === activeTabId ? "text-foreground" : "text-muted-foreground"}`} >
{tabIcon(tab.type)} {tab.type === "dashboard" ? "Dashboard" : tab.label}
{tab.type !== "dashboard" && ( )}
))}
{!open && ( )}
); }