feat(ui): add theme switching system with Twitter, Claude, and Neo Brutalism themes

Add a comprehensive theme system allowing users to switch between three
distinct visual themes, each supporting both light and dark modes:

- Twitter (default): Clean blue design with soft shadows
- Claude: Warm beige/cream tones with orange primary accents
- Neo Brutalism: Bold colors, hard shadows, 0px border radius

New files:
- ui/src/hooks/useTheme.ts: Theme state management hook with localStorage
  persistence for both theme selection and dark mode preference
- ui/src/components/ThemeSelector.tsx: Header dropdown with hover preview
  and color swatches for quick theme switching

Modified files:
- ui/src/styles/globals.css: Added CSS custom properties for Claude and
  Neo Brutalism themes with light/dark variants, shadow variables
  integrated into @theme inline block
- ui/src/App.tsx: Integrated useTheme hook and ThemeSelector component
- ui/src/components/SettingsModal.tsx: Added theme selection UI with
  preview swatches and dark mode toggle
- ui/index.html: Added DM Sans and Space Mono fonts for Neo Brutalism

Features:
- Independent theme and dark mode controls
- Smooth CSS transitions when switching themes
- Theme-specific shadow styles (soft vs hard)
- Theme-specific fonts and border radius
- Persisted preferences in localStorage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-01-26 18:40:24 +02:00
parent c917582a64
commit c402736b92
6 changed files with 637 additions and 27 deletions

View File

@@ -1,5 +1,6 @@
import { Loader2, AlertCircle } from 'lucide-react'
import { Loader2, AlertCircle, Check, Moon, Sun } from 'lucide-react'
import { useSettings, useUpdateSettings, useAvailableModels } from '../hooks/useProjects'
import { useTheme, THEMES } from '../hooks/useTheme'
import {
Dialog,
DialogContent,
@@ -20,6 +21,7 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
const { data: settings, isLoading, isError, refetch } = useSettings()
const { data: modelsData } = useAvailableModels()
const updateSettings = useUpdateSettings()
const { theme, setTheme, darkMode, toggleDarkMode } = useTheme()
const handleYoloToggle = () => {
if (settings && !updateSettings.isPending) {
@@ -80,6 +82,77 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
{/* Settings Content */}
{settings && !isLoading && (
<div className="space-y-6">
{/* Theme Selection */}
<div className="space-y-3">
<Label className="font-medium">Theme</Label>
<div className="grid gap-2">
{THEMES.map((themeOption) => (
<button
key={themeOption.id}
onClick={() => setTheme(themeOption.id)}
className={`flex items-center gap-3 p-3 rounded-lg border-2 transition-colors text-left ${
theme === themeOption.id
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/50 hover:bg-muted/50'
}`}
>
{/* Color swatches */}
<div className="flex gap-0.5 shrink-0">
<div
className="w-5 h-5 rounded-sm border border-border/50"
style={{ backgroundColor: themeOption.previewColors.background }}
/>
<div
className="w-5 h-5 rounded-sm border border-border/50"
style={{ backgroundColor: themeOption.previewColors.primary }}
/>
<div
className="w-5 h-5 rounded-sm border border-border/50"
style={{ backgroundColor: themeOption.previewColors.accent }}
/>
</div>
{/* Theme info */}
<div className="flex-1 min-w-0">
<div className="font-medium text-sm">{themeOption.name}</div>
<div className="text-xs text-muted-foreground">
{themeOption.description}
</div>
</div>
{/* Checkmark */}
{theme === themeOption.id && (
<Check size={18} className="text-primary shrink-0" />
)}
</button>
))}
</div>
</div>
{/* Dark Mode Toggle */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="dark-mode" className="font-medium">
Dark Mode
</Label>
<p className="text-sm text-muted-foreground">
Switch between light and dark appearance
</p>
</div>
<Button
id="dark-mode"
variant="outline"
size="sm"
onClick={toggleDarkMode}
className="gap-2"
>
{darkMode ? <Sun size={16} /> : <Moon size={16} />}
{darkMode ? 'Light' : 'Dark'}
</Button>
</div>
<hr className="border-border" />
{/* YOLO Mode Toggle */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">