diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index a94b87d6..74c22a7e 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -259,7 +259,8 @@ "closeTab": "Close Tab", "sshManager": "SSH Manager", "hostManager": "Host Manager", - "cannotSplitTab": "Cannot split this tab" + "cannotSplitTab": "Cannot split this tab", + "tabNavigation": "Tab Navigation" }, "admin": { "title": "Admin Settings", diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index 678d383d..d417b67f 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -259,7 +259,8 @@ "closeTab": "关闭标签页", "sshManager": "SSH 管理器", "hostManager": "主机管理器", - "cannotSplitTab": "无法分割此标签页" + "cannotSplitTab": "无法分割此标签页", + "tabNavigation": "标签导航" }, "admin": { "title": "管理员设置", diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..92bdb930 --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,198 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} \ No newline at end of file diff --git a/src/ui/Desktop/Navigation/Tabs/TabDropdown.tsx b/src/ui/Desktop/Navigation/Tabs/TabDropdown.tsx new file mode 100644 index 00000000..25d9d4de --- /dev/null +++ b/src/ui/Desktop/Navigation/Tabs/TabDropdown.tsx @@ -0,0 +1,111 @@ +import React from "react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { + ChevronDown, + Home, + Terminal as TerminalIcon, + Server as ServerIcon, + Folder as FolderIcon, + Shield as AdminIcon, + Network as SshManagerIcon +} from "lucide-react"; +import { useTabs, type Tab } from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx"; +import { useTranslation } from "react-i18next"; + +export function TabDropdown(): React.ReactElement { + const { tabs, currentTab, setCurrentTab } = useTabs(); + const { t } = useTranslation(); + + const getTabIcon = (tabType: Tab['type']) => { + switch (tabType) { + case 'home': + return ; + case 'terminal': + return ; + case 'server': + return ; + case 'file_manager': + return ; + case 'ssh_manager': + return ; + case 'admin': + return ; + default: + return ; + } + }; + + const getTabDisplayTitle = (tab: Tab) => { + switch (tab.type) { + case 'home': + return t('nav.home'); + case 'server': + return tab.title || t('nav.serverStats'); + case 'file_manager': + return tab.title || t('nav.fileManager'); + case 'ssh_manager': + return tab.title || t('nav.sshManager'); + case 'admin': + return tab.title || t('nav.admin'); + case 'terminal': + default: + return tab.title || t('nav.terminal'); + } + }; + + const handleTabSwitch = (tabId: number) => { + setCurrentTab(tabId); + }; + + // If only one tab (home), don't show dropdown + if (tabs.length <= 1) { + return null; + } + + return ( + + + + + + {tabs.map((tab) => { + const isActive = tab.id === currentTab; + return ( + handleTabSwitch(tab.id)} + className={`flex items-center gap-2 cursor-pointer px-3 py-2 ${ + isActive + ? 'bg-[#1d1d1f] text-white' + : 'hover:bg-[#2d2d30] text-gray-300' + }`} + > + {getTabIcon(tab.type)} + + {getTabDisplayTitle(tab)} + + {isActive && ( +
+ )} + + ); + })} + + + ); +} \ No newline at end of file diff --git a/src/ui/Desktop/Navigation/TopNavbar.tsx b/src/ui/Desktop/Navigation/TopNavbar.tsx index 90432de1..fbaffd2d 100644 --- a/src/ui/Desktop/Navigation/TopNavbar.tsx +++ b/src/ui/Desktop/Navigation/TopNavbar.tsx @@ -14,6 +14,7 @@ import {Input} from "@/components/ui/input.tsx"; import {Checkbox} from "@/components/ui/checkbox.tsx"; import {Separator} from "@/components/ui/separator.tsx"; import {useTranslation} from "react-i18next"; +import {TabDropdown} from "@/ui/Desktop/Navigation/Tabs/TabDropdown.tsx"; interface TopNavbarProps { isTopbarOpen: boolean; @@ -262,6 +263,8 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
+ +