mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-02-01 15:03:36 +00:00
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:
124
ui/src/hooks/useTheme.ts
Normal file
124
ui/src/hooks/useTheme.ts
Normal file
@@ -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<ThemeId>(() => {
|
||||
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]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user