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:
OpsXShell Dev committed 2026-04-25 19:31:51 +08:00
1 parent 07c4a5b14f
commit e9a0d21758
44 files changed
+593 -464

No files matched your search

+2 -2
View File
@@ -244,7 +244,7 @@ function App() {
}, [handleExportSessions, handleImportSessions, handleOpenSettings]) }, [handleExportSessions, handleImportSessions, handleOpenSettings])
return ( return (
<div className="flex flex-col h-screen bg-dark-900 text-gray-200" style={{ backgroundColor: '#0f172a' }}> <div className="flex flex-col h-screen bg-dark-900 text-content-primary">
{/* Title bar spacer for macOS — left area draggable, right area handles double-click */} {/* Title bar spacer for macOS — left area draggable, right area handles double-click */}
<div className="h-8 bg-dark-900 flex"> <div className="h-8 bg-dark-900 flex">
<div className="w-20 drag" /> <div className="w-20 drag" />
@@ -299,7 +299,7 @@ function App() {
{isMobile && ( {isMobile && (
<button <button
onClick={toggleMobileSidebar} onClick={toggleMobileSidebar}
className="flex items-center justify-center w-10 h-10 text-gray-400 hover:text-gray-200 hover:bg-dark-800 transition-colors" className="flex items-center justify-center w-10 h-10 text-txt-muted hover:text-content-primary hover:bg-dark-800 transition-colors"
aria-label="Toggle sidebar" aria-label="Toggle sidebar"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+13 -13
View File
@@ -52,16 +52,16 @@ const STATUS_DOT_COLORS: Record<ConnectionState, string> = {
connected: 'bg-green-400', connected: 'bg-green-400',
connecting: 'bg-yellow-400 animate-pulse', connecting: 'bg-yellow-400 animate-pulse',
background: 'bg-green-400 opacity-60', background: 'bg-green-400 opacity-60',
suspended: 'bg-gray-400', suspended: 'bg-surface-active',
error: 'bg-red-400', error: 'bg-red-400',
disconnected: 'bg-gray-500', disconnected: 'bg-surface-active',
reconnecting: 'bg-yellow-400 animate-pulse', reconnecting: 'bg-yellow-400 animate-pulse',
} }
function StatusDot({ state }: StatusDotProps) { function StatusDot({ state }: StatusDotProps) {
return ( return (
<span <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 justify-between">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<StatusDot state={conn.state} /> <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} {conn.config.username}@{conn.config.host}
</span> </span>
</div> </div>
@@ -116,10 +116,10 @@ function ConnectionCard({ conn, onReconnect, onDisconnect, reconnectingId }: Con
: conn.state === 'background' : conn.state === 'background'
? 'bg-green-400/10 text-green-400 opacity-70' ? 'bg-green-400/10 text-green-400 opacity-70'
: conn.state === 'suspended' : conn.state === 'suspended'
? 'bg-gray-400/10 text-gray-400' ? 'bg-surface-active/10 text-txt-muted'
: conn.state === 'error' : conn.state === 'error'
? 'bg-red-400/10 text-red-400' ? '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]} {STATUS_LABELS[conn.state]}
@@ -127,7 +127,7 @@ function ConnectionCard({ conn, onReconnect, onDisconnect, reconnectingId }: Con
</div> </div>
{/* Details */} {/* 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 */} {/* Port */}
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Activity className="w-3 h-3" /> <Activity className="w-3 h-3" />
@@ -167,7 +167,7 @@ function ConnectionCard({ conn, onReconnect, onDisconnect, reconnectingId }: Con
onClick={() => onReconnect(conn.connectionId)} onClick={() => onReconnect(conn.connectionId)}
disabled={isReconnecting} disabled={isReconnecting}
className="flex items-center gap-1 px-2 py-1 rounded text-xs 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" disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
> >
<RefreshCw className={`w-3 h-3 ${isReconnecting ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-3 h-3 ${isReconnecting ? 'animate-spin' : ''}`} />
@@ -208,7 +208,7 @@ interface StatsBarProps {
function StatsOverview({ total, connected, background, error }: StatsBarProps) { function StatsOverview({ total, connected, background, error }: StatsBarProps) {
const items = [ 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: connected, color: 'text-green-400' },
{ label: '后台', value: background, color: 'text-yellow-400' }, { label: '后台', value: background, color: 'text-yellow-400' },
{ label: '断开/错误', value: error, color: 'text-red-400' }, { label: '断开/错误', value: error, color: 'text-red-400' },
@@ -219,7 +219,7 @@ function StatsOverview({ total, connected, background, error }: StatsBarProps) {
{items.map((item) => ( {items.map((item) => (
<div key={item.label} className="bg-dark-900 rounded-lg p-2 text-center"> <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-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>
))} ))}
</div> </div>
@@ -274,7 +274,7 @@ export default function ConnectionPanel() {
<> <>
{/* Backdrop */} {/* Backdrop */}
<div <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} onClick={close}
/> />
@@ -286,14 +286,14 @@ export default function ConnectionPanel() {
> >
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-4 h-12 border-b border-dark-700/30 shrink-0"> <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" /> <Wifi className="w-4 h-4 text-accent-blue" />
连接管理 连接管理
</h2> </h2>
<button <button
type="button" type="button"
onClick={close} 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" /> <X className="w-4 h-4" />
</button> </button>
+24 -24
View File
@@ -168,11 +168,11 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
{needsReconnect && currentTab?.sessionId && ( {needsReconnect && currentTab?.sessionId && (
<div className="absolute inset-0 bg-dark-900/90 flex items-center justify-center z-50"> <div className="absolute inset-0 bg-dark-900/90 flex items-center justify-center z-50">
<div className="text-center"> <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" /> <ArrowRightLeft className="w-8 h-8 text-accent-blue" />
</div> </div>
<div className="text-gray-200 mb-2 text-lg">会话已恢复</div> <div className="text-content-primary mb-2 text-lg">会话已恢复</div>
<div className="text-gray-400 text-sm mb-6"> <div className="text-txt-muted text-sm mb-6">
上次关闭时的端口转发标签已恢复<br /> 上次关闭时的端口转发标签已恢复<br />
需要重新建立 SSH 连接才能使用 需要重新建立 SSH 连接才能使用
</div> </div>
@@ -181,7 +181,7 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
updateTab(tabId, { needsReconnect: false }) 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> </button>
@@ -194,8 +194,8 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<ArrowRightLeft className="w-6 h-6 text-accent-blue" /> <ArrowRightLeft className="w-6 h-6 text-accent-blue" />
<div> <div>
<h2 className="text-lg font-semibold text-white">端口转发</h2> <h2 className="text-lg font-semibold text-content-primary">端口转发</h2>
<p className="text-sm text-gray-500"> <p className="text-sm text-txt-muted">
{connection {connection
? `已连接: ${connection.config.username}@${connection.config.host}` ? `已连接: ${connection.config.username}@${connection.config.host}`
: '未连接 - 请先建立 SSH 连接'} : '未连接 - 请先建立 SSH 连接'}
@@ -205,7 +205,7 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
<button <button
onClick={() => setShowAddModal(true)} onClick={() => setShowAddModal(true)}
disabled={!connection} 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" /> <Plus className="w-4 h-4" />
添加规则 添加规则
@@ -215,7 +215,7 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
{/* Rules List */} {/* Rules List */}
<div className="flex-1 overflow-y-auto p-6"> <div className="flex-1 overflow-y-auto p-6">
{rules.length === 0 ? ( {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" /> <Globe className="w-16 h-16 mb-4 opacity-50" />
<p className="text-lg">暂无端口转发规则</p> <p className="text-lg">暂无端口转发规则</p>
<p className="text-sm mt-1">点击右上角添加规则</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"> <div className="flex items-center gap-4">
{/* Type Icon */} {/* Type Icon */}
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${ <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' : rule.type === 'remote' ? 'bg-purple-500/20 text-purple-400' :
'bg-orange-500/20 text-orange-400' 'bg-orange-500/20 text-orange-400'
}`}> }`}>
@@ -247,9 +247,9 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
{/* Rule Info */} {/* Rule Info */}
<div> <div>
<div className="flex items-center gap-2"> <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 ${ <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' : rule.type === 'remote' ? 'bg-purple-500/20 text-purple-400' :
'bg-orange-500/20 text-orange-400' 'bg-orange-500/20 text-orange-400'
}`}> }`}>
@@ -262,7 +262,7 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
</span> </span>
)} )}
</div> </div>
<div className="text-sm text-gray-400 mt-1"> <div className="text-sm text-txt-muted mt-1">
{rule.type === 'dynamic' ? ( {rule.type === 'dynamic' ? (
<span>SOCKS5 代理: 127.0.0.1:{rule.localPort}</span> <span>SOCKS5 代理: 127.0.0.1:{rule.localPort}</span>
) : rule.type === 'local' ? ( ) : rule.type === 'local' ? (
@@ -320,7 +320,7 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
{showAddModal && ( {showAddModal && (
<div className="absolute inset-0 bg-dark-900/90 flex items-center justify-center z-50"> <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"> <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 */} {/* Type Selection */}
<div className="grid grid-cols-3 gap-2 mb-4"> <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' })} onClick={() => setNewRule({ ...newRule, type: t.type as 'local' | 'remote' | 'dynamic' })}
className={`p-3 rounded-lg border text-left transition-colors ${ className={`p-3 rounded-lg border text-left transition-colors ${
newRule.type === t.type newRule.type === t.type
? 'bg-accent-blue/20 border-accent-blue text-white' ? 'bg-surface-baseccent-blue/20 border-accent-blue text-content-primary'
: 'bg-dark-700 border-dark-600 text-gray-400 hover:bg-dark-600' : 'bg-dark-700 border-dark-600 text-txt-muted hover:bg-dark-600'
}`} }`}
> >
<div className="text-sm font-medium">{t.label}</div> <div className="text-sm font-medium">{t.label}</div>
@@ -347,19 +347,19 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
{/* Form Fields */} {/* Form Fields */}
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm text-gray-400 mb-1">规则名称</label> <label className="block text-sm text-txt-muted mb-1">规则名称</label>
<input <input
type="text" type="text"
value={newRule.name} value={newRule.name}
onChange={(e) => setNewRule({ ...newRule, name: e.target.value })} onChange={(e) => setNewRule({ ...newRule, name: e.target.value })}
placeholder="例如: MySQL 转发" 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>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm text-gray-400 mb-1"> <label className="block text-sm text-txt-muted mb-1">
{newRule.type === 'remote' ? '远程端口' : '本地端口'} {newRule.type === 'remote' ? '远程端口' : '本地端口'}
</label> </label>
<input <input
@@ -367,19 +367,19 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
value={newRule.localPort || ''} value={newRule.localPort || ''}
onChange={(e) => setNewRule({ ...newRule, localPort: parseInt(e.target.value) })} onChange={(e) => setNewRule({ ...newRule, localPort: parseInt(e.target.value) })}
placeholder="例如: 3306" 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> </div>
{newRule.type !== 'dynamic' && ( {newRule.type !== 'dynamic' && (
<div> <div>
<label className="block text-sm text-gray-400 mb-1">远程端口</label> <label className="block text-sm text-txt-muted mb-1">远程端口</label>
<input <input
type="number" type="number"
value={newRule.remotePort || ''} value={newRule.remotePort || ''}
onChange={(e) => setNewRule({ ...newRule, remotePort: parseInt(e.target.value) })} onChange={(e) => setNewRule({ ...newRule, remotePort: parseInt(e.target.value) })}
placeholder="例如: 3306" 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> </div>
)} )}
@@ -387,13 +387,13 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
{newRule.type === 'local' && ( {newRule.type === 'local' && (
<div> <div>
<label className="block text-sm text-gray-400 mb-1">远程主机</label> <label className="block text-sm text-txt-muted mb-1">远程主机</label>
<input <input
type="text" type="text"
value={newRule.remoteHost} value={newRule.remoteHost}
onChange={(e) => setNewRule({ ...newRule, remoteHost: e.target.value })} onChange={(e) => setNewRule({ ...newRule, remoteHost: e.target.value })}
placeholder="例如: localhost 或 192.168.1.100" 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> </div>
)} )}
@@ -409,7 +409,7 @@ export default function PortForwardView({ tabId, isActive: _isActive = true }: P
</button> </button>
<button <button
onClick={handleAddRule} 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> </button>
+16 -16
View File
@@ -60,22 +60,22 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
} }
return ( 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"> <div className="w-full max-w-md bg-dark-800 rounded-xl border border-dark-700 shadow-2xl animate-fade-in">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-6 border-b border-dark-700"> <div className="flex items-center justify-between p-6 border-b border-dark-700">
<div className="flex items-center gap-3"> <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" /> <Zap className="w-5 h-5 text-accent-blue" />
</div> </div>
<div> <div>
<h2 className="text-lg font-semibold text-gray-200">快速连接</h2> <h2 className="text-lg font-semibold text-content-primary">快速连接</h2>
<p className="text-sm text-gray-500">临时连接到服务器</p> <p className="text-sm text-txt-muted">临时连接到服务器</p>
</div> </div>
</div> </div>
<button <button
onClick={onClose} 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" /> <X className="w-5 h-5" />
</button> </button>
@@ -86,7 +86,7 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
{/* Host and Port */} {/* Host and Port */}
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
<div className="col-span-2"> <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" /> <Globe className="inline w-3 h-3 mr-1" />
主机地址 主机地址
</label> </label>
@@ -97,11 +97,11 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
value={formData.host} value={formData.host}
onChange={(e) => setFormData({ ...formData, host: e.target.value })} onChange={(e) => setFormData({ ...formData, host: e.target.value })}
placeholder="192.168.1.1" 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>
<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> </label>
<input <input
@@ -109,14 +109,14 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
value={formData.port} value={formData.port}
onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) })} onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) })}
placeholder="22" 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> </div>
{/* Username */} {/* Username */}
<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">
<User className="inline w-3 h-3 mr-1" /> <User className="inline w-3 h-3 mr-1" />
用户名 用户名
</label> </label>
@@ -126,13 +126,13 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
value={formData.username} value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })} onChange={(e) => setFormData({ ...formData, username: e.target.value })}
placeholder="root" 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> </div>
{/* Password */} {/* Password */}
<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">
<Lock className="inline w-3 h-3 mr-1" /> <Lock className="inline w-3 h-3 mr-1" />
密码 密码
</label> </label>
@@ -141,7 +141,7 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
value={formData.password} value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })} onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder="••••••••" 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>
@@ -155,7 +155,7 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
{/* Hint */} {/* Hint */}
<div className="p-3 bg-dark-700/50 rounded-lg"> <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> </p>
</div> </div>
@@ -165,14 +165,14 @@ export default function QuickConnectDialog({ isOpen, onClose }: QuickConnectDial
<button <button
type="button" type="button"
onClick={onClose} 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>
<button <button
type="submit" type="submit"
disabled={isConnecting} 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 ? ( {isConnecting ? (
'连接中...' '连接中...'
+44 -44
View File
@@ -142,22 +142,22 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
} }
return ( 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"> <div className="w-full max-w-lg bg-dark-800 rounded-xl border border-dark-700 shadow-2xl animate-fade-in">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-6 border-b border-dark-700"> <div className="flex items-center justify-between p-6 border-b border-dark-700">
<div className="flex items-center gap-3"> <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" /> <Server className="w-5 h-5 text-accent-green" />
</div> </div>
<div> <div>
<h2 className="text-lg font-semibold text-gray-200">{isEditing ? '编辑会话' : '新建会话'}</h2> <h2 className="text-lg font-semibold text-content-primary">{isEditing ? '编辑会话' : '新建会话'}</h2>
<p className="text-sm text-gray-500">{isEditing ? '修改 SSH 连接配置' : '配置 SSH 连接信息'}</p> <p className="text-sm text-txt-muted">{isEditing ? '修改 SSH 连接配置' : '配置 SSH 连接信息'}</p>
</div> </div>
</div> </div>
<button <button
onClick={onClose} 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" /> <X className="w-5 h-5" />
</button> </button>
@@ -168,7 +168,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
{/* Basic info */} {/* Basic info */}
<div className="space-y-4"> <div className="space-y-4">
<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> </label>
<input <input
@@ -177,29 +177,29 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="例如: 生产服务器" 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>
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
<div className="col-span-2"> <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> </label>
<div className="relative"> <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 <input
type="text" type="text"
required required
value={formData.host} value={formData.host}
onChange={(e) => setFormData({ ...formData, host: e.target.value })} onChange={(e) => setFormData({ ...formData, host: e.target.value })}
placeholder="192.168.1.1" 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> </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> </label>
<input <input
@@ -207,24 +207,24 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
value={formData.port} value={formData.port}
onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) })} onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) })}
placeholder="22" 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> </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> </label>
<div className="relative"> <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 <input
type="text" type="text"
required required
value={formData.username} value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })} onChange={(e) => setFormData({ ...formData, username: e.target.value })}
placeholder="root" 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>
</div> </div>
@@ -232,7 +232,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
{/* Authentication */} {/* Authentication */}
<div className="border-t border-dark-700 pt-4"> <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> </label>
<div className="flex gap-2 mb-4"> <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 })} 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 ${ 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 formData.authType === type.value
? 'border-accent-blue bg-accent-blue/20 text-accent-blue' ? 'border-accent-blue bg-surface-baseccent-blue/20 text-accent-blue'
: 'border-dark-700 hover:border-dark-600 text-gray-400' : 'border-dark-700 hover:border-dark-600 text-txt-muted'
}`} }`}
> >
<Icon className="w-4 h-4" /> <Icon className="w-4 h-4" />
@@ -261,17 +261,17 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
{formData.authType === 'password' && ( {formData.authType === 'password' && (
<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> </label>
<div className="relative"> <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 <input
type="password" type="password"
value={formData.password} value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })} onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder="••••••••" 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>
</div> </div>
@@ -280,7 +280,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
{formData.authType === 'key' && ( {formData.authType === 'key' && (
<div className="space-y-3"> <div className="space-y-3">
<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> </label>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -289,11 +289,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
value={formData.privateKey} value={formData.privateKey}
onChange={(e) => setFormData({ ...formData, privateKey: e.target.value })} onChange={(e) => setFormData({ ...formData, privateKey: e.target.value })}
placeholder="~/.ssh/id_rsa" 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 <button
type="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 () => { onClick={async () => {
const result = await window.electronAPI?.showOpenDialog({ const result = await window.electronAPI?.showOpenDialog({
title: '选择私钥文件', title: '选择私钥文件',
@@ -315,7 +315,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
</div> </div>
</div> </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> </label>
<input <input
@@ -323,7 +323,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
value={formData.passphrase} value={formData.passphrase}
onChange={(e) => setFormData({ ...formData, passphrase: e.target.value })} onChange={(e) => setFormData({ ...formData, passphrase: e.target.value })}
placeholder="••••••••" 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>
</div> </div>
@@ -335,11 +335,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
<button <button
type="button" type="button"
onClick={() => setShowProxySettings(!showProxySettings)} 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" /> <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()})` : '(未配置)'} {formData.proxy.type !== 'none' && formData.proxy.host ? `(已配置 ${formData.proxy.type.toUpperCase()})` : '(未配置)'}
</span> </span>
<svg <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 ${ className={`flex-1 px-3 py-1.5 rounded-lg border text-xs transition-colors ${
formData.proxy.type === type.value formData.proxy.type === type.value
? 'border-accent-blue bg-accent-blue/20 text-accent-blue' ? 'border-accent-blue bg-surface-baseccent-blue/20 text-accent-blue'
: 'border-dark-700 hover:border-dark-600 text-gray-500' : 'border-dark-700 hover:border-dark-600 text-txt-muted'
}`} }`}
> >
{type.label} {type.label}
@@ -386,7 +386,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
{/* Proxy Host & Port */} {/* Proxy Host & Port */}
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
<div className="col-span-2"> <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 <input
type="text" type="text"
value={formData.proxy.host} value={formData.proxy.host}
@@ -395,11 +395,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
proxy: { ...formData.proxy, host: e.target.value } proxy: { ...formData.proxy, host: e.target.value }
})} })}
placeholder="proxy.example.com" 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>
<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 <input
type="number" type="number"
value={formData.proxy.port || ''} 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 } proxy: { ...formData.proxy, port: parseInt(e.target.value) || 0 }
})} })}
placeholder={formData.proxy.type === 'http' ? '8080' : '1080'} 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>
</div> </div>
@@ -416,7 +416,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
{/* Proxy Auth */} {/* Proxy Auth */}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<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 <input
type="text" type="text"
value={formData.proxy.username} value={formData.proxy.username}
@@ -425,11 +425,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
proxy: { ...formData.proxy, username: e.target.value } proxy: { ...formData.proxy, username: e.target.value }
})} })}
placeholder="username" 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>
<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 <input
type="password" type="password"
value={formData.proxy.password} value={formData.proxy.password}
@@ -438,7 +438,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
proxy: { ...formData.proxy, password: e.target.value } proxy: { ...formData.proxy, password: e.target.value }
})} })}
placeholder="password" 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>
</div> </div>
@@ -451,7 +451,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
{/* Group and tags */} {/* Group and tags */}
<div className="border-t border-dark-700 pt-4 grid grid-cols-2 gap-3"> <div className="border-t border-dark-700 pt-4 grid grid-cols-2 gap-3">
<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">
<Folder className="inline w-3 h-3 mr-1" /> <Folder className="inline w-3 h-3 mr-1" />
分组 分组
</label> </label>
@@ -460,11 +460,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
value={formData.group} value={formData.group}
onChange={(e) => setFormData({ ...formData, group: e.target.value })} onChange={(e) => setFormData({ ...formData, group: e.target.value })}
placeholder="default" 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>
<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" /> <Tag className="inline w-3 h-3 mr-1" />
标签 标签
</label> </label>
@@ -473,7 +473,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
value={formData.tags} value={formData.tags}
onChange={(e) => setFormData({ ...formData, tags: e.target.value })} onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
placeholder="生产, 阿里云" 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>
</div> </div>
@@ -483,14 +483,14 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
<button <button
type="button" type="button"
onClick={onClose} 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>
<button <button
type="submit" type="submit"
disabled={isSubmitting} 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 ? '保存' : '创建会话')} {isSubmitting ? (isEditing ? '保存中...' : '创建中...') : (isEditing ? '保存' : '创建会话')}
</button> </button>
+60 -60
View File
@@ -141,7 +141,7 @@ export default function SettingsView() {
if (!isLoaded) { if (!isLoaded) {
return ( return (
<div className="h-full flex items-center justify-center bg-dark-900"> <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> </div>
) )
} }
@@ -197,8 +197,8 @@ export default function SettingsView() {
onClick={() => setSetting('theme', theme.value as 'dark' | 'light' | 'system')} onClick={() => setSetting('theme', theme.value as 'dark' | 'light' | 'system')}
className={`p-3 rounded-lg border text-sm transition-colors ${ className={`p-3 rounded-lg border text-sm transition-colors ${
settings.theme === theme.value settings.theme === theme.value
? 'border-accent-blue bg-accent-blue/20 text-accent-blue' ? 'border-accent-blue bg-surface-baseccent-blue/20 text-accent-blue'
: 'border-dark-700/30 hover:border-dark-600 text-gray-300' : 'border-dark-700/30 hover:border-dark-600 text-content-secondary'
}`} }`}
> >
{theme.label} {theme.label}
@@ -212,7 +212,7 @@ export default function SettingsView() {
<select <select
value={settings.language} value={settings.language}
onChange={(e) => setSetting('language', e.target.value as 'zh-CN' | 'en-US')} 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="zh-CN">简体中文</option>
<option value="en-US">English</option> <option value="en-US">English</option>
@@ -229,11 +229,11 @@ export default function SettingsView() {
<SettingSectionTitle>字体</SettingSectionTitle> <SettingSectionTitle>字体</SettingSectionTitle>
<div className="grid grid-cols-2 gap-4 mt-3"> <div className="grid grid-cols-2 gap-4 mt-3">
<div> <div>
<label className="text-sm text-gray-500">字体族</label> <label className="text-sm text-txt-muted">字体族</label>
<select <select
value={settings.fontFamily} value={settings.fontFamily}
onChange={(e) => setSetting('fontFamily', e.target.value)} 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>JetBrains Mono</option>
<option>Fira Code</option> <option>Fira Code</option>
@@ -242,11 +242,11 @@ export default function SettingsView() {
</select> </select>
</div> </div>
<div> <div>
<label className="text-sm text-gray-500">字体大小</label> <label className="text-sm text-txt-muted">字体大小</label>
<select <select
value={settings.fontSize} value={settings.fontSize}
onChange={(e) => setSetting('fontSize', parseInt(e.target.value))} 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) => ( {[12, 13, 14, 15, 16, 18, 20].map((size) => (
<option key={size} value={size}>{size}px</option> <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')} onClick={() => setSetting('terminalTheme', key as 'dracula' | 'monokai' | 'solarized' | 'one-dark')}
className={`p-3 rounded-lg border text-sm transition-colors ${ className={`p-3 rounded-lg border text-sm transition-colors ${
settings.terminalTheme === key settings.terminalTheme === key
? 'border-accent-blue bg-accent-blue/20 text-accent-blue' ? 'border-accent-blue bg-surface-baseccent-blue/20 text-accent-blue'
: 'border-dark-700/30 hover:border-dark-600 text-gray-300' : 'border-dark-700/30 hover:border-dark-600 text-content-secondary'
}`} }`}
> >
<div <div
@@ -299,7 +299,7 @@ export default function SettingsView() {
<div className="flex justify-end"> <div className="flex justify-end">
<button <button
onClick={() => keybindingState.resetAll()} 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" /> <RotateCcw className="w-3.5 h-3.5" />
重置全部为默认 重置全部为默认
@@ -311,7 +311,7 @@ export default function SettingsView() {
const defs = getKeybindingsByCategory(category) const defs = getKeybindingsByCategory(category)
return ( return (
<SettingCard key={category}> <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"> <div className="space-y-1">
{defs.map((def) => { {defs.map((def) => {
const currentKeys = allKeys[def.id] const currentKeys = allKeys[def.id]
@@ -323,14 +323,14 @@ export default function SettingsView() {
key={def.id} key={def.id}
className={`flex items-center justify-between px-3 py-2 rounded-lg transition-colors ${ className={`flex items-center justify-between px-3 py-2 rounded-lg transition-colors ${
isRecordingThis 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' : 'hover:bg-dark-800/60'
}`} }`}
> >
<div className="flex-1 min-w-0"> <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 && ( {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> </span>
)} )}
@@ -339,12 +339,12 @@ export default function SettingsView() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isRecordingThis ? ( {isRecordingThis ? (
<div className="flex items-center gap-2"> <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 || '按下快捷键...'} {recordedKeys || '按下快捷键...'}
</kbd> </kbd>
<button <button
onClick={() => stopRecording()} onClick={() => stopRecording()}
className="text-xs text-gray-500 hover:text-gray-300" className="text-xs text-txt-muted hover:text-content-secondary"
> >
取消 取消
</button> </button>
@@ -368,8 +368,8 @@ export default function SettingsView() {
<kbd <kbd
className={`inline-flex items-center px-2.5 py-1 rounded text-xs font-mono transition-colors ${ className={`inline-flex items-center px-2.5 py-1 rounded text-xs font-mono transition-colors ${
isCustom isCustom
? 'bg-accent-blue/10 border border-accent-blue/30 text-accent-blue' ? 'bg-surface-baseccent-blue/10 border border-accent-blue/30 text-accent-blue'
: 'bg-dark-700 border border-dark-600 text-gray-300' : 'bg-dark-700 border border-dark-600 text-content-secondary'
} group-hover:border-accent-blue/50`} } group-hover:border-accent-blue/50`}
> >
{currentKeys} {currentKeys}
@@ -378,7 +378,7 @@ export default function SettingsView() {
{isCustom && ( {isCustom && (
<button <button
onClick={() => keybindingState.resetKeybinding(def.id)} 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="重置为默认" title="重置为默认"
> >
<RotateCcw className="w-3 h-3" /> <RotateCcw className="w-3 h-3" />
@@ -398,7 +398,7 @@ export default function SettingsView() {
{/* 录入确认弹层 */} {/* 录入确认弹层 */}
{recordingId && recordedKeys && ( {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"> <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> 设置为 <kbd className="px-1.5 py-0.5 rounded bg-dark-700 text-accent-blue font-mono text-xs">{recordedKeys}</kbd>
</span> </span>
<button <button
@@ -406,20 +406,20 @@ export default function SettingsView() {
await keybindingState.setCustomKeybinding(recordingId, recordedKeys) await keybindingState.setCustomKeybinding(recordingId, recordedKeys)
stopRecording() 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>
<button <button
onClick={stopRecording} 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> </button>
</div> </div>
)} )}
<p className="text-xs text-gray-600"> <p className="text-xs text-txt-faint">
点击快捷键可重新绑定 · 按 Esc 取消录入 点击快捷键可重新绑定 · 按 Esc 取消录入
</p> </p>
</div> </div>
@@ -445,7 +445,7 @@ export default function SettingsView() {
type="number" type="number"
value={settings.defaultPort} value={settings.defaultPort}
onChange={(e) => setSetting('defaultPort', parseInt(e.target.value) || 22)} 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> </SettingCard>
@@ -455,7 +455,7 @@ export default function SettingsView() {
type="number" type="number"
value={settings.timeout} value={settings.timeout}
onChange={(e) => setSetting('timeout', parseInt(e.target.value) || 30)} 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> </SettingCard>
@@ -465,60 +465,60 @@ export default function SettingsView() {
<span className="text-accent-blue text-xs font-semibold uppercase tracking-wider">Pro</span> <span className="text-accent-blue text-xs font-semibold uppercase tracking-wider">Pro</span>
<SettingSectionTitle className="mb-0">连接管理</SettingSectionTitle> <SettingSectionTitle className="mb-0">连接管理</SettingSectionTitle>
</div> </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="space-y-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="text-sm text-gray-500">最大连接数</label> <label className="text-sm text-txt-muted">最大连接数</label>
<input <input
type="number" type="number"
min={1} min={1}
max={50} max={50}
value={settings.maxConnections || 10} value={settings.maxConnections || 10}
onChange={(e) => setSetting('maxConnections', parseInt(e.target.value) || 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>
<div> <div>
<label className="text-sm text-gray-500">最大后台连接</label> <label className="text-sm text-txt-muted">最大后台连接</label>
<input <input
type="number" type="number"
min={1} min={1}
max={20} max={20}
value={settings.maxBackgroundConnections || 5} value={settings.maxBackgroundConnections || 5}
onChange={(e) => setSetting('maxBackgroundConnections', parseInt(e.target.value) || 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> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="text-sm text-gray-500">空闲超时 (分钟)</label> <label className="text-sm text-txt-muted">空闲超时 (分钟)</label>
<input <input
type="number" type="number"
min={1} min={1}
max={120} max={120}
value={Math.floor((settings.idleTimeoutMs || 5 * 60 * 1000) / 60000)} value={Math.floor((settings.idleTimeoutMs || 5 * 60 * 1000) / 60000)}
onChange={(e) => setSetting('idleTimeoutMs', (parseInt(e.target.value) || 5) * 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>
<div> <div>
<label className="text-sm text-gray-500">挂起超时 (分钟)</label> <label className="text-sm text-txt-muted">挂起超时 (分钟)</label>
<input <input
type="number" type="number"
min={1} min={1}
max={240} max={240}
value={Math.floor((settings.suspendTimeoutMs || 30 * 60 * 1000) / 60000)} value={Math.floor((settings.suspendTimeoutMs || 30 * 60 * 1000) / 60000)}
onChange={(e) => setSetting('suspendTimeoutMs', (parseInt(e.target.value) || 30) * 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>
</div> </div>
@@ -571,11 +571,11 @@ export default function SettingsView() {
<SettingCard> <SettingCard>
<div className="flex flex-col items-center py-6"> <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"> <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> </div>
<h3 className="text-xl font-semibold text-gray-200 mb-1">OpsXShell</h3> <h3 className="text-xl font-semibold text-content-primary mb-1">OpsXShell</h3>
<p className="text-sm text-gray-500 mb-4">版本 {appVersion}</p> <p className="text-sm text-txt-muted mb-4">版本 {appVersion}</p>
<p className="text-sm text-gray-400 max-w-md text-center"> <p className="text-sm text-txt-muted max-w-md text-center">
跨平台 SSH/SFTP 终端工具 · 简洁高效 · 安全可靠 · 操作便捷人性化 跨平台 SSH/SFTP 终端工具 · 简洁高效 · 安全可靠 · 操作便捷人性化
</p> </p>
</div> </div>
@@ -595,8 +595,8 @@ export default function SettingsView() {
['Lucide React', '0.294+'], ['Lucide React', '0.294+'],
].map(([name, ver]) => ( ].map(([name, ver]) => (
<div key={name} className="flex justify-between py-1 border-b border-dark-700/20"> <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-txt-muted">{name}</span>
<span className="text-gray-500 font-mono">{ver}</span> <span className="text-txt-muted font-mono">{ver}</span>
</div> </div>
))} ))}
</div> </div>
@@ -604,10 +604,10 @@ export default function SettingsView() {
<SettingCard> <SettingCard>
<SettingSectionTitle>开源许可</SettingSectionTitle> <SettingSectionTitle>开源许可</SettingSectionTitle>
<p className="text-sm text-gray-400 mt-2"> <p className="text-sm text-txt-muted mt-2">
OpsXShell 基于 MIT 许可证开源。本项目使用了以下开源组件: OpsXShell 基于 MIT 许可证开源。本项目使用了以下开源组件:
</p> </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>ssh2 — MIT License</li>
<li>xterm.js — MIT License</li> <li>xterm.js — MIT License</li>
<li>zmodem-ts — 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="p-3 border-b border-dark-700/20">
<div className="relative"> <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 <input
type="text" type="text"
placeholder="搜索设置..." placeholder="搜索设置..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} 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 && ( {searchQuery && (
<button <button
onClick={() => setSearchQuery('')} 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" /> <X className="w-3.5 h-3.5" />
</button> </button>
@@ -661,14 +661,14 @@ export default function SettingsView() {
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-all ${ className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-all ${
active active
? 'bg-accent-blue text-white shadow-sm' ? 'bg-surface-baseccent-blue text-content-primary shadow-sm'
: 'text-gray-400 hover:bg-dark-700/50 hover:text-gray-200' : '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="min-w-0">
<div className="text-sm font-medium truncate">{tab.label}</div> <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} {tab.description}
</div> </div>
</div> </div>
@@ -682,8 +682,8 @@ export default function SettingsView() {
<div className="flex-1 flex flex-col min-w-0"> <div className="flex-1 flex flex-col min-w-0">
{/* Header bar */} {/* Header bar */}
<div className="flex-shrink-0 px-8 pt-6 pb-4 border-b border-dark-700/20"> <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> <h1 className="text-xl font-semibold text-content-primary">{currentMeta.label}</h1>
<p className="text-sm text-gray-500 mt-0.5">{currentMeta.description}</p> <p className="text-sm text-txt-muted mt-0.5">{currentMeta.description}</p>
</div> </div>
{/* 内容 */} {/* 内容 */}
@@ -727,7 +727,7 @@ function SettingSectionTitle({
className?: string className?: string
}) { }) {
return ( 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} {children}
</div> </div>
) )
@@ -748,13 +748,13 @@ function SettingToggle({
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="mr-4"> <div className="mr-4">
<div className="font-medium text-gray-200 text-sm">{label}</div> <div className="font-medium text-content-primary text-sm">{label}</div>
<div className="text-xs text-gray-500 mt-0.5">{description}</div> <div className="text-xs text-txt-muted mt-0.5">{description}</div>
</div> </div>
<button <button
onClick={() => onChange(!checked)} onClick={() => onChange(!checked)}
className={`relative flex-shrink-0 w-11 h-6 rounded-full transition-colors ${ 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 <span
+7 -7
View File
@@ -185,8 +185,8 @@ export default function Sidebar({ onNewSession, onQuickConnect, onEditSession }:
{/* Header */} {/* Header */}
<div className="p-4 border-b border-dark-700/20"> <div className="p-4 border-b border-dark-700/20">
<div className="flex items-center gap-2 mb-4"> <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"> <div className="w-8 h-8 bg-surface-baseccent-blue rounded-lg flex items-center justify-center">
<Server className="w-5 h-5 text-white" /> <Server className="w-5 h-5 text-content-primary" />
</div> </div>
<span className="font-semibold text-lg">OpsXShell</span> <span className="font-semibold text-lg">OpsXShell</span>
</div> </div>
@@ -195,7 +195,7 @@ export default function Sidebar({ onNewSession, onQuickConnect, onEditSession }:
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={onQuickConnect} 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" /> <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"> <div className="p-3 border-t border-dark-700/20">
<button <button
onClick={() => addTab({ title: '设置', type: 'settings' })} 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" /> <Settings className="w-4 h-4" />
<span>设置</span> <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 w-full px-4 py-2 text-left text-sm transition-colors
${item.danger ${item.danger
? 'text-red-400 hover:bg-red-500/20' ? '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} onMouseDown={handleResizeStart}
className={` className={`
absolute top-0 right-0 bottom-0 w-1 cursor-col-resize 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)] hover:w-1.5 hover:shadow-[0_0_6px_rgba(59,130,246,0.4)]
transition-all duration-150 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> </div>
+9 -9
View File
@@ -54,7 +54,7 @@ const STATUS_CONFIG: Record<string, StatusDisplay> = {
disconnected: { disconnected: {
icon: <WifiOff className="w-3 h-3" />, icon: <WifiOff className="w-3 h-3" />,
label: '未连接', label: '未连接',
colorClass: 'text-gray-400', colorClass: 'text-txt-muted',
animateClass: '', animateClass: '',
}, },
} }
@@ -163,7 +163,7 @@ export default function StatusBar() {
: null : null
return ( 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 */} {/* Left: Connection status area — clickable to toggle ConnectionPanel */}
<button <button
type="button" type="button"
@@ -179,21 +179,21 @@ export default function StatusBar() {
<span className="transition-colors duration-300">{statusCfg.label}</span> <span className="transition-colors duration-300">{statusCfg.label}</span>
{hostLabel && ( {hostLabel && (
<> <>
<span className="text-gray-500 mx-0.5">·</span> <span className="text-txt-muted mx-0.5">·</span>
<span className="text-gray-300">{hostLabel}</span> <span className="text-content-secondary">{hostLabel}</span>
</> </>
)} )}
{totalConnections > 0 && ( {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} {totalConnections}
</span> </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> </button>
{/* Center: Transfer stats */} {/* Center: Transfer stats */}
{transferLabel && ( {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" /> <HardDrive className="w-3 h-3" />
<span>{transferLabel}</span> <span>{transferLabel}</span>
</div> </div>
@@ -204,11 +204,11 @@ export default function StatusBar() {
{/* Right: Clock + version */} {/* Right: Clock + version */}
<div className="flex items-center gap-4"> <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" /> <Clock className="w-3 h-3" />
<span>{formatTime(currentTime)}</span> <span>{formatTime(currentTime)}</span>
</div> </div>
<span className="text-gray-500">OpsXShell v{version}</span> <span className="text-txt-muted">OpsXShell v{version}</span>
</div> </div>
</div> </div>
) )
+1 -1
View File
@@ -137,7 +137,7 @@ function TabBar() {
{/* Add Tab Button */} {/* Add Tab Button */}
<button <button
onClick={() => addTab({ title: '新标签', type: 'welcome' })} 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" /> <Plus className="w-5 h-5" />
</button> </button>
+18 -18
View File
@@ -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="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="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"> <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>
</div> </div>
@@ -36,12 +36,12 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
onClick={onQuickConnect} 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" 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" /> <Zap className="w-6 h-6 text-accent-blue" />
</div> </div>
<div className="text-left"> <div className="text-left">
<div className="font-semibold text-gray-200">快速连接</div> <div className="font-semibold text-content-primary">快速连接</div>
<div className="text-sm text-gray-500">输入地址快速建立连接</div> <div className="text-sm text-txt-muted">输入地址快速建立连接</div>
</div> </div>
</button> </button>
@@ -49,12 +49,12 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
onClick={onNewSession} 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" 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"> <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-white" /> <Plus className="w-6 h-6 text-content-primary" />
</div> </div>
<div className="text-left"> <div className="text-left">
<div className="font-semibold text-gray-200">新建会话</div> <div className="font-semibold text-content-primary">新建会话</div>
<div className="text-sm text-gray-500">创建并保存新的会话</div> <div className="text-sm text-txt-muted">创建并保存新的会话</div>
</div> </div>
</button> </button>
@@ -62,12 +62,12 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
onClick={() => addTab({ title: 'SFTP', type: 'sftp' })} 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" 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" /> <FolderSync className="w-6 h-6 text-accent-purple" />
</div> </div>
<div className="text-left"> <div className="text-left">
<div className="font-semibold text-gray-200">SFTP 传输</div> <div className="font-semibold text-content-primary">SFTP 传输</div>
<div className="text-sm text-gray-500">可视化文件管理</div> <div className="text-sm text-txt-muted">可视化文件管理</div>
</div> </div>
</button> </button>
@@ -79,15 +79,15 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
<ArrowRightLeft className="w-6 h-6 text-orange-400" /> <ArrowRightLeft className="w-6 h-6 text-orange-400" />
</div> </div>
<div className="text-left"> <div className="text-left">
<div className="font-semibold text-gray-200">端口转发</div> <div className="font-semibold text-content-primary">端口转发</div>
<div className="text-sm text-gray-500">本地/远程/SOCKS5 代理</div> <div className="text-sm text-txt-muted">本地/远程/SOCKS5 代理</div>
</div> </div>
</button> </button>
</div> </div>
{/* Recent connections */} {/* Recent connections */}
<div className="w-full max-w-2xl"> <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 ? ( {recentSessions.length === 0 ? (
<EmptyState <EmptyState
icon={Clock} icon={Clock}
@@ -105,13 +105,13 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Globe className="w-5 h-5 text-accent-blue" /> <Globe className="w-5 h-5 text-accent-blue" />
<div className="text-left"> <div className="text-left">
<div className="font-medium text-gray-200">{session.name}</div> <div className="font-medium text-content-primary">{session.name}</div>
<div className="text-sm text-gray-500"> <div className="text-sm text-txt-muted">
{session.username}@{session.host}{session.port !== 22 ? `:${session.port}` : ''} {session.username}@{session.host}{session.port !== 22 ? `:${session.port}` : ''}
</div> </div>
</div> </div>
</div> </div>
<span className="text-xs text-gray-500">--</span> <span className="text-xs text-txt-muted">--</span>
</button> </button>
))} ))}
</div> </div>
@@ -119,7 +119,7 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
</div> </div>
{/* Shortcuts hint */} {/* 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>快捷键:</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+N</kbd> 新建会话</span>
<span><kbd className="px-2 py-1 bg-dark-800 rounded">Ctrl+K</kbd> 快速连接</span> <span><kbd className="px-2 py-1 bg-dark-800 rounded">Ctrl+K</kbd> 快速连接</span>
+4 -4
View File
@@ -20,10 +20,10 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
} }
const variantStyles = { const variantStyles = {
primary: 'bg-accent-blue hover:bg-blue-600 text-white', primary: 'bg-surface-baseccent-blue hover:bg-surface-baselue-600 text-content-primary',
secondary: 'bg-dark-700 hover:bg-dark-600 text-gray-200', secondary: 'bg-dark-700 hover:bg-dark-600 text-content-primary',
danger: 'bg-red-600 hover:bg-red-500 text-white', danger: 'bg-red-600 hover:bg-red-500 text-content-primary',
ghost: 'hover:bg-dark-700 text-gray-400 hover:text-gray-200', ghost: 'hover:bg-dark-700 text-txt-muted hover:text-content-primary',
} }
const sizeStyles = { const sizeStyles = {
+17 -17
View File
@@ -45,9 +45,9 @@ interface CommandPaletteProps {
// ─── 类型 badge 颜色映射 ────────────────────────────────────────────── // ─── 类型 badge 颜色映射 ──────────────────────────────────────────────
const TYPE_BADGE: Record<CommandPaletteItemType, { label: string; className: string }> = { const TYPE_BADGE: Record<CommandPaletteItemType, { label: string; className: string }> = {
command: { label: '命令', className: 'bg-accent-blue/20 text-accent-blue' }, command: { label: '命令', className: 'bg-surface-baseccent-blue/20 text-accent-blue' },
session: { label: '会话', className: 'bg-accent-green/20 text-accent-green' }, session: { label: '会话', className: 'bg-surface-baseccent-green/20 text-accent-green' },
snippet: { label: '片段', className: 'bg-accent-purple/20 text-accent-purple' }, snippet: { label: '片段', className: 'bg-surface-baseccent-purple/20 text-accent-purple' },
} }
// ─── 组件 ────────────────────────────────────────────────────────────── // ─── 组件 ──────────────────────────────────────────────────────────────
@@ -305,7 +305,7 @@ export function CommandPalette({
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
{/* Backdrop */} {/* 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 */} {/* Palette container */}
<div <div
@@ -316,18 +316,18 @@ export function CommandPalette({
> >
{/* Search input */} {/* Search input */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-dark-700"> <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 <input
ref={inputRef} ref={inputRef}
type="text" type="text"
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="搜索命令、会话或片段..." 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" autoComplete="off"
spellCheck={false} 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 ESC
</kbd> </kbd>
</div> </div>
@@ -335,10 +335,10 @@ export function CommandPalette({
{/* Results list */} {/* Results list */}
<div ref={listRef} className="flex-1 overflow-y-auto py-1"> <div ref={listRef} className="flex-1 overflow-y-auto py-1">
{filteredItems.length === 0 ? ( {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" /> <Search size={32} className="mb-2 opacity-30" />
<p className="text-sm">没有找到匹配的结果</p> <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> </div>
) : ( ) : (
groupedItems.map((group) => { groupedItems.map((group) => {
@@ -348,14 +348,14 @@ export function CommandPalette({
return ( return (
<div key={group.type}> <div key={group.type}>
{/* Group header */} {/* 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 <span
className={`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold ${TYPE_BADGE[group.type].className}`} 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} {TYPE_BADGE[group.type].label}
</span> </span>
<span className="flex-1 h-px bg-dark-700" /> <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> </div>
{/* Group items */} {/* Group items */}
@@ -369,8 +369,8 @@ export function CommandPalette({
className={` className={`
flex items-center gap-3 px-4 py-2 cursor-pointer transition-colors duration-75 flex items-center gap-3 px-4 py-2 cursor-pointer transition-colors duration-75
${isSelected ${isSelected
? 'bg-accent-blue/15 text-gray-100' ? 'bg-surface-baseccent-blue/15 text-content-primary'
: 'text-gray-300 hover:bg-dark-700' : 'text-content-secondary hover:bg-dark-700'
} }
`} `}
onClick={() => { onClick={() => {
@@ -382,7 +382,7 @@ export function CommandPalette({
{/* Icon */} {/* Icon */}
<span <span
className={`flex-shrink-0 w-5 h-5 flex items-center justify-center ${ 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} {item.icon}
@@ -392,7 +392,7 @@ export function CommandPalette({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-sm truncate">{item.label}</div> <div className="text-sm truncate">{item.label}</div>
{item.description && ( {item.description && (
<div className="text-[11px] text-gray-500 truncate"> <div className="text-[11px] text-txt-muted truncate">
{item.description} {item.description}
</div> </div>
)} )}
@@ -400,7 +400,7 @@ export function CommandPalette({
{/* Shortcut */} {/* Shortcut */}
{item.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} {item.shortcut}
</kbd> </kbd>
)} )}
@@ -421,7 +421,7 @@ export function CommandPalette({
</div> </div>
{/* Footer hint */} {/* 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"> <div className="flex items-center gap-3">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<kbd className="px-1 py-0.5 bg-dark-700 rounded border border-dark-600">↑↓</kbd> <kbd className="px-1 py-0.5 bg-dark-700 rounded border border-dark-600">↑↓</kbd>
+1 -1
View File
@@ -94,7 +94,7 @@ export function useConfirmDialog() {
{state.danger && ( {state.danger && (
<AlertTriangle className="w-5 h-5 text-red-400 mt-0.5 flex-shrink-0" /> <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> </div>
</Modal> </Modal>
) )
+6 -6
View File
@@ -18,13 +18,13 @@ export default function DefaultFallback({ error, onReset }: DefaultFallbackProps
</svg> </svg>
</div> </div>
<div> <div>
<h2 className="text-lg font-semibold text-gray-200">出错了</h2> <h2 className="text-lg font-semibold text-content-primary">出错了</h2>
<p className="text-sm text-gray-500">应用遇到意外问题</p> <p className="text-sm text-txt-muted">应用遇到意外问题</p>
</div> </div>
</div> </div>
<div className="bg-dark-900 rounded-lg p-4 mb-4"> <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 && ( {appError.recoverable && (
<p className="text-xs text-green-400">此错误可以恢复,请尝试刷新页面</p> <p className="text-xs text-green-400">此错误可以恢复,请尝试刷新页面</p>
)} )}
@@ -33,19 +33,19 @@ export default function DefaultFallback({ error, onReset }: DefaultFallbackProps
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
onClick={onReset} 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>
<button <button
onClick={() => window.location.reload()} 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> </button>
</div> </div>
<p className="mt-4 text-xs text-gray-600 text-center"> <p className="mt-4 text-xs text-txt-faint text-center">
如果问题持续存在,请检查日志或联系支持 如果问题持续存在,请检查日志或联系支持
</p> </p>
</div> </div>
+2 -2
View File
@@ -38,10 +38,10 @@ export default function EmptyState({
</div> </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 && ( {action && (
+1 -1
View File
@@ -50,7 +50,7 @@ export default function Loading({
<div className={`flex flex-col items-center gap-3 ${className}`}> <div className={`flex flex-col items-center gap-3 ${className}`}>
<Loader2 className={`${sizeMap[size].icon} text-accent-blue animate-spin`} /> <Loader2 className={`${sizeMap[size].icon} text-accent-blue animate-spin`} />
{text && ( {text && (
<span className={`${sizeMap[size].text} text-gray-400`}>{text}</span> <span className={`${sizeMap[size].text} text-txt-muted`}>{text}</span>
)} )}
</div> </div>
) )
+3 -3
View File
@@ -74,7 +74,7 @@ export default function Modal({
<div className="fixed inset-0 z-50 flex items-center justify-center p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* 遮罩 */} {/* 遮罩 */}
<div <div
className="absolute inset-0 bg-black/60 backdrop-blur-sm" className="absolute inset-0 bg-surface-baselack/60 backdrop-blur-sm"
onClick={onClose} 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="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"> <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 <button
onClick={onClose} onClick={onClose}
className="p-1 hover:bg-dark-700 rounded-lg transition-colors" 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> </button>
</div> </div>
+2 -2
View File
@@ -104,7 +104,7 @@ export function usePromptDialog() {
cancelText={state.cancelText} cancelText={state.cancelText}
> >
<div> <div>
<p className="text-gray-300 mb-3">{state.message}</p> <p className="text-content-secondary mb-3">{state.message}</p>
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"
@@ -112,7 +112,7 @@ export function usePromptDialog() {
onChange={(e) => setState((prev) => ({ ...prev, value: e.target.value }))} onChange={(e) => setState((prev) => ({ ...prev, value: e.target.value }))}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={state.placeholder} 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> </div>
</Modal> </Modal>
+2 -2
View File
@@ -257,7 +257,7 @@ export function TabContentSkeleton({ className }: TabContentSkeletonProps) {
return ( return (
<div <div
className={mergeClass( 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, className,
)} )}
> >
@@ -287,7 +287,7 @@ export function TabContentSkeleton({ className }: TabContentSkeletonProps) {
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1">
<span className="text-accent-green/40 select-none">$</span> <span className="text-accent-green/40 select-none">$</span>
<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>
</div> </div>
+3 -3
View File
@@ -32,13 +32,13 @@ const colorMap: Record<ToastType, { bg: string; border: string; icon: string; te
text: 'text-red-200', text: 'text-red-200',
}, },
warning: { warning: {
bg: 'bg-amber-900/40', bg: 'bg-surface-basember-900/40',
border: 'border-amber-700/60', border: 'border-amber-700/60',
icon: 'text-amber-400', icon: 'text-amber-400',
text: 'text-amber-200', text: 'text-amber-200',
}, },
info: { info: {
bg: 'bg-blue-900/40', bg: 'bg-surface-baselue-900/40',
border: 'border-blue-700/60', border: 'border-blue-700/60',
icon: 'text-blue-400', icon: 'text-blue-400',
text: 'text-blue-200', 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" className="p-0.5 rounded hover:bg-white/10 transition-colors shrink-0"
aria-label="关闭通知" 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> </button>
</div> </div>
) )
+3 -3
View File
@@ -18,7 +18,7 @@ const iconMap: Record<ToastType, React.ReactNode> = {
} }
const bgMap: Record<ToastType, string> = { 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', success: 'bg-green-900/80 border-green-700',
error: 'bg-red-900/80 border-red-700', error: 'bg-red-900/80 border-red-700',
warning: 'bg-yellow-900/80 border-yellow-700', warning: 'bg-yellow-900/80 border-yellow-700',
@@ -32,7 +32,7 @@ export function ToastContainer({ toasts, onRemove }: ToastContainerProps) {
{toasts.map((toast) => ( {toasts.map((toast) => (
<div <div
key={toast.id} 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]} {iconMap[toast.type]}
<span className="flex-1">{toast.message}</span> <span className="flex-1">{toast.message}</span>
@@ -40,7 +40,7 @@ export function ToastContainer({ toasts, onRemove }: ToastContainerProps) {
onClick={() => onRemove(toast.id)} onClick={() => onRemove(toast.id)}
className="p-0.5 hover:bg-white/10 rounded" 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> </button>
</div> </div>
))} ))}
+3 -3
View File
@@ -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" className="p-1.5 hover:bg-dark-700 rounded transition-colors flex-shrink-0"
title="主目录" title="主目录"
> >
<Home className="w-4 h-4 text-gray-400" /> <Home className="w-4 h-4 text-txt-muted" />
</button> </button>
<button <button
onClick={() => onNavigate('/')} 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> </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" /> <ChevronRight className="w-3 h-3 text-dark-600 mx-0.5 flex-shrink-0" />
<button <button
onClick={() => onNavigate('/' + parts.slice(0, index + 1).join('/'))} 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} {part}
</button> </button>
+8 -8
View File
@@ -108,7 +108,7 @@ export default function ChmodDialog({
onChange={() => togglePerm(scope, perm)} 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" 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> </label>
) )
@@ -120,7 +120,7 @@ export default function ChmodDialog({
label: string label: string
}) => ( }) => (
<div className="flex items-center gap-4"> <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="read" label="读取" />
<PermCheckbox scope={scope} perm="write" label="写入" /> <PermCheckbox scope={scope} perm="write" label="写入" />
<PermCheckbox scope={scope} perm="execute" 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"> <div className="flex items-center gap-3 pb-3 border-b border-dark-600/30">
<Shield className="w-5 h-5 text-accent-blue" /> <Shield className="w-5 h-5 text-accent-blue" />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-white font-medium truncate">{fileName}</p> <p className="text-content-primary font-medium truncate">{fileName}</p>
<p className="text-gray-500 text-xs font-mono truncate">{filePath}</p> <p className="text-txt-muted text-xs font-mono truncate">{filePath}</p>
</div> </div>
</div> </div>
{/* Current octal mode display */} {/* Current octal mode display */}
<div className="flex items-center justify-center gap-2 py-2"> <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> <span className="text-accent-blue font-mono font-bold text-lg">{octalString}</span>
</div> </div>
@@ -156,9 +156,9 @@ export default function ChmodDialog({
<div className="space-y-3 bg-dark-700 rounded-lg p-4"> <div className="space-y-3 bg-dark-700 rounded-lg p-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="w-16" /> <span className="w-16" />
<span className="text-gray-500 text-xs w-16">读取 (r)</span> <span className="text-txt-muted text-xs w-16">读取 (r)</span>
<span className="text-gray-500 text-xs w-16">写入 (w)</span> <span className="text-txt-muted 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">执行 (x)</span>
</div> </div>
<PermRow scope="owner" label="所有者" /> <PermRow scope="owner" label="所有者" />
<PermRow scope="group" label="所属组" /> <PermRow scope="group" label="所属组" />
+3 -3
View File
@@ -42,7 +42,7 @@ export function ConnectionPanel({
// Connecting State // Connecting State
if (isConnecting) { if (isConnecting) {
return ( 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" /> <Loader2 className="w-4 h-4 text-accent-blue animate-spin" />
<span className="text-sm text-accent-blue">连接中...</span> <span className="text-sm text-accent-blue">连接中...</span>
</div> </div>
@@ -71,10 +71,10 @@ export function ConnectionPanel({
return ( return (
<> <>
<div className="flex items-center justify-between px-4 py-2 bg-dark-800 border-b border-dark-700/30"> <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 <button
onClick={onShowSelector} 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> </button>
+6 -6
View File
@@ -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="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="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"> <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"> <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> </button>
</div> </div>
<div className="p-4 max-h-80 overflow-y-auto"> <div className="p-4 max-h-80 overflow-y-auto">
{sessions.length === 0 ? ( {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" /> <Monitor className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p>暂无保存的会话</p> <p>暂无保存的会话</p>
<p className="text-sm mt-1">请先在侧边栏创建会话</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" /> <Monitor className="w-8 h-8 text-cyan-400 flex-shrink-0" />
<div className="flex-1 min-w-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-slate-200 font-medium truncate group-hover:text-content-primary">{session.name}</div>
<div className="text-sm text-gray-500 truncate"> <div className="text-sm text-txt-muted truncate">
{session.username}@{session.host}:{session.port} {session.username}@{session.host}:{session.port}
</div> </div>
</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> </button>
))} ))}
</div> </div>
+1 -1
View File
@@ -56,7 +56,7 @@ export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
onClose() onClose()
}} }}
className={`w-full flex items-center gap-3 px-4 py-2 text-sm transition-colors ${ 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} {item.icon}
+3 -3
View File
@@ -156,7 +156,7 @@ export function FileListPanel({
</div> </div>
{/* Column Headers */} {/* 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} /> <SortHeader sortField={sortField} sortOrder={sortOrder} onSort={onSort} />
</div> </div>
@@ -185,7 +185,7 @@ export function FileListPanel({
{/* Drag overlay - pointer-events-none so it doesn't interfere with drop events */} {/* Drag overlay - pointer-events-none so it doesn't interfere with drop events */}
{isDragOver && ( {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"> <div className="flex flex-col items-center gap-2 text-accent-blue">
<Upload className="w-10 h-10" /> <Upload className="w-10 h-10" />
<span className="text-sm font-medium">拖拽文件到此处上传</span> <span className="text-sm font-medium">拖拽文件到此处上传</span>
@@ -195,7 +195,7 @@ export function FileListPanel({
</div> </div>
{/* Status Bar */} {/* 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} 项`} {loading ? '加载中...' : selected.size > 0 ? `已选择 ${selected.size} 项` : `共 ${files.length} 项`}
</div> </div>
+6 -6
View File
@@ -128,13 +128,13 @@ export default function FilePropertiesDialog({
<TypeIcon className="w-8 h-8 text-accent-blue" /> <TypeIcon className="w-8 h-8 text-accent-blue" />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-white font-semibold truncate">{fileName}</p> <p className="text-content-primary font-semibold truncate">{fileName}</p>
<p className="text-gray-400 text-xs">{typeLabel}</p> <p className="text-txt-muted text-xs">{typeLabel}</p>
</div> </div>
</div> </div>
{loading && ( {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" /> <div className="animate-spin h-4 w-4 border-2 border-accent-blue border-t-transparent rounded-full" />
<span>加载详细信息...</span> <span>加载详细信息...</span>
</div> </div>
@@ -169,10 +169,10 @@ function PropertyRow({
}) { }) {
return ( return (
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span className="text-gray-500 mt-0.5 shrink-0">{icon}</span> <span className="text-txt-muted mt-0.5 shrink-0">{icon}</span>
<span className="text-gray-400 w-20 shrink-0">{label}</span> <span className="text-txt-muted w-20 shrink-0">{label}</span>
<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} {value}
</span> </span>
+6 -6
View File
@@ -24,7 +24,7 @@ export function FileRow({ file, isSelected, onSelect, onOpen, onContextMenu, onD
draggable={file.type === 'file'} draggable={file.type === 'file'}
onDragStart={onDragStart} onDragStart={onDragStart}
className={`h-10 flex items-center px-4 cursor-pointer border-b border-dark-700/20 transition-colors group ${ 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"> <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' ? ( ) : file.type === 'symlink' ? (
<Folder className="w-4 h-4 text-yellow-400 flex-shrink-0" /> <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>} {file.type === 'symlink' && <span className="text-xs text-yellow-500">→</span>}
</div> </div>
<div className="w-32 text-right text-xs text-gray-500">{formatDate(file.modified)}</div> <div className="w-32 text-right text-xs text-txt-muted">{formatDate(file.modified)}</div>
<div className="w-24 text-right text-xs text-gray-500"> <div className="w-24 text-right text-xs text-txt-muted">
{file.type === 'directory' ? '--' : formatSize(file.size)} {file.type === 'directory' ? '--' : formatSize(file.size)}
</div> </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 === 'directory'
? '文件夹' ? '文件夹'
: file.type === 'symlink' : file.type === 'symlink'
+2 -2
View File
@@ -20,12 +20,12 @@ export function SortHeader({ onSort, sortField, sortOrder }: SortHeaderProps) {
] ]
return ( 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 }) => ( {fields.map(({ key, label, width, align }) => (
<button <button
key={key} key={key}
onClick={() => onSort(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> <span>{label}</span>
{sortField === key && (sortOrder === 'asc' ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />)} {sortField === key && (sortOrder === 'asc' ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />)}
+4 -4
View File
@@ -21,7 +21,7 @@ export function Toolbar({ isConnected, onUpload, onDownload, onRefresh, onNewFol
<button <button
onClick={onUpload} onClick={onUpload}
disabled={!isConnected} 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" /> <Upload className="w-4 h-4" />
上传 上传
@@ -29,7 +29,7 @@ export function Toolbar({ isConnected, onUpload, onDownload, onRefresh, onNewFol
<button <button
onClick={onDownload} onClick={onDownload}
disabled={!isConnected} 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" /> <Download className="w-4 h-4" />
下载 下载
@@ -37,7 +37,7 @@ export function Toolbar({ isConnected, onUpload, onDownload, onRefresh, onNewFol
<button <button
onClick={onRefresh} onClick={onRefresh}
disabled={!isConnected} 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" /> <RefreshCw className="w-4 h-4" />
刷新 刷新
@@ -45,7 +45,7 @@ export function Toolbar({ isConnected, onUpload, onDownload, onRefresh, onNewFol
<button <button
onClick={onNewFolder} onClick={onNewFolder}
disabled={!isConnected} 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" /> <FolderPlus className="w-4 h-4" />
新建文件夹 新建文件夹
+8 -8
View File
@@ -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" />} {task.type === 'download' ? <Download className="w-4 h-4" /> : <Upload className="w-4 h-4" />}
</div> </div>
<div className="flex-1 min-w-0"> <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"> <div className="flex items-center gap-2 mt-0.5">
{isActive && ( {isActive && (
<> <>
<div className="flex-1 h-1.5 bg-dark-700 rounded-full overflow-hidden max-w-[120px]"> <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> </div>
<span className="text-xs text-gray-500">{task.progress}%</span> <span className="text-xs text-txt-muted">{task.progress}%</span>
{task.speed !== undefined && <span className="text-xs text-gray-500">{formatSpeed(task.speed)}</span>} {task.speed !== undefined && <span className="text-xs text-txt-muted">{formatSpeed(task.speed)}</span>}
</> </>
)} )}
{task.status === 'pending' && ( {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" /> <Clock className="w-3 h-3" />
等待中 等待中
</span> </span>
@@ -51,7 +51,7 @@ export function TransferItem({ task, onCancel, onRemove }: TransferItemProps) {
</span> </span>
)} )}
{task.status === 'canceled' && ( {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" /> <XCircle className="w-3 h-3" />
已取消 已取消
</span> </span>
@@ -62,7 +62,7 @@ export function TransferItem({ task, onCancel, onRemove }: TransferItemProps) {
{task.status === 'transferring' && ( {task.status === 'transferring' && (
<button <button
onClick={onCancel} 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="取消" title="取消"
> >
<X className="w-3.5 h-3.5" /> <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') && ( {(task.status === 'completed' || task.status === 'failed' || task.status === 'canceled') && (
<button <button
onClick={onRemove} 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="移除" title="移除"
> >
<X className="w-3.5 h-3.5" /> <X className="w-3.5 h-3.5" />
+3 -3
View File
@@ -30,7 +30,7 @@ export function TransferPanel({ tasks, isOpen, onToggle, onCancel, onRemove }: T
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-medium">传输队列</span> <span className="text-sm font-medium">传输队列</span>
{activeCount > 0 && ( {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} 进行中 {activeCount} 进行中
</span> </span>
)} )}
@@ -41,7 +41,7 @@ export function TransferPanel({ tasks, isOpen, onToggle, onCancel, onRemove }: T
)} )}
</div> </div>
<div className="flex items-center gap-2"> <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" />} {isOpen ? <ChevronDown className="w-4 h-4" /> : <ChevronUp className="w-4 h-4" />}
</div> </div>
</button> </button>
@@ -61,7 +61,7 @@ export function TransferPanel({ tasks, isOpen, onToggle, onCancel, onRemove }: T
)} )}
{isOpen && tasks.length === 0 && ( {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> </div>
)} )}
+1 -1
View File
@@ -13,7 +13,7 @@ interface SearchBoxProps {
export function SearchBox({ value, onChange, placeholder = '搜索...' }: SearchBoxProps) { export function SearchBox({ value, onChange, placeholder = '搜索...' }: SearchBoxProps) {
return ( return (
<div className="relative"> <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 <input
type="text" type="text"
placeholder={placeholder} placeholder={placeholder}
+4 -4
View File
@@ -66,7 +66,7 @@ export function SessionTree({
{/* Group Header */} {/* Group Header */}
<button <button
onClick={() => onToggleGroup(group)} 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 <ChevronRight
className="w-4 h-4 transition-transform duration-200 ease-in-out" 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" /> <Folder className="w-4 h-4" />
<span className="flex-1 text-left">{group === 'default' ? '默认分组' : group}</span> <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> </button>
{/* Sessions - CSS Grid collapse animation */} {/* Sessions - CSS Grid collapse animation */}
@@ -100,8 +100,8 @@ export function SessionTree({
> >
<Server className="w-4 h-4 text-accent-green flex-shrink-0" /> <Server className="w-4 h-4 text-accent-green flex-shrink-0" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-medium text-gray-200 truncate">{session.name}</div> <div className="font-medium text-content-primary truncate">{session.name}</div>
<div className="text-xs text-gray-500 truncate">{session.host}:{session.port}</div> <div className="text-xs text-txt-muted truncate">{session.host}:{session.port}</div>
</div> </div>
</div> </div>
))} ))}
+19 -19
View File
@@ -54,37 +54,37 @@ function SnippetFormModal({ isOpen, initial, title, confirmText, onConfirm, onCl
<div className="space-y-4"> <div className="space-y-4">
{/* 名称 */} {/* 名称 */}
<div> <div>
<label className="block text-sm text-gray-400 mb-1">名称</label> <label className="block text-sm text-txt-muted mb-1">名称</label>
<input <input
type="text" type="text"
value={form.name} value={form.name}
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
placeholder="例如:重启 Nginx" 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>
{/* 命令内容 */} {/* 命令内容 */}
<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 <textarea
value={form.content} value={form.content}
onChange={(e) => setForm((f) => ({ ...f, content: e.target.value }))} onChange={(e) => setForm((f) => ({ ...f, content: e.target.value }))}
placeholder="sudo systemctl restart nginx" placeholder="sudo systemctl restart nginx"
rows={4} 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>
{/* 标签 */} {/* 标签 */}
<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 <input
type="text" type="text"
value={form.tags} value={form.tags}
onChange={(e) => setForm((f) => ({ ...f, tags: e.target.value }))} onChange={(e) => setForm((f) => ({ ...f, tags: e.target.value }))}
placeholder="linux, nginx, ops" 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>
</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"> <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" /> <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>
{/* 搜索框 */} {/* 搜索框 */}
<div className="p-3 border-b border-dark-700/30"> <div className="p-3 border-b border-dark-700/30">
<div className="relative"> <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 <input
type="text" type="text"
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="搜索片段..." 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 && ( {query && (
<button <button
onClick={() => setQuery('')} onClick={() => setQuery('')}
className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 hover:bg-dark-600 rounded" 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> </button>
)} )}
</div> </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="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"> <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} {snippet.name}
</span> </span>
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity duration-150 flex-shrink-0"> <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)} onClick={() => onInsert(snippet.content)}
disabled={!canInsert} disabled={!canInsert}
title={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" /> <Code2 className="w-3.5 h-3.5" />
</button> </button>
@@ -324,7 +324,7 @@ function SnippetItem({ snippet, copied, canInsert, onInsert, onCopy, onEdit, onD
<button <button
onClick={() => onCopy(snippet)} onClick={() => onCopy(snippet)}
title="复制" 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 ? ( {copied ? (
<span className="text-xs text-green-400 px-1 animate-scale-in">✓</span> <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 <button
onClick={() => onEdit(snippet)} onClick={() => onEdit(snippet)}
title="编辑" 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" /> <Pencil className="w-3.5 h-3.5" />
</button> </button>
@@ -348,13 +348,13 @@ function SnippetItem({ snippet, copied, canInsert, onInsert, onCopy, onEdit, onD
await onDelete(snippet.id) await onDelete(snippet.id)
setConfirmDelete(false) 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>
<button <button
onClick={() => setConfirmDelete(false)} 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> </button>
@@ -363,7 +363,7 @@ function SnippetItem({ snippet, copied, canInsert, onInsert, onCopy, onEdit, onD
<button <button
onClick={() => setConfirmDelete(true)} onClick={() => setConfirmDelete(true)}
title="删除" 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" /> <Trash2 className="w-3.5 h-3.5" />
</button> </button>
@@ -373,7 +373,7 @@ function SnippetItem({ snippet, copied, canInsert, onInsert, onCopy, onEdit, onD
{/* 命令预览 */} {/* 命令预览 */}
<p <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} title={snippet.content}
> >
{preview} {preview}
@@ -385,7 +385,7 @@ function SnippetItem({ snippet, copied, canInsert, onInsert, onCopy, onEdit, onD
{snippet.tags.map((tag) => ( {snippet.tags.map((tag) => (
<span <span
key={tag} 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} {tag}
</span> </span>
+1 -1
View File
@@ -77,7 +77,7 @@ export function TabContextMenu({
w-full px-4 py-2 text-left text-sm transition-colors w-full px-4 py-2 text-left text-sm transition-colors
${item.danger ${item.danger
? 'text-red-400 hover:bg-red-500/20' ? 'text-red-400 hover:bg-red-500/20'
: 'text-gray-300 hover:bg-dark-700' : 'text-content-secondary hover:bg-dark-700'
} }
`} `}
> >
+5 -5
View File
@@ -63,19 +63,19 @@ export function TabItem({
border-r border-dark-700/20 cursor-pointer select-none border-r border-dark-700/20 cursor-pointer select-none
transition-all duration-150 relative transition-all duration-150 relative
${isActive ${isActive
? 'bg-dark-900 text-gray-200' ? 'bg-dark-900 text-content-primary'
: 'bg-dark-800 text-gray-500 hover:bg-dark-700 hover:text-gray-400' : 'bg-dark-800 text-txt-muted hover:bg-dark-700 hover:text-txt-muted'
} }
${isDragging ? 'opacity-50' : 'opacity-100'} ${isDragging ? 'opacity-50' : 'opacity-100'}
`} `}
> >
{/* Left drop indicator */} {/* Left drop indicator */}
{showLeftIndicator && ( {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 */} {/* Right drop indicator */}
{showRightIndicator && ( {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 */} {/* Status indicator */}
@@ -110,7 +110,7 @@ export function TabItem({
{/* Active tab bottom indicator — always rendered for width transition */} {/* Active tab bottom indicator — always rendered for width transition */}
<div <div
className={` 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 transition-[opacity,transform] duration-150 origin-left
${isActive ? 'opacity-100 scale-x-100' : 'opacity-0 scale-x-0'} ${isActive ? 'opacity-100 scale-x-100' : 'opacity-0 scale-x-0'}
`} `}
+9 -9
View File
@@ -25,17 +25,17 @@ function TerminalOverlay({
{connectionState === 'disconnected' && needsReconnect && ( {connectionState === 'disconnected' && needsReconnect && (
<div className="absolute inset-0 bg-dark-900/90 flex items-center justify-center z-10"> <div className="absolute inset-0 bg-dark-900/90 flex items-center justify-center z-10">
<div className="text-center"> <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" /> <TerminalIcon className="w-8 h-8 text-accent-blue" />
</div> </div>
<div className="text-gray-200 mb-2 text-lg">会话已恢复</div> <div className="text-content-primary mb-2 text-lg">会话已恢复</div>
<div className="text-gray-400 text-sm mb-6"> <div className="text-txt-muted text-sm mb-6">
上次关闭时的终端标签已恢复<br /> 上次关闭时的终端标签已恢复<br />
点击下方按钮重新建立 SSH 连接 点击下方按钮重新建立 SSH 连接
</div> </div>
<button <button
onClick={onReconnect} 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> </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="absolute inset-0 bg-dark-900/90 flex items-center justify-center z-10">
<div className="text-center"> <div className="text-center">
<div className="text-yellow-400 mb-2 text-lg">连接已挂起</div> <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> </div>
<button <button
onClick={onResume} 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>
<button <button
onClick={onReconnect} 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> </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="absolute inset-0 bg-dark-900/80 flex items-center justify-center z-10">
<div className="text-center"> <div className="text-center">
<div className="text-red-400 mb-2">连接失败</div> <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 <button
onClick={onReconnect} 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> </button>
@@ -24,7 +24,7 @@ function TerminalSearchBar({
}: TerminalSearchBarProps) { }: TerminalSearchBarProps) {
return ( 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"> <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 <input
ref={searchInputRef} ref={searchInputRef}
type="text" type="text"
@@ -41,30 +41,30 @@ function TerminalSearchBar({
e.stopPropagation() e.stopPropagation()
}} }}
placeholder="搜索..." 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 && ( {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} {searchResultIndex ?? 0}/{searchResultCount}
</span> </span>
)} )}
<button <button
onClick={onSearchPrevious} 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)" title="上一个 (Shift+Enter)"
> >
<ChevronUp size={16} /> <ChevronUp size={16} />
</button> </button>
<button <button
onClick={onSearchNext} 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)" title="下一个 (Enter)"
> >
<ChevronDown size={16} /> <ChevronDown size={16} />
</button> </button>
<button <button
onClick={onClose} 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)" title="关闭 (Escape)"
> >
<X size={16} /> <X size={16} />
+21 -9
View File
@@ -6,9 +6,12 @@ import { darkColors, lightColors, type AppThemeMode, type ColorMap } from '../th
* Resolves the *effective* theme ('dark' | 'light') from the user preference, * Resolves the *effective* theme ('dark' | 'light') from the user preference,
* including the 'system' → prefers-color-scheme mapping. * including the 'system' → prefers-color-scheme mapping.
*/ */
function resolveEffectiveTheme(_preference: AppThemeMode): 'dark' | 'light' { function resolveEffectiveTheme(preference: AppThemeMode): 'dark' | 'light' {
// 当前版本固定使用 dark 主题,后续版本开放主题切换 if (preference === 'system') {
return 'dark' if (typeof window === 'undefined') return 'dark'
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
return preference
} }
/** /**
@@ -38,8 +41,10 @@ export function useTheme() {
useEffect(() => { useEffect(() => {
const colors = effective === 'dark' ? darkColors : lightColors const colors = effective === 'dark' ? darkColors : lightColors
// Set data attribute for potential direct CSS selectors // Set data attribute for CSS selectors
document.documentElement.setAttribute('data-theme', effective) document.documentElement.setAttribute('data-theme', effective)
// Toggle Tailwind dark class
if (effective === 'dark') { if (effective === 'dark') {
document.documentElement.classList.add('dark') document.documentElement.classList.add('dark')
} else { } else {
@@ -49,9 +54,9 @@ export function useTheme() {
// Apply CSS custom properties // Apply CSS custom properties
applyCSSVars(colors) applyCSSVars(colors)
// Update body bg & colour (mirrors index.css body rules but via vars) // Update body bg & colour from tokens (not hardcoded)
document.body.style.backgroundColor = '#0f172a' document.body.style.backgroundColor = colors['surface-base'] || '#0f172a'
document.body.style.color = '#e2e8f0' document.body.style.color = colors['text-primary'] || '#e2e8f0'
}, [effective]) }, [effective])
// Listen for system colour-scheme changes when preference is 'system' // Listen for system colour-scheme changes when preference is 'system'
@@ -64,9 +69,16 @@ export function useTheme() {
const newEffective = e.matches ? 'dark' : 'light' const newEffective = e.matches ? 'dark' : 'light'
const colors = e.matches ? darkColors : lightColors const colors = e.matches ? darkColors : lightColors
document.documentElement.setAttribute('data-theme', newEffective) document.documentElement.setAttribute('data-theme', newEffective)
if (newEffective === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
applyCSSVars(colors) applyCSSVars(colors)
document.body.style.backgroundColor = '#0f172a' document.body.style.backgroundColor = colors['surface-base'] || '#0f172a'
document.body.style.color = '#e2e8f0' document.body.style.color = colors['text-primary'] || '#e2e8f0'
} }
mql.addEventListener('change', handler) mql.addEventListener('change', handler)
+53 -30
View File
@@ -3,55 +3,78 @@
@tailwind utilities; @tailwind utilities;
/* =================================================================== /* ===================================================================
* OpsXShell v1.3.0 — Theme CSS Variables * OpsXShell v1.4.0 — Professional Theme System
* *
* The default (no data-theme attribute) keeps dark mode as before so * Default (no data-theme) is dark mode for correct first paint.
* that the very first paint looks correct even before React hydrates. * useTheme.ts immediately sets data-theme + overrides vars on mount.
* useTheme.ts will immediately set data-theme + override vars on mount. *
* Design: Warp-inspired warm dark + Linear luminance stacking
* =================================================================== */ * =================================================================== */
:root { :root {
/* ---- Dark (default, matches original hard-coded values) ---- */ /* ---- Dark (default, matches warm slate-navy palette) ---- */
--color-dark-900: #0f172a; --color-dark-900: #0f172a;
--color-dark-800: #1e293b; --color-dark-800: #1e293b;
--color-dark-700: #334155; --color-dark-700: #334155;
--color-dark-600: #475569; --color-dark-600: #475569;
/* Semantic tokens (dark default) */ /* Semantic surface tokens */
--color-surface-base: #0f172a; --color-surface-base: #0f172a;
--color-surface-raised: #1e293b; --color-surface-raised: #1e293b;
--color-surface-overlay: #334155; --color-surface-overlay: #334155;
--color-surface-inset: #475569; --color-surface-inset: #475569;
--color-surface-hover: #374151;
--color-surface-active: #4b5563;
--color-text-primary: #e2e8f0; /* Text */
--color-text-primary: #f1f5f9;
--color-text-secondary: #cbd5e1; --color-text-secondary: #cbd5e1;
--color-text-muted: #94a3b8; --color-text-muted: #94a3b8;
--color-text-faint: #64748b; --color-text-faint: #64748b;
--color-text-dim: #475569; --color-text-dim: #475569;
--color-content-primary: #e2e8f0; /* Content */
--color-content-primary: #f1f5f9;
--color-content-secondary: #cbd5e1; --color-content-secondary: #cbd5e1;
--color-content-tertiary: #94a3b8; --color-content-tertiary: #94a3b8;
--color-content-inverse: #0f172a; --color-content-inverse: #0f172a;
--color-border: #334155; /* Border */
--color-border-strong: #475569; --color-border: #1e293b;
--color-border-subtle: #1e293b;
--color-border-default: #334155;
--color-border-strong: #475569;
--color-border-focus: #3b82f6;
--color-accent-blue: #3b82f6; /* Accent */
--color-accent-green: #10b981; --color-accent-blue: #3b82f6;
--color-accent-purple: #8b5cf6; --color-accent-blue-hover: #60a5fa;
--color-accent-orange: #f97316; --color-accent-green: #10b981;
--color-accent-red: #ef4444; --color-accent-purple: #8b5cf6;
--color-accent-orange: #f97316;
--color-accent-red: #ef4444;
--color-feedback-success: #10b981; /* Feedback */
--color-feedback-warning: #f59e0b; --color-feedback-success: #10b981;
--color-feedback-error: #ef4444; --color-feedback-success-bg: #064e3b;
--color-feedback-info: #3b82f6; --color-feedback-warning: #f59e0b;
--color-feedback-warning-bg: #78350f;
--color-feedback-error: #ef4444;
--color-feedback-error-bg: #7f1d1d;
--color-feedback-info: #3b82f6;
--color-feedback-info-bg: #1e3a5f;
--color-scrollbar-track: #1e293b; /* Placeholder */
--color-scrollbar-thumb: #475569; --color-placeholder: #64748b;
--color-scrollbar-thumb-h: #64748b; --color-placeholder-disabled: #475569;
/* UI chrome */
--color-scrollbar-track: #1e293b;
--color-scrollbar-thumb: #475569;
--color-scrollbar-thumb-h: #64748b;
--color-shadow-color: rgba(0, 0, 0, 0.4);
/* Terminal */
--color-terminal-bg: #0f172a; --color-terminal-bg: #0f172a;
} }
@@ -67,8 +90,8 @@ html, body {
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
background-color: #0f172a !important; background-color: var(--color-surface-base) !important;
color: #e2e8f0; color: var(--color-text-primary);
overflow: hidden; overflow: hidden;
} }
@@ -217,7 +240,7 @@ button {
100% { background-position: 200% 0; } 100% { background-position: 200% 0; }
} }
.animate-shimmer { .animate-shimmer {
background: linear-gradient(90deg, var(--color-dark-800) 25%, var(--color-dark-700) 50%, var(--color-dark-800) 75%); background: linear-gradient(90deg, var(--color-surface-raised) 25%, var(--color-surface-overlay) 50%, var(--color-surface-raised) 75%);
background-size: 200% 100%; background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite; animation: shimmer 1.5s ease-in-out infinite;
} }
+140 -78
View File
@@ -1,13 +1,14 @@
/** /**
* OpsXShell v1.3.0 — App Theme System * OpsXShell v1.4.0 — Professional Theme System
* *
* CSS-variable-driven theme definitions for Dark / Light / System modes. * CSS-variable-driven theme definitions for Dark / Light / System modes.
* Terminal themes (terminal.ts) remain independent — this only covers the * Inspired by Warp (warm dark terminal) + Linear (precise luminance stacking).
* Chrome / UI layer.
* *
* Design references: * Design principles:
* - Dark: existing slate-blue palette * - Dark: warm near-black base, soft parchment text, restrained accent
* - Light: VS Code Light+ inspired — white canvas, soft grey borders, blue accent * - Light: clean canvas, ink-dark text, vibrant accent
* - All colors use semantic tokens so components never reference raw values
* - Surface elevation via luminance stepping, not shadows
*/ */
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -20,109 +21,170 @@ export type AppThemeMode = 'dark' | 'light' | 'system'
export type ColorMap = Record<string, string> export type ColorMap = Record<string, string>
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Dark palette (unchanged from original hard-coded values) // Dark palette
//
// Inspired by:
// - Warp: warm near-black base (#0f172a slate-blue), cream-white text
// - Linear: luminance-stacking surface hierarchy, semi-transparent borders
//
// Surface ladder: base → raised → overlay → inset (deep → shallow)
// Text ladder: primary → secondary → muted → faint → dim (hot → cold)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const darkColors: ColorMap = { export const darkColors: ColorMap = {
// Background surfaces (deepest → shallowest) // ── Background surfaces (deepest → shallowest) ──────────────────────
// Level 0: Page canvas — deep slate-navy (Warp-inspired warmth)
'dark-900': '#0f172a', 'dark-900': '#0f172a',
// Level 1: Sidebar / panels — one step up
'dark-800': '#1e293b', 'dark-800': '#1e293b',
// Level 2: Cards / elevated surfaces — visible but restrained
'dark-700': '#334155', 'dark-700': '#334155',
// Level 3: Hover / active states — lightest dark surface
'dark-600': '#475569', 'dark-600': '#475569',
// Semantic surface tokens // ── Semantic surface tokens ──────────────────────────────────────────
'surface-base': '#0f172a', 'surface-base': '#0f172a',
'surface-raised': '#1e293b', 'surface-raised': '#1e293b',
'surface-overlay': '#334155', 'surface-overlay': '#334155',
'surface-inset': '#475569', 'surface-inset': '#475569',
// Hover elevation
'surface-hover': '#374151', // gray-700 — subtle lift on hover
// Active/pressed
'surface-active': '#4b5563', // gray-600 — pressed state
// Text // ── Text ─────────────────────────────────────────────────────────────
'text-primary': '#e2e8f0', // gray-200 'text-primary': '#f1f5f9', // warm parchment (slate-50) — not pure white
'text-secondary': '#cbd5e1', // gray-300 'text-secondary': '#cbd5e1', // slate-300 — comfortable reading
'text-muted': '#94a3b8', // gray-400 'text-muted': '#94a3b8', // slate-400 — secondary info
'text-faint': '#64748b', // gray-500 'text-faint': '#64748b', // slate-500 — placeholder / dim
'text-dim': '#475569', // gray-600 'text-dim': '#475569', // slate-600 — disabled / decorative
// Content tokens // ── Content tokens ───────────────────────────────────────────────────
'content-primary': '#e2e8f0', 'content-primary': '#f1f5f9',
'content-secondary': '#cbd5e1', 'content-secondary': '#cbd5e1',
'content-tertiary': '#94a3b8', 'content-tertiary': '#94a3b8',
'content-inverse': '#0f172a', 'content-inverse': '#0f172a',
// Border tokens // ── Border tokens ────────────────────────────────────────────────────
'border': '#334155', // Linear-inspired: semi-transparent feel via opaque colors
'border-strong': '#475569', 'border': '#1e293b', // whisper-thin separator
'border-subtle': '#1e293b', // minimal visibility
'border-default': '#334155', // standard component border
'border-strong': '#475569', // emphasis / focus
'border-focus': '#3b82f6', // input focus ring (blue accent)
// Accent // ── Accent ───────────────────────────────────────────────────────────
'accent-blue': '#3b82f6', 'accent-blue': '#3b82f6',
'accent-green': '#10b981', 'accent-blue-hover': '#60a5fa', // lighter blue for hover
'accent-purple': '#8b5cf6', 'accent-green': '#10b981',
'accent-orange': '#f97316', 'accent-purple': '#8b5cf6',
'accent-red': '#ef4444', 'accent-orange': '#f97316',
'accent-red': '#ef4444',
// Feedback tokens // ── Feedback tokens ──────────────────────────────────────────────────
'feedback-success': '#10b981', 'feedback-success': '#10b981',
'feedback-warning': '#f59e0b', 'feedback-success-bg': '#064e3b', // emerald-900 tint
'feedback-error': '#ef4444', 'feedback-warning': '#f59e0b',
'feedback-info': '#3b82f6', 'feedback-warning-bg': '#78350f', // amber-900 tint
'feedback-error': '#ef4444',
'feedback-error-bg': '#7f1d1d', // red-900 tint
'feedback-info': '#3b82f6',
'feedback-info-bg': '#1e3a5f', // blue-900 tint
// UI chrome // ── Placeholder ──────────────────────────────────────────────────────
'placeholder': '#64748b', // slate-500
'placeholder-disabled': '#475569', // slate-600
// ── UI chrome ────────────────────────────────────────────────────────
'scrollbar-track': '#1e293b', 'scrollbar-track': '#1e293b',
'scrollbar-thumb': '#475569', 'scrollbar-thumb': '#475569',
'scrollbar-thumb-h': '#64748b', 'scrollbar-thumb-h': '#64748b',
'shadow-color': 'rgba(0, 0, 0, 0.4)',
// ── Terminal ─────────────────────────────────────────────────────────
'terminal-bg': '#0f172a',
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Light palette (VS Code Light+ inspired) // Light palette
//
// Inspired by:
// - Notion: warm white canvas, soft borders, clean hierarchy
// - VS Code Light+: readable code editor aesthetic
// - Linear light: subtle cool gray surfaces
//
// Key: warmer than typical "sterile white" — invites long sessions
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const lightColors: ColorMap = { export const lightColors: ColorMap = {
// Background surfaces // ── Background surfaces (lightest → darkest) ────────────────────────
// Level 0: Page canvas — warm white
'dark-900': '#ffffff', 'dark-900': '#ffffff',
'dark-800': '#f3f4f6', // gray-100 // Level 1: Sidebar / panels — barely-warm gray
'dark-700': '#e5e7eb', // gray-200 'dark-800': '#f8fafc', // slate-50
'dark-600': '#d1d5db', // gray-300 // Level 2: Cards / elevated — soft boundary
'dark-700': '#f1f5f9', // slate-100
// Level 3: Hover / active — noticeable surface
'dark-600': '#e2e8f0', // slate-200
// Semantic surface tokens // ── Semantic surface tokens ──────────────────────────────────────────
'surface-base': '#ffffff', 'surface-base': '#ffffff',
'surface-raised': '#f3f4f6', 'surface-raised': '#f8fafc',
'surface-overlay': '#e5e7eb', 'surface-overlay': '#f1f5f9',
'surface-inset': '#d1d5db', 'surface-inset': '#e2e8f0',
// Hover elevation
'surface-hover': '#e2e8f0', // slate-200
// Active/pressed
'surface-active': '#cbd5e1', // slate-300
// Text // ── Text ─────────────────────────────────────────────────────────────
'text-primary': '#1f2937', // gray-800 'text-primary': '#0f172a', // slate-900 — deep ink (NOT pure black)
'text-secondary': '#374151', // gray-700 'text-secondary': '#334155', // slate-700 — comfortable body text
'text-muted': '#6b7280', // gray-500 'text-muted': '#64748b', // slate-500 — secondary info
'text-faint': '#9ca3af', // gray-400 'text-faint': '#94a3b8', // slate-400 — placeholder / dim
'text-dim': '#d1d5db', // gray-300 'text-dim': '#cbd5e1', // slate-300 — disabled / decorative
// Content tokens // ── Content tokens ───────────────────────────────────────────────────
'content-primary': '#1f2937', 'content-primary': '#0f172a',
'content-secondary': '#374151', 'content-secondary': '#334155',
'content-tertiary': '#6b7280', 'content-tertiary': '#64748b',
'content-inverse': '#ffffff', 'content-inverse': '#ffffff',
// Border tokens // ── Border tokens ────────────────────────────────────────────────────
'border': '#e5e7eb', 'border': '#f1f5f9', // slate-100 — whisper thin
'border-strong': '#d1d5db', 'border-subtle': '#f1f5f9', // minimal
'border-default': '#e2e8f0', // slate-200 — standard
'border-strong': '#cbd5e1', // slate-300 — emphasis
'border-focus': '#3b82f6', // same blue accent
// Accent (kept the same hue family, slightly adjusted for light bg) // ── Accent ───────────────────────────────────────────────────────────
'accent-blue': '#2563eb', 'accent-blue': '#2563eb', // blue-600 — deeper for white bg
'accent-green': '#059669', 'accent-blue-hover': '#1d4ed8', // blue-700 — hover
'accent-purple': '#7c3aed', 'accent-green': '#059669', // emerald-600
'accent-orange': '#ea580c', 'accent-purple': '#7c3aed', // violet-600
'accent-red': '#dc2626', 'accent-orange': '#ea580c', // orange-600
'accent-red': '#dc2626', // red-600
// Feedback tokens // ── Feedback tokens ──────────────────────────────────────────────────
'feedback-success': '#059669', 'feedback-success': '#059669',
'feedback-warning': '#d97706', 'feedback-success-bg': '#d1fae5', // emerald-100
'feedback-error': '#dc2626', 'feedback-warning': '#d97706',
'feedback-info': '#2563eb', 'feedback-warning-bg': '#fef3c7', // amber-100
'feedback-error': '#dc2626',
'feedback-error-bg': '#fee2e2', // red-100
'feedback-info': '#2563eb',
'feedback-info-bg': '#dbeafe', // blue-100
// UI chrome // ── Placeholder ──────────────────────────────────────────────────────
'scrollbar-track': '#f3f4f6', 'placeholder': '#94a3b8', // slate-400
'scrollbar-thumb': '#d1d5db', 'placeholder-disabled': '#cbd5e1', // slate-300
'scrollbar-thumb-h': '#9ca3af',
// ── UI chrome ────────────────────────────────────────────────────────
'scrollbar-track': '#f8fafc',
'scrollbar-thumb': '#cbd5e1',
'scrollbar-thumb-h': '#94a3b8',
'shadow-color': 'rgba(0, 0, 0, 0.08)',
// ── Terminal ─────────────────────────────────────────────────────────
'terminal-bg': '#ffffff',
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+43 -11
View File
@@ -8,36 +8,68 @@ export default {
theme: { theme: {
extend: { extend: {
colors: { colors: {
// Semantic color tokens — driven by CSS custom properties set per theme // ── Semantic surface tokens (theme-aware via CSS vars) ──────────
surface: { surface: {
base: 'var(--color-surface-base)', base: 'var(--color-surface-base)',
raised: 'var(--color-surface-raised)', raised: 'var(--color-surface-raised)',
overlay: 'var(--color-surface-overlay)', overlay: 'var(--color-surface-overlay)',
inset: 'var(--color-surface-inset)', inset: 'var(--color-surface-inset)',
hover: 'var(--color-surface-hover)',
active: 'var(--color-surface-active)',
}, },
// Legacy dark-* aliases for backward compatibility // ── Legacy dark-* aliases (backward compat → surface tokens) ───
dark: { dark: {
900: 'var(--color-surface-base)', 900: 'var(--color-surface-base)',
800: 'var(--color-surface-raised)', 800: 'var(--color-surface-raised)',
700: 'var(--color-surface-overlay)', 700: 'var(--color-surface-overlay)',
600: 'var(--color-surface-inset)', 600: 'var(--color-surface-inset)',
}, },
// ── Accent colors ──────────────────────────────────────────────
accent: { accent: {
blue: 'var(--color-accent-blue)', blue: 'var(--color-accent-blue)',
green: 'var(--color-accent-green)', 'blue-hover':'var(--color-accent-blue-hover)',
purple: 'var(--color-accent-purple)', green: 'var(--color-accent-green)',
orange: 'var(--color-accent-orange)', purple: 'var(--color-accent-purple)',
red: 'var(--color-accent-red)', orange: 'var(--color-accent-orange)',
red: 'var(--color-accent-red)',
}, },
// ── Content / text tokens ──────────────────────────────────────
content: { content: {
primary: 'var(--color-content-primary)', primary: 'var(--color-content-primary)',
secondary: 'var(--color-content-secondary)', secondary: 'var(--color-content-secondary)',
tertiary: 'var(--color-content-tertiary)', tertiary: 'var(--color-content-tertiary)',
inverse: 'var(--color-content-inverse)', inverse: 'var(--color-content-inverse)',
}, },
// ── Text tokens (direct mapping) ───────────────────────────────
txt: {
primary: 'var(--color-text-primary)',
secondary: 'var(--color-text-secondary)',
muted: 'var(--color-text-muted)',
faint: 'var(--color-text-faint)',
dim: 'var(--color-text-dim)',
},
// ── Border tokens ──────────────────────────────────────────────
border: { border: {
DEFAULT: 'var(--color-border)', DEFAULT: 'var(--color-border-default)',
subtle: 'var(--color-border-subtle)',
strong: 'var(--color-border-strong)', strong: 'var(--color-border-strong)',
focus: 'var(--color-border-focus)',
},
// ── Placeholder tokens ─────────────────────────────────────────
ph: {
DEFAULT: 'var(--color-placeholder)',
disabled: 'var(--color-placeholder-disabled)',
},
// ── Feedback / status tokens ───────────────────────────────────
feedback: {
'success': 'var(--color-feedback-success)',
'success-bg': 'var(--color-feedback-success-bg)',
'warning': 'var(--color-feedback-warning)',
'warning-bg': 'var(--color-feedback-warning-bg)',
'error': 'var(--color-feedback-error)',
'error-bg': 'var(--color-feedback-error-bg)',
'info': 'var(--color-feedback-info)',
'info-bg': 'var(--color-feedback-info-bg)',
}, },
}, },
fontFamily: { fontFamily: {