import React, { useState } from "react"; import { flushSync } from "react-dom"; import { useSidebar } from "@/components/ui/sidebar.tsx"; import { Button } from "@/components/ui/button.tsx"; import { ChevronDown, ChevronUpIcon, Hammer } from "lucide-react"; import { Tab } from "@/ui/desktop/navigation/tabs/Tab.tsx"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; import { useTranslation } from "react-i18next"; import { TabDropdown } from "@/ui/desktop/navigation/tabs/TabDropdown.tsx"; import { SSHToolsSidebar } from "@/ui/desktop/apps/tools/SSHToolsSidebar.tsx"; import { useCommandHistory } from "@/ui/desktop/apps/terminal/command-history/CommandHistoryContext.tsx"; interface TabData { id: number; type: string; title: string; terminalRef?: { current?: { sendInput?: (data: string) => void; }; }; [key: string]: unknown; } interface TopNavbarProps { isTopbarOpen: boolean; setIsTopbarOpen: (open: boolean) => void; onOpenCommandPalette: () => void; onRightSidebarStateChange?: (isOpen: boolean, width: number) => void; } export function TopNavbar({ isTopbarOpen, setIsTopbarOpen, onOpenCommandPalette, onRightSidebarStateChange, }: TopNavbarProps): React.ReactElement { const { state } = useSidebar(); const { tabs, currentTab, setCurrentTab, setSplitScreenTab, removeTab, allSplitScreenTab, reorderTabs, } = useTabs() as { tabs: TabData[]; currentTab: number; setCurrentTab: (id: number) => void; setSplitScreenTab: (id: number) => void; removeTab: (id: number) => void; allSplitScreenTab: number[]; reorderTabs: (fromIndex: number, toIndex: number) => void; }; const leftPosition = state === "collapsed" ? "26px" : "calc(var(--sidebar-width) + 8px)"; const { t } = useTranslation(); const commandHistory = useCommandHistory(); const [toolsSidebarOpen, setToolsSidebarOpen] = useState(false); const [commandHistoryTabActive, setCommandHistoryTabActive] = useState(false); const [splitScreenTabActive, setSplitScreenTabActive] = useState(false); const [rightSidebarWidth, setRightSidebarWidth] = useState(() => { const saved = localStorage.getItem("rightSidebarWidth"); const defaultWidth = 400; const savedWidth = saved !== null ? parseInt(saved, 10) : defaultWidth; const minWidth = Math.min(300, Math.floor(window.innerWidth * 0.2)); const maxWidth = Math.floor(window.innerWidth * 0.3); return Math.min(savedWidth, Math.max(minWidth, maxWidth)); }); React.useEffect(() => { localStorage.setItem("rightSidebarWidth", String(rightSidebarWidth)); }, [rightSidebarWidth]); React.useEffect(() => { const handleResize = () => { const minWidth = Math.min(300, Math.floor(window.innerWidth * 0.2)); const maxWidth = Math.floor(window.innerWidth * 0.3); if (rightSidebarWidth > maxWidth) { setRightSidebarWidth(Math.max(minWidth, maxWidth)); } else if (rightSidebarWidth < minWidth) { setRightSidebarWidth(minWidth); } }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [rightSidebarWidth]); React.useEffect(() => { if (onRightSidebarStateChange) { onRightSidebarStateChange(toolsSidebarOpen, rightSidebarWidth); } }, [toolsSidebarOpen, rightSidebarWidth, onRightSidebarStateChange]); const openCommandHistorySidebar = React.useCallback(() => { setToolsSidebarOpen(true); setCommandHistoryTabActive(true); }, []); React.useEffect(() => { commandHistory.setOpenCommandHistory(openCommandHistorySidebar); }, [commandHistory, openCommandHistorySidebar]); const rightPosition = toolsSidebarOpen ? `calc(var(--right-sidebar-width, ${rightSidebarWidth}px) + 8px)` : "17px"; const [justDroppedTabId, setJustDroppedTabId] = useState(null); const [isInDropAnimation, setIsInDropAnimation] = useState(false); const [dragState, setDragState] = useState<{ draggedId: number | null; draggedIndex: number | null; currentX: number; startX: number; targetIndex: number | null; }>({ draggedId: null, draggedIndex: null, currentX: 0, startX: 0, targetIndex: null, }); const containerRef = React.useRef(null); const tabRefs = React.useRef>(new Map()); const isProcessingDropRef = React.useRef(false); const prevTabsRef = React.useRef([]); const handleTabActivate = (tabId: number) => { setCurrentTab(tabId); }; const handleTabSplit = (tabId: number) => { setToolsSidebarOpen(true); setCommandHistoryTabActive(false); setSplitScreenTabActive(true); }; const handleTabClose = (tabId: number) => { removeTab(tabId); }; const handleSnippetExecute = (content: string) => { const tab = tabs.find((t: TabData) => t.id === currentTab); if (tab?.terminalRef?.current?.sendInput) { tab.terminalRef.current.sendInput(content + "\n"); } }; React.useEffect(() => { if (prevTabsRef.current.length > 0 && tabs !== prevTabsRef.current) { prevTabsRef.current = []; } }, [tabs]); React.useEffect(() => { if (justDroppedTabId !== null) { const timer = setTimeout(() => setJustDroppedTabId(null), 50); return () => clearTimeout(timer); } }, [justDroppedTabId]); const handleDragStart = (e: React.DragEvent, index: number) => { const img = new Image(); img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; e.dataTransfer.setDragImage(img, 0, 0); setDragState({ draggedId: tabs[index].id, draggedIndex: index, startX: e.clientX, currentX: e.clientX, targetIndex: index, }); }; const handleDrag = (e: React.DragEvent) => { if (e.clientX === 0) return; if (dragState.draggedIndex === null) return; setDragState((prev) => ({ ...prev, currentX: e.clientX, })); }; const calculateTargetIndex = () => { if (!containerRef.current || dragState.draggedIndex === null) return null; const draggedIndex = dragState.draggedIndex; const tabBoundaries: { index: number; start: number; end: number; mid: number; }[] = []; let accumulatedX = 0; tabs.forEach((tab, i) => { const tabEl = tabRefs.current.get(i); if (!tabEl) return; const tabWidth = tabEl.getBoundingClientRect().width; tabBoundaries.push({ index: i, start: accumulatedX, end: accumulatedX + tabWidth, mid: accumulatedX + tabWidth / 2, }); accumulatedX += tabWidth + 4; }); if (tabBoundaries.length === 0) return null; const containerRect = containerRef.current.getBoundingClientRect(); const draggedTab = tabBoundaries[draggedIndex]; const currentX = dragState.currentX - containerRect.left; const startX = dragState.startX - containerRect.left; const offset = currentX - startX; const draggedCenter = draggedTab.mid + offset; let newTargetIndex = draggedIndex; if (offset < 0) { for (let i = draggedIndex - 1; i >= 0; i--) { if (draggedCenter < tabBoundaries[i].mid) { newTargetIndex = i; } else { break; } } } else if (offset > 0) { for (let i = draggedIndex + 1; i < tabBoundaries.length; i++) { if (draggedCenter > tabBoundaries[i].mid) { newTargetIndex = i; } else { break; } } const lastTabIndex = tabBoundaries.length - 1; if (lastTabIndex >= 0) { const lastTabEl = tabRefs.current.get(lastTabIndex); if (lastTabEl) { const lastTabRect = lastTabEl.getBoundingClientRect(); const containerRect = containerRef.current.getBoundingClientRect(); const lastTabEndInContainer = lastTabRect.right - containerRect.left; if (currentX > lastTabEndInContainer) { newTargetIndex = lastTabIndex; } } } } return newTargetIndex; }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); if (dragState.draggedIndex === null) return; const containerRect = containerRef.current?.getBoundingClientRect(); if (!containerRect) return; if (e.clientX !== 0) { setDragState((prev) => ({ ...prev, currentX: e.clientX, })); } const newTargetIndex = calculateTargetIndex(); if (newTargetIndex !== null && newTargetIndex !== dragState.targetIndex) { setDragState((prev) => ({ ...prev, targetIndex: newTargetIndex, })); } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); if (isProcessingDropRef.current) return; isProcessingDropRef.current = true; const fromIndex = dragState.draggedIndex; const toIndex = dragState.targetIndex; const draggedId = dragState.draggedId; if (fromIndex !== null && toIndex !== null && fromIndex !== toIndex) { prevTabsRef.current = tabs; flushSync(() => { setIsInDropAnimation(true); setDragState({ draggedId: null, draggedIndex: null, startX: 0, currentX: 0, targetIndex: null, }); }); reorderTabs(fromIndex, toIndex); if (draggedId !== null) { setJustDroppedTabId(draggedId); } } else { setDragState({ draggedId: null, draggedIndex: null, startX: 0, currentX: 0, targetIndex: null, }); } setTimeout(() => { isProcessingDropRef.current = false; setIsInDropAnimation(false); }, 50); }; const handleDragEnd = () => { setIsInDropAnimation(false); setDragState({ draggedId: null, draggedIndex: null, startX: 0, currentX: 0, targetIndex: null, }); }; const isSplitScreenActive = Array.isArray(allSplitScreenTab) && allSplitScreenTab.length > 0; const currentTabObj = tabs.find((t: TabData) => t.id === currentTab); const currentTabIsHome = currentTabObj?.type === "home"; const currentTabIsSshManager = currentTabObj?.type === "ssh_manager"; const currentTabIsAdmin = currentTabObj?.type === "admin"; const currentTabIsUserProfile = currentTabObj?.type === "user_profile"; return (
{tabs.map((tab: TabData, index: number) => { const isActive = tab.id === currentTab; const isSplit = Array.isArray(allSplitScreenTab) && allSplitScreenTab.includes(tab.id); const isTerminal = tab.type === "terminal"; const isServer = tab.type === "server"; const isFileManager = tab.type === "file_manager"; const isSshManager = tab.type === "ssh_manager"; const isAdmin = tab.type === "admin"; const isUserProfile = tab.type === "user_profile"; const isSplittable = isTerminal || isServer || isFileManager; const disableSplit = !isSplittable; const disableActivate = isSplit || ((tab.type === "home" || tab.type === "ssh_manager" || tab.type === "admin" || tab.type === "user_profile") && isSplitScreenActive); const isHome = tab.type === "home"; const disableClose = isHome; const isDraggingThisTab = dragState.draggedIndex === index; const isTheDraggedTab = tab.id === dragState.draggedId; const isDroppedAndSnapping = tab.id === justDroppedTabId; const dragOffset = isDraggingThisTab ? dragState.currentX - dragState.startX : 0; let transform = ""; if (!isInDropAnimation) { if (isDraggingThisTab) { transform = `translateX(${dragOffset}px)`; } else if ( dragState.draggedIndex !== null && dragState.targetIndex !== null ) { const draggedOriginalIndex = dragState.draggedIndex; const currentTargetIndex = dragState.targetIndex; if ( draggedOriginalIndex < currentTargetIndex && index > draggedOriginalIndex && index <= currentTargetIndex ) { const draggedTabWidth = tabRefs.current .get(draggedOriginalIndex) ?.getBoundingClientRect().width || 0; const gap = 4; transform = `translateX(-${draggedTabWidth + gap}px)`; } else if ( draggedOriginalIndex > currentTargetIndex && index >= currentTargetIndex && index < draggedOriginalIndex ) { const draggedTabWidth = tabRefs.current .get(draggedOriginalIndex) ?.getBoundingClientRect().width || 0; const gap = 4; transform = `translateX(${draggedTabWidth + gap}px)`; } } } return (
{ if (el) { tabRefs.current.set(index, el); } else { tabRefs.current.delete(index); } }} draggable={true} onDragStart={(e) => { e.stopPropagation(); handleDragStart(e, index); }} onDrag={handleDrag} onDragOver={handleDragOver} onDrop={handleDrop} onDragEnd={handleDragEnd} e onMouseDown={(e) => { if (e.button === 1 && !disableClose) { e.preventDefault(); handleTabClose(tab.id); } }} style={{ transform, transition: isDraggingThisTab || isDroppedAndSnapping || isInDropAnimation ? "none" : "transform 200ms ease-out", zIndex: isDraggingThisTab ? 1000 : 1, position: "relative", cursor: isDraggingThisTab ? "grabbing" : "grab", userSelect: "none", WebkitUserSelect: "none", flex: tab.type === "home" ? "0 0 auto" : "1 1 150px", minWidth: tab.type === "home" ? "auto" : "150px", maxWidth: tab.type === "home" ? "auto" : "450px", display: "flex", }} > handleTabActivate(tab.id)} onClose={ isTerminal || isServer || isFileManager || isSshManager || isAdmin || isUserProfile ? () => handleTabClose(tab.id) : undefined } onSplit={ isSplittable ? () => handleTabSplit(tab.id) : undefined } canSplit={isSplittable} canClose={ isTerminal || isServer || isFileManager || isSshManager || isAdmin || isUserProfile } disableActivate={disableActivate} disableSplit={disableSplit} disableClose={disableClose} isDragging={isDraggingThisTab} isDragOver={false} />
); })}
{!isTopbarOpen && (
setIsTopbarOpen(true)} className="fixed top-0 cursor-pointer flex items-center justify-center rounded-bl-md rounded-br-md" style={{ left: leftPosition, right: rightPosition, height: "10px", zIndex: 9999, backgroundColor: "#18181b", border: "2px solid #27272a", borderTop: "none", }} >
)} setToolsSidebarOpen(false)} onSnippetExecute={handleSnippetExecute} sidebarWidth={rightSidebarWidth} setSidebarWidth={setRightSidebarWidth} initialTab={ commandHistoryTabActive ? "command-history" : splitScreenTabActive ? "split-screen" : undefined } onTabChange={() => { setCommandHistoryTabActive(false); setSplitScreenTabActive(false); }} />
); }