# Light/Dark Theme Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Add a light theme and implement dark/light theme switching persisted in localStorage. **Architecture:** CSS variables in `index.css` define light (`:root`) and dark (`.dark`) color sets. A `ThemeProvider` React context toggles the `dark` class on ``. All hardcoded dark color classes in components/pages get replaced with semantic Tailwind classes that map to the CSS variables. **Tech Stack:** React 19, TypeScript, Tailwind CSS v4, CSS Custom Properties (OKLCH) --- ### Task 1: Create ThemeContext provider **Files:** - Create: `frontend/react-shadcn/pc/src/contexts/ThemeContext.tsx` **Step 1: Create ThemeContext** ```tsx import { createContext, useContext, useState, useEffect, type ReactNode } from 'react' type Theme = 'light' | 'dark' interface ThemeContextType { theme: Theme toggleTheme: () => void isDark: boolean } const ThemeContext = createContext(undefined) export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState(() => { const stored = localStorage.getItem('settings:darkMode') return stored === 'false' ? 'light' : 'dark' }) useEffect(() => { const root = document.documentElement if (theme === 'dark') { root.classList.add('dark') } else { root.classList.remove('dark') } localStorage.setItem('settings:darkMode', theme === 'dark' ? 'true' : 'false') }, [theme]) const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark') return ( {children} ) } export function useTheme() { const context = useContext(ThemeContext) if (!context) throw new Error('useTheme must be used within ThemeProvider') return context } ``` **Step 2: Verify file compiles** Run: `cd frontend/react-shadcn/pc && npx tsc --noEmit src/contexts/ThemeContext.tsx` or check dev server for errors. **Step 3: Commit** ```bash git add frontend/react-shadcn/pc/src/contexts/ThemeContext.tsx git commit -m "feat: create ThemeContext provider for light/dark switching" ``` --- ### Task 2: Update CSS variables for dual theme **Files:** - Modify: `frontend/react-shadcn/pc/src/index.css` **Step 1: Restructure CSS variables** Replace the existing `@theme` block and `body` styles. Keep the dark values in `.dark` scope, add light values as `:root` default. The key change: the `@theme` section keeps animation/font/radius vars. Color vars move into `:root` (light) and `.dark` (dark) blocks using standard CSS custom properties. Then add a new `@theme` block that references these custom properties so Tailwind can use them as utilities. ```css @import "tailwindcss"; /* ===== Semantic color tokens ===== */ :root { --bg-page: oklch(0.97 0.003 240); --bg-card: oklch(1.0 0 0); --bg-sidebar: oklch(0.97 0.003 240); --bg-input: oklch(1.0 0 0); --bg-muted: oklch(0.95 0.005 240); --bg-hover: oklch(0.93 0.005 240); --bg-backdrop: rgba(0, 0, 0, 0.4); --text-primary: oklch(0.15 0.01 240); --text-secondary: oklch(0.40 0.01 240); --text-muted: oklch(0.55 0.01 240); --text-inverted: oklch(0.98 0.005 240); --border-primary: oklch(0.88 0.005 240); --border-secondary: oklch(0.82 0.01 240); --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06); --scrollbar-track: oklch(0.95 0.003 240); --scrollbar-thumb: oklch(0.80 0.01 240); --scrollbar-thumb-hover: oklch(0.70 0.01 240); } .dark { --bg-page: oklch(0.10 0.01 240); --bg-card: oklch(0.13 0.01 240); --bg-sidebar: oklch(0.13 0.01 240); --bg-input: oklch(0.13 0.01 240); --bg-muted: oklch(0.18 0.015 240); --bg-hover: oklch(0.20 0.015 240); --bg-backdrop: rgba(0, 0, 0, 0.7); --text-primary: oklch(0.98 0.005 240); --text-secondary: oklch(0.65 0.015 240); --text-muted: oklch(0.50 0.01 240); --text-inverted: oklch(0.15 0.01 240); --border-primary: oklch(0.22 0.015 240); --border-secondary: oklch(0.28 0.02 240); --shadow-card: 0 0 0 transparent; --scrollbar-track: oklch(0.15 0.01 240); --scrollbar-thumb: oklch(0.30 0.02 240); --scrollbar-thumb-hover: oklch(0.35 0.02 240); } @theme { --color-background: var(--bg-page); --color-foreground: var(--text-primary); --color-card: var(--bg-card); --color-card-foreground: var(--text-primary); --color-popover: var(--bg-card); --color-popover-foreground: var(--text-primary); --color-primary: oklch(0.65 0.2 240); --color-primary-foreground: oklch(0.98 0.005 240); --color-secondary: var(--bg-muted); --color-secondary-foreground: var(--text-primary); --color-muted: var(--bg-muted); --color-muted-foreground: var(--text-secondary); --color-accent: var(--bg-muted); --color-accent-foreground: var(--text-primary); --color-destructive: oklch(0.55 0.22 25); --color-destructive-foreground: oklch(0.98 0.005 240); --color-border: var(--border-primary); --color-input: var(--border-primary); --color-ring: oklch(0.65 0.2 240); --color-sidebar-bg: var(--bg-sidebar); --color-surface: var(--bg-card); --color-surface-hover: var(--bg-hover); --color-text-secondary: var(--text-secondary); --color-text-muted: var(--text-muted); --color-border-secondary: var(--border-secondary); --radius: 0.5rem; --font-display: 'Oswald', sans-serif; --font-body: 'Space Grotesk', sans-serif; /* Keep all existing animation keyframes unchanged */ --animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out; --animate-slide-in-left: slide-in-left 0.4s ease-out; --animate-slide-in-right: slide-in-right 0.4s ease-out; --animate-fade-in: fade-in 0.3s ease-out; --animate-scale-in: scale-in 0.2s ease-out; --animate-login-float: login-float 6s ease-in-out infinite; @keyframes login-float { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-20px) scale(1.05); } } @keyframes accordion-down { from { height: 0; } to { height: var(--radix-accordion-content-height); } } @keyframes accordion-up { from { height: var(--radix-accordion-content-height); } to { height: 0; } } @keyframes slide-in-left { from { transform: translateX(-100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slide-in-right { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes scale-in { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } } } body { background-color: var(--bg-page); color: var(--text-primary); font-family: var(--font-body); transition: background-color 0.3s ease, color 0.3s ease; } .dark body { background-image: radial-gradient(circle at 15% 50%, rgba(30, 58, 138, 0.15) 0%, transparent 25%), radial-gradient(circle at 85% 30%, rgba(139, 92, 246, 0.1) 0%, transparent 25%), linear-gradient(rgba(15, 23, 42, 0.95) 1px, transparent 1px), linear-gradient(90deg, rgba(15, 23, 42, 0.95) 1px, transparent 1px); background-size: 100% 100%, 100% 100%, 40px 40px, 40px 40px; } /* Custom scrollbar */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background-color: var(--scrollbar-track); } ::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 0.125rem; } ::-webkit-scrollbar-thumb:hover { background-color: var(--scrollbar-thumb-hover); } /* Keep all existing utility classes (font-display, font-body, grid-pattern, glow-primary, etc.) */ ``` NOTE: Keep all the existing utility classes (.font-display, .font-body, .grid-pattern, .glow-primary, .glow-accent, .stripe-pattern, .tech-border, .scanline, .noise-overlay) unchanged at the bottom of the file. **Step 2: Verify dev server picks up changes** Check browser — dark theme should still look the same (since `.dark` class is applied by ThemeProvider). **Step 3: Commit** ```bash git add frontend/react-shadcn/pc/src/index.css git commit -m "feat: add light/dark CSS variable system" ``` --- ### Task 3: Wire ThemeProvider into App and update SettingsPage **Files:** - Modify: `frontend/react-shadcn/pc/src/App.tsx` - Modify: `frontend/react-shadcn/pc/src/pages/SettingsPage.tsx` **Step 1: Wrap App with ThemeProvider** In `App.tsx`, import `ThemeProvider` and wrap above `AuthProvider`: ```tsx import { ThemeProvider } from './contexts/ThemeContext' // In return: ... ``` **Step 2: Update SettingsPage to use ThemeContext** Replace the `useToggle` for `darkMode` with the `useTheme` hook: ```tsx import { useTheme } from '@/contexts/ThemeContext' // Inside component: const { isDark, toggleTheme } = useTheme() // Replace darkMode toggle usage: // Old: const [darkMode, toggleDarkMode] = useToggle('settings:darkMode', true) // New: use isDark and toggleTheme directly // In the checkbox: ``` Keep the `useToggle` hook for emailNotify and sysNotify — those are unrelated to the theme system. **Step 3: Commit** ```bash git add frontend/react-shadcn/pc/src/App.tsx frontend/react-shadcn/pc/src/pages/SettingsPage.tsx git commit -m "feat: wire ThemeProvider into App and SettingsPage" ``` --- ### Task 4: Update core UI components (Button, Card, Input, Modal, Table) **Files:** - Modify: `frontend/react-shadcn/pc/src/components/ui/Button.tsx` - Modify: `frontend/react-shadcn/pc/src/components/ui/Card.tsx` - Modify: `frontend/react-shadcn/pc/src/components/ui/Input.tsx` - Modify: `frontend/react-shadcn/pc/src/components/ui/Modal.tsx` - Modify: `frontend/react-shadcn/pc/src/components/ui/Table.tsx` **Key changes per component:** **Card.tsx:** - `bg-gray-900/80` → `bg-card shadow-[var(--shadow-card)]` - `text-white` in CardTitle → `text-foreground` - `text-gray-400` in CardDescription → `text-text-secondary` **Button.tsx:** - `secondary`: `bg-gray-800/80 text-gray-200 hover:bg-gray-700/80` → `bg-muted text-foreground hover:bg-surface-hover` - `ghost`: `text-gray-300 hover:bg-gray-800/50` → `text-text-secondary hover:bg-surface-hover` - `outline`: `text-gray-200 hover:bg-gray-800/50 border-gray-700` → `text-foreground hover:bg-surface-hover border-border` - `focus-visible:ring-offset-gray-950` → `focus-visible:ring-offset-background` - primary/destructive gradients: KEEP AS-IS (work on both themes) **Input.tsx:** - `bg-gray-900/80` → `bg-card` - `text-gray-100` → `text-foreground` - `placeholder:text-gray-500` → `placeholder:text-text-muted` - `text-gray-300` (label) → `text-text-secondary` - `text-gray-500` (icon) → `text-text-muted` **Modal.tsx:** - `bg-black/70` → `bg-[var(--bg-backdrop)]` - `bg-gray-900/95` → `bg-card` - `border-gray-800` → `border-border` - `text-white` (title) → `text-foreground` **Table.tsx:** - `border-gray-800` → `border-border` - `text-gray-400` → `text-text-secondary` - `text-gray-300` → `text-foreground` - `hover:bg-gray-800/30` → `hover:bg-surface-hover` - `bg-gray-900/50` → `bg-muted` - `text-gray-500` → `text-text-muted` **Step: Commit** ```bash git add frontend/react-shadcn/pc/src/components/ui/ git commit -m "feat: update core UI components for theme-aware colors" ``` --- ### Task 5: Update layout components (Sidebar, Header, MainLayout) **Files:** - Modify: `frontend/react-shadcn/pc/src/components/layout/Sidebar.tsx` - Modify: `frontend/react-shadcn/pc/src/components/layout/Header.tsx` - Modify: `frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx` **Sidebar.tsx key changes:** - `bg-gray-900/80` → `bg-sidebar-bg` - `border-gray-800` → `border-border` - `text-white` → `text-foreground` - `text-gray-500` → `text-text-muted` - `text-gray-400` → `text-text-secondary` - `hover:bg-gray-800/50` → `hover:bg-surface-hover` - `hover:text-gray-200` → `hover:text-foreground` - `bg-gray-800/50` → `bg-muted` - `bg-gray-800` dropdown → `bg-card` - `border-gray-700` → `border-border-secondary` - Keep all gradient backgrounds (logo, avatar, active nav) — they work on both themes **Header.tsx key changes:** - `bg-gray-900/50` → `bg-card/80` - `border-gray-800` → `border-border` - `text-white` → `text-foreground` - `text-gray-500` → `text-text-muted` **MainLayout.tsx key changes:** - `bg-gray-950` → `bg-background` **Step: Commit** ```bash git add frontend/react-shadcn/pc/src/components/layout/ git commit -m "feat: update layout components for theme-aware colors" ``` --- ### Task 6: Update DashboardPage **Files:** - Modify: `frontend/react-shadcn/pc/src/pages/DashboardPage.tsx` **Key changes:** - All `text-white` → `text-foreground` - All `text-gray-500` → `text-text-muted` - All `text-gray-400` → `text-text-secondary` - `bg-gray-800/50` → `bg-muted` - `hover:bg-gray-800/50` → `hover:bg-surface-hover` - `border-gray-800` → `border-border` - `border-gray-700` → `border-border-secondary` - Keep all status colors (green-500, sky-400, etc.) — work on both themes - Keep gradient bars and chart colors **Step: Commit** ```bash git add frontend/react-shadcn/pc/src/pages/DashboardPage.tsx git commit -m "feat: update DashboardPage for theme-aware colors" ``` --- ### Task 7: Update UserManagementPage **Files:** - Modify: `frontend/react-shadcn/pc/src/pages/UserManagementPage.tsx` **Key changes:** - All `text-white` → `text-foreground` - All `text-gray-400/500` → `text-text-secondary` / `text-text-muted` - `bg-gray-800` select/options → `bg-card` - `border-gray-800` → `border-border` - `border-gray-700` → `border-border-secondary` - `border-gray-600` checkboxes → `border-border-secondary` - Keep all role badge colors (bg-red-500/20, etc.) — work on both themes **Step: Commit** ```bash git add frontend/react-shadcn/pc/src/pages/UserManagementPage.tsx git commit -m "feat: update UserManagementPage for theme-aware colors" ``` --- ### Task 8: Update FileManagementPage **Files:** - Modify: `frontend/react-shadcn/pc/src/pages/FileManagementPage.tsx` **Key changes:** - Same pattern: text-white → text-foreground, gray backgrounds → semantic vars - Upload area: `border-gray-700` → `border-border-secondary`, `bg-white/5` → `bg-muted` - Keep MIME type colors and file status colors **Step: Commit** ```bash git add frontend/react-shadcn/pc/src/pages/FileManagementPage.tsx git commit -m "feat: update FileManagementPage for theme-aware colors" ``` --- ### Task 9: Update MyPage, MenuManagementPage, RoleManagementPage, OrganizationManagementPage **Files:** - Modify: `frontend/react-shadcn/pc/src/pages/MyPage.tsx` - Modify: `frontend/react-shadcn/pc/src/pages/MenuManagementPage.tsx` - Modify: `frontend/react-shadcn/pc/src/pages/RoleManagementPage.tsx` - Modify: `frontend/react-shadcn/pc/src/pages/OrganizationManagementPage.tsx` **Same systematic replacement pattern across all 4 files:** - `text-white` → `text-foreground` - `text-gray-400/500` → `text-text-secondary` / `text-text-muted` - `text-gray-300` → `text-foreground` - `bg-gray-800/50` → `bg-muted` - `bg-gray-800` → `bg-surface` - `bg-white/5` → `bg-muted` - `border-gray-800` → `border-border` - `border-gray-700` → `border-border-secondary` - `border-gray-600` → `border-border-secondary` - `border-white/10` → `border-border` - `hover:bg-gray-800/50` → `hover:bg-surface-hover` - Keep all badge/status colors as-is **Step: Commit** ```bash git add frontend/react-shadcn/pc/src/pages/MyPage.tsx frontend/react-shadcn/pc/src/pages/MenuManagementPage.tsx frontend/react-shadcn/pc/src/pages/RoleManagementPage.tsx frontend/react-shadcn/pc/src/pages/OrganizationManagementPage.tsx git commit -m "feat: update My/Menu/Role/Org pages for theme-aware colors" ``` --- ### Task 10: Update LoginPage and SSOCallbackPage **Files:** - Modify: `frontend/react-shadcn/pc/src/pages/LoginPage.tsx` - Modify: `frontend/react-shadcn/pc/src/pages/SSOCallbackPage.tsx` **LoginPage.tsx — the most complex page:** The left brand panel (gradient) stays the same on both themes — it's a branded element. The right panel needs theme adaptation: - `bg-[oklch(0.10_0.01_240)]` on outer div → `bg-background` - `text-white` (header) → `text-foreground` - `text-gray-400` → `text-text-secondary` - `bg-white/[0.03]` card → In dark mode this is a subtle glass effect. In light mode: `bg-card` with shadow. - Use: `bg-card/80 dark:bg-white/[0.03]` or just `bg-card` with `backdrop-blur-xl` - `border-white/[0.06]` → `border-border` - SSO buttons: `bg-white/[0.03] hover:bg-white/[0.07]` → `bg-muted hover:bg-surface-hover` - `text-gray-300` SSO text → `text-text-secondary` - `text-gray-500` → `text-text-muted` - `text-gray-600` footer → `text-text-muted` - `bg-[oklch(0.12_0.01_240)]` divider bg → `bg-background` - `border-white/[0.06]` divider → `border-border` - `text-indigo-400 hover:text-indigo-300` register link → keep as-is (accent color) **SSOCallbackPage.tsx:** - `bg-[oklch(0.10_0.01_240)]` → `bg-background` - `text-white` → `text-foreground` - `text-gray-400` → `text-text-secondary` **Step: Commit** ```bash git add frontend/react-shadcn/pc/src/pages/LoginPage.tsx frontend/react-shadcn/pc/src/pages/SSOCallbackPage.tsx git commit -m "feat: update LoginPage and SSOCallbackPage for theme-aware colors" ``` --- ### Task 11: Update SettingsPage remaining colors **Files:** - Modify: `frontend/react-shadcn/pc/src/pages/SettingsPage.tsx` **Key changes (in addition to Task 3 ThemeContext integration):** - `text-white` → `text-foreground` - `text-gray-500` → `text-text-muted` - `bg-gray-800/50` toggle containers → `bg-muted` - `bg-gray-700` toggle track → keep or use `bg-border-secondary` - `bg-green-500/10 text-green-400` success → keep - `bg-red-500/10 text-red-400` error → keep **Step: Commit** ```bash git add frontend/react-shadcn/pc/src/pages/SettingsPage.tsx git commit -m "feat: update SettingsPage remaining colors for theme" ``` --- ### Task 12: Update ProtectedRoute and RouteGuard **Files:** - Modify: `frontend/react-shadcn/pc/src/components/layout/ProtectedRoute.tsx` - Modify: `frontend/react-shadcn/pc/src/components/layout/RouteGuard.tsx` **Key changes:** - Spinner colors: `border-sky-500` stays, but `bg-gray-950` → `bg-background` - `text-gray-400` → `text-text-secondary` **Step: Commit** ```bash git add frontend/react-shadcn/pc/src/components/layout/ProtectedRoute.tsx frontend/react-shadcn/pc/src/components/layout/RouteGuard.tsx git commit -m "feat: update ProtectedRoute and RouteGuard for theme" ``` --- ### Task 13: Visual verification and polish **Steps:** 1. Open browser, verify dark theme looks identical to before 2. Toggle to light theme via Settings 3. Check each page: Login, My, Dashboard, Users, Files, Menus, Roles, Orgs, Settings 4. Verify: readable text, proper contrast, no invisible elements 5. Toggle back to dark — confirm everything still works 6. Reload page — confirm theme persists 7. Fix any visual issues found during review **Step: Commit fixes** ```bash git add -A git commit -m "fix: polish light theme visual issues" ``` --- ### Task 14: E2E test for theme switching **Files:** - Update: `frontend/react-shadcn/pc/tests/settings.e2e.test.ts` **Test steps via Playwright:** 1. Login as admin 2. Navigate to /settings 3. Verify dark mode is on (default) 4. Check `` has `dark` class 5. Toggle dark mode off 6. Verify `` no longer has `dark` class 7. Verify body background color changed (light) 8. Reload page 9. Verify light theme persists (no `dark` class) 10. Toggle dark mode back on 11. Verify `` has `dark` class again **Step: Commit** ```bash git add frontend/react-shadcn/pc/tests/settings.e2e.test.ts git commit -m "test: add E2E test for theme switching" ```