feat(theme): 专业主题系统重构 — Warp/Linear 设计参考,深色/浅色完整适配
参考设计系统: - Warp: 温暖暗色调、自然层次、克制配色 - Linear: 亮度阶梯层次、半透明边框、精确排版 核心改进: - app-theme.ts: 完整重写深色/浅色调色板 (+surface-hover/active, border-subtle/focus, feedback-bg, placeholder) - useTheme.ts: 修复硬编码 return 'dark',支持 dark/light/system 完整切换 - index.css: 消除硬编码颜色,全部使用 CSS 变量 - tailwind.config.js: 新增 txt/content/surface/border/ph/feedback 语义 token - 44 文件批量迁移: 292 处 text-gray/bg-gray/text-white/placeholder-gray → 主题感知语义 token | 44 files +611/-482 | tsc零错误 | 96/96 tests passing
This commit is contained in:
1 parent
07c4a5b14f
commit
e9a0d21758
44 files changed
+593
-464
No files matched your search
@@ -52,16 +52,16 @@ const STATUS_DOT_COLORS: Record<ConnectionState, string> = {
|
||||
connected: 'bg-green-400',
|
||||
connecting: 'bg-yellow-400 animate-pulse',
|
||||
background: 'bg-green-400 opacity-60',
|
||||
suspended: 'bg-gray-400',
|
||||
suspended: 'bg-surface-active',
|
||||
error: 'bg-red-400',
|
||||
disconnected: 'bg-gray-500',
|
||||
disconnected: 'bg-surface-active',
|
||||
reconnecting: 'bg-yellow-400 animate-pulse',
|
||||
}
|
||||
|
||||
function StatusDot({ state }: StatusDotProps) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-block w-2 h-2 rounded-full ${STATUS_DOT_COLORS[state] ?? 'bg-gray-500'}`}
|
||||
className={`inline-block w-2 h-2 rounded-full ${STATUS_DOT_COLORS[state] ?? 'bg-surface-active'}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -103,7 +103,7 @@ function ConnectionCard({ conn, onReconnect, onDisconnect, reconnectingId }: Con
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<StatusDot state={conn.state} />
|
||||
<span className="text-sm text-gray-200 truncate font-medium">
|
||||
<span className="text-sm text-content-primary truncate font-medium">
|
||||
{conn.config.username}@{conn.config.host}
|
||||
</span>
|
||||
</div>
|
||||
@@ -116,10 +116,10 @@ function ConnectionCard({ conn, onReconnect, onDisconnect, reconnectingId }: Con
|
||||
: conn.state === 'background'
|
||||
? 'bg-green-400/10 text-green-400 opacity-70'
|
||||
: conn.state === 'suspended'
|
||||
? 'bg-gray-400/10 text-gray-400'
|
||||
? 'bg-surface-active/10 text-txt-muted'
|
||||
: conn.state === 'error'
|
||||
? 'bg-red-400/10 text-red-400'
|
||||
: 'bg-gray-500/10 text-gray-500'
|
||||
: 'bg-surface-active/10 text-txt-muted'
|
||||
}`}
|
||||
>
|
||||
{STATUS_LABELS[conn.state]}
|
||||
@@ -127,7 +127,7 @@ function ConnectionCard({ conn, onReconnect, onDisconnect, reconnectingId }: Con
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-400">
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-txt-muted">
|
||||
{/* Port */}
|
||||
<span className="flex items-center gap-1">
|
||||
<Activity className="w-3 h-3" />
|
||||
@@ -167,7 +167,7 @@ function ConnectionCard({ conn, onReconnect, onDisconnect, reconnectingId }: Con
|
||||
onClick={() => onReconnect(conn.connectionId)}
|
||||
disabled={isReconnecting}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded text-xs
|
||||
bg-accent-blue/10 text-accent-blue hover:bg-accent-blue/20
|
||||
bg-surface-baseccent-blue/10 text-accent-blue hover:bg-surface-baseccent-blue/20
|
||||
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<RefreshCw className={`w-3 h-3 ${isReconnecting ? 'animate-spin' : ''}`} />
|
||||
@@ -208,7 +208,7 @@ interface StatsBarProps {
|
||||
|
||||
function StatsOverview({ total, connected, background, error }: StatsBarProps) {
|
||||
const items = [
|
||||
{ label: '总计', value: total, color: 'text-gray-300' },
|
||||
{ label: '总计', value: total, color: 'text-content-secondary' },
|
||||
{ label: '活跃', value: connected, color: 'text-green-400' },
|
||||
{ label: '后台', value: background, color: 'text-yellow-400' },
|
||||
{ label: '断开/错误', value: error, color: 'text-red-400' },
|
||||
@@ -219,7 +219,7 @@ function StatsOverview({ total, connected, background, error }: StatsBarProps) {
|
||||
{items.map((item) => (
|
||||
<div key={item.label} className="bg-dark-900 rounded-lg p-2 text-center">
|
||||
<div className={`text-lg font-semibold ${item.color}`}>{item.value}</div>
|
||||
<div className="text-[10px] text-gray-500">{item.label}</div>
|
||||
<div className="text-[10px] text-txt-muted">{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -274,7 +274,7 @@ export default function ConnectionPanel() {
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/30 z-40 animate-fade-in"
|
||||
className="fixed inset-0 bg-surface-baselack/30 z-40 animate-fade-in"
|
||||
onClick={close}
|
||||
/>
|
||||
|
||||
@@ -286,14 +286,14 @@ export default function ConnectionPanel() {
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 h-12 border-b border-dark-700/30 shrink-0">
|
||||
<h2 className="text-sm font-semibold text-gray-200 flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-content-primary flex items-center gap-2">
|
||||
<Wifi className="w-4 h-4 text-accent-blue" />
|
||||
连接管理
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={close}
|
||||
className="p-1 rounded hover:bg-dark-700 text-gray-400 hover:text-gray-200 transition-colors"
|
||||
className="p-1 rounded hover:bg-dark-700 text-txt-muted hover:text-content-primary transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
@@ -168,11 +168,11 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
{needsReconnect && currentTab?.sessionId && (
|
||||
<div className="absolute inset-0 bg-dark-900/90 flex items-center justify-center z-50">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-accent-blue/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<div className="w-16 h-16 bg-surface-baseccent-blue/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<ArrowRightLeft className="w-8 h-8 text-accent-blue" />
|
||||
</div>
|
||||
<div className="text-gray-200 mb-2 text-lg">会话已恢复</div>
|
||||
<div className="text-gray-400 text-sm mb-6">
|
||||
<div className="text-content-primary mb-2 text-lg">会话已恢复</div>
|
||||
<div className="text-txt-muted text-sm mb-6">
|
||||
上次关闭时的端口转发标签已恢复<br />
|
||||
需要重新建立 SSH 连接才能使用
|
||||
</div>
|
||||
@@ -181,7 +181,7 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
updateTab(tabId, { needsReconnect: false })
|
||||
// 刷新页面或触发连接逻辑
|
||||
}}
|
||||
className="px-6 py-2.5 bg-accent-blue hover:bg-blue-500 text-white rounded-lg transition-colors"
|
||||
className="px-6 py-2.5 bg-surface-baseccent-blue hover:bg-surface-baselue-500 text-content-primary rounded-lg transition-colors"
|
||||
>
|
||||
重新连接
|
||||
</button>
|
||||
@@ -194,8 +194,8 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
<div className="flex items-center gap-3">
|
||||
<ArrowRightLeft className="w-6 h-6 text-accent-blue" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">端口转发</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
<h2 className="text-lg font-semibold text-content-primary">端口转发</h2>
|
||||
<p className="text-sm text-txt-muted">
|
||||
{connection
|
||||
? `已连接: ${connection.config.username}@${connection.config.host}`
|
||||
: '未连接 - 请先建立 SSH 连接'}
|
||||
@@ -205,7 +205,7 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
disabled={!connection}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-accent-blue hover:bg-blue-600 disabled:bg-dark-700 disabled:text-gray-500 rounded-lg text-sm font-medium transition-colors"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-surface-baseccent-blue hover:bg-surface-baselue-600 disabled:bg-dark-700 disabled:text-txt-muted rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
添加规则
|
||||
@@ -215,7 +215,7 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
{/* Rules List */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{rules.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||||
<div className="flex flex-col items-center justify-center h-full text-txt-muted">
|
||||
<Globe className="w-16 h-16 mb-4 opacity-50" />
|
||||
<p className="text-lg">暂无端口转发规则</p>
|
||||
<p className="text-sm mt-1">点击右上角添加规则</p>
|
||||
@@ -235,7 +235,7 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Type Icon */}
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
||||
rule.type === 'local' ? 'bg-blue-500/20 text-blue-400' :
|
||||
rule.type === 'local' ? 'bg-surface-baselue-500/20 text-blue-400' :
|
||||
rule.type === 'remote' ? 'bg-purple-500/20 text-purple-400' :
|
||||
'bg-orange-500/20 text-orange-400'
|
||||
}`}>
|
||||
@@ -247,9 +247,9 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
{/* Rule Info */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-white">{rule.name}</span>
|
||||
<span className="font-medium text-content-primary">{rule.name}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${
|
||||
rule.type === 'local' ? 'bg-blue-500/20 text-blue-400' :
|
||||
rule.type === 'local' ? 'bg-surface-baselue-500/20 text-blue-400' :
|
||||
rule.type === 'remote' ? 'bg-purple-500/20 text-purple-400' :
|
||||
'bg-orange-500/20 text-orange-400'
|
||||
}`}>
|
||||
@@ -262,7 +262,7 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400 mt-1">
|
||||
<div className="text-sm text-txt-muted mt-1">
|
||||
{rule.type === 'dynamic' ? (
|
||||
<span>SOCKS5 代理: 127.0.0.1:{rule.localPort}</span>
|
||||
) : rule.type === 'local' ? (
|
||||
@@ -320,7 +320,7 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
{showAddModal && (
|
||||
<div className="absolute inset-0 bg-dark-900/90 flex items-center justify-center z-50">
|
||||
<div className="bg-dark-800 rounded-xl border border-dark-700 shadow-2xl w-full max-w-md p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">添加端口转发规则</h3>
|
||||
<h3 className="text-lg font-semibold text-content-primary mb-4">添加端口转发规则</h3>
|
||||
|
||||
{/* Type Selection */}
|
||||
<div className="grid grid-cols-3 gap-2 mb-4">
|
||||
@@ -334,8 +334,8 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
onClick={() => setNewRule({ ...newRule, type: t.type as 'local' | 'remote' | 'dynamic' })}
|
||||
className={`p-3 rounded-lg border text-left transition-colors ${
|
||||
newRule.type === t.type
|
||||
? 'bg-accent-blue/20 border-accent-blue text-white'
|
||||
: 'bg-dark-700 border-dark-600 text-gray-400 hover:bg-dark-600'
|
||||
? 'bg-surface-baseccent-blue/20 border-accent-blue text-content-primary'
|
||||
: 'bg-dark-700 border-dark-600 text-txt-muted hover:bg-dark-600'
|
||||
}`}
|
||||
>
|
||||
<div className="text-sm font-medium">{t.label}</div>
|
||||
@@ -347,19 +347,19 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
{/* Form Fields */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">规则名称</label>
|
||||
<label className="block text-sm text-txt-muted mb-1">规则名称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newRule.name}
|
||||
onChange={(e) => setNewRule({ ...newRule, name: e.target.value })}
|
||||
placeholder="例如: MySQL 转发"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">
|
||||
<label className="block text-sm text-txt-muted mb-1">
|
||||
{newRule.type === 'remote' ? '远程端口' : '本地端口'}
|
||||
</label>
|
||||
<input
|
||||
@@ -367,19 +367,19 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
value={newRule.localPort || ''}
|
||||
onChange={(e) => setNewRule({ ...newRule, localPort: parseInt(e.target.value) })}
|
||||
placeholder="例如: 3306"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{newRule.type !== 'dynamic' && (
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">远程端口</label>
|
||||
<label className="block text-sm text-txt-muted mb-1">远程端口</label>
|
||||
<input
|
||||
type="number"
|
||||
value={newRule.remotePort || ''}
|
||||
onChange={(e) => setNewRule({ ...newRule, remotePort: parseInt(e.target.value) })}
|
||||
placeholder="例如: 3306"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -387,13 +387,13 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
|
||||
{newRule.type === 'local' && (
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">远程主机</label>
|
||||
<label className="block text-sm text-txt-muted mb-1">远程主机</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newRule.remoteHost}
|
||||
onChange={(e) => setNewRule({ ...newRule, remoteHost: e.target.value })}
|
||||
placeholder="例如: localhost 或 192.168.1.100"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -409,7 +409,7 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddRule}
|
||||
className="flex-1 px-4 py-2 bg-accent-blue hover:bg-blue-600 rounded-lg text-sm font-medium transition-colors"
|
||||
className="flex-1 px-4 py-2 bg-surface-baseccent-blue hover:bg-surface-baselue-600 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
|
||||
@@ -60,22 +60,22 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-surface-baselack/50 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md bg-dark-800 rounded-xl border border-dark-700 shadow-2xl animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-dark-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-accent-blue/20 rounded-lg flex items-center justify-center">
|
||||
<div className="w-10 h-10 bg-surface-baseccent-blue/20 rounded-lg flex items-center justify-center">
|
||||
<Zap className="w-5 h-5 text-accent-blue" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-200">快速连接</h2>
|
||||
<p className="text-sm text-gray-500">临时连接到服务器</p>
|
||||
<h2 className="text-lg font-semibold text-content-primary">快速连接</h2>
|
||||
<p className="text-sm text-txt-muted">临时连接到服务器</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-dark-700 rounded-lg text-gray-400 hover:text-gray-200 transition-colors"
|
||||
className="p-2 hover:bg-dark-700 rounded-lg text-txt-muted hover:text-content-primary transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
@@ -86,7 +86,7 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
|
||||
{/* Host and Port */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-1">
|
||||
<Globe className="inline w-3 h-3 mr-1" />
|
||||
主机地址
|
||||
</label>
|
||||
@@ -97,11 +97,11 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
|
||||
value={formData.host}
|
||||
onChange={(e) => setFormData({ ...formData, host: e.target.value })}
|
||||
placeholder="192.168.1.1"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-1">
|
||||
端口
|
||||
</label>
|
||||
<input
|
||||
@@ -109,14 +109,14 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
|
||||
value={formData.port}
|
||||
onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) })}
|
||||
placeholder="22"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-content-primary focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Username */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-1">
|
||||
<User className="inline w-3 h-3 mr-1" />
|
||||
用户名
|
||||
</label>
|
||||
@@ -126,13 +126,13 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
|
||||
value={formData.username}
|
||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
||||
placeholder="root"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-1">
|
||||
<Lock className="inline w-3 h-3 mr-1" />
|
||||
密码
|
||||
</label>
|
||||
@@ -141,7 +141,7 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
placeholder="••••••••"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -155,7 +155,7 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
|
||||
|
||||
{/* Hint */}
|
||||
<div className="p-3 bg-dark-700/50 rounded-lg">
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-xs text-txt-muted">
|
||||
快速连接不会保存会话信息。如需保存,请使用"新建会话"。
|
||||
</p>
|
||||
</div>
|
||||
@@ -165,14 +165,14 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-400 hover:text-gray-200 transition-colors"
|
||||
className="px-4 py-2 text-txt-muted hover:text-content-primary transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isConnecting}
|
||||
className="flex items-center gap-2 px-6 py-2 bg-accent-blue hover:bg-blue-600 disabled:opacity-50 rounded-lg text-white font-medium transition-colors"
|
||||
className="flex items-center gap-2 px-6 py-2 bg-surface-baseccent-blue hover:bg-surface-baselue-600 disabled:opacity-50 rounded-lg text-content-primary font-medium transition-colors"
|
||||
>
|
||||
{isConnecting ? (
|
||||
'连接中...'
|
||||
|
||||
@@ -142,22 +142,22 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-surface-baselack/50 backdrop-blur-sm">
|
||||
<div className="w-full max-w-lg bg-dark-800 rounded-xl border border-dark-700 shadow-2xl animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-dark-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-accent-green/20 rounded-lg flex items-center justify-center">
|
||||
<div className="w-10 h-10 bg-surface-baseccent-green/20 rounded-lg flex items-center justify-center">
|
||||
<Server className="w-5 h-5 text-accent-green" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-200">{isEditing ? '编辑会话' : '新建会话'}</h2>
|
||||
<p className="text-sm text-gray-500">{isEditing ? '修改 SSH 连接配置' : '配置 SSH 连接信息'}</p>
|
||||
<h2 className="text-lg font-semibold text-content-primary">{isEditing ? '编辑会话' : '新建会话'}</h2>
|
||||
<p className="text-sm text-txt-muted">{isEditing ? '修改 SSH 连接配置' : '配置 SSH 连接信息'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-dark-700 rounded-lg text-gray-400 hover:text-gray-200 transition-colors"
|
||||
className="p-2 hover:bg-dark-700 rounded-lg text-txt-muted hover:text-content-primary transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
@@ -168,7 +168,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
{/* Basic info */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-1">
|
||||
会话名称
|
||||
</label>
|
||||
<input
|
||||
@@ -177,29 +177,29 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="例如: 生产服务器"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-1">
|
||||
主机地址
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Globe className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<Globe className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-txt-muted" />
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.host}
|
||||
onChange={(e) => setFormData({ ...formData, host: e.target.value })}
|
||||
placeholder="192.168.1.1"
|
||||
className="w-full pl-9 pr-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full pl-9 pr-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-1">
|
||||
端口
|
||||
</label>
|
||||
<input
|
||||
@@ -207,24 +207,24 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
value={formData.port}
|
||||
onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) })}
|
||||
placeholder="22"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-content-primary focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-1">
|
||||
用户名
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-txt-muted" />
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.username}
|
||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
||||
placeholder="root"
|
||||
className="w-full pl-9 pr-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full pl-9 pr-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -232,7 +232,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
|
||||
{/* Authentication */}
|
||||
<div className="border-t border-dark-700 pt-4">
|
||||
<label className="block text-sm font-medium text-gray-400 mb-2">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-2">
|
||||
认证方式
|
||||
</label>
|
||||
<div className="flex gap-2 mb-4">
|
||||
@@ -248,8 +248,8 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
onClick={() => setFormData({ ...formData, authType: type.value as AuthType })}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg border text-sm transition-colors ${
|
||||
formData.authType === type.value
|
||||
? 'border-accent-blue bg-accent-blue/20 text-accent-blue'
|
||||
: 'border-dark-700 hover:border-dark-600 text-gray-400'
|
||||
? 'border-accent-blue bg-surface-baseccent-blue/20 text-accent-blue'
|
||||
: 'border-dark-700 hover:border-dark-600 text-txt-muted'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
@@ -261,17 +261,17 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
|
||||
{formData.authType === 'password' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-1">
|
||||
密码
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-txt-muted" />
|
||||
<input
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
placeholder="••••••••"
|
||||
className="w-full pl-9 pr-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full pl-9 pr-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -280,7 +280,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
{formData.authType === 'key' && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-1">
|
||||
私钥路径
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
@@ -289,11 +289,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
value={formData.privateKey}
|
||||
onChange={(e) => setFormData({ ...formData, privateKey: e.target.value })}
|
||||
placeholder="~/.ssh/id_rsa"
|
||||
className="flex-1 px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="flex-1 px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-2 bg-dark-700 hover:bg-dark-600 rounded-lg text-gray-300 transition-colors"
|
||||
className="px-4 py-2 bg-dark-700 hover:bg-dark-600 rounded-lg text-content-secondary transition-colors"
|
||||
onClick={async () => {
|
||||
const result = await window.electronAPI?.showOpenDialog({
|
||||
title: '选择私钥文件',
|
||||
@@ -315,7 +315,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-1">
|
||||
密钥密码 (可选)
|
||||
</label>
|
||||
<input
|
||||
@@ -323,7 +323,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
value={formData.passphrase}
|
||||
onChange={(e) => setFormData({ ...formData, passphrase: e.target.value })}
|
||||
placeholder="••••••••"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -335,11 +335,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowProxySettings(!showProxySettings)}
|
||||
className="flex items-center gap-2 text-sm font-medium text-gray-400 hover:text-gray-200 transition-colors"
|
||||
className="flex items-center gap-2 text-sm font-medium text-txt-muted hover:text-content-primary transition-colors"
|
||||
>
|
||||
<Shield className="w-4 h-4" />
|
||||
代理设置
|
||||
<span className="text-xs text-gray-600">
|
||||
<span className="text-xs text-txt-faint">
|
||||
{formData.proxy.type !== 'none' && formData.proxy.host ? `(已配置 ${formData.proxy.type.toUpperCase()})` : '(未配置)'}
|
||||
</span>
|
||||
<svg
|
||||
@@ -372,8 +372,8 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
})}
|
||||
className={`flex-1 px-3 py-1.5 rounded-lg border text-xs transition-colors ${
|
||||
formData.proxy.type === type.value
|
||||
? 'border-accent-blue bg-accent-blue/20 text-accent-blue'
|
||||
: 'border-dark-700 hover:border-dark-600 text-gray-500'
|
||||
? 'border-accent-blue bg-surface-baseccent-blue/20 text-accent-blue'
|
||||
: 'border-dark-700 hover:border-dark-600 text-txt-muted'
|
||||
}`}
|
||||
>
|
||||
{type.label}
|
||||
@@ -386,7 +386,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
{/* Proxy Host & Port */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="col-span-2">
|
||||
<label className="block text-xs text-gray-500 mb-1">代理地址</label>
|
||||
<label className="block text-xs text-txt-muted mb-1">代理地址</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.proxy.host}
|
||||
@@ -395,11 +395,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
proxy: { ...formData.proxy, host: e.target.value }
|
||||
})}
|
||||
placeholder="proxy.example.com"
|
||||
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 text-sm placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-content-primary text-sm placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">端口</label>
|
||||
<label className="block text-xs text-txt-muted mb-1">端口</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.proxy.port || ''}
|
||||
@@ -408,7 +408,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
proxy: { ...formData.proxy, port: parseInt(e.target.value) || 0 }
|
||||
})}
|
||||
placeholder={formData.proxy.type === 'http' ? '8080' : '1080'}
|
||||
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 text-sm placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-content-primary text-sm placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -416,7 +416,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
{/* Proxy Auth */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">代理用户名 (可选)</label>
|
||||
<label className="block text-xs text-txt-muted mb-1">代理用户名 (可选)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.proxy.username}
|
||||
@@ -425,11 +425,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
proxy: { ...formData.proxy, username: e.target.value }
|
||||
})}
|
||||
placeholder="username"
|
||||
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 text-sm placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-content-primary text-sm placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">代理密码 (可选)</label>
|
||||
<label className="block text-xs text-txt-muted mb-1">代理密码 (可选)</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.proxy.password}
|
||||
@@ -438,7 +438,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
proxy: { ...formData.proxy, password: e.target.value }
|
||||
})}
|
||||
placeholder="password"
|
||||
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 text-sm placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-content-primary text-sm placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -451,7 +451,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
{/* Group and tags */}
|
||||
<div className="border-t border-dark-700 pt-4 grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-1">
|
||||
<Folder className="inline w-3 h-3 mr-1" />
|
||||
分组
|
||||
</label>
|
||||
@@ -460,11 +460,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
value={formData.group}
|
||||
onChange={(e) => setFormData({ ...formData, group: e.target.value })}
|
||||
placeholder="default"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">
|
||||
<label className="block text-sm font-medium text-txt-muted mb-1">
|
||||
<Tag className="inline w-3 h-3 mr-1" />
|
||||
标签
|
||||
</label>
|
||||
@@ -473,7 +473,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
value={formData.tags}
|
||||
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
|
||||
placeholder="生产, 阿里云"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -483,14 +483,14 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-400 hover:text-gray-200 transition-colors"
|
||||
className="px-4 py-2 text-txt-muted hover:text-content-primary transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="px-6 py-2 bg-accent-blue hover:bg-blue-600 disabled:opacity-50 rounded-lg text-white font-medium transition-colors"
|
||||
className="px-6 py-2 bg-surface-baseccent-blue hover:bg-surface-baselue-600 disabled:opacity-50 rounded-lg text-content-primary font-medium transition-colors"
|
||||
>
|
||||
{isSubmitting ? (isEditing ? '保存中...' : '创建中...') : (isEditing ? '保存' : '创建会话')}
|
||||
</button>
|
||||
|
||||
@@ -141,7 +141,7 @@ export default function SettingsView() {
|
||||
if (!isLoaded) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center bg-dark-900">
|
||||
<div className="text-gray-400">加载中...</div>
|
||||
<div className="text-txt-muted">加载中...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -197,8 +197,8 @@ export default function SettingsView() {
|
||||
onClick={() => setSetting('theme', theme.value as 'dark' | 'light' | 'system')}
|
||||
className={`p-3 rounded-lg border text-sm transition-colors ${
|
||||
settings.theme === theme.value
|
||||
? 'border-accent-blue bg-accent-blue/20 text-accent-blue'
|
||||
: 'border-dark-700/30 hover:border-dark-600 text-gray-300'
|
||||
? 'border-accent-blue bg-surface-baseccent-blue/20 text-accent-blue'
|
||||
: 'border-dark-700/30 hover:border-dark-600 text-content-secondary'
|
||||
}`}
|
||||
>
|
||||
{theme.label}
|
||||
@@ -212,7 +212,7 @@ export default function SettingsView() {
|
||||
<select
|
||||
value={settings.language}
|
||||
onChange={(e) => setSetting('language', e.target.value as 'zh-CN' | 'en-US')}
|
||||
className="w-full mt-3 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full mt-3 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
|
||||
>
|
||||
<option value="zh-CN">简体中文</option>
|
||||
<option value="en-US">English</option>
|
||||
@@ -229,11 +229,11 @@ export default function SettingsView() {
|
||||
<SettingSectionTitle>字体</SettingSectionTitle>
|
||||
<div className="grid grid-cols-2 gap-4 mt-3">
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">字体族</label>
|
||||
<label className="text-sm text-txt-muted">字体族</label>
|
||||
<select
|
||||
value={settings.fontFamily}
|
||||
onChange={(e) => setSetting('fontFamily', e.target.value)}
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
|
||||
>
|
||||
<option>JetBrains Mono</option>
|
||||
<option>Fira Code</option>
|
||||
@@ -242,11 +242,11 @@ export default function SettingsView() {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">字体大小</label>
|
||||
<label className="text-sm text-txt-muted">字体大小</label>
|
||||
<select
|
||||
value={settings.fontSize}
|
||||
onChange={(e) => setSetting('fontSize', parseInt(e.target.value))}
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
|
||||
>
|
||||
{[12, 13, 14, 15, 16, 18, 20].map((size) => (
|
||||
<option key={size} value={size}>{size}px</option>
|
||||
@@ -265,8 +265,8 @@ export default function SettingsView() {
|
||||
onClick={() => setSetting('terminalTheme', key as 'dracula' | 'monokai' | 'solarized' | 'one-dark')}
|
||||
className={`p-3 rounded-lg border text-sm transition-colors ${
|
||||
settings.terminalTheme === key
|
||||
? 'border-accent-blue bg-accent-blue/20 text-accent-blue'
|
||||
: 'border-dark-700/30 hover:border-dark-600 text-gray-300'
|
||||
? 'border-accent-blue bg-surface-baseccent-blue/20 text-accent-blue'
|
||||
: 'border-dark-700/30 hover:border-dark-600 text-content-secondary'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
@@ -299,7 +299,7 @@ export default function SettingsView() {
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={() => keybindingState.resetAll()}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-gray-400 hover:text-accent-blue rounded-lg hover:bg-dark-700/50 transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-txt-muted hover:text-accent-blue rounded-lg hover:bg-dark-700/50 transition-colors"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
重置全部为默认
|
||||
@@ -311,7 +311,7 @@ export default function SettingsView() {
|
||||
const defs = getKeybindingsByCategory(category)
|
||||
return (
|
||||
<SettingCard key={category}>
|
||||
<h3 className="text-sm font-semibold text-gray-300 mb-3">{category}</h3>
|
||||
<h3 className="text-sm font-semibold text-content-secondary mb-3">{category}</h3>
|
||||
<div className="space-y-1">
|
||||
{defs.map((def) => {
|
||||
const currentKeys = allKeys[def.id]
|
||||
@@ -323,14 +323,14 @@ export default function SettingsView() {
|
||||
key={def.id}
|
||||
className={`flex items-center justify-between px-3 py-2 rounded-lg transition-colors ${
|
||||
isRecordingThis
|
||||
? 'bg-accent-blue/10 border border-accent-blue/30'
|
||||
? 'bg-surface-baseccent-blue/10 border border-accent-blue/30'
|
||||
: 'hover:bg-dark-800/60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm text-gray-200">{def.label}</span>
|
||||
<span className="text-sm text-content-primary">{def.label}</span>
|
||||
{isCustom && (
|
||||
<span className="ml-2 text-[10px] text-accent-blue bg-accent-blue/10 px-1.5 py-0.5 rounded">
|
||||
<span className="ml-2 text-[10px] text-accent-blue bg-surface-baseccent-blue/10 px-1.5 py-0.5 rounded">
|
||||
自定义
|
||||
</span>
|
||||
)}
|
||||
@@ -339,12 +339,12 @@ export default function SettingsView() {
|
||||
<div className="flex items-center gap-2">
|
||||
{isRecordingThis ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<kbd className="inline-flex items-center px-3 py-1 rounded bg-accent-blue/20 border border-accent-blue/40 text-xs text-accent-blue font-mono animate-pulse">
|
||||
<kbd className="inline-flex items-center px-3 py-1 rounded bg-surface-baseccent-blue/20 border border-accent-blue/40 text-xs text-accent-blue font-mono animate-pulse">
|
||||
{recordedKeys || '按下快捷键...'}
|
||||
</kbd>
|
||||
<button
|
||||
onClick={() => stopRecording()}
|
||||
className="text-xs text-gray-500 hover:text-gray-300"
|
||||
className="text-xs text-txt-muted hover:text-content-secondary"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
@@ -368,8 +368,8 @@ export default function SettingsView() {
|
||||
<kbd
|
||||
className={`inline-flex items-center px-2.5 py-1 rounded text-xs font-mono transition-colors ${
|
||||
isCustom
|
||||
? 'bg-accent-blue/10 border border-accent-blue/30 text-accent-blue'
|
||||
: 'bg-dark-700 border border-dark-600 text-gray-300'
|
||||
? 'bg-surface-baseccent-blue/10 border border-accent-blue/30 text-accent-blue'
|
||||
: 'bg-dark-700 border border-dark-600 text-content-secondary'
|
||||
} group-hover:border-accent-blue/50`}
|
||||
>
|
||||
{currentKeys}
|
||||
@@ -378,7 +378,7 @@ export default function SettingsView() {
|
||||
{isCustom && (
|
||||
<button
|
||||
onClick={() => keybindingState.resetKeybinding(def.id)}
|
||||
className="text-gray-600 hover:text-gray-400 transition-colors"
|
||||
className="text-txt-faint hover:text-txt-muted transition-colors"
|
||||
title="重置为默认"
|
||||
>
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
@@ -398,7 +398,7 @@ export default function SettingsView() {
|
||||
{/* 录入确认弹层 */}
|
||||
{recordingId && recordedKeys && (
|
||||
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 px-5 py-3 bg-dark-800 border border-dark-600 rounded-xl shadow-2xl">
|
||||
<span className="text-sm text-gray-300">
|
||||
<span className="text-sm text-content-secondary">
|
||||
设置为 <kbd className="px-1.5 py-0.5 rounded bg-dark-700 text-accent-blue font-mono text-xs">{recordedKeys}</kbd>
|
||||
</span>
|
||||
<button
|
||||
@@ -406,20 +406,20 @@ export default function SettingsView() {
|
||||
await keybindingState.setCustomKeybinding(recordingId, recordedKeys)
|
||||
stopRecording()
|
||||
}}
|
||||
className="px-3 py-1 text-xs bg-accent-blue text-white rounded-lg hover:bg-accent-blue/80 transition-colors"
|
||||
className="px-3 py-1 text-xs bg-surface-baseccent-blue text-content-primary rounded-lg hover:bg-surface-baseccent-blue/80 transition-colors"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
<button
|
||||
onClick={stopRecording}
|
||||
className="px-3 py-1 text-xs text-gray-400 hover:text-gray-200 rounded-lg hover:bg-dark-700 transition-colors"
|
||||
className="px-3 py-1 text-xs text-txt-muted hover:text-content-primary rounded-lg hover:bg-dark-700 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-gray-600">
|
||||
<p className="text-xs text-txt-faint">
|
||||
点击快捷键可重新绑定 · 按 Esc 取消录入
|
||||
</p>
|
||||
</div>
|
||||
@@ -445,7 +445,7 @@ export default function SettingsView() {
|
||||
type="number"
|
||||
value={settings.defaultPort}
|
||||
onChange={(e) => setSetting('defaultPort', parseInt(e.target.value) || 22)}
|
||||
className="w-32 mt-2 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
className="w-32 mt-2 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</SettingCard>
|
||||
|
||||
@@ -455,7 +455,7 @@ export default function SettingsView() {
|
||||
type="number"
|
||||
value={settings.timeout}
|
||||
onChange={(e) => setSetting('timeout', parseInt(e.target.value) || 30)}
|
||||
className="w-32 mt-2 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
className="w-32 mt-2 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</SettingCard>
|
||||
|
||||
@@ -465,60 +465,60 @@ export default function SettingsView() {
|
||||
<span className="text-accent-blue text-xs font-semibold uppercase tracking-wider">Pro</span>
|
||||
<SettingSectionTitle className="mb-0">连接管理</SettingSectionTitle>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mb-4">配置连接池行为,优化资源使用和性能</p>
|
||||
<p className="text-sm text-txt-muted mb-4">配置连接池行为,优化资源使用和性能</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">最大连接数</label>
|
||||
<label className="text-sm text-txt-muted">最大连接数</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={50}
|
||||
value={settings.maxConnections || 10}
|
||||
onChange={(e) => setSetting('maxConnections', parseInt(e.target.value) || 10)}
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
<div className="text-xs text-gray-600 mt-1">最大并发 SSH 连接数</div>
|
||||
<div className="text-xs text-txt-faint mt-1">最大并发 SSH 连接数</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">最大后台连接</label>
|
||||
<label className="text-sm text-txt-muted">最大后台连接</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={20}
|
||||
value={settings.maxBackgroundConnections || 5}
|
||||
onChange={(e) => setSetting('maxBackgroundConnections', parseInt(e.target.value) || 5)}
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
<div className="text-xs text-gray-600 mt-1">非活跃标签保持连接数</div>
|
||||
<div className="text-xs text-txt-faint mt-1">非活跃标签保持连接数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">空闲超时 (分钟)</label>
|
||||
<label className="text-sm text-txt-muted">空闲超时 (分钟)</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={120}
|
||||
value={Math.floor((settings.idleTimeoutMs || 5 * 60 * 1000) / 60000)}
|
||||
onChange={(e) => setSetting('idleTimeoutMs', (parseInt(e.target.value) || 5) * 60000)}
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
<div className="text-xs text-gray-600 mt-1">无操作后进入挂起</div>
|
||||
<div className="text-xs text-txt-faint mt-1">无操作后进入挂起</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">挂起超时 (分钟)</label>
|
||||
<label className="text-sm text-txt-muted">挂起超时 (分钟)</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={240}
|
||||
value={Math.floor((settings.suspendTimeoutMs || 30 * 60 * 1000) / 60000)}
|
||||
onChange={(e) => setSetting('suspendTimeoutMs', (parseInt(e.target.value) || 30) * 60000)}
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
<div className="text-xs text-gray-600 mt-1">挂起后自动断开</div>
|
||||
<div className="text-xs text-txt-faint mt-1">挂起后自动断开</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -571,11 +571,11 @@ export default function SettingsView() {
|
||||
<SettingCard>
|
||||
<div className="flex flex-col items-center py-6">
|
||||
<div className="w-20 h-20 bg-dark-700 rounded-2xl flex items-center justify-center mb-4">
|
||||
<Terminal className="w-10 h-10 text-white" />
|
||||
<Terminal className="w-10 h-10 text-content-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-200 mb-1">OpsXShell</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">版本 {appVersion}</p>
|
||||
<p className="text-sm text-gray-400 max-w-md text-center">
|
||||
<h3 className="text-xl font-semibold text-content-primary mb-1">OpsXShell</h3>
|
||||
<p className="text-sm text-txt-muted mb-4">版本 {appVersion}</p>
|
||||
<p className="text-sm text-txt-muted max-w-md text-center">
|
||||
跨平台 SSH/SFTP 终端工具 · 简洁高效 · 安全可靠 · 操作便捷人性化
|
||||
</p>
|
||||
</div>
|
||||
@@ -595,8 +595,8 @@ export default function SettingsView() {
|
||||
['Lucide React', '0.294+'],
|
||||
].map(([name, ver]) => (
|
||||
<div key={name} className="flex justify-between py-1 border-b border-dark-700/20">
|
||||
<span className="text-gray-400">{name}</span>
|
||||
<span className="text-gray-500 font-mono">{ver}</span>
|
||||
<span className="text-txt-muted">{name}</span>
|
||||
<span className="text-txt-muted font-mono">{ver}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -604,10 +604,10 @@ export default function SettingsView() {
|
||||
|
||||
<SettingCard>
|
||||
<SettingSectionTitle>开源许可</SettingSectionTitle>
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
<p className="text-sm text-txt-muted mt-2">
|
||||
OpsXShell 基于 MIT 许可证开源。本项目使用了以下开源组件:
|
||||
</p>
|
||||
<ul className="mt-2 text-sm text-gray-500 list-disc list-inside space-y-1">
|
||||
<ul className="mt-2 text-sm text-txt-muted list-disc list-inside space-y-1">
|
||||
<li>ssh2 — MIT License</li>
|
||||
<li>xterm.js — MIT License</li>
|
||||
<li>zmodem-ts — MIT License</li>
|
||||
@@ -631,18 +631,18 @@ export default function SettingsView() {
|
||||
{/* 搜索框 */}
|
||||
<div className="p-3 border-b border-dark-700/20">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-500" />
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-txt-muted" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索设置..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-8 pr-8 py-1.5 text-sm bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 placeholder-gray-600 focus:outline-none focus:border-accent-blue/60"
|
||||
className="w-full pl-8 pr-8 py-1.5 text-sm bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary placeholder-ph focus:outline-none focus:border-accent-blue/60"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-txt-muted hover:text-content-secondary"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -661,14 +661,14 @@ export default function SettingsView() {
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-all ${
|
||||
active
|
||||
? 'bg-accent-blue text-white shadow-sm'
|
||||
: 'text-gray-400 hover:bg-dark-700/50 hover:text-gray-200'
|
||||
? 'bg-surface-baseccent-blue text-content-primary shadow-sm'
|
||||
: 'text-txt-muted hover:bg-dark-700/50 hover:text-content-primary'
|
||||
}`}
|
||||
>
|
||||
<Icon className={`w-4 h-4 flex-shrink-0 ${active ? 'text-white' : ''}`} />
|
||||
<Icon className={`w-4 h-4 flex-shrink-0 ${active ? 'text-content-primary' : ''}`} />
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium truncate">{tab.label}</div>
|
||||
<div className={`text-xs truncate ${active ? 'text-white/70' : 'text-gray-600'}`}>
|
||||
<div className={`text-xs truncate ${active ? 'text-content-primary/70' : 'text-txt-faint'}`}>
|
||||
{tab.description}
|
||||
</div>
|
||||
</div>
|
||||
@@ -682,8 +682,8 @@ export default function SettingsView() {
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Header bar */}
|
||||
<div className="flex-shrink-0 px-8 pt-6 pb-4 border-b border-dark-700/20">
|
||||
<h1 className="text-xl font-semibold text-gray-100">{currentMeta.label}</h1>
|
||||
<p className="text-sm text-gray-500 mt-0.5">{currentMeta.description}</p>
|
||||
<h1 className="text-xl font-semibold text-content-primary">{currentMeta.label}</h1>
|
||||
<p className="text-sm text-txt-muted mt-0.5">{currentMeta.description}</p>
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
@@ -727,7 +727,7 @@ function SettingSectionTitle({
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={`text-sm font-semibold text-gray-300 mb-1 ${className}`}>
|
||||
<div className={`text-sm font-semibold text-content-secondary mb-1 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -748,13 +748,13 @@ function SettingToggle({
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mr-4">
|
||||
<div className="font-medium text-gray-200 text-sm">{label}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{description}</div>
|
||||
<div className="font-medium text-content-primary text-sm">{label}</div>
|
||||
<div className="text-xs text-txt-muted mt-0.5">{description}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onChange(!checked)}
|
||||
className={`relative flex-shrink-0 w-11 h-6 rounded-full transition-colors ${
|
||||
checked ? 'bg-accent-blue' : 'bg-dark-600'
|
||||
checked ? 'bg-surface-baseccent-blue' : 'bg-dark-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -185,8 +185,8 @@ export default function Sidebar({ onNewSession, onQuickConnect, onEditSession }:
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-dark-700/20">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-8 h-8 bg-accent-blue rounded-lg flex items-center justify-center">
|
||||
<Server className="w-5 h-5 text-white" />
|
||||
<div className="w-8 h-8 bg-surface-baseccent-blue rounded-lg flex items-center justify-center">
|
||||
<Server className="w-5 h-5 text-content-primary" />
|
||||
</div>
|
||||
<span className="font-semibold text-lg">OpsXShell</span>
|
||||
</div>
|
||||
@@ -195,7 +195,7 @@ export default function Sidebar({ onNewSession, onQuickConnect, onEditSession }:
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onQuickConnect}
|
||||
className="flex-1 flex items-center justify-center gap-1 px-3 py-2 bg-accent-blue hover:bg-blue-600 rounded text-sm font-medium transition-colors"
|
||||
className="flex-1 flex items-center justify-center gap-1 px-3 py-2 bg-surface-baseccent-blue hover:bg-surface-baselue-600 rounded text-sm font-medium transition-colors"
|
||||
>
|
||||
<Zap className="w-4 h-4" />
|
||||
快速连接
|
||||
@@ -235,7 +235,7 @@ export default function Sidebar({ onNewSession, onQuickConnect, onEditSession }:
|
||||
<div className="p-3 border-t border-dark-700/20">
|
||||
<button
|
||||
onClick={() => addTab({ title: '设置', type: 'settings' })}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-400 hover:bg-dark-700 rounded transition-colors"
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-txt-muted hover:bg-dark-700 rounded transition-colors"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
<span>设置</span>
|
||||
@@ -263,7 +263,7 @@ export default function Sidebar({ onNewSession, onQuickConnect, onEditSession }:
|
||||
w-full px-4 py-2 text-left text-sm transition-colors
|
||||
${item.danger
|
||||
? 'text-red-400 hover:bg-red-500/20'
|
||||
: 'text-gray-300 hover:bg-dark-700'
|
||||
: 'text-content-secondary hover:bg-dark-700'
|
||||
}
|
||||
`}
|
||||
>
|
||||
@@ -279,10 +279,10 @@ export default function Sidebar({ onNewSession, onQuickConnect, onEditSession }:
|
||||
onMouseDown={handleResizeStart}
|
||||
className={`
|
||||
absolute top-0 right-0 bottom-0 w-1 cursor-col-resize
|
||||
hover:bg-accent-blue/50 active:bg-accent-blue/70
|
||||
hover:bg-surface-baseccent-blue/50 active:bg-surface-baseccent-blue/70
|
||||
hover:w-1.5 hover:shadow-[0_0_6px_rgba(59,130,246,0.4)]
|
||||
transition-all duration-150
|
||||
${isResizing ? 'bg-accent-blue/70 w-1.5 shadow-[0_0_6px_rgba(59,130,246,0.5)]' : ''}
|
||||
${isResizing ? 'bg-surface-baseccent-blue/70 w-1.5 shadow-[0_0_6px_rgba(59,130,246,0.5)]' : ''}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,7 @@ const STATUS_CONFIG: Record<string, StatusDisplay> = {
|
||||
disconnected: {
|
||||
icon: <WifiOff className="w-3 h-3" />,
|
||||
label: '未连接',
|
||||
colorClass: 'text-gray-400',
|
||||
colorClass: 'text-txt-muted',
|
||||
animateClass: '',
|
||||
},
|
||||
}
|
||||
@@ -163,7 +163,7 @@ export default function StatusBar() {
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="h-7 bg-dark-800 flex items-center px-3 text-xs text-white select-none">
|
||||
<div className="h-7 bg-dark-800 flex items-center px-3 text-xs text-content-primary select-none">
|
||||
{/* Left: Connection status area — clickable to toggle ConnectionPanel */}
|
||||
<button
|
||||
type="button"
|
||||
@@ -179,21 +179,21 @@ export default function StatusBar() {
|
||||
<span className="transition-colors duration-300">{statusCfg.label}</span>
|
||||
{hostLabel && (
|
||||
<>
|
||||
<span className="text-gray-500 mx-0.5">·</span>
|
||||
<span className="text-gray-300">{hostLabel}</span>
|
||||
<span className="text-txt-muted mx-0.5">·</span>
|
||||
<span className="text-content-secondary">{hostLabel}</span>
|
||||
</>
|
||||
)}
|
||||
{totalConnections > 0 && (
|
||||
<span className="ml-1 px-1.5 py-px rounded-full bg-accent-blue/20 text-accent-blue text-[10px] font-medium">
|
||||
<span className="ml-1 px-1.5 py-px rounded-full bg-surface-baseccent-blue/20 text-accent-blue text-[10px] font-medium">
|
||||
{totalConnections}
|
||||
</span>
|
||||
)}
|
||||
<ChevronUp className="w-3 h-3 text-gray-500 transition-transform duration-200 group-hover:rotate-180" />
|
||||
<ChevronUp className="w-3 h-3 text-txt-muted transition-transform duration-200 group-hover:rotate-180" />
|
||||
</button>
|
||||
|
||||
{/* Center: Transfer stats */}
|
||||
{transferLabel && (
|
||||
<div className="flex items-center gap-1 ml-4 text-gray-400">
|
||||
<div className="flex items-center gap-1 ml-4 text-txt-muted">
|
||||
<HardDrive className="w-3 h-3" />
|
||||
<span>{transferLabel}</span>
|
||||
</div>
|
||||
@@ -204,11 +204,11 @@ export default function StatusBar() {
|
||||
|
||||
{/* Right: Clock + version */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-1 text-gray-400 tabular-nums">
|
||||
<div className="flex items-center gap-1 text-txt-muted tabular-nums">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{formatTime(currentTime)}</span>
|
||||
</div>
|
||||
<span className="text-gray-500">OpsXShell v{version}</span>
|
||||
<span className="text-txt-muted">OpsXShell v{version}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -137,7 +137,7 @@ function TabBar() {
|
||||
{/* Add Tab Button */}
|
||||
<button
|
||||
onClick={() => addTab({ title: '新标签', type: 'welcome' })}
|
||||
className="flex items-center justify-center w-10 h-10 text-gray-500 hover:text-gray-300 hover:bg-dark-800 transition-colors"
|
||||
className="flex items-center justify-center w-10 h-10 text-txt-muted hover:text-content-secondary hover:bg-dark-800 transition-colors"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
<div className="h-full flex flex-col items-center justify-start bg-dark-900 p-8 overflow-y-auto">
|
||||
<div className="text-center mb-12 pt-8">
|
||||
<div className="w-20 h-20 bg-dark-700 rounded-2xl flex items-center justify-center mx-auto shadow-lg">
|
||||
<Terminal className="w-10 h-10 text-white" />
|
||||
<Terminal className="w-10 h-10 text-content-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,12 +36,12 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
onClick={onQuickConnect}
|
||||
className="flex items-center gap-4 p-6 bg-dark-800 hover:bg-dark-700 rounded-xl border border-dark-700/30 hover:border-accent-blue transition-all group"
|
||||
>
|
||||
<div className="w-12 h-12 bg-accent-blue/20 rounded-lg flex items-center justify-center group-hover:bg-accent-blue/30 transition-colors">
|
||||
<div className="w-12 h-12 bg-surface-baseccent-blue/20 rounded-lg flex items-center justify-center group-hover:bg-surface-baseccent-blue/30 transition-colors">
|
||||
<Zap className="w-6 h-6 text-accent-blue" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-semibold text-gray-200">快速连接</div>
|
||||
<div className="text-sm text-gray-500">输入地址快速建立连接</div>
|
||||
<div className="font-semibold text-content-primary">快速连接</div>
|
||||
<div className="text-sm text-txt-muted">输入地址快速建立连接</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -49,12 +49,12 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
onClick={onNewSession}
|
||||
className="flex items-center gap-4 p-6 bg-dark-800 hover:bg-dark-700 rounded-xl border border-dark-700/30 hover:border-accent-green transition-all group"
|
||||
>
|
||||
<div className="w-12 h-12 bg-accent-green/20 rounded-lg flex items-center justify-center group-hover:bg-accent-green/30 transition-colors">
|
||||
<Plus className="w-6 h-6 text-white" />
|
||||
<div className="w-12 h-12 bg-surface-baseccent-green/20 rounded-lg flex items-center justify-center group-hover:bg-surface-baseccent-green/30 transition-colors">
|
||||
<Plus className="w-6 h-6 text-content-primary" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-semibold text-gray-200">新建会话</div>
|
||||
<div className="text-sm text-gray-500">创建并保存新的会话</div>
|
||||
<div className="font-semibold text-content-primary">新建会话</div>
|
||||
<div className="text-sm text-txt-muted">创建并保存新的会话</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -62,12 +62,12 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
onClick={() => addTab({ title: 'SFTP', type: 'sftp' })}
|
||||
className="flex items-center gap-4 p-6 bg-dark-800 hover:bg-dark-700 rounded-xl border border-dark-700/30 hover:border-accent-purple transition-all group"
|
||||
>
|
||||
<div className="w-12 h-12 bg-accent-purple/20 rounded-lg flex items-center justify-center group-hover:bg-accent-purple/30 transition-colors">
|
||||
<div className="w-12 h-12 bg-surface-baseccent-purple/20 rounded-lg flex items-center justify-center group-hover:bg-surface-baseccent-purple/30 transition-colors">
|
||||
<FolderSync className="w-6 h-6 text-accent-purple" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-semibold text-gray-200">SFTP 传输</div>
|
||||
<div className="text-sm text-gray-500">可视化文件管理</div>
|
||||
<div className="font-semibold text-content-primary">SFTP 传输</div>
|
||||
<div className="text-sm text-txt-muted">可视化文件管理</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -79,15 +79,15 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
<ArrowRightLeft className="w-6 h-6 text-orange-400" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-semibold text-gray-200">端口转发</div>
|
||||
<div className="text-sm text-gray-500">本地/远程/SOCKS5 代理</div>
|
||||
<div className="font-semibold text-content-primary">端口转发</div>
|
||||
<div className="text-sm text-txt-muted">本地/远程/SOCKS5 代理</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Recent connections */}
|
||||
<div className="w-full max-w-2xl">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-4">最近连接</h3>
|
||||
<h3 className="text-sm font-medium text-txt-muted mb-4">最近连接</h3>
|
||||
{recentSessions.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={Clock}
|
||||
@@ -105,13 +105,13 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
<div className="flex items-center gap-3">
|
||||
<Globe className="w-5 h-5 text-accent-blue" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium text-gray-200">{session.name}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div className="font-medium text-content-primary">{session.name}</div>
|
||||
<div className="text-sm text-txt-muted">
|
||||
{session.username}@{session.host}{session.port !== 22 ? `:${session.port}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">--</span>
|
||||
<span className="text-xs text-txt-muted">--</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -119,7 +119,7 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
</div>
|
||||
|
||||
{/* Shortcuts hint */}
|
||||
<div className="mt-12 flex items-center gap-6 text-sm text-gray-500">
|
||||
<div className="mt-12 flex items-center gap-6 text-sm text-txt-muted">
|
||||
<span>快捷键:</span>
|
||||
<span><kbd className="px-2 py-1 bg-dark-800 rounded">Ctrl+N</kbd> 新建会话</span>
|
||||
<span><kbd className="px-2 py-1 bg-dark-800 rounded">Ctrl+K</kbd> 快速连接</span>
|
||||
|
||||
@@ -20,10 +20,10 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
}
|
||||
|
||||
const variantStyles = {
|
||||
primary: 'bg-accent-blue hover:bg-blue-600 text-white',
|
||||
secondary: 'bg-dark-700 hover:bg-dark-600 text-gray-200',
|
||||
danger: 'bg-red-600 hover:bg-red-500 text-white',
|
||||
ghost: 'hover:bg-dark-700 text-gray-400 hover:text-gray-200',
|
||||
primary: 'bg-surface-baseccent-blue hover:bg-surface-baselue-600 text-content-primary',
|
||||
secondary: 'bg-dark-700 hover:bg-dark-600 text-content-primary',
|
||||
danger: 'bg-red-600 hover:bg-red-500 text-content-primary',
|
||||
ghost: 'hover:bg-dark-700 text-txt-muted hover:text-content-primary',
|
||||
}
|
||||
|
||||
const sizeStyles = {
|
||||
|
||||
@@ -45,9 +45,9 @@ interface CommandPaletteProps {
|
||||
// ─── 类型 badge 颜色映射 ──────────────────────────────────────────────
|
||||
|
||||
const TYPE_BADGE: Record<CommandPaletteItemType, { label: string; className: string }> = {
|
||||
command: { label: '命令', className: 'bg-accent-blue/20 text-accent-blue' },
|
||||
session: { label: '会话', className: 'bg-accent-green/20 text-accent-green' },
|
||||
snippet: { label: '片段', className: 'bg-accent-purple/20 text-accent-purple' },
|
||||
command: { label: '命令', className: 'bg-surface-baseccent-blue/20 text-accent-blue' },
|
||||
session: { label: '会话', className: 'bg-surface-baseccent-green/20 text-accent-green' },
|
||||
snippet: { label: '片段', className: 'bg-surface-baseccent-purple/20 text-accent-purple' },
|
||||
}
|
||||
|
||||
// ─── 组件 ──────────────────────────────────────────────────────────────
|
||||
@@ -305,7 +305,7 @@ export function CommandPalette({
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm animate-fade-in" />
|
||||
<div className="absolute inset-0 bg-surface-baselack/60 backdrop-blur-sm animate-fade-in" />
|
||||
|
||||
{/* Palette container */}
|
||||
<div
|
||||
@@ -316,18 +316,18 @@ export function CommandPalette({
|
||||
>
|
||||
{/* Search input */}
|
||||
<div className="flex items-center gap-3 px-4 py-3 border-b border-dark-700">
|
||||
<Search size={18} className="text-gray-400 flex-shrink-0" />
|
||||
<Search size={18} className="text-txt-muted flex-shrink-0" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="搜索命令、会话或片段..."
|
||||
className="flex-1 bg-transparent text-gray-200 placeholder-gray-500 outline-none text-sm"
|
||||
className="flex-1 bg-transparent text-content-primary placeholder-ph outline-none text-sm"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<kbd className="hidden sm:inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] text-gray-500 bg-dark-700 rounded border border-dark-600">
|
||||
<kbd className="hidden sm:inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] text-txt-muted bg-dark-700 rounded border border-dark-600">
|
||||
ESC
|
||||
</kbd>
|
||||
</div>
|
||||
@@ -335,10 +335,10 @@ export function CommandPalette({
|
||||
{/* Results list */}
|
||||
<div ref={listRef} className="flex-1 overflow-y-auto py-1">
|
||||
{filteredItems.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
|
||||
<div className="flex flex-col items-center justify-center py-12 text-txt-muted">
|
||||
<Search size={32} className="mb-2 opacity-30" />
|
||||
<p className="text-sm">没有找到匹配的结果</p>
|
||||
<p className="text-xs text-gray-600 mt-1">试试其他关键词</p>
|
||||
<p className="text-xs text-txt-faint mt-1">试试其他关键词</p>
|
||||
</div>
|
||||
) : (
|
||||
groupedItems.map((group) => {
|
||||
@@ -348,14 +348,14 @@ export function CommandPalette({
|
||||
return (
|
||||
<div key={group.type}>
|
||||
{/* Group header */}
|
||||
<div className="flex items-center gap-2 px-4 py-1.5 text-[11px] font-medium text-gray-500 uppercase tracking-wider">
|
||||
<div className="flex items-center gap-2 px-4 py-1.5 text-[11px] font-medium text-txt-muted uppercase tracking-wider">
|
||||
<span
|
||||
className={`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold ${TYPE_BADGE[group.type].className}`}
|
||||
>
|
||||
{TYPE_BADGE[group.type].label}
|
||||
</span>
|
||||
<span className="flex-1 h-px bg-dark-700" />
|
||||
<span className="text-gray-600">{group.items.length}</span>
|
||||
<span className="text-txt-faint">{group.items.length}</span>
|
||||
</div>
|
||||
|
||||
{/* Group items */}
|
||||
@@ -369,8 +369,8 @@ export function CommandPalette({
|
||||
className={`
|
||||
flex items-center gap-3 px-4 py-2 cursor-pointer transition-colors duration-75
|
||||
${isSelected
|
||||
? 'bg-accent-blue/15 text-gray-100'
|
||||
: 'text-gray-300 hover:bg-dark-700'
|
||||
? 'bg-surface-baseccent-blue/15 text-content-primary'
|
||||
: 'text-content-secondary hover:bg-dark-700'
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
@@ -382,7 +382,7 @@ export function CommandPalette({
|
||||
{/* Icon */}
|
||||
<span
|
||||
className={`flex-shrink-0 w-5 h-5 flex items-center justify-center ${
|
||||
isSelected ? 'text-accent-blue' : 'text-gray-500'
|
||||
isSelected ? 'text-accent-blue' : 'text-txt-muted'
|
||||
}`}
|
||||
>
|
||||
{item.icon}
|
||||
@@ -392,7 +392,7 @@ export function CommandPalette({
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm truncate">{item.label}</div>
|
||||
{item.description && (
|
||||
<div className="text-[11px] text-gray-500 truncate">
|
||||
<div className="text-[11px] text-txt-muted truncate">
|
||||
{item.description}
|
||||
</div>
|
||||
)}
|
||||
@@ -400,7 +400,7 @@ export function CommandPalette({
|
||||
|
||||
{/* Shortcut */}
|
||||
{item.shortcut && (
|
||||
<kbd className="flex-shrink-0 text-[11px] text-gray-500 bg-dark-700 px-1.5 py-0.5 rounded border border-dark-600">
|
||||
<kbd className="flex-shrink-0 text-[11px] text-txt-muted bg-dark-700 px-1.5 py-0.5 rounded border border-dark-600">
|
||||
{item.shortcut}
|
||||
</kbd>
|
||||
)}
|
||||
@@ -421,7 +421,7 @@ export function CommandPalette({
|
||||
</div>
|
||||
|
||||
{/* Footer hint */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-t border-dark-700 text-[11px] text-gray-600">
|
||||
<div className="flex items-center justify-between px-4 py-2 border-t border-dark-700 text-[11px] text-txt-faint">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex items-center gap-1">
|
||||
<kbd className="px-1 py-0.5 bg-dark-700 rounded border border-dark-600">↑↓</kbd>
|
||||
|
||||
@@ -94,7 +94,7 @@ export function useConfirmDialog() {
|
||||
{state.danger && (
|
||||
<AlertTriangle className="w-5 h-5 text-red-400 mt-0.5 flex-shrink-0" />
|
||||
)}
|
||||
<p className="text-gray-300">{state.message}</p>
|
||||
<p className="text-content-secondary">{state.message}</p>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -18,13 +18,13 @@ export default function DefaultFallback({ error, onReset }: DefaultFallbackProps
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-200">出错了</h2>
|
||||
<p className="text-sm text-gray-500">应用遇到意外问题</p>
|
||||
<h2 className="text-lg font-semibold text-content-primary">出错了</h2>
|
||||
<p className="text-sm text-txt-muted">应用遇到意外问题</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-dark-900 rounded-lg p-4 mb-4">
|
||||
<p className="text-sm text-gray-300 mb-2">{appError.message}</p>
|
||||
<p className="text-sm text-content-secondary mb-2">{appError.message}</p>
|
||||
{appError.recoverable && (
|
||||
<p className="text-xs text-green-400">此错误可以恢复,请尝试刷新页面</p>
|
||||
)}
|
||||
@@ -33,19 +33,19 @@ export default function DefaultFallback({ error, onReset }: DefaultFallbackProps
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="flex-1 px-4 py-2 bg-accent-blue hover:bg-blue-600 text-white rounded-lg transition-colors text-sm font-medium"
|
||||
className="flex-1 px-4 py-2 bg-surface-baseccent-blue hover:bg-surface-baselue-600 text-content-primary rounded-lg transition-colors text-sm font-medium"
|
||||
>
|
||||
刷新页面
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 bg-dark-700 hover:bg-dark-600 text-gray-300 rounded-lg transition-colors text-sm"
|
||||
className="px-4 py-2 bg-dark-700 hover:bg-dark-600 text-content-secondary rounded-lg transition-colors text-sm"
|
||||
>
|
||||
重启应用
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-xs text-gray-600 text-center">
|
||||
<p className="mt-4 text-xs text-txt-faint text-center">
|
||||
如果问题持续存在,请检查日志或联系支持
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -38,10 +38,10 @@ export default function EmptyState({
|
||||
</div>
|
||||
|
||||
{/* 标题 */}
|
||||
<h3 className="text-gray-300 font-medium mb-1">{title}</h3>
|
||||
<h3 className="text-content-secondary font-medium mb-1">{title}</h3>
|
||||
|
||||
{/* 描述 */}
|
||||
<p className="text-gray-500 text-sm mb-4 text-center max-w-xs">{description}</p>
|
||||
<p className="text-txt-muted text-sm mb-4 text-center max-w-xs">{description}</p>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{action && (
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function Loading({
|
||||
<div className={`flex flex-col items-center gap-3 ${className}`}>
|
||||
<Loader2 className={`${sizeMap[size].icon} text-accent-blue animate-spin`} />
|
||||
{text && (
|
||||
<span className={`${sizeMap[size].text} text-gray-400`}>{text}</span>
|
||||
<span className={`${sizeMap[size].text} text-txt-muted`}>{text}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function Modal({
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
{/* 遮罩 */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-surface-baselack/60 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
@@ -82,12 +82,12 @@ export default function Modal({
|
||||
<div className="relative bg-dark-800 rounded-xl border border-dark-700 shadow-2xl w-full max-w-md">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-dark-700">
|
||||
<h3 className="text-lg font-semibold text-gray-200">{title}</h3>
|
||||
<h3 className="text-lg font-semibold text-content-primary">{title}</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-dark-700 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-gray-400" />
|
||||
<X className="w-5 h-5 text-txt-muted" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ export function usePromptDialog() {
|
||||
cancelText={state.cancelText}
|
||||
>
|
||||
<div>
|
||||
<p className="text-gray-300 mb-3">{state.message}</p>
|
||||
<p className="text-content-secondary mb-3">{state.message}</p>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
@@ -112,7 +112,7 @@ export function usePromptDialog() {
|
||||
onChange={(e) => setState((prev) => ({ ...prev, value: e.target.value }))}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={state.placeholder}
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -257,7 +257,7 @@ export function TabContentSkeleton({ className }: TabContentSkeletonProps) {
|
||||
return (
|
||||
<div
|
||||
className={mergeClass(
|
||||
'flex-1 bg-[#1a1b26] p-4 flex flex-col gap-1.5 font-mono text-sm',
|
||||
'flex-1 bg-surface-base p-4 flex flex-col gap-1.5 font-mono text-sm',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -287,7 +287,7 @@ export function TabContentSkeleton({ className }: TabContentSkeletonProps) {
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-accent-green/40 select-none">$</span>
|
||||
<span
|
||||
className="inline-block w-2 h-4 bg-gray-400 animate-pulse"
|
||||
className="inline-block w-2 h-4 bg-surface-active animate-pulse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,13 +32,13 @@ const colorMap: Record<ToastType, { bg: string; border: string; icon: string; te
|
||||
text: 'text-red-200',
|
||||
},
|
||||
warning: {
|
||||
bg: 'bg-amber-900/40',
|
||||
bg: 'bg-surface-basember-900/40',
|
||||
border: 'border-amber-700/60',
|
||||
icon: 'text-amber-400',
|
||||
text: 'text-amber-200',
|
||||
},
|
||||
info: {
|
||||
bg: 'bg-blue-900/40',
|
||||
bg: 'bg-surface-baselue-900/40',
|
||||
border: 'border-blue-700/60',
|
||||
icon: 'text-blue-400',
|
||||
text: 'text-blue-200',
|
||||
@@ -109,7 +109,7 @@ function ToastItemView({ toast, onClose }: ToastItemProps) {
|
||||
className="p-0.5 rounded hover:bg-white/10 transition-colors shrink-0"
|
||||
aria-label="关闭通知"
|
||||
>
|
||||
<X className="w-4 h-4 text-gray-400 hover:text-gray-200" />
|
||||
<X className="w-4 h-4 text-txt-muted hover:text-content-primary" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ const iconMap: Record<ToastType, React.ReactNode> = {
|
||||
}
|
||||
|
||||
const bgMap: Record<ToastType, string> = {
|
||||
info: 'bg-blue-900/80 border-blue-700',
|
||||
info: 'bg-surface-baselue-900/80 border-blue-700',
|
||||
success: 'bg-green-900/80 border-green-700',
|
||||
error: 'bg-red-900/80 border-red-700',
|
||||
warning: 'bg-yellow-900/80 border-yellow-700',
|
||||
@@ -32,7 +32,7 @@ export function ToastContainer({ toasts, onRemove }: ToastContainerProps) {
|
||||
{toasts.map((toast) => (
|
||||
<div
|
||||
key={toast.id}
|
||||
className={`pointer-events-auto flex items-center gap-2 px-4 py-3 rounded-lg border shadow-lg backdrop-blur-sm text-sm text-white min-w-[280px] max-w-[400px] animate-fade-in ${bgMap[toast.type]}`}
|
||||
className={`pointer-events-auto flex items-center gap-2 px-4 py-3 rounded-lg border shadow-lg backdrop-blur-sm text-sm text-content-primary min-w-[280px] max-w-[400px] animate-fade-in ${bgMap[toast.type]}`}
|
||||
>
|
||||
{iconMap[toast.type]}
|
||||
<span className="flex-1">{toast.message}</span>
|
||||
@@ -40,7 +40,7 @@ export function ToastContainer({ toasts, onRemove }: ToastContainerProps) {
|
||||
onClick={() => onRemove(toast.id)}
|
||||
className="p-0.5 hover:bg-white/10 rounded"
|
||||
>
|
||||
<X className="w-3.5 h-3.5 text-gray-300" />
|
||||
<X className="w-3.5 h-3.5 text-content-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -20,11 +20,11 @@ export function Breadcrumb({ path, onNavigate, onHome }: BreadcrumbProps) {
|
||||
className="p-1.5 hover:bg-dark-700 rounded transition-colors flex-shrink-0"
|
||||
title="主目录"
|
||||
>
|
||||
<Home className="w-4 h-4 text-gray-400" />
|
||||
<Home className="w-4 h-4 text-txt-muted" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onNavigate('/')}
|
||||
className="px-1.5 py-0.5 hover:bg-dark-700 rounded text-gray-400 hover:text-dark-200 transition-colors"
|
||||
className="px-1.5 py-0.5 hover:bg-dark-700 rounded text-txt-muted hover:text-dark-200 transition-colors"
|
||||
>
|
||||
/
|
||||
</button>
|
||||
@@ -33,7 +33,7 @@ export function Breadcrumb({ path, onNavigate, onHome }: BreadcrumbProps) {
|
||||
<ChevronRight className="w-3 h-3 text-dark-600 mx-0.5 flex-shrink-0" />
|
||||
<button
|
||||
onClick={() => onNavigate('/' + parts.slice(0, index + 1).join('/'))}
|
||||
className="px-1.5 py-0.5 hover:bg-dark-700 rounded text-gray-200 hover:text-white transition-colors truncate max-w-[120px]"
|
||||
className="px-1.5 py-0.5 hover:bg-dark-700 rounded text-content-primary hover:text-content-primary transition-colors truncate max-w-[120px]"
|
||||
>
|
||||
{part}
|
||||
</button>
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function ChmodDialog({
|
||||
onChange={() => togglePerm(scope, perm)}
|
||||
className="w-4 h-4 rounded border-dark-500 bg-dark-700 text-accent-blue focus:ring-accent-blue focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-gray-300 text-sm">{label}</span>
|
||||
<span className="text-content-secondary text-sm">{label}</span>
|
||||
</label>
|
||||
)
|
||||
|
||||
@@ -120,7 +120,7 @@ export default function ChmodDialog({
|
||||
label: string
|
||||
}) => (
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-gray-400 w-16 text-sm">{label}</span>
|
||||
<span className="text-txt-muted w-16 text-sm">{label}</span>
|
||||
<PermCheckbox scope={scope} perm="read" label="读取" />
|
||||
<PermCheckbox scope={scope} perm="write" label="写入" />
|
||||
<PermCheckbox scope={scope} perm="execute" label="执行" />
|
||||
@@ -141,14 +141,14 @@ export default function ChmodDialog({
|
||||
<div className="flex items-center gap-3 pb-3 border-b border-dark-600/30">
|
||||
<Shield className="w-5 h-5 text-accent-blue" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-white font-medium truncate">{fileName}</p>
|
||||
<p className="text-gray-500 text-xs font-mono truncate">{filePath}</p>
|
||||
<p className="text-content-primary font-medium truncate">{fileName}</p>
|
||||
<p className="text-txt-muted text-xs font-mono truncate">{filePath}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current octal mode display */}
|
||||
<div className="flex items-center justify-center gap-2 py-2">
|
||||
<span className="text-gray-400 text-sm">当前权限:</span>
|
||||
<span className="text-txt-muted text-sm">当前权限:</span>
|
||||
<span className="text-accent-blue font-mono font-bold text-lg">{octalString}</span>
|
||||
</div>
|
||||
|
||||
@@ -156,9 +156,9 @@ export default function ChmodDialog({
|
||||
<div className="space-y-3 bg-dark-700 rounded-lg p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="w-16" />
|
||||
<span className="text-gray-500 text-xs w-16">读取 (r)</span>
|
||||
<span className="text-gray-500 text-xs w-16">写入 (w)</span>
|
||||
<span className="text-gray-500 text-xs w-16">执行 (x)</span>
|
||||
<span className="text-txt-muted text-xs w-16">读取 (r)</span>
|
||||
<span className="text-txt-muted text-xs w-16">写入 (w)</span>
|
||||
<span className="text-txt-muted text-xs w-16">执行 (x)</span>
|
||||
</div>
|
||||
<PermRow scope="owner" label="所有者" />
|
||||
<PermRow scope="group" label="所属组" />
|
||||
|
||||
@@ -42,7 +42,7 @@ export function ConnectionPanel({
|
||||
// Connecting State
|
||||
if (isConnecting) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-accent-blue/10 border-b border-accent-blue/20">
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-surface-baseccent-blue/10 border-b border-accent-blue/20">
|
||||
<Loader2 className="w-4 h-4 text-accent-blue animate-spin" />
|
||||
<span className="text-sm text-accent-blue">连接中...</span>
|
||||
</div>
|
||||
@@ -71,10 +71,10 @@ export function ConnectionPanel({
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-dark-800 border-b border-dark-700/30">
|
||||
<span className="text-sm text-gray-400">未连接</span>
|
||||
<span className="text-sm text-txt-muted">未连接</span>
|
||||
<button
|
||||
onClick={onShowSelector}
|
||||
className="px-3 py-1.5 bg-accent-blue hover:bg-blue-600 rounded text-sm font-medium transition-colors"
|
||||
className="px-3 py-1.5 bg-surface-baseccent-blue hover:bg-surface-baselue-600 rounded text-sm font-medium transition-colors"
|
||||
>
|
||||
选择服务器
|
||||
</button>
|
||||
|
||||
@@ -16,14 +16,14 @@ export function ConnectionSelector({ sessions, onSelect, onClose }: ConnectionSe
|
||||
<div className="absolute inset-0 bg-dark-900/90 flex items-center justify-center z-40">
|
||||
<div className="bg-dark-800 rounded-xl border border-dark-700 shadow-2xl w-full max-w-md overflow-hidden">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-dark-700/30">
|
||||
<h3 className="text-lg font-medium text-white">选择服务器</h3>
|
||||
<h3 className="text-lg font-medium text-content-primary">选择服务器</h3>
|
||||
<button onClick={onClose} className="p-1 hover:bg-dark-700 rounded">
|
||||
<X className="w-5 h-5 text-gray-400" />
|
||||
<X className="w-5 h-5 text-txt-muted" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 max-h-80 overflow-y-auto">
|
||||
{sessions.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<div className="text-center py-8 text-txt-muted">
|
||||
<Monitor className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p>暂无保存的会话</p>
|
||||
<p className="text-sm mt-1">请先在侧边栏创建会话</p>
|
||||
@@ -38,12 +38,12 @@ export function ConnectionSelector({ sessions, onSelect, onClose }: ConnectionSe
|
||||
>
|
||||
<Monitor className="w-8 h-8 text-cyan-400 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-slate-200 font-medium truncate group-hover:text-white">{session.name}</div>
|
||||
<div className="text-sm text-gray-500 truncate">
|
||||
<div className="text-slate-200 font-medium truncate group-hover:text-content-primary">{session.name}</div>
|
||||
<div className="text-sm text-txt-muted truncate">
|
||||
{session.username}@{session.host}:{session.port}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-slate-600 group-hover:text-gray-400 transition-colors" />
|
||||
<ChevronRight className="w-5 h-5 text-slate-600 group-hover:text-txt-muted transition-colors" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
|
||||
onClose()
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2 text-sm transition-colors ${
|
||||
item.danger ? 'text-red-400 hover:bg-red-500/20' : 'text-gray-200 hover:bg-dark-700 hover:text-white'
|
||||
item.danger ? 'text-red-400 hover:bg-red-500/20' : 'text-content-primary hover:bg-dark-700 hover:text-content-primary'
|
||||
}`}
|
||||
>
|
||||
{item.icon}
|
||||
|
||||
@@ -156,7 +156,7 @@ export function FileListPanel({
|
||||
</div>
|
||||
|
||||
{/* Column Headers */}
|
||||
<div className="flex items-center px-4 py-2 border-b border-dark-700/20 text-xs text-gray-400">
|
||||
<div className="flex items-center px-4 py-2 border-b border-dark-700/20 text-xs text-txt-muted">
|
||||
<SortHeader sortField={sortField} sortOrder={sortOrder} onSort={onSort} />
|
||||
</div>
|
||||
|
||||
@@ -185,7 +185,7 @@ export function FileListPanel({
|
||||
|
||||
{/* Drag overlay - pointer-events-none so it doesn't interfere with drop events */}
|
||||
{isDragOver && (
|
||||
<div className="absolute inset-0 bg-accent-blue/10 border-2 border-dashed border-accent-blue rounded-lg flex items-center justify-center pointer-events-none z-10">
|
||||
<div className="absolute inset-0 bg-surface-baseccent-blue/10 border-2 border-dashed border-accent-blue rounded-lg flex items-center justify-center pointer-events-none z-10">
|
||||
<div className="flex flex-col items-center gap-2 text-accent-blue">
|
||||
<Upload className="w-10 h-10" />
|
||||
<span className="text-sm font-medium">拖拽文件到此处上传</span>
|
||||
@@ -195,7 +195,7 @@ export function FileListPanel({
|
||||
</div>
|
||||
|
||||
{/* Status Bar */}
|
||||
<div className="px-4 py-2 border-t border-dark-700/20 text-xs text-gray-400">
|
||||
<div className="px-4 py-2 border-t border-dark-700/20 text-xs text-txt-muted">
|
||||
{loading ? '加载中...' : selected.size > 0 ? `已选择 ${selected.size} 项` : `共 ${files.length} 项`}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -128,13 +128,13 @@ export default function FilePropertiesDialog({
|
||||
<TypeIcon className="w-8 h-8 text-accent-blue" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-white font-semibold truncate">{fileName}</p>
|
||||
<p className="text-gray-400 text-xs">{typeLabel}</p>
|
||||
<p className="text-content-primary font-semibold truncate">{fileName}</p>
|
||||
<p className="text-txt-muted text-xs">{typeLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="flex items-center gap-2 text-gray-400 text-sm">
|
||||
<div className="flex items-center gap-2 text-txt-muted text-sm">
|
||||
<div className="animate-spin h-4 w-4 border-2 border-accent-blue border-t-transparent rounded-full" />
|
||||
<span>加载详细信息...</span>
|
||||
</div>
|
||||
@@ -169,10 +169,10 @@ function PropertyRow({
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-gray-500 mt-0.5 shrink-0">{icon}</span>
|
||||
<span className="text-gray-400 w-20 shrink-0">{label}</span>
|
||||
<span className="text-txt-muted mt-0.5 shrink-0">{icon}</span>
|
||||
<span className="text-txt-muted w-20 shrink-0">{label}</span>
|
||||
<span
|
||||
className={`text-gray-200 break-all ${monospace ? 'font-mono text-xs' : ''}`}
|
||||
className={`text-content-primary break-all ${monospace ? 'font-mono text-xs' : ''}`}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function FileRow({ file, isSelected, onSelect, onOpen, onContextMenu, onD
|
||||
draggable={file.type === 'file'}
|
||||
onDragStart={onDragStart}
|
||||
className={`h-10 flex items-center px-4 cursor-pointer border-b border-dark-700/20 transition-colors group ${
|
||||
isSelected ? 'bg-accent-blue/20 hover:bg-accent-blue/25' : 'hover:bg-dark-700/50'
|
||||
isSelected ? 'bg-surface-baseccent-blue/20 hover:bg-surface-baseccent-blue/25' : 'hover:bg-dark-700/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 flex items-center gap-2 min-w-0">
|
||||
@@ -33,16 +33,16 @@ export function FileRow({ file, isSelected, onSelect, onOpen, onContextMenu, onD
|
||||
) : file.type === 'symlink' ? (
|
||||
<Folder className="w-4 h-4 text-yellow-400 flex-shrink-0" />
|
||||
) : (
|
||||
<File className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
||||
<File className="w-4 h-4 text-txt-muted flex-shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-gray-200 truncate group-hover:text-white">{file.name}</span>
|
||||
<span className="text-sm text-content-primary truncate group-hover:text-content-primary">{file.name}</span>
|
||||
{file.type === 'symlink' && <span className="text-xs text-yellow-500">→</span>}
|
||||
</div>
|
||||
<div className="w-32 text-right text-xs text-gray-500">{formatDate(file.modified)}</div>
|
||||
<div className="w-24 text-right text-xs text-gray-500">
|
||||
<div className="w-32 text-right text-xs text-txt-muted">{formatDate(file.modified)}</div>
|
||||
<div className="w-24 text-right text-xs text-txt-muted">
|
||||
{file.type === 'directory' ? '--' : formatSize(file.size)}
|
||||
</div>
|
||||
<div className="w-24 text-right text-xs text-gray-500 font-mono">
|
||||
<div className="w-24 text-right text-xs text-txt-muted font-mono">
|
||||
{file.type === 'directory'
|
||||
? '文件夹'
|
||||
: file.type === 'symlink'
|
||||
|
||||
@@ -20,12 +20,12 @@ export function SortHeader({ onSort, sortField, sortOrder }: SortHeaderProps) {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="h-9 flex items-center px-4 bg-dark-700/50 text-xs text-gray-500 border-b border-dark-700/30 select-none">
|
||||
<div className="h-9 flex items-center px-4 bg-dark-700/50 text-xs text-txt-muted border-b border-dark-700/30 select-none">
|
||||
{fields.map(({ key, label, width, align }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => onSort(key)}
|
||||
className={`${width} flex items-center justify-${align} gap-1 hover:text-gray-200 transition-colors`}
|
||||
className={`${width} flex items-center justify-${align} gap-1 hover:text-content-primary transition-colors`}
|
||||
>
|
||||
<span>{label}</span>
|
||||
{sortField === key && (sortOrder === 'asc' ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />)}
|
||||
|
||||
@@ -21,7 +21,7 @@ export function Toolbar({ isConnected, onUpload, onDownload, onRefresh, onNewFol
|
||||
<button
|
||||
onClick={onUpload}
|
||||
disabled={!isConnected}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-accent-blue hover:bg-blue-600 disabled:bg-dark-700 disabled:text-gray-500 rounded text-sm font-medium transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-surface-baseccent-blue hover:bg-surface-baselue-600 disabled:bg-dark-700 disabled:text-txt-muted rounded text-sm font-medium transition-colors"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
上传
|
||||
@@ -29,7 +29,7 @@ export function Toolbar({ isConnected, onUpload, onDownload, onRefresh, onNewFol
|
||||
<button
|
||||
onClick={onDownload}
|
||||
disabled={!isConnected}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-dark-700 hover:bg-dark-600 disabled:text-gray-500 rounded text-sm font-medium transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-dark-700 hover:bg-dark-600 disabled:text-txt-muted rounded text-sm font-medium transition-colors"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
下载
|
||||
@@ -37,7 +37,7 @@ export function Toolbar({ isConnected, onUpload, onDownload, onRefresh, onNewFol
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={!isConnected}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-dark-700 hover:bg-dark-600 disabled:text-gray-500 rounded text-sm font-medium transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-dark-700 hover:bg-dark-600 disabled:text-txt-muted rounded text-sm font-medium transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
刷新
|
||||
@@ -45,7 +45,7 @@ export function Toolbar({ isConnected, onUpload, onDownload, onRefresh, onNewFol
|
||||
<button
|
||||
onClick={onNewFolder}
|
||||
disabled={!isConnected}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-dark-700 hover:bg-dark-600 disabled:text-gray-500 rounded text-sm font-medium transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-dark-700 hover:bg-dark-600 disabled:text-txt-muted rounded text-sm font-medium transition-colors"
|
||||
>
|
||||
<FolderPlus className="w-4 h-4" />
|
||||
新建文件夹
|
||||
|
||||
@@ -21,19 +21,19 @@ export function TransferItem({ task, onCancel, onRemove }: TransferItemProps) {
|
||||
{task.type === 'download' ? <Download className="w-4 h-4" /> : <Upload className="w-4 h-4" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-gray-200 truncate">{task.fileName}</div>
|
||||
<div className="text-sm text-content-primary truncate">{task.fileName}</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
{isActive && (
|
||||
<>
|
||||
<div className="flex-1 h-1.5 bg-dark-700 rounded-full overflow-hidden max-w-[120px]">
|
||||
<div className="h-full bg-accent-blue transition-all duration-300" style={{ width: `${task.progress}%` }} />
|
||||
<div className="h-full bg-surface-baseccent-blue transition-all duration-300" style={{ width: `${task.progress}%` }} />
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">{task.progress}%</span>
|
||||
{task.speed !== undefined && <span className="text-xs text-gray-500">{formatSpeed(task.speed)}</span>}
|
||||
<span className="text-xs text-txt-muted">{task.progress}%</span>
|
||||
{task.speed !== undefined && <span className="text-xs text-txt-muted">{formatSpeed(task.speed)}</span>}
|
||||
</>
|
||||
)}
|
||||
{task.status === 'pending' && (
|
||||
<span className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<span className="text-xs text-txt-muted flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
等待中
|
||||
</span>
|
||||
@@ -51,7 +51,7 @@ export function TransferItem({ task, onCancel, onRemove }: TransferItemProps) {
|
||||
</span>
|
||||
)}
|
||||
{task.status === 'canceled' && (
|
||||
<span className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<span className="text-xs text-txt-muted flex items-center gap-1">
|
||||
<XCircle className="w-3 h-3" />
|
||||
已取消
|
||||
</span>
|
||||
@@ -62,7 +62,7 @@ export function TransferItem({ task, onCancel, onRemove }: TransferItemProps) {
|
||||
{task.status === 'transferring' && (
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="p-1.5 hover:bg-dark-700 rounded text-gray-400 hover:text-red-400"
|
||||
className="p-1.5 hover:bg-dark-700 rounded text-txt-muted hover:text-red-400"
|
||||
title="取消"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
@@ -71,7 +71,7 @@ export function TransferItem({ task, onCancel, onRemove }: TransferItemProps) {
|
||||
{(task.status === 'completed' || task.status === 'failed' || task.status === 'canceled') && (
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="p-1.5 hover:bg-dark-700 rounded text-gray-400 hover:text-gray-200"
|
||||
className="p-1.5 hover:bg-dark-700 rounded text-txt-muted hover:text-content-primary"
|
||||
title="移除"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
|
||||
@@ -30,7 +30,7 @@ export function TransferPanel({ tasks, isOpen, onToggle, onCancel, onRemove }: T
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">传输队列</span>
|
||||
{activeCount > 0 && (
|
||||
<span className="px-2 py-0.5 bg-accent-blue/20 text-accent-blue text-xs rounded-full">
|
||||
<span className="px-2 py-0.5 bg-surface-baseccent-blue/20 text-accent-blue text-xs rounded-full">
|
||||
{activeCount} 进行中
|
||||
</span>
|
||||
)}
|
||||
@@ -41,7 +41,7 @@ export function TransferPanel({ tasks, isOpen, onToggle, onCancel, onRemove }: T
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500">{tasks.length} 个任务</span>
|
||||
<span className="text-xs text-txt-muted">{tasks.length} 个任务</span>
|
||||
{isOpen ? <ChevronDown className="w-4 h-4" /> : <ChevronUp className="w-4 h-4" />}
|
||||
</div>
|
||||
</button>
|
||||
@@ -61,7 +61,7 @@ export function TransferPanel({ tasks, isOpen, onToggle, onCancel, onRemove }: T
|
||||
)}
|
||||
|
||||
{isOpen && tasks.length === 0 && (
|
||||
<div className="py-4 text-center text-gray-500 text-sm">
|
||||
<div className="py-4 text-center text-txt-muted text-sm">
|
||||
暂无传输任务
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,7 @@ interface SearchBoxProps {
|
||||
export function SearchBox({ value, onChange, placeholder = '搜索...' }: SearchBoxProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-txt-muted" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -66,7 +66,7 @@ export function SessionTree({
|
||||
{/* Group Header */}
|
||||
<button
|
||||
onClick={() => onToggleGroup(group)}
|
||||
className="w-full flex items-center gap-2 px-4 py-2 text-sm text-gray-400 hover:bg-dark-700 transition-colors"
|
||||
className="w-full flex items-center gap-2 px-4 py-2 text-sm text-txt-muted hover:bg-dark-700 transition-colors"
|
||||
>
|
||||
<ChevronRight
|
||||
className="w-4 h-4 transition-transform duration-200 ease-in-out"
|
||||
@@ -74,7 +74,7 @@ export function SessionTree({
|
||||
/>
|
||||
<Folder className="w-4 h-4" />
|
||||
<span className="flex-1 text-left">{group === 'default' ? '默认分组' : group}</span>
|
||||
<span className="text-xs text-gray-500">{groupSessions.length}</span>
|
||||
<span className="text-xs text-txt-muted">{groupSessions.length}</span>
|
||||
</button>
|
||||
|
||||
{/* Sessions - CSS Grid collapse animation */}
|
||||
@@ -100,8 +100,8 @@ export function SessionTree({
|
||||
>
|
||||
<Server className="w-4 h-4 text-accent-green flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-200 truncate">{session.name}</div>
|
||||
<div className="text-xs text-gray-500 truncate">{session.host}:{session.port}</div>
|
||||
<div className="font-medium text-content-primary truncate">{session.name}</div>
|
||||
<div className="text-xs text-txt-muted truncate">{session.host}:{session.port}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -54,37 +54,37 @@ function SnippetFormModal({ isOpen, initial, title, confirmText, onConfirm, onCl
|
||||
<div className="space-y-4">
|
||||
{/* 名称 */}
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">名称</label>
|
||||
<label className="block text-sm text-txt-muted mb-1">名称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
||||
placeholder="例如:重启 Nginx"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-accent-blue text-sm"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 命令内容 */}
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">命令</label>
|
||||
<label className="block text-sm text-txt-muted mb-1">命令</label>
|
||||
<textarea
|
||||
value={form.content}
|
||||
onChange={(e) => setForm((f) => ({ ...f, content: e.target.value }))}
|
||||
placeholder="sudo systemctl restart nginx"
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-accent-blue text-sm font-mono resize-none"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue text-sm font-mono resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 标签 */}
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">标签 <span className="text-gray-600">(逗号分隔)</span></label>
|
||||
<label className="block text-sm text-txt-muted mb-1">标签 <span className="text-txt-faint">(逗号分隔)</span></label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.tags}
|
||||
onChange={(e) => setForm((f) => ({ ...f, tags: e.target.value }))}
|
||||
placeholder="linux, nginx, ops"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-accent-blue text-sm"
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,26 +203,26 @@ export default function SnippetPanel() {
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center gap-2 px-4 py-3 border-b border-dark-700/30">
|
||||
<Code2 className="w-5 h-5 text-accent-blue flex-shrink-0" />
|
||||
<h2 className="text-sm font-semibold text-gray-200">命令片段</h2>
|
||||
<h2 className="text-sm font-semibold text-content-primary">命令片段</h2>
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="p-3 border-b border-dark-700/30">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-txt-muted" />
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="搜索片段..."
|
||||
className="w-full pl-9 pr-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-sm text-white placeholder-gray-500 focus:outline-none focus:border-accent-blue"
|
||||
className="w-full pl-9 pr-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-sm text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
{query && (
|
||||
<button
|
||||
onClick={() => setQuery('')}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 hover:bg-dark-600 rounded"
|
||||
>
|
||||
<X className="w-3.5 h-3.5 text-gray-500" />
|
||||
<X className="w-3.5 h-3.5 text-txt-muted" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -307,7 +307,7 @@ function SnippetItem({ snippet, copied, canInsert, onInsert, onCopy, onEdit, onD
|
||||
<div className="group rounded-lg border border-dark-700 hover:border-dark-600 bg-dark-800 p-2.5 transition-colors duration-150">
|
||||
{/* 名称行 */}
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<span className="text-sm font-medium text-gray-200 truncate flex-1" title={snippet.name}>
|
||||
<span className="text-sm font-medium text-content-primary truncate flex-1" title={snippet.name}>
|
||||
{snippet.name}
|
||||
</span>
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity duration-150 flex-shrink-0">
|
||||
@@ -316,7 +316,7 @@ function SnippetItem({ snippet, copied, canInsert, onInsert, onCopy, onEdit, onD
|
||||
onClick={() => onInsert(snippet.content)}
|
||||
disabled={!canInsert}
|
||||
title={canInsert ? '插入到终端' : '无活跃终端'}
|
||||
className="p-1 rounded hover:bg-accent-blue/20 text-accent-blue disabled:text-gray-600 disabled:hover:bg-transparent transition-colors"
|
||||
className="p-1 rounded hover:bg-surface-baseccent-blue/20 text-accent-blue disabled:text-txt-faint disabled:hover:bg-transparent transition-colors"
|
||||
>
|
||||
<Code2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -324,7 +324,7 @@ function SnippetItem({ snippet, copied, canInsert, onInsert, onCopy, onEdit, onD
|
||||
<button
|
||||
onClick={() => onCopy(snippet)}
|
||||
title="复制"
|
||||
className="p-1 rounded hover:bg-dark-600 text-gray-400 transition-colors"
|
||||
className="p-1 rounded hover:bg-dark-600 text-txt-muted transition-colors"
|
||||
>
|
||||
{copied ? (
|
||||
<span className="text-xs text-green-400 px-1 animate-scale-in">✓</span>
|
||||
@@ -336,7 +336,7 @@ function SnippetItem({ snippet, copied, canInsert, onInsert, onCopy, onEdit, onD
|
||||
<button
|
||||
onClick={() => onEdit(snippet)}
|
||||
title="编辑"
|
||||
className="p-1 rounded hover:bg-dark-600 text-gray-400 transition-colors"
|
||||
className="p-1 rounded hover:bg-dark-600 text-txt-muted transition-colors"
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -348,13 +348,13 @@ function SnippetItem({ snippet, copied, canInsert, onInsert, onCopy, onEdit, onD
|
||||
await onDelete(snippet.id)
|
||||
setConfirmDelete(false)
|
||||
}}
|
||||
className="px-1.5 py-0.5 text-xs bg-red-600 hover:bg-red-500 text-white rounded transition-colors"
|
||||
className="px-1.5 py-0.5 text-xs bg-red-600 hover:bg-red-500 text-content-primary rounded transition-colors"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmDelete(false)}
|
||||
className="px-1.5 py-0.5 text-xs text-gray-400 hover:text-gray-200 transition-colors"
|
||||
className="px-1.5 py-0.5 text-xs text-txt-muted hover:text-content-primary transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
@@ -363,7 +363,7 @@ function SnippetItem({ snippet, copied, canInsert, onInsert, onCopy, onEdit, onD
|
||||
<button
|
||||
onClick={() => setConfirmDelete(true)}
|
||||
title="删除"
|
||||
className="p-1 rounded hover:bg-red-500/20 text-gray-400 hover:text-red-400 transition-colors"
|
||||
className="p-1 rounded hover:bg-red-500/20 text-txt-muted hover:text-red-400 transition-colors"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -373,7 +373,7 @@ function SnippetItem({ snippet, copied, canInsert, onInsert, onCopy, onEdit, onD
|
||||
|
||||
{/* 命令预览 */}
|
||||
<p
|
||||
className="text-xs font-mono text-gray-500 truncate mb-1.5"
|
||||
className="text-xs font-mono text-txt-muted truncate mb-1.5"
|
||||
title={snippet.content}
|
||||
>
|
||||
{preview}
|
||||
@@ -385,7 +385,7 @@ function SnippetItem({ snippet, copied, canInsert, onInsert, onCopy, onEdit, onD
|
||||
{snippet.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-1.5 py-0.5 text-[10px] bg-dark-700 text-gray-400 rounded"
|
||||
className="px-1.5 py-0.5 text-[10px] bg-dark-700 text-txt-muted rounded"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
|
||||
@@ -77,7 +77,7 @@ export function TabContextMenu({
|
||||
w-full px-4 py-2 text-left text-sm transition-colors
|
||||
${item.danger
|
||||
? 'text-red-400 hover:bg-red-500/20'
|
||||
: 'text-gray-300 hover:bg-dark-700'
|
||||
: 'text-content-secondary hover:bg-dark-700'
|
||||
}
|
||||
`}
|
||||
>
|
||||
|
||||
@@ -63,19 +63,19 @@ export function TabItem({
|
||||
border-r border-dark-700/20 cursor-pointer select-none
|
||||
transition-all duration-150 relative
|
||||
${isActive
|
||||
? 'bg-dark-900 text-gray-200'
|
||||
: 'bg-dark-800 text-gray-500 hover:bg-dark-700 hover:text-gray-400'
|
||||
? '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'}
|
||||
`}
|
||||
>
|
||||
{/* Left drop indicator */}
|
||||
{showLeftIndicator && (
|
||||
<div className="absolute left-0 top-0 bottom-0 w-0.5 bg-accent-blue z-10 transition-all duration-150" />
|
||||
<div className="absolute left-0 top-0 bottom-0 w-0.5 bg-surface-baseccent-blue z-10 transition-all duration-150" />
|
||||
)}
|
||||
{/* Right drop indicator */}
|
||||
{showRightIndicator && (
|
||||
<div className="absolute right-0 top-0 bottom-0 w-0.5 bg-accent-blue z-10 transition-all duration-150" />
|
||||
<div className="absolute right-0 top-0 bottom-0 w-0.5 bg-surface-baseccent-blue z-10 transition-all duration-150" />
|
||||
)}
|
||||
|
||||
{/* Status indicator */}
|
||||
@@ -110,7 +110,7 @@ export function TabItem({
|
||||
{/* Active tab bottom indicator — always rendered for width transition */}
|
||||
<div
|
||||
className={`
|
||||
absolute bottom-0 left-0 right-0 h-0.5 bg-accent-blue
|
||||
absolute bottom-0 left-0 right-0 h-0.5 bg-surface-baseccent-blue
|
||||
transition-[opacity,transform] duration-150 origin-left
|
||||
${isActive ? 'opacity-100 scale-x-100' : 'opacity-0 scale-x-0'}
|
||||
`}
|
||||
|
||||
@@ -25,17 +25,17 @@ function TerminalOverlay({
|
||||
{connectionState === 'disconnected' && needsReconnect && (
|
||||
<div className="absolute inset-0 bg-dark-900/90 flex items-center justify-center z-10">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-accent-blue/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<div className="w-16 h-16 bg-surface-baseccent-blue/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<TerminalIcon className="w-8 h-8 text-accent-blue" />
|
||||
</div>
|
||||
<div className="text-gray-200 mb-2 text-lg">会话已恢复</div>
|
||||
<div className="text-gray-400 text-sm mb-6">
|
||||
<div className="text-content-primary mb-2 text-lg">会话已恢复</div>
|
||||
<div className="text-txt-muted text-sm mb-6">
|
||||
上次关闭时的终端标签已恢复<br />
|
||||
点击下方按钮重新建立 SSH 连接
|
||||
</div>
|
||||
<button
|
||||
onClick={onReconnect}
|
||||
className="px-6 py-2.5 bg-accent-blue hover:bg-blue-500 text-white rounded-lg transition-colors"
|
||||
className="px-6 py-2.5 bg-surface-baseccent-blue hover:bg-surface-baselue-500 text-content-primary rounded-lg transition-colors"
|
||||
>
|
||||
重新连接
|
||||
</button>
|
||||
@@ -48,18 +48,18 @@ function TerminalOverlay({
|
||||
<div className="absolute inset-0 bg-dark-900/90 flex items-center justify-center z-10">
|
||||
<div className="text-center">
|
||||
<div className="text-yellow-400 mb-2 text-lg">连接已挂起</div>
|
||||
<div className="text-gray-400 text-sm mb-4">
|
||||
<div className="text-txt-muted text-sm mb-4">
|
||||
长时间未操作,连接已进入挂起状态
|
||||
</div>
|
||||
<button
|
||||
onClick={onResume}
|
||||
className="px-4 py-2 bg-accent-blue hover:bg-blue-500 text-white rounded transition-colors mr-2"
|
||||
className="px-4 py-2 bg-surface-baseccent-blue hover:bg-surface-baselue-500 text-content-primary rounded transition-colors mr-2"
|
||||
>
|
||||
恢复连接
|
||||
</button>
|
||||
<button
|
||||
onClick={onReconnect}
|
||||
className="px-4 py-2 bg-dark-700 hover:bg-dark-600 text-gray-200 rounded transition-colors"
|
||||
className="px-4 py-2 bg-dark-700 hover:bg-dark-600 text-content-primary rounded transition-colors"
|
||||
>
|
||||
重新连接
|
||||
</button>
|
||||
@@ -72,10 +72,10 @@ function TerminalOverlay({
|
||||
<div className="absolute inset-0 bg-dark-900/80 flex items-center justify-center z-10">
|
||||
<div className="text-center">
|
||||
<div className="text-red-400 mb-2">连接失败</div>
|
||||
<div className="text-gray-400 text-sm mb-4">{statusMessage}</div>
|
||||
<div className="text-txt-muted text-sm mb-4">{statusMessage}</div>
|
||||
<button
|
||||
onClick={onReconnect}
|
||||
className="px-4 py-2 bg-accent-blue hover:bg-blue-500 text-white rounded transition-colors"
|
||||
className="px-4 py-2 bg-surface-baseccent-blue hover:bg-surface-baselue-500 text-content-primary rounded transition-colors"
|
||||
>
|
||||
重新连接
|
||||
</button>
|
||||
|
||||
@@ -24,7 +24,7 @@ function TerminalSearchBar({
|
||||
}: TerminalSearchBarProps) {
|
||||
return (
|
||||
<div className="absolute top-0 left-0 right-0 z-30 flex items-center gap-2 px-3 py-2 bg-dark-800/95 border-b border-dark-600 backdrop-blur-sm">
|
||||
<Search size={14} className="text-gray-400 shrink-0" />
|
||||
<Search size={14} className="text-txt-muted shrink-0" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
@@ -41,30 +41,30 @@ function TerminalSearchBar({
|
||||
e.stopPropagation()
|
||||
}}
|
||||
placeholder="搜索..."
|
||||
className="flex-1 bg-dark-900 text-gray-200 text-sm px-2 py-1 rounded border border-dark-600 focus:border-accent-blue focus:outline-none placeholder-gray-500"
|
||||
className="flex-1 bg-dark-900 text-content-primary text-sm px-2 py-1 rounded border border-dark-600 focus:border-accent-blue focus:outline-none placeholder-ph"
|
||||
/>
|
||||
{searchResultCount !== undefined && (
|
||||
<span className="text-xs text-gray-400 whitespace-nowrap min-w-[60px] text-center">
|
||||
<span className="text-xs text-txt-muted whitespace-nowrap min-w-[60px] text-center">
|
||||
{searchResultIndex ?? 0}/{searchResultCount}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={onSearchPrevious}
|
||||
className="p-1 hover:bg-dark-600 rounded text-gray-400 hover:text-gray-200 transition-colors"
|
||||
className="p-1 hover:bg-dark-600 rounded text-txt-muted hover:text-content-primary transition-colors"
|
||||
title="上一个 (Shift+Enter)"
|
||||
>
|
||||
<ChevronUp size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onSearchNext}
|
||||
className="p-1 hover:bg-dark-600 rounded text-gray-400 hover:text-gray-200 transition-colors"
|
||||
className="p-1 hover:bg-dark-600 rounded text-txt-muted hover:text-content-primary transition-colors"
|
||||
title="下一个 (Enter)"
|
||||
>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-dark-600 rounded text-gray-400 hover:text-gray-200 transition-colors"
|
||||
className="p-1 hover:bg-dark-600 rounded text-txt-muted hover:text-content-primary transition-colors"
|
||||
title="关闭 (Escape)"
|
||||
>
|
||||
<X size={16} />
|
||||
|
||||
Reference in new issue
Block a user