feat: release OpsXShell 2.1.0 native Tauri edition
This commit is contained in:
1 parent
e9a0d21758
commit
a3dba0c6ed
311 files changed
+31748
-14897
No files matched your search
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* TabItem — local shell shows zsh/bash badge like professional terminal UIs
|
||||
*/
|
||||
|
||||
import { X, Terminal, FolderSync, Cable } 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' : ''}`} />
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user