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

@@ -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>

View File

@@ -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"

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">

View 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
View 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]
}
}

View File

@@ -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);
}
}
/* ============================================================================