Add light/dark theme toggle with CSS custom properties

- Wire next-themes ThemeProvider in layout with class strategy, defaultTheme=dark
- Define all Gemini colors as CSS custom properties (--gem-*) in globals.css
  with light (:root) and dark (.dark) variants
- Replace all hardcoded hex colors across 7 Gemini components with var() refs
- Add Sun/Moon toggle button in GeminiTopbar (right side, before Settings)
- Theme persists to localStorage via next-themes (key: gem-theme)
- Dark theme renders identically to before (same hex values)
- Light theme: white bg, light gray surfaces, same blue-purple accent gradient

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gongzhiyong
2026-04-08 16:52:34 +08:00
co-authored by Claude Sonnet 4.6
parent 3232739a32
commit 39aad16373
9 changed files with 211 additions and 127 deletions
+96 -24
View File
@@ -3,39 +3,111 @@
@custom-variant dark (&:is(.dark *));
/* ── Light theme (default) ─────────────────────────────────────────────── */
:root {
/* Gemini Dark Theme — always dark */
--background: #131314;
--foreground: #e3e3e3;
--card: #1e1e1e;
--card-foreground: #e3e3e3;
--popover: #2a2a2a;
--popover-foreground: #e3e3e3;
--gem-bg: #ffffff;
--gem-surface: #f0f4f9;
--gem-surface-hover: #e8ecf1;
--gem-surface-2: #f5f5f5;
--gem-surface-3: #eaeaea;
--gem-border: #d9dce0;
--gem-border-hover: #c0c4c8;
--gem-text: #1f1f1f;
--gem-text-secondary: #444746;
--gem-text-muted: #5f6368;
--gem-text-placeholder: #9aa0a6;
--gem-blue: #4285f4;
--gem-blue-hover: #3b78e7;
--gem-purple: #a855f7;
--gem-mid-gradient: #7c55f0;
--gem-active-bg: #d3e3fd;
--gem-ext-connected-bg: #e6f4e6;
--gem-ext-connected-border: rgba(0, 128, 0, 0.2);
--gem-scrollbar-thumb: #c0c4c8;
--gem-scrollbar-thumb-hover: #a0a4a8;
/* shadcn vars — keep for UI lib compat */
--background: var(--gem-bg);
--foreground: var(--gem-text);
--card: var(--gem-surface);
--card-foreground: var(--gem-text);
--popover: var(--gem-surface-2);
--popover-foreground: var(--gem-text);
--primary: #4285f4;
--primary-foreground: #ffffff;
--secondary: #2a2a2a;
--secondary-foreground: #e3e3e3;
--muted: #2a2a2a;
--muted-foreground: #9aa0a6;
--accent: #2a2a2a;
--accent-foreground: #e3e3e3;
--secondary: var(--gem-surface-2);
--secondary-foreground: var(--gem-text);
--muted: var(--gem-surface-2);
--muted-foreground: var(--gem-text-muted);
--accent: var(--gem-surface-2);
--accent-foreground: var(--gem-text);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: #3a3a3a;
--input: #1e1e1e;
--border: var(--gem-border);
--input: var(--gem-surface);
--ring: #4285f4;
--radius: 0.75rem;
--sidebar: #1e1e1e;
--sidebar-foreground: #e3e3e3;
--sidebar: var(--gem-surface);
--sidebar-foreground: var(--gem-text);
--sidebar-primary: #4285f4;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #2a2a2a;
--sidebar-accent-foreground: #e3e3e3;
--sidebar-border: #3a3a3a;
--sidebar-accent: var(--gem-surface-2);
--sidebar-accent-foreground: var(--gem-text);
--sidebar-border: var(--gem-border);
--sidebar-ring: #4285f4;
/* Gemini brand gradient stops */
}
/* ── Dark theme ────────────────────────────────────────────────────────── */
.dark {
--gem-bg: #131314;
--gem-surface: #1e1e1e;
--gem-surface-hover: #252526;
--gem-surface-2: #2a2a2a;
--gem-surface-3: #333333;
--gem-border: #3a3a3a;
--gem-border-hover: #4a4a4a;
--gem-text: #e3e3e3;
--gem-text-secondary: #c4c7c5;
--gem-text-muted: #9aa0a6;
--gem-text-placeholder: #5f6368;
--gem-blue: #4285f4;
--gem-blue-hover: #3b78e7;
--gem-purple: #a855f7;
--gem-mid-gradient: #7c55f0;
--gem-active-bg: #1a2a40;
--gem-ext-connected-bg: #1a2a1a;
--gem-ext-connected-border: rgba(0, 128, 0, 0.3);
--gem-scrollbar-thumb: #3a3a3a;
--gem-scrollbar-thumb-hover: #4a4a4a;
--background: var(--gem-bg);
--foreground: var(--gem-text);
--card: var(--gem-surface);
--card-foreground: var(--gem-text);
--popover: var(--gem-surface-2);
--popover-foreground: var(--gem-text);
--primary: #4285f4;
--primary-foreground: #ffffff;
--secondary: var(--gem-surface-2);
--secondary-foreground: var(--gem-text);
--muted: var(--gem-surface-2);
--muted-foreground: var(--gem-text-muted);
--accent: var(--gem-surface-2);
--accent-foreground: var(--gem-text);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: var(--gem-border);
--input: var(--gem-surface);
--ring: #4285f4;
--radius: 0.75rem;
--sidebar: var(--gem-surface);
--sidebar-foreground: var(--gem-text);
--sidebar-primary: #4285f4;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: var(--gem-surface-2);
--sidebar-accent-foreground: var(--gem-text);
--sidebar-border: var(--gem-border);
--sidebar-ring: #4285f4;
}
@theme inline {
@@ -80,7 +152,7 @@
* {
@apply border-border outline-ring/50;
scrollbar-width: thin;
scrollbar-color: #3a3a3a transparent;
scrollbar-color: var(--gem-scrollbar-thumb) transparent;
}
body {
@apply bg-background text-foreground;
@@ -94,10 +166,10 @@
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #3a3a3a;
background: var(--gem-scrollbar-thumb);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #4a4a4a;
background: var(--gem-scrollbar-thumb-hover);
}
}
+5 -2
View File
@@ -1,6 +1,7 @@
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import { Analytics } from '@vercel/analytics/next'
import { ThemeProvider } from '@/components/theme-provider'
import './globals.css'
const _geist = Geist({ subsets: ["latin"] });
@@ -35,9 +36,11 @@ export default function RootLayout({
children: React.ReactNode
}>) {
return (
<html lang="zh-CN">
<html lang="zh-CN" suppressHydrationWarning>
<body className="font-sans antialiased">
{children}
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false} storageKey="gem-theme">
{children}
</ThemeProvider>
{process.env.NODE_ENV === 'production' && <Analytics />}
</body>
</html>
+36 -36
View File
@@ -43,11 +43,11 @@ export function ExtensionsPanel({
const handleConnect = async (ext: Extension) => {
if (!tempKey.trim()) return;
setConnecting(ext.id);
// 模拟API验证
await new Promise((r) => setTimeout(r, 1500));
onUpdateExtension(ext.id, tempKey, true);
setConnecting(null);
setEditingId(null);
@@ -79,13 +79,13 @@ export function ExtensionsPanel({
/>
{/* Panel */}
<div className="relative w-full max-w-lg bg-[#1e1e1e] border border-[#3a3a3a] rounded-2xl shadow-2xl overflow-hidden">
<div className="relative w-full max-w-lg bg-[var(--gem-surface)] border border-[var(--gem-border)] rounded-2xl shadow-2xl overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-[#3a3a3a]">
<h2 className="text-lg font-semibold text-[#e3e3e3]">扩展程序</h2>
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--gem-border)]">
<h2 className="text-lg font-semibold text-[var(--gem-text)]">扩展程序</h2>
<button
onClick={onClose}
className="p-1.5 rounded-full hover:bg-[#2a2a2a] text-[#9aa0a6] hover:text-[#e3e3e3] transition-colors"
className="p-1.5 rounded-full hover:bg-[var(--gem-surface-2)] text-[var(--gem-text-muted)] hover:text-[var(--gem-text)] transition-colors"
>
<X size={20} />
</button>
@@ -104,8 +104,8 @@ export function ExtensionsPanel({
className={cn(
"rounded-xl border transition-all duration-200",
ext.connected
? "bg-[#1a2a1a] border-green-800/50"
: "bg-[#2a2a2a] border-[#3a3a3a]"
? "bg-[var(--gem-ext-connected-bg)] border-green-800/50"
: "bg-[var(--gem-surface-2)] border-[var(--gem-border)]"
)}
>
{/* Extension header */}
@@ -113,17 +113,17 @@ export function ExtensionsPanel({
<div
className={cn(
"w-10 h-10 rounded-lg flex items-center justify-center",
ext.connected ? "bg-green-900/50" : "bg-[#3a3a3a]"
ext.connected ? "bg-green-900/50" : "bg-[var(--gem-border)]"
)}
>
<Icon
size={20}
className={ext.connected ? "text-green-400" : "text-[#9aa0a6]"}
className={ext.connected ? "text-green-400" : "text-[var(--gem-text-muted)]"}
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="font-medium text-[#e3e3e3]">{ext.name}</h3>
<h3 className="font-medium text-[var(--gem-text)]">{ext.name}</h3>
{ext.connected && (
<span className="flex items-center gap-1 text-xs text-green-400">
<Check size={12} />
@@ -131,7 +131,7 @@ export function ExtensionsPanel({
</span>
)}
</div>
<p className="text-sm text-[#9aa0a6] truncate">
<p className="text-sm text-[var(--gem-text-muted)] truncate">
{ext.description}
</p>
</div>
@@ -144,7 +144,7 @@ export function ExtensionsPanel({
"px-3 py-1.5 text-sm font-medium rounded-lg transition-colors",
ext.connected
? "text-red-400 hover:bg-red-900/30"
: "text-[#4285f4] hover:bg-[#4285f4]/10"
: "text-[var(--gem-blue)] hover:bg-[var(--gem-blue)]/10"
)}
>
{ext.connected ? "断开" : "连接"}
@@ -156,7 +156,7 @@ export function ExtensionsPanel({
{isEditing && (
<div className="px-4 pb-4 space-y-3">
<div className="space-y-1.5">
<label className="text-xs text-[#9aa0a6] flex items-center gap-1">
<label className="text-xs text-[var(--gem-text-muted)] flex items-center gap-1">
<AlertCircle size={12} />
API Key(密钥将安全存储)
</label>
@@ -166,7 +166,7 @@ export function ExtensionsPanel({
value={tempKey}
onChange={(e) => setTempKey(e.target.value)}
placeholder="请输入 API Key"
className="w-full px-3 py-2 pr-10 bg-[#1e1e1e] border border-[#3a3a3a] rounded-lg text-[#e3e3e3] placeholder:text-[#5f6368] focus:outline-none focus:border-[#4285f4] text-sm"
className="w-full px-3 py-2 pr-10 bg-[var(--gem-surface)] border border-[var(--gem-border)] rounded-lg text-[var(--gem-text)] placeholder:text-[var(--gem-text-placeholder)] focus:outline-none focus:border-[var(--gem-blue)] text-sm"
/>
<button
type="button"
@@ -176,7 +176,7 @@ export function ExtensionsPanel({
[ext.id]: !prev[ext.id],
}))
}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-[#9aa0a6] hover:text-[#e3e3e3]"
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-[var(--gem-text-muted)] hover:text-[var(--gem-text)]"
>
{showKey[ext.id] ? (
<EyeOff size={16} />
@@ -189,7 +189,7 @@ export function ExtensionsPanel({
<div className="flex gap-2">
<button
onClick={handleCancel}
className="flex-1 px-3 py-2 text-sm text-[#9aa0a6] hover:text-[#e3e3e3] hover:bg-[#3a3a3a] rounded-lg transition-colors"
className="flex-1 px-3 py-2 text-sm text-[var(--gem-text-muted)] hover:text-[var(--gem-text)] hover:bg-[var(--gem-border)] rounded-lg transition-colors"
>
取消
</button>
@@ -199,8 +199,8 @@ export function ExtensionsPanel({
className={cn(
"flex-1 px-3 py-2 text-sm font-medium rounded-lg transition-colors flex items-center justify-center gap-2",
tempKey.trim() && !isConnecting
? "bg-[#4285f4] text-white hover:bg-[#3b78e7]"
: "bg-[#3a3a3a] text-[#5f6368] cursor-not-allowed"
? "bg-[var(--gem-blue)] text-white hover:bg-[var(--gem-blue-hover)]"
: "bg-[var(--gem-border)] text-[var(--gem-text-placeholder)] cursor-not-allowed"
)}
>
{isConnecting ? (
@@ -219,14 +219,14 @@ export function ExtensionsPanel({
{/* Connected info */}
{ext.connected && !isEditing && (
<div className="px-4 pb-4">
<div className="flex items-center gap-2 text-xs text-[#9aa0a6]">
<div className="flex items-center gap-2 text-xs text-[var(--gem-text-muted)]">
<span>API Key:</span>
<code className="px-2 py-0.5 bg-[#2a2a2a] rounded text-[#9aa0a6]">
<code className="px-2 py-0.5 bg-[var(--gem-surface-2)] rounded text-[var(--gem-text-muted)]">
{ext.apiKey.slice(0, 8)}****{ext.apiKey.slice(-4)}
</code>
<button
onClick={() => handleEdit(ext)}
className="text-[#4285f4] hover:underline ml-auto"
className="text-[var(--gem-blue)] hover:underline ml-auto"
>
修改
</button>
@@ -239,8 +239,8 @@ export function ExtensionsPanel({
</div>
{/* Footer */}
<div className="px-6 py-3 border-t border-[#3a3a3a] bg-[#1a1a1a]">
<p className="text-xs text-[#5f6368] text-center">
<div className="px-6 py-3 border-t border-[var(--gem-border)] bg-[var(--gem-bg)]">
<p className="text-xs text-[var(--gem-text-placeholder)] text-center">
连接扩展后,可在对话中查看相关系统数据摘要
</p>
</div>
@@ -271,29 +271,29 @@ export function TicketSummary({ tickets }: TicketSummaryProps) {
P0: "text-red-400",
P1: "text-orange-400",
P2: "text-yellow-400",
P3: "text-[#9aa0a6]",
P3: "text-[var(--gem-text-muted)]",
};
if (tickets.length === 0) return null;
return (
<div className="mx-auto max-w-3xl px-4 mb-4">
<div className="bg-[#1e1e1e] border border-[#3a3a3a] rounded-xl overflow-hidden">
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-[#3a3a3a] bg-[#252525]">
<Ticket size={16} className="text-[#4285f4]" />
<span className="text-sm font-medium text-[#e3e3e3]">工单摘要</span>
<span className="text-xs text-[#9aa0a6]">({tickets.length} 条待关注)</span>
<div className="bg-[var(--gem-surface)] border border-[var(--gem-border)] rounded-xl overflow-hidden">
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-[var(--gem-border)] bg-[var(--gem-surface-hover)]">
<Ticket size={16} className="text-[var(--gem-blue)]" />
<span className="text-sm font-medium text-[var(--gem-text)]">工单摘要</span>
<span className="text-xs text-[var(--gem-text-muted)]">({tickets.length} 条待关注)</span>
</div>
<div className="divide-y divide-[#2a2a2a]">
<div className="divide-y divide-[var(--gem-surface-2)]">
{tickets.slice(0, 5).map((ticket) => (
<div
key={ticket.id}
className="flex items-center gap-3 px-4 py-2.5 hover:bg-[#2a2a2a] transition-colors cursor-pointer"
className="flex items-center gap-3 px-4 py-2.5 hover:bg-[var(--gem-surface-2)] transition-colors cursor-pointer"
>
<span className={cn("text-xs font-mono", priorityColors[ticket.priority])}>
{ticket.priority}
</span>
<span className="flex-1 text-sm text-[#e3e3e3] truncate">
<span className="flex-1 text-sm text-[var(--gem-text)] truncate">
{ticket.title}
</span>
<span
@@ -304,13 +304,13 @@ export function TicketSummary({ tickets }: TicketSummaryProps) {
>
{statusMap[ticket.status].label}
</span>
<span className="text-xs text-[#5f6368]">{ticket.createdAt}</span>
<span className="text-xs text-[var(--gem-text-placeholder)]">{ticket.createdAt}</span>
</div>
))}
</div>
{tickets.length > 5 && (
<div className="px-4 py-2 text-center border-t border-[#2a2a2a]">
<button className="text-xs text-[#4285f4] hover:underline">
<div className="px-4 py-2 text-center border-t border-[var(--gem-surface-2)]">
<button className="text-xs text-[var(--gem-blue)] hover:underline">
查看全部 {tickets.length} 条工单
</button>
</div>
+1 -1
View File
@@ -317,7 +317,7 @@ export function GeminiChat() {
}, []);
return (
<div className="flex h-screen bg-[#131314] overflow-hidden font-sans" role="main">
<div className="flex h-screen bg-[var(--gem-bg)] overflow-hidden font-sans" role="main">
{/* Sidebar */}
<GeminiSidebar
isOpen={sidebarOpen}
+19 -19
View File
@@ -94,10 +94,10 @@ export function GeminiInput({
};
return (
<div className="sticky bottom-0 left-0 right-0 pb-6 pt-4 bg-gradient-to-t from-[#131314] via-[#131314]/90 to-transparent pointer-events-none">
<div className="sticky bottom-0 left-0 right-0 pb-6 pt-4 bg-gradient-to-t from-[var(--gem-bg)] via-[var(--gem-bg)]/90 to-transparent pointer-events-none">
<div className="max-w-3xl mx-auto px-4 pointer-events-auto">
{/* Input container */}
<div className="bg-[#1e1e1e] border border-[#3a3a3a] rounded-3xl shadow-lg focus-within:border-[#4a4a4a] transition-colors duration-150">
<div className="bg-[var(--gem-surface)] border border-[var(--gem-border)] rounded-3xl shadow-lg focus-within:border-[var(--gem-border-hover)] transition-colors duration-150">
{/* Selected tools chips (auto show when has active tools) */}
{activeTools.size > 0 && (
<div className="flex items-center gap-2 px-4 pt-3 pb-1 flex-wrap">
@@ -108,7 +108,7 @@ export function GeminiInput({
return (
<span
key={tool.id}
className="flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-[#4285f4] text-white border border-[#4285f4]"
className="flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-[var(--gem-blue)] text-white border border-[var(--gem-blue)]"
>
<Icon size={14} />
<span>{tool.label}</span>
@@ -130,7 +130,7 @@ export function GeminiInput({
{/* Left: File upload button */}
<button
onClick={() => fileInputRef.current?.click()}
className="p-1.5 rounded-full text-[#9aa0a6] hover:text-[#e3e3e3] hover:bg-[#2a2a2a] transition-colors duration-150 flex-shrink-0 cursor-pointer mb-0.5"
className="p-1.5 rounded-full text-[var(--gem-text-muted)] hover:text-[var(--gem-text)] hover:bg-[var(--gem-surface-2)] transition-colors duration-150 flex-shrink-0 cursor-pointer mb-0.5"
aria-label="文件上传"
title="上传文件"
>
@@ -151,8 +151,8 @@ export function GeminiInput({
className={cn(
"p-1.5 rounded-full transition-colors duration-150 flex-shrink-0 cursor-pointer mb-0.5",
showTools
? "bg-[#4285f4]/20 text-[#4285f4]"
: "text-[#9aa0a6] hover:text-[#e3e3e3] hover:bg-[#2a2a2a]"
? "bg-[var(--gem-blue)]/20 text-[var(--gem-blue)]"
: "text-[var(--gem-text-muted)] hover:text-[var(--gem-text)] hover:bg-[var(--gem-surface-2)]"
)}
aria-label="工具菜单"
title="工具"
@@ -170,7 +170,7 @@ export function GeminiInput({
placeholder="阐述你的图片"
rows={1}
className={cn(
"flex-1 bg-transparent text-[#e3e3e3] text-sm placeholder:text-[#5f6368] resize-none outline-none",
"flex-1 bg-transparent text-[var(--gem-text)] text-sm placeholder:text-[var(--gem-text-placeholder)] resize-none outline-none",
"leading-relaxed py-0.5 min-h-[28px] max-h-[200px] overflow-y-auto scrollbar-thin"
)}
aria-label="消息输入"
@@ -180,7 +180,7 @@ export function GeminiInput({
{/* Right controls */}
<div className="flex items-center gap-1 flex-shrink-0 mb-0.5">
<button
className="p-1.5 rounded-full text-[#9aa0a6] hover:text-[#e3e3e3] hover:bg-[#2a2a2a] transition-colors duration-150 cursor-pointer"
className="p-1.5 rounded-full text-[var(--gem-text-muted)] hover:text-[var(--gem-text)] hover:bg-[var(--gem-surface-2)] transition-colors duration-150 cursor-pointer"
aria-label="语音输入"
>
<Mic size={20} />
@@ -192,8 +192,8 @@ export function GeminiInput({
className={cn(
"w-9 h-9 rounded-full flex items-center justify-center transition-all duration-150",
canSend
? "bg-[#4285f4] text-white hover:bg-[#3b78e7] cursor-pointer shadow-md"
: "bg-[#2a2a2a] text-[#5f6368] cursor-not-allowed"
? "bg-[var(--gem-blue)] text-white hover:bg-[var(--gem-blue-hover)] cursor-pointer shadow-md"
: "bg-[var(--gem-surface-2)] text-[var(--gem-text-placeholder)] cursor-not-allowed"
)}
>
<ArrowUp
@@ -211,8 +211,8 @@ export function GeminiInput({
className={cn(
"px-3 py-1.5 rounded-full text-xs font-medium transition-all duration-150 cursor-pointer",
selectedModel === "flash"
? "bg-[#4285f4] text-white"
: "bg-[#2a2a2a] text-[#9aa0a6] hover:bg-[#333333] hover:text-[#e3e3e3]"
? "bg-[var(--gem-blue)] text-white"
: "bg-[var(--gem-surface-2)] text-[var(--gem-text-muted)] hover:bg-[var(--gem-surface-3)] hover:text-[var(--gem-text)]"
)}
aria-pressed={selectedModel === "flash"}
>
@@ -223,8 +223,8 @@ export function GeminiInput({
className={cn(
"px-3 py-1.5 rounded-full text-xs font-medium transition-all duration-150 cursor-pointer",
selectedModel === "pro"
? "bg-[#a855f7] text-white"
: "bg-[#2a2a2a] text-[#9aa0a6] hover:bg-[#333333] hover:text-[#e3e3e3]"
? "bg-[var(--gem-purple)] text-white"
: "bg-[var(--gem-surface-2)] text-[var(--gem-text-muted)] hover:bg-[var(--gem-surface-3)] hover:text-[var(--gem-text)]"
)}
aria-pressed={selectedModel === "pro"}
>
@@ -234,7 +234,7 @@ export function GeminiInput({
{/* Tools panel (collapsible, below input) */}
{showTools && (
<div className="px-4 pb-3 pt-2 border-t border-[#2a2a2a]">
<div className="px-4 pb-3 pt-2 border-t border-[var(--gem-surface-2)]">
<div className="flex items-center gap-2 flex-wrap">
{TOOLS.map((tool) => {
const Icon = tool.icon;
@@ -246,8 +246,8 @@ export function GeminiInput({
className={cn(
"flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-150 cursor-pointer whitespace-nowrap",
isActive
? "bg-[#4285f4] text-white border border-[#4285f4]"
: "bg-[#2a2a2a] text-[#9aa0a6] border border-transparent hover:bg-[#333333] hover:text-[#e3e3e3]"
? "bg-[var(--gem-blue)] text-white border border-[var(--gem-blue)]"
: "bg-[var(--gem-surface-2)] text-[var(--gem-text-muted)] border border-transparent hover:bg-[var(--gem-surface-3)] hover:text-[var(--gem-text)]"
)}
aria-pressed={isActive}
>
@@ -262,9 +262,9 @@ export function GeminiInput({
</div>
{/* Disclaimer */}
<p className="text-center text-[11px] text-[#5f6368] mt-2.5">
<p className="text-center text-[11px] text-[var(--gem-text-placeholder)] mt-2.5">
运营大脑可能会出错,请仔细检查其回复。{" "}
<button className="underline hover:text-[#9aa0a6] transition-colors cursor-pointer">
<button className="underline hover:text-[var(--gem-text-muted)] transition-colors cursor-pointer">
你的隐私与运营大脑应用
</button>
</p>
+12 -12
View File
@@ -37,10 +37,10 @@ function renderContent(content: string) {
elements.push(
<pre
key={i}
className="bg-[#1e1e1e] border border-[#3a3a3a] rounded-xl p-4 overflow-x-auto my-3 text-sm font-mono text-[#c4c7c5]"
className="bg-[var(--gem-surface)] border border-[var(--gem-border)] rounded-xl p-4 overflow-x-auto my-3 text-sm font-mono text-[var(--gem-text-secondary)]"
>
{lang && (
<div className="text-xs text-[#9aa0a6] mb-2 uppercase tracking-wide">{lang}</div>
<div className="text-xs text-[var(--gem-text-muted)] mb-2 uppercase tracking-wide">{lang}</div>
)}
<code>{codeLines.join("\n")}</code>
</pre>
@@ -49,7 +49,7 @@ function renderContent(content: string) {
// H2
else if (line.startsWith("## ")) {
elements.push(
<h2 key={i} className="text-lg font-semibold text-[#e3e3e3] mt-4 mb-2">
<h2 key={i} className="text-lg font-semibold text-[var(--gem-text)] mt-4 mb-2">
{line.slice(3)}
</h2>
);
@@ -57,7 +57,7 @@ function renderContent(content: string) {
// H3
else if (line.startsWith("### ")) {
elements.push(
<h3 key={i} className="text-base font-semibold text-[#e3e3e3] mt-3 mb-1.5">
<h3 key={i} className="text-base font-semibold text-[var(--gem-text)] mt-3 mb-1.5">
{line.slice(4)}
</h3>
);
@@ -70,7 +70,7 @@ function renderContent(content: string) {
i++;
}
elements.push(
<ul key={i} className="list-disc list-inside space-y-1 my-2 text-[#c4c7c5] text-sm leading-relaxed">
<ul key={i} className="list-disc list-inside space-y-1 my-2 text-[var(--gem-text-secondary)] text-sm leading-relaxed">
{items.map((item, idx) => (
<li key={idx} dangerouslySetInnerHTML={{ __html: inlineFormat(item) }} />
))}
@@ -86,7 +86,7 @@ function renderContent(content: string) {
i++;
}
elements.push(
<ol key={i} className="list-decimal list-inside space-y-1 my-2 text-[#c4c7c5] text-sm leading-relaxed">
<ol key={i} className="list-decimal list-inside space-y-1 my-2 text-[var(--gem-text-secondary)] text-sm leading-relaxed">
{items.map((item, idx) => (
<li key={idx} dangerouslySetInnerHTML={{ __html: inlineFormat(item) }} />
))}
@@ -103,7 +103,7 @@ function renderContent(content: string) {
elements.push(
<p
key={i}
className="text-[#c4c7c5] text-sm leading-relaxed"
className="text-[var(--gem-text-secondary)] text-sm leading-relaxed"
dangerouslySetInnerHTML={{ __html: inlineFormat(line) }}
/>
);
@@ -116,9 +116,9 @@ function renderContent(content: string) {
function inlineFormat(text: string): string {
return text
.replace(/\*\*(.+?)\*\*/g, '<strong class="text-[#e3e3e3] font-semibold">$1</strong>')
.replace(/\*\*(.+?)\*\*/g, '<strong style="color:var(--gem-text);font-weight:600">$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`(.+?)`/g, '<code class="bg-[#2a2a2a] px-1.5 py-0.5 rounded text-xs font-mono text-[#a8c4f5]">$1</code>');
.replace(/`(.+?)`/g, '<code style="background:var(--gem-surface-2);color:#a8c4f5;padding:2px 6px;border-radius:4px;font-size:0.75rem;font-family:monospace">$1</code>');
}
const GemIcon = ({ size = 18 }: { size?: number }) => (
@@ -149,7 +149,7 @@ export function GeminiMessage({ message, onRegenerate }: GeminiMessageProps) {
if (message.role === "user") {
return (
<div className="flex justify-end mb-4" role="article" aria-label="Your message">
<div className="max-w-[70%] bg-[#2a2a2a] rounded-2xl px-5 py-3.5 text-[#e3e3e3] text-sm leading-relaxed whitespace-pre-wrap">
<div className="max-w-[70%] bg-[var(--gem-surface-2)] rounded-2xl px-5 py-3.5 text-[var(--gem-text)] text-sm leading-relaxed whitespace-pre-wrap">
{message.content}
</div>
</div>
@@ -221,8 +221,8 @@ function ActionButton({
className={cn(
"p-1.5 rounded-lg transition-colors duration-150 cursor-pointer",
active
? "text-[#4285f4] bg-[#1a2a40]"
: "text-[#9aa0a6] hover:text-[#e3e3e3] hover:bg-[#2a2a2a]"
? "text-[var(--gem-blue)] bg-[var(--gem-active-bg)]"
: "text-[var(--gem-text-muted)] hover:text-[var(--gem-text)] hover:bg-[var(--gem-surface-2)]"
)}
>
{children}
+15 -15
View File
@@ -54,7 +54,7 @@ export function GeminiSidebar({
<aside
className={cn(
"fixed left-0 top-0 h-full z-20 flex flex-col transition-all duration-300 ease-in-out",
"bg-[#1e1e1e] border-r border-[#3a3a3a]",
"bg-[var(--gem-surface)] border-r border-[var(--gem-border)]",
isOpen ? "w-[260px] opacity-100" : "w-0 opacity-0 overflow-hidden pointer-events-none"
)}
aria-label="Sidebar navigation"
@@ -62,7 +62,7 @@ export function GeminiSidebar({
{/* Logo */}
<div className="flex items-center gap-2.5 px-4 pt-5 pb-4">
<GemIcon />
<span className="text-xl font-semibold bg-gradient-to-r from-[#4285f4] to-[#a855f7] bg-clip-text text-transparent select-none">
<span className="text-xl font-semibold bg-gradient-to-r from-[var(--gem-blue)] to-[var(--gem-purple)] bg-clip-text text-transparent select-none">
运营大脑
</span>
</div>
@@ -71,17 +71,17 @@ export function GeminiSidebar({
<div className="px-3 mb-4">
<button
onClick={onNewChat}
className="w-full flex items-center gap-2.5 px-4 py-2.5 rounded-full border border-[#3a3a3a] text-[#e3e3e3] text-sm font-medium hover:bg-[#2a2a2a] transition-colors duration-150 cursor-pointer"
className="w-full flex items-center gap-2.5 px-4 py-2.5 rounded-full border border-[var(--gem-border)] text-[var(--gem-text)] text-sm font-medium hover:bg-[var(--gem-surface-2)] transition-colors duration-150 cursor-pointer"
aria-label="新建对话"
>
<PencilLine size={16} className="text-[#9aa0a6]" />
<PencilLine size={16} className="text-[var(--gem-text-muted)]" />
<span>新建对话</span>
</button>
</div>
{/* Recent conversations */}
<div className="flex-1 overflow-y-auto px-2 scrollbar-thin">
<p className="text-xs font-medium text-[#9aa0a6] px-3 mb-2 uppercase tracking-wide">
<p className="text-xs font-medium text-[var(--gem-text-muted)] px-3 mb-2 uppercase tracking-wide">
最近的对话
</p>
<nav aria-label="Recent conversations">
@@ -95,8 +95,8 @@ export function GeminiSidebar({
className={cn(
"w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm text-left transition-colors duration-150 cursor-pointer group",
activeConversationId === conv.id
? "bg-[#2a2a2a] text-[#e3e3e3]"
: "text-[#c4c7c5] hover:bg-[#2a2a2a] hover:text-[#e3e3e3]"
? "bg-[var(--gem-surface-2)] text-[var(--gem-text)]"
: "text-[var(--gem-text-secondary)] hover:bg-[var(--gem-surface-2)] hover:text-[var(--gem-text)]"
)}
aria-current={activeConversationId === conv.id ? "page" : undefined}
>
@@ -104,7 +104,7 @@ export function GeminiSidebar({
{hoveredId === conv.id && (
<MoreHorizontal
size={16}
className="flex-shrink-0 ml-1 text-[#9aa0a6] hover:text-[#e3e3e3]"
className="flex-shrink-0 ml-1 text-[var(--gem-text-muted)] hover:text-[var(--gem-text)]"
aria-label="More options"
/>
)}
@@ -116,7 +116,7 @@ export function GeminiSidebar({
</div>
{/* Bottom nav */}
<div className="border-t border-[#3a3a3a] pt-2 pb-4 px-2">
<div className="border-t border-[var(--gem-border)] pt-2 pb-4 px-2">
<nav aria-label="App navigation">
<ul className="space-y-0.5">
{[
@@ -126,12 +126,12 @@ export function GeminiSidebar({
<li key={label}>
<button
onClick={onClick}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm text-[#9aa0a6] hover:bg-[#2a2a2a] hover:text-[#e3e3e3] transition-colors duration-150 cursor-pointer"
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm text-[var(--gem-text-muted)] hover:bg-[var(--gem-surface-2)] hover:text-[var(--gem-text)] transition-colors duration-150 cursor-pointer"
>
<Icon size={18} />
<span>{label}</span>
{label === "扩展程序" && connectedExtensionsCount > 0 && (
<span className="ml-auto text-xs bg-[#4285f4] text-white px-1.5 py-0.5 rounded-full">
<span className="ml-auto text-xs bg-[var(--gem-blue)] text-white px-1.5 py-0.5 rounded-full">
{connectedExtensionsCount}
</span>
)}
@@ -142,16 +142,16 @@ export function GeminiSidebar({
</nav>
{/* User avatar */}
<div className="flex items-center gap-3 px-3 pt-3 mt-1 border-t border-[#3a3a3a]">
<div className="flex items-center gap-3 px-3 pt-3 mt-1 border-t border-[var(--gem-border)]">
<div
className="w-8 h-8 rounded-full bg-gradient-to-br from-[#4285f4] to-[#a855f7] flex items-center justify-center text-white text-sm font-semibold flex-shrink-0"
className="w-8 h-8 rounded-full bg-gradient-to-br from-[var(--gem-blue)] to-[var(--gem-purple)] flex items-center justify-center text-white text-sm font-semibold flex-shrink-0"
aria-label="用户头像"
>
U
</div>
<div className="min-w-0">
<p className="text-sm text-[#e3e3e3] font-medium truncate">用户</p>
<p className="text-xs text-[#9aa0a6] truncate">user@gmail.com</p>
<p className="text-sm text-[var(--gem-text)] font-medium truncate">用户</p>
<p className="text-xs text-[var(--gem-text-muted)] truncate">user@gmail.com</p>
</div>
</div>
</div>
+20 -11
View File
@@ -1,7 +1,8 @@
"use client";
import { useState } from "react";
import { Menu, ChevronDown, Settings } from "lucide-react";
import { Menu, ChevronDown, Settings, Sun, Moon } from "lucide-react";
import { useTheme } from "next-themes";
import { cn } from "@/lib/utils";
interface GeminiTopbarProps {
@@ -19,12 +20,13 @@ const models = [
export function GeminiTopbar({ sidebarOpen, onToggleSidebar }: GeminiTopbarProps) {
const [selectedModel, setSelectedModel] = useState("Gemini 2.0 Flash");
const [modelDropdownOpen, setModelDropdownOpen] = useState(false);
const { theme, setTheme } = useTheme();
return (
<header
className={cn(
"fixed top-0 right-0 z-10 flex items-center justify-between px-4 h-14",
"bg-[#131314]/80 backdrop-blur-md border-b border-[#3a3a3a]/50",
"bg-[var(--gem-bg)]/80 backdrop-blur-md border-b border-[var(--gem-border)]/50",
"transition-all duration-300",
sidebarOpen ? "left-[260px]" : "left-0"
)}
@@ -33,7 +35,7 @@ export function GeminiTopbar({ sidebarOpen, onToggleSidebar }: GeminiTopbarProps
{/* Left: hamburger */}
<button
onClick={onToggleSidebar}
className="p-2 rounded-full hover:bg-[#2a2a2a] text-[#9aa0a6] hover:text-[#e3e3e3] transition-colors duration-150 cursor-pointer"
className="p-2 rounded-full hover:bg-[var(--gem-surface-2)] text-[var(--gem-text-muted)] hover:text-[var(--gem-text)] transition-colors duration-150 cursor-pointer"
aria-label={sidebarOpen ? "关闭侧边栏" : "打开侧边栏"}
aria-expanded={sidebarOpen}
>
@@ -44,7 +46,7 @@ export function GeminiTopbar({ sidebarOpen, onToggleSidebar }: GeminiTopbarProps
<div className="relative">
<button
onClick={() => setModelDropdownOpen((prev) => !prev)}
className="flex items-center gap-1.5 px-4 py-1.5 rounded-full border border-[#3a3a3a] text-[#e3e3e3] text-sm font-medium hover:bg-[#2a2a2a] transition-colors duration-150 cursor-pointer"
className="flex items-center gap-1.5 px-4 py-1.5 rounded-full border border-[var(--gem-border)] text-[var(--gem-text)] text-sm font-medium hover:bg-[var(--gem-surface-2)] transition-colors duration-150 cursor-pointer"
aria-haspopup="listbox"
aria-expanded={modelDropdownOpen}
aria-label="选择模型"
@@ -53,7 +55,7 @@ export function GeminiTopbar({ sidebarOpen, onToggleSidebar }: GeminiTopbarProps
<ChevronDown
size={14}
className={cn(
"text-[#9aa0a6] transition-transform duration-200",
"text-[var(--gem-text-muted)] transition-transform duration-200",
modelDropdownOpen && "rotate-180"
)}
/>
@@ -69,7 +71,7 @@ export function GeminiTopbar({ sidebarOpen, onToggleSidebar }: GeminiTopbarProps
/>
<ul
role="listbox"
className="absolute top-full left-1/2 -translate-x-1/2 mt-2 w-52 bg-[#2a2a2a] border border-[#3a3a3a] rounded-2xl shadow-2xl overflow-hidden z-20 py-1.5"
className="absolute top-full left-1/2 -translate-x-1/2 mt-2 w-52 bg-[var(--gem-surface-2)] border border-[var(--gem-border)] rounded-2xl shadow-2xl overflow-hidden z-20 py-1.5"
aria-label="可用的模型"
>
{models.map((model) => (
@@ -82,8 +84,8 @@ export function GeminiTopbar({ sidebarOpen, onToggleSidebar }: GeminiTopbarProps
className={cn(
"w-full text-left px-4 py-2.5 text-sm transition-colors duration-150 cursor-pointer",
selectedModel === model
? "text-[#4285f4] bg-[#1e1e1e]"
: "text-[#e3e3e3] hover:bg-[#333333]"
? "text-[var(--gem-blue)] bg-[var(--gem-surface)]"
: "text-[var(--gem-text)] hover:bg-[var(--gem-surface-3)]"
)}
>
{model}
@@ -95,16 +97,23 @@ export function GeminiTopbar({ sidebarOpen, onToggleSidebar }: GeminiTopbarProps
)}
</div>
{/* Right: settings + avatar */}
{/* Right: theme toggle + settings + avatar */}
<div className="flex items-center gap-1">
<button
className="p-2 rounded-full hover:bg-[#2a2a2a] text-[#9aa0a6] hover:text-[#e3e3e3] transition-colors duration-150 cursor-pointer"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="p-2 rounded-full hover:bg-[var(--gem-surface-2)] text-[var(--gem-text-muted)] hover:text-[var(--gem-text)] transition-colors duration-150 cursor-pointer"
aria-label={theme === "dark" ? "切换亮色主题" : "切换暗色主题"}
>
{theme === "dark" ? <Sun size={20} /> : <Moon size={20} />}
</button>
<button
className="p-2 rounded-full hover:bg-[var(--gem-surface-2)] text-[var(--gem-text-muted)] hover:text-[var(--gem-text)] transition-colors duration-150 cursor-pointer"
aria-label="设置"
>
<Settings size={20} />
</button>
<button
className="w-8 h-8 rounded-full bg-gradient-to-br from-[#4285f4] to-[#a855f7] flex items-center justify-center text-white text-sm font-semibold ml-1 cursor-pointer hover:opacity-90 transition-opacity"
className="w-8 h-8 rounded-full bg-gradient-to-br from-[var(--gem-blue)] to-[var(--gem-purple)] flex items-center justify-center text-white text-sm font-semibold ml-1 cursor-pointer hover:opacity-90 transition-opacity"
aria-label="用户资料"
>
U
+7 -7
View File
@@ -44,12 +44,12 @@ export function GeminiWelcome({ onSuggestionClick }: GeminiWelcomeProps) {
{/* Greeting */}
<div className="text-center mb-10">
<h1 className="text-4xl font-semibold mb-3 leading-tight text-balance">
<span className="bg-gradient-to-r from-[#4285f4] via-[#7c55f0] to-[#a855f7] bg-clip-text text-transparent">
<span className="bg-gradient-to-r from-[var(--gem-blue)] via-[var(--gem-mid-gradient)] to-[var(--gem-purple)] bg-clip-text text-transparent">
{greeting},
</span>{" "}
<span className="text-[#e3e3e3]">用户</span>
<span className="text-[var(--gem-text)]">用户</span>
</h1>
<p className="text-[#9aa0a6] text-xl">我今天可以帮你做什么运营工作?</p>
<p className="text-[var(--gem-text-muted)] text-xl">我今天可以帮你做什么运营工作?</p>
</div>
{/* Suggestion cards */}
@@ -58,13 +58,13 @@ export function GeminiWelcome({ onSuggestionClick }: GeminiWelcomeProps) {
<button
key={short}
onClick={() => onSuggestionClick(text)}
className="flex flex-col gap-2 p-4 rounded-2xl border border-[#3a3a3a] bg-[#1e1e1e] hover:bg-[#252526] hover:border-[#4a4a4a] transition-all duration-150 text-left cursor-pointer group"
className="flex flex-col gap-2 p-4 rounded-2xl border border-[var(--gem-border)] bg-[var(--gem-surface)] hover:bg-[var(--gem-surface-hover)] hover:border-[var(--gem-border-hover)] transition-all duration-150 text-left cursor-pointer group"
aria-label={`Suggestion: ${text}`}
>
<div className="w-8 h-8 rounded-full bg-[#2a2a2a] flex items-center justify-center group-hover:bg-[#333333] transition-colors">
<Icon size={16} className="text-[#9aa0a6]" />
<div className="w-8 h-8 rounded-full bg-[var(--gem-surface-2)] flex items-center justify-center group-hover:bg-[var(--gem-surface-3)] transition-colors">
<Icon size={16} className="text-[var(--gem-text-muted)]" />
</div>
<span className="text-[#c4c7c5] text-sm leading-snug line-clamp-2">{text}</span>
<span className="text-[var(--gem-text-secondary)] text-sm leading-snug line-clamp-2">{text}</span>
</button>
))}
</div>