diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index ff0c3a792c..e102e464b2 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -4,8 +4,9 @@ import { useState, useEffect } from 'react' import { useApp } from './hooks/useApp' import { SessionList, ChatView, MessageInput, StatusBar, Settings } from './components' +import { ThemeProvider } from './contexts/ThemeContext' -function App(): React.JSX.Element { +function AppContent(): React.JSX.Element { const { // State sessions, @@ -188,4 +189,12 @@ function App(): React.JSX.Element { ) } +function App(): React.JSX.Element { + return ( + + + + ) +} + export default App diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index c567b93a63..0729ff0b6f 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -4,6 +4,7 @@ */ import { useState, useEffect } from 'react' import type { AgentCheckResult } from '../../../shared/electron-api' +import { useTheme } from '../contexts/ThemeContext' interface SettingsProps { isOpen: boolean @@ -20,11 +21,14 @@ const AGENT_ICONS: Record = { claude: '◉', } +type ThemeMode = 'light' | 'dark' | 'system' + export function Settings({ isOpen, onClose, currentAgentId, onSwitchAgent }: SettingsProps) { const [agents, setAgents] = useState([]) const [loading, setLoading] = useState(true) const [switching, setSwitching] = useState(null) const [selectedAgent, setSelectedAgent] = useState(null) + const { mode, setMode } = useTheme() useEffect(() => { if (isOpen) { @@ -79,68 +83,138 @@ export function Settings({ isOpen, onClose, currentAgentId, onSwitchAgent }: Set return ( - + {/* Header */} - - Set up agents - - Select a coding agent to use. Agents use local authentication. - + + Settings - {/* Agent Cards */} - {loading ? ( - - - - ) : ( - - {agents.map((agent) => ( - agent.installed && setSelectedAgent(agent.id)} - /> - ))} - - )} + {/* Appearance Section */} + + Appearance + + + + {/* Agent Section */} + + Coding Agent + + Select a coding agent. Agents use local authentication. + + + {/* Agent Cards */} + {loading ? ( + + + + ) : ( + + {agents.map((agent) => ( + agent.installed && setSelectedAgent(agent.id)} + /> + ))} + + )} + {/* Footer */} - + - Refresh + Refresh agents - - {switching ? ( - <> - - Switching... - > - ) : ( - <> - Continue - - - - > - )} - + + + Cancel + + + {switching ? ( + <> + + Switching... + > + ) : ( + 'Done' + )} + + ) } +interface ThemeSelectorProps { + value: ThemeMode + onChange: (mode: ThemeMode) => void +} + +function ThemeSelector({ value, onChange }: ThemeSelectorProps) { + const options: { mode: ThemeMode; label: string; icon: React.ReactNode }[] = [ + { + mode: 'light', + label: 'Light', + icon: ( + + + + ), + }, + { + mode: 'dark', + label: 'Dark', + icon: ( + + + + ), + }, + { + mode: 'system', + label: 'System', + icon: ( + + + + ), + }, + ] + + return ( + + {options.map(({ mode, label, icon }) => ( + onChange(mode)} + className={`flex flex-1 items-center justify-center gap-2 rounded-lg border px-3 py-2.5 text-sm transition-all ${ + value === mode + ? 'border-[var(--color-accent)] bg-[var(--color-accent-muted)] text-[var(--color-text)]' + : 'border-[var(--color-border)] text-[var(--color-text-muted)] hover:border-[var(--color-text-muted)]' + }`} + > + {icon} + {label} + + ))} + + ) +} + interface AgentCardProps { agent: AgentCheckResult isSelected: boolean diff --git a/src/renderer/src/contexts/ThemeContext.tsx b/src/renderer/src/contexts/ThemeContext.tsx new file mode 100644 index 0000000000..ff2e8429ef --- /dev/null +++ b/src/renderer/src/contexts/ThemeContext.tsx @@ -0,0 +1,73 @@ +/** + * Theme context for managing light/dark/system mode + */ +import { createContext, useContext, useEffect, useState, type ReactNode } from 'react' + +type ThemeMode = 'light' | 'dark' | 'system' + +interface ThemeContextValue { + mode: ThemeMode + setMode: (mode: ThemeMode) => void + resolvedTheme: 'light' | 'dark' +} + +const ThemeContext = createContext(null) + +const STORAGE_KEY = 'multica-theme' + +export function ThemeProvider({ children }: { children: ReactNode }) { + const [mode, setModeState] = useState(() => { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored === 'light' || stored === 'dark' || stored === 'system') { + return stored + } + } + return 'system' + }) + + const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>(() => { + if (typeof window !== 'undefined') { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' + } + return 'dark' + }) + + // Listen for system theme changes + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + const handler = (e: MediaQueryListEvent) => { + setSystemTheme(e.matches ? 'dark' : 'light') + } + mediaQuery.addEventListener('change', handler) + return () => mediaQuery.removeEventListener('change', handler) + }, []) + + const resolvedTheme = mode === 'system' ? systemTheme : mode + + // Apply theme to document + useEffect(() => { + const root = document.documentElement + root.classList.remove('light', 'dark') + root.classList.add(resolvedTheme) + }, [resolvedTheme]) + + const setMode = (newMode: ThemeMode) => { + setModeState(newMode) + localStorage.setItem(STORAGE_KEY, newMode) + } + + return ( + + {children} + + ) +} + +export function useTheme() { + const context = useContext(ThemeContext) + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider') + } + return context +} diff --git a/src/renderer/src/styles/index.css b/src/renderer/src/styles/index.css index 185c03d1f3..223da8e307 100644 --- a/src/renderer/src/styles/index.css +++ b/src/renderer/src/styles/index.css @@ -1,7 +1,8 @@ @import 'tailwindcss'; -/* Custom theme configuration */ -@theme { +/* Dark theme (default) */ +:root, +.dark { --color-primary: #292929; --color-primary-dark: #363636; --color-primary-text: #ffffff; @@ -15,13 +16,30 @@ --color-text-muted: #a1a1aa; } +/* Light theme */ +.light { + --color-primary: #f5f5f5; + --color-primary-dark: #e5e5e5; + --color-primary-text: #171717; + --color-accent: #16a34a; + --color-accent-muted: rgba(22, 163, 74, 0.1); + --color-background: #ffffff; + --color-surface: #f5f5f5; + --color-surface-hover: #e5e5e5; + --color-border: #e5e5e5; + --color-text: #171717; + --color-text-muted: #525252; +} + /* Base styles */ * { box-sizing: border-box; } body { - @apply bg-[var(--color-background)] text-[var(--color-text)] antialiased; + background-color: var(--color-background); + color: var(--color-text); + -webkit-font-smoothing: antialiased; font-family: -apple-system, BlinkMacSystemFont, @@ -52,9 +70,10 @@ body { } ::-webkit-scrollbar-thumb { - @apply bg-[var(--color-border)] rounded-full; + background-color: var(--color-border); + border-radius: 9999px; } ::-webkit-scrollbar-thumb:hover { - @apply bg-[var(--color-text-muted)]; + background-color: var(--color-text-muted); }
- Select a coding agent to use. Agents use local authentication. -
+ Select a coding agent. Agents use local authentication. +