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

+60 -60
View File
@@ -141,7 +141,7 @@ export default function SettingsView() {
if (!isLoaded) {
return (
<div className="h-full flex items-center justify-center bg-dark-900">
<div className="text-gray-400">加载中...</div>
<div className="text-txt-muted">加载中...</div>
</div>
)
}
@@ -197,8 +197,8 @@ export default function SettingsView() {
onClick={() => setSetting('theme', theme.value as 'dark' | 'light' | 'system')}
className={`p-3 rounded-lg border text-sm transition-colors ${
settings.theme === theme.value
? 'border-accent-blue bg-accent-blue/20 text-accent-blue'
: 'border-dark-700/30 hover:border-dark-600 text-gray-300'
? 'border-accent-blue bg-surface-baseccent-blue/20 text-accent-blue'
: 'border-dark-700/30 hover:border-dark-600 text-content-secondary'
}`}
>
{theme.label}
@@ -212,7 +212,7 @@ export default function SettingsView() {
<select
value={settings.language}
onChange={(e) => setSetting('language', e.target.value as 'zh-CN' | 'en-US')}
className="w-full mt-3 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
className="w-full mt-3 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
>
<option value="zh-CN">简体中文</option>
<option value="en-US">English</option>
@@ -229,11 +229,11 @@ export default function SettingsView() {
<SettingSectionTitle>字体</SettingSectionTitle>
<div className="grid grid-cols-2 gap-4 mt-3">
<div>
<label className="text-sm text-gray-500">字体族</label>
<label className="text-sm text-txt-muted">字体族</label>
<select
value={settings.fontFamily}
onChange={(e) => setSetting('fontFamily', e.target.value)}
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
>
<option>JetBrains Mono</option>
<option>Fira Code</option>
@@ -242,11 +242,11 @@ export default function SettingsView() {
</select>
</div>
<div>
<label className="text-sm text-gray-500">字体大小</label>
<label className="text-sm text-txt-muted">字体大小</label>
<select
value={settings.fontSize}
onChange={(e) => setSetting('fontSize', parseInt(e.target.value))}
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
>
{[12, 13, 14, 15, 16, 18, 20].map((size) => (
<option key={size} value={size}>{size}px</option>
@@ -265,8 +265,8 @@ export default function SettingsView() {
onClick={() => setSetting('terminalTheme', key as 'dracula' | 'monokai' | 'solarized' | 'one-dark')}
className={`p-3 rounded-lg border text-sm transition-colors ${
settings.terminalTheme === key
? 'border-accent-blue bg-accent-blue/20 text-accent-blue'
: 'border-dark-700/30 hover:border-dark-600 text-gray-300'
? 'border-accent-blue bg-surface-baseccent-blue/20 text-accent-blue'
: 'border-dark-700/30 hover:border-dark-600 text-content-secondary'
}`}
>
<div
@@ -299,7 +299,7 @@ export default function SettingsView() {
<div className="flex justify-end">
<button
onClick={() => keybindingState.resetAll()}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-gray-400 hover:text-accent-blue rounded-lg hover:bg-dark-700/50 transition-colors"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-txt-muted hover:text-accent-blue rounded-lg hover:bg-dark-700/50 transition-colors"
>
<RotateCcw className="w-3.5 h-3.5" />
重置全部为默认
@@ -311,7 +311,7 @@ export default function SettingsView() {
const defs = getKeybindingsByCategory(category)
return (
<SettingCard key={category}>
<h3 className="text-sm font-semibold text-gray-300 mb-3">{category}</h3>
<h3 className="text-sm font-semibold text-content-secondary mb-3">{category}</h3>
<div className="space-y-1">
{defs.map((def) => {
const currentKeys = allKeys[def.id]
@@ -323,14 +323,14 @@ export default function SettingsView() {
key={def.id}
className={`flex items-center justify-between px-3 py-2 rounded-lg transition-colors ${
isRecordingThis
? 'bg-accent-blue/10 border border-accent-blue/30'
? 'bg-surface-baseccent-blue/10 border border-accent-blue/30'
: 'hover:bg-dark-800/60'
}`}
>
<div className="flex-1 min-w-0">
<span className="text-sm text-gray-200">{def.label}</span>
<span className="text-sm text-content-primary">{def.label}</span>
{isCustom && (
<span className="ml-2 text-[10px] text-accent-blue bg-accent-blue/10 px-1.5 py-0.5 rounded">
<span className="ml-2 text-[10px] text-accent-blue bg-surface-baseccent-blue/10 px-1.5 py-0.5 rounded">
自定义
</span>
)}
@@ -339,12 +339,12 @@ export default function SettingsView() {
<div className="flex items-center gap-2">
{isRecordingThis ? (
<div className="flex items-center gap-2">
<kbd className="inline-flex items-center px-3 py-1 rounded bg-accent-blue/20 border border-accent-blue/40 text-xs text-accent-blue font-mono animate-pulse">
<kbd className="inline-flex items-center px-3 py-1 rounded bg-surface-baseccent-blue/20 border border-accent-blue/40 text-xs text-accent-blue font-mono animate-pulse">
{recordedKeys || '按下快捷键...'}
</kbd>
<button
onClick={() => stopRecording()}
className="text-xs text-gray-500 hover:text-gray-300"
className="text-xs text-txt-muted hover:text-content-secondary"
>
取消
</button>
@@ -368,8 +368,8 @@ export default function SettingsView() {
<kbd
className={`inline-flex items-center px-2.5 py-1 rounded text-xs font-mono transition-colors ${
isCustom
? 'bg-accent-blue/10 border border-accent-blue/30 text-accent-blue'
: 'bg-dark-700 border border-dark-600 text-gray-300'
? 'bg-surface-baseccent-blue/10 border border-accent-blue/30 text-accent-blue'
: 'bg-dark-700 border border-dark-600 text-content-secondary'
} group-hover:border-accent-blue/50`}
>
{currentKeys}
@@ -378,7 +378,7 @@ export default function SettingsView() {
{isCustom && (
<button
onClick={() => keybindingState.resetKeybinding(def.id)}
className="text-gray-600 hover:text-gray-400 transition-colors"
className="text-txt-faint hover:text-txt-muted transition-colors"
title="重置为默认"
>
<RotateCcw className="w-3 h-3" />
@@ -398,7 +398,7 @@ export default function SettingsView() {
{/* 录入确认弹层 */}
{recordingId && recordedKeys && (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 px-5 py-3 bg-dark-800 border border-dark-600 rounded-xl shadow-2xl">
<span className="text-sm text-gray-300">
<span className="text-sm text-content-secondary">
设置为 <kbd className="px-1.5 py-0.5 rounded bg-dark-700 text-accent-blue font-mono text-xs">{recordedKeys}</kbd>
</span>
<button
@@ -406,20 +406,20 @@ export default function SettingsView() {
await keybindingState.setCustomKeybinding(recordingId, recordedKeys)
stopRecording()
}}
className="px-3 py-1 text-xs bg-accent-blue text-white rounded-lg hover:bg-accent-blue/80 transition-colors"
className="px-3 py-1 text-xs bg-surface-baseccent-blue text-content-primary rounded-lg hover:bg-surface-baseccent-blue/80 transition-colors"
>
确认
</button>
<button
onClick={stopRecording}
className="px-3 py-1 text-xs text-gray-400 hover:text-gray-200 rounded-lg hover:bg-dark-700 transition-colors"
className="px-3 py-1 text-xs text-txt-muted hover:text-content-primary rounded-lg hover:bg-dark-700 transition-colors"
>
取消
</button>
</div>
)}
<p className="text-xs text-gray-600">
<p className="text-xs text-txt-faint">
点击快捷键可重新绑定 · 按 Esc 取消录入
</p>
</div>
@@ -445,7 +445,7 @@ export default function SettingsView() {
type="number"
value={settings.defaultPort}
onChange={(e) => setSetting('defaultPort', parseInt(e.target.value) || 22)}
className="w-32 mt-2 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
className="w-32 mt-2 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
/>
</SettingCard>
@@ -455,7 +455,7 @@ export default function SettingsView() {
type="number"
value={settings.timeout}
onChange={(e) => setSetting('timeout', parseInt(e.target.value) || 30)}
className="w-32 mt-2 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
className="w-32 mt-2 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
/>
</SettingCard>
@@ -465,60 +465,60 @@ export default function SettingsView() {
<span className="text-accent-blue text-xs font-semibold uppercase tracking-wider">Pro</span>
<SettingSectionTitle className="mb-0">连接管理</SettingSectionTitle>
</div>
<p className="text-sm text-gray-500 mb-4">配置连接池行为,优化资源使用和性能</p>
<p className="text-sm text-txt-muted mb-4">配置连接池行为,优化资源使用和性能</p>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-gray-500">最大连接数</label>
<label className="text-sm text-txt-muted">最大连接数</label>
<input
type="number"
min={1}
max={50}
value={settings.maxConnections || 10}
onChange={(e) => setSetting('maxConnections', parseInt(e.target.value) || 10)}
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
/>
<div className="text-xs text-gray-600 mt-1">最大并发 SSH 连接数</div>
<div className="text-xs text-txt-faint mt-1">最大并发 SSH 连接数</div>
</div>
<div>
<label className="text-sm text-gray-500">最大后台连接</label>
<label className="text-sm text-txt-muted">最大后台连接</label>
<input
type="number"
min={1}
max={20}
value={settings.maxBackgroundConnections || 5}
onChange={(e) => setSetting('maxBackgroundConnections', parseInt(e.target.value) || 5)}
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
/>
<div className="text-xs text-gray-600 mt-1">非活跃标签保持连接数</div>
<div className="text-xs text-txt-faint mt-1">非活跃标签保持连接数</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-gray-500">空闲超时 (分钟)</label>
<label className="text-sm text-txt-muted">空闲超时 (分钟)</label>
<input
type="number"
min={1}
max={120}
value={Math.floor((settings.idleTimeoutMs || 5 * 60 * 1000) / 60000)}
onChange={(e) => setSetting('idleTimeoutMs', (parseInt(e.target.value) || 5) * 60000)}
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
/>
<div className="text-xs text-gray-600 mt-1">无操作后进入挂起</div>
<div className="text-xs text-txt-faint mt-1">无操作后进入挂起</div>
</div>
<div>
<label className="text-sm text-gray-500">挂起超时 (分钟)</label>
<label className="text-sm text-txt-muted">挂起超时 (分钟)</label>
<input
type="number"
min={1}
max={240}
value={Math.floor((settings.suspendTimeoutMs || 30 * 60 * 1000) / 60000)}
onChange={(e) => setSetting('suspendTimeoutMs', (parseInt(e.target.value) || 30) * 60000)}
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary focus:outline-none focus:border-accent-blue"
/>
<div className="text-xs text-gray-600 mt-1">挂起后自动断开</div>
<div className="text-xs text-txt-faint mt-1">挂起后自动断开</div>
</div>
</div>
@@ -571,11 +571,11 @@ export default function SettingsView() {
<SettingCard>
<div className="flex flex-col items-center py-6">
<div className="w-20 h-20 bg-dark-700 rounded-2xl flex items-center justify-center mb-4">
<Terminal className="w-10 h-10 text-white" />
<Terminal className="w-10 h-10 text-content-primary" />
</div>
<h3 className="text-xl font-semibold text-gray-200 mb-1">OpsXShell</h3>
<p className="text-sm text-gray-500 mb-4">版本 {appVersion}</p>
<p className="text-sm text-gray-400 max-w-md text-center">
<h3 className="text-xl font-semibold text-content-primary mb-1">OpsXShell</h3>
<p className="text-sm text-txt-muted mb-4">版本 {appVersion}</p>
<p className="text-sm text-txt-muted max-w-md text-center">
跨平台 SSH/SFTP 终端工具 · 简洁高效 · 安全可靠 · 操作便捷人性化
</p>
</div>
@@ -595,8 +595,8 @@ export default function SettingsView() {
['Lucide React', '0.294+'],
].map(([name, ver]) => (
<div key={name} className="flex justify-between py-1 border-b border-dark-700/20">
<span className="text-gray-400">{name}</span>
<span className="text-gray-500 font-mono">{ver}</span>
<span className="text-txt-muted">{name}</span>
<span className="text-txt-muted font-mono">{ver}</span>
</div>
))}
</div>
@@ -604,10 +604,10 @@ export default function SettingsView() {
<SettingCard>
<SettingSectionTitle>开源许可</SettingSectionTitle>
<p className="text-sm text-gray-400 mt-2">
<p className="text-sm text-txt-muted mt-2">
OpsXShell 基于 MIT 许可证开源。本项目使用了以下开源组件:
</p>
<ul className="mt-2 text-sm text-gray-500 list-disc list-inside space-y-1">
<ul className="mt-2 text-sm text-txt-muted list-disc list-inside space-y-1">
<li>ssh2 — MIT License</li>
<li>xterm.js — MIT License</li>
<li>zmodem-ts — MIT License</li>
@@ -631,18 +631,18 @@ export default function SettingsView() {
{/* 搜索框 */}
<div className="p-3 border-b border-dark-700/20">
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-500" />
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-txt-muted" />
<input
type="text"
placeholder="搜索设置..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-8 pr-8 py-1.5 text-sm bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 placeholder-gray-600 focus:outline-none focus:border-accent-blue/60"
className="w-full pl-8 pr-8 py-1.5 text-sm bg-dark-900 border border-dark-700/30 rounded-lg text-content-secondary placeholder-ph focus:outline-none focus:border-accent-blue/60"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300"
className="absolute right-2 top-1/2 -translate-y-1/2 text-txt-muted hover:text-content-secondary"
>
<X className="w-3.5 h-3.5" />
</button>
@@ -661,14 +661,14 @@ export default function SettingsView() {
onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-all ${
active
? 'bg-accent-blue text-white shadow-sm'
: 'text-gray-400 hover:bg-dark-700/50 hover:text-gray-200'
? 'bg-surface-baseccent-blue text-content-primary shadow-sm'
: 'text-txt-muted hover:bg-dark-700/50 hover:text-content-primary'
}`}
>
<Icon className={`w-4 h-4 flex-shrink-0 ${active ? 'text-white' : ''}`} />
<Icon className={`w-4 h-4 flex-shrink-0 ${active ? 'text-content-primary' : ''}`} />
<div className="min-w-0">
<div className="text-sm font-medium truncate">{tab.label}</div>
<div className={`text-xs truncate ${active ? 'text-white/70' : 'text-gray-600'}`}>
<div className={`text-xs truncate ${active ? 'text-content-primary/70' : 'text-txt-faint'}`}>
{tab.description}
</div>
</div>
@@ -682,8 +682,8 @@ export default function SettingsView() {
<div className="flex-1 flex flex-col min-w-0">
{/* Header bar */}
<div className="flex-shrink-0 px-8 pt-6 pb-4 border-b border-dark-700/20">
<h1 className="text-xl font-semibold text-gray-100">{currentMeta.label}</h1>
<p className="text-sm text-gray-500 mt-0.5">{currentMeta.description}</p>
<h1 className="text-xl font-semibold text-content-primary">{currentMeta.label}</h1>
<p className="text-sm text-txt-muted mt-0.5">{currentMeta.description}</p>
</div>
{/* 内容 */}
@@ -727,7 +727,7 @@ function SettingSectionTitle({
className?: string
}) {
return (
<div className={`text-sm font-semibold text-gray-300 mb-1 ${className}`}>
<div className={`text-sm font-semibold text-content-secondary mb-1 ${className}`}>
{children}
</div>
)
@@ -748,13 +748,13 @@ function SettingToggle({
return (
<div className="flex items-center justify-between">
<div className="mr-4">
<div className="font-medium text-gray-200 text-sm">{label}</div>
<div className="text-xs text-gray-500 mt-0.5">{description}</div>
<div className="font-medium text-content-primary text-sm">{label}</div>
<div className="text-xs text-txt-muted mt-0.5">{description}</div>
</div>
<button
onClick={() => onChange(!checked)}
className={`relative flex-shrink-0 w-11 h-6 rounded-full transition-colors ${
checked ? 'bg-accent-blue' : 'bg-dark-600'
checked ? 'bg-surface-baseccent-blue' : 'bg-dark-600'
}`}
>
<span