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
+114 -109
View File
@@ -16,6 +16,7 @@ import {
CheckpointPanel,
ProvidersPanel,
Toaster,
ThemeProvider,
listSessions,
createSession,
type Session,
@@ -94,129 +95,133 @@ export function App() {
if (isInitializing) {
return (
<div className="h-screen flex items-center justify-center bg-gray-900">
<div className="text-center">
<div className="w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-400">Initializing...</p>
<ThemeProvider>
<div className="h-screen flex items-center justify-center bg-surface-base">
<div className="text-center">
<div className="w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-fg-muted">Initializing...</p>
</div>
</div>
</div>
</ThemeProvider>
);
}
return (
<div className="h-screen flex bg-gray-900">
<Sidebar
currentSessionId={currentSessionId}
onSelectSession={handleSelectSession}
onCreateSession={handleCreateSession}
responsive
sessionTitleUpdate={sessionTitleUpdate}
/>
<ThemeProvider>
<div className="h-screen flex bg-surface-base">
<Sidebar
currentSessionId={currentSessionId}
onSelectSession={handleSelectSession}
onCreateSession={handleCreateSession}
responsive
sessionTitleUpdate={sessionTitleUpdate}
/>
{/* 主内容区域 */}
<div className="flex-1 flex min-w-0">
{/* 聊天区域 */}
<div className={`flex-1 min-w-0 ${showFileBrowser ? 'hidden md:block md:w-1/2' : 'w-full'}`}>
{currentSessionId ? (
<ChatPage
key={currentSessionId}
sessionId={currentSessionId}
onSessionNotFound={handleSessionNotFound}
onSessionUpdated={handleSessionUpdated}
responsive
showFileBrowser={showFileBrowser}
onToggleFileBrowser={() => setShowFileBrowser(!showFileBrowser)}
onOpenConfig={() => setShowConfig(true)}
onOpenCommands={() => setShowCommands(true)}
onOpenMCP={() => setShowMCP(true)}
onOpenHooks={() => setShowHooks(true)}
onOpenAgents={() => setShowAgents(true)}
onOpenCheckpoints={() => setShowCheckpoints(true)}
onOpenProviders={() => setShowProviders(true)}
/>
) : (
<div className="flex-1 flex items-center justify-center h-full">
<p className="text-gray-400">Select or create a session</p>
</div>
)}
</div>
{/* 文件浏览器 - 桌面端侧边栏,移动端全屏覆盖 */}
{showFileBrowser && (
<>
{/* 移动端: 全屏覆盖 */}
<div className="fixed inset-0 z-50 bg-gray-900 md:hidden">
<div className="flex items-center justify-between p-3 border-b border-gray-700">
<span className="text-lg font-semibold">Files</span>
<button
onClick={() => setShowFileBrowser(false)}
className="p-2 rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/* 主内容区域 */}
<div className="flex-1 flex min-w-0">
{/* 聊天区域 */}
<div className={`flex-1 min-w-0 ${showFileBrowser ? 'hidden md:block md:w-1/2' : 'w-full'}`}>
{currentSessionId ? (
<ChatPage
key={currentSessionId}
sessionId={currentSessionId}
onSessionNotFound={handleSessionNotFound}
onSessionUpdated={handleSessionUpdated}
responsive
showFileBrowser={showFileBrowser}
onToggleFileBrowser={() => setShowFileBrowser(!showFileBrowser)}
onOpenConfig={() => setShowConfig(true)}
onOpenCommands={() => setShowCommands(true)}
onOpenMCP={() => setShowMCP(true)}
onOpenHooks={() => setShowHooks(true)}
onOpenAgents={() => setShowAgents(true)}
onOpenCheckpoints={() => setShowCheckpoints(true)}
onOpenProviders={() => setShowProviders(true)}
/>
) : (
<div className="flex-1 flex items-center justify-center h-full">
<p className="text-fg-muted">Select or create a session</p>
</div>
<div className="h-[calc(100%-56px)]">
)}
</div>
{/* 文件浏览器 - 桌面端侧边栏,移动端全屏覆盖 */}
{showFileBrowser && (
<>
{/* 移动端: 全屏覆盖 */}
<div className="fixed inset-0 z-50 bg-surface-base md:hidden">
<div className="flex items-center justify-between p-3 border-b border-line">
<span className="text-lg font-semibold text-fg">Files</span>
<button
onClick={() => setShowFileBrowser(false)}
className="p-2 rounded-lg bg-surface-muted text-fg-muted hover:bg-surface-emphasis"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="h-[calc(100%-56px)]">
<FileBrowser
onFileSelect={(path, _content) => {
console.log('Selected file:', path);
}}
/>
</div>
</div>
{/* 桌面端: 侧边栏 */}
<div className="hidden md:block w-1/2 border-l border-line">
<FileBrowser
onFileSelect={(path, _content) => {
console.log('Selected file:', path);
}}
/>
</div>
</div>
</>
)}
</div>
{/* 桌面端: 侧边栏 */}
<div className="hidden md:block w-1/2 border-l border-gray-700">
<FileBrowser
onFileSelect={(path, _content) => {
console.log('Selected file:', path);
}}
/>
</div>
</>
)}
{/* 配置面板 */}
{showConfig && <ConfigPanel onClose={() => setShowConfig(false)} responsive />}
{/* 命令面板 */}
{showCommands && <CommandPanel onClose={() => setShowCommands(false)} responsive />}
{/* MCP 面板 */}
{showMCP && <MCPPanel onClose={() => setShowMCP(false)} responsive />}
{/* Hooks 面板 */}
{showHooks && <HooksPanel onClose={() => setShowHooks(false)} responsive />}
{/* Agents 面板 */}
{showAgents && <AgentsPanel onClose={() => setShowAgents(false)} responsive />}
{/* Checkpoints 面板 */}
{showCheckpoints && <CheckpointPanel onClose={() => setShowCheckpoints(false)} responsive />}
{/* Providers 面板 */}
{showProviders && <ProvidersPanel onClose={() => setShowProviders(false)} responsive />}
{/* 移动端底部文件按钮 */}
<button
onClick={() => setShowFileBrowser(true)}
className="fixed bottom-20 right-4 z-30 p-3 rounded-full bg-surface-muted text-fg-muted hover:bg-surface-emphasis active:bg-surface-emphasis shadow-lg md:hidden"
title="Browse Files"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
/>
</svg>
</button>
{/* Toast 通知 */}
<Toaster />
</div>
{/* 配置面板 */}
{showConfig && <ConfigPanel onClose={() => setShowConfig(false)} responsive />}
{/* 命令面板 */}
{showCommands && <CommandPanel onClose={() => setShowCommands(false)} responsive />}
{/* MCP 面板 */}
{showMCP && <MCPPanel onClose={() => setShowMCP(false)} responsive />}
{/* Hooks 面板 */}
{showHooks && <HooksPanel onClose={() => setShowHooks(false)} responsive />}
{/* Agents 面板 */}
{showAgents && <AgentsPanel onClose={() => setShowAgents(false)} responsive />}
{/* Checkpoints 面板 */}
{showCheckpoints && <CheckpointPanel onClose={() => setShowCheckpoints(false)} responsive />}
{/* Providers 面板 */}
{showProviders && <ProvidersPanel onClose={() => setShowProviders(false)} responsive />}
{/* 移动端底部文件按钮 */}
<button
onClick={() => setShowFileBrowser(true)}
className="fixed bottom-20 right-4 z-30 p-3 rounded-full bg-gray-700 text-gray-300 hover:bg-gray-600 active:bg-gray-500 shadow-lg md:hidden"
title="Browse Files"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
/>
</svg>
</button>
{/* Toast 通知 */}
<Toaster />
</div>
</ThemeProvider>
);
}