텍스트 스타일 및 프리뷰 렌더링 정리
CI / test (push) Successful in 11m5s
CI / pr_and_merge (push) Successful in 1m40s
CI / test (pull_request) Failing after 35m42s
CI / pr_and_merge (pull_request) Has been skipped

This commit is contained in:
2025-11-20 23:36:47 +09:00
parent 9e40ee405c
commit 2f59e3781e
19 changed files with 3831 additions and 556 deletions
@@ -0,0 +1,290 @@
"use client";
import type { Block, FormInputBlockProps } from "@/features/editor/state/editorStore";
import { ColorPickerField, TEXT_COLOR_PALETTE } from "@/features/editor/components/ColorPickerField";
import { NumericPropertyControl } from "@/features/editor/components/NumericPropertyControl";
interface FormInputPropertiesPanelProps {
block: Block;
selectedBlockId: string | null;
updateBlock: (id: string, partial: any) => void;
}
export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }: FormInputPropertiesPanelProps) {
if (!selectedBlockId || block.id !== selectedBlockId || block.type !== "formInput") return null;
const inputProps = block.props as FormInputBlockProps;
return (
<div className="space-y-3 text-xs">
<h3 className="text-[11px] font-semibold text-slate-200"> </h3>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </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.labelMode ?? "text"}
onChange={(e) =>
updateBlock(selectedBlockId, {
labelMode: e.target.value,
} as any)
}
>
<option value="text"></option>
<option value="image"></option>
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<input
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs outline-none focus:border-sky-500"
value={inputProps.label ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
label: e.target.value,
} as any)
}
/>
</label>
{inputProps.labelMode === "image" && (
<>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> URL</span>
<input
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs outline-none focus:border-sky-500"
value={inputProps.labelImageUrl ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
labelImageUrl: e.target.value,
} as any)
}
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<input
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs outline-none focus:border-sky-500"
value={inputProps.labelImageAlt ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
labelImageAlt: e.target.value,
} as any)
}
/>
</label>
</>
)}
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<input
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs outline-none focus:border-sky-500"
value={inputProps.formFieldName ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
formFieldName: e.target.value,
} as any)
}
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </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"
aria-label="필드 타입"
value={inputProps.inputType ?? "text"}
onChange={(e) =>
updateBlock(selectedBlockId, {
inputType: e.target.value,
} as any)
}
>
<option value="text"></option>
<option value="email"></option>
<option value="textarea"> </option>
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400">Placeholder</span>
<input
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs outline-none focus:border-sky-500"
aria-label="Placeholder"
value={inputProps.placeholder ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
placeholder: e.target.value,
} as any)
}
/>
</label>
<label className="inline-flex items-center gap-2">
<input
type="checkbox"
className="h-3 w-3 rounded border border-slate-600 bg-slate-900"
checked={Boolean(inputProps.required)}
onChange={(e) =>
updateBlock(selectedBlockId, {
required: e.target.checked,
} as any)
}
/>
<span className="text-slate-400"> </span>
</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>
<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={inputProps.align ?? "left"}
onChange={(e) =>
updateBlock(selectedBlockId, {
align: e.target.value,
} as any)
}
>
<option value="left"></option>
<option value="center"></option>
<option value="right"></option>
</select>
</label>
<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={inputProps.labelLayout ?? "stacked"}
onChange={(e) =>
updateBlock(selectedBlockId, {
labelLayout: e.target.value,
} as any)
}
>
<option value="stacked"> ()</option>
<option value="inline"> ()</option>
</select>
</label>
<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={inputProps.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>
{(inputProps.widthMode ?? "full") === "fixed" && (
<NumericPropertyControl
label="필드 고정 너비"
unitLabel="(px)"
value={inputProps.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);
}}
/>
)}
<ColorPickerField
label="필드 텍스트 색상"
ariaLabelColorInput="필드 텍스트 색상 피커"
ariaLabelHexInput="필드 텍스트 색상 HEX"
value={
inputProps.textColorCustom && inputProps.textColorCustom.trim() !== ""
? inputProps.textColorCustom
: "#f9fafb"
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
textColorCustom: hex,
} as any);
}}
palette={TEXT_COLOR_PALETTE}
onPaletteSelect={(item) => {
updateBlock(selectedBlockId, {
textColorCustom: item.color,
} as any);
}}
/>
<ColorPickerField
label="필드 채움 색상"
ariaLabelColorInput="필드 채움 색상 피커"
ariaLabelHexInput="필드 채움 색상 HEX"
value={
inputProps.fillColorCustom && inputProps.fillColorCustom.trim() !== ""
? inputProps.fillColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
fillColorCustom: hex,
} as any);
}}
palette={TEXT_COLOR_PALETTE}
onPaletteSelect={(item) => {
updateBlock(selectedBlockId, {
fillColorCustom: item.color,
} as any);
}}
/>
<ColorPickerField
label="필드 테두리 색상"
ariaLabelColorInput="필드 테두리 색상 피커"
ariaLabelHexInput="필드 테두리 색상 HEX"
value={
inputProps.strokeColorCustom && inputProps.strokeColorCustom.trim() !== ""
? inputProps.strokeColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
strokeColorCustom: hex,
} as any);
}}
palette={TEXT_COLOR_PALETTE}
onPaletteSelect={(item) => {
updateBlock(selectedBlockId, {
strokeColorCustom: item.color,
} as any);
}}
/>
<NumericPropertyControl
label="필드 모서리 둥글기"
value={(() => {
const r = inputProps.borderRadius ?? "md";
return r === "none" ? 0 : r === "sm" ? 2 : r === "lg" ? 6 : r === "full" ? 8 : 4;
})()}
min={0}
max={8}
step={1}
presets={[
{ id: "none", label: "없음", value: 0 },
{ id: "sm", label: "작게", value: 2 },
{ id: "md", label: "보통", value: 4 },
{ id: "lg", label: "크게", value: 6 },
{ id: "full", label: "완전 둥글게", value: 8 },
]}
onChangeValue={(v) => {
const next =
v <= 0 ? "none" : v <= 2 ? "sm" : v <= 5 ? "md" : v <= 7 ? "lg" : "full";
updateBlock(selectedBlockId, {
borderRadius: next,
} as any);
}}
/>
</div>
</div>
);
}