섹션 삭제 후 텍스트 블록 추가 버그 수정 및 버튼/구분선 스타일-속성 패널 동기화
This commit is contained in:
@@ -183,6 +183,175 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
|
||||
|
||||
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3">
|
||||
<h4 className="text-[11px] font-semibold text-slate-200">필드 스타일</h4>
|
||||
{/* px 기반 체크박스 타이포 입력값을 노출하여 em 스케일 변환을 위한 근거를 남긴다 */}
|
||||
<NumericPropertyControl
|
||||
label="체크박스 텍스트 크기 (px)"
|
||||
unitLabel="(px)"
|
||||
value={(() => {
|
||||
const raw = checkboxProps.fontSizeCustom ?? "";
|
||||
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
|
||||
if (match) return Number(match[1]);
|
||||
return 14;
|
||||
})()}
|
||||
min={8}
|
||||
max={40}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "xs", label: "XS", value: 12 },
|
||||
{ id: "sm", label: "S", value: 14 },
|
||||
{ id: "md", label: "M", value: 16 },
|
||||
{ id: "lg", label: "L", value: 18 },
|
||||
{ id: "xl", label: "XL", value: 20 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
fontSizeCustom: `${v}px`,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
{/* 줄간격 px 입력값을 노출해 preview em 변환과 TDD 케이스를 연동한다 */}
|
||||
<NumericPropertyControl
|
||||
label="체크박스 줄간격 (px)"
|
||||
unitLabel="(px)"
|
||||
value={(() => {
|
||||
const raw = checkboxProps.lineHeightCustom ?? "";
|
||||
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
|
||||
if (match) return Number(match[1]);
|
||||
return 20;
|
||||
})()}
|
||||
min={8}
|
||||
max={60}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "tight", label: "타이트", value: 18 },
|
||||
{ id: "normal", label: "보통", value: 24 },
|
||||
{ id: "loose", label: "루즈", value: 32 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
lineHeightCustom: `${v}px`,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
{/* 자간 px 입력을 통한 폼 체크박스 미세 조정값을 노출 */}
|
||||
<NumericPropertyControl
|
||||
label="체크박스 자간 (px)"
|
||||
unitLabel="(px)"
|
||||
value={(() => {
|
||||
const raw = checkboxProps.letterSpacingCustom ?? "";
|
||||
const match = raw.match(/(-?[0-9]+(?:\.[0-9]+)?)/);
|
||||
if (match) return Number(match[1]);
|
||||
return 0;
|
||||
})()}
|
||||
min={-10}
|
||||
max={20}
|
||||
step={0.5}
|
||||
presets={[
|
||||
{ id: "tight", label: "좁게", value: -1 },
|
||||
{ id: "normal", label: "보통", value: 0 },
|
||||
{ id: "wide", label: "넓게", value: 2 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
letterSpacingCustom: `${v}px`,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
|
||||
<span>필드 너비</span>
|
||||
<select
|
||||
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
|
||||
value={checkboxProps.widthMode ?? "full"}
|
||||
onChange={(e) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
widthMode: e.target.value,
|
||||
} as any)
|
||||
}
|
||||
>
|
||||
<option value="auto">자동</option>
|
||||
<option value="full">전체 폭</option>
|
||||
<option value="fixed">고정 값</option>
|
||||
</select>
|
||||
</label>
|
||||
{(checkboxProps.widthMode ?? "full") === "fixed" && (
|
||||
<NumericPropertyControl
|
||||
label="필드 고정 너비"
|
||||
unitLabel="(px)"
|
||||
value={checkboxProps.widthPx ?? 240}
|
||||
min={80}
|
||||
max={800}
|
||||
step={10}
|
||||
presets={[
|
||||
{ id: "sm", label: "작게", value: 200 },
|
||||
{ id: "md", label: "보통", value: 280 },
|
||||
{ id: "lg", label: "넓게", value: 360 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
widthPx: v,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<NumericPropertyControl
|
||||
label="체크박스 가로 패딩 (px)"
|
||||
unitLabel="(px)"
|
||||
value={typeof checkboxProps.paddingX === "number" ? checkboxProps.paddingX : 12}
|
||||
min={0}
|
||||
max={60}
|
||||
step={2}
|
||||
presets={[
|
||||
{ id: "xs", label: "아주 얇게", value: 6 },
|
||||
{ id: "sm", label: "얇게", value: 10 },
|
||||
{ id: "md", label: "보통", value: 12 },
|
||||
{ id: "lg", label: "넓게", value: 16 },
|
||||
{ id: "xl", label: "아주 넓게", value: 20 },
|
||||
]}
|
||||
onChangeValue={(v) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
paddingX: v,
|
||||
} as any)
|
||||
}
|
||||
/>
|
||||
<NumericPropertyControl
|
||||
label="체크박스 세로 패딩 (px)"
|
||||
unitLabel="(px)"
|
||||
value={typeof checkboxProps.paddingY === "number" ? checkboxProps.paddingY : 10}
|
||||
min={0}
|
||||
max={40}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "xs", label: "아주 얇게", value: 6 },
|
||||
{ id: "sm", label: "얇게", value: 8 },
|
||||
{ id: "md", label: "보통", value: 10 },
|
||||
{ id: "lg", label: "넓게", value: 12 },
|
||||
{ id: "xl", label: "아주 넓게", value: 16 },
|
||||
]}
|
||||
onChangeValue={(v) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
paddingY: v,
|
||||
} as any)
|
||||
}
|
||||
/>
|
||||
<NumericPropertyControl
|
||||
label="체크박스 옵션 간격 (px)"
|
||||
unitLabel="(px)"
|
||||
value={typeof checkboxProps.optionGapPx === "number" ? checkboxProps.optionGapPx : 4}
|
||||
min={0}
|
||||
max={40}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "tight", label: "타이트", value: 2 },
|
||||
{ id: "normal", label: "보통", value: 4 },
|
||||
{ id: "relaxed", label: "느슨", value: 8 },
|
||||
{ id: "extra", label: "넓게", value: 12 },
|
||||
]}
|
||||
onChangeValue={(v) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
optionGapPx: v,
|
||||
} as any)
|
||||
}
|
||||
/>
|
||||
<ColorPickerField
|
||||
label="텍스트 색상"
|
||||
ariaLabelColorInput="체크박스 텍스트 색상 피커"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { Block, ButtonBlockProps, FormBlockProps } from "@/features/editor/state/editorStore";
|
||||
import { NumericPropertyControl } from "@/features/editor/components/NumericPropertyControl";
|
||||
|
||||
interface FormControllerPanelProps {
|
||||
block: Block; // type === "form"
|
||||
@@ -129,6 +130,64 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 border-t border-slate-800 pt-3">
|
||||
<h3 className="text-[11px] font-semibold text-slate-200">폼 레이아웃</h3>
|
||||
<label className="flex flex-col gap-1 text-xs text-slate-400">
|
||||
<span>폼 너비 모드</span>
|
||||
<select
|
||||
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
|
||||
value={formProps.formWidthMode ?? "auto"}
|
||||
onChange={(e) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
formWidthMode: e.target.value,
|
||||
} as any)
|
||||
}
|
||||
>
|
||||
<option value="auto">내용에 맞춤</option>
|
||||
<option value="full">전체 폭</option>
|
||||
<option value="fixed">고정 폭 (px)</option>
|
||||
</select>
|
||||
</label>
|
||||
{(formProps.formWidthMode ?? "auto") === "fixed" && (
|
||||
<NumericPropertyControl
|
||||
label="폼 고정 너비 (px)"
|
||||
unitLabel="(px)"
|
||||
value={typeof formProps.formWidthPx === "number" ? formProps.formWidthPx : 360}
|
||||
min={160}
|
||||
max={960}
|
||||
step={10}
|
||||
presets={[
|
||||
{ id: "sm", label: "좁게", value: 280 },
|
||||
{ id: "md", label: "보통", value: 360 },
|
||||
{ id: "lg", label: "넓게", value: 480 },
|
||||
]}
|
||||
onChangeValue={(v) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
formWidthPx: v,
|
||||
} as any)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<NumericPropertyControl
|
||||
label="폼 위/아래 여백 (px)"
|
||||
unitLabel="(px)"
|
||||
value={typeof formProps.marginYPx === "number" ? formProps.marginYPx : 16}
|
||||
min={0}
|
||||
max={80}
|
||||
step={2}
|
||||
presets={[
|
||||
{ id: "tight", label: "좁게", value: 8 },
|
||||
{ id: "normal", label: "보통", value: 16 },
|
||||
{ id: "relaxed", label: "넓게", value: 32 },
|
||||
]}
|
||||
onChangeValue={(v) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
marginYPx: v,
|
||||
} as any)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 border-t border-slate-800 pt-3 mt-4">
|
||||
<h3 className="text-[11px] font-semibold text-slate-200">폼 컨트롤러</h3>
|
||||
|
||||
|
||||
@@ -130,6 +130,79 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
|
||||
</label>
|
||||
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3">
|
||||
<h4 className="text-[11px] font-semibold text-slate-200">필드 스타일</h4>
|
||||
<NumericPropertyControl
|
||||
label="필드 텍스트 크기 (px)"
|
||||
unitLabel="(px)"
|
||||
value={(() => {
|
||||
const raw = inputProps.fontSizeCustom ?? "";
|
||||
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
|
||||
if (match) return Number(match[1]);
|
||||
return 14;
|
||||
})()}
|
||||
min={8}
|
||||
max={40}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "xs", label: "XS", value: 12 },
|
||||
{ id: "sm", label: "S", value: 14 },
|
||||
{ id: "md", label: "M", value: 16 },
|
||||
{ id: "lg", label: "L", value: 18 },
|
||||
{ id: "xl", label: "XL", value: 20 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
fontSizeCustom: `${v}px`,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
{/* 줄간격 px 입력을 노출하여 preview em 변환의 근거를 제공 */}
|
||||
<NumericPropertyControl
|
||||
label="필드 줄간격 (px)"
|
||||
unitLabel="(px)"
|
||||
value={(() => {
|
||||
const raw = inputProps.lineHeightCustom ?? "";
|
||||
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
|
||||
if (match) return Number(match[1]);
|
||||
return 20;
|
||||
})()}
|
||||
min={8}
|
||||
max={60}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "tight", label: "타이트", value: 18 },
|
||||
{ id: "normal", label: "보통", value: 24 },
|
||||
{ id: "loose", label: "루즈", value: 32 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
lineHeightCustom: `${v}px`,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
{/* 자간 px 입력으로 폼 입력 타이포를 세밀하게 조정 */}
|
||||
<NumericPropertyControl
|
||||
label="필드 자간 (px)"
|
||||
unitLabel="(px)"
|
||||
value={(() => {
|
||||
const raw = inputProps.letterSpacingCustom ?? "";
|
||||
const match = raw.match(/(-?[0-9]+(?:\.[0-9]+)?)/);
|
||||
if (match) return Number(match[1]);
|
||||
return 0;
|
||||
})()}
|
||||
min={-10}
|
||||
max={20}
|
||||
step={0.5}
|
||||
presets={[
|
||||
{ id: "tight", label: "좁게", value: -1 },
|
||||
{ id: "normal", label: "보통", value: 0 },
|
||||
{ id: "wide", label: "넓게", value: 2 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
letterSpacingCustom: `${v}px`,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
|
||||
<span>텍스트 정렬</span>
|
||||
<select
|
||||
@@ -161,8 +234,29 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
|
||||
<option value="inline">가로 (인라인)</option>
|
||||
</select>
|
||||
</label>
|
||||
{(inputProps.labelLayout ?? "stacked") === "inline" && (
|
||||
<NumericPropertyControl
|
||||
label="라벨/필드 간격 (px)"
|
||||
unitLabel="(px)"
|
||||
value={typeof inputProps.labelGapPx === "number" ? inputProps.labelGapPx : 8}
|
||||
min={0}
|
||||
max={80}
|
||||
step={2}
|
||||
presets={[
|
||||
{ id: "tight", label: "타이트", value: 4 },
|
||||
{ id: "normal", label: "보통", value: 8 },
|
||||
{ id: "relaxed", label: "느슨", value: 12 },
|
||||
{ id: "extra", label: "넓게", value: 16 },
|
||||
]}
|
||||
onChangeValue={(v) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
labelGapPx: v,
|
||||
} as any)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
|
||||
<span>필드 너비</span>
|
||||
<span>너비</span>
|
||||
<select
|
||||
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
|
||||
value={inputProps.widthMode ?? "full"}
|
||||
@@ -197,6 +291,46 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<NumericPropertyControl
|
||||
label="필드 가로 패딩 (px)"
|
||||
unitLabel="(px)"
|
||||
value={typeof inputProps.paddingX === "number" ? inputProps.paddingX : 12}
|
||||
min={0}
|
||||
max={60}
|
||||
step={2}
|
||||
presets={[
|
||||
{ id: "xs", label: "아주 얇게", value: 6 },
|
||||
{ id: "sm", label: "얇게", value: 10 },
|
||||
{ id: "md", label: "보통", value: 12 },
|
||||
{ id: "lg", label: "넓게", value: 16 },
|
||||
{ id: "xl", label: "아주 넓게", value: 20 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
paddingX: v,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
<NumericPropertyControl
|
||||
label="필드 세로 패딩 (px)"
|
||||
unitLabel="(px)"
|
||||
value={typeof inputProps.paddingY === "number" ? inputProps.paddingY : 10}
|
||||
min={0}
|
||||
max={40}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "xs", label: "아주 얇게", value: 6 },
|
||||
{ id: "sm", label: "얇게", value: 8 },
|
||||
{ id: "md", label: "보통", value: 10 },
|
||||
{ id: "lg", label: "넓게", value: 12 },
|
||||
{ id: "xl", label: "아주 넓게", value: 16 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
paddingY: v,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
<ColorPickerField
|
||||
label="필드 텍스트 색상"
|
||||
ariaLabelColorInput="필드 텍스트 색상 피커"
|
||||
|
||||
@@ -183,6 +183,175 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
|
||||
|
||||
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3">
|
||||
<h4 className="text-[11px] font-semibold text-slate-200">필드 스타일</h4>
|
||||
{/* 라디오 타이포 px 값을 직접 입력받아 em 스케일 변환 로직과 연동한다 */}
|
||||
<NumericPropertyControl
|
||||
label="라디오 텍스트 크기 (px)"
|
||||
unitLabel="(px)"
|
||||
value={(() => {
|
||||
const raw = radioProps.fontSizeCustom ?? "";
|
||||
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
|
||||
if (match) return Number(match[1]);
|
||||
return 14;
|
||||
})()}
|
||||
min={8}
|
||||
max={40}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "xs", label: "XS", value: 12 },
|
||||
{ id: "sm", label: "S", value: 14 },
|
||||
{ id: "md", label: "M", value: 16 },
|
||||
{ id: "lg", label: "L", value: 18 },
|
||||
{ id: "xl", label: "XL", value: 20 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
fontSizeCustom: `${v}px`,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
{/* 줄간격 px 입력을 노출하여 preview 에서 em 변환 근거로 사용 */}
|
||||
<NumericPropertyControl
|
||||
label="라디오 줄간격 (px)"
|
||||
unitLabel="(px)"
|
||||
value={(() => {
|
||||
const raw = radioProps.lineHeightCustom ?? "";
|
||||
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
|
||||
if (match) return Number(match[1]);
|
||||
return 20;
|
||||
})()}
|
||||
min={8}
|
||||
max={60}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "tight", label: "타이트", value: 18 },
|
||||
{ id: "normal", label: "보통", value: 24 },
|
||||
{ id: "loose", label: "루즈", value: 32 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
lineHeightCustom: `${v}px`,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
{/* 자간 px 입력 컨트롤로 프리뷰 적용 근거 제공 */}
|
||||
<NumericPropertyControl
|
||||
label="라디오 자간 (px)"
|
||||
unitLabel="(px)"
|
||||
value={(() => {
|
||||
const raw = radioProps.letterSpacingCustom ?? "";
|
||||
const match = raw.match(/(-?[0-9]+(?:\.[0-9]+)?)/);
|
||||
if (match) return Number(match[1]);
|
||||
return 0;
|
||||
})()}
|
||||
min={-10}
|
||||
max={20}
|
||||
step={0.5}
|
||||
presets={[
|
||||
{ id: "tight", label: "좁게", value: -1 },
|
||||
{ id: "normal", label: "보통", value: 0 },
|
||||
{ id: "wide", label: "넓게", value: 2 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
letterSpacingCustom: `${v}px`,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
|
||||
<span>필드 너비</span>
|
||||
<select
|
||||
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
|
||||
value={radioProps.widthMode ?? "full"}
|
||||
onChange={(e) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
widthMode: e.target.value,
|
||||
} as any)
|
||||
}
|
||||
>
|
||||
<option value="auto">자동</option>
|
||||
<option value="full">전체 폭</option>
|
||||
<option value="fixed">고정 값</option>
|
||||
</select>
|
||||
</label>
|
||||
{(radioProps.widthMode ?? "full") === "fixed" && (
|
||||
<NumericPropertyControl
|
||||
label="필드 고정 너비"
|
||||
unitLabel="(px)"
|
||||
value={radioProps.widthPx ?? 240}
|
||||
min={80}
|
||||
max={800}
|
||||
step={10}
|
||||
presets={[
|
||||
{ id: "sm", label: "작게", value: 200 },
|
||||
{ id: "md", label: "보통", value: 280 },
|
||||
{ id: "lg", label: "넓게", value: 360 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
widthPx: v,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<NumericPropertyControl
|
||||
label="라디오 가로 패딩 (px)"
|
||||
unitLabel="(px)"
|
||||
value={typeof radioProps.paddingX === "number" ? radioProps.paddingX : 12}
|
||||
min={0}
|
||||
max={60}
|
||||
step={2}
|
||||
presets={[
|
||||
{ id: "xs", label: "아주 얇게", value: 6 },
|
||||
{ id: "sm", label: "얇게", value: 10 },
|
||||
{ id: "md", label: "보통", value: 12 },
|
||||
{ id: "lg", label: "넓게", value: 16 },
|
||||
{ id: "xl", label: "아주 넓게", value: 20 },
|
||||
]}
|
||||
onChangeValue={(v) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
paddingX: v,
|
||||
} as any)
|
||||
}
|
||||
/>
|
||||
<NumericPropertyControl
|
||||
label="라디오 세로 패딩 (px)"
|
||||
unitLabel="(px)"
|
||||
value={typeof radioProps.paddingY === "number" ? radioProps.paddingY : 10}
|
||||
min={0}
|
||||
max={40}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "xs", label: "아주 얇게", value: 6 },
|
||||
{ id: "sm", label: "얇게", value: 8 },
|
||||
{ id: "md", label: "보통", value: 10 },
|
||||
{ id: "lg", label: "넓게", value: 12 },
|
||||
{ id: "xl", label: "아주 넓게", value: 16 },
|
||||
]}
|
||||
onChangeValue={(v) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
paddingY: v,
|
||||
} as any)
|
||||
}
|
||||
/>
|
||||
<NumericPropertyControl
|
||||
label="라디오 옵션 간격 (px)"
|
||||
unitLabel="(px)"
|
||||
value={typeof radioProps.optionGapPx === "number" ? radioProps.optionGapPx : 4}
|
||||
min={0}
|
||||
max={40}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "tight", label: "타이트", value: 2 },
|
||||
{ id: "normal", label: "보통", value: 4 },
|
||||
{ id: "relaxed", label: "느슨", value: 8 },
|
||||
{ id: "extra", label: "넓게", value: 12 },
|
||||
]}
|
||||
onChangeValue={(v) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
optionGapPx: v,
|
||||
} as any)
|
||||
}
|
||||
/>
|
||||
<ColorPickerField
|
||||
label="텍스트 색상"
|
||||
ariaLabelColorInput="라디오 텍스트 색상 피커"
|
||||
|
||||
@@ -150,6 +150,155 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
|
||||
</label>
|
||||
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3">
|
||||
<h4 className="text-[11px] font-semibold text-slate-200">필드 스타일</h4>
|
||||
<NumericPropertyControl
|
||||
label="셀렉트 텍스트 크기 (px)"
|
||||
unitLabel="(px)"
|
||||
value={(() => {
|
||||
const raw = selectProps.fontSizeCustom ?? "";
|
||||
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
|
||||
if (match) return Number(match[1]);
|
||||
return 14;
|
||||
})()}
|
||||
min={8}
|
||||
max={40}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "xs", label: "XS", value: 12 },
|
||||
{ id: "sm", label: "S", value: 14 },
|
||||
{ id: "md", label: "M", value: 16 },
|
||||
{ id: "lg", label: "L", value: 18 },
|
||||
{ id: "xl", label: "XL", value: 20 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
fontSizeCustom: `${v}px`,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
{/* 줄간격 px 입력을 받아 preview 렌더러의 em 변환 근거로 사용한다 */}
|
||||
<NumericPropertyControl
|
||||
label="셀렉트 줄간격 (px)"
|
||||
unitLabel="(px)"
|
||||
value={(() => {
|
||||
const raw = selectProps.lineHeightCustom ?? "";
|
||||
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
|
||||
if (match) return Number(match[1]);
|
||||
return 20;
|
||||
})()}
|
||||
min={8}
|
||||
max={60}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "tight", label: "타이트", value: 18 },
|
||||
{ id: "normal", label: "보통", value: 24 },
|
||||
{ id: "loose", label: "루즈", value: 32 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
lineHeightCustom: `${v}px`,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
{/* 자간 px 입력을 제공하여 TDD 시나리오와 동기화한다 */}
|
||||
<NumericPropertyControl
|
||||
label="셀렉트 자간 (px)"
|
||||
unitLabel="(px)"
|
||||
value={(() => {
|
||||
const raw = selectProps.letterSpacingCustom ?? "";
|
||||
const match = raw.match(/(-?[0-9]+(?:\.[0-9]+)?)/);
|
||||
if (match) return Number(match[1]);
|
||||
return 0;
|
||||
})()}
|
||||
min={-10}
|
||||
max={20}
|
||||
step={0.5}
|
||||
presets={[
|
||||
{ id: "tight", label: "좁게", value: -1 },
|
||||
{ id: "normal", label: "보통", value: 0 },
|
||||
{ id: "wide", label: "넓게", value: 2 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
letterSpacingCustom: `${v}px`,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
|
||||
<span>필드 너비</span>
|
||||
<select
|
||||
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
|
||||
value={selectProps.widthMode ?? "full"}
|
||||
onChange={(e) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
widthMode: e.target.value,
|
||||
} as any)
|
||||
}
|
||||
>
|
||||
<option value="auto">자동</option>
|
||||
<option value="full">전체 폭</option>
|
||||
<option value="fixed">고정 값</option>
|
||||
</select>
|
||||
</label>
|
||||
{(selectProps.widthMode ?? "full") === "fixed" && (
|
||||
<NumericPropertyControl
|
||||
label="필드 고정 너비"
|
||||
unitLabel="(px)"
|
||||
value={selectProps.widthPx ?? 240}
|
||||
min={80}
|
||||
max={800}
|
||||
step={10}
|
||||
presets={[
|
||||
{ id: "sm", label: "작게", value: 200 },
|
||||
{ id: "md", label: "보통", value: 280 },
|
||||
{ id: "lg", label: "넓게", value: 360 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
widthPx: v,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<NumericPropertyControl
|
||||
label="셀렉트 가로 패딩 (px)"
|
||||
unitLabel="(px)"
|
||||
value={typeof selectProps.paddingX === "number" ? selectProps.paddingX : 12}
|
||||
min={0}
|
||||
max={60}
|
||||
step={2}
|
||||
presets={[
|
||||
{ id: "xs", label: "아주 얇게", value: 6 },
|
||||
{ id: "sm", label: "얇게", value: 10 },
|
||||
{ id: "md", label: "보통", value: 12 },
|
||||
{ id: "lg", label: "넓게", value: 16 },
|
||||
{ id: "xl", label: "아주 넓게", value: 20 },
|
||||
]}
|
||||
onChangeValue={(v) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
paddingX: v,
|
||||
} as any)
|
||||
}
|
||||
/>
|
||||
<NumericPropertyControl
|
||||
label="셀렉트 세로 패딩 (px)"
|
||||
unitLabel="(px)"
|
||||
value={typeof selectProps.paddingY === "number" ? selectProps.paddingY : 10}
|
||||
min={0}
|
||||
max={40}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "xs", label: "아주 얇게", value: 6 },
|
||||
{ id: "sm", label: "얇게", value: 8 },
|
||||
{ id: "md", label: "보통", value: 10 },
|
||||
{ id: "lg", label: "넓게", value: 12 },
|
||||
{ id: "xl", label: "아주 넓게", value: 16 },
|
||||
]}
|
||||
onChangeValue={(v) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
paddingY: v,
|
||||
} as any)
|
||||
}
|
||||
/>
|
||||
<ColorPickerField
|
||||
label="필드 텍스트 색상"
|
||||
ariaLabelColorInput="셀렉트 텍스트 색상 피커"
|
||||
|
||||
Reference in New Issue
Block a user