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:
2025-11-27 23:34:08 +09:00
parent 86fe466b55
commit a7296be512
6 changed files with 1007 additions and 41 deletions

View 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>
);
};