183 lines
6.0 KiB
TypeScript
183 lines
6.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect } from "react";
|
|
import { useTheme } from "next-themes";
|
|
import { Check, Moon, Sun, X, PanelLeftClose, PanelLeft, AlignJustify, AlignHorizontalJustifyCenter } from "lucide-react";
|
|
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;
|
|
sidebarCollapsed?: boolean;
|
|
onSidebarCollapsedChange?: (collapsed: boolean) => void;
|
|
navOrientation?: "vertical" | "horizontal";
|
|
onNavOrientationChange?: (next: "vertical" | "horizontal") => void;
|
|
}
|
|
|
|
/**
|
|
* Tweaks panel — 디자인시스템 `Tweaks` (app.jsx) 포팅.
|
|
* 중앙 모달 → 우하단 플로팅 240px 패널로 변경 (2026-04-21 재단).
|
|
* 키워드 "설정" 유지 (파일명/컴포넌트명). 외부 API 는 props.open/onOpenChange 기존과 동일.
|
|
*/
|
|
export function SettingsModal({
|
|
open,
|
|
onOpenChange,
|
|
sidebarCollapsed,
|
|
onSidebarCollapsedChange,
|
|
navOrientation,
|
|
onNavOrientationChange,
|
|
}: SettingsModalProps) {
|
|
const { color, setColor } = useColorTheme();
|
|
const { theme, setTheme } = useTheme();
|
|
const isDark = theme === "dark";
|
|
|
|
// Escape 로 닫기
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") onOpenChange(false);
|
|
};
|
|
window.addEventListener("keydown", onKey);
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
}, [open, onOpenChange]);
|
|
|
|
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 (
|
|
<div
|
|
className={`v5-tweaks-panel${open ? " on" : ""}`}
|
|
role="dialog"
|
|
aria-label="Tweaks"
|
|
aria-hidden={!open}
|
|
>
|
|
<div className="v5-tweaks-head">
|
|
<span>Tweaks</span>
|
|
<button
|
|
className="v5-hdr-icon"
|
|
onClick={() => onOpenChange(false)}
|
|
aria-label="Tweaks 닫기"
|
|
style={{ width: 24, height: 24, borderRadius: 8 }}
|
|
>
|
|
<X size={13} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* === 테마 컬러 === */}
|
|
<div className="v5-tweaks-row">
|
|
<label>테마 컬러</label>
|
|
<div className="v5-tweaks-swatches">
|
|
{COLOR_THEMES.map((c) => {
|
|
const swatch = isDark ? c.dark : c.light;
|
|
const isActive = color === c.id;
|
|
return (
|
|
<button
|
|
key={c.id}
|
|
type="button"
|
|
className={`v5-tweaks-swatch${isActive ? " on" : ""}`}
|
|
onClick={(e) => handleColorClick(c.id as ColorTheme, e)}
|
|
title={c.label}
|
|
aria-label={c.label}
|
|
style={{ background: swatch }}
|
|
>
|
|
{isActive && <Check size={11} strokeWidth={3.5} />}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* === 모드 === */}
|
|
<div className="v5-tweaks-row">
|
|
<label>모드</label>
|
|
<div className="v5-tweaks-seg">
|
|
<button
|
|
type="button"
|
|
className={`v5-btn ${!isDark ? "primary" : "secondary"} sm`}
|
|
onClick={(e) => handleModeClick("light", e)}
|
|
>
|
|
<Sun size={12} />
|
|
라이트
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`v5-btn ${isDark ? "primary" : "secondary"} sm`}
|
|
onClick={(e) => handleModeClick("dark", e)}
|
|
>
|
|
<Moon size={12} />
|
|
다크
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* === 메뉴 방향 === */}
|
|
{onNavOrientationChange && (
|
|
<div className="v5-tweaks-row">
|
|
<label>메뉴 방향</label>
|
|
<div className="v5-tweaks-seg">
|
|
<button
|
|
type="button"
|
|
className={`v5-btn ${navOrientation === "vertical" ? "primary" : "secondary"} sm`}
|
|
onClick={() => onNavOrientationChange("vertical")}
|
|
>
|
|
<AlignJustify size={12} />
|
|
세로
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`v5-btn ${navOrientation === "horizontal" ? "primary" : "secondary"} sm`}
|
|
onClick={() => onNavOrientationChange("horizontal")}
|
|
>
|
|
<AlignHorizontalJustifyCenter size={12} />
|
|
가로
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* === 사이드바 (세로일 때만 의미있음) === */}
|
|
{onSidebarCollapsedChange && navOrientation !== "horizontal" && (
|
|
<div className="v5-tweaks-row">
|
|
<label>사이드바</label>
|
|
<div className="v5-tweaks-seg">
|
|
<button
|
|
type="button"
|
|
className={`v5-btn ${!sidebarCollapsed ? "primary" : "secondary"} sm`}
|
|
onClick={() => onSidebarCollapsedChange(false)}
|
|
>
|
|
<PanelLeft size={12} />
|
|
펼침
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`v5-btn ${sidebarCollapsed ? "primary" : "secondary"} sm`}
|
|
onClick={() => onSidebarCollapsedChange(true)}
|
|
>
|
|
<PanelLeftClose size={12} />
|
|
접힘
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="v5-tweaks-foot">
|
|
우측 상단 슬라이더 아이콘으로 다시 열기
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|