新增 entitlement/license IPC 与 Waffo 一次性结账服务骨架,挂载 AI/转发/代理/串口/广播/批量导入导出门禁;批量执行改为结果矩阵并复用连接池,SFTP 工具栏图标化并去掉已连接条,同时完善跨平台本地终端与窗口配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
180 lines
5.4 KiB
TypeScript
180 lines
5.4 KiB
TypeScript
/**
|
|
* TabItem — local shell shows zsh/bash badge like professional terminal UIs
|
|
*/
|
|
|
|
import { X, Terminal, FolderSync, Cable, Crown, TerminalSquare } from 'lucide-react'
|
|
import type { Tab } from '../../../stores/tabStore'
|
|
|
|
interface TabItemProps {
|
|
tab: Tab
|
|
index: number
|
|
isActive: boolean
|
|
isDragging: boolean
|
|
dragOverIndex: number | null
|
|
onActivate: () => void
|
|
onClose: (e: React.MouseEvent) => void
|
|
onContextMenu: (e: React.MouseEvent) => void
|
|
onDragStart: (e: React.DragEvent, index: number) => void
|
|
onDragOver: (e: React.DragEvent, index: number) => void
|
|
onDrop: (e: React.DragEvent, index: number) => void
|
|
onDragEnd: () => void
|
|
}
|
|
|
|
function LocalShellBadge({ kind }: { kind?: string }) {
|
|
const k = (kind || 'unknown').toLowerCase()
|
|
if (k === 'zsh') {
|
|
return (
|
|
<span
|
|
className="inline-flex items-center justify-center w-6 h-6 rounded-md bg-teal-800/90 text-[8px] font-bold text-teal-100 flex-shrink-0 border border-teal-600/40"
|
|
title="Zsh"
|
|
>
|
|
zsh
|
|
</span>
|
|
)
|
|
}
|
|
if (k === 'bash') {
|
|
return (
|
|
<span
|
|
className="inline-flex items-center justify-center w-6 h-6 rounded-md bg-dark-600 text-emerald-400 flex-shrink-0 border border-dark-500"
|
|
title="Bash"
|
|
>
|
|
<span className="text-[11px] font-bold leading-none">$_</span>
|
|
</span>
|
|
)
|
|
}
|
|
if (k === 'fish') {
|
|
return (
|
|
<span
|
|
className="inline-flex items-center justify-center w-6 h-6 rounded-md bg-violet-800 text-[8px] font-bold text-violet-100 flex-shrink-0"
|
|
title="Fish"
|
|
>
|
|
fi
|
|
</span>
|
|
)
|
|
}
|
|
return (
|
|
<span className="inline-flex items-center justify-center w-6 h-6 rounded-md bg-dark-600 text-[9px] font-bold text-content-secondary flex-shrink-0">
|
|
sh
|
|
</span>
|
|
)
|
|
}
|
|
|
|
export function TabItem({
|
|
tab,
|
|
index,
|
|
isActive,
|
|
isDragging,
|
|
dragOverIndex,
|
|
onActivate,
|
|
onClose,
|
|
onContextMenu,
|
|
onDragStart,
|
|
onDragOver,
|
|
onDrop,
|
|
onDragEnd,
|
|
}: TabItemProps) {
|
|
const isLocalTerm = tab.type === 'terminal' && tab.isLocal
|
|
|
|
const getIcon = () => {
|
|
if (isLocalTerm) {
|
|
return <LocalShellBadge kind={tab.localShellKind} />
|
|
}
|
|
switch (tab.type) {
|
|
case 'terminal':
|
|
return <Terminal className={`w-3.5 h-3.5 ${isActive ? 'text-accent-blue' : ''}`} />
|
|
case 'sftp':
|
|
return <FolderSync className={`w-3.5 h-3.5 ${isActive ? 'text-accent-purple' : ''}`} />
|
|
case 'serial':
|
|
return <Cable className={`w-3.5 h-3.5 ${isActive ? 'text-accent-green' : ''}`} />
|
|
case 'upgrade':
|
|
return <Crown className={`w-3.5 h-3.5 ${isActive ? 'text-accent-blue' : ''}`} />
|
|
case 'batch':
|
|
return <TerminalSquare className={`w-3.5 h-3.5 ${isActive ? 'text-accent-blue' : ''}`} />
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
const showLeftIndicator = dragOverIndex === index && !isDragging
|
|
const showRightIndicator = dragOverIndex === index + 1 && !isDragging
|
|
|
|
return (
|
|
<div
|
|
data-tab-item
|
|
draggable
|
|
onClick={onActivate}
|
|
onContextMenu={onContextMenu}
|
|
onDragStart={(e) => onDragStart(e, index)}
|
|
onDragOver={(e) => onDragOver(e, index)}
|
|
onDrop={(e) => onDrop(e, index)}
|
|
onDragEnd={onDragEnd}
|
|
className={`
|
|
group flex items-center gap-2 px-3 py-2 min-w-[120px] max-w-[220px]
|
|
border-r border-dark-700/20 cursor-pointer select-none
|
|
transition-all duration-150 relative titlebar-no-drag
|
|
${isActive
|
|
? 'bg-dark-900 text-content-primary'
|
|
: 'bg-dark-800 text-txt-muted hover:bg-dark-700 hover:text-txt-muted'
|
|
}
|
|
${isDragging ? 'opacity-50' : 'opacity-100'}
|
|
`}
|
|
>
|
|
{showLeftIndicator && (
|
|
<div className="absolute left-0 top-0 bottom-0 w-0.5 bg-accent-blue z-10" />
|
|
)}
|
|
{showRightIndicator && (
|
|
<div className="absolute right-0 top-0 bottom-0 w-0.5 bg-accent-blue z-10" />
|
|
)}
|
|
|
|
{isLocalTerm ? (
|
|
<>
|
|
{getIcon()}
|
|
{tab.connectionStatus === 'connected' && (
|
|
<span className="w-1.5 h-1.5 rounded-full bg-green-500 flex-shrink-0" />
|
|
)}
|
|
{tab.connectionStatus === 'connecting' && (
|
|
<span className="w-1.5 h-1.5 rounded-full bg-yellow-400 animate-pulse flex-shrink-0" />
|
|
)}
|
|
{tab.connectionStatus === 'error' && (
|
|
<span className="w-1.5 h-1.5 rounded-full bg-red-500 flex-shrink-0" />
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
{tab.connectionStatus === 'connecting' && (
|
|
<span className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse" />
|
|
)}
|
|
{tab.connectionStatus === 'connected' && (
|
|
<span className="w-2 h-2 rounded-full bg-green-500" />
|
|
)}
|
|
{tab.connectionStatus === 'error' && (
|
|
<span className="w-2 h-2 rounded-full bg-red-500" />
|
|
)}
|
|
{getIcon()}
|
|
</>
|
|
)}
|
|
|
|
<span className="flex-1 truncate text-sm font-medium">{tab.title}</span>
|
|
|
|
<button
|
|
onClick={onClose}
|
|
className={`
|
|
p-0.5 rounded opacity-0 group-hover:opacity-100
|
|
hover:bg-dark-600 transition-opacity
|
|
${isActive ? 'opacity-100' : ''}
|
|
`}
|
|
>
|
|
<X className="w-3.5 h-3.5" />
|
|
</button>
|
|
|
|
<div
|
|
className={`
|
|
absolute bottom-0 left-0 right-0 h-0.5 bg-accent-blue
|
|
transition-[opacity,transform] duration-150 origin-left
|
|
${isActive ? 'opacity-100 scale-x-100' : 'opacity-0 scale-x-0'}
|
|
`}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|