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

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