You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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/80bg-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/80bg-muted text-foreground hover:bg-surface-hover
  • ghost: text-gray-300 hover:bg-gray-800/50text-text-secondary hover:bg-surface-hover
  • outline: text-gray-200 hover:bg-gray-800/50 border-gray-700text-foreground hover:bg-surface-hover border-border
  • focus-visible:ring-offset-gray-950focus-visible:ring-offset-background
  • primary/destructive gradients: KEEP AS-IS (work on both themes)

Input.tsx:

  • bg-gray-900/80bg-card
  • text-gray-100text-foreground
  • placeholder:text-gray-500placeholder:text-text-muted
  • text-gray-300 (label) → text-text-secondary
  • text-gray-500 (icon) → text-text-muted

Modal.tsx:

  • bg-black/70bg-[var(--bg-backdrop)]
  • bg-gray-900/95bg-card
  • border-gray-800border-border
  • text-white (title) → text-foreground

Table.tsx:

  • border-gray-800border-border
  • text-gray-400text-text-secondary
  • text-gray-300text-foreground
  • hover:bg-gray-800/30hover:bg-surface-hover
  • bg-gray-900/50bg-muted
  • text-gray-500text-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/80bg-sidebar-bg
  • border-gray-800border-border
  • text-whitetext-foreground
  • text-gray-500text-text-muted
  • text-gray-400text-text-secondary
  • hover:bg-gray-800/50hover:bg-surface-hover
  • hover:text-gray-200hover:text-foreground
  • bg-gray-800/50bg-muted
  • bg-gray-800 dropdown → bg-card
  • border-gray-700border-border-secondary
  • Keep all gradient backgrounds (logo, avatar, active nav) — they work on both themes

Header.tsx key changes:

  • bg-gray-900/50bg-card/80
  • border-gray-800border-border
  • text-whitetext-foreground
  • text-gray-500text-text-muted

MainLayout.tsx key changes:

  • bg-gray-950bg-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-whitetext-foreground
  • All text-gray-500text-text-muted
  • All text-gray-400text-text-secondary
  • bg-gray-800/50bg-muted
  • hover:bg-gray-800/50hover:bg-surface-hover
  • border-gray-800border-border
  • border-gray-700border-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-whitetext-foreground
  • All text-gray-400/500text-text-secondary / text-text-muted
  • bg-gray-800 select/options → bg-card
  • border-gray-800border-border
  • border-gray-700border-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

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-700border-border-secondary, bg-white/5bg-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-whitetext-foreground
  • text-gray-400/500text-text-secondary / text-text-muted
  • text-gray-300text-foreground
  • bg-gray-800/50bg-muted
  • bg-gray-800bg-surface
  • bg-white/5bg-muted
  • border-gray-800border-border
  • border-gray-700border-border-secondary
  • border-gray-600border-border-secondary
  • border-white/10border-border
  • hover:bg-gray-800/50hover: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-background
  • text-white (header) → text-foreground
  • text-gray-400text-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-500text-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-whitetext-foreground
  • text-gray-400text-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-whitetext-foreground
  • text-gray-500text-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

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-950bg-background
  • text-gray-400text-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:

  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

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 <html> has dark class
  5. Toggle dark mode off
  6. Verify <html> 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 <html> has dark class again

Step: Commit

git add frontend/react-shadcn/pc/tests/settings.e2e.test.ts
git commit -m "test: add E2E test for theme switching"