diff --git a/ui/src/components/ChatInterface.tsx b/ui/src/components/ChatInterface.tsx index d0c75365eb348cccf70e1e3209cdf192cc30936e..ee4b7263eadbca5c98da960469ebc4e83147222c 100644 --- a/ui/src/components/ChatInterface.tsx +++ b/ui/src/components/ChatInterface.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useRef } from "react"; import { Message, Conversation, StreamResponse, LLMContent } from "../types"; import { api } from "../services/api"; +import { ThemeMode, getStoredTheme, setStoredTheme, applyTheme } from "../services/theme"; import MessageComponent from "./Message"; import MessageInput from "./MessageInput"; import DiffViewer from "./DiffViewer"; @@ -428,6 +429,7 @@ function ChatInterface({ const [showDirectoryPicker, setShowDirectoryPicker] = useState(false); // Settings modal removed - configuration moved to status bar for empty conversations const [showOverflowMenu, setShowOverflowMenu] = useState(false); + const [themeMode, setThemeMode] = useState(getStoredTheme); const [showDiffViewer, setShowDiffViewer] = useState(false); const [diffViewerInitialCommit, setDiffViewerInitialCommit] = useState( undefined, @@ -1091,6 +1093,65 @@ function ChatInterface({ {link.title} ))} + + {/* Theme selector */} +
+
+ + + +
)} diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 0a063d242343951c7a5262768221b3675d37ec47..7f2ae20b5cc530b00009a0322ae19f718c57f951 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -1,6 +1,10 @@ import React from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; +import { initializeTheme } from "./services/theme"; + +// Apply theme before render to avoid flash +initializeTheme(); // Render main app const rootContainer = document.getElementById("root"); diff --git a/ui/src/services/theme.ts b/ui/src/services/theme.ts new file mode 100644 index 0000000000000000000000000000000000000000..95cedafdf965189aff9203ea313777d3745caa87 --- /dev/null +++ b/ui/src/services/theme.ts @@ -0,0 +1,38 @@ +export type ThemeMode = "system" | "light" | "dark"; + +const STORAGE_KEY = "shelley-theme"; + +export function getStoredTheme(): ThemeMode { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored === "light" || stored === "dark" || stored === "system") { + return stored; + } + return "system"; +} + +export function setStoredTheme(theme: ThemeMode): void { + localStorage.setItem(STORAGE_KEY, theme); +} + +export function getSystemPrefersDark(): boolean { + return window.matchMedia("(prefers-color-scheme: dark)").matches; +} + +export function applyTheme(theme: ThemeMode): void { + const isDark = theme === "dark" || (theme === "system" && getSystemPrefersDark()); + document.documentElement.classList.toggle("dark", isDark); +} + +// Initialize theme on load +export function initializeTheme(): void { + const theme = getStoredTheme(); + applyTheme(theme); + + // Listen for system preference changes + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { + const currentTheme = getStoredTheme(); + if (currentTheme === "system") { + applyTheme("system"); + } + }); +} diff --git a/ui/src/styles.css b/ui/src/styles.css index 1f038c4dc7058c0eb2f9ef2036b9eae6be25daac..22cb0aeea3783fe1521d9548908afb3d3548ee71 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -18,6 +18,8 @@ body { line-height: 1.6; -webkit-font-smoothing: antialiased; letter-spacing: -0.01em; + color: var(--text-primary); + background: var(--bg-secondary); } /* CSS Variables */ @@ -42,6 +44,8 @@ body { --blue-bg: #eff6ff; --blue-border: #bfdbfe; --blue-text: #1e40af; + --user-message-text: #2563eb; + --link-color: #2563eb; --green-600: #16a34a; --green-700: #15803d; --gray-50: #f9fafb; @@ -73,6 +77,8 @@ body { --blue-bg: rgba(59, 130, 246, 0.1); --blue-border: rgba(59, 130, 246, 0.3); --blue-text: #93c5fd; + --user-message-text: #60a5fa; + --link-color: #60a5fa; } /* Layout */ @@ -422,6 +428,7 @@ button { max-width: calc(100vw - 3rem); /* Ensure drawer doesn't fill entire screen on mobile */ background: var(--bg-base); border-right: 1px solid var(--border); + color: var(--text-primary); display: flex; flex-direction: column; height: 100%; @@ -569,13 +576,14 @@ button { .message-user .message-content { margin-left: auto; max-width: 80%; - color: var(--primary); + color: var(--user-message-text); } .message-agent .message-content, .message-tool .message-content { margin-right: auto; max-width: 100%; + color: var(--text-primary); } .thinking-indicator { @@ -1990,6 +1998,54 @@ svg { border-bottom-right-radius: 0.5rem; } +.overflow-menu-divider { + height: 1px; + background: var(--border); + margin: 0.5rem 0; +} + +.theme-toggle-row { + display: flex; + gap: 0.25rem; + padding: 0.5rem; +} + +.theme-toggle-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + background: transparent; + border: 1px solid var(--border); + border-radius: 0.375rem; + color: var(--text-secondary); + cursor: pointer; + transition: all 0.15s; +} + +.theme-toggle-btn svg { + width: 1.125rem; + height: 1.125rem; +} + +.theme-toggle-btn:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.theme-toggle-btn-selected { + background: var(--primary); + border-color: var(--primary); + color: white; +} + +.theme-toggle-btn-selected:hover { + background: var(--primary-dark); + border-color: var(--primary-dark); + color: white; +} + /* Diff display styling */ .diff-display { font-family: var(--font-mono);