98 lines
2.8 KiB
TypeScript
98 lines
2.8 KiB
TypeScript
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-500 dark: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-300 bg-white px-2 py-1 text-[11px] text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
|
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>
|
|
);
|
|
}
|