feat(ui): 实现深色/浅色主题切换功能
- 添加 CSS 变量定义浅色和深色主题色板 - 扩展 Tailwind 配置支持语义化颜色 (surface-*, fg-*, line-*, code) - 创建 useTheme hook 管理主题状态和持久化 - 创建 ThemeToggle 组件支持三种模式 (light/dark/system) - 迁移所有组件从硬编码 gray-* 到语义化颜色 - 支持系统主题偏好检测 (prefers-color-scheme) - 添加主题初始化脚本防止闪烁 (FOUC)
This commit is contained in:
+15
-3
@@ -1,12 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1.0, user-scalable=no" />
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="theme-color" content="#111827" />
|
||||
<meta name="theme-color" content="#111827" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="AI Assistant" />
|
||||
@@ -19,8 +20,19 @@
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<title>AI Assistant</title>
|
||||
|
||||
<!-- Theme initialization script (prevents flash) -->
|
||||
<script>
|
||||
(function() {
|
||||
const STORAGE_KEY = 'ai-assistant-theme';
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
const theme = stored === 'light' || stored === 'dark' ? stored : 'system';
|
||||
const isDark = theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-gray-900 text-gray-100">
|
||||
<body class="bg-surface-base text-fg">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
+114
-109
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,11 +97,11 @@ export function ChatPage({
|
||||
<MessageSquare size={32} className="text-primary-400" />
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-semibold mb-2 bg-gradient-to-r from-white to-gray-400 bg-clip-text text-transparent">
|
||||
<h2 className="text-2xl font-semibold mb-2 text-fg">
|
||||
Start a conversation
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-400 mb-6 max-w-md mx-auto">
|
||||
<p className="text-fg-muted mb-6 max-w-md mx-auto">
|
||||
Ask me anything about coding, debugging, or software development.
|
||||
</p>
|
||||
|
||||
@@ -112,7 +112,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => sendMessage(suggestion)}
|
||||
className="px-3 py-1.5 bg-gray-800 hover:bg-gray-700 rounded-full text-sm text-gray-300 transition-colors"
|
||||
className="px-3 py-1.5 bg-surface-subtle hover:bg-surface-muted rounded-full text-sm text-fg-secondary transition-colors"
|
||||
>
|
||||
"{suggestion}"
|
||||
</motion.button>
|
||||
@@ -148,8 +148,8 @@ export function ChatPage({
|
||||
return (
|
||||
<div className="flex-1 flex flex-col h-screen">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 md:px-6 py-3 border-b border-gray-700 bg-gray-800">
|
||||
<h1 className="text-lg font-medium">Chat</h1>
|
||||
<div className="flex items-center justify-between px-4 md:px-6 py-3 border-b border-line bg-surface-subtle">
|
||||
<h1 className="text-lg font-medium text-fg">Chat</h1>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 上下文使用情况 - 紧凑模式 */}
|
||||
{sessionId && (
|
||||
@@ -166,14 +166,14 @@ export function ChatPage({
|
||||
|
||||
{/* 工具栏按钮 */}
|
||||
{(onOpenConfig || onToggleFileBrowser || onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints || onOpenProviders) && (
|
||||
<div className="flex items-center gap-1.5 border-l border-gray-600 pl-3">
|
||||
<div className="flex items-center gap-1.5 border-l border-line-muted pl-3">
|
||||
{/* Checkpoints 按钮 */}
|
||||
{onOpenCheckpoints && (
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenCheckpoints}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Checkpoints"
|
||||
>
|
||||
<History size={20} />
|
||||
@@ -186,7 +186,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenProviders}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Model Providers"
|
||||
>
|
||||
<Server size={20} />
|
||||
@@ -199,7 +199,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenAgents}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Agent Presets"
|
||||
>
|
||||
<Bot size={20} />
|
||||
@@ -212,7 +212,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenHooks}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Hooks"
|
||||
>
|
||||
<Zap size={20} />
|
||||
@@ -225,7 +225,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenMCP}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="MCP Servers"
|
||||
>
|
||||
<Plug size={20} />
|
||||
@@ -238,7 +238,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenCommands}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Commands"
|
||||
>
|
||||
<Terminal size={20} />
|
||||
@@ -251,7 +251,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenConfig}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Settings"
|
||||
>
|
||||
<Settings size={20} />
|
||||
@@ -267,7 +267,7 @@ export function ChatPage({
|
||||
className={`hidden md:block p-1.5 rounded-lg transition-colors ${
|
||||
showFileBrowser
|
||||
? 'text-blue-400 bg-blue-500/20'
|
||||
: 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
|
||||
: 'text-fg-muted hover:text-fg-secondary hover:bg-surface-muted'
|
||||
}`}
|
||||
title={showFileBrowser ? 'Hide Files' : 'Show Files'}
|
||||
>
|
||||
|
||||
@@ -9,6 +9,26 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// 语义化颜色 (引用 CSS 变量)
|
||||
surface: {
|
||||
base: 'rgb(var(--color-bg-base) / <alpha-value>)',
|
||||
subtle: 'rgb(var(--color-bg-subtle) / <alpha-value>)',
|
||||
muted: 'rgb(var(--color-bg-muted) / <alpha-value>)',
|
||||
emphasis: 'rgb(var(--color-bg-emphasis) / <alpha-value>)',
|
||||
},
|
||||
fg: {
|
||||
DEFAULT: 'rgb(var(--color-text-primary) / <alpha-value>)',
|
||||
secondary: 'rgb(var(--color-text-secondary) / <alpha-value>)',
|
||||
muted: 'rgb(var(--color-text-muted) / <alpha-value>)',
|
||||
subtle: 'rgb(var(--color-text-subtle) / <alpha-value>)',
|
||||
},
|
||||
line: {
|
||||
DEFAULT: 'rgb(var(--color-border-default) / <alpha-value>)',
|
||||
muted: 'rgb(var(--color-border-muted) / <alpha-value>)',
|
||||
},
|
||||
// 代码块背景
|
||||
code: 'rgb(var(--color-code-bg) / <alpha-value>)',
|
||||
// 保留现有 primary 色板
|
||||
primary: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
|
||||
Reference in New Issue
Block a user