diff options
Diffstat (limited to 'ui/tailwindcss/.claude/commands/setup-dark-mode.md')
| -rw-r--r-- | ui/tailwindcss/.claude/commands/setup-dark-mode.md | 721 |
1 files changed, 721 insertions, 0 deletions
diff --git a/ui/tailwindcss/.claude/commands/setup-dark-mode.md b/ui/tailwindcss/.claude/commands/setup-dark-mode.md new file mode 100644 index 0000000..7b18b13 --- /dev/null +++ b/ui/tailwindcss/.claude/commands/setup-dark-mode.md @@ -0,0 +1,721 @@ +--- +name: setup-dark-mode +description: Set up comprehensive dark mode support with TailwindCSS using CSS variables, theme switching, and system preferences +tools: Write, Edit, Read, Bash +--- + +# Setup Dark Mode with TailwindCSS + +This command sets up a complete dark mode system using TailwindCSS with CSS variables, automatic theme detection, and smooth transitions. + +## What This Command Does + +1. **CSS Variables Configuration** + - Sets up semantic color system using CSS variables + - Configures light and dark theme variants + - Creates smooth transition system between themes + - Implements proper contrast ratios for accessibility + +2. **Theme Configuration** + - Configures TailwindCSS for class-based dark mode + - Sets up color palette using CSS variables + - Creates theme-aware utility classes + - Optimizes for design system consistency + +3. **JavaScript Theme Controller** + - Detects system theme preferences + - Provides manual theme switching functionality + - Persists user theme preferences + - Handles theme transitions smoothly + +4. **Component Integration** + - Creates theme-aware components + - Implements proper dark mode patterns + - Sets up theme toggle components + - Provides theme context for React/Vue apps + +## Configuration Setup + +### TailwindCSS Configuration + +```javascript +// tailwind.config.js +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + './app/**/*.{js,ts,jsx,tsx,mdx}', + './src/**/*.{js,ts,jsx,tsx,mdx}', + ], + darkMode: 'class', // Enable class-based dark mode + theme: { + extend: { + colors: { + // CSS variable-based color system + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + + // Semantic colors + success: { + DEFAULT: 'hsl(var(--success))', + foreground: 'hsl(var(--success-foreground))', + }, + + warning: { + DEFAULT: 'hsl(var(--warning))', + foreground: 'hsl(var(--warning-foreground))', + }, + + info: { + DEFAULT: 'hsl(var(--info))', + foreground: 'hsl(var(--info-foreground))', + }, + }, + + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + + boxShadow: { + 'sm': 'var(--shadow-sm)', + 'DEFAULT': 'var(--shadow)', + 'md': 'var(--shadow-md)', + 'lg': 'var(--shadow-lg)', + 'xl': 'var(--shadow-xl)', + }, + }, + }, + plugins: [], +} +``` + +### CSS Variables Setup + +```css +/* globals.css */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + /* Light theme colors */ + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + /* Semantic colors */ + --success: 142.1 76.2% 36.3%; + --success-foreground: 355.7 100% 97.3%; + + --warning: 32.5 94.6% 43.7%; + --warning-foreground: 26 83.3% 14.1%; + + --info: 217.2 91.2% 59.8%; + --info-foreground: 210 40% 98%; + + /* Design tokens */ + --radius: 0.5rem; + + /* Shadows */ + --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); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + } + + .dark { + /* Dark theme colors */ + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 84% 4.9%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + + /* Semantic colors for dark theme */ + --success: 142.1 70.6% 45.3%; + --success-foreground: 144.9 80.4% 10%; + + --warning: 32.5 94.6% 43.7%; + --warning-foreground: 26 83.3% 14.1%; + + --info: 217.2 91.2% 59.8%; + --info-foreground: 222.2 84% 4.9%; + + /* Dark theme shadows */ + --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); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.4), 0 8px 10px -6px rgb(0 0 0 / 0.3); + } + + /* Global base styles */ + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + font-feature-settings: "rlig" 1, "calt" 1; + } + + /* Smooth theme transitions */ + html { + transition: color-scheme 0.2s ease-in-out; + } + + * { + transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out; + } + + /* Focus styles */ + .focus-visible { + @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2; + } +} + +/* Custom scrollbar for dark mode */ +@layer utilities { + .scrollbar-thin { + scrollbar-width: thin; + } + + .scrollbar-track-transparent { + scrollbar-color: hsl(var(--muted)) transparent; + } + + .dark .scrollbar-track-transparent { + scrollbar-color: hsl(var(--muted)) transparent; + } +} +``` + +## Theme Management + +### JavaScript Theme Controller + +```javascript +// lib/theme.js +class ThemeManager { + constructor() { + this.theme = 'system' + this.systemTheme = 'light' + this.init() + } + + init() { + // Get stored theme or default to system + this.theme = localStorage.getItem('theme') || 'system' + + // Listen for system theme changes + this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + this.systemTheme = this.mediaQuery.matches ? 'dark' : 'light' + + this.mediaQuery.addEventListener('change', (e) => { + this.systemTheme = e.matches ? 'dark' : 'light' + if (this.theme === 'system') { + this.applyTheme() + } + }) + + // Apply initial theme + this.applyTheme() + } + + setTheme(theme) { + this.theme = theme + localStorage.setItem('theme', theme) + this.applyTheme() + this.notifyListeners() + } + + applyTheme() { + const root = document.documentElement + const isDark = this.theme === 'dark' || (this.theme === 'system' && this.systemTheme === 'dark') + + if (isDark) { + root.classList.add('dark') + root.style.colorScheme = 'dark' + } else { + root.classList.remove('dark') + root.style.colorScheme = 'light' + } + } + + getTheme() { + return this.theme + } + + getEffectiveTheme() { + return this.theme === 'system' ? this.systemTheme : this.theme + } + + // Event listener system + listeners = new Set() + + subscribe(callback) { + this.listeners.add(callback) + return () => this.listeners.delete(callback) + } + + notifyListeners() { + this.listeners.forEach(callback => { + callback({ + theme: this.theme, + effectiveTheme: this.getEffectiveTheme() + }) + }) + } +} + +// Create global instance +const themeManager = new ThemeManager() + +export { themeManager } +``` + +### React Theme Hook + +```jsx +// hooks/useTheme.js +import { useState, useEffect } from 'react' +import { themeManager } from '@/lib/theme' + +export function useTheme() { + const [theme, setThemeState] = useState(themeManager.getTheme()) + const [effectiveTheme, setEffectiveTheme] = useState(themeManager.getEffectiveTheme()) + + useEffect(() => { + const unsubscribe = themeManager.subscribe(({ theme, effectiveTheme }) => { + setThemeState(theme) + setEffectiveTheme(effectiveTheme) + }) + + return unsubscribe + }, []) + + const setTheme = (newTheme) => { + themeManager.setTheme(newTheme) + } + + return { + theme, + effectiveTheme, + setTheme, + themes: ['light', 'dark', 'system'] + } +} +``` + +### React Theme Provider + +```jsx +// providers/ThemeProvider.jsx +import React, { createContext, useContext, useEffect, useState } from 'react' + +const ThemeProviderContext = createContext({ + theme: 'system', + setTheme: () => null, +}) + +export function ThemeProvider({ children, defaultTheme = 'system' }) { + const [theme, setTheme] = useState(() => { + if (typeof window !== 'undefined') { + return localStorage.getItem('theme') || defaultTheme + } + return defaultTheme + }) + + useEffect(() => { + const root = window.document.documentElement + root.classList.remove('light', 'dark') + + if (theme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' + root.classList.add(systemTheme) + return + } + + root.classList.add(theme) + }, [theme]) + + const value = { + theme, + setTheme: (theme) => { + localStorage.setItem('theme', theme) + setTheme(theme) + }, + } + + return ( + <ThemeProviderContext.Provider value={value}> + {children} + </ThemeProviderContext.Provider> + ) +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext) + + if (context === undefined) + throw new Error('useTheme must be used within a ThemeProvider') + + return context +} +``` + +## Theme Toggle Components + +### Simple Theme Toggle + +```jsx +// components/ThemeToggle.jsx +import React from 'react' +import { Moon, Sun } from 'lucide-react' +import { useTheme } from '@/hooks/useTheme' +import { Button } from '@/components/ui/Button' + +export function ThemeToggle() { + const { effectiveTheme, setTheme } = useTheme() + + const toggleTheme = () => { + setTheme(effectiveTheme === 'light' ? 'dark' : 'light') + } + + return ( + <Button + variant="ghost" + size="icon" + onClick={toggleTheme} + className="relative" + aria-label="Toggle theme" + > + <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> + <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> + </Button> + ) +} +``` + +### Advanced Theme Selector + +```jsx +// components/ThemeSelector.jsx +import React from 'react' +import { Monitor, Moon, Sun } from 'lucide-react' +import { useTheme } from '@/hooks/useTheme' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/DropdownMenu' +import { Button } from '@/components/ui/Button' + +export function ThemeSelector() { + const { theme, setTheme } = useTheme() + + const themes = [ + { value: 'light', label: 'Light', icon: Sun }, + { value: 'dark', label: 'Dark', icon: Moon }, + { value: 'system', label: 'System', icon: Monitor }, + ] + + const currentTheme = themes.find(t => t.value === theme) + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" className="w-full justify-start"> + <currentTheme.icon className="mr-2 h-4 w-4" /> + {currentTheme.label} + </Button> + </DropdownMenuTrigger> + + <DropdownMenuContent align="end"> + {themes.map(({ value, label, icon: Icon }) => ( + <DropdownMenuItem + key={value} + onClick={() => setTheme(value)} + className="cursor-pointer" + > + <Icon className="mr-2 h-4 w-4" /> + {label} + {theme === value && ( + <span className="ml-auto">✓</span> + )} + </DropdownMenuItem> + ))} + </DropdownMenuContent> + </DropdownMenu> + ) +} +``` + +### Animated Theme Toggle + +```jsx +// components/AnimatedThemeToggle.jsx +import React from 'react' +import { useTheme } from '@/hooks/useTheme' +import { cn } from '@/lib/utils' + +export function AnimatedThemeToggle() { + const { effectiveTheme, setTheme } = useTheme() + const isDark = effectiveTheme === 'dark' + + const toggleTheme = () => { + setTheme(isDark ? 'light' : 'dark') + } + + return ( + <button + onClick={toggleTheme} + className={cn( + 'relative inline-flex h-12 w-12 items-center justify-center rounded-full', + 'bg-background border-2 border-border shadow-lg', + 'transition-all duration-300 ease-in-out', + 'hover:scale-110 hover:shadow-xl', + 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2' + )} + aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`} + > + <div className="relative h-6 w-6 overflow-hidden"> + {/* Sun icon */} + <svg + className={cn( + 'absolute inset-0 h-6 w-6 text-yellow-500 transition-all duration-300', + isDark ? 'rotate-90 scale-0 opacity-0' : 'rotate-0 scale-100 opacity-100' + )} + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" + /> + </svg> + + {/* Moon icon */} + <svg + className={cn( + 'absolute inset-0 h-6 w-6 text-blue-400 transition-all duration-300', + isDark ? 'rotate-0 scale-100 opacity-100' : '-rotate-90 scale-0 opacity-0' + )} + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" + /> + </svg> + </div> + </button> + ) +} +``` + +## Theme-Aware Components + +### Dark Mode Image Component + +```jsx +// components/ThemeAwareImage.jsx +import React from 'react' +import { useTheme } from '@/hooks/useTheme' + +export function ThemeAwareImage({ + lightSrc, + darkSrc, + alt, + className, + ...props +}) { + const { effectiveTheme } = useTheme() + const src = effectiveTheme === 'dark' ? darkSrc : lightSrc + + return ( + <img + src={src} + alt={alt} + className={className} + {...props} + /> + ) +} +``` + +### Theme Detection Script + +```html +<!-- Add to document head for no-flash theme detection --> +<script> + (function() { + const theme = localStorage.getItem('theme') + const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + + if (theme === 'dark' || (!theme && systemPrefersDark)) { + document.documentElement.classList.add('dark') + document.documentElement.style.colorScheme = 'dark' + } else { + document.documentElement.classList.remove('dark') + document.documentElement.style.colorScheme = 'light' + } + })() +</script> +``` + +## Testing Dark Mode + +### Dark Mode Test Suite + +```javascript +// tests/dark-mode.test.js +import { render, screen, fireEvent } from '@testing-library/react' +import { ThemeProvider } from '@/providers/ThemeProvider' +import { ThemeToggle } from '@/components/ThemeToggle' + +describe('Dark Mode', () => { + beforeEach(() => { + localStorage.clear() + document.documentElement.className = '' + }) + + test('applies dark mode class when theme is dark', () => { + render( + <ThemeProvider defaultTheme="dark"> + <div>Test content</div> + </ThemeProvider> + ) + + expect(document.documentElement).toHaveClass('dark') + }) + + test('toggles theme when button is clicked', () => { + render( + <ThemeProvider> + <ThemeToggle /> + </ThemeProvider> + ) + + const toggleButton = screen.getByLabelText(/toggle theme/i) + fireEvent.click(toggleButton) + + expect(document.documentElement).toHaveClass('dark') + }) + + test('persists theme preference', () => { + render( + <ThemeProvider> + <ThemeToggle /> + </ThemeProvider> + ) + + const toggleButton = screen.getByLabelText(/toggle theme/i) + fireEvent.click(toggleButton) + + expect(localStorage.getItem('theme')).toBe('dark') + }) +}) +``` + +Remember: **Dark mode should enhance user experience with proper contrast ratios, smooth transitions, and respect for user preferences!** |
