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:
@@ -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">
|
||||
|
@@ -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}
|
||||
|
@@ -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={{
|
||||
|
Reference in New Issue
Block a user