20 KiB
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 <html>. 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
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
type Theme = 'light' | 'dark'
interface ThemeContextType {
theme: Theme
toggleTheme: () => void
isDark: boolean
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>(() => {
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 (
<ThemeContext.Provider value={{ theme, toggleTheme, isDark: theme === 'dark' }}>
{children}
</ThemeContext.Provider>
)
}
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
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.
@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
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:
import { ThemeProvider } from './contexts/ThemeContext'
// In return:
<ThemeProvider>
<AuthProvider>
<BrowserRouter>
...
</BrowserRouter>
</AuthProvider>
</ThemeProvider>
Step 2: Update SettingsPage to use ThemeContext
Replace the useToggle for darkMode with the useTheme hook:
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:
<input type="checkbox" checked={isDark} onChange={toggleTheme} className="sr-only peer" />
Keep the useToggle hook for emailNotify and sysNotify — those are unrelated to the theme system.
Step 3: Commit
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-whitein CardTitle →text-foregroundtext-gray-400in 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-hoverghost:text-gray-300 hover:bg-gray-800/50→text-text-secondary hover:bg-surface-hoveroutline:text-gray-200 hover:bg-gray-800/50 border-gray-700→text-foreground hover:bg-surface-hover border-borderfocus-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-cardtext-gray-100→text-foregroundplaceholder:text-gray-500→placeholder:text-text-mutedtext-gray-300(label) →text-text-secondarytext-gray-500(icon) →text-text-muted
Modal.tsx:
bg-black/70→bg-[var(--bg-backdrop)]bg-gray-900/95→bg-cardborder-gray-800→border-bordertext-white(title) →text-foreground
Table.tsx:
border-gray-800→border-bordertext-gray-400→text-text-secondarytext-gray-300→text-foregroundhover:bg-gray-800/30→hover:bg-surface-hoverbg-gray-900/50→bg-mutedtext-gray-500→text-text-muted
Step: Commit
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-bgborder-gray-800→border-bordertext-white→text-foregroundtext-gray-500→text-text-mutedtext-gray-400→text-text-secondaryhover:bg-gray-800/50→hover:bg-surface-hoverhover:text-gray-200→hover:text-foregroundbg-gray-800/50→bg-mutedbg-gray-800dropdown →bg-cardborder-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/80border-gray-800→border-bordertext-white→text-foregroundtext-gray-500→text-text-muted
MainLayout.tsx key changes:
bg-gray-950→bg-background
Step: Commit
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-mutedhover:bg-gray-800/50→hover:bg-surface-hoverborder-gray-800→border-borderborder-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
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-800select/options →bg-cardborder-gray-800→border-borderborder-gray-700→border-border-secondaryborder-gray-600checkboxes →border-border-secondary- Keep all role badge colors (bg-red-500/20, etc.) — work on both themes
Step: Commit
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
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-foregroundtext-gray-400/500→text-text-secondary/text-text-mutedtext-gray-300→text-foregroundbg-gray-800/50→bg-mutedbg-gray-800→bg-surfacebg-white/5→bg-mutedborder-gray-800→border-borderborder-gray-700→border-border-secondaryborder-gray-600→border-border-secondaryborder-white/10→border-borderhover:bg-gray-800/50→hover:bg-surface-hover- Keep all badge/status colors as-is
Step: Commit
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-backgroundtext-white(header) →text-foregroundtext-gray-400→text-text-secondarybg-white/[0.03]card → In dark mode this is a subtle glass effect. In light mode:bg-cardwith shadow.- Use:
bg-card/80 dark:bg-white/[0.03]or justbg-cardwithbackdrop-blur-xl
- Use:
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-300SSO text →text-text-secondarytext-gray-500→text-text-mutedtext-gray-600footer →text-text-mutedbg-[oklch(0.12_0.01_240)]divider bg →bg-backgroundborder-white/[0.06]divider →border-bordertext-indigo-400 hover:text-indigo-300register link → keep as-is (accent color)
SSOCallbackPage.tsx:
bg-[oklch(0.10_0.01_240)]→bg-backgroundtext-white→text-foregroundtext-gray-400→text-text-secondary
Step: Commit
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-foregroundtext-gray-500→text-text-mutedbg-gray-800/50toggle containers →bg-mutedbg-gray-700toggle track → keep or usebg-border-secondarybg-green-500/10 text-green-400success → keepbg-red-500/10 text-red-400error → keep
Step: Commit
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-500stays, butbg-gray-950→bg-background text-gray-400→text-text-secondary
Step: Commit
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:
- Open browser, verify dark theme looks identical to before
- Toggle to light theme via Settings
- Check each page: Login, My, Dashboard, Users, Files, Menus, Roles, Orgs, Settings
- Verify: readable text, proper contrast, no invisible elements
- Toggle back to dark — confirm everything still works
- Reload page — confirm theme persists
- Fix any visual issues found during review
Step: Commit fixes
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:
- Login as admin
- Navigate to /settings
- Verify dark mode is on (default)
- Check
<html>hasdarkclass - Toggle dark mode off
- Verify
<html>no longer hasdarkclass - Verify body background color changed (light)
- Reload page
- Verify light theme persists (no
darkclass) - Toggle dark mode back on
- Verify
<html>hasdarkclass again
Step: Commit
git add frontend/react-shadcn/pc/tests/settings.e2e.test.ts
git commit -m "test: add E2E test for theme switching"