diff --git a/ui/src/components/ThemeSelector.tsx b/ui/src/components/ThemeSelector.tsx
new file mode 100644
index 0000000..025ec8d
--- /dev/null
+++ b/ui/src/components/ThemeSelector.tsx
@@ -0,0 +1,155 @@
+import { useState, useRef, useEffect } from 'react'
+import { Palette, Check } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import type { ThemeId, ThemeOption } from '../hooks/useTheme'
+
+interface ThemeSelectorProps {
+ themes: ThemeOption[]
+ currentTheme: ThemeId
+ onThemeChange: (theme: ThemeId) => void
+}
+
+export function ThemeSelector({ themes, currentTheme, onThemeChange }: ThemeSelectorProps) {
+ const [isOpen, setIsOpen] = useState(false)
+ const [previewTheme, setPreviewTheme] = useState
(null)
+ const containerRef = useRef(null)
+ const timeoutRef = useRef(null)
+
+ // Close dropdown when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
+ setIsOpen(false)
+ setPreviewTheme(null)
+ }
+ }
+
+ document.addEventListener('mousedown', handleClickOutside)
+ return () => document.removeEventListener('mousedown', handleClickOutside)
+ }, [])
+
+ // Apply preview theme temporarily
+ useEffect(() => {
+ if (previewTheme) {
+ const root = document.documentElement
+ root.classList.remove('theme-claude', 'theme-neo-brutalism')
+ if (previewTheme === 'claude') {
+ root.classList.add('theme-claude')
+ } else if (previewTheme === 'neo-brutalism') {
+ root.classList.add('theme-neo-brutalism')
+ }
+ }
+
+ // Cleanup: restore current theme when preview ends
+ return () => {
+ if (previewTheme) {
+ const root = document.documentElement
+ root.classList.remove('theme-claude', 'theme-neo-brutalism')
+ if (currentTheme === 'claude') {
+ root.classList.add('theme-claude')
+ } else if (currentTheme === 'neo-brutalism') {
+ root.classList.add('theme-neo-brutalism')
+ }
+ }
+ }
+ }, [previewTheme, currentTheme])
+
+ const handleMouseEnter = () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current)
+ }
+ setIsOpen(true)
+ }
+
+ const handleMouseLeave = () => {
+ timeoutRef.current = setTimeout(() => {
+ setIsOpen(false)
+ setPreviewTheme(null)
+ }, 150)
+ }
+
+ const handleThemeHover = (themeId: ThemeId) => {
+ setPreviewTheme(themeId)
+ }
+
+ const handleThemeClick = (themeId: ThemeId) => {
+ onThemeChange(themeId)
+ setPreviewTheme(null)
+ setIsOpen(false)
+ }
+
+ return (
+
+
+
+ {/* Dropdown */}
+ {isOpen && (
+
+
+ {themes.map((theme) => (
+
+ ))}
+
+
+ )}
+
+ )
+}
diff --git a/ui/src/hooks/useTheme.ts b/ui/src/hooks/useTheme.ts
new file mode 100644
index 0000000..5acfda2
--- /dev/null
+++ b/ui/src/hooks/useTheme.ts
@@ -0,0 +1,124 @@
+import { useState, useEffect, useCallback } from 'react'
+
+export type ThemeId = 'twitter' | 'claude' | 'neo-brutalism'
+
+export interface ThemeOption {
+ id: ThemeId
+ name: string
+ description: string
+ previewColors: {
+ primary: string
+ background: string
+ accent: string
+ }
+}
+
+export const THEMES: ThemeOption[] = [
+ {
+ id: 'twitter',
+ name: 'Twitter',
+ description: 'Clean and modern blue design',
+ previewColors: { primary: '#4a9eff', background: '#ffffff', accent: '#e8f4ff' }
+ },
+ {
+ id: 'claude',
+ name: 'Claude',
+ description: 'Warm beige tones with orange accents',
+ previewColors: { primary: '#c75b2a', background: '#faf6f0', accent: '#f5ede4' }
+ },
+ {
+ id: 'neo-brutalism',
+ name: 'Neo Brutalism',
+ description: 'Bold colors with hard shadows',
+ previewColors: { primary: '#ff4d00', background: '#ffffff', accent: '#ffeb00' }
+ }
+]
+
+const THEME_STORAGE_KEY = 'autocoder-theme'
+const DARK_MODE_STORAGE_KEY = 'autocoder-dark-mode'
+
+function getThemeClass(themeId: ThemeId): string {
+ switch (themeId) {
+ case 'twitter':
+ return '' // Default, no class needed
+ case 'claude':
+ return 'theme-claude'
+ case 'neo-brutalism':
+ return 'theme-neo-brutalism'
+ default:
+ return ''
+ }
+}
+
+export function useTheme() {
+ const [theme, setThemeState] = useState(() => {
+ try {
+ const stored = localStorage.getItem(THEME_STORAGE_KEY)
+ if (stored === 'twitter' || stored === 'claude' || stored === 'neo-brutalism') {
+ return stored
+ }
+ } catch {
+ // localStorage not available
+ }
+ return 'twitter'
+ })
+
+ const [darkMode, setDarkModeState] = useState(() => {
+ try {
+ return localStorage.getItem(DARK_MODE_STORAGE_KEY) === 'true'
+ } catch {
+ return false
+ }
+ })
+
+ // Apply theme and dark mode classes to document
+ useEffect(() => {
+ const root = document.documentElement
+
+ // Remove all theme classes
+ root.classList.remove('theme-claude', 'theme-neo-brutalism')
+
+ // Add current theme class (if not twitter/default)
+ const themeClass = getThemeClass(theme)
+ if (themeClass) {
+ root.classList.add(themeClass)
+ }
+
+ // Handle dark mode
+ if (darkMode) {
+ root.classList.add('dark')
+ } else {
+ root.classList.remove('dark')
+ }
+
+ // Persist to localStorage
+ try {
+ localStorage.setItem(THEME_STORAGE_KEY, theme)
+ localStorage.setItem(DARK_MODE_STORAGE_KEY, String(darkMode))
+ } catch {
+ // localStorage not available
+ }
+ }, [theme, darkMode])
+
+ const setTheme = useCallback((newTheme: ThemeId) => {
+ setThemeState(newTheme)
+ }, [])
+
+ const setDarkMode = useCallback((enabled: boolean) => {
+ setDarkModeState(enabled)
+ }, [])
+
+ const toggleDarkMode = useCallback(() => {
+ setDarkModeState(prev => !prev)
+ }, [])
+
+ return {
+ theme,
+ setTheme,
+ darkMode,
+ setDarkMode,
+ toggleDarkMode,
+ themes: THEMES,
+ currentTheme: THEMES.find(t => t.id === theme) ?? THEMES[0]
+ }
+}
diff --git a/ui/src/styles/globals.css b/ui/src/styles/globals.css
index 58b2fd3..edbe89a 100644
--- a/ui/src/styles/globals.css
+++ b/ui/src/styles/globals.css
@@ -5,7 +5,8 @@
@custom-variant dark (&:where(.dark, .dark *));
/* ============================================================================
- ShadCN Theme - Clean Twitter-Style Design
+ Theme: Twitter (Default)
+ Clean, modern blue design
============================================================================ */
:root {
@@ -43,6 +44,12 @@
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
+ /* Shadow variables */
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
+
/* Log level colors (kept for Terminal/Debug components) */
--color-log-error: #ef4444;
--color-log-warning: #f59e0b;
@@ -99,6 +106,12 @@
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
+ /* Shadow variables - dark mode */
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.4), 0 1px 2px -1px rgb(0 0 0 / 0.3);
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
+
/* Log level colors for dark mode */
--color-log-error: #f87171;
--color-log-warning: #fbbf24;
@@ -112,7 +125,242 @@
--color-status-done: oklch(0.25 0.05 245);
}
-/* ShadCN Tailwind v4 Theme Integration */
+/* ============================================================================
+ Theme: Claude
+ Warm beige/cream tones with orange primary
+ ============================================================================ */
+
+.theme-claude {
+ --radius: 0.5rem;
+ --background: oklch(0.9818 0.0054 95.0986);
+ --foreground: oklch(0.3438 0.0269 95.7226);
+ --card: oklch(0.9650 0.0080 90);
+ --card-foreground: oklch(0.3438 0.0269 95.7226);
+ --popover: oklch(0.9818 0.0054 95.0986);
+ --popover-foreground: oklch(0.3438 0.0269 95.7226);
+ --primary: oklch(0.6171 0.1375 39.0427);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.9400 0.0120 85);
+ --secondary-foreground: oklch(0.3438 0.0269 95.7226);
+ --muted: oklch(0.9300 0.0100 90);
+ --muted-foreground: oklch(0.5500 0.0200 95);
+ --accent: oklch(0.9200 0.0150 80);
+ --accent-foreground: oklch(0.3438 0.0269 95.7226);
+ --destructive: oklch(0.6188 0.2376 25.7658);
+ --destructive-foreground: oklch(0.985 0 0);
+ --border: oklch(0.8900 0.0180 85);
+ --input: oklch(0.9500 0.0080 90);
+ --ring: oklch(0.6171 0.1375 39.0427);
+ --chart-1: oklch(0.6171 0.1375 39.0427);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.9700 0.0070 92);
+ --sidebar-foreground: oklch(0.3438 0.0269 95.7226);
+ --sidebar-primary: oklch(0.6171 0.1375 39.0427);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.9200 0.0150 80);
+ --sidebar-accent-foreground: oklch(0.3438 0.0269 95.7226);
+ --sidebar-border: oklch(0.8900 0.0180 85);
+ --sidebar-ring: oklch(0.6171 0.1375 39.0427);
+
+ /* Shadow variables - softer for Claude theme */
+ --shadow-sm: 0 1px 2px 0 rgb(139 115 85 / 0.05);
+ --shadow: 0 1px 3px 0 rgb(139 115 85 / 0.08), 0 1px 2px -1px rgb(139 115 85 / 0.06);
+ --shadow-md: 0 4px 6px -1px rgb(139 115 85 / 0.08), 0 2px 4px -2px rgb(139 115 85 / 0.06);
+ --shadow-lg: 0 10px 15px -3px rgb(139 115 85 / 0.08), 0 4px 6px -4px rgb(139 115 85 / 0.06);
+
+ /* Log level colors */
+ --color-log-error: #dc6b52;
+ --color-log-warning: #d9a74a;
+ --color-log-info: #6b9dc4;
+ --color-log-debug: #8b8578;
+ --color-log-success: #6b9e6b;
+
+ /* Status colors for Kanban */
+ --color-status-pending: oklch(0.9200 0.0300 80);
+ --color-status-progress: oklch(0.8800 0.0500 60);
+ --color-status-done: oklch(0.8800 0.0500 140);
+
+ /* Font stacks - system fonts for Claude theme */
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ --font-mono: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas, monospace;
+}
+
+.theme-claude.dark {
+ --background: oklch(0.2679 0.0036 106.6427);
+ --foreground: oklch(0.8074 0.0142 93.0137);
+ --card: oklch(0.3200 0.0050 100);
+ --card-foreground: oklch(0.8074 0.0142 93.0137);
+ --popover: oklch(0.3200 0.0050 100);
+ --popover-foreground: oklch(0.8074 0.0142 93.0137);
+ --primary: oklch(0.6800 0.1500 39);
+ --primary-foreground: oklch(0.15 0 0);
+ --secondary: oklch(0.3500 0.0080 100);
+ --secondary-foreground: oklch(0.8074 0.0142 93.0137);
+ --muted: oklch(0.3800 0.0060 100);
+ --muted-foreground: oklch(0.6500 0.0120 93);
+ --accent: oklch(0.4000 0.0100 90);
+ --accent-foreground: oklch(0.8074 0.0142 93.0137);
+ --destructive: oklch(0.704 0.191 22.216);
+ --destructive-foreground: oklch(0.985 0 0);
+ --border: oklch(0.4200 0.0080 95);
+ --input: oklch(0.3500 0.0050 100);
+ --ring: oklch(0.6800 0.1500 39);
+ --chart-1: oklch(0.6800 0.1500 39);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.2900 0.0040 105);
+ --sidebar-foreground: oklch(0.8074 0.0142 93.0137);
+ --sidebar-primary: oklch(0.6800 0.1500 39);
+ --sidebar-primary-foreground: oklch(0.15 0 0);
+ --sidebar-accent: oklch(0.3800 0.0080 95);
+ --sidebar-accent-foreground: oklch(0.8074 0.0142 93.0137);
+ --sidebar-border: oklch(0.4000 0.0060 100);
+ --sidebar-ring: oklch(0.6800 0.1500 39);
+
+ /* Shadow variables - dark mode */
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.25);
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.35), 0 1px 2px -1px rgb(0 0 0 / 0.25);
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.35), 0 2px 4px -2px rgb(0 0 0 / 0.25);
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.35), 0 4px 6px -4px rgb(0 0 0 / 0.25);
+
+ /* Log level colors for dark mode */
+ --color-log-error: #e8877a;
+ --color-log-warning: #e5be6d;
+ --color-log-info: #8bb5d6;
+ --color-log-debug: #a8a49a;
+ --color-log-success: #8bb58b;
+
+ /* Status colors for Kanban - dark mode */
+ --color-status-pending: oklch(0.3500 0.0300 80);
+ --color-status-progress: oklch(0.4000 0.0500 60);
+ --color-status-done: oklch(0.4000 0.0500 140);
+}
+
+/* ============================================================================
+ Theme: Neo Brutalism
+ Bold colors, hard shadows, no border radius
+ ============================================================================ */
+
+.theme-neo-brutalism {
+ --radius: 0px;
+ --background: oklch(1.0000 0 0);
+ --foreground: oklch(0 0 0);
+ --card: oklch(0.9800 0.0150 95);
+ --card-foreground: oklch(0 0 0);
+ --popover: oklch(1.0000 0 0);
+ --popover-foreground: oklch(0 0 0);
+ --primary: oklch(0.6489 0.2370 26.9728);
+ --primary-foreground: oklch(0 0 0);
+ --secondary: oklch(0.9500 0.1500 100);
+ --secondary-foreground: oklch(0 0 0);
+ --muted: oklch(0.9400 0.0100 90);
+ --muted-foreground: oklch(0.4000 0 0);
+ --accent: oklch(0.8800 0.1800 85);
+ --accent-foreground: oklch(0 0 0);
+ --destructive: oklch(0.6500 0.2500 25);
+ --destructive-foreground: oklch(0 0 0);
+ --border: oklch(0 0 0);
+ --input: oklch(1.0000 0 0);
+ --ring: oklch(0.6489 0.2370 26.9728);
+ --chart-1: oklch(0.6489 0.2370 26.9728);
+ --chart-2: oklch(0.8000 0.2000 130);
+ --chart-3: oklch(0.7000 0.2200 280);
+ --chart-4: oklch(0.8800 0.1800 85);
+ --chart-5: oklch(0.6500 0.2500 330);
+ --sidebar: oklch(0.9500 0.1500 100);
+ --sidebar-foreground: oklch(0 0 0);
+ --sidebar-primary: oklch(0.6489 0.2370 26.9728);
+ --sidebar-primary-foreground: oklch(0 0 0);
+ --sidebar-accent: oklch(0.8800 0.1800 85);
+ --sidebar-accent-foreground: oklch(0 0 0);
+ --sidebar-border: oklch(0 0 0);
+ --sidebar-ring: oklch(0.6489 0.2370 26.9728);
+
+ /* Shadow variables - hard shadows */
+ --shadow-sm: 2px 2px 0px rgb(0 0 0);
+ --shadow: 3px 3px 0px rgb(0 0 0);
+ --shadow-md: 4px 4px 0px rgb(0 0 0);
+ --shadow-lg: 6px 6px 0px rgb(0 0 0);
+
+ /* Log level colors */
+ --color-log-error: #ff0000;
+ --color-log-warning: #ffaa00;
+ --color-log-info: #0066ff;
+ --color-log-debug: #666666;
+ --color-log-success: #00cc00;
+
+ /* Status colors for Kanban */
+ --color-status-pending: oklch(0.9500 0.1500 100);
+ --color-status-progress: oklch(0.8200 0.1800 200);
+ --color-status-done: oklch(0.8000 0.2000 130);
+
+ /* Font stacks - DM Sans for Neo Brutalism */
+ --font-sans: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
+ --font-mono: 'Space Mono', 'JetBrains Mono', monospace;
+}
+
+.theme-neo-brutalism.dark {
+ --background: oklch(0.1200 0 0);
+ --foreground: oklch(1.0000 0 0);
+ --card: oklch(0.1800 0.0080 280);
+ --card-foreground: oklch(1.0000 0 0);
+ --popover: oklch(0.1500 0 0);
+ --popover-foreground: oklch(1.0000 0 0);
+ --primary: oklch(0.7200 0.2500 27);
+ --primary-foreground: oklch(0 0 0);
+ --secondary: oklch(0.4500 0.1200 100);
+ --secondary-foreground: oklch(1.0000 0 0);
+ --muted: oklch(0.2500 0.0050 0);
+ --muted-foreground: oklch(0.6500 0 0);
+ --accent: oklch(0.5500 0.1500 85);
+ --accent-foreground: oklch(0 0 0);
+ --destructive: oklch(0.6500 0.2500 25);
+ --destructive-foreground: oklch(0 0 0);
+ --border: oklch(0.7000 0 0);
+ --input: oklch(0.2000 0 0);
+ --ring: oklch(0.7200 0.2500 27);
+ --chart-1: oklch(0.7200 0.2500 27);
+ --chart-2: oklch(0.7500 0.1800 130);
+ --chart-3: oklch(0.6500 0.2000 280);
+ --chart-4: oklch(0.7000 0.1500 85);
+ --chart-5: oklch(0.6000 0.2200 330);
+ --sidebar: oklch(0.1500 0.0050 280);
+ --sidebar-foreground: oklch(1.0000 0 0);
+ --sidebar-primary: oklch(0.7200 0.2500 27);
+ --sidebar-primary-foreground: oklch(0 0 0);
+ --sidebar-accent: oklch(0.4500 0.1200 85);
+ --sidebar-accent-foreground: oklch(1.0000 0 0);
+ --sidebar-border: oklch(0.5000 0 0);
+ --sidebar-ring: oklch(0.7200 0.2500 27);
+
+ /* Shadow variables - hard shadows for dark mode */
+ --shadow-sm: 2px 2px 0px rgb(255 255 255 / 0.3);
+ --shadow: 3px 3px 0px rgb(255 255 255 / 0.3);
+ --shadow-md: 4px 4px 0px rgb(255 255 255 / 0.3);
+ --shadow-lg: 6px 6px 0px rgb(255 255 255 / 0.3);
+
+ /* Log level colors for dark mode */
+ --color-log-error: #ff4444;
+ --color-log-warning: #ffcc00;
+ --color-log-info: #4499ff;
+ --color-log-debug: #999999;
+ --color-log-success: #44dd44;
+
+ /* Status colors for Kanban - dark mode */
+ --color-status-pending: oklch(0.4500 0.1200 100);
+ --color-status-progress: oklch(0.4500 0.1500 200);
+ --color-status-done: oklch(0.4500 0.1500 130);
+}
+
+/* ============================================================================
+ ShadCN Tailwind v4 Theme Integration
+ ============================================================================ */
+
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
@@ -155,6 +403,10 @@
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
+ --shadow-sm: var(--shadow-sm);
+ --shadow: var(--shadow);
+ --shadow-md: var(--shadow-md);
+ --shadow-lg: var(--shadow-lg);
}
/* ============================================================================
@@ -182,6 +434,11 @@
color: inherit;
font-family: inherit;
}
+
+ /* Smooth theme transitions */
+ :root {
+ transition: background-color 0.2s ease, color 0.2s ease;
+ }
}
/* ============================================================================
@@ -496,6 +753,19 @@
.font-mono {
font-family: var(--font-mono);
}
+
+ /* Neo Brutalism specific utilities */
+ .shadow-neo {
+ box-shadow: var(--shadow-md);
+ }
+
+ .shadow-neo-sm {
+ box-shadow: var(--shadow-sm);
+ }
+
+ .shadow-neo-lg {
+ box-shadow: var(--shadow-lg);
+ }
}
/* ============================================================================