feat: implement comprehensive theming system

- Add ThemeContext with support for dark, gray, light, and custom themes
- Create theme switching UI in Settings with theme selector
- Add custom color editor for custom theme mode
- Update styles.css with theme-specific CSS variables
- Add theme storage API methods for persistence
- Update syntax highlighting to match selected theme
- Wrap App with ThemeProvider for global theme access

The theming system allows users to switch between predefined themes
or create their own custom theme with live color editing.
This commit is contained in:
Mufeed VH
2025-07-28 15:05:46 +05:30
parent 4ddb6a1995
commit c87d36e118
10 changed files with 809 additions and 192 deletions

View File

@@ -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 (
<OutputCacheProvider>
<TabProvider>
<AppContent />
</TabProvider>
</OutputCacheProvider>
<ThemeProvider>
<OutputCacheProvider>
<TabProvider>
<AppContent />
</TabProvider>
</OutputCacheProvider>
</ThemeProvider>
);
}

View File

@@ -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<SettingsProps> = ({
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<SettingsProps> = ({
<h3 className="text-base font-semibold mb-4">General Settings</h3>
<div className="space-y-4">
{/* Theme Selector */}
<div className="space-y-2">
<Label htmlFor="theme">Theme</Label>
<Select
value={theme}
onValueChange={(value) => setTheme(value as any)}
>
<SelectTrigger id="theme" className="w-full">
<SelectValue placeholder="Select a theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="gray">Gray</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="custom">Custom</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Choose your preferred color theme for the interface
</p>
</div>
{/* Custom Color Editor */}
{theme === 'custom' && (
<div className="space-y-4 p-4 border rounded-lg bg-muted/20">
<h4 className="text-sm font-medium">Custom Theme Colors</h4>
<div className="grid grid-cols-2 gap-4">
{/* Background Color */}
<div className="space-y-2">
<Label htmlFor="color-background" className="text-xs">Background</Label>
<div className="flex gap-2">
<Input
id="color-background"
type="text"
value={customColors.background}
onChange={(e) => setCustomColors({ background: e.target.value })}
placeholder="oklch(0.12 0.01 240)"
className="font-mono text-xs"
/>
<div
className="w-10 h-10 rounded border"
style={{ backgroundColor: customColors.background }}
/>
</div>
</div>
{/* Foreground Color */}
<div className="space-y-2">
<Label htmlFor="color-foreground" className="text-xs">Foreground</Label>
<div className="flex gap-2">
<Input
id="color-foreground"
type="text"
value={customColors.foreground}
onChange={(e) => setCustomColors({ foreground: e.target.value })}
placeholder="oklch(0.98 0.01 240)"
className="font-mono text-xs"
/>
<div
className="w-10 h-10 rounded border"
style={{ backgroundColor: customColors.foreground }}
/>
</div>
</div>
{/* Primary Color */}
<div className="space-y-2">
<Label htmlFor="color-primary" className="text-xs">Primary</Label>
<div className="flex gap-2">
<Input
id="color-primary"
type="text"
value={customColors.primary}
onChange={(e) => setCustomColors({ primary: e.target.value })}
placeholder="oklch(0.98 0.01 240)"
className="font-mono text-xs"
/>
<div
className="w-10 h-10 rounded border"
style={{ backgroundColor: customColors.primary }}
/>
</div>
</div>
{/* Card Color */}
<div className="space-y-2">
<Label htmlFor="color-card" className="text-xs">Card</Label>
<div className="flex gap-2">
<Input
id="color-card"
type="text"
value={customColors.card}
onChange={(e) => setCustomColors({ card: e.target.value })}
placeholder="oklch(0.14 0.01 240)"
className="font-mono text-xs"
/>
<div
className="w-10 h-10 rounded border"
style={{ backgroundColor: customColors.card }}
/>
</div>
</div>
{/* Accent Color */}
<div className="space-y-2">
<Label htmlFor="color-accent" className="text-xs">Accent</Label>
<div className="flex gap-2">
<Input
id="color-accent"
type="text"
value={customColors.accent}
onChange={(e) => setCustomColors({ accent: e.target.value })}
placeholder="oklch(0.16 0.01 240)"
className="font-mono text-xs"
/>
<div
className="w-10 h-10 rounded border"
style={{ backgroundColor: customColors.accent }}
/>
</div>
</div>
{/* Destructive Color */}
<div className="space-y-2">
<Label htmlFor="color-destructive" className="text-xs">Destructive</Label>
<div className="flex gap-2">
<Input
id="color-destructive"
type="text"
value={customColors.destructive}
onChange={(e) => setCustomColors({ destructive: e.target.value })}
placeholder="oklch(0.6 0.2 25)"
className="font-mono text-xs"
/>
<div
className="w-10 h-10 rounded border"
style={{ backgroundColor: customColors.destructive }}
/>
</div>
</div>
</div>
<p className="text-xs text-muted-foreground">
Use CSS color values (hex, rgb, oklch, etc.). Changes apply immediately.
</p>
</div>
)}
{/* Include Co-authored By */}
<div className="flex items-center justify-between">
<div className="space-y-0.5 flex-1">

View File

@@ -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<StreamMessageProps> = ({ message, classNa
// State to track tool results mapped by tool call ID
const [toolResults, setToolResults] = useState<Map<string, any>>(new Map());
// Get current theme
const { theme } = useTheme();
const syntaxTheme = getClaudeSyntaxTheme(theme);
// Extract all tool results from stream messages
useEffect(() => {
const results = new Map<string, any>();
@@ -131,7 +136,7 @@ const StreamMessageComponent: React.FC<StreamMessageProps> = ({ message, classNa
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={claudeSyntaxTheme}
style={syntaxTheme}
language={match[1]}
PreTag="div"
{...props}
@@ -660,7 +665,7 @@ const StreamMessageComponent: React.FC<StreamMessageProps> = ({ message, classNa
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={claudeSyntaxTheme}
style={syntaxTheme}
language={match[1]}
PreTag="div"
{...props}

View File

@@ -51,7 +51,8 @@ import {
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { claudeSyntaxTheme } from "@/lib/claudeSyntaxTheme";
import { getClaudeSyntaxTheme } from "@/lib/claudeSyntaxTheme";
import { useTheme } from "@/hooks";
import { Button } from "@/components/ui/button";
import { createPortal } from "react-dom";
import * as Diff from 'diff';
@@ -400,6 +401,8 @@ export const ReadWidget: React.FC<{ filePath: string; result?: any }> = ({ 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 }>
<div className="relative overflow-x-auto">
<SyntaxHighlighter
language={language}
style={claudeSyntaxTheme}
style={syntaxTheme}
showLineNumbers
startingLineNumber={startLineNumber}
wrapLongLines={false}
@@ -629,6 +632,9 @@ export const BashWidget: React.FC<{
description?: string;
result?: any;
}> = ({ 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?:
<div className="flex-1 overflow-auto">
<SyntaxHighlighter
language={language}
style={claudeSyntaxTheme}
style={syntaxTheme}
customStyle={{
margin: 0,
padding: '1.5rem',
@@ -827,7 +835,7 @@ export const WriteWidget: React.FC<{ filePath: string; content: string; result?:
<div className="overflow-auto flex-1">
<SyntaxHighlighter
language={language}
style={claudeSyntaxTheme}
style={syntaxTheme}
customStyle={{
margin: 0,
padding: '1rem',
@@ -1121,6 +1129,8 @@ export const EditWidget: React.FC<{
new_string: string;
result?: any;
}> = ({ 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<{
<div className="flex-1">
<SyntaxHighlighter
language={language}
style={claudeSyntaxTheme}
style={syntaxTheme}
PreTag="div"
wrapLongLines={false}
customStyle={{
@@ -1196,6 +1206,9 @@ export const EditWidget: React.FC<{
* Widget for Edit tool result - shows a diff view
*/
export const EditResultWidget: React.FC<{ content: string }> = ({ 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 }) =>
<div className="overflow-x-auto max-h-[440px]">
<SyntaxHighlighter
language={language}
style={claudeSyntaxTheme}
style={syntaxTheme}
showLineNumbers
startingLineNumber={startLineNumber}
wrapLongLines={false}
@@ -1282,6 +1295,8 @@ export const MCPWidget: React.FC<{
result?: any;
}> = ({ 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<{
)}>
<SyntaxHighlighter
language="json"
style={claudeSyntaxTheme}
style={syntaxTheme}
customStyle={{
margin: 0,
padding: '0.75rem',
@@ -1585,6 +1600,8 @@ export const MultiEditWidget: 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 (
<div className="space-y-2">
@@ -1645,7 +1662,7 @@ export const MultiEditWidget: React.FC<{
<div className="flex-1">
<SyntaxHighlighter
language={language}
style={claudeSyntaxTheme}
style={syntaxTheme}
PreTag="div"
wrapLongLines={false}
customStyle={{

View File

@@ -0,0 +1,182 @@
import React, { createContext, useState, useContext, useCallback, useEffect } from 'react';
import { api } from '../lib/api';
export type ThemeMode = 'dark' | 'gray' | 'light' | 'custom';
export interface CustomThemeColors {
background: string;
foreground: string;
card: string;
cardForeground: string;
primary: string;
primaryForeground: string;
secondary: string;
secondaryForeground: string;
muted: string;
mutedForeground: string;
accent: string;
accentForeground: string;
destructive: string;
destructiveForeground: string;
border: string;
input: string;
ring: string;
}
interface ThemeContextType {
theme: ThemeMode;
customColors: CustomThemeColors;
setTheme: (theme: ThemeMode) => Promise<void>;
setCustomColors: (colors: Partial<CustomThemeColors>) => Promise<void>;
isLoading: boolean;
}
const ThemeContext = createContext<ThemeContextType | undefined>(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<ThemeMode>('dark');
const [customColors, setCustomColorsState] = useState<CustomThemeColors>(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<CustomThemeColors>) => {
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 (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
export const useThemeContext = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useThemeContext must be used within a ThemeProvider');
}
return context;
};

View File

@@ -2,4 +2,5 @@
export { useLoadingState } from './useLoadingState';
export { useDebounce, useDebouncedCallback } from './useDebounce';
export { useApiCall } from './useApiCall';
export { usePagination } from './usePagination';
export { usePagination } from './usePagination';
export { useTheme } from './useTheme';

24
src/hooks/useTheme.ts Normal file
View File

@@ -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();
};

View File

@@ -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<string | null> {
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<void> {
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'

View File

@@ -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
},
};
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');

View File

@@ -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;