feat: Add theme system with 7 themes including Otaku and Pink Pink
- Implement ThemeContext with CSS variables for dynamic theming - Add theme presets: Cyberpunk, Windows Classic, Dark Modern, Matrix, Industrial, Otaku Mode, Pink Pink - Add theme selector UI in Footer with color preview - Theme persists to localStorage - Each theme has unique background effects, colors, and styling - Otaku Mode: anime-style dark purple with pink/purple glow and star effects - Pink Pink: cute pastel pink with heart pattern overlay 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
320
FrontEnd/contexts/ThemeContext.tsx
Normal file
320
FrontEnd/contexts/ThemeContext.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
|
||||
// Theme types
|
||||
export type ThemeName = 'cyberpunk' | 'windows-classic' | 'dark-modern' | 'matrix' | 'industrial' | 'otaku' | 'pink-pink';
|
||||
|
||||
export interface ThemeColors {
|
||||
primary: string;
|
||||
primaryRgb: string;
|
||||
secondary: string;
|
||||
secondaryRgb: string;
|
||||
accent: string;
|
||||
success: string;
|
||||
error: string;
|
||||
warning: string;
|
||||
bgPrimary: string;
|
||||
bgSecondary: string;
|
||||
bgTertiary: string;
|
||||
textPrimary: string;
|
||||
textSecondary: string;
|
||||
border: string;
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
name: ThemeName;
|
||||
displayName: string;
|
||||
colors: ThemeColors;
|
||||
effects: {
|
||||
glow: boolean;
|
||||
scanlines: boolean;
|
||||
gridBg: boolean;
|
||||
animations: boolean;
|
||||
glassMorphism: boolean;
|
||||
};
|
||||
borderRadius: string;
|
||||
fontStyle: 'tech' | 'classic' | 'modern';
|
||||
}
|
||||
|
||||
// Theme Presets
|
||||
export const themes: Record<ThemeName, Theme> = {
|
||||
'cyberpunk': {
|
||||
name: 'cyberpunk',
|
||||
displayName: 'Cyberpunk',
|
||||
colors: {
|
||||
primary: '#00f3ff',
|
||||
primaryRgb: '0, 243, 255',
|
||||
secondary: '#bc13fe',
|
||||
secondaryRgb: '188, 19, 254',
|
||||
accent: '#ff0099',
|
||||
success: '#0aff00',
|
||||
error: '#ff0055',
|
||||
warning: '#ffe600',
|
||||
bgPrimary: '#020617',
|
||||
bgSecondary: '#0f172a',
|
||||
bgTertiary: '#1e293b',
|
||||
textPrimary: '#f1f5f9',
|
||||
textSecondary: '#94a3b8',
|
||||
border: 'rgba(0, 243, 255, 0.2)',
|
||||
},
|
||||
effects: {
|
||||
glow: true,
|
||||
scanlines: true,
|
||||
gridBg: true,
|
||||
animations: true,
|
||||
glassMorphism: true,
|
||||
},
|
||||
borderRadius: '0px',
|
||||
fontStyle: 'tech',
|
||||
},
|
||||
'windows-classic': {
|
||||
name: 'windows-classic',
|
||||
displayName: 'Windows Classic',
|
||||
colors: {
|
||||
primary: '#000080',
|
||||
primaryRgb: '0, 0, 128',
|
||||
secondary: '#008080',
|
||||
secondaryRgb: '0, 128, 128',
|
||||
accent: '#800000',
|
||||
success: '#008000',
|
||||
error: '#ff0000',
|
||||
warning: '#808000',
|
||||
bgPrimary: '#c0c0c0',
|
||||
bgSecondary: '#dfdfdf',
|
||||
bgTertiary: '#ffffff',
|
||||
textPrimary: '#000000',
|
||||
textSecondary: '#404040',
|
||||
border: '#808080',
|
||||
},
|
||||
effects: {
|
||||
glow: false,
|
||||
scanlines: false,
|
||||
gridBg: false,
|
||||
animations: false,
|
||||
glassMorphism: false,
|
||||
},
|
||||
borderRadius: '0px',
|
||||
fontStyle: 'classic',
|
||||
},
|
||||
'dark-modern': {
|
||||
name: 'dark-modern',
|
||||
displayName: 'Dark Modern',
|
||||
colors: {
|
||||
primary: '#3b82f6',
|
||||
primaryRgb: '59, 130, 246',
|
||||
secondary: '#8b5cf6',
|
||||
secondaryRgb: '139, 92, 246',
|
||||
accent: '#ec4899',
|
||||
success: '#22c55e',
|
||||
error: '#ef4444',
|
||||
warning: '#f59e0b',
|
||||
bgPrimary: '#09090b',
|
||||
bgSecondary: '#18181b',
|
||||
bgTertiary: '#27272a',
|
||||
textPrimary: '#fafafa',
|
||||
textSecondary: '#a1a1aa',
|
||||
border: 'rgba(63, 63, 70, 0.5)',
|
||||
},
|
||||
effects: {
|
||||
glow: false,
|
||||
scanlines: false,
|
||||
gridBg: false,
|
||||
animations: true,
|
||||
glassMorphism: true,
|
||||
},
|
||||
borderRadius: '8px',
|
||||
fontStyle: 'modern',
|
||||
},
|
||||
'matrix': {
|
||||
name: 'matrix',
|
||||
displayName: 'Matrix',
|
||||
colors: {
|
||||
primary: '#00ff00',
|
||||
primaryRgb: '0, 255, 0',
|
||||
secondary: '#00cc00',
|
||||
secondaryRgb: '0, 204, 0',
|
||||
accent: '#00ff00',
|
||||
success: '#00ff00',
|
||||
error: '#ff0000',
|
||||
warning: '#ffff00',
|
||||
bgPrimary: '#000000',
|
||||
bgSecondary: '#0a0a0a',
|
||||
bgTertiary: '#141414',
|
||||
textPrimary: '#00ff00',
|
||||
textSecondary: '#00aa00',
|
||||
border: 'rgba(0, 255, 0, 0.3)',
|
||||
},
|
||||
effects: {
|
||||
glow: true,
|
||||
scanlines: true,
|
||||
gridBg: false,
|
||||
animations: true,
|
||||
glassMorphism: false,
|
||||
},
|
||||
borderRadius: '0px',
|
||||
fontStyle: 'tech',
|
||||
},
|
||||
'industrial': {
|
||||
name: 'industrial',
|
||||
displayName: 'Industrial',
|
||||
colors: {
|
||||
primary: '#f97316',
|
||||
primaryRgb: '249, 115, 22',
|
||||
secondary: '#eab308',
|
||||
secondaryRgb: '234, 179, 8',
|
||||
accent: '#dc2626',
|
||||
success: '#16a34a',
|
||||
error: '#dc2626',
|
||||
warning: '#eab308',
|
||||
bgPrimary: '#1c1917',
|
||||
bgSecondary: '#292524',
|
||||
bgTertiary: '#44403c',
|
||||
textPrimary: '#fafaf9',
|
||||
textSecondary: '#a8a29e',
|
||||
border: 'rgba(249, 115, 22, 0.3)',
|
||||
},
|
||||
effects: {
|
||||
glow: true,
|
||||
scanlines: false,
|
||||
gridBg: false,
|
||||
animations: true,
|
||||
glassMorphism: false,
|
||||
},
|
||||
borderRadius: '4px',
|
||||
fontStyle: 'tech',
|
||||
},
|
||||
'otaku': {
|
||||
name: 'otaku',
|
||||
displayName: 'Otaku Mode',
|
||||
colors: {
|
||||
primary: '#ff6b9d', // 애니메이션 핑크
|
||||
primaryRgb: '255, 107, 157',
|
||||
secondary: '#c084fc', // 보라색
|
||||
secondaryRgb: '192, 132, 252',
|
||||
accent: '#fbbf24', // 골드 (별/하이라이트)
|
||||
success: '#4ade80', // 밝은 그린
|
||||
error: '#f87171', // 소프트 레드
|
||||
warning: '#fcd34d', // 옐로우
|
||||
bgPrimary: '#1a1025', // 다크 퍼플
|
||||
bgSecondary: '#2d1f3d', // 미드 퍼플
|
||||
bgTertiary: '#3d2a54', // 라이트 퍼플
|
||||
textPrimary: '#fdf4ff', // 거의 화이트 (핑크톤)
|
||||
textSecondary: '#d8b4fe', // 라벤더
|
||||
border: 'rgba(255, 107, 157, 0.3)',
|
||||
},
|
||||
effects: {
|
||||
glow: true,
|
||||
scanlines: false,
|
||||
gridBg: true,
|
||||
animations: true,
|
||||
glassMorphism: true,
|
||||
},
|
||||
borderRadius: '12px',
|
||||
fontStyle: 'modern',
|
||||
},
|
||||
'pink-pink': {
|
||||
name: 'pink-pink',
|
||||
displayName: 'Pink Pink',
|
||||
colors: {
|
||||
primary: '#ff69b4', // 핫핑크
|
||||
primaryRgb: '255, 105, 180',
|
||||
secondary: '#ff1493', // 딥핑크
|
||||
secondaryRgb: '255, 20, 147',
|
||||
accent: '#ffb6c1', // 라이트핑크
|
||||
success: '#98fb98', // 페일그린
|
||||
error: '#ff6b6b', // 코랄레드
|
||||
warning: '#ffa07a', // 라이트살몬
|
||||
bgPrimary: '#fff0f5', // 라벤더블러쉬 (밝은 배경)
|
||||
bgSecondary: '#ffe4e9', // 미스티로즈
|
||||
bgTertiary: '#ffccd5', // 핑크
|
||||
textPrimary: '#8b008b', // 다크마젠타
|
||||
textSecondary: '#c71585', // 미디엄바이올렛레드
|
||||
border: 'rgba(255, 105, 180, 0.4)',
|
||||
},
|
||||
effects: {
|
||||
glow: true,
|
||||
scanlines: false,
|
||||
gridBg: false,
|
||||
animations: true,
|
||||
glassMorphism: true,
|
||||
},
|
||||
borderRadius: '16px',
|
||||
fontStyle: 'modern',
|
||||
},
|
||||
};
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
themeName: ThemeName;
|
||||
setTheme: (name: ThemeName) => void;
|
||||
availableThemes: ThemeName[];
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
const [themeName, setThemeName] = useState<ThemeName>(() => {
|
||||
// Load from localStorage on initial render
|
||||
const saved = localStorage.getItem('app-theme');
|
||||
return (saved as ThemeName) || 'cyberpunk';
|
||||
});
|
||||
|
||||
const theme = themes[themeName];
|
||||
|
||||
// Apply CSS variables when theme changes
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
const colors = theme.colors;
|
||||
|
||||
// Set CSS variables
|
||||
root.style.setProperty('--color-primary', colors.primary);
|
||||
root.style.setProperty('--color-primary-rgb', colors.primaryRgb);
|
||||
root.style.setProperty('--color-secondary', colors.secondary);
|
||||
root.style.setProperty('--color-secondary-rgb', colors.secondaryRgb);
|
||||
root.style.setProperty('--color-accent', colors.accent);
|
||||
root.style.setProperty('--color-success', colors.success);
|
||||
root.style.setProperty('--color-error', colors.error);
|
||||
root.style.setProperty('--color-warning', colors.warning);
|
||||
root.style.setProperty('--color-bg-primary', colors.bgPrimary);
|
||||
root.style.setProperty('--color-bg-secondary', colors.bgSecondary);
|
||||
root.style.setProperty('--color-bg-tertiary', colors.bgTertiary);
|
||||
root.style.setProperty('--color-text-primary', colors.textPrimary);
|
||||
root.style.setProperty('--color-text-secondary', colors.textSecondary);
|
||||
root.style.setProperty('--color-border', colors.border);
|
||||
root.style.setProperty('--border-radius', theme.borderRadius);
|
||||
|
||||
// Set data attribute for theme-specific CSS
|
||||
root.setAttribute('data-theme', themeName);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('app-theme', themeName);
|
||||
}, [theme, themeName]);
|
||||
|
||||
const setTheme = (name: ThemeName) => {
|
||||
setThemeName(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
theme,
|
||||
themeName,
|
||||
setTheme,
|
||||
availableThemes: Object.keys(themes) as ThemeName[],
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user