diff --git a/src/App.tsx b/src/App.tsx index 083fa31..cde4a6a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { Plus, Loader2, Bot, FolderCode } from "lucide-react"; import { api, type Project, type Session, type ClaudeMdFile } from "@/lib/api"; import { OutputCacheProvider } from "@/lib/outputCache"; import { TabProvider } from "@/contexts/TabContext"; +import { ThemeProvider } from "@/contexts/ThemeContext"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { ProjectList } from "@/components/ProjectList"; @@ -508,11 +509,13 @@ function AppContent() { */ function App() { return ( - - - - - + + + + + + + ); } diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 10e677c..5e0c162 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -14,6 +14,7 @@ import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Card } from "@/components/ui/card"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { api, type ClaudeSettings, @@ -25,6 +26,7 @@ import { ClaudeVersionSelector } from "./ClaudeVersionSelector"; import { StorageTab } from "./StorageTab"; import { HooksEditor } from "./HooksEditor"; import { SlashCommandsManager } from "./SlashCommandsManager"; +import { useTheme } from "@/hooks"; interface SettingsProps { /** @@ -77,6 +79,9 @@ export const Settings: React.FC = ({ const [userHooksChanged, setUserHooksChanged] = useState(false); const getUserHooks = React.useRef<(() => any) | null>(null); + // Theme hook + const { theme, setTheme, customColors, setCustomColors } = useTheme(); + // Load settings on mount useEffect(() => { loadSettings(); @@ -375,6 +380,155 @@ export const Settings: React.FC = ({

General Settings

+ {/* Theme Selector */} +
+ + +

+ Choose your preferred color theme for the interface +

+
+ + {/* Custom Color Editor */} + {theme === 'custom' && ( +
+

Custom Theme Colors

+ +
+ {/* Background Color */} +
+ +
+ setCustomColors({ background: e.target.value })} + placeholder="oklch(0.12 0.01 240)" + className="font-mono text-xs" + /> +
+
+
+ + {/* Foreground Color */} +
+ +
+ setCustomColors({ foreground: e.target.value })} + placeholder="oklch(0.98 0.01 240)" + className="font-mono text-xs" + /> +
+
+
+ + {/* Primary Color */} +
+ +
+ setCustomColors({ primary: e.target.value })} + placeholder="oklch(0.98 0.01 240)" + className="font-mono text-xs" + /> +
+
+
+ + {/* Card Color */} +
+ +
+ setCustomColors({ card: e.target.value })} + placeholder="oklch(0.14 0.01 240)" + className="font-mono text-xs" + /> +
+
+
+ + {/* Accent Color */} +
+ +
+ setCustomColors({ accent: e.target.value })} + placeholder="oklch(0.16 0.01 240)" + className="font-mono text-xs" + /> +
+
+
+ + {/* Destructive Color */} +
+ +
+ setCustomColors({ destructive: e.target.value })} + placeholder="oklch(0.6 0.2 25)" + className="font-mono text-xs" + /> +
+
+
+
+ +

+ Use CSS color values (hex, rgb, oklch, etc.). Changes apply immediately. +

