feat: 라이트/다크 모드 - 팝업 메뉴 및 핵심 색상 변경
- 테마 토글을 팝업 메뉴로 변경 (시스템/다크/라이트 선택) - App.jsx: 모든 레이아웃을 CSS 변수로 변경 - Sidebar.jsx: 배경색, 프로필 메뉴, 드롭다운을 CSS 변수로 변경 - 라이트 모드에서 기본 배경/텍스트 색상 적용
This commit is contained in:
parent
91d751fc93
commit
47cf949a32
2 changed files with 77 additions and 21 deletions
|
|
@ -49,7 +49,7 @@ function App() {
|
|||
return (
|
||||
<ThemeProvider>
|
||||
<DeviceLayout>
|
||||
<div className="min-h-screen bg-mc-bg text-gray-200 font-sans selection:bg-mc-green/30">
|
||||
<div className="min-h-screen bg-[var(--bg-primary)] text-[var(--text-primary)] font-sans selection:bg-mc-green/30">
|
||||
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-[0.03] pointer-events-none"></div>
|
||||
<AnimatePresence mode="wait">
|
||||
<Routes location={location} key={location.pathname}>
|
||||
|
|
@ -69,7 +69,7 @@ function App() {
|
|||
return (
|
||||
<ThemeProvider>
|
||||
<DeviceLayout>
|
||||
<div className="bg-mc-bg text-gray-200 font-sans selection:bg-mc-green/30">
|
||||
<div className="bg-[var(--bg-primary)] text-[var(--text-primary)] font-sans selection:bg-mc-green/30">
|
||||
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-[0.03] pointer-events-none"></div>
|
||||
<AnimatePresence mode="wait">
|
||||
<Routes location={location} key={location.pathname}>
|
||||
|
|
@ -86,7 +86,7 @@ function App() {
|
|||
return (
|
||||
<ThemeProvider>
|
||||
<DeviceLayout>
|
||||
<div className="min-h-screen bg-mc-bg text-gray-200 font-sans selection:bg-mc-green/30">
|
||||
<div className="min-h-screen bg-[var(--bg-primary)] text-[var(--text-primary)] font-sans selection:bg-mc-green/30">
|
||||
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-[0.03] pointer-events-none"></div>
|
||||
|
||||
{/* 사이드바 + 메인 콘텐츠 레이아웃 */}
|
||||
|
|
|
|||
|
|
@ -175,10 +175,10 @@ const Sidebar = ({ isMobile = false }) => {
|
|||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="absolute bottom-full left-0 right-0 mb-2 bg-zinc-800 border border-zinc-700 rounded-xl shadow-xl overflow-hidden z-50"
|
||||
className="absolute bottom-full left-0 right-0 mb-2 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl shadow-xl overflow-hidden z-50"
|
||||
>
|
||||
{/* 프로필 정보 */}
|
||||
<div className="p-4 bg-zinc-800/80 border-b border-zinc-700">
|
||||
<div className="p-4 bg-[var(--bg-tertiary)] border-b border-[var(--border-color)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={user?.profileUrl || '/default-profile.png'}
|
||||
|
|
@ -202,7 +202,7 @@ const Sidebar = ({ isMobile = false }) => {
|
|||
<div className="py-1">
|
||||
<button
|
||||
onClick={() => { setShowProfileMenu(false); navigate('/profile'); }}
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-zinc-300 hover:bg-zinc-700 hover:text-white transition-colors"
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)] transition-colors"
|
||||
>
|
||||
<User size={16} />
|
||||
프로필 관리
|
||||
|
|
@ -290,8 +290,24 @@ const Sidebar = ({ isMobile = false }) => {
|
|||
</AnimatePresence>
|
||||
);
|
||||
|
||||
// 테마 토글 버튼 컴포넌트
|
||||
// 테마 선택 메뉴 컴포넌트
|
||||
const [showThemeMenu, setShowThemeMenu] = useState(false);
|
||||
const themeMenuRef = useRef(null);
|
||||
|
||||
// 테마 메뉴 외부 클릭 시 닫기
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e) => {
|
||||
if (themeMenuRef.current && !themeMenuRef.current.contains(e.target)) {
|
||||
setShowThemeMenu(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const getThemeIcon = () => {
|
||||
if (theme === 'system') return <Monitor size={18} />;
|
||||
if (theme === 'dark') return <Moon size={18} />;
|
||||
|
|
@ -304,15 +320,55 @@ const Sidebar = ({ isMobile = false }) => {
|
|||
return '라이트';
|
||||
};
|
||||
|
||||
const themeOptions = [
|
||||
{ value: 'system', label: '시스템', icon: <Monitor size={16} /> },
|
||||
{ value: 'dark', label: '다크', icon: <Moon size={16} /> },
|
||||
{ value: 'light', label: '라이트', icon: <Sun size={16} /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative px-4 mb-2" ref={themeMenuRef}>
|
||||
<button
|
||||
onClick={cycleTheme}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 mx-4 mb-2 rounded-xl text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] transition-colors"
|
||||
style={{ width: 'calc(100% - 32px)' }}
|
||||
onClick={() => setShowThemeMenu(!showThemeMenu)}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] transition-colors"
|
||||
>
|
||||
{getThemeIcon()}
|
||||
<span className="font-medium">테마: {getThemeLabel()}</span>
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{showThemeMenu && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="absolute bottom-full left-4 right-4 mb-2 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl shadow-xl overflow-hidden z-50"
|
||||
>
|
||||
{themeOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
setTheme(option.value);
|
||||
setShowThemeMenu(false);
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
|
||||
theme === option.value
|
||||
? 'text-mc-green bg-mc-green/10'
|
||||
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
}`}
|
||||
>
|
||||
{option.icon}
|
||||
{option.label}
|
||||
{theme === option.value && (
|
||||
<span className="ml-auto text-mc-green">✓</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -358,7 +414,7 @@ const Sidebar = ({ isMobile = false }) => {
|
|||
)}
|
||||
</AnimatePresence>
|
||||
{/* 상단 툴바 */}
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-mc-dark/95 backdrop-blur-xl border-b border-white/10 safe-area-top">
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-[var(--bg-secondary)]/95 backdrop-blur-xl border-b border-white/10 safe-area-top">
|
||||
<div className="flex items-center justify-between h-14 px-4">
|
||||
{/* 햄버거 메뉴 버튼 */}
|
||||
<button
|
||||
|
|
@ -394,10 +450,10 @@ const Sidebar = ({ isMobile = false }) => {
|
|||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="absolute top-full right-0 mt-2 w-56 bg-zinc-800 border border-zinc-700 rounded-xl shadow-xl overflow-hidden z-[60]"
|
||||
className="absolute top-full right-0 mt-2 w-56 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl shadow-xl overflow-hidden z-[60]"
|
||||
>
|
||||
{/* 프로필 정보 */}
|
||||
<div className="p-3 bg-zinc-800/80 border-b border-zinc-700">
|
||||
<div className="p-3 bg-[var(--bg-tertiary)] border-b border-[var(--border-color)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={user?.profileUrl || '/default-profile.png'}
|
||||
|
|
@ -416,7 +472,7 @@ const Sidebar = ({ isMobile = false }) => {
|
|||
<div className="py-1">
|
||||
<button
|
||||
onClick={() => { setShowProfileMenu(false); navigate('/profile'); }}
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-zinc-300 hover:bg-zinc-700 hover:text-white transition-colors"
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)] transition-colors"
|
||||
>
|
||||
<User size={16} />
|
||||
프로필 수정
|
||||
|
|
@ -489,7 +545,7 @@ const Sidebar = ({ isMobile = false }) => {
|
|||
animate={{ x: 0 }}
|
||||
exit={{ x: -280 }}
|
||||
transition={{ type: 'tween', duration: 0.25, ease: 'easeOut' }}
|
||||
className="fixed left-0 top-0 bottom-0 w-72 bg-mc-dark/98 backdrop-blur-xl border-r border-white/10 z-[70] flex flex-col safe-area-left"
|
||||
className="fixed left-0 top-0 bottom-0 w-72 bg-[var(--bg-secondary)]/98 backdrop-blur-xl border-r border-white/10 z-[70] flex flex-col safe-area-left"
|
||||
>
|
||||
{/* 사이드바 헤더 */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
|
|
@ -571,7 +627,7 @@ const Sidebar = ({ isMobile = false }) => {
|
|||
)}
|
||||
</AnimatePresence>
|
||||
{/* 데스크탑 사이드바 (항상 표시) */}
|
||||
<aside className="hidden md:flex fixed left-0 top-0 bottom-0 w-64 bg-mc-dark/95 backdrop-blur-xl border-r border-white/10 z-30 flex-col">
|
||||
<aside className="hidden md:flex fixed left-0 top-0 bottom-0 w-64 bg-[var(--bg-secondary)]/95 backdrop-blur-xl border-r border-white/10 z-30 flex-col">
|
||||
{/* 로고 */}
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue