Files
invyone/frontend/components/layout/SettingsModal.tsx
T
gbpark 5153386fce
Build & Deploy to K8s / build-and-deploy (push) Successful in 3m59s
디자인 수정
2026-04-21 22:59:51 +09:00

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>
);
}