Files
XCDesktop/src/components/layout/TabBar/TabBar.tsx

161 lines
5.6 KiB
TypeScript
Raw Normal View History

2026-03-08 01:34:54 +08:00
import { FileItem } from '@/lib/api'
import { X } from 'lucide-react'
import { clsx } from 'clsx'
import { useWallpaper } from '@/stores'
import { useEffect, useRef, useState } from 'react'
import { ContextMenu } from '@/components/common/ContextMenu'
import { getModuleTabId } from '@/lib/module-registry'
const HOME_TAB_ID = getModuleTabId('home') ?? 'home-tab'
export interface TabBarProps {
openFiles: FileItem[]
activeFile: FileItem | null
onTabClick: (file: FileItem) => void
onTabClose: (file: FileItem, e: React.MouseEvent) => void
onCloseOther?: (file: FileItem) => void
onCloseAll?: () => void
onPopOut?: (file: FileItem) => void
2026-03-08 01:34:54 +08:00
className?: string
style?: React.CSSProperties
variant?: 'default' | 'titlebar'
}
export const TabBar = ({ openFiles, activeFile, onTabClick, onTabClose, onCloseOther, onCloseAll, onPopOut, className, style, variant = 'default' }: TabBarProps) => {
2026-03-08 01:34:54 +08:00
const scrollContainerRef = useRef<HTMLDivElement>(null)
const { opacity } = useWallpaper()
const [contextMenu, setContextMenu] = useState<{
isOpen: boolean
position: { x: number; y: number }
file: FileItem | null
}>({
isOpen: false,
position: { x: 0, y: 0 },
file: null
})
const handleContextMenu = (e: React.MouseEvent, file: FileItem) => {
e.preventDefault()
e.stopPropagation()
setContextMenu({
isOpen: true,
position: { x: e.clientX, y: e.clientY },
file
})
}
const handleCloseContextMenu = () => {
setContextMenu(prev => ({ ...prev, isOpen: false }))
}
const handleCloseOther = () => {
if (contextMenu.file && onCloseOther) {
onCloseOther(contextMenu.file)
}
handleCloseContextMenu()
}
const handleCloseAll = () => {
if (onCloseAll) {
onCloseAll()
}
handleCloseContextMenu()
}
const handlePopOut = () => {
if (contextMenu.file && onPopOut) {
onPopOut(contextMenu.file)
}
handleCloseContextMenu()
}
const isHomeTab = contextMenu.file?.path === HOME_TAB_ID
2026-03-08 01:34:54 +08:00
const contextMenuItems = [
...(!isHomeTab ? [{ label: '在新窗口中打开', onClick: handlePopOut }] : []),
2026-03-08 01:34:54 +08:00
{ label: '关闭其他标签页', onClick: handleCloseOther },
{ label: '关闭所有标签页', onClick: handleCloseAll }
]
useEffect(() => {
if (activeFile && scrollContainerRef.current) {
const activeTab = scrollContainerRef.current.querySelector('[data-active="true"]')
if (activeTab) {
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
}
}
}, [activeFile?.path, openFiles.length])
if (openFiles.length === 0) return null
const isTitlebar = variant === 'titlebar'
return (
<div
ref={scrollContainerRef}
className={clsx(
isTitlebar
? "flex overflow-hidden"
: "flex backdrop-blur-sm border-b border-gray-200 dark:border-gray-700/60 overflow-hidden",
className
)}
style={{
backgroundColor: isTitlebar ? 'transparent' : `rgba(var(--app-tabbar-bg-rgb), ${opacity})`,
'--tab-opacity': opacity,
...style
} as React.CSSProperties}
>
{openFiles.map((file) => {
const isActive = activeFile?.path === file.path
const isOnlyHomeTab = openFiles.length === 1 && file.path === HOME_TAB_ID
return (
<div
key={file.path}
data-active={isActive}
onClick={() => onTabClick(file)}
onContextMenu={(e) => handleContextMenu(e, file)}
className={clsx(
"group relative flex items-center px-3 min-w-[80px] max-w-[240px] text-sm cursor-pointer border-r border-gray-200 dark:border-gray-700/60 select-none rounded-t-md flex-1 titlebar-no-drag",
isTitlebar ? "h-full" : "h-10",
isActive
? "backdrop-blur-md text-gray-800 dark:text-gray-200 font-medium border-t-2 border-t-gray-400 dark:border-t-gray-500"
: "text-gray-600 dark:text-gray-300 hover:bg-gray-100/[var(--tab-opacity)] dark:hover:bg-gray-700/[var(--tab-opacity)] border-t-2 border-t-transparent"
)}
style={{
backgroundColor: isActive
? `rgba(var(--app-tab-active-bg-rgb), ${opacity * 0.55})`
: `rgba(var(--app-tab-inactive-bg-rgb), ${opacity * 0.65})`,
}}
>
<span className={clsx(
"w-full overflow-hidden whitespace-nowrap",
!isOnlyHomeTab && "pr-4"
)} style={{
maskImage: 'linear-gradient(to right, black calc(100% - 20px), transparent 100%)',
WebkitMaskImage: 'linear-gradient(to right, black calc(100% - 20px), transparent 100%)'
}}>{file.name}</span>
{!isOnlyHomeTab && (
<button
onClick={(e) => onTabClose(file, e)}
className={clsx(
"absolute right-2 p-0.5 rounded-sm opacity-0 group-hover:opacity-100 transition-opacity bg-inherit",
isActive
? "opacity-100 hover:bg-gray-100/[var(--tab-opacity)] dark:hover:bg-gray-700/[var(--tab-opacity)] text-gray-500 dark:text-gray-300"
: "hover:bg-gray-200/[var(--tab-opacity)] dark:hover:bg-gray-600/[var(--tab-opacity)] text-gray-400 dark:text-gray-400"
)}
>
<X size={14} />
</button>
)}
</div>
)
})}
<ContextMenu
items={contextMenuItems}
isOpen={contextMenu.isOpen}
position={contextMenu.position}
onClose={handleCloseContextMenu}
/>
</div>
)
}