+
+ )} + {/* Include Co-authored By */}
diff --git a/src/components/StreamMessage.tsx b/src/components/StreamMessage.tsx index decf67c..ae43a59 100644 --- a/src/components/StreamMessage.tsx +++ b/src/components/StreamMessage.tsx @@ -11,7 +11,8 @@ import { cn } from "@/lib/utils"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; -import { claudeSyntaxTheme } from "@/lib/claudeSyntaxTheme"; +import { getClaudeSyntaxTheme } from "@/lib/claudeSyntaxTheme"; +import { useTheme } from "@/hooks"; import type { ClaudeStreamMessage } from "./AgentExecution"; import { TodoWidget, @@ -54,6 +55,10 @@ const StreamMessageComponent: React.FC = ({ message, classNa // State to track tool results mapped by tool call ID const [toolResults, setToolResults] = useState>(new Map()); + // Get current theme + const { theme } = useTheme(); + const syntaxTheme = getClaudeSyntaxTheme(theme); + // Extract all tool results from stream messages useEffect(() => { const results = new Map(); @@ -131,7 +136,7 @@ const StreamMessageComponent: React.FC = ({ message, classNa const match = /language-(\w+)/.exec(className || ''); return !inline && match ? ( = ({ message, classNa const match = /language-(\w+)/.exec(className || ''); return !inline && match ? ( = ({ fileP */ export const ReadResultWidget: React.FC<{ content: string; filePath?: string }> = ({ content, filePath }) => { const [isExpanded, setIsExpanded] = useState(false); + const { theme } = useTheme(); + const syntaxTheme = getClaudeSyntaxTheme(theme); // Extract file extension for syntax highlighting const getLanguage = (path?: string) => { @@ -530,7 +533,7 @@ export const ReadResultWidget: React.FC<{ content: string; filePath?: string }>
= ({ command, description, result }) => { + const { theme } = useTheme(); + const syntaxTheme = getClaudeSyntaxTheme(theme); + // Extract result content if available let resultContent = ''; let isError = false; @@ -695,6 +701,8 @@ export const BashWidget: React.FC<{ */ export const WriteWidget: React.FC<{ filePath: string; content: string; result?: any }> = ({ filePath, content, result: _result }) => { const [isMaximized, setIsMaximized] = useState(false); + const { theme } = useTheme(); + const syntaxTheme = getClaudeSyntaxTheme(theme); // Extract file extension for syntax highlighting const getLanguage = (path: string) => { @@ -776,7 +784,7 @@ export const WriteWidget: React.FC<{ filePath: string; content: string; result?:
= ({ file_path, old_string, new_string, result: _result }) => { + const { theme } = useTheme(); + const syntaxTheme = getClaudeSyntaxTheme(theme); const diffResult = Diff.diffLines(old_string || '', new_string || '', { newlineIsToken: true, @@ -1165,7 +1175,7 @@ export const EditWidget: React.FC<{
= ({ content }) => { + const { theme } = useTheme(); + const syntaxTheme = getClaudeSyntaxTheme(theme); + // Parse the content to extract file path and code snippet const lines = content.split('\n'); let filePath = ''; @@ -1245,7 +1258,7 @@ export const EditResultWidget: React.FC<{ content: string }> = ({ content }) =>
= ({ toolName, input, result: _result }) => { const [isExpanded, setIsExpanded] = useState(false); + const { theme } = useTheme(); + const syntaxTheme = getClaudeSyntaxTheme(theme); // Parse the tool name to extract components // Format: mcp__namespace__method @@ -1396,7 +1411,7 @@ export const MCPWidget: React.FC<{ )}> = ({ file_path, edits, result: _result }) => { const [isExpanded, setIsExpanded] = useState(false); const language = getLanguage(file_path); + const { theme } = useTheme(); + const syntaxTheme = getClaudeSyntaxTheme(theme); return (
@@ -1645,7 +1662,7 @@ export const MultiEditWidget: React.FC<{
Promise; + setCustomColors: (colors: Partial) => Promise; + isLoading: boolean; +} + +const ThemeContext = createContext(undefined); + +const THEME_STORAGE_KEY = 'theme_preference'; +const CUSTOM_COLORS_STORAGE_KEY = 'theme_custom_colors'; + +// Default custom theme colors (based on current dark theme) +const DEFAULT_CUSTOM_COLORS: CustomThemeColors = { + background: 'oklch(0.12 0.01 240)', + foreground: 'oklch(0.98 0.01 240)', + card: 'oklch(0.14 0.01 240)', + cardForeground: 'oklch(0.98 0.01 240)', + primary: 'oklch(0.98 0.01 240)', + primaryForeground: 'oklch(0.12 0.01 240)', + secondary: 'oklch(0.16 0.01 240)', + secondaryForeground: 'oklch(0.98 0.01 240)', + muted: 'oklch(0.16 0.01 240)', + mutedForeground: 'oklch(0.65 0.01 240)', + accent: 'oklch(0.16 0.01 240)', + accentForeground: 'oklch(0.98 0.01 240)', + destructive: 'oklch(0.6 0.2 25)', + destructiveForeground: 'oklch(0.98 0.01 240)', + border: 'oklch(0.16 0.01 240)', + input: 'oklch(0.16 0.01 240)', + ring: 'oklch(0.98 0.01 240)', +}; + +export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [theme, setThemeState] = useState('dark'); + const [customColors, setCustomColorsState] = useState(DEFAULT_CUSTOM_COLORS); + const [isLoading, setIsLoading] = useState(true); + + // Load theme preference and custom colors from storage + useEffect(() => { + const loadTheme = async () => { + try { + // Load theme preference + const savedTheme = await api.getSetting(THEME_STORAGE_KEY); + + if (savedTheme) { + const themeMode = savedTheme as ThemeMode; + setThemeState(themeMode); + applyTheme(themeMode, customColors); + } + + // Load custom colors + const savedColors = await api.getSetting(CUSTOM_COLORS_STORAGE_KEY); + + if (savedColors) { + const colors = JSON.parse(savedColors) as CustomThemeColors; + setCustomColorsState(colors); + if (theme === 'custom') { + applyTheme('custom', colors); + } + } + } catch (error) { + console.error('Failed to load theme settings:', error); + } finally { + setIsLoading(false); + } + }; + + loadTheme(); + }, []); + + // Apply theme to document + const applyTheme = useCallback((themeMode: ThemeMode, colors: CustomThemeColors) => { + const root = document.documentElement; + + // Remove all theme classes + root.classList.remove('theme-dark', 'theme-gray', 'theme-light', 'theme-custom'); + + // Add new theme class + root.classList.add(`theme-${themeMode}`); + + // If custom theme, apply custom colors as CSS variables + if (themeMode === 'custom') { + Object.entries(colors).forEach(([key, value]) => { + const cssVarName = `--color-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`; + root.style.setProperty(cssVarName, value); + }); + } else { + // Clear custom CSS variables when not using custom theme + Object.keys(colors).forEach((key) => { + const cssVarName = `--color-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`; + root.style.removeProperty(cssVarName); + }); + } + }, []); + + const setTheme = useCallback(async (newTheme: ThemeMode) => { + try { + setIsLoading(true); + + // Apply theme immediately + setThemeState(newTheme); + applyTheme(newTheme, customColors); + + // Save to storage + await api.saveSetting(THEME_STORAGE_KEY, newTheme); + } catch (error) { + console.error('Failed to save theme preference:', error); + } finally { + setIsLoading(false); + } + }, [customColors, applyTheme]); + + const setCustomColors = useCallback(async (colors: Partial) => { + try { + setIsLoading(true); + + const newColors = { ...customColors, ...colors }; + setCustomColorsState(newColors); + + // Apply immediately if custom theme is active + if (theme === 'custom') { + applyTheme('custom', newColors); + } + + // Save to storage + await api.saveSetting(CUSTOM_COLORS_STORAGE_KEY, JSON.stringify(newColors)); + } catch (error) { + console.error('Failed to save custom colors:', error); + } finally { + setIsLoading(false); + } + }, [theme, customColors, applyTheme]); + + const value: ThemeContextType = { + theme, + customColors, + setTheme, + setCustomColors, + isLoading, + }; + + return ( + + {children} + + ); +}; + +export const useThemeContext = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useThemeContext must be used within a ThemeProvider'); + } + return context; +}; \ No newline at end of file diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 34163e3..b3bc5d2 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,4 +2,5 @@ export { useLoadingState } from './useLoadingState'; export { useDebounce, useDebouncedCallback } from './useDebounce'; export { useApiCall } from './useApiCall'; -export { usePagination } from './usePagination'; \ No newline at end of file +export { usePagination } from './usePagination'; +export { useTheme } from './useTheme'; \ No newline at end of file diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts new file mode 100644 index 0000000..7a6bd7b --- /dev/null +++ b/src/hooks/useTheme.ts @@ -0,0 +1,24 @@ +import { useThemeContext } from '../contexts/ThemeContext'; + +/** + * Hook to access and control the theme system + * + * @returns {Object} Theme utilities and state + * @returns {ThemeMode} theme - Current theme mode ('dark' | 'gray' | 'light' | 'custom') + * @returns {CustomThemeColors} customColors - Custom theme color configuration + * @returns {Function} setTheme - Function to change the theme mode + * @returns {Function} setCustomColors - Function to update custom theme colors + * @returns {boolean} isLoading - Whether theme operations are in progress + * + * @example + * const { theme, setTheme } = useTheme(); + * + * // Change theme + * await setTheme('light'); + * + * // Update custom colors + * await setCustomColors({ background: 'oklch(0.98 0.01 240)' }); + */ +export const useTheme = () => { + return useThemeContext(); +}; \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts index 1d89071..9a78640 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1685,6 +1685,50 @@ export const api = { } }, + // Theme settings helpers + + /** + * Gets a setting from the app_settings table + * @param key - The setting key to retrieve + * @returns Promise resolving to the setting value or null if not found + */ + async getSetting(key: string): Promise { + try { + // Use storageReadTable to safely query the app_settings table + const result = await this.storageReadTable('app_settings', 1, 1000); + const setting = result?.data?.find((row: any) => row.key === key); + return setting?.value || null; + } catch (error) { + console.error(`Failed to get setting ${key}:`, error); + return null; + } + }, + + /** + * Saves a setting to the app_settings table (insert or update) + * @param key - The setting key + * @param value - The setting value + * @returns Promise resolving when the setting is saved + */ + async saveSetting(key: string, value: string): Promise { + try { + // Try to update first + try { + await this.storageUpdateRow( + 'app_settings', + { key }, + { value } + ); + } catch (updateError) { + // If update fails (row doesn't exist), insert new row + await this.storageInsertRow('app_settings', { key, value }); + } + } catch (error) { + console.error(`Failed to save setting ${key}:`, error); + throw error; + } + }, + /** * Get hooks configuration for a specific scope * @param scope - The configuration scope: 'user', 'project', or 'local' diff --git a/src/lib/claudeSyntaxTheme.ts b/src/lib/claudeSyntaxTheme.ts index 61b67fa..a4aa9ed 100644 --- a/src/lib/claudeSyntaxTheme.ts +++ b/src/lib/claudeSyntaxTheme.ts @@ -1,175 +1,258 @@ +import { ThemeMode } from '@/contexts/ThemeContext'; + /** - * Claude-themed syntax highlighting theme - * Features orange, purple, and violet colors to match Claude's aesthetic + * Claude-themed syntax highlighting theme factory + * Returns different syntax themes based on the current theme mode + * + * @param theme - The current theme mode + * @returns Prism syntax highlighting theme object */ -export const claudeSyntaxTheme: any = { - 'code[class*="language-"]': { - color: '#e3e8f0', - background: 'transparent', - textShadow: 'none', - fontFamily: 'var(--font-mono)', - fontSize: '0.875em', - textAlign: 'left', - whiteSpace: 'pre', - wordSpacing: 'normal', - wordBreak: 'normal', - wordWrap: 'normal', - lineHeight: '1.5', - MozTabSize: '4', - OTabSize: '4', - tabSize: '4', - WebkitHyphens: 'none', - MozHyphens: 'none', - msHyphens: 'none', - hyphens: 'none', - }, - 'pre[class*="language-"]': { - color: '#e3e8f0', - background: 'transparent', - textShadow: 'none', - fontFamily: 'var(--font-mono)', - fontSize: '0.875em', - textAlign: 'left', - whiteSpace: 'pre', - wordSpacing: 'normal', - wordBreak: 'normal', - wordWrap: 'normal', - lineHeight: '1.5', - MozTabSize: '4', - OTabSize: '4', - tabSize: '4', - WebkitHyphens: 'none', - MozHyphens: 'none', - msHyphens: 'none', - hyphens: 'none', - padding: '1em', - margin: '0', - overflow: 'auto', - }, - ':not(pre) > code[class*="language-"]': { - background: 'rgba(139, 92, 246, 0.1)', - padding: '0.1em 0.3em', - borderRadius: '0.3em', - whiteSpace: 'normal', - }, - 'comment': { - color: '#6b7280', - fontStyle: 'italic', - }, - 'prolog': { - color: '#6b7280', - }, - 'doctype': { - color: '#6b7280', - }, - 'cdata': { - color: '#6b7280', - }, - 'punctuation': { - color: '#9ca3af', - }, - 'namespace': { - opacity: '0.7', - }, - 'property': { - color: '#f59e0b', // Amber/Orange - }, - 'tag': { - color: '#8b5cf6', // Violet - }, - 'boolean': { - color: '#f59e0b', // Amber/Orange - }, - 'number': { - color: '#f59e0b', // Amber/Orange - }, - 'constant': { - color: '#f59e0b', // Amber/Orange - }, - 'symbol': { - color: '#f59e0b', // Amber/Orange - }, - 'deleted': { - color: '#ef4444', - }, - 'selector': { - color: '#a78bfa', // Light Purple - }, - 'attr-name': { - color: '#a78bfa', // Light Purple - }, - 'string': { - color: '#10b981', // Emerald Green - }, - 'char': { - color: '#10b981', // Emerald Green - }, - 'builtin': { - color: '#8b5cf6', // Violet - }, - 'url': { - color: '#10b981', // Emerald Green - }, - 'inserted': { - color: '#10b981', // Emerald Green - }, - 'entity': { - color: '#a78bfa', // Light Purple - cursor: 'help', - }, - 'atrule': { - color: '#c084fc', // Light Violet - }, - 'attr-value': { - color: '#10b981', // Emerald Green - }, - 'keyword': { - color: '#c084fc', // Light Violet - }, - 'function': { - color: '#818cf8', // Indigo - }, - 'class-name': { - color: '#f59e0b', // Amber/Orange - }, - 'regex': { - color: '#06b6d4', // Cyan - }, - 'important': { - color: '#f59e0b', // Amber/Orange - fontWeight: 'bold', - }, - 'variable': { - color: '#a78bfa', // Light Purple - }, - 'bold': { - fontWeight: 'bold', - }, - 'italic': { - fontStyle: 'italic', - }, - 'operator': { - color: '#9ca3af', - }, - 'script': { - color: '#e3e8f0', - }, - 'parameter': { - color: '#fbbf24', // Yellow - }, - 'method': { - color: '#818cf8', // Indigo - }, - 'field': { - color: '#f59e0b', // Amber/Orange - }, - 'annotation': { - color: '#6b7280', - }, - 'type': { - color: '#a78bfa', // Light Purple - }, - 'module': { - color: '#8b5cf6', // Violet - }, -}; \ No newline at end of file +export const getClaudeSyntaxTheme = (theme: ThemeMode): any => { + const themes = { + dark: { + base: '#e3e8f0', + background: 'transparent', + comment: '#6b7280', + punctuation: '#9ca3af', + property: '#f59e0b', // Amber/Orange + tag: '#8b5cf6', // Violet + string: '#10b981', // Emerald Green + function: '#818cf8', // Indigo + keyword: '#c084fc', // Light Violet + variable: '#a78bfa', // Light Purple + operator: '#9ca3af', + }, + gray: { + base: '#e3e8f0', + background: 'transparent', + comment: '#71717a', + punctuation: '#a1a1aa', + property: '#fbbf24', // Yellow + tag: '#a78bfa', // Light Purple + string: '#34d399', // Green + function: '#93bbfc', // Light Blue + keyword: '#d8b4fe', // Light Purple + variable: '#c084fc', // Purple + operator: '#a1a1aa', + }, + light: { + base: '#1f2937', + background: 'transparent', + comment: '#9ca3af', + punctuation: '#6b7280', + property: '#dc2626', // Red + tag: '#7c3aed', // Purple + string: '#059669', // Green + function: '#2563eb', // Blue + keyword: '#9333ea', // Purple + variable: '#8b5cf6', // Violet + operator: '#6b7280', + }, + white: { + base: '#000000', + background: 'transparent', + comment: '#6b7280', + punctuation: '#374151', + property: '#dc2626', // Red + tag: '#5b21b6', // Deep Purple + string: '#047857', // Dark Green + function: '#1e40af', // Dark Blue + keyword: '#6b21a8', // Dark Purple + variable: '#6d28d9', // Dark Violet + operator: '#374151', + }, + custom: { + // Default to dark theme colors for custom + base: '#e3e8f0', + background: 'transparent', + comment: '#6b7280', + punctuation: '#9ca3af', + property: '#f59e0b', + tag: '#8b5cf6', + string: '#10b981', + function: '#818cf8', + keyword: '#c084fc', + variable: '#a78bfa', + operator: '#9ca3af', + } + }; + + const colors = themes[theme] || themes.dark; + + return { + 'code[class*="language-"]': { + color: colors.base, + background: colors.background, + textShadow: 'none', + fontFamily: 'var(--font-mono)', + fontSize: '0.875em', + textAlign: 'left', + whiteSpace: 'pre', + wordSpacing: 'normal', + wordBreak: 'normal', + wordWrap: 'normal', + lineHeight: '1.5', + MozTabSize: '4', + OTabSize: '4', + tabSize: '4', + WebkitHyphens: 'none', + MozHyphens: 'none', + msHyphens: 'none', + hyphens: 'none', + }, + 'pre[class*="language-"]': { + color: colors.base, + background: colors.background, + textShadow: 'none', + fontFamily: 'var(--font-mono)', + fontSize: '0.875em', + textAlign: 'left', + whiteSpace: 'pre', + wordSpacing: 'normal', + wordBreak: 'normal', + wordWrap: 'normal', + lineHeight: '1.5', + MozTabSize: '4', + OTabSize: '4', + tabSize: '4', + WebkitHyphens: 'none', + MozHyphens: 'none', + msHyphens: 'none', + hyphens: 'none', + padding: '1em', + margin: '0', + overflow: 'auto', + }, + ':not(pre) > code[class*="language-"]': { + background: theme === 'light' || theme === 'white' + ? 'rgba(139, 92, 246, 0.1)' + : 'rgba(139, 92, 246, 0.1)', + padding: '0.1em 0.3em', + borderRadius: '0.3em', + whiteSpace: 'normal', + }, + 'comment': { + color: colors.comment, + fontStyle: 'italic', + }, + 'prolog': { + color: colors.comment, + }, + 'doctype': { + color: colors.comment, + }, + 'cdata': { + color: colors.comment, + }, + 'punctuation': { + color: colors.punctuation, + }, + 'namespace': { + opacity: '0.7', + }, + 'property': { + color: colors.property, + }, + 'tag': { + color: colors.tag, + }, + 'boolean': { + color: colors.property, + }, + 'number': { + color: colors.property, + }, + 'constant': { + color: colors.property, + }, + 'symbol': { + color: colors.property, + }, + 'deleted': { + color: '#ef4444', + }, + 'selector': { + color: colors.variable, + }, + 'attr-name': { + color: colors.variable, + }, + 'string': { + color: colors.string, + }, + 'char': { + color: colors.string, + }, + 'builtin': { + color: colors.tag, + }, + 'url': { + color: colors.string, + }, + 'inserted': { + color: colors.string, + }, + 'entity': { + color: colors.variable, + cursor: 'help', + }, + 'atrule': { + color: colors.keyword, + }, + 'attr-value': { + color: colors.string, + }, + 'keyword': { + color: colors.keyword, + }, + 'function': { + color: colors.function, + }, + 'class-name': { + color: colors.property, + }, + 'regex': { + color: '#06b6d4', // Cyan + }, + 'important': { + color: colors.property, + fontWeight: 'bold', + }, + 'variable': { + color: colors.variable, + }, + 'bold': { + fontWeight: 'bold', + }, + 'italic': { + fontStyle: 'italic', + }, + 'operator': { + color: colors.operator, + }, + 'script': { + color: colors.base, + }, + 'parameter': { + color: colors.property, + }, + 'method': { + color: colors.function, + }, + 'field': { + color: colors.property, + }, + 'annotation': { + color: colors.comment, + }, + 'type': { + color: colors.variable, + }, + 'module': { + color: colors.tag, + }, + }; +}; + +// Export default dark theme for backward compatibility +export const claudeSyntaxTheme = getClaudeSyntaxTheme('dark'); \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index 5b1fa96..7615fca 100644 --- a/src/styles.css +++ b/src/styles.css @@ -43,6 +43,95 @@ --ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55); } +/* Theme Variations */ +/* Default is dark theme - already defined above */ + +/* Light Theme */ +.theme-light { + --color-background: oklch(0.98 0.01 240); + --color-foreground: oklch(0.12 0.01 240); + --color-card: oklch(0.96 0.01 240); + --color-card-foreground: oklch(0.12 0.01 240); + --color-popover: oklch(0.98 0.01 240); + --color-popover-foreground: oklch(0.12 0.01 240); + --color-primary: oklch(0.12 0.01 240); + --color-primary-foreground: oklch(0.98 0.01 240); + --color-secondary: oklch(0.94 0.01 240); + --color-secondary-foreground: oklch(0.12 0.01 240); + --color-muted: oklch(0.94 0.01 240); + --color-muted-foreground: oklch(0.45 0.01 240); + --color-accent: oklch(0.94 0.01 240); + --color-accent-foreground: oklch(0.12 0.01 240); + --color-destructive: oklch(0.6 0.2 25); + --color-destructive-foreground: oklch(0.98 0.01 240); + --color-border: oklch(0.90 0.01 240); + --color-input: oklch(0.90 0.01 240); + --color-ring: oklch(0.52 0.015 240); + + /* Additional colors for status messages */ + --color-green-500: oklch(0.62 0.20 142); + --color-green-600: oklch(0.54 0.22 142); +} + +/* Gray Theme */ +.theme-gray { + --color-background: oklch(0.22 0.01 240); + --color-foreground: oklch(0.98 0.01 240); + --color-card: oklch(0.26 0.01 240); + --color-card-foreground: oklch(0.98 0.01 240); + --color-popover: oklch(0.22 0.01 240); + --color-popover-foreground: oklch(0.98 0.01 240); + --color-primary: oklch(0.98 0.01 240); + --color-primary-foreground: oklch(0.22 0.01 240); + --color-secondary: oklch(0.30 0.01 240); + --color-secondary-foreground: oklch(0.98 0.01 240); + --color-muted: oklch(0.30 0.01 240); + --color-muted-foreground: oklch(0.70 0.01 240); + --color-accent: oklch(0.30 0.01 240); + --color-accent-foreground: oklch(0.98 0.01 240); + --color-destructive: oklch(0.6 0.2 25); + --color-destructive-foreground: oklch(0.98 0.01 240); + --color-border: oklch(0.30 0.01 240); + --color-input: oklch(0.30 0.01 240); + --color-ring: oklch(0.60 0.015 240); + + /* Additional colors for status messages */ + --color-green-500: oklch(0.72 0.20 142); + --color-green-600: oklch(0.64 0.22 142); +} + +/* White Theme (High Contrast Light) */ +.theme-white { + --color-background: oklch(1.0 0 240); + --color-foreground: oklch(0.0 0 240); + --color-card: oklch(0.98 0.01 240); + --color-card-foreground: oklch(0.0 0 240); + --color-popover: oklch(1.0 0 240); + --color-popover-foreground: oklch(0.0 0 240); + --color-primary: oklch(0.0 0 240); + --color-primary-foreground: oklch(1.0 0 240); + --color-secondary: oklch(0.96 0.01 240); + --color-secondary-foreground: oklch(0.0 0 240); + --color-muted: oklch(0.96 0.01 240); + --color-muted-foreground: oklch(0.35 0.01 240); + --color-accent: oklch(0.96 0.01 240); + --color-accent-foreground: oklch(0.0 0 240); + --color-destructive: oklch(0.55 0.25 25); + --color-destructive-foreground: oklch(1.0 0 240); + --color-border: oklch(0.85 0.01 240); + --color-input: oklch(0.85 0.01 240); + --color-ring: oklch(0.40 0.015 240); + + /* Additional colors for status messages */ + --color-green-500: oklch(0.55 0.25 142); + --color-green-600: oklch(0.47 0.27 142); +} + +/* Custom Theme - CSS variables will be set dynamically by ThemeContext */ +.theme-custom { + /* Custom theme variables are applied dynamically via JavaScript */ +} + /* Reset and base styles */ * { border-color: var(--color-border); @@ -157,8 +246,10 @@ button:focus-visible, } } -/* Markdown Editor Dark Mode Styles */ -[data-color-mode="dark"] { +/* Markdown Editor Theme-aware Styles */ +[data-color-mode="dark"], +.theme-dark [data-color-mode="dark"], +.theme-gray [data-color-mode="dark"] { --color-border-default: rgb(48, 54, 61); --color-canvas-default: rgb(13, 17, 23); --color-canvas-subtle: rgb(22, 27, 34); @@ -169,6 +260,19 @@ button:focus-visible, --color-danger-fg: rgb(248, 81, 73); } +[data-color-mode="light"], +.theme-light [data-color-mode="light"], +.theme-white [data-color-mode="light"] { + --color-border-default: rgb(216, 222, 228); + --color-canvas-default: rgb(255, 255, 255); + --color-canvas-subtle: rgb(246, 248, 250); + --color-fg-default: rgb(31, 35, 40); + --color-fg-muted: rgb(101, 109, 118); + --color-fg-subtle: rgb(149, 157, 165); + --color-accent-fg: rgb(9, 105, 218); + --color-danger-fg: rgb(207, 34, 46); +} + .w-md-editor { background-color: transparent !important; color: var(--color-foreground) !important;