feat(ui): 实现深色/浅色主题切换功能

- 添加 CSS 变量定义浅色和深色主题色板
- 扩展 Tailwind 配置支持语义化颜色 (surface-*, fg-*, line-*, code)
- 创建 useTheme hook 管理主题状态和持久化
- 创建 ThemeToggle 组件支持三种模式 (light/dark/system)
- 迁移所有组件从硬编码 gray-* 到语义化颜色
- 支持系统主题偏好检测 (prefers-color-scheme)
- 添加主题初始化脚本防止闪烁 (FOUC)
This commit is contained in:
2025-12-15 15:47:32 +08:00
parent cd0dd814ab
commit 5b7b0ff1e4
39 changed files with 1002 additions and 652 deletions
+31 -31
View File
@@ -56,7 +56,7 @@ function getModeColor(mode: AgentListItem['mode']) {
case 'internal':
return 'bg-slate-500/20 text-slate-400';
default:
return 'bg-gray-500/20 text-gray-400';
return 'bg-surface-muted/20 text-fg-muted';
}
}
@@ -203,7 +203,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
const LoadingSkeleton = () => (
<div className="space-y-3 p-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="flex items-center gap-3 p-3 bg-gray-900/50 rounded-lg">
<div key={i} className="flex items-center gap-3 p-3 bg-surface-base/50 rounded-lg">
<Skeleton className="h-4 w-4" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-32" />
@@ -227,18 +227,18 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
layout
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gray-900/50 rounded-lg overflow-hidden"
className="bg-surface-base/50 rounded-lg overflow-hidden"
>
{/* Agent Header */}
<div
className={cn(
'flex items-center gap-3 p-3',
'hover:bg-gray-900/80 transition-colors cursor-pointer'
'hover:bg-surface-base/80 transition-colors cursor-pointer'
)}
onClick={() => toggleExpanded(agent.name)}
>
{/* Expand Icon */}
<button className="text-gray-500 hover:text-gray-300">
<button className="text-fg-subtle hover:text-fg-secondary">
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
@@ -254,7 +254,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-200">{agent.name}</span>
<span className="font-medium text-fg-secondary">{agent.name}</span>
<span className={cn('text-xs px-2 py-0.5 rounded-full', getModeColor(agent.mode))}>
{getModeText(agent.mode)}
</span>
@@ -264,7 +264,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
</span>
)}
</div>
<p className="text-xs text-gray-500 truncate">{agent.description}</p>
<p className="text-xs text-fg-subtle truncate">{agent.description}</p>
</div>
{/* Actions */}
@@ -289,7 +289,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
variant="ghost"
size="sm"
onClick={() => toggleExpanded(agent.name)}
className="text-gray-400 hover:text-gray-300"
className="text-fg-muted hover:text-fg-secondary"
title="View"
>
<Eye size={14} />
@@ -312,7 +312,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
variant="ghost"
size="sm"
onClick={() => handleCopy(agent.name)}
className="text-gray-400 hover:text-gray-300"
className="text-fg-muted hover:text-fg-secondary"
title="Copy"
>
<Copy size={14} />
@@ -346,21 +346,21 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="px-4 pb-3 pt-1 border-t border-gray-700/50 space-y-3">
<div className="px-4 pb-3 pt-1 border-t border-line/50 space-y-3">
{detail ? (
<>
{/* Model Info */}
{detail.model && (
<div className="flex items-start gap-2">
<Cpu size={14} className="text-gray-500 mt-0.5" />
<Cpu size={14} className="text-fg-subtle mt-0.5" />
<div className="text-xs">
<span className="text-gray-400">Model:</span>{' '}
<span className="text-gray-300">
<span className="text-fg-muted">Model:</span>{' '}
<span className="text-fg-secondary">
{detail.model.provider && `${detail.model.provider}/`}
{detail.model.model || 'default'}
</span>
{detail.model.temperature !== undefined && (
<span className="text-gray-500 ml-2">
<span className="text-fg-subtle ml-2">
temp: {detail.model.temperature}
</span>
)}
@@ -371,9 +371,9 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{/* Tools Config */}
{detail.tools && (
<div className="flex items-start gap-2">
<Layers size={14} className="text-gray-500 mt-0.5" />
<Layers size={14} className="text-fg-subtle mt-0.5" />
<div className="text-xs">
<span className="text-gray-400">Tools:</span>{' '}
<span className="text-fg-muted">Tools:</span>{' '}
{detail.tools.enabled ? (
<span className="text-green-400">
Only: {detail.tools.enabled.join(', ')}
@@ -383,7 +383,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
Disabled: {detail.tools.disabled.join(', ')}
</span>
) : (
<span className="text-gray-300">All enabled</span>
<span className="text-fg-secondary">All enabled</span>
)}
{detail.tools.noTask && (
<span className="text-red-400 ml-2">(No nested tasks)</span>
@@ -395,16 +395,16 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{/* Max Steps */}
{detail.maxSteps && (
<div className="text-xs">
<span className="text-gray-400">Max Steps:</span>{' '}
<span className="text-gray-300">{detail.maxSteps}</span>
<span className="text-fg-muted">Max Steps:</span>{' '}
<span className="text-fg-secondary">{detail.maxSteps}</span>
</div>
)}
{/* Prompt Preview */}
{detail.prompt && (
<div className="text-xs">
<span className="text-gray-400">System Prompt:</span>
<pre className="mt-1 p-2 bg-gray-800/50 rounded text-gray-300 overflow-x-auto max-h-32 text-[11px] leading-relaxed">
<span className="text-fg-muted">System Prompt:</span>
<pre className="mt-1 p-2 bg-surface-subtle/50 rounded text-fg-secondary overflow-x-auto max-h-32 text-[11px] leading-relaxed">
{detail.prompt.slice(0, 500)}
{detail.prompt.length > 500 && '...'}
</pre>
@@ -474,7 +474,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
transition={smoothTransition}
onClick={(e) => e.stopPropagation()}
className={cn(
'bg-gray-800 max-h-[90vh] overflow-hidden flex flex-col',
'bg-surface-subtle max-h-[90vh] overflow-hidden flex flex-col',
responsive
? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg'
: 'rounded-lg w-full max-w-2xl mx-4'
@@ -483,19 +483,19 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{/* Header */}
<div
className={cn(
'flex items-center justify-between border-b border-gray-700',
'flex items-center justify-between border-b border-line',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<div className={cn(responsive && 'mt-2 md:mt-0')}>
<h2 className="text-lg font-semibold flex items-center gap-2">
<Bot size={20} className="text-primary-400" />
Agent Presets
</h2>
<p className="text-xs text-gray-500">
<p className="text-xs text-fg-subtle">
{agents.length} agents ({internalAgents.length} system, {presetAgents.length} preset, {customAgents.length} custom)
</p>
</div>
@@ -544,7 +544,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{loading ? (
<LoadingSkeleton />
) : agents.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-fg-subtle">
<Bot size={48} className="mb-4 opacity-50" />
<p className="text-center">No agents available</p>
<Button
@@ -580,7 +580,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{/* Preset Agents */}
{presetAgents.length > 0 && (
<div>
<h3 className="text-xs font-medium text-gray-400 uppercase tracking-wide mb-2 flex items-center gap-2">
<h3 className="text-xs font-medium text-fg-muted uppercase tracking-wide mb-2 flex items-center gap-2">
<Sparkles size={12} />
Preset Agents
</h3>
@@ -594,7 +594,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{/* Custom Agents */}
<div>
<h3 className="text-xs font-medium text-gray-400 uppercase tracking-wide mb-2 flex items-center gap-2">
<h3 className="text-xs font-medium text-fg-muted uppercase tracking-wide mb-2 flex items-center gap-2">
<Bot size={12} />
Custom Agents
</h3>
@@ -605,7 +605,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
))}
</div>
) : (
<div className="text-center py-6 text-gray-500 text-sm">
<div className="text-center py-6 text-fg-subtle text-sm">
<p>No custom agents yet</p>
<Button
variant="ghost"
@@ -626,12 +626,12 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{/* Footer Info */}
<div
className={cn(
'border-t border-gray-700 text-xs text-gray-500 text-center',
'border-t border-line text-xs text-fg-subtle text-center',
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
)}
>
Config stored in{' '}
<code className="font-mono bg-gray-900 px-1 rounded">.ai-assist/agents.json</code>
<code className="font-mono bg-surface-base px-1 rounded">.ai-assist/agents.json</code>
</div>
</motion.div>
</motion.div>