101 lines
3.5 KiB
TypeScript
101 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { useTheme } from "next-themes";
|
|
import { Check, Moon, Sun } from "lucide-react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
} from "@/components/ui/dialog";
|
|
import { useColorTheme, COLOR_THEMES, type ColorTheme } from "@/hooks/useColorTheme";
|
|
import { animatedThemeChange } from "@/lib/themeTransition";
|
|
import { animatedColorChange } from "@/lib/colorTransition";
|
|
|
|
interface SettingsModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
}
|
|
|
|
export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
|
const { color, setColor } = useColorTheme();
|
|
const { theme, setTheme } = useTheme();
|
|
const isDark = theme === "dark";
|
|
|
|
const handleModeClick = (next: "light" | "dark", e: React.MouseEvent) => {
|
|
if (next === theme) return;
|
|
animatedThemeChange(next, setTheme, { x: e.clientX, y: e.clientY });
|
|
};
|
|
|
|
const handleColorClick = (id: ColorTheme, e: React.MouseEvent<HTMLButtonElement>) => {
|
|
if (id === color) return;
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
const cx = rect.left + rect.width / 2;
|
|
const cy = rect.top + rect.height / 2;
|
|
const meta = COLOR_THEMES.find((c) => c.id === id);
|
|
const swatchColor = (isDark ? meta?.dark : meta?.light) ?? "#6c5ce7";
|
|
animatedColorChange(id, () => setColor(id), { x: cx, y: cy, color: swatchColor });
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-[480px]">
|
|
<DialogHeader>
|
|
<DialogTitle>설정</DialogTitle>
|
|
<DialogDescription>화면 테마와 색상을 변경합니다.</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{/* === 모드 (라이트/다크) === */}
|
|
<div className="settings-section">
|
|
<div className="settings-label">화면 모드</div>
|
|
<div className="settings-mode-row">
|
|
<button
|
|
type="button"
|
|
className={`settings-mode-btn ${!isDark ? "on" : ""}`}
|
|
onClick={(e) => handleModeClick("light", e)}
|
|
>
|
|
<Sun size={14} />
|
|
<span>라이트</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`settings-mode-btn ${isDark ? "on" : ""}`}
|
|
onClick={(e) => handleModeClick("dark", e)}
|
|
>
|
|
<Moon size={14} />
|
|
<span>다크</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* === 색상 테마 === */}
|
|
<div className="settings-section">
|
|
<div className="settings-label">색상 테마</div>
|
|
<div className="settings-color-grid">
|
|
{COLOR_THEMES.map((c) => {
|
|
const swatch = isDark ? c.dark : c.light;
|
|
const isActive = color === c.id;
|
|
return (
|
|
<button
|
|
key={c.id}
|
|
type="button"
|
|
className={`settings-color-swatch ${isActive ? "on" : ""}`}
|
|
onClick={(e) => handleColorClick(c.id as ColorTheme, e)}
|
|
title={c.label}
|
|
aria-label={c.label}
|
|
>
|
|
<span className="swatch-circle" style={{ background: swatch }}>
|
|
{isActive && <Check size={12} strokeWidth={3} />}
|
|
</span>
|
|
<span className="swatch-label">{c.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|