에디터 인라인/속성 패널 리팩터링 및 폼 전송 기본 기능 추가
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import type { ChangeEvent } from "react";
|
||||
import { PropertySliderField } from "@/features/editor/components/PropertySliderField";
|
||||
|
||||
export type NumericPresetOption = {
|
||||
// 프리셋 식별자 (예: "tight", "normal")
|
||||
id: string;
|
||||
// UI 에 표시할 라벨
|
||||
label: string;
|
||||
// UI 기준 값 (px, 배율 등)
|
||||
value: number;
|
||||
};
|
||||
|
||||
export type NumericPropertyControlProps = {
|
||||
// 필드 라벨 (예: 글자 크기, 줄 간격)
|
||||
label: string;
|
||||
// 단위 설명 (예: px, 배율 등)
|
||||
unitLabel?: string;
|
||||
// 현재 UI 값
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
// 프리셋 목록 (없으면 select 를 렌더링하지 않음)
|
||||
presets?: NumericPresetOption[];
|
||||
// 값 변경 시 호출되는 콜백 (UI 값 기준)
|
||||
onChangeValue: (value: number) => void;
|
||||
};
|
||||
|
||||
export function NumericPropertyControl({
|
||||
label,
|
||||
unitLabel,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
presets,
|
||||
onChangeValue,
|
||||
}: NumericPropertyControlProps) {
|
||||
// 현재 값에 가장 가까운 프리셋 ID 를 계산한다.
|
||||
const currentPresetId = (() => {
|
||||
if (!presets || presets.length === 0) return "";
|
||||
let bestId = presets[0].id;
|
||||
let bestDist = Math.abs(presets[0].value - value);
|
||||
for (let i = 1; i < presets.length; i += 1) {
|
||||
const dist = Math.abs(presets[i].value - value);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
bestId = presets[i].id;
|
||||
}
|
||||
}
|
||||
return bestId;
|
||||
})();
|
||||
|
||||
const handlePresetChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
if (!presets || presets.length === 0) return;
|
||||
const id = event.target.value;
|
||||
const found = presets.find((p) => p.id === id);
|
||||
if (!found) return;
|
||||
onChangeValue(found.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<label className="flex flex-col gap-1 text-xs text-slate-400">
|
||||
<span>
|
||||
{label}
|
||||
{unitLabel ? ` ${unitLabel}` : ""}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{presets && presets.length > 0 ? (
|
||||
<select
|
||||
className="w-32 rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
|
||||
aria-label={`${label} 프리셋`}
|
||||
value={currentPresetId}
|
||||
onChange={handlePresetChange}
|
||||
>
|
||||
{presets.map((preset) => (
|
||||
<option key={preset.id} value={preset.id}>
|
||||
{preset.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : null}
|
||||
|
||||
<PropertySliderField
|
||||
label=""
|
||||
ariaLabelSlider={`${label} 슬라이더`}
|
||||
ariaLabelInput={`${label} 커스텀${unitLabel ? ` ${unitLabel}` : ""}`}
|
||||
value={Number.isFinite(value) ? value : 0}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
onChange={onChangeValue}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user