mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +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:
@@ -7,7 +7,7 @@
|
||||
<title>AutoCoder</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Work+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Work+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useProjects, useFeatures, useAgentStatus, useSettings } from './hooks/u
|
||||
import { useProjectWebSocket } from './hooks/useWebSocket'
|
||||
import { useFeatureSound } from './hooks/useFeatureSound'
|
||||
import { useCelebration } from './hooks/useCelebration'
|
||||
import { useTheme } from './hooks/useTheme'
|
||||
import { ProjectSelector } from './components/ProjectSelector'
|
||||
import { KanbanBoard } from './components/KanbanBoard'
|
||||
import { AgentControl } from './components/AgentControl'
|
||||
@@ -24,6 +25,7 @@ import { DevServerControl } from './components/DevServerControl'
|
||||
import { ViewToggle, type ViewMode } from './components/ViewToggle'
|
||||
import { DependencyGraph } from './components/DependencyGraph'
|
||||
import { KeyboardShortcutsHelp } from './components/KeyboardShortcutsHelp'
|
||||
import { ThemeSelector } from './components/ThemeSelector'
|
||||
import { getDependencyGraph } from './lib/api'
|
||||
import { Loader2, Settings, Moon, Sun } from 'lucide-react'
|
||||
import type { Feature } from './lib/types'
|
||||
@@ -32,7 +34,6 @@ import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
const STORAGE_KEY = 'autocoder-selected-project'
|
||||
const DARK_MODE_KEY = 'autocoder-dark-mode'
|
||||
const VIEW_MODE_KEY = 'autocoder-view-mode'
|
||||
|
||||
function App() {
|
||||
@@ -56,13 +57,6 @@ function App() {
|
||||
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false)
|
||||
const [isSpecCreating, setIsSpecCreating] = useState(false)
|
||||
const [showSpecChat, setShowSpecChat] = useState(false) // For "Create Spec" button in empty kanban
|
||||
const [darkMode, setDarkMode] = useState(() => {
|
||||
try {
|
||||
return localStorage.getItem(DARK_MODE_KEY) === 'true'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(VIEW_MODE_KEY)
|
||||
@@ -78,6 +72,7 @@ function App() {
|
||||
const { data: settings } = useSettings()
|
||||
useAgentStatus(selectedProject) // Keep polling for status updates
|
||||
const wsState = useProjectWebSocket(selectedProject)
|
||||
const { theme, setTheme, darkMode, toggleDarkMode, themes } = useTheme()
|
||||
|
||||
// Get has_spec from the selected project
|
||||
const selectedProjectData = projects?.find(p => p.name === selectedProject)
|
||||
@@ -91,20 +86,6 @@ function App() {
|
||||
refetchInterval: 5000, // Refresh every 5 seconds
|
||||
})
|
||||
|
||||
// Apply dark mode class to document
|
||||
useEffect(() => {
|
||||
if (darkMode) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
try {
|
||||
localStorage.setItem(DARK_MODE_KEY, String(darkMode))
|
||||
} catch {
|
||||
// localStorage not available
|
||||
}
|
||||
}, [darkMode])
|
||||
|
||||
// Persist view mode to localStorage
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -325,9 +306,16 @@ function App() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Theme selector */}
|
||||
<ThemeSelector
|
||||
themes={themes}
|
||||
currentTheme={theme}
|
||||
onThemeChange={setTheme}
|
||||
/>
|
||||
|
||||
{/* Dark mode toggle - always visible */}
|
||||
<Button
|
||||
onClick={() => setDarkMode(!darkMode)}
|
||||
onClick={toggleDarkMode}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
title="Toggle dark mode"
|
||||
|
||||
@@ -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">
|
||||
|
||||
155
ui/src/components/ThemeSelector.tsx
Normal file
155
ui/src/components/ThemeSelector.tsx
Normal file
@@ -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<ThemeId | null>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
title="Theme"
|
||||
aria-label="Select theme"
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<Palette size={18} />
|
||||
</Button>
|
||||
|
||||
{/* Dropdown */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="absolute right-0 top-full mt-2 w-56 bg-popover border-2 border-border rounded-lg shadow-lg z-50 animate-slide-in-down overflow-hidden"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
>
|
||||
<div className="p-2 space-y-1">
|
||||
{themes.map((theme) => (
|
||||
<button
|
||||
key={theme.id}
|
||||
onClick={() => handleThemeClick(theme.id)}
|
||||
onMouseEnter={() => handleThemeHover(theme.id)}
|
||||
onMouseLeave={() => setPreviewTheme(null)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-md text-left transition-colors ${
|
||||
currentTheme === theme.id
|
||||
? 'bg-primary/10 text-foreground'
|
||||
: 'hover:bg-muted text-foreground'
|
||||
}`}
|
||||
role="menuitem"
|
||||
>
|
||||
{/* Color swatches */}
|
||||
<div className="flex gap-0.5 shrink-0">
|
||||
<div
|
||||
className="w-4 h-4 rounded-sm border border-border/50"
|
||||
style={{ backgroundColor: theme.previewColors.background }}
|
||||
/>
|
||||
<div
|
||||
className="w-4 h-4 rounded-sm border border-border/50"
|
||||
style={{ backgroundColor: theme.previewColors.primary }}
|
||||
/>
|
||||
<div
|
||||
className="w-4 h-4 rounded-sm border border-border/50"
|
||||
style={{ backgroundColor: theme.previewColors.accent }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Theme name and description */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm">{theme.name}</div>
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
{theme.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Checkmark for current theme */}
|
||||
{currentTheme === theme.id && (
|
||||
<Check size={16} className="text-primary shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
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]
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user