From 39aad16373553db02a399c969fd2bee18774c7a9 Mon Sep 17 00:00:00 2001 From: gongzhiyong Date: Wed, 8 Apr 2026 16:52:34 +0800 Subject: [PATCH] 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) --- frontend/app/globals.css | 120 ++++++++++++++---- frontend/app/layout.tsx | 7 +- .../components/gemini/ExtensionsPanel.tsx | 72 +++++------ frontend/components/gemini/GeminiChat.tsx | 2 +- frontend/components/gemini/GeminiInput.tsx | 38 +++--- frontend/components/gemini/GeminiMessage.tsx | 24 ++-- frontend/components/gemini/GeminiSidebar.tsx | 30 ++--- frontend/components/gemini/GeminiTopbar.tsx | 31 +++-- frontend/components/gemini/GeminiWelcome.tsx | 14 +- 9 files changed, 211 insertions(+), 127 deletions(-) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 70ce64f..e87db13 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -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); } } diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 43c8721..23feaec 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -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 ( - + - {children} + + {children} + {process.env.NODE_ENV === 'production' && } diff --git a/frontend/components/gemini/ExtensionsPanel.tsx b/frontend/components/gemini/ExtensionsPanel.tsx index feaea9e..3f0d62d 100644 --- a/frontend/components/gemini/ExtensionsPanel.tsx +++ b/frontend/components/gemini/ExtensionsPanel.tsx @@ -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 */} -
+
{/* Header */} -
-

扩展程序

+
+

扩展程序

@@ -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({
-

{ext.name}

+

{ext.name}

{ext.connected && ( @@ -131,7 +131,7 @@ export function ExtensionsPanel({ )}
-

+

{ext.description}

@@ -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 && (
-