From a7296be5120f4a351484bb6bfb9b37b5ce362979 Mon Sep 17 00:00:00 2001 From: arDTDev Date: Thu, 27 Nov 2025 23:34:08 +0900 Subject: [PATCH] feat: Add theme system with 7 themes including Otaku and Pink Pink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- FrontEnd/App.tsx | 11 +- FrontEnd/components/layout/Footer.tsx | 124 ++++++- FrontEnd/components/layout/Layout.tsx | 103 +++++- FrontEnd/contexts/ThemeContext.tsx | 320 ++++++++++++++++++ FrontEnd/index.css | 456 +++++++++++++++++++++++++- FrontEnd/tailwind.config.js | 34 +- 6 files changed, 1007 insertions(+), 41 deletions(-) create mode 100644 FrontEnd/contexts/ThemeContext.tsx diff --git a/FrontEnd/App.tsx b/FrontEnd/App.tsx index 7e84094..45aed04 100644 --- a/FrontEnd/App.tsx +++ b/FrontEnd/App.tsx @@ -8,6 +8,7 @@ import { HistoryPage } from './pages/HistoryPage'; import { SystemState, Recipe, IOPoint, LogEntry, RobotTarget, ConfigItem } from './types'; import { comms } from './communication'; import { AlertProvider, useAlert } from './contexts/AlertContext'; +import { ThemeProvider } from './contexts/ThemeContext'; import { PickerMoveDialog } from './components/PickerMoveDialog'; // PickerMoveDialog ์ „์—ญ ์ƒํƒœ Context @@ -329,11 +330,13 @@ function AppContent() { ); } -// ์™ธ๋ถ€ App ์ปดํฌ๋„ŒํŠธ - AlertProvider๋กœ ๊ฐ์‹ธ๊ธฐ +// ์™ธ๋ถ€ App ์ปดํฌ๋„ŒํŠธ - ThemeProvider + AlertProvider๋กœ ๊ฐ์‹ธ๊ธฐ export default function App() { return ( - - - + + + + + ); } diff --git a/FrontEnd/components/layout/Footer.tsx b/FrontEnd/components/layout/Footer.tsx index 37b1cac..5b6fe2b 100644 --- a/FrontEnd/components/layout/Footer.tsx +++ b/FrontEnd/components/layout/Footer.tsx @@ -1,6 +1,8 @@ import React, { useState, useEffect, useMemo } from 'react'; +import { Palette, ChevronUp } from 'lucide-react'; import { RobotTarget } from '../../types'; import { comms } from '../../communication'; +import { useTheme, themes, ThemeName } from '../../contexts/ThemeContext'; // HW ์ƒํƒœ ํƒ€์ž… (์œˆํผ HWState์™€ ๋™์ผ) // status: 0=SET(๋ฏธ์„ค์ •/ํšŒ์ƒ‰), 1=ON(์—ฐ๊ฒฐ/๋…น์ƒ‰), 2=TRIG(ํŠธ๋ฆฌ๊ฑฐ/๋…ธ๋ž€์ƒ‰), 3=OFF(์—ฐ๊ฒฐ์•ˆ๋จ/๋นจ๊ฐ„์ƒ‰) @@ -38,6 +40,8 @@ const getStatusColor = (status: number): { bg: string; shadow: string; text: str export const Footer: React.FC = ({ isHostConnected, robotTarget }) => { const [hwStatus, setHwStatus] = useState([]); + const [showThemeSelector, setShowThemeSelector] = useState(false); + const { theme, themeName, setTheme, availableThemes } = useTheme(); useEffect(() => { // HW_STATUS_UPDATE ์ด๋ฒคํŠธ ๊ตฌ๋… @@ -75,6 +79,44 @@ export const Footer: React.FC = ({ isHostConnected, robotTarget }) return errors.sort((a, b) => a.priority - b.priority)[0]; }, [hwStatus]); + // ํ…Œ๋งˆ ์„ ํƒ ํŒ์—…์„ ๋‹ซ๊ธฐ ์œ„ํ•œ ์™ธ๋ถ€ ํด๋ฆญ ๊ฐ์ง€ + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (!target.closest('.theme-selector')) { + setShowThemeSelector(false); + } + }; + + if (showThemeSelector) { + document.addEventListener('click', handleClickOutside); + } + + return () => { + document.removeEventListener('click', handleClickOutside); + }; + }, [showThemeSelector]); + + // ํ…Œ๋งˆ๋ณ„ ๋ฐฐ๊ฒฝ์ƒ‰ ์Šคํƒ€์ผ (Windows Classic์€ ํŠน๋ณ„ ์ฒ˜๋ฆฌ) + const getFooterBgClass = () => { + switch (themeName) { + case 'windows-classic': + return 'bg-[#c0c0c0] border-t-2 border-white shadow-[inset_0_-1px_0_#808080]'; + case 'dark-modern': + return 'bg-zinc-900/90 border-t border-zinc-700/50'; + case 'matrix': + return 'bg-black/95 border-t border-green-500/30'; + case 'industrial': + return 'bg-stone-900/95 border-t border-orange-500/30'; + case 'otaku': + return 'bg-[#1a1025]/95 border-t border-pink-500/30 shadow-[0_-2px_20px_rgba(255,107,157,0.1)]'; + case 'pink-pink': + return 'bg-[#ffe4e9]/95 border-t-2 border-pink-300 shadow-[0_-2px_10px_rgba(255,105,180,0.2)]'; + default: + return 'bg-black/80 border-t border-neon-blue/30'; + } + }; + return ( <> {/* ํ•˜๋“œ์›จ์–ด ์˜ค๋ฅ˜ ํ‘œ์‹œ ๋ฐฐ๋„ˆ - ํ™”๋ฉด ์ค‘์•™ ์ƒ๋‹จ์— ํฌ๊ฒŒ ํ‘œ์‹œ */} @@ -94,8 +136,8 @@ export const Footer: React.FC = ({ isHostConnected, robotTarget }) )} -