diff --git a/src/app/editor/forms/FormCheckboxPropertiesPanel.tsx b/src/app/editor/forms/FormCheckboxPropertiesPanel.tsx index 0cf056b..1903a46 100644 --- a/src/app/editor/forms/FormCheckboxPropertiesPanel.tsx +++ b/src/app/editor/forms/FormCheckboxPropertiesPanel.tsx @@ -183,6 +183,175 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc

필드 스타일

+ {/* px 기반 체크박스 타이포 입력값을 노출하여 em 스케일 변환을 위한 근거를 남긴다 */} + { + 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 케이스를 연동한다 */} + { + 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 입력을 통한 폼 체크박스 미세 조정값을 노출 */} + { + 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); + }} + /> + + {(checkboxProps.widthMode ?? "full") === "fixed" && ( + { + updateBlock(selectedBlockId, { + widthPx: v, + } as any); + }} + /> + )} + + updateBlock(selectedBlockId, { + paddingX: v, + } as any) + } + /> + + updateBlock(selectedBlockId, { + paddingY: v, + } as any) + } + /> + + updateBlock(selectedBlockId, { + optionGapPx: v, + } as any) + } + /> +
+

폼 레이아웃

+ + {(formProps.formWidthMode ?? "auto") === "fixed" && ( + + updateBlock(selectedBlockId, { + formWidthPx: v, + } as any) + } + /> + )} + + updateBlock(selectedBlockId, { + marginYPx: v, + } as any) + } + /> +
+

폼 컨트롤러

diff --git a/src/app/editor/forms/FormInputPropertiesPanel.tsx b/src/app/editor/forms/FormInputPropertiesPanel.tsx index cd4a670..e6861b0 100644 --- a/src/app/editor/forms/FormInputPropertiesPanel.tsx +++ b/src/app/editor/forms/FormInputPropertiesPanel.tsx @@ -130,6 +130,79 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }

필드 스타일

+ { + 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 변환의 근거를 제공 */} + { + 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 입력으로 폼 입력 타이포를 세밀하게 조정 */} + { + 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); + }} + /> + {(inputProps.labelLayout ?? "stacked") === "inline" && ( + + updateBlock(selectedBlockId, { + labelGapPx: v, + } as any) + } + /> + )} + {(radioProps.widthMode ?? "full") === "fixed" && ( + { + updateBlock(selectedBlockId, { + widthPx: v, + } as any); + }} + /> + )} + + updateBlock(selectedBlockId, { + paddingX: v, + } as any) + } + /> + + updateBlock(selectedBlockId, { + paddingY: v, + } as any) + } + /> + + updateBlock(selectedBlockId, { + optionGapPx: v, + } as any) + } + />

필드 스타일

+ { + 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 변환 근거로 사용한다 */} + { + 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 시나리오와 동기화한다 */} + { + 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); + }} + /> + + {(selectProps.widthMode ?? "full") === "fixed" && ( + { + updateBlock(selectedBlockId, { + widthPx: v, + } as any); + }} + /> + )} + + updateBlock(selectedBlockId, { + paddingX: v, + } as any) + } + /> + + updateBlock(selectedBlockId, { + paddingY: v, + } as any) + } + /> { + if (block.type === "text") { + // 더블클릭 시 텍스트 블록 인라인 편집 모드로 진입한다. + event.stopPropagation(); + startEditing(block.id, (block.props as TextBlockProps).text); + } + }} >
- -

- Text / Image / Button / Divider / List / Section 블록을 이 영역에서 추가할 수 있습니다. -

); } diff --git a/src/app/editor/panels/ButtonPropertiesPanel.tsx b/src/app/editor/panels/ButtonPropertiesPanel.tsx index 5ca8b9e..b84b6f4 100644 --- a/src/app/editor/panels/ButtonPropertiesPanel.tsx +++ b/src/app/editor/panels/ButtonPropertiesPanel.tsx @@ -26,6 +26,50 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc />
+
+ { + updateBlock(selectedBlockId, { + paddingX: v, + } as any); + }} + /> +
+
+ { + updateBlock(selectedBlockId, { + paddingY: v, + } as any); + }} + /> +
- { - const s = buttonProps.size ?? "md"; - return s === "xs" ? 1 : s === "sm" ? 2 : s === "lg" ? 4 : s === "xl" ? 5 : 3; - })()} - min={1} - max={5} - step={1} - presets={[ - { id: "xs", label: "아주 작게", value: 1 }, - { id: "sm", label: "작게", value: 2 }, - { id: "md", label: "보통", value: 3 }, - { id: "lg", label: "크게", value: 4 }, - { id: "xl", label: "아주 크게", value: 5 }, - ]} - onChangeValue={(v) => { - const next = v <= 1 ? "xs" : v === 2 ? "sm" : v === 4 ? "lg" : v >= 5 ? "xl" : "md"; - updateBlock(selectedBlockId, { size: next } as any); - }} - /> + {/* 버튼 크기 스케일 컨트롤은 paddingX/paddingY 및 폰트 크기(px) 컨트롤로 충분하므로 제거했다. */}
+
+ + {(buttonProps.widthMode ?? (buttonProps.fullWidth ? "full" : "auto")) === "fixed" && ( + { + updateBlock(selectedBlockId, { + widthPx: v, + } as any); + }} + /> + )} +
{ const raw = buttonProps.fontSizeCustom ?? ""; diff --git a/src/app/editor/panels/ListPropertiesPanel.tsx b/src/app/editor/panels/ListPropertiesPanel.tsx index fb083ce..13f5a16 100644 --- a/src/app/editor/panels/ListPropertiesPanel.tsx +++ b/src/app/editor/panels/ListPropertiesPanel.tsx @@ -105,7 +105,7 @@ export function ListPropertiesPanel({ {/* 글자 크기 */} { const raw = listProps.fontSizeCustom ?? ""; @@ -204,7 +204,7 @@ export function ListPropertiesPanel({ {/* 아이템 간 여백 (슬라이더) */} { if (typeof listProps.gapYPx === "number") { diff --git a/src/app/editor/panels/PropertiesSidebar.tsx b/src/app/editor/panels/PropertiesSidebar.tsx index dbebceb..ff7df0b 100644 --- a/src/app/editor/panels/PropertiesSidebar.tsx +++ b/src/app/editor/panels/PropertiesSidebar.tsx @@ -48,6 +48,7 @@ export function PropertiesSidebar(props: PropertiesSidebarProps) { return (