Skip to content

Commit c87d36e

Browse files
committed
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.
1 parent 4ddb6a1 commit c87d36e

10 files changed

Lines changed: 809 additions & 192 deletions

File tree

src/App.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Plus, Loader2, Bot, FolderCode } from "lucide-react";
44
import { api, type Project, type Session, type ClaudeMdFile } from "@/lib/api";
55
import { OutputCacheProvider } from "@/lib/outputCache";
66
import { TabProvider } from "@/contexts/TabContext";
7+
import { ThemeProvider } from "@/contexts/ThemeContext";
78
import { Button } from "@/components/ui/button";
89
import { Card } from "@/components/ui/card";
910
import { ProjectList } from "@/components/ProjectList";
@@ -508,11 +509,13 @@ function AppContent() {
508509
*/
509510
function App() {
510511
return (
511-
<OutputCacheProvider>
512-
<TabProvider>
513-
<AppContent />
514-
</TabProvider>
515-
</OutputCacheProvider>
512+
<ThemeProvider>
513+
<OutputCacheProvider>
514+
<TabProvider>
515+
<AppContent />
516+
</TabProvider>
517+
</OutputCacheProvider>
518+
</ThemeProvider>
516519
);
517520
}
518521

src/components/Settings.tsx

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Label } from "@/components/ui/label";
1414
import { Switch } from "@/components/ui/switch";
1515
import { Card } from "@/components/ui/card";
1616
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
17+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
1718
import {
1819
api,
1920
type ClaudeSettings,
@@ -25,6 +26,7 @@ import { ClaudeVersionSelector } from "./ClaudeVersionSelector";
2526
import { StorageTab } from "./StorageTab";
2627
import { HooksEditor } from "./HooksEditor";
2728
import { SlashCommandsManager } from "./SlashCommandsManager";
29+
import { useTheme } from "@/hooks";
2830

2931
interface SettingsProps {
3032
/**
@@ -77,6 +79,9 @@ export const Settings: React.FC<SettingsProps> = ({
7779
const [userHooksChanged, setUserHooksChanged] = useState(false);
7880
const getUserHooks = React.useRef<(() => any) | null>(null);
7981

82+
// Theme hook
83+
const { theme, setTheme, customColors, setCustomColors } = useTheme();
84+
8085
// Load settings on mount
8186
useEffect(() => {
8287
loadSettings();
@@ -375,6 +380,155 @@ export const Settings: React.FC<SettingsProps> = ({
375380
<h3 className="text-base font-semibold mb-4">General Settings</h3>
376381

377382
<div className="space-y-4">
383+
{/* Theme Selector */}
384+
<div className="space-y-2">
385+
<Label htmlFor="theme">Theme</Label>
386+
<Select
387+
value={theme}
388+
onValueChange={(value) => setTheme(value as any)}
389+
>
390+
<SelectTrigger id="theme" className="w-full">
391+
<SelectValue placeholder="Select a theme" />
392+
</SelectTrigger>
393+
<SelectContent>
394+
<SelectItem value="dark">Dark</SelectItem>
395+
<SelectItem value="gray">Gray</SelectItem>
396+
<SelectItem value="light">Light</SelectItem>
397+
<SelectItem value="custom">Custom</SelectItem>
398+
</SelectContent>
399+
</Select>
400+
<p className="text-xs text-muted-foreground">
401+
Choose your preferred color theme for the interface
402+
</p>
403+
</div>
404+
405+
{/* Custom Color Editor */}
406+
{theme === 'custom' && (
407+
<div className="space-y-4 p-4 border rounded-lg bg-muted/20">
408+
<h4 className="text-sm font-medium">Custom Theme Colors</h4>
409+
410+
<div className="grid grid-cols-2 gap-4">
411+
{/* Background Color */}
412+
<div className="space-y-2">
413+
<Label htmlFor="color-background" className="text-xs">Background</Label>
414+
<div className="flex gap-2">
415+
<Input
416+
id="color-background"
417+
type="text"
418+
value={customColors.background}
419+
onChange={(e) => setCustomColors({ background: e.target.value })}
420+
placeholder="oklch(0.12 0.01 240)"
421+
className="font-mono text-xs"
422+
/>
423+
<div
424+
className="w-10 h-10 rounded border"
425+
style={{ backgroundColor: customColors.background }}
426+
/>
427+
</div>
428+
</div>
429+
430+
{/* Foreground Color */}
431+
<div className="space-y-2">
432+
<Label htmlFor="color-foreground" className="text-xs">Foreground</Label>
433+
<div className="flex gap-2">
434+
<Input
435+
id="color-foreground"
436+
type="text"
437+
value={customColors.foreground}
438+
onChange={(e) => setCustomColors({ foreground: e.target.value })}
439+
placeholder="oklch(0.98 0.01 240)"
440+
className="font-mono text-xs"
441+
/>
442+
<div
443+
className="w-10 h-10 rounded border"
444+
style={{ backgroundColor: customColors.foreground }}
445+
/>
446+
</div>
447+
</div>
448+
449+
{/* Primary Color */}
450+
<div className="space-y-2">
451+
<Label htmlFor="color-primary" className="text-xs">Primary</Label>
452+
<div className="flex gap-2">
453+
<Input
454+
id="color-primary"
455+
type="text"
456+
value={customColors.primary}
457+
onChange={(e) => setCustomColors({ primary: e.target.value })}
458+
placeholder="oklch(0.98 0.01 240)"
459+
className="font-mono text-xs"
460+
/>
461+
<div
462+
className="w-10 h-10 rounded border"
463+
style={{ backgroundColor: customColors.primary }}
464+
/>
465+
</div>
466+
</div>
467+
468+
{/* Card Color */}
469+
<div className="space-y-2">
470+
<Label htmlFor="color-card" className="text-xs">Card</Label>
471+
<div className="flex gap-2">
472+
<Input
473+
id="color-card"
474+
type="text"
475+
value={customColors.card}
476+
onChange={(e) => setCustomColors({ card: e.target.value })}
477+
placeholder="oklch(0.14 0.01 240)"
478+
className="font-mono text-xs"
479+
/>
480+
<div
481+
className="w-10 h-10 rounded border"
482+
style={{ backgroundColor: customColors.card }}
483+
/>
484+
</div>
485+
</div>
486+
487+
{/* Accent Color */}
488+
<div className="space-y-2">
489+
<Label htmlFor="color-accent" className="text-xs">Accent</Label>
490+
<div className="flex gap-2">
491+
<Input
492+
id="color-accent"
493+
type="text"
494+
value={customColors.accent}
495+
onChange={(e) => setCustomColors({ accent: e.target.value })}
496+
placeholder="oklch(0.16 0.01 240)"
497+
className="font-mono text-xs"
498+
/>
499+
<div
500+
className="w-10 h-10 rounded border"
501+
style={{ backgroundColor: customColors.accent }}
502+
/>
503+
</div>
504+
</div>
505+
506+
{/* Destructive Color */}
507+
<div className="space-y-2">
508+
<Label htmlFor="color-destructive" className="text-xs">Destructive</Label>
509+
<div className="flex gap-2">
510+
<Input
511+
id="color-destructive"
512+
type="text"
513+
value={customColors.destructive}
514+
onChange={(e) => setCustomColors({ destructive: e.target.value })}
515+
placeholder="oklch(0.6 0.2 25)"
516+
className="font-mono text-xs"
517+
/>
518+
<div
519+
className="w-10 h-10 rounded border"
520+
style={{ backgroundColor: customColors.destructive }}
521+
/>
522+
</div>
523+
</div>
524+
</div>
525+
526+
<p className="text-xs text-muted-foreground">
527+
Use CSS color values (hex, rgb, oklch, etc.). Changes apply immediately.
528+
</p>
529+
</div>
530+
)}
531+
378532
{/* Include Co-authored By */}
379533
<div className="flex items-center justify-between">
380534
<div className="space-y-0.5 flex-1">

src/components/StreamMessage.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import { cn } from "@/lib/utils";
1111
import ReactMarkdown from "react-markdown";
1212
import remarkGfm from "remark-gfm";
1313
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
14-
import { claudeSyntaxTheme } from "@/lib/claudeSyntaxTheme";
14+
import { getClaudeSyntaxTheme } from "@/lib/claudeSyntaxTheme";
15+
import { useTheme } from "@/hooks";
1516
import type { ClaudeStreamMessage } from "./AgentExecution";
1617
import {
1718
TodoWidget,
@@ -54,6 +55,10 @@ const StreamMessageComponent: React.FC<StreamMessageProps> = ({ message, classNa
5455
// State to track tool results mapped by tool call ID
5556
const [toolResults, setToolResults] = useState<Map<string, any>>(new Map());
5657

58+
// Get current theme
59+
const { theme } = useTheme();
60+
const syntaxTheme = getClaudeSyntaxTheme(theme);
61+
5762
// Extract all tool results from stream messages
5863
useEffect(() => {
5964
const results = new Map<string, any>();
@@ -131,7 +136,7 @@ const StreamMessageComponent: React.FC<StreamMessageProps> = ({ message, classNa
131136
const match = /language-(\w+)/.exec(className || '');
132137
return !inline && match ? (
133138
<SyntaxHighlighter
134-
style={claudeSyntaxTheme}
139+
style={syntaxTheme}
135140
language={match[1]}
136141
PreTag="div"
137142
{...props}
@@ -660,7 +665,7 @@ const StreamMessageComponent: React.FC<StreamMessageProps> = ({ message, classNa
660665
const match = /language-(\w+)/.exec(className || '');
661666
return !inline && match ? (
662667
<SyntaxHighlighter
663-
style={claudeSyntaxTheme}
668+
style={syntaxTheme}
664669
language={match[1]}
665670
PreTag="div"
666671
{...props}

src/components/ToolWidgets.tsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ import {
5151
import { Badge } from "@/components/ui/badge";
5252
import { cn } from "@/lib/utils";
5353
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
54-
import { claudeSyntaxTheme } from "@/lib/claudeSyntaxTheme";
54+
import { getClaudeSyntaxTheme } from "@/lib/claudeSyntaxTheme";
55+
import { useTheme } from "@/hooks";
5556
import { Button } from "@/components/ui/button";
5657
import { createPortal } from "react-dom";
5758
import * as Diff from 'diff';
@@ -400,6 +401,8 @@ export const ReadWidget: React.FC<{ filePath: string; result?: any }> = ({ fileP
400401
*/
401402
export const ReadResultWidget: React.FC<{ content: string; filePath?: string }> = ({ content, filePath }) => {
402403
const [isExpanded, setIsExpanded] = useState(false);
404+
const { theme } = useTheme();
405+
const syntaxTheme = getClaudeSyntaxTheme(theme);
403406

404407
// Extract file extension for syntax highlighting
405408
const getLanguage = (path?: string) => {
@@ -530,7 +533,7 @@ export const ReadResultWidget: React.FC<{ content: string; filePath?: string }>
530533
<div className="relative overflow-x-auto">
531534
<SyntaxHighlighter
532535
language={language}
533-
style={claudeSyntaxTheme}
536+
style={syntaxTheme}
534537
showLineNumbers
535538
startingLineNumber={startLineNumber}
536539
wrapLongLines={false}
@@ -629,6 +632,9 @@ export const BashWidget: React.FC<{
629632
description?: string;
630633
result?: any;
631634
}> = ({ command, description, result }) => {
635+
const { theme } = useTheme();
636+
const syntaxTheme = getClaudeSyntaxTheme(theme);
637+
632638
// Extract result content if available
633639
let resultContent = '';
634640
let isError = false;
@@ -695,6 +701,8 @@ export const BashWidget: React.FC<{
695701
*/
696702
export const WriteWidget: React.FC<{ filePath: string; content: string; result?: any }> = ({ filePath, content, result: _result }) => {
697703
const [isMaximized, setIsMaximized] = useState(false);
704+
const { theme } = useTheme();
705+
const syntaxTheme = getClaudeSyntaxTheme(theme);
698706

699707
// Extract file extension for syntax highlighting
700708
const getLanguage = (path: string) => {
@@ -776,7 +784,7 @@ export const WriteWidget: React.FC<{ filePath: string; content: string; result?:
776784
<div className="flex-1 overflow-auto">
777785
<SyntaxHighlighter
778786
language={language}
779-
style={claudeSyntaxTheme}
787+
style={syntaxTheme}
780788
customStyle={{
781789
margin: 0,
782790
padding: '1.5rem',
@@ -827,7 +835,7 @@ export const WriteWidget: React.FC<{ filePath: string; content: string; result?:
827835
<div className="overflow-auto flex-1">
828836
<SyntaxHighlighter
829837
language={language}
830-
style={claudeSyntaxTheme}
838+
style={syntaxTheme}
831839
customStyle={{
832840
margin: 0,
833841
padding: '1rem',
@@ -1121,6 +1129,8 @@ export const EditWidget: React.FC<{
11211129
new_string: string;
11221130
result?: any;
11231131
}> = ({ file_path, old_string, new_string, result: _result }) => {
1132+
const { theme } = useTheme();
1133+
const syntaxTheme = getClaudeSyntaxTheme(theme);
11241134

11251135
const diffResult = Diff.diffLines(old_string || '', new_string || '', {
11261136
newlineIsToken: true,
@@ -1165,7 +1175,7 @@ export const EditWidget: React.FC<{
11651175
<div className="flex-1">
11661176
<SyntaxHighlighter
11671177
language={language}
1168-
style={claudeSyntaxTheme}
1178+
style={syntaxTheme}
11691179
PreTag="div"
11701180
wrapLongLines={false}
11711181
customStyle={{
@@ -1196,6 +1206,9 @@ export const EditWidget: React.FC<{
11961206
* Widget for Edit tool result - shows a diff view
11971207
*/
11981208
export const EditResultWidget: React.FC<{ content: string }> = ({ content }) => {
1209+
const { theme } = useTheme();
1210+
const syntaxTheme = getClaudeSyntaxTheme(theme);
1211+
11991212
// Parse the content to extract file path and code snippet
12001213
const lines = content.split('\n');
12011214
let filePath = '';
@@ -1245,7 +1258,7 @@ export const EditResultWidget: React.FC<{ content: string }> = ({ content }) =>
12451258
<div className="overflow-x-auto max-h-[440px]">
12461259
<SyntaxHighlighter
12471260
language={language}
1248-
style={claudeSyntaxTheme}
1261+
style={syntaxTheme}
12491262
showLineNumbers
12501263
startingLineNumber={startLineNumber}
12511264
wrapLongLines={false}
@@ -1282,6 +1295,8 @@ export const MCPWidget: React.FC<{
12821295
result?: any;
12831296
}> = ({ toolName, input, result: _result }) => {
12841297
const [isExpanded, setIsExpanded] = useState(false);
1298+
const { theme } = useTheme();
1299+
const syntaxTheme = getClaudeSyntaxTheme(theme);
12851300

12861301
// Parse the tool name to extract components
12871302
// Format: mcp__namespace__method
@@ -1396,7 +1411,7 @@ export const MCPWidget: React.FC<{
13961411
)}>
13971412
<SyntaxHighlighter
13981413
language="json"
1399-
style={claudeSyntaxTheme}
1414+
style={syntaxTheme}
14001415
customStyle={{
14011416
margin: 0,
14021417
padding: '0.75rem',
@@ -1585,6 +1600,8 @@ export const MultiEditWidget: React.FC<{
15851600
}> = ({ file_path, edits, result: _result }) => {
15861601
const [isExpanded, setIsExpanded] = useState(false);
15871602
const language = getLanguage(file_path);
1603+
const { theme } = useTheme();
1604+
const syntaxTheme = getClaudeSyntaxTheme(theme);
15881605

15891606
return (
15901607
<div className="space-y-2">
@@ -1645,7 +1662,7 @@ export const MultiEditWidget: React.FC<{
16451662
<div className="flex-1">
16461663
<SyntaxHighlighter
16471664
language={language}
1648-
style={claudeSyntaxTheme}
1665+
style={syntaxTheme}
16491666
PreTag="div"
16501667
wrapLongLines={false}
16511668
customStyle={{

0 commit comments

Comments
 (0)