feat(ui): 实现深色/浅色主题切换功能
- 添加 CSS 变量定义浅色和深色主题色板 - 扩展 Tailwind 配置支持语义化颜色 (surface-*, fg-*, line-*, code) - 创建 useTheme hook 管理主题状态和持久化 - 创建 ThemeToggle 组件支持三种模式 (light/dark/system) - 迁移所有组件从硬编码 gray-* 到语义化颜色 - 支持系统主题偏好检测 (prefers-color-scheme) - 添加主题初始化脚本防止闪烁 (FOUC)
This commit is contained in:
@@ -217,7 +217,7 @@ export function ProviderEditor({
|
||||
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-lg md:mx-4 rounded-t-2xl md:rounded-lg'
|
||||
: 'rounded-lg w-full max-w-lg mx-4'
|
||||
@@ -226,12 +226,12 @@ export function ProviderEditor({
|
||||
{/* 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">
|
||||
@@ -239,7 +239,7 @@ export function ProviderEditor({
|
||||
{loading ? 'Loading...' : provider?.name || providerId}
|
||||
</h2>
|
||||
{provider && (
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-xs text-fg-subtle">
|
||||
{provider.builtin ? 'Built-in Provider' : 'Custom Provider'}
|
||||
</p>
|
||||
)}
|
||||
@@ -283,17 +283,17 @@ export function ProviderEditor({
|
||||
)}
|
||||
|
||||
{/* Enabled Toggle */}
|
||||
<div className="flex items-center justify-between p-3 bg-gray-900/50 rounded-lg">
|
||||
<div className="flex items-center justify-between p-3 bg-surface-base/50 rounded-lg">
|
||||
<div>
|
||||
<span className="text-sm font-medium">Enabled</span>
|
||||
<p className="text-xs text-gray-500">Use this provider for model selection</p>
|
||||
<p className="text-xs text-fg-subtle">Use this provider for model selection</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEnabled(!enabled)}
|
||||
className={cn(
|
||||
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
||||
enabled ? 'bg-primary-500' : 'bg-gray-600'
|
||||
enabled ? 'bg-primary-500' : 'bg-surface-emphasis'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
@@ -307,14 +307,14 @@ export function ProviderEditor({
|
||||
|
||||
{/* API Key Section */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2">
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2">
|
||||
<Key size={14} />
|
||||
API Key Configuration
|
||||
</h3>
|
||||
|
||||
{/* API Key Input */}
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">API Key</label>
|
||||
<label className="block text-xs text-fg-muted mb-1">API Key</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showApiKey ? 'text' : 'password'}
|
||||
@@ -326,7 +326,7 @@ export function ProviderEditor({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowApiKey(!showApiKey)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-300"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-fg-muted hover:text-fg-secondary"
|
||||
>
|
||||
{showApiKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
@@ -338,7 +338,7 @@ export function ProviderEditor({
|
||||
|
||||
{/* API Key Env Var */}
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">
|
||||
<label className="block text-xs text-fg-muted mb-1">
|
||||
Environment Variable (alternative)
|
||||
</label>
|
||||
<Input
|
||||
@@ -346,7 +346,7 @@ export function ProviderEditor({
|
||||
onChange={(e) => setApiKeyEnvVar(e.target.value)}
|
||||
placeholder={provider?.apiKeyEnvVar || 'PROVIDER_API_KEY'}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
<p className="text-xs text-fg-subtle mt-1">
|
||||
If no API key is set, this env var will be used
|
||||
</p>
|
||||
</div>
|
||||
@@ -354,20 +354,20 @@ export function ProviderEditor({
|
||||
|
||||
{/* Base URL Section */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2">
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2">
|
||||
<Globe size={14} />
|
||||
Endpoint Configuration
|
||||
</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Base URL</label>
|
||||
<label className="block text-xs text-fg-muted mb-1">Base URL</label>
|
||||
<Input
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
placeholder={provider?.baseUrl || 'https://api.provider.com/v1'}
|
||||
/>
|
||||
{provider?.builtin && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
<p className="text-xs text-fg-subtle mt-1">
|
||||
Leave empty to use default endpoint
|
||||
</p>
|
||||
)}
|
||||
@@ -378,7 +378,7 @@ export function ProviderEditor({
|
||||
{provider?.allowCustomModels && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2">
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2">
|
||||
<Cpu size={14} />
|
||||
Custom Models ({provider.config.customModels.length})
|
||||
</h3>
|
||||
@@ -402,10 +402,10 @@ export function ProviderEditor({
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="p-3 bg-gray-900/50 rounded-lg space-y-3 border border-gray-700">
|
||||
<div className="p-3 bg-surface-base/50 rounded-lg space-y-3 border border-line">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Model ID</label>
|
||||
<label className="block text-xs text-fg-muted mb-1">Model ID</label>
|
||||
<Input
|
||||
value={newModelId}
|
||||
onChange={(e) => setNewModelId(e.target.value)}
|
||||
@@ -414,7 +414,7 @@ export function ProviderEditor({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Display Name</label>
|
||||
<label className="block text-xs text-fg-muted mb-1">Display Name</label>
|
||||
<Input
|
||||
value={newModelName}
|
||||
onChange={(e) => setNewModelName(e.target.value)}
|
||||
@@ -424,21 +424,21 @@ export function ProviderEditor({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-2 text-sm text-gray-400 cursor-pointer">
|
||||
<label className="flex items-center gap-2 text-sm text-fg-muted cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newModelVision}
|
||||
onChange={(e) => setNewModelVision(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-primary-500"
|
||||
className="w-4 h-4 rounded border-line-muted bg-surface-base text-primary-500"
|
||||
/>
|
||||
Vision
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-400 cursor-pointer">
|
||||
<label className="flex items-center gap-2 text-sm text-fg-muted cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newModelTools}
|
||||
onChange={(e) => setNewModelTools(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-primary-500"
|
||||
className="w-4 h-4 rounded border-line-muted bg-surface-base text-primary-500"
|
||||
/>
|
||||
Function Calling
|
||||
</label>
|
||||
@@ -470,17 +470,17 @@ export function ProviderEditor({
|
||||
{provider.config.customModels.map((model) => (
|
||||
<div
|
||||
key={model.id}
|
||||
className="flex items-center justify-between p-2 bg-gray-900/50 rounded-lg text-sm"
|
||||
className="flex items-center justify-between p-2 bg-surface-base/50 rounded-lg text-sm"
|
||||
>
|
||||
<div>
|
||||
<span className="text-gray-200">{model.name}</span>
|
||||
<span className="text-gray-500 ml-2">({model.id})</span>
|
||||
<span className="text-fg-secondary">{model.name}</span>
|
||||
<span className="text-fg-subtle ml-2">({model.id})</span>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
{model.capabilities?.vision && (
|
||||
<span className="text-[10px] text-gray-500">Vision</span>
|
||||
<span className="text-[10px] text-fg-subtle">Vision</span>
|
||||
)}
|
||||
{model.capabilities?.functionCalling && (
|
||||
<span className="text-[10px] text-gray-500">Tools</span>
|
||||
<span className="text-[10px] text-fg-subtle">Tools</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -496,7 +496,7 @@ export function ProviderEditor({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500 text-center py-2">
|
||||
<p className="text-xs text-fg-subtle text-center py-2">
|
||||
No custom models added yet
|
||||
</p>
|
||||
)}
|
||||
@@ -505,16 +505,16 @@ export function ProviderEditor({
|
||||
|
||||
{/* Built-in Models Info */}
|
||||
{provider && (
|
||||
<div className="text-xs text-gray-500 p-3 bg-gray-900/30 rounded-lg">
|
||||
<div className="text-xs text-fg-subtle p-3 bg-surface-base/30 rounded-lg">
|
||||
<p className="font-medium mb-1">Built-in Models:</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{provider.models.slice(0, 5).map((m) => (
|
||||
<span key={m.id} className="px-1.5 py-0.5 bg-gray-800 rounded">
|
||||
<span key={m.id} className="px-1.5 py-0.5 bg-surface-subtle rounded">
|
||||
{m.name}
|
||||
</span>
|
||||
))}
|
||||
{provider.models.length > 5 && (
|
||||
<span className="px-1.5 py-0.5 text-gray-400">
|
||||
<span className="px-1.5 py-0.5 text-fg-muted">
|
||||
+{provider.models.length - 5} more
|
||||
</span>
|
||||
)}
|
||||
@@ -528,7 +528,7 @@ export function ProviderEditor({
|
||||
{/* Footer */}
|
||||
<div
|
||||
className={cn(
|
||||
'border-t border-gray-700 flex justify-end gap-2',
|
||||
'border-t border-line flex justify-end gap-2',
|
||||
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user