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 (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<DeviceLayout>
|
<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>
|
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-[0.03] pointer-events-none"></div>
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<Routes location={location} key={location.pathname}>
|
<Routes location={location} key={location.pathname}>
|
||||||
|
|
@ -69,7 +69,7 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<DeviceLayout>
|
<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>
|
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-[0.03] pointer-events-none"></div>
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<Routes location={location} key={location.pathname}>
|
<Routes location={location} key={location.pathname}>
|
||||||
|
|
@ -86,7 +86,7 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<DeviceLayout>
|
<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>
|
<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 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
||||||
transition={{ duration: 0.15 }}
|
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">
|
<div className="flex items-center gap-3">
|
||||||
<img
|
<img
|
||||||
src={user?.profileUrl || '/default-profile.png'}
|
src={user?.profileUrl || '/default-profile.png'}
|
||||||
|
|
@ -202,7 +202,7 @@ const Sidebar = ({ isMobile = false }) => {
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setShowProfileMenu(false); navigate('/profile'); }}
|
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} />
|
<User size={16} />
|
||||||
프로필 관리
|
프로필 관리
|
||||||
|
|
@ -290,8 +290,24 @@ const Sidebar = ({ isMobile = false }) => {
|
||||||
</AnimatePresence>
|
</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 ThemeToggle = () => {
|
||||||
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
const getThemeIcon = () => {
|
const getThemeIcon = () => {
|
||||||
if (theme === 'system') return <Monitor size={18} />;
|
if (theme === 'system') return <Monitor size={18} />;
|
||||||
if (theme === 'dark') return <Moon size={18} />;
|
if (theme === 'dark') return <Moon size={18} />;
|
||||||
|
|
@ -303,16 +319,56 @@ const Sidebar = ({ isMobile = false }) => {
|
||||||
if (theme === 'dark') return '다크';
|
if (theme === 'dark') return '다크';
|
||||||
return '라이트';
|
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 (
|
return (
|
||||||
<button
|
<div className="relative px-4 mb-2" ref={themeMenuRef}>
|
||||||
onClick={cycleTheme}
|
<button
|
||||||
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"
|
onClick={() => setShowThemeMenu(!showThemeMenu)}
|
||||||
style={{ width: 'calc(100% - 32px)' }}
|
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()}
|
{getThemeIcon()}
|
||||||
<span className="font-medium">테마: {getThemeLabel()}</span>
|
<span className="font-medium">테마: {getThemeLabel()}</span>
|
||||||
</button>
|
</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>
|
</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">
|
<div className="flex items-center justify-between h-14 px-4">
|
||||||
{/* 햄버거 메뉴 버튼 */}
|
{/* 햄버거 메뉴 버튼 */}
|
||||||
<button
|
<button
|
||||||
|
|
@ -394,10 +450,10 @@ const Sidebar = ({ isMobile = false }) => {
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
||||||
transition={{ duration: 0.15 }}
|
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">
|
<div className="flex items-center gap-3">
|
||||||
<img
|
<img
|
||||||
src={user?.profileUrl || '/default-profile.png'}
|
src={user?.profileUrl || '/default-profile.png'}
|
||||||
|
|
@ -416,7 +472,7 @@ const Sidebar = ({ isMobile = false }) => {
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setShowProfileMenu(false); navigate('/profile'); }}
|
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} />
|
<User size={16} />
|
||||||
프로필 수정
|
프로필 수정
|
||||||
|
|
@ -489,7 +545,7 @@ const Sidebar = ({ isMobile = false }) => {
|
||||||
animate={{ x: 0 }}
|
animate={{ x: 0 }}
|
||||||
exit={{ x: -280 }}
|
exit={{ x: -280 }}
|
||||||
transition={{ type: 'tween', duration: 0.25, ease: 'easeOut' }}
|
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">
|
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||||
|
|
@ -571,7 +627,7 @@ const Sidebar = ({ isMobile = false }) => {
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</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="p-6 border-b border-white/10">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue